Rust for Linux:用 Rust 编写生产级内核模块的工程实战

自 2022 年 Linux 6.1 合并 Rust 基础支持以来,Rust 在内核开发领域已从实验性功能稳步走向生产可用。本文深入剖析 Rust for Linux 的工程实战路径:从环境搭建、内存模型差异,到真实字符设备与文件系统模块的实现,以及如何处理与 C 子系统、并发原语、错误处理的交互。


一、为什么内核需要 Rust?

Linux 内核代码量已超 3000 万行,其中 C 代码贡献了约 70% 的高危 CVE。缓冲区溢出、Use-After-Free、空指针解引用等内存安全问题在传统 C 编码中难以根除。Rust 的类型系统与借用检查器在编译期消除整类内存错误,这对于特权级运行的kernel代码意义重大。

Linux 内核中 Rust 的目标不是重写所有模块,而是:

  • 新驱动开发:特别是外设驱动、文件系统、网络协议栈新模块
  • 逐步替换:高安全风险子系统(如 Android Binder 已在评估中)
  • fuzzing 防护:Rust 的边界检查天然限制模糊测试的破裂面

二、环境搭建与工程配置

2.1 工具链要求

# 需要 nightly Rust 提供的在内核中需要的特性
rustup toolchain install nightly-2024-09-01
rustup component add rust-src

# 内核源码需要开启 CONFIG_RUST=y
# .config 关键配置项:
# CONFIG_RUST=y
# CONFIG_RUST_BUILD_ASSERT_ALLOW=y
# CONFIG_SAMPLES_RUST=y

2.2 Cargo 与 Rust 模块的集成

Rust 内核模块的入口不是 main(),而是通过 module! 宏定义:

// src/lib.rs
use kernel::prelude::*;

module! {
    type: RustFsModule,
    author: "ybb.press",
    description: "Rust production filesystem example",
    license: "GPL",
}

struct RustFsModule {
    _data: Pin<Box<RustFsState>>,
}

impl kernel::Module for RustFsModule {
    fn init(module: &'static ThisModule) -> Result<Self> {
        pr_info!("Rust kernel module loaded\n");
        Ok(Self {
            _data: Box::pin_init(box RustFsState::new())?,
        })
    }
}

2.3 关键依赖:kernel crate

rust-for-linux 仓库提供的 kernel crate 封装了: - 内存分配器(alloc::boxed::Box、Vec 重定向至 kmalloc) - 自旋锁(kernel::sync::SpinLock)、互斥量、RCU 接口 - C 字符串封装(CStr、CString) - 错误处理(Result<T, Error> 对接内核错误码)


三、字符设备实战:带 ioctl 的安全驱动

实现一个可 mmap、支持 ioctl 的字符设备,重点展示 Rust 如何消除 C 实现中的漏洞模式。

use kernel::io_buffer::{IoBufferReader, IoBufferWriter};
use kernel::sync::{Mutex, Arc, UniqueArc};
use kernel::file::{self, File, Operations};
use kernel::task::Task;
use kernel::user_ptr::{UserSlicePtrReader, UserSlicePtrWriter};

const RUSTFS_MAGIC: u32 = 0x5246_534D; // 'RFSM'
const IOCTL_SET_KEY: u32 = IOWR('R', 0,kernel::types::c_uint);

struct DeviceState {
    buffer: Mutex<Vec<u8>>,
    access_key: Mutex<u32>,
}

struct RustDevice {
    _inner: Pin<Box<Mutex<DeviceState>>>,
}

#[vtable]
impl file::Operations for RustDevice {
    fn open(_shared: &(), _file: &File) -> Result {
        Ok(())
    }

    fn read(
        data: Pin<&mut DeviceState>,
        _file: &File,
        writer: &mut impl IoBufferWriter,
        offset: u64,
    ) -> Result<usize> {
        let state = data.lock();
        let buf = &state.buffer;
        let offset = offset as usize;
        if offset >= buf.len() {
            return Ok(0);
        }
        let to_copy = core::cmp::min(writer.len(), buf.len() - offset);
        writer.write_slice(&buf[offset..offset + to_copy])?;
        Ok(to_copy)
    }

    fn write(
        data: Pin<&mut DeviceState>,
        _file: &File,
        reader: &mut impl IoBufferReader,
        offset: u32,
    ) -> Result<usize> {
        let mut state = data.lock();
        let offset = offset as usize;
        let len = reader.len();
        if offset + len > state.buffer.len() {
            state.buffer.try_resize(offset + len, 0)?;
        }
        state.buffer[offset..offset + len].copy_from_slice(...);
        Ok(len)
    }

    fn ioctl(
        data: Pin<&Mut DeviceState>,
        _file: &File,
        cmd: u32,
        arg: usize,
    ) -> Result<u64> {
        match cmd {
            IOCTL_SET_KEY => {
                let mut key = data.access_key.lock();
                *key = arg as u32;
                Ok(0)
            }
            _ => Err(EINVAL),
        }
    }
}

关键设计点解析

C 常见漏洞 Rust 防护机制 内核场景
缓冲区越界 编译期索引检查 + write_slice 自动边界验证 用户态数据读取
竞争条件 Mutex 锁保护 + 借用检查器禁止可变别名 设备状态修改
未初始化内存 try_zeroed_init() 强制显式初始化 DMA 缓冲区
double-free Box + Pin 所有权语义 资源释放

四、与 C 子系统 FFI 交互

Rust for Linux 不可避免需要调用现有 C 代码(如块层、网络栈),安全地进行 FFI 是工程化的核心。

4.1 手动封装 C 函数(以 ext4 交互为例)

use kernel::c_types;

extern "C" {
    fn ext4_get_inode_loc(
        inode: *mut c_types::c_void,
        block: *mut c_types::c_ulonglong,
    ) -> c_types::c_int;
}

/// 安全的 Rust 封装
pub fn get_inode_location(inode: &mut Ext4Inode) -> Result<u64> {
    let mut block: u64 = 0;
    let ret = unsafe {
        ext4_get_inode_loc(
            inode as *mut _ as *mut c_types::c_void,
            &mut block,
        )
    };
    if ret != 0 {
        return Err(Error::from_kernel_errno(ret));
    }
    Ok(block)
}

安全契约说明: - unsafe 块是 FFI 与 C 的所有权边界,必须在此处检查所有前置条件 - 返回值映射为 Result,强制调用者处理错误 - C 指针必须明确生命周期,使用 NonNull 代替裸指针可增强空值检查

4.2 bindgen 自动生成绑定

对于大型 C 头文件(如 linux/fs.h),使用 bindgen 自动处理:

# Cargo.toml
[build-dependencies]
bindgen = "0.68"
// build.rs
fn main() {
    let bindings = bindgen::Builder::default()
        .header("wrapper.h")
        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
        .generate()
        .expect("Unable to generate bindings");
    bindings.write_to_file("src/bindings.rs").unwrap();
}

五、并发原语选型:SpinLock vs Mutex vs RCU

内核 Rust 暴露的并发原语与 C 侧一致,但 Rust 的类型系统强制使用规则:

use kernel::sync::{SpinLock, Mutex, RcuGuard};

// 中断上下文必须用 SpinLock
struct IrqSafeData {
    stats: SpinLock<IrqStats>,
}

// 进程上下文慢路径可用 Mutex
struct FsTable {
    entries: Mutex<HashMap<InodeNumber, Entry>>,
}

// 读多写少场景使用 RCU
struct RcuConfig {
    // RcuHead 自动管理 grace period
    head: kernel::sync::RcuHead,
}

选型原则:

中断上下文 ──────► SpinLock(关抢占)
可睡眠区域 ──────► Mutex(支持 lockdep 检测)
读多写少 ────────► RCU(读侧零开销)
原子操作 ────────► Atomic + Memory Ordering 显存屏障

Rust 的类型状态模式(typestate)还能在编译期保证 mutex 锁的获取顺序,避免死锁 —— 这是 C 代码中 lockdep 动态检测无法实现的。


六、错误处理与资源清理的 RAII 模式

内核 Rust 利用所有权机制实现自动资源管理,替代 C 的 goto err 清理链:

fn setup_device() -> Result<Device> {
    let mem = alloc::alloc::kmalloc(4096, GFP_KERNEL)?;
    // mem 离开作用域自动调用 kfree,无需手动清理

    let clk = clk_get(dev, "bus")?;
    // clk_put 自动释放

    Ok(Device::register(...)?)
}

kernel::error::Error 类型封装了内核错误码,与 C 的 -ENOMEM、-EINVAL 一一对应。? 运算符使得错误传播简洁,且编译器确保不遗漏错误路径。


七、性能考量与传统认知

7.1 零成本抽象验证

Rust 的 trait 虚表调用会带来间接跳转开销。在内核热路径(如网络收包)中,应使用泛型模板而非 dyn Trait:

// 避免:热路径中使用虚表
fn process(packet: &dyn Trait) { ... }

// 推荐:泛型单态化,编译期展开
fn process<T: PacketHandler>(packet: &T) { ... }

7.2 与 C 代码性能对比实测

在 2024 LPC(Linux Plumbers Conference)的 benchmark 中,Rust 实现的 ext4 扩展属性操作与 C 版本性能差异 < 2%,主要开销差异来自: - panicking 路径的栈展开(生产环境中通过 no_panic 宏规避) - 边界检查(release 模式下编译器自动消除可证明安全的检查)

7.3 内核不支持 std

Rust for Linux 使用 no_std + 自定义 kernel crate,因此无法直接使用: - tokio / async-std 等异步运行时 - serde 等序列化框架(理论上可通过 kernel crate 适配) - HashMap 等标准集合(使用 kernel::collections 替代)


八、工程投产路线图

阶段一(当前推荐):新外设驱动、新文件系统模块

风险等级:低 → 出问题可热卸载
适用场景:SDIO驱动、加密硬件驱动、OverlayFS扩展

阶段二(试验中):关键子系统抽象层

Binder 驱动 Rust 重写(Android 团队推进)
网络协议栈新协议(如 MPTCP 部分组件)

阶段三(远期):核心调度器组件

进程调度器 Rust 重写(需更成熟验证)
内存管理子系统(仍在学术论证阶段)

九、调试与观测工具

# 查看 Rust 符号
cat /proc/kallsyms | grep rust

# GDB 打印 Rust 结构体
p *(struct rust_device*)0xffffffffxxxxxx

# Rust 的 debuginfo 已在 6.8+ 内核中可用
# 通过 crash 工具可解析 panic backtrace

pr_info! / pr_err! 宏生成的日志自动带有 Rust 模块标记,便于 dmesg | grep "Rust" 过滤。


十、总结:Rust 内核开发者的五个进阶习惯

  1. Pin 优先:自引用结构(如 file_operations vtable)一律使用 Pin<Box<T>>
  2. CStr 边界:用户态字符串永远通过 CStr::from_ptr 安全封装
  3. unsafe 最小化:将 unsafe 封装到单独模块,公开接口保持 safe
  4. 错误传播显式化:使用 Result + ?,禁止静默吞错
  5. kunit 单元测试:利用 Rust 原生测试框架做模块级 correctness 验证

Rust for Linux 不是银弹,但它正在重新定义"内核代码的安全基线"。从字符设备到新文件系统,从驱动开发到子系统抽象层,Rust 正在从内核边缘走向核心。对驱动开发者而言,现在正是积累 Rust 内核经验的战略窗口期。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部