4.5 管程机制
使用信号量机制在访问临界资源的进程都必须自备同步操作wait(S)和signal(S)。这种大量的同步操作就分散在各个进程中,这样就可能会带来死锁。因此产生了一种新的进程同步工具——管程(monitor)。
1 管程
管程:代表共享资源的数据结构,以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序,共同构成了一个OS的资源管理模块。
管程的组成:管程的名称;局限于管程内的共享数据结构说明;对该数据结构进行操作的一组过程;设置局限于管程内的共享数据初值的语句。
管程中包含了面向对象的思想,将表征共享资源的数据结构以及对数据结构操作的一组过程(同步机制),都集中封装在了一个对象内部。封装于管程内部的数据结构仅能被封装于管程内部的过程所访问,管程外的任何过程都不能访问它;封装于管程内部的过程也仅能访问管程内的数据结构。
管程是一种程序设计语言结构的成分,它和信号量有同等的表达能力。
(1)模块化:管程是一个基本程序单位,可以单独编译;
(2)抽象数据类型:管程中不仅有数据,还有对数据的操作;
(3)信息屏蔽:管程内部的数据结构仅能被封装于管程内部的过程所访问。
管程与线程的区别:
(1)二者虽然都定义了数据结构,但进程定义的是私有数据结构——PCB,管程定义的是公共数据结构,如消息队列等。
(2)进程是由顺序程序执行有关操作的;管程则主要进行同步操作和初始化操作。
(3)进程目的在于实现系统的并发性;管程目的在于解决共享资源的互斥使用问题。
(4)进程为主动工作方式;管程为被动工作方式。
(5)进程之间能并发执行;管程不能与其调用者并发。
(6)进程具有动态性;管程是OS中一个资源管理模块,仅供进程调用。
2 条件变量
在利用管程实现进程同步时,必须设置同步工具,如两个同步操作原语wait和signal。
在进程调用管程时,该进程出现阻塞或挂起,在这个期间,其他进程无法访问管程从而导致长时间等待,因此引入了条件变量condition。管程中对每个条件变量都须予以说明:condition x,y。
4.6 经典的进程同步问题
4.6.1 生产者—消费者问题
问题描述:一组生产者进程和一组消费者进程共享一个初始为空、大小为n的缓冲区,只有缓冲区没满时,生产者才能把消息放入缓冲区,否则必须等待;只有缓冲区不空时,消费者才能从中取出消息,否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入消息,或一个消费者从中取出消息。
问题分析:
(1)关系分析。生产者和消费者对缓冲区互斥访问是互斥关系,同时生产者和消费者又是一个互相协作的关系,只有生产者生产之后,消费者才能消费,它们也是同步关系。
(2)整理思路。这里比较简单,只有生产者和消费者两个进程,正好是这两个进程存在着互斥关系和同步关系。那么需要解决的是互斥和同步PV操作的位置。
假设:信号量mutex实现各进程对缓冲池的互斥使用;信号量empty表示缓冲池中空缓冲区的数量;信号量full表示缓冲池中满缓冲区的数量。
1 利用记录型信号量解决生产者—消费者问题
伪代码如下:
semaphore mutex = 1; //设置取筷子的信号量
semaphore empty = n; //空闲缓冲区
semaphore full = 0; //缓冲区初始化为空
Producer(){ //生产者进程
do{
produce an item in nextp; //生产数据
P(empty); //获取空缓冲区单元
P(mutex); //进入临界区
add nextp to buffer; //将数据放入缓冲区
V(mutex); //离开临界区,释放互斥信号量
V(full); //满缓冲区数加1
} while (true);
}
Consumer(){ //消费者进程
do{
P(full); //获取满缓冲区单元
P(mutex); //进入临界区
remove an item from buffer; //将数据放入缓冲区
V(mutex); //离开临界区,释放互斥信号量
V(empty); //空缓冲区数加1
consumer the item; //消费数据
} while (true);
}
2 利用AND型信号量解决生产者—消费者问题
对于生产者—消费者问题也可利用AND信号量来解决,即用Swait(empty,mutex)来代替wait(empty)和wait(mutex),用Ssignal(mutex,full)来代替signal(mutex)和signal(full),用Swait(full,empty)来代替wait(full)和wait(empty),用Ssignal(mutex,empty)来代替signal(mutex)和signal(empty)。
semaphore mutex = 1; //设置取筷子的信号量
semaphore empty = n; //空闲缓冲区
semaphore full = 0; //缓冲区初始化为空
Producer(){ //生产者进程
do{
produce an item in nextp; //生产数据
Swait(empty,mutex);
add nextp to buffer; //将数据放入缓冲区
Ssignal(mutex,full);
} while (true);
}
Consumer(){ //消费者进程
do{
Swait(full,mutex);
remove an item from buffer; //将数据放入缓冲区
Ssignal(mutex,empty);
consumer the item; //消费数据
} while (true);
}
3 利用管程解决生产者—消费者问题
在使用管程解决生产者-消费者问题的时候,首先便是给它们建立一个管程,并命名为producerconsumer,或者简称为PC。其中包括两个过程:
(1)put(x)过程,生产者利用该过程将自己生产的产品投放到缓冲池中,并用整型变量count来表示在缓冲池中已有的产品数量,当count>=N时,表示缓冲池已满,生产者须等待。
(2)get(x)过程,消费者利用该过程从缓冲池中取出一个产品,当count
对于条件变量notfull和notempty,分别有两个过程cwait和csignal对它们进行操作:
(1)cwait(condition)过程:当管程被一个进程占用时,其他进程调用该过程时阻塞,并挂在条件condition上;
(2)csignal(condition)过程:唤醒在cwait执行后阻塞在条件condition队列上的进程,如果这样的进程不止一个,则选择其中一个实施唤醒操作;如果队列是空,则没有操作而返回。
PC管程的描述:
monitor ProducterConsumer
condition notfull,notempty; //条件变量用来实现同步(排队)
int count=0; // 缓冲区中的产品数
void insert(Item item){
//把产品item放入缓冲区
if(count == N)
cwait(notfull);
count++;
insert_item(item);
if(count == 1)
csignal(notempty);
}
Item remove(){ //从缓冲区取出一个产品
if(count==0)
ccwait(notempty);
count--;
if(count == N-1)
csignal(notfull);
return remove_item();
}
end monitor;
生产者进程:
producter(){
while(1){
item = 生产一个产品;
ProducterConsumer.insert(item);
}
}
消费者进程:
consumer(){
while(1){
item = ProducterConsumer.remove();
消费产品;
}
}
4.6.2 哲学家就餐问题
问题描述:有五位哲学家,它们的生活方式是交替的进行思考和进餐。哲学家门共用一张圆桌,分别坐在周围的五张椅子上。在圆桌上有五只碗和五根筷子,平时哲学家进行思考,饥饿的时候试图取其左右的靠他最近的筷子,只有当拿到两根筷子时才能进餐。
问题分析:
(1)关系分析。5名哲学家与左右邻居对其中间筷子的访问是互斥关系。
(2)整理思路。显然,这里有5个进程。本题的关键是如何让一名哲学家拿到左右两根筷子而不造成死锁或饥饿现象。解决方法有两个:一是如何让他们同时拿两根筷子;而是对每名哲学家的动作制定规则,避免饥饿或死锁现象的发生。
(3)制定规则。为防止死锁发生,可对哲学家进程施加一些限制条件,比如至多允许4名哲学家同时进餐;仅当一名哲学家左右两边的筷子都可用时,才允许他抓起筷子;对哲学家顺序编号,要求奇数号哲学家先拿左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反。假设我们采用第二种方法,当一名哲学家左右两边的筷子都可用时,才允许他抓起筷子。
1 利用记录型信号量解决生产者—消费者问题
伪代码如下:
semaphore chopstick[5] = {1,1,1,1,1}; //初始化信号量
semaphore mutex = 1; //设置取筷子的信号量
Pi(){ //i号哲学家的进程
do{
P(mutex); //在取筷子前获得信号量
P(chopstick[i]); //取左边筷子
P(chopstick[(i + 1) % 5)]); //取右边筷子
V(mutex); //释放取筷子的信号量
eat; //进餐
V(chopstick[i]); //放回左边筷子
V(chopstick[(i + 1) % 5)]); //放回右边筷子
think; //思考
} while (true);
}
哲学家饥饿时总是会先拿他左边的筷子,即执行P(chopstick[i]),成功后,再拿他右边的筷子,即执行P(chopstick[(i + 1) % 5)]);成功后可以就餐。
2 利用AND型信号量解决哲学家就餐问题
哲学家就餐问题,要求每个哲学家先获得两个临界资源(筷子)后方能进餐,这本质上就是AND同步问题,就可利用AND型信号量机制解决。
semaphore chopstick[5] = {1,1,1,1,1}; //初始化信号量
semaphore mutex = 1; //设置取筷子的信号量
Pi(){ //i号哲学家的进程
do{
...
think; //思考
...
Swait(chopstick[(i + 1) % 5)],chopstick[i]); //取右边筷子 取左边筷子
...
eat; //进餐
...
Ssignal(chopstick[(i + 1) % 5)],chopstick[i]); //放回右边筷子 放回左边筷子
} while (true);
}
3 利用管程解决哲学家就餐问题
筷子拿起与否由管程dp来控制,管程dp描述如下:
monitor dp{
enum{thinking,hungry,eating} state[5];
condition self[5];
initialization_code(){
for(int i = 0;i < 5;i++){
state[i] = thinking;
}
}
void pickup(int i){
state[i] = hungry;
test(i);
if(state[i] != eating){
self[i].wait();
}
}
void putdown(int i){
state[i] = thinking;
test((i + 4) % 5); //判断左边哲学家
test((i + 1) % 5); //判断右边哲学家
}
void test(int i){
if(state[(i + 4) % 5] != eating) && (state[i] == hungry) && (state[(i + 1) % 5] != eating){
state[i] = eating;
self[i].signal();
}
}
}
哲学家i描述为:
do{
dp.pickup(i);
...
eat
...
dp.putdown(i);
}while(true);
4.6.3 读者—写者问题
读者—写者问题常被用于测试新同步原语。
问题描述:有读者和写者两组并发进程,共享一个文件,当两个或以上读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求:①允许多个读者可以同时对文件进行读操作;②只允许一个写者往文件中写信息;③任意写者在完成写操作之前不允许其他读者或写者工作;④写者执行写操作前,应让已有的读者和写者全部退出。
问题分析:
(1)关系分析。由题目分析读者和写者是互斥的,写者和写者也是互斥的,而读者和读者不存在互斥关系。
(2)整理思路。两个进程,即读者和写者。写者是比较简单的,它和任何进程互斥,用互斥的信号量的P操作、V操作即可解决。读者的问题比较复杂,它必须在实现与写者互斥的同时,实现与其他读者的同步。因此,这里用到了一个计数器,用它来判断当前是否有读者读文件。当有读者时,写者是无法写文件的,此时读者会一直占用文件,当没有读者时,写者才可以写文件。同时,这里不同读者对计数器的访问也应该是互斥的。
假设:count用来表示正在读的进程数目;rw为实现reader和writer进程在读/写时的互斥的信号量;mutex为count(一个被多个reader进程访问的临界资源)设置的一个互斥信号量。
1 利用记录型信号量解决读者—写者问题
伪代码如下:
int count = 0; //用于记录当前读者的数量
semaphore mutex = 1; //用于保护更新count变量时的互斥
semaphore rw = 1; //用于保证读者和写者互斥地访问文件
Writer(){ //写者进程
do{
P(rw); //互斥访问共享文件
writing; //写入
V(rw); //释放共享文件
} while (true);
}
Reader(){ //读者进程
do{
P(mutex); //互斥访问count变量
if(count == 0) //当第一个读进程读共享文件时
P(rw); //阻止写进程写
count++; //读者数量加一
V(mutex); //释放count变量
reading; //读取
P(mutex); //互斥访问count变量
count--; //读者数量减一
if(count == 0) //当第一个读进程读共享文件时
V(rw); //允许写进程写
V(mutex); //释放count变量
} while (true);
}
2 利用“信号量集”解决读者—写者问题
int RN; //用于记录当前读者的数量
semaphore L = RN; //用于保护更新count变量时的互斥
semaphore mx = 1; //用于保证读者和写者互斥地访问文件
Reader(){ //读者进程
do{
Swait(L,1,1); //互斥访问共享文件
Swait(mx,1,0);
...
perform read operation; //写入
...
Ssignal(L,1); //释放共享文件
} while (true);
}
Writer(){ //写者进程
do{
Swait(mx,1,1;L,RN,0); //互斥访问count变量
perform write operation; //写入
Ssignal(mutex); //释放count变量
} while (true);
}
Main(){
cobegin;
Reader();
Writer();
coend;
}
其中,Swait(mx,1,0)起着“开关”的作用。只要无writer进程进行写操作,mx=1,reader进程就可以读。但只要有writer进程进程写操作,mx=0,任何reader进程就都无法进行读操作。Swait(mx,1,1;L,RN,0)语句表示,仅当既无writer进程在写(mx=1),又无reader进程在读(L=RN)时,writer进程才能进入临界区写。
4.7 解决进程同步和互斥的方法
1 进程同步分析方法
(1)找出需要同步的代码片段;
(2)分析所找代码片段的执行次序;
(3)增加同步信号量并赋初值;
(4)再所找代码片段前后加wait(S)和signal(S)操作。
2 进程互斥分析方法
(1)查找临界资源;
(2)划分临界区;
(3)定义互斥信号量并赋初值;
(4)在临界区前后的进入区和退出区中分别加入wait(S)和signal(S)操作。