目录
引言
我们在进程与线程(上篇)里分别学习了进程与线程的相关概念,这篇博文,我们讲讨论有关进程间通信和进程调度算法的知识。
2.3 进程间通信
进程经常需要与其他进程通信。我们讨论有关进程间通信(Inter Process Communication,IPC)的三个问题:
一个进程如何把信息传递给另一个。
确保两个或更多的进程在关键活动中不会出现交叉
保持正确的顺序(如果该顺序是有关联的话),比如,如果进程A产生数据而进程B打印数据,那么B在打印之前必须等待,直到A已经产生一些数据。
有必要说明,这三个问题中的两个问题对于线程来说是同样适用的。第一个问题对线程而言比较容易,因为它们共享一个地址空间。但是另外两个问题同样适用于线程。同样的问题可用同样的方法解决。下面开始讨论进程间通信问题,不过请记住,同样的问题和解决方法也适用于线程。
2.3.1 竞争条件
在一些操作系统中,协作的进程可能共享一些彼此都能读写的公用存储区。这个公用存储区可能在内存中(可能是在内核数据结构中),也可能是一个共享文件。这里共享存储区的位置并不影响通信的本质及其带来的问题。
这样的情况,即两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序,称为竞争条件(race condition)。
调试包含有竞争条件的程序是一件很头痛的事。大多数的测试运行结果都很好,但在极少数情况下会发生一些无法解释的奇怪现象。不幸的是,多核增长带来的并行使得竞争条件越来越普遍。
2.3.2 临界区
怎样避免竞争条件?实际上凡涉及共享内存、共享文件以及共享任何资源的情况都会引发与前面类似的错误,要避免这种错误,关键是要找出某种途径来阻止多个进程同时读写共享的数据,即是互斥(mutual exclusion),即以某种手段确保当一个进程在使用一个共享变量或文件时,其他进程不能做同样的操作。
避免竞争条件的问题也可以用一种抽象的方式进行描述。一个进程的一部分时间做内部计算或另外一些不会引发竞争条件的操作。在某些时候进程可能需要访问共享内存或共享文件,或执行另外些会导致竞争的操作。我们把对共享内存进行访向的程序片段称作临界区域(critical region)或临界区(criticalsection)。如果我们能够适当地安排,使得两个进程不可能同时处于临界区中,就能够避免竞争条件。
尽管这样的要求避免了竞争条件,但它还不能保证使用共享数据的并发进程能够正确和高效地进行协作。对于一个好的解决方案,需要满足以下4个条件:
- 任何两个进程不能同时处于其临界区。
- 不应对CPU的速度和数量做任何假设。
- 临界区外运行的进程不得阻塞其他进程。
- 不得使进程无限期等待进入临界区。
从抽象的角度看,人们所希望的进程行为如下图所示:
图中进程A在T1时刻进入临界区。稍后,在T2时刻进程B试图进入临界区,但是失败了,因为另一个进程已经在该临界区内,而一个时刻只允许一个进程在临界区内。随后,B被暂时挂起直到T3,时刻A离开临界区为止,从而允许B立即进入。最后,B离开(在时刻T4),回到了在临界区中没有进程的原始状态。
2.3.3 忙等待的互斥
我们将讨论几种实现互斥的方案。在这些方案中,当一个进程在临界区中更新共享内存时,其他进程将不会进入其临界区,也不会带来任何麻烦。
2.3.3.1 屏蔽中断
在单处理器系统中,最简单的方法是使每个进程在刚刚进入临界区后立即屏蔽所有中断,并在就要离开之前再打开中断。屏蔽中断后,时钟中断也被屏蔽。CPU只有发生时钟中断或其他中断时才会进行进程切换,这样,在屏蔽中断之后CPU将不会被切换到其他进程。于是,一旦某个进程屏蔽中断之后,它就可以检查和修改共享内存,而不必担心其他进程介入。
这个方案并不好,因为:
- 把屏蔽中断的权力交给用户进程是不明智的。设想一下,若一个进程屏蔽中断后不再打开中断,整个系统可能会因此终止。
- 如果系统是多处理器(有两个或可能更多的处理器),则屏蔽中断仅仅对执行disable指令的那个CPU有效。其他CPU仍将继续运行,并可以访问共享内存。
另一方面,对内核来说,当它在更新变量或列表的几条指令期间将中断屏蔽是很方便的。当就绪进程队列之类的数据状态不一致时发生中断,则将导致竞争条件。所以结论是:屏蔽中断对于操作系统本身而言是一项很有用的技术,但对于用户进程则不是一种合适的通用互斥机制。
2.3.3.2 锁变量
作为第二种尝试,可以寻找一种软件解决方案。设想有一个共享(锁)变量,其初始值为0。当一个进程想进人其临界区时,它首先测试这把锁。如果该锁的值为0,则该进程将其设置为1并进入临界区。若这把锁的值已经为1,则该进程将等待直到其值变为0。于是,0就表示临界区内没有进程,1表示已经有某个进程进入临界区。
但是,这种想法也包含一种的疏漏。假设一个进程读出锁变量的值并发现它为0,而恰好在它将其值设置为1之前,另一个进程被调度运行,将该锁变量设置为1。当第一个进程再次运行时,它同样也将该锁设置为1,则此时同时有两个进程进人临界区中。
可能读者会想,先读出锁变量,紧接着在改变其值之前再检查一遍它的值,这样便可以解决问题。但这实际上无济于事,如果第二个进程恰好在第一个进程完成第二次检查之后修改了锁变量的值,则同样还会发生竞争条件。
2.3.3.3 严格轮换法
第三种互斥的方法如下图所示:
图中,整型变量turn,初始值为0,用于记录轮到哪个进程进入临界区,并检查或更新共享内存。开始时,进程0检查turn,发现其值为0,于是进入临界区。进程1也发现其值为0,所以在一个等待循环中不停地测试turn,看其值何时变为1。连续测试一个变量直到某个值出现为止,称为忙等待(busywaiting)。由于这种方式浪费CPU时间,所以通常应该避免。只有在有理由认为等待时间是非常短的情形下,才使用忙等待。用于忙等待的锁,称为自旋锁(spin lock)。
进程0离开临界区时,它将turn的值设置为1,以便允许进程1进入其临界区。假设进程1很快便离开了临界区,则此时两个进程都处于临界区之外,turn的值又被设置为0。现在进程0很快就执行完其整个循环,它退出临界区,并将turn的值设置为1。此时,turm的值为1,两个进程都在其临界区外执行。突然,进程0结束了非临界区的操作并且返回到循环的开始。但是,这时它不能进入临界区,因为turn的当前值为1,而此时进程1还在忙于非临界区的操作,进程0只有继续while循环,直到进程1把turn的值改为0。这说明,在一个进程比另一个慢了很多的情况下,轮流进入临界区并不是一个好办法。
这种情况违反了前面叙述的条件3:进程0被一个临界区之外的进程阻塞。实际上,该方案要求两个进程严格地轮流进入它们的临界区。尽管该算法的确避免了所有的竞争条件,但由于它违反了条件3,所以不能作为一个很好的备选方案。
2.3.3.4 TSL指令
现在来看需要硬件支持的一种方案。某些计算机中,特别是那些设计为多处理器的计算机,都有下面一条指令:
TSL RX, LOCK
称为测试并加锁(test and set lock),它将一个内存字lock读到寄存器RX中,然后在该内存地址上存一个非零值。读字和写字操作保证是不可分割的,即该指令结束之前其他处理器均不允许访问该内存字。
执行TSL指令的CPU将锁住内存总线,以禁止其他CPU在本指令结束之前访问内存。着重说明一下,锁住存储总线不同于屏蔽中断。屏蔽中断,然后在读内存字之后跟着写操作并不能阻止总线上的第二个处理器在读操作和写操作之间访问该内存字。事实上,在处理器1上屏蔽中断对处理器2根本没有任何影响。让处理器2远离内存直到处理器1完成的唯一方法就是锁住总线,这需要一个特殊的硬件设施(基本上,一根总线就可以确保总线由锁住它的处理器使用,而其他的处理器不能用)。
为了使用TSL指令,要使用一个共享变量lock来协调对共享内存的访问。当lock为0时,任何进程都可以使用TSL指令将其设置为1,并读写共享内存。当操作结束时,进程用一条普通的move指令将lock的值重新设置为0。
这条指令如何防止两个进程同时进入临界区呢?解决方案如图所示:
假定(典型假设)存在如下共4条指令的汇编语言子程序。第一条指令将lock原来的值复制到寄存器中并将lock设置为1,随后这个原来的值与0相比较。如果它非零,则说明以前已被加锁,则程序将回到开始并再次测试。经过或长或短的一段时间后,该值将变为0(当前处于临界区中的进程退出临界区时),于是过程返回,此时已加锁。要清除这个锁非常简单,程序只需将0存入lock即可,不需要特殊的同步指令。
现在有一种很明确的解法了。进程在进入临界区之前先调用 enter_region,这将导致忙等待,直到锁空闲为止,随后它获得该锁并返回。在进程从临界区返回时它调用leave_ region, 这将把lock设置为0。
与基于临界区问题的所有解法一样, 进程必须在正确的时间调用enter_ region和 leave_ region,解法才能奏效。如果一个进程有欺诈行为,则互斥将会失败。换言之,只有进程合作,临界区才能工作。
2.4 调度
当计算机系统是多道程序设计系统时,通常就会有多个进程或线程同时竞争CPU。只要有两个或更多的进程处于就绪状态,这种情形就会发生。如果只有一个CPU可用,那么就必须选择下一个要运行的进程。在操作系统中,完成选择工作的这一部分称为调度程序(scheduler), 该程序使用的算法称为调度算法(scheduling algorithm)。
尽管有一些不同,但许多适用于进程调度的处理方法也同样适用于线程调度。当内核管理线程的时候,调度经常是按线程级别的,与线程所属的进程基本或根本没有关联。下面我们将首先关注适用于进程与线程两者的调度问题,然后会明确地介绍线程调度以及它所产生的独特问题。
2.4.1 调度简介
让我们回到早期以磁带上的卡片作为输入的批处理系统时代,那时的调度算法很简单:依次运行磁带上的每一个作业。对于多道程序设计系统,调度算法要复杂一些,因为经常有多个用户等候服务。有些大型机系统仍旧将批处理和分时服务结合使用,需要调度程序决定下一个运行的是一个批处理作业还是终端上的一个交互用户。由于在这些机器中,CPU是稀缺资源,所以好的调度程序可以在提高性能和用户的满意度方面取得很大的成果。因此,大量的研究工作都花费在创造聪明而有效的调度算法上了。
在个人计算机出现之后,整个情形向两个方面发展。首先,在多数时间内只有一个活动进程。其次,同CPU是稀缺资源时的年代相比,现在计算机速度极快。个人计算机的多数程序受到的是用户当前输入速率(键入或敲击鼠标)的限制,而不是CPU处理速率的限制。这样的结果是,调度程序在简单的PC上并不重要。当然,总有应用程序会实际消耗掉CPU。
对于网络服务器,情况略微有些改变。这里,多个进程经常竞争CPU,因此调度功能再一次变得至关重要。“资源充足"这个论据在很多移动设备上也不成立,比如智能手机以及传感器网络的节点。这些情况下,CPU依然薄弱,内存也偏小。此外,因为电池寿命短是这些设备最重要的约束之一,所以一些调度算法在努力优化电量损耗。
另外,为了选取正确的进程运行,调度程序还要考虑CPU的利用率,因为进程切换的代价是比较高的。首先用户态必须切换到内核态:然后要保存当前进程的状态,包括在进程表中存储寄存器值以便以后重新装载。在许多系统中,内存映像(例如,页表内的内存访问位)也必须保存;接着,通过运行调度算法选定一个新进程;之后,应该将新进程的内存映像重新装人MMU;最后新进程开始运行。除此之外,进程切换还要使整个内存高速缓存失效,强迫缓存从内存中动态重新装入两次(进入内核一次,离开内核一次)。总之,如果每秒钟切换进程的次数太多,会耗费大量CPU时间,所以有必要提醒注意。
2.4.1.1 进程行为
几乎所有进程的I/O请求和计算都是交替突发的,如图所示:
典型地,CPU不停顿地运行一段时间,然后发出一个系统调用以便读写文件。在完成系统调用之后,CPU又开始计算,直到它需要读更多的数据或写更多的数据为止。请注意,某些I/O活动可以看作计算。例如,当CPU向视频RAM复制数据以更新屏幕时,因为使用了CPU,所以这是计算,而不是I/O活动。按照这种观点,当一个进程等待外部设备完成工作而被阻塞时,才是I/O活动。
图中有一件值得注意的事,即某些进程花费了绝大多数时间在计算上,而其他进程则在等待I/O上花费了绝大多数时间。前者称为计算密集型(compute-bound),后者称为I/O密集型(I/O-bound)。典型的计算密集型进程具有较长时间的CPU集中使用和较小频度的I/O等待。I/O密集型进程具有较短时间的CPU集中使用和频繁的I/O等待。它是I/O类的,因为这种进程在I/O请求之间较少进行计算,并不是因为它们有特别长的I/O请求。在I/O开始后无论处理数据是多还是少,它们都花费同样的时间提出硬件请求读取磁盘块。
有必要指出,随着CPU变得越来越快,更多的进程倾向为I/O密集型。这种现象之所以发生是因为
CPU的改进比磁盘的改进快得多,其结果是,未来对I/O密集型进程的调度处理似乎更为重要。这里的基本思想是,如果需要运行I/O密集型进程,那么就应该让它尽快得到机会,以便发出磁盘请求并保持磁盘始终忙碌。从图中可以看到,如果进程是I/O密集型的,则需要多运行一些这类进程以保持CPU的充分利用。
2.4.1.2 何时调度
有关调度处理的一个关键问题是何时进行调度决策。存在着需要调度处理的各种情形:
第一,在创建一个新进程之后,需要决定是运行父进程还是运行子进程。由于这两种进程都处于就绪状态,所以这是一种正常的调度决策,可以任意决定,也就是说,调度程序可以合法选择先运行父进程还是先运行子进程。
第二,在一个进程退出时必须做出调度决策。一个进程不再运行(因为它不再存在),所以必须从就绪进程集中选择另外某个进程。如果没有就绪的进程,通常会运行一个系统提供的空闲进程。
第三,当一个进程阻塞在I/O和信号量上或由于其他原因阻塞时,必须选择另一个进程运行。有时,阻塞的原因会成为选择的因素。例如,如果A是一个重要的进程,并正在等待B退出临界区,让B随后运行将会使得B退出临界区,从而可以让A运行。不过问题是,通常调度程序并不拥有做出这种相关考虑的必要信息。
第四,在一个I/O中断发生时,必须做出调度决策。如果中断来自I/O设备,而该设备现在完成了工作,某些被阻塞的等待该I/O的进程就成为可运行的就绪进程了。是否让新就绪的进程运行,这取决于调度程序的决定,或者让中断发生时运行的进程继续运行,或者应该让某个其他进程运行。
如果硬件时钟提供50Hz、60Hz或其他频率的周期性中断,可以在每个时钟中断或者在每k个时钟中断时做出调度决策。根据如何处理时钟中断,可以把调度算法分为两类。
非抢占式调度算法
非抢占式调度算法挑选一个进程,然后让该进程运行直至被阻塞(阻塞在I/O上或等待另一个进程),或者直到该进程自动释放CPU。即使该进程运行了若干个小时,它也不会被强迫挂起。这样做的结果是,在时钟中断发生时不会进行调度。在处理完时钟中断后,如果没有更高优先级的进程等待到时,则被中断的进程会继续执行。
抢占式调度算法
抢占式调度算法挑选一个进程,并且让该进程运行某个固定时段的最大值。如果在该时段结束时,该进程仍在运行,它就被挂起,而调度程序挑选另一个进程运行(如果存在一个就绪进程)。进行抢占式调度处理,需要在时间间隔的末端发生时钟中断,以便把CPU控制返回给调度程序。如果没有可用的时钟,那么非抢占式调度就是唯一的选择了。
2.4.1.3 调度算法分类
毫无疑问,不同的环境需要不同的调度算法。之所以出现这种情形,是因为不同的应用领域(以及不同的操作系统)有不同的目标。换句话说,在不同的系统中,调度程序的优化是不同的。这里有必要划分出三种环境:
批处理
交互式
实时
批处理
批处理系统在商业领域仍在广泛应用,用来处理薪水册、存货清单、账目收入、账目支出、利息计算(在银行)、索赔处理(在保险公司)和其他的周期性的作业。在批处理系统中,不会有用户不耐烦地在终端旁等待一个短请求的快捷响应。因此,非抢占式算法,或对每个进程都有长时间周期的抢占式算法,通常都是可接受的。这种处理方式减少了进程的切换从而改善了性能。这些批处理算法实际上相当普及,并经常可以应用在其他场合,这使得人们值得去学习它们,甚至是对于那些没有接触过大型机计算的人们。
交互式
而在交互式用户环境中,为了避免一个进程霸占CPU拒绝为其他进程服务,抢占是必需的。即便没有进程想永远运行,但是,某个进程由于一个程序错误也可能无限期地排斥所有其他进程。为了避免这种现象发生,抢占也是必要的。服务器也归于此类,因为通常它们要服务多个突发的(远程)用户。
实时
然而在有实时限制的系统中,抢占有时是不需要的,因为进程了解它们可能会长时间得不到运行,所以通常很快地完成各自的工作并阻塞。实时系统与交互式系统的差别是,实时系统只运行那些用来推进现有应用的程序,而交互式系统是通用的,它可以运行任意的非协作甚至是有恶意的程序。
2.4.1.4 调度算法的目标
为了设计调度算法,有必要考虑什么是一个好的调度算法。某些目标取决于环境,但是还有一些目标是适用于所有情形的。在下图中列出了一些目标:
公平
在所有的情形中,公平是很重要的。相似的进程应该得到相似的服务。对一个进程给予较其他等价的进程更多的CPU时间是不公平的。当然,不同类型的进程可以采用不同方式处理。可以考虑一下在核反应堆计算机中心安全控制与发放薪水处理之间的差别。
系统策略的强制执行
与公平有关的是系统策略的强制执行。如果局部策略是,只要需要就必须运行安全控制进程(即便这意味着推迟30秒钟发薪),那么调度程序就必须保证能够强制执行该策略。
平衡
另一个共同的目标是保持系统的所有部分尽可能忙碌。如果CPU和所有I/O设备能够始终运行,那么相对于让某些部件空转而言,每秒钟就可以完成更多的工作。例如,在批处理系统中,调度程序控制哪个作业调入内存运行。在内存中既有一些CPU密集型进程又有一些I/O密集型进程是一个较好的想法,好于先调入和运行所有的CPU密集型作业,然后在它们完成之后再调入和运行所有I/O密集型作业的做法。如果使用后面一种策略,在CPU密集型进程运行时,它们就要竞争CPU,而磁盘却在空转。稍后,当I/O密集型作业来了之后,它们要为磁盘而竞争,而CPU又空转了。显然,通过仔细组合进程,可以保持整个系统运行得更好一些。
运行大量批处理作业的大型计算中心的管理者们为了掌握其系统的工作状态,通常检查三个指标:
吞吐量、周转时间以及CPU利用率。
吞吐量
吞吐量(throughout)是系统每小时完成的作业数量。把所有的因素考虑进去之后,每小时完成50个作业好于每小时完成40个作业。
周转时间
周转时间(turnaround time)是指从一个批处理作业提交时刻开始直到该作业完成时刻为止的统计平均时间。该数据度量了用户要得到输出所需的平均等待时间。规则是小就是好的。
能够使吞吐量最大化的调度算法不定就有最小的周转时间。例如,对于确定的短作业和长作业的一个组合,总是运行短作业而不运行长作业的调度程序,可能会获得出色的吞吐性能(每小时大量的短作业),但是其代价是对于长的作业周转时间很差。如果短作业以一个稳定的速率不断到达,长作业可能根本运行不了,这样平均周转时间是无限长,但是得到了高的吞吐量。
CPU利用率
CPU利用率常常用于对批处理系统的度量。尽管这样,CPU利用率并不是一个好的度量参数。真正有价值的是,系统每小时可完成多少作业(吞吐量),以及完成作业需要多长时间(周转时间)。把CPU利用率作为度量依据,就像用引擎每小时转动了多少次来比较汽车的好坏一样。另一方面,知道什么时候CPU利用率接近100%比知道什么时候要求得到更多的计算能力要有用。
对于交互式系统,则有不同的指标。
最小响应时间
最重要的是最小响应时间,即从发出命令到得到响应之间的时间。在有后台进程运行的个人计算机上,用户请求启动一个程序或打开一个文件应该优先于后台的工作。能够让所有的交互式请求首先运行的则是好服务。
均衡性
一个相关的问题是均衡性。用户对做二件事情需要多长时间总是有一种固有的(不过通常不正确)看法。当认为一个请求很复杂需要较多的时间时,用户会接受这个看法,但是当认为一个请求很简单,但也需要较多的时间时,用户就会急躁。
实时系统有着与交互式系统不一样的特性,所以有不同的调度目标。
满足截止时间
实时系统的特点是或多或少必须满足截止时间。例如,如果计算机正在控制一个以正常速率产生数据的设备,若一个按时运行的数据收集进程出现失败,会导致数据丢失。所以,实时系统最主要的要求是满足所有的(或大多数)截止时间要求。
可预测性
在多数实时系统中,特别是那些涉及多媒体的实时系统中,可预测性是很重要的。偶尔不能满足截止时间要求的问题并不严重,但是如果音频进程运行的错误太多,那么音质就会下降得很快。视频品质也是一个问题,但是人的耳朵比眼睛对抖动要敏感得多。为了避免这些问题,进程调度程序必须是高度可预测和有规律的。本章介绍批处理系统和交互式系统中的调度算法。本书不介绍实时系统的调度算法。
2.4.2 批处理系统中的调度
现在从一般的调度处理问题转向特定的调度算法。我们将考察在批处理系统中使用的算法,随后将讨论交互式和实时系统中的调度算法。有必要指出,某些算法既可以用在批处理系统中,也可以用在交互式系统中。我们将稍后讨论这个问题。
2.4.2.1 先来先服务
在所有调度算法中,最简单的是非抢占式的先来先服务(first-come first-served)算法。使用该算法,进程按照它们请求CPU的顺序使用CPU。基本上,有一个就绪进程的单一队列。上午,当第一个作业从外部进入系统后,就立即开始并允许运行它所期望的时间长度,该作业不会因为运行太长时间而被中断。当其他作业进入时,它们排到就绪队列尾部。当正在运行的进程被阻塞时,就绪队列中的第一个进程接着运行。当在被阻塞的进程变为就绪时,就像个新来到的作业一样,排到就绪队列的末尾,即排在所有进程最后。
这个算法的主要优点是易于理解并且便于在程序中运用。在这个算法中,一个单链表记录了所有就绪进程。要选取一个进程运行,只要从该队列的头部移走一个进程即可要添加一个新的作业或阻塞一个进程,只要把该作业或进程附加在相应队列的末尾即可。
不过,先来先服务也有明显的缺点。假设有一个一次运行1秒钟的计算密集型进程和很少使用CPU但是每个都要进行1000次磁盘读操作才能完成的大量I/O密集型进程存在。计算密集进程运行1秒钟,接着读一个磁盘块。所有的I/O进程开始运行并读磁盘。当该计算密集进程获得其磁盘块时,它运行下一个1秒钟,紧跟随着的是所有I/O进程。这样做的结果是,每个I/O进程在每秒钟内读到一个磁盘块,要花费1000秒钟才能完成操作。如果有一个调度算法每10ms抢占计算密集型进程,那么I/O进程将在10秒钟内完成而不是1000秒钟,而且还不会对计算密集型进程产生多少延迟。
2.4.2.2 最短作业优先
现在来看一种适用于运行时间可以预知的另一个非抢占式的批处理调度算法。例如,一家保险公司,因为每天都做类似的工作,所以人们可以相当精确地预测处理1000个索赔的一批作业需要多少时间。当输入队列中有若千个同等重要的作业被启动时,调度程序应使用最短作业优先(shortest job first)算法,请看下图:
这里有4个作业A、B、C、D,运行时间分别为8、4、4、4分钟。若按图中的次序运行,则A的周转时间为8分钟,B为12分钟,C为16分钟,D为20分钟,平均为14分钟。
现在考虑使用最短作业优先算法运行这4个作业,如图b所示。目前周转时间分别为4、8. 12和20分钟,平均为11分钟。可以证明最短作业优先是最优的。考虑有4个作业的情况,其运行时间分别为a、b. c、d。第一个作业在时间a结束,第二个在时间a + b结束,以此类推。平均周转时间为(4a+ 3b+ 2c +d) /4。显然a对平均值影响最大,所以它应是最短作业,其次是b,再次是c,最后的d只影响它自己的周转时间。对任意数目作业的情况,道理完全一样。
有必要指出,只有在所有的作业都可同时运行的情形下(都已经就绪),最短作业优先算法才是最优化的。作为一个反例,考虑5个作业,从A到E,运行时间分别是2、4、1、1和1。它们的到达时间是0、0、3、3和3。开始,只能选择A或B,因为其他三个作业还没有到达。使用最短作业优先,将按照A、B. C、D、E的顺序运行作业,其平均等待时间是4.6。但是,按照B、C、D、E、A的顺序运行作业,其平均等待时间则是4.4。
2.4.2.3 最短剩余时间优先
最短作业优先的抢占式版本是最短剩余时间优先(shortest remaining time next)算法。使用这个算法,调度程序总是选择剩余运行时间最短的那个进程运行。再次提醒,有关的运行时间必须提前掌握。
当一个新的作业到达时,其整个时间同当前进程的剩余时间做比较。如果新的进程比当前运行进程需要更少的时间,当前进程就被挂起,而运行新的进程。这种方式可以使新的短作业获得良好的服务。
2.4.3 交互式系统中的调度
现在考察用于交互式系统中的一些调度算法,它们在个人计算机、服务器和其他类系统中都是常用的。
2.4.3.1 轮转调度
下一种最古老、最简单、最公平且使用最广的算法是轮转调度(round robin)。 每个进程被分配一个时间段,称为时间片(quantum),即允许该进程在该时间段中运行。如果在时间片结束时该进程还在运行,则将剥夺CPU并分配给另一个进程。如果该进程在时间片结東前阻塞或结束,则CPU立即进行切换。
时间片轮转调度很容易实现,调度程序所要做的就是维护一张可运行进程列表,如图所示:
当一个进程用完它的时间片后,就被移到队列的末尾,如图b) 所示。时间片轮转调度中唯一有趣的一点是时间片的长度。从一个进程切换到另一个进程是需要一定时间进行管理事务处理的一一保存和装入寄存器值及内存映像、更新各种表格和列表、清除和重新调入内存高速缓存等。假如进程切换(process switch), 有时称为上下文切换(context switch),需要1ms,包括切换内存映像、清除和重新调入高速缓存等。再假设时间片设为4ms。有了这些参数,则CPU在做完4ms有用的工作之后,CPU将花费(即浪费)1ms来进 行进程切换。因此,CPU时间的20%浪费在管理开销上。很明显,管理时间太多了。
为了提高CPU的效率,可以将时间片设置成,比方说100ms,这样浪费的时间只有1%。但是,如果在一段非常短的时间间隔内到达50个请求,并且对CPU有不同的需求,那么,考虑一下, 在一个服务器系统中会发生什么呢?50个进程会放在可运行进程的列表中。如果CPU是空闲的,第一个进程会立即开始执行,第二个直到100ms以后才会启动,以此类推。假设所有其他进程都用足了它们的时间片的话,最不幸的是最后一个进程在获得运行机会之前将不得不等待5秒钟。大部分用户会认为5秒的响应对于一个短命令来说是缓慢的。如果一些在就绪队列后边的请求仅需要几毫秒的CPU时间,上面的情况会变得尤其糟糕。如果使用较短的时间片的话,它们将会获得更好的服务。
另一个因素是,如果时间片设置长于平均的CPU突发时间,那么不会经常发生抢占。相反,在时间片耗费完之前多数进程会完成一个阻塞操作,引起进程的切换。抢占的消失改善了性能,因为进程切换只会发生在确实逻辑上有需要的时候,即进程被阻塞不能够继续运行。
可以归结如下结论:
时间片设得太短会导致过多的进程切换,降低了CPU效率
而设得太长又可能引起对短的交互请求的响应时间变长。将时间片设为20~50ms通常是一个比较合理的折中。
2.4.3.2 优先级调度
轮转调度做了一个隐含的假设,即所有的进程同等重要。而将外部因素考虑在内的需要就导致了优先级调度。其基本思想很清楚:每个进程被赋予一个优先级,允许优先级最高的可运行进程先运行。
即使在只有一个用户的PC上,也会有多个进程,其中一些比另一些更重要。为了防止高优先级进程无休止地运行下去,调度程序可能在每个时钟滴答(即每个时钟中断)降低当前进程的优先级。如果这行为导致该进程的优先级低于次高优先级的进程,则进行进程切换。另一种方法是,给每个进程赋予一个允许运行的最大时间片,当用完这个时间片时,次高优先级的进程便获得运行机会。
优先级可以是静态赋予或动态赋予。
可以很方便地将一组进程按优先级分成若干类:
- 并且在各类之间采用优先级调度;
- 而在各类进程的内部采用轮转调度。
下图给出了一个有4类优先级的系统:
其调度算法如下:只要存在优先级为第4类的可运行进程,就按照轮转法为每个进程运行一个时间片,此时不理会较低优先级的进程。若第4类进程为空,则按照轮转法运行第3类进程。若第4类和第3类均为空,则按轮转法运行第2类进程。如果不偶尔对优先级进行调整,则低优先级进程很可能会产生饥饿现象。
2.4.3.3 多级队列
为CPU密集型进程设置较长的时间片比频繁地分给它们很短的时间片要更为高效(减少交换次数)。另一方面,如前所述,长时间片的进程又会影响到响应时间,其解决办法是设立优先级类
属于最高优先级类的进程运行一个时间片,属于次高优先级类的进程运行2个时间片,再次一级运行4个时间片,以此类推。当一个进程用完分配的时间片后,它被移到下一类。
作为一个例子,考虑有一个进程需要连续计算100个时间片。它最初被分配1个时间片,然后被换出。下次它将获得2个时间片,接下来分别是4、8、16、 32和64。当然最后一次它只使用64个时间片中的37个便可以结束工作。该进程需要7次交换(包括最初的装入),而如果采用纯粹的轮转算法则需要100次交换。而且,随着进程优先级的不断降低,它的运行频度逐渐放慢,从而为短的交互进程让出CPU。
而对于那些刚开始运行一段长时间,而后来又需要交互的进程,为了防止其永远处于被惩罚状态,可以采取下面的策略。只要终端上有回车键(Enter键)按下,则属于该终端的所有进程就都被移到最高优先级,这样做的原因是假设此时进程即将需要交互。但可能有一天,一台CPU密集的重载机器上有几个用户偶然发现,只需坐在那里随机地每隔几秒钟敲一下回车键就可以大大提高响应时间。于是他们又告诉他们的朋友....这个故事的寓意是:在实践上可行比理论上可行要困难得多。