Unix 设计哲学

Unix 设计哲学

Unix 管道的发明者道格·麦克罗伊(Doug McIlroy)在 1964 年首先描述了这种情况:“当我们需要将消息从一个程序传递另一个程序时,我们需要一种类似水管法兰的拼接程序的方式,I/O 应该也按照这种方式进行“。水管的类比仍然在生效,通过管道连接程序的想法成为了现在被称为 Unix 哲学的一部分,这一组设计原则在 Unix 用户与开发者之间流行起来,该哲学在 1978 年表述如下:

  • Do one thing and do it well - Write programs that do one thing and do it well. Write programs to work together. Write programs to handle text streams, because that is a universal interface.
  • Everything is file - Ease of use and security is offered by treating hardware as a file.
  • Small is beautiful.
  • Store data and configuration in flat text files - Text file is a universal interface. Easy to create, backup and move to another system.
  • Use shell scripts to increase leverage and portability - Use shell script to automate common tasks across various UNIX / Linux installations.
  • Chain programs together to complete complex task - Use shell pipes and filters to chain small utilities that perform one task at time.
  • Choose portability over efficiency.
  • Keep it Simple, Stupid (KISS).

其中文归纳如下:

  • 让每个程序都做好一件事。要做一件新的工作,写一个新程序,而不是通过添加“功能”让老程序复杂化。
  • 期待每个程序的输出成为另一个程序的输入。不要将无关信息混入输出。避免使用严格的列数据或二进制输入格式。不要坚持交互式输入。
  • 设计和构建软件,甚至是操作系统,要尽早尝试,最好在几周内完成。不要犹豫,扔掉笨拙的部分,重建它们。
  • 优先使用工具来减轻编程任务,即使必须曲线救国编写工具,且在用完后很可能要扔掉大部分。

这种方法:自动化,快速原型设计,增量式迭代,对实验友好,将大型项目分解成可管理的块,听起来非常像今天的敏捷开发和 DevOps 运动。sort 工具是一个很好的例子。可以说它比大多数编程语言标准库中的实现(它们不会利用磁盘或使用多线程,即使这样做有很大好处)要更好。然而,单独使用 sort 几乎没什么用。它只能与其他 Unix 工具(如 uniq)结合使用。像 bash 这样的 Unix shell 可以让我们轻松地将这些小程序组合成令人讶异的强大数据处理任务。尽管这些程序中有很多是由不同人群编写的,但它们可以灵活地结合在一起。

统一的接口

如果你希望一个程序的输出成为另一个程序的输入,那意味着这些程序必须使用相同的数据格式:换句话说,一个兼容的接口。如果你希望能够将任何程序的输出连接到任何程序的输入,那意味着所有程序必须使用相同的 I/O 接口。

在 Unix 中,这种接口是一个文件(file)(更准确地说,是一个文件描述符)。一个文件只是一串有序的字节序列。因为这是一个非常简单的接口,所以可以使用相同的接口来表示许多不同的东西:文件系统上的真实文件,到另一个进程(Unix 套接字,stdin,stdout)的通信通道,设备驱动程序(比如/dev/audio 或/dev/lp0),表示 TCP 连接的套接字等等。很容易将这些设计视为理所当然的,但实际上能让这些差异巨大的东西共享一个统一的接口是非常厉害的,这使得它们可以很容易地连接在一起。

统一接口的另一个例子是 URL 和 HTTP,这是 Web 的基石。一个 URL 标识一个网站上的一个特定的东西(资源),你可以链接到任何其他网站的任何网址。具有网络浏览器的用户因此可以通过跟随链接在网站之间无缝跳转,即使服务器可能由完全不相关的组织维护。这个原则现在似乎非常明显,但它却是网络取能取得今天成就的关键。之前的系统并不是那么统一:例如,在公告板系统(BBS)时代,每个系统都有自己的电话号码和波特率配置。从一个 BBS 到另一个 BBS 的引用必须以电话号码和调制解调器设置的形式;用户将不得不挂断,拨打其他 BBS,然后手动找到他们正在寻找的信息。这是不可能的直接链接到另一个 BBS 内的一些内容。

按照惯例,许多(但不是全部)Unix 程序将这个字节序列视为 ASCII 文本。我们的日志分析示例使用了这个事实:awk,sort,uniq 和 head 都将它们的输入文件视为由 \n(换行符,ASCII 0x0A)字符分隔的记录列表。\n 的选择是任意的:可以说,ASCII 记录分隔符 0x1E 本来就是一个更好的选择,因为它是为了这个目的而设计的,但是无论如何,所有这些程序都使用相同的记录分隔符允许它们互操作。

每条记录(即一行输入)的解析则更加模糊。Unix 工具通常通过空白或制表符将行分割成字段,但也使用 CSV(逗号分隔),管道分隔和其他编码。即使像 xargs 这样一个相当简单的工具也有六个命令行选项,用于指定如何解析输入。ASCII 文本的统一接口大多数时候都能工作,但它不是很优雅:我们的日志分析示例使用{print $7}来提取网址,这样可读性不是很好。在理想的世界中可能是{print $request_url}或类似的东西。我们稍后会回顾这个想法。

尽管几十年后还不够完美,但统一的 Unix 接口仍然是非常出色的设计。没有多少软件能像 Unix 工具一样交互组合的这么好:你不能通过自定义分析工具轻松地将电子邮件帐户的内容和在线购物历史记录以管道传送至电子表格中,并将结果发布到社交网络或维基。今天,像 Unix 工具一样流畅地运行程序是一种例外,而不是规范。即使是具有相同数据模型的数据库,将数据从一种导出再导入另一种也并不容易。缺乏整合导致了数据的巴尔干化。

逻辑与布线相分离

Unix 工具的另一个特点是使用标准输入(stdin)和标准输出(stdout)。如果你运行一个程序,而不指定任何其他的东西,标准输入来自键盘,标准输出指向屏幕。但是,你也可以从文件输入和/或将输出重定向到文件。管道允许你将一个进程的标准输出附加到另一个进程的标准输入(有个小内存缓冲区,而不需要将整个中间数据流写入磁盘)。

程序仍然可以直接读取和写入文件,但如果程序不担心特定的文件路径,只使用标准输入和标准输出,则 Unix 方法效果最好。这允许 shell 用户以任何他们想要的方式连接输入和输出;该程序不知道或不关心输入来自哪里以及输出到哪里。(人们可以说这是一种松耦合(loose coupling),晚期绑定(late binding)或控制反转(inversion of control))。将输入/输出布线与程序逻辑分开,可以将小工具组合成更大的系统。

你甚至可以编写自己的程序,并将它们与操作系统提供的工具组合在一起。你的程序只需要从标准输入读取输入,并将输出写入标准输出,它就可以加入数据处理的管道中。在日志分析示例中,你可以编写一个将 Usage-Agent 字符串转换为更灵敏的浏览器标识符,或者将 IP 地址转换为国家代码的工具,并将其插入管道。sort 程序并不关心它是否与操作系统的另一部分或者你写的程序通信。

但是,使用 stdin 和 stdout 能做的事情是有限的。需要多个输入或输出的程序是可能的,但非常棘手。你没法将程序的输出管道连接至网络连接中。如果程序直接打开文件进行读取和写入,或者将另一个程序作为子进程启动,或者打开网络连接,那么 I/O 的布线就取决于程序本身了。它仍然可以被配置(例如通过命令行选项),但在 Shell 中对输入和输出进行布线的灵活性就少了。

透明度和实验

使 Unix 工具如此成功的部分原因是,它们使查看正在发生的事情变得非常容易:

  • Unix 命令的输入文件通常被视为不可变的。这意味着你可以随意运行命令,尝试各种命令行选项,而不会损坏输入文件。

  • 你可以在任何时候结束管道,将管道输出到 less,然后查看它是否具有预期的形式。这种检查能力对调试非常有用。

  • 你可以将一个流水线阶段的输出写入文件,并将该文件用作下一阶段的输入。这使你可以重新启动后面的阶段,而无需重新运行整个管道。

因此,与关系数据库的查询优化器相比,即使 Unix 工具非常简单,但仍然非常有用,特别是对于实验而言。

下一页