队列机制详解:队列的创建、发送、接收与删除
队列这东西,说白了就是任务间传递数据的“管道”。
我刚开始用RTOS那会儿,总觉得队列不就是个数组嘛,有啥好讲的?后来踩了坑才明白——队列远不止“放数据、取数据”这么简单。它背后牵涉到任务调度、临界区保护、内存管理一堆事儿。
今天咱们就把队列的四个核心操作掰开揉碎讲清楚:创建、发送、接收、删除。
1. 队列的创建——你得先挖个坑
创建队列,就像在系统里挖一个蓄水池。你得告诉RTOS:这个池子多大?能存多少条消息?每条消息多长?
以FreeRTOS为例,创建队列的API长这样:
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
- uxQueueLength:队列能容纳的消息数量。比如设成5,最多存5条。
- uxItemSize:每条消息的字节数。注意,这里是拷贝数据,不是传指针。
我习惯把队列长度设成2的幂次,比如4、8、16。为什么?因为有些RTOS内部会用位运算做取模,效率高那么一丢丢。当然,这不是必须的,算是个小癖好。
创建成功后,返回一个队列句柄。这个句柄就是你的“通行证”,后面所有操作都靠它。
举个例子:
QueueHandle_t xMsgQueue;
xMsgQueue = xQueueCreate( 10, sizeof( uint32_t ) );
if( xMsgQueue == NULL )
{
// 创建失败,多半是内存不够
// 我遇到过这种情况:堆设小了,队列一多就崩
}
2. 队列的发送——把数据扔进去
发送数据,就是把消息塞进队列。RTOS会帮你把数据拷贝到队列内部缓冲区。
发送API有两种:
- xQueueSend():往队列尾部发送。最常用。
- xQueueSendToFront():插队,塞到队列头部。优先级高的消息可以用这个。
原型长这样:
BaseType_t xQueueSend( QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait );
第三个参数是等待时间。如果队列满了,任务可以选择等一会儿,或者直接返回。
我见过有人把1KB的传感器数据直接往队列里塞,结果CPU一半时间都在拷贝数据。后来改成传指针,问题就解决了。
发送的返回值:
| 返回值 | 含义 |
|---|---|
| pdPASS | 发送成功 |
| errQUEUE_FULL | 队列满了,发送失败 |
代码示例:
uint32_t ulData = 100;
BaseType_t xResult;
xResult = xQueueSend( xMsgQueue, &ulData, 0 );
if( xResult != pdPASS )
{
// 队列满了,数据丢了
// 我曾经在这里吃过亏:中断里发数据,忘了检查返回值
}
3. 队列的接收——把数据掏出来
接收数据,就是从队列里取出一条消息。如果队列是空的,任务可以选择阻塞等待。
接收API:
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * pvBuffer,
TickType_t xTicksToWait );
- pvBuffer:接收缓冲区,用来存放取出的数据。
- xTicksToWait:最大等待时间。设成portMAX_DELAY就是死等。
接收成功后,数据从队列里移除。这一点和邮箱不同——邮箱是覆盖式存储,队列是先进先出。
接收示例:
uint32_t ulReceivedData;
if( xQueueReceive( xMsgQueue, &ulReceivedData, pdMS_TO_TICKS( 100 ) ) == pdPASS )
{
// 成功收到数据
printf("收到数据: %lu\n", ulReceivedData);
}
else
{
// 等了100ms还没数据,超时了
}
还有个变种叫xQueuePeek(),它只“看一眼”队列头部的数据,但不移除。我偶尔用它做调试,或者做“预览”功能。
4. 队列的删除——清理战场
队列用完了,得删掉。不然内存就泄漏了。
删除API很简单:
void vQueueDelete( QueueHandle_t xQueue );
调用后,RTOS会释放队列占用的内存。注意:
- 删除前,确保没有任务在等待这个队列。否则那些任务会一直阻塞,变成“僵尸任务”。
- 删除后,队列句柄就失效了。再使用就是野指针操作。
代码:
vQueueDelete( xMsgQueue );
xMsgQueue = NULL; // 防止误用
5. 避坑指南——我踩过的雷
队列用起来简单,但坑也不少。分享几个我亲身经历过的:
- 中断里发数据,忘了检查返回值。队列满了怎么办?数据丢了,系统行为就不可控了。后来我改成在中断里用
xQueueSendFromISR(),并且检查返回值,满了就丢弃旧数据。 - 队列长度设得太小。高负载时,发送方频繁失败。我一般根据数据产生速率和消费速率,留出20%~30%的余量。
- 消息体太大。拷贝开销高,还占内存。建议消息体控制在几十字节以内。大的数据块,传指针就好。
- 多个任务同时操作同一个队列。RTOS内部会保护,但如果你在中断和任务间共享队列,记得用中断安全版本API。
6. 小结
队列是RTOS里最常用的通信手段。创建时想好长度和消息大小,发送时检查返回值,接收时注意缓冲区大小,删除时清理干净。
嗯,掌握了这四个操作,队列这块基本就稳了。下一章咱们聊聊邮箱——它和队列有啥区别?什么时候该用邮箱而不是队列?到时候见。