Archive for 2011年8月

为什么 Mac OS X 先进?

2011/08/19

这个世界上,接触过三大主流桌面操作系统的人,总会有相当一部分承认 OS X 的相对先进,也会有很多人反对。我认为讨论 OS X 先进性的文章里《开发人员为何应该使用 Mac OS X 兼 OS X 小史》是比较全面的。包括 OS X 先进的图形系统,完全继承 UNIX 的命令行优势,发扬光大的 scriptability ,以及让开发者有机会避开在其它平台上避之不及的 C++ 等等。还有 2003 年出版的《 The Art of UNIX Programming 》,虽然当时 OS X 尚不成熟,作者 Eric Raymond 已经对其继承 UNIX 的风格大加赞扬了。

如果对这些赞扬 OS X 先进性的文字不屑一顾,也请忽略我这篇「枪文」,因为我想的问题更虚无:不是要在上面的鸿篇巨著后面再添上 OS X 如何先进的证据,而是思考一下促成这些成就的原因。

我认为根本原因在于 OS X 是一个「新」的系统。它的开发始于 1997 年。那时,操作系统中各种技术决策的优点和失算之处都逐渐显露出来。拿窗口管理系统 (window manager) 来说,UNIX 的 X 的体现了分离窗口系统和系统底层内核的正确性,也体现了把上层策略(如窗口的 theme/style )和底层机制分离的正确性。但是 X 系统也有层次过多和协议过于繁琐的缺陷,让 UNIX/Linux 在图形硬件高速发展的时期陷入困境。OS X 作为后来者有机会审视和改进这一切。所以它最终采用了和 X 相似的用户态窗口管理系统,采用了先进的图层混合 (layer blending) 模式,同时抛弃了繁琐的 X 协议和 client-server 架构。另一个例子是绘图模式 (drawing model) 。Windows 的 GDI 绘图模式在当时质量和能力已显粗糙,而在出版业 PDF 正在成为标准,所以 OS X 采用了 PDF 成熟的绘图模式。几年以后 Windows 才推出 GDI+ 效仿 PDF 的高级绘图模式,但是让 Windows 开发者完全脱离旧的 GDI 模式还要等上几年。还可以举出的例子是,究竟让 toolkit(按钮等控件)作为用户进程的一部分来管理,让窗口系统只负责像素;还是像 Windows 那样把 toolkit 作为内核对象来管理。Cocoa 给出的答案也是对先行方案的反思和改进。同样,几年以后,.NET 的 WPF 才开始仿效这种让窗口管理和 toolkit 分离的做法。

所以,公平地说,Windows 的很多问题在于它的「陈旧」,甚至可以归功于它是「先行者」。我想有些读者可能已经等不及要反驳我了:第一,上面说的很多技术(比如 X )并不是在 1997 年前后才显露其优势和不足的,它们的成熟和优劣定论要早得多;第二,OS X 的开发不是 1997 年才开始白纸一张地开始,它的前身 NeXTSTEP  在1980 年代中期就动土了。

没错,反驳的一语中的!即便如此,我仍然认为 OS X (NeXTSTEP) 有机会审视和反思前人的优秀技术(或者优秀的失误),而 Windows 则没有这种机会。为什么?因为当年这些技术被认为是「严肃计算 (serious computing) 」,圈限在服务器和高端工作站领域。当时 Microsoft 的判断是这些基于昂贵硬件的技术一时半会(也就是十来年吧)不会进入 PC 领域。所以,弃这些计算机工程领域的思想珍宝不顾,Microsoft 忙着在 PC 领域重新发明关于安全、多任务、多线程、图形等等功能。甚至在 Windows NT 4.0 中,为了考虑 PC 的「特殊情况」,把 NT 3.5 已经实现的而且被业界认为是正确趋势的用户态窗口管理系统搬到了内核态,开历史的倒车。另一方面,乔布斯离开 Apple 之后,给自己的创业定了一个奇特的目标 —— 创造最好的学术研究电脑。为此而打造的 NeXT 硬件系统真的是贵族血统,却也落得个血本无归的局面。但正是在这个贵族平台上,NeXT 有机会审视和改进那些 Microsoft 不敢问津的「严肃」技术。Windows 的「陈旧」和 OS X 的「新」不在于时间,而在于前者 quick-and-dirty 的战略和后者略显固执的坚持不妥协。

微内核领域的传说

2011/08/05

IT 业和自然科学领域常说的「传说」一词来源于英文 myth ,是个负面的形容词,更接近「流言」、「谣言」的意思。如著名的电视节目《流言终结者》(mythblaster) 。也经常被翻译成「神话」,如著名的《人月神话》(Man Month Myth) 。这篇文章不是要贬低微内核 (micro-kernel) 这个概念本身,而是说人们对这个领域中的很多东西存在不小的误解。

Mac OS X 是微内核

OS X 的内核叫做 XNU ,是一个基于 Mach 和 BSD 的内核。因为 Mach 是最早的微内核,所以人们自然而然地认为 XNU 也是一个微内核。在软件领域「基于」这个概念是很模糊的。甚至一个系统只是从另一个系统的设计中借鉴了一些思路,也可以称为「基于」。虽然我相信 Windows NT 和 VMS 没有一丝一毫共同的 code base ,但还是认为前者「基于」后者。XNU 确实是继承了 Mach 和 BSD 不少的 code base ,但 XNU 抛弃了 Mach 最核心的设计选择 —— 微内核。所以即便二者有大量共同的 code base ,也只能勉强称为「基于」。

为什么继承了 Mach 大量 code 的衍生产品 XNU 反而抛弃了被继承者的核心设计?这要回头看 Mach 是如何开发的。开发一个全新的内核是件非常困难的事情。在内核的基本功能完成之前,开发者的工作几乎都是不可见的。Linus 在《 Just for Fun 》里回忆,到第一个 shell 被成功移植到 Linux kernel 上之前,他都看不到自己的工作有什么成效,突然( shell 移植完毕),似乎一切一下子完成了。这种最后突现的情形,对软件开发来说风险很大。因为错与对要到很晚才能揭晓。Mach 的开发采取了另一种渐进的、随时可以检验成果的方式:

  1. 取一个完整的 BSD 内核,改名叫 Mach 。
  2. 在这个内核上开发一套底层 IPC 机制。一般 UNIX 的 IPC 机制会利用内核模块的功能,比如 I/O ,内存管理,等等。这在微内核上是不推荐的,因为像内存管理或者 I/O 这类功能在理想的微内核上要作为用户态 (user-space) 服务。所以微内核的 IPC 非常底层。在 Mach 上是基于 message 和 port 的消息队列通信。
  3. 把原来的 BSD 内核各模块间通信(主要是方法调用)替换为 message/port 通信。
  4. 把已经改为通过 message/port 通信的模块从内核态 (kernel-space) 移出到用户态。

XNU 的做法是 undo 了第 4 步(有时连第 3 步也 undo 了),保留底层 IPC「机制」(mechanism) ,抛弃了 Mach 的微内核策略 (policy) 。通过这么做 XNU 拥有一个独特的优势 —— device driver 可以采用两种状态来运行,运行在用户态的时候利于开发和调试,运行在内核态的时候又不会引入微内核的开销。前者基本上只用来提高驱动开发的效率,实际发布版的 device driver 总是运行在内核态。所以,OS X 的内核 XNU 不是微内核。讨论微内核的成功与失败,性能高低等等问题,OS X 都不是一个适当的例子。

微内核解决了 Monolithic 内核不能解决的问题

往往认为微内核解决的最大问题是 monolithic 内核的复杂度和可维护性问题。可实际上微内核和 monolithic 内核在复杂度上并无显著差别,甚至后者还简单一些。从 Mach 和 OS X 内核 XNU 的例子可以看到,加上第 4 步,得到的就是微内核。去掉第 4 步,得到的就是 monolithic 内核。去掉第 3 步,得到的就是一个进一步简化了许多的 monolithic 内核。进一步去掉第 2 步回到原来的 BSD 内核,仍然是一个设计良好的内核,这也是 Mach 能在其上循序渐进的开发成功的原因。

其实,代码运行在内核态还是用户态不是构建其上的系统整体复杂度的决定因素,决定整体复杂度的关键在于各部分代码之间的功能边界是否清晰。微内核的作用在于强制约束了各个功能之间的边界,并且提供了有限的保护,也就是利用硬件 MMU 进行内存保护(但是这种保护并不真的总是有效,由于模块的 bug 造成的 crash ,常常并不能仅靠重启该模块来修复,而是必须重启整个微内核和所有服务,所以一个单独模块在内存保护之下 crash 所造成的危害并不见得一定比整个内核 crash 来的小)。这种约束的代价并不低廉。实际上,微内核本来的雄心是把 task schedule 和 IPC 之外的功能都放到用户态,但是由于硬件设计的限制(很多硬件其实都是按照 monolithic 内核的需求设计的),性能因素,以及内存保护能提供的实际错误恢复能力,实际中以微内核自许的产品 —— 比如 MINIX —— 一般也只能把 device driver 这样的外围模块放到用户态。提供强制的设计边界固然是好的,但是只有开发社区达到很大规模,这种需要付出代价的强制边界才能真正带来价值。小规模的开发社区更容易自律,从而更好的发挥 monolithic 内核本身结构精简的优势。而 device driver 的开发从来不是一个从业者人如潮涌的领域。

另一方面,monolithic 内核并不是真的铁板一块。Monolithic 的定义是不「强迫」开发者把模块放到用户态。「不强迫」不是「强迫」的对立物,而是后者的「超集」(superset) 。Monolithic 内核并不会阻止开发者把模块放到用户态,它所要求的是开发者自行开发一个小规模的内核态 stab 来负责把用户态模块的操作 relay 到内核。FUSE 就是一个在 monolithic 内核中实现用户态 driver 的例子。

所以微内核只是一个试图规约开发者的规范。它能解决的问题,并非 monolithic 内核所无法解决的,只不过侧重的灵活度角度不同。

微内核的效率很低下

这是个很有争议的问题。有些微内核将处理系统调用 (system call) 的模块也做成了用户态服务(仅仅是处理系统调用,而不是这些调用真正代表的功能。这里的处理系统调用模块只负责把用户态应用的请求分发到实际的功能服务)。即使是一次什么都不做的空系统调用,也要导致四次上下文切换 (context switch) 。

  1. 从发起系统调用的进程,切换到内核态。
    (只有栈和寄存器等状态切换,不需内存映射表的切换)
  2. 从内核态切换到处理系统调用的服务。
    (既有栈和寄存器等状态切换,也有内存映射表的切换)
  3. 从处理系统调用的服务切换回内核态。
    (只有栈和寄存器等状态切换,不需内存映射表的切换)
  4. 从内核态切换回发起系统调用的进程。
    (既有栈和寄存器等状态切换,也有内存映射表的切换)

常见的 monolithic 内核一般只需两次无需内存映射表的切换的上下文切换( 1 和 3 )。而且,由于处理系统调用的模块往往并非真正提供服务的模块,所以对微内核来说非空的系统调用会有另外的两到四次切换,而对 monolithic 来说只是一些普通的函数调用。作为 monolithic 内核 XNU 无需切换 2 和 4 ,但《地址空间划分》中介绍过,由于比较独特的地址空间,需要在切换 1 和 3 时切换内存映射表,所以开销会稍大。有 benchmark 测试曾经以 XNU 处理空系统调用慢于 Linux kernel 来说明微内核的效率低下,其实是误解了这个细节差异。

支持上面这个理论分析的实际性能数据大多都是在 Mach 微内核上得出。但是有人指出 Mach 是微内核的第一次尝试,设计和实现失误很多。比如很多的性能开销 (overhead) 其实发生在消息的合法性检验操作上,而非上下文切换。特别是用 Linux kernel 改写而成的 L4 微内核的支持者认为 L4 更好的精简了消息 IPC 机制,更多的利用了 x86 架构的独特硬件特性,性能和 monolithic 内核不相上下。

在我看来,这是「 Java 的性能已经和 C++ 不相上下」的另一个版本。不论怎么说,微内核引入的开销是客观存在的,更多细心精巧的设计也只能是降低这个开销,不可能完全消除。而微内核的安全保护,只有当内核的开发社团拥有一定规模的时候,才能体现出其价值。可是作为核心底层的模块,内核和 device driver 的开发永远是一个小团体的世界。特别是在 Mac 和 mobile 这类软硬件结合的生态体系重新获得成功,替代了以 multi-vendor 为特征的 PC 生态体系之后。