第 20 讲:死锁、银行家算法与资源分配图
死锁不是“程序慢”,也不是“某个锁被占用”。它是一组执行流中,每一个都在等待只能由该组内其他成员产生的资源/事件,没有外力就永远不会前进。
资源既可以是用完后释放、可反复分配的锁和设备,也可以是消息、信号等产生后被消费的临时资源。因此死锁不只来自“两把互斥锁”:通信双方以错误顺序互等消息、先持有缓冲槽再等对方数据,也能形成同样的等待环。
一个最小死锁
P1: acquire(file) P2: acquire(printer)
acquire(printer) acquire(file)
... ...
若 P1 已有 file、P2 已有 printer,再各等对方资源,就形成环。“再等一会”不会改变任何资源状态。
资源可分为:
- 可剥夺:系统能安全收回,例如通过保存上下文收回 CPU;
- 不可剥夺:强行收回会破坏操作,如打印到一半的打印机;
- 可消耗/临时性资源:一个进程产生、另一个消费的消息、中断等,错误的接收—发送顺序也会环路等待。
死锁的四个必要条件
- 互斥:资源同时只能给有限进程使用;
- 请求并保持:进程持有旧资源时继续等新资源;
- 不可剥夺:资源只能由占有者主动释放;
- 循环等待: 等 的资源,…, 又等 的资源。
四者同时成立才可能死锁。破坏任一条可预防死锁,但往往以便利性、并发度或资源利用率为代价。
死锁、活锁和饥饿
| 现象 | 执行流是否运行 | 为什么不完成 |
|---|---|---|
| 死锁 | 通常阻塞 | 等待环内不可能自发产生的条件 |
| 活锁 | 一直在改状态/重试 | 彼此过度礼让或同步重试,没有有效进展 |
| 饥饿 | 其他人在进展 | 调度/锁策略让某个人长期得不到资源 |
随机退避可打破活锁的同步节奏,公平队列和 aging 可缓解饥饿;它们不是同一个问题的三个名字。
四类处理态度
- 忽略:假定极少发生,由应用/人工重启;
- 预防:用静态规则破坏四个必要条件之一;
- 避免:保留四个条件的灵活性,但每次分配前预测是否仍安全;
- 检测与解除:允许死锁发生,定期/按需找出,再终止、回滚或剥夺。
死锁预防:每种方法付出什么
破坏互斥
只适用本来就可共享或能虚拟化的资源。例如 SPOOLing 把多个打印请求先写到磁盘队列,由唯一后台进程真正占用打印机。但同一物理时刻的打印机仍是互斥的。
破坏请求并保持
要求进程开始前一次申请全部资源,或申请新资源前先释放已有资源。问题是需求可能事先不可知,而且很早占着暂时不用的资源,利用率低。
破坏不可剥夺
申请新资源失败时,强制释放/回滚已有资源。只适合能保存和恢复状态的资源;强行收回正在输出的设备可能产生不可恢复外部效果。
破坏循环等待
为所有资源类型设置全局序号,只允许按递增序申请。如果等待边永远从小编号指向大编号,就无法绕回起点。这是锁层次和 lock ordering 常用的工程方法,但全局顺序设计和遵守有成本。
安全状态不等于当前没死锁
安全状态:存在一个进程顺序,使每个进程都能用当时可用资源加上前面已完成进程释放的资源,获得其最大剩余需求并完成。
不安全状态表示 OS 已经无法保证所有进程在声明的最大需求内都能完成;它可能尚未死锁,因为进程未必真的同时要满。但避免策略不允许走入这种无保证状态。
银行家算法的数据结构
设 个进程、 类资源:
Available[m]:当前未分配的每类资源数;Max[n][m]:每进程声明的最大需求;Allocation[n][m]:当前已分配;Need=Max-Allocation:完成最多还可能需要多少。
进程 提出 Request_i 时:
- 检查
Request_i <= Need_i,否则超出自己声明; - 检查
Request_i <= Available,否则现在只能等; - 暂时扣减
Available、增加Allocation_i、减少Need_i; - 执行安全性算法;
- 仍安全才真正批准,否则回滚试分配并让进程等待。
安全性算法完整例子
资源总量 A/B/C = 10/5/7,当前:
| 进程 | Allocation | Max | Need |
|---|---|---|---|
| P0 | 0 1 0 | 7 5 3 | 7 4 3 |
| P1 | 2 0 0 | 3 2 2 | 1 2 2 |
| P2 | 3 0 2 | 9 0 2 | 6 0 0 |
| P3 | 2 1 1 | 2 2 2 | 0 1 1 |
| P4 | 0 0 2 | 4 3 3 | 4 3 1 |
已分配合计为 7/2/5,因此 Available = 3 3 2。设 Work=Available:
- P1 的 Need 1/2/2 3/3/2,可完成,
Work += Allocation1 = 5 3 2; - P3 的 0/1/1 5/3/2,完成后
Work = 7 4 3; - P4 的 4/3/1 7/4/3,完成后
Work = 7 4 5; - P0 的 7/4/3 7/4/5,完成后
Work = 7 5 5; - P2 的 6/0/0 7/5/5,完成。
所以 <P1,P3,P4,P0,P2> 是一条安全序列。安全序列不唯一;每一步都只需找一个 Need <= Work 的未完成进程。
试分配一个请求
若 P1 请求 1/0/2:
- 1/0/2 P1 Need 1/2/2;
- 1/0/2 Available 3/3/2;
- 试分配后
Available=2/3/0,P1Allocation=3/0/2,Need=0/2/0。
此时 P1 可先完成,Work 从 2/3/0 变为 5/3/2,后面仍可按 P3、P4、P0、P2 完成,所以请求可批准。
银行家算法为什么不是免费午餐
- 进程必须事先声明最大需求,而现实程序可能动态决定;
- 资源总数和进程集应相对稳定;
- 每次请求要执行矩阵检查;
- 为保持安全,可能暂拒一个当前明明有资源可满足的请求。
它保留了比静态预防更高的利用率,但前提是系统能得到可信的最大需求。
资源分配图(RAG)
图中:
- 进程结点 ;
- 资源类结点 ,结点内可有多个实例;
- 是请求边;
- 是分配边。
如果每类资源只有一个实例,有向环与死锁等价。有多实例时,有环只说明可能死锁:环外的某个资源实例可能先被释放,使环能打开。

课件还用“化简”表述检测:反复找出当前未阻塞、其剩余请求可被可用资源满足的进程,假想它完成并释放资源,删去相关边。若最终仍有无法化简的边/进程集,它们就是死锁候选。
检测到之后怎么解除
- 终止所有死锁进程:快,但损失大;
- 每次终止一个,重新检测:可减少损失,但检测开销增加;
- 剥夺资源:需选牺牲者,并能回滚到一致检查点;
- 选牺牲者时可考虑优先级、已运行时间、还需多少资源、已产生外部效果等;
- 需防止总选中同一进程导致饥饿。
“释放它所有锁然后继续运行”通常不安全:进程可能已在共享对象上做了半套更新。解锁必须同时考虑状态回滚或重启。
死锁题的四步过滤
- 先画清“谁持有什么、还等什么”;
- 检查四条必要条件,说清方案破坏了哪一条;
- 银行家题先重算
Need=Max-Allocation和Available=Total-sum(Allocation); - 安全性检查每一步都写
Work怎么因完成进程而增加,不要只凭感觉列序列。