如何用 Rust 编写一个 Linux 内核模块
近些年来 Rust 语言由于其内存安全性和性能等优势得到了很多关注，尤其是 Linux 内核也在准备将其集成到其中，因此，我们特邀阿里云工程师苏子彬为我们介绍一下如何在 Linux 内核中集成 Rust 支持。 
Linux 内核 Rust
1.56.199.157
Rust要支棱起来了。
来自黑龙江牡丹江的 Firefox 89.0|GNU/Linux 用户
2021-07-06T15:23:07.000
120.197.198.41
还是用ada吧
来自广东佛山的 Chrome Mobile 71.0|Android 6.0 用户
2021-10-09T14:58:46.000
159.223.57.77
为什么内核配置中的General setup&nbsp;&nbsp;---&gt;Rust support找不到，rustc编译环境已经安装了
来自美国的 Chrome 96.0|Windows 10 用户
2021-12-21T22:10:25.000
222.205.8.88
有没有解决啊，我的也是这种情况
来自浙江杭州浙江大学的 Chrome 104.0|Windows 10 用户
2022-09-05T18:05:12.000
近些年来 Rust 语言由于其内存安全性和性能等优势得到了很多关注，尤其是 Linux 内核也在准备将其集成到其中，因此，我们特邀阿里云工程师苏子彬为我们介绍一下如何在 Linux 内核中集成 Rust 支持。 
![](/data/attachment/album/202107/06/123438wl3c1llssams0aa5.jpg)



> 
> 编者按：近些年来 Rust 语言由于其内存安全性和性能等优势得到了很多关注，尤其是 Linux 内核也在准备将其集成到其中，因此，我们特邀阿里云工程师苏子彬为我们介绍一下如何在 Linux 内核中集成 Rust 支持。 
> 
> 
> 


2021 年 4 月 14 号，一封主题名为《[Rust support](https://lkml.org/lkml/2021/4/14/1023)》的邮件出现在 LKML 邮件组中。这封邮件主要介绍了向内核引入 Rust 语言支持的一些看法以及所做的工作。邮件的发送者是 [Miguel Ojeda](https://ojeda.dev/)，为内核中 Compiler attributes、.clang-format 等多个模块的维护者，也是目前 Rust for Linux 项目的维护者。


Rust for Linux 项目目前得到了 [Google 的大力支持](https://www.zdnet.com/article/rust-in-the-linux-kernel-just-got-a-big-boost-from-google/)，[Miguel Ojeda](https://ojeda.dev/) 当前的全职工作就是负责 Rust for Linux 项目。


长期以来，内核使用 C 语言和汇编语言作为主要的开发语言，部分辅助语言包括 Python、Perl、shell 被用来进行代码生成、打补丁、检查等工作。2016 年 Linux 25 岁生日时，在对 Linus Torvalds 的一篇 [采访](https://www.infoworld.com/article/3109150/linux-at-25-linus-torvalds-on-the-evolution-and-future-of-linux.html)中，他就曾表示过：



> 
> 这根本不是一个新现象。我们有过使用 Modula-2 或 Ada 的系统人员，我不得不说 Rust 看起来比这两个灾难要好得多。
> 
> 
> 我对 Rust 用于操作系统内核并不信服（虽然系统编程不仅限于内核），但同时，毫无疑问，C 有很多局限性。
> 
> 
> 


在最新的对 [Rust support](https://lkml.org/lkml/2021/4/14/1023) 的 RFC 邮件的回复中，他更是说：



> 
> 所以我对几个个别补丁做了回应，但总体上我不讨厌它。
> 
> 
> 


没有用他特有的回复方式来反击，应该就是暗自喜欢了吧。


目前 Rust for Linux 依然是一个独立于上游的项目，并且主要工作还集中的驱动接口相关的开发上，并非一个完善的项目。


项目地址： <https://github.com/Rust-for-Linux/linux>


### 为什么是 Rust


在 [Miguel Ojeda](https://ojeda.dev/) 的第一个 RFC 邮件中，他已经提到了 “Why Rust”，简单总结下：


* 在<ruby> 安全子集 <rt>  safe subset </rt></ruby>中不存在未定义行为，包括内存安全和数据竞争；
* 更加严格的类型检测系统能够进一步减少逻辑错误；
* 明确区分 `safe` 和 `unsafe` 代码；
* 更加面向未来的语言：`sum` 类型、模式匹配、泛型、RAII、生命周期、共享及专属引用、模块与可见性等等；
* 可扩展的独立标准库；
* 集成的开箱可用工具：文档生成、代码格式化、linter 等，这些都基于编译器本身。


### 编译支持 Rust 的内核


根据 [Rust for Linux 文档](https://github.com/Rust-for-Linux/linux/blob/rust/Documentation/rust/quick-start.rst)，编译一个包含 Rust 支持的内核需要如下步骤：


1. 安装 `rustc` 编译器。Rust for Linux 不依赖 cargo，但需要最新的 beta 版本的 `rustc`。使用 `rustup`命令安装：



```
rustup default beta-2021-06-23

```
2. 安装 Rust 标准库的源码。Rust for Linux 会交叉编译 Rust 的 `core` 库，并将这两个库链接进内核镜像。



```
rustup component add rust-src

```
3. 安装 `libclang` 库。`libclang` 被 `bindgen` 用做前端，用来处理 C 代码。`libclang` 可以从 [llvm 官方主页](https://github.com/llvm/llvm-project/releases) 下载预编译好的版本。
4. 安装 `bindgen` 工具，`bindgen` 是一个自动将 C 接口转为 RustFFI 接口的库：



```
cargo install --locked --version 0.56.0 bindgen

```
5. 克隆最新的 Rust for Linux 代码：



```
git clone https://github.com/Rust-for-Linux/linux.git

```
6. 配置内核启用 Rust 支持：



```
Kernel hacking
  -> Sample kernel code
    -> Rust samples

```
7. 构建：



```
LIBCLANG_PATH=/path/to/libclang make -j LLVM=1 bzImage

```

这里我们使用 `clang` 作为默认的内核编译器，使用 `gcc` 理论上是可以的，但还处于 [早期实验](https://github.com/Rust-for-Linux/linux/blob/rust/Documentation/rust/quick-start.rst#building) 阶段。


### Rust 是如何集成进内核的


#### 目录结构


为了将 Rust 集成进内核中，开发者首先对 Kbuild 系统进行修改，加入了相关配置项来开启/关闭 Rust 的支持。


此外，为了编译 `rs` 文件，添加了一些 `Makefile` 的规则。这些修改分散在内核目录中的不同文件里。


Rust 生成的目标代码中的符号会因为 `Mangling` 导致其长度超过同样的 C 程序所生成符号的长度，因此，需要对内核的符号长度相关的逻辑进行补丁。开发者引入了 “大内核符号”的概念，用来在保证向前兼容的情况下，支持 Rust 生成的目标文件符号长度。


其他 Rust 相关的代码都被放置在了 `rust` 目录下。


#### 在 Rust 中使用 C 函数


Rust 提供 FFI（<ruby> 外部函数接口 <rt>  Foreign Function Interface </rt></ruby>）用来支持对 C 代码的调用。[Bindgen](https://github.com/rust-lang/rust-bindgen) 是一个 Rust 官方的工具，用来自动化地从 C 函数中生成 Rust 的 FFI 绑定。内核中的 Rust 也使用该工具从原生的内核 C 接口中生成 Rust 的 FFI 绑定。



```
quiet_cmd_bindgen = BINDGEN $@
      cmd_bindgen = \
	$(BINDGEN) $< $(shell grep -v '^\#\|^$$' $(srctree)/rust/bindgen_parameters) \
		--use-core --with-derive-default --ctypes-prefix c_types \
		--no-debug '.*' \
		--size_t-is-usize -o $@ -- $(bindgen_c_flags_final) -DMODULE

$(objtree)/rust/bindings_generated.rs: $(srctree)/rust/kernel/bindings_helper.h \
	$(srctree)/rust/bindgen_parameters FORCE
	$(call if_changed_dep,bindgen)

```

#### ABI


Rust 相关的代码会单独从 `rs` 编译为 `.o`，生成的目标文件是标准的 ELF 文件。在链接阶段，内核的链接器将 Rust 生成的目标文件与其他 C 程序生成的目标文件一起链接为内核镜像文件。因此，只要 Rust 生成的目标文件 ABI 与 C 程序的一致，就可以无差别的被链接（当然，被引用的符号还是要存在的）。


#### Rust 的 `alloc` 与 `core` 库


目前 Rust for Linux 依赖于 `core` 库。在 `core` 中定义了基本的 Rust 数据结构与语言特性，例如熟悉的 `Option<>` 和 `Result<>` 就是 `core` 库所提供。


这个库被交叉编译后被直接链接进内核镜像文件，这也是导致启用 Rust 的内核镜像文件尺寸较大的原因。在未来的工作中，这两个库会被进一步被优化，去除掉某些无用的部分，例如浮点操作，Unicode 相关的内容，Futures 相关的功能等。


之前的 Rust for Linux 项目还依赖于 Rust 的 `alloc` 库。Rust for Linux 定义了自己的 `GlobalAlloc` 用来管理基本的堆内存分配。主要被用来进行堆内存分配，并且使用 `GFP_KERNEL` 标识作为默认的内存分配模式。


不过在在最新的 [拉取请求](https://github.com/Rust-for-Linux/linux/pull/402) 中，社区已经将移植并修改了 Rust的 `alloc` 库，使其能够在尽量保证与 Rust 上游统一的情况下，允许开发者定制自己的内存分配器。不过目前使用自定义的 `GFP_` 标识来分配内存依然是不支持的，但好消息是这个功能正在开发中。


### “Hello World” 内核模块


用一个简单的 Hello World 来展示如何使用 Rust 语言编写驱动代码，`hello_world.rs`:



```
#![no_std]
#![feature(allocator_api, global_asm)]

use kernel::prelude::*;

module! {
    type: HelloWorld,
    name: b"hello_world",
    author: b"d0u9",
    description: b"A simple hello world example",
    license: b"GPL v2",
}

struct HelloWorld;

impl KernelModule for HelloWorld {
    fn init() -> Result<Self> {
        pr_info!("Hello world from rust!\n");

        Ok(HelloWorld)
    }
}

impl Drop for HelloWorld {
    fn drop(&mut self) {
        pr_info!("Bye world from rust!\n");
    }
}

```

与之对应的 `Makefile`：



```
obj-m := hello_world.o

```

构建：



```
make -C /path/to/linux_src M=$(pwd) LLVM=1 modules

```

之后就和使用普通的内核模块一样，使用 `insmod` 工具或者 `modprobe` 工具加载就可以了。在使用体验上是没有区别的。


#### `module! { }` 宏


这个宏可以被认为是 Rust 内核模块的入口，因为在其中定义了一个内核模块所需的所有信息，包括：`Author`、`License`、`Description` 等。其中最重要的是 `type` 字段，在其中需要指定内核模块结构的名字。在这个例子中：



```
module! {
    ...
    type: HelloWorld,
    ...
}

struct HelloWorld;

```

#### `module_init()` 与 `module_exit()`


在使用 C 编写的内核模块中，这两个宏定义了模块的入口函数与退出函数。在 Rust 编写的内核模块中，对应的功能由 `trait KernelModule` 和 `trait Drop` 来实现。`trait KernelModule` 中定义 `init()` 函数，会在模块驱动初始化时被调用；`trait Drop` 是 Rust 的内置 trait，其中定义的 `drop()` 函数会在变量生命周期结束时被调用。


#### 编译与链接


所有的内核模块文件会首先被编译成 `.o` 目标文件，之后由内核链接器将这些 `.o` 文件和自动生成的模块目标文件 `.mod.o` 一起链接成为 `.ko` 文件。这个 `.ko` 文件符合动态库 ELF 文件格式，能够被内核识别并加载。


### 其他


完整的介绍 Rust 是如何被集成进内核的文章可以在 [我的 Github](https://github.com/d0u9/Linux-Device-Driver-Rust/tree/master/00_Introduction_to_Rust_Module_in_Linux) 上找到，由于写的仓促，可能存在一些不足，还请见谅。




---


作者：苏子彬，阿里云 PAI 平台开发工程师，主要从事 Linux 系统及驱动的相关开发，曾为 PAI 平台编写 FPGA 加速卡驱动。

