- 操作系统
- 嵌入式
- 嵌入式OS
【免费下载链接】tock
A secure embedded operating system for microcontrollers
本篇技术指南以 Tock 内核 2021-05-21 核心工作组例会纪要为核心,还原三场关键技术讨论:allow缓冲区重叠的运行时检查开销与 slice-of-cells 方案、进程控制台(Process Console)的 writer 队列/状态机与内存映射打印扩展、以及 libtock-rs 中 Exit 系统调用 API 的未来设计。结合当前仓库源码,读者可完整理解这些议题对应的内核实现位置、设计权衡与最终落地方案,并能直接定位到 syscall.rs、kernel.rs、processbuffer.rs 与 process_console.rs 中的具体实现。
会议背景与参与成员
本次例会由 Tock 核心开发组成员参加,与会者包括 Alex Radovici、Amit Levy、Andrew Malty、Brad Campbell、Gabe Marcano、Hudson Ayers、Johnathan Van Why、Leon Schuermann、Philip Levis、Vadim Sukhomlinov。会议聚焦三个主题:用户态缓冲区重叠问题的处理策略、Andrew Malty 的进程控制台扩展项目展示、以及 libtock-rs 用户库 Exit API 的接口形态设计。这些议题分别对应内核内存安全机制、调试基础设施和系统调用 ABI 三个层面,属于 Tock 在 2021 年中期推进的核心工作。
议题一:allow 缓冲区重叠的运行时检查与 slice-of-cells 方案
运行时重叠检查的开销数据
Phil 首先汇报了为缓冲区重叠做运行时检查的最新测量数据(此前已在 tock-dev 邮件列表中展开讨论)。核心数据如下:
- 整体开销范围为70~300 个 CPU 周期,具体数值取决于同时 outstanding(未完成)的缓冲区数量;测量只覆盖了 0~8 个 outstanding 缓冲区的情形。
- 开销的主要来源是寄存器溢出(register spillage),即编译器为保存/恢复现场产生的额外指令,而非检查逻辑本身。
- 一个典型案例:"insert zero" 场景整体约68 个周期;但如果重构代码,使其不必承担与其他分支相同的前导(preamble)开销,可以降到17 个周期。
Phil 倾向于优先优化"较大规模"的场景,理由是内核应更关注最大可能开销(最坏情况)而非平均值。他也坦言,在不借助汇编编程(出于可移植性考虑他不愿采用)的前提下,进一步压缩开销的空间已经很小。
这一讨论的背景可以在内核源码中找到:Tock 允许用户进程将内存allow给内核,而 Rust 的别名规则(aliasing rules)不允许同一内存区域同时存在可变与只读的 Rust 切片引用。当前内核在 processbuffer.rs 的文档注释中明确写道:用户进程可以把重叠的内存区域allow到不同的ReadableProcessSlice,此时"至少一个可变 Rust 切片与指向重叠内存的只读切片共存会违反 Rust 的别名规则"。这正是会议讨论的"是否需要运行时重叠检查"问题的源头——Rust 类型系统本身无法静态禁止这种重叠。
slice-of-cells 方案:零开销地表达重叠合法性
Hudson 与 Leon 展示了他们用 Rust playground 实现的slice-of-cells原型,目标 API 与当时的AppSlice接近。核心设计要点:
- 该方案不再向内核提供指向底层内存的 Rust 引用,所有变更都通过
copy-to-place、copy-from-slice之类的函数完成,作者认为这样是 sound(内存安全的)。 - 开销非常低;主要限制是无法像普通缓冲区那样整体传递"cells 切片",某些场景必然引入拷贝。
- 关于向 C 函数传递的问题:Hudson 起初认为必须拷贝进 Rust 缓冲区才安全;Amit 指出该表示的底层是透明的(与字节切片表示完全一致),且FFI 本来就是 unsafe 的,调用方本就需要自行推理安全性。Hudson 随后确认:可以直接把 cells 切片引用传给 C 函数。
- Vadim 总结:对 C 函数而言与旧实现没有本质差别,真正的 soundness 保障来自"检查缓冲区不重叠"。
这一设计与当前仓库的实现方向一致。在 processbuffer.rs 中,ReadOnlyProcessBuffer的注释说明:由于用户态可能allow重叠内存,内核选择把缓冲区建立在Cell切片之上,利用Cell的内部可变性(interior mutability)显式支持重叠读写,从而规避 Rust 别名规则;同时仍需在切回用户态前插入内存屏障,因为编译器即使透过Cell也可能重排读写。
重叠读写的实际使用场景
Phil 追问:为什么需要进程向内核传递多个互相重叠的读/写缓冲区?Vadim 给出了典型用例——原地(in-place)加密:当加密大块数据且不希望结果进入另一块内存分配时,可以直接在原始缓冲区上按 16 字节分块处理:读 16 字节、加密、写回。若不允许重叠,就必须准备另一块缓冲区,造成内存浪费。Phil 表示对细节还需邮件讨论,不占用会议时间;Amit 建议感兴趣的成员另行安排专项讨论(Leon 原计划主持,但本周在休假)。
议题二:进程控制台扩展(Process Console Extension)
Andrew Malty 以毕业项目的形式,在过去约 8 周内对进程控制台做了系统扩展,Phil 与 Brad 认为提升进程控制台的功能性是很有价值的工作。扩展内容如下。
writer 队列与大型打印状态机
- 独立 writer:进程控制台最初没有真正的 writer,而是借用 debug writer,仅用于在用户输入末尾添加换行。Andrew 将其升级为功能完整的 writer,并支持 debug 输出语句。
- 队列:为 writer 增加队列,避免数据丢失。
- 状态机处理大打印:新增若干产生大段输出的命令。与其配置大容量队列,不如用状态机分片输出,从而用很小的队列即可打印大段内容而不丢包。
这一设计在当前源码中有直接对应物。在 process_console.rs 中可以看到:
WRITE_BUF_LEN: usize = 500——交给 UART 硬件的外发数据缓冲区;QUEUE_BUF_LEN: usize = 300——响应先暂存于此,再拷贝到 TX 缓冲发送;READ_BUF_LEN: usize = 4——由于按字节读取,回显只需很小的读缓冲;COMMAND_BUF_LEN: usize = 64——命令最长 32 字节(命令本身约 4~5 字符,参数约 25 字节);DEFAULT_COMMAND_HISTORY_LEN: usize = 10——history 命令默认长度;WriterState枚举——状态机各状态:Empty、KernelStart、KernelBss、KernelInit、KernelStack、KernelRoData、KernelText、ProcessPrint { process_id, context }、List { index, total }。注释明确指出:状态机允许跨多次调用异步打印大字符串,从而减小打印每段 debug 消息所需的缓冲区。
memmap:打印进程内存映射
新增命令可打印指定进程的内存映射(memory map)。由于输出很大,必须借助状态机分多段打印,而不是一次性输出一大块。这与上文的WriterState设计配套:内存映射数据量大,单次塞不进 300 字节的队列缓冲,只能按段异步发送。
kernel 命令与驱动列表宏
- 新增一条对内核影响更大的命令,主要用于获取信息供打印输出,可打印内核自身的内存映射(kernel map)。
- 为了展示板级结构(platform structure)中可用的驱动,Andrew 在板级 main 文件中使用了一个宏。他坦言没有找到宏之外的可行思路(曾考虑 derive,但因找不到形态相似的先例,且咨询 Phil 后认为不必要而放弃),宏工作正常但并非理想方案。
关于宏与打印内容的讨论
会议对宏和打印格式展开了深入讨论:
- 宏的 opt-in/opt-out:Amit 询问不使用宏的板子是否会失去进程控制台。Andrew 澄清:进程控制台可以完全独立工作,宏应作为可选输入;当前虽未做成可选,但不加也不影响功能。Phil 强调可选性重要,因为宏会带来一块额外开销。
- 打印字段名还是类型:Alex 提出打印 platform 结构的字段名可能因命名随意而难懂,能否同时打印 driver number。Amit 指出 driver number 目前编码在
Platformtrait 实例的控制流中——按惯例存放在 capsule 的DRIVER_NUM常量里,但并非强制,板子技术上可以用不同编号。Leon 认为字段名更有价值(保证唯一);Amit 认为类型名(驱动模块名)不依赖板子命名,但可能更长或重复(如两个 timer/console 实例)。结论是宏可以扩展出打印类型的变体,属于很小的改动,只需决定是改代码还是由进程控制台请求。
议题三:libtock-rs Exit API 设计讨论
Johnathan 发起讨论:Tock 的 TRD(技术需求文档)中,若干系统调用存在子变体——Memop 有子变体(Tock 1.0 即如此),Yield 有,Exit 也有。TRD 定义了 Exit 的两种变体:**terminate(终止)**与restart(重启),两者都恰好接受一个参数。隐含语义是"一个函数、两个参数"(一个指定 Exit 后的行为,一个指定 completion code),但 TRD 并未如此组织,未来可能出现带不同参数的新 Exit 类型。问题在于libtock-rs 的 Exit API 应当如何设计。
三个候选方案
单一函数、两个参数:第一个参数指定 terminate/restart,第二个指定 completion code。
- 优点:想写可配置退出行为的库时,可把行为枚举整体传入。
- 缺点:若 TRD 将来新增第三种 Exit,该 API 形态可能不再成立。
两个独立函数:
exit_terminate与exit_restart,各自接受 completion code。- 优点:命名清晰、类型安全。
- 缺点:可配置场景需要分别调用。
两者并存(Amit 提出):提供底层灵活函数 + 两个包装函数,引导不需要两参函数功能的人使用独立函数。
- 优点:若未来新增第三种 Exit,可以删除/替换底层单函数,而
exit_terminate、exit_restart保持不变,使用它们的代码不会 break。
- 优点:若未来新增第三种 Exit,可以删除/替换底层单函数,而
各方观点
- Vadim:进程要么终止要么重启,从语义看不必要拆两个函数,一个变体函数即可;且该函数使用频率很低,主要用于实现 panic 之类,大多数进程是常驻的。对枚举可扩展性的担忧,Vadim 认为"扩展枚举即可,不影响大多数应用"。
- Phil:问题在于若新类型带参数,就会改变签名,枚举扩展解决不了。他认可 Amit 的方案"threaded the needle"(两头兼顾),且 Rust 编译器可以可靠内联;从人体工学看,两个独立函数更好记(记方法名比记方法名+枚举名容易),需要动态选择的人可以用底层函数。
- Leon:倾向两个独立函数。核心原则是:大部分 syscall 操作类型不应把自己绑定到具体行为类型上;未来完全可能加入与现状截然不同的调用。若出现完全不兼容的变体(如可能失败的变体),可以另加独立函数,同时保留覆盖现有两种变体的函数。
- Johnathan:担心命名尴尬(如
exit与exit_advanced),且改名仍是 breaking change。 - Amit:从工程角度,底层共用函数本来就有意义——它要用汇编为各架构各写一次实现,写一次比写两次更少出错。Johnathan 澄清:底层汇编函数其实是"带两个参数发起系统调用",与 Memop 共享,已经在其上一层;讨论的 API 比那还高两层,可平凡内联。Amit 还提出,若担心未来扩展,可以把灵活函数标为
unsafe;他本人并不太被"未来扩展系统调用接口会出问题"的论点说服。 - Vadim:难以想象该函数会用于应用逻辑,应被隐藏在库内部,因此对具体实现没有强烈偏好。
会议结论
最终达成一致:同时提供两种 API——灵活的底层函数与exit_terminate/exit_restart包装函数。Leon 表示认可(Rust 的类型安全使未来的 breaking change 重构易于实现),Johnathan 将据此提交 PR。
该设计在 TRD 与内核源码中均有印证。在 syscall.rs 中,系统调用类按 Tock ABI 编码为 8 位值:Yield = 0、Subscribe = 1、Command = 2、ReadWriteAllow = 3、ReadOnlyAllow = 4、Memop = 5、Exit = 6、UserspaceReadableAllow = 7。Syscall::Exit变体(syscall.rs)携带两个字段:which: usize(exit 标识,即 terminate/restart 变体选择)与completion_code: usize(传入内核的完成码)——与会议讨论的"两参函数"语义一一对应。在 kernel.rs 中,Exit与Yield、Memop被明确标注为不可被 syscall filter 过滤的系统调用("Exit is not filterable"),而Memop则由memop::memop(process, operand, arg0)处理(kernel.rs),其operand/arg0双参数结构与会议中"Memop 与 Exit 底层共享'带两参发起系统调用'的汇编函数"的描述吻合。
会议议题的后续影响与实现定位
从当前仓库源码反推,本次会议讨论的成果大多已落地,读者可按以下路径深入:
- 缓冲区重叠与进程缓冲区:processbuffer.rs 中的
ReadOnlyProcessBuffer/ReadWriteProcessBuffer均以Cell切片为底层表示,显式容忍重叠;注释中保留了"内存屏障与别名规则"的完整论证。 - 进程控制台:process_console.rs 实现全部命令(
help status list stop start fault boot terminate process kernel reset panic console-start console-stop),WriterState状态机、独立 writer 队列、ANSI 转义序列处理与命令历史等机制均可在该文件与 capsules/core/src/lib.rs 中查阅;文档注释提示更详细的说明参见仓库中的 Process Console 文档。 - Exit 系统调用:syscall.rs 定义
Syscall::Exit { which, completion_code },kernel.rs 确认其不可过滤,体现了会议"Exit 语义简洁、内核不做策略过滤"的设计取向;libtock-rs 侧的双 API 形态则由 Johnathan 后续的 PR 落地。
结语
本次例会浓缩了 Tock 在内存安全、调试设施与用户库 API 三个层面的典型工程权衡:重叠缓冲区问题上,"运行时检查开销 vs. 类型系统表达力"的两难最终由 slice-of-cells 与Cell切片方案化解;进程控制台通过 writer 队列与分片状态机,在不膨胀队列缓冲的前提下实现了内存映射等大打印能力;Exit API 则确立了"底层灵活函数 + 类型安全包装函数"并存、面向未来扩展的接口哲学。对希望深入 Tock 内核源码的读者,上述四个文件是理解这些设计的最佳起点。
- 操作系统
- 嵌入式
- 嵌入式OS
【免费下载链接】tock
A secure embedded operating system for microcontrollers
相关推荐
Go Mono Nerd Font 补丁字体全指南:Bold-Italic 变体的选择、获取与自行补丁
Go Mono Nerd Font 补丁字体全指南:Bold Italic 变体的选择、获取与自行补丁 本文以 Nerd Fonts 仓库中 Go Mono/B
操作系统嵌入式嵌入式OSOumi 训练方法完全指南:从 SFT、DPO 到 GRPO 的配置与实践
Oumi 训练方法完全指南:从 SFT、DPO 到 GRPO 的配置与实践 Oumi 开源框架(Oumi OSS)在同一套配置体系内封装了监督微调(SFT)、视
操作系统嵌入式嵌入式OSOOTDiffusion AI虚拟试衣完整指南:从部署到出图
OOTDiffusion AI虚拟试衣完整指南:从部署到出图 OOTDiffusion 是基于潜在扩散模型的开源 AI 虚拟试衣工具,能把服装图自然融合进人物照
操作系统嵌入式嵌入式OS
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考