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 死锁的产生与预防
死锁,就是两个或多个任务互相等待对方释放资源,结果谁也动不了。就像两个人过独木桥,谁也不让谁,最后都过不去。
死锁的四个必要条件
- 互斥:资源一次只能被一个任务使用。
- 持有并等待:任务拿着一个资源,同时等待另一个资源。
- 不可剥夺:资源不能被强制拿走,只能由持有者主动释放。
- 循环等待:任务A等任务B的资源,任务B等任务A的资源。
我曾经在一个项目中,两个任务分别锁了UART和SPI,然后互相等对方释放——直接死锁。系统卡死,看门狗都救不了。从那以后,我定了个规矩:所有任务按固定顺序加锁。
死锁预防策略
| 策略 | 做法 | 效果 |
|---|---|---|
| 锁顺序 | 所有任务按相同顺序加锁 | 消除循环等待 |
| 锁超时 | 加锁时设置超时时间 | 避免永久等待 |
| 一次性申请 | 要么拿到所有锁,要么一个都不拿 | 消除持有并等待 |
| 减少锁粒度 | 用更细的锁代替大锁 | 降低冲突概率 |
实战建议:我个人的习惯是,在项目初期就定义好锁的优先级顺序。比如:Mutex_A > Mutex_B > Mutex_C。所有任务必须按这个顺序加锁,解锁顺序相反。这样能从根本上避免死锁。
4.5 知识体系总览
下面这张图把本章的核心逻辑串起来了。你仔细看看,资源管理其实就三件事:识别共享资源、选择合适的同步机制、避免死锁。
你想想看,资源管理其实没那么玄乎。核心就三件事:识别共享资源、选对同步机制、防死锁。把这三点做到位,多核MCU的资源管理就稳了八成。
嗯,本章的内容就到这里。记住我强调的那几个坑:临界区别干多余的事、Mutex和Semaphore别混用、加锁顺序要统一。这些都是我用实际教训换来的经验,你记住了,能少走很多弯路。