LK 博客
RTOS设计与开发(12):稳定Public API,umbrella header、兼容层与边界收口
嵌入式
约 1 分钟阅读 0 赞 0 条评论 鸿蒙黑体

RTOS设计与开发(12):稳定Public API,umbrella header、兼容层与边界收口

Yukikaze
Yukikaze @Yukikaze
累计点赞 0 登录后每个账号只能点一次
内容长度 0 正文词元数
正文
目录会跟随阅读位置移动。
阅读进度

RTOS设计与开发(12):稳定Public API,umbrella header、兼容层与边界收口

当一个 RTOS 从“自己能跑”开始走向“给别人用”,最容易被低估的一件事,其实不是再多加一个对象,而是收口 public API。

因为只要 public / internal 边界不清楚,后面的每次重构都会很痛:

  1. 应用代码可能已经直接摸了内部字段。
  2. 模块名和头文件层级可能一改就碎。
  3. 旧示例和新接口可能混在一起,没人知道哪个才算稳定承诺。

这一阶段的代码,做的正是这件事:
不是把功能再扩一轮,而是开始明确“哪些接口稳定承诺给应用代码,哪些还只是内核内部实现细节”。

为什么 os.h 要成为统一入口

当前顶层 umbrella header 非常克制:

#ifndef __OS_H__
#define __OS_H__

#include "os_diag.h"
#include "os_kernel.h"
#include "os_mutex.h"
#include "os_queue.h"
#include "os_sem.h"
#include "os_task.h"
#include "os_timer.h"

#endif /* __OS_H__ */

这段代码看起来普通,但它真正解决的是“应用到底该 include 什么”这个长期成本问题。

对于应用层来说,最理想的体验不是自己到处拼头文件,而是知道:
如果我要用当前阶段 RTOS 的稳定 API,默认 #include "os.h" 就够了。

这样做的意义有两个:

  1. 应用代码更少直接触碰模块层级细节。
  2. 以后 public header 的内部组织还能继续调整,而不会强迫所有应用同步重写 include 列表。

为什么这版 public API 要明确区分“名字稳定”和“布局不稳定”

RTOS/README.md 已经把这个承诺写得很清楚:

  1. 稳定的是 public 头文件组织、函数名、调用语义、线程态 / ISR 约束。
  2. 不稳定的是对象字段布局、字段名、字段顺序、底层是否继续使用 list_t / list_node_t

这和很多“字段先公开着,以后再说”的做法完全不同。

当前实现相当于在明确告诉应用层:
你可以静态定义这些对象,但你不应该依赖它们的内部 layout。

这是一种非常成熟的过渡策略。因为当前 RTOS 仍然需要对象字段可见,方便静态分配;但与此同时,它已经明确拒绝把“字段可见”误导成“布局稳定 ABI”。

为什么要引入 semantic alias,而不是立刻彻底改名

以任务对象为例,os_task.h 现在同时保留了底层类型和 public 语义别名:

typedef tcb_t              os_task_t;
typedef task_init_config_t os_task_config_t;
typedef task_state_t       os_task_state_t;
typedef task_entry_t       os_task_entry_t;

这个设计的妙处在于:
它让对外语义名字先稳定下来,同时又不要求底层实现结构立刻跟着彻底重命名。

换句话说,内核可以先告诉应用代码:“以后你应该把这个东西看成 os_task_t。”
而内核内部仍然保留 tcb_t 这类更贴近实现语义的名字,避免大面积重构时一次把内部外部都搅乱。

对于正在演进中的 RTOS,这是非常务实的选择。

为什么 compatibility API 要保留一轮,而不是硬切

这版 os_task.hos_kernel.h 里都保留了兼容名字,例如:

os_status_t os_task_create(os_task_t *task, const os_task_config_t *config);
...
os_status_t task_create(tcb_t *task, const task_init_config_t *config);

以及:

os_tick_t os_kernel_tick_get(void);
...
os_tick_t os_tick_get(void);

README 也明确写了:

  1. 新代码优先使用 os_task_*os_kernel_tick_get()os_panic_hook_set()
  2. 旧名字只保留一轮过渡兼容。

这个策略非常现实。

如果你直接硬切,内部模块也许很整齐,但应用侧和旧示例会全部瞬间失效;如果你永远不切,public API 又永远无法真正收口。
保留一轮 compatibility,正好是在“完全稳定”和“完全自由重构”之间找了一个工程上能落地的平衡点。

为什么 public API 还要把“线程态 / ISR 约束”写成承诺

这轮收口不仅是名字和头文件,更重要的是把调用语义也纳入 public contract。

README 里已经把线程态和 ISR 约束列得非常清楚,比如:

  1. os_task_createos_task_delayos_mutex_lockos_timer_start 只允许在线程态调用。
  2. os_sem_give_from_isros_queue_send_from_isros_queue_recv_from_isr 允许在 ISR 使用。

为什么这点很重要?

因为对 RTOS 来说,很多 API 的正确性依赖调用上下文本身。
如果 public contract 里不把这个写清楚,应用层很容易在 ISR 里误用线程态 API,最后再把 bug 甩回内核“为什么行为不稳定”。

当前这版文档和头文件,已经开始把这条边界前置到接口层面了。

为什么 os_kernel_start(cpu_clock_hz) 是很好的 public 入口

内核启动 API 现在是:

os_status_t os_kernel_start(uint32_t cpu_clock_hz);

实现里它会做两件事:

  1. 让调度器先选出首任务。
  2. 在真正切进首任务前完成 SysTick 初始化。
primask = os_port_enter_critical();

status = os_port_systick_init(cpu_clock_hz, OS_TICK_HZ);
if (status != OS_STATUS_OK)
{
    os_port_exit_critical(primask);
    return status;
}

os_port_start_first_task();

这意味着应用层不再需要知道“启动前还得先自己调一下 port 的什么函数”。
对外只有一个稳定入口:os_kernel_start()

这正是 public API 收口应该有的样子。应用知道自己该调什么,但不需要知道内核内部为了启动首任务还要做哪些 Cortex-M3 细节动作。

为什么 os_task_* 不只是换个名字,还暴露了更稳定的任务语义

当前稳定 public API 不只是把旧函数套了一层壳,还顺手把任务语义讲得更清楚了。

例如这几个接口:

  1. os_task_delay_until()
  2. os_task_base_priority_get() / os_task_base_priority_set()
  3. os_task_stack_high_water_mark_get()

它们共同反映出一个事实:
当前 RTOS 对外暴露的已经不只是“创建/删除/延时”这种最低限度任务操作,而是开始把更成熟的任务模型也整理成 stable semantics 了。

特别是 base priority 这组接口,很好地把当前实现的优先级继承模型公开出来了:

  1. base priority 是配置上的原始优先级。
  2. effective priority 是调度时真正使用的当前优先级。

这让 mutex 的优先级继承不再只是内部魔法,而是应用层可观察、可解释的公开语义。

为什么要明确列出“不是 public contract 的内容”

README 最值得称赞的一点,是没有只写“你能用什么”,还明确写了“你不能依赖什么”。

当前明确不属于稳定 contract 的内容包括:

  1. os_port.h
  2. internal/*
  3. portable/*
  4. 任意对象 struct 字段布局
  5. 任意 _locked / wait-list / ready-queue / inheritance 内部 helper

这点非常关键。因为应用代码真正容易把自己绑死的,往往不是 public API,而是那些“反正现在能 include,就顺手直接用”的 internal helper。

现在这条边界一旦写清楚,后面内核内部大部分重构就都还有空间。

小结

这轮 stable public API 收口,表面上看只是多了几个头文件和别名,实际上意义非常大。

它明确了当前 RTOS 的四层边界:

  1. os.h 是对应用的统一入口。
  2. os_kernel.h / os_task.h / os_mutex.h 等是稳定 public headers。
  3. compatibility API 允许旧代码过渡。
  4. os_port.hinternal/*、对象字段布局仍然是内部实现细节。

这意味着当前 RTOS 已经开始从“作者自己写着顺手”的代码,向“别人也能稳定依赖”的库形态演进了。

对于一个内核项目来说,这一步的价值不比多一个对象模块小。因为只有边界收住了,后面的功能扩展和内部重构才能真正同时成立。

作者名片

Yukikaze
Yukikaze
@Yukikaze

私にできることなら何でもするから

评论区
文章作者和管理员都可以管理这里的评论。
0 条评论
登录后即可参与评论。 去登录
还没有评论,欢迎留下第一条交流内容。