4、任务间通信:队列(queue)原理与使用、队列集、队列的阻塞机制
好,咱们今天聊聊任务间通信。说白了,在FreeRTOS里,任务之间怎么传数据?最常用的就是队列(Queue)。
我刚开始用FreeRTOS那会儿,总觉得队列就是个“放数据、取数据”的容器。后来踩了坑才明白——队列的核心价值,其实是解耦。发送任务不用管谁在收,接收任务也不用管谁在发。两个任务各干各的,通过队列这个“信箱”来沟通。
4.1 队列的基本原理
队列本质上是一个环形缓冲区。每个队列都有固定的长度和固定的数据单元大小。你往队列里放数据,它会把数据拷贝进去;你从队列里取数据,它会把数据拷贝出来。
嗯,这里要注意:是拷贝,不是传指针。如果你传的是大结构体,每次拷贝的开销可不小。我个人习惯是,传指针而不是传整个数据块。比如传一个指向结构体的指针,队列里只存4字节的地址值,效率高很多。
核心数据结构:
- 存储区:一块连续内存,用来存放消息
- 读指针:指向下一个要取出的消息位置
- 写指针:指向下一个要写入的消息位置
- 消息计数:当前队列中有多少条消息
- 等待列表:记录哪些任务在等队列可写/可读
4.2 队列的创建与使用
创建队列用 xQueueCreate()。两个参数:队列长度、每个数据单元的大小。
// 创建一个能存放5个int的队列
QueueHandle_t xQueue;
xQueue = xQueueCreate(5, sizeof(int));
if (xQueue == NULL) {
// 创建失败,通常是内存不够
// 我建议你在这里打日志,方便调试
}
发送数据用 xQueueSend() 或 xQueueSendFromISR()。接收数据用 xQueueReceive() 或 xQueueReceiveFromISR()。
// 任务A:发送数据
void TaskA(void *pvParameters) {
int data = 100;
while(1) {
xQueueSend(xQueue, &data, portMAX_DELAY);
data++;
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// 任务B:接收数据
void TaskB(void *pvParameters) {
int received;
while(1) {
if (xQueueReceive(xQueue, &received, portMAX_DELAY) == pdTRUE) {
// 处理接收到的数据
printf("收到: %d\n", received);
}
}
}
我在项目中遇到过一个问题:发送任务往队列里塞数据,接收任务却一直收不到。查了半天,发现是队列创建时数据单元大小设错了。我传的是结构体指针,但队列单元大小设成了结构体本身的大小。嗯,这种低级错误,犯过一次就记住了。
4.3 队列的阻塞机制
这是队列最强大的地方。你想想看,如果队列空了,接收任务调用 xQueueReceive() 会发生什么?
答案是:任务可以阻塞等待。第三个参数就是阻塞时间,单位是tick。
- portMAX_DELAY:一直等,直到有数据
- 0:不等待,立即返回
- 其他值:最多等这么多tick
为什么会阻塞?因为FreeRTOS把任务放到了队列的“等待读取列表”里。当有数据写入时,系统会检查这个列表,如果有任务在等,就把数据直接拷贝给那个任务,然后唤醒它。
我的经验:阻塞时间别设太长。如果接收任务优先级高,设portMAX_DELAY没问题。但如果优先级低,长时间阻塞可能导致其他任务饿死。我一般设个100~500ms的超时,配合错误处理,系统更健壮。
4.4 队列集(Queue Set)
当你的任务需要从多个队列接收数据时,怎么办?一个个轮询?太浪费CPU了。
FreeRTOS提供了队列集。你可以把多个队列注册到一个队列集里。然后任务只需要等待这个队列集,哪个队列有数据,它就会告诉你。
// 创建队列集,最多支持3个队列
QueueSetHandle_t xQueueSet = xQueueCreateSet(3);
// 创建两个队列
QueueHandle_t xQueue1 = xQueueCreate(5, sizeof(int));
QueueHandle_t xQueue2 = xQueueCreate(5, sizeof(int));
// 注册到队列集
xQueueAddToSet(xQueue1, xQueueSet);
xQueueAddToSet(xQueue2, xQueueSet);
// 任务中等待队列集
void Task(void *pvParameters) {
QueueSetMemberHandle_t xActivatedQueue;
int data;
while(1) {
// 等待队列集中任意队列有数据
xActivatedQueue = xQueueSelectFromSet(xQueueSet, portMAX_DELAY);
// 判断是哪个队列被激活
if (xActivatedQueue == xQueue1) {
xQueueReceive(xQueue1, &data, 0);
// 处理队列1的数据
} else if (xActivatedQueue == xQueue2) {
xQueueReceive(xQueue2, &data, 0);
// 处理队列2的数据
}
}
}
注意:队列集有个限制——不能同时注册队列和信号量到同一个集合。另外,从队列集取出队列句柄后,要立即读取数据,否则可能被其他任务抢走。我曾经因为这个bug,调试了整整一个下午。
4.5 队列的避坑指南
我总结几个常见问题,都是血泪教训:
- 队列溢出:队列满了还往里写,数据会丢。可以用
xQueueSend()的返回值判断是否成功。或者用xQueueOverwrite()覆盖旧数据(只适用于长度为1的队列)。 - ISR中使用队列:中断服务函数里只能用
FromISR版本的API。而且中断里不要用portMAX_DELAY阻塞,中断必须快速返回。 - 队列大小与内存:每个队列会占用
队列长度 * 数据单元大小 + 管理结构的内存。如果创建很多队列,堆可能不够用。我建议用uxQueueSpacesAvailable()监控队列使用情况。 - 优先级反转:高优先级任务等队列,低优先级任务占着队列不放,中间优先级任务抢了CPU。嗯,这个问题比较复杂,后面讲互斥量时会详细说。
4.6 实际项目中的队列设计
我参与过一个数据采集项目,需要从多个传感器读取数据,然后汇总处理。当时的设计是这样的:
| 模块 | 队列作用 | 队列长度 | 数据大小 |
|---|---|---|---|
| 传感器1任务 | 发送采集数据 | 10 | 32字节 |
| 传感器2任务 | 发送采集数据 | 10 | 32字节 |
| 汇总处理任务 | 从队列集接收 | - | - |
| 日志输出任务 | 接收日志消息 | 20 | 64字节 |
每个传感器任务有自己的队列,汇总任务通过队列集统一接收。日志队列独立,避免影响数据通路。这样设计的好处是:传感器任务只管发,汇总任务只管收,互不干扰。而且队列集让汇总任务不用轮询,省CPU。
小技巧:队列长度不要设太大。够用就行,设大了浪费内存。我一般根据数据产生速率和消费速率来估算。比如传感器每秒产生10个数据,处理任务每秒能处理20个,那队列长度设5~10就够。如果设100,纯属浪费。
最后说一句:队列是FreeRTOS里最基础、最常用的通信机制。你把它用好了,大部分任务间通信问题都能解决。下一章咱们聊聊信号量和互斥量,那又是另一番天地了。