调度的思路
调度的目的是为了让需要被执行的程序在期望的时间内被执行。而通常调度的方法又可以分为主动调度和被动调度。可以类比下:
CPU就是一个篮球场,而程序A是广场舞吴大妈,程序B就是篮球狂魔老王,吴大妈想占据篮球场来跳舞,而老王则想占据篮球场来打篮球。显然没办法让他们同时都占据篮球场,因为这样可能会导致受伤,因此老王觉得,好男不跟女斗,就说把篮球场让给吴大妈,这种主动让出篮球场的行为就是主动调度(yield)。吴大妈也不是那种不讲人情世故的人,一看老王都这么大方了,就退一步说,要不我们轮流来用篮球场吧,我用一个小时,你用一个小时,按照这个时间片来轮转,于是就产生了时间片调度的概念,但这个时候有个问题,谁来帮他们统计时间以及通知他们时间已经耗尽了,篮球场管理员老秦就站出来了,充当了调度人员,负责管理篮球场上吴大妈和老王的使用时间,并到时提醒他们。在这种节奏下,吴大妈和老王都在各自的时间片上占据者篮球场,但一旦时间耗尽,就必须立刻换另外一个人使用,这种就是被动调度。
这天这个篮球场要迎来一场篮球比赛,相对于吴大妈和老王,篮球赛明显比他们的广场舞和篮球训练更重要,因此,这时候如果再让吴大妈和老王轮流使用篮球场,估计篮球场管理员老秦就要下岗了。老秦就找到吴大妈和老王说,现在有个篮球赛,你们就先不要使用篮球场了啊,篮球赛的优先级比你们跳舞和篮球训练的优先级高,等篮球赛结束后,你们再使用篮球场吧,老王和吴大妈也没得办法,只能按照老秦的安排,先让出篮球场,等高优先级的篮球赛结束后再使用吧,这就引入了实时调度器,必须保证高优先级的任务在规定的时间内被执行。篮球赛结束后,吴大妈和老王又在各自的时间片上轮流占据着篮球场。
这天篮球场上要迎来一个街舞比赛,耗时两周,老秦又去找老王和吴大妈,说让她两休息两周,这把老王和吴大妈不高兴了,两周谁受得了不打篮球不跳舞啊,就跟老秦发牢骚。老秦心想估计这次没办法去说服吴大妈和老王了,就想了个办法,跟吴大妈和老王说:你看街舞比赛相比你们广场舞和篮球训练是不是更重要,现在你们还是轮流使用篮球场,但街舞比赛的优先级要比你们高,所以街舞比赛使用的时间要比你们长,街舞比赛每次使用3小时,你们还是各自1小时,这样轮转执行可以吗?吴大妈和老王一琢磨,既然老秦也让步了,就同意了这个方案。于是基于时间片和优先级的调度策略就诞生了。这既是linux2.6之前的O(1)调度器。
linux调度器
linux调度器是以模块方式提供的,调度器类有
- dl_sched_class
- rt_sched_class
- fair_sched_class
- idle_sched_class
每个调度器都有相关的调度策略
调度类 | 描述 | 调度策略 |
dl_sched_class | Deadline调度器 | SCHED_DEADLINE |
rt_sched_class | 实时调度器 | SCHED_FIFO、SCHED_RR |
fair_sched_class | 公平调度器 | SCHED_NORMAL、SCHED_BATCH |
idle_sched_class | 空闲任务调度器 | SCHED_IDLE |
调度策略是针对每个进程而言的,不是针对系统而言,线程也是一种特殊的进程。也就是说,linux系统中支持多种调度策略的线程,在线程创建时就可以指定线程的调度策略。使用sched_setscheduler()来设置进程(线程)的调度策略。
系统默认创建的线程的调度策略是SCHED_NORMAL,也就是公平调度类,在此调度策略下,linux内核采用了CFS的调度算法。在其他调度类下,linux内核并不是采用CFS调度策略,这点需要注意。
-
SCHED_FIFO
先进先出的调度策略,特征是不基于时间片,高优先级抢占低优先级,相同优先级的顺序执行。
-
SCHED_RR
基于时间片的实时调度策略,特征是基于时间片,高优先级抢占低优先级,相同优先级的按照时间片轮转执行。
-
SCHED_NORMAL
绝对公平调度,其实是没办法实现绝对公平,但调度的目标是绝对公平,因此称为绝对公平调度。CFS不直接使用时间片,而是利用处理器的使用比例。其实这个使用比例也是从时间维度上来定义的,并不是从物理上进行分配(CPU只能当作一个整体来使用),就是进程A使用了CPU多长时间,B使用了CPU多长时间。
其实CFS最终还是根据时间来作为调度的依据,但只是不直接使用时间片,而是将时间片经过一次转换,转换为虚拟运行时间(vruntime)。CFS追求的目标就是所有进程的vruntime是相等的。
CFS调度策略
CFS调度策略的目标就是追求所有进程的vruntime是相等的。那这里就有几个问题:
1. vruntime怎么计算?
Vruntime是虚拟运行时间,虽然是虚拟运行时间,但跟真实时间(wall time)也是有关系的,关系就是:
vruntime= NICE_0_LOADWeight *wall time (公式1)
NICE_0_LOAD就是NICE值为0时的权重,在linux代码中为:1024(32位系统)1048576(64位系统)。
解释下NICE,对于实时调度器,linux内核提供实时优先级来区分不同的进程优先级,对于公平调度器,linux内核提供nice值来表示进程优先级。Nice的值是从-20~19,默认值为0。数值越大,优先级越低。
解释下weight,weight就是根据nice计算得来的,计算公式:
weight = 1024 / 1.25nice (公式2)
公式中的1.25取值依据是:进程每降低一个nice值,将多获得10% cpu的时间。公式中以1024权重为基准值计算得来,1024权重对应nice值为0,其权重被称为NICE_0_LOAD。默认情况下,大部分进程的权重基本都是NICE_0_LOAD。
把公式2代入公式1,就可以得到:
vruntime= NICE_0_LOADWeight *wall time= 102410241.25nice *wall time= 1.25nice*wall time
分析上述公式,可知,在vruntime相同的情况下,nice值越高,优先级越低,其获得的wall time就越少。这跟操作系统的期望是一致的。
2. 什么时候计算vruntime?
Vruntime是由系统定时器周期性调用的,也就是说在定时器中断处理函数中会计算一次vruntime,系统定时器中断的周期是通过CONFIG_HZ来计算的,CONFIG_HZ在内核编译时可设置为200Hz,250HZ,500HZ,100HZ。对应的定时器中断周期就是5ms,4ms,2ms,1ms。
3. vruntime值有什么用?
每次进定时器中断处理函数,计算出的vruntime有什么用?这里需要引入另外一个时间概念,就是分配给进程的时间。
进程的时间 = 总的cpu时间 * 进程的权重/就绪队列所有进程权重之和。
在进程创建时,内核就会为当前进程计算出在一个调度周期内,应该分配给当前进程的运行时间,此运行时间是实际的运行时间,也即是前面的wall time,但注意此处的wall time是个固定值,是在进程创建时就已经计算好了,暂且我们将其重定义为fix wall time。计算vruntime中的wall time是个变量,其随着程序的运行会不断增大。
在定时器中断处理函数中获取wall time,根据公式计算出vruntime,将这个vruntime与就绪进程列表中的其他vruntime进行比较,寻找vruntime最小的进程去运行。注意,在中断处理函数中只是计算下vruntime,并减少当前进程的时间片。如果当前时间片已经耗尽,则将当前进程设置为need_resched,在中断程序返回时会检查need_resched,如果有need_resched,内核则调用schedule()进行进程切换。
此时又有一个疑问,linux内核提供的定时器周期是ms级,如果仅仅通过定时器中断来触发中断,linux系统的响应能力就很差,必然还能有其他途径去触发调度。
Linux 2.6版本后支持用户抢占和内核抢占。以下情况包含了所有可能触发调度的地方。
抢占模式 | 场景 |
用户抢占 | 从系统调用返回用户空间 |
从中断处理程序返回用户空间 | |
内核抢占 | 中断处理程序正在执行,且返回内核空间之前 |
内核代码再一次具有可抢占性的时候 | |
内核中的任务显式调用schedule() | |
内核中的任务阻塞 |
也就是说只有在上述情况下,进程间才会进行调度。