深入理解操作系统核心机制,掌握并发编程中的资源竞争与死锁破解之道
在操作系统和多线程编程中,死锁(Deadlock)是一个极其重要且令人头疼的问题。想象一下,你和朋友在餐厅吃饭,你需要筷子才能吃,朋友也需要筷子才能吃。如果你手里拿着一双筷子,而朋友手里拿着另一双筷子,你们都在等待对方放下筷子,结果就是谁也无法吃饭。这就是死锁的直观体现。
从技术角度定义,死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。死锁不仅存在于操作系统内核中,也广泛存在于数据库事务、分布式系统以及日常的多线程应用程序中。理解产生死锁的4个必要条件是解决这一问题的前提。
若要发生死锁,必须同时满足以下四个条件。只要其中一个条件不成立,死锁就不会发生。这四个条件分别是:互斥条件、占有且等待条件、不剥夺条件和循环等待条件。
指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。例如:打印机、磁带机等物理设备,以及许多软件资源(如全局变量、临界区)。
指进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程阻塞,但对自己已获得的资源保持不放。简言之,就是“手里拿着羊,眼里看着狼”。
指进程已获得的资源,在未使用完之前,不能强行剥夺,只能在使用完时由自己释放。操作系统无法强制收回进程占有的资源,除非进程主动释放。
指若干进程之间形成一种头尾相接的循环等待资源关系。例如:P1等待P2占有的资源,P2等待P3占有的资源,...,Pn等待P1占有的资源。这种环路是死锁发生的直观表现。
这四个条件构成了死锁发生的充分必要条件。如果破坏了其中任何一个,死锁就不可能发生。在实际工程实践中,我们通常通过破坏其中一个或多个条件来预防或避免死锁。
针对死锁问题,操作系统和软件工程师通常采取四种策略:预防死锁、避免死锁、检测死锁和解除死锁。
预防死锁的核心思想是通过设置限制条件,破坏产生死锁的四个必要条件之一。这种方法实现简单,但可能导致资源利用率降低,进程吞吐量减少。
避免死锁不限制资源分配的方式,而是在资源动态分配过程中,用某种方法防止系统进入不安全状态。最经典的算法是银行家算法(Banker's Algorithm)。
银行家算法的核心是:安全序列。系统在分配资源前,先试探性分配,然后执行安全性算法检查系统是否处于安全状态。如果存在一个安全序列,则分配;否则,进程等待。
// 伪代码示意
function BankerAlgorithm(process, resources):
if isSafeState(process, resources):
allocateResources(process, resources)
return true
else:
wait(process)
return false不采取任何限制措施,允许死锁发生。系统定期运行死锁检测算法,一旦发现死锁,就采取措施解除。
检测算法:通常使用资源分配图(Resource Allocation Graph, RAG)或等待图(Wait-For Graph)来检测环路。如果图中存在环路,且环路中的每个节点都只等待一个资源,则可能存在死锁。
解除策略:
哲学家进餐问题(Dining Philosophers Problem)是操作系统中经典的死锁演示问题。由Dijkstra提出,用于说明死锁的产生和解决。
五位哲学家围坐在一张圆桌旁,每人面前有一盘意大利面,但只有五根筷子(每两人之间一根)。哲学家需要两根筷子才能吃面,否则只能思考。
如果每位哲学家都同时拿起左边的筷子,那么每个人都拿着一根筷子,等待右边的筷子。此时,所有哲学家都在等待,无人能吃到面,形成循环等待,即死锁。
规定哲学家必须按顺序拿起筷子,例如编号小的先拿,编号大的后拿。或者规定奇数号哲学家先拿左边,偶数号先拿右边。这样可以打破环路。
规定哲学家必须同时拿到两根筷子才能开始吃面。如果左边筷子不可用,则不拿右边筷子,等待后再尝试。这可以通过信号量或互斥锁实现。
| 策略 | 破坏的条件 | 优点 | 缺点 |
|---|---|---|---|
| 资源有序分配 | 循环等待 | 实现简单,无开销 | 限制资源使用顺序,可能降低利用率 |
| 静态资源分配 | 占有且等待 | 完全避免死锁 | 资源利用率低,可能导致饥饿 |
| 银行家算法 | 避免不安全状态 | 资源利用率高,灵活 | 实现复杂,需预知最大需求,开销大 |
| 死锁检测与恢复 | 无 | 资源利用率高,无预知需求 | 检测开销大,恢复可能丢失数据 |
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。简单来说,就是大家都拿着对方需要的资源,又都在等对方释放,结果谁也动不了。
预防死锁主要通过破坏产生死锁的四个必要条件之一来实现。常见方法包括:资源静态分配(破坏占有且等待)、资源有序分配(破坏循环等待)、允许资源剥夺(破坏不剥夺条件)等。其中,资源有序分配法最为常用。
银行家算法是一种避免死锁的算法。它在资源分配前试探性地进行分配,并检查系统是否处于安全状态。如果处于安全状态,则分配资源;否则,进程等待。这样可以确保系统始终处于安全状态,从而避免死锁。
死锁是指进程无限期等待资源,且这些资源被其他等待的进程占有,形成环路。饥饿是指进程长期无法获得所需资源,可能是因为优先级过低或被其他高优先级进程持续抢占。死锁是相互等待,饥饿是单向被剥夺。
在编程中避免死锁的建议包括:1. 按固定顺序获取锁;2. 使用超时机制获取锁;3. 尽量减少锁的粒度;4. 使用死锁检测工具;5. 避免嵌套锁。这些方法可以帮助开发者减少死锁发生的概率。