信号量S的物理含义
S>0:表示有S个资源可用;S=0表示无资源可用;S<0绝对值表示等待队列或链表中的进程个数。信号量的初值应大于等于0。
PV原语小结
通过操作信号量来处理进程间的同步与互斥的问题。其核心就是一段不可分割不可中断的程序。
信号量是由操作系统来维护的,用户进程只能通过初始化和两个标准原语(P、V原语)来访问,它们在执行时是不可中断的。初始化可指定一个非负整数,即空闲资源总数。
P原语:P是荷兰语Proberen(测试)的首字母。为阻塞原语,负责把当前进程由运行状态转换为阻塞状态,直到另外一个进程唤醒它。操作为:申请一个空闲资源(把信号量减1),则若成功,则退出;若失败,则该进程被阻塞;
V原语:V是荷兰语Verhogen(增加)的首字母。为唤醒原语,负责把一个被阻塞的进程唤醒,它有一个参数表,存放着等待被唤醒的进程信息。操作为:释放一个被占用的资源(把信号量加1),如果发现有被阻塞的进程,则选择一个唤醒之。
P(S):表示申请一个资源,S减 1;若 减1 后仍S>=0,则该进程继续执行;若 减1 后 S<0,表示已无资源可用,需要将自己阻塞起来。
V(S):表示释放一个资源,S加 1;若 加1 后S>0,则该进程继续执行;若 加1 后 S<=0,表示等待队列上有等待进程,需要将第一个等待的进程唤醒。
PV原语对信号量的操作可以分为三种情况:
.
1. 把信号量视为一个加锁标志位,实现对一个共享变量的互斥访问。
实现过程:
P(mutex); // mutex的初始值为1
访问该共享数据
V(mutex);
非临界区
互斥信号量是根据临界资源的类型设置的。有几种类型的临界资源就设置几个互斥信号量。它代表该类临界资源的数量,或表示是否可用,其初值一般为“1”。
.
2. 把信号量视为是某种类型的共享资源的剩余个数,实现对一类共享资源的访问。
实现过程:
P(resource); // resource的初始值为该资源的个数N
使用该资源;
V(resource);
非临界区
.
3. 把信号量作为进程间的同步工具
实现过程:
临界区C1; P(S);
V(S); 临界区C2;
同步信号量是根据进程的数量设置的。一般情况下,有几个进程就设置几个同步信号量,表示该进程是否可以执行,或表示该进程是否执行结束,其初值一般为“0”。
同步与互斥的解题思路
分清哪些是互斥问题(互斥访问临界资源的),哪些是同步问题(具有前后执行顺序要求的)。
对互斥问题要设置互斥信号量,不管有互斥关系的进程有几个或几类,通常只设置一个互斥信号量,且初值为1,代表一次只允许一个进程对临界资源访问。
对同步问题要设置同步信号量,通常同步信号量的个数与参与同步的进程种类有关,即同步关系涉及几类进程,就有几个同步信号量。同步信号量表示该进程是否可以开始或该进程是否已经结束。
在每个进程中用于实现互斥的PV操作必须成对出现;用于实现同步的PV操作也必须成对出现,但可以分别出现在不同的进程中;在某个进程中如果同时存在互斥与同步的P操作,则其顺序不能颠倒,必须先执行对同步信号量的P操作,再执行对互斥信号量的P操作,但V操作的顺序没有严格要求。
为什么P操作不能颠倒?
解进程同步和互斥问题的方法步骤
1) 关系分析。找出问题中的进程数,并且分析它们之间的同步和互斥关系。同步、互斥关系直接按照上面的经典范式改写。
2) 整理思路。找出解决问题的关键点,并且根据做过的题目找出解决的思路。根据进程的操作流程确定P操作、V操作的大致顺序。
3) 设置信号量。根据上面两步,设置需要的信号量,确定初值,完善整理。
生产者消费者问题
问题描述
一组生产者进程和一组消费者进程共享一个初始为空、大小为n的缓冲区,只有缓冲区没满时,生产者才能把消息放入到缓冲区,否则必须等待;只有缓冲区不空时,消费者才能从中取出消息,否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入消息,或者一个消费者从中取出消息。
问题分析
关系分析:生产者往缓冲区写消息,,消费者从缓冲区取消息必须分开进行,所以两者是互斥关系;同时只有生产者放入消息后,消费者才能从中取出消息,所以两者还是同步关系。
整理思路:两个进程,既是互斥关系,又是同步关系。此时需要注意互斥同步PV操作的位置。
信号量设置:互斥信号量mutex,初值为1,保证互斥的访问缓冲池;信号量full记录当前缓冲池中满缓冲区个数,初值为0,信号量empty记录当前缓冲池空缓冲区个数,初值为n。
类C语言代码
semaphore mutex=1; //临界区互斥信号量
semaphore empty=n; //空闲缓冲区
semaphore full=0; //缓冲区初始化为空
producer () { //生产者进程
while(1){
produce an item in nextp; //生产数据
P(empty); //获取空缓冲区单元
P(mutex); //进入临界区.
add nextp to buffer; //将数据放入缓冲区
V(mutex); //离开临界区,释放互斥信号量
V(full); //满缓冲区数加1
}
}
consumer () { //消费者进程
while(1){
P(full); //获取满缓冲区单元
P(mutex); // 进入临界区
remove an item from buffer; //从缓冲区中取出数据
V (mutex); //离开临界区,释放互斥信号量
V (empty) ; //空缓冲区数加1
consume the item; //消费数据
}
}
生产者消费者问题扩展
问题描述
桌子上有一只盘子,每次只能向其中放入一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专等吃盘子中的橘子,女儿专等吃盘子中的苹果。只有盘子为空时,爸爸或妈妈就可向盘子中放一个水果;仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出。
问题分析
关系分析:爸妈往盘子里放水果,必须互斥进行,所以爸妈是互斥关系;爸爸和女儿、妈妈和儿子是同步关系,类似生产者和消费者;儿子和女儿没有什么关系,有相应需求的水果就拿走
整理思路:有四个进程:爸爸放苹果、女儿吃苹果、妈妈放橘子、儿子吃橘子,可抽象为两对生产者消费者对一个缓冲区(盘子)进行操作
信号量设置:盘子plate为互斥信号量,表示是否允许向盘子中放置水果,初值为1, 表示允许放入。同步信号量设置两个,分别为apple、orange,因为两类进程:爸爸和女儿取放苹果、妈妈和儿子取放橘子。apple表示盘中是否有苹果,初值为0,表示盘子为空,不可取,若apple为1可以取;orange类似。
类C语言代码
semaphore plate=1, apple=0, orange=0;
father() //父亲进程
{
while(1)
{
P(plate); //互斥向盘中放水果
向盘中放苹果;
V(apple); //放好后,此时可以取苹果
}
}
mother() //母亲进程
{
while(1)
{
P(plate); //互斥向盘中放水果
向盘中放橘子;
V(orange); //放好后,此时可以取橘子
}
}
sun() //儿子进程
{
while(1)
{
P(orange); //互斥从盘中取橘子
从盘中取橘子;
V(plate); //取完后盘子可用
}
}
daughter() //女儿进程
{
while(1)
{
P(apple); //互斥从盘中取苹果
从盘中取苹果;
V(plate); //取完后盘子可用
}
}
PV原语第二种类型示例
问题描述
某超市门口为顾客准备了100辆手推车,每位顾客在进去买东西时取一辆推车,在买完东西结完帐以后再把推车还回去。试用P、V操作正确实现顾客进程的同步互斥关系。
问题分析
把手推车视为某种资源,每个顾客为一个要互斥访问该资源的进程。因此这个例子为PV原语的第二种应用类型。
类C代码
semaphore num=100;
consumer()
{
P(num);
买东西;
结帐;
V(num);
}
哲学家就餐问题
问题描述
一张圆桌上坐着5名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭,如图所示。哲学家们倾注毕生精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿的时候,才试图拿起左、 右两根筷子(一根一根地拿起)。如果筷子已在他人手上,则需等待。饥饿的哲学家只有同时拿到了两根筷子才可以开始进餐,当进餐完毕后,放下筷子继续思考。
问题分析
关系分析:5名哲学家与左右邻居对其中间筷子的访问是互斥关系。
整理思路:这里有五个进程。本题的关键是如何让一个哲学家拿到左右两个筷子而不造成死锁或者饥饿现象。那么解决方法有两个,一个是让他们同时拿两个筷子;二是对每个哲学家的动作制定规则,避免饥饿或者死锁现象的发生。为了防止死锁的发生,可以对哲学家进程施加一些限制条件,比如至多允许四个哲学家同时进餐;仅当一个哲学家左右两边的筷子都可用时才允许他抓起筷子;对哲学家顺序编号,要求奇数号哲学家先抓左边的筷子,然后再转他右边的筷子,而偶数号哲学家刚好相反。正解制定规则如下:假设釆用第二种方法,当一个哲学家左右两边的筷子都可用时,才允许他抓起筷子。
信号量设置:定义互斥信号量数组Chopstick[5] = {l, 1, 1, 1, 1}用于对5个筷子的互斥访问。取筷子的信号量mutex,初值为1
类C语言代码
semaphore chopstick[5] = {1,1,1,1,1}; //初始化信号量
semaphore mutex=1; //设置取筷子的信号量
Pi(){ //i号哲学家的进程
while(1)
{
P (mutex) ; //在取筷子前获得互斥量
P (chopstick [i]) ; //取左边筷子
P (chopstick[ (i+1) %5]) ; //取右边筷子
V (mutex) ; //释放取筷子的信号量
eat; //进餐
V(chopstick[i] ) ; //放回左边筷子
V(chopstick[ (i+l)%5]) ; //放回右边筷子
think; // 思考
}
}
会发生死锁的算法
semaphore chopstick[5] = {1,1,1,1,1}; //初始化信号量
Pi(){ //i号哲学家的进程
while(1)
{
P (chopstick [i]) ; //取左边筷子
P (chopstick[ (i+1) %5]) ; //取右边筷子
eat; //进餐
V(chopstick[i] ) ; //放回左边筷子
V(chopstick[ (i+l)%5]) ; //放回右边筷子
think; // 思考
}
}
/*当每个哲学家同时拿起同一侧的筷子时,会发生死锁*/