目录

一、进程同步与互斥的核心概念

1.1 互斥

1.2 同步

1.3 临界资源与临界区

(1)临界资源

(2)临界区访问的四个阶段

二、进程同步机制的设计准则

2.1 四大基本准则

2.2 死等与忙等的核心区别

三、临界区互斥的软件实现方案

3.1 单标志法

3.2 双标志先检查法

3.3 双标志后检查法

3.4 Peterson 算法

3.5 软件实现的整体局限性

四、临界区互斥的硬件实现方案

4.1 关中断

4.2 TestAndSet(TS)指令

4.3 Swap 指令

五、信号量机制核心原理

5.1 信号量与 PV 操作定义

5.2 整型信号量

5.3 记录型信号量

5.4 用信号量实现进程互斥

5.5 用信号量实现进程同步

六、经典问题实战:生产者 - 消费者模型

6.1 通用解题四步法

6.2 问题描述与分析

问题描述

问题分析

6.3 完整 PV 操作实现

6.4 易错点避坑指南

七、各类实现方案的对比与适用边界

八、本章总结与思考

互动思考

写作说明


在多道程序设计环境中,系统内同时存在多个并发执行的进程,它们共享系统中的硬件、软件资源,也会相互协作完成复杂任务。如果不对进程的资源访问和执行顺序加以约束,就会出现数据不一致、资源竞争冲突、进程饥饿甚至死锁等问题,最终导致系统运行异常。

本文要解决的核心问题是:如何通过合理的机制,保证多进程并发场景下临界资源的安全共享,以及协作进程的执行顺序正确。全文按照: 概念定义→设计准则→软件实现→硬件实现→信号量机制→经典问题实战→方案对比

一、进程同步与互斥的核心概念

1.1 互斥

互斥的核心特征是排他性访问,无固定执行顺序。当多个进程竞争同一个临界资源时,同一时刻只能有一个进程占用该资源;进程之间没有严格的先后执行要求,谁先申请到资源谁先使用。

常见的互斥场景包括:公共卫生间坑位、办公室共享打印机、多进程共享的全局余额变量balance等。从本质上看,互斥是进程间的间接制约关系,源于对临界资源的竞争,属于竞争关系。

1.2 同步

同步的核心特征是顺序性执行,有严格先后依赖。多个进程为了完成协作任务,存在明确的执行先后顺序,后续操作必须等待前置操作完成后才能执行,执行顺序不能颠倒或提前。

常见的同步场景包括:施工队必须先打地基再盖楼、生产者生产产品后消费者才能消费、道口栏杆放行后车辆才能通行等。从本质上看,同步是进程间的直接制约关系,源于进程之间的任务协作,属于合作关系。

为了直观区分二者,我们通过下图进行对比:

image

1.3 临界资源与临界区

(1)临界资源

临界资源指一个时间段内只允许一个进程使用的资源,既可以是打印机、IO 设备等硬件资源,也可以是全局变量、共享缓冲区、文件数据等软件资源。 临界资源的核心特点可以总结为:多进程共享 + 单进程独占,所有进程必须以互斥的方式共享这类资源。

(2)临界区访问的四个阶段

一个进程访问临界资源的完整代码逻辑,可以划分为 4 个标准部分:

void process() {
    while (TRUE) {
        // 1. 进入区:检查是否可以进入临界区,若可以则设置访问标志
        entry_section();
        
        // 2. 临界区:真正访问临界资源的代码段,是互斥保护的核心
        critical_section();
        
        // 3. 退出区:清除访问标志,释放临界资源
        exit_section();
        
        // 4. 剩余区:进程中与临界资源无关的其他业务代码
        remainder_section();
    }
}

二、进程同步机制的设计准则

所有实现进程互斥与同步的机制,都需要遵循统一的设计准则,以此保证机制的正确性、公平性与高效性。

2.1 四大基本准则

  1. 空闲让进:当临界资源处于空闲状态时,应当立即允许申请资源的进程进入临界区,最大化提升资源利用率。
  2. 忙则等待:当临界资源正在被访问时,其他申请该资源的进程必须进入等待状态,严格保证临界资源的互斥访问。
  3. 有限等待:进程必须在有限的时间内进入临界区,不能无限期等待下去,避免出现进程 "饥饿" 现象。
  4. 让权等待:如果进程当前无法进入临界区,应当立即释放 CPU 资源,进入阻塞状态,避免占用 CPU 空转,提升 CPU 整体利用率。

2.2 死等与忙等的核心区别

初学者很容易混淆 "死等" 与 "忙等" 两种不良等待状态,二者在 CPU 占用、等待性质上有本质区别,具体对比如下:

表格

等待类型 别名 核心表现 核心特点
死等 阻塞等待 / Dead Waiting 进程无法获得资源,被无限期阻塞,没有机制保证其能在有限时间内获取资源,最终陷入永久等待 不占用 CPU,被动等待,等待无上限
忙等 自旋等待 / Busy Waiting 进程不释放 CPU,持续循环检查条件是否满足,直到条件成立才继续执行,不会因条件不满足进入阻塞态 占用 CPU,主动轮询,可能无限等待

简单来说:死等是 "睡过去等,不知道什么时候能醒";忙等是 "站在原地反复看,一直占用资源"。


三、临界区互斥的软件实现方案

早期操作系统通过纯软件算法,在用户态实现临界区的互斥访问。这类方案主要针对双进程场景,通过共享标志位协调进程执行顺序,是理解同步互斥逻辑的基础。

3.1 单标志法

  • 核心思想:设置一个共享全局变量turn作为标志位,用来表示当前允许哪个进程进入临界区。只有当turn等于自身进程编号时,进程才能进入临界区;退出临界区后,将turn修改为对方进程编号。
  • 存在问题:严重违背 "空闲让进" 准则。即使临界资源完全空闲,只要turn不属于当前进程,该进程也无法进入,资源利用率极低。

3.2 双标志先检查法

  • 核心思想:设置布尔型数组flag[]flag[i]表示进程 i 想要进入临界区的意愿。每个进程进入临界区前,先检查是否有其他进程想进入;如果没有,就将自身的flag设为true,然后访问临界区。
  • 伪代码实现
    // 进程i的执行逻辑
    while (flag[j] == TRUE);  // 第一步:检查对方是否想进入临界区
    flag[i] = TRUE;            // 第二步:标记自己想要进入
    critical_section();        // 访问临界资源
    flag[i] = FALSE;           // 退出临界区,取消标记
    
  • 存在问题:违背 "忙则等待" 准则,两个进程可能同时通过检查步骤,同时进入临界区,破坏互斥性;同时存在忙等问题,违背让权等待。

3.3 双标志后检查法

  • 核心思想:针对先检查法的并发漏洞,改为 "先上锁,后检查" 的逻辑 —— 进程先将自身的flag设为true表示想要进入,再检查对方进程的标志,如果对方也想进入就循环等待。
  • 伪代码实现
    // 进程i的执行逻辑
    flag[i] = TRUE;            // 第一步:先标记自己想进入临界区
    while (flag[j] == TRUE);  // 第二步:再检查对方是否想进入
    critical_section();        // 访问临界资源
    flag[i] = FALSE;           // 退出临界区,取消标记
    
  • 存在问题:违背 "空闲让进" 与 "有限等待" 准则。两个进程可能同时上锁,互相等待,谁都无法进入临界区,造成资源空闲但无进程访问的情况,同时可能导致进程饥饿。

3.4 Peterson 算法

  • 核心思想:综合双标志先检查和后检查的优点,在双标志的基础上,增加一个turn变量表示 "谦让权"。进程进入前先标记自身意愿,再主动谦让对方,最后检查对方是否想进入且当前轮到对方,如果是则等待,否则进入临界区。
  • 伪代码实现
    // 进程i的执行逻辑
    flag[i] = TRUE;            // 标记自己想要进入临界区
    turn = j;                  // 主动谦让,让对方进程优先进入
    while (flag[j] == TRUE && turn == j);  // 对方想进且轮到对方,则循环等待
    critical_section();        // 访问临界资源
    flag[i] = FALSE;           // 退出临界区,取消标记
    
  • 算法评价:完美遵循 "空闲让进"" 忙则等待 ""有限等待" 三大准则,解决了互斥破坏和进程饥饿问题;但依然违背 "让权等待" 准则,存在忙等问题。

四种软件方案的演进逻辑与缺陷可以通过下图直观梳理:

3.5 软件实现的整体局限性

  1. 始终无法解决 "忙等" 问题,CPU 利用率低,执行效率差;
  2. 依赖对进程执行顺序的假设,鲁棒性差,复杂并发场景下容易出现逻辑漏洞;
  3. 绝大多数方案针对双进程设计,扩展性差,难以直接应用于多进程场景;
  4. 依赖底层运行环境,缺乏通用性,不同硬件架构下执行表现不一致。

四、临界区互斥的硬件实现方案

针对软件方案的固有缺陷,硬件层面提供了更简单、高效的互斥实现,核心思路是保证检查和修改操作的原子性—— 操作要么全部执行,要么全部不执行,中间不会被进程调度打断。

4.1 关中断

  • 核心思想:进程进入临界区前,先关闭 CPU 的外部中断,保证临界区代码执行过程中不会被调度中断;退出临界区后再开启中断。
  • 实现逻辑
    disable_interrupts();  // 关中断,禁止进程调度
    critical_section();    // 访问临界区,全程不会被打断
    enable_interrupts();   // 开中断,恢复进程调度能力
    
  • 优缺点
    • 优点:实现逻辑最简单,能彻底保证临界区代码的原子性;
    • 缺点:操作权限高,仅内核态可使用,用户态无法调用;关中断时间过长会影响系统中断响应;不适用于多处理机系统。

4.2 TestAndSet(TS)指令

TestAndSet 是一条硬件原子指令,能一次性完成 "读取标志→判断→修改标志" 的完整操作,执行过程不可被打断。

  • 指令底层逻辑(伪代码)
    bool TestAndSet(bool *lock) {
        bool old_value = *lock;
        *lock = TRUE;
        return old_value;
    }
    
  • 互斥实现逻辑
    while (TestAndSet(&lock));  // 循环申请锁,直到锁处于空闲状态
    critical_section();         // 访问临界资源
    lock = FALSE;               // 访问完成,释放锁
    
  • 优缺点
    • 优点:实现简单,适用于多处理机环境,无需复杂的软件逻辑协调;
    • 缺点:不满足 "让权等待" 准则,暂时无法进入临界区的进程会循环执行 TS 指令,陷入忙等。

4.3 Swap 指令

Swap 指令也是一条硬件原子指令,作用是原子性地交换两个变量的值,核心逻辑与 TS 指令一致,只是实现形式不同。

  • 指令底层逻辑(伪代码)
    void Swap(bool *a, bool *b) {
        bool temp = *a;
        *a = *b;
        *b = temp;
    }
    
  • 互斥实现逻辑
    bool key = TRUE;
    do {
        Swap(&lock, &key);  // 原子交换锁变量和key变量
    } while (key == TRUE);  // 若key为TRUE,说明锁被占用,继续循环等待
    critical_section();     // 访问临界资源
    lock = FALSE;           // 访问完成,释放锁
    
  • 本质说明:Swap 是标准机器指令,本身具备原子性,CPU 会在执行完这条指令后再检查是否需要切换进程,因此整个交换操作不会被打断。

五、信号量机制核心原理

信号量机制由荷兰科学家 Dijkstra 提出,是操作系统中最经典、应用最广泛的同步互斥工具。它完美解决了 "忙等" 问题,同时能灵活支持各种复杂的同步互斥场景,是现代操作系统并发控制的核心基础。

5.1 信号量与 PV 操作定义

信号量本质上是一个表示系统中空闲临界资源数量的变量,其初始值≥0。信号量一旦完成初始化,就只能通过两个标准原语操作修改其值,原语的执行过程不可被中断。

两个标准原语分别是 P 操作(wait 操作)和 V 操作(signal 操作):

  • P 操作(申请资源):执行一次 P 操作,信号量值减 1。若减 1 后信号量≥0,说明申请成功,进程继续执行;若减 1 后信号量 < 0,说明没有空闲资源,进程自我阻塞,进入该信号量的等待队列。
  • V 操作(释放资源):执行一次 V 操作,信号量值加 1。若加 1 后信号量≤0,说明等待队列中还有阻塞的进程,就唤醒队列中的一个进程,让它从阻塞态转为就绪态。

5.2 整型信号量

整型信号量是最基础的实现形式,用一个整数表示资源数量,P 操作通过循环等待实现:

// P操作(申请资源)
void wait(int S) {
    while (S <= 0);  // 资源不足,循环等待
    S = S - 1;       // 申请一个资源
}

// V操作(释放资源)
void signal(int S) {
    S = S + 1;       // 释放一个资源
}
  • 核心缺点:进程等待时持续循环判断条件,存在忙等问题,不满足让权等待准则。

5.3 记录型信号量

记录型信号量在整型信号量的基础上,增加了阻塞等待队列,彻底解决了忙等问题,是实际操作系统中使用的标准版本。它包含两个核心成员:

  • value:资源数量计数器;
  • list:阻塞进程的等待队列链表。
// 记录型信号量结构体定义
typedef struct {
    int value;               // 资源数量计数器
    struct process *list;    // 阻塞进程等待队列
} semaphore;

// P操作(申请资源)
void wait(semaphore *S) {
    S->value--;
    if (S->value < 0) {
        block(S->list);  // 资源不足,进程自我阻塞,主动释放CPU
    }
}

// V操作(释放资源)
void signal(semaphore *S) {
    S->value++;
    if (S->value <= 0) {
        wakeup(S->list); // 仍有进程在等待,唤醒队列中的一个阻塞进程
    }
}
  • value 值的物理意义
    • value > 0:表示当前系统中空闲资源的数量;
    • value = 0:表示资源刚好被全部用完,且没有进程在等待;
    • value < 0:其绝对值表示当前阻塞等待队列中的进程总数量。

记录型信号量的 P、V 操作完整执行流程,可以通过下图直观理解:

5.4 用信号量实现进程互斥

实现互斥逻辑非常简单:设置一个互斥信号量mutex,初始值为 1,代表临界资源初始可用。在临界区前执行 P 操作,临界区后执行 V 操作即可。

semaphore mutex = 1;  // 互斥信号量,初值为1,表示资源初始可用

// 进程A
void process_A() {
    wait(&mutex);     // 申请互斥访问权限
    critical_section(); // 临界区代码
    signal(&mutex);   // 释放互斥访问权限
}

// 进程B
void process_B() {
    wait(&mutex);
    critical_section();
    signal(&mutex);
}

5.5 用信号量实现进程同步

进程同步的核心口诀是:前操作后 V,后操作前 P。设置一个同步信号量,初始值为 0;在先执行的操作之后执行 V 操作,在后执行的操作之前执行 P 操作。

例如场景:要求进程 A 的代码段 S1 执行完之后,进程 B 的代码段 S2 才能执行。

semaphore sync = 0;   // 同步信号量,初值为0

// 进程A(先执行的进程)
void process_A() {
    S1;               // 前置操作代码
    signal(&sync);    // 通知后置进程:前置操作已完成
}

// 进程B(后执行的进程)
void process_B() {
    wait(&sync);      // 等待前置操作完成
    S2;               // 后置操作代码
}

六、经典问题实战:生产者 - 消费者模型

生产者 - 消费者问题是操作系统同步互斥模块最经典的实战模型,也是期末考试、考研、面试的高频考点。我们通过标准化解题步骤完成该问题的实现。

6.1 通用解题四步法

解决所有进程同步互斥问题,都可以遵循以下固定步骤,思路清晰不易出错:

  1. 判断问题类型:区分是纯互斥、纯同步,还是同步 + 互斥混合。如果是混合类型,优先分析同步关系,再处理互斥关系。
  2. 定义信号量与初值:根据同步点和互斥资源,确定需要的信号量,并设置正确的初始值。
  3. 梳理运行主体:明确场景中有几类进程,站在每个进程的角度,分析它需要申请什么资源、执行什么操作、释放什么资源。
  4. 排布 PV 操作:按照 "同步 P 在前、互斥 P 在后" 的原则,在对应位置插入 P、V 操作,检查逻辑是否通顺。

6.2 问题描述与分析

问题描述

系统中有一组生产者进程和一组消费者进程,它们共享一个大小为 n、初始为空的缓冲区。

  • 生产者每次生产一个产品,放入缓冲区;
  • 消费者每次从缓冲区取出一个产品并消费;
  • 所有进程不能同时访问缓冲区。
问题分析
  1. 类型判断:同步 + 互斥混合问题
    • 同步关系:缓冲区为空时消费者不能取产品,必须等生产者放入;缓冲区满时生产者不能放产品,必须等消费者取出。
    • 互斥关系:缓冲区是临界资源,所有进程不能同时访问缓冲区。
  2. 信号量定义
    • empty:空缓冲区数量,初值为 n,代表生产者还能放入多少个产品;
    • full:满缓冲区数量,初值为 0,代表缓冲区中已有多少个产品;
    • mutex:缓冲区互斥信号量,初值为 1,保证同一时间只有一个进程访问缓冲区。

生产者 - 消费者模型的整体运行逻辑如下图所示:

image

6.3 完整 PV 操作实现

// 信号量初始化
semaphore empty = n;   // 空缓冲区数量,初值为n
semaphore full = 0;    // 满缓冲区数量,初值为0
semaphore mutex = 1;   // 缓冲区互斥信号量,初值为1

// 生产者进程
void producer() {
    while (TRUE) {
        produce_item();    // 生产一个产品
        wait(&empty);      // 申请空缓冲区(同步P操作)
        wait(&mutex);      // 申请缓冲区访问权限(互斥P操作)
        put_item_into_buffer(); // 将产品放入缓冲区
        signal(&mutex);    // 释放缓冲区访问权限(互斥V操作)
        signal(&full);     // 增加满缓冲区数量(同步V操作)
    }
}

// 消费者进程
void consumer() {
    while (TRUE) {
        wait(&full);       // 申请满缓冲区(同步P操作)
        wait(&mutex);      // 申请缓冲区访问权限(互斥P操作)
        get_item_from_buffer(); // 从缓冲区取出产品
        signal(&mutex);    // 释放缓冲区访问权限(互斥V操作)
        signal(&empty);    // 增加空缓冲区数量(同步V操作)
        consume_item();    // 消费产品
    }
}

6.4 易错点避坑指南

  1. P 操作顺序不能颠倒:必须先执行同步 P 操作,再执行互斥 P 操作。如果先执行互斥 P 再执行同步 P,可能会出现死锁(生产者占有缓冲区却等待空缓冲区,消费者占有缓冲区却等待满缓冲区)。
  2. 信号量初值不能写错:互斥信号量初值固定为 1;同步信号量初值根据初始资源数量设置。
  3. V 操作顺序无影响:同步 V 和互斥 V 的顺序可以互换,不会影响逻辑正确性。
  4. P 和 V 必须成对出现:有一个 P 操作就必须对应一个 V 操作,避免资源泄漏或死锁。

七、各类实现方案的对比与适用边界

我们将软件实现、硬件实现、信号量机制三类方案从多个维度进行对比,明确各自的适用场景与限制条件:

方案类型 代表实现 互斥性 有限等待 让权等待 适用场景 核心限制
软件实现 Peterson 算法 满足 满足 不满足 双进程简单场景、教学演示 存在忙等,扩展性差,仅适用于双进程
硬件实现 TS 指令、Swap 指令 满足 不满足 不满足 多处理机系统、内核态简单互斥 存在忙等,仅能实现互斥,无法支持复杂同步
信号量机制 记录型信号量 满足 满足 满足 所有多进程同步互斥场景、复杂协作模型 原语需硬件 / 内核支持,使用不当易引发死锁

边界说明

  1. 软件互斥方案仅适用于理论学习与双进程简单场景,实际工业系统中不会单独使用纯软件方案;
  2. 硬件原子指令是现代锁机制的底层基础,用户态开发通常不会直接调用,而是封装在编程语言的锁机制中;
  3. 信号量机制是通用解决方案,但需要开发者正确设置信号量初值与 PV 操作位置,使用不当会导致死锁、饥饿等问题。

从技术演进的角度看,现代并发编程中的互斥锁、条件变量、分布式锁等机制,底层逻辑都源于本章的同步互斥理论。掌握这些基础原理,有助于理解高级并发工具的底层实现。


八、本章总结与思考

本文围绕 "多进程并发下的资源安全共享与执行顺序控制" 这一核心问题,从概念定义、设计准则、软件实现、硬件实现、信号量机制、经典问题六个层面进行了完整的分析与推导,形成了完整的知识闭环。

核心结论总结如下:

  1. 互斥与同步是进程并发控制的两大核心,互斥解决资源竞争的排他性问题,同步解决进程协作的顺序性问题;
  2. 同步机制的四大准则是衡量方案优劣的核心标准,其中 "让权等待" 是提升 CPU 利用率的关键;
  3. 纯软件方案无法彻底解决忙等问题,硬件方案通过原子操作保证互斥性,信号量机制则同时满足所有设计准则,是最通用的解决方案;
  4. 解决经典同步问题的核心是先分析同步关系、再处理互斥关系,严格遵循 "同步 P 在前,互斥 P 在后" 的原则。

互动思考

你在学习进程同步与互斥的过程中,遇到过哪些容易混淆的概念或者易错的 PV 操作场景?欢迎在评论区留言讨论,我们一起交流避坑经验。

写作说明

本文内容源自笔者近期系统学习操作系统进程管理时的个人笔记。为了提升阅读体验,笔者借助AI工具对原始笔记进行了逻辑梳理、表格优化和排版润色,但所有核心知识点均经过笔者逐一核对与校正。如有疏漏,欢迎指正交流。

Logo

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

更多推荐