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_exit
  • Result<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,确保与 sla​​b 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(&regs->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,这个方案正从实验走向生产。对于希望在内核开发的区分度上取得优势的开发者,现在正是入场的窗口期。


参考资源

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部