目录
1 死锁的基本概念
死锁 (Deadlock) 指的是计算机系统中多道程序并发执行时,两个或两个以上的进程由于竞争资源而造成的一种互相等待的现象(僵局),如无外力作用,这些进程将永远不能再向前推进。
下面是一个现实中过桥的例子,如图所示桥比较窄,只能同时通过一辆车,此时有两辆车面对面都想要通过桥,而两辆车互不相让,导致都无法通过,这就是现实中“死锁”的例子。我们可以把桥看作是资源,对于车来说是共享的,当死锁发生时,要解决死锁其中一种解决方法是让其中某辆车后退,现假设让右边的车后退,则它后面的所有车都要后退,若每次都是右边的车让道的话,右边有些车就一直不能通行,就会造成饥饿现象。
死锁现象产生的本质就是供小于求,也就是资源满足不了进程的需求,每一个进程如下的利用资源:
- 申请 (Request): 如果申请不能立即被允许,那么进程必须等待直到能获取资源。(通过系统调用或者信号量来进行资源的申请和释放);
- 使用 (Use): 进程使用资源进行相关操作;
- 释放 (Release) :进程释放资源。
除了对资源的竞争会产生死锁外,P,V操作顺序的不当也会产生死锁,如现有进程 P 0 P_0 P0 和 P 1 P_1 P1,他们都要申请信号量A和信号量B,他们的操作如下表:
时间 | P 0 P_0 P0 | P 1 P_1 P1 |
---|---|---|
t 1 t_1 t1 | wait(A); | wait(B); |
t 2 t_2 t2 | wait(B); | wait(A); |
在 t 1 t_1 t1 时刻, P 0 P_0 P0 和 P 1 P_1 P1 分别申请了信号量A和信号量B,那么在 t 2 t_2 t2时刻由于信号量B已经被 P 1 P_1 P1申请,所以 P 0 P_0 P0申请不到信号量B,同理 P 1 P_1 P1也申请不到信号量A,这时就发生了死锁。
总结来说,产生死锁的原因如下:
- 竞争资源引起死锁:当系统中供多个进程所使用的资源,不足以同时满足它们的需要时,引起它们对资源的竞争而产生死锁;
- 进程推进顺序不当引起死锁:在多道程序系统中,并发执行的进程推进序列不可预测,有些推进顺序,进程可以顺利完成,有的推进顺序会引起进程无限期地等待永远不会发生的条件而不能向前推进,造成死锁。
2 死锁的必要条件
死锁的产生是有必要条件的,当下面四个条件同时出现,死锁将会发生:
- 互斥 (Mutual exclusion):一次只有一个进程可以使用一个资源,因为有些资源的使用是必须独占的;
- 占有并等待 (Hold and wait):一个至少持有一个资源的进程等待获得额外的由其他进程所持有的资源,又叫请求与保持;
- 不可抢占 (No preemption):一个资源只有当持有它的进程完成任务后才自由的释放,也就是非剥夺。若可以抢占的话,别的进程需要某个资源可以直接抢,就不存在死锁问题了;
- 环路等待 (Circular wait):等待资源的进程之间存在环,举个例子来说
进程1
要获得进程2
手中的资源才能执行结束,进程2
要获得进程3
手中的资源才能运行结束,进程3
要获得进程1
手中的资源才能运行结束,这样就形成了一个等待环,谁也无法运行结束离开这个环。
死锁指的是一组进程对一组资源使用时的某种状态,那么描述这个状态有如下参数:
- 资源类型:可以用 R R R 来表示,如 R 1 R_1 R1, R 2 R_2 R2, R 3 R_3 R3 表示CPU 周期,内存空间, I/O 设备;
- 实例:每个资源可能有多个实例,用 W W W 表示;
- 进程:用 P P P 来表示。
此时状态就可以用资源分配图来表示,在数据结构中,图是由一组顶点的集合V和边的集合E组成,在资源分配图中有两种类型的节点:
- P:系统中全部的进程构成的节点;
- R:系统中全部的资源构成的节点。
进程和资源之间的边也存在着两种边:
- 请求边:即某个进程 P i P_i Pi 要请求某个资源 R j R_j Rj 的边,箭头是从进程指向资源;
- 分配边:即某个资源 R j R_j Rj 已经被分配给某个进程 P i P_i Pi 的边,箭头是从资源指向进程。
可以把进程表示为圆点,把资源表示成方形,把资源中的实例表示为更小的方形,如下图所示:
上述两种边就可以表示为如下形式:
有了上面的方法就可以方便的描述进程和资源的分配关系,如某时刻的关系如下图所示:
从资源分配图中,可以得出如下结论:
- 如果图没有环,那么不会有死锁;
- 如果图有环,若每一种资源类型只有一个实例,那么一定会死锁。若每种资源类型有多个实例,可能死锁。
3 死锁预防
处理死锁有忽略、预防、避免、检测、解除五种措施,对于忽略,即假装系统中从未出现过死锁。这个方法被大部分的操作系统采用,包括 UNIX 中的鸵鸟策略。预防死锁,避免死锁则是确保系统永远不会进入死锁状态。死锁检测和解除则是允许系统进入死锁状态,然后恢复系统。
鸵鸟策略:鸵鸟都遇到危险时会将头埋在沙子里,这里比喻像鸵鸟一样对死锁视而不见,因为处理死锁的代价很大,而且常常给用户带来许多不便的限制,大多数用户宁可在极偶然的情况下发生死锁,也不愿限制每个用户只能创建一个进程、只能打开一个文件等等,于是不得不在方便性和正确性之间作出折衷。
3.1 抑制死锁发生的必要条件
对于互斥条件来讲,是不可以打破的,因为在某些条件下,必须互斥访问,详细内容可以参考我的这篇文章 操作系统原理第六章:进程同步。
对于占有并等待条件,要打破这个条件的话必须保证进程申请资源的时候没有占有其他资源,要做到这一点有如下两种方式:
- 要求进程在执行前一次申请全部的资源,这样的话后续就不会再有资源的申请了;
- 没有资源时,可以申请资源。在申请更多其它资源之前,需要释放已有资源。
实际上这两种方法都是代价不小的,比如第一种方法,进程使用资源不是一次性全部使用的,而是有次序的使用的,这样就导致某些资源明明现在用不到,却占有着,而其他着急使用该资源的进程要等待。
对于非抢占式,打破这个条件就是运行抢占,抢占的方式是如果一个进程的申请没有实现,它要释放所有占有的资源,抢占的资源放入进程等待资源列表中,只有进程能够重新得到旧的资源和新申请的资源时,才可以重新开始。
对于环路等待条件,打破这个条件可以将所有的资源类型放入资源列表中,并且要求进程按照资源表申请资源,也就是把每个资源编个号,所有进程对资源的请求必须严格按资源序号递增的次序提出,如已经申请了1号资源CPU和2号资源打印机,那么后面再申请资源必须按次序3、4、5等等。总有一个进程占据了较高序号的资源,它继续请求的资源必然是空闲的,可以一直向前推进。在资源分配图中不可能出现环路,因而摒弃了“环路等待”条件,这种策略可以提高资源利用率,但在进程使用各类资源的顺序与系统规定的顺序不同时会造成资源浪费的情况。
总结来说上述预防死锁的方法通过限制资源请求来打破死锁的四个必要条件之一,从而预防死锁的发生,但可能带来副作用,如降低设备利用率和吞吐量,可能有进程饥饿等。
4 死锁避免
死锁避免不会去限制资源的使用,而是允许进程动态地申请资源,但在系统进行资源分配之前,先计算资源分配的安全性,若此次分配不会导致系统从安全状态向不安全状态转换,便可将资源分配给进程;否则不分配资源,进程必须阻塞等待。
安全状态是指系统的一种状态,在此状态下,系统能按某种顺序(例如 P 1 P_1 P1, P 1 P_1 P1,…, P n P_n Pn)来为各个进程分配其所需资源,直至最大需求,使每个进程都可顺序地一个个地完成。这个序列( P 1 P_1 P1, P 1 P_1 P1,…, P n P_n Pn)称为安全序列。 如果存在一个安全序列系统处于安全态,若某一时刻不存在一个安全序列,则称系统处于不安全状态。
死锁属于不安全状态,是不安全状态的一个子集,它们之间关系如下图所示:
如果一个系统在安全状态,就没有死锁;如果系统死锁,则处于不安全状态;如果一个系统处于不安全状态,就有可能死锁;那么既然要避免死锁,那就要避免系统进入不安全状态。
避免死锁有两种常用方式:
- 当资源有单个实例的时候,可以用之前提到的资源分配图来实现,若图中出现了环,这表示出现了死锁,那么可以检测资源分配图判断是否有环来避免死锁;
- 当资源有多个实例的时候,使用银行家算法,第五节会详细介绍。
4.1 资源分配图法
在第二节也提到过资源分配图,在用资源分配图法避免死锁时,添加了一条新的边叫做需求边,即表示进程 P i P_i Pi 可能会申请到资源 R j R_j Rj,箭头从进程指向资源,但是箭头图形为虚线。当一个进程申请资源的时候,需求边转化为请求边;当资源被进程释放的时候,分配边转化为需求边;当然系统中的资源必须被事先声明。
用资源分配图法来避免死锁的过程如下,当进程
P
i
P_i
Pi 申请资源
R
j
R_j
Rj 时,把图中的所有需求边转换为分配边,看图中是否出现环路,只有不出现环路,才实施资源分配,如下图:
4.2 银行家算法
在现实世界中,银行的利益方式之一是提供贷款,当银行给某人提供贷款时,会考查他是否有能力偿还贷款,以此来判断贷出去的这笔钱是否是安全的。那么银行家算法就是借助这个思想,当某个进程需要分配资源的时候,操作系统会判断这个进程能不能把资源安全的归还,如果可以的话操作系统就分配,否则不分配。
银行家算法适用于多个实例的情况,每一个进程必须事先声明使用的最大量,当一个进程请求资源 , 它可能要等待,当一个进程得到所有的资源,它必须在有限的时间释放它们。
在实现银行家算法时,需要定义如下的参数:
n
为进程的数目,m
为资源类型的数目;Available
:系统中有多少个可用资源,由于银行家算法用在多实例中,则如果available[ j ]=k
,表示资源 R j R_j Rj 有k
个实例有效;Max
:每个进程最多需要请求的资源个数,如果Max[ i, j ]=k
,那么进程 P i P_i Pi 可以最多请求资源 R j R_j Rj 的k
个实例;Allocation
:每个进程已经分配了多少资源,如果Allocation[ i, j ]=k
,那么进程 P i P_i Pi 当前分配了k
个资源 R j R_j Rj 的实例;Need
:每个进程还需要多少资源,如果Need[ i, j ]=k
,那么进程 P i P_i Pi 还需要k
个资源 R j R_j Rj 的实例。上面几个参数也存在这样的关系:Need [ i, j ] = Max [ i, j ] – Allocation [ i, j ]
,表示还需要的等于最多需要的减去已经拥有的;Request
:进程当前要申请多少资源,如果Request[ i, j ]=k
,那么进程 P i P_i Pi 现在想申请k
个资源 R j R_j Rj 的实例。
银行家算法具体的实现过程如下:
- 若
Request <= Need
,执行下一步,否则不执行,意思是进程当前想申请的资源必须是小于它实际所需要的,不能超出实际所需要的; - 若
Request <= Available
,执行下一步,意思是当前申请的资源数量,系统是否可以满足,若满足不了,则需要等待,直到系统可以满足为止; - 试着去给该进程分配资源,会执行如下操作
Available=Available-Request
,Allocation=Allocation+Request
,Need=Need-Request
; - 用安全算法检查,看系统是否处于安全状态,如果安全就分配资源,否则不分配并让之前执行的三个操作回到原来状态。
在安全算法中,有两个参数:
Work
:Work=Available
表示当前系统中可提供资源的数量;Finish
:它为一个布尔变量,描述进程是否执行结束,如Finish[ i ]=false
表示进程 P i P_i Pi 还没有执行结束或还没有执行。
在进程序列中,若进程 P i P_i Pi 满足如下条件:
Finish[ i ]=false
,这个进程还没执行完;Need <= Work
,进程 P i P_i Pi 所需的资源数量是小于系统可用资源数量的,换句话说就是系统是可以满足该进程的需求的。
则执行 Work=Work+Allocation
,Finish[ i ]=true
,这两句的意思是进程
P
i
P_i
Pi 执行完后把它的 Allocation
给释放掉,然后标明 Finish
为已经执行完。然后按照该步骤依次检查所有的进程,如果最后所有进程的 Finish
都为 true
则代表所有进程都能顺利结束,那么说明系统为安全状态。
银行家算法举例:现假设有五个进程 P 0 P_0 P0 ~ P 4 P_4 P4,三个资源类型A(10个实例),B(5个实例),C(7个实例),下表是在 T 0 T_0 T0 时刻系统的状态:
进程 | Allocation (已分配) | Max (总需求) | Available (当前可分配) |
---|---|---|---|
A B C | A B C | A B C | |
P 0 P_0 P0 | 0 1 0 | 7 5 3 | 3 3 2 |
P 1 P_1 P1 | 2 0 0 | 3 2 2 | |
P 2 P_2 P2 | 3 0 2 | 9 0 2 | |
P 3 P_3 P3 | 2 1 1 | 2 2 2 | |
P 4 P_4 P4 | 0 0 2 | 4 3 3 |
要判断当前是否处于安全状态,要计算 Need
,如下表:
进程 | Need (还需的资源数)=Max-Allocation |
---|---|
A B C | |
P 0 P_0 P0 | 7 4 3 |
P 1 P_1 P1 | 1 2 2 |
P 2 P_2 P2 | 6 0 0 |
P 3 P_3 P3 | 0 1 1 |
P 4 P_4 P4 | 4 3 1 |
此时使用安全算法来验证是否有安全序列,初始条件下 Work = available = (3,3,2)
,Finish[ i ]=false (i=0..4)
:
进程 | Work | Need | Allocation | Finish |
---|---|---|---|---|
A B C | A B C | A B C | T/F | |
P 1 P_1 P1 | 3 3 2 | 1 2 2 | 2 0 0 | T |
P 3 P_3 P3 | 5 3 2 | 0 1 1 | 2 1 1 | T |
P 4 P_4 P4 | 7 4 3 | 4 3 1 | 0 0 2 | T |
P 2 P_2 P2 | 7 4 5 | 6 0 0 | 3 0 2 | T |
P 0 P_0 P0 | 10 4 7 | 7 4 3 | 0 1 0 | T |
所以存在安全序列( P 1 P_1 P1, P 3 P_3 P3, P 4 P_4 P4, P 2 P_2 P2, P 0 P_0 P0),系统处于安全状态。
注意:安全序列有时不是唯一的,但只要找到一个,就认为系统是安全的。
5 死锁的检测
死锁的检测分为两种情况,一种是每一种资源类型只有一个实例,另一种是一个资源类型的多个实例。
5.1 每一种资源类型只有一个实例
通过把资源分配图转换成维护等待图,来看维护等待图中是否出现了环来看当前系统是否出现了死锁。在维护等待图中只有一种节点就是进程,若进程
P
i
P_i
Pi 指向进程
P
j
P_j
Pj,则表示
P
i
P_i
Pi 在等待
P
j
P_j
Pj,如下图左边为资源分配图,右边为转换的维护等待图:
可以发现维护等待图中是存在环的,所以表明当前系统出现死锁了。
5.2 一个资源类型的多个实例
这里用到的算法叫做死锁检测算法,类似于之前讲过的安全算法,死锁检测算法定义了如下参数:
Available
:每种资源可用的资源数量;Allocation
:每个进程已经分配的资源数量;Request
:进程请求的资源数量。Work
:Work = Available
;Finish
:这里的Finish
和之前的安全算法有所不同,这里当Allocation != 0
则Finish = false
否则Finish = true
,也就是若某进程的Allocation = 0
时说明这个进程没有被分配到资源,也就这个进程是不参与死锁的,所以Finish = true
。
找到某进程满足如下条件:
Finish = false
;Request <= Work
:当前请求的资源数量小于系统可分配资源的数量;
该进程能执行结束,执行结束后执行 Work = Work + Allocation
, Finish = true
,也就是把资源给释放掉,把所有的进程按如上操作遍历结束后检查会不会有一个进程的 Finish = false
,也就是某个进程无法结束,那么就发生了死锁,并且可以根据 Finish
的下标找到是哪一个进程发生了死锁。
检测死锁算法的例子:现假设有五个进程 P 0 P_0 P0 ~ P 4 P_4 P4,三个资源类型A(7个实例),B(2个实例),C(6个实例),下表是在 T 0 T_0 T0 时刻系统的状态:
进程 | Allocation (已分配) | Request (当前请求) | Available (当前可分配) |
---|---|---|---|
A B C | A B C | A B C | |
P 0 P_0 P0 | 0 1 0 | 0 0 0 | 0 0 0 |
P 1 P_1 P1 | 2 0 0 | 2 0 2 | |
P 2 P_2 P2 | 3 0 3 | 0 0 0 | |
P 3 P_3 P3 | 2 1 1 | 1 0 0 | |
P 4 P_4 P4 | 0 0 2 | 0 0 2 |
所以存在安全序列( P 0 P_0 P0, P 2 P_2 P2, P 3 P_3 P3, P 1 P_1 P1, P 4 P_4 P4),当前是没有死锁的。
现在假设进程 P 2 P_2 P2 请求 C 的一个实例,如下表:
进程 | Request (当前请求) |
---|---|
A B C | |
P 0 P_0 P0 | 0 0 0 |
P 1 P_1 P1 | 2 0 1 |
P 2 P_2 P2 | 0 0 1 |
P 3 P_3 P3 | 1 0 0 |
P 4 P_4 P4 | 0 0 2 |
此时可以归还 P 0 P_0 P0 所有的资源,但是资源不够完成其他进程的请求,所以死锁存在,包括进程 P 1 P_1 P1, P 2 P_2 P2, P 3 P_3 P3, P 4 P_4 P4。
5.3 处理死锁
当系统出现死锁,通常有如下三种方式:
- 操作员人工处理
- 进程终止
- 资源抢占
其中进程终止和资源抢占是由操作系统来完成的,进程终止的方法如下:
- 终止所有的死锁进程;
- 一次终止一个进程直到死锁环消失;
- 选择终止顺序,比如按照进程的优先级来终止,先终止优先级低的进程,或者按照进程计算了多少时间,还需要多长时间来选择终止。
资源抢占的方法如下:
- 选择一个牺牲品:最小化代价;
- 回退:返回到安全的状态,然后重新开始进程;
但是会造成饥饿,因为同一个进程可能总是被选中,包括在回退时。