信号量机制是一种功能较强的机制,可以用来解决互斥与同步问题,它只能被两个标准的原语
wait(S) 和 signal(S) 来访问,也可以记为“P操作”和“V操作”。
原语是指完成某种功能而不被分割和不被中断执行的操作序列,通常可由硬件来实现完成不被分割
执行特性的功能。如前述的“TestAndSet”和“Swap” 指令,就是由硬件实现的原子操作。
原语功能的不被中断执行特性在单处理机时可由软件通过屏蔽中断方法来实现。
原语之所以不能被中断执行,是因为原语对变量的操作过程如果被打断,可能会去运行另一个对同一个
变量的操作过程,从而出现临界段问题。如果能找到一种解决临界段问题的元方法,就可以实现对共享
变量操作的原子性。
1.整型信号量:
整型信号量被定义为一个用于表示资源数目的整型变量 S ,wait 和 signal 操作可描述为:
wait(S){
while(S<=0);
S = S - 1;
}
signal(S){
S = S + 1;
}
wait 操作中,只要信号量S <= 0 , 就会不断地测试。因此,该机制并未遵循“让权等待”的准则,
而是使进程处于“忙等”的状态。
2.记录型信号量:
记录型信号量是不存在“忙等”现象的进程同步机制。除了需要用一个代表资源数目的整型value
外,再增加一个进程链表L,用于链接所有等待该资源的进程,记录型信号量由于采用了记录型的数
据结构而得名的。记录型信号量可描述为:
typedef struct{
ine value;
struct process *L;
}semaphore;
相应的wait(S) 和 signal(S) 的操作如下:
void wait(semaphore S){ //相当于申请资源
S.value--;
if(S.value<0){
add this process to S.L;
block(S.L);
}
}
wait 操作,S.value-- ,表示进程请求一个该类资源,当 S.value < 0 时,表示该类资源已
分配完毕,因此进程应调用 block 原语,进行自我阻塞,放弃处理机,并插入到该类资源的等
待队列 S.L 中,可见该机制遵循了“让权等待”的准则。
void signal(semaphore S){ //相当于释放资源
S.value++;
if(S.value <= 0){
remove a process P from S.L;
wakeup(P);
}
}
signal 操作,表示进程释放了一个资源,使系统中可供分配的该类资源数增加1,故 S.value++。
若加1后仍是 S.value <= 0 ,则表示仍有等待该类资源的进程被阻塞,故还应调用wakeup 原语,
将 S.L 中的第一个等待进程被唤醒。
3.利用信号量实现同步:
信号量机构能用于解决进程间各种同步问题。设 S 为实现进程P1 、 P2 同步的公共信号量, 初
值为0 。进程 P2中的语句 y 要使用进程 P1 中语句 x 的运行结果,所以只有当语句 x 执行完成
之后语句y 才可以执行。其实现进程同步的算法如下:
semaphore = 0; //初始信号量
P1(){
...
x; //语句x
V(S); //告诉进程 P2 ,语句 x 已经完成
...
}
P2(){
...
P(S); //检查语句x 是否运行完成
y; //检查无误,运行y语句
...
}
若 P2 先执行到 P(S) 时,S 为 0 , 执行P C操作会把进程 P2阻塞,并放入阻塞队列中,当
进程P1中的 x 执行完后,执行 V 操作,把 P2 从阻塞队列中放回就绪队列中,当P2得到处理机
时,就得以继续执行。
4.利用信号量实现互斥:
信号量机制也能很方便地解决进程互斥问题。设 S 为实现进程 P1 和 P2 互斥的信号量,由于
每次只允许一个进程进入临界区,所以 S 的初始值为1(即可用资源数目是1)。只需要把临界区
P(S)和V(S)之间,即可实现两个进程对临界资源的互斥访问。其算法如下:
semaphore = 0 ; // 初始信号量
P1(){
...
P(S); //准备开始访问临界资源,加锁
进程P1的临界区;
V(S); //访问结束,解锁
...
}
P2(){
...
P(S); //准备开始访问临界资源,加锁
进程P2的临界区;
V(S); //访问结束,解锁
...
}
当没有进程进入临界区时,任意一个进程要进入临界区会执行 P操作,把 S 的值减为 0 ,然后
进入临界区,而当有进程存在于临界区时, S 的值为 0 ,再有进程要进入临界区时,执行 P
操作时将会被阻塞,直至在临界区中的进程退出,这样便实现了临界区的互斥。
互斥的实现是不同进程对同一个信号量进行 P、V 操作,一个进程在成功地对信号量执行了P
操作后进入临界区,并在退出临界区后,由该进程本身对该信号量执行 V 操作,表示当前没有
进程进入临界区,可以让其他进程进入临界区。
简单总结一下P、V操作在同步互斥中的应用:
》》在同步问题中,如果某个行为要用到某种资源,那么在那个行为前面P那种资源一下,
如果某个行为会提供某种资源,就在那个行为后面 V 那种资源一下。
》》在互斥问题中,P、V操作要紧紧夹着使用互斥资源的那个行为,中间不能有其他冗余代码。
5.利用信号量实现前驱关系:
信号量也可以用来描述程序之间或者语句之间的前驱关系。下面给出一个前驱图,其中S1 , S2 , S3, S4 , ..,S6 是最简单的程序段(只有一条语句)。
为使各程序段能正确执行,应设置若干个初始值为“0”的信号量 。例如,为保证 S1--->S2 , S1---->S3 的前驱关系,应分别设置信号量 a1 , a2 。同样,
为了保证 S2--->S4 , S2---->S5 , S3---->S6 , S4--->S6 , S5---->S6 应设置信号量 b1 、 b2 、 c 、 d 、 e 。
实现算法如下:
semaphore a1=a2=b1=b2=c=d=e=0; //初始化信号量
S1(){
...
V(a1);V(a2); // S1 已经运行完成
}
S2(){
p(a1); //检查S1是否运行完成
...
V(b1);V(b2); //S2已经运行完成
}
S3(){
P(a2); //检查S1是否运行完成
...
V(c); //S3已经运行完成
}
S4(){
P(b1); //检查S2是否运行完成
...
V(d); //S4已经运行完成
}
S5(){
P(b2); //检查S2是否已经运行完成
...
V(e); // S5已经运行完成
}
S6(){
P(c); //检查S3是否运行完成
P(d); //检查S4是否运行完成
P(e); //检查S5是否运行完成
}
6.分析进程同步和互斥问题的方法步骤:
(1)、关系分析。找出问题中的进程数,并且分析它们之间的同步和互斥关系。同步、互斥、前驱关系直接按照上面例子中的经典范式改写。
(2)、整理思路。找出解决问题的关键点,并且根据做过的题目找出解决问题的思路。根据进程的操作流程确定 P操作、V操作的大致顺序。
(3)、设置信号量。根据上面个两步,设置需要的信号量,确定初值,完善整理。