第3章 进程线程模型
操作系统中最核心的概念是进程,这是对正在运行程序的一个抽象。操作系统的其他所有 内容都是围绕着进程的概念展开的,所以,透彻地理解进程是非常重要的。即使可以利用的 CPU只有一个,但是通过进程,可以使系统具有支持并发操作的能力,可将一个单独的CPU变换 成多个虚拟的CPU。本章通过大量的细节探究进程,以及进程中的运行实体——线程。
3.1多道程序设计模型
采用多道程序设计可以提高CPU的利用率。多道程序设计技术充分发挥了处理器与外围 设备以及外围设备之间的并行工作能力,从而提高处理器和其他各种资源的利用率。下面从程 序的顺序执行、程序的执行环境和程序的并发执行几方面介绍多道程序设计模型。
3.1.1程序的顺序执行
程序是一个在时间上按严格次序前后相继的操作序列,这些操作是机器指令或高级语言编 写的语句。人们习惯的传统程序设计方法是顺序程序设计,计算机也是以顺序方式工作的:CPU 一次执行一条指令,对内存一次访问一个字节或字,对外部设备一次传送一个数据块。顺序处理 也是人们习惯的思考方法,为了解决一个复杂的问题,人们把它分解成一些较为简单、易于分析 的小问题,然后逐个解决。也可以把一个复杂的程序划分为若干个程序段,然后按照某种次序逐 个执行这些程序段。
我们把一个具有独立功能的程序独占CPU直到得到最终结果的过程称为程序的顺序执行。 程序的顺序执行具有如下特点:
1)顺序性
程序所规定的动作在机器上严格地按顺序执行。每个动作的执行都以前一个动作的结束为 前提条件,即程序和机器执行的活动严格一一对应。
2)封闭性
程序运行后,其计算结果只取决于程序自身,程序执行得到的最终结果由给定的初始条件决 定,不受外界因素的影响。程序所使用的资源(包括CPU、内存、文件等)是专有的,这些资源的状态(除了初始状态外)只有程序本身的动作才能改变。
3)程序执行结果的确定性
程序执行的结果与它的执行速度无关,也称为程序执行结果与时间无关性。即CPU在执行 程序时,任意两个动作之间的停顿对程序的计算结果都不会产生影响。
4)程序执行结果的可再现性
如果程序在不同的时间执行,只要输入的初始条件相同,则无论何时重复执行该程序都会得 到相同的结果
程序的顺序性和封闭性是一切顺序程序所应具有的特性。从这两个特性出发,不难引出程 序执行时所具有的另外两个特性。顺序程序与时间无关的特性,可使程序的编制者不必去关心 不属于其控制的那些细节(如操作系统的调度算法和外部设备操作的精确时间等);顺序程序执 行结果的可再现性,则对程序检测和校正程序的错误带来了方便。
3.1.2 多道程序系统中程序执行环境的变化
1.多道程序设计技术的引入
为了提高计算机系统中各种资源的利用效率,缩短作业的周转时间,在现代计算机中广泛采 用多道程序技术,使多种硬件资源能并行工作。顺序程序的上述特性是为人们所理解和熟悉的, 但这不是一切程序所共有的。在追求多部件并行和多任务共享资源的多道程序操作系统的程序 设计中,这些性质就不复存在了。
在许多情况下,要求计算机能够同时处理多个具有独立功能的程序,以增强系统的处理能力 和提高机器的利用率。通常采用并行操作技术,使系统的各种硬件资源尽量做到并行工作。
多道程序同时在系统中存在并且运行,这时的工作环境与单道程序的运行条件大不相同。 首先,每个用户程序都需要一定的资源,如内存、设备、CPU时间等,因此系统中的软、硬件资源 不再是单个程序独占,而是由几道程序所共享。这样,共享资源的状态就由多道程序的活动共同决定。
此外,系统中各部分的工作方式不再是单纯串行的,而是并发执行的。所谓并发执行,对于单CPU,这些并发程序按给定的时间片交替地在处理机上执行,其执行的时间是重叠的;对于多CPU,这些并发程序在各自处理机上运行。
举一个例子,假定有两个程序A和B都要执行。A程序的执行顺序为:在CPU上执行10S,在设备DEV1上执行5s、又在CPU上执行5s、在设备DEV2上执行10s、最后在CPU上执行10s;B程序的执行顺序为:在设备DEV2上执行10s、在CPU上执行10s、在设备DEV1上执行5S、又在CPU上执行5S、最后在设备DEV2上执行10s0
在顺序环境下,或者程序A先执行,然后程序B执行;或者程序B先执行,程序A后执行。假设程序A先执行,如图3-1(a)所示,程序A、B全部执行完毕需要80s时间,其中有40s是程序使用CPU,15s使用设备DEV1,25s使用设备DEV2。经过计算,得出在顺序环境下:
CPU的利用率=40/80=50%
DEVt的利用率二15/80二18.+5%
DEV2的利用率=25/80=31.25%
而在并发环境下,程序A、B可以同时执行,当程序A在CPU上执行时,程序B可以在设备
DEV1,上执行,如图3-l(b)所示,程序A、B全部执行完毕需要45s时间。经过计算,得出在并发环境下:
CPU的利用率=40/45=89%
DEV,的利用率=15/45=33%
DEV2的利用率=25/45=56%
由此可见,采用多道程序设计技术执行同样的两个程序,就能大大改进系统性能。
图3-1多道程序设计例子
2.多道程序设计环境的特点
所谓多道程序设计,就是允许多个程序同时进入内存并运行。多道程序设计是操作系统所采用的最基本、最重要的技术,其根本目的是提高整个系统的效率。
衡量系统效率的尺度是系统吞吐量。所谓吞吐量是指单位时间内系统所处理作业(程序)的道数(数量)。如果系统的资源利用率高,则单位时间内所完成的有效工作多,吞吐量大;反之,如果系统的资源利用率低,则单位时间内所完成的有效工作少,吞吐量小。引入多道程序设计后,提高了设备资源利用率,使系统中各种设备经常处于忙碌状态,提高了内存资源利用率;同时进人系统中的多个程序可以保存于内存的不同区域中,提高了处理机资源利用率。最终,提高系统吞吐量。
多道程序设计改善了各种资源的使用情况,从而增加了吞吐量,提高了系统效率,但也带来了资源竞争。因此,在实现多道程序设计时,必须协调好资源使用者与被使用资源之间的关系,即对处理机资源加以管理,以实现处理机在各个可运行程序之间的分配与调度;对内存资源加以管理,将内存分配给各个运行程序,还要解决程序在内存的定位问题,并防止内存中各个程序之间互相干扰或对操作系统的干扰;对设备资源进行管理,使各个程序在使用设备时不发生冲突。
多道程序设计环境具有以下特点:
1)独立性
在多道环境下执行的每道程序都是逻辑上独立的,且执行速度与其他程序无关,执行的起止时间也是独立的。
2)随机性
在多道程序环境下,程序和数据的输入与执行开始时间都是随机的。
3)资源共享性
一般来说,多道环境下执行程序的道数总是多于计算机系统中CPU的个数,单CPU系统更是如此。显然,同时执行的各个程序只能共享系统中已有的CPU。同样,输入输出设备、内存、信息等资源都将被各个程序所共享。资源共享将导致对进程执行速度的制约。
3.1.3程序的并发执行
所谓程序并发执行,是指两个或两个以上程序在计算机系统中同处于已开始执行且尚未结束的状态。能够参与并发执行的程序称为并发程序。引人程序并发执行,是为了充分利用系统资源,提高计算机的处理能力。但是,程序并发执行产生了一些和程序顺序执行时不同的特性,概括如下。
1.并发程序在执行期间具有相互制约关系
多道程序的并发执行总是伴随着资源的共享和竞争,从而制约了各道程序的执行速度,使本来并无逻辑关系的程序之间产生了相互制约的关系;而各程序活动的工作状态与所处环境有密切关系,使并发执行的程序具有“执行一暂停一执行”的活动规律。
2.程序与计算不再一一对应
在并发执行中,允许多个用户作业调用一个共享程序段,从而形成了多个“计算”。例如,在分时系统中,一个编译程序往往同时为几个用户服务,该编译程序便对应了几个“计算”。
3.并发程序执行结果不可再现
并发程序执行结果与其执行的相对速度有关,是不确定的。
多道程序的并发执行是指它们在宏观上是同时进行的,但从微观上看,在单CPU系统中,它们仍然是顺序执行的。
3.2进程模型
并发程序和顺序程序的执行有本质上的差异。为了能更好地描述程序的并发执行,实现操作系统的并发性和共享性,引入“进程”的概念。本节介绍进程的概念、进程状态及状态转换、描述进程属性的数据结构——进程控制块,以及对进程可实施的主要操作。
3.2.1进程的概念
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资 分配和调度的一个独立单位。
从操作系统角度来看,可将进程分为系统进程和用户进程两类。系统进程执行操作系统程序,完成操作系统的某些功能。用户进程运行用户程序,直接为用户服务。系统进程的优先级通常高于一般用户进程的优先级。
1.进程与程序的联系和区别
进程和程序既有联系又有别。
1)联系
程序是构成进程的组成部分之一,一个进程的运行目标是执行它所对应的程序,如果没有程序,进程就失去了其存在的意义。从静态的角度看,进程是由程序、数据和进程控制块(PCB)三部分组成。
2)区别
程序是静态的,而进程是动态的。
进程既然是程序的执行过程,因而进程是有生命周期的,有诞生亦有消亡。因此,程序的存在是永久的,而进程的存在是暂时的,动态地产生和消亡。一个进程可以执行一个或几个程序,一个程序亦可以构成多个进程。例如,一个编译进程在运行时,要执行词法分析、语法分析、代码生成和优化等几个程序。或者一个编译程序可以同时生成几个编译进程,为几个用户服务。进程具有创建其他进程的功能。被创建的进程称为子进程,创建者称为父进程,从而构成进程家族。
2.进程的特性
进程的概念能很好地描述程序的并发执行,并且能够揭示操作系统的内部特性。事实上,操作系统的并发性和共享性正是通过进程的活动体现出来的。
进程具有以下特性:
1)并发性
可以同其他进程一道向前推进,即一个进程的第一个动作可以在另一个进程的最后一个动作结束之前幵始。
2)动态性
进程对应着程序的执行过程,体现在两方面:其一,进程动态产生、动态消亡;其二,在进程生命周期内,其状态动态变化。
3)独立性
一个进程是一个相对完整的资源分配单位。
4)交往性
一个进程在运行过程中可能会与其他进程发生直接的或间接的相互作用。
5)异步性
每个进程按照各自独立的、不可预知的速度向前推进。
3.2.2进程的状态及其状态转换
进程在从创建到终止的全过程中一直处于一个不断变化的过程。为了刻画进程的这个变化过程,所有操作系统都把进程分成若干种状态,约定各种状态间的转换条件。对进程状态的刻画也经历了一个不断精确化的过程。下面我们就讨论进程的状态模型。
1.三状态进程模型
运行中的进程可以处于以下三种状态之一:运行、就绪、等待。
1)运行状态(Running)
运行状态是指进程已获得CPU,并且在CPU上执行的状态。显然,在一个单CPU系统中,最多只有一个进程处于运行态。
2)就绪状态(Ready)
就绪状态是指一个进程已经具备运行条件,但由于没有获得CPU而不能运行所处的状态。一旦把CPU分配给它,该进程就可运行。处于就绪状态的进程可以是多个。
3)等待状态(Waiting)
等待状态也称阻塞状态或封锁状态。是指进程因等待某种事件发生而暂时不能运行的状态。例如,当两个进程竞争使用同一个资源时,没有占用该资源的进程便处于等待状态,它必须等到该资源被释放后才可以去使用它。引起等待的原因一旦消失,进程便转为就绪状态,以便在适当的时候投入运行。系统中处于等待状态的进程可以有多个。
在任何时刻,任何进程都处于且仅处于三种状态之一。进程在运行过程中,由于它自身的进展情况和外界环境条件的变化,三种基本状态可以相互转换。这种转换由操作系统完成,对用户是透明的。它也体现了进程的动态性。图3-2表示了三种基本状态之间的转换及其典型的转换原因。
1)就绪—运行
处于就绪状态的进程已具备了运行的条件,但由于未能获得处理机,故仍然不能运行。对于单处理机系统而言,因为处于就绪状态的进程往往不止一个,同一时刻只能有一个就绪进程获得处理机。进程调度程序根据调度算法(如优先级或时间片)把处理机分配给某个就绪进程,建立该进程运行状态标记,并把控制转入该进程的启动程序,把它由就绪状态变为运行状态。这样进程就投入运行。
2)运行—就绪
这种状态变化通常出现在分时操作系统中。正在运行的进程由于规定的运行时间片用完而使系统发出超时中断请求,超时中断处理程序把该进程的状态修改为就绪状态,根据其自身的特征而插人就绪队列的适当位置,保留进程现场信息,收回处理机并转人进程调度程序。于是,正在运行的进程就由运行状态变为就绪状态。
3)运行—等待
处于运行状态的进程能否继续运行,除了受时间限制外,还受其他种种因素的影响。例如,运行中的进程需要等待文件的输入(或其他进程同步操作的影响)时,控制便自动转入系统控制程序,通过信息管理程序及设备管理程序进行文件输入。在输入过程中,这个进程并不恢复到运行状态,而是由运行变成等待(此时,标记等待原因,并保留当前进程现场信息),然后控制转入进程调度程序。进程调度程序根据调度算法把处理机分配给原已处于就绪状态的进程。
4)等待—就绪
等待的进程在其被阻塞的原因获得解除后,并不能立即投入运行,因为处理机满足不了进程的需要,于是将其状态由等待变成就绪,仅当进程调度程序把处理机再次分配给它时,才可恢复现场继续运行。
2.五状态进程模型
五状态进程模型中,进程状态被分成下列五种状态。进程在运行过程中主要是在就绪、运行和阻塞三种状态间进行转换。创建状态和退出状态描述进程创建的过程和进程退出的过程。
(1)运行状态(Rimningh进程占用处理机资源;处于此状态的进程的数目小于等于处理机的数目。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
(2)就绪状态(Ready):进程已获得除处理机外的所需资源,等待分配处理机资源;只要分配处理机就可执行。如图3-3所示,就绪进程可以排成一个就绪队列,也可以按多个优先级来划分队列。例如:当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由于i/o操作完成而进入就绪状态时,排入高优先级队列。
(3)阻塞状态(Blocked):由于进程等待I/O操作或进程同步等条件而暂停运行时处于阻塞状态。在条件满足之前,即使把处理机分配给该进程,也是无法继续执行的。如图3-3所示,所有处于阻塞状态的进程排成一个阻塞队列,也可以按等待事件的不同分成多个子队列。
(4)创建状态(New):进程正在创建过程中,还不能运行。操作系统在创建状态要进行的工作包括分配和建立进程控制块表项、建立资源表格(如打开文件表)并分配资源,加载程序并建立地址空间表等。
(5)结束状态(Exit):进程已结束运行,回收除进程控制块之外的其他资源,并让其他进程从进程控制块中收集有关信息(如记账和将退出代码传递给父进程)。
操作系统中多个进程的并发执行是通过进程交替进入运行状态来实现的。在五状态进程模型中进程的主要状态交替循环有两个,一个由调度与超时这两个转换构成。当一个正处于运行状态的进程超时后放入就绪队列,并修改进程状态为就绪状态;然后通过调度选择另一个就绪进程进入运行状态,完成一次运行进程的交替。另一个由调度、等待事件和事件出现这三个转换构成。当一个正处于运行状态的进程需要等待某事件发生时主动放弃处理机,进人阻塞状态;然后通过调度选择另一个就绪进程进入运行状态;当进程所等待的事件出现时,相应进程被从阻塞队列中取出,并放人就绪队列,同时进程状态从阻塞状态转换成就绪状态,等待进入运行状态。这样就完成了一次从运行到阻塞,再到就绪的状态循环。
下面是五状态进程模型中的主要状态转换。
(1)创建进程:创建一个新进程,以运行一个程序。创建进程的可能原因包括用户登录、操作系统创建以提供某项服务、批处理作业等。
(2)提交(Admith完成一个新进程的创建过程,新进程进入就绪状态。由于性能、内存、进程总数等原因,系统会限制并发进程总数。
(3)调度运行(Dispatch):从就绪进程表中选择一个进程,进入运行状态。
(4)释放(Release):由于进程完成或失败而终止进程运行,进入结束状态。为了简洁,状态变迁图中只画出了运行状态到结束状态间的释放转换;但实际上,还存在从就绪状态或阻塞状态到结束状态的释放转换。运行到结束的转换可分为正常退出(Exit)和异常退出(abort);其中异常退出是指进程执行超时、内存不够、非法指令或地址访问、I/O操作失败、被其他进程所终止等原因而退出。可能导致从就绪或阻塞到结束转换的可能是由于多种原因引发的,如父进程可在任何时间终止子进程。
(5)超时(Timeout):由于用完时间片或高优先级进程就绪等原因导致进程暂停运行。
(6)事件等待(Event Wait):进程要求的事件未出现而进入阻塞;可能的原因包括:申请系统服务或资源、通信、I/O操作等。
(7)事件出现(Event Occurs):进程等待的事件出现;如:操作完成、申请成功等。
3.七状态进程模型
五状态进程模型没有区分进程地址空间位于内存还是外存,而在操作系统中引入虚拟存储管理技术后,需要进一步区分进程的地址空间状态。该问题的出现是由于进程优先级的引入,一些低优先级进程可能等待较长时间,从而被对换至外存。这种做法可得到下列的好处:
(1)提高处理机效率:就绪进程表为空时,有空闲内存空间用于提交新进程,可提高处理机效率;
(2)可为运行进程提供足够内存:资源紧张时,可把某些进程对换至外存;
(3)有利于调试:在调试时,挂起被调试进程,可方便对其地址空间进行读写。
如图3-4所示,与五状态进程模型相比,七状态进程模型把原来的就绪状态和阻塞状态进行了细分,增加了就绪挂起和阻塞挂起两个状态。这时原来的就绪状态和阻塞状态的意义也发生了一些变化。下面列出的是在挂起进程模型中的四种意义有变化或新的状态。
(1)就绪状态(Ready):进程在内存且可立即进入运行状态;
(2)阻塞状态(Blocked):进程在内存并等待某事件的出现;
(3)阻塞挂起状态(Blocked’Suspend):进程在外存并等待某事件的出现。
(4)就绪挂起状态(Ready,Suspend):进程在外存,但只要进入内存,即可运行;
在七状态进程模型中,新引入的状态转换有挂起和激活两类,意义有变化的转换有事件出现和进程提交两类。
图3-4 七状态进程模型
(1)挂起(Suspend):把一个进程从内存转到外存;可能有以下几种情况:
•阻塞到阻塞挂起:没有进程处于就绪状态或就绪进程要求更多内存资源时,会进行这种转换,以提交新进程或运行就绪进程。
•就绪到就绪挂起:当有高优先级阻塞(系统认为会很快就绪的)进程和低优先级就绪进程时,系统会选择挂起低优先级就绪进程。
•运行到就绪挂起:对抢先式分时系统,当有高优先级阻塞挂起进程因事件出现而进入就绪挂起时,系统可能会把运行进程转到就绪挂起状态。
(2)激活(Activate):把一个进程从外存转到内存;可能有以下几种情况:
•就绪挂起到就绪:就绪挂起进程优先级高于就绪进程或没有就绪进程时,会进行这种转换。
•阻塞挂起到阻塞:当一个进程释放足够内存时,系统会把一个高优先级阻塞挂起进程激活,系统认为会很快出现该进程所等待的事件。
(3)事件出现(Event Occurs):进程等待的事件出现;如操作完成、申请成功等;可能的情况有:
•阻塞到就绪:针对内存进程的事件出现。
•阻塞挂起到就绪挂起:针对外存进程的事件出现。
(4)提交(Admit):完成一个新进程的创建过程,新进程进入就绪状态或就绪挂起状态。进人就绪挂起的原因是系统希望保持一个大的就绪进程表(挂起和非挂起)。
3.2.3进程控制块
为了便于系统控制和描述进程的活动过程,在操作系统核心中为进程定义了一个专门的数据结构,称为进程控制块(Process Control Block,PCB)。
系统利用PCB来描述进程的基本情况以及进程的运行变化过程。PCB是进程存在的唯一标志,当系统创建一个进程时,为进程设置一个PCB,再利用PCB对进程进行控制和管理。撤销进程时,系统收回它的PCB,进程也随之消亡。
1.PCB的内容
PCB的内容可以分成调度信息和现场信息两大部分。调度信息供进程调度时使用,描述了进程当前所处的状况,它包括进程名、进程号、存储信息、优先级、当前状态、资源清单、“家族”关系、消息队列指针、进程队列指针和当前打开文件等。现场信息刻画了进程的运行情况,由于每个进程都有自己专用的工作存储区,其他进程运行时不会改变它的内容。所以,PCB中的现场信息只记录那些可能会被其他进程改变的寄存器,如程序状态字、时钟、界地址寄存器等。一旦中断进程的运行,必须把中断时刻的内容记入PCB的现场信息。
需要指出的是,PCB的内容和大小随系统不同而异,它不仅和具体系统的管理及控制方法有关,也和系统规模的大小有关。
2.进程的组成
进程由指令、数据和进程控制块三部分组成。PCB是进程的“灵魂”,由于进程控制块中保存有进程的地址信息,通过PCB可以得到进程程序的存储位置,也可以找到整个进程。指令和数据是进程的“躯体”。由于现代操作系统提供指令共享的功能,这就要求该段代码是可再人程序,且与数据分离。
所谓可再人程序是指“纯”代码的程序,由指令组成,即在运行过程中不修改自身。
3.PCB组织
为了便于管理,系统把所有的PCB用适当方式组织起来。一般说来,大致有以下三种组织方式:
1)线性方式
将所有的PCB不分状态组织在一个连续表(称PCB表)中,该方式的优点是简单,且不需要额外的开销,适用于进程数目不多的系统;但缺点是往往需要扫描整个PCB表。如图3-5(a)所示。
2)索引方式
对于具有相同状态的进程,分别设置各自的PCB索引表,表目为PCB在PCB表(线性表)中的地址。于是就构成了就绪索引表和等待索引表。另外,在内存固定单元设置三个指针,分别指示就绪索引表和等待索引表的起始地址以及执行态PCB在PCB表中的地址,如图3-5(b)所示。
3)链接方式
对于具有相同状态进程的PCB,通过PCB中的链接字构成一个队列。链接字指出本队列下一PCB在PCB表中的编号(或地址),编号为0表示队尾。队首由内存固定单元中相应的队列指针指示。如此便形成就绪队列和等待队列,等待队列可以有多个,对应于不同的等待原因,如等待I/O操作完成,等待分配内存,等待接受消息等。就绪队列的排队原则跟高度策略有关,可以按优先数据排序,也可以按“先进先出”的原则出队等。另外,还可以将PCB表中的各空表目连接起来构成一个自由队列。若队列指针为0,表示该队列为空,如图3-5(c)所示。
图3-5 PCB的组织方式
- 进程的队列
为了实现对进程的管理,系统将所有进程的PCB排成若干个队列。通常,系统中进程队列分成如下三类。如图3-6所示。
1)就绪队列
整个系统一个,所有处于就绪状态的进程都按照某种原则排在该队列中。进程入队和出队的次序与处理机调度算法有关。在有些系统中,就绪队列可能有若干个。
2)等待队列
每一个等待事件一个队列。当进程等待某一事件时,进人与该事件相应的等待队列。当某事件发生时,与该事件相关的一个或多个进程离开相应的等待队列。
3)运行队列
在单CPU系统中整个系统有一个运行队列。实际上,一个运行队列中只有一个进程,可用一个指针指向该进程。
3.2.4进程控制
进程有一个从创建到消亡的生命周期,进程控制的作用就是对进程在整个生命周期中各种状态之间的转换进行有效的控制。进程控制是通过原语来实现的。
原语通常由若干条指令所组成,用来实现某个特定的操作。通过一段不可分割的或不可中断的程序实现其功能。原语的执行必须是连续的,一旦开始执行就不能间断,直到执行结束。原语是操作系统核心(不是由进程而是由一组程序模块所组成)的一个组成部分,它必须在管态下执行,并且常驻内存。原语和系统调用都可以被进程所调用,两者的差别在于原语有不可中断性,它是通过在其执行过程中关闭中断实现的,且一般由系统进程调用。许多系统调用的功能都可用目态下运行的系统进程完成,而不一定要在管态下完成。例如文件的建立、打开、关闭、删除等系统调用,都是借助中断进入管态,然后转交给相应的进程,最终由进程实现其功能。
1.进程控制原语
用于进程控制的原语一般有:创建进程、撤销进程、挂起进程、激活进程、阻塞进程、唤醒进程以及改变进程优先级等。
1)创建原语
一个进程可以使用创建原语创建一个新的进程,前者称为父进程,后者称为子进程,子进程又可以细建新的子进程,构成新的父子关系。从而整个系统可以形成一个树形结构的进程家族。
创建一个进程的主要任务是建立进程控制块PCB。具体操作过程是:先申请一空闲PCB区域,将有关信息填入PCB,置该进程为就绪状态,最后把它插入就绪队列中。
2)撤销原语
当一个进程完成任务后,应当撤销它,以便及时释放它所占用的资源。撤销进程的实质是撤销PCB。一旦PCB撤销,进程就消亡了。
具体操作过程是:找到要被撤销进程的PCB,将它从所在队列中消去,撤销属于该进程的一切“子孙进程”,释放被撤销进程所占用的全部资源,并消去被撤销进程的PCB。
3)阻塞原语
某进程执行过程中,需要执行I/O操作,则由该进程调用阻塞原语把进程从运行状态转换为阻塞状态。
具体操作过程是:由于进程正处于运行状态,因此首先应中断CPU执行,把CPU的当前状态保存在PCB的现场信息中,把进程的当前状态置为等待状态,并把它插入到该事件的等待队列中去。
4)唤醒原语
一个进程因为等待事件的发生而处于等待状态,当等待事件完成后,就用唤醒原语将其转换为就绪状态。
具体操作过程是:在等待队列中找到该进程,置进程的当前状态为就绪状态,然后将它从等待队列中撤出并插入到就绪队列中排队,等待调度执行。
2.UNIX类操作系统的进程控制操作
在UNIX类操作系统中,父进程通过调用fork()函数创建子进程。典型的步骤包括:
(1)为子进程分配一个空闲的proc结构(进程描述符);
(2)赋予子进程唯一标识pid;
(3)以一次一页的方式复制父进程用户地址空间;
(4)获得子进程继承的共享资源的指针,如打开的文件和当前工作目录等;
(5)子进程就绪,加入调度队列;
(6)对子进程返回标识符0;向父进程返回子进程的Pid。
以上步骤说明新创建的子进程基本与父进程相同:子进程得到与父进程用户地址空间相同的一份拷贝,包括文本、数据和bss段、堆以及用户栈;子进程还获得与父进程任何打开文件描述符相同的拷贝,这就意味着当父进程调用fork()函数时,子进程可以读写父进程中打开的任何文件。父进程和新建子进程的区别在于它们有不同的pit
fork()函数执行的特点是,只被调用一次,却会返回两次:一次是在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork()返回子进程的pid。在子进程中,fork()返回0。因为子进程的pid总是非零的,通过返回值就可以区分程序是在父进程还是在子进程中执行。fork()函数复制了一个自己,但是,创建了子进程并非要运行另一个与父进程一模一样的进,绝大部分子进程需要运行不同于父进程的程序代码,这时我们需要调用exec()函数来替换、父进程的代码。而exec()函数为子进程运行不同于父进程
的代码提供了一条途径。
执行exec(),其典型的步骤包括:
(1)在原进程空间装入新程序的代码、数据、堆和栈;
(2)保持进程ID和父进程ID等;
(3)继承控制终端;
(4)保留所有文件信息,如目录、文件模式和文件锁等。
在UNIX中父进程与子进程的执行是异步的,因此,父进程可能早于子进程结束,如此一来,子进程的资源,例如内存,就有可能无法归还给父进程,引起内存泄露等问题。wait()函数就是父进程用来获取子进程的结束状态并回收资源的,父进程调用wait()函数自我阻塞,等候子进程结束发来信号,该信号唤醒父进程后由父进程回收子进程的各项资源、清理表格及回收内存等;若子进程先于父进程结束,此时,子进程会暂时变为僵尸状态,继续占有部分资源,直到父进程运行wait()时资源才被回收(此时父进程不需要阻塞)。
如果子进程不等父进程回收,而是在程序末尾直接调用函数exit()退出,那么,在标准UNIX系统上,由于子进程调用了exit(),会刷新关闭所有标准I/O流,包括标准输出。虽然这是由子进程执行的,但却是在父进程的地址空间中进行的,所以所有受到影响的标准I/O对象都是在父进程中的。当父进程再调用标准输出时,标准输出已被关闭了,于是出错返回-1。因此一般f进程不使用exit()函数,而是用内核的_exit()函数,并等待父进程回收。(注:改进后的Linux操作系统与UNIX不同,有兴趣的读者可以自行查阅相关手册。)
信号signal是UNIX处理异步事件的经典方法。信号可以说是进程控制的一部分。信号的产生条件如下:
(1)当用户按某些终端键时,产生信号。
(2)硬件异常产生信号:除数为0、无效的存储访问等。
(3)进程用km()函数可将信号发送给另一个进程或进程组。
(4)用户可用kill命令将信号发送给其他进程。
(5)当检测到某种软件条件已经发生,并将其通知有关进程。
下面给出几个实例来说明上述函数的使用。
首先给出一个使用fork()创建子进程的示例。当fork()调用返回时,在父进程和子进程中x的值都为1。随后,子进程执行对x的增值并输出其x的副本。相似地,父进程中减少x的值并输出其x的副本。
int main()
{
pid_t pid;
int x=1;
pid=fork();
if (pid==0{/*子进程*/
printf(“child:x=%d\n”,++x);
exit(0);
}
/*父进程*/
printf(“parent:x=%d\n”,--x);
exit(0);
}
在UNIX系统上运行以上程序时,会得到如下结果:
parent:x=0
child:x=2
下面给出三个程序A、B和C:
程序A int main() { fork(); printf(“hello!\n”); exit(0) } |
程序B int main() { fork(); fork(); printf(“hello!\n”); exit(0) } |
程序C int main() { fork(); fork(); fork(); printf(“hello!\n”); exit(0) } |
程序A中,父进程执行fork()函数后创建了一个子进程。父子进程都调用一次printf(),所以程序A打印两个hdl%程序B中,父进程调用fork()创建了一个子进程,然后父子进程都调用一次fork(),又创建了两个进程,于是就有了4个进程。4个进程都调用一次printf(),所以程序B打印4个hello。按此思路分析,程序C将打印8个hello。
下面给出aec()的使用示例。
int main()
{
int pid=0;
if (pid=fork()<0 /*若创建失败*/
status=-1;
else if(pid==0){ /*若位于子进程*/
exec("1s”); /*运行外部程序1s*/
printf(“can'tgohere\n”);
_exit(127);
}
else{
while(wait(NULL)<0) /*回收子进程资源*/
{
}
}
上述程序经正确编译链接后运行,子进程运行的是命令“1s”,而exec()函数其后的代码将会被1s替换不再运行。只有当exec()在调用“1s”出错时(例如无权限执行),才会打印出“can’t go here”并执行_exit(127),等待父进程用wait()回收。
对于signal()函数,下面是相关的示例。
int main()
{
signal(SIGINT,SIG_DFL);
for(;;);
return 0;
}
运行该程序,并在运行期间按下Ctrl+C组合键可以终止该进程。
把signal(SIGINT,SIG_DFL);这句去掉,效果也是一样的(信号缺省)。
如果不想上述程序被按Ctrl+C组合键终止,那么可以将signal(SIGINT,SIG_DFL);语句改为signal(SIGINT,SIG_IGN);语句,该语句将忽略任何按Ctrl+C组合键的信号。只有按下Ctrl+C组合键时才会退出。
再看下面实例。 、
int main()
{
signal_handlerp_signal=signal_handler_fun;
signal(SIGINT,p_signal); /*接收到信号*/
for(;;);
return0;
}
void signal_handler_fun(intsignal_num)
{
printf("Opps!I catch the signal!\n”,signal_num);
}
上述程序运行时,当按下Ctrl+C组合键时,主程序接收到信号,通常情况下是退出程序,但彳是在本例中,当接收到信号时,将根据signal_hangler的指示调用相应函数并执行,显示出:
Opps! I catch the signal!
(本例中,Ctrl+C组合键的信号值为2。)
3.3线程模型
自从20世纪60年代提出进程概念以来,在操作系统中一直都是以进程作为独立运行的基本单位。直到加世纪80年代中期,人们又提出了比进程更小的能独立运行的基本单位——线程,并试图用它来提高系统内程序并发执行的程度,从而可进一步提高系统效率。近几年,线程概念已得到广泛应用,不仅在新推出的操作系统中大多都已引入了线程概念,而且在新推出的数据库管理系统和其他应用软件中,也都纷纷引入线程来改善系统的性能。
3.3.1线程的引入
如果说在操作系统中引人进程的目的是为了使多个程序并发执行,以改善资源利用率及提高系统效率,那么,在操作系统中再引入线程,则是为了减少程序并发执行时所付出的时间和空间开销,使操作系统具有更好的并发性。
进程具有两个基本属性,即进程是一个可拥有资源的独立单位;同时又是一个可以独立调度和分派的基本单位。正是由于进程具有这两个基本属性,才使之成为一个能独立运行的基本单位,从而也构成了进程并发执行的基础。
然而为使程序能并发执行,系统还必须进行以下的一系列操作:
(1)创建进程。系统在创建一个进程时,必须为其分配所需的所有资源(除CPU外),包括内存空间、1/0设备以及建立相应的数据结构PCB。
(2)撤销进程。系统在撤销进程时必须先对这些资源进行回收操作,然后再撤销PCB
(3)进程切换。在对进程进行切换时,由于要保留当前进程的CPU环境和设置新选中进程的CPU环境,为此需花费不少CPU时间。
总而言之,由于进程是一个资源拥有者,因而在进程的创建、撤销和切换中,系统必须为之付出较大的时空开销。也正因为如此,在系统中所设置的进程数目不宜过多,进程切换的频率也不宜过高,但这也就限制了并发程度的进一步提高。
如何能使多个程序更好地并发执行,同时又尽量减少系统的开销,已成为近年来设计操作系统时所追求的重要目标。于是,有不少操作系统的开发者想到,可否将进程的上述两个属性分开,由操作系统分开进行处理。即如果将作为调度和分派的基本单位不同时作为独立分配资源的单位,以使之轻装运行;而对拥有资源的基本单位,又不频繁地对之进行切换。正是在这种思想的指导下,产生了线程的概念。
3.3.2线程的基本概念
在引入线程的操作系统中,线程是进程中的一个实体,是CPU调度和分派的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈),但它可与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中也呈现出间断性。相应地,线程也同样有就绪、等待和运行三种基本状态。在有的系统中,线程还有终止状态。
1.线程的属性
线程有如下属性:
●每个线程有一个唯一的标识符和一张线程描述表,线程描述表记录了线程执行的寄存器和栈等现场状态。
●不同的线程可以执行相同的程序,即同一个服务程序被不同用户调用时操作系统为它们创建不同的线程。
●同一进程中的各个线程共享该进程的内存地址空间。
●线程是处理器的独立调度单位,多个线程是可以并发执行的。在单CPU的计算机系统中,各线程可交替地占用CPU;在多CPU的计算机系统中,各线程可同时占用不同的CPU,若各个CPU同时为一个进程内的各线程服务则可缩短进程的处理时间。
●一个线程被创建后便开始了它的生命周期,直至终止,线程在生命周期内会经历等待、就绪和运行等各种状态变化。
2.引入线程的好处
●创建一个新线程花费时间少(结束亦如此)。创建线程不需另行分配资源,因而创建线程的速度比创建进程的速度快,且系统的开销也少。
●两个线程的切换花费时间少。由于同一进程内的线程共享内存和文件,线程之间相互通信无须调用内核,故不需要额外的通信机制,使通信更简便,信息传送速度也快。
●线程能独立执行,能充分利用和发挥处理器与外围设备并行工作能力。
3.线程与进程的比较
线程具有许多传统进程所具有的特征,故又称为轻量级进程(Light-Weight Process)或进程元;而把传统的进程称为重量级进程(Heavy-Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少也需要有一个线程。下面,主要从调度、并发性、系统开销、拥有资源等方面来对线程和进程进行比较。
1)调度
在传统的操作系统中,拥有资源的基本单位和独立调度、分派的基本单位都是进程。而在引人线程的操作系统中,则把线程作为调度和分派的基本单位,把进程作为资源拥有的基本单位,从而使传统进程的两个属性分开,线程便能轻装运行,这样可以显著地提高系统的并发程度。在同一进程中,线程的切换不会引起进程切换;而在由一个进程中的线程切换到另一进程中的线程时,将会引起进程切换。
2)并发性
在引入线程的操作系统中,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间也可以并发执行,因而使操作系统具有更好的并发性,能更有效地使用系统资源和提高系统的吞吐量。例如,在一个未引入线程的单CPU操作系统中,若仅设置一个文件服务进程,当它由于某种原因被封锁时,便没有其他的文件服务进程来提供服务。在引人了线程的操作系统中,可以在一个文件服务进程中设置多个服务线程。当第一个线程等待时,文件服务进程中的第二个线程可以继续运行;当第二个线程封锁时,第三个线程可以继续执行,从而显著地提高了文件服务的质量以及系统的吞吐量。
3)拥有资源
不论是传统的操作系统,还是设有线程的操作系统,进程都是拥有资源的一个独立单位,它可以拥有自己的资源。一般地说,线程自己不拥有系统资源(也有一点必不可少的资源),但它可以访问其隶属进程的资源。亦即一个进程的代码段、数据段以及系统资源(如已打开的文件、I/O设备等)。可供同一进程的其他所有线程共享。
4)系统开销
由于在创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O设备等。因此,操作系统所付出的开销将显著地大于在创建或撤销线程时的开销。类似地,在进行进程切换时,涉及整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置。而线程切换只需保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。可见,进程切换的开销也远大于线程切换的开销。此外,由于同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现也变得比较容易。在有的系统中,线程的切换、同步和通信都无需操作系统内核的干预。
3.3.3线程实现机制
线程已在许多系统中实现,但实现的方式并不完全相同。
1.用户级线程
第一种实现方式是用户级线程(User-Level Threads),这种线程不依赖于内核。
用户级线程只存在于用户态中,对它的创建、撤销和切换不会通过系统调用来实现,因而这种线程与内核无关。相应地,内核也并不知道有用户级线程的存在,从内核角度考虑,就是按正常的方式管理,即单线程进程。支持用户级线程的典型操作系统是Linux。
这种方法最明显的优点是,用户级线程包可以在不支持线程的操作系统上实现。过去所有的操作系统都属于这个范围,即使现在也有一些操作系统还是不支持线程。通过这一方法,可以用函数库实现线程。
这类实现都有同样的通用结构,如图3-7(a)所示。线程在一个运行时系统的顶部运行,这个运行时系统是一个管理线程的过程的集合,如线程创建、退出、等待等。
在用户空间管理线程时,每个进程需要有其专用的线程表(Thread Table),用来跟踪该进程中的线程。这些表和内核中的进程控制块类似,不过它仅仅记录各个线程的属性,如每个线程的程序计数器、堆栈指针、寄存器和状态等。该线程表由运行时系统管理。当一个线程转换到就绪状态或阻塞状态时,在该线程表中存放重新启动该线程所需的信息,与内核在进程表中存放进程的信息完全一样。
当某个线程做了一些会引起在本地被阻塞的事情之后,例如,等待进程中另一个线程完成某项工作,它调用一个运行时系统的过程,这个过程检查该线程是否必须进入阻塞状态。如果是,它在线程表中保存该线程的寄存器(即它本身的),查看表中可运行的就绪线程,并把新线程的保存值重新装入机器的寄存器中。只要堆栈指针和程序计数器一被切换,新的线程就又自动投入运行。如果机器有一条保存所有寄存器的指令和另一条装入全部寄存器的指令,那么整个线程的切换可以在几条指令内完成。进行类似于这样的线程切换至少比陷人内核要快(或许更多)一个数量级,这是使用用户级线程包的优点。
图3-7线程的实现方式
在线程完成运行时,运行时系统可以把该线程的信息保存在线程表中,进而,它可以调用线程调度程序来选择另一个要运行的线程。保存该线程状态的过程和调度程序都只是本地过程,所以启动它们比进行内核调用效率更高。另一方面,不需要陷入,不需要上下文切换,也不需要对内存高速缓存进行刷新,等等,这就使得线程调度非常快捷。
用户级线程还有另一个优点,即它允许每个进程有自己定制的调度算法。
2.内核级线程
第二种实现方式是内核级线程(Kernel-Supported Threads),这种线程依赖于内核。
内核级线程依赖于内核,即无论是在用户进程中的线程,还是系统进程中的线程,它们的创建、撤销和切换都由内核实现。在内核中保留了一个线程控制块,系统根据该控制块而感知该线程的存在并对线程进行控制。支持内核级线程的典型操作系统是Windows。
如图3-7(b)所示,此时不再需要运行系统了,每个进程中也没有线程表。相反,在内核中有用来记录系统中所有线程的线程表。当某个线程希望创建一个新线程或撤销一个已有线程时,它进行一个系统调用,这个系统调用通过对线程表的更新就完成线程创建或撤销工作。
内核的线程表保存了每个线程的寄存器、状态和其他信息。这些信息和在用户空间中(在运行时系统中)的线程是一样的,但是现在保存在内核中。这些信息是传统内核所维护的每个单线程进程信息(即进程状态)的子集。另外,内核还维护了传统的进程表,以便跟踪进程的状态。
所有能够阻塞线程的调用都以系统调用的形式实现,这与运行时系统过程相比,代价很大。当一个线程阻塞时,内核可以选择运行同一个进程中的另一个线程(若有一个就绪线程)或者运行另一个进程中的线程。而在用户级线程中,运行时系统始终运行自己进程中的线程,直到内核剥夺它的CPU(或者没有可运行的线程存在了)为止。
内核线程不需要任何新的、非阻塞系统调用。另外,如果某个进程中的线程引起了页面失效,内核可以很方便地检查该进程是否有任何其他可运行的线程,如果有,在等待所需要的页面从磁盘读入时,就选择一个可运行的线程运行。这样做的主要缺点是系统调用的代价比较大,所以如果线程的操作(创建、终止等)比较多,就会带来很大的开销。
下面从几个方面对用户级线程和内核级线程进行比较。
1)线程的调度与切换速度
核心级线程的调度和切换与进程的调度和切换十分相似。例如,在线程调度时的调度方式,同样也是采用抢占方式和非抢占方式两种。在线程的调度算法上,也同样可采用时间片轮转法、优先权算法等。当线程调度选中一个线程后,再将处理机分配给它9当然,线程在调度和切换上所花费的开销要比进程小得多。用户级线程的切换通常是发生在一个应用进程的诸线程之间,这时,不仅无需通过中断进入操作系统的内核,而且切换的规则也远比进程调度和切换的规则来得简单。例如,当一个线程封锁后会自动切换到下一个具有相同功能的线程。因此,用户级线程的切换速度特别快。
2)系统调用
当传统的用户进程调用一个系统调用时,要由用户状态转入核心状态,用户进程将被封锁。
当内核完成系统调用而返回时,才将该进程唤醒,继续执行。而在用户级线程调用一个系统调用时,由于内核并不知道有该用户级线程的存在,因而把系统调用看作是整个进程的行为,于是使该进程等待,而调度另一个进程执行。同样是在内核完成系统调用而返回的,进程才能继续执行。如果系统中设置的是内核支持线程,则调度是以线程为单位。当一个线程调用一个系统调用时,内核把系统调用只看作是该线程的行为,因而封锁该线程,于是可以再调度该进程中的其他线程执行。
3)线程执行时间
对于只设置了用户级线程的系统,调度是以进程为单位进行的。在采用轮转调度算法时,各个进程轮流执行一个时间片,这对诸进程而言似乎是公平的。但假如在进程A中包含了一个用户级线程,而在另一个进程B中含有100个线程,这样,进程A中线程的运行时间,将是进程B中各线程运行时间的100倍;相应地,进程A的运行速度比进程B的运行速度快100倍。假如系统中设置的是核心级线程,其调度是以线程为单位进行的,这样,进程B可以获得的CPU时间是进程A的100倍,进程B可使100个系统调用并发工作。
3.混合实现方式
有一些系统同时实现了用户级线程和内核级线程。支持混合方式线程的典型操作系统是Solaris。
人们已经研究了各种试图将用户级线程的优点和内核级线程的优点结合起来的方法。一种方法是使用内核级线程,然后将用户级线程与某些或者全部内核线程多路复用起来。如果釆用这种方法,编程人员可以决定有多少个内核级线程和多少个用户级线程彼此多路复用。这一模型带来最大的灵活度。
采用这种方法,内核只识别内核级线程,并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。如同在没有多线程能力操作系统中某个进程中的用户级线程一样,可以创建、撤销和调度这些用户级线程。在这种模型中,每个内核级线程有一个可以轮流使用的用户级线程集合。
3.3.4Pthread线程包
为编写线程程序,IEEE标准1003.lc定义了线程标准,Pthread是基于该标准实现的线程包。大部分UNIX系统都支持该标准。该标准定义了超过60个函数调用,表3-1中列举了几个主要的函数调用。
所有Pthread线程都有某些特性。每一个都含有一个标识符、一组寄存器(包括程序计数器)和一组存储在结构中的属性。这些属性包括栈大小、调度参数以及其他线程需要的项目。
表3-1 几个主要函数调用
线程调用 |
描述 |
pthread_create |
创建一个新线程 |
pthread_exit |
结束调用的线程 |
pthread_join |
等待一个特定的线程退出 |
pthread_yield |
释放CPU来运行另外一个线程 |
pthread_attr_init |
创建并初始化一个线程的属性结构 |
pthread_attr_destroy |
删除一个线程的属性结构 |
创建一个新线程需要使用pthread_create调用。新创建线程的线程标识符作为函数值返回。
当一个线程完成分配给它的工作时,可以通过调用pthread exit来终止。这个调用终止该线程并释放它的栈。
一般一个线程在继续运行前需要等待另一个线程完成它的工作并退出。可以通过pthread join线程调用来等待别的特定线程的终止。而要等待线程的线程标识符作为一个参数给出。
有时会出现这种情况:一个线程逻辑上没有阻塞,但它已经运行了足够长的时间并且希望给另外一个线程机会去运行。这时可以通过调用pthread_yield完成这一目标而进程中没有这种调用。
pthread_attr_init建立关联一个线程的属性结构并初始化成默认值。这些值(例如优先级)可以通过修改属性结构中的域值来改变。
pthread_attr_destroy删除一个线程的属性结构,释放它占用的内存。它不会影响调用它的线程;这些线程会继续存在。
为了更好地了解pthread是如何工作的,下面给出一个简单的例子。
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#defineNUMBER_OF_THREADS10
void*print_hello_world(void*tid)
{
/*本函数输出线程的标识符,然后退出。*/printf("HelloWorld.%d0,tid");pthread_exit(null);
}
intmain(intargc,char*argv[])
{
/*主程序创建10个线程,然后退出。*/pthread_tthreads[NUMBER_OF_THREADS]intstatus,i;
for(i=1;i<NUMBER_OF_THREADS;i++)printf("Mainhere.Creatingthread%dO,i")
status=pthread_create(&thread[i],NULL,print_hello_world,(void*)i);
if(status!=0)j
printf(npthread_createreturnederrorcode%d0,status'*)exit(-l);
}
}
exit(null);
}
这里主程序在发布其意图后,循环NUMBER_OF_THREADS次,每次创建一个新的线程。如果线程创建失败,会打印出一条错误信息后退出。在创建完所有线程之后,主程序退出。
当创建一个线程时,它打印一条发布信息,然后退出。这些不同信息交错的顺序是不确定的,并且可能在连续运行程序的情况下发生变化。
3.4进程(线程)调度
调度是分层次的,操作系统中,一般将调度分为高级调度、中级调度和低级调度。高级调度
也称作业调度,其主要任务是按一定的原则,对磁盘中处于后备状态的作业进行选择并创建为进程;中级调度的主要任务是按照给定的原则和策略,将处于磁盘对换区中且具备运行条件的就绪进程调人内存,或将处于内存就绪状态或内存阻塞状态的进程交换到对换区;低级调度即进程(线程)调度,是决定就绪队列中哪个进程将获得处理机,并实际将处理机分配给该进程的操作。
一般在大型批处理系统中配有作业调度,而在其他系统中,通常不需配置作业调度;而在采用虚拟存储管理的操作系统中,中级调度被页面调入策略、页面置换策略和页面清除策略所取代,因此,计算机系统中使用最频繁、算法最复杂的是进程(线程)调度。
进程(线程)调度即处理机调度。在多道程序设计环境中,进程(线程)数目往往多于处理机数,这将导致多个进程(线程)互相争夺处理机。进程(线程)调度的任务是控制、协调进程(线程)对CPU的竞争,按照一定的调度算法,使某一就绪进程获得CPU的控制权,转换成运行状态。实际上进程(线程)调度完成一台物理的CPU转变成多台虚拟的(或逻辑的)CPU的工作。
3.4.1概述
1.进程(线程)调度的主要功能
记录系统中所有进程(线程)的执行状况;根据一定的调度算法,从就绪队列中选出一个进程(线程)来,准备把CPU分配给它;把CPU分配给进程(线程),即把选中进程(线程)的进程(线程)控制块内有关的现场信息,如程序状态字、通用寄存器等内容送入处理器相应的寄存器中,从而让它占用CPU运行。
2.进程(线程)调度的时机
执行进程调度一般是在下述情况下发生的:
●正在执行的进程(线程)运行完毕;
●正在执行的进程(线程)调用阻塞原语将自己阻塞起来进入等待状态;
●正在执行的进程(线程)调用了阻塞原语操作,并且因为资源不足而被阻塞;或调用了唤醒原语操作激活了等待资源的进程(线程);
●时间片用完;
以上都是在CPU为不可抢占方式下的引起进程调度的原因。在CPU方式是可抢占方式时,还有下面的原因:
●就绪队列中的某个进程(线程)的优先级高于当前运行进程(线程)的优先级时,引发进程(线程)调度。
所谓可抢占方式,即就绪队列中一旦有优先级高于当前运行进程(线程)优先级的进程(线程)存在时,便立即进行调度,转让CPU。而不可抢占方式,即一旦把CPU分配给一个进程(线程),它就一直占用CPU,直到该进程(线程)自己因调用原语操作或等待I/o而进入阻塞状态7或时间片用完时才让出CPU,重新执行进程(线程)调度。
3.4.2调度算法设计原则
1.进程行为
几乎所有进程的(磁盘)1/0请求或计算都是交替突发的。典型地,CPU运行一段时间后,发出一个读写文件的系统调用。在完成系统调用之后,CPU又开始计算,直到它需要读写更多的数据为止。当然,某些I/O活动可以看作是计算。例如,当CPU向视频RAM复制数据以更新屏幕时,因为使用了CPU,所以这是计算,而不是I/O。按照这种观点,当一个进程等待外部设备完成工作而被阻塞的行为属于I/O
因此,某些进程花费了绝大多数时间在计算上,而其他进程则在等待I/O上花费了绝大多数时间。前者称为计算密集型(Compute-Bound),也称为CPU密集型,后者称为I/O密集型(I/O-Bound),典型的计算密集型进程具有较长时间的CPU集中使用和较小频度的I/O等待。I/O密集型进程是i/o类的,因为这种进程在I/O请求之间较少进行计算,并不是因为它们有特别长的I/O请求。在I/O开始后无论处理数据是多还是少,它们都花费同样的时间提出硬件请求读取磁盘块。
随着CPU变得越来越快,更多的进程倾向为I/O密集型。这种现象之所以发生是因为CPU的改进比磁盘的改进快得多,其结果是,未来对I/O密集型进程的调度处理似乎更为重要。其基本思想是,如果需要运行I/o密集型进程,那么就应该让它尽快得到机会,以便发出磁盘请求并保持磁盘始终忙碌。而且,如果进程是I/O密集型的,则需要多运行一些这类进程以保持CPU的充分利用。
2.系统分类
因为不同的应用领域(以及不同的操作系统)有不同的目标,所以,不同的环境需要不同的调度算法。
通常可以分为三类环境:批处理、交互式和实时系统。
批处理系统在商业领域仍在广泛应用,用来处理薪水册、存货清单、账目收入、账目支出、利息计算(在银行)、索赔处理(在保险公司)和其他的周期性的作业。在批处理系统中,不会有用户不耐烦地在终端旁等待一个短请求的快捷响应。因此,非抢占式算法,或对每个进程都有长时间周期的抢占式算法,通常都是可接受的。这种处理方式减少了进程的切换从而改善了性能。这些批处理算法实际上相当普及,并经常可以应用在其他场合,这使得人们值得去学习它们,甚至是对于那些没有接触过大型机计算的人们。
在交互式用户环境中,为了避免一个进程霸占CPU拒绝为其他进程服务,抢占是必需的。即便没有进程想永远运行,但是,某个进程由于一个程序错误也可能无限期的排斥所有其他进程。为了避免这种现象发生,抢占也是必要的。服务器也归于此类,因为通常它们要服务多个突发的(远程)用户。
然而在有实时限制的系统中,抢占有时是不需要的,因为进程了解它们可能会长时间得不到运行,所以通常很快地完成各自的工作并阻塞。实时系统与交互式系统的差别是,实时系统只运行那些用来推进现有应用的程序,而交互式系统是通用的,它可以运行任意的非协作甚至是有恶意的程序。
3.调度算法的设计目标
设计调度算法的目标取决于环境,例如批处理、交互式或实时,但是,有一些目标是适用于所有系统的。
在所有的情形中,公平是很重要的。相似的进程应该得到相似的服务。对一个进程给予较其他等价的迸程更多的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秒之后,他就要气得要命了。之所以有这种行为,其原因是:一般用户认为拿起听筒并建立通话连接所需的时间要比挂掉电话所需的时间长。在有些情形下(如本例),调度程序对响应时间指标起不了作用;但是在另外一些情形下,调度程序还是能够做一些事的,特别是在出现差的进程顺序选择时。
实时系统有着与交互式系统不一样的特性,所以有不同的调度目标。实时系统的特点是或多或少必须满足截止时间。例如,如果计算机正在控制一个以正常速率产生数据的设备,若一个按时运行的数据收集进程出现失败,会导致数据丢失。所以,实时系统最主要的要求是满足所有的(或大多数)截止时间要求。
在多数实时系统中,特别是那些涉及多媒体的实时系统中,可预测性是很重要的。偶尔不能满足截止时间要求的问题并不严重,但是如果音频进程运行的错误太多,那么音质就会下降很快。视频品质也是一个问题,但是人的耳朵比眼睛对抖动要敏感得多。为了避免这些问题,进程调度程序必须是高度可预测的和有规律的。
3.4.3进程(线程)调度算法
进程(线程)调度算法解决以何种次序对各就绪进程(线程)进行处理机的分配以及按何种时间比例让进程(线程)占用处理机。
1.先来先服务
在所有调度算法中,最简单的是非抢占式的先来先服务(First-ComeFirst-Severed,FCFS)算法。使用该算法,进程按照它们请求CPU的顺序使用CPU。基本上,有一个就绪进程的单一队列。早上,当第一个作业从外部进入系统,就立即开始并允许运行它所期望的时间。不会中断该作业,因为它需要很长的时间运行。当其他作业进入时,它们就被安排到队列的尾部。当正在运行的进程被阻塞时,队列中的第一个进程就接着运行。在被阻塞的进程变为就绪时,就像一个新来到的作业一样,排到队列的末尾。
这个算法的主要优点是易于理解并且便于在程序中运用。就难以得到的体育或音乐会票的分配问题而言,这对那些愿意在早上两点就去排队的人们也是公平的。在这个算法中,一个单链表记录了所有就绪进程。要选取一个进程运行,只要从该队列的头部移走一个进程即可;要添加一个新的作业或阻塞一个进程,只要把该作业或进程附加在相应队列的末尾即可。还有比这更简单的理解和实现吗?
不过,先来先服务也有明显的缺点。假设有一个一次运行1秒钟的计算密集型进程和很少使用CPU但是每个都要进行1000次磁盘读操作才能完成的大量I/O密集型进程存在。计算密集进程运行1秒钟,接着读一个磁盘块。所有的I/O进程开始运行并读磁盘。当该计算密集进程获得其磁盘块时,它运行下一个1秒钟,紧跟随着的是所有I/o进程。
这样做的结果是,每个I/O进程在每秒钟内读到一个磁盘块,要花费1000秒钟才能完成操作。如果有一个调度算法每10ms抢占计算密集进程,那么I/O进程将在10秒钟内完成而不是1000秒钟,而且还不会对计算密集进程产生多少延迟。
2.最短作业优先
现在来看一种适用于运行时可以预知的另一个非抢占式的批处理调度算法。例如,一家保险公司,因为每天都做类似的工作,所以人们可以相当精确地预测处理1000个索赔的一批作业需要多少时间。当输入队列中有若干个同等重要的作业被启动时,调度程序应使用最短作业优先(Shortest Job First,SJF)算法。这里举一个例子,有4个作业A、B、C、D,运行时间分别为8、4、4,4分钟。若按图A、B、C、D的次序运行,则A的周转时间为8分钟,B为12分钟,C为16分钟,D为20分钟,平均为14分钟。
现在考虑使用最短作业优先算法运行这4个作业,运行顺序为B、C、D、A。则周转时间分别为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。
3.最短剩余时间优先
最短作业优先的抢占式版本是最短剩余时间优先(ShortestRemainingTimeNext,SRTN)算法。使用这个算法,调度程序总是选择其剩余运行时间最短的那个进程运行。再次提醒,有关的运行时间必须提前掌握。当一个新的作业到达时,其整个时间同当前进程的剩余时间做比较。如果新的进程比当前运行进程需要更少的时间,当前进程就被挂起,而运行新的进程。这种方式可以使新的短作业获得良好的服务。
4.最高响应比优先算法
在批处理系统中,最高响应比优先算法(Highest Response Rate First,HRRF)的性能是介于先
先服务和最短作业优先算法之间的折中算法。先来先服务算法在调度中最为公平,但是一旦>现计算密集型的长作业则会对其他进程造成较长时间的等待;最短作业优先算法又偏好短作!业,当短作业源源不断进入后备池时,长作业将会长时间滞留在后备池中,其运行将得不到保证,出现这种现象我们称为长作业处于“饥饿(starvation)”。
如果能为每个作业引人响应比,情况就会有所改善。响应比的计算式为:
响应比Rp=(等待时间+预计运行时间)/预计运行时间=周转时间/预计运行时间每个作业随着在后备池等待时间的增长其响应比也不断增长,而且,预计运行时间越短的作业响应比增长越快。最高响应比优先算法在每次调度时选择响应比最高的作业投入运行,这种算法较好地适应了长短作业混合的系统,使得调度的性能指标趋于合理。
最高响应比优先算法在一定程度上改善了调度的公平性和调度的效率,响应比在每次调度\前进行计算,作业运行期间不计算。计算需要消耗系统的资源,存在一定的系统开销。
5.轮转法
轮转(R0imd-R0bin,RR)算法最早来自分时系统。轮转法的基本思想是,将CPU的处理时间划分成一个个时间片,就绪队列中的诸进程轮流运行一个时间片。当时间片结束时,就强迫运行进程让出CPU,该进程进人就绪队列,等待下一次调度。同时,进程调度又去选择就绪队列中的一个进程,分配给它一个时间片,以投入运行。如此轮流调度,使得就绪队列中的所有进程在一个有限的时间内都可以依次轮流获得一个时间片的处理机时间,从而满足了系统对用户分时响应的要求。RR的调度模型如图3-8所示。
在轮转法中,时间片0长度的选取非常重要,将直接影响系统开销和响应时间。如果时间片长度很小,则调度程序剥夺处理机的次数频繁,加重系统开销;反之,如果时间片长度选择过长,比方说一个时间片就能保证就绪队列中所有进程都执行完毕,则轮转法就退化成先进先出算法。
图3-8时间片轮转调度算法
下面是影响时间片值设置的几个主要因素:
(1)系统响应时间:当进程数目一定时,时间片Q值的大小正比于系统对响应时间的要求,例如进程数目为要求的响应时间为T,则Q=T/N,Q值随r值的大或小而大或小;
(2)就绪进程的数目:当系统响应时间—定时,时间片值的大小反比于就绪进程数;
(3)计算机的处理能力:计算机的处理能力直接决定了每道程序的处理时间,显然,处理速度越高,时间片值就可以越小。
此外,从一个进程切换到另一个进程是需要一定时间进行管理事务处理的——保存和装入寄存器值及内存映像,更新各种表格和列表,清除和重新调入内存高速缓存等。假如进程切换(又称为上下文切换),需要1ms,包括切换内存映像,清除和重新调入高速缓存等。再假设时间片设为4ms0有了这些参数,则CPU在做完4ms有用的工作之后,CPU将花费1ms来进行进程切换。因此,CPU时间的20%被浪费在管理开销上。很清楚,这一管理时间太多了。
为了提高CPU的效率,我们可以将时间片设置成100ms,这样浪费的时间只有1%。但是,如果在一段非常短的时间间隔内到达50个请求,并且对CPU有不同的需求,那么,考虑一下,在一个服务器系统中会发生什么呢?50个进程会放在可运行进程的列表中。如果CPU是空闲的,第一个进程会立即开始执行,第二个直到100ms以后才会启动,以此类推。假设所有其他进程都用足了它们的时间片的话,最不幸的是最后一个进程在获得运行机会之前将不得不等待5秒钟。大部分用户会认为5秒的响应对于一个短命令来说是缓慢的。如果一些在队列后端附近的请求仅要求几毫秒的CPU时间,上面的情况会变得尤其糟糕。如果使用较短的时间片的话,它们将会获得更好的服务。
另一个因素是,如果时间片设置长于平均的CPU突发时间,那么不会经常发生抢占。相反,在时间片耗费完之前多数进程会完成一个阻塞操作,引起进程的切换。抢占的消失改善了性能,因为进程切换只会发生在确实逻辑上有需要的时候,即进程被阻塞不能够继续运行。
可以归结如下结论:时间片设得太短会导致过多的进程切换,降低了CPU效率;而设得太长又可能引起对短的交互请求的响应时间变长。将时间片设为20〜50ms通常是一个比较合理的折中。
为每个进程分配固定时间片的方法显然简单易行,微型计算机分时系统多采用之。也可采用可变时间片的方法,以进一步改善RR的调度性能。例如,根据进程的优先数分配适当的时间片,优先数较高的进程,给予较大的时间片;又如,依据在某段时间中系统中存在的就绪进程数目动态调整时间片值。
6.最高优先级算法
最高优先级(Highest Priority First,HPF)进程(线程)调度每次将处理机分配给具有最高优先级的就绪进程(线程)。进程(线程)的优先级由进程(线程)优先数决定。
进程(线程)优先数的设置可以是静态的,也可以是动态的。静态优先数是在进程(线程)创建时根据进程(线程)初始特性或用户要求而确定的,在进程(线程)运行期间不能再改变。动态优先数则是指在进程(线程)创建时先确定一个初始优先数,以后在进程(线程)运行中随着进程(线程)特性的改变(如等待时间增长),不断修改优先数。在有的系统中,优先数小的进程(线程)优先级高。
最高优先级算法还可以和不同的CPU调度方式结合起来,从而形成可抢占式最高优先级算法和不可抢占式最高优先级算法。显然,抢占式算法更好地反映了优先级的特征,可以使高优先级进程尽可能快地完成其任务的目标,从而获得了较好的服务质量。但是抢占式算法无疑也增加了系统的开销。
为达到某种目的,优先级也可以由系统动态确定。例如,有些进程为I/o密集型,其多数时间用来等待I/O结束。当这样的进程需要CPU时,应立即给它分配CPU,以便启动下一个I/O请求,这样就可以在另一个进程计算的同时执行操作。使这类1/0密集型进程长时间等待CPU只会造成它无谓地长时间占用内存。使I/O密集型进程获得较好服务的一种简单算法是,将其优先级设为1/f,f为该进程在上一时间片中所占的部分。一个在其50ms的时间片中只使用1ms的进程将获得优先级50,而在阻塞之前用掉25ms的进程将具有优先级2,而使用掉全部时间片的进程将得到优先级1。
可以很方便地将一组进程按优先级分成若干类,并且在各类之间采用优先级调度,而在各类进程的内部采用轮转调度。例如,有一个具有4类优先级的系统,其调度算法如下:只要存在优先级为第4类的可运行进程,就按照轮转法为每个进程运行一个时间片,此时不理会较低优先级的进程。若第4类进程为空,则按照轮转法运行第3类进程。若第4类和第3类均为空,则按轮转法运行第2类进程。如果不对优先级进行调整,则低优先级进程很可能会产生饥饿现象。
7.多级反馈队列算法
在实际的计算机系统中,进程(线程)的调度模式往往是几种调度算法的结合。例如,可以以最高优先级算法作为主要的调度模式,但对于具有相同优先数的进程(线程)则按先进先出调度算法处理。又如,可以将时间片轮转算法和最高优先级算法结合,对于具有相同优先数的进程(线程)按时间片轮转调度算法处理。多级队列反馈法就是综合了先进先出调度算法、时间片轮转算法和可抢占式最高优先级算法的一种进程(线程)调度算法。
多级反馈队列法的基本思想有如下几个基本要点:
(1)被调度队列的设置。系统按优先级别设置若干个就绪队列;不同优先级别的队列有不同的时间片,对级别较高的队列分配较小时间片f=l,2,…,〃),从而有S1<S2<…<SN。
(2)在同一个队列之内的调度原则。除了第〃级队列是按RR法调度之外,其他各级队列均按先进先出调度算法调度。
(3)在不同队列之间的调度原则。系统总是先调度级别较高的队列,仅当级别较高的队列为空时才去调度次一级队列中的进程(线程)。
(4)(线程)优先级的调整原则。当正在执行的进程(线程)用完其时间片之后,便被换出并进入次一级的就绪队列。当等待进程(钱程)被唤醒时,它进入与其优先级同的就绪队列;若该进程(线程)优先级高于正在执行的进程(线程),便抢占CPU。
图3-9给出了多级反馈队列法的图示。
8.最短迸程优先
对于批处理系统而言,由于最短作业优先常常伴随着最短响应时间,所以如果能够把它用于交互进程,那将是非常好的_交互进程通常遵循下列模式:等待命令V执行命令,等待命令,执行命令,如此不断反复。如果将每一条命令的执行看作i一个独立的“作^k”,则可以通过首先运行最短的作业来使响应时间最短。这里唯了的问题是如何从当前可运行进程中找出最短的那一个进程。
一种办法是根据进程过去的行为进行推测,莽执行估计运行財间最短的那一个。假设某个终端上每条命令的估计运行时间为现在假设测量到箕下一次运行时间为7]。可以用这两个值的加权和来改进估计时间,即aT0+(1-a)T1。通过选择a的值,可以决定是尽快忘掉老的运行时间,还是在一段长时间内始终记住它们。当(1=1/2时,可以得謂如下序列:
T0,T0/2+T1/2,T0/4+T1/4+T2/2,T0/8+T1/8+T2/8+T3/2
可以看到,在三轮过后,T0在新的估计值中所占的比重下降到1/8,有时把这种通过当前测量值和先前估计值进行加权平均而得到下一个估计值的技米称作老化(Aging)。它适用于许多预测值必须基于先前值的情况。老化算法在a=1/2时特别容易实现,只需将新值加到当前估计值上,然后除以2(即右移一位)。
9.实时系统中的调度算法
实时系统是一种时间起着主导作用的系统,即系统的正确性不仅取决于计算的逻辑结果,而且还依赖于产生结果的时间。典型地,外部物理设备给计算机发送了一个信号,则计算机必须在一个确定的时间范围内恰当地作出反应。例如,在视频播放器获得从驱动器来的位流后,必须在非常短的时间间隔内将位流转换为视频。如果计算时间过长,那么视频效果就会有异常。目前实时系统应用的例子包括实验控制、过程控制设备、机器人、空中交通管制、电信、军事指挥与控制系统,下一代系统将包括自动驾驶汽车、具有弹性关节的机器人控制器、智能化生产中的系统查找、空间站和海底勘探等。
在实时系统中,某些任务是实时任务,它们具有一定的紧急程度。通常给该任务规定一个最后期限,最后期限指定了开始时间或结束时间。这类任务可以分成硬实时任务或软实时任务两类。硬实时任务指必须满足最后期限的限制,否则会给系统带来不可接受的破坏或者致命的错误。软实时任务也有一个与之关联的最后期限,并希望能满足这个期限的要求,但这并不是强制的,即使超过了最后期限,调度和完成这个任务仍然是有意义的。
在这两种情形中,实时性能都是通过把程序划分为一组进程而实现的,其中每个进程的行为是可预测和提前掌握的。这些进程一般寿命较短,并且极快地运行完成。在检测到一个外部信号时,调度程序的任务就是按照满足所有最后期限的要求来调度进程。
实时任务可以按照响应方式进一步分类为周期性(以规则的时间间隔发生)事件或非周期性(发生时间不可预知)事件。一个系统可能要响应多个周期性事件流。根据每个事件需要处理时间的长短,系统甚至有可能无法处理完所有的事件。例如,如果有m个周期事件,事件i以周期发生,并需要秒CPU时间处理一个事件,那么可以处理负载的条件。
满足这个条件的实时系统称为是可调度的。
作为一个例子,考虑一个有三个周期性事件的软实时系统,其周期分别是100ms、200ms和300ms。如果这些事件分别需要50ms,30ms和100ms的CPU时间,那么该系统是可调度的,因为0.5+0.15+0.2<1。如果有第4个事件加入,其周期为1秒,那么只要这个事件的处理不超过150ms的CPU时间,那么该系统就仍然是可调度的。当然这里隐含了一个假设,即上下文切换的开销很小,可以忽略不计。
实时系统的调度算法可以是静态或动态的。前者在系统开始运行之前作出调度决策;后者在运行过程中进行调度决策。只有在可以提前掌握所完成的工作以及必须满足的截止时间等全部信息时,静态调度才能工作。而动态调度算法不需要这些限制。
1)速率单调调度算法
适用于可抢先的周期性进程的经典静态实时调度算法是速率单调调度(Rate Monotonic Scheduling,RMS),它可以用于满足下列条件的进程:
①每个周期性进程必须在其周期内完成。
②没有进程依赖于任何其他进程。
③每一进程在一次突发中需要相同的CPU时间量。
④任何非周期性进程都没有最终时限。
⑤进程抢先即刻发生而没有系统开销。
RMS分配给每个进程一个固定的优先级,优先级等于进程触发事件发生的频率。例如,必须每30ms运行一次(每秒33次)的进程获得的优先级为33,必须每40ms运行一次(每秒25次)的进程获得的优先级为25,必须每50ms运行一次(每秒20次)的进程获得的优先级为20。所以,优先级与进程的速率(每秒运行进程的次数)呈线性关系,这正是为什么将其称为速率单调的原因。在运行时,调度程序总是运行优先级最高的就绪进程,如果需要则抢先正在运行的进程。可以证明,在静态调度算法种类中RMS是最优的。
2)最早最终时限优先调度
另一个流行的实时调度算法是最早最终时限优先(Earliest Dead lineFirst,EDF)算法。EDF是一个动态算法,它不像速率单调算法那样要求进程是周期性的,它也不像RMS那样要求每个CPU突发有相同的运行时间。只要一个进程需要CPU时间,它就宣布它的到来和最终时限。调度程序维持一个可运行进程的列表,该列表按最终时限排序。EDF算法运行列表中的第一个进程,也就是具有最近最终时限的进程。当一个新的进程就绪时,系统进行检查以了解其最终时限是否发生在当前运行的进程结束之前。如果是这样,新的进程就抢先当前正在运行的进程。
来源:https://www.cnblogs.com/jtd666/p/12505420.html