上周在翻阅 Linux kernel 7.1 RC2 的 patch 时,我注意到一个有意思的现象:内核中的 Rust 代码充满了 unsafe 块。这很奇怪——引进 Rust 难道不是为了解决 C 的内存安全问题吗?为什么引入了一个以安全著称的语言,却在内核中大量使用它的「不安全」特性?
这背后是一个真实的工程权衡:内核对零开销抽象的执念 vs 内存安全带来的运行时开销。
问题的本质
Linux 内核引进 Rust,官方说法是「improve safety, security, and maintainability」。但这里有一个根本性的冲突:
- Rust 的安全模型依赖于运行时检查——引用必须有效,数值必须在范围内。这些检查在用户态是零开销的,因为 panic 可以直接终止进程。
- 内核不能 panic——内核 panic 意味着整个系统挂掉。你不能在内核里抛出一个异常然后让进程退出。
这个冲突导致了一个工程困境:Rust 的很多安全特性在内核中无法直接使用。
被放弃的方案
方案一:完全依赖 Rust 的安全特性
这意味着在内核中放弃 panic 策略,改为返回 Result。但内核的调用深度是个问题——如果底层函数返回 Result,上层所有调用它的地方都要 match 处理错误。一层传一层,最终整个内核代码充满 ? 和 unwrap(),这不现实。
方案二:将内核代码完全重写为 Rust
完全没有可行性。几千万行 C 代码,不可能一夜之间重写。而且内核社区也不可能接受这种风险。
方案三:在用户态模拟内核环境
有人提议在用户态先验证 Rust 的安全性,但这忽略了内核的关键约束:没有 glibc、没有文件系统抽象、只能通过系统调用交互。
真正的设计决策
Linux 内核的选择是:在 Rust 代码中大量使用 unsafe,同时通过代码审查和静态分析工具来保证安全。
看一下内核中 Rust 驱动的一个典型模式:
// drivers/net/ethernet/example.rs (简化示意)pub struct Device{ ptr:*mut hw_descriptor,// 裸指针}impl Device{pub fn new()->Self{ // 直接分配物理连续的 DMA 内存 // 这个操作只能用 unsafe let layout = Layout::new::<hw_descriptor>(); let ptr=unsafe{alloc(layout)}as*mut_; Device{ptr}}pub fn tx<'a>(&'amutself,data:&'a[u8])->&'amut[u8]{ // 这里的生命周期标注是关键 // 编译器要求 tx 的返回值必须和 data 同一作用域 // 这是 Rust 在内核中唯一强制的东西——生命周期 unsafe{ copy_nonoverlapping(data.as_ptr(),self.ptr.as_mut().unwrap().data.as_mut_ptr(),data.len()); } // 返回的 buffer 必须在硬件处理完后才能释放 // 编译器帮你保证了这一点 unsafe{slice::from_raw_parts_mut(self.ptr.as_mut().unwrap().data.as_mut_ptr(),data.len())}}}
这里的核心洞察是:Linux 内核引进 Rust,不是为了借用检查(borrow checker),而是为了生命周期检查( lifetimes)。
在内核中,很多资源的有效性不是由 Rust 的引用计数管理的,而是由硬件 DMA 环、中断处理时机决定的。Rust 的生命周期系统可以静态保证「这块内存在硬件处理完之前不会被释放」,这是 C 做不到的。
反直觉的地方
你可能以为 Rust 解决了内核的内存安全问题。
实际上,Rust 在内核中的核心价值不是内存安全,而是生命周期的静态证明。
内存安全在内核中仍然需要靠开发者手动保证——该用 unsafe 的地方还是得用 unsafe。但 Rust 的生命周期系统可以静态证明「这个 skb 在被 DMA 处理完之前不会被 free」,这在内核网络栈中是一个巨大的进步。
这就像 C++ 的 RAII 在内核中早已广泛使用,但 Rust 的生命周期是编译时静态检查,运行时零开销。
边界条件:什么时候这个设计会失效
- 中断处理上下文——Rust 的生命周期检查基于函数调用栈,但中断处理不在正常的调用栈中。在硬中断上下文中,Rust 的任何安全检查都无法工作。
- 实时性要求极高的地方——内核的 RT 部分(PREEMPT_RT)需要更精细的锁控制,Rust 的 borrow checker 无法表达「这个锁必须在中断处理中释放」这种约束。
- 与旧 C 代码的交互——在内核混合编程中,Rust 和 C 的边界是一个脆弱的点。unsafe 关键字只是声明「这里我来保证安全」,但如果 C 代码破坏了 Rust 的假设,编译期无法检测。
一句话带走
Linux 内核引进 Rust,不是为了借用检查,而是为了用生命周期系统静态证明「硬件还在使用这块内存时,Rust 不会提前释放它」——这是 C 做不到的。
原文:[Link to original context]