操作系统死锁
·
一、什么是死锁?
死锁 是指两个或两个以上的进程(或线程)在执行过程中,由于竞争资源或彼此通信而造成的一种相互等待的现象。若无外力干涉,这些进程都将无法向前推进。
一个经典的死锁类比是 “哲学家就餐问题”:
- 有五位哲学家围坐在一张圆桌旁,他们只在思考和吃饭两种行为间交替。
- 桌子中间有一大盘米饭,每个哲学家左右两边各有一根筷子。
- 哲学家需要同时拿起左右两边的两根筷子才能吃饭。
- 如果所有哲学家都同时拿起自己左边的筷子,那么每个人都会等待右边的人放下筷子。由于没有人会先放下筷子,所有人都将永远等待下去,这就是死锁。
二、死锁产生的四个必要条件(Coffman条件)
这四个条件必须同时满足,死锁才可能发生。只要打破其中任意一个,死锁就可以被预防。
- 互斥访问:资源不能被共享,一次只能被一个进程独占使用(例如:打印机、磁带机)。
- 持有并等待:进程已经持有了至少一个资源,但又提出了新的资源请求,而该请求资源已被其他进程占用,此时该进程进入等待状态,但并不释放自己已持有的资源。
- 不可剥夺:进程已获得的资源,在未使用完之前,不能被其他进程强行夺走,只能由该进程主动释放。
- 循环等待:存在一个进程资源的循环等待链。例如,进程P1等待P2占用的资源,P2等待P3占用的资源,……,Pn等待P1占用的资源,形成一个环路。
三、一个简单的代码示例
下面的Java代码模拟了一个可能发生死锁的场景:
public class DeadlockDemo {
private static final Object resource1 = new Object();
private static final Object resource2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: Locked resource 1");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (resource2) { // 尝试获取resource2的锁
System.out.println("Thread 1: Locked resource 2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2: Locked resource 2");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (resource1) { // 尝试获取resource1的锁
System.out.println("Thread 2: Locked resource 1");
}
}
});
thread1.start();
thread2.start();
}
}
运行结果分析:
- 线程1锁住了resource1,然后尝试锁住resource2。
- 线程2锁住了resource2,然后尝试锁住resource1。
- 此时,线程1在等待线程2释放resource2,线程2在等待线程1释放resource1。双方都持有对方所需的资源且互不释放,形成循环等待,导致死锁。程序将卡住,无法输出最后两条信息。
四、处理死锁的策略
操作系统和应用程序设计者通常采用以下三种策略之一来处理死锁:
1. 死锁预防
在系统设计时,通过协议来确保死锁的四个必要条件至少有一个不成立。这是一种静态的、严苛的策略。
- 破坏“互斥访问”:对于某些资源(如只读文件),可以让其共享访问。但对于打印机等临界资源,互斥是无法避免的。
- 破坏“持有并等待”:要求进程在执行前一次性申请其所需的所有资源。如果无法满足,则一直等待。这会导致资源利用率极低,可能产生“饥饿”现象。
- 破坏“不可剥夺”:如果一个进程请求资源失败,它必须释放其当前持有的所有资源。这对于像打印机这样的资源来说实施起来非常困难。
- 破坏“循环等待”:对所有资源类型进行线性排序,并要求每个进程按递增顺序申请资源。这是最实用的一种预防方法。例如,在上面的代码中,如果强制两个线程都先申请resource1,再申请resource2,就可以避免死锁。
2. 死锁避免
系统在分配资源时进行动态检查,判断此次分配是否会导致系统进入不安全状态(即可能发生死锁的状态)。如果会,则拒绝分配。
最著名的算法是 银行家算法:
- 系统就像一家银行,资源就是资金。
- 进程在运行前声明其所需的最大资源量。
- 当进程请求资源时,系统会模拟分配,并检查分配后系统是否仍处于安全状态(即存在一个能让所有进程顺利完成的执行序列)。如果是,则分配;否则,让进程等待。
死锁避免比死锁预防允许更高的资源并发度,但需要事先知道进程的最大资源需求,并且有较大的计算开销。
3. 死锁检测与恢复
这种策略不试图阻止死锁的发生,而是允许死锁发生,然后定期检测并采取措施恢复。
- 死锁检测:
* 系统维护一个资源分配图。
* 定期调用检测算法(如基于资源分配图的简化算法)来判断图中是否存在环路。如果存在,则说明发生了死锁。 - 死锁恢复:一旦检测到死锁,就需要打破它。
* 进程终止:
* 终止所有死锁进程:简单粗暴,但代价大。
* 逐个终止进程:每次终止一个进程,释放其资源,直到死锁环被打破。需要选择终止哪个进程(如优先级最低、代价最小、完成进度最少等)。
* 资源剥夺:从一个或多个死锁进程那里剥夺资源,分配给其他进程,直到死锁被打破。需要考虑三个问题:
1. 选择剥夺哪个进程的资源?
2. 如何进行“回滚”?(让进程恢复到某个安全状态重新运行)
3. 如何防止“饥饿”?(避免同一个进程总是被选中剥夺资源)
总结
| 策略 | 核心思想 | 优点 | 缺点 |
|---|---|---|---|
| 死锁预防 | 破坏四个必要条件之一 | 简单、可靠,从根本上避免 | 资源利用率低,限制并发性 |
| 死锁避免 | 动态检测分配后是否安全(如银行家算法) | 比预防允许更高的并发度 | 需要预知资源需求,计算开销大 |
| 死锁检测与恢复 | 允许死锁发生,定期检测并修复 | 对资源分配限制最少,并发度最高 | 检测和恢复的实现复杂,有恢复开销 |
在实际操作系统中(如Linux、Windows),由于死锁预防和避免的开销较大,通常更依赖于严谨的编程实践(如按固定顺序获取锁)来避免死锁,并结合一些轻量级的检测机制。对于应用程序开发者而言,理解死锁的原理并编写无死锁的代码是至关重要的。
更多推荐
所有评论(0)