在前面不止一次的提到过死锁。
所谓死锁(Deadlock)
是指多个进程在运行过程中因争夺资源而造成的一种僵局(DeadlyEmbrace),当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
死锁的定义:集合中的每一个进程都在等待只能由本集合中的其他进程才能引发的事件,那么该组进程是死锁的。
也就是说集合中的人需要等待本集合中的其他人来帮忙, 但是,可怕的是所有的人都是这状态。
引起死锁的主要原因是:“需要采用互斥方法访问的、不可以被抢占的资源“。
因为需要互斥,所以就产生了竞争,出现了竞争就会出现等待,但是资源又不可被抢占,所以可能会被别人一直占有,那么就可能无限的等待,这就形成了死锁。
计算机资源可以从两个维度进行划分,重用性以及抢占性。
不管是可重用资源还是消耗性资源,他们都不是可以任意请求的
系统中可重用资源的个数相对来说比较固定,消耗性资源尽管是个数不固定,动态的,但是某瞬间也都是有个数的,所以也不是可以任意请求的。
所以不管是否可重用,只要有竞争访问,就可能出现死锁。
对于不可抢占资源,一旦被请求了,如果不能够释放,那么别人就必须要等待。
可抢占资源即使被分配,仍旧可以被抢占,所以这类资源不会引起死锁。
所以,从资源的角度看,只需要关注是否是可抢占资源,如果不可抢占,那么就有可能出现死锁。
为了直观的分析死锁的情况,可以使用资源分配图
是一种描述资源申请与分配关系的图
使用圆圈表示进程,矩形表示资源;
箭头表示资源的申请与释放,资源->进程表示分配,进程->资源表示资源申请。
如下图所示,表示P1获得了R1在等待R2,P2获得了R2 在等待R1
P1 P2
wait(R1) wait(R2)
wait(R2) wait(R1)
如上所示,进程P1和P2,一个先申请资源R1,一个先申请资源R2,一旦资源R1和R2同时被两个不同的进程获得,将会进入死锁状态。
如果一个结束之后,另一个开始,那么就不会出现死锁。
设有进程P1、P2、P3,有可消耗资源R1、R2、R3
如果如下顺序推进
P1: send(p2, R1); receive(p3, R3);
P2: send(p3, R2); receive(p1, R1);
P3: send(p1, R3); receive(p2, R2);
如下图所示,每个进程都先生产资源给别人,然后才会等待别人的资源,每个人最终都能够获得资源
如果是
P1: receive(p3, R3); send(p2, R1);
P2: receive(p1, R1); send(p3, R2);
P3: receive(p2, R2); send(p1, R3);
所有的人都在等待别人的资源,才会生产消息,形成了死锁。
下图中,横坐标为进程1,纵坐标为进程2
进程1的活动过程有Request(R1) Request(R2) Release(R1) Release(R2)
进程2的活动过程有Request(R2) Request(R1) Release(R2) Release(R1)
显然,图中的阴影区域D,阴影区域的左下角表示进程1申请了资源R1,进程2申请了资源R2,如果此时进程1申请R2或者进程2申请R1或者两者都有,必然会发生死锁
如果避开这个区域,比如一个进程结束后另一个开始,1号曲线或者2号曲线,或者进程1释放了R1后,进程2才开始申请R2就不会进入死锁
通过这种活动顺序图,可以推测出来可能会出现死锁的时空区域。
《计算机操作系统 第四版》 图3-14
前面从资源以及场景的角度分析了死锁,其根本也还是“需要采用互斥方法访问的、不可以被抢占的资源”。
死锁形成有四大必要条件,也就是说如果死锁了必然存在这些。
如果不互斥,大家都可以访问,就没可能死锁;
如果没有请求和保持,比如一次性分配,如果分配不到等待别人使用后释放即可,保持和请求必然会导致“拿走了比人需要的,还等待别人”的场景;
如果可以抢占,即使已经死锁,肯定会被打破;
如果没有循环等待,终究会有一个进程会自己完成,完成后便会释放自己持有的资源,整个系统就会被激活。
所以说,想要处理死锁,或者说避免死锁,关键点就是这几个条件,只要条件被打破,就不会存在继续死锁下去的可能。
从预防-避免-检测-解除,对死锁的防范程度依次减弱,但是对应的资源的利用率依次提高,也就是并发程度依次变高。
预防就像接种疫苗,可能你这辈子都不会接触到病毒。
避免就是在可能出事情的地方,做一些保障处理,比如发现有些场合人员混乱,全是二手烟,那就不进房间了。
检测就好像是定期的体检,没问题继续生活,有什么小问题就去治疗一下。
解除就是真的去看病了。
预防就是事先前的准备,如同疫苗,死锁的预防通常就是增加限制,破坏必要条件。
破坏“请求和保持”
所有的资源必须一次性分配,或者不分配,这样能够保障一个进程要么就等待,要么就可以获得全部的资源,而不会出现保持了资源,然后再去请求的可能。
但是很显然,资源利用率低,并发程度低
比如说有一个任务三个阶段,每个阶段一种资源,每个阶段十分钟,如果一次性分配的话,每个资源都会有二十分钟的闲置,极大地浪费。
这种方案可以进一步优化,分阶段处理,而不是一次性,还是刚才的示例,每个阶段仅仅申请该阶段的资源,使用完毕后,将资源释放,然后再去获取下一阶段的资源
也就是说需要合理的划分阶段,一个完整任务中的一个子任务(也就是一个阶段)一次性分配资源,使用完毕后释放,再去请求,就不存在保持请求了。
破坏“不可抢占”
如果资源是可抢占的,自然就不会死锁,终究会自动解锁,如果能够合理的将不可抢占资源转换为“可抢占”那么就可以预防死锁
当一个进程持有了某些资源后,如果又提出了新的请求,如果该请求并不能满足,那么他必须释放已有的资源,也就可以说是被抢占了
不过这个思路实现复杂,可能会付出很大的代价,比如打印机开始处理了,你现在要切换,肯定不会很容易。
破坏“循环等待”
将资源按照一定的顺序进行申请,可以保证资源的有序性,也就可以破坏循环等待,正是因为资源的顺序很随意,所以才导致很容易死锁
比如所有的进程全部都是R1然后R2,就永运不会死锁
所以可以采取对系统内资源编号的形式,所有的资源申请必须是从小到大的顺序。
如此,就肯定不会循环成环。
但是,号码如何编?到底谁大谁小?要统计下平时资源的申请顺序进行编号
然后如果新增加设备?
另外如果有些进程就是跟系统的排序不同怎么办?
在死锁避免方法中,把系统的状态分为安全状态和不安全状态。
安全状态就是可以找到资源分配的有序序列, 各进程可以顺利推进完成。
不安全状态如果找不到一个合理的资源分配的有序序列,不能保障各进程可以顺利完成,那么就是不安全。
当系统处于安全状态时,可避免发生死锁。反之,当系统处于不安全状态时,则可能进入到死锁状态。
简言之,避免死锁就是在资源分配时,借助于算法对资源分配进行计算评估,相当于风险评估机构。
经典的算法有Dijkstra提出的银行家算法
死锁的检测也是借助于算法进行处理,想要检测死锁
首先,系统中必须能够记录资源的请求和分配记录,其次需要提供一种算法,通过对请求和分配记录进行分析,计算出当前的状态。
如果检测出死锁,那么必须进行解除,常用的解除方式有两种,抢占资源和终止进程,本质都是强行将资源夺回到系统中。
终止进程的话最简单的就是全部终止,将涉及的死锁进程全部都终止掉,显然全部终止就好像将一台工作中的电脑强行重启一样,代价很大
所以还可以逐个终止,直到死锁解除。
本文对操作系统中死锁的概念进行了简单的介绍,不仅仅进程有死锁,线程的运行仍旧也会有死锁。
多线程编程中也会出现死锁,在这些场景中,死锁的概念是相同的---都是同一个集合中的线程都在等待本集合中的线程
对于操作系统对死锁的处理与解决,对于编程中不无借鉴之处,我们应该深刻理解死锁形成的条件,才能够在编程中尽可能的避免死锁。