进程调度算法
进程调度算法也称
CPU
调度算法,毕竟进程是由
CPU
调度的。
当
CPU
空闲时,操作系统就选择内存中的某个「就绪状态」的进程,并给其分配
CPU
。
什么时候会发⽣
CPU
调度呢?通常有以下情况:
1. 当进程从运⾏状态转到等待状态;
2. 当进程从运⾏状态转到就绪状态;
3. 当进程从等待状态转到就绪状态;
4. 当进程从运⾏状态转到终⽌状态;
其中发⽣在
1
和
4
两种情况下的调度称为「⾮抢占式调度」,
2
和
3
两种情况下发⽣的调度称为「抢占式调度」。
⾮抢占式的意思就是,当进程正在运⾏时,它就会⼀直运⾏,直到该进程完成或发⽣某个事件⽽被阻塞时,才会把 CPU
让给其他进程。
⽽抢占式调度,顾名思义就是进程正在运⾏的时,可以被打断,使其把
CPU
让给其他进程。那抢占的原则⼀般有三种,分别是时间⽚原则、优先权原则、短作业优先原则。
你可能会好奇为什么第
3
种情况也会发⽣
CPU
调度呢?假设有⼀个进程是处于等待状态的,但是它的优先级⽐较⾼,如果该进程等待的事件发⽣了,它就会转到就绪状态,⼀旦它转到就绪状态,如果我们的调度算法是以优先级来进⾏调度的,那么它就会⽴⻢抢占正在运⾏的进程,所以这个时候就会发⽣ CPU
调度。
那第
2
种状态通常是时间⽚到的情况,因为时间⽚到了就会发⽣中断,于是就会抢占正在运⾏的进程,从⽽占⽤ CPU
。
调度算法影响的是等待时间(进程在就绪队列中等待调度的时间总和),⽽不能影响进程真在使⽤
CPU
的时间和 I/O
时间。
接下来,说说常⻅的调度算法:
- 先来先服务调度算法
- 最短作业优先调度算法
- ⾼响应⽐优先调度算法
- 时间⽚轮转调度算法
- 最⾼优先级调度算法
- 多级反馈队列调度算法
先来先服务调度算法
最简单的⼀个调度算法,就是⾮抢占式的
先来先服务(
First Come First Severd, FCFS
)算法
了。

顾名思义,先来后到,
每次从就绪队列选择最先进入队列的进程,然后⼀直运⾏,直到进程退出或被阻
塞,才会继续从队列中选择第⼀个进程接着运行。
这似乎很公平,但是当⼀个⻓作业先运⾏了,那么后⾯的短作业等待的时间就会很⻓,不利于短作业。
FCFS
对⻓作业有利,适⽤于
CPU
繁忙型作业的系统,⽽不适⽤于
I/O
繁忙型作业的系统
最短作业优先调度算法
最短作业优先(
Shortest Job First, SJF
)调度算法
同样也是顾名思义,它会
优先选择运⾏时间最短的进
程来运⾏
,这有助于提⾼系统的吞吐量。

这显然对⻓作业不利,很容易造成⼀种极端现象。
⽐如,⼀个⻓作业在就绪队列等待运⾏,⽽这个就绪队列有⾮常多的短作业,那么就会使得⻓作业不断的往后推,周转时间变⻓,致使⻓作业⻓期不会被运⾏。
高响应比优先调度算法
前⾯的「先来先服务调度算法」和「最短作业优先调度算法」都没有很好的权衡短作业和⻓作业。
那么,
高响应比优先
(
Highest Response Ratio Next, HRRN
)调度算法
主要是权衡了短作业和⻓作业。
每次进⾏进程调度时,先计算「响应⽐优先级」,然后把「响应⽐优先级」最⾼的进程投⼊运⾏
,「响应⽐优先级」的计算公式:

从上⾯的公式,可以发现:
- 如果两个进程的「等待时间」相同时,「要求的服务时间」越短,「响应⽐」就越⾼,这样短作业的进程容易被选中运⾏;
- 如果两个进程「要求的服务时间」相同时,「等待时间」越⻓,「响应⽐」就越⾼,这就兼顾到了⻓作业进程,因为进程的响应⽐可以随时间等待的增加⽽提⾼,当其等待时间⾜够⻓时,其响应⽐便可以升到很⾼,从⽽获得运⾏的机会;
时间片轮转调度算法
最古⽼、最简单、最公平且使⽤最⼴的算法就是
时间⽚轮转(
Round Robin, RR
)调度算法
。

每个进程被分配⼀个时间段,称为时间⽚(
Quantum
),即允许该进程在该时间段中运行。
- 如果时间⽚⽤完,进程还在运⾏,那么将会把此进程从 CPU 释放出来,并把 CPU 分配另外⼀个进程;
- 如果该进程在时间⽚结束前阻塞或结束,则 CPU ⽴即进⾏切换;
另外,时间⽚的⻓度就是⼀个很关键的点:
- 如果时间⽚设得太短会导致过多的进程上下⽂切换,降低了 CPU 效率;
- 如果设得太⻓⼜可能引起对短作业进程的响应时间变⻓。
通常时间⽚设为
20ms~50ms
通常是⼀个⽐较合理的折中值。
最高优先级调度算法
前⾯的「时间⽚轮转算法」做了个假设,即让所有的进程同等重要,也不偏袒谁,⼤家的运⾏时间都⼀样。
但是,对于多⽤户计算机系统就有不同的看法了,它们希望调度是有优先级的,即希望调度程序能
从就绪
队列中选择最⾼优先级的进程进⾏运⾏,这称为最⾼优先级(
Highest Priority First,
HPF
)调度算法
。
进程的优先级可以分为,静态优先级或动态优先级:
- 静态优先级:创建进程时候,就已经确定了优先级了,然后整个运⾏时间优先级都不会变化;
- 动态优先级:根据进程的动态变化调整优先级,⽐如如果进程运⾏时间增加,则降低其优先级,如果进程等待时间(就绪队列的等待时间)增加,则升⾼其优先级,也就是随着时间的推移增加等待进程的优先级。
该算法也有两种处理优先级⾼的⽅法,⾮抢占式和抢占式:
- ⾮抢占式:当就绪队列中出现优先级⾼的进程,运⾏完当前进程,再选择优先级⾼的进程。
- 抢占式:当就绪队列中出现优先级⾼的进程,当前进程挂起,调度优先级⾼的进程运⾏。
但是依然有缺点,可能会导致低优先级的进程永远不会运⾏。
多级反馈队列调度算法
多级反馈队列(
Multilevel Feedback Queue
)调度算法
是「时间⽚轮转算法」和「最⾼优先级算法」的综合和发展。
顾名思义:
- 「多级」表示有多个队列,每个队列优先级从⾼到低,同时优先级越⾼时间⽚越短。
- 「反馈」表示如果有新的进程加⼊优先级⾼的队列时,⽴刻停⽌当前正在运⾏的进程,转⽽去运⾏优先级⾼的队列;
来看看,它是如何⼯作的:
- 设置了多个队列,赋予每个队列不同的优先级,每个队列优先级从高到低,同时优先级越高时间⽚越短;
- 新的进程会被放⼊到第⼀级队列的末尾,按先来先服务的原则排队等待被调度,如果在第⼀级队列规定的时间⽚没运⾏完成,则将其转⼊到第⼆级队列的末尾,以此类推,直⾄完成;
- 当较⾼优先级的队列为空,才调度较低优先级的队列中的进程运⾏。如果进程运⾏时,有新进程进⼊较⾼优先级的队列,则停⽌当前运⾏的进程并将其移⼊到原队列末尾,接着让较⾼优先级的进程运⾏;
可以发现,对于短作业可能可以在第⼀级队列很快被处理完。对于⻓作业,如果在第⼀级队列处理不完, 可以移⼊下次队列等待被执⾏,虽然等待的时间变⻓了,但是运⾏时间也会更⻓了,所以该算法很好的
兼
顾了⻓短作业,同时有较好的响应时间。
内存页面置换算法
在了解内存⻚⾯置换算法前,我们得先谈⼀下
缺⻚异常(缺⻚中断)
。
当
CPU
访问的⻚⾯不在物理内存时,便会产⽣⼀个缺⻚中断,请求操作系统将所缺⻚调⼊到物理内存。那
它与⼀般中断的主要区别在于:
- 缺⻚中断在指令执⾏「期间」产⽣和处理中断信号,⽽⼀般中断在⼀条指令执⾏「完成」后检查和处理中断信号。
- 缺⻚中断返回到该指令的开始重新执⾏「该指令」,⽽⼀般中断返回回到该指令的「下⼀个指令」执⾏。
我们来看⼀下缺⻚中断的处理流程,如下图:

1.在
CPU
⾥访问⼀条
Load M
指令,然后
CPU
会去找
M
所对应的⻚表项。
2.
如果该⻚表项的状态位是「有效的」,那
CPU
就可以直接去访问物理内存了,如果状态位是「⽆效的」,则 CPU
则会发送缺⻚中断请求。
3.
操作系统收到了缺⻚中断,则会执⾏缺⻚中断处理函数,先会查找该⻚⾯在磁盘中的⻚⾯的位置。
4.
找到磁盘中对应的⻚⾯后,需要把该⻚⾯换⼊到物理内存中,但是在换⼊前,需要在物理内存中找空
闲⻚,如果找到空闲⻚,就把⻚⾯换⼊到物理内存中。
5.
⻚⾯从磁盘换⼊到物理内存完成后,则把⻚表项中的状态位修改为「有效的」。
6.
最后,
CPU
重新执⾏导致缺⻚异常的指令。
上⾯所说的过程,第
4
步是能在物理内存找到空闲⻚的情况,那如果找不到呢?
找不到空闲⻚的话,就说明此时内存已满了,这时候,就需要「⻚⾯置换算法」选择⼀个物理⻚,如果该物理⻚有被修改过(脏⻚),则把它换出到磁盘,然后把该被置换出去的⻚表项的状态改成「⽆效的」, 最后把正在访问的⻚⾯装⼊到这个物理⻚中。
这⾥提⼀下,⻚表项通常有如下图的字段:

那其中:
- 状态位:⽤于表示该⻚是否有效,也就是说是否在物理内存中,供程序访问时参考。
- 访问字段:⽤于记录该⻚在⼀段时间被访问的次数,供⻚⾯置换算法选择出⻚⾯时参考。
- 修改位:表示该⻚在调⼊内存后是否有被修改过,由于内存中的每⼀⻚都在磁盘上保留⼀份副本,因此,如果没有修改,在置换该⻚时就不需要将该⻚写回到磁盘上,以减少系统的开销;如果已经被修改,则将该⻚重写到磁盘上,以保证磁盘中所保留的始终是最新的副本。
- 硬盘地址:⽤于指出该⻚在硬盘上的地址,通常是物理块号,供调⼊该⻚时使⽤。
这⾥我整理了虚拟内存的管理整个流程,你可以从下⾯这张图看到:

所以,⻚⾯置换算法的功能是,
当出现缺⻚异常,需调⼊新⻚⾯⽽内存已满时,选择被置换的物理⻚⾯
,也就是说选择⼀个物理⻚⾯换出到磁盘,然后把需要访问的⻚⾯换⼊到物理⻚。
那其算法⽬标则是,尽可能减少⻚⾯的换⼊换出的次数,常⻅的⻚⾯置换算法有如下⼏种:
- 最佳⻚⾯置换算法(OPT)
- 先进先出置换算法(FIFO)
- 最近最久未使⽤的置换算法(LRU)
- 时钟⻚⾯置换算法(Lock)
- 最不常⽤置换算法(LFU)
最佳页面置换算法
最佳⻚⾯置换算法基本思路是,
置换在「未来」最长时间不访问的页面
。
所以,该算法实现需要计算内存中每个逻辑⻚⾯的「下⼀次」访问时间,然后⽐较,选择未来最⻓时间不访问的页面。
我们举个例⼦,假设⼀开始有
3
个空闲的物理⻚,然后有请求的⻚⾯序列,那它的置换过程如下图:

在这个请求的⻚⾯序列中,缺⻚共发⽣了
7
次(空闲⻚换⼊
3
次
+
最优⻚⾯置换
4
次),⻚⾯置换共发 ⽣了
4
次。
这很理想,但是实际系统中⽆法实现,因为程序访问⻚⾯时是动态的,我们是⽆法预知每个⻚⾯在「下⼀次」访问前的等待时间。
所以,最佳⻚⾯置换算法作⽤是为了衡量你的算法的效率,你的算法效率越接近该算法的效率,那么说明你的算法是⾼效的。
先进先出置换算法
既然我们⽆法预知⻚⾯在下⼀次访问前所需的等待时间,那我们可以
选择在内存驻留时间很长的页面进行
置换
,这个就是「先进先出置换」算法的思想。
还是以前⾯的请求的⻚⾯序列作为例⼦,假设使⽤先进先出置换算法,则过程如下图:
在这个请求的⻚⾯序列中,缺⻚共发⽣了
10
次,⻚⾯置换共发⽣了
7
次,跟最佳⻚⾯置换算法⽐较起来,性能明显差了很多。
最近最久未使用的置换算法
最近最久未使⽤(
LRU
)的置换算法的基本思路是,发⽣缺⻚时,
选择最⻓时间没有被访问的⻚⾯进⾏置
换
,也就是说,该算法假设已经很久没有使⽤的⻚⾯很有可能在未来较⻓的⼀段时间内仍然不会被使⽤。
这种算法近似最优置换算法,最优置换算法是通过「未来」的使⽤情况来推测要淘汰的⻚⾯,⽽
LRU
则是通过「历史」的使⽤情况来推测要淘汰的⻚⾯。
还是以前⾯的请求的页面序列作为例⼦,假设使⽤最近最久未使⽤的置换算法,则过程如下图:
在这个请求的⻚⾯序列中,缺⻚共发⽣了
9
次,⻚⾯置换共发⽣了
6
次,跟先进先出置换算法⽐较起来,性能提⾼了⼀些。
虽然
LRU
在理论上是可以实现的,但代价很⾼。为了完全实现
LRU
,需要在内存中维护⼀个所有⻚⾯的链表,最近最多使⽤的⻚⾯在表头,最近最少使⽤的⻚⾯在表尾。
困难的是,在每次访问内存时都必须要更新「整个链表」。在链表中找到⼀个⻚⾯,删除它,然后把它移动到表头是⼀个⾮常费时的操作。
所以,
LRU
虽然看上去不错,但是由于开销⽐较⼤,实际应⽤中⽐较少使⽤。
时钟页面置换算法
那有没有⼀种即能优化置换的次数,也能⽅便实现的算法呢?
时钟⻚⾯置换算法就可以两者兼得,它跟
LRU
近似,⼜是对
FIFO
的⼀种改进。
该算法的思路是,把所有的⻚⾯都保存在⼀个类似钟⾯的「环形链表」中,⼀个表针指向最⽼的⻚⾯。
当发⽣缺⻚中断时,算法⾸先检查表针指向的⻚⾯:
- 如果它的访问位位是 0 就淘汰该⻚⾯,并把新的⻚⾯插⼊这个位置,然后把表针前移⼀个位置;
- 如果访问位是 1 就清除访问位,并把表针前移⼀个位置,重复这个过程直到找到了⼀个访问位为 0 的⻚⾯为⽌;
我画了⼀副时钟⻚⾯置换算法的⼯作流程图,你可以在下⽅看到:

了解了这个算法的工作方式,就明白为什么它被称为时钟(
Clock)算法了。
最不常用算法
最不常⽤(
LFU
)算法,这名字听起来很调⽪,但是它的意思不是指这个算法不常⽤,⽽是
当发⽣缺页中
断时,选择「访问次数」最少的那个页面,并将其淘汰
。
它的实现⽅式是,对每个⻚⾯设置⼀个「访问计数器」,每当⼀个⻚⾯被访问时,该⻚⾯的访问计数器就累加 1
。在发⽣缺⻚中断时,淘汰计数器值最⼩的那个⻚⾯。
看起来很简单,每个⻚⾯加⼀个计数器就可以实现了,但是在操作系统中实现的时候,我们需要考虑效率和硬件成本的。
要增加⼀个计数器来实现,这个硬件成本是⽐较⾼的,另外如果要对这个计数器查找哪个⻚⾯访问次数最⼩,查找链表本身,如果链表⻓度很⼤,是⾮常耗时的,效率不⾼。
但还有个问题,
LFU
算法只考虑了频率问题,没考虑时间的问题,⽐如有些⻚⾯在过去时间⾥访问的频率很⾼,但是现在已经没有访问了,⽽当前频繁访问的⻚⾯由于没有这些⻚⾯访问的次数⾼,在发⽣缺⻚中断时,就会可能会误伤当前刚开始频繁访问,但访问次数还不⾼的⻚⾯。
那这个问题的解决的办法还是有的,可以定期减少访问的次数,⽐如当发⽣时间中断时,把过去时间访问的⻚⾯的访问次数除以 2
,也就说,随着时间的流失,以前的⾼访问次数的⻚⾯会慢慢减少,相当于加⼤了被置换的概率。
磁盘调度算法
我们来看看磁盘的结构,如下图:

常⻅的机械磁盘是上图左边的样⼦,中间圆的部分是磁盘的盘⽚,⼀般会有多个盘⽚,每个盘⾯都有⾃⼰的磁头。右边的图就是⼀个盘⽚的结构,盘⽚中的每⼀层分为多个磁道,每个磁道分多个扇区,每个扇区是
512
字节。那么,多个具有相同编号的磁道形成⼀个圆柱,称之为磁盘的柱⾯,如上图⾥中间的样⼦。
磁盘调度算法的⽬的很简单,就是为了提⾼磁盘的访问性能,⼀般是通过优化磁盘的访问请求顺序来做到的。
寻道的时间是磁盘访问最耗时的部分,如果请求顺序优化的得当,必然可以节省⼀些不必要的寻道时间, 从⽽提⾼磁盘的访问性能。
假设有下⾯⼀个请求序列,每个数字代表磁道的位置:
98
,
183
,
37
,
122
,
14
,
124
,
65
,
67
初始磁头当前的位置是在第
53
磁道。
接下来,分别对以上的序列,作为每个调度算法的例⼦,那常⻅的磁盘调度算法有:
- 先来先服务算法
- 最短寻道时间优先算法
- 扫描算法算法
- 循环扫描算法
- LOOK 与 C-LOOK 算法
先来先服务
先来先服务(
First-Come
,
First-Served
,
FCFS
),顾名思义,先到来的请求,先被服务。
那按照这个序列的话:
98
,
183
,
37
,
122
,
14
,
124
,
65
,
67
那么,磁盘的写⼊顺序是从左到右,如下图:
先来先服务算法总共移动了
640
个磁道的距离,这么⼀看这种算法,⽐较简单粗暴,但是如果⼤量进程竞争使⽤磁盘,请求访问的磁道可能会很分散,那先来先服务算法在性能上就会显得很差,因为寻道时间过⻓。
最短寻道时间优先
最短寻道时间优先(
Shortest Seek First
,
SSF
)算法的⼯作⽅式是,优先选择从当前磁头位置所需寻道时间最短的请求,还是以这个序列为例⼦:
98
,
183
,
37
,
122
,
14
,
124
,
65
,
67
那么,那么根据距离磁头(
53
位置)最近的请求的算法,具体的请求则会是下列从左到右的顺序:
65
,
67
,
37
,
14
,
98
,
122
,
124
,
183
磁头移动的总距离是
236
磁道,相⽐先来先服务性能提⾼了不少。
但这个算法可能存在某些请求的
饥饿
,因为本次例⼦我们是静态的序列,看不出问题,假设是⼀个动态的请求,如果后续来的请求都是⼩于 183磁道的,那么 183
磁道可能永远不会被响应,于是就产⽣了饥饿现象,这⾥
产⽣饥饿的原因是磁头在一小
块区域来回移动
。
扫描算法
最短寻道时间优先算法会产⽣饥饿的原因在于:磁头有可能再⼀个⼩区域内来回得移动。
为了防⽌这个问题,可以规定:
磁头在⼀个⽅向上移动,访问所有未完成的请求,直到磁头到达该方向上
的最后的磁道,才调换⽅向,这就是扫描(
Scan
)算法
。
这种算法也叫做电梯算法,⽐如电梯保持按⼀个⽅向移动,直到在那个⽅向上没有请求为⽌,然后改变⽅向。
还是以这个序列为例⼦,磁头的初始位置是
53
:
98
,
183
,
37
,
122
,
14
,
124
,
65
,
67
那么,假设扫描调度算先朝磁道号减少的⽅向移动,具体请求则会是下列从左到右的顺序:
37
,
14
,
0
,
65
,
67
,
98
,
122
,
124
,
183

磁头先响应左边的请求,直到到达最左端(
0
磁道)后,才开始反向移动,响应右边的请求。
扫描调度算法性能较好,不会产⽣饥饿现象,但是存在这样的问题,中间部分的磁道会⽐较占便宜,中间部分相⽐其他部分响应的频率会⽐较多,也就是说每个磁道的响应频率存在差异。
循环扫描算法
扫描算法使得每个磁道响应的频率存在差异,那么要优化这个问题的话,可以总是按相同的⽅向进⾏扫描,使得每个磁道的响应频率基本⼀致。
循环扫描(
Circular Scan, CSCAN
)规定:只有磁头朝某个特定⽅向移动时,才处理磁道访问请求,⽽返回时直接快速移动⾄最靠边缘的磁道,也就是复位磁头,这个过程是很快的,并且
返回中途不处理任何请
求
,该算法的特点,就是
磁道只响应⼀个方向上的请求
。
还是以这个序列为例⼦,磁头的初始位置是
53
:
98
,
183
,
37
,
122
,
14
,
124
,
65
,
67
那么,假设循环扫描调度算先朝磁道增加的⽅向移动,具体请求会是下列从左到右的顺序:
65
,
67
,
98
,
122
,
124
,
183
,
199
,
0
,
14
,
37
磁头先响应了右边的请求,直到碰到了最右端的磁道
199
,就⽴即回到磁盘的开始处(磁道
0
),但这个返回的途中是不响应任何请求的,直到到达最开始的磁道后,才继续顺序响应右边的请求。
循环扫描算法相⽐于扫描算法,对于各个位置磁道响应频率相对⽐较平均。
LOOK与C-LOOK算法
我们前⾯说到的扫描算法和循环扫描算法,都是磁头移动到磁盘「最始端或最末端」才开始调换⽅向。
那这其实是可以优化的,优化的思路就是
磁头在移动到「最远的请求」位置,然后⽴即反向移动。
那针对
SCAN
算法的优化则叫
LOOK
算法,它的⼯作⽅式,磁头在每个⽅向上仅仅移动到最远的请求位置,然后⽴即反向移动,⽽不需要移动到磁盘的最始端或最末端,
反向移动的途中会响应请求
。
⽽针 C-SCAN 算法的优化则叫 C-LOOK,它的⼯作⽅式,磁头在每个⽅向上仅仅移动到最远的请求位置, 然后⽴即反向移动,⽽不需要移动到磁盘的最始端或最末端,反向移动的途中不会响应请求。