Back to Blog

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)适合描述资源的可用数量,例如本例中的 emptyfull

方案设计

为满足“一写三读” 的约束,需要以下几类信号量:

信号量初始值作用
emptyn(本例取 1表示缓冲区空闲,可写入。A 在写前 P(empty),写完后 V(full)
full0表示缓冲区已填充,可读取。B/C/D 在读取前 P(full)
mutex1保护对共享缓冲区的互斥访问,防止读写冲突。
bcd1分别用于标记 B、C、D 是否已经完成本轮读取。A 在写入前必须 P(b)·P(c)·P(d),确保上一轮的三个读取全部结束。

同步流程概览

  1. 写进程 A

    • P(empty):确保缓冲区空。
    • P(b)·P(c)·P(d):等待 B、C、D 上一轮的 V 完成,保证它们已经读完。
    • P(mutex) → 写入 bufV(mutex):对缓冲区的写操作加互斥保护。
    • V(full):通知任意一个读取进程可以开始读取。
  2. 读取进程 B / C / D(三者逻辑相同)

    • P(full):等待写进程发布的 full
    • P(mutex) → 读取 bufV(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) 是否被执行,确认 fullempty 的对应关系。
B/C/D 进程一直阻塞在 p(full)A 未执行 v(full),或 full 初始值不为 0确认 A 的 v(full) 在写完后一定会执行;检查 full 的初始值是否正确。
数据竞争导致读到脏数据mutex 未正确保护读写确认每个进程在访问 buf 前后都执行了 p(mutex) / v(mutex),且没有遗漏。
死锁(所有进程都卡住)b、c、dVP 不匹配逐步打印每个信号量的计数变化,确保每一次 P 都对应一次 V

调试技巧

  • 在每个 pv 前后打印日志,例如 printf("A: P(empty) -> %d\n", get_sem_value(empty));,帮助观察信号量状态。
  • 使用系统提供的信号量查看工具(如 ipcssemctl)实时监控计数。
  • while(true) 改为有限循环或加入计数变量,便于在调试阶段快速定位问题。

小结

本文通过 四进程“一写三读” 的案例,展示了如何仅凭 P/V 信号量实现严格的生产者-消费者同步。核心思路在于:

  1. 使用 emptyfull 描述缓冲区的可写/可读状态。
  2. 引入 b、c、d 三个二值信号量,分别标记每个读取进程的完成情况。
  3. 通过 mutex 保证对共享缓冲区的互斥访问,防止竞争。

掌握这些基本模式后,工程师可以轻松扩展到 多生产者/多消费者多缓冲区 等更复杂的场景。希望本文的解析与代码示例能帮助读者快速上手信号量同步,构建可靠的并发系统。