长期以来,Rust 编程语言的一个目标都是能替代在操作系统内核开发中最常用的 C 语言。随着 Rust 的逐步成熟,许多开发人员越来越有兴趣在 Linux 内核中尝试 Rust。在 2020 (virtual) Linux Plumbers Conference 会议上,LLVM 这个微会议的诸多议题中就举办了一场讨论,关于 Linux 内核中接受 Rust 代码还有那些未解决的问题或者障碍。这是 2020 年活动中参加人数最多的一次会议,从中可以看出人们对这个议题有多么感兴趣了。
这个会议之前已经有许多开发者做了不少工作了,包括去年 Alex Gaynor 和 Geoffrey Thomas在 Linux Security Summit 安全峰会上的一次演讲。当时,他们介绍了 Rust 内核模块的一个原型,并提出了在内核中采用 Rust 的理由。他们的重点是在安全问题上,指出在 Android 和 Ubuntu 中,约有三分之二的内核漏洞被分配到 CVE 中,这些漏洞都是来自于内存安全问题。原则上,Rust 可以通过其 type system 和 borrow checker 所提供的更安全的 API 来完全避免这类错误。
此后,Linus Torvalds 和其他核心内核维护者都表示原则上对支持 Rust 的内核开发持开放态度,因此 Plumbers 的会议旨在列出具体能让 Rust 进入 Linux kernel 的一些要求。这次会议是在 linux-kernel 邮件列表上提出并讨论的,当时就已经提出了一部分讨论主题。
这次会议的主角也是 Thomas 和 Gaynor,还有 Josh Triplett——Rust 语言团队的联合领导者,也是一位长期从事 Linux 内核开发的人——以及其他一些对此感兴趣的开发者。他们简单地谈了一下他们到目前为止的工作,以及他们最初的一些想法和问题,然后的大部分时间进行讨论。他们举了一个简单的例子,说明内核模式的 Rust 代码可能是什么样的(来自 Thomas 和 Gaynor 的 linux-kernel-module-rust 项目,https://github.com/fishinabarrel/linux-kernel-module-rust/)。
发言者强调,他们并不是提议将 Linux 内核重写成 Rust,他们只是关注于走向一个可以用 Rust 编写新代码的世界。接下来的对话集中在 Rust 支持的三个潜在关注点上:利用内核中现有的 API,架构支持,以及关于 Rust 和 C 之间 ABI 兼容性的问题。
对现有 C API 的绑定(Binding to existing C APIs)
要想对内核开发能有实际价值的话,Rust 如果只是能够生成可以链接到内核的代码,这是不够的,还需要有一种方法让 Rust 能够访问 Linux 内核中在使用的大量 API,目前这些 API 都是在 C 头文件中定义的。Rust 对与 C 代码的互操作有很好的支持,包括既支持使用 C ABI 调用函数,也支持定义与 C 兼容的 ABI 的函数,这些函数可以由 C 语言中调用。此外,bindgen 工具能够解析 C 头文件,生成相应的 Rust 声明,这样 Rust 就不需要从 C 中重复定义,这也提供了一定程度的跨语言类型检查。
从表面上看,这些特性使 Rust 具备了与现有 C API 集成的能力,但魔鬼就在细节中,迄今为止的工作和会议上的对话都说明这里实现中有不少的挑战。例如,Linux 大量使用预处理宏(preprocessor macro)和内联函数(inline function),而这些函数并不容易被 bindgen 和 Rust 的 foreign-function interface 接口所支持。
例如,非常常用的 kmalloc() 函数就被定义为 __always_inline,这意味着它的所有调用都是 inline 的,内核符号表中没有 kmalloc() 符号, Rust 也就无法进行链接调用。这个问题可以很容易地解决,我们可以定义一个包含非 inline 版本的 kmalloc_for_rust() 符号,但是手工处理这些变通解决方案会导致大量的手工工作和重复的代码。这项工作有可能通过改进版的 bindgen 自动完成,但目前工具还不具备这个功能。
对话中还提到了第二个关于 API 绑定的问题:C API 需要进行多大程度的手动 "wrapped(包装)"才能提供出地道的 Rust 接口?看看现有的两个 Rust 内核模块项目,就能体会到这里的一些麻烦。
在 linux-kernel-module-rust 项目中,进入用户空间的指针被 wrap 成 UserSlicePtr 类型,这确保了 copy_to_user()或 copy_from_user()的可以正确使用。这个 wrapper 在 Rust 代码中提供了一定程度的安全性功能(因为这类指针不能直接 dereference),同时也使 Rust 代码更加地道。要想写入用户空间指针的话,代码看起来就像是这样:
代码语言:javascript复制user_buf.write(&kernel_buffer)?
这里的"?"是 Rust 错误处理机制的一部分,这种 return and handling error 的处理风格在 Rust 中无处不在。这类 wrapper 使现有的 Rust 开发者更加熟悉所产生的 Rust,并使 Rust 的 type system 和 borrow checker 能够尽量确保安全。然而,每一个 API 都需要这样的精心设计和开发,工作量巨大,也会导致 C 和 Rust 编写的模块有不同的 API。
John Baublitz 的 demo module(https://github.com/jbaublitz/knock-out ),则是直接地绑定了内核的用户访问函数。相应的代码看起来是这样的:
代码语言:javascript复制if kernel::copy_to_user(buf, &kernel_buffer[0...count]) != 0 {
return -kernel::EFAULT;
}
这种改法实现起来很容易,binding 主要是由 bindgen 自动生成的,而且对于现有的内核开发者来说,他们也会不那么排斥 review 或者修改 Rust 代码。然而,对于 Rust 开发者来说,这种代码就不那么习惯了,而且可能会损失 Rust 原本可以保证的一些安全性。
会议上大家一致认为,对于一些最常见的关键 API,有必要编写 Rust 版本的 wrapper,但不可能对每一个内核 API 都手动去写一个 wrapper,这是不可取的做法。Thomas 提到 Google 正在研究自动生成 C 代码的规范化绑定动作,不知道内核是否可以做一些类似的事情,也许是建立在现有的 sparse annotation(kernel 中使用的语义检查工具)基础之上,或者是在现有的 C 代码中添加一些新的注释来引导 binding generator 的工作。
架构支持(Architecture support)
下一个讨论话题是架构支持。目前,唯一成熟 Rust 编译器只有 rustc 这一个,它是通过 LLVM 来生成指令码。Linux 内核支持许多种体系架构,其中一些架构并没有现成的 LLVM 后端(backend)。其他一些架构存在 LLVM 后端,但 rustc 还尚未不支持该后端。演讲者想知道,全架构支持是否是在内核中启用 Rust 的一个障碍。
有几个人说,在 Rust 中实现驱动是可以接受的,但无论如何,这些驱动永远不会用在比较少见的架构上。Triplett 以他在 Debian 项目中的经验为例,认为在内核中加入 Rust 将有助于推动更多架构对 Rust 的支持。他提到,将 Rust 软件引入 Debian 有助于激励小众架构的爱好者和用户提高对 Rust 的支持,他认为在内核中加入 Rust 后也会有类似的效果。尤其是,任何具有 LLVM 后端的架构预计都可以很快得到 rustc 的支持。
对话中也提及了还有其他一些可选的 Rust 实现版本,这样可以有助于支持更多的架构。mrustc 项目就是一个实验性的 Rust 编译器,它可以生成 C 代码。使用 mrustc 有可能可以让 Rust 通过编译内核其他部分所用的那个 C 编译器来编译。
此外,Triplett 还提到了一些针对 GCC 的 Rust 前端(front end)的工作,这些工作有可能使 Rust 能够支持 GCC 所支持的任一架构。这个项目还处于早期阶段,但它也提供了另一条途径,可以在将来弥补架构支持方面的缺陷。本节的结论虽然没有给出非常确定的答案,但看起来似乎并没有人强烈反对这个“先支持 Rust 设备驱动程序再完成更多架构支持”的计划。
ABI 与内核的兼容性(ABI compatibility with the kernel)
Gaynor 还就 ABI 的兼容性问题请教大家。由于 Rust(目前)是通过 LLVM 编译的,而内核主流上来说还是用 GCC 构建的,因此,将 Rust 代码链接到内核中可能意味着混合 GCC 和 LLVM 生成的代码。尽管 LLVM 的目标是与 GCC 的 ABI 兼容,但还是有点担心这种做法会造成微小的 ABI 不兼容的风险,因此碰到一些阻力。演讲者们想知道内核社区是否更愿意将 Rust 的支持限制在用 Clang 构建的内核上,从而确保兼容性。
Greg Kroah-Hartman 确认说当前的内核规则是:只有当内核中的所有对象文件(object file)都是用相同的编译器,使用相同的 flags 来构建时,兼容性才会得到保证。然而,他也表示,可以将 LLVM 构建的 Rust object 链接到 GCC 构建的内核中,只要这些对象是同时构建的,并设置了适当的选项,而且所产生的这组 configuration 是经过充分测试的,那就可以。他认为在没有出现实际问题之前,没有必要进行额外的限制。Florian Weimer 澄清说,ABI 问题往往是在语言中不明显的角落。例如,通过返回值来返回一个包含 bitfield 的结构这种情况。他认为 ABI 中最主要且常用的部分应该不会造成兼容性问题。
Triplett 强调,GCC 和 Rust 之间的调用在用户空间中是很常见的,也是正常用法,所以从 Rust 方面来说,他并不担心兼容性问题。听起来,这个问题最终应该不会成为将 Rust 引入内核的障碍。
结论(Conclusions)
会议结束时没有定出具体的下一步,但总体看来,人们对最终支持 Rust module 的热情以及需求 越来越形成了一致意见。下一个重要的步骤很可能是某人提出一个真正的 Rust 驱动,并将其纳入内核。只要有一个具体的用例和实现,总是大力推动来搞清楚任何剩余有争议的问题,并达成设计结论。