本节摘要:通信机制解决「数据如何在执行流之间安全移动」。消息队列是其中的核心构件:固定槽位、按值拷贝、阻塞收发。本节讲透队列的运作与收发范式,展开满队列、指针传递、最新值模式三个关键决策,并用一条 CAN 接收管线做完整演示。
给通信机制下定义前先划清边界:同步原语(4.1 节)传递的是「许可与通知」,本身不承载数据;通信机制传递的是数据本身,同时顺带完成了同步——收方拿到的每个数据都自带「它确实存在」的保证。RTOS 里这个角色几乎总由消息队列扮演:一段固定槽位的环形缓冲,外加两条等待队列(4.2 节开头描述过的那对列表节点),构成任务与任务、中断与任务之间最常用的数据通道。
使用队列前必须接受它的三个语义。第一,按值拷贝:发送时把数据逐字节复制进槽位,接收时再复制出来,因此队列里的数据永远是独立的快照,收发双方不必担心对方改写——代价是大结构体的拷贝开销,指针传递的变式后文专门讨论。第二,先进先出:槽位按环形顺序使用,写指针追着读指针跑,多数实时场景正需要这种公平顺序。第三,满写空读可阻塞:写方遇满队列可选择阻塞、放弃或覆盖,读方遇空队列可选择阻塞、超时或空手返回,每个队列创建时定好槽位深度与条目长度。
/* 创建:深度十槽,每槽一条报文结构 */ xCanQueue = xQueueCreate(10, sizeof(CanMsg)); /* 中断侧投递:不放就放弃,绝不等待 */ void CAN_IRQHandler(void) { CanMsg msg; BaseType_t xHigher = pdFALSE; if (CAN_ReadFifo(&msg) == CAN_OK) { xQueueSendFromISR(xCanQueue, &msg, &xHigher); } portYIELD_FROM_ISR(xHigher); } /* 任务侧消费:永久等待,醒来即有货 */ void vCanTask(void *arg) { CanMsg msg; for (;;) { xQueueReceive(xCanQueue, &msg, portMAX_DELAY); vCanDispatch(&msg); } }
这段管线是嵌入式里出现频率最高的代码形态之一,值得整段记住:中断只做「取硬件数据加投递」两件事,解析与分发全在任务侧完成。

队列满不是异常,而是设计决策点,三种策略各有明确适用面。阻塞等待:写方排队等空位,保证不丢数据,适合日志这类「宁可等不可丢」的流。放弃新数据:中断侧管线的默认选择——ISR 不允许阻塞,放不下就丢弃并计数,适合传感器流这类「新的比旧的重要」。覆盖旧数据:特殊模式下写入总是成功、挤掉最老条目,适合「只需最新值」的场景,比如任务只需知道当前温度。第三种配合「读而不取」的窥视接口,就是常说的邮箱模式。无论选哪种,请在设计文档里写明丢弃策略与计数位置——联调时「报文计数对不上」能不能快速定位,取决于今天有没有记下这句话。
按值拷贝对几十字节的报文很合适,对一整帧图像就是灾难:两轮内存搬运把缓存与总线带宽全部点燃。正确姿势是静态池发筹码、队列传指针:从固定分块池里取一块,把数据写进去,把指针塞进队列;收方用完必须原路归还。池保证每块内存有主、无碎片(5.1 节展开),队列保证指针传递的原子性与顺序性,两个机制各干各的专业活。唯一的纪律是归还时机:收方处理完立即归还,持有时间过长会让池耗尽、上游阻塞——这个模式的生命线就是「快用快还」。
背景。某充电桩控制板的 CAN 总线上挂着电表与计费模块,报文速率正常时每秒五十帧,计费高峰时突发到每秒三百帧,控制任务还要求任何一帧的处理延迟不超过二十毫秒。
操作。工程师把队列深度定为十六槽、条目为报文结构,中断侧投递失败即丢弃并让计数器加一;消费任务优先级设为中档,收到后先查报文类型再分发,单帧处理最坏耗时两毫秒。同时在任务里维护「队列当前水位」统计,每秒通过诊断口上报一次。
结果。正常速率下水位不超过二;突发时水位冲到十一,峰值延迟九毫秒,仍在二十毫秒红线内;一次异常设备连发导致水位触顶,丢弃计数开始增长,但处理延迟始终稳定——溢出被隔离在队列入口,没有传导到处理时延上。
解读。这个案例给出深度设计的方法:队列深度不是拍脑袋的整数,而是「突发速率 × 允许的缓冲时长」的乘积再留余量,十六槽约等于「三百帧每秒的突发里缓冲五十毫秒」。水位统计把队列从黑盒变成可观测对象,容量规划从此有据可依。丢弃计数则定义了系统的失效形态:宁可丢帧,不可延迟——这正是实时系统的经典取舍在通信层的落点。
变式。若需求改为「任何帧都不许丢」,工程师需要把丢弃改为阻塞投递,同时核算消费任务的最坏吞吐:三百帧每秒乘以两毫秒等于零点六的利用率,加上自身周期负载后重新过一遍 3.2 节的两道闸门——通信参数的修改最终又回到了调度分析。若报文结构扩到上百字节,则应切换到静态池加指针传递的变式,并把池深与队列深度一起纳入水位监控。
问:队列条目尺寸怎么定? 恰好装下对象且对齐到总线宽度,避免为一个字节多搬四个。结构体尺寸受编译器对齐影响,用 sizeof 而不是手算。大结构体直接换本节的指针模式,别在队列里搬图像。
问:一个队列多个消费者会怎样? 每条数据只被一个消费者取走,天然形成负载分派——这正是「工作队列」模式的实现方式。需要广播语义(每个消费者都拿到同一份数据)时,改用多个队列或事件组,别指望单个队列广播。
问:队列能在创建后改深度吗? 常见内核不支持运行期改深,深度是创建期契约。水位统计显示长期顶格,正确的响应是重新设计深度并回归验证,而不是幻想运行期扩容。
问:调试时怎么快速看队列内容? 水位查询加关键位置快照是第一步;部分跟踪工具能录制队列收发事件流。直接偷看缓冲区内容要小心并发——快照最好在持有相关锁或暂停调度时进行。