若说到在Linux下的编程,稍微对编程有所了解的人都会想到C语言。Linux的内核、shell、基础命令程序,也的确是用C语言编写的。这首先证明了一点:C语言很强很通用。到目前为止,C语言依然垄断着计算机工业中几乎所有的系统编程。而且也正因为是C语言,才使得Unix,以及后来的Linux能够这么广泛地被人们去研究、去改进、去制作自己的分支,以至于我们能在各种硬件平台上使用它们。 但是细心的人会发现,Linux启动过程中所涉及的各种程序,很少有C语言的痕迹。它们大多是“文本”,能够被计算机执行的“文本”。不单单在启动过程中是这样,那些用于安装软件的工具yum、apt-get,甚至是configure和Makefile也都是文本。而且你可能还没注意到,那些用于系统管理的工具,如配置ADSL拨号上网的工具、配置守护进程的工具等,很多也都是“文本”的。 估计你现在一定会骂我在混淆视听,因为那些根本就不是什么“文本”,那是程序,是“脚本程序”。是的,那些就是“脚本程序”。我的确有混淆视听的嫌疑,不过我只是想强调一下“脚本程序”是以文本的形式存在的这个特点。可以很容易地让人看清它们做了什么,也很容易修改。 四处用脚本是所有类Unix系统不同于其他系统的一个显著特征。 4.3.1 富饶的脚本语言 催生人们在Linux中大量使用“脚本”来编写程序,并不仅仅是因为“脚本”对人直观、容易修改这种显著特性所决定的。另外一个主要的原因就是Linux所支持的脚本语言种类十分丰富。 所有类Unix系统所必备的shell,其本身就是一个强大的脚本解释器。所以从shell诞生的那一天起,shell就是那些不懂C语言,又必须在Unix上编写程序的用户们的首选工具。 这就给了人们一种新的选择。使用shell编程不用去理会让人头晕的“指针”;shell程序可以直接利用系统命令来完成一些需要用大量C代码的功能;shell编程不用去理会数据类型,不用考虑烦人的数值和字符数据的转换问题;shell程序同样提供顺序、选择分支和循环这三种能够构建任意算法的基础设施。因此,很快就能够被非专业用户所接受、掌握,并编写出非常实用的程序。 随着时间的推移,这些非专业用户想往更高的方向发展,遇到了一些shell处理起来会很蹩脚的问题,比如分析文本和修改文本(别忘了“万般皆文本”)。这个时候他们会发现有awk和sed。也只需要写几行脚本就能将这些问题处理得很好。而且它们也跟shell配合得天衣无缝。或许这个时候会觉得加入了awk和sed的shell脚本有些难看。没关系,还有Perl和TCL。Perl天生就是为处理文本而存在的,TCL也不含糊。 如果觉得这些语言都太老气了,有些过时了。不要紧,还有Python、Ruby等这些现代脚本语言。它们除了不能写操作系统内核之外,几乎什么都能干,而且还是面向对象的。 不管怎样,在Linux下能够选择的脚本语言都是极其丰富的。它们最大的特点就是简单、好学且资料丰富。简单就意味着容易维护,好学就容易吸引用户,资料丰富就不会在解决bug上出现障碍。即便是专业的程序员,也会因为这些特点而特别偏好脚本语言。导致的一个结果就是脚本程序在Linux中的大爆发。 4.3.2 为什么不是C C语言是Unix的母语,这是毋庸置疑的。前面也说过,正是因为有了C语言,才使得Unix有了今天的成就。但为什么在Linux中有这么多程序,甚至是关键程序,不是用C语言编写的呢? 脚本程序由于是解释执行的,在执行效率上自然是会有很大损失的。并且大家都知道,C语言所编写的程序又是以效率著称的。但是C语言是一种编译型语言。要想让C语言的程序能够运行,必须经过编译和链接这两个步骤。能够将由几十个源代码文件构成的C语言程序,有条不紊地编译完成并能最终链接成一个可执行程序,本身就是一件费时又费力的事情。如果一旦程序有问题,还必须使用专门的调试工具一点点地去跟踪判断,修正之后再重复那些复杂的编译和链接步骤,这又是一个极需技巧的事情。积累并掌握技巧又是一件费时又费力的事情。在早些年,计算机性能不佳的时候,这些付出或许是值得的。但是放到现在,处理器的速度至少快了几千倍,内存大了几千倍,硬盘甚至大了几万倍,而价格却更低了。从经济角度分析,机器的时间成本早已远远低于人的时间成本了。那么C语言在机器效率上的优势根本没有任何意义。脚本程序能够给人节省下来的时间成本,则更具经济效益。其实要论机器效率,汇编语言比C语言要好上几十倍,但是目前还有谁在用汇编语言编程呢? C语言在设计的时候,最主要的一个目标就是能够让程序员自己处理内存管理的问题。这使得C语言很强大但又太过于灵活,导致了很多陷阱的出现。稍微一不注意,程序中就会存在难以发觉的Bug,甚至是严重的安全漏洞。程序员们大多是要以时间或失败为代价去积累经验,才能尽量避免这些问题的发生。而且效率在大多数应用中根本就不是问题,首要的是正确。脚本程序的简单和直观正是正确的起点,C语言的灵活却是错误的根源。 但是C语言并不是一无是处,也是Unix的精华。C语言作为通用程序设计语言是所向无敌的。C语言本身也非常简洁和紧凑,资料丰富且容易学习。C语言之后的少数语言设计,为了不被C语言所吞并,不得不进行大的改动,比如引进垃圾回收机制等,以和C语言能够在功能上保持足够距离。也正是因为这样,C语言始终没有消失,只是它的光辉在Linux中稍稍地被脚本程序所遮挡了一下。 4.3.3 脚本的不足和混合编程 虽然效率并不是脚本程序的缺点,但是种类过于丰富却是一个极大不足。编写一个复杂的应用,往往很难使用一种脚本语言包杆到底。因为脚本语言都有自己适用的场景。为了能够快速有效地完成某个应用,就需扬长避短,利用多种脚本语言混合编程。 多脚本语言的混合编程是一种知识密集型的编程方法,但不是编码密集型的(这是能够被普遍接受的原因)。为了能够良好地使用这种方法,就要求程序员不仅仅要具备相当数量的多种语言知识,还必须具备能够判断这些语言的适用场景、以及如何将它们有效地组合在一起的经验。 实际上混合编程并不是脚本语言的专利,任何编程语言都行,只要你能找准那些语言的特点。比如我就曾经使用过Basic和C进行混合编程,去完成一个DOS版万年历程序。为了支持鼠标点击操作,用C完成了鼠标中断的处理。余下的部分都用Basic来完成。 在Linux中大量应用脚本程序的场景,好多都是这种混合编程的典范。比如Linux的启动过程。主程序init是用C语言写的,具体到启动流程的各个环节则是shell脚本程序。这也就引出了脚本程序的一个主要用途…… 4.3.4 强力胶水 一般来说,程序的设计是有两个方向可以选择的。一个是自底向上的设计,一个是自顶向下的设计。 自底向上的设计,就是从程序要完成的功能出发,从确定要进行的具体操作开始,向上进行。比如,设计一个网页浏览器,一些基本操作原型需要包括HTML解释器,JavaScript解释器、网络协议处理等。然后将这些功能串起来,构成一个浏览器。 自顶向下的设计,就是从程序的规格说明或应用逻辑开始,向下进行。比如,同样的网页浏览器,这个时候要先确定功能:支持什么类型的URL(http:、ftp:还是file:),支持何种规范的HTML,是否提供JavaScript的支持等。然后一一去实现这些功能。 具体采用何种设计方法,是一个非常重要的问题。因为对应层次的功能实现很可能会受到最初选择的限制。尤其是采用自顶向下设计时,应用逻辑所需要的操作可能根本无法实现。然而,如果采用自底向上的设计,很可能做了很多与应用逻辑无关的操作——或者,你想造房子,最后你造出了一堆砖头。 在目前的大多数实践中,很多人采用的是被认为“正确的自顶向下”方法。如果能够精确的定义程序的需求,且在实现中不会有需求上的变化,而且保证能够实现,那么这的确是一个好方法。但别忘了我说的是如果,事实的情况是没有如果。因为经常遇到的情况是需求很难精确定义,从而导致需求还没有来得及实现就发生了变化。而且很多需求都只是用户的臆想,受制于其他需求而根本无法实现。 为了应对这种两难的问题,程序员的选择就是双管齐下——一边以自顶向下的方式规划具体程序的应用逻辑,一边采用自底向上的方法去封装常用操作原型而形成程序库。这样,即便当需求发生变化时,程序库仍然可以重用。所以大多数情况下,我们经常使用的程序就是这两种方法所结合的产物,这就导致了“胶合层”的出现。因为自顶向下和自底向上是两种容易产生冲突的方法。顶层的应用逻辑和底层的常用操作原型必须用胶合逻辑进行适配。 其实胶合层是一个非常讨厌的东西,必须尽可能的“薄”。这一点非常重要。胶合层用来将东西粘在一起,但不应该用来隐藏各层的裂痕和不平整。脚本语言因其本身的简单和功能限制,在胶合层的实现时,充当了“强力胶水”的角色。 在网页浏览器的例子中,最为著名的应属Mozilla的设计。图4.1展示了Mozilla的基本架构。 图4.1 Mozilla基本架构 Mozilla作为网页浏览器,复杂程度是不亚于操作系统的,它由数百万行的C++代码构成。这样复杂的软件能够被很好的维护起来,最根本的原因就是架构的优良所带来的结果。主要体现在:1.分离界面和实现;2.针对接口编程;3.分层设计;4.容易扩展。能够将这4个方面真切地付诸于实际,胶合层功不可没。从一个包含HTML标签、CSS、JavaScript等内容的html文件到将这些内容显示为用户所见的网页效果,这个过程被称之为页面渲染。页面渲染代码因是作为浏览器最容易导致bug丛生的地方而臭名昭著。这种架构也使得页面渲染代码能够藉由用于胶合层的脚本语言来实现。脚本语言简单、清晰易读的特性,使得bug缺少了一个滋生的温床。 4.3.5 极端的例子 有一个使用胶合层更为极端的例子就是Emacs。 或许到目前,大家已经能够略有所察觉,在Unix的文化里,是极其强调简约主义的。换句话说就是崇尚少吃饭多干活。然而Emacs却恰恰与这相反。它异常庞大,维护起来着实不易;大量使用Lisp脚本来胶合少得可怜的C程序,非常消耗系统资源。但Emacs能做的事儿也是异常丰富的。只有你想不到,没有它做不到。 在这个地方,忠于Unix文化的人们可以用两种方式来看待Emacs的这种设计风格。一种是完全否定它,另一种就是发展出一种考虑到复杂度的非教条的方法。我个人更推崇后者。因为抱怨Emacs庞大,和把Linux系统中的全部shell脚本程序算在一起而抱怨shell庞大,是一样地不公平。 Emacs完全可以视为一个围绕在小巧锐利的工具集合上的虚拟机或框架,只是这个工具集恰好是Lisp编写的罢了。从这个角度出发,shell和Emacs的主要区别就在于Linux发行版并没有把所有shell脚本程序连同shell一起发布。因为Emacs内置了感觉臃肿的通用语言而反对Emacs,就像因为shell具有条件和for循环而拒绝使用shell脚本一样无聊和可笑。毕竟使用shell跟会不会写shell脚本程序没有半毛钱关系,使用Emacs也没人强迫你必须会Lisp。如果一定要对Emacs鸡蛋里挑骨头,那只能说由于历史过久,导致其内置工具积累过多而产生了一些问题,但是这个跟用户有什么关系呢?讨厌就不用,Emacs也不会罢工。 基于这种观点,GNOME和KDE的开发者们从来不会因自己所维护的系统过于庞大而惶惶不可终日。而且还应该籍此而总结出另外一个直观重要的概念,那就是…… 4.3.6 软件的适度规模 崇尚简约是无可厚非的优良传统,但小巧未必就能万事大吉。小巧锐利的工具很难跨越的一个鸿沟就是数据共享问题,除非它们能生存在彼此之间通信便利的框架之中。非常幸运的是,Linux系统本身就是这样一个框架,GNOME是,KDE是,Emacs更是。只是为了获取这种能够对共享上下文环境进行统一管理的框架,是要以高复杂度作为代价来换取的。能够对共享上下文环境进行统一管理的最直接的益处就是,用户不再需要担负底层部件的命名和资源管理问题。 Linux中所提供的这种框架就是前面说到的管道、IO重定向以及shell。整合工作全部由shell脚本来完成。但是对于某些大型应用软件,由于Linux自身的这种框架在功能上、效率上和可移植性上并不能满足它们的需求,因此它们不得不寻求自己的解决之道。比如:Emacs很重要的一个任务是要将非常多的文本缓冲区及其辅助进程与文件系统统一在一起,这大大超出了shell的能力。像Gnome和KDE这种桌面环境,甚至都将shell本身并入了自身,而且它们最核心的GUI通信机制,也不是简单的管道或IO重定向能够满足的。 一味地追求简约、小巧锐利,那是教条主义。从这些活生生的例子所展示出的始终能够吸引着无数人对其追捧中可以看到:简约不是太简单。 但是这种共享上下文的统一管理究竟能给你带来多大益处,也是需要三思而后行的。由于需求的贪婪,赋予程序太多的设想和任务而最终失败的例子在我们日常工作中比比皆是。如果你很幸运,你的产品需要运行在Linux上。那么解决这个问题的最好办法就是尝试使用Linux自身的框架,只有实证了它不行才去考虑编写庞大的程序。绝大多数的结果就是你的程序运行良好。Linux中那些无处不在的脚本不就是最好的证据吗?