解决复杂性的所有方法都基于一个基本原理:问题分解和各个击破。也就是说,都是把大
型的、难以解决的问题(或系统)分解成一定数量的复杂度较低的子问题(或子系统),
再根据需要重复这一过程直到每一部分都小到可以解决为止,而各种方法只是这种原理的
一些不同运用而已。
计算机科学中有三种经典的方法比较适合于构建大型系统(我首先必须说明的是,这些定
义都是经过我深思熟虑的讨论对象)。
1) 层次(Layer)—将解决方案分解成若干部分,在这些部分中存在一个问题域的最底层
,它为上层的抽象层次较高的工作提供基础。较高层建立在其低层基础之上。OSI和
TCP/IP协议堆栈是众所周知的层次化软件设计的成功的例子。操作系统设计的层次化解决
方案可能会包含一个可以直接和硬件通讯的层次,然后在其上提供为更高层提供抽象支持
的层次。这样更高层就可以对磁盘、网卡等硬件进行访问,而并不需要了解这些设备的具
体细节。
层次化设计的一个特征是要逐步构建符号集(vocabulary)。随着层次的升高,符号集的
功能将越来越强大。层次化设计的另外一个特征是完全可以在对其上下层透明的条件下替
换某一层次。在最理想的情况下,移植层次化的操作系统只需要重写最底层的代码。纯层
次化模型实现的执行速度可能会很慢,因为高层必须(间接的)通过调用一系列连续的低
层才能处理完自己的任务—N层调用N-1层,N-1层调用N-2层,等等,直到实际的工作在0
层被处理完成。接着,结果当然是通过同样的路径反向传递回来。因此,层次化设计通常
会包含对某些高层直接和某些低层通讯的支持;这样虽然提高了速度,但是却使得各个层
次的替换工作更加困难(因为不止一个高层会直接依赖于这个你所希望进行替换的层次)
。
* 模块(Module)—模块将具体的一部分功能块隐藏在抽象的接口背后。模块的最大特点
是将接口和其实现分离开来,这样就能够保证一个模块可以在不影响其他模块的情况下进
行改变。这样也将模块之间的依赖关系仅仅限定于接口。模块的范围是试图反映求解域内
一些方面的自然的概念性界限。纯模块化的操作系统因而就可能有一个磁盘子系统模块,
一个内存管理子系统模块,等等。纯模块化和纯层次化的操作系统之间的主要区别是,一
个可以由其他模块自由调用,模块间没有上层和下层的概念(从这个意义上来说,模块是
广义的层次。按照纯粹的观点,层次是最多可供一个其它模块调用的模块,这个模块也就
是它的直接上层模块)。
* 对象(Object)—对象和模块不同,因为对于初学者来说它们具有不同的问题考虑方式
,实现的方法也可能各自独立。但是,就我们当前的目的来说,对象不过是结构化使用模
块的方法。组件(component)作为对象思想的进一步改进,目前还没有在操作系统设计
中广泛使用。即便如此(按照我们的观点),我们也没有足够的理由将其和模块划分在不
同的范畴中。
图3-1强调了内核的层次化的视图,而且是体系结构无关层次位于体系结构相关层次之上
(更为精确的视图是在顶层增加一个附加的体系结构相关的层次。这是因为系统调用接口
位于应用程序和内核之间,而且是体系结构相关的)。图3-2着重强调了更加模块化的内
核视图。
从合理的表述层次上看,这两种观点都是正确的。但也可以说这两种观点都是错误的。我
可以用大量的图片向你证明内核是遵从所有你所能够指出的设计原则集合的,因为它就是
从众多思想中抽取出来的。简单说来,事实是Linux内核既不是严格层次化的,也不是严
格模块化的,也不是严格意义上的任何类型,而是以实用为主要依据的(实际上,如果要
用一个词来概括Linux从设计到实现的所有特点,那么实用就是最确切的)。也许最保守
的观点是内核的实现是模块化的,虽然这些模块有时会为了追求速度而有意跨越模块的界
限。
这样,Linux的设计同时兼顾了理论和实际。Linux并没有忽视设计方法;相反,在Linux
的开发基本思想中,设计方法的作用就像是编译器:它是完成工作的有力工具。选择一个
基本的设计原则(例如对象)并完全使用这种原则,不允许有任何例外,这对于测试该原
则的限制,或者构建以说明这些方法为目的的教学系统来说都是一个不错的方法。但是如
果要用它来达到Linux的设计目标则会引起许多问题。而且Linux的设计目标中也并不包括
要使内核成为一个完全纯化的系统。Linux开发者为了达到设计目标宁愿违背妨碍目标实
现的原则。
实际上,如果对于Linux来说是正确的,那么它们对于所有最成功的设计来说都是正确的
。最成功、应用最广泛的实际系统必然是实用的系统。有些开发人员试图寻找功能强大的
可以解决所有问题的特殊方法。他们一旦找到了这种方法,所有的问题就都迎刃而解了。
像Linux内核一样的成功设计通常需要为系统的不同部分和描述上的不同层次使用不同的
方法。这样做的结果可能不是很清晰,也不是很纯粹,但是这种混合产物比同等功能的纯
粹系统要强大而且优秀得多。
Linux大部分都是单内核的
操作系统内核可能是微内核,也可能是单内核(后者有时称之为宏内核Macrokernel)。
按照类似封装的形式,这些术语定义如下:
* 微内核(microkernel)—在微内核中,大部分内核都作为独立的进程在特权状态下运
行,它们通过消息传递进行通讯。在典型情况下,每个概念模块都有一个进程。因此,如
果在设计中有一个系统调用模块,那么就必然有一个相应的进程来接收系统调用,并和能
够执行系统调用的其他进程(或模块)通讯以完成所需任务。
在这些设计中,微内核部分经常只不过是一个消息转发站:当系统调用模块要给文件系统
模块发送消息时,消息直接通过内核转发。这种方式有助于实现模块间的隔离(某些时候
,模块也可以直接给其他模块传递消息)。在一些微内核的设计中,更多的功能,如I/O
等,也都被封装在内核中了。但是最根本的思想还是要保持微内核尽量小,这样只需要把
微内核本身进行移植就可以完成将整个内核移植到新的平台上。其他模块都只依赖于微内
核或其他模块,并不直接直接依赖硬件。
微内核设计的一个优点是在不影响系统其他部分的情况下,用更高效的实现代替现有文件
系统模块将会更加容易。我们甚至可以在系统运行时将开发出的新系统模块或者需要替换
现有模块的模块直接而迅速地加入系统。另外一个优点是不需要的模块将不会被加载到内
存中,因此,微内核就可以更有效地利用内存。
* 单内核(monolithic kernel)—单内核是一个很大的进程。它的内部又可以被分为若
干模块(或者是层次或其他)。但是在运行的时候,它是一个独立的二进制大映象。其模
块间的通讯是通过直接调用其他模块中的函数实现的,而不是消息传递。
单内核的支持者声称微内核的消息传递开销引起了效率的损失。微内核的支持者则认为因
此而增加的内核设计的灵活性和可维护性可以弥补任何损失。
我并不想讨论这些问题,但必须说明非常有趣的一点是,这种争论经常会令人想到前几年
CPU领域中RISC和CISC的斗争。现代成功的CPU设计中包含了所有这两种技术,就像Linux
内核是微内核和单内核的混合产物一样。Linux内核基本上是单一的,但是它并不是一个
纯粹的集成内核。前面一章所介绍的内核模块系统将微内核的许多优点引入到Linux的单
内核设计中。(顺便提一下,我考虑过一种有趣的情况,就是Linux的内核模块系统可以
将系统内核转化成为简单的不传递消息的微内核设计。虽然我并不赞成,但是它仍然是一
个有趣的想法。)
为什么Linux必然是单内核的呢?一个方面是历史的原因:在Linus的观点看来,通过把内
核以单一的方式进行组织并在最初始的空间中运行是相当容易的事情。这种决策避免了有
关消息传递体系结构、计算模块装载方式等相关工作。(内核模块系统在随后的几年中又
进行了不断地改进。)
另外一个原因是充足的开发时间的结果。Linux既没有开发时间的限制,也没有来自于市
场压力的发行进度。 所有的限制只有并不过分的对内核的修改与扩充。内核的单一设计
在内部实现了充分的模块化,在这种条件下的修改或增加都并不怎么困难。而且问题还在
于没有必要为了追求尚未证实的可维护性的微小增长而重写Linux的内核(Linus曾多次特
别强调了如下的观点:为了这点利益而损耗速度是不值得的)。后面章节中将详细讨论充
足开发时间的效果。
如果Linux是纯微内核设计,那么向其他体系结构上的移植将会比较容易。实际上,有一
些微内核,如Mach微内核,就已经成功地证明了这种可移植性的优点。实际的情况是,
Linux内核的移植虽然不是很简单,但也绝不是不可能的:大约的数字是,向一个全新的
体系结构上的典型的移植工作需要30 000到60 000行代码,再加上不到20 000行的驱动程
序代码(并不是所有的移植都需要新的驱动程序代码)。粗略计算一下,一个典型的移植
大约平均需要50 000行代码。这对于一个程序员或者最多一个程序小组来说是力所能及的
,可以在一年之内完成。虽然这比微内核的移植需要更多的代码,但是Linux的支持者将
会提出,这样的Linux内核移