出现的背景
总结来说是希望不同分组的任务在高负载下能分配可控比例的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
#endif
struct 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_SCHED
if (!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);
else
curr = 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运行时间超过分配限额时间就需要重新调度。