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 饥饿

这三个概念容易搞混。我简单说说:

  • 死锁:任务卡死了,不动了
  • 活锁:任务还在动,但一直在做无用功,比如两个任务互相谦让资源,让来让去谁也用不上
  • 饥饿:某个任务一直拿不到资源,但其他任务正常执行

死锁是最严重的,因为系统完全停止。活锁和饥饿至少系统还在跑,只是某个任务或某些任务受影响。

好了,这一章就到这里。记住这四个条件,它们是后续所有死锁检测和避免方法的基础。下一章咱们聊聊怎么在实时系统中检测死锁。