Cat 命令的源码历史
| 2018-12-12 16:40:50 评论: 0
以前我和我的一些亲戚争论过计算机科学的学位值不值得读。当时我正在上大学,并要决定是不是该主修计算机。我姨和我表姐觉得我不应该主修计算机。她们承认知道如何编程肯定是很有用且对自己有利的一件事,但是她们认为计算机科学现在发展的如此迅速以至于我学的东西几乎马上就过时了。建议我更好是把编程作为辅业,选择一个基础原理可以受用终身的领域主修,比如经济学或物理学。
我知道我姨和我表姐说的不对,并决定主修计算机科学。(对不住啊!)平常人可能会觉得像计算机科学领域和软件工程专业每隔几年就完全和之前不一样了。其原因很容易理解。我们有了个人电脑,然后有了互联网,有了手机,之后还有了机器学习…… 科技总是在更新,支撑科技发展的原理和技能当然也在改变。当然,最惊人的是其实原理的改变竟然如此之小。我敢肯定,大多数人在知道了他们电脑里一些重要的软件的历史是多么久远时他们一定会深感震惊。当然我不是说那些刷版本号的浮夸软件 —— 我电脑上的 Firefox 浏览器副本,可能是我用的最多的软件,可能两周前就更新过。如果你看了比如 grep
的手册页,你就会发现它在 2010 年后就没有过更新了(至少在 MacOS 上如此)。初版 grep
是在 1974 年写就的,那时可以算是计算机世界的侏罗纪了。直到现在,人们(还有程序)仍然依赖 grep
来完成日常工作。
我姨和我表姐认为计算机技术就像一系列日渐精致的沙堡,在潮水抹净沙滩后新的沙堡完全取代旧的。但事实上,在很多领域上,我们都是不断积累能够解决问题的程序。我们可能不得不偶尔修改这些程序以避免软件无法使用,但大多数情况下我们都可以不修改。grep
是一个简单的程序,可以解决一个仍然存在的需求,所以它能够存活下来。 大多数应用程序编程都是在非常高的级别上完成的,它们建立在解决了旧问题的旧程序的金字塔上。 30 年或 40 年前的思路和概念,远非过时,在很多情况下它们依然在您的笔记本电脑上软件中存在着。
我想追溯这样的老程序自第一次写就以来改变了多少回很有趣。 cat
可能是所有 Unix 实用程序中最简单的,因此我们以它为例。Ken Thompson 于 1969 年编写了 cat
的原始实现。如果我告诉别人我的电脑上安装了个来自 1969 年的程序,这准确吗?我们电脑上的程序多大了?
感谢这种这种仓库,我们可以完整的看到 cat
自 1969 年后是如何发展的。我会先聚焦于可以算得上是我的 MacBook 上的 cat
的祖先的 cat
实现。随着我们从 Unix 上的第一版 cat
追踪到现在 MacOS 上的 cat
,你会发现,这个程序被重写的次数比你想的还要多 —— 但是直到现在它运行的方式和五十年前多少是完全一致的。
研究 Unix
Ken Thompson 和 Dennis Ritchie 在 PDP 7 上开始写 Unix。那还是 1969 年,C 还没被发明出来,因此所有早期的 Unix 软件都是用 PDP 7 汇编实现的。他们使用的汇编种类是 Unix 特有的,Ken Thompson 在 DEC(PDP 7 的厂商)提供的汇编器之上加了些特性,实现了自己的汇编器。Thompson 的更改在最初的 Unix 程序员手册的 as
(也就是汇编器)条目下均有所记录。
因此,最初的 cat
也是使用 PDP 7 汇编实现的。 我添加了一些注释,试图解释每条指令的作用,但除非你理解 Thompson 在编写汇编器时加的特性,否则程序仍然很难理解。在那些特性中有两个很重要:其一是 ;
这个字符可以在一行中用来分隔多条语句,它多出现于在使用 sys
指令时将系统调用的多个参数放在同一行上。其二是, Thompson 的汇编器支持使用 0 到 9 作为“临时标签”,这是在程序内可以重用的标签。因此。就如 Unix 程序员手册中所说:“对程序员的想象力和汇编程序的符号空间的要求都降低了”。在任何给定的指令内,你都可以使用 nf
和 nb
来引用下一个或最近的临时标签 n
。 例如,如果存在标记为 1:
的代码块,你就可以使用指令 jmp 1b
从下游代码跳回该块。 (但是你不使用 jmp 1f
的话就没法从上面的代码跳到这里。)
初版 cat
最有趣的就是它包含着我们应该认识的符号。有一块指令块标记为 getc
,还有一个标记为 putc
,可以看到这两个符号比 C 标准还古老。第一版的 cat
函数实际上已经包含了这两个函数的实现。该实现做了输入缓存,这样它就不需要一次只读写一个字母。
cat
的第一个版本并没有持续多久。 Ken Thompson 和 Dennis Ritchie 说服贝尔实验室购买了 PDP 11,这样他们就能够继续扩展和改进 Unix。 PDP 11 的指令集和之前不一样,因此必须重写 cat
。 我也注释了这个第二版 cat
。 它为新的指令集使用新的汇编程序助记符,并利用了 PDP 11 的各种寻址模式。(如果你对源代码中的括号和美元符号感到困惑,那是因为这些符号用于指示不同的寻址模式。)但它也使用 ;
字符和临时标签,和 cat
的第一个版本一样,这意味着当把 as
移植到 PDP 11 上时,必须要保留这些功能。
cat
的第二个版本比第一个版本简单得多。 它也更有 Unix 味儿,它不只是依靠参数列表,一旦没给参数列表,它将从 stdin
读取数据,这也就是今天 cat
仍在做的事情。 你也可以在此版本的 cat
中以 -
为参数,以表示它应该从stdin
读取。
在 1973 年,为了准备发布第四版 Unix,大部分代码都用 C 语言重写了。但是 cat
似乎在之后一段时间内并没有使用 C 重写。 cat 的第一个 C 语言实现出现在第七版 Unix 中。 这个实现非常有趣,因为它很简单。 在所有以后的实现中,这个实现和在 K&R 的 C 语言教科书中用作教学示范的理想化 cat
最相似。这个程序的核心就是经典的两行:
while ((c = getc(fi)) != EOF)
putchar(c);
当然实际代码要比这多一些,额外的代码主要是为了确保你没有在读/写同一个文件。另一个有趣的事情是,cat
的这一版实现只识别一个标志位 -u
。 -u
标志可用于避免缓冲输入和输出,否则 cat
将以 512 字节为块进行输入输出。
BSD
在第七版 Unix 之后,Unix 出现了各种衍生品和分支。 MacOS 建立于 Darwin 之上,而 Darwin 又源自 伯克利软件分发版 (BSD),因此 BSD 是我们最感兴趣的 Unix 分支。 BSD 最初只是 Unix 中的实用程序和附加组件的集合,但它最终成为了一个完整的操作系统。直到第四版 BSD,人称 4BSD,为一大堆新标志添加了支持之前,BSD 似乎还是依赖于最初的 cat
实现的。cat
的 4BSD 实现 显然是从原始实现中衍生出来的,尽管它添加了一个新函数来实现由新标志触发的行为。已经在文件中使用的 fflg
变量(用于标记输入是从 stdin
还是文件读取的)的命名约定,被新添加的 nflg
、bflg
、vflg
、sflg
、eflg
和 tflg
沿袭了下来,这些变量记录了在调用程序时是否使用了这些新标志。这些是最后一批添加到 cat
的命令行标志。如今 cat
的手册页列出了这些标志,没有其他的标志了,至少在 Mac OS 上是如此。 4BSD 于 1980 年发布,因此这套标志已有 38 年历史。
cat
最后一次被完全重写是在 BSD NET/2 上,其目的是通过替换 AT&T 发布的全部 Unix 源代码来规避许可证问题。BSD Net/2 在 1991 年发布。这一版本的 cat
是由 Kevin Fall 重写的。 Kevin Fall 于 1988 年毕业于加州大学伯克利分校并在下一年成为 计算机系统研究组 (CSRG)的组员,Fall 和我说当时使用 AT&T 代码的 Unix 工具被列在了 CSRG 的墙上,组员需要从中选出他们想要重写的工具; Fall 选了 cat
以及 mknod
。 MacOS 系统内自带的 cat
实现源码的最上面还有着他的名字。他的这一版 cat
,尽管平淡无奇,在今天还是被无数人使用着。
Fall 的原始 cat 实现 比我们迄今为止看到的版本都要长。 除了支持 -?
帮助标志外,它没有增加任何新功能。 从概念上讲,它与 4BSD 的实现非常相似。 它长是因为 Fall 将实现分为 “原始” 模式和 “加工” 模式。 “原始” 模式是 cat
的经典实现;它一个字符一个字符的打印文件。 “加工” 模式是带有所有 4BSD 命令行选项的 cat
。 如此区别不无道理,但这么办也扩充了实现规模,因此乍一看其源码似乎比实际上更复杂。文件末尾还有一个奇特的错误处理函数,进一步地增加了实现的长度。
MacOS
在 2001 年,苹果发布了 MacOS X。这一发布对苹果意义重大。因为苹果用了多年的时间尝试以取代其现有的老旧操作系统(经典的 Mac OS),但是都失败了。 在 Mac OS X 之前苹果两次尝试在内部创建一个新的操作系统,但两者都无疾而终。 最后,苹果收购了史蒂夫·乔布斯的 NeXT 公司,后者开发了一个名为 NeXTSTEP 的操作系统和面向对象编程框架。 苹果将 NeXTSTEP 作为 Mac OS X 的基础。因为 NeXTSTEP 部分基于 BSD,使以 NeXTSTEP 为基础的 Mac OS X 的自然就把 BSD 系的代码直接带入苹果宇宙的中心。
因此,Mac OS X 的非常早期的第一个版本包含了从 NetBSD 项目中提取的 cat
的实现。如今仍保持开发的 NetBSD 最初是 386BSD 的分支,而后者又直接基于 BSD Net/2。所以 Mac OS X 里面的第一个 cat
的实现就是 Kevin Fall 的 cat
。唯一改变的是,Fall 的错误处理函数 err()
被 err.h
提供的 err()
函数取代了。 err.h
是 C 标准库的 BSD 扩展。
之后不久,这里的 cat
的 NetBSD 实现被换成了 FreeBSD 中的 cat
实现。 根据维基百科,苹果在 Mac OS X 10.3(Panther)中开始使用 FreeBSD 的实现而不是 NetBSD 的实现。但根据苹果自己开源的版本,cat
的 Mac OS X 实现在 2007 年发布的 Mac OS X 10.5(Leopard)之前没有被替换。苹果为 Leopard 替换的的 FreeBSD 实现与今天苹果计算机上的实现相同。截至 2018 年,2007 年以来的这个实现仍未被更新或修改。
所以 Mac OS 上的 cat
已经很老了。实际上,这一实现在 2007 年在 MacOS X 上露面两年前就被发布了。 这个 2005 年的修改 在 FreeBSD 的 Github 镜像中可见,是在苹果将其合并入 Mac OS X 前对 FreeBSD 的 cat
实现进行的最后一次更改。所以 Mac OS X 中的实现没有与 FreeBSD 的 cat
实现保持同步,它如今已经 13 岁了。对于软件修改了多少代码才能仍是算是同一软件这一话题有着旷日持久的争论。不过,在这种情况下,源文件自 2005 年以来根本没有变化。
现在 Mac OS 使用的 cat
实现与 Fall 1991 年为 BSD Net/2 版本编写的实现没有什么不同。最大的区别是添加了一个全新的功能来提供 Unix 域套接字支持。FreeBSD 开发人员似乎将 Fall 的 raw_args()
函数和 cook_args()
函数组合成一个名为scanfiles()
的函数。否则,程序的核心就仍是 Fall 的代码。
我问过 Fall 对编写了如今被数以百万计的苹果用户(直接或者间接通过依赖 cat
的某些程序)使用的 cat
实现有何感想。Fall,如今是一位顾问,也是最新版《TCP/IP 详解》的合著者,他说,当人们从了解他对 cat
所做的工作中收获颇丰时,他感到很惊讶。 Fall 在计算机领域有着悠久的职业生涯,曾参与许多备受瞩目的项目,但似乎很多人仍对他在 1989 年重写 cat
的那六个月的工作感到最为兴奋。
百年老程序
在宏伟的发明史中,计算机并不是一项古老的发明。我们已经习惯了百年的照片甚至是百年的视频短片。但是计算机程序不一样 —— 它们代表着高科技和新技术。至少,他们是现代的技术造出来的。随着计算行业的成熟,我们有朝一日会发现自己正在使用有着接近百年历史的程序吗?
计算机硬件可能会发生较大的变化,使得我们也许无法让现在编译的可执行文件在一个世纪后的硬件上运行。也许编程语言设计的进步让未来没有人能理解 C 语言,cat
将来也可能也被别的语言重写很久了。 (尽管 C 已经存在了五十年了,而且它似乎不会很快就被替换掉。)但除此之外,为什么不永远使用我们现在的 cat
?
我认为 cat
的历史表明,计算机科学中的一些想法确实非常持久。事实上,对于 cat
,这个想法和程序本身都很古老。不准确地说,我的电脑上的 cat
来自 1969 年。但我也可以说我的计算机上的 cat
来自1989 年,当时 Fall 写了他的 cat
实现。许多其他软件也同样古老。因此,也许我们不应该把计算机科学和软件开发视为不断破坏现状和发明新事物的领域。我们的计算机系统是由诸多历史文物构建的。有时,我们可能会花费更多时间在理解和维护这些历史文物上,而不是花在编写新代码上。
如果你喜欢本文,你可能更喜欢两周来一篇更新!在推特上关注 @TwoBitHistory 或者订阅这个 RSS 源 以保证接受到新的文章。