Overview
如果一个进程集合中的每个进程都在等待只能由该进程集合中的其他进程才能引发的事件,那么该进程集合就是 死锁 的。
上面的定义比较抽象,用大白话讲就是:多个进程(线程)互相请求其他进程持有的资源而又不肯放弃自己持有的资源,就会发生死锁。本质上是资源在时序上的分配不合理造成的问题。
资源 按照被剥夺后所造成的后果严重性,分为 可抢占资源 和 不可抢占资源。可抢占资源如主存,由操作系统保证进程所占有内存页面的换入换出,即使被抢占也不会造成严重的后果;不可抢占资源如打印机,如果打印机被抢占,那打印的内容将会混乱。
显然,(在系统满足的前提下)可抢占肯定不会造成死锁。
什么情况下可能会产生死锁?产生死锁的必要条件:
- 资源互斥:一个资源只有两种状态,被占用和可用;
只读资源可以不互斥,就不会死锁; - 非剥夺控制:即进程在持有某资源时,不可被抢占;
- 逐次请求:即请求的多个资源不能一次性打包请求;
一次性请求可能会饿死但不会死锁; - 环路等待:进程和资源在请求与分配的关系上的有向图形成一个环路。
描述资源请求与分配的模型
把进程对资源的一系列操作抽象为三个步骤,请求、使用、释放,所以就有这两种状态:进程阻塞在请求的资源上和进程正在占有该资源。
资源分配图
用圆形来表示进程,用方形表示资源,进程到资源的有向边表示进程正在请求并阻塞在该资源,资源到进程的有向边表示进程占有资源。

如上左图所示,P1 占有资源 S2 又请求 S1,P2 占有资源 S1 又请求 S2,资源分配图形成了一个环路,即发生死锁。
为什么说 死锁是资源分配在时序上的错误,如上右图所示,进程 P1 和 P2 同时请求资源 S1,若将 S1 分配给 P1,P2 会被阻塞,但 P1 满足条件运行完后会释放资源,唤醒阻塞的 P2;若将 S1 分配给 P2,则和左图情形一样,形成环路发生死锁。
资源分配矩阵

如图所示,共有 m 种资源,向量 E 表示每种资源的总数,向量 A 表示某一时刻每种资源的可用个数;左边为分配矩阵,每行表示一个进程已经获得的对应资源的个数;右边为请求矩阵,每一行表示一个进程仍需要对应资源的个数才能运行。
显然,某时刻 E - 分配矩阵的所有行 = A,用 A 和请求矩阵的每一行相比较,即能发现将资源分配给哪一个进程可以避免死锁。
死锁的检测
检测的结果是死锁可能发生也可能没有发生,当系统发现 CPU 的利用率降到了某一阈值,有多个进程阻塞很久时,就要质疑是否发生了死锁,对其进行检测。若发生死锁,通常的做法是剥夺一些死锁进程的资源重新分配,或按优先级逐个杀死一些死锁进程直到死锁解除。
结合资源分配图,使用相关算法来 判断图中有没有环路,进而判断是否发生死锁。有常用的两种方法,拓扑排序和深度优先遍历(Depth First Search,DFS):
拓扑排序
对一个有向无环图 (Directed Acyclic Graph 简称 DAG)G 进行拓扑排序,是将 G 中所有顶点排成一个线性序列,使得图中任意一对顶点 u 和 v,若边<u,v>∈E(G),则u 在线性序列中出现在 v 之前。通常,这样的线性序列称为满足拓扑次序 (Topological Order) 的序列,简称拓扑序列,结果可能不唯一。
步骤:
- 从图中找到一个没有前驱结点(入度为 0)的顶点输出;
- 删除以这个顶点为起点的边(以产生下一个入度为 0 的节点);
- 重复上述过程,若最终有顶点没有被输出,说明有环。
举例:

如上图,先找到一个入度为 0 的节点 1,输出并删除其出边;然后 2、3 成为新的入度为 0 的节点,随机选取一个,假设选取 2;然后 3 成为唯一一个入度为 0 的节点,选择 3,剩下的点入度都不为 0,即存在环路;
深度优先遍历
步骤:
- 使用一个空的点集 L 记录已经遍历过的点,还要有方法标记遍历过的边,对图中的每一个节点 N,都执行下面几个步骤;
- 将 L 初始化为空集,清除所有边的标记;
- 将当前节点添加到集合 L 尾部,检测当前节点在 L 中是否唯一,不唯一则说明成环,算法结束;
- 从当前节点开始检测是否存在没有被标记的出边,如果有执行第 4 步,否则执行第 5 步;
- 随机选取一条没有被标记的出边,标记它,并顺着它找到新的当前节点,跳到第 3 步;
- 回溯到上一节点,若这一节点是起始节点,则表明没有环路。
举例:

如上图,假设先选取 4,深度遍历 L={4,6,7},回溯两次回到节点 4,说明从节点 4 开始没有环路;再选择节点 1,可能出现 L={1,3,2,4,6,7},回溯到 2,再依次走到节点 5,3,当前 L={1,3,2,4,6,7,5,3},出现重复节点 3 说明出现环路。
显然 DFS 没有拓扑排序方便,因为 DFS 要将所有节点都作为根节点穷举一遍所有路径,才能确保真的没有环路。
死锁的预防
死锁的检测是怀疑发生死锁并对其进行检测,而死锁的预防,就是要未雨绸缪,提前采取一些措施判断将资源分配给某进程后是否可能会引发死锁,从而预防死锁的发生。
银行家算法
将金钱和借款人分别比作资源和进程,将贷款动作比作进程请求资源的动作,银行家需要合理的调度金钱以满足借款人的需求,保证银行的正常运行。即要判断将资源分配给某进程后是否安全,结合资源分配矩阵,可以比较容易理解。将剩余资源向量和资源请求矩阵的每一行进行对比,能满足的就是安全的。

如图所示,向量 E 是每种资源的个数,P 是当前已分配出的相应资源个数,A 是当前剩余相应资源的个数。将向量 A 与请求矩阵的每一行一一比较,容易发现,这一时刻只有将资源分配给 D 进程才可以避免死锁,D 完成后释放资源又可以满足其他进程。
破坏死锁的必要条件
银行家算法是比较难以实现的,因为系统实际运行中,进程数目可能是不断变化的,而且进程可能在运行过程中才知道自己需要哪些资源,进而发出请求,所以想要预判的较为精确是很困难的,除非某一系统就像上面例子一样,相对固定的进程、资源和请求数。
1、破坏资源互斥:
我们一直说死锁是资源在时序上的分配错误,那当资源无限多的时候就不会发生死锁。当然资源不可能无限多,但可以从另一个角度考虑,若我们能缩短资源被占有的时间,让资源提升它的服务效率,那是不是也能降低死锁发生的概率。
这其实也可以看做是快速设备和慢速设备之间的不匹配问题,进程在内存中运行的速度快,资源比如打印机这样的外部设备速度慢。我们怎么解决 CPU 和主存之间的速度不匹配的,我们也就可以解决进程和打印机之间的速度不匹配,答案是提取出一个中间的缓存层,如 假脱机方式。
脱机,可以想到浏览器处于断网状态时,脱机工作还可以访问本地缓存的页面,这是被迫使用缓存。而假脱机是主动使用缓存,多个要打印的进程不直接请求打印机,而是将要打印的内容输出到一个中间层,由打印机程序去决定要打印哪些内容,这样每个进程不再独立的占用打印机资源,打破了资源互斥条件,不会发生死锁。
2、破坏非剥夺:
如一开始对资源的分类中举的例子,进程对内存的使用就可以被剥夺,因为有操作系统来保证内存页面的换入换出;对 CPU 的使用也是这个道理,保证进程调度之后能从断点处继续运行。这就是破坏了非剥夺条件,不会发生死锁,但是要考虑剥夺的代价。
3、破坏逐次请求:
比如多个进程都要请求扫描仪和打印机资源,逐次请求就可能发生死锁,而若将扫描仪和打印机封装成一个资源“扫描打印机”,那逐次请求变为一次请求,就不会出现互相请求对方的资源又不放手自己的资源这种情况。但是这样会造成资源利用率的降低,假设一个进程扫描工作达 10 分钟,那其他只想要打印的进程在这段时间内也无法时候用打印机。
4、破坏环路:
可以对资源进行编号,一个进程想要请求多个资源,必须按照资源的编号升序进行请求与获取。如进程 P 想要请求1号扫描仪,2号打印机,必须先获得扫描仪才有可能再获得打印机。