Starnix 内核在 Fuchsia 上实现了 Linux 用户空间 API (UAPI)。Starnix 内核会拦截来自 Linux 进程的系统调用,并管理正确执行 Linux 程序所需的语义。本文档概述了 Starnix 内核的内部结构。
解决方法
Starnix 旨在运行未修改的 Linux 二进制文件。为了按原样执行这些二进制文件,Starnix 与 Linux 内核保持了 bug-for-bug 兼容性。这种高保真互操作性依赖于广泛的测试。
为了解 Linux UAPI 语义,测试会探测接口的边缘情况和极端情况。当这些测试在 Linux 内核上通过后,它们会在 Fuchsia 的持续集成基础架构中运行,最初标记为在 Starnix 上失败。随着 Starnix 的改进,这些测试最终会通过,从而可以标记为通过。
单元测试与用户空间测试
在大多数情况下,Starnix 的测试是编译为 Linux 二进制文件的用户空间程序。这种方法可让相同的测试针对 Starnix 和 Linux 内核运行,从而确保行为完全一致。
在某些情况下,单元测试在 Starnix 内核中运行。这些测试依赖于未通过 UAPI 公开的实现细节。虽然这允许测试内部逻辑,但无法保证行为与 Linux 内核匹配。此外,随着实现方式的不断发展,这些测试通常需要更多维护。不过,对于通过访问内核内部信息更易于测试的场景,此方法非常有用。
在其他情况下,集成测试会在 Fuchsia 端运行用户空间程序,并进行断言。这些测试用于验证无法从用户空间轻松访问的不变性。
track_stub! 宏
Linux UAPI 语义非常丰富。单个系统调用或伪文件可能具有比当前实现更多的功能。track_stub! 宏用于跟踪未实现的语义,记录缺失的代码路径并将其与问题跟踪器 bug 相关联。它还会检测 Starnix 内核二进制文件,以观察 Linux 程序何时触发这些路径。
从概念上讲,原始的 Starnix 内核实现是针对系统调用号的 match 语句,其中每个系统调用都会调用 track_stub! 宏。随着对更复杂的 Linux 程序的支持不断增加,实际实现取代了这些宏。对于具有多个选项或模式的系统调用,track_stub! 会被推送到函数内部以“实现”缺失的选项。
在内部,有一个信息中心会跟踪 track_stub! 宏执行的频率和场景的统计信息。随着 Starnix 功能的扩展,track_stub! 宏会继续跟踪进度。
结构
Starnix 内核由多个 Rust 箱组成。Starnix 内核本身是一个包含 main.rs(主要入口点)的箱。核心机制位于依赖关系图中间的 starnix_core 箱中。
流程模型
Starnix 内核作为作业内的一组 Fuchsia 进程运行。每个 Linux 地址空间(从概念上讲是一个 Linux 进程)对应一个 Fuchsia 进程,此外还有一个额外的初始 Fuchsia 进程。在此附加进程中,Starnix 内核二进制文件会被加载并开始执行。此进程包含 Starnix 共享地址空间,但不包含受限地址空间。
初始进程的主线程运行标准的 Fuchsia 异步执行器来处理 FIDL 请求,例如运行 Starnix 容器的请求。此进程还包含称为 kthread 的后台线程,用于运行内核后台任务。在初始进程中运行这些线程可确保它们比可能触发其创建的用户空间进程更长寿。
starnix_syscall_loop 箱子
创建后,用户空间线程会进入由 starnix_syscall_loop crate 实现的 main syscall 循环。在此循环中,线程以特定的机器状态进入用户模式(即受限模式)。当 Linux 程序退出用户模式时(通常是由于系统调用、异常或被踢回内核模式),线程控制权会返回到 Starnix 内核。
当 Linux 程序发出系统调用时,starnix_syscall_loop crate 中的 dispatch_syscall 函数会对系统调用进行解码,并调用相应的系统调用实现函数。将 dispatch_syscall 与系统调用实现分离,可实现跨多个箱的分片实现。目前,大多数实现都位于 starnix_core 中,但计划将它们移出,以降低复杂性。
模块
许多 Starnix 内核功能都实现为模块。在初始化期间,Starnix 内核仅初始化通过相应的功能标志启用的模块。模块通常会自行向 starnix_core 注册。例如,设备模块使用 DeviceRegistry 注册为特定设备编号的处理程序,而文件系统模块使用 FsRegistry 进行注册。
模块不会直接由 starnix_core 调用。而是实现与其提供的抽象对应的特征。例如,设备模块实现 DeviceOps 特征,而文件系统模块实现 FileSystemOps 特征。这些特征通常会返回实现其他特征的对象,例如 FileOps 和 FsNodeOps。
需要内核全局状态的模块应使用 kernel.expando 对象,而不是在 Kernel 结构体上定义字段。kernel.expando 以 Rust 类型为键,这使得模块可以定义唯一的存储槽,而不会发生冲突风险。此机制还可避免为未使用的模块分配资源。
starnix_core 箱子
starnix_core 箱包含 Starnix 内核的核心机制,包括任务、内存管理、设备注册和虚拟文件系统 (VFS)。这些紧密相关的子系统具有许多循环依赖项。starnix_core 的大部分设计都通过源代码中的 rustdoc 注释记录在案。
库
starnix_core、模块或其他组件所需但独立于 starnix_core 的代码应位于 //src/starnix/lib 目录中的单独箱中。单独的箱可清晰显示代码的依赖关系图,并缩短增量构建时间,因为对 starnix_core 的更改不会触发这些独立库的重新构建。
UAPI
starnix_uapi 箱位于 Starnix 内核依赖关系图的底部。它为 Linux UAPI 概念定义了符合人体工程学的 Rust 类型。例如,如果 Linux UAPI 定义了具有特定位语义的 u32,starnix_uapi 可能会使用 bitflags! 宏来定义相应的 Rust 类型。
starnix_uapi crate 还定义了 UserAddress 类型,该类型表示用户空间地址。Starnix 内核使用 UserAddress 而不是 Rust 指针来处理用户空间内存,以防止意外的取消引用。这可缓解两种危害:
- 用户空间提供内核地址来操纵内核内存。
- 在不使用安全
usercopy机制的情况下访问用户空间地址时,内核会发生 panic。
starnix_uapi crate 依赖于 linux_uapi,这是一个使用 bindgen 从 Linux 程序使用的 Linux UAPI 的 C 定义自动生成的 crate。