进程管理笔记三、CFS调度算法
引言:CFS是英文Completely Fair Scheduler的缩写,即完全公平调度器,负责进程调度。在Linux Kernel 2.6.23之后采用,它负责将CPU资源,分配给正在执行的进程,目标在于最大化程式互动效能,最小化整体CPU的运用。使用红黑树来实现,算法效率为O(log(n))。
一、CFS调度算法原理
调度算法最核心的两点即为调度哪个进程执行、被调度进程执行的时间多久。前者称为调度策略,后者为执行时间。
1.1、调度策略
cfs定义一种新的模型,它给cfs_rq(cfs的run queue)中的每一个进程安排一个虚拟时钟,vruntime。如果一个进程得以执行,随着时间的增长(即一个个tick的到来),其vruntime将不断增大。没有得到执行的进程vruntime不变。
调度器总是选择vruntime值最低的进程执行。这就是所谓的“完全公平”。对于不同进程,优先级高的进程vruntime增长慢,以至于它能得到更多的运行时间。
1)、公平的体现:机会平等,时间差异
公平体现在vruntime (virtual runtime, 虚拟运行时间)上面,它记录着进程已经运行的时间,其大小与进程的权重、运行时间存在一个定量计算关系。
vruntime = 实际运行时间 * 1024 / 进程权重
实际上1024等于nice为0的进程的权重,代码中是NICE_0_LOAD,也就是说,所有进程都以nice值为0的权重1024作为基准,计算自己的vruntime增加速度。结合分配给进程实际运行的时间,可得如下换算关系:
分配给进程的时间 = 调度周期 * 进程权重 / 全部进程权重之和
vruntime = 实际运行时间 * 1024 / 进程权重
vruntime = (调度周期 * 进程权重 / 全部进程权重之和) * 1024 / 进程权重
vruntime = (调度周期 / 全部进程权重之和) * 1024
可以看到进程在一个调度周期内的vruntime值大小与进程权重无关,所有进程的vruntime值在一个周期内增长是一致的。vruntime值较小的进程,说明它以前占用cpu的时间较短,受到了不公平对待,因此选择作为下一次运行的进程。
这样既能公平选择进程,又能保证高优先级进程获得较多运行时间,就是cfs的主要思想了。其可以简单概括为:机会平等、时间差异。
1.2、执行时间
cfs采用当前系统中全部可调度进程优先级的比重确定每一个进程执行的时间片,即:
分配给进程的时间 = 调度周期 * 进程权重 / 全部进程之和。
假如有三个可调度进程A、B、C,它们的优先级分别为5,10,15,调度周期为60ms, 则它们的时间片分别为:60ms * 5 / 30 = 10ms、60ms * 10 / 30 = 20ms、60ms * 15 / 30 = 30ms
二、CFS调度算法内核实现
2.1、骨架—红黑树
cfs调度算法使用红黑树来实现,其详细内容可以参考维基百科红黑树的介绍。这里简单讲一下cfs的结构。第一个是调度实体sched_entity,它代表一个调度单位,在组调度关闭的时候可以把他等同为进程。每一个task_struct中都有一个sched_entity,进程的vruntime和权重都保存在这个结构中。
sched_entity通过红黑树组织在一起,所有的sched_entity以vruntime为key(实际上是以vruntime-min_vruntime为key,是为了防止溢出)插入到红黑树中,同时缓存树的最左侧节点,也就是vruntime最小的节点,这样可以迅速选中vruntime最小的进程。
仅处于就绪态的进程在这棵树上,睡眠进程和正在运行的进程都不在树上。
2.2、nice值与权重的关系
每一个进程都有一个nice值,代表其静态优先级。可以参考 Linux nice及renice命令使用。nice值和进程的权重的关系存储在数组prio_to_weight中,如下所示:
/*prio_to_weight数组反应的是nice值与权重的对应关系*/
static const int prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};
可以看到,nice值越小,进程的权重越大。CFS调度器的一个调度周期是固定的,由sysctl_sched_latency变量保存。
2.3、两个重要的结构体
1)、完全公平队列cfs_rq:描述运行在一个cpu上的处于TASK_RUNNING状态的普通进程的各种运行信息:
struct cfs_rq {
struct load_weight load; //运行队列总的进程权重
unsigned int nr_running, h_nr_running; //进程的个数
u64 exec_clock; //运行的时钟
u64 min_vruntime; //该cpu运行队列的vruntime推进值, 一般是红黑树中最小的vruntime值
struct rb_root tasks_timeline; //红黑树的根结点
struct rb_node *rb_leftmost; //指向vruntime值最小的结点
//当前运行进程, 下一个将要调度的进程, 马上要抢占的进程,
struct sched_entity *curr, *next, *last, *skip;
struct rq *rq; //系统中有普通进程的运行队列, 实时进程的运行队列, 这些队列都包含在rq运行队列中
...
};
2)、调度实体sched_entity:记录一个进程的运行状态信息
struct sched_entity {
struct load_weight load; //进程的权重
struct rb_node run_node; //运行队列中的红黑树结点
struct list_head group_node; //与组调度有关
unsigned int on_rq; //进程现在是否处于TASK_RUNNING状态
u64 exec_start; //一个调度tick的开始时间
u64 sum_exec_runtime; //进程从出生开始, 已经运行的实际时间
u64 vruntime; //虚拟运行时间
u64 prev_sum_exec_runtime; //本次调度之前, 进程已经运行的实际时间
struct sched_entity *parent; //组调度中的父进程
struct cfs_rq *cfs_rq; //进程此时在哪个运行队列中
};
2.4、几个与cfs有关的过程:
1)、创建新进程:需要设置新进程的vruntime值及将新进程加入红黑树中,并判断是否需要抢占当前进程。
2)、进程唤醒:需要调整睡眠进程的vruntime值, 并且将睡眠进程加入红黑树中. 并判断是否需要抢占当前进程
3)、进程调度:需要把当前进程加入红黑树中, 还要从红黑树中挑选出下一个要运行的进程.
4)、时钟周期中断:在时钟中断周期函数中, 需要更新当前运行进程的vruntime值, 并判断是否需要抢占当前进程
这里详细的代码实现,可以参考:Linux的CFS(完全公平调度)算法,代码解释非常详实。
三、CFS调度算法相关的有趣问题
相关代码实现,参考:从几个问题开始理解CFS调度器
3.1、新进程的vruntime的初始值是不是0?
假如新进程的vruntime初值为0的话,比老进程的值小很多,那么它在相当长的时间内都会保持抢占CPU的优势,老进程就要饿死了,这显然是不公平的。所以CFS是这样做的:每个CPU的运行队列cfs_rq都维护一个 min_vruntime 字段,记录该运行队列中所有进程的vruntime最小值,新进程的初始vruntime值就以它所在运行队列的min_vruntime为基础来设置,与老进程保持在合理的差距范围内。
3.2、休眠进程的vruntime的值一直保持不变吗?
如果休眠进程的 vruntime 保持不变,而其他运行进程的 vruntime 一直在推进,那么等到休眠进程终于唤醒的时候,它的vruntime比别人小很多,会使它获得长时间抢占CPU的优势,其他进程就要饿死了。这显然是另一种形式的不公平。CFS是这样做的:在休眠进程被唤醒时重新设置vruntime值,以min_vruntime值为基础,给予一定的补偿,但不能补偿太多。
3.3、进程占用的时间片可以无穷小吗?
假设有两个进程,它们的vruntime初值都是一样的,第一个进程只要一运行,它的vruntime马上就比第二个进程更大了,那么它的CPU会立即被第二个进程抢占吗?答案是这样的:为了避免过于短暂的进程切换造成太大的消耗,CFS设定了进程占用CPU的最小时间值, sched_min_granularity_ns ,正在CPU上运行的进程如果不足这个时间是不可以被调离CPU的。
3.4、进程从一个CPU迁移至另外一个CPU的时候vruntime会变化吗?
当进程从一个CPU的运行队列中出来 (dequeue_entity) 的时候,它的vruntime要减去队列的min_vruntime值; 而当进程加入另一个CPU的运行队列 ( enqueue_entiry) 时,它的vruntime要加上该队列的min_vruntime值。 这样,进程从一个CPU迁移到另一个CPU之后,vruntime保持相对公平。
小结:这里参考了很多人的博客,大家从不同角度描述了他们理解的CFS算法,可以概括为算法原理、算法实现。我是一个有强迫症的人,前一篇写了进程调度算法,第三篇一定要写CFS,这样才觉得学习是有连贯性的,知识也是成体系有脉络的。后面有精力,在大的框架搭建完成以后,会仔细学习红黑树,推敲cfs核心结构体的组织关系。
参考资料:
1、linux内核分析——CFS(完全公平调度算法)理论讲的很棒
2、Linux的CFS(完全公平调度)算法将相关数据结构,内核实现代码讲解很清楚
3、从几个问题开始理解CFS调度器对于具体问题,都有内核代码解释
纠错及建议:[email protected]