即一个有向无循环图,用于描述进程之间执行的先后顺序。
初始结点:指没有前驱的结点。
终止节点:没有后继的结点。
注意:前趋图中不允许有循环,否则必然存在不可能实现的前趋关系!
通常,一个程序由若干个程序段组成,每一个程序段完成特定的功能,它们在执行时都需要按照某种先后次序顺序执行,仅当前一程序段执行完之后,才执行后一程序段。
程序顺序执行的特征:
指两个或多个程序或程序段在同一时间间隔内执行。
只有不存在前趋关系的程序之间才有可能并发执行,否则无法并发执行!
如图:
图中仅S1与S2可以并发执行!
程序并发执行的特征:
为什么要引入进程?
答:在多道程序环境下,程序的并发执行破坏了程序的封闭性和可再现性,使得程序和计算不再一一对应,程序活动不再处于一个封闭系统中,程序的运行出现了许多新的特征。在这种情况下,程序这种静态概念已经不能如实地反映程序活动的这些特征,为此引入了一个新的概念—-进程。
什么是进程控制块(PCB)?
进程控制块是一个专门的数据结构,目的是使并发执行的每个程序都能独立的运行。
系统利用PCB来描述进程的基本情况和活动过程,进而控制和管理进程。
进程实体(进程映像):
由程序段、相关数据段、PCB三部分组成。
一般情况下。进程实体简称为进程,
进程的特征:
进程的三种基本状态:
创建状态和终止状态
如果系统资源不能得到满足,创建工作未完成,则把此时进程所处的状态称为创建状态。
进程进入终止状态,则不再执行。
进程管理中的数据结构:
为了便于对计算机中的各类资源(包括硬件和信息)的使用和管理,OS将它们抽象为相应的各种数据结构,以及提供一组对资源进行操作的命令,用户可利用这些数据结构及操作命令来执行相关的操作,而无需关心其实现的具体细节。
在计算机系统中,对于每个资源和每个进程都设置了一个数据结构,用于表征其实体,
称之为资源信息表或进程信息表,其中包含了资源或进程的标识、描述、状态等信息。
OS管理的数据结构一般有四种:内存表、设备表、文件表、进程表(PCB)
进程控制块中的信息:
进程控制是进程管理最基本的功能,主要包括:
进程控制一般是通过OS内核中的原语来实现的。
为了防止OS本身及其关键数据遭受到应用程序有意或者无意的破坏,通常将处理机的执行状态分为系统态和用户态两种:
① 系统态 (又称为管态、内核态)
具有较高的特权,能执行一切指令,访问所有寄存器和存储区,传统的OS都在系统态运行。
② 用户态 (目态)
具有较低特权的执行状态,仅能执行规定的指令,访问指定的寄存器和存储区。
一般应用程序仅能在用户态运行,这样可以防止应用程序对OS的破坏。
支撑功能:
注:原语是指由若干条指令组成,用于完成一定功能的一个过程。
(原语在执行过程中不允许被阻断)
资源管理功能
通常把创建进程的进程称为父进程,被创建的进程称为子进程。
引起创建进程的事件:
进程的创建:
创建原语(Create)
引起进程终止的事件:
进程的终止过程:
引起进程阻塞或被唤醒条件:
(参照上面进程转换图)
进程阻塞:
正在执行的进程,如果发生上述某事件,进程便通过调用阻塞原语Block,将自己阻塞起来。
阻塞是进程自身的一种主动行为!
进程唤醒:
当被阻塞进程所期待的事件发生时,比如它所启动的I/O操作已完成,或其所期待的数据已经到达,则由有关进程(比如提供数据的进程)调用唤醒原语wakeup,将等待该事件的进程唤醒。
注意:block原语和wakeup原语必须成对使用。
挂起操作:
为了系统和用户观察、分析进程的需要,由此引入挂起操作。
当该操作作用于某个进程时,该进程将被挂起,意味着此时进程处于静止状态。
挂起原语(Suspend)
激活原语(Active)
为什么要引入进程同步的概念?
答:在多道程序环境下,进程是并发执行的,不同进程之间存在着不同的相互制约关系。为了协调进程之间的相互制约关系,引入了进程同步的概念。
进程同步机制的主要任务:
对多个相关进程在执行次序上进行协调,使并发执行的诸进程之间能够按照一定的规则(或时序)共享系统资源,并能很好的相互合作,从而使程序的执行具有可再现性。
① 间接相互制约关系(互斥)
当一个进程进入临界区使用临界资源时,另一个进程必须等待,当占用临界资源的进程退出临界区后,另一进程才允许去访问此临界资源。
② 直接相互制约关系 (同步)
指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。进程间的直接制约关系源于它们之间的相互合作。
临界资源:一次仅允许一个进程使用的资源。如打印机、磁带机。
临界区:在每个进程中访问临界资源的那段代码称为临界区。
为禁止两个进程同时进入临界区,同步机制应遵循以下准则:
① 空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。
② 忙则等待。当已有进程进入临界区时,其他试图进入临界区的进程必须等待。
③ 有限等待。对请求访问的进程,应保证能在有限时间内进入临界区。
④ 让权等待。当进程不能进入临界区时,应立即释放处理器,防止进程忙等待。
软件实现方法:
硬件实现方法:
1965年荷兰学者Dijkstra提出了信号量机制(Semaphores)。信号量机制是一种功能较强的机制,可用来解决互斥与同步问题,它只能被两个标准的原语 wait(S) 和 signal(S) 访问,也可记为“Р操作”和“V操作”。
原语:是指完成某种功能且不被分割、不被中断执行的操作序列,通常可由硬件来实现。
整形信号量被定义为一个 表示资源数目的整型量S, wait 和signal操作可以描述为:
wait(S){
while(S<=0);
S--;
}
signal(S){
S++;
}
在整型信号量机制中的 wait 操作,只要信号量S≤0,就会不断地测试。因此,该机制并未遵循“让权等待”的准则,而是使进程处于“忙等”的状态。
AND同步机制的基本思想是:
将进程在整个运行过程中需要的所有资源,一次性全部地分配给进程,待进程使用完后再一起释放。只要尚有一个资源未能分配给进程,其它所有可能为之分配的资源也不分配给它。
亦即,对若干个临界资源的分配采取原子操作方式:要么把它所请求的资源全部分配到进程,要么一个也不分配。由死锁理论可知,这样就可避免死锁情况的发生。
设S为实现进程 P1,P2 同步的公共信号量,初值为 0。进程 P2 中的语句 y 要使用进程 P1 中语句 x 的运行结果,所以只有当语句 x 执行完成之后语句 y才可以执行。其实现进程同步的算法如下:
【wait(S) 相当于 P(S),signal(S) 相当于V(S) 】
semaphore S=0;
P1(){
x;
signal(S); //语句x已完成 ,S=1
...
}
P2(){
...
wait(S); //检查语句x是否完成,
y;
...
}
若 P2 先执行到 P(S) 时,S 为 0,执行 Р 操作会把进程 P2 阻塞,并放入阻塞队列;当进程 P1 中的 x 执行完后,执行V操作,把 P2 从阻塞队列中放回就绪队列,当 P2 得到处理机时,就得以继续执行。
为使多个进程能互斥的访问某临界资源, 设置一个互斥信号量mutex, 赋初值=1,然后将各进程访问该资源的临界区CS放在 wait(mutex) 和 signal(mutex) 之间即可。
设mutex=1,取值范围为(-1,0,1)。
当mutex = 1时,表示两个进程皆未进入需要互斥的临界区;
当mutex =0时,表示有一个进程进入临界区运行,另外一个必须等待,挂入阻塞队列;
当mutex =-1时,表示有一个进程正在临界区运行,另外一个进程因等待而阻塞在信号量队列中,需要被当前已在临界区运行的进程退出时唤醒。
semaphore mutex = 1;
PA(){
while(1){
wait(mutex); //准备访问临界资源,P操作 加锁
临界区
signal(mutex) //访问结束 V操作 解锁
剩余区
}
}
PB(){
while(1){
wait(mutex); //准备访问临界资源,P操作 加锁
临界区
signal(mutex) //访问结束 V操作 解锁
剩余区
}
}
总结一下PV操作在同步互斥中的应用:
为什么引入管程?
虽然信号量机制是一种既方便、又有效的进程同步机制,但每个要访问临界资源的进程都必须自备同步操作 wait(S)和 signal(S)。这就使大量的同步操作分散在各个进程中。这不仅给系统的管理带来了麻烦,而且还会因同步操作的使用不当而导致系统死锁。这样,在解决上述问题的过程中,便产生了一种新的进程同步工具—─管程(Monitors)。
管程实现:
利用共享数据结构抽象地表示系统中的共享资源,而把对该数据结构实施的操作定义为一组过程。进程对共享资源的申请、释放等操作,都通过这组过程来实现,这组过程还可以根据资源情况,或接受或阻塞进程的访问,确保每次仅有一个进程使用共享资源,这样就可以统一管理对共享资源的所有访问,实现进程互斥。
这个代表共享资源的数据结构,以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序,称为管程(monitor )。
管程定义了一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据。
由上述定义可知,管程由4部分组成:
①管程的名称;
②局部于管程内部的共享数据结构说明;
③对该数据结构进行操作的一组过程(或函数);
④对局部于管程内部的共享数据设置初始值的语句。
问题描述: 一组生产者进程和一组消费者进程共享一个初始为空、大小为n的缓冲区,只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待;只有缓冲区不空时,消费者才能从中取出产品,否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入产品,或一个消费者从中取出产品。
生产者-消费者问题是相互合作的进程关系的一种抽象,例如,在输入时,输入进程是生产者,计算进程是消费者;而在输出时,则计算进程是生产者,而打印进程是消费者。
利用记录型信号量解决生产者-消费者问题
信号量设置:
算法描述如下:
int in=0, out=0;
item buffer[n]; //缓冲池buffer,有n个缓冲区
semaphore mutex=1, empty=n, full=0;
void producer(){
do{
producer an item nextp;
...
wait(empty); //判断是否有空闲缓冲区,有则继续往下执行 empty-1,否则先将自己阻塞
wait(mutex); // 上锁 申请缓冲区资源
buffer[in]=nextp; //将产品放入一个缓冲区
in=(in+1)%n; //输入指针+1
signal(mutex); //解锁 释放缓冲区资源
signal(full); // 满缓冲区数+1
}while(true);
}
void consumer(){
do{
wait(full); //判断是否有满缓冲区,有则继续往下执行 full减一,否则先将自己阻塞
wait(mutex); //上锁 申请缓冲区资源
nextc=buffer[out]; //取产品
out=(out+1)%n; //输出指针+1
signal(mutex); //解锁 释放缓冲区资源
signal(empty); // 空缓冲区数+1
consumer the item in nextc;
...
}while(true);
}
void main(){
cobegin
//并发执行
producer(); consumer();
coend
}
/*in=out: 缓冲池空
(in+1)%n=out: 缓冲池满
*/
利用 AND 信号量解决生产者-消费者问题
算法描述如下:
int in=0, out=0;
item buffer[n]; //缓冲池buffer,有n个缓冲区
semaphore mutex=1, empty=n, full=0;
void producer(){
do{
producer an item nextp;
...
Swait(empty,mutex);
buffer[in]=nextp; //将产品放入一个缓冲区
in=(in+1)%n; //输入指针+1
Ssignal(mutex,full);
}while(true);
}
void consumer(){
do{
Swait(full,mutex);
nextc=buffer[out]; //取产品
out=(out+1)%n; //输出指针+1
Ssignal(mutex,empty); //解锁 释放缓冲区资源
consumer the item in nextc;
...
}while(true);
}
void main(){
cobegin
//并发执行
producer(); consumer();
coend
}
利用 管程 解决生产者-消费者问题:
首先建立一个管程procducerconsumer,其中包括两个过程:
(1) put(x)过程。生产者利用该过程将自己生产的产品投放到缓冲池中,并用整型变量count来表示在缓冲池中已有的产品数目,当count≥N时,表示缓冲池已满,生产者须等待。
(2) get(x)过程。消费者利用该过程从缓冲池中取出一个产品,当count≤0时,表示缓冲池中已无可取用的产品,消费者应等待。
对于条件变量notfull和notempty,分别有两个过程cwait和 csignal对它们进行操作:
(1) cwait(condition)过程:当管程被一个进程占用时,其他进程调用该过程时阻塞,并挂在条件condition的队列上。
(2) csignal(condition)过程:唤醒在cwait执行后阻塞在条件condition队列上的进程,如果这样的进程不止一个,则选择其中一个实施唤醒操作;如果队列为空,则无操作而返回。
算法描述如下:
Monitor procducerconsumer {
//对管程的定义
item buffer[N]; //缓冲池
int in, out;
condition notfull, notempty; //条件变量
int count; //缓冲池中已有产品数量
public:
void put(item x) {
//放产品
if (count>=N) cwait(notfull);
buffer[in] = x;
in = (in+1) % N;
count++;
csignal(notempty);
}
void get(item x){
//取产品
if (count<=O) cwait(notempty);
x = buffer[out];
out = (out+1) % N;
count--;
csignal(notfull);
}
{in=O;out=O;count=O;}
}PC;
void producer() {
item x;
while(TRUE){
...
produce an item in nextp;
PC.put(x);
}
}
void consumer() {
item x;
while(TRUE) {
PC.get(x);
consume the item in nextc;
...
}
}
void main() {
cobegin
proceducer();consumer();
coend
}
问题描述:
有五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替地进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐毕,放下筷子继续思考。
利用记录型信号量解决哲学家进餐问题
经分析可知,放在桌子上的筷子是临界资源,在一段时间内只允许一位哲学家使用。为了实现对筷子的互斥使用,可以用一个信号量表示一只筷子,由这五个信号量构成信号量数组。其描述如下:
semaphore chopstick[5]={1,1,1,1,1};
所有信号量均被初始化为1,第i位哲学家的活动可描述为:
semaphore chopstick[5]={1,1,1,1,1};
do {
wait(chopstick[i]); //拿左边筷子
wait(chopstick[(i+1)%5]); //拿右边筷子
//eat
…
signal(chopstick[i]); //放左边筷子
signal(chopstick[(i+1)%5]); //放右边筷子
…
//think
}while[TRUE];
假如每个哲学家都拿左边筷子,即5个信号量chopstick同时为0,则5个进程同时无限期等待而引起死锁。
解决办法:
(1)至多只允许有四位哲学家同时去拿左边的筷子,最终能保证至少有一位哲学家能够进餐,并在用毕时能释放出他用过的两只筷子,从而使更多的哲学家能够进餐。
(2)仅当哲学家的左、右两只筷子均可用时,才允许他拿起筷子进餐。
(3)规定奇数号哲学家先拿他左边的筷子,然后再去拿右边的筷子;而偶数号哲学家则相反。按此规定,将是1、2号哲学家竞争1号筷子;3、4号哲学家竞争3号筷子。即五位哲学家都先竞争奇数号筷子,获得后,再去竞争偶数号筷子,最后总会有一位哲学家能获得两只筷子而进餐。
利用AND信号量机制解决哲学家进餐问题
在哲学家进餐问题中,要求每个哲学家先获得两个临界资源(筷子)后方能进餐,这在本质上就是前面所介绍的AND同步问题,故用AND信号量机制可获得最简洁的解法。
semaphore chopstick[5]={1,1,1,1,1};
do {
…
//think
...
Swait(chopstick[(i+1)%5], chopstick[i]);
...
//eat
...
Ssignal(chopstick[(i+1)%5], chopstick[i]);
}while[TRUE];
问题描述:
有读者和写者两组并发进程,共享一个文件,当两个或以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求:
信号量设置:
互斥信号量wmutex: 保证读写进程 读写互斥,写写互斥。
整型变量readcount:记录读者进程数目,初值为0。
互斥信号量rmutex: 实现对readcount互斥访问。
算法描述如下:
semaphore rmutex=1,wmutex=1;
int readcount=O;
void reader() {
do {
wait(rmutex); //有读者进入,上锁 互斥访问readcount
if (readcount==O) wait(wmutex); //判断是否是第一个读者进程 ,若是则申请资源 ,上锁书 (直到readcount=0 才释放资源,在此期间不允许写进程执行);若不是第一个则不用申请资源,直接往下进行
readcount++; //读者进程+1
signal(rmutex); //开锁 释放readcount变量
perform read operation;
wait(rmutex); //读者进程已读完准备离开,上锁 互斥访问readcount
readcount--; //读者进程-1
if (readcount==O) signal(wmutex); //如果是最后一个读者进程离开,则释放掉资源,此时写进程才允许运行;否则继续占用资源
signal(rmutex); //开锁 释放readcount变量
} while(TRUE);
}
void writer() {
do {
wait(wmutex); //申请资源
perform write operation;
signal(wmutex); //释放资源
} while(TRUE);
}
void main() {
cobegin
reader(); writer();
coend
}
高级进程通信可以归为四大类:
进程之间基于共享空间进行通信
指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名 pipe文件。向管道提供输入的发送进程(即写进程)以字符流形式将大量的数据送入管道;而接受管道输出的接收进程(即读进程)则从管道中接收(读)数据。由于发送进程和接收进程是利用管道进行通信的,故又称为管道通信。
为了协调双方的通信,管道机制必须提供以下三方面的协调能力:
①互斥
②同步
③确定对方是否存在。
管道通信必然是半双工通信。
在该机制中,进程不必借助任何共享存储区或数据结构,而是以格式化的消息(message)为单位,将通信的数据封装在消息中,并利用操作系统提供的一组通信命令(原语),在进程间进行消息传递,完成进程间的数据交换。
该方式隐藏了通信实现细节,使通信过程对用户透明化,降低了通信程序设计的复杂性和错误率。
主要的实现方法分为三类:套接字、远程过程调用和远程方法调用。
消息传递通信实现方式:
引入进程的目的是更好地使多道程序并发执行,提高资源利用率和系统吞吐量;而引入线程的目的则是减小程序在并发执行时所付出的时空开销,提高操作系统的并发性能。
线程最直接的理解就是“轻量级进程”,它是一个基本的CPU执行单元,也是程序执行流的最小单元,由线程ID、程序计数器、寄存器集合和堆栈组成。
线程运行的三种状态:就绪-执行-阻塞(和进程状态转换一样)
线程控制块:TCB
线程的实现方式:
①内核支持线程KST
②用户级线程ULT
③组合方式(多对一、一对一、多对多)
1.信号量的物理意义是:当前信号量的值大于零时,表示(系统中某类资源的数目 );当信号量值小于零时,其绝对值表示 ( 在该信号量链表中已阻塞进程的数目)。
2.信号量的初值必须是大于零的整数。(Ⅹ)
3.进程间的高级通信机制可归结为3大类,分别是
(1) 共享存储器系统
(2) 消息传递系统
(3) 管道通信
4.设有n个进程共用一个相同的程序段(临界区),如果每次最多允许m个进程(m<=n)同时进入临界区,则信号量的初值为(m).
所学课本:
计算机操作系统 第四版(汤小丹等)西安电子科技大学出版社
王道操作系统复习指导(王道论坛)电子工业出版社