Rust写Linux驱动:Linus观望背后,技术可行但生态早期
1. 这件事到底在讨论什么Rust 要进 Linux 内核这件事其实已经吵了好几年了。从 2022 年 Linux 6.1 首次合并 Rust 基础设施开始到后面几个版本陆续有 Rust 写的驱动模块进入主线这个话题每隔一段时间就会被翻出来重新讨论一轮。而这次被推到风口浪尖的是有人直接问 Linus Torvalds用 Rust 写 Linux 驱动你到底怎么看他的回答大意是——感兴趣但保持观望。这句话听起来很官方但如果你真的写过内核模块或者哪怕只是在树莓派上编译过一个 out-of-tree 的驱动你就会明白这句话背后的分量。Linus 不是那种会随便给新技术站台的人他当年对 C 进内核的态度是直接骂回去的。所以感兴趣这三个字从他嘴里说出来已经算是相当高的评价了。而保持观望则说明他还没有被说服到愿意为 Rust 在内核里的未来背书。这篇文章我想聊的不是新闻本身而是这件事背后那一堆实际问题Rust 写驱动到底难在哪、现在能写到什么程度、一个普通的嵌入式或者驱动开发者要不要现在就跟进、如果跟进的话从哪下手。我会尽量把我知道的、踩过的、以及从社区里看到的东西都摊开讲。适合的读者是有 C 语言基础、对 Linux 内核或者驱动开发有兴趣、正在犹豫要不要学 Rust 的那批人。如果你完全是新手也能看我会尽量把背景补全。先说结论免得你看到一半才发现方向不对Rust 写 Linux 驱动在技术上已经可行在工程上还不成熟在生态上处于早期。Linus 的观望态度本质上是对这个现状的准确描述而不是对 Rust 语言的否定。2. 为什么 Rust 进内核这件事这么敏感2.1 内核开发几十年来的默认前提Linux 内核从 1991 年到现在代码量超过三千万行全部是 C 语言加上少量汇编。这不是偶然而是因为 C 语言有几个内核开发离不开的特性没有运行时、没有垃圾回收、内存布局完全可控、可以直接操作硬件地址。内核代码跑在最高权限级别任何一次内存越界都可能直接导致整机崩溃或者安全漏洞所以可控比安全更重要——至少在过去几十年里是这样。但 C 语言的问题也很明显。内核里最常见的 bug 类型是什么根据多年的统计数据内存安全问题use-after-free、buffer overflow、null pointer dereference占了内核 CVE 的很大一部分。这些问题在 C 里靠代码审查和静态分析工具来防但防不胜防。一个驱动开发者写了几百行代码里面有一个指针在错误的分支上被释放了可能几个月都不会被发现直到有人构造出攻击载荷。Rust 的核心卖点正好打在这个痛点上所有权系统和借用检查器在编译期就能消除大部分内存安全问题。不需要垃圾回收不需要运行时编译出来的机器码和 C 差不多。这就是为什么有人觉得 Rust 是内核开发的天选之子。2.2 但内核不是普通项目问题在于内核不是一个可以从零开始的新项目。它有三十年的历史包袱、有几千个活跃贡献者、有一整套根深蒂固的开发流程和文化。Rust 要进来不是写个新驱动这么简单而是要解决一系列结构性问题。第一个问题是ABI 兼容。内核内部的函数调用约定、数据结构布局、内存分配接口全都是围绕 C 设计的。Rust 要和这些 C 代码互操作就必须通过 FFI外部函数接口来桥接。桥接本身不难难的是桥接之后 Rust 那套安全保证还能剩下多少。你在 Rust 里调用一个 C 函数那个函数返回的指针是不是有效的、生命周期有多长Rust 编译器是不知道的。你得手动用unsafe块把这些东西包起来而unsafe块里的代码安全性就回到了 C 的水平。第二个问题是维护成本。内核维护者最怕的是什么是引入一种新技术之后维护者自己看不懂。如果 Rust 驱动出了问题而维护者只会 C那这个驱动就成了没人敢碰的黑盒。Linus 的观望很大程度上就是在看这个问题能不能解决——不是技术能不能跑通而是社区能不能消化。第三个问题是工具链和构建系统。内核用的是 Kbuild一套基于 Makefile 的构建系统。Rust 有自己的 Cargo 和 rustc。把这两套东西整合到一起让make menuconfig能配置 Rust 支持、让交叉编译能正常工作、让不同架构都能编译这些工作量比想象中大得多。而且 Rust 编译器本身的版本更新很快内核要不要跟着更新、怎么保证稳定性都是问题。2.3 Linus 的态度其实很一致如果你回顾 Linus 对新技术的一贯态度会发现他从来不是技术先进就支持而是能解决问题且不引入更大问题才支持。当年他对 C 的拒绝理由是 C 的异常处理和模板会让内核代码变得不可预测。他对 Rust 的感兴趣但观望逻辑是一样的Rust 确实能解决内存安全问题但它引入的复杂性FFI、工具链、学习成本、社区分裂是不是值得还需要时间验证。这个态度对普通开发者的启示是不要因为 Linus 说感兴趣就一头扎进去也不要因为他说观望就觉得 Rust 没戏。真正该关注的是具体的技术进展——哪些驱动已经用 Rust 写了、效果怎么样、踩了哪些坑。3. Rust 写驱动的技术现状拆解3.1 目前能写到什么程度截至我写这篇文章的时候Rust 在内核里的支持已经覆盖了这些场景字符设备驱动可以注册字符设备、实现 file_operations 里的 read/write/ioctl 等回调。这是最基础也最常用的驱动类型。平台驱动可以写 platform_driver处理 probe/remove和设备树配合。PCI 驱动有 Rust 的 PCI 抽象层可以枚举设备、映射 BAR 空间。网络设备有 Rust 写的网络驱动示例能注册 net_device。文件系统有 Rust 写的只读文件系统示例比如 tarfs证明 VFS 层也能对接。但要注意这些能写和生产可用之间还有距离。很多抽象层还在演进API 可能在下个版本就变了。而且 Rust 内核代码目前主要支持 x86_64 和 aarch64其他架构的支持还在补。3.2 一个最小的 Rust 字符设备驱动长什么样我给你看一个简化过的骨架让你感受一下 Rust 内核代码的风格。这不是完整可编译的代码但结构是真实的// 简化示例展示结构 use kernel::prelude::*; use kernel::file; use kernel::io_buffer::{IoBufferReader, IoBufferWriter}; module! { type: MyCharDev, name: my_char_dev, author: Your Name, description: A minimal Rust char driver, license: GPL, } struct MyCharDev { _dev: PinBoxkernel::chrdev::Registration, } #[vtable] impl file::Operations for MyCharDev { type OpenData (); type Data (); fn open(_: (), _file: file::File) - Result() { pr_info!(device opened\n); Ok(()) } fn read( _: (), _file: file::File, writer: mut impl IoBufferWriter, _offset: u64, ) - Resultusize { let msg bhello from rust\n; writer.write_slice(msg)?; Ok(msg.len()) } } impl kernel::Module for MyCharDev { fn init(_module: static ThisModule) - ResultSelf { pr_info!(my_char_dev loaded\n); let dev kernel::chrdev::Registration::new_pinned( cmy_char_dev, 0, None )?; Ok(MyCharDev { _dev: dev }) } }对比一下 C 版本的字符设备驱动你会发现几个明显差异第一没有手动管理 file_operations 结构体的初始化。C 里你要定义一个struct file_operations然后逐个字段赋值漏掉一个字段可能就是个 bug。Rust 里用 trait 实现编译器会检查你有没有实现必要的方法。第二错误处理是显式的。Rust 用ResultT, E强制你处理错误而 C 里经常是返回一个负数调用者忘了检查就出问题。第三生命周期和所有权是编译期检查的。比如Registration被PinBox包起来保证它不会被移动这在 C 里要靠开发者自己记住。但你也看到了代码里还是有unsafe的痕迹虽然这个例子里没直接出现而且kernelcrate 的 API 和标准库完全不一样学习曲线不低。3.3 抽象层的设计难点Rust 内核抽象层最难的地方是怎么把 C 的不安全接口包装成 Rust 的安全接口。举个例子内核里的copy_to_user函数它接收一个用户空间指针和一个内核空间指针把数据从内核拷到用户空间。这个操作可能失败用户空间地址无效也可能被信号打断。在 C 里你调用它检查返回值完事。在 Rust 里你要设计一个接口让调用者不能传错指针类型、不能忽略错误、不能在不该调用的上下文里调用。这就涉及到类型系统的设计——比如用 newtype 区分用户空间指针和内核空间指针用Result强制处理错误用生命周期标注保证缓冲区在调用期间有效。这些设计工作量大而且一旦设计得不好后面改起来很痛苦因为所有用这个抽象层的驱动都要跟着改。这就是为什么 Rust 内核支持推进得慢——不是写不出来而是要把接口设计对。4. 如果你现在想上手该怎么走4.1 先决条件C 和内核基础不能跳过我见过一些人听说 Rust 能写驱动就直接去学 Rust 内核编程结果卡在半路。原因是他们跳过了两个必要的基础C 语言和 Linux 内核的基本概念。为什么 C 不能跳过因为内核里大量的代码、文档、邮件列表讨论都是 C 的。你要理解一个 Rust 抽象层为什么这么设计往往需要看它对应的 C 实现。而且调试的时候你可能会遇到需要读 C 代码才能定位的问题。为什么内核基础不能跳过因为驱动开发的核心不是语言而是理解内核提供的机制设备模型、中断处理、内存映射、并发控制、电源管理。这些概念在 C 和 Rust 里是一样的只是表达方式不同。你不懂这些换什么语言都写不好驱动。我的建议是如果你已经有 C 基础先花时间读一遍《Linux Device Drivers》第三版虽然有点老但概念不过时然后在树莓派或者虚拟机上写一个最简单的 C 字符设备驱动跑通 insmod/rmmod/read/write 的流程。这一步走完你再看 Rust 驱动代码会顺畅很多。4.2 Rust 语言本身要学到什么程度不需要把 Rust 学到精通再开始但有几个概念必须扎实所有权和借用这是 Rust 的核心不理解这个内核代码里的生命周期标注你根本看不懂。trait 和泛型内核抽象层大量使用 trait 来定义接口。Result和错误处理内核代码里到处都是Result。unsafe的含义知道什么时候必须用 unsafe以及 unsafe 块里你要自己保证什么。Pin和自引用结构内核里很多结构不能被移动Pin 就是干这个的。学习路径上我建议先在用户空间写几个小项目练手。比如用 Rust 写一个简单的命令行工具、写一个 TCP 服务器、用sqlx连一下 MySQL 感受一下异步和错误处理。这些练手项目不需要多复杂目的是让 Rust 的语法和思维方式变成肌肉记忆。4.3 环境搭建从编译一个 Rust 内核模块开始真正开始内核 Rust 开发你需要一个 Linux 开发环境物理机或者虚拟机都行Ubuntu 22.04 或更新版本比较省事。不建议用 WSL因为内核模块编译和加载在 WSL 里限制很多。Rust 工具链通过 rustup 安装但要注意内核需要的 Rust 版本可能和最新稳定版不一样要按内核文档指定的版本装。内核源码从 kernel.org 下载或者用你发行版的内核源码包。建议用主线较新的版本因为 Rust 支持在新版本里更完整。编译依赖flex、bison、libssl-dev、libelf-dev、bc这些是编译内核的常规依赖一个都不能少。Rust 相关组件rustc、bindgen、rustfmt、clippy以及内核指定的rust-src。配置内核的时候要打开CONFIG_RUSTy以及相关的CONFIG_RUST_IS_AVAILABLE。然后make LLVM1编译。第一次编译会很久取决于你的机器性能半小时到两小时都正常。编译出 Rust 模块之后用insmod加载dmesg看输出。如果看到你写的pr_info!打印出来了恭喜环境通了。注意加载自定义内核模块需要 root 权限而且如果模块有问题可能导致内核 panic。建议在虚拟机里做实验别拿主力工作机冒险。4.4 从哪个驱动类型入手我的建议是从字符设备驱动入手。原因有几个字符设备是最简单的驱动模型不涉及复杂的硬件交互。内核里已经有 Rust 字符设备的示例代码可以对照着改。字符设备的测试很方便用cat、echo、dd就能验证读写。不需要真实硬件虚拟设备就能跑通整个流程。等你把字符设备写熟了再往平台驱动、中断处理、DMA 这些方向走。每一步都确保你能解释清楚为什么这么写而不是照抄示例。5. 实际开发中会踩的坑5.1 编译期的坑坑一Rust 版本和内核版本不匹配。内核的 Rust 支持对 rustc 版本有要求用太新的或者太旧的都可能编译失败。解决办法是看内核源码里的Documentation/rust/quick-start.rst里面会写清楚需要的版本。坑二bindgen 生成的绑定不对。内核用 bindgen 从 C 头文件生成 Rust 绑定但有时候生成的代码会因为头文件里的宏或者复杂类型而出错。遇到这种情况通常需要调整 bindgen 的参数或者手动补一些绑定。坑三交叉编译配置复杂。如果你要在 ARM 开发板上跑交叉编译 Rust 内核模块需要配置 target、linker、sysroot 等一堆东西。建议先用 x86_64 本机编译跑通再折腾交叉编译。5.2 运行期的坑坑四panic 直接导致内核崩溃。Rust 的 panic 在用户空间是程序退出在内核里就是内核崩溃。所以内核 Rust 代码里要尽量避免会 panic 的操作比如数组越界、unwrap 一个 None。内核提供了try_*系列的方法来替代会 panic 的操作。坑五并发问题依然存在。Rust 的所有权系统能防数据竞争但前提是你用对了同步原语。如果你在 Rust 里用unsafe绕过了借用检查或者用了不正确的锁照样会出并发 bug。Rust 不是银弹它只是把一类问题变成了编译错误另一类问题还是要靠你自己。坑六内存分配失败的处理。内核里内存分配可能失败尤其是用 GFP_ATOMIC 的时候Rust 的Box::try_new会返回Result你必须处理。如果直接unwrap在内存紧张的时候就是内核 panic。5.3 常见问题速查表问题现象可能原因排查方向编译报错找不到 kernel crate内核没开 CONFIG_RUST检查 .config 里的 RUST 相关配置insmod 报 Invalid module format内核版本和模块编译版本不一致用modinfo看 vermagic重新编译dmesg 没有输出pr_info 级别被过滤检查dmesg -n或者/proc/sys/kernel/printk加载后系统卡死驱动里有死循环或者死锁用虚拟机调试加 printk 定位Rust 代码编译通过但链接失败缺少符号或者 ABI 不匹配检查 Cargo.toml 和内核 Makefile 的配置提示调试内核模块最有效的工具还是 printkRust 里是 pr_info/pr_err 等宏。别指望 gdb 能像调试用户空间程序那么顺畅内核调试的门槛高很多。6. 这件事对普通开发者的实际影响6.1 现在要不要学 Rust 驱动我的看法是如果你已经在做驱动开发可以开始关注但不必立刻转型。原因是 Rust 驱动在内核里的占比还很小短期内不会取代 C。你现在花大力气学的 Rust 内核 API可能过两个版本就变了。但如果你的工作涉及安全敏感的场景比如车载、工业控制Rust 的价值会更快体现出来值得提前布局。如果你是学生或者刚入行的开发者我的建议不一样可以学而且越早越好。因为你现在学的东西三五年后可能正好是市场需要的。而且 Rust 的思维方式所有权、生命周期、错误处理对你的编程能力提升是有帮助的即使最后不用它写驱动这些概念也能让你写 C 代码时更谨慎。6.2 对嵌入式行业的影响嵌入式领域对 Rust 的兴趣其实比内核社区更早。原因很实际嵌入式设备一旦出货固件更新成本很高一个内存安全漏洞可能导致召回。Rust 能在编译期消除这类问题对厂商来说是有经济价值的。现在已经有一些 RTOS 和嵌入式框架支持 Rust比如 embassy、RTIC 这些。它们和 Linux 驱动不是一回事但学习曲线有重叠。如果你做嵌入式可以先从这些入手感受一下 Rust 在裸机或者 RTOS 环境下的写法再决定要不要深入 Linux 内核方向。6.3 生态现状工具和文档还差得远必须承认Rust 内核开发的生态还很早期。C 驱动开发有几十年的文档、书籍、论坛帖子、Stack Overflow 问答。Rust 内核开发呢官方文档就那几个 rst 文件示例代码有限遇到问题基本只能去邮件列表或者 Zulip 频道问。而且工具链也不完善。C 有 sparse、coccinelle、smatch 这些静态分析工具Rust 这边对应的工具还在建设中。调试工具、性能分析工具、内存检查工具都还在补。这不是说 Rust 不行而是说现在进入这个领域你要有当小白鼠的心理准备。你会遇到别人没遇到过的问题需要自己摸索解决。这对喜欢挑战的人是好事对想快速出活的人是障碍。7. 我个人的一些判断Linus 说感兴趣但观望我觉得这个态度至少还会持续两三年。判断依据是Rust 内核支持目前还处于证明可行性阶段没有到证明优越性阶段。什么叫证明优越性就是有一个用 Rust 写的驱动在性能、稳定性、安全性上明显优于同类的 C 驱动而且维护成本可接受。这样的案例目前还没有。但这不代表 Rust 没机会。我观察到几个积极信号一是内核社区对 Rust 的接受度在提高早期反对的声音在减弱二是大厂在投入资源Google、Microsoft、Amazon 都有工程师在做 Rust 内核相关工作三是硬件厂商开始关注因为他们的驱动经常是安全漏洞的重灾区。对个人开发者来说我的建议是保持关注适度投入。不用 all in但可以花 20% 的精力去了解。具体做法订阅 Rust for Linux 的邮件列表、偶尔看看内核 Rust 相关的 commit、在虚拟机里跑一跑示例代码。这样等风向明确的时候你能快速跟上而不是从零开始。最后分享一个我自己的体会学 Rust 写驱动这件事最大的收获可能不是会用 Rust 写驱动而是通过 Rust 的视角重新理解了 C 驱动里的那些坑。当你被借用检查器逼着去想这个指针的生命周期到底多长的时候你会发现自己以前写 C 代码时有多随意。这种思维方式的转变比学会一门语言更有价值。