Supporting Linux kernel development in Rust
发布日期: 2020 年 8 月 31 日贡献者: Nelson Elhage
长期以来,Rust 编程语言一直致力于成为操作系统内核开发中 C 语言的理想替代品。随着 Rust 的日益成熟,越来越多的开发者对将其应用于 Linux 内核表现出浓厚兴趣。在 2020 年(线上举行的)Linux Plumbers Conference 上,LLVM 微会议专场组织了一场关于 Linux 内核上游接纳 Rust 存在的开放问题与障碍的讨论会。该议题的热度可见一斑,这场会议也成为 2020 年活动中参与人数最多的一场讨论。
此次会议建立在前众多开发者的工作基础上,包括去年 Alex Gaynor 和 Geoffrey Thomas 在 Linux 安全峰会上的报告。在那次报告中,他们展示了 Rust 内核模块的原型工作,并论证了在内核中采用 Rust 的优势。他们重点关注安全问题,引用数据显示,Android 和 Ubuntu 中被分配 CVE 的内核漏洞中,约三分之二源于内存安全问题。理论上,Rust 可以通过其类型系统和借用检查器提供的更安全 API,完全避免此类错误。
此后,Linus Torvalds 及其他核心内核维护者已表达了在原则上支持使用 Rust 进行内核开发的开放态度。因此,Plumbers 会议的这一专场旨在探讨未来允许 Rust 进入内核代码库所需满足的一些条件。该议题已在 linux-kernel 邮件列表中提出并讨论,部分话题已预先展开。
本次会议也邀请了 Thomas 和 Gaynor,以及 Rust 语言团队联合负责人、长期从事 Linux 内核开发的 Josh Triplett,以及其他多位对此感兴趣的开发者参与。他们简要介绍了目前的工作进展、初步思考及问题,随后将大部分时间留给开放讨论。他们还通过 Thomas 和 Gaynor 的 linux-kernel-module-rust 项目展示了一个简短的示例,展示了内核模式下的 Rust 代码可能的形式。
演讲者强调,他们并非提议将整个 Linux 内核用 Rust 重写,而是专注于推动一个新的方向,使得未来的新代码可以用 Rust 编写。随后的讨论主要围绕 Rust 支持的三个潜在关切领域展开:如何利用内核现有的 API、架构支持问题,以及 Rust 与 C 之间的 ABI 兼容性问题。
与现有 C API 的绑定
要使 Rust 在内核开发中真正发挥作用,仅仅能够生成可链接到内核的代码是远远不够的;还需要让 Rust 能够访问 Linux 内核中大量现有的 API,而这些 API 目前全部定义在 C 头文件中。
Rust 在与 C 代码交互方面拥有良好的支持,包括支持调用使用 C ABI 的函数,以及定义能被 C 代码调用的、兼容 C ABI 的函数。此外,bindgen 工具能够解析 C 头文件以生成相应的 Rust 声明,这样 Rust 就不需要重复 C 中的定义,同时也提供了一定程度的跨语言类型检查。
从表面上看,这些特性使 Rust 具备了与现有 C API 集成的良好条件,但难点在于细节。无论是迄今为止的工作还是会上的讨论,都揭示了一些尚未解决的挑战。例如,Linux 大量使用了预处理宏和内联函数,而 bindgen 和 Rust 的外部函数接口对这些功能的支持并不完善。以无处不在的 kmalloc() 函数为例,它被定义为 __always_inline,这意味着该函数会被内联到所有调用它的地方,内核符号表中并不存在可供 Rust 链接的 kmalloc() 符号。这个问题可以很容易地解决——可以定义一个包含非内联版本的 kmalloc_for_rust() 符号——但手动进行这些变通工作将导致大量的手动劳动和代码重复。这项工作本可以通过改进版的 bindgen 来自动完成,但这样的工具目前尚不存在。
讨论还触及了关于 API 绑定的第二个问题:为了呈现符合 Rust 惯用法的接口,C API 需要多大程度上进行手动“封装”?通过查看两个现有的 Rust 内核模块项目,我们可以了解到这里的一些选择。
在 linux-kernel-module-rust 项目中,指向用户空间的指针被封装在一个 UserSlicePtr 类型中,以确保正确使用 copy_to_user() 或 copy_from_user()。这种封装为 Rust 代码提供了一定程度的安全性(这些指针不能直接解引用),同时也使 Rust 代码更符合惯用法;写入用户空间指针的代码看起来类似于:
user_buf.write(&kernel_buffer)?;
这里的 ? 是 Rust 错误处理机制的一部分;这种返回和处理错误的风格在 Rust 中无处不在。这样的封装使得生成的 Rust 代码对现有的 Rust 开发者来说更加熟悉,并且能让 Rust 的类型系统和借用检查器提供最大程度的安全性。然而,必须为每个 API 精心设计和开发这些封装,这是一项繁重的工作,并且会为用 C 和 Rust 编写的模块创建不同的 API。
相反,John Baublitz 的 演示模块 更直接地绑定了内核的用户访问函数;相应的代码看起来类似于:
if kernel::copy_to_user(buf, &kernel_buffer[0..count]) != 0 {return -kernel::EFAULT;}
这种风格易于实现——绑定主要由 bindgen 自动生成——并且对于那些必须审查或修补 Rust 代码的现有内核开发者来说也会更加熟悉。然而,这种代码对 Rust 开发者来说远不够地道,并且可能会放弃很多 Rust 承诺提供的安全性保证。
会上达成了一些共识,即对于某些最常见和最关键的 API 编写 Rust 封装是有意义的,但手动封装每一个内核 API 将是不可行且不受欢迎的。Thomas 提到,谷歌正在致力于自动生成符合 C++ 代码惯用法的绑定,并思考内核是否可以做类似的事情,也许是基于现有的 sparse 注释,或者在现有 C 代码中添加一些新的注释来指导绑定生成器。
架构支持
讨论的下一个议题是架构支持。目前唯一成熟的 Rust 实现是 rustc 编译器,它通过 LLVM 生成代码。Linux 内核支持广泛的架构,其中一些架构尚无可用的 LLVM 后端。另有少数架构虽有 LLVM 后端,但 rustc 尚未支持该后端。演讲者希望明确:完整的架构支持是否是将 Rust 引入内核的前提条件。
多位与会者表示,在 Rust 中实现那些本就不会在冷门架构上使用的驱动程序是可以接受的。Triplett 以他在 Debian 项目中的经验为例,指出将 Rust 引入内核将有助于推动 Rust 对更多架构的支持。他提到,将 Rust 软件引入 Debian 促使小众架构的爱好者和用户积极改进 Rust 支持,预计在内核中添加支持也会产生类似效果。他特别有信心地表示,任何具备 LLVM 后端的架构都能很快获得 rustc 的支持。
讨论还探讨了通过替代 Rust 实现来拓宽架构支持的路径。**mrustc** 项目是一个实验性的 Rust 编译器,它能生成 C 代码。使用 mrustc 有可能让 Rust 通过编译内核其余部分的同一 C 编译器进行编译。
此外,Triplett 提到,为 GCC 开发 Rust 前端的工作已引起关注并取得进展,这可能使 Rust 能够面向 GCC 支持的所有架构。该项目虽处于早期阶段,但为未来弥合架构差距提供了另一条途径。本部分的结论略显不确定,但似乎并未出现强烈反对在不等待更广泛架构支持的情况下支持 Rust 设备驱动程序的观点。
与内核的 ABI 兼容性
Gaynor 还就 ABI 兼容性问题寻求建议。由于 Rust(目前)通过 LLVM 编译,而内核通常使用 GCC 构建,将 Rust 代码链接到内核中可能意味着混合由 GCC 和 LLVM 生成的代码。尽管 LLVM 旨在保持与 GCC 的 ABI 兼容性,但已有一些反对意见认为该策略存在细微 ABI 不兼容的风险。演讲者想知道内核社区是否更倾向于将 Rust 支持限制在使用 Clang 构建的内核,以确保兼容性。
Greg Kroah-Hartman 确认了当前内核的规则:只有当内核中的所有目标文件均使用同一编译器及相同标志构建时,才能保证兼容性。但他也表示,只要以适当的选项同时构建 LLVM 编译的 Rust 目标文件和 GCC 编译的内核,并对最终配置进行全面测试,将两者链接在一起是可以接受的。他认为除非实际问题出现,否则无需施加额外限制。Florian Weimer 澄清说,ABI 问题往往出现在语言的冷僻角落——例如,按值返回包含位域的结构体——而 ABI 中核心、常用的部分应该不会造成兼容性问题。
Triplett 强调,在用户空间中,GCC 与 Rust 之间的调用是常规且普遍的,因此从 Rust 的角度看,他对兼容性并不担忧。听起来,这一顾虑最终不应成为将 Rust 引入内核的障碍。
结论
会议结束时并未确定具体的后续步骤,但总体来看,社区对最终支持 Rust 模块充满热情,且对支持所需的广泛要求越来越达成共识。下一步的重大进展很可能出现在有人提议将一个真正的 Rust 驱动程序纳入内核之时。具体的用例和实现总能帮助澄清任何遗留的争议问题和设计决策。
Src
https://lwn.net/Articles/829858/