Unix编程艺术笔记

第1章 Unix 哲学

   K.I.S.S Keep It Simple, Stupid!

  一个程序只做好一件事,并做好。程序要能够协作,要能够处理文本流,因为这是最通用的接口。
   不懂Unix的人最终还要发明一个蹩脚的Unix。

   Robe Pike的表述:

  • a. 你无法断定程序会在什么地方耗费运行时间。(不要过早优化!)
  • b. 估量。在对代码没有进行估量前,不要优化速度。
  • c. 花哨的算法在n很小时通常很慢,而n通常很小。花哨算法的的常数复杂度通常很大。除非你确定n总是很大,否则不要用花哨的算法(即使n很大,也优先考虑原则b)。
  • d. 花哨的算法比简单算法更容易出bug、更难实现。尽量使用简单的数据算法配合简单的数据结构。(这个原则突出表现了Unix哲学传统追求实用、实效的特点,这也决定了它不可能诞生于那些‘过于理论的’‘学者’之手。)*(Ken Thomson: 拿不准就穷举。)*
  • e.数据压倒一切。如果已经选择了正确的数据结构并且把一切都组织得仅仅有条,正确的算法也就不言自明。编程的核心是数据结构,而不是算法。(SICP:一种数据结构和操作它的一百种方法要比10种数据结构,每种都有10种操作方法更简单。复杂的数据数据弥漫在各种流行的编程语言中。)

   总结:

  • a.模块原则:使用简单的接口拼合简单的部件。(Fred Brooks: 没有万能药。)
  • b.清晰原则:清晰胜于机巧。(很多人重机巧来‘炫耀’自己;一些公司管理拙劣,研发的系统过分追求机巧,可维护性极差,还没有自知之明。)……为了取得程序一丁点的性能提升就大幅度增加技术的复杂度和晦涩性。
  • c.组合原则:设计时考虑拼接组合。(既联结又独立)要想让程序具有组合性,就要使程序彼此独立。
  • d.分离原则:策略同机制分离,接口同引擎分离。
  • e.简洁原则:设计要简洁,复杂度能低就低。技术上的虚荣心理:程序员都很聪明,常常以能玩转复杂东西和卖弄抽象概念的能力为傲,这一点也无可厚非。(但程序员和一些科学家都‘嫉妒’数学家,因为没有什么人可以比数学家更能‘摆弄’抽象概念。)更为常见的是商业环境下的要求使得软件的复杂度上升。要避免这些陷阱,唯一的方法就是鼓励另一种软件文化,以简洁为美。
  • f.吝啬原则:除非别无它法,不要编写庞大的程序。(Windows平台的软件都以‘庞大’自居自傲!)
  • g.透明性原则:设计要可见,以便审查和调试。软件系统的透明性是指你一眼就能看出软件是做什么的以及怎么做的。显见性是指程序带有监视和显示内部状态的功能,这样程序不仅能够运行良好,而且能够看得出它以何种方式运行。
  • h.健壮性原则:健壮源于透明和简洁。在有异常输入的情况下,保证软件健壮的一个相当重要的策略就是避免在代码中出现特例。bug通常隐藏在处理特例的代码以及处理不同特殊情况的交互操作部分的代码中。
  • i.表示原则:把知识叠入数据以求逻辑质朴而健壮。数据要比编程逻辑更容易驾驭。在设计中,你应该主动将代码的复杂度转移到数据之中去。
  • j.通俗原则:接口设计避免标新立异。(最少惊奇原则)最易用的程序就是用户需要学习新东西最小的东西——或者,换句话说,最易用的程序就是最切合用户已有知识的程序。(程序和求知的关系,程序如果能够使得用户获得新的知识或者对已有知识产生更深刻的理解,那么程序和人的交互就会产生质的变化,程序不在仅仅作为一种解决问题的工具,而是一种认知的方式和媒介。)
  • k.缄默原则:如果一个程序没什么好说的,就保持沉默。
  • l.补救原则:出现异常时,马上退出并给出足够错误信息。
  • m.经济原则:宁花机器一分,不花程序员一秒。
  • n.生成原则:避免手工hack, 尽量编写程序去生成程序。
  • o.优化原则:雕琢前先得有原型, 跑之前先学会走。”90%的功能现在能够实现,比100%的功能永远实现不了强。”做好原型设计可以帮助你避免为蝇头小利而投入过多的时间。(Donald Knuth: 过早优化是万恶之源。)先制作原型,再精雕细琢。优化之前先确保能用。先给你的设计做个未优化的、运行缓慢、很耗内存但是正确的实现,然后进行系统地调整,寻找那些可以铜鼓牺牲最小的局部简洁性而获得较大性能提升的地方。Ken Thompson: “我最有成效的一天就是扔掉了1000行代码。”
  • p.多样性原则:绝不相信“不二法门”的断言。设计一种僵化、封闭、不愿意与外界沟通的软件,简直就是一种病态的傲慢。(这一点还表现在人的性格上!!!)
  • q.扩展原则:设计着眼未来,未来总比预想快。绝不要认为自己找到了最终答案。

第4章 模块性:保持清晰,保持简单

1. 紧凑性和正交性:

  • 紧凑性:这是一个设计能否装入人脑的特性。有经验的用户需要手册吗?许多软件是相对紧凑或者半紧凑的,即它们有一个紧凑的功能子集,能够满足专家用户80%以上的一般需求。
  • 正交性:任何操作都没有副作用,每一个动作只改变一件事,不会影响其他。无论你控制什么系统,改变每个属性的方法有且只有一个。重构:改变代码的组织和结构,而不改变其外在行为。重构的原则性目标就是提高正交性。
  • STOP原则:不要重复自身。任何一个知识点在系统内都应当有一个唯一、明确和权威的表述。无论何时,重复代码都是危险信号。
  • 强单一中心:Unix的许多有效工具都是围绕强单一算法直接转换的瘦包装器。
      不要想着多做什么事情,要尽量去想最少能做的事情。

2. 层次化:

  • 自底向上和自顶向下的结合。
  • 胶合层的作用:顶层的应用逻辑和底层的域原语必须用胶合逻辑层来进行阻抗匹配。胶合层要尽可能薄。 完美之道,不在无可增加,而在无可删减。

3. 程序库:
  Unix编程风格强调模块性和定义良好的API,它所产生的影响之一就是:强烈倾向于将程序分解为由胶合层连接的库集合,特别是共享库。

4.OO面向对象:
  OO往往产生过厚的胶合层,OO在GUI、仿真和图形领域取得成功,在于在这些领域很难弄错类型的本体问题。(或者说这些领域中模拟的对象以及对象间的关系与现实世界有着清晰的对应关系。)

5.模块化编码

  Unix 風格程序设计所面临的主要挑战就是如何将分离法的优点(将问题从原始的场景中简化、归纳)同代码和设计的薄胶合、浅平透层次结构的优点相结合。

   (LLVM的设计就是程序库、模块化和及模块、组合原则的应用体现。)

第5章 文本化:好协议产生好实践

  为了便于数据的传输和存储,像链表这样的数据结构,其可遍历的准空间部署需要平整化或者序列化为字节流表达,以便日后能够从这个表达中恢复数据结构。(我以前没有深入思考过这点,数据结构如何在不同的机器间的移植问题,现代的网络协议是如何做到这点的?)
  互用性,透明性,可扩展性和存储/事务处理的经济性——这些都是设计文件格式和应用协议时需要考虑的重要方向。
数据文件元格式:(范例:SMTP、POP、IMAP)

  • DSV风格: 使用分隔符分隔值(例如passwd文件使用冒号作为值分隔符)
  • RFC822风格:如HTTP1.1
  • Cookie-Jar风格:%%或%作为文本分隔符
  • Record-Jar风格:
  • XML
  • Windows INI

** Unix文本文件格式的约定:**

  • 1.如果可能,以新行符结束的每一行只存一个记录
  • 2.如果可能,每行不超过80个字符
  • 3.使用#引入注释
  • 4.支持反斜杠约定
  • 5.在每行一条记录的格式中,使用冒号或任何连续的空白作为字段分割符
  • 6.不要过分区分tab和whitespace
  • 7.优先使用十六进制而不是八进制
  • 8.对于复杂的记录,使用节(stanza)格式,一个记录若有多行,就使用%%\n或%\n作为记录分割符
  • 9.在节格式中,要么每行一个记录字段,要么让记录格式和RFC822电子邮件头类似,用冒号终止的字段名关键字作为引导字段
  • 10.在节格式中,支持连续行
  • 11.要么包含一个版本号,要么将格式设计成相互独立的自描述字节块
  • 12.注意浮点取整问题
  • 13.不要仅对文件的一部分压缩或二进制编码

  (LLVM IR 的设计体现了该原则的应用。)

第6章 透明性:来点光

  美在计算机科学中的地位,要比在其他任何技术中的地位都重要。因为软件太复杂了,而美是抵御复杂的终极武器。——《机器美学:优雅和技术本质》

  如果软件所包含的功能是为了帮助用户建立对软件”做什么,怎么做”的心理模型而设计的,这个软件系统就是可显的。…可显性是一种主动品质。

  优雅是力量和简洁的结合。优雅的代码事半功倍;优雅的代码不仅正确,而且显然正确;优雅的代码不仅将算法传递给计算机,同时还将见解和信心传递给阅读代码的人。通过追求优雅的代码,我们可以编写更好的代码。

  为透明性和可显性设计:

  • 不要在具体操作的代码上叠放太多的抽象层。

  为可维护性而设计

第7章 多道程序设计:分离进程为独立的功能

   做单件事并做好

  1. 从性能调整中分离复杂度控制
      尽量使用协作进程,而不是线程。
  2. IPC分类:
  • 将任务传给专门程序(shellout):两者不需要交流
  • 管道、重定向和过滤器
  • wrapper(包装器)
  • 从进程:需要内部状态机处理两者的通信协议(如scp和ssh)
  • 对等进程间通信:临时文件、信号、套接字、共享内存。
       慎用RPC和线程

第8章 微型语言:寻找歌唱的音符

   专门领域的小语言是非常强大的设计理念。使用微型语言比起采用低级硬编码的设计而言,降低了全局复杂度。首先,微型语言提高了编程问题规格的层次,这种表示法与通用语言所支持的表示法相比,更加紧凑,更具有表现力。(和SICP中的元语言抽象概念相似)。
  分类:

  • 数据格式:SNG,passwd,vimrc等
  • 微型语言:m4、yacc、lex、make、xslt、pic、tbl、eqn、awk、troff、script、(dc|bc)等。趋势是从声明性发展到命令性。
  • 解释器:(dc|bc)、Lisp、JS、tcl、shell、Perl、Python、Java

第9章 生成:提高规格说明的层次

  数据比程序更容易理解,不管这些数据是普通表格、说明性标记语言、模板系统还是一组可以扩展为逻辑控制的宏。尽可能把设计的复杂度由程序代码转移到数据中去。(说明式标记语言区别于过程性或者命令式语言,它只提供说明式的’做什么‘的知识,而不包含’怎么做‘的知识。)

  • 数据驱动编程 :(注意与SICP中数据导向风格编程的对比,在那里,数据与过程的界限已经很模糊。)与OO相比,这里的数据不仅仅指对象的状态还表示程序的控制流。这里重要的思想其实是将程序逻辑从硬编定的控制结构转移到数据中。*尽量避免手工编码。*
  • 专用代码的生成:尽量少干活、让数据塑造代码、依靠工具、将机制从策略中分离。建设性的懒惰是大师级程序员的美德。

第10章 配置:迈出正确的第一步

Unix:一切都是可配置的。 但什么是不应该可配置的?

  • a. 能够可靠的进行自动检测的东西,就不要提供配置开关(自动检测可能会稍微增加运行时的延迟,但延迟不超过一定界限就是可以接受的);
  • b. 能够用脚本包装器或简单管道实现的任务就不要用配置开关实现。能够简单使用其他程序实现的任务,就不要增加本程序的复杂度。(*不要过度配置*)

配置的位置:对调用时可能发生变化的选项,使用命令和开关;对改动很少但确实应该由各个用户控制的选项,使用用户主目录的运行控制文件;对需要由系统管理员设置而不是由用户改变的整个系统级选项数据,使用整个系统里运行控制文件。

  • 特殊情况是坏消息。(特殊情况会破坏程序的简单性和透明性,增加复杂度。)

运行控制文件配置的一般通用规则:

  • 支持说明性注释
  • 不要区别隐匿的空白符
  • 把多个空行和注释行视为单个空行
  • 词法上把文件视为简单的用空白分隔的标记序列,或多行标记
  • 支持反斜杠语法以在字符串中嵌入不可打印字符或者特殊字符

环境变量:系统环境变量、用户环境变量。在有些情况下,使用用户环境变量是有用的,特别当这些变量由大量不同程序共享,独立于应用程序的优先选项。**

使用环境变量的情况:

  • 变量值根据共享点文件或者父进程需要向多个子进程传递信息的上下文环境的不同而变化。
  • 变量值随点文件不同而频繁改变,但每次启动都不变化。
  • 进程唯一的覆盖必须以不要求改变命令行调用的方式来表述。例如LD_LIBRARY_PATH环境变量。

命令行选项:

第11章 接口

  最小立异原则:如有可能,尽量允许用户将接口功能委派给熟悉的程序来完成。提倡以共生或委派策略来提高代码的复用并降低软件复杂度。**不能委派就仿效。**
   评估标准:简洁、表现力、易用、透明和脚本化能力。
  Unix接口设计模式:

  • 过滤器模式
  • Cantrip模式:没有输入、没有输出、只被调用一次,产生退出状态数值
  • 源模式:不需要输入、输出在启动条件中控制。如ls、who、ps等。
  • 接收器模式:只接纳标准输入而不发送任何东西到标准输出。
  • 编译器模式:无标准输入、无标准输出,只将错误信息发送到标准错误端。
  • ed模式:交互设计模式
  • Roguelike模式:支持全屏幕、可视界面风格、但使用字符界面显示,没有鼠标和图形界面。
  • 引擎与接口分离模式:将程序的引擎部分与接口部分分离。(参考MVC模式,模型-视图-控制器)。这种模式的一种形式是将策略接口(通常是结合了视图和控制器功能的GUI)和包含了一个专用定义域微型语言解释器的引擎相连。(如mysql数据库程序)
    • 配置者/执行者组合
    • 假脱机/守护进程组合: Spooler将任务和数据放入spool目录,daemon不断轮询spool目录,如果有任务就执行任务,完成后将其从spool目录中删除。 (如Unix打印程序 lpr/lprd)
    • 驱动/引擎组合:a)交互性 b)引擎以自身接口单独运行的能力。
    • CS组合:两者之间的协议可以由开放标准定义(如HTTP、FTP、DNS等)
    • CLI服务器
  • 基于语言的接口模式
  • 多价程序模式:程序的应用定义域逻辑封存在一个文档化的API库中,该库可以被其他程序链接。程序同外部的接口逻辑是一个基于库的薄胶合层。或者有几个不同风格的UI层,每一个层都可以链接该库。

第12章 优化

   过早优化是万恶之源
  程序员工具箱中最强大的优化技术就是不去优化。
   小就是美

第13章 复杂度:尽可能简单,但不要简单过了头

   代码量、实现复杂度和接口复杂度
   本质的、偶然的和选择复杂度
  不要为了简洁而简洁。同其他美学形式一样,我们需要注意何时设计上的简约已经不再是有价值的自律形式,而开始成为一件伪装的苦行者外衣——一种实际上把美德作为接口来敷衍工作的纵容方式。
   框架的重要性:对共享上下文环境的统一管理,是Emacs的选择复杂度换来的。小巧锐利工具的教义,降低接口复杂度和代码库规模的压力,可能正好导向手工陷阱——用户不得不自己维护所有共享的上下文环境。(vi的专用陷阱问题)

第14章 C还是非C

  要有效应用Unix哲学,在工具包中就不能只有C语言,必须学会使用Unix的其他语言(特别是脚本语言),并且学会如何在大型程序系统中把担任各个专门角色的多个语言轻松地融合在一起。

第15章 工具,开发的战术

  • 操作系统:IDE对于缺乏工具的单一语言编程非常有意义。然而在Unix下,语言和工具的选择广泛多样。所以同时使用多个代码生成器、定制配置器以及许多其他标准定制工具就是司空见惯的事。Unix提倡一种更灵活的风格,一种以编辑/编译/调试循环为中心、排它性更少的风格。
  • 编辑器viEmacs
  • 专用代码生成器lexyacc
  • 自动化编译: make工具,尽量不要嵌套使用make,这样会使问题复杂化。
  • 版本控制系统
  • 运行期调试:牢记Unix哲学。将时间花费在设计质量上,而不是低层次的细节上,尽可能自动化一切——包括运行期调试的细节工作。
  • 性能分析
  • 使用Emacs整合工具

第16章 重用:不要重复发明轮子

   重用代码是程序员良好的美德。

   透明性是重用的关键。

   开源:设计最好的实践需要情感的投入,而不是冷漠无聊的过程。软件开发者,同任何其他类型的工匠和技师一样:它们想要成为艺术家,这并不是什么私密。他们不仅仅希望重用代码,也希望自己的代码得到重用。

第17章 可移植性

第18章 文档:向网络世界阐释代码

   troffTEX排版工具; TextinfoHTMLDocBook

第19章 开放源码:在Unix社区中编程