如何用 Rust 編寫一個 Linux 內核模塊
編者按:近些年來 Rust 語言由于其內存安全性和性能等優勢得到了很多關注,尤其是 Linux 內核也在準備將其集成到其中,因此,我們特邀阿里云工程師蘇子彬為我們介紹一下如何在 Linux 內核中集成 Rust 支持。
2021 年 4 月 14 號,一封主題名為《Rust support》的郵件出現在 LKML 郵件組中。這封郵件主要介紹了向內核引入 Rust 語言支持的一些看法以及所做的工作。郵件的發送者是 Miguel Ojeda,為內核中 Compiler attributes、.clang-format 等多個模塊的維護者,也是目前 Rust for Linux 項目的維護者。
Rust for Linux 項目目前得到了 Google 的大力支持,Miguel Ojeda 當前的全職工作就是負責 Rust for Linux 項目。
長期以來,內核使用 C 語言和匯編語言作為主要的開發語言,部分輔助語言包括 Python、Perl、shell 被用來進行代碼生成、打補丁、檢查等工作。2016 年 Linux 25 歲生日時,在對 Linus Torvalds 的一篇 采訪中,他就曾表示過:
這根本不是一個新現象。我們有過使用 Modula-2 或 Ada 的系統人員,我不得不說 Rust 看起來比這兩個災難要好得多。
我對 Rust 用于操作系統內核并不信服(雖然系統編程不僅限于內核),但同時,毫無疑問,C 有很多局限性。
在最新的對 Rust support 的 RFC 郵件的回復中,他更是說:
所以我對幾個個別補丁做了回應,但總體上我不討厭它。
沒有用他特有的回復方式來反擊,應該就是暗自喜歡了吧。
目前 Rust for Linux 依然是一個獨立于上游的項目,并且主要工作還集中的驅動接口相關的開發上,并非一個完善的項目。
項目地址: https://github.com/Rust-for-Linux/linux
為什么是 Rust
在 Miguel Ojeda 的第一個 RFC 郵件中,他已經提到了 “Why Rust”,簡單總結下:
- 在安全子集中不存在未定義行為,包括內存安全和數據競爭;
- 更加嚴格的類型檢測系統能夠進一步減少邏輯錯誤;
- 明確區分
safe
和unsafe
代碼; - 更加面向未來的語言:
sum
類型、模式匹配、泛型、RAII、生命周期、共享及專屬引用、模塊與可見性等等; - 可擴展的獨立標準庫;
- 集成的開箱可用工具:文檔生成、代碼格式化、linter 等,這些都基于編譯器本身。
編譯支持 Rust 的內核
根據 Rust for Linux 文檔,編譯一個包含 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 官方主頁 下載預編譯好的版本。 -
安裝
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
理論上是可以的,但還處于 早期實驗 階段。
Rust 是如何集成進內核的
目錄結構
為了將 Rust 集成進內核中,開發者首先對 Kbuild 系統進行修改,加入了相關配置項來開啟/關閉 Rust 的支持。
此外,為了編譯 rs
文件,添加了一些 Makefile
的規則。這些修改分散在內核目錄中的不同文件里。
Rust 生成的目標代碼中的符號會因為 Mangling
導致其長度超過同樣的 C 程序所生成符號的長度,因此,需要對內核的符號長度相關的邏輯進行補丁。開發者引入了 “大內核符號”的概念,用來在保證向前兼容的情況下,支持 Rust 生成的目標文件符號長度。
其他 Rust 相關的代碼都被放置在了 rust
目錄下。
在 Rust 中使用 C 函數
Rust 提供 FFI(外部函數接口)用來支持對 C 代碼的調用。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
標識作為默認的內存分配模式。
不過在在最新的 拉取請求 中,社區已經將移植并修改了 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 上找到,由于寫的倉促,可能存在一些不足,還請見諒。
作者簡介
蘇子彬,阿里云 PAI 平臺開發工程師,主要從事 Linux 系統及驅動的相關開發,曾為 PAI 平臺編寫 FPGA 加速卡驅動。