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