12000行代码,8个人,两年
在这一期的十几篇报道里,有一篇标题很不起眼的短文——"Rust在内核中的性能对比测试"。但如果你真的点进去读,会发现里面藏着一个让所有参与Linux内核Rust移植的工程师都沉默了三天的数据。
事情是这样的。
Meta(就是以前的Facebook)从2022年开始就在往Linux内核里写Rust代码。他们的第一个大项目是一个叫做Bcachefs文件系统相关模块的Rust重写,后来扩展到了网络协议栈的一些组件。到了2025年春天,Meta内部的Linux内核Rust代码已经超过了12000行。
12000行。这是一个什么概念?
Linux内核到2025年初总共大概有3000万行代码。12000行,占比不到万分之四。但是Meta为此投入了至少8名全职工程师,工作了将近两年。
8个人,两年,12000行代码。
平均每名工程师每年写750行生产级内核代码。作为对比,Linux内核里写C代码的资深开发者,比如网子系统维护者Jakub Kicinski,在同一个时期内单人提交量超过了8000行。
差距接近10倍。
P99延迟差了整整3倍
但性能慢才是真正的痛点。
LWN那篇文章引用了一组来自Meta内部benchmark的数据。他们把同一套网络过滤模块分别用C和Rust实现,然后跑了一个高压测试:每秒处理200万个网络包(2Mpps)。
C版本的延迟分布:
p50: 1.2μsp99: 4.8μsp999: 12.3μs
Rust版本的延迟分布:
p50: 3.7μsp99: 15.1μsp999: 38.6μs
P99延迟差了整整3倍。
Meta的工程师在内核邮件列表上贴这组数据的时候,语气相当克制。原话大意是:"我们对这个结果并不感到意外,但这意味着Rust目前不适合对延迟敏感的数据路径(data path)。"
这话说得客气。翻译成大白话就是:性能差太多,暂时用不了。
问题出在内核态
为什么Rust会比C慢3倍?
原因不是Rust语言本身的效率问题。Rust编译出来的二进制代码,在用户态的benchmark里和C几乎持平,差距在1%-2%之间,完全可以忽略。
问题出在内核态。
内核代码和用户态代码最大的区别在于:内核代码要频繁地和硬件打交道,要用到大量的inline函数、编译器内建指令(compiler built-ins),以及各种和架构相关的汇编代码。
Linux内核的网络栈里到处都是这种东西:
static inline void skb_push(struct sk_buff *skb, unsigned int len){ skb->data -= len; skb->len += len;}这种一行搞定的函数,在C里可以无条件地被内联。编译器看到它,直接把代码展开到调用处,连函数调用的开销都没有。在热路径(hot path)上,这种inline至关重要。
但Rust的FFI层做不到这一点。
Meta的Rust模块要调用Linux内核的C函数,比如skb_push、skb_pull这些,必须通过Rust的extern "C"接口。每次调用都要走一遍C ABI(Application Binary Interface)的参数传递流程。即使编译器尝试内联,也经常因为Rust的safety检查机制而失败。
// Rust调用内核C函数的方式extern "C" { fn skb_push(skb: *mut sk_buff, len: c_uint) -> *mut c_void;}// 每次调用都有overheadunsafe { skb_push(skb_ptr, len); }更要命的是,Linux内核的struct sk_buff(网络包的核心数据结构)有超过60个字段,总大小超过200字节。Rust要操作这个结构体,要么每次都做一次unsafe块内的原始指针操作,要么封装一个safe的wrapper。无论哪种方式,都会引入额外的check。
在每秒200万次调用的场景下,每次多出的100纳秒就是200毫秒的总开销。听起来不多?但如果你的SLA是P99延迟不超过5微秒,那200毫秒的总延迟直接就把你的服务搞崩了。
eBPF:几十行代码干掉50%延迟
但事情没有那么简单。
LWN那篇文章最精彩的部分,是后面关于SCHED_EXT的一段讨论。
SCHED_EXT是Linux 6.12合并进主线的一个新特性。它的核心思想是:用eBPF来实现CPU调度器。也就是说,你可以写一段eBPF程序,在运行时替换掉Linux默认的CFS(Completely Fair Scheduler)调度器。
这听起来很疯狂。CPU调度是操作系统里最核心、最敏感的模块之一,每秒钟要做几万次决策,每次决策的延迟要求在微秒级别。用eBPF来实现这个东西,不是找死吗?
事实证明,不仅不慢,有些场景下还更快了。
Meta在他们的数据中心里做了一组测试。传统的CFS调度器在处理延迟敏感型任务(比如推理服务器的请求调度)时,P99延迟是23微秒。换成SCHED_EXT + 自定义eBPF调度策略后,P99降到了11微秒。
降了一半。
原因是CFS的设计目标是"公平",它要保证每个进程都能分到合理的CPU时间。但Meta的推理服务器根本不在乎公平——它们需要的是让高优先级的请求尽可能快地被处理完,低优先级的可以等。
// 一个简化的SCHED_EXT eBPF调度策略SEC("struct_ops/enqueue")void BPF_PROG(my_enqueue, struct task_struct *p, u64 enq_flags){ // 如果是推理请求相关的任务,直接提升优先级 if (task_is_inference(p)) { struct task_struct *high = get_highest_priority_task(); if (!high || p->prio < high->prio) { scx_bpf_dispatch(p, SCX_DS_LOCAL, SCX_SLICE_DFL, 0); return; } } // 否则放入低优先级队列 scx_bpf_dispatch(p, SCX_DS_GLOBAL, SCX_SLICE_DFL >> 2, 0);}这段eBPF代码只有不到20行,但它做的事情相当于给内核调度器做了一次"手术"。而且它是在运行时动态加载的,不需要重启系统,不需要重新编译内核。
改砖头 vs 开窗户
这就是LWN这期周刊最反直觉的地方。
一方面,Meta用Rust写的网络模块慢了3倍,12000行代码花了8个人两年时间,性能还是不行。
另一方面,有人用eBPF写了几百行代码,就实现了比内核默认调度器快50%的性能。
一个花了巨大投入,效果却不理想;另一个投入极小,却取得了巨大突破。
差别在哪里?
我琢磨了很久,觉得关键在于:谁在定义问题。
Rust-in-Linux这个项目的出发点是"用更安全的语言重写现有代码"。它要解决的问题是内核里几十年来积累的内存安全漏洞。这个目标很高尚,但它本质上是在用新的技术去套旧的架构。C代码在Linux内核里运行了30多年,无数的优化、内联、宏、hack都是为C量身定制的。Rust想要在不改动这些基础设施的情况下达到同等性能,几乎是不可能的。
而SCHED_EXT的出发点完全不同。它不是要重写调度器,而是要让调度器变得可编程。它没有试图用新的技术去替代旧的,而是给了用户一个接口,让用户自己去定义什么叫做"好的调度"。
一个是在改底层的砖头,另一个是在开上层的窗户。
砖头改了30年,你很难再找到空间。但窗户只要开对了位置,光就进来了。
未来:可编程内核
LWN的编辑Jonathan Corbet
这段话值得反复读。
因为它的潜台词是:Linux内核的未来,可能不是用新的语言把旧代码重写一遍,而是保留旧代码的内核,但在上面叠加一层可编程的中间层。
Rust在内核里的角色,也许不是替代C,而是作为一个安全的胶水层(safe glue layer),把各种可编程的组件(比如eBPF程序、WASM模块)安全地接入内核。
这听起来很科幻,但2025年的Linux内核里已经在发生这件事了。
*参考来源:LWN.net #1026 (June 2, 2025), Meta Engineering Blog, Linux Kernel Mailing List*