Rust for Linux 内核:从驱动到文件系统的深度工程实战
Linux 6.1 的 merge 标志着 Rust 正式成为内核第二语言。本文不是又一个"Hello World"教程,而是深入剖析 Rust 在内核态下的核心机制、实际落地中的架构决策,以及它给内核开发带来的真实变化。
一、为什么内核需要 Rust?— 从 CVE 数据说起
Linux 内核约 70% 的 CVE 源自内存安全问题(CWE-120 缓冲区溢出、CWE-416 Use-After-Free、CWE-190 整数溢出等)。微软 Azure 团队的数据也显示,约 70% 的 Windows CVE 同样源于内存安全。C 语言的手动内存管理在超大规模并发场景下,几乎无法靠人力和 review 完全避免这些问题。
Rust 的核心保证是:在 safe Rust 中,编译器在编译期消除数据竞争、空指针解引用、Use-After-Free 和缓冲区溢出。这恰好覆盖了内核中最常见的安全漏洞类别。
但 Rust 进入内核并非一帆风顺。关键挑战在于:
- 内核有自己的 alloc 系统(slab allocator),不能直接用 std
- 需要处理自旋锁、RCU、MMIO、DMA 等底层原语
- 需要与现有 C 代码无缝 FFI
- 编译时间不能失控
二、内核 Rust 的核心架构:kernel crate 解析
内核 Rust 代码运行在 no_std 环境下,依赖一个名为 kernel 的特殊 crate,它提供了 C 内核功能的安全抽象层。
// 内核模块的骨架
use kernel::prelude::*;
use kernel::file_operations::{FileOperations, File};
module! {
type: RustMinimal,
name: "rust_minimal",
author: "Your Name",
description: "Rust kernel module example",
license: "GPL",
}
struct RustMinimal {
_dev: Pin<Box<Registration<RustFileOps>>>,
}
impl kernel::Module for RustMinimal {
fn init(name: &'static CStr, module: &'static ThisModule) -> Result<Self> {
pr_info!("Rust minimal module initialized\n");
let reg = Registration::new_pinned(fmt!("rust_device"), ())?;
Ok(RustMinimal { _dev: reg })
}
}
#[vtable]
impl FileOperations for RustFileOps {
fn read(_this: &Self, _file: &File, data: &mut impl kernel::io_buffer::IoBufferWriter) -> Result<usize> {
Ok(0)
}
}
注意几个关键点:
module!宏取代了 C 的module_init/module_exitResult<T>是内核自己定义的,与core::result不同,它携带 errno 错误码Pin<Box<T>>用于固定内存位置,防止被移到其他 CPU 或释放pr_info!对应 C 的pr_info(),宏展开后调用bindings::printk
三、内存安全性如何兑现?— 从 Box 到 Arena Allocator
3.1 智能指针适配
内核 Rust 重新实现了几个核心智能指针:
// kernel/src/alloc.rs 中的简化版
use core::alloc::Layout;
use core::ptr::NonNull;
/// 内核对 Box — 使用内核的 kmalloc/kfree
pub struct Box<T: ?Sized, A: Allocator>(Unique<T>, A);
// drop 时调用 kernel::alloc::krealloc 释放
/// 内核对 Arc — 引用计数智能指针
pub struct Arc<T: ?Sized> { /* ... */ }
关键实现:Box 在 Drop 时调用 bindings::kfree,确保与 slab allocator 集成。
3.2 自定义 Allocator 与 GFP flags
内核分配器需要 GFP_KERNEL/GFP_ATOMIC 等 flags,Rust 通过关联类型实现:
use kernel::alloc::{Allocator, Flags};
let flags = GFP_KERNEL; // 对应 C 的 GFP_KERNEL = 0xD0
let boxed = Box::try_new_in(value, flags)?;
// ^^^^^^
// 编译期强制 flags 正确
如果在中断上下文误用 GFP_KERNEL(可睡眠),Rust 的类型系统可以在编码阶段通过 Context 约束捕获:
fn interrupt_handler(ctx: IRQContext, ...) {
let data: Box<u64> = Box::try_new_in(42, GFP_KERNEL, ctx)?;
// ^^^ 编译错误!IRQContext 约束不允许 GFP_KERNEL
}
四、同步原语:Mutex、Spinlock 和 RCU 的 Rust 化身
4.1 Mutex 的 Comp-State 模式
C 内核中,锁的持有状态完全依赖开发者自觉。Rust 将锁与数据打包::
use kernel::sync::Mutex;
struct DeviceState {
registers: Mutex<RegisterFile>,
dma_buf: DmaBuffer,
}
impl DeviceState {
fn read_status(&self) -> u32 {
// lock() 返回 Guard,离开作用域自动释放
let regs = self.registers.lock();
regs.status
// <-- 自动调用 unlock()
}
}
这与 C 的对比:C 代码需要手动 mutex_lock(®s->lock) 再 mutex_unlock,任何忘记 unlock、异常路径未 unlock 或 double-unlock 都造成死锁/崩溃。Rust 中 Guard 的 RAII 由编译器保证。
4.2 Spinlock 的 irqsave 问题
自旋锁在内核中常需要同时关中断(spin_lock_irqsave),防止中断处理程序死锁。Rust 用类型系统跟踪:
use kernel::sync::SpinLock;
struct NetDeviceStats {
lock: SpinLock<Stats>,
}
impl NetDeviceStats {
fn update_rx_count(&self, count: u64) -> u64 {
let guard = self.lock.lock(); // 自动关中断,save flags
self.stats.rx_packets += count;
self.stats.rx_packets
// Drop 时自动开中断
}
}
五、FFI 的双向桥梁:Rust ↔ C 的实际对接
5.1 bindings 自动生成
rust-for-linux 项目使用 bindgen 在编译时从 C 头文件自动生成 Rust FFI:
// rust/build.rs 中调用 bindgen
let bindings = bindgen::Builder::default()
.header("include/linux/mypci.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
生成的 bindings.rs 把 C 的 struct pci_dev、pci_probe() 等转为 unsafe extern "C" 函数声明。
5.2 实际驱动骨架:PCI 设备驱动
#[repr(C)] // 保证内存布局与 C 一致
struct PhantomPciDriver {
// 对应 C 的 struct pci_driver
}
// 安全的 Rust API 封装 unsafe C FFI
unsafe extern "C" fn probe(dev: *mut bindings::pci_dev) -> core::ffi::c_int {
// 1. 安全地包装原始指针
let dev = match unsafe { Device::from_raw(dev) } {
Some(d) => d,
None => return bindings::EINVAL as _,
};
// 2. 安全资源分配
let bar = dev.iomap(0)?;
...
0
}
unsafe extern "C" fn remove(dev: *mut bindings::pci_dev) {
// 清理:释放 DMA 内存、注销中断...
}
注意 unsafe 块的使用原则:只在 FFI 边界标记 unsafe,内部全部用 safe Rust 操作。
六、真实案例:NVMe 驱动的 Rust 重写
Google 正在将 NVMe 驱动用 Rust 重写(尚未合入主线)。关键数据点:
| 指标 | C 版本 | Rust 版本 | 变化 |
|---|---|---|---|
| 代码行数 | ~18,000 | ~15,200 | -15% |
| 内存安全缺陷历史 | 12 个 CVE | 0 | -100% |
| 新贡献者上手时间 | ~3 周 | ~2 周 | -33% |
| 编译时间(增量) | 8s | 14s | +75% |
核心洞察:Rust 版本的 bug 率显著下降,主要得益于——
- 编译期强制初始化检查(禁止未初始化变量)
- Option/Result 消灭了 NULL 指针链式崩溃
- 枚举 exhaustive match 消除了遗漏分支
七、实战:从零写一个字符设备驱动
让我们写一个真正能编译的内核字符设备(简化版):
Step 1: Cargo 配置
# rust/Cargo.toml
[package]
name = "rust_chrdev"
version = "0.1.0"
[lib]
crate-type = ["staticlib"]
[dependencies]
kernel = { path = "../linux/rust/kernel" }
Step 2: 驱动实现
// rust/rust_chrdev.rs
use kernel::prelude::*;
use kernel::file_operations::{FileOperations, File, IoBufferWriter, IoBufferReader};
use kernel::sync::Mutex;
use kernel::user_ptr::UserSlicePtrWriter;
module! {
type: RustChrdev,
name: "rust_chrdev",
author: "ybb",
description: "Example Rust character device",
license: "GPL v2",
}
const BUF_SIZE: usize = 1024;
struct RustChrdev {
_reg: Pin<Box<kernel::cdev::Cdev>>,
}
struct DeviceData {
buf: Mutex<[u8; BUF_SIZE]>,
len: Mutex<usize>,
}
static DEVICE: kernel::sync::UniqueRef<DeviceData> = kernel::sync::UniqueRef::new();
impl kernel::Module for RustChrdev {
fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> {
let data = DEVICE.try_new(|| DeviceData {
buf: Mutex::new([0u8; BUF_SIZE]),
len: Mutex::new(0),
})?;
let cdev = kernel::cdev::Cdev::new("rust_chrdev", data)?;
cdev.add(0, 1)?; // 主设备号 0, 次设备号 1
Ok(RustChrdev { _reg: cdev })
}
}
#[vtable]
impl FileOperations for RustChrdev {
fn read(this: &DeviceData, _file: &File, uwriter: &mut UserSlicePtrWriter, offset: u64) -> Result<usize> {
let buf = this.buf.lock();
let len = *this.len.lock();
if offset as usize >= len {
return Ok(0); // EOF
}
let available = len - offset as usize;
let to_copy = core::cmp::min(available, uwriter.len());
uwriter.write_slice(&buf[offset as usize..offset as usize + to_copy])?;
Ok(to_copy)
}
fn write(this: &DeviceData, _file: &File, ureader: &mut UserSlicePtrReader, offset: u64) -> Result<usize> {
let mut buf = this.buf.lock();
let mut len = this.len.lock();
let offset = offset as usize;
if offset >= BUF_SIZE {
return Err(ENOSPC);
}
let space = BUF_SIZE - offset;
let to_copy = core::cmp::min(space, ureader.len());
let mut tmp = [0u8; BUF_SIZE];
ureader.read_slice(&mut tmp[..to_copy])?;
buf[offset..offset + to_copy].copy_from_slice(&tmp[..to_copy]);
*len = core::cmp::max(*len, offset + to_copy);
Ok(to_copy)
}
}
对比 C 版本的差异:
- C 需要
copy_from_user()+ 错误检查 + 手动kmalloc - Rust 的
UserSlicePtrWriter在类型层面区分用户态/内核态指针 - Mutex Guard 防止忘记释放锁
*len = ...不需要WRITE_ONCE()或smp_wmb()——因为 Mutex 提供内存序保证
八、性能开销实测:Rust 与 C 的差距
Rust Arc/Box 在 hot path 上的开销实测(Phoronix 测试,Linux 6.6):
测试: kernel build allyesconfig
┌──────────────────────┬──────────┬──────────┐
│ │ C │ Rust │
├──────────────────────┼──────────┼──────────┤
│ 系统调用延迟 (μs) │ 0.82 │ 0.84 │
│ IOPS (NVMe 随机读) │ 1.2M │ 1.18M │
│ 编译时间增量 │ 基线 │ +15~30% │
│ 代码缓存缺失率 │ 8.2% │ 8.4% │
└──────────────────────┴──────────┴──────────┘
结论:运行时性能差异在 2% 以内,但编译时间增加 15~30%。这是当前最主要的 trade-off。
九、当前生态与落地路线
截至 Linux 6.12,主线已合入的 Rust 组件包括:
- drivers/net/: Realtek r8169 网卡驱动(已用于部分产品)
- fs/: 实验性文件系统框架(已支持简易文件系统)
- drivers/gpu: GPU DRM 驱动骨架
- samples/: 多种示例驱动
路线图显示:
- Linux 7.x: 目标合入 5+ 生产级驱动
- Android Binder: 已启动 Rust 重写评估
- BPF 工具链: rust-bpf 逐渐成熟
对于普通开发者,现在最佳切入点是: 1. 写字符设备驱动(学习 sync、ioctl、mmap) 2. 贡献 drivers/net/ 子系统的简单驱动 3. 探索用 Rust 写 eBPF 用户态加载器(aya 项目)
十、总结
Rust for Linux 不是银弹,但它解决了一个核心问题:将内核中最常见的一类安全漏洞从"review+测试+运气"转移到"编译器静态保证"。代价是更陡峭的学习曲线和稍慢的编译速度。
随着编译缓存(rust-cache)和增量编译的完善,以及更多驱动被重写为 Rust,这个方案正从实验走向生产。对于希望在内核开发的区分度上取得优势的开发者,现在正是入场的窗口期。
参考资源

发表评论 取消回复