# 1971-1973，C 语言和管道

在贝尔电话实验室的某些部门内部，Unix 取得了成功。仅仅几个月之后，Thompson 和 Ritchie 就开始编写新的手册。第二版于 1972 年 6 月中旬问世。“前言”中写道：

> 自本手册首次发布以来的几个月中，系统本身以及其使用方式都发生了许多变化。
>
> ……
>
> 在此期间，投入相当时间编写 UNIX 软件的人数有所增加。L. L. Cherry、M. D. McIlroy、L. E. McMahon、R. Morris 和 J. F. Ossanna 应当因其贡献而受到表彰。
>
> 最后，UNIX 的安装数量已经增长到 10 台。

当然，最初的安装是在研究部门和专利部门。第一位位于新泽西中部以外的用户是 Neil Groundwater，当时他在纽约电话公司（他现在在 Sun Microsystems）。他告诉我：

> 我在 1972 年 2 月加入纽约电话公司，此前刚刚在宾夕法尼亚州立大学获得计算机科学学士学位。
>
> 当时计算机专业毕业生的就业市场并不景气，但我之所以能被录用，是因为我在宾州州立大学时接触过一台小型计算机（一套 ADAGE 图形系统）。1972 年春天，我开始前往新泽西州惠帕尼的贝尔实验室。由于我在曼哈顿有公寓，而且没有车，所以我通常在周一早上乘公交车去惠帕尼，在那边的酒店住一周，周五再回到纽约。一周一周地接近实验室的环境让我很快沉浸在工作之中。我对那份工作的最初记忆之一，是他们对学习 Unix 系统所做的描述：这就像是从里面爬烟囱，你得同时沿着四周一点一点往上爬。关于系统的许多部分确实有文档，但正如我们多年后常说的那样：“使用源码吧，卢克。”
>
> 我在惠帕尼所参与的团队正在“机械化”分析与纽约市 ESS（电子交换系统）办公室有关的一些“外线设施”任务。电子交换系统设备会在中央局的电传打字机上生成呼叫失败消息（我们最早处理的那种叫作“TN08”）。虽然 Unix 主机不能直接回传信号来控制“交换机”，但通过一些巧妙的接线，我们让调制解调器连接到了电传打字机的电流环中，而我们办公室（位于曼哈顿麦迪逊大道 330 号第 14 层）的另一台调制解调器则把信号送入一个多路复用器的端口中。
>
> 1972 年夏季，纽约站点所需的硬件到货：
>
> * DEC PDP-11/20 处理器
> * 56 KB 核心内存
> * 高速纸带读写机
> * ASR-33 电传打字机（控制台）
> * DECtape 双驱动器
> * RK11/RK05 磁盘（2 个）——容量 2.4 MB
> * RF11 固定磁头磁盘（起初 2 个，后来又增加了 3 个）
> * DC11（6 线路）用于本地终端
> * DM11 16 线路多路复用器（3 个）
>
> 我的第一个编程任务是重写一款驱动程序，它原本只支持一个 DM11 多路复用器，我要让它能处理多个（在这个案例中是三个）多路复用器……
>
> 当时的 Unix“shell”程序只占用了七页行式打印纸的篇幅。没有任何库函数，只有直接的系统调用用于读写。许多如今人们习以为常的功能那时已经存在于 shell 中：输入和输出重定向（但还没有管道）、通配符展开（虽然是调用外部程序 `/etc/glob` 来实现的），还有 shell 脚本。
>
> 写错程序很容易会覆盖内核的一部分。在 PDP-11 上，`halt` 指令的机器码是“0”，你可以想象，如果清除了一个由寄存器指针引用的位置，CPU 很可能立刻停机。有一种方法可以对用户程序进行核心转储，可以通过程序控制或者键盘序列触发，这使得事后调试成为可能。
>
> 程序开发通常是在下班后或者在惠帕尼进行的。在惠帕尼，开发机的终端位于一间公共房间里，当有几个人同时在工作时，每当有人要运行新的 `a.out` 文件（链接编辑器默认的输出文件），就会喊一声“危险程序！”。这样别人可以赶快保存编辑中的文件（而且通常都这么做）。
>
> 如果有人要用行式打印机，也会发出类似的喊声。当时没有后台排队机制和锁定机制。执行 `pr 我的文件 > /dev/lp` 就是把你的输出发送到打印机。如果两个人同时发送打印任务，他们的输出就会交叉混杂在一起。谁先喊出“行式打印机！”谁就拥有了打印队列的控制权。
>
> Unix 系统通过调制解调器将信号返回给 ESS 交换局以实现反向通信；不过在交换局那一端，接收的信号并不是发回电子交换系统的电传打字机，而是发送到 Execuport 的硬拷贝终端（类似于打印在热敏纸上的 TI Silent 700）。由于 DM11 支持不同的收发速率，接收端（电传打字机）的速率是 110 波特，而发送到 Execuport 的输出线速率是 300 波特。
>
> 在收集到故障报告后，FORTRAN 程序会对其进行排序并生成“异常报告”，如果多个报告中都包含了某个特定设备的标识符，就会被归入其中。电话网络的运行方式决定了，一个设备可能报告的是其外部的问题；也就是说，呼叫不会失败（例如，两个电话之间的连接依然会成功），但交换设备会尝试重拨呼叫，并在控制台上打印出一份故障报告。实际上，报告有很多种类型，但 TN08 被认为是最先尝试用新系统排查的对象。
>
> 启动 PDP-11/20 的过程是，将一个启动地址（磁盘启动和磁带启动各有不同的地址）加载到控制台的开关中，然后按下“执行”按钮。启动 ROM 真的是一块布满二极管的板卡，这块板卡来自 DEC，所有位（每一位对应一个二极管）初始状态都是连通的，然后通过剪断特定的二极管来生成引导指令。
>
> 1973 年，我们在同一地点新增了一台配置类似的又一台 PDP-11/20。它配有一台光学卡片阅读器，用于读取“感应标记”卡片——这些卡片由局方技术人员用铅笔手动划线，当“发送器”卡死时会使用这种方式记录。“Panel stuck sender”是他们对这种状况的术语。就像对 ESS 系统的 TN08 报告一样，如果你收集到足够的报告，计算机就可以识别出重复的项目，从而帮助定位故障。

第三版在八个月后、即 1973 年 2 月发布。E.N. Pinson 的名字被加入到了贡献者名单中。而且，最重要的是：

> 最后，Unix 的安装数量已经增长到 16，并预计还会继续增加。

第三版的前言部分在“目录”之前就有将近十二页内容。除了包含“入门指南”、“如何通过终端通信”、“Shell”以及路径名的章节外，还有几段关于“编写程序”和“文本处理”的内容。其中第一段开头是：

> 要将源程序文本输入到 Unix 文件中，可以使用 ed(1)。Unix 中的三种主要语言是汇编语言（参见 as(1)）、FORTRAN（参见 fc(1)）和 C（参见 cc(1)）……

C？什么是 C？cc 的手册页（日期为 72 年 3 月 15 日）告诉你它是一个 C 编译器，并且提到了《C 参考手册》。直到五年后，Brian Kernighan 和 Dennis Ritchie 才出版了《C 程序设计语言》，尽管该手册早些时候已经被纳入文档中。但 Unix 用这种新语言重写（在第 4 版实现）极为重要，而这门语言本身也变得对整个计算领域具有重要意义。

> Mike Mahoney 询问 Dennis Ritchie 关于设计 C 语言的事情：
>
> “它是 Ken 基于 B 语言做的一个改编。B 语言其实最初是系统 FORTRAN……不过他大概花了一天时间意识到他根本不想做一个 FORTRAN 编译器。所以他设计了这个非常简单的语言叫做 B 语言，并且让它在 PDP-7 上运行。B 语言实际上后来被移植到了 PDP-11 上。有一些系统程序是用它写的，不是操作系统本身，而是一些工具程序。它运行得相当慢，因为它是解释执行的。关于 B 语言的问题，主要有两个认识：第一，因为实现是解释执行的，运行速度总是很慢；第二，与之前使用的那些面向字（word-oriented）的机器不同，我们现在用的是面向字节（byte-oriented）的机器，而 B 语言本身是基于 BCPL，设计时并没有很好地适应面向字节的机器。尤其是 B 和 BCPL 有指针的概念，指针是存储单元的名称……但有各种不同大小的对象，B 语言和 BCPL 其实只针对一种大小的对象。从语言学的角度来看，这是 B 语言最大的限制；不仅所有对象大小相同，而且指针的概念也不太适合……所以，我差不多同时开始给 B 语言添加类型，不久后又尝试为它写编译器。语言先变更了一些。有一段时间它被称为 NB（New B）；那时它还是解释执行的，我实际上是先从 B 语言编译器开始……因为 C 语言在每个阶段都是用很像它自己的语言写成的……然后把它融合进了 C 语言编译器，并添加了各种类型结构，然后尝试把它转换成一款编译器。”
>
> 编译器的基本构造——特别是编译器的代码生成器——是基于听说过的一个想法；那是贝尔实验室印度山分部的某人提出的。我其实没亲自找到，也没读过那篇论文，但有人给我解释过其中的思想，NB（后来变成 C）的部分代码生成器就是基于这篇博士论文设计的。这个技术也被用在一种叫 EPL 的语言中，EPL 是用于交换系统和 ESS 机器的，代表 ESS 编程语言。
>
> 所以 C 语言的第一个阶段，实际上是紧接着经历了两个阶段：第一阶段是对 B 语言做一些语言改动，主要是添加类型结构，语法没有太大改变，并实现了编译器。
>
> 第二阶段比较慢，虽然整个过程只用了几年时间，但感觉进展较慢。这是源于首次尝试用 C 语言重写 Unix。Ken 大概在 1972 年夏天开始尝试，但最终放弃了。原因可能是他觉得太累，或者其他原因。失败主要有两个：一是他没搞明白基本的协程、多程序原语如何运行——也就是如何在进程间切换控制，以及内核中不同进程的关系；二是从我看来更重要的问题，是很难设计合适的数据结构。C 语言最初版本没有结构体（struct），因此构建对象表——比如进程表、文件表等各种表——相当痛苦……这很笨拙，我猜现在的人在 FORTRAN 中也还在用类似的方法。
>
> 这些问题叠加起来，导致 Ken 在那个夏天放弃了。之后的一年里，我添加了结构体，可能也让编译器变得更好，生成了更优代码。来年那个夏天，我们集中精力，真正完成了整个操作系统的 C 语言重写。

第三版中另一个创新是管道（pipe）。管道（和过滤器）是非常简单的概念：它们是一种统一的机制，用来将一个程序的输出连接到另一个程序的输入。达特茅斯分时系统（Dartmouth Time-sharing System）有通信文件，这在某种程度上预示了管道的出现，但那种方式更具体（不够通用）。这一概念是 Doug McIlroy 的发明。实现则是 Thompson 完成的，且是在 McIlroy 的坚持下完成的（“这是我几乎唯一一次对 Unix 施加管理控制的地方，”他说）。McIlroy 曾对 Mike Mahoney 说：

> 在六十年代早期，Conway 在《ACM 通讯》（Communications of the ACM）上写过一篇关于协程（coroutines）的文章，大约是在 1963 年左右。而我从 1959、1960 年开始就在做宏（macro）\[宏是一种指令，指向一组指令；基本上，它是一种一对多的映射]。如果你思考宏，它们主要涉及切换数据流。就是说，当你正在读取输入时，突然遇到一个宏调用，它会说：“停止从这里读取输入，改为从宏定义处读取。”而在宏定义的中间，你又会遇到另一个宏调用。所以早在 1964 年左右，我曾把宏处理器称作“数据流的转换场”。同样在 1964 年，有一篇论文一直挂在 Brian 的墙上，\[这篇论文] 是他某处挖掘出来的，我在里面谈到如何把数据流像花园水管那样“接合”起来。所以这个想法在我脑中已经酝酿很久了。
>
> 正当 Thompson 和 Ritchie 在黑板上勾画文件系统的时候，我也在黑板上构思如何通过连接一连串的进程来进行数据处理，并且在寻找一种用前缀表示法连接进程的语言，但最终失败了。因为很容易说“cat 传给 grep，传给...”，或者“who 传给 cat，再传给 grep”，这种表达很自然，也一开始就很清楚这是你想要的方式。但这些命令都有各种附加参数，不仅仅是输入和输出参数，还有各种选项。从语法上看，如何把这些选项放进用前缀表示法写成的链条里，比如 cat(grep(who ...))，却不清楚该怎么做。语法上的限制让我看不到解决办法，所以我在黑板上写了很多漂亮的程序，但语言本身不够强大，无法应对现实需求。因此，我们实际上并没有实现它。
>
> 在 1970 年到 1972 年这段时间里，我时不时会提出“要不做点类似这样的东西？”不断地提出一个又一个方案。终于有一天，我想出了一个和管道配合使用的 shell 语法，Ken 说：“我要去做它！”他已经听腻了这些讨论。你肯定多次读过这个故事，那真的是令人难忘的一天。第二天他就说：“我要去做了。”他没有完全照搬我提议的管道系统调用，而是发明了一个稍微更好的，后来又改成了我们现在使用的版本。但他确实用了我那个笨拙的语法。
>
> 他一夜之间把管道功能加进了 Unix，也把这种表示法（麦克尔罗指着他写在黑板上的：`f > g > c`）加进了 shell……在一夜之间完成的。直到那个时候，大多数程序还不能接受标准输入，因为当时并不真正需要。它们都有文件参数；比如 grep 有文件参数，cat 也有文件参数。Thompson 看到这样子不适合这个新方案，于是他也在同一夜里修改了所有这些程序。我不知道他怎么做到的……第二天早晨，我们就开始疯狂地写那些单行命令。

Dick Haight，后来成为 PWB 的经理，告诉 August Mohr：

> 我恰好在他们实现管道的那天拜访了研究团队。几乎在系统带有管道功能上线的几分钟内，大家就清楚这是一件了不起的事情。如果可以，没人会愿意回头放弃它。

在那天晚上之前，Unix 并没有“工具箱”这一概念。管道的发明为一种全新的软件思维方式奠定了基础，这种思维方式在随后的多年里得到了深入探索。这种思维方式最终形成了一种独特的哲学。关于工具箱，Mcllroy 说道：管道创造了它。

Mahoney 问他：“管道出现后，Unix 看起来有了变化吗？”McIlroy 回答说：

> 是的，这种哲学就是大家开始提出的：“这就是 Unix 哲学。编写只做一件事且做好它的程序。编写能够协同工作的程序。编写处理文本流的程序，因为那是一种通用接口。”所有这些思想，加起来就是工具箱的方法，或许在管道出现之前已经以某种未成形的方式存在，但它们真正是在管道之后才确立的。

工具和工具箱是 Brian Kernighan 后来参与的内容。但起初他只是对 Thompson 实现的管道做了一个修改——他用 `^` 替换了 `>`。他告诉 Mahoney，管道是

> 他说，管道在某种意义上是让一切运作起来的关键。并不是说你之前不能做那些事情，因为输入输出重定向（I/O redirection）比管道早出现了一段时间——虽然不是很长，但确实早于管道；这其实是一个比较老的想法。而这已经足够完成你现在用管道做的大部分事情，只是它的符号表示远没有管道方便。就好比用罗马数字计算和用阿拉伯数字计算，算数不是不能做，只是更麻烦。也许更难用脑子去约束理解。所有这些东西现在都压缩在了一个极短的时间内，我甚至不知道具体是什么时候发生的。我记得最荒谬的语法，比如 `>>` 或别的什么，突然间出现了竖线符号（`|`），一切就豁然开朗了。那时候，我开始编写一些非常棒的例子，比如先运行 who 命令，把输出收集到文件里，再对文件做字数统计，看看有多少用户，然后演示用 who 连接 grep，开始展示一些之前从未想到过但却非常容易组合的用法，只需在键盘上输入组合命令就能一次成功。那时我们才开始有意识地思考工具，因为你可以将它们组合起来，只要你设计它们能够真正协同工作。

Doug McIlroy 告诉我，Thompson 之所以把管道符号替换成“`|`”，是为了在伦敦的一次演讲中使用，因为他实在无法忍受展示我那个丑陋的语法。

Unix 的强大正是源于程序之间产生的关系，而非单个程序本身。

Unix 现在拥有了一种独特的语言，一种哲学，一种精神。它只有一小群忠实用户，分布在少数几个 AT\&T Bell 的站点。但它还没有真正的受众。

> **Unix 哲学**
>
> 编写只做一件事且做好这件事的程序。
>
> 编写能够协同工作的程序。
>
> 编写处理文本流的程序，因为这是通用接口。
