unix发展史看软件开发

读Dennis Ritchie写的unix演变史,结合当前做的项目,有很多感触。

1. 文件系统

Dennis Ritche写的unix演变中,非常强调文件系统的作用。文件系统是什么?文件系统是一个可自由命名的、可持久的数据库。计算机系统最主要的信息就是数据,代码也是数据,而文件系统作为一种数据库,就是整个计算机系统的核心。文件系统组织了所有的信息,包括console、真正的文件、network socket等,提供了一种统一的定位、查询、修改、创建、删除各种信息的机制。在学习各种语言时,如果一种语言提供了很简单、高效、统一的方法看到其内部结构,则这种语言会很让人着迷。比如python的命名空间机制提供了代码的组织方式,而通过dir可以很容易遍历命名空间的各个组成部分,forth的字典是更简单的代码组织方式,通过see命令可以看到字典中的所有内容。操作系统也是一样。当然,任何系统都是一样,所以云计算平台如果也提供这种统一的方式就好了。

2. 进程

unix中,最早发展起来的,一直应用的特性是文件系统,而进程控制则是后来才发展而来的。最初一个终端对应一个进程,shell在执行其他进程时,其他进程将覆盖掉内存中的shell,其他进程调用exit结束时,exit再使用shell覆盖掉其他进程。在同一时刻,一个终端永远只对应一个进程。

后续在实现多进程时,发现很多地方需要修改。比如chdir命令,原先只有一个进程,chdir作为单独进程,执行后仍然起作用,但多进程后,chdir作为单独一个进程,执行之后对父进程没有任何效果,需要将其作为shell的内嵌命令。还有已打开文件的读写指针,原先是保存在进程中的,但多进程后,每个进程有一个,导致文件内容被破坏,从而需要将这个指针放到进程外的一个系统表中。

从多进程支持的修改可以看出,任何一个软件系统如果要长期使用,随着对其需求的不断变更,无论其最初设计和实现有多完美,都需要不断演变。因此长时间运行的软件的可读性和可修改性将变得非常重要,因此云平台使用一种更加简单的语言实现(比如python)可能非常有利。

3. shell

最初的shell是操作系统的一部分,后来才慢慢分化出来作为一个用户态应用程序。这点是不是意味着,当前云平台的管理portal,目前可以看做是云平台的一部分,后续也可以作为一种外部系统拿出去呢?

4. IO重定向

IO重定向是unix中一个非常好用,非常简单,非常重要的特性:

ls >file1

 

简单的一行命令就把当前目录的所有文件的列表放到了file1文件中。

这个特性是从multics中继承来的,multics中要实现上述功能需要做如下操作:
iocall attach user_output file file1
list
iocall attach user_output sync user_i/o

看起来非常准确,但冗长,复杂,暴露了很多内部概念给用户,无论写起来、用起来还是看起来,都远远不如unix中的好。同样的,一种新的语言,必须从使用者角度出发,考虑真正适合用户的表达方式来设计。

multics中为什么会设计出这么痛苦地语法呢?按照Ritchie的描述,主要原因在于multics项目太大了。IO系统是在BellLabs做的,而shell是在MIT做的。使用shell的人(ritchie他们)从来没有想过要改shell(因为那是“他们”的程序),而MIT设计shell中的iocall的人,可能根本就不知道iocall到底有多有用,或者到底有多愚蠢。而对于unix,IO系统和shell都完全由Thompson一个人控制,当出现一个好的想法时,也就是一个小时的修改量。

这一点让我想起我们在云平台C01版本中让各个开发人员都深恶痛绝的问题:联调太痛苦了,加班加点没日没夜去联调,用来解决模块间接口不一致的问题。在C02设计过程中,大家一个劲儿强调系统组的人员一定要定好接口,确定正确、清晰的接口,然后基线化,再也不能出现C01那么多不一致的问题了。但这样就能解决问题吗?C01的接口也不可谓不明确,函数名,参数也都定的好好的,可为什么出现那么多问题呢?接口从来都不是一份文档就能搞定的事,接口牵扯到调用这个接口会产生什么影响,后台做的操作和调用者的期望是否是一样的,多个接口不同顺序调用产生的结果是否一致等等,再详细的接口文档也无法描述清楚接口的各种影响,可以说只有实现代码才是接口的最好描述。想想unix的posixapi,经历过这么多年的使用和修订,结果不同厂商实现中仍然有微小变化,很常用的select实现都有细微差别,而这些是在使用中才会发现,文档中即使写了,谁又总看得那么仔细呢?跨unix平台的库一般都是非常难写,因为要处理各个平台的这类差异。

那如何才能解决这个问题?减小规模,加强交流。尤其是配合密切的系统,开发一定要坐在一起,经常讨论。unix的成功在于开发者和使用者就是一个团队,甚至开发者本身就是使用者,可以实时获取反馈。像C01中做界面的人在一个地方,做后台提供接口的人在另外一个地方,两部分同时开发,结果开发界面时只能打个桩测一下,打的桩永远都是返回成功,很少考虑失败的情况。C02要解决这个问题,一定要让前台界面的人和后台开发的人坐在一起进行开发。这不是一个接口文档或者几次讨论就能搞定的事。C01人员上百人,沟通是个非常大的问题,以前HAE十来个人,开发很迅速,开发的功能也很多,那时接口文档比C01还少,不过联调好像也没那么痛苦。

5. pipe

管道,是unix为操作系统和命令语言贡献的一个非常有用的概念。管道是1972年在McIlroy的建议和坚持下添加到unix的。在管道出现的多年前,McIlroy就认为命令应该作为二元操作符,其左操作数是其输入,右操作数是其输出,这样copy命令要如下写法:

file1 copy file2

 而要对一个文件排序,分页,然后线下打印,可以这么写:

file1 sort paginate offprint

 

在某天下午McIlroy在黑板前讲解了他的这个想法后,大家都认为很有启发性,但没有立刻实现,因为当时这样认为的:1.中缀写法太激进了,大家都习惯写cpfile1file2了;2.没办法区分第一个单词是命令字还是输入文件名;3.一个输入和一个输出的命令执行模型看起来太局限了,不通用,如果同时有多个输入或多个输出时怎么办?事实证明,第三个看法完全错误,大家的想象力太有限了。

从这一点也可以看出来,系统设计人员在设计时,经常自以为是的以自己的想法代替用户的想法。实际上用户的想法永远无法猜测,即使调研也不一定知道用户的真实想法:用户还没有见过你的系统,他怎么知道应该怎么用?除非他已经用过了很多类似的系统。只有实践才是发现用户真实想法的唯一途径。

管道的实现也很有意思,最初为了保持写法的统一,使用与重定向同样的方式来表示管道:cmd1 > cmd2。这样的话,原先的sort可以写成这样:
sort file1 >paginate>offprint>

 

最后一个>是必须的,否则offprint就会被当成一个文件来用了。这种写法的目的是为了用统一的重定向概念来表示管道:当重定向目标是另一个程序时就是管道。不过最后的那个>看起来有点别扭。

另外,为了保持统一,<也可以用在管道里,这样,上面的命令实际上可以有多个写法:
offprint <paginate<"sort files"<
pagenate<"sort files"< >offprint>

 

因为这些混乱,这种表示法只用了几个月就换成了当前的|表示法。

可见,有时设计上感觉很优美,但实际使用中很难用。这一点在设计一种语言时尤其需要注意,添加一个看起来很美的新功能时,一定要让用户反馈是否好用,不好用,立刻改,千万不能靠自己猜。还是那句话,只有多实践、多反馈才是发现最佳方法的唯一途径。

管道的天才部分在于,使用起来很简单,但是看到这种使用管道的可能性以及选择一种合适的表达方式需要的思想转变很大。 

6. 高级语言实现

最初的unix完全使用汇编写就。“完全使用汇编”的意思是全部使用汇编代码写成,没有任何一句宏。那时没有连接器,没有加载器,每个程序必须是独立可运行的。Thompson认为一个计算系统不能没有Fortran,于是他就坐下来写一个fortran的编译器,结果最终出来的是B语言的定义以及编译器。B语言生成解释性代码,B语言编译器和它生成的代码都很慢,但是让生活一下变美好了。

但是B语言生成解释性代码,太慢了,无法用它来写操作系统,因此Ritchie老兄开始设计C语言。1973年,unix内核使用C语言重新写了一遍。这个成功应用说明C语言是可以用来写系统软件的语言,而不是一个写简单应用的玩具。

相关推荐