CFS(Completely Fair Scheduler,完全公平调度器)用于Linux系统中普通进程的调度。它给cfs_rq(cfs的run queue)中的每一个进程设置一个虚拟时钟,vruntime。如果一个进程得以执行,随着时间的增长(一个个tick的到来),其vruntime将不断增大。没有得到执行的进程vruntime不变。调度器总是选择vruntime跑得最慢的那个进程来执行。这就是所谓的“完全公平”。为了区别不同优先级的进程,优先级高的进程vruntime增长得慢,以至于它可能得到更多的运行机会。
CFS不区分具体的cpu算力消耗型进程,还是io消耗型进程,统一采用红黑树算法来管理所有的调度实体sched_entity,算法效率为O(log(n))。每个进程都由一个struct task_struct来表示,为什么这里又定义了一个sched_entity?这是由于调度需要一些更详细的信息,比如当前运行时间或上次运行时间,因此就搞出了一个sched_entity的概念,为了存储一些调度相关的信息,给调度器用。CFS跟踪调度实体sched_entity的虚拟运行时间vruntime,平等对待运行队列中的调度实体sched_entity,将执行时间少的调度实体sched_entity排列到红黑树的最左边。调度实体sched_entity通过enqueue_entity()和dequeue_entity()来进行红黑树的出队入队。

- //红黑树
- struct rb_root_cached {
- struct rb_root rb_root; //红黑树的根节点
- struct rb_node *rb_leftmost; //红黑树中最左边节点
- };
-
- //cfs就绪队列
- struct cfs_rq {
- struct load_weight load;
- unsigned int nr_running; //就绪队列上调度实体的个数
- u64 min_vruntime; //就绪队列上所有调度实体中的最小虚拟时间
-
- /*用于跟踪调度实体按虚拟时间大小排序的红黑树的信息
- (包含红黑树的根以及红黑树中最左边节点)*/
- struct rb_root_cached tasks_timeline;
- };
- struct sched_entity {
- struct load_weight load; //权重信息,在计算虚拟时间的时候会用到其中的inv_weight成员
-
- /*CFS调度器的每个就绪队列维护了一颗红黑树,上面挂满了就绪等待执行的task
- run_node就是挂载点*/
- struct rb_node run_node;
-
- //调度实体se加入就绪队列后,on_rq置1。从就绪队列删除后,on_rq置0
- unsigned int on_rq;
-
- //调度实体已经运行实际时间总合
- u64 sum_exec_runtime;
-
- //调度实体已经运行的虚拟时间总合
- u64 vruntime;
- };
- struct sched_class {
- const struct sched_class *next;
-
- void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
- void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
- struct task_struct * (*pick_next_task)(struct rq *rq,
- struct task_struct *prev,
- struct rq_flags *rf);
- void (*yield_task) (struct rq *rq);
- bool (*yield_to_task)(struct rq *rq, struct task_struct *p, bool preempt);
-
- void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);
现在我们总结一下。Linux中所有的进程使用task_struct描述。task_struct包含很多进程相关的信息(例如,优先级、进程状态以及调度实体等)。但是,每一个调度类并不是直接管理task_struct,而是引入调度实体的概念。CFS调度器使用sched_entity跟踪调度信息。CFS调度器使用cfs_rq跟踪就绪队列信息以及管理就绪态调度实体,并维护一棵按照虚拟时间排序的红黑树。tasks_timeline->rb_root是红黑树的根,tasks_timeline->rb_leftmost指向红黑树中最左边的调度实体,即虚拟时间最小的调度实体(为了更快的选择最适合运行的调度实体,因此rb_leftmost相当于一个缓存)。每个就绪态的调度实体sched_entity包含插入红黑树中使用的节点rb_node,同时vruntime成员记录已经运行的虚拟时间。
CFS调度器针对优先级提出了nice值的概念,其实和权重是一一对应的关系。nice值就是一个具体的数字,取值范围是[-20, 19]。数值越小代表优先级越大,同时也意味着权重值越大,nice值和权重之间可以互相转换。内核提供了一个表格转换nice值和权重。
- const int sched_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,
- };
数组的值可以看作是公式:weight = 1024 / 1.25nice计算得到。公式中的1.25取值依据是:进程每降低一个nice值,将多获得10% cpu的时间。公式中以1024权重为基准值计算得来,1024权重对应nice值为0,其权重被称为NICE_0_LOAD。默认情况下,大部分进程的权重基本都是NICE_0_LOAD。
CFS调度器没有时间的概念,而是根据虚拟运行时间(抽象表示任务已运行时间)对任务进行排序,选择虚拟运行时间最小的进程进行调度
实际运行时间到 vruntime 的计算公式为:
[ vruntime = 实际运行时间 * 1024 / 进程权重 ]
vruntime值较小就说明它以前占用cpu的时间较短,受到了“不公平”对待,因此下一个运行进程就是它。这样既能公平选择进程,又能保证高优先级进程获得较多的运行时间,这就是CFS的主要思想。
挑选的进程进行运行了,它运行多久?
进程运行的时间是根据进程的权重进行分配。
[ 分配给进程的运行时间 = 调度周期 *(进程权重 / 所有进程权重之和) ]