UNIX编程艺术 Jun, 2015

读书笔记 编程

这本书极为经典,虽然后半部分的细节有些过时,但是前面的哲学影响深远且富有启发性。

哲学

UNIX有它独有的文化和哲学,它富有生命力且影响深远。UNIX诞生于1969年,而今天化身为Linux、BSD、MacOS X等,应用广泛且强大。

UNIX文件在字节层次上再无结构,文件删除了无法恢复,作业控制有欠精致,命名方式混乱。这些都是UNIX的缺点。

最大的争议在于,提供机制,而不是策略。 比如X Window,提供一套极端通用的图形操作,将界面的观感(策略)推后到应用层。 这使得UNIX可以提供很多行为选项和令人眼花缭乱的定制功能。 然而它的代价就是当用户“可以”设置自己的策略时,他们“必须”设置自己的策略。 这使得UNIX失去了很多非技术用户,但是策略相对短寿,机制才会长存。 只提供机制才能使得UNIX长久保鲜。

同时,UNIX还有着及其丰富和优秀的外围文化。 开源软件,跨平台可移植和开放标准(IEEE的可移植操作系统标准POS很快被大家加后缀变成了POSIX),Internet和TCP/IP协议,开源社区,从头到脚的灵活性(在其他系统中,完成设计者预见的任务容易,但是设计者没有预料到的就很难),以及UNIX hack很有乐趣。

UNIX哲学起源与Ken Thompson早期关于如何设计一个服务接口简洁、小巧精干的操作系统的思考,一路成长且博采众长。 UNIX管道的发明人Doug Mcllroy总结,UNIX哲学是一个程序只做一件事,并做好。程序要能协作。 程序要能处理文本流,因为这是最通用的接口。整体上,UNIX哲学可以概括为一下几点:

  • 模块原则:使用简洁的接口拼合简单的部件

    编制复杂软件而又不至于一败涂地的唯一非方法就是降低其整体复杂度,用清晰的接口把若干简单的模块组合成一个复杂的软件。

  • 清晰原则:清晰胜于机巧

    些程序时,要想到你不是写给计算机看,而是写给人看的。优雅而清晰的代码不仅不容易崩溃,而且更易于让后来的修改者立刻理解。

  • 组合原则:设计时考虑拼接

    在输入输出方面,UNIX传统极力提倡采用简单、文本化、面向流、设备无关的格式,否则很难和其它程序衔接。要想让程序具有组合性,就要使程序彼此独立。

  • 分离原则:策略同机制分离,接口同引擎分离

    策略短寿,机制长存。在探索新策略的时候尽量不要打破机制,这样也可以为机制编写更好的测试。

    在GUI之外也可以应用这个原则,如Emacs编辑器使用内嵌的脚本语言Lisp解释器来控制用C编写的编辑原语操作。

  • 简洁原则:设计要简洁,复杂度要低

    复杂的东西代价更高,bug更多。以简洁为美,总设法将程序系统分解成几个能够协作的小部分。

  • 透明性原则:设计要可见,以便审查和调试。

    透明性是说一眼能看出软件是在做什么以及怎样做的,显见性是说程序带有监视和显示内部状态的功能。尽早设置调试选项。

  • 健壮原则:健壮源于透明与简洁

    健壮性指在超出设计者预想外的条件下也能运行良好。

  • 表示原则:把知识叠入数据以求逻辑质朴而健壮。

    数据比变成逻辑更容易驾驭。主动将代码的复杂度转移到数据之中去。

  • 通俗原则:接口设计避免标新立异

  • 缄默原则:如果一个程序没什么好说的,就保持沉默

    只输出重要的东西。

  • 补救原则:出现异常时,马上退出并给出足量错误信息

    “宽容的收,谨慎的发”。

  • 经济原则:宁花机器一分,不花程序员一秒

  • 生成原则:避免手工hack,尽量编写程序去生成程序

  • 优化原则:雕琢之前先有型

    先制作原型,再精雕细琢。从原型中寻找通过牺牲最小局部简洁性而获得较大性能提升的地方。

  • 多样原则:绝不相信所谓“不二法门”断言

  • 扩展原则:设计着眼未来,未来总比预想的快

    一言以蔽之,就是KISS,Keep it simple and stupid。

历史————双流记

忘记过去的人,注定要重蹈覆辙。

对比:UNIX哲学同其他哲学的比较

UNIX风格,一切皆文件,以及在此之上的管道概念。

UNIX中,进程生成代价低,并且有简便的进程间通讯。使得众多小工具、管道和过滤器组成了一个均衡系统。 而反UNIX系统必须编写庞大的单个程序,并且进程间通讯要知晓彼此的细节,否则就只能采取低效不安去的方式。

UNIX中内部边界清晰。 有多用户权限,以及把设计关键安全性的功能限制在尽可能小的可信代码块上。 即使shell也不是什么特权程序。

UNIX文件不是靠后缀名识别的,没有文件属性,但是可以通过magic number来识别文件。 文件属性在管道中会引发棘手的问题,对文件属性的支持会使程序员使用不透明的文件格式。

如果不像UNIX强调CLI命令行界面,那么程序设计不会考虑以未预料的方式合作,难以实现远程系统管理,守护进程难以实现。

UNIX开发门槛很低,有大量廉价工具和简单接口,开创了轻松编程的先河。

模块性,保持清晰,保持简洁

封装和最佳模块大小

模块太小时,复杂性全在于接口,阅读代码需要知道大量接口。模块太大时,模块内部代码bug太多。这都不是很好。

紧凑型和正交性

紧凑性是指一个设计内否装进人脑,有经验的用户是否不需要手册。 不要有内部功能冗余,否则会出现功能子集成为“方言”,“方言”之间相互不能理解。

正交性是说每一个动作只改变一件事而不影响其他。正交性缩短了测试和开发的时间。重构的原则性目标就是提高正交性。

任何一个知识点在系统内都应当有一个唯一、权威的描述。

围绕“解决一个定义明确的问题”的强核心算法组织设计,避免臆断和捏造。

软件是多层的

纯粹自顶向下设计和自底向上设计都不好,可以结合使用。 但是需要胶合层来协调策略和机制。

C语言作为结构汇编程序是薄胶合层的经典代表。 完美之道,不在无可增加,而在无可减少。

UNIX强烈倾向于把程序分解成由胶合层连接的库集合。

OO经常被过分推崇为解决软件复杂性的唯一方法,这违背了多样性原则。

OO在其取得成功的领域(GUI,仿真,图形)之所以成功的主要原因之一是因为在这些领域里很难弄错类型的问题本体。

文本化:好协议产生好实践

文本化的重要性

文本流非常通用,并且通过压缩文本可以有很好的性能。

二进制格式限制了位数,文本可拓展性更强。

数据文件元格式

尽可能使用元格式而不是标新立异。

DSV,分隔符分隔值。如/etc/password用冒号分隔,反斜杠转义。

RFC 822,电子邮件格式。HTTP1.1也是这种格式。属性每行一个,冒号接空白结束。

Cookie-Jar和Record-Jar,用%%\n结束。

XML,适合复杂数据格式,但是不能被传统UNIX工具解析。

UNIX文本文件的约定:

  • 如果可能,以换行符结束的每一行只存一个记录。

  • 如果可能,每行不超过80个字符。

  • 使用#引入注释。

  • 支持反斜杠约定。

  • 使用冒号或连续空白作为分隔符。

  • 不要过分区别TAB和空格(令人头疼的python缩进)。

  • 对于复杂的记录,使用节格式。使用%%\n作为分隔符。并支持连续行(如Cookie-Jar)。

  • 包含版本号,或将格式设计成相互独立的自描述字节块(如PNG)。

  • 不要仅对文件的一部分进行压缩或二进制编码。

应用协议设计

//TODO 留着以后看。。。

透明性,来点儿光

透明性是说知道程序在干什么,可显性是说知道程序怎么干的。

优雅的代码不仅正确而且显然正确。优雅的代码不仅将算法传递给计算机而且将见解传递和信心传递给阅读代码的人。

编写透明可显的系统节省的精力,将来可能就是自己的财富。

为透明性和可显性设计

要追求代码的透明,最有效的方法很简单,就是不要在具体操作的代码上加上太多抽象层。

可以默认隐藏细节,但不要使其无法被访问。

反例,不透明的Windows注册表,注册表蠕变。

为可维护性设计

选择简单的算法,提供开发者手册。

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

做单件事并做好。

真正挑战的不是协议语法而是协议逻辑。

从性能调整中分离复杂度控制

除非万不得已(性能太差)尽量避免使用线程。

将程序划分成多个子进程可以使需要特权的代码块尽量少。

UNIX IPC方法分类

将任务转给专门程序。UNIX shellout。

管道,重定向和过滤器。

  • 管道中所有阶段程序都是并发执行的。

  • 管道的缺点或局限是单向性。

包装器。

  • 隐藏shell管线的复杂细节。

  • 安全性包装器。

从进程。

对等进程间通信。

  • 临时文件。用文件名包含$$,即PID保证文件名唯一。

  • 信号。需要信号的程序会向var/run写入pidfile。

  • 套接字。

  • 共享内存。

要避免的问题和方法

废弃的UNIX IPC方法。

远程过程调用。不够透明可显。

线程。同步互斥锁开销大且bug多。

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

更高级的语言可以使用更少的行数完成更多任务,也意味着更少的bug。

UNIX包容小型的、为专门领域特制、大量减少程序行数的语言。

//TODO 留着以后看。。。

生成:提升规格说明的层次

尽可能把设计的复杂度从代码转移到数据中去。

配置:迈出正确的第一步

什么应该是可配置的

对于能够进行可靠检测的东西,就不要提供配置开关。

提高程序适应能力,除非这样做会产生超过0.7秒延时。

增加一个配置选项,就会减少测试覆盖率。

配置在哪里

局部覆盖全局。

使用同参数选项预期寿命最匹配的机制。

命令行选项

传统UNIX风格如-ab

GNU风格如``–a –b`

接口:UNIX环境下的用户接口设计模式

最小立异原则。新颖性提高了用户与接口最初几次的交互成本。仔细掂量你的折衷。

提倡以共生和委派策略提高代码复用,降低复杂度。

UNIX接口历史。面向行的,面向字符阵列的,和基于X的。

接口设计在这些条件间权衡:简洁,表现力,易用,透明,脚本化能力。

现代接口应当既支持CLI又支持可视接口,它们各有优缺点。

UNIX程序员愿意使接口富有表现力和可配置。

接口设计模式:

  • 过滤器:catlike型,宽进严出,不需要的也不丢弃,绝不增加无用数据。

  • canstrip模式,如clear。

  • 源模式,如ls。

  • 引擎和接口分离。MVC模式。

  • 基于语言的接口。

  • 等等各种模式。

网页浏览器作为通用前端,避免了后端大量GUI冗余代码。

优化

知道何时不去优化。

最有效的优化往往是优化以外的事情,如清晰干净的设计。

最强大的优化技术是不优化。摩尔定律的指数效应。不值得为常数项优化。

先估量,后优化。明确瓶颈所在,使用性能评估工具。

小即是美,尽量将核心代码和数据结构放进缓存。

降低延时。可以批处理、重叠操作、缓存。

复杂度:尽可能简单,但别简单过了头

谈谈复杂度

程序员在意实现的复杂度,用户在意界面或接口的复杂度,第三个标准是代码总行数。

接口复杂度和实现复杂度需要折衷。

旧学派UNIX唯一的框架就是重定向、管道和shell,而Emacs将非常多的文本缓冲区和援助进程同文件系统统一在一起,大大超越shell框架。

选择需要管理的上下文环境,并且按照边界所允许的最小化方式构建程序。只有实证其他方法行不通时才编写大程序。

语言:C还是非C

C的内存管理是复杂性和错误的深渊。

真实世界中的程序往往受I/O、网络等的影响远大于CPU的影响。

shell即使一种最早的解释型语言,高级shell编程可以混合各个语言。

C语言资源效率接近机器语言,但是编程是资源管理的炼狱。

C++最佳之处是编译效率以及面向对象和泛型编程的结合,最糟之处是它太过怪异复杂,其设计者承认他不指望任何一个程序员能够完全掌握C++。

shell最佳之处在书写小型程序自然快捷,不用安装,但是不适合大型程序,并且不好移植。

Perl最佳之处是作为加强shell的强力胶合程序,但是Perl有些部分很丑陋。

Python清晰易读,易学易用,容易和C结合,但是效率相对低下。

Java移植性好,效率较高,面向对象,但是爹不好。。。

工具:开发的战术

讲解了各种UNIX开发工具,但是太过陈旧了。

重用:论不要重新发明轮子

透明性是重用的关键。

开源后换工作也可以使用原来的代码。

编写开源软件的人往往自己也使用,并且开源社区不会羞于抓bug。

许可证问题

OSD,OSI Cerified Open Source认证标志的法律定义,是最好的自由软件定义。所有标准许可证都满足他。

MIT,BSD,GPL,MPL等等。

可移植性:软件可移植性与遵循标准

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

开放源码:在UNIX新社区中编程

开发源码的规则很简单:

  • 源码公开

  • 尽早发布,经常发布

  • 给贡献以表扬

开源开发利用散布在互联网上,而且主要通过email和网络文档交流的大型程序员团队。一般有核心开发者或核心开发组来指导项目。

代码质量很难判断,开发者通常通过提交质量来评估补丁。

未来:危机与机遇

PLAN 9, 未来之路

所有挂载的文件服务器都具备相同的类文件系统接口。

PLAN 9 失败了,说明更优秀方案的最危险敌人,就是一个现存的足够优秀的架构。

UNIX设计中的问题

UNIX文件就是一大袋字节。

UNIX对GUI支持孱弱。

文件删除不可撤销。

UNIX假定文件系统是静态的。Linux改进了这一点,具有文件和目录更改通知功能。

作业控制设计拙劣。

UNIX API没有使用异常。

UNIX环境问题

UNIX文化问题

信任的理由

迄今为止,打赌UNIX玩家会输的人总是聪明一时糊涂一世。我们能赢————只要我们想赢。