前言
在《Linux内核对比学习系列(3)——进程调度》一文中,笔者对 Linux2.6 版本关于进程调度流程进行了梳理。其中提及,该版本通过调度器类实现调度,提升了内核关于调度算法的拓展性。本文对其中一种调度器类,CFS完全公平调度进行浅析
CFS概念
试想原先通过时间片控制进程调度会有什么问题
场景 1 不考虑优先级 A,B,C三个进程,均分60ms,每个进程获得20ms的时间片。60ms能够保证轮执该三个进程
场景 2 考虑优先级 A,B,C三个进程,根据优先级,获得时间片为100ms,20ms,5ms。125ms能够保证轮执该三个进程。优先级只影响了时间片多少,实际上并没有体现“优先”这一概念,真正实现“优先”,必须实现抢占功能。该情况下,若C进程为交互进程,则久久无法响应。
如何才能实现抢占并体现“优先”呢?
首先,优先应该如何定义?自然地,当进程实际执行时间 小于内核分配时间 ,下一次调度时应优先对其进行调度,此为优先。怎么理解?内核分配时间指的是内核综合考量每个进程优先级后给出的每个进程最多执行时间。而实际执行时间为进程实际运行的时间。进程实际执行时间 小于内核分配时间 就比如我(内核)允许你最多花100块,而你(进程)实际只花了50块,那么你(进程)还有50块的额度可以花。你们(多个进程)谁剩的额度多,我(内核)就让谁优先花钱(被调度)。让剩余额度最多的进程优先执行,还能降低其透支额度的风险。
因此,优先与否,可以转化为判断谁余额最多的问题。对于该策略有两种实现方式
方式一
根据每个进程的优先级,分配进程最多执行时间,优先级越高,最多执行时间越长 。每次设置重调度标志flag时,判断当前进程最多执行时间-实际执行时间 是否仍最大,否则设置重调度。在执行调度时,则选择就绪队列中,进程最多执行时间-实际执行时间 最大的进程进行调度
例如A、B、C根据优先级被分配最多执行时间为100ms,50ms,30ms。当前其实际执行时间分别为90ms,45ms,10ms。调度时应执行C进程
方式二
每个进程最多执行时间一致,根据优先级放大或缩小其实际执行时间,优先级越高,实际执行时间缩小越多,反之放大 。每次设置重调度标志flag时,判断当前进程最多执行时间-缩放后的实际执行时间 是否仍最大,否则设置重调度。在执行调度时,则选择就绪队列中,进程最多执行时间-缩放后的实际执行时间 最大的进程进行调度
例如A、B、C根据优先级被分配最多执行时间为100ms,100ms,100ms。当前其实际执行时间分别为50ms,50ms,50ms。根据优先级缩放实际执行时间,50/0.1,50/1,50/10,优先级越大,分母越大,缩放后的实际执行时间越小。调度时应执行C进程
另一方面,由于每个进程最多执行时间一致,实际上只需要比较缩放的实际执行实际谁小,则谁优先被调度。这便是CFS的逻辑,所以可以看到内核源码实现中,不再考虑时间片的概念,而是专注于对vruntime的管理,vruntime便是上述根据优先级缩放后的实际执行时间
代码实现
根据上述分析,我们理解CFS完全公平调度,只需要跟踪代码看看
- 何时会更新vruntime
- 如何更新vruntime
- 如何利用vruntime
何时更新vruntime
笔者在《Linux内核对比学习系列(3)——进程调度》中简单介绍了调度时机,对于CFS调度器类,时钟中断会触发update_process_times ->scheduler_tick ->task_tick_fair ->entity_tick ,文中没有对entity_tick 进行展开,事实上,该函数除了调用check_preempt_tick 检查是否满足调度条件设置重调度标志外,还会通过update_curr 进行vruntime 的更新
kernel\sched_fair.c
static void
entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
update_curr(cfs_rq);
#ifdef CONFIG_SCHED_HRTICK
if (queued) {
resched_task(rq_of(cfs_rq)->curr);
return;
}
if (!sched_feat(DOUBLE_TICK) &&
hrtimer_active(&rq_of(cfs_rq)->hrtick_timer))
return;
#endif
if (cfs_rq->nr_running > 1 || !sched_feat(WAKEUP_PREEMPT))
check_preempt_tick(cfs_rq, curr);
}
update_curr 函数内容很好理解,计算delta_exec (进程实际执行时间增量,注意是增量),并调用__update_curr 。__update_curr 做的就是上述概念提及的步骤,即通过calc_delta_fair 缩放delta_exec ,加到vruntime 中
kernel\sched_fair.c
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_of(cfs_rq)->clock;
unsigned long delta_exec;
if (unlikely(!curr))
return;
delta_exec = (unsigned long)(now - curr->exec_start);
if (!delta_exec)
return;
__update_curr(cfs_rq, curr, delta_exec);
curr->exec_start = now;
if (entity_is_task(curr)) {
struct task_struct *curtask = task_of(curr);
trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
cpuacct_charge(curtask, delta_exec);
account_group_exec_runtime(curtask, delta_exec);
}
}
static inline void
__update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,
unsigned long delta_exec)
{
unsigned long delta_exec_weighted;
schedstat_set(curr->exec_max, max((u64)delta_exec, curr->exec_max));
curr->sum_exec_runtime += delta_exec;
schedstat_add(cfs_rq, exec_clock, delta_exec);
delta_exec_weighted = calc_delta_fair(delta_exec, curr);
curr->vruntime += delta_exec_weighted;
update_min_vruntime(cfs_rq);
}
static inline unsigned long
calc_delta_fair(unsigned long delta, struct sched_entity *se)
{
if (unlikely(se->load.weight != NICE_0_LOAD))
delta = calc_delta_mine(delta, NICE_0_LOAD, &se->load);
return delta;
}
至此,何时更新vruntime及如何更新vruntime梳理完成(ps:可能还有其他更新时机,就不深入探究了
如何利用vruntime
自然地,vruntime是作为CFS调度器判断是否需要调度的条件,与重调度标志flag的设置有关。因此我们根据在《Linux内核对比学习系列(3)——进程调度》提及的内容,定位check_preempt_tick
check_preempt_tick 首先判断cfs的就绪队列cfs_rq上是否还有其他进程,如果有说明可能需要调度。通过__pick_next_entity 获取下一个被调度实体,判断两者 vruntime 差距是否大于 ideal_runtime ,若大于则设置重调度标志。意味着,如果下一个被调度实体还是自己,则不需要调度,如果是别的进程,但两者实际运行时间相差不大,也不会调度,如此一来也能够避免频繁调度,减少上下文切换的次数,提升执行效率。(妙啊
static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
if (cfs_rq->nr_running > 1) {
struct sched_entity *se = __pick_next_entity(cfs_rq);
s64 delta = curr->vruntime - se->vruntime;
if (delta > ideal_runtime)
resched_task(rq_of(cfs_rq)->curr);
}
}
根据先前的分析可知,__pick_next_entity 所选出的调度实体 se 必须是当前所有进程中 vruntime 最小的。那么它如何才能极高效率地选出最小值呢,当然是使用红黑树啦!
代码较简单,直接从就绪队列里取个红黑树节点,再通过 container_of 获取到对应 se 即可。意味着,提前已经将红黑树更新,并计算出当前 vruntime 最小的节点。
kernel\sched_fair.c
#define rb_entry(ptr, type, member) container_of(ptr, type, member)
static struct sched_entity *__pick_next_entity(struct cfs_rq *cfs_rq)
{
struct rb_node *left = cfs_rq->rb_leftmost;
if (!left)
return NULL;
return rb_entry(left, struct sched_entity, run_node);
}
我们回顾前述,实际上在更新 vruntime 时,就必须更新红黑树了,见 __update_curr 中的update_min_vruntime(),具体就不展开了。同时,进程在创建时也应插入CFS的红黑树中,具体地如书上所言会调用enqueue_entity() ,删除时执行dequeue_entity() ,在此就不展开探究了
static void update_min_vruntime(struct cfs_rq *cfs_rq)
{
u64 vruntime = cfs_rq->min_vruntime;
if (cfs_rq->curr)
vruntime = cfs_rq->curr->vruntime;
if (cfs_rq->rb_leftmost) {
struct sched_entity *se = rb_entry(cfs_rq->rb_leftmost,
struct sched_entity,
run_node);
if (!cfs_rq->curr)
vruntime = se->vruntime;
else
vruntime = min_vruntime(vruntime, se->vruntime);
}
cfs_rq->min_vruntime = max_vruntime(cfs_rq->min_vruntime, vruntime);
}
|