死锁的条件和处理方法

本文探讨了死锁的四个必要条件:互斥、占有和等待、不可抢占和环路等待,并介绍了处理死锁的方法,包括鸵鸟策略、死锁检测与恢复(资源分配图检测、抢占恢复、回滚恢复)、死锁预防(破坏四个条件)。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

必要条件

  • 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。
  • 占有和等待:已经得到了某个资源的进程可以再请求新的资源。
  • 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。
  • 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。

处理方法

主要有以下四种方法:

  • 鸵鸟策略
  • 死锁检测与死锁恢复
  • 死锁预防
  • 死锁避免

鸵鸟策略

把头埋在沙子里,假装根本没发生问题。

因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。

当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。

大多数操作系统,包括 Unix,Linux 和 Windows,处理死锁问题的办法仅仅是忽略它。

死锁检测与死锁恢复

不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。

1. 每种类型一个资源的死锁检测在这里插入图片描述

上图为资源分配图,其中方框表示资源,圆圈表示进程。资源指向进程表示该资源已经分配给该进程,进程指向资源表示进程请求获取该资源。

图 a 可以抽取出环,如图 b,它满足了环路等待条件,因此会发生死锁。

每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生

2. 每种类型多个资源的死锁检测

在这里插入图片描述
上图中,有三个进程四个资源,每个数据代表的含义如下:

  • E 向量:资源总量
  • A 向量:资源剩余量
  • C 矩阵:每个进程所拥有的资源数量,每一行都代表一个进程拥有资源的数量
  • R 矩阵:每个进程请求的资源数量

进程 P1 和 P2 所请求的资源都得不到满足,只有进程 P3 可以,让 P3 执行,之后释放 P3 拥有的资源,此时 A = (2 2 2 0)。P2 可以执行,执行后释放 P2 拥有的资源,A = (4 2 2 1) 。P1 也可以执行。所有进程都可以顺利执行,没有死锁。

算法总结如下:

每个进程最开始时都不被标记,执行过程有可能被标记。当算法结束时,任何没有被标记的进程都是死锁进程。

寻找一个没有标记的进程 Pi,它所请求的资源小于等于 A。

如果找到了这样一个进程,那么将 C 矩阵的第 i 行向量加到 A 中,标记该进程,并转回 1。

如果没有这样一个进程,算法终止。

何时执行死锁检测

现在我们知道了如何检测死锁(至少是在这种预先知道静态资源请求的情况下),但问题在于何时去检测它们。一种方法是每当有资源请求时去检测。毫无疑问越早发现越好,但这种方法会占用昂贵的CPU时间。另一种方法是每隔k分钟检测一次,或者当CPU的使用率降到某一域值时去检测。考虑到CPU使用效率的原因,如果死锁进程数达到一定数量,就没有多少进程可运行了,所以CPU会经常空闲。

3. 死锁恢复

3.1利用抢占恢复

在某些情况下,可能会临时将某个资源从它的当前所有者那里转移到另一个进程。许多情况下,尤其是对运行在大型主机上的批处理操作系统来说,需要人工进行干预。

比如,要将激光打印机从它的持有进程那里拿走,管理员可以收集已打印好的文档并将其堆积在一旁。然后,该进程被挂起(标记为不可运行)。接着,打印机被分配给另一个进程。当那个进程结束后,堆在一旁的文档再被重新放回原处,原进程可重新继续工作。

在不通知原进程的情况下,将某一资源从一个进程强行取走给另一个进程使用,接着又送回,这种做法是否可行主要取决于该资源本身的特性。用这种方法恢复通常比较困难或者说不太可能。若选择挂起某个进程,则在很大程度上取决于哪一个进程拥有比较容易收回的资源。

3.2 利用回滚恢复

如果系统设计人员以及主机操作员了解到死锁有可能发生,他们就可以周期性地对进程进行检查点检查(checkpointed)。进程检查点检查就是将进程的状态写入一个文件以备以后重启。该检查点中不仅包括存储映像,还包括了资源状态,即哪些资源分配给了该进程。为了使这一过程更有效,新的检查点不应覆盖原有的文件,而应写到新文件中。这样,当进程执行时,将会有一系列的检查点文件被累积起来。

假设,检测到进程1与进程2构成死锁.进程1需要的资源A此时被进程2占用.我们准备通过重新分配资源满足进程1,从而消除死锁.此时,我们需要检查进程2的检查点,将其恢复到某一检查点文件上, 在该检查点上,进程2尚未占用资源A. 通过对进程2进程复位,其在检查点后的所有工作将被清除.然后我们可以把资源A分配给进程1.如果复位后的进程2试图重新获得对该资源的控制,它必须等到该资源可用时为止, 至少需要在进程1执行完毕并释放该资源后.

3.3 通过杀死进程恢复

最直接也是最简单的解决死锁的方法是杀死一个或若干个进程。一种方法是杀掉环中的一个进程。如果走运的话,其他进程将可以继续。如果这样做行不通的话,就需要继续杀死别的进程直到打破死锁环。

另一种方法是选一个环外的进程作为牺牲品以释放该进程的资源。在使用这种方法时,选择一个要被杀死的进程要特别小心,它应该正好持有环中某些进程所需的资源。比如,一个进程可能持有一台绘图仪而需要一台打印机,而另一个进程可能持有一台打印机而需要一台绘图仪,因而这两个进程是死锁的。第三个进程可能持有另一台同样的打印机和另一台同样的绘图仪而且正在运行着。杀死第三个进程将释放这些资源,从而打破前两个进程的死锁。

有可能的话,最好杀死可以从头开始重新运行而且不会带来副作用的进程。比如,编译进程可以被重复运行,由于它只需要读入一个源文件和产生一个目标文件。如果将它中途杀死,它的第一次运行不会影响到第二次运行。

另一方面,更新数据库的进程在第二次运行时并非总是安全的。如果一个进程将数据库的某个记录加1,那么运行它一次,将它杀死后,再次执行,就会对该记录加2,这显然是错误的。

死锁预防

在程序运行之前预防发生死锁。

1. 破坏互斥条件

例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。

2. 破坏占有和等待条件

这样的解决方案虽然简单粗暴,但这种简单粗暴也带来了一些问题:

这种实现会降低系统的并发性,因为所有需要获取锁的线程都要去竞争同一个加锁令牌锁;
并且因为要在程序的一开始就获取所有需要的锁,这就导致了线程持有锁的时间超出了实际需要,很多锁资源被长时间的持有所浪费,而其他线程只能等待之前的线程执行结束后统一释放所有锁;
另一方面,现代程序设计理念要求我们提高程序的封装性,不同模块之间的细节要互相隐藏,这就使得在一个统一的位置一次性获取所有锁变得不再可能。

3. 破坏不可抢占条件

如果一个线程已经获取到了一些锁,那么在这个线程释放锁之前这些锁是不会被强制抢占的。但是为了防止死锁的发生,我们可以选择让线程在获取后续的锁失败时主动放弃自己已经持有的锁并在之后重试整个任务,这样其他等待这些锁的线程就可以继续执行了。

同样的,这个方案也会有自己的缺陷:

  1. 虽然这种方式可以避免死锁,但是如果几个互相存在竞争的线程不断地放弃、重试、放弃,那么就会导致活锁问题(livelock)。在这种情况下,虽然线程没有因为锁冲突被卡死,但是仍然会被阻塞相当长的时间甚至一直处于重试当中。

这个问题的一种解决方式是给任务重试添加一个随机的延迟时间,这样就能大大降低任务冲突的概率了。在一些接口请求框架中也使用了这种技巧来分散服务高峰期的请求重试操作,防止服务陷入阻塞、崩溃、阻塞的恶性循环。
2. 还是因为程序的封装性,在一个模块中难以释放其他模块中已经获取到的锁。

4. 破坏环路等待

给资源统一编号,进程只能按编号顺序来请求资源。
实践中最有效也是最常用的一种死锁阻止技术就是锁排序,通过对加锁的操作进行排序我们就能够破坏环路等待条件。例如当我们需要获取数组中某一个位置对应的锁来修改这个位置上保存的值时,如果需要同时获取多个位置对应的锁,那么我们就可以按位置在数组中的排列先后顺序统一从前往后加锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一叶一菩提魁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值