原文出自:
http://www.huihoo.com/os/process/main.htm
多进程的系统中避免不了进程间的相互关系。本讲将介绍进程间的两种主要关系——同步与互斥,然后着重讲解解决进程同步的几种机制。
进程互斥是进程之间发生的一种间接性作用,一般是程序不希望的。通常的情况是两个或两个以上的进程需要同时访问某个共享变量。我们一般将发生能够问共享变量的程序段成为临界区。两个进程不能同时进入临界区,否则就会导致数据的不一致,产生与时间有关的错误。解决互斥问题应该满足互斥和公平两个原则,即任意时刻只能允许一个进程处于同一共享变量的临界区,而且不能让任一进程无限期地等待。互斥问题可以用硬件方法解决,我们不作展开;也可以用软件方法,这将会在本讲详细介绍。
进程同步是进程之间直接的相互作用,是合作进程间有意识的行为典型的例子是公共汽车上司机与售票员的合作。
只有当售票员关门之后司机才能启动车辆,只有司机停车之后售票员才能开车门。司机和售票员的行动需要一定的协调。同样地,两个进程之间有时也有这样的依赖关系,因此我们也要有一定的同步机制保证它们的执行次序。
本讲主要介绍以下四种同步和互斥机制:
信号量
管程
会合
分布式系统
信号量 | |||||||||||||||||||
信号量是最早出现的用来解决进程同步与互斥问题的机制, 包括一个称为信号量的变量及对它进行的两个原语操作。 |
|||||||||||||||||||
本节将从以下几个方面进行介绍--
|
一. 信号量的概念 |
1. 信号量的类型定义 |
每个信号量至少须记录两个信息:信号量的值和等待该信号量的进程队列。它的类型定义如下:(用类PASCAL语言表述) semaphore = record value: integer; queue: ^PCB; end; 其中PCB是进程控制块,是操作系统为每个进程建立的数据结构。 s.value>=0时,s.queue为空; s.value<0时,s.value的绝对值为s.queue中等待进程的个数; |
返回 |
2. PV原语 |
对一个信号量变量可以进行两种原语操作:p操作和v操作,定义如下: procedure p(var s:samephore); { s.value=s.value-1; if (s.value<0) asleep(s.queue); } procedure v(var s:samephore); { s.value=s.value+1; if (s.value<=0) wakeup(s.queue); } 其中用到两个标准过程: asleep(s.queue);执行此操作的进程的PCB进入s.queue尾部,进程变成等待状态 wakeup(s.queue);将s.queue头进程唤醒插入就绪队列 s.value初值为1时,可以用来实现进程的互斥。 p操作和v操作是不可中断的程序段,称为原语。如果将信号量看作共享变量,则pv操作为其临界区,多个进程不能同时执行,一般用硬件方法保证。一个信号量只能置一次初值,以后只能对之进行p操作或v操作。 由此也可以看到,信号量机制必须有公共内存,不能用于分布式操作系统,这是它最大的弱点。 |
返回 |
二. 实例 |
1. 生产者-消费者问题(有buffer) |
问题描述: 一个仓库可以存放K件物品。生产者每生产一件产品,将产品放入仓库,仓库满了就停止生产。消费者每次从仓库中去一件物品,然后进行消费,仓库空时就停止消费。 解答: 进程:Producer - 生产者进程,Consumer - 消费者进程 共有的数据结构: buffer: array [0..k-1] of integer; in,out: 0..k-1; — in记录第一个空缓冲区,out记录第一个不空的缓冲区 s1,s2,mutex: semaphore; — s1控制缓冲区不满,s2控制缓冲区不空,mutex保护临界区; 初始化s1=k,s2=0,mutex=1 producer(生产者进程): Item_Type item; { while (true) { produce(&item); p(s1); p(mutex); buffer[in]:=item; in:=(in+1) mod k; v(mutex); v(s2); } } consumer(消费者进程): Item_Type item; { while (true) { p(s2); p(mutex); item:=buffer[out]; out:=(out+1) mod k; v(mutex); v(s1); consume(&item); } } |
返回 |
2. 第一类读-写者问题 |
问题描述: 一些读者和一些写者对同一个黑板进行读写。多个读者可同时读黑板,但一个时刻只能有一个写者,读者写者不能同时使用黑板。对使用黑板优先级的不同规定使读者-写者问题又可分为几类。第一类问题规定读者优先级较高,仅当无读者时允许写者使用黑板。 解答: 进程:writer - 写者进程,reader - 读者进程 共有的数据结构: read_account:integer; r_w,mutex: semaphore; — r_w控制谁使用黑板,mutex保护临界区,初值都为1 reader - (读者进程): { while (true) { p(mutex); read_account++; if(read_account=1) p(r_w); v(mutex); read(); p(mutex); read_account--; if(read_account=0) v(r_w);; v(mutex); } } writer - (写者进程): { while (true) { p(mutex); write(); v(mutex); } } |
返回 |
3. 哲学家问题 |
问题描述: 一个房间内有5个哲学家,他们的生活就是思考和进食。房间里有一张圆桌,中间放着一盘通心粉(假定通心粉无限多)。桌子周围放有五把椅子,分别属于五位哲学家每两位哲学家之间有一把叉子,哲学家进食时必须同时使用左右两把叉子。 解答: 进程:philosopher - 哲学家 共有的数据结构&过程: state: array [0..4] of (think,hungry,eat); ph: array [0..4] of semaphore; — 每个哲学家有一个信号量,初值为0 mutex: semaphore; — mutex保护临界区,初值=1 procedure test(i:0..4); { if ((state[i]=hungry) and (state[(i+1)mod 5]<>eating) and (state[(i-1)mod 5]<>eating)) { state[i]=eating; V(ph[i]); } } philosopher(i:0..4): { while (true) { think(); p(mutex); state[i]=hungry; test(i); v(mutex); p(ph[i]); eat(); p(mutex); state[i]=think; test((i-1) mod 5); test((i+1) mod 5); v(mutex); } } |
管程 | |||||||||||||||||||
信号量机制功能强大,但使用时对信号量的操作分散,而且难以控制,读写和维护都很困难。因此后来又提出了一种集中式同步进程——管程。其基本思想是将共享变量和对它们的操作集中在一个模块中,操作系统或并发程序就由这样的模块构成。这样模块之间联系清晰,便于维护和修改,易于保证正确性。 |
|||||||||||||||||||
本节将从以下几个方面进行介绍--
|
一. 管程的概念 |
1. 管程的形式 |
管程作为一个模块,它的类型定义如下: monitor_name = MoNITOR; 共享变量说明; define 本管程内部定义、外部可调用的函数名表; use 本管程外部定义、内部可调用的函数名表; 内部定义的函数说明和函数体 { 共享变量初始化语句; } |
返回 |
2. 管程的特点 |
从语言的角度看,管程主要有以下特性: (1)模块化。管程是一个基本程序单位,可以单独编译; (2)抽象数据类型。管程是中不仅有数据,而且有对数据的操作; (3)信息掩蔽。管程外可以调用管程内部定义的一些函数,但函数的具体实现外部不可见; 对于管程中定义的共享变量的所有操作都局限在管程中,外部只能通过调用管程的某些函数来间接访问这些变量。因此管程有很好的封装性。 为了保证共享变量的数据一致性,管程应互斥使用。 管程通常是用于管理资源的,因此管程中有进程等待队列和相应的等待和唤醒操作。在管程入口有一个等待队列,称为入口等待队列。当一个已进入管程的进程等待时,就释放管程的互斥使用权;当已进入管程的一个进程唤醒另一个进程时,两者必须有一个退出或停止使用管程。在管程内部,由于执行唤醒操作,可能存在多个等待进程(等待使用管程),称为紧急等待队列,它的优先级高于入口等待队列。 因此,一个进程进入管程之前要先申请,一般由管程提供一个enter过程;离开时释放使用权,如果紧急等待队列不空,则唤醒第一个等待者,一般也由管程提供外部过程leave。 管程内部有自己的等待机制。管程可以说明一种特殊的条件型变量:var c:condition;实际上是一个指针,指向一个等待该条件的PCB队列。对条件型变量可执行wait和signal操作: wait(c):若紧急等待队列不空,唤醒第一个等待者,否则释放管程使用权。执行本操作的进程进入C队列尾部; signal(c):若C队列为空,继续原进程,否则唤醒队列第一个等待者,自己进入紧急等待队列尾部。 |
返回 |
二. 实例 |
1. 生产者-消费者问题(有buffer) |
问题描述:(见信号量部分) 解答: 管程:buffer=MODULE; (假设已实现一基本管程monitor,提供enter,leave,signal,wait等操作) notfull,notempty:condition; — notfull控制缓冲区不满,notempty控制缓冲区不空; count,in,out: integer; — count记录共有几件物品,in记录第一个空缓冲区,out记录第一个不空的缓冲区 buf:array [0..k-1] of item_type; define deposit,fetch; use monitor.enter,monitor.leave,monitor.wait,monitor.signal; procedure deposit(item); { if(count=k) monitor.wait(notfull); buf[in]=item; in:=(in+1) mod k; count++; monitor.signal(notempty); } procedure fetch:Item_type; { if(count=0) monitor.wait(notempty); item=buf[out]; in:=(in+1) mod k; count--; monitor.signal(notfull); return(item); } { count=0; in=0; out=0; } 进程:producer,consumer; producer(生产者进程): Item_Type item; { while (true) { produce(&item); buffer.enter(); buffer.deposit(item); buffer.leave(); } } consumer(消费者进程): Item_Type item; { while (true) { buffer.enter(); item=buffer.fetch(); buffer.leave(); consume(&item); } } |
返回 |
2. 第一类读-写者问题 |
问题描述:(见信号量部分) 解答: 管程:bulletin=MODULE; (假设已实现一基本管程monitor,提供enter,leave,signal,wait等操作) r,w:condition; — r控制读者使用黑板,w控制写者; writing:boolean; — 当前是否写者使用黑板 read_account:integer; define start_read,end_read,start_write,end_write; use monitor.enter,monitor.leave,monitor.wait,monitor.signal; procedure start_read(); { if(writing) monitor.wait(r); read_account++; monitor.signal(r); } procedure end_read(); { read_account--; if (read_account=0) monitor.signal(w); } procedure start_write(); { if((read_account<>0) or writing) monitor.wait(w); writing=true; } procedure end_write(); { writing=false; if (r<>NULL) monitor.signal(r); else monitor.signal(w); } 进程:writer - 写者进程,reader - 读者进程 reader - (读者进程): { while (true) { bulletin.enter(); bulletin.start_read(); read(); bulletin.end_read(); bulletin.leave(); } } writer - (写者进程): { while (true) { bulletin.enter(); bulletin.start_write(); write(); bulletin.end_write(); bulletin.leave(); } } |
返回 |
3. 哲学家问题 |
问题描述:(见信号量部分) 解答: 管程:dining=MODULE; (假设已实现一基本管程monitor,提供enter,leave,signal,wait等操作) queue:array [0..4] of condition; — 控制哲学家能否进食; fork:array[0..4] of (free,use); — 当前各个叉子的状态 define pickup,test,putdown; use monitor.enter,monitor.leave,monitor.wait,monitor.signal; procedure pickup(i:0..4); { if(fork[i]=used) monitor.wait(queue[i]); fork[i]=used; } procedure putdown(i:0..4); { fork[i]=free; monitor.signal(queue[i]); } procedure test(i:0..4):boolean; { if(fork[i]=use) ok=false else { fork[i]=use; ok=true; } ; return ok; } 管程:dining=MODULE; philosopher(i:0..4); with_two_forks:boolean; { while (true) { think(); with_two_forks=false;; while(!with_two_forks) { dining.enter(); dining.pickup(i); if(dining.test((i+1) mod 5) with_two_forks=true; else dining.putdown(i); dining.leave(); } eat(); dining.enter(); dining.putdown(i); dining.putdown((i+1) mod 5)); dining.leave(); } } |
其余部分不再引述。