是什么

  • 定义:多个并发进程通过互斥信号量(mutex)与多组资源信号量(empty/full)协同访问容量有限的共享缓冲区,满足“缓冲区互斥操作”与“特定产品供需同步”的问题模型。

  • 大白话:货架容量有限,生产商送货前得看有没有空位;送上架的商品分为奇数和偶数两种,买奇数的人只挑奇数,买偶数的人只挑偶数。为了防止多个人同时伸手把货架打翻,拿放动作还要单独上一把锁。

核心内容

1. 通用分析原理与四步解题法

遇到任何复杂的 PV 同步互斥大题,按以下四步流水线化推进:

第一步:找出所有进程角色与动作(划分实体)

  • 明确谁是生产方、谁是消费方,提取各自的核心动作(生产、放入、取出、消费)。

  • 本题角色:(通用生产)、(奇数消费)、(偶数消费)。

第二步:识别资源与产品类别(确定同步信号量)

  • 空闲槽位:无论放什么,只要占格子就需要申请空槽 设置 empty = N

  • 多类产品拆分:如果消费者对产品有针对性要求(如奇数/偶数、水果/信件),==绝对不能只用一个统一的 full==,必须为每类产品单独设立资源信号量:

    • 奇数产品数量 odd = 0

    • 偶数产品数量 even = 0

第三步:识别共享临界资源(确定互斥信号量)

  • 判断是否有两个及以上进程会并发访问同一块存储区域(如公用同一个缓冲区或指针)。

  • 题干声明“互斥使用一个包含 个单元的缓冲区” 设置 mutex = 1

第四步:推导执行链条与代码模板(套用防死锁骨架)

  • 核心铁律先申请资源(P操作),再申请互斥锁;先释放互斥锁,再释放资源(V操作)

  • 操作骨架

C

// 生产者模板
P(empty);           // 1. 占空位
P(mutex);           // 2. 拿互斥锁
put_item();         // 3. 临界区:放入数据
V(mutex);           // 4. 还互斥锁
if (条件A) V(full_A); // 5. 唤醒对应消费者
else V(full_B);

// 消费者模板
P(full_X);          // 1. 占专属商品
P(mutex);           // 2. 拿互斥锁
get_item();         // 3. 临界区:取出数据
V(mutex);           // 4. 还互斥锁
V(empty);           // 5. 腾出空位
consume();          // 6. 进程私有处理(放在临界区外!)

2. 信号量体系结构对照表

信号量名称信号量类型初始值作用与物理含义
mutex互斥信号量保证 互斥访问共享缓冲区
empty资源同步信号量表示缓冲区中当前剩余的空闲单元数量
odd资源同步信号量表示缓冲区中已存放且未被消费的奇数数量
even资源同步信号量表示缓冲区中已存放且未被消费的偶数数量

例题

  • 题目

    【2009 统考真题】三个进程 互斥使用一个包含 个单元的缓冲区。 每次用 produce() 生成一个正整数并用 put() 送入缓冲区某一空单元; 每次用 getodd() 从该缓冲区中取出一个奇数并用 countodd() 统计奇数个数; 每次用 geteven() 从该缓冲区中取出一个偶数并用 counteven() 统计偶数个数。请用信号量机制实现这三个进程的同步与互斥活动,并说明所定义的信号量的含义(要求用伪代码描述)。

  • 分析

    • 缓冲区大小为 ,空闲单元初值为 ;放入奇数唤醒 ,放入偶数唤醒 ,因此需要分离 oddeven 两个同步信号量。

    • 三者共享同一缓冲区,修改数据指针会冲突,因此需要 mutex 实现互斥。

    • 产生数字后进行奇偶判断,走不同的 V 操作分支。

  • 答案

C

// 1. 信号量定义及初值说明
semaphore mutex = 1; // 控制对缓冲区的互斥访问
semaphore empty = N; // 缓冲区中空闲单元的数量,初值为 N
semaphore odd = 0;   // 缓冲区中奇数的数量,初值为 0
semaphore even = 0;  // 缓冲区中偶数的数量,初值为 0

// 2. 进程伪代码描述
void P1() {
    int x;
    while (1) {
        x = produce();     // 生产一个正整数
        P(empty);          // 申请一个空闲缓冲区单元
        P(mutex);          // 申请缓冲区互斥锁
        put();             // 将 x 送入缓冲区某一空单元
        V(mutex);          // 释放缓冲区互斥锁
        if (x % 2 != 0) {
            V(odd);        // 放入奇数,奇数可用数+1
        } else {
            V(even);       // 放入偶数,偶数可用数+1
        }
    }
}

void P2() {
    while (1) {
        P(odd);            // 申请一个奇数
        P(mutex);          // 申请缓冲区互斥锁
        getodd();          // 从缓冲区取出一个奇数
        V(mutex);          // 释放缓冲区互斥锁
        V(empty);          // 释放一个空闲单元
        countodd();        // 统计奇数个数(放在临界区外)
    }
}

void P3() {
    while (1) {
        P(even);           // 申请一个偶数
        P(mutex);          // 申请缓冲区互斥锁
        geteven();         // 从缓冲区取出一个偶数
        V(mutex);          // 释放缓冲区互斥锁
        V(empty);          // 释放一个空闲单元
        counteven();       // 统计偶数个数(放在临界区外)
    }
}

⚠️ 易错点

Warning

  • P 操作顺序颠倒引发死锁(高频扣分点)

    • 必须先 P(empty)P(mutex)

    • 如果写反为 P(mutex); P(empty);:当缓冲区满时, 锁死 mutex 挂在 empty 队列上, 无法通过 P(mutex) 进入缓冲区取数,造成永久互相等待死锁。

  • 非共享操作塞入临界区(并发性能降级)

    • produce()countodd()counteven() 属于进程私有处理,==严禁放入 P(mutex)V(mutex) 之间==。放入临界区虽逻辑正确,但会无意义拉长阻塞时间,阅卷标准通常会扣除规范分。

  • 合并信号量导致假死

    • 切忌将 oddeven 合并为一个 full。如果只有一个 full,缓冲区只有偶数时 P2 可能会抢到锁并一直寻找奇数,导致队列假死。

1. semaphore mutex = 1; 里的 semaphore 是什么意思?

semaphore 是操作系统中用来表示“信号量”的变量类型(类似于 C 语言中的 intfloat)。

在标准 C 语言中并没有原生保留字叫 semaphore,但在 408 考研和经典操作系统教材(如汤子瀛《计算机操作系统》、王道)中,为了方便书写伪代码,通常用它来声明一个信号量对象。

它的底层结构实际上是一个包含数值等待队列的结构体:

C

typedef struct {
    int value;           // 资源计数器(表示当前可用资源数,负数表示排队等待的进程数)
    struct PCB *queue;   // 进程等待队列(因该资源不足而被阻塞的进程链表)
} semaphore;

semaphore mutex = 1;,本质上就是告诉阅卷老师:“我定义了一个初值为 1 的互斥信号量变量,名字叫 mutex”。

2. 为什么申请的是 oddP(odd)),释放的却是 emptyV(empty))?

这是因为“进出缓冲区”消耗的是不同的资源,产生了不同的结果。

  • 进入缓冲区前(消费者的前提条件)

    • 作为一个“奇数消费者”,它的需求是:缓冲区里必须有一个奇数给我取

    • 所以它必须先执行 P(odd)。如果此时缓冲区里只有偶数或者为空,odd == 0 就会老老实实挂起等待,不去瞎捣乱。

  • 离开缓冲区后(消费者的产出结果)

    • 成功取走了一个奇数,那块内存单元就空出来了。

    • 空间腾出来了,谁最需要这个空间?是生产者

    • 所以 必须执行 V(empty),让“空闲槽位数”加 1,并负责唤醒可能正因为缓冲区满了而被卡住的

一句话底层逻辑

消耗了一个奇数(所以减 odd),创造了一个空位(所以加 empty)。这并不是“同一种资源的配对”,而是两个进程间的交替供需闭环 耗空位供奇数, 耗奇数供空位。

3. countodd() 必须在 V(empty) 之后才能统计吗?

从逻辑正确性来说,不需要;但在考研答题和工程规范中,极度推荐放在最后。

  • 只要在 V(mutex) 之后就可以

    • countodd() 自己的私有统计动作(比如执行 total_odd++),它完全不触碰公用的共享缓冲区。

    • 只要出了缓冲区互斥区(即在 V(mutex) 之后),放在 V(empty) 之前或之后,程序逻辑都完全正确,绝不会发生死锁或数据错乱。

  • 为什么强烈建议放在 V(empty) 之后?

    • 让生产者尽早干活(提升并发度)

      你取完数据、出了临界区后,第一件利他事情就是赶紧 V(empty) 告诉生产者“有空位了,你快来放”,让生产者并发去跑。通知完之后,你再慢慢去算你的 countodd(),这样 CPU 和 I/O/内存 的并行效率最高。

    • 阅卷标准偏好

      考研阅卷老师最喜欢的标准模式就是:“解互斥锁 释放资源 私有处理”。把属于当前进程的私有耗时操作放在最外层,规范且不易被误扣步骤分。

P、V 操作是操作系统中用于实现进程同步与互斥的两个最核心的原语(Primitive)。 它们由荷兰计算机科学家 Dijkstra 提出,P 来自荷兰语 Proberen(意为测试/申请),V 来自荷兰语 Verhogen(意为增加/释放)。

原语的核心特性是原子性(Atomicity):执行过程中不可被打断,通常由硬件指令(如关中断、TSL 等)提供支持。

核心数据结构

一个记录型信号量 S 包含两个核心分量:

  • S.value:整型数值,表示当前可用资源的数量

  • S.L:等待队列指针,挂接因申请该资源不足而处于阻塞态的进程 PCB 链表

P 操作(Wait / 申请资源)

  • 伪代码定义

    C

    void P(semaphore S) {
        S.value--;               // 1. 资源数先减 1
        if (S.value < 0) {       // 2. 减完后若小于 0,说明资源不够用
            block(S.L);          // 3. 将当前进程设为阻塞态,挂入等待队列 S.L
        }
    }
    
  • 物理含义

    • 先扣减资源:S.value--

    • S.value >= 0:说明系统刚才还有空余资源,当前进程申请成功,继续往下执行

    • S.value < 0:说明资源已被耗尽,当前进程无法继续运行,系统执行 block 原语将其主动阻塞并挂入等待队列,让出 CPU。

V 操作(Signal / 释放资源)

  • 伪代码定义

    C

    void V(semaphore S) {
        S.value++;               // 1. 资源数先加 1
        if (S.value <= 0) {      // 2. 加完后若仍小于或等于 0,说明有进程在排队
            wakeup(S.L);         // 3. 从等待队列 S.L 中唤醒一个阻塞的进程
        }
    }
    
  • 物理含义

    • 先归还资源:S.value++

    • S.value > 0:说明等待队列里根本没有进程在排队,释放后资源留给后续进程直接取用。

    • S.value <= 0:说明在此之前队列里至少有一个进程正被卡住。系统调用 wakeup 原语从队列中唤醒一个阻塞进程,将其状态改为就绪态并放入就绪队列。

信号量数值的物理状态总结

S.value 的取值状态实际物理含义
S.value > 0系统中当前剩余的可用资源数量(等于该数值)。此时等待队列为空。
S.value = 0资源刚好全部分配完毕,系统中既无多余资源,也没有进程被阻塞
S.value < 0资源已耗尽,且其绝对值 精确等于当前正在等待队列中被阻塞的进程总数

经典应用模式对比

  • 实现互斥(临界区保护)

    • 信号量初值设为 mutex = 1)。

    • 紧挨临界区写成:P(mutex); [访问临界区]; V(mutex);

    • 在同一个进程内部成对出现

  • 实现同步(前驱后继关系)

    • 信号量初值设为 sync = 0)。

    • 规则是“必须先做操作 A,才能做操作 B”。

    • 进程 1(负责前驱动作):[执行 A]; V(sync);

    • 进程 2(负责后继动作):P(sync); [执行 B];

    • 分别分布在两个不同的进程中(前驱做完 V 释放,后继做前 P 检查)。