Linux内核源码分析-进程调度(五)-组调度

news/2024/10/30 23:24:04/

出现的背景

总结来说是希望不同分组的任务在高负载下能分配可控比例的CPU资源。为什么会有这个需求呢,假设多用户计算机系统每个用户的所有任务划分到一个分组中,A用户90个任务,而B用户只有10个任务(这100个任务假设都是优先级一样的普通进程),在CPU完全跑满的情况下,那么A用户将占90%的CPU时间,而B用户只占到了10%的CPU时间,这对B用户显然是不公平的。再或者同一个用户,既想-j64快速编译,又不想被编译任务影响使用体验,也可将编译任务设置到对应分组中,限制其CPU资源。组调度的引入,正是解决此问题的,即可以将任务分配给不同的任务组来实现CPU资源的合理利用。

我们举个例子来进一步阐述一下上面这段话。现在的计算机都支持多用户登录,如果一台计算机被两个用户A和B同时使用,假设用户A运行8个进程,用户B运行2个进程,按照之前对CFS的理解,CFS的调度粒度都是一个个的进程,我们认为用户A获得80%的cpu时间,用户B获得20%的cpu时间。随着用户A不停的增加运行进程数量,用户B可使用的CPU时间越来越少,这就完全不符合我们的预期了。因此,我们引入了组调度(Group Scheduling)的概念。我们将一个用户的任务放在同一个任务组中,这样用户A和用户B各获得50%cpu时间。用户A中的每个进程获得6.25%(50% / 8)cpu时间,用户B的每个进程获得25%(50% / 2)cpu时间,这样的结果是符合我们预期的。

task group

前面我们说过,CFS调度器管理的是调度实体。每一个进程通过task_struct描述,task_struct对应一个调度实体sched_entity。针对task_struct对应的调度实体,我们称之为task se。现在我们引入任务组的概念,我们使用task_group描述一个任务组,这个组管理组内所有的进程。因为CFS就绪队列管理的单位是调度实体,因此,task_group也脱离不了sched_entity,即task_group也映射为一个调度实体,我们称这种调度实体为group se。

/* Task group related information */
/** 组调度,Linux支持将任务分组来对CPU资源进行分配管理。* 该结构中为系统中的每个CPU都分配了struct sched_entity调度实体和struct cfs_rq运行队列,* 其中struct sched_entity用于参与CFS的调度。*/
struct task_group {struct cgroup_subsys_state css;#ifdef CONFIG_FAIR_GROUP_SCHED/* schedulable entities of this group on each CPU *//** 所以se代表cpu数量个group se。* 在alloc_fair_sched_group()中进程初始化及分配内存。*/struct sched_entity	**se;/* runqueue "owned" by this group on each CPU */struct cfs_rq		**cfs_rq;  // 每一个se[cpu]对应一个group cfs_rq。unsigned long		shares;  // 整个调度组的权重。#ifdef	CONFIG_SMP/** load_avg can be heavily contended at clock tick time, so put* it in its own cacheline separated from the fields above which* will also be accessed at each tick.*/atomic_long_t		load_avg ____cacheline_aligned;  // 整个tg的负载贡献总和。
#endif
#endifstruct cfs_bandwidth	cfs_bandwidth;  // cpu带宽控制
};
struct task_group root_task_group;

task group与cpu rq的cfs_rq的关系

在这里插入图片描述
现在我们来详细说一下这张图:
系统启动后默认有一个root_task_group,管理系统中最顶层CFS就绪队列cfs_rq(即cpu rq对应的CFS就绪队列),对应上图即为root tg的cfs_rq[cpu1]和cfs_rq[cpu2]。
cpu2 rq的cfs_rq队列包含9个task se和一个group se,此group se又包含9个task se,且此group se会对应一个task group,即有几层group se,就会有几层tg(不包含root tg)。
tg的parent指向了上一级tg,se维护了cpu数量的group se,cfs_rq[cpu]维护了对应的group se下属的se。
cfs_rq的nr_running:就绪队列上调度实体的个数(包括task se和group se,比如上图中的cpu rq的cfs_rq的task se个数为9,group se个数为1,则nr_running为10)。
cfs_rq的h_nr_running:就绪队列上真实的调度实体的个数(比如上图中的cpu rq的cfs_rq的task se个数为9,group se个数为1(group se中包含9个task se,则),则h_nr_running为18)。

组进程调度

组内的进程该如何调度呢?通过上面的分析,我们可以通过cpu rq的CFS就绪队列(也称根就绪队列)一层层往下遍历选择合适进程。例如,先从根就绪队列选择适合运行的group se,然后找到对应的group cfs_rq,再从group cfs_rq上选择task se。在CFS调度类中,选择进程的函数是pick_next_task_fair()。

struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{struct cfs_rq *cfs_rq = &rq->cfs;  // 从per-cpu rq中获取cfs就绪队列struct sched_entity *se;struct task_struct *p;int new_tasks;again:if (!sched_fair_runnable(rq))  // 判断per-cpu rq的cfs就绪队列上是否还有调度实体,若无则选择idle调度器。goto idle;#ifdef CONFIG_FAIR_GROUP_SCHEDif (!prev || prev->sched_class != &fair_sched_class)goto simple;/** Because of the set_next_buddy() in dequeue_task_fair() it is rather* likely that a next task is from the same cgroup as the current.** Therefore attempt to avoid putting and setting the entire cgroup* hierarchy, only change the part that actually changes.*/do {struct sched_entity *curr = cfs_rq->curr;  // 获取当前正在cpu上运行的调度实体。/** Since we got here without doing put_prev_entity() we also* have to consider cfs_rq->curr. If it is still a runnable* entity, update_curr() will update its vruntime, otherwise* forget we've ever seen it.*/if (curr) {if (curr->on_rq)update_curr(cfs_rq);elsecurr = NULL;/** This call to check_cfs_rq_runtime() will do the* throttle and dequeue its entity in the parent(s).* Therefore the nr_running test will indeed* be correct.*/if (unlikely(check_cfs_rq_runtime(cfs_rq))) {cfs_rq = &rq->cfs;if (!cfs_rq->nr_running)goto idle;goto simple;}}se = pick_next_entity(cfs_rq, curr);  // 从rq的cfs队列中选取虚拟运行时间最小的调度实体。cfs_rq = group_cfs_rq(se);  // 返回se->my_q成员(即获取se的就绪队列)。如果是task se,则返回NULL;如果是group se,则返回group se中的csf_rq就绪队列。} while (cfs_rq);  // 如果是group se,则需要继续在group se中的cfs_rq上选择虚拟运行时间最小的se,直到找到可以最小虚拟运行时间的task se。p = task_of(se);  // 获取调度实体se对应的进程p。return p;
}

组进程抢占

组进程抢占即为周期性调度,会调用task_tick_fair()函数。

/** 周期性调度*/
static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
{struct cfs_rq *cfs_rq;struct sched_entity *se = &curr->se;  // for_each_sched_entity(se) {  // for_each_sched_entity(se)是一个宏定义for (; se; se = se->parent),顺着se的parent链表往上走。更新子调度实体的同时必须更新父调度实体cfs_rq = cfs_rq_of(se);  // 获取se所属的cfs_rq;entity_tick(cfs_rq, se, queued);  // 完成周期性调度}
}

entity_tick的函数实现可以参考前面的文章。
entity_tick()函数继续调用check_preempt_tick()函数,这部分在之前的文章已经说过了。check_preempt_tick()函数会根据满足抢占当前进程的条件下设置TIF_NEED_RESCHED标志位。满足抢占条件也很简单,只要顺着se->parent这条链表便利下去,如果有一个se运行时间超过分配限额时间就需要重新调度。


http://www.ppmy.cn/news/78453.html

相关文章

JDK8以后接口的新特性

JDK8以前,接口内只能定义抽象方法; JDK8,接口内允许定义默认方法、静态方法; JDK9,接口内允许定义私有方法 default:默认方法 public interface Essay01 {/*** 在接口内定义默认方法*/public default v…

SSM编程---Day 01

目录 一、Maven简介 (一)软件开发中的阶段 (二)Maven能做什么 (三)没有使用maven怎么管理依赖 (四)什么是maven (五)maven中的概念 二、Maven的核心概…

移动应用架构解析:用户界面层、业务逻辑层、数据访问层

移动应用的成功离不开一个良好的架构设计,在移动应用开发过程中,合理的层次结构对于应用的可维护性、可扩展性和可测试性至关重要。 移动应用的常见层次结构包括用户界面层、业务逻辑层、数据访问层,但是随着跨平台开发框架的不断发展&#…

媒体传输协议的演进与未来

音视频应用近年来呈现出迅猛的发展趋势,成为互联网流量的主要载体,其玩法丰富,形态多样,众多繁杂的媒体传输协议也应运而生。LiveVideoStackCon 2022北京站邀请到快手传输算法负责人周超,结合快手在媒体传输上的优化与…

算法分析基础

问题:如何比较不同算法的性能? 分析算法的运行时间 算法分析的原则 归纳基本操作 如:运算、赋值、比较 统一机器性能 假设基本操作代价均为1 统一机器性能后,算法运行时间依赖于问题输入规模与实例 相同输入规模&#xff0c…

金领冠520解密母乳源代码,助推婴配粉中国式现代化高速发展

又是一年520,又是一个“全国母乳喂养宣传日”。 1990年5月10日,为保护、促进和支持母乳喂养,更好地实行优生优育,原中华人民共和国国家卫生部召开新闻发布会,确立每年5月20日为“全国母乳喂养宣传日”。 那时&#x…

Jenkins发送邮件、定时执行、持续部署

集成Allure报告只需要配置构建后操作即可。但如果是web自动化,或是用HTMLTestRunner生成报告,构建后操作要选择Publish HTML reports,而构建中还要添加Execute system Groovy script插件,内容: System.setProperty(&q…

effective c++ 11 operator= 处理自我赋值

effective c 11 operator 处理自我赋值 我们知道复制构造函数和赋值运算符的区别是赋值构造函数用于创建一个新的对象,而赋值运算符用于给一个已经存在的对象重新赋值。 因此赋值运算符就可能存在把自己赋值给自己的情况,本节就是专门讨论这个场景的。…