Posts archived in reading

UNIX 痛恨者手册[PDF]可以算是一本奇书了。一般的技术书,写作缘由大多是作者特别喜欢某样技术,兴高采烈地拿出来和读者分享。而此书的几个作者,都是因为恨 UNIX 恨到“人生长恨水长东”的境界了,于是乎搞了个邮件组,广泛收集各种愤怒,最后基于邮件组里面张贴的各种抱怨,编撰成了痛恨者手册这样一本书,来专门宣泄对 UNIX 的愤怒,也算得上是空前绝后了。

尽管这本书视角独特,以现在的眼光看,作者的抱怨中,真正属于 UNIX 固有问题的只占 50%,其他如对 sendmail 排山倒海的批评,对 C++ 的尽情嘲弄,实际上都不属于 UNIX 系统特有的。其他的 50%,则颇有历史意义,可以看到当年的 UNIX 系统是何其的“原始”。特别是对照现在的 Linux 来看,可以看出 Linux 作为当年 UNIX 的继承人,在文件系统,安全性,稳定性等等方面的巨大的进步。

除去一些对 UNIX 中具体 BUG 的批评,这本书的背后实际上是三种设计哲学间的交锋,我把这三种哲学叫做 MIT 哲学,UNIX 哲学和 GUI 系统哲学。“MIT 哲学”这个词,是借用那篇著名的 Worse is better 文章中的叫法。MIT 哲学的代表是 LISP 机器,即提供一个 LISP 环境的机器。这个机器提供给用户的,是优雅的编程环境,如统一的内存管理,统一的函数式接口,良好的文档等等,一切程序员所需要的,都给准备好了。但这个系统不管是作为个人计算机还是作为工作站都没有获得成功。

GUI 系统哲学从施乐的 Alto 开始,到90年代中期 Windows 95 出现之前,已经颇有气象,特别是在个人计算机领域,几乎所有的个人计算机厂商都在提供自己的图形界面操作系统。GUI 系统的哲学,是友好的用户界面和一致的使用体验。至于具体的功能,则委托给具体的应用程序实现。

而 UNIX 哲学,则像是一种开放式系统的哲学:除去提供统一的系统调用和标准工具外(POSIX),不强调系统的一致性。UNIX 像是一堆松散的积木堆起来的一个系统,在遵守 POSIX 标准的前提下(其实是个非常松散的标准)各个厂商都可以自己选择积木搭建系统。

UNIX 这种开放的,允许自由搭积木的做法,是和信奉MIT哲学的人水火不容的。这些用户在 UNIX 的因为开放造成的不一致性上尽情吐槽。比如说,UNIX 一个饱受诟病的缺憾是其命令行参数不统一。在命令行下,有的命令加 -h 是显示帮助,有的却是显示隐藏文件,还有的命令压根不接受 -h 参数。这样的问题,反映了 UNIX 在演化过程中缺少一个统一的规划。这在演化路径单一的其他操作系统上是不可想象的。再比如,UNIX 的计算模型很简单,即用 C 语言和 shell 对系统调用做一个胶水包装,不提供内存管理也不提供异常处理,文件系统也很低级,不支持文件恢复也不支持文件的元信息存储。而 MIT 的LISP 机器的计算模型和存储系统看上去都更加高级,统一的函数式接口,自动内存管理等等。用过 LISP 机器的人自然不习惯 UNIX 这种看上去“低级”的操作系统。结果是,用过 LISP Machine 的用户除了抱怨 UNIX 外,只能寻求在 UNIX 上构建一个新的层,来弥补 UNIX 的不足。这事情的一个结果就是造就了Emacs 这个怪兽,到最后几乎所有能在 UNIX 里做的事情,都能在 Emacs 里完成。这样,除了操作系统内核外,Emacs 完全取代了 UNIX 环境。Emacs 功能强大到大家都同意 Emacs 是个万能软件,而 vi 用户则开玩笑说 Emacs 是个缺少一个好编辑器的操作系统。大家都知道,Emacs 的作者正是从 MIT 出来的 Richard Stallman。

痛恨者手册的作者也是在 MIT 的 AI 实验室工作多年的技术人员。为了解释 UNIX 的成功,他借用了 Worse is better 中的说法(Garbriel 断言 C 语言和 UNIX 是终极计算机病毒),把 UNIX 归类为世界上第一个计算机病毒。书中提到,UNIX 和病毒的共同特征为:体积小,可传染多种宿主(可移植),变异快速等等。书中说, UNIX 的普及并不是因为它在技术比其他操作系统更加优越,只是因为可移植,可传染和变异快,才占据了很大一块用户份额。

这个解释我认为是相当精当的。相对于其他操作系统,UNIX 基于C书写,可移植和早期的免费分发方式,即使技术上不够好,仍然像流行性感冒一样蔓延。一传十,十传百,快速攻城略地。当时 UNIX 的风行程度可以从几个侧面来证明。八十年代初雨后春笋一般地冒出了很多新的UNIX公司,SUN 和 SGI 就是是借着 UNIX 成长起来的典型例子。他们短短几年间就靠 UNIX 工作站业务跑上了纳斯达克。微软和苹果是靠个人电脑业务起家的,各自都有自己的操作系统,却也跑到UNIX世界下注,都曾经推出过自家的UNIX发行版(分别叫做 XenixA/UX)。UNIX这个”病毒”在工作站和服务器上的寄生能力极强,直到后来演化能力和传播能力更强的“病毒” Linux 的出现,加上 .COM 泡沫破裂的一场大洗牌,才把 UNIX 的市场份额压了下去。Linux 则彻底继承了所有的“病毒”特性,除去原有的体积小,可移植外,通过开放内核源代码,造就了现在从超级计算机服务器到嵌入式系统无处不在的现状。从设备总量来说,世界上从未有一个操作系统如 Linux 如此成功。

可惜的是 MIT 哲学派本身没有成功的操作系统产品用来作为比较(除了Emacs这个运行在UNIX上的程序外),因此在批评 UNIX 上火力就欠缺了一分。为了写出一本厚厚的痛恨者手册砸向UNIX,就需要来自另外一派,即信奉GUI哲学的用户的愤怒。

这些用户的愤怒,主要集中在易用性上。图形界面操作系统的出现,本质上就是为了解决计算机的可用性问题。在图形界面系统出现之前,掌握计算机的使用需要的是阅读厚厚的手册。图形界面出现后,只需要几分钟的演示,普通用户即可操作计算机完成一些简单的任务。这种效率的本质提升,正是施乐的 Alto 和苹果的 Macintosh 的革命性所在。而 UNIX 所拥有的,是一堆两个字母的命令,不一致的命令行参数,和一个实际上不是为 GUI 系统设计的 X 图形系统。命令难记,X 又臃肿,即使有了这些仍然没有构成一个统一的桌面系统(所以后来才有KDE 和  Gnome),也难怪用户吐槽不已了。

在这类来自 GUI 用户的抱怨中,出乎我意料的一条是对UNIX管道的抱怨。主要的批评点在于管道作为一种 IPC 机制本身不够强大,包括管道不支持双向数据流(双向管道的用例也极少),只能把数据作为字节流而不能传递结构化数据,和指针等等。从传统 UNIX 用户的眼光来看,这些指责是很不公平的。管道的作用是串接程序的输入输出,将小工具串成强大的工具链。但管道并不是 UNIX 上唯一的 IPC 机制,UNIX 有其他的 IPC 机制来支持管道之外的功能。换一个角度看,要求管道支持双向通信,结构化通信等等,正是从 GUI 哲学出发的对管道的批判。在 GUI 世界,进程间的通信有了两种新的方式:1、把小程序全部集成到一个大的多线程窗口程序中来进行线程间通信; 2、通过在不同程序间复制粘贴对象。从这两个角度考虑,自然会要求 UNIX 管道能像线程间通信一样双向,以及支持有结构的对象而不是单纯的字节流。

GUI 程序的这一套新的进程间通信机制,改变了所在平台的软件架构。UNIX 的软件架构,是围绕软件工具(Software Tools) 的概念展开的,归结起来就是每个工具做一件事情,且做到最好的哲学。因为 GUI 程序本身的复杂性,把林林总总的功能,放入一个大程序中让各模块直接在一个进程空间里互相通信成了一个通行的做法。比如电子表格软件中的公式计算,无需代理到 bc 这样的外部计算器中,直接由内置的模块完成。在这种哲学的指导下,为了给提供全面的解决方案,各种商业程序都追求大而全,内置各种可能用到的功能,因此体积也越来越大。几百兆大小的商业软件不足为奇了。UNIX 痛恨者手册推崇这种只能算局部最优的程序构建方法,而反过来抱怨管道这个另一个局部最优不够好,在我看来是有历史局限性的。

总的来说,这本书代表了 UNIX哲学以外的其他两种哲学对 UNIX 尖锐的批评,是值得当成UNIX 发展史的一部分而一读的。

对航空感兴趣的人都知道,美国自取消阿波罗登月计划之后,其空间探索的主要精力,都放在了航天飞机主导的太空任务和国际空间站上。 和中国,俄罗斯的宇宙飞船不一样的地方在于,航天飞机是一种可以重复飞行的飞行器。

美国和前苏联都设计过航天飞机。他们设计的初衷都是一样: 航天器重复飞行能够降低制造和设计成本。 美国成功的做出了7艘航天飞机,而前苏联的航天飞机计划从来没成功的进入空间轨道过。 NASA 虽然成功的做出了航天飞机,限于当时的科学技术和认识的缺陷,还是没有避免一些致命的设计缺陷。 挑战者号和哥伦比亚号的事故都集中体现了这些设计缺陷。

Space shuttle Endeavor (Source: NASA)

航天飞机在发射时候,是将一个飞机状的东西(最后进入轨道并返回的飞行器,也叫轨道飞行器)挂载在一个巨大的土黄色的液氢/液氧燃料仓上。这个燃料仓旁边,又绑上了两个固体火箭增加推力。 发射后,固体火箭会掉入海中被回收,而巨大的土黄色燃料仓会坠入大气层烧毁。 不需要懂太多火箭知识就知道, 这等于是把航天飞机挂在一个火药桶上起飞,而且发射一旦出现什么问题,载有宇航员的轨道飞行器不能与燃料仓分离,固态火箭的使用更加使得火箭变得不可控(液态燃料可以随时关闭,固态燃料一点燃就没法熄灭) 。 这样,一旦火箭有了问题,爆炸和机毁人亡全都无法避免。挑战者号的事故就正好反映了这些设计缺陷。

同时,航天飞机使用表面瓷砖隔热,这些瓷砖在飞出和重返大气层的时候会被烧灼,可能会被损坏,所以航天飞机每次起飞都要重新装修一次,而且一旦在起飞的时候损坏,重返的时候就是听天由命了。哥伦比亚号航天飞机就是这样的事故。 航天飞机从设计到现在已经近30年了,因为当年使用的技术老旧,NASA 不得不养着一帮工程师来支撑航天飞机的飞行和维护。这些团队,不管航天飞机在天上还是在地下,都是要拿工资的。 于是,算下来,航天飞机的发射总成本,居然比发射一枚不回收的火箭还要高。

航天飞机有这么多的问题,是必然要退役的。这一点在哥伦比亚号事故后就变得尤其清晰。但是迄今为止,除了航天飞机和俄罗斯的大火箭,人类还没有其他方法把超过20顿的有效载荷送上 LEO (地球低轨轨道)的能力。而俄罗斯的火箭的载荷还是太小,送人没问题,送哈勃天文望远镜这样的大东西就要靠N次输送,对接和太空行走装配才能完成。之所以要巨大的火箭,因为克服地球重力井所需要的推力是如此之大。所以不管是登月也好,维护国际空间站也好,未来去火星探索也好,只要想让足够的东西逃离地球的引力,都离不开一个东西: 大推力火箭。 而美国除了航天飞机,居然就没有大推力火箭(送阿波罗登月的土星五号早就退役几十年了,当年的那些工程师都找不着了)。

小布什做总统的时候,带着和其他国家太空竞赛的意思,提出了 2020 年重返月球的计划。航天飞机自然不是送人和设备去月球的选择,于是,NASA 顺着登月的思路,提出了星座 (Constellation) 计划。 整个星座计划的总思路,是研发两枚大推力火箭,分开执行送人和送设备上天的任务,再将人和设备在月球轨道上对接。总所周知,在美国,人的生命是最重要的,因此载人的那枚火箭侧重于安全性,而载物的那枚火箭则侧重于大推力,和土星5号一样,有粗大的身材(见下图,胖的火箭旁边绑了两个固体的推进器,而瘦的火箭则完全是安全的液体推进,且有逃逸装置)。

Constellation Project (Source: NASA)

在我看来,星座计划是一个先天不足,又完全没什么创新的计划。首先,NASA 根本没有那么多钱来烧两枚火箭的研发。何况,月球美国已经去过好几次了,再去也是挖土。 最为关键的是,Bush 的星座计划里,居然要求 NASA 最大程度的重用原来阿波罗计划和航天飞机的技术和知识。 这就把一个本来可以做出革命性技术的项目,变成了重复一次阿波罗计划的项目,这样就完全失去了技术转化和民用化的巨大前景。 本来美国政府就已经是赤字累累了,再烧这个钱,完全没理由没价值。事实也的确如此,在布什任期,NASA 并没有在星座计划上按照时间表研发出对应的火箭技术,也没有看到什么革新的技术用在星座计划上。

Obama 总统上台后,委托一个由航空专家组成的小组对美国下一代空间探测计划做了一个总的评测。这个委员会对星座计划的意见是:工期拖沓,超过预算,没什么创新。于是,Obama 政府顺着这个,在2010年一月份的时候起草了一个提案,主要的思路是放弃重返月球的计划,跳过月球直接研究火星和小行星的机器人和载人探测计划,航天飞机退役,把往国际空间站等低轨轨道送人和设备的任务交给商业化的航空公司托管,不依赖于航天飞机技术发展下一代大推力火箭。平心而论,这个计划是完全可行也有创新意识的,而且可以降低不少成本。特别是现在私人航空公司送一个人上地球低轨轨道完全不费力气,况且美国作为月球俱乐部的唯一一个成员,和中印俄日这几个国家比谁在21世纪先登月一点意思没有,不如探索太阳系内其他星球。 这一点完全秉承了二战时候美国海军攻占西太平洋时使用的蛙跳跨岛战术,跳过月球,宇宙还很广阔。

当然,一个提案要想通过,还得参众两院投票通过才行。NASA 在航天飞机和火箭研发上已经雇佣了很多人,如果将 NASA 的一些项目外包,或者不依赖于NASA 现有技术,就意味着 NASA 雇员的失业。 在美国,失业和地方经济和选票都是直接关联的。因此,这个法案自然会得到 NASA 基地所在的那些州的议员的反对。加上普通人对放弃登月,感情上完全接受不了(想想中国还计划2025年登月呢),觉得是放弃了美国在太空探索上的领头羊地位,所以这个提案直接被枪毙了。

于是,参院的部分议员修改了原来的提案,这就是昨天参院刚刚通过的提案。其中,Obama 政府做了不少的妥协,原来计划的投资给私人航天的三十多亿美元砍成了16亿,国际空间站延长服役几年,航天飞机延长服役一次(这样NASA的配套工程师不至于瞬间失业),登月计划取消,大推力火箭换个名字继续开发,准备火星和小行星的机器人和载人探测。在上篇文章里我说了,载人火星探测要想成功,没有新一代的推进技术是痴人说梦。NASA 现在没有了登月的压力,应该可以潜心开发下一代的可以让人在太阳系自由穿梭的推进技术。 到时候,人类就真的摆脱了地球的重力的限制了。

跨过月球,2030年登上火星,这就是美国下一代空间计划的大体意思。

(本来想写 Jolt 大奖的八卦的,发现读书太少没法写,只能写成扯蛋文了)

在互联网,特别是 web 出现之前,信息的比特是通过其他网络传播的。在程序员之间,那时候大部分小程序都是直接贴在杂志上的,而大的程序则通过人之间互相拷贝来实现。

在 杂志上贴可以运行的代码和实现技巧对于我们现代人来说已经不是什么潮流的东西了,可对当年的程序员来说,每月一期的杂志简直就是无价的宝贝--上面充满了 其他天才写的几十行的小游戏和小工具,还有实现技巧。 在计算机科学中,CACM 可算是杂志上贴代码的巅峰。在 Communication of ACM 还是非常高深的期刊的时代(现在变成科普期刊了,以前大多是专业文章),因为人人都在这个杂志上贴代码,所以 ACM 决定对算法编号,这些编了号的算法日后就成了一个巨大的算法库,被称作 CACM Algorithms. 有兴趣的读者可以自己去看看这些千奇百怪的算法 ( http://calgo.acm.org/). 这套算法命名体系是曾经是如此的通用,覆盖又是如此的广,以至于现在仍然会有黑客直接说 algorithm 232 (CACM 232号算法是一个堆排序的实现) 而不是 heapsort 等等。

在美国,这样的为开发人员服务的杂志全盛时期并不长,尤其是 mailing list 和互联网普及之后,就在逐渐的消亡。web 出现之后,这些杂志很快就被全部打败了。 我们这里要八卦的 Jolt 大奖,就是由曾经这样的一个为开发人员服务的杂志发起的。这个杂志的名字叫做 Software Development。 可以想像,随着软件工程往大方向走,这个杂志除了放点技术评论和书评,基本上很难和互联网以及mailing list 抗衡。 软件工程复杂化一方面造成原来愿意给杂志写稿的人更加愿意写书,另一方面造成原来愿意读杂志的人,要不去上网获取快速知识,要不干脆捧起大部头。 所以,这样一个杂志,在互联网时代,就只能以技术和行业评论为利基了。 SD 杂志在可以说在这个利基上做的非常好,最有名的就是推出了 Jolt 大奖。

Jolt 是美国的一种能量饮料,据说喝了能活力百倍。Jolt 大奖就是奖励给那些给程序员提供活力,生产率的工具,产品,图书,理念,最佳实践等等。 因为 Jolt 从一开始就邀请专家组来提名评审,而非让普通人投票,所以有很浓重的奥斯卡色彩,这 Jolt 大奖也被成为软件产业的奥斯卡。 Jolt 大奖其实只是一个概称,常常一个 Jolt 奖, 三个 productivity 奖,基本可以理解成一个一等奖三个二等奖。 读者都知道软件行业里图书也好,工具也好,每年都层出不穷,自然 Jolt 奖是一个竞争激烈的奖。Jolt 奖从 1990 年开始颁发,到现在已经将近 20 年了。我整理了一份这20年 Jolt 奖的所有图书的单子,有心的读者可以看看,应该能看出这20年软件工程发展的脉络的。 Jolt 奖现在已经涵盖到 12 个分项(所以每个月提名一项),我觉得一个追求不落后时代的开发人员还是可以时不时看看 Jolt 奖的提名的那些技术和图书的。

Jolt 奖并没有能够拯救 SD 杂志被 Web 杀死的趋势。 在 2006 年左右的时候,SD 杂志把出版部门全部打包卖给了 DDJ, 另一个以贴 BASIC 代码起家的杂志,自己专心做 web 新闻站了。 DDJ 也没有撑过几年,于 2009 年,把出版部门全部出让给了 Information Week, 仅每月在杂志中间留这么10页左右的技术评论。 早在互联网杀死新闻周刊之前,互联网就把这些技术杂志全部杀死了。 好在 DDJ 这类杂志还能继续在网上运营,所以 Jolt 大奖始终还有评选。 只是互联网成了程序员的新 Jolt,这个 Jolt 是如此的无孔不入,以至于传统媒介都被杀得丢盔卸甲了。

本文尤其欢迎图老师留言讨论。

待月西厢下,迎风户半开。隔墙花影动,疑是玉人来。

最近 twitter 上最流行的一个关键词是”西厢计划”. 这个计划名字取得很浪漫,客户端叫做张生,对,就是西厢记里面那个翻墙去见崔莺莺小姐的张生;显然,服务器端必然叫做崔莺莺。客户端的张生是最重要的部件,可以不依赖于服务端工作。

我是个特别好奇的人,遇到好玩的总要学习一下看看是怎么弄的。因为西厢计划的作者只是简要的介绍了一下原理,其他报道又语焉不详,我当时就觉得很好奇,花了昨天一个晚上详细读了一下源代码,终于知道怎么回事了,觉得原理非常漂亮,所以写篇文章介绍总结一下。

先说大方向。大家都知道,连接被重置的本质,是因为收到了破坏连接的一个 TCP Reset 包。以前剑桥大学有人实验过,客户端和服务器都忽略 Reset, 则通信可以不受影响。但是这个方法其实只有理论价值,因为绝大多数服务器都不可能忽略 Reset 的 (比如 Linux, 需要 root 权限配置iptables, 而且这本身也把正常的 Reset 给忽略了)。只要服务器不忽略 Reset, 客户端再怎么弄都没用,因为服务器会停止发送数据,Reset 这条连接。所以,很多报道说西厢计划是忽略 Reset, 我从源代码来看应该不是这样。在我看来,西厢计划是利用了墙的一个可能的弱点–墙只在连接发起的时候把一个 TCP 连接加入监听序列,如果墙认为这个连接终止了,就会从监听序列中去掉这条记录,这样,这条连接上后续的包就不会被监听。西厢计划就是让墙“认为”这个连接终止的一个绝妙的方法。只要墙认为这个连接两端都是死老虎,墙就不会触发关键词检测,其后所有的数据,都不存在连接被重置的问题了。

如何让一个连接置之死地而后生,就是西厢计划那帮黑客神奇的地方了。这也不是一日之功。 首先,这帮牛人发现,墙的是一个入侵检测系统,把含有关键字的包当成一种“入侵”来对待。采取这种设计有很多好处,但缺点是入侵检测系统可能具有的问题,墙都可能有。西厢计划主页上那篇著名的论文就是讲这些七七八八的漏洞的。可以说处理这些七七八八的漏洞是非常困难的,迫使墙的设计者“拆东墙,补西墙”。这样补来补去,外表看起来好像很牛逼的墙,其实有很多本质上无法简单修补的漏洞,其中有一个致命的,就是 TCP 连接状态的判定问题。 出于入侵检测系统这种设计的局限,墙没有,也没办法准确判定一条 TCP 连接的状态,而只是根据两边收到的数据来“推测”连接的状态。而所有的关键词检测功能,都是基于“连接还活着”的这个推测的结果的。因为墙的规则是在连接发起的时候开始对这条连接的检测,在连接终止的时候停止对这条连接的检测,所以,一旦对连接的状态推测错误,把还活着的连接当成已经关闭的连接,墙就会放弃对这条连接上随后所有的包的检测,他们都会都透明的穿过墙的入侵检测。

上面只是想法,具体到 TCP 协议实现这一层,就要只迷惑墙,还不能触及我要通信的服务器。最理想的情况下,在任何有效通信之前,就能让墙出现错误判断,这些,就需要对 TCP 协议有深刻理解了。西厢计划的那帮黑客,居然真的去读 TCP 几百页的 RFC,还居然就发现了方法(这里我假设读者都知道 TCP 的三次握手过程和序列号每次加一的规则)。 我们都知道,三次握手的时候,在收到服务器的 SYN/ACK 的时候,客户端如果发送 ACK 并且序列号+1 就算建立连接了,但是客户端如果发送一个序列号没 +1 的 FIN (表示连接终止,但是服务器知道,这时候连接还没建立呢, FIN 这个包状态是错的,加上序列号也是错的,服务器自己一判断,就知道这个包是坏包,按照标准协议,服务器随手丢弃了这个包), 但这个包,过墙的时候,在墙看来,是表示连接终止的(墙是 ma de in china, 是比较山寨的,不维护连接状态,并且,墙并没有记下刚才服务器出去的 SYN/ACK 的序列号,所以墙不知道序列号错了)。所以,墙很高兴的理解为连接终止,舒了一口气去重置其他连接了, 而这个连接,就成了僵尸,墙不管你客户端了,而这时候,好戏才刚刚开始。

事实上,墙是双向检测的(或者说对每个包都检测的),因此,对服务器和客户端实现相同的对待方法,所以,墙不管客户端还不行,假如服务端有关键词传给客户端,墙还是有可能要发飙的(这里说有可能,因为我也不知道)。所以,最好的办法就是,让服务端也给墙一个终止连接的标志就好了。可是这个说起来简单,做起来难,怎么能让不受自己控制的服务器发一个自己想要的包呢? 西厢计划的那帮黑客,再次去读几百页的 RFC, 令人惊讶的发现,他们居然在 RFC 上发现了一个可以用的特性。我们上面说了,三次握手的时候,在收到 SYN/ACK 后,客户端要给服务器发送一个序列号+1 的ACK,可是,假如我不+1呢,直接发 ACK 包给服务器。 墙已经认为你客户端是死老虎了,不理你了,不知道你搞什么飞机,让这个 ACK 过了。可是服务器一看,不对啊,你给我的不是我期待的那个序列号, RFC 上说了,TCP 包如果序列号错了的话,就回复一个 Reset. 所以,服务器就回复了一个 Reset。这个 Reset 过墙的时候,墙一看乐了,服务器也终止连接了,好吧,两边都是死老虎了,我就不监听这条连接了。而至于客户端,这个服务器过来的 Reset 非常好识别,忽略就是。随后,客户端开始正确的发送 ACK, 至此,三次握手成功,真正的好戏开始,而墙则认为客户端和服务器都是死老虎,直接放过。所以,张生就这样透明的过了墙。 至于过墙以后所有的事情,《西厢记》里面都有记载,各位读者自行买书学习。

现在的西厢计划客户端,即“张生”模块的防连接重置的原理就是这样,服务器端,即莺莺模块的实现也是类似的。防DNS那个,不懂 DNS 协议,所以看不懂。我猜想,因为开发人员都是黑客,所以自然喜欢用最经得起折腾和高度定制的 Linux 开发。 现在看西厢计划的实现,因为依赖于 Linux 内核模块 netfilter, 在 Linux 上如鱼得水,但往其他平台的移植可能是个亟待解决的问题。 我觉得,在其他平台上,可以通过 libpcap 和 libnet ,在用户态实现相同的功能,就是有点麻烦而已,有兴趣的懂网络的可以照西厢计划原理,在家自行做出此功能;当然,全中国人民都用 Linux 最好 :)

PS 1: 据说是西厢计划一个作者画的原理图:http://img.ly/DIi
PS 2: 我对 TCP 的理解仅限于课本,如果上面的对技术的理解有错,请大家指出。
PS 3: 有些漏洞,可能是设计上本质缺陷,不是那么容易修复的。
PS 4: 除了最后一个图,本文没有其他相关链接,如需相关资料,自行 Google。

一周前我和 Tinyfool 闲聊苹果操作系统,都认为对于开发人员来说,苹果操作系统(Mac OS)是上佳的选择。 Tinyfool 笔头很快,当即就写了一篇长文章, 我则笔头很慢,今天才全部码好。 他的文章的主要切入点在于 Mac 平台作为目标开发平台的优势,而我这篇的切入点主要是 Mac OS 作为一种开发工具的优势。

开发人员的趁手工具
对于开发人员来说,所有的开发工具的最大的用途,就是最大限度的提高开发人员的生产率 (productivity) 和创造力(creativity)。在我们这个时代,使用 GUI (图形界面) 是一个提高生产率的好手段。虽然上一代的那些 UNIX 开发人员的确不需要 GUI。一个屏幕,一个键盘,一个编辑器,在陋巷,人不堪其忧,也不改其乐的黑客比比皆是, 但二十多年过去了, 现如今开发环境发生了巨大的变化。 比如说,相比较于当年程序员使用的基于文本的环境,在 GUI 下格式丰富的文档显得更直观,阅读体验更加好;就算工作中不需要开发任何 GUI 程序,现代开发人员也会使用 GUI 来完成网页图片和文档阅览等等。 因此,即使是最传统的用命令行的开发人员,其实也能沾 GUI 的光。 比如说现在最好的终端程序,都是 X 下模拟的,因为这些模拟的终端的出现,一些复杂的可视化功能可以在这些终端中实现了,比如 Unicode 的显示(rxvt-unicode)等等。

对于开发人员,拥有一组非常好用的,能够最大程度的提高生产率的开发工具乃是一大人生梦想。那么,这套开发工具从何而来呢? 大体来说,这些工具来自于三个方面: 1. 通过系统和单一的应用软件提供的;2. 通过搭配使用各种应用软件 3. 通过定制和改变现有的应用软件。 这三点,对于 UNIX 开发人员是再熟悉不过的了, 无非就是写脚本,走管道而已。 所以,在前 GUI 时代,这一套哲学非常盛行, 开发人员都知道,需要通过安装脚本解析器,写一些的脚本,配置一些环境等等,才能把刚出厂的 UNIX 系统,改造成自己使用起来得心应手的系统。 基本上任何一个使用 UNIX/Linux 系统多年的人,机器里面都有各种各样的“私藏”的脚本。离开了这些脚本,他的效率会大打折扣。

GUI 时代传统的丧失

上世纪 80年代的时候,GUI 时代和个人计算机普及的时代降临了。从此,计算机变成了个人电脑,历史上第一次,计算机不是专为开发人员设计,而是为了普通用户设计。普通用户的需求就是完成一个一个的现实问题,软件产业提供的解决办法就是为用户提供一个一个的应用软件,而不是让用户自己一行一行的编程和写脚本,巨大的软件需求瞬间成就了一个巨大的软件产业。 这样的一个间接后果就是,对于普通用户来说,让一台计算机变成能够帮助自己完成任务的“个人计算机”的唯一手段,就是叠床架屋的不断的装各种应用软件。

我们可以用一个简单的例子说明这种使用模式。 我们都知道,安装 Windows 系统的一个经验原则是把操作系统和应用程序分成两个逻辑盘,一个在 C 盘,一个在 D 盘。这个磁盘分区的经验原则不光网吧老板知道,连我大学里面只会点鼠标的那些女同学都知道。为什么有这个奇妙现象呢?其实,这是由 Windows 系统的用户的典型使用模式决定的。 在 Windows 系统上, 应用程序和文档是关键,操作系统只是一个随时可以重装的东西而已,所以干脆两者分开,互不影响。在这样的使用模式引导下,Windows 系统上格盘重装是非常低成本的,只要文档不丢,应用程序不丢就行。这种使用习惯,浪费了多少 geek 男美好的时光为人重装系统,又促成了多少美妙的姻缘 :)。 总之,在 GUI 时代,要解决一个问题,就装一个应用程序。至于应用程序之间的通信,和用非键盘鼠标的方法控制应用程序等等,都不再是要考虑的问题,有这样的需求的人成了非主流,非主流到以致于主流的操作系统和应用软件都不让你这么干了。 操作系统把所有其他的路都封死,就是明摆着告诉你,要想某样功能,请出门买软件。

Smalltalk 的启示


其实 GUI 时代原本不应该是这样的。 我们都知道,GUI 原本是施乐的 Alan Kay 那一帮人做科研做出来的,Bill Gates 和 Steve Jobs 各自到施乐”抄袭” 了一部分过来,于是窗口啊按钮啊就到处都是了。 他们都看到了图形界面和面向对象的形, 看到了图形界面就是把按钮图标等等对象放好,然后鼠标点击拖动等等这些表面的东西。 因为所有的 GUI 界面都是从文字界面起步的,所以所有的 GUI 程序,其实就是原来的可执行程序的包装。 C++ 这个语言的出现也很讨巧,把 C 包装成了一个面向对象的语言,包装对包装, C++ 很讨巧的适应了把可执行程序 GUI 化的趋势, 成了 GUI 时代的主流开发语言。从表面上看,只要运行这些可执行的程序,就能够看到图形界面,就能够用鼠标点击操作他们,可是这些东西的底层,都是一个编译过了的可执行程序,原先 Smalltalk 中的那些运行时环境啊,对象容器啊,都统统不见了,所有的图形界面程序,还是直接运行在计算机的 CPU 上,而不是一个虚拟的面向对象的容器上。而这个面向对象的容器(也叫做“运行时”或者“运行环境”),才是 Smalltalk 的神。 简单的说,Smalltalk 本身具有一个面向对象的运行时,所以即使到了执行的时候,里面所有的对象还是可以互联互通的。 而 C++ 写出来的程序,除了编译之前是面向对象外,只要一编译,就全部变成机器码,和对象就再也没有任何关系了,也就不存在运行时去动态的查看(inspect) 和改变(modify) 这些程序对象的说法。 总之,因为历史的局限,这些 GUI 的平台,都是渐进的照猫画虎的演变的,所以没有一个平台像 Smalltalk 那样细致地考量过对象的互相通信的问题,再加上我们上面说了,反正扩展系统的方法就是引入新的应用软件而已,本身也没有互联互通的需求,所以这种抛弃运行时的,不让对象被外部程序控制的实现方法也无所谓不好。

可是开发人员不是普通用户啊,他们依然要改造计算机成为自己的工具的。在现有的现有工具不能解决问题的时候,要不然自己重新发明轮子,要不然就复用现有的一些工具,或者重新按自己的需求重新配置这些工具。 所以,和一般用户不一样,开发人员需要这些 GUI 的可配置性,也需要这些 GUI 程序之间的互联互通。 用黑话来说,第一个问题关系到 GUI 应用程序的脚本化, 第二个问题关系到 GUI 程序之间的进程间通信。 这两个问题,说起来简单,但都牵扯到 GUI 系统的根本设计问题。 历史在这里开了一个不大不小的玩笑,把这个唯一的机会给了 Mac OS X。其他操作系统,都因为这样那样的原因,在这两个问题上没有很好的解决方案。

进程间通信,苹果的方案

花开两朵,各表一只。我们先说 GUI 程序的进程间通讯的问题。 所谓的进程间通信 (IPC),就是两个程序之间的信息共享。 我们都知道,*nix 的一个强大之处就在于管道,管道是最简单,最廉价也是最常用的 *nix 进程间通信的方法。在 GUI 时代,最常用的 IPC 机制成了剪切板和鼠标拖放操作。这两个操作虽然都很直观,但都要人操作,离开了人,程序根本无法自动完成进程间通信。 而要工作效率的提高,就是要让计算机离开了人的干涉,也能完成这些任务。为了自动化这些任务,操作系统就不能简单的绘制窗口然后万事大吉了,它必须要知道哪些程序在运行,哪个运行的程序可以给哪个程序发消息通信等等,比如说,如果我们想自动的在阅读器里面选择一个词送给字典程序查释义,计算机就需要知道字典程序在运行的时候可以接受一个字符串,但是不可以接受图片。如果我们把字典程序抽象成一个可以提供“查字典”服务的对象的话,毫无疑问,如果想要向字典程序发送字符,必须首先知道字典程序能够接受什么,用什么方式把这个单词发送给字典等等。 所有的这些信息,都必须由操作系统托管才行(不可能每个应用程序里面都要记着字典这个程序能接受字符串不能接受图片,这样每个应用程序都要记下所有其他可能的应用程序的信息,这是一个平方级别的关系,需要开发人员开发一个程序的时候还要兼顾其他所有程序,这显然是不现实的)。用行话来说,必须要有一个统一管理的运行环境,来管理这些程序之间的互相通信问题。 我们上面说了,Smalltalk 的神在于一个统一的面向对象的运行时,使得所有的应用程序能互联互通。 可是所有平台上的 GUI 程序的演化进程都没有走这条路,而是只把外表给模仿走了;有的平台即使想做互联互通,也做得不彻底(比如微软的 OLE,COM 等等)。

是好东西,总会发光的。 但是要想让这个好东西被新的操作系统全盘采纳,要想让一个系统能够从底层到上层全部采用统一的运行环境,就要扔掉很多的历史包袱。甩掉这种历史包袱,对于任何操作系统都是不容易的。如果我们回到当年,一定会幻想,要是有个神人,能够不管市场也不管现有平台,从头打造一个没有任何历史包袱的干净整洁的 GUI 系统该多好。 历史就是这么戏剧,还真就安排了一个人,做成了这件事情,这个人,就是那个斯蒂夫乔布斯。

1985 年,乔布斯被苹果扫地出门,成立了 Next 公司, 一心想要做出质量上乘的 GUI 计算机系统。 历史给了乔布斯一个全部从头做的机会。这一次,乔老师和 Next 的开发人员意识到,光照搬 Smalltalk 的形是不行的,要连它的神也拿过来,重头设计进程间通信和 GUI 系统。 在内核层面,他们用了 Mach 这个为 BSD 设计的微内核。 这个操作系统内核就是为了替换已经过时的 UNIX 内核而设计的,其中的一个核心设计哲学就是重新设计进程间通信; 虽然现在基于微内核的操作系统已经不是什么潮流(为此 Linus 和 Tanenbaum 吵了一场著名的架),但在相比较于当时 UNIX 系统的内核(此时 Linux 还没出现的,UNIX 内核只有 BSD, Bell, SUN 等几套),Mach 算是一个高的起点。在这个内核上,Next 公司的工程师开始构建面向对象的基础系统。 这套系统在 Smalltalk 中已经有了蓝图,因此这些工程师以 Smalltalk 为蓝图,先设计了一套基于 C 的语言,也就是 Objective C,照搬了 Smalltalk 的经典的 [对象 消息: 参数] 语法。 (我个人不喜欢 Objective C 这个语言,Smalltalk 是一种纯面向对象的动态类型的语言,Next 公司当年完全有机会用 Smalltalk 语言的,如果用了 Smalltalk,现在的 Cocoa 框架还会更加漂亮,代码更加干净;用 Objective C 这个自创的语言,不知道是不是因为专利的考虑,反正 Objective C 这20年的所有创新,就是在慢慢的更像 Smalltalk 而已,Java 和 Ruby 这几年也是不断的从 Smalltalk 拿东西)。有了内核,有了语言,Next 构建了一个纯的面向对象的运行环境和类库(和 Java 和 .Net 的统一类库想法类似,只不过超前了十几年), 这套类库,在当时叫做 NextStep, 所以所有的类名前面都带有 NS 前缀,无比丑陋。可惜的是,当年这个超越时代的类库太阳春白雪了,话说 Smalltalk 超越了时代 20年,所以90 年代中期的时候, 程序员才想起来当年 Smalltalk 的好,出现了 Java Ruby 等等受  Smalltalk 启发的语言。 乔老师虽然落后了 Smalltalk 5 年,却领先也业界 5-10 年,所以在 1995 年的时候, Windows 95 卖疯了, 乔老师的 NextStep 却没动静,只能把这个类库重新打包当成 Web 类库卖卖,即 WebObjects。这倒是无心插柳,生意不错,因为当时的 Web 开发已经吃尽了没有一个统一的运行环境的苦头(这也是日后 Java 风行的原因)。 我们说,是金子总要发光的,但是前提是要 (1) Next 再等几年,等业界回过神来认识到它的好处,(2) 获得一个主流的操作系统支持,把底层全换成乔老师的东西。 乔老师也知道这两个条件,所以加快了和 SUN 合作的步伐,想要把这套系统放到 SUN 的工作站上。 但是 SUN 本身有很强的底层技术,那段时间又狂推 Java, 所以其实乔老师在 SUN 这条路上胜算不大,况且 SUN 自己内核技术很强,所以肯定要肢解 NextStep 把内核重写,如果不和 SUN 玩,一来Next 这家公司能够多撑 5 年都是问题,二来几乎每家做个人计算机的公司都倒戈微软了,其他做工作站的公司又都有自己很强的底层技术,不可能用乔老师的玩意儿的,所以看起来乔老师和他的阳春白雪好像前景不妙。 可是天无绝人之路,放眼看当年的市场,只有一家公司没有倒戈微软,又没有很强的底层技术,又和乔老师有一些渊源,历史就是这么戏剧,这家公司就是把乔老师扫地出门的苹果。

90年代中期苹果的日子很不好过,个人电脑市场败给了 Wintel 联盟,新兴的市场上成绩也一塌糊涂,投资人也不糊涂,把当年让乔老师扫地出门的 Sculley 也扫地出门了,随后就把乔老师的公司给买了回来,让乔老师复职负责复兴苹果。 所以,上面我们说的两个条件就这样突然的满足了: 第一,他现在是老大了,所以可以彻底的把原来苹果的系统推倒重来,用自己的新家伙;第二,原来 Next 公司的那帮工程师不要担心失业了,现在由苹果负责发工资了,所以,正好可以让这些人着手改造苹果系统,主要的工作就是用自己带过来的新系统取代苹果的旧系统,并且让新系统的图形界面和旧系统保持风格的一致。 这个工作,从1995年 Next 被收购,到 2001 左右的时候才做好,这6年的时间里, 乔老师也顺带让苹果重新盈利了。

2001 年发布的 Mac OS X, 是苹果操作系统的第十代,完全基于了乔老师在 Next 开发出来的那套类库,所以自然的,具有了一个统一的面向对象的运行时。 这个运行时和类库系统,Mac OS X 把它叫做 Cocoa。其实 Mac OS X 刚出来的时候也不怎么好,不过依赖于这套设计精良的底层系统,Mac OS X 的迭代开发周期要比其他操作系统短多了 (仅慢于Linux, 不过 Linux 只有内核部分). 在短短的 8 年里,Mac OS X 就搞出了 7 次大的版本发布。 虽然我们看 Mac OS 好像从 10.0 到 10.6 只是次版本号在进步, 其实每次都是一个 major release, 大致相当于从 Window 95 到 Windows 98 或者 Windows 2000 到 Windows XP 这样级别的升级。 这样的发布却不改主版本号,一方面是从市场上考虑,另一方面也的确说明 OS X 的底层已经处于一个相对稳定的状态。 有很多 Windows 程序员非常推崇 .Net。 是的,.Net 的确是一个非常好的框架,可是想像一下,苹果在1995年的时候就有了一个统一的运行时,加上这么多年所有的程序都在这个统一的框架上开发,如果论在 Mac OS X 这个平台上的经验积累,应该说 Cocoa 社区是比 .Net 社区更加成熟的。

应用程序脚本化

光有进程间通信的系统还不能算是一个完全成熟的 GUI 系统,因为进程间通信依然是相对底层,而 GUI 上的应用软件是层出不穷的,不可能任何问题都跑到底层用进程间通信解决;所以,要想让 GUI 系统进化到易用和易于定制的水平,就需要开放对 GUI 程序的脚本控制。只有 GUI 程序能被外部控制了,才能真正的达到搭配使用 GUI 系统的效果。 其实,一旦有了一个统一的运行时,只要开发应用软件的时候统一设计一下脚本接口,用脚本控制 GUI 程序应该不难。 比如说,微软的 Office 系列套件, 就完全可以用 VBScript 去控制。 可惜的是,没有一个系统能够实现全系统的控制。 要实现全系统的控制,不仅仅要这个系统能够提供底层的支持,更重要的是要能说服所有的开发人员,或者说让所有的开发人员养成开放脚本接口的好习惯。 从技术上来说,这不是太大的问题,只要开发人员按照统一的脚本通信协议,实现特定的接口就行了,可是,如果一个平台上开发 GUI 的方法太多,开发人员只选自己喜欢的来,这种标准就不可能统一。 比如说 Linux 上 KDE 和 Gnome 都有自己的脚本化系统,可是开发人员有的用 KDE, 有的用 Gnome, 有的干脆两者都不用,这就谈不成有一致的接口。 一个平台要想有一致的脚本控制接口,除非 (1). 这个平台上就一种 GUI 开发方法,自古华山路一条,要不不做,做出来的东西就只能是标准的接口; (2). 这个平台上大部分的,主流的应用软件,都实现了这个脚本接口,这样因为这些程序的拉动,其他 GUI 程序想要融入这个平台上现有的应用软件的圈子相互通信,那也就必须要实现这个接口。 在 2000 年的时候,又只有一家公司能够同时满足这两个要求,就是苹果。 微软部分做到地了这两条,基本上用 VBA 统一了 Office 的控制,但是跳出 Office,微软的 OLE 对象模型几乎没有任何用武之地,与之捆绑密切的 VBA 自然无人问津。 不过据一些在金融行业工作的朋友说, VBA 能够大大提高 M$ Office 的生产率。

GUI 脚本化不是一夜之功,特别是我们说要做出统一的脚本接口,能兼顾各种程序的需求,这就完全不是一两年的时间能够搞定的,总需要很多年的技术积累和设计取舍后才能收敛到一个相对稳定成熟的系统, 而苹果,居然很神奇在十几年前就有这方面的经验,苹果再次怎么这么幸运呢?

在 80 年代后期的时候,苹果机上有一个非常超越时代的软件,叫做 Hypercard。 这个软件我曾经在上一代苹果上玩过,具体的思想就是你可以存储一张一张的“卡片”,这些卡片上面可以放置多媒体的声音,图像文字和其他对象,基本上就和现在网页一回事,唯一的区别就是这些卡片都存在本机。 在没有 Powerpoint 这类软件之前,这个 Hypercard 的软件可以用来做课件,做幻灯片演示等等,是个极其强大的工具。 为了让用户可以定制这个卡片,这个程序提供了一套非常强大的编程系统,叫做 Hypertalk。 因为这种编程语言是给普通人而不是程序员用的,所以你会感觉根本不是编程,而是写英语。这套东西,虽然本质上也是从 Smalltalk 学来的,但是用英语语法的方法编程的确是一个全新的思路,苹果把这个给普通人编程的语言发扬光大了,用更加贴近自然语言的方法重写了语言和文档,模仿 Hypertalk 系统,发布了一个跨系统的脚本控制语言,叫做 Applescript。这个语言就和自然语言没什么区别,比方说, 获取窗口的大小不再是
window.getSize()
而是
get size of window

显示第 22 段的 第一个单词不再是
print(paragraph[22].getWordByIndex[0])
而是
print the first word of paragraph 22

更狠的是,你还能用法语和日语写。 80年代后期的时候和整个 90年代初期,苹果基本上已经被 PC 机逼到墙角了,只剩下出版行业,设计行业等等专业的行业因为应用软件和图形处理能力的关系,依旧在守着苹果机。 两个行业的用户都需要自动化的 GUI 控制,但是编程都不怎么样,于是,这些应用软件的开发商也主动掺合加入 Applescript 旗下。 在90年代乔老师没有加入前,苹果自己把 Finder 全部脚本化,出版业的 QuarkXPress 和 Filemaker 也都完全脚本化,等乔老师入主苹果后,基于 Cocoa 的新技术,苹果一口气在 Mac OS X 上推出了 Safari, iTunes, iPhotos 等等软件,一股脑儿的全部脚本化了。 在别的公司都可望而不可求的历史机遇,又是被苹果给抓住了,一股脑儿全部塞进了 Mac OS X。这下,所有的第三方开发的工具,如 Firefox, Adium 这些,其实本来都不是苹果开发的,也没有太强的苹果渊源,但是 Firefox 要读 Safari 书签吧,哈,那就用 Applescript 吧,所以, Firefox 也逼着脚本化了(这个在其他平台上都不存在的事情)。 Adium 也是,这个聊天软件想要把 iTunes 正在播放的歌曲当成状态信息,好呀, Applescript,所以,也被带着脚本化了,而在 Linux 上的对应产品 pidgin 就没有这么脚本化。 所以,苹果平台已经成了一个惯性,你不想脚本化,就不带你玩,看你还脚本化不?

结语

我们都知道, UNIX 时代的主要哲学是提供给开发人员一组小巧精美且可以任意搭配使用的小工具,也就是所谓的 Software Tools, 然后任由开发人员由此出发,自己搭建自己的工具,打造自己的瑞士军刀。而开发人员所用的操作系统的目的,要不就是提供这样的一组开发工具,要不就是为这样的开发工具提供一个便利的平台,使得这样的工具变为可能。如果说 UNIX 是命令行时代的一个易于改造成 “自己的操作系统” 的操作系统的话, Mac OS X 就是 GUI 时代的这样的一个操作系统。 即使是从应用软件的层面看, Mac OS X 的底子好,更加容易出精品软件,所以即使仅使用应用软件,开发人员也应当优先考虑 Mac OS X。

附A: 相对正确的 Mac OS X 使用习惯

0. 一定要装 Quicksilver 或者用“服务”,否则就是把苹果当 Windows 用。
1. 在苹果计算机上,因为有服务和 Quicksilver 这样的工具,90% 的程序间的拷贝粘帖都是可以避免的。
2. 剩下的 10% 的程序内的拷贝粘帖,如果用一个好的编辑器的话,又可以省略掉 90%。

附B: 为什么 Linux 系统在这个方面还不够好

第一, Linux 上的 GUI 子系统,其实不是 Linux 的一部分,而是 X 和上面的 KDE 以及 Gnome
等等。 这几年,这些系统终于开始统一管理一个面向对象的运行环境了。可是这两个系统都是用C++ 所写,所以免不了费很大的力气才有了运行时信息,绕了一个大弯路,如果一开始这两个系统就用 Smalltalk 之类的有运行时的语言编写,至少现在应该有能和 Cocoa 抗衡的框架。

第二, 这几年 X 也认识到了在脚本化控制上面的不足,所以几年前做桌面的 Redhat 提出了 DBus 标准。 可惜的是不是每个程序都开放了 Dbus 接口,所以和苹果比起来,还有比较长的路要走。

以前我在博客里说过我用 Gtalk 控制我家的空调,这样我在回家之前就可以先开空调, 到家的时候正好家里凉爽起来了。 这个技术很简单, 只需要弄一个所谓的 X10 设备就行了。 我的朋友 Bao Sheng 做了一个视频, 有兴趣的可以看看他的演示和讲解。

X10 是一种非常方便的协议, 所有的控制信号都通过家中的电源线传播, 所以设备一插到插座上, 就可以被控制,不需要另外布线, 即插即用,对于我这种租房的人来说非常方便。 X10 设备有很多控制的方式。 有一种 X10 设备, 可以连接在计算机上, 然后计算机就可以发送指令控制其他X10设备。 计算机和X10 之间的接口是串口,所以如果你觉得不爽,用单片机和一个 MAX232 芯片就可以控制了,不需要庞大的计算机。

如果家中的电灯等不是插在插座上,而是通过墙上开关控制,你只需要将家中原有的电灯开关换成支持 X10 的电灯开关就行了。这种 X10 的电灯开关, 和普通墙上开关大小一样,功能一样, 但在 eBay 上的售价比在 Home Depot 买的普通开关还便宜,所以完全可以把家中开关全部换掉。这样就可以用计算机控制家中原有的电灯。

空调, 电饭锅, 风扇, 电视机等电器都可以如此控制。 至于我说的网络控制,就更加简单了, Gtalk 协议都是公开的,只需要写个客户端挂机监听就行了。 如果想玩这套东西的, 我这里有源代码。 X10 还有无线射频接口, 所以可以在车上控制车库的门或者门口的灯, 在家也可以躺在床上一键关灯,都是无线接口能干的事情,这里面的技术,和电动车锁的原理是一样的,你甚至可以把家里面的锁换成车上那种,一有人撬,立即杀猪似的发出警报。

控制电器有很多应用, 比如看电影的时候,能够达到一键打开电视和DVD以及关掉大灯的效果。早晨也可以通过控制灯慢慢亮起来让人自然醒来。至于控制空调,电饭锅这些,都是完全看需要看想象力了。

以上这些玩意,属于第一代智能家庭,我家基本上都部署了。 我现在考虑把我们家往二代智能家庭进化, 具体来说要能做到以下几点, 有些技术细节我没想好, 所以写出来请各位读者大牛出主意。

1. 在计算机上可视化控制一切电器

这个不难,只要写一个 GUI 客户端就行了。 如果想做得比较好玩一点,可以像  Second Life 一样, 把家中做成一个虚拟现实系统, 人走到哪个房间, 哪个房间灯自动点亮。 这个需要一个人的定位机制, 而且人身上的传感器要很小, 便于携带。 这个目前还没想好怎么弄。 大家帮我出出主意? 虚拟现实有什么好的软件? 是不是在门口装个摄像头做一下 Motion Detection 比较好?

2. 语音控制

我想语音控制所有的家电。 我看到别人 hack 了一个无线对讲机,让这个对讲机的信号送到计算机里面,但是随身放一个对讲机好像太庞大了,目前正在考虑弄一个蓝牙耳机,这个蓝牙耳机和计算机连接起来,做语音识别,然后发送信号到家电。 计算机端的语音识别软件很多的,应该很好弄。

3. 大脑直接控制和其他控制

BCI 技术还不成熟, 不过带一两个传感器的检测眼球运动的东西早就有了,如果要控制的东西不复杂,可以使用 Ocz Nia,  如果要复杂一点, 可以使用 Emotiv. 唯一的不好就是要带个帽子到处走,倒没有一个随身的车钥匙大小的遥控器方便。 自从有了 WII, 我们可以 Hack 一个 WII Remote, 然后在家挥一下 WII Remote, 就可以让灯亮起来。 要是 WII Remote 再小一点,或者自己买一个加速传感器和位置传感器粘在筷子上, 就很有魔棒的感觉了,到时候对着灯一点,灯就亮了, 哈里波特保证拜你为师 :)

4. 传感器通信

我家现在卧室的门上面都装了一个红外发射器(垃圾堆上捡了个电视遥控器,用了里面的红外发光二极管)和一个接收器,人进入屋子的时候会切断红外线一次,我就自动打开屋子里的灯,本来是贴在地上的, 可是我们家的猫老是走来走去,干扰系统,所以我不得不放在半人高的地方,可是这些器件总是要拖很长的线才能把信号送回去,我现在放半人高的地方,线拖下来, 猫就老在我接出去的线上面蹭痒,搞得系统三天两头挂掉。 各位知道有没有什么便宜的无线协议,能够把这些小器件之间的无线通信问题解决? 市面上的 WIFI 传感器都要大几十美元一个,不够便宜,这些小器件走 X10 也不行,我家没那么多插口, 而且我还得投资 TTL 到 X10 的接口。 蓝牙和WIFI 模块都太贵,RF 技术我是白痴,而且也不知道怎么做一个不互相干扰的系统,各位有高人指点指点?

5. 微型化计算机

我最近找了一些不带风扇的,可以跑 Linux 的作为家庭媒体存储中心和控制中心的计算机, 我的要求是体积要小,耗能要低,可以放在客厅或者锁在锁在壁橱里面。 其实一个几十块钱的二手计算机就可以干这些事情了,但是噪音太大,长得难看,实在不适合放在客厅。

还有就是接口,至少要支持一些 USB 口,这样我外面才能接其他的外设。 我找了一顿没找到什么好的。 各位有什么建议? 我目前找到的包括 WD My Book World Edition, 中科梦龙盒子(太贵), Foxboard, 和 Marvell Plug Computer. 或者自己 DIY 一个 ATOM 芯片的机器, 或者买一个 ARM 开发版, 或者 Hack 一个  20 块钱的二手 TiVo. 我现在还在这些技术之间纠结,想要找性价比最好的。

同时我还想搞一块很节能的液晶面板,带触摸的,算是家庭控制中枢,可以看到所有的灯和传感器的工作情况,就是不知道有便宜的小触摸面板没有?

我的目标是建立一个智能的,让我安居乐业的家。 各位读着还有什么奇妙的想法?

FORTRAN 语言是怎么来的

在高级语言是怎么来的子系列的第一篇中, 我们结合当时硬件的特点,分析了 FORTRAN 为什么一开始不支持递归。但是 FORTRAN 本身是怎么来的这个问题其实还是没有得到正面回答,本节我们就谈谈 FORTRAN 语言本身是怎么来的。

其实,FORTRAN 语言也是现实驱动的。 所以我们还是回到当时,看看当时程序员的需求和软硬件条件,看看 FORTRAN 是怎么来的。 了解历史的另一个好处是, 因为 FORTRAN 的发展历史正好和高级语言的发展历史高度重合,所以了解 FORTRAN 的背景,对于理解其他高级语言的产生都是大有帮助的。

1. 困难的浮点计算
我们先从硬件的角度说起。 大致从 1946 年第一台计算机诞生,到 1953 年,计算机一直都缺少两件非常重要的功能,一个叫浮点计算,一个叫数组下标寻址,这两个功能的缺失直接导致了高级语言的兴起。 我们依次单个分析。读者对浮点计算应该都不陌生,用通俗的话说就是如 0.98×12.6 这样的实数乘法,或者  0.98 + 12.6 这样的实数加法的运算。用行话说,就是用计算机进行大范围高精度数的算术运算。

学过二进制的同学都知道,二进制整数之间的乘法和加法等运算都是相对简单的,和正常的十进制运算是一样的,只是把加法和乘法这些基本操作用更加简单的逻辑或(OR) 和 逻辑与 (AND) 实现而已,在电子电路上也很好实现。 因此,就是世界上最早的电子计算机,ENIAC,也是支持整数的乘法加法等算术操作的。

可是浮点运算就不一样了。 因为一个额外的小数点的引入,在任何时候都要注意小数点的对齐。 如果用定点计数,则计数的范围受到限制,不能表示非常大或者非常小的数。所以,浮点数一般都是用科学记数法表示的,比如 IEEE 754 标准。(不熟悉 IEEE 754 的读者也可以想像一下如何设计一套高效的存储和操作浮点数的规范和标准,以及浮点算法), 科学记数法表示的浮点数的加减法每次都要对齐小数点,乘除法为了保持精度,在设计算法上也有很多技巧,所以说,相比较于整数的运算和逻辑运算,浮点运算是一件复杂的事情。落实到硬件上就是说,在硬件上设计一个浮点运算,需要复杂的电路和大量的电子元器件。在早期电子管计算机中,是很少能做到这么大的集成度的。因此,不支持浮点也是自然的设计取舍。在计算机上放一个浮点模块这个想法,需要等电子工业继续发展,使得电子管体积小一点,功耗低一点后,才能进入实践。

2. 关于浮点计算的一些八卦

关于浮点,这里顺带八卦一点浮点计算的事情。在计算机芯片设计中,浮点计算一直是一个让硬件工程师头疼的事情,即使到了386时代,386 处理器 (CPU)的浮点乘法也是用软件模拟的,如果想用硬件做浮点乘法,需要额外购买一块 80387 浮点协处理器 FPU,否则就在 386 上做软件的模拟。这样做的原因在一块硅片上刻蚀一个 CPU 和一个FPU 需要的集成度还是太高,当时的工艺根本没法实现。真的把 FPU 和 CPU 融在一起刻蚀到一块硅片上,已经是 1989 年的事情了。当时,Intel 把融合了 80386 和 80387 的芯片改了改,起了个名字叫 80486,推向了市场。带着浮点的处理器的普及,使得个人计算机能做的事情变多了。极度依赖于浮点计算的多媒体计算机(视频和声音等多媒体的压缩,转换和回放都是要依赖于浮点运算的),也正好随着 80486 的流行,逐渐普及开来。

在处理器上融合浮点运算依然是困难的。即使到今天,很多低端的处理器,都不带有浮点处理器。 所以,号称能够上天入地的,被移植到很多低端设备比如手机上的 Linux 内核,必然是不能支持浮点运算的,因为这样会破坏内核的可移植性。我们都知道, 在内核模式下,为了保证内核操作的原子性,一般在内核从事关键任务的时候所有中断是要被屏蔽的,用通俗的话说就是内核在做事情的时候,其他任何人不得打 扰。 如果内核支持浮点运算,不管是硬件实现也好,软件模拟也罢, 如果允许在内核中进行像浮点计算这样复杂而耗时的操作,整个系统的性能和实时响应能力会急剧下降。  即使是在硬件上实现的浮点运算,也不是件容易的事情,会耗费CPU较多的时钟周期,比如 Pentium 上的浮点数除法,需要耗费 39 个时钟周期才行,在流水线设计的CPU中,这种占用多个时钟周期的浮点运算会让整个流水线暂停,让CPU的吞吐量下降。在现代 CPU 设计中,工程师们发明了超标量,乱序执行,SIMD 等多种方式来克服流水线被浮点运算这种长周期指令堵塞的问题,这都是后话了。

正因为对于计算机来说,浮点运算是一个挑战性的操作,但又是做科学计算所需要的基本操作,所以浮点计算能力就成了计算机能力的一个测试标准。我们常常听说有一个世界上前 500 台最快的超级计算机列表,这里所谓的“快”的衡量标准,就是以每秒钟进行多少次浮点计算(FLOPS) 为准。按照 Top500.org, 即评选世界上前 500 台超级计算机的机构 2009年6月的数据,世界上最快的计算机,部署在美国能源部位于新墨西哥的洛斯阿拉莫斯国家实验室 (Los Alamos National Laboratory),当年造出第一颗原子弹的实验室。这台超级计算机,浮点计算速度的峰值高达 1456 TFlops,主要用来模拟核试验。因为美国的所有核弹头,海军核动力航母中的反应堆以及核试验,都由能源部国家核安全署(NNSA) 管理,所以能源部一直在投资用超级计算机进行核试验。 在 1996 年美国宣布不再进行大规模的物理核试验后的这么多年,美国能源部一直用超级计算机来做核试验,所以在 Top500 列表中,美国能源部拥有最多数量的超级计算机。

3. 数组下标寻址之障

言归正传,我们刚才说了在早期计算机发展史上,浮点计算的困难。除了浮点计算,还有一件事情特别困难,叫做数组下标寻址。用现代通俗的话说,就是当年的计算机,不直接支持 A[3] 这样的数组索引操作,即使这个操作从逻辑上说很简单:把数组 A 的地址加上 3,就得到了 A[3] 的地址,然后去访问这个地址。

这个困难在今天的程序员看来是不可思议的。 为什么这么简单的数组下标寻址机制最一开始的计算机没有支持呢? 原来,当年的计算机内存很小,只有一千到两千的存储空间,所以,描述地址只需要几位二/十进制数(BCD)。从而,在每条指令后面直接加一个物理地址是可行且高效的寻址方式。这种寻址方式,叫做直接寻址,当时所有的机器,都只支持直接寻址,因为在机器码中直接指出操作数的准确地址是最简单直接的方法,计算机不需要任何复杂的地址解码电路。但坏处是,这个设计太不灵活了,比如说 A[3] 这个例子,就没法用直接寻址来表示。

一般情况下,如果知道数组A, 对于 A[3] 这样的例子,用直接寻址问题去模拟间接寻址的困难还不是很大,只要程序员事先记住数组 A 的地址然后手工加上 3 就行了 (A也是程序员分配的,因为当时没有操作系统,所以程序员手工管理内存的一切)。可是,也有一些情况这样直接寻址是不行的。比如说,当时计算机已经能支持跳转和判断指令了,也就是说,可以写循环语句了。我们可以很容易看到, 以 i 为循环变量的循环体内,对 A[i] 的访问是不能写成一个静态的直接寻址的,因为 i 一直在变化,所以不可能事先一劳永逸的定好 A[i] 的所在位置,然后静态写在程序中。

这样,即使写一个简单的 10×10 矩阵的乘法,程序员就不得不死写 10的三次方即1000 行地址访问,而没办法用几行循环代替。当时的一些聪明人,也想了一些方法去克服这个问题,比如说,他们先取出 A 的地址,然后做一次加法,把结果,也就是当时 A[i] 的地址,注射到一个读内存的 LOAD 指令后面。然后执行那条 LOAD 指令。比如我要读 A[i],我先看,A的地址是 600,再看看 i 是3, 就加上 i,变成603,然后,把后面的指令改成 LOAD 603, 这样,就可以读到 A[i]。这个小技巧之所以可行,要感谢冯诺依曼爷爷的体系设计。在冯诺依曼计算机中,数据和程序是混在一起不加区分的,所以程序员可以随时像修改数据一样修改将要运行的下一条程序指令。就这样,靠着这个小技巧, 好歹程序员再也不要用1000行代码表示一个矩阵乘法了。

4. SpeedCoding 的出现

计算机本来就是用来做数学计算的,可是科学计算里面最最基本的两个要素–浮点计算和数组下标访问,在当时的计算机上都缺少支持。这种需求和实际的巨大落差,必然会召唤出一个中间层来消弭这种落差。 其实计算机科学的一般规律就是这样:当 A 和 C 相差巨大的时候,我们就引入一个中间层 B,用 B 来弥合 A 和 C 之间的不兼容。 当年的这个中间层,就叫做 SpeedCoding,由 IBM 的工程师 John Backus 开发。

SpeedCoding,顾名思义,就是让程序员编程更快。它其实是一个简单,运行在 IBM 701 计算机上的解释器。它允许程序员直接写浮点计算和下标寻址的指令,并且在底层把这些 “伪指令” 翻译成对应的机器码,用软件模拟浮点计算,自动修改地址等等。这样,程序员就可以从没完没了的手工实现浮点运算和下标寻址实现中解放出来,快速的编程。这个 SpeedCoding,这可以算得上是 FORTRAN 的种子了。

虽然这个解释器超级慢,程序员用这个解释器也用得很爽,也不感到它非常慢。 这是因为当年计算机浮点计算都绕不过软件模拟,即使最好的程序员用机器码而不用这个解释器,写出来的程序,也不比这个解释器下运行快多少。另一个更加重要的原因是,这个解释器极大的减少了程序员 debug 和 code 的时间。随着计算机速度的提高,当年一个程序耗费的计算成本和程序员编程耗费的人力成本基本上已经持平了,所以,相比较于写更加底层的机器码,用了 SpeedCoding 的程序员的程序虽然慢点,但人力成本瞬间降成 0,总体下来,用 SpeedCoding 比起不用来,总体成本还要低不少。

好景不长,因为客户一直的要求和电子工业的发展,IBM 在 1954 年,终于发布了划时代的 704 计算机,很多经典的语言和程序,都首次在 704 上完成了。比如之前我们在本系列的D篇中提到的 Steve Russell 的 LISP 解释器,就是在 704 上完成的。 704 计算机一下子支持了浮点计算和间接下标寻址。 这下用 SpeedCoding 的人没优势了,因为机器码支持浮点和下标寻址之后,写机器码比写 SpeedCoding 复杂不了多少,但是速度快了很多倍,因为 SpeedCoding 解释器太慢了,以前因为浮点和解释器一样慢,所以大家不在意它慢,现在浮点和寻址快了,就剩下解释器慢,写机器码的反而占了上风,程序员也就不用 SpeedCoding 了。

5. FORTRAN 创世纪

在 704 出来之前,做 SpeedCoding 的 John Backus 就认识到,要想让大家用他的 SpeedCoding, 或者说,想要从软件工具上入手,减少程序的开发成本,只有两个方法: 1. 程序员可以方便的写数学公式  2. 这个系统最后能够解析/生成足够的快的程序。他认为,只有达到了这两点,程序员才会乐意使用高级的像 SpeedCoding 这样的工具,而不是随着硬件的发展在机器码和 SpeedCoding 这样的工具之间跳来跳去。他本人通过实现 SpeedCoding, 也认识到如果有一个比机器码高级的语言, 生产效率会高很多倍。那么,现在唯一的问题就是实现它,当然,这就不是一个小项目了,就需要 IBM 来支持他的开发了。 所以,在 1953年,他把他的想法写成了一个文档,送给了 IBM 的经理。项目在 1954 年, 704 发布的当年,终于启动。John Backus 领导的设计一个能达到上面两点的编程系统的项目的成果,就是日后的 FORTRAN。

和现在大多数编程语言不一样,FORTRAN 语言的设计的主要问题不是语法和功能,而是编译器怎么写才能高性能。John Backus 日后回忆说,当时谁也没把精力放在语言细节上,语言设计很潦草的就完成了(所以其后正式发布后又经过了N多修订),他们所有的功夫都是花在怎么写一个高性能的编译器上。这个高性能的编译器很难写,到 1957 年才写好,总共花了 IBM 216 个人月。等到 FORTRAN 一推出,不到一年的时间,在 IBM 总共售出的 60 台 704上,就部署了超过一半。现在没啥编程语言能够这么牛的攻城掠地了 :)

6. 结语

放到历史的上下文中看,FORTRAN 的出现是很自然的。一方面,复杂的数学运算使得一个能够表述数学计算的高级语言成为必须,计算机的发展也为这个需求提供的硬件条件;另一方面,随着计算机的发展,程序员的时间成本一直不变,但是计算的成本一直在降低,用高级语言和用机器码在性能上的些许差异变得可以忽略。这样的历史现实,必然会召唤出以少量的增加计算机工作量为代价,但能大幅度降低程序员时间成本的新的工具和设计。这种新的工具,新的设计,又对程序设计产生革命性的影响。在整个编程发展的历史上,FORTRAN 和其他高级语言的出现可以说是第一波的革命;而后, UNIX和C语言的兴盛,使得系统编程的效率得到革命性提升,可以算是第二波革命;而面向对象方法,使得复杂的 GUI 等系统的编程效率得到提升,应该算得上是第三波革命。到如今,现在各种各样的方法论就更加多了,且看以后回看,哪种方法和工具能够大浪淘沙留下来。

终于放暑假了, 有心情来八卦了. 我主要想八卦一下高级语言的设计思想和各种范式的来龙去脉, 也就是回答这个问题: 编程语言为什么会发生成现在这个样子哩? 这里面的奥妙又在哪里哩? 我尝试着把这个系列的八卦写下去, 包括虚拟机的设计, 线程的设计, 栈和寄存器两大流派的来龙去脉等等, 也算是完成年初给大家许下的诺言.

高级编程语言的创始纪上写道:”初, 世间无语言, 仅电路与连线. 及大牛出, 天地开, 始有FORTRAN, LISP. ALGOL 随之, 乃有万种语.” 我们都知道, LISP 是基于递归函数的, FORTRAN 是做科学计算的. 现在的C 等等, 都比较像 FORTRAN 不像 LISP. 可是很少有人知道, 最初, FORTRAN 是不支持函数递归调用的, 而LISP是一生下来就支持的, 所有高级语言里面的递归调用, 都是逐渐从 LISP 那里学来的. 这段尘封的历史非常有趣, 值得八卦一番.

一般人学编程, 除了写 Hello World 之外, 人生写的第二个程序, 不是阶乘就是菲波拉契数列, 要不就是汉洛塔. 而这几个程序, 基本上都是因为函数的递归调用才显得简单漂亮. 没有递归的日子里, 人民非常想念您. 可是, 第一版的 FORTRAN 就居然居然不支持递归. 细心的读者要问了, 不支持递归的语言能图灵完全么? 当然可以, 图灵机就是没递归的典型的例子. 但是没递归调用的程序会很难写, 尤其像汉诺塔这种. 那么, FORTRAN 他怎么就悍然不支持递归呢, 让我们回到 1960 年.

话说当年, IBM 是计算机行业的领军者. 那时候的计算机, 都是比柜子还大的大家伙, 至于计算能力嘛, 却比你的手机还弱. 那时候计算机所做的最多的事情, 不是发邮件打游戏, 而是作计算. 作计算嘛, 自然需要一种和数学语言比较接近的编程语言. 于是, 1960年, IBM 就捣鼓出了 FORTRAN, 用行话说, 就是公式翻译系统. 这个公式翻译系统, 就成了世界上第一个编程语言. 这个编程语言能做数学计算, 能作条件判断, 能 GOTO. 用现在的眼光看, 这个语言能构模拟图灵机上的一切操作, 所以是图灵完全的. 学过数值计算的同学都知道, 科学计算无非就是一大堆数学计算按照步骤进行而已. 所以, 一些控制判断语句, 数学公式加上一个数组, 基本上就能完成所有的科学计算了. IBM 觉得这个语言够用了, 就发布了 FORTRAN 语言规范, 并且在自家的大型机上实现了这个语言. 

在实现这个语言的时候, IBM 的工程师要写一个 FORTRAN 编译器 (请注意那时候的大型机没有操作系统). 那时候的编译器都是用机器语言或者很低级的汇编语言写成的, 所以编译器要越简单越好. 这些工程师觉得, 弄一个让用户运行时动态开辟内存的机制太麻烦了, 所以干脆, 强迫用户在写程序的时候, 就要定好数组的大小, 变量的类型和数目. 这个要求并不过分, 因为在科学计算中, 数组的维度, 用到的变量等, 在计算之前, 就是可以知道大小的. 用现在的话说, 就是不能动态开辟内存空间, 也就相当于没有 malloc 的 C, 或者没有 new 的 C++. 这样的好处是, 一个程序要多少内存, 编译的时候就知道的一清二楚了. 这个主意看上去很聪明, 不过 IBM 的工程师比你想得更加聪明, 他们想, 既然一个程序或者子程序要多少内存在编译的时候都知道了, 我们干脆就静态的把每个子程序在内存中的位置, 子程序中参数, 返回值和局部变量放的位置, 大小都定好, 不久更加整齐高效么. 是的, 我们都知道, 在没有操作系统管理的情况下, 程序的内存策略越简单越好, 如果内存放的整整齐齐的, 计算机的管理员就能够很好的管理机器的内存, 这样也是一件非常好的事情. (再次强调, 当年还没有操作系统呢, 操作系统要等到 1964年发布的 IBM 360 才有, 具体开发一个操作系统之难度可参考< 人月神话>).

可是, 聪明的读者一下子就看出来了, 这样静态的搞内存分配, 就递不成归不了了. 为啥呢. 试想, 我有个 Fib 函数, 用来计算第 N 个菲波拉契数. 这个函数输入一个整数, 返回一个整数, FORTRAN 编译器帮我把这个函数给静态分配了. 好, 我运行 Fib(5) 起来, FORTRAN 帮我把 5 存在某个专门给输入参数的位置. 我在 Fib(5) 里面递归的调用了Fib(4), FORTRAN 一看, 哈, 不还是 Fib 么, 参数是 4, 我存. 这一存, 新的参数4, 就把原来的 5 给覆盖掉了, 新的返回值, 也把原来的返回值给覆盖掉了. 大事不好了, 这么一搞, 新的调用的状态居然覆盖了老的调用, 这下, 就没法返回原来的 Fib(5) 了, 这样一搞, 怎么递归啊?

IBM 这些写编译器的老前辈们, 不是不知道这个问题, 而是压根就鄙视提出这个问题的人: 你丫科学计算递归什么呀, 通通给我展开成循环, 展不开是你数学没学好, 想用递归, 你就不要用 FORTRAN 了. 那时候 IBM 乃是老大, 只有他们家才生产大型机, 老大发话, 下面的消费者只能听他的.

既然软件不支持, 硬件也就可以偷工减料嘛, 所以, 硬件上, 就压根没有任何栈支持. 我们都知道, 计算机发展史上, 软件和硬件是相互作用的. 我们现在也很难猜测, 是IBM 的软件工程师因为 IBM 的硬件工程师没有在硬件上设计出堆栈所以没有能在 FORTRAN 里面设计出递归调用呢, 还是 IBM 的硬件工程师觉得既然软件没要求, 我就不设计了呢? 不管怎么样, 我们看到的是, 1960 年前, 所有的机器的硬件都没有直接支持栈的机制. 熟悉CPU的都知道, 现代 CPU 里面, 都有两个至关重要的地址寄存器, 一个叫做 PC, 用来标记下一条要执行的指令的位置, 还有一个就是栈顶指针 SP. 如果没有后者, 程序之间的调用就会非常麻烦, 因为需要程序员手工维护一个栈, 才能保证程序之间调用最后还能正确的返回. 而当年, 因为 FORTRAN 压根就不支持递归, 所以支持 FORTRAN 的硬件, 就省去了栈指针了. 如果一个程序员想要递归调用, 唯一的实现方法, 就是让程序员借用一个通用寄存器作为栈指针, 自己硬写一个栈, 而且不能用 FORTRAN.

因为 FORTRAN 不支持递归调用, 按照自然规律, 自然会有支持递归的语言在同时代出现. 于是, 很快的, LISP 和 ALGOL 这两个新语言就出道了. 我们只说 LISP. 它的创始人 John McCarchy 是 MIT 教授, 也是人工智能之父, 是学院派人物. 他喜欢丘齐的那一套 Lambda 演算, 而非图灵的机械构造. 所以, LISP 从一开始, 就支持递归的调用, 因为递归就是 lambda 演算的灵魂. 但是有两大问题摆在 McCarchy 面前. 一是他的 LISP 理论模型找不到一个可以跑的机器, 二是他的 LISP 模型中有一个叫做 eval 的指令, 可以把一个字符串当成指令在运行时求值, 而这个, 当时还没有人解决过. 按照 Paul Graham 大叔在他的 Hackers and Painters 里面的说法, McCarchy 甚至压根就不想实现这个 eval 指令, 因为当 IBM 的一个叫 Steve Russell的工程师宣称要实现 eval 的时候, McCarthy 还连连摇手说理论是理论, 实际是实际, 我不指望这个能被实现. 可是, Russell 居然就把这两个问题一并给解决了(这哥们也是电子游戏创始人, 史上第一个电子游戏就是他写的, 叫 Space War). 他的方法, 说来也简单, 就是写了一个解释器, 让 LISP 在这个解释器里面跑. 这个创举, 让传统上编译-> 运行 的高级语言流程, 变成了 编写-> 解释执行的流程, 也就是著名的 REPL 流程. 他做的事情, 相当于在IBM 的机器上用机器码写了一个通用图灵机, 用来解释所有的 LISP 指令. 这个创举, 就让 LISP 从理论走到了实践.

因为有了运行时的概念, LISP 想怎么递归, 就可以怎么递归, 只要运行时支持一个软件实现的栈就可以了. 上面我也说了, 也就是写解释器的人麻烦一点而已, 写LISP程序的人完全就可以不管下层怎么管理栈的了. 同时, 有了解释器, 也解放了原来动态分配空间的麻烦, 因为现在所有的空间分配都可以由解释器管理了, 所以, 运行时环境允许你动态的分配空间. 对空间分配的动态支持, 随之就带来了一项新技术: 垃圾收集器. 这个技术出现在 LISP 里面不是偶然的, 是解释器的自然要求和归宿. 在 FORTRAN 上本来被绕过的问题, 就在 LISP 里面用全新的方法被解决了. LISP 的划时代意义和解释器技术, 使得伴随的很多技术, 比如抽象语法树, 动态数据结构, 垃圾收集, 字节码等等, 都很早的出现在了 LISP 中, 加上 LISP 本身规则很少, 使用起来非常灵活, 所以, 每当有一项新技术出现, 特别是和解释器和运行时相关的一项新技术出现, 我们就会听到有人说, “这玩意儿 LISP 里早就有了”, 这话, 是有一定道理的.

除了上面的软件模拟之外, MIT 还有一派在作硬件模拟, 这一派, 以后发展成了灿烂一时的 LISP machine, 为日后所有虚拟机理论铺开了一条新路. 这一派在70, 80年代迅速崛起, 然后随着 PC 的兴起又迅速的陨落, 让人唏嘘不已.

最后附送一个八卦: 1960 年的时候, 高级语言编程领域也发生了一件大事, 即 ALGOL 60 的提出. ALGOL 是划时代的标准, 我们今天用的 C/Java 全是 ALGOL 家族的. ALGOL 注意到了 FORTRAN 的不支持递归的问题, 于是从一开始, 就订立标准支持递归. 但是, 处理递归需要很小心的安排每个函数每次调用的地址和所谓的活动窗口(Active Frame), 而并不是每个编译器都是牛人写的, 所以在处理递归这样一个新事物上, 难免会出点小问题和小 BUG. 这时候, 搞笑的高爷爷(Knuth) 出场了, 他提出了一个测试, 叫做 “是男人就得负67″. (The man or boy test). 恕我功底不深, 不能给各位读者把这个男人测试的关窍讲清楚, 但是, 我知道, 这个测试, 乃是看 ALGOL 60 编译器有没有正确的实现递归和外部引用的. 照高爷爷的说法, 真的男人要能得到正确答案, 不是男人的就得不到正确答案. 当然, 高爷爷当时自己也没有男人编译器, 所以自己猜了一个 -121, 后来, 真的男人编译器出来了, 正确答案是 -67. 可见, 高爷爷的人脑编译器, 也不是男人编译器…

各位欲知详情的, 猛点这个.