【操作系统—虚拟化】进程调度

介绍

工作负载假设

探讨可能的策略范围之前,我们先做一些简化假设。这些假设与系统中运行的进程有关,有时候统称为工作负载(workload)。在这里我们对工作负载所做的假设是不切实际的,但将来会放宽这些假设。现在,我们对操作系统中运行的进程(有时也叫工作任务)做出如下的假设:

  1. 每一个工作运行相同的时间。
  2. 所有的工作同时到达。
  3. 一旦开始,每个工作保持运行直到完成。
  4. 所有的工作只是用CPU(即它们不执行IO操作)。
  5. 每个工作的运行时间是已知的。

调度指标

除了做出工作负载假设之外,还需要一个东西能让我们比较不同的调度策略:调度指标。指标是我们用来衡量某些东西的东西,在进程调度中,有一些不同的指标是有意义的。

现在,让我们简化一下,只用一个指标:周转时间(turnaround time)。任务的周转时间定义为任务完成时间减去任务到达系统的时间。

周转时间是一个性能(performance)指标,另一个有趣的指标是公平(fairness),性能和公平在调度系统中往往是矛盾的。

先进先出(FIFO)

我们可以实现的最基本的算法,被称为先进先出(First In First Out或FIFO)调度,有时候也称为先到先服务(First Come First Served或FCFS)。

FIFO有一些积极的特性:它很简单,而且易于实现,但是在某些情况下的性能很差。

假设我们有3个任务(A、B和C),A运行100s,而B和C运行10s。而且都几乎同时到达,A比B早一点点,然后B比C早到达一点点。此时系统的平均周转时间是比较高的:110s((100 + 110 + 120)/ 3 = 110)。

【操作系统—虚拟化】进程调度_第1张图片

这个问题通常被称为护航效应(convoy effect),一些耗时较少的潜在资源消费者被排在重量级的资源消费者之后。

最短任务优先(SJF)

事实证明,一个非常简单的方法解决了这个问题。这个新的调度准则被称为最短任务优先(ShortestJob First,SJF):先运行最短的任务,然后是次短的任务,如此下去。

事实上,考虑到所有工作同时到达的假设,我们可以证明SJF确实是一个最优(optimal)调度算法。但是我们的假设仍然是不切实际的,让我们放宽另一个假设。我们可以针对假设2,现在假设工作可以随时到达,而不是同时到达,这会导致什么问题?

举个例子,假设A在t = 0时到达,且需要运行100s。而B和C在t = 10到达,且各需要运行10s。即使B和C在A之后不久到达,它们仍然被迫等到A完成,从而遭遇同样的护航问题。

【操作系统—虚拟化】进程调度_第2张图片

最短完成时间优先(STCF)

为了解决这个问题,需要放宽假设条件(工作必须保持运行直到完成)。我们还需要调度程序本身的一些机制。鉴于我们先前关于时钟中断和上下文切换的讨论,当B和C到达时,调度程序当然可以做其他事情:它可以抢占(preempt)工作A,并决定运行另一个工作,或许稍后继续工作A。根据我们的定义,SJF是一种非抢占式(non-preemptive)调度程序,因此存在上述问题。

有一个调度程序完全就是这样做的:向SJF添加抢占,称为最短完成时间优先(ShortestTime-to-Completion First,STCF)或抢占式最短作业优先(Preemptive Shortest Job First ,PSJF)调度程序。每当新工作进入系统时,它就会确定剩余工作和新工作中,谁的剩余时间最少,然后调度该工作。因此,在我们的例子中,STCF将抢占A并运行B和C以完成。只有在它们完成后,才能调度A的剩余时间。和以前一样,考虑到我们的新假设,STCF可证明是最优的。

【操作系统—虚拟化】进程调度_第3张图片

新度量指标:响应时间

如果我们知道任务长度,而且任务只使用CPU,而我们唯一的衡量是周转时间,STCF将是一个很好的策略。然而,引入分时系统改变了这一切。现在,用户将会坐在终端前面,同时也要求系统的交互性好。因此,一个新的度量标准诞生了:响应时间(response time)。响应时间定义为从任务到达系统到首次运行的时间。

STCF和相关方法在响应时间上并不是很好。例如,如果3个工作同时到达,第三个工作必须等待前两个工作全部运行后才能运行。这种方法虽然有很好的周转时间,但对于响应时间和交互性是相当糟糕的。

轮转

为了解决这个问题,我们将介绍一种新的调度算法,通常被称为轮转(Round-Robin,RR)调度。基本思想很简单:RR在一个时间片(time slice,有时称为调度量子,schedulingquantum)内运行一个工作,然后切换到运行队列中的下一个任务,而不是运行一个任务直到结束。它反复执行,直到所有任务完成。

【操作系统—虚拟化】进程调度_第4张图片

时间片长度对于RR是至关重要的。越短,RR在响应时间上表现越好。然而,时间片太短是有问题的:突然上下文切换的成本将影响整体性能。上下文切换的成本不仅仅来自保存和恢复少量寄存器的操作系统操作,程序运行时,它们在CPU高速缓存、TLB、分支预测器和其他片上硬件中建立了大量的状态。切换到另一个工作会导致此状态被刷新,且与当前运行的作业相关的新状态被引入,这可能导致显著的性能成本。

如果响应时间是我们的唯一指标,那么带有合理时间片的RR,就会是非常好的调度程序。如果周转时间是我们的指标,那么RR却是最糟糕的策略之一。更一般地说,任何公平(fair)的政策(如RR),即在小规模的时间内将CPU均匀分配到活动进程之间,在周转时间这类指标上表现不佳。

其他因素

首先,我们得放宽假设4:因为几乎所有的程序都要执行I/O。调度程序显然要在工作发起I/O请求时做出决定,应该在CPU上安排另一项工作,调度程序还必须在I/O完成时做出决定。

有了应对I/O的基本方法,我们来看最后的假设:调度程序知道每个工作的长度。如前所述,这可能是可以做出的最糟糕的假设。事实上,在一个通用的操作系统中,系统通常对每个作业的长度知之甚少。

实现:多级反馈队列

我们将介绍一种著名的调度方法——多级反馈队列(Multi-level Feedback Queue,MLFQ)。该调度程序经过多年的一系列优化,出现在许多现代操作系统中。多级反馈队列需要解决两方面的问题。首先,它要优化周转时间。其次,MLFQ希望给交互用户(如用户坐在屏幕前,等着进程结束)很好的交互体验,因此需要降低响应时间。

基本规则

MLFQ中有许多独立的队列(queue),每个队列有不同的优先级(priority level)。任何时刻,一个工作只能存在于一个队列中。MLFQ总是优先执行较高优先级的工作(即在较高级队列中的工作)。

当然,每个队列中可能会有多个工作,因此具有同样的优先级。在这种情况下,我们就对这些工作采用轮转调度。

至此,我们得到了MLFQ的两条基本规则:

  • 规则1:如果A的优先级 > B的优先级,运行A(不运行B)。
  • 规则2:如果A的优先级 = B的优先级,轮转运行A和B。

尝试1:如何改变优先级

我们必须决定,在一个工作的生命周期中,MLFQ如何改变其优先级(在哪个队列中)。要做到这一点,我们必须记得工作负载:既有运行时间很短、频繁放弃CPU的交互型工作,也有需要很多CPU时间、响应时间却不重要的长时间计算密集型工作。下面是我们第一次尝试优先级调整算法。

  • 规则3:工作进入系统时,放在最高优先级(最上层队列)。
  • 规则4a:工作用完整个时间片后,降低其优先级(移入下一个队列)。
  • 规则4b:如果工作在其时间片以内主动释放CPU,则优先级不变。

然而,这种算法有一些非常严重的缺点。

首先,会有饥饿(starvation)问题。如果系统有“太多”交互型工作,就会不断占用CPU,导致长工作永远无法得到CPU。

其次,聪明的用户会重写程序,愚弄调度程序(game the scheduler)。愚弄调度程序指的是用一些卑鄙的手段欺骗调度程序,让它给你远超公平的资源。例如进程在时间片用完之前,调用一个I/O操作(比如访问一个无关的文件),从而主动释放CPU。如此便可以保持在高优先级,占用更多的CPU时间。

最后,一个程序可能在不同时间表现不同。一个计算密集的进程可能在某段时间表现为一个交互型的进程。用我们目前的方法,它不会享受系统中其他交互型工作的待遇。

尝试2:提升优先级

一个简单的思路是周期性地提升(boost)所有工作的优先级。可以有很多方法做到,我们就用最简单的:将所有工作扔到最高优先级队列。于是有了如下的新规则:

  • 规则5:经过一段时间S,就将系统中所有工作重新加入最高优先级队列。

新规则解决了两个问题。首先,进程不会饿死——在最高优先级队列中,它会以轮转的方式,与其他高优先级工作分享CPU,从而最终获得执行。其次,如果一个CPU密集型工作变成了交互型,当它优先级提升时,调度程序会正确对待它。

不过,添加时间段S导致了明显的问题:S的值应该如何设置?如果S设置得太高,长工作会饥饿;如果设置得太低,交互型工作又得不到合适的CPU时间比例。

尝试3:更好的计时方式

现在还有一个问题要解决:如何阻止调度程序被愚弄?可以看出,这里的元凶是规则4a和4b,导致工作在时间片以内释放CPU,就保留它的优先级。那么应该怎么做?

这里的解决方案,是为MLFQ的每层队列提供更完善的CPU计时方式(accounting)。调度程序应该记录一个进程在某一层中消耗的总时间,而不是在调度时重新计时。只要进程用完了自己的配额,就将它降到低一优先级的队列中去。

因此,我们重写规则4a和4b:

  • 规则4:一旦工作用完了其在某一层中的时间配额(无论中间主动放弃了多少次CPU),就降低其优先级(移入低一级队列)。

MLFQ调优及其他问题

关于MLFQ调度算法还有一些问题。其中一个大问题是如何配置一个调度程序,例如,配置多少队列?每一层队列的时间片配置多大?为了避免饥饿问题以及进程行为改变,应该多久提升一次进程的优先级?这些问题都没有显而易见的答案,因此只有利用对工作负载的经验,以及后续对调度程序的调优,才会导致令人满意的平衡。

例如,大多数的MLFQ变体都支持不同队列可变的时间片长度。高优先级队列通常只有较短的时间片(比如10ms或者更少),因而这一层的交互工作可以更快地切换。相反,低优先级队列中更多的是CPU密集型工作,配置更长的时间片会取得更好的效果。

你可能感兴趣的:(操作系统)