Linux中國

如何用 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 subset 中不存在未定義行為,包括內存安全和數據競爭;
  • 更加嚴格的類型檢測系統能夠進一步減少邏輯錯誤;
  • 明確區分 safeunsafe 代碼;
  • 更加面向未來的語言:sum 類型、模式匹配、泛型、RAII、生命周期、共享及專屬引用、模塊與可見性等等;
  • 可擴展的獨立標準庫;
  • 集成的開箱可用工具:文檔生成、代碼格式化、linter 等,這些都基於編譯器本身。

編譯支持 Rust 的內核

根據 Rust for Linux 文檔,編譯一個包含 Rust 支持的內核需要如下步驟:

  1. 安裝 rustc 編譯器。Rust for Linux 不依賴 cargo,但需要最新的 beta 版本的 rustc。使用 rustup命令安裝:
rustup default beta-2021-06-23
  1. 安裝 Rust 標準庫的源碼。Rust for Linux 會交叉編譯 Rust 的 core 庫,並將這兩個庫鏈接進內核鏡像。
rustup component add rust-src
  1. 安裝 libclang 庫。libclangbindgen 用做前端,用來處理 C 代碼。libclang 可以從 llvm 官方主頁 下載預編譯好的版本。
  2. 安裝 bindgen 工具,bindgen 是一個自動將 C 介面轉為 RustFFI 介面的庫:
cargo install --locked --version 0.56.0 bindgen
  1. 克隆最新的 Rust for Linux 代碼:
git clone https://github.com/Rust-for-Linux/linux.git
  1. 配置內核啟用 Rust 支持:
Kernel hacking
  -> Sample kernel code
    -> Rust samples
  1. 構建:
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( 外部函數介面 Foreign Function Interface )用來支持對 C 代碼的調用。Bindgen 是一個 Rust 官方的工具,用來自動化地從 C 函數中生成 Rust 的 FFI 綁定。內核中的 Rust 也使用該工具從原生的內核 C 介面中生成 Rust 的 FFI 綁定。

quiet_cmd_bindgen = BINDGEN $@
      cmd_bindgen = 
    $(BINDGEN) $< $(shell grep -v &apos;^#|^$$&apos; $(srctree)/rust/bindgen_parameters) 
        --use-core --with-derive-default --ctypes-prefix c_types 
        --no-debug &apos;.*&apos; 
        --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 的 alloccore

目前 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 內核模塊的入口,因為在其中定義了一個內核模塊所需的所有信息,包括:AuthorLicenseDescription 等。其中最重要的是 type 欄位,在其中需要指定內核模塊結構的名字。在這個例子中:

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

struct HelloWorld;

module_init()module_exit()

在使用 C 編寫的內核模塊中,這兩個宏定義了模塊的入口函數與退出函數。在 Rust 編寫的內核模塊中,對應的功能由 trait KernelModuletrait Drop 來實現。trait KernelModule 中定義 init() 函數,會在模塊驅動初始化時被調用;trait Drop 是 Rust 的內置 trait,其中定義的 drop() 函數會在變數生命周期結束時被調用。

編譯與鏈接

所有的內核模塊文件會首先被編譯成 .o 目標文件,之後由內核鏈接器將這些 .o 文件和自動生成的模塊目標文件 .mod.o 一起鏈接成為 .ko 文件。這個 .ko 文件符合動態庫 ELF 文件格式,能夠被內核識別並載入。

其他

完整的介紹 Rust 是如何被集成進內核的文章可以在 我的 Github 上找到,由於寫的倉促,可能存在一些不足,還請見諒。

作者:蘇子彬,阿里雲 PAI 平台開發工程師,主要從事 Linux 系統及驅動的相關開發,曾為 PAI 平台編寫 FPGA 加速卡驅動。


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國