信号量机制的概念
用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而解决互斥与同步问题
信号量:其实就是一个变量(可以是一个整数,也可以是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量,比如:系统中只有一台打印机,就可以设置一个初值为1的信号量
一对原语:wait(S) 原语和 signal(S) 原语(也可记为“P操作”和"V操作”,分别写为 P(S)、V(S)),可以把原语理解为我们自己写的函数,函数名分别为 wait 和 signal,括号里的信号量 S 其实就是函数调用时传入的一个参数
两种信号量
上面介绍到:信号量其实就是一个变量(可以是一个整数,也可以是更复杂的记录型变量)
整型信号量
整型信号量:用一个整数型的变量作为信号量,用来表示系统中某种资源的数量
与普通整数型变量的区别:对信号量的操作只有三种,即 初始化、P操作、V操作
【例】某计算机系统中有一台打印机
int S = 1; //初始化整型信号量S,表示当前系统中可用的打印机资源数
void wait (int S){ //wait原语,相当于“进入区”
while (S <= 0); //如果资源数不够,就一直循环等待
S = S-1; //如果资源数够,则占用一个资源
}
void signal (int S){ //signal原语,相当于“退出区”
S = S +1; //使用完资源后,在退出区释放资源
}
优点:在 wait(S) 原语中,“检查”和“上锁”是一气呵成的,避免了并发、异步导致的问题
缺点:在整型信号量机制中的 wait 操作,只要信号量 S≤0 ,就会不断地测试,因此该机制并未遵循“让权等待”的原则,而是使进程处于“忙等”的状态
记录型信号量
记录型信号量机制是一种不存在“忙等”现象的进程同步机制,除了需要一个用于代表资源数目的整型变量 value外,再增加一个进程链表 L,用于链接所有等待该资源的进程
/*记录型信号量的定义*/
typedef struct {
int value;
struct process *L;
} semaphore;
相应的 wait(S) 和 signal(S) 的操作如下:
/*某进程需要使用资源时,通过 wait 原语申请*/
void wait (semaphore S){
S.value--;
if(S.value < 0){
block (S.L);
/*如果剩余资源数不够,
使用block原语使进程从运行态进入阻塞态,
并挂到信号量S的等待队列(即阻塞队列)中*/
}
}
/*某进程使用完资源后,通过 signal 原语释放*/
void signal (semaphore S) {
S.value++;
if(S.value <= 0) {
wakeup(S.L);
/*释放资源后,若还有别的进程在等待这种资源,
则使用wakeup原语唤醒等待队列中的一个进程,
该进程从阻塞态变为就绪态*/
}
}
利用信号量实现同步&互斥&前驱关系
实现进程互斥
信号量机制能很方便地解决进程互斥问题。设 S 为实现进程 P1、P2 互斥的信号量,由于每次只允许一个进程进入临界区,所以 S 的初值应为 1 (即可用资源数为1),只需把临界区置于 P(S) 和 V(S) 之间,即可实现两个进程对临界资源的互斥访问
算法如下:
semaphore S = 1; //初始化信号量
P1(){
...
P(S); //使用临界资源前要上锁
临界区代码...
V(S); //使用完临界资源后要解锁
...
}
P2(){
...
P(S);
临界区代码...
V(S);
...
}
当没有进程在临界区时,任意一个进程要进入临界区,就要执行 P 操作,把 S 的值减为 0,然后进入临界区;
当有进程存在于临界区时,S 的值为 0, 再有进程要进入临界区,执行 P 操作时将会被阻塞,直至在临界区中的进程退出,这样便实现了临界区的互斥
【注意】
① 对不同的临界资源需要设置不同的互斥信号量
② P、V 操作必须成对出现,缺少 P(S) 就不能保证临界资源的互斥访问,缺少 V(S) 会导致资源永不被释放,等待进程永不被唤醒
实现进程同步
信号量机制能用于解决进程间的各种同步问题。设 S 为实现进程 P1、 P2 同步的公共信号量, 初值为 0,进程P2 中的语句 y 要使用进程 P1 中语句 x 的运行结果,所以只有当语句 x 执行完成之后语句 y 才可以执行
算法如下:
semaphore S = 0; //初始化信号量
P1(){
x; //语句x
V(S); //告诉进程P2,语句x已经完成
...
}
P2(){
...
P(S); //检查语句x是否运行完成
y; //检查无误,运行y语句
...
}
【算法分析】
若先执行到 V(S) 操作,则 S++ 后 S=1,之后当执行到 P(S) 操作时,由于 S=1,表示有可用资源,会执行 S–,S的值变回 0,P2 进程不会执行 block 原语,而是继续往下执行 y 语句
若先执行到 P(S) 操作,由于S=0,S-- 后 S=-1,表示此时没有可用资源,因此 P 操作中会执行 block 原语,主动请求阻塞,之后当执行完 x 语句,继而执行 V(S) 操作,S++,使 S 变回 0,由于此时有进程在该信号量对应的阻塞队列中,因此会在 V 操作中执行 wakeup 原语,唤醒 P2进程,这样 P2 就可以继续执行 y 语句了
实现进程的前驱关系
实现进程的前驱关系可以理解为实现进程同步的进阶版本
【计算机操作系统】 专栏的文章 均有参考 《王道计算机考研 操作系统》 课程视频