从经典问题到银行家算法:死锁的预防、避免、检测与解除

引言

你正在食堂吃饭,左手抓着筷子,右手去拿酱油瓶。旁边的人也在吃饭,他左手拿着酱油瓶,右手想来抓你的筷子。你们两个都等着对方先松手,但谁也不肯先放——饭凉了,人还饿着。这就是死锁。

操作系统中,死锁(Deadlock)指两个或以上进程因互相等待对方持有的资源而永久阻塞的现象。如果没有外力干预,这些进程将永远卡在原地。这是我们操作系统第二章"进程与线程"的第四篇,也是收官篇。前三篇我们讨论了进程基础、调度算法与同步互斥——今天要解决的问题是:当进程间的资源竞争陷入僵局,操作系统该如何应对?

📌 核心要点

  • 死锁的四个必要条件缺一不可:互斥、不剥夺、请求保持、循环等待——破坏任一条件即可防止死锁
  • 三种处理策略各有取舍:预防简单粗暴但资源利用率低,避免(银行家算法)折中但需预知需求,检测解除追求利用率但恢复代价高
  • 银行家算法的核心操作只有两步:假设分配 → 检查是否存在安全序列。手动推演是408必考技能
  • 死锁、饥饿、死循环是三个完全不同的概念——这个选择题考点区分度极高,务必精确记忆
  • 哲学家进餐问题是死锁的经典模型,四种解法恰好对应预防死锁的四种思路

死锁的概念

什么是死锁?

死锁(Deadlock) 是指多个进程因竞争资源而造成的一种僵局——每个进程都在等待其他进程释放资源,但没有一个进程能主动释放自己已占有的资源,结果大家都无法向前推进。

三个关键点定义了死锁:至少两个进程参与;每个进程已占有某些资源,同时在等待其他资源;等待关系构成了闭环。三者缺一,就不构成死锁。

死锁一旦形成,仅靠进程自身无法打破。这也是它和"等一等就能过去"的临时阻塞的本质区别——死锁是结构性的、永久的阻塞。

为什么会发生死锁?

死锁产生的原因有三个:

  1. 对不可剥夺资源的竞争:多个进程竞争打印机、摄像头等独占性资源,且资源不可被强行剥夺
  2. 进程推进顺序非法:进程申请和释放资源的时机不当,导致循环等待链形成
  3. 信号量使用不当:P/V操作顺序错误会直接制造死锁——比如生产者-消费者问题中,把 P(mutex)P(empty) 顺序写反

[UNIQUE INSIGHT] 很多同学以为只有"竞争不可剥夺资源"才会导致死锁。实际上,信号量使用不当造成的死锁在考研大题中出现频率非常高——这属于"逻辑死锁"而非"物理死锁"。本质是:不管资源是物理设备还是信号量,只要满足四个必要条件,死锁就会发生。

死锁的四个必要条件

这四个条件必须同时成立才会产生死锁。换句话说,只要有一个不满足,死锁就不会发生:

序号 条件 含义 能否破坏?
1 互斥条件 资源每次只能被一个进程使用 不太可行(很多资源天生互斥)
2 不剥夺条件 进程获得的资源在未使用完前不能被抢走 可以,但代价大
3 请求和保持条件 进程已持有资源,又去申请新资源(被阻塞但旧资源不释放) 可以破坏
4 循环等待条件 存在一个"进程-资源"的闭环等待链 可以破坏

⚠️ 考点辨析:发生死锁一定存在循环等待;但存在循环等待未必发生死锁。如果同类资源有多个实例(比如有2台打印机),循环等待链可能被"多出来"的资源打破。408真题多次利用这个细节出迷惑选项。

下面用一个资源分配图直观展示:

[IMAGE] 资源分配图:进程P1持有资源R1、等待R2;进程P2持有资源R2、等待R1。P1→R2→P2→R1→P1 形成闭环,两个进程死锁。

     ┌─────── 等待 ───────┐
     ▼                    │
   ┌───┐               ┌───┐
   │ P1 │               │ P2 │
   └─┬─┘               └─┬─┘
     │ 持有               │ 持有
     ▼                    ▼
   ┌───┐               ┌───┐
   │ R1 │               │ R2 │
   └───┘               └───┘

死锁 vs 饥饿 vs 死循环(408高频考点)

这三个概念容易被混用,但在408考试中是明确定义的不同术语:

对比维度 死锁(Deadlock) 饥饿(Starvation) 死循环(Infinite Loop)
涉及进程数 至少两个进程互相等待 可能是一个进程长期得不到资源 可能是一个进程
进程状态 阻塞态 阻塞态或就绪态均可 运行态(占着CPU不撒手)
本质原因 操作系统资源分配策略或进程推进顺序有问题 操作系统调度策略不公(如SPF长进程饿死) 程序员的代码逻辑bug
责任归属 操作系统(管理者)的问题 操作系统的问题 应用程序开发者(被管理者)的问题
解决方案 预防/避免/检测解除 FCFS等公平调度策略 程序员debug
能否自行解开 不能——无外力介入永远阻塞 可能——如果短进程都结束 不能——除非外部中断

💡 记忆技巧:死锁是"互相等"(至少两人,都在阻塞态),饥饿是"一直等"(受害者一个人),死循环是"自己转"(运行态,代码写错了)。408选择题中经常混着考——看到"某个进程运行态一直执行"直接排除死锁和饥饿的选项。


死锁的处理策略总览

面对死锁问题,操作系统有三种态度,对应三类策略:

[CHART] 死锁三种处理策略对比图

策略 思路 时机 资源利用率 实现复杂度 典型算法
预防(Prevention) 破坏四个必要条件之一 死锁发生 较低 一次性分配、顺序资源分配
避免(Avoidance) 判断分配是否导致不安全状态 死锁发生 中等 银行家算法
检测与解除(Detection & Recovery) 允许死锁发生,检测到后解除 死锁发生 资源分配图化简、进程终止

📌 死锁的预防和避免都属于事先预防策略,但"避免"并不等同于"预防"——408考试中需严格区分为三种策略。

[INTERNAL-LINK] 如果对进程调度策略(如SPF)不够熟悉,建议回顾本系列第二篇《调度算法:从FCFS到多级反馈队列》。


死锁的预防:破坏四个必要条件

死锁预防(Deadlock Prevention)的思路直截了当:不让四个必要条件同时满足。破坏其中任意一个,死锁就不会发生。

破坏互斥条件

思路:让资源可以被同时访问,而不需要互斥使用。

可行性分析:不太可行。打印机、摄像头、写文件等资源本身就是互斥的——让两个进程同时写入同一块磁盘扇区会造成数据损坏。只有少数资源(如只读文件)可以共享。SPOOLing技术可以将独占设备改造成共享设备(如打印机假脱机),但这属于设备管理的范畴,不能推广到所有资源。

破坏不剥夺条件

思路:当进程申请的资源被占用时,操作系统可以强行夺走它已持有的资源。

具体做法:进程在申请新资源失败时,必须释放手中已有的全部资源,再重新申请。

缺点:代价较大。一个进程写文件写到一半,被剥夺了文件访问权——之前写的内容怎么办?回滚?保存断点?实现复杂且增加系统开销。只适用于处理器和内存这类"状态可保存恢复"的资源。

破坏请求和保持条件

思路:进程在开始运行前,一次性申请所有需要的资源。

具体做法(静态分配法):进程运行前一次性申请全部资源——要么全部拿到,要么一个都不拿。拿不到就等待,等待期间不占任何资源。

优点:简单、安全。

缺点:资源利用率极低——进程可能很晚才用到某个资源,但一开局就占着不放,其他进程只能干等。此外,很多进程在运行前并不知道自己需要哪些资源的全部。

另一种实现:进程申请新资源被阻塞时,先释放手中已有的全部资源。这实际上是在请求和保持条件上打了补丁。

⚠️ 这两种方法的核心区别:静态分配法强调"一开始就全拿",释放重申请法强调"拿不到就全放"。

破坏循环等待条件

思路:给系统中所有资源编号,规定进程只能按编号递增顺序申请资源。

具体做法(顺序资源分配法):假设资源有类型1、2、3、4、5。进程先申请资源2,之后只能申请编号大于2的资源(3、4、5),不能再回头申请资源1。这从根本上杜绝了循环等待链——循环等待链要求"等待关系可以首尾相连",而递增规则让箭头只能朝一个方向走。

优点:相比前几种方法,对资源利用率的折损最小。

缺点:给资源合理编号并不容易。如果进程真的需要先大后小的资源呢?只能被迫改变资源申请策略。新增资源时编号体系可能需要重新调整。

四种预防方案小结

破坏的条件 方法 实用程度
互斥 SPOOLing(有限的场景)
不可剥夺 强行剥夺已占有资源 低(代价大)
请求和保持 静态分配(一次性申请)
循环等待 顺序资源分配(按递增编号)

[PERSONAL EXPERIENCE] 实际工程中,死锁预防用得并不多——因为资源利用率损失太大。现代操作系统更倾向于"允许死锁,但不让它扩散"的策略(如Linux对内核锁的lockdep检测机制)。但对于考研,四种方法必须完整掌握。


死锁的避免:银行家算法

死锁避免(Deadlock Avoidance)比预防更聪明——它不是一刀切地破坏条件,而是在每次资源分配前做安全判断。如果这次分配会导致系统进入"不安全状态",就拒绝本次分配。

安全状态与安全序列

安全状态:系统能按照某个顺序(安全序列)将资源全部分配完毕,所有进程都能顺利完成。处于安全状态的系统,一定不会发生死锁。

不安全状态:找不到这样的顺序。注意——不安全状态不等于死锁!只能说"有可能"发生死锁。就像闯红灯未必出车祸,但风险急剧上升。

安全序列的存在性:如果存在一个序列 P1, P2, …, Pn,使得对每个 Pi,Pi 当前还需要的资源量(Need)≤ 系统当前可用的资源量(Available)+ 排在 Pi 前面的进程已持有的资源量(因为前面的进程完成后会释放资源)——那么这个序列就是安全序列。

⚠️ 408高频考点:安全状态一定无死锁,不安全状态不一定死锁。这一条被翻来覆去地考。

银行家算法原理

**银行家算法(Banker’s Algorithm)**由 Dijkstra 在1965年提出,名字的由来是一个形象的比喻:操作系统就像银行家,进程就像客户。银行家手里有一笔现金(系统资源),客户会申请贷款(资源请求)。银行家放款前需要判断——把钱借给这个客户后,剩下的钱还够不够满足所有其他客户的最大贷款额度,从而保证所有客户最终都能还钱。

算法需要维护四个数据结构:

数据结构 含义 维度
Available 向量 系统当前每种资源的可用数量 m(资源类型数)
Max 矩阵 每个进程对每种资源的最大需求 n × m(n为进程数)
Allocation 矩阵 每个进程当前已分配的资源量 n × m
Need 矩阵 每个进程还需要的资源量 n × m

满足恒等式:Need[i][j] = Max[i][j] - Allocation[i][j]

算法核心逻辑:

当进程 Pi 请求一组资源 Request[i] 时:
  1. 如果 Request[i] > Need[i] → 拒绝(申请超过了声明的最大需求)
  2. 如果 Request[i] > Available → 让 Pi 等待(资源不够用)
  3. 假设分配(试探性):
     Available = Available - Request[i]
     Allocation[i] = Allocation[i] + Request[i]
     Need[i] = Need[i] - Request[i]
  4. 对假设分配后的状态执行安全性检查
     如果存在安全序列 → 批准分配
     否则 → 拒绝分配,恢复原状态

安全性检查算法步骤

1. 初始化 Work = Available,Finish[i] = false(对所有 i)
2. 从进程集合中找一个满足以下条件的 Pi:
   Finish[i] == false 且 Need[i] ≤ Work
3. 如果找到:
   Work = Work + Allocation[i]  (模拟 Pi 完成后释放所有资源)
   Finish[i] = true
   回到步骤2
4. 如果所有 Finish[i] 都为 true → 系统处于安全状态
   否则 → 系统处于不安全状态

完整推演示例

下面用一个完整例子演示银行家算法的手动计算过程。这是408考研大题的标准步骤。

初始状态:假设系统中有5个进程 P0~P4,3类资源 A、B、C。已知:

Max 矩阵(每个进程对每类资源的最大需求):

进程 A B C
P0 7 5 3
P1 3 2 2
P2 9 0 2
P3 2 2 2
P4 4 3 3

Allocation 矩阵(已分配):

进程 A B C
P0 0 1 0
P1 2 0 0
P2 3 0 2
P3 2 1 1
P4 0 0 2

Available 向量:A=3, B=3, C=2

第一步:计算 Need 矩阵

Need = Max - Allocation:

进程 A B C
P0 7 4 3
P1 1 2 2
P2 6 0 0
P3 0 1 1
P4 4 3 1

第二步:寻找初始安全序列

Work = Available = (3, 3, 2),Finish 全部为 false。

轮次 可选进程 选中 Work变化 Finish 理由
1 P1(1,2,2)≤(3,3,2) ✓
P3(0,1,1)≤(3,3,2) ✓
P1 (3,3,2)+(2,0,0)=(5,3,2) P1=true P1的Need小且Work够
2 P3(0,1,1)≤(5,3,2) ✓
P4(4,3,1)≤(5,3,2) ✓
P3 (5,3,2)+(2,1,1)=(7,4,3) P3=true 选P3因为它的Need最小
3 P0(7,4,3)≤(7,4,3) ✓
P2(6,0,0)≤(7,4,3) ✓
P4(4,3,1)≤(7,4,3) ✓
P4 (7,4,3)+(0,0,2)=(7,4,5) P4=true 选三者均可,先选P4
4 P0(7,4,3)≤(7,4,5) ✓
P2(6,0,0)≤(7,4,5) ✓
P2 (7,4,5)+(3,0,2)=(10,4,7) P2=true P2只剩Need A
5 P0(7,4,3)≤(10,4,7) ✓ P0 Done 全部true P0最后完成

安全序列:P1 → P3 → P4 → P2 → P0(不唯一,P3→P1→P4→P2→P0 同样可行)

初始状态是安全的。

第三步:处理资源请求

现在 P1 申请资源 Request = (1, 0, 2)。判断能否批准:

  1. Request(1,0,2) ≤ Need1
  2. Request(1,0,2) ≤ Available(3,3,2) ✓
  3. 假设分配——更新各表:
Available P1-Alloc P1-Need
分配前 (3,3,2) (2,0,0) (1,2,2)
分配后 (2,3,0) (3,0,2) (0,2,0)
  1. 安全性检查——寻找安全序列:

Work = (2, 3, 0)。寻找 Need ≤ Work 的进程:

  • P1: (0,2,0) ≤ (2,3,0) ✓ → Work = (2,3,0)+(3,0,2)=(5,3,2),P1完成
  • P3: (0,1,1) ≤ (5,3,2) ✓ → Work = (5,3,2)+(2,1,1)=(7,4,3),P3完成
  • P4: (4,3,1) ≤ (7,4,3) ✓ → Work = (7,4,3)+(0,0,2)=(7,4,5),P4完成
  • P2: (6,0,0) ≤ (7,4,5) ✓ → Work = (7,4,5)+(3,0,2)=(10,4,7),P2完成
  • P0: (7,4,3) ≤ (10,4,7) ✓ → 全部完成

存在安全序列 P1 → P3 → P4 → P2 → P0,批准分配

再测试 P4 申请 Request = (3, 3, 0)(注意现在状态已经变化):

  1. Request(3,3,0) ≤ Need4
  2. Request(3,3,0) ≤ Available(2,3,0)?——B只有3个,申请需要3个,刚好够。但A只有2个,不够3个 → ❌ 资源不足,拒绝(让P4等待)

再测试 P0 申请 Request = (0, 2, 0)

  1. Request(0,2,0) ≤ Need0

  2. Request(0,2,0) ≤ Available(2,3,0) ✓

  3. 假设分配:Available=(2,1,0),P0-Alloc=(0,3,0),P0-Need=(7,2,3)

  4. 安全性检查:Work=(2,1,0),检查每个进程Need:

    • P1: Need(0,2,0) ≤ (2,1,0)?B=2 > Work的1 → ❌
    • P3: Need(0,1,1) ≤ (2,1,0)?C=1 > Work的0 → ❌
    • 没有任何进程的Need ≤ Work → 找不到安全序列

    拒绝分配。即使当时资源数量够,但分配后会让系统进入不安全状态。

📌 这是银行家算法考生最容易犯错的地方:资源数量够 ≠ 可以分配。分配后系统是否仍处于安全状态,才是判断依据。

银行家算法的工程局限性

[PERSONAL EXPERIENCE] 银行家算法在实际操作系统中几乎没有被完整实现过。原因有三:(1) 进程需要在运行前声明最大资源需求——多数程序做不到;(2) 进程数量和资源数量动态变化,矩阵维护开销大;(3) 安全性检查算法复杂度 O(n²m),在大规模系统中不可接受。但它在理论上的价值极高——它是"避免死锁"这一策略最优雅的形式化表达,也是408考试大题的核心考点。


死锁的检测与解除

有些系统采取"先不管,出了问题再说"的策略——允许死锁发生,然后检测它、解除它。

死锁检测:资源分配图化简

资源分配图由两类节点和两类边组成:

  • 进程节点(圆圈):代表一个进程
  • 资源节点(方框):代表一类资源,框内的圆点代表该资源的实例数
  • 分配边(资源→进程):资源已分配给该进程
  • 请求边(进程→资源):进程在等待该资源

化简算法(死锁定理):反复执行以下操作——找到一个既不阻塞也不孤立的进程节点(即该进程所有请求的资源都能被满足),删除该节点及其所有关联边(模拟该进程获得资源、执行完毕、释放所有资源)。如果最终所有节点都能被消除,则图是可完全简化的,系统没有死锁;否则存在死锁。

示例推演

[IMAGE] 资源分配图逐步化简示意:原始图→消除进程P3→消除进程P2→消除进程P1→全图清空,可完全简化,无死锁。

初始状态

     P1 ─────→ R1 (有1个实例,已分给P2)
     │          ↑
     │          │
     └────→ R2 (有2个实例,已分给P1和P2各1个)
              ↑
              │
     P2 ─────→│
              │
     P3 ───→ R2(请求)

用文字描述化简过程:

步骤 可用资源 找到的进程 动作
初始 R1=0, R2=0
1 R1=0, R2=0 P3(P3请求R2但无资源可用) ❌ 跳过——P3被阻塞
P2(P2不请求,只占有R1和R2) ✅ 消除P2,释放R1(1个)+R2(1个)
2 R1=1, R2=1 P1(P1请求R1和R2) ✅ 消除P1,释放P1占有的R2(1个)
3 R1=1, R2=2 P3(P3请求R2) ✅ 消除P3

所有节点消除 → 可完全简化 → 无死锁

如果 P1 占有的不是 R2 而是 R1,那情况就不一样了——P2和P1互相等待,P3也被牵连,谁也释放不了资源。

检测时机

  • 定时检测:每隔固定时间间隔执行一次检测算法
  • 按需检测:当CPU利用率异常下降、大量进程长时间阻塞时触发

死锁解除

一旦检测到死锁,必须用外力打破僵局。三种方法按破坏性递增排列:

方法 操作 代价 适用场景
资源剥夺法 从部分进程中抢占资源,分配给死锁进程 资源状态可保存恢复时
撤销进程法 终止部分或全部死锁进程 无法单独剥夺资源时
进程回退法 让进程回退到足够消除死锁的检查点 中高 需要系统支持检查点机制

选择"牺牲品"时通常考虑:进程优先级(低优先的先牺牲)、已运行时间(短的可少浪费)、已使用的资源量(占用资源多的优先牺牲,一次释放足够多资源)。


经典问题:哲学家进餐

哲学家进餐问题(Dining Philosophers Problem)由 Dijkstra 在1965年提出,是死锁问题的标准模型。五位哲学家围坐圆桌,每人面前一盘意面。每两位哲学家之间放一根筷子,共5根。哲学家只有拿到左右两根筷子才能吃饭,吃完后放下筷子。

为什么会死锁?

五位哲学家同时感到饥饿,都拿起了左手边的筷子,然后都去拿右手边的筷子——但每根右边的筷子都已经被右边的哲学家拿走了(对那个人而言是左手边的)。5个人每个人手里握着一根,等着另一根,循环等待形成——死锁。

四种解法对应四种预防思路

解法 破坏的条件 具体做法
最多4人同时进餐 循环等待 限制同时申请资源的进程数(设置上限为n-1)
奇数先左后右,偶数先右后左 循环等待 打破对称性,让等待链不能首尾闭合
同时拿两根 请求和保持 要么两根一起拿,要么一根都不拿(加互斥信号量保护取筷子操作)
限制一根先 互斥(变相) 实际上是通过限制并发度来减缓解除的复杂度

[UNIQUE INSIGHT] 四种解法中,“同时拿两根"在概念上最接近预防死锁中的"静态分配法”——要么全拿要么全不放。"奇数先左后右、偶数先右后左"最巧妙,它用极低的代价(只需一个if判断奇偶)就打破了循环等待。这提醒我们:在并发编程中,打破对称性是避免死锁最经济的手段之一

生产者-消费者中的死锁

将生产者-消费者问题的P操作顺序写错,会直接制造死锁:

/* 错误写法——会导致死锁 */
void producer() {
    while (1) {
        P(mutex);      // 先锁住缓冲区
        P(empty);      // 再等空位——如果没有空位,阻塞在这里
                        // 但mutex还没释放!消费者永远无法进入缓冲区取数据
        // ... 生产数据
        V(mutex);
        V(full);
    }
}

为什么死锁:当缓冲区满了(empty=0),生产者先拿到mutex再去P(empty),因为empty=0而阻塞。消费者想进入缓冲区必须先P(mutex),但mutex被生产者占着——双方互相等待,死锁。正确的顺序是先P(empty)再P(mutex)。

[INTERNAL-LINK] 关于信号量和PV操作的详细讲解,参见本系列第三篇《进程同步与互斥:从信号量到管程》。


代码示例:死锁复现与银行家算法模拟

死锁复现(C语言/Pthreads)

下面用两线程竞争两把锁来复现最简单的死锁场景:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;

void *thread_a(void *arg) {
    pthread_mutex_lock(&lock1);
    printf("Thread A: got lock1\n");
    sleep(1);                    // 留时间让Thread B拿到lock2
    pthread_mutex_lock(&lock2);  // 等待lock2——但已被Thread B持有
    printf("Thread A: got lock2 (never reached)\n");
    pthread_mutex_unlock(&lock2);
    pthread_mutex_unlock(&lock1);
    return NULL;
}

void *thread_b(void *arg) {
    pthread_mutex_lock(&lock2);
    printf("Thread B: got lock2\n");
    sleep(1);                    // 留时间让Thread A拿到lock1
    pthread_mutex_lock(&lock1);  // 等待lock1——但已被Thread A持有
    printf("Thread B: got lock1 (never reached)\n");
    pthread_mutex_unlock(&lock1);
    pthread_mutex_unlock(&lock2);
    return NULL;
}

int main() {
    pthread_t ta, tb;
    pthread_create(&ta, NULL, thread_a, NULL);
    pthread_create(&tb, NULL, thread_b, NULL);
    pthread_join(ta, NULL);      // 永远不会返回
    pthread_join(tb, NULL);
    printf("Done\n");
    return 0;
}

运行预期:程序输出两行 “got lock1” 和 “got lock2” 后永久卡死,pthread_join 永不返回。这就是最简化的死锁——两个线程、两把锁、相反的获取顺序。

银行家算法C语言简化实现

#include <stdio.h>
#include <stdbool.h>

#define N 5   // 进程数
#define M 3   // 资源类型数

int available[M] = {3, 3, 2};
int max[N][M] = {
    {7, 5, 3}, {3, 2, 2}, {9, 0, 2}, {2, 2, 2}, {4, 3, 3}
};
int allocation[N][M] = {
    {0, 1, 0}, {2, 0, 0}, {3, 0, 2}, {2, 1, 1}, {0, 0, 2}
};
int need[N][M];

// 计算Need矩阵 = Max - Allocation
void calculate_need() {
    for (int i = 0; i < N; i++)
        for (int j = 0; j < M; j++)
            need[i][j] = max[i][j] - allocation[i][j];
}

// 安全性检查:存在安全序列返回true,否则false
bool is_safe() {
    int work[M];
    bool finish[N] = {false};
    for (int j = 0; j < M; j++) work[j] = available[j];

    int count = 0;
    int safe_seq[N];  // 记录安全序列

    while (count < N) {
        bool found = false;
        for (int i = 0; i < N; i++) {
            if (finish[i]) continue;
            bool can_allocate = true;
            for (int j = 0; j < M; j++) {
                if (need[i][j] > work[j]) {
                    can_allocate = false;
                    break;
                }
            }
            if (can_allocate) {
                // 模拟Pi完成,释放资源
                for (int j = 0; j < M; j++)
                    work[j] += allocation[i][j];
                finish[i] = true;
                safe_seq[count++] = i;
                found = true;
            }
        }
        if (!found) return false;  // 找不到可完成的进程
    }

    printf("安全序列: ");
    for (int i = 0; i < N; i++) printf("P%d ", safe_seq[i]);
    printf("\n");
    return true;
}

// 处理进程pid的资源请求req[M],成功返回true
bool request_resources(int pid, int req[M]) {
    // 验证请求合法性
    for (int j = 0; j < M; j++) {
        if (req[j] > need[pid][j]) {
            printf("错误:请求超过声明的最大需求\n");
            return false;
        }
        if (req[j] > available[j]) {
            printf("资源不足,P%d等待\n", pid);
            return false;
        }
    }
    // 试探性分配
    for (int j = 0; j < M; j++) {
        available[j] -= req[j];
        allocation[pid][j] += req[j];
        need[pid][j] -= req[j];
    }
    // 安全性检查
    if (is_safe()) {
        printf("批准P%d的资源请求\n", pid);
        return true;
    } else {
        // 不安全——回滚
        for (int j = 0; j < M; j++) {
            available[j] += req[j];
            allocation[pid][j] -= req[j];
            need[pid][j] += req[j];
        }
        printf("拒绝P%d的资源请求(会导致不安全状态)\n", pid);
        return false;
    }
}

int main() {
    calculate_need();
    printf("=== 初始安全状态检查 ===\n");
    is_safe();

    printf("\n=== P1请求(1,0,2) ===\n");
    int req1[M] = {1, 0, 2};
    request_resources(1, req1);

    printf("\n=== P4请求(3,3,0) ===\n");
    int req4[M] = {3, 3, 0};
    request_resources(4, req4);

    printf("\n=== P0请求(0,2,0) ===\n");
    int req0[M] = {0, 2, 0};
    request_resources(0, req0);

    return 0;
}

运行结果:P1(1,0,2)批准,P4(3,3,0)因资源不足拒绝,P0(0,2,0)因会导致不安全状态拒绝——与我们手动推演一致。


常见疑问

银行家算法的安全性检查中,如果同时有多个进程满足条件,选哪个?

选哪个都可以——安全序列不是唯一的。通常选择Need最小的进程优先(贪心策略),能更快释放资源。对于408考试,只要写出任意一条安全序列就行。建议按进程编号从小到大依次检查,这样操作规范且不易遗漏。

循环等待一定死锁吗?

不一定。如果同类资源有多个实例,循环等待链可能被"空闲的那个实例"打破。例如R1有两个实例——P1占一个等R2,P2占一个等R3,P3占R3等R1。还剩一个R1空闲,P3可以直接拿到——链就断了。死锁一定是循环等待,循环等待不一定是死锁。

死锁避免和死锁预防的核心区别是什么?

预防是不管当前资源够不够,直接在规则层面让四个必要条件无法同时成立——比如顺序资源分配法永远禁止你申请编号更小的资源。避免则是在每次分配前做一次安全判断,分配本身在规则上是被允许的,只有当它会导致不安全状态时才被拒绝。打个比方:预防是"禁止逆行",避免是"允许超车,但要先看对向来车"。

三种死锁处理策略实际用在什么系统?

[PERSONAL EXPERIENCE] 预防几乎没有被现代通用操作系统全面采用(代价太大),但在嵌入式实时系统中常见(一次性分配,保证确定性)。避免(银行家算法)主要在教学中出现,工程中极少有系统完整实现——但有近似思想的应用(如数据库事务管理中的死锁检测与回滚)。检测与解除在数据库系统(MySQL InnoDB的死锁检测)和Linux内核(lockdep死锁检测框架)中大量使用——这些系统允许部分死锁发生,但通过定期检测和受害者回滚来保证整体可用性。

考研大题中银行家算法考到什么程度?

408统考真题中,银行家算法以计算题形式出现。给定Max/Allocation/Available数据,要求:(1) 计算Need矩阵;(2) 找出一个安全序列;(3) 判断某次资源请求能否被批准。步骤必须完整,每一步的Work变化要写清楚。建议在草稿纸上用表格跟踪Work、Finish的变化,避免遗漏。


小结

死锁管理的三种策略构成了一条从"保守"到"激进"的光谱:

  • 预防最保守——在规则层面消灭死锁的可能性,代价是资源利用率低
  • 避免折中——每次分资源前做一次安全预判,银行家算法是其最优美的表达
  • 检测与解除最激进——允许死锁发生,出事了再收拾,资源利用率最高但恢复代价也最大

408考试中,死锁四大必要条件和三策略对比是选择题高频考点,银行家算法的大题几乎每年都有变体。哲学家进餐问题则是死锁理论最浓缩的模型,四种解法恰好映射四种预防思路。


延伸阅读

  • [INTERNAL-LINK] 《进程的基本概念与状态转换》—— 本系列第1篇,回顾进程状态(阻塞态、就绪态与死锁的关系)
  • [INTERNAL-LINK] 《调度算法:从FCFS到多级反馈队列》—— 本系列第2篇,理解饥饿与调度策略的关系
  • [INTERNAL-LINK] 《进程同步与互斥:从信号量到管程》—— 本系列第3篇,掌握PV操作与死锁的关联

Logo

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

更多推荐