文章目录
- 2.3_5 用信号量实现互斥、同步、前驱关系
- (一)信号量机制实现进程互斥
- (二)信号量机制实现进程同步
- (三)信号量机制实现前驱关系
- 总结
2.3_5 用信号量实现互斥、同步、前驱关系
我们之前学习了进程互斥的几种软件实现方式、硬件实现方式,但是这些实现方式都有一个共同的缺点——没有办法实现“让权等待”这一原则。
而信号量机制当中,设置了进程的阻塞、唤醒,就刚好可以解决“让权等待”这一问题。
所以,信号量机制是一种更先进的解决方式。
注:要注意理解信号量背后的含义,不要死盯着代码。一个信号量对应一种资源。
信号量的值 = 这种资源的剩余数量。信号量的值如果小于0,说明此时有进程在等待这种资源。
P(S) —— 申请一个资源S,如果资源不够就阻塞等待。
V(S) —— 释放一个资源S,如果有进程在等待该资源,则唤醒一个进程。
(一)信号量机制实现进程互斥
1.分析并发进程的关键活动,划定临界区。(即:哪段代码是用于访问临界资源的)
如:对临界资源——打印机的访问,就应放在临界区。
2.设置互斥信号量mutex,初值为1。
一个进程在进入临界区之前,需要对mutex执行P()操作;在执行完临界区之后,需要对mutex执行V()操作。以此来完成对临界区代码的互斥访问。
我们之前说过,信号量表示的就是某种临界资源,而mutex这个信号量,我们可以理解为“进入临界区的名额”这种资源。
mutex初值为1,就是表示刚开始时,可以进入临界区的名额只有1个。
而某个进程执行
P(mutex)
其实就是在表达“想要申请一个进入临界区的名额”。而这种资源如果还有剩余的话,这个进程就可以顺利进入临界区。此时如果还有另一个进程也想要P(mutex)
,那么它的申请就得不到满足,它必须阻塞等待。直到第一个进程使用完临界区之后,执行V(mutex)
归还这个名额,才可以把第二个进程唤醒。通过这种方式——对mutex进行P、V操作,就实现了对临界区的互斥访问。
3.在进入区P(mutex)——申请资源
4.在退出区V(mutex)——释放资源
注意1:我们若想定义一个记录型信号量
,则只需像semaphore mutex = 1;
这样简单定义一下即可。如果题目中没有特别要求,则不需要再把semaphore
具体的结构体定义给完整写出(如下)。当然,你自己要会写。
/* 记录型信号量的定义 */
typedef struct {
int value; //剩余资源数
struct process *L; //等待队列
}
将信号量定义为
记录型信号量
,即表示这个信号量本身就是带有排队、阻塞、唤醒
等一系列功能的,因此也不会产生“忙等”问题。
注意2:对不同的临界资源需要设置不同的信号量。
比如此时系统中,P1、P2进程需要访问打印机这种临界资源;而P3、P4进程需要访问摄像头这种临界资源。那在这种情况下,我们要给
访问打印机的临界区
设置一个信号量mutex1
,给访问摄像头的临界区
设置另一个信号量mutex2
。
注意3:P、V操作必须成对出现。缺少P(mutex)就不能保证临界资源的互斥访问。缺少V(mutex)会导致资源永不被释放,等待进程永不被唤醒。
(二)信号量机制实现进程同步
进程同步:要让各并发进程按要求有序地推进。
比如说有P1、P2这两个进程,当它们在系统当中并发地运行的时候,由于系统的环境很复杂,所以操作系统在调度的时候,有可能是P1先上处理机运行,也有可能是P2先上处理机运行……
比如,P2先上处理机运行了
代码4、代码5
,而此时它的时间片用完了,又切换到P1,P1上处理机运行了代码1、代码2
,之后它的时间片也用完了,又切换到P2,P2上处理机运行了代码6
……总之,由于这两个进程在系统中是并发地运行的,因此它们之间代码执行的先后顺序是我们不可预知的。而有的时候我们又必须让这些代码的执行顺序按照我们想要的那种方式进行。
例如,P1、P2并发执行,由于存在异步性,因此二者交替推进的次序是不确定的。
若P2的代码4
要基于P1的代码1和代码2
的运行结果才能执行,那么我们就必须保证代码4
一定是在代码2
之后才会执行。
这就是所谓的进程同步问题。即,让本来异步并发的进程互相配合,有序推进。
用信号量实现进程同步:
1.分析什么地方需要实现“同步关系”,即必须保证“一前一后”执行的两个操作(或两句代码)。
2.设置同步信号量S,初始为0。
如图,以进程P1、P2为例。
如果
代码4
必须在代码2
之后执行,那么P2需要在代码4
之前对S这个信号量执行一个P()操作,而P1需要在代码2
之后对S这个信号量执行一个V()操作。
分析1:
若P1先上处理机运行,执行了
代码1、代码2、V(S)
,那么此时S=1
。之后,再切换到P2上处理机运行,此时,P2进程执行
P(S)
时就可以顺利运行,并不会被阻塞,从而正常执行代码4
。分析2:
若P2先上处理机运行,则当它执行
P(S)
时就会被阻塞,从而没办法继续向下运行。而直到P1上处理机运行,运行了
代码1、代码2、V(S)
之后,才会唤醒此时正在等待信号量S的进程,即唤醒P2。
若先执行到V(S)操作,则S++后S=1。之后当执行到P(S)操作时,由于S=1,表示有可用资源,会执行S–,S的值变为0,P2进程不会执行block原语,而是继续往下执行代码4。
若先执行到P(S)操作,由于S=0,S–后S=-1,表示此时没有可用资源,因此P操作中会执行block原语,主动请求阻塞。之后当执行完代码2,继而执行V(S)操作,S++,使S变回0,由于此时有进程在该信号量对应的阻塞队列中,因此会在V操作中执行wakeup原语,唤醒P2进程。这样P2就可以继续执行代码4了。
理解:
信号量S代表某种“资源”,刚开始是没有这种资源的。P2需要使用这种资源,而又只能由P1产生这种资源。这样就实现了进程之间的同步关系。
此外,我们将该信号量初值设为0,表示刚开始这种资源是没有的。而只有执行“前操作”之后,释放了一个这种资源(“前V”),执行“后操作”之前去申请这种资源才能顺利得到(“后P”)。否则,“后操作”就会被阻塞,而即使是“后操作”被阻塞的状态下,也只有“前操作”之后的V()能够将其唤醒。——至此,我们便使用信号量机制实现了进程同步。
3.在“前操作”之后执行V(S)
4.在“后操作”之前执行P(S)
口诀:前V后P。
例如,代码4必须在代码2之后执行,因此代码2是“前操作”、代码4是“后操作”。因此,我们需要在代码2之后执行一个V(),在代码4之前执行一个P()。
(三)信号量机制实现前驱关系
是一种更复杂的进程同步问题。
进程P1中有句代码S1,P2中有句代码S2,P3中有句代码S3……P6中有句代码S6。
这些代码要求按照如上图所示的顺序来执行,即,S1执行完后,S2、S3才能执行;S2执行完后,S4、S5才能执行;S3、S4、S5执行完后,才能执行S6。
现在的问题是,如何使用信号量机制解决这么复杂的进程同步问题。
在这个前驱图中,其实每一对前驱关系(即每一条箭头)都是一个进程同步问题(需要保证一前一后的操作)。因此,
1.要为每一对前驱关系各设置一个同步信号量
注:同步信号量的初值为0。因为,这种资源必须刚开始是没有的,必须等“前操作”执行完后释放一个该信号量,“后操作”才能顺势执行,从而实现同步关系。
2.在“前操作”之后对相应的同步信号量执行V操作。
3.在“后操作”之前对相应的同步信号量执行P操作。
对于
S1 ---> S2
这个同步关系而言,由于S1执行之后,S2才能执行,因此我们要在“前操作”,即S1之后执行一个V(a);而在“后操作”,即S2之前执行一个P(a)。其它的同步关系也都同理,按照前V后P来执行即可。
经过这样的分析,对于这么复杂的同步关系,我们也可以很轻松的实现它们。
检验:这样的P、V操作,是否真的能正确实现如图所示的前驱关系?
例如,先让P5上处理机运行,会由于
P(d)
而发生阻塞。而之后,尽管能够执行V(d)的进程P2上处理机运行,但是P2也会由于P(a)
而发生阻塞……最终,除非P1先上处理机运行,执行V(a)
之后,S2才有可能执行,进而S5才有可能执行……