3. 任务间通信:队列、信号量与互斥量
任务间通信,说白了就是让多个任务能“说上话”。
我刚开始做嵌入式开发时,总觉得任务之间各干各的就行。直到有一次,一个传感器采集任务和一个显示任务因为共享变量没处理好,系统跑着跑着就崩了。嗯,从那以后我才真正重视起这个问题。
RTOS 里最常用的三种通信机制就是:队列(Queue)、信号量(Semaphore)和互斥量(Mutex)。它们各有各的脾气,用对了地方,系统稳如泰山;用错了,调试起来能让你怀疑人生。
3.1 队列(Queue)—— 任务间的“邮局”
队列的本质是一个先进先出(FIFO)的缓冲区。一个任务往里放数据,另一个任务从里面取数据。数据是拷贝进去的,不是传指针,这就避免了共享资源竞争的问题。
核心特点:
- 数据传递:发送方拷贝数据到队列,接收方从队列拷贝数据出来
- 同步机制:队列空时,接收任务可以阻塞等待;队列满时,发送任务也可以阻塞等待
- 多对多:多个任务可以往同一个队列发,也可以从同一个队列收
我在项目中遇到过这样一个场景:一个 UART 接收中断服务程序(ISR)把收到的字节放进队列,然后一个处理任务从队列里取出来做协议解析。这样 ISR 只做最轻量的事,剩下的交给任务去处理,系统响应非常快。
队列的典型使用场景
- 数据流处理:比如传感器数据采集、网络数据包收发
- 任务解耦:生产者和消费者模式,双方不需要知道对方的存在
- 中断与任务通信:ISR 中不能阻塞,但可以往队列里放数据
代码示例:创建和使用队列
// 定义队列句柄
QueueHandle_t xDataQueue;
// 创建队列,每个元素 4 字节(一个 int),队列深度 10
xDataQueue = xQueueCreate(10, sizeof(int));
// 发送任务:往队列里放数据
int data = 100;
xQueueSend(xDataQueue, &data, portMAX_DELAY);
// 接收任务:从队列里取数据
int received;
if (xQueueReceive(xDataQueue, &received, pdMS_TO_TICKS(100)) == pdPASS) {
// 成功收到数据
printf("Received: %d\n", received);
} else {
// 超时,没收到数据
printf("Timeout\n");
}
我的经验:队列深度一定要根据实际数据峰值来估算。我曾经因为队列设得太浅,导致高频数据丢失,查了两天才找到原因。建议留出 20%-30% 的余量。
3.2 信号量(Semaphore)—— 资源的“计数器”
信号量本质上是一个非负整数计数器。它用来管理有限数量的资源访问权。
你想想看,一个停车场有 10 个车位。每进来一辆车,计数器减 1;每出去一辆车,计数器加 1。当计数器为 0 时,后来的车就得等着。这就是信号量的工作原理。
两种信号量
| 类型 | 初始值 | 典型用途 |
|---|---|---|
| 二值信号量 | 0 或 1 | 任务同步、中断与任务同步 |
| 计数信号量 | N(N > 1) | 管理多个相同资源(如内存池、缓冲区) |
信号量的典型使用场景
- 任务同步:任务 A 做完某件事后,通过信号量通知任务 B 继续执行
- 中断同步:ISR 中释放信号量,任务中获取信号量,实现“中断下半部”处理
- 资源计数:管理有限数量的硬件资源,比如 DMA 通道、硬件定时器
注意:信号量不适合保护共享资源!因为它没有“优先级继承”机制,容易导致优先级反转问题。保护共享资源请用互斥量。
3.3 互斥量(Mutex)—— 资源的“钥匙”
互斥量是二值信号量的“升级版”。它专门用来保护共享资源,防止多个任务同时访问。
为什么不用二值信号量?因为互斥量多了一个关键特性:优先级继承。
优先级反转问题
假设有三个任务:高优先级任务 H、中优先级任务 M、低优先级任务 L。L 先拿到了互斥量,然后 H 来了想拿,但拿不到,只能阻塞等待。这时候 M 开始运行,它不需要互斥量,但它抢占了 L 的 CPU 时间,导致 L 无法释放互斥量。结果就是:H 被 M 间接阻塞了,优先级反转发生了。
互斥量的优先级继承机制能解决这个问题:当 H 被 L 阻塞时,系统会临时把 L 的优先级提升到 H 的级别,这样 M 就无法抢占 L,L 能尽快释放互斥量。
互斥量的核心特性:
- 优先级继承:防止优先级反转
- 递归锁定:同一个任务可以多次获取同一个互斥量(需要相同次数的释放)
- 只能用于任务上下文,不能在 ISR 中使用
互斥量的典型使用场景
- 保护全局变量:多个任务读写同一个数据结构
- 保护硬件外设:比如 SPI、I2C 总线,同一时刻只能一个任务使用
- 保护内存分配:多个任务共享同一个内存池
代码示例:使用互斥量保护共享资源
// 定义互斥量句柄
SemaphoreHandle_t xMutex;
// 创建互斥量
xMutex = xSemaphoreCreateMutex();
// 任务中访问共享资源
void vTaskFunction(void *pvParameters) {
for (;;) {
// 获取互斥量
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdPASS) {
// 访问共享资源
shared_variable++;
printf("Shared: %d\n", shared_variable);
// 释放互斥量
xSemaphoreGive(xMutex);
}
}
}
我的习惯:获取互斥量后,尽量在最短时间内释放。不要在持有互斥量时调用可能阻塞的函数,比如延时、等待队列等。否则容易造成死锁。
3.4 三者对比与选择
| 特性 | 队列 | 信号量 | 互斥量 |
|---|---|---|---|
| 主要用途 | 数据传递 | 资源计数/同步 | 保护共享资源 |
| 数据传递 | 支持(拷贝数据) | 不支持 | 不支持 |
| 优先级继承 | 无 | 无 | 有 |
| ISR 中使用 | 可以(发送) | 可以(释放) | 不可以 |
| 递归锁定 | 无 | 无 | 有 |
| 典型场景 | 生产者-消费者 | 任务同步 | 临界区保护 |
选择其实很简单:
- 要传数据?用队列。
- 要同步任务或计数资源?用信号量。
- 要保护共享资源?用互斥量。
我曾经踩过的坑:在 ISR 里用了互斥量,结果系统直接死机。互斥量只能在任务上下文中使用,ISR 里只能用信号量或队列的发送接口。这个错误很隐蔽,调试时一定要留意上下文环境。
3.5 知识体系图
下面这张图展示了三种通信机制在 RTOS 中的位置和关系:
这张图把三种机制放在一起对比。你可以看到,任务 A、B、C 通过不同的通信机制交互。队列负责数据流动,信号量负责同步信号,互斥量负责资源保护。各司其职,互不干扰。
我的建议:刚开始学 RTOS 时,先熟练掌握队列和信号量。互斥量虽然重要,但用不好容易引入死锁。等你对任务调度有了更深的理解,再深入使用互斥量。
好了,这一章的内容就到这里。三种通信机制的原理和用法都讲清楚了,剩下的就是在实际项目中多练多用。记住,没有最好的机制,只有最合适的场景。