Read OSS

Rust 进入内核:在不安全基础上构建安全抽象

高级

前置知识

  • 第 1 篇:架构与目录结构
  • 第 2 篇:启动流程与初始化
  • Rust 基础(所有权、trait、no_std、unsafe)
  • 第 4 篇中介绍的 C 函数指针结构体

Rust 进入内核:在不安全基础上构建安全抽象

纵观本系列,我们反复见到同一种模式:充满函数指针的 C 结构体——sched_classfile_operationsinode_operationsio_issue_def——它们充当运行时多态的 vtable。这种方式确实可行,但隐患重重:没有任何机制阻止文件系统将某个函数指针留为 NULL,也没有任何机制提醒驱动程序释放锁,更无法在编译期发现 use-after-free 问题。Rust-for-Linux 项目的目标,正是改变这一现状——不是重写内核,而是用安全的 Rust 抽象层来包裹这些 C 模式,从根源上消除整类 bug。

正如第 1 篇所介绍的,Rust 支持通过顶层 Kbuild 中的 obj-$(CONFIG_RUST) += rust/ 条件编译启用。本文将深入探讨 rust/ 目录的内容、C 绑定如何流转为安全的 Rust API,以及一个真实的 GPU 驱动如何使用这些抽象。

kernel Crate 的结构

kernel crate 是内核 Rust 代码的核心库。其 lib.rs 揭示了一套与 C 内核 Kconfig 驱动编译模型高度对应的架构:

rust/kernel/lib.rs#L1-L160

#![no_std]
...
#[cfg(not(CONFIG_RUST))]
compile_error!("Missing kernel configuration for conditional compilation");

extern crate self as kernel;

pub use ffi;

pub mod alloc;
#[cfg(CONFIG_AUXILIARY_BUS)]
pub mod auxiliary;
#[cfg(CONFIG_BLOCK)]
pub mod block;
pub mod clk;
#[cfg(CONFIG_CONFIGFS_FS)]
pub mod configfs;
pub mod device;
pub mod driver;
#[cfg(CONFIG_DRM = "y")]
pub mod drm;
pub mod error;
pub mod fs;
#[cfg(CONFIG_I2C = "y")]
pub mod i2c;
pub mod init;
pub mod irq;
#[cfg(CONFIG_NET)]
pub mod net;
#[cfg(CONFIG_PCI)]
pub mod pci;
pub mod platform;
pub mod prelude;
pub mod sync;
pub mod task;
...
graph TD
    subgraph "kernel crate (rust/kernel/)"
        lib["lib.rs<br/>#![no_std]"]
        lib --> prelude["prelude.rs"]
        lib --> alloc["alloc/"]
        lib --> device["device.rs"]
        lib --> driver["driver.rs"]
        lib --> drm["drm/<br/>#[cfg(CONFIG_DRM)]"]
        lib --> auxiliary["auxiliary.rs<br/>#[cfg(CONFIG_AUXILIARY_BUS)]"]
        lib --> pci["pci.rs<br/>#[cfg(CONFIG_PCI)]"]
        lib --> init["init.rs"]
        lib --> sync["sync/"]
        lib --> fs["fs.rs"]
        lib --> net["net/<br/>#[cfg(CONFIG_NET)]"]
    end

#[cfg(CONFIG_*)] 属性是 C 中 #ifdef CONFIG_* 的 Rust 等价写法。内核的 Kconfig 符号以 --cfg 标志的形式传递给 rustc,因此 #[cfg(CONFIG_DRM = "y")] 只在 DRM 以内建方式编译时(而非作为可加载模块)才会包含 DRM 模块。这种紧密集成意味着 Rust 代码与整个 3000 万行 C 代码库共享同一套条件编译体系。

注意第 14 行的 #![no_std]——这里没有标准库。内核提供了自己的分配器、字符串类型和同步原语。Rust 在内核中运行于一个独立(freestanding)环境之中。

Prelude 与通用抽象

每个 Rust 内核模块都以 use kernel::prelude::* 开头:

rust/kernel/prelude.rs

pub use crate::alloc::{flags::*, Box, KBox, KVBox, KVVec, KVec, VBox, VVec, Vec};

pub use macros::{export, fmt, kunit_tests, module, vtable};

pub use pin_init::{init, pin_data, pin_init, pinned_drop, InPlaceWrite, Init, PinInit, Zeroable};

pub use super::error::{code::*, Error, Result};

pub use super::{str::CStrExt as _, ThisModule};

pub use super::{pr_alert, pr_crit, pr_debug, pr_emerg, pr_err, pr_info, pr_notice, pr_warn};

核心抽象一览:

  • KVecKBox — 内核专用分配器,使用 GFP_KERNEL 标志而非标准库分配器
  • Error / Result — 将内核错误码(-ENOMEM-EINVAL)封装进 Rust 类型系统
  • ThisModule — 当前内核模块的引用,用于所有权追踪
  • pr_info! 等宏 — 带格式检查的 printk() 包装
  • pin_datapin_init — 用于安全初始化固定(不可移动)类型的宏
  • vtable — 将 Rust trait 桥接到 C 函数指针结构体的宏

提示: 编写新的 Rust 内核模块时,从 use kernel::prelude::*module! 宏入手即可。二者配合,足以满足基本模块生命周期管理的所有需求。

C → Rust 绑定流水线

Rust 内核代码不直接调用 C 函数,而是经过一条分层流水线:

flowchart TD
    A["C Headers<br/>(include/linux/*.h)"] -->|bindgen| B["bindings_generated.rs<br/>(raw unsafe FFI)"]
    B --> C["rust/bindings/lib.rs<br/>(raw module, re-exported)"]
    C --> D["rust/kernel/*.rs<br/>(safe Rust abstractions)"]
    D --> E["Kernel Modules<br/>(drivers/gpu/drm/nova/, etc.)"]

    style A fill:#f9f,stroke:#333
    style B fill:#fbb,stroke:#333
    style C fill:#fbb,stroke:#333
    style D fill:#bfb,stroke:#333
    style E fill:#bfb,stroke:#333

第一层:原始绑定bindgen 处理 C 头文件,生成包含原始 extern "C" 函数声明和类型定义的 bindings_generated.rs。这一层是不安全且不易使用的。

rust/bindings/lib.rs 用适当的 #![allow] 属性包裹生成的代码:

//! This crate may not be directly used. If you need a kernel C API that is
//! not ported or wrapped in the `kernel` crate, then do so first instead of
//! using this crate.

#![no_std]
#![allow(clippy::all, missing_docs, non_camel_case_types, ...)]

mod bindings_raw {
    ...
    include!(concat!(env!("OBJTREE"), "/rust/bindings/bindings_generated.rs"));
}

注释本身就是一条规范:驱动程序永远不应直接使用 bindings,原始绑定只是中间表示。

第二层:安全抽象rust/kernel/ 中的模块将原始绑定包装成安全的 Rust 类型。原始的 struct device * 变为带 RAII 清理的 Device,原始的自旋锁变为类型安全的 SpinLock<T>。这正是 Rust 所有权系统发挥价值之处——抽象层在编译期强制执行生命周期和加锁规则。

第三层:驱动程序 — 实际的内核驱动使用安全抽象层,只需面对 Rust trait 和类型,而无需接触原始 C 指针。

rust/Makefile 统筹整条流水线:

obj-$(CONFIG_RUST) += core.o compiler_builtins.o ffi.o
always-$(CONFIG_RUST) += bindings/bindings_generated.rs bindings/bindings_helpers_generated.rs
obj-$(CONFIG_RUST) += bindings.o pin_init.o kernel.o

#[vtable] 与 Pin 初始化模式

#[vtable] 宏

第 4 篇介绍了 C 如何通过 struct file_operations 使用函数指针。Rust 的对应方案是 #[vtable] 属性宏。它标注一个 trait 实现,并自动生成对应的 C 操作结构体——未实现的可选操作自动填 NULL,并相应设置 has_* 标志。

当你这样写:

#[vtable]
impl drm::Driver for NovaDriver {
    type Data = NovaData;
    type File = File;
    type Object = gem::Object<NovaObject>;
    const INFO: drm::DriverInfo = INFO;
    ...
}

#[vtable] 宏会生成一个兼容 C 的 struct drm_driver,其中的函数指针回调到 Rust trait 方法,自动处理 C 与 Rust 之间的 FFI 边界。未实现的必选方法会直接触发编译错误;未实现的可选方法则生成 NULL 函数指针,并将对应的 HAS_* 常量置为 false

Pin 初始化

内核数据结构往往是自引用的(例如,结构体中包含指回自身的链表头),或必须固定在特定内存地址(例如,已向硬件注册的结构体)。在 C 中,这类结构体在原地初始化后永远不会被移动。在 Rust 中,移动语义让这件事变得棘手。

#[pin_data]try_pin_init! 宏正是为此而生:

#[pin_data]
pub(crate) struct NovaData {
    pub(crate) adev: ARef<auxiliary::Device>,
}

#[pin_data] 属性标记哪些字段在结构上固定(不得移动)。try_pin_init! 提供原地初始化能力,直接在最终内存位置构造值,全程无需移动。对于在构造过程中就需要向 C 子系统注册自身的内核对象,这一点至关重要。

classDiagram
    class RustTrait["trait drm::Driver"] {
        +type Data
        +type File
        +const INFO: DriverInfo
    }
    class VtableMacro["#[vtable] generates"] {
        +C struct drm_driver
        +fn pointers → Rust methods
        +NULL for optional ops
    }
    class PinInit["#[pin_data] + try_pin_init!"] {
        +In-place construction
        +No move after init
        +Self-referential safe
    }
    RustTrait --> VtableMacro : "attribute macro"
    PinInit --> RustTrait : "initializes pinned data"

Nova DRM 驱动详解

Nova 是一个真实的 Rust 内核驱动,用于支持 NVIDIA GPU——它不是玩具示例,而是已合入内核树的驱动,完整展示了 Rust 驱动模型的全貌。它位于 drivers/gpu/drm/nova/

模块入口:

drivers/gpu/drm/nova/nova.rs

mod driver;
mod file;
mod gem;

use crate::driver::NovaDriver;

kernel::module_auxiliary_driver! {
    type: NovaDriver,
    name: "Nova",
    authors: ["Danilo Krummrich"],
    description: "Nova GPU driver",
    license: "GPL v2",
}

module_auxiliary_driver! 宏是 C 中 module_init() / module_exit() 的 Rust 等价写法。它生成 initcall(详见第 2 篇)、模块元数据,以及注册/注销代码。type: NovaDriver 告诉宏哪个类型实现了该驱动。

驱动实现:

drivers/gpu/drm/nova/driver.rs#L1-L79

pub(crate) struct NovaDriver {
    drm: ARef<drm::Device<Self>>,
}

impl auxiliary::Driver for NovaDriver {
    type IdInfo = ();
    const ID_TABLE: auxiliary::IdTable<Self::IdInfo> = &AUX_TABLE;

    fn probe(adev: &auxiliary::Device<Core>, _info: &Self::IdInfo) -> impl PinInit<Self, Error> {
        let data = try_pin_init!(NovaData { adev: adev.into() });
        let drm = drm::Device::<Self>::new(adev.as_ref(), data)?;
        drm::Registration::new_foreign_owned(&drm, adev.as_ref(), 0)?;
        Ok(Self { drm })
    }
}

#[vtable]
impl drm::Driver for NovaDriver {
    type Data = NovaData;
    type File = File;
    type Object = gem::Object<NovaObject>;
    const INFO: drm::DriverInfo = INFO;

    kernel::declare_drm_ioctls! {
        (NOVA_GETPARAM, drm_nova_getparam, ioctl::RENDER_ALLOW, File::get_param),
        (NOVA_GEM_CREATE, drm_nova_gem_create, ioctl::AUTH | ioctl::RENDER_ALLOW, File::gem_create),
        (NOVA_GEM_INFO, drm_nova_gem_info, ioctl::AUTH | ioctl::RENDER_ALLOW, File::gem_info),
    }
}
sequenceDiagram
    participant K as Kernel (C)
    participant R as Rust Abstractions
    participant N as Nova Driver

    K->>R: auxiliary bus probe callback
    R->>N: NovaDriver::probe(adev, info)
    N->>N: try_pin_init!(NovaData { ... })
    N->>R: drm::Device::new(adev, data)
    R->>K: C drm_dev_alloc() via FFI
    K-->>R: struct drm_device*
    R-->>N: ARef<drm::Device>
    N->>R: drm::Registration::new_foreign_owned()
    R->>K: C drm_dev_register() via FFI
    K-->>R: Success
    R-->>N: Ok(Self { drm })

让我们逐一梳理这些模式:

  1. impl auxiliary::Driver — 包裹 C struct auxiliary_driver 的 Rust trait。当内核将该驱动与某个设备匹配时,probe 函数被调用。
  2. try_pin_init! — 如前所述,原地初始化 NovaData
  3. drm::Device::<Self>::new() — 创建 DRM 设备,将 C 的 drm_dev_alloc() 包装为类型安全的 Rust 接口,并自动处理清理工作。
  4. drm::Registration::new_foreign_owned() — 向 DRM 子系统注册设备(对应 C 的 drm_dev_register())。
  5. #[vtable] impl drm::Driver — 声明 DRM 驱动操作。declare_drm_ioctls! 宏生成 ioctl 分发表。

这段代码真正的美妙之处在于那些不存在的东西:没有手动编写的错误清理路径,不会因失败而忘记注销,也不存在 NULL 函数指针的风险。Rust 类型系统通过 Drop 自动管理资源清理,? 运算符则以自动展开的方式传播错误。

Kbuild 集成与未来展望

整个集成只需顶层 Kbuild 中的一行:

Kbuild#L103

obj-$(CONFIG_RUST)  += rust/

CONFIG_RUST=y 时,构建系统会依次:

  1. 编译 corecompiler_builtinsffi crate
  2. 运行 bindgen 生成 bindings_generated.rs
  3. 编译 kernel crate
  4. 编译配置中启用的所有 Rust 驱动

当前内核的 lib.rs(第 19–59 行)依赖若干不稳定的 Rust 特性——arbitrary_self_typesderive_coerce_pointee 等。随着新版 Rust 的发布,这些特性中有些已经稳定,依赖列表也在逐步缩短。长远目标是只依赖稳定版 Rust,让任何安装了标准 rustc 的开发者都能参与内核 Rust 开发。

Rust-for-Linux 项目并非要重写内核,而是为新代码开辟一条并行路径——尤其是驱动程序。驱动程序占内核代码库的 60%,也是 bug 的主要来源。随着抽象覆盖范围的扩大(设备模型、DRM、文件系统、网络),越来越多的新驱动可以用 Rust 编写,在获得内存安全保证的同时,与本系列探讨的现有 C 子系统无缝协作。

提示: 要编译支持 Rust 的内核,需要安装 rustcbindgenrustfmt。运行 make LLVM=1 rustavailable 检查工具链是否兼容,然后在 .config 中启用 CONFIG_RUST=y

系列总结

经过六篇文章,我们从 Linux 内核 3000 万行代码的目录结构出发,依次深入启动初始化、进程调度、系统调用入口、异步 I/O,最终到达 Rust 安全层。贯穿始终的是那些反复出现的模式:利用链接器节(linker section)实现去中心化注册、用 C 函数指针结构体实现多态、以缓存行对齐的数据布局提升性能、以共享内存消除开销。这些并非孤立的技巧,而是世界上部署最广泛的操作系统所使用的架构语汇。理解它们,你便拥有了读懂内核任意角落的基础。