banner
NEWS LETTER

第七章:进程同步与死锁

Scroll down

一、进程同步算法题解法详解

1. 基本概念深入理解

临界资源
- 定义:一次仅允许一个进程使用的共享资源
- 例子:打印机、共享变量、数据文件等
- 特性:必须互斥访问,否则会产生数据不一致

同步
- 定义:多个进程为了完成共同任务而需要协调执行顺序
- 例子:生产者-消费者问题,生产者生产后消费者才能消费

互斥
- 定义:保证多个进程不能同时进入临界区
- 实现方式:信号量、锁等机制

2. 信号量使用范式

二元信号量(互斥锁):

Semaphore mutex = 1;  // 初始值为1  

Process(){  
    wait(mutex);      // 进入临界区  
    // 临界区代码  
    signal(mutex);    // 离开临界区  
}  

计数信号量(资源计数):

Semaphore resources = N;  // 初始有N个资源  

Process(){  
    wait(resources);     // 申请一个资源  
    // 使用资源  
    signal(resources);   // 释放资源  
}  

3. 经典例题详细分析

例1:水果盘子问题(生产者-消费者变种)

问题定义
- 一个盘子,每次只能放一个水果
- 爸爸:专门放苹果
- 妈妈:专门放橘子
- 儿子:专门吃橘子
- 女儿:专门吃苹果
- 同步关系:
- 盘子空时,爸爸或妈妈才能放水果
- 有对应水果时,儿子或女儿才能取水果

信号量设计

Semaphore plate = 1;   // 盘子互斥,初始有1个空位  
Semaphore apple = 0;   // 苹果数量,初始0个  
Semaphore orange = 0;  // 橘子数量,初始0个  

代码逻辑

// 爸爸进程:生产者(苹果)  
Dad(){  
    while(1){  
        prepare_apple();  // 准备苹果(非临界区)  
        wait(plate);      // 等待盘子空  
        put_apple();      // 放苹果到盘子  
        signal(apple);    // 通知有苹果可用  
        // 注意:这里没有signal(plate),因为盘子已被占用  
    }  
}  

// 女儿进程:消费者(苹果)  
Daughter(){  
    while(1){  
        wait(apple);      // 等待有苹果  
        take_apple();     // 从盘子取苹果  
        signal(plate);    // 释放盘子  
        eat_apple();      // 吃苹果(非临界区)  
    }  
}  
// 妈妈和儿子的代码类似,只是操作的对象不同  

关键点
- plate信号量控制盘子访问的互斥
- appleorange信号量实现生产者-消费者同步
- 每个水果的放和取是配对的


例2:和尚打水问题(多资源同步)

问题定义
- 资源:水井(1口)、水缸(容量10桶)、水桶(3个)
- 小和尚:从井打水倒入缸
- 老和尚:从缸取水喝
- 约束条件:
- 水井窄,每次只能一个桶取水
- 水缸不能同时入水和取水
- 水桶总数3个,要竞争使用

信号量设计

Semaphore well = 1;    // 水井互斥  
Semaphore vat = 1;     // 水缸互斥  
Semaphore pail = 3;    // 可用水桶数  
Semaphore empty = 10;  // 水缸剩余空间  
Semaphore full = 0;    // 水缸当前水量  

老和尚取水逻辑

Old_Monk(){  
    while(1){  
        wait(full);     // 等待水缸有水  
        wait(pail);     // 申请水桶  
        wait(vat);      // 申请水缸访问权  
        
        take_water_from_vat();  // 从水缸取水  
        
        signal(vat);    // 释放水缸  
        signal(empty);  // 水缸空间+1  
        drink_water();  // 喝水  
        signal(pail);   // 归还水桶  
    }  
}  

小和尚打水逻辑

Young_Monk(){  
    while(1){  
        wait(empty);    // 等待水缸有空间  
        wait(pail);     // 申请水桶  
        wait(well);     // 申请水井  
        
        fetch_water_from_well();  // 从井打水  
        
        signal(well);   // 释放水井  
        wait(vat);      // 申请水缸  
        pour_water_to_vat();  // 倒水入缸  
        signal(vat);    // 释放水缸  
        signal(full);   // 水缸水量+1  
        signal(pail);   // 归还水桶  
    }  
}  

同步关系分析
- 水井和水缸都是临界资源,需要互斥访问
- 水桶数量有限,需要信号量控制
- 水缸容量限制,通过empty/full信号量同步


例3:吸烟者问题(多生产者单消费者变种)

问题定义
- 供应者:提供三种材料中的两种
- 三个吸烟者:各拥有一种固定材料
- 规则:
- 供应者随机放两种材料到桌子
- 拥有第三种材料的吸烟者可以卷烟抽烟
- 抽烟完成后通知供应者

信号量设计

Semaphore offer1 = 0;  // 烟草+纸组合就绪  
Semaphore offer2 = 0;  // 烟草+胶水组合就绪  
Semaphore offer3 = 0;  // 纸+胶水组合就绪  
Semaphore finish = 0;  // 抽烟完成信号  

供应者逻辑

Supplier(){  
    while(1){  
        int random = rand() % 3;  // 随机选择组合  
        
        switch(random){  
            case 0: signal(offer1); break;  // 提供烟草+纸  
            case 1: signal(offer2); break;  // 提供烟草+胶水  
            case 2: signal(offer3); break;  // 提供纸+胶水  
        }  
        
        wait(finish);  // 等待吸烟完成  
    }  
}  

吸烟者逻辑

// 拥有烟草的吸烟者(需要纸+胶水)  
Smoker_Tobacco(){  
    while(1){  
        wait(offer3);      // 等待纸+胶水组合  
        make_cigarette();  // 卷烟  
        smoke();           // 抽烟  
        signal(finish);    // 通知完成  
    }  
}  

// 拥有纸的吸烟者(需要烟草+胶水)  
Smoker_Paper(){  
    while(1){  
        wait(offer2);      // 等待烟草+胶水组合  
        make_cigarette();  
        smoke();  
        signal(finish);  
    }  
}  

// 拥有胶水的吸烟者(需要烟草+纸)  
Smoker_Glue(){  
    while(1){  
        wait(offer1);      // 等待烟草+纸组合  
        make_cigarette();  
        smoke();  
        signal(finish);  
    }  
}  

关键特性
- 供应者相当于生产者,吸烟者相当于消费者
- 但每个消费者只消费特定的”产品组合”
- 通过不同的信号量区分不同材料组合


例4:独木桥问题(读者-写者问题变种)

问题定义
- 独木桥:一次只能通行一个方向的队伍
- 两个士兵队伍:分别有m和n个士兵
- 规则:
- 桥上不能有相反方向的士兵
- 同一队伍可以连续过桥
- 必须等前一队伍全部过完,另一队才能开始

这实际上是读者-写者问题的变种
- 每个队伍相当于”读者”
- 关键是要保证同一时间只有一个方向的”读”

信号量设计

int count1 = 0, count2 = 0;      // 各队桥上人数计数  
Semaphore mutex1 = 1, mutex2 = 1; // 保护count1, count2  
Semaphore bridge = 1;            // 桥的互斥访问  

第一队士兵逻辑

Soldier_Team1(){  
    wait(mutex1);        // 申请count1的访问权  
    count1++;            // 本队桥上人数+1  
    
    if(count1 == 1){     // 如果是本队第一个上桥  
        wait(bridge);    // 申请桥的使用权(阻止另一队)  
    }  
    signal(mutex1);      // 释放count1  
    
    // 过桥(临界区)  
    cross_bridge();  
    
    wait(mutex1);        // 再次申请count1访问权  
    count1--;            // 本队桥上人数-1  
    
    if(count1 == 0){     // 如果是本队最后一个下桥  
        signal(bridge);  // 释放桥的使用权(允许另一队)  
    }  
    signal(mutex1);      // 释放count1  
}  

第二队士兵逻辑类似,只是操作count2mutex2

工作原理
- bridge信号量保证两个队伍不会同时使用桥
- count1count2记录各队当前在桥上的人数
- 第一个上桥的士兵申请bridge,最后一个下桥的士兵释放bridge
- mutex1mutex2保护各自的计数器


二、死锁详解

1. 死锁定义与实例

死锁定义
一组进程因竞争资源而陷入相互等待的状态,每个进程都在等待另一个进程释放资源,导致所有进程都无法继续执行。

经典死锁例子

// 两个进程,两个资源(A和B)  
Semaphore A = 1, B = 1;  

Process_P0(){  
    wait(A);    // 持有A,等待B  
    wait(B);  
    // ...  
}  

Process_P1(){  
    wait(B);    // 持有B,等待A  
    wait(A);  
    // ...  
}  

执行顺序可能导致死锁

时间点    P0操作      P1操作      状态  
t0       wait(A)                 P0获得A  
t1                   wait(B)     P1获得B  
t2       wait(B)                 P0等待B(但B被P1持有)  
t3                   wait(A)     P1等待A(但A被P0持有)  
→ 死锁发生!  

2. 死锁的四个必要条件

  1. 互斥(Mutual Exclusion)
    • 资源只能独占使用
    • 例子:打印机不能同时被多个进程使用
  2. 持有并等待(Hold and Wait)
    • 进程持有至少一个资源,同时等待其他资源
    • 例子:P0持有A等待B,P1持有B等待A
  3. 不可抢占(No Preemption)
    • 资源只能由持有进程自愿释放
    • 系统不能强制收回资源
  4. 循环等待(Circular Wait)
    • 存在进程等待环:P0→P1→P2→…→P0
    • 每个进程都在等待下一个进程持有的资源

四个条件必须同时满足才会发生死锁

3. 资源分配图详解

资源分配图组成
- 圆形节点:表示进程(P₁, P₂, …)
- 矩形节点:表示资源类型(R₁, R₂, …)
- 圆点:矩形中的圆点表示资源实例
- 请求边:从进程指向资源的箭头(Pᵢ → Rⱼ)
- 分配边:从资源实例指向进程的箭头(Rⱼ → Pᵢ)

具体例子(来自PDF第38页):

    R1 (有1个实例)     R2 (有2个实例)     R3 (有1个实例)  
     ●                 ●●                ●  
     |                 |                 |  
     |                 |                 |  
    P1 ---→ R2        P2 ---→ R3        P3 ---→ R1  
            |                 |  
            |                 |  
           P2 ---→ R1        P3 ---→ R2  

这个图表示
- P1:持有R1的一个实例,请求R2
- P2:持有R2的一个实例,请求R3
- P3:持有R3的一个实例,请求R1

图中有循环:P1→R2→P2→R3→P3→R1→P1

死锁判断
- 有循环 + 每个资源只有一个实例 ⇒ 必然死锁
- 有循环 + 资源有多个实例 ⇒ 可能死锁(需要进一步分析)

4. 死锁处理方法对比

(1)死锁预防

策略:破坏四个必要条件中的至少一个

具体方法
- 破坏互斥:让资源可共享(但很多资源本质不能共享)
- 破坏持有等待:进程开始前申请所有所需资源
- 优点:简单
- 缺点:资源利用率低,可能饥饿
- 破坏不可抢占:强制收回资源
- 实现复杂,适用于易保存恢复状态的资源
- 破坏循环等待:给资源编号,按序申请
- 常用方法,但可能降低资源利用率

(2) 死锁避免 (Deadlock Avoidance)

策略:系统在分配资源前,会预先判断这次分配是否会导致系统进入不安全状态 (Unsafe State)。如果不安全,则进程必须等待。

银行家算法详解 (Banker’s Algorithm)

核心思想:银行家在批准贷款(分配资源)前,必须确保即使所有客户(进程)都提出最大贷款需求(Max),银行(系统)仍然有办法(一个安全序列)让所有客户都能最终完成贷款(执行完毕)。

  1. 数据结构
  • Available (可用资源):向量,表示当前每种资源有多少可用实例。

    • 示例: Available = (3, 3, 2) (A有3个, B有3个, C有2个)。
  • Max (最大需求):矩阵,表示 总共需要多少 资源。

    • 示例 (): Max[0] = (7, 5, 3)
  • Allocation (已分配):矩阵,表示 当前已持有 资源。

    • 示例 (): Allocation[0] = (0, 1, 0)
  • Need (还需资源):矩阵,表示 还需多少 才能完成。

    • Need = Max - Allocation

    • 示例 (): Need[0] = (7, 5, 3) - (0, 1, 0) = (7, 4, 3)

  1. 安全算法 (Safety Algorithm)

目的:检查当前系统状态是否安全。

  • 步骤 1: 初始化 Work = AvailableFinish[i] = false

    • 示例: Work = (3, 3, 2)Finish = [F, F, F, F, F]
  • 步骤 2: 寻找一个进程 ,满足 Finish[i] == falseNeed[i] <= Work

    • 示例 (第1轮):

      • ? Need[0]=(7,4,3) (3,3,2)?

      • ? Need[1]=(1,2,2) (3,3,2)?

  • 步骤 3: 找到 。模拟 执行完毕并释放资源

    • Work = Work + Allocation[1]

    • Work = (3, 3, 2) + (2, 0, 0) = (5, 3, 2)

    • Finish[1] = true

  • 步骤 2 (重复): Work 现在是 (5, 3, 2)

    • ? Need[0]=(7,4,3) (5,3,2)?

    • ? Need[2]=(6,0,0) (5,3,2)?

    • ? Need[3]=(0,1,1) (5,3,2)?

  • 步骤 3 (重复): 找到 。模拟 执行完毕并释放资源。

    • Work = Work + Allocation[3]

    • Work = (5, 3, 2) + (2, 1, 1) = (7, 4, 3)

    • Finish[3] = true

  • … (依此类推):

    • 接下来,Work = (7, 4, 3),可以满足 (Need=(7,4,3))。Work 变为 (7, 5, 3)

    • 接下来,Work = (7, 5, 3),可以满足 (Need=(6,0,0))。Work 变为 (10, 5, 5)

    • 接下来,Work = (10, 5, 5),可以满足 (Need=(4,3,1))。Work 变为 (10, 5, 7)

  • 步骤 4: 检查 Finish 数组是否全为 true

    • 示例: Finish 变为 [T, T, T, T, T]

    • 结论: 系统是安全的。安全序列之一为

  1. 资源请求算法 (Resource-Request Algorithm)

目的:当 发出请求 Request_i 时,决定是否分配。

  • 场景: 进程 请求 Request_1 = (1, 0, 2)

  • 步骤 1: 检查请求是否超过最大需求:

    • Request_1(1,0,2) Need_1(1,2,2)? 。继续。
  • 步骤 2: 检查是否有足够可用资源:

    • Request_1(1,0,2) Available(3,3,2)? 。继续。
  • 步骤 3: 系统假装分配资源:

    • Available = (3,3,2) - (1,0,2) = (2, 3, 0)

    • Allocation[1] = (2,0,0) + (1,0,2) = (3, 0, 2)

    • Need[1] = (1,2,2) - (1,0,2) = (0, 2, 0)

  • 步骤 4: 在这个新的假状态下,运行安全算法

    • Work = (2, 3, 0)

    • 检查发现,序列 仍然是一个安全序列。

    • 结论: 因为假装分配后系统仍然安全,所以批准 的请求。如果(在另一个例子中)运行安全算法发现系统变为不安全,那么 必须等待,并且系统状态回滚到第3步之前。


(3) 死锁检测 (Deadlock Detection)

策略:允许系统进入不安全状态甚至死锁状态,但系统会周期性地运行检测算法来发现死锁。

等待图方法 (单实例资源)

  • 节点是进程。

  • 表示 正在等待 释放资源。

  • 算法检测图中的环路。如果有环,就存在死锁。

检测算法 (多实例资源)

目的:检测当前系统状态是否已存在死锁。它与安全算法非常相似,但有关键区别:

  • 安全算法检查 Need,看未来是否安全。

  • 检测算法检查 Request(当前请求),看现在是否已卡住。

算法步骤 (基于 PDF 第81-84页的例子)

  1. 初始化: Work = AvailableFinish[i] = false (如果 持有资源)。

    • 场景: Available = (0, 0, 0)Work = (0, 0, 0)Finish = [F, F, F, F, F] (因为 都有 Allocation 不为0)。
  2. 查找: 寻找一个进程 ,满足 Finish[i] == falseRequest[i] <= Work

    • 示例:

      • ? Request[0]=(0,0,0) Work(0,0,0)?
  3. 模拟执行 (这就是你问的“为什么是加法”):

    • 找到了 ,意味着 不需要新资源就能执行完毕

    • 模拟 跑完并释放它持有的所有资源。

    • Work = Work + Allocation[0]

    • Work = (0, 0, 0) + (0, 1, 0) = (0, 1, 0)

    • Finish[0] = true

  4. 重复查找: Work 现在是 (0, 1, 0)

    • 示例:

      • ? Request[1]=(2,0,2) Work(0,1,0)?

      • ? Request[2]=(0,0,1) Work(0,1,0)?

      • ? Request[3]=(1,0,0) Work(0,1,0)?

      • ? Request[4]=(0,0,2) $Work(0,1,0)?

    • 找不到任何可以执行的进程。

  5. 检查: 算法结束。检查 Finish 数组。

    • Finish[i] == false 的是

    • 结论: 系统处于死锁状态,死锁的进程是


(4) 死锁恢复 (Recovery from Deadlock)

当检测到死锁后,系统必须打破它。

进程终止

  • 终止所有死锁进程:最简单粗暴的方法。在我们的检测示例中,将 全部终止。

  • 逐个终止进程:一次只终止一个进程,直到死锁环路被打破。选择终止谁,取决于:

    • 进程的优先级。

    • 进程已运行多久,还需多久。

    • 进程占用了多少资源。

    • 进程是交互式还是批处理。

资源抢占

  • 选择牺牲者:选择一个进程(或资源)作为“牺牲者”,成本最低。

  • 回滚 (Rollback):将牺牲进程回滚到某个安全状态,并释放其资源。

  • 饥饿 (Starvation):必须防止同一个进程总是被选为牺牲者。

如果您喜欢我的文章,可以考虑打赏以支持我继续创作.

其他文章
目录导航 置顶
  1. 1. 一、进程同步算法题解法详解
    1. 1.1. 1. 基本概念深入理解
    2. 1.2. 2. 信号量使用范式
    3. 1.3. 3. 经典例题详细分析
  2. 2. 二、死锁详解
    1. 2.1. 1. 死锁定义与实例
    2. 2.2. 2. 死锁的四个必要条件
    3. 2.3. 3. 资源分配图详解
    4. 2.4. 4. 死锁处理方法对比
请输入关键词进行搜索