前言
在《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 run-time statistics of the 'current'.
*/
update_curr(cfs_rq);
#ifdef CONFIG_SCHED_HRTICK
/*
* queued ticks are scheduled to match the slice, so don't bother
* validating it and just reschedule.
*/
if (queued) {
resched_task(rq_of(cfs_rq)->curr)