cFS与SB学习记录(一):先弄清cFS、cFE和OSAL各自做什么
前面整理 EDS 和 PUS 时,关注点主要是“一条消息表达什么、怎样编码”。继续往下读 cFS,会碰到另一个问题:消息已经定义好了,运行中的几个应用怎样把它交给彼此?谁创建任务,谁维护队列,换一个操作系统后又该改哪一层?
我这次把阅读范围收到了 cFE 的 Software Bus,也就是 SB。不过在钻进发送和接收函数之前,先把 cFS、cFE、OSAL、PSP 这几个名字放回各自的位置。否则看到一个 OS_QueueCreate(),很容易把业务通信、cFE 服务和操作系统实现混在一起。
从一个应用场景理解 cFS
假设星上有三个软件功能:采集温度、检查温度是否越界、把温度送回地面。最直接的写法,是让采集程序分别调用另外两个程序,或者给两个接收者各维护一条队列。
功能少时这样能工作。应用增加后,每个应用都需要处理任务启动、消息交接、错误日志、参数表、时间和运行状态,应用之间还会形成很多直接依赖。更换硬件或操作系统时,相似的基础设施又要重新做一遍。
cFS(Core Flight System)把其中可复用的部分组织成一套飞行软件架构和组件生态。实际任务再选择需要的应用、设备接口和配置,组成自己的飞行软件。NASA 的公开 cFS bundle 提供了一套能组合构建的起点,其中的实验应用也方便在开发机上学习。下载 bundle、运行样例,与完成某个任务的飞行软件验证,是不同阶段的事。NASA 的 bundle 说明对这层关系有明确说明。
对我现在的阅读,先抓住下面这张图就够了:
1 | |
这张图用于分清职责。真实调用不总是严格逐层向下:应用也会直接用 OSAL,cFE 的服务之间也会互相调用。
cFE 提供运行环境,底下仍然需要操作系统
cFE 的全称是 Core Flight Executive。它是 cFS 框架的一部分,提供一组可供应用调用的公共服务和管理机制。这里最需要避免的误解,是把名字里的 Executive 当成一个替代 Linux 或 RTOS 的内核。
以 Linux 构建为例,线程调度最终由 Linux 完成,消息队列和互斥量也落到相应的操作系统实现。cFE 在上面管理应用身份、组织公共服务和处理运行状态。一个 cFE 应用还可能创建子任务,因此“cFE 应用”和“操作系统线程”不能简单画等号。
我先按使用场景记这些服务:
| 服务 | 在程序中遇到它的典型场景 |
|---|---|
| ES,Executive Services | 应用注册、启动/退出、任务身份、性能记录、内存池等 |
| SB,Software Bus | 创建接收管道,订阅 MsgId,发布和接收消息 |
| EVS,Event Services | 上报带事件号与严重级别的运行信息,配置过滤 |
| TBL,Table Services | 管理可加载、校验和更新的应用参数表 |
| TIME,Time Services | 获取和管理任务使用的时间信息 |
| FS、MSG 等支持模块 | 文件相关公共处理,消息头访问和布局抽象 |
这张表不是完整 API 清单,作用是读代码时能认出依赖。例如 SB 请求一个内存块时会调用 ES 的池接口;SB 发现管道溢出时会调用 EVS;SB 发管理遥测时又会用消息和时间相关接口。把 sb/ 文件夹单独复制出来,并不会让这些依赖自动消失。
OSAL 比“把 pthread 换个名字”多了一层
OSAL 是 Operating System Abstraction Layer。它给上层一组相对稳定的 API,再由不同操作系统后端落实这些 API。任务、队列、互斥量、计数信号量等能力,正是我这次读 SB 时接触最多的部分。
以创建队列为例,上层调用的是:
1 | |
公共实现先检查参数,为对象分配 OSAL ID,记录名称、深度和单条数据大小,再调用操作系统实现。Linux/POSIX 后端继续处理 POSIX 消息队列的建立和读写。
1 | |
因此 osal_id_t 应当作为 OSAL 句柄使用,不能假设它就是 pthread_t、POSIX mqd_t 或数组下标。公共对象管理还负责检查 ID 对应的类型和状态,这也是为什么移植 OSAL 时,不能只找几个同名 POSIX 函数替换掉就结束。
我在这里区分了三种身份:
- AppId:cFE 眼中的应用身份,SB 用它判断管道归属等事项。
- TaskId:执行当前调用的任务身份,一个应用可以有多个任务。
- PipeId:SB 接收管道的句柄,管道内部还关联着 OSAL 队列 ID。
数字都能打印出来,但它们代表的对象不同。后面的 Linux 实验里,两个接收任务可以属于同一个应用,却必须有各自不同的任务身份。
PSP 与 OSAL 的分界
PSP 是 Platform Support Package,处理处理器和平台环境相关的支持,例如某些启动、复位、时基和处理器实例信息。OSAL 则围绕操作系统对象和操作展开。
OSAL 自己也有 BSP 启动代码,因此源码里的“BSP”不能看到名字就全归到 cFE PSP。阅读具体函数时,还是要问它负责什么:是在建立 OSAL 的运行入口,还是在提供 cFE 需要的处理器/平台能力?
这个区别会直接影响独立移植。Linux 已有线程和消息队列,适合复用 OSAL 的 POSIX 后端;但从完整 cFE 中抽出 SB 后,应用身份、配置数据库和处理器实例号仍需要有人提供。它们不会因为 OSAL 初始化成功就自动具备。
SB 让发送者和接收者通过消息身份相遇
回到温度例子。采集应用发一条带 MsgId 的消息,越界检查应用和遥测输出应用各自订阅这个 MsgId。采集应用不需要知道两者的函数地址、任务句柄或队列名称。
1 | |
一条管道可以订阅多个 MsgId,一个 MsgId 也可以对应多条管道。发送时,SB 查到这些关系,把消息交给目标队列;接收任务再从自己的管道取消息处理。
这里有个后面会反复用到的事实:这版 SB 的管道队列存放的是缓冲区描述符指针,消息内容由 SB 的内存池和引用计数共同管理。因此“两个订阅者都收到了”,不等于为两个订阅者分别复制了整份消息。
同样,“软件总线”也不能直接理解成网络总线。本文研究的是同一运行环境中的任务间通信。网络接入、跨节点转发和星地链路,需要相应应用或扩展完成;SBN 与 SBR 也不是同一个缩写。
与之前的 EDS、PUS 怎样接上
我把这几项分别放在三个问题下理解:
1 | |
SB 可以使用默认消息实现,也可以在 EDS 构建中配合 EDSMSG、EdsLib 和任务接口数据库。启用 EDS 后,消息类型、TopicId 映射和命令分发有了数据库参与,底下的管道、订阅、缓冲区和投递机制仍然有共同主线。
PUS 服务则还要由业务代码实现。把一个 PUS 包定义进 EDS,再放进 SB,并不会自动执行 PUS 的状态机。反过来,学 SB 也不必先把所有 PUS 服务学完。
这次源码阅读按什么顺序展开
逐文件读,很容易记住函数名却忘掉它们怎么连起来。这次我沿一条消息的过程整理:先创建管道和订阅,发送时查路由、分配缓冲区并投递,接收时交出引用,最后在接收结束或管道删除时归还资源。SB 自己的管理任务再放到这条主线上看。
具体分成下面几篇:
- 管道、初始化与订阅关系
- 一条消息的发送过程
- 接收、借用缓冲区与零拷贝
- SBR 怎样查找路由
- SB 管理任务、命令和统计
- EDS 模式里的 MSG、TopicId 与分发
- 把 SB 独立放到 Linux 下运行
代码细节以本次使用的固定源码为准:cFE c5fb2b4、OSAL d2d877a,不是对所有 cFS 版本作同一结论。需要对照时,可以从 cFE 的说明、SB 公共接口和 OSAL 说明进入。后面的正文会把理解所需的结构和片段放出来,文件路径只用来追溯。
cFS与SB学习记录(一):先弄清cFS、cFE和OSAL各自做什么

