编者按:近些年来 Rust 语言由于其内存安全性和性能等优势得到了很多关注,尤其是 Linux 内核也在准备将其集成到其中,因此,我们特邀阿里云工程师苏子彬为我们介绍一下如何在 Linux 内核中集成 Rust 支持。
2021 年 4 月 14 号,一封主题名为《Rust support[1]》的邮件出现在 LKML 邮件组中。这封邮件主要介绍了向内核引入 Rust 语言支持的一些看法以及所做的工作。邮件的发送者是 Miguel Ojeda[2],为内核中 Compiler attributes、.clang-format 等多个模块的维护者,也是目前 Rust for Linux 项目的维护者。
Rust for Linux 项目目前得到了 Google 的大力支持[3],Miguel Ojeda[4] 当前的全职工作就是负责 Rust for Linux 项目。
长期以来,内核使用 C 语言和汇编语言作为主要的开发语言,部分辅助语言包括 Python、Perl、shell 被用来进行代码生成、打补丁、检查等工作。2016 年 Linux 25 岁生日时,在对 Linus Torvalds 的一篇 采访[5]中,他就曾表示过:
这根本不是一个新现象。我们有过使用 Modula-2 或 Ada 的系统人员,我不得不说 Rust 看起来比这两个灾难要好得多。
我对 Rust 用于操作系统内核并不信服(虽然系统编程不仅限于内核),但同时,毫无疑问,C 有很多局限性。
在最新的对 Rust support[6] 的 RFC 邮件的回复中,他更是说:
所以我对几个个别补丁做了回应,但总体上我不讨厌它。
没有用他特有的回复方式来反击,应该就是暗自喜欢了吧。
目前 Rust for Linux 依然是一个独立于上游的项目,并且主要工作还集中的驱动接口相关的开发上,并非一个完善的项目。
项目地址:https://github.com/Rust-for-Linux/linux
为什么是 Rust
在 Miguel Ojeda[7] 的第一个 RFC 邮件中,他已经提到了 “Why Rust”,简单总结下:
在安全子集safe subset中不存在未定义行为,包括内存安全和数据竞争;
更加严格的类型检测系统能够进一步减少逻辑错误;
明确区分 safe 和 unsafe 代码;
更加面向未来的语言:sum 类型、模式匹配、泛型、RAII、生命周期、共享及专属引用、模块与可见性等等;
可扩展的独立标准库;
集成的开箱可用工具:文档生成、代码格式化、linter 等,这些都基于编译器本身。
编译支持 Rust 的内核
根据 Rust for Linux 文档[8],编译一个包含 Rust 支持的内核需要如下步骤:
安装 rustc 编译器。Rust for Linux 不依赖 cargo,但需要最新的 beta 版本的 rustc。使用 rustup命令安装:
rustup default beta-2021-06-23
安装 Rust 标准库的源码。Rust for Linux 会交叉编译 Rust 的 core 库,并将这两个库链接进内核镜像。
rustup component add rust-src
安装 libclang 库。libclang 被 bindgen 用做前端,用来处理 C 代码。libclang 可以从 llvm 官方主页[9] 下载预编译好的版本。
安装 bindgen 工具,bindgen 是一个自动将 C 接口转为 RustFFI 接口的库:
cargo install --locked --version 0.56.0 bindgen
克隆最新的 Rust for Linux 代码:
git clone https://github.com/Rust-for-Linux/linux.git
配置内核启用 Rust 支持:
Kernel hacking -》 Sample kernel code -》 Rust samples
构建:
LIBCLANG_PATH=/path/to/libclang make -j LLVM=1 bzImage
这里我们使用 clang 作为默认的内核编译器,使用 gcc 理论上是可以的,但还处于 早期实验[10] 阶段。
Rust 是如何集成进内核的
目录结构
为了将 Rust 集成进内核中,开发者首先对 Kbuild 系统进行修改,加入了相关配置项来开启/关闭 Rust 的支持。
此外,为了编译 rs 文件,添加了一些 Makefile 的规则。这些修改分散在内核目录中的不同文件里。
Rust 生成的目标代码中的符号会因为 Mangling 导致其长度超过同样的 C 程序所生成符号的长度,因此,需要对内核的符号长度相关的逻辑进行补丁。开发者引入了 “大内核符号”的概念,用来在保证向前兼容的情况下,支持 Rust 生成的目标文件符号长度。
其他 Rust 相关的代码都被放置在了 rust 目录下。
在 Rust 中使用 C 函数
Rust 提供 FFI(外部函数接口Foreign Function Interface)用来支持对 C 代码的调用。Bindgen[11] 是一个 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 标识作为默认的内存分配模式。
不过在在最新的 拉取请求[12] 中,社区已经将移植并修改了 Rust的 alloc 库,使其能够在尽量保证与 Rust 上游统一的情况下,允许开发者定制自己的内存分配器。不过目前使用自定义的 GFP_ 标识来分配内存依然是不支持的,但好消息是这个功能正在开发中。
“Hello World” 内核模块
用一个简单的 Hello World 来展示如何使用 Rust 语言编写驱动代码,hello_world.rs:
#![no_std]#![feature(allocator_api, global_asm)]use kernel::*;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!
”); Ok(HelloWorld) }}impl Drop for HelloWorld { fn drop(&mut self) { pr_info!(“Bye world from rust!
”); }}
与之对应的 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[13] 上找到,由于写的仓促,可能存在一些不足,还请见谅。
作者:苏子彬,阿里云 PAI 平台开发工程师,主要从事 Linux 系统及驱动的相关开发,曾为 PAI 平台编写 FPGA 加速卡驱动。
[1]Rust support:https://lkml.org/lkml/2021/4/14/1023
[2]Miguel Ojeda:https://ojeda.dev/
[3]Google 的大力支持:https://www.zdnet.com/article/rust-in-the-linux-kernel-just-got-a-big-boost-from-google/
[4]Miguel Ojeda:https://ojeda.dev/
[5]采访:https://www.infoworld.com/article/3109150/linux-at-25-linus-torvalds-on-the-evolution-and-future-of-linux.html
[6]Rust support:https://lkml.org/lkml/2021/4/14/1023
[7]Miguel Ojeda:https://ojeda.dev/
[8]Rust for Linux 文档:https://github.com/Rust-forLinux/linux/blob/rust/Documentation/rust/quick-start.rst
[9]llvm 官方主页:https://github.com/llvm/llvm-project/releases
[10]早期实验:https://github.com/Rust-for-Linux/linux/blob/rust/Documentation/rust/quick-start.rst#building
[11]Bindgen:https://github.com/rust-lang/rust-bindgen
[12]拉取请求:https://github.com/Rust-for-Linux/linux/pull/402
[13]我的 Github:https://github.com/d0u9/Linux-Device-Driver-Rust/tree/master/00_Introduction_to_Rust_Module_in_Linux
编辑:jq
-
Google
+关注
关注
5文章
1762浏览量
57493 -
python
+关注
关注
56文章
4792浏览量
84614 -
C 语言
+关注
关注
0文章
18浏览量
14224 -
Rust
+关注
关注
1文章
228浏览量
6595
原文标题:如何用 Rust 编写一个 Linux 内核模块
文章出处:【微信号:gh_3980db2283cd,微信公众号:开关电源芯片】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论