探讨可能的策略范围以前,咱们先作一些简化假设。这些假设与系统中运行的进程有关,有时候统称为工做负载(workload)。在这里咱们对工做负载所作的假设是不切实际的,但未来会放宽这些假设。如今,咱们对操做系统中运行的进程(有时也叫工做任务)作出以下的假设:算法
除了作出工做负载假设以外,还须要一个东西能让咱们比较不一样的调度策略:调度指标。指标是咱们用来衡量某些东西的东西,在进程调度中,有一些不一样的指标是有意义的。缓存
如今,让咱们简化一下,只用一个指标:周转时间(turnaround time)。任务的周转时间定义为任务完成时间减去任务到达系统的时间。性能
周转时间是一个性能(performance)指标,另外一个有趣的指标是公平(fairness),性能和公平在调度系统中每每是矛盾的。优化
咱们能够实现的最基本的算法,被称为先进先出(First In First Out或FIFO)调度,有时候也称为先到先服务(First Come First Served或FCFS)。spa
FIFO有一些积极的特性:它很简单,并且易于实现,可是在某些状况下的性能不好。操作系统
假设咱们有3个任务(A、B和C),A运行100s,而B和C运行10s。并且都几乎同时到达,A比B早一点点,而后B比C早到达一点点。此时系统的平均周转时间是比较高的:110s((100 + 110 + 120)/ 3 = 110)。orm
这个问题一般被称为护航效应(convoy effect),一些耗时较少的潜在资源消费者被排在重量级的资源消费者以后。生命周期
事实证实,一个很是简单的方法解决了这个问题。这个新的调度准则被称为最短任务优先(ShortestJob First,SJF):先运行最短的任务,而后是次短的任务,如此下去。队列
事实上,考虑到全部工做同时到达的假设,咱们能够证实SJF确实是一个最优(optimal)调度算法。可是咱们的假设仍然是不切实际的,让咱们放宽另外一个假设。咱们能够针对假设2,如今假设工做能够随时到达,而不是同时到达,这会致使什么问题?进程
举个例子,假设A在t = 0时到达,且须要运行100s。而B和C在t = 10到达,且各须要运行10s。即便B和C在A以后不久到达,它们仍然被迫等到A完成,从而遭遇一样的护航问题。
为了解决这个问题,须要放宽假设条件(工做必须保持运行直到完成)。咱们还须要调度程序自己的一些机制。鉴于咱们先前关于时钟中断和上下文切换的讨论,当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可证实是最优的。
若是咱们知道任务长度,并且任务只使用CPU,而咱们惟一的衡量是周转时间,STCF将是一个很好的策略。然而,引入分时系统改变了这一切。如今,用户将会坐在终端前面,同时也要求系统的交互性好。所以,一个新的度量标准诞生了:响应时间(response time)。响应时间定义为从任务到达系统到首次运行的时间。
STCF和相关方法在响应时间上并非很好。例如,若是3个工做同时到达,第三个工做必须等待前两个工做所有运行后才能运行。这种方法虽然有很好的周转时间,但对于响应时间和交互性是至关糟糕的。
为了解决这个问题,咱们将介绍一种新的调度算法,一般被称为轮转(Round-Robin,RR)调度。基本思想很简单:RR在一个时间片(time slice,有时称为调度量子,schedulingquantum)内运行一个工做,而后切换到运行队列中的下一个任务,而不是运行一个任务直到结束。它反复执行,直到全部任务完成。
时间片长度对于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的两条基本规则:
咱们必须决定,在一个工做的生命周期中,MLFQ如何改变其优先级(在哪一个队列中)。要作到这一点,咱们必须记得工做负载:既有运行时间很短、频繁放弃CPU的交互型工做,也有须要不少CPU时间、响应时间却不重要的长时间计算密集型工做。下面是咱们第一次尝试优先级调整算法。
然而,这种算法有一些很是严重的缺点。
首先,会有饥饿(starvation)问题。若是系统有“太多”交互型工做,就会不断占用CPU,致使长工做永远没法获得CPU。
其次,聪明的用户会重写程序,愚弄调度程序(game the scheduler)。愚弄调度程序指的是用一些卑鄙的手段欺骗调度程序,让它给你远超公平的资源。例如进程在时间片用完以前,调用一个I/O操做(好比访问一个无关的文件),从而主动释放CPU。如此即可以保持在高优先级,占用更多的CPU时间。
最后,一个程序可能在不一样时间表现不一样。一个计算密集的进程可能在某段时间表现为一个交互型的进程。用咱们目前的方法,它不会享受系统中其余交互型工做的待遇。
一个简单的思路是周期性地提高(boost)全部工做的优先级。能够有不少方法作到,咱们就用最简单的:将全部工做扔到最高优先级队列。因而有了以下的新规则:
新规则解决了两个问题。首先,进程不会饿死——在最高优先级队列中,它会以轮转的方式,与其余高优先级工做分享CPU,从而最终得到执行。其次,若是一个CPU密集型工做变成了交互型,当它优先级提高时,调度程序会正确对待它。
不过,添加时间段S致使了明显的问题:S的值应该如何设置?若是S设置得过高,长工做会饥饿;若是设置得过低,交互型工做又得不到合适的CPU时间比例。
如今还有一个问题要解决:如何阻止调度程序被愚弄?能够看出,这里的元凶是规则4a和4b,致使工做在时间片之内释放CPU,就保留它的优先级。那么应该怎么作?
这里的解决方案,是为MLFQ的每层队列提供更完善的CPU计时方式(accounting)。调度程序应该记录一个进程在某一层中消耗的总时间,而不是在调度时从新计时。只要进程用完了本身的配额,就将它降到低一优先级的队列中去。
所以,咱们重写规则4a和4b:
关于MLFQ调度算法还有一些问题。其中一个大问题是如何配置一个调度程序,例如,配置多少队列?每一层队列的时间片配置多大?为了不饥饿问题以及进程行为改变,应该多久提高一次进程的优先级?这些问题都没有显而易见的答案,所以只有利用对工做负载的经验,以及后续对调度程序的调优,才会致使使人满意的平衡。
例如,大多数的MLFQ变体都支持不一样队列可变的时间片长度。高优先级队列一般只有较短的时间片(好比10ms或者更少),于是这一层的交互工做能够更快地切换。相反,低优先级队列中更多的是CPU密集型工做,配置更长的时间片会取得更好的效果。