在传统的操作系统中,程序并不能独立运行,作为资源分配和独立运行的基本单位的是进程。操作系统所具有的四大特征也都是基于进程而形成的,并可以从进程的观点来研究操作系统。显然,在操作系统中,进程是一个极其重要的概念。
在未配置OS的系统中,程序的执行方式是顺序执行,也就是说必须在一个程序执行完后,才允许另一个程序执行;在多道程序环境下,则允许多个程序并发执行。程序的这两种执行方式间有着显著的不同。也正是程序并发执行的这种特征,才导致了在操作系统中引入了进程的概念。
前驱图是一个有向无循环图,记为DAG,用于描述进程之间执行的前后关系。图中的每个节点可用于描述一个程序段或进程,乃至一条语句;结点间的有向边则用于表示连个结点之间存在的偏序或前驱关系->。
→={(Pi,Pj) | Pi must complete before Pj may start},如果(Pi,Pj)∈→,可写成 Pi→Pj,称 Pi 是 Pj 的直接前趋,而称 Pj 是 Pi 的直接后继。在前趋图中,把没有前趋的结点称为初始结点(Initial Node),把没有后继的结点称为终止结点(Final Node)。此外,每个结点还具有一个重量(Weight),用于表示该结点所含有的程序量或结点的执行时间。在图 2-1(a)和 2-1(b)中 分别存在着这样的前趋关系:
Ii→Ci→Pi
和
S1→S2→S3
对于图 2-2(a)所示的前趋图,存在下述前趋关系:
P1→P2,P1→P3,P1→P4,P2→P5,P3→P5,P4→P6,P4→P7,P5→P8,P6→P8,P7→P9,P8→P9
或表示为:
P={P1,P2,P3,P4,P5,P6,P7,P8,P9}
→={(P1,P2),(P1,P3),(P1,P4),(P2,P5),(P3,P5),(P4,P6),(P4,P7),(P5,P8),(P6,P8),
(P7,P9),(P8,P9)}
应当注意,前趋图中必须不存在循环,但在图 2-2(b)中却有着下述的前趋关系: S2→S3,S3→S2 显然,这种前趋关系是不可能满足的。
(1)间断性:程序在并发执行时,由于他们共享系统资源,以及为完成同一项任务而相互合作,致使这些并发执行的程序之间,形成了相互制约的关系。相互制约将导致并发程序具有“执行——暂停——执行”这种间断性的规律。
(2)失去封闭性:程序在并发执行时,是多个程序共享系统中的各种资源,因而这些资源的状态将由多个程序来改变,致使程序的运行失去了封闭性。这样,某程序在执行时,必然会受到其它程序的影响。
(3)不可再现性:程序在并发执行时,由于失去了封闭性,也将导致其再失去可再现性。
(1)结构特征
进程印象:
PCB:通常的程序是不能并发执行的。为使程序(含数据)能独立运行,应为之配置一进程控制块,这个进程控制块叫做PCB。
程序段
数据段
在许多情况下所说的进程,实际上是指进城实体。例如,所谓创建进程,实质上是创建进程实体中的 PCB;而撤消进程,实质上是撤消进程的 PCB。
(2)动态性
进程的实质是进程实体的一次执行过程,因此,动态性是进程的最基本的特征。动态性还表现在:“它由创建而产生,由调度而执行,由撤消而消亡”。可见,进程实体有一定的生命期,而程序则只是一组有序指令的集合,并存放于某种介质上,其本身并不具有运 动的含义,因而是静态的。
(3)并发性
指多个进程实体同存于内存中,且能在一段时间内同时运行。并发性是进程的重要特征,同时也成为 OS 的重要特征。引入进程的目的也正是为了使其进程实体能和其它进程实体并发执行;而程序(没有建立 PCB)是不能并发执行的。
(4)独立性
在传统的 OS 中,独立性是指进程实体是一个能独立运行、独立分配资源和独立接受调 度的基本单位。凡未建立 PCB 的程序都不能作为一个独立的单位参与运行。
(5)异步性
指进程按各自独立的、 不可预知的速度向前推进,或说进程实体按异步方式运行。进程的定义:
(1)进程是程序的一次执行。
(2) 进程是一个程序及其数据在处理机上顺序执行时所发生的活动。
(3) 进程是程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。
在引入了进程实体的概念后,我们可以把传统 OS 中的进程定义为:“进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位”。
致使进程阻塞的典型事件有:
请求 I/O,申请缓冲空间等。
通常将这种处于阻塞状态的进程也排成一个队列。有的系统则根据阻塞原因的不同而把处于阻塞状态的进程排成多个队列。
处于就绪状态的进程,在调度程序为之分配了处理机之后,该进程便可执行,相应地,它就由就绪状态转变为执行状态。正在执行的进程也称为当前进程,如果因分配给它的时间片已完而被暂停执行时,该进程便由执行状态又回复到就绪状态;
如果因发生某事件而使进程的执行受阻(例如,进程请求访问某临界资源,而该
资源正被其它进程访问时),使之无法继续执行,该进 程将由执行状态转变为阻塞状态。图 2-5 示出了进程的三种基本状态以及各状态之间的转换关系。
3. 挂起状态
1) 引入挂起状态的原因
在不少系统中进程只有上述三种状态,但在另一些系统中,又增加了一些新状态,最 重要的是挂起状态。引入挂起状态的原因有:
(1) 终端用户的请求。当终端用户在自己的程序运行期间发现有可疑问题时,希望暂时使自己的程序静止下来。亦即,使正在执行的进程暂停执行;若此时用户进程正处于就绪状态而未执行,则该进程暂不接受调度,以便用户研究其执行情况或对程序进行修改。我 们把这种静止状态称为挂起状态。
(2) 父进程请求。有时父进程希望挂起自己的某个子进程,以便考查和修改该子进程,或者协调各子进程间的活动。
(3) 负荷调节的需要。当实时系统中的工作负荷较重,已可能影响到对实时任务的控制时,可由系统把一些不重要的进程挂起,以保证系统能正常运行。
(4) 操作系统的需要。操作系统有时希望挂起某些进程,以便检查运行中的资源使用情况或进行记账。
2)进程状态的转换
在引入挂起状态后,又将增加从挂起状态到非挂起状态的转换;或者相反。可有以下几种情况:
(1) 活动就绪→静止就绪。当进程处于未被挂起的就绪状态时,称此为活动就绪状态,表示为 Readya。当用挂起原语 Suspend 将该进程挂起后,该进程便转变为静止就绪状态,表示为 Readys,处于 Readys 状态的进程不再被调度执行。
(2) 活动阻塞→静止阻塞。当进程处于未被挂起的阻塞状态时,称它是处于活动阻塞状 态,表示为 Blockeda。当用 Suspend 原语将它挂 起后,进程便转变为静止阻塞状态,表示为Blockeds。处于该状态的进程在其所期待的事件 出现后,将从静止阻塞变为静止就绪。
(3) 静止就绪→活动就绪。处于 Readys 状 态的进程,若用激活原语 Active 激活后,该进 程将转变为 Readya 状态。
(4) 静止阻塞→活动阻塞。处于 Blockeds 状 态的进程,若用激活原语 Active 激活后,该进 程将转变为 Blockeda 状态。图 2-6 示出了具有挂起状态的进程状态图。
1)创建状态
先为一个新进程创建PCB,并填写必要的管理信息;
其次,把该进程转入就绪状态并插入就绪队列之中;
当一个新进程被创建时,系统已为其分配了 PCB,填写了进程标识等信息,但由于该进程所必需的资源或其它信息, 如主存资源尚未分配等,一般而言,此时的进程已拥有了自己的 PCB,但进程自身还未进入主存,即创建工作尚未完成,进程还不能被调度运行,其所处的状态就是创建状态。
引入创建状态,是为了保证进程的调度必须在创建工作完成后进行,以确保对进程控 制块操作的完整性。同时,创建状态的引入,也增加了管理的灵活性,操作系统可以根据系统性能或主存容量的限制,推迟创建状态进程的提交。对于处于创建状态的进程,获得了其所必需的资源,以及对其PCB初始化工作完成后,进程状态便可由创建状态转入就绪状态。
2)终止状态
进程的终止也要通过两个步骤:首先等待操作系统进行善后处理,然后将其 PCB 清零, 并将 PCB 空间返还系统。当一个进程到达了自然结束点,或是出现了无法克服的错误,或 是被操作系统所终结,或是被其他有终止权的进程所终结,它将进入终止状态。进入终止态的进程以后不能再执行,但在操作系统中依然保留一个记录,其中保存状态码和一些计时统计数据,供其它进程收集。一旦其它进程完成了对终止状态进程的信息提取之后,操作系统将删除该进程。
图 2-7 示出了增加了创建状态和终止状态后,进程的三种基本状态及转换图衍变为五种状态及转换关系图。
图 2-8 示出了增加了创建状态和终止状态后,具有挂起状态的进程状态及转换图。
如图 2-8 所示,引进创建和终止状态后,在进程状态转换时,相比较图 2-7 所示的进程五状态转换而言,需要增加考虑下面的几种情况。
(1) NULL→创建:一个新进程产生时,该进程处于创建状态。
(2) 创建→活动就绪:在当前系统的性能和内存的容量均允许的情况下,完成对进程创 建的必要操作后,相应的系统进程将进程的状态转换为活动就绪状态。
(3) 创建→静止就绪:考虑到系统当前资源状况和性能要求,并不分配给新建进程所需资源,主要是主存资源,相应的系统进程将进程状态转为静止就绪状态,对换到外存,不再参与调度,此时进程创建工作尚未完成。
(4) 执行→终止:当一个进程到达了自然结束点,或是出现了无法克服的错误,或是被操作系统所终结,或是被其他有终止权的进程所终结,进程即进终止状态。
1)进程标识符:进程标识符用于唯一地标识一个进程。一个进程通常有两种标识符。
(1)内部标识符:在所有的操作系统中,都为每一个进程赋予了一个唯一的数字标识符,它通常是一个进程的序号。设置内部标识符的主要作用是为了方便系统使用。
(2)外部标识符:由创建者提供,通常是由字母数字组成,往往是由用户在访问该进程时使用。为了描述进程的家族关系,应设置 父进程标识以及 子进程标识。此外还可以设置用户标识,已指示拥有该进程的用户。
2)处理机状态
处理机状态信息主要是由处理机的各种寄存器中的内容组成的。处理机在运行时,许多信息都放在寄存器中。当处理机被中断时,所有这些信息都必须保存在 PCB 中,以便在 该进程重新执行时,能从断点继续执行。
这些寄存器包括:
① 通用寄存器,又称为用户可视寄存器,它们是用户程序可以访问的,用于暂存信息。
② 指令计数器,其中存放了要访问的下一条指令的地址;
③ 程序状态字 PSW,其中含有状态信息,如条件码、执行方式、中 断屏蔽标志等;④ 用户栈指针,每个用户进程都有一个或若干个与之相关的系统栈,用于存放过程和系统调用参数及调用地址,栈指针指向该栈的栈顶。
3)进程调度信息
在 PCB 中还存放一些与进程调度和进程对换有关的信息,包括:
① 进程状态,指明进程的当前状态,作为进程调度和对换时的依据;
② 进程优先级,用于描述进程使用处理机的优先级别的一个整数,优先级高的进程应优先获得处理机;
③ 进程调度所需的其它信息,它们与所采用的进程调度算法有关,比如,进程已等待 CPU 的时间总和、进程已执行的时
间总和等;
④ 事件,指进程由执行状态转变为阻塞状态所等待发生的事件,即阻塞原因。
4)进程控制信息
① 程序和数据的地址,指进程的程序和数据所在的内存或外存地(首)址,以便再调度到该进程执行时,能从 PCB 中找到其程序和数据;
② 进程同步和通信 机制,指实现进程同步和进程通信时必需的机制,如消息队列指针、信号量等,它们可能 全部或部分地放在 PCB 中;
③ 资源清单,即一张列出了除 CPU 以外的、进程所需的全部资源及已经分配到该进程的资源的清单;
④ 链接指针,它给出了本进程(PCB)所在队列中的下一个进程的 PCB 的首地址。
进程控制是进程管理中最基本的功能。它用于创建一个新进程,终止一个已完成的进程,或终止一个因出现某事件而使其无法运行下去的进程,还可负责进程运行中的状态转换。进程控制一般是由 OS的内核中的原语来实现的。
原语是由若干条指令组成的,用于完成一定功能的一个过程。它与一般过程的区别在于:它们是“原子操作”。所谓原子操作,是指一个操作中的所有动作要么全做,要么全不做。换言之,它是一个不可分割的基本单位,因此,在执行过程中不允许被中断。原子操作在管态下执行,常驻内存。 原语的作用是为了实现进程的通信和控制,系统对进程的控制如不使用原语,就会造成其状态的不确定性,从而达不到进程控制的目的。
进程图
进程图是用于描述一个进程的家族关系的有向树,如图 2-11 所示。图中的结点(圆圈)代表进程。在进程 D 创建了进程 I 之后,称 D 是 I 的父进程,I 是 D 的子进程。 这里可用一条由父进程指向子进程的有向边来描述它们之间的父子关系。创建父进程的进程称为祖先进程,这样便形成了一棵进程树,把树的根结 点作为进程家族的祖先。
了解进程间的这种关系是十分重要的。因为子进程可以继承父进程所拥有的资源,例如,继承父进程打开的文件,继承父进程所分配到的缓冲区等。当子进程被撤消时,应将其从父进程那里获得的资源归还给父进程。此外,在撤消父进程时,也必须同时撤消其所有的子进程。为了标识进程之间的家族关系,在 PCB 中都设置了家族关系表项,以标明自己的父进程及所有的子进程。
引起创建进程的事件
(1)用户登录
(2)作业调度。在批处理系统中,当作业调度程序按一定的算法调度到某作业时,便将该作业装入内存,为它分配必要的资源,并立即为它创建进程,再插入就绪队列中。
(3)提供服务。当运行中的用户程序提出某种请求后,系统将专门创建一个进程来提供 用户所需要的服务
(4)应用请求。在上述三种情况下,都是由系统内核为它创建一个新进程;而第 4 类事 件则是基于应用进程的需求,由它自己创建一个新进程,以便使新进程以并发运行方式完成特定任务。
进程的创建
一旦操作系统发现了要求创建新进程的事件后,便调用进程创建原语Creat( )按下述步 骤创建一个新进程。
(1) 申请空白 PCB。
(2) 为新进程分配资源。
(3) 初始化进程控制块。PCB 的初始化包括:
① 初始化标识信息,将系统分配的标识符和父进程标识符填入新 PCB 中;
② 初始化处理机状态信息,使程序计数器指向程序的入口地址,使栈指针指向栈顶;
③ 初始化处理机控制信息,将进程的状态设置为就绪状态或 静止就绪状态,对于优先级,通常是将它设置为最低优先级,除非用户以显式方式提出高优先级要求。
(4) 将新进程插入就绪队列,如果进程就绪队列能够接纳新进程,便将新进程插入就绪队列。
(1)正常结束
(2)异常结束
(1) 越界错误。这是指程序所访问的存储区已越出该进程的区域。
(2) 保护错。这是指进程试图去访问一个不允许访问的资源或文件,或者以不适当的方式进行访问,例如,进程试图去写一个只读文件。
(3) 非法指令。这是指程序试图去执行一条不存在的指令。出现该错误的原因,可能是程序错误地转移到数据区,把数据当成了指令。
(4) 特权指令错。这是指用户进程试图去执行一条只允许 OS 执行的指令。
(5) 运行超时。这是指进程的执行时间超过了指定的最大值。
(6) 等待超时。这是指进程等待某事件的时间超过了规定的最大值。
(7) 算术运算错。这是指进程试图去执行一个被禁止的运算,例如被 0 除。
(8) I/O 故障。这是指在 I/O 过程中发生了错误等。
(1) 根据被终止进程的标识符,从 PCB 集合中检索出该进程的 PCB,从中读出该进程的状态。
(2) 若被终止进程正处于执行状态,应立即终止该进程的执行,并置调度标志为真,用于指示该进程被终止后应重新进行调度。
(3) 若该进程还有子孙进程,还应将其所有子孙进程予以终止,以防它们成为不可控的进程。
(4) 将被终止进程所拥有的全部资源,或者归还给其父进程,或者归还给系统。
(5) 将被终止进程(PCB)从所在队列(或链表)中移出,等待其他程序来搜集信息。
(1)请求系统服务。当正在执行的进程请求操作系统提供服务时,由于某种原因,操作系统并不立即满足该进程的要求时,该进程只能转变为阻塞状态来等待。
(2)请求某种操作。当进程启动某种操作后,如果该进程必须在该操作完成之后才能继续执行,则必须先使该进程阻塞,以等待该操作完成。
(3)新数据尚未到达。对于相互合作的进程,如果其中一个进程需要先获得另一(合作)进程提供的数据后才能 对数据进行处理,则只要其所需数据尚未到达,该进程只有(等待)阻塞。
(4)无新工作可做。系统往往设置一些具有某特定功能的系统进程,每当这种进程完成任务后,便把自己阻塞起来以等待新任务到来。
正在执行的进程,当发现上述某事件时,由于无法继续执行,于是进程便通过调用阻 塞原语 block 把自己阻塞。可见,进程的阻塞是进程自身的一种主动行为。
进入 block 过程后,由于此时该进程还处于执行状态,所以应先立即停止执行,把进程控制块中的现行状 态由“执行”改为“阻塞”,并将 PCB 插入阻塞队列。如果系统中设置了因不同事件而阻塞的多个阻塞队列,则应将本进程插入到具有相同事件的阻塞(等待)队列。
最后,转调度程序进行重新调度,将处理机分配给另一就绪进程并进行切换,保留被阻塞进程的处理机状态(在 PCB 中),再按新进程的 PCB 中的处理机状态设置 CPU 的环境。
当被阻塞进程所期待的事件出现时,如 I/O 完成或其所期待的数据已经到达,则由关进程调用唤醒原语 wakeup( ),将等待该事件的进程唤醒。
唤醒原语执行的过程是:首先把被阻塞的进程从等待该事件的阻塞队列中移出,将其 PCB 中的现行状态由阻塞改为就绪,然后再将该 PCB 插入到就绪队列中。
应当指出,block 原语和 wakeup 原语是一对作用刚好相反的原语。因此,如果在某进程中调用了阻塞原语,则必须在与之相合作的另一进程中或其他相关的进程中安排唤醒原语,以能唤醒阻塞进程;否则,被阻塞进程将会因不能被唤醒而长久地处于阻塞状态,从 而再无机会继续运行。
当出现了引起进程挂起的事件时,比如,用户进程请求将自己挂起,或父进程请求将自己的某个子进程挂起,系统将利用挂起原语 suspend( )将指定进程或处于阻塞状态的进程挂起。
挂起原语的执行过程是:首先检查被挂起进程的状态,若处于活动就绪状态,便将其改为静止就绪; 对于活动阻塞状态的进程,则将之改为静止阻塞。为了方便用户或父进程考查该进程的运行情况而把该进程的 PCB 复制到某指定的内存区域。最后,若被挂起的进程正在执行,则转向调度程序重新调度。
当发生激活进程的事件时,例如,父进程或用户进程请求激活指定进程,若该进程驻留在外存而内存中已有足够的空间时,则可将在外存上处于静止就绪状态的该进程换入内存。这时,系统将利用激活原语 active( )将指定进程激活。
激活原语先将进程从外存调入内存,检查该进程的现行状态,若是静止就绪,便将之改为活动就绪;若为静止阻塞,便将之改为活动阻塞。假如采用的是抢占调度策略,则每当有新进程进入就绪队列时,应检查是否要进行重新调度,即由调度程序将被激活进程与当前进程进行优先级的比较,如果被激活进程的优先级更低,就不必重新调度;否则,立即剥夺当前进程的运行,把处理机分配给刚被激活的进程。
进程同步的主要任务是对多个相关进程在执行次序上进行协调,以使并发执行的诸进程之间能有效地共享资源和相互合作,从而使程序的执行具有可再现性。
两种形式的制约
(1) 间接相互制约关系。同处于一个系统中的进程,通常都共享着某种系统资源,如共 享 CPU、共享 I/O 设备等。所谓间接相互制约即源于这种资源共享,例如,有两个进程 A 和 B,如果在 A 进程提出打印请求时,系统已将惟一的一台打印机分配给了进程 B,则此时进程 A 只能阻塞;一旦进程 B 将打印机释放,则 A 进程才能由阻塞改为就绪状态。
(2) 直接相互制约关系。这种制约主要源于进程间的合作。例如,有一输入进程 A 通过 单缓冲向进程 B 提供数据。当该缓冲空时,计算进程因不能获得所需数据而阻塞,而当进程 A 把数据输入缓冲区后,便将进程 B 唤醒;反之,当缓冲区已满时,进程 A 因不能再向缓冲区投放数据而阻塞,当进程 B 将缓冲区数据取走后便可唤醒 A。
临界资源
生产者-消费者问题是一个著名的进程同步问题。它描述的是:有一群生产者进程在生产产品,并将这些产品提供给消费者进程去消费。为使生产者进程与消费者进程能并发执行,在两者之间设置了一个具有 n 个缓冲区的缓冲池,生产者进程将它所生产的产品放入一个缓冲区中;消费者进程可从一个缓冲区中取走产品去消费。
尽管所有的生产者进程和消费者进程都是以异步方式运行的,但它们之间必须保持同步,即不允许消费者进程到一个空缓冲区去取产品,也不允许生产者进程向一个已装满产品且尚未被取走的缓冲区中投放产品。
我们可利用一个数组来表示上述的具有 n 个(0,1,…,n-1)缓冲区的缓冲池。
用输入指针 in 来指示下一个可投放产品的缓冲区,每当生产者进程生产并投放一个产品后,输入 指针加 1;
用一个输出指针 out 来指示下一个可从中获取产品的缓冲区,每当消费者进程取 走一个产品后,输出指针加 1。
由于这里的缓冲池是组织成循环缓冲的,故应把输入指针加 1 表示成 in:= (in+1)mod n; 输出指针加 1 表示成 out:= (out+1) mod n。当 (in+1) mod n=out时表示缓冲池满;而 in=out 则表示缓冲池空。此外,还引入了一个整型变量 counter,其初始值为 0。每当生产者进程向缓冲池中投放一个产品后,counter 加 1;反之,每当消费 者进程从中取走一个产品时,使 counter 减 1。生产者和消费者两进程共享下面的变量:
Var n,integer; type item=…; var buffer: array[0,1,…,n-1] of item; in,out: 0,1,…,n-1; counter: 0,1,…,n;
指针 in 和 out 初始化为 1。在生产者和消费者进程的描述中,noop 是一条空操作指令, while condition do no-op 语句表示重复的测试条件(condication),重复测试应进行到该条件变为 false(假),即到该条件不成立时为止。在生产者进程中使用一局部变量 nextp,用于暂时存放每次刚生产出来的产品;而在消费者进程中,则使用一个局部变量 nextc,用于存放每次要消费的产品。
producer: repeat ... produce an item in nextp; ... while counter=n do no-op; buffer[in]:=nextp; in:=in+1 mod n; counter:=counter+1; until false; consumer: repeat while counter=0 do no-op; nextc:=buffer[out]; out:=(out+1) mod n; counter:=counter-1; consumer the item in nextc; until false;
虽然上面的生产者程序和消费者程序在分别看时都是正确的,而且两者在顺序执行时其结果也会是正确的,但若并发执行时就会出现差错,问题就在于这两个进程共享变量 counter。生产者对它做加 1 操作,消费者对它做减 1 操作,这两个操作在用机器语言实现时, 常可用下面的形式描述:
register1:=counter; register2:=counter; register1:=register1+1; register2:=register2-1; counter:=register1; counter:=register2;
假设 counter 的当前值是 5。如果生产者进程先执行左列的三条机器语言语句,然后消 费者进程再执行右列的三条语句,则最后共享变量 counter 的值仍为 5; 反之,如果让消费 者进程先执行右列的三条语句,然后再让生产者进程执行左列的三条语句,则 counter 值也 还是 5,但是,如果按下述顺序执行:
register1:=counter; (register1=5) register1:=register1+1; (register1=6) register2:=counter; (register2=5) register2:=register2-1; (register2=4) counter:=register1; (counter=6) counter:=register2; (counter=4)
正确的 counter 值应当是 5,但现在是 4。倘若再将两段程序中各语句
交叉执行的顺序改变,将可看到又可能得到 counter=6 的答案,这表明程序的执行已经失去了再现性。为了预防产生这种错误,解决此问题的关键是应把变量 counter 作为临界资源处理,令生产者进程和消费者进程互斥地访问变量 counter。
4. 同步机制应遵循的规则
为实现进程互斥地进入自已的临界区,可用软件方法,更多的是在系统中设置专门的 同步机构来协调各进程间的运行。所有同步机制都应遵循下述四条准则:
(1)== 空闲让进==。当无进程处于临界区时,表明临界资源处于空闲状态,应允许一个请求进入临界区的进程立即进入自己的临界区,以有效地利用临界资源。
(2) 忙则等待。当已有进程进入临界区时,表明临界资源正在被访问,因而其它试图进 入临界区的进程必须等待,以保证对临界资源的互斥访问。
(3) 有限等待。对要求访问临界资源的进程,应保证在有限时间内能进入自己的临界区, 以免陷入“死等”状态。
(4) 让权等待。当进程不能进入自己的临界区时,应立即释放处理机,以免进程陷入“忙 等”状态。
整型信号量
最初由 Dijkstra 把整型信号量定义为一个用于表示资源数目的整型量 S它与一般整型 量不同,除初始化外,仅能通过两个标准的原子操作(Atomic Operation) wait(S)和 signal(S)来访问。很长时间以来,这两个操作一直被分别称为 P、V 操作。Wait(S)和 signal(S)操作可描述为:
wait(S): while S<=0 do no-op; S:=S-1; signal(S): S:=S+1
wait(S)和 signal(S)是两个原子操作,因此,它们在执行时是不可中断的。当一个进程在修改某信号量时,没有其他进程可同时对该信号量进行修改。此外,在 wait 操作中,对 S 值的测试和做 S:=S-1 操作时都不可中断。
在记录型信号量机制中,S.value 的初值表示系统中某类资源的数目,因而又称为资源信号量。对它的每次 wait 操作,意味着进程请求一个单位的该类资源,使系统中可供分配的该类资源数减少一个,因此描述为 S.value:=S.value-1;当 S.value<0 时,表示该类资源已分配完毕,因此进程应调用 block 原语,进行自我阻塞,放弃处理机,并插入到信号量链表 S.L 中。
可见,该机制遵循了“让权等待”准则。此时 S.value 的绝对值表示在该信号量链 表中已阻塞进程的数目。对信号量的每次 signal 操作,表示执行进程释放一个单位资源,使系统中可供分配的该类资源数增加一个,故 S.value:=S.value+1 操作表示资源数目加 1。若加 1 后仍是 S.value≤0,则表示在该信号量链表中,仍有等待该资源的进程被阻塞,故还应 调用 wakeup 原语,将 S.L 链表中的第一个等待进程唤醒。如果 S.value 的初值为 1,表示只允许一个进程访问临界资源,此时的信号量转化为互斥信号量,用于进程互斥。
最后,进程 A 和 B 处于僵持状态。在无外力作用下,两者都将无法从僵持状态中解脱 出来。我们称此时的进程 A 和 B 已进入死锁状态。显然,当进程同时要求的共享资源愈多时,发生进程死锁的可能性也就愈大。
AND 同步机制的基本思想是:将进程在整个运行过程中需要的所有资源,一次性全部 地分配给进程,待进程使用完后再一起释放。只要尚有一个资源未能分配给进程,其它所有可能为之分配的资源也不分配给它。
即对若干个临界资源的分配,采取原子操作方式:要么把它所请求的资源全部分配到进程,要么一个也不分配。由死锁理论可知,这样就可避免上述死锁情况的发生。为此,在 wait 操作中,增加了一个“AND”条件,故称为 AND 同步,或称为同时 wait 操作,即 Swait(Simultaneous wait)定义如下:
利用信号量实现进程互斥
为使多个进程能互斥地访问某临界资源,只须为该资源设置一互斥信号量 mutex,并设其初始值为 1,然后将各进程访问该资源的临界区 CS 置于wait(mutex)和 signal(mutex)操作之间即可。这样,每个欲访问该临界资源的进程在进入临界区之前,都要先对 mutex 执行wait 操作,若该资源此刻未被访问,本次 wait 操作必然成功,进程便可进入自己的临界区,这时若再有其他进程也欲进入自己的临界区,此时由于对 mutex 执行 wait 操作定会失败,因而该进程阻塞,从而保证了该临界资源能被互斥地访问。当访问临界资源的进程退出临 界区后,又应对 mutex 执行 signal 操作,以便释放该临界资源。利用信号量实现进程互斥的进程可描述如下:
在利用信号量机制实现进程互斥时应注意,==wait(mutex)和 signal(mutex)==必须成对地出现。 缺少 wait(mutex)将会导致系统混乱,不能保证对临界资源的互斥访问;而缺少 signal(mutex)将会使临界资源永远不被释放,从而使因等待该资源而阻塞的进程不能被唤醒。
利用信号量实现前驱关系
还可利用信号量来描述程序或语句之间的前趋关系。设有两个并发执行的进程 P1 和 P2。 P1 中有语句 S1;P2 中有语句 S2。我们希望在 S1 执行后再执行 S2。为实现这种前趋关系,我们只须使进程 P1 和 P2 共享一个公用信号量 S,并赋予其初值为 0,将 signal(S)操作放在语句 S1 后面;而在 S2 语句前面插入 wait(S)操作,即
在进程 P1 中,用 S1;signal(S);
在进程 P2 中,用 wait(S);S2;
由于 S 被初始化为 0,这样,若 P2 先执行必定阻塞, 只有在进程 P1 执行完 S1;signal(S);操作后使 S 增为 1 时,P2 进程方能执行语句 S2 成功。同样,我们可以利用信号 量,按照语句间的前趋关系(见图 2-12),写出一个更为复
杂的可并发执行的程序。
图 2-12 示出了一个前趋图,其中 S1,S2,S3,…,S6 是最简单的程序段(只有一条语句)。 为使各程序段能正确执行,应设置若干个初始值为“0”的信号量。如为保证 S1→S2,S1→S3 的前趋关系,应分别设置信号量 a 和 b,同样,为了保证 S2→S4,S2→S5,S3→S6,S4→S6 和 S5→S6,应设置信号量 c,d,e,f,g。
Var a,b,c,d,e,f,g:semaphore: =0,0,0,0,0,0,0; begin parbegin begin S1; signal(a); signal(b); end; begin wait(a); S2; signal(c); signal(d); end; begin wait(b); S3; signal(e); end; begin wait(c); S4; signal(f); end; begin wait(d); S5; signal(g); end; begin wait(e); wait(f); wait(g); S6; end; parend end
虽然信号量机制是一种既方便、又有效的进程同步机制,但每个要访问临界资源的进程都必须自备同步操作 wait(S)和 signal(S)。这就使大量的同步操作分散在各个进程中。这不仅给系统的管理带来了麻烦,而且还会因同步操作的使用不当而导致系统死锁。这样, 在解决上述问题的过程中,便产生了一种新的进程同步工具——管程(Monitors)。
(1) 模块化。管程是一个基本程序单位,可以单独编译。
(2) 抽象数据类型。管程中不仅有数据,而且有对数据的操作。
(3) 信息掩蔽。管程中的数据结构只能被管程中的过程访问,这些过程也是在管程内部
定义的,供管程外的进程调用,而管程中的数据结构以及过程(函数)的具体实现外部不可见。
管程和进程不同,主要体现在以下几个方面:
(1) 虽然二者都定义了数据结构,但进程定义的是私有数据结构 PCB,管程定义的是公共数据结构,如消息队列等;
(2) 二者都存在对各自数据结构上的操作,但进程是由顺序程序执行有关的操作,而管程主要是进行同步操作和初始化操作;
(3) 设置进程的目的在于实现系统的并发性,而管程的设置则是解决共享资源的互斥使 用问题;
(4) 进程通过调用管程中的过程对共享数据结构实行操作,该过程就如通常的子程序一样被调用,因而管程为被动工作方式,进程则为主动工作方式;
(5) 进程之间能并发执行,而管程则不能与其调用者并发;
(6) 进程具有动态性,由“创建”而诞生,由“撤销”而消亡,而管程则是操作系统中的一个资源管理模块,供进程调用。
前面我们已经对生产者—消费者问题做了一些描述,但未考虑进程的互斥与同步问题,因而造成了数据(Counter)的不定性。由于生产者—消费者问题是相互合作的进程关系的一种抽象,例如,在输入时,输入进程是生产者,计算进程是消费者;而在输出时,计算进程是生产者,而打印进程是消费者。因此,该问题有很大的代表性及实用价值。本小节将利用信号量机制来解决生产者—消费者问题。
利用记录型信号量解决 生产者——消费者问题
假定在生产者和消费者之间的公用缓冲池中,具有 n 个缓冲区,这时可利用互斥信号 量 mutex 实现诸进程对缓冲池的互斥使用。利用信号量 empty 和 full 分别表示缓冲池中空缓冲区和满缓冲区的数量。又假定这些生产者和消费者相互等效,只要缓冲池未满,生产者便可将消息送入缓冲池;只要缓冲池未空,消费者便可从缓冲池中取走一个消息。对生产者—消费者问题可描述如下:
在生产者—消费者问题中应注意:首先,在每个程序中用于实现互斥的 wait(mutex)和 signal(mutex)必须成对地出现;其次,对资源信号量 empty 和 full 的 wait 和 signal 操作,同样需要成对地出现,但它们分别处于不同的程序中。例如,wait(empty)在计算进程中,而 signal(empty)则在打印进程中,计算进程若因执行 wait(empty)而阻塞,则以后将由打印进程将它唤醒;最后,在每个程序中的多个 wait 操作顺序不能颠倒,应先执行对资源信号量的 wait 操作,然后再执行对互斥信号量的 wait 操作,否则可能引起进程死锁。
利用AND信号量解决生产者——消费者问题
对于生产者—消费者问题,也可利用 AND 信号量来解决,即用 Swait(empty,mutex)
来代替 wait(empty)和 wait(mutex);用 Ssignal(mutex,full)来代替 signal(mutex)和 signal(full); 用 Swait(full,mutex)来代替 wait(full)和 wait(mutex),以及用 Ssignal(mutex,empty)代替 Signal(mutex)和 Signal(empty)。利用 AND 信号量来解决生产者—消费者问题的算法描述
如下:
由 Dijkstra 提出并解决的哲学家进餐问题是典型的同步问题。该问题是描述有五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌 上有五个碗和五只筷子,他们的生活方式是交替地进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。 进餐完毕,放下筷子继续思考。
利用记录型信号量解决哲学家进餐问题
经分析可知,放在桌子上的筷子是临界资源,在一段时间内只允许一位哲学家使用。为了实现对筷子的互斥使用,可以用一个信号量表示一只筷子,由这五个信号量构成信号 量数组。其描述如下:Var chopstick: array[0,…,4] of semaphore;
所有信号量均被初始化为1,第 i 位哲学家的活动可描述为:
在以上描述中,当哲学家饥饿时,总是先去拿他左边的筷子,即执行 wait(chopstick[i]); 成功后,再去拿他右边的筷子,即执行 wait(chopstick[(i+1)mod 5]);又成功后便可进餐。进餐完毕,又先放下他左边的筷子,然后再放右边的筷子。虽然,上述解法可保证不会有两个相邻的哲学家同时进餐,但有可能引起死锁。假如五位哲学家同时饥饿而各自拿起左边的筷子时,就会使五个信号量 chopstick 均为 0; 当他们再试图去拿右边的筷子时,都将因 无筷子可拿而无限期地等待。对于这样的死锁问题,可采取以下几种解决方法:
(1) 至多只允许有四位哲学家同时去拿左边的筷子,最终能保证至少有一位哲学家能够 进餐,并在用毕时能释放出他用过的两只筷子,从而使更多的哲学家能够进餐。
(2) 仅当哲学家的左、右两只筷子均可用时,才允许他拿起筷子进餐。 (3) 规定奇数号哲学家先拿他左边的筷子,然后再去拿右边的筷子,而偶数号哲学家则相反。按此规定,将是 1、2 号哲学家竞争 1 号筷子;3、4 号哲学家竞争 3 号筷子。即五位哲学家都先竞争奇数号筷子,获得后,再去竞争偶数号筷子,最后总会有一位哲学家能获 得两只筷子而进餐。
利用 AND 信号量机制解决哲学家进餐问题
在哲学家进餐问题中,要求每个哲学家先获得两个临界资源(筷子)后方能进餐,这在本质上就是前面所介绍的 AND 同步问题,故用 AND 信号量机制可获得最简洁的解法。描述
如下:
一个数据文件或记录,可被多个进程共享,我们把只要求读该文件的进程称为“Reader进程”,其他进程则称为“Writer 进程”。允许多个进程同时读一个共享对象,因为读操作不会使数据文件混乱。但不允许一个 Writer 进程和其他 Reader 进程或 Writer 进程同时访问共 享对象,因为这种访问将会引起混乱。所谓“读者—写者问题(Reader-Writer Problem)”是 指保证一个 Writer 进程必须与其他进程互斥地访问共享对象的同步问题。读者—写者问题常被用来测试新同步原语。
进程通信,是指进程之间的信息交换,其所交换的信息量少者是一个状态或数值,多者则是成千上万个字节。进程之间的互斥和同步,由于其所交换的信息量少而被归结为低级通信。在进程互斥中,进程通过只修改信号量来向其他进程表明临界资源是否可用。在生产者—消费者问题中,生产者通过缓冲池将所生产的产品传送给消费者。
应当指出,信号量机制作为同步工具是卓有成效的,但作为通信工具,则不够理想, 主要表现在下述两方面:
(1) 效率低,生产者每次只能向缓冲池投放一个产品(消息),消费者每次只能从缓冲区中取得一个消息;
(2) 通信对用户不透明。 可见,用户要利用低级通信工具实现进程通信是非常不方便的。因为共享数据结构的设置、数据的传送、进程的互斥与同步等,都必须由程序员去实现,操作系统只能提供共享存储器。
本节所要介绍的是高级进程通信,是指用户可直接利用操作系统所提供的一组通信命 令高效地传送大量数据的一种通信方式。操作系统隐藏了进程通信的实现细节。或者说, 通信过程对用户是透明的。这样就大大减少了通信程序编制上的复杂性。
###2.5.1 共享存储器系统
随着 OS 的发展,用于进程之间实现通信的机制也在发展,并已由早期的低级进程通信 机制发展为能传送大量数据的高级通信工具机制。目前,高级通信机制可归结为三大类:共享存储器系统、消息传递系统以及管道通信系统。
共享器存储系统
在共享存储器系统中,相互通信的进程共享某些数据结构或共享存储区,进程之间能够通过这些空间进行通信。据此,又可把它们分成以下两种类型: (1) 基于共享数据结构的通信方式。在这种通信方式中,要求诸进程公用某些数据结构, 借以实现诸进程间的信息交换。这里,公用数据结构的设置及对进程间同步的处理,都是程序员的职 责。这无疑增加了程序员的负担,而操作系统却只须提供共享存储器。因此,这种通信方式是低效的,只适于传递相对少量的数据。
(2) 基于共享存储区的通信方式。为了传输大量数据,在存储器中划出了一块共享存储 区,诸进程可通过对共享存储区中数据的读或写来实现通信。这种通信方式属于高级通信。进程在通信前,先向系统申请获得共享存储区中的一个分区,并指定该分区的关键字;若系统已经给其他进程分配了这样的分区,则将该分区的描述符返回给申请者,继之,由申请者把获得的共享存储分区连接到本进程上;此后,便可像读、写普通存储器一样地读、写该公用存储分区。
消息传递系统
消息传递系统是当前应用最为广泛的一种进程间的通信机制。 在该机制中,进程间的数据交换是以格式化的消息(message)为单位的;在计算机网络中,又把 message 称为报文。程序员直接利用操作系统提供的一组通信命令(原语),不仅能实现大量数据的传递,而且还隐藏了通信的实现细节,使通信过程对用户是透明的,从而大大减化了通信程序编制的复杂性,因而获得了广泛的应用。特别值得一提的是,在当今最为流行的微内核操作系统中,微内核与服务器之间的通信,无一例外地都采用了消息传递机制。又由于它能很好地支持多处理机系统、分布式系统和计算机网络,因此它也成为这些领域最主要的通信工具。消息传递系统的通信方式属 于高级通信方式。又因其实现方式的不同而进一步分成直接通信方式和间接通信方式两种。
管道通信
所谓“管道”,是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名 pipe 文件。向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入管道;而接受管道输出的接收进程(即读进程),则从管道中接收(读)数据。 由于发送进程和接收进程是利用管道进行通信的,故又称为管道通信。这种方式首创于 UNIX 系统,由于它能有效地传送大量数据,因而又被引入到许多其它的操作系统中。为了协调双方的通信,管道机制必须提供以下三方面的协调能力:
(1) 互斥,即当一个进程正在对 pipe 执行读/写操作时,其它(另一)进程必须等待。
(2) 同步,指当写(输入)进程把一定数量(如 4 KB)的数据写入 pipe,便去睡眠等待,直到读(输出)进程取走数据后,再把它唤醒。当读进程读一空 pipe 时,也应睡眠等待,直至写进程将数据写入管道后,才将之唤醒。
(3) 确定对方是否存在,只有确定了对方已存在时,才能进行通信。
在进程之间通信时,源进程可以直接或间接地将消息传送给目标进程,由此可将进程通信分为直接通信和间接通信两种通信方式。
Send(Receiver,message); 发送一个消息给接收进程;
Receive(Sender,message); 接收 Sender 发来的消息;
例如,原语 Send(P2,m1)表示将消息 m1 发送给接收进程 P2;而原语 Receive(P1,m1) 则表示接收由 P1 发来的消息 m1。
在某些情况下,接收进程可与多个发送进程通信,因此,它不可能事先指定发送进程。例如,用于提供打印服务的进程,它可以接收来自任何一个进程的“打印请求”消息。对于这样的应用,在接收进程接收消息的原语中,表示源进程的参数,也是完成通信后的返回值,接收原语可表示为:
Receive (id,message);
我们还可以利用直接通信原语来解决生产者—消费者问题。当生产者生产出一个产品 (消息)后,便用 Send 原语将消息发送给消费者进程;而消费者进程则利用 Receive 原语来得 到一个消息。如果消息尚未生产出来,消费者必须等待,直至生产者进程将消息发送过来。生产者—消费者的通信过程可分别描述如下:
(1) 一对一关系。这时可为发送进程和接收进程建立一条两者专用的通信链路,使两者之间的交互不受其他进程的干扰。
(2) 多对一关系。允许提供服务的进程与多个用户进程之间进行交互,也称为客户/服 务器交互。
(3) 一对多关系。允许一个发送进程与多个接收进程进行交互,使发送进程可用广播方 式向接收者(多个)发送消息。
(4) 多对多关系。允许建立一个公用信箱,让多个进程都能向信箱中投递消息;也可从 信箱中取走属于自己的消息。
在单机和计算机网络环境下,高级进程通信广泛采用消息传递系统。故本小节将对这种通信中的几个主要问题做扼要的阐述。
通信链路
为使在发送进程和接收进程之间能进行通信,必须在两者之间建立一条通信链路。有两种方式建立通信链路。
第一种方式是由发送进程在通信之前用显式的“建立连接”命令(原语)请求系统为之建立一条通信链路;在链路使用完后,也用显式方式拆除链路。这种方式主要用于计算机网络中。
第二种方式是发送进程无须明确提出建立链路的请求,只须利用系统提供的发送命令(原语),系统会自动地为之建立一条链路。这种方式主要用于单机系统中。
根据通信链路的连接方法,又可把通信链路分为两类:
(1) 点——点连接通信链路,这时的一条链路只连接两个结点(进程);
(2) 多点连接链路,指用一条链路连接多个(n>2)结点(进程)。
而根据通信方式的不同,则又可把链路分成两种:
(1) 单向通信链路,只允许发送进程向接收进程发送消息,或者相反;
(2) 双向链路,既允许由进程 A 向进程 B 发送消息,也允许进程 B 同时向进程 A 发送 消息。
还可根据通信链路容量的不同而把链路分成两类:
一是无容量通信链路,在这种通信链路上没有缓冲区,因而不能暂存任何消息;
二是有容量通信链路,指在通信链路中 设置了缓冲区,因而能暂存消息。缓冲区数目愈多,通信链路的容量愈大。
消息的格式
在消息传递系统中所传递的消息,必须具有一定的消息格式。在单机系统环境中,由于发送进程和接收进程处于同一台机器中,有着相同的环境,故其消息格式比较简单;但在计算机网络环境下,不仅源和目标进程所处的环境不同,而且信息的传输距离很远,可能要跨越若干个完全不同的网络,致使所用的消息格式比较复杂。通常,可把一个消息分成消息头和消息正文两部分。消息头包括消息在传输时所需的控制信息,如源进程名、目 标进程名、消息长度、消息类型、消息编号及发送的日期和时间;而消息正文则是发送进 程实际上所发送的数据。
在某些 OS 中,消息采用比较短的定长消息格式,这便减少了对消息的处理和存储开销。这种方式可用于办公自动化系统中,为用户提供快速的便笺式通信;但这对要发送较长消息的用户是不方便的。在有的 OS 中,采用变长的消息格式,即进程所发送消息的长度是可变的。系统无论在处理还是在存储变长消息时,都可能会付出更多的开销,但这方便了用户。这两种消息格式各有其优缺点,故在很多系统(包括计算机网络)中,是同时都用的。
进程同步方式
在进程之间进行通信时,同样需要有进程同步机制,以使诸进程间能协调通信。不论是发送进程,还是接收进程,在完成消息的发送或接收后,都存在两种可能性,即进程或者继续发送(接收),或者阻塞。由此,我们可得到以下三种情况: (1) 发送进程阻塞,接收进程阻塞。这种情况主要用于进程之间紧密同步,发送进程和接收进程之间无缓冲时。这两个进程平时都处于阻塞状态,直到有消息传递时。这种同步方式称为汇合。
(2) 发送进程不阻塞,接收进程阻塞。这是一种应用最广的进程同步方式。平时,发送进程不阻塞,因而它可以尽快地把一个或多个消息发送给多个目标; 而接收进程平时则处 于阻塞状态,直到发送进程发来消息时才被唤醒。例如,在服务器上通常都设置了多个服务进程,它们分别用于提供不同的服务,如打印服务。平时,这些服务进程都处于阻塞状 态,一旦有请求服务的消息到达时,系统便唤醒相应的服务进程,去完成用户所要求的服 务。处理完后,若无新的服务请求,服务进程又阻塞。
(3) 发送进程和接收进程均不阻塞。这也是一种较常见的进程同步形式。平时,发送进程和接收进程都在忙于自己的事情,仅当发生某事件使它无法继续运行时,才把自己阻塞起来等待。例如,在发送进程和接收进程之间联系着一个消息队列时,该消息队列最多能 接纳 n 个消息,这样,发送进程便可以连续地向消息队列中发送消息而不必等待;接收进程也可以连续地从消息队列中取得消息,也不必等待。只有当消息队列中的消息数已达到 n个时,即消息队列已满,发送进程无法向消息队列中发送消息时才会阻塞;类似地,只有 当消息队列中的消息数为 0,接收进程已无法从消息队列中取得消息时才会阻塞。
消息缓冲队列通信机制首先由美国的 Hansan 提出,并在 RC 4000 系统上实现,后来被广泛应用于本地进程之间的通信中。在这种通信机制中,发送进程利用 Send 原语将消息直接发送给接收进程;接收进程则利用 Receive 原语接收消息。
消息缓冲队列通信机制中的数据结构
1)消息缓冲区
在消息缓冲队列通信方式中,主要利用的数据结构是消息缓冲区。它可描述如下:
2)PCB 中有关通信的数据项
在操作系统中采用了消息缓冲队列通信机制时,除了需要为进程设置消息缓冲队列外, 还应在进程的 PCB 中增加消息队列队首指针,用于对消息队列进行操作,以及用于实现同 步的互斥信号量 mutex 和资源信号量 sm。在 PCB 中应增加的数据项可描述如下:
发送原语
发送进程在利用发送原语发送消息之前,应先在自己的内存空间设置一发送区 a,见图 2-14 所示。把待发送的消息正文、发送进程标识符、消息长度等信息填入其中,然后调用发送原语,把消息发送给目标(接收)进程。发送原语首先根据发送区 a 中所设置的消息长度 a.size 来申请一缓冲区 i,接着把发送区 a 中的信息复制到缓冲区 i 中。为了能将 i 挂在接收进程的消息队列 mq 上,应先获得接收进程的内部标识符 j,然后将 i 挂在 j.mq 上。由于该队列属于临界资源,故在执行 insert 操作的前后,都要执行 wait 和 signal 操作。
发送原语可描述如下:
接收原语
接收进程调用接收原语 receive(b),从自己的消息缓冲队列 mq 中摘下第一个消息缓冲区 i,并将其中的数据复制到以 b 为首址的指定消息接收区内。接收原语描述如下:
自从在 20 世纪 60 年代人们提出了进程的概念后,在 OS 中一直都是以进程作为能拥有资源和独立运行的基本单位的。直到 20 世纪 80 年代中期,人们又提出了比进程更小的能 独立运行的基本单位——线程(Threads),试图用它来提高系统内程序并发执行的程度,从而可进一步提高系统的吞吐量。特别是在进入 20 世纪 90 年代后,多处理机系统得到迅速发 展,线程能比进程更好地提高程序的并行执行程度,充分地发挥多处理机的优越性,因而 在近几年所推出的多处理机 OS 中也都引入了线程,以改善 OS 的性能。
1) 创建进程
2) 撤消进程
3) 进程切换
换言之,由于进程是一个资源的拥有者,因而在创建、撤消和切换中,系统必须为之付出较大的时空开销。正因如此,在系统中所设置的进程,其数目不宜过多,进程切换的频率也不宜过高,这也就限制了并发程度的进一步提高。 如何能使多个程序更好地并发执行同时又尽量减少系统的开销,已成为近年来设计操作系统时所追求的重要目标。有不少研究操作系统的学者们想到,若能将进程的上述两个
属性分开,由操作系统分开处理,亦即对于作为调度和分派的基本单位,不同时作为拥有资源的单位,以做到“轻装上阵”;而对于拥有资源的基本单位,又不对之进行频繁的切换。正是在这种思想的指导下,形成了线程的概念。
因为进程“太重”,致使实现多处理机环境下的进程调度、分派和切换时,都 需花费较大的时间和空间开销。如果在 OS 中引入线程,以线程作为调度和分派的基本单位,则可以有效地改善多处理机系统的性能。因此,一些主要的 OS(UNIX、OS/2、Windows)厂 家都又进一步对线程技术做了开发,使之适用于 SMP 的计算机系统。
- 调度
在传统的操作系统中,作为拥有资源的基本单位和独立调度、分派的基本单位都是进程。而在引入线程的操作系统中,则把线程作为调度和分派的基本单位,而进程作为资源拥有的基本单位,把传统进程的两个属性分开,使线程基本上不拥有资源,这样线程便能轻装前进,从而可显著地提高系统的并发程度。在同一进程中,线程的切换不会引起进程的切换,但从一个进程中的线程切换到另一个进程中的线程时,将会引起进程的切换。- 并发性
在引入线程的操作系统中,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间亦可并发执行,使得操作系统具有更好的并发性,从而能更加有效地提高系统资源的利用率和系统的吞吐量。例如,在一个未引入线程的单 CPU 操作系统中,若仅设置一个文件服务进程,当该进程由于某种原因而被阻塞时,便没有其它的文件服务进程来提供服务。在引入线程的操作系统中,则可以在一个文件服务进程中设置多个服务线程。当第一 个线程等待时,文件服务进程中的第二个线程可以继续运行,以提供文件服务;当第二个 线程阻塞时,则可由第三个继续执行,提供服务。显然,这样的方法可以显著地提高文件 服务的质量和系统的吞吐量。- 拥有资源
不论是传统的操作系统,还是引入了线程的操作系统,进程都可以拥有资源,是系统中拥有资源的一个基本单位。一般而言,线程自己不拥有系统资源(也有一点必不可少的资 源),但它可以访问其隶属进程的资源,即一个进程的代码段、数据段及所拥有的系统资源, 如已打开的文件、I/O 设备等,可以供该进程中的所有线程所共享。- 系统开销
在创建或撤消进程时,系统都要为之创建和回收进程控制块,分配或回收资源,如内存空间和 I/O 设备等,操作系统所付出的开销明显大于线程创建或撤消时的开销。类似地,在进程切换时,涉及到当前进程 CPU 环境的保存及新被调度运行进程的 CPU 环境的设置,而线程的切换则仅需保存和设置少量寄存器内容,不涉及存储器管理方面的操作,所以就 切换代价而言,进程也是远高于线程的。此外,由于一个进程中的多个线程具有相同的地址空间,在同步和通信的实现方面线程也比进程容易。在一些操作系统中,线程的切换、 同步和通信都无须操作系统内核的干预。
线程的属性
在多线程 OS 中,通常是在一个进程中包括多个线程,每个线程都是作为利用 CPU 的 基本单位,是花费最小开销的实体。线程具有下述属性。
(1) 轻型实体。线程中的实体基本上不拥有系统资源,只是有一点必不可少的、 能保 证其独立运行的资源,比如,在每个线程中都应具有一个用于控制线程运行的线程控制块 TCB,用于指示被执行指令序列的程序计数器,保留局部变量、少数状态参数和返回地址 等的一组寄存器和堆栈。
(2) 独立调度和分派的基本单位。在多线程 OS 中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小。
(3) 可并发执行。在一个进程中的多个线程之间可以并发执行,甚至允许在一个进程中的所有线程都能并发执行;同样,不同进程中的线程也能并发执行。
(4) 共享进程资源。在同一进程中的各个线程都可以共享该进程所拥有的资源,这首先表现在所有线程都具有相同的地址空间(进程的地址空间)。这意味着线程可以访问该地址空间中的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构 等。
线程的状态
(1) 状态参数。在 OS 中的每一个线程都可以利用线程标识符和一组状态参数进行描述。状态参数通常有这样几项:
① 寄存器状态,它包括程序计数器 PC 和堆栈指针中的内容;
② 堆栈,在堆栈中通常保存有局部变量和返回地址;
③ 线程运行状态,用于描述线程正处于何种运行状态;
④ 优先级,描述线程执行的优先程度;
⑤ 线程专有存储器,用于保 存线程自己的局部变量拷贝;
⑥ 信号屏蔽,即对某些信号加以屏蔽。
(2) 线程运行状态。如同传统的进程一样,在各线程之间也存在着共享资源和相互合作 的制约关系,致使线程在运行时也具有间断性。相应地,线程在运行时也具有下述三种基本状态:
① 执行状态,表示线程正获得处理机而运行;
② 就绪状态,指线程已具备了各种执行条件,一旦获得 CPU 便可执行的状态;
③ 阻塞状态,指线程在执行中因某事件而受 阻,处于暂停执行时的状态。
线程的创建和终止
在多线程 OS 环境下,应用程序在启动时,通常仅有一个线程在执行,该线程被人们称 为“初始化线程”。它可根据需要再去创建若干个线程。在创建新线程时,需要利用一个线程创建函数(或系统调用),并提供相应的参数,如指向线程主程序的入口指针、堆栈的大 小,以及用于调度的优先级等。在线程创建函数执行完后,将返回一个线程标识符供以后使用。
如同进程一样,线程也是具有生命期的。终止线程的方式有两种:
一种是在线程完成了自己的工作后自愿退出;
一种是线程在运行中出现错误或由于某种原因而被其它线程 强行终止。
但有些线程(主要是系统线程),在它们一旦被建立起来之后,便一直运行下去而不再被终止。在大多数的 OS 中,线程被中止后并不立即释放它所占有的资源,只有当进程中的其它线程执行了分离函数后,被终止的线程才与资源分离,此时的资源才能被其它线 程利用。虽已被终止但尚未释放资源的线程,仍可以被需要它的线程所调用,以使被终止线程重新恢复运行。
为此,调用者线程须调用一条被称为“等待线程终止”的连接命令,来与 该线程进行连接。如果在一个调用者线程调用“等待线程终止”的连接命令试图与指定线程相连接时,若指定线程尚未被终止,则调用连接命令的线程将会阻塞,直至指定线程被终止后才能实现它与调用者线程的连接并继续执行;若指定线程已被终止,则调用者线程不会被阻塞而是继续执行。
多线程 OS 中的进程
在多线程 OS 中,进程是作为拥有系统资源的基本单位,通常的进程都包含多个线程并为它们提供资源,但此时的进程就不再作为一个执行的实体。多线程 OS 中的进程有以下属性:
(1) 作为系统资源分配的单位。在多线程 OS 中,仍是将进程作为系统资源分配的基本 单位,在任一进程中所拥有的资源包括受到分别保护的用户地址空间、用于实现进程间和线程间同步和通信的机制、已打开的文件和已申请到的 I/O 设备,以及一张由核心进程维护 的地址映射表,该表用于实现用户程序的逻辑地址到其内存物理地址的映射。
(2) 可包括多个线程。通常,一个进程都含有多个相对独立的线程,其数目可多可少,但至少也要有一个线程,由进程为这些(个)线程提供资源及运行环境,使这些线程可并发执行。在 OS 中的所有线程都只能属于某一个特定进程。
(3) 进程不是一个可执行的实体。在多线程 OS 中,是把线程作为独立运行的基本单位, 所以此时的进程已不再是一个可执行的实体。虽然如此,进程仍具有与执行相关的状态。例如,所谓进程处于“执行”状态,实际上是指该进程中的某线程正在执行。此外,对进程所施加的与进程状态有关的操作,也对其线程起作用。例如,在把某个进程挂起时,该进程中的所有线程也都将被挂起;又如,在把某进激活时,属于该进程的所有线程也都 将被激活。
为使系统中的多线程能有条不紊地运行,在系统中必须提供用于实现线程间同步和通信的机制。为了支持不同频率的交互操作和不同程度的并行性,在多线程 OS 中通常提供多种同步机制,如互斥锁、条件变量、计数信号量以及多读、单写锁等。
互斥锁
互斥锁是一种比较简单的、用于实现线程间对资源互斥访问的机制。由于操作互斥锁的时间和空间开销都较低,因而较适合于高频度使用的关键共享数据和程序段。互斥锁可以有两种状态,即开锁(unlock)和关锁(lock)状态。相应地,可用两条命令(函数)对互斥锁进行操作。其中的关锁 lock 操作用于将 mutex 关上,开锁操作 unlock 则用于打开 mutex。
当一个线程需要读/写一个共享数据段时,线程首先应为该数据段所设置的 mutex 执行关锁命令。命令首先判别 mutex 的状态,如果它已处于关锁状态,则试图访问该数据段的线程将被阻塞;而如果 mutex 是处于开锁状态,则将 mutex 关上后便去读/写该数据段。
在线程完成对数据的读/写后,必须再发出开锁命令将 mutex 打开,同时还须将阻塞在该互斥锁上的一个线程唤醒,其它的线程仍被阻塞在等待 mutex 打开的队列上。 另外,为了减少线程被阻塞的机会,在有的系统中还提供了一种用于 mutex 上的操作命令 Trylock。当一个线程在利用 Trylock 命令去访问 mutex 时,若 mutex 处于开锁状态, Trylock 将返回一个指示成功的状态码;反之,若 mutex 处于关锁状态,则 Trylock 并不会阻塞该线程,而只是返回一个指示操作失败的状态码。
条件变量
在许多情况下,只利用 mutex 来实现互斥访问可能会引起死锁,我们通过一个例子来 说明这一点。有一个线程在对 mutex 1 执行关锁操作成功后,便进入一临界区 C,若在临界 区内该线程又须访问某个临界资源 R,同样也为 R 设置另一互斥锁 mutex 2。假如资源 R 此时正处于忙碌状态,线程在对 mutex 2 执行关锁操作后必将被阻塞,这样将使 mutex 1 一直保持关锁状态;如果保持了资源 R 的线程也要求进入临界区 C,但由于 mutex 1 一直保持关 锁状态而无法进入临界区,这样便形成了死锁。为了解决这个问题便引入了条件变量。
每一个条件变量通常都与一个互斥锁一起使用,亦即,在创建一个互斥锁时便联系着一个条件变量。单纯的互斥锁用于短期锁定,主要是用来保证对临界区的互斥进入。而条件变量则用于线程的长期等待,直至所等待的资源成为可用的资源。
现在,我们看看如何利用互斥锁和条件变量来实现对资源 R 的访问。线程首先对 mutex执行关锁操作,若成功便进入临界区,然后查找用于描述该资源状态的数据结构,以了解资源的情况。只要发现所需资源 R 正处于忙碌状态,线程便转为等待状态,并对 mutex 执 行开锁操作后,等待该资源被释放;若资源处于空闲状态,表明线程可以使用该资源,于是将该资源设置为忙碌状态,再对 mutex 执行开锁操作。下面给出了对上述资源的申请(左 半部分)和释放(右半部分)操作的描述。
原来占有资源 R 的线程在使用完该资源后,便按照右半部分的描述释放该资源,其中 的 wakeup表示去唤醒在指定条件变量上等待的一个或多个线程。在大多数情况下,由于所释放的是临界资源,此时所唤醒的只能是在条件变量上等待的某一个线程,其它线程仍继续在该队列上等待。但如果线程所释放的是一个数据文件,该文件允许多个线程同时对它执行读操作。在这种情况下,当一个写线程完成写操作并释放该文件后,如果此时在该条件变量上还有多个读线程在等待,则该线程可以唤醒所有的等待线程。
信号量机制
前面所介绍的用于实现进程同步的最常用工具——信号量机制,也可用于多线程 OS中,实现诸线程或进程之间的同步。为了提高效率,可为线程和进程分别设置相应的信号量。
- 私用信号量(private samephore)
当某线程需利用信号量来实现同一进程中各线程之间的同步时,可调用创建信号量的命令来创建一私用信号量,其数据结构存放在应用程序的地址空间中。私用信号量属于特定的进程所有,OS 并不知道私用信号量的存在,因此,一旦发生私用信号量的占用者异常结束或正常结束,但并未释放该信号量所占有空间的情况时,系统将无法使它恢复为 0(空),也不能将它传送给下一个请求它的线程。- 公用信号量(public semaphort)
公用信号量是为实现不同进程间或不同进程中各线程之间的同步而设置的。由于它有着一个公开的名字供所有的进程使用,故而把它称为公用信号量。其数据结构是存放在受保护的系统存储区中,由 OS 为它分配空间并进行管理,故也称为系统信号量。如果信号量的占有者在结束时未释放该公用信号量,则 OS 会自动将该信号量空间回收,并通知下一进程。可见,公用信号量是一种比较安全的同步机制。
这种线程实现方式主要有如下四个优点:
(1) 在多处理器系统中,内核能够同时调度同一进程中多个线程并行执行;
(2) 如果进程中的一个线程被阻塞了,内核可以调度该进程中的其它线程占有处理器运行,也可以运行其它进程中的线程;
(3) 内核支持线程具有很小的数据结构和堆栈,线程的切换比较快,切换开销小;
(4) 内核本身也可以采用多线程技术,可以提高系统的执行速度和效率。
内核支持线程的主要缺点是:
对于用户的线程切换而言,其模式切换的开销较大,在同一个进程中,从一个线程切换到另一个线程时,需要从用户态转到内核态进行,这是因为用户进程的线程在用户态运行,而线程调度和管理是在内核实现的,系统开销较大。
(1) 线程切换不需要转换到内核空间,对一个进程而言,其所有线程的管理数据结构均 在该进程的用户空间中,管理线程切换的线程库也在用户地址空间运行。因此,进程不必切换到内核方式来做线程管理,从而节省了模式切换的开销,也节省了内核的宝贵资源。
(2) 调度算法可以是进程专用的。在不干扰操作系统调度的情况下,不同的进程可以根 据自身需要,选择不同的调度算法对自己的线程进行管理和调度,而与操作系统的低级调 度算法是无关的。
(3) 用户级线程的实现与操作系统平台无关,因为对于线程管理的代码是在用户程序内的,属于用户程序的一部分,所有的应用程序都可以对之进行共享。因此,用户级线程甚至可以在不支持线程机制的操作系统平台上实现。
用户级线程实现方式的主要缺点在于如下两个方面:
(1) 系统调用的阻塞问题。在基于进程机制的操作系统中,大多数系统调用将阻塞进程,因此,当线程执行一个系统调用时,不仅该线程被阻塞,而且进程内的所有线程都会被阻塞。而在内核支持线程方式中,则进程中的其它线程仍然可以运行。
(2) 在单纯的用户级线程实现方式中,多线程应用不能利用多处理机进行多重处理的优点。内核每次分配给一个进程的仅有一个 CPU,因此进程中仅有一个线程能执行,在该线程放弃 CPU 之前,其它线程只能等待。
每当进程要创建一个线程时,便为新线程分配一个TCB,将有关信息填入该 TCB 中,并为之分配必要的资 源,如为线程分配数百至数千个字节的栈空间和局部存储区,于是新创建的线程便有条件立即执行。当 PTDA中的所有 TCB 空间已用完,而进程又要创建新的线程时,只要其所创建的线程数目未超过系统的允许值(通常为数十至数百个),系统可再为之分配新的 TCB 空间;在撤消一个线程时,也应回收该线程的所有资源和 TCB。
可见,内核支持线程的创建、 撤消均与进程的相类似。在有的系统中为了减少创建和撤消一个线程时的开销,在撤消一个线程时,并不立即回收该线程的资源和 TCB,当以后再要创建一个新线程时,便可直接利用已被撤消但仍保持有资源和 TCB 的线程作为新线程。
内核支持线程的调度和切换与进程的调度和切换十分相似,也分抢占式方式和非抢占方式两种。在线程的调度算法上,同样可采用时间片轮转法、优先权算法等。当线程调度选中一个线程后,便将处理机分配给它。当然,线程在调度和切换上所花费的开销,要比进程的小得多。
1)运行时系统(Runtime System)
所谓“运行时系统”,实质上是用于管理和控制线程的函数(过程)的集合,其中包括用于创建和撤消线程的函数、 线程同步和通信的函数以及实现线程调度的函数等。正因为有这些函数,才能使用户级线程与内核无关。运行时系统中的所有函数都驻留在用户空间,并作为用户级线程与内核之间的接口。
在传统的 OS 中,进程在切换时必须先由用户态转为核心态,再由核心来执行切换任务; 而用户级线程在切换时则不需转入核心态,而是由运行时系统中的线程切换过程来执行切换任务。该过程将线程的 CPU 状态保存在该线程的堆栈中,然后按照一定的算法选择一个处于就绪状态的新线程运行,将新线程堆栈中的 CPU 状态装入到 CPU 相应的寄存器中,一旦将栈指针和程序计数器切换后,便开始了新线程的运行。
由于用户级线程的切换无需进 入内核,且切换操作简单,因而使用户级线程的切换速度非常快。 不论在传统的 OS 中,还是在多线程 OS 中,系统资源都是由内核管理的。在传统的OS 中,进程是利用 OS 提供的系统调用来请求系统资源的,系统调用通过软中断(如 trap)机制进入 OS 内核,由内核来完成相应资源的分配。用户级线程是不能利用系统调用的。当线程需要系统资源时,是将该要求传送给运行时系统,由后者通过相应的系统调用来获得系统资源的。
2)内核控制线程
这种线程又称为轻型进程 LWP。每一个进程都可拥有多个 LWP,同用户级线程一样,每个 LWP 都有自己的数据结构(如 TCB),其中包括线程标识符、优先级、状态,另外还有栈和局部存储区等。它们也可以共享进程所拥有的资源。LWP 可通过系统调用来获得内核提供的服务,这样,当一个用户级线程运行时,只要将它连接到一个LWP 上,此时它便具有了内核支持线程的所有属性。这种线程实现方式就是组合方式。
在一个系统中的用户级线程数量可能很大,为了节省系统开销,不可能设置太多的LWP,而把这些 LWP 做成一个缓冲池,称为“线程池”。用户进程中的任一用户线程都可 以连接到 LWP 池中的任何一个 LWP 上。为使每一用户级线程都能利用 LWP 与内核通信,可以使多个用户级线程多路复用一个 LWP,但只有当前连接到 LWP 上的线程才能与内核通 信,其余进程或者阻塞,或者等待 LWP。而每一个 LWP 都要连接到一个内核级线程上,这样,通过 LWP 可把用户级线程与内核线程连接起来,用户级线程可通过 LWP 来访问内核, 但内核所看到的总是多个 LWP 而看不到用户级线程。亦即,由 LWP 实现了在内核与用户 级线程之间的隔离,从而使用户级线程与内核无关。
图 2-16 示出了利用轻型进程作为中间系统时用户级线程的实现方法。
当用户级线程不需要与内核通信时,并不需要 LWP;而当要通信时,便需借助于 LWP, 而且每个要通信的用户级线程都需要一个 LWP。
例如,在一个任务中,如果同时有 5 个用户级线程发出了对文件的读、写请求,这就需要有 5 个 LWP 来予以帮助,即由 LWP 将对 文件的读、写请求发送给相应的内核级线程,再由后者执行具体的读、写操作。
如果一个任务中只有 4 个 LWP,则只能有 4 个用户级线程的读、写请求被传送给内核线程,余下的一个用户级线程必须等待。 在内核级线程执行操作时,如果发生阻塞,则与之相连接的多个 LWP 也将随之阻塞, 进而使连接到 LWP 上的用户级线程也被阻塞。如果进程中只包含了一个 LWP,此时进程也应阻塞。这种情况与前述的传统 OS 一样,在进程执行系统调用时,该进程实际上是阻塞的。
但如果在一个进程中含有多个 LWP,则当一个 LWP 阻塞时,进程中的另一个 LWP 可继续执行;即使进程中的所有 LWP 全部阻塞,进程中的线程也仍然能继续执行,只是不能再去访问内核。
- 一对一模型
该模型是为每一个用户线程都设置一个内核控制线程与之连接,当一个线程阻塞时,允许调度另一个线程运行。在多处理机系统中,则有多个线程并行执行。该模型并行能力较强,但每创建一个用户线程相应地就需要创建一个内核线程,开销 较大,因此需要限制整个系统的线程数。Windows 2000、Windows NT、OS/2 等系统上都实 现了该模型。- 多对一模型
该模型是将多个用户线程映射到一个内核控制线程,为了管理方便,这些用户线程一般属于一个进程,运行在该进程的用户空间,对这些线程的调度和管理也是在该进程的用户空间中完成。当用户线程需要访问内核时,才将其映射到一个内核控制线程上,但每次 只允许一个线程进行映射。 该模型的主要优点是线程管理的开销小,效率高,但当一个线程在访问内核时发生阻 塞,则整个进程都会被阻塞,而且在多处理机系统中,一个进程的多个线程无法实现并行。- 多对多模型
该模型结合上述两种模型的优点,将多个用户线程映射到多个内核控制线程,内核控制线程的数目可以根据应用进程和系统的不同而变化,可以比用户线程少,也可以与之相同。