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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://book.bsdcn.org/unix-si-fen-zhi-yi-shi-ji/yi-kuan-xi-tong-de-dan-sheng/7-c_and_pipes_1971_to_1973.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
