1. 死锁基础:从定义到四个必要条件
各位同学好,我是老张。在嵌入式系统里摸爬滚打了十几年,死锁这玩意儿,我见得多了。今天咱们就来聊聊死锁的基础知识。
死锁是什么?说白了,就是两个或多个任务互相等待对方释放资源,结果谁也动不了。就像两个人过独木桥,谁也不让谁,最后都卡在那儿。
1.1 死锁的正式定义
在实时系统中,死锁是指一组并发任务中的每个任务都在等待某个资源,而这个资源又被组内其他任务持有,导致所有任务都无法继续执行的状态。
嗯,这里要注意:死锁不是系统崩溃,但比崩溃更麻烦。系统崩溃了还能重启,死锁了,你得先找到谁锁了谁,再想办法解开。
死锁的核心特征:所有任务都在等待,但谁也不会主动释放资源。
1.2 四个必要条件
死锁的发生,必须同时满足四个条件。缺一个,死锁就形成不了。我当年调试一个多任务系统时,就是靠这个理论一步步排查的。
| 条件 | 说明 | 我的经验 |
|---|---|---|
| 互斥 | 资源一次只能被一个任务使用 | 比如串口、I2C总线,同一时刻只能一个任务操作 |
| 持有并等待 | 任务拿着一个资源,同时等待另一个资源 | 我见过最典型的:任务A拿着锁1,等锁2;任务B拿着锁2,等锁1 |
| 非剥夺 | 资源不能被强制拿走,只能由持有者主动释放 | RTOS里的互斥锁,基本都是非剥夺的 |
| 循环等待 | 存在一个任务-资源的环形等待链 | 这是死锁的"最后一根稻草" |
避坑指南:我曾经在一个项目中,只检查了前三个条件,觉得没问题就上线了。结果运行了三天,死锁了。后来才发现,循环等待在特定时序下才会触发。所以,四个条件必须同时检查,一个都不能少。
1.3 一个直观的例子
咱们来看个实际代码。假设有两个任务,共享两个互斥锁:
// 任务A
void taskA(void *param) {
while(1) {
take_mutex(mutex1); // 拿锁1
printf("TaskA: 拿到锁1\n");
// 模拟一些处理
delay_ms(10);
take_mutex(mutex2); // 等锁2
printf("TaskA: 拿到锁2\n");
release_mutex(mutex2);
release_mutex(mutex1);
}
}
// 任务B
void taskB(void *param) {
while(1) {
take_mutex(mutex2); // 拿锁2
printf("TaskB: 拿到锁2\n");
delay_ms(10);
take_mutex(mutex1); // 等锁1
printf("TaskB: 拿到锁1\n");
release_mutex(mutex1);
release_mutex(mutex2);
}
}
你想想看,如果任务A拿了锁1,任务B拿了锁2,然后A等锁2,B等锁1。结果就是:两个任务都卡在take_mutex那里,谁也动不了。
为什么会这样?因为四个条件全满足了:
- 互斥:mutex1和mutex2一次只能被一个任务持有
- 持有并等待:A拿着mutex1等mutex2,B拿着mutex2等mutex1
- 非剥夺:RTOS的mutex不能强制从任务手里拿走
- 循环等待:A→mutex2→B→mutex1→A,形成闭环
我的习惯:写代码时,我会给每个锁编号,然后规定所有任务必须按编号从小到大拿锁。比如先拿mutex1,再拿mutex2。这样循环等待就破了。这个习惯帮我避免了好几次死锁。
1.4 死锁 vs 活锁 vs 饥饿
这三个概念容易搞混。我简单说说:
- 死锁:任务卡死了,不动了
- 活锁:任务还在动,但一直在做无用功,比如两个任务互相谦让资源,让来让去谁也用不上
- 饥饿:某个任务一直拿不到资源,但其他任务正常执行
死锁是最严重的,因为系统完全停止。活锁和饥饿至少系统还在跑,只是某个任务或某些任务受影响。
好了,这一章就到这里。记住这四个条件,它们是后续所有死锁检测和避免方法的基础。下一章咱们聊聊怎么在实时系统中检测死锁。