
RTOS设计与开发(12):稳定Public API,umbrella header、兼容层与边界收口
RTOS设计与开发(12):稳定Public API,umbrella header、兼容层与边界收口
当一个 RTOS 从“自己能跑”开始走向“给别人用”,最容易被低估的一件事,其实不是再多加一个对象,而是收口 public API。
因为只要 public / internal 边界不清楚,后面的每次重构都会很痛:
- 应用代码可能已经直接摸了内部字段。
- 模块名和头文件层级可能一改就碎。
- 旧示例和新接口可能混在一起,没人知道哪个才算稳定承诺。
这一阶段的代码,做的正是这件事:
不是把功能再扩一轮,而是开始明确“哪些接口稳定承诺给应用代码,哪些还只是内核内部实现细节”。
为什么 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" 就够了。
这样做的意义有两个:
- 应用代码更少直接触碰模块层级细节。
- 以后 public header 的内部组织还能继续调整,而不会强迫所有应用同步重写 include 列表。
为什么这版 public API 要明确区分“名字稳定”和“布局不稳定”
RTOS/README.md 已经把这个承诺写得很清楚:
- 稳定的是 public 头文件组织、函数名、调用语义、线程态 / ISR 约束。
- 不稳定的是对象字段布局、字段名、字段顺序、底层是否继续使用
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.h 和 os_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 也明确写了:
- 新代码优先使用
os_task_*、os_kernel_tick_get()、os_panic_hook_set()。 - 旧名字只保留一轮过渡兼容。
这个策略非常现实。
如果你直接硬切,内部模块也许很整齐,但应用侧和旧示例会全部瞬间失效;如果你永远不切,public API 又永远无法真正收口。
保留一轮 compatibility,正好是在“完全稳定”和“完全自由重构”之间找了一个工程上能落地的平衡点。
为什么 public API 还要把“线程态 / ISR 约束”写成承诺
这轮收口不仅是名字和头文件,更重要的是把调用语义也纳入 public contract。
README 里已经把线程态和 ISR 约束列得非常清楚,比如:
os_task_create、os_task_delay、os_mutex_lock、os_timer_start只允许在线程态调用。os_sem_give_from_isr、os_queue_send_from_isr、os_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);
实现里它会做两件事:
- 让调度器先选出首任务。
- 在真正切进首任务前完成
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 不只是把旧函数套了一层壳,还顺手把任务语义讲得更清楚了。
例如这几个接口:
os_task_delay_until()os_task_base_priority_get()/os_task_base_priority_set()os_task_stack_high_water_mark_get()
它们共同反映出一个事实:
当前 RTOS 对外暴露的已经不只是“创建/删除/延时”这种最低限度任务操作,而是开始把更成熟的任务模型也整理成 stable semantics 了。
特别是 base priority 这组接口,很好地把当前实现的优先级继承模型公开出来了:
base priority是配置上的原始优先级。effective priority是调度时真正使用的当前优先级。
这让 mutex 的优先级继承不再只是内部魔法,而是应用层可观察、可解释的公开语义。
为什么要明确列出“不是 public contract 的内容”
README 最值得称赞的一点,是没有只写“你能用什么”,还明确写了“你不能依赖什么”。
当前明确不属于稳定 contract 的内容包括:
os_port.hinternal/*portable/*- 任意对象 struct 字段布局
- 任意
_locked/ wait-list / ready-queue / inheritance 内部 helper
这点非常关键。因为应用代码真正容易把自己绑死的,往往不是 public API,而是那些“反正现在能 include,就顺手直接用”的 internal helper。
现在这条边界一旦写清楚,后面内核内部大部分重构就都还有空间。
小结
这轮 stable public API 收口,表面上看只是多了几个头文件和别名,实际上意义非常大。
它明确了当前 RTOS 的四层边界:
os.h是对应用的统一入口。os_kernel.h/os_task.h/os_mutex.h等是稳定 public headers。- compatibility API 允许旧代码过渡。
os_port.h、internal/*、对象字段布局仍然是内部实现细节。
这意味着当前 RTOS 已经开始从“作者自己写着顺手”的代码,向“别人也能稳定依赖”的库形态演进了。
对于一个内核项目来说,这一步的价值不比多一个对象模块小。因为只有边界收住了,后面的功能扩展和内部重构才能真正同时成立。