持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第27天,点击查看活动详情
1 进程调度:介绍
1.1 工作负载假设
这些假设与系统中运行的进程有关,有时候统称为工作负载(workload)。确定工作负载是构建调度策略的关键部分。工作负载了解得越多,你的策略就越优化。、
一个完全可操作的调度准则(a fully-operational scheduling discipline。我们对操作系统中运行的进程(有时也叫工作任务)做出如下的假设:
1.每一个工作运行相同的时间。
2.所有的工作同时到达。
3.一旦开始,每个工作保持运行直到完成。
4.所有的工作只是用CPU(即它们不执行IO操作)。
5.每个工作的运行时间是已知的。
这些假设中许多是不现实的。
1.2 调度指标
任务的周转时间定义为任务完成时间减去任务到达系统的时间。
周转时间= T完成时间−T到达时间 (1.1)
1.3 先进先出(FIFO)
想象一下,3个工作A、B和C在大致相同的时间(T到达时间 = 0)到达系统。因为FIFO必须将某个工作放在前面,所以我们假设当它们都同时到达时,A比B早一点点,然后B比C早到达一点点。假设每个工作运行10s。这些工作的平均周转时间(average turnaround time)是多少?
因此,这3个任务的平均周转时间就是(10 + 20 + 30)/ 3 = 20。计算周转时间就这么简单。
1.4 最短任务优先(SJF)
最短任务优先(Shortest Job First,SJF),该名称应该很容易记住,因为它完全描述了这个策略:先运行最短的任务,然后是次短的任务,如此下去。
我们再次假设3个任务(A、B和C),但这次A运行100s,而B和C运行10s。如图7.2所示,A先运行100s,B或C才有机会运行。因此,系统的平均周转时间是比较高的:令人不快的110s((100 + 110 + 120)/ 3 = 110)。
几乎所有现代化的调度程序都是抢占式的(preemptive),非常愿意停止一个进程以运行另一个进程。这意味着调度程序采用了我们之前学习的机制。特别是调度程序可以进行上下文切换,临时停止一个运行进程,并恢复(或启动)另一个进程。
因此,我们找到了一个用SJF进行调度的好方法,但是我们的假设仍然是不切实际的。让我们放宽另一个假设。具体来说,我们可以针对假设2,现在假设工作可以随时到达,而不是同时到达。这导致了什么问题?
7.5 最短完成时间优先(STCF)
向SJF添加抢占,称为最短完成时间优先(Shortest Time-to-Completion First,STCF)或抢占式最短作业优先(Preemptive Shortest Job First ,PSJF)调度程序。
结果是平均周转时间大大提高:50s(……)。
1.6 新度量指标:响应时间
对于许多早期批处理系统,这些类型的调度算法有一定的意义。然而,引入分时系统改变了这一切。现在,用户将会坐在终端前面,同时也要求系统的交互性好。因此,一个新的度量标准诞生了:响应时间(response time)。
响应时间定义为从任务到达系统到首次运行的时间。
更正式的定义是:T响应时间= T首次运行−T到达时间
例如,如果我们有上面的调度(A在时间0到达,B和C在时间10达到),每个作业的响应时间如下:作业A为0,B为0,C为10(平均:3.33)。你可能会想,STCF和相关方法在响应时间上并不是很好。例如,如果3个工作同时到达,第三个工作必须等待前两个工作全部运行后才能运行。这种方法虽然有很好的周转时间,但对于响应时间和交互性是相当糟糕的。假设你在终端前输入,不得不等待10s才能看到系统的回应
1.7 轮转
为了解决这个问题,我们将介绍一种新的调度算法,通常被称为轮转(Round-Robin,RR)调度[K64]。基本思想很简单:RR在一个时间片(time slice,有时称为调度量子,scheduling quantum)内运行一个工作,然后切换到运行队列中的下一个任务,而不是运行一个任务直到结束。它反复执行,直到所有任务完成。因此,RR有时被称为时间切片(time-slicing)。请注意,时间片长度必须是时钟中断周期的倍数。因此,如果时钟中断是每10ms中断一次,则时间片可以是10ms、20ms或10ms的任何其他倍数。
RR的平均响应时间是:(0 + 1 + 2)/3 = 1; SJF 算法平均响应时间是:(0 + 5+ 10)/ 3 = 5。
事实上,这是固有的权衡:如果你愿意不公平,你可以运行较短的工作直到完成,但是要以响应时间为代价。如果你重视公平性,则响应时间会较短,但会以周转时间为代价。这种权衡在系统中很常见。
提示:重叠可以提高利用率 如有可能,重叠(overlap)操作可以最大限度地提高系统的利用率。重叠在许多不同的领域很有用,包括执行磁盘I/O或将消息发送到远程机器时。在任何一种情况下,开始操作然后切换到其他工作都是一个好主意,这也提高了系统的整体利用率和效率。
1.8 结合I/O
首先,我们将放宽假设4:当然所有程序都执行I/O。
调度程序显然要在工作发起I/O请求时做出决定,因为当前正在运行的作业在I/O期间不会使用CPU,它被阻塞等待I/O完成。如果将I/O发送到硬盘驱动器,则进程可能会被阻塞几毫秒或更长时间,具体取决于驱动器当前的I/O负载。因此,这时调度程序应该在CPU上安排另一项工作。
调度程序还必须在I/O完成时做出决定。发生这种情况时,会产生中断,操作系统运行并将发出I/O的进程从阻塞状态移回就绪状态。当然,它甚至可以决定在那个时候运行该项工作。操作系统应该如何处理每项工作?
一种常见的方法是将A的每个10ms的子工作视为一项独立的工作。因此,当系统启动时,它的选择是调度10ms的A,还是50ms的B。对于STCF,选择是明确的:选择较短的一个,在这种情况下是A。然后,A的工作已完成,只剩下B,并开始运行。然后提交A的一个新子工作,它抢占B并运行10ms。这样做可以实现重叠(overlap),一个进程在等待另一个进程的I/O完成时使用CPU,系统因此得到更好的利用(见图)。
有了应对I/O的基本方法,我们来到最后的假设:调度程序知道每个工作的长度。如前所述,这可能是可以做出的最糟糕的假设。事实上,在一个通用的操作系统中(比如我们所关心的操作系统),操作系统通常对每个作业的长度知之甚少。因此,我们如何建立一个没有这种先验知识的SJF/STCF?更进一步,我们如何能够将已经看到的一些想法与RR调度程序结合起来,以便响应时间也变得相当不错?
我们介绍了调度的基本思想,并开发了两类方法。第一类是运行最短的工作,从而优化周转时间。第二类是交替运行所有工作,从而优化响应时间。但很难做到“鱼与熊掌兼得”,这是系统中常见的、固有的折中。我们也看到了如何将I/O结合到场景中,但仍未解决操作系统根本无法看到未来的问题。稍后,我们将看到如何通过构建一个调度程序,利用最近的历史预测未来,从而解决这个问题。这个调度程序称为多级反馈队列,
2 调度:多级反馈队列
本章将介绍一种著名的调度方法——多级反馈队列(Multi-level Feedback Queue,MLFQ)。
多级反馈队列需要解决两方面的问题。首先,它要优化周转时间。在第7章中我们看到,这通过先执行短工作来实现。然而,操作系统通常不知道工作要运行多久,而这又是SJF(或STCF)等算法所必需的。其次,MLFQ希望给交互用户(如用户坐在屏幕前,等着进程结束)很好的交互体验,因此需要降低响应时间。然而,像轮转这样的算法虽然降低了响应时间,周转时间却很差。
多级反馈队列是用历史经验预测未来的一个典型的例子,操作系统中有很多地方采用了这种技术(同样存在于计算机科学领域的很多其他地方,比如硬件的分支预测及缓存算法)。
2.1 MLFQ:基本规则
因此,MLFQ调度策略的关键在于如何设置优先级。MLFQ没有为每个工作指定不变的优先情绪而已,而是根据观察到的行为调整它的优先级。例如,如果一个工作不断放弃CPU去等待键盘输入,这是交互型进程的可能行为,MLFQ因此会让它保持高优先级。相反,如果一个工作长时间地占用CPU,MLFQ会降低其优先级。通过这种方式,MLFQ在进程运行过程中学习其行为,从而利用工作的历史来预测它未来的行为。
● 规则1:如果A的优先级 > B的优先级,运行A(不运行B)。 ● 规则2:如果A的优先级 = B的优先级,轮转运行A和B。
2.2 尝试1:如何改变优先级
要做到这一点,我们必须记得工作负载:既有运行时间很短、频繁放弃CPU的交互型工作,也有需要很多CPU时间、响应时间却不重要的长时间计算密集型工作。
● 规则3:工作进入系统时,放在最高优先级(最上层队列)。 ● 规则4a:工作用完整个时间片后,降低其优先级(移入下一个队列)。 ● 规则4b:如果工作在其时间片以内主动释放CPU,则优先级不变。
实例1:单个长工作
实例2:来了一个短工作
如果不知道工作是短工作还是长工作,那么就在开始的时候假设其是短工作,并赋予最高优先级。如果确实是短工作,则很快会执行完毕,否则将被慢慢移入低优先级队列,而这时该工作也被认为是长工作了。通过这种方式,MLFQ近似于SJF。
实例3:如果有I/O呢
如果进程在时间片用完之前主动放弃CPU,则保持它的优先级不变。这条规则的意图很简单:假设交互型工作中有大量的I/O操作(比如等待用户的键盘或鼠标输入),它会在时间片用完之前放弃CPU。在这种情况下,我们不想处罚它,只是保持它的优先级不变。
一个交互型工作
混合I/O密集型和CPU密集型工作负载
当前MLFQ的一些问题
如果系统有“太多”交互型工作,就会不断占用CPU,导致长工作永远无法得到CPU
进程在时间片用完之前,调用一个I/O操作(比如访问一个无关的文件),从而主动释放CPU。如此便可以保持在高优先级,占用更多的CPU时间。做得好时(比如,每运行99%的时间片时间就主动放弃一次CPU),工作可以几乎独占CPU
2.3 尝试2:提升优先级
● 规则5:经过一段时间S,就将系统中所有工作重新加入最高优先级队列。
新规则一下解决了两个问题。首先,进程不会饿死——在最高优先级队列中,它会以轮转的方式,与其他高优先级工作分享CPU,从而最终获得执行。其次,如果一个CPU密集型工作变成了交互型,当它优先级提升时,调度程序会正确对待它。
不采用优先级提升(左)和采用(右)
右边每50ms就有一次优先级提升(这里只是举例,这个值可能过小),因此至少保证长工作会有一些进展,每过50ms就被提升到最高优先级,从而定期获得执行。
添加时间段S导致了明显的问题:S的值应该如何设置?德高望重的系统研究员 John Ousterhout[O11]曾将这种值称为“巫毒常量(voo-doo constant)”,因为似乎需要一些黑魔法才能正确设置。如果S设置得太高,长工作会饥饿;如果设置得太低,交互型工作又得不到合适的CPU时间比例。
2.4 尝试3:更好的计时方式
现在还有一个问题要解决:如何阻止调度程序被愚弄?可以看出,这里的元凶是规则4a和4b,导致工作在时间片以内释放CPU,就保留它的优先级。那么应该怎么做?
这里的解决方案,是为MLFQ的每层队列提供更完善的CPU计时方式(accounting)。调度程序应该记录一个进程在某一层中消耗的总时间,而不是在调度时重新计时。只要进程用完了自己的配额,就将它降到低一优先级的队列中去。不论它是一次用完的,还是拆成很多次用完。因此,我们重写规则4a和4b。
规则4:一旦工作用完了其在某一层中的时间配额(无论中间主动放弃了多少次CPU),就降低其优先级(移入低一级队列)。
没有规则4的保护时,进程可以在每个时间片结束前发起一次I/O操作,从而垄断CPU时间。有了这样的保护后,不论进程的I/O行为如何,都会慢慢地降低优先级,因而无法获得超过公平的CPU时间比例。
2.5 MLFQ调优及其他问题
其中一个大问题是如何配置一个调度程序,例如,配置多少队列?每一层队列的时间片配置多大?为了避免饥饿问题以及进程行为改变,应该多久提升一次进程的优先级?这些问题都没有显而易见的答案,因此只有利用对工作负载的经验,以及后续对调度程序的调优,才会导致令人满意的平衡。
大多数的MLFQ变体都支持不同队列可变的时间片长度。高优先级队列通常只有较短的时间片(比如10ms或者更少),因而这一层的交互工作可以更快地切换。相反,低优先级队列中更多的是CPU密集型工作,配置更长的时间片会取得更好的效果。图8.7展示了一个例子,两个长工作在高优先级队列执行10ms,中间队列执行20ms,最后在最低优先级队列执行40ms。
Solaris的MLFQ实现(时分调度类TS)很容易配置。它提供了一组表来决定进程在其生命周期中如何调整优先级,每层的时间片多大,以及多久提升一个工作的优先级。管理员可以通过这些表,让调度程序的行为方式不同。该表默认有60层队列,时间片长度从20ms(最高优先级),到几百ms(最低优先级),每一秒左右提升一次进程的优先级。
优先级越低,时间片越长
MLFQ规则
● 规则1:如果A的优先级 > B的优先级,运行A(不运行B)。 ● 规则2:如果A的优先级 = B的优先级,轮转运行A和B。 ● 规则3:工作进入系统时,放在最高优先级(最上层队列)。 ● 规则 4:一旦工作用完了其在某一层中的时间配额(无论中间主动放弃了多少次CPU),就降低其优先级(移入低一级队列)。 ● 规则5:经过一段时间S,就将系统中所有工作重新加入最高优先级队列。
参考文献:《操作系统导论》