关于微内核的对话

不知怎么的,最近“微内核 vs 宏内核”又成了一个热门话题……

我很久没有关心操作系统了,因为通常所谓的“操作系统”,在我心里不过是一个 C 语言的运行时系统。由于 C 语言的缺陷,这些系统引入了各种无需有的概念:进程,线程,虚拟地址…… 微内核与宏内核之争当然也包括在内。

在我的理念里,一个优秀的操作系统本应该是这个样子

跟很多人聊操作系统是一件麻烦事,因为我为了得到本质的原理,往往会故意模糊化一些术语和概念。我试图从“计算本质”的出发点来理解这类事物。我所关心的,往往是“这个事物应该是什么样子”,“可以是什么样子”,而不只是“它现在是什么样子”。不明白我的这一特性,又自恃懂点东西的人,往往会误以为我连基本的术语都不明白。

幸运的是我有几个聊得来的朋友,他们不会那么教条主义。于是今天我跟一个朋友在微信上聊起了“微内核 vs 宏内核”这件事。其实这个问题在我脑子里已经比较清楚了,可是通过这些对话,我发现学到了新的东西。这些东西是我们两个人在对话之前可能都没有完全理解的,也许很多其他人也没有理解。所以我觉得可以把这些有价值的对话记录下来。

我不想从头解释这件事情,因为你可以从网络上找到微内核和宏内核的设计原理。我想展示在这里的,只是我们的对话,以及最后达成的一致结论。对话是一个很有意思的东西,我觉得也许它比平铺直叙的文章还要有效一些。

好了,现在开始。对话人物“IAN”是我,“LD”是我的一个朋友。

IAN:好多年没折腾 OS,现在再折腾应该有新的发现。这篇文章说 Minix 3 比 Linux 要慢 510%。通常的定义,说微内核只需要 send 和 receive 两个系统调用。你不觉得有问题吗?其实函数调用的本质就是 send 和 receive。

LD:是。

(下载 Minix 3 源代码看了一会儿。上网搜索关于微内核的资料……)

IAN:微内核似乎一直没解决性能问题。后面的 L4, QNX… 把 sever 隔离在不同的地址空间似乎是个最大的问题。

LD:导致通讯成本特别大。本来传递个地址就可以的事。现在要整个复制过去。

IAN:地址空间不应该分开。或者也许可以在 MMU 上面做文章。传递时把那片内存给 map 过去。这样上下文切换又是一个开销…… 函数调用被搞的这么麻烦,微内核似乎确实是不行。对了,微内核服务调用时会产生进程切换吗?

LD:会,按照微内核的定义,每一个基本单元都是一个进程。

IAN:完蛋了。

LD:内存管理是一个进程,IO 管理是一个进程,每个设备驱动是一个进程,中断管理是一个进程。

IAN:进程切换的开销……

LD:为了降低进程间通信开销,所以定义了 L4。我也不太懂这个有啥用。

IAN:但仍然有进程切换。我刚看了一下L4。从寄存器传值,但是进程切换会把寄存器都放到内存吧。

LD:一个外设产生了中断,中断管理进程接收到到中断,发一个消息给相应的设备驱动进程,这个进程处理中断请求,如果设备驱动有 bug,挂了,也不会干扰 OS。这就是微内核逻辑。

LD:对呀,所以 L4 意义似乎不大。

LD:带“微”的除了微软和微信,没一个成功的。

LD:最近流行的所谓微服务。

IAN:驱动的 bug 应该有其他办法。

LD:现在的 OS 的问题,就是内核微小的错误,都是让整个系统挂掉。这和我们写软件应该用多进程还是多线程。同样的问题。

IAN:应该从硬件底层彻底抛弃现在的进程切换方式。保存的上下文太多。

LD:现在 OS 不是分成 user 和 kernel 保护级别么。 我觉得再增加一个两个保护级别,专门针对设备驱动程序似乎是更好的选择。

IAN:我以前设想一个办法可以完全不需要保护级别。而且不需要虚拟内存。

LD:怎么办? 编译器静态分析搞定?Rust?

IAN:完全使用实地址,但是代码无法访问对象外面的内存。

LD:靠编译器保证?

IAN:没有指针。你没法访问不是给你的对象。嗯,需要抛弃 C 语言……

LD:Rust!

IAN:其实 JVM 早就是那样了。只不过通常不认为它是一个操作系统,但操作系统完全可以做成那样。

LD:所谓对象,就是每次地址访问。除了地址还有一个 size? 超过 size 不允许?还是编译器确保一定不会超过 size?

IAN:你在 Java 或者其它高级语言比如 Python… 都没法访问对象外面的内存啊。只有 C 可以。

LD:是的。C 这种方式,就是天天在没有护栏的桥上走来走去。除了越界访问,还有一个问题,就是多个 task 同时改一块内存。

IAN:然后为了防止越界,有了“进程”,“虚拟地址”这种概念。

LD:虚拟地址,还是为了用虚拟内存。

IAN:虚拟地址就是为了隔离。每个进程都以为地址从0开始,然后本来可以容易的函数调用被隔离开了。如果改变了这个,微内核就真的可以很快了。实际上内核就不存在了…… 哦,还是有。就只剩下调度器,内存管理。IPC 没了,被函数调用所取代。

LD:硬件级别 callcc。

IAN:也没有 callcc。被调用的函数并没有得到 continuation。只是它可以异步执行,就成了“超轻量线程”。

LD:假如硬件支持 callcc。好像对保护问题也没啥用。换个思路。其实 OS 最容易出问题的是硬件驱动,所以尽量让硬件标准化,别每个硬件都搞一套自己的驱动。让一套驱动支持多种硬件,问题就解决了。比如 usb 驱动。完全可以做到一类硬件都用一个设备驱动。

IAN:我还是觉得驱动程序 bug 其实可以不导致当机。用内核线程行不行?共享地址空间,但是异步执行。

LD:Linux 似乎就是这样。tasklet,可以被调度的。

IAN:所以驱动程序要是当掉,可以不死对吗?我回去查一下。

LD:看啥错误了。不小心修改了其它模块的内存就完蛋了。其它错误最多硬件本身不能用了。

IAN:所以就是为什么你说再多一个保护级别。

LD:嗯,别碰了内核关键的代码。但是驱动之间还是可以互相干扰的。

IAN:是个不错的折中方案。所以微内核解决了一个不是那么关键的问题。

LD:是的。这个问题不重要。哦,对了,Windows 是微内核的。好像从 2000 开始。

IAN:只是号称吧。Mac OS X 不是号称 Mach 微内核加 BSD 吗?

LD:对。MacOS 也是微内核。

IAN:那他们怎么解决的性能问题呢?

LD:不知道,Windows 蓝屏可不少,显然没做到完全隔离。至于 Mac,不清楚为啥那么稳定。

IAN:根据我们之前的讨论,所以 Mac 微内核可能是假的。因为 Mac 的 driver 就没几个吧。硬件都是固定选好的。

LD:嗯,也是稳定的主要原因。

IAN:这个英明了…… 而且看来微内核在集群方面也没什么用处。

LD:集群,每个计算机是一个 node。挂了也不怕。

……

(如果你有什么不同意见,欢迎联系我。也许接下去的讨论就会有你的输入 :)