cFS与SB学习记录(四):ReceiveBuffer返回以后,这块内存归谁
第一次只看接口时,CFE_SB_ReceiveBuffer() 很像“从队列拿一块内存,用完再释放”。源码里的行为却有一个很重要的差别:应用拿到的是 SB 管理的借用指针,管道会记住这次交出的缓冲区,下一次接收时再放下前一次引用。
如果这一点没想清楚,很容易把接收指针丢给另一个线程,或者调用名字看起来很合适的 CFE_SB_ReleaseMessageBuffer()。这两种写法都需要重新检查所有权。
一个普通的接收循环
应用通常在循环里等待自己的管道。下面的 process_message() 只是表示“在本次循环中消费数据”,不是 cFE API:
1 | |
buffer 的类型没有 const,不表示接收者可以随意写。源码在公共入口特意说明这是兼容性保留,调用者应把返回内容当只读对象使用。多个订阅者可能正在读同一块底层消息,修改它会影响别人。
这个循环背后的隐含协议是:再次对同一管道调用接收,表示上一次返回的数据已经处理完。
旧消息在等待新消息之前就可能失效
接收事务首先设置超时并选择管道。在 CFE_SB_ReceiveTxn_SetPipeId() 中,有这样一段逻辑:
1 | |
这是在真正等待新队列数据之前执行的。也就是说,第二次接收调用后来即使超时,第一次的指针也不能继续按原来的借用关系使用。
1 | |
“等到收到 B,A 才失效”这个说法还不够准确。应用应该把有效期边界放在下一次接收开始。删除管道等清理操作也会释放相应引用,因此不能让其他任务在清理后继续访问这个指针。
OSAL 出队的是描述符地址
发送端把描述符指针放进队列,接收端就按同样大小取出:
1 | |
SB 检查 OSAL 是否成功、描述符是否非空,以及实际取得的条目大小是否等于指针大小。这里检查的是队列条目的形状,消息本身的语义仍要由后续 MSG 验证和应用分发处理。
如果取得有效描述符,CFE_SB_ReceiveTxn_ExportReference() 重新加锁,确认管道仍有效,然后做两件互相对应的事:给 LastBuffer 建立一个新引用,撤掉原来由队列持有的引用。调用者拿到的内容指针借用了 LastBuffer 那份引用,并没有再得到一份可以独立释放的所有权。
1 | |
这一转换期间引用不能先降到零,否则内存可能在交给应用前就被回收。源码先建立 LastBuffer 引用,再放下队列引用,正好保证了中间没有空档。
继续算完两个订阅者的例子
上一篇结束时,发送方已经放下引用,A/B 两条队列各持有一个,总计数为 2。
| 动作 | 计数变化 | 结果 |
|---|---|---|
A 出队,建立 LastBuffer,撤队列引用 |
先 +1,再 -1 | 仍为 2 |
| B 同样完成接收 | 先 +1,再 -1 | 仍为 2 |
| A 开始下一次接收 | -1 | 剩 B 保持,计数为 1 |
| B 开始下一次接收或删除管道 | -1 | 计数为 0,归还池 |
这里有三个不同数量:消息内容只占一个缓冲区,两个接收者各有一份使用关系,两条队列还各自统计排队深度。不能把“两个接收者”“两个队列条目”和“两块消息内存”当成同一件事。
如果 A 已经处理完但暂时没有再次接收,它的 LastBuffer 引用仍存在。只看队列为空,无法推出消息池中所有相关缓冲区都已归还。
为什么同一个管道不适合直接挂一组 worker
假设 worker 1 从管道拿到 A 后开始处理,worker 2 紧接着对同一 PipeId 接收。第二次调用会释放这个管道的前一次 LastBuffer,但 worker 1 可能仍在读 A。
这不是给 process_message() 加一把局部锁就自然解决的问题。根源是此版本把借用期绑定到“管道的下一次接收”,而不是绑定到某个 worker 手中的独立引用。
如果处理需要异步延续,可以在有效期内把必要字段复制到自己管理的工作对象,再交给其他任务;或者让各接收任务使用独立管道和订阅关系。两种做法的分发语义不同:两个管道都订阅同一 MsgId,会各收到一份投递,不能直接当作竞争消费的 worker 队列。
三种等待方式怎样返回
TimeOut |
行为 | 没取到消息的常见结果 |
|---|---|---|
CFE_SB_POLL |
立即检查 | CFE_SB_NO_MESSAGE |
| 大于 0 的毫秒数 | 有限等待 | CFE_SB_TIME_OUT |
CFE_SB_PEND_FOREVER |
持续等待 | 到消息或发生实际错误 |
“暂时没有消息”和“管道坏了”不应走完全相同的处理分支。使用轮询的程序若把 NO_MESSAGE 当致命错误,会在队列第一次为空时退出。
有限等待还涉及一条容易漏看的实现细节:事务把相对等待时间转成绝对截止时间,后续需要继续尝试时再计算剩余时间,避免每次内部重试都重新获得完整的等待时长。移植中的时钟实现和 OSAL 超时语义因此也会影响 SB 接收。
接收成功后,还会经过 MSG 验证 hook
接收事务会调用 CFE_MSG_VerificationAction()。若具体实现拒绝消息,SB 记录事件、放下这块缓冲区引用,再继续尝试接收。
不过接口存在,并不表示当前构建真的做了 CRC 验证。我这份 EDSMSG 的 VerificationAction() 对正常参数直接设置 IsAcceptable=true,是一个可扩展入口。不能因为函数名带 Verification,就把一次 SB 接收写成“完成 EDS 全包解码和 CRC 检查”。
应用收到后仍要按自己的协议检查消息身份、长度和业务字段,避免将任意数据直接强转为预期结构。
“零拷贝发送”是另一份所有权合同
SB 还允许发布者先申请内部消息缓冲区:
1 | |
AllocateMessageBuffer() 得到的缓冲区记录了发布者 AppId,并挂到零拷贝跟踪链。应用直接在这块内存中构造消息,成功调用 CFE_SB_TransmitBuffer() 后,SB 接管广播和回收,应用不能再读写或释放它。
这条路径省掉的是普通 TransmitMsg() 中“用户缓冲区 → SB 缓冲区”的一次复制。它要求应用更认真地处理长度、初始化失败和所有权转移。公共 API 对失败时的所有权也有约定;扩展 MSG hook 或修改发送流程时,还应核对具体失败发生在转交前还是后。
我把三个容易混淆的指针来源单独记下来:
| 指针来源 | 调用者怎样使用 | 回收方式 |
|---|---|---|
应用自己的对象,供 TransmitMsg 读取 |
调用期间保持有效;返回后可复用原对象 | 应用自行管理原对象 |
AllocateMessageBuffer 返回 |
发布者构造消息;成功转交后停止访问 | 转交前放弃用 ReleaseMessageBuffer;转交后由 SB 管理 |
ReceiveBuffer 返回 |
只读借用,下一次同管道接收前完成使用 | 由管道后续接收或清理放下引用 |
因此 ReleaseMessageBuffer() 不是通用的“释放收到的消息”函数。它处理的是尚未被发送消费的发布者零拷贝缓冲区。把它用于接收借用对象,违背了对象来源和所有权约定。
这条内存主线读通以后,UseCount 不再只是一个加减数字:发送调用、队列和 LastBuffer 分别在不同时刻持有引用,归零才会返回分级池。实际看泄漏或提前释放时,我会逐个找这些持有关系,而不是仅仅搜索哪个函数叫 free。
相关代码在 SB 公共接口、cfe_sb_priv.c及 cfe_sb_buf.c。下一篇:SBR 路由。
cFS与SB学习记录(四):ReceiveBuffer返回以后,这块内存归谁
https://goko-son626.github.io/post/cfs-sb-03-receive-buffers.html

