一、死锁现象的出现
死锁现象的出现是因为不同进程之间在推进的过程中会出现资源相互依赖的情况,即进程P0依赖于P1占有的资源,需要等待P1执行完成释放资源,而同时P1由依赖于P0所占用的资源,这种情况下P0和P1都无法继续执行下去,出现了死锁的情况。
以上面例子来说明,当缓存区已满后,生产者想要把一个item放入缓存区,于是申请mutex互斥信号量,即执行P(mutex),当继续申请empty信号量时,发现缓存区已满,那么生产者进程进入阻塞状态,等待消费者进程执行V(empty)操作;接下来消费者进程开始执行,申请mutex互斥量的时候发现该信号量已经被占用,不得不停下来等待。
这就陷入了一个僵局,双方都在等待对方进程执行以获得必要的资源,进入了死锁状态。就像下图所示的火车一样,形成环形等待。
二、死锁出现的条件
死锁的出现是有条件的,还是以上面的火车为例,如果一条道路可以通行两列火车,或者火车可以沿着铁路倒退,火车在申请下一条路不成功时不占用当前的道路,没有形成环形路等的情况下,都不会出现死锁现象,因此我们可以抽象出死锁出现的条件:
(1)互斥:资源不能够被共享,同一时间只要由一个进程使用;
(2)不可剥夺:进程已经获得的资源,在进程未完成之前不可以被剥夺;
(3)请求与保持:进程因请求资源而阻塞时,对已获得的资源保持不放;
(4)循环等待:若干进程形成头尾相接的循环资源等待关系。
三、死锁处理的方法
1.死锁预防
要避免死锁的发生,关键就在于要破坏死锁存在的条件,一般来说,互斥和不可剥夺这两个条件是很难破坏的,因此,死锁预防主要是针对请求与保持、循环等待这两个条件而言。
如何破坏请求与等待条件了?我们可以一次性申请所有的资源,如果有资源是不可获得的,那么该进程就会在不获得任何资源的情况下进入了阻塞状态。以上面的火车为例,如果火车不能申请到它要通过的铁路的使用权,就不会驶入交叉路口,就不会出现被堵塞的情况。
那么又该如何破坏循环等待这个条件了?如下是一个资源分配图例。
C为需要使用资源的进程,而R就是资源。我们给资源和进程按1~4进行编号,之后我们让进程按编号的顺序大小来进行资源申请。比如先让C4进行资源申请,按从小到大的顺序,C4先申请R1再申请R4,而此时R1已经被C1占有,所以C4就会阻塞,而此时C4还没申请R4,这样就不会出现R3申请不到R4的情况,不会出现循环等待。
上述死锁预防的方法的确可以避免死锁情况的出现,但它会造成很大的资源浪费,很多申请的资源可能会在很久以后才会被用上,而计算进程进行所要使用的资源也是很难实现的。
2.死锁避免
出于死锁预防所存在的缺点,我们考虑采用一种死锁避免的方法,而不去破坏死锁产生所需要的条件,同样达到解决死锁问题的目的。
在每次申请资源之前,我们都预先判断一下是否会有死锁的风险,如果有的话,我们就驳回这次申请。那么关键问题就是设计一个算法,能够对死锁的风险进行预测,其中最经典的就是银行家算法。
在银行中,客户会向银行申请贷款,在贷款时会申明所要贷款的最大金额,之后会进行分期贷款,且总的贷款额度不会大于最大金额,而只有当客户拿到所有需要的资金,才能够完成项目,并向银行返还贷款。银行在考虑贷款问题的时候,要保证银行有足够的放贷款,每个贷款人能拿到需要的贷款,最终能够向银行偿还款项,这样银行才能正常运营。
银行家算法的核心就是要确定系统是否是安全的,即判断客户最后能不能偿还所有贷款。对所有资源的申请都存在一种调度方案让他满足,会形成一个能够安全执行的序列P0、P1……
在遇到一个资源请求时,首先假设允许此次申请,如果发现操作系统上仍可以找到安全序列,那么就可以真正地为这次请求分配资源了,否则这次申请就会被拒绝。
3.死锁检测/恢复、死锁忽略
死锁检测即随意地让进程使用资源,但会定期地检测哪些进程锁死了,并通过回滚(rollback)等方法让它解决死锁。虽然检测算法的复杂度不比银行家算法低,但检测算法是定时检测的,使用的频率不那么高,但问题在于如何让进程恢复,这是很不容易实现的。
而死锁忽略就是忽略死锁的情况,因为在很多情况下,死锁都没必要刻意去处理,往往通过重启就可以达到解除死锁的目的,比如PC,往往就是通过重启PC实现死锁的恢复。