本系列博客重点在深圳大学操作系统课程的核心内容梳理,参考书目《计算机操作系统》(有问题欢迎在评论区讨论指出,或直接私信联系我)。
梗概
本篇博客主要介绍操作系统第二章进程的描述与控制的相关知识。
目录
先来道小例题吧:
我们都知道,cpu是不停地在进程之间切换的.那么对于一个进程来说,在下面哪种情况下,它一定获得cpu?
A.进程未退出
B.进程在等待一个I/O操作结束
C.进程正在做一个复杂的运算
D.以上都不是
综述:在操作系统中,CPU 资源是由调度器进行分配的。调度器决定了哪个进程获得 CPU 的使用权。当系统有多个进程在运行时,调度器会将 CPU 时间分配给这些进程,让它们轮流占用 CPU,以完成它们的工作。
A:进程未退出,若处于阻塞状态不会占用CPU,被唤醒进入就绪态才会等待获取CPU,同时操作系统还有优先级的概念,高优先级先获得CPU
B:进程在等待一个I/O操作结束,同样可能主动放弃CPU,使其他就绪进程获得CPU(一般通过阻塞等待),
Tips:能获得CPU的状态如下:当进程发出I/O请求后,如果设备未就绪,则进程可能会被操作系统挂起并等待I/O完成,此时进程被称为阻塞状态。在阻塞状态下,进程会暂时放弃CPU,直到I/O操作完成并且进程准备好再次运行。
C:进程正在做一个复杂的运算,同上,取决于程序的优先级与操作系统的调度策略。
一、前驱图与程序执行
1.程序顺序执行
仅当前一操作(程序段)执行完后,才能执行后继操作。
此时程序独占处理机,特征有:顺序性、封闭性(运行时各资源只受该程序控制改变)、可再现性
2.前驱图
前趋图是一个有向无循环图,记为DAG,用于描述进程之间执行的前后关系。图中的每个结点可用于描述一个程序段或进程,乃至一条语句,同时含有一个重量,用于表示该结点所含有的程序量或结点的执行时间。
Tips:Pi→Pj,称Pi是Pj的直接前趋,而称Pj是Pi的直接后继。没有前驱或后继即起始/终止结点。
对于示例的前驱图,有以下前驱关系:
3.程序的并发执行
顺序执行的程序虽然方便,但系统资源利用率很低,故引入多道程序技术,使其并发执行。
Tips:只有不存在前驱关系的程序之间才有可能并发执行。
下面来看一个例子:
对于上图的顺序执行,很容易发现,存在Ii→Ci→Pi(泛化为一个作业的输入、计算、打印),但实际上可以并发执行优化效率,如下(可并发的部分用红线标出,Pi-1和Ci以及Ii+1之间):
再看一个例子:
对于具有下述四条语句的程序段:
S1: a∶=x+2 S2: b∶=y+4 S3: c∶=a+b S4: d∶=c+b
前驱图如下:
引入并发执行后,提高了系统的吞吐量与资源利用率,但它们合作完成同一任务也互相制约,故出现了一些新特征如下:
间断性(相互制约将导致并发程序具有“执行-暂停-执行”这种间断性活动规律)
失去封闭性(系统资源由多个程序改变)
不可再现性(程序多次在环境与初始条件相同的情况下运行,得到的结果各不相同)
一个例子如下:
二、进程的描述
由于程序在并发执行时,可能会造成执行结果的不可再现,所以用“程序”这个概念已无法描述程序的并发执行,所以必须引入新的概念---进程来描述程序的并发执行,并要对进程进行必要的管理,以保证进程在并发执行时结果可再现。
1.定义与特征
进程定义:可并发执行的程序在一个数据集合上的运行过程 —— 麻省理工,最早定义
其他典型定义:
(1) 进程是程序的一次执行。
(2) 进程是一个程序及其数据在处理机上顺序执行时所发生的活动。
(3) 进程是程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。
在引入了进程实体的概念后,我们可以把传统OS中的进程定义为:“进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位”。
进程与程序是两个截然不同的概念,进程的特征如下:
动态性:动态性是进程的最基本特征,它是程序执行过程,它是有一定的生命期(创建、调度、暂停、终止)。而程序是静态的,它是存放在介质上一组有序指令的集合。
并发性:并发性是进程的重要特征,同时也是OS的重要特征。并发性指多个进程实体同存于内存中,能在一段时间内同时运行。而程序是不能并发执行。
独立性:进程是一个能独立运行的基本单位,即是一个独立获得资源和独立调度的单位,而程序不作为独立单位参加运行。
异步性:进程按各自独立的不可预知的速度向前推进,即进程按异步方式进行,正是这一特征,将导致程序执行的不可再现性,因此OS必须采用某种措施来限制各进程推进序列以保证各程序间正常协调运行。
结构特征:从结构上,进程实体由程序段、数据段和进程控制块(PCB)三部分组成,UNIX中称为“进程映象”。
一个并发运行实例如下:
2.进程状态
2.1 基本状态
首先引入进程的三种基本状态
运行态/执行态(Running):一个进程在处理机(CPU)上运行。
就绪态(Ready):一个进程获得了除处理机(CPU)外的一切所需资源,得到处理机即可运行。
阻塞态(Blocked):(又称挂起状态、等待状态):一个进程正在等待某一事件发生(例如请求I/O、申请缓冲区失败)而暂时停止运行,这时即使把处理机分配给进程也无法运行。
三种基本状态及其转换图如下:
接下来看一道例题:
一个只有一个处理机的系统中,OS的进程有运行、就绪、阻塞三个基本状态。假如某时刻该系统中有10个进程并发执行,在略去调度程序所占用时间情况下试问:
这时刻系统中处于运行态的进程数最多几个?最少几个 (1 0)
这时刻系统中处于就绪态的进程数最多几个?最少几个 (9 0)
这时刻系统中处于阻塞态的进程数最多几个?最少几个 (10 0)
一个CPU某时刻最多进程就1个,如果为0其它10个进程一定全部排在各阻塞队列中,CPU有空,调度程序马上调度(略去调度程序所占用时间情况下,要不然处于就绪态最多10)。
系统各进程状态管理如下:
- 处于运行态进程:如系统有一个处理机,则在任何一时刻,最多只有一个进程处于运行态。
- 处于就绪态进程:一般处于就绪态的进程按照一定的算法(如先来的进程排在前面,或采用优先权高的进程排在前面)排成一个就绪队列RL。
- 处于阻塞态进程:处于阻塞态的进程排在阻塞队列中。由于等待事件原因不同,阻塞队列也按事件分成几个队列WLi。
2.2 状态补充
根据上述进程状态管理,为提高灵活性,通常会引入创建状态与终止状态。
在许多系统中,为了系统和用户观察和分析进程的需要,常引入挂起与激活操作。
Tips:挂起主要是停止进程,排除系统故障或观察中间结果。
3.进程控制块(PCB)
为了使参与并发执行的每个程序(含数据)能独立执行,操作系统为其配备了专门的记录型数据结构——结构控制块(PCB),系统利用PCB来描述进程的基本情况和活动过程。 或者说,OS是根据PCB来对并发执行的进程进行控制和管理的。
3.1 包含信息
PCB包含信息如下:
1) 进程标识符
进程标识符用于惟一地标识一个进程。一个进程通常有两种标识符:
(1) 内部标识符。在所有的操作系统中,都为每一个进程赋予一个惟一的数字标识符,它通常是一个进程的序号。 设置内部标识符主要是为了方便系统使用。
(2) 外部标识符。它由创建者提供,通常是由字母、数字组成,往往是由用户(进程)在访问该进程时使用。为了描述进程的家族关系, 还应设置父进程标识及子进程标识。此外,还可设置用户标识,以指示拥有该进程的用户。
2) 处理机(CPU)状态
处理机状态信息主要是由处理机的各种寄存器中的内容组成的。
① 通用寄存器,又称为用户可视寄存器,它们是用户程序可以访问的,用于暂存信息, 在大多数处理机中,有 8~32 个通用寄存器,在RISC结构的计算机中可超过 100 个;
② 指令计数器,其中存放了要访问的下一条指令的地址;
③ 程序状态字PSW,其中含有状态信息,如条件码、执行方式、 中断屏蔽标志等;
④ 用户栈指针, 指每个用户进程都有一个或若干个与之相关的系统栈,用于存放过程和系统调用参数及调用地址。栈指针指向该栈的栈顶。
3) 进程调度信息
在PCB中还存放一些与进程调度和进程对换有关的信息,包括:
① 进程状态,指明进程的当前状态, 作为进程调度和对换时的依据;
② 进程优先级,描述进程使用处理机的优先级别的一个整数,优先级高的进程优先获得处理机;
③ 进程调度所需的其它信息,它们与所采用的进程调度算法有关,比如,进程已等待CPU的时间总和、 进程已执行的时间总和等;
④ 事件,是指进程由执行状态转变为阻塞状态所等待发生的事件,即阻塞原因。
4) 进程控制信息
进程控制信息包括:
① 程序和数据的地址, 是指进程的程序和数据所在的内存或外存地(首)址,以便再调度到该进程执行时,能从PCB中找到其程序和数据;
② 进程同步和通信机制,指实现进程同步和进程通信时必需的机制, 如消息队列指针、信号量等,它们可能全部或部分地放在PCB中;
③ 资源清单,一张列出了除CPU外的、进程所需的全部资源及已经分配到该进程的资源的清单;
④ 链接指针, 它给出了本进程(PCB)所在队列中的下一个进程的PCB的首地址。
3.2 PCB组织方式
一个系统一般有数十个甚至数千个PCB,为了有效管理,一般有不同的组织方式:
- 线性方式:所有PCB在一张线性表里, 表的首地址放在内存一个专用区域。 - 简单、开销小,但每次查找要扫描整张表,适合少PCB。
- 链接方式:相同状态进程的PCB组成一个队列,按一定顺序排列(优先级或者进入顺序)
- 索引方式:根据进程状态不同,建立几张索引表,首地址记录在内存专用单元。
三、进程控制
进程控制是进程管理中最基本的功能,主要包括进程创建、终止、阻塞、状态转换等。
1.操作系统内核
现代操作系统一般将OS划分为若干层次,并将不同功能设置在不同层次中。一些关键模块(与硬件紧密相关、驱动、运行频率较高的模块)会设置在紧靠硬件的软件层次中,即OS内核。
相对应的是,为防止OS及其关键数据被破坏,将CPU状态分为系统态和用户态。
- 系统态:内核态,较高特权,可执行一切指令,访问寄存器与内存区,传统OS态
- 用户态:相反,应用程序态(一般情况)
大多数OS都包含支撑功能和资源管理功能。
1.1 支撑功能
提供给OS与其他模块的基本功能,三种基本的支撑功能如下:
- 中断处理:操作系统赖以活动的基础。
- 时钟管理:时间片的管理与控制。
- 原语操作:原语即若干条指令用于完成一定功能的一个过程,是“原子操作”,即不可分割的基本单位,在系统态下执行,不允许被中断。
1.2 资源管理功能
一般包含进程管理、存储器管理、设备管理三个功能。
2.进程创建
OS中,允许一个进程(父进程)创建另一个进程(子进程),如此反复形成进程家族(组)。
Tips:子进程可以继承父进程所拥有的资源(打开的文件、分配到的缓冲区等),被撤销时要归还。为了标识子父关系,PCB中设置了家族关系表项。Windows是没有进程层次结构的,所有进程地位相同,但创建新进程的进程获得句柄(可以传递),可以控制其他进程。
为了清晰的描述进程组的关系引入进程图(树),样例如下:
引起进程创建的典型事件有四类:
系统创建:(1)用户登录 (2) 作业调度 (3) 提供服务
用户创建:(4) 应用请求
而进程的创建流程如下:
(1) 申请空白PCB。
(2) 为新进程分配资源。
(3) 初始化进程控制块。
(4) 将新进程插入就绪队列,如果进程就绪队列能够接纳新进程,便将新进程插入就绪队列。
3.进程终止
引起进程终止的典型事件有三类:
1) 正常结束 通常有指令或信息告知OS结束,引起中断。
2) 异常结束 在进程运行期间,由于出现某些错误和故障而迫使进程终止。这类异常事件很多,常见的有:① 越界错误。这是指程序所访问的存储区,已越出该进程的区域; ② 保护错。进程试图去访问一个不允许访问的资源或文件,或者以不适当的方式进行访问,例如,进程试图去写一个只读文件; ③ 非法指令。程序试图去执行一条不存在的指令。出现该错误的原因,可能是程序错误地转移到数据区,把数据当成了指令;④ 特权指令错。用户进程试图去执行一条只允许OS执行的指令; ⑤ 运行超时。进程的执行时间超过了指定的最大值; ⑥ 等待超时。进程等待某事件的时间, 超过了规定的最大值;⑦ 算术运算错。进程试图去执行一个被禁止的运算,例如,被0除;⑧ I/O故障。这是指在I/O过程中发生了错误等。
3) 外界干预 指进程因外界的请求而终止运行。这些干预有: ① 操作员或操作系统干预。 由于某种原因,例如,发生了死锁, 由操作员或操作系统终止该进程; ② 父进程请求。 由于父进程具有终止自己的任何子孙进程的权利, 因而当父进程提出请求时,系统将终止该进程; ③ 父进程终止。 当父进程终止时,OS也将他的所有子孙进程终止。
而终止进程的流程如下:
(1) 根据被终止进程的标识符,从PCB集合中检索出该进程的PCB,从中读出该进程的状态。
(2) 若被终止进程正处于执行状态,应立即终止该进程的执行,并置调度标志为真,用于指示该进程被终止后应重新进行调度。
(3) 若该进程还有子孙进程,还应将其所有子孙进程予以终止,以防他们成为不可控的进程。
(4) 将被终止进程所拥有的全部资源,或者归还给其父进程, 或者归还给系统。
(5) 将被终止进程(它的PCB)从所在队列(或链表)中移出, 等待其他程序来搜集信息。
4.进程的阻塞与唤醒
引起进程阻塞与唤醒的典型事件有四类:1)请求系统服务 2) 启动某种操作 3) 新数据尚未到达 4) 无新工作可做
4.1 进程阻塞过程
正在执行的进程,当发现上述某事件时,由于无法继续执行,于是进程便通过调用阻塞原语block把自己阻塞(主动)。应先立即停止进程执行,把进程控制块中的现行状态由“执行”改为阻塞,并插入阻塞队列。如果系统中设置了因不同事件而阻塞的多个阻塞队列,则应将本进程插入到具有相同事件的阻塞(等待)队列。 最后,转调度程序进行重新调度,将处理机分配给另一就绪进程,并进行切换,亦即,保留被阻塞进程的处理机状态(在PCB中),再按新进程的PCB中的处理机状态设置CPU的环境。
4.2 进程唤醒过程
当被阻塞进程所期待的事件出现时,如I/O完成或其所期待的数据已经到达,则由有关进程(比如,用完并释放了该I/O设备的进程)调用唤醒原语wakeup,将等待该事件的进程唤醒。唤醒原语执行的过程是:首先把被阻塞的进程从等待该事件的阻塞队列中移出,将其PCB中的现行状态由阻塞改为就绪,然后再将该PCB插入到就绪队列中。
5.进程的挂起与激活
5.1 进程挂起
引起挂起的事件如下:(1)终端用户的请求、(2)父进程的请求、(3)负荷调节的需要、(4)操作系统的需要、(5)对换的需要
当出现了引起进程挂起的事件时,系统将利用挂起原语suspend将指定进程或处于阻塞状态的进程挂起。挂起原语的执行过程是:首先检查被挂起进程的状态,活动就绪->静止就绪;活动阻塞->静止阻塞。 为了方便用户或父进程考查该进程的运行情况而把该进程的PCB复制到某指定的内存区域。最后,若被挂起的进程正在执行,则转向调度程序重新调度。
5.2 进程激活
当发生激活进程的事件时,系统将利用激活原语active将指定进程激活。 激活原语先将进程从外存调入内存,检查该进程的现行状态,静止就绪->活动就绪;静止阻塞->活动阻塞。假如采用的是抢占调度策略,则每当有新进程进入就绪队列时,应检查是否要进行重新调度,即由调度程序将被激活进程与当前进程进行优先级的比较,如果被激活进程的优先级更低,就不必重新调度;否则,立即剥夺当前进程的运行,把处理机分配给刚被激活的进程。
接下来看两道例题:
为使进程由活动就绪转变为静止就绪,应利用__⑴__原语;为使进程由执行状态变为阻塞状态,应利用__⑵__原语;为使进程由静止就绪变为活动就绪,应利用__⑶__原语;从阻塞状态变为就绪状态应利用__⑷__原语。
⑴:suspend
⑵:block
⑶:active
⑷:wakeup
正在执行的进程由于时间片用完而被暂停执行,此时进程应从执行状态变为__⑴__状态;处于静止阻塞状态的进程,在进程等待事件出现后,应转变为__⑵__状态;若进程正处于执行状态时,应终端的请求而暂停下来以便研究其运行情况,这时进程应转变为__⑶__状态,若进程已处于阻塞状态,则此时应转变为__⑷__状态。
⑴:活动就绪
⑵:静止就绪
⑶:静止就绪
⑷:静止阻塞
6.进程控制原语补充 Linux/UNIX
1.fork:创建新的子进程 pid=int fork()
2.exec:系统调用
3.exit:进程执行终止
4.wait:等待子进程暂停或终止
创建子进程的C语言样例如下:
#include <stdio.h>
void main()
{ int pid;
pid=fork(); /* fork child process */
if (pid<0){ fprintf(stderr, “Fork Failed”); exit(-1);
}
else if (pid==0) { execlp(“/bin/ls”,”ls”,NULL);
} /* child process */
else { wait(NULL);
printf(“child Complete”);
exit(0);
} /*parent process */
}
程序说明:该程序说明主进程创建了一个子程序后,二个进程并发执行的情况。
主进程在执行fork系统调用前是一个进程,执行fork系统调用后,系统中又增加了一个与原过程环境相同的子进程,它们执行程序中fork语句以后相同的程序,父和子进程中都有自己的变量pid,但它们的值不同,它是fork调用后的返回值,父进程的pid为大于0的值,它代表新创建子进程的标识符,而子进程的pid为0。这样父子进程执行相同一个程序,但却执行不同的程序段。子进程执行if(pid= = 0)以后的大括号内的程序,即execlp语句;而父进程执行else以后的大括号内的程序。
Tips:父子进程并发执行,执行序列任意。但由于父进程执行的第一条语句是wait(null),它表示父进程将挂起,直到该进程的一个子进程暂停或终止为止
四、进程同步
1.两种制约
由于进程的异步性,导致了结果的不可再现性,为防止这种结构,异步进程间受两种限制:
- 资源共享关系/间接相互制约:互相共享/竞争某些资源,面临三个控制问题:
1.互斥:指多个进程不能同时使用同一个资源;
2.死锁:指多个进程互不相让,都得不到足够的资源;
3.饥饿:指一个进程一直得不到资源(其他进程可能轮流占用资源)
- 相互合作关系/直接相互制约:某些并发步骤的同时有时间序列关系(例:输入、计算、打印三个程序段作为三个进程并发执行),也称同步关系
2.临界资源 - “生产者-消费者”问题
定义:一次只允许一个进程使用的资源称为临界资源,又称独享资源(例:硬件打印机、磁带机等)
以下以“生产者-消费者”问题来说明这一过程:
------------“生产者-消费者”问题------------
有一群生产者进程在生产产品,并将这些产品提供给消费者进程去消费。为使生产者进程与消费者进程能并发执行,在两者之间设置了一个具有n个缓冲区的缓冲池buffer,生产者进程将它所生产的产品放入一个缓冲区中; 消费者进程可从一个缓冲区中取走产品去消费。
尽管所有的生产者进程和消费者进程都是以异步方式运行的,但它们之间必须保持同步,即不允许消费者进程到一个空缓冲区去取产品;也不允许生产者进程向一个已装满产品且尚未被取走的缓冲区中投放产品。
(见书P53-P54)结论:应互斥访问临界资源!!!
3.临界区
综上,互斥访问临界资源的样例如下:
A: begin
Input data 1 form I/O 1 ;
Computer……;
Print results 1 by printer ; A临界区
end
B: begin
Input data 1 form I/O 2 ;
Computer……;
Print results 2 by printer ; B临界区
end
分析:A、B都需要打印机,不能直接限制A、B的顺序(无法保证并发),限制要尽可能少
解决方案:把各进程代码分解,把访问临界资源的那段代码(称为临界区)与其它段代码分割开来,只对各种进程进入自己的临界区加以限制,即各进程互斥地进入自己的临界区。在上述A、B两程序中我们分别把A和B的使用打印机的二段程序print result 1 by printer和print result 2 by printer 称为A和B进程使用打印机的临界区A和临界区B,进程A和B必须互斥地分别进入各自的临界区A和B。
临界区:每个进程中访问临界资源的代码
一个访问临界资源的循环进程如下:
While(TRUE)
{
- 进入区:检查预访问的临界资源,若未被访问则进入且设置它为正被访问的标志。
- 临界区:对临界资源操作。
- 退出区:将正被访问的标志改为未被访问。
- 剩余区:进程中除上述三个区外的代码
}
一个样例如下:
begin remainder section 1; 剩余区1
进入区
critical section ; 临界区
退出区
remainder section 2 ; 剩余区2
end
4.程序同步机制
进程同步:多个相关进程在执行次序上的协调称
进程同步机制:用于保证多个进程在执行次序上的协调关系
有以下四个同步机制准则:
(1)空闲让进:当无进程进入临界区时,相应的临界资源处于空闲状态,因而允许一个请求进入临界区的进程立即进入自己的临界区。
(2)忙则等待:当已有进程进入自己的临界区时,即相应的临界资源正被访问,因而其它试图进入临界区的进程必须等待,以保证进程互斥地访问临界资源。
(3)有限等待:对要求访问临界资源的进程,应保证进程能在有限时间进入临界区,以免陷入“饥饿”状态。
(4)让权等待:当进程不能进入自己的临界区时,应立即释放处理机,以免进程陷入忙等。
补充:软件方法可以解决进程互斥进入临界区的问题,但有一定难度,且局限性很大,一般使用硬件同步机制,如关中断、“Test-and-Set”指令实现互斥、利用Swap指令实现程序互斥。
5.信号量机制
信号量机制是一种进程同步工具,由Dijkstra提出。
5.1 整型信号量
最初信号量被定义为一个用于表示资源数目的整型S。
除初始化外,仅能通过两个标准的原子操作wait(S)和signal(S)来访问。这两个操作一直被分别称为P、V操作。 wait和signal操作可描述为:
wait(S): while S≤0 do no-op
S∶=S-1;
signal(S): S ∶=S+1;
Tips:这里的原子操作即某个进程修改某信号量的时候,其他进程不能修改(原子操作不能中断)
5.2 记录型信号量
整型信号量机制中的wait操作,只要是信号量S≤0, 就会不断地测试,存在忙等。记录型信号量机制做了优化。但在采取了“让权等待”的策略后,又会出现多个进程等待访问同一临界资源的情况。为此,在信号量机制中,除了需要一个用于代表资源数目的整型变量value外,还应增加一个进程链表指针list(L),链接上述的所有等待进程。它所包含的上述两个数据项可描述为:
type semaphore=record
value:integer;
L:list of process; //记录阻塞控制进程
end
相应地,wait(S)和signal(S)操作可描述为:
procedure wait(S)
var S: semaphore;
begin
S.value∶ =S.value-1;
if S.value<0 then block(S,L) //该类资源已分配完成,自我阻塞(勿忙等),让出CPU
end
procedure signal(S)
var S: semaphore;
begin
S.value∶ =S.value+1;
if S.value≤0 then wakeup(S,L); //如果增加了资源,仍有等待该资源的进程被阻塞,则唤醒它
end
Tips:若S->value = 1(初始化),此时信号量变为互斥信号量,用于进程互斥。
5.3 信号量类型
信号量按联系进程的关系分成二类:
(1)公用信号量(互斥信号量):它为一组需互斥共享临界资源的并发进程而设置,它代表永久性的共享的临界资源,每个进程均可对它施加P、V操作,即都可申请和释放该临界资源,其初始值置为1。
(2)专用信号量(同步信号量):它为一组需同步协作完成任务的并发进程而设置,它代表消耗性的专用资源,只有拥有该资源的进程才能对它施加P操作(即可申请资源),而由其合作进程对它施加V操作(即释放资源)。
信号量S(S.value)取值意义:
- >0 ;表示可供使用资源数
- =0 ;表示资源已被占用,无其它进程等待。
- <0(=-n) ;表示资源已被占用,还有n个进程因等待资源而阻塞。
一个使用信号量保护共享数据的样例如下:
5.4 AND型信号量
前面的信号量主要还是针对多个进程共享一个临界资源,但实际情况一般是多对多,一个例子如下:
在两个进程中都要包含两个对Dmutex和Emutex的操作(A、B两个进程都要访问共享数据D和E),即:
若进程A和B按下述次序交替执行wait操作:
Ⅰ、process A: wait(Dmutex); 于是Dmutex=0
Ⅱ、process B: wait(Emutex); 于是Emutex=0
Ⅲ、process A: wait(Emutex); 于是Emutex=-1 A阻塞
Ⅳ、process B: wait(Dmutex); 于是Dmutex=-1 B阻塞
至此,A、B进入死锁(无外力无法解脱僵持,都得不到想要的资源),程序间需要共享的资源越多,越容易发生死锁。
故引入AND型信号量,其同步机制的基本思想是:将进程在整个运行过程中需要的所有资源,一次性全部地分配给进程,待进程使用完后再一起释放。即,对若干个临界资源的分配,采取原子操作方式:要么全部分配到进程,要么一个也不分配。 这样就可避免上述死锁情况的发生。为此,在wait操作中,增加了一个“AND”条件,故称为AND同步,或称为同时wait操作, 即Swait(Simultaneous wait)定义如下:
Swait(S1, S2, …, Sn)
if Si≥1 and … and Sn≥1 then
for i∶ =1 to n do
Si∶=Si-1;
endfor
else
place the process in the waiting queue associated with the first Si found with Si<1, and set the program count of this process to the beginning of Swait operation
endif
Ssignal(S1, S2, …, Sn)
for i∶ =1 to n do
Si=Si+1;
Remove all the process waiting in the queue associated with Si into the ready queue.
endfor;
5.5 信号量集
在之前的信号量操作中,只能一次申请/释放一个单位的临界资源,比较低效,增加了死锁的概率, 且为了系统安全每次要去判断资源的数量。
故引入信号量集,在AND的基础上扩充,对于申请的所有资源以及每种资源的不同的需求量,在一次P、V原语中完全申请或释放。
范式:Swait(S,t,d) S为信号量(目前总量) t为一次最少分配多少 d为一次需求多少
Ssignal(S,d)意思同上
流程:循环进行判断->分配
Swait(S1, t1, d1, …, Sn, tn, dn)
if S1≥t1 and … and Sn≥tn then //ti为该资源分配下限值
for i∶=1 to n do
Si∶=Si-di //di为对该资源的需求值
endfor
else
Place the executing process in the waiting queue of the first Si with Si<ti and set its program counter to the beginning of the Swait Operation.
endif
Ssignal(S1, d1, …, Sn, dn)
for i∶=1 to n do
Si ∶=Si+di;
Remove all the process waiting in the queue associated with Si into the ready queue
endfor;
一般“信号量集”的几种特殊情况:
(1) Swait(S, d, d)。 此时在信号量集中只有一个信号量S, 但允许它每次申请d个资源,当现有资源数少于d时,不予分配。
(2) Swait(S, 1, 1)。 此时的信号量集已蜕化为一般的记录型信号量(S>1时)或互斥信号量(S=1时)。
(3) Swait(S, 1, 0)。这是一种很特殊且很有用的信号量操作。当S≥1时,允许多个进程进入某特定区;当S变为0后,将阻止任何进程进入特定区。换言之,它相当于一个可控开关。
6.信号量的应用
6.1 利用信号量实现进程互斥
只有当一种资源成为临界资源(有比较多),有可能被多个用户(进程)访问时才需要互斥信号量,哲学家进餐问题因为筷子虽然可以被多个人尝试拿,但是只有一个,所以不需要mutex。
核心:设置一个互斥信号量mutex,初始化为1,用wait(mutex)和signal(mutex)访问。
用信号量实现两个进程互斥的样例如下:
var mutex:=semaphore:=1 ;
begin
parbegin
A:begin B:begin
Input data 1 from I/0 1 ; Input data 2 from I/O 2 ;
Compute……; Compute……;
P(mutex) ;申请资源 P(mutex) ;申请资源
Print results1 by printer; Print results2 by printer;
(临界区A--使用资源) (临界区B--使用资源)
V(mutex) ;释放资源 V(mutex) ;释放资源
end end
parend
end
Tips:wait(mutex)和signal(mutex)应成对出现。
6.2 利用信号量实现前驱关系
我们以如下前驱图为例:
用信号量实现的代码如下:
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
6.3 利用信号量机制实现进程同步
一个例题(计算进程C->缓冲区Buffer->打印进程P)如下:
C和P两进程基本算法如下:
C:begin P: begin
repeat repeat
Compute next number ; remove from Buffer ;
add to Buffer ; print last number ;
until false until false
end end
Tips:为保证正常的并发执行,必须遵守顺序(C进P才拿,P拿C再进新)
为实现同步,需采用同步信号量。
(1)为了满足第一条同步规则:只有当C进程把数据送入Buffer后,P进程才能从Buffer中取出数据来打印。设置一个同步信号量full,它代表的消耗性的专用资源是缓冲器装满数据,这个资源只是后面动作(Remove from buffer)的进程(P进程)所拥有,P进程在动作前可以申请该资源,对它施加P操作,如条件满足P进程可从Buffer中取数,它的初值为0。而前面动作的进程(P进程的合作进程C)在动作(Add to buffer)完成后对full信号量施加V操作,即当C进程将数据存入Buffer后,即可释放该资源供P进程再使用。实现C和P两进程第一条同步规则的类PASCAL程序:
var: full:semaphore:=0 ;
begin
parbegin
C: begin
repeat
Compute next number ;
Add to buffer ;
V(full) ; //告知P有新数据
until false
end
P: begin
repeat
P(full) ; //有新数据才拿
remove from Buffer ;
Print last number ;
until false
end
parend
end
(2)为了满足第二条同步规则:只有当P进程从Buffer中取走数据后,C进程才能将新计算的数据再存入Buffer。设置另一个同步信号量empty,它代表的消耗性的专用资源是缓冲器空,这个资源只是后面动作(Add to buffer)的进程(C进程)所拥有,C进程在动作前可以申请该资源,对它施加P操作,如条件满足进程C可以申请该资源,它的初值为1 。而前面动作(Remove from buffer)的进程(C进程的合作进程P)在动作完成后对empty信号量施加V操作,即当P进程从Buffer中取走数据后,即可释放该资源供C进程再使用。
考虑两个同步关系的代码如下(主要看注释的四句,即信号量机制):
var: empty,full:semaphore:=1,0 ;
begin
parbegin
C: begin
repeat
Compute next number ;
P(empty) ;// 若数据已被读取过,将新数据放入buffer
Add to buffer ;
V(full) ;// 告知P有新数据
until false
end
P: begin
repeat
P(full) ;// 若有新数据,则拿
remove from Buffer ;
V(empty) ;// 告知C数据已读取
Print last number ;
until false
end
parend
end
同步的物理意义:V操作代表发送消息;P操作,代表测试消息是否到达。
习题如下:
问:有三个共行进程P、Q和R以及一对供存数据的缓冲BufI和BufO,P进程把数据输入BufI,R进程输出BufO中的数据。Q地把BufI中的数据变换后送入BufO,在上述假定之下,使三个进程实现最大并行性。试在下述类PASCAL程序中虚线位置分别填上信号量、信号量初值和P、V操作实现三个进程正确的并发执行。
类似上面例题的代码,只不过Q既要做后者也要做前者,信号量也有两组full和empty,代码如下:
Q:begin
repeat
P(full1)
Remove from BufI;
V(empty1)
transform;
P(empty2)
Add to BufO;
V(full2)
until false
end;
7.经典进程同步问题
归纳了一些进程并发执行的典型例子,常用于测试新同步机制可行性。
7.1 生产者-消费者问题
生产者-消费者问题是最著名的同步问题,它描述一组生产者(P1 ……Pm)向一组消费者(C1……Cq)提供消息。它们共享一个有界缓冲池(bounded buffer pool),生产者向其中投放消息,消费者从中取得消息,如下图所示。生产者-消费者问题是许多相互合作进程的一种抽象。
Tips:生产者之间、生产者与消费者之间、消费者之间都必须互斥使用缓冲池。所以必须设置互斥信号量mutex,它代表缓冲池资源,它的数值为1。
而与6、3例题类似(除了这里的buffer是临界资源),同步问题中顺序非常重要,得P传了C才能接,C得接了P才能传新的,所以也需要full信号量与empty信号量。
代码样例(使用记录型信号量)如下:
var mutex,empty,full:semaphore:=1,n,o ;
Buffer : array [0……n-1] of message ;
in, out : o……n-1:=0,0 ;
begin
parbegin
Pi: begin (i=1...m)
repeat
Produce a new message m ;
P (empty) ;申请empty资源
P (mutex) ;申请共享资源
Buffer[in]=m ;
in :=(in+1) mod n ;
V (mutex) ;释放共享资源
V (full) ; 释放full资源
until false
end
Cj: begin (j=1...q)
repeat
P (full) ; 申请full资源
P (mutex) ;申请共享资源
m := buffer[out] ;
out : = (out+1) mod n ;
V (mutex) ;释放共享资源
V (empty) ;释放empty资源
Consume message m ;
until false
end
parend
end
资源信号量 - full empty(先执行)、互斥信号量 - mutex(后执行)
Tips:这里的缓存为循环缓存,故改变条件为change :=(base+1) mod n ;缓冲池满为change_in((in+1) mod n )= out,in == out即为池空。
Tips:wait和signal要成对出现,资源信号量的对在不同程序中。
代码样例(使用AND信号量)如下:
Var mutex, empty, full:semaphore∶ =1, n, 0;
buffer:array[0, …, n-1] of item;
in out:integer∶ =0, 0;
begin
parbegin
producer:begin
repeat
…
produce an item in nextp;
…
Swait(empty, mutex);
buffer(in)∶ =nextp;
in∶ =(in+1)mod n;
Ssignal(mutex, full);
until false;
end
consumer:begi
repeat
Swait(full, mutex);
nextc∶ =buffer(out);
out∶ =(out+1) mod n;
Ssignal(mutex, empty);
consumer the item in nextc;
until false;
end
parend
end
用Swait(x,y)与Ssignal(x,y)来代替原本的双wait与双signal。
7.2 阅览室管理问题(生消变式)
- 阅览室有一批座位,可以坐下若干同学自习;有一张唯一的登记表,记录空座位数目
- 某一同学入室时,查看登记表;如果空座位数目>0,则座位数减一,并进入阅览室坐下自习;否则,在阅览室门口等待
- 当从阅览室中出来时,使登记表中空座位数加1
与“生产者-消费者”的唯一不同,座位仅需要一个信号量seat,声明就好(不需要full和empty)。
一个样例如下(50个座位):
var mutex, seat: semaphore := 1, 50 //定义变量
procedure roomin; //进门进程
begin
wait(seat);
wait(mutex);
登记,同学进入阅览室……;
signal(mutex);
:
end;
procedure roomout; //出门进程
begin
wait(mutex);
撤消登记,同学走出阅览室….;
signal(mutex);
signal(seat);
:
end;
7.3 哲学家进餐问题
五个哲学家共用一张圆桌,桌上有五个碗和五只筷子,他们交替思考和进餐。饥饿时试图取左右最靠近的两只筷子,只有拿到两只才进餐,进餐毕放下筷子继续思考。
先用记录型信号量解决问题:
筷子是临界资源,可用一个信号量(均初始化为1)表示一只筷子。
//第i位哲学家的活动
repeat
wait(chopstick[i]); // 拿左边的筷子
wait(chopstick[(i+1) mod 5]); //拿右边的筷子
…
eat;
…
signal(chopstick[i]);
signal(chopstick[(i+1) mod 5]);
…
think;
until false;
以上代码不会使两个哲学家同时进餐,但可能引起死锁(五个人都拿到了左边筷子)。有以下几种解决方案:
(1) 至多只允许有四位哲学家同时去拿左边的筷子,最终能保证至少有一位哲学家能够进餐,并在用毕时能释放出他用过的两只筷子,从而使更多的哲学家能够进餐。
(2) 仅当哲学家的左、右两只筷子均可用时,才允许他拿起筷子进餐。
(3) 规定奇数号哲学家先拿他左边的筷子,然后再去拿右边的筷子;而偶数号哲学家则相反。按此规定,将是1、 2号哲学家竞争1号筷子;3、4号哲学家竞争3号筷子。即五位哲学家都先竞争奇数号筷子,获得后,再去竞争偶数号筷子,最后总会有一位哲学家能获得两只筷子而进餐。
再来到AND信号量解决哲学家进餐问题(最简洁的解法),本质上就是第(2)种解决方案:
Var chopsiick array [0, …, 4] of semaphore∶ =(1,1,1,1,1);
processi
repeat
think;
Sswait(chopstick[(i+1) mod 5], chopstick [i]);
eat;
Ssignat(chopstick [(i+1) mod 5], chopstick [i]);
until false;
7.4 读者-写者问题
一个数据文件或记录可被多个进程共享,我们把只要求读该文件的进程称为“Reader”,其他进程称为“Writer”。读者-写者问题本质上即保证一个Writer进程必须与其他进程互斥(读操作不会改变数据,写操作会)。
首先利用记录型信号量解决,核心如下:
Readcount - 正在读的进程数目 Wmutex - Writer进程互斥信号量
rmutex - Reader进程互斥信号量(由于Readcount可被多个进程读)
详细解析:为实现Reader与Writer进程间在读或写时的互斥而设置了一个互斥信号量Wmutex。另外,再设置一个整型变量Readcount表示正在读的进程数目。由于只要有一个Reader进程在读,便不允许Writer进程去写。因此,仅当Readcount=0, 表示尚无Reader进程在读时,Reader进程才需要执行Wait(Wmutex)操作。若wait(Wmutex)操作成功,Reader进程便可去读,相应地,做Readcount+1操作。同理,仅当Reader进程在执行了Readcount减1操作后其值为0时,才须执行signal(Wmutex)操作,以便让Writer进程写。又因为Readcount是一个可被多个Reader进程访问的临界资源,因此,应该为它设置一个互斥信号量rmutex。
var rmutex, wmutex: semaphore := 1, 1 ; 定义变量
readcount: integer: =0;
procedure reader; 读者进程
begin
repeat
wait(rmutex);
if readcount = 0 then wait(wmutex); // then即满足条件执行,这里即有读进程不让写所以wait让其-1
readcount := readcount + 1;
signal(rmutex);
执行读操作
wait(rmutex);
readcount := readcount - 1;
if readcount = 0 then signal(wmutex);
signal(rmutex);
until false;
end;
procedure writer; 写者进程
begin
repeat
wait(wmutex);
执行写操作;
signal(wmutex);
until false;
end;
在这里加上一个限制,最多只允许RN个读者同时读,所以引入信号量集机制解决。
核心:第RN+1读者进入时,因为Swait(L, 1, 1);被阻塞。
var L, mx: semaphore := RN, 1 ; 定义变量
procedure reader; 读者进程
begin
repeat
Swait(L, 1, 1);
Swait(mx, 1, 0);
执行读操作
Ssignal(L, 1);
until false;
end;
procedure writer; 写者进程
begin
repeat
Swait(mx, 1, 1; L, RN, 0);
执行写操作;
Ssignal(mx, 1);
until false;
end;
分析:Swait(mx, 1, 0);是开关,判断mx是否等于1(有无writer)由其确定是否读。Swait(mx, 1, 1; L, RN, 0);是双判断,既无writer在写(mx>=1)又无reader在读(L >= RN),才让写进程进入临界区。
Todo:写者优先
7.5 过独木桥问题(读写变式)
有一座南北向独木桥,如果桥上有一个人,则同向的人可以上桥并通过,反向的人只能等待。
过独木桥问题实际上是两类(读者)进程同时访问共享对象的问题,同种类的(读者)进程可以同时操作(读),但不同种类的(读者)进程不能同时操作(读),实际样例如下:
三峡大坝航运问题
巴拿马运河航运问题
一个实际样例通过代码给出:
//因为sCount和nCount都被多程序共享,故访问/改变前要使用smutex和nmutex
var mutex, smutex, nmutex: semaphore := 1, 1, 1 ; 定义变量(桥信息量,南桥信息量,北桥信息量)
sCount, nCount: integer: =0;
procedure SouthPassenger; //南桥头行人进程
begin
wait(smutex);
if sCount = 0 then wait(mutex); //得到过桥权
sCount := sCount + 1;
signal(smutex);
向北,过桥……
wait(smutex);
sCount := sCount - 1;
if sCount = 0 then signal(mutex); //释放过桥权
signal(smutex);
end;
procedure NorthPassenger; //北桥头行人进程
begin
wait(nmutex);
if nCount = 0 then wait(mutex); //得到过桥权
nCount := nCount + 1;
signal(nmutex);
向南,过桥……
wait(nmutex);
nCount := nCount - 1;
if nCount = 0 then signal(mutex); //释放过桥权
signal(nmutex);
end;
8.管程机制
信号量机制虽方便,但大部分同步操作分散在各进程中,且可能因为操作不当导致死锁,故引入另一种进程同步工具——管程。
8.1 定义
一个管程定义了一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据
管程实际上是一种能实现进程同步的特殊的子程序(函数、过程)的集合
一个管程组成如下:
- 名称:该管程的标识
- 共享变量说明:局部于管程的变量说明(包括特殊同步变量即条件变量)
- 一组过程:对该数据结构进行操作的程序段(相当于临界区代码段)
- 初始化:对局部于管程的数据设置初始值
语法结构如下:
Type 管程名称 = monitor
共享变量说明语句
procedure entry P1(…)
begin … end;
procedure entry P1(…)
begin … end;
:
begin
初始化语句
end;
而管程实现进程同步的核心如下:
进程的互斥访问:
- 进程访问临界资源,必须经过管程
- 管程每次只允许一个进程进入
进程的同步:
- 设置条件变量(相当于互斥信号量) var x: condition
- 当临界资源已被占用,执行x.wait,将进程挂在x条件变量的阻塞等待队列中
- 当临界资源已空闲,执行x.signal,从x条件变量的阻塞等待队列中,唤醒第一个进程
8.2 样例(生产者-消费问题)
1、过程:
- put(item)过程 生产者将产品放入缓冲区
- get(item)过程 消费者从缓冲区取得一个产品
2、变量:
- 整型量in, out:初值均为0,in指示首空缓冲块序号,out指示首满缓冲块序号
- Buffer[0…n-1]:缓冲区
- 整形量count:满缓冲块数目
- 条件变量notfull, notempty:指示缓冲区已满、已空(管程特有)
3、管程描述
Type PC = monitor
var in, out, count: integer;
buffer: array[0, …, n-1] of item;
notfull, notempty: condition;
procedure entry put(var nextp: item)
begin
if count >= n then notfull.wait //缓冲区已满 生产者需等待
buffer(in) := nextp;
in := (in + 1) mod n;
count := count + 1;
if notempty.queue then notempty.signal //队列不为空选择一个唤醒
end;
procedure entry get(var nextc: item)
begin
if count <= 0 then notempty.wait //缓冲区为空 消费者需等待
nextc := buffer(in);
out := (out + 1) mod n;
count := count - 1;
if notfull.queue then notfull.signal //队列不为空选择一个唤醒
end;
begin
in :=0; out := 0; count := 0;
end;
4、进程描述:
procedure producer; 生产者进程
begin
repeat
生产一个产品nextp;
PC.put(nextp);
until false;
end;
procedure consumer; 消费者进程
begin
repeat
PC.get(nextc);
消费一个产品nextc;
until false;
end;
管程优势:
- 使用临界资源的进程进行调用时非常简单
- 进程结构清晰
- 易于查错
五、进程通信
进程通信指进程之间的信息交换,而互斥与同步属于低级通信,效率低且对用户不透明,故需要其他高级通信工具/方式。
1.进程通信类型
1.1 共享存储器系统
共享存储器系统一般基于共享数据结构与共享存储区两种。
Ⅰ、基于共享数据结构通信
利用共享数据结构,如生产者-消费者中buffer,低效 - 适用于少量数据传输。
Ⅱ、基于共享存储区通信
UNIX系统中通信速度最高的机制,为了传送大量数据,在存储区中划出一块存储区,供多个进程共享。
进程A和B在建立了一个共享存储区并获得了其描述符后,还需分别利用系统调用shmat将共享存储区附接到各自进程的虚地址空间上,如下图所示。此后共享存储区便成为进程虚地址空间的一部分,进程可采取与其它虚地址空间一样的存取方法来存取它。
进程A和B利用共享存储区进行通信时,需自己设置进程同步机制,才能保证实现正确的通信。 进程A和B在利用共享存储区进行通信时,还可利用系统调用shmctl对共享存储区的状态信息(如长度,当前连接的进程数等)进行读取,也可设置和改变共享存储区的属性(如共享存储区的许可权等)。最后仍可利用系统调用shmctl断开进程与共享存储区的连接。当所有进程都断开了与共享存储区的连接时,便可取消共享存储区。
1.2 消息传递系统
消息传递机制是用得最广泛的一种进程间通信的机制。进程间的数据交换,是以格式化的消息(message)为单位的。程序员直接利用系统提供的一组通信命令(原语)进行通信。操作系统隐藏了通信的实现细节,大大降低通信程序编制的复杂性。消息传递系统的通信方式属于高级通信方式。分成直接通信方式和间接通信方式两种:
Ⅰ、直接通信
利用OS原语直接发送
Ⅱ、间接通信
通过共享中间实体(邮箱)进行发送与接收。
1.3 管道(Pipe)通信
“管道”,是指用于连接读进程和写进程以实现他们之间通信的一个共享文件。向管道提供输入的发送进程, 以字符流形式将大量的数据送入管道;而接受管道输出的接收进程,则从管道中接收(读)数据。
#include <stdio.h>
main()
{
int x,fd[2];
char buf[30],s[30];
pipe(fd); /*创建管道*/
while((x=fork()) == -1); /*创建子进程失败时,循环*/
if(x == 0)
{
sprintf(buf, "This is an example\n");
write(fd[1],buf,30); /*把buf中字符写入管道*/
exit(0);
}
else {
wait(0);
read(fd[0],s,30); /*父进程读管道中字符*/
printf("%s",s);
}
}
为了协调双方通信,管道机制必须有以下三方面协调能力:
①互斥: 一个进程正在对pipe执行读/写操作时,其它(另一)进程必须等待。
②同步:当写进程把一定量的数据写入pipe,便去睡眠等待, 直到读进程取走数据后,再把他唤醒。当读进程读一空pipe时,也应睡眠等待,直至写进程将数据写入管道后,才将之唤醒。
③存在性检验:只有确定了对方已存在时,才能进行通信。
1.4 客户机 - 服务器系统
当前主流的通信实现机制,主要分为三类:套接字、远程过程调用、远程方法调用
2.消息传递通信实现
2.1 直接通信 - 直接消息传递
这是指发送进程利用OS所提供的发送命令,直接把消息发送给目标进程。此时,要求发送进程和接收进程都以显式方式提供对方的标识符。通常,系统提供下述两条通信命令(原语):
- Send(Receiver, message); 发送一个消息给接收进程;
- Receive(Sender, message); 接收Sender发来的消息;
这是对称寻址方式,但一旦一个进程名修改了,会影响很多通信,不利于模块化。
某些情况下,接收进程可与多个发送进程通信,因此,它不可能事先指定发送进程。例如,用于提供打印服务的进程,它可以接收来自任何一个进程的“打印请求”消息。对于这样的应用,在接收进程接收原语中的源进程参数,是完成通信后的返回值,接收原语可表示为:
- Receive (id, message);
Tips:id可设置为进行通信的发送方进程或名字,这就是非对称寻址方式。
我们可以用直接通信原语,解决”生产者-消费者“问题
repeat
…
produce an item in nextp;
…
send(consumer, nextp);
until false;
repeat
receive(producer, nextc);
…
consume the item in nextc;
until false;
2.2 间接通信 - 信箱通信
(1) 信箱的创建和撤消。进程可利用信箱创建原语来建立一个新信箱。创建者进程应给出信箱名字、信箱属性(公用、私用或共享);对于共享信箱, 还应给出共享者的名字。当进程不再需要读信箱时,可用信箱撤消原语将之撤消。
(2) 消息的发送和接收。当进程之间要利用信箱进行通信时,必须使用共享信箱,并利用系统提供的下述通信原语进行通信。
- Send(mailbox, message); 将一个消息发送到指定信箱;
- Receive(mailbox, message); 从指定信箱中接收一个消息;
信箱可由系统或用户进程创建(拥有者),可分为以下三类:
Ⅰ、私用信箱
用户进程可为自己建立一个新信箱,并作为该进程的一部分。信箱的拥有者有权从信箱中读取消息,其他用户则只能将自己构成的消息发送到该信箱中。这种私用信箱可采用单向通信链路的信箱来实现。 当拥有该信箱的进程结束时,信箱也随之消失。
Ⅱ、公用信箱
它由操作系统创建,并提供给系统中的所有核准进程使用(可发可接)。显然,公用信箱应采用双向通信链路的信箱来实现。通常,公用信箱在系统运行期间始终存在。
Ⅲ、共享信箱
它由某进程创建,在创建时或创建后,指明它是可共享的,同时须指出共享进程(用户)的名字。信箱的拥有者和共享者,都有权从信箱中取走发送给自己的消息。
在利用信箱通信时,在发送进程和接收进程之间,存在以下四种关系:
(1) 一对一关系。这时可为发送进程和接收进程建立一条两者专用的通信链路,使两者之间的交互不受其他进程的干扰。
(2) 多对一关系。允许提供服务的进程与多个用户进程之间进行交互,也称为客户/服务器交互。
(3) 一对多关系。允许一个发送进程与多个接收进程进行交互,使发送进程可用广播方式,向接收者(多个)发送消息。
(4) 多对多关系。允许建立一个公用信箱,让多个进程都能向信箱中投递消息;也可从信箱中取走属于自己的消息。
3.消息传递系统实现中的若干问题
3.1 通信链路
为使在发送进程和接收进程之间能进行通信,必须在两者之间建立一条通信链路。第一种方式是:由发送进程在通信之前,用显式的“建立连接”命令(原语)建立与拆除。
第二种方式是发送进程无须明确提出建立链路的请求,只须利用系统提供的发送命令(原语),系统会自动地为之建立一条链路。这种方式主要用于单机系统中。
根据通信链路的连接方法,又可把通信链路分为两类:
① 点—点连接通信链路,这时的一条链路只连接两个结点(进程);
② 多点连接链路,指用一条链路连接多个(n>2)结点(进程)。
而根据通信方式的不同,则又可把链路分成两种:
① 单向通信链路,只允许发送进程向接收进程发送消息;
② 双向链路,既允许由进程A向进程B发送消息,也允许进程B同时向进程A发送消息。
3.2 消息的格式
在某些OS中,消息是采用比较短的定长消息格式,这减少了对消息的处理和存储开销。这种方式可用于办公自动化系统中,为用户提供快速的便笺式通信;但这对要发送较长消息的用户是不方便的。在有的OS中,采用另一种变长的消息格式,即进程所发送消息的长度是可变的。系统在处理和存储变长消息时,须付出更多的开销,但方便了用户。 这两种消息格式各有其优缺点,故在很多系统(包括计算机网络)中,是同时都用的。
3.3 进程同步方式
无论发送/接收进程,完成消息发送/接收后,都存在进行发送/接收或阻塞两种可能。
(1) 发送进程阻塞、 接收进程阻塞。
(2) 发送进程不阻塞、 接收进程阻塞。
(3) 发送进程和接收进程均不阻塞。
3.4 利用消息传递实现互斥
采用发送进程不阻塞、 接收进程阻塞
多个并发执行的发送进程和接收进程共享一个邮箱mutex,它被初始化为一个无内容的“空消息”
如果一个进程希望进入临界区,首先必须申请从mutex邮箱接收一条消息。若邮箱为空,则该进程阻塞;若收到,则进入临界区,执行完毕,退出临界区,并将该消息发送回邮箱。
Program mutualexclusion
Const n=…;//进程数
Procedure P(i:integer)
Var msg:message
Begin
Repeat
receive(mutex,msg)
<临界区>
Send(mutex,msg)
Forever
End
Begin/*主程序*/
Create_mailbox(mutex);
Send(mutex,null);
Parbegin
P(1);
P(2);
….
P(n)
Parend
end
4.直接消息传递系统实例
核心:消息缓存队列、发送原语、接收原语
4.1 消息缓冲队列通信机制
(1) 原理:由系统管理一组缓冲区,每个缓冲区可以存放一个消息。当发送进程要发送消息时先要向系统申请一个缓冲区,然后把消息写进去,接着把该缓冲区连接到接收进程的消息缓冲队列中。接收进程可以在适当的时候从消息缓冲队列中摘下消息缓冲区,读取消息,并释放该缓冲区。
(2) 数据结构:消息缓冲区和进程PCB中有关通信的扩充数据项:
//消息缓冲区
type message buffer=record
sender ;发送进程的标识符
size ;消息长度
text ;消息正文
next ;指向下一个消息缓冲区的指针
end
PCB增加的数据项如下(对消息队列进行操作和实现同步的信号量):
type processcontrol block=record
mq; 消息队列队首指针
mutex; 消息队列互斥信号量
sm; 消息队列资源信号量
end
4.2 发送原语
发送进程在利用发送原语发送消息之前,应先在自己的内存空间,设置一发送区a,见图 2 - 12 所示,把待发送的消息正文、发送进程标识符、消息长度等信息填入其中,然后调用发送原语,把消息发送给目标(接收)进程。
发送原语首先根据发送区a中所设置的消息长度a.size来申请一缓冲区i,接着,把发送区a中的信息复制到缓冲区i中。为了能将i挂在接收进程的消息队列mq上,应先获得接收进程的内部标识符j,然后将i挂在j.mq上。由于该队列属于临界资源, 故在执行insert操作的前后,都要执行wait和signal操作。
procedure send(receiver, a)
begin
getbuf(a.size,i); 根据a.size申请缓冲区;
i.sender∶ =a.sender; 将发送区a中的信息复制到消息缓冲区之中;
i.size∶ =a.size;
i.text∶ =a.text;
i.next∶ =0;
getid(PCB set, receiver.j); 获得接收进程内部标识符;
wait(j.mutex);
insert(j.mq, i); 将消息缓冲区插入消息队列;
signal(j.mutex);
signal(j.sm);
end
4.3 接收原语
procedure receive(b)
begin
j∶ =internal name; j为接收进程内部的标识符;
wait(j.sm);
wait(j.mutex);
remove(j.mq, i); 将消息队列中第一个消息移出;
signal(j.mutex);
b.sender∶ =i.sender; 将消息缓冲区i中的信息复制到接收区b;
b.size∶ =i.size;
b.text∶ =i.text;
end
一个系统地址空间的样例如图(消息队列中有两个消息):
六、线程
在之前一直以进程作为拥有资源且独立调度(运行)的基本单位,但后续发现了更小的调度与分配的基本单位——线程,可以提高程序并发执行的程度。
1.概述
线程的基本属性为:(1)轻型实体。 (2) 独立调度和分派的基本单位。 (3) 可并发执行。 (4) 共享进程资源。
线程与进程类似,每一个线程都可以利用线程标识符和一组状态参数进行描述,通过线程控制块TCB来控制与管理相关信息。运行状态上也是三大状态(执行、就绪、阻塞),运行时也具有间断性。
在创建新线程时,需要利用一个线程创建函数(或系统调用),并提供相应的参数,一般会有一个“初始化线程”在程序启动时被创建。终止进程的方式有(1)完成工作(2)遇到错误或某些原因
在多线程OS中,进程是作为拥有系统资源的基本单位,但不是可执行的实体。
2.线程间同步与通信
1、互斥锁:
互斥锁可以有两种状态, 即开锁(unlock)和关锁(lock)状态。 相应地,可用两条命令(函数)对互斥锁进行操作。其中的关锁lock操作用于将mutex关上,开锁操作unlock则用于打开mutex。
2、条件变量:
每一个条件变量通常都与一个互斥锁一起使用。单纯的互斥锁用于短期锁定,主要是用来保证对临界区的互斥进入。而条件变量则用于线程的长期等待(控制线程是否等待), 直至所等待的资源成为可用的。
线程首先对mutex执行关锁操作,若成功便进入临界区,然后查找用于描述资源状态的数据结构,以了解资源的情况。 只要发现所需资源R正处于忙碌状态,线程便转为等待状态,并对mutex执行开锁操作后,等待该资源被释放; 若资源处于空闲状态,表明线程可以使用该资源,于是将该资源设置为忙碌状态,再对mutex执行开锁操作。
Lock mutex Lock mutex
check data structures; mark resource as free;
while( resource busy) unlock mutex;
wait(condition variable); wakeup(condition variable);
mark resource as busy;
unlock mutex;
假设有共享的资源sum,与之相关联的mutex 是lock_s.假设每个线程对sum的操作很简单的,与sum的状态无关,比如只是sum++.那么只用mutex足够了.程序员只要确保每个线程操作前,取得lock,然后sum++,再unlock即可.每个线程的代码将像这样 add()
{ pthread_mutex_lock(lock_s); sum++; pthread_mutex_unlock(lock_s); }
线程t3则是在sum到达100的时候,打印出一条信息,并对sum清零.这种情况下,如果只用mutex, 则t3需要一个循环,每个循环里先取得lock_s,然后检查sum的状态,如果sum>=100,则打印并清零,然后unlock.如果sum<100,则unlock,并sleep()本线程合适的一段时间.这个时候,t0,t1,t2的代码不变,t3的代码如下:
Problems:
(1)sum在大多数情况下不会到达100,那么对t3的代码来说,大多数情况下,走的是else分支,只是lock和unlock,然后sleep().这浪费了CPU处理时间.
(2)为了节省CPU处理时间,t3会在探测到sum没到达100的时候sleep()一段时间.这样却又带来另外一个问题,亦即t3响应速度下降.可能在sum到达200的时候,t3才会醒过来.
Solutions: 条件变量!
3、信号量机制:
与进程一致,有两类(公用、私用)信号量。
3.内核支持线程和用户级线程
内核支持线程和用户级线程的区分及运行的位置与创建方式,一个来自系统,一个来自用户。
对于内核支持线程的实现,系统在创建一个新进程时,分配一个任务数据区空间。
对于用户级线程,有运行时系统和内核控制线程两种。
1、 运行时系统:
所谓“运行时系统”,实质上是用于管理和控制线程的函数(过程)的集合, 其中包括用于创建和撤消线程的函数、 线程同步和通信的函数以及实现线程调度的函数等。正因为有这些函数,才能使用户级线程与内核无关。运行时系统中的所有函数都驻留在用户空间,并作为用户级线程与内核之间的接口。
2、内核控制线程:
这种线程又称为轻型进程LWP(Light Weight Process)。 每一个进程都可拥有多个LWP, 同用户级线程一样, 每个LWP都有自己的数据结构(如TCB),其中包括线程标识符、优先级、 状态, 另外还有栈和局部存储区等。 它们也可以共享进程所拥有的资源。LWP可通过系统调用来获得内核提供的服务,这样,当一个用户级线程运行时,只要将它连接到一个LWP上,此时它便具有了内核支持线程的所有属性。
七、一些例题
最后用一些例题来结束第二章吧~
如图,试用信号量实现这6个进程的同步
Tips:同一个进程到不同的进程应用不同的信号量。
用P.V操作解决司机与售票员的问题
Tips:这是一个同步问题,没有资源的竞争,主要注意执行顺序。关门->开车,停车->开门
桌上有一空盘,最多允许存放一只水果。爸爸可向盘中放一个苹果或放一个桔子,儿子专等吃盘中的桔子,女儿专等吃苹果。
试用P、V操作实现爸爸、儿子、女儿三个并发进程的同步。
核心: 设置三个信号量S,So,Sa ,初值分别为1,0,0。分别表示可否向盘中放水果,可否取桔子,可否取苹果。