《操作系统导论》第7章 | 进程调度

探讨可能的调度策略之前,我们先做一些简化假设。这些假设与系统中运行的进程有关,有时候统称为工作负载(workload)。确定工作负载是构建调度策略的关键部分。这里做的工作负载假设是不切实际的,但我们会逐渐放宽这些假定,并最终开发出一个完全可操作的调度准则。

我们对操作系统中运行的进程(有时也叫工作任务)做出如下的假设:

  1. 每一个工作运行相同的时间
  2. 所有的工作同时到达
  3. 一旦开始,每个工作保持运行直到完成
  4. 所有的工作只使用CPU(即没有I/O操作)
  5. 每个工作的运行时间是已知的

评价指标

除了做出工作负载假设之外,还需要一个东西能让我们比较不同的调度策略:调度指标。任务的周转时间(T_{turnaround})定义为任务完成时间(T_{completion})减去任务到达系统的时间(T_{arrival})。更正式的周转时间定义为:

[T_{turnaround} = T_{completion} - T_{arrival} ]

注意到,周转时间是一个性能指标,而另一个有趣的指标是公平。性能和公平在调度系统中往往是矛盾的。例如,调度程序可以优化性能,但代价是以阻止一些任务运行,这就降低了公平。

先进先出(FIFO)

最基本的算法被称为先进先出(First In First Out)调度,有时候也称为先到先服务(First Come First Served)。它很简单,易于实现,而且对于目前的假设效果不错。

假设3个工作A、B和C在大致相同的时间到达系统。因为FIFO必须将某个工作放在前面,所以我们假设当它们都同时到达时,A比B早一点点,然后B比C早到达一点点。假设每个工作运行10s,从图上可以看出,A在10s时完成,B在20s时完成,C在30s时完成。因此,这3个任务的平均周转时间就是((10 + 20 + 30)/ 3 = 20s)

现在,我们放宽假设1,每个任务的运行时间不再相同。这种情况下FIFO的表现如何?具体来说,我们再次假设3个任务(A、B和C),但这次A运行100s,而B和C运行10s。

如图所示,A先运行100s,B或C才有机会运行。因此,系统的平均周转时间是比较高的((100 + 110 + 120)/ 3 = 110s)。这个问题通常被称为护航效应(convoy effect),一些耗时较少的潜在资源消费者被排在重量级的资源消费者之后。

最短任务优先(SJF)

事实上,我们一个非常简单的方法来解决护航问题。这个新的调度准则被称为最短任务优先(Shortest Job First,SJF),即先运行最短的任务,然后是次短的任务,以此类推。

还是上面的例子,但这次以SJF作为调度策略。上图展示的是运行A、B和C的结果。通过在A之前运行B和C,SJF将平均周转时间从110s降低到((10 + 20 + 120)/3 = 50s)。在所有工作同时到达的情况下,可以证明SJF是一个最有调度算法。

现在,让我们放宽假设2,即工作可以随时到达,而不是同时到达。假设A在t=0时到达,且需要运行100s。而B和C在t=10时到达,且各需要运行10s。使用SJF,可以得到如下所示的调度。

从图中可以看出,即使B和C在A之后不久到达,它们仍然被迫等到A完成,从而遭遇同样的护航问题。这3项工作的平均周转时间为((100+(110−10)+(120−10))/3=103.33s)

最短完成时间优先(STCF)

为了解决这个问题,需要放宽假设3(工作必须保持运行直到完成)。也就是说,当B和C到达时,调度程序可以做其他事情:它可以抢占(preempt)工作A,并决定运行另一个工作,或许稍后继续工作A。根据我们的定义,SJF是一种非抢占式(non-preemptive)调度程序,因此还是存在护航问题。

通过向SJF添加抢占,我们可以得到最短完成时间优先(Shortest Time-to-Completion First,STCF)调度程序。每当新工作进入系统时,它就会确定剩余工作和新工作中,谁的剩余时间最少,然后调度该工作。因此,在我们的例子中,STCF将抢占A并运行B和C以完成。只有在它们完成后,才能调度A的剩余时间。

根据上图,3项工作的平均周转时间是(((20-10)+(30-10)+120) / 3 = 50s)。基于新的假设,可证明STCF是最优的。

响应时间

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

[T_{response} = T_{firsturn} - T_{arrival} ]

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

轮转(RR)

为了提高响应时间,我们将引入轮转(Round-Robin,RR)调度。其基本思想很简单:在一个时间片内运行一个工作,然后切换到运行队列中的下一个任务,而不是运行一个任务直到结束,它反复执行,直到所有任务完成。注意,时间片长度必须是时钟中断周期的倍数。因此,如果时钟中断是每10ms中断一次,则时间片可以是10ms、20ms或10ms的任何其他倍数。

假设3个任务A、B和C在系统中同时到达,并且它们都希望运行5s。如下所示,SJF调度程序必须运行完当前任务才可运行下一个任务,其平均响应时间是((0+5+10)/3 = 5s)

相比之下,1s时间片的RR可以快速地循环工作,其平均响应时间为((0+1+2)/3=1s)

显然,时间片长度对于RR是至关重要的。时间片越短,RR在响应时间上表现越好。然而,时间片太短是有问题的:突然上下文切换的成本将影响整体性能。因此,系统设计者需要权衡时间片的长度,使其足够长,以便摊销上下文切换成本,而又不会使系统不及时响应。

结合I/O

接下来我们放宽假设4,现在所有的程序都会执行I/O。调度程序显然要在工作发起I/O请求时做出决定,因为当前正在运行的作业在I/O期间不会使用CPU,它被阻塞等待I/O完成。如果将I/O发送到硬盘驱动器,则进程可能会被阻塞几毫秒或更长时间。因此,这时调度程序应该在CPU上安排另一项工作。调度程序还必须在I/O完成时做出决定。发生这种情况时,会产生中断,操作系统运行并将发出I/O的进程从阻塞状态移回就绪状态。当然,它甚至可以决定在那个时候运行该项工作。操作系统应该如何处理每项工作?

假设有两项工作A和B,每项工作需要50ms的CPU时间。但是A先运行10ms,然后发出I/O请求(假设I/O每个都需要10ms),而B只是使用CPU 50ms,不执行I/O。调度程序先运行A,然后运行B。

毫无疑问,上图所示的调度是非常糟糕的。常见的方法是将A的每个10ms的子工作视为一项独立的工作。因此,当系统启动时,它的选择是调度10ms的A,还是50ms的B。STCF会选择较短的A。然后,A的工作已完成,只剩下B,并开始运行。然后提交A的一个新子工作,它抢占B并运行10ms。这样做可以实现重叠,一个进程在等待另一个进程的I/O完成时使用CPU,系统因此得到更好的利用。

有了应对I/O的基本方法,我们来到最后的假设5:调度程序知道每个工作的长度。如前所述,这可能是可以做出的最糟糕的假设。事实上,操作系统通常不知道每个作业的长度。因此,我们将在第8章利用最近的历史预测未来,从而解决这个问题。

原文地址:https://www.cnblogs.com/littleorange/p/12740241.html