4. 任务间通信:队列、信号量、互斥量、事件组的使用场景与性能对比
各位同学,咱们今天聊点实在的。
任务间通信,说白了就是让多个任务能「好好说话」。你想想看,一个车载系统里,传感器采集、控制算法、显示刷新,几十个任务同时跑。它们之间怎么交换数据?怎么同步动作?这就是我们今天要啃的硬骨头。
我个人习惯把任务间通信分成两类:数据传递和事件通知。队列负责前者,信号量、互斥量、事件组负责后者。但实际用起来,边界没那么清晰。嗯,咱们一个一个说。
4.1 队列:最可靠的数据搬运工
队列,我愿称之为RTOS里的「快递驿站」。一个任务往里放数据,另一个任务取数据。先进先出,秩序井然。
核心特点:
- 支持多任务同时读写(但内部有锁保护)
- 数据是拷贝传递,不是指针传递
- 可以设置超时等待
我在项目中遇到过一个问题:两个任务通过全局数组传数据,结果数据被覆盖了。查了两天,最后换成队列,一行代码解决。为什么?因为队列自带互斥机制,你不用操心竞争问题。
// 创建队列,每个元素32字节,深度10
QueueHandle_t xQueue = xQueueCreate(10, sizeof(uint32_t));
// 发送任务
uint32_t data = 100;
xQueueSend(xQueue, &data, portMAX_DELAY);
// 接收任务
uint32_t received;
xQueueReceive(xQueue, &received, pdMS_TO_TICKS(100));
这里有个坑:队列深度别设太大。我曾经在一个项目里设了256的深度,结果内存爆了。你想想看,每个元素32字节,256个就是8KB。在资源紧张的车载MCU上,这可不是小数目。
我的建议:队列深度设为最大可能积压量的1.5倍。别贪多,够用就行。
4.2 信号量:轻量级的「好了叫我」
信号量,说白了就是个计数器。它不传数据,只传「有没有」这个状态。
我记得刚做车载那会儿,总把信号量和队列搞混。后来想通了:队列传的是「什么」,信号量传的是「有没有」。
信号量分两种:
- 二值信号量:只有0和1,适合做同步。比如中断通知任务去处理数据。
- 计数信号量:可以累加,适合管理资源。比如控制5个UART端口的访问。
性能对比:信号量的开销比队列小得多。为什么?因为它不需要拷贝数据。在FreeRTOS里,信号量的操作只需要几十个指令周期,而队列需要几百个。
// 二值信号量示例
SemaphoreHandle_t xSemaphore = xSemaphoreCreateBinary();
// 中断中给出信号量
void ISR_Handler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// 任务中等待信号量
void Task_Handler(void *pvParameters) {
while(1) {
if(xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
// 处理数据
}
}
}
我曾经犯过一个错:在中断里用xSemaphoreGive而不是xSemaphoreGiveFromISR。结果系统随机死机。查了三天,最后发现是中断上下文调用了非中断安全的API。嗯,这个坑你们别踩。
注意:中断服务函数中只能调用带FromISR后缀的API。这是RTOS的铁律,没得商量。
4.3 互斥量:带优先级继承的信号量
互斥量,很多人觉得它就是二值信号量。其实不然。
互斥量有个独门绝技:优先级继承。什么意思?当一个低优先级任务持有互斥量,高优先级任务在等它时,系统会临时提升低优先级任务的优先级。这样能防止优先级反转。
我在一个车载项目中遇到过优先级反转的问题:一个中等优先级的任务抢占了CPU,导致高优先级任务迟迟拿不到互斥量。最后系统响应超时,客户投诉。后来换成互斥量,问题解决。
什么时候用互斥量?
- 多个任务访问共享资源(如全局变量、外设寄存器)
- 任务优先级不同,且存在优先级反转风险
- 需要递归加锁的场景(互斥量支持递归)
// 创建互斥量
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
// 访问共享资源
void WriteToUART(char *data) {
if(xSemaphoreTake(xMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
// 安全地操作UART
UART_Send(data);
xSemaphoreGive(xMutex);
} else {
// 超时处理
LogError("UART mutex timeout");
}
}
但互斥量也有代价。它的优先级继承机制需要额外的内核开销。在低优先级任务很少的场景下,用二值信号量反而更高效。
我的经验:如果只有两个任务竞争资源,且优先级相同,用二值信号量就够了。互斥量不是万能的,别滥用。
4.4 事件组:一对多的通知利器
事件组,我愿称之为「信号量的升级版」。它用位来表示事件,一个事件组可以管理最多24个事件(在FreeRTOS里)。
你想想看,一个任务要等三个条件都满足才能执行:传感器数据就绪、CAN总线空闲、定时器到点。用信号量你得等三个,用事件组一个就够了。
事件组的优势:
- 一次等待多个事件
- 支持「与」和「或」两种等待模式
- 多个任务可以等待同一个事件组
// 创建事件组
EventGroupHandle_t xEventGroup = xEventGroupCreate();
#define BIT_SENSOR_READY (1 << 0)
#define BIT_CAN_IDLE (1 << 1)
#define BIT_TIMER_EXPIRED (1 << 2)
// 等待所有事件
EventBits_t uxBits = xEventGroupWaitBits(
xEventGroup,
BIT_SENSOR_READY | BIT_CAN_IDLE | BIT_TIMER_EXPIRED,
pdTRUE, // 退出时清除位
pdTRUE, // 等待所有位(与模式)
portMAX_DELAY
);
if((uxBits & (BIT_SENSOR_READY | BIT_CAN_IDLE | BIT_TIMER_EXPIRED)) ==
(BIT_SENSOR_READY | BIT_CAN_IDLE | BIT_TIMER_EXPIRED)) {
// 三个条件都满足,开始执行
}
事件组的性能介于信号量和队列之间。它比信号量灵活,但比队列轻量。我一般用它来做多条件触发,比如启动自检、模式切换。
注意:事件组的位宽有限。FreeRTOS里是24位,别超了。另外,多个任务同时等待同一个事件组时,要注意事件被谁消费了。
4.5 性能对比:选哪个?
好了,四种机制都讲完了。咱们做个对比,方便你选型。
| 通信机制 | 数据传递 | 开销(指令周期) | 适用场景 | 避坑点 |
|---|---|---|---|---|
| 队列 | 是(拷贝) | 200-500 | 数据流、消息传递 | 深度别太大,注意内存 |
| 二值信号量 | 否 | 30-80 | 中断同步、简单通知 | 中断中必须用FromISR |
| 计数信号量 | 否 | 40-100 | 资源计数、多实例管理 | 初始值别设错 |
| 互斥量 | 否 | 50-150 | 共享资源保护、优先级反转 | 不要中断中使用 |
| 事件组 | 否 | 60-120 | 多条件等待、状态同步 | 位宽有限,注意事件消费 |
这些数据是我在Cortex-M4上实测的,不同平台会有差异。但趋势是一致的:信号量最快,队列最慢。
选型口诀:
- 传数据 → 队列
- 做同步 → 信号量
- 保护资源 → 互斥量
- 多条件等待 → 事件组
最后说一句:别为了炫技用复杂的机制。能用信号量解决的,别上互斥量。能用队列解决的,别搞事件组。简单就是美,尤其是在车载这种对可靠性要求极高的领域。
好了,这一章就到这儿。下一章咱们聊聊任务优先级的设计,那也是个容易翻车的地方。