死锁产生的四个必要条件

死锁这东西,说白了就是几个任务互相掐住了对方的命门,谁都不肯松手。我在嵌入式系统里调试过不少死锁问题,每次排查到最后,基本都能归到这四个条件上。你只要把这四个条件记牢了,分析死锁就像拿着地图找路——清晰得很。

条件一:互斥(Mutual Exclusion)

互斥,意思就是资源一次只能被一个任务占用。打个比方,厕所只有一个坑,你进去了别人就得等着。

在操作系统里,互斥资源包括:

  • 打印机、串口这类外设
  • 共享内存中的某个数据结构
  • 全局变量的写操作(如果没加锁)

我记得有一次,一个同事在驱动里直接操作了硬件寄存器,没加任何保护。结果两个任务同时写,寄存器值乱得一塌糊涂。嗯,这就是典型的互斥没做好——但反过来,如果资源本身就不需要互斥,死锁也就无从谈起。

关键点:互斥是死锁的“土壤”。没有互斥,资源可以随便共享,死锁自然不存在。但现实中,很多资源天生就是互斥的,你绕不开。

条件二:持有并等待(Hold and Wait)

这个条件的意思是:任务已经占着一个资源,同时还在等另一个资源。而且它不放手。

我习惯用一个场景来理解:你左手拿着一个杯子,右手想去拿另一个杯子。但你不肯把左手的杯子放下,就这么僵着。

在代码里,它长这样:

// 任务A
lock(mutex1);          // 持有资源1
// ... 做点事情
lock(mutex2);          // 等待资源2
// 此时,如果任务B已经锁了mutex2,死锁就来了

你想想看,如果任务在等待新资源时,能先把已占有的资源释放掉,那死锁就不会发生。但现实往往不允许——因为释放了可能就被别人抢走了。

避坑指南:我曾经在一个项目中,发现某个任务同时锁了三个信号量。排查下来,其实只需要两个。多出来的那个锁,纯粹是代码写顺手了。所以我的建议是:能少锁就少锁,能晚锁就晚锁

条件三:不可剥夺(No Preemption)

不可剥夺,意思是资源一旦被任务占有,除非任务自己主动释放,否则谁也抢不走。

这其实是一种保护机制。你想想,如果A正在用打印机打印,突然被B强行抢走,那打印出来的东西肯定乱七八糟。所以操作系统通常不允许强制剥夺资源。

但问题也出在这里——如果A占着资源不撒手,B就只能干等。等多久?不知道。如果A自己也被别的资源卡住了,那就彻底死锁了。

我遇到过这样一个案例:一个低优先级任务锁了共享缓冲区,然后被高优先级任务抢占了CPU。高优先级任务也想锁这个缓冲区,结果发现锁被低优先级任务拿着,而低优先级任务又因为被抢占无法继续执行。说白了,这就是不可剥夺带来的连锁反应。

注意:有些系统支持“资源剥夺”,比如实时操作系统里的优先级继承协议。但这属于高级玩法,普通场景下别乱用,搞不好会引入新的问题。

条件四:循环等待(Circular Wait)

循环等待,就是多个任务之间形成了一个等待环。A等B,B等C,C等A。环一旦形成,谁也跑不掉。

我习惯用一张图来理解:

任务A 任务B 任务C 任务D R1 R2 R3 R4 实线:持有资源 虚线:等待资源

这张图里,每个任务都持有一个资源,同时又在等待另一个任务持有的资源。箭头转了一圈,回到了起点——这就是循环等待。

我个人习惯在排查死锁时,先画出资源分配图。如果发现环,那基本就锁定问题了。剩下的就是看这个环是怎么形成的。

四个条件缺一不可:只有这四个条件同时满足,死锁才会发生。反过来,你只要打破其中任意一个,死锁就解了。这是解决死锁问题的根本思路。

四个条件的总结

条件 含义 打破方法
互斥 资源一次只能被一个任务使用 使用共享资源(如只读数据)
持有并等待 任务占着资源不放手,同时等别的资源 一次申请所有资源,或申请失败时释放已占资源
不可剥夺 资源不能被强制抢走 允许资源被剥夺(需谨慎)
循环等待 多个任务形成等待环 给资源编号,按顺序申请

这四个条件,说白了就是死锁的“配方”。你只要在代码里看到它们同时出现,那死锁基本跑不掉。反过来,你写代码时能刻意避免其中任意一个,死锁就不会发生。

我的经验:在实际项目中,最容易出问题的是“持有并等待”和“循环等待”。尤其是多个任务嵌套锁的时候,稍不注意就绕进去了。我建议你在设计阶段就画好资源依赖图,别等到调试时再抓瞎。

嗯,这四个条件就讲到这里。记住它们,分析死锁问题的时候,你会发现自己思路清晰很多。


专注资料整理