一、概述
首先简单介绍一下基本的设计思路,
CFS思路很简单,就是根据各个进程的权重分配运行时间(权重怎么来的后面再说)。
进程的运行时间计算公式为:
分配给进程的运行时间 = 调度周期 * 进程权重 / 所有进程权重之和 (公式1)
调度周期很好理解,就是将所有处于TASK_RUNNING态进程都调度一遍的时间,
差不多相当于O(1)调度算法中运行队列和过期队列切换一次的时间
(我对O(1)调度算法看得不是很熟,如有错误还望各位大虾指出)。
举个例子,比如只有两个进程A, B,权重分别为1和2,
调度周期设为30ms,那么分配给A的CPU时间为
30ms * (1/(1+2)) = 10ms
而B的CPU时间为
30ms * (2/(1+2)) = 20ms
那么在这30ms中A将运行10ms,B将运行20ms。
公平怎么体现呢?它们的运行时间并不一样阿?
其实公平是体现在另外一个量上面,叫做virtual runtime(vruntime),它记录着进程已经运行的时间,
但是并不是直接记录,而是要根据进程的权重将运行时间放大或者缩小一个比例。
我们来看下从实际运行时间到vruntime的换算公式
vruntime = 实际运行时间 * 1024 / 进程权重。 (公式2)
为了不把大家搞晕,这里我直接写1024,实际上它等于nice为0的进程的权重,代码中是NICE_0_LOAD。
也就是说,所有进程都以nice为0的进程的权重1024作为基准,计算自己的vruntime增加速度。
还以上面AB两个进程为例,B的权重是A的2倍,那么B的vruntime增加速度只有A的一半。
现在我们把公式2中的实际运行时间用公式1来替换,可以得到这么一个结果:
vruntime = (调度周期 * 进程权重 / 所有进程总权重) * 1024 / 进程权重=调度周期 * 1024 / 所有进程总权重
看出什么眉目没有?没错,虽然进程的权重不同,但是它们的vruntime增长速度应该是一样的(这里所说的增长速度一样,是从宏观上来看的,从上一篇文章可以看出来,而在上一篇文章中说vruntime的增量不同,是从公式分析得到的,算是局部分析,在公式2中,如果实际运行时间都是一样,很显然权重小的增长的多,权重大的增长的小,我个人觉得正是虚拟时钟的存在,转换了思想,才有了这个CFS,其实还是根据权重来决定一个进程在一个调用周期内运行了多长时间,但是虚拟时钟决定了怎么调度这个过程,这就是思想),与权重无关。
好,既然所有进程的vruntime增长速度宏观上看应该是同时推进的,
那么就可以用这个vruntime来选择运行的进程,谁的vruntime值较小就说明它以前占用cpu的时间较短,
受到了“不公平”对待,因此下一个运行进程就是它。这样既能公平选择进程,又能保证高优先级进程
获得较多的运行时间。
这就是CFS的主要思想了。
再补充一下权重的来源,权重跟进程nice值之间有一一对应的关系,可以通过全局数组prio_to_weight来转换,
nice值越大,权重越低
下面来分析代码。网上已经有很多cfs的文章,因此我打算换一个方式来写,选择几个点来进行情景分析,
包括进程创建时,进程被唤醒,主动调度(schedule),时钟中断。
介绍代码之前先介绍一下CFS相关的结构
第一个是调度实体sched_entity,它代表一个调度单位,在组调度关闭的时候可以把他等同为进程。
每一个task_struct中都有一个sched_entity,进程的vruntime和权重都保存在这个结构中。
那么所有的sched_entity怎么组织在一起呢?红黑树。所有的sched_entity以vruntime为key
(实际上是以vruntime-min_vruntime为单位,难道是防止溢出?反正结果是一样的)插入到红黑树中,
同时缓存树的最左侧节点,也就是vruntime最小的节点,这样可以迅速选中vruntime最小的进程。
注意只有等待CPU的就绪态进程在这棵树上,睡眠进程和正在运行的进程都不在树上。
我从ibm developer works上偷过来一张图来展示一下它们的关系:
汗,图片上传功能被关闭了,先盗链一个过来,别怪我没品哈。。。
现在开始分情景解析CFS。
二、创建进程
第一个情景选为进程创建时CFS相关变量的初始化。
我们知道,Linux创建进程使用fork或者clone或者vfork等系统调用,最终都会到do_fork。
如果没有设置CLONE_STOPPED,则会进入wake_up_new_task函数,我们看看这个函数的关键部分
- /*
- * wake_up_new_task - wake up a newly created task for the first time.
- *
- * This function will do some initial scheduler statistics housekeeping
- * that must be done for every newly created context, then puts the task
- * on the runqueue and wakes it.
- */
- void wake_up_new_task(struct task_struct *p, unsigned long clone_flags)
- {
- .....
- if (!p->sched_class->task_new || !current->se.on_rq) {
- activate_task(rq, p, 0);
- } else {
- /*
- * Let the scheduling class do new task startup
- * management (if any):
- */
- p->sched_class->task_new(rq, p);
- inc_nr_running(rq);
- }
- check_preempt_curr(rq, p, 0);
- .....
- }
判断为真的情况只有2次(我毫无根据的猜测是idle进程和init进程),而判断为假的情况有近万次。
因此我们只看下面的分支,如果哪位前辈知道真相的话还望告诉我一声,万分感谢。
再下面就是检测是否能够形成抢占,如果新进程能够抢占当前进程则进行进程切换。
我们一个一个函数来看
p->sched_class->task_new对应的函数是task_new_fair:
- /*
- * Share the fairness runtime between parent and child, thus the
- * total amount of pressure for CPU stays equal - new tasks
- * get a chance to run but frequent forkers are not allowed to
- * monopolize the CPU. Note: the parent runqueue is locked,
- * the child is not running yet.
- */
- static void task_new_fair(struct rq *rq, struct task_struct *p)
- {
- struct cfs_rq *cfs_rq = task_cfs_rq(p);
- struct sched_entity *se = &p->se, *curr = cfs_rq->curr;
- int this_cpu = smp_processor_id();
- sched_info_queued(p);
- update_curr(cfs_rq);
- place_entity(cfs_rq, se, 1);
- /* 'curr' will be NULL if the child belongs to a different group */
- if (sysctl_sched_child_runs_first && this_cpu == task_cpu(p) &&
- curr && curr->vruntime < se->vruntime) {
- /*
- * Upon rescheduling, sched_class::put_prev_task() will place
- * 'current' within the tree based on its new key value.
- */
- swap(curr->vruntime, se->vruntime);
- resched_task(rq->curr);
- }
- enqueue_task_fair(rq, p, 0);
- }
其中update_curr在这里可以忽略,它是更新进程的一些随时间变化的信息,我们放到后面再看,
place_entity是更新新进程的vruntime值,以便把他插入红黑树。
新进程的vruntime确定之后有一个判断,满足以下几个条件时,交换父子进程的vruntime:
1.sysctl设置了子进程优先运行
2.fork出的子进程与父进程在同一个cpu上
3.父进程不为空(这个条件为什么会发生暂不明白,难道是fork第一个进程的时候?)
4.父进程的vruntime小于子进程的vruntime
几个条件都还比较好理解,说下第四个,因为CFS总是选择vruntime最小的进程运行,
因此必须保证子进程vruntime比父进程小,作者没有直接把子进程的vruntime设置为较小的值,
而是采用交换的方法,可以防止通过fork新进程来大量占用cpu时间,马上还要讲到。
最后,调用enqueue_task_fair将新进程插入CFS红黑树中
下面我们看下place_entity是怎么计算新进程的vruntime的。
- static void
- place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
- {
- u64 vruntime = cfs_rq->min_vruntime;
- /*
- * The 'current' period is already promised to the current tasks,
- * however the extra weight of the new task will slow them down a
- * little, place the new task so that it fits in the slot that
- * stays open at the end.
- */
- if (initial && sched_feat(START_DEBIT))
- vruntime += sched_vslice(cfs_rq, se);
- if (!initial) {
- //先不看这里,
- }
- se->vruntime = vruntime;
- }
这里是计算进程的初始vruntime。
它以cfs队列的min_vruntime为基准