死锁
前言
这一章与进程同步相比较较为轻松,但也是很重要。死锁避免的算法会经常考到。
系统模型
死锁(deadlock): 在多道程序环境下,多个进程可能竞争一定数量的资源。某个进程申请资源,如果这时资源不可用,那么该进程进入等待状态。如果申请的资源被其他等待进程所占有,那么该等待进程有可能再也无法改变其状态,称为死锁。
资源: 对于操作系统,都拥有一定数量的资源,包括内存空间、CPU周期、文件、I/O设备等都属于资源。对于每个相同类型的资源,有一定数量的实例。比如,如果系统有八个CPU,那么CPU资源类型就有8个实例。
进程申请资源的顺序:
- 申请: 如果申请不能被立即允许,那么进程等待,直到它获得该资源为止。
- 使用: 进程对资源进行操作。
- 释放: 进程释放资源。
死锁特征
产生死锁的四个必要条件
- 互斥: 至少有一个资源必须处于非共享模式。
- 占有并等待: 一个进程必须占有至少一个资源,并等待另一资源,而该资源为其它进程所占有。
- 非抢占: 资源不能被抢占,即资源只能在进程完成任务后自动释放。
- 循环等待: 对于一组进程{ P 0 P_0 P0、 P 1 P_1 P1、 P 2 P_2 P2… P n P_n Pn}, P 0 P_0 P0所等待的资源为 P 1 P_1 P1所占有, P 1 P_1 P1所等待的资源为 P 2 P_2 P2所占有, P n − 1 P_{n-1} Pn−1所等待的资源为 P n P_n Pn所占有, P n P_n Pn所等待的资源为 P 0 P_0 P0所占有。
资源分配图
死锁问题可用称为系统资源分配图的有向图进行精确地描述。
下面观察几个资源分配图实例:
- 图一: 不存在死锁。
- 图二: 存在死锁,进程 P 1 P_1 P1、 P 2 P_2 P2、 P 3 P_3 P3为死锁。
- 图三: 不存在死锁。
存在死锁的充分必要条件: 如果每个资源类型都只有一个实例,那么有环就意味着存在死锁。
存在死锁的必要条件: 图中存在环(如果资源分配图中没有环,那么就不存在死锁)。
死锁处理方法
从原理上来说,有三种办法可以处理死锁问题:
- 可以使用协议预防死锁或避免死锁,确保系统不会进入死锁状态。
- 可允许系统进入死锁状态,然后进行死锁检测,再进行死锁恢复。
- 忽视这个问题,认为死锁不可能在系统内发生。
死锁预防: 因为产生死锁需要四个必要条件,只要使任一条件不成立,死锁就不会发生,称为死锁预防。
死锁避免: 要求操作系统事先得到有关进程申请资源和使用资源的额外信息。有了这些信息,系统可以确定,对于一个进程的资源申请,进程是否应该等待。所以,系统必须考虑现有可用资源、已分配给每个进程的资源、每个进程将来申请和释放的资源。
死锁检测: 提供一个算法来检查系统状态以确定死锁是否发生。
死锁恢复: 提供一个算法使操作系统从死锁中恢复。
死锁忽略: 未检查死锁会导致系统性能下降,因为资源被不能运行的进程所占有,越来越多的进程会因为申请资源进入思索。最后,整个系统会停止工作,并且需要进行人工启动(如果死锁很少发生,比如一年一次,那么死锁忽略就是可行的,因为这种方法更为经济)。
1. 死锁预防
死锁预防便是打破形成死锁的四个必要条件之一,有四种策略:
互斥
非共享资源必须互斥,共享资源不要求互斥所以不会死锁。无法从互斥条件下手避免死锁。
占有并等待
- 可以是进程循序拥有不等待,即要求一个进程在执行前获得所有资源。
- 等待不拥有,进程在申请其他资源的时候必须释放已分配的资源。
缺点:
- 资源利用率低。比如一个进程需要三个资源,它必须一开始就申请。如果需要100个时间单元,但其中一个资源可能只需要被占有10个时间单元,那么剩余的90个时间单元就被浪费了。
- 可能会产生饥饿。如果一个进程需要多个资源,可能会永久等待。
非抢占
解决方法(抢占):如果一个进程占有一些资源并在申请一些无法立刻分配到的资源,那么它占有的资源就可以被抢占。
循环等待
确保此条件不成立的条件是对所有资源类型进行完全排序,且要求每个进程按递增顺序来申请资源。举个例子,比如有1,2,3,4四种类型的资源。不管进程使用资源的顺序如何,都必须按编号从小到大申请。
2. 死锁避免
死锁避免原理: 死锁避免算法动态地检测资源分配状态以确保循环等待条件不可能成立。资源分配状态是由可用资源和已分配资源,以及最大需求决定的。
安全序列: 如果系统能按某个殊勋为每个进程分配资源并能避免死锁,那么系统状态就是安全的,对于进程们,就存在一个安全序列。反之就是不安全序列。
如上图,安全状态一定不会导致死锁。不安全状态可能会导致死锁,导致死锁的一定处于不安全状态。
资源分配图算法(适用于资源单实例)
银行家算法引入了一个新的边——需求边
需求边: 需求边用虚线表示,表示进程 P i P_i Pi可能在将来某个时候申请资源 R j R_j Rj。
我们通过两种方式看上图。
- 首先忽视蓝色的边,那么资源分配图不构成环,即目前系统处于安全状态。
- 这次我们假定进程 P 1 P_1 P1成功申请 R 3 R_3 R3的资源,红色的边消失,换成蓝色的边,此时上图构成环(需求边也算是边)。系统处于不安全状态,可能出现死锁。
所以资源分配图算法就是,如果没有环存在,那么资源分配会使系统处于安全状态。如果有环存在,那么资源分配会使系统处于不安全状态。
银行家算法(适用于资源多实例)
对于多实例资源分配,资源分配图算法不再适用。我们可以使用银行家算法,但是银行家算法要比资源分配图方案低。
银行家算法的一些准备
前提和原理:
- 使用银行家算法,当新进程进入系统时,它必须说明其可能需要的每种类型资源实例的最大数量,这一数量不能超过系统资源的总和。
- 当用户申请一组资源时,系统必须确定这些资源的分配是否会使系统处于安全状态。如果是,可以分配资源。否则,进程必须等待直到某个其他进程释放足够资源为止。
算法需要的数据结构:
- 设n为系统进程的个数,m为资源类型的种类。还需要以下数据结构:
- Available: 长度为m的向量,表示每种资源的现有实例的数量。如果 A v a i l a b l e [ j ] = k Available[j] = k Available[j]=k,那么资源类型 R i R_i Ri现有k个实例。
- Max: n ∗ m n*m n∗m矩阵,定义每个进程的最大需求。如果 M a x [ i ] [ j ] = k Max[i][j]=k Max[i][j]=k,那么进程 P i P_i Pi最多可申请 k k k个资源类型 R j R_j Rj的实例。
- Allocation: n ∗ m n*m n∗m矩阵,定义每个进程现在所分配的各种资源类型的实例数量。如果 A l l o c a t i o n [ i ] [ j ] = k Allocation[i][j] = k Allocation[i][j]=k,那么进程 P i P_i Pi现在已分配了 k k k个资源类型 R j R_j Rj的实例。
- Need: n ∗ m n*m n∗m矩阵,表示每个进程还需要的剩余的资源。如果 N e e d [ i ] [ j ] = k Need[i][j] = k Need[i][j]=k,那么进程 P i P_i Pi还可能申请 k k k个资源类型 R j R_j Rj的实例。其中, N e e d [ i ] [ j ] = M a x [ i ] [ j ] − A l l o c a t i o n [ i ] [ j ] Need[i][j] = Max[i][j] - Allocation[i][j] Need[i][j]=Max[i][j]−Allocation[i][j]。
- 向量的简要表示。大写字母X、Y等都可表示向量。
- Request n ∗ m n*m n∗m矩阵,表示每个进程想要申请的资源。如果 R e q u e s t [ i ] [ j ] = k Request[i][j] = k Request[i][j]=k,那么进程 P i P_i Pi要申请 k k k个资源类型 R j R_j Rj的实例。
- Work为长度为m的向量,表示目前可用的资源。
- Finish为长度为n的布尔类型向量, F i n i s h i Finish_i Finishi表示进程 P i P_i Pi是否已经申请到所有资源并执行完成。
安全性算法
安全性算法确定计算机系统是否处于安全状态
资源请求算法
资源请求算法确定进程 P i P_i Pi是否可以获得所请求的资源
3. 死锁检测
死锁检测和死锁避免采用的算法十分相似,以至于我一开始都没分出来有啥子区别。其实本来就没有什么区别,只是从不同角度的相同的算法。
资源单实例死锁检测
死锁检测算法不使用资源分配图,使用等待图。
等待图: 就是把资源分配图中的资源全部省略,只表示进程间的关系。
算法: 与资源分配图算法一样,当且仅当等待图中存在一个环,系统中存在死锁。为了检测死锁,系统需要维护等待图,并周期性地调用在图中进行搜索的算法。
资源多实例死锁检测
与银行家算法是分相似,唯一不同是不需要知道每个进程的最大需求资源量,而是假定在一个进程的请求被接受以后执行完这段程序就好释放占用的资源。
本质上和银行家算法是一样的,不过在计算的时候把need换成了request。
4. 死锁恢复
具体就是死锁恢复有什么举措:
进程终止
- 终止所有的进程。
- 一次只终止一个进程直到取消死循环为止。
资源抢占
通过抢占资源以取消死锁,逐步从进程中抢占资源给其他进程使用,直到死循环被打破。
资源抢占要处理的问题:
- 选择一个牺牲品,使代价最小化。
- 回滚:被抢占的进程需要回滚到安全状态。
- 饥饿:必须保证不会发生饥饿,因为有可能被抢占的总是同一个进程。