从Linux 2.6.23开始,Linux引入scheduling class的概念,目的是将调度器模块化。这样提高了扩展性,添加一个新的调度器也变得简单起来。一个系统中还可以共存多个调度器。在Linux中,将调度器公共的部分抽象,使用
struct sched_class
结构体描述一个具体的调度类。系统核心调度代码会通过
struct sched_class
结构体的成员调用具体调度类的核心算法。先简单的介绍下
struct sched_class
部分成员作用。
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);
void (*check_preempt_curr)(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);
/* ... */
check_preempt_curr:当一个进程被唤醒或者创建的时候,需要检查当前进程是否可以抢占当前cpu上正在运行的进程,如果可以抢占需要标记TIF_NEED_RESCHED flag。
pick_next_task:从runqueue中选择一个最适合运行的task。这也算是调度器比较核心的一个操作。例如,我们依据什么挑选最适合运行的进程呢?这就是每一个调度器需要关注的问题。
sched_class_highest----->stop_sched_class
.next---------->dl_sched_class
.next---------->rt_sched_class
.next--------->fair_sched_class
.next----------->idle_sched_class
.next = NULL
Linux调度核心在选择下一个合适的task运行的时候,会按照优先级的顺序便利调度类的pick_next_task函数。因此,SCHED_FIFO调度策略的实时进程永远比SCHED_NORMAL调度策略的普通进程优先运行。代码中pick_next_task函数也有体现。pick_next_task函数就是负责选择一个即将运行的进程,以下贴出省略版代码。
static inline struct task_struct *pick_next_task(struct rq *rq,
struct task_struct *prev, struct rq_flags *rf)
const struct sched_class *class;
struct task_struct *p;
for_each_class(class) { /* 按照优先级顺序便利所有的调度类,通过next指针便利单链表 */
p = class->pick_next_task(rq, prev, rf);
if (p)
return p;
针对CFS调度器,管理的进程都属于SCHED_NORMAL或者SCHED_BATCH策略。后面的部分主要针对CFS调度器讲解。
普通进程的优先级
CFS是Completely Fair Scheduler简称,即完全公平调度器。CFS的设计理念是在真实硬件上实现理想的、精确的多任务CPU。CFS调度器和以往的调度器不同之处在于没有时间片的概念,而是分配cpu使用时间的比例。例如:2个相同优先级的进程在一个cpu上运行,那么每个进程都将会分配50%的cpu运行时间。这就是要实现的公平。
以上举例是基于同等优先级的情况下。但是现实却并非如此,有些任务优先级就是比较高。那么CFS调度器的优先级是如何实现的呢?首先,我们引入权重的概念,权重代表着进程的优先级。各个进程之间按照权重的比例分配cpu时间。例如:2个进程A和B。A的权重是1024,B的权重是2048。那么A获得cpu的时间比例是1024/(1024+2048) = 33.3%。B进程获得的cpu时间比例是2048/(1024+2048)=66.7%。我们可以看出,权重越大分配的时间比例越大,相当于优先级越高。在引入权重之后,分配给进程的时间计算公式如下:
分配给进程的时间 = 总的cpu时间 * 进程的权重/就绪队列(runqueue)所有进程权重之和
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。
什么是调度延迟?调度延迟就是保证每一个可运行进程都至少运行一次的时间间隔。例如,每个进程都运行10ms,系统中总共有2个进程,那么调度延迟就是20ms。如果有5个进程,那么调度延迟就是50ms。如果现在保证调度延迟不变,固定是6ms,那么系统中如果有2个进程,那么每个进程运行3ms。如果有6个进程,那么每个进程运行1ms。如果有100个进程,那么每个进程分配到的时间就是0.06ms。随着进程的增加,每个进程分配的时间在减少,进程调度过于频繁,上下文切换时间开销就会变大。因此,CFS调度器的调度延迟时间的设定并不是固定的。当系统处于就绪态的进程少于一个定值(默认值8)的时候,调度延迟也是固定一个值不变(默认值6ms)。当系统就绪态进程个数超过这个值时,我们保证每个进程至少运行一定的时间才让出cpu。这个“至少一定的时间”被称为最小粒度时间。在CFS默认设置中,最小粒度时间是0.75ms。用变量sysctl_sched_min_granularity记录。因此,调度周期是一个动态变化的值。调度周期计算函数是__sched_period()。
static u64 __sched_period(unsigned long nr_running)
if (unlikely(nr_running > sched_nr_latency))
return nr_running * sysctl_sched_min_granularity;
return sysctl_sched_latency;
nr_running是系统中就绪进程数量,当超过sched_nr_latency时,我们无法保证调度延迟,因此转为保证调度最小粒度。如果nr_running并没有超过sched_nr_latency,那么调度周期就等于调度延迟sysctl_sched_latency(6ms)。
虚拟时间(virtual time)
CFS调度器的目标是保证每一个进程的完全公平调度。CFS调度器就像是一个母亲,她有很多个孩子(进程)。但是,手上只有一个玩具(cpu)需要公平的分配给孩子玩。假设有2个孩子,那么一个玩具怎么才可以公平让2个孩子玩呢?简单点的思路就是第一个孩子玩10分钟,然后第二个孩子玩10分钟,以此循环下去。CFS调度器也是这样记录每一个进程的执行时间,保证每个进程获取CPU执行时间的公平。因此,哪个进程运行的时间最少,应该让哪个进程运行。
例如,调度周期是6ms,系统一共2个相同优先级的进程A和B,那么每个进程都将在6ms周期时间内内各运行3ms。如果进程A和B,他们的权重分别是1024和820(nice值分别是0和1)。进程A获得的运行时间是6x1024/(1024+820)=3.3ms,进程B获得的执行时间是6x820/(1024+820)=2.7ms。进程A的cpu使用比例是3.3/6x100%=55%,进程B的cpu使用比例是2.7/6x100%=45%。计算结果也符合上面说的“进程每降低一个nice值,将多获得10% CPU的时间”。很明显,2个进程的实际执行时间是不相等的,但是CFS想保证每个进程运行时间相等。因此CFS引入了虚拟时间的概念,也就是说上面的2.7ms和3.3ms经过一个公式的转换可以得到一样的值,这个转换后的值称作虚拟时间。这样的话,CFS只需要保证每个进程运行的虚拟时间是相等的即可。虚拟时间vriture_runtime和实际时间(wall time)转换公式如下:
weight
= (wall_time * NICE_0_LOAD * inv_weight) >> 32 (inv_weight = ------------ )
weight
权重的值已经计算保存到sched_prio_to_weight数组中,根据这个数组我们可以很容易计算inv_weight的值。内核中使用sched_prio_to_wmult数组保存inv_weight的值。计算公式是:sched_prio_to_wmult[i] = 232/sched_prio_to_weight[i]。
const u32 sched_prio_to_wmult[40] = {
/* -20 */ 48388, 59856, 76040, 92818, 118348,
/* -15 */ 147320, 184698, 229616, 287308, 360437,
/* -10 */ 449829, 563644, 704093, 875809, 1099582,
/* -5 */ 1376151, 1717300, 2157191, 2708050, 3363326,
/* 0 */ 4194304, 5237765, 6557202, 8165337, 10153587,
/* 5 */ 12820798, 15790321, 19976592, 24970740, 31350126,
/* 10 */ 39045157, 49367440, 61356676, 76695844, 95443717,
/* 15 */ 119304647, 148102320, 186737708, 238609294, 286331153,
系统中使用struct load_weight
结构体描述进程的权重信息。weight代表进程的权重,inv_weight等于232/weight。
__calc_delta() = (delta_exec * weight * lw->inv_weight) >> 32
weight 2^32
= delta_exec * ---------------- (lw->inv_weight = --------------- )
lw->weight lw->weight
和上面计算虚拟时间计算公式对比发现。如果需要计算进程的虚拟时间,这里的weight只需要传递参数NICE_0_LOAD,lw参数是进程对应的struct load_weight
结构体。
static u64 __calc_delta(u64 delta_exec, unsigned long weight, struct load_weight *lw)
u64 fact = scale_load_down(weight);
int shift = 32;
__update_inv_weight(lw);
if (unlikely(fact >> 32)) {
while (fact >> 32) {
fact >>= 1;
shift--;
fact = (u64)(u32)fact * lw->inv_weight;
while (fact >> 32) {
fact >>= 1;
shift--;
return mul_u64_u32_shr(delta_exec, fact, shift);
按照上面说的理论,calc_delta_fair()函数调用__calc_delta()的时候传递的weight参数是NICE_0_LOAD,lw参数是进程对应的struct load_weight
结构体。
static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se)
if (unlikely(se->load.weight != NICE_0_LOAD)) /* 1 */
delta = __calc_delta(delta, NICE_0_LOAD, &se->load); /* 2 */
return delta;
现在我们总结一下。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
成员记录已经运行的虚拟时间。我们将这几个数据结构简单梳理,如下图所示。
hahaha
linuxer
bsp
linuxer
yyll
StevenSun
see
ccilery
abcdefg345
湫
figo
s
小亮
markened
markened
smcdef
markened
markened
smcdef
smcdef
Linux内核分析(24)
统一设备模型(15)
电源管理子系统(43)
中断子系统(15)
进程管理(31)
内核同步机制(25)
GPIO子系统(5)
时间子系统(14)
通信类协议(7)
内存管理(31)
图形子系统(2)
文件系统(5)
TTY子系统(6)
在我的理解中,cfs的_sched_period是使每个任务都能运行一次的时间。当进程数小于sched_nr_latency_延迟时,CFS会将时间分割为每个进程预计运行一次的时段
u64 __sched_period(unsigned long nr_running)
{
if (unlikely(nr_running > sched_nr_latency))
return nr_running * sysctl_sched_min_granularity;
else
return sysctl_sched_latency;
}
1. 如果有两个具有相同权重的cfs进程,每个进程的 最大 运行时间是否为“sysctl_sched_latency/2”(进程可以不运行这么多),还是说我们是否必须为每个进程执行“sysctl_sched_latency/2”?
2. 那么上下文切换时间、调度程序成本计算和其他因素产生的时间呢? sched_period到底由哪些东西构成 调度延时/调度周期存在的意义是什么,感觉每时每刻都会有进程从TASK_RUNNING转变成睡眠,nr_running会不断变化,那每次被调度的进程进来算调度时期的值都不一样...