<H1 12pt 0cm; TEXT-INDENT: 0cm"><FONT face="Times New Roman">第1章 Linux</FONT>简介</H1>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">让用户很详细地了解现有操作系统的实际工作方式是不可能的,因为大多数操作系统的源代码都是严格保密的。其例外是一些研究用的系统,另外一些是明确为操作系统教学而设计的系统。(还有一些系统则是同时出于这两种目的。)尽管研究和教学这两个目的都很好,但是这类系统很少能够通过对正式操作系统的小部分实现来体现操作系统的实际功能。对于操作系统的一些特殊问题,这种折衷系统所能够表现的就更是少得可怜了。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">在以实际使用为目标的操作系统中,让任何人都可以自由获取系统源代码,无论目的是要了解、学习还是改进,这样的现实系统并不多。本书的主题就是这些少数操作系统中的一个:<FONT face="Times New Roman">Linux</FONT>。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">Linux</FONT>的工作方式类似于<FONT face="Times New Roman">Uinx</FONT>,是免费的,源代码也是开放的,符合标准规范的<FONT face="Times New Roman">32</FONT>位(在<FONT face="Times New Roman">64</FONT>位<FONT face="Times New Roman">CPU</FONT>上是<FONT face="Times New Roman">64</FONT>位)操作系统。<FONT face="Times New Roman">Linux</FONT>拥有现代操作系统的所具有的内容,例如:</P>< 0cm 0cm 0pt 42.5pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.5pt">l 真正的抢先式多任务处理,支持多用户</P>< 0cm 0cm 0pt 42.5pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.5pt">l 内存保护</P>< 0cm 0cm 0pt 42.5pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.5pt">l 虚拟内存</P>< 0cm 0cm 0pt 42.5pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.5pt">l 支持对称多处理机<FONT face="Times New Roman">SMP</FONT>(<FONT face="Times New Roman">symmetric multiprocessing</FONT>),即多个<FONT face="Times New Roman">CPU</FONT>机器,以及通常的单<FONT face="Times New Roman">CPU</FONT>(<FONT face="Times New Roman">UP</FONT>)机器</P>< 0cm 0cm 0pt 42.5pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.5pt">l 符合<FONT face="Times New Roman">OSIX</FONT>标准</P>< 0cm 0cm 0pt 42.5pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.5pt">l 联网</P>< 0cm 0cm 0pt 42.5pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.5pt">l 图形用户接口和桌面环境(实际上桌面环境并不只一个)</P>< 0cm 0cm 0pt 42.5pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.5pt">l 速度和稳定性</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">严格说来,<FONT face="Times New Roman">Linux</FONT>并不是一个完整的操作系统。当我们在安装通常所说的<FONT face="Times New Roman">Linux</FONT>时,我们实际安装的是很多工具的集合。这些工具协同工作以组成一个功能强大的实用系统。<FONT face="Times New Roman">Linux</FONT>本身只是这个操作系统的内核,是操作系统的心脏、灵魂、指挥中心。(整个系统应该称为<FONT face="Times New Roman">GNU/Linux</FONT>,其原因在本章的后续内容中将会给以介绍。)内核以独占的方式执行最底层任务,保证系统正常运行——协调多个并发进程,管理进程使用的内存,使它们相互之间不产生冲突,满足进程访问磁盘的请求等等。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">在本书中,我们给大家揭示的就是<FONT face="Times New Roman">Linux</FONT>是如何完成这一具有挑战性的工作的。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">Linux(和Unix)的简明历史</H2>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">为了让大家对本书所讨论的内容有更清楚的了解,让我们先来简要回顾一下<FONT face="Times New Roman">Linux</FONT>的历史。由于<FONT face="Times New Roman">Linux</FONT>是在<FONT face="Times New Roman">Unix</FONT>的基础上发展而来的,我们的话题就从<FONT face="Times New Roman">Unix</FONT>开始。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">Unix</FONT>是由<FONT face="Times New Roman">AT&T</FONT>贝尔实验室的<FONT face="Times New Roman">Ken Thompson</FONT>和<FONT face="Times New Roman">Dennis Ritchie</FONT>于<FONT face="Times New Roman">1969</FONT>年在一台已经废弃了的<FONT face="Times New Roman">DP-7</FONT>上开发的;它最初是一个用汇编语言写成的单用户操作系统。不久,<FONT face="Times New Roman">Thompson</FONT>和<FONT face="Times New Roman">Ritchie</FONT>成功地说服管理部门为他们购买更新的机器,以便该开发小组可以实现一个文本处理系统,<FONT face="Times New Roman">Unix</FONT>就在<FONT face="Times New Roman">DP-11</FONT>上用<FONT face="Times New Roman">C</FONT>语言重新编写(发明<FONT face="Times New Roman">C</FONT>语言的部分目的就在于此)。它果真变成了一个文本处理系统——不久之后。只不过问题是他们先实现了一个操作系统而已…</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">最终,他们实现了该文本处理工具,而且<FONT face="Times New Roman">Unix</FONT>(以及<FONT face="Times New Roman">Unix</FONT>上运行的工具)也在<FONT face="Times New Roman">AT&T</FONT>得到广泛应用。在<FONT face="Times New Roman">1973</FONT>年,<FONT face="Times New Roman">Thompson</FONT>和<FONT face="Times New Roman">Ritchie</FONT>在一个操作系统会议上就这个系统发表了一篇论文,该论文引起了学术界对<FONT face="Times New Roman">Unix</FONT>系统的极大兴趣。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">由于<FONT face="Times New Roman">1956</FONT>年反托拉斯法案的限制,<FONT face="Times New Roman">AT&T</FONT>不能涉足计算机业务,但允许它可以以象征性的费用发售该系统。就这样,<FONT face="Times New Roman">Unix</FONT>被广泛发布,首先是学术科研用户,后来又扩展到政府和商业用户。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">伯克利(<FONT face="Times New Roman">Berkeley</FONT>)的加州大学是学术用户中的一个。在这里<FONT face="Times New Roman">Unix</FONT>得到了计算机系统研究小组(<FONT face="Times New Roman">CSRG</FONT>)的广泛应用。并且在这里所进行的修改引发了<FONT face="Times New Roman">Unix</FONT>的一大系列,这就是广为人知的伯克利软件开发(<FONT face="Times New Roman">BSD</FONT>)<FONT face="Times New Roman">Unix</FONT>。除了<FONT face="Times New Roman">AT&T</FONT>所提供的<FONT face="Times New Roman">Unix</FONT>系列之外,<FONT face="Times New Roman">BSD</FONT>是最有影响力的<FONT face="Times New Roman">Unix</FONT>系列。<FONT face="Times New Roman">BSD</FONT>在<FONT face="Times New Roman">Unix</FONT>中增加了很多显著特性,例如<FONT face="Times New Roman">TCP/IP</FONT>网络,更好的用户文件系统(<FONT face="Times New Roman">UFS</FONT>),工作控制,并且改进了<FONT face="Times New Roman">AT&T</FONT>的内存管理代码。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">多年以来,<FONT face="Times New Roman">BSD</FONT>版本的<FONT face="Times New Roman">Unix</FONT>一直在学术环境中占据主导地位,但最终发展成为<FONT face="Times New Roman">System V</FONT>版本的<FONT face="Times New Roman">AT&T</FONT>的<FONT face="Times New Roman">Unix</FONT>则成为商业领域的主宰。从某种程度上来说,这是有社会原因的:学校倾向于使用非正式但通常更好用的<FONT face="Times New Roman">BSD</FONT>风格的<FONT face="Times New Roman">Unix</FONT>,而商业界则倾向于从<FONT face="Times New Roman">AT&T</FONT>获取<FONT face="Times New Roman">Unix</FONT>。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">在用户需求驱动和用户编程改进特性的促进下,<FONT face="Times New Roman">BSD</FONT>风格的<FONT face="Times New Roman">Unix</FONT>一般要比<FONT face="Times New Roman">AT&T</FONT>的<FONT face="Times New Roman">Unix</FONT>更具有创新性,而且改进也更为迅速。但是,在<FONT face="Times New Roman">AT&T</FONT>发布最后一个正式版本<FONT face="Times New Roman">System V Release 4</FONT>(<FONT face="Times New Roman">SVR4</FONT>)时,<FONT face="Times New Roman">System V Unix</FONT>已经吸收了<FONT face="Times New Roman">BSD</FONT>的大多数重要的优点,并且还增加了一些自己的优势。这种现象的部分原因在于从<FONT face="Times New Roman">1984</FONT>年开始,<FONT face="Times New Roman">AT&T</FONT>逐渐可以将<FONT face="Times New Roman">Unix</FONT>商业化,而伯克利<FONT face="Times New Roman">Unix</FONT>的开发工作在<FONT face="Times New Roman">1993</FONT>年<FONT face="Times New Roman">BSD4.4</FONT>版本完成以后就逐渐收缩以至终止了。然而,<FONT face="Times New Roman">BSD</FONT>的进一步改进由外界开发者延续下来,到今天还在继续进行。正在进行的<FONT face="Times New Roman">Unix</FONT>系列开发中至少有四个独立的版本是直接起源于<FONT face="Times New Roman">BSD4.4</FONT>,这还不包括几个厂商的<FONT face="Times New Roman">Unix</FONT>版本,例如惠普的<FONT face="Times New Roman">HP-UX</FONT>,都是部分地或者全部地基于<FONT face="Times New Roman">BSD</FONT>而发展起来的。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">实际上<FONT face="Times New Roman">Unix</FONT>的变种并不止<FONT face="Times New Roman">BSD</FONT>和<FONT face="Times New Roman">System V</FONT>。由于<FONT face="Times New Roman">Unix</FONT>主要使用<FONT face="Times New Roman">C</FONT>语言来编写,这就使得它相对比较容易地移植到新的机器上,它的简单性也使其相对比较容易重新设计与开发。<FONT face="Times New Roman">Unix</FONT>的这些特点大受商业界硬件供应商的欢迎,比如<FONT face="Times New Roman">Sun</FONT>、<FONT face="Times New Roman">SGI</FONT>、惠普、<FONT face="Times New Roman">IBM</FONT>、<FONT face="Times New Roman">DEC</FONT>(数字设备公司)、<FONT face="Times New Roman">Amdahl</FONT>等等;<FONT face="Times New Roman">IBM</FONT>还不止一次对<FONT face="Times New Roman">Unix</FONT>进行了再开发。厂商们设计开发出新的硬件并简单地将<FONT face="Times New Roman">Unix</FONT>移植到新的硬件上,这样新的硬件一经发布便具备一定的功能。经过一段时间之后,这些厂商都拥有了自己的专有<FONT face="Times New Roman">Unix</FONT>版本。而且为了占有市场,这些版本故意以不同的侧重点发布出来以更好的占有用户。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">版本混乱的状态促进了标准化工作的进行。其中最主要的就是<FONT face="Times New Roman">OSIX</FONT>系列标准,它定义了一套标准的操作系统接口和工具。从理论上说,<FONT face="Times New Roman">OSIX</FONT>标准代码很容易移植到任何遵守<FONT face="Times New Roman">OSIX</FONT>标准的操作系统中,而且严格的<FONT face="Times New Roman">OSIX</FONT>测试已经把这种理论上的可移植性转化为现实。直到今天,几乎所有的正式操作系统都以支持<FONT face="Times New Roman">OSIX</FONT>标准为目标。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">现在让我们回顾一下,在<FONT face="Times New Roman">1984</FONT>年,杰出的电脑黑客<FONT face="Times New Roman">Richard Stallman</FONT>独立开发出一个类<FONT face="Times New Roman">Unix</FONT>的操作系统,该操作系统具有完全的内核、开发工具和终端用户应用程序。在<FONT face="Times New Roman">GNU</FONT>(“<FONT face="Times New Roman">GNU’s Not Unix</FONT>”首字母的缩写)计划的配合下,<FONT face="Times New Roman">Stallman</FONT>开发这个产品有自己的技术理想:他想开发出一个质量高而且自由的操作系统。<FONT face="Times New Roman">Stallman</FONT>使用了“自由”(<FONT face="Times New Roman">free</FONT>)这个词,不仅意味着用户可以免费的获取软件;而且更重要的是,它将意味着某种程度的“解放”:用户可以自由使用、拷贝、查询、重用、修改甚至是分发这份软件,完全没有软件使用协议的限制。这也正是<FONT face="Times New Roman">Stallman</FONT>创建自由软件基金会(<FONT face="Times New Roman">FSF</FONT>)资助<FONT face="Times New Roman">GNU</FONT>软件开发的本意(<FONT face="Times New Roman">FSF</FONT>也在资助其它科研方面的开发工作)。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">15</FONT>年以来,<FONT face="Times New Roman">GNU</FONT>工程已经吸收、产生了大量的程序,这不仅包括<FONT face="Times New Roman">Emacs</FONT>,<FONT face="Times New Roman">gcc</FONT>(<FONT face="Times New Roman">GNU</FONT>的<FONT face="Times New Roman">C</FONT>编译器),<FONT face="Times New Roman">bash</FONT>(<FONT face="Times New Roman">shell</FONT>命令),还有大部分<FONT face="Times New Roman">Linux</FONT>用户所熟知的许多应用程序。现在正在进行开发的项目是<FONT face="Times New Roman">GNU Hurd</FONT>内核,这是<FONT face="Times New Roman">GNU</FONT>操作系统的最后一个主要部件(实际上<FONT face="Times New Roman">Hurd</FONT>内核早已能够使用了,不过当前的版本号为<FONT face="Times New Roman">0.3</FONT>的系统在什么时候能够完成,还是未知数)。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">尽管<FONT face="Times New Roman">Linux</FONT>大受欢迎,但是<FONT face="Times New Roman">Hurd</FONT>内核还在继续开发。这种情况的原因有几个方面,其一是<FONT face="Times New Roman">Hurd</FONT>的体系结构十分清晰的体现了<FONT face="Times New Roman">Stallman</FONT>关于操作系统工作方式的思想,例如,在运行期间,任何用户都可以部分的改变或替换<FONT face="Times New Roman">Hurd</FONT>(这种替换不是对每个用户都是可见的,而是只对申请修改的用户可见,而且还必须符合安全规范)。另一个原因是据介绍<FONT face="Times New Roman">Hurd</FONT>对于多处理器的支持比<FONT face="Times New Roman">Linux</FONT>本身的内核要好。还有一个简单的原因是兴趣的驱动,因为程序员们希望能够自由地进行自己所喜欢的工作。只要有人希望为<FONT face="Times New Roman">Hurd</FONT>工作,<FONT face="Times New Roman">Hurd</FONT>的开发就不会停止。如果他们能够如愿以偿,<FONT face="Times New Roman">Hurd</FONT>有朝一日将成为<FONT face="Times New Roman">Linux</FONT>的强劲对手。不过在今天,<FONT face="Times New Roman">Linux</FONT>还是自由内核王国里无可争议的主宰。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">在<FONT face="Times New Roman">GNU</FONT>发展的中期,也就是<FONT face="Times New Roman">1991</FONT>年,一个名叫<FONT face="Times New Roman">Linus Torvalds</FONT>的芬兰大学生想要了解<FONT face="Times New Roman">Intel</FONT>的新<FONT face="Times New Roman">CPU</FONT>——<FONT face="Times New Roman">80386</FONT>。他认为比较好的学习方法是自己编写一个操作系统的内核。出于这种目的,加上他对当时<FONT face="Times New Roman">Unix</FONT>变种版本对于<FONT face="Times New Roman">80386</FONT>类机器的脆弱支持十分不满,他决定要开发出一个全功能的、支持<FONT face="Times New Roman">POSIX</FONT>标准的、类<FONT face="Times New Roman">Unix</FONT>的操作系统内核,该系统吸收了<FONT face="Times New Roman">BSD</FONT>和<FONT face="Times New Roman">System V</FONT>的优点,同时摒弃了它们的缺点。<FONT face="Times New Roman">Linus</FONT>(虽然我知道我应该称他为<FONT face="Times New Roman">Torvalds</FONT>,但是所有人都称他为<FONT face="Times New Roman">Linus</FONT>)独立把这个内核开发到<FONT face="Times New Roman">0.02</FONT>版,这个版本已经可以运行<FONT face="Times New Roman">gcc</FONT>,<FONT face="Times New Roman">bash</FONT>和很少的一些应用程序。这些就是他开始的全部工作了。后来,他又开始在因特网络上寻求广泛的帮助。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">不到三年,<FONT face="Times New Roman">Linus</FONT>的<FONT face="Times New Roman">Unix</FONT>—<FONT face="Times New Roman">Linux</FONT>—已经升级到<FONT face="Times New Roman">1.0</FONT>版本。它的源代码量也呈指数形式增长,实现了基本的<FONT face="Times New Roman">TCP/IP</FONT>功能(网络部分的代码后来重写过,而且还可能会再次重写)。此时<FONT face="Times New Roman">Linux</FONT>就已经拥有大约<FONT face="Times New Roman">10</FONT>万用户了。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">现在的<FONT face="Times New Roman">Linux</FONT>内核由<FONT face="Times New Roman">150</FONT>多万行代码组成,<FONT face="Times New Roman">Linux</FONT>也已经拥有了大约<FONT face="Times New Roman">1000</FONT>万用户(由于<FONT face="Times New Roman">Linux</FONT>可以自由获取和拷贝,获取具体的统计数字是不可能的)。<FONT face="Times New Roman">Linux</FONT>内核<FONT face="Times New Roman">GNU/Linux</FONT>附同<FONT face="Times New Roman">GNU</FONT>工具已经占据了<FONT face="Times New Roman">Unix 50%</FONT>的市场。一些公司正在把内核和一些应用程序同安装软件打包在一起,生产出<FONT face="Times New Roman">Linux</FONT>的<FONT face="Times New Roman">distribution</FONT>(发行版本),这些公司包括<FONT face="Times New Roman">Red Hat</FONT>和<FONT face="Times New Roman">Calera prominent</FONT>公司。现在的<FONT face="Times New Roman">GNU/Linux</FONT>已经备受注目,得到了诸如<FONT face="Times New Roman">Sun</FONT>、<FONT face="Times New Roman">IBM</FONT>、<FONT face="Times New Roman">SGI</FONT>等公司的广泛支持。<FONT face="Times New Roman">SGI</FONT>最近决定在其基于<FONT face="Times New Roman">Intel</FONT>的<FONT face="Times New Roman">Merced</FONT>的系列机器上不再搭载自己的<FONT face="Times New Roman">Unix</FONT>变种版本<FONT face="Times New Roman">IRIX</FONT>,而是直接采用<FONT face="Times New Roman">GNU/Linux</FONT>;<FONT face="Times New Roman">Linux</FONT>甚至被指定为<FONT face="Times New Roman">Amiga</FONT>将要发布的新操作系统的基础。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">GNU通用公共许可证</H2><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">这样一个如此流行大受欢迎的操作系统当然值得我们学习。按照通用公共许可证<FONT face="Times New Roman">(GPL</FONT>,(<FONT face="Times New Roman">General Public License</FONT>)<FONT face="Times New Roman">)</FONT>的规定,<FONT face="Times New Roman">Linux</FONT>的源代码可以自由获取,这使得我们学习该系统的强烈愿望得以实现。<FONT face="Times New Roman">GPL</FONT>这份非同寻常的软件许可证,充分体现了上面提到的<FONT face="Times New Roman">Stallman</FONT>的思想:只要用户所做的修改是同等自由的,用户可以自由地使用、拷贝、查询、重用、修改甚至重新发布这个软件。通过这种方式,<FONT face="Times New Roman">GPL</FONT>保证了<FONT face="Times New Roman">Linux</FONT>(以及同一许可证保证下的大量其它软件)不仅现在自由可用,而且以后经过任何修改之后都仍然可以自由使用。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">请注意这里的自由并不是说没有人靠这个软件盈利,有一些日益兴起的公司,比如发行最流行的<FONT face="Times New Roman">Linux</FONT>发行版本的<FONT face="Times New Roman">Red Hat</FONT>,就是一个例子。(<FONT face="Times New Roman">Red Hat</FONT>自从面世以来,市值已经突破数十亿美元,每年盈利数十万美元,而且这些数字还在不断增长)。但是任何人都不能限制其它用户涉足本软件领域,而且所作的修改不能减少其自由程度。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">本书的附录<FONT face="Times New Roman">B</FONT>中收录有<FONT face="Times New Roman">GNU</FONT>通用公共许可证协议的全文。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">Linux开发过程</H2><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">如上所述,由于<FONT face="Times New Roman">Linux</FONT>是一款自由软件,它可以免费获取以供学习研究。<FONT face="Times New Roman">Linux</FONT>之所以值得学习研究,是因为它是相当优秀的操作系统。如果<FONT face="Times New Roman">Linux</FONT>操作系统相当糟糕,那它就根本不值得被我们使用,也就没有必要去研究相关的书籍。(除非一种可能,为了追求刺激)。<FONT face="Times New Roman">Linux</FONT>是一款十分优秀的操作系统还在于几个相互关联的原因。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">Linux</FONT>优秀的原因之一在于它是基于天才的思想开发而成的。在学生时代就开始推动整个系统开发的<FONT face="Times New Roman">Linus Torvads</FONT>是一个天才,他的才能不仅展现在编程能力方面,而且组织技巧也相当杰出。<FONT face="Times New Roman">Linux</FONT>的内核是由世界上一些最优秀的程序员开发并不断完善的,他们通过<FONT face="Times New Roman">Internet</FONT>相互协作,开发理想的操作系统;他们享受着工作中的乐趣,而且也获得了充分的自豪感。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">Linux</FONT>优秀的另外一个原因在于它是基于一组优秀的概念。<FONT face="Times New Roman">Unix</FONT>是一个简单却非常优秀的模型。在<FONT face="Times New Roman">Linux</FONT>创建之前,<FONT face="Times New Roman">Unix</FONT>已经有<FONT face="Times New Roman">20</FONT>年的发展历史。<FONT face="Times New Roman">Linux</FONT>从<FONT face="Times New Roman">Unix</FONT>的各个流派中不断吸取成功经验,模仿<FONT face="Times New Roman">Unix</FONT>的优点,抛弃<FONT face="Times New Roman">Unix</FONT>的缺点。这样做的结果是<FONT face="Times New Roman">Linux </FONT>成为了<FONT face="Times New Roman">Unix</FONT>系列中的佼佼者:高速、健壮、完整,而且抛弃了历史包袱。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">然而,<FONT face="Times New Roman">Linux</FONT>最强大的生命力还在于其公开的开发过程。每个人都可以自由获取内核源程序,每个人都可以对源程序加以修改,而后他人也可以自由获取你修改后的源程序。如果你发现了缺陷(<FONT face="Times New Roman">bug</FONT>),你可以对它进行修正,而不用去乞求不知名的公司来为你修正。如果你有什么最优化或者新特点的创意,你也可以直接在系统中增加功能,而不用向操作系统供应商解释你的想法,指望他们将来会增加相应的功能。当发现一个安全漏洞后,你可以通过编程来弥补这个漏洞,而不用关闭系统直到你的供应商为你提供修补程序。由于你拥有直接访问源代码的能力,你也可以直接阅读代码来寻找缺陷,或是效率不高的代码,或是安全漏洞,以防患于未然。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">除非你是一个程序员,否则这一点听起来仿佛没有多少吸引力。实际上即使你不是程序员,这种开发模型也将使你受益匪浅,这主要体现在以下两个方面:</P><P 0cm 0cm 0pt 42.5pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo3; tab-stops: list 42.5pt">l 可以间接受益于世界各地成千上万的程序员随时进行的改进工作。</P><P 0cm 0cm 0pt 42.5pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo3; tab-stops: list 42.5pt">l 如果你需要对系统进行修改,你可以雇用程序员为你完成工作。这部分人将根据你的需求定义单独为你服务。可以设想,这在源程序不公开的操作系统中它将是什么样子。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">Linux</FONT>这种独特的自由流畅的开发模型已被命名为<FONT face="Times New Roman">bazaar</FONT>(集市模型),它是相对于<FONT face="Times New Roman">cathedral</FONT>(教堂)模型而言的。在<FONT face="Times New Roman">cathedral</FONT>模型中,源程序代码被锁定在一个保密的小范围内。只有开发者(很多情况下是市场)认为能够发行一个新版本,这个新版本才会被推向市场。这些术语在<FONT face="Times New Roman">Eric S. Raymond</FONT>的<FONT face="Times New Roman">The Cathedral and the Bazaar</FONT>一文中有所介绍,大家可以在<B normal"><FONT face="Times New Roman">http://www.tuxedo.org/~esr/writings/</FONT></B>找到这篇文章。<FONT face="Times New Roman">Bazaar</FONT>开发模型通过重视实验,征集并充分利用早期的反馈,对巨大数量的脑力资源进行平衡配置,可以开发出更优秀的软件。(顺便说一下,虽然<FONT face="Times New Roman">Linux</FONT>是最为明显的使用<FONT face="Times New Roman">bazaar</FONT>开发模型的例子,但是它却远不是第一个使用这个模型的系统。)</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">为了确保这些无序的开发过程能够有序地进行,<FONT face="Times New Roman">Linux</FONT>采用了双树系统。一个树是稳定树(<FONT face="Times New Roman">stable tree</FONT>),另一个树是非稳定树(<FONT face="Times New Roman">unstable tree</FONT>)或者开发树(<FONT face="Times New Roman">development tree</FONT>)。一些新特性、实验性改进等都将首先在开发树中进行。如果在开发树中所做的改进也可以应用于稳定树,那么在开发树中经过测试以后,在稳定树中将进行相同的改进。按照<FONT face="Times New Roman">Linus</FONT>的观点,一旦开发树经过了足够的发展,开发树就会成为新的稳定树,如此周而复始的进行下去。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">源程序版本号的形式为<FONT face="Times New Roman">x.y.z</FONT>。对于稳定树来说,<FONT face="Times New Roman">y</FONT>是偶数;对于开发树来说,<FONT face="Times New Roman">y</FONT>比相应的稳定树大一(因此,是奇数)。截至到本书截稿时,最新的稳定内核版本号是<st1:chsdate w:st="on" IsROCDate="False" IsLunarDate="False" Day="30" Month="12" Year="1899"><FONT face="Times New Roman">2.2.10</FONT></st1:chsdate>,最新的开发内核的版本号是<FONT face="Times New Roman">2.3.12</FONT>。对<FONT face="Times New Roman">2.3</FONT>树的缺陷修正会回溯影响(<FONT face="Times New Roman">back-propagated</FONT>)<FONT face="Times New Roman">2.2</FONT>树,而当<FONT face="Times New Roman">2.3</FONT>树足够成熟的时候会发展成为<FONT face="Times New Roman">2.4.0</FONT>。(顺便说一下,这种开发会比常规惯例要快,因为每一版本所包含的改变比以前更少了,内核开发人员只需花很短的时间就能够完成一个实验开发周期。)</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><B normal"><FONT face="Times New Roman">http://www.kernel.org</FONT></B>及其镜像站点提供了最新的可供下载的内核版本,而且同时包括稳定和开发版本。如果你愿意的话,不需要很长时间,这些站点所提供的最新版本中就可能包含了你的一部分源程序代码。</P><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P>
<H1 12pt 0cm; TEXT-INDENT: 0cm">第<FONT face="Times New Roman">4</FONT>章<FONT face="Times New Roman"> </FONT>系统初始化</H1>< 0cm 0cm 0pt">当你想要运行程序时,你需要把程序的文件名敲入<FONT face="Times New Roman">shell</FONT>――或者更为流行的,在如<FONT face="Times New Roman">GNOME</FONT>或者<FONT face="Times New Roman">KDE</FONT>等之类桌面环境中点击相应的图标――这样就能将其装载进内核并运行。但是,首先必须有其它的软件来装载并运行内核;这通常是诸如<FONT face="Times New Roman">LOADLIN</FONT>或者<FONT face="Times New Roman">LILO</FONT>之类的内核引导程序。更进一步,我们还需要其它的软件来装载运行内核引导程序――称之“内核引导程序的引导程序”――而且看起来似乎运行内核引导程序的引导程序也需要内核引导程序的引导程序的引导程序,等等,这个过程是无限的。</P>< 0cm 0cm 0pt">这个无限循环的过程必然最终在某个地方终止,这就是硬件。因此,在最低的层次上,启动系统的第一步是从硬件中获得帮助。该硬件总是运行一些短小的内置程序――软件,但是这些软件是被固化在只读存储器中,存储在已知地址中。因此,在这种情况下就不需要软件引导程序了――它能够运行更大更复杂的程序,直到内核自身装载成功为止。按照这种方式,系统自己的引导过程(<FONT face="Times New Roman">bootstrap</FONT>)会引发系统的启动,当然这只是术语“系统引导(<FONT face="Times New Roman">booting</FONT>)”的一个比喻。虽然不同体系结构的引导过程的具体细节差异很大,但是它们的原则都基本相同。</P>< 0cm 0cm 0pt">前面的工作都完成以后,内核就已经成功装载了。随后内核可以初始化自身以及系统的其它部分。</P>< 0cm 0cm 0pt">本章首先将简单介绍基于<FONT face="Times New Roman">x86 PC</FONT>机的典型自启动方式,接着回顾一下每一步工作在什么时机发生,最后我们还要介绍的是内核的相应部分。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">引导PC机</H2>< 0cm 0cm 0pt">本节简要介绍<FONT face="Times New Roman">x86 PC</FONT>是如何引导的。本节的目的不是让你精通<FONT face="Times New Roman">C</FONT>是怎样引导的――这超出了本书的范围――而是向你展示特定体系结构一般的引导方式,为下文中的内核初始化进行铺垫。</P>< 0cm 0cm 0pt">首先,机器中的每个<FONT face="Times New Roman">CPU</FONT>都要自行初始化,接着可能要用几分之一秒的时间来执行自测试。在多处理器的系统中,这个过程会更复杂些――但是实际上也并不多。在双处理器的<FONT face="Times New Roman">entium</FONT>系统中,一个<FONT face="Times New Roman">CPU</FONT>总是作为主<FONT face="Times New Roman">CPU</FONT>存在,另外一个<FONT face="Times New Roman">CPU</FONT>则是辅<FONT face="Times New Roman">CPU</FONT>。主<FONT face="Times New Roman">CPU</FONT>执行启动过程中的剩余工作,随后内核才会激活辅<FONT face="Times New Roman">CPU</FONT>。在多处理器的<FONT face="Times New Roman">entium Pro</FONT>系统中,<FONT face="Times New Roman">CPU</FONT>必须根据<FONT face="Times New Roman">Intel</FONT>定义的算法“抢夺标志”――来动态决定由哪个<FONT face="Times New Roman">CPU</FONT>启动系统。取得标志的<FONT face="Times New Roman">CPU</FONT>启动系统,随后内核激活其它的<FONT face="Times New Roman">CPU</FONT>。无论是哪种情况,启动程序的剩余部分只与一个<FONT face="Times New Roman">CPU</FONT>有关。这样,在随后的一段时间内,我们可以认为该系统中只有一个<FONT face="Times New Roman">CPU</FONT>是可用的,而不考虑其它的<FONT face="Times New Roman">CPU</FONT>,或者说这些<FONT face="Times New Roman">CPU</FONT>被暂时隐藏了。另一方面,内核还需要明确的激活所有其它的<FONT face="Times New Roman">CPU</FONT>――这一点你可以在本章后续部分看到。</P>< 0cm 0cm 0pt">接下来,<FONT face="Times New Roman"> CPU</FONT>从<FONT face="Times New Roman">0xfffffff0</FONT>单元中取得指令并执行,这个地址非常接近于<FONT face="Times New Roman">32</FONT>位<FONT face="Times New Roman">CPU</FONT>的最后可用的地址。因为大多数<FONT face="Times New Roman">C</FONT>都没有<FONT face="Times New Roman">4GB</FONT>的<FONT face="Times New Roman">RAM</FONT>,所以通常在这个地址上并没有实际内存的。内存硬件可以虚拟使用它。对那些确实有<FONT face="Times New Roman">4GB</FONT>内存的机器来说,它们也只是仅仅损失了供<FONT face="Times New Roman">BIOS</FONT>使用的顶端地址空间末尾的少量内存(实际上<FONT face="Times New Roman">BIOS</FONT>在这里只保留了<FONT face="Times New Roman">64K</FONT>的空间――这种损失在<FONT face="Times New Roman">4GB</FONT>的机器中是可以忽略的)。</P>< 0cm 0cm 0pt">该地址单元中存储的指令是一条跳转指令,这条指令跳转到基本输入输出(<FONT face="Times New Roman">BIOS</FONT>)代码的首部。<FONT face="Times New Roman">BIOS</FONT>内置在主板中,它主要负责控制系统的启动。请注意<FONT face="Times New Roman">CPU</FONT>实际上并不真正关心<FONT face="Times New Roman">BIOS</FONT>是否存在,这样就使得在诸如用户定制的嵌入系统之类的非<FONT face="Times New Roman">C</FONT>体系结构的计算机中使用<FONT face="Times New Roman">Intel</FONT>的<FONT face="Times New Roman">CPU</FONT>成为可能。<FONT face="Times New Roman">CPU</FONT>执行在目标地址中发现的任何指令,在这里使用跳转指令转移到<FONT face="Times New Roman">BIOS</FONT>只是<FONT face="Times New Roman">C</FONT>体系结构的一部分。(实际上,跳转指令自己是<FONT face="Times New Roman">BIOS</FONT>的一部分,但是这不是考虑这个问题的最方便的方法。)</P>< 0cm 0cm 0pt"><FONT face="Times New Roman">BIOS</FONT>使用内置的规则来选择启动设备。通常情况下,这些规则是可以改变的,方法是在启动过程开始时按下一个键(例如,在我的系统中是<FONT face="Times New Roman">Delete</FONT>键)并通过一些菜单选项浏览选择。但是,通常的过程是<FONT face="Times New Roman">BIOS</FONT>首先试图从软盘启动,如果失败了,就再试图从主硬盘上启动。如果又失败了,就再试图从<FONT face="Times New Roman">CD-ROM</FONT>上启动。为了使问题更具体,这里讨论的情况假定是最普通的,也就是启动设备是硬盘。</P>< 0cm 0cm 0pt">从这种启动设备上启动,<FONT face="Times New Roman">BIOS</FONT>读取第一个扇区的信息――首<FONT face="Times New Roman">512</FONT>个字节――称之为主引导记录(<FONT face="Times New Roman">MBR</FONT>)。接下来发生的内容有赖于<FONT face="Times New Roman">Linux</FONT>是怎样在系统上安装的。为使讨论形象具体,我们假定<FONT face="Times New Roman">LILO</FONT>是内核的载入程序。在典型的设置中,<FONT face="Times New Roman">BIOS</FONT>检测<FONT face="Times New Roman">MBR</FONT>中的关键数字(为了确认该数据段的确是<FONT face="Times New Roman">MBR</FONT>)并在<FONT face="Times New Roman">MBR</FONT>中检测引导扇区的位置。这一扇区包含了<FONT face="Times New Roman">LILO</FONT>的开始部分,然后<FONT face="Times New Roman">BIOS</FONT>将其装入内存,开始执行。</P>< 0cm 0cm 0pt">注意我们现在已经实现了从硬件和内置软件的范围到实际软件范围的转变,从有形范围到无形范围,也就是说从你可以接触的部分到不可接触的部分。</P>< 0cm 0cm 0pt">下面就是<FONT face="Times New Roman">LILO</FONT>的责任了。它把自己其余的部分装载进来,在磁盘上找到配置数据,这些数据指明从什么地方可以得到内核,启动时要通过什么选项。<FONT face="Times New Roman">LILO</FONT>接着装载内核到内存并跳转到内核。</P>< 0cm 0cm 0pt">通常,内核以压缩形式存储,只有少量指令足以完成解压缩的任务,也就是自解压可执行文件,是以非压缩形式存储的。因此,内核的下一步工作是自解压缩内核镜像。到这里,内核就已经完成了装载的过程。</P>< 0cm 0cm 0pt">下面是到现在进行的步骤地简要描述:</P>< 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo2; tab-stops: list 21.25pt 42.25pt"><FONT face="Times New Roman">1. CPU</FONT>初始化自身,接着在固定位置执行一条指令。</P>< 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo2; tab-stops: list 21.25pt 42.25pt"><FONT face="Times New Roman">2. </FONT>这条指令跳转到<FONT face="Times New Roman">BIOS</FONT>中。</P>< 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo2; tab-stops: list 21.25pt 42.25pt"><FONT face="Times New Roman">3. BIOS</FONT>找到启动设备并获取<FONT face="Times New Roman">MBR</FONT>,该<FONT face="Times New Roman">MBR</FONT>指向<FONT face="Times New Roman">LILO</FONT>。</P>< 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo2; tab-stops: list 21.25pt 42.25pt"><FONT face="Times New Roman">4. BIOS</FONT>装载并把控制权转交给<FONT face="Times New Roman">LILO</FONT>。</P>< 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo2; tab-stops: list 21.25pt 42.25pt"><FONT face="Times New Roman">5. LILO</FONT>装载压缩内核。</P>< 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo2; tab-stops: list 21.25pt 42.25pt"><FONT face="Times New Roman">6. </FONT>压缩内核自解压并把控制权转交给解压的内核</P>< 0cm 0cm 0pt">正如你所见到的,引导过程每一步都将你带入更大量更复杂的代码块中,一直到最后成功地运行了内核为止。</P>< 0cm 0cm 0pt">依赖于你计算层次的方式,<FONT face="Times New Roman">CPU</FONT>成为内核引导程序的引导程序的引导程序的引导程序(<FONT face="Times New Roman">CPU</FONT>装载<FONT face="Times New Roman">BIOS</FONT>,<FONT face="Times New Roman">BIOS</FONT>装载<FONT face="Times New Roman">LILO</FONT>,<FONT face="Times New Roman">LILO</FONT>装载压缩内核,压缩内核装载解压内核;但是你可以合理的考虑是否这些步骤都满足引导程序的定义)。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">初始化Linux内核</H2>< 0cm 0cm 0pt">在内核成功装入内存(如果需要就解压缩)以及一些关键硬件,例如已经在低层设置过的内存管理器(<FONT face="Times New Roman">MMU</FONT>,请参看第<FONT face="Times New Roman">8</FONT>章)之后,内核将跳转到<B normal"><FONT face="Times New Roman">start_kernel</FONT></B>(<FONT face="Times New Roman">19802</FONT>行)。这个函数完成其余的系统初始化工作――实际上,几乎所有的初始化工作都是由这个函数实现的。因此,<B normal"><FONT face="Times New Roman">start_kernel</FONT></B>就是本节的核心。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">start_kernel</H4>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19802</FONT>:<B normal"><FONT face="Times New Roman">__init</FONT></B>标示符在<FONT face="Times New Roman">gcc</FONT>编译器中指定将该函数置于内核的特定区域。在内核完成自身初始化之后,就试图释放这个特定区域。实际上,内核中存在两个这样的区域,<FONT face="Times New Roman">.text.init</FONT>和<FONT face="Times New Roman">.data.init</FONT>――第一个是代码初始化使用的,另外一个是数据初始化使用的。(诸如可以在进程间共享的代码和字符串常量之类的“文本(<FONT face="Times New Roman">Text</FONT>)”是在可执行程序中的“纯区域”中使用的一个术语。)另外你也可以看到<B normal"><FONT face="Times New Roman">__initfunc</FONT></B>和<B normal"><FONT face="Times New Roman">__initdata</FONT></B>标志,前者和<B normal"><FONT face="Times New Roman">__init</FONT></B>类似,标志初始化专用代码,后者则标志初始化专用数据。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19807</FONT>:如前所述,即使在多处理器系统中,在启动时也只使用一个<FONT face="Times New Roman">CPU</FONT>。<FONT face="Times New Roman">Intel</FONT>称之为引导程序处理器(<FONT face="Times New Roman">Bootstrap Processor</FONT>,简称为<FONT face="Times New Roman">BSP</FONT>),它在内核代码的某些地方有时也称之为<FONT face="Times New Roman">BP</FONT>。<FONT face="Times New Roman">BSP</FONT>首次运行这一行时,跳过后面的<B normal"><FONT face="Times New Roman">if</FONT></B>语句,并减小<B normal"><FONT face="Times New Roman">boot_cpu</FONT></B>标志,从而当其它<FONT face="Times New Roman">CPU</FONT>运行到此处时,都要运行<FONT face="Times New Roman">if</FONT>语句。等到其它<FONT face="Times New Roman">CPU</FONT>被激活执行到这里时,<FONT face="Times New Roman">BSP</FONT>已经在<FONT face="Times New Roman">idle</FONT>循环中了(本章稍后会更详细的讨论这个问题),<B normal"><FONT face="Times New Roman">initialize_secondary</FONT></B>(<FONT face="Times New Roman">4355</FONT>行)负责把其它<FONT face="Times New Roman">CPU</FONT>加入到<FONT face="Times New Roman">BSP</FONT>中。这样,其它<FONT face="Times New Roman">CPU</FONT>就不用执行<B normal"><FONT face="Times New Roman">start_kernel</FONT></B>的剩余部分了――这也是一件好事,因为这意味着不用再进行对许多硬件进行冗余初始化等工作了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>顺便说一下,这种奇异的小小的改动只有对于<FONT face="Times New Roman">x86</FONT>是必需的;对于其它平台,调用<B normal"><FONT face="Times New Roman">smp_init</FONT></B>完全可以处理<FONT face="Times New Roman">SMP</FONT>设置的其它部分,这一点马上就会讨论。因此,其它平台的<B normal"><FONT face="Times New Roman">initialize_secondary</FONT></B>的定义都是空的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19816</FONT>:打印内核标题信息(<FONT face="Times New Roman">20099</FONT>行),这里显示了有关内核如何编译的信息,包括在什么机器上编译,什么时间编译,使用什么版本的编译器,等等。如果中间任何一步发生了错误,在寻找机器不能启动的原因时查明内核的来源是一个有用的线索。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19817</FONT>:初始化内核自身的部分组件――内存,硬件中断,调度程序,等等。尤其是<B normal"><FONT face="Times New Roman">setup_arch</FONT></B>函数(<FONT face="Times New Roman">19765</FONT>行)完成体系结构相关的设置,此后在<B normal"><FONT face="Times New Roman">command_line</FONT></B>(传递到内核的参数,在下面讨论)、<B normal"><FONT face="Times New Roman">memory_start</FONT></B>和<B normal"><FONT face="Times New Roman">memory_end</FONT></B>(内核可用物理地址范围)中返回结果。下面这些函数都希望驻留在内存低端的;它们使用<B normal"><FONT face="Times New Roman">memory_start</FONT></B>和<B normal"><FONT face="Times New Roman">memory_end</FONT></B>来传递该信息。在函数获得所希望的值后,返回值指明了新的<B normal"><FONT face="Times New Roman">memory_start</FONT></B>的值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19823</FONT>:分析传给内核的各种选项。<B normal"><FONT face="Times New Roman">parse_options</FONT></B>函数(<FONT face="Times New Roman">19707</FONT>行,在随后的分析内核选项一节中讨论)也设置了<B normal"><FONT face="Times New Roman">argv</FONT></B>和<B normal"><FONT face="Times New Roman">envp</FONT></B>的初值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19833</FONT>:内核运行过程中也可以自行对所进行的工作进行记录,周期性地对所执行的指令进行抽样,并使用所获得的结果更新表格。这在定时器中断过程中通过调用<B normal"><FONT face="Times New Roman">x86_do_profile</FONT></B>(<FONT face="Times New Roman">1896</FONT>行)来实现,该部分将在第<FONT face="Times New Roman">6</FONT>章中介绍。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>如图<FONT face="Times New Roman">4.1</FONT>中说明的那样,这个表格把内核划分为几个大小相同的范围,并简单跟踪在一次中断的时间内每个范围中运行多少条指令。这种记录当然是非常粗糙的――甚至不是依据函数和行号进行划分的,而只是使用近似的地址――但是这样代价很低、快速、短小,而且有助于专家判断最关键的问题要点。每个表格条目所涉及到地址的多少――还有问题发生地点的不确定性――可以通过简单修改<B normal"><FONT face="Times New Roman">prof_shift</FONT></B>(<FONT face="Times New Roman">26142</FONT>行)来调节。<B normal"><FONT face="Times New Roman">profile_setup</FONT></B>(<FONT face="Times New Roman">19076</FONT>行,在本章中后面讨论)可以让你在启动的时候设置<B normal"><FONT face="Times New Roman">prof_shift</FONT></B>的值,这样比为修改这个数字而重新编译内核要清晰方便得多。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><wrapblock><v:shapetype><v:stroke joinstyle="miter"></v:stroke><v:formulas><v:f eqn="if lineDrawn pixelLineWidth 0"></v:f><v:f eqn="sum @0 1 0"></v:f><v:f eqn="sum 0 0 @1"></v:f><v:f eqn="prod @2 1 2"></v:f><v:f eqn="prod @3 21600 pixelWidth"></v:f><v:f eqn="prod @3 21600 pixelHeight"></v:f><v:f eqn="sum @0 0 1"></v:f><v:f eqn="prod @6 1 2"></v:f><v:f eqn="prod @7 21600 pixelWidth"></v:f><v:f eqn="sum @8 21600 0"></v:f><v:f eqn="prod @7 21600 pixelHeight"></v:f><v:f eqn="sum @10 21600 0"></v:f></v:formulas><v:path connecttype="rect" gradientshapeok="t" extrusionok="f"></v:path><lock aspectratio="t" v:ext="edit"></lock></v:shapetype><v:shape><FONT face="Times New Roman"><v:imagedata></v:imagedata><w:wrap type="topAndBottom"></w:wrap></FONT></v:shape></wrapblock><BR vglayout" clear=all></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt; TEXT-ALIGN: center" align=center>图<FONT face="Times New Roman">4.1 </FONT>描述用缓存(<FONT face="Times New Roman">profiling buffer</FONT>)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt; TEXT-ALIGN: center" align=center><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>这个<B normal"><FONT face="Times New Roman">if</FONT></B>程序块为记录表格分配内存,并把所有项都清零。注意到如果<B normal"><FONT face="Times New Roman">prof_shift</FONT></B>是<FONT face="Times New Roman">0</FONT>(缺省值),那么记录功能就被关掉了,<B normal"><FONT face="Times New Roman">if</FONT></B>程序段不再被执行,也不为表格分配空间。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19846</FONT>:内核通过调用<B normal"><FONT face="Times New Roman">sti</FONT></B>(<FONT face="Times New Roman">13104</FONT>行是<FONT face="Times New Roman">UP</FONT>版本的――注意该主题在第<FONT face="Times New Roman">6</FONT>章中有更详细的介绍)开始接收硬件中断。首先需要激活定时器中断,以便后来对<B normal"><FONT face="Times New Roman">calibrate_delay</FONT></B>(<FONT face="Times New Roman">19654</FONT>行)的调用可以计算机器的<FONT face="Times New Roman">BogoMIPS</FONT>的值(在下一节“<FONT face="Times New Roman">BogoMIPS</FONT>”中介绍)。因为一些设备驱动程序需要<FONT face="Times New Roman">BogoMIPS</FONT>的值,所以内核必需在大部分硬件、文件系统等等初始化之前计算出这个值来。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19876</FONT>:测试该<FONT face="Times New Roman">CPU</FONT>的各种缺陷,比如<FONT face="Times New Roman">Pentium F00F</FONT>缺陷(请参看第<FONT face="Times New Roman">8</FONT>章),记录检测到的缺陷,以便于内核的其它部分以后可以使用它们的工作。(为了节省空间起见,我们省略掉了<B normal"><FONT face="Times New Roman">check_bugs</FONT></B>函数。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19882</FONT>:调用<B normal"><FONT face="Times New Roman">smp_init</FONT></B>(<FONT face="Times New Roman">19787</FONT>行),它又调用了其它的函数来激活<FONT face="Times New Roman">SMP</FONT>系统中其它<FONT face="Times New Roman">CPU</FONT>:在<FONT face="Times New Roman">x86</FONT>的平台上,<B normal"><FONT face="Times New Roman">smp_boot_cpus</FONT></B>(<FONT face="Times New Roman">4614</FONT>行)初始化一些内核数据结构,这些数据结构跟踪检测另外的<FONT face="Times New Roman">CPU</FONT>并简单的将其改为保持模式;最后<B normal"><FONT face="Times New Roman">smp_commence</FONT></B>(<FONT face="Times New Roman">4195</FONT>行)使这些<FONT face="Times New Roman">CPU</FONT>继续执行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19883</FONT>:把<FONT face="Times New Roman">init</FONT>函数作为内核线程终止,这比较复杂;请参看本章后面有关<B normal"><FONT face="Times New Roman">init</FONT></B>的讨论。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19885</FONT>:增加<FONT face="Times New Roman">idle</FONT>进程的<B normal"><FONT face="Times New Roman">need_resched</FONT></B>标志,这样做的原因在此时可能还比较模糊。直到读完了第<FONT face="Times New Roman">5</FONT>、<FONT face="Times New Roman">6</FONT>、<FONT face="Times New Roman">7</FONT>章以后,才能有个清楚的概念;但是,在下一个定时器中断结束之前(在第<FONT face="Times New Roman">6</FONT>章中讨论),<B normal"><FONT face="Times New Roman">system_call</FONT></B>(<FONT face="Times New Roman">171</FONT>行,在第<FONT face="Times New Roman">5</FONT>章中讨论)函数中会注意到<FONT face="Times New Roman">idle</FONT>进程的<B normal"><FONT face="Times New Roman">need_fesched</FONT></B>标志增加了,并且调用<B normal"><FONT face="Times New Roman">schedule</FONT></B>(<FONT face="Times New Roman">26686</FONT>行,第<FONT face="Times New Roman">7</FONT>章)释放<FONT face="Times New Roman">CPU</FONT>,并将其赋给更应该获取<FONT face="Times New Roman">CPU</FONT>的进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19886</FONT>:已经完成了内核初始化的工作――或者不管怎样,已经把需要完成的少量责任传递给了<B normal"><FONT face="Times New Roman">init</FONT></B>――所剩余的工作不过是进入<FONT face="Times New Roman">idle</FONT>循环以消耗空闲的<FONT face="Times New Roman">CPU</FONT>时间片。因此,本行调用<B normal"><FONT face="Times New Roman">cpu_idle</FONT></B>(<FONT face="Times New Roman">2014</FONT>行)――<FONT face="Times New Roman">idle</FONT>循环。正如你可以从<B normal"><FONT face="Times New Roman">cpu_idle</FONT></B>本身可以发现的一样,该函数从不返回。然而,当有实际工作要处理时,该函数就会被抢占。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>注意到<B normal"><FONT face="Times New Roman">cpu_idle</FONT></B>只是反复调用<B normal"><FONT face="Times New Roman">idle</FONT></B>系统调用(下一章将讨论系统调用),它通过<B normal"><FONT face="Times New Roman">sys_idle</FONT></B>(<FONT face="Times New Roman">2064</FONT>行)实现真正的<FONT face="Times New Roman">idle</FONT>循环――<FONT face="Times New Roman">2014</FONT>行对应<FONT face="Times New Roman">UP</FONT>版本,<FONT face="Times New Roman">2044</FONT>行针对<FONT face="Times New Roman">SMP</FONT>版本。它们通过执行<B normal"><FONT face="Times New Roman">hlt</FONT></B>(对应“<FONT face="Times New Roman">halt</FONT>”)指令把<FONT face="Times New Roman">CPU</FONT>转入低功耗的“睡眠”状态。只要没有实际的工作处理,<FONT face="Times New Roman">CPU</FONT>都将转入这种状态。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">BogoMIPS</H2><P 0cm 0cm 0pt"><FONT face="Times New Roman">BogoMIPS</FONT>的数字由内核计算并在系统初始化的时候打印。它近似的给出了每秒钟<FONT face="Times New Roman">CPU</FONT>可以执行一个短延迟循环的次数。在内核中,这个结果主要用于需要等待非常短周期的设备驱动程序――例如,等待几微秒并查看设备的某些信息是否已经可用。</P><P 0cm 0cm 0pt">由于没有正确理解<FONT face="Times New Roman">BogoMIPS</FONT>的含义,<FONT face="Times New Roman">BogoMIPS</FONT>在各处都被滥用,就仿佛它可以满足人类最原始、最深层次的需求:把所有计算机性能的信息简化为一个数字。“<FONT face="Times New Roman">BogoMIPS</FONT>”中的“<FONT face="Times New Roman">Bogo</FONT>”部分来源于“伪(<FONT face="Times New Roman">bogus</FONT>)”,就正是为了防止这种用法:虽然这个数字比大多数性能比较有效很多,但是它仍然是不准确的、容易引起误解的、无用的和不真实的,根本不适合将它用于机器间差别的对比。但是这个数字仍然非常吸引人,这也正是我们在这里讨论这个问题的原因。(顺便说一下,<FONT face="Times New Roman">BogoMIPS </FONT>中“<FONT face="Times New Roman">MIPS</FONT>”部分是“<FONT face="Times New Roman">millions of instructions per second</FONT>(百万条指令每秒)”的缩写,这是计算机性能对比中的一个常用单位。)</P><H4 6pt 0cm; TEXT-INDENT: 0cm">calibrate_delay</H4><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">19654</FONT>:<B normal"><FONT face="Times New Roman">calibrate_delay</FONT></B>是近似计算<FONT face="Times New Roman">BogoMIPS</FONT>数字的内核函数。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19622</FONT>:作为第一次估算,<B normal"><FONT face="Times New Roman">calibrate_delay</FONT></B>计算出在每一秒内执行多少次<B normal"><FONT face="Times New Roman">__delay</FONT></B>循环(<FONT face="Times New Roman">6866</FONT>行),也就是每个定时器滴答(<FONT face="Times New Roman">timer tick</FONT>)――百分之一秒――内延时循环可以执行多少次。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19664</FONT>:计算一个定时器滴答内可以执行多少次循环需要在滴答开始时就开始计数,或者应该尽可能与它接近。全局变量<B normal"><FONT face="Times New Roman">jiffies</FONT></B>(<FONT face="Times New Roman">16588</FONT>行)中存储了从内核开始保持跟踪时间开始到现在已经经过的定时器滴答数;第<FONT face="Times New Roman">6</FONT>章中将介绍它的实现方式。<B normal"><FONT face="Times New Roman">jiffies</FONT></B>保持异步更新,在一个中断内——每秒一百次,内核暂时挂起正在处理的内容,更新变量,然后继续刚才的工作。如果不这样处理,下一行的循环就永远不可能退出。从而,如果<B normal"><FONT face="Times New Roman">jiffies</FONT></B>不声明为<B normal"><FONT face="Times New Roman">volatile</FONT></B>――简单的说,这个值变化的原因对于编译器是透明的――<FONT face="Times New Roman">gcc</FONT>仍然可能对该循环进行优化,并引起该循环进入不能退出的状态。虽然目前的<FONT face="Times New Roman">gcc</FONT>还没有如此高的智能,然而它的维护者应该完全能够为它实现这种智能。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19669</FONT>:定时器又前移了一个滴答,因此又产生一个新的滴答。下一步是要等待<B normal"><FONT face="Times New Roman">loops_per_sec</FONT></B>延时循环调用定时器循环,接着检测是否最少有一个完整的滴答已经完成。如果是这样,就退出首次近似估算循环;如果没有,就把<B normal"><FONT face="Times New Roman">loops_per_sec</FONT></B>的值加倍,然后重新启动这个过程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>这个循环的正确性依赖于如下的事实:现有的机器在任何地方都不能每秒执行<FONT face="Times New Roman">2<SUP>32</SUP></FONT>次延时循环――对于<FONT face="Times New Roman">64</FONT>位机来说则远低于每秒<FONT face="Times New Roman">2<SUP>64</SUP></FONT>次――虽然这只是一个微不足道的问题。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19677</FONT>:现在内核已经清楚<B normal"><FONT face="Times New Roman">loops_per_sec</FONT></B>循环调用延时循环在这台机器上要花费超过百分之一秒的时间才能完成,因此,内核将重新开始进行估算。为了提高效率,内核使用折半查找算法计算<B normal"><FONT face="Times New Roman">loops_per_sec</FONT></B>的实际值,我们假定开始的时候,实际值在现在计算结果和其一半之间――实际值不可能比现在计算值还大,但是可以(而且可能)稍微小一点。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19681</FONT>:和前面使用的方式一样,<B normal"><FONT face="Times New Roman">calibrate_delay</FONT></B>查看是否这个<B normal"><FONT face="Times New Roman">loops_per_sec</FONT></B>已经减小了的值还是比较大,需要耗费一个完整的定时器间隔。如果还是相当大,实际值应该小于当前计算值或者就是当前值,因此,使用更小的值继续查询;如果不够大,就使用一个更大的值继续查询。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19691</FONT>:内核有一种很好的方法来计算一个定时器滴答中执行延时循环的次数。这个数字乘以一秒内滴答的数量就得到了每秒内可以执行的延时循环的次数。这种计算只是一种估算,乘法也累积了误差,因此结果并不能精确到纳秒。但是这个数字供内核使用已经足够精确了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19693</FONT>:为了让用户感到激动,内核打印出这个数字。注意这里明显省略了<FONT face="Times New Roman">%f</FONT>的格式限定――内核尽量避免浮点数运算。这个计算过程中最有用的常量是<FONT face="Times New Roman">500,000</FONT>;它是用一百万除以<FONT face="Times New Roman">2</FONT>得来,理由是每秒钟执行一百万条指令,而每个<FONT face="Times New Roman">delay</FONT>循环的核心是<FONT face="Times New Roman">2</FONT>条指令(<B normal"><FONT face="Times New Roman">decl</FONT></B>和一条跳转指令)。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">分析内核选项</H2><P 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">parse_options</FONT></B>函数分析由内核引导程序发送给内核的启动选项,在初始化过程中按照某些选项运行,并将剩余部分传送给<FONT face="Times New Roman">init</FONT>进程(在本章后面部分提到)。这些选项可能已经存储在配置文件中了,也可能是由用户在系统启动时敲入的――内核并不关心这些。类似的细节全部是内核引导程序应该关注的内容。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">parse_options</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19707</FONT>:参数已经收集在一条长的命令行中,内核被赋给指向该命令行头部的一个指针;内核引导程序在前面已经将该行存储在一个指定地址中。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19718</FONT>:中断下一个参数,保持指向下一个参数的指针以供下一次循环使用。注意系统使用空格而不是通常的空白来分隔内核参数;制表符并不能把当前参数和下一个参数分隔开。如果发现了分隔字符空格,下一行就使用字节<FONT face="Times New Roman">0</FONT>覆盖,这样<B normal"><FONT face="Times New Roman">line</FONT></B>可以作为包含有唯一一个内核选项的标准<FONT face="Times New Roman">C</FONT>字符串来使用了。如果没有发现空格,就该函数关心的内容而言,其余的部分都具有相同的属性――这只有在处理<B normal"><FONT face="Times New Roman">line</FONT></B>中最后一个选项的情况下才会发生,循环就会在下次开始时结束。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>注意该代码不会跳过多个空格。假设<B normal"><FONT face="Times New Roman">line</FONT></B>值如下所述(两个空格):</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> rw debug</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>这会被当作三个选项:“<FONT face="Times New Roman">rw</FONT>”,“”(空字符串)和“<FONT face="Times New Roman">debug</FONT>”。因为空字符串不是有效的内核选项,它将会被传递到初始化的过程(这一点随后就可以看到)――这当然不是用户所希望的。因此,内核引导程序应该负责对多个空格进行压缩。<FONT face="Times New Roman">LILO</FONT>通过忽略用户多敲的空格,完美的解决了这个问题。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19721</FONT>:现在开始解释这些选项。最前面的两个选项――<B normal"><FONT face="Times New Roman">ro</FONT></B>和<B normal"><FONT face="Times New Roman">rw</FONT></B>――指明内核要装载根文件系统,也就是根目录(<FONT face="Times New Roman"> / </FONT>目录)所在的位置,而分别处于只读和读<FONT face="Times New Roman">/</FONT>写模式。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19729</FONT>:第三种可能性,<B normal"><FONT face="Times New Roman">debug</FONT></B>,增加了调试信息的数量;这些调试信息要通过调用<B normal"><FONT face="Times New Roman">do_syslog</FONT></B>打印出来(<FONT face="Times New Roman">25724</FONT>行)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19733</FONT>:开始几个选项是简单的独立标志,它们并不使用参数。内核也可以辨认形为<B normal"><FONT face="Times New Roman">option=value</FONT></B>的选项。本行就是一个例子,这里内核引导程序定义了一个命令来代替<FONT face="Times New Roman">init</FONT>运行;它使用<B normal"><FONT face="Times New Roman">init=<I normal">/some/other/program</I></FONT></B>的形式。这里的代码舍弃了<FONT face="Times New Roman"><B normal">init=</B> </FONT>部分,为随后<B normal"><FONT face="Times New Roman">init</FONT></B>的使用而把剩余部分在<B normal"><FONT face="Times New Roman">execute_command</FONT></B>中保存起来(<FONT face="Times New Roman">20044</FONT>行,后面会讨论到)。和其它大部分参数的处理方法不同,本处功能不能在<B normal"><FONT face="Times New Roman">checksetup</FONT></B>(<FONT face="Times New Roman">19612</FONT>行,马上就讨论到)中实现,这是因为它改变了该函数的局部变量。很快,你就可以看到前面三个选项之所以也在这里处理而不是在<B normal"><FONT face="Times New Roman">checksetup</FONT></B>中处理的原因。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19745</FONT>:大部分内核选项都是由<B normal"><FONT face="Times New Roman">checksetup</FONT></B>函数分析的。如果<B normal"><FONT face="Times New Roman">checksetup</FONT></B>处理了某个选项,就返回真值,循环继续进行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19750</FONT>:否则,<B normal"><FONT face="Times New Roman">line</FONT></B>中没有已经被辨认的内核选项。在这种情况下,它被作为一个供<FONT face="Times New Roman">init</FONT>进程使用的选项或者环境变量来处理――如果其形式为<B normal"><FONT face="Times New Roman">envar=<I normal">value</I></FONT></B>,就作为环境变量处理;否则,就作为选项处理。如果<B normal"><FONT face="Times New Roman">argv_init</FONT></B>和<B normal"><FONT face="Times New Roman">envp_init</FONT></B>(分别见<FONT face="Times New Roman">19057</FONT>和<FONT face="Times New Roman">19059</FONT>行)数组中有足够的空间,选项和环境变量就存储在里面供以后<B normal"><FONT face="Times New Roman">init</FONT></B>函数使用。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>这解释了从<FONT face="Times New Roman">19736</FONT>行开始的注释。字符串<B normal"><FONT face="Times New Roman">auto</FONT></B>并不是任何内核选项的前缀,因此它应该被作为<FONT face="Times New Roman">init</FONT>的一个参数存储在<B normal"><FONT face="Times New Roman">argv_init</FONT></B>数组中――这在大多数情况下都是可行的,因为<B normal"><FONT face="Times New Roman">auto</FONT></B>是<FONT face="Times New Roman">init</FONT>可以识别的选项。但是,当使用<B normal"><FONT face="Times New Roman">init=</FONT></B>的形式给出内核选项时,通常是执行<FONT face="Times New Roman">shell</FONT>而不是<FONT face="Times New Roman">init</FONT>,<B normal"><FONT face="Times New Roman">auto</FONT></B>会使<FONT face="Times New Roman">shell</FONT>混淆;因此,安全一点的方法是,<B normal"><FONT face="Times New Roman">parse_options</FONT></B>在此处忽略所有与此有关的<FONT face="Times New Roman">init</FONT>参数。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>奇怪的是,当<B normal"><FONT face="Times New Roman">argv_init</FONT></B>或者<B normal"><FONT face="Times New Roman">envp_init</FONT></B>空间用完时,整个循环就结束了。仅仅因为<B normal"><FONT face="Times New Roman">argv_init</FONT></B>的空间用完了并不意味着<FONT face="Times New Roman">line</FONT>中就不再含有<FONT face="Times New Roman">init</FONT>使用的环境变量,反之亦然。此外,可能还剩下许多内核选项没有处理。当你考虑到<B normal"><FONT face="Times New Roman">MAX_INIT_ARGX</FONT></B>(<FONT face="Times New Roman">19029</FONT>行)和<B normal"><FONT face="Times New Roman">MAX_INIT_ENVS</FONT></B>(<FONT face="Times New Roman">19030</FONT>行)都通过使用<B normal"><FONT face="Times New Roman">#define</FONT></B>被预定义为<FONT face="Times New Roman">8</FONT>――这是一个很容易超过的下限――这种行为就更奇怪了。如果在<FONT face="Times New Roman">19752</FONT>行和<FONT face="Times New Roman">19756</FONT>行的<B normal"><FONT face="Times New Roman">break</FONT></B>改成<B normal"><FONT face="Times New Roman">continue</FONT></B>,那么循环可以继续处理内核选项,而不会写入超过<B normal"><FONT face="Times New Roman">argv_init</FONT></B>和<B normal"><FONT face="Times New Roman">envp_init</FONT></B>数组界限的空间。如果<B normal"><FONT face="Times New Roman">command_line</FONT></B>中仍然包含有并不是为<FONT face="Times New Roman">init</FONT>而定义的内核选项,那么这一点就是非常重要的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19760</FONT>:所有的内核选项都处理完成了。最后一步是要使用<B normal"><FONT face="Times New Roman">NULL</FONT></B>填充<B normal"><FONT face="Times New Roman">argv_init</FONT></B>和<B normal"><FONT face="Times New Roman">envp_init</FONT></B>数组的末尾,从而使得<FONT face="Times New Roman">init</FONT>可以知道在哪里终止。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">checksetup</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19612</FONT>:<B normal"><FONT face="Times New Roman">checksetup</FONT></B>函数负责进行大部分内核选项的处理过程。它把这些内核选项分为三类:一类使用内核普通参数来分析<FONT face="Times New Roman">=sign</FONT>之后的部分;另一类自行分析<FONT face="Times New Roman">=sign</FONT>之后的部分;还有一类自行分析整个行,包括<FONT face="Times New Roman"><B normal">=</B> sign</FONT>前面的部分和<FONT face="Times New Roman"><B normal">=</B> sign</FONT>后面的部分。第一类被认为是使用“现成”的参数,这与为第二类提供的“原始”参数相对应。最后一类只由一个<FONT face="Times New Roman">IDE</FONT>驱动程序组成;内核首先在<FONT face="Times New Roman">19619</FONT>行检查并处理这种情况,以使其不会在随后的处理中造成麻烦。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19625</FONT>:接下来,<B normal"><FONT face="Times New Roman">checksetup</FONT></B>扫描整个<B normal"><FONT face="Times New Roman">raw_params</FONT></B>数组(<FONT face="Times New Roman">19552</FONT>行)并判断是否该内核选项应该不加处理的保留。<B normal"><FONT face="Times New Roman">raw_params</FONT></B>中的元素是<B normal"><FONT face="Times New Roman">struct kernel_param</FONT></B>类型(<FONT face="Times New Roman">19223</FONT>行)的,它把内核选项前缀和装载选项时调用的函数联系起来。如果数组中的某些项的<B normal"><FONT face="Times New Roman">str</FONT></B>成员以<B normal"><FONT face="Times New Roman">line</FONT></B>为前缀,就会调用<B normal"><FONT face="Times New Roman">line</FONT></B>后面的相应函数(也就是前缀之后的部分),随后<B normal"><FONT face="Times New Roman">checksetup</FONT></B>会返回一个非零值以表明它已经对该内核选项进行了处理。<B normal"><FONT face="Times New Roman">raw_params</FONT></B>数组以两个<B normal"><FONT face="Times New Roman">NULL</FONT></B>结束,因此在检测到<B normal"><FONT face="Times New Roman">str</FONT></B>成员是<B normal"><FONT face="Times New Roman">NULL</FONT></B>时,循环就可以结束了。在这种情况下,显然循环已经到达了<B normal"><FONT face="Times New Roman">raw_params</FONT></B>数组的结尾,但是仍然没有找到匹配的情况。当然,测试<B normal"><FONT face="Times New Roman">setup_func</FONT></B>成员也可以取得同样好的效果。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>这个循环说明了一点:与大多数内核非常不同的是,这里的初始化并不需要尽可能的快。如果内核比从前多用几微秒来启动,这并没有什么实际的损失――毕竟用户应用程序还没有开始运行,所以他们并没有损失什么东西。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>最终结果是代码效率很低,而且存在很多优化的可能。例如,<B normal"><FONT face="Times New Roman">raw_params</FONT></B>数组中字符串的长度可以在<B normal"><FONT face="Times New Roman">raw_params</FONT></B>中暂存,而不用在<FONT face="Times New Roman">19626</FONT>行多次重复计算。更好的解决方法是,可以把<FONT face="Times New Roman">raw_params</FONT>数组中的项按照字符顺序排序,这样<B normal"><FONT face="Times New Roman">checksetup</FONT></B>就可以进行折半查找。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>在<B normal"><FONT face="Times New Roman">raw_params</FONT></B>的情况中实现排序并没有什么障碍,但是这样也可能并不能获得很大的优势,因为折半查找的优点只有在比较大的数组中才能充分表现出来(所谓比较大的确切值在不同的环境中也有所不同)。<B normal"><FONT face="Times New Roman">raw_params</FONT></B>的姊妹数组<B normal"><FONT face="Times New Roman">cooked_params</FONT></B>(<FONT face="Times New Roman">19228</FONT>行)当然是足够大的,可以显示出折半查找的优势;但是这样就引发了一个新的问题:对<B normal"><FONT face="Times New Roman">cooked_params</FONT></B>进行排序比较难用,因为这可能需要分隔一些<B normal"><FONT face="Times New Roman">#ifdef</FONT></B>程序段――请参看从<FONT face="Times New Roman">19268</FONT>行到<FONT face="Times New Roman">19272</FONT>行的例子。进一步说,因为算法只是查找前缀,而不使用完全匹配,在遍历数组中的各个项时对遍历次序比较敏感,所以这种特性在使用不同的查找次序时就很难再保持了。然而,这些问题并不是不可克服的(程序员可以预先静态地为引导程序建立一颗前缀树),如果性能在这里是主要因素,那么这种努力也是值得的。但是,由于性能在这里并不是主要问题,所以简单性才被作为最重要的因素体现出来。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>即使这样,在类似的<B normal"><FONT face="Times New Roman">root_dev_names</FONT></B>数组(<FONT face="Times New Roman">19085</FONT>行)中――这个数组把硬件设备名的前缀映射到它们的主<FONT face="Times New Roman">ID</FONT>号上――开发者仍然可以简单地通过把比较常用的项(<FONT face="Times New Roman">IDE</FONT>和<FONT face="Times New Roman">SCSI</FONT>磁盘)放在不太常用的项(串口<FONT face="Times New Roman">IDE CDs</FONT>)的前面以节省出一点性能。但是我在<B normal"><FONT face="Times New Roman">raw_params</FONT></B>或<B normal"><FONT face="Times New Roman">cooked_params</FONT></B>中并没有发现与之类似的模式。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>另外一件需要注意的事是:现在你可以猜想一下为什么<B normal"><FONT face="Times New Roman">ro</FONT></B>,<B normal"><FONT face="Times New Roman">rw</FONT></B>和<B normal"><FONT face="Times New Roman">debug</FONT></B>选项在<B normal"><FONT face="Times New Roman">parse_options</FONT></B>中测试而不在这里测试――<B normal"><FONT face="Times New Roman">parse_options</FONT></B>要检测精确的匹配,但是<B normal"><FONT face="Times New Roman">checksetup</FONT></B>只检测前缀。作为一个特殊的情况,<B normal"><FONT face="Times New Roman">ro</FONT></B>选项碰巧正好是<B normal"><FONT face="Times New Roman">root=</FONT></B>(<FONT face="Times New Roman">19553</FONT>行)的前缀,这样如果这三个选项彼此合并,就需要仔细处理了。这似乎仍然是一个相当无力的原因。考虑一下<B normal"><FONT face="Times New Roman">noinitrd</FONT></B>选项(<FONT face="Times New Roman">19251</FONT>行)。这是<B normal"><FONT face="Times New Roman">cooked_params</FONT></B>的一个项,因而只需要匹配前缀,而且与之相关联的设置函数(<B normal"><FONT face="Times New Roman">no_initrd</FONT></B>,<FONT face="Times New Roman">19902</FONT>行)将忽略所有可能已经传递给它们的参数――这正像<B normal"><FONT face="Times New Roman">ro</FONT></B>,<B normal"><FONT face="Times New Roman">rw</FONT></B>和<B normal"><FONT face="Times New Roman">debug</FONT></B>被包含在<B normal"><FONT face="Times New Roman">cooked_params</FONT></B>中时所可能进行的工作一样。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19632</FONT>:这个循环为<B normal"><FONT face="Times New Roman">cooked_params</FONT></B>数组的处理工作和前面一个循环为<B normal"><FONT face="Times New Roman">raw_params</FONT></B>数组的处理工作相同。这两个循环(当然不包括循环使用的数组)间的唯一区别是本循环在调用设置函数之前,使用<B normal"><FONT face="Times New Roman">get_options</FONT></B>(<FONT face="Times New Roman">19062</FONT>行)处理<B normal"><FONT face="Times New Roman">line</FONT></B>中<FONT face="Times New Roman">=sign</FONT>后面的部分。简单的说,<B normal"><FONT face="Times New Roman">get_options</FONT></B>使用<FONT face="Times New Roman">10</FONT>个负整数填充<B normal"><FONT face="Times New Roman">ints[1]</FONT></B>到<B normal"><FONT face="Times New Roman">ints[10]</FONT></B>。<B normal"><FONT face="Times New Roman">ints[0]</FONT></B>中是<B normal"><FONT face="Times New Roman">ints</FONT></B>中使用元素的个数――也就是,它记录了存储在<B normal"><FONT face="Times New Roman">ints</FONT></B>中的<B normal"><FONT face="Times New Roman">intsget_options</FONT></B>数量。接着这个数组将被传递给设置函数,该设置函数则会按照自己喜欢的方式对该数组内容进行解释。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19640</FONT>:返回<FONT face="Times New Roman">0</FONT>,说明<B normal"><FONT face="Times New Roman">line</FONT></B>中所包含的内核选项不能被函数理解。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">profile_setup</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19076</FONT>:<B normal"><FONT face="Times New Roman">profile_setup</FONT></B>是<B normal"><FONT face="Times New Roman">checksetup</FONT></B>调用的设置函数的一个完美的例子:这个函数十分短小,使用<B normal"><FONT face="Times New Roman">ints</FONT></B>参数处理了部分内容。而且到目前为止你也应该对它的目的有了一定了解。正如前面提到的一样,用户可以在启动的时候设置<B normal"><FONT face="Times New Roman">prof_shift</FONT></B>的值――好,这里正是它的实现方式。当内核启动过程提供<B normal"><FONT face="Times New Roman">profile=</FONT></B>选项时,就调用<B normal"><FONT face="Times New Roman">profile_setup</FONT></B>函数。前缀字符串和函数在<FONT face="Times New Roman">19235</FONT>行被联系在一起。注意这是在<B normal"><FONT face="Times New Roman">cooked_params</FONT></B>中,因此<B normal"><FONT face="Times New Roman">profile_setup</FONT></B>取得的是处理过的参数。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19079</FONT>:如果参数中存在<B normal"><FONT face="Times New Roman">profile=</FONT></B>的形式,就使用<B normal"><FONT face="Times New Roman">profile=</FONT></B>后面的第一个数字作为<B normal"><FONT face="Times New Roman">prof_shift</FONT></B>的新值。选项给出的其它参数都被简单的忽略了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">19081</FONT>:如果给出了<B normal"><FONT face="Times New Roman">profile=</FONT></B>选项,但是没有为它提供参数,<FONT face="Times New Roman"><B normal">prof_shif</B>t</FONT>的缺省值就是<FONT face="Times New Roman">2</FONT>。这个缺省值有些奇怪,因为我们已经知道,这意味着使用四分之一的内核可用内存来配置其余部分――这是一个很大的开销。但是另一方面,使用这些内存有助于更精确的定位问题热点――只有很少的几条指令存在不确定性,这样应该比较容易地把问题限制在一两行源程序代码内。那张图也并不是像我所画的那样简单:因为图中只描述了内核代码,这种开销还不到内核所有内存空间的<FONT face="Times New Roman">25%</FONT>,但是对于所覆盖的代码量来说却并不止<FONT face="Times New Roman">25%</FONT>。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">init</H2><P 0cm 0cm 0pt"><FONT face="Times New Roman">init</FONT>从许多方面看都是一个非常特殊的进程。这是内核运行的第一个用户进程,它要负责触发其它必需的进程以使系统作为一个整体进入可用的状态。这些工作由<FONT face="Times New Roman">/etc/inittab</FONT>文件控制,通常包括设置<FONT face="Times New Roman">getty</FONT>进程以接受用户登录;建立网络服务,例如<FONT face="Times New Roman">FTP</FONT>和<FONT face="Times New Roman">HTTP</FONT>守护进程;等等。如果没有这些进程,用户就不可能完成多少工作,这样成功启动内核就显得没有多大意义了。</P><P 0cm 0cm 0pt">这种设计的另外一个重要的副作用是<FONT face="Times New Roman">init</FONT>是系统中所有进程的祖先。<FONT face="Times New Roman">init</FONT>产生<FONT face="Times New Roman">getty</FONT>进程,<FONT face="Times New Roman">getty</FONT>进程产生<FONT face="Times New Roman">login</FONT>进程,<FONT face="Times New Roman">login</FONT>进程产生你自己的<FONT face="Times New Roman">shell</FONT>,使用自己的<FONT face="Times New Roman">shell</FONT>,可以产生每一个你运行的进程。在所有的结果中,这有助于确保内核进程表中的所有项最终都能够得到处理。进程结束以后将其清除(回收)的工作首先应由其父进程完成;如果父进程已经退出,那么祖父进程就要担负起这种责任;如果祖父进程已经退出,那么曾祖父进程就要担负起这种责任,周而复始。通过这种方式,从不退出的<FONT face="Times New Roman">init</FONT>进程就可能要负责回收其它进程。</P><P 0cm 0cm 0pt">因此,为了确保这些重要的工作都能正确执行,内核初始化进程所需要做的最后一步工作就是创建<FONT face="Times New Roman">init</FONT>进程,接下来就加以描述。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">init</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">20044</FONT>:<B normal"><FONT face="Times New Roman">unused</FONT></B>参数来源于该函数的非常规调用。<B normal"><FONT face="Times New Roman">init</FONT></B>函数――不要和<I normal"><FONT face="Times New Roman">init</FONT></I><I normal">进程</I>搞混了,后者是它随后要创建的――作为内核线程开始生命周期,一个作为内核的一部分运行的进程。(如果你编写过多线程的程序,这里的内核线程可能会同你所已经知道的线程意义有所不同――在那种意义下,它不是一个内核线程。)实际上,<B normal"><FONT face="Times New Roman">init</FONT></B>函数就像是新进程使用的剥离出来了的<B normal"><FONT face="Times New Roman">main</FONT></B>函数,<B normal"><FONT face="Times New Roman">unused</FONT></B>参数是一个独立的指针,其值指向为给定进程所提供的信息――这比通常使用<B normal"><FONT face="Times New Roman">argc</FONT></B>,<B normal"><FONT face="Times New Roman">argv</FONT></B>和<B normal"><FONT face="Times New Roman">envp</FONT></B>参数传递的信息要少得多。<B normal"><FONT face="Times New Roman">init</FONT></B>函数碰巧不需要额外的信息,因此这个参数命名为<B normal"><FONT face="Times New Roman">unused</FONT></B>,就是要强调这一点。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>为了确保在这一点上你不会产生困惑,我们在这里再对整个机制进行扼要重复:<B normal"><FONT face="Times New Roman">init</FONT></B>函数是内核的一部分;它在内核中作为内核的一个独立的执行部分运行;也就是说,无论从哪个方面看它都是内核代码。但是,<FONT face="Times New Roman">init</FONT>进程就不是这样了。在某些方面,<FONT face="Times New Roman">init</FONT>进程是一个特殊的进程,但是不属于内核本身;其代码存储在磁盘上单独的可执行映像中,这和其它程序一样。因为<B normal"><FONT face="Times New Roman">init</FONT></B>函数后来产生<FONT face="Times New Roman">init</FONT>进程,而它自己又恰好作为进程运行,这样就很容易产生混淆。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>因为<FONT face="Times New Roman">idle</FONT>进程已经占据了进程<FONT face="Times New Roman">ID</FONT>号(<FONT face="Times New Roman">PID</FONT>)<FONT face="Times New Roman">0</FONT>,<B normal"><FONT face="Times New Roman">init</FONT></B>(当然是<FONT face="Times New Roman">init</FONT>)就被赋值为下一个可用的<FONT face="Times New Roman">PID</FONT>,也就是<FONT face="Times New Roman">1</FONT>。(进程<FONT face="Times New Roman">ID</FONT>在第<FONT face="Times New Roman">7</FONT>章中讨论。)内核重复假定<FONT face="Times New Roman">PID</FONT>为<FONT face="Times New Roman">1</FONT>的进程是<FONT face="Times New Roman">init</FONT>,因此这种特性在没有充分地相互作用,也就是没有同步地进行修改的情况下是不能改变的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">20046</FONT>:调用<B normal"><FONT face="Times New Roman">lock_kernel</FONT></B>(<FONT face="Times New Roman">17492</FONT>行对应<FONT face="Times New Roman">UP</FONT>版本;<FONT face="Times New Roman">10174</FONT>行对应<FONT face="Times New Roman">SMP</FONT>版本)执行后续几行,而不会受到其它会受到随后工作的影响的内核模块的干扰。内核锁随后在<FONT face="Times New Roman">20053</FONT>行被释放。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">20047</FONT>:调用<B normal"><FONT face="Times New Roman">do_basic_setup</FONT></B>(<FONT face="Times New Roman">19916</FONT>行)初始化总线并随同其它工作产生一些其它内核线程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">20052</FONT>:内核已完全完成初始化了,因此<B normal"><FONT face="Times New Roman">free_initmen</FONT></B>(<FONT face="Times New Roman">7620</FONT>行)可以舍弃内核的<FONT face="Times New Roman">.text.init</FONT>节的函数和<FONT face="Times New Roman">.data.init</FONT>节的数据。所有使用<B normal"><FONT face="Times New Roman">__initfunc</FONT></B>标记过的函数和使用<B normal"><FONT face="Times New Roman">__initdata</FONT></B>标记过的数据现在都不能使用了,它们曾经获得的内存现在也可能重新用于其它目的了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">20055</FONT>:如果可能,打开控制台设备,这样<FONT face="Times New Roman">init</FONT>进程就拥有一个控制台,可以向其中写入信息,也可以从其中读取输入信息。实际上<FONT face="Times New Roman">init</FONT>进程除了打印错误信息以外,并不使用控制台,但是如果调用的是<FONT face="Times New Roman">shell</FONT>或者其它需要交互的进程,而不是<FONT face="Times New Roman">init</FONT>,那么就需要一个可以交互的输入源。如果成功执行<B normal"><FONT face="Times New Roman">open</FONT></B>,<FONT face="Times New Roman">/dev/console</FONT>就成为<FONT face="Times New Roman">init</FONT>的标准输入源(文件描述符<FONT face="Times New Roman">0</FONT>)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">20059</FONT>:调用<B normal"><FONT face="Times New Roman">dup</FONT></B>打开<FONT face="Times New Roman">/dev/console</FONT>文件描述符两次,这样,<FONT face="Times New Roman">init</FONT>就也使用它供标准输出和标准错误使用(文件描述符<FONT face="Times New Roman">1</FONT>和<FONT face="Times New Roman">2</FONT>)。假设<FONT face="Times New Roman">20055</FONT>行的<B normal"><FONT face="Times New Roman">open</FONT></B>成功执行(正常情况),<FONT face="Times New Roman">init</FONT>现在就有三个文件描述符――标准输入、标准输出以及标准错误――全都加载在系统控制台之上。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">20067</FONT>:如果内核命令行中给出了到<FONT face="Times New Roman">init</FONT>的直接路径(或者别的可替代的程序),现在就试图执行<B normal"><FONT face="Times New Roman">init</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>因为当<B normal"><FONT face="Times New Roman">execve</FONT></B>成功执行目标程序时并不返回,只有当前面的所有处理过程都失败时,才能执行相关的表达式。接下来的几行在几个地方查找<FONT face="Times New Roman">init</FONT>,按照可能性由高到低的顺序依次是:首先是<FONT face="Times New Roman">/sbin/init</FONT>,这是<FONT face="Times New Roman">init</FONT>标准的位置;接下来是两个可能的位置,<FONT face="Times New Roman">/etc/init</FONT>和<FONT face="Times New Roman">/bin/init</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">20072</FONT>:这些是<FONT face="Times New Roman">init</FONT>可能出现的所有地方。如果现在还没有出现,<B normal"><FONT face="Times New Roman">init</FONT></B>就无法找到它的这个同名者了,机器可能就崩溃了。因此,它就会试图建立一个交互的<FONT face="Times New Roman">shell</FONT>(<FONT face="Times New Roman">/bin/sh</FONT>)来代替。现在<B normal"><FONT face="Times New Roman">init</FONT></B>最后的希望就是<FONT face="Times New Roman">root</FONT>用户可以修复这种错误并重新启动机器。(可以肯定,<FONT face="Times New Roman">root</FONT>也正是希望如此。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">20073</FONT>:<B normal"><FONT face="Times New Roman">init</FONT></B>甚至不能创建<FONT face="Times New Roman">shell</FONT>――一定是发生了什么问题!好,按照它们所说的,当所有其它情况都失败时,调用<B normal"><FONT face="Times New Roman">panic</FONT></B>(<FONT face="Times New Roman">25563</FONT>行)。这样内核就会试图同步磁盘,确保其状态一致,然后暂停进程的执行。如果超过了内核选项中定义的时间,它也可能会重新启动机器。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><p><FONT face="Times New Roman"> </FONT></p></P>
<H1 12pt 0cm; TEXT-INDENT: 0cm"><FONT face="Times New Roman"> </FONT>第<FONT face="Times New Roman">5</FONT>章<FONT face="Times New Roman"> </FONT>系统调用</H1>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">大部分介绍<FONT face="Times New Roman">Unix</FONT>内核的书籍都没有仔细说明系统调用,我认为这是一个失误。实际上,我们实际需要的系统调用现在已经十分完美。因此,从某种意义上来说,研究系统调用的实现是无意义的——如果你想为<FONT face="Times New Roman">Linux</FONT>内核的改进贡献自己的力量,还有其它许多方面更值得投入精力。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">然而,对于我们来说,仔细研究少量系统调用是十分值得的。这样就有机会初步了解一些概念,这些概念将随本书发展而进行详细介绍,例如进程处理和内存。这使得你可以趁机详细了解一下<FONT face="Times New Roman">Linux</FONT>内核编程的特点。这包括一些和你过去在学校里(或工作中)所学的内容不同的方法。和其它编程任务相比,<FONT face="Times New Roman">Linux</FONT>内核编程的一个显著特点是它不断同三个成见进行斗争——这三个成见就是速度、正确和清晰——我们不可能同时获取这三个方面<FONT face="Times New Roman">…</FONT>至少并不总是能够。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">什么是系统调用</H2>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">系统调用发生在用户进程(比如<FONT face="Times New Roman">emacs</FONT>)通过调用特殊函数(例如<B normal"><FONT face="Times New Roman">open</FONT></B>)以请求内核提供服务的时候。在这里,用户进程被暂时挂起。内核检验用户请求,尝试执行,并把结果反馈给用户进程,接着用户进程重新启动,随后我们就将详细讨论这种机制。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">系统调用负责保护对内核所管理的资源的访问,系统调用中的几个大类主要有:处理<FONT face="Times New Roman">I/O</FONT>请求(<B normal"><FONT face="Times New Roman">open</FONT></B>,<B normal"><FONT face="Times New Roman">close</FONT></B>,<B normal"><FONT face="Times New Roman">read</FONT></B>,<B normal"><FONT face="Times New Roman">write</FONT></B>,<B normal"><FONT face="Times New Roman">poll</FONT></B>等等),进程(<B normal"><FONT face="Times New Roman">fork</FONT></B>,<B normal"><FONT face="Times New Roman">execve</FONT></B>,<B normal"><FONT face="Times New Roman">kill</FONT></B>,等等),时间(<B normal"><FONT face="Times New Roman">time</FONT></B>,<B normal"><FONT face="Times New Roman">settimeofday</FONT></B>等等)以及内存(<B normal"><FONT face="Times New Roman">mmap</FONT></B>,<B normal"><FONT face="Times New Roman">brk</FONT></B>,等等)的系统调用。几乎所有的系统调用都可以归入这几类中。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">然而,从根本上来说,系统调用可能和它表面上有所不同。首先,在<FONT face="Times New Roman">Linux</FONT>中,<FONT face="Times New Roman">C</FONT>库中对于一些系统调用的实现是建立在其它系统调用的基础之上的。例如,<B normal"><FONT face="Times New Roman">waitpid</FONT></B>是通过简单调用<B normal"><FONT face="Times New Roman">wait4</FONT></B>实现的,但是它们两个都是作为独立的系统调用说明的。其它的传统系统调用,如<B normal"><FONT face="Times New Roman">sigmask</FONT></B>和<B normal"><FONT face="Times New Roman">ftime</FONT></B>是由<FONT face="Times New Roman">C</FONT>库而不是由<FONT face="Times New Roman">Linux</FONT>内核本身实现的;即使不是全部,至少大部分是如此。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">当然,从技巧的一面来看这是无害的——从应用程序的观点来看,系统调用就和其它的函数调用一样。只要结果符合预计的情况,应用程序就不能确定是否真正使用到了内核。(这种处理方式还有一个潜在的优点:用户可以直接触发的内核代码越少,出现安全漏洞的机会也就越少。)但是,由于使用这种技巧所引起的困扰将会使我们的讨论更为困难。实际上,系统调用这一术语通常被演讲者用来说明在第一个<FONT face="Times New Roman">Unix</FONT>版本中的任何对系统的调用。但是在本章中我们只对“真正”的系统调用感兴趣——真正的系统调用至少包括用户进程对部分内核代码的调用。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">系统调用必须返回<B normal"><FONT face="Times New Roman">int</FONT></B>的值,并且也只能返回<B normal"><FONT face="Times New Roman">int</FONT></B>的值。为了方便起见,返回值如果为零或者为正,就说明调用成功;为负则说明发生了错误。就像老练的<FONT face="Times New Roman">C</FONT>程序员所知道的一样,当标准<FONT face="Times New Roman">C</FONT>库中的函数发生错误时会通过设置全局整型变量<B normal"><FONT face="Times New Roman">errno</FONT></B>指明发生错误的属性,系统调用的原理和它相同。然而,仅仅研究内核源程序代码并不能够获得这种系统调用方式的全部意义。如果发生了错误,系统调用简单返回自己所期望的负数错误号,其余部分则由标准<FONT face="Times New Roman">C</FONT>库实现。(正常情况下,用户代码并不直接调用内核系统函数,而是要通过标准<FONT face="Times New Roman">C</FONT>库中专门负责翻译的一个小层次(<FONT face="Times New Roman">thin layer</FONT>)实现。)我们随便举一个例子,<FONT face="Times New Roman">27825</FONT>行(<B normal"><FONT face="Times New Roman">sys_nanosleep</FONT></B>的一部分)返回<B normal"><FONT face="Times New Roman">-EINVAL</FONT></B>指明所提供的值越界了。标准<FONT face="Times New Roman">C</FONT>库中实际处理<B normal"><FONT face="Times New Roman">sys_nanosleep</FONT></B>的代码会注意到返回的负值,从而设置<B normal"><FONT face="Times New Roman">errno</FONT></B>和<B normal"><FONT face="Times New Roman">EINVAL</FONT></B>,并且自己返回<FONT face="Times New Roman">-1</FONT>给原始的调用者。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">在最近的内核版本中,系统调用返回负值偶尔也不一定表示错误了。在目前的几个系统调用中(例如<B normal"><FONT face="Times New Roman">lseek</FONT></B>),即使结果正确也会返回一个很大的负值。最近,错误返回值是在<FONT face="Times New Roman">-1</FONT>到<FONT face="Times New Roman">-4095</FONT>范围之内。现在,标准<FONT face="Times New Roman">C</FONT>库实现能够以更加成熟和高级的方式解释系统调用的返回值;当返回值为负时,内核本身就不用再做任何特殊的处理了。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5>中断、内核空间和用户空间</FONT></H3>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">我们将在第<FONT face="Times New Roman">6</FONT>章中介绍中断和在第<FONT face="Times New Roman">8</FONT>章中介绍内存时再次明确这些概念。但是在本章中,我们只需要粗略地了解一些术语。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">第一个术语是中断(<FONT face="Times New Roman">interrupt</FONT>),它来源于两个方面:硬件中断,例如磁盘指明其中存放一些数据(这与本章无关);和软件中断,一种等价的软件机制。在<FONT face="Times New Roman">x86</FONT>系列<FONT face="Times New Roman">CPU</FONT>中,软件中断是用户进程通知内核需要触发系统调用的基本方法(出于这种目的使用的中断号是<FONT face="Times New Roman">0x80</FONT>,对于<FONT face="Times New Roman">Intel</FONT>芯片的研究者来说更为熟悉的是<FONT face="Times New Roman">INT 80</FONT>)。内核通过<B normal"><FONT face="Times New Roman">system_call</FONT></B>(<FONT face="Times New Roman">171</FONT>行)函数响应中断,这一点我们马上就会介绍。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">另外两个术语是内核空间(<FONT face="Times New Roman">kernel space</FONT>)和用户空间(<FONT face="Times New Roman">user space</FONT>),它们分别对应内核保留的内存和用户进程保留的内存。当然,多用户进程也经常同时运行,而且各个进程之间通常不会共享它们的内存,但是,任何一个用户进程使用的内存都称为用户空间。内核在某一个时刻通常只和一个用户进程交互,因此实际上不会引起任何混乱。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">由于这些内存空间是相互独立的,用户进程根本不能直接访问内核空间,内核也只能通过<B normal"><FONT face="Times New Roman">put_user</FONT></B>(<FONT face="Times New Roman">13278</FONT>行)和<B normal"><FONT face="Times New Roman">get_user</FONT></B>(<FONT face="Times New Roman">13254</FONT>行)宏和类似的宏才可以访问用户空间。因为系统调用是进程和进程所运行的操作系统之间的接口,所以系统调用需要频繁地和用户空间交互,因此这些宏也就会不时的在系统调用中出现。在通过数值传递参数的情况下并不需要它们,但是当用户把指针——内核通过这个指针进行读写——传递给系统调用时,就需要这些宏了。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">如何激活系统调用</H2>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">系统调用的的激活有两种方法:<B normal"><FONT face="Times New Roman">system_call</FONT></B>函数和<B normal"><FONT face="Times New Roman">lcall7</FONT></B>调用门(<FONT face="Times New Roman">call gate</FONT>)(请参看<FONT face="Times New Roman">135</FONT>行)。(你可能听说过还有一种机制,<B normal"><FONT face="Times New Roman">syscall</FONT></B>函数,是通过调用<B normal"><FONT face="Times New Roman">lcall7</FONT></B>实现的——至少在<FONT face="Times New Roman">x86</FONT>平台上是如此——因此,它并不是一个特有的方法。)本节将细致地讨论一下这两种机制。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">在阅读的过程中请注意系统调用本身并不关心它们是由<B normal"><FONT face="Times New Roman">system_call</FONT></B>还是由<B normal"><FONT face="Times New Roman">lcall7</FONT></B>激活的。这种把系统调用和其实现方式区别开来的方法是十分精巧的。这样,如果出于某种原因我们不得不增加一种激活系统调用的方法,我们也不必修改系统调用本身来支持这种方法。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">在你浏览这些汇编代码之前要注意这些机器指令中操作数的顺序和普通<FONT face="Times New Roman">Intel</FONT>的次序相反。虽然还有一些其它的语法区别,但是操作数反序是最令人迷惑的。如果你还记得<FONT face="Times New Roman">Intel</FONT>的语法:</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">mov eax, 0</FONT></P>< 0cm 0cm 0pt; TEXT-INDENT: 0cm">(本句代码的意思是把常数<FONT face="Times New Roman">0</FONT>传送到寄存器<FONT face="Times New Roman">EAX</FONT>中)在这里应该写作:</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">mov1 $0, %eax</FONT></P>< 0cm 0cm 0pt; TEXT-INDENT: 0cm">这样你就能够正确通过。(内核使用的语法是<FONT face="Times New Roman">AT&T</FONT>的汇编语法。在<FONT face="Times New Roman">GNU</FONT>汇编文档中有更多资料。)</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT face="Times New Roman" size=5>system_call</FONT></H3>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><B normal"><FONT face="Times New Roman">system_call</FONT></B>(<FONT face="Times New Roman">171</FONT>行)是所有系统调用的入口点(这是对于内部代码来说的;<B normal"><FONT face="Times New Roman">lcall7</FONT></B>用来支持<FONT face="Times New Roman">iBCS2</FONT>,这一点我们很快就会讨论)。正如前面标题注释中说明的一样,目的是为普通情况简单地实现直接的流程,不采用跳转,因此函数的各个部分都是离散的——整体的流量控制已经因为要避免普通情况下的多分支而变得非常复杂。(分支的避免是十分值得的,因为它们引起的代价非常昂贵。它们可以清空<FONT face="Times New Roman">CPU</FONT>管道,使现存<FONT face="Times New Roman">CPU</FONT>的并行加速机制失效。)</P>< 0cm 0cm 0pt; TEXT-INDENT: 0cm; TEXT-ALIGN: center" align=center><wrapblock><v:shapetype><v:stroke joinstyle="miter"></v:stroke><v:formulas><v:f eqn="if lineDrawn pixelLineWidth 0"></v:f><v:f eqn="sum @0 1 0"></v:f><v:f eqn="sum 0 0 @1"></v:f><v:f eqn="prod @2 1 2"></v:f><v:f eqn="prod @3 21600 pixelWidth"></v:f><v:f eqn="prod @3 21600 pixelHeight"></v:f><v:f eqn="sum @0 0 1"></v:f><v:f eqn="prod @6 1 2"></v:f><v:f eqn="prod @7 21600 pixelWidth"></v:f><v:f eqn="sum @8 21600 0"></v:f><v:f eqn="prod @7 21600 pixelHeight"></v:f><v:f eqn="sum @10 21600 0"></v:f></v:formulas><v:path connecttype="rect" gradientshapeok="t" extrusionok="f"></v:path><lock aspectratio="t" v:ext="edit"></lock></v:shapetype><v:shape><v:imagedata></v:imagedata><w:wrap type="topAndBottom"></w:wrap></v:shape></wrapblock><BR vglayout" clear=all>图<FONT face="Times New Roman">5.1 <B normal">system_call</B></FONT>的流程控制</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><p><FONT face="Times New Roman"> </FONT></p></P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">图<FONT face="Times New Roman">5.1</FONT>显示了作为<B normal"><FONT face="Times New Roman">system_call</FONT></B>的一部分出现的分支目标标签以及它们之间的流程控制方向,该图可以在你阅读本部分讨论内容时提供很大的帮助。图中<B normal"><FONT face="Times New Roman">system_call</FONT></B>和<B normal"><FONT face="Times New Roman">restore_all</FONT></B>两个标签比其它标签都要大,因为这两处是该函数正常的出口点和入口点;然而,还有另外两个入口点,这一点在本章的后续内容中很快就可以看到。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><B normal"><FONT face="Times New Roman">system_call</FONT></B>是由标准<FONT face="Times New Roman">C</FONT>库激活的,该标准<FONT face="Times New Roman">C</FONT>库会把自己希望传递的参数装载到<FONT face="Times New Roman">CPU</FONT>寄存器中,并触发<FONT face="Times New Roman">0x80</FONT>软件中断。(<B normal"><FONT face="Times New Roman">system_call</FONT></B>在这里是一个中断处理程序。)内核记录了软件中断和<FONT face="Times New Roman">6828</FONT>行的<B normal"><FONT face="Times New Roman">system_call</FONT></B>函数的联系(<B normal"><FONT face="Times New Roman">SYSCALL_VECTOR</FONT></B>是在<FONT face="Times New Roman">1713</FONT>行宏定义为<FONT face="Times New Roman">0x80</FONT>的)。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">system_call</H4>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">172</FONT>:<FONT face="Times New Roman"> <B normal">system_call</B></FONT>的第一个参数是所希望激活的系统调用的数目;它存储在<FONT face="Times New Roman">EAX</FONT>寄存器中。<B normal"><FONT face="Times New Roman">system_call</FONT></B>还允许有多达四个的参数和系统调用一起传送。在一些极其罕见的情况下使用四个参数的限制是负担繁重的,通常可以建立一个指向结构的指针参数来巧妙地完成同样功能,指针指向的结构中可以包含你所需要的一切信息。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>随后可能需要<FONT face="Times New Roman">EAX</FONT>值的一个额外拷贝,因此通过将其压栈而保存起来;这个值就是<FONT face="Times New Roman">218</FONT>行的<B normal"><FONT face="Times New Roman">ORIG_EAX</FONT></B>(<FONT face="Times New Roman">%<B normal">esp</B></FONT>)表达式的值。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">173</FONT>:<FONT face="Times New Roman"> <B normal">SAVE_ALL</B></FONT>宏是在<FONT face="Times New Roman">85</FONT>行定义的;它把所有寄存器的值压入<FONT face="Times New Roman">CPU</FONT>的堆栈。随后,就在<B normal"><FONT face="Times New Roman">system_call</FONT></B>返回之前,使用<B normal"><FONT face="Times New Roman">RESTALL_ALL</FONT></B>(<FONT face="Times New Roman">100</FONT>行)把栈中的值弹出。在这中间,<B normal"><FONT face="Times New Roman">system_call</FONT></B>可以根据需要自由使用寄存器的值。更重要的是,任何它所调用的<FONT face="Times New Roman">C</FONT>函数都可以从栈中查找到所希望的参数,因为<B normal"><FONT face="Times New Roman">SAVE_ALL</FONT></B>已经把所有寄存器的值都压入栈中了。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>结果栈的结构从<FONT face="Times New Roman">26</FONT>行开始描述。象<B normal"><FONT face="Times New Roman">0</FONT></B><B normal">(<FONT face="Times New Roman">%esp</FONT></B><B normal">)</B>和<B normal"><FONT face="Times New Roman">4</FONT></B><B normal">(<FONT face="Times New Roman">%esp</FONT></B><B normal">)</B>一样的表达式指明了堆栈指针(<FONT face="Times New Roman">ESP</FONT>寄存器)的一种替换形式——分别表示<FONT face="Times New Roman">ESP</FONT>上的<FONT face="Times New Roman">0</FONT>字节,<FONT face="Times New Roman">ESP</FONT>上的<FONT face="Times New Roman">4</FONT>字节,等等。特别要注意的是在前面一行中压入堆栈的<FONT face="Times New Roman">EAX</FONT>的拷贝已经变成本标题注释作为<B normal"><FONT face="Times New Roman">orig_eax</FONT></B>所描述的内容;它们是由<B normal"><FONT face="Times New Roman">SAVE_ALL</FONT></B>压入寄存器之上的堆栈的(<B normal"><FONT face="Times New Roman">orig_eax</FONT></B>之上的寄存器在这里早已就绪了)。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>还需注意:这可能有点令人迷惑——由于我们调用<B normal"><FONT face="Times New Roman">orig_eax</FONT></B>时<FONT face="Times New Roman">EAX</FONT>的拷贝已经压入了堆栈,它是否有可能在其它寄存器下面而不是在其它寄存器上面呢?答案既是肯定的,也是否定的。<FONT face="Times New Roman">x86</FONT>的堆栈指针寄存器<FONT face="Times New Roman">ESP</FONT>在有数据压入堆栈时会减少——堆栈会向内存低地址发展。因此,<B normal"><FONT face="Times New Roman">orig_eax</FONT></B>逻辑上是在其它值的下面,但是物理上却是在其它值的上面。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>从<FONT face="Times New Roman">51</FONT>行开始的一系列宏有助于使这些替换更容易理解。例如,<B normal"><FONT face="Times New Roman">EAX</FONT></B><B normal">(<FONT face="Times New Roman">%esp</FONT></B><B normal">)</B>就和<B normal"><FONT face="Times New Roman">18</FONT></B><B normal">(<FONT face="Times New Roman">%esp</FONT></B><B normal">)</B>相同——然而前一种方法通过表达式引用存储在堆栈中的<FONT face="Times New Roman">EAX</FONT>寄存器副本的决定可以使整个过程更加简单。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">174</FONT>:<FONT face="Times New Roman"> </FONT>从<FONT face="Times New Roman">EBX</FONT>寄存器中取得指向当前任务的指针。完成这个工作的宏<B normal"><FONT face="Times New Roman">GET_CURRENT</FONT></B>(<FONT face="Times New Roman">131</FONT>行)对于在大部分代码中使用的<FONT face="Times New Roman">C</FONT>函数<B normal"><FONT face="Times New Roman">get_current</FONT></B>(<FONT face="Times New Roman">10277</FONT>行)来说是一个无限循环。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>此后,当看到类似于<B normal"><FONT face="Times New Roman">foo</FONT></B><B normal">(<FONT face="Times New Roman">%ebx</FONT></B><B normal">)</B>或者<B normal"><FONT face="Times New Roman">foo</FONT></B><B normal">(<FONT face="Times New Roman">%esp</FONT></B><B normal">)</B>的表达式时,这意味着这些的代码正在引用代表当前进程的结构的字段——<FONT face="Times New Roman">16325</FONT>行的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>——这在第<FONT face="Times New Roman">7</FONT>章中将对它进行更详细的介绍。(更确切的描述是,<FONT face="Times New Roman"> <B normal">%ebx</B></FONT>的置换在<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>中,<B normal"><FONT face="Times New Roman">%esp</FONT></B>的置换在与<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>相关联的<B normal"><FONT face="Times New Roman">struct pt_regs</FONT></B>结构中。但是这些细节在这里都并不重要。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">175</FONT>:<FONT face="Times New Roman"> </FONT>检查(<FONT face="Times New Roman">EAX</FONT>中的)系统调用的数目是否超过系统调用的最大数量。(此处<FONT face="Times New Roman">EAX</FONT>为一个无符号数,因此不可能为负值。)如果的确超过了,就向前跳转到<B normal"><FONT face="Times New Roman">badsys</FONT></B>(<FONT face="Times New Roman">223</FONT>行)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">177</FONT>:<FONT face="Times New Roman"> </FONT>检测系统调用是否正被跟踪。如<FONT face="Times New Roman">strace</FONT>之类的程序为有兴趣的人提供了系统调用的跟踪工具,或者额外的调试信息:如果能够监测到正在执行的系统调用,那么你就可以了解到当前程序正在处理的内容。如果系统调用正被跟踪,控制流程就向前跳转到<B normal"><FONT face="Times New Roman">tracesys</FONT></B>(<FONT face="Times New Roman">215</FONT>行)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">179</FONT>:<FONT face="Times New Roman"> </FONT>调用系统函数。此处有很多工作需要处理。首先,<B normal"><FONT face="Times New Roman">SYSMOL_NAME</FONT></B>宏不处理任何工作,只是简单的为参数文本所替换,因此可以将其忽略。<B normal"><FONT face="Times New Roman">sys_call_table</FONT></B>是在当前文件(<FONT face="Times New Roman">arch/i386/kernel/entry.S</FONT>)的末尾从<FONT face="Times New Roman">373</FONT>行开始定义的。这是一张由指向实现各种系统调用的内核函数的函数指针组成的表。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>本行中第二对圆括号中包含了三个使用逗号分割开的参数(第一个参数为空);这里就是实现数组索引的地方。当然,这个数组是以<B normal"><FONT face="Times New Roman">sys_call_table</FONT></B>作为索引的,这称为偏移(<FONT face="Times New Roman">displacement</FONT>)。这三个参数是数组的基地址、索引(<FONT face="Times New Roman">EAX</FONT>,系统调用的数目)和大小,或者每个数组元素中的字节数——在这里就是<FONT face="Times New Roman">4</FONT>。由于数组基地址为空,就将其当作<FONT face="Times New Roman">0</FONT>——但是它要和偏移地址,<B normal"><FONT face="Times New Roman">sys_call_table</FONT></B><B normal">,</B>相加,简单的说就是<B normal"><FONT face="Times New Roman">sys_call_table</FONT></B>被当作数组的基地址。本行基本上等同于如下的<FONT face="Times New Roman">C</FONT>表达式:</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> /* Call a function in an array of functions. */</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> (sys_call_table[eax])();</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>然而,<FONT face="Times New Roman"> C</FONT>当然还要处理许多繁重的工作,例如为你记录数组元素的大小。不要忘记,系统调用的参数早已经存储在堆栈中了,这主要由调用者提供给<B normal"><FONT face="Times New Roman">system_call</FONT></B>并使用<B normal"><FONT face="Times New Roman">SAVE_ALL</FONT></B>把它们压栈。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">180</FONT>:<FONT face="Times New Roman"> </FONT>系统调用已经返回。它在<FONT face="Times New Roman">EAX</FONT>寄存器中的返回值(这个值同时也是<B normal"><FONT face="Times New Roman">system_call</FONT></B>的返回值)被存储起来。返回值被存储在堆栈中的<FONT face="Times New Roman">EAX</FONT>内,以使得<B normal"><FONT face="Times New Roman">RESTORE_ALL</FONT></B>可以迅速地恢复实际的<FONT face="Times New Roman">EAX</FONT>寄存器以及其它寄存器的值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">182</FONT>:<FONT face="Times New Roman"> </FONT>接下来的代码仍然是<B normal"><FONT face="Times New Roman">system_call</FONT></B>的一部分,它是一个也可以命名为<B normal"><FONT face="Times New Roman">ret_from_sys_call</FONT></B>和<B normal"><FONT face="Times New Roman">ret_from_intr</FONT></B>的独立入口点。它们偶尔会被<FONT face="Times New Roman">C</FONT>直接调用,也可以从<B normal"><FONT face="Times New Roman">system_call</FONT></B>的其它部分跳转过来。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">185</FONT>:<FONT face="Times New Roman"> </FONT>接下来的几行检测“下半部分(<FONT face="Times New Roman">bottom half</FONT>)”是否激活;如果激活了,就跳转到<B normal"><FONT face="Times New Roman">handle_bottom_half</FONT></B>标号(<FONT face="Times New Roman">242</FONT>行)并立即开始处理。下半部分是中断进程的一部分,将在下一章中讨论。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">189</FONT>:<FONT face="Times New Roman"> </FONT>检查该进程是否为再次调度做了标记(记住表达式<B normal"><FONT face="Times New Roman">$0</FONT></B>就是常量<FONT face="Times New Roman">0</FONT>的系统简单表示)。如果的确如此,就跳转到<B normal"><FONT face="Times New Roman">reschedule</FONT></B>标号(<FONT face="Times New Roman">247</FONT>行)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">191</FONT>:<FONT face="Times New Roman"> </FONT>检测是否还有挂起的信号量,如果有的话,下一行就向前跳转到<B normal"><FONT face="Times New Roman">signal_return</FONT></B>(<FONT face="Times New Roman">197</FONT>行)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">193</FONT>:<FONT face="Times New Roman"> <B normal">restore_all</B></FONT>标号是<B normal"><FONT face="Times New Roman">system_call</FONT></B>的退出点。其主体就是简单的<B normal"><FONT face="Times New Roman">RESTORE_ALL</FONT></B>宏(<FONT face="Times New Roman">100</FONT>行),该宏将恢复早先由<B normal"><FONT face="Times New Roman">SAVE_ALL</FONT></B>存储的参数并返回给<B normal"><FONT face="Times New Roman">system_call</FONT></B>的调用者。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">197</FONT>:<FONT face="Times New Roman"> </FONT>当<B normal"><FONT face="Times New Roman">system_call</FONT></B>从系统调用返回前,如果它检测到需要将信号量传送给当前的进程时,才会执行到<B normal"><FONT face="Times New Roman">signal_return</FONT></B>。它通过使中断再次可用开始执行,有关内容将在第<FONT face="Times New Roman">6</FONT>章中介绍。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">199</FONT>:<FONT face="Times New Roman"> </FONT>如果返回虚拟<FONT face="Times New Roman">8086</FONT>模式(这不是本书的主题),就向前跳转到<B normal"><FONT face="Times New Roman">v86_signal_return</FONT></B>(<FONT face="Times New Roman">207</FONT>行)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">202</FONT>:<FONT face="Times New Roman"> <B normal">system_call</B></FONT>要调用<FONT face="Times New Roman">C</FONT>函数<B normal"><FONT face="Times New Roman">do_signal</FONT></B>(<FONT face="Times New Roman">3364</FONT>行,在第<FONT face="Times New Roman">6</FONT>章中讨论)来释放信号量。<B normal"><FONT face="Times New Roman">do_signal</FONT></B>需要两个参数,这两个参数都是通过寄存器传递的;第一个是<FONT face="Times New Roman">EAX</FONT>寄存器,另一个是<FONT face="Times New Roman">EDX</FONT>寄存器。<B normal"><FONT face="Times New Roman">system_call</FONT></B>(在<FONT face="Times New Roman">200</FONT>行)早已把第一个参数的值赋给了<FONT face="Times New Roman">EAX</FONT>;现在,就把<FONT face="Times New Roman">EDX</FONT>寄存器和寄存器本身进行<FONT face="Times New Roman">XOR</FONT>操作,从而将其清<FONT face="Times New Roman">0</FONT>,这样<B normal"><FONT face="Times New Roman">do_signal</FONT></B>就认为这是一个空指针。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">203</FONT>:<FONT face="Times New Roman"> </FONT>调用<B normal"><FONT face="Times New Roman">do_signal</FONT></B>传递信号量,并且跳回到<B normal"><FONT face="Times New Roman">restore_all</FONT></B>(<FONT face="Times New Roman">193</FONT>行)结束。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">207</FONT>:<FONT face="Times New Roman"> </FONT>由于虚拟<FONT face="Times New Roman">8086</FONT>模式不是本书的主题,我们将忽略大部分<B normal"><FONT face="Times New Roman">v86_signal_return</FONT></B>。然而,它和<B normal"><FONT face="Times New Roman">signal_return</FONT></B>的情况非常类似。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">215</FONT>:<FONT face="Times New Roman"> </FONT>如果当前进程的系统调用正由其祖先跟踪,就像<FONT face="Times New Roman">strace</FONT>程序中那样,那么就可以执行到<B normal"><FONT face="Times New Roman">tracesys</FONT></B>标号。这一部分的基本思想如同<FONT face="Times New Roman">179</FONT>行一样是通过<B normal"><FONT face="Times New Roman">syscall_table</FONT></B>调用系统函数,但是这里把该调用和对<B normal"><FONT face="Times New Roman">syscall_trace</FONT></B>函数的调用捆绑在一起。后面的这个函数在本书中并没有涉及到,它能够中止当前进程并通知其祖先注意当前进程将要激活一个系统调用。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> EAX</FONT>操作和这些代码的交错使用最初可能容易令人产生困惑。<B normal"><FONT face="Times New Roman">system_call</FONT></B>把存储在堆栈中的<FONT face="Times New Roman">EAX</FONT>拷贝赋给<B normal"><FONT face="Times New Roman">-ENOSYS</FONT></B>,调用<B normal"><FONT face="Times New Roman">syscall_trace</FONT></B>,在<FONT face="Times New Roman">172</FONT>行再从所做的备份中恢复<FONT face="Times New Roman">EAX</FONT>的值,调用实际的系统调用,把系统调用的返回值置入堆栈中<FONT face="Times New Roman">EAX</FONT>的位置,再次调用<B normal"><FONT face="Times New Roman">syscall_trace</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>这种方式背后的原因是<B normal"><FONT face="Times New Roman">syscall_trace</FONT></B>(或者更准确的说是它所要使用到的跟踪程序)需要知道在它是在实际系统调用之前还是之后被调用的。<B normal"><FONT face="Times New Roman">-ENOSYS</FONT></B>的值能够用来指示它是在实际系统调用执行之前被调用的,因为实际中所有实现的系统调用的执行都不会返回<B normal"><FONT face="Times New Roman">-ENOSYS</FONT></B>。因此,<FONT face="Times New Roman">EAX</FONT>在堆栈中的备份在第一次调用<B normal"><FONT face="Times New Roman">syscall_trace</FONT></B>之前是<B normal"><FONT face="Times New Roman">-ENOSYS</FONT></B>,但是在第二次调用<B normal"><FONT face="Times New Roman">syscall_trace</FONT></B>之前就不再是了(除非是调用<B normal"><FONT face="Times New Roman">sys_ni_syscall</FONT></B>的时候,在这种情况下,我们并不关心是怎样跟踪的)。<FONT face="Times New Roman">218</FONT>行和<FONT face="Times New Roman">219</FONT>行中<FONT face="Times New Roman">EAX</FONT>的作用只是找出要调用的系统调用,这和无须跟踪的情况是一致的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">222</FONT>:<FONT face="Times New Roman"> </FONT>被跟踪的系统调用已经返回;流程控制跳转回<B normal"><FONT face="Times New Roman">ret_from_sys_call</FONT></B>(<FONT face="Times New Roman">184</FONT>行)并以与普通的无须跟踪的情况相同的方式结束。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">223</FONT>:<FONT face="Times New Roman"> </FONT>当系统调用的数目越界时,就可以执行到<B normal"><FONT face="Times New Roman">badsys</FONT></B>标号。在这种情况下,<B normal"><FONT face="Times New Roman">system_call</FONT></B>必须返回<B normal"><FONT face="Times New Roman">-ENOSYS</FONT></B>(<B normal"><FONT face="Times New Roman">ENOSYS</FONT></B>在<FONT face="Times New Roman">82</FONT>行将它赋值为<FONT face="Times New Roman">38</FONT>)。正如前面提到的一样,调用者会识别出这是一个错误,因为返回值在<FONT face="Times New Roman">-1</FONT>到<FONT face="Times New Roman">-4,095</FONT>之间。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">228</FONT>:<FONT face="Times New Roman"> </FONT>在诸如除零错误(请参看<FONT face="Times New Roman">279</FONT>行)之类的<FONT face="Times New Roman">CPU</FONT>异常中断情况下将执行到<B normal"><FONT face="Times New Roman">ret_from_exception</FONT></B>标号;但是<B normal"><FONT face="Times New Roman">system_call</FONT></B>内部的所有代码都不会执行到这个标号。如果有下半部分是激活的,现在就是它在起作用了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">233</FONT>:<FONT face="Times New Roman"> </FONT>处理完下半部分之后或者从上面的情况简单的执行下来(虽然没有下半部分是激活的,但是同样也触发了<FONT face="Times New Roman">CPU</FONT>异常),就执行到了<B normal"><FONT face="Times New Roman">ret_from_intr</FONT></B>标号。这是一个全局符号变量,因此可能在内核的其它部分也会有对它的调用。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">237</FONT>:<FONT face="Times New Roman"> </FONT>被保存的<FONT face="Times New Roman">CPU</FONT>的<FONT face="Times New Roman">EFLAGS</FONT>和<FONT face="Times New Roman">CS</FONT>寄存器在此已经被并入<FONT face="Times New Roman">EAX</FONT>,因而高<FONT face="Times New Roman">24</FONT>位的值(其中恰好包含了一位在<FONT face="Times New Roman">70</FONT>行定义的非常有用的<B normal"><FONT face="Times New Roman">VM_MASK</FONT></B>)来源于<FONT face="Times New Roman">EFLAGS</FONT>,其它低<FONT face="Times New Roman">8</FONT>位的值来源于<FONT face="Times New Roman">CS</FONT>。该行隐式的同时对这两部分进行测试以判断进程到底返回虚拟<FONT face="Times New Roman">8086</FONT>模式(这是<B normal"><FONT face="Times New Roman">VM_MASK</FONT></B>的部分)还是用户模式(这是<FONT face="Times New Roman">3</FONT>的部分——用户模式的优先等级是<FONT face="Times New Roman">3</FONT>)。下面是近似的等价<FONT face="Times New Roman">C</FONT>代码:</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> /* Mix eflags and cs in eax. */</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> eax = eflags & ~0xff;</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> eax |= cs & ~0xff</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> /* Simultaneously test lower 2 bits </FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> * and VM_MASK bit. */</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> if (eax & (VM_MASK | 3))</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> goto ret_with_reschedule;</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> goto restore_all; </FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">238</FONT>:<FONT face="Times New Roman"> </FONT>如果这些条件中有一个能得到满足,流程控制就跳转到<B normal"><FONT face="Times New Roman">ret_with_reschedule</FONT></B>(<FONT face="Times New Roman">188</FONT>行)标号来测试在<B normal"><FONT face="Times New Roman">system_call</FONT></B>返回之前进程是否需要再次调度。否则,调用者就是一个内核任务,因此<B normal"><FONT face="Times New Roman">system_call</FONT></B>通过跳转到<FONT face="Times New Roman"><B normal">restore_all </B>(193</FONT>行<FONT face="Times New Roman">)</FONT>来跳过重新调度的内容。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">242</FONT>:<FONT face="Times New Roman"> </FONT>无论何时<B normal"><FONT face="Times New Roman">system_call</FONT></B>使用一个下半部分服务时都可以执行到<B normal"><FONT face="Times New Roman">handle_bottom_half</FONT></B>标号。它简单的调用第<FONT face="Times New Roman">6</FONT>章中介绍的<FONT face="Times New Roman">C</FONT>函数<B normal"><FONT face="Times New Roman">bottom_half</FONT></B>(<FONT face="Times New Roman">29126</FONT>行),然后跳回到<B normal"><FONT face="Times New Roman">ret_from_intrr</FONT></B>(<FONT face="Times New Roman">233</FONT>行)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">248</FONT>:<FONT face="Times New Roman"> <B normal">system_call</B></FONT>的最后一个部分在<B normal"><FONT face="Times New Roman">reschedule</FONT></B>标号之下。当产生系统调用的进程已经被标记为需要进行重新调度时,就可以执行到这个标号;典型地,这是因为进程的时间片已经用完了——也就是说,进程到目前为止已经尽可能的拥有<FONT face="Times New Roman">CPU</FONT>了,应该给其它进程一个机会来运行了。因此,在必要的情况下就可以调用<FONT face="Times New Roman">C</FONT>函数<B normal"><FONT face="Times New Roman">schedule</FONT></B>(<FONT face="Times New Roman">26686</FONT>行)交出<FONT face="Times New Roman">CPU</FONT>,同时流程控制转回<FONT face="Times New Roman">249</FONT>行。<FONT face="Times New Roman">CPU</FONT>调度是第<FONT face="Times New Roman">7</FONT>章中讨论的一个主题。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT face="Times New Roman" size=5>lcall7</FONT></H3><P 0cm 0cm 0pt"><FONT face="Times New Roman">Linux</FONT>支持<FONT face="Times New Roman">Intel</FONT>二进制兼容规范标准的版本<FONT face="Times New Roman">2</FONT>(<FONT face="Times New Roman">iBCS2</FONT>)。(<FONT face="Times New Roman">iBCS2</FONT>中的小写字母<FONT face="Times New Roman">i</FONT>显然是有意的,但是该标准却没有对此进行解释;这样看来似乎和现实的<FONT face="Times New Roman">Intel</FONT>系列的<FONT face="Times New Roman">CPU</FONT>例如<FONT face="Times New Roman">i386</FONT>,<FONT face="Times New Roman">i486</FONT>等等是一致的。)<FONT face="Times New Roman">iBCS2</FONT>的规范中规定了所有基于<FONT face="Times New Roman">x86</FONT>的<FONT face="Times New Roman">Unix</FONT>系统的应用程序的标准内核接口,这些系统不仅包括<FONT face="Times New Roman">Linux</FONT>,而且还包括其它自由的<FONT face="Times New Roman">x86 Unix</FONT>(例如<FONT face="Times New Roman">FreeBSD</FONT>),也还包括<FONT face="Times New Roman">Solaris/x86</FONT>,<FONT face="Times New Roman">SCO Unix</FONT>等等。这些标准接口使得为其它<FONT face="Times New Roman">Unix</FONT>系统开发的二进制商业软件在<FONT face="Times New Roman">Linux</FONT>系统中能够直接运行,反之亦然(而且,近期新开发软件向其它<FONT face="Times New Roman">Unix</FONT>移植的情况越来越多)。例如,<FONT face="Times New Roman">Corel</FONT>公司的<FONT face="Times New Roman">WordPerfect</FONT>的<FONT face="Times New Roman">SCO Unix</FONT>的二进制代码在还没有<FONT face="Times New Roman">Linux</FONT>的本地版本的<FONT face="Times New Roman">WordPerfect</FONT>之前就可以使用<FONT face="Times New Roman">iBCS2</FONT>在<FONT face="Times New Roman">Linux</FONT>上良好地运行。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">iBCS2</FONT>标准有很多组成部分,但是我们现在关心的是这些系统调用如何协调一致来适应这些迥然不同的<FONT face="Times New Roman">Unix</FONT>系统。这是通过<B normal"><FONT face="Times New Roman">lcall7</FONT></B>调用门实现的。它是一个相当简单的汇编函数(尤其是和<B normal"><FONT face="Times New Roman">system_call</FONT></B>相比而言更是如此),仅仅定位并全权委托一个<FONT face="Times New Roman">C</FONT>函数来处理细节。(调用门是<FONT face="Times New Roman">x86 CPU</FONT>的一种特性,通过这种特性用户任务可以在安全受控的模式下调用内核代码。)这种调用门在<FONT face="Times New Roman">6802</FONT>行进行设定。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">lcall7</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">136</FONT>:<FONT face="Times New Roman"> </FONT>前面的几行将通过调整处理器堆栈以使堆栈的内容和<B normal"><FONT face="Times New Roman">system_call</FONT></B>预期的相同——<B normal"><FONT face="Times New Roman">system_call</FONT></B>中的一些代码将会完成清理工作,这样所有的内容都可以连续存放了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">145</FONT>:<FONT face="Times New Roman"> </FONT>基于同样的思想,<B normal"><FONT face="Times New Roman">lcall7</FONT></B>把指向当前任务的指针置入<FONT face="Times New Roman">EBX</FONT>寄存器,这一点和<B normal"><FONT face="Times New Roman">system_call</FONT></B>的情况是相同的。但是,它的执行方式却与<B normal"><FONT face="Times New Roman">system_call</FONT></B>不同,这就比较奇怪了。这三行可以等价地按如下形式书写:</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> push1 %esp</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> GET_CURRENT</FONT>(<FONT face="Times New Roman">%ebx</FONT>)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>这种实现的执行速度并不比原有的更快;在将宏展开以后,实际上这还是同样的三条指令以不同的次序组合在一起而已。这样做的优点是可以和文件中的其它代码更为一致,而且代码也许会更清晰一些。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">148</FONT>:<FONT face="Times New Roman"> </FONT>取得指向当前任务<B normal"><FONT face="Times New Roman">exec_domain</FONT></B>域的指针,使用这个域以获取指向其<B normal"><FONT face="Times New Roman">lcall7</FONT></B>处理程序的指针,接着调用这个处理程序。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>本书中并没有对执行域(<FONT face="Times New Roman">execution domains</FONT>)进行详细说明——但是简单说来,内核使用执行域实现了部分<FONT face="Times New Roman">iBCS2</FONT>标准。在<FONT face="Times New Roman">15977</FONT>行你可以找到<B normal"><FONT face="Times New Roman">struct exec_domain</FONT></B>结构。<B normal"><FONT face="Times New Roman">default_exec_domain</FONT></B>(<FONT face="Times New Roman">22807</FONT>行)是缺省的执行域,它拥有一个缺省的<B normal"><FONT face="Times New Roman">lcall7</FONT></B>处理程序。它就是<B normal"><FONT face="Times New Roman">no_lcall7</FONT></B>(<FONT face="Times New Roman">22820</FONT>行)。其基本的执行方式类似于<FONT face="Times New Roman">SVR4</FONT>风格的<FONT face="Times New Roman">Unix</FONT>,如果调用进程没有成功,就传送一个分段违例信号量(<FONT face="Times New Roman">segmentation violation signal</FONT>)给调用的进程,。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">152</FONT>:<FONT face="Times New Roman"> </FONT>跳转到<B normal"><FONT face="Times New Roman">ret_from_sys_call</FONT></B>标号(<FONT face="Times New Roman">184</FONT>行——注意这是在<B normal"><FONT face="Times New Roman">system_call</FONT></B>内部的)清除并返回,就像是正常的系统调用一样。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">系统调用样例</H2><P 0cm 0cm 0pt">现在你已经知道了系统调用是如何激活的,接下来我们将通过几个系统调用例子的剖析来了解一下它们的工作方式。注意系统调用<B normal"><FONT face="Times New Roman">foo</FONT></B>几乎都是使用名为<B normal"><FONT face="Times New Roman">sys_foo</FONT></B>的内核函数实现的,但是在某些情况下该函数也会使用一个名为<B normal"><FONT face="Times New Roman">do_foo</FONT></B>的辅助函数。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sys_ni_syscall</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29185</FONT>:<B normal"><FONT face="Times New Roman">sys_ni_syscall</FONT></B>的确是最简单的系统调用;它只是简单的返回<B normal"><FONT face="Times New Roman">ENOSYS</FONT></B>错误。最初的时候这可能显得没有什么作用,但是它的确是有用的。实际上,<B normal"><FONT face="Times New Roman">sys_ni_syscall</FONT></B>在<B normal"><FONT face="Times New Roman">sys_call_table</FONT></B>中占据了很多位置——而且其原因并不只有一个。开始的时候,<B normal"><FONT face="Times New Roman">sys_ni_syscall</FONT></B>在位置<FONT face="Times New Roman">0</FONT>(<FONT face="Times New Roman">374</FONT>行),因为如果漏洞百出的代码错误地调用了<B normal"><FONT face="Times New Roman">system_call</FONT></B>——例如,没有初始化作为参数传递给<B normal"><FONT face="Times New Roman">system_call</FONT></B>的变量——在这种偶然的变量定义中,<FONT face="Times New Roman">0</FONT>是最可能的值。如果我们能够避免这种情况,那么在错误发生时就不用采取象杀掉进程一样的剧烈措施。(当然,只要允许有用工作的进行,就不可能防止所有的错误。)这种使用表的元素<FONT face="Times New Roman">0</FONT>作为抵御错误的手段在内核中被作为良好的经验而广泛使用。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>而且,你还会发现<B normal"><FONT face="Times New Roman">sys_ni_syscall</FONT></B>在表中明显出现的地方就多达十几处。这些条目代表了那些已经从内核中移出的系统调用——例如在<FONT face="Times New Roman">418</FONT>行,就代替了已经废弃了的<B normal"><FONT face="Times New Roman">prof</FONT></B>系统调用。我们不能简单地把另外的实际系统调用放在这里,因为老的二进制代码可能还会使用到这些已经废弃了的系统调用号。如果一个程序试图调用这些老的系统调用,但是结果却与预期的完全不同,例如打开了一个文件,这会比较令人感到奇怪的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>最后,<B normal"><FONT face="Times New Roman">sys_ni_syscall</FONT></B>将占据表尾部所有未用的空间;这一点是在从<FONT face="Times New Roman">572</FONT>行到<FONT face="Times New Roman">574</FONT>行的代码实现的,它根据需要重复使用这些项来填充表。由于<B normal"><FONT face="Times New Roman">sys_ni_syscall</FONT></B>只是简单返回<B normal"><FONT face="Times New Roman">ENOSYS</FONT></B>错误号,对它的调用和跳转到<B normal"><FONT face="Times New Roman">system_call</FONT></B>中的<B normal"><FONT face="Times New Roman">badsys</FONT></B>标号作用是相同的——也就是说,使用指向这些表项的系统调用号和在表外对整个表进行全部索引具有相同的作用。因此,我们不用改变<B normal"><FONT face="Times New Roman">NR_syscalls</FONT></B>就可以在表中增加(或者删除)系统调用,但是其效果与我们真的对<B normal"><FONT face="Times New Roman">NR_syscalls</FONT></B>进行了修改一样(不管怎样,这都是由<B normal"><FONT face="Times New Roman">NR_syscalls</FONT></B>所建立的限制条件所决定的)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>到现在也许你已经猜到了,<B normal"><FONT face="Times New Roman">sys_ni_syscall</FONT></B>中的“<FONT face="Times New Roman">ni</FONT>”并不是指<FONT face="Times New Roman">Monty Python</FONT>的“说<FONT face="Times New Roman"> </FONT>‘<FONT face="Times New Roman">Ni</FONT>’<FONT face="Times New Roman"> </FONT>的骑士”;而是指“<FONT face="Times New Roman">not implemented</FONT>(没有实现)”这一相较而言并不太诙谐的短语。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>对于这个简单的函数我们需要研究的另外一个问题是<B normal"><FONT face="Times New Roman">asmlinkage</FONT></B>标签。这是为一些<FONT face="Times New Roman">gcc</FONT>功能定义的一个宏,它告诉编译器该函数不希望从寄存器中(这是一种普通的优化<FONT face="Times New Roman"> </FONT>)取得任何参数,而希望仅仅从<FONT face="Times New Roman">CPU</FONT>堆栈中取得参数。回忆一下我们前面提到过<B normal"><FONT face="Times New Roman">system_call</FONT></B>使用第一个参数作为系统调用的数目,同时还允许另外四个参数和系统调用一起传递。<B normal"><FONT face="Times New Roman">system_call</FONT></B>通过把其它参数(这些参数是通过寄存器传递过来的)滞留在堆栈中的方法简单的实现了这种技巧。所有的系统调用都使用<B normal"><FONT face="Times New Roman">asmlinkage</FONT></B>标签作了标记,因此它们都要查找堆栈以获得参数。当然,在<B normal"><FONT face="Times New Roman">sys_ni_syscall</FONT></B>的情况下这并没有任何区别,因为<B normal"><FONT face="Times New Roman">sys_ni_syscall</FONT></B>并不需要任何参数。但是对于其它大部分系统调用来说这就是个问题了。并且,由于在其它很多函数前面都有<B normal"><FONT face="Times New Roman">asmlinkage</FONT></B>标签,我想你也应该对它有些了解。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sys_time</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31394</FONT>:<B normal"><FONT face="Times New Roman">sys_time</FONT></B>是包含几个重要概念的简单系统调用。它实现了系统调用<B normal"><FONT face="Times New Roman">time</FONT></B>,返回值是从某个特定的时间点(<st1:chsdate w:st="on" IsROCDate="False" IsLunarDate="False" Day="1" Month="1" Year="1970"><FONT face="Times New Roman">1970</FONT>年<FONT face="Times New Roman">1</FONT>月<FONT face="Times New Roman">1</FONT>日</st1:chsdate>午夜<FONT face="Times New Roman">UTC</FONT>)以来经过的秒数。这个数字被作为全局变量<B normal"><FONT face="Times New Roman">xtime</FONT></B>(请参看<FONT face="Times New Roman">26095</FONT>行;它被声明为<B normal"><FONT face="Times New Roman">volatile</FONT></B>型的变量,因为它可以通过中断加以修改,这一点我们在第<FONT face="Times New Roman">6</FONT>章中就会看到)的一部分,通过<B normal"><FONT face="Times New Roman">CURRENT_TIME</FONT></B>宏(请参看<FONT face="Times New Roman">16598</FONT>行)可以访问它。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31400</FONT>:该函数非常直接的实现了它的简单定义。当前时间首先被存储在局部变量<B normal"><FONT face="Times New Roman">i</FONT></B>中。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31402</FONT>:如果所提供的指针<B normal"><FONT face="Times New Roman">tloc</FONT></B>是非空的,返回值也将被拷贝到指针指向的位置。该函数的一个微妙之处就在于此;它把<FONT face="Times New Roman">i</FONT>拷贝到用户空间中而不是使用<B normal"><FONT face="Times New Roman">CURRENT_TIME</FONT></B>宏来重新对其进行计算,这基于两个原因:</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 57.25pt">l <B normal"><FONT face="Times New Roman">CURRENT_TIME</FONT></B>宏的定义以后可能会改变,新的实现方法可能会由于某种原因而速度比较慢,但是对于<FONT face="Times New Roman">i</FONT>的访问至少应该和<B normal"><FONT face="Times New Roman">CURRENT_TIME</FONT></B>宏展开的速度同样快。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo3; tab-stops: list 57.25pt">l 使用这种方式处理,确保结果的一致性:如果代码刚好执行到<FONT face="Times New Roman">31400</FONT>行和<FONT face="Times New Roman">31402</FONT>行之间时时间发生了改变,<B normal"><FONT face="Times New Roman">sys_time</FONT></B>可能把一个值拷贝到<B normal"><FONT face="Times New Roman">*tloc</FONT></B>中,但是在结束之后却返回另一个值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>另外还有一个小的方面需要注意,此处的代码不使用<B normal"><FONT face="Times New Roman">&&</FONT></B>来编写而是使用两个<B normal"><FONT face="Times New Roman">if</FONT></B>,这可能有一点令人奇怪。内核中采用这些看起来非常特殊的代码的一般原因都是由于速度的要求,但是<FONT face="Times New Roman">gcc</FONT>为<B normal"><FONT face="Times New Roman">&&</FONT></B>版本和两个<B normal"><FONT face="Times New Roman">if</FONT></B>版本的代码生成的代码是等同的,因此这里的原因就不可能是速度的要求——除非这些代码是在早期<FONT face="Times New Roman">gcc</FONT>版本下开发的,这样才有些意义。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31403</FONT>:如果<B normal"><FONT face="Times New Roman">sys_time</FONT></B>不能访问所提供的位置(一般都是因为<B normal"><FONT face="Times New Roman">tloc</FONT></B>无效),它就把<B normal"><FONT face="Times New Roman">-EFAULT</FONT></B>的值赋给<B normal"><FONT face="Times New Roman">i</FONT></B>,从而在<FONT face="Times New Roman">31405</FONT>行返回错误代码。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31405</FONT>:为调用者返回的<B normal"><FONT face="Times New Roman">i</FONT></B>或者是当前时间,或者是<B normal"><FONT face="Times New Roman">-EFAULT</FONT></B>。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sys_reboot</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29298</FONT>:内核中其他地方可能都没有<B normal"><FONT face="Times New Roman">sys_reboot</FONT></B>的实现方法这样先进。其原因是可以理解为:根据调用的名字我们就可以知道,<B normal"><FONT face="Times New Roman">reboot</FONT></B>系统调用可以用来重新启动机器。根据所提供的参数,它还能够挂起机器,关闭电源,允许或者禁止使用<FONT face="Times New Roman">Ctrl+Alt_Del</FONT>组合键来重启机器。如果你要使用这个函数编写代码,需要特别注意它上面的注释标题的警告:首先同步磁盘,否则磁盘缓冲区中的数据可能会丢失。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>由于它可能为系统引发的潜在后果,<B normal"><FONT face="Times New Roman">sys_reboot</FONT></B>需要几个特殊参数,这一点马上就会讨论。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29305</FONT>:如果调用者不具有<B normal"><FONT face="Times New Roman">CAP_SYS_BOOT</FONT></B>(<FONT face="Times New Roman">14096</FONT>行)权能(<FONT face="Times New Roman">capability</FONT>),系统就会返回<B normal"><FONT face="Times New Roman">EPERM</FONT></B>错误。权能在第<FONT face="Times New Roman">7</FONT>章中会详细讨论。现在,简单的说就是:权能是检测用户是否具有特定权限的方法。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29309</FONT>:在这里,这种偏执的思想充分发挥了作用。<B normal"><FONT face="Times New Roman">syst_reboot</FONT></B>根据从<FONT face="Times New Roman">16002</FONT>到<FONT face="Times New Roman">16005</FONT>行定义的特殊数字检测参数<B normal"><FONT face="Times New Roman">magic1</FONT></B>和<B normal"><FONT face="Times New Roman">magic2</FONT></B>。这种思想是如果<B normal"><FONT face="Times New Roman">sys_reboot</FONT></B>在某种程度上是被偶然调用的,那么就不太可能再从由<B normal"><FONT face="Times New Roman">magic1</FONT></B>和<B normal"><FONT face="Times New Roman">magic2</FONT></B>组成的小集合中同时提取值。注意这并不意味着这是一个防止粗心的安全措施。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>顺便说一下,这些特殊数字并不是随机选取的。第一个参数的关系是十分明显的,它是“感受死亡(<FONT face="Times New Roman">feel dead</FONT>)”的双关语。后面的三个参数要用十六进制才能了解它们全部的意思:它们分别是<FONT face="Times New Roman">0x28121969</FONT>,<FONT face="Times New Roman">0x5121996</FONT>,<FONT face="Times New Roman">0x16041998</FONT>。这似乎代表<FONT face="Times New Roman">Linus</FONT>的妻子(或者就是<FONT face="Times New Roman">Linus</FONT>自己)和他两个女儿的生日。由此推论,当<FONT face="Times New Roman">Linus</FONT>和他的妻子养育了更多儿女的时候,重启动需要的特殊参数可能在某种程度上会增加。不过我想在他们用尽<FONT face="Times New Roman">32</FONT>位可能空间之前,他的妻子就会制止他的行为了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29315</FONT>:请求内核锁,这样能保证这段代码在某一时间只能由一个处理器执行。使用<FONT face="Times New Roman"><B normal">lock_kernel</B>/<B normal">unlock_kernel</B></FONT>函数对所保护起来的任何其它代码对其它<FONT face="Times New Roman">CPU</FONT>都同样是不可访问的。在单处理器的机器中,这只是一个<FONT face="Times New Roman">no-op</FONT>(不处理任何事情);而详细讨论它在多处理器上的作用则是第<FONT face="Times New Roman">10</FONT>章的内容。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29317</FONT>:在<B normal"><FONT face="Times New Roman">LINUX_REBOOT_CMD_RESTART</FONT></B>的情况中,<B normal"><FONT face="Times New Roman">sys_reboot</FONT></B>调用一系列基于<B normal"><FONT face="Times New Roman">reboot_notifier_list</FONT></B>的函数来通知它们系统正在重新启动。正常情况下,这些函数都是操作系统关闭时需要清除的模块的一部分。这个列表函数似乎并不在内核中的其它地方使用——至少在标准内核发行版本中是这样,也许此外的其它模块可能使用这个列表。不管怎样,这个列表的存在可以方便其他人使用。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> <B normal">LINUX_REBOOT_CMD_RESTART</B></FONT>和其它<B normal"><FONT face="Times New Roman">cmd</FONT></B>识别出的值从<FONT face="Times New Roman">16023</FONT>行开始通过<B normal"><FONT face="Times New Roman">#define</FONT></B>进行宏定义。这些值并没有潜在的意义,选用它们的简单原因是它们一般不会发生意外并且相互之间各不相同。(有趣的是,<B normal"><FONT face="Times New Roman">LINUX_REBOOT_CMD_OFF</FONT></B>是零,这是在意外情况下最不可能出现的一个值。但是,由于<B normal"><FONT face="Times New Roman">LINUX_REBOOT_CMD_OFF</FONT></B>简单的禁止用户使用<FONT face="Times New Roman">Ctrl+Alt+Del</FONT>重新启动机器,它就是一种“安全”的意外了。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29321</FONT>:打印警告信息以后,<B normal"><FONT face="Times New Roman">sys_reboot</FONT></B>调用<B normal"><FONT face="Times New Roman">machine_restart</FONT></B>(<FONT face="Times New Roman">2185</FONT>行)重启机器。正如你从<FONT face="Times New Roman">2298</FONT>行中所看到的一样,<B normal"><FONT face="Times New Roman">machine_restart</FONT></B>函数从来不会返回。但是不管怎样,对于<B normal"><FONT face="Times New Roman">machine_restart</FONT></B>的调用后面都跟着一个<B normal"><FONT face="Times New Roman">break</FONT></B>语句。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>这仅仅是经典的良好的编程风格吗?的确如此,但是却又不仅仅如此。文件<FONT face="Times New Roman">kernel/sys.c</FONT>的代码是属于体系结构无关部分的。但是<B normal"><FONT face="Times New Roman">machine_restart</FONT></B>,它显然是体系结构所特有的,属于代码的体系结构特有的部分(<FONT face="Times New Roman">arch/i386/kernel/process.c</FONT>)。因而对于不同的移植版本也有所不同。我们并不清楚以后内核的每个移植版本的实现都不会返回——例如,它可能调度底层硬件重启但是本身要仍然持续运行几分钟,这就需要首先从函数中返回。或者更为确切的说法是,由于某些特定的原因,系统可能并不总是能够重启;或许某些软件所控制的硬件根本就不能重启。在这种平台上,<B normal"><FONT face="Times New Roman">machine_restart</FONT></B>就应该可以返回,因此体系结构无关的代码应该对这种可能性有所准备。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>针对这个问题,正式的发行版本中都至少包含一个退出端口,使<B normal"><FONT face="Times New Roman">machine_restart</FONT></B>函数可以从这个端口返回:<FONT face="Times New Roman">m68k</FONT>端口。不同的基于<FONT face="Times New Roman">m68k</FONT>的机器支持的代码也各不相同,由于本书主要是针对<FONT face="Times New Roman">x86</FONT>的,我不希望花费过多的时间来解析所有的细节。但是这的确是可能的。(在其它情况下,<B normal"><FONT face="Times New Roman">machine_restart</FONT></B>简单进入一个无限循环——既不重新启动机器,也不返回。但是这里我们担心的是需要返回的情况。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>因此,我们毕竟是需要<B normal"><FONT face="Times New Roman">break</FONT></B>的。前面看起来只是简单的习惯甚至是偏执的思想在这里为了内核的移植性已经变成必须的了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29324</FONT>:接下来的两种情况分别允许和禁止臭名卓著的<FONT face="Times New Roman">Ctrl+Alt+Del</FONT>组合键(这三个组合键也被称为“<FONT face="Times New Roman">Vulcan</FONT>神经收缩(<FONT face="Times New Roman">Vulcan nerve pinch</FONT>)”,“黑客之手(<FONT face="Times New Roman">hacker’s claw</FONT>)”,“三指之礼(<FONT face="Times New Roman">three-fingered salute</FONT>)”,我最喜欢后面这个)。这些只是简单的设置全局<B normal"><FONT face="Times New Roman">C_A_D</FONT></B>标志(在<FONT face="Times New Roman">29160</FONT>行定义,在<FONT face="Times New Roman">29378</FONT>行检测)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29332</FONT>:这种情况和<B normal"><FONT face="Times New Roman">LINUX_REBOOT_CMD_RESTART</FONT></B>类似,但只是暂停系统而不是将其重新启动。两者之间的一个区别是它调用<B normal"><FONT face="Times New Roman">machine_halt</FONT></B>(<FONT face="Times New Roman">2304</FONT>行)——这是<FONT face="Times New Roman">x86</FONT>上的一条<FONT face="Times New Roman">no-op</FONT>指令,但是在其它平台上却要完成关闭系统的实际工作——而不是调用<B normal"><FONT face="Times New Roman">machine_restart</FONT></B>。并且它会把<B normal"><FONT face="Times New Roman">machine_halt</FONT></B>不能使之暂停的机器转入低功耗模式运行。它使用<B normal"><FONT face="Times New Roman">do_exit</FONT></B>(<FONT face="Times New Roman">23267</FONT>行)杀死内核本身。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29340</FONT>:到现在为止,这已经是一种比较熟悉的模式了。这里,<B normal"><FONT face="Times New Roman">sys_reboot</FONT></B>关闭机器电源,除了为可以使用软件自行关闭电源的系统调用<B normal"><FONT face="Times New Roman">machine_power_off</FONT></B>(<FONT face="Times New Roman">2307</FONT>行)之外,其它的应该和暂停机器情况完全相同。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29348</FONT>:<FONT face="Times New Roman"><B normal">LINUX_REBOOT_CMD_RESTART</B>2</FONT>的情况是已建立主题的一个变种。它接收命令,将其作为<FONT face="Times New Roman">ASCII</FONT>字符串传递,该字符串说明了机器应该如何关闭。字符串不会由<B normal"><FONT face="Times New Roman">sys_reboot</FONT></B>本身来解释,而是使用<B normal"><FONT face="Times New Roman">machine_restart</FONT></B>函数来解释;因而这种模式的意义,如果有的话,就是这些代码是平台相关的。(我使用“如果有”的原因是启动机器——特别是在<FONT face="Times New Roman">x86</FONT>中——一般只有一种方法,因此其它的信息都可以被<B normal"><FONT face="Times New Roman">machine_restart</FONT></B>忽略。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29365</FONT>:调用者传递了一个无法识别的命令。<B normal"><FONT face="Times New Roman">sys_reboot</FONT></B>不作任何处理,仅仅返回一个错误。因此,即使由<B normal"><FONT face="Times New Roman">magic1</FONT></B>和<B normal"><FONT face="Times New Roman">magic2</FONT></B>传递给<B normal"><FONT face="Times New Roman">sys_reboot</FONT></B>正确的<FONT face="Times New Roman">magic</FONT>数值,它也无须处理任何内容。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29369</FONT>:一个可识别的命令被传递给<B normal"><FONT face="Times New Roman">sys_reboot</FONT></B>。如果流程执行到这里,它可能就是两个设置<B normal"><FONT face="Times New Roman">C_A_D</FONT></B>的命令之一,因为其它情况通常都是停止或者重新启动机器。在任何情况下,<B normal"><FONT face="Times New Roman">sys_reboot</FONT></B>都简单把内核解锁并返回<FONT face="Times New Roman">0</FONT>以表示成功。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sys_sysinfo</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24142</FONT>:一个只能返回一个整型值的系统调用。如果需要返回更多的信息,我们只需要使用类似于在系统调用中传递多于四个参数时所使用的技巧就可以了:我们通过一个指向结构的指针将结果返回。收集系统资源使用情况的<B normal"><FONT face="Times New Roman">sysinfo</FONT></B>系统调用就是这种函数的一个样例。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24144</FONT>:分配并清空一个<B normal"><FONT face="Times New Roman">struct sysinfo</FONT></B>结构(<FONT face="Times New Roman">15004</FONT>行)以暂时存储返回值。<B normal"><FONT face="Times New Roman">sys_sysinfo</FONT></B>可以把结构中的每个域都独立地拷贝出来,但是这样会速度很慢、很不方便,而且必然不容易阅读。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24148</FONT>:禁止中断。这在第<FONT face="Times New Roman">6</FONT>章中会有详细的介绍;作为目前来说,我们只要说明这种模式在使用的过程中能够确保<B normal"><FONT face="Times New Roman">sys_sysinfo</FONT></B>正在使用的值不会改变就足够了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24149</FONT>:<B normal"><FONT face="Times New Roman">struct sysinfo</FONT></B>结构的<B normal"><FONT face="Times New Roman">uptime</FONT></B>域用来指明系统已经启动并运行了的秒数。这个值是使用<B normal"><FONT face="Times New Roman">jiffies</FONT></B>(<FONT face="Times New Roman">26146</FONT>行)和<B normal"><FONT face="Times New Roman">HZ</FONT></B>来计算的。<B normal"><FONT face="Times New Roman">jiffies</FONT></B>计算了系统运行过程中时钟的滴答次数;<B normal"><FONT face="Times New Roman">HZ</FONT></B>是系统相关的一个参数,它十分简单,就是每秒内部时钟滴答的次数。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24151</FONT>:数组<B normal"><FONT face="Times New Roman">avenrun</FONT></B>(<FONT face="Times New Roman">27116</FONT>行)记录了运行队列的平均长度——也就是等待<FONT face="Times New Roman">CPU</FONT>的平均进程数——在最后的<FONT face="Times New Roman">1</FONT>秒钟,<FONT face="Times New Roman">5</FONT>秒钟和<FONT face="Times New Roman">15</FONT>秒钟。<B normal"><FONT face="Times New Roman">calc_load</FONT></B>(<FONT face="Times New Roman">27135</FONT>行)周期性的重复计算它的值。由于内核中是要严格禁止浮点数运算的,所以只能通过计算变化的次数这一修正值来计算。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24155</FONT>:同样记录系统中当前运行的进程数。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24158</FONT>:<B normal"><FONT face="Times New Roman">si_meminfo</FONT></B>(<FONT face="Times New Roman">07635</FONT>行)写入这个结构中的内存相关成员,<B normal"><FONT face="Times New Roman">si_swapinfo</FONT></B>(<FONT face="Times New Roman">38544</FONT>行)写入与虚拟内存相关的部分。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24161</FONT>:现在整个结构都已经全部填充了。<B normal"><FONT face="Times New Roman">sysinfo</FONT></B>试图将其拷贝回用户空间,如果失败就返回<B normal"><FONT face="Times New Roman">EFAULT</FONT></B>,如果成功就返回<FONT face="Times New Roman">0</FONT>。</P><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P>
< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.25pt">l 分可能会通过清空<B normal"><FONT face="Times New Roman">bh_mask</FONT></B>中的相关位来整个跳过——通过调整<B normal"><FONT face="Times New Roman">bh_mask_count</FONT></B>入口,<B normal"><FONT face="Times New Roman">enable_bh</FONT></B>和<B normal"><FONT face="Times New Roman">disable_bh</FONT></B>完成了这个功能。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: 0cm">因此,对<B normal"><FONT face="Times New Roman">bh_mask</FONT></B>和<B normal"><FONT face="Times New Roman">bh_active</FONT></B>进行位<FONT face="Times New Roman">AND</FONT>运算就能够表明应该运行哪一个下半部分。特别是如果位与运算的结果是<FONT face="Times New Roman">0</FONT>,就没有下半部分需要运行。这种技术在内核中多次使用,例如在宏<B normal"><FONT face="Times New Roman">get_active_bhs</FONT></B>(<FONT face="Times New Roman">12480</FONT>行)中就使用了这种技术</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.25pt">l <B normal"><FONT face="Times New Roman">bh_base</FONT></B>(<FONT face="Times New Roman">14858</FONT>行)——这是一组简单的指向下半部分函数的指针。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.25pt">l 未命名的<B normal"><FONT face="Times New Roman">enum</FONT></B>——从<FONT face="Times New Roman">14866</FONT>行开始的未命名的<B normal"><FONT face="Times New Roman">enum</FONT></B>为内核使用的每一个下半部分指定了一个符号名称。例如,为了把计数器的下半部分标记为活动的,你可以这样的语句:</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">mark_bh</FONT>(<FONT face="Times New Roman">TIME_BH</FONT>)<FONT face="Times New Roman">;</FONT></P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">27450</FONT>行的确就是这样处理的。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">操作和IRQ</H2>< 0cm 0cm 0pt">一个经过仔细选择的小型函数集合处理了操作和<FONT face="Times New Roman">IRQ</FONT>之间的链接和解除链接。本节就是要讨论这些函数,以及那些从整体上对<FONT face="Times New Roman">IRQ</FONT>系统进行初始化的函数。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">init_ IRQ</H4>< 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">1597</FONT>:<FONT face="Times New Roman"> <B normal">init_ IRQ</B></FONT>初始化<FONT face="Times New Roman">IRQ</FONT>的处理。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1601</FONT>:<FONT face="Times New Roman"> </FONT>符号<B normal"><FONT face="Times New Roman">CONFIG_X86_ISWS_APIC</FONT></B>是为<FONT face="Times New Roman">SGI</FONT>虚拟工作站以及<FONT face="Times New Roman">SGI</FONT>的基于<FONT face="Times New Roman">x86</FONT>的工作站流水线而设置的。虽然同样基于<FONT face="Times New Roman">x86</FONT>的<FONT face="Times New Roman">CPU</FONT>,虚拟工作站不能和基于<FONT face="Times New Roman">IBM PC</FONT>的体系结构共享很多其它特性——特别是如同你看到的,它们的中断处理有些不同。我们以后将忽略虚拟工作站所特有的代码。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1609</FONT>:<FONT face="Times New Roman"> </FONT>建立中断描述符表,给<FONT face="Times New Roman">32</FONT>项到<FONT face="Times New Roman">95</FONT>项(十进制)赋缺省值。在这个过程中使用了<B normal"><FONT face="Times New Roman">set_nitr_gate</FONT></B>(<FONT face="Times New Roman">6647</FONT>行),该函数很快就会介绍到。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1651</FONT>:<FONT face="Times New Roman"> </FONT>建立<FONT face="Times New Roman">IRQ 2</FONT>(级联中断)和<FONT face="Times New Roman">IRQ13</FONT>(为<FONT face="Times New Roman">FPU</FONT>使用——请参看<FONT face="Times New Roman">955</FONT>行)。和这两个<FONT face="Times New Roman">IRQ</FONT>有关的<B normal"><FONT face="Times New Roman">irqaction</FONT></B>结构分别是<B normal"><FONT face="Times New Roman">irq2</FONT></B>(<FONT face="Times New Roman">979</FONT>行)和<B normal"><FONT face="Times New Roman">irq13</FONT></B>(<FONT face="Times New Roman">974</FONT>行)。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">init_ISA_irqs</H4>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1578</FONT>:<FONT face="Times New Roman"> </FONT>该函数填充<B normal"><FONT face="Times New Roman">irq_desc</FONT></B>数组,为<FONT face="Times New Roman">ISA</FONT>总线类型的机器(也就是所有标准<FONT face="Times New Roman">C</FONT>)初始化所有<FONT face="Times New Roman">IRQ</FONT>。虽然该函数没有声明为<B normal"><FONT face="Times New Roman">static</FONT></B>类型的,也没有使用<B normal"><FONT face="Times New Roman">__initfunc</FONT></B>标签标记,但是它只会被<B normal"><FONT face="Times New Roman">init_ IRQ</FONT></B>调用。因此,只有在内核初始化过程中这个函数才是必要的。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1583</FONT>:<FONT face="Times New Roman"> </FONT>对<B normal"><FONT face="Times New Roman">irq_desc</FONT></B>中的每一个元素,系统为<B normal"><FONT face="Times New Roman">status</FONT></B>,<B normal"><FONT face="Times New Roman">action</FONT></B>和<B normal"><FONT face="Times New Roman">depth</FONT></B>成员赋与了不会惹人反对的,也不会使人吃惊的的缺省值。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1589</FONT>:<FONT face="Times New Roman"> </FONT>原来的(在<FONT face="Times New Roman">CI</FONT>之前)<FONT face="Times New Roman">IRQ</FONT>使用<B normal"><FONT face="Times New Roman">i8259A_irq_type</FONT></B>(<FONT face="Times New Roman">723</FONT>行)处理。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1592</FONT>:<FONT face="Times New Roman"> </FONT>编号比较高的<FONT face="Times New Roman">IRQ</FONT>初始化为<B normal"><FONT face="Times New Roman">no_irq_type</FONT></B>(<FONT face="Times New Roman">701</FONT>行),这是一个必要的空处理程序。后来它们可能会改变——实际上,如果你使用了<FONT face="Times New Roman">CI</FONT>卡,就确实会改变,就象现在的大多数<FONT face="Times New Roman">C</FONT>一样。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">set_intr_gate</H4>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">6647</FONT>:<FONT face="Times New Roman"> <B normal">set_intr_gate</B></FONT>在<FONT face="Times New Roman">x86CPU</FONT>的中断描述符表(<FONT face="Times New Roman">IDT</FONT>)中建立一个项。在基于<FONT face="Times New Roman">x86</FONT>的系统中发生的每一个软件中断和硬件中断都有一个编号,这个编号被<FONT face="Times New Roman">CPU</FONT>用作是对这个表的索引。(包括系统调用中断——编号为<FONT face="Times New Roman">0x80</FONT>——在第<FONT face="Times New Roman">5</FONT>章中我们已经介绍过了。)表中相关的项是中断发生时(内核)函数需要跳转到的地址。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">setup_x86_irq</H4>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1388</FONT>:<B normal"><FONT face="Times New Roman">setup_x86_irq</FONT></B>给指定的<FONT face="Times New Roman">IRQ</FONT>增加了一个操作(一个<B normal"><FONT face="Times New Roman">struct irqaction</FONT></B>结构)。例如,在<FONT face="Times New Roman">6088</FONT>行使用它来记录定时器的中断。它还可以通过<B normal"><FONT face="Times New Roman">request_irq</FONT></B>(<FONT face="Times New Roman">1439</FONT>行)使用,这在下一节介绍。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1398</FONT>:<FONT face="Times New Roman"> Linux</FONT>使用了几种物理的随机源——例如中断——把一系列不可预知的值提供给设备<FONT face="Times New Roman">/dev/random</FONT>,这是一个有限却具有很高随机性的数据源,还有<FONT face="Times New Roman">/dev/urandom</FONT>,这是对应<FONT face="Times New Roman">/dev/random</FONT>的无限的但是随机性较小的对应版本。随机系统作为一个整体在本书中并没有涉及,但是如果你不知道这个概念,这一大部分代码就会显得十分神秘。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1412</FONT>:<FONT face="Times New Roman"> </FONT>如果现存的操作列表非空,<B normal"><FONT face="Times New Roman">setup_x86_irq</FONT></B>必须保证现存的操作和新的操作可以共享这个<FONT face="Times New Roman">IRQ</FONT>。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1414</FONT>:<FONT face="Times New Roman"> </FONT>验证这个<FONT face="Times New Roman">IRQ</FONT>可以和其上现存的<B normal"><FONT face="Times New Roman">struct irqaction</FONT></B>结构共享。这种测试是十分有效的,它部分是基于我们的一些认识:没有必要遍历执行队列中的所有操作,也没有必要检测它们可能共享的所有情况。除非这两个操作和第一个操作都允许共享<FONT face="Times New Roman">IRQ</FONT>,否则不会允许第一个操作后的所有操作都进入队列。因此,如果第一个操作可以共享<FONT face="Times New Roman">IRQ</FONT>,那么队列中的其它操作也就可以共享<FONT face="Times New Roman">IRQ</FONT>;如果第一个操作不能共享,那么队列中的其它任何操作也都不能共享<FONT face="Times New Roman">IRQ</FONT>。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1420</FONT>:<FONT face="Times New Roman"> IRQ</FONT>正在被共享。<B normal"><FONT face="Times New Roman">setup_x86_irq</FONT></B>利用<B normal"><FONT face="Times New Roman">p</FONT></B>向前执行操作队列直到末尾,离开时<B normal"><FONT face="Times New Roman">p</FONT></B>指向队列的最后一个元素的<B normal"><FONT face="Times New Roman">next</FONT></B>域。它也会增加<B normal"><FONT face="Times New Roman">shared</FONT></B>标志的值,这将会在<FONT face="Times New Roman">1429</FONT>行中被使用。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1427</FONT>:<FONT face="Times New Roman"> <B normal">p</B></FONT>现在指向队列中的最后一个元素的<B normal"><FONT face="Times New Roman">next</FONT></B>域,如果要共享<FONT face="Times New Roman">IRQ</FONT>,或者<B normal"><FONT face="Times New Roman">p</FONT></B>在不共享的情况下指向<B normal"><FONT face="Times New Roman">irq_desc[irq].action</FONT></B>——指向队列的头节点的指针。不管怎样,指针现在被设置为新的元素了。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1429</FONT>:<FONT face="Times New Roman"> </FONT>如果还没有操作和这个<FONT face="Times New Roman">IRQ</FONT>关联,<B normal"><FONT face="Times New Roman">irq_desc[irq]</FONT></B>的其它部分也就还没有设置,在这里就需要对其初始化了。特别要注意<FONT face="Times New Roman">1433</FONT>行中为这个<FONT face="Times New Roman">IRQ</FONT>调用了<B normal"><FONT face="Times New Roman">startup</FONT></B>函数。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">request_irq</H4>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1439</FONT>:<FONT face="Times New Roman"> <B normal">request_irq</B></FONT>从提供的值中创建一个<B normal"><FONT face="Times New Roman">struct irqaction</FONT></B>结构,并将其加入对应给定的<FONT face="Times New Roman">IRQ</FONT>的<B normal"><FONT face="Times New Roman">struct irqaction</FONT></B>列表中。(如果你对<FONT face="Times New Roman">C++</FONT>和<FONT face="Times New Roman">Java</FONT>比较熟悉,可以把它当作是操作的构造函数。)它的实现非常简单明了。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1448</FONT>:<FONT face="Times New Roman"> </FONT>对一对输入值进行健全性检测。注意没有必要测试<B normal"><FONT face="Times New Roman">irq</FONT></B>是否小于<FONT face="Times New Roman">0</FONT>,因为它是一个无符号数。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1453</FONT>:<FONT face="Times New Roman"> </FONT>动态分配新的<B normal"><FONT face="Times New Roman">struct irqaction</FONT></B>结构。为此目的使用的函数<B normal"><FONT face="Times New Roman">kmalloc</FONT></B>在第<FONT face="Times New Roman">8</FONT>章中简单介绍。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1458</FONT>:<FONT face="Times New Roman"> </FONT>填充新的操作并使用<B normal"><FONT face="Times New Roman">setup_x86_irq</FONT></B>将其加入操作列表。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">free_irq</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1472</FONT>:<FONT face="Times New Roman"> <B normal">free_irq</B></FONT>是<B normal"><FONT face="Times New Roman">request_irq</FONT></B>的补数(<FONT face="Times New Roman">inverse</FONT>)。如果<B normal"><FONT face="Times New Roman">request_irq</FONT></B>类似于操作的构造函数,那么这就是操作的析构函数。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1481</FONT>:<FONT face="Times New Roman"> </FONT>在确保<B normal"><FONT face="Times New Roman">irq</FONT></B>在范围内以后,<B normal"><FONT face="Times New Roman">free_irq</FONT></B>找到有关的<B normal"><FONT face="Times New Roman">irq_desc</FONT></B>项并且开始遍历操作列表。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1483</FONT>:<FONT face="Times New Roman"> </FONT>除非它有正确的设备<FONT face="Times New Roman">ID</FONT>,否则就忽略这个队列元素。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1487</FONT>:<FONT face="Times New Roman"> </FONT>把这个元素从队列中分离出来并且释放其所占用的内存。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1489</FONT>:<FONT face="Times New Roman"> </FONT>如果现在操作队列为空——也就是如果队列中只有唯一一个元素没有被链接——设备就会被关闭。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1495</FONT>:<FONT face="Times New Roman"> </FONT>如果控制流程执行到这里,就意味着<B normal"><FONT face="Times New Roman">free_irq</FONT></B>处理了整个操作列表而没有发现匹配的<B normal"><FONT face="Times New Roman">dev_id</FONT></B>。如果发现了匹配对象,<FONT face="Times New Roman">1493</FONT>行的<B normal"><FONT face="Times New Roman">goto</FONT></B>语句就已经跳过了本行。因此,这个试图释放<FONT face="Times New Roman">IRQ</FONT>操作的努力是错误的;在这种情况下<B normal"><FONT face="Times New Roman">free_irq</FONT></B>会打印出一条警告信息对当前状况进行描述。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">prove_irq_on</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1506</FONT>:<FONT face="Times New Roman"> <B normal">probe_irq_on</B></FONT>实现了内核<FONT face="Times New Roman">IRQ</FONT>自动探测的重要的一部分。阅读<FONT face="Times New Roman">14889</FONT>行开始的标题注释就得到了对整个进程的描述。根据描述我们知道这里要作的工作(只)是执行步骤三:暂时使能所有没有定义的<FONT face="Times New Roman">IRQ</FONT>,以使得<B normal"><FONT face="Times New Roman">probe_irq_on</FONT></B>的调用者可以检测它们。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1514</FONT>:<FONT face="Times New Roman"> </FONT>对于除<FONT face="Times New Roman">IRQ 0</FONT>之外的每一个<FONT face="Times New Roman">IRQ</FONT>,如果这个<FONT face="Times New Roman">IRQ</FONT>还没有与之相关的操作,<B normal"><FONT face="Times New Roman">probe_irq_on</FONT></B>会记录下这个<FONT face="Times New Roman">IRQ</FONT>正在自动探测的事实并启动关联设备。顺便说明一下,我不认为有任何的原因使这个循环向后执行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1524</FONT>:<FONT face="Times New Roman"> </FONT>忙等待约十分之一秒时间以允许生成伪中断的设备取消自己。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1530</FONT>:<FONT face="Times New Roman"> </FONT>循环再次遍历所有的<FONT face="Times New Roman">IRQ</FONT>,这一次要过滤出所有生成伪中断的设备。这个循环每次重复执行都是从<FONT face="Times New Roman">1</FONT>开始而不是从<FONT face="Times New Roman">0</FONT>开始,这是因为不需要自动检测的<FONT face="Times New Roman">IRQ</FONT>都被忽略掉了,而<FONT face="Times New Roman">IRQ0</FONT>是从来都不会自动检测的。速度在这里也是一个问题;在十分之一秒的延时之后——这是很长的一段时间,即使是从慢速的<FONT face="Times New Roman">CPU</FONT>的观点来看也是如此——一个循环或多或少都是有些不合理的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1537</FONT>:<FONT face="Times New Roman"> </FONT>如果设备在<FONT face="Times New Roman">1524</FONT>行的等待过程中触发了中断,这个中断可能就是伪中断:在此期间系统应该还没有和设备通讯过,因此设备也应该还没有和系统通讯过。因此自动探测位将被清空,处理程序再次关闭。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1544</FONT>:<FONT face="Times New Roman"> </FONT>返回特殊数字<FONT face="Times New Roman">0x12345678</FONT>,其原因将在下面的讨论中进行说明。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">prove_irq_off</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1547</FONT>:<FONT face="Times New Roman"> <B normal">prove_irq_off</B></FONT>实现了<FONT face="Times New Roman">IRQ</FONT>自动探测的另外一部分重要的内容。这里的工作是决定对探测到的哪一个<FONT face="Times New Roman">IRQ</FONT>做出响应,并返回其中的一个<FONT face="Times New Roman">IRQ</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1551</FONT>:<FONT face="Times New Roman"> </FONT>检测名字很容易让人误解的参数<B normal"><FONT face="Times New Roman">unused</FONT></B>和<B normal"><FONT face="Times New Roman">probe_irq_on</FONT></B>返回的特殊数字是否相同。调用者假定象下面这样处理:</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> magic = prove_irq_on</FONT>()</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> /* … */</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> probe_irq_off</FONT>(<FONT face="Times New Roman">magic</FONT>)<FONT face="Times New Roman">;</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>如果偶然使用了其它的方法调用了<B normal"><FONT face="Times New Roman">probe_irq_off</FONT></B>(例如,如果由于其它一些逻辑调用者偶尔跳过了对<B normal"><FONT face="Times New Roman">probe_irq_on</FONT></B>的调用),那么提供的参数可能不会包含正确的值。可能更重要的是这个参数给正在编写代码使用这个函数的程序员提供了一些信息:在研究其参数应该是什么的时候,你会发现在调用它的时候一直遵守的规则。这种规则很容易就被过度使用。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>通过对紧随的错误消息的严格调整,似乎该函数的早期版本中可能已经在其参数中采用了调用者的地址。如果的确如此,这个测试就具有了第三种目的:把仍然不正确使用这个函数的调用者检测出来。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1557</FONT>:<FONT face="Times New Roman"> </FONT>循环遍历所有的<FONT face="Times New Roman">IRQ</FONT>,搜寻响应调用者探测的所有设备。这个循环也可以从<FONT face="Times New Roman">1</FONT>开始循环,这和前面讨论的<B normal"><FONT face="Times New Roman">probe_irq_on</FONT></B>的原因是相同的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1560</FONT>:<FONT face="Times New Roman"> </FONT>内核没有试图自动检测这个<FONT face="Times New Roman">IRQ</FONT>上的任何内容;它跳到了下一个<FONT face="Times New Roman">IRQ</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1563</FONT>:<FONT face="Times New Roman"> <B normal">IRQ_INPROGRESS</B></FONT>标志指明了该<FONT face="Times New Roman">IRQ</FONT>的一个中断已经到达。由于<B normal"><FONT face="Times New Roman">probe_irq_on</FONT></B>可能捕获所有的伪中断,假定这是对探测的真实响应。成功地自动检测到<FONT face="Times New Roman">IRQ</FONT>的数量因此而增一,同时保存第一次的数字。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1568</FONT>:<FONT face="Times New Roman"> </FONT>不管是否成功自动探测到<FONT face="Times New Roman">IRQ</FONT>,自动探测标志都要减少,并且再次结束处理程序。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1573</FONT>:<FONT face="Times New Roman"> </FONT>如果不止一个<FONT face="Times New Roman">IRQ</FONT>被成功地自动探测到,就通过否定的<B normal"><FONT face="Times New Roman">irq_found</FONT></B>来通知调用者。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1575</FONT>:<FONT face="Times New Roman"> </FONT>返回<B normal"><FONT face="Times New Roman">irq_found</FONT></B>——<FONT face="Times New Roman">0</FONT>,或者(可能是经过求反的)第一个成功地自动探测到的<FONT face="Times New Roman">IRQ</FONT>号。注意如果发现了设备,则返回值决不会是<FONT face="Times New Roman">0</FONT>,因为内核不会试图自动检测<FONT face="Times New Roman">IRQ 0</FONT>。因此当没有自动检测到<FONT face="Times New Roman">IRQ</FONT>时,<B normal"><FONT face="Times New Roman">probe_irq_off</FONT></B>就返回<FONT face="Times New Roman">0</FONT>。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">硬件中断处理程序和下半部分</H2><P 0cm 0cm 0pt"><FONT face="Times New Roman">x86</FONT>系列的实际中断处理程序是微不足道的;在最低的层次上,这是通过反复使用<B normal"><FONT face="Times New Roman">BUILD_IRQ</FONT></B>宏(<FONT face="Times New Roman">1886</FONT>行)建立了一系列小汇编函数而实现的。<B normal"><FONT face="Times New Roman">BUILD_ IRQ</FONT></B>自己被<B normal"><FONT face="Times New Roman">BI</FONT></B>宏调用(<FONT face="Times New Roman">866</FONT>行),这个宏又顺次被<B normal"><FONT face="Times New Roman">BUILD_16_IRQS</FONT></B>宏(<FONT face="Times New Roman">869</FONT>行)调用,该宏在<FONT face="Times New Roman">878</FONT>行到<FONT face="Times New Roman">895</FONT>行的代码中用来建立汇编程序。这一连串的宏调用的目的仅仅是试图减少必须编写的代码数量和复杂度——我们应该使用<FONT face="Times New Roman">256</FONT>次对<B normal"><FONT face="Times New Roman">BUILD_IRQ</FONT></B>的调用,而不是<FONT face="Times New Roman">16</FONT>次对<B normal"><FONT face="Times New Roman">BUILD_16_IRQS</FONT></B>的调用。</P><P 0cm 0cm 0pt">汇编程序和如下代码相类似:</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">IRQ0x00_interrupt:</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman"> push1 0x00-256</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman"> jmp common_interrupt</FONT></P><P 0cm 0cm 0pt">也就是每一次都简单地把它的<FONT face="Times New Roman">IRQ</FONT>号(减去<FONT face="Times New Roman">256</FONT>,原因在<FONT face="Times New Roman">1897</FONT>行有论述)压入堆栈并且跳转到正常的中断程序。</P><P 0cm 0cm 0pt">正常的中断处理程序是调用<B normal"><FONT face="Times New Roman">common_interrupt</FONT></B>,该函数也十分简短。它是使用<B normal"><FONT face="Times New Roman">BUILD_COMMON_IRQ</FONT></B>宏(<FONT face="Times New Roman">1871</FONT>行)建立的,在为<B normal"><FONT face="Times New Roman">do_IRQ</FONT></B>进行安排之后简单调用<B normal"><FONT face="Times New Roman">do_IRQ</FONT></B>返回给<B normal"><FONT face="Times New Roman">from_intr</FONT></B>(<FONT face="Times New Roman">233</FONT>行)——这是第<FONT face="Times New Roman">5</FONT>章中介绍的系统调用的一部分。随后将要介绍的<B normal"><FONT face="Times New Roman">do_ IRQ</FONT></B>(<FONT face="Times New Roman">1362</FONT>行)负责查看中断是否已经被处理了。</P><P 0cm 0cm 0pt">在介绍这些代码之前,从总体上观察一下在处理单个中断时这些部分如何组织在一起是很有帮助的:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">1. CPU</FONT>跳转到<B normal"><FONT face="Times New Roman">IRQ0x<I normal">NN</I>_interrupt</FONT></B>程序(其中的<I normal"><FONT face="Times New Roman">NN</FONT></I>是中断号),它将其唯一的中断号压入堆栈并跳转到<B normal"><FONT face="Times New Roman">common_interrupt</FONT></B>。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">2. <B normal">common_interrupt</B></FONT>调用<B normal"><FONT face="Times New Roman">do_IRQ</FONT></B>并保证当<B normal"><FONT face="Times New Roman">do_IRQ</FONT></B>返回时控制流程能够转向<B normal"><FONT face="Times New Roman">ret_from_intr</FONT></B>。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">3. <B normal">do_IRQ</B></FONT>调用中断处理器芯片独有的代码——直接和芯片通讯的代码,如果需要就用它来处理中断。对于<FONT face="Times New Roman">PC</FONT>体系结构中流行的<FONT face="Times New Roman">8259A</FONT>控制器芯片,处理函数是<B normal"><FONT face="Times New Roman">do_8259A_IRQ</FONT></B>,在这里提到它仅仅是为了举一个例子而已。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">4. <B normal">do_8259A_IRQ</B></FONT>暂时禁止正在处理的特殊<FONT face="Times New Roman">IRQ</FONT>,调用<B normal"><FONT face="Times New Roman">handle_IRQ_event</FONT></B>,接着重新使能这个<FONT face="Times New Roman">IRQ</FONT>。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">5. <B normal">handle_IRQ_event</B></FONT>为慢速<FONT face="Times New Roman">IRQ</FONT>使能中断,或者为处理快速<FONT face="Times New Roman">IRQ</FONT>而将这些中断保持在禁止状态。接着遍历一个已经和这个<FONT face="Times New Roman">IRQ</FONT>建立联系的函数队列,并依次调用这些函数。由于中断为慢速<FONT face="Times New Roman">IRQ</FONT>而使能,这里就是慢速<FONT face="Times New Roman">IRQ</FONT>的处理程序可能被其它中断所中断的地方。在执行完队列中的所有函数之后,<B normal"><FONT face="Times New Roman">handle_IRQ_event</FONT></B>禁止中断并返回控制器所特有的处理函数,而该函数将返回到<B normal"><FONT face="Times New Roman">do_IRQ</FONT></B>。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">6. <B normal">do_IRQ</B></FONT>处理所有挂起等待的下半部分,接着返回。如同你已经知道的那样,它要返回到<B normal"><FONT face="Times New Roman">ret_from_intr</FONT></B>。第<FONT face="Times New Roman">5</FONT>章中介绍了从此之后的处理内容。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">do_IRQ</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1375</FONT>:<FONT face="Times New Roman"> </FONT>更新内核的一些统计数字并调用与该<FONT face="Times New Roman">IRQ</FONT>相关的处理函数。对于一些老式<FONT face="Times New Roman"> PC</FONT>上的标号较小的<FONT face="Times New Roman">IRQ</FONT>来说,其处理程序是<B normal"><FONT face="Times New Roman">handle_IRQ_event</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1383</FONT>:<FONT face="Times New Roman"> </FONT>如果下半部分是激活的,内核现在就使用<B normal"><FONT face="Times New Roman">do_bottom_half</FONT></B>(<FONT face="Times New Roman">29126</FONT>行)对它们进行处理。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">do_IRQ_event</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1292</FONT>:<B normal"><FONT face="Times New Roman">do_IRQ_event</FONT></B>为<B normal"><FONT face="Times New Roman">do_8259A_IRQ</FONT></B>(<FONT face="Times New Roman">821</FONT>行)负担了大半重要的工作。本书中没有涉及到的其它一些代码也会调用这个函数。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1302</FONT>:与其描述(<FONT face="Times New Roman">12094</FONT>行)正好相反,<B normal"><FONT face="Times New Roman">SA_INTERRUPT</FONT></B>标志并不是一个<FONT face="Times New Roman">no_op</FONT>。如果没有设置该标志,在接下来的代码中就允许中断。这是快速中断和慢速中断之间历史上遗留下来的区别,这一点我们已经讨论过了。(处理这两种类型中断的代码通常有很多差异,但是结果是相同的——代码已经被处理得更加出色了。)恰当的说,这个标志似乎是大多数情况下都为慢速设备使用——顾名思义,就是软盘设备。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1305</FONT>:<FONT face="Times New Roman"> </FONT>通过调用每一个的处理函数来遍历执行这个<FONT face="Times New Roman">IRQ</FONT>的操作队列(队列头是由调用者提供的)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1310</FONT>:<FONT face="Times New Roman"> </FONT>这里的中断触发是用来为<FONT face="Times New Roman">/dev/random</FONT>和<FONT face="Times New Roman">/dev/urandom</FONT>增加一些随机信息——从大体上看来,大部分中断都是随机发生的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1312</FONT>:<FONT face="Times New Roman"> </FONT>禁止中断(当条件具备时调用者可以再次允许这些中断)。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">do_bottom_half</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29126</FONT>:<FONT face="Times New Roman">Linux</FONT>代码中有三处调用了<B normal"><FONT face="Times New Roman">do_bottom_half</FONT></B>:<FONT face="Times New Roman">26707</FONT>行,<FONT face="Times New Roman">243</FONT>行,<FONT face="Times New Roman">1384</FONT>行。(你会发现,其中的两个是在体系结构特有的文件中的;在非<FONT face="Times New Roman">x86</FONT>平台体系结构特有的文件中对应部分也会调用这个函数。)因此,下半部分是用来处理如下三种情况的:</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo4; tab-stops: list 57.25pt">l 当决定随后哪一个进程应该获得<FONT face="Times New Roman">CPU</FONT>时。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo4; tab-stops: list 57.25pt">l 当从系统调用中返回时。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo4; tab-stops: list 57.25pt">l 在从<B normal"><FONT face="Times New Roman">do_IRQ</FONT></B>中返回之前——也就是说,在每个中断之后。代码中的注释暗示了在内核的未来版本中不一定总在这里运行下半部分。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29130</FONT>:下半部分的一个理想特性是在某一时刻只能有一个下半部分处于运行状态。这种特性在这里,也就是锁定(<FONT face="Times New Roman">locking</FONT>)在<FONT face="Times New Roman">UP</FONT>代码中体现出重要意义的位置之一,得到了强化。首先调用<B normal"><FONT face="Times New Roman">soft_trylock</FONT></B>(<FONT face="Times New Roman">UP</FONT>版本在<FONT face="Times New Roman">12559</FONT>行——第<FONT face="Times New Roman">10</FONT>章中可以查看所有的<FONT face="Times New Roman">SMP</FONT>版本),只有在<B normal"><FONT face="Times New Roman">local_bh_count[cpu]</FONT></B>原来为<FONT face="Times New Roman">0</FONT>时这个函数才把<B normal"><FONT face="Times New Roman">local_bh_count [cpu]</FONT></B>设置为<FONT face="Times New Roman">1</FONT>并返回真值。根据<FONT face="Times New Roman">17479</FONT>行,对于<FONT face="Times New Roman">UP</FONT>来说<B normal"><FONT face="Times New Roman">cpu</FONT></B>总是<FONT face="Times New Roman">0</FONT>,而且你应该注意到<B normal"><FONT face="Times New Roman">softirq_trylock</FONT></B>自己是不能被中断的,因为在这里中断已经被禁止了。<B normal"><FONT face="Times New Roman">softirq_trylock</FONT></B>和对应的<B normal"><FONT face="Times New Roman">softirq_endlock</FONT></B>(<FONT face="Times New Roman">12561</FONT>行),就是仅仅为了以下目的而退出的,而不存在其它原因:协助保证这个下半部分不会被其它下半部分中断(虽然它们可以被上半部分所中断)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29131</FONT>:如果成功获得了锁,那么该函数就尝试另一个函数,<FONT face="Times New Roman">10736</FONT>行的<B normal"><FONT face="Times New Roman">hardirq_trylock</FONT></B>。它只报告当前执行进程是否位于<B normal"><FONT face="Times New Roman">hardirq_enter/hardirq_exit</FONT></B>对(<FONT face="Times New Roman">10739</FONT>行和<FONT face="Times New Roman">10740</FONT>行)之间。对于<FONT face="Times New Roman">UP</FONT>,这两者的定义是和<B normal"><FONT face="Times New Roman">irq_enter</FONT></B>与<B normal"><FONT face="Times New Roman">irq_exit</FONT></B>(<FONT face="Times New Roman">1810</FONT>行和<FONT face="Times New Roman">1811</FONT>行)两者的定义相同的;后两个函数在<B normal"><FONT face="Times New Roman">handle_IRQ_event</FONT></B>中使用,当然在其它我们所没有讨论到的地方也对它们有所引用。这些宏协同工作来保证<B normal"><FONT face="Times New Roman">__cli</FONT></B>和<B normal"><FONT face="Times New Roman">__sti</FONT></B>对能够正确的进行嵌套——由于<FONT face="Times New Roman">CPU</FONT>不会嵌套使用这些宏,我们必须保证不会使用<B normal"><FONT face="Times New Roman">__sti</FONT></B>处理其它的<B normal"><FONT face="Times New Roman">__cli</FONT></B>,而这也不是我们所希望的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29132</FONT>:没有其它下半部分在运行,而且<B normal"><FONT face="Times New Roman">do_bottom_half</FONT></B>有权使能硬件中断。因此,它就使能硬件中断,运行下半部分,接着再次禁止中断。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29135</FONT>:释放该函数已经获取的锁并返回。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">run_bottom_halves</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29110</FONT>:现在内核能够运行挂起等待的下半部分。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29115</FONT>:存储当前局部变量<B normal"><FONT face="Times New Roman">active</FONT></B>中活动的——也就是被标记过的——下半部分的集合,并使用<B normal"><FONT face="Times New Roman">clear_active_bh</FONT></B>宏(<FONT face="Times New Roman">12481</FONT>行)清空全局变量<B normal"><FONT face="Times New Roman">bh_active</FONT></B>中的设置位。对<B normal"><FONT face="Times New Roman">bh_active</FONT></B>中这些位的清除将同时取消对所有下半部分的标记。</P><P 0cm 0cm 0pt">现在你就应该可以看到下半部分有时候是批量处理的,这一点在前面已经进行了没有论证的说明。此处中断是被使能的,因此如果在<B normal"><FONT face="Times New Roman">run_bottom_halves</FONT></B>把<B normal"><FONT face="Times New Roman">bh_active</FONT></B>拷贝到<B normal"><FONT face="Times New Roman">active</FONT></B>之前就有中断触发并标记已经标记过的下半部分,那么上半部分就已经运行了两次,然而下半部分只运行了一次。还有,由于这个中断可以被自己中断,在下半部分运行一次的时候上半部分就已经运行了三次,等等。但是随着递归调用数量的增长,这很快就会变得不再可能了。</P><P 0cm 0cm 0pt">代码有可能忽略某个下半部分吗?假定中断在最坏的时刻发生:在<FONT face="Times New Roman">29115</FONT>行和<FONT face="Times New Roman">29116</FONT>行之间——也就是在拷贝<B normal"><FONT face="Times New Roman">bh_active</FONT></B>之后,但是在清空其中的设置位之前。下面是三种可能的情况:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo5; tab-stops: list 42.25pt">l <I normal">新的中断没有标记下半部分</I>。这种情况显然不会引起什么问题——中断之后处理的下半部分集合和前面的集合相同,因此<B normal"><FONT face="Times New Roman">run_bottom_halves</FONT></B>仍将运行所有它应该运行的下半部分。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo5; tab-stops: list 42.25pt">l <I normal">新的中断标记了一个已经标记过了的下半部分</I>。这种情况也不会引起问题——<B normal"><FONT face="Times New Roman">run_bottom_halves</FONT></B>不管怎样都要运行这个下半部分,而且在新中断返回之后就运行它。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo5; tab-stops: list 42.25pt">l <I normal">新的中断标记了一个前面没有标记过的下半部分</I>。在这种情况下,当<B normal"><FONT face="Times New Roman">run_bottom_halves</FONT></B>遍历执行所有的下半部分时,<B normal"><FONT face="Times New Roman">active</FONT></B>就不再和<B normal"><FONT face="Times New Roman">bh_active</FONT></B>匹配了。然而,由于<B normal"><FONT face="Times New Roman">clear_active_bhs</FONT></B>只会清空<B normal"><FONT face="Times New Roman">active</FONT></B>集合中设置的位,所以<FONT face="Times New Roman">29116</FONT>行不会清空<B normal"><FONT face="Times New Roman">bh_active</FONT></B>中新近标记的位。<B normal"><FONT face="Times New Roman">clear_active_bhs</FONT></B>使用<B normal"><FONT face="Times New Roman">atomic_clear_mask</FONT></B>(<FONT face="Times New Roman">10262</FONT>行),后者简单的对<B normal"><FONT face="Times New Roman">active</FONT></B>集合中的设置位进行位<FONT face="Times New Roman">AND</FONT>运算,而并不处理其余部分。因此,当<B normal"><FONT face="Times New Roman">run_bottom_halves</FONT></B>执行循环时,就不会立刻对新近标记的下半部分进行处理;但是由于它的位仍然在<B normal"><FONT face="Times New Roman">bh_active</FONT></B>中设置,<B normal"><FONT face="Times New Roman">run_bottom_halves</FONT></B>就仍然会在最后对它进行处理。更为准确的说法是,通过这种方法跳过的下半部分会在处理随后的一个定时器中断的过程中一起处理,这只是一个瞬间的延迟而已——或者如果有其它中断首先发生了,那么这段时间延迟会更短。因此,迷途的下半部分通常的等待时间不会超过百分之一秒,而且根据定义,下半部分毕竟不是时效性要求非常高的,这种少量延时不会引起任何问题。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29118</FONT>:同时遍历执行<B normal"><FONT face="Times New Roman">bh_base</FONT></B>数组和<B normal"><FONT face="Times New Roman">active</FONT></B>中的位。如果<B normal"><FONT face="Times New Roman">active</FONT></B>中最低的位被设置了,就调用相关的下半部分;接着循环推进到下一个<B normal"><FONT face="Times New Roman">bh_base</FONT></B>项和<B normal"><FONT face="Times New Roman">active</FONT></B>中的下一位继续执行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman"> </FONT>因为这是一个<B normal"><FONT face="Times New Roman">do/while</FONT></B>循环,所以它最少执行一次。部分原因是由于在调用<B normal"><FONT face="Times New Roman">do_bottom_half</FONT></B>之前,调用者都要检测是否有下半部分需要处理,所以这个循环最少要执行一次。在一些情况下这种检测会执行到<B normal"><FONT face="Times New Roman">do_bottom_half</FONT></B>本身中,但是如果没有下半部分需要运行,在调用之前执行测试就能够节省函数调用的开销。不管怎样,我们很容易就可以看出即使没有下半部分需要运行,这个循环也可以正确执行;虽然这会浪费一些时间,但是不这样就会引起错误。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29123</FONT>:当<B normal"><FONT face="Times New Roman">active</FONT></B>中没有任何位被设置时,循环就终止退出。由于在循环执行的过程中<B normal"><FONT face="Times New Roman">active</FONT></B>是不断移位的,这样就同时测试了其余的位,而没有必要对它们的每一个都进行循环处理。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">时间</H2><P 0cm 0cm 0pt">本节通过观察一个中断的例子——定时器中断——的工作方式来使你能够将中断和下半部分的知识融会贯通起来。</P><P 0cm 0cm 0pt">定时器中断函数<B normal"><FONT face="Times New Roman">timer_interrupt</FONT></B>是和<FONT face="Times New Roman">6086</FONT>行的<FONT face="Times New Roman">IRQ 0</FONT>相关的。此处使用的<B normal"><FONT face="Times New Roman">irq0</FONT></B>变量是在<FONT face="Times New Roman">5937</FONT>行定义的。<FONT face="Times New Roman">27972</FONT>行通过使用<B normal"><FONT face="Times New Roman">init_bh</FONT></B>(<FONT face="Times New Roman">12484</FONT>行)把<B normal"><FONT face="Times New Roman">timer_bh</FONT></B>函数注册为定时器的下半部分。</P><P 0cm 0cm 0pt">当触发<FONT face="Times New Roman">IRQ 0</FONT>时,<B normal"><FONT face="Times New Roman">timer_interrupt</FONT></B>从<FONT face="Times New Roman">CPU</FONT>时间戳计数器中读取一些属性值,如果<FONT face="Times New Roman">CPU</FONT>中有值(这在本书中没有涉及到的一些代码中使用),就调用<B normal"><FONT face="Times New Roman">do_timer_interrupt</FONT></B>(<FONT face="Times New Roman">5758</FONT>行)。除了其它一些工作之外,它会调用<B normal"><FONT face="Times New Roman">do_timer</FONT></B>,这是定时器中断非常有趣的一部分。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">do_timer</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27446</FONT>:从我们的出发点来看,这是我们感兴趣的定时器的上半部分。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27448</FONT>:更新全局变量<B normal"><FONT face="Times New Roman">jiffies</FONT></B>,这个值记录了机器启动以来系统时钟跳动的次数。(显然,这实际上记录的是从定时器中断装载以来已经经过的定时器跳动的次数,定时器中断在系统启动的瞬间是不会发生的。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27449</FONT>:递增丢失的定时器跳动的数目——也就是那些没有被下半部分处理的定时器的跳动。很快你就会看到有定时器的下半部分是怎样使用这个变量的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27450</FONT>:上半部分已经运行了,因此其下半部分被标记为只要可能就运行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27451</FONT>:除了要记录从上一次定时器的下半部分运行以来定时器跳动发生的次数之外,我们还要知道有多少这种跳动发生在系统模式下。很快我们会再次看到为什么下半部分需要它;但是在目前,如果进程运行在系统(内核)模式下而不是用户模式下,那么只需要递增<B normal"><FONT face="Times New Roman">lost_ticks_system</FONT></B>的值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27453</FONT>:如果定时器队列中有任务在等待,定时器队列的下半部分被标记为准备好运行(我们很快就会对定时器队列进行讨论)。而这就是整个定时器中断。虽然这看起来十分简单,但是这很大程度上是由于主要的工作都适当的延迟到下半部分处理了。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">timer_bh</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27439</FONT>:这是定时器的下半部分。它调用函数为进程和内核本身更新有关时间的统计数字,并同时对老式内核定时器和新式内核定时器进行处理。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">update_times</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27412</FONT>:这个函数主要是更新统计数字:计算系统的平均负载,更新记录当前时间的全局变量,并更新内核的当前进程使用的<FONT face="Times New Roman">CPU</FONT>时间的估计值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27422</FONT>:取得从上次下半部分运行以来发生的定时器跳动的数目,并重置计数器。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27427</FONT>:如果数字非空——正常情况下都是这样——<B normal"><FONT face="Times New Roman">update_times</FONT></B>会找出有多少这种跳动发生在系统模式下。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27429</FONT>:调用<B normal"><FONT face="Times New Roman">calc_load</FONT></B>(<FONT face="Times New Roman">27135</FONT>行,马上就介绍)更新内核有关系统负载要素的估计值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27430</FONT>:调用<B normal"><FONT face="Times New Roman">calc_wall_time</FONT></B>(<FONT face="Times New Roman">27311</FONT>行,后面会讨论)更新<B normal"><FONT face="Times New Roman">xtime</FONT></B>,它记录了当前的<FONT face="Times New Roman">wall_clock</FONT>时间。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27433</FONT>:调用<B normal"><FONT face="Times New Roman">update_process_times</FONT></B>(<FONT face="Times New Roman">27382</FONT>行),该函数同一个辅助函数<B normal"><FONT face="Times New Roman">update_one_process</FONT></B>(<FONT face="Times New Roman">27371</FONT>行)共同作用,更新内核中有关当前进程已经运行的时间长度的统计。这些统计数字可以使用诸如<FONT face="Times New Roman">time</FONT>,<FONT face="Times New Roman">top</FONT>,和<FONT face="Times New Roman">ps</FONT>之类的普通程序得到。现在你就可以看出,这些统计资料未必一定要正确:如果一个进程能够做到在所有时钟中断触发的时候都不处于运行状态,那么它就可以偷偷的使用大部分<FONT face="Times New Roman">CPU</FONT>而且还不会被认为使用了任何<FONT face="Times New Roman">CPU</FONT>资源。然而,还是比较难以理解为什么(或者怎样)恶意的进程要试图执行它,对于意图良好的进程来说,统计资料自然要进行平均——它们要为一些自己几乎不能使用的<FONT face="Times New Roman">CPU</FONT>定时器跳动负责,但是却不用为那些自己几乎在整个定时器跳动期间都在使用<FONT face="Times New Roman">CPU</FONT>的另一种情况负责,但是这些最终都不存在了。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">update_wall_time</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27313</FONT>:为每一个必须要处理的跳动调用<B normal"><FONT face="Times New Roman">update_wall_time_one_tick</FONT></B>(<FONT face="Times New Roman">27271</FONT>行)。它更新了全局变量<B normal"><FONT face="Times New Roman">xtime</FONT></B>,如果可能,就通过遵守网络时间协议(<FONT face="Times New Roman">Network Time Protocol</FONT>)努力将其和实际时间保持同步。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27318</FONT>:将<B normal"><FONT face="Times New Roman">xtime</FONT></B>标准化,使得微秒数在<FONT face="Times New Roman">0</FONT>到<FONT face="Times New Roman">999,999</FONT>的范围内。在极端的情况下可能会丢失多于一秒的定时器跳动,那么<B normal"><FONT face="Times New Roman">xtime</FONT></B>的<B normal"><FONT face="Times New Roman">tv_usec</FONT></B>部分就可能会超出<FONT face="Times New Roman">2</FONT>百万,这段代码就可能在标准化<B normal"><FONT face="Times New Roman">xtime</FONT></B>时失败。但是随后的调用可以把<B normal"><FONT face="Times New Roman">xtime</FONT></B>完全标准化。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">calc_load</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27135</FONT>:<B normal"><FONT face="Times New Roman">calc_load</FONT></B>从定时器的下半部分中调用以更新内核对于当前系统负载的估计值。虽然对于它的介绍有点离题,但是对于每个对这个随处可见的数字是如何计算的感到疑惑的人都会对这个函数很感兴趣,因此它还是值得一看的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27138</FONT>:静态变量<B normal"><FONT face="Times New Roman">count</FONT></B>记录了从上次计算平均负载以来已经经过了多少时间。它被初始化为<B normal"><FONT face="Times New Roman">LOAD_FREQ</FONT></B>,这在<FONT face="Times New Roman">16164</FONT>行中进行宏定义以代表相当于<FONT face="Times New Roman">5</FONT>秒时间的定时器间隔。对于这个函数的每一个调用都要递减遗留到下一次计算负载的定时器跳动数量。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27142</FONT>:当剩余的跳动数小于<FONT face="Times New Roman">0</FONT>时,就应该重新计算了。(我倒是希望这个数是小于或者等于<FONT face="Times New Roman">0</FONT>,而不是仅仅小于<FONT face="Times New Roman">0</FONT>,但是实际上这两个值差不了多少。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27143</FONT>:为了能在另一<FONT face="Times New Roman">5</FONT>秒内可以再次触发,重新初始化<B normal"><FONT face="Times New Roman">count</FONT></B>,此后,<B normal"><FONT face="Times New Roman">count_active_tasks</FONT></B>(<FONT face="Times New Roman">27119</FONT>行)被用来监视系统中当前有多少任务。现在<B normal"><FONT face="Times New Roman">count_active_tasks</FONT></B>的实现可能已经不是什么奇妙的事情了,但是你所有有关它的问题在阅读完第<FONT face="Times New Roman">7</FONT>章后都应该得到解答。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27144</FONT>:在<FONT face="Times New Roman">16169</FONT>行定义的<B normal"><FONT face="Times New Roman">CALC_LOAD</FONT></B>宏用来更新<B normal"><FONT face="Times New Roman">avenrun</FONT></B>数组(<FONT face="Times New Roman">27116</FONT>行)的三个项,它的三个元素分别记录了前面<FONT face="Times New Roman">5</FONT>秒、<FONT face="Times New Roman">10</FONT>秒、<FONT face="Times New Roman">15</FONT>秒的系统负载。与前一章中说明的一样,内核中尽量避免浮点数运算,因此这些计算都是在固定点进行的。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">run_old_timers</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27077</FONT>:内核提供了类似于下半部分的已经不再使用了的技巧,通过这种技巧内核函数可以被登记到表中,和超时时间建立联系,并在指定时间到达时调用。这个函数根据需要调用其它函数。由于现在使用这个函数的唯一目的是为了支持原来的代码,所以我就不仔细地介绍它了。它简单的遍历处理<B normal"><FONT face="Times New Roman">timer_table</FONT></B>数组(<FONT face="Times New Roman">27109</FONT>行)的列表项,如果定时器已经触发就调用相关函数。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">定时器队列</H2><P 0cm 0cm 0pt">定时器队列最初的背景思想是与上半部分相关的下半部分并不一定非要与中断处理有关。相反的,它也可能是我们所需要周期性处理的任何内容。将一个函数定义为定时器中断处理程序下半部分的一部分能够保证这个函数每秒钟大约被调用<FONT face="Times New Roman">100</FONT>次。(如果每秒运行<FONT face="Times New Roman">100</FONT>次太频繁了,那么这个函数可以保持一个计数器并每<FONT face="Times New Roman">10</FONT>次调用都简单返回<FONT face="Times New Roman">9</FONT>次,例如——我们看到<B normal"><FONT face="Times New Roman">calc_load</FONT></B>就是这样处理的。)结果就像是创建了一个进程,它的<B normal"><FONT face="Times New Roman">main</FONT></B>部分在无穷循环中调用这个函数,于是它没有占用通常的和进程相关的开销。</P><P 0cm 0cm 0pt">但是,下半部分是一种有限的资源——只存在<FONT face="Times New Roman">32</FONT>个,这是因为我们希望<B normal"><FONT face="Times New Roman">bh_mask</FONT></B>和<B normal"><FONT face="Times New Roman">bh_active</FONT></B>各自都匹配一个无符号长整型数。我们可以通过使用系统中类似于实现信号量的方法来扩展下半部分的数量,但是这样仅仅能增加静态可用的下半部分的数量——它并不能使我们能够动态地扩展下半部分列表。</P><P 0cm 0cm 0pt">从本质上说,这是定时器队列所提供的内容:一个动态的可增长的下半部分的列表,所有项都和定时器中断有关。这里有一个独立的下半部分以处理这种情况——<B normal"><FONT face="Times New Roman">TQUEUE_BH</FONT></B>——如同你看到的一样,如果定时器队列中有任务,它就和<B normal"><FONT face="Times New Roman">TIMER_BH</FONT></B>一起被标记。因此,定时器中断有两个下半部分。</P><P 0cm 0cm 0pt">定时器队列实际上只是更普通的内核特性——任务队列的一个实例。根据说明文档,任务队列在内核本身中是相当自由的——请参看文件<FONT face="Times New Roman">include/linux/tqueue.h</FONT>从<FONT face="Times New Roman">18527</FONT>行开始的部分。因此,接下来我们就不再介绍它们了。但是,它们是很重要的内核服务,而且那个简短的文件也很值得一读。</P>