死锁
1.死锁的概念
死锁(Deadlock) 是操作系统和并发编程中的一个经典问题。简单来说,死锁是指两个或多个进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象。
如果没有外力干涉,这些进程都将永远无法向前推进,处于“僵死”状态。
1. 死锁的定义
在计算机科学和操作系统领域,死锁(Deadlock) 的正式定义如下:
死锁是指两个或两个以上的进程(或线程)在执行过程中,由于竞争资源或者彼此通信而造成的一种阻塞现象。若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
简单来说,死锁就是一种“僵持”状态:你占着我的资源在等我手里的,我占着你的资源在等如果你手里的,结果谁也动弹不了。
1. 死锁的四个必要条件
要理解死锁的本质,必须明白它发生的四个必要条件(Coffman 条件)。只有这四个条件同时成立,死锁才会发生:
| 条件 | 定义 | 核心逻辑 |
|---|---|---|
| 互斥 (Mutual Exclusion) | 资源是独占的,任一时刻只能由一个进程占用。 | “这东西只能我一个人用。” |
| 请求并保持 (Hold and Wait) | 进程至少持有一个资源,且在请求新资源时,对自己已占有的资源保持不放。 | “我拿着 A,但我还要 B。” |
| 不可剥夺 (No Preemption) | 资源只能由占有它的进程主动释放,不能被系统或其他进程强行抢走。 | “你不给,我不能抢。” |
| 循环等待 (Circular Wait) | 存在一个闭环链:P1 等 P2,P2 等 P3... Pn 等 P1。 | “大家排成圈互相等。” |
2. 典型场景:资源竞争
假设有两个线程 $T1$ 和 $T2$,以及两把互斥锁 $Lock A$ 和 $Lock B$:
- T1 获取了 Lock A,准备去获取 Lock B。
- 与此同时,T2 获取了 Lock B,准备去获取 Lock A。
- T1 发现 Lock B 被占用,开始等待;T2 发现 Lock A 被占用,也开始等待。
- 由于双方都在等待对方释放资源,且都不会主动交出已有的锁,程序彻底卡死。
3. 死锁 vs 活锁 (Livelock)
为了更精准地定义死锁,通常会将其与活锁进行对比:
- 死锁:进程处于“等待/阻塞”状态,不消耗 CPU,完全静止。
- 活锁:进程并未阻塞,而是在不断尝试改变状态(例如:由于互相礼让,两个人在走廊里同时向左移、同时向右移),虽然在动,但依然无法前进。
4. 解决思路
在定义之外,处理死锁的思路通常分为:
- 预防:破坏四个必要条件之一(例如强制所有资源按顺序申请)。
- 避免:在分配资源前进行风险评估(如银行家算法)。
- 检测与恢复:允许死锁发生,但系统会定期扫描并强行杀死某个进程来释放资源。
2.死锁和饥饿
在操作系统和并发编程中,死锁(Deadlock)和饥饿(Starvation)是两个非常容易混淆但本质不同的概念。它们都涉及进程无法继续执行,但原因和表现完全不同。
1. 死锁 (Deadlock)
定义: 两个或多个进程在循环等待对方手中持有的资源,导致所有相关进程都永久处于阻塞状态。
- 打个比方: 两个人在窄路上相遇,谁都不肯让路,结果两人都永远困在原地。
- 系统状态: 系统处于“僵死”状态,没有任何进展。
- 核心原因: 循环等待(Circular Wait)。
2. 饥饿 (Starvation)
定义: 一个进程长时间(甚至无限期)无法获得所需的资源或 CPU 时间,因为它一直被系统“忽视”,而其他进程(通常是优先级更高的进程)一直在抢占资源。
- 打个比方: 在餐厅排队,如果系统规定“有钱人可以插队”,那么一个没钱的人可能一辈子也吃不上饭,尽管餐厅一直在运转。
- 系统状态: 系统整体是在运行的(高优先级进程在不断完成),只是特定的某些进程被“饿死”了。
- 核心原因: 资源分配策略不公平(如:纯优先级调度)。
3. 死锁 vs 饥饿:深度对比
| 特性 | 死锁 (Deadlock) | 饥饿 (Starvation) |
|---|---|---|
| 定义 | 多个进程互相等待资源,都无法运行。 | 某些进程因优先级低而永远等不到资源。 |
| 系统进展 | 完全停止。涉及死锁的进程都不动。 | 仍在进行。高优先级进程正常运行。 |
| 涉及进程数 | 至少涉及 2 个 进程。 | 可能只有 1 个 进程受害。 |
| 触发条件 | 必须满足四个必要条件(互斥、持有等待等)。 | 通常由不公平的调度算法引起。 |
| 解决办法 | 打破循环等待、银行家算法。 | 老化技术 (Aging)。 |
4. 解决饥饿的关键:老化技术 (Aging)
为了防止“饥饿”现象,操作系统通常采用老化技术(Aging)。其核心逻辑是:
如果一个进程在就绪队列中等待的时间越长,它的优先级就会随之逐渐提高。
最终,即使是优先级最低的进程,只要等得足够久,也会变成“最高优先级”从而获得 CPU 执行权限。
5. 两者的关系
- 死锁一定包含饥饿:如果进程陷入死锁,它显然也在遭受“资源饥饿”,因为它永远拿不到资源。
- 饥饿不一定是死锁:进程可能因为运气不好一直排不上队,但并没有陷入“互相等待”的逻辑闭环。
3.死锁产生的原因
死锁的产生可以归结为外部原因(系统资源不足)和内部原因(进程推进顺序不当)。但从理论上讲,死锁的发生必须同时具备四个必要条件(Coffman 条件)。
以下是死锁产生原因的详细拆解:
1. 必要条件:死锁发生的“土壤”
根据 Coffman 的研究,死锁的发生源于以下四个条件的同时满足。只要破坏其中任何一个,死锁就不会发生:
-
互斥条件 (Mutual Exclusion):
某些资源是不可共享的(如打印机、写锁)。一个资源在一段时间内只能被一个进程占用。
-
请求并保持条件 (Hold and Wait):
进程已经持有了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有。此时进程会被阻塞,但对已获得的资源“紧握不放”。
-
不可剥夺条件 (No Preemption):
进程已获得的资源在未使用完之前,不能被外界强行抢夺,只能由进程在完成任务后自行释放。
-
循环等待条件 (Circular Wait):
存在一个闭环的等待链。例如:进程 A 等进程 B,进程 B 等进程 C,进程 C 等进程 A。
2. 根本原因:系统资源不足
如果系统拥有无限的资源,任何进程的请求都能立即得到满足,死锁自然不会发生。死锁的客观基础是资源有限:
- 不可剥夺资源:如磁带机、打印机、互斥锁等。当多个进程竞争这些有限资源时,由于资源无法同时满足所有人,就可能陷入僵局。
- 临时性资源:如进程间通信的信号量、消息。如果发送和接收的顺序逻辑有误,也会导致永久等待。
3. 直接原因:进程推进顺序不当
即使资源有限,如果进程的执行顺序科学,也可以避开死锁。死锁往往发生在错误的申请时机:
场景示例:
假设系统有两个资源 $R1$ 和 $R2$,两个进程 $P1$ 和 $P2$。
- 正常顺序:$P1$ 申请并使用 $R1, R2$ 后释放;接着 $P2$ 申请并使用 $R1, R2$。系统安全。
- 错误顺序:
- $P1$ 申请了 $R1$。
- $P2$ 申请了 $R2$。
- 此时,$P1$ 接着申请 $R2$(被阻塞,因为 $P2$ 拿着)。
- 同时,$P2$ 接着申请 $R1$(被阻塞,因为 $P1$ 拿着)。
- 结果:双方进入“循环等待”。
4. 总结与归纳
死锁产生的逻辑关系可以概括为:
**由于系统资源有限 $\rightarrow$ 进程间产生资源竞争 $\rightarrow$ 如果代码逻辑导致了循环等待且不可剥夺 $\rightarrow$ 最终触发死锁。
4.死锁和循环等待
1. 循环等待是死锁的“闭环”
在死锁发生的四个必要条件(互斥、请求并保持、不可剥夺、循环等待)中,循环等待描述了进程之间资源请求的逻辑结构。
- 定义:存在一个进程集合 ${P_0, P_1, ..., P_n}$,其中 $P_0$ 正在等待 $P_1$ 占有的资源,$P_1$ 在等待 $P_2$ 占有的资源……最后 $P_n$ 正在等待 $P_0$ 占有的资源。
- 直观表现:在资源分配图(Resource Allocation Graph, RAG)中,这种状态表现为一个封闭的环(Cycle)。
2. 环路是否等同于死锁?
这是学习中的一个重点。环路(Cycle)是死锁的必要条件,但并不总是充分条件。 这取决于资源的类型:
情况 A:每种资源类只有一个实例(Single Instance)
如果每种资源(如打印机、特定的锁)只有一个,那么:
- 有环 = 死锁。
- 无环 = 无死锁。
情况 B:每种资源类有多个实例(Multiple Instances)
如果资源有多个副本(比如系统有 3 台同样的打印机),那么:
- 有环 ≠ 一定死锁。因为环路中的某个进程可能会从环外的其他资源实例中获得满足,从而打破僵局。
- 死锁必然有环。
3. 如何通过打破“循环等待”来预防死锁?
在实际编程中,破坏“循环等待”是最常用、最有效的死锁预防手段。核心策略是:资源线性排序(Total Ordering)。
具体做法:
- 给系统中的所有资源分配一个唯一的序号(如:Lock A = 1, Lock B = 2, Lock C = 3)。
- 规定所有进程必须严格按照序号递增的顺序申请资源。
为什么有效?
- 如果 $P1$ 拿了 1 号资源想申请 2 号,而 $P2$ 想申请 1 号资源,它必须先申请 1 号再申请 2 号。
- 这样就不可能出现“$P1$ 拿着 1 等 2,同时 $P2$ 拿着 2 等 1”的情况,因为 $P2$ 在没拿到 1 之前是不允许去拿 2 的。环路被物理性地切断了。
4. 总结对比
| 维度 | 死锁 (Deadlock) | 循环等待 (Circular Wait) |
|---|---|---|
| 本质 | 系统的状态(一种僵局)。 | 资源请求的拓扑结构(一个环)。 |
| 地位 | 结果。 | 原因/必要条件之一。 |
| 判定 | 进程无法继续执行。 | 资源分配图中存在闭合路径。 |
| 处理思路 | 预防、避免、检测、解除。 | 重点在于“破坏环路”。 |
5.死锁的处理策略
处理死锁通常有四种主要的策略,按照从严厉到宽松的顺序,分别是:预防、避免、检测与解除、以及忽略。
1. 死锁预防 (Deadlock Prevention)
这种策略的核心思想是:通过设置严格的限制,破坏死锁产生的四个必要条件之一,从而确保系统永远不会进入死锁状态。
- 破坏“请求并保持”条件:要求进程在开始运行前,必须一次性申请它所需要的全部资源。如果资源不满足,则一个都不给它。
- 破坏“不可剥夺”条件:当一个已经持有资源的进程申请新资源被拒绝时,它必须释放手中所有的资源,以后再重新申请。
- 破坏“循环等待”条件:给所有资源编号,规定每个进程必须按序号递增的顺序申请资源(这是最常用的编程实践)。
2. 死锁避免 (Deadlock Avoidance)
与预防不同,避免策略不限制资源的申请逻辑,而是在动态分配资源时进行计算。如果分配后系统会进入“不安全状态”,则拒绝分配。
-
安全状态:如果系统能按某种顺序(安全序列)为每个进程分配资源并顺利完成,则系统是安全的。
-
经典算法:银行家算法 (Banker's Algorithm)
在分配前,系统会模拟分配后的结果。如果发现分配后可能导致没有足够的资源让所有进程完成,就拒绝这次请求。
-
Shutterstock
3. 死锁检测与解除 (Deadlock Detection & Recovery)
这种策略比较“佛系”:允许死锁发生,但系统会定期检查是否有死锁,一旦发现就动手清理。
检测手段:
系统维护一张资源分配图 (Resource Allocation Graph),通过特定的算法(如死锁检测算法)来寻找图中是否存在不可打破的环路。
解除手段:
一旦检测到死锁,通常采用以下方式打破僵局:
- 资源剥夺:从一个或多个进程中抢占足够数量的资源,分配给其他死锁进程。
- 撤销进程(最常用):强制终止一个或多个涉及死锁的进程,直到死锁消失。
- 进程回退:让涉及死锁的进程回退到之前的某个“检查点”,并重新尝试。
4. 鸵鸟策略 (The Ostrich Algorithm)
定义: 像鸵鸟一样把头埋在沙子里,假装问题不存在。
- 逻辑:如果死锁发生的概率非常低,而预防或检测死锁的开销(CPU消耗、内存占用)非常大,那么直接忽略死锁反而能提高系统整体性能。
- 应用场景:大多数通用操作系统(如 Windows、Linux、macOS)在内核级别主要采用这种策略。如果系统真卡死了,通常由用户手动强制重启或结束进程。
综合对比总结
| 策略 | 优点 | 缺点 | 资源利用率 |
|---|---|---|---|
| 预防 | 简单、直接,不会死锁 | 限制太多,可能导致资源浪费 | 低 |
| 避免 | 比较灵活 | 每次分配都要计算,开销大 | 中 |
| 检测 | 资源利用率高 | 检测到死锁时撤销进程代价高 | 高 |
| 忽略 | 无额外系统开销 | 可能导致系统崩溃 | 最高 |
2.死锁的预防
死锁预防 (Deadlock Prevention) 是处理死锁最直接、最严厉的策略。它的核心思想是:通过在系统设计时增加限制条件,人为地破坏导致死锁的四个必要条件(Coffman 条件)中的一个或多个,从而从根本上确保死锁永远不会发生。
1. 破坏“互斥”条件 (Mutual Exclusion)
- 思路: 将独占资源改造为共享资源。
- 手段: * 虚拟化技术(如 Spooling):例如打印机本是独占资源,但通过引入脱机打印技术,多个进程可以将打印任务发送到磁盘队列(Spooling 缓冲区),由系统统一调度打印。
- 局限性: 并不是所有资源都能共享(如写锁、磁带机)。出于数据一致性和物理限制,这个条件往往最难被破坏。
2. 破坏“请求并保持”条件 (Hold and Wait)
- 思路: 禁止进程在持有资源的同时去等待新资源。
- 具体方案:
- 静态分配(预先分配):进程在运行前必须一次性申请它所需的全部资源。只有当系统能满足其所有需求时,才一次性分配给它。在运行期间,该进程不再请求新资源。
- 空手申请:进程在申请新资源之前,必须先释放掉当前持有的所有资源。
- 缺点: * 资源利用率低:有些资源可能只在运行末尾才用到,但由于预先占有,导致其他进程无法使用。
- 可能导致“饥饿”:如果一个进程需要很多资源,可能因为其中一个资源总被占用而永远无法启动。
3. 破坏“不可剥夺”条件 (No Preemption)
- 思路: 当一个进程请求新资源得不到满足时,必须释放已占有的资源。
- 具体方案:
- 如果进程 $A$ 请求资源 $R1$ 被拒绝,系统会检查 $R1$ 的状态。
- 如果 $R1$ 被另一个正在等待资源的进程 $B$ 占用,则系统可以强行剥夺 $B$ 的资源给 $A$。
- 或者让进程 $A$ 释放自己手里所有的资源,进入等待状态,稍后重新开始。
- 局限性: 这种方案只适用于易于保存和恢复状态的资源(如 CPU 寄存器、内存)。对于打印机等状态不可逆的资源,强行剥夺会导致严重的错误。
4. 破坏“循环等待”条件 (Circular Wait) —— 最常用
- 思路: 建立资源的线性顺序,打破资源请求的环路。
- 具体方案(资源有序分配法):
- 给系统中的所有资源类型分配一个唯一的整数编号(如:磁盘=1,打印机=2,锁A=3)。
- 规定进程必须按照编号递增的顺序请求资源。
- 如果一个进程已经持有了编号为 $i$ 的资源,它下次只能申请编号大于 $i$ 的资源。
- 原理: 因为所有人都在往“高处”走,不可能有人回头去等比自己编号小的资源,从而物理上无法形成环路。
- 优点: 相比前两种,资源利用率更高,是目前工业界(如数据库锁管理、底层驱动开发)最常用的预防手段。
总结对比
| 破坏的条件 | 核心手段 | 主要评价 |
|---|---|---|
| 互斥 | 资源共享/虚拟化 | 适用范围窄,很多硬件无法实现。 |
| 请求并保持 | 一次性申请所有资源 | 简单但浪费资源,易导致饥饿。 |
| 不可剥夺 | 强制释放或抢占 | 实现复杂,可能导致进程状态受损。 |
| 循环等待 | 资源按序号申请 | 最实用、最高效,但编号管理有开销。 |
3.死锁的避免
死锁避免 (Deadlock Avoidance) 是一种在资源分配过程中进行动态评估的策略。
与“死锁预防”通过强制性规则(如一次性申请所有资源)不同,死锁避免允许进程动态地申请资源,但在系统每一次进行资源分配之前,都会先计算这次分配是否会导致系统进入“不安全状态”。
在操作系统中,系统安全状态(Safe State)是一个专门用于死锁避免(Deadlock Avoidance)的核心概念。它主要描述的是:在资源分配过程中,系统是否能够找到一种分配顺序,使得所有进程都能顺利完成。
以下是关于系统安全状态的详细解析:
1. 什么是安全状态?
如果系统能够按照某种进程顺序(如 $P_1, P_2, ..., P_n$)为每个进程分配资源,直到满足每个进程对资源的最大需求,并使每个进程都能顺利终止,那么称系统处于安全状态。
这个序列 $P_1, P_2, ..., P_n$ 被称为安全序列(Safe Sequence)。
核心逻辑
- 安全状态:至少存在一个安全序列。
- 不安全状态:不存在任何安全序列。
2. 安全状态与死锁的关系
理解这两者的关系至关重要。虽然“不安全状态”听起来很危险,但它并不等同于“死锁”:
- 安全状态 $\implies$ 无死锁:只要系统处于安全状态,就一定不会发生死锁。
- 不安全状态 $\implies$ 可能导致死锁:如果系统进入不安全状态,操作系统就失去了对资源的调度主动权。如果此时进程刚好同时申请最大资源,就会发生死锁;但如果进程实际使用的资源较少,也可能侥幸不发生死锁。
结论: 避免死锁的实质,就是通过资源分配算法,确保系统永远不会进入不安全状态。
3. 如何判断安全状态:银行家算法
最著名的检测系统安全状态的算法是银行家算法(Banker's Algorithm)。其核心数据结构包括:
- Available (可利用资源向量):系统中每种资源剩余的可用数量。
- Max (最大需求矩阵):每个进程对每种资源的最大需求量。
- Allocation (已分配矩阵):每个进程当前已占有的资源量。
- Need (需求矩阵):每个进程未来还需要的资源量($Need = Max - Allocation$)。
安全性检查步骤:
- 初始化:设置一个工作向量
Work = Available。 - 寻找进程:在进程集合中找出一个满足以下条件的进程 $P_i$:
- 该进程尚未完成。
- 该进程的剩余需求小于等于系统当前可用资源($Need_i \le Work$)。
- 释放资源:如果找到了这样的进程,假设它执行完毕,将其占有的资源全部归还($Work = Work + Allocation_i$),并标记该进程已完成。
- 循环/判断:重复步骤 2。如果所有进程都能标记为完成,则系统处于安全状态;否则,系统处于不安全状态。
4. 总结对比
| 状态 | 定义 | 后果 |
|---|---|---|
| 安全状态 | 存在至少一个安全序列 | 保证不会发生死锁 |
| 不安全状态 | 不存在安全序列 | 可能发生死锁(风险状态) |
| 死锁状态 | 多个进程因竞争资源而永久阻塞 | 系统功能受损,需要人工干预或重启 |
银行家算法(Banker's Algorithm) 是由艾兹格·迪杰斯特拉(Edsger Dijkstra)为 Dijkstra 操作系统设计的,是操作系统中死锁避免(Deadlock Avoidance)最著名的算法。
它的名字来源于银行:银行家在贷款时,必须保证贷款后剩下的资金仍能满足至少一个客户的最大需求,从而保证资金能够回收并循环使用。
1. 核心数据结构
为了实现该算法,我们需要维护四个关键的矩阵和向量(假设有 $n$ 个进程,$m$ 种资源):
- 最大需求矩阵 $Max[n, m]$:定义每个进程对每种资源的最大需求量。
- 分配矩阵 $Allocation[n, m]$:定义每个进程当前已分配到的每种资源量。
- 需求矩阵 $Need[n, m]$:定义每个进程还需要多少资源。
- 公式:$Need[i, j] = Max[i, j] - Allocation[i, j]$
- 可利用资源向量 $Available[m]$:系统中当前剩余可用的每种资源量。
2. 算法的工作流程
银行家算法分为两个主要部分:资源请求算法和安全性检查算法。
A. 资源请求算法 (Request Algorithm)
当进程 $P_i$ 发出资源请求 $Request_i$ 时,系统按以下步骤操作:
-
检查请求合法性:如果 $Request_i \le Need_i$,转步骤 2;否则出错(请求超过了它宣称的最大值)。
-
检查资源是否足够:如果 $Request_i \le Available$,转步骤 3;否则 $P_i$ 必须等待(系统当前资源不足)。
-
预分配(试探性分配):系统假装把资源分配给 $P_i$,并修改数据结构:
- $Available = Available - Request_i$
- $Allocation_i = Allocation_i + Request_i$
- $Need_i = Need_i - Request_i$
-
执行安全性检查:调用安全性算法。
- 如果结果为安全:正式分配资源。
- 如果结果为不安全:撤销预分配,恢复原状,$P_i$ 继续等待。
-
Shutterstock
B. 安全性检查算法 (Safety Algorithm)
这是判断系统是否处于“安全状态”的核心逻辑。它尝试寻找一个安全序列:
- 初始化:
Work = Available,Finish[all] = false。 - 寻找满足条件的进程 $P_i$:
Finish[i] == falseNeed_i \le Work
- 如果找到:
- 假设 $P_i$ 获得资源并完成后,释放其占有的所有资源:
Work = Work + Allocation_i - 标记
Finish[i] = true - 重复步骤 2。
- 假设 $P_i$ 获得资源并完成后,释放其占有的所有资源:
- 如果所有进程的
Finish都为true,则系统处于安全状态。
3. 实例演示
假设系统有 3 种资源 (A, B, C),当前 Available = (3, 3, 2)。
| 进程 | Allocation (已分配) | Need (还需) | Available (当前可用) |
|---|---|---|---|
| P0 | (0, 1, 0) | (7, 4, 3) | (3, 3, 2) |
| P1 | (2, 0, 0) | (1, 2, 2) | |
| P2 | (3, 0, 2) | (6, 0, 0) | |
| P3 | (2, 1, 1) | (0, 1, 1) | |
| P4 | (0, 0, 2) | (4, 3, 1) |
安全性检查过程:
-
找 P1:$Need(1, 2, 2) \le Available(3, 3, 2)$。P1 完成,释放资源后 $Work = (3,3,2) + (2,0,0) = (5, 3, 2)$。
-
找 P3:$Need(0, 1, 1) \le Work(5, 3, 2)$。P3 完成,释放后 $Work = (5,3,2) + (2,1,1) = (7, 4, 3)$。
-
找 P0:$Need(7, 4, 3) \le Work(7, 4, 3)$。P0 完成,释放后 $Work = (7,4,3) + (0,1,0) = (7, 5, 3)$。
-
以此类推,P2、P4 都能完成。
安全序列: $<P1, P3, P0, P2, P4>$(序列不唯一)。
4. 银行家算法的优缺点
- 优点:
- 允许进程并发执行,只要不进入不安全状态。
- 比“静态分配资源”更灵活,资源利用率更高。
- 缺点(局限性):
- 难以预知:要求进程提前声明最大资源需求,在实际应用中很难做到。
- 进程固定:要求进程数保持固定,无法处理动态增加的进程。
- 开销大:每次申请资源都要运行一遍算法,对 CPU 性能有一定消耗。
4.安全性算法举例
为了让你彻底理解安全性算法,我们通过一个经典的数值练习题来一步步推演。
安全性算法的目标是:找到一个“安全序列”。只要能找到一个顺序,让所有进程都顺利拿到资源并运行结束,系统就是安全的。
1. 案例背景
假设系统中有 5 个进程 ${P_0, P_1, P_2, P_3, P_4}$ 和 3 种资源 ${A, B, C}$。
目前系统中 A、B、C 资源的剩余可用量 (Available) 为:(3, 3, 2)。
下表是当前的资源分配情况:
| 进程 | Max (最大需求) | Allocation (已分配) | Need (还需) |
|---|---|---|---|
| P0 | (7, 5, 3) | (0, 1, 0) | (7, 4, 3) |
| P1 | (3, 2, 2) | (2, 0, 0) | (1, 2, 2) |
| P2 | (9, 0, 2) | (3, 0, 2) | (6, 0, 0) |
| P3 | (2, 2, 2) | (2, 1, 1) | (0, 1, 1) |
| P4 | (4, 3, 3) | (0, 0, 2) | (4, 3, 1) |
提示:$Need = Max - Allocation$(还差多少 = 总共要的 - 已经给的)。
2. 安全性检查步骤 (推演过程)
我们要准备一个“工作账本” Work,初始值等于当前可用资源 (3, 3, 2)。
第一轮检查:
- 看 P0:它还差 (7, 4, 3),但我们手里只有 (3, 3, 2)。不够,跳过。
- 看 P1:它还差 (1, 2, 2),我们手里有 (3, 3, 2)。够了!
- 动作:假设 P1 拿走资源,运行完后会把之前占有的 (2, 0, 0) 也还回来。
- 更新 Work:$(3, 3, 2) + (2, 0, 0) = \mathbf{(5, 3, 2)}$
- 状态:P1 完成。
第二轮检查(此时手里有 5, 3, 2):
- 看 P0:还差 (7, 4, 3),手里只有 (5, 3, 2)。不够,跳过。
- 看 P2:还差 (6, 0, 0),手里只有 (5, 3, 2)。不够,跳过。
- 看 P3:它还差 (0, 1, 1),手里有 (5, 3, 2)。够了!
- 动作:P3 运行完,还回它占有的 (2, 1, 1)。
- 更新 Work:$(5, 3, 2) + (2, 1, 1) = \mathbf{(7, 4, 3)}$
- 状态:P3 完成。
第三轮检查(此时手里有 7, 4, 3):
- 看 P0:它还差 (7, 4, 3),手里刚好有 (7, 4, 3)。够了!
- 动作:P0 运行完,还回它占有的 (0, 1, 0)。
- 更新 Work:$(7, 4, 3) + (0, 1, 0) = \mathbf{(7, 5, 3)}$
- 状态:P0 完成。
- 看 P2:还差 (6, 0, 0),手里有 (7, 5, 3)。够了!
- 动作:P2 运行完,还回它占有的 (3, 0, 2)。
- 更新 Work:$(7, 5, 3) + (3, 0, 2) = \mathbf{(10, 5, 5)}$
- 状态:P2 完成。
第四轮检查(此时手里有 10, 5, 5):
- 看 P4:最后剩下的 P4 还差 (4, 3, 1),手里绰绰有余。
- 动作:P4 运行完,还回它占有的 (0, 0, 2)。
- 更新 Work:$(10, 5, 5) + (0, 0, 2) = \mathbf{(10, 5, 7)}$
- 状态:P4 完成。
3. 结论
结果:所有进程都顺利完成了(Finish 全部为 true)。
安全序列:$<P1, P3, P0, P2, P4>$
因为我们找到了至少一个安全序列,所以判定:当前系统处于安全状态。
4. 关键点总结:
- 为什么要加上 Allocation? 因为算法是“预演”。当一个进程满足 Need 运行完后,它不仅会还回你刚才借它的钱,还会把它以前攒在手里的钱 (Allocation) 全还回来。你的资源会越变越多。
- 如果不安全会怎样? 如果你查了一圈,发现没有任何一个进程的
Need小于等于你手里的Work,而还有进程没完成,那么检查失败。这说明系统进入了不安全状态。
5.银行家算法举例
为了让你更完整地理解银行家算法,我们不仅要看“安全性检查”,还要看“当一个进程突然开口要钱(资源)时,银行家是如何做决策的”。
银行家算法的完整过程 = 资源请求算法 + 安全性检查算法。
1. 初始状态(T0时刻)
假设系统有 3 种资源:A (10), B (5), C (7)。
目前有 5 个进程,它们的分配情况如下表:
| 进程 | Max (最大需求) | Allocation (已分配) | Need (还需) | Available (剩余可用) |
|---|---|---|---|---|
| P0 | (7, 5, 3) | (0, 1, 0) | (7, 4, 3) | (3, 3, 2) |
| P1 | (3, 2, 2) | (2, 0, 0) | (1, 2, 2) | |
| P2 | (9, 0, 2) | (3, 0, 2) | (6, 0, 0) | |
| P3 | (2, 2, 2) | (2, 1, 1) | (0, 1, 1) | |
| P4 | (4, 3, 3) | (0, 0, 2) | (4, 3, 1) |
2. 发生请求:P1 请求资源
场景:进程 P1 突然发出请求,想要 (1, 0, 2) 个资源。
银行家开始决策:
第一步:检查请求是否合法
- P1 的请求 $(1, 0, 2) \le$ P1 的 Need $(1, 2, 2)$。
- 结论:请求合法,没有超过它声明的最大需求。
第二步:检查银行手里钱够不够
- P1 的请求 $(1, 0, 2) \le$ 系统当前 Available $(3, 3, 2)$。
- 结论:银行手里确实有这么多资源。
第三步:试探性分配(假装借给他)
系统在账本上模拟分配后的样子:
- Available 减少:$(3, 3, 2) - (1, 0, 2) = \mathbf{(2, 3, 0)}$
- P1 的 Allocation 增加:$(2, 0, 0) + (1, 0, 2) = \mathbf{(3, 0, 2)}$
- P1 的 Need 减少:$(1, 2, 2) - (1, 0, 2) = \mathbf{(0, 2, 0)}$
第四步:执行安全性检查(核心)
现在的关键是:如果真的借给 P1,剩下的 (2, 3, 0) 还能让大家活下去吗?
- 看 P1:现在还差 (0, 2, 0),我手里有 (2, 3, 0)。够!
- P1 执行完,归还全部:$(2, 3, 0) + (3, 0, 2) = \mathbf{(5, 3, 2)}$
- 看 P3:还差 (0, 1, 1),我手里有 (5, 3, 2)。够!
- P3 执行完,归还全部:$(5, 3, 2) + (2, 1, 1) = \mathbf{(7, 4, 3)}$
- 看 P0:还差 (7, 4, 3),我手里有 (7, 4, 3)。够!
- P0 执行完,归还全部:$(7, 4, 3) + (0, 1, 0) = \mathbf{(7, 5, 3)}$
- 以此类推,P2 和 P4 也能顺利完成。
检查结果:存在安全序列 $<P1, P3, P0, P2, P4>$。
3. 最终决策
因为通过了安全性检查,系统宣布:同意 P1 的请求,正式划拨资源!
4. 如果换个场景:P4 请求 (3, 3, 0)
假设 P4 这时候请求 (3, 3, 0):
- Request (3, 3, 0) $\le$ Need (4, 3, 1):合法。
- Request (3, 3, 0) $\le$ Available (3, 3, 2):银行手里有钱。
- 试探分配:Available 变成 (0, 0, 2)。
- 安全性检查:
- 盯着手里仅剩的 (0, 0, 2),去看一圈:
- P0 还差 (7, 4, 3) -> 没钱,跳过。
- P1 还差 (1, 2, 2) -> 没钱,跳过。
- P2 还差 (6, 0, 0) -> 没钱,跳过。
- P3 还差 (0, 1, 1) -> 没钱,跳过。
- P4 还差 (1, 0, 1) -> 没钱,跳过。
- 结论:没有任何人能活下去,系统进入不安全状态!
- 最终决策:拒绝 P4 的请求,P4 必须进入等待状态。
总结
银行家算法就像一个深谋远虑的管家:
- 每一个请求,它都会假设如果同意了会怎样。
- 如果同意后,哪怕只有一条路(安全序列)能让大家都不死,它就同意。
- 如果同意后,大家都会因为差那么一点资源而互相等待(死锁),它就断然拒绝。
6.死锁检测和解除
1.死锁检测
如果说“银行家算法”是死锁避免(即:在发生危险前预防),那么死锁检测就是事后诊断。
当系统不采取预防或避免措施时,死锁可能会发生。此时,操作系统需要定期运行一个检测算法,看看死锁是否已经存在。如果发现了死锁,还需要采取措施进行死锁恢复。
1. 资源分配图 (Resource Allocation Graph, RAG)
死锁检测的核心工具是资源分配图。
- 圆圈:表示进程(Process)。
- 方框:表示资源类型(Resource Type),方框里的点表示该类资源的实例个数。
- 请求边 $P \to R$:进程 $P$ 正在请求资源 $R$。
- 分配边 $R \to P$:资源 $R$ 已分配给进程 $P$。
判定准则:
- 如果图中没有环路,则系统一定没有死锁。
- 如果图中有环路:
- 若每种资源只有一个实例,则一定发生了死锁。
- 若每种资源有多个实例,则可能发生了死锁(需要进一步算法检测)。
2. 检测算法:情况 A(每种资源只有一个实例)
在这种简化情况下,我们可以将资源分配图简化为等待图 (Wait-for Graph)。
- 逻辑:如果进程 $P_1$ 在等待资源 $R_a$,而 $R_a$ 正被 $P_2$ 占用,那么就在图中画一条 $P_1 \to P_2$ 的边。
- 检测结果:只要图中出现了环,就说明发生了死锁。
3. 检测算法:情况 B(每种资源有多个实例)
当资源有多个实例时,简单的找环就不够了。我们需要使用类似于“银行家算法”的逻辑,但更简单,因为此时我们已知每个进程当前的实际请求量。
数据结构:
- Available: 现有的空闲资源。
- Allocation: 每个进程已经占有的资源。
- Request: 每个进程当前正在等待的资源请求(注意:银行家算法用的是 $Need$,这里是 $Request$)。
算法逻辑(消去法):
- 寻找:找出一个当前没有被标记,且其 Request $\le$ Available 的进程 $P_i$。
- 释放:
- 如果找到了,假定这个进程能顺利拿到资源并运行完,然后释放它占有的所有资源:$Available = Available + Allocation_i$。
- 标记 $P_i$ 为已完成。
- 循环:重复上述步骤。
- 结论:如果最后还有进程没有被标记,那么这些进程就是死锁进程。
4. 什么时候进行检测?
死锁检测不是每秒都在运行的,因为它会消耗 CPU 性能。通常在以下时机触发:
- 定时检测:例如每隔 10 分钟运行一次。
- 资源利用率下降时:如果 CPU 突然变空闲,可能是因为大量进程因死锁而阻塞了。
- 资源请求失败时:当某个进程申请资源被阻塞得太久时触发。
2.死锁解除
当死锁检测算法发现系统已经陷入死锁时,操作系统必须介入以打破僵局。这就是死锁解除(Deadlock Recovery)。
解除死锁的基本思想只有一条:破坏死锁的四个必要条件之一(通常是破坏“请求和保持”或“环路等待”条件)。
1. 撤销进程法 (Process Termination)
这是最直接、最粗暴的方法,通过终止参与死锁的进程来回收资源。
- 终止所有死锁进程:
- 做法:一次性杀掉所有处于死锁环路中的进程。
- 缺点:代价极大。有些进程可能已经运行了很久,所有的计算成果都会丢失,必须从头开始。
- 逐个终止进程:
- 做法:杀掉一个死锁进程,然后立即运行死锁检测算法,看死锁是否还存在。如果还在,再杀下一个。
- 优点:损失相对较小。
- 缺点:每次杀掉进程都要重新检测,增加了系统开销。
2. 资源抢占法 (Resource Preemption)
不杀掉进程,而是强行从某个进程手中“抢走”它占有的资源,分配给其他死锁进程。
- 选择牺牲者 (Selecting a Victim):
- 必须决定抢谁的资源?通常会根据进程的优先级、已运行时间、占用的资源量等指标来计算,选择代价最小的进程。
- 回退 (Rollback):
- 被抢走资源的进程由于失去了必要的资源,无法继续运行。系统通常需要将其回退到之前的某个“安全检查点”(Checkpoint),或者干脆重置到初始状态。
- 防止饥饿 (Starvation):
- 如果系统总是抢占同一个进程的资源,该进程将永远无法完成。解决方法:在选择牺牲者时参考“抢占次数”,增加被抢占过进程的优先级。
3. 如何选择“牺牲”哪个进程?
无论是撤销还是抢占,系统都需要一套标准来决定谁该被“牺牲”。通常遵循以下原则:
- 进程优先级:优先牺牲优先级低的进程。
- 运行时间:优先牺牲刚开始运行不久的进程(运行很久的进程如果撤销太可惜)。
- 资源占用量:优先抢占占用资源多、且该资源正是别人急需的进程。
- 进程性质:优先牺牲交互式进程(相对于核心批处理任务,通常损失较小)。
- 启动次数:优先牺牲已经被撤销/抢占过多次的进程(防止饥饿)。
4. 总结:死锁处理的三道防线
为了方便记忆,你可以把操作系统处理死锁的逻辑看作三道防线:
| 防线 | 策略名称 | 核心逻辑 | 灵活度 |
|---|---|---|---|
| 第一道 | 死锁预防/避免 | 严格限制申请、银行家算法。不让死锁发生。 | 最保守,资源利用率低 |
| 第二道 | 死锁检测 | 定期画图找环,发现问题。 | 较灵活,资源利用率高 |
| 第三道 | 死锁解除 | 杀进程、抢资源。解决已经发生的问题。 | 最后的补救措施 |
补充:
-
同时检查两支筷子是否可用的方法可以预防死锁,但是不会导致饥饿问题
-
选项 D 正确性: 死锁避免的核心思想是在动态分配资源的过程中,预先评估分配后的安全性。通过银行家算法等手段,确保系统始终处于安全状态(Safe State),从而防止进入可能发生死锁的不安全状态。
选项 C 的迷惑性: 破坏死锁的四个必要条件属于死锁预防(Deadlock Prevention),而非避免。
-
选项 方法名称 破坏的必要条件 原理简述 A 银行家算法 不属于预防策略 属于死锁避免(Avoidance)策略。它在动态分配时检查系统是否处于安全状态。 B 一次性分配策略 请求并保持 (Hold and Wait) 要求进程在开始运行前一次性申请所有资源,若不满足则不运行。 C 剥夺资源法 不可剥夺 (No Preemption) 当一个已保持某些资源的进程请求新资源得不到满足时,必须释放已占有的资源。 D 资源有序分配策略 循环等待 (Circular Wait) 给所有资源编号,规定进程必须按编号递增的顺序请求资源,从而防止环路的形成。 -
死锁定理(Deadlock Theorem),也称为资源分配图简化定理。其核心内容是:
- 判定标准:系统状态为死锁的充要条件是,当且仅当该状态下的资源分配图是不可完全简化的。
- 操作过程:系统通过定期运行死锁检测算法,尝试简化资源分配图。如果图中所有的边都能被消去(即所有进程都能顺利执行完毕),说明没有死锁;如果有边残留,则说明发生了死锁。
-
处理策略 核心思想 典型算法/手段 A. 预防死锁 破坏死锁产生的四个必要条件之一。 资源静态分配、限制资源请求顺序。 B. 避免死锁 在分配资源前判断安全性。 银行家算法。 C. 检测死锁 允许死锁发生,但能通过手段发现它。 死锁定理(资源分配图简化)。 D. 解除死锁 发现死锁后,采取措施打破死锁。 撤销进程、挂起进程、剥夺资源。
浙公网安备 33010602011771号