预计阅读本页时间:-
2.4 调度
当计算机系统是多道程序设计系统时,通常就会有多个进程或线程同时竞争CPU。只要有两个或更多的进程处于就绪状态,这种情形就会发生。如果只有一个CPU可用,那么就必须选择下一个要运行的进程。在操作系统中,完成选择工作的这一部分称为调度程序(scheduler),该程序使用的算法称为调度算法(scheduling algorithm)。
尽管有一些不同,但许多适用于进程调度的处理方法也同样适用于线程调度。当内核管理线程的时候,调度经常是按线程级别的,与线程所属的进程基本或根本没有关联。下面我们将首先关注适用于进程与线程两者的调度问题,然后会明确地介绍线程调度以及它所产生的独特问题。第8章将讨论多核芯片的问题。
2.4.1 调度介绍
让我们回到早期以磁带上的卡片作为输入的批处理系统时代,那时的调度算法很简单:依次运行磁带上的每一个作业。对于多道程序设计系统,调度算法要复杂一些,因为经常有多个用户等候服务。有些大型机系统仍旧将批处理和分时服务结合使用,需要调度程序决定下一个运行的是一个批处理作业还是终端上的一个交互用户。(顺便提及,一个批处理作业可能需要连续运行多个程序,不过在本节中,我们假设它只是一个运行单个程序的请求。)由于在这些机器中,CPU是稀缺资源,所以好的调度程序可以在提高性能和用户的满意度方面取得很大的成果。因此,大量的研究工作都花费在创造聪明而有效的调度算法上了。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
在拥有了个人计算机的优势之后,整个情形向两个方面发展。首先,在多数时间内只有一个活动进程。一个用户进入文字处理软件编辑一个文件时,一般不会同时在后台编译一个程序。在用户向文字处理软件键入一条命令时,调度程序不用做多少工作来判定哪个进程要运行——惟一的候选者是文字处理软件。
其次,同CPU是稀缺资源时的年代相比,现在计算机速度极快。个人计算机的多数程序受到的是用户当前输入速率(键入或敲击鼠标)的限制,而不是CPU处理速率的限制。即便对于编译(这是过去CPU周期的主要消耗者)现在大多数情况下也只要花费仅仅几秒钟。甚至两个实际同时运行的程序,诸如一个文字处理软件和一个电子表单,由于用户在等待两者完成工作,因此很难说需要哪一个先完成。这样的结果是,调度程序在简单的PC机上并不重要。当然,总有应用程序会实际消耗掉CPU,例如,为绘制一小时高精度视频而调整108 000帧(NTSC制)或90 000帧(PAL制)中的每一帧颜色就需要大量工业强度的计算能力。然而,类似的应用程序不在我们的考虑范围。
当我们转向网络服务器时,情况略微有些改变。这里,多个进程经常竞争CPU,因此调度功能再一次变得至关重要。例如,当CPU必须在运行一个收集每日统计数据的进程和服务用户需求的进程之间进行选择的时候,如果后者首先占用了CPU,用户将会更高兴。
另外,为了选取正确的进程运行,调度程序还要考虑CPU的利用率,因为进程切换的代价是比较高的。首先用户态必须切换到内核态;然后要保存当前进程的状态,包括在进程表中存储寄存器值以便以后重新装载。在许多系统中,内存映像(例如,页表内的内存访问位)也必须保存;接着,通过运行调度算法选定一个新进程;之后,应该将新进程的内存映像重新装入MMU;最后新进程开始运行。除此之外,进程切换还要使整个内存高速缓存失效,强迫缓存从内存中动态重新装入两次(进入内核一次,离开内核一次)。总之,如果每秒钟切换进程的次数太多,会耗费大量CPU时间,所以有必要提醒注意。
1.进程行为
几乎所有进程的(磁盘)I/O请求或计算都是交替突发的,如图2-38所示。典型地,CPU不停顿地运行一段时间,然后发出一个系统调用以便读写文件。在完成系统调用之后,CPU又开始计算,直到它需要读更多的数据或写更多的数据为止。请注意,某些I/O活动可以看作是计算。例如,当CPU向视频RAM复制数据以更新屏幕时,因为使用了CPU,所以这是计算,而不是I/O活动。按照这种观点,当一个进程等待外部设备完成工作而被阻塞时,才是I/O活动。

图2-38中有一件值得注意的事,即某些进程(图2-38a的进程)花费了绝大多数时间在计算上,而其他进程(图2-38b的进程)则在等待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密集型进程,那么就应该让它尽快得到机会,以便发出磁盘请求并保持磁盘始终忙碌。从图2-6中可以看到,如果进程是I/O密集型的,则需要多运行一些这类进程以保持CPU的充分利用。
2.何时调度
有关调度处理的一个关键问题是何时进行调度决策。存在着需要调度处理的各种情形。第一,在创建一个新进程之后,需要决定是运行父进程还是运行子进程。由于这两种进程都处于就绪状态,所以这是一种正常的调度决策,可以任意决定,也就是说,调度程序可以合法选择先运行父进程还是先运行子进程。
第二,在一个进程退出时必须做出调度决策。一个进程不再运行(因为它不再存在),所以必须从就绪进程集中选择另外某个进程。如果没有就绪的进程,通常会运行一个系统提供的空闲进程。
第三,当一个进程阻塞在I/O和信号量上或由于其他原因阻塞时,必须选择另一个进程运行。有时,阻塞的原因会成为选择的因素。例如,如果A是一个重要的进程,并正在等待B退出临界区,让B随后运行将会使得B退出临界区,从而可以让A运行。不过问题是,通常调度程序并不拥有做出这种相关考虑的必要信息。
第四,在一个I/O中断发生时,必须做出调度决策。如果中断来自I/O设备,而该设备现在完成了工作,某些被阻塞的等待该I/O的进程就成为可运行的就绪进程了。是否让新就绪的进程运行,这取决于调度程序的决定,或者让中断发生时运行的进程继续运行,或者应该让某个其他进程运行。
如果硬件时钟提供50Hz、60Hz或其他频率的周期性中断,可以在每个时钟中断或者在每k个时钟中断时做出调度决策。根据如何处理时钟中断,可以把调度算法分为两类。非抢占式调度算法挑选一个进程,然后让该进程运行直至被阻塞(阻塞在I/O上或等待另一个进程),或者直到该进程自动释放CPU。即使该进程运行了若干个小时,它也不会被强迫挂起。这样做的结果是,在时钟中断发生时不会进行调度。在处理完时钟中断后,如果没有更高优先级的进程等待到时,则被中断的进程会继续执行。
相反,抢占式调度算法挑选一个进程,并且让该进程运行某个固定时段的最大值。如果在该时段结束时,该进程仍在运行,它就被挂起,而调度程序挑选另一个进程运行(如果存在一个就绪进程)。进行抢占式调度处理,需要在时间间隔的末端发生时钟中断,以便把CPU控制返回给调度程序。如果没有可用的时钟,那么非抢占式调度就是惟一的选择了。
3.调度算法分类
毫无疑问,不同的环境需要不同的调度算法。之所以出现这种情形,是因为不同的应用领域(以及不同的操作系统)有不同的目标。换句话说,在不同的系统中,调度程序的优化是不同的。这里有必要划分出三种环境:
1)批处理。
2)交互式。
3)实时。
批处理系统在商业领域仍在广泛应用,用来处理薪水册、存货清单、账目收入、账目支出、利息计算(在银行)、索赔处理(在保险公司)和其他的周期性的作业。在批处理系统中,不会有用户不耐烦地在终端旁等待一个短请求的快捷响应。因此,非抢占式算法,或对每个进程都有长时间周期的抢占式算法,通常都是可接受的。这种处理方式减少了进程的切换从而改善了性能。这些批处理算法实际上相当普及,并经常可以应用在其他场合,这使得人们值得去学习它们,甚至是对于那些没有接触过大型机计算的人们。
在交互式用户环境中,为了避免一个进程霸占CPU拒绝为其他进程服务,抢占是必需的。即便没有进程想永远运行,但是,某个进程由于一个程序错误也可能无限期地排斥所有其他进程。为了避免这种现象发生,抢占也是必要的。服务器也归于此类,因为通常它们要服务多个突发的(远程)用户。
然而在有实时限制的系统中,抢占有时是不需要的,因为进程了解它们可能会长时间得不到运行,所以通常很快地完成各自的工作并阻塞。实时系统与交互式系统的差别是,实时系统只运行那些用来推进现有应用的程序,而交互式系统是通用的,它可以运行任意的非协作甚至是有恶意的程序。
4.调度算法的目标
为了设计调度算法,有必要考虑什么是一个好的调度算法。某些目标取决于环境(批处理、交互式或实时),但是还有一些目标是适用于所有情形的。在图2-39中列出了一些目标,我们将在下面逐一讨论。

在所有的情形中,公平是很重要的。相似的进程应该得到相似的服务。对一个进程给予较其他等价的进程更多的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利用率接近100%比知道什么时候要求得到更多的计算能力要有用。
对于交互式系统,则有不同的指标。最重要的是最小响应时间,即从发出命令到得到响应之间的时间。在有后台进程运行(例如,从网络上读取和存储电子邮件)的个人计算机上,用户请求启动一个程序或打开一个文件应该优先于后台的工作。能够让所有的交互式请求首先运行的则是好服务。
一个相关的问题是均衡性。用户对做一件事情需要多长时间总是有一种固有的(不过通常不正确)看法。当认为一个请求很复杂需要较多的时间时,用户会接受这个看法,但是当认为一个请求很简单,但也需要较多的时间时,用户就会急躁。例如,如果点击一个图标花费了60秒钟发送完成一份传真,用户大概会接受这个事实,因为他没有期望花5秒钟得到传真。
另一方面,当传真发送完成,用户点击断开电话连接的图标时,该用户就有不一样的期待了。如果30秒之后还没有完成断开操作,用户就可能会抱怨,而60秒之后,他就要气得要命了。之所以有这种行为,其原因是:一般用户认为拿起听筒并建立通话连接所需的时间要比挂掉电话所需的时间长。在有些情形下(如本例),调度程序对响应时间指标起不了作用;但是在另外一些情形下,调度程序还是能够做一些事的,特别是在出现差的进程顺序选择时。
实时系统有着与交互式系统不一样的特性,所以有不同的调度目标。实时系统的特点是或多或少必须满足截止时间。例如,如果计算机正在控制一个以正常速率产生数据的设备,若一个按时运行的数据收集进程出现失败,会导致数据丢失。所以,实时系统最主要的要求是满足所有的(或大多数)截止时间要求。
在多数实时系统中,特别是那些涉及多媒体的实时系统中,可预测性是很重要的。偶尔不能满足截止时间要求的问题并不严重,但是如果音频进程运行的错误太多,那么音质就会下降得很快。视频品质也是一个问题,但是人的耳朵比眼睛对抖动要敏感得多。为了避免这些问题,进程调度程序必须是高度可预测和有规律的。在本章中我们将研究批处理和交互式调度算法,而把有关实时调度处理的研究放到第7章多媒体操作系统中。