cFS与SB学习记录(八):把EDS模式的SB单独放到Linux下运行
读完 SB 以后,我想把它从完整 cFE 的启动环境中拿出来,在 Linux 上单独观察一次真实的订阅、发布和接收。这样每一层依赖都要明确回答:SB 要的应用身份从哪里来,内存池由谁建立,EDS 数据库在哪里,原来的管理任务又怎样启动。
这次以 cfs_sb_linux 的三次主要移植记录为主线。后来另一个目录 sb_port_linux 做了进一步拆解,我放在文末补充,避免把整理中的代码状态与前一个已经留下运行记录的版本混写。
先确定这次“移植”的范围
cFS 本身已有 Linux 运行方式,OSAL 也已有 POSIX 后端。这次需要处理的主要问题,是抽出 EDS 模式 SB/SBR,在一个独立 Linux 进程里补齐它原来从 cFE 取得的环境。
我保留 SB 的收发、缓冲区和管理任务代码,SBR 使用哈希映射,MSG 使用 EDSMSG。Linux/POSIX OSAL 负责实际任务、队列和同步对象;ES、EVS、TIME、FS、PSP、Config 中被调用的部分,则放到本地适配层明确实现或返回不支持。
1 | |
这份程序没有带入完整 ES 的应用加载与生命周期,也没有通过网络发送 PUS 包。先把这个范围写清楚,后面的依赖取舍才有依据。
三次提交分别解决了什么
这段工作先后留下三个主要节点:
| 节点 | 解决的问题 |
|---|---|
bf90035 |
取出同一版本的 SB/SBR、OSAL、EDSMSG、运行库与生成快照 |
ef32588 |
组织独立构建,补齐 SB 需要的 cFE 适配入口 |
7f2af23 |
加入收发、原管理任务和安装后外部调用示例 |
后续还有文档整理。这里按照技术问题展开,不要求读者先熟悉这几个提交。
有一个历史切分细节需要保留:第二次的 CMake 已经引用演示源码,文件到第三次才补齐。因此复现最终流程应使用包含第三次提交的完整版本,不能把后来版本的成功输出当作第二次单独检出就能运行的证据。
第一步不是删文件,而是固定一套匹配的输入
使用的来源为同一套工作区快照:cFS bundle 088b2fa、cFE c5fb2b4、OSAL d2d877a、EDS 工具 2acc963。这里记录版本,是为了以后能对照 API、生成类型和配置;不能拿一个新版头文件去解释旧版运行库的行为。
除了 sb/、sbr/,还需要 EDSMSG、ResourceID、必要的核心声明,以及 EdsLib/MissionLib 运行时。生成数据库取自 SampleMission 已生成的 C/H 快照。原始许可文本和来源关系也一并保留。
最初看文件数量时,容易觉得某些头“好像不属于 SB”。但源码调用可能需要它们声明的结构体、状态码和配置。我的处理顺序是先追清编译与调用依赖,再决定哪些后端和功能不选,尽量让原 SB 算法保持可对照。
生成头里的绝对路径要先处理
完整 cFS 构建会生成一些包装头,其中可能只有一条指向原源码目录的绝对 #include。直接复制到独立工程,当前机器上也许仍能找到原目录,换个位置就失败。
这类文件要和真正的 EDS 类型/数据库区分。准备快照时,工具把指向已确认来源树的包装头固化为实际内容,清理注释中的机器路径。它是移植材料准备过程,不是每次编译重新生成 EDS,也没有手工更改数据库中的类型语义。
先让 OSAL 接受外部程序的入口
OSAL 的 Linux BSP 原来自己定义 main(),会完成 BSP 初始化,再调用 OSAL 应用入口。独立演示也要有自己的 main(),于是出现了入口归属问题。
我在复制版 BSP 的原 main() 外加 OSAL_NO_BSP_MAIN 守卫,只对相应目标启用这个定义,保留可调用的 OS_BSP_Initialize()。演示入口负责接上初始化:
1 | |
解决重复 main 之后,还要确认原入口中必要的初始化没有一并丢掉。能链接只是第一步,运行时对象表和 BSP 状态仍需要正确建立。
构建使用 OSAL 自己的配置选择 generic-linux 与 POSIX 实现。这次关闭网络、DNS、加载器、shell 等不需要的能力,保留任务、队列和同步等 SB 依赖。
关闭网络后还遇到一个选源问题:其他对象仍引用 hostname/hostid 接口,而配方没有为当前关闭配置补齐对应符号。这里加入的是上游已有的 os-impl-no-network-gethostname.c 和 os-impl-no-network-gethostid.c,让未提供的能力走明确的无网络实现,而不是为了消除链接错误再把整个网络功能打开。
管道深度 32,为什么在主机上建不起来
SB 管理命令管道的默认深度是 32,而这台 Linux 主机的 /proc/sys/fs/mqueue/msg_max 为 10。上层请求即使通过了某些编译期范围检查,最后到 POSIX 队列建立时仍受内核限制。
我把这个限制留在 Linux 目标的配置上处理:保持源码默认值可辨认,为该目标定义 CFE_SB_CMD_PIPE_DEPTH=8,演示接收管道也选深度 8。
1 | |
这里的 8 是当前实验选择,不是推荐给所有任务的容量。换机器时可以先读主机上限,换到目标 OS 时还要检查 OSAL 队列支持的深度与单项大小。队列创建失败,也不应立刻归因于 SB 路由算法。
只带 OSAL,为什么 SB 仍然缺很多符号
OSAL 解决操作系统能力,SB 还依赖 cFE 服务语义。我把这些依赖归到 port/,按真实调用分别处理:
| 依赖 | 独立版本提供的内容 |
|---|---|
| ES | 单池外壳、当前 AppId/TaskId、名称和最小启动状态 |
| EVS | 控制台输出,以及演示要读取的事件计数 |
| TIME / PSP | 主机时间、处理器实例等当前需要的入口 |
| Config | SB 块尺寸配置、真实 EDS 与 SB 接口数据库指针 |
| FS | 保留命令依赖的边界,后台转储明确返回不支持 |
内存池没有直接换成 malloc
SB 的块大小、容量限制、回收和统计依赖分级池。若把 CFE_ES_GetPoolBuf() 简单改成 malloc(),收发可能看起来能工作,但观察到的已是另一套资源行为。
所以我复用了 cfe_es_generic_pool.c 的原算法,适配层提供单池的建立、取块和还块外壳,用地址与偏移连接原池实现。SB 建池时选择无内部互斥模式,因为相应操作由 SB 的共享锁协议保护;不能忽略这层约定,把同一个外壳当成任意调用者都可并发使用的通用 ES 池服务。
这版只提供一个 SB 使用的池。它没有实现完整 ES 的全部内存池与应用生命周期。
应用身份和任务身份必须各自真实
独立程序没有完整 ES 来替任务登记应用归属,因此每个示例任务先调用本地 SB_Port_SetCurrentApp(),建立当前 AppId 和名称。TaskId 则从当前 OSAL 任务取得,适配层保存它与应用的对应关系。
两个接收任务都属于 DEMO 应用,但 TaskId 不相同。若为了省事,让所有任务都返回一个固定 ID,就可能让事件抑制、归属查询和清理路径互相混淆。验证“两行 RX 来自两个不同 TaskId”,就是为这个依赖留一个具体检查点。
Config 返回的必须是实际对象
EDS 模式会通过 Config 取任务数据库和 SB 接口数据库。这里分别返回 EDS_DATABASE 与 CFE_SOFTWAREBUS_INTERFACE 的地址;池的块尺寸数组也来自匹配的配置。
处理器实例号同样参与映射。这个 SampleMission 的本地 STATS TopicId 为 275,取实例 1 才得到预期的 0x80a。实例号设错导致无效映射时,改发送程序里的 MsgId 常量不能解决根因。
把原版管理任务也启动起来
只做两个任务互发消息,仍不知道 SB 自己的命令循环有没有被接好。我保留原 CFE_SB_TaskMain(),用一个 OSAL 任务进入它,在任务入口登记 SB 应用身份。
演示启动顺序是:
1 | |
“先接收者、后发送者”在这里具体指订阅已经建立,不是仅仅创建了一个线程。发送方不保存未订阅时期的历史消息;如果靠延迟猜线程启动速度,实验会偶尔错过第一包。
为什么就绪通知用了计数信号量
两个接收任务各通知一次准备步骤完成。二值信号量只能表达有/无状态,两次通知若先于等待发生,可能合并成一个信号。计数信号量能保留两次通知,主线程等待成功两次后再继续。
不过当前示例的 ready 通知表示创建/订阅尝试已经结束,不单独代表尝试成功;任务内的失败仍通过 failures 累计,最终检查一起判断。这是读失败分支时需要保留的语义。
运行时实际检查了哪些关系
发布者发送一次带 0x1234 标记的 STATS 消息,两个管道订阅同一 MsgId。接收者检查消息身份、native 大小和 payload,验证任务 ID 不同,然后删除自己的管道。
接收指针由 SB 管理,没有调用发布者专用的 ReleaseMessageBuffer()。管道删除会处理队列和 LastBuffer 引用,这与前面几篇的生命周期一致。
随后发布者发 NOOP;主流程等待原管理任务的 NOOP 事件。另一个阶段先订阅 HK 遥测,再发 HK 请求,读取返回的正常命令计数。这样覆盖了普通数据投递和管理命令两条路径。
移植文档保存的本机输出包括:
1 | |
省略号隐藏的是每次可能不同的 TaskId 打印,不是完整日志原样抄录。received=2 对应两个接收者,noop=1 来自管理任务事件计数,failures=0 才说明这次演示中的检查没有记录失败。这里引用已有 Linux 运行记录,不能据此推出目标 RTOS、全部管理命令或长期压力条件已经通过。
构建、运行与安装后消费
对应完整独立工程,以下命令从工程根目录执行。使用新的构建目录,可以减少旧缓存和旧生成物的影响:
1 | |
这套构建需要支持相应 POSIX 能力的 Linux、C11 编译器和 CMake 3.24 及以上版本。静态库之间有交叉依赖,演示链接使用 CMake 的 LINK_GROUP:RESCAN 组织重复扫描。它解决的是静态归档提取顺序,不负责修复真正缺失的实现。
我还保留了只使用安装产物的外部工程:
1 | |
外部工程用 find_package(CfsSb REQUIRED CONFIG) 和 CfsSb::cfs_sb,不直接借源码树、原构建目录里的头。其输出 external EDS MsgId = 0x80a,说明安装后的头、库与映射入口可以被调用。这个小 consumer 没有再次运行完整的多任务收发,因此它证明的是安装接口可消费,不能替代主示例。
薄适配也需要保持接口约定
为了让第一条路径连起来,适配层做了范围限制,但返回值和输出参数仍必须遵守调用者预期。例如后台文件转储没有实现,就返回 CFE_FS_NOT_IMPLEMENTED,不能返回成功却没有任何落盘动作。
另一方面,仅返回“不支持”也不代表前置处理可以忽略边界。当前文件名适配中还存在 InputBufSize 未参与拷贝上界的问题;某些 AppId 查询失败后的输出也需要明确初始化。它们说明正常演示通过后,仍要沿着有限长度输入、无效身份和其他失败分支继续补检查。
这篇记录的是已有移植路径,没有把这些待完善项写成已修复结果。
后来在 sb_port_linux 里继续整理了什么
后续目录把构建和示例进一步拆开:重新编辑了 ES、EVS、FS、TIME 适配,精简了原先的装配代码,示例入口也缩成了 OS_API_Init() 后调用 CFE_SB_TaskMain() 的形状。
这适合用来继续追问“哪些入口不可缺”,但它当时不是前面完整流程的等价替代:当前 CMake 中演示可执行目标被注释;部分原适配源没有进入目标,入口也没有体现完整的 SB 早期初始化、身份登记与核心就绪安排。
这里还有一个构建知识点:静态库归档成功,不代表最后的可执行程序已经链接成功。静态库可以暂时保留未解析的外部调用;要等真正链接使用者,才能暴露缺失符号和链接顺序。即使磁盘上存在旧的演示产物,也必须确认它对应当前源文件与目标配方。
所以我把这份后续整理作为继续拆解依赖的学习记录,前面展示的双订阅者、NOOP/HK 和安装检查仍归到 cfs_sb_linux 的完整基线。接下来若要把整理版本接成新的可运行程序,需要按相同层次补齐并重新验证,不能沿用旧版本的结果。
回看这次过程,最有用的收获是把“移植 SB”拆成了可解释的依赖:原消息投递算法、操作系统对象、cFE 服务边界、任务配置和 EDS 数据库。换一个目标环境时,可以沿这些接口逐项接回去,仍然保留一条能追到原代码的收发主线。
原始资料可对照 固定版本 cFS bundle、cFE、OSAL和 EdsLib。
cFS与SB学习记录(八):把EDS模式的SB单独放到Linux下运行
https://goko-son626.github.io/post/cfs-sb-07-linux-port.html

