操作系统|信号量机制
前文我们讲完了软件、硬件两大类互斥实现方案,但不管是 Peterson 算法,还是 TSL、开关中断,全都卡在无法满足「让权等待」 这一条核心原则:进程拿不到锁时会循环忙等,死死占用 CPU 空耗算力。 而信号量机制 是操作系统标准解法,分为整型信号量 与记录型信号量两代,最终记录型信号量可以完整满足互斥四大准则,完美实现进程互斥与同步,也是 PV 操作经典题型的底层根基。
一、信号量基础概念铺垫
1. 信号量是什么
信号量本质就是用来统计系统可用资源数量的变量:
- 数值 = 当前还剩下多少份可用资源;
- 系统严格规定:信号量仅能执行三种操作:初始化、P 操作(申请资源)、V 操作(释放资源),不允许程序随意直接修改变量数值,从根源避免并发错乱。
2. PV 原语(Wait / Signal)
P 操作 = wait(S):向系统申请占用一份资源 ,对应进入区上锁; V 操作 = signal(S):使用完毕归还释放一份资源 ,对应退出区解锁。 P、V 属于原语,执行过程关中断、一气呵成不可被打断,杜绝多进程并发修改导致逻辑出错。
词源小知识:P 来自荷兰语
proberen(尝试、申请),V 来自verhogen(增加、释放)。
二、第一代:整型信号量
1. 结构定义
只用一个单独整型变量S.value代表剩余资源总数,仅依靠循环判断完成 P/V:
// P操作 申请资源
while(S.value <= 0); // 资源不足就一直循环轮询
S.value = S.value - 1;
// V操作 释放资源
S.value = S.value + 1;
2. 举例
公寓里有 1 台公用洗衣机(初始化 S=1)。 租客想洗衣服时执行 P 操作:如果洗衣机有人用(S≤0),就站在洗衣机门口不停反复查看、原地打转;直到别人用完 S>0,立刻占用洗衣机。 用完执行 V,把资源数 + 1 归还。
3. 缺陷
完全不满足「让权等待」原则 。 资源被占用时,进程不会放弃 CPU、不会进入阻塞休眠,而是无限 while 循环轮询查询状态,也就是忙等,CPU 大量算力浪费在无效循环上,系统资源利用率极低。
三、第二代:记录型信号量(
为了解决整型信号量忙等问题,给信号量新增阻塞等待队列,拿不到资源的进程直接休眠阻塞、让出 CPU,完美补齐四大互斥原则最后一块短板。
1. 结构体完整定义
typedef struct{
int value; // 剩余可用资源数量
struct process *L; // 阻塞等待该资源的进程队列
} semaphore;
value:资源计数器,正数 = 空闲资源;负数绝对值 = 当前正在排队等待的进程个数;L:队列,存放所有因缺资源而被挂起阻塞的进程。
2. P 操作(Wait 申请资源)
void wait(semaphore S){
S.value --; // 先主动扣减一份资源额度
if(S.value < 0){ // 扣完后资源不够,需要阻塞
block(S.L); // 进程自身进入阻塞态,插入等待队列,让出CPU
}
}
执行步骤拆解:
- 先把剩余资源数 - 1,代表我要占用一份;
- 如果减完之后
value<0,说明原本没有空闲资源,当前进程必须阻塞休眠,挂进信号量的等待队列,主动放弃处理机,不再占用 CPU 轮询。
3. V 操作(Signal 释放资源)
void signal(semaphore S){
S.value ++; // 先归还一份资源,计数器+1
if(S.value <= 0){ // 还有进程在队列里排队等待
wakeup(S.L); // 从等待队列队首唤醒一个进程,转为就绪态
}
}
执行步骤拆解:
- 使用完毕,资源计数器 + 1,归还占用的资源;
- 如果
value≤0,代表还有进程卡在队列里等待资源,操作系统唤醒队首一个阻塞进程,让它进入就绪队列等待调度上 CPU。
4. 举例
依旧是公寓 1 台洗衣机,初始化S.value=1,等待队列为空。
- 租客 A 执行 P:
value变为 0,没有小于 0,直接使用洗衣机; - 租客 B 紧接着执行 P:
value变成 - 1,触发block,B 不再站门口死等,回房间睡觉(阻塞),排入等待队列; - A 洗完执行 V:
value变回 0,此时value≤0,系统叫醒队列里的 B,B 从阻塞变就绪,后续调度使用洗衣机。
优势:拿不到资源的进程直接休眠,彻底告别忙等,CPU 可以调度其他无关进程执行,完全符合让权等待; 同时天然满足空闲让进、忙则等待、有限等待,
四、关键数值规律
- 初始化
S=n:代表系统一共有n 份同类临界资源; - P 一定先
value--,V 一定先value++,顺序绝对不能颠倒; S.value > 0:正数 = 当前还有多少空闲资源;S.value < 0:负数的绝对值 = 此刻正在等待队列里排队阻塞的进程数量;- V 操作只有
value++之后依旧≤0,才需要唤醒等待队列里的进程。
五、信号量两大核心用途
1. 实现进程互斥
互斥场景:同一资源同一时间只允许 1 个进程访问,信号量初值固定设为 1(二元信号量 / 互斥锁)。 模板写法:
semaphore mutex = 1;
// 进程进入临界区前
P(mutex);
临界区代码;
// 退出临界区后
V(mutex);
2. 实现进程同步
同步场景:约束多个进程固定先后执行顺序,信号量初值一般设为 0。 比如必须 A 进程先执行完,B 才能运行:
- 初始化
S=0 - A 进程末尾写
V(S) - B 进程开头写
P(S)B 会一直阻塞,直到 A 执行 V 释放信号量,B 才可以继续往下走,强制绑定执行先后顺序。
六、两代信号量核心对比
| 类型 | 数据结构 | 拿不到资源时行为 | 是否忙等 | 是否满足四大互斥原则 |
|---|---|---|---|---|
| 整型信号量 | 仅单个 int 变量 | while 循环轮询 | 是 | 缺少让权等待,不完整 |
| 记录型信号量 | 资源计数 + 阻塞队列 | 进程阻塞休眠、让出 CPU | 否 | 四条原则全部满足,标准方案 |
七、总结
- 信号量本质是资源计数器,P 申请减一,V 释放加一,PV 属于不可中断原语;
- 整型信号量存在忙等缺陷,记录型信号量新增等待队列,阻塞进程彻底解决 CPU 空转问题;
- P 先减后判断阻塞,V 先加后判断唤醒,牢记执行顺序是做题核心;
- 初值为 1 用于互斥,初值为 0 多用于同步,记录型信号量是操作系统解决进程同步互斥的通用底层机制。