4. 资源管理基础:共享资源与临界区、互斥锁与信号量、死锁

多核MCU开发,说白了就是一群人抢一间屋子的活儿。每个核都是一个独立的人,它们要共用屋子里的桌子、椅子、水杯——这些就是共享资源。怎么让它们不打架、不互相使绊子,就是本章要聊的核心。

我刚开始做多核项目时,总觉得资源管理不就是加个锁嘛。结果第一次联调,两个核同时操作一个全局变量,数据直接乱掉。嗯,从那以后我再也不敢小看这章内容了。

4.1 共享资源与临界区

什么是共享资源?简单说,就是能被多个任务同时访问的硬件或软件资源。比如全局变量、外设寄存器、内存缓冲区、DMA通道等等。

临界区呢?就是访问这些共享资源的代码段。这段代码必须互斥执行——一个任务进去了,其他任务就得等着。

核心原则:临界区要短、要快、要可预测。别在临界区里做延时、等待或复杂计算。

我在项目中遇到过一个问题:一个任务在临界区里调用了printf,结果打印函数内部用了另一个锁,直接导致死锁。所以记住,临界区里只做必要的资源访问,别干多余的事。

临界区的实现方式

方式 适用场景 缺点
关中断 极短操作(如原子变量读写) 影响中断响应,多核无效
自旋锁 多核间短时等待 浪费CPU周期
互斥锁 任务间长时等待 有上下文切换开销
信号量 资源计数、同步 使用不当易出问题

4.2 互斥锁(Mutex)

互斥锁,英文Mutex,全称Mutual Exclusion。它的核心特性是:谁持有谁释放。一个任务拿了锁,就必须由同一个任务释放。这个特性很重要,后面讲死锁时会提到。

我个人的习惯是:能用Mutex就不用Semaphore。为什么?因为Mutex有优先级继承机制,能避免优先级反转。这个后面章节会细讲,你先记住这个结论。

Mutex的使用示例

// 定义互斥锁
mutex_t uart_mutex;

// 任务A:发送数据
void task_send(void *arg) {
    while (1) {
        mutex_lock(&uart_mutex);
        // 临界区:操作UART
        uart_send("Hello from Core0");
        mutex_unlock(&uart_mutex);
        vTaskDelay(100);
    }
}

// 任务B:接收数据
void task_recv(void *arg) {
    while (1) {
        mutex_lock(&uart_mutex);
        // 临界区:读取UART
        uint8_t data = uart_recv();
        mutex_unlock(&uart_mutex);
        process_data(data);
    }
}

小技巧:加锁和解锁要成对出现。我见过有人写代码,加锁后条件判断提前return,忘了解锁——这种bug最难查。建议用宏或函数封装,确保解锁一定会执行。

4.3 信号量(Semaphore)

信号量分两种:二值信号量和计数信号量。二值信号量像一把钥匙,只能一个人用。计数信号量像一筐钥匙,可以多个人同时用,但总数有限。

说白了,Mutex是保护资源的,Semaphore是管理资源的。Mutex强调互斥,Semaphore强调同步和计数。

信号量的典型应用

  • 二值信号量:任务同步。比如任务A做完某件事,通过信号量通知任务B开始工作。
  • 计数信号量:管理有限资源。比如有3个DMA通道,最多允许3个任务同时使用。
// 计数信号量:管理3个DMA通道
semaphore_t dma_sem;
semaphore_init(&dma_sem, 3);  // 初始可用3个

void task_dma_operation(void *arg) {
    while (1) {
        // 获取一个DMA通道
        semaphore_take(&dma_sem, portMAX_DELAY);
        
        // 使用DMA传输数据
        dma_transfer(data_buffer, size);
        
        // 释放DMA通道
        semaphore_give(&dma_sem);
        
        vTaskDelay(50);
    }
}

注意:信号量的give和take可以在不同任务中执行。这和Mutex不同。Mutex要求谁拿谁放,信号量没这个限制。这个区别很重要,用错了会出大问题。

4.4 死锁的产生与预防

死锁,就是两个或多个任务互相等待对方释放资源,结果谁也动不了。就像两个人过独木桥,谁也不让谁,最后都过不去。

死锁的四个必要条件

  1. 互斥:资源一次只能被一个任务使用。
  2. 持有并等待:任务拿着一个资源,同时等待另一个资源。
  3. 不可剥夺:资源不能被强制拿走,只能由持有者主动释放。
  4. 循环等待:任务A等任务B的资源,任务B等任务A的资源。

我曾经在一个项目中,两个任务分别锁了UART和SPI,然后互相等对方释放——直接死锁。系统卡死,看门狗都救不了。从那以后,我定了个规矩:所有任务按固定顺序加锁。

死锁预防策略

策略 做法 效果
锁顺序 所有任务按相同顺序加锁 消除循环等待
锁超时 加锁时设置超时时间 避免永久等待
一次性申请 要么拿到所有锁,要么一个都不拿 消除持有并等待
减少锁粒度 用更细的锁代替大锁 降低冲突概率

实战建议:我个人的习惯是,在项目初期就定义好锁的优先级顺序。比如:Mutex_A > Mutex_B > Mutex_C。所有任务必须按这个顺序加锁,解锁顺序相反。这样能从根本上避免死锁。

4.5 知识体系总览

下面这张图把本章的核心逻辑串起来了。你仔细看看,资源管理其实就三件事:识别共享资源、选择合适的同步机制、避免死锁。

资源管理知识体系 共享资源管理 临界区 关中断 / 自旋锁 短、快、可预测 同步机制 互斥锁(Mutex) 信号量(Semaphore) 死锁 四个必要条件 预防策略 核心原则:识别资源 → 选择机制 → 避免死锁 锁顺序 + 锁超时 + 减少锁粒度

你想想看,资源管理其实没那么玄乎。核心就三件事:识别共享资源、选对同步机制、防死锁。把这三点做到位,多核MCU的资源管理就稳了八成。

嗯,本章的内容就到这里。记住我强调的那几个坑:临界区别干多余的事、Mutex和Semaphore别混用、加锁顺序要统一。这些都是我用实际教训换来的经验,你记住了,能少走很多弯路。