第3章 内核体系结构概述
本章从较高层次上对内核进行说明。从顺序上来说,本章首先介绍内核设计目标,接下来介绍内核体系结构,最后介绍内核源程序目录结构。
3.1 内核设计目标
Linux 的内核展现出了几个相互关联的设计目标,它们依次是:清晰性、兼容性、可移植性、健壮性、安全性和速度。这些目标有时是互补的,有时则是矛盾的。但是它们被尽可能的保持在相互一致的状态,内核设计和实现的特性通常都要回归到这些问题上来。本节接下来的部分将分别讨论这些设计目标,同时还将对它们之间的取舍与平衡进行简要的说明。
3.1.1 清晰性
稍微简化点说,内核目标是在保证速度和健壮性的前提下尽量清晰。这和现在的大多数应用程序的开发有所区别,后者的目标通常是在保证清晰性和健壮性的基础上尽量提高速度。因而在内核内部,速度和清晰性经常是一对矛盾。
在某种程度上,清晰性是健壮性的必要补充:一个很容易理解的实现方法比较容易证明是正确的;或者即使不正确,也能比较容易的找出其问题所在。从而这两个目标很少会发生冲突。
但是清晰性和速度通常却是一对矛盾。经过仔细手工优化的算法通常都使用了编译器生成代码的类似技术,很少可能是最清晰的解决方案。当内核中清晰性和速度要求不一致时,通常都是以牺牲清晰性来保证速度的。即便如此,程序员仍然清楚地知道清晰性的重要性,而且他们也做了大量的工作以使用最清晰的方法保证速度。
3.1.2 兼容性
正如第1章中所述,Linux最初的编写目的是为了实现一个完整的、与Unix兼容的操作系统内核。随着开发过程的展开,它也开始以符合POSIX标准为目标。就内核而言,兼容Unix(至少是同某一现代的Unix实现相兼容)和符合 POSIX标准并没有什么区别,因此我们也不会在这个问题上详细追究。
内核提供了另外一种类型的兼容性。基于Linux 的系统能够提供可选择的对Java.class文件的本地运行支持(据说Linux是第一个提供这种支持的操作系统)。尽管实际负责Java程序解释执行的是另外一个Java虚拟机进程,该虚拟机并没有内置到内核中。但是内核提供的这种机制可以使得这种支持对用户是透明的。通过内核本身提供的程度不同的支持(这并不代表大部分工作像Java的解决方式一样能够通过外部进程实现),对其他可执行文件格式的支持也能够以同样的方式插入内核中。这方面的内容将在第7章中详细介绍。
另外需要说明的是,GNU/Linux系统作为一个整体通过DOSEMU仿真机器提供了对DOS可执行程序的支持,而且也通过WINE设计提供了对Windows可执行程序的部分支持。系统还以同样的方式通过SAMBA提供了对 Windows兼容文件和打印服务的支持。但是这些都不是同内核密切相关的问题,因此在本书中我们不再对它们进行讨论。
兼容性的另外一个方面是兼容异种文件系统,本章中稍后会有更为详细的介绍,但是大部分内容已经超出了本书的范围。Linux能够支持很多文件系统,例如ext2(“本地”文件系统)、ISO-9660(CD-ROM使用的文件系统)、 MS-DOS、网络文件系统(NFS)等许多其他文件系统。如果你有使用其他操作系统格式的磁盘或者一个网络磁盘服务器,那么Linux将能够和这些不同的文件系统进行交互。
兼容性的另外一个问题是网络,这在当今Internet流行的时代尤为重要。作为 Unix的一个变种,Linux自然从很早就开始提供对TCP/IP的支持。内核还支持其他许多网络协议,它们包括AppleTalk协议的代码,这使得 Linux单元可以和Macintosh机自由通讯;Novell的网络协议,也就是网络报文交换(IPX),分组报文交换(SPX)和NetWare核心协议(NCP);IP协议的新版本IPv6以及其它一些不太出名的协议。
兼容性考虑的最后一个方面是硬件兼容性。似乎每个不常见的显卡、市场份额小的网卡、非标准的CD-ROM接口和专用磁带设备都有Linux的驱动程序(只要它不是专为特定操作系统设计的专用硬件)。而且只要越来越多的厂商也逐渐认识到Linux的优势,并为更容易地实现向Linux上移植而开放相应的源程序代码,Linux对硬件支持会越来越好。
这些兼容性必须通过一个重要的子目标:模块度(modularity)来实现。在可能的情况下,内核只定义子系统的抽象接口,这种抽象接口可以通过任何方法来实现。例如,内核对于新文件系统的支持将简化为对虚拟文件系统(VFS)接口的代码实现。第7章中介绍的是另外一个例子,内核对二进制句柄的抽象支持是实现对诸如Java之类的新可执行格式的支持的方法。增加新的可执行格式的支持将转变为对相应的二进制句柄接口的实现。
3.1.3 可移植性
与硬件兼容性相关的设计目标是可移植性,也就是在不同硬件平台上运行Linux的能力。系统最初是为运行在标准IBM兼容机上的Intel x86 CPU而设计的,当时根本没有考虑到可移植性的问题。但是从那以后情况已经发生了很大的变化。现在正式的内核移植包括向基于Alpha、ARM、 Motorola 68x0、MIPS、PowerPC、SPARC及SPARC-64 CPU系统的移植。因而,Linux可以在Amigas、旧版或新版的Macintosh、Sun和SGI工作站及NeXT机等机器上运行。而且这些还只是标准内核发行版本的移植范围。从老的DEC VAX到3Com掌上系列个人数字助理(例如Palm III)的非正式的移植工作也在不断进行中。成功的非正式移植版本后来通常都会变成正式的移植版本,因此这些非正式的移植版本很多最终都会出现在主开发树中。
广泛平台支持之所以能够成功的部分原因在于内核把源程序代码清晰地划分为体系结构无关部分和体系结构相关部分。在本章的后续部分将对这个问题进行更深入的讨论。
3.1.4 健壮性和安全性
Linux必须健壮、稳定。系统自身应该没有任何缺陷,它还应该可以保护进程(用户)以防止互相干扰,这就像把整个系统从其他系统中隔离开来加以保护一样。后一种考虑很大程度上是受信任的用户空间应用程序领域的问题,但是内核至少也应该提供支撑安全体系的原语(primitive)。健壮性和安全性比任何别的目标都要重要,包括速度。(系统崩溃的速度很快又有什么好处呢?)
保证Linux健壮性和安全性的唯一一个最重要的因素是其开放的开发过程,它可以被看作是一种广泛而严格的检查。内核中的每一行代码、每一个改变都会很快由世界上数不清的程序员检验。还有一些程序员专门负责寻找和报告潜在的缺陷—他们这样做完全是出于自己的个人爱好,因为他们也希望自己的Linux系统能够健壮安全。以前检查中所没有发现的缺陷可以通过这类人的努力来定位、修复,而这种修复又合并进主开发树以使所有的人都能够受益。安全警告和缺陷报告通常在几天甚至几个小时内就能够得到处理和修复。
Linux可能并不一定是现有的最安全的操作系统(很多人认为这项桂冠应该属于OpenBSD,它是一个以安全性为主要目标的Unix变种),但是它是一个有力的竞争者。而且Linux健壮性远没有发展到尽头。
3.1.5 速度
这个术语自己就可以说明问题。速度几乎是最重要的衡量标准,虽然其等级比健壮性、安全性和(在有些时候)兼容性的等级要低。然而它却是代码最直观的几个方面之一。Linux内核代码经过了彻底的优化,而最经常使用的部分—例如调度,则是优化工作的重点。几乎在任何时候都有一些不可思议的代码,这是由于这种方式的执行速度比较快(这并不总是很明显,但是你经常不得不通过自己的试验来对这种优化代码进行确认)。虽然有时一些更直接的实现方法速度也很快,但是我所见过的这种情况很少。
在某些情况下,本书推荐用可读性更好的代码来替代那些打着速度的名义而被故意扭曲了的代码。虽然速度是一个设计目标,但我基本上只在以下两种情况时才会这样做:a) 在所考虑的问题中,速度明显不是关键问题 b) 没有其他的办法。
3.2 内核体系结构初识
图3-1是一种类Unix操作系统的相当标准的视图,实际上,更细致地说,该图能够说明所有期望具有平台无关特性的操作系统。它着重强调了内核的两个特性:
* 内核将应用程序和硬件分离开来。
* 部分内核是体系结构和硬件特有的,而部分内核则是可移植的。
第一点我们在前面章节中已经讨论清楚了,在这里没有必要重复说明。第二点,也就是与体系结构无关和与体系结构相关代码的内容对于我们的讨论比较有意义。内核通过使用与处理用户应用程序相同的技巧来实现部分可移植性。这也就是说,如同内核把用户应用程序和硬件分离一样,部分内核将会因为与硬件的联系而同其他内核分离开来。通过这种分离,用户应用程序和部分内核都成为可移植的。
虽然这通常并不能够使得内核本身更清楚,但是源程序代码的体系结构无关部分通常定义了与低层,也就是体系结构相关部分(或假定)的接口。作为一个简单的例子,内存管理代码中的体系结构无关部分假定只要包含特定的头文件就可以获得合适的 PAGE_SIZE 宏(参见10791行)的定义,该宏定义了系统的内存管理硬件用于分割系统地址空间的内存块的大小(参见第8章)。体系结构无关代码并不关心宏的确切定义,而把这些问题都留给体系结构相关代码去处理(顺便一提,这比到处使用#ifdef/#endif程序块来定义平台相关代码要清晰易懂得多)。
这样,内核向新的体系结构的移植就转变成为确认这些特性及在新内核上实现它们的问题。
另外,用户应用程序的可移植性还可以通过它和内核的中间层次—标准C库(libc) —的协助来实现。应用程序实际上从不和内核直接通讯,而只通过libc来实现。图3-1中显示应用程序和内核直接通讯的唯一原因在于它们能够和内核通讯。虽然在实际上应用程序并不同内核直接通讯—这样做是毫无意义的。通过直接和内核通讯所能处理的问题都可以通过使用libc实现,而且更容易。
libc和内核通讯的方式是体系结构相关的(这和图中有一点矛盾),libc负责将用户代码从实现细节中解放出来。有趣的是,甚至大部分libc都不了解这些细节。大部分的libc,例如atoi和rand的实现,都根本不需要和内核进行通讯。剩余的大部分libc,例如printf函数,在涉及到内核之前或之后就已经处理了大量的工作(printf必需首先解释格式化字符串,分析相应参数,设定打印方法,在临时内部缓冲器中记录预期输出。直到此时它才调用底层系统,调用write来实际打印该缓冲区)。其他部分的libc 则只是相应系统调用的简单代理。因而一旦发生函数调用时,它们会立即调用内核相应函数以完成主要工作。在最低层次上,大部分libc通过单通道同内核进行交流,而它们所使用的机制将在第5章进行详细介绍。
由于这种设计,所有的用户应用程序,甚至大部分的C库,都是通过体系结构无关的方式和内核通讯的。
3.3 内核体系结构的深入了解
图3-2显示了内核概念化的一种可能方式。该图和区分内核的体系结构无关及体系结构相关的方法有所不同,它是一种更具有普遍性的结构视图。在“Kernel”框内的本书中有所涉及的内核部分都用括号注明了相应的章节编号。虽然有关对称多处理(SMP)的支持也属于本书的范围,但是在这里我们却没有标明章号。部分原因在于相当多的SMP代码广泛地分布于整个内核中,因此很难将它与某一个模块联系起来。同样,对于内核初始化的支持也属于本书的范围,但是也没有标明章号。这样做是因为从设计的观点上看,该问题并不重要。最后,虽然在图中我们将第6章和“进程间通讯”框联系在一起,但是该章只涉及一部分进程间通讯的内容。
进程和内核的交互通常需要通过如下步骤:
1) 用户应用程序调用系统调用,通常是使用libc。
2) 该调用被内核的system_call函数截获(第5章,171行),此后该函数会将调用请求转发给另外的执行请求的内核函数。
3) 该函数随即和相关内部代码模块建立通讯,而这些模块还可能需要和其他的代码模块或者底层硬件通讯。
4) 结果按照同样的路径依次返回。
然而,并不是所有内核和进程间的交互都是由进程发起的。内核有时也会自行决定同哪个进程交互,例如通过释放信号或者简单的采用直接杀死进程的方法终止该进程的执行(如当进程用完所有可用的CPU时间片),以便使其他进程有机会运行。这些交互过程在该图中并没有表示,主要是因为它们通常都只是内核对自己的内部数据结构的修改(信号传递对于这种规则来说是一个例外)。
