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