Archive for the ‘Mac OS X’ Category

Objective-C 到底给了程序员什么

2011/11/27

不少程序员和我一样,对 Objective-C 经历了从反感到喜爱的转变。反感的是方括号语法和没有虚拟机 (virtual machine) 的动态语言实现。转变因为两个原因。一是 Objective-C 相对简单的语言构造。需要面向对象编程又惧怕 OOC 的程序员们终于摆脱了 C++ 的绑架,没有了跨函数的异常处理,没有了声东击西的操作符重载 (operator overloading) ,获得了统一的内存管理从而摆脱了 C++ 的 value copy 和 block-scope 语义(什么时候 copy constructor 会调用?什么时候 destructor 会调用?)。第二,尽管基于虚拟机的动态语言更强大,但完全脱离 C 还是不现实的,构建实际的软件需要 hybrid 编程,而没有虚拟机的 Objective-C 是一种「单一」语言,还是单一语言好用。

首先谈谈第一点,Objective-C 比 C++ 简单是否仅仅限于砍掉了一堆 C++ 语言的 feature 而已?那么又增加了方括号语法是不是在「砍掉 feature」大基调下一个不和谐的杂音?

范式分割

C++ 被其设计者 Bjarne Stroustrup 定义为「多范式 (multi-paradigm) 的通用编程语言」[1]。Stroustup 的基本出发点看似没有什么问题,解决不同的问题,尤其是不同层次的问题,需要不同范式的编程语言。但是,能不能把不同范式揉合到一种语言当中呢?至少 C++ 的具体方式很糟糕。我认为正确的方式是让程序员在(面向不同问题,或者分析不同代码的)不同时候用不同范式思考,提供在范式之间切换的手段。尽量让程序员在每个时刻只需用单一范式思考。同时混用多个范式是巨大的智力负担。C++ 正好反其道行之:对所有范式都选取相似的语法符号,更糟糕的是为了加大相似度特地提供了操作符重载。Stroustrup 在自己的书里多次提到他的出发点是让所有范式的所有元素都成为 first-class citizen。这是个似是而非的肤浅观点。平等的成为 first-class citizen 并不的意味着被相似的符号表示。C++ 不仅鼓励没有节制的混用范式,即使某段代码实际上只采用单一范式,维护它的程序员也无法放心的排除其它范式。结果是对任何看似简单的代码都必须同时考虑 C++ 的所有范式。

应该说这个问题并非 C++ 独有。高级动态语言同样可能在没有明显代码风格差异的情况下改变范式。但我在之前的 blog 里讨论过 [2],丰富表现力的子语言解决复杂单一的问题的可以采用这种方式,却并非适合团队协作的通用编程语言应该采用。

Objective-C 和 C++ 相比无论语言元素是增是减,始终为了一个目的,向程序员清晰的表达目前所处的范式。首先它的目标更简单,没有难以融合的 generic 范式,集中精力实现 OO 范式。它提供了方括号语法明确表示范式的切换。排除了操作符重载,避免混淆范式。没有跨函数的异常处理,避免破坏 C 的过程范式的重要部分,也是起系统胶合剂作用的 ABI 重要部分 —— call stack 规范。所以,当程序员看到一段 Objective-C 代码时,他可以根据方括号的出现与否轻松的决定是否让头脑卸下思考 OO 动态行为的负担,集中注意力到 C 的静态部分;还是让头脑卸下思考内存管理等底层问题的负担,用 OO 的高级抽象思考问题。

Objective-C 的设计者 Tom Love 解释过方括号语法确实是为了让 C 部分和 OO 扩展部分成为有鲜明区别的「两个」语言 [3]。虽然 hybrid 编程会带来的互操作和调试的负担,但是程序员都喜欢用不同范式解决不同层次的问题来减轻头脑的负担。适合问题的范式和范式之间明确的界限是 hybrid 编程最大的优势。

工具融合

如果 Objective-C 是两种语言的 hybrid 编程。那么一开始提到,和其它高级动态语言与 C 语言的 hybrid 编程相比,Objective-C 是一种「单一」语言又该怎么理解?这取决于工具支持。Objective-C 在语言设计方面保持两个独立的语言定义,又用工具把实际使用中可以融合的部分尽量融合到一起。

首先,hybrid 编程依靠不同的语法来区分范式。大多数 hybrid 编程把针对不同语法的编译器也区分开来。比如,Lua 的编译器是一个 library,和 C 编译器没有任何联系。参数通过 API 传递。Objective-C 的做法是提供可以同时处理两种语言的单一编译器。虽然揉合了两种语言的处理,但是可以设想,除去前端 parser 的语法树 (AST) 产生可能复杂一些,这个编译器的后端处理不会比单独写两个编译器更复杂。和 C++ 融合多种范式的编译器复杂度不可相提并论。对程序员来说,Objective-C 的源文件作为一个整体是极大的便利。

不论范式如何不同(摧残头脑的 functional 编程除外),程序的运行都是线性的,所以每个线程中包括所有范式语言的单一 call-stack 是存在的。尽管逻辑上可行,许多 hybrid 编程环境里并不提供能展示单一 call-stack 的调试工具。如果需要,程序员只能自己用头脑分析和想像。Objective-C 极大的改进了这点。除统一的编译器之外,让 Objective-C 成为「单一」语言的另一个要素是能集成分析 C 和 OO 扩展部分运行状态的调试器。

从工具集合来看,Objective-C 比大多数开源语言都丰富。无论开源还是商业领域,直接支持 hybrid 编程的单一编译器和展示 hybrid 运行时的单一调试器都是 Objective-C 独特的优势。

共生系统

Objective-C 体现了独特但合理的设计编程环境的思路:使用同一工具的并不非得是一个语言,多个语言也不见得非要使用多个分立的工具。集成不同语言的可以是方便的工具而不是脆弱的 API。对两种定义清晰的语言提供高度集成的同一工具是 Objective-C 比很多晚出现十几年的语言更显先进的原因。太多语言设计者抱着这样的错误成见:我的语言必须是一个整体;我的语言可以令程序员从开始 coding 一刻起就避免和解决很多其它语言需要用工具解决的问题,所以不需要太多工具;我的语言这么好所以会有无数的人为它写工具,我只要专心设计语言最小核心的编译解释工具。我曾经写过 Lua 最大限度避免了集成污染 [4],因为 Lua 利用了 C 编译器对 ANSI C 的良好支持。遗憾的是,在这个利用工具的良好开端之后,和其它语言一样,Lua 的开发团队始终是限于语言本身的改进而没有任何附带的增强工具。

编程环境是语言和工具的共生体,但是人们经常忽视这个事实,或者只是关心共生体的一小部分,徒劳的希望别人来改进其它部分。Mac OS X 的编程环境,得益于 Apple 这家强于 end-to-end control [5] 的公司选择了独特的 Objective-C。

脚注:

  1. Design and Evolution of C++》。
  2. 高级动态语言和软件业》,《高级动态语言和软件业 —— 交流与内省》。
  3. 《Masterminds of Programming》。
  4. 集成污染
  5. Objective-C 说明了 end-to-end control 并不等同封闭。它的实现 Clang 和 LLVM/LLDB,乃至之前的 GCC 都是 open source 实现。Control 只是推动了它们的整体发展和最终集成。Apple 选择了工具支持最好的 Objective-C,同时在业界对其缺乏兴趣的环境下一力发展 Objective-C 的语言和工具是有勇气的。

OpenGL 随想(五):从 Fixed-Function 到 3.2

2011/10/23

国庆之后一直没写 blog。一来其它事情很多;二来闲下来就想阅读。长假还算没有荒废,以 OS X Lion 支持 OpenGL 3.2 为动力开始重启 OpenGL 的研究。假期前两天在基于 OpenGL 2.1 的产品中加上了用 GLSL 1.1 写的 bicubic interpolation 的 fragment shader,给学习 OpenGL 2.1 画上一个句号。接下来用其余的假期调通了第一个 3.2 core profile 程序。这个假期可以说是从主要基于 fixed-function pipeline 的旧 OpenGL 向完全可编程 pipeline 的新 OpenGL 前进的一步。

从 OpenGL 2.1 到 OpenGL 3.0 是一道分水岭。在其后 OpenGL 开始致力于抛弃之前版本中只适合过时硬件结构的 API,让开发高质量高性能的 driver 变得更容易,从而带动整个 OpenGL 平台的改善。到 OpenGL 3.1 之后,虽然不少实现仍然保留旧功能(nVidia 的 Windows driver 号称永不去除任何功能),但是在同一 OpenGL context 中混用二者越来越困难。OS X Lion 允许创建两种 context:OpenGL 2.1 和 OpenGL 3.2 core profile。前者只能通过 EXT 使用 3.x 有限的部分功能,后者可以使用 3.2 的所有新功能(以及通过 EXT 使用更高版本的部分功能),但无法使用 2.1 的旧功能。

被抛弃的最大部分是立即模式 (immediate mode),也就是初学者熟悉的 glBegin/glEnd/glColor/glTexCoord/glVertex 等等函数,以及和矩阵相关的功能。再者是去掉了原来固化在 driver 或者硬件中的坐标变换、光照等等 fixed-function pipeline 功能,转而必须由 shading language (GLSL) 编写的 shader 完成 (OpenGL 2.1 里可以用 shader 来替代 (override) fixed-function pipeline,但并非必须,在没有 shader 的时候 fixed-function pipeline 依然工作) 。我认为去掉立即模式的原因是 GPU 能直接访问的独立显存数量和 CPU-GPU 间的带宽不断增加,以致立即模式一次搬运少量 vertex 信息节省显存和带宽的优点逐渐失去意义,反而突显了性能方面和 driver 复杂度方面的劣势。用 shader 取代 fixed-function pipeline 是对应用程序开发者暴露更多的硬件灵活性。而且,在立即模式下,shader 只能通过预定义的 built-in variable 访问 CPU-side client 提供的 vertex 数据,去掉立即模式让 CPU 上的 client 代码和 GPU 上 shader 能直接传送任意命名的数据,二者的衔接更加直观。

OpenGL 是显卡的 C 格式机器码。有意思的是,当硬件处于初级阶段的时候,这种「机器码」还呈现一定程度的描述语言 (declaritive language) 风格。而硬件发展到高级阶段之后,反而彻底失去了高级风格。说到这儿,要回头谈谈 3D 开发的两种模式:场景 (scene) 模式和管线 (pipeline) 模式。场景模式中应用负责描述 3D 场景,由 API 负责把场景的描述翻译成屏幕显示。对开发者来说场景模式固然更容易理解,但是在《OpenGL 随想(四):计算机如何解决问题》里解释过,现有的计算能力很难完全满足场景模式。光照、阴影、平滑曲面、镜面反射等等都很难离开程序员的干预纯粹通过场景描述来自动完成。为每种场景效果都设计一个可配置的属性也让 API 过于繁琐。所以场景模式大都存在于 AutoCAD、3DS Max 这样的交互式建模工具中。而编程 API 往往采用更底层的管线模式,即在程序员理解硬件功能的基础上为硬件的计算提供必要的数据、参数和代码 (shader) 。

在 OpenGL 2.0 之前,由于硬件和 driver 只能提供固定功能的 pipeline,所以调用这些功能的方式向最常用的场景描述信息靠拢。用立即模式编写的代码,除去一些 OpenGL 状态机的状态变换,很像场景描述。所以初学 OpenGL 容易误认为这是一个简单的场景模式 API。只有接触到反射、阴影等等复杂用例之后才会转换思维。

从 OpenGL 3.0 之后,整条图形处理 pipeline 都由程序员掌握(除了少数功能,比如从 geometry shader 到 fragment shader 的光栅化)。在《OpenGL 随想(四):计算机如何解决问题》里提到过,像曲面平滑这类效果是先计算关键点在投影平面上(也就是 viewport)的颜色,然后对 viewport 中其它像素的颜色进行二维插值 (interpolation) 来完成的。在 3.2 之后,程序员可以做更灵活的选择,比如可以对每一点的法向量 (normal) 和光源向量进行插值后分别计算各点颜色,而非基于关键点的颜色插值。程序员编写的代码时会更多地从控制 pipeline 而非「描述场景」的角度思考。OpenGL 3.2 成为主流之后,初学 OpenGL 的曲线可能会更陡 (我的第一个 3.2 程序调试了四天时间。因为当整条 pipeline 的功能完全暴露在程序员面前的时候,驾驭起来并不容易。即使是一个小小的 hello world 也可能出很多错误),不过以后接触复杂用例的时候不用再经历思维模式的转换。

越灵活的功能必然呈现越底层的界面。而越高级的抽象只能暴露越僵化的功能。三年前 Ars Technica 访谈 3D FPS 引擎 Unreal 的设计师 Tim Sweeney,后者预测不光 fixed-function pipeline 会被可编程 pipeline 取代,甚至 pipeline 概念本身也会消失,3D 开发从基于 API 的方式专为使用通用编程语言。

三年后的今天,3D 开发仍旧以 OpenGL 4.2/DirectX 11 这样的 API 为最前沿,而 OS X 和 Windows 7 这样的主流环境处于 OpenGL 3.2 的水平。也许这就如同 CPU 上的编程语言一样。我不是要讨论汇编语言,这里不讨论编程语言对硬件能力的驾驭,而是说对问题描述的表现力。人们曾经用 COBOL 尝试模仿自然语言编程,就像最初的看似场景模式的 fixed-function pipeline 和立即模式,最终证明那实际上只是针对某些模式问题的肤浅方案。于是又有人像 Tim Sweeney 展望 3D 开发那样想像最灵活最有力的表达方式,得到几乎没有语法的 Lisp 语言。最终我们在一定程度上接受了动态类型、lexical closure 这样的概念,但还是在 C、Objective-C、Python、Lua 这样的中间点安顿下来。同样,在很长一段时间内 3D 开发还是会保持 API/Shading Language 的可编程 pipeline 模式。

为什么 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 生态体系之后。

整洁之下的隐患

2011/02/23

上个月写了篇《技术的洁癖》比较了一下 Mac OS X 的单文件应用和 installer 两种发布手段,基本上只针对安装和卸载的完全度。不久之后看到某国企发布的一款应用的 installer 居然把 /etc 之下的所有文件权限改成『 777 』,想到单文件应用绝不会有这样的行为,所以写了这篇谈论一下安全问题。

安全是一个复杂的多层面的话题,在讨论普通 Unix 和 Windows 这样的基于 access control list 的 DAC 安全模型时,往往仅仅限于满足一个基本的要求 —— 恶意代码不能修改或者毁坏当前用户之外其他用户的和系统的数据,但是并不要求恶意代码无法修改或者毁坏当前用户自己的数据。在某些高标准的环境中,这个基本要求是远远不够的。但是对于大多数个人应用来说,只要满足这个基本要求,就可以通过系统本身的合理配置建立『沙箱』隔离恶意代码。

这个标准同样可以用来要求应用程序的发布方案:安装和卸载应用程序的过程不能修改或者毁坏当前用户之外其他用户的和系统的数据,但是并不要求恶意代码不修改或者毁坏当前用户本身的数据。

要分析这个要求是否能得到满足,先分解一下安装过程。一般来说,安装应用包括两个动作:

  1. 把应用程序的可执行文件拷贝到目标机器的硬盘(或其它存储设备)。这个过程可能包括文件的解压缩等操作 [1]。
  2. 执行一个或者多个『脚本』[2] 来创建或者修改一些配置文件。

简单考虑,如果保证每个用户拥有各自独立的可执行文件拷贝和配置文件,那么只要让安装的整个过程都只在普通用户权限下执行,前面所说的对安全的基本要求即可得到满足。

对今天的系统和大多数应用来说,保证每个用户有各自独立的配置文件不成问题,但是大多数系统都要求应用的可执行文件由所有用户共享。一般情况下,应用的可执行文件都要拷贝到系统文件夹之下。这意味着如果使用 installer ,就必须用系统管理员权限来执行。这就给了 installer 绕过『不能修改或者毁坏当前用户之外的其他用户和系统的数据』这个要求的机会。

有人会考虑把 installer 分解成两部分,分别执行上面的第一步和第二步,而只给第一部分系统管理员权限。可是,对于一个可执行的 installer ,即使名义上分成了两部分,用户也很难限制第一部分做什么,它完全可能毁坏系统数据,或者更糟,把安装的可执行文件的 ACL 设置成可提升权限 [3] ,又或者把所有系统文件的权限改写成『 777 』。这是一个看似可行但是错误的方案,因为最小权限分离仅仅适用于防止可信程序由于 bug 被恶意代码劫持权限,而 installer 在这个情景中是『可疑』的,其行为是用户不能确定的。

真正可行的办法是让用户调用操作系统的命令而非 installer 来执行第一步 [4],因为系统命令本身是可信的(trusted),即使用管理员权限运行系统命令,只要正确输入参数,其行为是用户可以确定的,不会造成危害。

上面这个过程其实就是设计应用发布方案过程中的一个需求迭代,当安全因素纳入考虑之后,迭代的结果把最初的基于 installer 的方案变成了一个类似 Mac OS X 单文件应用的方案:

  1. 单文件应用的第一步完全依靠 OS X 本身的 Finder 完成。虽然需要管理员权限,但是应用本身,即便是恶意的,也没有机会在这个过程中采取恶意行动。
  2. 单文件应用的配置文件在应用的第一次运行时生成,只要始终用普通用户运行,恶意程序没有机会毁坏其它用户和系统数据。

从这个对比可以看到,installer 确实给了有技术洁癖的人一个整洁的装饰板,可是饰板之下未必就是优雅。以高度互信作为前提的『整洁』的方案,到了『可疑』环境中可能悄悄的毁坏数据。如果非要坚持其表面的整洁,整个方案也很难通过简单的改动弥补安全方面的问题。

当然,在满足本文涉及的安全需求方面,只有单文件应用拥有优势。而 OS X 本身就存在其它安装方式,而且也在不断引入新的安装方式,比如 Photoshop 的定制 installer 和 Mac App Store。不过本文主要针对的是在不考虑 kernel 的严重 bug 的情况下,限制『可疑』应用危害的方法。而 Photoshop 和 Mac App Store 之类的方案则从其它方面提高了应用的『可信度』。那是另外一个话题了。

注释:

  1. Mac OS X 的单文件应用不包括这种操作。
  2. 这里的脚本并非狭义的纯文本解释执行的脚本文件,而是值这种配置操作实质只是一种脚本类的操作。
  3. 比如 Unix 文件系统的 s 属性,可以让一个可执行文件即使被非管理员用户执行也拥有管理员权限。
  4. 对于采用特殊的私有格式进行打包或者压缩的应用,这种操作很困难。

OpenGL 随想(四):计算机如何解决问题

2011/02/05

之前《OpenGL 随想》已经写了三篇()。题目中虽有 OpenGL,但内容并非介绍 OpenGL 或者讨论其技术细节,而是讨论学习过程中联想到的一些计算、软件方面的问题。OpenGL 主要作为引发思考的灵感和一些例子的来源。

这些联系很松散的文章被归于一个系列为的是体现研究 OpenGL 这样一个复杂系统的过程中能引发各种思考。在计算软件领域之外也有类似情况:研究第二次世界大战的历史可以引发人际关系、项目管理、市场走向等等方面的思考。这个系列算是对这些辉煌的灵感源泉一个微不足道的致敬。

聪明和笨

前几个月,有人总结:计算机系统的性能提升中,算法改进导致的部分超过了遵循摩尔定律的硬件发展所带来的部分,前者达到后者的四十多倍。考虑到数学等基础领域出现突破进展是多么困难,而另一方面硬件一直沿着摩尔定律设定的路线坚定不移地提升(指数发展,每 18 个月每单位晶体管数量增加一倍),这个数字有点让人惊讶。

换个角度想,也就是说计算性能的提高的很大一部分不是归功于电子学和材料学的进展,而是归功于人们原本采用了比较『笨』的数学方法。而且提高的幅度之大说明一开始的方法『笨』的不轻。从纯数字看似乎的确如此,但从另一个角度看这是个错觉。

OpenGL 的策略

OpenGL 和计算机三维图形领域的一些方法所体现的『聪明』可以很好的解释『笨』的含义。比如考虑一个基本的问题,在计算机图形中如何表示一个三维平滑曲面,比如一个球体?最直接的方法是把曲面的表面离散化,通过数学方法(比如球的解析式或者 NURBS 表示)求出各个离散点的坐标,然后用离散点构成平面。离散点分布越密集(术语叫采样频率高),每个平面越小,平面越多,结果就和真实的曲面越吻合。当采样频率在 viewport [1] 上的投影超过屏幕的分辨率的时候,就会和平滑曲面无异 [2]。遗憾的是,估计当今主流台式机硬件用这个方法连五年前的高端游戏效果都得不到。OpenGL 采用另一种方法:smooth-shading 。这种方法采用的采样频率不是很高,只计算每个离散点当前视角和光照条件下在 viewport 上的位置和色彩。然后在二维的 viewport 上对各个离散点的投影之间的区域进行二维色彩线性插值。也就是说,OpenGL 的曲面是在 viewport 上用二维位图处理,或者可以叫『伪 3D 』方法实现的。

正是这种『伪 3D 』方法,让个人计算机在七八年以前就实现了高度仿真的实时三维图形效果。那么是不是可以说这是一个『聪明』的方法而提高采样频率是个『笨』办法呢?并非如此。如果希望计算机图形系统能自动完成任何光照条件下对各种材质、包括反射等等效果的自动处理,特别是当仅仅知道光照条件和材料特征的情况下希望通过计算的方式得出场景呈现的效果,这是唯一的方法。OpenGL 采用的『伪 3D 』方法不能直接推演真实的效果,它所做的只是用一些近似方法得到任意的效果,而后必须由人来主观判断这些效果是否足够和真实场景接近。除平滑曲面之外,OpenGL 中通过 viewport『伪 3D 』方法来完成的效果还包括物体遮盖、半透明、反射等等。

另一个例子是阴影。如果要自动生成阴影,图形系统必需跟踪场景中物体对光源遮挡的效果。由于性能的原因,OpenGL 完全不做这种光线追踪处理,阴影效果要求程序员通过投射变换计算出位置和形状之后手工绘制的黑色或者灰色形状(只有透射变换计算本身可以通过 OpenGL 提供的函数完成)。而且,简单的投影计算只能呈现理想点光源的全影,无法实现太阳这样的面积光源形成的半影。如果要形成半影,程序员还必须自己写代码通过二维位图处理(也可以看作『伪 3D 』)算法生成阴影然后画在投影面上。同样通过手工计算完成的效果还有镜面反射等等。

OpenGL 并不是能直接从物体描述自动生成场景的虚拟照相机,大量二维位图处理的应用使它更像画具,只不过增加了一些稍稍方便和精确的三维旋转和透视的处理。用 OpenGL 直接构建应用程序的过程更接近绘画而非场景布置或者摄影。[3] 虽然从历史角度说 OpenGL 的设计不见得就真的经历了从『面向场景和摄制』到『面向绘制』的转变,但是很多惊叹 OpenGL 效果而不明就里的人头脑中确实把 OpenGL 的功能想像成前者。所以从认识的角度以及类比到更广阔的领域,OpenGL 做出的主要『优化』本质上是改变了需求或者说问题域。

这是计算机提高解决问题效率的一个主要方式:回避问题,改变问题!虽然目标仍然是为用户呈现高度真实感的虚拟场景,但是 OpenGL 几乎完全剔除了一切『虚拟现实』的能力。它采用与真实场景和摄影相比高度简化的近似方法,要求系统的构建者通过实际经历、想象力、或者其它 OpenGL 之外 [4] 的方法另行得出,甚至是事先得出真实场景应该呈现的效果,通过主观比较判断 OpenGL 的近似效果是否令人满意再对不够满意的地方做出调整。和表面的感觉不同,OpenGL 把看似精确的计算机图形变成了一个主观创意的问题。

问题域和人的价值

计算机领域的很多所谓『算法优化』其实是『问题域』的简化或者说窄化。一开始面对一个过于复杂的问题域,采用性能无法被接受的算法。而后慢慢收回过大的野心,用更粗糙的算法来满足更简单的问题。

对问题域的修改即使并非唯一方法,也是从硬件之外提升计算生产力的主要方式。比如代码优化这个问题,C++ 就开始放弃了对语义和程序员透明的原则,引入了需要程序员干预的 ROV 优化。相比之下『空间换时间』等等经典的优化理论受到硬件的制约比较大,可以视为硬件发展的附庸,比如廉价的内存才能保证大 cache 算法的可行。而且硬件的改进会让一些优化技术失效,比如近年 CPU 片内 cache 和主内存之间速度差距的迅速增大已经让编译器的『体积最大』优化策略难以换取『速度最快』的结果。

对原问题域的简化并非意味着对被简化部分的简单忽略,很多的简化是把问题中计算机可解部分和人类干预部分的合理划分 [5]。OpenGL 这样的计算机图形系统的功能必须通过各种建模工具的交互式界面和额外材料来填补其『面向绘制』问题域与普通人能胜任的『面向场景和摄制』问题域的差距,才能在人类的辅助下释放最大潜力。严格限制计算机可解部分而得到更高的计算效率,通过良好的UI 把其它交给人类解决。这也是 UI 设计的意义,精致 UI 的作用不光是让人类用户享有更好的用户体验,也是为了释放计算机自动处理部分的最大潜力。

注释:

  1. Viewport 可以理解为 OpenGL 中虚拟摄像机的底片在操作系统的窗口上的实际呈现。
  2. 这里的『无异』是在硬件呈现能力的范围内而言。不过今天的硬件也已经进入了 ratinal screen 的时代,可以说在人的感知能力范围内也可以做到『无异』了。
  3. 很多三维设计师的自我感觉更像摄影师或者场景布置师,这正是 3DS Max 或者 AutoCAD 等软件在 OpenGL 之上提供的附加值。
  4. 这种方法不一定是脱离计算机辅助的。比如上面提到的 AutoCAD 和 3DS Max 等软件的帮助。但是它们是 OpenGL 之外的。而且是通过人类主导的。
  5. 某些创造性工作或者人脑结构更适合解决的问题还能让人类保持一种优越感和尊严。

OpenGL 随想(三):同步与多线程

2011/01/21

去年十月写过一篇《并行执行》,讨论了多核与锁的问题。提到了『并行执行』和『并发访问』两个概念。最近看公司里面老外写的文档,也用了同样的概念,不同的名称,分别叫做『性能化多线程』和『功能化多线程』。根据公司里项目的实践,『并行执行』除了像《并行执行》里讨论的向 GPGPU 转化之外,也在向 Intel TBBMKL 之类的 non-lock-based 技术转化。锁这个技术,在『性能化多线程』中的应用已经逐渐萎缩,并且我认为『功能化多线程』也会为了降低复杂度而逐渐向 timer 等 cooperative 并发技术转移。

本质上说,OpenGL 也是『性能化多线程』。这种多线程的特点是所有线程都不共享数据,所以无需中间同步。只需要最后的结果同步一次。类似主线程对所有性能化线程调用 join 。OpenGL 有两个操作可以提供这种 join 的语义,glReadPixels()glFlush() 。这两个操作都是为了防止用户看到数据(渲染结果)的不合法状态。比如 glFlush() 经常作为 glutSwapBuffers() 的隐含步骤执行。

技术的洁癖

2011/01/11

看到 Twitter 上讨论 Mac 的单文件应用的卸载问题。首先说明一下,单文件应用是 Mac 上一种很常见的发布方式,整个应用只有一个文件(其实是一个目录),安装即拷贝该文件到 /Applications 下,卸载即删除这个文件。Mac 下还有很多种其它发布方式,比如 Xcode 使用的是 Apple 推荐的一种多文件 installer ,Photoshop 是 Adobe 自己设计的 installer ,而且 Mac OS X 也不会限制任何人发明定义自己的安装方式。这里只讨论单文件应用。

精确的说,单文件应用的安装实际发生在第一次启动,这时应用会按照需要在用户的数据目录下创建必要的配置文件。所以,简单的删除应用文件这种卸载方式其实确实会留下一些垃圾。

这个问题给了两种人不同的反应。第一种人是一定要把 OS X 的单文件卸载和 Windows 下流行的反安装(uninstall)比较的人。我想分两个层面讨论这种反应。首先,Windows 的反安装鼓励软件发行者安装的时候做了什么卸载的时候就反过来做什么;而 Mac 的单文件应用卸载鼓励发行者置少量垃圾于不顾。从理想层面说 Windows 是占优的。可是实际上,没有一个 Windows 应用的反安装能完全兑现理想,甚至很多应用连自己的 DLL —— 也就是可执行文件都删不掉。Windows 和 Mac 的方案相比较,前者是一个完全兑现就能美好的不得了,但是稍有差池就跌下地狱的方案,后者是一个基本能 100% 兑现但是天然有轻微缺陷的方案。

第二个层面是要追寻卸载应用的动机。自从买过上一台 MacBook 到现在用这台 MacBook Pro ,我从来没有卸载过一个应用。原因很简单,硬盘够用,系统也没有变慢。用 Windows 的时候我也几乎不用反安装功能,因为卸也卸不干净;而且一般三个月左右,最多四个半月系统就慢到必须重装的地步了。

被这个问题引发兴趣的第二种人是宣称自己喜爱整洁的人。对他们来说,就算系统不变慢,硬盘还有 2T 空间,只要想到还有几个 k 的垃圾文件隐藏在边边角角就浑身不得劲。对于这种把『喜好』、『就是』作为原因的人,仔细从人性和人类发展方面思考一番总是能得到些乐趣。比如说,装修摸墙面的时候都得把墙面刮花。如果一个人坐在整洁的客厅,想到墙面下其实是刮得乱七八糟的墙体而百爪挠心呢,是不是比较分裂?还有今天的文件系统早就不是那个一周用 defrag 整理几次的 FAT16 系统了,它会按照自己的工作把文件随意摆放。喜好整洁的人是否也不乐意呢?Joel 曾经看到过一台『肮脏至极』的面包机,而事后证明那是制造美食的好工具。其实在维系人类正常发展的效率需求之前,洁癖是没有生存空间的。

最后声明一下,我不认为 Mac 的方案是完美的。更进一步,不论是 Windows 的安装/反安装加注册表,Mac 的单文件应用加不完全卸载,还是 Linux 的集中化包管理,都有很大的改进空间。只是所谓基于『卸的干净』的洁癖这种思考方式太过简单了。

分裂的代价

2011/01/01

当 Microsoft 刚刚推出 Direct3D 的时候,PC 还是一个弱小的平台,接受 OpenGL 这样的工作站级别标准很遥远。所以『单独搞一套』似乎是天经地义的。当 NT 平台和 Windows 95/98 逐渐合并的时候,工作站级别的 OpenGL 和 Direct3D 功能重叠的问题开始被关注。最后,Microsoft 退出 OpenGL ARB 说明了它的决定 —— 用私有标准来 lock-in 开发者的 code base 和 knowledge base 。

经过了近十年之后,Direct3D 已经成为 3D 领域中一个功能上受人尊敬的竞争者,但是其分裂初衷实现得如何呢?去年年初,游戏大牛 David Rosen 写了 《 Why You Should Use OpenGL and not DirectX 》,说明在游戏开发界有众多严肃的开发者坚持开发基于 OpenGL 的产品,其中部分甚至是只在 Windows 上开发也不涉及 OpenGL 之外的接口。而在 Rosen 文章之下的评论里不乏反对的声音,其中一个理由是:只要软件的结构清晰,从 Direct3D 转向 OpenGL 代价并不高,可以说除了资源的确捉襟见肘的 just startup 都能应付。两种声音说明,无论是对于有强烈主观倾向的开发者,还是对于重视实用可操作的开发者,lock-in 都并不成功。

对 Microsoft 来说,进一步展示抛弃 OpenGL 的决心,也许会让开发者三思。所以出现了 Vista 剔除 OpenGL 的言论。但是事实是,Microsoft 不能在大量的现有 code base 的压力下冒天下之大不韪。它仍然必须提供与 Direct3D 同样完美的 OpenGL 支持。

Dirtect3D 让 Microsoft 拥有一个完全可控的标准。但是这种可控度并非在 OpenGL 上不可达到。OpenGL 和 Java,HTML 这类标准不同。作为一个对功能覆盖十分克制的专业标准,OpenGL 天然的必须和平台相关的代码混用。作为 native code 的源代码级别标准,OpenGL 天然的必须重编译才能跨平台。所以 OpenGL 的开发者对于私有扩展和细节差异的容忍度要远远高于 Java/HTML 之类开发者。为了平台可控性而创立 Direct3D 的必要性是值得质疑的(当然平台可控并非 Microsoft 的全部目的)。

应该说 Direct3D —— 尤其是 DirectX 11 的版本 —— 是一个优秀的开发平台。但是对微软来说,现状是这样的:无法抛弃 OpenGL,所以必须维护两套并行的接口,加大了维护成本。对于可控性的收益来说,投入大大高于必要。Lock-in 开发者 code base 的目标基本失败。对于平台的创立者来说,这是一个教训:应该对 JVM 这种基于 VM 的跨平台方案保持距离,但是对 OpenGL 和 POSIX 这样宽松的源代码级接口进行分裂是毫无必要的。(附带说一句,Windows 的 POSIX 子系统面临类似的程度稍轻的窘境。)

OpenGL 随想(二):声明式与 MVC

2010/12/10

上篇的结尾提到了声明式 DSL 和 OpenGL 的汇编风格的差别和联系。其实我当时还联想到 model-view-controller 模式,这次另起一篇。

能有这样的联想是因为引入 OpenGL 的产品几乎都是 MVC 架构,而且代码中 OpenGL 相关的部分几乎完全被限制在 view 模块中。具体点说,在基于 Cocoa 的代码中,OpenGL 的使用几乎完全被限制在 NSView 子类的 drawRect: 消息中。在 Win32 程序中几乎完全被限制在 WM_PAINT 的处理函数中。也就是说,复杂的 MVC 架构的应用是用汇编风格来写 view 。另一方面,可以把 model 看成是 view 的声明式描述。

所以可以这样来解释 MVC 模式:每个 model 模块的定义都是一个针对当前应用定制的声明式 DSL ,每个 view 模块都是这个 DSL 的(可视化)解释器。OpenGL 的风格正好符合高效解释器通常需要汇编语言来编写这个惯例。在一个设计良好的 MVC 应用中,不仅是模块功能的分离,而且隐含了两种编程范式(paradigm)—— 声明式和命令式。

如果有一天真的出现了声明式的 3D 引擎又会如何呢?比如 Java 3D 当年就尝试过类似声明式的概念。我想也许到那时 view 模块会成为 MVC 模式中对底层引擎很薄的一层封装(而今天 MVC 中 controller 是最简单的一层)。又或许我们的应用永远需要一个比通用的声明式语言更定制化的 DSL ,而 view 就成为这两种声明式 DSL 的翻译器。