介绍
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 sched_entity:调度实体,这个也是CFS调度管理的对象
- struct cfs_rq:CFS运行队列,该结构中包含了struct rb_root_cached红黑树,用于链接调度实体struct sched_entity。rq运行队列中对应了一个CFS运行队列,此外,在task_group结构中也会为每个CPU再维护一个CFS运行队列。
- struct task_group:组调度,Linux支持将任务分组来对CPU资源进行分配管理,该结构中为系统中的每个CPU都分配了struct sched_entity调度实体和struct cfs_rq运行队列,其中struct sched_entity用于参与CFS的调度
//红黑树
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);
- next:next成员指向下一个调度类(比自己低一个优先级)。在Linux中,每一个调度类都是有明确的优先级关系,高优先级调度类管理的进程会优先获得cpu使用权。
- enqueue_task:向该调度器管理的runqueue中添加一个进程。我们把这个操作称为入队。
- dequeue_task:向该调度器管理的runqueue中删除一个进程。我们把这个操作称为出队。
- check_preempt_curr:当一个进程被唤醒或者创建的时候,需要检查当前进程是否可以抢占当前cpu上正在运行的进程,如果可以抢占需要标记TIF_NEED_RESCHED flag。
- pick_next_task:通过next指针便利调度类链表,按照优先级顺序选择下一个最适合调度运行的任务,将其从rq移除,。
现在我们总结一下。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成员记录已经运行的虚拟时间。
基本概念
nice值(普通进程的优先级)
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。
virtual time(虚拟时间)
CFS调度器没有时间的概念,而是根据虚拟运行时间(抽象表示任务已运行时间)对任务进行排序,选择虚拟运行时间最小的进程进行调度
实际运行时间到 vruntime 的计算公式为:
[ vruntime = 实际运行时间 * 1024 / 进程权重 ]
vruntime值较小就说明它以前占用cpu的时间较短,受到了“不公平”对待,因此下一个运行进程就是它。这样既能公平选择进程,又能保证高优先级进程获得较多的运行时间,这就是CFS的主要思想。
执行时间
挑选的进程进行运行了,它运行多久?
进程运行的时间是根据进程的权重进行分配。
[ 分配给进程的运行时间 = 调度周期 *(进程权重 / 所有进程权重之和) ]