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 内核开发者的五个进阶习惯
- Pin 优先:自引用结构(如
file_operationsvtable)一律使用Pin<Box<T>> - CStr 边界:用户态字符串永远通过
CStr::from_ptr安全封装 - unsafe 最小化:将 unsafe 封装到单独模块,公开接口保持 safe
- 错误传播显式化:使用
Result+?,禁止静默吞错 - kunit 单元测试:利用 Rust 原生测试框架做模块级 correctness 验证
Rust for Linux 不是银弹,但它正在重新定义"内核代码的安全基线"。从字符设备到新文件系统,从驱动开发到子系统抽象层,Rust 正在从内核边缘走向核心。对驱动开发者而言,现在正是积累 Rust 内核经验的战略窗口期。

发表评论 取消回复