cFS与SB学习记录(四):ReceiveBuffer返回以后,这块内存归谁

第一次只看接口时,CFE_SB_ReceiveBuffer() 很像“从队列拿一块内存,用完再释放”。源码里的行为却有一个很重要的差别:应用拿到的是 SB 管理的借用指针,管道会记住这次交出的缓冲区,下一次接收时再放下前一次引用。

如果这一点没想清楚,很容易把接收指针丢给另一个线程,或者调用名字看起来很合适的 CFE_SB_ReleaseMessageBuffer()。这两种写法都需要重新检查所有权。

系列目录 · 上一篇:发送过程

一个普通的接收循环

应用通常在循环里等待自己的管道。下面的 process_message() 只是表示“在本次循环中消费数据”,不是 cFE API:

1
2
3
4
5
6
7
8
9
10
11
for (;;) {
CFE_SB_Buffer_t *buffer = NULL;
CFE_Status_t status;

status = CFE_SB_ReceiveBuffer(&buffer, pipe,
CFE_SB_PEND_FOREVER);
if (status != CFE_SUCCESS)
break;

process_message(buffer); /* 在下一次接收之前完成 */
}

buffer 的类型没有 const,不表示接收者可以随意写。源码在公共入口特意说明这是兼容性保留,调用者应把返回内容当只读对象使用。多个订阅者可能正在读同一块底层消息,修改它会影响别人。

这个循环背后的隐含协议是:再次对同一管道调用接收,表示上一次返回的数据已经处理完。

旧消息在等待新消息之前就可能失效

接收事务首先设置超时并选择管道。在 CFE_SB_ReceiveTxn_SetPipeId() 中,有这样一段逻辑:

1
2
3
4
if (PipeDscPtr->LastBuffer != NULL) {
CFE_SB_DecrBufUseCnt(PipeDscPtr->LastBuffer);
PipeDscPtr->LastBuffer = NULL;
}

这是在真正等待新队列数据之前执行的。也就是说,第二次接收调用后来即使超时,第一次的指针也不能继续按原来的借用关系使用。

1
2
3
4
第一次 ReceiveBuffer → 得到消息 A
处理 A
第二次 ReceiveBuffer 开始 → 放下 A 的管道引用
等待消息 B……

“等到收到 B,A 才失效”这个说法还不够准确。应用应该把有效期边界放在下一次接收开始。删除管道等清理操作也会释放相应引用,因此不能让其他任务在清理后继续访问这个指针。

OSAL 出队的是描述符地址

发送端把描述符指针放进队列,接收端就按同样大小取出:

1
2
OS_QueueGet(queue_id, &descriptor,
sizeof(descriptor), &copied_size, timeout);

SB 检查 OSAL 是否成功、描述符是否非空,以及实际取得的条目大小是否等于指针大小。这里检查的是队列条目的形状,消息本身的语义仍要由后续 MSG 验证和应用分发处理。

如果取得有效描述符,CFE_SB_ReceiveTxn_ExportReference() 重新加锁,确认管道仍有效,然后做两件互相对应的事:给 LastBuffer 建立一个新引用,撤掉原来由队列持有的引用。调用者拿到的内容指针借用了 LastBuffer 那份引用,并没有再得到一份可以独立释放的所有权。

1
2
3
4
队列持有描述符引用
→ 出队
→ 管道 LastBuffer 持有引用
→ 应用临时借用 Content

这一转换期间引用不能先降到零,否则内存可能在交给应用前就被回收。源码先建立 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
2
3
4
5
6
7
8
9
10
11
12
CFE_SB_Buffer_t *buffer;

buffer = CFE_SB_AllocateMessageBuffer(message_size);
if (buffer == NULL) {
/* 池容量或请求大小等问题 */
return;
}

/* 在 buffer 中初始化消息头、填写内容 */
/* 初始化阶段放弃发送时,ReleaseMessageBuffer(buffer) */

/* 成功传给 TransmitBuffer 后,应用不再访问 buffer */

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

作者

GoKo Mell

发布于

2026-10-23

更新于

2026-10-23

许可协议

评论