408 · 操作系统 · 第 2 章(下)

死锁

几个进程各自攥着一部分资源,又都在等别人手里的那一份,谁也不肯先放,于是一起卡死。这一章讲两件事:死锁是怎么凑出来的(四个必要条件),以及操作系统在“限制得多严”和“出事后赔多少”之间怎么取舍(预防、避免、检测解除、不管)。

四个条件缺一不可死锁 ⇒ 互斥、不剥夺、请求并保持、循环等待同时成立。破坏任意一个就不会死锁,这是预防的依据
安全 ⇒ 不死锁;死锁 ⇒ 不安全不安全只是“有可能”死锁。银行家算法不让系统进入不安全状态,这是避免的依据
不可完全简化 ⇔ 死锁资源分配图消不完边,就是死锁(死锁定理),这是检测的依据
一 · 演进导图

每一步都在解决上一步留下的问题

死锁多个进程因竞争资源而互相等待,没有外力就都无法推进
死锁从哪来先弄清它是怎么凑出来的,才知道从哪里下手拆

并发 + 共享资源

背景
多道程序让多个进程同时推进,它们要共用打印机、磁带机、信号量这类资源
新问题
资源不够分,进程之间开始互相等

两个原因

不可剥夺资源
资源竞争
不可剥夺资源数量不够,大家都拿了一部分
推进顺序非法
请求和释放的顺序不当,比如 P1 先拿 R1 再要 R2,P2 先拿 R2 再要 R1

四个必要条件

同时成立才会死锁
内容
互斥、不剥夺、请求并保持、循环等待
意义
缺一个就不会死锁,所以处理死锁可以从“拆掉某个条件”入手
四种处理策略从上到下限制越来越松,出事的可能和出事后的代价越来越大

死锁预防

静态 · 事前
做法
设计时就破坏四个条件之一,死锁根本不可能出现
代价
限制太死:资源利用率低、并发度低,有的还会饿死进程
新问题
能不能别一刀切,只在“真的危险”时才拒绝?

死锁避免

动态 · 事前
做法
每次分配前先算一算:分出去后系统还安全吗?不安全就不分(银行家算法)
代价
要预先知道每个进程的最大需求;每次分配都要跑一遍安全性检查
新问题
最大需求常常事先不知道,干脆先分了再说?

检测与解除

事后
做法
分配时不设限,允许死锁发生;定期化简资源分配图检查,发现后再剥夺、撤销或回退
代价
检测本身有开销;解除时要牺牲一部分进程已做的工作
新问题
死锁很少见的话,连检测都嫌贵

鸵鸟策略

不处理
做法
假装死锁不会发生,出了事靠人工重启
代价
平时零开销;真死锁时损失全由用户承担。死锁足够少见、处理成本又高时,这反而划算

回头看 → 同步与互斥:信号量用错顺序也会死锁。生产者–消费者里,如果先 P(mutex) 再 P(empty),缓冲区满时生产者拿着 mutex 阻塞,消费者进不了临界区,两边互相等。这正是“推进顺序非法”。

二 · 死锁是什么

“都在等,而且都等不到”

定义:多个进程因竞争资源而造成的一种互相等待,若无外力作用,这些进程都将无法向前推进。三个关键词:多个、互相等待、无外力就推进不了。

死锁饥饿死循环
是什么多个进程互相等对方手里的资源某进程长时间得不到想要的资源(如短进程优先下的长进程),一直无法推进进程执行时一直跳不出某个循环
是否必然多个进程是 至少 2 个,才能“互相”等否 可能只有 1 个进程饥饿否 1 个进程就能死循环
是否可能处于就绪态否 死锁进程一定都在阻塞态可能 等 CPU 时在就绪态,等 I/O 设备时在阻塞态可能 它能上 CPU 运行,时间片用完回到就绪态
是否是管理者的问题是 OS 资源分配策略不当是 OS 资源分配策略不公平否 被管理者(程序员)写的代码逻辑错误
OS 能否自己发现能(死锁检测)较难,要看等待时间不能,是程序自身的 bug

产生原因

原因 1

系统资源的竞争

系统里不可剥夺资源(磁带机、打印机)数量不够多个进程同时用,大家各拿一部分,都在等剩下的。

对可剥夺资源(CPU、主存)的竞争不会引起死锁:OS 随时能把它收回来给别人。

原因 2

进程推进顺序非法

请求和释放资源的顺序不当。下面两个进程单独跑都没问题,交替执行到各自的第二行就卡死:

// P1
P(R1);
P(R2);  // 等 P2 放 R2
...
V(R2); V(R1);
// P2
P(R2);
P(R1);  // 等 P1 放 R1
...
V(R1); V(R2);

信号量使用不当也属于这一类,比如生产者–消费者里两个 P 操作颠倒顺序。

三 · 四个必要条件

死锁发生时,这四条一定同时成立

记法:前三条是“资源和进程的性质”,第四条是“这些性质凑到一起形成的局面”。前三条成立只是具备了死锁的可能,循环等待出现才可能真正卡住。

条件 1

互斥

资源一段时间内只能被一个进程占用。能同时共享的资源(如只读文件)不会让人等。

条件 2

不剥夺

进程拿到的资源在用完之前不能被别人强行夺走,只能由自己主动释放。

条件 3

请求并保持

进程已经占着至少一个资源,又去申请新资源;新资源被别人占着,它阻塞,但手里的也不放。

条件 4

循环等待

存在一条“进程–资源”的循环等待链:链中每个进程占着的资源,正被下一个进程请求。

循环等待是必要条件,但不是充分条件

左:每类资源只有 1 个。P1 占 R1 要 R2,P2 占 R2 要 R1,成环。R2 唯一的实例在 P2 手里,谁都等不到,死锁。
右:R2 有 2 个。同样的环还在,但 R2 的另一个实例在环外的 P3 手里。P3 不需要别的资源,跑完就释放,P1 拿到 R2,环被打破,不死锁。
请求边:进程 → 资源框分配边:资源实例(点)→ 进程红圈 = 阻塞的进程

同类资源数大于 1 时,有循环等待不一定死锁;每类资源都只有 1 个时,循环等待(资源分配图有环)就是死锁的充分必要条件。

四 · 死锁预防

拆掉四个条件中的一个

预防是静态的:在系统设计阶段就定好规矩,运行时不用判断。规矩越死,死锁越不可能,资源也越浪费。

破坏哪个条件方法为什么有效缺点
互斥把独占资源改造成可共享,如 SPOOLing 技术把独占的打印机在逻辑上变成共享设备进程把数据交给输出井就返回,不用等打印机不普适 很多资源没法改造,而且很多场合必须保护互斥
不剥夺① 占着资源的进程申请新资源失败时,释放手里所有资源,以后再重新申请;② 所需资源被别人占着时,由 OS 按优先级强行剥夺资源可以被收回,等待链就断了代价大 实现复杂;前一阶段的工作可能作废,只适合 CPU 这类易保存恢复状态的资源;反复申请释放增加开销;方案 ① 可能导致饥饿
请求并保持静态分配:进程运行前一次申请完它需要的全部资源,没凑齐就不投入运行;运行中不再申请运行中不再申请,就不会“占着又等”浪费 + 饥饿 有的资源只在开头或结尾用一下,却被占满全程;个别资源长期被占,等它的进程迟迟开始不了
循环等待顺序资源分配法:给资源编号,进程必须按编号递增的顺序申请,同类资源一次申请完占着大号资源的进程不可能回头申请小号资源,等待链只能单向延伸,成不了环不灵活 编号要相对稳定,不便增加新设备;实际使用顺序和编号顺序不一致时,要提前占着大号资源,造成浪费;编程麻烦
五 · 死锁避免

分之前先算:分出去以后,还能让所有人都跑完吗

避免不拆任何条件,而是在每次分配前动态判断。它依赖一个概念:安全状态。

安全状态与安全序列

安全序列:按这个顺序 ⟨P1, P2, …, Pn⟩ 给进程分资源,每个进程都能拿到它的最大需求、顺利跑完并归还资源。

安全状态:至少存在一个安全序列。安全序列可以不止一个。

不安全状态:一个安全序列都找不到。

三者的关系

  • 安全状态 ⇒ 一定不会死锁
  • 不安全状态 ⇒ 可能死锁,不是一定。进程实际不一定会申请到最大需求,也可能提前释放
  • 死锁状态 ⇒ 一定是不安全状态

银行家算法的思路:不安全虽然不一定死锁,但不冒这个险,只允许系统停在安全状态。

银行家算法的数据结构(n 个进程,m 类资源)

Available[m]每类资源现在还空闲多少
Max[n][m]进程 i 对资源 j 的最大需求,运行前声明
Allocation[n][m]进程 i 已经拿到多少资源 j
Need[n][m]还要多少:Need = Max − Allocation

资源请求算法 + 安全性算法

// 进程 Pi 发出请求向量 Request
// 向量比较 a ≤ b:每个分量都 ≤
if (Request > Need[i])
    error();   // ① 超过它声明的最大需求:出错
if (Request > Available)
    wait();    // ② 资源不够:Pi 阻塞等待
// ③ 试探分配
Available     -= Request;
Allocation[i] += Request;
Need[i]       -= Request;
// ④ 安全性检查
if (!Safe()) {
    撤销③的三行;  // 恢复原状态
    wait();       // Pi 等待
}             // 安全:正式分配
bool Safe() {
    int  Work[m] = Available;
    bool Finish[n] = {false};
    while (存在 i: !Finish[i] && Need[i] <= Work) {
        Work += Allocation[i]; // Pi 跑完,归还资源
        Finish[i] = true;
    }
    return 所有 Finish[i] 都为 true;
}
// 为什么是 Work += Allocation 而不是 += Max?
// Pi 先拿 Need 再全部归还:
// Work − Need + Max = Work + Allocation
六 · 动手算 · 银行家算法

经典例题:5 个进程、3 类资源

表格可以直接改数字,Need 自动算。“安全性检查”一步步展示 Work 的变化;“资源请求”按 ①②③④ 判断能不能分。本页约定:每一轮从 P0 往后扫,选编号最小的、满足 Need ≤ Work 的进程。考试里写出任意一个合法的安全序列都算对。

    七 · 死锁检测与解除

    先放手分配,再用资源分配图查有没有卡死

    检测不需要知道最大需求,只看“现在谁占着什么、谁在等什么”。工具是资源分配图。

    资源分配图怎么画

    • 圆圈 = 进程;方框 = 一类资源;框里的点 = 该类资源的一个实例
    • 请求边:进程 → 资源方框(指向框的边缘),表示申请一个
    • 分配边:资源框里的某个点 → 进程,表示这个实例已分给它

    化简与死锁定理

    1. 找一个既不阻塞又不孤立的进程:它的每条请求边,所申请的资源都有足够空闲实例
    2. 让它跑完:消去它所有请求边和分配边,它变成孤立点,资源归还
    3. 归还后可能有别的进程不再阻塞,重复 1、2

    死锁定理:状态 S 为死锁 ⇔ S 的资源分配图不可完全简化。消不掉的边所连的进程就是死锁进程。

    动手看 · 一步步化简

    请求边 进程→资源 分配边 实例→进程 本步消去的边 已消去(淡显)

      约定:每步从 P1 往后找,消去编号最小的可消去进程。化简结果与消去顺序无关。

      检测算法和银行家的安全性算法长得几乎一样,区别在于:检测用的是进程当前的请求(Request),不是最大需求剩下的 Need;并且没占任何资源的进程直接记为 Finish = true。

      死锁解除

      方法 1

      资源剥夺法

      挂起某些死锁进程,抢占它们的资源分给其他死锁进程。

      要防止被挂起的进程长时间拿不回资源而饥饿。

      方法 2

      撤销进程法

      强制撤销部分甚至全部死锁进程,收回它们的资源。可按优先级和撤销代价挑选。

      实现简单,但代价可能很大:快跑完的进程一撤销就前功尽弃。

      方法 3

      进程回退法

      让一个或多个进程回退到足以回避死锁的地步,回退时自愿释放资源,而不是被剥夺。

      要求系统保存进程的历史信息,设置还原点。

      选谁开刀,一般看:优先级、已执行多久、还要多久完成、已占多少资源、交互式还是批处理。

      八 · 常考计算

      “最少几个资源保证不死锁”:考虑最坏情况,每人都只差 1 个

      推导

      n 个进程,每个最多需要 k 个同类资源。最坏的情况:资源被平均分掉,每个进程都拿到 k − 1 个,都还差 1 个,谁也跑不完,这时用掉了 n(k − 1) 个。

      只要再多 1 个,就一定能让某个进程凑满 k 个跑完,它释放 k 个,其他进程接着都能完成。

      不死锁的最少资源数 = n(k − 1) + 1

      各进程需求不同时同理:最少资源数 = Σ(ki − 1) + 1。

      试一试

      正着问:最少要多少资源

      3 个进程,每个最多要 4 台打印机。

      3 × (4 − 1) + 1 = 10 台。9 台时可能每人 3 台全卡住。

      反着问:最多允许几个进程

      有 m 个资源,每个进程最多要 k 个。解不等式 n(k − 1) + 1 ≤ m,得 n ≤ (m − 1) ÷ (k − 1),向下取整。

      例:m = 8,k = 3 → n ≤ 3.5 → 最多 3 个进程(4 个时 4 × 2 + 1 = 9 > 8)。

      反着问:每个进程最多能要几个

      m 个资源、n 个进程,求 k 的最大值:k ≤ (m − 1) ÷ n + 1,向下取整。

      例:m = 10,n = 3 → k ≤ 4 → k 最大为 4(k = 5 时要 13 个)。

      判断:给定数字会不会死锁

      直接比较 m 和 n(k − 1) + 1:m ≥ 它就不可能死锁;m 更小就可能死锁(不是一定)。

      九 · 易错速记 & 策略速查

      选择题直接对表

      策略时机分配资源的态度主要手段优点缺点
      预防事前 · 静态保守,宁可资源闲置一次申请全部、剥夺、按编号顺序分配不会死锁;适合做突发式处理的进程;静态分配时不必剥夺效率低,进程初始化时间长;剥夺次数多;不便灵活申请新资源
      避免事前 · 动态折中,运行时判断是否可能死锁银行家算法,找安全序列不必剥夺;比预防限制少,资源利用率更高必须事先知道最大需求;进程不能被长时间阻塞;每次分配都要计算
      检测与解除事后宽松,只要资源够就分化简资源分配图;剥夺、撤销、回退不延长进程初始化时间,允许对死锁现场处理检测有开销;解除要剥夺或撤销,造成损失
      鸵鸟策略不处理完全不管忽略死锁零开销死锁发生只能人工处理(重启)
      不安全 ≠ 死锁不安全状态只是可能死锁;死锁一定是不安全状态;安全状态一定不死锁。
      银行家算法是“避免”,不是“预防”它不破坏任何必要条件,而是动态判断,不让系统进入不安全状态。
      循环等待:必要不充分每类资源只有 1 个时,有环 ⇔ 死锁。
      资源分配图有环不一定死锁资源实例数 > 1 时,要化简才能判断;不可完全简化才是死锁。
      死锁进程至少 2 个,且都在阻塞态饥饿可以只有 1 个,也可能在就绪态;死循环是程序员的问题。
      Request > Need 是出错,不是等待Request > Available 才是等待;两关都过了才做安全性检查。
      安全序列不唯一题目问“以下哪个是安全序列”时,逐个代入模拟 Work 即可。
      各方法对应的条件SPOOLing → 互斥;静态分配(一次申请全部)→ 请求并保持;顺序资源分配 → 循环等待;释放已占资源 → 不剥夺。
      可剥夺资源不会引起死锁竞争 CPU、主存不会死锁;死锁来自不可剥夺资源和推进顺序非法。
      n(k − 1) + 1最坏情况每人差 1 个;反问最多几个进程时向下取整。