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 系统,核心机制包括:
- rustc 与 gcc 协同编译——内核构建系统同时调用
rustc和gcc,分别编译.rs和.c文件,最终由ld链接为统一的内核镜像 - 自定义 target spec——使用
x86_64-unknown-none或aarch64-unknown-none等no_stdtarget,并扩展内核特定的 ABI 约定 - bindgen 自动生成绑定——通过
bindgen从内核头文件自动生成 Rust FFI 绑定,保持与 C 内核 API 的同步 - ** 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_testResult::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 当前局限
- 内联汇编——
asm!宏在内核环境受限,部分场景仍需 C 桥接 - 链接器脚本——Rust 无法直接定义自定义 ELF section,复杂模块布局依赖 C 辅助
- const 泛型——部分内核数据结构需要复杂的 const generic,编译期检查耗时
- unsafe 负担——FFI 调用链中的 unsafe 需要人工审计,工具链辅助有限
- 社区分裂风险——极少数资深维护者持抵制态度,代码审查摩擦客观存在
7.3 学习资源
- Rust for Linux 官方文档
- rust-bindgen
rust/目录下的示例代码(rust/samples/、rust/net/)- The Rust Reference(理解 UB 边界)
八、结语
Rust for Linux 不是"用 Rust 重写内核",而是一种渐进的安全策略。对于驱动开发者,它提供了编译期的内存安全保证;对于维护者,减少了 review 心智负担;对于最终用户,降低了漏洞利用的可能性。
未来 3-5 年,随着 Rust 编译器在内核编译流程中愈发成熟、驱动生态日趋完善,Rust 代码将成为 Linux 内核不可分割的一部分。对于后端工程师而言,掌握 Rust 内核开发技能,等于握住了一扇通往底层系统编程安全化的大门。
关键词:Rust for Linux、内核模块、驱动开发、内存安全、所有权系统、字符设备 分类:Linux Kernel、Rust、系统编程

发表评论 取消回复