图木舒克市网站建设_网站建设公司_原型设计_seo优化
2025/12/20 19:51:27 网站建设 项目流程

1.同步与互斥的基本概念

1.临界资源

1. 什么是临界资源?

虽然在多道程序环境下,系统中的多个进程可以共享各种资源,但有些资源受物理限制或逻辑一致性的要求,无法同时被多个对象操作。

常见的临界资源包括:

  • 硬件设备: 打印机、磁带机、绘图仪等。
  • 软件资源: 共享变量、数据结构、共享文件、数据库表格等。

2. 核心概念:临界区 (Critical Section)

为了保护临界资源,我们不能直接对资源加锁,而是通过控制访问这些资源的代码来实现。

  • 临界区: 每个进程中访问临界资源的那段代码
  • 访问逻辑: 只要保证同时只有一个进程进入它的临界区,就能保证临界资源的安全。

3. 访问临界资源的四个原则

为了高效且安全地利用临界资源,操作系统设计通常遵循以下四个准则:

  1. 空闲让进 (Progress): 当临界资源处于空闲状态时,允许一个请求访问该资源的进程立即进入临界区。
  2. 忙则等待 (Mutual Exclusion): 当临界资源正在被访问时,其他试图进入临界区的进程必须等待,以保证互斥
  3. 有限等待 (Bounded Waiting): 对要求访问临界资源的进程,应保证在有限时间内能进入临界区,避免“死等”(饥饿现象)。
  4. 让权等待 (Yielding CPU): 当进程不能进入临界区时,应立即释放处理机(CPU),防止进程处于“忙等”状态(即不停地循环检查,浪费CPU资源)。

4. 常见的同步机制

为了管理临界资源,开发者通常会使用以下技术:

机制 描述
互斥锁 (Mutex) 最简单的锁,只有“锁定”和“释放”两种状态。
信号量 (Semaphore) 计数器机制,可以控制多个(或单个)资源副本的访问。
自旋锁 (Spinlock) 进程在等待时不停地循环检查锁是否可用(适用于等待时间极短的场景)。
管程 (Monitor) 一种高级同步原语,将资源和操作封装在一起。

2.同步

1. 什么是同步?

同步是指在多道程序环境下,为了完成某个共同任务,多个进程(或线程)之间需要协调它们的工作次序。

  • 核心逻辑: 进程 A 的执行依赖于进程 B 产生的信息或信号。在 B 完成特定动作之前,A 必须停下来等待。
  • 本质: 一种直接制约关系,即“你没做完,我不能开始”。

2. 同步与互斥的区别

这两个概念经常成对出现,但侧重点不同:

特性 互斥 (Mutual Exclusion) 同步 (Synchronization)
关系类型 间接制约(竞争关系) 直接制约(协作关系)
目的 防止多个进程同时访问临界资源,造成混乱。 确保进程按预定的先后顺序执行。
触发点 资源被占用。 某个条件尚未满足。
例子 两个进程争夺同一台打印机。 进程 A 必须等进程 B 计算出结果后才能进行统计。

3. 经典模型:生产者-消费者问题 (Producer-Consumer)

这是理解同步最经典的案例:

  1. 生产者生产数据放入缓冲区。
  2. 消费者从缓冲区取出数据处理。

这里的同步逻辑是:

  • 如果缓冲区了,生产者必须等待(同步:等消费者取走数据)。
  • 如果缓冲区了,消费者必须等待(同步:等生产者放入数据)。
  • 同时,缓冲区本身是临界资源,存取操作必须互斥。

4. 实现同步的机制:信号量 (Semaphore)

最常用的同步手段是信号量机制,由荷兰科学家 Dijkstra 提出。它主要依靠两个原语操作:

  • P 操作 (Wait/Down): 申请资源。如果信号量 $S \le 0$,进程进入阻塞状态;否则 $S = S - 1$。
  • V 操作 (Signal/Up): 释放资源。$S = S + 1$,如果有进程在等待该信号,则唤醒它。

同步的逻辑表达:

假设进程 A 运行到某处需要等待进程 B 的结果:

  1. 初始化信号量 $S = 0$。
  2. 进程 B 执行完关键操作后,执行 $V(S)$(发出“我做好了”的信号)。
  3. 进程 A 在需要结果的地方执行 $P(S)$。如果 B 没做完,A 就会在 $P$ 操作处阻塞。

5. 同步设计的四个准则

为了防止程序死锁或效率低下,同步机制应满足:

  • 空闲让进: 没任务时不要卡住 CPU。
  • 忙则等待: 条件不满足时必须等。
  • 有限等待: 不能让某个进程无限期等下去。
  • 让权等待: 等待时应释放 CPU 资源(避免忙等)。

总结

  • 互斥是“我有你没有”,保护的是资源
  • 同步是“你完我才开始”,保护的是逻辑顺序

3,互斥

1. 什么是互斥?

互斥是指当一个进程(或线程)正在访问某个临界资源(如共享变量、文件、打印机)时,其他任何试图访问该资源的进程都必须被阻塞或等待,直到当前进程访问完毕并释放资源。

  • 生活类比: 互斥就像是火车的“单线轨道”。在同一时间,只能有一列火车在轨道上行驶,否则就会发生碰撞。
  • 核心目标: 解决竞争条件 (Race Condition),确保数据的一致性和完整性。

2. 互斥的实现机制

为了实现互斥,通常需要一套“进入”和“退出”的控制协议。

A. 软件方案 (如 Peterson 算法)

这是一种纯软件的逻辑实现,通过设置标志位(Flag)和让步变量(Turn)来协商谁进入临界区。虽然在现代多核处理器上很少直接使用,但它是理解互斥逻辑的基础。

B. 硬件方案

现代 CPU 提供了底层指令来支持原子操作:

  • 关中断: 在进入临界区前关闭中断,执行完再开启。缺点是风险大,不适合多处理机。
  • TSL (Test-and-Set) 指令: 一个不可分割的原子指令,用于检查并设置锁的状态。

C. 锁机制 (Mutex / Locks)

这是开发者最常用的方式。进程在进入临界区前必须先“获取锁”,离开时“释放锁”。

  • 如果锁已被占用,申请者会被挂起或自旋(Spinning)。

3. 互斥带来的副作用:死锁 (Deadlock)

互斥是产生死锁的必要条件之一。

  • 场景: 进程 A 占有资源 1,申请资源 2;进程 B 占有资源 2,申请资源 1。
  • 结果: 双方都因为互斥机制而无法获得对方手里的资源,导致永久等待。

2.实现临界区的基本方法

1.单标志法

单标志法 (Single Flag Method),也被称为“强制轮流法”,是实现进程互斥的一种纯软件解决方案。它的核心思想是设置一个公共变量 turn,用来指明当前允许进入临界区的进程编号。


1. 算法逻辑

假设有两个进程 $P_0$ 和 $P_1$,它们共享一个变量 turn

  • 如果 turn == 0,则允许 $P_0$ 进入临界区。
  • 如果 turn == 1,则允许 $P_1$ 进入临界区。

代码实现 (以 C 语言风格为例)

进程 $P_0$:

while (turn != 0);  // ① 进入区:如果不是我的轮次,就一直循环等待(忙等)
critical_section();  // ② 临界区:访问共享资源
turn = 1;            // ③ 退出区:访问完后,把权利交给 P1
remainder_section(); // ④ 剩余区

进程 $P_1$:

while (turn != 1);  // ① 进入区:如果不是我的轮次,就一直循环等待
critical_section();  // ② 临界区:访问共享资源
turn = 0;            // ③ 退出区:访问完后,把权利交给 P0
remainder_section(); // ④ 剩余区

2. 算法评价

这个算法虽然简单,但在实际应用中存在明显的缺陷。

优点

  • 满足“忙则等待”原则: 在任何时刻,由于 turn 的值只能为 0 或 1,因此不可能有两个进程同时进入临界区,有效实现了互斥

缺点(致命伤)

  • 违背“空闲让进”原则: 这是单标志法最大的问题。该算法强制要求两个进程交替访问
    • 场景模拟: 如果此时临界资源是空闲的,但 turn 的值是 0(该 $P_0$ 进),而 $P_0$ 恰好此时不需要访问临界资源(卡在剩余区或未启动)。
    • 结果: 想要访问资源的 $P_1$ 会因为 turn 始终为 0 而被阻塞在 while 循环里。明明资源空闲,却不让进。
  • 违背“让权等待”原则: 进程在无法进入临界区时,会一直在 while 循环中检查,白白消耗 CPU 时间(即“忙等”)。

3. 总结

单标志法就像是一把只有一张通行证的门,且规定这号证必须在 A 和 B 之间轮流转交

结论: 单标志法虽然实现了互斥,但因为它过于僵化(必须交替),导致资源利用率低下。如果其中一个进程挂掉了,另一个进程可能永远也进不去临界区。

2.双标志先检查法

双标志先检查法 (Dual-flag Pre-check Method) 是为了克服“单标志法”必须交替访问、资源利用率低的缺点而提出的。

它的核心思想是:不再由别人交还权利,而是每个进程通过一个布尔数组 flag[] 表达自己的“意愿”。


1. 算法逻辑

设置一个布尔型数组 flag[2]

  • flag[0] = true 表示进程 $P_0$ 想要进入临界区。
  • flag[1] = true 表示进程 $P_1$ 想要进入临界区。

每个进程在进入临界区之前,先检查对方是否想进去。如果对方不想进,自己再进去。

代码实现

进程 $P_0$:

while (flag[1]);      // ① 进入区:检查 P1 是否想进,如果 P1 想进,我就等待
flag[0] = true;       // ② 进入区:P1 不想进,那我就标记自己想进
critical_section();   // ③ 临界区:执行操作
flag[0] = false;      // ④ 退出区:执行完,标记自己不想进了
remainder_section();  // ⑤ 剩余区

进程 $P_1$:

while (flag[0]);      // ① 进入区:检查 P0 是否想进
flag[1] = true;       // ② 进入区:P0 不想进,我标记想进
critical_section();   // ③ 临界区
flag[1] = false;      // ④ 退出区
remainder_section();  // ⑤ 剩余区

2. 算法评价:致命的逻辑漏洞

虽然这个算法解决了单标志法“不能主动申请”的问题,但它却引出了一个更严重的安全性问题:违背了“忙则等待”原则。

为什么不安全?

由于 $P_0$ 和 $P_1$ 是并发执行的,“检查”“设置标志”这两个动作并不是原子的(即不是一口气完成的)。

  1. 第一步: $P_0$ 执行 while (flag[1]),发现 $P_1$ 目前不想进(flag[1]false)。
  2. 第二步(发生切换): 此时 CPU 切换到了 $P_1$。$P_1$ 执行 while (flag[0]),发现 $P_0$ 目前也没设置标志(flag[0] 还是 false)。
  3. 第三步: $P_0$ 接下来执行 flag[0] = true 并进入临界区。
  4. 第四步: $P_1$ 接下来也执行 flag[1] = true 并进入临界区。

后果: $P_0$ 和 $P_1$ 同时位于临界区内,互斥机制失效!


3. 核心总结

失败的原因: “先检查”和“后置位”之间存在时间差。在这个空隙里,对方可能趁虚而入,导致两个进程都认为对方不想进,从而同时闯入。

3.双标志后检查法

1. 核心思想

该算法的基本逻辑是:先“表达意愿”(将自己的标志设为 true),然后再“检查对方状态”。如果对方也想进入,则自己循环等待。


2. 算法代码描述(C语言风格)

假设有两个进程 $P_0$ 和 $P_1$,使用一个布尔数组 flag[2],初始值均为 false

进程 $P_i$ 的逻辑:

while (true) {flag[i] = true;           // Step 1: 先设置自己的标志,表示我想进入临界区while (flag[j]);          // Step 2: 再检查对方的标志,如果对方也想进,则我循环等待// --- 临界区 (Critical Section) ---flag[i] = false;          // Step 3: 退出临界区,清除自己的标志// --- 剩余区 (Remainder Section) ---
}

3. 运行流程分析

这种方法解决了双标志先检查法中“两个进程可能同时进入临界区”的问题,但引入了新的致命缺陷。

为什么会发生死锁?

双标志后检查法的主要问题在于违背了“空闲让进”原则。考虑以下执行时序:

  1. $P_0$ 设置 flag[0] = true(表示 $P_0$ 想进入)。
  2. 此时发生进程切换,CPU 调度到 $P_1$。
  3. $P_1$ 设置 flag[1] = true(表示 $P_1$ 也想进入)。
  4. 此时,两个标志位均为 true
  5. $P_1$ 执行 while(flag[0]),发现 $P_0$ 想进,于是 $P_1$ 开始循环等待
  6. 切换回 $P_0$,$P_0$ 执行 while(flag[1]),发现 $P_1$ 想进,于是 $P_0$ 也开始循环等待

结论: 两个进程都互不相让,最终都无法进入临界区,导致“死锁”(Deadlock)或相互推诿。

4.Peterson

1. 核心思想:意愿 + 谦让

Peterson 算法使用两个变量:

  1. flag[] 数组:布尔型,表示进程是否有意愿进入临界区(如 flag[0] = true 表示 $P_0$ 想进)。
  2. turn 变量:整型,表示最后该谁进(即“谦让”给谁)。

口诀: “先表达意愿,再主动谦让,最后检查条件。”


2. 算法代码描述

假设有两个进程 $P_i$ 和 $P_j$($i$ 为当前进程,$j$ 为对方进程):

bool flag[2]; // 初始均为 false
int turn = 0; // 初始值无所谓// 进程 Pi 的逻辑
while (true) {flag[i] = true;           // Step 1: 表达意愿,我想进turn = j;                 // Step 2: 主动谦让,把优先权给对方进程 j// Step 3: 检查条件。如果对方也想进,且最后确实是让给对方了,我就循环等待while (flag[j] && turn == j); // --- 临界区 (Critical Section) ---flag[i] = false;          // Step 4: 退出临界区,表示我不想进了// --- 剩余区 (Remainder Section) ---
}

3. 为什么它能完美解决问题?

让我们分析最极端的场景:$P_0$ 和 $P_1$ 同时想进入临界区。

  1. 表达意愿:$P_0$ 设置 flag[0]=true,$P_1$ 设置 flag[1]=true
  2. 发生谦让
    • 假设 $P_0$ 先执行 turn = 1,接着 $P_1$ 执行 turn = 0
    • 此时,turn 的最终值是 0(因为 $P_1$ 是后执行赋值的,覆盖了之前的值)。
  3. 条件检查
    • 对于 $P_1$:执行 while(flag[0] && turn == 0)。因为 $P_0$ 的 flagtrueturn 确实是 0,条件成立,$P_1$ 循环等待
    • 对于 $P_0$:执行 while(flag[1] && turn == 1)。虽然 $P_1$ 的 flagtrue,但此时 turn 已经是 0 了(不等于 1),条件不成立,$P_0$ 顺利进入临界区

结论:

  • 不会死锁turn 变量在同一时刻只能有一个值,因此 while 循环总能放行其中一个。
  • 互斥访问:同一时刻只有一个进程能满足跳出循环的条件。
  • 空闲让进:如果对方不想进(flag[j] == false),自己可以直接进入。

4. 算法优缺点评价

维度 评价
优点 满足同步机制的所有准则:空闲让进、忙则等待、有限等待、让权等待(逻辑上)
局限性 1 忙等(Busy Waiting):进程在等待时依然占用 CPU 执行 while 循环,没有做到“让权等待”(即挂起进程)。
局限性 2 仅限双进程:虽然可以推广到多进程(如 Lamport 面包店算法),但代码会变得非常复杂。
现代问题 内存乱序优化:在现代高性能处理器上,由于指令重排(Out-of-order execution),Peterson 算法如果不加内存屏障(Memory Barrier),可能会失效。

总结:四种软件方案的进化

  1. 单标志法:太客气(只能轮流),导致空闲不能进。
  2. 双标志先检查:太猴急(先看别人),导致可能同时进。
  3. 双标志后检查:太贪心(先占位置),导致死锁谁也别想进。
  4. Peterson 算法又表明意愿又客气谦让,达到了完美的平衡。

5.中断屏蔽方法

1. 核心逻辑:掐断“变心”的可能性

在单处理机(单核 CPU)系统中,进程切换的唯一诱因就是中断(比如时间片到了、I/O 结束了)。

  • 关中断:相当于 CPU 戴上了耳塞,屏蔽了一切外部消息。
  • 临界区:这时候 CPU 只听当前进程的话。
  • 开中断:忙完了,把耳塞摘掉,恢复正常。

执行模式:

关中断 -> 进入临界区(干活) -> 开中断


2. 为什么这种方法有“缺陷”?(结合图片内容)

虽然这种方法逻辑上很完美,但在实际应用中却有很多“坑”:

  • 1. 效率太低(系统变慢): CPU 的强大之处在于它能通过中断来“多任务并行”。你一关中断,所有的时钟中断、磁盘反馈、键盘输入都被挡在门外。如果临界区代码很长,整个系统看起来就像“卡死”了一样。
  • 2. 权力太大(不安全): “关中断/开中断”属于特权指令
    • 如果你把这个权利交给普通用户程序,万一有个进程“关了中断”后故意陷入死循环,或者忘记“开中断”,整个电脑就彻底瘫痪了,你连 Ctrl+Alt+Del 都按不出来。
  • 3. 多核 CPU 无效(最致命的缺点): 现在的电脑都是多核的。你在 CPU 核心 A 上关了中断,只能保证 A 不切换进程,但 CPU 核心 B 依然在跑代码。如果 B 上的进程也要进入同一个临界区,关中断法完全拦不住它。

5.TestAndSet

1. 核心定义

TestAndSet 指令在一个不可分割的原子操作中完成两件事:

  1. 读取内存中变量的旧值(Test)。
  2. 设置该变量为一个新值(通常是 true1)(Set)。

其关键在于其原子性(Atomicity):即便在多核 CPU 环境下,当一个线程执行此指令时,其他线程无法中途插入或修改该变量。


2. 逻辑模拟 (C 语言描述)

虽然 TestAndSet 是由硬件实现的,但我们可以用如下逻辑代码来理解它的工作原理:

C

// 这是一个原子操作,硬件保证执行期间不会被中断
boolean TestAndSet(boolean *target) {boolean rv = *target; // 1. 记录旧值*target = TRUE;       // 2. 将值设为 TRUEreturn rv;            // 3. 返回旧值
}

3. 如何实现互斥锁 (Spinlock)

TestAndSet 最常见的应用是实现自旋锁(Spinlock)。以下是线程进入和离开“临界区”(Critical Section)的典型逻辑:

实现代码示例:

C

// 初始化 lock 为 FALSE
while (TestAndSet(&lock)) {// 如果返回 TRUE,说明锁已被占用,在这里循环等待(自旋)
}// --- 临界区 (Critical Section) ---
// 此时线程已获得锁,可以安全访问共享资源lock = FALSE; // 释放锁

工作流程说明:

  • 第一个到达的线程:执行 TestAndSet,由于 lock 初始为 FALSE,函数返回 FALSE,跳出循环进入临界区。同时,lock 被设置为 TRUE
  • 后续到达的线程:执行 TestAndSet 时,lock 已经是 TRUE,函数返回 TRUE,线程会在 while 循环中不断重试(这就是所谓的“自旋”)。
  • 释放锁:第一个线程完成后将 lock 设回 FALSE,下一个自旋的线程才能成功获取锁。

4. 优缺点分析

优点 缺点
实现简单:易于在硬件层面实现。 忙等待 (Busy Waiting):线程在等待锁时会一直消耗 CPU 资源,不适合长时间持有锁的场景。
开销小:对于极短时间内就能获得的锁,自旋比上下文切换(Context Switch)更快。 饥饿问题 (Starvation):TAS 无法保证公平性,某些线程可能永远抢不到锁。
多核支持:是构建更高级同步原语(如信号量、互斥锁)的基础。 优先级反转:低优先级线程持有锁,高优先级线程自旋抢占 CPU 导致死锁风险。

6.swap指令

1. 基本定义与逻辑

Swap 指令的作用是交换两个字(Word)的内容(通常是一个寄存器值和一个内存值,或者两个内存值)。

虽然在高级语言中我们看似需要三步来实现交换(借助临时变量),但在硬件层面,这作为一个不可分割的单元执行。

逻辑定义的伪代码(C 风格):

C

// 注意:这段代码在硬件上是原子执行的,不可中断
void Swap(bool *a, bool *b) {bool temp = *a;*a = *b;*b = temp;
}

2. 如何利用 Swap 实现互斥锁?

要使用 Swap 指令解决临界区(Critical Section)问题,我们需要定义一个全局布尔变量 lock

  • lock = false:表示临界区空闲(未上锁)。
  • lock = true:表示临界区被占用(已上锁)。

每个想要进入临界区的进程,都会在局部维护一个变量 key

实现步骤:

  1. 进程将自己的局部变量 key 设置为 true
  2. 使用 Swap 指令交换 lockkey 的值。
  3. 检查 key 的值。

代码实现逻辑:

C

// 全局共享变量
bool lock = false; void process_i() {bool key = true; // 局部变量do {key = true; // 忙等待 (Busy Waiting)// 不断交换 key 和 lock 的值// 如果 lock 原本是 true (忙),交换后 key 还是 true,继续循环// 如果 lock 原本是 false (闲),交换后 key 变成 false,lock 变成 true,退出循环while (key == true) {Swap(&lock, &key);}// --- 临界区 (Critical Section) ---// 执行只有该进程能执行的操作// -------------------------------// 退出区 (Exit Section)lock = false; // 释放锁,让其他进程可以进入// --- 剩余区 (Remainder Section) ---} while (true);
}

工作原理解析

  • 当锁空闲时 (lock = false): 进程 A 执行 Swap。交换后,lock 变为 true(表示上锁),进程 A 的 key 变为 false。循环条件 key == true 不满足,进程 A 进入临界区
  • 当锁被占用时 (lock = true): 进程 B 执行 Swap。交换后,lock 保持 true,进程 B 的 key 也保持 true。循环条件满足,进程 B 继续循环等待

3. Swap 指令的优缺点

与其他硬件指令(如 Test-and-Set)或软件方法相比,Swap 指令有以下特点:

优点

  1. 简单易用: 逻辑简单,易于在多处理器系统中验证。
  2. 适用于多临界区: 可以支持任意数量的临界区(只需定义不同的 lock 变量)。
  3. 支持多处理器: 现代 CPU 原生支持原子交换,适用于多核环境下的同步。

缺点

  1. 忙等待(Busy Waiting):
    • 这是最大的缺点。当一个进程在临界区内时,其他试图进入的进程必须不断地执行循环和 Swap 指令。这会消耗大量的 CPU 时间,这被称为自旋锁(Spinlock)
  2. 可能导致饥饿(Starvation):
    • Swap 指令不保证公平性。如果多个进程都在忙等待,当锁被释放时,硬件随机决定哪个进程的 Swap 指令先执行成功。运气不好的进程可能永远抢不到锁。
  3. 可能导致死锁(Deadlock):
    • 虽然简单的互斥不会死锁,但在复杂场景下(如优先级反转或持有锁时崩溃),可能会引发问题。

3.互斥锁

1. 核心比喻:更衣室的钥匙

想象一个商场的更衣室:

  • 更衣室就是“共享资源”。
  • 钥匙就是“互斥锁”。
  • 顾客就是“线程”。

当一名顾客(线程)想进入更衣室时,必须先去前台拿钥匙。

  1. 如果钥匙在,该顾客拿走钥匙并锁门进去(加锁 Lock)。
  2. 其他顾客如果想进去,发现没钥匙,只能在外面排队等待(阻塞 Block)。
  3. 里边的顾客出来后,把钥匙还回前台(解锁 Unlock)。
  4. 下一位排队的顾客才能拿到钥匙进去。

2. 为什么需要互斥锁?

如果不使用互斥锁,多个线程同时修改同一个数据,就会发生竞态条件(Race Condition),导致数据混乱。

例子:银行存款 假设账户里有 100 元,两个线程同时存入 10 元:

  • 线程 A 读取余额 100。
  • 线程 B 也读取余额 100。
  • 线程 A 计算 100+10=110,并写入。
  • 线程 B 计算 100+10=110,并写入。
  • 结果: 存了两次钱,余额却是 110 元,丢失了 10 元。

使用互斥锁后: 线程 A 锁住账户 -> 读取 -> 计算 -> 写入 -> 解锁。在此期间线程 B 只能等待,从而保证余额正确变为 120 元。


3. 互斥锁的工作流程

在一个典型的程序中,使用互斥锁通常遵循以下步骤:

  1. 初始化:创建一个互斥锁对象。
  2. 加锁 (Lock):在进入临界区(访问共享资源的代码段)之前,尝试获取锁。
    • 如果锁已被占用,线程会挂起(休眠),直到锁被释放。
  3. 执行临界区代码:安全地读写共享资源。
  4. 解锁 (Unlock):完成操作后释放锁,唤醒其他正在等待的线程。
  5. 销毁:不再需要时释放锁资源。

4. 常见问题:死锁 (Deadlock)

互斥锁虽然安全,但如果使用不当,会导致程序“卡死”,即死锁

死锁经典场景: 线程 1 拿着 A 锁,尝试去拿 B 锁。 线程 2 拿着 B 锁,尝试去拿 A 锁。 两个线程都在等对方放手,结果谁也动不了。

避免死锁的常见方法:

  • 按顺序加锁:所有线程都必须先锁 A 再锁 B。
  • 超时机制:尝试获取锁,如果超过一定时间拿不到就放弃并报错。

4.信号量

1.整形型号

1. 定义与核心思想

整型信号量定义为一个整型变量,记为 $S$

  • $S$ 的数值含义: 表示当前系统中某种资源的可用数量。
  • 操作限制: 除了初始化外,仅能通过两个原子操作来访问它:Wait (等待)Signal (信号)。在历史上,这两个操作通常被称为 P 操作V 操作(来自荷兰语 ProberenVerhogen)。

2. 两个原子操作 (P/V 操作)

整型信号量的逻辑非常简单,假设 $S$ 是信号量:

A. Wait(S) 或 P(S) —— 申请资源

当一个进程想要使用资源时,它执行 wait(S)

wait(S) {while (S <= 0); // 如果没有资源 (S<=0),就在这里循环等待(忙等)S = S - 1;      // 一旦有资源 (S>0),占用一个资源
}
  • 含义: 如果 $S \le 0$,说明没资源了,进程就一直“盯着”看(忙等),直到有人释放资源。如果 $S > 0$,说明有资源,那就把资源数减 1,然后继续执行。

B. Signal(S) 或 V(S) —— 释放资源

当进程用完资源后,它执行 signal(S)

signal(S) {S = S + 1;      // 归还一个资源
}
  • 含义: 使用完毕,将资源数加 1。如果有其他进程正在 while 循环里苦苦等待,这个操作会让 $S$ 变大,从而让等待的进程有机会跳出循环拿到资源。

注意: 这两个操作必须是原子 (Atomic) 的,即执行过程中不能被中断,要么全做完,要么一点也不做,以防止并发错误。


3. 主要缺点:忙等待 (Busy Waiting)

整型信号量最显著的特征(也是最大的缺点)在于它的 wait 操作中的 while 循环:

  • 现象: 当进程拿不到资源时,它不会进入休眠状态,而是不断地循环测试 $S$ 的值。
  • 后果: 就像一个人去餐厅排队,他不是坐下来等叫号,而是每隔一秒就问服务员“有位置了吗?”。这占用了 CPU 时间,却不做任何有效工作。
  • 术语: 这种现象被称为“忙等” (Busy Waiting)。这违反了“让权等待”的原则(即当进程无法进入临界区时,应立即释放处理机,以免进程陷入“忙等”)。

2.记录型信号

1. 核心改进:从“忙等”到“阻塞”

  • 整型信号量: 如果没资源,进程就在那儿不停地循环(像个强迫症,浪费 CPU)。
  • 记录型信号量: 如果没资源,进程就主动睡觉 (Block),进入等待队列;当别人用完资源释放后,再由系统唤醒 (Wakeup) 它。这完全符合操作系统中“让权等待”的原则。

2. 数据结构

记录型信号量之所以叫“记录型”,是因为它不仅有一个代表资源数的整数,还维护了一个链表,用来记录正在等待该资源的所有进程。

在 C 语言中可以描述为:

typedef struct {int value;         // 剩余资源数struct process *L; // 等待该资源的进程队列(链表)
} semaphore;

3. P/V 操作逻辑 (Wait / Signal)

P 操作 (Wait) —— 申请资源

void wait(semaphore S) {S.value--;          // 1. 先申请(资源数减 1)if (S.value < 0) {  // 2. 如果减完发现资源不够(小于 0)// 将当前进程插入到等待队列 S.L 中block();        // 3. 阻塞当前进程,让出 CPU}
}
  • 注意:S.value 为负数时,其绝对值 $|S.value|$ 就表示当前正在队列中排队等待的进程数量。

V 操作 (Signal) —— 释放资源

void signal(semaphore S) {S.value++;          // 1. 释放(资源数加 1)if (S.value <= 0) { // 2. 如果加完后发现还是小于等于 0// 说明队列里还有人在排队睡觉// 从等待队列 S.L 中移除一个进程wakeup(P);      // 3. 唤醒那个被阻塞的进程}
}

4. 关键点理解

  1. value 的含义:

    • S.value > 0:表示系统中对应资源的可用数量
    • S.value = 0:表示资源刚好用完,没有进程在等待。
    • S.value < 0:表示资源已耗尽,且有 $|S.value|$ 个进程正在阻塞等待
  2. 让权等待 (Yielding CPU):

    由于引入了 block 和 wakeup 原语,进程在拿不到资源时不再占用 CPU 资源,从而大大提高了系统的整体效率。


5. 对比总结

特性 整型信号量 记录型信号量
等待行为 忙等(不停地 while 循环) 阻塞(去睡觉,加入队列)
CPU 消耗 浪费 CPU 周期 不浪费 CPU,效率高
适用场景 仅适用于多处理器或极短时间的锁 通用的进程同步与互斥
是否遵循原则 违反“让权等待” 遵循“让权等待”

3.信号量实现进程互斥

1. 基本原理

为了实现互斥,我们需要确保在任何时刻只有一个进程进入临界区(Critical Section)

关键组件

  • 信号量 $S$:初始化为 1。这里的 1 表示当前有一个可用资源(即允许一个进程进入临界区)。
  • P(S) 操作:尝试进入临界区。如果 $S > 0$,则 $S$ 减 1 并继续执行;如果 $S \le 0$,进程将被阻塞并放入等待队列。
  • V(S) 操作:退出临界区。将 $S$ 加 1。如果有进程在等待,则唤醒其中一个。

2. 算法实现

假设有两个或多个进程需要互斥地访问某个共享资源,其伪代码结构如下:

// 初始化信号量
semaphore mutex = 1;void process_i() {while (true) {// 1. 进入区:执行 P 操作P(mutex); // 2. 临界区 (Critical Section)// 访问共享资源的代码块...// 3. 退出区:执行 V 操作V(mutex);// 4. 剩余区 (Remainder Section)// 其他不相关的代码...}
}

逻辑说明:

  1. 第一个进程到达:调用 $P(mutex)$,由于 $mutex=1$,它将其减为 0,并直接进入临界区。
  2. 第二个进程到达:调用 $P(mutex)$,此时 $mutex=0$,减 1 后变为 -1。该进程发现 $mutex < 0$,于是进入阻塞状态,等待唤醒。
  3. 第一个进程离开:调用 $V(mutex)$,将 $mutex$ 从 -1 加回到 0。因为结果 $\le 0$,系统知道有进程在排队,于是唤醒等待中的第二个进程。
  4. 第二个进程进入:被唤醒后,它成功获得“锁”进入临界区。

3. 信号量取值的含义

在互斥应用场景下,信号量 $S$ 的值具有明确的物理意义:

  • $S = 1$:表示没有进程进入临界区。
  • $S = 0$:表示有一个进程正在临界区内,且没有其他进程在等待。
  • $S < 0$:表示有一个进程在临界区内,且有 $|S|$ 个进程正处于阻塞等待状态。

4. 必须遵守的原则

使用信号量实现互斥时,必须满足以下准则:

  1. 空闲让进:当临界区空闲时,应允许一个请求进入的进程立即进入。
  2. 忙则等待:当临界区已有进程时,其他试图进入的进程必须等待。
  3. 有限等待:对要求访问的进程,应保证在有限时间内能进入临界区,避免“死锁”。
  4. 让权等待:当进程不能进入临界区时,应立即释放处理机,防止“忙等”(Busy Waiting)。

总结比较

特性 信号量互斥 传统锁 (Spinlock)
等待方式 阻塞挂起(不占用CPU) 忙等(持续轮询CPU)
适用场景 临界区较长的情况 临界区极短的情况
实现复杂度 需操作系统内核支持 硬件指令即可实现

4.利用信号量同步

在操作系统中,信号量同步(Synchronization)是指协调多个进程的执行次序,使得它们能够按照预定的逻辑顺序配合工作。

与上一条讨论的“互斥”(保护资源不被同时访问)不同,同步的核心是“前趋关系”,即:进程 B 的某个操作必须在进程 A 的某个操作完成之后才能执行。


1. 基本原理

为了实现同步,信号量的初始值通常设置为 0

  • 信号量 $S$:初始化为 0
  • V(S) 操作:由“前趋进程”执行。代表“我已完成,你可以开始了”。
  • P(S) 操作:由“后继进程”执行。代表“我必须等待信号才能开始”。

2. 算法实现(前趋关系)

假设有两个进程 $P_1$ 和 $P_2$,我们希望 $P_1$ 中的代码块 $C_1$ 执行完后,$P_2$ 中的代码块 $C_2$ 才能开始执行。

// 初始化同步信号量为 0
semaphore sync = 0;void P1() {// ... 执行一些操作 ...C1;          // 领先执行的任务V(sync);     // 【释放信号】告诉 P2:我已经做好了
}void P2() {P(sync);     // 【等待信号】如果 P1 还没做完(sync=0),则阻塞C2;          // 紧跟执行的任务// ... 执行其他操作 ...
}

逻辑过程:

  1. 如果 $P_2$ 先运行:执行 $P(sync)$,由于 $sync=0$,进程 $P_2$ 会被阻塞,进入等待队列。
  2. 当 $P_1$ 完成 $C_1$ 后:执行 $V(sync)$,$sync$ 变为 1,系统唤醒 $P_2$。
  3. $P_2$ 被唤醒:继续执行 $C_2$。这样就保证了 $C_1$ 必定在 $C_2$ 之前完成。

3. 经典案例:生产者-消费者问题

同步最典型的应用是处理“生产者”与“消费者”之间的协作。这里需要两个同步信号量:

  1. empty:表示缓冲区空位的数量(初始为 $n$)。
  2. full:表示缓冲区内产品的数量(初始为 $0$)。
semaphore empty = n; // 同步:空位
semaphore full = 0;  // 同步:产品
semaphore mutex = 1; // 互斥:保证缓冲区的原子访问void producer() {while(true) {item = produce_item();P(empty);    // 等待空位(同步)P(mutex);    // 进入临界区(互斥)add_to_buffer(item);V(mutex);    // 离开临界区(互斥)V(full);     // 增加一个产品(同步:通知消费者)}
}void consumer() {while(true) {P(full);     // 等待产品(同步)P(mutex);    // 进入临界区(互斥)item = remove_from_buffer();V(mutex);    // 离开临界区(互斥)V(empty);    // 增加一个空位(同步:通知生产者)consume_item(item);}
}

4. 互斥与同步的区别总结

特性 进程互斥 (Mutual Exclusion) 进程同步 (Synchronization)
目的 防止多个进程同时进入临界区 协调进程间的执行顺序
初值 通常为 1 通常为 0 (或资源数量)
P/V位置 紧贴临界区前后,成对出现在同一进程中 $P$ 和 $V$ 通常分属不同的进程
关系 “你进我就退” “你完我再干”

5.利用信号量实现前驱关系

1. 实现的核心规则

实现前驱关系可以总结为以下三个步骤:

  1. 为每一条边设置信号量:在前驱图中,每一条有向边 $S_i \to S_j$ 都代表一个同步要求,我们需要为此定义一个唯一的信号量(如 a, b, c...)。
  2. 初始化为 0:所有用于同步前驱关系的信号量初始值必须设为 0
  3. 在进程中使用 P/V 操作
    • 在前驱进程($S_i$)中:任务完成后,对所有关联的出口边执行 V 操作(发送完成信号)。
    • 在后继进程($S_j$)中:任务开始前,对所有关联的入口边执行 P 操作(等待信号)。

2. 具体实例演示

假设我们有如下的前驱图:

  • $S_1$ 是起始任务。
  • $S_1$ 完成后,$S_2$ 和 $S_3$ 可以并行开始。
  • 只有当 $S_2$ 和 $S_3$ 都完成后,$S_4$ 才能开始。

前驱图表示:

$S_1 \to S_2$

$S_1 \to S_3$

$S_2 \to S_4$

$S_3 \to S_4$

第一步:定义信号量

  • 边 $S_1 \to S_2$:信号量 a = 0
  • 边 $S_1 \to S_3$:信号量 b = 0
  • 边 $S_2 \to S_4$:信号量 c = 0
  • 边 $S_3 \to S_4$:信号量 d = 0

第二步:伪代码实现

semaphore a=0, b=0, c=0, d=0;// 进程 P1
void S1() {// 执行 S1 的任务...V(a); // 通知 S2V(b); // 通知 S3
}// 进程 P2
void S2() {P(a); // 等待 S1// 执行 S2 的任务...V(c); // 通知 S4
}// 进程 P3
void S3() {P(b); // 等待 S1// 执行 S3 的任务...V(d); // 通知 S4
}// 进程 P4
void S4() {P(c); // 等待 S2P(d); // 等待 S3// 执行 S4 的任务...
}

3. 原理解析

  • 为什么初值为 0?

    初值为 0 意味着“资源尚未准备好”。后继进程执行 P 操作时会立即被阻塞,直到前驱进程执行 V 操作将其唤醒。这强制规定了先后顺序。

  • 如何处理并行?

    在上面的例子中,$S_2$ 和 $S_3$ 在 $S_1$ 发出信号后可以同时运行(取决于系统的调度),这体现了信号量既能控制先后,又不限制不相关的并行。

  • 多重依赖(汇合点):

    如 $S_4$ 依赖于 $S_2$ 和 $S_3$。在 $S_4$ 开始前执行两次 P 操作(P(c) 和 P(d)),只有当两个信号都到位时,$S_4$ 才会从阻塞态转入就绪态。


4. 总结规律

对于前驱图中的任意一个节点 $S_i$:

  1. 查“入度”:入度是多少,就在代码开头写多少个 P 操作(对应所有进入该节点的边)。
  2. 做任务:执行 $S_i$ 自身的逻辑。
  3. 查“出度”:出度是多少,就在代码结尾写多少个 V 操作(对应所有从该节点出去的边)。

6.分析进程同步和互斥问题的步骤

第一步:分析进程关系

首先要理清题目中涉及了多少个进程,以及它们之间存在什么样的约束关系。

  • 互斥关系:两个或多个进程是否试图同时访问同一个共享资源(如缓冲区、打印机、变量)?如果是,则需要设置互斥信号量。
  • 同步关系:一个进程的执行是否依赖于另一个进程的消息或产出?(即“前驱关系”)。
  • 数量关系:确定进程的数量。是单一的生产者/消费者,还是多类型的进程(如读者-写者问题)?

第二步:定义信号量

根据第一步的分析,设置相应的信号量,并赋予明确的初始值

信号量类型 用途 初始值建议 命名示例
互斥信号量 保护临界区,确保同一时间只有一个进程进入 通常为 1 mutex, lock
同步信号量 (资源型) 代表可用资源的数量 根据资源初始数量设定 (0, 1, 或 n) empty, full, count
同步信号量 (顺序型) 确保 $P_1$ 在 $P_2$ 之前执行 通常为 0 S1_finished, can_start

第三步:编写伪代码逻辑(P/V操作)

这是最关键的一步。在进程的执行逻辑中插入 P、V 操作。

  1. 确定动作顺序:先确定进程的核心动作(如“取号”、“生产”、“读文件”)。
  2. 插入 P 操作(检查条件):在核心动作之前。
  3. 插入 V 操作(释放资源/信号):在核心动作之后。

核心准则:P操作顺序不可颠倒

如果一个进程既有同步 P 操作,又有互斥 P 操作,必须先执行同步 P,再执行互斥 P。

  • 正确:P(empty); P(mutex);
  • 错误:P(mutex); P(empty);(这会导致死锁:进程占着锁等资源,而能产生资源的进程因为没锁进不来)。

第四步:最终检查(闭环测试)

完成代码编写后,通过以下三个维度进行复核:

  1. 死锁检查:是否存在两个进程互相等待对方释放信号量的情况?
  2. P/V 成对检查:每一个信号量在逻辑全路径中,P 操作和 V 操作的总数是否平衡?(互斥信号量通常在同一进程成对;同步信号量在不同进程成对)。
  3. 边界情况
    • 当资源为空时,消费者会阻塞吗?
    • 当缓冲区满时,生产者会阻塞吗?
    • 多个进程同时到达时,是否只有一个能进入临界区?

5.经典同步问题

1.生产者消费问题

第一步:分析进程关系

  1. 互斥关系
    • 生产者和消费者共同访问一个共享缓冲区。为了保证数据一致性,同一时刻只能有一个进程(无论是生产者还是消费者)对缓冲区进行操作。因此,需要一个互斥信号量
  2. 同步关系(前驱关系):
    • 缓冲区满时:生产者必须等待消费者取走产品(即等待“空位”)。
    • 缓冲区空时:消费者必须等待生产者生产产品(即等待“产品”)。
    • 这就是两个典型的“前驱关系”:有空位才能产,有产品才能消。

第二步:定义信号量

我们需要三个信号量:

  1. mutex:用于互斥访问缓冲区。
    • 初始值 = 1
  2. empty:用于同步,记录缓冲区中空位的数量。
    • 初始值 = n(缓冲区的大小)。
  3. full:用于同步,记录缓冲区中产品的数量。
    • 初始值 = 0(刚开始缓冲区是空的)。

第三步:编写伪代码逻辑

semaphore mutex = 1;    // 互斥信号量
semaphore empty = n;    // 同步信号量:空位数
semaphore full = 0;     // 同步信号量:产品数// 生产者进程
void producer() {while (true) {item = produce_item(); // 1. 生产一个产品(在临界区外)P(empty);              // 2. 检查是否有空位 (wait for empty slot)P(mutex);              // 3. 加锁,准备进入临界区add_to_buffer(item);   // 4. 将产品放入缓冲区 (临界区操作)V(mutex);              // 5. 解锁,离开临界区V(full);               // 6. 增加一个产品信号 (signal full slot)}
}// 消费者进程
void consumer() {while (true) {P(full);               // 1. 检查是否有产品 (wait for item)P(mutex);              // 2. 加锁,准备进入临界区item = remove_from_buffer(); // 3. 从缓冲区取走产品 (临界区操作)V(mutex);              // 4. 解锁,离开临界区V(empty);              // 5. 释放一个空位信号 (signal empty slot)consume_item(item);    // 6. 消费产品(在临界区外)}
}

第四步:关键点深度检查

1. 为什么 P 操作的顺序不能颠倒?(重点!)

在生产者中,如果我们将 P(mutex) 放在 P(empty) 之前,会发生什么?

  • 假设缓冲区已满(empty = 0)。
  • 生产者先执行 P(mutex),成功获得锁,进入临界区。
  • 随后执行 P(empty),由于没有空位,生产者被阻塞,带着锁进入了睡眠状态
  • 消费者想要运行,执行 P(full) 没问题,但执行 P(mutex) 时发现锁被生产者占着,也被阻塞。
  • 结果:生产者在等消费者取货释放空位,消费者在等生产者放开锁。两者永久互相等待,发生死锁
  • 结论必须先检查同步资源(P同步),再进入临界区(P互斥)。

2. V 操作的顺序可以颠倒吗?

可以。V(mutex)V(full) 的顺序颠倒通常不会引起死锁,因为 V 操作不会导致进程阻塞。但为了逻辑清晰,通常先释放锁(退出临界区),再发送资源信号。

3. 缓冲区大小 $n=1$ 时的情况

当缓冲区大小只有 1 时,该问题退化为最简单的同步问题。即使不使用 mutex,仅靠 emptyfull 信号量也能在某种程度上保证互斥(因为 emptyfull 永远不可能同时为 1),但在标准工程实践中,为了代码的可扩展性,依然建议保留 mutex


总结

生产者-消费者问题的核心在于:“先同步,后互斥;先产后消,满则等,空则待”。

2.复杂的生产者消费问题

1. 场景描述

桌子上有一个盘子,每次只能放入一个水果。

  • 父亲专门向盘中放苹果
  • 母亲专门向盘中放橘子
  • 儿子专等吃盘中的橘子
  • 女儿专等吃盘中的苹果

2. 分析进程关系

互斥关系:

  • 盘子是共享资源,任何时候只能有一个人对盘子进行操作(放或取)。需要一个互斥信号量 mutex

同步关系(前驱关系):

  1. 父亲 $\to$ 女儿:父亲放了苹果,女儿才能取苹果(apple 信号量)。
  2. 母亲 $\to$ 儿子:母亲放了橘子,儿子才能取橘子(orange 信号量)。
  3. 儿女 $\to$ 父母:只有盘子为空时,父亲或母亲才能放水果(plate 信号量)。

3. 定义信号量

  • mutex = 1:用于互斥访问盘子。
  • plate = 1:代表盘子里的空位数量(初始为 1)。
  • apple = 0:代表盘子里苹果的数量。
  • orange = 0:代表盘子里橘子的数量。

4. 算法实现(伪代码)

semaphore mutex = 1;  // 互斥信号量
semaphore plate = 1;  // 盘子里的空位
semaphore apple = 0;  // 苹果数
semaphore orange = 0; // 橘子数// 父亲进程
void father() {while(true) {准备一个苹果;P(plate);     // 1. 检查盘子是否有空位P(mutex);     // 2. 锁定盘子把苹果放入盘子;V(mutex);     // 3. 释放锁V(apple);     // 4. 告诉女儿:有苹果了}
}// 母亲进程
void mother() {while(true) {准备一个橘子;P(plate);     // 1. 检查盘子是否有空位P(mutex);     // 2. 锁定盘子把橘子放入盘子;V(mutex);     // 3. 释放锁V(orange);    // 4. 告诉儿子:有橘子了}
}// 女儿进程
void daughter() {while(true) {P(apple);     // 1. 检查是否有苹果P(mutex);     // 2. 锁定盘子从盘中取出苹果;V(mutex);     // 3. 释放锁V(plate);     // 4. 告诉父母:盘子空了吃苹果;}
}// 儿子进程
void son() {while(true) {P(orange);    // 1. 检查是否有橘子P(mutex);     // 2. 锁定盘子从盘中取出橘子;V(mutex);     // 3. 释放锁V(plate);     // 4. 告诉父母:盘子空了吃橘子;}
}

5. 关键点深入讨论

Q:在这个特定的问题中,mutex 信号量是必须的吗?

答案:在这个 $n=1$ 的特殊情况下,不是必须的。

因为 plate、apple、orange 三个同步信号量在任何时刻最多只有一个为 1(盘子要么是空的,要么装了苹果,要么装了橘子)。这意味着同一时间只有一个进程能通过 P 操作。

  • 但是,为了程序的健壮性和可扩展性(比如以后盘子能装 2 个水果了),加上 mutex 是标准做法,也是考试中最稳妥的答案。

Q:为什么 P 操作顺序必须是 P(plate) 在前,P(mutex) 在后?

这依然是为了防止死锁。如果父亲先执行 P(mutex) 拿到了锁,但发现盘子不满无法放入(假设这题里盘子逻辑更复杂),而此时儿女也无法进入临界区取水果释放空间,就会导致死锁。

3.读者-写者问题

1. 核心约束关系

在读者-写者问题中,有三类必须遵守的限制:

  1. 读-读允许:多个读者可以同时读取共享资源,不会造成数据损坏。
  2. 读-写互斥:当有人在写时,任何人(读者或写者)都不能进入。
  3. 写-写互斥:当有人在写时,其他写者必须等待。

2. 读者优先(Reader-First)算法实现

这是最常见的版本。其核心思想是:只要有一个读者正在读,就允许后续的读者直接进入,而写者必须等待所有读者离开。

定义信号量与变量

  • $rw_mutex = 1$:用于实现对共享资源的互斥访问(写者与写者、写者与读者之间)。
  • $mutex = 1$:用于互斥地更新 read_count 变量。
  • read_count = 0:整数变量,记录当前有多少个读者正在阅读。

伪代码实现

semaphore rw_mutex = 1; // 保护共享资源
semaphore mutex = 1;    // 保护 read_count 的原子操作
int read_count = 0;     // 记录读者数量// 写者进程
void writer() {while(true) {P(rw_mutex);     // 申请资源锁// 执行写操作...V(rw_mutex);     // 释放资源锁}
}// 读者进程
void reader() {while(true) {P(mutex);        // 锁定 count 变量if (read_count == 0) {P(rw_mutex); // 第一个读者进入,负责把门锁上,不让写者进来}read_count++;V(mutex);        // 解锁 count 变量// 执行读操作...P(mutex);        // 锁定 count 变量read_count--;if (read_count == 0) {V(rw_mutex); // 最后一个读者离开,负责把门打开,允许写者进来}V(mutex);        // 解锁 count 变量}
}

3. 关键逻辑解析:第一个与最后一个

这个算法的精妙之处在于对 read_count 的判断:

  • 第一个读者(First Reader):它承担了“外交官”的角色。如果它是第一个来的,它负责去申请 $rw_mutex$ 锁。一旦申请成功,后续的读者只需增加计数即可进入,无需再碰 $rw_mutex$。
  • 最后一个读者(Last Reader):它承担了“清道夫”的角色。只有当它是最后一个离开时(计数变回 0),它才负责释放 $rw_mutex$ 锁,给等待的写者机会。

4. 读者优先的缺陷:写者饥饿

读者优先存在一个明显的问题:如果读者源源不断地到来,read_count 永远不会减到 0,那么 $rw_mutex$ 就永远不会释放。

结果:写者会陷入无限期的等待,即写者饥饿(Starvation)

5. 信号量设置

  • rw_mutex = 1:保护共享资源的互斥锁。
  • r_mutex = 1:保护 read_count 的原子操作。
  • w_mutex = 1:保护 write_count 的原子操作。
  • gate = 1(关键):用于控制读者进入的“大门”。写者会锁定这把锁来封锁读者。
  • read_count = 0:当前正在读的读者数量。
  • write_count = 0:当前正在等待或正在写的写者数量。

6. 代码实现

// 写者进程
void writer() {while(true) {P(w_mutex);if (write_count == 0) {P(gate);      // 第一个写者到来,把“大门”锁死,禁止后续读者进入}write_count++;V(w_mutex);P(rw_mutex);      // 申请资源锁,确保同一时间只有一个写者在写// 执行写操作...V(rw_mutex);      // 释放资源锁P(w_mutex);write_count--;if (write_count == 0) {V(gate);      // 最后一个写者离开,打开“大门”,允许读者进入}V(w_mutex);}
}// 读者进程
void reader() {while(true) {P(gate);          // 读者必须先通过这扇“门”P(r_mutex);       // 锁定 read_countif (read_count == 0) {P(rw_mutex);  // 第一个读者负责抢资源锁(针对写者)}read_count++;V(r_mutex);V(gate);          // 读者登记完后,立刻释放门锁,允许其他读者进来// 执行读操作...P(r_mutex);read_count--;if (read_count == 0) {V(rw_mutex);  // 最后一个读者负责释放资源锁}V(r_mutex);}
}

7. 为什么这样能实现“写者优先”?

我们可以分情况看这个逻辑:

情况 A:写者到来时,已有读者在读

  1. 第一个写者到达,执行 P(gate)。此时 gate 被写者占用。
  2. 后续到来的读者会被阻塞在 P(gate),无法进入临界区,即使现在还有旧读者在读。
  3. 已经在读的旧读者继续读,读完后 read_count 逐渐减小。
  4. 最后一个旧读者离开,执行 V(rw_mutex)
  5. 写者拿到 rw_mutex,开始写操作。

情况 B:多个写者连续到来

  1. 第一个写者锁定了 gate
  2. 后续写者只需要增加 write_count,并排队等 rw_mutex
  3. 只要 write_count 不减到 0,gate 就一直被锁定,任何新读者都进不来
  4. 这样就保证了写者可以“插队”到新读者前面,优先处理所有的写请求。

总结

  • 读者优先:读者之间可以“拉帮结伙”,只要队伍不断,写者就得等。
  • 写者优先:写者拥有“封锁权”(gate信号量)。只要写者想写,它就先把门关上,等屋里剩下的读者走完,它就立刻进去,新读者只能在门口排队。

注意: 极致的写者优先可能会导致读者饥饿(如果写者源源不断地来)。在现代操作系统中,通常会使用更复杂的机制(如公平读写锁)来平衡两者的关系。

4.哲学家就餐问题

1. 场景设定

想象一下,有 5 位哲学家围坐在一张圆桌旁,他们的一生只做两件事:思考吃饭

  • 圆桌: 桌子上有一碗无限量的意大利面(或米饭)。
  • 餐具: 桌上有 5 支叉子(或筷子),分别放置在每两位哲学家之间。
  • 规则:
    1. 哲学家在思考时,不与外界交互。
    2. 当哲学家饿了,他会试图捡起左右两边的叉子(一次只能捡一支)。
    3. 只有当他同时拿到两支叉子时,才能进食。
    4. 吃完后,他放下两支叉子,继续思考。

2. 核心问题

如果没有任何规则限制,让哲学家们自由行动,系统会出现两个严重问题:

A. 死锁 (Deadlock)

假设所有哲学家同时决定进食,并且都同时拿起了自己左手边的叉子。

  • 每个人手里都有一支叉子(左手的)。
  • 每个人都在等待右手边的叉子。
  • 因为每个人都拿着左边的叉子不放,导致右手边的叉子永远被邻居占用。
  • 结果: 所有人都在无限等待,没有人能吃到东西,程序卡死。

B. 饥饿 (Starvation)

即使没有发生死锁,也可能出现分配不均。如果哲学家 P1 和 P3 进食非常快且频繁,夹在中间的 P2 可能永远抢不到叉子(当 P1 吃完放下时,P3 正在吃;当 P3 放下时,P1 又拿起来了)。

  • 结果: 某些进程(哲学家)长期得不到资源,无法推进。

3. 常见的解决方案

为了解决这些问题,我们需要引入一种算法或协议来管理叉子的分配。

方案一:资源分级 (Resource Hierarchy) —— 迪杰斯特拉解法

这是最著名的解法。给每支叉子编号(1 到 5)。

  • 规则: 哲学家必须总是先拿编号较小的叉子,再拿编号较大的叉子。
  • 效果:
    • 哲学家 P1, P2, P3, P4 都会先拿左手边的叉子(低号)。
    • 关键在于最后一位哲学家 P5。他的左手是叉子 5,右手是叉子 1。根据规则,他必须先拿叉子 1。
    • 如果叉子 1 已经被 P1 拿走了,P5 就会阻塞等待,而不会拿起叉子 5
    • 这样叉子 5 就空出来了,P4 可以拿到并进食。死锁被打破。

方案二:引入服务员 (The Arbiter / Monitor)

引入一个中心管理者(服务员)。

  • 规则: 哲学家在拿叉子之前必须先征得服务员同意。服务员只在“两支叉子都空闲”的情况下才允许哲学家拿起叉子。
  • 计算机术语: 这相当于使用了互斥锁 (Mutex)管程 (Monitor) 机制,保证拿叉子这个动作的原子性。

方案三:限制就餐人数 (N-1 策略)

  • 规则: 桌上有 5 个座位,但最多只允许 4 位哲学家同时坐下(申请进食)。
  • 原理: 根据鸽巢原理,如果有 5 支叉子和 4 个人,至少有 1 个人能拿到 2 支叉子。这样就能保证系统总是在运行,不会全部卡死。

方案四:奇偶策略 (Chandy/Misra 解法的简化版)

  • 规则: 奇数号哲学家先拿左边叉子,偶数号哲学家先拿右边叉子。
  • 效果: 这会破坏环形的等待依赖,从而避免死锁。

4. 总结:这在计算机中意味着什么?

哲学家就餐问题并不是真的关于吃饭,它是操作系统设计的核心隐喻:

  • 哲学家 = 进程/线程 (Process/Thread)
  • 叉子 = 共享系统资源 (如:打印机、数据库锁、内存区域)
  • 死锁 = 多个进程互相持有对方需要的资源且不释放。
  • 饥饿 = 进程调度算法不公平,导致低优先级进程永远无法运行。

这个模型教会了工程师:在设计并发系统时,必须精心设计资源申请的顺序锁的机制,否则系统极其容易崩溃。

6.管程

1.管程的定义

管程 (Monitor) 是操作系统和并发编程中的一个高级同步原语(Synchronization Primitive)。

简单来说,管程是一种“类” (Class) 或者一种特殊的软件模块,它把共享变量以及对这些变量进行操作的函数(过程)封装在了一起。

它的核心思想是:把复杂的同步机制(如加锁、解锁)封装在黑盒子里,让程序员更简单、安全地使用共享资源。


1. 核心定义

在计算机科学中,管程由以下三部分组成:

  1. 共享变量 (Shared Variables): 这里面存放的是所有线程需要争抢的公共资源(比如“哲学家问题”里的叉子状态)。
  2. 入口过程 (Procedures/Methods): 一组函数。外部线程只能通过调用这些函数来访问内部的共享变量,除此之外无法直接触碰数据。
  3. 条件变量 (Condition Variables): 用于管理线程的等待和唤醒(比如 wait()signal())。

2. 管程的两大特性

管程之所以强大,是因为它自动解决了并发编程中最头疼的两个问题:

A. 自动互斥 (Mutual Exclusion)

这是管程最重要的特性。

  • 规则: 在任何时刻,最多只能有一个线程在管程内部执行某个过程。
  • 如何实现: 编译器会自动为管程的方法加上“锁”。如果有两个线程同时调用管程里的方法,第二个线程会被自动阻塞,直到第一个线程执行完退出管程。
  • 好处: 程序员不需要手动写 lock.acquire()lock.release(),避免了忘记解锁导致的死锁问题。

B. 条件同步 (Condition Synchronization)

有时候线程进来了,但条件不满足(比如哲学家进来了,但发现叉子被占用了),它不能干占着位置。

  • 规则: 线程可以调用 wait() 操作,暂时放弃管程的控制权(释放锁),进入等待队列休眠。
  • 唤醒: 当另一个线程执行完某些操作(比如放下了叉子),可以调用 signal() (或 notify) 来唤醒正在等待的线程。

3. 通俗类比:医院诊室

为了理解管程,我们可以把它想象成一个只有一位医生的诊室

  1. 诊室 (Monitor): 包含了医生、医疗器械(共享资源)。
  2. 门锁 (互斥锁): 诊室的门一次只能进一个病人。如果里面有人,外面的人必须排队(互斥)。
  3. 候诊区 (条件变量/等待队列):
    • 情景: 病人 A 进去了(获得了锁),但医生说:“你需要先拍个片子才能看病。”(条件不满足)。
    • Wait: 病人 A 不能占着诊室不走,他必须出门去候诊区等待(释放锁,进入 Wait 队列)。
    • Signal: 这时,病人 B 进来了。看完病后,护士可能会喊:“刚才那个拍片子的,你可以回来了。”(发送 Signal,唤醒 A)。

4. 编程语言中的例子

管程的概念在现代编程语言中应用非常广泛,最典型的例子就是 Java

在 Java 中,synchronized 关键字和 Object 类的 wait() / notify() 方法共同构成了一个管程机制。

class DiningPhilosophers {// 这是一个管程 (Monitor) 类// 1. 共享数据boolean[] forks = new boolean[5]; // 2. 入口过程 (被 synchronized 保护,实现了自动互斥)public synchronized void takeForks(int i) {while (forks[i] || forks[(i + 1) % 5]) { // 如果叉子被占用try {wait(); // 3. 条件变量:进入等待,释放锁} catch (InterruptedException e) {}}// 拿到叉子forks[i] = true;forks[(i + 1) % 5] = true;}public synchronized void putForks(int i) {// 放下叉子forks[i] = false;forks[(i + 1) % 5] = false;notifyAll(); // 3. 条件变量:唤醒其他等待的哲学家}
}

5. 总结:管程 vs 信号量 (Semaphore)

  • 信号量 (Semaphore) 像是“红绿灯”。它是低级的同步工具,程序员必须小心翼翼地手动控制 P操作(减)和 V操作(加)。如果写错了(比如写了两个 P),程序就会死锁。
  • 管程 (Monitor) 像是“带自动门的房间”。它是高级的同步工具,利用面向对象的封装思想,把锁的管理交给编译器/运行时环境,让代码更清晰、更不容易出错。

在解决哲学家就餐问题时,使用管程(方案二)通常比使用纯信号量代码更易读,逻辑也更清晰。

2.管程的条件变量

如果把互斥锁看作是管程的“外层大门”(控制谁能进来),那么条件变量就是管程内部的“休息室”或“红绿灯”(控制谁能继续往下执行)。

以下是关于管程中条件变量的深度解析:

1. 为什么光有“锁”还不够?

管程的互斥锁(Mutex)只能保证“同一时间只有一个人在屋里”。但在屋里的人可能会遇到这种情况:“我进来了,但发现干活的材料还没准备好。”

  • 如果没有条件变量: 这个人(线程)只能占着屋子(持有锁)死等,或者不断进出屋子去检查(忙等待,Busy Waiting)。这会导致效率极低,甚至死锁(因为他占着锁,别人没法进来送材料)。
  • 有了条件变量: 这个人可以主动说:“材料没好,我先交出钥匙(释放锁),去旁边的长椅上睡觉(阻塞),等材料来了你们叫醒我。”

2. 条件变量的内部结构

一个条件变量内部主要包含两个东西:

  1. 一个等待队列 (Wait Queue):存放那些因为条件不满足而挂起(睡眠)的线程。
  2. 相关操作方法:即 wait(等待)和 signal(唤醒)。

注意:一个管程可以包含多个条件变量。

  • 例如在“生产者-消费者”问题中,管程里通常有两个条件变量:
    • not_full:队列满了,生产者去这里排队。
    • not_empty:队列空了,消费者去这里排队。

3. 核心操作详解

条件变量的操作必须在持有管程锁的前提下调用。

A. Wait (等待)

当线程发现条件不满足(比如缓冲区空了)时调用。它的执行步骤非常关键,是原子性的:

  1. 释放锁:当前线程释放管程的互斥锁(这让其他线程有机会进入管程修改状态)。
  2. 自我阻塞:当前线程进入该条件变量的等待队列,线程状态变为 Blocked/Waiting。
  3. 等待唤醒:线程在这里停止执行,直到被别人唤醒。
  4. 重新抢锁:被唤醒后,线程必须重新竞争并获取管程的互斥锁,才能从 wait 函数返回并继续执行。

B. Signal / Notify (唤醒)

当某个线程改变了管程内部的数据(比如生产者放入了一个数据),它调用此方法。

  • 它会从等待队列中取出一个线程,将其状态改为 Ready(就绪),让它有机会去抢锁。
  • 如果没有线程在等待,Signal 操作通常会被忽略(它是无记忆的,不同于信号量的 V 操作)。

C. Broadcast / NotifyAll (广播)

  • 唤醒等待队列中的所有线程。通常用于状态发生根本性改变,可能满足多个等待者需求的场景。

4. 两种著名的管程模型(语义)

当一个线程 A 执行 Signal 唤醒了线程 B 时,现在有两个线程(A 和 B)都想在管程里跑,谁先跑?这取决于管程的实现模型:

(1) Hoare 管程 (霍尔语义) —— "急不可待"

  • 逻辑:发出 Signal 的线程 A 立即阻塞,把锁和 CPU 让给被唤醒的线程 B。B 执行完或挂起后,A 再继续。

  • 特点:B 醒来时,条件一定是满足的(因为 A 刚改完就切给 B 了)。

  • 代码写法:可以用 if 判断条件。

    if (queue.isEmpty()) wait(); // 醒来时肯定有数据
    
  • 现状:虽然理论完美,但实现复杂且上下文切换开销大,现代系统很少用

(2) Mesa 管程 (汉森语义) —— "通知完继续干"

  • 逻辑:发出 Signal 的线程 A 继续执行直到离开管程或自己 Wait。被唤醒的线程 B 只是从等待队列移到了就绪队列,它需要等 A 释放锁后,去和其他线程重新抢锁

  • 特点:当 B 真正抢到锁进去时,原来的条件可能又变了(可能被路人甲线程 C 抢先冲进来把数据拿走了)。

  • 代码写法必须while 循环判断条件。

    while (queue.isEmpty()) wait(); // 醒来后必须再检查一遍
    
  • 现状Java (Object monitor), C++ (std::condition_variable), Pthreads 几乎所有主流编程语言和库都使用这种模型。

7.为什么要引入进程同步的概念

1. 根本原因:并发带来的“竞争条件” (Race Condition)

在现代操作系统中,多个进程(或线程)是并发运行的(看起来像是同时在跑)。 如果这些进程只是各玩各的(互不相干),那没问题。但问题在于,它们通常需要操作同一个东西(比如同一个全局变量、同一个文件、同一个数据库记录)。

当多个进程同时争抢修改同一个数据,且没有任何规则限制时,执行结果就取决于运气(即谁先跑、谁后跑、谁在中间被 CPU 切走了)。这就叫竞争条件

经典的“银行账户”灾难案例

假设账户余额 Balance = 100

  • 进程 A 想存 10 元(预期结果 110)。
  • 进程 B 想取 10 元(预期结果 90)。
  • 正确结果 应该是 100(+10 -10)。

如果没有同步,可能会发生这种“时序错误”:

  1. 进程 A 读取余额:拿到了 100
  2. (此时 CPU 发生切换,暂停 A,运行 B)
  3. 进程 B 读取余额:也拿到了 100(因为 A 还没来得及写回去)。
  4. 进程 B 计算 100 - 10 = 90,将 90 写入数据库。
  5. (此时 CPU 切回 A)
  6. 进程 A 继续刚才的计算 100 + 10 = 110,将 110 写入数据库。

结果: 余额变成了 110。进程 B 的取款操作被“覆盖”了,银行亏了 10 块钱。这就是丢失修改 (Lost Update)


2. 硬件层面的真相:操作不是“原子”的

你可能会问:“写一行代码 count++ 不就是一步吗?为什么会被打断?”

这是程序员的错觉。在高级语言里的一行代码,翻译成机器指令(汇编)通常是三步

  1. LOAD: 把内存里的值读到 CPU 寄存器。
  2. ADD: 在寄存器里加 1。
  3. STORE: 把寄存器的值写回内存。

操作系统可能在任何两步指令之间发生中断和上下文切换。只要在第 3 步还没完成前切走了,数据就会烂掉。

进程同步就是为了保证这三步操作要么全做完,要么全不做(原子性)。


3. 同步的两大具体目标

引入进程同步,主要为了实现两个目的:

A. 互斥 (Mutual Exclusion) —— "别打架"

  • 含义: 某些资源(临界资源)一次只能给一个人用。
  • 例子: 打印机。
  • 如果不刚步: 进程 A 打印“Hello”,进程 B 打印“World”。如果没互斥,打印纸上可能会出现 HWeolrldlo 这种乱码。
  • 解决: 也就是我们之前讨论的锁 (Lock / Mutex)

B. 顺序协调 (Ordering) —— "排好队"

  • 含义: 进程之间有依赖关系,必须按特定顺序执行。
  • 例子: 生产者-消费者。
  • 如果不同步: 消费者去仓库取货,但生产者还没把货做出来。消费者会取到一个空值或报错。
  • 解决: 消费者必须生产者发信号说“好了”才能动。也就是我们讨论的条件变量 (Condition Variable)信号量 (Semaphore)

总结

为什么要引入进程同步?

  1. 为了正确性: 防止因为争抢资源导致数据错乱(如银行余额算错)。
  2. 为了有序性: 让需要配合的进程按照既定的逻辑顺序(你做完我再做)执行。

8.进程间的关系

在操作系统中,虽然每个进程都有自己独立的内存空间(看起来像是老死不相往来的孤岛),但为了完成复杂的任务,它们之间必然存在各种联系。

进程间的关系主要可以归纳为以下 三类核心关系一种特殊的层级关系


1. 竞争关系 (Competition) —— "抢资源"

这是最基础、最常见的关系。

  • 定义: 多个进程同时想要访问同一个不可共享的资源(称为临界资源,如打印机、磁带机、共享变量)。
  • 特点:
    • 排他性: 你用了,我就不能用。
    • 被动: 进程彼此之间其实并不认识,只是因为都要抢同一个东西才产生了联系。
  • 核心机制:互斥 (Mutual Exclusion)
    • 需要使用“锁”机制。
    • 口号: “借过,让我先用一下,你排队。”
  • 潜在风险: 如果处理不好,会导致死锁 (Deadlock)饥饿 (Starvation)

2. 协作关系 (Cooperation) —— "接力跑"

这是比竞争更高级的关系。

  • 定义: 多个进程为了完成一个共同的任务,需要按照先后顺序互相配合。
  • 特点:
    • 有序性: 必须是你做完了第一步,我才能做第二步。
    • 主动: 进程之间知道对方的存在(或者至少知道需要等待某个信号)。
  • 核心机制:同步 (Synchronization)
    • 使用信号量、条件变量。
    • 口号: “你先跑,我不急,等你把棒交给我,我再跑。”
  • 典型例子:
    • 生产者-消费者: 厨师做完菜(进程A),服务员(进程B)才能端走。
    • 输入-计算-输出: 数据必须先录入,才能计算,最后才能打印。

3. 通信关系 (Communication) —— "打电话"

有时候进程之间不仅是简单的“排队”或“等待”,而是需要交换大量数据

  • 定义: 进程A把数据发送给进程B。
  • 难点: 因为操作系统为了安全,隔离了每个进程的内存,进程A不能直接去读写进程B的内存。
  • 核心机制:进程间通信 (IPC, Inter-Process Communication)
    • 操作系统必须提供“中间介质”来传递消息。
    • 常见方式:
      • 管道 (Pipe): 像水管一样,单向流动数据。
      • 共享内存 (Shared Memory): 划出一块大家都能看到的内存区域(速度最快)。
      • 消息队列 (Message Queue): 像写信投递一样。
      • 套接字 (Socket): 不同电脑上的进程跨网络聊天。

4. 亲缘关系 (Kinship) —— "父与子"

这是从生命周期的角度来看的关系。

  • 定义: 一个进程(父进程)创建了另一个进程(子进程)。
  • 特点:
    • 继承: 子进程通常会继承父进程的很多属性(如用户ID、文件描述符等)。
    • 责任: 父进程通常有义务回收子进程结束后的资源。
  • Linux 中的体现:
    • 使用 fork() 系统调用创建子进程。
    • 僵尸进程 (Zombie): 如果子进程死了,父进程没给它“收尸”(读取退出状态),子进程就会变成僵尸。
    • 孤儿进程 (Orphan): 如果父进程先死了,子进程还没死,子进程就成了孤儿,通常会被“进程祖先”(init/systemd)收养。

总结对比表

关系类型 核心行为 关键词 生活类比 解决机制
竞争关系 争抢资源 互斥 (Mutual Exclusion) 独木桥,一次只能过一人 互斥锁 (Mutex)
协作关系 互相配合 同步 (Synchronization) 流水线,拧完螺丝再喷漆 信号量、条件变量
通信关系 交换数据 IPC (Inter-Process) 打电话、发邮件 管道、共享内存
亲缘关系 创建与被创建 父子 (Parent-Child) 父亲生孩子,孩子继承基因 fork() / wait()

9.补充

  1. 正在访问临界资源的进程被中断

    正确答案:C. 允许其他进程抢占处理器,但不得进入该进程的临界区

    详细解析: 这道题考察的是“临界区”与“处理器调度”之间的区别。

    1. 关于处理器抢占
      • 当一个进程因为申请 I/O 操作而阻塞时,它会主动放弃 CPU(或被系统中断挂起)。为了提高 CPU 利用率,操作系统必须允许调度其他就绪进程来使用处理器。因此 D 错误。
    2. 关于临界区保护
      • 互斥原则:临界资源在同一时刻只能由一个进程访问。
      • 虽然该进程现在去“睡觉”等 I/O 了,但它并没有退出临界区,它依然占有着那个临界资源(比如它还没打印完,或者还没改完那个共享变量)。
      • 如果此时允许其他进程进入同一个临界区(选项 A),就会破坏互斥性,导致数据混乱。
    3. 结论
      • 其他进程可以跑(抢占处理器),但如果其他进程也想进入同一个临界区,它们会被挡在门外(阻塞),直到原进程回来并完成访问退出临界区为止。

    知识点小贴士

    • 并发执行 = 只要路够宽,大家都能跑。
    • 信号量 = 红绿灯,用来防止相撞(互斥)或指挥先后顺序(同步)。
    • 临界区 = 窄桥。你在桥上抛锚了(I/O 中断),别的车可以绕道走别的路,但绝对不能上你占着的这座窄桥。
  2. 题目问:“临界区是指并发进程访问共享变量段的( )。”

    • A. 管理信息:指系统为了管理进程而维护的数据结构(如 PCB),并非临界区。
    • B. 信息存储:指的是存储数据的介质或空间。
    • C. 数据:共享变量本身被称为临界资源(Critical Resource),而不是临界区。
    • D. 代码程序:正确。临界区是指每个进程中访问临界资源的那段代码
  3. 可重入代码(又称“纯代码”,Pure Code)是指一种允许多个进程同时访问,并且能够正确运行的程序代码。即使一个进程在执行这段代码的过程中被中断,转而去运行另一个也调用该代码的进程,待原进程恢复执行时,其运行结果依然正确,不会发生混乱。

    2. 可重入代码

    • 不修改自身:代码段在执行过程中不会修改其指令部分。
    • 不使用全局/静态变量:它不依赖于任何全局或静态变量来存储中间状态,而是将数据保存在进程各自的栈(Stack)*或*寄存器中。
    • 只读性:通常情况下,可重入代码被视为只读的,这样多个进程并发执行时,谁也不会干扰谁。
  4. B. 进程具有动态性: 动态性是指进程有生命周期(创建、运行、撤销),这是进程的基本特征,但不是需要同步的根本原因。

    D. 进程具有结构性: 结构性是指进程由程序段、数据段和进程控制块(PCB)组成,这描述的是进程的形态,而非执行逻辑。

  5. 在操作系统中,进程 A(生产者)和进程 B(消费者)共享同一个缓冲区,它们之间存在两种基本的制约关系:

    1. 同步关系 (Synchronization)

    • 逻辑顺序: 进程 A 必须先产生数据并放入缓冲区,进程 B 才能从缓冲区读取数据。
    • 协作制约: 如果缓冲区为空,进程 B 必须等待进程 A 放入数据;如果缓冲区满了,进程 A 必须等待进程 B 取走数据。这种因为执行顺序而产生的制约就是同步

    2. 互斥关系 (Mutual Exclusion)

    • 资源竞争: 缓冲区是一个临界资源(Critical Resource)。
    • 访问限制: 为了防止数据混乱(例如 A 正在写入时 B 同时读取,导致读取到错误或不完整的数据),必须保证同一时刻只有一个进程在操作该缓冲区。这种排他性的访问制约就是互斥
  6. A. 机器指令: P、V 操作是逻辑上的一组操作,并非单条的硬件 CPU 指令。

    B. 系统调用命令: 虽然用户程序通过系统调用来请求 P、V 操作,但从其本质定义来看,它们属于内核中的“原语”。

    C. 作业控制命令: 这是用户在控制台或脚本中使用的(如 run, stop),与进程间的微观同步无关。

  7. 第 27 题:临界区的构成

    题目: 一个系统中共有 5 个并发进程涉及某个相同的变量 A,变量 A 的相关临界区是由( )个临界区构成的。

    答案:C

    解析:

    1. 什么是临界资源? 变量 A 是被多个进程共享的资源,每次只允许一个进程访问,因此它是“临界资源”。
    2. 什么是临界区(Critical Section)? 临界区是指每个进程中访问临界资源的那段代码
    3. 计算逻辑: * 因为系统中共有 5 个进程
      • 每个进程为了访问变量 A,在其代码中都必须包含一段专门访问该变量的区域(临界区)。
      • 因此,总共会有 5 条对应的代码段散落在 5 个不同的进程中。
      • 结论:变量 A 相关的临界区由 5 个 临界区构成。
  8. 我们可以把管程类比为一个面向对象编程中的“类(Class)”:

    • 私有成员变量 = 局限于管程的共享数据结构(选项 A)
    • 公共成员方法 = 对数据操作的过程(选项 B)
    • 构造函数 = 设置初始值的语句(选项 D)
    • C. 管程外过程调用管程内数据结构的说明
    • 为什么 C 是错误的?
      • 封装性原则: 管程的核心特性之一是信息掩蔽。管程内部的数据结构是私有的(局部于管程),外部过程绝对不能直接访问或调用管程内部的数据结构。
      • 调用方式: 外部只能调用管程提供的“过程入口”,而不需要(也不被允许)包含任何关于“直接调用管程内数据结构”的说明。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询