如何用 Rust 編寫一個 Linux 內(nèi)核模塊
編者按:近些年來 Rust 語言由于其內(nèi)存安全性和性能等優(yōu)勢得到了很多關(guān)注,尤其是 Linux 內(nèi)核也在準(zhǔn)備將其集成到其中,因此,我們特邀阿里云工程師蘇子彬為我們介紹一下如何在 Linux 內(nèi)核中集成 Rust 支持。
2021 年 4 月 14 號,一封主題名為《Rust support》的郵件出現(xiàn)在 LKML 郵件組中。這封郵件主要介紹了向內(nèi)核引入 Rust 語言支持的一些看法以及所做的工作。郵件的發(fā)送者是 Miguel Ojeda,為內(nèi)核中 Compiler attributes、.clang-format 等多個模塊的維護(hù)者,也是目前 Rust for Linux 項目的維護(hù)者。
Rust for Linux 項目目前得到了 Google 的大力支持,Miguel Ojeda 當(dāng)前的全職工作就是負(fù)責(zé) Rust for Linux 項目。
長期以來,內(nèi)核使用 C 語言和匯編語言作為主要的開發(fā)語言,部分輔助語言包括 Python、Perl、shell 被用來進(jìn)行代碼生成、打補(bǔ)丁、檢查等工作。2016 年 Linux 25 歲生日時,在對 Linus Torvalds 的一篇 采訪中,他就曾表示過:
這根本不是一個新現(xiàn)象。我們有過使用 Modula-2 或 Ada 的系統(tǒng)人員,我不得不說 Rust 看起來比這兩個災(zāi)難要好得多。
我對 Rust 用于操作系統(tǒng)內(nèi)核并不信服(雖然系統(tǒng)編程不僅限于內(nèi)核),但同時,毫無疑問,C 有很多局限性。
在最新的對 Rust support 的 RFC 郵件的回復(fù)中,他更是說:
所以我對幾個個別補(bǔ)丁做了回應(yīng),但總體上我不討厭它。
沒有用他特有的回復(fù)方式來反擊,應(yīng)該就是暗自喜歡了吧。
目前 Rust for Linux 依然是一個獨立于上游的項目,并且主要工作還集中的驅(qū)動接口相關(guān)的開發(fā)上,并非一個完善的項目。
項目地址: https://github.com/Rust-for-Linux/linux
為什么是 Rust
在 Miguel Ojeda 的第一個 RFC 郵件中,他已經(jīng)提到了 “Why Rust”,簡單總結(jié)下:
- 在安全子集中不存在未定義行為,包括內(nèi)存安全和數(shù)據(jù)競爭;
- 更加嚴(yán)格的類型檢測系統(tǒng)能夠進(jìn)一步減少邏輯錯誤;
- 明確區(qū)分
safe
和unsafe
代碼; - 更加面向未來的語言:
sum
類型、模式匹配、泛型、RAII、生命周期、共享及專屬引用、模塊與可見性等等; - 可擴(kuò)展的獨立標(biāo)準(zhǔn)庫;
- 集成的開箱可用工具:文檔生成、代碼格式化、linter 等,這些都基于編譯器本身。
編譯支持 Rust 的內(nèi)核
根據(jù) Rust for Linux 文檔,編譯一個包含 Rust 支持的內(nèi)核需要如下步驟:
-
安裝
rustc
編譯器。Rust for Linux 不依賴 cargo,但需要最新的 beta 版本的rustc
。使用rustup
命令安裝:rustup default beta-2021-06-23
-
安裝 Rust 標(biāo)準(zhǔn)庫的源碼。Rust for Linux 會交叉編譯 Rust 的
core
庫,并將這兩個庫鏈接進(jìn)內(nèi)核鏡像。rustup component add rust-src
-
安裝
libclang
庫。libclang
被bindgen
用做前端,用來處理 C 代碼。libclang
可以從 llvm 官方主頁 下載預(yù)編譯好的版本。 -
安裝
bindgen
工具,bindgen
是一個自動將 C 接口轉(zhuǎn)為 RustFFI 接口的庫:cargo install --locked --version 0.56.0 bindgen
-
克隆最新的 Rust for Linux 代碼:
git clone https://github.com/Rust-for-Linux/linux.git
-
配置內(nèi)核啟用 Rust 支持:
Kernel hacking
-> Sample kernel code
-> Rust samples
-
構(gòu)建:
LIBCLANG_PATH=/path/to/libclang make -j LLVM=1 bzImage
這里我們使用
clang
作為默認(rèn)的內(nèi)核編譯器,使用gcc
理論上是可以的,但還處于 早期實驗 階段。
Rust 是如何集成進(jìn)內(nèi)核的
目錄結(jié)構(gòu)
為了將 Rust 集成進(jìn)內(nèi)核中,開發(fā)者首先對 Kbuild 系統(tǒng)進(jìn)行修改,加入了相關(guān)配置項來開啟/關(guān)閉 Rust 的支持。
此外,為了編譯 rs
文件,添加了一些 Makefile
的規(guī)則。這些修改分散在內(nèi)核目錄中的不同文件里。
Rust 生成的目標(biāo)代碼中的符號會因為 Mangling
導(dǎo)致其長度超過同樣的 C 程序所生成符號的長度,因此,需要對內(nèi)核的符號長度相關(guān)的邏輯進(jìn)行補(bǔ)丁。開發(fā)者引入了 “大內(nèi)核符號”的概念,用來在保證向前兼容的情況下,支持 Rust 生成的目標(biāo)文件符號長度。
其他 Rust 相關(guān)的代碼都被放置在了 rust
目錄下。
在 Rust 中使用 C 函數(shù)
Rust 提供 FFI(外部函數(shù)接口)用來支持對 C 代碼的調(diào)用。Bindgen 是一個 Rust 官方的工具,用來自動化地從 C 函數(shù)中生成 Rust 的 FFI 綁定。內(nèi)核中的 Rust 也使用該工具從原生的內(nèi)核 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 相關(guān)的代碼會單獨從 rs
編譯為 .o
,生成的目標(biāo)文件是標(biāo)準(zhǔn)的 ELF 文件。在鏈接階段,內(nèi)核的鏈接器將 Rust 生成的目標(biāo)文件與其他 C 程序生成的目標(biāo)文件一起鏈接為內(nèi)核鏡像文件。因此,只要 Rust 生成的目標(biāo)文件 ABI 與 C 程序的一致,就可以無差別的被鏈接(當(dāng)然,被引用的符號還是要存在的)。
Rust 的 alloc
與 core
庫
目前 Rust for Linux 依賴于 core
庫。在 core
中定義了基本的 Rust 數(shù)據(jù)結(jié)構(gòu)與語言特性,例如熟悉的 Option<>
和 Result<>
就是 core
庫所提供。
這個庫被交叉編譯后被直接鏈接進(jìn)內(nèi)核鏡像文件,這也是導(dǎo)致啟用 Rust 的內(nèi)核鏡像文件尺寸較大的原因。在未來的工作中,這兩個庫會被進(jìn)一步被優(yōu)化,去除掉某些無用的部分,例如浮點操作,Unicode 相關(guān)的內(nèi)容,F(xiàn)utures 相關(guān)的功能等。
之前的 Rust for Linux 項目還依賴于 Rust 的 alloc
庫。Rust for Linux 定義了自己的 GlobalAlloc
用來管理基本的堆內(nèi)存分配。主要被用來進(jìn)行堆內(nèi)存分配,并且使用 GFP_KERNEL
標(biāo)識作為默認(rèn)的內(nèi)存分配模式。
不過在在最新的 拉取請求 中,社區(qū)已經(jīng)將移植并修改了 Rust的 alloc
庫,使其能夠在盡量保證與 Rust 上游統(tǒng)一的情況下,允許開發(fā)者定制自己的內(nèi)存分配器。不過目前使用自定義的 GFP_
標(biāo)識來分配內(nèi)存依然是不支持的,但好消息是這個功能正在開發(fā)中。
“Hello World” 內(nèi)核模塊
用一個簡單的 Hello World 來展示如何使用 Rust 語言編寫驅(qū)動代碼,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");
}
}
與之對應(yīng)的 Makefile
:
obj-m := hello_world.o
構(gòu)建:
make -C /path/to/linux_src M=$(pwd) LLVM=1 modules
之后就和使用普通的內(nèi)核模塊一樣,使用 insmod
工具或者 modprobe
工具加載就可以了。在使用體驗上是沒有區(qū)別的。
module! { }
宏
這個宏可以被認(rèn)為是 Rust 內(nèi)核模塊的入口,因為在其中定義了一個內(nèi)核模塊所需的所有信息,包括:Author
、License
、Description
等。其中最重要的是 type
字段,在其中需要指定內(nèi)核模塊結(jié)構(gòu)的名字。在這個例子中:
module! {
...
type: HelloWorld,
...
}
struct HelloWorld;
module_init()
與 module_exit()
在使用 C 編寫的內(nèi)核模塊中,這兩個宏定義了模塊的入口函數(shù)與退出函數(shù)。在 Rust 編寫的內(nèi)核模塊中,對應(yīng)的功能由 trait KernelModule
和 trait Drop
來實現(xiàn)。trait KernelModule
中定義 init()
函數(shù),會在模塊驅(qū)動初始化時被調(diào)用;trait Drop
是 Rust 的內(nèi)置 trait,其中定義的 drop()
函數(shù)會在變量生命周期結(jié)束時被調(diào)用。
編譯與鏈接
所有的內(nèi)核模塊文件會首先被編譯成 .o
目標(biāo)文件,之后由內(nèi)核鏈接器將這些 .o
文件和自動生成的模塊目標(biāo)文件 .mod.o
一起鏈接成為 .ko
文件。這個 .ko
文件符合動態(tài)庫 ELF 文件格式,能夠被內(nèi)核識別并加載。
其他
完整的介紹 Rust 是如何被集成進(jìn)內(nèi)核的文章可以在 我的 Github 上找到,由于寫的倉促,可能存在一些不足,還請見諒。
作者簡介
蘇子彬,阿里云 PAI 平臺開發(fā)工程師,主要從事 Linux 系統(tǒng)及驅(qū)動的相關(guān)開發(fā),曾為 PAI 平臺編寫 FPGA 加速卡驅(qū)動。