死锁

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$:

  1. T1 获取了 Lock A,准备去获取 Lock B
  2. 与此同时,T2 获取了 Lock B,准备去获取 Lock A
  3. T1 发现 Lock B 被占用,开始等待;T2 发现 Lock A 被占用,也开始等待。
  4. 由于双方都在等待对方释放资源,且都不会主动交出已有的锁,程序彻底卡死。

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$。

  1. 正常顺序:$P1$ 申请并使用 $R1, R2$ 后释放;接着 $P2$ 申请并使用 $R1, R2$。系统安全。
  2. 错误顺序
    • $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)

具体做法:

  1. 给系统中的所有资源分配一个唯一的序号(如:Lock A = 1, Lock B = 2, Lock C = 3)。
  2. 规定所有进程必须严格按照序号递增的顺序申请资源。

为什么有效?

  • 如果 $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)

    在分配前,系统会模拟分配后的结果。如果发现分配后可能导致没有足够的资源让所有进程完成,就拒绝这次请求。

  • Banker's Algorithm flowchart的图片

    Shutterstock


3. 死锁检测与解除 (Deadlock Detection & Recovery)

这种策略比较“佛系”:允许死锁发生,但系统会定期检查是否有死锁,一旦发现就动手清理。

检测手段:

系统维护一张资源分配图 (Resource Allocation Graph),通过特定的算法(如死锁检测算法)来寻找图中是否存在不可打破的环路。

解除手段:

一旦检测到死锁,通常采用以下方式打破僵局:

  1. 资源剥夺:从一个或多个进程中抢占足够数量的资源,分配给其他死锁进程。
  2. 撤销进程(最常用):强制终止一个或多个涉及死锁的进程,直到死锁消失。
  3. 进程回退:让涉及死锁的进程回退到之前的某个“检查点”,并重新尝试。

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. 给系统中的所有资源类型分配一个唯一的整数编号(如:磁盘=1,打印机=2,锁A=3)。
    2. 规定进程必须按照编号递增的顺序请求资源。
    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$)。

安全性检查步骤:

  1. 初始化:设置一个工作向量 Work = Available
  2. 寻找进程:在进程集合中找出一个满足以下条件的进程 $P_i$:
    • 该进程尚未完成。
    • 该进程的剩余需求小于等于系统当前可用资源($Need_i \le Work$)。
  3. 释放资源:如果找到了这样的进程,假设它执行完毕,将其占有的资源全部归还($Work = Work + Allocation_i$),并标记该进程已完成。
  4. 循环/判断:重复步骤 2。如果所有进程都能标记为完成,则系统处于安全状态;否则,系统处于不安全状态

4. 总结对比

状态 定义 后果
安全状态 存在至少一个安全序列 保证不会发生死锁
不安全状态 不存在安全序列 可能发生死锁(风险状态)
死锁状态 多个进程因竞争资源而永久阻塞 系统功能受损,需要人工干预或重启

银行家算法(Banker's Algorithm) 是由艾兹格·迪杰斯特拉(Edsger Dijkstra)为 Dijkstra 操作系统设计的,是操作系统中死锁避免(Deadlock Avoidance)最著名的算法。

它的名字来源于银行:银行家在贷款时,必须保证贷款后剩下的资金仍能满足至少一个客户的最大需求,从而保证资金能够回收并循环使用。


1. 核心数据结构

为了实现该算法,我们需要维护四个关键的矩阵和向量(假设有 $n$ 个进程,$m$ 种资源):

  1. 最大需求矩阵 $Max[n, m]$:定义每个进程对每种资源的最大需求量。
  2. 分配矩阵 $Allocation[n, m]$:定义每个进程当前已分配到的每种资源量。
  3. 需求矩阵 $Need[n, m]$:定义每个进程还需要多少资源。
    • 公式:$Need[i, j] = Max[i, j] - Allocation[i, j]$
  4. 可利用资源向量 $Available[m]$:系统中当前剩余可用的每种资源量。

2. 算法的工作流程

银行家算法分为两个主要部分:资源请求算法安全性检查算法

A. 资源请求算法 (Request Algorithm)

当进程 $P_i$ 发出资源请求 $Request_i$ 时,系统按以下步骤操作:

  1. 检查请求合法性:如果 $Request_i \le Need_i$,转步骤 2;否则出错(请求超过了它宣称的最大值)。

  2. 检查资源是否足够:如果 $Request_i \le Available$,转步骤 3;否则 $P_i$ 必须等待(系统当前资源不足)。

  3. 预分配(试探性分配):系统假装把资源分配给 $P_i$,并修改数据结构:

    • $Available = Available - Request_i$
    • $Allocation_i = Allocation_i + Request_i$
    • $Need_i = Need_i - Request_i$
  4. 执行安全性检查:调用安全性算法。

    • 如果结果为安全:正式分配资源。
    • 如果结果为不安全:撤销预分配,恢复原状,$P_i$ 继续等待。
  5. Banker's Algorithm flowchart的图片

    Shutterstock

B. 安全性检查算法 (Safety Algorithm)

这是判断系统是否处于“安全状态”的核心逻辑。它尝试寻找一个安全序列

  1. 初始化:Work = AvailableFinish[all] = false
  2. 寻找满足条件的进程 $P_i$:
    • Finish[i] == false
    • Need_i \le Work
  3. 如果找到:
    • 假设 $P_i$ 获得资源并完成后,释放其占有的所有资源:Work = Work + Allocation_i
    • 标记 Finish[i] = true
    • 重复步骤 2。
  4. 如果所有进程的 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)

安全性检查过程:

  1. 找 P1:$Need(1, 2, 2) \le Available(3, 3, 2)$。P1 完成,释放资源后 $Work = (3,3,2) + (2,0,0) = (5, 3, 2)$。

  2. 找 P3:$Need(0, 1, 1) \le Work(5, 3, 2)$。P3 完成,释放后 $Work = (5,3,2) + (2,1,1) = (7, 4, 3)$。

  3. 找 P0:$Need(7, 4, 3) \le Work(7, 4, 3)$。P0 完成,释放后 $Work = (7,4,3) + (0,1,0) = (7, 5, 3)$。

  4. 以此类推,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)

第一轮检查:

  1. 看 P0:它还差 (7, 4, 3),但我们手里只有 (3, 3, 2)。不够,跳过。
  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):

  1. 看 P0:还差 (7, 4, 3),手里只有 (5, 3, 2)。不够,跳过。
  2. 看 P2:还差 (6, 0, 0),手里只有 (5, 3, 2)。不够,跳过。
  3. 看 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):

  1. 看 P0:它还差 (7, 4, 3),手里刚好有 (7, 4, 3)。够了!
    • 动作:P0 运行完,还回它占有的 (0, 1, 0)。
    • 更新 Work:$(7, 4, 3) + (0, 1, 0) = \mathbf{(7, 5, 3)}$
    • 状态:P0 完成。
  2. 看 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):

  1. 看 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) 还能让大家活下去吗?

  1. 看 P1:现在还差 (0, 2, 0),我手里有 (2, 3, 0)。够!
    • P1 执行完,归还全部:$(2, 3, 0) + (3, 0, 2) = \mathbf{(5, 3, 2)}$
  2. 看 P3:还差 (0, 1, 1),我手里有 (5, 3, 2)。够!
    • P3 执行完,归还全部:$(5, 3, 2) + (2, 1, 1) = \mathbf{(7, 4, 3)}$
  3. 看 P0:还差 (7, 4, 3),我手里有 (7, 4, 3)。够!
    • P0 执行完,归还全部:$(7, 4, 3) + (0, 1, 0) = \mathbf{(7, 5, 3)}$
  4. 以此类推,P2 和 P4 也能顺利完成。

检查结果:存在安全序列 $<P1, P3, P0, P2, P4>$。


3. 最终决策

因为通过了安全性检查,系统宣布:同意 P1 的请求,正式划拨资源!


4. 如果换个场景:P4 请求 (3, 3, 0)

假设 P4 这时候请求 (3, 3, 0):

  1. Request (3, 3, 0) $\le$ Need (4, 3, 1):合法。
  2. Request (3, 3, 0) $\le$ Available (3, 3, 2):银行手里有钱。
  3. 试探分配:Available 变成 (0, 0, 2)
  4. 安全性检查
    • 盯着手里仅剩的 (0, 0, 2),去看一圈:
    • P0 还差 (7, 4, 3) -> 没钱,跳过。
    • P1 还差 (1, 2, 2) -> 没钱,跳过。
    • P2 还差 (6, 0, 0) -> 没钱,跳过。
    • P3 还差 (0, 1, 1) -> 没钱,跳过。
    • P4 还差 (1, 0, 1) -> 没钱,跳过。
  5. 结论:没有任何人能活下去,系统进入不安全状态
  6. 最终决策拒绝 P4 的请求,P4 必须进入等待状态。

总结

银行家算法就像一个深谋远虑的管家

  • 每一个请求,它都会假设如果同意了会怎样。
  • 如果同意后,哪怕只有一条路(安全序列)能让大家都不死,它就同意。
  • 如果同意后,大家都会因为差那么一点资源而互相等待(死锁),它就断然拒绝。

6.死锁检测和解除

1.死锁检测

如果说“银行家算法”是死锁避免(即:在发生危险前预防),那么死锁检测就是事后诊断

当系统不采取预防或避免措施时,死锁可能会发生。此时,操作系统需要定期运行一个检测算法,看看死锁是否已经存在。如果发现了死锁,还需要采取措施进行死锁恢复


1. 资源分配图 (Resource Allocation Graph, RAG)

死锁检测的核心工具是资源分配图

  • 圆圈:表示进程(Process)。
  • 方框:表示资源类型(Resource Type),方框里的点表示该类资源的实例个数。
  • 请求边 $P \to R$:进程 $P$ 正在请求资源 $R$。
  • 分配边 $R \to P$:资源 $R$ 已分配给进程 $P$。

判定准则:

  1. 如果图中没有环路,则系统一定没有死锁
  2. 如果图中有环路
    • 若每种资源只有一个实例,则一定发生了死锁
    • 若每种资源有多个实例,则可能发生了死锁(需要进一步算法检测)。

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$)。

算法逻辑(消去法):

  1. 寻找:找出一个当前没有被标记,且其 Request $\le$ Available 的进程 $P_i$。
  2. 释放
    • 如果找到了,假定这个进程能顺利拿到资源并运行完,然后释放它占有的所有资源:$Available = Available + Allocation_i$。
    • 标记 $P_i$ 为已完成。
  3. 循环:重复上述步骤。
  4. 结论:如果最后还有进程没有被标记,那么这些进程就是死锁进程

4. 什么时候进行检测?

死锁检测不是每秒都在运行的,因为它会消耗 CPU 性能。通常在以下时机触发:

  • 定时检测:例如每隔 10 分钟运行一次。
  • 资源利用率下降时:如果 CPU 突然变空闲,可能是因为大量进程因死锁而阻塞了。
  • 资源请求失败时:当某个进程申请资源被阻塞得太久时触发。

2.死锁解除

当死锁检测算法发现系统已经陷入死锁时,操作系统必须介入以打破僵局。这就是死锁解除(Deadlock Recovery)

解除死锁的基本思想只有一条:破坏死锁的四个必要条件之一(通常是破坏“请求和保持”或“环路等待”条件)


1. 撤销进程法 (Process Termination)

这是最直接、最粗暴的方法,通过终止参与死锁的进程来回收资源。

  • 终止所有死锁进程
    • 做法:一次性杀掉所有处于死锁环路中的进程。
    • 缺点:代价极大。有些进程可能已经运行了很久,所有的计算成果都会丢失,必须从头开始。
  • 逐个终止进程
    • 做法:杀掉一个死锁进程,然后立即运行死锁检测算法,看死锁是否还存在。如果还在,再杀下一个。
    • 优点:损失相对较小。
    • 缺点:每次杀掉进程都要重新检测,增加了系统开销。

2. 资源抢占法 (Resource Preemption)

不杀掉进程,而是强行从某个进程手中“抢走”它占有的资源,分配给其他死锁进程。

  • 选择牺牲者 (Selecting a Victim)
    • 必须决定抢谁的资源?通常会根据进程的优先级、已运行时间、占用的资源量等指标来计算,选择代价最小的进程。
  • 回退 (Rollback)
    • 被抢走资源的进程由于失去了必要的资源,无法继续运行。系统通常需要将其回退到之前的某个“安全检查点”(Checkpoint),或者干脆重置到初始状态。
  • 防止饥饿 (Starvation)
    • 如果系统总是抢占同一个进程的资源,该进程将永远无法完成。解决方法:在选择牺牲者时参考“抢占次数”,增加被抢占过进程的优先级。

3. 如何选择“牺牲”哪个进程?

无论是撤销还是抢占,系统都需要一套标准来决定谁该被“牺牲”。通常遵循以下原则:

  1. 进程优先级:优先牺牲优先级低的进程。
  2. 运行时间:优先牺牲刚开始运行不久的进程(运行很久的进程如果撤销太可惜)。
  3. 资源占用量:优先抢占占用资源多、且该资源正是别人急需的进程。
  4. 进程性质:优先牺牲交互式进程(相对于核心批处理任务,通常损失较小)。
  5. 启动次数:优先牺牲已经被撤销/抢占过多次的进程(防止饥饿)。

4. 总结:死锁处理的三道防线

为了方便记忆,你可以把操作系统处理死锁的逻辑看作三道防线:

防线 策略名称 核心逻辑 灵活度
第一道 死锁预防/避免 严格限制申请、银行家算法。不让死锁发生。 最保守,资源利用率低
第二道 死锁检测 定期画图找环,发现问题。 较灵活,资源利用率高
第三道 死锁解除 杀进程、抢资源。解决已经发生的问题。 最后的补救措施

补充:

  1. 同时检查两支筷子是否可用的方法可以预防死锁,但是不会导致饥饿问题

  2. 选项 D 正确性: 死锁避免的核心思想是在动态分配资源的过程中,预先评估分配后的安全性。通过银行家算法等手段,确保系统始终处于安全状态(Safe State),从而防止进入可能发生死锁的不安全状态。

    选项 C 的迷惑性: 破坏死锁的四个必要条件属于死锁预防(Deadlock Prevention),而非避免。

  3. 选项 方法名称 破坏的必要条件 原理简述
    A 银行家算法 不属于预防策略 属于死锁避免(Avoidance)策略。它在动态分配时检查系统是否处于安全状态。
    B 一次性分配策略 请求并保持 (Hold and Wait) 要求进程在开始运行前一次性申请所有资源,若不满足则不运行。
    C 剥夺资源法 不可剥夺 (No Preemption) 当一个已保持某些资源的进程请求新资源得不到满足时,必须释放已占有的资源。
    D 资源有序分配策略 循环等待 (Circular Wait) 给所有资源编号,规定进程必须按编号递增的顺序请求资源,从而防止环路的形成。
  4. 死锁定理(Deadlock Theorem),也称为资源分配图简化定理。其核心内容是:

    • 判定标准:系统状态为死锁的充要条件是,当且仅当该状态下的资源分配图是不可完全简化的。
    • 操作过程:系统通过定期运行死锁检测算法,尝试简化资源分配图。如果图中所有的边都能被消去(即所有进程都能顺利执行完毕),说明没有死锁;如果有边残留,则说明发生了死锁。
  5. 处理策略 核心思想 典型算法/手段
    A. 预防死锁 破坏死锁产生的四个必要条件之一。 资源静态分配、限制资源请求顺序。
    B. 避免死锁 在分配资源前判断安全性。 银行家算法
    C. 检测死锁 允许死锁发生,但能通过手段发现它。 死锁定理(资源分配图简化)
    D. 解除死锁 发现死锁后,采取措施打破死锁。 撤销进程、挂起进程、剥夺资源。
posted @ 2025-12-21 21:24  belief73  阅读(2)  评论(0)    收藏  举报