cFS与SB学习记录(五):SBR怎样把MsgId变成目的地列表
读发送代码时,CFE_SBR_GetRouteId() 看起来只是一个查表函数。但想清楚它的输入、输出和后面那条链表,就能把“订阅为什么有用”与“发送怎样找到接收者”接起来。
SBR 是 Software Bus Routing,本文只讨论它在本地 SB 里的路由作用。它与负责网络扩展的 SBN 不是同一个模块,也不在这里计算 IP 路由或选择物理链路。
MsgId、RouteId、PipeId 各回答一个问题
我先用一张关系图代替源码中的一串类型名:
1 | |
MsgId 是消息对外使用的身份;RouteId 是 SBR 内部找到某条路由的句柄;PipeId 表示最终的接收管道。它们不是三个能互换使用的下标。
CFE_SBR_RouteId_t 被包在一个小结构体里,目的之一就是让编译器阻止不同句柄被直接混赋。本文版本把零保留为无效 RouteId,数组下标转换成 RouteId 时加 1,转换回下标时减 1。
IsValidRouteId() 做的是编码范围的基本检查,不能把名字扩展成“已经确认这个对象的所有运行状态”。读这类短 helper 时,我现在会先看函数体,再决定“有效”究竟检查了哪些条件。
两层表为什么要拆开
路由实体由 cfe_sbr_route_unsorted.c 管理。每项主要包含 MsgId、目的地链表头和序列计数器。它只需要知道“路由第几项”,不必关心外部 MsgId 的数值分布。
另一层负责 MsgId → RouteId。源码给出直接映射和哈希映射两种选择,可以让路由实体部分保持不变。
假设只使用 100 种消息,但合法 MsgId 的数值空间远大于 100。按 MsgId 直接建数组很容易查找,却可能为许多从未使用的值预留空位;把 MsgId 映射到较紧凑的路由数组,则可以把“可表示的身份数”与“实际最多支持的路由数”分开。
这里涉及两种容量:
CFE_PLATFORM_SB_HIGHEST_VALID_MSGID:当前消息身份空间的上界。CFE_PLATFORM_SB_MAX_MSG_IDS:SBR 路由实体表最多容纳多少项。
增加其中一个值,不意味着另一个限制也自动扩大。
直接映射:用空间换一个简单查找
直接映射实现的核心很直观:
1 | |
表长是最高合法 MsgId 加 1。查找时取相同下标,得到 RouteId,再进入路由实体表。它不需要处理哈希冲突,但内存占用跟消息身份空间有关。
例如一个假设配置允许 0~65535 的身份值,而最多实际登记 256 条路由。直接映射表仍需要覆盖 65536 个位置,不能按“只用了 256 条消息”估算这张映射表。这个数字只是帮助计算空间关系,不是本文任务配置的固定值。
哈希映射:先缩小范围,再比较真正的 MsgId
Linux 独立基线选择的是 cfe_sbr_map_hash.c。这份实现的映射表长度是 4 × CFE_PLATFORM_SB_MAX_MSG_IDS,并要求大小为 2 的幂,便于掩码回绕。
添加路由时先算 MsgId 的哈希位置,若该槽已使用,就向后探测,走到数组末尾再回到开头。查找同样从哈希位置开始,但看到有效 RouteId 后还要拿路由中的真实 MsgId 比较:
1 | |
哈希值相等不代表消息身份相同。若只用哈希位置返回路由,不做真实 MsgId 比较,就会在冲突时把消息交给另一组订阅者。
表比路由上限大,可以留下足够空位结束探测,但冲突仍会影响查找步数。源码在添加时返回冲突计数,供上层观察。它是一种固定容量的线性探测实现,不是可以无限增长的通用哈希容器。
一条新路由什么时候建立
订阅端先查路由,没有时才调用 CFE_SBR_AddRoute()。这版 unsorted 实现从 RouteIdxTop 指向的位置分配一项,设置 MsgId 和映射,然后把游标加一。后续对相同 MsgId 的订阅复用这条路由,只增加目的地。
一个需要特别记住的版本细节是:退订删除的是目的地节点。本文的路由实现没有相应的删除/回收 RouteEntry 流程,最后一个订阅者退出后,路由项和 MsgId 映射仍然可以保留。
如果反复订阅许多不同身份,即使当前同时活跃的订阅不多,也要考虑路由表累计分配上限。观察容量时,要把已经建立过的 MsgId 路由数和当前目的地数量分别记下来。
这一点还影响日志解释:找不到路由与找到一条目的地为空的路由是不同分支。发送代码在前者增加 NoSubscribersCounter;不能只凭这一个计数器推断所有“当前无人接收”的情况。
目的地的启停,不必删除整条路由
每个目的地有 Active 状态。关闭某条 route-to-pipe 投递关系后,它仍在结构中,但发送筛选阶段不会按激活目的地处理它。重新启用与退订再订阅也不是同一个操作。
例如 TEMP 同时送 A、B,临时禁用 TEMP→B,并不应该影响 TEMP→A,也不要求删除整个 TEMP 路由。这就是为什么“路由存在”还要与“目的地存在且启用”继续区分。
SBR 负责存取这些结构;SB 负责根据应用身份、管道状态和限额决定是否投递。职责拆开后,路由模块不需要知道队列里是否已经满了。
序列计数为什么放在路由中
同一 MsgId 会发出许多消息,路由项为它保存 SeqCnt。产生点发送时,SB 让 SBR 按 MSG 规则取得下一个序列值,再写入内部消息。
序列号跟随消息流,而不是跟随某个接收管道,所以 A/B 两个订阅者看到的是同一次产生的消息序列。消息经过转发时是否更新,需要由 IsOrigination 决定,不能在每经过一个模块时都无条件重新编号。
它也不是送达证明。序列连续不能证明业务已处理;发现缺口时,还要结合产生端、入队失败、接收和后续传输来定位。
遍历路由时,为什么还要关心锁
SBR 的这些表访问函数并不各自维护完整的锁协议,源码注明由调用方使用 SB 共享锁保护。一个函数看起来只是读取链表头,后面遍历时链表仍可能变化;锁的范围必须覆盖整个需要稳定的过程。
CFE_SBR_ForEachRouteId() 可以逐项调用回调,还能用 StartIndex、MaxLoop、NextIndex 分段遍历。这能避免某些后台工作一次处理过多条目,但接口本身不会把每个回调变成天然安全的快照。
订阅报告就是一个实际例子:外层遍历路由,回调把已有订阅装进报告包,包满后还要调用 SB 发送。发送也会拿 SB 共享锁,因而报告路径在发送前解锁,发送后再加锁。这里解释的是为什么必须看锁边界;不能据此假定整个多包报告对应同一瞬间的原子快照。
我读完 SBR 后再回看订阅代码,主线清楚了很多:订阅写目的地,发送读目的地,接收用路由身份寻找排队计数,管理任务再遍历这些关系生成报告。路由模块代码不长,但它把前面几篇连成了一张可以反复查验的表。
cFS与SB学习记录(五):SBR怎样把MsgId变成目的地列表

