操作系统(13)-操作系统中的死锁及其预防、避免、检测与解除

1 死锁的基本概念

死锁的定义:一组进程中,每个进程都无限等待被该组进程中另一进程所占有的资源,因而永远无法得到的资源,这种现象称为进程死锁,这一组进程就称为死锁进程。如果死锁发生,会浪费大量系统资源,甚至导致系统崩溃

从死锁的定义中可以得到几个推论:参与死锁的所有进程都在等待资源;参与死锁的进程是当前系统中所有进程的子集。

资源数量有限、锁和信号量错误使用都可能导致死锁,锁和信号量的使用可能是开发人员编程导致的,下面着重介绍一下有限资源导致的死锁问题。在操作系统中资源使用的一般模式是:进程提出申请,操作系统进行相应的分配,如果进程所需要的资源不能满足的话,这个进程就进入阻塞或者等待状态,如果可以满足的话进程就直接使用资源,当进程使用完毕之后就释放资源。由于资源的使用方式就是申请-分配-使用-释放,因此有些进程得不到资源就会处理阻塞等待状态,资源有限的话就有可能出现死锁。

从死锁的角度可以把资源分为两类:一类是可重用资源,即可被多个进程多次使用。可重用资源包括处理器、I/O部件、内存、文件、数据库、信号量等,可重用资源又可以进一步划分为可抢占资源与不可抢占资源,例如cpu就是可抢占资源,进程在cpu上运行的时候允许比它优先级更高的进程抢占cpu,还有一些资源例如打印机是不可抢占的;另一类资源是可消耗资源,即只可使用一次、可创建和销毁的资源,包括信号、中断、消息等。下面讨论的死锁主要针对于可重用资源。

活锁和饥饿
活锁的情况是先对资源进行加锁,获得另外一个资源的锁时需要轮询,情况就是每个进程可以上cpu执行,cpu时间片执行完了之后又下cpu,进程既没有进展也不产生阻塞,这种现象就是活锁,而死锁是进程进入等待状态无法获取cpu的执行权。饥饿往往是由于资源分配的策略所决定等待,某个进程一直得不到cpu的使用权,就会造成饥饿。

产生死锁的必要条件
(1)互斥使用(资源独占):一个资源每次只能给一个进程使用
(2)占有且等待(请求和保持,部分分配):进程在申请新的资源的同时保持对原有资源的占有
(3)不可抢占(不可剥夺):资源申请者不能强行的从资源占有者手中夺取资源,资源只能由占有者自愿释放
(4)循环等待:存在一个进程等待队列 {P1 , P2 , … , Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路。
当死锁产生的时候一定会有这四个条件,有一个条件不成立都不会造成死锁。

2 资源分配图(RAG)

用有向图描述系统资源和进程的状态,从图的角度为解决死锁提供理论和依据。定义一个二元组G=(V,E)有两个集合组成。其中V是结点的集合,分为P(进程),R(资源)两部分,P = {P1, P2, … , Pn},R = {R1, R2, … , Rm}。E是有向边的集合,其元素为有序二元组,可以是进程节点指向资源节点的有向边后者资源节点指向进程节点的有序边。

资源分配图图画法说明
系统由若干类资源构成,一类资源称为一个资源类;每个资源类中包含若干个同种资源,称为资源实例。使用用方框表示资源类;用方框中的黑圆点表示资源实例;用圆圈中加进程名表示进程。分配边是资源实例指向的进程有向边,而申请边是进程指向资源实例的有向边。
 由资源分配图得到的死锁定理:如果资源分配图中没有环路,则系统中没有死锁,如果图中存在环路则系统中可能存在死锁;如果每个资源类中只包含一个资源实例,则环路是死锁存在的充分必要条件。
 
可以对资源分配图进行化简来查看是否有死锁产生,简化步骤:(1)找一个非孤立、且只有分配边的进程结点去掉分配边,将其变为孤立结点;(2)再把相应的资源分配给一个等待该资源的进程即将该进程的申请边变为分配边。反复进行(1)(2)直到找不到满足非孤立并且只有分配边的进程节点,简化之后如果系统中的进程节点都是孤立的,那么系统中既没有死锁发生;否则系统中一定有环路存在。

3 处理死锁的方法

目前处理死锁的方法可以归纳为如下:
(1)不考虑此问题(鸵鸟算法):把头埋在沙子里,假装根本没发生问题。因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。大多数操作系统,包括 Unix,Linux 和 Windows,处理死锁问题的办法仅仅是忽略它。

(2)不让死锁发生,分为以下两种:

  • 死锁预防:不让死锁发生的静态策略,通过设计合适的资源分配算法,由资源分配策略保证不让死锁发生
  • 死锁避免:不让死锁发生的动态策略,以不让死锁发生为目标,跟踪并评估资源分配过程,根据评估结果决策是否分配

(3)让死锁发生
通过死锁的检测判断死锁是否真的发生,然后采用一些方法来解除死锁的问题。

总体解决锁死发生的方法可以分为四类:鸵鸟算法、死锁预防、死锁避免以及死锁检测与解除。

3.1 死锁预防

在设计系统时,通过确定资源分配算法,排除发生死锁的可能性。具体做法是防止产生死锁的四个必要条件中任何一个条件发生即破坏产生死锁的四个条件之一。

(1)破坏“互斥使用/资源独占”条件
资源本身的特性是独占的,是排他性使用的,所以要使用一种资源转换技术,把独占资源变为共享资源。例如针对于打印机,SPOOLing技术的引入解决不允许任何进程直接占有打印机的问题。设计一个“守护进程/线程”负责管理打印机,进程需要打印时,将请求发给该daemon,由它完成打印任务。

(2)破坏“占有且等待”条件
指一个进程占有了一部分资源,在申请其他资源的时候由于得不到满足而进入等待状态。有下面两种方案实现:

  • 实现方案1:要求每个进程在运行前必须一次性申请它所要求的所有资源,且仅当该进程所要资源均可满足时才给予一次性分配。这种实现会使得资源的利用率很低,当一个进程所需要的资源不能同时满足的情况下可能一直处于等待状态,会产生饥饿现象。
  • 实现方案2:在允许进程动态申请资源前提下规定,一个进程在申请新的资源不能立即得到满足而变为等待状态之前,必须释放已占有的全部资源,若需要再重新申请。

(3)破坏“不可抢占”条件
实现方案是当一个进程申请的资源被其他进程占用时,可以通过操作系统抢占这一资源(两个进程优先级不同)。这种方法具有局限性,适用于状态易于保存和恢复的资源,如CPU、内存资源。

(4)破坏“循环等待”条件
主要思想是通过定义资源类型的线性顺序实现,实现方案是资源有序分配法,把系统中所有资源编号,进程在申请资源时必须严格按资源编号的递增次序进行,否则操作系统不予分配。实现资源的有序分配时需要考虑如何对资源进行编号,通常可以利用资源使用的频繁性进行排序。

假如有P1,P2…Pn共n个进程,每个进程都需要一定的资源,一定可以找到某个进程所需要申请的资源的序号是最大的这个进程,从这个进程开始执行;这个进程执行完之后再继续找下一个所需要资源序号最大的进程,以此类推,因此使用资源的有序分配法一定可以解决死锁问题。

3.2 死锁避免

通过一个例子来讨论死锁避免的设计思想,如下图(a)有两个进程P和Q,系统中一共有M个资源,假设A为进程P对资源需求的最大量,B是进程Q对资源需求的最大量,如果P进程对资源的占用量超过X则Q进程无法运行,如果Q进程对资源的占用量超过Y,则P进程无法运行。
(b)中X1为进程P的资源占用量,Y1是Q进程的占有量,那么剩余的资源能满足P或Q进程对资源的需求量;©中X2为进程P的资源占用量,Y1是Q进程的占有量,那么剩余的资源可以满足P进程对资源的最大需求量,等P进程释放资源后Q进程就可以继续执行,这时对资源的分配就是有条件的;(d)中剩余资源既不能满足P进程也不能满足Q进程对资源的最大需求量。

 
根据以上分析,在进程执行过程中,提出资源申请时,操作系统应该根据当时系统所处的状态或者把资源分配给该进程后进入的一个新的状态来调整资源分配策略。具体分析如下:
OXO3Y区域,P进程Q进程提出申请资源时操作系统都可以满足;XAO2O3区域内,如果P进程提出申请可以无条件满足,Q进程提出申请总数只要不超过Y就可以满足;同时操作系统应该控制不要让资源的申请分配出现在O1O2O3区域,否则系统剩余的资源数量既不能满足P进程也不能满足Q进程,会导致死锁。
 

通过上面的例子可以给出死锁避免的设计思想:在系统运行过程中,对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,若分配后系统发生死锁或可能发生死锁,则不予分配,否则予以分配,即在资源动态分配过程中,防止系统进入不安全状态,以避免死锁的发生

安全状态:如果系统中存在一个由所有进程构成的安全序列P1,…,Pn,则称系统处于安全状态。
安全序列:一个进程序列{P1,…,Pn}是安全的,如果对于每一个进程Pi(1≤i≤n),它以后还需要的资源量不超过系统当前剩余资源量与所有进程Pj (j < i )当前占有资源量之和,则称系统处于安全状态。
注意:安全状态一定没有死锁发生
不安全状态:系统中不存在一个安全序列。不安全状态一定导致死锁,但是可能目前还没有进入死锁,死锁是不安全状态的一个子集。

利用银行家算法避免死锁
银行家算法是仿照银行发放贷款时采取的控制方式而设计的一种死锁避免算法,起这样的名字是因为该算法原本是为银行系统设计的,以确保银行在发放贷款时,不会发生不满足客户需要的情况,在操作系统中也可以用它来避免死锁。

为实现银行家算法,每一个进程进入系统时,他它须申明在运行过程中,可能需要每种资源类型的最大数目,其数目不能超过系统所拥有的资源总量。当进程请求一组资源时,系统必须首先确定是否有足够的资源分配给该进程。若有,再进一步计算在将这资源分配给进程后,是否会使系统处于不安全状态。如果不会,才将资源分配给它,否则让进程等待。

应用条件如下:

  • 在固定数量的进程中共享数量固定的资源
  • 每个进程预先指定完成工作所需的最大资源数量
  • 进程不能申请比系统中可用资源总数还多的资源
  • 进程等待资源的时间是有限的
  • 如果系统满足了进程对资源的最大需求,那么,进程应该在有限的时间内使用资源,然后归还给系统

3.3 死锁检测与解除

死锁检测与解除:允许死锁发生,但是操作系统会不断监视系统进展情况,判断死锁是否真的发生;一旦死锁发生则采取专门的措施,解除死锁并以最小的代价恢复操作系统运行。

检测死锁是否发生有三个典型的检测时机:(1)当进程由于资源请求不满足而等待时检测死锁,缺点是系统开销大;(2)定时检测;(3)系统资源利用率下降时检测死锁。

一个简单的死锁检测算法
给每个进程、每个资源指定唯一编号;设置一张资源分配表记录各进程与其占用资源之间的关系;设置一张进程等待表记录各进程与要申请资源之间的关系。从资源等待表出发,看有没有形成等待的环路。即可以利用资源分配图的思想来检测是否有死锁发生。

死锁避免
死锁避免的重要是以最小的代价恢复系统的运行,具体的死锁解除的方法有几个:(1)撤消所有死锁进程,代价较大;(2)进程回退(Roll back)再启动,进程在执行过程中,系统会为进程记录一些中间节点,当出现死锁的时候进行回退再一起重新运行,由于需要记录每个进程的现场,所以系统代价也比较大;(3)按照某种原则逐一撤消死锁进程,直到没有死锁;(4)按照某种原则逐一抢占资源(资源被抢占的进程必须回退到之前的对应状态),直到没有死锁。

4 哲学家就餐问题

下面以哲学家就餐问题来总结归纳解决死锁问题的各种方法。哲学家就餐问题是一个经典的进程之间同步互斥问题,问题描述为:有五个哲学家围坐在一圆桌旁,桌中央有一盘通心粉,每人面前有一只空盘子,每两人之间放一只筷子;每个哲学家的行为是思考,感到饥饿,然后吃通心粉;为了吃通心粉,每个哲学家必须拿到两只筷子,并且每个人只能直接从自己的左边或右边去取筷子。在解决哲学家就餐问题的时候,要考虑到筷子的互斥使用、不能出现死锁现象。

哲学家就餐问题的问题模型是:应用程序中并发线程执行时,协调处理共享资源。

下面是一种错误的解法,考虑到如果所有哲学家同时拿起左手边的筷子,那么就无法拿起右手边的筷子,造成死锁。

void philosopher(int i) {
    while(TRUE) {
        think();
        take(i);       // 拿起左边的筷子
        take((i+1)%N); // 拿起右边的筷子
        eat();
        put(i);
        put((i+1)%N);
    }
}

为了防止死锁的发生,可以设置两个条件:必须同时拿起左右两根筷子;只有在两个邻居都没有进餐的情况下才允许进餐。

#define LEFT (i + N - 1) % N // 左邻居
#define RIGHT (i + 1) % N    // 右邻居
#define THINKING 0
#define HUNGRY   1
#define EATING   2
typedef int semaphore;
int state[N];                // 跟踪每个哲学家的状态
semaphore mutex = 1;         // 临界区的互斥
semaphore s[N];              // 每个哲学家一个信号量

void philosopher(int i) {
    while(TRUE) {
        think();
        take_two(i);
        eat();
        put_two(i);
    }
}

void take_two(int i) {
    down(&mutex);
    state[i] = HUNGRY;
    test(i);
    up(&mutex);
    down(&s[i]);
}

void put_two(i) {
    down(&mutex);
    state[i] = THINKING;
    test(LEFT);
    test(RIGHT);
    up(&mutex);
}

void test(i) {         // 尝试拿起两把筷子
    if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] !=EATING) {
        state[i] = EATING;
        up(&s[i]);
    }
}

六、其他问题

1、两阶段加锁

这是针对数据库的一种方法,第一阶段对所有需要更新的记录进行加锁,一旦某个记录已经被加锁,就释放之前的锁,从头进行重试。只有当第一阶段所有获取锁的行为都成功,才进行第二阶段的更新,否则放弃所有的锁。

2、通信死锁

这种情况其实是很常见的。当一个进程A向B发送信息后挂起,需要B进程的回复唤醒时,如果请求信息丢失,A就会被阻塞以等待回复,B会阻塞等待一个向其发送命令的请求,因而发生死锁。

通信死锁不涉及资源,不能通过合理调度资源来避免。一般通信协议会解决这种问题,包括超时重传等技术。

3、活锁

轮询(忙等待)的方式,在有时是有效的,因为挂起进程等待的开销很大。考虑如下程序:

 

enter_region()通过轮询获取资源,假设A获得了资源1,B获得了资源2,那么两个进程都不会阻塞,而是不停地进行轮询以获取资源。两个进程总是运行完系统分配的时间片,没有阻塞但是不会取得进展。

4、饥饿

饥饿的概念,其实与死锁和活锁差别比较大。考虑进程调度,基于优先级的调度中,如果总是有高优先级的进程就绪,那么一个低优先级的进程可能长时间无法上CPU运行。这就是饥饿现象,可以考虑通过动态优先级机制,可以动态提高长时间得不到运行的进程的优先级,从而使它可以运行。


参考资料:

1、 视频资料:[操作系统原理(北京大学)][Link1];
2、 书籍:《计算机操作系统》第四版汤小丹;《现代操作系统》第四版陈向群译;
3、 参考博客:CS-Notes/操作系统
4、 https://www.cnblogs.com/lustar/p/8087496.html

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: