Rust 进入内核:在不安全基础上构建安全抽象
前置知识
- ›第 1 篇:架构与目录结构
- ›第 2 篇:启动流程与初始化
- ›Rust 基础(所有权、trait、no_std、unsafe)
- ›第 4 篇中介绍的 C 函数指针结构体
Rust 进入内核:在不安全基础上构建安全抽象
纵观本系列,我们反复见到同一种模式:充满函数指针的 C 结构体——sched_class、file_operations、inode_operations、io_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 驱动编译模型高度对应的架构:
#![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::* 开头:
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};
核心抽象一览:
KVec、KBox— 内核专用分配器,使用GFP_KERNEL标志而非标准库分配器Error/Result— 将内核错误码(-ENOMEM、-EINVAL)封装进 Rust 类型系统ThisModule— 当前内核模块的引用,用于所有权追踪pr_info!等宏 — 带格式检查的printk()包装pin_data、pin_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/。
模块入口:
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 })
让我们逐一梳理这些模式:
impl auxiliary::Driver— 包裹 Cstruct auxiliary_driver的 Rust trait。当内核将该驱动与某个设备匹配时,probe函数被调用。try_pin_init!— 如前所述,原地初始化NovaData。drm::Device::<Self>::new()— 创建 DRM 设备,将 C 的drm_dev_alloc()包装为类型安全的 Rust 接口,并自动处理清理工作。drm::Registration::new_foreign_owned()— 向 DRM 子系统注册设备(对应 C 的drm_dev_register())。#[vtable] impl drm::Driver— 声明 DRM 驱动操作。declare_drm_ioctls!宏生成 ioctl 分发表。
这段代码真正的美妙之处在于那些不存在的东西:没有手动编写的错误清理路径,不会因失败而忘记注销,也不存在 NULL 函数指针的风险。Rust 类型系统通过 Drop 自动管理资源清理,? 运算符则以自动展开的方式传播错误。
Kbuild 集成与未来展望
整个集成只需顶层 Kbuild 中的一行:
obj-$(CONFIG_RUST) += rust/
当 CONFIG_RUST=y 时,构建系统会依次:
- 编译
core、compiler_builtins和fficrate - 运行
bindgen生成bindings_generated.rs - 编译
kernelcrate - 编译配置中启用的所有 Rust 驱动
当前内核的 lib.rs(第 19–59 行)依赖若干不稳定的 Rust 特性——arbitrary_self_types、derive_coerce_pointee 等。随着新版 Rust 的发布,这些特性中有些已经稳定,依赖列表也在逐步缩短。长远目标是只依赖稳定版 Rust,让任何安装了标准 rustc 的开发者都能参与内核 Rust 开发。
Rust-for-Linux 项目并非要重写内核,而是为新代码开辟一条并行路径——尤其是驱动程序。驱动程序占内核代码库的 60%,也是 bug 的主要来源。随着抽象覆盖范围的扩大(设备模型、DRM、文件系统、网络),越来越多的新驱动可以用 Rust 编写,在获得内存安全保证的同时,与本系列探讨的现有 C 子系统无缝协作。
提示: 要编译支持 Rust 的内核,需要安装
rustc、bindgen和rustfmt。运行make LLVM=1 rustavailable检查工具链是否兼容,然后在.config中启用CONFIG_RUST=y。
系列总结
经过六篇文章,我们从 Linux 内核 3000 万行代码的目录结构出发,依次深入启动初始化、进程调度、系统调用入口、异步 I/O,最终到达 Rust 安全层。贯穿始终的是那些反复出现的模式:利用链接器节(linker section)实现去中心化注册、用 C 函数指针结构体实现多态、以缓存行对齐的数据布局提升性能、以共享内存消除开销。这些并非孤立的技巧,而是世界上部署最广泛的操作系统所使用的架构语汇。理解它们,你便拥有了读懂内核任意角落的基础。