Rust for Linux 内核:从驱动程序 ABI 到生产级硬件驱动工程
Linux 内核自 6.1 版本起正式支持 Rust 作为第二语言,这是自 1991 年内核诞生以来最大的语言扩展。本文深入解析 Rust 在内核中的技术实现路径、所有权模型如何映射内核内存管理、以及如何编写一个可运行的生产级 misc 设备驱动。
一、为什么是 Rust?内核开发的痛点与破局
Linux 内核代码中约 70% 的 CVE 漏洞源于内存安全问题:缓冲区溢出、Use-After-Free、Double-Free、NULL 指针解引用等。微软和 Google 的公开数据显示,其系统软件中分别有约 70% 和 65% 的安全漏洞与内存安全相关。
C 语言自由度过高,静态分析工具(Sparse、Coverity、KASAN)只能在开发或测试阶段事后发现问题。而 Rust 的编译期借用检查器(Borrow Checker)从根本上消除了整类内存安全问题,其核心保证是:要么存在多个不可变引用,要么存在唯一可变引用。
Linus Torvalds 在 2022 年内核邮件列表中明确表态:"Rust 的内存安全保证正是内核所需要的,我不希望看到任何关于 'C 语言更安全' 的争论阻碍这一进程。"
1.1 Rust 在内核中的定位:不是替代 C,而是增量引入
Rust for Linux 项目的目标是允许用 Rust 编写新的内核模块、驱动和子系统。现有的 C 代码不会被重写。这种渐进式策略意味着:
- Rust 代码通过
bindgen自动生成对 C 内核 API 的 FFI 绑定 - Rust 模块与 C 模块共存于同一个内核地址空间
- 编译系统通过 Kconfig 选项
CONFIG_RUST控制
二、核心机制:从 Rust ABI 到内核 Module 宏
2.1 ABI 兼容性问题
Rust 默认使用自身的不稳定 ABI,这意味着不同编译器版本编译的 Rust 代码可能不兼容。内核使用 -Z randomize-layout 和 -Z mutable-noalias 等不稳定 flags 来解决这个问题,同时通过 bindgen 生成与内核 C ABI 完全匹配的绑定。
内核构建系统在 scripts/rustc-local-version 中硬编码了特定的 rustc 版本要求(当前为 1.81+),并通过 rust/ 目录下的 Makefile 管理编译流程:
# scripts/rust/Makefile 核心片段
RUSTC := $(shell command -v rustc 2>/dev/null)
BINDGEN := $(shell command -v bindgen 2>/dev/null)
# 内核使用的特殊 flags
RUSTFLAGS = -Zunstable-options \
-Zmacro-backtrace \
-Ccode-model=kernel \
-C relocation-model=pic \
-Clink-args=-nostdlib
2.2 内核 Module 宏
Rust 内核模块使用 module! 宏进行声明。这个宏会生成初始化/退出函数的原型,并确保正确注册到内核模块子系统:
use kernel::prelude::*;
module! {
type: RustMiscDevice,
name: "rust_misc_device",
author: "ybb.press",
description: "A simple misc device driver in Rust",
license: "GPL",
}
struct RustMiscDevice {
registration: misc::Registration,
}
该宏展开后会生成类似 C 模块的 module_init() 和 module_exit() 入口点,但其类型安全性由 Rust 编译器在编译期保证。
2.3 错误处理映射
内核 C 函数通常返回负数错误码(如 -ENOMEM)。Rust for Linux 将其映射为 ResultError 封装了内核错误码:
// from kernel/error.rs
pub struct Error(c_int);
impl Error {
pub const ENOMEM: Self = Error(-(ENOERR as c_int));
pub const EINVAL: Self = Error(-(EINVAL as c_int));
pub const EFAULT: Self = Error(-(EFAULT as c_int));
}
// 使用 Result 替代 C 中的错误码判断
fn read_data(buf: &mut [u8]) -> Result<usize> {
if buf.is_empty() {
return Err(EINVAL);
}
// ... 正常逻辑
Ok(bytes_read)
}
三、所有权模型如何适配内核内存管理
3.1 Box 与 kmalloc
Rust 的 Box 在用户态使用全局分配器,在内核中则通过自定义分配器映射到内核的内存分配接口。Rust for Linux 实现了 kernel::alloc::Box,其底层调用 kmalloc()(GFP_KERNEL 或 GFP_ATOMIC):
use kernel::alloc::Box;
// 在内核堆上分配一个结构体,类似 C 中的 kmalloc(sizeof(struct foo), GFP_KERNEL)
let device: Box<MyDevice> = Box::try_new(MyDevice::new())?;
当 Box 离开作用域时,其 Drop 实现会自动调用 kfree(),避免了 C 语言中常见的内存泄漏问题。
3.2 Arc 与内核引用计数
内核中大量使用 kref 管理对象生命周期。Rust for Linux 提供了 Arc(Atomic Reference Counting),它在语义上对应内核的 kref_get()/kref_put(),但由编译器保证引用计数的原子操作不会遗漏:
use kernel::sync::Arc;
struct SharedState {
counter: AtomicU64,
}
let state = Arc::try_new(SharedState { counter: AtomicU64::new(0) })?;
// 创建多个引用,类似 kref_get()
let ref2 = Arc::clone(&state);
let ref3 = Arc::clone(&state);
// 所有引用离开作用域后自动调用 kref_put() 及其 release 函数
3.3 Mutex 与自旋锁
Rust 的类型系统将锁保护的值与锁本身绑定。你必须先获取锁才能访问数据,这消除了 C 语言中常见的"忘记加锁"或"错误顺序加锁"问题:
use kernel::sync::Mutex;
struct ProtectedData {
inner: Mutex<InnerData>,
}
struct InnerData {
value: u64,
}
fn update_value(data: &ProtectedData, new_val: u64) -> Result<()> {
// lock() 返回 Guard 类型,离开作用域时自动释放锁
let mut guard = data.inner.lock();
guard.value = new_val;
// 锁在这里自动释放,即使在 return 或 panic 情况下
Ok(())
}
对比 C 语言中典型的死锁场景:
// C 中容易出错的模式
spin_lock(&lock1);
if (condition) {
return -EINVAL; // 错误!忘记释放 lock1
}
spin_lock(&lock2);
Rust 的 RAII(Resource Acquisition Is Initialization)模式确保锁的释放路径由编译器保证。
四、实战:编写一个 misc 设备驱动
让我们通过一个完整的示例,展示如何用 Rust 实现一个可与用户态通过 /dev 交互的字符设备。这个驱动会实现一个简单的 echo 缓冲区。
4.1 完整驱动代码
// rust_echo_device.rs
use kernel::prelude::*;
use kernel::sync::Mutex;
use kernel::file;
use kernel::miscdev;
use kernel::io_buffer::IoBufferWriter;
use kernel::io_buffer::IoBufferReader;
use kernel::task::Task;
module! {
type: RustEchoDevice,
name: b"rust_echo_device",
author: b"ybb.press",
description: b"Echo buffer character device driver written in Rust",
license: b"GPL",
}
const BUF_SIZE: usize = 1024;
struct EchoBuffer {
data: [u8; BUF_SIZE],
len: usize,
}
struct RustEchoDevice {
buffer: Mutex<EchoBuffer>,
dev: miscdev::Registration<RustEchoDevice>,
}
#[vtable]
impl file::Operations for RustEchoDevice {
type Data = ();
type OpenData = ();
fn open(_data: (), _file: &file::File) -> Result {
pr_info!("rust_echo_device: device opened\n");
Ok(())
}
fn read(
_data: (),
file: &file::File,
writer: &mut IoBufferReader,
offset: u64,
) -> Result<usize> {
let dev = file.private_data::<RustEchoDevice>()
.ok_or(EINVAL)?;
let buffer = dev.buffer.lock();
if offset as usize >= buffer.len {
return Ok(0); // EOF
}
let available = &buffer.data[offset as usize..buffer.len];
writer.read_buffer(available)
}
fn write(
_data: (),
_file: &file::File,
reader: &mut IoBufferWriter,
_offset: u64,
) -> Result<usize> {
let dev = file.private_data::<RustEchoDevice>()
.ok_or(EINVAL)?;
let mut buffer = dev.buffer.lock();
let to_copy = core::cmp::min(BUF_SIZE - buffer.len, reader.len());
if to_copy == 0 {
pr_warn!("rust_echo_device: buffer full, dropping data\n");
return Ok(0);
}
reader.read_slice(&mut buffer.data[buffer.len..buffer.len + to_copy])?;
buffer.len += to_copy;
pr_info!("rust_echo_device: wrote {} bytes (total: {})\n", to_copy, buffer.len);
Ok(to_copy)
}
fn release(_data: (), _file: &file::File) {
pr_info!("rust_echo_device: device closed\n");
}
}
impl kernel::Module for RustEchoDevice {
fn init(module: &'static ThisModule) -> Result<Self> {
pr_info!("rust_echo_device: initializing Rust misc device driver\n");
let buffer = Mutex::new(EchoBuffer {
data: [0u8; BUF_SIZE],
len: 0,
});
let dev = miscdev::Registration::new_pinned(fmt!("rust_echo"), ())?;
Ok(RustEchoDevice { buffer, dev })
}
}
4.2 编译配置
内核配置需要启用 Rust 支持:
# .config
CONFIG_RUST=y
CONFIG_RUSTC_VERSION_TEXT="rustc 1.81.0"
CONFIG_BINDGEN_VERSION=0.70.1
CONFIG_RUST_IS_AVAILABLE=y
# 在 Kconfig 中定义模块
config RUST_ECHO_DEVICE
tristate "Rust Echo Character Device"
depends on RUST
help
A simple echo buffer character device driver written in Rust.
4.3 用户态测试
# 编译并加载模块
$ make -j$(nproc) M=drivers/rust/echo/
$ sudo insmod rust_echo_device.ko
$ ls -la /dev/rust_echo
crw-rw-rw- 1 root root 10, 123 Oct 8 01:00 /dev/rust_echo
# 写入数据
$ echo "Hello from Rust for Linux!" > /dev/rust_echo
$ dmesg | tail -1
[ 123.456] rust_echo_device: wrote 26 bytes (total: 26)
# 读回数据
$ cat /dev/rust_echo
Hello from Rust for Linux!
五、FFI 边界:Rust 与 C 的互操作
5.1 bindgen 自动生成绑定
bindgen parse C 头文件并生成对应的 Rust 代码。对于内核 API,Rust for Linux 使用特殊的生成流程:
# 生成内核 C API 的 Rust 绑定
scripts/rust_is_late_filter_bindgen.sh \
--include-linux-h \
--kernel-dir=$(srctree)/vmlinux \
> rust/bindings/bindings_generated.rs
生成的代码包含了内核中所有可从 Rust 调用的函数原型。例如 printk 对应:
extern "C" {
pub fn printk(fmt: *const core::ffi::c_char, ...) -> core::ffi::c_int;
}
但更推荐使用内核提供的宏:
// pr_info! 的展开等价于 printk(KERN_INFO_fmt, ##__VA_ARGS__)
pr_info!("Hello from kernel! value = {}\n", 42);
5.2 安全封装模式
Rust for Linux 遵循"unsafe FFI 调用必须封装在安全抽象之后"的原则。以 kmalloc 为例:
// 不安全:直接调用
unsafe { __kmalloc(size, flags) as *mut T }
// 安全封装:kernel::alloc::Box
pub fn try_new(value: T) -> Result<Box<T>> {
// 验证分配成功、初始化、返回安全引用
let ptr = unsafe { __kmalloc(core::mem::size_of::<T>(), flags) };
if ptr.is_null() {
return Err(ENOMEM);
}
Ok(unsafe { Box::from_raw(ptr as *mut T) })
}
六、性能开销与安全检测工具协同
6.1 零成本抽象
Rust 的"零成本抽象"原则在内核语境下成立。以 Box 和 Arc 为例:
| 操作 | C 等价物 | Rust 开销 |
|---|---|---|
Box::new() | kmalloc() | 零额外开销 |
Arc::clone() | kref_get() | 原子递增(与 C 相同) |
Arc::drop() | kref_put() | 原子递减+条件释放 |
| Mutex 锁 | mutex_lock() | 零额外开销 |
6.2 与 KASAN 协同工作
KASAN(Kernel Address Sanitizer)可以在 Rust 模块中正常工作。对于 Rust 代码中的边界越界访问,KASAN 会报告如下信息:
[ 78.123] ==================================================================
[ 78.124] BUG: KASAN: slab-out-of-bounds in rust_echo_device_write+0x123/0x1a0 [rust_echo_device]
[ 78.125] Read of size 1 at addr ffff88800abcdef0 by task test_thread
由于 Rust 的借用检查器在编译时就阻止了大部分越界访问,实际运行时的 KASAN 报告应该很少——这正是向 Rust 迁移的价值所在。
6.3 KCSAN 数据竞争检测
KCSAN(Kernel Concurrency Sanitizer)同样适用于 Rust 模块。它可以帮助发现 Rust unsafe 块中的潜在数据竞争,以及 FF 边界上的同步问题。
七、当前生态与社区进展
7.1 主线合并时间线
| 内核版本 | 重要里程碑 |
|---|---|
| 6.1 (2022.12) | Rust 基础设施合并,支持 x86_64 |
| 6.4 | ARM64 架构支持 |
| 6.8 | RISCV 架构支持,集成 Rust 版 NVMe 驱动示例 |
| 6.11 | 网络子系统 Rust 绑定,rnull(Rust 版 null 设备)替代 C 版 |
7.2 驱动生态现状
截至目前,Rust for Linux 仓库中已有:
- rnull: 替代 C 语言 null 设备驱动,已合入 6.11 主线
- network: 网络设备 trait 和 phylayer 绑定框架
- NVMe: NVMe 驱动的子系统绑定(部分合入)
- GPU: Nova 项目(社区 RISC-V GPU 驱动,使用 Rust)
- 文件系统: 早期 FS 绑定原型
7.3 问题与挑战
Rust for Linux 仍面临的挑战包括:
- 编译器版本耦合: 每个内核版本绑定到特定 rustc 版本
- 不稳定特性: 依赖多个 rustc nightly
-Zflags,维护负担重 - C 与 Rust 代码的语义鸿沟: 如 C 的 flexible array member、typeof、statement expressions 等
- 构建时间: Rust 模块编译速度比 C 慢约 3-5x
- 调试体验: GDB 对 Rust 内核模块的符号解析尚不完善
- 网络旁路工具(XDP/eBPF related userspace helper)
- 新的字符设备驱动
- 文件系统新的 in-memory 类型
- 新项目(如 Nova GPU 驱动)直接采用 Rust
- 新的网络协议栈实现
- 等 rustc 稳定 ABI 后,逐步重写既有驱动
八、部署建议与检查清单
考虑将 Rust 用于生产级内核模块时,建议逐步推进:
阶段一:旁路组件(推荐起步点)
阶段二:新建子系统
阶段三:驱动迁移
部署前检查清单
□ 目标平台 rustc 版本 >= 内核要求的最低版本
□ bindgen 版本匹配
□ 内核配置启用 CONFIG_RUST=y
□ 模块编译无 unsafe 滥用审计通过
□ 与 C 层交互的 FFI 边界已人工 review
□ 已用 KASAN + KCSAN 跑过压力测试
□ 性能基准与 C 实现对比(应无显著退化)
□ 恐慌(panic)处理策略已定义(oops / 优雅降级)
九、总结
Rust for Linux 不是简单地把 Rust 代码塞进内核模块目录,而是经过多年工程实践形成的技术体系。从 bindgen 自动绑定生成,到 module! 宏的类型安全封装,再到 Box/Arc 与 kmalloc/kref 的语义映射——每一层都在解决 Rust 类型系统与内核 C ABI 的语义鸿沟。
对于驱动开发者来说,迁移到 Rust 的最大收益在于:将运行时难以复现的内存安全问题,转变为编译期的确定性错误。这在内核场景中意味着更少的 CVE、更短的漏洞修复周期,以及最终更可靠的系统。
随着 Nova GPU 驱动等更大的 Rust 内核项目进入主线,我们有理由期待——下一个五年内,Rust 将成为内核开发的"第二默认语言"。
参考资料

发表评论 取消回复