Rust for Linux:用 Rust 编写内核模块的工程实践

Linux 6.1 正式合并 Rust 支持,标志着内核开发进入双语言时代。本文深入剖析 Rust for Linux 的架构设计、内存安全模型、FFI 互操作机制,并实战编写一个字符设备内核模块。

一、为什么内核需要 Rust

自 1991 年诞生以来,C 语言一直是 Linux 内核的唯一开发语言。C 赋予了开发者极致的底层控制力,但也带来了挥之不去的安全隐患。根据微软和谷歌的研究报告,约 70% 的 CVE 安全漏洞源于内存安全问题,而在内核场景中,这些问题往往意味着特权提升或远程代码执行。

C 语言的内存安全问题根植于其设计哲学:

  • 未定义行为(UB)的普遍存在——空指针解引用、缓冲区溢出、use-after-free 在 C 中均为 UB
  • 缺乏边界检查——数组越界不会触发运行时 panic
  • 手动内存管理——free 后悬挂指针的问题无法在编译期消除
  • 数据竞争——C 的内存模型对多线程场景缺乏形式化保证

Rust 的所有权系统能在编译期消除以上绝大多数问题。当 Linux 内核面对日益严峻的安全形势,引入 Rust 成为必然选择。这不是要替代 C,而是在关键新代码中提供内存安全保证,特别是驱动程序、网络栈、文件系统等攻击面较大的子系统。

二、Rust for Linux 架构解析

2.1 编译工具链集成

Rust for Linux 并非简单地在内核目录里放几个 .rs 文件。它深度整合进 KBuild 系统,核心机制包括:

  1. rustc 与 gcc 协同编译——内核构建系统同时调用 rustc 和 gcc,分别编译 .rs 和 .c 文件,最终由 ld 链接为统一的内核镜像
  2. 自定义 target spec——使用 x86_64-unknown-none 或 aarch64-unknown-none 等 no_std target,并扩展内核特定的 ABI 约定
  3. bindgen 自动生成绑定——通过 bindgen 从内核头文件自动生成 Rust FFI 绑定,保持与 C 内核 API 的同步
  4. ** compiler_builtins 重写**——内核环境下不提供标准库的 memcpy/memset,需使用内核自带的优化实现

2.2 内核预lude 与核心抽象

Rust for Linux 提供了一套 kernel crate,类似于用户空间的 std,但极度精简:

//! 内核模块入口声明示例
module! {
    type: RustMinimal,
    author: "ybb",
    description: "Rust for Linux minimal example",
    license: "GPL",
}

核心模块位于 rust/ 目录: - rust/kernel/——核心 prelude、sync、types 抽象 - rust/macros/——module!、vtable!、pin_init! 等声明宏 - rust/bindings/——bindgen 生成的 C 绑定 - rust/helpers/——Rust 实现的内核辅助函数(供 C 侧调用)

2.3 pin-init:安全初始化的核心创新

内核中大量数据结构需要在堆栈上分配、通过指针引用(不可能移动),且初始化可能失败需要回收。Rust 的 Pin trait 部分解决了移动问题,但初始化时的"先构造后 pinning"的安全保证需要专门抽象。

Rust for Linux 引入了 pin_init 宏系统:

use kernel::pin_init::*;
use kernel::sync::Mutex;

#[pin_data]
struct SharedState {
    #[pin]
    inner: Mutex<InnerState>,
    data: Vec<u8>,
}

impl SharedState {
    fn new() -> impl PinInit<Self> {
        pin_init! {
            Self {
                inner <- pin_init!(Mutex::new(InnerState::default())),
                data <- pin_init!(vec_init!(vec![0u8; 4096])),
            }
        }
    }
}

pin_init! 在编译期构建了一个状态机:每个字段按顺序初始化,任意步骤失败时自动回滚已完成初始化。这解决了 Rust 中"部分初始化结构体"的经典难题,完美适配内核"goto err"错误处理模式。

三、所有权模型与内核惯用法的碰撞

3.1 引用计数:Arc vs kref

内核中广泛使用 kref 进行引用计数。Rust 提供了 Arc(Atomic Reference Counted),但两者有本质区别:

  • Arc——强制 unsafe 解引用内部数据(Arc::get_mut 仅在计数为 1 时可用)
  • kref——不保证独占访问,仅当 kref_put 时调用 release 回调
  • 内存顺序——内核使用更精细的内存屏障,而 Arc 使用 Acquire/Release

在 Rust for Linux 中,共享内核对象通常用 Arc,但需要手动管理原始指针引用计数的场景则需自定义智能指针封装。

3.2 并发原语

内核同步机制与 Rust 的对齐:

内核原语 Rust for Linux 封装 说明
spinlock_t kernel::sync::SpinLock 关闭抢占的自旋锁
mutex_t kernel::sync::Mutex 可睡眠互斥锁
rw_semaphore kernel::sync::RwSemaphore 读写信号量
rcu_lock kernel::sync::RawSpinLock RCU 读端使用 preempt_disable
completion kernel::sync::Completion 完成量(条件变量替代)

一个常见陷阱:Rust MutexGuard 在 async fn 中跨越 .await 点会导致编译错误,因为可能持有锁睡眠。在内核中通过 MutexGuard::must_not_sleep() 静态检查解决。

3.3 错误处理

内核使用整数错误码(负数 errno),Rust 使用 Result。Rust for Linux 通过 kernel::error 模块桥接:

use kernel::error::Result;
use kernel::error::codes;

fn do_io() -> Result<usize> {
    // 自动将 Err(-EINVAL) 转为 -> Result
    let ptr = unsafe { alloc_memory(size) };
    if ptr.is_null() {
        return Err(codes::ENOMEM); // 内联常量
    }
    Ok(len)
}

? 操作符在内核模块中可用,因为 From<Error> 为所有 i32 错误码实现了转换。

四、FFI 互操作:Rust 与 C 内核代码的共舞

4.1 调用 C 函数

bindgen 生成的函数签名均为 unsafe extern "C":

extern "C" {
    fn kmalloc(size: usize, gfp: gfp_t) -> *mut c_void;
    fn kfree(ptr: *const c_void);
}

unsafe fn rust_kmalloc(size: usize, flags: gfp_t) -> Result<*mut u8> {
    let ptr = kmalloc(size, flags);
    if ptr.is_null() {
        Err(ENOMEM)
    } else {
        Ok(ptr as *mut u8)
    }
}

4.2 向 C 提供 Rust 回调

通过 #[vtable] trait 将 Rust 方法暴露给 C 侧的函数指针表:

use kernel::device::Device;
use kernel::file_operations::{FileOperations, Registration};

#[vtable]
impl FileOperations for RustFileOps {
    type Data = Arc<Mutex<FilePrivate>>;

    fn read(
        _data: <Self::Data>::Borrow<'_>,
        _file: &File,
        buf: &mut UserSliceWriter,
        offset: &mut u64,
    ) -> Result<usize> {
        let data = b"Hello from Rust kernel module!\n";
        buf.write_slice(data)?;
        Ok(data.len())
    }

    fn write(
        data: <Self::Data>::Borrow<'_>,
        _file: &File,
        buf: &mut UserSliceReader,
        _offset: &mut u64,
    ) -> Result<usize> {
        // 写入长度限制
        let len = buf.len().min(1024);
        let mut tmp = vec![0u8; len];
        buf.read_slice(&mut tmp)?;
        pr_info!("Received {} bytes from userspace\n", len);
        Ok(len)
    }
}

4.3 共享结构体定义

通过 #[repr(C)] 保证内存布局兼容:

#[repr(C)]
pub struct SharedRingBuffer {
    head: atomic_t,     // 由 C 侧的 atomic_inc 操作
    tail: atomic_t,
    size: usize,
    data: [u8; 0],      // 柔性数组
}

五、实战:编写一个 Rust 字符设备内核模块

5.1 完整模块代码

// rust_chrdev.rs
use kernel::prelude::*;
use kernel::sync::{Mutex, Arc};
use kernel::file_operations::{FileOperations, Registration};
use kernel::user_slice::{UserSliceReader, UserSliceWriter};
use kernel::c_str;

module! {
    type: RustChrDev,
    author: "ybb <[email protected]>",
    description: "Rust for Linux Char Device Example",
    license: "GPL",
}

const DEVICE_NAME: &CStr = c_str!("rust_chrdev");
const BUF_SIZE: usize = 4096;

struct ChrDevState {
    buffer: [u8; BUF_SIZE],
    len: usize,
    open_count: u64,
}

struct RustChrdev {
    state: Mutex<ChrDevState>,
}

#[vtable]
impl FileOperations for RustChrDev {
    type Data = Arc<Mutex<ChrDevState>>;

    fn open(
        data: <Self::Data>::Borrow<'_>,
        _file: &File,
    ) -> Result<Self::Data> {
        pr_info!("rust_chrdev: device opened\n");
        Ok(data.clone())
    }

    fn read(
        data: <Self::Data>::Borrow<'_>,
        _file: &File,
        buf: &mut UserSliceWriter,
        offset: &mut u64,
    ) -> Result<usize> {
        let state = data.lock();
        let avail = state.len.saturating_sub(*offset as usize);
        if avail == 0 {
            return Ok(0); // EOF
        }
        let to_write = buf.len().min(avail);
        buf.write_slice(&state.buffer[*offset as usize..][..to_write])?;
        *offset += to_write as u64;
        Ok(to_write)
    }

    fn write(
        data: <Self::Data>::Borrow<'_>,
        _file: &File,
        buf: &mut UserSliceReader,
        _offset: &mut u64,
    ) -> Result<usize> {
        let mut state = data.lock();
        let to_copy = buf.len().min(BUF_SIZE - state.len);
        if to_copy == 0 {
            return Err(codes::ENOSPC);
        }
        let tail = state.len;
        state.buffer[tail..][..to_copy].fill(0);
        buf.read_slice(&mut state.buffer[tail..][..to_copy])?;
        state.len += to_copy;
        pr_info!("rust_chrdev: wrote {} bytes (total: {})\n", to_copy, state.len);
        Ok(to_copy)
    }

    fn release(
        _data: <Self::Data>::Borrow<'_>,
        _file: &File,
    ) {
        pr_info!("rust_chrdev: device closed\n");
    }
}

kernel::init::module! {
    fn init() -> Result<Self> {
        pr_info!("rust_chrdev: module loaded\n");

        // 注册字符设备
        let reg = Registration::new_pinned(DEVICE_NAME, 0, RustChrDev {
            state: Mutex::new(ChrDevState {
                buffer: [0u8; BUF_SIZE],
                len: 0,
                open_count: 0,
            }),
        })?;

        pr_info!("rust_chrdev: registered with major number\n");
        Ok(reg)
    }
}

5.2 Makefile 集成

# 需要 CONFIG_RUST=y
obj-m += rust_chrdev.o
rust_chrdev-objs := rust_chrdev.rsi.o

# 内核构建系统自动检测 .rs 文件并调用 rustc

5.3 编译与加载

# 编译内核模块(在内核源码树中)
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

# 加载模块
sudo insmod rust_chrdev.ko

# 测试
echo "Hello from userspace!" > /dev/rust_chrdev
cat /dev/rust_chrdev | xxd | head

# 查看内核日志
dmesg | tail -5
# [  123.456] rust_chrdev: module loaded
# [  125.789] rust_chrdev: device opened
# [  125.790] rust_chrdev: wrote 21 bytes (total: 21)
# [  126.012] rust_chrdev: device closed

# 卸载
sudo rmmod rust_chrdev

六、性能与代价分析

6.1 二进制体积

Rust 编译产物体积通常略大于等效 C 代码,主要原因: - 标准库部分内联(即使 no_std 也有 core) - 单态化产生的多份泛型代码 - 默认包含 panic 处理逻辑

通过 -C opt-level=z 和 -C panic=abort 可显著优化。实际测试显示,简单字符驱动的 Rust 实现比 C 等效实现大 15%-25%。

6.2 运行时开销

零成本抽象原则下,Rust 封装在 release 构建中与 C 几乎相同:

  • MutexGuard → 编译为 mutex_lock / mutex_unlock 的直接调用
  • Arc 递增递减 → atomic_inc_not_zero / atomic_dec_and_test
  • Result::Ok 编译为空标记联合体,无额外分支
  • Drop 实现 → 与 C 的 goto err 清理代码等价

6.3 编译时间

首次构建 Rust for Linux .binding 可能耗时较长(bindgen 解析大量 C 头文件)。增量构建时仅重新编译修改的 crate,影响有限。实际项目中 Rust 文件编译时间通常比 C 慢 2-3 倍。

七、生态进展与局限

7.1 2025-2026 进展

  • Linux 6.12——合并 NVMe 驱动 Rust 实现(drivers/nvme/ 部分模块)
  • Linux 6.14——Binder IPC 驱动 Rust 重写进入审查阶段
  • DMA 框架——引入 DmaPool、DmaMapping 等安全抽象
  • PCI 子系统——新增 Rust PCI 驱动框架
  • 网络——Rust MAC/V PHY 驱动抽象持续完善
  • Android——强烈推动 Rust 驱动迁移(GKI 兼容性要求)

7.2 当前局限

  1. 内联汇编——asm! 宏在内核环境受限,部分场景仍需 C 桥接
  2. 链接器脚本——Rust 无法直接定义自定义 ELF section,复杂模块布局依赖 C 辅助
  3. const 泛型——部分内核数据结构需要复杂的 const generic,编译期检查耗时
  4. unsafe 负担——FFI 调用链中的 unsafe 需要人工审计,工具链辅助有限
  5. 社区分裂风险——极少数资深维护者持抵制态度,代码审查摩擦客观存在

7.3 学习资源

八、结语

Rust for Linux 不是"用 Rust 重写内核",而是一种渐进的安全策略。对于驱动开发者,它提供了编译期的内存安全保证;对于维护者,减少了 review 心智负担;对于最终用户,降低了漏洞利用的可能性。

未来 3-5 年,随着 Rust 编译器在内核编译流程中愈发成熟、驱动生态日趋完善,Rust 代码将成为 Linux 内核不可分割的一部分。对于后端工程师而言,掌握 Rust 内核开发技能,等于握住了一扇通往底层系统编程安全化的大门。


关键词:Rust for Linux、内核模块、驱动开发、内存安全、所有权系统、字符设备 分类:Linux Kernel、Rust、系统编程

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部