并发 死锁 & 饥饿
一、概述
并发处理中需要解决两个问题,死锁和饥饿。
处理死锁问题又三种常用方法:预防、检测、避免
二、死锁原理
死锁:一组相互竞争系统资源或进行通信的进程间的 “永久” 阻塞。
所有死锁都涉及 两个 / 多个 进程之间对资源需求的冲突。
我们使用 联合进程图(joint progress diagram) 来表示两个进程竞争两个资源的进展情况。
在下图中,X轴表示P的进展,Y轴表示Q的执行进展,因此两个进程的共同进展由从原点开始,到东北方向的前进路径表示。
图中显示了 P 和 Q 都请求 资源A、资源B、资源A和B 的区域,假定每个进程需要对资源进行互斥访问控制,有如下六条路径:
- Q 获得 B,然后获得 A;再后释放 B 和 A;当 P 恢复执行时,它可获得全部资源。
- Q 获得 B,然后获得 A;P 执行 并阻塞在对 A 的请求上;Q 释放 B 和 A;当 P 恢复执行时,它可以获得全部资源
- Q 获得 B,P 获得 A;继续执行时,Q 阻塞在 A 上、P 阻塞在 B 上,死锁不可避免
- P 获得 A,Q 获得 B;继续执行时,Q 阻塞在 A 上、P 阻塞在 B 上,死锁不可避免
- P 获得 A,然后获得 B;Q 执行 并阻塞在对 B 的请求上;P 释放 A 和 B;当 Q 恢复执行时,它可以获得全部资源
- P 获得 A,然后获得 B;再后释放 A 和 B;当 Q 恢复执行时,它可获得全部资源。
图中灰色阴影区域称为 敏感区域(fatal region),执行路径进入敏感区域,死锁将不可避免。
1. 资源分类
资源通常分为两类:可重用资源、可消耗资源。
① 可重用资源
可重用资源:一次进攻一个进程安全使用且不因使用而耗尽的资源。进程得到并使用后,释放资源供其他进程再次使用。
举例:处理器、I/O通道、内存&外存 等硬件资源,设备、文件、数据库、信号量等数据结构。
② 可消耗资源
可消耗资源:可被创建(生产)、销毁(消耗)的资源。某种类型可消耗资源的数量通常没有限制,无阻塞生产进程可以创建任意数量的这类资源,为一次性使用资源。
举例:中断、信号、消息、I/O缓存区信息。
2. 资源分配图
表征进程资源分配的 有效工具 是 资源分配图(resource allocation graph),它是有向图,说明了系统资源、进程状态,其中每个资源、进程用节点表示,圆点表示资源的一个示例,一个资源可拥有多个实例。(I/O设备有多个资源实例,它由操作系统中的资源分配模块分配。)
下图是一个死锁的例子(对应开头的小汽车):
3. 条件
必要条件(死锁可能性):
- 互斥
一次只有一个进程可以使用资一个资源 - 占有且等待
一个进程等待其他进程时,继续占有已分配资源 - 不可抢占
不能强行抢占进程已占有资源
充分条件(死锁存在性):
- 循环等待
存在闭合的进程链,每个进程至少占有此链中下一个进程所需资源。
三、死锁预防
死锁预防(deadlock prevention)策略是试图设计一种系统来排除发生死锁的可能性。
死锁预防两类方法:
- 间接预防:防止三个必要条件发生(互斥、占有且等待、不可抢占)
- 直接预防:防止一个充分条件发生(循环等待)
互斥:
若要对资源进行互斥访问,操作系统需支持互斥。如某些文件支持多个读访问,但允许互斥的写访问,有可能会在请求写权限时发生死锁。
占有且等待:
可以要求进程一次性请求所有资源,并阻塞这个进程直至所有请求满足。该方法存在三个方面的低效性:
- 不利己: 一个进程可能被阻塞很长时间,以等待满足其所有的资源请求,实际上只要有部分资源,就可先继续执行一部分。
- 不利他: 分配给一个进程的资源可能会在相当一段时间内不会被进程使用,而其他进程也无法使用。
- 未知性: 进程可能实现并不知道它需要的所有资源。
不可抢占:
- 占有某资源的一个进程进一步申请资源时,若被拒绝则必须释放最初占有资源,必要时才可再次申请。
- 一个进程请求被另一个进程占有的资源时,操作系统可以抢占并要求另一个进程释放资源,这取决于进程优先级是否不同。
循环等待:
循环等待条件可通过定义资源的线性顺序预防:若进程已分配了R类型的资源,则其接下来请求的资源只能是哪些排在R类型之后的资源,但该方法可能较为低效。(证明:假设进程A、B死锁,原因:A 获得 资源Ri 并请求 资源Rj,而 B 获得 资源Rj 并请求 资源Ri,则需 i < j 且 j < i,无法成立)
四、死锁避免
死锁避免(deadlock avoidance)与死锁预防差距很小,它允许三个必要条件,而通过明智地选择保证永远不会到达死锁点。
死锁避免的两种方法:
- 若一个进程的请求会导致死锁,则不启动进程
- 若一个进程增加的资源请求会导致死锁,则不允许这一资源分配
1. 进程启动拒绝
考虑一个有 n 个进程和 m 种不同类型资源的系统,定义以下向量、矩阵:
从中可以看出以下关系成立:
-
R j = V j + ∑ i = 1 N A j R_j = V_j + \sum_{\mathclap{i=1}}^{N} A_j Rj=Vj+i=1∑NAj
对所有j,所有资源要么可用,要么已被分配 -
C i j ≤ R i C_{ij} ≤ R_i Cij≤Ri
对所有i,j,任何一个进程对任何一种资源都不会超过这个进程最初声明这种资源的总量 -
A i j ≤ C i j A_{ij} ≤ C_{ij} Aij≤Cij
对所有i,j,分配给任何一个进程的任何一个资源都不会超过这个进程最初声明的此资源的最大请求量
所以,死锁避免策略如下:
若一个新进程的资源需求会导致死锁,则拒绝启动这个新进程,即只有
R
j
≥
C
(
n
+
1
)
j
+
∑
C
i
j
R_j ≥ C_{(n+1)j} + \sum C_{ij}
Rj≥C(n+1)j+∑Cij
(满足所有进程的 最大请求量 + 新的进程请求 时)才会启动一个新进程 Pn+1,该策略并非最优,它包含最坏情况:所有进程同时发出最大请求。
2. 资源分配拒绝
资源分配拒绝策略,又称为 银行家算法(banker algorithm),它需要定义状态、安全状态的概念。
考虑一个系统,它有固定数量的进程和固定数量的资源,任何时候一个进程可能分配到 0个/多个资源,因此状态包含上一小节定义的两个向量 Resource、Available 及两个矩阵 Claim、Allocation。
- 安全状态(safe state) :至少有一个资源分配序列不会导致死锁(所有进程都能运行直至结束)
- 不安全状态(unsafe state) :非安全的一个状态,不安全状态不一定导致死锁
在当前资源情况下,可用资源应满足当前的分配情况和任何一个请求的最大需求,即:
C
i
j
−
A
i
j
≤
V
j
C_{ij}-A_{ij}≤V_{j}
Cij−Aij≤Vj
即系统在进行资源分配之前,应先计算此次分配资源的安全性,若此次分配不会导致系统进入不安全状态,则将资源分配给线程,否则进程等待。死锁避免策略能保证系统中的进程、资源总处于安全状态。
安全状态确定:
在下图中,针对P1,我们无法分配任何资源,但我们可以给P2分配一个R3单元,这样P2就拥有了所需的最大资源,从而可以运行到结束。此后,其持有的所有资源回到可用资源池。依次为P1、P3、P4分配资源,所有进程运行结束,此时定义状态为安全状态。
不安全状态确定:
在下图中,假设P1请求另外一个R1和R3单元,若同意该请求,则到达第②步,该状态不安全(每个进程至少需要一个额外的R1单元,但现在没有一个可用的R1单元),因此P1将阻塞等待。此时,并不处于死锁状态,它仅有死锁的可能,若P1运行后先行释放一个R1和一个R3单元,后来再次需要,系统将到达安全状态。
因此死锁避免策略,不准确地预测死锁,而是保证其永远不会出现。
死锁避免:
优点:
- 无需抢占、回滚,限制较少
限制:
- 必须事先声明每个进程请求最大资源
- 所讨论的进程必须无关,即执行顺序无同步要求限制
- 分配的资源数量必须固定
- 占有资源,进程不能退出
五、死锁检测
死锁检测(deadlock detection):通过限制访问资源在进程上加以约束来解决死锁问题。只要有可能,就给进程分配所请求的资源,操作系统周期性的执行算法检测循环等待条件。
1. 死锁检测算法
死锁检测算法使用了上节定义的 Allocation矩阵、Available 向量,此外还定义了一个请求矩阵Q,其中 Qij 表示 进程i 请求的 j类资源 的数量,它主要是标记为死锁进程的过程。最初,所有进程都是未标记的,执行以下步骤:
- 标记 Allocation 矩阵种 一行全为0的进程
- 初始化一个临时向量W,令其等于 Available 向量
- 查找下标i,对所有的1 ≤ k≤ m, Qik ≤ Wk,若找不到这样的行,终止算法
- 若找到这样的行,标记进程i,并把 Allocation 矩阵中相应行加到 W 中,即对所有的 1 ≤ k≤ m, 令Wk = Wk + Aik
我们举例描述该算法:
- 由于 P4 没有已分配资源,标记 P4
- 令 W = (0 0 0 0 1)
- 进程 P3 的请求小于等于W,因此标记 P3,并令 W = W + (0 0 0 0 1) = (0 0 0 1 0)
- 不存在其他未标记进程在 Q 中的行小于等于 W,因此终止算法。
算法结果 P1 和 P2 未标记,表明这两个进程是死锁的。
2. 恢复
检测到死锁,需要某种策略进行恢复(按复杂度递增):
- 取消所有的死锁进程。
- 把每个死锁进程回滚到前面定义的某些检查点,并重新启动所有进程。
此时要求构建回滚、重启机制,再次死锁风险仍旧存在,但因并发的不确定性,一般不会出现。 - 连续取消死锁进程直至不在存在死锁。
所取消进程的顺序应基于某种最小代价原则,在每次取消后,再次调用检测算法,测试是否仍存在死锁。 - 连续抢占资源直至不在出现死锁。
类似第3种策略,每次抢占后重新调用检测算法,被抢占资源的进程需回滚到获得资源前的状态。
策略3/4 可选择原则如下:
- 目前为止消耗的处理器时间最少
- 目前为止产生的输出最小
- 预计剩下的时间最长
- 目前为止分配的资源总量最小
- 优先级最低
六、综合的死锁策略
解决死锁各种策略都有优缺点,所以我们在不同的情况下使用不同的策略:
- 把资源分成几组不同的资源类
- 为预防在资源类之间由于循环等待产生死锁,可使用前面定义的线性排序策略
- 在一个资源类中使用最合适的算法
常见资源类:
- 可交换空间:进程交换所用外存中的存储块
- 进程资源:可分配设备(如磁带设备、文件)
- 内存:可按 页/段 分配给进程
- 内部资源:诸如 I/O 通道
可采用的策略:
- 可交换空间:若知道最大存储需求,可要求一次性分配所有请求资源来预防死锁
- 进程资源:死锁避免策略通常很有效
- 内存:基于抢占的与方式最适合的策略,当被抢占被换至外存以解决死锁
- 内部资源:可使用基于资源排序的预防策略