- 操作系统
- 嵌入式
- 嵌入式OS
【免费下载链接】tock
A secure embedded operating system for microcontrollers
导读
本文基于 Tock 核心工作组(Core WG)2025 年 10 月 2 日的会议纪要(doc/wg/core/notes/core-notes-2025-10-02.md),深入解读本次会议敲定的两项关键技术决策:以SingleThreadValue逐步替换static mut的 panic 基础设施迁移路线,以及为tock-registers引入外部依赖的例外政策。文章将结合当前仓库中 kernel/src/utilities/single_thread_value.rs、kernel/src/platform/chip.rs、kernel/src/deferred_call.rs 与 doc/ExternalDependencies.md 等源码与文档,还原讨论背景、实现原理与最终结论,帮助读者理解 Tock 如何在不牺牲安全性的前提下推进内核现代化。
会议概览与议程
本次会议由 Amit Levy、Brad Campbell、Branden Ghena、Hudson Ayers、Johnathan Van Why 与 Leon Schuermann 六位核心工作组成员参加,议程集中在三个相互关联的话题上:
- Cargo.lock PR(#4605):合并进度与构建系统同步问题;
- SingleThreadValue(#4519):
static mut的替代方案如何推广到全部板卡,尤其是 panic 基础设施; - 外部依赖政策(#4616 与 #4589):为
tock-registers成为外部依赖而修改政策的两个竞争提案。
这三者共同指向一个更大的目标——解除 Tock 对旧式static mut的依赖、跟上 Rust 工具链的演进,同时为内核引入受控的外部依赖打开口子。
Cargo.lock PR(#4605):为外部依赖铺路的构建系统同步
讨论要点
会议首先确认了 Leon 提交的 Cargo.lock PR(#4605)可以合并。该 PR 之所以需要"尽快合并",是因为lockfile 必须与 Tock 的构建系统保持同步——一旦仓库内 crate 版本发生变动,Cargo.lock也会随之变化,长期不合并会产生大量冲突。
关于 Cargo.lock 是否与其他文件"绑定"的问题,会议结论是:历史上它们曾相互关联,但当前已经彼此独立。更重要的是,Leon 指出引入外部依赖之后,必须能够精确控制依赖版本,这正是 Cargo.lock 的职责所在。
为什么 Cargo.lock 对 Tock 如此关键
Cargo.lock 位于仓库根目录(Cargo.lock),锁定了工作区所有依赖(含传递依赖)的精确版本与校验哈希。在会议讨论中,Johnathan 与 Brad 进一步强调了它的安全价值:
- cargo 会对依赖进行哈希校验,比单纯的 git 哈希更强;
- cargo 的版本解析还会考虑传递依赖(transitive dependencies),保证整个依赖树的一致性。
从 Cargo.toml 的[workspace.dependencies]可以看到,tock-registers = { version = "0.10.0", default-features = false }已作为工作区级依赖出现,这正是外部依赖讨论落地后的直接产物。可以推断:合并 #4605 是让外部依赖版本可控、可审计的第一步。
SingleThreadValue(#4519):替换static mut的迁移工程
背景:panic 基础设施为什么成为瓶颈
Brad 在会上点明了问题的核心:panic 基础设施的代码被复制到了几乎所有板卡,如今要更新它,工作量巨大。这里的"panic 基础设施"指的是各架构、各板卡为打印 panic 信息而维护的全局状态。
更紧迫的约束是:只要还存在static mut,Tock 就无法升级 Rust 工具链。因为新版 Rust 对static mut的引用提出了更严格的借用规则(引用static mut在安全代码中不再被允许),而"半途而废"的迁移会阻塞整个版本的发布。
从当前仓库可以印证这一判断:static mut仍存在于 arch/cortex-m/src/syscall.rs(如SYSCALL_FIRED、SVC_SWITCH_TO_APP、APP_HARD_FAULT、SCB_REGISTERS等全局状态)、arch/x86/src/interrupts/poller.rs 的SINGLETON,以及 arch/riscv/src/lib.rs 的链接器符号(_szero、_ezero等)。这些正是迁移工作尚未完成的部分。
SingleThreadValue 的设计动机
SingleThreadValue(kernel/src/utilities/single_thread_value.rs)是本次迁移的核心替代品。它的设计目标是在不要求T: Sync的前提下,把非Sync的值安全地放进全局静态变量。
其文档明确说明了取舍逻辑(# Implementation Trade-Offs一节):
- 自动化执行优先于代码审查:Tock 一贯主张用 Rust 的类型系统与 API 强制正确性,即使检查发生在运行时;
- 运行时检查而非编译期检查:截至 2025 年 7 月,社区尚不知道如何把这些检查移到编译期;
- 接受"忘记绑定即失败"的接口缺陷:类型创建后必须显式绑定到线程,类似 Tock 常见的"先建对象、后设回调"模式;漏掉这一步会导致运行时检查永远失败,但这种代价换来了可保证的健全性(soundness)。
核心实现:三阶段绑定状态机
SingleThreadValue<T>内部持有三个字段:
value: UnsafeCell<MaybeUninit<T>>——被包裹的值,构造时不初始化,以避免T: Send约束;bound_to_thread: AtomicUsize——绑定状态的原子指示器,其取值对应BoundToThreadStage枚举;thread_id_and_fn: UnsafeCell<MaybeUninit<(fn() -> usize, usize)>>——记录查询"当前线程 ID"的函数指针与已绑定的线程 ID。
绑定过程是一个严格递增的三阶段状态机(#[repr(usize)]枚举,见源码第 29-64 行):
| 阶段 | 值 | 含义 |
|---|---|---|
Unbound | 0 | 尚未绑定,thread_id_and_fn未初始化,可以被绑定 |
Binding | 1 | 已通过compare_exchange原子操作"预留"初始化权,防止并发绑定;thread_id_and_fn仍未初始化 |
Bound | 2 | 已绑定到某线程,thread_id_and_fn初始化完毕且封存,不可再修改 |
两个绑定入口:
bind_to_thread<P: ThreadIdProvider>(安全方法):通过compare_exchange(Unbound → Binding)原子地抢占绑定权,依赖cfg(target_has_atomic = "ptr"),即目标平台需支持usize级别的原子比较交换;bind_to_thread_unsafe<P: ThreadIdProvider>(unsafe 方法):适用于不支持compare_exchange的平台,要求调用者在外部保证不会与其他绑定调用并发,因此可直接从Unbound跳到Bound,省去中间状态。
两个方法均在完成thread_id_and_fn与value的初始化后,以Ordering::Release将状态存储为Bound,确保后续Acquire加载能观察到完整的初始化写入(源码第 342-348、439-445 行)。
访问入口是get() -> Option<&T>:内部先调用bound_to_current_thread(),该方法以Acquire顺序加载bound_to_thread,确认状态为Bound后读取封存的线程 ID,并与ThreadIdProvider::running_thread_id()的当前返回值比对;一致才返回Some(&T),否则返回None。由于只允许同一线程获取共享引用,多线程下通过Cell、MapCell等内部可变性容器再获取可变访问(这正是 tock-cells 中MapCell/TakeCell的用武之地)。
类型整体通过unsafe impl<T> Sync for SingleThreadValue<T> {}标记为Sync(源码第 206 行),其 SAFETY 注释给出的理由是:该类型在运行时强制"值只对起源线程可见",与标准库LocalKey的语义类似。
ThreadIdProvider:线程识别的安全前提
SingleThreadValue的安全性完全建立在ThreadIdProvider的正确实现之上。该 trait 定义于 kernel/src/platform/chip.rs 第 128-137 行:
pub unsafe trait ThreadIdProvider { /// Return a unique ID for the currently executing thread. fn running_thread_id() -> usize; }它是一个unsafe trait,理由很直接:实现者必须保证返回的 ID 对当前执行线程唯一且一致,否则依赖它的SingleThreadValue会变得不健全。文档同时指出:嵌入式平台虽是单核、单执行线程,但中断服务例程(ISR)的执行构成了第二条执行线程,因此实现至少要能区分"主线程"与"ISR 上下文"。
ThreadIdProvider通过Chiptrait 的关联类型type ThreadIdProvider: ThreadIdProvider与具体芯片绑定(kernel/src/platform/chip.rs 第 26 行),即由各架构/芯片 crate 提供实现。
已完成的示例:deferred_call 与 debug
Brad 在会上强调"我们已经有两个示例"——一个使用 UART 的简单示例和一个使用 USB 协议栈的复杂示例。仓库中可确认的落地示例有两个:
示例一:deferred_call 子系统(kernel/src/deferred_call.rs)
static CTR: SingleThreadValue<Cell<usize>> = SingleThreadValue::new(); static BITMASK: SingleThreadValue<Cell<u32>> = SingleThreadValue::new(); static DEFCALLS: SingleThreadValue<[OptionalCell<DynDefCallRef<'static>>; 32]> = SingleThreadValue::new();并配套初始化函数initialize_deferred_call_state<P: ThreadIdProvider>()及其_unsafe变体(第 174-196 行),在启动阶段依次绑定三个全局状态。DeferredCall::new()通过CTR.get()读取计数器分配唯一 ID;若get()返回None(即板卡未先调用初始化函数),会直接 panic 并提示"DeferredCall state not initialized"——这正是文档中"忘记绑定即失败"设计取舍的实例。
示例二:debug 子系统(kernel/src/debug.rs)
pub static DEBUG_GPIOS: SingleThreadValue<MapCell<&'static [&'static dyn hil::gpio::Pin]>> = ... static DEBUG_WRITER: SingleThreadValue<MapCell<&'static dyn DebugWriter>> = ... static DEBUG_WRITER_COUNT: SingleThreadValue<Cell<usize>> = ...同样提供initialize_debug_gpio<P>/initialize_debug_gpio_unsafe<P>与initialize_debug_writer_wrapper<P>/_unsafe两组初始化入口(第 483、498、588、607 行)。
迁移策略讨论:测试、移植指南与增量路线
会议的核心分歧在于迁移节奏:
- Brad 的观点:两个示例已经足够,不需要再等更多示例;只要还有
static mut就无法升级 Rust、无法发布版本,且目前缺乏跟踪迁移进度的手段; - Leon 的观点:#4519 PR 使用了更新后的接口(同时覆盖 ARM 与 RISC-V),尚未充分测试,需要确认 panic 处理程序在所有可达路径上都正常工作,避免意外破坏;他主动提出在 RISC-V 平台上完成硬件测试;
- Amit 的折中方案:Leon 继续按计划做硬件测试,同时 Amit 自己投入时间研究如何让迁移增量进行,并形成书面策略;Hudson 补充了流程建议——Leon 完成硬件测试后把 PR 从 draft 状态转正、合并,再由该 PR 充当其他板卡的迁移模板;
- Johnathan 的提议:能否把 panic 处理程序实现通过宏(macro)搬进 kernel crate?Leon 基于在 Arty 与 QEMU 上的实践经验回应:各架构的 panic 实现存在大量细微差异,很难简单地统一抽取;
- 最终共识:尝试先抹平(elide)架构差异,再增量恢复;由 Leon 与 Amit 按该策略推进,与会者普遍同意。
会议还确认了一个现实的痛点:由于板卡需要跟踪 kernel crate 的改动,迁移可能无法按板卡逐个增量提交,而需要一个大型 PR 一并完成——Brad 称之为"现实情况"。
外部依赖政策(#4616 与 #4589):为 tock-registers 开放例外
两项竞争提案的取舍
Amit 提出了两个竞争提案,要求各 PR 作者为自己的方案辩护:
- Brad 的 #4616:不改变整体政策,仅添加一条针对
tock-registers的、非常明确的例外条款,并解释为什么它是例外; - Leon 的 #4589:提出一条兜底规则(catch-all rule),允许枚举之外的外部依赖也按类似标准进入。
Leon 在听完 Brad 的方案后主动撤回了自己的提案,理由是:#4616 是他提案的子集,枚举例外依赖的方式没有问题,而兜底规则"我们并不需要"。Branden 以"样本量为 1"表示保留意见,Leon 接受。最终 Amit 提议关闭 #4589、按原样批准合并 #4616,Hudson 表示已批准,Leon 随即关闭了自己的 PR。
讨论中澄清的边界
- "如何依赖"不在政策范围内:Leon 指出 #4616 没有讨论依赖以何种方式被引入(git 依赖 vs crates.io vs submodule 等)。Johnathan 认为这是工程问题而非政策问题,Hudson 也表示认可,因此政策刻意排除了这一维度;
- 与 Cargo.lock 的耦合:Brad 提醒 cargo 会校验依赖哈希,Leon 补充 cargo 的校验强于 git 哈希(因为它还覆盖传递依赖),再次印证了前面 Cargo.lock 讨论的必要性。
政策背景与例外理由
Tock 的总体政策是内核不引入 Rust 标准库之外的外部依赖(详见 doc/ExternalDependencies.md),动机是:
- 便于审计:只审查 Tock 仓库内的代码即可评估安全性,尤其是
unsafe的使用; - 依赖树不可控:外部 crate 通常自带依赖,而 cargo(截至 2023 年 5 月)缺乏审计、禁止依赖树中
unsafe的健全工具; - 影响面分级:外部依赖加在
kernel/、arch/、chips/等被反向依赖的 crate 上,影响远大于加在板卡 crate 上,因此政策按 crate 类型逐级放宽。
tock-registers是唯一被明确批准的内核级外部依赖例外,其理由包括:
- 促进更严格的向后兼容考虑(独立仓库避免破坏性变更藏在 Tock 专属 PR 中);
- 鼓励定期发布(独立版本追踪让更广泛的社区受益);
- 降低外部用户参与门槛(有明确的问题与提案入口);
- 鼓励独立贡献(明确其独立项目地位)。
例外条款同时限制了tock-registers自身的依赖:允许对syn、quote、proc-macro2的依赖(理由:过程宏需要解析/生成非平凡 Rust 代码,且这些 crate 是 Rust 生态的核心机制、子依赖仅unicode-ident一个且无传递依赖),以及仅用于单元测试的 dev-dependencies。
值得注意的是,该政策还列出了另一个例外flux-rs(精化类型形式化验证,仅作可选测试依赖),但本次会议讨论聚焦于tock-registers。
落地现状印证
从当前仓库的 Cargo.toml 可以看到,tock-registers已作为[workspace.dependencies]中的外部依赖(版本0.10.0,default-features = false)被引入,且仓库根目录存在 Cargo.lock。这印证了本次会议决策已实际落地:Tock 内核在"零外部依赖"的底线上,为自维护的高价值 crate 开了一条受控、明确、可审计的例外通道。
会议结论与后续行动
综合三部分讨论,本次会议形成了以下明确结论:
- Cargo.lock(#4605):立即合并,保持与构建系统同步,为外部依赖的版本控制铺路;
- SingleThreadValue(#4519):Leon 在 RISC-V 硬件上完成测试、验证 ARM 侧无回归后,将 PR 转正并合并,由该 PR 作为其他板卡的迁移模板;Amit 负责研究增量迁移策略并书面化;尝试先抹平各架构 panic 实现的差异再增量恢复;
- 外部依赖政策:关闭 #4589,按原样合并 Brad 的 #4616——以"明确例外"而非"兜底规则"的方式为
tock-registers打开政策口子。
延伸阅读
- 会议纪要原文:doc/wg/core/notes/core-notes-2025-10-02.md
SingleThreadValue完整实现与文档:kernel/src/utilities/single_thread_value.rsThreadIdProvidertrait 定义:kernel/src/platform/chip.rs- 迁移示例一(deferred_call):kernel/src/deferred_call.rs
- 迁移示例二(debug):kernel/src/debug.rs
- 外部依赖政策全文:doc/ExternalDependencies.md
- 工作区依赖声明:Cargo.toml
- 尚未迁移的
static mut现场:arch/cortex-m/src/syscall.rs、arch/x86/src/interrupts/poller.rs、arch/riscv/src/lib.rs
- 操作系统
- 嵌入式
- 嵌入式OS
【免费下载链接】tock
A secure embedded operating system for microcontrollers
相关推荐
Tock 核心工作组 2025-09-10 会议纪要解读:LLM 贡献政策、tock-registers 外部化与 SingleThreadValue
Tock 核心工作组 2025 09 10 会议纪要解读:LLM 贡献政策、tock registers 外部化与 SingleThreadValue 导读 本
操作系统嵌入式嵌入式OSanarlog 桌面端 SQLite 响应式 UI 实战:useDrizzleLiveQuery 六大稳定模式与反模式清单
anarlog 桌面端 SQLite 响应式 UI 实战:useDrizzleLiveQuery 六大稳定模式与反模式清单 本文是一份面向 anarlog 桌面
操作系统嵌入式嵌入式OSTock 网络工作组 2025-02-10 会议纪要解读:Ethernet Datapath HIL、StreamingProcessSlice 迁移与 libtock-rs 路线讨论
Tock 网络工作组 2025 02 10 会议纪要解读:Ethernet Datapath HIL、StreamingProcessSlice 迁移与 lib
操作系统嵌入式嵌入式OS
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考