这件事说出来很多人可能觉得夸张:Rust 从一门“实验性语言”,一路走到 Linux 内核的核心位置,而且不是停留在技术沙龙里的远景规划,是已经合并进主线、能编译、能加载、能真正跑驱动的现实状态。放在十年前,Linux 内核还是 C 的绝对主场,Linus 本人对 C++ 进内核的态度一直很冷淡,谁能想到一门新的系统语言会在内核里撕开一道口子。这篇文章我就从“Rust 为什么能进内核”讲起,拆一拆内核里 Rust 的设计思路,再带你把一个 Rust 内核模块从零编译、加载到验证的完整流程走一遍。想了解 Rust 在内核里到底干了什么的开发者,或者正在做嵌入式、内核相关工作的朋友,这篇应该能帮你省不少弯路的成本。
1. 从“实验性”到“核心语言”:这件事到底牛在哪
1.1 Rust 是怎么一步步走进内核的
先简单捋一遍时间线。Rust 在 2015 年发布 1.0 之后,很长一段时间里给人的印象是“有前景但还没看到杀手级应用”。2020 年前后,Rust for Linux 项目正式出现在内核邮件列表里,作者们给内核社区提交的是一整套基础设施补丁,而不是某个具体的驱动。这一步的意义很大,相当于先把“路”修好,后续的开发者才能在这条路上跑,而不是每个人都从零开始踩一遍 C 与 Rust 之间的互操作难题。
随后 Linux 6.1 版本合入了 Rust 基础支持,这是一个标志性节点。有了基础支持之后,新版本内核里开始不断出现 Rust 写成的抽象和示例模块,比如平台驱动抽象、PCI 驱动抽象、VirtIO 相关的尝试、Binder 相关工作的探索等等。当然,这些还不能说是 Rust 已经接管了内核,但“能在主线内核里用 Rust 写模块”这件事本身,已经从实验性质变成了被官方承认的能力。
这里有个细节值得说:内核社区接受一门新语言的门槛极高,不是“跑得通”就行,还要考虑长期维护、ABI 稳定性、工具链适配、开发者学习成本等一系列问题。Rust for Linux 项目能走到今天,靠的不是一口气提交大量 Rust 代码,而是先拿出可靠的安全抽象,再一点一点扩大覆盖范围。这个策略放在任何大型 C 项目里都有参考价值。
1.2 为什么偏偏是 Rust
这个问题背后的逻辑其实非常直白。Linux 内核大部分安全漏洞,追根溯源都来自 C 语言的内存安全问题:空指针解引用、缓冲区溢出、释放后使用、数据竞争……随便打开一份安全公告,几乎都能看到这些词。C 语言的哲学是“信任程序员”,由此带来的性能确实无可挑剔,但代价是人类在写复杂代码时总会犯某种错误,而且这类错误很难在测试阶段全部发现。
Rust 恰好踩中了这个痛点。它的所有权和借用检查机制,让很多内存安全问题在编译期就被拦截;它的 Send/Sync 约束,又把数据竞争的隐患提前暴露在类型系统里;而它的零成本抽象,让这些安全保障没有带来明显的运行时开销。对内核这种极度在意性能、又极度惧怕安全问题的场景来说,Rust 几乎是唯一能同时满足这两个需求的现实选择。
这里也顺便回应一个很多人会问的问题:为什么不是 C++ 或者 Go?C++ 虽然也在不断添加安全特性,但它的核心模型仍然允许任意指针操作,内存安全的保证无法在编译期强制落地;Go 有垃圾回收和相对厚重的运行时,在内核这种裸环境下非常别扭。Rust 是少数既没有运行时、又能在编译期给出硬性安全保证的语言。
为了更直观地对比,我把 C 和 Rust 在内核开发中的差异整理成了一个表:
| 对比维度 | C 语言 | Rust |
|---|---|---|
| 内存安全 | 靠人工审查和测试,容易遗漏 | 编译期强制检查,所有权模型兜底 |
| 并发安全 | 靠经验和规则约束 | Send/Sync 类型约束,交叉线程访问在编译期拦截 |
| 抽象成本 | 底层直白,但复杂逻辑容易失控 | 零成本抽象,高层的安全接口同样高效 |
| 运行时依赖 | 无 | 无,只用 core 和 alloc |
| 与内核 C 代码互操作 | 原生 | 通过 bindgen 生成 FFI 绑定,再包一层安全接口 |
| 工具链成熟度 | 极其成熟 | 仍在快速完善,版本对齐要求高 |
为什么不是 C++ 或者 Go,这个表格基本就能回答。可以说,Rust 没有替代 C,也不打算替代 C。它的定位是在内核里承担那些对内存安全和并发安全要求极高的部分,比如驱动、文件系统、协议栈相关代码,让最容易出错的代码由更严格的编译器来把关。这恰恰是它能被内核社区接受的关键原因。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 内核里的 Rust 长什么样:核心设计与实现思路
2.1 kernel crate:Rust 与内核 C 代码的桥梁
Rust 进内核不是直接把 Rust 代码塞进 C 世界就完事。内核里有一套专门的 crate 叫 kernel,它负责提供模块开发所需的抽象,包括 bindings、prelude、sync、alloc、error 等几大部分。bindings 是通过 bindgen 工具,从内核对 C 头文件自动生成的 FFI 绑定,让 Rust 代码能调用 C 导出的函数和类型;而 bindings 之上又包了一层更友好的安全接口,避免开发者直接碰裸指针。
内核里的 Rust 代码并不直接使用标准库的 std,而是使用 core 和 alloc。alloc 启用了全局分配器,并且把分配行为映射到内核的 kmalloc 等接口上。这一点很关键,因为内核内存分配有 GFP 标志体系,不经过正确的体系去分配内存,轻则性能异常,重则直接导致系统不稳定。Rust 模块里看似简单的 Box::new、Vec::new,底层实际上都走了内核自己的分配路径,而不是用户态的 glibc 分配器。
对一个写惯了用户态 Rust 的开发者来说,最需要适应的就是这个差异。用户态程序只管调用标准库,底层怎么分配内存不需要操心;内核 Rust 则一切都得向内核对齐,连 panic 行为、错误码映射、日志输出都要走内核自己的机制。这也是为什么内核里并不建议开发者大量手写 unsafe 代码,而是尽量复用 kernel crate 里已经封装好的抽象,把可能出错的地方限制在最小范围。
2.2 内存安全抽象背后的设计取舍
kernel crate 里有很多有意思的设计,都是围绕“让不安全代码尽量收敛在抽象内部”这个思路展开的。举个例子,它用 ARef 这类引用计数包装类型去管理带引用计数的内核对象,开发者拿到的都是受所有权约束的智能指针,而不用手动维护 kref 的引用和释放。再比如,Rust 的 Mutex 抽象对应内核的 mutex,它要求锁内保护的数据必须满足 Send 约束,一旦你把一个不能被安全跨线程访问的类型放进临界区,编译期就会报错。
错误处理上,内核 Rust 代码使用 Result 类型,错误码遵循内核的 errno 体系,比如 EINVAL、ENOMEM。这样 Rust 模块返回的错误能被上层 C 代码正确理解,FFI 边界的语义不会错乱。整个抽象层的目标是把“unsafe 代码块”压缩到最小范围:只有真正需要触碰硬件寄存器、操作裸指针的地方才用 unsafe,业务逻辑全部在安全 Rust 里完成。
这个“最小 unsafe”原则,听起来简单,做起来其实很难。因为内核的很多 C API 本身就不是安全接口,比如有些函数要求调用者持有特定锁,有些回调要求在特定上下文里执行,这些约束没法全靠 Rust 编译器识别。所以 kernel crate 的做法是:在抽象层里写清楚前置条件,再用注释和类型签名把这些条件固化下来,让普通模块开发者不需要深入了解 C 侧的细节也能安全调用。
2.3 Rust 能覆盖哪些内核子系统
从目前合入和正在推进的补丁看,Rust 在内核里覆盖面最广的领域是驱动开发,尤其是平台驱动、PCI 设备和网络设备这类“与硬件交互密集”的代码。除此之外,文件系统的部分抽象、VirtIO 相关探索、Android Binder 的 Rust 实现尝试,也都属于重点方向。调度器这方面,主流仍然是 C,像 CachyOS 这类发行版讨论的默认调度器、各种调度器优化,基本还是在 C 的体系里换算法,Rust 暂时没有直接染指内核核心调度路径。
不过 Rust 在更底层、更靠近硬件的方向,比如嵌入式领域的发展速度并不比内核慢。现在用 Rust 写 FreeRTOS 应用、在 CH32 这类 MCU 上用 Rust 做开发已经是不少团队的选择,嵌入式 Rust 生态里的 HAL 和驱动库早就不是玩具状态。你可以把内核里的 Rust 理解成一种“由点及面”的渗透:先在抽象的边界处建立安全地带,再逐步扩展到更多子系统。这个过程未必很快,但一旦某个抽象稳定下来,后续开发者就不会回去写 C 版本了。
还有一个方向值得一提,就是异步在内核驱动里的潜力。用户态 Rust 的 async 生态已经相当成熟,而内核驱动的很多场景天然适合异步模型,比如处理大量并发的 I/O 请求。如果未来 Rust 抽象能把异步运行时和安全接口结合起来,内核驱动开发的方式可能会迎来一轮真正的变化,不用再像现在这样靠 callback 和状态机把代码拆得七零八落。
3. 实操:把 Rust 写进内核模块的开发环境搭建
3.1 工具链准备与版本对齐
第一步是配环境。这里最容易踩的坑就是版本对齐:Rust 内核代码对 rustc 版本有严格要求,太老或者太新都可能编译失败。官方文档 Documentation/rust/quick-start.rst 里明确写了推荐的 rustc 版本和 bindgen 版本,建议直接用 rustup 安装指定工具链,然后加一个 rust-toolchain.toml 文件把版本锁死,避免某天 rustup update 把工具链升上去之后项目突然编不过。
需要装的组件包括 rust-src,这个组件给编译器提供 Rust 标准库的源码,内核的 build 系统在编译 Rust 代码时会用到。bindgen 则是从 C 头文件生成 Rust FFI 绑定的工具,内核里会调用它来处理内核对头文件。另外,由于 Rust 的编译过程依赖 LLVM 后端,建议统一用 LLVM=1 来编内核,避免 GCC 后端和 Rust 之间出现 ABI 或者内联汇编层面的奇怪问题。
实际开发时,我一般会在内核源码目录下放一个 rust-toolchain.toml,内容大致是:
toml复制[toolchain]
channel = "nightly-2023-06-01"
components = ["rust-src", "rustfmt", "clippy"]
这个 channel 的日期要和当前内核要求的版本匹配,具体看内核文档。使用 nightly 的原因很简单:Rust for Linux 目前还用了一批未稳定的特性,只有 nightly 工具链才支持。等到 Rust 官方把这些特性 stabilize 之后,未来可能就不需要 nightly 了,但短期之内日落版本还是别乱动。
3.2 最小 Rust 内核模块的完整实现
我平时验证环境时喜欢写一个最小模块,代码很短,但能覆盖模块加载和卸载的完整路径。Rust 内核模块目前最稳的做法是先放到内核源码树的 samples/rust 或 drivers 目录下面,这样 Kbuild 系统能正确处理依赖和链接关系。想直接从外部维护一个独立 crate 也不是不行,但 out-of-tree 支持目前不如 C 模块成熟,新手容易在链接阶段遇到一堆莫名其妙的问题。
目录结构可以参考内核自带的 rust_minimal 示例,核心代码就一个文件:
text复制samples/rust/
├── rust_minimal.rs
├── Makefile
└── Kconfig
rust_minimal.rs 的主体内容大致如下:
rust复制//! A minimal Rust kernel module.
use kernel::prelude::*;
module! {
type: RustMinimal,
name: "rust_minimal",
author: "Your Name",
description: "A minimal Rust kernel module",
license: "GPL",
}
struct RustMinimal;
impl kernel::Module for RustMinimal {
fn init(_module: &'static ThisModule) -> Result<Self> {
pr_info!("Rust minimal module loaded\n");
Ok(RustMinimal)
}
}
impl Drop for RustMinimal {
fn drop(&mut self) {
pr_info!("Rust minimal module unloaded\n");
}
}
这个代码里最关键的部分是 module! 宏。它负责生成内核模块需要的所有样板代码,包括 module_init、module_exit、MODULE_LICENSE 这些 C 内核模块必须的定义。pr_info! 对应 C 里的 printk(KERN_INFO ...),输出会进入 dmesg。
在 Feature 方面,不同版本的内核需要启用的 unstable feature 不一样。Kbuild 系统会自动处理一部分,但如果你通过 rust-analyzer 看代码时发现一堆错误,多半是编辑器没有正确加载内核的编译配置。内核目录下执行 make LLVM=1 rust-analyzer 可以生成 rust-project.json,然后用 VS Code 打开,代码补全和诊断就能正常工作了。
3.3 编译、加载与验证的完整流程
把写好的模块放进 samples/rust 之后,接下来就是配置内核。先在内核配置里打开 CONFIG_RUST 和对应的样本选项,这一步可以用 menuconfig:
bash复制make LLVM=1 menuconfig
在菜单里找到 General setup,把 Rust support 打开,然后在 Kernel hacking、Sample kernel code 下面找到对应的 Rust minimal module 示例项。保存退出之后,开始编译:
bash复制make LLVM=1 -j$(nproc)
如果你准备在虚拟机里跑,那编译完之后可以把整个内核镜像拷贝到虚拟机中启动。加载模块和验证输出很简单:
bash复制insmod rust_minimal.ko
rmmod rust_minimal
dmesg | tail
正常的话,dmesg 里应该能看到两行日志:一行是 "Rust minimal module loaded",另一行是 "Rust minimal module unloaded"。这两行日志出现,说明从工具链到编译流程再到模块加载机制,整条链路都是通的。
这里特别建议用 QEMU 或 VirtualBox 这类虚拟化环境来跑,原因很简单:内核模块崩溃会直接 oops,在虚拟机上不会影响宿主机,重置也快。实际开发中,为 Rust 模块单独开一个调试内核是很常见的做法,没必要在主力开发机上反复重启。
4. 常见问题与排查技巧实录
4.1 编译阶段的高频报错
先说我遇到最多的一个问题:error: couldn't read .../rust-src/lib/rustlib/src/rust/library/...,这个基本是 rust-src 组件没装,或者 rustup 没有把默认工具链切到内核要求的版本。解决办法是执行 rustup component add rust-src --toolchain <版本>,并且用 rustup override set 把当前目录固定到正确的工具链上。
第二个高频问题是 bindgen 报找不到 libclang.so,或者生成的绑定文件有大量错误。bindgen 依赖 libclang 来解析 C 头文件,你需要保证系统里有对应版本的 libclang,并且环境变量 LIBCLANG_PATH 指到了正确位置。版本太新的 libclang 也可能和内核要求的 bindgen 版本不搭,所以尽量按内核文档锁版本。
还有一个我经常在社区里看到的问题:error[E0433]: failed to resolve: could not find 'bindings' in 'kernel'。这类错误通常意味着你在编译一个外部独立项目,而不是在内核源码树内编译。Rust 内核模块目前对 out-of-tree 的支持不如 C 模块成熟,建议先放到内核源码树的 samples/rust 或 drivers 目录下,等环境完全跑通再考虑独立工程化。
4.2 运行时崩溃与调试思路
Rust 模块在内核里 panic 的结果和 C 模块出 BUG 一样,都是直接 oops。不过 Rust 的好处是很多错误在加载阶段就能暴露,比如模块里用 unwrap 处理了一个 Err,内核会打印 panic 信息和调用栈。看到这类信息不要慌,先用 pr_info/pr_err 把关键路径打点打上,缩小问题范围。
如果遇到的问题和 ABI 相关,比如模块加载时报 version magic 不匹配,那基本说明内核版本和模块编译环境不一致,重新用同一个内核源码树编译一次就好。比较难排查的是异步或并发场景下的问题,比如中断上下文里调用了可能睡眠的函数。Rust 的类型系统能拦住一部分,但拦截不了全部,所以在写内核代码时,还是要对上下文有清晰的判断。
这里分享一个我自己的排查习惯:凡是涉及指针或回调的代码,第一版先不追求优雅,直接把所有传入参数用 pr_info 打出来,打印得越详细越好。内核模块不像用户态程序可以随便上 debugger,很多时候最快的定位方式就是日志。等逻辑确认没问题,再逐步删减日志,把代码收敛成最终版本。
4.3 环境兼容性:虚拟机、非 GKI 内核与调度器
很多人会先在自己的发行版上装内核开发包,然后直接编译模块,结果在加载时遇到各种兼容性问题。其实内核模块对内核版本非常敏感,最稳的做法是直接编译一个完整内核,然后把模块编译进同一个源码树,再放进虚拟机里验证。
这里也顺带提一下 GKI 和非 GKI 的概念。GKI 内核把内核镜像和驱动模块之间的 ABI 做了稳定化处理,厂商模块可以跨内核小版本保持兼容;非 GKI 内核则没有这个保证,模块必须针对特定内核源码编译。对 Rust 内核模块来说,目前更常见的场景是在主线内核源码树中编译,第三方闭源模块在 Rust 体系里还很罕见,所以尽量跟紧主线或者长期支持版本,能少很多不必要的麻烦。
调度器这块,CachyOS 这类发行版会讨论默认调度器和调度器调优,但它们的实现仍然集中在 C 代码里。Rust 短期内不会去重写核心调度逻辑,但调度器外围的数据结构、统计模块、或者用户态调度策略,未来都存在 Rust 化的空间。
最后分享一点实际感受。我之前对“Rust 进内核”这件事是持保留态度的,总觉得一门语言想进入 Linux 内核这种历史包袱极重的项目,门槛高得离谱。但真把工具链配好、把最小模块在虚拟机里跑起来之后,我的看法确实变了。编译期就能抓住一批在 C 里只能靠 review 和经验规避的问题,这种感觉对写内核代码的人来说是很有冲击力的。Rust 现在肯定还不能叫“取代 C”,但它已经从实验性标签里走了出来,成为内核官方支持的第二语言。如果你本来就会 Rust,找个周末把环境搭起来,跑一个最小模块,你就能直观感受到这条新赛道到底意味着什么。后面如果 Rust 的异步机制能在内核里进一步落地,再配合用户态 Tauri、sqlx 这些成熟生态,整个系统软件栈的开发方式可能都会慢慢起变化。
