1 死锁的定义
死锁是一组互相竞争系统资源或进行通信的进程间的永久阻塞。当一组进程中的每一个进程都在等待某个事件,而仅有这组进程中被阻塞的其他进程才可触发该事件时,就称这组进程发生了死锁。死锁在没有外界干预的情况下是永久性的。
2 死锁的条件
死锁有3个必要条件:
- 互斥:一次只有一个进程可以使用一个资源,其他进程不能访问已经分配给其他进程的资源。
- 不可剥夺:不能够抢占其他进程已有的资源。
- 占有且等待:如果一个进程尝试获取一个资源没有成功,那么会进入等待状态,并且这个进程持有的资源不会被释放。
除此之外,要产生死锁,最关键的条件是循环等待:
- 循环等待:存在一个闭合的进程链,每个进程至少占有此链中下一个进程所需的资源。
第4个条件是前3个条件的潜在结果,之所以产生循环等待是因为有前三个条件。
解决死锁的方式有3种:
- 预防死锁
- 避免死锁
- 检测死锁并从中恢复。
3 预防死锁
死锁预防策略是试图设计一种方式来排除发生死锁的可能性,预防策略分为两类:
- 间接死锁预防方法,即防止前三个必要条件中任何一个条件的发生
- 直接死锁预防方法,即防止循环等待条件发生。
互斥条件
互斥条件一般不可能禁止,某些资源必须同一时间只能由一个进程操作才能保证其安全性。
占有且等待
为了预防占有且等待条件条件,可以要求进程一次性请求所有资源,如果无法一次性请求那么就进行等待。但是这样做存在的问题有:
- 一个进程可能会被阻塞很长时间,以等待可以一次性获取到所有资源的时机。
- 从实际角度出发,进程在持有一部分资源的时候就可以继续正常运行,其次某个资源该进程只会持有一小部分时间,大部分时间都不会使用该资源。
- 进程可能无法预知它将来需要的资源
不可抢占
预防不可抢占的策略有以下几种:
- 当占有某个资源的进程在进一步尝试获取其它资源时被拒绝,那么该进程必须释放自己占有的资源,在必要时可以尝试重新获取这个被释放的资源。
- 当一个进程请求的资源被其他资源占有时,操作系统可以抢占这个持有资源的进程,要求它释放资源。
循环等待
循环等待的预防可以通过定义资源获取的访问顺序。若定义资源获取序列 { A , B , C , D } \{A,B,C,D\} {A,B,C,D},当获取到资源 B B B 时,只能够尝试获取资源 C C C 或者 D D D。当然这个预防方式可能是低效的,可能会在没有必要的情况下拒绝资源的获取。
4 死锁避免
解决死锁问题的另一种方法是死锁避免,它和死锁预防的差别很小,可以把它理解为死锁预防的一种特例。
死锁避免策略在允许三个必要条件存在的条件下,来确保永远不会达到死锁点。
4.1 死锁避免方法
死锁避免方法有:
- 若一个进程的请求会导致死锁,那么不启动该进程。
- 若一个进程增加的资源请求会导致死锁,则不允许这个资源的分配。
相比死锁预防策略,死锁避免策略并发性更强。但是在使用中也有诸多限制:
- 必须事先声明每个进程请求的最大资源
- 分配的资源数量必须是固定的
- 在占有资源时,进程不能够退出
- 所讨论的进程的执行顺序必须没有任何同步要求的限制
我们先考虑第一个死锁避免方式,考虑一个有着 n n n 个进程和 m m m 种不同类型资源的系统,定义以下向量和矩阵。
- 系统中每种资源的总量: R = ( R 1 , R 2 , ⋯ , R m ) R=(R_1,R_2,\cdots,R_m) R=(R1,R2,⋯,Rm)
- 当前剩余的资源总量: V = ( V 1 , V 2 , ⋯ , V m ) V=(V_1,V_2,\cdots,V_m) V=(V1,V2,⋯,Vm)
- 进程
i
i
i 对资源
j
j
j 的需求矩阵,用
C
i
j
C_{ij}
Cij表示,下面矩阵
C
C
C 给出了每个进程对每种资源的最大需求,每行表示一个进程对所有类型资源的请求。为了避免死锁,这个矩阵必须事先声明:
C = [ C 11 C 12 C 13 ⋯ C 1 m C 21 C 22 C 23 ⋯ C 2 m ⋯ ⋯ ⋯ ⋯ ⋯ C n 1 C n 2 C n 3 ⋯ C n m ] C= \left[ \begin{matrix} C_{11} & C_{12} & C_{13} & \cdots & C_{1m}\\ C_{21} & C_{22} & C_{23} & \cdots & C_{2m} \\ \cdots & \cdots & \cdots & \cdots & \cdots \\ C_{n1} & C_{n2} & C_{n3} & \cdots & C_{nm} \end{matrix} \right] C=⎣⎢⎢⎡C11C21⋯Cn1C12C22⋯Cn2C13C23⋯Cn3⋯⋯⋯⋯C1mC2m⋯Cnm⎦⎥⎥⎤ - 当前分配给进程
i
i
i 的资源
j
j
j,用
A
i
j
A_{ij}
Aij表示,使用矩阵
A
A
A表示当前资源的分配情况:
A = [ A 11 A 12 A 13 ⋯ A 1 m A 21 A 22 A 23 ⋯ A 2 m ⋯ ⋯ ⋯ ⋯ ⋯ A n 1 A n 2 A n 3 ⋯ A n m ] A= \left[ \begin{matrix} A_{11} & A_{12} & A_{13} & \cdots & A_{1m}\\ A_{21} & A_{22} & A_{23} & \cdots & A_{2m} \\ \cdots & \cdots & \cdots & \cdots & \cdots \\ A_{n1} & A_{n2} & A_{n3} & \cdots & A_{nm} \end{matrix} \right] A=⎣⎢⎢⎡A11A21⋯An1A12A22⋯An2A13A23⋯An3⋯⋯⋯⋯A1mA2m⋯Anm⎦⎥⎥⎤
从中可以看出以下关系成立:
- 对所有资源 j j j,要么资源可用,要么已经被分配给进程,即: R j = V j + ∑ N i = 1 A i j R_j=V_j+\sum_N^{i=1}A_{ij} Rj=Vj+N∑i=1Aij
- C i j ≤ R i C_{ij}\leq R_i Cij≤Ri,对所有的 i i i、 j j j,任何一个进程对任何一种资源的请求都不能够超过系统中该资源的总量。
- A i j ≤ C i j A_{ij}\leq C_{ij} Aij≤Cij,对所有的 i i i、 j j j,分配给任何一个进程的任何一种资源都不会超过这个进程最初声明的此资源最大请求量。
所以死锁避免的策略为:若一个新的进程资源需求会导致死锁,则拒绝启动这个新进程,仅当
R
j
≥
C
j
(
n
+
1
)
+
∑
n
i
=
1
C
i
j
,
j
∈
[
0
,
m
]
R_j\geq C_{j(n+1)}+\sum_n^{i=1}C_{ij},j \in [0,m]
Rj≥Cj(n+1)+n∑i=1Cij,j∈[0,m]
时才启动一个新进程
P
n
+
1
P_{n+1}
Pn+1。当然这个策略不是最优的,它只假设了最坏的情况,即所有进程同时发出它们的最大请求。
4.2 银行家算法
第二个死锁避免方式又称银行家算法,需要定义安全状态和不安全状态,安全状态指至少一个资源分配序列不会导致死锁,即所有进程都能够顺利运行到结束,不安全状态指非安全的一个状态。
为了准确描述银行家算法,我们给出一个例子:
一共有4个进程和3种资源,资源总量的向量 R = ( 9 , 3 , 6 ) R=(9,3,6) R=(9,3,6),每个进程对每种资源的最大需求矩阵 C = [ 3 2 2 6 1 3 3 1 4 4 2 2 ] C=\left[ \begin{matrix} 3 & 2 & 2 \\ 6 & 1 & 3 \\ 3 & 1 & 4 \\ 4 & 2 & 2 \\ \end{matrix} \right] C=⎣⎢⎢⎡363421122342⎦⎥⎥⎤,假设当前资源分配矩阵 A A A 为: A = [ 1 0 0 6 1 2 2 1 1 0 0 2 ] A=\left[ \begin{matrix} 1 & 0 & 0 \\ 6 & 1 & 2 \\ 2 & 1 & 1 \\ 0 & 0 & 2 \\ \end{matrix} \right] A=⎣⎢⎢⎡162001100212⎦⎥⎥⎤,问当前状态是否为安全状态?或者说,可用资源能否满足当前的分配情况和任何一个进程的最大需求?
从上述条件可知,发现剩余资源向量为
V
=
(
0
,
1
,
1
)
V=(0,1,1)
V=(0,1,1)。
观察进程
P
1
P_1
P1,发现
P
1
P_1
P1是不可能在此时正常结束的,因为进程
P
1
P_1
P1还需要2个
R
1
R_1
R1资源、两个
R
2
R_2
R2资源和两个
R
3
R_3
R3资源。
观察进程
P
2
P_2
P2,发现进程只需要一个
R
3
R_3
R3资源就达到了所需的最大资源,从而可以顺利运行完成。当
P
2
P_2
P2进程执行结束后,资源便会归还给资源池。
P
2
P_2
P2进程在获得到一个
R
3
R_3
R3资源并运行结束后,此时的资源分配矩阵
A
=
[
1
0
0
0
0
0
2
1
1
0
0
2
]
A=\left[ \begin{matrix} 1 & 0 & 0 \\ 0 & 0 & 0 \\ 2 & 1 & 1 \\ 0 & 0 & 2 \\ \end{matrix} \right]
A=⎣⎢⎢⎡102000100012⎦⎥⎥⎤,可用资源向量
V
=
(
6
,
2
,
3
)
V=(6,2,3)
V=(6,2,3)。
现在再来看进程
P
1
P_1
P1,发现进程
P
1
P_1
P1可以正常完成,假设现在选择
P
1
P_1
P1进程执行并运行结束后,资源分配矩阵
A
=
[
0
0
0
0
0
0
2
1
1
0
0
2
]
A=\left[ \begin{matrix} 0 & 0 & 0 \\ 0 & 0 & 0 \\ 2 & 1 & 1 \\ 0 & 0 & 2 \\ \end{matrix} \right]
A=⎣⎢⎢⎡002000100012⎦⎥⎥⎤
现在还差进程
P
3
P_3
P3和进程
P
4
P_4
P4等待执行,此时剩余资源向量
V
=
(
7
,
2
,
3
)
V=(7,2,3)
V=(7,2,3),下一步也可以正常将全部资源分配给
P
3
P_3
P3并执行完成,最后再分配给
P
4
P_4
P4进程。
至此4个进程全部执行完成,执行顺序为
P
2
→
P
1
→
P
3
→
P
4
P_2→P_1→P_3→P_4
P2→P1→P3→P4。
总的来说,银行家算法就是当进程请求一组资源时,先判断这个进程在请求了指定的资源后能不能处于安全状态,如果可以,就同意这个请求,如果不行,则阻塞该进程直到能够满足。
再举个例子,同样是4个进程3种资源,资源总量和需求矩阵和上述例子相同,假设初始资源分配矩阵 A = [ 1 0 0 5 1 1 2 1 1 0 0 2 ] A=\left[ \begin{matrix} 1 & 0 & 0 \\ 5 & 1 & 1 \\ 2 & 1 & 1 \\ 0 & 0 & 2 \\ \end{matrix} \right] A=⎣⎢⎢⎡152001100112⎦⎥⎥⎤。
可以计算出剩余资源向量
V
=
(
1
,
1
,
2
)
V=(1,1,2)
V=(1,1,2),现在考虑进程
P
1
P_1
P1,假设
P
1
P_1
P1请求一个
R
1
R_1
R1资源和一个
R
3
R_3
R3资源,如果同意了,那么资源分配矩阵
A
=
[
2
0
1
5
1
1
2
1
1
0
0
2
]
A=\left[ \begin{matrix} 2 & 0 & 1 \\ 5 & 1 & 1 \\ 2 & 1 & 1 \\ 0 & 0 & 2 \\ \end{matrix} \right]
A=⎣⎢⎢⎡252001101112⎦⎥⎥⎤,
V
=
(
0
,
1
,
1
)
,
V=(0,1,1),
V=(0,1,1),此时可以得到每个进程需要的资源矩阵:
C
−
A
=
[
1
2
1
1
0
2
1
0
3
4
2
0
]
C-A=\left[ \begin{matrix} 1 & 2 & 1 \\ 1 & 0 & 2 \\ 1 & 0 & 3 \\ 4 & 2 & 0 \\ \end{matrix} \right]
C−A=⎣⎢⎢⎡111420021230⎦⎥⎥⎤
这是个不安全的状态,因为每个进程都需要至少一个
R
1
R_1
R1资源,因此进程
P
1
P_1
P1的请求将会被拒绝,因为如果请求了有可能会导致死锁。
银行家算法不能够准确预测死锁,它的策略是将死锁的可能性降到0。
5 死锁检测
死锁检测不会限制资源访问或者约束进程的行为,当发生死锁时,通过死锁的检测来解除死锁状态。
死锁检测的一个常用算法描述如下:
使用上述定义的矩阵
C
C
C和矩阵
A
A
A,然后定义一个请求矩阵
Q
Q
Q,
Q
i
j
Q_{ij}
Qij表示进程
i
i
i正在请求的
j
j
j类资源的数量。执行以下步骤:
- 首先标记矩阵 A A A 中一行全为0的进程,因为没有分配资源的进程不存在死锁。
- 初始化向量 W W W,令 W = V W=V W=V( V V V为未分配给进程的每种资源的总量)
- 查找下标 i i i,忽略已经标记的进程 P i P_i Pi,使 Q Q Q的第 i i i行小于等于 W W W,即 Q i k ≤ W k Q_{ik}\leq W_k Qik≤Wk。如果在 Q Q Q中找不到这样行,终止算法。
- 如果找到,标记进程 P i P_i Pi,并把矩阵 A A A中相应的行与 W W W相加,即 W k = W k + A i k W_k=W_k+A_{ik} Wk=Wk+Aik。
当上述算法执行结束后,每个未被标记的进程都存在死锁。该算法不能预防死锁,它只能确定是否存在死锁。
现在定义资源需求矩阵为 Q = [ 0 1 0 0 1 0 0 1 0 1 0 0 0 0 1 1 0 1 0 1 ] Q=\left[ \begin{matrix} 0 & 1 & 0 & 0 & 1\\ 0 & 0 & 1 & 0 & 1\\ 0 & 0 & 0 & 0 & 1\\ 1 & 0 & 1 & 0 & 1\\ \end{matrix} \right] Q=⎣⎢⎢⎡00011000010100001111⎦⎥⎥⎤,资源总量 R = ( 2 , 1 , 1 , 2 , 1 ) R=(2,1,1,2,1) R=(2,1,1,2,1),此时:资源分配矩阵 A = [ 1 0 1 1 0 1 1 0 0 0 0 0 0 1 0 0 0 0 0 0 ] A=\left[ \begin{matrix} 1 & 0 & 1 & 1 & 0\\ 1 & 1 & 0 & 0 & 0\\ 0 & 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 0 & 0\\ \end{matrix} \right] A=⎣⎢⎢⎡11000100100010100000⎦⎥⎥⎤, V = ( 0 , 0 , 0 , 0 , 1 ) V=(0,0,0,0,1) V=(0,0,0,0,1),问现在是否存在死锁?
按照算法执行步骤:
- 标记 P 4 P_4 P4,因为矩阵 A A A 中 P 4 P_4 P4 所在的行全为0。
- 令 W = V W=V W=V,此时 W = ( 0 , 0 , 0 , 0 , 1 ) W=(0,0,0,0,1) W=(0,0,0,0,1)
- 进程 P 3 P_3 P3请求小于等于 W W W,因此标记 P 3 P_3 P3,令 W = W + A 3 = ( 0 , 0 , 0 , 0 , 1 ) + ( 0 , 0 , 0 , 1 , 0 ) = ( 0 , 0 , 0 , 1 , 0 ) W=W+A_3=(0,0,0,0,1)+(0,0,0,1,0)=(0,0,0,1,0) W=W+A3=(0,0,0,0,1)+(0,0,0,1,0)=(0,0,0,1,0)
- 不存在其他未标记进程 P i P_i Pi在 Q i Q_i Qi中小于等于 W W W,此时终止算法。此时 P 1 P_1 P1和 P 2 P_2 P2尚未标记,表示两个进程存在死锁
参考资料:《操作系统精髓与设计》