- 操作系统
- 嵌入式
- 嵌入式OS
【免费下载链接】tock
A secure embedded operating system for microcontrollers
本篇技术指南以 Tock 核心工作组(Core Working Group)2024-06-07 会议纪要(原文见 doc/wg/core/notes/core-notes-2024-06-07.md)为骨架,逐项还原会上围绕三项内核级设计议题的讨论、分歧与最终共识:面向 ADC 流式传输的缓冲区交换方案(PR #4023)、yield-wait系统调用的落地推进、以及基于 AppID 的存储权限 TRD(PR #4021)。读完本文,你将理解 Tock 在"内核与用户态共享缓冲"、"进程等待异步事件"与"持久化存储访问控制"三条主线上正在发生的设计演进,并能结合当前仓库源码(kernel/src/syscall.rs、kernel/src/process_standard.rs、kernel/src/storage_permissions.rs)验证这些设计的实现形态。
会议背景与参与人员
本次例会由 Tock 核心工作组周例会机制召开,该工作组负责管理并引导 Tock 的代码、文档、测试与发布,其章程见 doc/wg/core/README.md。参会者包括 Alex Radovici、Brad Campbell、Amit Levy、Pat Pannuto、Leon Schuermann、Johnathan Van Why,以及被邀请参与 15.4 议题讨论的 Tyler。会议无例行进展更新(Updates: None),全部时间用于三个议题的技术讨论。
议题一:Buffer Swapping PR——流式数据的缓冲区交换设计
本议题围绕 PR #4023(Buffer Swapping)展开,核心场景是 ADC 等流式/网络数据传输中用户态与内核共享缓冲区的交接问题。
问题本质:驱动无法感知缓冲区何时被交换
Alex 首先阐述了问题背景:应用使用两个缓冲区交替使用(swap),而驱动并不知道应用在何时完成了交换。当内核需要按索引(indexing)写入数据时,如果用一个 command 系统调用去通知"交换已发生",就会引入竞态条件(race condition)。
Brad 在会议总结中对问题做了精炼概括:内核从数据源获取数据,当数据到来缓慢时大多数驱动没有问题;挑战在于数据快速到达时,内核必须始终有一个可以写入数据的缓冲区。为此需要多个始终可用的缓冲区。该方案采用单个 allow 缓冲区,内核可在其中放入多个数据项,通过 upcall 通知应用"有数据了",应用随后重新 allow 一个新缓冲区——这一重新 allow 在应用侧是近似原子的操作。
方案之争:双缓冲交换 vs 环绕式 ring buffer
会上存在两种设计路线:
- 双缓冲交换(swap):Alex 提出的方案,即维持两个缓冲区并由应用侧交换。
- 环绕式 ring buffer:由 Leon/Amit 在上一轮会议中命名,Alex 沿用该称呼,但明确承认"它实际上只是一个缓冲区"。
Leon 认为 ring buffer 是更通用的设计,并指出其内存安全性没有问题:因为 Tock 在用户态与内核态之间切换时,任意时刻只有一方在运行,不存在 mutability/immutability 的 unsoundness。但 Alex 随即提出关键质疑:一个在内存中不连续的 ring buffer 如何被应用消费?这暴露了环绕式 ring buffer 在"切片并取得所有权"(slice and take ownership)上的困难——Leon 补充道,若从 15.4 收到一个包再转发给以太网,Alex 的方案更容易把数据传递给其他子系统,而 ring buffer 在"包中途"环绕时很难实现。
Brad 进一步指出:ring buffer 的难点在于包大小不一定已知,而在 15.4 中包大小是已知的,因此 15.4 的 ring buffer(Tyler 实现,填满时覆盖旧包)得以成立;但对通用场景,实现一个"包中途环绕"的 ring buffer 很困难。
checksum 的作用与信任模型争议
Alex 的设计中引入了一个校验机制:对缓冲区前三个字节做 XOR,用于检查应用是否已把前四个字节置零。其作用是:当共享缓冲区未被正确初始化时(例如之前被用作其他用途),校验失败会让 capsule 重置缓冲区,从而避免"无法向格式错误的缓冲区写入"的窘境。
对此存在明显分歧:
- Leon 持保留态度:这类检查在其他实现中并没有做;内核默认应用只会 allow 未被其他应用使用的内存,这不该由内核负责管理。
- Amit 的视角:这并不是不信任应用,而是"允许应用自己坑自己"(shoot itself in the foot)。他追问:若没有 checksum,最坏情况是什么?答案是 capsule 无法向格式错误的缓冲区写入;而有了 checksum,内核可以检测出格式错误的缓冲区(未置零)并主动重置。
- Alex 本人表态:对是否保留 checksum 没有强烈立场。
这一争论本质上是对 Tock 内核信任模型的边界讨论:内核是否应当为应用提供"自我修复"能力,还是保持最小干预。
15.4 的兼容性与"新包 vs 旧包"权衡
Tyler 确认该方案对 15.4 可行:目前 15.4 使用两个由应用交换的缓冲区,每个缓冲区是一个覆盖最旧数据的 ring buffer,大小为 n 个 15.4 包大小;切换到 Alex 的方案改动不大。Brad 补充了现状差异:ADC 驱动有多个 allow 缓冲区并遍历使用,而该 PR 只使用一个 allow 缓冲区,但减少了系统调用次数。
会议还讨论了数据丢失语义:Alex 的方案在数据到来过快时丢失新包(从通知应用到应用重新 allow 之间存在窗口),而 15.4 的 ring buffer 丢失的是旧包。Leon 强调非定长包(non fixed size packets)同样重要。关于可变长度包,Amit 提出两种思路:让包自身携带长度信息,或在驱动语义中前置长度字段(prepending)。
会议结论
- 就"是否实现 ring buffer 功能"进行表决,Amit/Brad/Leon 一致投反对票:对 Alex 的 ADC 流式场景,ring buffer 过度复杂;该方案的核心价值在于流式传输(streaming)。
- Amit 将这一设计定位为"一个网络队列"(a network queue)。
- Alex 同意将 PR 更名,不再使用 "ring buffer" 命名,使其名称更贴合"流式缓冲"的实际语义。
- Tyler 将在会后详细复核,确认与 15.4 的兼容性,初步判断无问题。
议题二:Yield Wait——让应用可靠等待异步事件
第二个议题是yield-wait的推进状态。Brad 列出待办:审查现有实现、确认是否需要更新、并做更多测试。Alex 主动承担该项工作;Amit 评价其"影响面很大、非常重要"(very high impact and important)。
需求来源:教程暴露出的可靠性问题
Leon 说明需求来源:该需求在准备教程时出现,并产生了大量问题。有了yield-wait,将更容易编写可靠的应用;Alex 也反馈自己遇到过相关问题。这解释了为何该特性被列为高优先级:它直接改善用户态应用编写异步逻辑的体验。
源码视角:yield-wait 的语义与实现
从当前仓库源码可以印证该机制的设计与实现:
- 在 kernel/src/syscall.rs 的 yield 系统调用类说明中(第 43-51 行):当进程调用 yield 时,内核检查是否有待处理的 upcall;若有则将一个 upcall 压入进程栈;若没有,
yield-wait将使进程睡眠直到某个 upcall 被触发,而yield-no-wait则立即返回。 - kernel/src/syscall.rs 定义了返回类型
SyscallReturn::YieldWaitFor(usize, usize, usize),携带三个返回值参数。 - kernel/src/kernel.rs 在 syscall 分发处通过
.set_syscall_return_value(SyscallReturn::YieldWaitFor(a0, a1, a2))设置返回值。 - kernel/src/utilities/arch_helpers.rs 中将
YieldWaitFor与 TRD104 系统调用返回值规范互相转换(TRD104SyscallReturn::YieldWaitFor),涵盖 32 位与 RISC-V 64 位两种编码。
进程侧的关键实现在 kernel/src/process_standard.rs:
- 新增进程状态
State::YieldedFor(_)(等待特定 upcall 的 yield 状态),并配套字段is_yield_wait_for_ready: Cell<bool>(第 571 行)记录该进程在YieldedFor状态下是否有任务就绪。 enqueue_task(第 598-647 行)在入队任务时,若发现进程正等待该 upcall(State::YieldedFor(yielded_upcall_id)与入队任务的 upcall id 匹配),则设置is_yield_wait_for_ready为 true。代码注释特别说明:若进程等待的是另一个 upcall,不清除该标志,以免重新引入 issue #5195 的竞态条件。ready()(第 649-656 行)判断进程是否可调度:State::Running恒为就绪,State::YieldedFor(_)取决于就绪标志,普通State::Yielded则看任务环缓冲区是否有元素。
由此可见,yield-wait的落地方案不仅包含"睡眠直到 upcall 到来"的基本语义,还针对"等待指定 upcall"的场景做了精细的状态跟踪与竞态规避,这正是会议上"让可靠应用更易编写"诉求的实现基础。
议题三:Storage Permission TRD——基于 AppID 的存储权限架构
第三个议题讨论 PR #4021(Storage Permission TRD),其设计文档即 doc/reference/trd-storage-permissions.md。
演进脉络:从 TBF header 到 AppID
Brad 说明:现有实现源于 TBF(Tock Binary Format)header 中的存储权限字段,TRD 与之非常相似但有小改动。关键差异在于使用 AppID 标识任何存储内容的属主——旧有的 storage permissions header 未使用 AppID,因为当时 AppID 尚不存在。因此该 TRD 实际上是让存储权限架构与既有威胁模型(四年前已稳定)保持一致,正如 Johnathan 所指出的:"这个 TRD 只是让存储 TRD 符合我们已经稳定的威胁模型。"
TRD 的定位:默认策略还是强制策略?
Amit 提出两个关键问题:该 TRD 是否规定了未指定情况下的默认策略?是否意味着不允许存在采用不同策略的存储驱动?
Brad 的回答划定了边界:
- 策略如何设定是另一回事,不同驱动的不同实现可以采用不同策略;
- 设计意图是:只要没有东西使应用丧失访问自身状态的资格,应用就应当能访问自身状态,该设计让"需要被明确排除"这一点更加清晰;
- 若某实现要存储状态,它必须提供获取应用权限的 API,必须使用内核的权限系统,并且必须用 AppID 标识附着于应用的状态;使用不同的权限系统将需要 fork Tock。
Leon 追问该 TRD 与未来更丰富抽象(如文件系统)的关系。Johnathan 给出 Unix 类比:在 Unix 中与之对应的抽象是user,而这里 AppID 扮演了 user 的角色。Amit 设想可以将权限实现附着于每个存储系统对象上。讨论最终达成补充意见:可在 TRD 中加一句澄清——本 TRD 描述的是 canonical(规范)情形,不限制其他使用场景与未来实现。Alex 则预告计划在夏季实现 FAT 文件系统。
源码视角:权限模型的五种形态
当前仓库的 kernel/src/storage_permissions.rs 已实现该 TRD 描述的权限架构。核心类型StoragePermissions内部为私有枚举StoragePermissionsPrivate,包含五种权限形态(第 33-63 行):
| 变体 | 语义 |
|---|---|
SelfOnly(NonZeroU32) | 应用只能访问自己的状态,写入即写入自身 ShortId 标识的状态;NonZeroU32即应用的ShortId::Fixed |
FixedSize(FixedSizePermissions) | 可配置是否可写、是否可读写自身状态,并支持最多 8 个可读标识 + 8 个可修改标识(每个为 u32) |
Listed(ListedPermissions) | 同上,但读写标识来自静态切片(&'static [u32]),数量任意 |
Kernel | 仅供内核使用,允许内核存取标识为 0 的状态,但不授予内核访问应用状态的权限 |
Null | 应用对任何持久化状态无访问权限 |
构造方法均受 capability 限制(如ApplicationStorageCapability、KerneluserStorageCapability),确保权限只能通过受控路径创建。对外暴露的判定 API 与 TRD 一致:
check_read_permission(&self, stored_id: u32) -> bool:查询是否可读某存储标识;check_modify_permission(&self, stored_id: u32) -> bool:查询是否可修改某存储标识;get_write_id(&self) -> Option<u32>:取回写入时应使用的存储标识,无写权限时返回None。
对应 TRD 的设计要点(见 doc/reference/trd-storage-permissions.md):应用写入数据时,必须以其 ShortId 作为存储标识;权限分三种独立类型——write 是布尔权限(要么能写要么不能写),read 与 modify 是(权限类型, 存储状态标识)的元组,仅与特定存储标识关联。例如一个应用可以拥有读取特定数据的权限,却没有写入新数据的权限,三种权限彼此独立可组合。
权限检查的判定逻辑
以check_read_permission为例(kernel/src/storage_permissions.rs),各变体的判定规则为:
SelfOnly(id):仅当stored_id == id时允许;FixedSize(p)/Listed(p):若read_modify_self为真且stored_id等于应用自身 AppID 则允许,或stored_id != 0且出现在 read 列表中则允许(FixedSize只扫描前read_count个有效条目);Kernel:仅允许stored_id == 0;Null:恒为false。
这套逻辑清晰体现了"应用默认可访问自身状态、跨应用访问必须显式授权"的设计意图,与会议上 Brad 所述"除非被明确排除否则可访问"的方针完全对应。
会议共识与后续动作
综观本次会议,三项议题均取得明确结论:
- 流式缓冲(PR #4023):放弃 ring buffer 路线,保留单 allow 缓冲区 + upcall 通知 + 原子重新 allow 的简洁设计,PR 更名以反映流式语义;Tyler 会后复核 15.4 兼容性。
- yield-wait:由 Alex 负责推进,待办为审查实现、必要更新与扩充测试,目标是提升应用异步编程的可靠性。
- 存储权限 TRD(PR #4021):确认以 AppID(Unix 中 "user" 的对应物)作为存储属主标识,权限系统由内核统一提供,不同驱动可有不同策略但必须走内核权限 API;考虑补充"canonical case"澄清句,并预留文件系统等未来抽象的空间。
这些讨论与当前仓库中 kernel/src/syscall.rs、kernel/src/process_standard.rs、kernel/src/storage_permissions.rs 的实现相互印证:会议纪要是设计决策的现场记录,而源码则是决策落地后的最终形态,两者结合阅读,可以完整还原 Tock 内核在流式 I/O、异步等待与存储安全三条主线上的演进逻辑。
- 操作系统
- 嵌入式
- 嵌入式OS
【免费下载链接】tock
A secure embedded operating system for microcontrollers
相关推荐
Tock 2024-12-06 核心工作组会议纪要解读:隔离存储、寄存器类型抽象与硬件 CI 驱动的发布流程
Tock 2024 12 06 核心工作组会议纪要解读:隔离存储、寄存器类型抽象与硬件 CI 驱动的发布流程 本文是对 Tock 核心工作组(Core WG)2
操作系统嵌入式嵌入式OSTock 网络栈演进纪事:从 802.15.4 缓冲区安全到 PacketBuffer 上流化(Network WG 2024-07-15)
Tock 网络栈演进纪事:从 802.15.4 缓冲区安全到 PacketBuffer 上流化(Network WG 2024 07 15) 导读 :本文依据
操作系统嵌入式嵌入式OS2024-06-15 贡献者会议纪要
2024 06 15 贡献者会议纪要 决策事项 x 采用方案B实现泛型数组解析 1234 行动项 @username: 7月1日前提交方案B的实现PR @use
开发工具代码生成文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考