3、死锁的预防策略:破坏四个必要条件之一

死锁这东西,说白了就是几个任务互相掐着对方的资源不放,谁也别想往前走。我在项目中遇到过好几次死锁导致的系统卡死,最严重的一次是工业控制设备直接停机,现场工程师急得团团转。后来我养成了一个习惯:在设计阶段就把死锁扼杀在摇篮里。

怎么扼杀?核心思路就一条——破坏死锁的四个必要条件中的任意一个。这四个条件是:互斥、持有并等待、不可剥夺、循环等待。只要干掉其中一个,死锁就玩不转。

核心思想:死锁预防 = 破坏四个必要条件之一。不需要全部破坏,一个就够。

3.1 四个必要条件回顾

先快速过一下这四个条件,方便后面理解预防策略。

条件 含义 能否破坏?
互斥 资源一次只能给一个任务用 部分资源可以(如共享内存),但打印机、串口这类不行
持有并等待 任务拿着资源A,还等着资源B 可以!一次性分配法就是干这个的
不可剥夺 资源不能被强行抢走 可以,但实现起来有风险
循环等待 任务之间形成资源等待环 可以,给资源编号就能破

嗯,这里要注意:互斥条件在很多场景下是没法破坏的。比如你让两个任务同时往串口发数据,那不乱套了?所以实际工程中,我们主要盯着后面三个条件下手。

3.2 资源一次性分配法:破坏「持有并等待」

这个方法我个人特别喜欢,因为它简单粗暴。说白了就是:任务在开始运行之前,一次性申请它需要的所有资源。要么全给,要么一个都不给。

举个例子,任务A需要打印机和扫描仪。按照一次性分配法,任务A启动时必须同时拿到打印机和扫描仪。如果扫描仪被占用了,那打印机也不给A,A就乖乖等着。

我的经验:这个方法在小型嵌入式系统里特别好用。我曾经在一个RTOS项目里用这个策略,死锁问题直接清零。但要注意,任务必须提前知道自己需要哪些资源,不能动态变化。

3.3 一次性分配法的实现

实现起来其实不复杂。我习惯用一个资源管理模块,任务启动时调用一个分配函数,传入所有需要的资源ID列表。

/* 资源一次性分配示例 */
typedef struct {
    int resource_id;
    int is_available;
} Resource;

int request_all_resources(int task_id, int* res_list, int count) {
    int i;
    
    /* 先检查所有资源是否可用 */
    for (i = 0; i < count; i++) {
        if (!is_resource_available(res_list[i])) {
            return -1;  /* 有一个不可用,就不分配 */
        }
    }
    
    /* 全部可用,一次性分配 */
    for (i = 0; i < count; i++) {
        allocate_resource(task_id, res_list[i]);
    }
    
    return 0;  /* 分配成功 */
}

你看,逻辑很直白。先检查,再分配。中间没有「先拿一个,再等另一个」的窗口期,所以「持有并等待」这个条件就被破坏了。

注意:一次性分配法有个明显的缺点——资源利用率低。任务可能只需要打印机5分钟,但为了等扫描仪,打印机被白白占着半小时。这在资源紧张的系统里是个大问题。

3.4 其他预防策略简介

除了一次性分配法,还有几种常见策略,我简单说一下。

  • 破坏「不可剥夺」:允许操作系统强行回收资源。比如任务A占着打印机不放,系统可以把它挂起,把打印机给更高优先级的任务。我曾经在一个实时系统里用过,但实现起来要小心,容易引发优先级反转。
  • 破坏「循环等待」:给所有资源编号,任务必须按编号顺序申请。比如打印机编号1,扫描仪编号2,任务必须先申请1再申请2。这样就不会形成循环等待环。

我个人更推荐一次性分配法,因为它实现简单,逻辑清晰。但如果你做的是大型系统,资源很多,那资源编号法可能更合适。

3.5 知识体系图

下面这张图把死锁预防的核心逻辑串起来了。我画图时习惯把重点标红,方便一眼看清。

死锁预防策略 互斥 持有并等待 不可剥夺 循环等待 通常无法破坏 一次性分配法 资源抢占 资源编号法 推荐使用 一次性分配法优缺点 ✅ 优点:实现简单,逻辑清晰,彻底杜绝死锁 ❌ 缺点:资源利用率低,任务可能长时间占用不需要的资源

3.6 避坑指南

我曾经在一个项目里踩过坑,分享出来给大家提个醒。

  • 别把所有资源都一次性分配:有些资源是临时需要的,比如一块临时缓冲区。如果任务启动时就把所有缓冲区都占了,其他任务就没法用了。我建议只分配关键资源,临时资源按需申请。
  • 注意任务优先级:一次性分配法本身不解决优先级问题。高优先级任务可能因为等低优先级任务释放资源而被阻塞。这时候需要配合优先级继承协议。
  • 别在中断里用:中断服务程序里千万别搞资源一次性分配。中断上下文不能阻塞等待,否则系统直接崩。我见过有人这么干,结果就是死机。

我的建议:如果你刚开始做嵌入式系统,先从一次性分配法入手。它简单、可靠,适合资源不多的小系统。等经验丰富了,再尝试资源编号法或者更复杂的策略。

好了,死锁预防就聊到这儿。记住一句话:预防死锁,就是破坏四个必要条件中的一个。一次性分配法是最容易上手的,但也要注意它的局限性。实际项目中,根据资源类型和系统规模选择合适的策略,别死磕一种方法。


公众号:蓝海资料掘金营,微信deep3321