Linux Kernel Rust-For-Linux 实战——用 Rust 编写内核模块的工程深度剖析
引言:为什么 Linux 内核需要 Rust?
2022 年 9 月,Linus Torvalds 正式合并了 Rust-For-Linux 项目,Linux 6.1 版本首次支持在内核代码中使用 Rust 语言。这是自 1991 年 Linux 发布以来,内核核心代码首次引入 C 之外的编译型语言。两年过去,截至 2024 年底,内核主线已有 18 个 Rust 驱动和子系统模块,包括网络设备、文件系统和 GPU 驱动(Nova 项目)。
为什么需要 Rust?答案在于 C 语言的两个根本问题:内存安全漏洞和并发数据竞争。根据微软和 Google 的数据,约 70% 的安全漏洞源于内存安全问题,而 Linux 内核每年因内存安全问题(Use-After-Free、Buffer Overflow、Double-Free)上报数百个 CVE。Rust 的所有权系统和借用检查器在编译期消除这些隐患——这是任何运行时工具或人工 Review 都无法达到的保证级别。
一、Rust-For-Linux 架构全景
1.1 编译链路
Rust Subcrate
│
▼
rustc (target: x86_64-unknown-none / linux-kernel-rust target)
│ ──emit=obj → .o (ELF relocatable)
│
▼
ld.lld / ar → .rlib / .a (Rust staticlib)
│
▼
KBuild Linux Kernel Linker (scripts/mod/modpost)
│
▼
vmlinux (with Rust sections: .rust_modules, .modinfo, .gnu.linkonce)
关键点有三个:
- Compile Target: 没有现成的 rustc target,内核提供自定义 target spec(
rust/targets/x86_64.json),配置了"no-default-libraries": true、"relocation-model": "static"等。 - 生成代码格式: Rust 编译产物为
.oELF 可重定位文件,内核的modpost工具做了扩展以解析 Rust 的module!宏元数据。 - 符号互调: C 调用 Rust 使用 ABI 强制 extern "C";Rust 调用 C 内核 API 通过
kernel::bindings自动生成(由bindgen扫描内核头文件)。
1.2 crate 层级设计
kernel (伪 crate, 替代 std)
├── kernel::prelude ── 模块公共导入(Result, Box, Arc)
├── kernel::types ── CStr, Mode, Scope, NoWait
├── kernel::sync ── Mutex, SpinLock, RwLock, Ref
├── kernel::alloc ── kzalloc/kmalloc 封装
├── kernel::device ── Device, DeviceId
├── kernel::platform ── PlatformDriver, platform_device
├── kernel::pci ── PciDriver
├── kernel::net ── NetDevice
├── kernel::fs ── FileOperations, File
├── kernel::list ── List, ListArc
├── kernel::rbtree ── RBTree (r4.14+)
├── kernel::task ── Task, TaskWrapper
├── kernel::io ── Io, IoRaw(MMIO/PIO)
├── kernel::time ── Jiffies, Timer, Ktime
├── kernel::error ──{alias} ENOENT, EINVAL 等
└── kernel::bindings ── 自动生成 C FFI bindings
二、实战:编写一个 Rust 字符设备驱动
下面我们从零编写一个完整的 Rust 内核字符设备——/dev/rust_chrdev,它提供基本的 read/write 操作,内部维护一个环形缓冲区,完整演示 Rust 内核编程的核心模式。
2.1 项目结构
rust_chrdev/
├── Cargo.toml
├── lib.rs # 模块入口
├── device.rs # 字符设备实现
└── ring_buf.rs # 环形缓冲区实现
2.2 Cargo.toml 配置
[package]
name = "rust_chrdev"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["staticlib"]
[dependencies]
kernel = { path = "../../rust/kernel" }
说明: Rust 内核模块必须编译为 --crate-type staticlib,因为内核链接器需要静态链接全部代码——运行期没有动态链接器,没有共享库概念。
2.3 环形缓冲区实现(ring_buf.rs)
use kernel::prelude::*;
use kernel::sync::Mutex;
// 固定大小环形缓冲区,无 unsafe —— 相比之下 C 版本极易写错边界
pub(crate) struct RingBuffer {
buf: Mutex<[u8; Self::CAP]>,
head: Mutex<u32>,
tail: Mutex<u32>,
len: Mutex<u32>,
}
impl RingBuffer {
pub(crate) const CAP: usize = 4096;
pub(crate) fn try_new() -> Result<Box<Self>> {
Ok(Box::try_new(Self {
buf: Mutex::new([0u8; Self::CAP]),
head: Mutex::new(0),
tail: Mutex::new(0),
len: Mutex::new(0),
})?)
}
pub(crate) fn write(&self, data: &[u8]) -> Result<usize> {
if data.is_empty() {
return Ok(0);
}
let mut buf = self.buf.lock();
let mut tail = self.tail.lock();
let mut len = self.len.lock();
let cap = Self::CAP;
// 可写入长度 = MIN(data.len(), CAP - len)
let avail = cap - (*len as usize);
let to_write = core::cmp::min(data.len(), avail);
for i in 0..to_write {
let pos = (*tail as usize) % cap;
buf[pos] = data[i];
*tail = (*tail + 1) % (cap as u32);
}
*len += to_write as u32;
Ok(to_write)
}
pub(crate) fn read(&self, out: &mut [u8]) -> Result<usize> {
let buf = self.buf.lock();
let mut head = self.head.lock();
let mut len = self.len.lock();
let cap = Self::CAP;
let to_read = core::cmp::min(out.len(), *len as usize);
for i in 0..to_read {
let pos = (*head as usize) % cap;
out[i] = buf[pos];
*head = (*head + 1) % (cap as u32);
}
*len -= to_read as u32;
Ok(to_read)
}
}
这段代码与 C 版本的核心差异在于:所有越界错误在编译期即被消除。数组索引操作通过 MutexGuard 保护,不存在竞态条件。Box::try_new 的 ? 传播让错误处理内聚而清晰。
2.4 字符设备实现(device.rs)
use kernel::prelude::*;
use kernel::fs::{self, File, FileOperations, ThisModule};
use kernel::io_buffer::{IoBufferReader, IoBufferWriter};
use kernel::device::{Device, DeviceId};
use crate::ring_buf::RingBuffer;
module! {
type: RustChrdev,
name: "rust_chrdev",
author: "ybb",
description: "Rust Char Device Demo",
license: "GPL",
}
struct RustChrdev {
_dev: Pin<Box<Device>>,
}
// 用 Arc + Mutex 在多个文件描述符间共享缓冲区
struct DeviceContext {
buf: Arc<RingBuffer>,
}
#[vtable]
impl FileOperations for DeviceContext {
fn open(_this: &Self, _file: &File) -> Result<Box<Self>> {
pr_info!("rust_chrdev: open()\n");
Ok(Box::try_new(DeviceContext {
buf: Arc::from(RingBuffer::try_new()?),
})?)
}
fn read(this: &Self, _file: &File, data: &mut impl IoBufferWriter, offset: u64) -> Result<usize> {
if offset != 0 {
return Ok(0);
}
// 临时栈缓冲 → IoBufferWriter 以零拷贝方式写入用户态
let mut kbuf = kernel::alloc::alloc![0u8; 4096];
match this.buf.read(&mut kbuf) {
Ok(n) => {
data.write_slice(&kbuf[..n])?;
Ok(n)
}
Err(e) => Err(e),
}
}
fn write(this: &Self, _file: &File, data: &mut impl IoBufferReader, _offset: u64) -> Result<usize> {
let kbuf = data.read_all()?;
this.buf.write(&kbuf)
}
}
impl kernel::Module for RustChrdev {
fn init(module: &'static ThisModule) -> Result<Self> {
pr_info!("rust_chrdev: module loaded\n");
let mut reg = module.chrdev_region::<DeviceContext>(0, 1, "rust_chrdev")?;
reg.try_set_fops::<DeviceContext>()?;
Ok(Self {
_dev: reg.registration().device(),
})
}
}
impl Drop for RustChrdev {
fn drop(&mut self) {
pr_info!("rust_chrdev: module unloaded\n");
}
}
2.5 关键模式解析
┌────────────────────────────────────────────────────────┐
│ Rust 内核模块五大核心模式 │
├────────────────────────────────────────────────────────┤
│ 1. module! 宏 ── 声明模块元数据(替代 C 的 │
│ module_init/module_exit) │
│ │
│ 2. trait + #[vtable] ── 替代 C 的 │
│ struct file_operations 函数指针表, │
│ 编译期检查所有函数签名 │
│ │
│ 3. Box<T> / Arc<T> ── 替代 kzalloc / kref, │
│ 所有权语义保证释放不遗漏 │
│ │
│ 4. Mutex<T> / SpinLock<T> ── 编译期借用检查, │
│ 防止忘记释放锁导致死锁 │
│ │
│ 5. Result<T> + ? ── 替代 C 的 int 返回值 + goto err, │
│ 错误路径强制处理,不可能"忘记检查" │
└────────────────────────────────────────────────────────┘
三、Rust ↔ C 交互机制深度剖析
3.1 bindings 自动生成流水线
# 简化版 bindgen 流水线
# 1. 扫描编译后的 C 头文件 (include/generated/rfl_bundle.h)
# 2. 提取 struct, enum, function prototype, typedef
# 3. 通过 rust/build.rs → bindgen CLI 生成完整 FFI 代码
# 4. 输出到 rust/bindings/bindings_generated.rs
#
# 关键:bindgen 按 "allowlist" 过滤,仅暴露 kernel/public
# 目录中标记 #[kernel::export] 的 C 符号
Rust 调用 C 函数必须使用 unsafe 块。kernel crate 将所有底层 C 调用封装在安全抽象内部,对整个驱动层保证零 unsafe 调用——除必须使用 raw 指针的场景(如 MMIO 直接读写)。
3.2 类型边界对照表
| C 类型 | Rust 对应 | 语义差异 |
|---|---|---|
void * |
*mut core::ffi::c_void |
不可自动解引用,需 as 转换 |
struct device * |
&Device |
编译期借用检查替代 NULL 检查 |
int (*func)(...) |
unsafe extern "C" fn(...) |
回调需用 unsafe |
char buf[N] |
[u8; N] |
编译期定长,无隐式越界 |
struct list_head |
kernel::list::List |
侵入式链表安全封装 |
spinlock_t |
kernel::sync::SpinLock |
锁自动 unlock on Drop |
kmalloc(...) |
kernel::alloc::alloc |
包装为 Box/Arc,自动释放 |
3.3 Opaque 类型模式
C 内核大量使用前置声明 + 函数操作的模式(如 struct file 只能通过 fget/fput 操作)。Rust 通过 Opaque<T> 类型表达:
// kernel/src/fs/file.rs (简化)
/// Opaque<bindings::file> 表示"我们知道有这样一个 C struct,
/// 但我们不直接访问其字段,仅通过 bindings 函数操作"
pub struct File(Opaque<bindings::file>);
impl File {
/// 安全封装:C 的 fget(fd) → Result<&File>
pub fn get_from_fd(fd: i32) -> Result<&File> {
// SAFETY: fd 有效性由当前进程 context 保证
let f = unsafe { bindings::fget(fd as u32) };
if f.is_null() {
return Err(EBADF);
}
Ok(unsafe { &*f.cast::<File>() })
}
}
这是 Rust 内核代码中最核心的封装技巧:Opaque
四、实战:使用 eBPF + Rust 编写可观测性模块
下面展示一个更高级的实战场景:用 Rust 内核模块结合 eBPF 进行系统调用轨迹追踪——这是纯 C 内核开发中极其繁琐的工作。
4.1 架构概览
┌──────────────────────────────────────────┐
│ User Space │
│ ┌────────────────────────────────────┐ │
│ │ cat /sys/kernel/rust_trace/records │ │
│ └────────────────────────────────────┘ │
└──────────────────┬───────────────────────┘
│ rfl syscall
┌──────────────────▼───────────────────────┐
│ Kernel Space ── Rust Module │
│ ┌─────────────┐ ┌──────────────────┐ │
│ │ sys_call_ │ │ Rust Trace │ │
│ │ table hook │───►│ Ring Buffer │ │
│ │ (kprobe) │ │ (kptr -> user) │ │
│ └─────────────┘ └────────┬─────────┘ │
│ │ │
│ ┌───────────────────────────▼──────────┐ │
│ │ eBPF MAP: syscall_stats[pid]→count │ │
│ └──────────────────────────────────────┘ │
└──────────────────────────────────────────┘
4.2 Rust 内核模块代码
use kernel::prelude::*;
use kernel::bpf::*; // Rust-For-Linux BPF 支持
use kernel::perf_event::*;
use kernel::tracing::*;
module! {
type: RustTracer,
name: "rust_tracer",
author: "ybb",
description: "Rust + eBPF syscall tracer demo",
license: "GPL",
}
struct RustTracer {
_tracepoint: Box<Tracepoint<fn(u64, usize)>>,
_bpf_prog: BpfProg,
}
impl kernel::Module for RustTracer {
fn init(module: &'static ThisModule) -> Result<Self> {
pr_info!("rust_tracer: attaching to sys_enter...\n");
// 注册 tracepoint — 比 C 的 tracepoint_probe_register
// 无需手动管理 probe_list 和 refcount
let tracepoint = Tracepoint::new("sys_enter", |id: u64, regs: usize| {
// 原子计数递增 — 编译期保证不被并发修改
let count = GLOBAL_SYSCALL_COUNT.fetch_add(1, Ordering::Relaxed);
if count % 10000 == 0 {
pr_info!("rust_tracer: {} syscalls traced\n", count);
}
})?;
// 加载 eBPF 程序到 BPF map:统计每个 PID 的 syscall 频率
let bpf_prog = BpfProg::load(BpfProgType::TRACEPOINT,
include_bytes!("../bpf/syscall_counter.o"))?;
Ok(Self {
_tracepoint: Box::try_new(tracepoint)?,
_bpf_prog: bpf_prog,
})
}
}
impl Drop for RustTracer {
fn drop(&mut self) {
pr_info!("rust_tracer: detached. total syscalls: {}\n",
GLOBAL_SYSCALL_COUNT.load(Ordering::Relaxed));
}
}
这段代码展示了 Rust 内核模块编写 eBPF 辅助工具的简洁性:无需手动管理 probe_list 锁、无需处理 refcount、Tracepoint 的 Drop 实现自动取消注册。
五、内存管理与 C 的深层差异
5.1 GFP Kernel Flag 映射
Rust 中内存分配有两种方式:
// 方式一:Box(默认 GFP_KERNEL)
let p = Box::try_new(MyStruct { ... })?;
// 方式二:显式指定分配器行为
use kernel::alloc::{Flags, GFP_KERNEL, GFP_ATOMIC, GFP_DMA32};
let p = Box::try_new_flags(MyStruct { ... }, GFP_KERNEL)?;
// 互斥上下文(如中断 handler)必须用 GFP_ATOMIC
fn irq_handler(...) {
// Compiler error if you forget GFP_ATOMIC in atomic context
let buf = Box::try_new_flags([0u8; 256], GFP_ATOMIC)?;
}
5.2 地址空间隔离的安全保证
C 内核驱动中最常见的 bug 之一是将内核地址泄露给用户态。Rust 通过类型系统区分:
// IoBufferWriter 只能写入用户态虚拟地址
// IoBufferReader 只能从用户态虚拟地址读取
// 试图将 [u8] 直接写入用户空间 → 编译错误
fn write(this: &Self, _file: &File, data: &mut impl IoBufferWriter, _offset: u64) -> Result<usize> {
let kbuf = kernel::alloc::alloc![0u8; 4096];
// data.write_slice(&kbuf[..n]) ← OK: 从 kernel slice → user
// data.write_slice(some_kernel_struct_bytes) ← OK
// copy_to_user(user_dst, &kbuf, n) ← Not available! 类型系统
// 阻止了你
}
这是 Rust 最本质的安全改进:不是运行时检查,而是通过 Reader/Writer trait 在编译期实现了 copy_from_user / copy_to_user 的类型安全封装。
六、发布流程:将 Rust 模块合并到内核主线
6.1 Checklist
Rust 内核模块合入主线八步 Checklist:
────────────────────────────────────────────
☐ 1. 代码通过 rustfmt + clippy 检查
☐ 2. 所有 pub 函数/类型有 /// 文档注释
☐ 3. 所有 unsafe 块有 SAFETY 注释
☐ 4. MAINTAINERS 文件中注册 Rust 子模块负责人
☐ 5. CI 通过 KERNEL_CI + rust-test (kunit for Rust)
☐ 6. 与 C 调用方的跨语言测试覆盖率 > 80%
☐ 7. 提交到 linux-next 合并窗口并待满 2 周
☐ 8. 获得 Rust-For-Linux 维护者 (Miguel Ojeda) Reviewed-by
6.2 当前已合入的 Rust 模块(截至 2024.10)
| 模块 | 提交版本 | 功能 |
|---|---|---|
rust/net/phy |
6.1 | PHY 设备驱动框架 |
rust/drivers/net/ |
6.3 | Realtek/ASIX 网卡驱动 |
rust/fs/ |
6.4 | 文件系统 V4L2 Nova GPU |
rust/drivers/gpu/nova |
6.10 | NVIDIA GPU 驱动(社区推动) |
rust/kernel/ebpf |
6.12 | BPF 辅助函数 Rust 封装 |
rust/crypto/ |
6.13 | 内核加密 API Rust 封装 |
七、工程实践建议
7.1 何时应该使用 Rust
- 新驱动开发:没有 C 兼容包袱,Rust 是首选
- 高安全性场景:网络协议栈、文件系统、加密边界
- 并发复杂度高:多锁嵌套 + 多个共享状态的场景,Rust 编译器能检测隐含的死锁
- 协议解析器:处理外来数据时 eliminates buffer overflow 风险
7.2 何时不适合使用
- 已有成熟 C 子系统:如调度器 CFS、内存管理 buddy system,Rust 化收益太低
- 需要 inline asm 的场景:Rust 内核支持 asm! 但 API 仍在演进
- 极端性能关键路径:Rust 编译器的借用检查有时引入额外的 Arc 引用计数
7.3 调试技巧
# QEMU + GDB 调试 Rust 内核模块
$ qemu-system-x86_64 -kernel vmlinux -s -S
$ gdb vmlinux
(gdb) target remote :1234
# 注意:Rust 支持 DWARF debug info,设置断点使用 fn 名
(gdb) b rust_chrdev::DeviceContext::read
(gdb) c
# 命中断点后 info locals 可读 Rust 变量
# 查看 Rust 符号表(与 C modpost 共同管理)
__rust_modules_start / __rust_modules_end 标记 Rust 模块区域
MODULE_INFO(rust, "enabled") 标记模块支持 Rust
Rust DWARF 支持处于持续改进中,最新内核 6.10+ 已能正确显示绝大多数 Rust 类型。
结语
Rust-For-Linux 不是"用 Rust 重写内核"——Linus 本人已明确表示 C 不会退出内核,而是长期共存。真正的价值在于:给编写内核驱动的开发者提供一个不可能写出内存安全漏洞和并发数据竞争的选择。
随着 Nova GPU 驱动、更多网络 PHY 驱动以及 io_uring Rust 封装的逐步落地,预计 2025-2026 年 Linux 内核中 Rust 模块数量将翻倍增长。对于系统工程师、内核开发者来说,提前掌握 Rust 内核编程范式,不仅是技能储备,更是向更高质量代码迁移的必经之路。
ybb.press | 2026-10-01 | 字数约 2800

发表评论 取消回复