Unix编程艺术集萃

这本书从Unix的设计理念等各方面讲起,内容充实有趣,尽管介绍细节的部分对于不太了解Unix的人呢来讲有些生涩,但在道的介绍上有不少可取之处。由于某人的出现,生活中多了新的追求,比想象中多用了一些时间看完了它。下面摘取一些其中精华的观点,力图尽量还原书中的本意。

Context

哲学

  • 每过18个月,就有一半的知识会过时
  • X致力提供一套“机制,而不是策略”
  • 提供机制而不是方针的哲学让Unix长期保鲜
  • Unix传统将重点放在尽力使各程序接口相对小巧、简洁、正交
  • Unix管道发明人Doug McIlroy曾说过:
    • 让每个程序就做好一件事
    • 假定每个程序的输出都会成为另个程序的输入
    • 尽早将设计和编译的软件投入使用
    • 优先使用工具而不是拙劣的帮助来减轻编程任务的负担
  • Rob Pike曾从不同的角度表述了Unix的哲学
    • 你无法断定程序会在什么地方好费时间,所以别急于找地方改代码,除非你已经证实那儿就是瓶颈所在
    • 没对代码估量最耗时的部分前,别去优化速度
    • 花哨的算法在n很小的时候通常很慢,而n一般很小
    • 花哨的算法比简单算法更容易出bug、更难实现
    • 编程的核心是数据结构,而不是算法
  • 书中对Unix的哲学,总结为下面这些点
    • 模块原则,使用简洁的接口拼接简单的部件
    • 清晰原则,清晰胜于机巧,程序是写给人看的
    • 组合原则,设计时要考虑拼接组合
    • 分离原则,策略和机制分离,接口和引擎分离
    • 简洁原则,设计要简洁,复杂度能低则低
    • 吝啬原则,除非没有办法,不要写大程序
    • 透明性原则,设计要有可见性(如输入输出、打点上报),便于审查和调试
    • 健壮原则,健壮源于透明和简洁
    • 表示原则,把知识转移到数据中,保证逻辑的简朴而健壮
    • 通俗原则,即最少惊奇原则,接口设计避免标新立异,缓和学习曲线
    • 缄默原则,设计良好的程序将用户的注意力视为有限的宝贵资源
    • 补救原则,出现异常时,马上退出并给出错误信息
    • 经济原则,宁花机器1分,不花程序员1秒
    • 生成原则,避免手工hack,编写程序去生成程序
    • 优化原则,过早优化会妨碍全局优化,先制作原型,再精雕细琢;先可用,再优化
    • 多样原则,不相信“不二法门”的断言
    • 扩展原则,未来总比预想来得要快
  • Unix哲学一言以蔽之,即KISS(Keep It Simple, Stupid!)
  • 善用他人写好的工具,尽可能将一切自动化

历史

  • 计算机不应仅被视为一种逻辑设备而更应视为社群的立足点
  • 1985年IEEE支持的POSIX标准表述了BSD和SVR3(System V Release3)调用的交集
  • 1987年初,GNU C编译器第一版问世
  • 1995年,Linux找到自己的杀手级应用——开源的web服务器Apache
  • 过度依赖任何一种技术或者商业模式都是错误的

Unix哲学和其他哲学的对比

  • 统一性理念:一切皆文件 & 管道概念
  • 多任务能力:抢先式多任务
  • 协作进程:低价的进程生成和简便的进程间通讯
  • 内部边界:程序员最清楚一切
  • 文件属性和记录结构:没有文件属性
  • 减少使用不透明的二进制文件格式
  • 首选CLI命令行界面
  • Unix是程序员写给程序员的
  • 开发的门槛:轻松编程
  • 操作系统的比较
    • MacOS:MacOS有一个自己的界面方针,非常详细地说明了应用程序GUI的表现形式和行为模式
    • Windows NT:有注册表蠕变现象,不过支持了Cygwin,实现了Unix API的兼容
    • MVS:一切皆批处理
    • Linux:贴近终端用户的愿望使得Linux开发者比专有Unix更注重系统安装的平稳性和软件发布问题

Design

模块性

  • 展开来说就是,要编写复杂软件又不至于一败涂地的唯一方法就是用定义清晰的接口把若干简单模块组合起来
  • Unix程序员骨子里的传统是:更加笃信重视模块化、更注重正交性和紧凑性等问题
  • 封装良好的模块不会过多向外披露自身的细节
  • 缺陷个数随着模块的代码行数会先减小,随后按代码行数的平方上升
  • 紧凑性和正交性
    • 人类短期记忆能够容纳的不连续信息数就是7,加2或减2
    • 紧凑性:有经验的用户通常不需要操作手册,让人乐于使用,不会在想法和工作间格格不入;紧凑不等于薄弱;一个功能子集,能够满足专家用户80%以上的一般需求
    • 正交性:任何操作均无副作用,改变每个属性的方法有且仅有一个;重构代码就是改变代码的结构和组织,而不改变其外在行为
    • 任何一个知识点在系统内都应当有一个唯一、明确、权威的表述(Single Point of Truth, SPOT)
    • 提高紧凑性的精妙但强大的办法就是围绕“解决一个定义明确的问题”的强核心算法组织设计,避免臆断和捏造。形式化往往能极其明晰地阐述一项任务,与形式法相对的是试探法——凭经验法则得到的解决方案,这种思路的问题是回增生出大量特例和边界情况
  • “限制不仅提倡了经济性,而且某种程度上提倡了设计的优雅”。要达到这种简洁性,尽量不要去想一种语言或操作系统最多能做多少事情,而是尽量去想这种语言或操作系统最少能做的事情——不是带着假想行动
  • 设计有自顶向下和自底向上两种思路,前者通常先考虑主事件循环,再插入具体事件;后者通常先考虑封装具体任务,再按次序粘合在一起
  • 出于自我保护,程序员尽量双管齐下——一方面以自顶向下的应用逻辑表达抽象规范,另一方面以函数或库来手机底层的域原语(原子操作)
  • 实际代码往往是自顶向下和自底向上的综合产物。同一个项目经常同时兼有,这就导致了“胶合层”的出现
  • 胶合层是个挺讨厌的东西,必须尽可能薄,这一点极为重要。薄胶合层原则可以看做是分离原则的升华。策略(应用逻辑)应该与机制(原子操作集)清晰地分离和解耦。
  • “完美之道,不在无可增加,而在无可删减”
  • OO语言鼓励“具有厚重的胶合和复杂层次”的体系。当问题域真的很复杂、确实需要大量抽象时,这可能是好事,但如果编程员到头来用复杂的方法来做简单的事情——仅仅是为他们能够这样做,结果便适得其反
  • 全局变量意味着代码不能重入
  • “就我个人而言,如果局部变量太多,我倾向于拆分子程序。另一个方法是看代码行是否存在(太多)缩进。我几乎从来不看代码长度。——Ken Thompson”
  • 如果通过电话向另一个程序员描述说不清楚,API可能就是太复杂,设计太糟糕了。

文本化

  • 序列化有时被称为列集(marshaling),其反向操作(载入)过程称为散集(unmarshaling)
  • 互用性、透明性、可扩展性和经济性都是设计文本格式和应用协议需要考虑的问题
  • 设计一个文本协议往往可以微系统的未来省不少力气;使用二进制协议的唯一正当理由是:如果要处理大批量的数据集,因而确实关注能否在介质上获得最大位密度,或是关心数据转化时的时间或指令开销。大图像和多媒体数据的格式有时可以算是前者的例子,对延时有严格要求的网络协议有则算作后者的例子
  • 文本格式的位密度未必一定比二进制格式低多少;设计紧凑二进制格式的思路往往不能够兼顾干净扩展的要求
  • 数据文件元格式是一套句法和词法约定,已经正式标准化或者通过实践得到充分确定
    • DSV:冒号是默认的分隔符
    • RFC 822:字段名不得包含空格,通常用横线代替,空格和tab作为当前逻辑行的延续
    • XML:需要文档类型定义(如XHTML)和相关应用逻辑赋予其语义。通常可以语法检查就能发现形式问题或数据错误
    • Unix文本文件约定
      • 如果可能,以新行符结束的每一行只存一个记录
      • 每行建议少于80字符
      • #开始注释
      • 支持反斜线\
      • 用冒号或连续空白作为字段分隔符
      • 在节格式中,支持连续行
      • 要么包含一个版本号,要么将格式设计成相互独立的自描述字节块
      • 不要只压缩或者二进制编码文件的一部分
  • 应用协议设计:如果应用协议是文本式的,那仅凭肉眼就能容易地分析,例:SMTP、POP3、IMAP
  • 应用协议元格式:应用协议元格式是为了简化网络间事务处理的序列化操作而发展出来的,因为网络带宽要比存储昂贵得多,所以需要重视事务处理的经济性
  • 目前还没有个制订较完善的元协议非常适合真正的P2P应用,不像客户端-服务器应用——HTTP在这一领域的游刃有余

透明性

  • 如果软件系统包含的功是为了帮助人们对软件建立正确的“做什么、怎样做”的心理模型而设计,这个软件系统就是可显的
  • 用户喜欢UI中的透明性和可显性,是因为这意味着学习曲线比较平缓,而“最小立异原则”就是一个体现
  • 优雅是力量与简洁的结合。优雅的代码事半功倍;优雅的代码不仅正确,而且显然正确;优雅的代码不仅将算法传达给计算机,同时也把简洁和信心传递给阅读代码的人
  • 编写透明、可显的系统而节省的精力,将来完全可能就是自己的财富
  • 用户的注意力是宝贵的,让Unix工具正常运行的最好策略是在大部分时间里沉默
  • 真正的聪明是找到方法,可以访问部分细节,但是又不让它们太显眼
  • 为透明性和可显性而设计
    • 不要在具体操作的代码上叠放太多的抽象层
    • 透明性和可显性同模块性一样,主要是设计的特性而不是代码的特性
      • 程序调用层级最大深度是多少?
      • 代码是否有强大而明显的不变性质
      • API的各函数调用是否正交
      • 程序的数据结构或分类和它们代表的外部实体间,是否有一一映射关系
      • 有多少魔法数字
    • 隐藏细节和无法访问细节有着重要区别
    • 透明的系统更容易实施恢复措施,首先就是更能抵抗bug的破坏
  • Unix程序员的品性:“宁愿抛弃、重建代码也不远修补蹩脚的代码”
  • 选择简单的算法

多路程序控制

  • Unix最具特点的程序模块化技法就是将大型程序分解成多个合作进程
  • Unix的设计风格强调用定义良好的进程间通信或共享文件来联通小型进程。因此,Unix操作系统提倡把程序分解成更简单的子进程,并专注考虑它们之间的接口
    • 降低进程生成的开销
    • 提供方法(shellout、IO重定向、管道、消息传递、套接字)简化进程通信
    • 提倡使用简单透明的文本数据格式来通信
  • 真正的难题不在协议语法而是协议逻辑——协议必须既有充分的表达能力又有防范死锁的能力
  • Unix的IPC分类
    • 最简单的形式:调用另一个程序来完成任务;专门程序通常借由文件系统和父进程通信
    • 管道、重定向和过滤器:过滤器即从标准输入顺序读数据,然后向标准输出写数据;管道操作把程序的标准输出连接到另一个程序的标准输入
    • 包装器:包装器或者将调用程序专用化,或者为它创建新的接口
    • 从进程:子程序通过连接到标准输入和标准输出的管道,交互地和调用程序收发数据
    • 对等进程通信:需要对等的通道
      • 临时文件:最古老的的IPC技法,灵活但有风险
      • 信号:每个信号都对机收进程产生默认作用,进程可以声明信号处理程序,让信号处理程序覆盖信号的默认行为
        • SIGHUP,重新初始化
        • SIGTERM,温和的终止
        • SIGKILL,立即杀死进程
      • 套接字:套接字类似文件描述符,创建时可以指定协议族来告诉网络层如何解释套接字名称
      • 共享内存:共享内存通常依靠mmap,把文件映射成可以被多个进程共享的内存

微型语言

  • 程序员每百行代码出错率和所使用的编程语言在很大程度上无关
  • 有两个好方法和一个坏方法做好微型语言的设计
    • 预先认识到可以使用微型语言设计把变成问题的规格说明提升一个层次
    • 注意到规格说明文件格式越来越像微型语言——规格中蕴含着行为
    • 错误的方法是通过扩展变成微型语言
  • 微型语言的范畴从声明性发展到命令性,从而逐渐具有通用性,当他们明确为完备图灵机时,它们就是解释器
  • 样例
    • SNG,PNG的纯文本表达
    • 正则表达式
    • Glade,描述GUI界面的XML文件
    • m4,一套宏指令集,规定文本串扩展成其他文本串的方式
    • XSLT,描述XML数据的变换
    • awk,将文本输入变换成文本输出
    • PostScript,向成像设备描述排班文本和图片的微型语言
    • bc、dc,任意精度计算
    • Emacs Lisp
    • JavaScript
  • 设计微型语言
    • 控制复杂度,声明性微型语言应该具有一个明确、一直、类自然语言的语法被人类所阅读
    • 扩展和嵌入脚本语言,实现命令性语言
    • 编写自定义语法
    • 慎用宏

生成

  • 人类其实更善于肉眼观察数据而不是推导控制流程
  • 数据比程序逻辑更易驾驭
  • 数据驱动编程:把代码和代码作用的数据结构分清楚,始终把问题层次往上推,尽量把程序逻辑转移到数据中
  • 专用代码的生成:尽可能少干活,让数据塑造代码,依靠工具,分离机制和策略

配置

  • 无论何时想增加配置选项,最好考虑下下面的问题
    • 能省掉这个功能么
    • 能否有无伤大雅的方式改变程序常规行为
    • 选项是否过于花哨
    • 需不需要一个独立的额外程序
  • Unix的程序配置信息一般在以下5个位置
    • /etc下的运行控制文件
    • 系统设置的环境变量
    • 用户主目录下的运行控制文件(通常用.开头)
    • 用户设置的环境变量
    • 启动程序命令行参数
  • 可执行未见后面加rc表示“运行控制”(命名来自CTSS的runcom命令脚本功能)
  • 一些最为常见的系统环境变量:USERLOGNAMEHOMELINESSHELLPATH
  • 常见的从-a-z的命令行选项的可能含义
    • -a,所有、添加
    • -b,缓冲区、批处理
    • -c,命令、检查
    • -d,调试、删除
    • -D,定义
    • -e,执行、编辑
    • -f,文件、强制执行
    • -g,全局
    • -h,头部、帮助
    • -i,初始化、交互式
    • -k,保留、杀死
    • -l,列表、登录、加载
    • -m,消息、邮件
    • -n,数字、否
    • -o,输出
    • -p,端口
    • -q,安静
    • -r,递归
    • -s,缄默,大小
    • -t,标记
    • -u,用户
    • -v,冗长
    • -V,版本
    • -w,宽度
    • -y,是
    • -z,启用压缩

接口设计

  • 从最小立异原则出发,启动后程序通常从下列来源获得输入或命令
    • 程序标准输入端的数据和命令
    • 通过IPC的输入
    • 已知位置的文件和设备
  • 最小立异原则不应被理解为在设计中号召机械的保守主义,新颖性提高了用户与接口最初几次的交互成本,但是糟糕的设计永远使接口令人痛苦而多余
  • “我们提倡以共生和委派策略来提高代码的复用并降低软件复杂度”
  • 最小立异原则目的就是为了减少用户在使用接口时必须学习的复杂过程
  • Unix接口设计历史:CLI => X
  • 接口的5种度量标准:简洁、表现力、易用、透明和脚本化能力
    • 简洁:事务处理需要的时间和复杂度需要有上限
    • 表现力:接口可以触发广泛的行为
    • 易用性:接口要求用户记忆的东西较少
    • 透明度:用户使用接口时,几乎不用记忆什么问题、数据或者程序状态
    • 脚本能力:容易被其他程序使用
  • CLI和可视化接口的对比
    • CLI更具表达力、脚本化能力、简洁性,适用于举例:SQL
    • 可视化接口透明度、易用性较好,适用性举例:画图、网页浏览器
    • 随着用户越来越熟练,对CLI接口的抵触也越少
  • Unix接口设计模式
    • 过滤器:接受输入,转换成其他格式,再输出到标准输出端;宽进严出、不丢弃不需要的信息、不增加无用数据
    • cantrip(没有输入输出)、源模式(无输入且输出在启动条件中控制)、接收器模式(接收输入但不发送东西到输出)、编译器模式(无标准输入输出,但会发送信息到标准错误端)
    • ed模式(编辑器模式)
    • roguelike模式(来自BSD的地牢探险游戏rogue,用字符阵列显示界面UI),如vi、emacs,没有鼠标参与,适合指法熟练的人
    • 引擎和接口分离,又或者模型和视图分离,了解MVC模式的人自然了解
      • 配置者、执行者
      • 假脱机、守护进程
      • 驱动、引擎
      • 客户端、服务端
    • 基于语言的接口模式
  • 浏览器作为通用前端
  • 如果程序没有什么有趣的或者惊奇的东西要说就应该闭嘴(有点意思)

优化

  • Unix的经验告诉我们最主要的就是如何知道何时不去优化
  • 最强大的优化技术就是不去优化
  • 先估量,后优化,直觉是糟糕的向导
  • 最有效的代码优化方法是保持代码短小简单
  • 核心数据结构必须留在最快的缓存
  • 吞吐量和延迟时间的权衡是普遍现象,例TCP、UDP
  • 对于减少延迟来说,阻塞或等待中间结果都是致命的
  • 按需计算出昂贵的结果,再缓存起来之后使用,可以兼得低延迟高吞吐

复杂度

  • 简单即美即雅即善,而复杂即丑即怪即恶
  • 程序员为了理解一个程序,会建立思维模型并调试之;程序的复杂度即模型建立和程序调试的困难程度
  • Unix思想的一个主题就是工具小巧锐利,设计从零开始,接口简单一致
  • 偶然复杂度的产生是因为没有找到实现规定功能集合的最简方法,可以通过良好设计去除;选择复杂度和期望的功能相关联,只能通过修改工程目标解决
  • 计算资源以及人类的思考,同财富一样,不是靠储藏而是靠消费来证明价值的
  • 选择需要管理的上下文环境,并且按照边界所允许的最小化方式构建程序

Implementation

语言

  • C和C++以增加实现时间和(特别是)调试时间为代价来优化效率
  • C的内存管理是复杂性和错误的渊薮
  • C语言最佳之处是资源效率和接近机器语言,糟糕的地方是槟城简直是资源管理的炼狱
  • C++试图满足所有人的所有要求
  • Perl是增强版的shell,它为替代awk而设计,适合大量使用正则表达式的地方
  • Java的设计目标是“write once, run anywhere”,但它并没有实现这两个最初的设计目标
  • Java对小项目是大材小用
  • Emacs Lisp传统上只用于为Emacs编辑器编写本身的控制程序

重用

  • 重新发明轮子之所以糟糕不仅因为浪费时间,还因为它浪费的时间往往是平方级
  • 避免重新发明轮子的有效方法就是借用别人的设计和实现,即重用代码
  • 文档并不能传达代码具现的所有细微差别之处
  • 开放源码和代码重用的关系,许多地方很像浪漫爱情和有性生殖的关系
  • 设计最好的实践需要情感的投入;软件开发者,同其他任何类型的工匠和技师一样;他们想要成为艺术家,这并不是什么私密。他们有艺术家的动力和需求,也拥有听众的欲望
  • 开放源码是从意识形态上解决这些所有问题的优先方法
  • 发布不够格软件的作者会承受许多的社会压力来修正或撤回代码(不一定)
  • 阅读代码是为未来而投资
  • 许可证
    - 许可证能够授权代码以某种形式使用,否则在版权法之下是禁止或者需要付费的
    - 非商业使用的许可证并不等同于开源许可证

Community

可移植性

  • C语言基于早期Ken Thompson的B语言解析器,脱胎于BCPL(Basic Common Programming Language),因此这个C代表Common(通用)
  • 在IETF传统中,标准必须来自于一个可用原型实现的经验;不幸的是,这并不是标准通常发展的方式
  • 搞笑RFC大概是唯一能够立即成为RFC的提议,比如RFC 1149(IP数据报的信鸽传递),RFC 2324(超文本咖啡壶控制协议)
  • 对于具备提倡标准资格的RFC,其规格必须稳定,经过同行评审,并且已经吸引了互联网社区的极大兴趣
  • IETF标准化过程有意提倡由实践而非理论驱动的标准化过程
  • 国际化的首要动作:分离信息库(配置)和代码
  • “暗含的意思就是,成为标准的最好方法就是发布一个高质量的开源实现” —— Henry Spencer

文档

  • “一切皆HTML,所有引用都是URL”
  • 绝大多数软件的文档都是由技术人员写给可能连最小公分母都不知道的普通大众的——渊博者写给无知者
  • 编写Unix文档的最佳实践
    • 数量多不会被认为是质量高
    • 信息密度适中,少用屏幕截图
    • 没人喜欢庞大的文档,考虑提供快速的摘要

开放源码

  • 开源开发的规则
    • 源码公开
    • 尽早发布,经常发布
    • 给贡献以表扬
  • major.minor.patch,补丁号修正错误和次要功能;次版本号为兼容的新功能;主版本号为不兼容的更改
  • 发布前对文档和README进行拼写检查
  • 基于所需功能而不是平台来编写移植层(面向接口编程)
    • #ifdef#if是最后一招,这通常是思路不当、产品过度差异化,无理由‘优化’或是无用垃圾聚集的先兆” —— Doug Mcllroy
  • 选择一个编码规范(lint)
  • 常见的标准文件命名规范
    • README
    • INSTALL
    • AUTHORS
    • NEWS
    • HISTORY
    • CHANGES
    • COPYING 项目许可证条款
    • LICENSE
    • FAQ
  • 以版本号来命名目录,考虑多版本在同一系统共存
  • 在设计讨论中更广泛的参与常常是件好事,但是如果列表相对开放,迟早就会有些用户在其上询问一些初级问题
  • 开源许可证
    • MIT:授予无限权利的拷贝、使用、修改和对修改的再发布,只要在所有修改版本中保留版权和许可证条款
    • BSD:授予无限权利的拷贝、使用、修改和对修改的再发布,只要在所有修改版本中保留版权和许可证条款;同时在广告和软件包相关文档中包含致谢
    • Artistic:授予无限权利的拷贝、使用和本地修改的权利。允许在发行修改后的二进制版本,但是限制源码再发行
    • GPL、Mozilla

未来

  • 分离机制(配置)与策略(算法)成为一个明确准则
  • Unix文件仅仅是个字节大袋子,而没有其他文件属性
  • 开放源码将软件业转变为服务业

–END–