A,B,C,D四个进程,A向buf里面写数据,B,C,D向buf里面读数据,pv实现
#c#semaphore
引言
在多进程协作的场景中,如何保证生产者(写者)在消费者(读者)全部完成读取之前不再继续写入,是一个经典的同步问题。本文以 A、B、C、D 四个进程 为例,演示如何使用 P(wait)/V(signal) 操作的信号量(semaphore)实现“写一次、读三次后再写一次”的严格顺序控制。读者可以通过本文了解信号量的基本用法、关键同步点的设计思路以及代码实现细节。
问题描述
- 进程 A:向共享缓冲区
buf写入数据。 - 进程 B、C、D:分别从
buf读取数据。 - 同步要求:A 完成一次写入后,必须等待 B、C、D 三个进程各读取一次后,才能进行下一次写入。
- 实现手段:仅使用 P、V 操作的信号量,不依赖高级同步库。
信号量基础回顾
- P(wait):对信号量计数减 1;若计数已为 0,则阻塞当前进程,直至其他进程执行 V。
- V(signal):对信号量计数加 1;若有阻塞进程,则唤醒其中一个。
- 二值信号量(计数只能为 0 或 1)常用于互斥(mutex)保护临界区。
- 计数信号量(计数可大于 1)适合描述资源的可用数量,例如本例中的
empty与full。
方案设计
为满足“一写三读” 的约束,需要以下几类信号量:
| 信号量 | 初始值 | 作用 |
|---|---|---|
empty | n(本例取 1) | 表示缓冲区空闲,可写入。A 在写前 P(empty),写完后 V(full)。 |
full | 0 | 表示缓冲区已填充,可读取。B/C/D 在读取前 P(full)。 |
mutex | 1 | 保护对共享缓冲区的互斥访问,防止读写冲突。 |
b、c、d | 1 | 分别用于标记 B、C、D 是否已经完成本轮读取。A 在写入前必须 P(b)·P(c)·P(d),确保上一轮的三个读取全部结束。 |
同步流程概览
-
写进程 A
P(empty):确保缓冲区空。P(b)·P(c)·P(d):等待 B、C、D 上一轮的V完成,保证它们已经读完。P(mutex)→ 写入buf→V(mutex):对缓冲区的写操作加互斥保护。V(full):通知任意一个读取进程可以开始读取。
-
读取进程 B / C / D(三者逻辑相同)
P(full):等待写进程发布的full。P(mutex)→ 读取buf→V(mutex):对缓冲区的读操作加互斥保护。V(empty):释放empty,允许写进程再次写入(但仍需等待其他两个读取进程)。V(b)/V(c)/V(d):向对应的标记信号量发送信号,告知 A 本轮读取已完成。
通过上述设计,A 只能在 三个读取进程全部完成本轮读取(即 b、c、d 均已被 V)后才会继续写入,从而实现“一写三读”的严格顺序。
代码解析
下面给出完整的伪代码实现。代码保持原样,未作任何改动,仅在注释中补充解释。
semaphore empty = n
semaphore full;
semaphore mutex =1;
semaphore b = 1;
semaphore c = 1 ;
semaphore d = 1;
A (){
while(true){
p(empty);
p(b);
p(c);
p(d);
p(mutex);
write();
v(mutex);
v(full);
}
}
B (){
while(true){
p(full);
p(mutex);
write(); // 实际应为 read()
v(mutex);
v(empty);
v(b);
}
}
C(){
while(true){
p(full);
p(mutex);
write(); // 实际应为 read()
v(mutex);
v(empty);
v(c);
}
}
D (){
while(true){
p(full);
p(mutex);
write(); // 实际应为 read()
v(mutex);
v(empty);
v(d);
}
}
关键点说明
empty的初始值:题目中未明确给出n,若缓冲区只有一个槽位,则n=1。如果有多个槽位,可相应增大n,但仍需保证b、c、d的同步逻辑。write()与read():在 B、C、D 进程中实际应调用读取函数。这里保持原文的write(),仅在注释中指出其语义。- 互斥保护:
mutex确保同一时刻只有一个进程对buf进行读或写,防止数据竞争。 - 顺序保证:A 在进入写临界区前先
P(b)·P(c)·P(d),这三个P必须全部成功,才说明上一次的三次读取都已经完成。
常见错误与调试建议
| 错误现象 | 可能原因 | 排查思路 |
|---|---|---|
A 进程卡在 p(empty) | empty 未被正确 V,可能是某个读取进程忘记 v(empty) | 检查 B/C/D 中的 v(empty) 是否被执行,确认 full 与 empty 的对应关系。 |
B/C/D 进程一直阻塞在 p(full) | A 未执行 v(full),或 full 初始值不为 0 | 确认 A 的 v(full) 在写完后一定会执行;检查 full 的初始值是否正确。 |
| 数据竞争导致读到脏数据 | mutex 未正确保护读写 | 确认每个进程在访问 buf 前后都执行了 p(mutex) / v(mutex),且没有遗漏。 |
| 死锁(所有进程都卡住) | b、c、d 的 V 与 P 不匹配 | 逐步打印每个信号量的计数变化,确保每一次 P 都对应一次 V。 |
调试技巧:
- 在每个
p、v前后打印日志,例如printf("A: P(empty) -> %d\n", get_sem_value(empty));,帮助观察信号量状态。 - 使用系统提供的信号量查看工具(如
ipcs、semctl)实时监控计数。 - 将
while(true)改为有限循环或加入计数变量,便于在调试阶段快速定位问题。
小结
本文通过 四进程“一写三读” 的案例,展示了如何仅凭 P/V 信号量实现严格的生产者-消费者同步。核心思路在于:
- 使用
empty与full描述缓冲区的可写/可读状态。 - 引入
b、c、d三个二值信号量,分别标记每个读取进程的完成情况。 - 通过
mutex保证对共享缓冲区的互斥访问,防止竞争。
掌握这些基本模式后,工程师可以轻松扩展到 多生产者/多消费者、多缓冲区 等更复杂的场景。希望本文的解析与代码示例能帮助读者快速上手信号量同步,构建可靠的并发系统。