2021年4月

内核中的第二语言

邮件原文:https://lkml.org/lkml/2021/4/14/1023

众所周知,在内核中引入一种新语言有巨大成本和风险。因为这会分散精力,也会提升对内核某些部分的知识门槛。

最重要的是,引入新语言之后,如果该语言不再被支持了,那用这门语言写的模块之后很难被替换。

无论如何,我们相信,即使在今天,使用Rust也是利大于弊。这些后面会解释。

请注意,我们说的Rust支持,指的是支持用Rust写驱动以及类似的“叶”模块,至少在可见的将来是这样。特别地,并不是要重写内核的核心代码,也不是要重写内核的重要子系统(比如kernel/mmsched/,等等)。对Rust的支持会建立在这些之上。

目标

通过在Linux内核中使用Rust,我们希望达成如下目标。

  • 用Rust写的新代码能降低内存安全隐患、数据争用和整体的逻辑错误,这些是由下面提到的语言特性决定的。
  • 维护者更有信心重构和接收对模块的补丁,这利益于下面提到的Rust安全能力。
  • 新驱动和模块写起来会更容易,因为Rust基于现代语言特性的抽象更容易推断,而且有详尽的文档可供查阅。
  • 更多人可以参与内核开发,因为使用现代语言的人更多。
  • 利用Rust工具生态的优势,可以强制保证之前我们制定的文档规范落地。比如,要求所有公共API、安全前置条件、unsafe块和类型不变量都必须有文档说明。

为什么选Rust

Rust是系统编程语言,就Linux内核而言,拥有几个C所不具备的重要优势。

  • 使用其安全特性时不存在未定义行为(when unsafe code is sound),包括内存安全和不存在数据急用。
  • 类型系统更严格,进一步减少逻辑错误。
  • 可以清晰地区分安全与unsafe代码。
  • 特性丰富:sum类型、模式匹配、泛型、RAII、生命期、共享&专有引用、模块&可见性、强大的清洁能力(hygienic),以及过程式的宏,等等。
  • 大量独立的标准库:ResultOption等词表类型、迭代器、格式化、受控/丰富/包装的整数计算,等等。
  • 开箱即用的工具,如文档生成器、格式化、代码纠错,全部内置在编译器中。

总的来说,Rust这门语言成功吸引了数十年来系统编程乃至函数式编程的经验,而且增加了生命期和借用检查机制。

为什么不用

相对于C,就Linux内核而言,Rust也有一些不足。

  • 多年来围绕内核投入了非常多的人力开发C工具,包括编译器插件、坏码检查(sanitizer)、Coccinelle、lockdep、sparse等。不过,随着Rust在内核上的应用得越来越多,这些问题有可能改善。
  • 基于LLVM的唯一实现。有人正着手解决这个问题,比如GCC前端、基于Cranelift的rustc后端和旨在缩短自举流程的编译器mrustc。欢迎对任何这些项目提供帮助。
  • 没有标准化。尽管还不清楚标准化是否对内核有益,但有几点可以把风险降到最低:Rust稳定性承诺、丰富的文档、WIP参考,还有详尽的RFC,等等。
  • 编译通常比较慢,一方面语言特性比较复杂,另一方面当前的编译器也施加了诸多限制。
  • 目前,还需要一些最新的特性。换句话说,就是稳定版编译器还不支持的特性。无论如何,我们计划在一年内扫清这些限制,要么让rustc稳定版支持它们,要么在我们的代码中去掉相关特性。这里有我们维护的一个报告:https://github.com/Rust-for-Linux/linux/issues/2
  • 当前文本量比预期大,原因是corealloc这两个Rust标准库里有一些未用到的部分。我们计划逐步解决这个问题。

上述问题多数缘于Rust还是一个相对年轻、应用较少的语言。不过,相信Rust很有希望成为系统编程大家庭中的重要成员,就像过去几十年的C一样。因此,多数上述问题都会随着工业界将更多资源倾注于Rust而缓解。

设计

有几个重要的设计决定。

首先,Rust内核模块要求共享一些代码,可以通过一个配置项生效(CONFIG_RUST)。这样可以让个别模块更小。对Rust的支持包括:

  • Rust标准库。目前是corealloc,未来可能只需要core的子集。这些基本上相当于C标准库的那些独立子集。
  • 封装内核API的抽象。代码位于rust/kernel。目的是让它们尽可能安全,因而让用Rust写的模块尽可能少地依赖unsafe代码。
  • 其他,比如module!过程式宏、编译器内置能力、生成的绑定及辅助函数,等等。

支持Rust占据比较大的空间,当然把Rust标准库中那些未用代码删掉会变小。

以下是我们在CI中使用一个小x86_64配置的例子:

   text    data     bss      dec

7464833 1492128 2301996 11258957 vmlinux (without Rust support)
7682527 1709252 2301996 11693775 vmlinux (with    Rust support)
7682527 1721540 2301996 11706063 vmlinux (plus overflow checks)

   2224       0      16     2240 samples/rust/rust_semaphore_c.o
   3694       0      10     3704 samples/rust/rust_semaphore.o
   2367     768      16     3151 samples/rust/rust_semaphore_c.ko
   3829     768      10     4607 samples/rust/rust_semaphore.ko

  80554    5904   20249   106707 drivers/android/binder.o
  12365    1240       9    13614 drivers/android/binder_alloc.o
  92818       8      16    92842 drivers/android/rust_binder.o

在启动溢出检查的vmlinux中,文本(text)多出3%,整体多出4%。模块本身与对应的C代码接近。

上表也比较了Binder和Rust对应的原型实现。注意,虽然Rust版还达不到原始模块的要求,但可以进行大致的估计。如表所见,C Binder的文本总和少于Rust驱动,但加一块更大。

其次,Rust写的模块永远都不应该使用C内核API。因为在内核中使用Rust最主要的目的是提供安全抽象,让模块更容易推断,进而更容易评审、重构,等等。

另外,与内核C端的绑定是通过bindgen(Rust官方工具)动态生成的。这样可以不用在Rust端更新绑定。

宏还是需要手工处理,有些函数会被行内化,对此我们要创建辅助函数从Rust里调用。

第三,在Rust代码中,多数文档都是跟源代码写在一块的,格式为Markdown。我们也遵循了这个惯例,因而虽然一些通用文档在Documentation/rust/里,但实际上多数文档都在源代码文件中。

为了方便阅读文档,Rust提供了一个生成HTML文档的工具,类似于Sphinx/kernel-doc,只不过面向的是Rust代码和语言特性。

此外,正如上面解释的,我们也借这个机会推动了文档策略。同时也推动了代码的自动格式化,一套Clippy代码纠错工具。这里面都是按照Rust的惯例来的,比如保持rustfmt这个默认值。比如,代码缩进是4个空格,而不是制表符宽度。如果需要的话我们也乐意修改,关键在于自动格式化。

最后,为避免将GPL符号暴露为非GPL(即使是间接暴露),我们将Rust支持的所有符号都作为GPL导出到了内核中。

状态

这里展示的Rust支持是试验性的,因此很多内核API及抽象都看不到。要覆盖全部API需要很长时时间开发和成熟。其他实现细节也在进行中。

不过,这个支持已经足够好,可以用来实现模块原型了。本RFC包括对已有模块的可用移植,主要是Binder和Android IPC机制。尽管还不能在产品中使用,但它展示了可以做到哪一步,以及将来的Rust模块可能是什么样的。

说到编译器,我们支持Clang构建的内核,以及可能情况下的LLVM=1构建(比如,只要ClangBuiltLinux支持)。此外我们也维护了GCC构建内核的配置,不过现在已经不建议使用了。以bindgen为GCC后端很适合改进对这些构建的支持。

至于架构,我们已经支持了x86_64arm64ppc64le。要支持其他变体如riscvs390mips也不会太麻烦。

我们也加入了linux-next(签署了特别弃权状)。当前,我们的支持是通过!COMPILE_TEST控制的,因为不想由于错误破坏任何产品CI,但如果对本RFC的反馈是正向的,那我们会去掉这个限制。

上游计划

一如既往,早点介入维护是发现缺失细节的最佳方式,因此我们会在合并窗口一到就发送这些变更。

审阅本RFC

我们希望得到模块作者的意见。特别是关于补丁9的示例和补丁13的Binder。也就是说,身为模块作者,看到那里的Rust代码你会作何感想?你觉得自己将来也会写类似的Rust代码,考虑安全/无UB吗?

对Rust抽象本身及本RFC其他细节的意见我们也欢迎,但请注意这些都在进行中,还没最终完成。

另一个我们希望得到反馈的主题是Rust “原生”文档,即写在代码里的那些注释。已经上传到这里了: https://rust-for-linux.github.io/docs/kernel/

我们这种生成的文档还挺好的。请大家也看一眼,告诉我们你的想法。

测试本RFC

如果想测试一下,请遵循Documentation/rust/quick-start.rst中的Quick Start。其中包括如何设置Rust以及其他构建和测试本RFC所需的工具。

本文写作时,本RFC系列与仓库是对应的,但要想及时跟进,请检出我们主干中的rust分支:https://github.com/Rust-for-Linux/linux.git

致谢

(略)