vruntime是CFS的核心,到目前为止,我们已经详细探讨了vruntime的概念与用途,这一节我们深入代码,看一下调度器是如何计算与调整任务的vruntime的。
对于当前正在执行的任务,调度器每过一段时间就需要将该任务使用过的墙上时间计算成vruntime然后累计到 sched_entity->vruntime
中,具体的算法我们在前面已经讨论过了,内核通过函数 calc_delta_fair
来完成这项工作:
/* file: kernel/sched/fair.c */
/* 参数delta代表墙上时间 */
static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se) {
/* 如果se的权重不等于NICE_0_LOAD,则需要根据其权重计算出vruntime */
if (unlikely(se->load.weight != NICE_0_LOAD))
delta = __calc_delta(delta, NICE_0_LOAD, &se->load);
/* 如果se的权重等于NICE_0_LOAD, 则该se的vruntime就是墙上时间,即返回delta即可
*/
return delta;
}
函数 __calc_delta
就是算法 vruntime = (wall_time * ((NICE_0_LOAD * 2^32) / weight)) >> 32
的具体实现,由于 wall_time
, NICE_0_LOAD
与 weight
都是通过参数传递进去的,所以准确地说,该函数的结果就是后两个参数的比值乘以第一个参数,调度器在为每个任务计算一个调度周期内的时间配额时还会用到该函数。
除了为运行的任务计算vruntime之外,调度器还需要调整新创建的任务及久睡方醒的任务的vruntime, 否则他们的vruntime将远远落后于一直在执行的任务,从而导致长久的霸占CPU. 该任务通过函数 place_entity
完成:
/* file: kernel/sched/fair.c */
static void place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se,
int initial) {
/* 以队列的min_vruntime为基础进行调整 */
u64 vruntime = cfs_rq->min_vruntime;
/* 对新创建的进程(initial=1)适当的惩罚,为其加上一定的 vruntime,
* 函数sched_vslice将任务在一个调度周期内应当分配到的墙上时间换算成虚拟时间,调度周期会在下一节介绍
*/
if (initial && sched_feat(START_DEBIT))
vruntime += sched_vslice(cfs_rq, se);
if (!initial) {
/* 如果进程睡眠了很久,那么其 vruntime 可能远远小于队列中其他任务的vruntime,
* 我们也需要对其vruntime
* 进行惩罚,但进程被唤醒(initial=0)说明它所等待的事件已经得到了满足,需要马上干活,所以这里减去一定的vruntime作为补偿。*/
unsigned long thresh = sysctl_sched_latency;
if (sched_feat(GENTLE_FAIR_SLEEPERS))
thresh >>= 1;
vruntime -= thresh;
}
/* 确保不要因为 vruntime -=thresh 导致 se.vruntime 的值越来越小了 */
se->vruntime = max_vruntime(se->vruntime, vruntime);
}
该函数总是为目标任务加上一定的vruntime, 因此事实上是一个“惩罚函数”。
接下来我们再来看一下系统如何更新任务的vruntime以及其他的时间信息的,完成该任务的是函数 update_curr()
:
/* file: kernel/sched/fair.c */
static void update_curr(struct cfs_rq *cfs_rq) {
struct sched_entity *curr = cfs_rq->curr;
/* 获取当前时间 */
u64 now = rq_clock_task(rq_of(cfs_rq));
u64 delta_exec;
if (unlikely(!curr))
return;
/* 本次更新vruntime与上次更新vruntime之间的时间差,即任务本次的运行时间,该值为墙上时间
*/
delta_exec = now - curr->exec_start;
if (unlikely((s64)delta_exec <= 0))
return;
/* 记录这次更新的时间 */
curr->exec_start = now;
/* 更新总的运行时间 */
curr->sum_exec_runtime += delta_exec;
/* 更新vruntime, 通过函数calc_delta_fair计算出墙上时间delta_exec对应的虚拟时间
*/
curr->vruntime += calc_delta_fair(delta_exec, curr);
/* 更新队列的 min_vruntime */
update_min_vruntime(cfs_rq);
/* 下面的逻辑可以暂时不管 */
if (entity_is_task(curr)) {
struct task_struct *curtask = task_of(curr);
trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
cgroup_account_cputime(curtask, delta_exec);
account_group_exec_runtime(curtask, delta_exec);
}
account_cfs_rq_runtime(cfs_rq, delta_exec);
}
任务的vruntime就靠函数 update_curr
来维护,系统在很多情况下都会调用该方法,包括任务在入队、出队时,调度中断函数也会周期性地调用该方法,以确保任务的各种时间信息随时都是最新的状态。