Appearance
信号量
考情分析
信号量和 PV 操作是进程同步章节的核心工具,几乎所有同步互斥的大题都基于信号量。这是 92 分同步章节中最核心的知识点。🔥🔥🔥 绝对核心。
锁只有"开/关"两种状态,只能解决互斥。但很多场景需要更细粒度的控制——比如缓冲区有 N 个空位,允许 N 个生产者同时放入,但第 N+1 个必须等。信号量就像停车场入口的计数牌——有空位才放行,没空位就排队等。
信号量的定义
信号量(Semaphore)是一个整型变量,只能通过两个原子操作来访问:
- P 操作(wait / proberen / 申请)
- V 操作(signal / verhogen / 释放)
记录型信号量
c
typedef struct {
int value; // 信号量的值
struct process *L; // 等待队列
} semaphore;P 操作
P(S) {
S.value--;
if (S.value < 0) {
将当前进程加入 S.L;
block(); // 阻塞当前进程
}
}V 操作
V(S) {
S.value++;
if (S.value <= 0) {
从 S.L 中唤醒一个进程;
wakeup(P);
}
}易错
S.value 的物理含义(选择题/简答题高频):
S.value > 0:有 S.value 个资源可用S.value = 0:没有资源可用,也没有进程等待S.value < 0:|S.value| 个进程正在等待该资源
常见错误:认为 S.value < 0 时绝对值表示"已分配的资源数"——错,表示的是等待进程数。
用信号量实现互斥
semaphore mutex = 1; // 初值为 1
// 进程 P1 // 进程 P2
P(mutex); P(mutex);
临界区; 临界区;
V(mutex); V(mutex);互斥信号量初值为 1(表示同一时刻最多 1 个进程进入临界区)。
用信号量实现同步
让 P1 的操作 A 在 P2 的操作 B 之前执行:
semaphore S = 0; // 初值为 0
// 进程 P1 // 进程 P2
操作 A; P(S); ← 等待 P1 完成 A
V(S); 操作 B; ← A 完成后才执行 B同步信号量初值为 0——「前 V 后 P」。
用信号量实现前驱关系
如果有更复杂的执行顺序要求(如 S1→S2, S1→S3, S2→S4, S3→S4):
为每条前驱边设置一个同步信号量(初值均为 0):
semaphore a=0, b=0, c=0, d=0;
P1: S1; V(a); V(b);
P2: P(a); S2; V(c);
P3: P(b); S3; V(d);
P4: P(c); P(d); S4;信号量的使用要点
| 要点 | 说明 |
|---|---|
| P/V 必须成对 | 漏写 P → 互斥失效;漏写 V → 死锁 |
| 互斥信号量初值为 1 | 表示一次只允许一个进程 |
| 同步信号量初值为 0 | 表示事件还没发生 |
| P/V 顺序不能错 | 有多个 P 操作时顺序至关重要(下篇详述) |
同步互斥大题的解题步骤
同步互斥大题看着千变万化,其实都能套同一套流程走。记住这五步,再陌生的题也有下手的地方:
- 找出各类进程和各类资源 —— 先理清有几"类"进程(生产者 / 消费者、读者 / 写者……)、几"类"资源(缓冲区、打印机……),别把同一类的当成不同的。
- 分析进程之间的同步、互斥关系 —— 谁和谁互斥(抢同一临界资源)、谁必须等谁先做(同步 / 前驱),顺带想清楚哪种交错会导致死锁。
- 定义信号量并确定初值 —— 一个关系配一个信号量:互斥信号量初值为 1,同步信号量初值为 0,资源信号量初值为资源个数。
- 写出伪代码 —— 把 P、V 放到各进程的正确位置:进临界区前 P、出临界区后 V;等事件的一方 P、做完事件的一方 V。
- 回头检查 —— P/V 是否成对?会不会死锁(尤其多个 P 的先后顺序)?是否完全符合题意?
最容易翻车的一步
第 3、4 步里,当一个进程要同时 P 多个信号量时,对互斥信号量 mutex 的 P 要放在最后(最贴近临界区)、先 P 资源信号量。顺序反了就可能"抱着锁去等资源"而死锁——下一篇的经典问题会反复用到这条。
本文对应考点
- 🔥🔥🔥 用信号量实现互斥(mutex=1, P-临界区-V)
- 🔥🔥🔥 用信号量实现同步(S=0, 前V后P)
- 🔥🔥🔥 S.value 的物理含义(正=剩余资源数,负=等待进程数)
- 🔥🔥 P/V 操作的原子性(通过关中断实现)
- 🔥 用信号量实现前驱关系
信号量很强大但编程容易出错。下一篇看管程和条件变量如何提供更安全的同步抽象。