第20.4章:Linus Torvalds 为何称之为"BLOODY STUPID IDEA"
1. 那封邮件
这是 C++ 争论史上被引用最多的邮件,而引用它的人通常没有读本页之前的那两页。这里是它的全文,因为具体的论断比语气更重要:
From: Linus Torvalds
Subject: Re: Compiling C++ kernel module + Makefile
Date: Mon, 19 Jan 2004 22:46:23 -0800 (PST)
On Tue, 20 Jan 2004, Robin Rosenberg wrote:
>
> This is the "We've always used COBOL^H^H^H^H" argument.
In fact, in Linux we did try C++ once already, back in 1992.
It sucks. Trust me - writing kernel code in C++ is a BLOODY STUPID IDEA.
The fact is, C++ compilers are not trustworthy. They were even worse in
1992, but some fundamental facts haven't changed:
- the whole C++ exception handling thing is fundamentally broken. It's
_especially_ broken for kernels.
- any compiler or language that likes to hide things like memory
allocations behind your back just isn't a good choice for a kernel.
- you can write object-oriented code (useful for filesystems etc) in C,
_without_ the crap that is C++.
In general, I'd say that anybody who designs his kernel modules for C++ is
either
(a) looking for problems
(b) a C++ bigot that can't see what he is writing is really just C anyway
(c) was given an assignment in CS class to do so.
Feel free to make up (d).
Linus
这封邮件写于 2004 年 1 月。它已经二十年了,而它仍然是每一场关于"内核里能不能用 C++"的争论的开场白,这意味着它值得被核查,而不是被欢呼或者被嘘。所以:三条论断,一条一条来,对照本章实际测量到的东西。
2. 论断一:"C++ 的整套异常处理从根本上是坏的"
正确,而且对内核尤其如此,而这条论断不是观点。19.8 实测每次遗留抛出 934,540 ps,而每次赫布异常 885 ps;19.3 展示了那个机制。本章的 20.9 展示了它在链接期的代价:六个运行时函数、一个 unwinder、每个函数一张异常表、一个 personality routine,以及 RTTI。
"对内核尤其坏"说得完全正确,而且值得展开,因为这里其实是三个不同的理由,而人们通常只说一个:
- 失败路径会分配内存。一个靠分配内存来报告内存耗尽的内核,bug 出在报告上,而不是出在分配上。
- 失败路径会遍历内存。在一个可能正持有自旋锁或正在处理中断的上下文里,延迟没有上界不是"慢",而是不正确。
- 运行时可能根本不存在。内核本身就是 unwinder 要建在其上的那一层。下面没有别的东西可以问。
本书使用的机制回答了这三点,这就是为什么这条批评活过了那个机制:赫布异常把错误放在返回路径上的两个寄存器里(20.10),不分配任何东西,只需要链接器提供一个数据符号。
3. 论断二:"编译器喜欢在你背后隐藏内存分配"
也正确,而且这是随时间变得更成立的一条,因为隐藏分配的清单在 2004 年之后变长了而不是变短了:
-
new是一个全局堆加一套失败策略,在链接时替你选定(20.7)。 -
每一次遗留
throw都会分配异常对象。 -
::std::system_error在抛出开始之前就已经分配了两次(19.5)。 -
std::string的消息表、what()字符串和message()的返回值,全都在错误路径上分配。 - 一个需要扩容的容器,会在一个你以为是简单的函数内部调用那个全局堆。
答案不是"永远不要分配",而是"把分配来源做成类型,让调用者在签名里就能看到它"。这就是 fast_io 的分配器(19.9):策略是容器类型的一部分,所以一个内核函数不可能意外地从它并不拥有的堆里分配。而内核需要好几个堆 —— 按 zone、按 NUMA 节点、按设备、DMA 安全与否 —— 这就是为什么单一的 ::operator new 不只是隐藏,而是不够用。
4. 论断三:"你可以在 C 里写出面向对象的代码,而不带 C++ 那堆垃圾"
这条论断读起来像 1992 年,而且是这封邮件唯一在回答另一个问题的地方。Torvalds 当时拒绝的是当时那个 C++:异常、RTTI、隐藏分配,以及一个放错位置的 > 就产生四屏噪音的模板诊断。他当时捍卫的是用一张操作表写文件系统的能力 —— 而那张表就是 vtable,只是拼写成了 struct file_operations。
现代独立 C++ 保留了那些不需要运行时就能组织代码的部分:constexpr(第7章)、模板(第15章)、概念、确定性析构(第14章),以及现在的赫布异常。这些没有一个需要操作系统。那条抱怨从来不是关于语法的;而是关于当目标下面什么都没有时,生成的代码会做什么。
5. 2004 年以来什么变了、什么没变
| 论断 | 2004 | 今天 |
|---|---|---|
| 遗留异常对内核是坏的 | 成立 | 仍然成立,而且仍然是 C++ 默认提供的机制 |
| 编译器隐藏分配 | 成立 | 仍然成立;::operator new 在标准里没有竞争者 |
| C++ 独立实现是谎言 | 成立(C++03) | 部分修好(C++23),然后停住了:C++26 没有发布,所以 std::array 仍然不是一个独立头文件 |
| C 无法无代价地表达抽象 | 成立 | 不成立:constexpr、概念和模板在零运行期代价下的表达力,是 C 从未得到的东西 |
| 错误处理需要操作系统 | 成立 | 对赫布异常不成立 |
三条论断里有两条是关于机制的,而两个机制现在都有替代品。没有改变的是默认值:一个 2026 年的 C++ 构建仍然给你遗留异常、RTTI 和一个全局堆,除非你把它们关掉。这就是为什么本书在每个例子里都关掉它们,并在 19.7 里说明。