在操作系统中,多个进程并发执行时,往往存在两种基本关系:一是互斥,即多个进程需要共享独占性资源,同一时刻只允许一个进程访问;二是同步,即两个异步进程之间需要协作,一方需等待另一方完成特定动作后才能继续。为了有效处理这两种情况,荷兰计算机科学家艾兹赫尔·戴克斯特拉(E. W. Dijkstra)于1965年提出了信号量(Semaphore)PV操作,成为并发编程领域的基石。

一、信号量与PV操作的概述

信号量是一种特殊的变量,其内部包含一个整型值 S 和一个进程等待队列。对信号量的操作只有两种:P操作和V操作(名称来源于荷兰语“Proberen”(测试)和“Verhogen”(增加))。

  • P操作:执行 S = S - 1。若 S < 0,表示当前没有可用的资源,则执行该操作的进程暂停(阻塞),并被放入等待队列。
  • V操作:执行 S = S + 1。若 S ≤ 0,表示阻塞队列中有进程正在等待该资源,则唤醒等待队列中的第一个进程。

通过这两个简单的原语,PV操作为解决进程间的互斥与同步问题提供了统一而强大的工具。

二、PV操作的理解要点

要灵活运用PV操作,需要把握以下几个关键原则:

  1. 寻找关系:在并发问题中,互斥与同步是两大核心关系,解题时应首先识别。通常,一个互斥关系或一个同步关系对应一个信号量。
  2. 初值设定:信号量的初值通常表示可用资源的数量。对于初值为0的信号量,往往需要先执行V操作(例如在同步模型中,先到达的进程因P操作而阻塞,等待另一方执行V操作)。
  3. 成对出现:在使用资源的临界区之前执行P操作(申请资源),之后执行V操作(释放资源)。在互斥关系中,PV操作在同一进程中成对出现;而在同步关系中,PV操作在两个或多个进程中成对出现。

三、利用PV操作实现互斥控制

互斥控制的目的是保护共享资源(即临界资源),防止多个进程同时访问。相应的代码段称为临界区,一次只允许一个进程进入。

实现互斥的代码模板如下:

P(mutex);   // 进入临界区前申请
临界区代码;
V(mutex);   // 离开临界区后释放

此时,信号量 mutex 的初值应设为 1,表示只允许一个进程进入。当有进程在临界区内时,mutex = 0;若其他进程尝试进入,执行P操作后 mutex 变为负数,其绝对值表示等待队列中的进程数。当临界区内的进程执行V操作后,若 mutex ≤ 0,则唤醒等待队列中的一个进程。

四、利用PV操作实现同步控制

最简单的同步形式是:进程A必须在进程B到达某一点(如L2)之后,才能越过它自己的某一点(如L1)。这可以通过初值为0的信号量实现:

// 进程A                // 进程B
...                     ...
P(S);   // 等待信号      V(S);   // 发出信号
// 越过L1点             // 到达L2点

若进程A先到达L1,执行P操作后 S = -1,进程A阻塞;直到进程B执行V操作,S 变为0,唤醒进程A继续运行。这样就保证了严格的先后顺序。

五、经典案例:生产者-消费者问题

生产者-消费者问题是同步与互斥结合的典型范例。生产者进程生成数据放入缓冲区,消费者进程从缓冲区取出数据。问题要求:缓冲区满时生产者需等待,缓冲区空时消费者需等待,且对缓冲区的读写必须互斥。

通常需要三个信号量:

信号量 类别 功能说明 初值
empty 同步 空闲缓冲区数量 缓冲区总数
full 同步 已填充缓冲区数量 0
mutex 互斥 保证同时只有一个进程访问缓冲区 1

生产者执行流程:
P(empty)P(mutex) → 写入缓冲区 → V(mutex)V(full)

消费者执行流程:
P(full)P(mutex) → 读取缓冲区 → V(mutex)V(empty)

若无需对缓冲区的读写进行互斥控制(例如只有一个生产者和一个消费者),则可省略 mutex 信号量。

六、结语

信号量与PV操作是理解操作系统并发控制的重要桥梁。它们以简洁的语义——测试(P)与增加(V)——实现了对共享资源的有效管理。从互斥锁到复杂的进程同步,从生产者-消费者到读者-写者问题,PV操作的思想贯穿始终。掌握这一工具,对于编写正确、高效的多线程/多进程程序具有重要意义。
在这里插入图片描述


信号量与PV操作:从入门到生产者-消费者问题

前言

在操作系统的进程管理中,并发执行的两个或多个进程之间经常会出现两种关系:

  • 互斥:多个进程需要共享某个独占性资源(如打印机、共享内存),同一时刻只允许一个进程访问。
  • 同步:两个异步进程之间需要协作,一方必须等待另一方完成某个动作后才能继续(如生产者生产数据后消费者才能消费)。

为了有效处理这两种情况,荷兰计算机科学家Dijkstra于1965年提出了信号量(Semaphore)PV操作。直到今天,这一机制仍然是并发编程的基石。

本文将用通俗易懂的方式,带你全面理解信号量与PV操作,并最终掌握经典的生产者-消费者问题。


一、什么是信号量与PV操作?

1. 信号量(Semaphore)

信号量是一种特殊的变量,它包含两个部分:

  • 一个整型值 S(表示可用资源的数量)
  • 一个进程等待队列(用于存放被阻塞的进程)

2. P操作(wait操作)

P(S) {
    S = S - 1;
    if (S < 0) {
        // 没有可用资源,当前进程进入等待队列并阻塞
    }
}

3. V操作(signal操作)

V(S) {
    S = S + 1;
    if (S <= 0) {
        // 等待队列中有进程,唤醒其中的第一个
    }
}

注意:P和V是荷兰语中“测试”(Proberen)和“增加”(Verhogen)的缩写,很多教材中也记为 wait()signal()


二、PV操作的理解要点

在解题或实际编码时,牢记以下几点会非常有帮助:

  1. 先找关系:每个并发问题中,先识别出哪些是互斥关系,哪些是同步关系。通常一个关系对应一个信号量。
  2. 初值含义:信号量的初值通常表示资源的初始可用数量
    • 互斥信号量初值 = 1(二值信号量)
    • 同步信号量初值 = 0(等待另一方先唤醒)
    • 资源计数信号量初值 = 资源总数(如缓冲区数量)
  3. P、V成对出现
    • 互斥关系中,P和V出现在同一个进程的临界区前后。
    • 同步关系中,P和V出现在两个或多个进程中,成对使用。
  4. 使用口诀:资源使用前 P,资源使用后 V

三、利用PV操作实现互斥控制

什么是互斥?

当多个进程需要访问共享资源(也称临界资源)时,我们不允许它们同时进入访问该资源的代码段。这段代码称为临界区。互斥控制就是一次只允许一个进程进入临界区

代码模板

P(mutex);      // 进入临界区前申请
临界区代码;
V(mutex);      // 离开临界区后释放
  • 信号量 mutex 的初值设为 1
  • 如果 mutex 的值变为负数,其绝对值表示等待进入临界区的进程数。
  • 当进程离开临界区执行 V(mutex) 后,如果 mutex ≤ 0,则唤醒等待队列中的一个进程。
小例子:两个进程打印数据
semaphore mutex = 1;

void process_A() {
    P(mutex);
    // 打印共享数据
    V(mutex);
}

void process_B() {
    P(mutex);
    // 打印共享数据
    V(mutex);
}

这样就能保证两个进程不会同时打印,避免了输出内容交错的问题。


四、利用PV操作实现同步控制

什么是同步?

最简单的同步形式:进程A必须在进程B到达某个点之后,才能越过它自己的某个点

例如:B从缓冲区取出数据之前,A必须先生产好数据;或者B将数据准备好之前,A不能开始处理。

同步模型

// 进程A                     // 进程B
...                          ...
P(S);   // 等待信号           V(S);   // 发出信号
// 越过L1点(继续执行)       // 到达L2点
  • 信号量 S 的初值设为 0
  • 如果A先执行到 P(S),由于 S = 0,执行 S = S - 1S = -1,A被阻塞。
  • 当B执行到 V(S) 时,S = S + 1 = 0,此时 S ≤ 0 成立,于是唤醒A。
  • 被唤醒的A从 P(S) 处继续执行,越过L1点。

这样就实现了 A 在 B 之后执行 的同步需求。


五、经典案例:生产者-消费者问题

问题描述

  • 有一个缓冲区(可以看作一个数组或队列),大小为 N
  • 生产者进程负责生产数据,放入缓冲区。
  • 消费者进程负责从缓冲区取出数据,进行消费。
  • 需要满足的条件:
    1. 当缓冲区满时,生产者不能继续放入(必须等待消费者取走)。
    2. 当缓冲区空时,消费者不能继续取出(必须等待生产者放入)。
    3. 对缓冲区的访问必须互斥(不能同时有生产者和消费者在读写缓冲区)。

信号量设计

通常需要三个信号量

信号量 类型 含义 初值
empty 同步 空闲缓冲区数量 N
full 同步 已占用的缓冲区数量 0
mutex 互斥 保证缓冲区读写互斥 1

如果只有一个生产者和一个消费者,且缓冲区操作不会发生冲突(例如只有一个指针),有时可以省略 mutex。但在一般多生产者多消费者场景下,mutex 是必需的。

生产者代码

void producer() {
    while (true) {
        // 生产一个数据 item
        P(empty);       // 申请一个空闲缓冲区
        P(mutex);       // 互斥访问缓冲区
        // 将 item 放入缓冲区
        V(mutex);       // 释放互斥锁
        V(full);        // 填充数加1,并可能唤醒消费者
    }
}

消费者代码

void consumer() {
    while (true) {
        P(full);        // 检查是否有已填充的缓冲区
        P(mutex);       // 互斥访问缓冲区
        // 从缓冲区取出一个数据 item
        V(mutex);       // 释放互斥锁
        V(empty);       // 空闲缓冲区数量加1
        // 消费 item
    }
}

工作原理小剧场

  • 初始empty = Nfull = 0mutex = 1
  • 若生产者先运行:P(empty)empty 减1,申请到一个空闲缓冲区;P(mutex) 获得访问权;放入数据;V(mutex) 释放;V(full)full 加1,此时若消费者因 P(full) 被阻塞,则会被唤醒。
  • 若消费者先运行:P(full)full = 0full 变成 -1,消费者阻塞;直到生产者执行 V(full) 后唤醒消费者。

这样就完美实现了同步 + 互斥。


六、小结与面试点睛

知识点 要点
信号量 一个整型值 + 一个等待队列
P操作 S--,若 S<0 则阻塞
V操作 S++,若 S≤0 则唤醒一个等待进程
互斥实现 同一进程中 P(mutex) / V(mutex)mutex 初值=1
同步实现 不同进程中 P(S)V(S) 成对出现,S 初值=0
生产者-消费者 三个信号量:emptyfullmutex

常见面试题

  • 能否只用两个信号量实现生产者-消费者?
    → 如果只有一个生产者和一个消费者,且缓冲区操作天然互斥(例如单指针),可以省略 mutex。否则必须三个。
  • 信号量初值为什么有时是1有时是0?
    → 1 表示互斥锁(一次只允许一个进程);0 表示同步(需要等待对方先发信号)。

写在最后

信号量与PV操作是操作系统课程中一块“难啃的骨头”,但只要抓住了 P是申请资源、V是释放资源 这个核心,再结合互斥与同步的区分,就能一步步理解透彻。

希望这篇文章能帮助你彻底掌握信号量与PV操作。如果觉得有收获,欢迎点赞、评论、收藏,也欢迎在评论区留下你的疑问或补充。

推荐阅读

  • 《操作系统概念》(Silberschatz 著)
  • Dijkstra 原著论文 “Cooperating Sequential Processes”

#操作系统 #信号量 #PV操作 #生产者消费者 #并发编程

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐