数学建模社区-数学中国

标题: [分享]《Linux内核注释》 [打印本页]

作者: ilikenba    时间: 2005-3-4 21:40
标题: [分享]《Linux内核注释》
<  align=center><B >介  绍<p></p></B></P>% l' e, e; ^- G( B
<  align=left><FONT size=3>    《Linux内核注释》旨在给程序员和学生提供比以前更详细和更易理解的Linux内核代码注释。作者分析了核心代码,并对重要的函数、系统调用和数据结构提供了大量的注释。<p></p></FONT></P># F7 j3 w. G, e, O, \4 \! k
<  align=left><FONT size=3>    对《注释》系列丛书的写作灵感都来源于John Lions所著的大量流行的《Lions' Commentary on Unix》一书。无数的计算机专业的学生在复制和使用这本书。这本书对AT&amp;T的Unix操作系统的早期版本的内幕进行了深刻的剖析。<p></p></FONT></P># i+ L* J/ F! ~' Z- k3 Z
<  align=left><FONT size=3>    《Liunx内核注释》同样提供了对流行的功能强大的Liunx操作系统的结构和函数实现的内幕介绍。本书的主要目标是:<p></p></FONT></P>
3 U: X9 @4 ~. o/ l& y* I* J<  align=left><FONT size=3>    1.提供一个最新的和完整的服务器版本的完整源代码。(这本书分析的版本是<st1:chsdate w:st="on" IsROCDate="False" IsLunarDate="False" Day="30" Month="12" Year="1899">2.2.5</st1:chsdate>版,也是写这本书时发布的最新版本。)<p></p></FONT></P>8 F3 m" F3 F( T" q2 w& |
<  align=left><FONT size=3>    2.提供一个对每个子系统功能的一般性概述。<p></p></FONT></P>! w: a' h" I9 Y1 `
<  align=left><FONT size=3>    3.研究各个子系统主要的函数和数据结构。<p></p></FONT></P>  n6 K! U$ X$ y
<  align=left><FONT size=3>    4.对开发者应怎样通过修改源代码来改进和扩展内核提出建议。<p></p></FONT></P>
# {9 h) T' s. \<  align=left><FONT size=3>    本书的最后一项目标<FONT face="Times New Roman">—</FONT>定制--是你学习内核代码的最有说服力的原因。通过理解内核是怎样工作的,你能够编写自己的代码用以在你的操作系统中实现所需要的功能。如果允许其他人共享你的改进,你的代码甚至会在官方发行的内核代码中出现,被全世界数百万计的人们所使用。<p></p></FONT></P>
* G; j: e  s) V8 }  m1 S& D& }<  align=left><FONT size=3>    开放源代码是指让开发者研究源代码并实现功能性扩展。Linux是全世界成长最快的操作系统,开放源代码是其主要的原因之一。从玩游戏,到网上冲浪,到为大大小小的ISP们提供稳定的Web服务器平台以至解决最庞大的科学难题,Linux都能胜任全部工作。它之所以能如此强大是因为有像你一样的开发者在研究、学习并且扩充这个系统。<p></p></FONT></P>
- H: U1 {( ~, i9 [" R8 S5 b5 \<  align=left><B >你能从本书中学到什么</B><B ><p></p></B></P>0 j; R: F  r9 F& Z6 }/ V
<  align=left><FONT size=3>    这本书集中解释了Linux内核源代码的核心中专用代码行是如何运行的。你将学习到内核最内部的子系统是怎样构造和这种构造能够实现系统功能的理由。<p></p></FONT></P>
4 o4 J. X/ \1 }5 m4 q. D- Z<  align=left><FONT size=3>    本书的第一部分以易于阅读和交叉引用的格式复制了一个经过筛选的linux 内核源代码的子集。在这本书稍后的注释中,无论一行代码在何处被引用,你都会在这一行前面发现一个小箭头。这个箭头指出了对此行进行注释处的页号。<p></p></FONT></P>
2 E+ I" p8 k1 W" ^<  align=left><FONT size=3>    源代码后是这本书的第二部分,即注释部分,注释部分对源代码进行了讨论。注释部分的每一章讨论了一个不同的内核子系统,或者是其它的功能性逻辑组件,例如系统调用或内存管理。注释部分大量的行号引用为你指明了所讨论代码行的确切行号。<p></p></FONT></P>7 F" c0 m" p' v  R3 ^4 o# f
<  align=left><FONT size=3>    在本书正文后的附录部分,简洁地覆盖了自本书主要部分完成以后内核的变化。在附录中还包含了被内核用做软件许可证的完整的GNU常规公众许可证。最后,本书为你提供了一个索引。通过该索引你可以查询术语或主题。这将让你更快更有效的使用这本参考工具书。<p></p></FONT></P>
7 f7 N; r" M; [) T8 n6 c$ |% N/ d<  align=left><B >本书的使用对象</B><p></p></P>3 `4 G3 W. c4 s! Z* u# C* K( V
<  align=left><FONT size=3>    本书假设你能阅读C语言的代码,不怕偶尔读一些汇编语言代码。并且你想知道一个快速的、坚固的、可靠的、健壮的、现代的、实用的操作系统是如何工作的。一些读者也许是这样的程序员,他们想为前进中的Linux内核发展工作提供他们自己的改进和添加内容。<p></p></FONT></P>) o/ n8 ^, N+ |$ Z
<  align=left><B >如何使用本书</B><p></p></P>" {2 h! g! S/ e; L
<  align=left><FONT size=3>    用最适合你自己的方法放松地去看这本《linux 内核注释》。因为写这本书的目的是为提供一个参考资料,你不必从头看到尾。因为注释和代码是一一对应的,你可以从另外一个方向接近内核。<p></p></FONT></P>
+ m, b$ s7 B, L( Q+ P; f& r8 C<  align=left><FONT size=3>    欢迎你对我的第一本书提出意见。你可以通过e-mail和我联系。地址是:<a href="mailtlckc@ScottMaxwell.org" target="_blank" >lckc@ScottMaxwell.org</A>。 勘误表、更新和其它一些有用信息可以通过访问 <a href="http://www.scottmaxwell.org/lckc.html" target="_blank" >http://www.ScottMaxwell.org/lckc.html</A> 得到。<p></p></FONT></P># T: L7 C! V" T, v" U$ J' k
<  align=left><p><FONT face="Times New Roman" size=3> </FONT></p></P>
作者: ilikenba    时间: 2005-3-4 21:42
<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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;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&amp;T</FONT>的<FONT face="Times New Roman">Unix</FONT>更具有创新性,而且改进也更为迅速。但是,在<FONT face="Times New Roman">AT&amp;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&amp;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>
作者: ilikenba    时间: 2005-3-4 21:44
<H1 12pt 0cm; TEXT-INDENT: 0cm"><FONT face="Times New Roman">第2章  </FONT>代码初识</H1>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">本章首先从较高层次介绍<FONT face="Times New Roman">Linux</FONT>内核源程序的概况,这些都是大家关心的一些基本特点。随后将简要介绍一些实际代码。最后以如何编译内核来检验个人所进行的修改的讨论来作为本章的收尾。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">Linux内核源程序的部分特点</H2>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">在过去的一段时期,<FONT face="Times New Roman">Linux</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">C++</FONT>的对象特性,部分内核也可以在<FONT face="Times New Roman">g++</FONT>(<FONT face="Times New Roman">GNU</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">Linux</FONT>代码独有的特色。本节将讨论其中的一些问题。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">gcc特性的使用</H2>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">Linux</FONT>内核被设计为必须使用<FONT face="Times New Roman">GNU</FONT>的<FONT face="Times New Roman">C</FONT>编译器<FONT face="Times New Roman">gcc</FONT>来编译,而不是任何一种<FONT face="Times New Roman">C</FONT>编译器都可以使用。内核代码有时要使用<FONT face="Times New Roman">gcc</FONT>特性,伴随着本书的进程,我们将陆续介绍其中的一部分。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">一些<FONT face="Times New Roman">gcc</FONT>特有代码只是简单地使用<FONT face="Times New Roman">gcc</FONT>语言扩展,例如允许在<FONT face="Times New Roman">C</FONT>(不只是<FONT face="Times New Roman">C++</FONT>)中使用<B normal"><FONT face="Times New Roman">inline</FONT></B>关键字指示内联函数。也就是说,代码中被调用的函数在每次函数调用时都会被扩充,因而就可以节约实际函数调用的开销。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">更为普遍的情况是代码的编写方式比较复杂。因为对于某些类型的输入,<FONT face="Times New Roman">gcc</FONT>能够产生比其它输入效率更高的执行代码。从理论上讲,编译器可以优化具有相同功能的两种对等的方法,并且得到相同的结果。因此,代码的编写方式是无关紧要的。但在实际上,用一些方法编写所产生的代码要比用其它方法编写所产生的代码的执行速度快得多。内核开发人员清楚如何才能产生更高效的执行代码的方法,而且这种知识也不断在他们编写的代码中反映出来。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">例如,考虑内核中经常使用的<B normal"><FONT face="Times New Roman">goto</FONT></B>语句——为了提高速度,内核中经常大量使用这种一般要避免使用的语句。在本书中所包含的不到<FONT face="Times New Roman">40,000</FONT>行代码中,一共有<FONT face="Times New Roman">500</FONT>多条<B normal"><FONT face="Times New Roman">goto</FONT></B>语句,大约是每<FONT face="Times New Roman">80</FONT>行一个。除汇编文件外,精确的统计数字是接近每<FONT face="Times New Roman">72</FONT>行一个<B normal"><FONT face="Times New Roman">goto</FONT></B>语句。公平的说,这是选择偏向的结果:比例如此高的原因之一是本书中涉及的是内核源程序的核心,在这里速度比其它因素都需要优先考虑。整个内核的比例大概是每<FONT face="Times New Roman">260</FONT>行一个<B normal"><FONT face="Times New Roman">goto</FONT></B>语句。然而,这仍然是我不再使用<FONT face="Times New Roman">Basic</FONT>进行编程以来见过的使用<B normal"><FONT face="Times New Roman">goto</FONT></B>频率最高的地方。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">代码必需受特定编译器限制的特性不仅与普通应用程序的开发有很大不同,而且也不同于大多数内核的开发。大多数的开发人员使用<FONT face="Times New Roman">C</FONT>语言编写代码来保持较高的可移植性,即使在编写操作系统时也是如此。这样做的优点是显而易见的,最为重要的一点是一旦出现更好的编译器,程序员们可以随时进行更换。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">内核对于<FONT face="Times New Roman">gcc</FONT>特性的完全依赖使得内核向新的编译器上的移植工作更加困难。最近<FONT face="Times New Roman">Linus</FONT>对这一问题在有关内核的邮件列表上表明了自己的观点。“记住,编译器只是一个工具。”这是对依赖于<FONT face="Times New Roman">gcc</FONT>特性的一个很好的基本思想的表述:编译器只是为了完成工作。如果通过遵守标准还不能达到工作要求,那么就不是工作要求有问题,而是对于标准的依赖有问题。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">在大多数情况下,这种观点是不能够被人所接受的。通常情况下,为了保证和程序语言标准的一致,开发人员可能需要牺牲某些特性、速度或者其它相关因素。其它的选择可能会为后期开发造成很大的麻烦。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">但是,在这种特定的情况下,<FONT face="Times New Roman">Linus</FONT>是正确的。<FONT face="Times New Roman">Linux</FONT>内核是一个特例,因为其执行速度要比向其它编译器的可移植性远为重要。如果设计目标是编写一个可移植性好而不要求快速运行的内核,或者是编写一个任何人都可以使用自己喜欢的编译器进行编译的内核,那么结论就可能会有所不同了;而这些恰好不是<FONT face="Times New Roman">Linux</FONT>的设计目标。实际上,<FONT face="Times New Roman">gcc</FONT>几乎可以为所有能够运行<FONT face="Times New Roman">Linux</FONT>的<FONT face="Times New Roman">CPU</FONT>生成代码,因此,对于<FONT face="Times New Roman">gcc</FONT>的依赖并不是可移植性的严重障碍。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">在第<FONT face="Times New Roman">3</FONT>章中我们将对内核设计目标进行详细说明。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">内核代码习惯用语</H2>< 0cm 0cm 0pt">内核代码中使用了一些显著的习惯用语,本节将介绍常用的几个。当你通读源程序代码时,真正重要的问题是并不在这些习惯用语本身,而是这种类型的习惯用语的确存在,而且是不断被使用和发展的。如果你需要编写内核代码,你应该注意到内核中所使用的习惯用语,并把这些习惯用语应用到你的代码中。当通读本书(或者代码)时,注意你还能找到多少习惯用语。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">为了讨论这些习惯用语,我们首先需要对它们进行命名。为了便于讨论,笔者创造了这些名字。而在实际中,大家不一定非要参考这些用语,它们只是对内核工作方式的描述而已。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">一个普通的习惯用语笔者称之为“资源获取”(<FONT face="Times New Roman">resource acquisition idiom</FONT>)。在这个用语中,一个函数必须实现一系列资源的获取,包括内存、锁等等(这些资源的类型未必相同)。只有成功地获取当前所需要的资源之后,才能处理后面的资源请求。最后,该函数还必须释放所有已经获取的资源,而不必对没有获取的资源进行考虑。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">我采用“错误变量”这一用语(<FONT face="Times New Roman">error variable idiom</FONT>)来辅助说明资源获取用语,它使用一个临时变量来记录函数的期望返回值。当然,相当多的函数都能实现这个功能。但是错误变量的不同点在于它通常是用来处理由于速度的因素而变得非常复杂的流程控制中的问题。错误变量有两个典型的值,<FONT face="Times New Roman">0</FONT>(表示成功)和负数(表示有错)。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">这两个用语结合使用,我们就可以十分自然地得到符合模式的代码如下:</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><p><FONT face="Times New Roman"> </FONT></p></P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">Int f (void)<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">{<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">  int err;<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">  resource *r1, *r2;<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">  err = -ERR1   /*assume failure*/<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">  r1=acquire_ resource();<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">  if (!r1)       /*not aquired*/<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">     goto out      /*returns  -ERR1*/<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><p><FONT face="Times New Roman"> </FONT></p></P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">  Got resource r1,try for r2.*/<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">  err = - ERR2;<p></p></FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">  r2 = acquire_ resource2();<p></p></FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">  if (!r2)       /*not aquired*/<p></p></FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">     goto out1      /*returns –ERR2*/<p></p></FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">  /*have both r1 and r2.*/<p></p></FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">  err = 0;<p></p></FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">  <p></p></FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">  /* … use r1 and r2 … */<p></p></FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">out2:<p></p></FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">  release_resource(r2)<p></p></FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">out1:<p></p></FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">  release_resource(r1)<p></p></FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">out:<p></p></FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">    return err;<p></p></FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">}<p></p></FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">  </FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">(注意变量<B normal"><FONT face="Times New Roman">err</FONT></B>是使用错误变量的一个明确实例,同样,诸如<B normal"><FONT face="Times New Roman">out</FONT></B>之类的标号则指明了资源获取用语的使用。)</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">如果执行到标号<B normal"><FONT face="Times New Roman">out2</FONT></B>,则都已经获取了<B normal"><FONT face="Times New Roman">r1</FONT></B>和<B normal"><FONT face="Times New Roman">r2</FONT></B>资源,而且也都需要进行释放。如果执行到标号<B normal"><FONT face="Times New Roman">out1</FONT></B>(不管是顺序执行还是使用<B normal"><FONT face="Times New Roman">goto</FONT></B>语句进行跳转到),则<B normal"><FONT face="Times New Roman">r2</FONT></B>资源是无效的(也可能刚被释放),但是<B normal"><FONT face="Times New Roman">r1</FONT></B>资源却是有效的,而且必需在此将其释放。同理,如果标号<B normal"><FONT face="Times New Roman">out</FONT></B>能被执行,则<B normal"><FONT face="Times New Roman">r1</FONT></B>和<B normal"><FONT face="Times New Roman">r2</FONT></B>资源都无效,<B normal"><FONT face="Times New Roman">err</FONT></B>所返回的是错误或成功标志。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">在这个简单的例子中,对于<B normal"><FONT face="Times New Roman">err</FONT></B>的一些赋值是没有必要的。在实践中,实际代码必须遵守这种模式。这样做的原因主要在于同一行中可能包含有多种测试,而这些测试应该返回相同的错误代码,因此对错误变量统一赋值要比多次赋值更为简单。虽然在这个例子中对于这种属性的必要性并不非常迫切,但是我还是倾向于保留这种特点。有关的实际应用可以参考<B normal"><FONT face="Times New Roman">sys_shmctl</FONT></B>(第<FONT face="Times New Roman">21654</FONT>行),在第<FONT face="Times New Roman">9</FONT>章中还将详细介绍这个例子。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">减少#if和#ifdef的使用</H2><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">现在的<FONT face="Times New Roman">Linux</FONT>内核已经移植到不同的平台上,但是我们还必须解决移植过程中所出现的问题。大部分支持各种不同平台的代码由于包含许多预处理代码现都已变得非常不规范,例如:</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">#if defined(SOLARIS)</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">/* … do things the solaris way … */</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">#elif defined(HPUX)</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">/* … do things the HP-UX way … */</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">#elif defined(LINUX)</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">/* … do things the right way … */</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">#endif</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">这个例子试图实现操作系统的可移植性,虽然<FONT face="Times New Roman">Linux</FONT>关注的焦点很明显是实现代码在各种<FONT face="Times New Roman">CPU</FONT>上的可移植性,但是二者的基本原理是一致的。对于这类问题来说,预处理器是一种错误的解决方式。这些杂乱的问题使得代码晦涩难懂。更为糟糕的是,增加对新平台的支持有可能要求重新遍历这些杂乱分布的低质量代码段(实际上你很难能找到这类代码段的全部)。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">与现有方式不同的是,<FONT face="Times New Roman">Linux</FONT>一般通过简单函数(或者是宏)调用来抽象出不同平台间的差异。内核的移植可以通过实现适合于相应平台的函数(或宏)来实现。这样不仅使代码的主体简单易懂,而且在移植的过程中还可以比较容易地自动检测出你没有注意到的内容:如引用未声明函数时会出现链接错误。有时用预处理器来支持不同的体系结构,但这种方式并不常用,而相对于代码风格的变化就更是微不足道了。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">顺便说一下,我们可以注意到这种解决方法和使用用户对象(或者<FONT face="Times New Roman">C</FONT>语言中充满函数指针的<B normal"><FONT face="Times New Roman">struct</FONT></B>结构)来代替离散的<B normal"><FONT face="Times New Roman">switch</FONT></B>语句处理不同类型的方法十分相似。在某些层次上,这些问题和解决方法是统一的。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">可移植性的问题并不仅限于平台和<FONT face="Times New Roman">CPU</FONT>的移植,编译器也是一个重要的问题。此处为了简化,假设<FONT face="Times New Roman">Linux</FONT>只使用<FONT face="Times New Roman">gcc</FONT>来编译。由于<FONT face="Times New Roman">Linux</FONT>只使用同一个编译器,所以就没有必要使用<B normal"><FONT face="Times New Roman">#if</FONT></B>块(或者<B normal"><FONT face="Times New Roman">#ifdef</FONT></B>块)来选择不同的编译器。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">内核代码主要使用<B normal"><FONT face="Times New Roman">#ifdef</FONT></B>来区分需要编译或不需要编译的部分,从而对不同的结构提供支持。例如,代码经常测试<B normal"><FONT face="Times New Roman">SMP</FONT></B>宏是否定义过,从而决定是否支持<FONT face="Times New Roman">SMP</FONT>机。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">代码样例</H2><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">上一节仅仅是一些讨论,了解<FONT face="Times New Roman">Linux</FONT>代码风格最好的方法就是实际研究一下它的部分代码。即使你不完全理解本节所讨论代码的细节也无关紧要,毕竟本节的主要目的不是理解代码,一些读者可以只对本节进行浏览。本节的主要目的是让读者对<FONT face="Times New Roman">Linux</FONT>代码进行初步了解,对今后的工作提供必要基础。而讨论将涉及部分广泛使用到的内核代码。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT face="Times New Roman" size=5>printk</FONT></H3><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><B normal"><FONT face="Times New Roman">printk</FONT></B>(<FONT face="Times New Roman">25836</FONT>行)是内核内部消息日志记录函数。在出现诸如内核检测到其数据结构出现不一致的事件时,内核会使用<B normal"><FONT face="Times New Roman">printk</FONT></B>把相关信息打印到系统控制台上。对于<B normal"><FONT face="Times New Roman">printk</FONT></B>的调用一般分为如下几类:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo2; tab-stops: list 42.25pt">l         紧急事件(<FONT face="Times New Roman">emergency</FONT>)――例如,<B normal"><FONT face="Times New Roman">panic</FONT></B>函数(<FONT face="Times New Roman">25563</FONT>行)多次使用了<B normal"><FONT face="Times New Roman">printk</FONT></B>。当内核检测到发生不可恢复的内部错误时就会调用<B normal"><FONT face="Times New Roman">panic</FONT></B>函数,然后尽其所能的安全关闭计算机。这个函数中调用<B normal"><FONT face="Times New Roman">printk</FONT></B>以提示用户系统将要关闭。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo2; tab-stops: list 42.25pt">l         调试――从<FONT face="Times New Roman">3816</FONT>行开始的<B normal"><FONT face="Times New Roman">#ifdef</FONT></B>块使用<B normal"><FONT face="Times New Roman">printk</FONT></B>来打印<FONT face="Times New Roman">SMP</FONT>逻辑单元(<FONT face="Times New Roman">box</FONT>)中每一个处理器的相关配置信息,但是此过程只有在使用<B normal"><FONT face="Times New Roman">SMP_DEBUG</FONT></B>标志编译代码的情况下才能够被执行。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo2; tab-stops: list 42.25pt">l         普通信息――例如,当机器启动时,内核必需估计系统速度以确保设备驱动程序能够忙等待(<FONT face="Times New Roman">busy-waiting</FONT>)一个精确的极短周期。计算这种估计值的函数名为<B normal"><FONT face="Times New Roman">calibrate_delay</FONT></B>(<FONT face="Times New Roman">19654</FONT>行),它既在<FONT face="Times New Roman">19661</FONT>行使用<B normal"><FONT face="Times New Roman">printk</FONT></B>声明马上开始计算,又在<FONT face="Times New Roman">19693</FONT>行报告计算结果。另外,在第<FONT face="Times New Roman">4</FONT>章将详细的介绍<B normal"><FONT face="Times New Roman">calibrate_delay</FONT></B>函数。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21pt">如果你已经浏览过这些参照代码,你可能已经注意到<B normal"><FONT face="Times New Roman">printk</FONT></B>和<B normal"><FONT face="Times New Roman">printf</FONT></B>的参数十分类似:一个格式化字符串,后跟零个或者多个参数加入字符串中。格式化字符串可能是以一组“<FONT face="Times New Roman">&lt;N&gt;</FONT>”开始,这里的<FONT face="Times New Roman">N</FONT>是从<FONT face="Times New Roman">0</FONT>到<FONT face="Times New Roman">7</FONT>的数字,包括<FONT face="Times New Roman">0</FONT>和<FONT face="Times New Roman">7</FONT>在内。数字区分了消息的日志等级(<FONT face="Times New Roman">log level</FONT>),只有当日志等级高于当前控制台定义的日志等级(<B normal"><FONT face="Times New Roman">console_loglevel</FONT></B>,<FONT face="Times New Roman">25650</FONT>行)时,才会打印消息。<FONT face="Times New Roman">root</FONT>可以通过适当减小控制台的日志等级来过滤不是很紧急的消息。如果内核在格式化字符串中检测不到日志等级序列,那么就会一直打印消息。(实际上,日志等级序列并不一定要在格式化字符串中出现,可以在格式化文本中查找到它的代码。)</P><P 0cm 0cm 0pt; TEXT-INDENT: 21pt">从<FONT face="Times New Roman">14946</FONT>行开始的<B normal"><FONT face="Times New Roman">#define</FONT></B>块说明了这些特殊序列,这些定义可以帮助调用者正确区分对<B normal"><FONT face="Times New Roman">printk</FONT></B>的调用。简单的说,我称日志等级<FONT face="Times New Roman">0</FONT>到<FONT face="Times New Roman">4</FONT>为“紧急事件”,从等级<FONT face="Times New Roman">5</FONT>到等级<FONT face="Times New Roman">6</FONT>为“普通信息”,等级<FONT face="Times New Roman">7</FONT>自然就是我所说的“调试”。(这种分类方法并不意味着其它更好的分类方法没有用处,而只是目前我们还不关心它而已。)</P><P 0cm 0cm 0pt; TEXT-INDENT: 21pt">在上面讨论的基础上,我们研究一下代码本身。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">printk</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25836</FONT>:参数<B normal"><FONT face="Times New Roman">fmt</FONT></B>是<B normal"><FONT face="Times New Roman">printf</FONT></B>类型的格式化字符串。如果你对“<FONT face="Times New Roman">…</FONT>”部分的内容不熟悉,那就<FONT face="Times New Roman">             </FONT>需要参阅一本好的<FONT face="Times New Roman">C</FONT>语言参考书(在其索引中查找“变参函数,<FONT face="Times New Roman">variadic function</FONT>”)。另外,在安装的<FONT face="Times New Roman">GNU/Linux</FONT>中的<B normal"><FONT face="Times New Roman">stdarg</FONT></B>帮助里也包含了一个有关变参函数的简明描述,在这儿只需要敲入“<FONT face="Times New Roman">man stdarg</FONT>”就可以看到。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>简单的说,“<FONT face="Times New Roman">…</FONT>”部分提示编译器<B normal"><FONT face="Times New Roman">fmt</FONT></B>后面可能紧跟着数量不定的任何类型的参数。由于这些参数在编译的时候还没有类型和名字,内核使用由三个宏<B normal"><FONT face="Times New Roman">va_start</FONT></B>,<B normal"><FONT face="Times New Roman">va_arg</FONT></B>和<B normal"><FONT face="Times New Roman">va_end</FONT></B>组成的特殊组以及一个特殊类型――<B normal"><FONT face="Times New Roman">va_list</FONT></B>对它们进行处理。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25842</FONT>:<B normal"><FONT face="Times New Roman">msg_level</FONT></B>记录了当前消息的日志等级。它是静态的,这看起来可能会有些奇怪――为什么下一次对<B normal"><FONT face="Times New Roman">printk</FONT></B>的调用需要记录日志等级呢?问题的答案是只有打印出新行(<FONT face="Times New Roman">\n</FONT>)或者赋给一个新的日志等级序列以后,当前消息才会结束。这样通过在包含消息结束的新行里调用<B normal"><FONT face="Times New Roman">printk</FONT></B>,就保证了在多个短期冲突的情况下,调用者只打印唯一一个长消息。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25845</FONT>:在<FONT face="Times New Roman">SMP</FONT>逻辑单元中,内核可能试图从不同的<FONT face="Times New Roman">CPU</FONT>向控制台同时打印信息。(有时在单处理机(<FONT face="Times New Roman">UP</FONT>)逻辑单元中也会发生同样问题,但由于中断还未被覆盖掉,所以问题也并不十分明显。)如果不进行任何协同的话,结果就将处于完全无法让人了解的杂乱无章的状态,每个消息的各个部分都和其它消息的各个部分混杂交织在一起。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm">相反,内核使用旋转锁(<FONT face="Times New Roman">spin-lock</FONT>)来控制对控制台的访问。旋转锁将在第<FONT face="Times New Roman">10</FONT>章对它进行深入的介绍。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm">如果你对<B normal"><FONT face="Times New Roman">flags </FONT></B>在传送给<B normal"><FONT face="Times New Roman">spin_lock_irqsave</FONT></B>之前为什么不对它初始化感到疑惑,请不要担心:<B normal"><FONT face="Times New Roman">spin_lock_irqsave</FONT></B>(对于不同的版本请分别参看<FONT face="Times New Roman">12614</FONT>行,<FONT face="Times New Roman">12637</FONT>行,<FONT face="Times New Roman">12716</FONT>行,和<FONT face="Times New Roman">12837</FONT>行)是一个宏,而不是一个函数。该宏实际上是将值写入<B normal"><FONT face="Times New Roman">flags</FONT></B>中,而不是从<B normal"><FONT face="Times New Roman">flags</FONT></B>中读出值。(在<FONT face="Times New Roman">25895</FONT>行中,存储在<B normal"><FONT face="Times New Roman">flags</FONT></B>中的信息被<B normal"><FONT face="Times New Roman">spin_lock_irqsave</FONT></B>回读,请参看<FONT face="Times New Roman">12616</FONT>行,<FONT face="Times New Roman">12639</FONT>行,<FONT face="Times New Roman">12728</FONT>行和<FONT face="Times New Roman">12841</FONT>行)</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">25846</FONT>:初始化变量<B normal"><FONT face="Times New Roman">args</FONT></B>,该变量代表<B normal"><FONT face="Times New Roman">printk</FONT></B>参数中的“<FONT face="Times New Roman">…</FONT>”部分。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25848</FONT>:调用内核自身的<B normal"><FONT face="Times New Roman">vsprintf</FONT></B>(为节省空间而省略)实现。该函数的功能与标准<B normal"><FONT face="Times New Roman">vsprintf</FONT></B>函数非常相似,向<B normal"><FONT face="Times New Roman">buf</FONT></B>中写入格式化文本(<FONT face="Times New Roman">25634</FONT>行)并返回写入字符串的长度(长度不包括最后一位终止字符<FONT face="Times New Roman">0</FONT>字节)。很快,你将可以看到为什么这种机制会忽略<B normal"><FONT face="Times New Roman">buf</FONT></B>的前三个字符。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm">(正如<FONT face="Times New Roman">25847</FONT>行的注释中所述)我们应该注意到在这里并没有采取严格的措施来保证缓冲器不会过载。这里系统假定<FONT face="Times New Roman">1024</FONT>个字符长度的<B normal"><FONT face="Times New Roman">buf</FONT></B>已经足够使用(参阅<FONT face="Times New Roman">25634</FONT>行)。如果内核在这里能够使用<B normal"><FONT face="Times New Roman">vsnprintf</FONT></B>函数的话,情况就会好许多。然而,<B normal"><FONT face="Times New Roman">vsnprintf</FONT></B>还有另外一个参数限制了它能够写入缓冲器的字符长度。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">25849</FONT>:计算<B normal"><FONT face="Times New Roman">buf</FONT></B>中最近使用的元素,调用<B normal"><FONT face="Times New Roman">va_end</FONT></B>终止对“<FONT face="Times New Roman">…</FONT>”参数的处理。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25851</FONT>:开始格式化消息的循环。其中存在一个内部循环能够处理更多内容(这一点随后就能看到),因此,每次内循环开始,都开始一个新的打印行。由于通常情况下<B normal"><FONT face="Times New Roman">printk</FONT></B>只用于打印单行,所以在每次调用中这种循环通常只执行一次。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25853</FONT>:如果预先不知道消息的日志等级,<B normal"><FONT face="Times New Roman">printk</FONT></B>会检查当前行是否以日志等级序列开头。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25860</FONT>:如果不是,<B normal"><FONT face="Times New Roman">buf</FONT></B>中开始未使用的三个字符就能够起作用了。(第一次以后的每次循环,都会覆盖部分消息文本,但是这样并不会引起问题,因为这里的文本只是前面行中的一部分,它们已经被打印过,而且以后也不再需要了。)这样,就可以将日志等级插入<B normal"><FONT face="Times New Roman">buf</FONT></B>中。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25866</FONT>:此处有如下属性:<B normal"><FONT face="Times New Roman">p</FONT></B>指向日志等级序列(消息文本紧随其后),<B normal"><FONT face="Times New Roman">msg</FONT></B>指向消息文本——请注意<FONT face="Times New Roman">25852</FONT>行和<FONT face="Times New Roman">25865</FONT>行中对<B normal"><FONT face="Times New Roman">msg</FONT></B>的赋值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>由于已知<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">msg_level</FONT></B>中。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25868</FONT>:没有检测到新行,清空<B normal"><FONT face="Times New Roman">line_feed</FONT></B>标志。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25869</FONT>:这是前面谈到过的内循环,循环将运行到本行结束(也就是检测到新行标志)或者缓冲器的末尾为止。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25870</FONT>:除了将消息打印到控制台之外,<B normal"><FONT face="Times New Roman">printk</FONT></B>还能够记录最近打印的长度为<B normal"><FONT face="Times New Roman">LOG_BUF_LEN</FONT></B>的字符组。(<B normal"><FONT face="Times New Roman">LOG_BUF_LEN</FONT></B>为<FONT face="Times New Roman">16K</FONT>,请参看<FONT face="Times New Roman">25632</FONT>行。)如果在控制台打开之前,内核就已经调用<B normal"><FONT face="Times New Roman">printk</FONT></B>,则显然不能在控制台上正确打印消息,但是这些消息将被尽可能的存储到<B normal"><FONT face="Times New Roman">log_buf</FONT></B>中(<FONT face="Times New Roman">25656</FONT>行)。当控制台打开以后,缓存在<B normal"><FONT face="Times New Roman">log_buf</FONT></B>中的数据就可以转储并在控制台上打印出来,请参看<FONT face="Times New Roman">25988</FONT>行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            <B normal">log_buf</B></FONT>是一个循环缓冲器,<B normal"><FONT face="Times New Roman">log_start</FONT></B>和<B normal"><FONT face="Times New Roman">log_size</FONT></B>变量(<FONT face="Times New Roman">25657</FONT>行和<FONT face="Times New Roman">25646</FONT>行)分别记录当前缓冲器的开始位置和长度。本行中的按位与(<FONT face="Times New Roman">AND</FONT>)操作实际上是快速求模(%)运算,它的正确性依赖于<B normal"><FONT face="Times New Roman">LOG_BUF_LEN</FONT></B>的值是<FONT face="Times New Roman">2</FONT>的幂。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25872</FONT>:保存变量跟踪记录循环日志的值。显然,日志大小会不断增长,直至达到<B normal"><FONT face="Times New Roman">LOG_BUF_LEN</FONT></B>的值为止。此后,<B normal"><FONT face="Times New Roman">log_size</FONT></B>将保持不变,而插入新字符将导致<B normal"><FONT face="Times New Roman">log_start</FONT></B>的增长。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25878</FONT>:请注意<B normal"><FONT face="Times New Roman">logged_chars</FONT></B>(<FONT face="Times New Roman">25658</FONT>行)记录从机器启动之后<B normal"><FONT face="Times New Roman">printk</FONT></B>写入的所有字符的长度,它在每次循环中都会被更新,而不是在循环结束后才改变一次。基于同样的道理,<B normal"><FONT face="Times New Roman">log_start</FONT></B>和<B normal"><FONT face="Times New Roman">log_size</FONT></B>的处理方式也是一样。这实际上是一种优化的时机,但是我们将在结束对函数的介绍之后再对它详细讨论。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25879</FONT>:消息被分为若干行,这当然要使用新行标志符来进行分割。一旦内核检测到新行标志符,就写入一个完整行,从而内循环的执行也可以提前终止。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25884</FONT>:在这里我们先不考虑内部循环是否会提前退出,从<B normal"><FONT face="Times New Roman">msg</FONT></B>到<B normal"><FONT face="Times New Roman">p</FONT></B>的字符序列是专门提供给控制台使用的。(这种字符序列我称之为行,但是不要忘了,这里的行可能并不意味着新行终止,因为<B normal"><FONT face="Times New Roman">buf</FONT></B>也许还没有终止。)如果该行的日志等级高于系统控制台定义的日志等级,而且当前又有控制台可供打印,那么就能够正确打印该行。(记住,<B normal"><FONT face="Times New Roman">printk</FONT></B>可能在所有控制台打开之前就已经被调用过了。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>如果在该信息块中没有发现日志等级序列,并且在前面的<B normal"><FONT face="Times New Roman">printk</FONT></B>调用中也没有对<B normal"><FONT face="Times New Roman">msg_level</FONT></B>赋值,那么本行中的<B normal"><FONT face="Times New Roman">msg_level</FONT></B>就是<FONT face="Times New Roman">-1</FONT>。由于<B normal"><FONT face="Times New Roman">console_leglevel</FONT></B>总不小于<FONT face="Times New Roman">1</FONT>(除非<FONT face="Times New Roman">root</FONT>通过<FONT face="Times New Roman">sysctl</FONT>接口锁定),于是总是可以打印这些行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25886</FONT>:本行应该能够被打印。<B normal"><FONT face="Times New Roman">printk</FONT></B>通过遍历打开的控制台驱动链表告知每一个控制台驱动去打印当前行。(因为虽然设备驱动在本书的讨论范围之外,但是控制台驱动代码则并不包含在内。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25888</FONT>:请注意这里消息文本的开头使用的是<B normal"><FONT face="Times New Roman">msg</FONT></B>而不是<B normal"><FONT face="Times New Roman">p</FONT></B>,这样就在没有日志等级序列的情况下写入消息了。然而,日志等级序列已经被存储到<B normal"><FONT face="Times New Roman">log_buf</FONT></B>缓冲器中了。这样就可以使后来访问<B normal"><FONT face="Times New Roman">log_buf</FONT></B>以获取信息日志等级的代码(请参看<FONT face="Times New Roman">25998</FONT>行)能够正确执行,不会再产生显示混乱信息序列的现象。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25892</FONT>:如果内层<B normal"><FONT face="Times New Roman">for</FONT></B>循环发现一新行,那么<B normal"><FONT face="Times New Roman">buf</FONT></B>中的剩余字符(如果有的话)将被认为是新的消息,因此<B normal"><FONT face="Times New Roman">msg_level</FONT></B>会被重置。但是无论怎样,外层循环都会持续到<B normal"><FONT face="Times New Roman">buf</FONT></B>清空为止。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25895</FONT>:释放在<FONT face="Times New Roman">25845</FONT>行获取的控制台锁(<FONT face="Times New Roman">console lock</FONT>)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25896</FONT>:唤醒等待被写入控制台日志的所有进程。注意即使没有文本被实际写入任何控制台,这个过程也仍然会发生。这样处理是正确的,因为无论是否要往控制台中写入文本,等待进程实际上都是在等待从<B normal"><FONT face="Times New Roman">log_buf</FONT></B>中读出信息。在<FONT face="Times New Roman">25748</FONT>行,进程被转入休眠状态以等待<B normal"><FONT face="Times New Roman">log_buf</FONT></B>的活动。在休眠、唤醒和等待队列中所使用的机制将在下一节中进行讨论。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">25897</FONT>:返回日志中写入的字符长度。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       </FONT>如果对于每个字符的处理工作都能减少一点,那么从<FONT face="Times New Roman">25869</FONT>行开始的<B normal"><FONT face="Times New Roman">for</FONT></B>循环就能执行得更快一点。当循环存在时,我们可以通过只在循环退出时将<B normal"><FONT face="Times New Roman">logged_chars</FONT></B>更新一次来稍微提高运行速度。然而我们还可以通过其它努力来提高速度。由于我们可以预知消息的长度,因此<B normal"><FONT face="Times New Roman">log_size</FONT></B>和<B normal"><FONT face="Times New Roman">log_start</FONT></B>可以到最后再增长。让我们来实验一下这样能否提高速度,下面是一段经过理想优化的代码:</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       do {</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">static int wrapped = 0;</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">const int x = wrapped</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">  ? log_start</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">  : log_size;</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">const int lim = LOG_BUF_LEN – x;</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">int n = buf_end – p;</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">if ( n&gt;= lim)</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">  n = lim;</FONT></P><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">memcpy(log_buf + x, p, n);</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">p += n;</FONT></P><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">if(log_size &lt; LOG_BUF_LEN)</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">  log_size += n;</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">else {</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">  wrapped = 1;</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">  log_start += n;</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">  ;og_start &amp;= LOG_BUF_LEN – 1;</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">}</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">} while (p &lt; buf_end);</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       </FONT>请注意循环通常只需要执行一次,只有在<B normal"><FONT face="Times New Roman">log_buf</FONT></B>末尾写入信息需要折行时才会多次执行。因而<B normal"><FONT face="Times New Roman">log_size</FONT></B>和<B normal"><FONT face="Times New Roman">log_buf</FONT></B>只需要更新一次(或者当写入需要换行时是两次)。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       </FONT>这时速度的确提高了,但是有两个原因使我们并不能这样做。首先,内核可能有自己特有的<B normal"><FONT face="Times New Roman">memcpy</FONT></B>函数,我们必须确保对<B normal"><FONT face="Times New Roman">memcpy</FONT></B>的调用不会再次进入对<FONT face="Times New Roman">printk</FONT>的调用。(有一部分内核移植版定义了自己特有的速度较快的<B normal"><FONT face="Times New Roman">memcpy</FONT></B>函数版本,因此所有的移植都要在这一点上保持一致。)如果<B normal"><FONT face="Times New Roman">memecpy</FONT></B>调用<B normal"><FONT face="Times New Roman">printk</FONT></B>来报告失败,那么就有可能触发无限循环。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       </FONT>然而在这一点上也并不是真的无药可救。使用这种解决方案的最大问题在于该内核循环的形式中也要留意新行标志符,因此使用<B normal"><FONT face="Times New Roman">memcpy</FONT></B>将整个消息拷贝到<B normal"><FONT face="Times New Roman">log_buf</FONT></B>中是不正确的:如果此处存在新行,我们将无法对其进行处理。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       </FONT>我们可以试验一个一箭双雕的办法。下面这种替代的尝试虽然可能比前面那种初步解决方法速度要慢,但是它保持了内核版本的语意:</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">/* in declaration section:*/</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">int n;</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">char *start;</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">static char *log = log_buf;</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">/*……*/</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">for (start = p;p &lt; buf_end;p++) {</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">  *log++ = *p;</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">  if  (log &gt;= (log_buf + LOG_BUF_LEN))</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">log = log_buf ;  /* warp*/</FONT></P><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">  if (*p == ‘/n’) {</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">line_feed = 1;</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">break;</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">  }</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">}</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">/* p - start is number of chars copied. */</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">n = p – start;</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">logged_chars += n ;</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">/*</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">*exercise for the reader:</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">*also use n to update log</FONT> <FONT face="Times New Roman">size and log_ start.</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">*(it’s not as simple as may look.)</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">*/</FONT></P><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       </FONT>(请注意<FONT face="Times New Roman">gcc</FONT>的优化器十分灵敏,它足以能检测到循环内部的表达式<B normal"><FONT face="Times New Roman">log_buf+LOG_BUF_LEN</FONT></B>并没有改变,因此在上面的循环中试图手工加速计算是没有任何效果的。)</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       </FONT>不幸的是,这种方法并不能比现在内核版本在速度上快许多,而且那样会使得代码晦涩难懂(如果你编写过更新<B normal"><FONT face="Times New Roman">log_size</FONT></B>和<B normal"><FONT face="Times New Roman">log_start</FONT></B>的代码,你就能清楚地了解这一点)。你可以自己决定这种折衷是否值得。然而无论怎样,我们学到了一些东西,这是通常的成果:不管成功与否,改进内核代码都可以加深你对内核工作原理的理解。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5>等待队列</FONT></H3><P 0cm 0cm 0pt">前一节我们曾简要的提到进程(也就是正在运行的程序)可以转入休眠状态以等待某个特定事件,当该事件发生时这些进程能够被再次唤醒。内核实现这一功能的技术要点是把等待队列(<FONT face="Times New Roman">wait queue</FONT>)和每一个事件联系起来。需要等待事件的进程在转入休眠状态后插入到队列中。当事件发生之后,内核遍历相应队列,唤醒休眠的任务让它投入运行状态。任务负责将自己从等待队列中清除。</P><P 0cm 0cm 0pt">等待队列的功能强大得令人吃惊,它们被广泛应用于整个内核中。更重要的是,实现等待队列的代码量并不大。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">wait_queue结构</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">18662</FONT>:简单的数据结构就是等待队列节点,它包含两个元素:</P><P 0cm 0cm 0pt 64pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo3; tab-stops: list 64.0pt">l         <B normal"><FONT face="Times New Roman">tast</FONT></B>――指向<B normal"><FONT face="Times New Roman">struct task_struct</FONT></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>章中进行介绍。</P><P 0cm 0cm 0pt 64pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo3; tab-stops: list 64.0pt">l         <B normal"><FONT face="Times New Roman">next</FONT></B>――指向队列中下一节点的指针。因而,等待队列实际上是一个单链表。</P><P 0cm 0cm 0pt">通常,我们用指向等待队列队首的指针来表示等待队列。作为一个例子,请参看<B normal"><FONT face="Times New Roman">printk</FONT></B>使用的等待队列<B normal"><FONT face="Times New Roman">log_wait</FONT></B>(<FONT face="Times New Roman">25647</FONT>行)。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">wait_event</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16840</FONT>:通过使用这个宏,内核代码能够使当前执行的进程在等待队列<B normal"><FONT face="Times New Roman">wq</FONT></B>中等待直至给定<B normal"><FONT face="Times New Roman">condition</FONT></B>(可能是任何的表达式)得到满足。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16842</FONT>:如果条件已经为真,当前进程显然也就无需等待了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16844</FONT>:否则,进程必须等待给定条件转变为真。这可以通过调用<B normal"><FONT face="Times New Roman">__wait_event</FONT></B>来实现(<FONT face="Times New Roman">16824</FONT>行),我们将在下一节介绍它。由于<FONT face="Times New Roman"> <B normal">__wait_event</B></FONT>已经同<B normal"><FONT face="Times New Roman">wait_event</FONT></B>分离,已知条件为假的部分内核代码可以直接调用<B normal"><FONT face="Times New Roman">__wait_queue</FONT></B>,而不用通过宏来进行冗余的(特别是在这些情况下)测试,实际上也没有代码会真正这样处理。更为重要的是,如果条件已经为真,<B normal"><FONT face="Times New Roman">wait_event</FONT></B>会跳过将进程插入等待队列的代码。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       </FONT>注意<FONT face="Times New Roman">wait_event</FONT>的主体是用一个比较特殊的结构封闭起来的:</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       do {</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">         /* … */</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       } while (0)       </FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       </FONT>使我惊奇的是,这个小技巧并没有得到应有的重视。这里的主要思路是使被封闭的代码能够像一个单句一样使用。考虑下面这个宏,该宏的目的是如果<B normal"><FONT face="Times New Roman">p</FONT></B>是一个非空指针,则调用<B normal"><FONT face="Times New Roman">free</FONT></B>:</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       #define FREE1(p)  if  (p)  free (p)</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm">除非你在如下所述的情况下使用<B normal"><FONT face="Times New Roman">FREE1</FONT></B>,否则所有调用都是正确有效的:</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       if  (expression)</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">              FREE1(p)</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       else</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">              printf(“expression was false.\n”) ;  </FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><B normal"><FONT face="Times New Roman">FREE1</FONT></B>经扩展以后,<B normal"><FONT face="Times New Roman">else</FONT></B>就和错误的<B normal"><FONT face="Times New Roman">if</FONT></B>――<B normal"><FONT face="Times New Roman">FREE1</FONT></B>的<B normal"><FONT face="Times New Roman">if</FONT></B>――联系在一起。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       </FONT>我曾经发现有些程序员通过如下途径解决这种问题:</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       #define FREE2(p)  if (p) { free(p); }</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       #define FREE3(p)  { if (p) { free(p); } }</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm">这两种方法都不尽人意,程序员在调用宏以后自然而然使用的分号会把扩展信息弄乱。以<B normal"><FONT face="Times New Roman">FREE2</FONT></B>为例,在宏展开之后,为了使编译器能更准确的识别,我们还需要进行一定的缩进调节,最终代码如下所示:</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">if (expression)</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">  if (p) { free(p);}</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">else</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">  printf(“expression was false./n”);</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm">这样就会引起语法错误――<B normal"><FONT face="Times New Roman">else</FONT></B>和任何一个<B normal"><FONT face="Times New Roman">if</FONT></B>都不匹配。<B normal"><FONT face="Times New Roman">FREE3</FONT></B>从本质上讲也存在同样的问题。而且在研究问题产生原因的同时,你也能够明白为什么宏体里是否包含<B normal"><FONT face="Times New Roman">if</FONT></B>是无关紧要的。不管宏体内部内容如何,只要你使用一组括号来指定宏体,你就会碰到相同的问题。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       </FONT>这里是我们能够引入<B normal"><FONT face="Times New Roman">do/while(0)</FONT></B>技巧的地方。现在我们可以编写<B normal"><FONT face="Times New Roman">FREE4</FONT></B>,它能够克服前面所出现的所有问题。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">#define FREE4(P)  \</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">do {             \</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">if (p)        \</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">  free(p);    \ </FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">while (0)</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman"> <p></p></FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">将<B normal"><FONT face="Times New Roman">FREE4</FONT></B>和其它宏一样插入相同代码之后,宏展开后其代码如下所示(为清晰起见,我们再次调整了缩进格式):</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">if (expression)</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">  do {</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">if (p)        </FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">  free(p);   </FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">  } while (0);   /* “;” following macro.*/</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">这段代码当然可以正确执行。编译器能够优化这个伪循环,舍弃循环控制,因此执行代码并没有速度的损失,我们也从而得到了能够实现理想功能的宏。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       </FONT>虽然这是一个可以接受的解决方案,但是我们不能不提到的是编写函数要比编写宏好得多。不过如果你不能提供函数调用所需的开销,那么就需要使用内联函数。这种情况虽然在内核中经常出现,但是在其它地方就要少得多。(无可否认,当使用<FONT face="Times New Roman">C++</FONT>,<FONT face="Times New Roman">gcc</FONT>或者任何实现了将要出现的修正版<FONT face="Times New Roman">ISO</FONT>标准<FONT face="Times New Roman">C</FONT>的编译器时,这种方案只是一种选择,就是最后为<FONT face="Times New Roman">C</FONT>增加内联函数。)</P><H4 6pt 0cm; TEXT-INDENT: 0cm">__wait_event</H4><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">16842</FONT>:<B normal"><FONT face="Times New Roman">__wait_event</FONT></B>使当前进程在等待队列<B normal"><FONT face="Times New Roman">wq</FONT></B>中等待直至<B normal"><FONT face="Times New Roman">condition</FONT></B>为真。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16829</FONT>:通过调用<B normal"><FONT face="Times New Roman">add_wait_queue</FONT></B>(<FONT face="Times New Roman">16791</FONT>行),局部变量<B normal"><FONT face="Times New Roman">__wait</FONT></B>可以被链接到队列上。注意<B normal"><FONT face="Times New Roman">__wait</FONT></B>是在堆栈中而不是在内核堆中分配空间,这是内核中常用的一种技巧。在宏运行结束之前,<B normal"><FONT face="Times New Roman">__wait</FONT></B>就已经被从等待队列中移走了,因此等待队列中指向它的指针总是有效的。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">16830</FONT>:重复分配<FONT face="Times New Roman">CPU</FONT>给另一个进程直至条件满足,这一点将在下面几节中讨论。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16831</FONT>:进程被置为<B normal"><FONT face="Times New Roman">TASK_UNINTERRUPTIBLE</FONT></B>状态(<FONT face="Times New Roman">16190</FONT>行)。这意味着进程处于休眠状态,不应被唤醒,即使是信号量也不能打断该进程的休眠。信号量在第<FONT face="Times New Roman">6</FONT>章中介绍,而进程状态则在第<FONT face="Times New Roman">7</FONT>章中介绍。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16832</FONT>:如果条件已经满足,则可以退出循环。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>请注意如果在第一次循环时条件就已经满足,那么前面一行的赋值就浪费了(因为在循环结束之后进程状态会立刻被再次赋值)。<B normal"><FONT face="Times New Roman">__wait_event</FONT></B>假定宏开始执行时条件还没有得到满足。而且,这种对进程状态变量<B normal"><FONT face="Times New Roman">state</FONT></B>的延迟赋值也并没有什么害处。在某些特殊情况下,这种方法还十分有益。例如当<B normal"><FONT face="Times New Roman">__wait_event</FONT></B>开始执行时条件为假,但是在执行到<FONT face="Times New Roman">16832</FONT>行时就为真了。这种变化只有在为有关进程状态的代码计算<B normal"><FONT face="Times New Roman">condition</FONT></B>变量值时才会出现问题。但是在代码中这种情况我一处也没有发现。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16834</FONT>:调用<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>时,对<B normal"><FONT face="Times New Roman">schedule</FONT></B>的调用才会返回。这种情况只有当等待队列中的进程被唤醒时才会发生。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16836</FONT>:进程已经退出了,因此条件必定已经得到了满足。进程重置<B normal"><FONT face="Times New Roman">TASK_RUNNING</FONT></B>的状态(<FONT face="Times New Roman">16188</FONT>行),使其适合<FONT face="Times New Roman">CPU</FONT>运行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16837</FONT>:通过调用<B normal"><FONT face="Times New Roman">remove_wait_queue</FONT></B>(<FONT face="Times New Roman">16814</FONT>行)将进程从等待队列中移去。<B normal"><FONT face="Times New Roman">wait_event_interruptible</FONT></B>和<B normal"><FONT face="Times New Roman">__wait_event_interruptible</FONT></B>(分别参见<FONT face="Times New Roman">16868</FONT>行和<FONT face="Times New Roman">16847</FONT>)基本上与<B normal"><FONT face="Times New Roman">wait_event</FONT></B>和<B normal"><FONT face="Times New Roman">__wait_event</FONT></B>相同,但不同的是它们允许休眠的进程可以被信号量中断。如前所述,信号量将在第<FONT face="Times New Roman">6</FONT>章中介绍。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>请注意<B normal"><FONT face="Times New Roman">wait_event</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></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">do/while(0)</FONT></B>技巧一样,这样可以使被封闭起来的代码能够像一个单元一样运行。这样的封闭代码就是一个独立的表达式,而不是一个独立的语句。也就是说,它可以求值以供其它更复杂的表达式使用。发生这种情况的原因主要在于一些不可移植的<FONT face="Times New Roman">gcc</FONT>特有代码的存在。通过使用这类技巧,一个程序块中的最后一个表达式的值将定义为整个程序块的最终值。当在表达式中使用<B normal"><FONT face="Times New Roman">wait_event_interruptible</FONT></B>时,执行宏体后赋<B normal"><FONT face="Times New Roman">__ret</FONT></B>的值为宏体的值(参看<FONT face="Times New Roman">16873</FONT>行)。对于有<FONT face="Times New Roman">Lisp</FONT>背景知识的程序员来说,这是个很常见的概念。但是如果你仅仅了解一点<FONT face="Times New Roman">C</FONT>和其它一些相关的过程性程序设计语言,那么你可能就会觉得比较奇怪。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">__wake_up</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26829</FONT>:该函数用来唤醒等待队列中正在休眠的进程。它由<B normal"><FONT face="Times New Roman">wake_up</FONT></B>和<B normal"><FONT face="Times New Roman">wake_up_interruptible</FONT></B>调用(请分别参看<FONT face="Times New Roman">16612</FONT>行和<FONT face="Times New Roman">16614</FONT>行)。这些宏提供<B normal"><FONT face="Times New Roman">mode</FONT></B>参数,只有状态满足<B normal"><FONT face="Times New Roman">mode</FONT></B>所包含的状态之一的进程才可能被唤醒。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26833</FONT>:正如将在第<FONT face="Times New Roman">10</FONT>章中详细讨论的那样,锁(<FONT face="Times New Roman">lock</FONT>)是用来限制对资源的访问,这在<FONT face="Times New Roman">SMP</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">SMP</FONT>逻辑单元可能经常会发现自己正在等待一个实际上并不必须的锁。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26838</FONT>:本段代码遍历非空队列,为队列中正确状态的每一个进程调用<B normal"><FONT face="Times New Roman">wake_up_process</FONT></B>(<FONT face="Times New Roman">26356</FONT>行)。如前所述,进程(队列节点)在此可能并没有从队列中移走。这在很大程度上是由于即使队列中的进程正在被唤醒,它仍然可能希望继续存在于等待队列中,这一点正如我们在<B normal"><FONT face="Times New Roman">__wait_event</FONT></B>中发现的问题一样。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5>内核模块(<FONT face="Times New Roman">Kernel Modules</FONT>)</FONT></H3><P 0cm 0cm 0pt">整个内核并不需要同时装入内存。应该确认,为保证系统能够正常运行,一些特定的内核必须总是驻留在内存中,例如,进程调度代码就必须常驻内存。但是内核其它部分,例如大部分的设备驱动就应该仅在内核需要的时候才装载,而在其它情况下则无需占用内存。</P><P 0cm 0cm 0pt">举例来说,只有在内核真正和<FONT face="Times New Roman">CD-ROM</FONT>通讯时才需要使用完成内核与<FONT face="Times New Roman">CD-ROM</FONT>通讯的设备驱动程序,因此内核可以被设置为在和设备通讯之前才装载相应代码。内核完成和设备的通讯之后可以将这部分代码丢弃。也就是说,一旦代码不再需要,就可以从内存中移走。系统运行过程中可以增减的这部分内核称为内核模块。</P><P 0cm 0cm 0pt">内核模块的优点是可以简化内核自身的开发。假设你购买了一个新的高速<FONT face="Times New Roman">CD-ROM</FONT>驱动器,但是现有的<FONT face="Times New Roman">CD-ROM</FONT>驱动程序并不支持该设备。你自然就希望增加对这种高速模式的支持以提高系统光驱设备的性能。如果作为内核模块来编译驱动程序,你的工作将会方便得多:编译驱动程序,加载到内核,测试,卸载驱动程序,修改驱动程序,再次加载驱动程序到内核,测试,如此周而复始。如果你的驱动程序是直接编辑在内核中的,那么你就必须重新编译整个内核并且在每次修改驱动程序之后重新启动机器。这样慢得很多。</P><P 0cm 0cm 0pt">自然,你也必须留意内核模块。对于指明其它内核模块在磁盘上的驻留位置的那些模块,一定不能从内存中卸载,否则,内核将只能通过访问磁盘来装载处理磁盘访问的内核模块,这是不可能实现的。这也是我们要选择把部分内核作为模块编译还是直接编译进内核使其常驻内存的又一个原因。你知道自己系统的设置方式,因而也就可以自己选择正确使用的方式。(如果为了确保安全,你可以简单的忽略内核模块系统的优点,而把所有的内容都编译到内核里面。)</P><P 0cm 0cm 0pt">内核模块会带来一些速度上的损失,这是因为一些必需的代码现在并不在<FONT face="Times New Roman">RAM</FONT>中,必需要从磁盘读入。但是整个系统的性能通常会有所提高,这主要是因为通过丢弃暂时不使用的模块可以释放出额外的<FONT face="Times New Roman">RAM</FONT>供应用程序使用。如果这部分内存被内核所占用,应用程序将只能更加频繁地进行磁盘交换(<FONT face="Times New Roman">swap</FONT>),而这种磁盘交换会显著的降低应用程序的性能。(磁盘交换将在第<FONT face="Times New Roman">8</FONT>章中讨论。)</P><P 0cm 0cm 0pt">内核模块还会带来因复杂度的增加所造成的开销,这是因为在系统运行的过程中移进移出部分内核需要额外的代码。然而,正如你将在本节中看到的,复杂度的开销是可以管理的。通过使用外部程序来代理一些必需的工作还可以更进一步降低复杂度的开销。(更为确切的说法是,这样做不是减少了复杂度的开销,而是把复杂度的开销重新分配了一下。)这是对内核模块原理的一个小小的扩展:即使是内核的支持模块对于内核来说也只是外部的,部分可用的,只有在需要的时候才被装入内存。</P><P 0cm 0cm 0pt">通常用于这种目的程序称为<FONT face="Times New Roman">modprobe</FONT>。有关的<FONT face="Times New Roman">modprobe</FONT>代码超出了本书的范围,但是在<FONT face="Times New Roman">Linux</FONT>的每个发行版本中都有包含有它。本节的剩余部分将讨论同<FONT face="Times New Roman">modprobe</FONT>协同工作以装载内核模块的内核代码。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">request_module</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24432</FONT>:作为函数说明之前的注释,<B normal"><FONT face="Times New Roman">request_module</FONT></B>是一个函数。内核的其它模块在需要装载其它内核模块的时候,都必须调用这个函数。就像内核处理其它工作一样,这种调用也是为当前运行的进程进行的。从进程的角度来看,这种调用的请求通常是隐含的――正在执行进程其它请求的内核可能会发现必须调入一个模块才能够完成该请求。例如,请参看<FONT face="Times New Roman">10070</FONT>行,这里是一些将在第<FONT face="Times New Roman">7</FONT>章中讨论的代码。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24446</FONT>:以内核中的一个独立进程的形式执行<B normal"><FONT face="Times New Roman">exec_modprobe</FONT></B>函数(<FONT face="Times New Roman">24384</FONT>行,马上就会讨论到)。这并不能只通过函数的简单调用实现,因为<B normal"><FONT face="Times New Roman">exec_modprobe</FONT></B>要继续调用<B normal"><FONT face="Times New Roman">exec</FONT></B>来执行一个程序。因此,对函数<B normal"><FONT face="Times New Roman">exec_modprobe</FONT></B>的简单调用将永远不会有返回。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>这和使用<B normal"><FONT face="Times New Roman">fork</FONT></B>以准备<B normal"><FONT face="Times New Roman">exec</FONT></B>调用十分类似,你可以认为<B normal"><FONT face="Times New Roman">kernel_thread</FONT></B>对内核来说就是较低版本的<B normal"><FONT face="Times New Roman">fork</FONT></B>,虽然两者有很大不同。<B normal"><FONT face="Times New Roman">fork</FONT></B>是从指定函数开始执行新的进程,而不是从调用者的当前位置开始运行。正如<B normal"><FONT face="Times New Roman">fork</FONT></B>一样,<B normal"><FONT face="Times New Roman">kernel_thread</FONT></B>返回的值是新进程的进程号。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24448</FONT>:和<B normal"><FONT face="Times New Roman">fork</FONT></B>一样,从<B normal"><FONT face="Times New Roman">kernel_thread</FONT></B>返回的负值表示内部错误。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24455</FONT>:正如函数中论述的一样,大部分的信号量将因当前进程而被暂时阻塞。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24462</FONT>:等待<B normal"><FONT face="Times New Roman">exec_modprobe</FONT></B>执行完毕,同时指出所需要的模块是已经成功装入内存还是装载失败了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24465</FONT>:结束运行,恢复信号量。如果<B normal"><FONT face="Times New Roman">exec_modpro</FONT></B>返回错误代码,则打印错误消息。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">exec_modprobe</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24384</FONT>:<B normal"><FONT face="Times New Roman">exec_modprobe</FONT></B>运行为内核增加内核模块的程序。这里的模块名是一个<B normal"><FONT face="Times New Roman">void*</FONT></B>的指针,而不是<B normal"><FONT face="Times New Roman">char*</FONT></B>的指针。原因简单说来就是<FONT face="Times New Roman"><B normal">kernel_thread</B> </FONT>产生的函数通常都使用<B normal"><FONT face="Times New Roman">void*</FONT></B>指针参数。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24386</FONT>:设置<FONT face="Times New Roman">modprobe</FONT>的参数列表和环境。<B normal"><FONT face="Times New Roman">Modprobe_path</FONT></B>(<FONT face="Times New Roman">24363</FONT>行)用来定位<FONT face="Times New Roman">modprobe</FONT>程序的位置。它可以通过内核的<FONT face="Times New Roman">sysctl</FONT>特性来修改,这一点将在第<FONT face="Times New Roman">11</FONT>章中介绍(请参看<FONT face="Times New Roman">30388</FONT>行)。这意味着<FONT face="Times New Roman">root</FONT>可以动态选择不同于<FONT face="Times New Roman">/sbin/modprobe</FONT>的程序来运行,以适应当<FONT face="Times New Roman">modprobe</FONT>被安装到其它地方或者使用修改过的<FONT face="Times New Roman">modprobe</FONT>替换掉了原有的<FONT face="Times New Roman">modprobe</FONT>之类的情况。<p></p></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24400</FONT>:(正如代码中描述的一样)出于安全性考虑,丢弃所有挂起的信号量和信号量句柄(<FONT face="Times New Roman">handlers</FONT>)。这里最重要的部分是对<B normal"><FONT face="Times New Roman">flush_signal_handlers</FONT></B>的调用(<FONT face="Times New Roman">28041</FONT>行),它使用内核默认的信号量句柄代替所有用户定义的信号量句柄。如果在此时有信号量被传送到内核,它将获得默认响应——通常是忽略信号量或杀死进程。但是不管怎样都不会引起安全风险。由于该函数从触发它的进程中分离出来(如前所述),所以不管原始进程在此处是否改变其原来分配的信号量句柄都不会产生任何影响。<p></p></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24405</FONT>:关闭调用进程打开的所有文件。最重要的是,这意味着<FONT face="Times New Roman">modprobe</FONT>程序不再从调用进程中继承标准输入输出和标准错误。这很有可能会引起安全漏洞。(这可能在替代<FONT face="Times New Roman">modprobe</FONT>的程序中引起的问题,但是<FONT face="Times New Roman">modprobe</FONT>本身实际上并不关心这个差异。)<p></p></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24413</FONT>:<FONT face="Times New Roman">modprobe</FONT>程序作为<FONT face="Times New Roman">root</FONT>运行,它拥有<FONT face="Times New Roman">root</FONT>所拥有的所有权限。和整个内核中其它地方一样,请注意<FONT face="Times New Roman">root</FONT>使用用户<FONT face="Times New Roman">ID</FONT>号<FONT face="Times New Roman">0</FONT>的假定在这里已经被写入程序。用户<FONT face="Times New Roman">ID</FONT>号和权能系统(<FONT face="Times New Roman">capability system</FONT>)(在接下来的几行中会用到)将在第<FONT face="Times New Roman">7</FONT>章中介绍。<p></p></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24421</FONT>:试图执行<FONT face="Times New Roman">modprobe</FONT>程序。如果尝试失败,内核将使用<B normal"><FONT face="Times New Roman">printk</FONT></B>打印错误消息并返回错误代码。这里是可能产生<B normal"><FONT face="Times New Roman">printk</FONT></B>的缓冲器过载的地点之一。<B normal"><FONT face="Times New Roman">module_name</FONT></B>的长度并没有明确限制,就我们对该调用的看法而言,它可能长达一百万个字符。为防止<B normal"><FONT face="Times New Roman">printk</FONT></B>缓冲器过载,你必需遍历所有对于该函数的调用(实际上是对<B normal"><FONT face="Times New Roman">request_module</FONT></B>的调用)以保证每个调用者使用足够短的不会为<FONT face="Times New Roman">printk</FONT>造成麻烦的模块名。<p></p></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24427</FONT>:当<B normal"><FONT face="Times New Roman">execve</FONT></B>成功执行时,它不会返回任何结果,因此本处是不可能执行到的。但是编译器却并不知道这一点,因此此处使用了<B normal"><FONT face="Times New Roman">return</FONT></B>语句以保证<FONT face="Times New Roman">gcc</FONT>不出错。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       </FONT>对于内核的进一步讨论将超出本章的既定范围,因此在这个问题上我们到此为止。然而本书中也包括了其它必需的内核代码。在读完第<FONT face="Times New Roman">4</FONT>章和第<FONT face="Times New Roman">5</FONT>章之后,也许你会希望再次仔细研读一下这部分内容。有关这个问题的两个文件是<FONT face="Times New Roman">include/linux/module.h</FONT>(从<FONT face="Times New Roman">15529</FONT>行开始)和<FONT face="Times New Roman">/kernel/module.c</FONT>(从<FONT face="Times New Roman">24476</FONT>行开始)。和<B normal"><FONT face="Times New Roman">sys_create_module</FONT></B>(<FONT face="Times New Roman">24586</FONT>行),<B normal"><FONT face="Times New Roman">sys_init_module</FONT></B>(<FONT face="Times New Roman">24637</FONT>行),<B normal"><FONT face="Times New Roman">sys_delete_module</FONT></B>(<FONT face="Times New Roman">24860</FONT>行)和<B normal"><FONT face="Times New Roman">sys_query_module</FONT></B>(<FONT face="Times New Roman">25148</FONT>行)四个函数需要特别注意一样,<B normal"><FONT face="Times New Roman">struct module</FONT></B>(<FONT face="Times New Roman">15581</FONT>行)也要特别引起注意。这些函数实现了<FONT face="Times New Roman">modprobe</FONT>以及<FONT face="Times New Roman">insmod</FONT>,<FONT face="Times New Roman">lsmod</FONT>和<FONT face="Times New Roman">rmmod</FONT>所使用的系统调用以完成模块的装载、定位和卸载。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       </FONT>内核触发直接回调内核程序的现象看起来很令人奇怪。但是,实际上进行的工作不止于此。例如,<FONT face="Times New Roman">modprobe</FONT>必须实际访问磁盘以搜寻要装载的模块。而且更为重要的一点是这种方法赋予<FONT face="Times New Roman">root</FONT>对内核模块系统更多的控制能力。这主要是因为<FONT face="Times New Roman">root</FONT>也可以运行<FONT face="Times New Roman">modprobe</FONT>以及相关程序。因此,<FONT face="Times New Roman">root</FONT>既可以手工装载、查询、卸载模块,也可以由内核自动完成。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">配置与编译内核</H2><P 0cm 0cm 0pt">你可能仅仅研读、欣赏而并不修改<FONT face="Times New Roman">Linux</FONT>内核源代码。但是,更普遍的情况是,用户有强烈的愿望去改进内核代码并完成相应的测试,这样我们就需要知道如何重建内核。本节就是要告诉你如何实现这一点,而最终则归结于如何把你所做的修改发行给别人,以使得每个人都能从你的工作中受益。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5>配置内核</FONT></H3><P 0cm 0cm 0pt">编译内核的第一步就是配置内核,这是增加或者减少对内核特性的支持以及修改内核的一些特性发挥作用的方式的必要步骤。例如,你可以要求内核为自己的声卡指定一个不同的<FONT face="Times New Roman">DMA</FONT>通道。如果内核配置和你的需要相同,那么你可以直接跳过本节,否则请继续阅读以下内容。</P><P 0cm 0cm 0pt">为了完成内核的配置,请先切换到<FONT face="Times New Roman">root</FONT>用户,然后转入如下内核源程序目录:</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">cd /usr/src/linux</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm">接着敲入如下命令组:</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">make config</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">make menuconfig</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">make xconfig</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm">这三条命令都可以让你来配置内核,但它们发挥作用的方式各不相同:</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo4; tab-stops: list 21.25pt">l         <B normal"><FONT face="Times New Roman">make config</FONT></B>――三种方法中最简单也是最枯燥的一种。但是最基本的一点是,它可以适应任何情况。这种方法通过为每一个内核支持的特性向用户提问的方式来决定在内核中需要包含哪些特性。对于大多数问题,你只要回答<FONT face="Times New Roman">y</FONT>(<FONT face="Times New Roman">yes</FONT>,把该特性编译进内核中),<FONT face="Times New Roman">m</FONT>(作为模块编译)或者<FONT face="Times New Roman">n</FONT>(<FONT face="Times New Roman">no</FONT>,根本不对该特性提供支持)。在决定之前用户应该考虑清楚,因为这个过程是不可逆的。如果你在该过程中犯了错误,就只能按<FONT face="Times New Roman">Ctrl+C</FONT>退出。你也可以敲入<FONT face="Times New Roman">?</FONT>以获取帮助。图<FONT face="Times New Roman">2.1</FONT>显示了这种方法正在<FONT face="Times New Roman">X</FONT>终端上运行的情况。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><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><B normal"><p></p></B></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm; TEXT-ALIGN: center" align=center>图<FONT face="Times New Roman">2.1 </FONT>运行中的<B normal"><FONT face="Times New Roman">make config</FONT></B></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt 18pt; TEXT-INDENT: 18pt">幸运的是,这种方法还有一些智能。例如,如果你对<FONT face="Times New Roman">SCSI</FONT>支持回答<FONT face="Times New Roman">no</FONT>,那么系统就不会再询问你有关<FONT face="Times New Roman">SCSI</FONT>的细节问题了。而且你可以只按回车键以接受缺省的选择,也就是当前的设置(因此,如果当前内核将对于<FONT face="Times New Roman">SCSI</FONT>的支持编译进了内核,在这个问题上按回车键就意味着继续把对<FONT face="Times New Roman">SCSI</FONT>的支持编译进内核中)。即使是这样,大部分用户还是宁愿使用另外的两种方法。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l5 level1 lfo5; tab-stops: list 21.25pt">l         <B normal"><FONT face="Times New Roman">make menuconfig</FONT></B>―一种基于终端的配置机制,用户拥有通过移动光标来进行浏览等功能。图<FONT face="Times New Roman">2.2</FONT>显示了在<FONT face="Times New Roman">X</FONT>终端上运行的<B normal"><FONT face="Times New Roman">make menuconfig</FONT></B>。虽然在控制台上显示的是彩色,但是在终端上的显示仍然相当单调。使用<FONT face="Times New Roman">menuconfig</FONT>必须要有相应的<FONT face="Times New Roman">ncurses</FONT>类库。</P><P 0cm 0cm 0pt"><wrapblock><v:shape><v:imagedata></v:imagedata><w:wrap type="topAndBottom"></w:wrap></v:shape></wrapblock><BR vglayout" clear=all><B normal"><p></p></B></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm; TEXT-ALIGN: center" align=center>图<FONT face="Times New Roman">2.2 </FONT>运行中的<B normal"><FONT face="Times New Roman">make menuconfig</FONT></B></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm; TEXT-ALIGN: center" align=center><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l5 level1 lfo5; tab-stops: list 21.25pt">l         <wrapblock><v:shape><v:imagedata></v:imagedata><w:wrap type="topAndBottom"></w:wrap></v:shape></wrapblock><BR vglayout" clear=all><B normal"><FONT face="Times New Roman">make xconfig</FONT></B>――这是我最喜欢的一种配置方式。只有你能够在<FONT face="Times New Roman">X server</FONT>上用<FONT face="Times New Roman">root</FONT>用户身份运行<FONT face="Times New Roman">X</FONT>应用程序时,这种配置方式才可以使用(有些偏执的用户就不愿意使用这种方式)。你还必须拥有<FONT face="Times New Roman">Tcl</FONT>窗口系统(<FONT face="Times New Roman">Tcl windowing system</FONT>),这实际上还意味着你必须拥有<FONT face="Times New Roman">Tcl</FONT>,<FONT face="Times New Roman">Tk</FONT>以及一个正在运行的<FONT face="Times New Roman">X</FONT>安装程序。作为补偿,用户获得的是更漂亮的,基于<FONT face="Times New Roman">X</FONT>系统的以及和<FONT face="Times New Roman">menuconfig</FONT>功能相同的配置方法。图<FONT face="Times New Roman">2.3</FONT>显示这种方法运行过程中打开“可装载模块支持(<FONT face="Times New Roman">Loadable module support</FONT>)”子窗口的情况。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><B normal"><p><FONT face="Times New Roman"> </FONT></p></B></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><B normal"><p><FONT face="Times New Roman"> </FONT></p></B></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm; TEXT-ALIGN: center" align=center>图<FONT face="Times New Roman">2.3 </FONT>运行中的<B normal"><FONT face="Times New Roman">make xconfig</FONT></B></P><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt">如上所述,这三种方法都实现了相同的功能:它们都生成在构建内核时使用的<FONT face="Times New Roman">.config</FONT>文件。而唯一的区别是在于创建这个文件时的难易程度不同。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5>构建内核</FONT></H3><P 0cm 0cm 0pt">构建内核要做的工作要比配置内核所做的工作少得多。虽然有几种方式都能实现这一功能,但是选择哪一种依赖于你希望怎样对系统进行设置。长期以来,我已经形成了如下的习惯。虽然这种习惯比我所必须要做的略微多一些,但是它包含了所有基本的问题。首先,如果你还不在内核源程序目录中,请先再次转入这一目录:</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">cd  /usr/src/linux</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm">现在,切换到<FONT face="Times New Roman">root</FONT>用户,使用下面显示的命令生成内核。现在在<FONT face="Times New Roman">shell</FONT>中敲入下面的命令,注意<FONT face="Times New Roman">make</FONT>命令因为空间关系分成了两行,但实际上这在<FONT face="Times New Roman">shell</FONT>输入时是一个只有一行的命令:</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       make dep clean zlilo boot</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">              modules modules_install</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm">当给出了如上多个目标时,除非前面所有的目标都成功了,否则<FONT face="Times New Roman">make</FONT>能够知道没有必要继续尝试下面的目标。因此,如果<FONT face="Times New Roman">make</FONT>能够运行结束,成功退出,那么这就意味着所有的目标都正确构建了。现在你可以重新启动机器以运行新的内核。</P><H4 6pt 0cm; TEXT-INDENT: 0cm"><I normal">备份的重要性<p></p></I></H4><P 0cm 0cm 0pt">当修改(<FONT face="Times New Roman">fooling</FONT>)内核时,你必须准备一个能够启动的备用内核。实现该目的的一种方式是通过配置<FONT face="Times New Roman">Linux</FONT>加载程序(<FONT face="Times New Roman">LILO</FONT>)以允许用户选择启动的内核映象,其中之一是从没有修改过的内核的备份(我总是这样做的)。</P><P 0cm 0cm 0pt">如果你比较有耐心,那么你就可以使用<FONT face="Times New Roman">zdisk</FONT>目标而不使用<FONT face="Times New Roman">zlilo</FONT>目标;它可以把能够启动的内核映象写入软盘中。这样你就可以通过在启动时插入软盘的方式启动你的测试内核;如果没有插入软盘,则启动正常的内核。</P><P 0cm 0cm 0pt">但是请注意:内核模块并没有被装载到软盘中,它们实际上是装在硬盘中的(除非你愿意承担更多的麻烦)。因此,如果你弄乱了内核模块,即使是<FONT face="Times New Roman">zdisk</FONT>目标也救不了你。实际上,上面提到的这两种方法都存在这个问题。虽然有比较好的解决方法可用,但是最简单的方法(也就是我所使用的方法)是把备份内核作为严格独立的内核来编译,而不使用可装载模块的支持。通过这种方法,即使我弄乱了内核而不得不使用备份启动系统,那么不管问题是实验性内核不正确还是内核模块的原因都无关紧要。不管怎样,在备份的内核中已经有我需要的所有东西了。</P><P 0cm 0cm 0pt">由于用户所作的修改可能导致系统的崩溃,如损坏磁盘上的数据等等,并不仅仅只是打乱设备驱动程序或文件系统,在测试新内核之前,备份系统的最新数据也是一个英明的决策。(虽然设备驱动程序的开发不是本书的主题,但是必需指出的是,设备驱动程序的缺陷可能会引起系统的物理损坏。例如显示器是不能备份的,而且因价格昂贵而不易替换。)作为一个潜在的内核黑客,你的最佳投资(当然是读过本书以后)是一个磁带驱动器和充足的磁带。</P><H4 6pt 0cm; TEXT-INDENT: 0cm"><I normal">发行你的改进<p></p></I></H4><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: 0cm">下面是有关发行你所做修改的一些基本规则:</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo6; tab-stops: list 21.25pt">l         检查最新发行版本,确保你所处理的不是已经解决了的问题</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo6; tab-stops: list 21.25pt">l         遵守<FONT face="Times New Roman">Linux </FONT>内核代码编写的风格。简要的说就是<FONT face="Times New Roman">8</FONT>字符缩进以及<FONT face="Times New Roman">K&amp;R</FONT>括号风格(<B normal"><FONT face="Times New Roman">if</FONT></B>,<B normal"><FONT face="Times New Roman">else</FONT></B>,<B normal"><FONT face="Times New Roman">for</FONT></B>,<B normal"><FONT face="Times New Roman">while</FONT></B>,<B normal"><FONT face="Times New Roman">switch</FONT></B>或者<B normal"><FONT face="Times New Roman">do</FONT></B>后面同一行中紧跟着开括号)。在内核源程序目录下面的文档编写和代码风格文件给出了完整的规则,不过我们已经介绍了其中的关键部分。注意本书中包含的源程序代码为节省空间而进行了大量的重新编辑,在该过程中我可能打破了其中的一些规则。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo6; tab-stops: list 21.25pt">l         独立发行相对无关的修改。这样,只想使用你所做的某部分修改的人就可以十分方便地获得想要的东西,而不用一次检验所有的修改内容。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo6; tab-stops: list 21.25pt">l         让使用你所做修改的用户清楚他们可以从你的修改中获取什么。同样地,你也应该给出这些问题的可信度。你是<FONT face="Times New Roman">15</FONT>分钟之前才匆匆完成你的修改,甚至还没有时间对它们进行编译,还是已经在你和你的朋友的系统中从去年<FONT face="Times New Roman">3</FONT>月开始就长期稳定的运行过这个修改?</P><P 0cm 0cm 0pt">假设现在你已经准备好发行自己的修改版本了,那么要做的第一步是建立一个说明你所做的修改的文件。你可以使用<FONT face="Times New Roman">diff</FONT>程序自动创建这个文件。结果或者被称为<FONT face="Times New Roman">diffs</FONT>,也或者在<FONT face="Times New Roman">Linux</FONT>中更普遍的被称为补丁(<FONT face="Times New Roman">patch</FONT>)。</P><P 0cm 0cm 0pt">发布的过程十分简单。假设原来没有修改过的源程序代码在<FONT face="Times New Roman">linux-2.2.5</FONT>目录下,而你修改过的源程序代码在<FONT face="Times New Roman">linux-my</FONT>目录下,那么只要进行如下的简单工作就可以了(只有在链接不存在的情况下才需要执行<FONT face="Times New Roman">ln</FONT>):</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">ln –s linux-my linux</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">make –C linux-2.2.5 distclean</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">make –C linux distclean</FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">diff –urN linux-2.2.5 linux &gt;my.patch</FONT></P><P 0cm 0cm 0pt">现在,输出文件<FONT face="Times New Roman">my.patch</FONT>包含了其它用户应用这个修改程序时所必须的一切内容。(警告:如上所述,两个源程序间的所有差别都会包含在这个补丁文件中。<FONT face="Times New Roman">Diff</FONT>不能区分修改部分之间的关系,所以就把它们都罗列了出来。)如果补丁文件相对较小,你可以使用邮件直接发往内核邮件列表。如果补丁很大,那么就需要通过<FONT face="Times New Roman">FTP</FONT>或者<FONT face="Times New Roman">Web</FONT>站点发布。这时发给邮件列表的信件中就只需要包含一个<FONT face="Times New Roman">URL</FONT>。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">Linux</FONT>内核邮件列表的常见问题解答(<FONT face="Times New Roman">FAQ</FONT>)文件位于<B normal"><FONT face="Times New Roman">http://www.ececs.uc.edu/~rreilova/linux/lkmlfaq.html</FONT></B>。该<FONT face="Times New Roman">FAQ</FONT>中包含了邮件列表的订阅,邮件发布以及阅读邮件列表的注意事项等等。</P><P 0cm 0cm 0pt">顺便提一下,如果你想随时了解内核更新开发的进程,我向你强烈推荐下面这个具有很高价值的内核交流站点<FONT face="Times New Roman">Kernel Traffic</FONT>:<B normal"><FONT face="Times New Roman">http://www.kt.opensrc.org</FONT></B>。</P><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P>
作者: ilikenba    时间: 2005-3-4 21:46
<H1 12pt 0cm; TEXT-INDENT: 0cm"><FONT face="Times New Roman">第3章  </FONT>内核体系结构概述</H1>< 0cm 0cm 0pt">本章从较高层次上对内核进行说明。从顺序上来说,本章首先介绍内核设计目标,接下来介绍内核体系结构,最后介绍内核源程序目录结构。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">内核设计目标</H2>< 0cm 0cm 0pt"><FONT face="Times New Roman">Linux </FONT>的内核展现出了几个相互关联的设计目标,它们依次是:清晰性(<FONT face="Times New Roman">clarity</FONT>),兼容性(<FONT face="Times New Roman">compatibility</FONT>),可移植性(<FONT face="Times New Roman">portability</FONT>),健壮性(<FONT face="Times New Roman">robustness</FONT>),安全性(<FONT face="Times New Roman">security</FONT>)和速度(<FONT face="Times New Roman">speed</FONT>)。这些目标有时是互补的,有时则是矛盾的。但是它们被尽可能的保持在相互一致的状态,内核设计和实现的特性通常都要回归到这些问题上来。本节接下来的部分将分别讨论这些设计目标,同时还将对它们之间的取舍与平衡进行简要的说明。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5>清晰性</FONT></H3>< 0cm 0cm 0pt">稍微过于简化的说,内核目标是在保证速度和健壮性的前提下尽量清晰。这和现在的大多数应用程序的开发有所区别,后者的目标通常是在保证清晰性和健壮性的基础上尽量提高速度。因而在内核内部,速度和清晰性经常是一对矛盾。</P>< 0cm 0cm 0pt">在某种程度上,清晰性是健壮性的必要补充:一个很容易理解的实现方法比较容易证明是正确的;或者即使不正确,也能比较容易的找出其问题所在。从而这两个目标很少会发生冲突。</P>< 0cm 0cm 0pt">但是清晰性和速度通常却是一对矛盾。经过仔细手工优化的算法通常都使用了编译器生成代码的类似技术,很少可能是最清晰的解决方案。当内核中清晰性和速度要求不一致时,通常都是以牺牲清晰性来保证速度的。即便如此,程序员仍然清楚的知道清晰性的重要性,而且他们也做了大量完美的工作以使用最清晰的方法保证速度。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5>兼容性</FONT></H3>< 0cm 0cm 0pt">正如第<FONT face="Times New Roman">1</FONT>章中所述,<FONT face="Times New Roman">Linux</FONT>最初的编写目的是为了实现一个完整的、与<FONT face="Times New Roman">Unix</FONT>兼容的操作系统内核。随着开发过程的展开,它也开始以符合<FONT face="Times New Roman">OSIX</FONT>标准为目标。就内核而言,兼容<FONT face="Times New Roman">Unix</FONT>(至少是同某一现代的<FONT face="Times New Roman">Unix</FONT>实现相兼容)和符合<FONT face="Times New Roman">OSIX</FONT>标准并没有什么区别,因此我们也不会在这个问题上详细追究。</P>< 0cm 0cm 0pt">内核提供了另外一种类型的兼容性。基于<FONT face="Times New Roman">Linux </FONT>的系统能够提供可选择的对<FONT face="Times New Roman">Java.class</FONT>文件的本地运行支持。(据说<FONT face="Times New Roman">Linux</FONT>是第一个提供这种支持的操作系统。)尽管实际负责<FONT face="Times New Roman">Java</FONT>程序解释执行的是另外一个<FONT face="Times New Roman">Java</FONT>虚拟机进程,该虚拟机并没有内置到内核中。但是内核提供的这种机制可以使得这种支持对用户是透明的。通过内核本身提供的程度不同的支持(这并不代表大部分工作像<FONT face="Times New Roman">Java</FONT>的解决方式一样能够通过外部进程实现),对其它可执行文件格式的支持也能够以同样的方式插入内核中。这方面的内容将在第<FONT face="Times New Roman">7</FONT>章中详细介绍。</P>< 0cm 0cm 0pt">另外需要说明的是,<FONT face="Times New Roman">GNU/Linux</FONT>系统作为一个整体通过<FONT face="Times New Roman">DOSEMU</FONT>仿真机器提供了对<FONT face="Times New Roman">DOS</FONT>可执行程序的支持,而且也通过<FONT face="Times New Roman">WINE</FONT>设计提供了对<FONT face="Times New Roman">Windows</FONT>可执行程序的部分支持。系统还以同样的方式通过<FONT face="Times New Roman">SAMBA</FONT>提供了对<FONT face="Times New Roman">Windows</FONT>兼容文件和打印服务的支持。但是这些都不是同内核密切相关的问题,因此在本书中我们不再对它们进行讨论。</P>< 0cm 0cm 0pt">兼容性的另外一个方面是兼容异种文件系统,本章中稍后会有更为详细的介绍,但是大部分内容已经超出了本书的范围。<FONT face="Times New Roman">Linux</FONT>能够支持很多文件系统,例如<FONT face="Times New Roman">ext2</FONT>(“本地”文件系统),<FONT face="Times New Roman">ISO-9660</FONT>(<FONT face="Times New Roman">CD-ROM</FONT>使用的文件系统),<FONT face="Times New Roman">MS-DOS</FONT>,网络文件系统(<FONT face="Times New Roman">NFS</FONT>)等许多其它文件系统。如果你有使用其它操作系统格式的磁盘或者一个网络磁盘服务器,那么<FONT face="Times New Roman">Linux</FONT>将能够和这些不同的文件系统进行交互。</P>< 0cm 0cm 0pt">兼容性的另外一个问题是网络,这在当今<FONT face="Times New Roman">Internet</FONT>流行的时代尤为重要。作为<FONT face="Times New Roman">Unix</FONT>的一个变种,<FONT face="Times New Roman">Linux</FONT>自然从很早就开始提供对<FONT face="Times New Roman">TCP/IP</FONT>的支持。内核还支持其它许多网络协议,它们包括<FONT face="Times New Roman">AppleTalk</FONT>协议的代码,这使得<FONT face="Times New Roman">Linux</FONT>单元(<FONT face="Times New Roman">box</FONT>)可以和<FONT face="Times New Roman">Macintosh</FONT>机自由通讯;<FONT face="Times New Roman">Novell</FONT>的网络协议,也就是网络报文交换(<FONT face="Times New Roman">IPX</FONT>),分组报文交换(<FONT face="Times New Roman">SPX</FONT>),和<FONT face="Times New Roman">NetWare</FONT>核心协议(<FONT face="Times New Roman">NCP</FONT>);<FONT face="Times New Roman">IP</FONT>协议的新版本<FONT face="Times New Roman">IPv6</FONT>;以及其它一些不太出名的协议。</P>< 0cm 0cm 0pt">兼容性考虑的最后一个方面是硬件兼容性。似乎每个不常见的显卡,市场份额小的网卡,非标准的<FONT face="Times New Roman">CD-ROM</FONT>接口和专用磁带设备都有<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>< 0cm 0cm 0pt">这些兼容性必须通过一个重要的子目标:模块度(<FONT face="Times New Roman">Modularity</FONT>)来实现。在可能的情况下,内核只定义子系统的抽象接口,这种抽象接口可以通过任何方法来实现。例如,内核对于新文件系统的支持将简化为对虚拟文件系统(<FONT face="Times New Roman">VFS</FONT>)接口的代码实现。第<FONT face="Times New Roman">7</FONT>章中介绍的是另外一个例子,内核对二进制句柄的抽象支持是实现对诸如<FONT face="Times New Roman">Java</FONT>之类的新可执行格式的支持的方法。增加新的可执行格式的支持将转变为对相应的二进制句柄接口的实现。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5>可移植性</FONT></H3>< 0cm 0cm 0pt">与硬件兼容性相关的设计目标是可移植性(<FONT face="Times New Roman">portability</FONT>),也就是在不同硬件平台上运行<FONT face="Times New Roman">Linux</FONT>的能力。系统最初是为运行在标准<FONT face="Times New Roman">IBM</FONT>兼容机上的<FONT face="Times New Roman">Intel x86 CPU</FONT>而设计的,当时根本没有考虑到可移植性的问题。但是情况从那以后已经发生了很大的变化。现在正式的内核移植包括向基于<FONT face="Times New Roman">Alpha</FONT>,<FONT face="Times New Roman">ARM</FONT>,<FONT face="Times New Roman">Motorola 69x0</FONT>,<FONT face="Times New Roman">MIPS</FONT>,<FONT face="Times New Roman">owerPC</FONT>,<FONT face="Times New Roman">SPARC</FONT>以及<FONT face="Times New Roman">SPARC-64 CPU</FONT>系统的移植。因而,<FONT face="Times New Roman">Linux</FONT>可以在<FONT face="Times New Roman">Amigas</FONT>,旧版或新版的<FONT face="Times New Roman">Macintosh</FONT>,<FONT face="Times New Roman">Sun</FONT>和<FONT face="Times New Roman">SGI</FONT>工作站以及<FONT face="Times New Roman">NeXT</FONT>机等机器上运行。而且这些还只是标准内核发行版本的移植范围。从老的<FONT face="Times New Roman">DEC VAX</FONT>到<FONT face="Times New Roman">3Com</FONT>掌上系列个人数字助理(例如<FONT face="Times New Roman">alm III</FONT>)的非正式的移植工作也在不断进行中。成功的非正式移植版本后来通常都会变成正式的移植版本,因此这些非正式的移植版本很多最终都会出现在主开发树中。</P>< 0cm 0cm 0pt">广泛平台支持之所以能够成功的部分原因在于内核把源程序代码清晰地划分为体系结构无关部分和体系结构相关部分。在本章的后续部分将对这个问题进行更深入的讨论。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5>健壮性和安全性</FONT></H3>< 0cm 0cm 0pt"><FONT face="Times New Roman">Linux</FONT>必须健壮、稳定。系统自身应该没有任何缺陷,并它还应该可以保护进程(用户)以防止互相干扰,这就像把整个系统从其它系统中隔离开来加以保护一样。后一种考虑很大程度上是受信任的用户空间应用程序领域的问题,但是内核至少也应该提供支撑安全体系的原语(<FONT face="Times New Roman">primitive</FONT>)。健壮性和安全性比任何别的目标都要重要,包括速度。(系统崩溃的速度很快又有什么好处呢?)</P>< 0cm 0cm 0pt">保证<FONT face="Times New Roman">Linux</FONT>健壮性和安全性的唯一一个最重要的因素是其开放的开发过程,它可以被看作是一种广泛而严格的检查。内核中的每一行代码、每一个改变都会很快由世界上数不清的程序员检验。还有一些程序员专门负责寻找和报告潜在的缺陷――他们这样做完全是出于自己的个人爱好,因为他们也希望自己的<FONT face="Times New Roman">Linux</FONT>系统能够健壮安全。以前检查中所没有发现的缺陷可以通过这类人的努力来定位、修复,而这种修复又合并进主开发树以使所有的人都能够受益。安全警告和缺陷报告通常在几天甚至几个小时内就能够得到处理和修复。</P>< 0cm 0cm 0pt"><FONT face="Times New Roman">Linux</FONT>可能并不一定是现有的最安全的操作系统(很多人认为这项桂冠应该属于<FONT face="Times New Roman">OpenBSD</FONT>,它是一个以安全性为主要目标的<FONT face="Times New Roman">Unix</FONT>变种),但是它是一个有力的竞争者。而且<FONT face="Times New Roman">Linux</FONT>健壮性远没有发展到尽头。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5>速度</FONT></H3>< 0cm 0cm 0pt">这个术语经常自己就可以说明问题。速度几乎是最重要的衡量标准,虽然其等级比健壮性、安全性和(在有些时候)兼容性的等级要低。然而它却是代码最直观的几个方面之一。<FONT face="Times New Roman">Linux</FONT>内核代码经过了彻底的优化,而最经常使用的部分――例如调度――则是优化工作的重点。几乎在任何时候都有一些不可思议的代码,这是由于这种方式的执行速度比较快。(这并不总是很明显,但是你经常不得不通过自己的试验来对这种优化代码进行确认。)虽然有时一些更直接的实现方法速度也很快,但是我所见过的这种情况屈指可数。</P>< 0cm 0cm 0pt">在某些情况下,本书推荐用可读性更好的代码来替代那些以速度的名义而被故意扭曲了的代码。虽然速度是一个设计目标,但我基本上只在以下两种情况时才会这样做:<FONT face="Times New Roman">a) </FONT>在所考虑的问题中,速度明显不是关键问题<FONT face="Times New Roman"> b) </FONT>没有其它的办法。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">内核体系结构初始</H2>< 0cm 0cm 0pt">图<FONT face="Times New Roman">3.1</FONT>是一种类<FONT face="Times New Roman">Unix</FONT>操作系统的相当标准的视图,实际上,更细致的来说,该图能够说明所有期望具有平台无关特性的操作系统。它着重强调了内核的下面两个特性:</P>< 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l8 level1 lfo2; tab-stops: list 21.25pt">l         内核将应用程序和硬件分离开来。</P>< 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l8 level1 lfo2; tab-stops: list 21.25pt">l         部分内核是体系结构和硬件特有的,而部分内核则是可移植的。</P>< 0cm 0cm 0pt 21pt; 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></P>< 0cm 0cm 0pt 21pt; TEXT-INDENT: 0cm; TEXT-ALIGN: center" align=center><p><FONT face="Times New Roman"> </FONT></p></P>< 0cm 0cm 0pt 21pt; TEXT-INDENT: 0cm; TEXT-ALIGN: center" align=center>图<FONT face="Times New Roman">3.1 </FONT>内核体系结构基本结构图</P>< 0cm 0cm 0pt 21pt; TEXT-INDENT: 0cm; TEXT-ALIGN: center" align=center><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt">第一点我们在前面章节中已经讨论清楚了,在这里没有必要重复说明。第二点,也就是与体系结构无关和与体系结构相关代码的内容对于我们的讨论比较有意义。内核通过使用与处理用户应用程序相同的技巧来实现部分可移植性。这也就是说,如同内核把用户应用程序和硬件分离一样,部分内核将会因为与硬件的联系而同其它内核分离开来。通过这种分离,用户应用程序和部分内核都成为可移植的。</P><P 0cm 0cm 0pt">虽然这通常并不能够使得内核本身更清楚,但是源程序代码的体系结构无关部分通常定义了与低层,也就是体系结构相关部分(或假定)的接口。作为一个简单的例子,内存管理代码中的体系结构无关部分假定只要包含特定的头文件就可以获得合适的<FONT face="Times New Roman"><B normal">PAGE_SIZE</B> </FONT>宏(参看<FONT face="Times New Roman">10791</FONT>行)的定义,该宏定义了系统的内存管理硬件用于分割系统地址空间的内存块的大小(参看第<FONT face="Times New Roman">8</FONT>章)。体系结构无关代码并不关心宏的确切定义,而把这些问题都留给体系结构相关代码去处理。(顺便一提,这比到处使用<B normal"><FONT face="Times New Roman">#ifdef/#endif</FONT></B>程序块来定义平台相关代码要清晰易懂得多。)</P><P 0cm 0cm 0pt">这样,内核向新的体系结构的移植就转变成为确认这些特性以及在新内核上实现它们的问题。</P><P 0cm 0cm 0pt">另外,用户应用程序的可移植性还可以通过它和内核的中间层次――标准<FONT face="Times New Roman">C</FONT>库(<FONT face="Times New Roman">libc</FONT>)――的协助来实现。应用程序实际上从不和内核直接通讯,而只通过<FONT face="Times New Roman">libc</FONT>来实现。图<FONT face="Times New Roman">3.1</FONT>中显示应用程序和内核直接通讯的唯一原因在于它们能够和内核通讯。虽然在实际上应用程序并不同内核直接通讯――这样做是毫无意义的。通过直接和内核通讯所能处理的问题都可以通过使用<FONT face="Times New Roman">libc</FONT>实现,而且更容易。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">Libc</FONT>和内核通讯的方式是体系结构相关的(这和图中有一点矛盾),<FONT face="Times New Roman">libc</FONT>负责将用户代码从实现细节中解放出来。有趣的是,甚至大部分<FONT face="Times New Roman">libc</FONT>都不了解这些细节。大部分的<FONT face="Times New Roman">libc</FONT>,例如<B normal"><FONT face="Times New Roman">atoi</FONT></B>和<B normal"><FONT face="Times New Roman">rand</FONT></B>的实现,都根本不需要和内核进行通讯。剩余部分的大部分<FONT face="Times New Roman">libc</FONT>,例如<B normal"><FONT face="Times New Roman">printf</FONT></B>函数,在涉及到内核之前或之后就已经处理大量的工作。(<B normal"><FONT face="Times New Roman">printf</FONT></B>必需首先解释格式化字符串,分析相应参数,设定打印方法,在临时内部缓冲器中记录预期输出。直到此时它才调用底层系统调用<B normal"><FONT face="Times New Roman">write</FONT></B>来实际打印该缓冲区。)其它部分的<FONT face="Times New Roman">libc </FONT>则只是相应系统调用的简单代理。因而一旦发生函数调用时,它们会立即调用内核相应函数以完成主要工作。在最低层次上,大部分<FONT face="Times New Roman">libc</FONT>通过单通道同内核进行交流,而它们所使用的机制将第<FONT face="Times New Roman">5</FONT>章中进行详细介绍。</P><P 0cm 0cm 0pt">由于这种设计,所有的用户应用程序,甚至大部分的<FONT face="Times New Roman">C</FONT>库,都是通过体系结构无关的方式和内核通讯的。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">内核体系结构的深入了解</H2><P 0cm 0cm 0pt">图<FONT face="Times New Roman">3.2</FONT>显示了内核概念化的一种可能方式。该图和区分内核的体系结构无关和体系结构相关的方法有所不同,它是一种更具有普遍性的结构视图。在“<FONT face="Times New Roman">Kernel</FONT>”框内的本书中有所涉及的内核部分都用括号注明了相应的章节编号。虽然有关对称多处理(<FONT face="Times New Roman">SMP</FONT>)的支持也属于本书的范围,但是在这里我们却没有标明章号。部分原因在于相当多的<FONT face="Times New Roman">SMP</FONT>代码广泛地分布于整个内核中<B normal">,</B>因此很难将它与某一个模块联系起来。同样的道理,对于内核初始化的支持也属于本书的范围,但是也没有标明章号。这样做仅仅是因为从设计的观点上看,该问题并不重要。最后,虽然在图中我们将第<FONT face="Times New Roman">6</FONT>章和“进程间通讯”框联系在一起,但是该章只涉及一部分进程间通讯的内容。</P><P 0cm 0cm 0pt 21pt; TEXT-INDENT: 0cm; TEXT-ALIGN: center" align=center><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt 21pt; TEXT-INDENT: 0cm; TEXT-ALIGN: center" align=center><wrapblock><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 21pt; TEXT-INDENT: 0cm; TEXT-ALIGN: center" align=center>图<FONT face="Times New Roman">3.2 </FONT>详细的内核体系结构图</P><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt">进程和内核的交互通常需要通过如下步骤:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">1.         </FONT>用户应用程序调用系统调用,通常是使用<FONT face="Times New Roman">libc</FONT>。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">2.         </FONT>该调用被内核的<B normal"><FONT face="Times New Roman">system_call</FONT></B>函数截获(第<FONT face="Times New Roman">5</FONT>章,<FONT face="Times New Roman">171</FONT>行),此后该函数会将调用请求转发给另外的执行请求的内核函数。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">3.         </FONT>该函数随即和相关内部代码模块建立通讯,而这些模块还可能需要和其它的代码模块或者底层硬件通讯。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">4.         </FONT>结果按照同样的路径依次返回。</P><P 0cm 0cm 0pt">然而,并不是所有内核和进程间的交互都是由进程发起的。内核有时也会自行决定同哪个进程交互,例如通过释放信号量或者简单的采用直接杀死进程的方法终止该进程的执行(如当进程用完所有可用的<FONT face="Times New Roman">CPU</FONT>时间片),以便使其它进程有机会运行。这些交互过程在该图中并没有表示,主要是因为它们通常都只是内核对自己的内部数据结构的修改(信号量传递对于这种规则来说是一个例外)。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">是层次化(Layered),模块化(Modular)还是其它?</H2><P 0cm 0cm 0pt">解决复杂性的所有方法都基于一个基本原理:问题分解和各个击破。也就是说,都是把大型的、难以解决的问题(或系统)分解成一定数量的复杂度较低的子问题(或子系统),再根据需要重复这一过程直到每一部分都小到可以解决为止,而各种方法只是这种原理的一些不同运用而已。</P><P 0cm 0cm 0pt">计算机科学中有三种经典的方法比较适合于构建大型系统(我首先必须说明的是,这些定义都是经过我深思熟虑的讨论对象)。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo4; tab-stops: list 21.25pt">l         层次(<FONT face="Times New Roman">Layer</FONT>)――将解决方案分解成若干部分,在这些部分中存在一个问题域的最底层,它为上层的抽象层次较高的工作提供基础。较高层建立在其低层基础之上。<FONT face="Times New Roman">OSI</FONT>和<FONT face="Times New Roman">TCP/IP</FONT>协议堆栈是众所周知的层次化软件设计的成功的例子。操作系统设计的层次化解决方案可能会包含一个可以直接和硬件通讯的层次、然后在其上提供为更高层提供抽象支持的层次。这样更高层就可以对磁盘、网卡等硬件进行访问,而并不需要了解这些设备的具体细节。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: 0cm">层次化设计的一个特征是要逐步构建符号集(<FONT face="Times New Roman">vocabulary</FONT>)。随着层次的升高,符号集的功能将越来越强大。层次化设计的另外一个特征是完全可以在对其上下层透明的条件下替换某一层次。在最理想的情况下,移植层次化的操作系统只需要重写最低层的代码。纯层次化模型实现的执行速度可能会很慢,因为高层必须(间接的)通过调用一系列连续的低层才能处理完自己的任务――<FONT face="Times New Roman">N</FONT>层调用<FONT face="Times New Roman">N-1</FONT>层,<FONT face="Times New Roman">N-1</FONT>层调用<FONT face="Times New Roman">N-2</FONT>层,等等,直到实际的工作在<FONT face="Times New Roman">0</FONT>层被处理完成。接着,结果当然是通过同样的路径反向传递回来。因此,层次化设计通常会包含对某些高层直接和某些低层通讯的支持;这样虽然提高了速度,但是却使得各个层次的替换工作更加困难(因为不止一个高层会直接依赖于这个你所希望进行替换的层次)。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo5; tab-stops: list 21.25pt">l         模块(<FONT face="Times New Roman">Module</FONT>)――模块将具体的一部分功能块隐藏在抽象的接口背后。模块的最大特点是将接口和其实现分离开来,这样就能够保证一个模块可以在不影响其它模块的情况下进行改变。这样也将模块之间的依赖关系仅仅限定于接口。模块的范围是试图反映求解域内一些方面的自然的概念性界限。纯模块化的操作系统因而就可能有一个磁盘子系统模块,一个内存管理子系统模块,等等。纯模块化和纯层次化的操作系统之间的主要区别是一个可以由其它模块自由调用,模块间没有上层和下层的概念。(从这个意义上来说,模块是广义的层次。按照纯粹的观点,层次是最多可供一个其它模块调用的模块,这个模块也就是它的直接上层模块。)</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo5; tab-stops: list 21.25pt">l         对象(<FONT face="Times New Roman">Object</FONT>)――对象和模块不同,因为对于初学者来说它们具有不同的问题考虑方式,实现的方法也可能各自独立。但是,就我们当前的目的来说,对象不过是结构化使用模块的方法。组件(<FONT face="Times New Roman">Component</FONT>)作为对象思想的进一步改进目前还没有在操作系统设计中广泛使用。即便如此(按照我们的观点),我们也没有足够的理由将其和模块划分在不同的范畴中。</P><P 0cm 0cm 0pt">图<FONT face="Times New Roman">3.1</FONT>强调了内核的层次化的视图,而且是体系结构无关层次位于体系结构相关层次之上。(更为精确的视图是在顶层增加一个附加的体系结构相关的层次。这是因为系统调用接口位于应用程序和内核之间,而且是体系结构相关的。)图<FONT face="Times New Roman">3.2</FONT>着重强调了更加模块化的内核视图。</P><P 0cm 0cm 0pt">从合理的表述层次上看,这两种观点都是正确的。但也可以说这两种观点都是错误的。我可以用大量的图片向你证明内核是遵从所有你所能够指出的设计原则集合的,因为它就是从众多思想中抽取出来的。简单说来,事实是<FONT face="Times New Roman">Linux</FONT>内核既不是严格层次化的,也不是严格模块化的,也不是严格意义上的任何类型,而是以实用为主要依据的。(实际上,如果要用一个词来概括<FONT face="Times New Roman">Linux</FONT>从设计到实现的所有特点,那么实用就是最确切的。)也许最保守的观点是内核的实现是模块化的,虽然这些模块有时会为了追求速度而有意跨越模块的界限。</P><P 0cm 0cm 0pt">这样,<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>的设计目标则会引起许多问题。而且<FONT face="Times New Roman">Linux</FONT>的设计目标中也并不包括要使内核成为一个完全纯化的系统。<FONT face="Times New Roman">Linux</FONT>开发者为了达到设计目标宁愿违背妨碍目标实现的原则。</P><P 0cm 0cm 0pt">实际上,如果对于<FONT face="Times New Roman">Linux</FONT>来说是正确的,那么它们对于所有最成功的设计来说都是正确的。最成功、应用最广泛的实际系统必然是实用的系统。有些开发人员试图寻找功能强大的可以解决所有问题的特殊方法。他们一旦找到了这种方法,所有的问题就都迎刃而解了。像<FONT face="Times New Roman">Linux</FONT>内核一样的成功设计通常需要为系统的不同部分和描述上的不同层次使用不同的方法。这样做的结果可能不是很清晰,也不是很纯粹,但是这种混合产物比同等功能的纯粹系统要强大而且优秀得多。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5><FONT face="Times New Roman">Linux</FONT>大部分都是单内核的</FONT></H3><P 0cm 0cm 0pt">操作系统内核可能是微内核,也可能是单内核(后者有时称之为宏内核<FONT face="Times New Roman">Macrokernel</FONT>)。按照类似封装的形式,这些术语定义如下:</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l10 level1 lfo6; tab-stops: list 21.25pt">l         微内核(<FONT face="Times New Roman">Microkernel kernel</FONT>)――在微内核中,大部分内核都作为独立的进程在特权状态下运行,它们通过消息传递进行通讯。在典型情况下,每个概念模块都有一个进程。因此,如果在设计中有一个系统调用模块,那么就必然有一个相应的进程来接收系统调用,并和能够执行系统调用的其它进程(或模块)通讯以完成所需任务。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: 0cm">在这些设计中,微内核部分经常只不过是一个消息转发站:当系统调用模块要给文件系统模块发送消息时,消息直接通过内核转发。这种方式有助于实现模块间的隔离。(某些时候,模块也可以直接给其它模块传递消息。)在一些微内核的设计中,更多的功能,如<FONT face="Times New Roman">I/O</FONT>等,也都被封装在内核中了。但是最根本的思想还是要保持微内核尽量小,这样只需要把微内核本身进行移植就可以完成将整个内核移植到新的平台上。其它模块都只依赖于微内核或其它模块,并不直接直接依赖硬件。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: 0cm">微内核设计的一个优点是在不影响系统其它部分的情况下,用更高效的实现代替现有文件系统模块的工作将会更加容易。我们甚至可以在系统运行时将开发出的新系统模块或者需要替换现有模块的模块直接而且迅速的加入系统。另外一个优点是不需要的模块将不会被加载到内存中,因此微内核就可以更有效的利用内存。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l9 level1 lfo7; tab-stops: list 21.25pt">l         单内核(<FONT face="Times New Roman">Monolithic kernel</FONT>)――单内核是一个很大的进程。它的内部又可以被分为若干模块(或者是层次或其它)。但是在运行的时候,它是一个独立的二进制大映象。其模块间的通讯是通过直接调用其它模块中的函数实现的,而不是消息传递。</P><P 0cm 0cm 0pt">单内核的支持者声称微内核的消息传递开销引起了效率的损失。微内核的支持者则认为因此而增加的内核设计的灵活性和可维护性可以弥补任何损失。</P><P 0cm 0cm 0pt">我并不想讨论这些问题,但必须说明非常有趣的一点是,这种争论经常会令人想到前几年<FONT face="Times New Roman">CPU</FONT>领域中<FONT face="Times New Roman">RISC</FONT>和<FONT face="Times New Roman">CISC</FONT>的斗争。现代的成功<FONT face="Times New Roman">CPU</FONT>设计中包含了所有这两种技术,就像<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">为什么<FONT face="Times New Roman">Linux</FONT>必然是单内核的呢?一个方面是历史的原因:在<FONT face="Times New Roman">Linus</FONT>的观点看来,通过把内核以单一的方式进行组织并在最初始的空间中运行是相当容易的事情。这种决策避免了有关消息传递体系结构,计算模块装载方式等方面的相关工作。(内核模块系统在随后的几年中又进行了不断地改进。)</P><P 0cm 0cm 0pt">另外一个原因是充足的开发时间的结果。<FONT face="Times New Roman">Linux</FONT>既没有开发时间的限制,也没有深受市场压力的发行进度。<FONT face="Times New Roman"> </FONT>所有的限制只有并不过分的对内核的修改与扩充。内核的单一设计在内部实现了充分的模块化,在这种条件下的修改或增加都并不怎么困难。而且问题还在于没有必要为了追求尚未证实的可维护性的微小增长而重写<FONT face="Times New Roman">Linux</FONT>的内核。(<FONT face="Times New Roman">Linus</FONT>曾多次特别强调了如下的观点:为了这点利益而损耗速度是不值得的。)后面章节中的部分内容将详细的重新考虑充足开发时间的效果。</P><P 0cm 0cm 0pt">如果<FONT face="Times New Roman">Linux</FONT>是纯微内核设计,那么向其它体系结构上的移植将会比较容易。实际上,有一些微内核,如<FONT face="Times New Roman">Mach</FONT>微内核,就已经成功的证明了这种可移植性的优点。实际的情况是,<FONT face="Times New Roman">Linux</FONT>内核的移植虽然不是很简单,但也绝不是不可能的:大约的数字是,向一个全新的体系结构上的典型的移植工作需要<FONT face="Times New Roman">30,000</FONT>到<FONT face="Times New Roman">60,000</FONT>行代码,再加上不到<FONT face="Times New Roman">20,000</FONT>行的驱动程序代码。(并不是所有的移植都需要新的驱动程序代码。)粗略的计算一下,我估计一个典型的移植平均需要<FONT face="Times New Roman">50,000</FONT>行代码。这对于一个程序员或者最多一个程序小组来说是力所能及的,可以在一年之内完成。虽然这比微内核的移植需要更多的代码,但是<FONT face="Times New Roman">Linux</FONT>的支持者将会提出,这样的<FONT face="Times New Roman">Linux</FONT>内核移植版本比微内核更能够有效的利用底层硬件,因而移植过程中的额外工作是能够从系统性能的提高上得到补偿的。</P><P 0cm 0cm 0pt">这种特殊设计的权衡也不是很轻松就可以达到的,单内核的实现策略公然违背了传统的看法,后者认为微内核是未来发展的趋势。但是由于单一模式(大部分情况下)在<FONT face="Times New Roman">Linux</FONT>中运行状态良好,而且内核移植相对来说比较困难,但没有明显地阻碍程序员团体的工作,他们已经热情高涨地把内核成功的移植到了现存的大部分实际系统中,更不用说类似掌上型电脑的一些看起来很不实际的目标了。只要<FONT face="Times New Roman">Linux</FONT>的众多特点仍然值得移植,新的移植版本就会不断涌现。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">设计和实现的关系</H2><P 0cm 0cm 0pt">接下来的部分将介绍一些内核设计和实现之间的关系。本部分最重要的内容是对于内核源程序目录结构的概述,这一点随后就会提到。本章最后以实现中体系结构无关代码和体系结构相关代码的相对大小的估算作为总结。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5>内核源程序目录结构</FONT></H3><P 0cm 0cm 0pt">按照惯例,内核源程序代码安装在<FONT face="Times New Roman">/usr/src/linux</FONT>目录下。在该目录下还有几个其它目录,每一个都代表一个特定的内核功能性子集(或者非常粗略的说是高层代码模块)。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">Documentation</H4><P 0cm 0cm 0pt">这个目录下面没有内核代码,只有一套有用的文档。但是这些文档的质量不一。有一部分内核文档,例如文件系统,在该目录下有相当优秀而且相当完整的文档;而另外一部分内核,例如进程调度,则根本就没有文档。但是在这里你可以不时的发现自己所最需要的东西。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">arch</H4><P 0cm 0cm 0pt"><FONT face="Times New Roman">arch</FONT>目录下的所有子目录中都是体系结构相关的代码。每个体系结构特有的子目录下都又至少包含三个子目录:<FONT face="Times New Roman">kernel</FONT>,存放支持体系结构特有的诸如信号量处理和<FONT face="Times New Roman">SMP</FONT>之类特征的实现;<FONT face="Times New Roman">lib</FONT>,存放高速的体系结构特有的诸如<B normal"><FONT face="Times New Roman">strlen</FONT></B>和<B normal"><FONT face="Times New Roman">memcpy</FONT></B>之类的通用函数的实现;以及<FONT face="Times New Roman">mm</FONT>,存放体系结构特有的内存管理程序的实现。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman"> </FONT>除了这三个子目录以外,大多数体系结构在必要的情况下还都有一个<FONT face="Times New Roman">boot</FONT>子目录,该目录中包含有在这种平台上启动内核所使用的部分或全部平台特有代码。这些启动代码中的部分或全部也可以在平台特有的内核目录下找到。</P><P 0cm 0cm 0pt">最后,大部分体系结构所特有的目录还可以根据需要包含了供附加特性或改进的组织使用的其它子目录。例如,<FONT face="Times New Roman">i386</FONT>目录包含一个<FONT face="Times New Roman">math-emu</FONT>子目录,其中包括了在缺少数学协处理器(<FONT face="Times New Roman">FPU</FONT>)的<FONT face="Times New Roman">CPU</FONT>上运行模拟<FONT face="Times New Roman">FPU</FONT>的代码。作为另外一个例子,<FONT face="Times New Roman">m68k</FONT>移植版本中为每一个该移植版本所支持的基于<FONT face="Times New Roman">680x0</FONT>的机器建立了一个子目录,从而这些机器所特有的代码都有一个自然的根目录。</P><P 0cm 0cm 0pt">下面几个是<FONT face="Times New Roman">arch</FONT>目录下的子目录:</P><P 0cm 0cm 0pt 26.5pt; TEXT-INDENT: -21.25pt; mso-list: l6 level1 lfo8; tab-stops: list 26.5pt">l         <FONT face="Times New Roman">arch/alpha/</FONT>――<FONT face="Times New Roman">Linux</FONT>内核到基于<FONT face="Times New Roman">DEC Alphs CPU</FONT>工作站的移植。</P><P 0cm 0cm 0pt 26.5pt; TEXT-INDENT: -21.25pt; mso-list: l6 level1 lfo8; tab-stops: list 26.5pt">l         <FONT face="Times New Roman">arch/arm/</FONT>――<FONT face="Times New Roman">Linux</FONT>到<FONT face="Times New Roman">ARM</FONT>系列<FONT face="Times New Roman">CPU </FONT>的移植,该类<FONT face="Times New Roman">CPU</FONT>主要用于诸如<FONT face="Times New Roman">Corel</FONT>的<FONT face="Times New Roman">NetWinder</FONT>和<FONT face="Times New Roman">Acorn RiscPC</FONT>之类的机器。</P><P 0cm 0cm 0pt 26.5pt; TEXT-INDENT: -21.25pt; mso-list: l6 level1 lfo8; tab-stops: list 26.5pt">l         <FONT face="Times New Roman">arch/i386/</FONT>――最接近于<FONT face="Times New Roman">Linux</FONT>内核原始平台或标准平台。这是为<FONT face="Times New Roman">Intel</FONT>的<FONT face="Times New Roman">80386</FONT>结构使用的,当然包括对同一系列后来的<FONT face="Times New Roman">CPU</FONT>(<FONT face="Times New Roman">80486</FONT>,<FONT face="Times New Roman">Pentium</FONT>等等)的支持。它还包括了对<FONT face="Times New Roman">AMD</FONT>,<FONT face="Times New Roman">Cyrix</FONT>和<FONT face="Times New Roman">IDT</FONT>等公司的一些兼容产品的支持。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: 0cm">本书基本上将这种体系结构称为“<FONT face="Times New Roman">x86</FONT>”。即使这样,严格说来“<FONT face="Times New Roman">x86</FONT>”对于我们的目标来说还是要求得过于宽泛。早期的<FONT face="Times New Roman">Intel CPU</FONT>,例如<FONT face="Times New Roman">80286</FONT>,并没有包括<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">CPU</FONT>的独立移植版本是存在的,不过它在功能上有部分损失。)当本书中提到“<FONT face="Times New Roman">x86</FONT>平台”时,通常是指<FONT face="Times New Roman">80386</FONT>或更新的<FONT face="Times New Roman">CPU</FONT>。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l7 level1 lfo9; tab-stops: list 21.25pt">l         <FONT face="Times New Roman">arch/m68k/</FONT>――到<FONT face="Times New Roman">Motorola</FONT>的<FONT face="Times New Roman">680x0 CPU</FONT>系列的移植。该版本可以提供对基于从<FONT face="Times New Roman">68020</FONT>(只要它同内存管理单元(<FONT face="Times New Roman">MMU</FONT>)<FONT face="Times New Roman">68851</FONT>一起使用)到<FONT face="Times New Roman">68060</FONT>的一切机器的支持。很多公司在他们的产品中使用<FONT face="Times New Roman">680x0</FONT>系列芯片,例如<FONT face="Times New Roman">Commodore</FONT>(现在是<FONT face="Times New Roman">Gateway</FONT>)的<FONT face="Times New Roman">Amiga</FONT>,<FONT face="Times New Roman">Apple</FONT>的<FONT face="Times New Roman">Macintosh</FONT>,<FONT face="Times New Roman">Atari ST</FONT>,等等。这些老机器中的很多现在正充当可靠的<FONT face="Times New Roman">Linux</FONT>工作站。另外,到<FONT face="Times New Roman">NeXT</FONT>工作站和<FONT face="Times New Roman">SUN 3</FONT>工作站的移植也正在进行中。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l7 level1 lfo9; tab-stops: list 21.25pt">l         <FONT face="Times New Roman">arch/mips/</FONT>――到<FONT face="Times New Roman">MIPS</FONT>的<FONT face="Times New Roman">CPU</FONT>系列的移植。虽然有其它几个厂商也使用<FONT face="Times New Roman">MIPS</FONT>开发了一些系统,但是基于这种<FONT face="Times New Roman">CPU</FONT>的最出名的机器是<FONT face="Times New Roman">Silicon Graphics</FONT>(<FONT face="Times New Roman">SGI</FONT>)工作站。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l7 level1 lfo9; tab-stops: list 21.25pt">l         <FONT face="Times New Roman">arch/ppc/</FONT>――到<FONT face="Times New Roman">Motorola/IBM</FONT>的<FONT face="Times New Roman">PowerPC</FONT>系列<FONT face="Times New Roman">CPU</FONT>的移植。这包括对基于<FONT face="Times New Roman">PowerPC</FONT>的<FONT face="Times New Roman">Macintosh</FONT>和<FONT face="Times New Roman">Amiga</FONT>以及<FONT face="Times New Roman">BeBox</FONT>、<FONT face="Times New Roman">IBM</FONT>的<FONT face="Times New Roman">RS/6000</FONT>等其它一些机器的支持。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l7 level1 lfo9; tab-stops: list 21.25pt">l         <FONT face="Times New Roman">arch/sparc/</FONT>――到<FONT face="Times New Roman">32</FONT>位<FONT face="Times New Roman">SPARC CPU</FONT>的移植。这包括对从<FONT face="Times New Roman">Sun SPARC 1</FONT>到<FONT face="Times New Roman">SPARC 20</FONT>的全部支持。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l7 level1 lfo9; tab-stops: list 21.25pt">l         <FONT face="Times New Roman">arch/sparc64/</FONT>——到基于<FONT face="Times New Roman">64</FONT>位<FONT face="Times New Roman">SPARC CPU</FONT>(<FONT face="Times New Roman">UltraSPARC</FONT>系)系统的移植。这里所能够支持的机器包括<FONT face="Times New Roman">Sun</FONT>的<FONT face="Times New Roman">Ultra 1</FONT>,<FONT face="Times New Roman">Ultra 2</FONT>和更高配置的机器,直到<FONT face="Times New Roman">Sun</FONT>的最新产品<FONT face="Times New Roman">Enterprise 10000</FONT>。注意<FONT face="Times New Roman">32</FONT>位和<FONT face="Times New Roman">64</FONT>位的<FONT face="Times New Roman">SPARC</FONT>的移植版本正在合并中。</P><P 0cm 0cm 0pt">不幸的是,本书必须将注意力集中在<FONT face="Times New Roman">x86</FONT>上,因此只应用到了<FONT face="Times New Roman">arch/i386/</FONT>目录下的代码,而其它体系结构所特有的代码将不再涉及了。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">drivers</H4><P 0cm 0cm 0pt">这个目录是内核中非常大的一块。实际上,<FONT face="Times New Roman">drivers</FONT>目录下包含的代码占整个内核发行版本代码的一半以上。它包括显卡、网卡、<FONT face="Times New Roman">SCSI</FONT>适配器、软盘驱动器,<FONT face="Times New Roman">PCI</FONT>设备和其它任何你可以说出的<FONT face="Times New Roman">Linux</FONT>支持的外围设备的软件驱动程序。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">Drivers</FONT>目录下的一些子目录是平台特有的,例如,<FONT face="Times New Roman">zorro</FONT>子目录中包含有和<FONT face="Times New Roman">Zorro</FONT>总线通讯的代码。而<FONT face="Times New Roman">Zorro</FONT>总线只在<FONT face="Times New Roman">Amiga</FONT>中使用过,因此这些代码必然是<FONT face="Times New Roman">Amiga</FONT>特有的。而其它一些子目录,例如<FONT face="Times New Roman">pci</FONT>子目录,则至少是部分平台无关的。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">fs</H4><P 0cm 0cm 0pt"><FONT face="Times New Roman">Linux</FONT>支持的所有文件系统在<FONT face="Times New Roman">fs</FONT>目录下面都有一个对应的子目录。一个文件系统(<FONT face="Times New Roman">file system</FONT>)是存储设备和需要访问存储设备的进程之间的媒介。</P><P 0cm 0cm 0pt">文件系统可能是本地的物理上可访问的存储设备,例如硬盘或<FONT face="Times New Roman">CD-ROM</FONT>驱动器;在这两种情况下将分别使用<FONT face="Times New Roman">ext2</FONT>和<FONT face="Times New Roman">isofs</FONT>文件系统。文件系统也可能是可以通过网络访问的存储设备;这种情况下使用的文件系统是<FONT face="Times New Roman">NFS</FONT>。</P><P 0cm 0cm 0pt">还有一些伪文件系统,例如<FONT face="Times New Roman">proc</FONT>文件系统,可以以伪文件的形式提供其它信息(例如,在<FONT face="Times New Roman">proc</FONT>的情况下是提供内核的内部变量和数据结构)。虽然在底层并没有实际的存储设备与这些文件系统相对应,但是进程可以像有实际存储设备一样处理(<FONT face="Times New Roman">NFS</FONT>也可以作为伪文件系统来使用)。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">include</H4><P 0cm 0cm 0pt">这个目录包含了<FONT face="Times New Roman">Linux</FONT>源程序树中大部分的包含(<FONT face="Times New Roman">.h</FONT>)文件。这些文件按照下面的子目录进行分组:</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo10; tab-stops: list 21.25pt">l         <FONT face="Times New Roman">include/asm-*/</FONT>――这样的子目录有多个,每一个都对应着一个<FONT face="Times New Roman">arch</FONT>的子目录,例如<FONT face="Times New Roman">include/asm-alpha</FONT>,<FONT face="Times New Roman">include/asm-arm</FONT>,<FONT face="Times New Roman">include/asm-i386</FONT>等等。每个目录下的文件中包含了支持给定体系结构所必须的预处理器宏和短小的内联函数。这些内联函数很多都是全部或部分地使用汇编语言实现的,而且在<FONT face="Times New Roman">C</FONT>或者汇编代码中都会应用到这些文件。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: 0cm">当编译内核时,系统将建立一个从<FONT face="Times New Roman">include/asm</FONT>到目标体系结构特有的目录的符号链接。结果是体系结构无关内核源程序代码可以使用如下形式的代码来实现所需功能:</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">#include &lt;asm/some-file&gt;</FONT></P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: 0cm">这样就能够将适当地体系结构特有的文件包含(<B normal"><FONT face="Times New Roman">#include</FONT></B>)进来。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo11; tab-stops: list 21.25pt">l         <FONT face="Times New Roman">include/linux/</FONT>――内核和用户应用程序请求特定内核服务时所使用的常量和数据结构在头文件中定义,而该目录中就包含了这些头文件。这些文件大都是平台独立的。这个目录被全部复制(更多的情况是链接)到<FONT face="Times New Roman">/usr/inlude/linux</FONT>下。这样用户应用程序就可以使用<FONT face="Times New Roman">#include</FONT>包含这些头文件,而且能够保证所包含进来的头文件的内容和内核中的定义一致。第<FONT face="Times New Roman">9</FONT>章将会给出有关的一个样例。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo11; tab-stops: list 21.25pt">l         对这些文件的移植只有对于内核来说才是必须的,对用户应用程序则没有必要。移植工作可以按照如下的方式封装处理:</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">/* …  Stuff for user apps and kernel … */</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">#ifdef  __KERNEL__</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       /* … Stuff for kernel only … */</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       #endif  /* __KERNEL__ */</FONT></P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l11 level1 lfo12; tab-stops: list 21.25pt">l         <FONT face="Times New Roman">include/net/</FONT>――这个目录供与网络子系统有关的头文件使用。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l11 level1 lfo12; tab-stops: list 21.25pt">l         <FONT face="Times New Roman">include/scsi/</FONT>――这个目录供与<FONT face="Times New Roman">SCSI</FONT>控制器和<FONT face="Times New Roman">SCSI</FONT>设备有关的头文件使用。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l11 level1 lfo12; tab-stops: list 21.25pt">l         <FONT face="Times New Roman">include/video/</FONT>――这个目录供与显卡和帧显示缓存有关的头文件使用。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">init</H4><P 0cm 0cm 0pt">这个目录下面的两个文件中比较重要的一个是<FONT face="Times New Roman">main.c</FONT>,它包含了大部分协调内核初始化的代码。第<FONT face="Times New Roman">4</FONT>章将详细介绍这部分代码。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">ipc</H4><P 0cm 0cm 0pt">这个目录下的文件实现了<FONT face="Times New Roman">System V</FONT>的进程间通讯(<FONT face="Times New Roman">IPC</FONT>)。在第<FONT face="Times New Roman">9</FONT>章中将会对它们进行详细介绍。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">kernel</H4><P 0cm 0cm 0pt">这个目录中包含了<FONT face="Times New Roman">Linux</FONT>中最重要的部分:实现平台独立的基本功能。这部分内容包括进程调度(<FONT face="Times New Roman">kernel/sched.c</FONT>)以及创建和撤销进程的代码(<FONT face="Times New Roman">kernel/fork.c</FONT>和<FONT face="Times New Roman">kernel/exit.c</FONT>);以上所有的以及其它部分内容将在第<FONT face="Times New Roman">7</FONT>章中有所涉及。但是我并不想给你留下这样的印象:需要了解的内容都在这个目录下。实际上在其它目录下也有很多重要的内容。但是,不管怎样说,最重要部分的代码是在这个目录下的。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">lib</H4><P 0cm 0cm 0pt"><FONT face="Times New Roman">lib</FONT>目录包含两部分的内容。<FONT face="Times New Roman">lib/inflate.c</FONT>中的函数能够在系统启动时展开经过压缩的内核(请参看第<FONT face="Times New Roman">4</FONT>章)。<FONT face="Times New Roman">lib</FONT>目录下剩余的其它文件实现一个标准<FONT face="Times New Roman">C</FONT>库的有用子集。这些实现的焦点集中在字符串和内存操作的函数(<B normal"><FONT face="Times New Roman">strlen</FONT></B>,<B normal"><FONT face="Times New Roman">mmcpy</FONT></B>和其它类似的函数)以及有关<B normal"><FONT face="Times New Roman">sprintf</FONT></B>和<B normal"><FONT face="Times New Roman">atoi</FONT></B>的系列函数上。</P><P 0cm 0cm 0pt">这些文件都是使用<FONT face="Times New Roman">C</FONT>语言编写的,因此在新的内核移植版本中可以立即使用这些文件。正如本章前面部分说明的那样,一些移植提供了它们独有的高速的函数版本,这些函数通常是经过手工调整过的汇编程序,在移植后的系统使用这些函数来代替原来的通用函数。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">mm</H4><P 0cm 0cm 0pt">该目录包含了体系结构无关的内存管理代码。正如我们前面说明的那样,为每个平台实现最低层的原语的体系结构特有的内存管理程序是存储在<FONT face="Times New Roman">arch/platform/mm</FONT>中的。大部分平台独立和<FONT face="Times New Roman">x86</FONT>特有的内存管理代码将在第<FONT face="Times New Roman">8</FONT>章中介绍。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">net</H4><P 0cm 0cm 0pt">这个目录包含了<FONT face="Times New Roman">Linux</FONT>应用的网络协议代码,例如<FONT face="Times New Roman">AppleTalk</FONT>,<FONT face="Times New Roman">TCP/IP</FONT>,<FONT face="Times New Roman">IPX</FONT>等等。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">scripts</H4><P 0cm 0cm 0pt">该目录下没有内核代码,它包含了用来配置内核的脚本。当运行<B normal"><FONT face="Times New Roman">make menuconfig</FONT></B>或者<B normal"><FONT face="Times New Roman">make xconfig</FONT></B>之类的命令配置内核时,用户就是和位于这个目录下的脚本进行交互的。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5>体系结构相关和体系结构无关的代码</FONT></H3><P 0cm 0cm 0pt">现在我们来估计一下体系结构相关和体系结构无关代码的相对大小。我们首先给出一些数字。完整的<st1:chsdate w:st="on" IsROCDate="False" IsLunarDate="False" Day="30" Month="12" Year="1899"><FONT face="Times New Roman">2.2.5</FONT></st1:chsdate>的内核总共有<FONT face="Times New Roman">1,725,645</FONT>行代码。(顺便一提,请注意本书只包含了<FONT face="Times New Roman">39,000</FONT>行代码,但是我们仍然努力涵盖了相当部分的核心函数。)其中一共有<FONT face="Times New Roman">392,884</FONT>行代码在体系结构特有的目录之内,也就是<FONT face="Times New Roman">arch/*</FONT>和<FONT face="Times New Roman">include/asm-*</FONT>下面。我估计还有超过<FONT face="Times New Roman">64,000</FONT>行的代码是仅供一种体系结构专用的驱动程序。这意味着大约<FONT face="Times New Roman">26%</FONT>的代码是专用于特定体系结构的。</P><P 0cm 0cm 0pt">但是,对于单一一种体系结构,体系结构相关代码比例相对较小。不妨理想一点,如果某种体系结构所需要的特有代码约有<FONT face="Times New Roman">50,000</FONT>行,而体系结构无关代码则大约有<FONT face="Times New Roman">1,250,000</FONT>行,那么体系结构相关代码大概只占到<FONT face="Times New Roman">4%</FONT>。当然,在特定的一个内核中,并不是所有这些体系结构无关代码都会被用到,因此体系结构相关代码在特定内核中所占的比重与内核的配置有关。但是不管怎样,很显然大部分内核代码是平台独立的。</P>
作者: ilikenba    时间: 2005-3-4 21:48
<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>
作者: ilikenba    时间: 2005-3-4 21:50
<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&amp;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 &amp; ~0xff;</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            eax |= cs &amp; ~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 &amp; (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">&amp;&amp;</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">&amp;&amp;</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>
作者: ilikenba    时间: 2005-3-4 22:04
<H1 12pt 0cm; TEXT-INDENT: 0cm; tab-stops: 171.0pt"><FONT face="Times New Roman">  </FONT>第<FONT face="Times New Roman">6</FONT>章<FONT face="Times New Roman"> </FONT>信号量,中断和时间</H1>< 0cm 0cm 0pt">信号量(<FONT face="Times New Roman">Signal</FONT>)是进程间通讯(<FONT face="Times New Roman">IPC</FONT>)的一种形式——是一个进程给另一个进程发送信息的方法。但是信息不可能很多——一个信号量不可能携带详细的信息,即使是传送者的身份也不能被传递;唯一能够确定的事实是信号量的确被发送了。(然而和经典信号量不同,<FONT face="Times New Roman">OSIX</FONT>实时信号量允许传送稍微多一点的信息。)实际上,信号量对于双向通讯是没有用处的。还有,根据某些限定,信号量的接受者不必以任何方式作出响应,甚至可以直接忽略大部分信号量。</P>< 0cm 0cm 0pt">虽然有这么多的限制,然而信号量仍然是一种功能强大的十分有用的机制——勿庸置疑,这是<FONT face="Times New Roman">Unix IPC</FONT>中使用最频繁的机制。每当进程退出或者废弃一个空指针时,每当使用<FONT face="Times New Roman">Ctrl+C</FONT>键终止程序运行时,都要传递信号量。</P>< 0cm 0cm 0pt">第<FONT face="Times New Roman">9</FONT>章会更详细的讨论<FONT face="Times New Roman">IPC</FONT>机制。对于本章的讨论来说,信号量的内容就足够讨论了。</P>< 0cm 0cm 0pt">正如在<FONT face="Times New Roman">Linux</FONT>内核本身的代码注释中所说明的一样,中断(<FONT face="Times New Roman">Interrupt</FONT>)对于内核来说和信号量是类似的。中断一般都是从磁盘之类的硬件设备送往内核,用以提示内核该设备需要加以注意。一个重要的硬件中断源就是定时器设备,它周期性地通知内核已经通过的时间。如同第<FONT face="Times New Roman">5</FONT>章中阐述的一样,中断也可以由用户进程通过软件产生。</P>< 0cm 0cm 0pt">在本章中,我们首先讨论一下<FONT face="Times New Roman">Linux</FONT>中信号量和中断的实现,最后再浏览一下<FONT face="Times New Roman">Linux</FONT>的时间处理方式。</P>< 0cm 0cm 0pt">虽然内核对代码的要求标准非常严格,本章所涉及的代码仍然特别清晰明白。本章使用的一般方法是首先介绍相关的数据结构和它们之间的关系,接下来讨论操纵和查询它们的函数。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">锁的概述</H2>< 0cm 0cm 0pt">锁的基本思想是限制对共享资源的访问——共享资源包括共享的文件,共享的内存片,以及在一次只能由一个<FONT face="Times New Roman">CPU</FONT>执行的代码段。概括的说,在单处理器上运行的<FONT face="Times New Roman">Linux</FONT>内核并不需要锁,这是因为在编写<FONT face="Times New Roman">Linux</FONT>内核时就已经注意到要尽量避免各种可能需要锁的情况了。但是,在多处理器机器上,一个处理器有时需要防止其它处理器对它的有害的介入。</P>< 0cm 0cm 0pt"><FONT face="Times New Roman">include/asm-i386/spinlock.h</FONT>文件(从<FONT face="Times New Roman">12582</FONT>行开始)并不使用难看的<B normal"><FONT face="Times New Roman">#ifdef</FONT></B>把所有对锁函数的调用封装起来,它包含一系列对单处理器平台(<FONT face="Times New Roman">UP</FONT>)基本为空的宏,然而在多处理器平台(<FONT face="Times New Roman">SMP</FONT>)上这些宏将展开成为实际代码。因而内核的其它代码对<FONT face="Times New Roman">UP</FONT>和<FONT face="Times New Roman">SMP</FONT>(当涉及到这种特性时)都是相同的,但是它们两个的效果却是迥然不同的。</P>< 0cm 0cm 0pt">第<FONT face="Times New Roman">10</FONT>章中涉及<FONT face="Times New Roman">SMP</FONT>的部分会对锁做深入的介绍。但是,由于你在代码中将到处都能够看到对锁宏的调用,特别是在本章所讨论到的代码中这一点尤为明显,所以你应该首先对宏的用途有初步了解——以及为什么现在在大多数情况下我们都可以安全地将其忽略(我们将在讨论的过程中对其中的异常情况进行说明)。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">信号量</H2>< 0cm 0cm 0pt"><FONT face="Times New Roman">Linux</FONT>内核将信号量分为两类:</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo2; tab-stops: list 42.25pt">l         非实时的(<FONT face="Times New Roman">Nonrealtime</FONT>)——大部分是些传统的信号量,例如<B normal"><FONT face="Times New Roman">SIGSEGV</FONT></B>,<B normal"><FONT face="Times New Roman">SIGHUP</FONT></B>和<B normal"><FONT face="Times New Roman">SIGKILL</FONT></B>。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l5 level1 lfo3; tab-stops: list 42.25pt">l         实时的(<FONT face="Times New Roman">realtime</FONT>)——由<FONT face="Times New Roman">OSIX 1003.1b</FONT>标准规定,它们同非实时信号量有细微的区别。特别是实时信号量具有进程可以配置的意义——就像是非实时信号量<B normal"><FONT face="Times New Roman">SIGUSR1</FONT></B>和<B normal"><FONT face="Times New Roman">SIGUSR2</FONT></B>一样——额外的信息能够和这些信号量一起传送。它们也会排队,因此如果在第一个信号量处理完成之前有多个信号量实例到达,所有的信号量都能够被正确传送;这对于非实时信号量则是不可能的。</P>< 0cm 0cm 0pt">在第<FONT face="Times New Roman">7</FONT>章中我们将会对实时性对于<FONT face="Times New Roman">Linux</FONT>内核的意义进行更详细的介绍——特别是实时性所不能够说明的内容。</P>< 0cm 0cm 0pt">信号量数目的宏定义从<FONT face="Times New Roman">12048</FONT>行开始。实时信号量的数目在<B normal"><FONT face="Times New Roman">SIGRTMIN</FONT></B>和<B normal"><FONT face="Times New Roman">SIGRTMAX</FONT></B>(分别在<FONT face="Times New Roman">12087</FONT>行和<FONT face="Times New Roman">12088</FONT>行)所定义的范围之内。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5>数据结构</FONT></H3>< 0cm 0cm 0pt">本节讨论信号量代码使用的最重要的数据结构。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sigset_t</H4>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12035</FONT>:<B normal"><FONT face="Times New Roman">sigset_t</FONT></B>表示信号量的集合。根据使用地点的不同,它的意思也不同——例如,它可能记录着正在等待某一个进程的信号量(如<FONT face="Times New Roman">16425</FONT>行<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>的<B normal"><FONT face="Times New Roman">signal</FONT></B>成员)的集合,也可能是某个进程已经请求阻塞了的信号量(如同一行中定义的同一结构的<B normal"><FONT face="Times New Roman">blocked</FONT></B>成员)的集合。随着本书的进行,我们会逐渐看到这些类似的应用。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12036</FONT>:<B normal"><FONT face="Times New Roman">sigset_t</FONT></B>的唯一一个组成部分是一组<B normal"><FONT face="Times New Roman">unsigned long</FONT></B>(无符号长整型数),其中的每一位都代表一个信号量。注意到无符号长整型类型在整个内核代码中是作为一个字来处理的,这和你所希望的可能有所出入——即使是在当前<FONT face="Times New Roman">x86 CPU</FONT>的讨论中,有时候字也被用于说明<FONT face="Times New Roman">16</FONT>位类型。由于<FONT face="Times New Roman">Linux</FONT>是一个真<FONT face="Times New Roman">32</FONT>位操作系统,将<FONT face="Times New Roman">32</FONT>位看作是一个字在绝大多数情况下是正确的。(将<FONT face="Times New Roman">Linux</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>位操作系统。)</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>这个数组的大小<B normal"><FONT face="Times New Roman">_NSIG_WORDS</FONT></B>在<FONT face="Times New Roman">12031</FONT>行直接计算。(<B normal"><FONT face="Times New Roman">_NSIG_BPW</FONT></B>中的“<FONT face="Times New Roman">BPW</FONT>”是“<FONT face="Times New Roman">bits per word</FONT>(每字位数)”的缩写。)在不同的平台上,<B normal"><FONT face="Times New Roman">_NSIG_WORDS</FONT></B>的大小从<FONT face="Times New Roman">1</FONT>(<FONT face="Times New Roman">Alpha</FONT>平台中)到<FONT face="Times New Roman">4</FONT>(<FONT face="Times New Roman">MIPS</FONT>平台中)不等。如你所见,在<FONT face="Times New Roman">x86</FONT>平台中,该值正好是<FONT face="Times New Roman">2</FONT>,这意味着在<FONT face="Times New Roman">x86</FONT>平台上<FONT face="Times New Roman">2</FONT>个无符号数就可以包含足够的位数来代表所有<FONT face="Times New Roman">Linux</FONT>使用的信号量。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">struct sigaction</H4>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12165</FONT>:<B normal"><FONT face="Times New Roman">struct sigaction</FONT></B>代表信号量到达时进程应该执行的动作。它被封装在<B normal"><FONT face="Times New Roman">struct k_sigaction</FONT></B>(<FONT face="Times New Roman">12172</FONT>行)结构中,而该结构又是被封装在<B normal"><FONT face="Times New Roman">struct signal_struct </FONT></B>结构中的,后者是<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构的<B normal"><FONT face="Times New Roman">sig</FONT></B>成员所指向的一个实例(<FONT face="Times New Roman">16424</FONT>行)。如果这个指针为空,进程就会退出而不必接受任何信号量。否则,每个进程对于每个信号量数目都需要若干<FONT face="Times New Roman"><B normal">_NSIG</B> <B normal">struct sigaction</B></FONT>结构和一个<B normal"><FONT face="Times New Roman">struct sigaction</FONT></B>结构。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12166</FONT>:<B normal"><FONT face="Times New Roman">sa_handler</FONT></B>(<B normal"><FONT face="Times New Roman">__sighandler_t</FONT></B>类型——一个在<FONT face="Times New Roman">12148</FONT>行定义的函数指针类型)描述了进程希望处理信号量的方式。其值可以是下面中的一个:</P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo4; tab-stops: list 57.25pt">l         <B normal"><FONT face="Times New Roman">SIG_DFL</FONT></B>(<FONT face="Times New Roman">12151</FONT>行)申请处理信号量的缺省操作,不管该操作是什么——这是由信号量所决定的。注意它和<B normal"><FONT face="Times New Roman">NULL</FONT></B>是等同的。</P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo5; tab-stops: list 57.25pt">l         <B normal"><FONT face="Times New Roman">SIG_IGN</FONT></B>(<FONT face="Times New Roman">12153</FONT>行)意味着信号量应该忽略。但是,并不是所有的信号量都可以被忽略的。</P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l6 level1 lfo6; tab-stops: list 57.25pt">l         所有的其它值都是在信号量到达时所需要调用的用户空间函数的地址。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12167</FONT>:<B normal"><FONT face="Times New Roman">sa_flags</FONT></B>进一步调整信号量处理代码所完成的工作。可能的标志集合从<FONT face="Times New Roman">12108</FONT>行开始定义。这些标志允许用户代码在信号量实例发送以后(或者保留用户定制的操作时)请求恢复缺省操作,等等。这一点在宏定义块前面的标签注释中已经说明了。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12168</FONT>:<B normal"><FONT face="Times New Roman">sa_restorer</FONT></B>是本书中所没有涉及的一些信号量处理代码细节所使用的。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12169</FONT>:<B normal"><FONT face="Times New Roman">sa_mask</FONT></B>是一系列其它信号量的集合,进程在处理这些信号量的过程中可能需要进行锁定。例如,如果一个进程在处理<B normal"><FONT face="Times New Roman">SIGCHLD</FONT></B>的时候希望锁定<B normal"><FONT face="Times New Roman">SIGHUP</FONT></B>和<FONT face="Times New Roman">      <B normal">SIGINT</B></FONT>,进程的第<B normal"><FONT face="Times New Roman">SIGCHLD</FONT></B>个<B normal"><FONT face="Times New Roman">sa_mask</FONT></B>就会对与<B normal"><FONT face="Times New Roman">SIGHUP</FONT></B>和<B normal"><FONT face="Times New Roman">SIGINT</FONT></B>相关的位进行置位。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">siginfo_t</H4>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11851</FONT>:<B normal"><FONT face="Times New Roman">struct siginfo</FONT></B>(也称为<B normal"><FONT face="Times New Roman">siginfo_t</FONT></B>)是伴随着信号量,特别是在实时信号量,所传递的额外信息。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11852</FONT>:勿庸置疑,<B normal"><FONT face="Times New Roman">si_signo</FONT></B>是信号量的数目。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11853</FONT>:<B normal"><FONT face="Times New Roman">si_errno</FONT></B>应该是信号量传递时传送者的<B normal"><FONT face="Times New Roman">errno</FONT></B>的值,这样接收者就可以对它进行检测。内核本身并不关心这个值;当在某些情况下需要设置这个值时,内核将其设置为<FONT face="Times New Roman">0</FONT>。我推测如果这样,即使调用者没有设置这个值,它们仍然会发现<B normal"><FONT face="Times New Roman">si_error</FONT></B>的值被设为已知状态。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11854</FONT>:<B normal"><FONT face="Times New Roman">si_code</FONT></B>记录了信号量的来源(不是发送者的进程<FONT face="Times New Roman">ID</FONT>号,也就是<FONT face="Times New Roman">PID</FONT>——它在别处记录)。有效的信号量来源在<FONT face="Times New Roman">11915</FONT>行及其随后部分使用宏进行了定义。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11856</FONT>:该结构的最后一部分是<B normal"><FONT face="Times New Roman">union</FONT></B>类型的;该<B normal"><FONT face="Times New Roman">union</FONT></B>类型依赖于<B normal"><FONT face="Times New Roman">si_code</FONT></B>的值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11857</FONT>:<B normal"><FONT face="Times New Roman">union</FONT></B>的第一部分是<B normal"><FONT face="Times New Roman">_pad</FONT></B>,它将<B normal"><FONT face="Times New Roman">siginfo_t</FONT></B>的长度扩展填充为<FONT face="Times New Roman">128*<B normal">sizeof</B></FONT><B normal">(<FONT face="Times New Roman">int</FONT></B><B normal">)</B>字节(在<FONT face="Times New Roman">x86</FONT>平台上一共是<FONT face="Times New Roman">512</FONT>个字节)。留意一下这个数组的大小,也就是<B normal"><FONT face="Times New Roman">SI_PAD_SIZE</FONT></B>(<FONT face="Times New Roman">11849</FONT>行),代表了该结构的前三个成员——如果增加了更多的成员,<B normal"><FONT face="Times New Roman">SI_PAD_SIZE</FONT></B>就需要进行相应修改。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">struct signal_queue</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">17132</FONT>:<B normal"><FONT face="Times New Roman">struct signal_queue</FONT></B>结构用来确保所有的实时信号量都被正确传送了,如果可能,每一个都包含着额外信息(<B normal"><FONT face="Times New Roman">siginfo_t</FONT></B>)。如同后面你将会看到的一样,内核会为每个进程都设置一个队列,用来存放该进程的挂起的实时信号量。这个队列类型本身很小,仅仅由一个指向下一个节点的指针和<B normal"><FONT face="Times New Roman">siginfo_t</FONT></B>本身组成。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">应用函数</H2><P 0cm 0cm 0pt">有关信号量的一个最重要的数据结构是<B normal"><FONT face="Times New Roman">sigset_t</FONT></B>,它是由一系列在<FONT face="Times New Roman">include/linux/signal.h</FONT>文件中定义的简单函数所操纵的,这些函数的定义从<FONT face="Times New Roman">17123</FONT>行开始。在<FONT face="Times New Roman">x86</FONT>平台上,这些相同的函数可以——而且已经——使用汇编语言更加有效的实现了;这些更高效的版本从<FONT face="Times New Roman">12204</FONT>行开始。(<FONT face="Times New Roman">m68k</FONT>端口是唯一一个例外的端口,它使用体系结构特有的代码实现。)由于平台无关的版本和<FONT face="Times New Roman">x86</FONT>特有的版本都很重要,我们会对两者都加以介绍。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5><I normal">平台无关的<FONT face="Times New Roman">sigset_t</FONT></I><I normal">函数<p></p></I></FONT></H3><P 0cm 0cm 0pt">配合<B normal"><FONT face="Times New Roman">sigset_t</FONT></B>使用的平台无关的函数在<FONT face="Times New Roman">include/linux/sigal.h</FONT>文件中,从<FONT face="Times New Roman">17123</FONT>行开始。称为“<FONT face="Times New Roman">bitops</FONT>”(位级的操作)的函数将在后面介绍。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sigaddset</H4><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">17145</FONT>:<B normal"><FONT face="Times New Roman">sigaddset</FONT></B>把一个信号量加入集合——也就是说,它修改了集合中的一位。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">17147</FONT>:为了便于位操作,将基于<FONT face="Times New Roman">0</FONT>的信号量转化为基于<FONT face="Times New Roman">1</FONT>的信号量。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">17149</FONT>:如果信号量中填入一个无符号长整型数,恰当的位就会被设置。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">17151</FONT>:否则,<B normal"><FONT face="Times New Roman">sigaddset</FONT></B>就需要绕很多弯路,首先装入恰当的数组元素,接着设置该元素中相关位。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">17148</FONT>行的代码和该文件中后面的其它代码一样,第一次见到时可能会令人感到有些困惑。在内核代码中,速度是压倒一切的因素。从而,也许你并不会看到类似于下面的运行期间进行决定的代码:</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       if (_NSIG_WORDS == 1)</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">          set-&gt;sig[0] |=1UL &lt;&lt; sig;</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       else</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">          set-&gt;sig[sig / <I normal">NSIG</I>BPW] |= 1UL &lt;&lt; (sig % <I normal">NSIG</I>BPW);</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm">而你看到的是类似于下面的在编译期间决定的代码:</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       #if (_NSIG_WORDS == 1)</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">          set-&gt;sig[0] |=1UL &lt;&lt; sig;</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       #else</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">          set-&gt;sig[sig / <I normal">NSIG</I>BPW] |= 1UL &lt;&lt; (sig % <I normal">NSIG</I>BPW);</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">       #endif</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">难道这样不会运行的更快些吗?不要忘了,<B normal"><FONT face="Times New Roman">if</FONT></B>条件是能够在编译期间进行计算的,因此预处理器可以使系统没有必要在运行期间执行检测工作。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">当你认识到优化工作的实现方式时,这也就没有什么神秘的了。<FONT face="Times New Roman">gcc</FONT>的优化器的敏锐程度足以注意到<B normal"><FONT face="Times New Roman">if</FONT></B>表达式只有一个出口,因此它可以把那些不必要的代码移走。作为内核“运行期间”版本的结果代码和“编译期间”的版本是等同的。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">但是在我们使用优化器很糟糕的编译器时,基于预处理器的版本还会更好吗?这一点并不确定。问题之一是,基于预处理器的(编译期间的)版本有一点更难懂。当代码的复杂程度比前面的简单例子要高时,可读性的差别就会明显的显示出来。例如,让我们考虑一下<FONT face="Times New Roman"><B normal">sigemptyse</B>t</FONT>中的从<FONT face="Times New Roman">17264</FONT>行开始的<B normal"><FONT face="Times New Roman">switch</FONT></B>。现在的<B normal"><FONT face="Times New Roman">switch</FONT></B>类似于这样:</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">swithc (_NSIG_WORDS) {</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">default:</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">  memset(set, 0, sizeof(sigset_t));</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">  break;</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">case 2:  set-&gt;sig[1] = 0;</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">case 1:  set-&gt;sig[0] = 0;</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">  break;</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">}</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">(请注意经周密考虑的<FONT face="Times New Roman">case 2</FONT>随<FONT face="Times New Roman">case 1</FONT>连续执行的情况。)为了更好的利用预处理器而将其重写,它就可能类似于:</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">#if ((_NSIG_WORDS != 2)) &amp;&amp; \ (_NSIG_WORDS != 1)</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">  memset(set, 0, sizeof(sigset_t));</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">#else /* (_NSIG_WORDS is 2 or 1). */</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">#if (_NSIG_WORDS == 2)</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">  set-&gt;sig[1] = 0;</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">#endif</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">  set-&gt;sog[0] = 0;</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">#endif /* _NSIG_WORDS test. */</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">gcc</FONT>的优化器为两者产生的目标代码是相同的。你更希望读哪一种版本的源程序代码呢?</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">另外,即使编译器的优化器并没有这么好——这种优化实在相当简单——那么编译器就不可能生成很好的代码。不管我们提供多少帮助都注定是不够的,因此我们可能要编写一些更容易读、更容易维护的代码——这是又一项工程技术的权衡。最后,就象我们在前面的内容中已经看到而且还要不断看到的那样,使用除<FONT face="Times New Roman">gcc</FONT>之外的编译器编译内核本身就是个挑战——增加一段<FONT face="Times New Roman">gcc</FONT>特有代码不会引起更多问题的。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sigdelset</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">17154</FONT>:这些代码和<B normal"><FONT face="Times New Roman">sigaddset</FONT></B>非常类似;区别在于这里从集合中删去了一位——就是把相应的位设置为关。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sigismember</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">17163</FONT>:这些代码和<B normal"><FONT face="Times New Roman">sigaddset</FONT></B>也非常类似;这里是要测试某一位是否被设置。注意到<FONT face="Times New Roman">17167</FONT>行可能和下面的这种写法有同样的好处:</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            return set-&gt;sig[0] &amp; (1UL &lt;&lt; sig);</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>这种写法与<FONT face="Times New Roman">17169</FONT>行非常相似。虽然这样能够和其它函数的编写风格更加一致,但是这并不是什么改进。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>这些修改将对函数的行为方式稍有改动:它现在返回<FONT face="Times New Roman">0</FONT>或<FONT face="Times New Roman">1</FONT>,经过这种修改,就可以在一个位被设置时返回其它的非<FONT face="Times New Roman">0</FONT>值。但是,这种改变不会终止没有退出的代码,因为其调用者只关心返回值是否为<FONT face="Times New Roman">0</FONT>(它们并不特别在意是否为<FONT face="Times New Roman">1</FONT>)。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sigfindinword</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">17172</FONT>:这个函数返回<B normal"><FONT face="Times New Roman">word</FONT></B>中设置的第一个位的位置。函数<B normal"><FONT face="Times New Roman">ffz</FONT></B>(在本书中没有涉及)返回其参数中第一个<FONT face="Times New Roman">0</FONT>位的位置。在将位求补的字中的第一个<FONT face="Times New Roman">0</FONT>的位置——这正是这个函数搜寻的内容——显然是原始顺序中的第一个<FONT face="Times New Roman">1</FONT>的位置。它从最小位<FONT face="Times New Roman">0</FONT>开始计算。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sigmask</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">17177</FONT>:最后,这个有用的<B normal"><FONT face="Times New Roman">sigmask</FONT></B>宏简单的把信号量数目通过一个相应的位集合转化为一个位掩码。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5><I normal">平台相关的<FONT face="Times New Roman">sigset_t</FONT></I><I normal">函数</I></FONT></H3><P 0cm 0cm 0pt">即使平台无关的版本已经使用了简单有效的<FONT face="Times New Roman">C</FONT>代码,它也可以通过使用<FONT face="Times New Roman">x86 CPU</FONT>家族的方便而功能强大的位集指令在<FONT face="Times New Roman">x86</FONT>平台上更加有效地实现,。这些函数中的大部分都可以减少为单独的机器指令,因此这里的讨论也都很精简。</P><P 0cm 0cm 0pt">在<FONT face="Times New Roman">x86</FONT>平台(例如<FONT face="Times New Roman">m68k</FONT>)上平台无关的函数对于编译器甚至是不可见的。<FONT face="Times New Roman">17126</FONT>行包含进了<FONT face="Times New Roman">asm/signal.h</FONT>文件,在<FONT face="Times New Roman">x86</FONT>上这个文件被分解为<FONT face="Times New Roman">include/asm-i386/signal.h</FONT>,这都应该归功于设置文件所建立的符号链接。<FONT face="Times New Roman">12202</FONT>行定义了预处理器符号<B normal"><FONT face="Times New Roman">__HAVE_ARCH_SIG_BITOPS</FONT></B>,它消除了这些平台无关的函数的定义(请参看<FONT face="Times New Roman">17140</FONT>行)。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sigaddset</H4><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">12204</FONT>:<FONT face="Times New Roman">x86</FONT>特有的使用<B normal"><FONT face="Times New Roman">btsl</FONT></B>指令的<B normal"><FONT face="Times New Roman">sigaddset</FONT></B>实现,它仅对操作数的一个位进行设置。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sigdelset</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12210</FONT>:同样,这是<FONT face="Times New Roman">x86</FONT>特有的使用<B normal"><FONT face="Times New Roman">btrl</FONT></B>指令的<B normal"><FONT face="Times New Roman">sigdelset</FONT></B>实现,它对操作数的一个位进行重置(清除)。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sigismember</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12233</FONT>:<B normal"><FONT face="Times New Roman">sigismember</FONT></B>根据其<B normal"><FONT face="Times New Roman">sig</FONT></B>参数是否是一个编译期常量表达式来选择实现方法。文档中所没有说明的<FONT face="Times New Roman">gcc</FONT>编译器的强大的特殊参数<B normal"><FONT face="Times New Roman">__builtin_constant_p</FONT></B>是一个编译期操作符(就象<B normal"><FONT face="Times New Roman">sizeof</FONT></B>一样),它能够报告是否可以在编译期间计算其参数值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>如果可以,<B normal"><FONT face="Times New Roman">sigismember</FONT></B>使用<B normal"><FONT face="Times New Roman">__const_sigismember</FONT></B>函数(<FONT face="Times New Roman">12216</FONT>行)完成这项工作,因为它的大部分表达式都可以在编译期间计算。否则就使用更为普遍的版本<B normal"><FONT face="Times New Roman">__gen_sigismember</FONT></B>函数(<FONT face="Times New Roman">12224</FONT>行)来代替。更普遍的版本中使用的是<FONT face="Times New Roman">x86</FONT>的<B normal"><FONT face="Times New Roman">btl</FONT></B>指令,它需要测试其操作数中的某一位。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>注意到在编译期的常量合并和死锁代码消除通常意味着这样的完整测试只能在编译期间执行——关键是<B normal"><FONT face="Times New Roman">sigismember</FONT></B>要根据需要使用<B normal"><FONT face="Times New Roman">__const_sigismember</FONT></B>或者<B normal"><FONT face="Times New Roman">__gen_sigismember</FONT></B>替换,在作为结果的目标代码中甚至完全看不出来根本就没有对另一部分进行考虑。这样相当精简,难道不是吗?</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sigmask</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12238</FONT>:<FONT face="Times New Roman">x86</FONT>特有的<B normal"><FONT face="Times New Roman">sigmask</FONT></B>的实现,这与平台无关的版本是等同的。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sigfindinword</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12240</FONT>:最后,<FONT face="Times New Roman">x86</FONT>特有的<B normal"><FONT face="Times New Roman">sigfindinword</FONT></B>实现只使用了<FONT face="Times New Roman">x86</FONT>的<B normal"><FONT face="Times New Roman">bsfl</FONT></B>指令,它在自己的操作数中寻找一个设置位。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><I normal"><FONT size=5>设置函数<p></p></FONT></I></H3><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">除了前面的那一组函数之外,还有一组对<B normal"><FONT face="Times New Roman">sigset_t</FONT></B>执行设置操作的函数和宏。和前面一组类似,这些函数使用<B normal"><FONT face="Times New Roman">__HAVE_ARCH_SIG_SETOPS</FONT></B>预处理器符号保护起来。然而现在没有一种体系结构能够提供自己独有的这些函数的实现,正因为如此,体系结构无关的版本是现存的唯一版本。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">_SIG_SET_BINOP<p></p></H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">17184</FONT>:我们希望定义的全部三个二进制操作——<B normal"><FONT face="Times New Roman">sigorsets</FONT></B>,<B normal"><FONT face="Times New Roman">sigandsets</FONT></B>和<B normal"><FONT face="Times New Roman">signandsets</FONT></B>——的实现方式从本质上来说是相同的。这个宏简单的把这三个函数的共同代码分解出来,从而只给它们提供一个操作和一个名字。当然,这同<FONT face="Times New Roman">C++</FONT>模版函数类似,不过这样我们不得不自己处理一些记录工作,而不能完全信任编译器——这是我们使用<FONT face="Times New Roman">C</FONT>工作所付出的一部分代价。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">17191</FONT>:程序开始在<B normal"><FONT face="Times New Roman">sigset_t</FONT></B>中全部四个字节的无符号长整型数的循环,同时对这些操作数进行应用。这个循环是为了速度的原因而展开的——通过减少循环控制开销来提高速度,这是很出名的一种增加速度的方法。然而,大多数情况下,这个循环根本就不执行。例如,在<FONT face="Times New Roman">x86</FONT>平台上,编译器可以在运行期间就证实不会执行该循环体,因为截断取整以后,<FONT face="Times New Roman"><B normal">_NSIG_WORDS</B>/4</FONT>的结果是<FONT face="Times New Roman">0</FONT>。(回忆一下<B normal"><FONT face="Times New Roman">_NSIG_WORDS</FONT></B>在<FONT face="Times New Roman">x86</FONT>平台上的值为<FONT face="Times New Roman">2</FONT>。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">17201</FONT>:<B normal"><FONT face="Times New Roman">switch</FONT></B>从循环末尾处理剩余工作的这行开始。如果在某些平台上<B normal"><FONT face="Times New Roman">_NSIG_WORDS</FONT></B>正好为<FONT face="Times New Roman">6</FONT>,那么该循环就可以执行一次,而且<B normal"><FONT face="Times New Roman">switch</FONT></B>的情况<FONT face="Times New Roman">2</FONT>也可以被执行。在<FONT face="Times New Roman">x86</FONT>平台上,循环永远不会执行;只有<B normal"><FONT face="Times New Roman">switch</FONT></B>的情况<FONT face="Times New Roman">2</FONT>才可能执行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>顺便说一下,我并不清楚为什么<B normal"><FONT face="Times New Roman">switch</FONT></B>不和其类似的<B normal"><FONT face="Times New Roman">_SOG_SET_OP</FONT></B>一样使用直接流程的方式实现。通常情况下,现存的版本可以更充分的利用缓存(如果你试图重新编写它,那么你就可以清楚的认识到这一点)——但是如果实际原因的确如此,那么<B normal"><FONT face="Times New Roman">_SIG_SET_OP</FONT></B>也应该使用相同的参数。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">_SIG_SET_OP<I normal"><p></p></I></H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">17238</FONT>:<B normal"><FONT face="Times New Roman">_SIG_SET_OP</FONT></B>和<B normal"><FONT face="Times New Roman">_SIG_SET_BINOP</FONT></B>类似,但是它使用的是一元操作而不是二元操作,因此我们并不需要详细地介绍它。但是你应该注意的是,这只能使用一次——在<FONT face="Times New Roman">17257</FONT>行生成<B normal"><FONT face="Times New Roman">signotset</FONT></B>——这和<B normal"><FONT face="Times New Roman">_SIG_SET_BINOP</FONT></B>不同。因此,在某种程度上这是不需要的——其实现者可以直接编写<B normal"><FONT face="Times New Roman">signotset</FONT></B>,而不必借助<B normal"><FONT face="Times New Roman">_SIG_SET_OP</FONT></B>,这并没有产生任何重复代码。然而,二者生成的目标代码是相同的,这样如果我们以后选择增加一元操作,意义也就不大了。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sigemptyset</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">17262</FONT>:<B normal"><FONT face="Times New Roman">sigemptyset</FONT></B>清空所提供的集合——要把其中的每一位都清空。(下面一个函数<B normal"><FONT face="Times New Roman">sigfillset</FONT></B>和这个函数功能相同,不过它要设置所有的位而不是清除所有的位,因此我们就不再详细介绍了。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">17265</FONT>:普通情况下使用<B normal"><FONT face="Times New Roman">memset</FONT></B>把集合中的每一位都置为<FONT face="Times New Roman">0</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">17268</FONT>:对于<B normal"><FONT face="Times New Roman">_NSIG_WORDS</FONT></B>的一些比较小的值来说,简单的直接设置<B normal"><FONT face="Times New Roman">sigset_t</FONT></B>的一两个元素可能速度更快。在这里采用的就是这种直接流程实现。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sigaddsetmask</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">17292</FONT>:该函数和下面的几个函数是更简单快速设置和读取最低的<FONT face="Times New Roman">32</FONT>位(或者根据字的大小)信号量的一系列函数。<B normal"><FONT face="Times New Roman">sigaddsetmask</FONT></B>简单地把<B normal"><FONT face="Times New Roman">mask</FONT></B>所指定的位置位,而不对剩余的位进行任何处理——这是一个集合的联合操作。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">siginitset</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">17310</FONT>:根据提供的掩码对最低<FONT face="Times New Roman">32</FONT>位(或者是别的)置位,并将其它位设置为<FONT face="Times New Roman">0</FONT>。下面一个函数<B normal"><FONT face="Times New Roman">siginitsetinv</FONT></B>(<FONT face="Times New Roman">17323</FONT>行)正好相反:它根据掩码的补数设置最低<FONT face="Times New Roman">32</FONT>位(或者别的),并对其余的位置位。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">传送信号量</H2><P 0cm 0cm 0pt">从用户的观点来看,传送信号量相当简单:调用系统调用<B normal"><FONT face="Times New Roman">kill</FONT></B>,该调用只需要进程<FONT face="Times New Roman">ID</FONT>号和一个信号量。但是,正如本节中所显示的那样,其实现要复杂得多。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sys_kill</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28768</FONT>:<B normal"><FONT face="Times New Roman">sys_kill</FONT></B>是系统调用<B normal"><FONT face="Times New Roman">kill</FONT></B>的一个具有欺骗性的实现样例;真正的实际工作是在<B normal"><FONT face="Times New Roman">kill_somethig-info</FONT></B>中实现的,我们随后就将对这个方面进行研究。<B normal"><FONT face="Times New Roman">sys_kill</FONT></B>的参数是要传递的信号量<B normal"><FONT face="Times New Roman">sig</FONT></B>和信号量的目的<B normal"><FONT face="Times New Roman">pid</FONT></B>。就象你将看到的那样,参数<B normal"><FONT face="Times New Roman">pid</FONT></B>并不仅是进程<FONT face="Times New Roman">ID</FONT>。(<FONT face="Times New Roman">PID</FONT>和进程的其它概念都在第<FONT face="Times New Roman">7</FONT>章中详细介绍。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28770</FONT>:根据提供给<B normal"><FONT face="Times New Roman">sys_kill</FONT></B>的信息声明并填充<B normal"><FONT face="Times New Roman">struct siginfo</FONT></B>结构。特别要注意的是<B normal"><FONT face="Times New Roman">si_code</FONT></B>是<B normal"><FONT face="Times New Roman">SI_USER</FONT></B>(因为只有用户进程才可以调用该系统调用;内核本身是不会调用系统调用的,它更倾向于使用低层函数)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28778</FONT>:传递这些信息给<B normal"><FONT face="Times New Roman">kill_something_info</FONT></B>,该函数处理实际的工作。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">kill_something_info</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28484</FONT>:该函数的参数和<B normal"><FONT face="Times New Roman">sys_kill</FONT></B>类似,但是增加了一项<B normal"><FONT face="Times New Roman">siginfo</FONT></B>结构的指针。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28487</FONT>:如果<B normal"><FONT face="Times New Roman">pid</FONT></B>为<FONT face="Times New Roman">0</FONT>,就意味着当前进程希望把信号量传递给整个进程组,该工作由<B normal"><FONT face="Times New Roman">kill_pg_info</FONT></B>(<FONT face="Times New Roman">28408</FONT>行)完成。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28489</FONT>:如果<B normal"><FONT face="Times New Roman">pid</FONT></B>是<FONT face="Times New Roman">-1</FONT>,信号量(几乎)被送往系统中的每一个进程,这在下面的段落中介绍。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28494</FONT>:使用<FONT face="Times New Roman"><B normal">for_ech_tas</B>k</FONT>宏(在<FONT face="Times New Roman">16898</FONT>行宏定义,第<FONT face="Times New Roman">7</FONT>章中详细介绍)开始循环处理现存进程列表的每一项。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28496</FONT>:如果这不是<FONT face="Times New Roman">idle</FONT>进程(或<FONT face="Times New Roman">init</FONT>),就使用<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>(<FONT face="Times New Roman">28218</FONT>行,后面将会讨论)传递信号量。每次发现合适的任务时<B normal"><FONT face="Times New Roman">count</FONT></B>的值都会增加,虽然<B normal"><FONT face="Times New Roman">kill_something_info</FONT></B>并不关心<B normal"><FONT face="Times New Roman">count</FONT></B>的实际值,而是在意是否能够发现合适的进程。如果所有试图发送信号量的努力都失败了,将记录失败的过程以使得<B normal"><FONT face="Times New Roman">kill_something_info</FONT></B>可以在<FONT face="Times New Roman">28503</FONT>行返回错误代码;如果发生了多次错误,则只返回最后一次失败的情况。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28503</FONT>:如果发现了合适的候选进程,<B normal"><FONT face="Times New Roman">kill_something_info</FONT></B>就返回最近失败的错误代码,或者成功就返回<FONT face="Times New Roman">0</FONT>。如果没有发现任何合适的候选进程,就返回<B normal"><FONT face="Times New Roman">ESRCH</FONT></B>错误。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28504</FONT>:其它负的<B normal"><FONT face="Times New Roman">pid</FONT></B>(是负值,但不是<FONT face="Times New Roman">-1</FONT>)定义了接收信号量的进程组;<B normal"><FONT face="Times New Roman">pid</FONT></B>的绝对值是进程组号。和前面一样,<B normal"><FONT face="Times New Roman">kill_pg_info</FONT></B>的使用就是出于这种目的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28506</FONT>:其它的所有可能性都已经进行了说明;<B normal"><FONT face="Times New Roman">pid</FONT></B>必须为正数。在这种情况下,它是信号量传送的目的进程的<FONT face="Times New Roman">PID</FONT>。这由<B normal"><FONT face="Times New Roman">kill_proc_info</FONT></B>实现(<FONT face="Times New Roman">28463</FONT>行,很快就会讨论)。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">kill_pg_info</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28408</FONT>:这个函数给进程组中的每一个进程发送一个信号量和一个<B normal"><FONT face="Times New Roman">struct siginfo</FONT></B>结构。其函数体和前面介绍的<B normal"><FONT face="Times New Roman">kill_something_info</FONT></B>类似,因此我只是简单介绍一下。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28417</FONT>:开始循环处理系统中的所有进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28418</FONT>:如果进程在正确的进程组中,那么信号量就发送给它。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28427</FONT>:如果信号量成功发送给任何进程,<B normal"><FONT face="Times New Roman">retval</FONT></B>就设置为<FONT face="Times New Roman">0</FONT>,从而在<FONT face="Times New Roman">28430</FONT>行成功返回。如果信号量不能被发往任何进程,那么要么是所给的进程组中没有进程,在这种情况下,<B normal"><FONT face="Times New Roman">reval</FONT></B>仍然会在<FONT face="Times New Roman">28415</FONT>行赋值为<B normal"><FONT face="Times New Roman">-ESRCH</FONT></B>;或者<B normal"><FONT face="Times New Roman">kill_pg_info</FONT></B>发送信号量给一个或多个进程,但是每次都失败了,在这种情况下<B normal"><FONT face="Times New Roman">retval</FONT></B>值为从<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>得到的最近错误代码。注意它和<B normal"><FONT face="Times New Roman">kill_something_info</FONT></B>的细微区别,后者如果发送信号量失败时就返回错误。但是这里的<B normal"><FONT face="Times New Roman">kill_pg_info</FONT></B>即使在某些情况下出错了,只要信号能成功地传递给任意进程,就会返回成功信息。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28430</FONT>:在<FONT face="Times New Roman">28410</FONT>行中,如果进程组号无效,<B normal"><FONT face="Times New Roman">retval</FONT></B>或者是如前所述的赋值,或者就是<B normal"><FONT face="Times New Roman">-EINVAL</FONT></B>。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">kill_proc_info</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28463</FONT>:<B normal"><FONT face="Times New Roman">kill_proc_info</FONT></B>是一个相当简单的函数,它把信号量和<FONT face="Times New Roman"><B normal">struct</B> <B normal">siginfo</B></FONT>结构传递给由<FONT face="Times New Roman">PID</FONT>定义的单个进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28469</FONT>:通过所提供的<FONT face="Times New Roman">PID</FONT>查找相应的进程;如果成功<B normal"><FONT face="Times New Roman">find_task_by_pid</FONT></B>(<FONT face="Times New Roman">16570</FONT>行)返回一个指向该进程的指针,如果没有找到该进程就返回<B normal"><FONT face="Times New Roman">NULL</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28472</FONT>:如果找到匹配进程,就使用<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>把信号量传送给目的进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28474</FONT>:返回错误指示,或者是在<FONT face="Times New Roman">28470</FONT>行由于没有发现匹配进程而返回<B normal"><FONT face="Times New Roman">-ESRCH</FONT></B>,或者是其它情况下从<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>中返回的值。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">send_sig_info</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28218</FONT>:<FONT face="Times New Roman"> </FONT>我们最后看的几个函数中最重要的显然是<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>。这个函数使用不同的方法装载进程并处理实际的工作。现在应该了解一下实际的工作是如何完成的。<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>将使用<B normal"><FONT face="Times New Roman">info</FONT></B>指针(该指针也可能为<B normal"><FONT face="Times New Roman">NULL</FONT></B>)指向额外信息的信号量<B normal"><FONT face="Times New Roman">sig</FONT></B>传送给<B normal"><FONT face="Times New Roman">t</FONT></B>指针(调用者应该保证<B normal"><FONT face="Times New Roman">t</FONT></B>不会为<B normal"><FONT face="Times New Roman">NULL</FONT></B>)指向的进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28229</FONT>:确保<B normal"><FONT face="Times New Roman">sig</FONT></B>在范围之内。注意使用的是如下的测试</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            sig &gt; _NSIG</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">            sig &gt;= _NSIG</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>这是因为信号量的计数是从<FONT face="Times New Roman">1</FONT>开始的,而不是从<FONT face="Times New Roman">0</FONT>开始的。因此虽然不存在对这个信号量编号的定义,有效信号量的编号的标识符<B normal"><FONT face="Times New Roman">_NSIG</FONT></B>本身也是有效的信号量编号。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28233</FONT>:这是另外一个严密性检查——实际上包含多个检验。基本的思想是检测信号量的传送是否合法。虽然内核本身可以给任何进程传送信号量,但是除了在涉及<B normal"><FONT face="Times New Roman">SIGCONT</FONT></B>的情况之外,除<FONT face="Times New Roman">root</FONT>之外的用户都不能给其它用户的进程传送信号量。总之,这个长长的<B normal"><FONT face="Times New Roman">if</FONT></B>条件说明了如下问题:</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo7; tab-stops: list 57.25pt">l         (<FONT face="Times New Roman">28233</FONT>行)如果不存在补充信息,或者虽然存在补充信息,但是信号量来源于用户而不是内核,并且<FONT face="Times New Roman">…</FONT></P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo7; tab-stops: list 57.25pt">l         (<FONT face="Times New Roman">28235</FONT>行)<FONT face="Times New Roman">…</FONT>信号量不是<B normal"><FONT face="Times New Roman">SIGCONT</FONT></B>,或者虽然信号量是<B normal"><FONT face="Times New Roman">SIGCONT</FONT></B>,但是并不是传送给同一会话过程中的其它进程,并且<FONT face="Times New Roman">…</FONT></P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo7; tab-stops: list 57.25pt">l         (<FONT face="Times New Roman">28237</FONT>行和<FONT face="Times New Roman">28238</FONT>行)<FONT face="Times New Roman">…</FONT>发送者有效的用户<FONT face="Times New Roman">ID</FONT>既不是已经存储了的目标进程的用户<FONT face="Times New Roman">ID</FONT>,也不是目标进程的当前用户<FONT face="Times New Roman">ID</FONT>,并且<FONT face="Times New Roman">…</FONT></P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo7; tab-stops: list 57.25pt">l         (<FONT face="Times New Roman">28239</FONT>行和<FONT face="Times New Roman">28240</FONT>行)<FONT face="Times New Roman">…</FONT>发送者的当前用户<FONT face="Times New Roman">ID</FONT>既不是已经存储了的目标进程的用户<FONT face="Times New Roman">ID</FONT>,也不是目标进程的当前用户<FONT face="Times New Roman">ID</FONT>,并且<FONT face="Times New Roman">…</FONT></P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo7; tab-stops: list 57.25pt">l         (<FONT face="Times New Roman">28241</FONT>行)<FONT face="Times New Roman">…</FONT>此处不会允许用户超越普通许可(例如,由于用户是<FONT face="Times New Roman">root</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">if</FONT></B>条件必须明白两点。首先,当将<B normal"><FONT face="Times New Roman">info</FONT></B>映射为无符号长整型数的时候,如果它为<FONT face="Times New Roman">1</FONT>,这就不是一个实际指向<B normal"><FONT face="Times New Roman">struct siginfo</FONT></B>结构的指针。相反的,它是说明信号量来自于内核的特殊值,但是并没有进一步的附加信息可供使用。内核本身在最低的页(内存页在第<FONT face="Times New Roman">8</FONT>章中讨论)中并不分配空间,因此在<FONT face="Times New Roman">4,096</FONT>之下的任何地址(除了<FONT face="Times New Roman">0</FONT>,<B normal"><FONT face="Times New Roman">NULL</FONT></B>之外)都可以作为这种特殊值使用。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>其次,在这几种条件的情况中,位<FONT face="Times New Roman">XOR</FONT>运算操作符(<B normal"><FONT face="Times New Roman">^</FONT></B>)比不等运算操作符(<B normal"><FONT face="Times New Roman">!=</FONT></B>)使用得更为普遍。在这些情况下,两个操作符意义相同,因为如果两个相比较的正数之间有一位不同,在<FONT face="Times New Roman">XOR</FONT>运算的结构中就至少有一位被置位,所以结果非空,逻辑值为真。推测起来,<FONT face="Times New Roman">cc</FONT>的早期版本为<B normal"><FONT face="Times New Roman">^</FONT></B>生成的代码比为<B normal"><FONT face="Times New Roman">!=</FONT></B>生成的代码更为有效,但是在现在的编译器版本中就不是这样了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28248</FONT>:忽略信号量<FONT face="Times New Roman">0</FONT>并拒绝将信号量传送给僵进程(已经退出但是还尚未从系统的数据结构中移走的进程;请参看第<FONT face="Times New Roman">7</FONT>章的“进程状态”一节,它讨论了函数<B normal"><FONT face="Times New Roman">exit</FONT></B>)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28252</FONT>:对于一些信号量,在实际发送之前必须进行一些额外的工作。这些工作是在这里的<B normal"><FONT face="Times New Roman">switch</FONT></B>中处理的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28253</FONT>:如果正在发送<B normal"><FONT face="Times New Roman">SIGKLL</FONT></B>或者<B normal"><FONT face="Times New Roman">SIGCONT</FONT></B>,<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>就唤醒进程(也就是说,如果当前被停止了就允许它再次运行)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28257</FONT>:设置进程的返回代码为<FONT face="Times New Roman">0</FONT>——如果进程已经使用<B normal"><FONT face="Times New Roman">SIGSTOP</FONT></B>停止了,返回代码域就被用来在停止等待的信号量和其祖先间建立通讯。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28258</FONT>:取消任何挂起的<B normal"><FONT face="Times New Roman">SIGSTOP</FONT></B>(被调试器阻塞),<B normal"><FONT face="Times New Roman">SIGSTP</FONT></B>(由键盘输入的<FONT face="Times New Roman">Ctrl+Z</FONT>终止),<B normal"><FONT face="Times New Roman">SIGTTIN</FONT></B>(试图从<FONT face="Times New Roman">TTY</FONT>中读取信息的后台运行进程),<B normal"><FONT face="Times New Roman">SIGTTOU</FONT></B>(试图向<FONT face="Times New Roman">TTY</FONT>中写入信息的后台运行的进程);这些是所有可能中断进程的条件,也是<B normal"><FONT face="Times New Roman">SIGCONT</FONT></B>或者<B normal"><FONT face="Times New Roman">SIGKILL</FONT></B>最可能作为响应出现的情况。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28263</FONT>:在一些信号量被删除之后,调用<B normal"><FONT face="Times New Roman">recalc_sigpending</FONT></B>(<FONT face="Times New Roman">16654</FONT>行,将在后面讨论)来判断是否还有信号量仍然处于挂起状态以等待进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28266</FONT>:在前面的情况中,如果<B normal"><FONT face="Times New Roman">SIGCONT</FONT></B>或者<B normal"><FONT face="Times New Roman">SIGKILL</FONT></B>到达了,这四个信号量就会都被取消。但是看起来有些不太对称,如果这四个信号量有一个到达了,任何挂起等待的<B normal"><FONT face="Times New Roman">SIGCONT</FONT></B>都会被取消。然而<B normal"><FONT face="Times New Roman">SIGKILL</FONT></B>却不会被取消,这遵循<B normal"><FONT face="Times New Roman">SIGKILL</FONT></B>永远不会被锁定或者取消的规律。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28281</FONT>:如果目标进程希望忽略信号量并且允许不接收信号量,那么就跳过了信号量的接收过程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28284</FONT>:非实时信号量并不排队等待,这就意味着如果在进程处理第一个信号量实例之前,同一信号量的第二个实例就到达了,那么第二个实例就会被忽略。这一点就是在这里确保的(回想一下<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构的<B normal"><FONT face="Times New Roman">singnal</FONT></B>成员中保存着一个进程的当前正在挂起等待的信号量的集合)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28304</FONT>:在限制条件的控制下,实时信号量需要排队等待。最重要的限制是可以同时排队等待的实时信号量总数的可配置限制;这一限制值为<B normal"><FONT face="Times New Roman">max_queued_signal</FONT></B>,它是在<FONT face="Times New Roman">28007</FONT>行定义的,而且可以使用<FONT face="Times New Roman">Linux</FONT>的系统控制特性加以修改。如果有空间来容纳更多的信号量,就分配<B normal"><FONT face="Times New Roman">struct signal_queue</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">28310</FONT>:如果一个队列节点已经被分配,现在<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>就必须使有关这个信号量的信息进入队列。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28311</FONT>:把信息加入队列是很直接的:<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>把挂起等待的信号量数量(全局变量)增加<FONT face="Times New Roman">1</FONT>,接着把新的节点增加到目标进程的信号量队列中。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28315</FONT>:根据提供给<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>的<B normal"><FONT face="Times New Roman">info</FONT></B>参数填充队列节点的<B normal"><FONT face="Times New Roman">info</FONT></B>成员。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28316</FONT>:<FONT face="Times New Roman">0</FONT>(<B normal"><FONT face="Times New Roman">NULL</FONT></B>)意味着信号量是从用户发送而来的,而且可能使用了从<FONT face="Times New Roman">28513</FONT>行到<FONT face="Times New Roman">28544</FONT>行定义的向后兼容的信号量发送函数。目标<B normal"><FONT face="Times New Roman">siginfo_t</FONT></B>使用相对比较明确的值来填写。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28323</FONT>:值<FONT face="Times New Roman">1</FONT>是指示信号量来源于内核的一个特殊值——再一次的使用了向后兼容的函数。和前面的情况一样,目标<B normal"><FONT face="Times New Roman">siginfo_t</FONT></B>使用相对比较明确的值来填写。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28331</FONT>:正常情况下,<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>得到一个实际的<B normal"><FONT face="Times New Roman">siginfo_t</FONT></B>,它可以简单地将其拷贝到队列节点中。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28334</FONT>:没有分配队列节点——或者因为系统内存溢出而造成<B normal"><FONT face="Times New Roman">kmem_cache_alloc</FONT></B>在<FONT face="Times New Roman">28306</FONT>行返回<B normal"><FONT face="Times New Roman">NULL</FONT></B>;或者因为已经达到了信号量队列的最大值,<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>根本就没有试图分配节点。不管怎样,<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>所处理的内容是相同的:除非该信号量是通过内核或者老式的信号量函数(例如<B normal"><FONT face="Times New Roman">kill</FONT></B>)发出的,否则<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>就返回<B normal"><FONT face="Times New Roman">EAGAIN</FONT></B>错误,通知调用者现在信号量不能排队等待,但是后来调用者应该可以再次使用相同的参数成功执行调用。否则,<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>就传送该信号量但并不将其排入队列中。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28345</FONT>:最后,<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>从实际上准备好发送信号量。首先,信号量进入该进程的挂起等待的信号量的集合中。注意即使信号量被锁定了这个过程也要执行,这可能有点奇怪。但是这样处理是有原因的:内核必须提供<B normal"><FONT face="Times New Roman">sys_sigpending</FONT></B>(<FONT face="Times New Roman">28981</FONT>行,本章中后面部分将讨论),它允许进程查询在锁定时传送进来什么信号量。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28346</FONT>:如果信号量没有被锁定,那么进程应该被通知有信号量到达了。相应的,其<B normal"><FONT face="Times New Roman">sigpendig</FONT></B>标志被置位。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28370</FONT>:如果进程正在等待信号量的到达并且有信号量也正在等待它,那么这个进程就被唤醒(使用<B normal"><FONT face="Times New Roman">wake_up_process</FONT></B>,<FONT face="Times New Roman">26356</FONT>行)来处理信号量。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">force_sig_info</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28386</FONT>:这个函数被内核用来保证不管进程是否需要,它都确实接收了一个信号量。例如,在进程释放未用指针时,可以使用这个函数来确保该进程接收了<B normal"><FONT face="Times New Roman">SIGSEGV</FONT></B>(请参看<FONT face="Times New Roman">7070</FONT>行——实际上是调用了向后兼容的函数<B normal"><FONT face="Times New Roman">force_sig</FONT></B>,但是<B normal"><FONT face="Times New Roman">force_sig</FONT></B>完全是按照<B normal"><FONT face="Times New Roman">force_sig_info</FONT></B>实现的)。<B normal"><FONT face="Times New Roman">force_sig_info</FONT></B>的参数和<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>的参数相同,两者的意义也相同。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28392</FONT>:如果目标进程是僵进程,即使是内核也不应该给它发送任何信号量;所进行的尝试将被拒绝。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28397</FONT>:如果进程将要忽略这个信号量,<B normal"><FONT face="Times New Roman">force_sig_info</FONT></B>将通过强制它执行缺省操作的方式进行纠正。实际上它并不像外表所表现出来的那样无害:在内核使用该函数的情况下,对这个信号量的缺省操作是杀死进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28399</FONT>:把信号量从<B normal"><FONT face="Times New Roman">t</FONT></B>所锁定的集合中移走。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28402</FONT>:<B normal"><FONT face="Times New Roman">force_sig_info</FONT></B>现在已经建立了一些条件使得<B normal"><FONT face="Times New Roman">t</FONT></B>必须接收信号量,因此该信号量就可以使用<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>进行发送。如果<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>的实现改变了,这仍然可能造成信号量不能发送,因此这两个函数必须保持同步。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">recalc_sigpending</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16654</FONT>:这个函数重新计算进程的<B normal"><FONT face="Times New Roman">sigpending</FONT></B>标志;当进程的<B normal"><FONT face="Times New Roman">signal</FONT></B>或<B normal"><FONT face="Times New Roman">blocked</FONT></B>集合改变时就调用该函数。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16676</FONT>:在最简单的情况中,<B normal"><FONT face="Times New Roman">recalc_sigpending</FONT></B>将信号量和锁定集合求补的结果执行位<FONT face="Times New Roman">AND</FONT>操作。(对锁定集合求补就是允许的集合。)其它的情况仅仅是这种情况的泛化。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16679</FONT>:如果前面操作中的任何一个在<B normal"><FONT face="Times New Roman">ready</FONT></B>中遗留下了任何一位,那么挂起等待的信号量集合中最少有一个信号量现在还被锁定;因此<B normal"><FONT face="Times New Roman">recalc_sigpending</FONT></B>将增加<B normal"><FONT face="Times New Roman">sigpending</FONT></B>标志的值。</P><P 0cm 0cm 0pt">由于<B normal"><FONT face="Times New Roman">recalc_sigpending</FONT></B>所实际需要了解的全部内容只是是否至少有一个信号量在挂起等待——例如,如果不止一个,也并不需要知道有多少信号量在挂起等待——非平凡情况下的代码只要发现<B normal"><FONT face="Times New Roman">ready</FONT></B>的值被置为非<FONT face="Times New Roman">0</FONT>值就应该停止对其进行修改(例如,前面<FONT face="Times New Roman">16662</FONT>行通过中断循环)。但是,任何可能来对此优化所产生的效率增进都必须要同为此而进行的额外测试进行权衡。正是由于这个原因,又加上<B normal"><FONT face="Times New Roman">_NSIG_WORDS</FONT></B>很小(在实际中无论如何都是如此),改进的版本可能要比标准情况快一点。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">ignored_signal</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28183</FONT>:<B normal"><FONT face="Times New Roman">ignored_signal</FONT></B>有助于<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>决定是否给一个进程发送信号量。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28189</FONT>:如果进程正被其祖先跟踪(可能是调试器),或者信号量是在进程锁定的集合中,那么它就不能被忽略。第二种情况可能是我们过去所没有考虑过的;如果信号量被锁定了,<B normal"><FONT face="Times New Roman">send_sig_info</FONT></B>(还有<B normal"><FONT face="Times New Roman">ignored_signal</FONT></B>)难道不应该将其忽略吗?如果情况的确如此,还真不应该忽略。这个函数通过信号量是否应该被忽略表明了进程的信号量的<B normal"><FONT face="Times New Roman">signal</FONT></B>集合的相应位是否应该被置位。如同前面我们已经看到的那样,对<B normal"><FONT face="Times New Roman">sigpending</FONT></B>系统调用的支持要求如果在锁定过程中有信号量到达,内核就应该设置相应的位。因此,被锁定信号量不能简单地忽略。</P>
作者: ilikenba    时间: 2005-3-4 22:06
< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28194</FONT>:如果进程是一个僵进程,信号量就应该被忽略。这种测试是不必要的,因为这种情况甚至在<FONT face="Times New Roman">28248</FONT>行的<B normal"><FONT face="Times New Roman">ignored_signal</FONT></B>调用之前就会被发现。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28199</FONT>:在大多数情况下,<B normal"><FONT face="Times New Roman">SIG_DFL</FONT></B>(缺省的)操作是处理信号量而不是将其忽略。你所能看到的例外是<B normal"><FONT face="Times New Roman">SIGCONT</FONT></B>,<B normal"><FONT face="Times New Roman">SIGWINCH</FONT></B>,<B normal"><FONT face="Times New Roman">SIGCHLD</FONT></B>,和<B normal"><FONT face="Times New Roman">SIGURG</FONT></B>。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28207</FONT>:进程允许忽略大部分信号量,但是不能忽略<B normal"><FONT face="Times New Roman">SIGCHLD</FONT></B>。对于<B normal"><FONT face="Times New Roman">SIGCHLD</FONT></B>,<FONT face="Times New Roman">OSIX</FONT>赋予<B normal"><FONT face="Times New Roman">SIG_IGN</FONT></B>一种特殊的意义,这一点在<FONT face="Times New Roman">28831</FONT>行将会说明。这里所提到的“<FONT face="Times New Roman">automatic child reaping</FONT>”(自动子进程空间回收)将在<FONT face="Times New Roman">3426</FONT>行执行。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28211</FONT>:在缺省的情况下,可以假定<B normal"><FONT face="Times New Roman">ignored_signal</FONT></B>有一个实际的函数指针,而不是<B normal"><FONT face="Times New Roman">SIG_DFL</FONT></B>或者<B normal"><FONT face="Times New Roman">SIG_IGN</FONT></B>两个伪值的一个。这样,信号量就和用户定义的处理句柄联系起来,这意味着进程希望处理这个信号量。它通过返回<FONT face="Times New Roman">0</FONT>来指明信号量不应该被忽略。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">do_signal</H4>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3364</FONT>:<FONT face="Times New Roman"> <B normal">do_signal</B></FONT>在信号量到达进程时使用。这个函数在内核中被调用的地方不止一次——如我们在第<FONT face="Times New Roman">5</FONT>章中看到的从<FONT face="Times New Roman">203</FONT>行和<FONT face="Times New Roman">211</FONT>行,还有从<FONT face="Times New Roman">2797</FONT>行和<FONT face="Times New Roman">2827</FONT>行。通常所有这些情况都是当前进程希望处理挂起等待的信号量(如果有的话)。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3375</FONT>:<FONT face="Times New Roman"> </FONT>如果非空,<B normal"><FONT face="Times New Roman">oldset</FONT></B>用来返回当前进程锁定的信号量集合。由于<B normal"><FONT face="Times New Roman">do_signal</FONT></B>不会修改锁定的集合,它可以简单的返回一个指向现有锁定集合的指针。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3378</FONT>:<FONT face="Times New Roman"> </FONT>进入几乎扩展到该函数末尾(<FONT face="Times New Roman">3478</FONT>行)的循环。退出该循环的方法只有两种:把所有可能的信号量都处理了,或者处理唯一一个信号量。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3382</FONT>:<FONT face="Times New Roman"> </FONT>使用<B normal"><FONT face="Times New Roman">dequeue_signal</FONT></B>使信号量出队列(<FONT face="Times New Roman">28060</FONT>,后面将会介绍)。<B normal"><FONT face="Times New Roman">dequeue_signal</FONT></B>或者返回<FONT face="Times New Roman">0</FONT>,或者返回需要处理的信号量的编号,并且它还会填充<B normal"><FONT face="Times New Roman">info</FONT></B>中的附加信息。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3385</FONT>:<FONT face="Times New Roman"> </FONT>如果没有信号量处于等待状态,将在这里中断循环。正常情况下,它在循环第一次执行过程中是不会发生的。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3388</FONT>:<FONT face="Times New Roman"> </FONT>如果当前进程正在被其祖先跟踪(可能是调试器),而且信号量也并不是不可锁定的<B normal"><FONT face="Times New Roman">SIGKILL</FONT></B>,那么在信号量到达之前,进程的祖先就必须已经得到通知了。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3391</FONT>:<FONT face="Times New Roman"> </FONT>将传递给子孙进程的信号量编号传送到祖先进程中对应子孙进程的<B normal"><FONT face="Times New Roman">exit_code</FONT></B>域;祖先使用<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>收集这些信息(<FONT face="Times New Roman">23327</FONT>行,在第<FONT face="Times New Roman">7</FONT>章中介绍)。<B normal"><FONT face="Times New Roman">do_signal</FONT></B>停止子孙进程的运行,然后使用<B normal"><FONT face="Times New Roman">notify_parent</FONT></B>(<FONT face="Times New Roman">3393</FONT>行)给祖先进程发送<B normal"><FONT face="Times New Roman">SIGCHLD</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>章中介绍),给其它进程——尤其是其祖先进程——运行的机会。<B normal"><FONT face="Times New Roman">schedule</FONT></B>会把<FONT face="Times New Roman">CPU</FONT>分配给其它进程,因此直到内核跳转回这个进程才会返回。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3397</FONT>:<FONT face="Times New Roman"> </FONT>如果调试器取消了信号量,<B normal"><FONT face="Times New Roman">do_signal</FONT></B>在这里就不应该处理它;循环继续进行。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3402</FONT>:<FONT face="Times New Roman"> <B normal">SIGSTOP</B></FONT>可能只是由于进程正在被跟踪而产生。这里没有必要处理它;循环继续进行。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3406</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">do_signal</FONT></B>将根据新的信息填充<B normal"><FONT face="Times New Roman">info</FONT></B>。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3415</FONT>:<FONT face="Times New Roman"> </FONT>正如注释中所说明的一样,如果新的信号量被锁定了,就需要重新排队,循环继续进行。否则,控制流程将直接执行下面的代码。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3421</FONT>:<FONT face="Times New Roman"> </FONT>在这里,或者进程未被跟踪,或者进程正被跟踪但是得到了一个<B normal"><FONT face="Times New Roman">SIGKILL</FONT></B>信号量,或者控制流程直接从前面的代码块中执行下来。在任何一种情况中,<B normal"><FONT face="Times New Roman">do_signal</FONT></B>都有一个应该现在处理的信号量。它从获取<B normal"><FONT face="Times New Roman">struct k_sigaction</FONT></B>结构开始,这个结构指明了怎样处理这个信号量编号。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3423</FONT>:<FONT face="Times New Roman"> </FONT>如果进程试图忽略信号量,那么除非这个信号量是<B normal"><FONT face="Times New Roman">SIGCHLD</FONT></B>,否则<B normal"><FONT face="Times New Roman">do_signal</FONT></B>就继续执行循环从而忽略该信号量。为什么这个测试不能同时保证该进程不会忽略掉<B normal"><FONT face="Times New Roman">SIGKILL</FONT></B>这个注定不可忽略也不可锁定的信号量呢?答案在于和<B normal"><FONT face="Times New Roman">SIGKILL</FONT></B>相关的操作永远不会是<B normal"><FONT face="Times New Roman">SIG_IGN</FONT></B>的,实际上也不会是除<B normal"><FONT face="Times New Roman">SIG_DFL</FONT></B>之外的任何操作——<FONT face="Times New Roman">28807</FONT>行就保证了这一点(在<B normal"><FONT face="Times New Roman">do_sigaction</FONT></B>函数中)。这样,如果操作是<B normal"><FONT face="Times New Roman">SIG_IGN</FONT></B>,那么信号量编号就不可能是<B normal"><FONT face="Times New Roman">SIGKILL</FONT></B>。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3426</FONT>:<FONT face="Times New Roman"> </FONT>如同在从<FONT face="Times New Roman">28820</FONT>行开始的标题注释中说明的一样,<FONT face="Times New Roman">OSIX</FONT>标准说明了忽略<B normal"><FONT face="Times New Roman">SIGCHLD</FONT></B>的操作实际上意味着自动回收其子孙进程。子孙进程是通过使用<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>来回收的(<FONT face="Times New Roman">23327</FONT>行,在第<FONT face="Times New Roman">7</FONT>章中介绍),此后循环继续运行。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3435</FONT>:<FONT face="Times New Roman"> </FONT>进程为这个信号量采用缺省操作。专用的初始化进程接收到全部信号量所对应的缺省操作是把信号量整个忽略掉。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3439</FONT>:<FONT face="Times New Roman"> </FONT>对信号量<B normal"><FONT face="Times New Roman">SIGCONT</FONT></B>、<B normal"><FONT face="Times New Roman">SIGCHLD</FONT></B>和<B normal"><FONT face="Times New Roman">SIGWINCH</FONT></B>所采取的缺省操作是不加处理,只是简单地继续执行循环。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3442</FONT>:<FONT face="Times New Roman"> </FONT>对于信号量<B normal"><FONT face="Times New Roman">SIGSTP</FONT></B>,<B normal"><FONT face="Times New Roman">SIGTTIN</FONT></B>和<B normal"><FONT face="Times New Roman">SIGTTOU</FONT></B>,缺省的操作各自不同。如果该进程所归属的进程组是孤立的——简单的说就是没有连接到<FONT face="Times New Roman">TTY</FONT>上——那么<FONT face="Times New Roman">OSIX</FONT>规定对于这些基于终端的信号量的缺省操作是将其忽略。如果进程的进程组不是孤立的,缺省的操作是停止进程的运行——这和<B normal"><FONT face="Times New Roman">SIGSTOP</FONT></B>的情况是相同的,在这种情况下控制流程直接向下运行。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3447</FONT>:<FONT face="Times New Roman"> </FONT>在对<B normal"><FONT face="Times New Roman">SIGSTOP</FONT></B>的响应中(或者是从前面情况中直接执行下来),<B normal"><FONT face="Times New Roman">do_signal</FONT></B>终止了进程。另外,除非祖先进程已经规定对其子孙进程的终止运行不加理会,否则祖先进程将会在其子孙进程退出时被通知。和<FONT face="Times New Roman">3394</FONT>行一样,调用<B normal"><FONT face="Times New Roman">schedule</FONT></B>交出<FONT face="Times New Roman">CPU</FONT>给其它某一进程。当内核把<FONT face="Times New Roman">CPU</FONT>再次分配给当前进程的时候,该循环继续运行以处理队列中的另外一个信号量。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>这不是我们希望的——我认为当<B normal"><FONT face="Times New Roman">schedule</FONT></B>返回时,循环应该退出,因为信号量已经处理完了。其原理在于如果进程被终止了,唤醒进程的最可能的原因是进程又得到了信号量,可能是<B normal"><FONT face="Times New Roman">SIGCONT</FONT></B>,因此该进程现在就可以检测并处理信号量了。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3456</FONT>:<FONT face="Times New Roman"> </FONT>对于其它信号量的缺省操作是退出进程。它们中的一些将使进程首先清空内核(详细的介绍请参看第<FONT face="Times New Roman">8</FONT>章),这些信号量就是<B normal"><FONT face="Times New Roman">SIGQUIT</FONT></B>,<B normal"><FONT face="Times New Roman">SIGILL</FONT></B>,<B normal"><FONT face="Times New Roman">SIGTRRAP</FONT></B>,<B normal"><FONT face="Times New Roman">SIGABRT</FONT></B>,<B normal"><FONT face="Times New Roman">SIGFPE</FONT></B>和<B normal"><FONT face="Times New Roman">SIGSEGV</FONT></B>。如果此二进制格式(详细的介绍请参看第<FONT face="Times New Roman">7</FONT>章)知道如何清空内核并且成功地清空了内核,那么在进程的返回代码中就会有一位被设置来指明进程在退出之前就已经清空了内核。接着流程按照<B normal"><FONT face="Times New Roman">default</FONT></B>的情况继续执行,终止进程的运行。注意<B normal"><FONT face="Times New Roman">do_exit</FONT></B>(<FONT face="Times New Roman">23267</FONT>行,在第<FONT face="Times New Roman">7</FONT>章中也会有介绍)是从来不会返回的——因而在<FONT face="Times New Roman">3471</FONT>行中会有“<FONT face="Times New Roman">NOTREACHED</FONT>”注释。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3476</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">SIG_IGN</FONT></B>的操作有关,也不和<B normal"><FONT face="Times New Roman">SIG_DFL</FONT></B>的操作有关。唯一的另外一种可能性是这是用户定义的信号量处理函数。<B normal"><FONT face="Times New Roman">do_signal</FONT></B>调用<B normal"><FONT face="Times New Roman">handle_signal</FONT></B>(<FONT face="Times New Roman">3314</FONT>行,本章随后将会更为详细地讨论)来触发这个信号量处理函数,接着返回<FONT face="Times New Roman">1</FONT>向调用者声明这个信号量已经处理过了。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3481</FONT>:<FONT face="Times New Roman"> </FONT>此处,<B normal"><FONT face="Times New Roman">do_signal</FONT></B>不能为当前进程从队列中取出信号量。(只有从<FONT face="Times New Roman">3386</FONT>行的<B normal"><FONT face="Times New Roman">break</FONT></B>退出时才能执行到本行。)如果在系统调用的处理过程中间被中断了,<B normal"><FONT face="Times New Roman">do_signal</FONT></B>就要调整寄存器,从而系统调用将可以重新执行。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3490</FONT>:返回<FONT face="Times New Roman">0</FONT>以通知调用者<B normal"><FONT face="Times New Roman">do_signal</FONT></B>没有处理任何信号量。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">dequeue_signal</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28060</FONT>:<B normal"><FONT face="Times New Roman">dequeue_signal</FONT></B>将信号量从进程信号量队列中移出,同时忽略那些由掩码说明的信号量。它返回信号量的编号并使用指针参数<B normal"><FONT face="Times New Roman">info</FONT></B>返回相关的<B normal"><FONT face="Times New Roman">siginfo_t</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28071</FONT>:为了避免重复参照而建立一些别名:<B normal"><FONT face="Times New Roman">s</FONT></B>是进程的挂起等待的信号量的集合(记住其中可能包括了一些锁定的信号量),<B normal"><FONT face="Times New Roman">m</FONT></B>是掩码的集合。特别要注意的是<FONT face="Times New Roman">*s</FONT>表达式,在该函数中这个表达式出现了不止一次,但是它只是<B normal"><FONT face="Times New Roman">current-&gt;signal.sig[0]</FONT></B>的一种简单写法。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28073</FONT>:在这个<B normal"><FONT face="Times New Roman">switch</FONT></B>条件分支中,<B normal"><FONT face="Times New Roman">sig</FONT></B>被设置为第一个挂起等待的信号量。从最简单的情况入手最容易理解;其它的情况只是这种情况的泛化。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28091</FONT>:最简单的情况是:它把挂起等待的信号量和掩码求补后的结果进行位<FONT face="Times New Roman">AND</FONT>运算,结果被存储在临时变量<B normal"><FONT face="Times New Roman">x</FONT></B>中;<B normal"><FONT face="Times New Roman">x</FONT></B>现在就是掩码不能忽略的挂起等待的信号量的集合。如果<B normal"><FONT face="Times New Roman">x</FONT></B>不为<FONT face="Times New Roman">0</FONT>,那么就存在挂起等待的信号量(<B normal"><FONT face="Times New Roman">x</FONT></B>至少有一位被置位);<B normal"><FONT face="Times New Roman">dequeue_signal</FONT></B>使用<B normal"><FONT face="Times New Roman">ffz</FONT></B>(本书中没有涉及)取得相应的信号量编号,并将其转化为从<FONT face="Times New Roman">1</FONT>开始计数的信号量编号,将结果存储在<B normal"><FONT face="Times New Roman">sig</FONT></B>中。正如前面所说明的一样,其它情况只是这种情况的泛化;最重要的结果是<B normal"><FONT face="Times New Roman">sig</FONT></B>被置位,如果可能的话在每种情况下都是如此——此后其它变量(<B normal"><FONT face="Times New Roman">i</FONT></B>,<B normal"><FONT face="Times New Roman">s</FONT></B>,<B normal"><FONT face="Times New Roman">m</FONT></B>或者<B normal"><FONT face="Times New Roman">x</FONT></B>)的状态就不难理解了。如果在<B normal"><FONT face="Times New Roman">switch</FONT></B>之后的<B normal"><FONT face="Times New Roman">sig</FONT></B>是<FONT face="Times New Roman">0</FONT>,掩码中就没有传递挂起等待的信号量。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28097</FONT>:如果一个信号量正在挂起等待,那么<B normal"><FONT face="Times New Roman">dequeue_signal</FONT></B>应该试图将其从队列中释放出来。<B normal"><FONT face="Times New Roman">reset</FONT></B>跟踪<B normal"><FONT face="Times New Roman">dequeue_signal</FONT></B>以判定是否应该把信号量从进程的挂起等待的信号量队列中删除。将<B normal"><FONT face="Times New Roman">reset</FONT></B>初始化为<FONT face="Times New Roman">1</FONT>仅仅是由于在函数处理过程中它可能会改变的假定。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28107</FONT>:对于非实时信号量,内核不会保持原始的<B normal"><FONT face="Times New Roman">siginfo_t</FONT></B>(如果曾经有过的话),因此<B normal"><FONT face="Times New Roman">dequeue_signal</FONT></B>应该尽可能的重新组织有关的信息。不幸的是,当前实现方法中并没有多少信息——只有信号量编号自身而已。<B normal"><FONT face="Times New Roman">info</FONT></B>的其它成员都简单地被设置为<FONT face="Times New Roman">0</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28118</FONT>:在另一种情况,也就是实时信号量情况下,<B normal"><FONT face="Times New Roman">siginfo_t</FONT></B>只是一种点缀。<B normal"><FONT face="Times New Roman">dequeue_signal</FONT></B>会在进程的<FONT face="Times New Roman">sigqueue</FONT>中进行扫描以确定其值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28122</FONT>:如果找到了<B normal"><FONT face="Times New Roman">siginfo_t</FONT></B>,<B normal"><FONT face="Times New Roman">dequeue_signal</FONT></B>现在就使其出队列,将<B normal"><FONT face="Times New Roman">siginfo_t</FONT></B>的内容拷贝到<B normal"><FONT face="Times New Roman">info</FONT></B>中,并释放为这个队列节点分配的内存。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28129</FONT>:如果队列中没有这个信号量的更多实例,那么信号量就不会在挂起等待了。但是为了弄清楚队列中是否还有信号量的实例,<B normal"><FONT face="Times New Roman">dequeue_signal</FONT></B>需要遍历整个队列。因此,该函数需要扫描这个队列的其余元素来查询是否存在相同信号量的其它实例。如果发现了实例,<B normal"><FONT face="Times New Roman">dequeue_signal</FONT></B>就清空<B normal"><FONT face="Times New Roman">reset</FONT></B>标志——只有在这种独特的情况下才会进行的操作。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28142</FONT>:正在出队的信号量是实时信号量,但是在进程的挂起等待的实时信号量队列中却没有发现它,其原因在代码中已经进行了阐述。现在,<B normal"><FONT face="Times New Roman">dequeue_signal</FONT></B>和它有非实时信号量的情况相同了——它知道信号量是可以访问的,但是没有方法可以访问其原始值——并且其响应过程处理的工作也完全相同,仅仅使用信号量编号来填充<B normal"><FONT face="Times New Roman">info</FONT></B>,而没有其它属性值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28150</FONT>:除非<B normal"><FONT face="Times New Roman">reset</FONT></B>标志被清空了——也就是说除非这是一个实时信号量并且同一个信号量的其它实例仍然在挂起等待队列中——该信号量已经被处理过;它应该从进程的挂起等待集合中删除。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28152</FONT>:信号量脱离队列,因此<B normal"><FONT face="Times New Roman">dequeue_signal</FONT></B>应该重新计算进程的<B normal"><FONT face="Times New Roman">sigpending</FONT></B>标志。我认为这里有一个可以进行少量优化的机会:只用当<B normal"><FONT face="Times New Roman">reset</FONT></B>为真的时候<B normal"><FONT face="Times New Roman">dequeue_signal</FONT></B>才需要这样处理。<B normal"><FONT face="Times New Roman">recalc_sigpending</FONT></B>从进程的锁定集合和挂起等待集合中计算结果;锁定的集合没有改变,因此只有当挂起等待的集合发生改变时,<B normal"><FONT face="Times New Roman">dequeue_signal</FONT></B>才需要调用<B normal"><FONT face="Times New Roman">recalc_sigpending</FONT></B>。如果<B normal"><FONT face="Times New Roman">reset</FONT></B>为假,挂起等待的集合就不会改变,因此对于<B normal"><FONT face="Times New Roman">recalc_sigpending</FONT></B>的调用就是不必要的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28163</FONT>:<B normal"><FONT face="Times New Roman">switch</FONT></B>没有发现信号量,因此没有信号量正在挂起等待。作为内部正确性的检测,<B normal"><FONT face="Times New Roman">dequeue_signal</FONT></B>确保内核不会认为有信号量正在为某任务挂起等待。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28174</FONT>:返回出队的信号量编号,或者如果没有信号量出队,就返回<FONT face="Times New Roman">0</FONT>。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">notify_parent</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28548</FONT>:<B normal"><FONT face="Times New Roman">notify_parent</FONT></B>寻找进程的祖先进程并通知它其子孙进程的状态发生了改变——通常情况是其子孙进程或者被终止了,或者被杀死了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28553</FONT>:使用有关信号量发生的上下文的信息填充局部变量<B normal"><FONT face="Times New Roman">info</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28564</FONT>:如果子孙进程已经退出,<B normal"><FONT face="Times New Roman">why</FONT></B>被赋以适当的值以指明其原因是因为它清空了内核,或者被某信号量将其杀死,或者因为执行了非法操作。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28572</FONT>:类似地,如果使用信号量终止了进程,对<B normal"><FONT face="Times New Roman">why</FONT></B>赋值以说明发生的情况。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28578</FONT>:前面的情况几乎覆盖了所有的可能性。如果没有,函数打印出警告信息并继续运行;在这种情况下,系统会在<FONT face="Times New Roman">28562</FONT>行将<B normal"><FONT face="Times New Roman">why</FONT></B>的值赋为<B normal"><FONT face="Times New Roman">SI_KERNEL</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28586</FONT>:给进程的祖先进程发送信号量。下面一行唤醒任何等待这个子孙进程的进程并为其提供<B normal"><FONT face="Times New Roman">CPU</FONT></B>。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">handle_signal</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3314</FONT>:<FONT face="Times New Roman"> <B normal">handle_signal</B></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">3338</FONT>:<FONT face="Times New Roman"> </FONT>建立一个用户处理程序可以在其中运行的堆栈帧。如果进程已经请求了内核所拥有的有关信号量的原始值和其上下文的附加信息,那么堆栈帧就使用<B normal"><FONT face="Times New Roman">setup_rt_frame</FONT></B>(<FONT face="Times New Roman">3231</FONT>行)构建起来;否则就使用<B normal"><FONT face="Times New Roman">setup_frame</FONT></B>(<FONT face="Times New Roman">3161</FONT>行)构建。这两种方法都可以实现构建工作,这样控制流程会返回信号量处理程序。当它返回时,实际返回的是信号量到达的时候正在执行的代码。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3343</FONT>:<FONT face="Times New Roman"> </FONT>如果<B normal"><FONT face="Times New Roman">SA_ONESHOT</FONT></B>标志被设置,则信号量处理程序应该只执行一次。(注意<B normal"><FONT face="Times New Roman">sys_signal</FONT></B>是<B normal"><FONT face="Times New Roman">signal</FONT></B>系统调用的实现,它使用<B normal"><FONT face="Times New Roman">SA_ONESHOT</FONT></B>类型的信号量处理程序——请参看<FONT face="Times New Roman">29063</FONT>行。)在这种情况中,缺省的操作是立即将其恢复。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3346</FONT>:<FONT face="Times New Roman"> <B normal">SA_NODEFER</B></FONT>意味着在执行这个信号量的处理程序时,不应该有其它信号量被锁定。如果位没有设置,其它的位现在就会被加入进程的锁定的集合中。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">其它有关信号量的函数</H2><P 0cm 0cm 0pt">其它一些有关信号量处理的函数。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sys_sigpending</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28981</FONT>:这个简短的系统调用允许进程询问在信号量锁定期间是否有非实时信号量到达。通过所提供的指针,该函数返回一个位集以指明它们是哪些信号量。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28987</FONT>:这个函数的核心是进程的<B normal"><FONT face="Times New Roman">blocked</FONT></B>集合和<B normal"><FONT face="Times New Roman">signal</FONT></B>集合间的简单位<FONT face="Times New Roman">AND</FONT>操作。它只对最低<FONT face="Times New Roman">32</FONT>位感兴趣,这些都是非实时信号量。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28992</FONT>:使用所提供的指针把挂起等待的集合拷贝回用户空间。如果失败就返回<B normal"><FONT face="Times New Roman">-EFAULT</FONT></B>,如果成功就返回<FONT face="Times New Roman">0</FONT>。注意是否有信号量正在挂起等待——也就是说,返回值是否为空——并不是成功的判据之一。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">do_sigaction</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28801</FONT>:<B normal"><FONT face="Times New Roman">do_sigaction</FONT></B>实现了系统调用<B normal"><FONT face="Times New Roman">sigaction</FONT></B>有意义的部分。(其余部分在<FONT face="Times New Roman">2833</FONT>行的<B normal"><FONT face="Times New Roman">sys_sigaction</FONT></B>中。)<B normal"><FONT face="Times New Roman">sigaction</FONT></B>是<FONT face="Times New Roman">POSIX</FONT>中等价于<FONT face="Times New Roman">ISO C</FONT>的函数<B normal"><FONT face="Times New Roman">signal</FONT></B>——它把信号量和操作关联起来,这样进程接收到信号量时就能够执行相应的操作。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28806</FONT>:健全性检测:确保<B normal"><FONT face="Times New Roman">sig</FONT></B>在范围之内并且进程没有试图把<B normal"><FONT face="Times New Roman">SIGKILL</FONT></B>或者<B normal"><FONT face="Times New Roman">SIGCONT</FONT></B>和某种操作相关联。进程被简单地剥夺了覆盖这两个信号量的缺省操作的权力。然而,和<B normal"><FONT face="Times New Roman">signal</FONT></B>实现的处理程序不同,使用<B normal"><FONT face="Times New Roman">sigaction</FONT></B>实现的处理程序不是<B normal"><FONT face="Times New Roman">SA_ONESHOT</FONT></B>类型的,因此在处理程序被调用的时候就不用每次都将其重新装载。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28811</FONT>:获取和这个信号量相关的指向<B normal"><FONT face="Times New Roman">k_sigaction</FONT></B>结构的指针。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28813</FONT>:<B normal"><FONT face="Times New Roman">sigaction</FONT></B>可以通过一个过去所提供的指针返回旧有的操作。这在以堆栈方式存在的处理程序中是很有用的,在这里处理程序被临时覆盖,以后再恢复出来。如果<B normal"><FONT face="Times New Roman">oact</FONT></B>指针非空,旧有的操作就会被拷贝到其中。(但是这并不会把信息拷贝会用户空间;调用者必须执行这样的处理。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28815</FONT>:如果<B normal"><FONT face="Times New Roman">do_sigaction</FONT></B>被赋予一个需要同信号量相关联的操作,那么二者现在就相互关联起来。<B normal"><FONT face="Times New Roman">SIGKILL</FONT></B>和<B normal"><FONT face="Times New Roman">SIGSTOP</FONT></B>也必须被从操作的掩码中删除,为了确保这些信号量不会被锁定或者覆盖。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28836</FONT>:正如在<FONT face="Times New Roman">28820</FONT>行开始的标题中注释的一样,为了遵守<FONT face="Times New Roman">POSIX</FONT>标准,下面的几行代码必须要经过一定变形,并且在必要情况下还会舍弃某些信号量。对于这些细节情况,我们即使跳过也不会有什么损失。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sys_rt_sigtimedwait</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28694</FONT>:<B normal"><FONT face="Times New Roman">sys_rt_sigtimedwait</FONT></B>等待信号量的到达,它可能在经过一段特定的时间间隔以后超时退出。并不是所有的信号量都会接收;指针<B normal"><FONT face="Times New Roman">uthese</FONT></B>所指明的<B normal"><FONT face="Times New Roman">sigset_t</FONT></B>说明了调用者所感兴趣的信号量。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28714</FONT>:<B normal"><FONT face="Times New Roman">uthese</FONT></B>(它已经被拷贝到局部变量<B normal"><FONT face="Times New Roman">these</FONT></B>中了)是允许的信号量的集合,于是内核元语只知道如何锁定信号量。但这样也没有关系:对允许的信号量集合进行求补运算就得到了应该锁定的信号量,所得到的结果就可以直接使用了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28717</FONT>:如果调用者提供了超时时间,该超时时间就将被拷贝到用户空间中,而且其值也必须经过健全检测。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28726</FONT>:检查是否已经有信号量正在等待了——如果有,就没有必要为其等待了。否则,调用者必须等待。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28731</FONT>:保存原来的锁定信号量集合,然后阻塞由<B normal"><FONT face="Times New Roman">these</FONT></B>定义的所有信号量。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28737</FONT>:如果用户没有提供超时时间,那么超时时间会是<B normal"><FONT face="Times New Roman">MAX_SCHEDULE_TIMEOUT</FONT></B>(宏定义为<B normal"><FONT face="Times New Roman">LONG_MAX</FONT></B>,或者是<FONT face="Times New Roman">2<SUP>31</SUP>-1</FONT>,<FONT face="Times New Roman">16228</FONT>行)。但是并不永远都是如此——超时时间是以瞬间(<FONT face="Times New Roman">jiffy</FONT>)计数的,它的系统时钟以每秒<FONT face="Times New Roman">100</FONT>次的速度跳动着,因此大约有<FONT face="Times New Roman">248</FONT>天,超时时间就耗尽了。(在<FONT face="Times New Roman">64</FONT>位机器中,这大约需要三十亿年。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28739</FONT>:如果用户确实提供了超时时间,就将其转化为以瞬间计算的值。“<FONT face="Times New Roman">+</FONT>”后面的表达式是对下一个瞬间进行向上舍入的明智方法——其思想是<B normal"><FONT face="Times New Roman">timespec_to_jiffies</FONT></B>可能已经向下舍入了,但是内核必须是上舍入的,因为它必须等够用户请求的瞬间个数。它虽然可以检测<B normal"><FONT face="Times New Roman">timespec_to_jiffies</FONT></B>(<FONT face="Times New Roman">18357</FONT>行)是否是下舍入的,但是下面这种方法更为简单:如果用户提供的超时时间不是<FONT face="Times New Roman">0</FONT>就为其增加一个瞬间,并且认为是对它进行了调整。毕竟<FONT face="Times New Roman">Linux</FONT>不是一个真正的实时操作系统——当你指定了超时时间时,<FONT face="Times New Roman">Linux</FONT>只能保证至少等待如此长的时间。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28742</FONT>:设置当前用户的状态为<B normal"><FONT face="Times New Roman">TASK_INTERRUPTIBLE</FONT></B>(请参看第<FONT face="Times New Roman">7</FONT>章)。<B normal"><FONT face="Times New Roman">schedule_timeout</FONT></B>(<FONT face="Times New Roman">26577</FONT>行)用来让出<FONT face="Times New Roman">CPU</FONT>;在指定的时间用完以后或者其它事件到达并唤醒进程(比如接收了一个信号量)时,该进程才可以继续运行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28746</FONT>:进程希望被信号量唤醒。<B normal"><FONT face="Times New Roman">sys_rt_sigtimedwait</FONT></B>再次尝试从进程的等待信号量队列中取出信号量并恢复原来锁定的集合。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28752</FONT>:此处,该函数仍然不知道信号量是否已经到达了——它可能无需等待就可以得到一个信号量,或者在等待期间可能有另一个信号量到达,也或者该函数一直在等待但是没有信号量到达。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28753</FONT>:如果信号量到达,该函数就给用户进程传递信息并且返回信号量编号。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28759</FONT>:否则,虽然进行了等待,但是没有信号量到达。在这种情况下,该函数或者返回<B normal"><FONT face="Times New Roman">-EAGAIN</FONT></B>(说明用户进程可以再次使用相同的参数尝试),或者返回<B normal"><FONT face="Times New Roman">-EINTR</FONT></B>(说明其等待过程被由于某些原因而不能传递的信号量中断了)。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">内核如何区分实时信号量和非实时信号量</H2><P 0cm 0cm 0pt">简单的说,答案并不复杂。我几乎掩盖了其中的绝大部分区别,这是有一定原因的:退出语句不多。现在,为了使这一点更加清楚,让我们来看一下系统调用<B normal"><FONT face="Times New Roman">sigprocmask</FONT></B>的两个版本,它允许进程处理自己的锁定信号量的集合——增加,删除,或者简单地对信号量集合进行设置。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sys_sigprocmask</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28931</FONT>:<B normal"><FONT face="Times New Roman">sys_sigprocmask</FONT></B>是这个函数的原始版本,这一版本并不知道或者是不关心实时信号量。参数<B normal"><FONT face="Times New Roman">how</FONT></B>指明了要执行的操作;如果<B normal"><FONT face="Times New Roman">set</FONT></B>不为<B normal"><FONT face="Times New Roman">NULL</FONT></B>,就是这个操作的操作数;如果<B normal"><FONT face="Times New Roman">oset</FONT></B>是非空的,那么<B normal"><FONT face="Times New Roman">oset</FONT></B>返回的就是原始的锁定集合。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28937</FONT>:如果<B normal"><FONT face="Times New Roman">set</FONT></B>为空,那么<B normal"><FONT face="Times New Roman">how</FONT></B>的值就没有什么用处了:该操作就没有操作数了,因此该函数不会处理有关的内容。否则,就继续执行该操作。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28939</FONT>:在新的锁定集合中的拷贝,其中删除了不可锁定的<B normal"><FONT face="Times New Roman">SIGKILL</FONT></B>和<B normal"><FONT face="Times New Roman">SIGSTOP</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28944</FONT>:为了处理以后将当前锁定集合拷贝回用户空间的需要,在<B normal"><FONT face="Times New Roman">old_set</FONT></B>中存储当前锁定集合的一个备份。由于当前锁定集合在以后的代码中可能会被修改,因此在它改变之前必须对其值进行存储。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28948</FONT>:当然是忽略无效的操作。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28951</FONT>:<B normal"><FONT face="Times New Roman">SIG_BLOCK</FONT></B>操作符指明<B normal"><FONT face="Times New Roman">new_set</FONT></B>应该解释为要锁定的附加信号量的集合。这些信号量将被加入该锁定集合中。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28954</FONT>:<B normal"><FONT face="Times New Roman">SIG_UNBLOCK</FONT></B>操作符指明<B normal"><FONT face="Times New Roman">new_set</FONT></B>应该解释为要从锁定的信号量的集合移出的信号量集合。这些信号量现在被移出锁定集合。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28957</FONT>:<B normal"><FONT face="Times New Roman">SIG_SETMASK</FONT></B>操作符指明<B normal"><FONT face="Times New Roman">new_set</FONT></B>应该解释为新的锁定集合,简单覆盖该锁定集合原有的值。因此,<B normal"><FONT face="Times New Roman">sys_sigprocmask</FONT></B>正是实现这一点的。注意它只设置了<B normal"><FONT face="Times New Roman">bloched.set</FONT></B>数组的最低的元素——这个元素包含低<FONT face="Times New Roman">32</FONT>位非实时信号量,这是该函数所关心的内容。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28966</FONT>:如果调用者已经请求查询锁定的集合的原来的值,执行流程就向前跳到<B normal"><FONT face="Times New Roman">set_old</FONT></B>标号(<FONT face="Times New Roman">28970</FONT>行)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28968</FONT>:如果<B normal"><FONT face="Times New Roman">set</FONT></B>为空,意味着调用者没有请求对锁定集合进行修改,但是调用者可能仍然希望了解锁定集合的当前值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28972</FONT>:<B normal"><FONT face="Times New Roman">oset</FONT></B>非空(<B normal"><FONT face="Times New Roman">set</FONT></B>也可能为非空)。不管哪一种情况,<B normal"><FONT face="Times New Roman">old_set</FONT></B>都包含一个原来锁定的集合的备份,在返回之前<B normal"><FONT face="Times New Roman">sys_sigprocmask</FONT></B>会试图将其拷贝回用户空间。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sys_rt_sigprocmask</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28612</FONT>:<B normal"><FONT face="Times New Roman">sys_rt_sigprocmask</FONT></B>和<B normal"><FONT face="Times New Roman">sys_sigprocmask</FONT></B>非常类似,但是它也能够处理新的实时信号量。由于这两者之间的相似性,在这里我仅仅介绍一下它们之间有趣的区别。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">28638</FONT>:与如下代码不相类似的是</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">    /* how sys_sigprocmask does SIG_BLOCK.*/</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">      new_set =  *set ;<B normal">      /</B>* line 28938 */</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">      blocked |= new_set ;    /* line 28952 */</FONT></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"><FONT face="Times New Roman">            </FONT>(作为一个例子采用<B normal"><FONT face="Times New Roman">SIG_BLOCK</FONT></B>的情况),实际代码类似如下代码:</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">    /* how sys_rt_sigprocmask does SIG_BLOCK.*/</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">      new_set =  *set ;<B normal">      /</B>* line 28625 */</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">      new_set |= old_set ; <B normal">   /</B>* line 28639 */</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">      blocked |= new_set ;    /* line 28648 */</FONT></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"><FONT face="Times New Roman">            </FONT>我不明白为什么<B normal"><FONT face="Times New Roman">sys_rt_sigprocmask</FONT></B>不使用和<B normal"><FONT face="Times New Roman">sys_sigprocmask</FONT></B>相同的方式实现,而且这样还可以节约一点效率。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">中断</H2><P 0cm 0cm 0pt">中断的名字十分形象,因为它们终止了系统正常的处理过程。在前面第<FONT face="Times New Roman">5</FONT>章中你就已经看到了中断的一个例子:提供系统调用基本机制的软件中断。在本章中,我们来了解一下硬件中断。</P><P 0cm 0cm 0pt">和系统调用中断一样,硬件中断也可能转化为内核模式运行然后返回。如果用户进程运行时发生了中断,系统就转化为内核模式,并且内核要对中断做出响应。接着,内核将控制返回给用户进程,用户进程能够从当时离开的位置继续运行。</P><P 0cm 0cm 0pt">同系统调用中断的另一个区别是硬件中断可能在内核已经在内核模式下运行时发生。这在系统调用中很少发生——通常内核不会麻烦地触发系统调用中断,因为它可以直接调用目标内核函数。如果中断发生时系统处于内核模式,结果就同在用户模式下的机制相一致——唯一的区别是内核自身所特有的执行过程而不是用户进程的执行过程暂时地被中断。</P><P 0cm 0cm 0pt">如果内核在一段时期内不希望被中断,那么就可以使用<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">13105</FONT>行和<FONT face="Times New Roman">13104</FONT>行是<FONT face="Times New Roman">UP</FONT>版本;<FONT face="Times New Roman">1216</FONT>行和<FONT face="Times New Roman">1229</FONT>行是<FONT face="Times New Roman">SMP</FONT>版本)屏蔽和开启中断。这些函数根据底层的<FONT face="Times New Roman">x86</FONT>指令命名:<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">cli</FONT></B>清空这个标志从而禁止中断,也可以使用<B normal"><FONT face="Times New Roman">sti</FONT></B>设置这个标志从而允许中断。在<FONT face="Times New Roman">UP</FONT>代码中,你可以选择调用两个等价的宏<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">13105</FONT>行和<FONT face="Times New Roman">13104</FONT>行。</P><P 0cm 0cm 0pt">当然,把内核移植到非<FONT face="Times New Roman">x86</FONT>平台上会使用不同的底层指令——在这些体系结构中<B normal"><FONT face="Times New Roman">cli</FONT></B>和<B normal"><FONT face="Times New Roman">sti</FONT></B>函数的实现都不相同。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">IRQs</H2><P 0cm 0cm 0pt"><FONT face="Times New Roman">IRQ</FONT>,或者中断请求,是从硬件设备发往<FONT face="Times New Roman">CPU</FONT>的中断信号。作为对<FONT face="Times New Roman">IRQ</FONT>的响应,<FONT face="Times New Roman">CPU</FONT>跳转到某个地址——中断服务例行程序(<B normal"><I normal"><FONT face="Times New Roman">ISR</FONT></I></B>),更普通的情况是调用中断处理程序——内核在前面已经对这些处理程序进行了登记。中断处理程序是内核执行的为中断服务的函数;从中断处理程序中返回就继续执行中断前所在位置的代码。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">IRQ</FONT>是有编号的,每一个硬件设备在系统中都对应一个<FONT face="Times New Roman">IRQ</FONT>号码。例如在<FONT face="Times New Roman">IBM PC</FONT>体系结构中,<FONT face="Times New Roman">IRQ 0</FONT>就关联着一个每秒产生<FONT face="Times New Roman">100</FONT>次中断的定时器。把<FONT face="Times New Roman">IRQ</FONT>号码和设备关联起来,使得<FONT face="Times New Roman">CPU</FONT>可以区分每个中断是哪个设备产生的,从而允许它跳转到正确的中断处理程序。(在某些情况中,在一个系统中一个<FONT face="Times New Roman">IRQ</FONT>号可以被多个设备所共用,当然这不是非常普遍的情况。)</P><H2 13pt 0cm; TEXT-INDENT: 0cm">Bottom Halves</H2><P 0cm 0cm 0pt">中断处理程序的下半部分(<FONT face="Times New Roman">bottom half</FONT>)是无须立即执行的部分。在某些中断之后,你甚至可能根本就不需要执行它。</P><P 0cm 0cm 0pt">给定的中断处理程序从概念上可以被分为上半部分(<FONT face="Times New Roman">top half</FONT>)和下半部分(<FONT face="Times New Roman">bottom half</FONT>);在中断发生时上半部分的处理过程立即执行,但是下半部分(如果有的话)却推迟执行。这是通过把上半部分和下半部分处理为独立的函数并对其区别对待实现的。总之,上半部分要决定其相关的下半部分是否需要执行。不能推迟的部分显然不会属于下半部分,但是可以推迟的部分只是可能属于下半部分。</P><P 0cm 0cm 0pt">你也许会很奇怪为什么<FONT face="Times New Roman">Linux</FONT>会辛苦地把它们区分开——为什么要延迟呢?一个原因是要把中断的总延迟时间最小化。<FONT face="Times New Roman">Linux</FONT>内核定义了两种类型的中断,快速的和慢速的,这两者之间的一个区别是慢速中断自身还可以被中断,而快速中断则不能。因此,当处理快速中断时,如果有其它中断到达——不管是快速中断还是慢速中断——它们都必须等待。为了尽可能快地处理这些其它的中断,内核就需要尽可能地将处理延迟到下半部分执行。</P><P 0cm 0cm 0pt">另外一个原因是,在最低层,当内核执行上半部分时,中断控制芯片将被告知禁止正在服务的这个特殊<FONT face="Times New Roman">IRQ</FONT>(这和<FONT face="Times New Roman">CPU</FONT>级别的中断禁止不同,它把快速中断和慢速中断区别开来)。我们并不希望这种状态会持续地比需要的时间还长,因此只有上半部分中时间最为关键的部分才被处理,但是下半部分中其它的工作就要延迟处理了。</P><P 0cm 0cm 0pt">区分上下部分还有一个原因是处理程序的下半部分包含有一些中断所不一定非要处理的操作,只要内核可以在一系列设备中断之后可以从某些地方得到。在这种情况下,执行对于每个中断的下半部分的处理完全是一种浪费,它可以稍稍延迟并在后来只执行一次。</P><P 0cm 0cm 0pt">最后一段的一个暗示是值得说明的:没有必要每次中断都调用下半部分。相反,是上半部分(或者也可能是其它代码)简单地标记下半部分,通过设置某一位来指明下半部分必须执行。如果下半部分已经标记过需要执行了,现在又再次标记,那么内核就简单地保持这个标记;当情况允许的时候,内核就对它进行处理。如果在内核有机会运行其下半部分之前给定的设备就已经发生了<FONT face="Times New Roman">100</FONT>次中断,那么内核的上半部分就运行<FONT face="Times New Roman">100</FONT>次,下半部分运行<FONT face="Times New Roman">1</FONT>次。</P><P 0cm 0cm 0pt">下半部分在内核中有时候被认为是“软<FONT face="Times New Roman">IRQ</FONT>”或者“软中断处理程序”,这有助于你理解今后要遇到的一些文件名和术语。</P><P 0cm 0cm 0pt">在本节的剩余内容中,我们将保持下半部分概念的抽象。下一节深入介绍定时器中断,包括其下半部分的处理,并展示了下半部分概念的一个有趣的滥用现象——我的意思是一个有趣的变种。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">数据结构</H2><P 0cm 0cm 0pt">同对信号量的处理一样,我们首先介绍一下中断和下半部分使用的重要的数据结构。图<FONT face="Times New Roman">6.1</FONT>阐述了这些数据类型之间的关系。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><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></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm; TEXT-ALIGN: center" align=center>图<FONT face="Times New Roman">6.1 </FONT>有关中断的数据结构</P><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt">我们从这里开始,体系结构无关的头文件<FONT face="Times New Roman">linux/interrupt.h</FONT>定义了<B normal"><FONT face="Times New Roman">struct irqaction</FONT></B>结构(<FONT face="Times New Roman">14844</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>关联的)。其成员如下:</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">handler</FONT></B>——指向某一函数的指针,该函数是作为对中断的响应所执行的操作。</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">flags</FONT></B>——从与前面已经介绍过了的<B normal"><FONT face="Times New Roman">sa_flags</FONT></B>相同的集合中提取出来;这个集合从<FONT face="Times New Roman">12108</FONT>行开始。该集合中仅仅为此目的而出现的值只有<B normal"><FONT face="Times New Roman">SA_INTERRUPT</FONT></B>(使用另外一个中断来中断这个中断也是可以的),<B normal"><FONT face="Times New Roman">SA_SAMPLE_RANDOM</FONT></B>(考虑到这个中断也是源于物理随机性),和<B normal"><FONT face="Times New Roman">SA_SHIRQ</FONT></B>(这个<FONT face="Times New Roman">IRQ</FONT>和其它<B normal"><FONT face="Times New Roman">struct irqaction</FONT></B>共享)。</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">mask</FONT></B>——在<FONT face="Times New Roman">x86</FONT>或者体系结构无关的代码中不会使用(除非将其设置为<FONT face="Times New Roman">0</FONT>);看起来只有在<FONT face="Times New Roman">SPARC64</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">name</FONT></B>——生成中断的硬件设备的名字。由于不止一个硬件可以共享一个<FONT face="Times New Roman">IRQ</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">dev_id</FONT></B>——标识硬件类型的一个唯一的<FONT face="Times New Roman">ID</FONT>——<FONT face="Times New Roman">Linux</FONT>支持的所有硬件设备的每一种类型都有一个由制造厂商定义的在此成员中记录的设备<FONT face="Times New Roman">ID</FONT>。其所有的可能值都是从一个巨大的集合中抽取出来的,这个集合在本书中没有介绍,因为它包含的内容是十分繁琐的,而且都是重复的——它仅仅是结构上类似于下面一小段代码的巨大宏定义块。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">#define PCI_DEVICE_ID_S3_868 0x8880</FONT></P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">#define PCI_DEVICE_ID_S3_928 0x88b0</FONT></P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">#define PCI_DEVICE_ID_S3_864_1 0x88c0</FONT></P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">#define PCI_DEVICE_ID_S3_864_2 0x88c2</FONT></P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: 0cm">在你看完这些的一部分之后,也就相当于将其完整的看了一下。可能你已经发现了,摘录的这部分内容是从包含针对基于<FONT face="Times New Roman">S3</FONT>的<FONT face="Times New Roman">PCI</FONT>显卡的设备<FONT face="Times New Roman">ID</FONT>的文件中选取的。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: 0cm">虽然<B normal"><FONT face="Times New Roman">dev_id</FONT></B>是一个指针,可它并不指向任何内容,但若将其解除参照就会引起错误。能够说明问题的是它的位结构模式。</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">next</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">IRQ</FONT>不是共享的,因此这个成员就为空。</P><P 0cm 0cm 0pt">接下来我们感兴趣的两个数据结构存在于体系结构相关的文件<FONT face="Times New Roman">arch/i386/kernel/irq.h</FONT>中。第一个是<B normal"><FONT face="Times New Roman">struct hw_interrupt_type</FONT></B>结构(<FONT face="Times New Roman">1673</FONT>行),它是一个抽象的中断控制器。这是一系列的指向函数的指针,这些函数处理控制器特有的操作:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo3; tab-stops: list 42.25pt">l         <B normal"><FONT face="Times New Roman">typename</FONT></B>——赋给控制器的人工可读的名字。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo3; tab-stops: list 42.25pt">l         <B normal"><FONT face="Times New Roman">startup</FONT></B>——允许从给定的控制器的<FONT face="Times New Roman">IRQ</FONT>所产生的事件。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo3; tab-stops: list 42.25pt">l         <B normal"><FONT face="Times New Roman">shutdown</FONT></B>——禁止从给定的控制器的<FONT face="Times New Roman">IRQ</FONT>所产生的事件。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo3; tab-stops: list 42.25pt">l         <B normal"><FONT face="Times New Roman">handle</FONT></B>——根据提供给该函数的<FONT face="Times New Roman">IRQ</FONT>处理唯一的中断。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo3; tab-stops: list 42.25pt">l         <B normal"><FONT face="Times New Roman">enable</FONT></B>和<B normal"><FONT face="Times New Roman">disable</FONT></B>——这两个函数基本上和<B normal"><FONT face="Times New Roman">startup</FONT></B>和<B normal"><FONT face="Times New Roman">shutdown</FONT></B>相同;存在的差异对于本书中涉及的代码都不很重要。(实际上,对于本书中包含的所有代码来说,<FONT face="Times New Roman"><B normal">enable</B>/<B normal">disable</B></FONT>函数对和<FONT face="Times New Roman"><B normal">startup</B>/<B normal">shutdown</B></FONT>函数对都是相同的。)</P><P 0cm 0cm 0pt">这个文件中我们感兴趣的另外一个数据结构是<B normal"><FONT face="Times New Roman">irq_desc_t</FONT></B><B normal">(</B><FONT face="Times New Roman">1698</FONT>行),它具有如下成员:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo4; tab-stops: list 42.25pt">l         <B normal"><FONT face="Times New Roman">status</FONT></B>——一个整数,它的位或者为<FONT face="Times New Roman">0</FONT>,或者对应从<FONT face="Times New Roman">1685</FONT>行到<FONT face="Times New Roman">1689</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 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo4; tab-stops: list 42.25pt">l         <B normal"><FONT face="Times New Roman">handler</FONT></B>——指向<B normal"><FONT face="Times New Roman">hw_interrupt_type</FONT></B>的指针。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo4; tab-stops: list 42.25pt">l         <B normal"><FONT face="Times New Roman">action</FONT></B>——指向<B normal"><FONT face="Times New Roman">irqaction</FONT></B>结构组成的队列的头。如同前面说明的一样,正常情况下每个<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>被两个或者多个设备所共享,那么这个队列中就有多个操作了。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo4; tab-stops: list 42.25pt">l         <B normal"><FONT face="Times New Roman">depth</FONT></B>——<B normal"><FONT face="Times New Roman">irq_desc_t</FONT></B>的当前用户的个数。主要是用来保证事件正在处理的过程中<FONT face="Times New Roman">IRQ</FONT>不会被禁止。</P><P 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">irq_desc_t</FONT></B>是在<B normal"><FONT face="Times New Roman">irq_desc</FONT></B>数组中(<FONT face="Times New Roman">733</FONT>行)积聚起来的。对于每一个<FONT face="Times New Roman">IRQ</FONT>都有一个数组入口,因此数组把每一个<FONT face="Times New Roman">IRQ</FONT>映射到和它相关的处理程序和<B normal"><FONT face="Times New Roman">irq_desc_t</FONT></B>中的其它信息上。</P><P 0cm 0cm 0pt">最后一个需要说明的数据结构集合从<FONT face="Times New Roman">29094</FONT>行开始;这些都与前面所讨论的下半部分有关:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo5; tab-stops: list 42.25pt">l         <B normal"><FONT face="Times New Roman">bh_mask_count</FONT></B>(<FONT face="Times New Roman">29094</FONT>行)——跟踪为每个下半部分提出的<FONT face="Times New Roman"><B normal">enable</B>/<B normal">disable</B></FONT>请求嵌套对的数组。这些请求通过调用<B normal"><FONT face="Times New Roman">enable_bh</FONT></B>(<FONT face="Times New Roman">12575</FONT>行)和<B normal"><FONT face="Times New Roman">disable_bh</FONT></B>(<FONT face="Times New Roman">12568</FONT>行)实现。每个禁止请求都增加计数器;每个使能请求都减小计数器。当计数器达到<FONT face="Times New Roman">0</FONT>时,所有未完成的禁止语句都已经被使能语句所匹配了,因此下半部分最终被重新使能。</P><B normal">bh_mask</B>和<B normal">bh_active</B>(14856行和14857行)——它们共同控制下半部分是否运行。它们两个都有32位,而每一个下半部分都占用一位。当一个上半部分(或者一些其它代码)决定其下半部分需要运行时,就通过设置<B normal">bh_active</B>(12498行中使用<B normal">mark_bh</B>)中的一位来标记下半部分。不管是否经过了这样的标记,下半部
作者: ilikenba    时间: 2005-3-4 22:08
< 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>
作者: ilikenba    时间: 2005-3-4 22:10
<H1 12pt 0cm; TEXT-INDENT: 0cm">第<FONT face="Times New Roman">7</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>内核和其它<FONT face="Times New Roman">Unix</FONT>变种一样,都是采用了多任务技术;它可以在许多进程之间分配时间片从而使这些进程看起来似乎在同时运行一样。这里通常是内核对有关资源的访问作出仲裁;在这种情况下,资源就是<FONT face="Times New Roman">CPU</FONT>时间。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">进程传统上都有唯一的执行程序的上下文——这是说明在某个时刻它正在处理一项内容的流行的方法。在给定的时刻,我们可以精确地知道代码的哪一部分正在执行。但是有时我们希望一个进程同时处理多件事情。例如,我们可能希望<FONT face="Times New Roman">Web</FONT>浏览器获取并显示<FONT face="Times New Roman">Web</FONT>页,同时也要监视用户是否点击停止按钮。只为监视停止按钮而运行一个全新的程序显然是不必要的,但是对于<FONT face="Times New Roman">Web</FONT>浏览器来说要对其时间进行分隔也并不总是非常方便——获取一些<FONT face="Times New Roman">Web</FONT>页信息,检测停止按钮,再获取一些<FONT face="Times New Roman">Web</FONT>页信息,再重新检测停止按钮,等等。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">对于这个问题的比较流行的解决方法是线程。从概念上来说,线程是同一个进程中独立的执行上下文——更简单一点地说,它们为单一进程提供了一种同时处理多件事情的方法,就像是进程是一个自行控制的微缩化了的多任务操作系统。同一线程组中的线程共享它们的全局变量并有相同的堆(<FONT face="Times New Roman">heap</FONT>),因此使用<B normal"><FONT face="Times New Roman">malloc</FONT></B>给线程组中的一个线程分配的内存可以被该线程组中的其它线程读写。但是它们拥有不同的堆栈(它们的局部变量是不共享的)并可以同时在进程代码不同的地方运行。这样,你的<FONT face="Times New Roman">Web</FONT>浏览器可以让一个线程来获取并显示<FONT face="Times New Roman">Web</FONT>页,同时另外一个线程观测停止按钮是否被点击,并且在停止按钮被点击时停止第一个线程。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">和线程等价的一种观点——这是<FONT face="Times New Roman">Linux</FONT>内核使用的观点——线程只是偶然的共享相同的全局内存空间的进程。这意味着内核无需为线程创建一种全新的机制,否则必然会和现在已经编写完成的进程处理代码造成重复,而且有关进程的讨论绝大多数也都可以应用到线程上。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">当然,以上的说明仅仅适用于内核空间的线程。实际中也有用户空间的线程,它执行相同的功能,但是却是在应用层实现的。用户空间的线程和内核空间的线程相比有很多优点,也有很多缺点,但是有关这些问题的讨论超出了本书的范围。而使人更加容易造成混淆是一个名为<B normal"><FONT face="Times New Roman">kernel_thread</FONT></B>(<FONT face="Times New Roman">2426</FONT>行)的函数,尽管该函数被赋予了这样一个名字,但是它实际和内核空间的线程没有任何关系。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">部分是由于历史的原因,部分是由于<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">CPU</FONT>访问的裁决过程被称为调度(<FONT face="Times New Roman">Scheduling</FONT>)。良好的调度决策要尊重用户赋予的优先级,这可以建立一种所有进程都在同时运行的十分逼真的假象。糟糕的调度决策会使操作系统变得沉闷缓慢。这是<FONT face="Times New Roman">Linux</FONT>调度程序必须经过高度优化的一个原因。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">从概念上来说,调度程序把时间分为小片断,并根据一定的原则把这些片断分配给进程。你可能已经猜到,时间的这些小片断称为时间片。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">实时进程</H2>< 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">OSIX.1b</FONT>(原名为<FONT face="Times New Roman">OSIX.4</FONT>)操作系统标准所规定的“实时”调度程序。因此,本书中有时会使用实时进程(从技术上考虑,系统使用术语“非实时进程(<FONT face="Times New Roman">nonrealtime process</FONT>)”来作为实时进程的对应,虽然我更倾向于使用另外一个术语<FONT face="Times New Roman">unrealtime process</FONT>)。不要过分计较“实时”这个术语,虽然——如果从硬件的角度来看待这个问题,实时意味着你可以得到有关操作系统的某种性能保证,例如有关中断等待时间的承诺,但是这一点在<FONT face="Times New Roman">Linux</FONT>实时调度规则中并没有提供。相反的,<FONT face="Times New Roman">Linux</FONT>的调度规则是“软件实时”,也就是说如果实时进程需要,它们就只把<FONT face="Times New Roman">CPU</FONT>分配给实时进程;否则就把<FONT face="Times New Roman">CPU</FONT>时间让出给非实时进程。</P>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">但是如果你真正需要,一些<FONT face="Times New Roman">Linux</FONT>的变种也承诺提供一种“硬实时”。但是,在当前的<FONT face="Times New Roman">Linux</FONT>内核中——因此也就是在本章中——“实时”仅指“软件实时”。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">优先级</H2>< 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">非实时进程有两种优先级,一种是静态优先级,另一种是动态优先级。实时进程又增加了第三种优先级,实时优先级。优先级是一些简单的整数,它代表了为决定应该允许哪一个进程使用<FONT face="Times New Roman">CPU</FONT>的资源时判断方便而赋予进程的权值——优先级越高,它得到<FONT face="Times New Roman">CPU</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">CPU</FONT>之前该进程所应该被允许的时间片的最大值。(但是也可能由于其它原因,在该时间片耗尽之前进程就被迫交出了<FONT face="Times New Roman">CPU</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">CPU</FONT>,它就随着时间不断减小;当它小于<FONT face="Times New Roman">0</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">CPU</FONT>交给哪一个其它进程:较高权值的进程总是优先于较低权值的进程。因为如果一个进程不是实时进程,其优先级就是<FONT face="Times New Roman">0</FONT>,所以实时进程总是优先于非实时进程的。(这并不完全正确;如同后面论述的一样,实时进程也会明确地交出<FONT face="Times New Roman">CPU</FONT>,而在等待<FONT face="Times New Roman">I/O</FONT>时也会被迫交出<FONT face="Times New Roman">CPU</FONT>。前面的描述仅限于能够交付<FONT face="Times New Roman">CPU</FONT>运行的进程)</P><H2 13pt 0cm; TEXT-INDENT: 0cm">进程ID(PIDs)</H2>< 0cm 0cm 0pt">传统上每个<FONT face="Times New Roman">Unix</FONT>进程都有一个唯一的标志符,它是一个被称为进程标志符(<FONT face="Times New Roman">ID</FONT>)的,范围在<FONT face="Times New Roman">0</FONT>到<FONT face="Times New Roman">32,767</FONT>之间的整数。<FONT face="Times New Roman">ID 0</FONT>和<FONT face="Times New Roman">ID 1</FONT>对于系统有特定的意义;其它的进程标识符都被认为是普通进程。在本章后面对<B normal"><FONT face="Times New Roman">get_pid</FONT></B>的讨论中,你会看到<FONT face="Times New Roman">ID</FONT>是如何生成和赋值的。</P>< 0cm 0cm 0pt">在<FONT face="Times New Roman">Linux</FONT>中,<FONT face="Times New Roman">ID</FONT>不一定非要唯一——虽然通常都是唯一的,但是两个任务也可以共享一个<FONT face="Times New Roman">ID</FONT>。这是<FONT face="Times New Roman">Linux</FONT>对线程支持的一个副作用,这些线程从概念上讲应该共享一个<FONT face="Times New Roman">ID</FONT>,因为它们是同一个进程的一部分。在<FONT face="Times New Roman">Linux</FONT>中,你可以创建两个任务,并且共享且仅共享它们的<FONT face="Times New Roman">ID</FONT>——从实际使用角度讲它们不会是线程,但是它们可以使用同一个<FONT face="Times New Roman">ID</FONT>。这并没有多大的意义,但是如果你希望这样处理,<FONT face="Times New Roman">Linux</FONT>是支持的。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">引用计数</H2>< 0cm 0cm 0pt">引用计数是多个对象之间为共享普通信息而广泛使用的技术。使用更通用的术语来说,一个或多个“容器对象”携带指向共享数据对象的指针,其中包含了一个称为“引用计数(<FONT face="Times New Roman">Reference Count</FONT>)”的整数;这个引用计数的值和共享数据的容器对象的个数相同。希望共享数据的新容器对象将被赋予一个指向同一结构的指针,并且递增该共享数据对象的引用计数。</P>< 0cm 0cm 0pt">当容器对象离开时,就递减共享数据的引用计数,并做到“人走灯熄”——也就是当引用计数减小到<FONT face="Times New Roman">0</FONT>时,容器对象回收共享对象。图<FONT face="Times New Roman">7.1</FONT>阐述了这种技术。</P>< 0cm 0cm 0pt"><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></P><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-ALIGN: center; mso-outline-level: 1" align=center>图<FONT face="Times New Roman">7.1 </FONT>引用计数</P><P 0cm 0cm 0pt; TEXT-ALIGN: center; mso-outline-level: 1" align=center><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; mso-outline-level: 1">就象你随后会看到的那样,<FONT face="Times New Roman">Linux</FONT>通过使用引用计数技术来实现线程间的数据共享。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">权能</H2><P 0cm 0cm 0pt">在早期的<FONT face="Times New Roman">Unix</FONT>中,你或者是<FONT face="Times New Roman">root</FONT>用户,或者不是。如果你是<FONT face="Times New Roman">root</FONT>,你几乎可以进行任何希望进行的操作,即使你的想法实际上十分糟糕,例如删除系统引导盘上的所有文件。如果你不是<FONT face="Times New Roman">root</FONT>,那么你就不可能对系统造成太大的损害,但是你也不能执行任何重要的系统管理任务。</P><P 0cm 0cm 0pt">不幸的是,很多应用程序的需要都介于这两个安全性极端之间。例如,修改系统时间是只有<FONT face="Times New Roman">root</FONT>才能执行的操作,因此实现它的程序必须作为<FONT face="Times New Roman">root</FONT>运行。但是因为是作为<FONT face="Times New Roman">root</FONT>运行的,修改系统时间的进程也就能处理<FONT face="Times New Roman">root</FONT>可以完成的任何事情。对于编写良好的程序来说并不会造成问题,但是程序仍然会有意无意地把系统搞得一团糟。(数不清的计算机攻击事件都是欺骗<FONT face="Times New Roman">root</FONT>去运行一些看似值得信任的可执行代码,造成了一些恶作剧。)</P><P 0cm 0cm 0pt">这些问题中有一些可以通过正确使用组和诸如<FONT face="Times New Roman">sudo</FONT>之类的程序而避免,但是有一些则不行。对于某些重要的操作,虽然你可能只想允许它们执行一两种权限操作,你也只能给予这些进程普通<FONT face="Times New Roman">root</FONT>访问许可。<FONT face="Times New Roman">Linux</FONT>对于这个问题的解决方法是使用从现在已经舍弃了的<FONT face="Times New Roman">POSIX</FONT>草案标准中抽取出来的思想:权能。</P><P 0cm 0cm 0pt">权能使你可以更精确的定义经授权的进程所允许处理的事情。例如,你可以给一个进程授予修改系统时间的权力,而没有授予它可以杀掉系统中的其它进程、毁坏你的文件、并胡乱运行的权力。而且,为了帮助防止意外地滥用其优先级,长时间运行的进程可以暂时获得权能(如果允许),只要时间足够处理特殊的零碎工作就可以了,在处理完这个零碎的工作以后再收回权能。</P><P 0cm 0cm 0pt">在本书的编写期间,权能仍然处于开发状态。为了完全实现权能的预期功能,开发者们还必须要实现一些新的特性——例如,目前还没有内核支持将程序的权能附加到文件本身中。这样所造成的一个后果是<FONT face="Times New Roman">Linux</FONT>有时仍要检测进程是否作为<FONT face="Times New Roman">root</FONT>运行,而不是检测所进程需要的特殊权能。但是迄今为止已经实现了的内容仍然是十分有用的。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">进程在内核中是如何表示的</H2><P 0cm 0cm 0pt">内核使用几个数据结构来跟踪进程;其中有一些和进程自身的表示方法是密切相关的,另外一些则是独立的。图<FONT face="Times New Roman">7.2</FONT>阐述了这些数据结构,随后就会对它们进行详细介绍。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt; TEXT-ALIGN: center" align=center><wrapblock><v:shape><v:imagedata></v:imagedata><w:wrap type="topAndBottom"></w:wrap></v:shape></wrapblock><BR vglayout" clear=all>图<FONT face="Times New Roman">7.2 </FONT>管理任务使用的内核数据结构<p></p></P><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16325</FONT>:表示进程的内核数据结构是<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>。我们暂时向前跳过这个结构的定义,继续往下看。它相当大,但是可以从逻辑上划分为很多部分。随着本章讨论的展开,你将会逐渐清楚它们每一部分的意义。在阅读的过程中,要注意这个结构的很多部分都是指向其它结构的指针;这在子孙进程和祖先进程希望共享指针所指向的信息时可以灵活运用——很多指针都指向正在被引用计数的信息。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16350</FONT>:任务本身使用<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构的<B normal"><FONT face="Times New Roman">next_task</FONT></B>和<B normal"><FONT face="Times New Roman">prev_task</FONT></B>成员组成一个循环的双向链接列表,它被称为任务队列。的确,这忽略了一个事实,它们在中心数组<B normal"><FONT face="Times New Roman">task</FONT></B>(很快就会讨论)中早已存在了。最初这看起来可能有些奇怪,但实际上这是十分有用的,因为这样允许内核代码可以遍历执行所有现存的任务——也就是<B normal"><FONT face="Times New Roman">task</FONT></B>中所有经过填充的时间片——而无须浪费时间跳过空时间片。实际上对这个循环的访问是如此频繁,以至于在<FONT face="Times New Roman">16898</FONT>行单独为它定义了一个宏<B normal"><FONT face="Times New Roman">for_each_task</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>虽然<B normal"><FONT face="Times New Roman">for_each_task</FONT></B>是单向的,但是它有一些值得注意的特性。首先,注意到循环的开始和末尾都是<B normal"><FONT face="Times New Roman">init_task</FONT></B>。这是很安全的,因为<B normal"><FONT face="Times New Roman">init_task</FONT></B>从来不会退出;因此,作为标记它一直都是可用的。但是,注意到<B normal"><FONT face="Times New Roman">ini_task</FONT></B>本身不是作为循环的一部分而访问的——这恰好就是你使用这个宏时所需要的东西。还有,作为我们关心的一小部分,你总是使用<B normal"><FONT face="Times New Roman">next_task</FONT></B>成员直接向前遍历执行列表的;不存在相关的向后执行的宏。也没有必要需要这样一个宏——只有在需要及时把任务从列表中处理清除时才需要使用<B normal"><FONT face="Times New Roman">prev_task</FONT></B>成员。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16351</FONT>:<FONT face="Times New Roman">Linux</FONT>还保持一个和这个任务列表类似的循环的双向任务列表。这个列表使用<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构的<B normal"><FONT face="Times New Roman">prev_run</FONT></B>成员和<B normal"><FONT face="Times New Roman">next_tun</FONT></B>成员进行链接,基本上是作为队列来处理的(这真值得让人举杯庆祝);出于这个原因,这个列表通常被称为运行队列(<FONT face="Times New Roman">run queue</FONT>)。对于<B normal"><FONT face="Times New Roman">next_task</FONT></B>来说,只是因为需要高效地将一个项移出队列才会使用到<B normal"><FONT face="Times New Roman">prev_run</FONT></B>成员;对于这个列表的遍历循环执行通常都是使用<B normal"><FONT face="Times New Roman">next_run</FONT></B>向前的。同样,在这个任务队列中也使用<B normal"><FONT face="Times New Roman">init_task</FONT></B>来标记队列的开始和末尾。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>通过使用<B normal"><FONT face="Times New Roman">add_to_runqueue</FONT></B>(<FONT face="Times New Roman">26276</FONT>行)能够将任务加入队列,而使用<B normal"><FONT face="Times New Roman">del_from_runqueue</FONT></B>(<FONT face="Times New Roman">26287</FONT>行)则把任务移出队列。有时候分别使用<B normal"><FONT face="Times New Roman">move_first_runqueue</FONT></B>(<FONT face="Times New Roman">26318</FONT>行)和<B normal"><FONT face="Times New Roman">move_last_runqueue</FONT></B>(<FONT face="Times New Roman">26300</FONT>行)把它们强制移动到队列的开头和末尾。注意这些函数都是局限于<FONT face="Times New Roman">kernel/sched.c</FONT>的,在别的文件中不会使用<B normal"><FONT face="Times New Roman">prev_run</FONT></B>和<B normal"><FONT face="Times New Roman">next_run</FONT></B>域(特别是在<FONT face="Times New Roman">kernel/fork.c</FONT>文件中的进程创建期间);这是十分恰当的,因为只有在调度时才需要运行队列。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16370</FONT>:首先,任务能够组成一个图,该图的结构表达了任务之间的家族关系;由于我不清楚这个图所使用的通用术语,我就称它为进程图(<FONT face="Times New Roman">process graph</FONT>)。这和<FONT face="Times New Roman"><B normal">next_task</B>/<B normal">prev_task</B></FONT>之间的连接根本没有关系,在那里任务的位置是毫无意义的——只是一个偶然的历史事件而已。每一个<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>中有五个指向进程图表中自己位置的指针。这五个指针在从<FONT face="Times New Roman">16370</FONT>行到<FONT face="Times New Roman">16371</FONT>行的代码中被定义。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo3; tab-stops: list 57.25pt">l         <B normal"><FONT face="Times New Roman">p_opptr</FONT></B>指向进程的原始祖先;通常和<B normal"><FONT face="Times New Roman">p_pptr</FONT></B>类似。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo3; tab-stops: list 57.25pt">l         <B normal"><FONT face="Times New Roman">p_pptr</FONT></B>指向进程的当前祖先。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo3; tab-stops: list 57.25pt">l         <B normal"><FONT face="Times New Roman">p_cptr</FONT></B>指向进程的最年青(最近)子孙。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo3; tab-stops: list 57.25pt">l         <B normal"><FONT face="Times New Roman">p_ysptr</FONT></B>指向进程的下一个最年青(下一个最近)兄弟。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo3; tab-stops: list 57.25pt">l         <B normal"><FONT face="Times New Roman">p_osptr</FONT></B>指向进程的下一个最古老(下一个最远)兄弟。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><wrapblock><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 21pt; TEXT-INDENT: 0cm; TEXT-ALIGN: center" align=center>图<FONT face="Times New Roman">7.3  </FONT>进程图</P><P 0cm 0cm 0pt 21pt; TEXT-INDENT: 0cm; TEXT-ALIGN: center" align=center><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt">图<FONT face="Times New Roman">7.3</FONT>说明了它们之间的关系(整个链接集合都以标号为“<FONT face="Times New Roman">Me</FONT>”的节点为核心)。这个指针的集合还提供了浏览系统中进程集合的另外一种方法;显然,在处理诸如查找进程祖先或者查找列表中进程子孙时这个指针特别有用。这个指针是由两个宏维护的:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo4; tab-stops: list 42.25pt">l         <B normal"><FONT face="Times New Roman">REMOVE_LINKS</FONT></B>(<FONT face="Times New Roman">16876</FONT>行)从图中移出指针。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo4; tab-stops: list 42.25pt">l         <B normal"><FONT face="Times New Roman">SET_LINKS</FONT></B>(<FONT face="Times New Roman">16887</FONT>行)向图中插入指针。</P><P 0cm 0cm 0pt">这两个宏都可以调整<FONT face="Times New Roman"><B normal">next_task</B>/<B normal">prev_task</B></FONT>的链接。如果你仔细研究一下这两个宏,你就会发现它们只是增加或者删除叶子进程——而从不对拥有子孙进程的进程进行处理。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16517</FONT>:<B normal"><FONT face="Times New Roman">task</FONT></B>定义为由指向<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构的指针组成的数组。这个数组中的每一个项代表系统中的一个任务。数组的大小是<B normal"><FONT face="Times New Roman">NR_TASKS</FONT></B>(在<FONT face="Times New Roman">18320</FONT>行设置为<FONT face="Times New Roman">512</FONT>),它规定了系统中可以同时运行的任务数量的上限。由于一共有<FONT face="Times New Roman">32,768</FONT>个可能的<FONT face="Times New Roman">PID</FONT>,由于数组不够大,要通过它们的<FONT face="Times New Roman">PID</FONT>直接索引系统中所有任务显然是不可能的。(也就是<B normal"><FONT face="Times New Roman">task</FONT></B>未必是由<FONT face="Times New Roman">PID <B normal">i</B></FONT>指明的任务。)相反,<FONT face="Times New Roman">Linux</FONT>使用其它的数据结构来帮助系统管理这种有限的资源。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16519</FONT>:自由时间片列表<B normal"><FONT face="Times New Roman">tarray_freelist</FONT></B>拥有一个说明<B normal"><FONT face="Times New Roman">task</FONT></B>数组中自由位置的列表(实际上是一个堆栈)。它在<FONT face="Times New Roman">27966</FONT>行和<FONT face="Times New Roman">27967</FONT>行初始化,接着被两个在<FONT face="Times New Roman">16522</FONT>行到<FONT face="Times New Roman">16542</FONT>行定义的内联函数所使用。在<FONT face="Times New Roman">SMP</FONT>平台上,对于<B normal"><FONT face="Times New Roman">tarray_freelist</FONT></B>的访问必须受自旋锁<B normal"><FONT face="Times New Roman">taskslot_lock</FONT></B>(<FONT face="Times New Roman">23475</FONT>行)的限制。(自旋锁在第<FONT face="Times New Roman">10</FONT>章中详细讨论。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16546</FONT>:<B normal"><FONT face="Times New Roman">pidhash</FONT></B>数组有助于把<FONT face="Times New Roman">PID</FONT>映象到指向<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>的指针。<B normal"><FONT face="Times New Roman">pidhash</FONT></B>在<FONT face="Times New Roman">27969</FONT>行和<FONT face="Times New Roman">27970</FONT>行初始化,此后它被一系列在<FONT face="Times New Roman">16548</FONT>行到<FONT face="Times New Roman">16580</FONT>行定义的宏和内联函数所操纵。这些最终实现了一个普通的哈希表。注意,为了处理<FONT face="Times New Roman">hush</FONT>记录,维护<B normal"><FONT face="Times New Roman">pidhash</FONT></B>的函数使用了<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构中的两个成员——<B normal"><FONT face="Times New Roman">pidhash_next</FONT></B>(<FONT face="Times New Roman">16374</FONT>行)和<B normal"><FONT face="Times New Roman">pidhash_pprev</FONT></B>(<FONT face="Times New Roman">16375</FONT>行)。通过使用<B normal"><FONT face="Times New Roman">pidhash</FONT></B>,内核可以通过其<FONT face="Times New Roman">PID</FONT>有效地发现任务——虽然这种方式仍然比直接查找要慢。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>仅仅是为了好玩,你可以自己证明这个哈希函数——<B normal"><FONT face="Times New Roman">pid_hashfn</FONT></B>,<FONT face="Times New Roman">16548</FONT>行——提供了一个均匀覆盖其域<FONT face="Times New Roman">0</FONT>到<FONT face="Times New Roman">32,767</FONT>(所有有效的<FONT face="Times New Roman">PID</FONT>)的发行版本。除非你所谓的“好玩”的概念和我不一样,否则你会同我一样感到有趣。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>这些数据结构提供了有关当前运行系统的很多信息,但是这也需要付出代价:每当增加或删除进程时这些信息必须能够得到正确维护,否则系统就会变得混乱不堪。部分出于实现这种正确的维护非常困难的考虑,进程只在一个地方创建(使用<B normal"><FONT face="Times New Roman">do_fork</FONT></B>,后面会讨论),也只在一个地方删除(使用<B normal"><FONT face="Times New Roman">release</FONT></B>,也在后面中讨论)。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">如果我们能把<B normal"><FONT face="Times New Roman">task</FONT></B>处理为<FONT face="Times New Roman">32,768</FONT>个<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构组成的数组,其中的每一项代表一个可能的<FONT face="Times New Roman">PID</FONT>,那么至少可以消除一部分这种类型的复杂性。但是这样处理会大量增加内核对于内存的需求。每一个<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构在<FONT face="Times New Roman">UP</FONT>平台上占用<FONT face="Times New Roman">964</FONT>字节,在<FONT face="Times New Roman">SMP</FONT>平台上占用<FONT face="Times New Roman">1,212</FONT>字节——取整以后,近似的数字是<FONT face="Times New Roman">1K</FONT>。为了容纳所有这些结构,<B normal"><FONT face="Times New Roman">task</FONT></B>会像气球一样迅速膨胀到<FONT face="Times New Roman">32,768K</FONT>,也就是<FONT face="Times New Roman">32M</FONT>!(实际情况会更糟糕:我们尚未提到的有关任务的额外内存开销会把这个数字增长<FONT face="Times New Roman">8</FONT>倍——也就是<FONT face="Times New Roman">256M</FONT>——而且不要忘记了,这些开销实际上都还没有运行一个任务。)此外,<FONT face="Times New Roman">x86</FONT>的内存管理硬件把活动任务的数量限制在<FONT face="Times New Roman">4,000</FONT>左右;这一主题在下一章介绍。因此,数组中大多数的空间都会不可避免地被浪费了。</P><P 0cm 0cm 0pt">在目前的实现中,如果没有进程在运行,<B normal"><FONT face="Times New Roman">task</FONT></B>仅仅是<FONT face="Times New Roman">512</FONT>个<FONT face="Times New Roman">4</FONT>字节的指针,总共才<FONT face="Times New Roman">2K</FONT>。如果我们考虑到那些附加的数据结构会占用一些额外开销,可能有一些超过这个数字,但是比起<FONT face="Times New Roman">32M</FONT>来还差得远呢。即使是<B normal"><FONT face="Times New Roman">task</FONT></B>中的每一项都使用了,而且每个<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构也都分配了,总共使用的内存也才不过大约<FONT face="Times New Roman">512K</FONT>。应用程序能够忽略这种微小的区别。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">进程状态</H2><P 0cm 0cm 0pt">在一个给定的时间,进程处于下面注释中描述的六种状态中的一种。进程的当前状态被记录在<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构的<B normal"><FONT face="Times New Roman">state</FONT></B>成员中(<FONT face="Times New Roman">16328</FONT>行)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16188</FONT>:<B normal"><FONT face="Times New Roman">TASK_RUNNING</FONT></B>意味着进程准备好运行了。即使是在<FONT face="Times New Roman">UP</FONT>系统中,也有不止一个任务同时处于<B normal"><FONT face="Times New Roman">TASK_RUNNING</FONT></B>状态——<B normal"><FONT face="Times New Roman">TASK_RUNNING</FONT></B>并不意味着该进程可以立即获得<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">16189</FONT>:<B normal"><FONT face="Times New Roman">TASK_INTERRUPTIBLE</FONT></B>是两种等待状态的一种——这种状态意味着进程在等待特定事件,但是也可以被信号量中断。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16190</FONT>:<B normal"><FONT face="Times New Roman">TASK_UNINTERRUPTIBLE</FONT></B>是另外一种等待状态。这种状态意味着进程在等待硬件条件而且不能被信号量中断。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16191</FONT>:<B normal"><FONT face="Times New Roman">TASK_ZOMBIE</FONT></B>意味着进程已经退出了(或者已经被杀掉了),但是其相关的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构并没有被删除。这样即使子孙进程已经退出,也允许祖先进程对已经死去的子孙进程的状态进行查询。在本章后面我们会详细介绍这一点。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16192</FONT>:<B normal"><FONT face="Times New Roman">TASK_STOPPED</FONT></B>意味着进程已经停止运行了。一般情况下,这意味着进程已经接收到了<B normal"><FONT face="Times New Roman">SIGSTOP</FONT></B>,<B normal"><FONT face="Times New Roman">SIGSTP</FONT></B>,<B normal"><FONT face="Times New Roman">SITTIN</FONT></B>或者<B normal"><FONT face="Times New Roman">SIGTTOU</FONT></B>信号量中的一个,但是它也可能意味着当前进程正在被跟踪(例如,进程正在调试器下运行,用户正在单步执行代码)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16193</FONT>:<B normal"><FONT face="Times New Roman">TASK_SWAPPING</FONT></B>主要用于表明进程正在执行磁盘交换工作。然而,这种状态似乎是没有什么用处的——虽然该标志符在整个内核中出现了好几次,但是其值从来没有被赋给进程的<B normal"><FONT face="Times New Roman">state</FONT></B>成员。这种状态正在被逐渐淘汰。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">进程来源:fork和_ _clone</H2><P 0cm 0cm 0pt">传统的<FONT face="Times New Roman">Unix</FONT>实现方法在系统运行以后只给出了一种创建新进程的方法:系统调用<B normal"><FONT face="Times New Roman">fork</FONT></B>。(如果你奇怪第一个进程是哪里来的,实际上该进程是<FONT face="Times New Roman">init</FONT>,在第<FONT face="Times New Roman">4</FONT>章中已经讨论过。)当进程调用<B normal"><FONT face="Times New Roman">fork</FONT></B>时,该进程从概念上被分成了两部分——这就像是道路中的分支——祖先和子孙可以自由选择不同的路径。在<B normal"><FONT face="Times New Roman">fork</FONT></B>之后,祖先进程和其子进程几乎是等同的——它们所有的变量都有相同的值,它们打开的文件都相同,等等。但是,如果祖先进程改变了一个变量的值,子进程将不会看到这个变化,反之亦然。子进程是祖先进程的一个拷贝(至少最初是这样),但是它们并不共享内容。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">Linux</FONT>保留了传统的<B normal"><FONT face="Times New Roman">fork</FONT></B>并增加了一个更通用的函数<B normal"><FONT face="Times New Roman">_ _clone</FONT></B>。(前面的两个下划线有助于强调普通应用程序代码不应该直接调用<B normal"><FONT face="Times New Roman">_ _clone</FONT></B>,应该从在<B normal"><FONT face="Times New Roman">_ _clone</FONT></B>之上建立的线程库中调用这个函数。)鉴于<B normal"><FONT face="Times New Roman">fork</FONT></B>创建一个新的子孙进程后,子孙进程虽然是其祖先进程的拷贝,但是它们并不共享任何内容,<B normal"><FONT face="Times New Roman">_ _clone</FONT></B>允许你定义祖先进程和子孙进程所应该共享的内容。如果你没有给<B normal"><FONT face="Times New Roman">_ _clone</FONT></B>提供它所能够识别的五个标志,子孙进程和祖先进程之间就不会共享任何内容,这样它就和<B normal"><FONT face="Times New Roman">fork</FONT></B>类似。如果你提供了全部的五个标志,子孙进程就可以和祖先进程共享任何内容,这就和传统线程类似。其它标记的不同组合可以使你完成介于两者之间的功能。</P><P 0cm 0cm 0pt">顺便提一下,内核使用<B normal"><FONT face="Times New Roman">kernel_thread</FONT></B>函数(<FONT face="Times New Roman">2426</FONT>行)为了自己的使用创建了几个任务。用户从来不会调用这个函数——实际上,用户也不能调用这个函数;它只在创建例如<FONT face="Times New Roman">kswapd</FONT>(在第<FONT face="Times New Roman">8</FONT>章中介绍)之类的特殊进程时才会使用,这些特殊进程有效地把内核分为很多部分,为了简单起见也把它们当作任务处理。使用<B normal"><FONT face="Times New Roman">kernel_thread</FONT></B>创建的任务具有一些特殊的性质,这些性质我们在此不再深入介绍(例如,它们不能被抢占);但是现在主要需要引起注意的是<B normal"><FONT face="Times New Roman">kernel_thread</FONT></B>使用<B normal"><FONT face="Times New Roman">do_fork</FONT></B>处理其垃圾工作。因此,即使是这些特殊进程,它们最终也要使用你我所使用的普通进程的创建方法来创建。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">do_fork</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23953</FONT>:<B normal"><FONT face="Times New Roman">do_fork</FONT></B>是实现<B normal"><FONT face="Times New Roman">fork</FONT></B>和<B normal"><FONT face="Times New Roman">_ _clone</FONT></B>的内核程序。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23963</FONT>:分配<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构以代表一个新的进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23967</FONT>:给新的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构赋予初始值,该值直接从当前进程中拷贝而来。<B normal"><FONT face="Times New Roman">do_fork</FONT></B>的剩余工作主要包含为祖先进程和子孙进程不会共享的信息建立新的拷贝。(在本行和整个内核中你可以看到的<B normal"><FONT face="Times New Roman">current</FONT></B>是一个宏,它把一个指针指向代表当前正在执行的进程的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构。这在<FONT face="Times New Roman">10285</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">23981</FONT>:新到达者需要<B normal"><FONT face="Times New Roman">task</FONT></B>数组中的一个项;这个项是使用<B normal"><FONT face="Times New Roman">find_empty_process</FONT></B>(<FONT face="Times New Roman">23598</FONT>行——它严格依赖于<FONT face="Times New Roman">16532</FONT>行的<B normal"><FONT face="Times New Roman">get_free_taskslot</FONT></B>)找到的。然而,它工作的方式有点不明显:<B normal"><FONT face="Times New Roman">task</FONT></B>数组没有使用的成员不是设置为空,而是设置为自由列表的下一个元素(使用<B normal"><FONT face="Times New Roman">add_free_taskslot</FONT></B>,<FONT face="Times New Roman">16523</FONT>行)。因此,<B normal"><FONT face="Times New Roman">task</FONT></B>中没有使用的项指向链接列表中另外一个<B normal"><FONT face="Times New Roman">task</FONT></B>没有使用的项,而<B normal"><FONT face="Times New Roman">tarray_freelist</FONT></B>仅仅指向这个列表的表头。那么,返回一个自由位置就简单地变成了返回列表头的问题了(当然要把这个头指针指向下一个元素)。更传统的方法是使用一个独立的数据结构来管理这些信息,但是在内核中,空间总会显得有些不足。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23999</FONT>:给新的任务赋<FONT face="Times New Roman">PID</FONT>(其中的细节很快就会介绍)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24045</FONT>:本行和下面几行,使用该文件中别处定义的辅助函数,根据所提供的<B normal"><FONT face="Times New Roman">clone_flags</FONT></B>参数的值为子孙进程建立祖先进程的数据结构中子孙进程所选择部分的拷贝。如果<B normal"><FONT face="Times New Roman">clone_flags</FONT></B>指明相关的部分应该共享而不是拷贝,这时辅助函数(<FONT face="Times New Roman">help function</FONT>)就简单地增加引用计数接着返回;否则,它就创建新进程所独有的新的拷贝。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24078</FONT>:到现在为止,所有进程所有的数据结构都已经设置过了,但是大部分跟踪进程的数据结构还没有被设置。系统将通过把进程增加到进程图表中开始设置它们。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24079</FONT>:通过调用<B normal"><FONT face="Times New Roman">hash_pid</FONT></B>把新的进程置入<B normal"><FONT face="Times New Roman">pidhash</FONT></B>表中。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">24088</FONT>:通过调用<B normal"><FONT face="Times New Roman">wake_up_process</FONT></B>(<FONT face="Times New Roman">26356</FONT>行)把新的进程设置为<B normal"><FONT face="Times New Roman">TASK_RUNNING</FONT></B>状态并将其置入运行队列。</P><P 0cm 0cm 0pt">注意到现在不止是<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构被设置了,而且所有相关的数据结构——自由时间片列表,任务列表、进程图、运行队列和<FONT face="Times New Roman">PID hash</FONT>表——这些都已经为新的到达者正确地进行了修改。恭喜你,你现在已经得到了一个健康的子孙任务。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">PID的分配</H2><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">PID</FONT>是使用<B normal"><FONT face="Times New Roman">get_pid</FONT></B>函数(<FONT face="Times New Roman">23611</FONT>行)生成的,该函数能够返回一个没有使用的<FONT face="Times New Roman">PID</FONT>。它从<B normal"><FONT face="Times New Roman">last_pid</FONT></B>(<FONT face="Times New Roman">23464</FONT>行)开始——这是最近分配的<FONT face="Times New Roman">PID</FONT>。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">内核中使用的<B normal"><FONT face="Times New Roman">get_pid</FONT></B>的版本是内核复杂性和速度频繁折中的一个例子;这里速度更为重要一些。<B normal"><FONT face="Times New Roman">get_pid</FONT></B>经过了高度优化——它比直接向前的实现方法要复杂的多,但是速度也要快的多。最直接的实现方法将遍历执行整个任务列表——典型的情况可能有几十项,有时候也可能成百上千项——对每一个可能的<FONT face="Times New Roman">PID</FONT>进程检测并找出适当的值。我们见到的版本有时是必须执行这些步骤的,但是在大多数情况下都可以跳过。这一结果被用来帮助加速进程创建的操作,它在<FONT face="Times New Roman">Unix</FONT>上慢得臭名卓著。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">如果我们所需要的只是要为每一个运行进程都快速计算一个各不相同的整数,那么这里已经有现实可用的方法:只要取在<B normal"><FONT face="Times New Roman">task</FONT></B>数组中进程的索引就可以了。平均说来,这肯定要比现在的<B normal"><FONT face="Times New Roman">get_pid</FONT></B>速度要快——毕竟,这无须遍历任务列表。不幸的是,很多现存的应用程序都假定在一个<FONT face="Times New Roman">PID</FONT>可以再重用之前都需要等待一段时间。这种假定在任何情况下都是不安全的,但是在如果为了这些程序的问题而将内核牵涉进去可能仍然是一个很糟糕的思想。现存的<FONT face="Times New Roman">PID</FONT>分配策略速度仍然很快,并且它偶尔还有可以暴露这些应用程序中的潜在缺陷的优点,如果有的话(如果你认为这是一种优点)。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">get_pid</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23613</FONT>:<B normal"><FONT face="Times New Roman">next_safe</FONT></B>变量是一个为加快系统运行速度而设定的变量;它保持记录了可能保留的次最低的的候选<FONT face="Times New Roman">PID</FONT>。(更正确的应该把它命名为<B normal"><FONT face="Times New Roman">next_unsafe</FONT></B>。)当<B normal"><FONT face="Times New Roman">last_pid</FONT></B>递增并超过这个范围时,系统应该检测整个任务列表来保证这个候选<FONT face="Times New Roman">PID</FONT>是否仍在被保留着(原来保留这个<FONT face="Times New Roman">PID</FONT>的进程现在可能已经运行完了)。由于遍历这个任务列表可能会很慢,所以只要可能就应该避免执行这样的操作。因此,在执行这个遍历的过程中,<B normal"><FONT face="Times New Roman">get_pid</FONT></B>要重新计算<B normal"><FONT face="Times New Roman">next_safe</FONT></B>——如果有些进程已经死掉了,这个数字可能现在更大了,因此<B normal"><FONT face="Times New Roman">get_pid</FONT></B>可以避免一些将来对任务列表的遍历。(<B normal"><FONT face="Times New Roman">next_safe</FONT></B>是静态的,因此其值在下次<B normal"><FONT face="Times New Roman">get_pid</FONT></B>需要分配<FONT face="Times New Roman">PID</FONT>时就会保留下来。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23616</FONT>:如果新的进程要和其祖先共享<FONT face="Times New Roman">PID</FONT>,就返回祖先进程的<FONT face="Times New Roman">PID</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23620</FONT>:开始搜寻候选<FONT face="Times New Roman">PID</FONT>寻找未使用的值。位与运算只是通过测试低<FONT face="Times New Roman">15</FONT>位是否置位来简单测试<B normal"><FONT face="Times New Roman">last_pid</FONT></B>的新值是否超过了<FONT face="Times New Roman">32,767</FONT>(最大允许的<FONT face="Times New Roman">PID</FONT>)。我怀疑这些内核开发者真正需要通过这样做来获得微小的速度优势,但是你永远也不会知道;至少在这段代码编写期间,<FONT face="Times New Roman">gcc</FONT>还不够敏锐到足以注意到它们的等价性并在生成的代码中选择稍微快速的形式。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23621</FONT>:如果<B normal"><FONT face="Times New Roman">last_pid</FONT></B>已经超出了允许的最大值,它就会滚动到<FONT face="Times New Roman">300</FONT>。<FONT face="Times New Roman">300</FONT>这个数字并没有什么魔力——它对于内核并没有特别的意义——这是另外一个加速变量。其思想是数字比较小的<FONT face="Times New Roman">PID</FONT>通常都属于系统开始运行时就已经创建的,从不会退出的长时间运行的后台监控程序。由于它们总是占据着数字比较小的<FONT face="Times New Roman">PID</FONT>,所以如果不考虑对前面几百个值的重用问题,我们将会发现寻找可以使用的<FONT face="Times New Roman">PID</FONT>的过程会快许多。而且,由于<FONT face="Times New Roman">PID</FONT>的空间是同时允许的任务数(<FONT face="Times New Roman">512</FONT>)的<FONT face="Times New Roman">64</FONT>倍,为了追求速度而损失一些空间是一种非常值得的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23622</FONT>:由于<B normal"><FONT face="Times New Roman">last_pid</FONT></B>超出了最大允许的<FONT face="Times New Roman">PID</FONT>,它必然也就超出了<B normal"><FONT face="Times New Roman">next_safe</FONT></B>;因此,后面的<B normal"><FONT face="Times New Roman">if</FONT></B>测试也可以跳过。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23624</FONT>:如果<B normal"><FONT face="Times New Roman">last_pid</FONT></B>仍然小于<B normal"><FONT face="Times New Roman">next_safe</FONT></B>,其值就可以再用。否则,必须检查任务列表。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23633</FONT>:如果取得了<B normal"><FONT face="Times New Roman">last_pid</FONT></B>的当前值,它就简单的递增,如果需要就跳转到<FONT face="Times New Roman">300</FONT>,重新开始循环。初次看的时候,仿佛这个循环会一直运行下去——如果所有的<FONT face="Times New Roman">PID</FONT>都已经被使用了会出现什么情况呢?但是稍微考虑一下,我们就可以排除这种可能性:任务列表的最大值和同时并发的任务的最大数是相同的,有效的<FONT face="Times New Roman">PID</FONT>数目要比这两个数都大得多。因此,循环最终会找到有效的<FONT face="Times New Roman">PID</FONT>;这仅仅是个时间的问题。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23651</FONT>:<B normal"><FONT face="Times New Roman">get_pid</FONT></B>已经发现了一个没有被使用的<FONT face="Times New Roman">PID</FONT>,随后返回该<FONT face="Times New Roman">PID</FONT>。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">运行新程序</H2><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">如果我们能够进行的所有工作只是<B normal"><FONT face="Times New Roman">fork</FONT></B>(或者<B normal"><FONT face="Times New Roman">__clone</FONT></B>),那么我们就只能一次次建立同一个进程的拷贝就可以了——这样我们的<FONT face="Times New Roman">Linux</FONT>系统就只能运行在系统中第一个创建的用户进程<FONT face="Times New Roman">init</FONT>了。<FONT face="Times New Roman">Init</FONT>是很有用的,但是还没有功能如此强大;我们也还需要处理其它事情。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">在我们创建新的进程以后,它通过调用<B normal"><FONT face="Times New Roman">exec</FONT></B>就能够变成独立于其它进程的进程了。(这实际上不止是一个名为<B normal"><FONT face="Times New Roman">exec</FONT></B>的函数;而是<B normal"><FONT face="Times New Roman">exec</FONT></B>通常用作一个引用一系列函数的通用术语,所有这些函数基本上都处理相同的事情,但是使用的参数稍微有些不同。)</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">因此,创建一个“真正”的新进程——与其祖先不同的程序运行镜像——任务分为两步,一步是<B normal"><FONT face="Times New Roman">fork</FONT></B>,另一步是<B normal"><FONT face="Times New Roman">exec</FONT></B>,最后能够得出下面的风格非常熟悉的<FONT face="Times New Roman">C</FONT>代码:</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1"><FONT face="Times New Roman">P485 1<p></p></FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt; mso-outline-level: 1">(<B normal"><FONT face="Times New Roman">execl</FONT></B>是<B normal"><FONT face="Times New Roman">exec</FONT></B>家族若干函数中的一个。)</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">实现所有<B normal"><FONT face="Times New Roman">exec</FONT></B>家族函数的底层内核函数是<FONT face="Times New Roman">10079</FONT>行到<FONT face="Times New Roman">10141</FONT>行的<B normal"><FONT face="Times New Roman">do_execve</FONT></B>。<B normal"><FONT face="Times New Roman">do_execve</FONT></B>处理三种工作:</P><P 0cm 0cm 0pt 42.5pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo5; tab-stops: list 42.5pt">l          把一些定义信息从文件读入内存。(<B normal"><FONT face="Times New Roman">do_execve</FONT></B>把这个工作交给<B normal"><FONT face="Times New Roman">prepare_binprm</FONT></B>处理。)</P><P 0cm 0cm 0pt 42.5pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo5; tab-stops: list 42.5pt">l          准备新的参数和环境——这是<FONT face="Times New Roman">C</FONT>应用程序将它作为<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>使用的内容。</P><P 0cm 0cm 0pt 42.5pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo5; tab-stops: list 42.5pt">l          装载可以解析可执行文件的二进制处理程序,并让它处理剩余的修改内核数据结构的工作。</P><P 0cm 0cm 0pt; TEXT-INDENT: 21.25pt">记住这些任务,现在让我们开始仔细研究一下<B normal"><FONT face="Times New Roman">do_execve</FONT></B>。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">do_execve</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10082</FONT>:代表在使用<B normal"><FONT face="Times New Roman">exec</FONT></B>处理进程时所需要记录的全部信息的数据类型是<B normal"><FONT face="Times New Roman">struct linux_binprm</FONT></B>结构(请参看<FONT face="Times New Roman">13786</FONT>行)——我确信<B normal"><FONT face="Times New Roman">binprm</FONT></B>是“<FONT face="Times New Roman">binary parameters</FONT>(二进制参数)”的缩写。<B normal"><FONT face="Times New Roman">do_execve</FONT></B>处理自己的工作,并使用这种类型的变量<B normal"><FONT face="Times New Roman">bprm</FONT></B>同那些负责处理其部分工作的函数进行通信。注意到当<B normal"><FONT face="Times New Roman">do_execve</FONT></B>返回时<B normal"><FONT face="Times New Roman">bprm</FONT></B>就会被废弃——只有在执行<B normal"><FONT face="Times New Roman">exec</FONT></B>时才需要<B normal"><FONT face="Times New Roman">bprm</FONT></B>,它并不在该进程的整个生命期中存在。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10087</FONT>:<B normal"><FONT face="Times New Roman">do_execve</FONT></B>通过初始化一个记录新进程参数和环境分配的内存页的微型页表开始执行。它为这个目的总共需要申请<B normal"><FONT face="Times New Roman">MAX_ARG_PAGES</FONT></B>(在<FONT face="Times New Roman">13780</FONT>行宏定义为<FONT face="Times New Roman">32</FONT>)个页,在<FONT face="Times New Roman">x86</FONT>平台上每一页是<FONT face="Times New Roman">4K</FONT>,因此参数总共可以使用的空间加起来就是<FONT face="Times New Roman">32*4K=128K</FONT>。作为我个人而言,我很高兴了解到这个内容,因为我偶而会超过这个限定,通常是在一个具有成百个文件的目录下运行<FONT face="Times New Roman"><B normal">cat</B>*<B normal">&gt;/tmp/joined</B></FONT>之类的东西的时候——所有这些文件名连接起来可能就超过了<FONT face="Times New Roman">128K</FONT>。我通常是使用<FONT face="Times New Roman">xargs</FONT>程序解决这个问题,但是我现在也可以通过为<B normal"><FONT face="Times New Roman">MAX_ARG_PAGES</FONT></B>重新定义一个比较大的值并重新编译内核来解决这个问题。至少现在如果这个问题再困扰我,我也知道该如何增加这一限制了。(可能一些热心的读者会重新编写程序来去掉这段糟糕的限制。)所以我非常喜欢拥有内核的源代码。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10091</FONT>:下一步是要打开可执行文件。这不是简单的从文件中读出数据——现在的焦点是要确保文件存在,这样<B normal"><FONT face="Times New Roman">do_execve</FONT></B>就可以清楚是否有必要继续进行处理。如果这是第一步,而不是首先填充<B normal"><FONT face="Times New Roman">bprm</FONT></B>的页表的话,<B normal"><FONT face="Times New Roman">do_execve</FONT></B>在执行时有时能够获得很高的边际效应——如果这样失败了,用来初始化页表的时间就浪费了。然而,这只在文件不存在时才有用——这不是普通的情况,不值得优化。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10096</FONT>:继续填充<B normal"><FONT face="Times New Roman">bprm</FONT></B>,特别是其<B normal"><FONT face="Times New Roman">argc</FONT></B>和<B normal"><FONT face="Times New Roman">envc</FONT></B>成员。为了填充这些成员,<B normal"><FONT face="Times New Roman">do_execve</FONT></B>使用<B normal"><FONT face="Times New Roman">count</FONT></B>函数(<FONT face="Times New Roman">9480</FONT>行),它通过使用被传递进来的<FONT face="Times New Roman"> <B normal">argv</B></FONT>和<B normal"><FONT face="Times New Roman">envp</FONT></B>数组计算非空指针的个数。第一个空指针标志着列表结束,因此在到达空指针时就可以得到非空指针的个数并将其返回。这开始看起来似乎很可能因此而造成一些效率的损失:调用<B normal"><FONT face="Times New Roman">do_execve</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">do_execve</FONT></B>增加两个整型参数<B normal"><FONT face="Times New Roman">argc</FONT></B>和<B normal"><FONT face="Times New Roman">envc</FONT></B>。如果这两个参数都是非负的,那么它们就可以分别代表两个数组的长度。但是事情并没有这么简单:<B normal"><FONT face="Times New Roman">count</FONT></B>同时要检测它扫描的数组中是否有访问内存的错误发生。强迫(更多的情况是完全信任)<B normal"><FONT face="Times New Roman">do_execve</FONT></B>的调用者来对这些内容进行检测是不正确的。所以目前这样的处理方式要更好一些。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10115</FONT>:主要使用<B normal"><FONT face="Times New Roman">copy_strings</FONT></B>(<FONT face="Times New Roman">9519</FONT>行)把参数和环境变量拷贝到新进程中。<B normal"><FONT face="Times New Roman">copy_strings</FONT></B>看起来很复杂,但是它要处理的工作十分简单:把字符串拷贝到新进程的内存空间中,如果需要就给它们分配页。这种复杂性的增长主要出现在对页表的管理需要和跨越内核<FONT face="Times New Roman">/</FONT>用户空间限制的需要,这一点将在第<FONT face="Times New Roman">8</FONT>章中更详细地介绍。</P>
作者: ilikenba    时间: 2005-3-4 22:13
< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10126</FONT>:如果前面的工作可以很好地执行到此处,最后一步是要为新的可执行程序寻找一个二进制处理程序。如果<B normal"><FONT face="Times New Roman">search_binary_handler</FONT></B>成功找到了这种程序,整个过程就成功运行结束,并返回一个非负值以说明成功。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10134</FONT>:如果程序运行到了此处,那么前面的几步中肯定发生了错误。系统释放为新进程的参数和环境分配的所有页,接着必须返回一个负值通知调用者调用过程失败了。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">prepare_binprm</H4>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9832</FONT>:<FONT face="Times New Roman"> <B normal">prepare_binprm</B></FONT>填写<B normal"><FONT face="Times New Roman">do_execve</FONT></B>的重要部分<B normal"><FONT face="Times New Roman">bprm</FONT></B>。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9839</FONT>:<FONT face="Times New Roman"> </FONT>本行开始一些健全性检测,例如要确保执行的是文件而不是目录,并且文件的可执行位已经设置了。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9858</FONT>:<FONT face="Times New Roman"> </FONT>如果已经被设置过<FONT face="Times New Roman">setuid</FONT>和<FONT face="Times New Roman">setuid</FONT>位,就根据它们的提示新进程应该把当前执行的用户作为一个不同的用户(如果<FONT face="Times New Roman">setuid</FONT>被置位)并且<FONT face="Times New Roman">/</FONT>或者把它作为一个不同组的成员(如果<FONT face="Times New Roman">setgid</FONT>被置位)。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9933</FONT>:<FONT face="Times New Roman"> </FONT>最后,<B normal"><FONT face="Times New Roman">prepare_binprm</FONT></B>从文件中读取前<FONT face="Times New Roman">128</FONT>个字节(而不是像该函数标题注释里说明的一样是前<FONT face="Times New Roman">512</FONT>个字节)到<B normal"><FONT face="Times New Roman">bprm</FONT></B>的<B normal"><FONT face="Times New Roman">buf</FONT></B>成员中。</P>< 0cm 0cm 0pt">顺便说一下,这里有一个延续已久的争论:在<FONT face="Times New Roman">13787</FONT>行,<B normal"><FONT face="Times New Roman">struct linux_binprm</FONT></B>结构的<FONT face="Times New Roman">buf</FONT>成员被声明为是<FONT face="Times New Roman">128</FONT>字节长,在<FONT face="Times New Roman">9933</FONT>行读入了<FONT face="Times New Roman">128</FONT>字节。但是字面上常量<FONT face="Times New Roman">128</FONT>用在两个地方——没有宏定义表示有必要保持两个数字的一致;因此,有可能会出现对其中一个进行改变而不改变相关的另一个的情况,这样就很可能摧毁系统。即使不从学术上考虑,这种忽略在保证效率的基础上是不能防止的——我不能想象出还有什么其它理由。</P>< 0cm 0cm 0pt">这是一个很好的对内核做点简短却有用的修改的机会:在每处这样使用<FONT face="Times New Roman">128</FONT>的地方都使用一个<B normal"><FONT face="Times New Roman">#define</FONT></B>语句(或者是使用类似于<B normal"><FONT face="Times New Roman">sizeof</FONT></B><B normal">(<FONT face="Times New Roman">bprm-&gt;buf</FONT></B><B normal">)</B>的语句)代替;存在几个其它实例,我会让你把它们都找到。如果你实验一下,你就会发现在这种情况下<B normal"><FONT face="Times New Roman">#define</FONT></B>为什么比<B normal"><FONT face="Times New Roman">sizeof</FONT></B>要好。(把这种重复出现的神奇数字加以定义和修正对于内核是更好的贡献。但是总体的修正工作要比看起来的困难,这只由于正确的对所有相关部分进行定位是很困难的;让我们一点一点地开始,最终会将其全部解决。)</P><H4 6pt 0cm; TEXT-INDENT: 0cm">search_binary_handler</H4>< 0cm 0cm 0pt">二进制处理程序是<FONT face="Times New Roman">Linux</FONT>内核统一处理各种二进制格式的机制,这是我们需要的,因为不是所有的文件都是以相同的文件格式存储的。一个很合适的例子是<FONT face="Times New Roman">Java</FONT>的<FONT face="Times New Roman">.class</FONT>文件。<FONT face="Times New Roman">Java</FONT>定义了一种平台无关的二进制可执行格式——无论它们是在什么平台上运行,它们的文件本身都是相同的——因此这些文件显然应该和<FONT face="Times New Roman">Linux</FONT>特有的可执行格式一样构建。通过使用适当的二进制处理程序,<FONT face="Times New Roman">Linux</FONT>可以把它们仿佛当作是自己特有的可执行文件一样处理。</P>< 0cm 0cm 0pt">后面我们会详细介绍二进制处理程序,但是现在你应该了解一些有关内容以便理解<B normal"><FONT face="Times New Roman">do_execve</FONT></B>是如何发现匹配的。它把这一工作交给<B normal"><FONT face="Times New Roman">search_binary_handler</FONT></B>(<FONT face="Times New Roman">9996</FONT>行)处理。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10037</FONT>:开始遍历处理内核的二进制处理程序链接列表,依次将<B normal"><FONT face="Times New Roman">bprm</FONT></B>传递给它们。(我们现在并不关心<B normal"><FONT face="Times New Roman">regs</FONT></B>参数。)更确切的说,二进制处理程序的链接列表的每一个元素都包含一组指向函数的指针,这些函数一起提供了对一种二进制格式的支持。(<FONT face="Times New Roman">13803</FONT>行定义的<B normal"><FONT face="Times New Roman">struct linux_binfmt</FONT></B>结构显示了其中包含的内容:我们感兴趣的部分是装载二进制的部分<B normal"><FONT face="Times New Roman">load_binary</FONT></B>;装载共享库的部分<B normal"><FONT face="Times New Roman">load_shlib</FONT></B>;创建内核转储映象的部分<B normal"><FONT face="Times New Roman">core_dump</FONT></B>。)<B normal"><FONT face="Times New Roman">search_binary_handler</FONT></B>简单调用每一个<B normal"><FONT face="Times New Roman">load_binary</FONT></B>函数,知道其中一个返回非负值指明它成功识别并装载了文件。<B normal"><FONT face="Times New Roman">search_binary_handler</FONT></B>返回负值指明发生的错误,其中包括不能找到匹配的二进制处理程序的错误。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10070</FONT>:如果<FONT face="Times New Roman">10037</FONT>行开始的循环不能找到匹配的二进制处理程序,本行就试图装载新的二进制格式,它会引起第二次尝试,并应该取得成功。因此整个操作被包含在从<FONT face="Times New Roman">10036</FONT>行开始的两次执行的循环中。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">可执行格式</H2>< 0cm 0cm 0pt">正如前面一节中说明的一样,不是所有程序都使用相同的文件格式存储,<FONT face="Times New Roman">Linux</FONT>使用二进制处理程序把它们之间的区别掩盖掉了。</P>< 0cm 0cm 0pt"><FONT face="Times New Roman">Linux</FONT>当前“本地的”可执行格式(如果“本地”在系统中可以给各种格式提供良好支持)是可执行链接格式(<FONT face="Times New Roman">ELF</FONT>)。<FONT face="Times New Roman">ELF</FONT>只是全部替换了原来的称为<FONT face="Times New Roman">a.out</FONT>的格式,替换之前的格式很难说是灵活的——除了有一些其它缺点以外,<FONT face="Times New Roman">a.out</FONT>还很难适用于动态链接,这会使得共享库难于实现。<FONT face="Times New Roman">Linux</FONT>仍然为<FONT face="Times New Roman">a.out</FONT>保留了一个二进制处理程序,但通常是使用<FONT face="Times New Roman">ELF</FONT>。</P>< 0cm 0cm 0pt">二进制处理程序通过某种内嵌在文件开头的“<FONT face="Times New Roman">magic</FONT>序列”(一个特殊字节序列)来识别文件,有时也会通过文件名的一些特性。例如,你会看到的<FONT face="Times New Roman">Java</FONT>处理程序可以保证文件名以<FONT face="Times New Roman">.class</FONT>结尾并且前四个字节是(以十六进制)<FONT face="Times New Roman">0xcafebabe</FONT>,这是<FONT face="Times New Roman">Java</FONT>标准所定义的。</P>< 0cm 0cm 0pt">下面是<FONT face="Times New Roman">2.2</FONT>版本内核所提供的二进制处理程序(这是在我的<FONT face="Times New Roman">Intel</FONT>系统中的;<FONT face="Times New Roman">Linux</FONT>的其它平台的移植移植版本,例如<FONT face="Times New Roman">owerPC</FONT>和<FONT face="Times New Roman">SPARC</FONT>上,需要使用其它的处理程序):</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.25pt">l         <FONT face="Times New Roman">a.out</FONT>(在文件<FONT face="Times New Roman">fs/binfmt_aout.c</FONT>中)——这是为了支持原来风格的<FONT face="Times New Roman">Linux</FONT>二进制文件。这仍然是为了满足一些系统的向后兼容的需要,但是基本上<FONT face="Times New Roman">a.out</FONT>很快就会光荣退役了。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.25pt">l         <FONT face="Times New Roman">EFL</FONT>(在文件<FONT face="Times New Roman">fs/binfmt_elf.c</FONT>中)——只是为了支持现在新风格的<FONT face="Times New Roman">Linux</FONT>二进制文件。这在可执行文件和共享库中都广泛使用。最新的<FONT face="Times New Roman">Linux</FONT>系统(例如<FONT face="Times New Roman">Red Hat 5.2</FONT>)一般只预装了<FONT face="Times New Roman">ELF</FONT>二进制文件,但是特殊情况下如果你决定装载<FONT face="Times New Roman">a.out</FONT>二进制文件,那么系统也可以对它提供支持。注意即使<FONT face="Times New Roman">ELF</FONT>被作为惯用的<FONT face="Times New Roman">Linux</FONT>本地格式,也要和其它格式一样使用二进制处理程序——内核并没有特殊的偏好。避免特殊情况的惯例能够简化内核代码。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.25pt">l         <FONT face="Times New Roman">EM86</FONT>(在文件<FONT face="Times New Roman">fs/binfmt_em86.c</FONT>中)——帮你在<FONT face="Times New Roman">Alpha</FONT>机器上运行<FONT face="Times New Roman">Intel</FONT>的<FONT face="Times New Roman">Linux</FONT>二进制文件,仿佛它们就是<FONT face="Times New Roman">Alpha</FONT>的本地二进制文件。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.25pt">l         <FONT face="Times New Roman">Java</FONT>(在文件<FONT face="Times New Roman">fs/binfmt_java.c</FONT>中)——使你可以不必每次都麻烦地定义<FONT face="Times New Roman">Java</FONT>字节码的解释程序就可以执行<FONT face="Times New Roman">Java</FONT>的<FONT face="Times New Roman">.class</FONT>文件。这种机制和脚本中使用的机制类似;通过把<FONT face="Times New Roman">.class</FONT>文件的文件名作为参数传递,处理程序返回来为你整型字节码处理程序。从用户的观点来看,<FONT face="Times New Roman">Java</FONT>二进制文件是作为本地可执行文件处理的。在本章的后面内容中我们会详细介绍这个处理程序。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.25pt">l         <FONT face="Times New Roman">Misc</FONT>(在文件<FONT face="Times New Roman">fs/binfmt_misc.c</FONT>中)——这是最明智地使用二进制处理程序的方法,这个处理程序通过内嵌的特征数字或者文件名后缀可以识别出各种二进制格式——但是其最优秀的特性是它在运行期可以配置,而不是只能在编译器可以配置。因此,遵守这些限制,你就可以快速的增加对新二进制文件的支持,而不用重新编译内核,也无须重新启动机器。(这实在太棒了!)源程序文件中的注释建议最终使用它来取代<FONT face="Times New Roman">Java</FONT>和<FONT face="Times New Roman">M86</FONT>二进制处理程序。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 42.25pt">l         脚本(在文件<FONT face="Times New Roman">fs/binfmt_script.c</FONT>中)——对于<FONT face="Times New Roman">shell</FONT>脚本,<FONT face="Times New Roman">erl</FONT>脚本等提供支持。宽松一点地说,所有前面两个字符是<FONT face="Times New Roman">#!</FONT>的可执行文件都规由这个二进制处理程序进行处理。</P>< 0cm 0cm 0pt">在上面这些二进制处理程序中,本书中只对<FONT face="Times New Roman">Java</FONT>和<FONT face="Times New Roman">ELF</FONT>处理程序进行了说明(分别从<FONT face="Times New Roman">9083</FONT>行和<FONT face="Times New Roman">7656</FONT>行开始),因为作为我们关心的基本内容,我们更关心内核如何处理各种不同格式间的区别,而不是每一种单个二进制处理程序的细节(虽然它自己也是一个很有趣的主题)。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">一个例子:Java二进制处理程序</H2>< 0cm 0cm 0pt">如同前面你看到的一样,<B normal"><FONT face="Times New Roman">do_execve</FONT></B>遍历一个代表二进制处理程序的<B normal"><FONT face="Times New Roman">struct linux_binfmt</FONT></B>结构的链接列表,调用每个结构的<B normal"><FONT face="Times New Roman">load_binary</FONT></B>成员指向的函数直到其中一个成功(当然也或者到已经试验完了所有的格式为止)。但是这些结构又从何而来呢?函数<B normal"><FONT face="Times New Roman">load_binary</FONT></B>是如何实现的?为了寻找这些答案,让我们来看一下<FONT face="Times New Roman">fs/binfmt_java.c</FONT>文件。</P>< 0cm 0cm 0pt">这个模块处理一些不是涉及在<FONT face="Times New Roman">Web</FONT>浏览器上使用<B normal"><FONT face="Times New Roman">java_format</FONT></B>(<FONT face="Times New Roman">9236</FONT>行)执行的<FONT face="Times New Roman">Java</FONT>程序的<FONT face="Times New Roman">Java</FONT>二进制文件和相关的函数。它使用<B normal"><FONT face="Times New Roman">applet_format</FONT></B>(<FONT face="Times New Roman">9254</FONT>行)及相关函数处理<FONT face="Times New Roman">Java</FONT>小程序(<FONT face="Times New Roman">Applet</FONT>)。在本节剩余部分的内容中,我们会集中看一下对于非<FONT face="Times New Roman">Java</FONT>小程序的支持;对于<FONT face="Times New Roman">Java</FONT>小程序的支持实际上是相同的。</P>< 0cm 0cm 0pt">如果重写<FONT face="Times New Roman">fs/binfmt_java.c</FONT>中的函数用来加强<FONT face="Times New Roman">Java</FONT>小程序函数和非<FONT face="Times New Roman">Java</FONT>小程序函数之间的相同代码的数量就更好了。虽然它注定最终要被“<FONT face="Times New Roman">misc</FONT>”二进制处理程序取代,但是现在还只是在讨论,尚未实行。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">do_load_java</H4>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9108</FONT>:<FONT face="Times New Roman"> </FONT>这是实际处理装载<FONT face="Times New Roman">Java</FONT>的<FONT face="Times New Roman">.class</FONT>文件工作的函数。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9117</FONT>:<FONT face="Times New Roman"> </FONT>通过检测特征数字<FONT face="Times New Roman">0xcafebabe</FONT>开始,这是因为<FONT face="Times New Roman">Java</FONT>标准规定所有有效的类文件都使用这个字符序列开始。接着开始执行健全性检测,一直到<FONT face="Times New Roman">9147</FONT>行,确保没有递归调用而且正在请求执行的可执行文件是以<FONT face="Times New Roman">.class</FONT>结尾的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9148</FONT>:<FONT face="Times New Roman"> </FONT>此处,所有的健全性检测已经通过了。现在,<B normal"><FONT face="Times New Roman">do_load_java</FONT></B>取得文件的基本名字,将其和<FONT face="Times New Roman">Java</FONT>字节码解释程序一起放置到程序空间中,并试图执行<FONT face="Times New Roman">Java</FONT>字节码解释程序。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9165</FONT>:<FONT face="Times New Roman"> </FONT>使用我们在<B normal"><FONT face="Times New Roman">do_execve</FONT></B>中见到的同一个进程执行解释程序。特殊情况下,就像查询<B normal"><FONT face="Times New Roman">do_load_java</FONT></B>的方法一样,使用<B normal"><FONT face="Times New Roman">search_binary_handler</FONT></B>为解释程序查询二进制处理程序。(实际上,虽然它不一定非要是<FONT face="Times New Roman">ELF</FONT>二进制文件,但是它也可能是。)</P><P 0cm 0cm 0pt">记住其它处理程序不会分配新的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构——我们在使用<B normal"><FONT face="Times New Roman">fork</FONT></B>的时候也碰到了这个问题。其它处理程序只是修改现存进程的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构。如果你希望细致地了解这是如何实现的,你的入手点应该是<B normal"><FONT face="Times New Roman">do_load_elf_binary</FONT></B>(<FONT face="Times New Roman">8072</FONT>行)——我们关心的部分从<FONT face="Times New Roman">8273</FONT>行开始。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">load_java</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9226</FONT>:<B normal"><FONT face="Times New Roman">load_java</FONT></B>是其它外部对象装载<FONT face="Times New Roman">.class</FONT>文件时所使用的函数。它首先递增内核模块使用的计数(如果作为内核模块编译),随后又将其递减,但是实际的工作是由<B normal"><FONT face="Times New Roman">do_load_java</FONT></B>(<FONT face="Times New Roman">9108</FONT>行)处理的。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">java_format</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9236</FONT>:<FONT face="Times New Roman"> </FONT>通过比较<B normal"><FONT face="Times New Roman">java_format</FONT></B>的初始化和<B normal"><FONT face="Times New Roman">struct linux_binfmt</FONT></B>结构(<FONT face="Times New Roman">13803</FONT>行)的定义,你可以看出这个模块没有提供对共享库和内核卸载的支持,只提供了对装载可执行程序的支持;而且这种支持是通过<B normal"><FONT face="Times New Roman">load_java</FONT></B>函数实现的。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">init_java_binfmt</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9262</FONT>:<FONT face="Times New Roman"> </FONT>指向这个模块的项是<B normal"><FONT face="Times New Roman">init_java_binfmt</FONT></B>,它把两个静态<B normal"><FONT face="Times New Roman">struct linux_binfmt</FONT></B>结构<B normal"><FONT face="Times New Roman">java_format</FONT></B>和<B normal"><FONT face="Times New Roman">applet_format</FONT></B>的地址压入系统列表中。如果对<FONT face="Times New Roman">Java</FONT>二进制文件的支持被编译进了内核,就在<FONT face="Times New Roman">9355</FONT>行调用<B normal"><FONT face="Times New Roman">init_java_binfmt</FONT></B>,或者如果<FONT face="Times New Roman">Java</FONT>二进制文件的支持被作为一个内核模块编译进了内核,就使用<FONT face="Times New Roman">kmod</FONT>任务。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">调度:了解它们是如何运行的!</H2><P 0cm 0cm 0pt">在应用程序被装载以后,必须获得对<FONT face="Times New Roman">CPU</FONT>的访问。这是调度程序涉及的领域。操作系统调度程序基本上划分为两类:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo3; tab-stops: list 42.25pt">l         复杂调度程序——运行需要花费相当长的时间,但是希望可以全面提高系统性能。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo3; tab-stops: list 42.25pt">l         快餐式(<FONT face="Times New Roman">quick-and-dirty</FONT>)调度程序——只是试图处理一些尽量简单的合理的工作就退出,从而进程本身将可以尽可能多的获得<FONT face="Times New Roman">CPU</FONT>。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">Linux</FONT>调度程序是后面一种情况。不要把“<FONT face="Times New Roman">quick-and-dirty</FONT>”解释成贬义的词,虽然实际的情况是:<FONT face="Times New Roman">Linux</FONT>的调度程序在商业和自由领域中都从根本上痛击了其竞争者。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">调度函数和调度策略</H2><P 0cm 0cm 0pt">内核主要的调度函数经过仔细挑选使用<B normal"><FONT face="Times New Roman">schedule</FONT></B>这个名字,该函数从<FONT face="Times New Roman">26686</FONT>行开始。这实际上是个很简单的函数,比它看起来还要简单,虽然由于它把三种调度策略合成了一种而其意义显得有些不是很明显。而且对于<FONT face="Times New Roman">SMP</FONT>的支持也增加了一定的复杂性,这一点将在第<FONT face="Times New Roman">10</FONT>章中详细讨论。</P><P 0cm 0cm 0pt">通常情况下使用的调度策略和进程有关。给定进程使用的调度算法称为调度策略,这在进程的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构的<B normal"><FONT face="Times New Roman">policy</FONT></B>成员中有所反映。普通情况下,<B normal"><FONT face="Times New Roman">policy</FONT></B>是<B normal"><FONT face="Times New Roman">SCHED_OTHER</FONT></B><B normal">、<FONT face="Times New Roman">SCHED_FIFO</FONT></B>,或者<B normal"><FONT face="Times New Roman">SCHED_RR</FONT></B>其中一个的位集。但是它也可能含有<B normal"><FONT face="Times New Roman">SCHED_YIED</FONT></B>位集,如果进程决定交出<FONT face="Times New Roman">CPU</FONT>——例如,通过调用<B normal"><FONT face="Times New Roman">sched_yield</FONT></B>系统调用(请参看<B normal"><FONT face="Times New Roman">sched_yield</FONT></B>,<FONT face="Times New Roman">27757</FONT>行)。</P><P 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">SCHED_XXX</FONT></B>常量在<FONT face="Times New Roman">16196</FONT>行到<FONT face="Times New Roman">16202</FONT>行宏定义。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">16196</FONT>:<B normal"><FONT face="Times New Roman">SCHED_OTHER</FONT></B>意味着传统<FONT face="Times New Roman">Unix</FONT>调度是使用它的——这不是一个实时进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16197</FONT>:<B normal"><FONT face="Times New Roman">SCHED_FIFO</FONT></B>意味着这是一个实时进程,这要遵守<FONT face="Times New Roman">POSIX.1b</FONT>标准的<FONT face="Times New Roman">FIFO</FONT>(先进先出)调度程序。它会一直运行,直到有一个进程在<FONT face="Times New Roman">I/O</FONT>阻塞,因而明确释放<FONT face="Times New Roman">CPU</FONT>,或者是<FONT face="Times New Roman">CPU</FONT>被另一个具有更高<B normal"><FONT face="Times New Roman">rt_priority</FONT></B>的实时进程抢占了。在<FONT face="Times New Roman">Linux</FONT>实现中,<B normal"><FONT face="Times New Roman">SCHED_FIFO</FONT></B>进程拥有时间片——只有当时间片结束时它们才被迫释放<FONT face="Times New Roman">CPU</FONT>。因此,如同<FONT face="Times New Roman">POSIX.1b</FONT>中规定一样,这样的进程就像没有时间片一样运行。因此进程要保持对其时间片进行记录的这一事实主要是为了实现的方便,因此我们就不必使用<B normal"><FONT face="Times New Roman">if(!(current-&gt;policy &amp; SCHED_FIFO)) { … }</FONT></B>来弄乱这些代码。还有,这样处理速度可能会快一些——其它实际可行的策略都需要记录时间片,并持续检测是否我们需要记录时间片会比简单的跟踪它速度更慢。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16198</FONT>:<B normal"><FONT face="Times New Roman">SCHED_RR</FONT></B>意味着这是一个实时进程,要遵守<FONT face="Times New Roman">POSIX.1b</FONT>的<FONT face="Times New Roman">RR</FONT>(循环:<FONT face="Times New Roman">round-robin</FONT>)调度规则。除了时间片有些不同之外,这和<B normal"><FONT face="Times New Roman">SCHED_FIFO</FONT></B>类似。当<B normal"><FONT face="Times New Roman">SCHED_RR</FONT></B>进程的时间片用完后,就使用相同的<B normal"><FONT face="Times New Roman">rt_priority</FONT></B>跳转到<B normal"><FONT face="Times New Roman">SCHED_FIFO</FONT></B>和<B normal"><FONT face="Times New Roman">SCHED_RR</FONT></B>列表的最后。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">16202</FONT>:<B normal"><FONT face="Times New Roman">SCHED_YIELD</FONT></B>并不是一种调度策略,而是截取调度策略的一个附加位。如同前面说明的一样,如果有其它进程需要<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">schedule</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26689</FONT>:<B normal"><FONT face="Times New Roman">prev</FONT></B>和<B normal"><FONT face="Times New Roman">next</FONT></B>会被设置为<B normal"><FONT face="Times New Roman">schedule</FONT></B>最感兴趣的两个进程:其中一个是在调用<B normal"><FONT face="Times New Roman">schedule</FONT></B>时正在运行的进程(<B normal"><FONT face="Times New Roman">prev</FONT></B>),另外一个应该是接着就给予<FONT face="Times New Roman">CPU</FONT>的进程(<B normal"><FONT face="Times New Roman">next</FONT></B>)。记住<B normal"><FONT face="Times New Roman">prev</FONT></B>和<B normal"><FONT face="Times New Roman">next</FONT></B>可能是相同的——<B normal"><FONT face="Times New Roman">schedule</FONT></B>可以重新调度已经获得<FONT face="Times New Roman">CPU</FONT>的进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26706</FONT>:如同第<FONT face="Times New Roman">6</FONT>章中介绍的一样,这就是中断处理程序的“下半部分”运行的地方。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26715</FONT>:内核实时系统部分的实现,循环调度程序(<B normal"><FONT face="Times New Roman">SCHED_RR</FONT></B>)通过移动“耗尽的”<FONT face="Times New Roman">RR</FONT>进程——已经用完其时间片的进程——到队列末尾,这样具有相同优先级的其它<FONT face="Times New Roman">RR</FONT>进程就可以获得时间片了。同时这补充了耗尽进程的时间片。重要的是它并不是为<B normal"><FONT face="Times New Roman">SCHED_FIFO</FONT></B>这样处理的,这样和预计的一样,后面的进程在其时间片偶然用完时就无须释放<FONT face="Times New Roman">CPU</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26720</FONT>:由于代码的其它部分已经决定了进程必须被移进或移出<B normal"><FONT face="Times New Roman">TASK_RUNNING</FONT></B>状态,所以会经常使用<B normal"><FONT face="Times New Roman">schedule</FONT></B>——例如,如果进程正在等待的硬件条件已经发生了——所以如果必要,这个<B normal"><FONT face="Times New Roman">switch</FONT></B>会改变进程的状态。如果进程已经处于<B normal"><FONT face="Times New Roman">TASK_RUNNING</FONT></B>状态,它就无须处理了。如果它是可以中断的(等待信号量)并且信号量到达了进程,就返回<B normal"><FONT face="Times New Roman">TASK_RUNNING</FONT></B>状态。在所有其它情况下(例如,进程已经处于<B normal"><FONT face="Times New Roman">TASK_UNINTERUPTIBLE</FONT></B>状态了),应该从运行队列中将进程移走。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26735</FONT>:将<B normal"><FONT face="Times New Roman">p</FONT></B>初始化为运行队列中的第一个任务;<B normal"><FONT face="Times New Roman">p</FONT></B>会遍历队列中的所有任务。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26736</FONT>:<FONT face="Times New Roman">c</FONT>记录了运行队列中所有进程的最好“<FONT face="Times New Roman">goodness</FONT>”——具有最好“<FONT face="Times New Roman">goodness</FONT>”的进程是最易获得<FONT face="Times New Roman">CPU</FONT>的进程。(我们很快就会讨论<B normal"><FONT face="Times New Roman">goodness</FONT></B>。)<FONT face="Times New Roman">goodness</FONT>值越高越好,一个进程的<FONT face="Times New Roman">goodness</FONT>值永远不会为负——这是<FONT face="Times New Roman">Unix</FONT>用户经常见到的一种奇异情况,其中较高的优先级(通常称为较高“<FONT face="Times New Roman">niceness</FONT>”级)意味着进程会较少地获得<FONT face="Times New Roman">CPU</FONT>时间。(至少这在内核中是有意义的。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26757</FONT>:开始遍历执行任务列表,跟踪具有最好<FONT face="Times New Roman">goodness</FONT>的进程。注意只有在当前记录被破坏而不是当它简单地被约束时它才会改变最好进程的概念。因此,出于对队列中第一个进程的原因,这种约束就会被打破了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26758</FONT>:这个循环中只考虑了唯一一个可以调度的进程。<B normal"><FONT face="Times New Roman">can_schedule</FONT></B>宏的<FONT face="Times New Roman">SMP</FONT>版本在<FONT face="Times New Roman">26568</FONT>行定义;其定义使<FONT face="Times New Roman">SMP</FONT>内核只有任务尚未在<FONT face="Times New Roman">CPU</FONT>上运行才会把调度作为该<FONT face="Times New Roman">CPU</FONT>上的一个任务。(这样具有完美的意义——在几乎不必要的任务中造成混淆完全是一种浪费。)<FONT face="Times New Roman">UP</FONT>版本在<FONT face="Times New Roman">26573</FONT>行,它总是真值——换而言之,在<FONT face="Times New Roman">UP</FONT>的情况下,运行队列中的每一个进程都需要竞争<FONT face="Times New Roman">CPU</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26767</FONT>:值为<FONT face="Times New Roman">0</FONT>的<FONT face="Times New Roman">goodness</FONT>意味着进程已经用完它的时间片或者它已经明确说明要释放<FONT face="Times New Roman">CPU</FONT>。如果所有运行队列中的所有进程都具有<FONT face="Times New Roman">0</FONT>值的<FONT face="Times New Roman">goodness</FONT>,在循环结束后<B normal"><FONT face="Times New Roman">c</FONT></B>的值就是<FONT face="Times New Roman">0</FONT>。在这种情况下,<B normal"><FONT face="Times New Roman">schedule</FONT></B>要重新计算进程计数器;新计数器的值是原来值的一半加上进程的静态优先级——由于除非进程已经释放<FONT face="Times New Roman">CPU</FONT>,否则原来计数器的值都是<FONT face="Times New Roman">0</FONT>,<B normal"><FONT face="Times New Roman">schedule</FONT></B>通常只是把计数器重新初始化为静态优先级。(中断处理程序和由另外一个处理器引起的分支在<B normal"><FONT face="Times New Roman">schedule</FONT></B>搜寻<FONT face="Times New Roman">goodness</FONT>最大值时都将增加此循环中的计数器,因此由于这个原因计数器可能不会为<FONT face="Times New Roman">0</FONT>。虽然这有些罕见。)调度程序不必麻烦地重新计算现在哪一个进程具有最高的<FONT face="Times New Roman">goodness</FONT>值;它只是调度前面循环中遇到的第一个进程。此时,这个进程是它发现的第一个具有次高<FONT face="Times New Roman">goodness</FONT>值(<FONT face="Times New Roman">0</FONT>)的进程,因此<B normal"><FONT face="Times New Roman">schedule</FONT></B>就能够计算出自己现在和以后所应该运行的任务。(记住,这就是“<FONT face="Times New Roman">quick-and-dirty</FONT>”的思想。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26801</FONT>:如果<B normal"><FONT face="Times New Roman">schedule</FONT></B>已经选择了一个不同于前面正在运行的进程来调度,那么它就必须挂起原来的进程并允许新的进程运行。这是通过后面我们将介绍的<B normal"><FONT face="Times New Roman">switch_to</FONT></B>处理的。<B normal"><FONT face="Times New Roman">switch_to</FONT></B>的一个重要结果对于应用程序开发者来说可能显得有些奇怪:对于<B normal"><FONT face="Times New Roman">schedule</FONT></B>的调用并不返回。也就是它不是立即返回的;在系统条件判断语句返回到当前任务时调用就会返回。作为一个特殊情况,当任务退出而调用<B normal"><FONT face="Times New Roman">schedule</FONT></B>时,对于<B normal"><FONT face="Times New Roman">schedule</FONT></B>的调用从不会返回——因为内核不会返回已经退出的任务。还有另外一种特殊情况,如果<B normal"><FONT face="Times New Roman">schedule</FONT></B>不会调度其它进程——也就是说,如果在<B normal"><FONT face="Times New Roman">schedule</FONT></B>结束时<B normal"><FONT face="Times New Roman">next</FONT></B>和<B normal"><FONT face="Times New Roman">prev</FONT></B>是相同的——那么上下文中的跳转不会执行,<B normal"><FONT face="Times New Roman">schedule</FONT></B>实际上不会立即返回。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26809</FONT>:<B normal"><FONT face="Times New Roman">schedule</FONT></B>末尾的<B normal"><FONT face="Times New Roman">__schedule_tail</FONT></B>和<B normal"><FONT face="Times New Roman">reacquire_kernel_lock</FONT></B>函数在<FONT face="Times New Roman">UP</FONT>平台上不执行任何操作,因此现在我们就已经看完了调度程序的内核。顺便说一下,为了确保你已经正确的理解了这些代码,自己证明下面的性质:如果运行队列为空,那么下面就会调用<FONT face="Times New Roman">idle</FONT>任务。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">switch_to</H4><P 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">switch_to</FONT></B>处理从一个进程到下一个进程的跳转,称为上下文跳转(<FONT face="Times New Roman">context-switching</FONT>);这是在不同处理器上会不同处理之间进行的低级特性。有趣的是,在<FONT face="Times New Roman">x86</FONT>平台上内核开发人员使用软件处理大多数的上下文跳转,这样就忽略了一些硬件的支持。这种机制背后的原因在<B normal"><FONT face="Times New Roman">__switch_to</FONT></B>函数(<FONT face="Times New Roman">2638</FONT>行)上面的标题注释中有所说明,这个函数和<B normal"><FONT face="Times New Roman">switch_to</FONT></B>宏(<FONT face="Times New Roman">12939</FONT>行)一起处理上下文跳转。</P><P 0cm 0cm 0pt">由于很多上下文跳转要依赖于对内核处理内存方式的正确理解,这在下一章中才会详细介绍,本章只是稍微涉及一点。上下文跳转背后的基本思想是记忆当前位置和将要到达的位置——这是我们必须保存的当前上下文——接着跳转到另外一个前面已经存储过了的上下文。通过使用一部分汇编代码,<B normal"><FONT face="Times New Roman">switch_to</FONT></B>宏保存了后面将要介绍的上下文的两个重要的部分。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12945</FONT>:首先,<B normal"><FONT face="Times New Roman">switch_to</FONT></B>宏保存<FONT face="Times New Roman">ESP</FONT>寄存器的内容,它指向进程的当前堆栈。堆栈在下一章中将深入介绍;现在你只需要简单了解堆栈中保存的局部变量和函数调用信息。<B normal"><FONT face="Times New Roman">switch_to</FONT></B>宏也保存<FONT face="Times New Roman">EIP</FONT>寄存器的内容,这是进程的当前指令指针——如果允许继续运行时所执行的为下一条指令的地址。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12948</FONT>:把<B normal"><FONT face="Times New Roman">next-&gt;tss.eip</FONT></B>——保存指令的指针——压入返回堆栈,记录当后面紧跟的跳转到<B normal"><FONT face="Times New Roman">__switch_to</FONT></B>的<B normal"><FONT face="Times New Roman">jmp</FONT></B>返回时的返回地址。这样做的最终结果是当<B normal"><FONT face="Times New Roman">__switch_to</FONT></B>返回时,我们又回到了新的进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12949</FONT>:调用<B normal"><FONT face="Times New Roman">__switch_to</FONT></B>(<FONT face="Times New Roman">2638</FONT>行),它完成段寄存器和页表的保存和恢复工作。在你阅读完第<FONT face="Times New Roman">8</FONT>章以后这些特征数字就更有意义了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12955</FONT>:<B normal"><FONT face="Times New Roman">tss</FONT></B>代表<I normal"><FONT face="Times New Roman">task-state</FONT></I>段,这是<FONT face="Times New Roman">Intel</FONT>使用的支持硬件上下文跳转的<FONT face="Times New Roman">CPU</FONT>特性的术语。虽然内核代码使用软件实现上下文跳转,但是开发人员仍然会使用<FONT face="Times New Roman">TSS</FONT>来记录进程的状态。<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构的<B normal"><FONT face="Times New Roman">tss</FONT></B>成员的类型是<B normal"><FONT face="Times New Roman">struct thread_struct</FONT></B>结构,本书中为了节省空间,忽略了它的定义。其成员仅仅对应于<FONT face="Times New Roman">x86</FONT>的<FONT face="Times New Roman">TSS</FONT>——成员是为<FONT face="Times New Roman">EIP</FONT>和<FONT face="Times New Roman">ESP</FONT>而存在的,如此而已。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">计算goodness值</H2><P 0cm 0cm 0pt">进程的<FONT face="Times New Roman">goodness</FONT>值通过<B normal"><FONT face="Times New Roman">goodness</FONT></B>函数(<FONT face="Times New Roman">26388</FONT>行)计算。<B normal"><FONT face="Times New Roman">goodness</FONT></B>返回下面两类中的一个值:<FONT face="Times New Roman">1,000</FONT>以下或者<FONT face="Times New Roman">1,000</FONT>以上。<FONT face="Times New Roman">1,000</FONT>和<FONT face="Times New Roman">1,000</FONT>以上的值只能赋给“实时”进程,从<FONT face="Times New Roman">0</FONT>到<FONT face="Times New Roman">999</FONT>的值只能赋给“普通”进程。实际上普通进程的<FONT face="Times New Roman">goodness</FONT>值只使用了这个范围底部的一部分,从<FONT face="Times New Roman">0</FONT>到<FONT face="Times New Roman">41</FONT>(或者对于<FONT face="Times New Roman">SMP</FONT>来说是<FONT face="Times New Roman">0</FONT>到<FONT face="Times New Roman">56</FONT>,因为<FONT face="Times New Roman">SMP</FONT>模式会优先照顾等待同一个处理器的进程)。无论是在<FONT face="Times New Roman">SMP</FONT>还是在<FONT face="Times New Roman">UP</FONT>上,实时进程的<FONT face="Times New Roman">goodness</FONT>值的范围都是从<FONT face="Times New Roman">1,001</FONT>到<FONT face="Times New Roman">1,099</FONT>。</P><P 0cm 0cm 0pt">有关这两类<FONT face="Times New Roman">goodness</FONT>结果的重要的一点是该值在实时系统中的范围肯定会比非实时系统的范围要高(因此偏移量(<FONT face="Times New Roman">offset</FONT>)是<FONT face="Times New Roman">100</FONT>而不是<FONT face="Times New Roman">1000</FONT>)。<FONT face="Times New Roman">POSIX..1b</FONT>规定内核要确保在实时进程和非实时进程同时竞争<FONT face="Times New Roman">CPU</FONT>时,实时进程要优先于非实时进程。由于调度程序总是选择具有最大<FONT face="Times New Roman">goodness</FONT>值的进程,又由于任何尚未释放<FONT face="Times New Roman">CPU</FONT>的实时进程的<FONT face="Times New Roman">goodness</FONT>值总是比非实时进程的<FONT face="Times New Roman">goodness</FONT>大,<FONT face="Times New Roman">Linux</FONT>对这一点的遵守是很容易得到证明的。</P><P 0cm 0cm 0pt">尽管在<B normal"><FONT face="Times New Roman">goodness</FONT></B>上面的标题注释中有所说明,该函数还是从不会返回<FONT face="Times New Roman">-1,000</FONT>的,也不会返回其它的负值。由于<FONT face="Times New Roman">idle</FONT>进程的<B normal"><FONT face="Times New Roman">counter</FONT></B>值为负,所以如果使用<FONT face="Times New Roman">idle</FONT>进程作为参数调用<B normal"><FONT face="Times New Roman">goodness</FONT></B>,就会返回负值,但这是不会发生的。</P><P 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">goodness</FONT></B>只是一个简单的函数,但是它是<FONT face="Times New Roman">Linux</FONT>调度程序必不可少的部分。运行对立中的每个进程每次执行<B normal"><FONT face="Times New Roman">schedule</FONT></B>时都可能调用它,因此其执行速度必须很快。但是如果一旦它调度失误,那么整个系统都要遭殃了。考虑到这些冲突压力,我想改进现有的系统是相当困难的。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">goodness</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26394</FONT>:如果进程已经释放了<FONT face="Times New Roman">CPU</FONT>,就返回<FONT face="Times New Roman">0</FONT>(在清除<B normal"><FONT face="Times New Roman">SCHED_YIELD</FONT></B>位之后,这是因为进程只可能有一次想释放<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">26402</FONT>:如果这是一个实时进程,<B normal"><FONT face="Times New Roman">goodness</FONT></B>返回的值就属于数值较高的一类;这要精确地依赖于<B normal"><FONT face="Times New Roman">rt_priority</FONT></B>的值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26411</FONT>:此处,代码识别出这是一个非实时进程,它把<FONT face="Times New Roman">goodness</FONT>(在这个函数中被称为<B normal"><FONT face="Times New Roman">weight</FONT></B>)初始化为其当前的<B normal"><FONT face="Times New Roman">counter</FONT></B>值,这样如果进程已经占用<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">26412</FONT>:如果权值<B normal"><FONT face="Times New Roman">weight</FONT></B>的值为<FONT face="Times New Roman">0</FONT>,那么进程的计数器就已经被用完了,因此<B normal"><FONT face="Times New Roman">goodness</FONT></B>就不会再增加加权因素。其它进程就可以有机会运行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26418</FONT>:尽力优先考虑等待同一个处理器的进程(只在<FONT face="Times New Roman">SMP</FONT>系统中是这样——顺便说一下,考虑一下运行在一个双处理器的系统中的三个进程的实现情况)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26423</FONT>:给相关的当前进程或者当前线程增加了一些优点;这有助于合理使用缓存以避免使用昂贵的<FONT face="Times New Roman">MMU</FONT>上下文跳转。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26425</FONT>:增加进程的<B normal"><FONT face="Times New Roman">priority</FONT></B>。这样,<B normal"><FONT face="Times New Roman">goodness</FONT></B>(和其它类似的调度程序)就对较高优先级的进程比对较低优先级的进程更感兴趣,即使在前面进程已经部分用完了它们的时间片也是这样。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26428</FONT>:返回计算出来的<FONT face="Times New Roman">goodness</FONT>值。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">非实时优先级</H2><P 0cm 0cm 0pt">每个<FONT face="Times New Roman">Linux</FONT>进程都有一个优先级,这是从<FONT face="Times New Roman">1</FONT>到<FONT face="Times New Roman">40</FONT>的一个整数,其值存储在<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构的<B normal"><FONT face="Times New Roman">priority</FONT></B>成员中。(对于实时进程,在<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构中还会使用一个成员——<B normal"><FONT face="Times New Roman">rt_priority</FONT></B>成员。随后很快就会对它进行更详细的讨论。)它的范围使用<B normal"><FONT face="Times New Roman">PRIO_MIN</FONT></B>(在<FONT face="Times New Roman">16094</FONT>行宏定义为<FONT face="Times New Roman">-20</FONT>)和<B normal"><FONT face="Times New Roman">PRIO_MAX</FONT></B>(在<FONT face="Times New Roman">16095</FONT>行宏定义为<FONT face="Times New Roman">20</FONT>)限定——理论上来说,的确是这样。但是非常令人气恼的是,控制优先级的函数——<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>和<B normal"><FONT face="Times New Roman">sys_nice</FONT></B>——并没有注意到这些明显的常量,却相反宁愿使用一些固定的值。(它们也使用最大的完美值<FONT face="Times New Roman">19</FONT>,而不是<FONT face="Times New Roman">20</FONT>。)基于这个原因,<B normal"><FONT face="Times New Roman">PRIO_MIN</FONT></B>和<B normal"><FONT face="Times New Roman">PRIO_MAX</FONT></B>两个常量并没有广泛使用。不过这又是一个热心读者改进代码的机会。</P><P 0cm 0cm 0pt">由于已经在文档中说明<B normal"><FONT face="Times New Roman">sys_nice</FONT></B>(<FONT face="Times New Roman">27562</FONT>行)为要废弃不用了——可能会使用<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>来重新实现——我们就忽略前面一个函数,只讨论后面一个。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sys_setpriority</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29213</FONT>:<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>使用三个参数——<B normal"><FONT face="Times New Roman">which</FONT></B>,<B normal"><FONT face="Times New Roman">who</FONT></B>和<B normal"><FONT face="Times New Roman">niceval</FONT></B>。<B normal"><FONT face="Times New Roman">which</FONT></B>和<B normal"><FONT face="Times New Roman">who</FONT></B>参数提供了一种可以用来指定一个给定用户所拥有的单个进程,一组进程或者所有进程的方法。<B normal"><FONT face="Times New Roman">who</FONT></B>要根据<B normal"><FONT face="Times New Roman">which</FONT></B>的值做出不同的解释;它会作为一个进程<FONT face="Times New Roman">ID</FONT>,进程组<FONT face="Times New Roman">ID</FONT>或者用户<FONT face="Times New Roman">ID</FONT>读取。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29220</FONT>:这是确保<B normal"><FONT face="Times New Roman">which</FONT></B>有效地进行健全性检测。我认为这里的模糊不清是不必要的。如果我们不使用</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            if  ( which &gt; 2 || which &gt; 0 )</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">            if  ( which != PRIO_PROCESS &amp;&amp; wich != PRIO_PGRP &amp;&amp; which != PRIO_USER )</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">            if  ( which &gt; PRIO_USER || which &lt; PRIO_PGRP )</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>另外,在<FONT face="Times New Roman">29270</FONT>行也可以使用同样的方法。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29226</FONT>:<B normal"><FONT face="Times New Roman">niceval</FONT></B>是使用用户术语定义的——也就是说,它是在从<FONT face="Times New Roman">-20</FONT>到<FONT face="Times New Roman">19</FONT>的范围中,而不是象内核中使用的一样,在从<FONT face="Times New Roman">1</FONT>到<FONT face="Times New Roman">40</FONT>的范围中。如同变量名说明的一样,这是一个完美的值,但不是一个优先级。因此,为了实现这种转化,<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>应该跳过一些循环,同时要截断<B normal"><FONT face="Times New Roman">niceval</FONT></B>超出允许范围的值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>我承认自己被这段代码的复杂性所困扰着。使用实际上使用的<B normal"><FONT face="Times New Roman">DEF_PRIORITY</FONT></B>的值——<FONT face="Times New Roman">20</FONT>——以下的简化代码显然可以实现相同的效果:</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            if  ( niceval &lt;-19 )</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">              priority = 40;</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            else if  ( niceval &gt; 19 )</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">              priority = 1;</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            else </FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">              priority = 20 - niceval;</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>在保持比<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>中的代码简单的同时,我的实现方法中当然也可以用于处理<B normal"><FONT face="Times New Roman">DEF_PRIORITY</FONT></B>。因此,或者我严重误解了一些内容,或者就象我提出的代码本身,它根本就不需要这么复杂。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">29241</FONT>:循环遍历系统的任务列表中的所有任务,执行它可以允许修改。<B normal"><FONT face="Times New Roman">proc_sel</FONT></B>(<FONT face="Times New Roman">29190</FONT>行)说明了给定的进程是否对所提供的<B normal"><FONT face="Times New Roman">which</FONT></B>和<B normal"><FONT face="Times New Roman">who</FONT></B>值满意,可以用它来选择进程;由于<B normal"><FONT face="Times New Roman">sys_getpriority</FONT></B>也要使用这个函数,所以它也是<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>应该考虑的一个因素。</P><P 0cm 0cm 0pt">对于读取和设置单个进程优先级的普通情况(如果没有其它问题,就通过提早退出<B normal"><FONT face="Times New Roman">for_each_task</FONT></B>循环),<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>和<B normal"><FONT face="Times New Roman">sys_getpriority</FONT></B>(<FONT face="Times New Roman">29274</FONT>行开始的代码和此处有相似的内部循环)都对它有一点加速作用。<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>可能不会很频繁地被调用,但是<B normal"><FONT face="Times New Roman">sys_getpriority</FONT></B>却可能被很频繁调用,因而这样努力的是值得的。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">update_process_times</H4><P 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>只会影响进程的<B normal"><FONT face="Times New Roman">priority</FONT></B>成员——也就是其静态优先级。回忆一下进程也是具有动态优先级的,这由<B normal"><FONT face="Times New Roman">counter</FONT></B>成员表示,这一点我们在对<B normal"><FONT face="Times New Roman">schedule</FONT></B>和<B normal"><FONT face="Times New Roman">goodness</FONT></B>的讨论中就已经清楚地看到了。我们已经可以看出在调度程序发现<B normal"><FONT face="Times New Roman">counter</FONT></B>值为<FONT face="Times New Roman">0</FONT>时,<B normal"><FONT face="Times New Roman">schedule</FONT></B>会周期性地根据其静态优先级重新计算每一个进程的动态优先级。但是我们仍然还没有看到另外一部分困扰我们的问题:<B normal"><FONT face="Times New Roman">counter</FONT></B>是在哪里被递减的?它是怎样达到<FONT face="Times New Roman">0</FONT>的?</P><P 0cm 0cm 0pt">对于<FONT face="Times New Roman">UP</FONT>,答案与<B normal"><FONT face="Times New Roman">update_process_times</FONT></B>(<FONT face="Times New Roman">27382</FONT>行)有关。(和前面一样,我们把对于<FONT face="Times New Roman">SMP</FONT>问题的讨论延迟到第<FONT face="Times New Roman">10</FONT>章。)<B normal"><FONT face="Times New Roman">update_process_times</FONT></B>是作为<B normal"><FONT face="Times New Roman">update_time</FONT></B>(<FONT face="Times New Roman">27412</FONT>行)的一部分被调用的,它还是第<FONT face="Times New Roman">6</FONT>章中讨论的定时器中断的一部分。作为一个结果,它被相当频繁地调用——每秒钟<FONT face="Times New Roman">100</FONT>次。(当然,这只是对人类的内力来说是相当频繁的,对于<FONT face="Times New Roman">CPU</FONT>来说这实在是很慢的。)在每一次调用的时候,它都会把当前进程的<B normal"><FONT face="Times New Roman">counter</FONT></B>值减少从上次以来经过的“滴嗒”的数目(百分之一秒——请参看第<FONT face="Times New Roman">6</FONT>章)。通常,这只是一次跳动,但是如果内核正忙于处理中断,那么内核就可能会忽略定时器的跳动。当计数器减小到<FONT face="Times New Roman">0</FONT>以下时,<B normal"><FONT face="Times New Roman">update_process_times</FONT></B>就增加<B normal"><FONT face="Times New Roman">need_resched</FONT></B>标志,说明这个进程需要重新调度。</P><P 0cm 0cm 0pt">现在,由于进程缺省的优先级(使用内核优先级的术语,而不使用用户空间的完美值)是<FONT face="Times New Roman">20</FONT>,缺省情况下进程得到一个<FONT face="Times New Roman">21</FONT>次跳动的时间片。(的确这是<FONT face="Times New Roman">21</FONT>次跳动,而不是<FONT face="Times New Roman">20</FONT>次跳动,因为进程直到其动态优先级减少到<FONT face="Times New Roman">0</FONT>以下时才会为重新调度做出标记。)一次跳动是百分之一秒,或者是<FONT face="Times New Roman">10</FONT>微秒,因此缺省的时间片就是<FONT face="Times New Roman">210</FONT>微秒——大约是五分之一秒——在<FONT face="Times New Roman">16466</FONT>行有确切的描述。</P><P 0cm 0cm 0pt">我发现这个结果十分奇怪,因为原来以为理想的反应迅速的系统应该具有小很多的时间片——实际上我对这一点认识是如此强烈以至于开始的时候我还以为文档的说明发生了错误。但是,回顾一下,我觉得自己也不应该奇怪。毕竟,进程不会频繁地耗尽其整个时间片,因为它们经常都会因为<FONT face="Times New Roman">I/O</FONT>的原因而阻塞。在几个进程都绑定在<FONT face="Times New Roman">CPU</FONT>上时,在它们之间太频繁地跳转是没有必要的。(特别是在诸如<FONT face="Times New Roman">x86</FONT>之类的<FONT face="Times New Roman">CPU</FONT>上,这里的上下文跳转的代价是相当高的。)最后,我必须承认我从来没有注意到自己留意<FONT face="Times New Roman">Linux</FONT>逻辑单元的响应的迟缓特性,因此我觉得<FONT face="Times New Roman">210</FONT>微秒的时间片是个不错的选择——即使这在最初的时候看起来是太长了。</P><P 0cm 0cm 0pt">如果由于某些原因你需要时间片比当前最大值还长(<FONT face="Times New Roman">410</FONT>微秒,优先级上长到了<FONT face="Times New Roman">40</FONT>),你可以简单使用<B normal"><FONT face="Times New Roman">SCHED_FIFO</FONT></B>调度策略,在你准备好以后就可以释放<FONT face="Times New Roman">CPU</FONT>(或者重新编写<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>和<B normal"><FONT face="Times New Roman">sys_nice</FONT></B>)。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">实时优先级</H2><P 0cm 0cm 0pt"><FONT face="Times New Roman">Linux</FONT>的实时进程增加了一级优先级。实时优先级保存在<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构的<B normal"><FONT face="Times New Roman">rt_priority</FONT></B>成员中,它是一个从<FONT face="Times New Roman">0</FONT>到<FONT face="Times New Roman">99</FONT>的整数。(值<FONT face="Times New Roman">0</FONT>意味着进程不是实时进程,在这种情况下其<B normal"><FONT face="Times New Roman">policy</FONT></B>成员必须是<B normal"><FONT face="Times New Roman">SCHED_OTHER</FONT></B>。)</P><P 0cm 0cm 0pt">实时任务仍然使用相同的<B normal"><FONT face="Times New Roman">counter</FONT></B>成员作为它们的非实时的计数器部分。实时任务为了某些目的甚至使用与非实时任务使用的<B normal"><FONT face="Times New Roman">priority</FONT></B>成员相同的部分,这是当时间片用完时用来补充<B normal"><FONT face="Times New Roman">counter</FONT></B>值使用的值。为了清晰起见,<B normal"><FONT face="Times New Roman">rt_priority</FONT></B>只是用来对实时进程划分等级以对它们进行区分——否则它们的处理方式就和非实时进程相同了。</P><P 0cm 0cm 0pt">进程的<B normal"><FONT face="Times New Roman">rt_priority</FONT></B>被设定为使用<FONT face="Times New Roman">POSIX.1b</FONT>规定的函数<B normal"><FONT face="Times New Roman">sched_setscheduler</FONT></B>和<B normal"><FONT face="Times New Roman">sched_setparam</FONT></B>(通常只有<FONT face="Times New Roman">root</FONT>才可以调用这两个函数,这一点我们在讨论权能时会看到)设置其调度策略。注意这意味着如果具有修改权限,进程的调度策略在进程生命期结束以后就可以改变。</P><P 0cm 0cm 0pt">实现这些<FONT face="Times New Roman">POSIX</FONT>函数的系统调用<B normal"><FONT face="Times New Roman">sys_sched_setscheduler</FONT></B>(<FONT face="Times New Roman">27688</FONT>行)和<B normal"><FONT face="Times New Roman">sys_sched_setparam</FONT></B>(<FONT face="Times New Roman">27694</FONT>行)都会把实际的工作交给<B normal"><FONT face="Times New Roman">setschedular</FONT></B>(<FONT face="Times New Roman">27618</FONT>行)处理,这个函数我们现在就介绍。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">setscheduler</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27618</FONT>:这个函数的三个参数是目标进程<B normal"><FONT face="Times New Roman">pid</FONT></B>(<FONT face="Times New Roman">0</FONT>意味着当前进程),新的调度策略<B normal"><FONT face="Times New Roman">policy</FONT></B>,和包含附加信息的一个结构<B normal"><FONT face="Times New Roman">param</FONT></B>——它记录了<B normal"><FONT face="Times New Roman">rt_priority</FONT></B>的新值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27630</FONT>:在一些健全性检测之后,<B normal"><FONT face="Times New Roman">setscheduler</FONT></B>从用户空间中得到提供的<B normal"><FONT face="Times New Roman">struct sched_param</FONT></B>结构的备份。在<FONT face="Times New Roman">16204</FONT>行定义的<B normal"><FONT face="Times New Roman">struct sched_param</FONT></B>结构只有一个成员<B normal"><FONT face="Times New Roman">sched_priority</FONT></B>,它就是调用者为目标进程设计的<B normal"><I normal"><FONT face="Times New Roman">rt_priority</FONT></I></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27639</FONT>:使用<B normal"><FONT face="Times New Roman">find_process_by_pid</FONT></B>(<FONT face="Times New Roman">27608</FONT>行)找到目标进程,如果<B normal"><FONT face="Times New Roman">pid</FONT></B>是<FONT face="Times New Roman">0</FONT>,这个函数就返回一个指向当前任务的指针;如果存在指向具有给定<FONT face="Times New Roman">PID</FONT>进程,就返回指向该进程的指针;或者如果不存在具有这个<FONT face="Times New Roman">PID</FONT>的进程,就返回<B normal"><FONT face="Times New Roman">NULL</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27645</FONT>:如果<B normal"><FONT face="Times New Roman">policy</FONT></B>参数为负时,就保留当前的调度策略。否则,如果这是个有效值,那么现在就可以将其接收。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27657</FONT>:确保优先级没有越界。这是通过使用一点小技巧来加强的。该行只是第一步,它被用来确保所提供的值没有大得超出了范围。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27659</FONT>:现在已经确知新的实时优先级位于<FONT face="Times New Roman">0</FONT>到<FONT face="Times New Roman">99</FONT>的范围之内。如果<B normal"><FONT face="Times New Roman">policy</FONT></B>是<B normal"><FONT face="Times New Roman">SCHED_OTHER</FONT></B>,但是新的实时优先级不是<FONT face="Times New Roman">0</FONT>,那么这个测试就失败了。如果<B normal"><FONT face="Times New Roman">policy</FONT></B>指明了一个实时调度程序但是新的实时优先级是<FONT face="Times New Roman">0</FONT>(如果这里它不是<FONT face="Times New Roman">0</FONT>,就应该是从<FONT face="Times New Roman">1</FONT>到<FONT face="Times New Roman">99</FONT>),测试也会失败。否则,测试就能成功。这虽然并不是很易读,但它确实是正确的、最小的,(我想)速度也很快。我不确定这里我们是否对速度有所苛求,但是——到底一个进程需要多长时间需要设置它的调度程序?下面的代码就应该具有更好的可读性,而且当然也不会太慢:</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt; mso-outline-level: 1"><FONT face="Times New Roman">            P492 1</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27663</FONT>:不是每一个进程都可以设置自己的调度策略和其它进程的调度策略。如果所有进程都可以设置自己的调度策略,那么任何进程都可以简单地设置自己的调度策略为<B normal"><FONT face="Times New Roman">SCHED_FIFO</FONT></B>并进入一个无限循环来抢占<FONT face="Times New Roman">CPU</FONT>,这样必然会锁定系统。显然,是不能够允许这种做法的。因此,只有进程拥有这样处理的权能时,<B normal"><FONT face="Times New Roman">setscheduer</FONT></B>才会允许进程设置自己的调度策略。权能在下一节中将比较详细地介绍。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27666</FONT>:在相同的行中,我们不希望别人可以修改其它用户进程的调度策略;普通情况下,只允许你修改你自己所有的进程的调度策略。因此,<B normal"><FONT face="Times New Roman">setscheduer</FONT></B>要确保或者用户是设置自己所有的进程的处理程序或者具有修改其它进程的调度策略的权能。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">27672</FONT>:这里才是<B normal"><FONT face="Times New Roman">setscheduler</FONT></B>实际工作的地方,它在目标进程的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构中设置<B normal"><FONT face="Times New Roman">policy</FONT></B>和<B normal"><FONT face="Times New Roman">rt_priority</FONT></B>。如果该进程在运行队列中(这通过检测其<B normal"><FONT face="Times New Roman">next_run</FONT></B>成员非空来测试),就将它移到运行队列的顶部——这比较容易令人感到迷惑;可能这有助于<B normal"><FONT face="Times New Roman">SCHED_FIFO</FONT></B>进程实现对<FONT face="Times New Roman">CPU</FONT>的抢占。进程为重新调度做出标记,<B normal"><FONT face="Times New Roman">setscheduer</FONT></B>清空并退出。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">遵守限制</H2><P 0cm 0cm 0pt">内核经常需要决定是否允许进程执行某个操作。进程可能被简单的禁止执行某些操作,但却被允许在受限的环境中执行一些别的操作;这些操作基本上可以由权能表示,并且<FONT face="Times New Roman">/</FONT>或者可以从用户<FONT face="Times New Roman">ID</FONT>和组<FONT face="Times New Roman">ID</FONT>中推导出来。在其它期间,允许进程处理一些操作,但只是在受限的环境中——例如,它对<FONT face="Times New Roman">CPU</FONT>的使用必须受到限制。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5>权能</FONT></H3>在前面一节中,你已经看到了一个检测权能的例子——实际上是有两次相同的权能。这是<B normal">CAP_SYS_NICE</B>权能(14104行),它决定是否应该允许进程设置优先级(完美级别)
作者: ilikenba    时间: 2005-3-4 22:15
< 0cm 0cm 0pt">或调度策略。由于这比仅仅的完美级别要更适用,<B normal"><FONT face="Times New Roman">CAP_SYS_NICE</FONT></B>是一个误用的位——虽然很容易就可以看出设置调度策略和相关的概念是紧密相关的,而且你一般也不会要一个权能而不要另外一个权能。</P>< 0cm 0cm 0pt">每一个进程都有三个权能,它们被存储在进程的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构中(在<FONT face="Times New Roman">16400</FONT>行到<FONT face="Times New Roman">16401</FONT>行中):</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo2; tab-stops: list 42.25pt">l         <B normal"><FONT face="Times New Roman">cap_effective</FONT></B>——有效置位集合</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo2; tab-stops: list 42.25pt">l         <B normal"><FONT face="Times New Roman">cap_permitted</FONT></B>——允许位集合</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo2; tab-stops: list 42.25pt">l         <B normal"><FONT face="Times New Roman">cap_inheritable</FONT></B>——继承位集合</P>< 0cm 0cm 0pt">进程权能的有效位集合是当前可以处理的内容的集合;这是通过广泛使用的<B normal"><FONT face="Times New Roman">capable</FONT></B>函数检测的集合,这个函数在<FONT face="Times New Roman">16738</FONT>行定义。</P>< 0cm 0cm 0pt">允许位集合规定进程正常地可以被赋予的权能。这个集合通常不会增加——只有一种情况例外:如果一个进程具有<B normal"><FONT face="Times New Roman">CAP_SETPCAP</FONT></B>权能,那么它就可以将自己的允许位集合中的任何权能赋给其它进程,即使目标进程还没有拥有这个权能。</P>< 0cm 0cm 0pt">如果一个权能在允许位集合中,但是并不在有效位集合中,那么进程现在还没有马上拥有权能,但是它可以通过请求权能而获得。为什么要麻烦地区别它们呢?在本章开始我们第一次讨论权能的时候,我们简单地考虑了一个简单的例子:一个长期运行的进程只是偶然需要权能,而不是所有情况下都需要。为了保证进程不会偶然缺少权能,进程可以一直等待,直到它需要权能,接着请求权能,执行有权限的操作,并再次取消权能。这种方法比较安全。</P>< 0cm 0cm 0pt">继承位集合不像你想象的那么简单。它不是祖先继承在执行<B normal"><FONT face="Times New Roman">fork</FONT></B>的同时传递的权能集合——实际上,在创建的那一刻(也就是紧随着<B normal"><FONT face="Times New Roman">fork</FONT></B>),子孙进程的权能的三个集合和其祖先的三个权能集合都是相同的。相反,继承位集合在<B normal"><FONT face="Times New Roman">exec</FONT></B>运行期间才会起作用。进程在调用<B normal"><FONT face="Times New Roman">exec</FONT></B>之前的继承位集合有助于决定它的允许位集合和继承位集合,它们在<B normal"><FONT face="Times New Roman">exec</FONT></B>执行结束以后也会保留下来——仔细的介绍请参看<B normal"><FONT face="Times New Roman">compute_creds</FONT></B>(<FONT face="Times New Roman">9948</FONT>行)。注意在<B normal"><FONT face="Times New Roman">exec</FONT></B>之后权能是否保留要部分依赖于进程的继承位集合;它还要部分依赖于文件本身中的权能位集合(或者不管怎样,这至少是一个计划——虽然这种特性还没有完全实现)。</P>< 0cm 0cm 0pt">顺便提一下,注意到允许位集合必须总是有效位集合和继承位集合的超集(<FONT face="Times New Roman">superset</FONT>)(或者和有效位集合相同)。(只有对于有效位集合这才是严格正确的。一个进程可能会扩展另外一个进程的继承位集合从而它不再是其允许位集合的子集,但是就我知道的来说,这是无意义的,因此我们从现在就开始忽略这种可能性。)然而,和你可能希望的相反,有效位集合不一定要是继承位集合的超集(或者和继承位集合相同)。也就是说,在<B normal"><FONT face="Times New Roman">exec</FONT></B>结束以后,进程可能会拥有一个以前不曾有过的权能(虽然这个权能必须在其允许位集合中——也就是说,这是一个原来进程自己可能已经得到了的权能)。我认为这种需要只是局部的,这样进程就不需要暂时获得不需要的权能,而能够获得足以执行<B normal"><FONT face="Times New Roman">exec</FONT></B>程序的权能。</P>< 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P>< 0cm 0cm 0pt"><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>< 0cm 0cm 0pt; TEXT-ALIGN: center; mso-outline-level: 1" align=center>图<FONT face="Times New Roman">7.4 </FONT>权能集</P>< 0cm 0cm 0pt; TEXT-ALIGN: center; mso-outline-level: 1" align=center><p><FONT face="Times New Roman"> </FONT></p></P>< 0cm 0cm 0pt">图<FONT face="Times New Roman">7.4</FONT>说明了各种可能性。它显示了一个理想进程的三种权能集合,位从左到右计数。允许进程可以获得<B normal"><FONT face="Times New Roman">CAP_KILL</FONT></B>权能,这样就允许它不考虑其它属主而杀掉别的进程,但是它还没有立即拥有权限,而且也不会在<B normal"><FONT face="Times New Roman">exec</FONT></B>执行过程中自动获得。目前它具有增加和删除内核模块的权能(使用<B normal"><FONT face="Times New Roman">CAP_SYS_MODULE</FONT></B>),但是同样也不会在<B normal"><FONT face="Times New Roman">exec</FONT></B>执行过程中自动获得。它可以获得<B normal"><FONT face="Times New Roman">CAP_SYS_NICE</FONT></B>权能,但是直到<B normal"><FONT face="Times New Roman">exec</FONT></B>执行完后才会获得(假定文件权能位允许)。最后,它可以立即修改系统时间(<B normal"><FONT face="Times New Roman">CAP_SYS_TIME</FONT></B>),但是也是只有通过<B normal"><FONT face="Times New Roman">exec</FONT></B>才能获得这个权能。除非其它具有<B normal"><FONT face="Times New Roman">CAP_SETPCAP</FONT></B>权能的进程提供了这个权能,否则这个进程不能获得这个权能,它可能执行的其它进程也不可能获得这个权能。</P>< 0cm 0cm 0pt">保证这些不同性质的代码主要是在<FONT face="Times New Roman">kernel/capability.c</FONT>中,从<FONT face="Times New Roman">22460</FONT>行开始。两个主要的函数是读取权能的函数<B normal"><FONT face="Times New Roman">sys_capget</FONT></B>(<FONT face="Times New Roman">22480</FONT>行)和设置权能的函数<B normal"><FONT face="Times New Roman">sys_capset</FONT></B>(<FONT face="Times New Roman">22592</FONT>行);它们在下一节中讨论。通过<B normal"><FONT face="Times New Roman">exec</FONT></B>继承的权能使用<FONT face="Times New Roman">fs/exec.c</FONT>的<B normal"><FONT face="Times New Roman">compute_creds</FONT></B>(<FONT face="Times New Roman">9948</FONT>行)处理,这一点已经介绍过了。</P>< 0cm 0cm 0pt">当然,<FONT face="Times New Roman">root</FONT>肯定拥有所有的权能。内核权能特性给<FONT face="Times New Roman">root</FONT>提供了一种规则的方法来有所选择地只把需要的权能赋给特定的进程,而不用考虑该进程是否作为<FONT face="Times New Roman">root</FONT>用户运行。</P>< 0cm 0cm 0pt">权能一个有趣的特性是它们可以用来改变系统的“风格”。作为一个简单的例子,为所有的进程设置<B normal"><FONT face="Times New Roman">CAP_SYS_NICE</FONT></B>权能会使所有进程都增加自己的优先级(并设置它们的调度规则,等等)。如果你修改了系统中每一个进程的运行方式,那么你就改变了系统本身。自己设想一下发明一种新的可以通过更令人兴奋的方式修补系统的内核权能。</P>< 0cm 0cm 0pt">权能的尚未为人所知的优点是它们使源程序代码非常清晰。当检测当前进程是否允许设置系统时间时,却反而要检测当前进程是否以<FONT face="Times New Roman">root</FONT>运行,这种方式看起来似乎有些不很好。权能使我们可以了解它们的意思。权能的存在甚至还能够使查询进程的用户<FONT face="Times New Roman">ID</FONT>或组<FONT face="Times New Roman">ID</FONT>的代码更为清晰,这是因为这样的处理代码对这个问题的答案比较感兴趣,而是对从其中可以推导出的结论更感兴趣。否则,代码应该已经使用权能查询它需要了解的内容了。由于权能更加一致地和<FONT face="Times New Roman">Linux</FONT>内核代码结合起来,这种特性就变得更加可靠了。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">13916</FONT>:内核可以识别的权能从这里开始。因为这些宏定义的解释已非常详细了,我们就不再详细介绍其中每一个的内容了。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">14153</FONT>:赋给每一个权能的数字是简单的连续整数,但是由于要使用无符号整数中的位来编址,所以就使用<B normal"><FONT face="Times New Roman">CAP_TO_MASK</FONT></B>宏把它们转化为<FONT face="Times New Roman">2</FONT>的幂。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">14154</FONT>:设置和检测权能的核心只是一系列位操作;从这里到<FONT face="Times New Roman">include/linux/capability.h</FONT>中定义了用来使位操作更为清晰的宏和内联函数。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sys_capget</H4>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22480</FONT>:<B normal"><FONT face="Times New Roman">sys_capget</FONT></B>有两个参数:<B normal"><FONT face="Times New Roman">header</FONT></B>和<B normal"><FONT face="Times New Roman">dataptr</FONT></B>。<B normal"><FONT face="Times New Roman">header</FONT></B>是<B normal"><FONT face="Times New Roman">cap_user_header_t</FONT></B>类型(<FONT face="Times New Roman">13878</FONT>行)的,它是一个指向定义权能使用的版本和目标进程的<FONT face="Times New Roman">ID</FONT>的结构的指针;<B normal"><FONT face="Times New Roman">dataptr</FONT></B>是<B normal"><FONT face="Times New Roman">cap_user_data_t</FONT></B>类型(<FONT face="Times New Roman">13884</FONT>行)的,它也是一个指向结构类型的指针——这个结构包含有效位、允许位和继承位集合。<B normal"><FONT face="Times New Roman">sys_capget</FONT></B>通过第二个指针返回信息。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22492</FONT>:在版本不匹配的情况下,<B normal"><FONT face="Times New Roman">sys_capget</FONT></B>通过<B normal"><FONT face="Times New Roman">header</FONT></B>指针返回使用的版本,接着返回<FONT face="Times New Roman"><B normal">EINVA</B>L</FONT>错误(或者如果它不能把版本信息拷贝到调用者的空间中就返回<B normal"><FONT face="Times New Roman">EFAULT</FONT></B>)。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22509</FONT>:定义调用者希望了解其权能的进程;如果<B normal"><FONT face="Times New Roman">pid</FONT></B>不是<FONT face="Times New Roman">0</FONT>,也不是当前进程的<FONT face="Times New Roman">ID</FONT>,<B normal"><FONT face="Times New Roman">sys_capget</FONT></B>就要查询它。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22520</FONT>:如果它能装载目标进程,它就把自己的权能拷贝到临时变量<B normal"><FONT face="Times New Roman">data</FONT></B>中。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22530</FONT>:如果所有工作到目前为止都运行良好,它就把权能拷贝回用户空间中由<B normal"><FONT face="Times New Roman">dataptr</FONT></B>参数提供的地址中。然后,它返回<B normal"><FONT face="Times New Roman">error</FONT></B>变量——通常如果一切运行良好,这就是<FONT face="Times New Roman">0</FONT>;否则就是一个错误号。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sys_capset</H4>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22592</FONT>:<B normal"><FONT face="Times New Roman">sys_capset</FONT></B>的参数几乎和<B normal"><FONT face="Times New Roman">sys_capget</FONT></B>的参数类似。不同之处是<B normal"><FONT face="Times New Roman">data</FONT></B>(不再称为<B normal"><FONT face="Times New Roman">dataptr</FONT></B>了)是常量。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22600</FONT>:和<B normal"><FONT face="Times New Roman">sys_capget</FONT></B>一样,<B normal"><FONT face="Times New Roman">sys_capset</FONT></B>确保内核和调用进程使用一致的权能系统的版本。如果版本不一致,就拒绝尝试请求。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22613</FONT>:如果<B normal"><FONT face="Times New Roman">pid</FONT></B>不是<FONT face="Times New Roman">0</FONT>,就说明调用者希望设置其它进程的权能,在大多数情况下这种尝试都会遭到拒绝。如果调用者具有<B normal"><FONT face="Times New Roman">CAP_SETPCAP</FONT></B>权能,这意味着允许它设置任何进程的权能,<B normal"><FONT face="Times New Roman">sys_capset</FONT></B>就允许这种尝试。这种测试的前面部分有些太受限制了:如果它和当前进程的<B normal"><FONT face="Times New Roman">pid</FONT></B>相等,就接收这个<B normal"><FONT face="Times New Roman">pid</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22616</FONT>:从用户空间中拷贝新的权能,如果失败就返回错误。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22627</FONT>:和<FONT face="Times New Roman">22509</FONT>行开始的<B normal"><FONT face="Times New Roman">sys_capget</FONT></B>代码类似,<B normal"><FONT face="Times New Roman">sys_capset</FONT></B>定义了调用者希望了解其权能的进程。这就是两者的区别所在,<B normal"><FONT face="Times New Roman">sys_capset</FONT></B>为了说明进程组(或者是<FONT face="Times New Roman">-1</FONT>指明是所有进程)也允许其<B normal"><FONT face="Times New Roman">pid</FONT></B>值为负。在这种情况下,<B normal"><FONT face="Times New Roman">target</FONT></B>仍然设置为<B normal"><FONT face="Times New Roman">current</FONT></B>,因此当前进程的权能要在后面的计算中使用。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22642</FONT>:现在它必须保证合法地使用新的权能位集合,而且在内部保持一致。除非这种新特性在调用者的允许位集合中,否则这种测试会验证出新进程的继承位集合没有包含任何新鲜的东西。因此,它不会放弃调用者尚未拥有的任何权能。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22650</FONT>:类似地,<B normal"><FONT face="Times New Roman">sys_capset</FONT></B>也要确保除非调用者的允许位中包含新的特性,否则目标进程的允许位集合也不会包含尚未具有的特性。因此,它也不会放弃调用者尚未拥有的任何权能。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22658</FONT>:回想一下进程的有效位集合必须是其允许位集合的一个子集。这种性质在这里得到了保证。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22666</FONT>:<B normal"><FONT face="Times New Roman">sys_capset</FONT></B>现在已经准备对请求做出修改。负的<B normal"><FONT face="Times New Roman">pid</FONT></B>值意味着它正在给不止一个进程修改权能——如果<B normal"><FONT face="Times New Roman">pid</FONT></B>是<FONT face="Times New Roman">-1</FONT>,就是所有的进程;如果<B normal"><FONT face="Times New Roman">pid</FONT></B>是其它的负值,就是一个进程组中的所有进程。在这些情况下,实际工作分别由<B normal"><FONT face="Times New Roman">cap_set_all</FONT></B>(<FONT face="Times New Roman">22561</FONT>行)和<B normal"><FONT face="Times New Roman">cap_set_pg</FONT></B>(<FONT face="Times New Roman">22539</FONT>行)完成;这只是通过一些适当的进程集合循环,按照和单个进程相同的方法覆盖掉集合中的每一个进程的权能位集合。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22676</FONT>:如果<B normal"><FONT face="Times New Roman">pid</FONT></B>是正数(或者是<FONT face="Times New Roman">0</FONT>,表示当前进程),权能位集合只赋给目标进程。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5>用户<FONT face="Times New Roman">ID</FONT>和组<FONT face="Times New Roman">ID</FONT></FONT></H3><P 0cm 0cm 0pt">尽管权能功能强大、十分有用,但它并不是你实现访问控制的唯一武器。在一些情况中,我们需要了解哪个用户正在运行一个进程,或者进程是作为哪个用户来运行。用户使用整型的用户<FONT face="Times New Roman">ID</FONT>来区别,一个用户可以属于一个组或者多个组,每一个都有自己特有的整型<FONT face="Times New Roman">ID</FONT>。</P><P 0cm 0cm 0pt">有两种风格的用户<FONT face="Times New Roman">ID</FONT>和组<FONT face="Times New Roman">ID</FONT>:实际的<FONT face="Times New Roman">ID</FONT>和有效的<FONT face="Times New Roman">ID</FONT>。一般说来,实际用户(或组)<FONT face="Times New Roman">ID</FONT>为你说明了哪个用户创建了进程,有效用户(或组)<FONT face="Times New Roman">ID</FONT>为你说明在情况改变时进程作为哪个用户运行。由于访问控制的决定要更多依赖于进程作为哪儿用户运行,而不是哪个用户创建了这个进程,因此内核会比检测实际用户(和组)<FONT face="Times New Roman">ID</FONT>更加频繁地检测有效用户(或)<FONT face="Times New Roman">ID</FONT>——在我们现在关心的代码中就是这样处理的。<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构中的相关成员是<B normal"><FONT face="Times New Roman">uid</FONT></B>,<B normal"><FONT face="Times New Roman">euid</FONT></B>,<B normal"><FONT face="Times New Roman">gid</FONT></B>,和<B normal"><FONT face="Times New Roman">egid</FONT></B>(<FONT face="Times New Roman">16396</FONT>行到<FONT face="Times New Roman">16397</FONT>行)。注意用户<FONT face="Times New Roman">ID</FONT>和用户名不同,前者是一个整数,而后者是一个字符串。<FONT face="Times New Roman">/etc/passwd</FONT>文件把这两者关联起来。</P><P 0cm 0cm 0pt">让我们再回到<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>并看一下前面我们忽略了的从<FONT face="Times New Roman">29244</FONT>行到<FONT face="Times New Roman">29245</FONT>行的一些代码。<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>通常执行的操作都是让用户降低自己进程的优先级,但是不能降低其它用户进程的优先级——除非用户具有<B normal"><FONT face="Times New Roman">CAP_SYS_NICE</FONT></B>权能。因此,<FONT face="Times New Roman">if</FONT>表达式的前面两个术语要检测目标进程的用户<FONT face="Times New Roman">ID</FONT>是否和<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>的调用者的实际用户<FONT face="Times New Roman">ID</FONT>或者有效用户<FONT face="Times New Roman">ID</FONT>匹配。如果两个都不匹配,并且<B normal"><FONT face="Times New Roman">SYS_CAP_NICE</FONT></B>没有设置,<B normal"><FONT face="Times New Roman">sys_setpriority</FONT></B>就正确地拒绝这种尝试。</P><P 0cm 0cm 0pt">如果允许,进程可以使用<B normal"><FONT face="Times New Roman">sys_setuid</FONT></B>和<B normal"><FONT face="Times New Roman">sys_setgid</FONT></B>(<FONT face="Times New Roman">29578</FONT>行和<FONT face="Times New Roman">29445</FONT>行)和其它一些函数修改它们的用户<FONT face="Times New Roman">ID</FONT>和组<FONT face="Times New Roman">ID</FONT>。用户<FONT face="Times New Roman">ID</FONT>和组<FONT face="Times New Roman">ID</FONT>也可以通过执行可执行的<FONT face="Times New Roman">setuid</FONT>或<FONT face="Times New Roman">setgid</FONT>可执行程序进行修改。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT size=5>资源限制</FONT></H3><P 0cm 0cm 0pt">可以要求内核限制一个进程使用系统中的各种资源,包括内存和<FONT face="Times New Roman">CPU</FONT>时间。这可以通过<B normal"><FONT face="Times New Roman">sys_setrlimit</FONT></B>实现(<FONT face="Times New Roman">30057</FONT>行)。通过浏览<B normal"><FONT face="Times New Roman">struct rusage</FONT></B>结构(<FONT face="Times New Roman">16068</FONT>行)你对支持限制就可以有一个基本的概念。进程特有的限制在<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构中记录——还可能在什么地方?请参看<FONT face="Times New Roman">16404</FONT>行的<B normal"><FONT face="Times New Roman">rlim</FONT></B>数组成员。</P><P 0cm 0cm 0pt">违反限制的结果根据限制的不同也会有所不同。例如,对于<B normal"><FONT face="Times New Roman">RLIMIT_MPROC</FONT></B>(在本书的源程序代码中没有包括)——有关一个用户可以拥有的进程数目的限制——和你在<FONT face="Times New Roman">23974</FONT>行中看到的一样,结果仅仅和<B normal"><FONT face="Times New Roman">fork</FONT></B>失败一样。超出其它限制的后果对于一些进程可能比较严重,这样进程会被杀死(请参看<FONT face="Times New Roman">27333</FONT>行)。进程可以使用<B normal"><FONT face="Times New Roman">sys_getrlimit</FONT></B>(<FONT face="Times New Roman">30046</FONT>行)请求特殊限制,或者使用<B normal"><FONT face="Times New Roman">sys_getrusage</FONT></B>(<FONT face="Times New Roman">30143</FONT>行)请求资源使用限制。</P><P 0cm 0cm 0pt">在<FONT face="Times New Roman">30067</FONT>行中,注意进程可以随意减少自己的资源限制,但是它增加自己的资源限制时只能增加到一个最大值,这个值可以根据每一个资源限制进行具体设置。因此,当前的资源限制和所有的资源限制是分别记录的(使用在<FONT face="Times New Roman">16089</FONT>行定义的<B normal"><FONT face="Times New Roman">struct rlimit</FONT></B>结构的<B normal"><FONT face="Times New Roman">rlin_cur</FONT></B>成员和<B normal"><FONT face="Times New Roman">rlim_max</FONT></B>成员)。然而具有<B normal"><FONT face="Times New Roman">CAP_SYS_RESOURCE</FONT></B>权能的进程可以覆盖这个最大值。</P><P 0cm 0cm 0pt">这和优先级的规则不同:允许进程可以减小自己的优先级,但是为增加其优先级需要特殊许可,即使是它减少了自己的优先级接着又要马上增加它也是如此。当前资源限制和最大资源限制这两个相互关联的概念并没有反映在内核优先级的调度中。还有,注意到一个进程可以改变另一个进程的优先级(当然是假定它有权这样处理),但是一个进程只能修改自己的资源限制。</P><H2 13pt 0cm; TEXT-INDENT: 0cm">所有美好的事物都会结束——这就是它们如何处理的</H2><P 0cm 0cm 0pt">我们已经看到进程是如何生成的,怎样给它们赋予各自的生存周期。现在我们应该看一下它们是如何消亡的。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT face="Times New Roman" size=5>exit</FONT></H3><P 0cm 0cm 0pt">同第<FONT face="Times New Roman">6</FONT>章中介绍的一样,你可以通过给进程发送信号量<FONT face="Times New Roman">9</FONT>强行杀掉进程,但是更普通的情况是进程自动退出。进程通过调用系统调用<B normal"><FONT face="Times New Roman">exit</FONT></B>自动退出,它在内核中是由<B normal"><FONT face="Times New Roman">sys_exit</FONT></B>实现的(<FONT face="Times New Roman">23322</FONT>行)。(顺便说一下,当<FONT face="Times New Roman">C</FONT>程序从它的<B normal"><FONT face="Times New Roman">main</FONT></B>部分返回时,就会潜在调用<B normal"><FONT face="Times New Roman">exit</FONT></B>。)当进程退出时,内核释放所有分配给这个进程的资源——内存、文件,等等——当然,还要停止给它继续使用<FONT face="Times New Roman">CPU</FONT>的机会。</P><P 0cm 0cm 0pt">然而内核不能立即回收代表进程的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构,这是因为该进程的祖先必须能够使用<B normal"><FONT face="Times New Roman">wait</FONT></B>系统调用查询其子孙进程的退出状态。<B normal"><FONT face="Times New Roman">wait</FONT></B>返回它检测出的死亡状态的进程的<FONT face="Times New Roman">PID</FONT>,因此如果死亡的子孙进程在祖先进程仍在等待时就已经重新分配了,那么应用程序就会被搞乱(和其它问题一样,同一个祖先结束时可以有两个具有相同<FONT face="Times New Roman">PID</FONT>的子孙进程——一个进程是活动的,另一个进程是死亡的——祖先进程也不知道哪一个已经退出了)。因此,内核必须保留死亡子孙进程的<FONT face="Times New Roman">PID</FONT>直到<B normal"><FONT face="Times New Roman">wait</FONT></B>发生为止——这通过完整地保持其<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构来自动实现的;分配<FONT face="Times New Roman">PID</FONT>的代码就不用再查询它在任务列表中发现的进程是否是活动的。</P><P 0cm 0cm 0pt">处于这种在两种状态之间的进程——它既不是活动的,也没有真正死亡——被称为僵进程(<FONT face="Times New Roman">zombies</FONT>)。那么<B normal"><FONT face="Times New Roman">sys_exit</FONT></B>的任务就是把活动进程转化为僵进程。</P><P 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">sys_exit</FONT></B>本身的工作很少;它只是简单地把现存退出代码转化为<B normal"><FONT face="Times New Roman">do_exit</FONT></B>希望的格式,接着就会调用<B normal"><FONT face="Times New Roman">do_exit</FONT></B>,由它来处理实际的工作。(<B normal"><FONT face="Times New Roman">do_exit</FONT></B>也会作为发送信号量的一部分来调用,这一点我们在第<FONT face="Times New Roman">6</FONT>章中已经讨论过了。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23267</FONT>:<B normal"><FONT face="Times New Roman">do_exit</FONT></B>把退出代码作为参数处理,在其返回类型之前使用特殊符号<B normal"><FONT face="Times New Roman">NORET_TYPE</FONT></B>。虽然现在<B normal"><FONT face="Times New Roman">NORET_TYPE</FONT></B>(<FONT face="Times New Roman">14955</FONT>行)定义为空——因此它也就不起作用——但是原来它经常被定义为<B normal"><FONT face="Times New Roman">__volatile__</FONT></B>,用来提示<FONT face="Times New Roman">gcc</FONT>该函数不会返回。了解了这一点知识,<FONT face="Times New Roman">gcc</FONT>就执行一些额外的优化工作并取消有关函数不能成功返回的警告信息。使用其新的定义,<B normal"><FONT face="Times New Roman">NORET_TYPE</FONT></B>对于编译器就没有用处了,但是它仍然给我们人类传递了很多有用的信息。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23285</FONT>:释放它的信号量和其它<FONT face="Times New Roman">System V IPC</FONT>结构,这一点我们将在第<FONT face="Times New Roman">9</FONT>章中介绍。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23286</FONT>:释放分配给它的内存,这一点我们在第<FONT face="Times New Roman">8</FONT>章中介绍。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23290</FONT>:释放分配给它的文件,很快就会讨论。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23291</FONT>:释放它的文件系统数据,它超出了本书的范围。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23292</FONT>:释放它的信号量处理程序表,这一点我们在第<FONT face="Times New Roman">6</FONT>章中介绍过了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23294</FONT>:剩下的任务是进入<B normal"><FONT face="Times New Roman">TASK_ZOMBIE</FONT></B>状态,其退出代码被记录下来以供将来祖先进程使用。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23296</FONT>:调用<B normal"><FONT face="Times New Roman">exit_notify</FONT></B>(<FONT face="Times New Roman">23198</FONT>行),它会警告当前退出任务的祖先进程和其进程组中的所有成员该进程正在退出。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23304</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>。这个对于<B normal"><FONT face="Times New Roman">schedule</FONT></B>的调用从来不会返回,这是因为它跳转到下一个进程的上下文,从来不会再跳转回来,因此这是现在退出的进程的最后一次拥有<FONT face="Times New Roman">CPU</FONT>的机会。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">__exit_files</H4><P 0cm 0cm 0pt">进程如何和文件交互不是本书的主题。但是我们应该快速浏览一下<B normal"><FONT face="Times New Roman">__exit_files</FONT></B>(<FONT face="Times New Roman">23109</FONT>行),因为这样会有助于我们理解<B normal"><FONT face="Times New Roman">__clone</FONT></B>函数,这个函数使祖先进程和子孙进程可以共享特定的信息。祖先进程和子孙进程可以共享的一种信息是它们打开的文件列表。和当时说明的一样,<FONT face="Times New Roman">Linux</FONT>使用引用计数器规则来保证进程退出之后可以正确地处理扫尾工作。这里就有个扫尾工作的很好的例子。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23115</FONT>:假设进程已经打开了文件(几乎总会是这样的),<B normal"><FONT face="Times New Roman">__exit_files</FONT></B>会递减原来存储在<B normal"><FONT face="Times New Roman">tsk-&gt;files-&gt;count</FONT></B>中的引用计数器。诸如<B normal"><FONT face="Times New Roman">atomic_dec_and_test</FONT></B>之类的原子操作将在第<FONT face="Times New Roman">10</FONT>章详细介绍;知道<B normal"><FONT face="Times New Roman">atomic_dec_and_test</FONT></B>(<FONT face="Times New Roman">10249</FONT>行)递减其参数值并当参数新值是<FONT face="Times New Roman">0</FONT>时返回真值就足够了。因此,如果<B normal"><FONT face="Times New Roman">tsk</FONT></B>的对于目标<B normal"><FONT face="Times New Roman">struct files_struct</FONT></B>结构的引用是最后一次时,这就是正确的。(如果这是一个私有拷贝,没有和其它任何进程共享,那么引用计数器的初始值就是<FONT face="Times New Roman">1</FONT>,当然它被减小为<FONT face="Times New Roman">0</FONT>。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23116</FONT>:在释放记录进程的打开文件的内存之前,必须把这些文件都关闭,这是通过调用<B normal"><FONT face="Times New Roman">close_files</FONT></B>(<FONT face="Times New Roman">23081</FONT>行)实现的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23118</FONT>:释放保留进程的文件描述符数组<B normal"><FONT face="Times New Roman">fd</FONT></B>的内存,这个数组是<B normal"><FONT face="Times New Roman">files</FONT></B>的一个子域。打开文件(<B normal"><FONT face="Times New Roman">NR_OPEN</FONT></B>,在<FONT face="Times New Roman">15067</FONT>行中定义<FONT face="Times New Roman">1,024</FONT>)的最大数量要加以选择,这样本行中的<B normal"><FONT face="Times New Roman">if</FONT></B>测试就能正确——<B normal"><FONT face="Times New Roman">fd</FONT></B>数组必须刚好适合一个内存页的大小。这样做可以使得内存的分配(或释放)速度快许多;否则,<B normal"><FONT face="Times New Roman">__exit_files</FONT></B>只好使用更通用但是速度却慢得多的内核的内存函数了。下一章会加深你对这种决策的理解。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23122</FONT>:最后,<B normal"><FONT face="Times New Roman">__exit_files</FONT></B>释放<B normal"><FONT face="Times New Roman">files</FONT></B>本身。</P><P 0cm 0cm 0pt">其它<B normal"><FONT face="Times New Roman">__exit_xxx</FONT></B>函数背后的概念是类似的:它们减少了任务自有的对于潜在共享信息的引用计数器,如果这是最后一次引用,它们要负责执行所有必须的工作来将其清除。</P><H3 6pt 0cm; TEXT-INDENT: 0cm"><FONT face="Times New Roman" size=5>wait</FONT></H3><P 0cm 0cm 0pt">和<B normal"><FONT face="Times New Roman">exec</FONT></B>一样,<B normal"><FONT face="Times New Roman">wait</FONT></B>是一组函数,而不是一个函数。(但是和<B normal"><FONT face="Times New Roman">exec</FONT></B>不同,<B normal"><FONT face="Times New Roman">wait</FONT></B>家族的函数实际包含一个名为<B normal"><FONT face="Times New Roman">wait</FONT></B>的函数。)<B normal"><FONT face="Times New Roman">wait</FONT></B>家族中的其它函数最终都是使用一个系统调用<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>(<FONT face="Times New Roman">23327</FONT>行)实现的,这个系统调用的名字反映出它实现了<B normal"><FONT face="Times New Roman">wait</FONT></B>家族中最通用的函数<B normal"><FONT face="Times New Roman">wait4</FONT></B>。标准<FONT face="Times New Roman">C</FONT>库<FONT face="Times New Roman">libc</FONT>的实现必须重新组织对于其它<B normal"><FONT face="Times New Roman">wait</FONT></B>函数调用的参数并调用<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>。(这还不是问题的全部:由于历史的原因,内核到<FONT face="Times New Roman">Alpha</FONT>的移植也会提供<B normal"><FONT face="Times New Roman">sys_waitpid</FONT></B>。但是即使是<B normal"><FONT face="Times New Roman">sys_waitpid</FONT></B>也会反过来调用<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>。)</P><P 0cm 0cm 0pt">除了处理一些其它内容,<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>——也只有<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>——最终把僵进程送进坟墓。然而从应用程序的观点来看,<B normal"><FONT face="Times New Roman">wait</FONT></B>和相关函数要检测子孙进程的状态:检测是否有进程死亡了,如果有,到底是哪一个进程,这个进程是怎样死亡的。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">sys_wait4</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23327</FONT>:为了适合作为相当通用的一个函数,<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>有很多参数,其中一些是可选的。和通常情况一样,<B normal"><FONT face="Times New Roman">pid</FONT></B>是目标进程的<FONT face="Times New Roman">PID</FONT>;和你看到的一样,<FONT face="Times New Roman">0</FONT>和负值是特殊的。如果<B normal"><FONT face="Times New Roman">stat_addr</FONT></B>非空,那么它就是所得子孙进程的退出状态应该拷贝到的地址。<B normal"><FONT face="Times New Roman">options</FONT></B>是一些可能定义<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>的操作的标志的集合。如果<B normal"><FONT face="Times New Roman">ru</FONT></B>非空,那么它就是所获得的子孙进程资源使用信息所应该拷贝到的地址。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23335</FONT>:如果提供了无效选项,<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>就返回错误代码。这种决定看起来有点荒唐;我们可以简单忽略一些无关选项。当然,这样处理所需要的参数,如果调用者设置了自己不想设置的位,那么希望的操作是不要执行——在任何情况下,这都意味着调用者不能正确理解,在这种情况下发送一个失败信号量要比简单地忽略调用者的这种困惑要更多。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23342</FONT>:循环遍历该进程的直接子进程(但不包括其孙进程,曾孙进程,等等)。如同本章中前面说明的一样,进程的最年轻(最近创建的)子孙进程通过<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构的<B normal"><FONT face="Times New Roman">p_cptr</FONT></B>成员是可访问的,这个最年轻进程原来的兄弟进程通过其<B normal"><FONT face="Times New Roman">p_osptr</FONT></B>成员也是可以访问的;因此,<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>从这个最年轻子孙进程开始遍历其祖先的所有子孙进程,并逐渐遍历其原来的兄弟进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23343</FONT>:根据<B normal"><FONT face="Times New Roman">pid</FONT></B>参数的值筛选出不匹配的<FONT face="Times New Roman">PID</FONT>。注意值为<FONT face="Times New Roman">-1</FONT>的<B normal"><FONT face="Times New Roman">pid</FONT></B>参数是如何潜在的对进程进行选择的,正如我们所期望的:<B normal"><FONT face="Times New Roman">pid</FONT></B>值在<FONT face="Times New Roman">23343</FONT>,<FONT face="Times New Roman">23346</FONT>和<FONT face="Times New Roman">23349</FONT>行中的测试没有成功,因此它就不会遭到拒绝。这样,系统需要对每一个子孙进程进行考虑。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23376</FONT>:这就是我们现在感兴趣的情况——祖先进程正在等待一个已经结束了的进程。这是最后实际上得到僵进程的地方。它通过更新子孙进程使用的进程的用户时间和系统时间部分开始(这通过<FONT face="Times New Roman">29772</FONT>行的<B normal"><FONT face="Times New Roman">sys_times</FONT></B>系统调用实现),因为子孙进程不会再参与计算了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23382</FONT>:其它资源使用信息被收集起来(如果要求这样处理),子孙进程的退出状态被传递到特定的地址中(同样,如果要求这样处理)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23387</FONT>:设置<B normal"><FONT face="Times New Roman">retval</FONT></B>为当前得到的死亡子孙进程的<FONT face="Times New Roman">PID</FONT>。这就是最后的结果;<B normal"><FONT face="Times New Roman">retval</FONT></B>不会再改变了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23388</FONT>:如果这个垂死进程的当前祖先进程不是原来的祖先进程,那么进程就会离开进程图表中的当前位置(通过<B normal"><FONT face="Times New Roman">REMOVE_LINKS</FONT></B>,<FONT face="Times New Roman">16876</FONT>行),在其原始祖先的控制下重新安装自己(通过<B normal"><FONT face="Times New Roman">SET_LINKS</FONT></B>,<FONT face="Times New Roman">16887</FONT>行),接着给其祖先进程发送<B normal"><FONT face="Times New Roman">SIGCHLD</FONT></B>信号量,这样祖先进程就知道其子孙进程已经退出了。这种通知是通过<B normal"><FONT face="Times New Roman">notify_parent</FONT></B>(<FONT face="Times New Roman">28548</FONT>行,在第<FONT face="Times New Roman">6</FONT>章中介绍)传递的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23396</FONT>:否则——正常情况——最后可以调用<B normal"><FONT face="Times New Roman">release</FONT></B>(<FONT face="Times New Roman">22951</FONT>行)释放所得子孙进程的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构。(在看完<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>以后,我们马上就会看<B normal"><FONT face="Times New Roman">release</FONT></B>。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23400</FONT>:现在已经成功获取了子孙进程,因此<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>只需要返回成功信息就完美地完成了工作;它跳转到<FONT face="Times New Roman">23418</FONT>行,从这儿返回<B normal"><FONT face="Times New Roman">retval</FONT></B>(所获得子孙进程的<FONT face="Times New Roman">PID</FONT>)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23401</FONT>:注意特殊的流程控制;<B normal"><FONT face="Times New Roman">default</FONT></B>的情况需要继续执行从<FONT face="Times New Roman">23342</FONT>行开始的<B normal"><FONT face="Times New Roman">for</FONT></B>循环。因为只有既没有停止运行也不是僵进程的进程才会执行到<B normal"><FONT face="Times New Roman">default</FONT></B>的情况,所以这种流程控制是正确的,但是初次阅读时比较容易误解。而且,无论如何这也有些多余;没有它循环也一样能处理。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23406</FONT>:如果代码能运行到此处,<B normal"><FONT face="Times New Roman">for</FONT></B>循环就可以完整地运行下来——正在调用的进程遍历执行其子孙进程没有发现匹配的整个列表——计算的结果是三种状态中的一种。或者由于该任务没有和所提供的<B normal"><FONT face="Times New Roman">pid</FONT></B>参数匹配的子孙进程,因而还没有进程退出,或者(是前面情况的一个特例)该任务根本就没有子孙进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23408</FONT>:如果<B normal"><FONT face="Times New Roman">flag</FONT></B>不为<FONT face="Times New Roman">0</FONT>,在<B normal"><FONT face="Times New Roman">for</FONT></B>循环中就可以执行到<FONT face="Times New Roman">23358</FONT>行,这说明至少有一个进程和所提供的<B normal"><FONT face="Times New Roman">pid</FONT></B>参数匹配——它不是僵进程,也没有被终止,因此它就不能被获取。在这种情况下,如果提供了<B normal"><FONT face="Times New Roman">WNOHANG</FONT></B>选项——这意味着如果不能获取子孙进程,那么调用者就不会等待——它向前跳转到最后,返回<FONT face="Times New Roman">0</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23411</FONT>:如果有信号量被接收,就退出并返回一个错误。这个信号量不是<B normal"><FONT face="Times New Roman">SIGCHLD</FONT></B>——如果它是<B normal"><FONT face="Times New Roman">SIGCHLD</FONT></B>,就应该已经发现了死亡的进程,因此就不可能执行到此处。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23413</FONT>:否则,一切都没有问题;调用者只需要等待一个子孙进程退出。因此,进程的状态被设置为<B normal"><FONT face="Times New Roman">TASK_INTERRUPTIBLE</FONT></B>并调用<B normal"><FONT face="Times New Roman">schedule</FONT></B>释放<FONT face="Times New Roman">CPU</FONT>给另一个进程使用。正在等待的进程直到再次获得<FONT face="Times New Roman">CPU</FONT>时才会返回,同时要再次检测死亡子孙进程(通过向回跳转到<FONT face="Times New Roman">23339</FONT>行的<B normal"><FONT face="Times New Roman">repeat</FONT></B>标号)。回想一下处于<B normal"><FONT face="Times New Roman">TASK_INTERRUPTIBLE</FONT></B>状态的进程要等待信号量将其唤醒——在这种情况下,它特别希望<B normal"><FONT face="Times New Roman">SIGCHLD</FONT></B>来指明子孙进程已经退出了,但是任何信号量都可以到达。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">23417</FONT>:<B normal"><FONT face="Times New Roman">flag</FONT></B>是<FONT face="Times New Roman">0</FONT>,因为或者进程没有子孙进程,或者所提供的<B normal"><FONT face="Times New Roman">pid</FONT></B>参数不能和它的任何子孙进程匹配——不管怎样,<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>都给调用者返回一个<B normal"><FONT face="Times New Roman">ECHILD</FONT></B>错误。</P><H4 6pt 0cm; TEXT-INDENT: 0cm">release</H4><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22951</FONT>:<B normal"><FONT face="Times New Roman">release</FONT></B>的唯一一个参数是指向要释放的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>结构的指针。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22953</FONT>:确保该任务没有试图释放自身——这是会在内核中引起逻辑错误的一种无意义的情况。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22969</FONT>:<FONT face="Times New Roman">UP</FONT>代码实际上是通过调用<B normal"><FONT face="Times New Roman">free_uid</FONT></B>(<FONT face="Times New Roman">23532</FONT>行)开始的,它用来释放潜在共享的<B normal"><FONT face="Times New Roman">struct user_struct</FONT></B>结构,这个结构除了其它功能以外,还要帮助<B normal"><FONT face="Times New Roman">fork</FONT></B>确保不会出现单个用户影响所有进程的情况。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22970</FONT>:减小系统关于正在运行的任务总数的计数并释放<B normal"><FONT face="Times New Roman">tarray_freelist</FONT></B>中的僵死进程的时间片。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22974</FONT>:僵死进程的<FONT face="Times New Roman">PID</FONT>也会释放,并且使用<B normal"><FONT face="Times New Roman">REMOVE_LINKS</FONT></B>(<FONT face="Times New Roman">16876</FONT>行)解除它同进程表和任务列表的关联。注意,由于内核数据结构在此处正在做出修正,<B normal"><FONT face="Times New Roman">task</FONT></B>数组中的进程项并不需要被设置为<B normal"><FONT face="Times New Roman">NULL</FONT></B>;把它的空槽增加到自由列表中就足够了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22979</FONT>:僵死进程有关次要页表错误,主要页表错误的总数以及向外交换所使用的时间的数量被增加到当前进程对应的<FONT face="Times New Roman"> </FONT>“子孙进程计数”中——这是正确的;<B normal"><FONT face="Times New Roman">release</FONT></B>只能通过<B normal"><FONT face="Times New Roman">sys_wait4</FONT></B>调用,这样只允许进程释放自己的子孙进程。因此,当前进程必须是僵死进程的祖先。</P>22982:最后,应该回收垂死进程的<B normal">struct task_struct</B>结构,这可以通过对<B normal">free_task_struct</B>的调用(2391行)来实现。这个函数简单地回收存储在这个结构中的内存页。现在,进程最终功德圆满的寿终正寝了。
作者: ilikenba    时间: 2005-3-4 22:18
<H1 17pt 0cm 16.5pt; TEXT-ALIGN: center" align=center>第<FONT face="Times New Roman">8</FONT>章<FONT face="Times New Roman"> </FONT>内存</H1>< 0cm 0cm 0pt">内存是内核所管理的最重要的资源之一。某进程区别于其它进程的一个特征是两个进程存在于逻辑上相互独立的内存空间(与之相反,线程共享内存)。即使进程都是同一程序的实例,比如,两个<FONT face="Times New Roman">xterm</FONT>或两个<FONT face="Times New Roman">Emacse</FONT>,内核都会为每个进程安排内存空间,使得它们看起来像是在系统之上运行的唯一进程。当一个进程不可能偶然或恶意的修改其它进程的执行空间时,系统的安全性和稳定性就会得到增强。</P>< 0cm 0cm 0pt">内核也生存在它自己的内存空间之中,即内核空间(<FONT face="Times New Roman">kernel space</FONT>)。与之对应的是用户空间(<FONT face="Times New Roman">user space</FONT>),它是所有非内核任务所处的内存空间的一个通用术语。</P><H2 13pt 0cm">虚拟内存</H2>< 0cm 0cm 0pt">计算机系统包括不同级别的存储器。图<FONT face="Times New Roman">8-1</FONT>说明了这些存储器中最重要的几项,并且以我自己原有的<FONT face="Times New Roman">Linux</FONT>机器(<FONT face="Times New Roman">Linux box</FONT>)为例标注了一些参数的估计值。当你从左向右观察该图时,会发现存储器容量越来越大而速度却越来越慢(而且每字节价格也会更低)。尤其令人注意的是,访问速度跨越了<FONT face="Times New Roman">3</FONT>个数量级(乘数因子为<FONT face="Times New Roman">1000</FONT>),而容量竟跨越了超过<FONT face="Times New Roman">8</FONT>个数量级(乘数因子为<FONT face="Times New Roman">312500000</FONT>)。(实际上有时速度的差异是可以被掩盖的,不过这些数字足以很好的说明这一部分讨论的目的。)最大的差距体现在最后两个:<FONT face="Times New Roman">RAM</FONT>和磁盘上,它们又分别可被称作主存和辅存。<p></p></P>< 0cm 0cm 0pt">额外附加的存储器空间总是十分诱人的,即使它们也很慢。如果在<FONT face="Times New Roman">RAM</FONT>被用完时,通过暂时把不用的代码和数据转移到磁盘上以腾出更多空间的方法来使用磁盘代替<FONT face="Times New Roman">RAM</FONT>的话,那将是很好的一件事情。正如读者可能已经知道的,<FONT face="Times New Roman">Linux</FONT>恰好能够做到这一点,这被称之为虚拟内存(<FONT face="Times New Roman">virtual memory</FONT>)。<p></p></P>< 0cm 0cm 0pt">虚拟内存是一种对<FONT face="Times New Roman">RAM</FONT>和磁盘(或称之为:主存和辅存)进行无缝混合访问的技术。所有这些(虚拟)内存对于应用程序来说就好像它真的存在一样。当然我们知道它并非真的内存,这正是为什么它被称为是“虚拟的”,但是多亏了内核使得应用程序无法分辨出它们的区别。对于应用程序来说,就好像真的有很大数量的<FONT face="Times New Roman">RAM</FONT>,只不过有时候比较慢而已。<p></p></P>< 0cm 0cm 0pt">术语“虚拟内存”还有另外一层意思,从严格意义来讲是与前述的第一种意思没有关系的。这里的虚拟内存指的是对进程驻留地址进行欺骗的方法。每个进程都会有这样一种错觉,认为它的地址是从<FONT face="Times New Roman">0</FONT>开始并由此连续向上发展的。很明显,这一点同时对所有进程都成立是不可能的,但是在生成代码的时候这个假定(<FONT face="Times New Roman">fiction</FONT>)却能够带来很大方便,这是由于进程不必知道它们是否真正从<FONT face="Times New Roman">0</FONT>地址开始驻留,而且它们也不必去关心此事。<p></p></P>< 0cm 0cm 0pt">这两种意思也不必相关,因为一个操作系统从理论上可以给每个进程分配一个独有的逻辑地址空间而不用混合使用主存和辅存。然而在所有我已经知道的系统中(对这两种虚拟内存的实现方式)要么都采纳要么都不采纳,这一点可能会在开始时令人感到困惑。<p></p></P>< 0cm 0cm 0pt">为了避免这种意义上的分歧,有人倾向于术语“虚拟内存”代表逻辑地址空间(<FONT face="Times New Roman">logical- address-space</FONT>)的意义,同时使用“分页(<FONT face="Times New Roman">paging</FONT>)”或“交换”表示磁盘作为内存使用(<FONT face="Times New Roman">disk-as-memory</FONT>)的含义。尽管这种严格的区分具有充足的理由,但是我更喜欢普通的用法。除非上下文要求,否则我很少花费精力对它们进行区分。<p></p></P>< 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P>< 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P>< 0cm 0cm 0pt"><v:shapetype><v:stroke joinstyle="miter"></v:stroke><v:path connecttype="rect" gradientshapeok="t"></v:path></v:shapetype><v:shape><v:textbox style="mso-next-textbox: #_x0000_s1026"><FONT face="Times New Roman"></FONT></v:textbox></v:shape><v:shape><v:textbox style="mso-next-textbox: #_x0000_s1028"><FONT face="Times New Roman"></FONT></v:textbox></v:shape><v:shape><v:textbox style="mso-next-textbox: #_x0000_s1030"><FONT face="Times New Roman"></FONT></v:textbox></v:shape><v:shape><v:textbox style="mso-next-textbox: #_x0000_s1029"><FONT face="Times New Roman"></FONT></v:textbox></v:shape><v:shape><v:textbox style="mso-next-textbox: #_x0000_s1027"><FONT face="Times New Roman"></FONT></v:textbox></v:shape><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV class=shape 7.95pt; PADDING-LEFT: 7.95pt; PADDING-BOTTOM: 4.35pt; PADDING-TOP: 4.35pt" v:shape="_x0000_s1026">< 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><FONT face="Times New Roman">Registers<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><p><FONT face="Times New Roman"> </FONT></p></P>< 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><FONT face="Times New Roman">32 bytes<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><FONT face="Times New Roman">9 ns<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><p><FONT face="Times New Roman"> </FONT></p></P></DIV></TD></TR></TABLE><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV class=shape 7.95pt; PADDING-LEFT: 7.95pt; PADDING-BOTTOM: 4.35pt; PADDING-TOP: 4.35pt" v:shape="_x0000_s1027">< 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><FONT face="Times New Roman">On-chip<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><FONT face="Times New Roman">(L1)cache<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><FONT face="Times New Roman">16K<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><FONT face="Times New Roman">9 ns<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><p><FONT face="Times New Roman"> </FONT></p></P></DIV></TD></TR></TABLE><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV class=shape 7.95pt; PADDING-LEFT: 7.95pt; PADDING-BOTTOM: 4.35pt; PADDING-TOP: 4.35pt" v:shape="_x0000_s1030">< 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><FONT face="Times New Roman">On-chip<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><FONT face="Times New Roman">(L2)cache<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><FONT face="Times New Roman">256K<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><FONT face="Times New Roman">20 ns<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><p><FONT face="Times New Roman"> </FONT></p></P></DIV></TD></TR></TABLE><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV class=shape 7.95pt; PADDING-LEFT: 7.95pt; PADDING-BOTTOM: 4.35pt; PADDING-TOP: 4.35pt" v:shape="_x0000_s1028">< 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><FONT face="Times New Roman">RAM<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><p><FONT face="Times New Roman"> </FONT></p></P>< 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><FONT face="Times New Roman">96MB<p></p></FONT></P>< 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><FONT face="Times New Roman">70 ns<p></p></FONT></P><P 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><p><FONT face="Times New Roman"> </FONT></p></P></DIV></TD></TR></TABLE><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV class=shape 7.95pt; PADDING-LEFT: 7.95pt; PADDING-BOTTOM: 4.35pt; PADDING-TOP: 4.35pt" v:shape="_x0000_s1029"><P 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><FONT face="Times New Roman">Hard Disk<p></p></FONT></P><P 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><FONT face="Times New Roman">10GB<p></p></FONT></P><P 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><FONT face="Times New Roman">9 ms<p></p></FONT></P><P 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><p><FONT face="Times New Roman"> </FONT></p></P></DIV></TD></TR></TABLE><p><FONT face="Times New Roman"> </FONT></p></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><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><P 0cm 0cm 0pt; TEXT-ALIGN: center" align=center><p><FONT face="Times New Roman"> </FONT></p></P><BR vglayout" clear=all><P 0cm 0cm 0pt; TEXT-ALIGN: center" align=center>图<FONT face="Times New Roman">8-1 </FONT>具有速度和容量的存储级别<p></p></P><H3 13pt 0cm"><FONT size=5>交换和分页</FONT></H3><P 0cm 0cm 0pt">早期的虚拟内存(<FONT face="Times New Roman">VM</FONT>)系统仅能够把整个应用程序代码和数据,即完整的进程从磁盘上移出或移入磁盘。这种技术被称为交换(<FONT face="Times New Roman">swapping</FONT>),因为它是把一个进程同另一个进程进行了对调。出于这个原因,磁盘上为<FONT face="Times New Roman">VM</FONT>所保留的区域通常被称为交换空间(<FONT face="Times New Roman">swap space</FONT>),或简称为交换区(<FONT face="Times New Roman">swap</FONT>),尽管如我们所见,现代的系统已不再使用这种最初意义上的交换技术。与此类似,读者通常会见到的术语是交换设备(<FONT face="Times New Roman">swap device</FONT>)和交换分区(<FONT face="Times New Roman">swap partition</FONT>),它是磁盘分区的同义词,但是被专门作为交换空间使用,以及术语交换文件(<FONT face="Times New Roman">swap file</FONT>),这是一个用于交换的规则的、有固定长度的文件。</P><P 0cm 0cm 0pt">交换是很有用的,当然要比根本没有<FONT face="Times New Roman">VM</FONT>好的多,但是它也有一定局限性。首先,交换需要把整个进程同时调入内存,所以当运行一个需要比系统所有<FONT face="Times New Roman">RAM</FONT>还要大的存储空间的进程时,交换便于事无补了,即使磁盘有大量空间可供补充。</P><P 0cm 0cm 0pt">其次,交换可能会很低效。交换就必须把整个进程同时调出,这就意味着为了<FONT face="Times New Roman">2K</FONT>的空间你不得不把一个<FONT face="Times New Roman">8MB</FONT>的进程整个调出。同样的道理,即使仅仅需要执行被调进的应用程序代码的一小部分,你也必须把整个进程同时调进。</P><P 0cm 0cm 0pt">分页(<FONT face="Times New Roman">paging</FONT>)是把系统的内存划分成很小的块,即页面,每个页面可以独立的从磁盘调入或调出磁盘。分页与交换技术相似,但它使用更加细小的粒度(<FONT face="Times New Roman">granularity</FONT>)。分页比交换有更多的登记(<FONT face="Times New Roman">book-keeping</FONT>)开销,这是因为页面数远比进程数要多,然而通过分页可以获得更多的灵活性。而且分页也更快一些,原因之一就是不再需要把整个进程调进调出,而只需要交换必要的页面就足够了。要记住前述的<FONT face="Times New Roman">1000</FONT>倍的速度差异,所以我们应该尽可能避免磁盘的<FONT face="Times New Roman">I/O</FONT>操作。</P><P 0cm 0cm 0pt">传统上特定平台上页面的大小是固定的,比如<FONT face="Times New Roman">x86</FONT>平台为<FONT face="Times New Roman">4K</FONT>,这可以简化分页操作。不过,大多数<FONT face="Times New Roman">CPU</FONT>为可变大小的页面提供硬件支持,通常能够达到<FONT face="Times New Roman">4M</FONT>或者更大。可变大小页面可以使分页操作执行更快和更有效,不过要以复杂性为代价。标准发行的<FONT face="Times New Roman">Linux</FONT>内核不支持可变大小页面,所以我们仍然假定页面大小是<FONT face="Times New Roman">4K</FONT>。(已经有支持<FONT face="Times New Roman">Cyrix</FONT>可变大小页面机制的补丁程序,但它们不是本书中官方发行版本的部分。而且据闻由此获得的性能增益也并不非常显著。)<p></p></P><P 0cm 0cm 0pt">因为分页可以完成交换所能完成的所有工作,而且更加有效,所以类似于<FONT face="Times New Roman">Linux</FONT>一样的现代操作系统已不再使用交换,严格的说是只使用分页技术。但是术语“交换”已得到了广泛使用,以至于实际应用中术语“交换”和“分页”已经几乎可以通用;由于内核使用分页技术,所以本书就遵从这种用法。<p></p></P><P 0cm 0cm 0pt"><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">flavors</FONT>)不同,<FONT face="Times New Roman">Linux</FONT>即使没有任何交换空间也能运行得很好。<p></p></P><H3 13pt 0cm"><FONT size=5>地址空间</FONT></H3><P 0cm 0cm 0pt">地址空间(<FONT face="Times New Roman">address space</FONT>)是一段表示内存位置的地址范围。地址空间有三种:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo1; tab-stops: list 42.25pt">l           物理地址空间</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo1; tab-stops: list 42.25pt">l           线性地址空间</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo1; tab-stops: list 42.25pt">l           逻辑地址空间,也被称为虚拟地址空间</P><P 0cm 0cm 0pt">(需要指出的是,<FONT face="Times New Roman">I/O</FONT>地址能够被看作是第四种地址空间,但是本书中对其不作讨论。)<p></p></P><P 0cm 0cm 0pt">物理地址是一个系统中可用的真实的硬件地址。假如一个系统有<FONT face="Times New Roman">64M</FONT>内存,它的合法地址范围是从<FONT face="Times New Roman">0</FONT>到<FONT face="Times New Roman">0x4000000</FONT>(以十六进制表示)。每个地址都对应于所安装的<FONT face="Times New Roman">SIMMs</FONT>中的一组晶体管,而且对应于处理器地址总线上的一组特定信号。<p></p></P><P 0cm 0cm 0pt">分页可以在一个进程的生存期里,把它或它的片段移入或者移出不同的物理内存区域(或不同物理地址)。这正是进程被分配一个逻辑地址空间的原因之一。就任何特定的进程来说,从<FONT face="Times New Roman">0</FONT>开始扩展到十六进制地址<FONT face="Times New Roman">0xc0000000</FONT>共<FONT face="Times New Roman">3GB</FONT>的地址空间是绰绰有余的。即使每个进程有相同的逻辑地址空间,相应进程的物理地址也都是不同的,因此它们不会彼此重叠。<p></p></P><P 0cm 0cm 0pt">从内核的角度看来,逻辑和物理地址都被划分成页面。因此,就像我们所说的逻辑和物理地址一样,可以称它们为逻辑和物理页面:每个合法的逻辑地址恰好处于一个逻辑页面中,物理地址也是这样的。<p></p></P><P 0cm 0cm 0pt">与之相反,线性地址通常不被认为是分页的。<FONT face="Times New Roman">CPU</FONT>(实际是下文中的<FONT face="Times New Roman">MMU</FONT>)会以一种体系结构特有的方式把进程使用的逻辑地址转换成线性地址。在<FONT face="Times New Roman">x86</FONT>平台上,这种转换是简单地把虚拟地址与另一地址,即进程的段基址相加;因为每个任务的基址都被设置为<FONT face="Times New Roman">0</FONT>,所以在这种体系结构中,逻辑地址和线性地址是相同的。得到的线性地址接着被转换成物理地址并与系统的<FONT face="Times New Roman">RAM</FONT>直接作用。<p></p></P><H3 13pt 0cm"><FONT size=5>内存管理单元</FONT></H3><P 0cm 0cm 0pt">在逻辑地址和物理地址之间相互转换的工作是由内核和硬件内存管理单元(<FONT face="Times New Roman">MMU</FONT>—<FONT face="Times New Roman">memory management unit</FONT>)共同完成的。<FONT face="Times New Roman">MMU</FONT>是被集成进现代的<FONT face="Times New Roman">CPU</FONT>里的,它们都是同一块<FONT face="Times New Roman">CPU</FONT>芯片内的一个部分,但是把<FONT face="Times New Roman">MMU</FONT>当作一个独立的部分仍然非常有益。内核告诉<FONT face="Times New Roman">MMU</FONT>如何为每个进程把某逻辑页面映射到某特定物理页面,而<FONT face="Times New Roman">MMU</FONT>在进程提出内存请求时完成实际的转换工作。</P><P 0cm 0cm 0pt">当地址转换无法完成时,比如,由于给定的逻辑地址不合法或者由于逻辑页面没有对应的物理页面的时候,<FONT face="Times New Roman">MMU</FONT>就给内核发出信号。这种情况称为页面错误(<FONT face="Times New Roman">page fault</FONT>),本章后面会对此进行详细论述。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">MMU</FONT>也负责增强内存保护,比如当一个应用程序试图在它的内存中对一个已标明是只读的页面进行写操作时,<FONT face="Times New Roman">MMU</FONT>就会通知<FONT face="Times New Roman">OS</FONT>。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">MMU</FONT>的主要好处在于速度。缺少<FONT face="Times New Roman">MMU</FONT>时为了获得同样的效果,<FONT face="Times New Roman">OS</FONT>将不得不使用软件为每个进程的每一次内存引用进行校验,这种校验同时包括数据和指令在内,而这可能还包括要用为进程创建其生存所需的虚拟机。(<FONT face="Times New Roman">Java</FONT>所进行的一些工作与此类似。)这样做的结果将使系统慢得令人无法忍受。但是一个以这种内存访问合法性检查方式集成在计算机硬件里的<FONT face="Times New Roman">MMU</FONT>却根本不会使系统变慢。在<FONT face="Times New Roman">MMU</FONT>建立起一个进程以后,内核就只是偶尔参与工作,例如在发生页面错误时,而这与全部内存引用数量相比是非常少的。</P><P 0cm 0cm 0pt">除此而外,<FONT face="Times New Roman">MMU</FONT>还可以协助保护内存自身。没有<FONT face="Times New Roman">MMU</FONT>,内核可能不能够防止一个进程非法侵入它自己的内存空间或者是其它进程的内存空间。但是如何避免内核也会作同样的操作呢?在<FONT face="Times New Roman">Intel’s 80486</FONT>或更新的芯片上(不是<FONT face="Times New Roman">80386</FONT>),<FONT face="Times New Roman">MMU</FONT>的内存保护特性也适用于内核进程。</P><H3 13pt 0cm"><FONT size=5>页目录和页表</FONT></H3><P 0cm 0cm 0pt">在<FONT face="Times New Roman">x86</FONT>体系结构上,把线性地址(或者逻辑地址——记住在<FONT face="Times New Roman">Linux</FONT>上,这二者具有相同的值)解析(<FONT face="Times New Roman">resolving</FONT>)到物理地址分为两个步骤,整个过程如图<FONT face="Times New Roman">8-2</FONT>所示。提供给进程的线性地址被分为三个部分:一个页目录索引,一个页表索引和一个偏移量。页目录(<FONT face="Times New Roman">page directory</FONT>)是一个指向页表的指针数组,页表(<FONT face="Times New Roman">page table</FONT>)是一个指向页面的指针数组,因此地址解析就是一个跟踪指针链的过程。一个页目录使你能够确定一个页表,继而得到一个页面,然后页面中的偏移量(<FONT face="Times New Roman">offset</FONT>)能够指出该页面里的一个地址。</P><P 0cm 0cm 0pt">为了进行更详细因而也会更准确的描述:给定页目录索引中的页目录项保存着贮存在物理内存上的一个页表地址;给定页表索引中的页表项保存着物理内存上相应物理页面的基地址;然后线性地址的偏移量加到这个物理地址上形成最终物理页面内的目的地址。</P><P 0cm 0cm 0pt">其它<FONT face="Times New Roman">CPU</FONT>使用三级转换方法,如图<FONT face="Times New Roman">8-3</FONT>所示。这在<FONT face="Times New Roman">64</FONT>位体系中尤其有用,以<FONT face="Times New Roman">Alpha</FONT>为例,其更大的<FONT face="Times New Roman">64</FONT>位的地址空间意味着类似于<FONT face="Times New Roman">x86</FONT>体系的地址转换将要求大量的页目录、大量页表、大量偏移量,或三者兼有。对于这种情况,<FONT face="Times New Roman">Alpha</FONT>的设计者们向线性地址模式中引入了另一层次,即<FONT face="Times New Roman">Linux</FONT>所称的页面中间目录(<FONT face="Times New Roman">page middle directory</FONT>),它位于页目录和页表之间。</P><P 0cm 0cm 0pt">这个方案与以前实际是一样的,只不过多增加了一级。这种三级转换方法同样具有页目录,页目录的每一项包含一个页面中间目录的入口地址,页面中间目录的每一项包含一个页表的入口地址,而页表也同以前一样每一项包含物理内存中一个页面的地址,这个地址再加上偏移量就得到了最终的地址。</P><P 0cm 0cm 0pt">而使情况更为复杂的是,通过进一步观察可知,三部分地址模式与两级地址转换是相关联的,而四部分地址模式则与三级地址转换相关联的,这是由于我们通常所说的“级(或层次<FONT face="Times New Roman">levels</FONT>)”不包括索引到页目录的第一步(我想是因为这一步没有进行转换的缘故)。</P><P 0cm 0cm 0pt">令人奇怪的是内核开发者们决定只用其中一种模式来处理问题。绝大部分的内核代码对<FONT face="Times New Roman">MMU</FONT>一视同仁,就如同<FONT face="Times New Roman">MMU</FONT>都使用三级转换方法(也就是四部分地址模式)一样。在<FONT face="Times New Roman">x86</FONT>平台上,通过将页面中间目录定义为<FONT face="Times New Roman">1</FONT>,页面相关的宏可以把三级分解过程完美地转换到二级分解过程上去。这些宏认为页面中间目录和页目录是几乎可以进行相互替换的等价品,以至于内核的主要代码可以认为其地址就是由四个部分组成的。</P><P 0cm 0cm 0pt">在<FONT face="Times New Roman">x86</FONT>系统中,<FONT face="Times New Roman">32</FONT>位地址中<FONT face="Times New Roman">10</FONT>位是页目录索引,接下来<FONT face="Times New Roman">10</FONT>位是页表索引,剩下的<FONT face="Times New Roman">12</FONT>位用作偏移量,这就构成了<FONT face="Times New Roman">4K</FONT>大小的页面(<FONT face="Times New Roman">2<SUP>12</SUP></FONT>等于<FONT face="Times New Roman">4096</FONT>个偏移量)。</P><P 0cm 0cm 0pt">用于创建和操作每一级项的函数和宏定义在头文件<FONT face="Times New Roman">include/asm-i386/page.h</FONT>(第<FONT face="Times New Roman">10786</FONT>行)和<FONT face="Times New Roman">include/asm-i386/pgtable.h</FONT>(第<FONT face="Times New Roman">10876</FONT>行)之中。在读者浏览这些函数和宏的时候,记住<FONT face="Times New Roman">PGD</FONT>通常代表“页目录项(<FONT face="Times New Roman">page directory entry</FONT>)”(不只是“页目录”),<FONT face="Times New Roman">PMD</FONT>通常代表“页面中间目录项(<FONT face="Times New Roman">page middle directory entry</FONT>)”(不只是“页面中间目录”),同样<FONT face="Times New Roman">PTE</FONT>也通常代表“页表项”。而且,正如上面解释中限定词“通常”所暗示的那样,例外是存在的,例如下文将要提到的<B normal"><FONT face="Times New Roman">pte_alloc</FONT></B>就分配页表而不是(如你所可能会认为的)页表项。非常遗憾的是,由于篇幅所限我们不能对全部例程进行讨论,我们将在后面对其中的一部分进行讨论。</P><P 0cm 0cm 0pt">页表项不仅记录了一个页面的基地址,而且记录了它的保护信息(<FONT face="Times New Roman">protections</FONT>),也就是一组指定该页为可读、可写,和<FONT face="Times New Roman">/</FONT>或可执行的标志(这容易让人联想到文件的保护位)。<wrapblock><v:shapetype><FONT face="Times New Roman"> <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></FONT></v:shapetype><v:shape><v:imagedata></v:imagedata><w:wrap type="topAndBottom"></w:wrap></v:shape></wrapblock><BR vglayout" clear=all>随着我们对页面保护信息的进一步剖析,读者会看到页表项所包括的其它页面特有的标志。</P><P 0cm 0cm 0pt"><wrapblock><v:shape><v:imagedata></v:imagedata><w:wrap type="topAndBottom"></w:wrap></v:shape></wrapblock><BR vglayout" clear=all></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><H3 13pt 0cm"><FONT size=5>转换后备缓存(<FONT face="Times New Roman">Translation Lookaside Buffers</FONT>:<FONT face="Times New Roman">TLBs)</FONT></FONT></H3><P 0cm 0cm 0pt">如果简单的执行从线性地址到物理地址的转换过程,在跟踪指针链时将会需要几个内存引用。<FONT face="Times New Roman">RAM</FONT>虽然不像磁盘那么慢,但是仍然比<FONT face="Times New Roman">CPU</FONT>要慢的多,这样就容易形成性能的瓶颈。为了减少这种开销,最近被执行过的地址转换结果将被存储在<FONT face="Times New Roman">MMU</FONT>的转换后备缓存(<FONT face="Times New Roman">translation lookaside buffers</FONT>:<FONT face="Times New Roman">TLBs</FONT>)内。除了偶尔会通知<FONT face="Times New Roman">CPU</FONT>,由于内核的某操作致使<FONT face="Times New Roman">TLBs</FONT>无效之外,<FONT face="Times New Roman">Linux</FONT>不用明确管理<FONT face="Times New Roman">TLBs</FONT>。</P><P 0cm 0cm 0pt">在作用于<FONT face="Times New Roman">TLB</FONT>的函数和宏中,我们只研究一下<B normal"><FONT face="Times New Roman">__flush_tlb</FONT></B>,在<FONT face="Times New Roman">x86</FONT>平台上,它是其它大部分函数和宏的基础。</P><P 3pt 0cm 3.2pt"><b>__flush_tlb</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10917</FONT>:<FONT face="Times New Roman">CR3</FONT>(控制寄存器<FONT face="Times New Roman">3</FONT>)是<FONT face="Times New Roman">x86CPU</FONT>寄存器,它保存页目录的基地址。往这个寄存器送入一个值将会使<FONT face="Times New Roman">CPU</FONT>认为<FONT face="Times New Roman">TLBs</FONT>变成无效,甚至写入与<FONT face="Times New Roman">CR3</FONT>已有值相同的值也是这样。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">       </FONT>因此,<B normal"><FONT face="Times New Roman">__flush_tlb</FONT></B>仅是两条汇编程序指令:它把<FONT face="Times New Roman">CR3</FONT>的值保存在临时变量<B normal"><FONT face="Times New Roman">tmpreg</FONT></B>里,然后立刻把<B normal"><FONT face="Times New Roman">tmpreg</FONT></B>的值拷贝回<FONT face="Times New Roman">CR3</FONT>中,整个过程就这么简单!</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">       </FONT>注意<FONT face="Times New Roman">x86</FONT>系统也允许使某一个单独的<FONT face="Times New Roman">TLB</FONT>项无效,而并不一定非要使全部项,这种方法使用<B normal"><FONT face="Times New Roman">invipg</FONT></B>指令——参见第<FONT face="Times New Roman">10926</FONT>行它的使用信息。</P><H3 13pt 0cm"><FONT size=5>段</FONT></H3><P 0cm 0cm 0pt">由于段不是在所有<FONT face="Times New Roman">CPU</FONT>中均可用,所以<FONT face="Times New Roman">Linux</FONT>内核中与体系结构无关的部分不能对段进行辨识。在不同的<FONT face="Times New Roman">CPU</FONT>体系中,段的(如果段在体系中是可用的)处理方式大相径庭,这一点是非常重要的。因此,我们在这个问题上不会花费太多时间,不过<FONT face="Times New Roman">x86</FONT>系统上内核使用段的方式还是值得大概讨论一下的。</P><P 0cm 0cm 0pt">段可以被简单的看作是定义内存区域的另一种机制,有些类似于页。这两种机制可以重叠:地址总是在页面之内,也可能处于段内。与页不同,段的大小可以变化,甚至在其生存期里能够增长和收缩。与页相同的是,段可以被保护,而且其保护可由硬件实施;当<FONT face="Times New Roman">x86</FONT>的段保护和同一地址的页保护发生冲突时,段保护优先。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">X86</FONT>系统使用一些寄存器和全局描述表(<FONT face="Times New Roman">GDT</FONT>)和局部描述表(<FONT face="Times New Roman">LDT</FONT>)这两种表来对段进行跟踪。描述符(<FONT face="Times New Roman">descriptor</FONT>)是段的描述信息,它是用来保存段的大小和基址以及段的保护信息的<FONT face="Times New Roman">8</FONT>字节的对象。<FONT face="Times New Roman">GDT</FONT>在系统中只有一个,而<FONT face="Times New Roman">Linux</FONT>可以为每个任务建立一个<FONT face="Times New Roman">LDT</FONT>。</P><P 0cm 0cm 0pt">接下来我们将简单解释内核是如何使用这些表来建立段的。内核本身拥有分离的代码和数据段,它们被记述在<FONT face="Times New Roman">GDT</FONT>的第<FONT face="Times New Roman">2</FONT>和第<FONT face="Times New Roman">3</FONT>行项里。每个任务也有分离的代码和数据段。当前任务的代码段和数据段不仅在它自己的<FONT face="Times New Roman">LDT</FONT>的第<FONT face="Times New Roman">0</FONT>和第<FONT face="Times New Roman">1</FONT>行项被说明,而且还被记述在<FONT face="Times New Roman">GDT</FONT>的第<FONT face="Times New Roman">4</FONT>和第<FONT face="Times New Roman">5</FONT>行项里。</P><P 0cm 0cm 0pt">在<FONT face="Times New Roman">GDT</FONT>里,每个任务占两行项,一个用来定位它的<FONT face="Times New Roman">LDT</FONT>,一个用来定位它的<FONT face="Times New Roman">TSS</FONT>(前面章节曾简要提及的任务状态段)。因为<FONT face="Times New Roman">x86CPU</FONT>限制<FONT face="Times New Roman">GDT</FONT>的大小为<FONT face="Times New Roman">8192</FONT>个项,而且<FONT face="Times New Roman">Linux</FONT>为每个任务占用两行<FONT face="Times New Roman">GDT</FONT>项,因此显而易见的是我们不能拥有超过<FONT face="Times New Roman">4096</FONT>个任务,这也正是在第<FONT face="Times New Roman">7</FONT>章里提到的限制。事实上,任务的最大数目要稍小一点儿,不过仍有<FONT face="Times New Roman">4090</FONT>个,这是由于<FONT face="Times New Roman">GDT</FONT>的前<FONT face="Times New Roman">12</FONT>行项被保留用于其它目的。</P><P 0cm 0cm 0pt">富有经验的<FONT face="Times New Roman">x86</FONT>程序员可能已经注意到<FONT face="Times New Roman">Linux</FONT>所使用的<FONT face="Times New Roman">x86</FONT>分段机制是采用最低限度方式的;段的主要使用目的仅是为了避免用户代码出现在内核段中。<FONT face="Times New Roman">Linux</FONT>更倾向于分页机制。从大的方面来看,对于处理器来说分页或多或少都是相同的,或者说总的事实就是这样,因此内核越是以分页方式工作,它的可移植性就越好。</P><P 0cm 0cm 0pt">最后要提及的是,如果读者对于<FONT face="Times New Roman">x86</FONT>的分段机制很感兴趣的话,不妨阅读一下<FONT face="Times New Roman">Intel</FONT>体系结构下的软件开发手册第<FONT face="Times New Roman">3</FONT>卷(<FONT face="Times New Roman">Intel Architecture Software Developer’s Manual Volume 3</FONT>),该书可以从<FONT face="Times New Roman">Intel</FONT>站点上免费得到(<B normal"><FONT face="Times New Roman">developer.intel.com/design/pentiumii/manuals/243192. htm</FONT></B>)。</P><H2 13pt 0cm">进程的内存组织</H2><P 0cm 0cm 0pt">有三个重要的数据结构用于表示进程的内存使用:<B normal"><FONT face="Times New Roman">struct vm_area_struct</FONT></B>(第<FONT face="Times New Roman">15113</FONT>行)、<B normal"><FONT face="Times New Roman">struct vm_operations_struct</FONT></B>(第<FONT face="Times New Roman">15171</FONT>行),和<B normal"><FONT face="Times New Roman">struct mm_struct</FONT></B>(第<FONT face="Times New Roman">16270</FONT>行)。我们随后将对这三个数据结构进行逐一介绍。</P><P 3pt 0cm 3.2pt"><b><FONT size=5>Struct vm_area_struct</FONT></b></P><P 0cm 0cm 0pt">内核使用一个或更多的<B normal"><FONT face="Times New Roman">struct vm_area_struct</FONT></B>来跟踪进程使用的内存区域,该结构体通常缩写为<FONT face="Times New Roman">VMAs</FONT>。每个<FONT face="Times New Roman">VMA</FONT>代表进程地址空间的一块单独连续的区间。对于一个给定的进程,两个<FONT face="Times New Roman">VMAs</FONT>决不会重叠,一个地址最多被一个<FONT face="Times New Roman">VMA</FONT>所覆盖;进程从未访问过的的一个地址将不会在任何一个<FONT face="Times New Roman">VMA</FONT>中。</P><P 0cm 0cm 0pt">两个<FONT face="Times New Roman">VMA</FONT>之间的区别有两个特征:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo2; tab-stops: list 42.25pt">l           两个<FONT face="Times New Roman">VMA</FONT>可以不连续(<FONT face="Times New Roman">Two VMAs may be discontiguous</FONT>)——换句话说,一个<FONT face="Times New Roman">VMA</FONT>的末尾不一定是另一个的开头。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo3; tab-stops: list 42.25pt">l           两个<FONT face="Times New Roman">VMA</FONT>的保护模式可以不同(<FONT face="Times New Roman">Two VMAs may have different protections</FONT>)——例如,一个是可写的而另一个可能是不可写的。即使两个这样的<FONT face="Times New Roman">VMA</FONT>是连在一起的,它们也必须被分开管理,因为其不同的保护信息。</P><P 0cm 0cm 0pt">应注意的一个重点是,一个地址可以被一个<FONT face="Times New Roman">VMA</FONT>所覆盖,即使内核并没有分配一个页面来存贮这个地址。<FONT face="Times New Roman">VMA</FONT>的一个主要应用就是在页面错误时决定如何作出反应。我们可以将<FONT face="Times New Roman">VMAS</FONT>看作是一个进程所占用的内存空间以及这些空间的保护模式的总体视图。内核能够反复重新计算从页表而来的<FONT face="Times New Roman">VMA</FONT>中的大部分信息,不过那样速度会相当慢。<p></p></P><P 0cm 0cm 0pt">进程的所有<FONT face="Times New Roman">VMA</FONT>是以一个排序的双向链表方式存储的,并且它们使用自己的指针来管理该列表。当一个进程有多于<B normal"><FONT face="Times New Roman">avl_min_map_count</FONT></B>数目(在第<FONT face="Times New Roman">16286</FONT>行定义为<FONT face="Times New Roman">32</FONT>)的<FONT face="Times New Roman">VAM</FONT>时,内核也会创建一个<FONT face="Times New Roman">AVL</FONT>数来存储它们,此时仍然是使用<FONT face="Times New Roman">VMAs</FONT>自己的指针对该树进行管理。<FONT face="Times New Roman">AVL</FONT>树是一个平衡二叉树结构,因此这种方法在<FONT face="Times New Roman">VMA</FONT>数量巨大时查找效率十分高。不过,即使在<FONT face="Times New Roman">AVL</FONT>树被创建后,线性列表也会被保留以便内核即使不使用递归也能轻松的遍历一个进程的所有<FONT face="Times New Roman">VMA</FONT>。<p></p></P><P 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">Struct_vm_area_struct</FONT></B>的两个最重要的元素是它的<B normal"><FONT face="Times New Roman">vm_start</FONT></B>和<B normal"><FONT face="Times New Roman">vm_end</FONT></B>成员(分别在第<FONT face="Times New Roman">15115</FONT>行和<FONT face="Times New Roman">15116</FONT>行),它们定义了<FONT face="Times New Roman">VMA</FONT>所覆盖的起止范围,其中<B normal"><FONT face="Times New Roman">vm_start</FONT></B>是<FONT face="Times New Roman">VMA</FONT>中的最小地址,而<B normal"><FONT face="Times New Roman">vm_end</FONT></B>是比<FONT face="Times New Roman">VMA</FONT>最大地址大一位的地址。在本章后面我们会反复提及这些成员。<p></p></P><P 0cm 0cm 0pt">注意,<B normal"><FONT face="Times New Roman">vm_start</FONT></B>和<B normal"><FONT face="Times New Roman">vm_end</FONT></B>的类型是<B normal"><FONT face="Times New Roman">unsigned long</FONT></B>,而不是读者可能会认为的<B normal"><FONT face="Times New Roman">void*</FONT></B>。由于这个原因,内核在所有表示地址的地方都使用<B normal"><FONT face="Times New Roman">unsigned long</FONT></B>类型,而不用<B normal"><FONT face="Times New Roman">void*</FONT></B>类型。采用这种方法的部分原因是可以避免因内核对诸如比特一级的地址进行计算操作时引起的编译警告,还可能避免由于它们而偶然引起的间接错误。在引用内核空间的一个数据结构的地址时,内核代码使用指针变量;在对用户空间的地址进行操作时,内核却频繁的使用<B normal"><FONT face="Times New Roman">unsigned long</FONT></B>——实际上,几乎只有本章中所涉及的代码才是这样。<p></p></P><P 0cm 0cm 0pt">这样就给用来编译内核的编译器提出了要求。使用<B normal"><FONT face="Times New Roman">unsigned long</FONT></B>作为地址类型就意味着编译器必须使<B normal"><FONT face="Times New Roman">unsigned long</FONT></B>的类型长度和<B normal"><FONT face="Times New Roman">void*</FONT></B>的一样,尽管实践中对这一点的要求不是十分严格。对于<FONT face="Times New Roman">x86</FONT>寄存器上的<FONT face="Times New Roman">gcc</FONT>来说,两种类型很自然的都是<FONT face="Times New Roman">32</FONT>位长。在<FONT face="Times New Roman">64</FONT>位指针长度的体系中,比如<FONT face="Times New Roman">Alpha</FONT>,<FONT face="Times New Roman">gcc</FONT>的<B normal"><FONT face="Times New Roman">unsigned long</FONT></B>类型长度通常也是<FONT face="Times New Roman">64</FONT>位。尽管如此,在将来的体系结构上,<FONT face="Times New Roman">gcc</FONT>的一个端口可能提供与<B normal"><FONT face="Times New Roman">void*</FONT></B>不同的<B normal"><FONT face="Times New Roman">unsigned long</FONT></B>类型长度,这是需要内核的移植版本开发人员(<FONT face="Times New Roman">kernel porters</FONT>)注意的一点。<p></p></P><P 0cm 0cm 0pt">还要说明的是,除了<FONT face="Times New Roman">gcc</FONT>之外你不需要对编译器的性能有太多担心,因为其它大部分与<FONT face="Times New Roman">gcc</FONT>相关的特性都已经包括在代码之中了。假如读者试图用某个其它的编译器来编译内核的话,我想有关<B normal"><FONT face="Times New Roman">unsigned long</FONT></B>和<B normal"><FONT face="Times New Roman">void*</FONT></B>长度的错误将会占编译错误列表的绝大多数。<p></p></P><P 3pt 0cm 3.2pt"><b><FONT size=5>Struct vm_operations_struct</FONT></b></P><P 0cm 0cm 0pt">一个<FONT face="Times New Roman">VMA</FONT>可能代表一段平常的内存区间,就像是<B normal"><FONT face="Times New Roman">malloc</FONT></B>函数所返回的那样。但是它也可以是对应于一个文件、共享内存、交换设备,或是其它特别的对象而建立的一块内存区域;这种对应关系是由本章后面将要涉及的称为<B normal"><FONT face="Times New Roman">mmap</FONT></B>的系统调用所确定的。</P><P 0cm 0cm 0pt">我们不想牵扯太多关于<FONT face="Times New Roman">VMA</FONT>可以被映射的每一种对象的专门知识,这会使对内核代码的剖析变得凌乱不堪,因为那样就不得不反复决定是否要关闭一个文件、分离共享内存等等令人非常头疼的事情。与此不同,对象类型<B normal"><FONT face="Times New Roman">struct vm_operations_struct</FONT></B>抽象了各种可能提供给被映射对象的操作,比如打开、关闭之类。一个<B normal"><FONT face="Times New Roman">struct vm_operations_struct</FONT></B>结构体就是一组函数指针,它们之中可能会是<B normal"><FONT face="Times New Roman">NULL</FONT></B>用来表示一个操作对某个被映射对象是不可用的。举例来说,在一个共享内存没有被映射的情况下,把该共享内存对象的页面与磁盘进行同步是没有意义的,表示共享内存操作的<B normal"><FONT face="Times New Roman">struct vm_operations_struct</FONT></B>里的<B normal"><FONT face="Times New Roman">sync</FONT></B>成员就是<B normal"><FONT face="Times New Roman">NULL</FONT></B>。</P><P 0cm 0cm 0pt">总之,一旦<FONT face="Times New Roman">VMA</FONT>映射为一个对象,那么它的<FONT face="Times New Roman"><B normal">vm_op</B>s</FONT>成员就会是一个非空的指针,指向一个表示被映射对象所提供操作的<B normal"><FONT face="Times New Roman">struct vm_operations_struct</FONT></B>结构体。对于<FONT face="Times New Roman">VMA</FONT>可以映射的每一种对象类型,都有一个该<FONT face="Times New Roman">VMA</FONT>可能会在某处指向的静态<FONT face="Times New Roman"><B normal">static</B> <B normal">struct vm _ operations_struct</B></FONT>结构体。参见第<FONT face="Times New Roman">21809</FONT>行这样的一个例子。</P><P 3pt 0cm 3.2pt"><b><FONT size=5>Struct mm_struct</FONT></b></P><P 0cm 0cm 0pt">一个进程所保留的所有<FONT face="Times New Roman">VMA</FONT>都是由<B normal"><FONT face="Times New Roman">struct mm_struct</FONT></B>结构体来管理的。指向这种结构类型的指针在<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>中,确切的说,它就是后者的<B normal"><FONT face="Times New Roman">mm</FONT></B>成员。这个成员被前一章中所讨论的<B normal"><FONT face="Times New Roman">goodness</FONT></B>(第<FONT face="Times New Roman">26388</FONT>行)应用,来判断是否两个任务是在同一个线程组中。两个具有相同<B normal"><FONT face="Times New Roman">mm</FONT></B>成员(正如我们所见到的)的任务管理同一块全局内存区域,这也是线程的一个特点。</P><P 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">struct mm_struct</FONT></B>结构体的<B normal"><FONT face="Times New Roman">mmap</FONT></B>成员(第<FONT face="Times New Roman">16271</FONT>行)就是前述的<FONT face="Times New Roman">VMA</FONT>的链接列表,而它的<B normal"><FONT face="Times New Roman">mmap_avl</FONT></B>成员,如果非空,就是<FONT face="Times New Roman">VMA</FONT>的<FONT face="Times New Roman">AVL</FONT>树。读者可以浏览<B normal"><FONT face="Times New Roman">struct mm_struct</FONT></B>的定义,会发现它还包括相当多的其它成员,它们中的几个会在本章中涉及到。</P><H3 13pt 0cm"><FONT size=5><FONT face="Times New Roman">VMA</FONT>的操作</FONT></H3><P 0cm 0cm 0pt">本小节介绍后面要用到的<B normal"><FONT face="Times New Roman">find_vma</FONT></B>函数,并捎带简介它的同类函数<B normal"><FONT face="Times New Roman">find_vma_prev</FONT></B>。这将阐明<FONT face="Times New Roman">VMA</FONT>处理操作的一些方面,也为读者将要接触的代码做准备。</P><P 3pt 0cm 3.2pt"><b>find_vma</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33460</FONT>:简单说来,<B normal"><FONT face="Times New Roman">find_vma</FONT></B>函数的工作就是找到包含某特定地址的第一个<FONT face="Times New Roman">VMA</FONT>。更准确<FONT face="Times New Roman">   </FONT>的说,它的工作是找到其<B normal"><FONT face="Times New Roman">vm_end</FONT></B>比给定地址大的第一个<FONT face="Times New Roman">VMA</FONT>,这个地址可能会在该<FONT face="Times New Roman">VMA</FONT>之外,因为它可以比<FONT face="Times New Roman">VMA</FONT>的<B normal"><FONT face="Times New Roman">vm_start</FONT></B>要小。这个函数返回指向<FONT face="Times New Roman">VMA</FONT>的指针,如果没有满足要求的<FONT face="Times New Roman">VMA</FONT>就返回<B normal"><FONT face="Times New Roman">NULL</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33468</FONT>:首先,通过使用<B normal"><FONT face="Times New Roman">mm</FONT></B>的<B normal"><FONT face="Times New Roman">mmap_cache</FONT></B>成员,满足进程最近一次请求的同一<FONT face="Times New Roman">VMA</FONT>会被检查,而<B normal"><FONT face="Times New Roman">mmap_cache</FONT></B>正是为此目的而设。我没有亲自测试过,不过这个函数的文档中说高速缓存的命中率可以达到<FONT face="Times New Roman">35%</FONT>,考虑到高速缓存只由一个<FONT face="Times New Roman">VMA</FONT>组成,那么这个数字就相当好了。当然,著名的、被称之为“引用的局部性(<FONT face="Times New Roman">locality of reference</FONT>)”的特性一直在其中提供了很大帮助,这也是软件访问数据(和指令)时的一条原则,即访问最近使用过的数据(和指令)。由于<FONT face="Times New Roman">VMA</FONT>包含一块连续的地址区间,引用的局部性就使得所需的地址都在同一个<FONT face="Times New Roman">VMA</FONT>中变为可能,而这样的<FONT face="Times New Roman">VMA</FONT>就会满足前面的要求。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm">在修改<FONT face="Times New Roman">VMA</FONT>列表的其它几个地方,这个高速缓存的值被设为空,表明对<FONT face="Times New Roman">VMA</FONT>列表所做的修改可能会使它失效。至少在一个这种情况中,第<FONT face="Times New Roman">33953</FONT>行,使该高速缓存为空不总是必要的;这段代码如果能够再聪明一些的话,就可能从本质上改善高速缓存的命中率。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33471</FONT>:高速缓存没有命中。假如没有<FONT face="Times New Roman">AVL</FONT>树,<B normal"><FONT face="Times New Roman">find_vma</FONT></B>只是搜索列表上的所有<FONT face="Times New Roman">VMA</FONT>,然后返回第一个符合条件的<FONT face="Times New Roman">VMA</FONT>。回想一下<FONT face="Times New Roman">VMA</FONT>的列表是保持顺序的,所以符合条件的<FONT face="Times New Roman">VMA</FONT>也就是所有符合条件的<FONT face="Times New Roman">VMA</FONT>中地址最小的一个。假如搜索到列表的末尾都没有一个匹配,<B normal"><FONT face="Times New Roman">vma</FONT></B>就被置为<B normal"><FONT face="Times New Roman">NULL</FONT></B>,并被返回。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33476</FONT>:若有大量<FONT face="Times New Roman">VMA</FONT>,沿树遍历就比沿链表遍历要快;由于<FONT face="Times New Roman">AVL</FONT>树是平衡的,这就是一种对数时间操作而不是线性时间操作。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm">树的迭代遍历并不是十分少见的现象,不过一些特征也并不非常明显。首先注意第<FONT face="Times New Roman">33484</FONT>行的赋值;这个操作一直跟踪当前找到的最好节点,当不能找到更好的时,它就会被返回。接着的下一行中的<B normal"><FONT face="Times New Roman">if</FONT></B>语句是一个最优测试,检测<B normal"><FONT face="Times New Roman">addr</FONT></B>是否处于<FONT face="Times New Roman">VMA</FONT>中(我们已知的一点是<B normal"><FONT face="Times New Roman">addr</FONT></B>小于<FONT face="Times New Roman">VMA</FONT>的<B normal"><FONT face="Times New Roman">vm_end</FONT></B>)。因为<FONT face="Times New Roman">VMA</FONT>绝对不会彼此覆盖,没有其它<FONT face="Times New Roman">VMA</FONT>将是一个较贴近的匹配结果,所以树的遍历可以早些结束。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33492</FONT>:如果在树的遍历或列表搜索过程中找到一个<FONT face="Times New Roman">VMA</FONT>,找到的值就被保存在高速缓存里以便下一次查找。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33496</FONT>:在任何情况下,<B normal"><FONT face="Times New Roman">vma</FONT></B>都被返回;它的值或者是<B normal"><FONT face="Times New Roman">NULL</FONT></B>,或者是满足查找条件的第一个<FONT face="Times New Roman">VMA</FONT>。</P><P 3pt 0cm 3.2pt"><b>Find_vma_prev</b></P><P 0cm 0cm 0pt">如前所述,这个函数(从第<FONT face="Times New Roman">33501</FONT>行开始)和<B normal"><FONT face="Times New Roman">find_vma</FONT></B>函数是一样的,不过它还会额外的返回一个指向前一个限定的<B normal"><FONT face="Times New Roman">addr</FONT></B>(如果有)的<FONT face="Times New Roman">VMA</FONT>的指针。这个函数不仅是因为它本身的缘故而令人感兴趣,更主要是由于它的出现会告诉我们一些关于内核程序设计,特别是关于<FONT face="Times New Roman">Linux</FONT>内核程序设计的信息。</P><P 0cm 0cm 0pt">应用程序员很可能已经在更加通用的<B normal"><FONT face="Times New Roman">find_vma_prev</FONT></B>函数之上写出了<B normal"><FONT face="Times New Roman">find_vma</FONT></B>函数,这只需简单的把指向<FONT face="Times New Roman">VMA</FONT>的指针去掉即可,代码如下:</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">p504.1</FONT></P><P 0cm 0cm 0pt">应用程序员这样做的原因是具有代表性的应用程序并不太拘于速度因素。这并非纯粹是在为铺张浪费找借口,而是由于<FONT face="Times New Roman">CPU</FONT>速度的不断增加使得应用能够更关注于其它方面,我们现在可以出于可维护性的充分理由而提供一个可以到处使用的额外函数调用。</P><P 0cm 0cm 0pt">与之相反,一个内核程序员可能不会随便增加多余的函数调用;试着减少几个<FONT face="Times New Roman">CPU</FONT>时钟周期会被认为比负责维护某个近乎是副本的函数要更胜一筹。即使没有其它原因,我们也可以说内核开发者所持有的这种态度就是为了让应用程序员能相对自由一些。</P><P 0cm 0cm 0pt">为什么这种重复对于源代码相对封闭的操作系统,<FONT face="Times New Roman">Linux</FONT>而言不那么重要,这里是否有更深层次原因呢?尽管<FONT face="Times New Roman">Linux</FONT>内核必须限制它占用的<FONT face="Times New Roman">CPU</FONT>时间,<FONT face="Times New Roman">Linux</FONT>内核的开发工作却不受程序员时间的限制。(明确的说,我必须要指出<FONT face="Times New Roman">Linux</FONT>的开发者不必把他们的时间浪费在会议上的,他们也不必被人工制订的时间表所拘束。)正是由于这众多的队伍,众人的智慧,才改变了软件开发的规则。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">Linux</FONT>内核的源代码对任何人都是公开的,<FONT face="Times New Roman">Linus</FONT>本人曾说过的一句名言是“<FONT face="Times New Roman">…</FONT>只要眼睛够多,所有的臭虫(程序错误)都是浅薄的<a href="http://www.madio.net/bbs/dispbbs.asp?boardid=84&amp;id=3190&amp;star=2#_ftn1" target="_blank" >[1]</A>”。就算函数<FONT face="Times New Roman">find_vma</FONT>和<FONT face="Times New Roman">find_vma_prev</FONT>的执行会产生重大差异,在你能想到“重编译”之前,不知什么地方的某个<FONT face="Times New Roman">Linux</FONT>内核开发者就已经迅速发现并修复了这个问题。实际上,<FONT face="Times New Roman">Linux</FONT>内核开发者比它的商业对手动作快得多,所得到的代码运行更快而且错误更少,尽管有时偶然出现的结构会被认为在任何其它环境中都不可维护。</P><P 0cm 0cm 0pt">当然,如果没有人负责对这些函数的改进进行维护的话,我认为这也是非常愚蠢的。内核的下一个发布版本就把它们合并了。但是我仍然对此持怀疑态度,而且即使我在这个具体问题上所持的态度并不正确,我依然在总体上保持原有态度。<FONT face="Times New Roman"> </FONT>不同的事还会继续不同,而不同正是<FONT face="Times New Roman">Linux</FONT>之所以为<FONT face="Times New Roman">Linux</FONT>的一方面。<p></p></P><H3 13pt 0cm"><FONT size=5>分页</FONT></H3><P 0cm 0cm 0pt">本章前面对分页已作了概要描述,现在我们进一步来研究<FONT face="Times New Roman">Linux</FONT>是如何处理分页的。<p></p></P><H4 14pt 0cm 14.5pt"><FONT size=5>页面保护详述</FONT></H4><P 0cm 0cm 0pt">正如早先提及的,页表项不仅保存了一个页面的基地址,还有其它一些标志信息,这些标志指出了该页面上所能进行的操作。现在是仔细研究一下这些标志的时候了。</P><P 0cm 0cm 0pt">如果页表项只保存一个页面的基地址,并且页面是页对准的(<FONT face="Times New Roman">page-aligned</FONT>),这个地址的低<FONT face="Times New Roman">12</FONT>位(<FONT face="Times New Roman">x86</FONT>系统),即偏移量部分通常将总是为<FONT face="Times New Roman">0</FONT>。取代这些位置<FONT face="Times New Roman">0</FONT>的作法是把它们编码作为与页面有关的标志,在获取地址时只需简单的把它们屏蔽掉就行了。以下就是这<FONT face="Times New Roman">12</FONT>位中的标志位:</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo4; tab-stops: list 21.25pt">l           <B normal"><FONT face="Times New Roman">_PAGE_PRESENT</FONT></B>位(第<FONT face="Times New Roman">11092</FONT>行),若置位,当前页面物理存在于<FONT face="Times New Roman">RAM</FONT>中。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo4; tab-stops: list 21.25pt">l           <B normal"><FONT face="Times New Roman">_PAGE_RW</FONT></B>位(第<FONT face="Times New Roman">11093</FONT>行),置为<FONT face="Times New Roman">0</FONT>表示该页面是只读的,置为<FONT face="Times New Roman">1</FONT>表示可读可写。因此,没有只写的页面。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo4; tab-stops: list 21.25pt">l           <B normal"><FONT face="Times New Roman">_PAGE_USER</FONT></B>位(第<FONT face="Times New Roman">11094</FONT>行),置位表示某页面是用户空间页面,清空表示为内核空间页面。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo4; tab-stops: list 21.25pt">l           <B normal"><FONT face="Times New Roman">_PAGE_WT</FONT></B>位(第<FONT face="Times New Roman">11095</FONT>行),置为<FONT face="Times New Roman">1</FONT>表示页面高速缓存管理策略是透写,置为<FONT face="Times New Roman">0</FONT>表示管理策略是回写。透写(<FONT face="Times New Roman">writethrough</FONT>)会立刻把写入高速缓存的数据复制(拷贝)到主存储器内,即使保存在高速缓存的数据仍是读访问。与之相反,回写(<FONT face="Times New Roman">writeback</FONT>)具有更高的效率,写入高速缓存的数据仅当其必须为其它数据腾出空间,而必须移出时才被复制到主存储器内。(这是由硬件,而不是<FONT face="Times New Roman">Linux</FONT>完成的。)尽管直到本书写作之时,这个标志位在内核中的使用还并不非常普遍,不过这种情形有望很快改变。有时候,<FONT face="Times New Roman">Intel</FONT>公司的处理器资料中把<FONT face="Times New Roman">WT</FONT>位更多的称为<FONT face="Times New Roman">PWT</FONT>。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo4; tab-stops: list 21.25pt">l           <B normal"><FONT face="Times New Roman">_PAGE_PCD</FONT></B>位(第<FONT face="Times New Roman">11096</FONT>行),关闭页面高速缓存;本书中的代码不总是使用这个标志位。(缩写“<FONT face="Times New Roman">CD</FONT>”表示“<FONT face="Times New Roman">caching disabled</FONT>”。)如果我们恰好知道一个不经常使用的页面,那么就不必为它设置高速缓存,这可能会更有效率。这个标志位好像对于映射内存的<FONT face="Times New Roman">I/O</FONT>设备来说更有用处,尽管我们想确保对表示设备的内存进行的写操作不被高速缓存缓冲,但是取而代之的作法是立刻把数据直接拷贝到设备之中。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo4; tab-stops: list 21.25pt">l           <B normal"><FONT face="Times New Roman">_PAGE_ACCESSED</FONT></B>位(第<FONT face="Times New Roman">11097</FONT>行),若置位表示该页面最近曾被访问过。<FONT face="Times New Roman">Linux</FONT>可以设置或清除这个标志,不过通常这是由硬件完成的。因为清除了该标志的页面已很久未被使用过,所以它们会在交换时被优先调出主存。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo4; tab-stops: list 21.25pt">l           <B normal"><FONT face="Times New Roman">_PAGE_DIRTY</FONT></B>位(第<FONT face="Times New Roman">11098</FONT>行),若置位,表明该页面的内容自从上次该位被清除后已发生改变。这就意味着它是一个内容没有保存的页面,就不能简单的为交换而被删除。当一个页面第一次写入内存时,该标志位由<FONT face="Times New Roman">MMU</FONT>或<FONT face="Times New Roman">Linux</FONT>设置;当这个页面调出内存时,<FONT face="Times New Roman">Linux</FONT>要读取它的值。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo5; tab-stops: list 21.25pt">l           <B normal"><FONT face="Times New Roman">_PAGE_PROTNONE</FONT></B>位(第<FONT face="Times New Roman">11103</FONT>行),是一个以前的页表项没有使用过的标志位,用来跟踪当前页面。</P><P 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">_PAGE_4M</FONT></B>位和<B normal"><FONT face="Times New Roman">_PAGE_GLOBAL</FONT></B>位在同一个<B normal"><FONT face="Times New Roman">#define</FONT></B>定义块中出现,但是由于它们不像其它标志位那样用于页面级的保护,所以我们在此不予讨论。</P><P 0cm 0cm 0pt">随后的文件中,上述这些标志位被组合在一个高级宏内。</P><H4 14pt 0cm 14.5pt"><FONT size=5>写拷贝(copy-on-write)</FONT></H4><P 0cm 0cm 0pt">提高效率的一条路就是偷懒——只做最少量的必要工作,而且只在不得不做的时候才完成。现实生活中这可能是个坏习惯,至少它会导致拖拖拉拉。而在计算机的世界里,它可能更是一种优点。写拷贝(<FONT face="Times New Roman">Copy-On-Write</FONT>)就是<FONT face="Times New Roman">Linux</FONT>内核一种通过懒惰来获得效率的方法。其基本思想是把一个页面标记为只读,却把它所含的<FONT face="Times New Roman">VMA</FONT>标识为可写。任何对页面的写操作都会与页级保护相冲突,然后触发一个页面错误。页面错误处理程序会注意到这是由页级保护和<FONT face="Times New Roman">VMA</FONT>的保护不一致而导致的,然后它就会创建一个该页的可写拷贝作为代替。</P><P 0cm 0cm 0pt">写拷贝十分有用。进程经常<B normal"><FONT face="Times New Roman">fork</FONT></B>并立刻<B normal"><FONT face="Times New Roman">exec</FONT></B>,这样为<B normal"><FONT face="Times New Roman">fork</FONT></B>而复制的进程页面会造成浪费,因为<B normal"><FONT face="Times New Roman">exec</FONT></B>之后它们会不得不被抛弃。正如读者所见,进程分配大量内存时也使用同样的机制。所有被分配的页面都与一个单独的空白页面相映射,这就是写拷贝的原意。向某页面的第一次写操作会触发页面错误,然后空白页面执行复制。用这种办法,只有页面分配不能再延期时,它才会被分配。<p></p></P><H4 14pt 0cm 14.5pt"><FONT size=5>页面错误</FONT></H4><P 0cm 0cm 0pt">到现在为止,本章已几次提到一个页面可以不在<FONT face="Times New Roman">RAM</FONT>里的可能性——毕竟,如果页面总是在内存里,虚拟内存就没什么必要了。但是我们还没有详细介绍过当某页面不在<FONT face="Times New Roman">RAM</FONT>中会怎样。当处理器试图访问一个当前不在<FONT face="Times New Roman">RAM</FONT>中的页面时,<FONT face="Times New Roman">MMU</FONT>就会产生一次页面错误,而内核会尽力解决它。在进程违反页级保护时,页面错误也会产生,例如进程试图向只读内存区域写入。</P><P 0cm 0cm 0pt">因为任何无效内存访问都会导致页面错误,同样的机制支持请求分页。请求分页(<FONT face="Times New Roman">Demand paging</FONT>)的意思是只有在页面被引用的时候才从磁盘上读取它们——即按需分配——这是另一种通过懒惰来获得效率的方法。</P><P 0cm 0cm 0pt">特别地,请求分页用于实现被请求页面的可执行化。为了达到这个目的,应用程序第一次被装载时,只有一小部分可执行映象(<FONT face="Times New Roman">image</FONT>)被读入物理内存,然后内核就依靠页面错误来调入需要的(比如,进程首次跳转到一个子例程时)可执行页面。除了一些意外的情况,这样做总是要比把所有部分一次读入要快,这是因为磁盘较慢,而且并不是所有的程序都会用到的。事实上,因为一个大程序运行一次时,大部分功能特性都不会再用到,所以通常根本不需要全部都读入(这一点对大多数中小规模的程序也是成立的)。这对于按需分页(<FONT face="Times New Roman">demand-paged</FONT>)的可执行程序稍有不同——如果你对这种情况进行考虑的话,你就可以知道按需分页还需要二进制处理程序的支持,而且它是一个具有决定意义的部分。<p></p></P><P 3pt 0cm 3.2pt"><b>Do_page_fault</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">6980</FONT>:<FONT face="Times New Roman"> <B normal">do_page_fault</B></FONT>是内核函数,产生页面错误时(在第<FONT face="Times New Roman">363</FONT>行)被调用。当页面错误产生时,<FONT face="Times New Roman">CPU</FONT>调整进程的寄存器,当解决页面错误时,进程再从引起错误的指令处开始执行。通过这种方法,在内核使得冲突地内存访问操作完成后,会自动重试该操作。相反,如果页面错误仍然无法解决,内核就通知引起冲突的进程。当页面错误是由内核本身导致的时候,所采用的措施是近似的,但并不完全相同。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">6992</FONT>:<FONT face="Times New Roman"> </FONT>控制寄存器<FONT face="Times New Roman">2</FONT>(<FONT face="Times New Roman">CR2</FONT>)是<FONT face="Times New Roman">Intel CPU</FONT>的寄存器,保存引起页面错误的线性地址。该寄存器内的地址会被直接读入局部变量<B normal"><FONT face="Times New Roman">address</FONT></B>。</P>7004: 函数<B normal">find_vma</B>(第33460行)返回地址范围末尾在<B normal">address</B>之后的第一个VMA。大家知道,这并不能够保证该地址位于VMA的范围内,而仅保证该地址比VMA<FONT face="宋体, MS Song" size=3> </FONT><DIV footnote-list"><BR clear=all><FONT face="宋体, MS Song" size=3><HR align=left width="33%" SIZE=1></FONT><DIV><P 0cm 0cm 0pt"><FONT size=2><FONT face="Times New Roman">1 </FONT>原文为:<FONT face="Times New Roman">”…given enough eyeballs, all bugs are shallow.”</FONT></FONT></P></DIV></DIV>
作者: ilikenba    时间: 2005-3-4 22:22
< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt">的结束地址小,这样它就可能比<FONT face="Times New Roman">VMA</FONT>的初始地址还要小。因此这一点要被检查。假如通过判断,则<B normal"><FONT face="Times New Roman">address</FONT></B>在<FONT face="Times New Roman">VMA</FONT>之内,控制就会向前跳转到标号<B normal"><FONT face="Times New Roman">good_area</FONT></B>处(第<FONT face="Times New Roman">7023</FONT>行);我们随后就会对这一点进行讨论。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">7005</FONT>:<FONT face="Times New Roman"> </FONT>如果<B normal"><FONT face="Times New Roman">find_vma</FONT></B>返回空值<FONT face="Times New Roman">NULL</FONT>,那么<B normal"><FONT face="Times New Roman">address</FONT></B>就位于进程的所有<FONT face="Times New Roman">VMA</FONT>之后——换句话说就是超出了由进程引用的所有内存。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">7009</FONT>:<FONT face="Times New Roman"> <B normal">vma</B></FONT>的开头和结尾都确实超过了<B normal"><FONT face="Times New Roman">address</FONT></B>;因此<B normal"><FONT face="Times New Roman">address</FONT></B>在<FONT face="Times New Roman">VMA</FONT>低端地址以下。但是这并不会失去什么。如果<FONT face="Times New Roman">VMA</FONT>是向下扩展的类型——也可以说它是堆栈——这个堆栈可以简单的向下扩展来适应那个地址。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">7011</FONT>:<FONT face="Times New Roman"> CPU</FONT>提供的<B normal"><FONT face="Times New Roman">error_code</FONT></B>的测试位<FONT face="Times New Roman">2</FONT>。与监控(内核)模式相比,更多是在用户模式发生页面错误时设置此位。如果是在用户模式下,<B normal"><FONT face="Times New Roman">do_page_fault</FONT></B>函数会保证给定的地址在为进程建立的堆栈区域内,正如<FONT face="Times New Roman">ESP</FONT>寄存器所定义的那样。(例如,在代码溢出了被分配的堆栈矩阵时,就会产生这种情况。)如果是在监控(内核)模式下,就会跳过后一种判断,而简单的假定内核运行正常。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">7019</FONT>:<FONT face="Times New Roman"> </FONT>如果可能,使用<B normal"><FONT face="Times New Roman">expand_stack</FONT></B>(行<FONT face="Times New Roman">15480</FONT>)将扩展到包含新的地址。如果成功,<FONT face="Times New Roman">VMA<B normal"> </B></FONT>的<B normal"><FONT face="Times New Roman">vm_start</FONT></B>成员将调整到包含<B normal"><FONT face="Times New Roman">address</FONT></B>。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">7023</FONT>:<FONT face="Times New Roman"> </FONT>到达<B normal"><FONT face="Times New Roman">good_area</FONT></B>标记时,就意味着<FONT face="Times New Roman">VMA</FONT>包含<B normal"><FONT face="Times New Roman">address</FONT></B>,或者说要么它已经包括了<B normal"><FONT face="Times New Roman">address</FONT></B>,要么就是堆栈扩展后包括了该地址。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm">不管那一种方法,包括错误产生原因信息的<B normal"><FONT face="Times New Roman">error_code</FONT></B>最低两位现在都可以被测试了。第<FONT face="Times New Roman">0</FONT>位是存在<FONT face="Times New Roman">/</FONT>保护位:<FONT face="Times New Roman">0</FONT>表示该页不存在;<FONT face="Times New Roman">1</FONT>表示该页存在,但试图的访问操作与页级保护位冲突。第<FONT face="Times New Roman">1</FONT>位是读<FONT face="Times New Roman">/</FONT>写位:<FONT face="Times New Roman">0</FONT>表示读,<FONT face="Times New Roman">1</FONT>表示写。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">7025</FONT>:<FONT face="Times New Roman"> <B normal">switch</B></FONT>条件判断语句对于上述两个测试位所组合出的四种可能情况作出相应处理:</P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo1; tab-stops: list 57.25pt">l           <FONT face="Times New Roman">case 2</FONT>或<FONT face="Times New Roman">3</FONT>——检查包括的<FONT face="Times New Roman">VMA</FONT>是否可写。若可写,就是向一个写拷贝页面执行一次写操作;变量<B normal"><FONT face="Times New Roman">write</FONT></B>被增加(设置到<FONT face="Times New Roman">1</FONT>)以便接下来对<B normal"><FONT face="Times New Roman">hand_mm_fault</FONT></B>的调用能够完成写拷贝过程。<p></p></P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo1; tab-stops: list 57.25pt">l           <FONT face="Times New Roman">case 1</FONT>——这意味着页面错误是由试图从一个存在但不可读的页面中读数据而导致的;这个尝试会被拒绝。<p></p></P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo1; tab-stops: list 57.25pt">l           <FONT face="Times New Roman">case 0</FONT>——表示页面错误是由试图从一个不存在的页面中读数据而导致的。如果涉及的<FONT face="Times New Roman">VMA</FONT>保护指出该区间既不可读也不可写,读页面只不过是浪费时间——如果再次尝试,将引起另一个页面错误,这样<B normal"><FONT face="Times New Roman">do_page_fault</FONT></B>函数会以<FONT face="Times New Roman">case 1</FONT>的结果告终,即拒绝尝试。否则<B normal"><FONT face="Times New Roman">do_page_fault</FONT></B>函数继续执行并从磁盘上读入页面。<p></p></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">7047</FONT>:<FONT face="Times New Roman"> </FONT>请求<B normal"><FONT face="Times New Roman">handle_mm_fault</FONT></B>函数(下面讨论)使该页面变为当前页面。如果失败,则发出一个<B normal"><FONT face="Times New Roman">SIGBUS</FONT></B>错误。<p></p></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">7062</FONT>:<FONT face="Times New Roman"> </FONT>大多数内核函数的清除代码都不太显著。<B normal"><FONT face="Times New Roman">do_page_fault</FONT></B>函数是一个例外;我们会比较详尽的研究它的清除代码。下列任何情况发生都会跳到<B normal"><FONT face="Times New Roman">bad_area</FONT></B>标记处:<p></p></P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 57.25pt">l           被引用的地址超过了为进程分配的(或保留的)所有内存。<p></p></P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 57.25pt">l           被引用的地址位于所有<FONT face="Times New Roman">VMA</FONT>之外,而且可能由于比该地址小的<FONT face="Times New Roman">VMA</FONT>不是堆栈而无法扩展到这个地址。<p></p></P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 57.25pt">l           违反了页面的读<FONT face="Times New Roman">/</FONT>写保护。<p></p></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">7066</FONT>:<FONT face="Times New Roman"> </FONT>如果用户代码引起以上任何错误,那将发送致命的<B normal"><FONT face="Times New Roman">SIGSEGV</FONT></B>信号——一个分段违例。(注意术语“分段”在这里是历史上的说法而不是字面所表达的意思——对<FONT face="Times New Roman">CPU</FONT>来说,从技术角度看它是分页违例,不一定是分段违例。)这个信号通常会像第<FONT face="Times New Roman">6</FONT>章中讨论的那样杀死一个进程。<p></p></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">7075</FONT>:<FONT face="Times New Roman"> Intel Pentium CPU</FONT>(以及它的一些兼容产品)具有一个所谓的<FONT face="Times New Roman">f00f</FONT>缺陷,它允许任何进程用非法的<FONT face="Times New Roman">0xf00fc7C8</FONT>指令来冻结<FONT face="Times New Roman">CPU</FONT>。<FONT face="Times New Roman">Intel</FONT>所提议的弥补工作就是在这里实现的:中断描述表(见第<FONT face="Times New Roman">6</FONT>章)的一部分以前是被标识为只读的,因为这样会使非法指令执行时用产生页面错误代替冻结<FONT face="Times New Roman">CPU</FONT>。在这里,<B normal"><FONT face="Times New Roman">do_page_fault</FONT></B>函数检查导致页面错误的地址是否位于<FONT face="Times New Roman">IDT</FONT>中由非法指令执行而产生的位置上。如果是这样的,处理器会试着执行“<FONT face="Times New Roman">Invalid Opcode</FONT>”服务中断——<FONT face="Times New Roman">CPU</FONT>的缺陷会使得正确完成这一步失败,而代码却会通过直接调用<B normal"><FONT face="Times New Roman">do_invalid_op</FONT></B>函数而产生正确的结果。否则,<FONT face="Times New Roman">CPU</FONT>决不会对<FONT face="Times New Roman">IDT</FONT>进行写操作(即使没有标注为只读时也是如此),所以即使第<FONT face="Times New Roman">7080</FONT>行的检测失败,非法指令也是根本不会被执行的。<p></p></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">7086</FONT>:<FONT face="Times New Roman"> </FONT>下列情况发生时,标记<B normal"><FONT face="Times New Roman">no_context</FONT></B>会被执行:<p></p></P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo3; tab-stops: list 57.25pt">l           在内核(不是用户)模式里到达<B normal"><FONT face="Times New Roman">bad_area</FONT></B>,而且<FONT face="Times New Roman">CPU</FONT>不执行触发<FONT face="Times New Roman">f00f</FONT>缺陷的非法指令。<p></p></P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo3; tab-stops: list 57.25pt">l           在一个中断中或没有用户环境(用户任务没有处于正在执行状态)时发生的页面错误。<p></p></P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo3; tab-stops: list 57.25pt">l           <B normal"><FONT face="Times New Roman">Handle_mm_fault</FONT></B>函数错误并且系统处于内核模式中(我还从未遇到过这种情况)。<p></p></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm">这里的任何一种情形都是内核错误(经常由驱动程序所导致),它不是因为任何用户代码而造成的页面错误。如果内核(或驱动程序)事先为这种可能准备了错误处理代码,那么这些错误处理代码一定位于本书讨论范围之外,并在错误发生时可以通过某种特殊技术跳转过去。<p></p></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">7097</FONT>:<FONT face="Times New Roman"> </FONT>否则,内核试图访问一个坏页面,<B normal"><FONT face="Times New Roman">do_page_fault</FONT></B>函数将不知如何处理它。这可能也够被考虑到。内核启动代码检查是否<FONT face="Times New Roman">MMU</FONT>写保护工作正常;如果正常,那就不是一个真正的错误,<B normal"><FONT face="Times New Roman">do_page_fault</FONT></B>函数就可以简单的返回了。<p></p></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">7109</FONT>:<FONT face="Times New Roman"> </FONT>内核访问了一个坏页面,并且<B normal"><FONT face="Times New Roman">do_page_fault</FONT></B>函数无法修复这个错误。<B normal"><FONT face="Times New Roman">do_page_fault</FONT></B>函数会在第<FONT face="Times New Roman">7129</FONT>行显示出一些描述错误的信息,然后中止内核本身。这样整个系统就会被停止,很明显没有任何操作会被执行。不过,如果系统运行到了这一步,内核也别无选择了。<p></p></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">7134</FONT>:<FONT face="Times New Roman"> </FONT>最后一个标记是<B normal"><FONT face="Times New Roman">do_sigbus</FONT></B>,只有当<B normal"><FONT face="Times New Roman">handle_mm_fault</FONT></B>函数无法处理错误时才会执行到这里。这种情况相对简单;大体上是给违例的进程发送一个<B normal"><FONT face="Times New Roman">SIGBUS</FONT></B>错误信号,如果这是在内核模式里发生的就再跳回到<B normal"><FONT face="Times New Roman">no_context</FONT></B>标记处。<p></p></P>< 3pt 0cm 3.2pt"><b>Handle_mm_fault</b></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32725</FONT>:调用者已经检测到了需要一个可用的页面。该页面正是包含<B normal"><FONT face="Times New Roman">address</FONT></B>的页面,这个地址应归入<B normal"><FONT face="Times New Roman">vma</FONT></B>中。<B normal"><FONT face="Times New Roman">Handle_mm_fault</FONT></B>函数本身相当简洁,但是它建立在其它几个处理冗长细节问题的函数和宏之上。我们介绍完此函数后将逐一研究那些底层函数。<p></p></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32732</FONT>:查找关联的页目录和页面中间目录入口项(如前所述,这两者在<FONT face="Times New Roman">x86</FONT>平台上实际是一样的)。<p></p></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32735</FONT>:从页面中间目录项得到或定位(如果可能的话)页表。<p></p></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32737</FONT>:调用<B normal"><FONT face="Times New Roman">handle_pte_fault</FONT></B>函数把页面读入页表项(<FONT face="Times New Roman">page table entry</FONT>);如果成功,就调用<B normal"><FONT face="Times New Roman">update_mmu _cache</FONT></B>函数更新<FONT face="Times New Roman">MMU</FONT>的高速缓存。控制流程到此为止,一切顺利,<B normal"><FONT face="Times New Roman">handle_mm_fault</FONT></B>函数就可以返回一个非零值(<FONT face="Times New Roman">1</FONT>)表示成功了。如果此过程任何一步出错,控制就转向第<FONT face="Times New Roman">32744</FONT>行,函数返回<FONT face="Times New Roman">0</FONT>值表示失败。</P><P 3pt 0cm 3.2pt"><b>Pgd_offset</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11284</FONT>:这个宏将<B normal"><FONT face="Times New Roman">address</FONT></B>除以<FONT face="Times New Roman">2<SUP>PGDIR_SHIFT</SUP></FONT>(第<FONT face="Times New Roman">11052</FONT>行<FONT face="Times New Roman">#<B normal">define</B>d to 32</FONT>),并对结果向下舍入,然后把最终结果(移位之前的高端<FONT face="Times New Roman">10</FONT>位)作为提供的<B normal"><FONT face="Times New Roman">struct mm_struct</FONT></B>的<B normal"><FONT face="Times New Roman">pgd</FONT></B>数组的一个索引。因此,它的值就是页目录项,相应的页表<B normal"><FONT face="Times New Roman">address</FONT></B>地址就位于该项中。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">   </FONT>这等价于代码</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">&amp;((mm)-&gt;pgd[(address)&gt;&gt;PGDIR_SHIFT]);</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm">而且可能会更高效。</P><P 3pt 0cm 3.2pt"><b>Pmd_alloc</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11454</FONT>:因为<FONT face="Times New Roman">x86</FONT>平台上没有定义页面中间目录,这样就极其简单:它只需返回给定的<B normal"><FONT face="Times New Roman">pgd</FONT></B>指针,并映射为一种不同类型。在其它平台上,该函数与<B normal"><FONT face="Times New Roman">pte_alloc</FONT></B>类似,还要实现更多的工作。</P><P 3pt 0cm 3.2pt"><b>Pte_alloc</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11422</FONT>:<B normal"><FONT face="Times New Roman">Pte_alloc</FONT></B>函数有两个参数:一个是指针,指向目标地址所位于的页面中间目录项,另一个是地址本身。如果我们暂时跳过一部分内容,那么对该函数经过变形的逻辑的理解就会更容易,所以让我们看一下随后的若干行代码。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11425</FONT>:用一种几乎无法理解的方式把<B normal"><FONT face="Times New Roman">address</FONT></B>转换成<FONT face="Times New Roman">PMD</FONT>内的一个偏移量。</P><P 0cm 0cm 0pt; TEXT-INDENT: 18pt">这一行需要详细进行解释。首先,回忆一下<FONT face="Times New Roman">PMD</FONT>中的每项都是一个指针,在<FONT face="Times New Roman">x86</FONT>平台上它的长度是<FONT face="Times New Roman">4</FONT>个字节(这里的代码是与体系结构相关的,所以我们可以作出这样的假定)。用<FONT face="Times New Roman">C</FONT>语言来定义就是,</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 18pt"><FONT face="Times New Roman">&amp;pmd[middle_10_bits(address)]</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt">(为清晰起见,我在这里引入了假定的<B normal"><FONT face="Times New Roman">pmd</FONT></B>数组和<B normal"><FONT face="Times New Roman">middle_10_bits</FONT></B>函数)该代码等价于</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">pmd+middle_10_bits(address)</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt">这又与如下代码指向的地址相同</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">((char*)pmd)+middle_10_bits(address)*sizeof(pte_t*)</FONT></P><P 0cm 0cm 0pt 18pt; TEXT-INDENT: -18pt"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm">其技巧是在最后的公式中——或者更准确的说是<FONT face="Times New Roman">+</FONT>号后边的部分——最接近于第<FONT face="Times New Roman">11425</FONT>行所要计算的实际值。</P><P 0cm 0cm 0pt">为了使这一点更为明确,首先可知</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">4*(PTRS_PER_PTE-1)</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm">就是<FONT face="Times New Roman">4092</FONT>(第<FONT face="Times New Roman">11059</FONT>行<B normal"><FONT face="Times New Roman">PTRS_PER_PTE</FONT></B>被定义为<FONT face="Times New Roman">1024</FONT>)。用二进制表示,<FONT face="Times New Roman">4092</FONT>只用占最低<FONT face="Times New Roman">12</FONT>位,甚至最后<FONT face="Times New Roman">2</FONT>位也用不上。它和只占最低<FONT face="Times New Roman">10</FONT>位的<FONT face="Times New Roman">1023</FONT>左移<FONT face="Times New Roman">2</FONT>位后的值相同。这样就有</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">(address&gt;&gt;(PAGE_SHIFT-2))</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm">把<FONT face="Times New Roman">address</FONT>右移<FONT face="Times New Roman">10</FONT>位(第<FONT face="Times New Roman">10790</FONT>行<B normal"><FONT face="Times New Roman">PAGE_SHIFT</FONT></B>被定义为<FONT face="Times New Roman">12</FONT>)。这两个表达式结果再逐位进行与(<FONT face="Times New Roman">AND</FONT>)操作。最终的结果类似于:</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">((address&gt;&gt;PAGE_SHIFT)&amp;(PTRS_PER_PTE-1))&lt;&lt;2</FONT></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm">尽管这仍很复杂,不过它更简单明了:它把<B normal"><FONT face="Times New Roman">address</FONT></B>右移<FONT face="Times New Roman">12</FONT>位(为了去掉页面偏移量部分),屏蔽掉除最低<FONT face="Times New Roman">10</FONT>位的其它位(去掉页目录索引部分,只保留最低<FONT face="Times New Roman">10</FONT>位的页面中间目录索引),接着把结果左移<FONT face="Times New Roman">2</FONT>位(相当于乘以<FONT face="Times New Roman">4</FONT>,即指针长度的字节数<B normal"><FONT face="Times New Roman">sizeof(pte_t*)</FONT></B>)。更直接的方法可能会稍慢一些,但在内核里,我们终归是要尽力节省时间的。(虽然更直接的方法看来并非明显偏慢:同样版本内核进行两次移位、两次减法,以及按位与的操作,和进行两次移位、两次按位与的操作,就我的测试看来实际上是一样快。)</P><P 0cm 0cm 0pt">不管采用那一种方法,经过计算之后,把<B normal"><FONT face="Times New Roman">address</FONT></B>和<FONT face="Times New Roman">PMD</FONT>的基地址相加(在第<FONT face="Times New Roman">11432</FONT>行和别的地方执行),就得到了指向与<B normal"><FONT face="Times New Roman">address</FONT></B>初值关联的<FONT face="Times New Roman">PTE</FONT>的项指针。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">11428</FONT>:如果<FONT face="Times New Roman">PMD</FONT>项不指向任一个页表,函数向前跳到<B normal"><FONT face="Times New Roman">getnew</FONT></B>处分配一个页表。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11435</FONT>:通过调用<B normal"><FONT face="Times New Roman">get_pte_fast</FONT></B>(第<FONT face="Times New Roman">11357</FONT>行)尝试从<B normal"><FONT face="Times New Roman">pte_quicklist</FONT></B>中申请一个页表。这个页表是页表的一个高速缓存,其思想是分配页表(它们本身就是独立的页面)慢,而从一系列近期释放的页表中指定一个却会稍快一些。所以,代码经常用<B normal"><FONT face="Times New Roman">free_pte_fast</FONT></B>(第<FONT face="Times New Roman">11369</FONT>行)来释放页表,这会把它们放在<B normal"><FONT face="Times New Roman">pte_quicklist</FONT></B>里而不是确实把它们消除掉。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11439</FONT>:<B normal"><FONT face="Times New Roman">pte_quicklist</FONT></B>能够提供一个页表页面。页表可以被送入页面中间目录,并且函数返回页表中这个页面的偏移值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11438</FONT>:<B normal"><FONT face="Times New Roman">pte_quicklist</FONT></B>缓存里没有剩下页面,因此<B normal"><FONT face="Times New Roman">pte_alloc</FONT></B>需要调用<B normal"><FONT face="Times New Roman">get_pte_slow</FONT></B>函数(第<FONT face="Times New Roman">7216</FONT>行)来分配一个缓慢页面。该函数用<B normal"><FONT face="Times New Roman">__get_free_page</FONT></B>来分配页面,执行过程和一个页面被找到时相似。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11430</FONT>:如果<FONT face="Times New Roman">PMD</FONT>项不是<FONT face="Times New Roman">0</FONT>,但是是无效的,<B normal"><FONT face="Times New Roman">pte_alloc</FONT></B>显示一个警告(通过调用第<FONT face="Times New Roman">7187</FONT>行的<B normal"><FONT face="Times New Roman">bad_pte</FONT></B>)并放弃尝试。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11432</FONT>:所期待的正常情况:<B normal"><FONT face="Times New Roman">pte_alloc</FONT></B>函数返回一个指向包括<B normal"><FONT face="Times New Roman">address</FONT></B>地址的<FONT face="Times New Roman">PTE</FONT>的指针。</P><P 3pt 0cm 3.2pt"><b>Handle_pte_fault</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32690</FONT>:<B normal"><FONT face="Times New Roman">Handle_pte_fault</FONT></B>函数试图取回或者创建一个缺少的<FONT face="Times New Roman">PTE</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32702</FONT>:给定的项与物理内存中的任何一个页面都无关联(<FONT face="Times New Roman">32700</FONT>行),而且确实没有被设置(<FONT face="Times New Roman">32701</FONT>行)。这样,<B normal"><FONT face="Times New Roman">do_no_page</FONT></B>(<FONT face="Times New Roman">32633</FONT>行)将被调用以创建一个新的页面映射。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32704</FONT>:页面在内存中不存在,但是它有一个映射,所以它一定在交换空间里。函数<B normal"><FONT face="Times New Roman">do_swap_page</FONT></B>(<FONT face="Times New Roman">32569</FONT>行)将被调用来把该页面读回内存。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32708</FONT>:页面在内存里,所以情况可能是内核正在处理一个页面保护冲突。<B normal"><FONT face="Times New Roman">Handle_pte_fault</FONT></B>首先要用<B normal"><FONT face="Times New Roman">pte_mkyoung</FONT></B>(<FONT face="Times New Roman">11252</FONT>行)来把该页面标识为已被访问。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32713</FONT>:如果是一个写访问操作而页面又不是可写的,<B normal"><FONT face="Times New Roman">Handle_pte_fault</FONT></B>就调用<B normal"><FONT face="Times New Roman">do_wp_page</FONT></B>函数(<FONT face="Times New Roman">32401</FONT>行)。这个函数完成真正的写拷贝功能,因此我们要简单介绍一下。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32715</FONT>:这是一次对可写页面的写访问。<FONT face="Times New Roman"><B normal">Handle_pte_faul</B>t</FONT>设置该页面的“<FONT face="Times New Roman">dirty</FONT>”位,表示在它被丢弃之前必须被复制到交换空间。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32720</FONT>:所需的页面现在可被调用者使用,所以<B normal"><FONT face="Times New Roman">Handle_pte_fault</FONT></B>函数返回非零值(确定为<FONT face="Times New Roman">1</FONT>)以示成功。</P><P 3pt 0cm 3.2pt"><b>Update_mmu_cache</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11506</FONT>:在<FONT face="Times New Roman">x86</FONT>平台上,<B normal"><FONT face="Times New Roman">update_mmu_cache</FONT></B>函数是一个空操作。它是一种所谓的“挂钩(<FONT face="Times New Roman">hook</FONT>)”函数——这种函数要在内核的平台无关部分中适当地点处保证被调用,以便不同的移植版本都能够在必要的情况下对它进行定义。</P><P 3pt 0cm 3.2pt"><b>Do_wp_page</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32401</FONT>:如前所述,真正的写拷贝操作是在这里实现的,所以我们有必要介绍一下。<B normal"><FONT face="Times New Roman">tsk</FONT></B>试图写入<B normal"><FONT face="Times New Roman">address</FONT></B>,这个地址在给定的<B normal"><FONT face="Times New Roman">vma</FONT></B>里并由所提供的<B normal"><FONT face="Times New Roman">page_table</FONT></B>来控制。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32410</FONT>:调用<B normal"><FONT face="Times New Roman">__get_free_page</FONT></B>(<FONT face="Times New Roman">15364</FONT>行,简单的转向第<FONT face="Times New Roman">34696</FONT>行调用<B normal"><FONT face="Times New Roman">__get_free_pages</FONT></B>函数)为进程提供一个新页面,此页面是写保护页面的一个新拷贝。注意这里可以允许一个任务转换。有趣的是,这里的代码不检查<B normal"><FONT face="Times New Roman">__get_free_page</FONT></B>分配新页面时是否成功——它实际上可能不需要新的页面,因此到必要时才会去进行检查。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32422</FONT>:增加“次要(<FONT face="Times New Roman">minor</FONT>)”页面错误,这些错误无需访问磁盘就可被满足。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32438</FONT>:只有两个页面用户存在,其中一个是交换高速缓存(<FONT face="Times New Roman">swap cache</FONT>),它是已被交换出但还未被回收的页面的临时缓冲池。该页面被移出交换高速缓存后(利用<FONT face="Times New Roman">37686</FONT>行的函数<B normal"><FONT face="Times New Roman">delete_from_swap_cache</FONT></B>),现在它就只有一个用户了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32445</FONT>:要么从交换高速缓存里回收该页面,要么它只有一个开始用户。这个页面会被标识为可写和“脏”<FONT face="Times New Roman">dirty</FONT>(因为它已被写过)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32448</FONT>:如果已分配了一个新页面,它就没有用了:由于该页面只有一个用户,所以没有必要进行复制。<B normal"><FONT face="Times New Roman">do_wp_page</FONT></B>函数释放这个新页面,并返回非零值表示成功。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32454</FONT>:页面拥有不止一个用户,不能简单的从交换高速缓存里被收回。因此<B normal"><FONT face="Times New Roman">do_wp_page</FONT></B>函数将需要复制一个新页面。如果先前的页面分配失败,现在就是该结果产生作用的时候了,<B normal"><FONT face="Times New Roman">do_wp_page</FONT></B>函数将不得不返回错误。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32459</FONT>:利用<B normal"><FONT face="Times New Roman">copy_cow_page</FONT></B>(<FONT face="Times New Roman">31814</FONT>行)复制页面内容。这通常是调用<B normal"><FONT face="Times New Roman">copy_page</FONT></B>宏(<FONT face="Times New Roman">32814</FONT>行),它是一个<B normal"><FONT face="Times New Roman">memcpy</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32460</FONT>:利用<B normal"><FONT face="Times New Roman">flush_page_to_ram</FONT></B>(<FONT face="Times New Roman">10900</FONT>行)使<FONT face="Times New Roman">RAM</FONT>新旧页面拷贝同步。像<B normal"><FONT face="Times New Roman">update_mmu _cache</FONT></B>函数一样,在<FONT face="Times New Roman">x86</FONT>平台上这是一个空操作。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32463</FONT>:像以前一样,使得页面可写和“脏”,同时保留从封装的<FONT face="Times New Roman">VMA</FONT>而来的其它页面保护(比如可执行)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">32466</FONT>:对函数<B normal"><FONT face="Times New Roman">free_page</FONT></B>(在<FONT face="Times New Roman">15386</FONT>行,它只是调用<FONT face="Times New Roman">34633</FONT>行的<B normal"><FONT face="Times New Roman">free_pages</FONT></B>函数)的调用而不会真正释放旧的页面,因为该页面拥有多个用户——它只会减少旧页面被引用的次数。由于满足了调用者的请求,<B normal"><FONT face="Times New Roman">do_wp_page</FONT></B>就返回非零值表示成功。</P><H4 14pt 0cm 14.5pt"><FONT size=5>页面调出</FONT></H4><P 0cm 0cm 0pt">现在读者已经对交换页面调入有所了解,接下来看一看另一方面,交换页面的调出。</P><P 3pt 0cm 3.2pt"><b>Try_to_swap_out</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38863</FONT>:<B normal"><FONT face="Times New Roman">try_to_swap_out</FONT></B>函数是最低一级交换调出函数,它由内核任务<B normal"><FONT face="Times New Roman">kswapd</FONT></B>(见<FONT face="Times New Roman">39272</FONT>行<B normal"><FONT face="Times New Roman">kswapd</FONT></B>函数)周期性地调用(通过一系列其它函数调用)。这个函数用来写一个页面,该页面是由位于给定任务特定<FONT face="Times New Roman">VMA</FONT>中的一个单独页表项来控制的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38873</FONT>:如果内存中缺少该页面,它就不能从内存写回到磁盘,这样<B normal"><FONT face="Times New Roman">try_to_swap_out</FONT></B>函数就返回失败。如果给定的地址明显是不合法的(<B normal"><FONT face="Times New Roman">max_mapnr</FONT></B>是当前系统中物理内存的页面数目;参见<FONT face="Times New Roman">7546</FONT>行),它也会丢弃尝试操作。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38880</FONT>:如果页面被保留、锁住,或者被一个外设用于直接内存访问时,它就不能被调出。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38885</FONT>:如果页面最近被访问过,把它调出可能是不明智的,因为引用的局部性可能会使该页面不久将再被引用。把该页面标识成“旧的”,这样将来的再一次尝试就可能把它调出内存——这可能很快就会发生,如果内核不顾一切要这么做的话。但事实是,页面还没有被调出。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm">这一行之后的代码注释本身就含有大量信息,所以我们将跳过几段代码而不失完整性。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38965</FONT>:减少任务的驻留段长度(注意<B normal"><FONT face="Times New Roman">vma-&gt;vm_mm</FONT></B>是指向含有<B normal"><FONT face="Times New Roman">vma</FONT></B>的<B normal"><FONT face="Times New Roman">struct mm_struct</FONT></B>的指针)。驻留段长度是物理内存中的任务所占页面数目,而且很明显,这些页面中的一个现在已经不存在了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38968</FONT>:因为页面无效,所以<B normal"><FONT face="Times New Roman">try_to_swap_out</FONT></B>函数必须通知所有<FONT face="Times New Roman">TLB</FONT>以无效化它们对该页面的引用。<FONT face="Times New Roman">TLB</FONT>不应该再把地址解析到一个已经不存在了的页面。<B normal"><FONT face="Times New Roman">try_to_swap_out</FONT></B>函数接着把这个页面放入交换缓存。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38977</FONT>:最后,<B normal"><FONT face="Times New Roman">try_to_swap_out</FONT></B>函数通过使用<B normal"><FONT face="Times New Roman">rw_swap_cache</FONT></B>(<FONT face="Times New Roman">35186</FONT>行)把旧的页面写回磁盘,写操作是异步的,以便等待磁盘处理时系统也可以作其它工作。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38979</FONT>:用<B normal"><FONT face="Times New Roman">__free_page</FONT></B>(<FONT face="Times New Roman">34621</FONT>行)来释放页面,并返回非零值表示成功。</P><H2 13pt 0cm">交换设备</H2><P 0cm 0cm 0pt"><FONT face="Times New Roman">Linux</FONT>拥有一个按优先级排序的合法交换设备列表(以及文件,不过为了简单起见,这一部分通常用“设备”来代替这两者)。当需要分配一个交换页面时,<FONT face="Times New Roman">Linux</FONT>会在仍然拥有空间的优先级最高的交换设备上来分配它。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">Linux</FONT>也会在所有优先级相同的未满交换设备之间轮转使用,采用的是循环方式,通过这种在多个磁盘上分布分页请求的方法可以提高交换的性能。在等待第一个请求被满足时,另一个请求就可以分派到下一个磁盘上。最快的设置是把交换分区分布在几个相似的磁盘上,并给它们同样的优先级设置;而较慢的磁盘则有稍低一些的优先级。</P><P 0cm 0cm 0pt">不过循环也可能造成交换速度的降低。如果同一磁盘上的多个交换设备有同样的优先级,那么磁盘的读<FONT face="Times New Roman">/</FONT>写头将不得不在磁盘上来回的反复访问它们;在这种情况下,臭名卓著的<FONT face="Times New Roman">1000</FONT>倍的速度差异就不容忽视了。幸运的是,系统管理员会合理安排优先级以避免这种情况。<FONT face="Times New Roman">Linux</FONT>继承了<FONT face="Times New Roman">Unix</FONT>的传统特性,既能让你陷入绝境,也能使你达成非常良好的目标。最简单的方案是给每个交换设备分配不同的优先级;这会有助于避免最坏的情况,但可能也不会最好。尽管如此,由于该方法简单且不会最坏,如果你不指定优先级设置,它将是缺省设置。</P><P 0cm 0cm 0pt">交换设备用<B normal"><FONT face="Times New Roman">struct swap_info_struct</FONT></B>(<FONT face="Times New Roman">17554</FONT>行)结构体类型来表示。在<FONT face="Times New Roman">37834</FONT>行定义了这些结构体的一个数组<B normal"><FONT face="Times New Roman">swap_info</FONT></B>。好几个文件里的函数都操作和使用<B normal"><FONT face="Times New Roman">swap_info</FONT></B>数组来进行交换管理;很快我们就会对它们进行分析。先来分析一下<B normal"><FONT face="Times New Roman">struct swap_info_struct</FONT></B>的成员,这会使我们能够更清楚的了解这些函数。</P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo4; tab-stops: list 21.25pt">l           <B normal"><FONT face="Times New Roman">swap_device</FONT></B>——发生交换的设备号;如果<B normal"><FONT face="Times New Roman">struct</FONT></B>代表一个文件而不是分区,值是<FONT face="Times New Roman">0</FONT>。<B normal"><p></p></B></P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo4; tab-stops: list 21.25pt">l           <B normal"><FONT face="Times New Roman">swap_file</FONT></B>——<B normal"><FONT face="Times New Roman">struct</FONT></B>代表的交换文件或分区<B normal"><p></p></B></P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo4; tab-stops: list 21.25pt">l           <B normal"><FONT face="Times New Roman">swap_map</FONT></B>——对交换空间里每个交换页面的用户数进行计数的数组;为<FONT face="Times New Roman">0</FONT>则表示页面空闲。<B normal"><p></p></B></P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo4; tab-stops: list 21.25pt">l           <B normal"><FONT face="Times New Roman">swap_lockmap</FONT></B>——用来跟踪基于磁盘的页面当前是否正被读出或写入磁盘,数组里的每一位对应一个页面。在<FONT face="Times New Roman">I/O</FONT>过程中页面将被锁定以防止内核同时对同一页面执行两次<FONT face="Times New Roman">I/O</FONT>操作,或者其它类似的愚蠢操作——需要记住的是,一旦有可能,其它进程就会与<FONT face="Times New Roman">I/O</FONT>操作相重叠,所以发生这种情况并非难事。<B normal"><p></p></B></P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo4; tab-stops: list 21.25pt">l           <B normal"><FONT face="Times New Roman">lowest_bit</FONT></B>和<B normal"><FONT face="Times New Roman">highest_bit</FONT></B>——跟踪交换设备里第一个和最后一个可用的页面的位置。这可以有助于加快寻找空闲页面的循环。设备的第一个页面是一个不允许用于交换的头部,因此<B normal"><FONT face="Times New Roman">lowest_bit</FONT></B>不会是<FONT face="Times New Roman">0</FONT>。<B normal"><p></p></B></P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo4; tab-stops: list 21.25pt">l           <B normal"><FONT face="Times New Roman">cluster_next</FONT></B>和<B normal"><FONT face="Times New Roman">cluster_nr</FONT></B>——用来对磁盘上的交换页面进行分组以获得更高的效率。<B normal"><p></p></B></P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo4; tab-stops: list 21.25pt">l           <B normal"><FONT face="Times New Roman">prio</FONT></B>——交换设备的优先级。<B normal"><p></p></B></P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo4; tab-stops: list 21.25pt">l           <B normal"><FONT face="Times New Roman">pages</FONT></B>——设备上可用的页面数目。<B normal"><p></p></B></P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo4; tab-stops: list 21.25pt">l           <B normal"><FONT face="Times New Roman">max</FONT></B>——内核在此设备中所允许的最大页面数目。<B normal"><p></p></B></P><P 0cm 0cm 0pt 21.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo4; tab-stops: list 21.25pt">l           <B normal"><FONT face="Times New Roman">next</FONT></B>——把<B normal"><FONT face="Times New Roman">swap_info</FONT></B>数组中的所有<B normal"><FONT face="Times New Roman">struct</FONT></B>形成一个单独的链接列表(并保持优先级顺序)。这样,数组就被逻辑排序,而不是物理排序了。<B normal"><FONT face="Times New Roman">next</FONT></B>的值就是列表中逻辑指向下一个元素的索引,如果到达列表末尾它就是<FONT face="Times New Roman">-1</FONT>。<B normal"><p></p></B></P><P 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">swap_list</FONT></B>在<FONT face="Times New Roman">37832</FONT>行定义,包括列表头(即<B normal"><FONT face="Times New Roman">head</FONT></B>成员――参见<FONT face="Times New Roman">17627</FONT>行<B normal"><FONT face="Times New Roman">struct swap_list_t</FONT></B>的定义)的索引;如果列表为空则此索引为<FONT face="Times New Roman">-1</FONT>。它还包括名字很令人迷惑的<B normal"><FONT face="Times New Roman">next</FONT></B>成员,这个成员能够跟踪我们将要在其上尝试页面分配的下一个交换设备。因此<B normal"><FONT face="Times New Roman">next</FONT></B>是一个迭代指针。如果列表为空或者当前没有交换,它的值就是<FONT face="Times New Roman">-1</FONT>。</P><P 3pt 0cm 3.2pt"><b>Get_swap_page</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">37879</FONT>:<B normal"><FONT face="Times New Roman">get_swap_page</FONT></B>函数从最高优先级的拥有空间的可用交换设备里获得一个页面;如果找到一个,它就返回一个非零代码描述该项,如果系统没有交换就返回<FONT face="Times New Roman">0</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">37885</FONT>:从上一次停止的地方继续进行迭代。如果列表是空的或没有剩余交换设备,函数即刻返回。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">37891</FONT>:否则的话,有理由确信存在交换空间,<B normal"><FONT face="Times New Roman">get_swap_page</FONT></B>函数恰恰需要找到它。这个循环过程一直迭代,直到函数找到一项(很可能的情形)或者扫描了每一个交换设备但没有一个还有剩余空间(不太可能的情形)为止。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">37894</FONT>:利用<B normal"><FONT face="Times New Roman">scan_swap_map</FONT></B>(<FONT face="Times New Roman">37838</FONT>行)扫描当前交换设备的<B normal"><FONT face="Times New Roman">swap_map</FONT></B>以寻找一个空闲单元,如果找到了一项,<B normal"><FONT face="Times New Roman">lowest_bit</FONT></B>和<B normal"><FONT face="Times New Roman">highest_bit</FONT></B>成员也会被更新。要返回的<B normal"><FONT face="Times New Roman">offset</FONT></B>是<FONT face="Times New Roman">0</FONT>或者是该项。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">37897</FONT>:当前的交换设备能够分配一个页面。<B normal"><FONT face="Times New Roman">get_swap_page</FONT></B>函数现在把<B normal"><FONT face="Times New Roman">swap_map</FONT></B>的迭代游标向前推进以便请求能被正确的分布在交换设备上。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm">如果已经到达交换设备列表的末尾或是下一个交换设备的优先级低于当前设备,迭代过程就会从列表的头部重新开始。这产生两个重要作用:</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo5; tab-stops: list 21.25pt">l         如果较高优先级设备的交换空间又变得可用,<B normal"><FONT face="Times New Roman">get_swap_page</FONT></B>就会在下一次迭代时从那个设备开始分配交换。如果孤立的观察这些代码,读者会认为当高优先级设备可用的时候,这个函数仍可以从低优先级设备分配少数页面。然而事实并非如此,在我们对交换页面是如何被释放进行介绍的时候读者就会看到这一点。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo5; tab-stops: list 21.25pt">l         如果优先级高的交换设备不可用,那么在下一次内核分配一个交换项时,<B normal"><FONT face="Times New Roman">get_swap_page</FONT></B>函数将沿列表进行迭代直到它找到当前优先级的第一个设备为止,并试着从那个设备分配交换。因此,在内核转向优先级较低的设备之前,内核会继续考虑优先级较高的设备直至它们全部耗尽。这就是先前讨论过的循环执行过程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">37910</FONT>:当前设备没有可用的交换空间,或者当前设备是不可写的(这与我们所说的是同样的)。跳到下一个设备,这样如果它已经到达末尾但还未曾循环一整圈时,它就会再从头开始循环。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">37916</FONT>:如果<B normal"><FONT face="Times New Roman">get_swap_page</FONT></B>函数到达列表的末尾而且已经循环了一遍,它就已经考虑了所有交换设备但是没有一个拥有空余的空间。因此,结论是再也没有可用的交换空间了,函数返回<FONT face="Times New Roman">0</FONT>。</P><P 3pt 0cm 3.2pt"><b>Swap_free</b></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">37923</FONT>:<B normal"><FONT face="Times New Roman">swap_free</FONT></B>函数是与<B normal"><FONT face="Times New Roman">get_swap_page</FONT></B>函数相对的,它释放一个单独的交换项。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">37939</FONT>:通过许多简单而又周密的测试后,<B normal"><FONT face="Times New Roman">swap_free</FONT></B>函数检查是否正在释放交换页面的设备具有比随后将被考虑的设备更高的优先级。如果是,它就把此作为一个线索以将<B normal"><FONT face="Times New Roman">swap_list</FONT></B>的迭代器重新设置在列表头部。这样对<B normal"><FONT face="Times New Roman">get_swap_page</FONT></B>函数的下一次调用就会从列表头部开始并能够检测到新近被释放的高优先级空间。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">37944</FONT>:假如最新被释放的页面处于<B normal"><FONT face="Times New Roman">lowest_bit</FONT></B>和<B normal"><FONT face="Times New Roman">highest_bit</FONT></B>成员所定义的范围之外,就要相应的对它们进行调整。你可以看到如果<B normal"><FONT face="Times New Roman">swap_free</FONT></B>函数在一个以前已经耗尽了的设备中释放页面,这通常会引起对<B normal"><FONT face="Times New Roman">lowest_bit</FONT></B>或者<B normal"><FONT face="Times New Roman">highest_bit</FONT></B>的调整,但并非都要调整。这会使该区间比所需要的大,交换页面分配也会因此比所需要的要慢。不过这种情况很少发生。无论如何,交换范围都将调整自己以使更多的交换页面能够被分配和释放。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">37950</FONT>:对<B normal"><FONT face="Times New Roman">swap_map</FONT></B>每一元素的使用计数只维护到一个最大值,即<B normal"><FONT face="Times New Roman">SWAP_MAP_MAX</FONT></B>(<FONT face="Times New Roman">17551</FONT>行定义为<FONT face="Times New Roman">32767</FONT>)。达到这个最大值之后,内核将无法知道真正的计数值有多大;由此它也无法安全的减少该值。否则的话,<B normal"><FONT face="Times New Roman">swap_free</FONT></B>函数将减少使用计数并增加空闲页面的总数。</P><P 3pt 0cm 3.2pt"><b>Sys_swapoff</b></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">38161</FONT>:<B normal"><FONT face="Times New Roman">sys_swapoff</FONT></B>函数在可能情况下从交换设备列表中移去被指明的交换设备。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38178</FONT>:搜索<FONT face="Times New Roman"><B normal">swap_info_struct</B>s</FONT>的列表以查找匹配的项,设置<B normal"><FONT face="Times New Roman">p</FONT></B>指向这个数据项、<B normal"><FONT face="Times New Roman">type</FONT></B>指向该数据项的索引,以及<B normal"><FONT face="Times New Roman">prev</FONT></B>指向前一项的索引。如果第一个元素被删除,<B normal"><FONT face="Times New Roman">prev</FONT></B>将是<FONT face="Times New Roman">-1</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38195</FONT>:如果<B normal"><FONT face="Times New Roman">sys_swapoff</FONT></B>函数搜索了整个列表但没有找到匹配项,那显然是给定了一个无效的名字。函数返回错误。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38198</FONT>:如果<B normal"><FONT face="Times New Roman">prev</FONT></B>是负值,<B normal"><FONT face="Times New Roman">sys_swapoff</FONT></B>函数将删除列表的第一个元素;它相应的适当更新<B normal"><FONT face="Times New Roman">swap_list.head</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: 0cm"><FONT face="Times New Roman">swap_list.head=</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">  swap_info[swap_list.head].next</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm">不过速度更快,因为其中所牵扯的间接转换更少。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38203</FONT>:如果正被移去的设备是内核进行交换尝试的下一个设备,迭代游标会被重新设置在列表头部。这样下一次分配可能要稍慢一点儿,不过并不显著;无论如何,实际中这样的情况是相当少见的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38209</FONT>:由于设备仍在使用中而不能被释放时,它会被恢复到列表的适当地方。如果这是数个拥有同样优先级交换设备之中的一个,它可能不会回到同以前一样的相对位置了——它将是具有同样优先级的设备的第一个而不是最后一个——不过列表仍然是按照优先级进行排序的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm">从交换设备列表上删除一个仍有可能被我们又放回原处的设备,这看起来就象是在做无用功——为什么不等到可以确信它可以被删除时再删除它呢?</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm">答案在于经由一系列利用<B normal"><FONT face="Times New Roman">swap_list</FONT></B>的函数调用后,在前面代码行对<B normal"><FONT face="Times New Roman">try_to_unuse</FONT></B>(<FONT face="Times New Roman">38105</FONT>行)的调用能够结束。<FONT face="Times New Roman"> </FONT>如果正被删除的交换设备那时仍在<B normal"><FONT face="Times New Roman">swap_list</FONT></B>里,那么终止这一切的代码将会给系统造成极大的混乱。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38223</FONT>:若在一个分区上进行交换,<B normal"><FONT face="Times New Roman">sys_swapoff</FONT></B>函数将解除对它的引用。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38244</FONT>:<B normal"><FONT face="Times New Roman">sys_swapoff</FONT></B>函数以使所有数据域无效和释放已分配的内存而告终。特别的,这行代码清除<B normal"><FONT face="Times New Roman">SWP_USED</FONT></B>位,这样内核就会在它再次利用该交换设备时知道它已经是不可用的了。接下来,<B normal"><FONT face="Times New Roman">sys_swapoff</FONT></B>函数清除<B normal"><FONT face="Times New Roman">err</FONT></B>指示符并返回成功。</P><P 3pt 0cm 3.2pt"><b>Sys_swapon</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38300</FONT>:<B normal"><FONT face="Times New Roman">sys_swapon</FONT></B>函数是<B normal"><FONT face="Times New Roman">sys_swapoff</FONT></B>的对应函数,它向系统列表里增加交换设备或交换文件。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38321</FONT>:找寻未用的一个项。这里有一些微妙之处。读者可能会从<B normal"><FONT face="Times New Roman">nr_swapfiles</FONT></B>的名字推断出它就是交换文件(或者设备)的数目,但是实际它不是。它是曾被使用过的<B normal"><FONT face="Times New Roman">swap_info</FONT></B>的最大索引值,而且从不会被降低。(它记录着这个数组被使用的最高峰值。)因此,把<B normal"><FONT face="Times New Roman">swap_info</FONT></B>中的这许多项循环一遍的结果是,要么发现未用的一个项,要么在最后一次循环增量后让<B normal"><FONT face="Times New Roman">p</FONT></B>指向第<B normal"><FONT face="Times New Roman">nr_swapfiles</FONT></B>项之后。在上述的后一种情况下,若<B normal"><FONT face="Times New Roman">nr_swapfiles</FONT></B>比<B normal"><FONT face="Times New Roman">MAX_ SWAPFILES</FONT></B>小,那么所有用过的项恰好会排在数组的左边,而循环就使得<B normal"><FONT face="Times New Roman">p</FONT></B>指向它们右边的一个空位。这样,<B normal"><FONT face="Times New Roman">nr_swapfiles</FONT></B>就会被更新。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>有趣的是,即使<B normal"><FONT face="Times New Roman">nr_swapfiles</FONT></B>不是最高峰值而是活动交换设备的计数值,循环也能正确执行。不过若我们改变了<B normal"><FONT face="Times New Roman">nr_swapfiles</FONT></B>的原意,文件里的其它代码就会有问题了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38328</FONT>:在<B normal"><FONT face="Times New Roman">swap_info</FONT></B>里找到了一个未用的项;<B normal"><FONT face="Times New Roman">sys_swapon</FONT></B>函数开始对其进行填充。这里所提供的一些值将会发生变化。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38338</FONT>:若<B normal"><FONT face="Times New Roman">SWAP_FLAGS_PREFER</FONT></B>被置位,<B normal"><FONT face="Times New Roman">swap_flags</FONT></B>的低端<FONT face="Times New Roman">15</FONT>位就被编码为所需的优先级。(这里使用的常量和接着的几行代码在<FONT face="Times New Roman">17510</FONT>行进行定义。)否则,就不指定优先级。如前所述,在此情况下的缺省作法是给每一个设备分派一个逐渐降低的优先级,其目的是在无须人工干预时也能得到令人满意的交换性能。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38344</FONT>:保证内核允许交换的文件或设备可以被打开。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38352</FONT>:检查提供给<B normal"><FONT face="Times New Roman">sys_swapon</FONT></B>函数的是一个文件还是一个分区。若<B normal"><FONT face="Times New Roman">S_ISBLK</FONT></B>返回为真,它就是一个块设备,即磁盘分区。在此情形下,<B normal"><FONT face="Times New Roman">sys_swapon</FONT></B>函数继续确保能够打开该块设备而且内核此时没有同它进行交换。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38375</FONT>:同样的,若给定的不是分区,<B normal"><FONT face="Times New Roman">sys_swapon</FONT></B>函数必须确保它是一个普通文件。若是文件,函数还要确保内核此时没有同该文件进行交换。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38384</FONT>:如果两项测试均告失败,<B normal"><FONT face="Times New Roman">sys_swapon</FONT></B>函数就不会再被请求在磁盘分区或文件上进行交换;它已经拒绝了该尝试。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38396</FONT>:从交换设备里把头页面读入<B normal"><FONT face="Times New Roman">swap_header</FONT></B>;这是一个在<FONT face="Times New Roman">17516</FONT>行定义的<B normal"><FONT face="Times New Roman">union swap_ header</FONT></B>联合体类型。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38400</FONT>:检查一串特征字节序列,该序列记述了交换头部的版本信息,它是由<FONT face="Times New Roman">mkswap</FONT>程序给出的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38412</FONT>:交换类型<FONT face="Times New Roman">1</FONT>。此时,该头页面被当作一个大的位映射图,每一位代表设备中剩下的一个可用页面。同其它页面一样,头页面也是<FONT face="Times New Roman">4K</FONT>字节,即<FONT face="Times New Roman">32K</FONT>比特。由于每一位表示一个页面,设备就可以拥有<FONT face="Times New Roman">32768</FONT>个页面,也就是每个设备总计<FONT face="Times New Roman">128MB</FONT>。(实际上要稍小一些,因为头页面的最后<FONT face="Times New Roman">10</FONT>位用于签名,这样我们就不能假定它们对应的<FONT face="Times New Roman">80</FONT>个页面也是可用的;另外头页面本身也不能用于交换。)如果实际设备比这个值小,那么头页面中的一些位就不起作用。在<FONT face="Times New Roman">38417</FONT>行,函数进入循环来检查哪些页面是可用的,并对它正在创建的<B normal"><FONT face="Times New Roman">swap_info_struct</FONT></B>的<B normal"><FONT face="Times New Roman">lowest_bit</FONT></B>、<B normal"><FONT face="Times New Roman">highest_bit</FONT></B>以及<B normal"><FONT face="Times New Roman">max</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_swapon</FONT></B>函数结束时它就会被释放。内核利用交换映射表来跟踪正在使用的页面;该头页面位映射图仅被用来设置<B normal"><FONT face="Times New Roman">lowest_bit</FONT></B>和其它<B normal"><FONT face="Times New Roman">swap_info_struct</FONT></B>结构体的成员。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38427</FONT>:分配交换映射表并把所有使用计数值设置为<FONT face="Times New Roman">0</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38440</FONT>:交换类型为<FONT face="Times New Roman">2</FONT>的交换并没有减轻交换区容量的限制,不过它以一种更自然和有效的方式贮存头部的信息。在此情形之下,<B normal"><FONT face="Times New Roman">swap_header</FONT></B>的<B normal"><FONT face="Times New Roman">info</FONT></B>成员就包含了<B normal"><FONT face="Times New Roman">sys_swapon</FONT></B>函数所需的信息。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38451</FONT>:新的交换头部版本不需要<B normal"><FONT face="Times New Roman">sys_swapon</FONT></B>函数把头页面当作一个位映射图来计算<B normal"><FONT face="Times New Roman">lowest_bit</FONT></B>、<B normal"><FONT face="Times New Roman">highest_bit</FONT></B>,和<B normal"><FONT face="Times New Roman">max</FONT></B>的值——<B normal"><FONT face="Times New Roman">lowest_bit</FONT></B>总是<FONT face="Times New Roman">1</FONT>,另外两个值可以从明确储存在头部的信息在定长时间内计算出来。这要比执行<FONT face="Times New Roman">32768</FONT>次位测试的循环快的多也简单的多,而且后者的定义语句甚至比前者要多出两倍以上!尽管如此,这部分以及余下的工作从概念上讲还是与以前十分相似的;<B normal"><FONT face="Times New Roman">sys_swapon</FONT></B>函数只不过是从交换头部直接获取了它所需要的大部分信息,而无须在计算它们而已。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>读者现在可以看出我刚才撒的一个小谎;版本类型为<FONT face="Times New Roman">2</FONT>的交换实际上真正克服了交换区容量的限制。在这个版本中,文件末尾的<FONT face="Times New Roman">80</FONT>个页面不会由于交换头部签名而不可利用,因此单独一个设备有可以有<FONT face="Times New Roman">320K</FONT>用于交换。不过上限仍然是大概<FONT face="Times New Roman">128MB</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38491</FONT>:<B normal"><FONT face="Times New Roman">sys_swapon</FONT></B>函数忽略读取头部。它把设备交换映射表的第一个元素设置为<B normal"><FONT face="Times New Roman">SWAP_ MAP_BAD</FONT></B>(<FONT face="Times New Roman">17552</FONT>行)以避免内核在头页面上进行交换。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38492</FONT>:分配加锁映射表并清零。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38499</FONT>:更新可用的交换页面总数,并对此结果显示一个消息。(在<FONT face="Times New Roman">38502</FONT>行,从移位计数器里减去<FONT face="Times New Roman">10</FONT>以便输出结果是千字节表示,<FONT face="Times New Roman">2<SUP>10</SUP></FONT>就是<FONT face="Times New Roman">1K</FONT>。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38505</FONT>:在交换设备的逻辑列表中插入新元素,仍遵循优先级排序的顺序。这里的代码从功能上是与<B normal"><FONT face="Times New Roman">sys_swapoff</FONT></B>函数中相应的代码一样的,所以没理由把它们分离开来。一个能代替两者的内嵌函数就能简单的解决问题。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38519</FONT>:进行清理工作,然后结束。</P><H2 13pt 0cm">内存映射mmap</H2><P 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">mmap</FONT></B>是一个重要的系统调用,它允许为不同目的而设置专用的独享内存区域。该内存可能是一个文件或其它特别对象的代理,在这种情形中,内核将保持内存区域和潜在对象的一致,或者该内存可能是为一个应用程序所需要的简单的无格式内存。(应用程序通常不使用<B normal"><FONT face="Times New Roman">mmap</FONT></B>来分配无格式内存区,因为此时<B normal"><FONT face="Times New Roman">malloc</FONT></B>更符合其目的。)</P><P 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">mmap</FONT></B>最普遍的使用方法之一是为内核本身通过内存映射(<FONT face="Times New Roman">memory-map</FONT>)形成一个可执行文件(参见<FONT face="Times New Roman">8323</FONT>行的一个例子)。这是关于二进制处理程序如何同分页机制协同工作以提供所需要分页的可执行体,这正如本章早些时候所暗示的。可执行体通过<B normal"><FONT face="Times New Roman">mmap</FONT></B>被映射为进程内存空间中的适当区域,然后<B normal"><FONT face="Times New Roman">do_page_fault</FONT></B>函数调入执行体所需的剩余页面。</P><P 0cm 0cm 0pt">被<B normal"><FONT face="Times New Roman">mmap</FONT></B>分配的内存可能被标识为可执行,其中充满了指令代码,随后系统跳入其中开始执行;这正是<FONT face="Times New Roman">Java Just-In-Time</FONT>(<FONT face="Times New Roman">JIT</FONT>)编译器的工作方式。更简单的说,可执行文件能够被直接映射成一个正在运行的进程的内存空间;这项技术用于动态连接库的执行中。</P><P 0cm 0cm 0pt">执行<B normal"><FONT face="Times New Roman">mmap</FONT></B>功能的内核函数是<B normal"><FONT face="Times New Roman">do_ mmap</FONT></B>。</P><P 3pt 0cm 3.2pt"><b>do_ mmap</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33240</FONT>:<B normal"><FONT face="Times New Roman">do_ mmap</FONT></B>函数具有几个参数;它们共同定义应在内存中映射的文件或设备,并决定将被创建的内存区域的首选地址及其它特性。</P>33252:<B normal">TASK_SIZE</B>和在10867行定义的<B normal">PAGE_OFFSET</B>值相同——即是0xc0000000或
作者: ilikenba    时间: 2005-3-4 22:24
< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">3GB</FONT>。这是用户进程所能拥有的最大内存,在此基础上代码才有意义:显然,如果要求<B normal"><FONT face="Times New Roman">do_ mmap</FONT></B>函数分配大于<FONT face="Times New Roman">3GB</FONT>的内存,或者在<B normal"><FONT face="Times New Roman">addr</FONT></B>之后的<FONT face="Times New Roman">3GB</FONT>内存空间没有足够的空间,分配请求就必须被放弃。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33275</FONT>:如果<B normal"><FONT face="Times New Roman">file</FONT></B>为<B normal"><FONT face="Times New Roman">NULL</FONT></B>,<B normal"><FONT face="Times New Roman">do_ mmap</FONT></B>函数将被请求去执行匿名映射(<FONT face="Times New Roman">anonymous mapping</FONT>)操作,这是一种并不与任何一个文件或其它特别对象连接的映射过程。否则,映射将被关联到一个文件,接着<B normal"><FONT face="Times New Roman">do_ mmap</FONT></B>函数要继续检查为内存区域设置的标志位是否与用户在文件上允许执行的操作相兼容。举例来说,在<FONT face="Times New Roman">33278</FONT>行,函数要确保是否内存区可写,因为文件已经被打开并执行写操作了。省略这项判断将可能使文件打开时所作的检查发生混乱。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33307</FONT>:允许调用程序强调<B normal"><FONT face="Times New Roman">do_ mmap</FONT></B>函数应该或者在要求的地址上提供映射操作,或者根本没有什么也不做。如果提供地址,<B normal"><FONT face="Times New Roman">do_ mmap</FONT></B>函数只需保证提供的地址从一个页面的边界开始。否则,它将获得在<B normal"><FONT face="Times New Roman">addr</FONT></B>处或之后的第一个可用地址(通过调用开始于<FONT face="Times New Roman">33432</FONT>行的<B normal"><FONT face="Times New Roman">get_unmapped_area</FONT></B>函数),然后就使用这个地址。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33323</FONT>:创建一个<FONT face="Times New Roman">VMA</FONT>并对其进行填写。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33333</FONT>:如果内存映射着一个可读文件,则内存区域就被设为可读、可写和可执行。(<B normal"><FONT face="Times New Roman">do_ mmap</FONT></B>函数可以很快的取消写许可——这只是假定)另外,如果要求共享该内存区域,那么现在就可以满足该请求。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33347</FONT>:若文件不可写,则内存区域也必须不可写。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33351</FONT>:在此情形中,没有这样的文件,使得<B normal"><FONT face="Times New Roman">do_ mmap</FONT></B>函数必须与该文件的打开模式和许可权限相一致——就允许函数自由运行。因此,函数把内存区域设为可读、可写和可执行的。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33361</FONT>:在地址范围建立时,利用<B normal"><FONT face="Times New Roman">do_munmap</FONT></B>(很快就会被讨论到)来清除任何旧的内存映射。因为新的<FONT face="Times New Roman">VMA</FONT>还没有插入进程列表之中(只有<B normal"><FONT face="Times New Roman">do_ mmap</FONT></B>函数当前知道它的存在),所以新<FONT face="Times New Roman">VMA</FONT>不会被此次调用影响。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33406</FONT>:不会再有错误发生。<B normal"><FONT face="Times New Roman">do_ mmap</FONT></B>函数把新<FONT face="Times New Roman">VMA</FONT>插入进程的<FONT face="Times New Roman">VMA</FONT>列表(或是它的<FONT face="Times New Roman">AVL</FONT>树),合并所有新近相连的段片(接下来会对<B normal"><FONT face="Times New Roman">merge_segments</FONT></B>函数进行讨论),更新一些统计数字,并返回新映射的地址。</P>< 3pt 0cm 3.2pt"><b>Merge_segments</b></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33892</FONT>:<B normal"><FONT face="Times New Roman">merge_segments</FONT></B>函数是一个有趣的函数,它把相邻的<FONT face="Times New Roman">VMA</FONT>合并成单独的一个大范围的<FONT face="Times New Roman">VMA</FONT>。换句话说,如果一个<FONT face="Times New Roman">VMA</FONT>所覆盖(有意这样设计)范围是从<FONT face="Times New Roman">0x100</FONT>到<FONT face="Times New Roman">0x200</FONT>,而另一个<FONT face="Times New Roman">VMA</FONT>的覆盖范围是从<FONT face="Times New Roman">0x200</FONT>到<FONT face="Times New Roman">0x300</FONT>,并且两者保护信息相同,那么<B normal"><FONT face="Times New Roman">merge_segments</FONT></B>函数就会用一个覆盖范围从<FONT face="Times New Roman">0x100</FONT>到<FONT face="Times New Roman">0x300</FONT>的单独<FONT face="Times New Roman">VMA</FONT>来代替它们。(注意函数名中的“<FONT face="Times New Roman">segments</FONT>”并不暗示此时我们采用<FONT face="Times New Roman">CPU</FONT>分段机制。)</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            <B normal">merge_segments</B></FONT>函数的参数是结构体<B normal"><FONT face="Times New Roman">struct mm_struct</FONT></B>,它包含了我们该兴趣的<FONT face="Times New Roman">VMA</FONT>以及可能进行合并的开始地址和终止地址。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33897</FONT>:<B normal"><FONT face="Times New Roman">find_vma_prev</FONT></B>函数将其<B normal"><FONT face="Times New Roman">vm_end</FONT></B>定位在给定的<B normal"><FONT face="Times New Roman">start_addr</FONT></B>之后的第一个<FONT face="Times New Roman">VMA</FONT>上——由此,第一个<FONT face="Times New Roman">VMA</FONT>可能会包括<B normal"><FONT face="Times New Roman">start_addr</FONT></B>。回忆一下<B normal"><FONT face="Times New Roman">find_vma_prev</FONT></B>函数,它也返回一个指向前一个<FONT face="Times New Roman">VMA</FONT>的指针<B normal"><FONT face="Times New Roman">prevl</FONT></B>(如果第一个<FONT face="Times New Roman">VMA</FONT>满足条件则该返回值是<B normal"><FONT face="Times New Roman">NULL</FONT></B>)。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33911</FONT>:进入处理所有覆盖给定区间的<FONT face="Times New Roman">VMA</FONT>的循环。在该循环过程中,<B normal"><FONT face="Times New Roman">merge_segments</FONT></B>函数将尝试把每一个段片都与其前一个段片进行合并,而前一个段片的值可以通过<B normal"><FONT face="Times New Roman">prev</FONT></B>获得。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33921</FONT>:绝大部分条件判断都是相对直截了当的,不过最后一个测试就不这么简捷了。它确保<B normal"><FONT face="Times New Roman">prev</FONT></B>和<B normal"><FONT face="Times New Roman">mpnt</FONT></B>是连续的——也就是在<B normal"><FONT face="Times New Roman">prev</FONT></B>的结尾和<B normal"><FONT face="Times New Roman">mpnt</FONT></B>的开头之间没有未被映射的内存。即使检测结果是一个的<B normal"><FONT face="Times New Roman">vm_end</FONT></B>和另一个的<B normal"><FONT face="Times New Roman">vm_start</FONT></B>相等,这两块区域在这一点上也未必一定相互覆盖——回忆一下,<B normal"><FONT face="Times New Roman">vm_end</FONT></B>是要比<FONT face="Times New Roman">VMA</FONT>拥有的最后地址还要大一位的。从<FONT face="Times New Roman">33926</FONT>行到<FONT face="Times New Roman">33932</FONT>行的代码为被映射文件和共享内存坚持了同样的特性:一块区域的末尾要等于下一块的开头。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33937</FONT>:<B normal"><FONT face="Times New Roman">merge_segments</FONT></B>函数找到了可以合并的<FONT face="Times New Roman">VMA</FONT>。它把<B normal"><FONT face="Times New Roman">mpnt</FONT></B>从<FONT face="Times New Roman">VMA</FONT>列表(还可能是<FONT face="Times New Roman">AVL</FONT>树)里移出,再将它存入<B normal"><FONT face="Times New Roman">prev</FONT></B>。要注意的是即使<FONT face="Times New Roman">VMA</FONT>的数目降到了<B normal"><FONT face="Times New Roman">MIN_MAP_COUNT</FONT></B>以下,它都不会拆除<FONT face="Times New Roman">AVL</FONT>树。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33948</FONT>:如果将要消失的<FONT face="Times New Roman">VMA</FONT>是一个被内存映射的文件的一部分,<B normal"><FONT face="Times New Roman">merge_segments</FONT></B>函数就删除它对该文件的引用。</P>< 3pt 0cm 3.2pt"><b>do_munmap</b></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33689</FONT>:<B normal"><FONT face="Times New Roman">do_munmap</FONT></B>函数明显是<B normal"><FONT face="Times New Roman">do_mmap</FONT></B>函数的反作用函数;它从一个进程的内存空间里废除虚拟内存映射。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33695</FONT>:如果<B normal"><FONT face="Times New Roman">do_munmap</FONT></B>函数被要求取消映射的地址不是页面对准的,或者地址区域位于进程的内存空间之外,那么很明显它就是无效的,因此请求就会被拒绝。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33699</FONT>:如果连一个页面也没有被释放,就拒绝尝试。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33707</FONT>:查找包括给定地址的<FONT face="Times New Roman">VMA</FONT>。令人奇怪的是,<B normal"><FONT face="Times New Roman">do_munmap</FONT></B>函数返回的是<FONT face="Times New Roman">0</FONT>——而不是错误——如果地址不在任何一个<FONT face="Times New Roman">VMA</FONT>之内的话。从某种意义上讲,这是正确无误的;<B normal"><FONT face="Times New Roman">do_munmap</FONT></B>函数被要求用来确保一个进程不再对特定内存区域进行映射,如果一开始就没有这种映射的话,那就很容易办到。不过这仍颇为奇怪;在调用者看来这是一个错误而且<B normal"><FONT face="Times New Roman">do_munmap</FONT></B>函数也应该报告这个错误。然而,某些调用程序却希望它如<FONT face="Times New Roman">33361</FONT>行的示例那样执行工作。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33717</FONT>:如果给出的内存区域整个在单独的一个<FONT face="Times New Roman">VMA</FONT>中,但又不在该<FONT face="Times New Roman">VMA</FONT>的一端,那么移去这段区域就会在封闭的<FONT face="Times New Roman">VMA</FONT>里生成一个空洞。内核是不会容忍这个空洞的,因为按照定义,<FONT face="Times New Roman">VMA</FONT>应该是连续的一段内存。因此在这种情况之下,<B normal"><FONT face="Times New Roman">do_munmap</FONT></B>函数就需要创建另一个<FONT face="Times New Roman">VMA</FONT>,使得空洞的两边各有一个<FONT face="Times New Roman">VMA</FONT>。尽管如此,如果内核已经为该进程创建了所允许的所有<FONT face="Times New Roman">VMA</FONT>,那么函数就不能这样做了,所以此时<B normal"><FONT face="Times New Roman">do_munmap</FONT></B>函数不能满足请求。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33730</FONT>:标识所有与该区域相交迭或在区域里的<FONT face="Times New Roman">VMA</FONT>为空闲状态,同时把每一个都放在本地堆栈<B normal"><FONT face="Times New Roman">free</FONT></B>里。顺着这个过程,<B normal"><FONT face="Times New Roman">do_munmap</FONT></B>函数会把<FONT face="Times New Roman">VMA</FONT>从它们的<FONT face="Times New Roman">AVL</FONT>树中删除,如果有的话。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33743</FONT>:<B normal"><FONT face="Times New Roman">do_munmap</FONT></B>函数已经建造了要释放的<FONT face="Times New Roman">VMA</FONT>堆栈,现在释放它们。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33748</FONT>:计算要释放的准确范围,要牢牢记住的是这个范围可能不能以完整的<FONT face="Times New Roman">VMA</FONT>来度量。假如为<B normal"><FONT face="Times New Roman">min</FONT></B>和<B normal"><FONT face="Times New Roman">max</FONT></B>的定义适当,这三行可以被写成如下代码:</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            st  = max ( mpnt -&gt; vm_start, addr );</FONT></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            end = min ( mpnt -&gt; vm_end,  addr + len );</FONT></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"><FONT face="Times New Roman">            </FONT>由此,<B normal"><FONT face="Times New Roman">st</FONT></B>是<B normal"><FONT face="Times New Roman">do_munmap</FONT></B>函数实际开始释放区域的开头,<B normal"><FONT face="Times New Roman">end</FONT></B>是该区域的结尾。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33765</FONT>:如果<FONT face="Times New Roman">VMA</FONT>是共享映射的一部分,<B normal"><FONT face="Times New Roman">do_munmap</FONT></B>函数通过调用<B normal"><FONT face="Times New Roman">remove_shared_vm_ struct</FONT></B>(<FONT face="Times New Roman">33140</FONT>行)来断开<B normal"><FONT face="Times New Roman">mpnt</FONT></B>与共享<FONT face="Times New Roman">VMAs</FONT>列表的链接。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33759</FONT>:更新<FONT face="Times New Roman">MMU</FONT>数据结构,它对应于这个<FONT face="Times New Roman">VMA</FONT>里当前被释放掉的子区域。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33765</FONT>:调用<B normal"><FONT face="Times New Roman">unmap_fixup</FONT></B>函数来修补映射,我们接下来就会对这个函数进行研究。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33773</FONT>:<B normal"><FONT face="Times New Roman">do_munmap</FONT></B>函数已经释放了该范围内由<FONT face="Times New Roman">VMA</FONT>代表的所有映射;最后重要的一步就是要为同一区域释放页表,这是通过调用<B normal"><FONT face="Times New Roman">free_pgtables</FONT></B>(<FONT face="Times New Roman">33645</FONT>行)实现的。</P><P 3pt 0cm 3.2pt"><b>Unmap_fixup</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33578</FONT>:<B normal"><FONT face="Times New Roman">unmap_fixup</FONT></B>函数修复给定<FONT face="Times New Roman">VMA</FONT>的映射,这可以或者通过对一端进行调整,或者通过在中间制造一个空洞,再或者通过把<FONT face="Times New Roman">VMA</FONT>完全删除的方法来完成。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33590</FONT>:第一种情况比较简单:去掉整个区间的映射。<B normal"><FONT face="Times New Roman">do_munmap</FONT></B>函数仅仅需要关闭底下的文件或其它对象即可,如果它们有的话。读者可以看到,这无须把<FONT face="Times New Roman">VMA</FONT>本身从<B normal"><FONT face="Times New Roman">current-&gt;mm</FONT></B>里移出;它已经被调用者删除了。因为<FONT face="Times New Roman">VMA</FONT>的全部范围将被解除映射,没有什么要向后推移的,所以<B normal"><FONT face="Times New Roman">unmap_fixup</FONT></B>函数就此返回。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33599</FONT>:接下来的两种情况处理把<FONT face="Times New Roman">VMA</FONT>从开头到末尾一块区间移去的问题。这也是比较简单的;它们的主要工作是要调整<FONT face="Times New Roman">VMA</FONT>的<B normal"><FONT face="Times New Roman">vm_start</FONT></B>或<B normal"><FONT face="Times New Roman">vm_end</FONT></B>成员。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33608</FONT>:这是四种情况中最有意思的一种——从一个<FONT face="Times New Roman">VMA</FONT>的中间移去一块区域,从而会产生一个空洞。函数先开始要复制一份额外生成的<FONT face="Times New Roman">VMA</FONT>的本地拷贝,然后通过将<B normal"><FONT face="Times New Roman">*extra</FONT></B>设置为<B normal"><FONT face="Times New Roman">NULL</FONT></B>来通知调用程序该附加<FONT face="Times New Roman">VMA</FONT>已被使用。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33611</FONT>:图<FONT face="Times New Roman">8-4</FONT>表示了分裂<FONT face="Times New Roman">VMA</FONT>的过程。大部分信息被直接从旧<FONT face="Times New Roman">VMA</FONT>复制到了新<FONT face="Times New Roman">VMA</FONT>,在此之后,<B normal"><FONT face="Times New Roman">unmap_fixup</FONT></B>函数对两个<FONT face="Times New Roman">VMA</FONT>的范围都作了调整以解决空洞问题。原先的<FONT face="Times New Roman">VMA</FONT>,<B normal"><FONT face="Times New Roman">area</FONT></B>,被缩小到了表示低于空洞的子区域,而<B normal"><FONT face="Times New Roman">mpnt</FONT></B>则表示高于空洞的子区域。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33626</FONT>:把全部新子区域插入<B normal"><FONT face="Times New Roman">current-&gt;mm</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33629</FONT>:在除了第一种的其它情况里,<B normal"><FONT face="Times New Roman">unmap_fixup</FONT></B>函数保持了旧的<FONT face="Times New Roman">VMA</FONT>。它缩小了,但还未消失,因此它将被插回到<FONT face="Times New Roman">VMA</FONT>的<B normal"><FONT face="Times New Roman">current-&gt;mm</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"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">                                   </FONT></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"><p><FONT face="Times New Roman"> </FONT></p></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><v:shapetype><v:stroke joinstyle="miter"></v:stroke><v:path connecttype="rect" gradientshapeok="t"></v:path></v:shapetype><v:shape><v:textbox style="mso-next-textbox: #_x0000_s1035"><FONT face="Times New Roman"></FONT></v:textbox></v:shape><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV class=shape 7.2pt; PADDING-LEFT: 7.2pt; PADDING-BOTTOM: 3.6pt; PADDING-TOP: 3.6pt" v:shape="_x0000_s1035"><P 0cm 0cm 0pt"><FONT face="Times New Roman">Area -&gt; vm_start    addr          end       Area -&gt; vm_end  </FONT></P></DIV></TD></TR></TABLE><p><FONT face="Times New Roman"> </FONT></p></P><BR vglayout" clear=all><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><v:shape><v:textbox style="mso-next-textbox: #_x0000_s1032"><FONT face="Times New Roman"></FONT></v:textbox></v:shape><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV class=shape 7.2pt; PADDING-LEFT: 7.2pt; PADDING-BOTTOM: 3.6pt; PADDING-TOP: 3.6pt" v:shape="_x0000_s1032"><P 0cm 0cm 0pt"><FONT face="Times New Roman">befor<p></p></FONT></P></DIV></TD></TR></TABLE><v:shape><v:textbox style="mso-next-textbox: #_x0000_s1028"><FONT face="Times New Roman"></FONT></v:textbox></v:shape><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV class=shape 7.95pt; PADDING-LEFT: 7.95pt; PADDING-BOTTOM: 4.35pt; PADDING-TOP: 4.35pt" v:shape="_x0000_s1028"><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P></DIV></TD></TR></TABLE><v:shape><v:textbox style="mso-next-textbox: #_x0000_s1027"><FONT face="Times New Roman" size=3></FONT></v:textbox></v:shape><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV class=shape 7.95pt; PADDING-LEFT: 7.95pt; PADDING-BOTTOM: 4.35pt; PADDING-TOP: 4.35pt" v:shape="_x0000_s1027"><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P></DIV></TD></TR></TABLE><v:shape><v:textbox style="mso-next-textbox: #_x0000_s1026"><FONT face="Times New Roman" size=3></FONT></v:textbox></v:shape><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV class=shape 7.95pt; PADDING-LEFT: 7.95pt; PADDING-BOTTOM: 4.35pt; PADDING-TOP: 4.35pt" v:shape="_x0000_s1026"><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P></DIV></TD></TR></TABLE><FONT face="Times New Roman">                                    </FONT></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"><v:shape><v:textbox style="mso-next-textbox: #_x0000_s1029"><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P></DIV></TD></TR></TABLE></v:textbox></v:shape><v:shape><v:textbox style="mso-next-textbox: #_x0000_s1030"><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P></DIV></TD></TR></TABLE></v:textbox></v:shape><v:shape><v:textbox style="mso-next-textbox: #_x0000_s1031"><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P></DIV></TD></TR></TABLE></v:textbox></v:shape><v:shape><v:textbox style="mso-next-textbox: #_x0000_s1033"><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV><P 0cm 0cm 0pt"><FONT face="Times New Roman">after<p></p></FONT></P></DIV></TD></TR></TABLE></v:textbox></v:shape><v:shape><v:textbox style="mso-next-textbox: #_x0000_s1034"><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV><P 0cm 0cm 0pt"><FONT face="Times New Roman">Area -&gt; vm_start   Area -&gt; vm_end<p></p></FONT></P></DIV></TD></TR></TABLE></v:textbox></v:shape><v:shape><v:textbox style="mso-next-textbox: #_x0000_s1036"><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV><P 0cm 0cm 0pt"><FONT face="Times New Roman">Mpnt_ vm_start  Mpnt_ vm_end<p></p></FONT></P></DIV></TD></TR></TABLE></v:textbox></v:shape><p><FONT face="Times New Roman"> </FONT></p></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"><p><FONT face="Times New Roman"> </FONT></p></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"><p><FONT face="Times New Roman"> </FONT></p></P><BR vglayout" clear=all><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">                  </FONT>图<FONT face="Times New Roman"> 8.4</FONT>分裂<FONT face="Times New Roman">VMA</FONT></P><H2 13pt 0cm">用户空间和内核空间</H2><H3 13pt 0cm"><FONT size=5>动态内存</FONT></H3><P 0cm 0cm 0pt">用户任务和内核本身都经常需要快速分配内存。<FONT face="Times New Roman">C</FONT>程序一般使用著名的<B normal"><FONT face="Times New Roman">malloc</FONT></B>和<B normal"><FONT face="Times New Roman">free</FONT></B>函数来完成这项工作;内核也有它自己类似的机制。当然,内核必须至少提供支持<FONT face="Times New Roman">C</FONT>语言的<B normal"><FONT face="Times New Roman">malloc</FONT></B>和<B normal"><FONT face="Times New Roman">free</FONT></B>函数的低级操作。</P><P 0cm 0cm 0pt">在<FONT face="Times New Roman">Linux</FONT>平台上,就像其它的<FONT face="Times New Roman">Unix</FONT>变种一样,一个进程的数据区分为两个便于使用的部分,即栈(<FONT face="Times New Roman">stack</FONT>)和堆(<FONT face="Times New Roman">heap</FONT>)。为了避免这两个部分冲突,栈从(准确的是接近)可用地址空间的顶端开始并向下扩展,而堆从紧靠代码段上方开始并向上扩展。虽然可以使用<B normal"><FONT face="Times New Roman">mmap</FONT></B>在堆和栈之间分配内存,但是这部分空间通常是没有使用的内存的空白地带。</P><P 0cm 0cm 0pt">即使不去研究有关的内核代码(不过我们还是要继续这项工作),读者也能对这些地址区间所处位置有相当好的了解。下面的短程序显示了几个挑选出来的对象的地址,它们分处于三种不同内存区域之内。由于种种理由,我们不能保证它可以被移植到所有平台上,不过它可以在<FONT face="Times New Roman">Linux</FONT>的任何版本下工作,而且也应该可以被移植到你所尝试的大部分其它平台上。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">P515-1 </FONT>代码<p></p></P><P 0cm 0cm 0pt">在我的系统上,我得到了如下的数字。你的结果可能会稍有不同,除了所使用的编译器标志外,它还取决于你的内核及<FONT face="Times New Roman">gcc</FONT>的版本。即使不完全相同,它们也应该与下面结果相当接近。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">P515-2 </FONT>代码<p></p></P><P 0cm 0cm 0pt">从这里你不难看出,如果使用大概的数字的话,栈从接近<FONT face="Times New Roman">0xC0000000</FONT>处开始并向下生长,代码从<FONT face="Times New Roman">0x8000000</FONT>处开始,而堆则如前所述从临近代码上部的地方开始并向上扩展。</P><P 3pt 0cm 3.2pt"><b><FONT size=5>Brk</FONT></b></P><P 0cm 0cm 0pt">系统调用<B normal"><FONT face="Times New Roman">brk</FONT></B>是一个在<FONT face="Times New Roman">C</FONT>库函数<B normal"><FONT face="Times New Roman">malloc</FONT></B>和<B normal"><FONT face="Times New Roman">free</FONT></B>底层的原语操作。进程的<B normal"><FONT face="Times New Roman">brk</FONT></B>值是一个位于进程堆空间和它的堆、栈中间未映射区域之间的转折点。从另一个角度看,它就是进程的最高有效堆地址。</P><P 0cm 0cm 0pt">堆位于代码段顶端和<B normal"><FONT face="Times New Roman">brk</FONT></B>之间。如果<B normal"><FONT face="Times New Roman">brk</FONT></B>底下的可用自由空间不够满足请求,<FONT face="Times New Roman">C</FONT>库函数<B normal"><FONT face="Times New Roman">malloc</FONT></B>就抬高<B normal"><FONT face="Times New Roman">brk</FONT></B>;如果被释放的空间位于<B normal"><FONT face="Times New Roman">brk</FONT></B>之下,就降低<B normal"><FONT face="Times New Roman">brk</FONT></B>。顺便说一句,<FONT face="Times New Roman">Linux</FONT>是我所知道的唯一的在使用<B normal"><FONT face="Times New Roman">free</FONT></B>函数时真正的减少进程内存空间的<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">GNU</FONT>的<FONT face="Times New Roman">C</FONT>库使用<B normal"><FONT face="Times New Roman">mmap</FONT></B>和<B normal"><FONT face="Times New Roman">munmap</FONT></B>系统调用来执行<B normal"><FONT face="Times New Roman">malloc</FONT></B>和<B normal"><FONT face="Times New Roman">free</FONT></B>。</P><P 0cm 0cm 0pt">代码、数据,以及栈的关系如图<FONT face="Times New Roman">8-5</FONT>所示。</P><P 0cm 0cm 0pt"><v:shape><v:textbox style="mso-next-textbox: #_x0000_s1037"><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P></DIV></TD></TR></TABLE></v:textbox></v:shape><v:line><FONT face="Times New Roman" size=3></FONT></v:line><v:line><v:stroke endarrow="block"><FONT face="Times New Roman" size=3></FONT></v:stroke></v:line><v:shape><v:textbox><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV><P 0cm 0cm 0pt"><FONT face="Times New Roman">stack<p></p></FONT></P></DIV></TD></TR></TABLE></v:textbox></v:shape><v:shape><v:textbox><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV><P 0cm 0cm 0pt; LINE-HEIGHT: 12pt"><FONT face="Times New Roman">Free<p></p></FONT></P><P 0cm 0cm 0pt; LINE-HEIGHT: 12pt"><FONT face="Times New Roman">(possibility mmapped)<p></p></FONT></P></DIV></TD></TR></TABLE></v:textbox></v:shape><v:line><FONT face="Times New Roman"></FONT></v:line><v:shape><v:textbox><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV><P 0cm 0cm 0pt"><FONT face="Times New Roman">heap<p></p></FONT></P></DIV></TD></TR></TABLE></v:textbox></v:shape><v:line><FONT face="Times New Roman"></FONT></v:line><v:line><v:stroke startarrow="block"><FONT face="Times New Roman"></FONT></v:stroke></v:line><v:shape><v:textbox><TABLE cellSpacing=0 cellPadding=0 width="100%"><TR><TD #d4d0c8; BORDER-TOP: #d4d0c8; BORDER-LEFT: #d4d0c8; BORDER-BOTTOM: #d4d0c8; BACKGROUND-COLOR: transparent"><DIV><P 0cm 0cm 0pt"><FONT face="Times New Roman">Code<p></p></FONT></P><P 0cm 0cm 0pt"><FONT face="Times New Roman">(fixed size<p></p></FONT></P></DIV></TD></TR></TABLE></v:textbox></v:shape><p><FONT face="Times New Roman"> </FONT></p></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><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><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><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><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><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><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P><BR vglayout" clear=all><P 0cm 0cm 0pt"><FONT face="Times New Roman">                     </FONT>图<FONT face="Times New Roman">8.5 </FONT>代码、数据和栈</P><P 0cm 0cm 0pt"><p><FONT face="Times New Roman"> </FONT></p></P><P 3pt 0cm 3.2pt"><b>Sys_brk</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33155</FONT>:实现<B normal"><FONT face="Times New Roman">brk</FONT></B>的函数是<B normal"><FONT face="Times New Roman">sys_brk</FONT></B>。它可以修改进程的<B normal"><FONT face="Times New Roman">brk</FONT></B>值,还可以返回一个新值。如果无法修改<B normal"><FONT face="Times New Roman">brk</FONT></B>的值,返回的<B normal"><FONT face="Times New Roman">brk</FONT></B>值就等于其原值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33177</FONT>:如果<B normal"><FONT face="Times New Roman">brk</FONT></B>的新值位于代码区域之中,它就明显偏低而必须被抛弃。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33179</FONT>:通过使用宏<B normal"><FONT face="Times New Roman">PAGE_ALIGN</FONT></B>(<FONT face="Times New Roman">10842</FONT>行)把<B normal"><FONT face="Times New Roman">brk</FONT></B>参数向上取整到地址更高的下一个页面。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33180</FONT>:按页对准进程原有的<B normal"><FONT face="Times New Roman">brk</FONT></B>值。这看起来有些多余,因为如果进程的<B normal"><FONT face="Times New Roman">brk</FONT></B>只是在这里被设置,它就一定是按页排列的。但是在初始化一个进程的时候,进程的<B normal"><FONT face="Times New Roman">brk</FONT></B>可以被设置在别的地方,代码并不会把它按页对准排列。不管进程的<B normal"><FONT face="Times New Roman">brk</FONT></B>在哪里被设置,把它按页对准都可能会快一些;允许内核在这里跳过一次页对准操作,而且由于此处要比别的地方更频繁的对进程的<B normal"><FONT face="Times New Roman">brk</FONT></B>进行设置,它应该不会降低执行效率而且还会少许提高。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33185</FONT>:<B normal"><FONT face="Times New Roman">brk</FONT></B>被降低了,不过还没有进入代码区域,因此尝试被允许。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33192</FONT>:如果堆的大小有限制,它就要被考虑。图<FONT face="Times New Roman">8-5</FONT>清楚的表明,<B normal"><FONT face="Times New Roman">brk - mm-&gt;end_code</FONT></B>是堆的大小。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33197</FONT>:如果<B normal"><FONT face="Times New Roman">brk</FONT></B>扩展到了已被一个<FONT face="Times New Roman">VMA</FONT>所内存映射的(<FONT face="Times New Roman"><B normal">mmap</B>ped</FONT>)区域,它就是不可利用的,因此这个新<B normal"><FONT face="Times New Roman">brk</FONT></B>值要被舍弃。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33201</FONT>:最后一项必要的检查是察看是否存在足够的自由页面用于空间分配。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">33205</FONT>:使用<B normal"><FONT face="Times New Roman">do_mmap</FONT></B>函数(<FONT face="Times New Roman">33240</FONT>行)为新区域分配空间。然后,<B normal"><FONT face="Times New Roman">sys_brk</FONT></B>函数更新进程的<B normal"><FONT face="Times New Roman">brk</FONT></B>的位置并返回新值。</P><P 3pt 0cm 3.2pt"><b><FONT size=5>Vmalloc和vfree</FONT></b></P><P 0cm 0cm 0pt">内核编程中一个有趣的方面是并没有像应用程序编程人员通常所想当然的那样能够得到很多服务。就拿<B normal"><FONT face="Times New Roman">malloc</FONT></B>和<B normal"><FONT face="Times New Roman">free</FONT></B>作为例子,它们就是建立在一个内核原语<B normal"><FONT face="Times New Roman">brk</FONT></B>之上的<FONT face="Times New Roman">C</FONT>库函数。</P><P 0cm 0cm 0pt">假使内核被修订以使其可以和标准<FONT face="Times New Roman">C</FONT>库连接,并使用它的函数<B normal"><FONT face="Times New Roman">malloc</FONT></B>和<B normal"><FONT face="Times New Roman">free</FONT></B>,那么最终结果将是既笨拙又缓慢——这些函数被要求从用户模式调用,所以内核将不得不切换到用户模式去调用它们,然后它们又不得不掉转回到内核,还必须要对整个过程进行监控,等等。为了避免这一切,内核有许多十分熟悉的函数的自己的版本,它们包括<B normal"><FONT face="Times New Roman">malloc</FONT></B>和<B normal"><FONT face="Times New Roman">free</FONT></B>在内。</P><P 0cm 0cm 0pt">的确,内核提供了像<B normal"><FONT face="Times New Roman">malloc</FONT></B>和<B normal"><FONT face="Times New Roman">free</FONT></B>一样的两对独立的函数。第一对是<B normal"><FONT face="Times New Roman">kmalloc</FONT></B>和<B normal"><FONT face="Times New Roman">kfree</FONT></B>,管理在内核段内分配的内存——这是真实地址已知的实际和物理内存块。第二对是<B normal"><FONT face="Times New Roman">vmalloc</FONT></B>和<B normal"><FONT face="Times New Roman">vfree</FONT></B>,用于对内核使用的虚拟内存进行分配和释放。由<B normal"><FONT face="Times New Roman">kmalloc</FONT></B>返回的内存更适合于类似设备驱动的程序来使用,因为它在物理内存里而且是物理连续的。不过,<B normal"><FONT face="Times New Roman">kmalloc</FONT></B>要比<B normal"><FONT face="Times New Roman">vmalloc</FONT></B>所能使用的资源少,因为<B normal"><FONT face="Times New Roman">vmalloc</FONT></B>还可以处理交换空间。</P><P 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">vmalloc</FONT></B>和<B normal"><FONT face="Times New Roman">vfree</FONT></B>的一部分也是通过<B normal"><FONT face="Times New Roman">kmalloc</FONT></B>和<B normal"><FONT face="Times New Roman">kfree</FONT></B>来实现的,因为它们需要一部分不可交换的内存用于登记操作(<FONT face="Times New Roman">bookkeeping</FONT>)。<B normal"><FONT face="Times New Roman">kmalloc</FONT></B>和<B normal"><FONT face="Times New Roman">kfree</FONT></B>又依次使用<B normal"><FONT face="Times New Roman">__get_free_ pages</FONT></B>、<B normal"><FONT face="Times New Roman">free_pages</FONT></B>,以及其它低级页面操作函数实现的。</P><P 0cm 0cm 0pt">在此我不对<B normal"><FONT face="Times New Roman">kmalloc</FONT></B>和<B normal"><FONT face="Times New Roman">kfree</FONT></B>进行解释,不过本书中提供了相关代码以供读者阅读(分别见<FONT face="Times New Roman">37043</FONT>和<FONT face="Times New Roman">37058</FONT>行)。我将要讨论的是更有意思的函数<B normal"><FONT face="Times New Roman">vmalloc</FONT></B>和<B normal"><FONT face="Times New Roman">vfree</FONT></B>。</P><P 3pt 0cm 3.2pt"><b>Vmalloc</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38776</FONT>:<B normal"><FONT face="Times New Roman">vmalloc</FONT></B>函数拥有一个参数,即要分配的内存区域的大小。函数返回指向分配区域的指针,如果无法分配就返回<B normal"><FONT face="Times New Roman">NULL</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            <B normal">Vmalloc</B></FONT>可以分配内存的虚拟地址范围是由常量<B normal"><FONT face="Times New Roman">VMALLOC_START</FONT></B>(<FONT face="Times New Roman">11081</FONT>行)和<B normal"><FONT face="Times New Roman">VMALLOC_END</FONT></B>(<FONT face="Times New Roman">11084</FONT>行)决定的。<B normal"><FONT face="Times New Roman">VMALLOC_START</FONT></B>从超过物理内存结束地址<FONT face="Times New Roman">8MB</FONT>的地方开始,以便对任何在这一区域错误的内核内存访问进行截获,<B normal"><FONT face="Times New Roman">VMALLOC_END</FONT></B>在接近可能的最大<FONT face="Times New Roman">32</FONT>位地址<FONT face="Times New Roman">4GB</FONT>的地址处。除非你的系统拥有比我的系统多得多的物理内存,否则这就意味着几乎整个<FONT face="Times New Roman">CPU</FONT>地址空间都潜在的可为<B normal"><FONT face="Times New Roman">Vmalloc</FONT></B>所用。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38781</FONT>:<B normal"><FONT face="Times New Roman">vmalloc</FONT></B>函数首先把要求的区域大小向上取整到地址更高的下一个页面边界,如果它不在一个页面的边界上的话。(<B normal"><FONT face="Times New Roman">PAGE_ALIGN</FONT></B>宏在<FONT face="Times New Roman">10842</FONT>行定义。)如果最终范围结果太小(<FONT face="Times New Roman">0</FONT>)或明显过大,则请求会被拒绝。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38784</FONT>:利用<B normal"><FONT face="Times New Roman">get_vm_area</FONT></B>来为<B normal"><FONT face="Times New Roman">size</FONT></B>大小的块定位一段足够大的内存区域,这个函数接下来会进行介绍。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38788</FONT>:通过调用<B normal"><FONT face="Times New Roman">vmalloc_area_pages</FONT></B>(<FONT face="Times New Roman">38701</FONT>行)保证能够建立页表映射。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38792</FONT>:返回被分配的区域。</P><P 3pt 0cm 3.2pt"><b>get_vm_area</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38727</FONT>:<B normal"><FONT face="Times New Roman">get_vm_area</FONT></B>函数返回从<B normal"><FONT face="Times New Roman">VMALLOC_START</FONT></B>到<B normal"><FONT face="Times New Roman">VMALLOC_END</FONT></B>的一段自由内存区间。通常这就是<B normal"><FONT face="Times New Roman">vmalloc</FONT></B>函数的工作;它还被用于我未曾提及的其它少数场合。调用程序有责任确保参数<B normal"><FONT face="Times New Roman">size</FONT></B>是一个非零的页面大小的倍数值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            <B normal">vmalloc</B></FONT>函数采用所谓的首次适应算法(<FONT face="Times New Roman">first-fit algorithm</FONT>),因为它返回一个指向定位区域的指针,该区域是它所能找到的第一个满足请求的区域。除此而外,还有最佳适应算法(<FONT face="Times New Roman">best-fit algorithm</FONT>),该算法选取足够满足需求的最小的一块可用自由区域进行分配,以及最坏适应算法(<FONT face="Times New Roman">worst-fit algorithm</FONT>),该算法总是分配最大的一块可用自由区间。每种分配方式都有优点和缺点,不过首次适应算法在这里对要达到的目的来讲,就已经非常简单、快捷而且足以满足要求了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38732</FONT>:分配一个<B normal"><FONT face="Times New Roman">struct vm_struct</FONT></B>来代表新的区域。被分配的区域用一个有序链表,即<B normal"><FONT face="Times New Roman">vmlist</FONT></B>(<FONT face="Times New Roman">38578</FONT>行)来维护,该链表是由<FONT face="Times New Roman"><B normal">struct vm_struct</B>s</FONT>构成的。包括<B normal"><FONT face="Times New Roman">struct vm_struct</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: 0cm"><FONT face="Times New Roman">struct vm_struct {</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">              unsigned long flags;</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">              void* addr;</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">              unsigned long size;</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">              struct vm_struct* next;</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">            </FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>如图<FONT face="Times New Roman">8-6</FONT>所示,链表的每一个元素都与单独一块已分配了的内存块相关联。形象的看起来,<B normal"><FONT face="Times New Roman">get_vm_area</FONT></B>函数的任务就是在已分配的区域之间找出足够宽的间隔。</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><v:imagedata></v:imagedata><w:wrap type="topAndBottom"></w:wrap></v:shape></wrapblock><BR vglayout" clear=all></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; TEXT-ALIGN: center" align=center>图<FONT face="Times New Roman">8.6 VMLIST </FONT>列表</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"><FONT face="Times New Roman">38737</FONT>:沿着链表进行循环。循环的结果要么是找到一个足够大的自由区间,要么是证明这样的区域不存在。它会先从<B normal"><FONT face="Times New Roman">VMALLOC_START</FONT></B>开始尝试,然后挨个尝试紧随着每块被分配区域之后的地址。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38746</FONT>:链表为空或者循环发现了一个足够大的新块;无论哪种情形,现在<B normal"><FONT face="Times New Roman">addr</FONT></B>都是最小可用地址。填充新的<B normal"><FONT face="Times New Roman">struct vm_struct</FONT></B>结构体,它将会被返回。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38747</FONT>:给保留块增加一个页面的大小(<FONT face="Times New Roman">x86</FONT>平台上是<FONT face="Times New Roman">4K</FONT>),来捕获内核超出的内存——可能的话还包括下一个更高地址块下方的内存。因为在决定是否当前区域足够大的时候(<FONT face="Times New Roman">38738</FONT>行)并没有把这些额外的空间算在内,那么保留区域可能会与接下来的一个相重叠,而且内核内存中超出而进入这个“额外”区域的部分也确实可能覆盖到被分配了的内存。事实不是如此吗?</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>事实不是这样。我们很容易证明<B normal"><FONT face="Times New Roman">addr</FONT></B>总是页对准的,而且我们也已知道<B normal"><FONT face="Times New Roman">size</FONT></B>总是页面大小的倍数。因此,<B normal"><FONT face="Times New Roman">addr + size</FONT></B>要小于接下来区域的开始地址,它至少是一整页。当然超出范围多于一页的内存会进入下一个区域,不过超出范围少于一页的内存就不会这样。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>因为内核不会为额外内存建立页面映射,所以对它的错误访问将造成不可解决的页面错误(这在<FONT face="Times New Roman">Linux</FONT>的现代版本中几乎还未听说过!)。这将会带给内核一次痛苦的中断,不过那要比允许内核悄然无息地破坏自己的数据结构要好一些。至少你可以立刻知道这个折磨人的系统停机,它可以帮助你诊断问题所在;而后一种作法可能在内核已经破坏了你的磁盘之后,才能看出它的危害。</P><P 3pt 0cm 3.2pt"><b>Vfree</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38759</FONT>:<B normal"><FONT face="Times New Roman">vfree</FONT></B>函数比<B normal"><FONT face="Times New Roman">vmalloc</FONT></B>简单得多(要是把<B normal"><FONT face="Times New Roman">get_vm_area</FONT></B>加进<B normal"><FONT face="Times New Roman">vmalloc</FONT></B>至少是这样的),不过为了完整起见,我们还是要对<B normal"><FONT face="Times New Roman">vfree</FONT></B>略为讨论。当然<B normal"><FONT face="Times New Roman">addr</FONT></B>是要被释放的已分配区域的开头地址。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38763</FONT>:在几项简洁而又完善的测试之后,函数沿着<B normal"><FONT face="Times New Roman">vmlist</FONT></B>进行循环,搜索要释放的区域。这个线性查找过程使我想到一件有趣的事,假如采用一个如同<FONT face="Times New Roman">VMA</FONT>管理所用的<FONT face="Times New Roman">AVL</FONT>树那样的平衡树结构,也将会提高<B normal"><FONT face="Times New Roman">vmalloc</FONT></B>和<B normal"><FONT face="Times New Roman">vfree</FONT></B>函数的性能。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38764</FONT>:当与<B normal"><FONT face="Times New Roman">addr</FONT></B>相匹配的<B normal"><FONT face="Times New Roman">struct vm_struct</FONT></B>被找到时,<B normal"><FONT face="Times New Roman">vfree</FONT></B>函数就把它从链表里分离出去,并释放该结构体和它所关联的页面,然后返回。每个<B normal"><FONT face="Times New Roman">struct vm_struct</FONT></B>不仅记录它的初址还记录区域的大小,这一点对于<B normal"><FONT face="Times New Roman">get_vm_area</FONT></B>是便利的,在这里同样也颇为便利,因此<B normal"><FONT face="Times New Roman">vfree</FONT></B>函数是知道应该释放多大空间的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">38772</FONT>:如果<B normal"><FONT face="Times New Roman">vfree</FONT></B>函数在链表里找到了匹配项,它在此之前就应该已经返回了,所以没有找到匹配项。这是一个坏事,不过还未糟糕到不可收拾的地步。这样,<B normal"><FONT face="Times New Roman">vfree</FONT></B>函数以显示一个警告而结束。</P><H3 13pt 0cm"><FONT size=5>转储内核(<FONT face="Times New Roman">Dumping Core</FONT>)</FONT></H3><P 0cm 0cm 0pt">在一些情况之下,比如一个满是“臭虫”的程序试图去访问自己允许内存空间之外的内存时,进程可以转储内核。进行“转储内核”就是把一个进程的内存空间的映象(随同一些关于应用程序本身和其状态的识别信息一起)写入一个文件以备将来使用诸如<FONT face="Times New Roman">gdb</FONT>之类的调试器进行分析的过程(“内核”是一个差不多已经过时的内存术语)。</P><P 0cm 0cm 0pt">当然,或许你的代码从来不会犯这样的错误,但是这可能会发生在你隔壁不太聪明的程序员身上,而他可能在某一天会向你询问这件事,因此在此我要对此问题进行一些讨论。</P><P 0cm 0cm 0pt">不同的二进制处理程序完成转储内核的方式不同。(第<FONT face="Times New Roman">7</FONT>章里论述过二进制处理程序。)最常用的<FONT face="Times New Roman">Linux</FONT>二进制格式是<FONT face="Times New Roman">ELF</FONT>,所以我们来看看<FONT face="Times New Roman">ELF</FONT>二进制处理程序是如何进行转储内核的。</P><P 3pt 0cm 3.2pt"><b>Elf_core_dump</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">8748</FONT>:<FONT face="Times New Roman"> <B normal">elf_core_dump</B> </FONT>函数由此开始。因为一个进程转储内存是由接受到一个信号而引起的(它也可能发送给自己,例如通过对<B normal"><FONT face="Times New Roman">about</FONT></B>的调用),该信号编号在<B normal"><FONT face="Times New Roman">signr</FONT></B>中被给出。<B normal"><FONT face="Times New Roman">Signr</FONT></B>对进程是否或者如何执行转储内存没有影响,但是在调试器里看内存文件的用户却想要知道是哪个信号导致内存转储的,它就像是一个关于出了什么错的提示一样。指向<B normal"><FONT face="Times New Roman">struct pt_regs</FONT></B>(<FONT face="Times New Roman">11546</FONT>行)的<B normal"><FONT face="Times New Roman">regs</FONT></B>参数包含一份对<FONT face="Times New Roman">CPU</FONT>寄存器的描述。<B normal"><FONT face="Times New Roman">regs</FONT></B>的重要性除了一些其它原因之外还在于它包含了<FONT face="Times New Roman">EIP</FONT>寄存器的内容,该寄存器是指令指针,它决定了收到信号时所执行的指令。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">8771</FONT>:<FONT face="Times New Roman"> </FONT>假如进程未通过一些基本检查则立即返回,这些检查中的第一个是确保<B normal"><FONT face="Times New Roman">dumpable</FONT></B>标志被设置。进程的<B normal"><FONT face="Times New Roman">dumpable</FONT></B>标志(<FONT face="Times New Roman">16359</FONT>行)通常会被设置;它的清除主要是在进程改变其用户或组<FONT face="Times New Roman">ID</FONT>的时候。这似乎是一项安全措施。例如我们将不愿意创建一个被设定为<FONT face="Times New Roman">root</FONT>的不可读执行程序的可读内存文件——那会使得保证执行体不可读(出于安全考虑)的目的遭到失败。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            <B normal">elf_core_dump</B> </FONT>函数此时也会返回,假如内存文件的大小限制使得连一个页面也无法转储,或者如果有其它线程要引用将要转储的内存。转储内核是和退出进程相关的,从用户的角度看来,只要进程任何一个线程还存在,它就没有消亡。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><B normal"><FONT face="Times New Roman">            </FONT></B>如果进程通过了这些测试,<FONT face="Times New Roman"><B normal">elf_core_dump</B> </FONT>函数就继续运行并清除<B normal"><FONT face="Times New Roman">dumpable</FONT></B>位以便它不会再次尝试转储进程的内存。(尽管这种情形不能会发生;我认为这只是预防式的编程设计。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">8785</FONT>:<FONT face="Times New Roman"> </FONT>进入一个循环以对内存文件大小限制之内可以被转储的<FONT face="Times New Roman">VMA</FONT>个数进行计数。尽管<FONT face="Times New Roman"><B normal">elf_core_dump</B> </FONT>函数把计数植保存在叫做<B normal"><FONT face="Times New Roman">segs</FONT></B>的变量里,它并不表明我们正对本章中所使用过的“内存段”进行计数。不要认为这个变量的名字有其它特别的附加涵义。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>由于<FONT face="Times New Roman"><B normal">elf_core_dump</B> </FONT>函数在转储<FONT face="Times New Roman">VMA</FONT>之前要向内存文件写一些头部信息,而且这些头部的大小没有进行计算,因此输出结果可能会稍微超出内存文件的大小限制。这不难解决:一个简单的策略是在写入头部时递减<B normal"><FONT face="Times New Roman">limit</FONT></B>,并把循环计数移动到头部写入代码之后。实际解决方案要更麻烦一些,不过也并不是十分复杂。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">8805</FONT>:<FONT face="Times New Roman"> ELF</FONT>内存文件格式根据正式规范进行定义;第一个部分是描述文件的头部。结构体<B normal"><FONT face="Times New Roman">struct elfhdr</FONT></B>类型(参见<FONT face="Times New Roman">14726</FONT>和<FONT face="Times New Roman">14541</FONT>行)定义了头部的格式,<FONT face="Times New Roman"><B normal">elf_core_dump</B> </FONT>函数填写这个类型的一个局部变量<B normal"><FONT face="Times New Roman">elf</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">8827</FONT>:<FONT face="Times New Roman"> </FONT>创建要转储到的文件名,并尝试打开这个文件。通过把<FONT face="Times New Roman">8828</FONT>行的<B normal"><FONT face="Times New Roman">#if 0</FONT></B>改变为<B normal"><FONT face="Times New Roman">#if 1</FONT></B>,我们可以让内存文件名包括生成文件的执行程序的名字(或至少是名字的前<FONT face="Times New Roman">16</FONT>个字符——参见在<FONT face="Times New Roman">16406</FONT>行定义的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>的<B normal"><FONT face="Times New Roman">comm</FONT></B>成员)。有的时候这是一个很有用的特性;能够一看到内存文件的名字就可以马上知道是什么应用生成的将是一件很好的事情。不过,这种行为并不标准,而且还有可能破坏已有代码——比如监视器脚本程序,它周期性地检查名为“<FONT face="Times New Roman">code</FONT>”的文件是否存在——所以缺省行为还是为遵守标准惯例而把文件命名为普通的“<FONT face="Times New Roman">code</FONT>”。尽管如此,发现这么一个可以调整的内核参数还是不错的。这个可选项也对<FONT face="Times New Roman">8756</FONT>行局部变量<B normal"><FONT face="Times New Roman">corefile</FONT></B>那看似与众不同的定义方式进行了解释。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">8853</FONT>:<FONT face="Times New Roman"> </FONT>设置<B normal"><FONT face="Times New Roman">PF_DUMPCORE</FONT></B>标识(<FONT face="Times New Roman">16448</FONT>行),发出信号表明该进程正在转储内核。这个标识不在本书所涉及的任何代码中使用,它被用于读者将要了解的审计进程。审计进程(<FONT face="Times New Roman">process accounting </FONT>)跟踪一个进程的资源使用情况和其它的一些相关信息——包括它是否在退出时转储内核——这些信息原本是用来帮助计算中心计算应向每个资源使用部门或用户收取多少费用的。这些日子都已经离我们远去了,难道我们不应该为此而感到高兴吗?</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">8855</FONT>:<FONT face="Times New Roman"> </FONT>写入早先建立的<FONT face="Times New Roman">ELF</FONT>内存文件头部。这里要涉及一些隐含的流控制:定义在<FONT face="Times New Roman">8707</FONT>行的<B normal"><FONT face="Times New Roman">DUMP_WRITE</FONT></B>宏使得<FONT face="Times New Roman"><B normal">elf_core_dump</B> </FONT>函数在写操作失败时关闭文件并返回。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">8862</FONT>:<FONT face="Times New Roman"> </FONT>跟在<FONT face="Times New Roman">ELF</FONT>内存文件头部之后的是一系列节点(<FONT face="Times New Roman">note</FONT>);它们中的每一个都有特殊目的,记录着有关进程的特定信息。我们将逐一对其论述。一个注解(数据类型是<B normal"><FONT face="Times New Roman">struct memelfnote</FONT></B>,<FONT face="Times New Roman">8666</FONT>行)包括一个指向辅助数据(它的<B normal"><FONT face="Times New Roman">data</FONT></B>成员)的指针和该数据的长度(它的<B normal"><FONT face="Times New Roman">datasz</FONT></B>成员);填写一个注解的大部分工作就是填充辅助数据结构,然后使该注解指向它。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>有些信息被存储在若干个注解里。代码中没有对这种重复进行解释,但是其中至少有一部分原因是从<FONT face="Times New Roman">Unix</FONT>的变种中拷贝它们的行为方式。保持文件格式和其它平台一致有助于把诸如<FONT face="Times New Roman">gdb</FONT>这样的程序移植到<FONT face="Times New Roman">Linux</FONT>上来;少许重复要比延迟移植版本的进度和增加诸如此类的关键工具的维护复杂要好得多。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">8865</FONT>:<FONT face="Times New Roman"> </FONT>注解<FONT face="Times New Roman">0</FONT>在辅助数据结构体(类型<B normal"><FONT face="Times New Roman">struct_elf_prstatus</FONT></B>;参见<FONT face="Times New Roman">14774</FONT>行)里记录了进程的继承关系、信号量,以及<FONT face="Times New Roman">CPU</FONT>的使用情况。我们需要特别注意<FONT face="Times New Roman">8869</FONT>行的<B normal"><FONT face="Times New Roman">elf_core_dump</FONT></B>,它存储了引起进程转储内核的信号编号。所以当你(或者是你隔壁那个初级程序员)在一个内存文件上运行<FONT face="Times New Roman">gdb</FONT>而它显示<FONT face="Times New Roman">”Program terminated with signal 11, Segmentation fault”</FONT>的时候,你就会知道该信息是从哪里来的了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">8916</FONT>:<FONT face="Times New Roman"> </FONT>注解<FONT face="Times New Roman">1</FONT>在辅助数据结构体<B normal"><FONT face="Times New Roman">psinfo</FONT></B>(属于类型<B normal"><FONT face="Times New Roman">struct_elf_prpsinfo</FONT></B>;参见<FONT face="Times New Roman">14813</FONT>行)里记录了进程的属主、状态,优先级等等信息。<FONT face="Times New Roman">8922</FONT>行有一个虽然正确,但很不寻常的指向一个文字字符串常量的数组下标;被选择的字符是进程状态的一个记忆码。这与<FONT face="Times New Roman">ps</FONT>程序的<FONT face="Times New Roman">STAT</FONT>域报告的状态字是一样的(除非下标溢出)。更有意思的是<FONT face="Times New Roman">8945</FONT>行,代码把执行体的名字(如前所述,最多<FONT face="Times New Roman">16</FONT>个字符)复制进了注解。<FONT face="Times New Roman">Gdb</FONT>和程序“文件”都用这个字段来报告是哪一个程序生成的内核转储。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">8948</FONT>:<FONT face="Times New Roman"> </FONT>节点<FONT face="Times New Roman">2</FONT>记录转储进程的<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>,这明显存储了关于该进程的大量必要信息。因为<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>内的一些信息是由当调试器检查代码时便不再有效的指针组成的,<FONT face="Times New Roman"><B normal">elf_core_dump</B> </FONT>函数随后还会分别转储一些指针所指向的信息——最紧要的,如进程的内存空间。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">8954</FONT>:<FONT face="Times New Roman"> </FONT>如果这个系统包含一个<FONT face="Times New Roman">FPU</FONT>(浮点计算单元),那么就会据此而生成一个注解。否则,<FONT face="Times New Roman">8957</FONT>行对所要存储的注解数目进行递减。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">8968</FONT>:<FONT face="Times New Roman"> </FONT>对于每个被创建的注解,都有一个描述该注解的头部;而注解本身会紧随其后。注解头是<B normal"><FONT face="Times New Roman">struct elf_phdr</FONT></B>类型;参见<FONT face="Times New Roman">14727</FONT>和<FONT face="Times New Roman">14581</FONT>行它的定义。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">8992</FONT>:<FONT face="Times New Roman"> </FONT>这是写入进程内存空间的第一步。在这里,函数写入头部信息(又一次是<B normal"><FONT face="Times New Roman">phdr</FONT></B>),该头部描述了它将要写入的所有<FONT face="Times New Roman">VMA</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9016</FONT>:<FONT face="Times New Roman"> </FONT>最后,<FONT face="Times New Roman"><B normal">elf_core_dump</B> </FONT>函数才真正地写入它先前辛辛苦苦创建好的各个注解(内存文件)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9022</FONT>:<FONT face="Times New Roman"> </FONT>在文件里向前跳过<FONT face="Times New Roman">4K</FONT>到达下一个边界,内存文件真正的数据是从这里开始的。完成此项操作的<B normal"><FONT face="Times New Roman">DUMP_SEEK</FONT></B>宏在<FONT face="Times New Roman">8710</FONT>行定义,像<B normal"><FONT face="Times New Roman">DUMP_WRITE</FONT></B>宏一样,假如搜索失败它也会导致<FONT face="Times New Roman"><B normal">elf_core_dump</B> </FONT>函数的返回。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">9024</FONT>:在所有那些准备之后,这里的工作简直有些虎头蛇尾。不过,这才是转储内核的主要部分:写入进程的每一个<FONT face="Times New Roman">VMA</FONT>直至先前计算出并保存在<B normal"><FONT face="Times New Roman">segs</FONT></B>里的上限。接下来是少许收尾工作,然后<FONT face="Times New Roman"><B normal">elf_core_dump</B> </FONT>函数就完成了使命。</P>
作者: ilikenba    时间: 2005-3-4 22:25
<H1 17pt 0cm 16.5pt; TEXT-ALIGN: center" align=center>第<FONT face="Times New Roman">10</FONT>章<FONT face="Times New Roman"> </FONT>对称多处理(<FONT face="Times New Roman">SMP</FONT>)</H1>< 0cm 0cm 0pt">在全书的讨论过程中,我一直在忽略<FONT face="Times New Roman">SMP</FONT>代码,而倾向于把注意力集中在只涉及一个处理器的相对简单的情况。现在已经到了重新访问读者已经熟悉的一些内容的时候了,不过要从一个新的角度来审视它:当内核必须支持多于一个<FONT face="Times New Roman">CPU</FONT>的机器时将发生什么?</P>< 0cm 0cm 0pt">在一般情况下,使用多于一个<FONT face="Times New Roman">CPU</FONT>来完成工作被称为并行处理(<FONT face="Times New Roman">parallel processing</FONT>),它可以被想象成是一段频谱范围,分布式计算(<FONT face="Times New Roman">distributed computing</FONT>)在其中一端,而对称多处理(<FONT face="Times New Roman">SMP</FONT>—<FONT face="Times New Roman">symmetric multiprocessing</FONT>)在另一端。通常,当你沿着该频谱从分布式计算向<FONT face="Times New Roman">SMP</FONT>移动时,系统将变得更加紧密耦合——在<FONT face="Times New Roman">CPU</FONT>之间共享更多的资源——而且更加均匀。在一个典型的分布式系统中,每个<FONT face="Times New Roman">CPU</FONT>通常都至少拥有它自己的高速缓存和<FONT face="Times New Roman">RAM</FONT>。每个<FONT face="Times New Roman">CPU</FONT>还往往拥有自己的磁盘、图形子系统、声卡,监视器等等。</P>< 0cm 0cm 0pt">在极端的情形下,分布式系统经常不外乎就是一组普通的计算机,虽然它们可能具有完全不同的体系结构,但是都共同工作在某个网络之上——它们甚至不需要在同一个<FONT face="Times New Roman">LAN</FONT>里。读者可能知道的一些有趣的分布式系统包括:<FONT face="Times New Roman">Beowulf</FONT>,它是对相当传统而又极其强大的分布式系统的一个通用术语称谓;<FONT face="Times New Roman">SETI@home</FONT>,它通过利用上百万台计算机来协助搜寻地外生命的证据,以及<FONT face="Times New Roman">distributed.net</FONT>,它是类似想法的另一个实现,它主要关注于地球上产生的密码的破解。</P>< 0cm 0cm 0pt"><FONT face="Times New Roman">SMP</FONT>是并行处理的一个特殊情况,系统里所有<FONT face="Times New Roman">CPU </FONT>都是相同的。举例来说,<FONT face="Times New Roman">SMP</FONT>就是你共同支配两块<FONT face="Times New Roman">80486</FONT>或两块<FONT face="Times New Roman">entium</FONT>(具有相同的时钟速率)处理器,而不是一块<FONT face="Times New Roman">80486</FONT>和一块<FONT face="Times New Roman">entium</FONT>,或者一块<FONT face="Times New Roman">entium</FONT>和一块<FONT face="Times New Roman">owerPC</FONT>。在通常的用法中,<FONT face="Times New Roman">SMP</FONT>也意味着所有<FONT face="Times New Roman">CPU</FONT>都是“在相同处境下的”——那就是它们都在同一个计算机里,通过特殊用途的硬件进行彼此通信。</P>< 0cm 0cm 0pt"><FONT face="Times New Roman">SMP</FONT>系统通常是另一种平常的单一(<FONT face="Times New Roman">single</FONT>)计算机——只不过具有两个或更多的<FONT face="Times New Roman">CPU</FONT>。因此,<FONT face="Times New Roman">SMP</FONT>系统除了<FONT face="Times New Roman">CPU</FONT>以外每样东西只有一个——一块图形卡、一个声音卡,等等之类。诸如<FONT face="Times New Roman">RAM</FONT>和磁盘这样以及类似的资源都是为系统的<FONT face="Times New Roman">CPU</FONT>们所共享的。(尽管现在<FONT face="Times New Roman">SMP</FONT>系统中每个<FONT face="Times New Roman">CPU</FONT>都拥有自己的高存缓存的情况已经变得愈发普遍了。)</P>< 0cm 0cm 0pt">分布式配置需要很少的或者甚至不需要来自内核的特殊支持;节点之间的协同是依靠用户空间的应用程序或者诸如网络子系统之类未经修改的内核组件来处理的。但是<FONT face="Times New Roman">SMP</FONT>在计算机系统内创建了一个不同的硬件配置,并由此需要特殊用途的内核支持。比如,内核必须确保<FONT face="Times New Roman">CPU</FONT>在访问它们的共享资源时要相互合作——这是一个读者在<FONT face="Times New Roman">UP</FONT>世界中所不曾遇到的问题。</P>< 0cm 0cm 0pt"><FONT face="Times New Roman">SMP</FONT>的逐渐普及主要是因为通过<FONT face="Times New Roman">SMP</FONT>所获得的性能的提高要比购买几台独立的机器再把它们组合在一起更加便宜和简单,而且还因为它与等待下一代<FONT face="Times New Roman">CPU</FONT>面世相比要快的多。</P>< 0cm 0cm 0pt">非对称多<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">Linux</FONT>内核没有对此进行区别。</P>< 0cm 0cm 0pt">谚语说得好,“天下没有白吃的午餐”。对于<FONT face="Times New Roman">SMP</FONT>,为提高的性能所付出的代价就是内核复杂度的增加和协同开销的增加。<FONT face="Times New Roman">CPU</FONT>必须安排不互相干涉彼此的工作,但是它们又不能在这种协同上花费太多时间以至于它们显著地耗费额外的<FONT face="Times New Roman">CPU</FONT>能力。</P>< 0cm 0cm 0pt">代码的<FONT face="Times New Roman">SMP</FONT>特定部分由于<FONT face="Times New Roman">UP</FONT>机器存在的缘故而被单独编译,所以仅仅因为有了<FONT face="Times New Roman">SMP</FONT>寄存器是不会使<FONT face="Times New Roman">UP</FONT>寄存器慢下来的。这满足两条久经考验的原理:“为普遍情况进行优化”(<FONT face="Times New Roman">UP</FONT>机器远比<FONT face="Times New Roman">SMP</FONT>机器普遍的多)以及“不为用不着的东西花钱”。</P><H2 13pt 0cm">并行程序设计概念及其原语</H2>< 0cm 0cm 0pt"><FONT face="Times New Roman"> </FONT>具有两个<FONT face="Times New Roman">CPU</FONT>的<FONT face="Times New Roman">SMP</FONT>配置可能是最简单的并行配置,但就算是这最简单的配置也揭开了未知问题的新领域——即使要两块相同的<FONT face="Times New Roman">CPU</FONT>在一起协调的工作,时常也都像赶着猫去放牧一样困难。幸运的是,至少<FONT face="Times New Roman">30</FONT>年前以来,就在这个项目上作了大量和非常熟悉的研究工作。(考虑到第一台电子数字计算机也只是在<FONT face="Times New Roman">50</FONT>年前建造的,那这就是一段令人惊讶的相当长的时间了。)在分析对<FONT face="Times New Roman">SMP</FONT>的支持是如何影响内核代码之前,对该支持所基于的若干理论性概念进行一番浏览将能够极大的简化这个问题。</P>< 0cm 0cm 0pt">注意:并非所有这些信息都是针对<FONT face="Times New Roman">SMP</FONT>内核的。一些要讨论的问题甚至是由<FONT face="Times New Roman">UP</FONT>内核上的并行程序设计所引起的,既要支持中断也要处理进程之间的交互。因此即使你对<FONT face="Times New Roman">SMP</FONT>问题没有特别的兴趣,这部分的讨论也值得一看。</P><H3 13pt 0cm"><FONT size=5>原子操作</FONT></H3>< 0cm 0cm 0pt">在一个并行的环境里,某些动作必须以一种基本的原子方式(<FONT face="Times New Roman">atomically</FONT>)执行——即不可中断。这种操作必须是不可分割的,就象是原子曾经被认为的那样。</P>< 0cm 0cm 0pt">作为一个例子,考虑一下引用计数。如果你想要释放你所控制的一份共享资源并要了解是否还有其它(进程)仍在使用它,你就会减少对该共享资源的计数值并把该值与<FONT face="Times New Roman">0</FONT>进行对照测试。一个典型的动作顺序可能如下开始:</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l6 level1 lfo1; tab-stops: list 42.25pt"><FONT face="Times New Roman">1.         CPU</FONT>把当前计数值(假设是<FONT face="Times New Roman">2</FONT>)装载进它的一个寄存器里。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l6 level1 lfo1; tab-stops: list 42.25pt"><FONT face="Times New Roman">2.         CPU</FONT>在它的寄存器里把这个值递减;现在它是<FONT face="Times New Roman">1</FONT>。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l6 level1 lfo1; tab-stops: list 42.25pt"><FONT face="Times New Roman">3.         CPU</FONT>把新值(<FONT face="Times New Roman">1</FONT>)写回内存里。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l6 level1 lfo1; tab-stops: list 42.25pt"><FONT face="Times New Roman">4.         CPU</FONT>推断出:因为该值是<FONT face="Times New Roman">1</FONT>,某个其它进程仍在使用着共享对象,所以它将不会释放该对象。</P>< 0cm 0cm 0pt">对于<FONT face="Times New Roman">UP</FONT>,应不必在此考虑过多(除了某些情况)。但是对于<FONT face="Times New Roman">SMP</FONT>就是另一番景象了:如果另一个<FONT face="Times New Roman">CPU</FONT>碰巧同时也在作同样的事情应如何处理呢?最坏的情形可能是这样的:</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo2; tab-stops: list 42.25pt"><FONT face="Times New Roman">1.         CPU A</FONT>把当前计数值(<FONT face="Times New Roman">2</FONT>)装载进它的一个寄存器里。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo2; tab-stops: list 42.25pt"><FONT face="Times New Roman">2.         CPU B</FONT>把当前计数值(<FONT face="Times New Roman">2</FONT>)装载进它的一个寄存器里。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo2; tab-stops: list 42.25pt"><FONT face="Times New Roman">3.         CPU A</FONT>在它的寄存器里把这个值递减;现在它是<FONT face="Times New Roman">1</FONT>。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo2; tab-stops: list 42.25pt"><FONT face="Times New Roman">4.         CPU B</FONT>在它的寄存器里把这个值递减;现在它是<FONT face="Times New Roman">1</FONT>。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo2; tab-stops: list 42.25pt"><FONT face="Times New Roman">5.         CPU A</FONT>把新值(<FONT face="Times New Roman">1</FONT>)写回内存里。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo2; tab-stops: list 42.25pt"><FONT face="Times New Roman">6.         CPU B</FONT>把新值(<FONT face="Times New Roman">1</FONT>)写回内存里。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo2; tab-stops: list 42.25pt"><FONT face="Times New Roman">7.         CPU A</FONT>推断出:因为该值是<FONT face="Times New Roman">1</FONT>,某个其它进程仍在使用着共享对象,所以它将不会释放该对象。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l3 level1 lfo2; tab-stops: list 42.25pt"><FONT face="Times New Roman">8.         CPU B</FONT>推断出:因为该值是<FONT face="Times New Roman">1</FONT>,某个其它进程仍在使用着共享对象,所以它将不会释放该对象。</P><P 0cm 0cm 0pt">内存里的引用计数值现在应该是<FONT face="Times New Roman">0</FONT>,然而它却是<FONT face="Times New Roman">1</FONT>。两个进程都去掉了它们对该共享对象的引用,但是没有一个能够释放它。</P><P 0cm 0cm 0pt">这是一个有趣的失败,因为每个<FONT face="Times New Roman">CPU</FONT>都作了它所应该做的事情,尽管这样错误的结果还是发生了。当然这个问题就在于<FONT face="Times New Roman">CPU</FONT>没有协调它们的动作行为——右手不知道左手正在干什么。</P><P 0cm 0cm 0pt">你会怎样试图在软件中解决这个问题呢?从任何一个<FONT face="Times New Roman">CPU</FONT>的观点来看待它——比如说是<FONT face="Times New Roman">CPU A</FONT>。需要通知<FONT face="Times New Roman">CPU B</FONT>它不应使用引用计数值,由于你想要递减该值,所以不管怎样你最好改变某些<FONT face="Times New Roman">CPU B</FONT>所能见到的信息——也就是更新共享内存位置。举例来说,你可以为此目的而开辟出某个内存位置,并且对此达成一致:若任何一个<FONT face="Times New Roman">CPU</FONT>正试图减少引用计数它就包含一个<FONT face="Times New Roman">1</FONT>,如果不是它就为<FONT face="Times New Roman">0</FONT>。使用方法如下:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l7 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">1.         CPU A</FONT>从特殊内存位置出取出该值把它装载进它的一个寄存器里。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l7 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">2.         CPU A</FONT>检查它的寄存器里的值并发现它是<FONT face="Times New Roman">0</FONT>(如果不是,它再次尝试,重复直到该寄存器为<FONT face="Times New Roman">0</FONT>为止。)</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l7 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">3.         CPU A</FONT>把一个<FONT face="Times New Roman">1</FONT>写回特殊内存位置。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l7 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">4.         CPU A</FONT>访问受保护的引用计数值。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l7 level1 lfo3; tab-stops: list 42.25pt"><FONT face="Times New Roman">5.         CPU A</FONT>把一个<FONT face="Times New Roman">0</FONT>写回特殊内存位置。</P><P 0cm 0cm 0pt">糟糕,令人不安的熟悉情况又出现了。以下所发生的问题仍然无法避免:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">1.         CPU A</FONT>从特殊内存位置出取出该值把它装载进它的一个寄存器里。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">2.         CPU B</FONT>从特殊内存位置出取出该值把它装载进它的一个寄存器里。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">3.         CPU A</FONT>检查它的寄存器里的值并发现它是<FONT face="Times New Roman">0</FONT>。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">4.         CPU B</FONT>检查它的寄存器里的值并发现它是<FONT face="Times New Roman">0</FONT>。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">5.         CPU A</FONT>把一个<FONT face="Times New Roman">1</FONT>写回特殊内存位置。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">6.         CPU B</FONT>把一个<FONT face="Times New Roman">1</FONT>写回特殊内存位置。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">7.         CPU A</FONT>访问受保护的引用计数值。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">8.         CPU B</FONT>访问受保护的引用计数值。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">9.         CPU A</FONT>把一个<FONT face="Times New Roman">0</FONT>写回特殊内存位置。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo4; tab-stops: list 42.25pt"><FONT face="Times New Roman">10.     CPU B</FONT>把一个<FONT face="Times New Roman">0</FONT>写回特殊内存位置。</P><P 0cm 0cm 0pt">好吧,或许可以再使用一个特殊内存位置来保护被期望保护初始内存位置的那个特殊内存位置……。</P><P 0cm 0cm 0pt">面对这一点吧:我们在劫难逃。这种方案只会使问题向后再退一层,而不可能解决它。最后,原子性不可能由软件单独保证——必须要有硬件的特殊帮助。</P><P 0cm 0cm 0pt">在<FONT face="Times New Roman">x86</FONT>平台上,<B normal"><FONT face="Times New Roman">lock</FONT></B>指令正好能够提供这种帮助。(准确地说,<B normal"><FONT face="Times New Roman">lock</FONT></B>是一个前缀而非一个单独的指令,不过这种区别和我们的目的没有利害关系。)<B normal"><FONT face="Times New Roman">lock</FONT></B>指令用于在随后的指令执行期间锁住内存总线——至少是对目的内存地址。因为<FONT face="Times New Roman">x86</FONT>可以在内存里直接减值,而无需明确的先把它读入一个寄存器中,这样对于执行一个减值原子操作来说就是万事俱备了:<B normal"><FONT face="Times New Roman">lock</FONT></B>内存总线然后立刻对该内存位置执行<B normal"><FONT face="Times New Roman">decl</FONT></B>操作。</P><P 0cm 0cm 0pt">函数<B normal"><FONT face="Times New Roman">atomic_dec</FONT></B>(<FONT face="Times New Roman">10241</FONT>行)正好为<FONT face="Times New Roman">x86</FONT>平台完成这样的工作。<B normal"><FONT face="Times New Roman">LOCK</FONT></B>宏的<B normal"><FONT face="Times New Roman">SMP</FONT></B>版本在第<FONT face="Times New Roman">10192</FONT>行定义并扩展成<B normal"><FONT face="Times New Roman">lock</FONT></B>指令。(在随后的两行定义的<FONT face="Times New Roman">UP</FONT>版本完全就是空的——单<FONT face="Times New Roman">CPU</FONT>不需要保护自己以防其它<FONT face="Times New Roman">CPU</FONT>的干扰,所以锁住内存总线将完全是在浪费时间。)通过把<B normal"><FONT face="Times New Roman">LOCK</FONT></B>宏放在内嵌编译指令的前边,随后的指令就会为<FONT face="Times New Roman">SMP</FONT>内核而被锁定。如果<FONT face="Times New Roman">CPU B</FONT>在<FONT face="Times New Roman">CPU A</FONT>发挥作用时执行了<B normal"><FONT face="Times New Roman">atomic_dec</FONT></B>函数,那么<FONT face="Times New Roman">CPU B</FONT>就会自动的等待<FONT face="Times New Roman">CPU A</FONT>把锁移开。这样就能够成功了!</P><P 0cm 0cm 0pt">这样还只能说是差不多。最初的问题仍然没有被很好的解决。目标不仅是要自动递减引用计数值,而且还要知道结果值是否是<FONT face="Times New Roman">0</FONT>。现在可以完成原子递减了,可是如果另一个处理器在递减和结果测试之间又“偷偷的”进行了干预,那又怎么办呢?</P><P 0cm 0cm 0pt">幸运的是,解决这个部分问题不需要来自<FONT face="Times New Roman">CPU</FONT>的特殊目的的帮助。不管加锁还是未锁,<FONT face="Times New Roman">x86</FONT>的<B normal"><FONT face="Times New Roman">decl</FONT></B>指令总是会在结果为<FONT face="Times New Roman">0</FONT>时设置<FONT face="Times New Roman">CPU</FONT>的<FONT face="Times New Roman">Zero</FONT>标志位,而且这个标志位是<FONT face="Times New Roman">CPU</FONT>私有的,所以其它<FONT face="Times New Roman">CPU</FONT>的所为是不可能在递减步骤和测试步骤之间影响到这个标志位的。相应的,<B normal"><FONT face="Times New Roman">atomic_dec_and_test</FONT></B>(<FONT face="Times New Roman">10249</FONT>行)如前完成一次加锁的递减,接着依据<FONT face="Times New Roman">CPU</FONT>的<FONT face="Times New Roman">Zero</FONT>标志位来设置本地变量<FONT face="Times New Roman">c</FONT>。如果递减之后结果是<FONT face="Times New Roman">0</FONT>函数就返回非零值(真)。</P><P 0cm 0cm 0pt">如同其它定义在一个文件里的函数一样,<B normal"><FONT face="Times New Roman">atomic_dec</FONT></B>和<B normal"><FONT face="Times New Roman">atomic_dec_and_test</FONT></B>都对一个类型为<B normal"><FONT face="Times New Roman">atomic_t</FONT></B>的(<FONT face="Times New Roman">10205</FONT>行)对象进行操作。就像<B normal"><FONT face="Times New Roman">LOCK</FONT></B>,<B normal"><FONT face="Times New Roman">atomic_t</FONT></B>对于<FONT face="Times New Roman">UP</FONT>和<FONT face="Times New Roman">SMP</FONT>也有不同的定义方式——不同之处在于<FONT face="Times New Roman">SMP</FONT>情况里引入了<B normal"><FONT face="Times New Roman">volatile</FONT></B>限定词,它指示<FONT face="Times New Roman">gcc</FONT>不要对被标记的变量做某种假定(比如,不要假定它可以被安全的保存在一个寄存器里)。</P><P 0cm 0cm 0pt">顺便提及一下,读者在这段代码里看到的垃圾代码<FONT face="Times New Roman">&shy;&shy;&shy;&shy;&shy;<B normal">__atomic_fool_gcc</B></FONT>据报告已不再需要了;它曾用于纠正在<FONT face="Times New Roman">gcc</FONT>的早期版本下代码生成里的一个故障。</P><H3 13pt 0cm"><FONT face="Times New Roman" size=5>Test-And-Set</FONT></H3><P 0cm 0cm 0pt">经典的并行原语是<FONT face="Times New Roman">test-and-set</FONT>。<FONT face="Times New Roman">test-and-set</FONT>操作自动地从一个内存位置读取一个值然后写入一个新值,并把旧值返回。典型的,该位置可以保存<FONT face="Times New Roman">0</FONT>或者<FONT face="Times New Roman">1</FONT>,而且<FONT face="Times New Roman">test-and-set</FONT>所写的新值是<FONT face="Times New Roman">1</FONT>——因此是“设置(<FONT face="Times New Roman">set</FONT>)”。与<FONT face="Times New Roman">test-and-set</FONT>对等的是<FONT face="Times New Roman">test-and-clear</FONT>,它是同样的操作除了写入的是<FONT face="Times New Roman">0</FONT>而不是<FONT face="Times New Roman">1</FONT>。一些<FONT face="Times New Roman">test-and-set</FONT>的变体既能写入<FONT face="Times New Roman">1</FONT>也可以写入<FONT face="Times New Roman">0</FONT>,因此<FONT face="Times New Roman">test-and-set</FONT>和<FONT face="Times New Roman">test-and-clear</FONT>就能够成为一体,只是操作数不同而已。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">test-and-set</FONT>原语足以实现任何其它并行安全的操作。(实际上,在某些<FONT face="Times New Roman">CPU</FONT>上<FONT face="Times New Roman">test-and-set</FONT>是唯一被提供的此类原语。)比如,原本<FONT face="Times New Roman">test-and-set</FONT>是能够用于前边的例子之中来保护引用计数值的。相似的方法以被尝试——从一个内存位置读取一个值,检查它是否为<FONT face="Times New Roman">0</FONT>,如果是则写入一个<FONT face="Times New Roman">1</FONT>,然后继续访问受保护的值。这种尝试的失败并不是因为它在逻辑上是不健全的,而是因为没有可行的方法使其自动完成。假使有了一个原子的<FONT face="Times New Roman">test-and-set</FONT>,你就可以不通过使用<B normal"><FONT face="Times New Roman">lock</FONT></B>来原子化<B normal"><FONT face="Times New Roman">decl</FONT></B>的方法而顺利通过了。</P><P 0cm 0cm 0pt">然而,<FONT face="Times New Roman">test-and-set</FONT>也有缺点:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo5; tab-stops: list 42.25pt">l         它是一个低级的原语——在所有与它打交道时,其它原语都必须在它之上被执行。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo5; tab-stops: list 42.25pt">l         它并不经济——当机器测试该值并发现它已经是<FONT face="Times New Roman">1</FONT>了怎么办呢?这个值在内存里不会被搞乱,因为只要用同样的值复写它即可。可事实是它已被设置就意味着其它进程正在访问受保护的对象,所以还不能这样执行。额外需要的逻辑——测试并循环——会浪费<FONT face="Times New Roman">CPU</FONT>时钟周期并使得程序变得更大一些(它还会浪费高速缓存里的空间)。</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">x86</FONT>的<B normal"><FONT face="Times New Roman">lock</FONT></B>指令使高级指令更容易执行,但是你也可以在上执行原子<FONT face="Times New Roman">test-and-set</FONT>操作。最直接的方式是把<B normal"><FONT face="Times New Roman">lock</FONT></B>和<B normal"><FONT face="Times New Roman">btsl</FONT></B>指令(位<FONT face="Times New Roman">test-and-set</FONT>)联合起来使用。这种方法要被本章后边介绍的自旋锁(<FONT face="Times New Roman">spinlock</FONT>)所用到。</P><P 0cm 0cm 0pt">另一种在<FONT face="Times New Roman">x86</FONT>上实现的方法是用它的<B normal"><FONT face="Times New Roman">xchg</FONT></B>(<FONT face="Times New Roman">exchange</FONT>)指令,它能够被<FONT face="Times New Roman">x86</FONT>自动处理,就好像它的前面有一个<B normal"><FONT face="Times New Roman">lock</FONT></B>指令一样——只要它的一个操作数是在内存里。<B normal"><FONT face="Times New Roman">xchg</FONT></B>要比<B normal"><FONT face="Times New Roman">lock/  btsl</FONT></B>组合更为普遍,因为它可以一次交换<FONT face="Times New Roman">8</FONT>、<FONT face="Times New Roman">16</FONT>,或者<FONT face="Times New Roman">32</FONT>位而不仅仅是<FONT face="Times New Roman">1</FONT>位。除了一个在<FONT face="Times New Roman">arch/i386/kernel/entry.S</FONT>里的使用之外,内核对<B normal"><FONT face="Times New Roman">xchg</FONT></B>指令的使用都隐藏在<B normal"><FONT face="Times New Roman">xchg</FONT></B>宏(<FONT face="Times New Roman">13052</FONT>行)之后,而它又是在函数<FONT face="Times New Roman">__<B normal">xchg</B></FONT>(<FONT face="Times New Roman">13061</FONT>行)之上实现的。这样是便于在平台相关的代码里内核代码也可以使用<B normal"><FONT face="Times New Roman">xchg</FONT></B>宏;每种平台都提供它自己对于该宏的等价的实现。</P><P 0cm 0cm 0pt">有趣的时,<B normal"><FONT face="Times New Roman">xchg</FONT></B>宏是另一个宏,<B normal"><FONT face="Times New Roman">tas</FONT></B>(<FONT face="Times New Roman">test-and-set</FONT>——<FONT face="Times New Roman">13054</FONT>行)的基础。然而,内核代码的任何一个地方都没有用到这个宏。</P><P 0cm 0cm 0pt">内核有时候使用<B normal"><FONT face="Times New Roman">xchg</FONT></B>宏来完成简单的<FONT face="Times New Roman">test-and-set</FONT>操作(尽管不必在锁变得可用之前一直循环,如同第<FONT face="Times New Roman">22770</FONT>行),并把它用于其它目的(如同第<FONT face="Times New Roman">27427</FONT>行)。</P><H3 13pt 0cm"><FONT size=5>信号量</FONT></H3><P 0cm 0cm 0pt">第<FONT face="Times New Roman">9</FONT>章中讨论了信号量的基本概念并演示了它们在进程间通信中的用法。内核为达自己的目的有其特有的信号量实现,它们被特别的称为是“内核信号量”。(在这一章里,未经修饰的名词“信号量”应被理解为是“内核信号量”。)第<FONT face="Times New Roman">9</FONT>章里所讨论的基本信号量的概念同样适用于内核信号量:允许一个可访问某资源用户的最大数目(最初悬挂在吊钩上钥匙的特定数目),然后规定每个申请资源者都必须先获得一把钥匙才能使用该资源。</P><P 0cm 0cm 0pt">到目前为止,你大概应该已经发现信号量如何能够被建立在<FONT face="Times New Roman">test-and-set</FONT>之上并成为二元(“唯一钥匙”)信号量,或者在像<B normal"><FONT face="Times New Roman">atomic_dec_and_test</FONT></B>这样的函数之上成为计数信号量的过程。内核正好就完成着这样的工作:它用整数代表信号量并使用函数<B normal"><FONT face="Times New Roman">down</FONT></B>(<FONT face="Times New Roman">11644</FONT>行)和<B normal"><FONT face="Times New Roman">up</FONT></B>(<FONT face="Times New Roman">11714</FONT>行)以及其它一些函数来递减和递增该整数。读者将看到,用于减少和增加整数的底层代码和<B normal"><FONT face="Times New Roman">atomic_dec_and_test</FONT></B>及其它类似函数所使用的代码是一样的。</P><P 0cm 0cm 0pt">作为相关历史事件的提示,第一位规范信号量概念的研究者,<FONT face="Times New Roman">Edsger Dijistra</FONT>是荷兰人,所以信号量的基础操作就用荷兰语命名为:<FONT face="Times New Roman">Proberen</FONT>和<FONT face="Times New Roman">Verhogen</FONT>,常缩写成<FONT face="Times New Roman">P</FONT>和<FONT face="Times New Roman">V</FONT>。这对术语被翻译成“测试(<FONT face="Times New Roman">test</FONT>)”(检查是否还有一把钥匙可用,若是就取走)和“递增(<FONT face="Times New Roman">increment</FONT>)”(把一个钥匙放回到吊钩之上)。那些词首字母正是在前一章中所引入的术语“获得(<FONT face="Times New Roman">procure</FONT>)”和“交出(<FONT face="Times New Roman">vacate</FONT>)”的来源。<FONT face="Times New Roman">Linux</FONT>内核打破了这个传统,用操作<B normal"><FONT face="Times New Roman">down</FONT></B>和<B normal"><FONT face="Times New Roman">up</FONT></B>的称呼取代了它们。</P><P 0cm 0cm 0pt">内核用一个非常简单的类型来代表信号量:定义在<FONT face="Times New Roman">11609</FONT>行的<B normal"><FONT face="Times New Roman">struct semaphore</FONT></B>。他只有三个成员:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo6; tab-stops: list 42.25pt">l         <B normal"><FONT face="Times New Roman">count</FONT></B>——跟踪仍然可用的钥匙数目。如果是<FONT face="Times New Roman">0</FONT>,钥匙就被取完了;如果是负数,钥匙被取完而且还有其它申请者在等待它。另外,如果<B normal"><FONT face="Times New Roman">count</FONT></B>是<FONT face="Times New Roman">0</FONT>或负数,那么其它申请者的数目就等于<B normal"><FONT face="Times New Roman">count</FONT></B>的绝对值。<B normal"><p></p></B></P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: 0cm"><B normal"><FONT face="Times New Roman">Sema_init</FONT></B>宏(<FONT face="Times New Roman">11637</FONT>行)允许<B normal"><FONT face="Times New Roman">count</FONT></B>被初始化为任何值,所以内核信号量可以是二元的(初始化<B normal"><FONT face="Times New Roman">count</FONT></B>为<FONT face="Times New Roman">1</FONT>)也可以是计数型的(赋予它某个更大的初始值)。所有内核信号量代码都完全支持二元和计数型信号量,前者可作为后者的一个特例。不过在实践中<B normal"><FONT face="Times New Roman">count</FONT></B>总是被初始化为<FONT face="Times New Roman">1</FONT>,这样内核信号量也总是二元类型的。尽管如此,没有什么能够阻止一个开发者将来增加一个新的计数信号量。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: 0cm">要顺便提及的是,把<B normal"><FONT face="Times New Roman">count</FONT></B>初始化为正值而且用递减它来表明你需要一个信号量的方法并没有什么神秘之处。你也可以用一个负值(或者是<FONT face="Times New Roman">0</FONT>)来初始化计数值然后增加它,或者遵循其它的方案。使用正的数字只是内核所采用的办法,而这碰巧和我们头脑中的吊钩上的钥匙模型吻合得相当好。的确,正如你将看到的那样,内核锁采用的是另一种方式工作——它被初始化为负值,并在进程需要它时进行增加。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo6; tab-stops: list 42.25pt">l         <B normal"><FONT face="Times New Roman">waking</FONT></B>——在<B normal"><FONT face="Times New Roman">up</FONT></B>操作期间及之后被暂时使用;如果<B normal"><FONT face="Times New Roman">up</FONT></B>正在释放信号量则它被设置为<FONT face="Times New Roman">1</FONT>,否则是<FONT face="Times New Roman">0</FONT>。<B normal"><p></p></B></P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l4 level1 lfo6; tab-stops: list 42.25pt">l         <B normal"><FONT face="Times New Roman">wait</FONT></B>——因为要等待这个信号量再次变为可用而不得不被挂起的进程队列。</P><P 12pt 0cm 3.2pt"><b>down</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11644</FONT>:<B normal"><FONT face="Times New Roman">down</FONT></B>操作递减信号量计数值。你可能会认为它与概念里的实现一样简单,不过实际上远不是这样简单。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11648</FONT>:减少信号量计数值——要确保对<FONT face="Times New Roman">SMP</FONT>这是自动完成的。对于<FONT face="Times New Roman">SMP</FONT>来说(当然也适于<FONT face="Times New Roman">UP</FONT>),除了被访问的整数是在一个不同类型的<B normal"><FONT face="Times New Roman">struct</FONT></B>之内以外,这同在<B normal"><FONT face="Times New Roman">atomic_dec_and_test</FONT></B>中所完成的工作本质上是相同的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>读者可能会怀疑<B normal"><FONT face="Times New Roman">count</FONT></B>是否会下溢。它不会:进程总是在递减<B normal"><FONT face="Times New Roman">count</FONT></B>之后进入休眠,所以一个给定的进程一次只能获得一个信号量,而且<B normal"><FONT face="Times New Roman">int</FONT></B>具有的负值要比进程的数目多的多。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11652</FONT>:如果符号位被设置,信号量就是负值。这意味着甚至它在被递减之前就是<FONT face="Times New Roman">0</FONT>或者负值了,这样进程无法得到该信号量并因此而应该休眠一直到它变成可用。接下来的几行代码十分巧妙地完成了这一点。如果符号位被设置则执行<B normal"><FONT face="Times New Roman">js</FONT></B>跳转(即若<B normal"><FONT face="Times New Roman">decl</FONT></B>的结果是负的它就跳转),<B normal"><FONT face="Times New Roman">2f</FONT></B>标识出跳转的目的地。<B normal"><FONT face="Times New Roman">2f</FONT></B>并非十六进制值——它是特殊的<FONT face="Times New Roman">GNU</FONT>汇编程序语法:<B normal"><FONT face="Times New Roman">2</FONT></B>表示跳转到本地符号“<FONT face="Times New Roman">2</FONT>”,<B normal"><FONT face="Times New Roman">f</FONT></B>表示向前搜索这个符号。(<B normal"><FONT face="Times New Roman">2b</FONT></B>将表示向后搜索最近的本地符号“<FONT face="Times New Roman">2</FONT>”。)这个本地符号在第<FONT face="Times New Roman">11655</FONT>行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11653</FONT>:分支转移没有执行,所以进程得到了信号量。虽然看起来不是这样,但是这实际已经到达<B normal"><FONT face="Times New Roman">down</FONT></B>的末尾。稍后将对此进行解释。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11654</FONT>:<B normal"><FONT face="Times New Roman">down</FONT></B>的技巧在于指令<B normal"><FONT face="Times New Roman">.section</FONT></B>紧跟在跳转目标的前面,它表示把随后的代码汇编到内核的一个单独的段中——该段被称为<B normal"><FONT face="Times New Roman">.text.lock</FONT></B>。这个段将在内存中被分配并标识为可执行的。这一点是由跟在段名之后的<B normal"><FONT face="Times New Roman">ax</FONT></B>标志字符串来指定的——注意这个<B normal"><FONT face="Times New Roman">ax</FONT></B>与<FONT face="Times New Roman">x86</FONT>的<FONT face="Times New Roman">AX</FONT>寄存器无关。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>这样的结果是,汇编程序将把<FONT face="Times New Roman">11655</FONT>和<FONT face="Times New Roman">11656</FONT>行的指令从<B normal"><FONT face="Times New Roman">down</FONT></B>所在的段里转移到可执行内核的一个不同的段里。所以这些行生成的目标代码与其前边的行所生成的代码从物理上不是连续的。这就是为什么说<FONT face="Times New Roman">11653</FONT>行是<B normal"><FONT face="Times New Roman">down</FONT></B>的结尾的原因。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11655</FONT>:当信号量无法得到时跳转到的这一目的行。<B normal"><FONT face="Times New Roman">Pushl $1b</FONT></B>并不是要把十六进制值<FONT face="Times New Roman">1b</FONT>压入栈中——如果要执行那种工作应该使用<B normal"><FONT face="Times New Roman">pushl $0x1b</FONT></B>(也可以写成是不带<B normal"><FONT face="Times New Roman">$</FONT></B>的)。正确的解释是,这个<B normal"><FONT face="Times New Roman">1b</FONT></B>和前边见到的<B normal"><FONT face="Times New Roman">2f</FONT></B>一样都是<FONT face="Times New Roman">GNU</FONT>汇编程序语法——它指向一个指令的地址;在此情形中,它是向后搜索时碰到的第一个本地标识“<FONT face="Times New Roman">1</FONT>”的地址。所以,这条指令是把<FONT face="Times New Roman">11653</FONT>行代码的地址压入栈中;这个地址将成为返回地址,以便在随后的跳转操作之后,执行过程还能返回到<B normal"><FONT face="Times New Roman">down</FONT></B>的末尾。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11656</FONT>:开始跳转到<B normal"><FONT face="Times New Roman">__down_failed</FONT></B>(不包括在本书之内)。这个函数在栈里保存几个寄存器并调用后边要介绍的<B normal"><FONT face="Times New Roman">__down</FONT></B>(<FONT face="Times New Roman">26932</FONT>行)来完成等待信号量的工作。一旦<B normal"><FONT face="Times New Roman">__down</FONT></B>返回了,<B normal"><FONT face="Times New Roman">__down_failed</FONT></B>就返回到<B normal"><FONT face="Times New Roman">down</FONT></B>,而它也随之返回。一直到进程获得了信号量<B normal"><FONT face="Times New Roman">__down</FONT></B>才会返回;最终结果就是只要<B normal"><FONT face="Times New Roman">down</FONT></B>返回,进程就得到信号量了,而不管它是立刻还是经过等待后获得的它。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11657</FONT>:伪汇编程序指令<B normal"><FONT face="Times New Roman">.previous</FONT></B>的作用未在正式文档中说明,但是它的意思肯定是还原到以前的段中,结束<FONT face="Times New Roman">11654</FONT>行里的伪指令<B normal"><FONT face="Times New Roman">.section</FONT></B>的作用效果。</P><P 12pt 0cm 3.2pt"><b>down_interruptible</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11664</FONT>:<B normal"><FONT face="Times New Roman">down_interruptible</FONT></B>函数被用于进程想要获得信号量但也愿意在等待它时被信号中断的情况。这个函数与<B normal"><FONT face="Times New Roman">down</FONT></B>的实现非常相似,不过有两个区别将在随后的两段里进行解释。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11666</FONT>:第一个区别是<B normal"><FONT face="Times New Roman">down_interruptible</FONT></B>函数返回一个<B normal"><FONT face="Times New Roman">int</FONT></B>值来指示是否它获得了信号量或者被一个信号所打断。在前一种情况里返回值(在<B normal"><FONT face="Times New Roman">result</FONT></B>里)是<FONT face="Times New Roman">0</FONT>,在后一种情况里它是负值。这部分上是由<FONT face="Times New Roman">11675</FONT>行代码完成的,如果函数未经等待获得了信号量则该行把<B normal"><FONT face="Times New Roman">result</FONT></B>设置为<FONT face="Times New Roman">0</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11679</FONT>:第二个区别是<B normal"><FONT face="Times New Roman">down_interruptible</FONT></B>函数跳转到<FONT face="Times New Roman">__<B normal">down_failed_interruptible</B></FONT>(不包括在本书之内)而不是<FONT face="Times New Roman">__<B normal">down_failed</B></FONT>。因循<FONT face="Times New Roman">__<B normal">down_failed</B></FONT>建立起来的模式,<FONT face="Times New Roman">__<B normal">down _failed_interruptible</B></FONT>只是调整几个寄存器并调用将在随后进行研究的<FONT face="Times New Roman">__<B normal">down_interruptible</B></FONT>函数(<FONT face="Times New Roman">26942</FONT>行)。要注意的是<FONT face="Times New Roman">11676</FONT>行为<FONT face="Times New Roman">__<B normal">down_failed_ interruptible</B></FONT>设置的返回目标跟在<B normal"><FONT face="Times New Roman">xorl</FONT></B>之后,<B normal"><FONT face="Times New Roman">xorl</FONT></B>用于在信号量可以被立刻获得的情况中把<B normal"><FONT face="Times New Roman">result</FONT></B>归<FONT face="Times New Roman">0</FONT>。<B normal"><FONT face="Times New Roman">down_interruptible</FONT></B>函数的返回值再被复制进<B normal"><FONT face="Times New Roman">result</FONT></B>中。</P><P 12pt 0cm 3.2pt"><b>down_trylock</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11687</FONT>:除了调用<FONT face="Times New Roman">__<B normal">down_failed_trylock</B></FONT>函数(当然还要调用<FONT face="Times New Roman">26961</FONT>行的<FONT face="Times New Roman">__<B normal">down_trylock</B></FONT>函数,我们将在后面对它进行检查)之外,<B normal"><FONT face="Times New Roman">down_trylock</FONT></B>函数和<B normal"><FONT face="Times New Roman">down_interruptible</FONT></B>函数相同。因此,在这里不必对<B normal"><FONT face="Times New Roman">down_trylock</FONT></B>函数进行更多解释。</P><P 12pt 0cm 3.2pt"><b>DOWN_VAR</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26900</FONT>:这是作为<FONT face="Times New Roman">__<B normal">down</B></FONT>和<FONT face="Times New Roman">_<B normal">down_interruptible</B></FONT>共同代码因子的三个宏中的第一个。它只是声明了几个变量。</P><P 12pt 0cm 3.2pt"><b>DOWN_HEAD</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26904</FONT>:这个宏使任务<B normal"><FONT face="Times New Roman">tsk</FONT></B>(被<B normal"><FONT face="Times New Roman">DOWN_VAR</FONT></B>所声明)转移到<B normal"><FONT face="Times New Roman">task_state</FONT></B>给出的状态,然后把<B normal"><FONT face="Times New Roman">tsk</FONT></B>添加到等待信号量的任务队列。最后,它开始一个无限循环,在此循环期间当<B normal"><FONT face="Times New Roman">__down</FONT></B>和<B normal"><FONT face="Times New Roman">__down_interruptible</FONT></B>准备退出时将使用<B normal"><FONT face="Times New Roman">break</FONT></B>语句结束该循环。</P><P 12pt 0cm 3.2pt"><b>DOWN_TAIL</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26926</FONT>:这个宏完成循环收尾工作,把<B normal"><FONT face="Times New Roman">tsk</FONT></B>设置回<B normal"><FONT face="Times New Roman">task_state</FONT></B>的状态,为再次尝试获得信号量做准备。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26929</FONT>:循环已经退出;<B normal"><FONT face="Times New Roman">tsk</FONT></B>已或者得到了信号量或者被一个信号中断了(仅适于<B normal"><FONT face="Times New Roman">__down_ interruptible</FONT></B>)。无论哪一种方式,任务已准备再次运行而不再等待该信号量了,因此它被转移回<B normal"><FONT face="Times New Roman">TASK_RUNNING</FONT></B>并从信号量的等待队列里被注销。</P><P 12pt 0cm 3.2pt"><b>__down</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26932</FONT>:<B normal"><FONT face="Times New Roman">__down</FONT></B>和<B normal"><FONT face="Times New Roman">__down_interruptible</FONT></B>遵循以下模式:</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo7; tab-stops: list 57.25pt"><FONT face="Times New Roman">1.         </FONT>用<B normal"><FONT face="Times New Roman">DOWN_VAR</FONT></B>声明所需的本地变量,随后可能还有补充的本地变量声明。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo7; tab-stops: list 57.25pt"><FONT face="Times New Roman">2.         </FONT>以<B normal"><FONT face="Times New Roman">DOWN_HEAD</FONT></B>开始进入无穷循环。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo7; tab-stops: list 57.25pt"><FONT face="Times New Roman">3.         </FONT>在循环体内完成函数特定的(<FONT face="Times New Roman">function-specific</FONT>)工作。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo7; tab-stops: list 57.25pt"><FONT face="Times New Roman">4.         </FONT>重新调度。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo7; tab-stops: list 57.25pt"><FONT face="Times New Roman">5.         </FONT>以<B normal"><FONT face="Times New Roman">DOWN_TAIL</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>章里讨论过)可以被移进<B normal"><FONT face="Times New Roman">DOWN_TAIL</FONT></B>宏中。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo7; tab-stops: list 57.25pt"><FONT face="Times New Roman">6.         </FONT>完成任何函数特定的收尾工作。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm">我将只对函数特定的步骤(第<FONT face="Times New Roman">3</FONT>和第<FONT face="Times New Roman">6</FONT>步)进行讨论。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26936</FONT>:<B normal"><FONT face="Times New Roman">__down</FONT></B>的循环体调用<B normal"><FONT face="Times New Roman">waking_non_zero</FONT></B>(未包括),它自动检查<B normal"><FONT face="Times New Roman">sem-&gt;waking</FONT></B>来判断是否进程正被<B normal"><FONT face="Times New Roman">up</FONT></B>唤醒。如果是这样,它将<B normal"><FONT face="Times New Roman">waking</FONT></B>归零并返回<FONT face="Times New Roman">1</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">__down</FONT></B>尝试获得信号量是在调用<B normal"><FONT face="Times New Roman">schedule</FONT></B>之前。如果信号量的计数值已知为负值时,为什么不用另一种相反的方式来实现它呢?实际上它对于第一遍循环之后的任何一遍重复都是没有影响的,但是去掉一次没有必要的检查可以稍微加快第一遍循环的速度。如果需要为此提出什么特别的理由的话,那可能就是因为自从信号量第一次被检查之后的几个微秒内它就应该可以被释放(可能是在另一个处理器上),而且额外获取标志要比一次额外调度所付出的代价少得多。因此<B normal"><FONT face="Times New Roman">__down</FONT></B>可能还可以在重新调度之前做一次快速检查。</P><P 12pt 0cm 3.2pt"><b>__down_interruptible</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26942</FONT>:<B normal"><FONT face="Times New Roman">__down_interruptible</FONT></B>除了允许被信号中断以外,它和<B normal"><FONT face="Times New Roman">__down</FONT></B>在本质上是一样的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26948</FONT>:所以,当获取信号量时对<B normal"><FONT face="Times New Roman">waking_non_zero_interruptible</FONT></B>(未包括)进行调用。如果它没能得到信号量就返回<FONT face="Times New Roman">0</FONT>,如果得到就返回<FONT face="Times New Roman">1</FONT>,或者如果它被一个信号所中断就返回<B normal"><FONT face="Times New Roman">–EINTR</FONT></B>。在第一种情况下,循环继续。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26958</FONT>:否则,<B normal"><FONT face="Times New Roman">__down_interruptible</FONT></B>退出,如果它得到信号量就返回<FONT face="Times New Roman">0</FONT>(不是<FONT face="Times New Roman">1</FONT>),或者假如被中断则返回<B normal"><FONT face="Times New Roman">–EINTR</FONT></B>。</P><P 12pt 0cm 3.2pt"><b>__down_trylock</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26961</FONT>:有时在不能立刻获得信号量的情况下,内核也需要继续运行。所以,<B normal"><FONT face="Times New Roman">__down_trylock</FONT></B>不在循环之内。它仅仅调用<B normal"><FONT face="Times New Roman">waking_nonzero_trylock</FONT></B>(未包括),该函数夺取信号量,如果失败就递增该信号量的<B normal"><FONT face="Times New Roman">count</FONT></B>(因为内核不打算继续等待下去)然后返回。</P><P 12pt 0cm 3.2pt"><b>up</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11714</FONT>:我们已经详尽的分析了内核尝试获得信号量时的情况,也讨论了它失败时的情况。现在是考察另一面的时候了:当释放一个信号量时将发生什么。这一部分相对简单。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11721</FONT>:原子性地递增信号量的计数值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11722</FONT>:如果结果小于等于<FONT face="Times New Roman">0</FONT>,就有某个进程正在等待被唤醒。<FONT face="Times New Roman">up</FONT>向前跳转到<FONT face="Times New Roman">11725</FONT>行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">11724</FONT>:<B normal"><FONT face="Times New Roman">up</FONT></B>采用了<B normal"><FONT face="Times New Roman">down</FONT></B>里同样的技巧:这一行进入了内核的单独的一段,而不是在<B normal"><FONT face="Times New Roman">up</FONT></B>本身的段内。<B normal"><FONT face="Times New Roman">up</FONT></B>的末尾的地址被压入栈然后<B normal"><FONT face="Times New Roman">up</FONT></B>跳转到<B normal"><FONT face="Times New Roman">__up_wakeup</FONT></B>(未包括)。这里完成如同<B normal"><FONT face="Times New Roman">__down_failed</FONT></B>一样的寄存器操作并调用下边要讨论的<B normal"><FONT face="Times New Roman">__up</FONT></B>函数。</P><P 12pt 0cm 3.2pt"><b>__up</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26877</FONT>:<B normal"><FONT face="Times New Roman">__up</FONT></B>函数负责唤醒所有等待该信号量的进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26897</FONT>:调用<B normal"><FONT face="Times New Roman">wake_one_more</FONT></B>(未包括在本书中),该函数检查是否有进程在等待该信号量,如果有,就增加<B normal"><FONT face="Times New Roman">waking</FONT></B>成员来通知它们可以尝试获取它了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26880</FONT>:利用<B normal"><FONT face="Times New Roman">wake_up</FONT></B>宏(<FONT face="Times New Roman">16612</FONT>行),它只是调用<B normal"><FONT face="Times New Roman">__wake_up</FONT></B>函数(<FONT face="Times New Roman">26829</FONT>行)来唤醒所有等待进程。</P><P 12pt 0cm 3.2pt"><b>__wake_up</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26829</FONT>:正如在第<FONT face="Times New Roman">2</FONT>章中所讨论的那样,<B normal"><FONT face="Times New Roman">__wake_up</FONT></B>函数唤醒所有传递给它的在等待队列上的进程,假如它们处于被<B normal"><FONT face="Times New Roman">mode</FONT></B>所隐含的状态之一的话。当从<B normal"><FONT face="Times New Roman">wake_up</FONT></B>被调用时,函数唤醒所有处于<B normal"><FONT face="Times New Roman">TASK_UNINTERRUPTIBLE</FONT></B>或<B normal"><FONT face="Times New Roman">TASK_INTERRUPTIBLE</FONT></B>状态的进程;当从<B normal"><FONT face="Times New Roman">wake_up_interruptible</FONT></B>(<FONT face="Times New Roman">16614</FONT>行)被调用时,它只唤醒处于<B normal"><FONT face="Times New Roman">TASK_INTERRUPTIBLE</FONT></B>状态的任务。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26842</FONT>:进程用<B normal"><FONT face="Times New Roman">wake_up_process</FONT></B>(<FONT face="Times New Roman">26356</FONT>行)被唤醒,该函数曾在以前提到过,它将在本章随后进行详细介绍。</P><P 0cm 0cm 0pt">现在所感兴趣的是唤醒所有进程后的结果。因为<B normal"><FONT face="Times New Roman">__wake_up</FONT></B>唤醒所有队列里的进程,而不仅仅是队列里的第一个,所以它们都要竞争信号量——在<FONT face="Times New Roman">SMP</FONT>里,它们可以精确的同时做这件事。通常,获胜者将首先获得<FONT face="Times New Roman">CPU</FONT>。这个进程将是拥有最大“<FONT face="Times New Roman">goodness</FONT>”的进程(回忆一下第<FONT face="Times New Roman">7</FONT>章中<FONT face="Times New Roman">26338</FONT>行对<B normal"><FONT face="Times New Roman">goodness</FONT></B>的讨论)。<FONT face="Times New Roman"> </FONT>这一点意义非常重大,因为拥有更高优先权的进程应该首先被给予继续其工作的机会。(这对于实时进程尤其重要。)</P><P 0cm 0cm 0pt">这种方案的不足之处是有发生饥饿(<FONT face="Times New Roman">starvation</FONT>)的危险,这发生在一个进程永远不能得到它赖以继续运行的资源时。这里可能会发生饥饿现象:假如两个进程反复竞争同一个信号量,而第一个进程总是有比第二个更高的优先权,那么第二个进程将永远不会得到<FONT face="Times New Roman">CPU</FONT>。这种场景同它应该的运行方式存在一定差距——设想一个是实时进程而另一个以<FONT face="Times New Roman">20</FONT>的<FONT face="Times New Roman">niceness</FONT>运行。我们可以通过只唤醒队列里第一个进程的方法来避免这种饥饿的危险,可是那样又将意味着有时候会耽误从各个方面来说都更有资格的进程对<FONT face="Times New Roman">CPU</FONT>的使用。</P><P 0cm 0cm 0pt">以前对此没有讨论过,可是<FONT face="Times New Roman">Linux</FONT>的调度程序在适当的环境下也能够使得<FONT face="Times New Roman">CPU</FONT>的一个进程被彻底饿死。这不完全是一件坏事——只是一种设计决策而已——而且至少应用于通篇内核代码的原则是一致的,这就很好。还要注意的是使用前边讨论过的其它机制,饥饿现象也同样会发生。例如说,<FONT face="Times New Roman">test-and-set</FONT>原语就是和内核信号量一样的潜在饥饿根源。</P><P 0cm 0cm 0pt">无论如何,在实际中,饥饿是非常少见的——它只是一个有趣的理论案例。</P><H3 13pt 0cm"><FONT face="Times New Roman" size=5>Spinlocks</FONT></H3><P 0cm 0cm 0pt">这一章里最后一个重要的并行程序设计原语是自旋锁(<FONT face="Times New Roman">spinlock</FONT>)。自旋锁的思想就是在一个密封的循环里坚持反复尝试夺取一个资源(一把锁)直到成功为止。这通常是通过在类似<FONT face="Times New Roman">test-and-set</FONT>操作之上进行循环来实现的——即,旋转(<FONT face="Times New Roman">spinning</FONT>)——一直到获得该锁。</P><P 0cm 0cm 0pt">如果这听起来好像是一个二元信号量,那是因为它就是一个二元信号量。自旋锁和二元信号量唯一的概念区别就是你不必循环等待一个信号量——你可以夺取信号量,也可以在不能立刻得到它时放弃申请。因此,自旋锁原本是可以通过在信号量代码外再包裹一层循环来实现的。不过,因为自旋锁是信号量的一个受限特例,它们有更高效的实现方法。</P><P 0cm 0cm 0pt">自旋锁变量——其中的一位被测试和设置——总是<B normal"><FONT face="Times New Roman">spinlock_t</FONT></B>类型(<FONT face="Times New Roman">12785</FONT>行)。只有<B normal"><FONT face="Times New Roman">spinlock_t</FONT></B>的最低位被使用;如果锁可用,则它是<FONT face="Times New Roman">0</FONT>,如果被取走,则它是<FONT face="Times New Roman">1</FONT>。在一个声明里,自旋锁被初始化为值<B normal"><FONT face="Times New Roman">SPIN_LOCK_UNLOCKED</FONT></B>(<FONT face="Times New Roman">12789</FONT>行);它也可以用<B normal"><FONT face="Times New Roman">spin_lock_init</FONT></B>函数(<FONT face="Times New Roman">12791</FONT>行)来初始化。这两者都把<B normal"><FONT face="Times New Roman">spinlock_t</FONT></B>的<B normal"><FONT face="Times New Roman">lock</FONT></B>成员设置成<FONT face="Times New Roman">0</FONT>——也就是未锁状态。</P><P 0cm 0cm 0pt">注意<FONT face="Times New Roman">12795</FONT>行代码简洁地对公平性进行了考虑并最后抛弃了它——公平是饥饿的背面,正如我们前面已经介绍过的(使得一个<FONT face="Times New Roman">CPU</FONT>或进程饥饿应被认为是“不公平的”)。</P><P 0cm 0cm 0pt">自旋锁的加锁和解锁宏建立在<B normal"><FONT face="Times New Roman">spin_lock_string</FONT></B>和<B normal"><FONT face="Times New Roman">sping_unlock_string</FONT></B>函数之上,所以这一小节只对<B normal"><FONT face="Times New Roman">spin_lock_string</FONT></B>和<B normal"><FONT face="Times New Roman">sping_unlock_string</FONT></B>函数进行详述。其它宏如果有的话只是增加了<FONT face="Times New Roman">IRQ</FONT>加锁和解锁。</P><P 12pt 0cm 3.2pt"><b>spin_lock_string</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12805</FONT>:这个宏的代码对于所有自旋锁加锁的宏都是相同的。它也被用于<FONT face="Times New Roman">x86</FONT>专用的<B normal"><FONT face="Times New Roman">lock_ kernel</FONT></B>和<B normal"><FONT face="Times New Roman">unlock_kernel</FONT></B>版本之中(它们不在本书之列,不过其常规版本则是包括的——参见<FONT face="Times New Roman">10174</FONT>和<FONT face="Times New Roman">10182</FONT>行)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12807</FONT>:尝试测试和设置自旋锁的最低位,这要把内存总线锁住以便对于任何其它对同一个自旋锁的访问来说这个操作都是原子的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12808</FONT>:如果成功了,控制流程就继续向下运行;否则,<B normal"><FONT face="Times New Roman">spin_lock_string</FONT></B>函数向前跳转到第<FONT face="Times New Roman">12810</FONT>行(<B normal"><FONT face="Times New Roman">btsl</FONT></B>把这一位的原值放入<FONT face="Times New Roman">CPU</FONT>的进位标志位(<FONT face="Times New Roman">Carry flag</FONT>),这正是这里使用<B normal"><FONT face="Times New Roman">jc</FONT></B>的原因)。同样的技巧我们已经看到过三次了:跳转目标放在内核的单独一段中。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12811</FONT>:在封闭的循环里不停地检测循环锁的最低位。注意<B normal"><FONT face="Times New Roman">btsl</FONT></B>和<B normal"><FONT face="Times New Roman">testb</FONT></B>以不同方式解释它们第一个操作数——对于<B normal"><FONT face="Times New Roman">btsl</FONT></B>,它是一个位状态(<FONT face="Times New Roman">bit position</FONT>),而对于<B normal"><FONT face="Times New Roman">testb</FONT></B>,它是一个位屏蔽(<FONT face="Times New Roman">bitmask</FONT>)。因此,<FONT face="Times New Roman">12811</FONT>行在测试<B normal"><FONT face="Times New Roman">spin_lock_string</FONT></B>曾在<FONT face="Times New Roman">12807</FONT>行已经试图设置(但失败了)的同一位,尽管一个使用<B normal"><FONT face="Times New Roman">$0</FONT></B>而另一个使用<B normal"><FONT face="Times New Roman">$1</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12813</FONT>:该位被清除了,所以<B normal"><FONT face="Times New Roman">spin_lock_string</FONT></B>应该再次夺取它。函数调转回第<FONT face="Times New Roman">12806</FONT>行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>这个代码可以只用加上<B normal"><FONT face="Times New Roman">lock</FONT></B>前缀的两条代码加以简化:</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            1: lock ; btsl $0, %0</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">              jc 1b</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>不过,使用这个简化版本的话,系统性能将明显受到损害,这因为每次循环重复内存总线都要被加锁。内核使用的版本虽然长一些,但是它可以使其它<FONT face="Times New Roman">CPU</FONT>运行的更有效,这是由于该版本只有在它有充分理由相信能够获得锁的时候才会锁住内存总线。</P><P 12pt 0cm 3.2pt"><b>spin_unlock_string</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12816</FONT>:并不很重要:只是重新设置了自旋锁的锁定位(<FONT face="Times New Roman">lock bit</FONT>)。</P><P 12pt 0cm 3.2pt"><b><FONT size=3>读/写自旋锁</FONT></b></P><P 0cm 0cm 0pt">自旋锁的一个特殊情况就是读<FONT face="Times New Roman">/</FONT>写自旋锁。这里的思想是这样的:在某些情况中,我们想要允许某个对象有多个读者,但是当有一个写者正在写入这个对象时,则不允许它再有其它读者或者写者。</P><P 0cm 0cm 0pt">遵循基于<B normal"><FONT face="Times New Roman">spinlock_t</FONT></B>的自旋锁的同样模式,读<FONT face="Times New Roman">/</FONT>写自旋锁是用<B normal"><FONT face="Times New Roman">rwlock_t</FONT></B>(<FONT face="Times New Roman">12853</FONT>行)来代表的,它可以在有<B normal"><FONT face="Times New Roman">RW_LOCK_UNLOCKED</FONT></B>(<FONT face="Times New Roman">12858</FONT>行)的声明里被初始化。与<B normal"><FONT face="Times New Roman">rwlock_t</FONT></B>一起工作的最低级的宏是<B normal"><FONT face="Times New Roman">read_lock</FONT></B>、<B normal"><FONT face="Times New Roman">read_unlock</FONT></B>、<B normal"><FONT face="Times New Roman">write_lock</FONT></B>,以及<B normal"><FONT face="Times New Roman">write_unlock</FONT></B>,它们在本小节中进行描述。很明显,那些跟随在这些宏之后并建立在它们之上的宏,自然要在你理解了最初的这四个宏之后在去接触。</P><P 0cm 0cm 0pt">正如第<FONT face="Times New Roman">12860</FONT>行注释中所声明的,当写锁(<FONT face="Times New Roman">write lock</FONT>)被占有时,<B normal"><FONT face="Times New Roman">rwlock_t</FONT></B>的<B normal"><FONT face="Times New Roman">lock</FONT></B>成员是负值。当既没有读者也没有写者时它为<FONT face="Times New Roman">0</FONT>,当只有读者而没有写者时它是正值——在这种情况下,<B normal"><FONT face="Times New Roman">lock</FONT></B>将对读者的数目进行计数。</P><P 12pt 0cm 3.2pt"><b>read_lock</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12867</FONT>:开始于<B normal"><FONT face="Times New Roman">rwlock_t</FONT></B>的<B normal"><FONT face="Times New Roman">lock</FONT></B>成员的自动递增。这是推测性的操作——它可以被撤销。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12868</FONT>:如果它在增量之后为负,表示某个进程占用了写锁——或者至少是某个进程正试图得到它。<B normal"><FONT face="Times New Roman">read_lock</FONT></B>向前跳到第<FONT face="Times New Roman">12870</FONT>行(注意,在一个不同的内核段里)。否则,没有写者退出(尽管还有可能有,或者也有可能没有其它读者——这并不重要),所以可以继续执行读锁定(<FONT face="Times New Roman">read-locked</FONT>)代码。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12870</FONT>:一个写者出现了。<B normal"><FONT face="Times New Roman">read_lock</FONT></B>取消第<FONT face="Times New Roman">12867</FONT>行增值操作的影响。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12871</FONT>:循环等待<B normal"><FONT face="Times New Roman">rwlock_t</FONT></B>的<B normal"><FONT face="Times New Roman">lock</FONT></B>变为<FONT face="Times New Roman">0</FONT>或正值。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">12873</FONT>:跳回到第<FONT face="Times New Roman">12866</FONT>行再次尝试。</P><P 12pt 0cm 3.2pt"><b>read_unlock</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12878</FONT>:不太复杂:只是递减该计数值。</P><P 12pt 0cm 3.2pt"><b>write_lock</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12883</FONT>:表示出有一个进程需要写锁:检测并设置<B normal"><FONT face="Times New Roman">lock</FONT></B>的符号位并保证<B normal"><FONT face="Times New Roman">lock</FONT></B>的值是负的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12884</FONT>:如果符号位已经被设置,则另外有进程占有了写锁;<B normal"><FONT face="Times New Roman">write_lock</FONT></B>向前跳转到第<FONT face="Times New Roman">12889</FONT>行(同以前一样,那是在一个不同的内核段里)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12885</FONT>:没有别的进程正试图获得该写锁,可是读者仍可以退出。因为符号位被设置了,读者不能获得读锁,但是<B normal"><FONT face="Times New Roman">write_lock</FONT></B>仍然必须等待正在退出的读者完全离开。它通过检查低端的<FONT face="Times New Roman">31</FONT>位中是否任何一位被设置过开始,这可以表示<B normal"><FONT face="Times New Roman">lock</FONT></B>以前曾是正值。如果没有,则<B normal"><FONT face="Times New Roman">lock</FONT></B>在符号位反转之前曾是<FONT face="Times New Roman">0</FONT>,这意味着没有读者;因而,这对于写者的继续工作是很安全的,所以控制流程就可以继续向下运行了。不过,如果低端<FONT face="Times New Roman">31</FONT>位中任何一位被设置过了,也就是说有读者了,这样<B normal"><FONT face="Times New Roman">write_lock</FONT></B>就会向前跳转到第<FONT face="Times New Roman">12888</FONT>行等到它们结束。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12888</FONT>:该进程是仅有的写者,但是有若干读者。<B normal"><FONT face="Times New Roman">write_lock</FONT></B>会暂时清除符号位(这个宏稍后将再次操纵它)。有趣的是,对符号位进行这样的胡乱操作并不会影响读者操纵<B normal"><FONT face="Times New Roman">lock</FONT></B>的正确性。考虑作为示例的下列顺序事件:</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l5 level1 lfo8; tab-stops: list 57.25pt"><FONT face="Times New Roman">1.         </FONT>两个读者增加了<B normal"><FONT face="Times New Roman">lock</FONT></B>;<B normal"><FONT face="Times New Roman">lock</FONT></B>用十六进制表示现在是<FONT face="Times New Roman">0x00000002</FONT>。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l5 level1 lfo8; tab-stops: list 57.25pt"><FONT face="Times New Roman">2.         </FONT>一个即将成为写者的进程设置了符号位;<B normal"><FONT face="Times New Roman">lock</FONT></B>现在是<FONT face="Times New Roman">0x80000002</FONT>。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l5 level1 lfo8; tab-stops: list 57.25pt"><FONT face="Times New Roman">3.         </FONT>读者中的一个离开;<B normal"><FONT face="Times New Roman">lock</FONT></B>现在是<FONT face="Times New Roman">0x80000001</FONT>。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l5 level1 lfo8; tab-stops: list 57.25pt"><FONT face="Times New Roman">4.         </FONT>写者看到剩余的位不全部是<FONT face="Times New Roman">0</FONT>——仍然有读者存在。这样它根本没有写锁,因此它就清除符号位;<B normal"><FONT face="Times New Roman">lock</FONT></B>现在是<FONT face="Times New Roman">0x00000001</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">12889</FONT>:循环等待计数值降到<FONT face="Times New Roman">0</FONT>——也就是等待所有读者退出。实际上,<FONT face="Times New Roman">0</FONT>除了表示所有读者已离开之外,它还表示着没有其它进程获得了写锁。</P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">12891</FONT>:所有读者和写者都结束了操作;<B normal"><FONT face="Times New Roman">write_lock</FONT></B>又从头开始,并再次获得写锁。</P><P 12pt 0cm 3.2pt"><b>write_unlock</b></P><P 0cm 0cm 0pt; TEXT-INDENT: 0cm"><FONT face="Times New Roman">12896</FONT>:不太重要:只是重置符号位。</P><H2 13pt 0cm">APICs和CPU-To-CPU通信</H2><P 0cm 0cm 0pt"><FONT face="Times New Roman">Intel </FONT>多处理规范的核心就是高级可编程中断控制器(<FONT face="Times New Roman">Advanced Programmable Interrupt Controllers</FONT>——<FONT face="Times New Roman">APICs</FONT>)的使用。<FONT face="Times New Roman">CPU</FONT>通过彼此发送中断来完成它们之间的通信。通过给中断附加动作(<FONT face="Times New Roman">actions</FONT>),不同的<FONT face="Times New Roman">CPU</FONT>可以在某种程度上彼此进行控制。每个<FONT face="Times New Roman">CPU</FONT>有自己的<FONT face="Times New Roman">APIC</FONT>(成为那个<FONT face="Times New Roman">CPU</FONT>的本地<FONT face="Times New Roman">APIC</FONT>),并且还有一个<FONT face="Times New Roman">I/O APIC</FONT>来处理由<FONT face="Times New Roman">I/O</FONT>设备引起的中断。在普通的多处理器系统中,<FONT face="Times New Roman">I/O APIC</FONT>取代了第<FONT face="Times New Roman">6</FONT>章里提到的中断控制器芯片组的作用。</P><P 0cm 0cm 0pt">这里有几个示例性的函数来让你了解其工作方式的风格。</P><P 12pt 0cm 3.2pt"><b>smp_send_reschedule</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">5019</FONT>:<FONT face="Times New Roman"> </FONT>这个函数只有一行,其作用将在本章随后进行说明,它仅仅是给其<FONT face="Times New Roman">ID</FONT>以参数形式给出了的目标<FONT face="Times New Roman">CPU</FONT>发送一个中断。函数用<FONT face="Times New Roman">CPU ID</FONT>和<B normal"><FONT face="Times New Roman">RESCHEDULE_VECTOR</FONT></B>向量调用<B normal"><FONT face="Times New Roman">send_IPI_single</FONT></B>函数(<FONT face="Times New Roman">4937</FONT>行)。<B normal"><FONT face="Times New Roman">RESCHEDULE_VECTOR</FONT></B>与其它<FONT face="Times New Roman">CPU</FONT>中断向量是一起在第<FONT face="Times New Roman">1723</FONT>行开始的一个定义块中被定义的。</P>
作者: ilikenba    时间: 2005-3-4 22:29
< 12pt 0cm 3.2pt"><b>send_IPI_single</b></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">4937</FONT>:<FONT face="Times New Roman"> <B normal">send_IPI_single</B></FONT>函数发送一个<FONT face="Times New Roman">IPI</FONT>——那是<FONT face="Times New Roman">Intel</FONT>对处理器间中断(<FONT face="Times New Roman">interprocessor interrupt</FONT>)的称呼——给指定的目的<FONT face="Times New Roman">CPU</FONT>。在这一行,内核以相当低级的方式与发送<FONT face="Times New Roman">CPU</FONT>的本地<FONT face="Times New Roman">APIC</FONT>对话。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">4949</FONT>:<FONT face="Times New Roman"> </FONT>得到中断命令寄存器(<FONT face="Times New Roman">ICR</FONT>)高半段的内容——本地<FONT face="Times New Roman">APIC</FONT>就是通过这个寄存器进行编程的——不过它的目的信息段要被设置为<B normal"><FONT face="Times New Roman">dest</FONT></B>。尽管<B normal"><FONT face="Times New Roman">__prepare_ICR2</FONT></B>(<FONT face="Times New Roman">4885</FONT>行)里使用了“<FONT face="Times New Roman">2</FONT>”,<FONT face="Times New Roman">CPU</FONT>实际上只有一个<FONT face="Times New Roman">ICR</FONT>而不是两个。但是它是一个<FONT face="Times New Roman">64</FONT>位寄存器,内核更愿意把它看作是两个<FONT face="Times New Roman">32</FONT>位寄存器——在内核代码里,“<FONT face="Times New Roman">ICR</FONT>”表示这个寄存器的低端<FONT face="Times New Roman">32</FONT>位,所以“<FONT face="Times New Roman">ICR2</FONT>”就表示高端<FONT face="Times New Roman">32</FONT>位。我们想要设置的的目的信息段就在高端<FONT face="Times New Roman">32</FONT>位,即<FONT face="Times New Roman">ICR2</FONT>里。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">4950</FONT>:<FONT face="Times New Roman"> </FONT>把修改过的信息写回<FONT face="Times New Roman">ICR</FONT>。现在<FONT face="Times New Roman">ICR</FONT>知道了目的<FONT face="Times New Roman">CPU</FONT>。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">4953</FONT>:<FONT face="Times New Roman"> </FONT>调用<B normal"><FONT face="Times New Roman">__prepare_ICR</FONT></B>(<FONT face="Times New Roman">4874</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">ICR</FONT>完全能够发送一个<FONT face="Times New Roman">IPI</FONT>给它自己的<FONT face="Times New Roman">CPU</FONT>。尽管这样,我还是没有找到有任何理由要这样做。)</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">4957</FONT>:<FONT face="Times New Roman"> </FONT>通过往<FONT face="Times New Roman">ICR</FONT>里写入新的配置来发送中断。</P><H2 13pt 0cm">SMP支持如何影响内核</H2>< 0cm 0cm 0pt">既然读者已经学习了能够成功支持<FONT face="Times New Roman">SMP</FONT>的若干原语,那么就让我们来纵览一下内核的<FONT face="Times New Roman">SMP</FONT>支持吧。本章剩余的部分将局限于对分布在内核之中的那些具有代表性的<FONT face="Times New Roman">SMP</FONT>代码进行讨论。</P><H3 13pt 0cm"><FONT size=5>对调度的影响</FONT></H3>< 0cm 0cm 0pt"><B normal"><FONT face="Times New Roman">schedule</FONT></B>(<FONT face="Times New Roman">26686</FONT>行)正是内核的调度函数,它已在第<FONT face="Times New Roman">7</FONT>章中全面地介绍过了。<B normal"><FONT face="Times New Roman">schedule</FONT></B>的<FONT face="Times New Roman">SMP</FONT>版本与<FONT face="Times New Roman">UP</FONT>的相比有两个主要区别:</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo1; tab-stops: list 42.25pt">l           在<B normal"><FONT face="Times New Roman">schedule</FONT></B>里从第<FONT face="Times New Roman">26780</FONT>开始的一段代码要计算某些其它地方所需的信息。</P>< 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo1; tab-stops: list 42.25pt">l           在<FONT face="Times New Roman">SMP</FONT>和<FONT face="Times New Roman">UP</FONT>上都要发生的对<B normal"><FONT face="Times New Roman">__schedule_tail</FONT></B>的调用(<FONT face="Times New Roman">26638</FONT>行)实际上在<FONT face="Times New Roman">UP</FONT>上并无作用,因为<B normal"><FONT face="Times New Roman">__schedule_tail</FONT></B>完全是为<FONT face="Times New Roman">SMP</FONT>所写的代码,所以从实用的角度来说它就是<FONT face="Times New Roman">SMP</FONT>所特有的。</P>< 12pt 0cm 3.2pt"><b>schedule</b></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26784</FONT>:获取当前时间,也就是自从机器开机后时钟流逝的周期数。这很像是检查<B normal"><FONT face="Times New Roman">jiffies</FONT></B>,不过是以<FONT face="Times New Roman">CPU</FONT>周期而不是以时钟滴答作为计时方法的——显然,这要精确得多。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26785</FONT>:计算自从<B normal"><FONT face="Times New Roman">schedule</FONT></B>上一次在此<FONT face="Times New Roman">CPU</FONT>上进行调度后过去了多长时间,并且为下一次的计算而记录下当前周期计数。(<B normal"><FONT face="Times New Roman">schedule_data</FONT></B>是每个<FONT face="Times New Roman">CPU <B normal">aligned_data</B></FONT>数组的一部分,它在<FONT face="Times New Roman">26628</FONT>行定义。)</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26790</FONT>:进程的<B normal"><FONT face="Times New Roman">avg_slice</FONT></B>成员(<FONT face="Times New Roman">16342</FONT>行)记录该进程在其生命周期里占有<FONT face="Times New Roman">CPU</FONT>的平均时间。可是这并不是简单的平均——它是加权平均,进程近期的活动远比很久以前的活动权值大。(因为真实计算机的计算是有穷的,“很久以前”的部分在足够远以后,将逐渐趋近于<FONT face="Times New Roman">0</FONT>。)这将在<B normal"><FONT face="Times New Roman">reschedule_idle</FONT></B>中(<FONT face="Times New Roman">26221</FONT>行,下文讨论)被用来决定是否把进程调入另一个<FONT face="Times New Roman">CPU</FONT>中。因此,在<FONT face="Times New Roman">UP</FONT>的情况下它是无需而且也不会被计算的。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26797</FONT>:记录哪一个<FONT face="Times New Roman">CPU</FONT>将运行<B normal"><FONT face="Times New Roman">next</FONT></B>(它将在当前的<FONT face="Times New Roman">CPU</FONT>上被执行),并引发它的<B normal"><FONT face="Times New Roman">has_cpu</FONT></B>标志位。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26803</FONT>:如果上下文环境发生了切换,<B normal"><FONT face="Times New Roman">schedule</FONT></B>记录失去<FONT face="Times New Roman">CPU</FONT>的进程——这将在下文的<B normal"><FONT face="Times New Roman">__schedule_tail</FONT></B>中被使用到。</P>< 12pt 0cm 3.2pt"><b>__schedule_tail</b></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26654</FONT>:如果失去<FONT face="Times New Roman">CPU</FONT>的任务已经改变了状态(这一点在前边的注释里解释过了),它将被标记以便今后的重新调度。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26664</FONT>:因为内核已经调度出了这个进程,它就不再拥有<FONT face="Times New Roman">CPU</FONT>了——这样的事实也将被记录。</P>< 12pt 0cm 3.2pt"><b>reschedule_idle</b></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26221</FONT>:当已经不在运行队列里的进程被唤醒时,<B normal"><FONT face="Times New Roman">wake_up_process</FONT></B>将调用<B normal"><FONT face="Times New Roman">reschedule_idle</FONT></B>,进程是作为<B normal"><FONT face="Times New Roman">p</FONT></B>而被传递进<B normal"><FONT face="Times New Roman">reschedule_idle</FONT></B>中的。这个函数试图把新近唤醒的进程在一个不同的<FONT face="Times New Roman">CPU</FONT>上进行调度——即一个空闲的<FONT face="Times New Roman">CPU</FONT>上。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26225</FONT>:这个函数的第一部分在<FONT face="Times New Roman">SMP</FONT>和<FONT face="Times New Roman">UP</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 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26263</FONT>:现在来到<FONT face="Times New Roman">SMP</FONT>部分,它仅仅适用于在上述测试中失败了的那些进程——虽然这种现象经常发生。<B normal"><FONT face="Times New Roman">reschedule_idle</FONT></B>必须确定是否要在另一个<FONT face="Times New Roman">CPU</FONT>上尝试运行该进程。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>正如在对<B normal"><FONT face="Times New Roman">schedule</FONT></B>的讨论中所提到的那样,一个进程的<B normal"><FONT face="Times New Roman">avg_slice</FONT></B>成员是它对<FONT face="Times New Roman">CPU</FONT>使用的加权平均值;因此,它说明了假如该进程继续运行的话是否它可能要控制<FONT face="Times New Roman">CPU</FONT>一段相对来说较长的时间。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26264</FONT>:这个<B normal"><FONT face="Times New Roman">if</FONT></B>条件判断的第二个子句使用<B normal"><FONT face="Times New Roman">related</FONT></B>宏(就在本函数之上的第<FONT face="Times New Roman">26218</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 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26267</FONT>:否则,<B normal"><FONT face="Times New Roman">reschedule_idle_slow</FONT></B>(接下来讨论)被调用以决定是否进程应当被删除。</P>< 12pt 0cm 3.2pt"><b>reschedule_idle_slow</b></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26157</FONT>:正如注释中所说明的,<B normal"><FONT face="Times New Roman">reschedule_idle_slow</FONT></B>试图找出一个空闲<FONT face="Times New Roman">CPU</FONT>来贮存<B normal"><FONT face="Times New Roman">p</FONT></B>。这个算法是基于如下观察结果的,即<B normal"><FONT face="Times New Roman">task</FONT></B>数组的前<FONT face="Times New Roman">n</FONT>项是系统的空闲进程,机器的<FONT face="Times New Roman">n</FONT>个<FONT face="Times New Roman">CPU</FONT>中每个都对应一个这样的空闲进程。这些空闲进程当(且仅当)对应<FONT face="Times New Roman">CPU</FONT>上没有其它进程需要处理器时才会运行。如果可能,函数通常是用<B normal"><FONT face="Times New Roman">hlt</FONT></B>指令使<FONT face="Times New Roman">CPU</FONT>进入低功耗的“睡眠”状态。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>因此,如果有空闲<FONT face="Times New Roman">CPU</FONT>存在的话,对任务数组的前<FONT face="Times New Roman">n</FONT>个进程进行循环是找出一个空闲<FONT face="Times New Roman">CPU</FONT>所必须的。<B normal"><FONT face="Times New Roman">reschedule_idle_slow</FONT></B>函数只需简单的查询每个空闲进程是否此刻正在运行着;如果是这样,它所在的<FONT face="Times New Roman">CPU</FONT>就一定是空闲的,这就为进程<B normal"><FONT face="Times New Roman">p</FONT></B>提供了一个很好的候选地点来运行。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </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">quick-and-dirty</FONT>)”的处理方式的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26180</FONT>:建立本地变量。<B normal"><FONT face="Times New Roman">best_cpu</FONT></B>是此时正在运行的<FONT face="Times New Roman">CPU</FONT>;它是“最佳”的<FONT face="Times New Roman">CPU</FONT>,因为<B normal"><FONT face="Times New Roman">p</FONT></B>在其上会避免缓冲区溢出或其它的开销麻烦<B normal">。</B><B normal"><FONT face="Times New Roman">this_cpu</FONT></B>是运行<B normal"><FONT face="Times New Roman">reschedule_idle_slow</FONT></B>的<FONT face="Times New Roman">CPU</FONT>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26182</FONT>:<B normal"><FONT face="Times New Roman">idle</FONT></B>和<B normal"><FONT face="Times New Roman">tsk</FONT></B>将沿<B normal"><FONT face="Times New Roman">task</FONT></B>数组进行遍历,<B normal"><FONT face="Times New Roman">target_tsk</FONT></B>将是所找到的最后一个正在运行的空闲进程(或者假如没有空闲进程它就为<B normal"><FONT face="Times New Roman">NULL</FONT></B>)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26183</FONT>:<FONT face="Times New Roman"><B normal">i</B> </FONT>从<B normal"><FONT face="Times New Roman">smp_num_cpus</FONT></B>(前边被叫作<FONT face="Times New Roman">n</FONT>)开始并且在每一次循环后都递减。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26189</FONT>:假如这个空闲进程的<B normal"><FONT face="Times New Roman">has_cpu</FONT></B>标志被设置,它就正在它的<FONT face="Times New Roman">CPU</FONT>上运行着(我们将称这样的<FONT face="Times New Roman">CPU</FONT>为“目标(<FONT face="Times New Roman">target</FONT>)<FONT face="Times New Roman">CPU</FONT>”)。如果该标志没有被设置,那么目标<FONT face="Times New Roman">CPU</FONT>就正被某个其它进程占用着;因而,它也就不是空闲的,这样<B normal"><FONT face="Times New Roman">reschedule_idle_slow</FONT></B>将不会把<B normal"><FONT face="Times New Roman">p</FONT></B>发送到那里。刚刚提及问题的反面在这里出现了:现在仅因为<FONT face="Times New Roman">CPU</FONT>不空闲并不能表示它所有的进程都不会死亡而使其空闲下来。可是<B normal"><FONT face="Times New Roman">reschedule_idle_slow</FONT></B>无法知道这种情形,所以它最好还是假定目标<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">26190</FONT>:不过假如<FONT face="Times New Roman">CPU</FONT>目标就是当前<FONT face="Times New Roman">CPU</FONT>,它就会被跳过。这看来很怪,不过无论怎样这都是“不可能发生”的情况:一个空闲进程的<B normal"><FONT face="Times New Roman">counter</FONT></B>是负值,在第<FONT face="Times New Roman">26226</FONT>行的测试将早已阻止这个函数执行到这一步了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26192</FONT>:找到一个可用的空闲<FONT face="Times New Roman">CPU</FONT>;相关的空闲进程被保存在<B normal"><FONT face="Times New Roman">target_tsk</FONT></B>中。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>既然已找到了空闲<FONT face="Times New Roman">CPU</FONT>,为什么现在不中断循环呢?这是因为继续循环可能会发现<B normal"><FONT face="Times New Roman">p</FONT></B>当前所在的处理器也是空闲的,在两个<FONT face="Times New Roman">CPU</FONT>都空闲时,维持在当前处理器上运行要比把它送往另一个好一些。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26193</FONT>:这一步<B normal"><FONT face="Times New Roman">reschedule_idle_slow</FONT></B>检查是否<B normal"><FONT face="Times New Roman">p</FONT></B>所在的处理器空闲。如果刚才找到的空闲<FONT face="Times New Roman">CPU</FONT>就是<B normal"><FONT face="Times New Roman">p</FONT></B>所在的,函数将向前跳转到<B normal"><FONT face="Times New Roman">send</FONT></B>标记处(<FONT face="Times New Roman">26203</FONT>行)来在那个<FONT face="Times New Roman">CPU</FONT>上对<B normal"><FONT face="Times New Roman">p</FONT></B>进行调度。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26199</FONT>:函数已经转向另一个<FONT face="Times New Roman">CPU</FONT>;它要递减。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">26204</FONT>:如果循环遍历了所有空闲的<FONT face="Times New Roman">CPU</FONT>,该<FONT face="Times New Roman">CPU</FONT>的空闲任务就被标记为重新调度并且<B normal"><FONT face="Times New Roman">smp_ send_reschedule</FONT></B>(<FONT face="Times New Roman">26205</FONT>行)会给那个<FONT face="Times New Roman">CPU</FONT>发送一个<FONT face="Times New Roman">IPI</FONT>以便它可以重新对其进程进行调度。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>正如读者所见到的,<B normal"><FONT face="Times New Roman">reschedule_idle_slow</FONT></B>是<FONT face="Times New Roman">CPU</FONT>之间协调无需在<FONT face="Times New Roman">UP</FONT>系统中所进行的工作的典范示例。对于<FONT face="Times New Roman">UP</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">SMP</FONT>机器必须花费一些代价来决定系统中哪一个<FONT face="Times New Roman">CPU</FONT>是该进程的最佳栖身之所。当然,换来的速度极大提高使得这些额外的努力还是相当合算的。</P><P 12pt 0cm 3.2pt"><b>release</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22951</FONT>:<B normal"><FONT face="Times New Roman">release</FONT></B>中非<FONT face="Times New Roman">SMP</FONT>特有的部分在第<FONT face="Times New Roman">7</FONT>章中已经介绍过了——在这里,一个僵进程(<FONT face="Times New Roman">zombie</FONT>)将被送往坟墓,而且其<B normal"><FONT face="Times New Roman">struct task_struct</FONT></B>将被释放。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22960</FONT>:查看是否该进程拥有一个<FONT face="Times New Roman">CPU</FONT>。(拥有它的<FONT face="Times New Roman">CPU</FONT>可能还没有清除这个标志;但是它马上就将执行这个操作。)如果没有,<B normal"><FONT face="Times New Roman">release</FONT></B>退出循环并像往常一样接着释放<B normal"><FONT face="Times New Roman">struct task_ struct</FONT></B>结构体。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">22966</FONT>:否则,<B normal"><FONT face="Times New Roman">release</FONT></B>等待进程的<B normal"><FONT face="Times New Roman">has_cpu</FONT></B>标志被清除。当它被清除后,<B normal"><FONT face="Times New Roman">release</FONT></B>再次进行尝试。这种貌似奇特的情况——某进程正被删除,然而它仍占有<FONT face="Times New Roman">CPU</FONT>——确实少见,不过并非不可能。进程可能已经在一个<FONT face="Times New Roman">CPU</FONT>上被杀死,而且这个<FONT face="Times New Roman">CPU</FONT>还没来得及清除<B normal"><FONT face="Times New Roman">has_cpu</FONT></B>标志,但是它的父进程已经正在从另一个<FONT face="Times New Roman">CPU</FONT>对它进行释放了。</P><H4 14pt 0cm 14.5pt"><FONT size=5>smp_local_timer_interrupt</FONT></H4><P 0cm 0cm 0pt">对于<FONT face="Times New Roman">UP</FONT>专有的<B normal"><FONT face="Times New Roman">update_process_times</FONT></B>函数(<FONT face="Times New Roman">27382</FONT>行)来说,这个函数就是它在<FONT face="Times New Roman">SMP</FONT>上的对应。该函数能够完成<B normal"><FONT face="Times New Roman">update_process_times</FONT></B>所完成的所有任务——更新进程和内核在<FONT face="Times New Roman">CPU</FONT>使用方面的统计值——以及其它的一些操作。与众不同的地方在于拥有这个特性的<FONT face="Times New Roman">SMP</FONT>版本并没有被添加到一个<FONT face="Times New Roman">UP</FONT>函数中去,而是采用了一个具有同样功能,但却完全分离的功能程序。在浏览了函数之后,我们就能够很容易的知道这是为什么了——它与<FONT face="Times New Roman">UP</FONT>版本差别甚大到以至于试图将二者融为一体都将是无意义的。<B normal"><FONT face="Times New Roman">smp_local_timer_interrupt</FONT></B>可从两个地方进行调用:</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo2; tab-stops: list 42.25pt">l           从<B normal"><FONT face="Times New Roman">smp_apic_timer_interrupt</FONT></B>(<FONT face="Times New Roman">5118</FONT>行)调用,它用于<FONT face="Times New Roman">SMP</FONT>的时钟中断。这是通过使用在第<FONT face="Times New Roman">1856</FONT>行定义的<B normal"><FONT face="Times New Roman">BUILD_SMP_TIMER_INTERRUPT</FONT></B>宏于第<FONT face="Times New Roman">919</FONT>行建立起来的。</P><P 0cm 0cm 0pt 42.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo3; tab-stops: list 42.25pt">l           从第<FONT face="Times New Roman">5776</FONT>行通常的<FONT face="Times New Roman">UP</FONT>时钟中断函数里进行调用。只有当在<FONT face="Times New Roman">UP</FONT>机器上运行<FONT face="Times New Roman">SMP</FONT>内核时此种调用方式才会发生。</P><P 12pt 0cm 3.2pt"><b>smp_local_timer_interrupt</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">5059</FONT>:<FONT face="Times New Roman"> <B normal">prof_counter</B></FONT>(<FONT face="Times New Roman">4610</FONT>行)用于跟踪到更新进程和内核统计值之前内核应该等待多长时间;如果该计数器还没有到达<FONT face="Times New Roman">0</FONT>,控制流程会有效地跳转到函数的末尾。正如代码中所证明的,<B normal"><FONT face="Times New Roman">prof_counter</FONT></B>项目从<FONT face="Times New Roman">1</FONT>开始递减计数,除非由根(<FONT face="Times New Roman">root</FONT>)来增加这个值,因此在缺省情况下每次时钟滴答都要完成此项工作。然后,<B normal"><FONT face="Times New Roman">prof_counter[cpu]</FONT></B>从<B normal"><FONT face="Times New Roman">prof_multiplier[cpu]</FONT></B>处被重新初始化。</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>语句块里完成所有工作将相当的缓慢,所以我们可能想到以牺牲一些精确度的代价将工作分批完成。因为乘法器是可调的,所以你可以指定你所需要的速度频率来放松对准确度的要求。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>然而,关于这段代码我总感到有些困惑:确定无疑的是,当<B normal"><FONT face="Times New Roman">prof_multiplier[cpu]</FONT></B>耗尽时,统计值应该被更新,就像<B normal"><FONT face="Times New Roman">prof_multiplier[cpu]</FONT></B>的计数流逝一样——既然它们已经如此。(除了<B normal"><FONT face="Times New Roman">prof_multiplier[cpu]</FONT></B>本身刚刚被改变时,不过这已经偏离了这里讨论的主题。)与此不同的是,这里代码表现出来的就好像只经过了一次滴答计数。或许其用意是为了以后能把记录下来的滴答数目和<B normal"><FONT face="Times New Roman">prof_multiplier[cpu]</FONT></B>在某个地方相乘,不过现在并没有这样实现。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">5068</FONT>:<FONT face="Times New Roman"> </FONT>当时钟中断被触发时假如系统正在用户模式运行,<B normal"><FONT face="Times New Roman">smp_local_timer_interrupt</FONT></B>会假定全部滴答都是在用户模式里流逝的;否则,它将假定全部滴答是在系统模式里流逝的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">5073</FONT>:<FONT face="Times New Roman"> </FONT>用<B normal"><FONT face="Times New Roman">irq_enter</FONT></B>(<FONT face="Times New Roman">1792</FONT>行)来夺取全局<FONT face="Times New Roman">IRQ</FONT>锁。这是我们要分批处理这项工作的另一个原因:并不需要在每次时钟滴答时都要得到全局<FONT face="Times New Roman">IRQ</FONT>锁,这有可能成为<FONT face="Times New Roman">CPU</FONT>之间争夺的一个重要根源,实际中函数是以较低的频度来争取该锁的。因此,函数不经常夺取这个锁,可是一旦它获得了锁,就不会再使其被锁。在此我们又一次以准确度的代价换来了这种效率上的提高。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">5074</FONT>:<FONT face="Times New Roman"> </FONT>不用为保存空闲进程的统计值而操心。这样做只会浪费<FONT face="Times New Roman">CPU</FONT>的周期。总之,内核会跟踪系统处于空闲的总共时间,对空闲进程的更多细节进行统计价值不大(比如我们知道它们总是在系统模式下执行的,所以就没有必要再明确计算它们的系统时间了)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">5075</FONT>:<FONT face="Times New Roman"> <B normal">update_process_times</B></FONT>和<B normal"><FONT face="Times New Roman">smp_local_timer_interrupt</FONT></B>在这一点上是一致的:它们都调用<B normal"><FONT face="Times New Roman">update_process_times</FONT></B>来完成对单进程<FONT face="Times New Roman">CPU</FONT>使用统计的更新工作。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">5077</FONT>:<FONT face="Times New Roman"> </FONT>减少进程的<B normal"><FONT face="Times New Roman">counter</FONT></B>(它的动态优先级),如果它被耗尽就重新调度该进程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">5082</FONT>:<FONT face="Times New Roman"> </FONT>更新内核的统计数字。如在<B normal"><FONT face="Times New Roman">update_process_times</FONT></B>中一样,用户时间既可以用内核的“最优时间”也可以用常规的用户时间来计算,这要取决于进程的优先级是否低于<B normal"><FONT face="Times New Roman">DEF_PRIORITY</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">5094</FONT>:<FONT face="Times New Roman"> </FONT>重新初始化<FONT face="Times New Roman">CPU</FONT>的<B normal"><FONT face="Times New Roman">prof_counter</FONT></B>并释放全局<FONT face="Times New Roman">IRQ</FONT>锁。该工作必须要以这种顺序完成,当然——若以相反的方式,则可能在<B normal"><FONT face="Times New Roman">prof_counter</FONT></B>被重新初始化之前发生又一次时钟中断。</P><H4 14pt 0cm 14.5pt"><FONT size=5>lock_kernel和unlock_kernel</FONT></H4><P 0cm 0cm 0pt">这两个函数也有专门适应于<FONT face="Times New Roman">x86</FONT>平台的版本;但是在这里只介绍通用版本。</P><P 12pt 0cm 3.2pt"><b>lock_kernel</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10174</FONT>:这个函数相当简单,它获得全局内核锁——在任何一对<B normal"><FONT face="Times New Roman">lock_kernel/unlock_kernel</FONT></B>函数里至多可以有一个<FONT face="Times New Roman">CPU</FONT>。显然这在<FONT face="Times New Roman">UP</FONT>机器上是一个空操作(<FONT face="Times New Roman">no-op</FONT>)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10176</FONT>:进程的<B normal"><FONT face="Times New Roman">lock_depth</FONT></B>成员初始为<FONT face="Times New Roman">–1</FONT>(参见<FONT face="Times New Roman">24040</FONT>行)。在它小于<FONT face="Times New Roman">0</FONT>时(若小于<FONT face="Times New Roman">0</FONT>则恒为<FONT face="Times New Roman">-1</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">lock_kernel</FONT></B>,然后在运行到<B normal"><FONT face="Times New Roman">unlock_kernel</FONT></B>之前可能又将调用另一个要使用<B normal"><FONT face="Times New Roman">lock_kernel</FONT></B>的函数。在这种情况中,进程将立刻被赋予内核锁——而这正是我们所期望的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>其结果是,一旦增加进程的<B normal"><FONT face="Times New Roman">lock_depth</FONT></B>就会使<B normal"><FONT face="Times New Roman">lock_depth</FONT></B>为<FONT face="Times New Roman">0</FONT>,那么进程以前就是没有锁的。所以,函数在此情形下获得<B normal"><FONT face="Times New Roman">kernel_flag</FONT></B>自旋锁(<FONT face="Times New Roman">3587</FONT>行)。</P><P 12pt 0cm 3.2pt"><b>unlock_kernel</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">10182</FONT>:同样的,如果丢弃内核锁就会使<B normal"><FONT face="Times New Roman">lock_depth</FONT></B>低于<FONT face="Times New Roman">0</FONT>值,进程退出它所进入的最后一对<B normal"><FONT face="Times New Roman">lock_kernel/unlock_kernel</FONT></B>函数。此时,<B normal"><FONT face="Times New Roman">kernel_flag</FONT></B>自旋锁一定要被解锁以便其它进程可以给内核加锁。通过测试结果的符号位(即使用“<FONT face="Times New Roman">&lt;0</FONT>”而不是“<FONT face="Times New Roman">== -1</FONT>”)可以使<FONT face="Times New Roman">gcc</FONT>生成更高效的代码,除此之外,这还可能有利于内核在面对不配对的<B normal"><FONT face="Times New Roman">lock_ kernel/unlock_kernel</FONT></B>时可正确执行(或者不能,这取决于具体情况)。</P><H4 14pt 0cm 14.5pt"><FONT size=5>softirq_trylock</FONT></H4><P 0cm 0cm 0pt">你可能能够回忆起在第<FONT face="Times New Roman">6</FONT>章的讨论中,<B normal"><FONT face="Times New Roman">softirq_trylock</FONT></B>的作用是保证对于其它程序段来说下半部分代码(<FONT face="Times New Roman">bottom half</FONT>)是原子操作——也就是说,保证在任何特定时段的整个系统范围之内至多只有一个下半部分代码在运行。对于<FONT face="Times New Roman">UP</FONT>来说这相当容易:内核只不过需要检查或者还要设置一下标志位就可以了。不过对于<FONT face="Times New Roman">SMP</FONT>来说自然没有这样简单。</P><P 12pt 0cm 3.2pt"><b>softirq_trylock</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12528</FONT>:测试并设置(<FONT face="Times New Roman">tests-and-sets</FONT>)<B normal"><FONT face="Times New Roman">global_bh_count</FONT></B>的第<FONT face="Times New Roman">0</FONT>位。尽管读者可能会从<B normal"><FONT face="Times New Roman">global _bh_count</FONT></B>的名字上得到另外一种看法,实际它总是<FONT face="Times New Roman">0</FONT>或者<FONT face="Times New Roman">1</FONT>的——这样的考虑是适当的,因为至多运行一个下半部分程序代码。不管怎样,如果<B normal"><FONT face="Times New Roman">global_bh_count</FONT></B>已经是<FONT face="Times New Roman">1</FONT>了,那么就已经有一个下半部分代码在运行着,因此控制流程就跳转到函数末尾。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12529</FONT>:如果还可得到<B normal"><FONT face="Times New Roman">global_bh_lock</FONT></B>,那么下半部分代码就能够在这个<FONT face="Times New Roman">CPU</FONT>上运行。这种情况与<FONT face="Times New Roman">UP</FONT>机器上使用的双锁系统非常类似。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">12533</FONT>:<B normal"><FONT face="Times New Roman">softirq_trylock</FONT></B>无法获取<B normal"><FONT face="Times New Roman">global_bh_lock</FONT></B>,因此它的工作失败了。</P><H4 14pt 0cm 14.5pt"><FONT size=5>cli和sti</FONT></H4><P 0cm 0cm 0pt">正如在第<FONT face="Times New Roman">6</FONT>章中解释过的,<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">UP</FONT>这简化为单个<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">SMP</FONT>情况下,这就很不够了,我们不仅需要禁止本地<FONT face="Times New Roman">CPU</FONT>还要暂时避免其它<FONT face="Times New Roman">CPU</FONT>处理<FONT face="Times New Roman">IRQ</FONT>。因此对于<FONT face="Times New Roman">SMP</FONT>,宏就变成了对<B normal"><FONT face="Times New Roman">__global_cli</FONT></B>和<B normal"><FONT face="Times New Roman">__global_sti</FONT></B>函数的调用。</P><P 12pt 0cm 3.2pt"><b>__global_cli</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1220</FONT>:<FONT face="Times New Roman"> </FONT>把<FONT face="Times New Roman">CPU</FONT>的<FONT face="Times New Roman">EFLAGS</FONT>寄存器复制到本地变量<B normal"><FONT face="Times New Roman">flags</FONT></B>里。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1221</FONT>:<FONT face="Times New Roman"> x86</FONT>系统里的中断使能标志在<FONT face="Times New Roman">EFLAGS</FONT>寄存器的第<FONT face="Times New Roman">9</FONT>位——在第<FONT face="Times New Roman">1205</FONT>行解释了<FONT face="Times New Roman">EFLAG_IF_SHIFT</FONT>的定义。它被用来检测是否已经禁止了中断,这样就不再需要去禁止它们了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1223</FONT>:<FONT face="Times New Roman"> </FONT>禁止这个<FONT face="Times New Roman">CPU</FONT>的中断。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1224</FONT>:<FONT face="Times New Roman"> </FONT>如果该<FONT face="Times New Roman">CPU</FONT>没有正在对<FONT face="Times New Roman">IRQ</FONT>进行处理,<B normal"><FONT face="Times New Roman">__global_cli</FONT></B>就调用<B normal"><FONT face="Times New Roman">get_irqlock</FONT></B>(<FONT face="Times New Roman">1184</FONT>行)来获得全局<FONT face="Times New Roman">IRQ</FONT>锁。如果<FONT face="Times New Roman">CPU</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">            </FONT>现在本<FONT face="Times New Roman">CPU</FONT>已经禁止了中断,而且它也拥有了全局<FONT face="Times New Roman">IRQ</FONT>锁,这样任务就完成了。</P><P 12pt 0cm 3.2pt"><b>__global_sti</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1233</FONT>:<FONT face="Times New Roman"> </FONT>如果<FONT face="Times New Roman">CPU</FONT>没有正在对<FONT face="Times New Roman">IRQ</FONT>进行处理,<B normal"><FONT face="Times New Roman">__global_sti</FONT></B>就在<B normal"><FONT face="Times New Roman">__global_cli</FONT></B>中通过<B normal"><FONT face="Times New Roman">release_irqlock</FONT></B>(<FONT face="Times New Roman">10752</FONT>行)调用来实现对全局<FONT face="Times New Roman">IRQ</FONT>锁的释放工作。如果<FONT face="Times New Roman">CPU</FONT>已经在对<FONT face="Times New Roman">IRQ</FONT>进行处理了,那么它已经拥有了该全局<FONT face="Times New Roman">IRQ</FONT>锁,正如在接下来的部分中将要解释的那样,这个锁将在其它地方被释放掉。<FONT face="Times New Roman">  </FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1235</FONT>:<FONT face="Times New Roman"> </FONT>再次允许在本<FONT face="Times New Roman">CPU</FONT>上进行中断。</P><H4 14pt 0cm 14.5pt"><FONT size=5>irq_enter和irq_exit</FONT></H4><P 0cm 0cm 0pt">第<FONT face="Times New Roman">6</FONT>章中顺便提及了这两个函数的<FONT face="Times New Roman">UP</FONT>版本。包含在一对<B normal"><FONT face="Times New Roman">irq_enter/irq_exit</FONT></B>之中的代码段都是原子操作,这不仅对于其它这样的代码区域是原子的,而且对于<B normal"><FONT face="Times New Roman">cli/sti</FONT></B>宏对来说也是如此。</P><P 12pt 0cm 3.2pt"><b>irq_enter</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1794</FONT>:<FONT face="Times New Roman"> </FONT>调用<B normal"><FONT face="Times New Roman">hardirq_enter</FONT></B>(<FONT face="Times New Roman">10761</FONT>行)自动为本<FONT face="Times New Roman">CPU</FONT>增加全局<FONT face="Times New Roman">IRQ</FONT>计数和本地<FONT face="Times New Roman">IRQ</FONT>计数。这个函数记录了<FONT face="Times New Roman">CPU</FONT>正在处理一个<FONT face="Times New Roman">IRQ</FONT>的情况。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">1795</FONT>:<FONT face="Times New Roman"> </FONT>执行循环直到这个<FONT face="Times New Roman">CPU</FONT>得到全局<FONT face="Times New Roman">IRQ</FONT>锁为止。这就是为什么我要在前面说明如果<FONT face="Times New Roman">CPU</FONT>正在处理<FONT face="Times New Roman">IRQ</FONT>,那么它就已经获得了全局<FONT face="Times New Roman">IRQ</FONT>锁的原因:到这个函数退出时,这两个特性都将被加强。对于内核代码来说,把这两个特性分离出去并没有太大的意义——它可以直接调用<B normal"><FONT face="Times New Roman">hardirq_enter</FONT></B>,而且也不用去争夺全局<FONT face="Times New Roman">IRQ</FONT>锁。函数只是没有这样作而已。</P><P 12pt 0cm 3.2pt"><b>irq_exit</b></P>1802: 这个函数转向<B normal">hardirq_enter</B>的相反函数<B normal">hardirq_exit</B>(10767行)。顺便要提及的是,对<B normal">irq_enter</B>和<B normal">irq_exit</B>来说其<B normal">irq</B>参数都被忽略了——至少在x86平台上如此。
作者: ilikenba    时间: 2005-3-4 22:31
<H1 17pt 0cm 16.5pt; TEXT-ALIGN: center" align=center>第<FONT face="Times New Roman">11</FONT>章<FONT face="Times New Roman"> </FONT>可调内核参数</H1>< 0cm 0cm 0pt">遵循<FONT face="Times New Roman">Unix</FONT>的<FONT face="Times New Roman">BSD 4.4</FONT>版本所倡导的风格,<FONT face="Times New Roman">Linux</FONT>提供<B normal"><FONT face="Times New Roman">sysctl</FONT></B>系统调用以便在系统运行过程中对它所拥有的某些特性进行检查和重新配置,它并不需要你编辑内核的源代码、重新编译,然后重启机器。这是对早期<FONT face="Times New Roman">Unix</FONT>版本的一个十分重要的改进,在早期版本里调整系统经常是令人头痛的琐碎事务。<FONT face="Times New Roman">Linux</FONT>把可以被检查和重新配置的系统特性有机地组织成了几个种类:常规内核参数、虚拟内存参数、网络参数,等等。</P>< 0cm 0cm 0pt">同样的特性也可以从一个不同的接口进行访问:<FONT face="Times New Roman">/proc</FONT>文件系统。(因为它真正的是系统的一个透视区(<FONT face="Times New Roman">window</FONT>)而不只是真实文件的一个容器,所以<FONT face="Times New Roman">/proc</FONT>是一个“伪的文件系统”,不过那是一个蹩脚的词汇,而且无论如何这个区别在此并不重要。)每种可调内核参数在<FONT face="Times New Roman">/proc/sys</FONT>下都表现为一个子目录,而每个单独的可调系统参数由某个子目录下的一个文件来代表。这些子目录可能又包含一级子目录,它们仍然含有更多的代表可调系统参数的文件和子目录,等等,但是这种嵌套级数从来都不会很深。<FONT face="Times New Roman"> </FONT></P>< 0cm 0cm 0pt"><FONT face="Times New Roman">/proc/sys</FONT>绕过了通常的<B normal"><FONT face="Times New Roman">sysctl</FONT></B>接口:一个可调内核参数的值可以简单的通过读取相应的文件来得到,通过写入该文件可以设置它的值。普通<FONT face="Times New Roman">Unix</FONT>文件系统的许可被应用于这些文件,以便对能够对它们进行读写的用户进行控制。大多数文件对所有用户是可读的但是只对<FONT face="Times New Roman">root</FONT>(根用户)可写,不过也有例外:比如,<FONT face="Times New Roman">/proc/sys/vm</FONT>下的文件(虚拟内存参数)只能被<FONT face="Times New Roman">root</FONT>来读写。如果不使用<FONT face="Times New Roman">/proc/sys</FONT>,检查和调整系统将需要编写程序并使用必须的参数调用<B normal"><FONT face="Times New Roman">sysctl</FONT></B>——虽然不是任务艰巨的劳动,可是也比不上使用<FONT face="Times New Roman">/proc/sys</FONT>来得方便。</P>< 12pt 0cm 3.2pt"><b>struct ctl_table</b></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">18274</FONT>:这是本章涉及的代码中所使用的一个主要数据结构。<FONT face="Times New Roman"><B normal">struct ctl_table</B>s</FONT>通常是由数组聚合起来的,每个这样的数组对应于<FONT face="Times New Roman">/proc/sys</FONT>下某处一个单独目录里的条目。(依我之见,称它为<B normal"><FONT face="Times New Roman">struct ctl_table_entry</FONT></B>可能更好。)<B normal"><FONT face="Times New Roman">root_table</FONT></B>(<FONT face="Times New Roman">30328</FONT>行)以及在它之后的数组通过<B normal"><FONT face="Times New Roman">struct ctl_table</FONT></B>的<B normal"><FONT face="Times New Roman">child</FONT></B>指针连结节点而形成了一个数组树(<B normal"><FONT face="Times New Roman">child</FONT></B>将在下边的列表中介绍)。注意所有这些都是<B normal"><FONT face="Times New Roman">ctl_table</FONT></B>的数组,它只是为<B normal"><FONT face="Times New Roman">struct ctl_table</FONT></B>进行<B normal"><FONT face="Times New Roman">typedef</FONT></B>;<FONT face="Times New Roman">18184</FONT>行完成这项工作。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>图<FONT face="Times New Roman">11.1</FONT>示意出了数组树间的关系。这幅图显示了由<B normal"><FONT face="Times New Roman">root_table</FONT></B>形成的树的一小部分以及它所指向的树。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            <B normal">struct ctl_table</B></FONT>具有如下成员:</P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo1; tab-stops: list 57.25pt">l         <B normal"><FONT face="Times New Roman">ctl_name</FONT></B>——是唯一标识表项的一个整数——在它所在的数组中是唯一的;这个数字在不同的数组中是可以重用的。数组的任何一项都已经存在这样一个唯一的数字了——就是它的数组下标——可是这个数字不能被用于该目的,因为我们想要维护不同内核发布版本中的二进制兼容性。与某内核版本里一个数组项相关联的可调内核参数可能不会出现在将来的内核版本里,所以假如参数是被它们的数组下标定义的,对数组里废弃项目位置的重新使用将使还没有在新内核版本下编译过的程序变得混乱。随着时间的推移,为了向后兼容而带上的只浪费空间但没有作用的元素项将会使数组变得乱七八糟。相反的,这种方法只会“浪费”整数,而整数资源却无疑是非常丰富的。(另一方面,查找也会更慢,因为一个简单的数组下标还不足以满足这种方法。)<B normal"><p></p></B></P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: 0cm">要注意的是这与有系统调用的情形相当类似:每个系统调用都与一个在系统调用表里唯一标识它位置的数字相关联。但是在这种情况里使用了一个不同的解决办法,可能由于速度在此并不重要的缘故。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><p><FONT face="Times New Roman"> </FONT></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>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt; TEXT-ALIGN: center" align=center>图<FONT face="Times New Roman">11.1 <B normal">ctl_table</B> </FONT>树的一部分</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><p><FONT face="Times New Roman"> </FONT></p></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            <B normal">struct ctl_table</B></FONT>具有如下成员:</P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo1; tab-stops: list 57.25pt">l         <B normal"><FONT face="Times New Roman">ctl_name</FONT></B>——是唯一标识表项的一个整数——在它所在的数组中是唯一的;这个数字在不同的数组中是可以重用的。数组的任何一项都已经存在这样一个唯一的数字了——就是它的数组下标——可是这个数字不能被用于该目的,因为我们想要维护不同内核发布版本中的二进制兼容性。与某内核版本里一个数组项相关联的可调内核参数可能不会出现在将来的内核版本里,所以假如参数是被它们的数组下标定义的,对数组里废弃项目位置的重新使用将使还没有在新内核版本下编译过的程序变得混乱。随着时间的推移,为了向后兼容而带上的只浪费空间但没有作用的元素项将会使数组变得乱七八糟。相反的,这种方法只会“浪费”整数,而整数资源却无疑是非常丰富的。(另一方面,查找也会更慢,因为一个简单的数组下标还不足以满足这种方法。)<B normal"><p></p></B></P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: 0cm">要注意的是这与有系统调用的情形相当类似:每个系统调用都与一个在系统调用表里唯一标识它位置的数字相关联。但是在这种情况里使用了一个不同的解决办法,可能由于速度在此并不重要的缘故。</P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo1; tab-stops: list 57.25pt">l         <B normal"><FONT face="Times New Roman">procname</FONT></B>——是用于<FONT face="Times New Roman">/proc/sys</FONT>下的相应项的一个可供我们阅读的简短文件名。<B normal"><p></p></B></P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo1; tab-stops: list 57.25pt">l         <B normal"><FONT face="Times New Roman">data</FONT></B>——一个指向与此表项关联的数据的指针。它通常指向一个<B normal"><FONT face="Times New Roman">int</FONT></B>或者一个<B normal"><FONT face="Times New Roman">char</FONT></B>(当然,指向<B normal"><FONT face="Times New Roman">char</FONT></B>的指针是字符串)。<B normal"><p></p></B></P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo1; tab-stops: list 57.25pt">l         <B normal"><FONT face="Times New Roman">maxlen</FONT></B>——可以读取或者写入<B normal"><FONT face="Times New Roman">data</FONT></B>的最大字节数。如果<B normal"><FONT face="Times New Roman">data</FONT></B>指向一个单精度型的<B normal"><FONT face="Times New Roman">int</FONT></B>,举例来说,<B normal"><FONT face="Times New Roman">maxlen</FONT></B>就应该是<B normal"><FONT face="Times New Roman">sizeof</FONT></B><B normal">(<FONT face="Times New Roman">int</FONT></B><B normal">)</B>。<B normal"><p></p></B></P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo1; tab-stops: list 57.25pt">l         <B normal"><FONT face="Times New Roman">mode</FONT></B>——<FONT face="Times New Roman">Unix</FONT>类型的文件许可位,它对应于这一项的<FONT face="Times New Roman">/proc</FONT>文件(或目录)。对此的解释需要少量文件系统的内容。就像其它<FONT face="Times New Roman">Unix</FONT>的实现一样,<FONT face="Times New Roman">Linux</FONT>使用三个三元组,其中每一位都记录一个文件许可(在<B normal"><FONT face="Times New Roman">ls -l</FONT></B>命令产生的列表里它们表现为<FONT face="Times New Roman">r</FONT>、<FONT face="Times New Roman">w</FONT>,和<FONT face="Times New Roman">x</FONT>的三组字母)——参见图<FONT face="Times New Roman">11.2</FONT>。它们占据<B normal"><FONT face="Times New Roman">mode</FONT></B>的低端<FONT face="Times New Roman">9</FONT>位。文件系统把文件的<B normal"><FONT face="Times New Roman">mode</FONT></B>里剩余的位留作它用,比如用来跟踪是否文件是常规文件(第<FONT face="Times New Roman">16</FONT>位,当它如此时)、目录(第<FONT face="Times New Roman">15</FONT>位)、<FONT face="Times New Roman">setuid</FONT>或<FONT face="Times New Roman">setgid</FONT>执行程序(第<FONT face="Times New Roman">12</FONT>和<FONT face="Times New Roman">11</FONT>位),等等。不过就本章的目的来说,那些其它位都不是我们所关心的内容。<B normal"><p></p></B></P>< 0cm 0cm 0pt"><wrapblock><v:shape><v:imagedata></v:imagedata><w:wrap type="topAndBottom"></w:wrap></v:shape></wrapblock><BR vglayout" clear=all><B normal"><p></p></B></P>< 0cm 0cm 0pt; TEXT-INDENT: 0cm"><B normal"><p><FONT face="Times New Roman"> </FONT></p></B></P>< 0cm 0cm 0pt; TEXT-INDENT: 0cm; TEXT-ALIGN: center" align=center>图<FONT face="Times New Roman">11.2 </FONT>文件的<B normal"><FONT face="Times New Roman">mode</FONT></B>位<B normal"><p></p></B></P>< 0cm 0cm 0pt; TEXT-INDENT: 0cm"><B normal"><p><FONT face="Times New Roman"> </FONT></p></B></P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: 0cm">这种方式的结果是,读者将经常见到八进制的常数<FONT face="Times New Roman">004</FONT>、<FONT face="Times New Roman">002</FONT>,和<FONT face="Times New Roman">001</FONT>与<B normal"><FONT face="Times New Roman">mode</FONT></B>一起使用——它们分别是在移位<B normal"><FONT face="Times New Roman">mode</FONT></B>后可能得到的适当的三位组中检测读(<FONT face="Times New Roman">r</FONT>)、写(<FONT face="Times New Roman">w</FONT>),和执行(<FONT face="Times New Roman">x</FONT>)位。这种移位和检查工作基本上是在<FONT face="Times New Roman">30544</FONT>行的<B normal"><FONT face="Times New Roman">test_perm</FONT></B>里完成的。</P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: 0cm">注意如果一个表项的<B normal"><FONT face="Times New Roman">maxlen</FONT></B>是<FONT face="Times New Roman">0</FONT>,那么不管它的<B normal"><FONT face="Times New Roman">mode</FONT></B>是什么,从最终效果上看它都是既不可读也不可写的。</P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo1; tab-stops: list 57.25pt">l         <B normal"><FONT face="Times New Roman">child</FONT></B>——如果这是一个目录类型的条目,那么它就是指向子表(<FONT face="Times New Roman">child table</FONT>)的一个指针。在这样的情况下,因为没有数据与此条目相关联,<B normal"><FONT face="Times New Roman">data</FONT></B>将是<B normal"><FONT face="Times New Roman">NULL</FONT></B>,而<B normal"><FONT face="Times New Roman">maxlen</FONT></B>则将是<FONT face="Times New Roman">0</FONT>。<B normal"><p></p></B></P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo1; tab-stops: list 57.25pt">l         <B normal"><FONT face="Times New Roman">proc_handler</FONT></B>——指针,指向对<B normal"><FONT face="Times New Roman">data</FONT></B>成员实际进行读取和写入操作的一个函数;它在通过<FONT face="Times New Roman">/proc</FONT>文件系统读写数据时被使用。以这种方法,任何类型的数据都可以通过<B normal"><FONT face="Times New Roman">data</FONT></B>来进行指向,而且<B normal"><FONT face="Times New Roman">proc_handler</FONT></B>函数会正确的处理对它的工作。<B normal"><FONT face="Times New Roman">roc_handler</FONT></B>通常指向<B normal"><FONT face="Times New Roman">proc_dostring</FONT></B>函数(<FONT face="Times New Roman">30820</FONT>行)或<B normal"><FONT face="Times New Roman">proc_dointvec</FONT></B>函数(<FONT face="Times New Roman">30881</FONT>行);这两个以及其它被普遍适用的函数将在本章后面被讨论。(当然,任何具有适当原型(<FONT face="Times New Roman">prototype</FONT>)的函数都可以使用。)对于目录类型的条目,<B normal"><FONT face="Times New Roman">proc_ handler</FONT></B>是<B normal"><FONT face="Times New Roman">NULL</FONT></B>。<B normal"><p></p></B></P>< 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo1; tab-stops: list 57.25pt">l         <B normal"><FONT face="Times New Roman">strategy</FONT></B>——指针,指向对<B normal"><FONT face="Times New Roman">data</FONT></B>成员实际进行读取和写入操作的另一个函数;它使用在通过<B normal"><FONT face="Times New Roman">sysctl</FONT></B>系统调用进行读写的时候。它通常是<B normal"><FONT face="Times New Roman">sysctl_string</FONT></B>(<FONT face="Times New Roman">31121</FONT>行),不过也可以是<B normal"><FONT face="Times New Roman">stringctl_intvec</FONT></B>(<FONT face="Times New Roman">31163</FONT>行);这两个函数在本章后面进行讨论。出于种种原因,大多数可调内核参数是通过<FONT face="Times New Roman">/proc</FONT>接口而不是<B normal"><FONT face="Times New Roman">sysctl</FONT></B>系统调用进行调整的,所以这个指针是<B normal"><FONT face="Times New Roman">NULL</FONT></B>会比非空更为常见。<B normal"><p></p></B></P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo1; tab-stops: list 57.25pt">l         <B normal"><FONT face="Times New Roman">de</FONT></B>——指向<B normal"><FONT face="Times New Roman">struct proc_dir_entry</FONT></B>的一个指针,它在<FONT face="Times New Roman">/proc</FONT>文件系统代码中使用以追踪文件系统里的文件或目录。如果它非空,<B normal"><FONT face="Times New Roman">struct ctl_table</FONT></B>就在<FONT face="Times New Roman">/proc</FONT>下的某处注册过了。<B normal"><p></p></B></P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l0 level1 lfo1; tab-stops: list 57.25pt">l         <B normal"><FONT face="Times New Roman">extra</FONT></B>和<B normal"><FONT face="Times New Roman">extra2</FONT></B>——指向在处理这个表元素时所需的任何补充数据。它们当前只用于指定某些整数参数的最小和最大值。</P><H2 13pt 0cm">/proc/sys 支持</H2><P 0cm 0cm 0pt">不是所有实现用于可调内核参数<FONT face="Times New Roman">/proc/sys</FONT>接口的代码都包括在这本书中——的确,大部分代码并没有包括在内,因为它们基本上属于<FONT face="Times New Roman">/proc</FONT>文件系统本身。尽管如此,只要你不关心<FONT face="Times New Roman">/proc</FONT>剩下的部分是如何工作的,就不难理解在<FONT face="Times New Roman">kernel/sysctl.c</FONT>里的代码,它们与<FONT face="Times New Roman">/proc</FONT>文件系统一起工作用来使<FONT face="Times New Roman">/proc</FONT>下的可调内核参数是可见的。</P><P 12pt 0cm 3.2pt"><b>register_proc_table</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30689</FONT>:<B normal"><FONT face="Times New Roman">register_proc_table</FONT></B>函数在<FONT face="Times New Roman">/proc/sys</FONT>下注册一个<B normal"><FONT face="Times New Roman">ctl_table</FONT></B>。注意这里并不要求所提供的表是根一级的节点(即<B normal"><FONT face="Times New Roman">ctl_table</FONT></B>没有双亲)——它本应该是,不过这取决于调用者是否能够进行保证。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>这个表被直接建立在<B normal"><FONT face="Times New Roman">root</FONT></B>之下,它应该对应于<FONT face="Times New Roman">/proc/sys</FONT>或者其下的一个子目录。(在初次调用时,<B normal"><FONT face="Times New Roman">root</FONT></B>总是指向<B normal"><FONT face="Times New Roman">proc_sys_root</FONT></B>的,但是在递归调用时它的值改变了。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30696</FONT>:开始在<B normal"><FONT face="Times New Roman">table</FONT></B>数组的所有元素中进行循环;在当前元素的<B normal"><FONT face="Times New Roman">clt_name</FONT></B>成员为<FONT face="Times New Roman">0</FONT>时循环结束,表示这是数组的末尾。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30698</FONT>:如果<B normal"><FONT face="Times New Roman">ctl_table</FONT></B><B normal">的<FONT face="Times New Roman">procname</FONT></B>元素是<B normal"><FONT face="Times New Roman">NULL</FONT></B>,那么即使同一数组的其它元素都可以为用户所见,它也不可以在<FONT face="Times New Roman">/proc/sys</FONT>下被用户所见。这样的数组元素会被跳过。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30701</FONT>:如果表项有<B normal"><FONT face="Times New Roman">procname</FONT></B>,表明它应该在<FONT face="Times New Roman">/proc/sys</FONT>下被注册,那么它一定还有一个<B normal"><FONT face="Times New Roman">proc _handler</FONT></B>(如果是一个叶子,或文件类型的节点)或者一个<B normal"><FONT face="Times New Roman">child</FONT></B>(如果是一个目录类型的节点)。如果它同时缺少这两者,那么系统将显示一条警告,而后循环继续进行。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30711</FONT>:若表项有一个<B normal"><FONT face="Times New Roman">proc_handler</FONT></B>,它被标记成常规文件。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30713</FONT>:否则,正如可从第<FONT face="Times New Roman">30701</FONT>行推断的那样,它一定有一个非空的<B normal"><FONT face="Times New Roman">child</FONT></B>,这样该条目将被看作是一个目录。注意并没有禁止<B normal"><FONT face="Times New Roman">ctl_table</FONT></B>同时拥有非空<B normal"><FONT face="Times New Roman">proc_handler</FONT></B>和<B normal"><FONT face="Times New Roman">child</FONT></B>这两者——在这种情形下,所有代码将对其一视同仁。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30715</FONT>:用给定的名字搜索一个存在的子目录,如果找到就让<B normal"><FONT face="Times New Roman">de</FONT></B>指向它,如果没找到则<B normal"><FONT face="Times New Roman">de</FONT></B>为<B normal"><FONT face="Times New Roman">NULL</FONT></B>。为什么对文件不做类似的检查比较难于理解——这可能是我没有领会的文件系统的某个细节问题,答案无疑就在那里。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30723</FONT>:如果指定的子目录已经不存在了,或者假如<B normal"><FONT face="Times New Roman">table</FONT></B>对应于一个文件而不是一个目录,新的文件或者目录就会通过调用<B normal"><FONT face="Times New Roman">create_proc_entry</FONT></B>(未包含在本书中)来创建。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30728</FONT>:如果表项是一个叶子节点,<B normal"><FONT face="Times New Roman">register_proc_table</FONT></B>会告诉文件系统代码使用由<B normal"><FONT face="Times New Roman">proc_sys_ inode_operations</FONT></B>(<FONT face="Times New Roman">30295</FONT>行)定义的文件操作。<B normal"><FONT face="Times New Roman">proc_sys_inode_operations</FONT></B>只定义了两个操作,读和写(不是搜索、内存映射,或者其它)。这些操作是用<B normal"><FONT face="Times New Roman">proc_readsys</FONT></B>和<B normal"><FONT face="Times New Roman">proc_writesys</FONT></B>函数(<FONT face="Times New Roman">30802</FONT>和<FONT face="Times New Roman">30808</FONT>行)来执行的,在本章的后面章节中将对它们进行介绍。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30731</FONT>:到了这一行,<B normal"><FONT face="Times New Roman">de</FONT></B>就不可能是<B normal"><FONT face="Times New Roman">NULL</FONT></B>了——它或者已经非空或者在第<FONT face="Times New Roman">30723</FONT>行被初始化了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30733</FONT>:如果增加的条目是目录类型,<B normal"><FONT face="Times New Roman">register_proc_table</FONT></B>会被递归调用来增加这一项的所有子孙。这是内核里不多见的一次递归调用。</P><P 12pt 0cm 3.2pt"><b>unregister_proc_table</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30739</FONT>:<B normal"><FONT face="Times New Roman">unregister_proc_table</FONT></B>函数删除<B normal"><FONT face="Times New Roman">ctl_table</FONT></B>数组树和<FONT face="Times New Roman">/proc</FONT>文件系统之间的关联。<B normal"><FONT face="Times New Roman">ctl_  table</FONT></B>里的条目以及它们下面所有的“子目录”里的条目也将会从<FONT face="Times New Roman">/proc/sys</FONT>消失。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30743</FONT>:同第<FONT face="Times New Roman">30396</FONT>行一样,这一行开始在给定的表项数组上进行循环。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30744</FONT>:与<FONT face="Times New Roman">/proc/sys</FONT>下任意条目都不关联的表项具有一个为<B normal"><FONT face="Times New Roman">NULL</FONT></B>的<B normal"><FONT face="Times New Roman">de</FONT></B>成员;显然这些表项可被忽略。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30748</FONT>:如果<FONT face="Times New Roman">/proc</FONT>文件系统认为这是一个目录,但表项是一个叶子(非目录),这两个结构就是不一致的。<B normal"><FONT face="Times New Roman">unregister_proc_table</FONT></B>就会显示一条警告并继续循环,而不会移去这一项。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30752</FONT>:目录被逐层的进行释放——内核中另一次并不多见的递归过程。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30756</FONT>:在递归调用结束之后,<B normal"><FONT face="Times New Roman">unregister_proc_table</FONT></B>检查是否所有子目录和文件都被逐层删除了——如果不是,当前元素就不能被安全的移去,接着要继续循环。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30762</FONT>:这里就是为什么子目录(以及其中的文件)可能还没有被移去的原因:它们可能当前还正被使用着。如果这个元素正在被使用,循环将继续,这样该元素就不会被移走。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30765</FONT>:节点通过<B normal"><FONT face="Times New Roman">proc_unregister</FONT></B>(本书不进行介绍)从文件系统里被删除,接着用于追踪该节点而分配的内存被释放。</P><P 12pt 0cm 3.2pt"><b>do_rw_proc</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30771</FONT>:<B normal"><FONT face="Times New Roman">do_rw_proc</FONT></B>实现<B normal"><FONT face="Times New Roman">proc_readsys</FONT></B>(<FONT face="Times New Roman">30802</FONT>行)和<B normal"><FONT face="Times New Roman">proc_writesys</FONT></B>(<FONT face="Times New Roman">30806</FONT>行)函数的核心部分,这两个函数被<FONT face="Times New Roman">/proc</FONT>文件系统代码用于对<B normal"><FONT face="Times New Roman">ctl_table</FONT></B>执行读取和写入操作。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30782</FONT>:确保一个表与<FONT face="Times New Roman">/proc/sys</FONT>下的这一条目相关联。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30785</FONT>:注意这一行的第一个测试与第<FONT face="Times New Roman">30782</FONT>行的第二个测试是相重复的,这是因为<B normal"><FONT face="Times New Roman">table</FONT></B>是从<B normal"><FONT face="Times New Roman">de-&gt;data</FONT></B>初始而来。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30788</FONT>:确保调用进程有适当的读或写权限。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30795</FONT>:调用该表项的<B normal"><FONT face="Times New Roman">proc_handler</FONT></B>来完成真正的读操作或写操作。(要注意第<FONT face="Times New Roman">30785</FONT>行证实了<B normal"><FONT face="Times New Roman">proc_ handler</FONT></B>成员是非空的。)如前所述,<B normal"><FONT face="Times New Roman">proc_handler</FONT></B>成员通常是<B normal"><FONT face="Times New Roman">proc_dostring</FONT></B>或<B normal"><FONT face="Times New Roman">proc_ dointvec</FONT></B>(<FONT face="Times New Roman">30820</FONT>行和<FONT face="Times New Roman">30792</FONT>行),在随后的几段中我们将对它们进行讨论。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30799</FONT>:<B normal"><FONT face="Times New Roman">do_rw_proc</FONT></B>返回实际读取或写入的字节数。注意到本地变量<B normal"><FONT face="Times New Roman">res</FONT></B>完全是多余的;它可以被参数<B normal"><FONT face="Times New Roman">count</FONT></B>所替代。</P><P 12pt 0cm 3.2pt"><b>proc_dostring</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30820</FONT>:<B normal"><FONT face="Times New Roman">proc_dostring</FONT></B>是供文件系统代码调用以对<FONT face="Times New Roman">C</FONT>语言字符串型的内核参数进行读取或写入操作的函数。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>注意<B normal"><FONT face="Times New Roman">write</FONT></B>标志表示调用者正在写表元素,不过这主要是涉及从输入缓冲区里进行读取——因此,用来写入的代码是受读控制的。类似的,如果<B normal"><FONT face="Times New Roman">write</FONT></B>未被设置,调用者正从该表项读取,这里主要涉及的是写入给定的缓冲区。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>这个函数在第<FONT face="Times New Roman">31085</FONT>行还实现了一个存根程序(<FONT face="Times New Roman">stub</FONT>);这个存根程序在<FONT face="Times New Roman">/proc</FONT>文件系统被编译出内核时使用。大多数其它函数中的类似存根程序将在这个存根程序之后被介绍。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30835</FONT>:从输入缓冲区内读取字符直到一个表示结束的<FONT face="Times New Roman">ASCII NUL</FONT>(<FONT face="Times New Roman">0</FONT>)字节或者发现新的一行,再或者到达了被允许从该输入缓冲区内读出数据的最大值(被<B normal"><FONT face="Times New Roman">lenp</FONT></B>所指定)为止。(为了不引起混淆,牢记<B normal"><FONT face="Times New Roman">NULL</FONT></B>是一个<FONT face="Times New Roman">C</FONT>指针常量,而<FONT face="Times New Roman">NUL</FONT>——只有一个<FONT face="Times New Roman">L</FONT>——是<FONT face="Times New Roman">ASCII</FONT>用于字符数字<FONT face="Times New Roman">0</FONT>的术语。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30842</FONT>:如果从缓冲区读出的字符数超出了可在表项里存储的限度,该数目会被降低。在循环之前就限制最大输入长度(<B normal"><FONT face="Times New Roman">lenp</FONT></B>)可能会更高效,因为不管怎样从<B normal"><FONT face="Times New Roman">buffer</FONT></B>里读取大于<B normal"><FONT face="Times New Roman">table-&gt;maxlen</FONT></B>字节的数据是无意义的。实际上,循环可能读出,假设是<FONT face="Times New Roman">1024</FONT>字节,然后降低计数到<FONT face="Times New Roman">64</FONT>,因为表项里只能存储这么多。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30844</FONT>:该字符串从输入缓冲区里被读出,然后以<FONT face="Times New Roman">NUL</FONT>结束。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30847</FONT>:内核为每个进程所拥有的每个文件维护一个“当前位置”变量;这就是<B normal"><FONT face="Times New Roman">struct file</FONT></B>的<B normal"><FONT face="Times New Roman">f_pos</FONT></B>成员。它是<B normal"><FONT face="Times New Roman">tell</FONT></B>系统调用返回的值并由<B normal"><FONT face="Times New Roman">seek</FONT></B>系统调用进行设置。因此,文件的当前位置是由写入的字节数所推进的。</P><P 12pt 0cm 3.2pt"><b>proc_doutsstring</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30871</FONT>:在获得<B normal"><FONT face="Times New Roman">uts_sem</FONT></B>信号量后(<FONT face="Times New Roman">29975</FONT>行),<B normal"><FONT face="Times New Roman">proc_doutsstring</FONT></B>仅是调用<B normal"><FONT face="Times New Roman">proc_dostring</FONT></B>。这个函数被<B normal"><FONT face="Times New Roman">kern_table</FONT></B>(<FONT face="Times New Roman">30341</FONT>行)里的一些条目用来设置<B normal"><FONT face="Times New Roman">system_utsname</FONT></B>结构体的不同部分(<FONT face="Times New Roman">20094</FONT>行)。</P><P 12pt 0cm 3.2pt"><b>do_proc_dointvec</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30881</FONT>:<B normal"><FONT face="Times New Roman">proc_dointvec</FONT></B>(<FONT face="Times New Roman">30972</FONT>行)把它的工作委托给了该函数。<B normal"><FONT face="Times New Roman">do_proc_dointvec</FONT></B>读或写一个被<B normal"><FONT face="Times New Roman">table</FONT></B>的<B normal"><FONT face="Times New Roman">data</FONT></B>成员所指向的<B normal"><FONT face="Times New Roman">int</FONT></B>类型数组。要读写的<B normal"><FONT face="Times New Roman">int</FONT></B>类型数目通过<B normal"><FONT face="Times New Roman">lenp</FONT></B>传递;它通常是<FONT face="Times New Roman">1</FONT>,所以本函数通常只被用于读写单独一个<B normal"><FONT face="Times New Roman">int</FONT></B>。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>用于<B normal"><FONT face="Times New Roman">int</FONT></B>的值是被<B normal"><FONT face="Times New Roman">buffer</FONT></B>指定的。这些<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">ASCII</FONT>文本给出,而这正是用户写入相关<FONT face="Times New Roman">/proc</FONT>文件的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30898</FONT>:在所有要读写的<B normal"><FONT face="Times New Roman">int</FONT></B>中循环。<B normal"><FONT face="Times New Roman">left</FONT></B>追踪调用者想要读写<B normal"><FONT face="Times New Roman">int</FONT></B>的剩余数目,而<B normal"><FONT face="Times New Roman">vleft</FONT></B>追踪<B normal"><FONT face="Times New Roman">table-&gt;data</FONT></B>里剩余的有效元素数目。在这二者中任何一个到达<FONT face="Times New Roman">0</FONT>,或它从半途退出时,该循环结束。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>注意如果从循环中去掉第<FONT face="Times New Roman">30899</FONT>行的<B normal"><FONT face="Times New Roman">if</FONT></B>语句,可以使整个循环的效率稍微提高一些,尽管这样做的结果较难维护。取代的代码如下:</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">              </FONT></P><P 0cm 0cm 0pt 14.75pt; TEXT-INDENT: 21.25pt"><FONT face="Times New Roman">P556—1<p></p></FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            <p></p></FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm">这种方式使得并不在循环内改变的<B normal"><FONT face="Times New Roman">write</FONT></B>的值将只需被检查一次,而不必在每次循环重复检查。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30900</FONT>:向前搜索一个不是空格的字符,它是输入(缓冲区)里下一个数字的开头。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30913</FONT>:从用户空间把一大块数据复制到本地缓冲区<B normal"><FONT face="Times New Roman">buf</FONT></B>,然后以<FONT face="Times New Roman">NUL</FONT>结束<B normal"><FONT face="Times New Roman">buf</FONT></B>。现在<B normal"><FONT face="Times New Roman">buf</FONT></B>里包含了所有输入缓冲里剩余的<FONT face="Times New Roman">ASCII</FONT>文本——或者是它所能容纳的那些文本。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>这种方法看起来不很有效率,原因在于它可能读取的超出了它所需要的。然而,因为<B normal"><FONT face="Times New Roman">buf</FONT></B>的容量仅为<FONT face="Times New Roman">20</FONT>(<B normal"><FONT face="Times New Roman">TMPBUFLEN</FONT></B>,<FONT face="Times New Roman">30885</FONT>行),它就不可能读取比它所需多出许多的数据。这里的思想可能是读入稍多一些数据要比检查每个字节以确定是否应该停止读操作所付出的代价要少些。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>计划使<B normal"><FONT face="Times New Roman">buf</FONT></B>足够大来包括任何<FONT face="Times New Roman">64</FONT>位整数的<FONT face="Times New Roman">ASCII</FONT>表示,以便这个函数不仅可以支持<FONT face="Times New Roman">32</FONT>位平台还可以支持<FONT face="Times New Roman">64</FONT>位平台。的确,它只能满足最大的正<FONT face="Times New Roman">64</FONT>位整数,它有<FONT face="Times New Roman">19</FONT>个数位(使终结的<FONT face="Times New Roman">NUL</FONT>字节是第<FONT face="Times New Roman">20</FONT>个字节)。可是要记住这些是有符号的整数,所以最小的<FONT face="Times New Roman">64</FONT>位有符号整数,即<FONT face="Times New Roman">-9,223,372,032,854,775,808</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">30919</FONT>:处理打头的减号(<FONT face="Times New Roman">-</FONT>),如果发现一个减号就跳过它并设置一个标志。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30923</FONT>:确保从<B normal"><FONT face="Times New Roman">buffer</FONT></B>读取的文本(可能是打头的减号之后的部分)至少是以一个数字开始的,这样它才能顺利的转换为一个整数。若没有这次检查,就不可能分辨出第<FONT face="Times New Roman">30925</FONT>行调用<B normal"><FONT face="Times New Roman">simple_strtoul</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">30925</FONT>:把文本转换为一个整数,用<B normal"><FONT face="Times New Roman">conv</FONT></B>参数换算结果。这个换算步骤对于<B normal"><FONT face="Times New Roman">proc_dointvec _jiffies</FONT></B>这样的函数(<FONT face="Times New Roman">31077</FONT>行)比较有用,它用乘以常数<B normal"><FONT face="Times New Roman">HZ</FONT></B>的简单手段把它的输入从秒转换为一段时间值(<FONT face="Times New Roman">jiffies</FONT>)。然而一般情况里,这个比例因子是<FONT face="Times New Roman">1</FONT>——即没有换算。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30927</FONT>:如果还要从缓冲区读取更多的文本,而且下一个要读的字符不是分割参数的(<FONT face="Times New Roman">argument-separating</FONT>)空格,那么整个参数(<FONT face="Times New Roman">argument</FONT>)就无法装进<B normal"><FONT face="Times New Roman">buf</FONT></B>。这样的输入是无效的,所以循环提早结束。(一种可以导致函数处于这种状态的方式就是前边所描述的,输入表示的是最小的有符号<FONT face="Times New Roman">64</FONT>位整数。)不过,没有错误代码会被返回,因此调用者可能会错误地认为一切正常。当然这也不完全正确:一个错误代码将在第<FONT face="Times New Roman">31070</FONT>行被返回,不过这仅当无效参数是在第一次循环重复中被检测到的时候;如果它在后续的循环里被检测到,错误就不会被注意到。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30929</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">30936</FONT>:调用者从表项里读取值——由于无需对<FONT face="Times New Roman">ASCII</FONT>文本进行语法分析,这就是一种更为简单的情形。输出是由<FONT face="Times New Roman">tab</FONT>(制表符)分隔的,所以在除了第一次之外的任何一次循环里都把一个<FONT face="Times New Roman">tab</FONT>写入临时缓冲区里(在最后一个参数之后也不用写,只需在参数之间即可)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30938</FONT>:接着,当前的整数被<B normal"><FONT face="Times New Roman">conv</FONT></B>因子按比例缩减并打印到临时缓冲区里。这段代码同样会受读者前边已经见到的问题的损害:临时缓冲区<B normal"><FONT face="Times New Roman">buf</FONT></B>的大小可能不足以容纳打印到它里边的全部整数值。在这种情况下,实际问题还会因缓冲区的第一个位置可以是一个<FONT face="Times New Roman">tab</FONT>制表符而被恶化。这会使<B normal"><FONT face="Times New Roman">buf</FONT></B>的可用部分减少一个字符,进一步还会降低可被正确处理的输入范围。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>在这里过大或过小的整数所造成的结果要比在写入情形里严重的多。在那种情形中,代码只要抛弃一些本应接受的输入即可。而在这儿,<B normal"><FONT face="Times New Roman">sprintf</FONT></B>会越过<B normal"><FONT face="Times New Roman">buf</FONT></B>的末尾继续写下去。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>然而令人惊讶的是,这正是实际工作中可能发生的。在一次典型的执行过程中将有可能发生如下执行过程:从总体上来说,超过<B normal"><FONT face="Times New Roman">buf</FONT></B>的末尾之后还要写入两个额外的字节(一个是因为它可以写入比预期更长的数字,另一个是<FONT face="Times New Roman">tab</FONT>制表符)。在栈里<B normal"><FONT face="Times New Roman">p</FONT></B>通常是紧跟在<B normal"><FONT face="Times New Roman">buf</FONT></B>之后的,所以超出<B normal"><FONT face="Times New Roman">buf</FONT></B>末尾写入的部分将会覆盖<B normal"><FONT face="Times New Roman">p</FONT></B>。可是由于<B normal"><FONT face="Times New Roman">p</FONT></B>没有先被重新初始化时它是不会再被使用的,因此暂时覆盖它的值并没有危害。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>这是一个看似有趣的事故,但是仅仅通过使<B normal"><FONT face="Times New Roman">buf</FONT></B>稍微大一些就能够成为一个更好的解决方式,这样便于代码为正确的而不是错误的前提(<FONT face="Times New Roman">reason</FONT>)而工作。依照原样,对于<FONT face="Times New Roman">gcc</FONT>的代码生成器进行完全合法的很小的修改就能够揭示出潜在的缺陷。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30939</FONT>:把当前<B normal"><FONT face="Times New Roman">int</FONT></B>的文本型表示复制进输出缓冲区里——或者和它所能容纳的相等的文本——接着更新本地变量使其转移到表项的下一个数组元素。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30949</FONT>:如果调用者刚才在读取,输出就被新的一行结束。<B normal"><FONT face="Times New Roman">if</FONT></B>条件语句也保证循环不会在其第一遍执行而且还有空间来写入新行时就结束。注意输出缓冲区不是用<FONT face="Times New Roman">ASCII NUL</FONT>字节(读者可能会这样猜测)来结束的,因为它无需如此:调用者能够利用<B normal"><FONT face="Times New Roman">lenp</FONT></B>被写入新值来减少返回字符串的长度。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30954</FONT>:如果调用者正向表项里写入数值,则略过从输入缓冲区读取的最后参数之后所有的空格。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30967</FONT>:更新文件的当前位置和<B normal"><FONT face="Times New Roman">lenp</FONT></B>,然后返回<FONT face="Times New Roman">0</FONT>表示成功。</P><P 12pt 0cm 3.2pt"><b>proc_dointvec_minmax</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30978</FONT>:<B normal"><FONT face="Times New Roman">proc_dointvec_minmax</FONT></B>函数类似于<B normal"><FONT face="Times New Roman">do_proc_dointvec</FONT></B>,区别是这个函数还把表项的<B normal"><FONT face="Times New Roman">extra1</FONT></B>和<B normal"><FONT face="Times New Roman">extra2</FONT></B>成员作为可以写入该表项的限制值数组来处理。<B normal"><FONT face="Times New Roman">extra1</FONT></B>里的值是最小限度,而<B normal"><FONT face="Times New Roman">extra2</FONT></B>里的值则是最大限度。另一点区别是<B normal"><FONT face="Times New Roman">proc_dointvec_minmax</FONT></B>不使用<B normal"><FONT face="Times New Roman">conv</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">31033</FONT>:最大的区别在于:当写入时,超过被<B normal"><FONT face="Times New Roman">min</FONT></B>和<B normal"><FONT face="Times New Roman">max</FONT></B>(在<B normal"><FONT face="Times New Roman">extra1</FONT></B>和<B normal"><FONT face="Times New Roman">extra2</FONT></B>数组上循环得到)所定义的范围之外的值将悄无声息的被略过。这段代码的目的明显是要使<B normal"><FONT face="Times New Roman">min</FONT></B>和<B normal"><FONT face="Times New Roman">max</FONT></B>伴随着<B normal"><FONT face="Times New Roman">val</FONT></B>一起继续。当一个数值从输入缓冲里被读取时,它应该被下一个<B normal"><FONT face="Times New Roman">min</FONT></B>和<B normal"><FONT face="Times New Roman">max</FONT></B>来检查,然后才能决定被接受或被忽略。可是,这并非是实际所发生的那样。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>假设从<B normal"><FONT face="Times New Roman">buffer</FONT></B>而来的当前值已经进行了语法分析并存入里<B normal"><FONT face="Times New Roman">val</FONT></B>,它小于最小值;为了更具体一些,再假设已是第三遍循环,以便<B normal"><FONT face="Times New Roman">min</FONT></B>和<B normal"><FONT face="Times New Roman">max</FONT></B>分别指向对应数组中的第三个元素。然后<B normal"><FONT face="Times New Roman">val</FONT></B>将用<B normal"><FONT face="Times New Roman">min</FONT></B>来检查并发现它超出了范围(太小),接着循环还要继续。可是<B normal"><FONT face="Times New Roman">min</FONT></B>会作为检查的副作用被更新,而<B normal"><FONT face="Times New Roman">max</FONT></B>则没有。现在,<B normal"><FONT face="Times New Roman">min</FONT></B>指向它对应数组的第四个元素了,可是<B normal"><FONT face="Times New Roman">max</FONT></B>仍然指向它的数组的第三个元素。这两者不再同步,而且它们还将保存这种状态,这样在下一个从<B normal"><FONT face="Times New Roman">buffer</FONT></B>里读取的值被检验时采用的就是错误的界限。下列代码是最简单的一种修补程序:</P><P 0cm 0cm 0pt"><FONT face="Times New Roman">              P558—1<p></p></FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>正如读者将要在本章后边看到的,现在的<FONT face="Times New Roman">Linux</FONT>源代码永远不会暴露出这个缺陷。(未来发行的版本情况将有所不同,尽管还未曾明确写出。)</P><H2 13pt 0cm">sysctl系统调用</H2><P 0cm 0cm 0pt">用于可调内核参数的另一个接口是<B normal"><FONT face="Times New Roman">sysctl</FONT></B>系统调用,以及相关函数。我不很喜欢这个接口。为什么不呢?对于大部分实际工作目的来说,使用<B normal"><FONT face="Times New Roman">sysctl</FONT></B>——不过这种方法比修改源代码的旧方法来调整内核能够获得更大的性能提高——只会比访问<FONT face="Times New Roman">/proc</FONT>文件更为笨拙。通过<B normal"><FONT face="Times New Roman">sysctl</FONT></B>来进行读写需要<FONT face="Times New Roman">C</FONT>程序(或相似的东西),而<FONT face="Times New Roman">/proc</FONT>却很容易通过外壳(<FONT face="Times New Roman">shell</FONT>)命令(或等价的通过命令解释程序脚本)来进行访问。</P><P 0cm 0cm 0pt">另一方面,如果你正在<FONT face="Times New Roman">C</FONT>环境下工作,调用<B normal"><FONT face="Times New Roman">sysctl</FONT></B>就比打开文件、读取并<FONT face="Times New Roman">/</FONT>或写入,以及再关闭它要方便的多,所以<B normal"><FONT face="Times New Roman">sysctl</FONT></B>在今后也有它的用武之地。与此同时,还是让我们来看一看它的实现吧。</P><P 12pt 0cm 3.2pt"><b>do_sysctl</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30471</FONT>:<B normal"><FONT face="Times New Roman">do_sysctl</FONT></B>实现<B normal"><FONT face="Times New Roman">sys_sysctl</FONT></B>(<FONT face="Times New Roman">30504</FONT>行),即<B normal"><FONT face="Times New Roman">sysctl</FONT></B>系统调用的主要内容。注意<B normal"><FONT face="Times New Roman">sys_sysctl</FONT></B>还在第<FONT face="Times New Roman">31275</FONT>行出现过——那个版本只是在<B normal"><FONT face="Times New Roman">sysctl</FONT></B>系统调用被编译出内核时所使用的一个简单的存根程序(<FONT face="Times New Roman">stub</FONT>)函数。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>如果<B normal"><FONT face="Times New Roman">oldval</FONT></B>非空就用<B normal"><FONT face="Times New Roman">oldval</FONT></B>返回内核参数原有的值,而它的新值在<B normal"><FONT face="Times New Roman">newval</FONT></B>非空时从<B normal"><FONT face="Times New Roman">newval</FONT></B>来进行设置。<B normal"><FONT face="Times New Roman">oldlenp</FONT></B>和<B normal"><FONT face="Times New Roman">newlen</FONT></B>分别标识出有多少字节应被写入<B normal"><FONT face="Times New Roman">oldval</FONT></B>和从<B normal"><FONT face="Times New Roman">newval</FONT></B>读出,这是在相应的指针不是<B normal"><FONT face="Times New Roman">NULL</FONT></B>的时候;当指针为<B normal"><FONT face="Times New Roman">NULL</FONT></B>的时候,它们将被忽略。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>要注意这里的不对称性:函数对旧值的长度使用指针,而对新的长度不使用指针。这是因为旧的长度既是输入参数也是输出参数;它的输入值是可以通过<B normal"><FONT face="Times New Roman">oldval</FONT></B>返回的最大字节数,而它的输出值是实际返回的字节数。与之相反,新的长度只是一个输入参数。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30482</FONT>:如果调用者需要旧的内核参数值,从<B normal"><FONT face="Times New Roman">oldlenp</FONT></B>来对<B normal"><FONT face="Times New Roman">old_len</FONT></B>进行设置。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30490</FONT>:开始遍历表树的循环列表。(参见本章随后对<B normal"><FONT face="Times New Roman">register_sysctl_table</FONT></B>的讨论。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30493</FONT>:使用<B normal"><FONT face="Times New Roman">parse_table</FONT></B>(<FONT face="Times New Roman">30560</FONT>行,在下一段里讨论)来定位可调内核参数,然后读和<FONT face="Times New Roman">/</FONT>或写它的值。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30495</FONT>:如果<B normal"><FONT face="Times New Roman">parse_table</FONT></B>分配了所有环境信息,它就被释放。很难准确地说出这个环境信息表示着什么。它不被本书所讨论的任何代码使用——据我所知,它目前甚至没有被内核里的任何代码所使用。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30497</FONT>:<B normal"><FONT face="Times New Roman">ENOTDIR</FONT></B>错误表示没有在这一棵表树中找到指定的内核参数——它可能在另一棵还没有查找过的表树中。否则,<B normal"><FONT face="Times New Roman">error</FONT></B>将为某个其它的错误代码,或者是代表成功的<FONT face="Times New Roman">0</FONT>;无论如何,函数应该返回了。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30499</FONT>:用<B normal"><FONT face="Times New Roman">DLIST_NEXT</FONT></B>宏(本书对此不做介绍)来增加循环控制变量的值(<FONT face="Times New Roman">loop iterator</FONT>)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30501</FONT>:返回<B normal"><FONT face="Times New Roman">ENOTDIR</FONT></B>错误,报告出指定的内核参数在任何一个表里都未找到。</P><P 12pt 0cm 3.2pt"><b>parse_table</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30560</FONT>:<B normal"><FONT face="Times New Roman">parse_table</FONT></B>用于在表树里查找一个条目,其方法类同于在一个目录树里解析出一个完全合格的文件名的方法。其思想如下:沿着一个<B normal"><FONT face="Times New Roman">int</FONT></B>数组(数组<B normal"><FONT face="Times New Roman">name</FONT></B>)进行查找,并在一个<B normal"><FONT face="Times New Roman">ctl_table</FONT></B>数组里搜索每个<B normal"><FONT face="Times New Roman">int</FONT></B>。当找到一个匹配项时,它对应的子孙表就被递归查阅(如果匹配项是目录类型的条目),或者该条目被读和<FONT face="Times New Roman">/</FONT>或写(如果它是文件类型的条目)。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30566</FONT>:多少有些令人惊讶的是,这一行就开始了对整型数组<B normal"><FONT face="Times New Roman">name</FONT></B>内所有元素的循环。习惯上的方法原本是把从这一行到第<FONT face="Times New Roman">30597</FONT>行所有代码用一个<B normal"><FONT face="Times New Roman">for</FONT></B>循环包括起来,它的开始是这样的:</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            for ( ; nlen ; ++name , --nlen , table = table -&gt; child )</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>(这个循环还需要删除第<FONT face="Times New Roman">30567</FONT>和<FONT face="Times New Roman">30568</FONT>行代码,并用一个语句来替代从<FONT face="Times New Roman">30587</FONT>直到<FONT face="Times New Roman">30590</FONT>行的代码。)推测起来,可能是实际使用的版本可以生成更好的目标代码吧。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30570</FONT>:开始循环所有的表项,查找与当前<B normal"><FONT face="Times New Roman">name</FONT></B>匹配的一项;当表已被遍历结束(<B normal"><FONT face="Times New Roman">table-&gt;ctl_  name</FONT></B>为<FONT face="Times New Roman">0</FONT>了)或者指定的表项已被找到并处理时本循环结束。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30572</FONT>:把<B normal"><FONT face="Times New Roman">name</FONT></B>数组的当前项读入<B normal"><FONT face="Times New Roman">n</FONT></B>里,以便它可以与当前表项的<B normal"><FONT face="Times New Roman">ctl_name</FONT></B>进行检查。因为<B normal"><FONT face="Times New Roman">name</FONT></B>在内层循环中没有变化,这个读取操作可以放在循环外边(也就是移至<FONT face="Times New Roman">30569</FONT>行)以提高一点速度。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30574</FONT>:核查是否当前<B normal"><FONT face="Times New Roman">ctl_table</FONT></B>的名字与被找到的名字相匹配,或是否它有特殊的“通配符(<FONT face="Times New Roman">wildcard</FONT>)”值,即<B normal"><FONT face="Times New Roman">CTL_ANY</FONT></B>(<FONT face="Times New Roman">17761</FONT>行)。后者的使用目的还不清楚,因为现在并没有内核源代码的任何一处使用过<B normal"><FONT face="Times New Roman">CTL_ANY</FONT></B>。它可能用于将来的方案中——我也不认为它是过去版本的一个遗留物,因为<B normal"><FONT face="Times New Roman">CTL_ANY</FONT></B>在<FONT face="Times New Roman">2.0</FONT>内核里也没有被用到,而且整个<B normal"><FONT face="Times New Roman">sysctl</FONT></B>接口也只向后兼容到<FONT face="Times New Roman">2.0</FONT>以前的开发树版本。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30576</FONT>:如果这个表元素有一个孩子,它就是一个“目录”。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30577</FONT>:遵循标准<FONT face="Times New Roman">Unix</FONT>行为,检查目录的<FONT face="Times New Roman">x</FONT>(可执行)位来判断是否当前进程可被允许对它进行访问。注意到这与文件系统所实现的工作非常类似,虽然这并不是(<FONT face="Times New Roman">/proc</FONT>)文件系统接口。这样可以使这两种接口在施用于可调内核参数时能够得到一致的结果——如果一个用户有通过一种接口来修改某个内核参数的权限而通过另一种却没有该权限,那么将是非常不可思议的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30579</FONT>:如果表项有一个策略(<FONT face="Times New Roman">strategy</FONT>)函数,它可能需要覆盖允许该进程进入目录的授权。这个策略函数将被访问,如果它返回一个非零值,整个查找就被中止。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30587</FONT>:进入目录。本行有效的继续外层循环,并转移到该名字的下一部分。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30592</FONT>:这个表节点是一个叶子节点,因此内核参数就被找到了。注意这并不打扰对<B normal"><FONT face="Times New Roman">name</FONT></B>数组是否已到其最后元素的检查(也就是现在<B normal"><FONT face="Times New Roman">nlen</FONT></B>是否为<FONT face="Times New Roman">1</FONT>),虽然可以证明如果不是那样就会有某类型错误产生。不管哪一种情况,<B normal"><FONT face="Times New Roman">do_sysctl_strategy</FONT></B>(<FONT face="Times New Roman">30603</FONT>行)都要负责对当前表元素进行读和<FONT face="Times New Roman">/</FONT>或写操作。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30598</FONT>:<B normal"><FONT face="Times New Roman">name</FONT></B>数组非空,可是它的元素在叶子节点被找到之前均已用完。<B normal"><FONT face="Times New Roman">parse_table</FONT></B>就返回<B normal"><FONT face="Times New Roman">ENOTDIR</FONT></B>错误,来表示查找指定节点失败。顺便提及一点,注意前一行里的分号是多余的。</P><P 12pt 0cm 3.2pt"><b>do_sysctl_strategy</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30603</FONT>:<B normal"><FONT face="Times New Roman">do_sysctl_strategy</FONT></B>在单独一个<B normal"><FONT face="Times New Roman">ctl_table</FONT></B>里读和<FONT face="Times New Roman">/</FONT>或写数据。计划使用该表元素里的<B normal"><FONT face="Times New Roman">strategy</FONT></B>成员,如果存在的话,来完成读<FONT face="Times New Roman">/</FONT>写工作。如果表元素没有它自己的<B normal"><FONT face="Times New Roman">strategy</FONT></B>例程,某些通用的读<FONT face="Times New Roman">/</FONT>写代码将被替代使用。不过读者将要看到,它并不完全按照计划工作。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30610</FONT>:如果<B normal"><FONT face="Times New Roman">oldval</FONT></B>非空,调用者将读取旧值,这样<FONT face="Times New Roman">r</FONT>位就会在<B normal"><FONT face="Times New Roman">op</FONT></B>里被设置。类似的,如果<B normal"><FONT face="Times New Roman">newval</FONT></B>非空则<FONT face="Times New Roman">w</FONT>位被设置。接着,第<FONT face="Times New Roman">30614</FONT>行核查许可,如果当前进程缺少所需的授权就返回<B normal"><FONT face="Times New Roman">EPERM</FONT></B>错误。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30617</FONT>:如果表项有它自己的<B normal"><FONT face="Times New Roman">strategy</FONT></B>例程,这个例程就要处理读<FONT face="Times New Roman">/</FONT>写请求。如果它返回负数——一个错误——这个错误就被传送给调用者。如果返回的是正数,<FONT face="Times New Roman">0</FONT>(成功)就会被传送给调用者。如果是<FONT face="Times New Roman">0</FONT>,<B normal"><FONT face="Times New Roman">strategy</FONT></B>例程就拒绝由它自己来处理请求,取而代之的将是缺省行为。(读者可以设想只返回<FONT face="Times New Roman">0</FONT>的<B normal"><FONT face="Times New Roman">strategy</FONT></B>例程,如果它完成一些其它诸如收集被调用次数的统计数据这样的工作,它仍然是有用处的。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30630</FONT>:这里是通用读取代码开始的地方。注意<B normal"><FONT face="Times New Roman">get_user</FONT></B>(<FONT face="Times New Roman">13254</FONT>行)的返回值不被检查。(类似的缺陷发生在第<FONT face="Times New Roman">9537</FONT>和<FONT face="Times New Roman">31186</FONT>行。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30632</FONT>:确保不会有多于与该表项的<B normal"><FONT face="Times New Roman">maxlen</FONT></B>成员所指定的数值相等的数据被返回。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30634</FONT>:通过<B normal"><FONT face="Times New Roman">oldval</FONT></B>从表里复制所要求的数据,再将真正被写的数据总量存储在<B normal"><FONT face="Times New Roman">oldlenp</FONT></B>中。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30642</FONT>:类似于<B normal"><FONT face="Times New Roman">oldlenp</FONT></B>,要确保写入表项的数据不能多于它的<B normal"><FONT face="Times New Roman">maxlen</FONT></B>成员所允许的值。注意如果<B normal"><FONT face="Times New Roman">copy_from_user</FONT></B>在中途的第<FONT face="Times New Roman">30644</FONT>行检测到一个错误,<B normal"><FONT face="Times New Roman">tabel-&gt;data</FONT></B>可能会在仅仅被部分更新的情况下就结束。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30648</FONT>:返回<FONT face="Times New Roman">0</FONT>表示成功。以下三种情况都可以达到这一点:</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 57.25pt">l         调用者对这个表项既不读也不写。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l2 level1 lfo2; tab-stops: list 57.25pt">l         调用者尝试读和<FONT face="Times New Roman">/</FONT>或写这个表项,而且所有步骤都被成功执行。</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">maxlen</FONT></B>是<FONT face="Times New Roman">0</FONT>,所以它是只读的。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm">三种情形中的第一种有点儿奇怪,而最后一种则更令人奇怪。第一种情况有些奇特是因为调用<B normal"><FONT face="Times New Roman">sysctl</FONT></B>却要求它对指定的表项既不读也不写,这并没有多少意义,所以可以正当的把它当作一个错误来处理。尽管如此,它要与其它系统调用的内核实现保持基本一致,那就是把一个无操作请求不看作是一个错误。比如说,在第<FONT face="Times New Roman">8</FONT>章中介绍的<B normal"><FONT face="Times New Roman">sys_brk</FONT></B>(<FONT face="Times New Roman">33155</FONT>行)在由调用者指定的新<B normal"><FONT face="Times New Roman">brk</FONT></B>值与旧值相同时并不产生一个错误信号。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: 0cm">第三种情况要比第一种奇怪一些,因为它可能真的反映着一个错误。例如,调用代码尝试写入一个<B normal"><FONT face="Times New Roman">maxlen</FONT></B>是<FONT face="Times New Roman">0</FONT>的参数,而且由于系统调用返回成功值而认为该尝试已被完成。看起来事情好像不是这样,因为不管怎样为<FONT face="Times New Roman">0</FONT>的<B normal"><FONT face="Times New Roman">maxlen</FONT></B>都会使该条目失效,不过还真的存在一个<B normal"><FONT face="Times New Roman">maxlen</FONT></B>为<FONT face="Times New Roman">0</FONT>的表项——参见第<FONT face="Times New Roman">30380</FONT>行。最终,一切都归结为<B normal"><FONT face="Times New Roman">sysctl</FONT></B>是怎样在文档中描述的,但是<FONT face="Times New Roman">man</FONT>帮助程序中却对此没有任何记载。我仍然认为<B normal"><FONT face="Times New Roman">do_sysctl_strategy</FONT></B>在这种情况下应该返回一个<B normal"><FONT face="Times New Roman">EPERM</FONT></B>错误。</P><P 12pt 0cm 3.2pt"><b>register_sysctl_table</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30651</FONT>:把一个新的根已经被给出的<B normal"><FONT face="Times New Roman">ctl_table</FONT></B>树插入到其它树所形成的循环链表里。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30655</FONT>:分配一个<B normal"><FONT face="Times New Roman">struct ctl_table_header</FONT></B>用来管理新树的信息。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30659</FONT>:把新的首部(跟踪<FONT face="Times New Roman"><B normal">ctl_table</B>s</FONT>数组形成的新树)插入到首部组成的链表里。<FONT face="Times New Roman"> </FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30666</FONT>:调用<B normal"><FONT face="Times New Roman">register_proc_table</FONT></B>(<FONT face="Times New Roman">30689</FONT>行,本章前边讨论过)把新的表树注册在<FONT face="Times New Roman">/proc/sys</FONT>目录下。如果没有内核在没有<FONT face="Times New Roman">/proc</FONT>文件系统支持的情况下进行编译时,则这一行将被编译到内核以外。<p></p></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30688</FONT>:新分配的首部被返回给调用程序,以便调用程序能够在以后通过把该首部转递给<B normal"><FONT face="Times New Roman">unregister_sysctl_table</FONT></B>(<FONT face="Times New Roman">30672</FONT>行)来删除相应的树。</P><P 12pt 0cm 3.2pt"><b>unregister_sysctl_table</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">30672</FONT>:如前所述,这个简单函数只是把一个<B normal"><FONT face="Times New Roman">ctl_table</FONT></B>的树从内核里这样的树所组成的循环链表里删除。如果内核是在支持<FONT face="Times New Roman">/proc</FONT>的情况下编译的,它也用于从<FONT face="Times New Roman">/proc</FONT>文件系统里删除相应的数据。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>回顾一下第<FONT face="Times New Roman">30490</FONT>和<FONT face="Times New Roman">30500</FONT>行,读者不难发现<B normal"><FONT face="Times New Roman">root_table_header</FONT></B>(<FONT face="Times New Roman">30256</FONT>行)——对应于<B normal"><FONT face="Times New Roman">root_table</FONT></B>的列表节点——是在遍历树的循环链表时被用作头和尾节点的。读者现在能够明白实际上在<B normal"><FONT face="Times New Roman">unregister_sysctl_table</FONT></B>函数里没有什么可以避免<B normal"><FONT face="Times New Roman">root_table_header</FONT></B>被从表头列表里删除——它只是还没有这样做而已。</P><P 12pt 0cm 3.2pt"><b>sysctl_string</b></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31121</FONT>:<B normal"><FONT face="Times New Roman">sysctl_string</FONT></B>是<B normal"><FONT face="Times New Roman">ctl_table</FONT></B>的策略例程之一。回忆一下,策略例程可以从第<FONT face="Times New Roman">30618</FONT>行(在<B normal"><FONT face="Times New Roman">do_sysctl_strategy</FONT></B>里)被调用来有选择的覆盖一个表项的缺省读<FONT face="Times New Roman">/</FONT>写代码。(策略例程也可以从第<FONT face="Times New Roman">30580</FONT>行被调用,不过该例程却从不会被调用。)</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31127</FONT>:如果该表没有相关数据,或者如果可访问部分的长度是<FONT face="Times New Roman">0</FONT>,则返回<B normal"><FONT face="Times New Roman">ENOTDIR</FONT></B>错误。这与<B normal"><FONT face="Times New Roman">do_sysctl_strategy</FONT></B>的做法是不一致的,在同样的情况里它返回的是成功。</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31138</FONT>:当前字符串的值被复制到用户空间,然后结果以<FONT face="Times New Roman">NUL</FONT>来结束(这意味着比由<B normal"><FONT face="Times New Roman">lenp</FONT></B>指定值多一个字节的数据可能会被复制——依据文档记录,这可能是一个缺陷)。因为当前值已经是<FONT face="Times New Roman">NUL</FONT>结束的,这四行代码可以被简化为两行:</P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            if ( copy_to_user ( oldval , table -&gt; data , len + 1 ) )</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">              return –EFAULT ;</FONT></P><P 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>这种改变的正确性部分上依赖于当写入<B normal"><FONT face="Times New Roman">table-&gt;data</FONT></B>时代码剩余部分所遵循的三个特征:</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo3; tab-stops: list 57.25pt">l         代码剩余部分不能把多于<B normal"><FONT face="Times New Roman">table-&gt;maxlen</FONT></B>的<B normal"><FONT face="Times New Roman">char</FONT></B>数据复制进<B normal"><FONT face="Times New Roman">table-&gt;data</FONT></B>里。(这也使得第<FONT face="Times New Roman">31136</FONT>行的测试变得没有必要。即使还需要该测试,那也只用检查<FONT face="Times New Roman">&gt;</FONT>,而不用检查<FONT face="Times New Roman">&gt;=</FONT>了。)</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo3; tab-stops: list 57.25pt">l         然后<B normal"><FONT face="Times New Roman">table-&gt;data</FONT></B>以<FONT face="Times New Roman">NUL</FONT>来结束,如果必要就复写最后一个拷贝进来的字节,以便包括<FONT face="Times New Roman">NUL</FONT>在内的总长度不大于<B normal"><FONT face="Times New Roman">table-&gt;maxlen</FONT></B>。</P><P 0cm 0cm 0pt 57.25pt; TEXT-INDENT: -21.25pt; mso-list: l1 level1 lfo3; tab-stops: list 57.25pt">l         <B normal"><FONT face="Times New Roman">table-&gt;maxlen</FONT></B>永不发生变化。</P><B normal">       </B>因为所有三个特征都有效,所以在第31138行<B normal">len</B>将总是严格小于<B normal">table-&gt;maxlen</B>,而且结束NUL字节一定会在<B normal">table-&gt;data[len+1]</B>或之前的位置出现。
作者: ilikenba    时间: 2005-3-4 22:33
< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31146</FONT>:与前边的情况类似,从用户空间中复制新值,而且结果以<FONT face="Times New Roman">NUL</FONT>来结束。不过在这种情形下,不从用户空间复制<FONT face="Times New Roman">NUL</FONT>字节是一种正确的做法,因为把它从用户空间复制进来要比仅仅在<FONT face="Times New Roman">data</FONT>的适当字节安排一个<FONT face="Times New Roman">NUL</FONT>效率低。而且以这种方式,即使输入不是<FONT face="Times New Roman">NUL</FONT>结束的,<B normal"><FONT face="Times New Roman">table-&gt;data</FONT></B>也要如此。当然,从<B normal"><FONT face="Times New Roman">newval</FONT></B>读出的字符串可能已经是<FONT face="Times New Roman">NUL</FONT>结束的,在那种情况里第<FONT face="Times New Roman">31154</FONT>行的赋值就是多余的。这还是另一种情况,直接完成工作比检查需要是否执行它还要快。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31156</FONT>:返回<FONT face="Times New Roman">0</FONT>表示成功。相反,返回的值应该为正数,以便<FONT face="Times New Roman">30618</FONT>行代码认为结果是成功的。而又相反,调用代码认为<B normal"><FONT face="Times New Roman">sysctl_string</FONT></B>想让缺省处理发生,然后它就继续从用户空间再次复制多余的数据。</P>< 12pt 0cm 3.2pt"><b>sysctl_intvec</b></P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31163</FONT>:<B normal"><FONT face="Times New Roman">sysctl_intvec</FONT></B>是在<FONT face="Times New Roman">kernel/sysctl.c</FONT>里定义的另一个策略例程。它确保假如调用程序正在写入表项,所有被写的<B normal"><FONT face="Times New Roman">int</FONT></B>都应位于某个最小和最大值之间。(顺便提及一下,<B normal"><FONT face="Times New Roman">sysctl_intvec</FONT></B>在这个文件里只被使用了一次——在第<FONT face="Times New Roman">30414</FONT>行——尽管它被广泛的用于本书所没有包括的内核的其它代码之中。)</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31170</FONT>:如果新的欲写数据总量不符合一个<B normal"><FONT face="Times New Roman">int</FONT></B>大小的边界,它就无效,所以尝试被抛弃。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31173</FONT>:假如表项没有指定一组最大或最小值,输入的值就永远不可能超出范围,这样调用程序里的普通写代码(<B normal"><FONT face="Times New Roman">do_sysctl_strategy</FONT></B>,<FONT face="Times New Roman">30603</FONT>行)就足够好了。因此在这种情况里<B normal"><FONT face="Times New Roman">sysctl_intvec</FONT></B>返回<FONT face="Times New Roman">0</FONT>。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31184</FONT>:进行循环以确保所有来自输入数组的值都位于适当范围之内。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31186</FONT>:这行代码不检查<B normal"><FONT face="Times New Roman">get_user</FONT></B>的返回值——没有迫切的需要去这样做。如果当不能读取一个输入内存位置时,<B normal"><FONT face="Times New Roman">sysctl_intvec</FONT></B>返回<FONT face="Times New Roman">0</FONT>(成功),那么当它试图读取整个数组时<B normal"><FONT face="Times New Roman">do_sysctl_strategy</FONT></B>就会注意到这个问题。作为另一选择,假如<B normal"><FONT face="Times New Roman">get_user</FONT></B>无法读取内存位置,无用信息(<FONT face="Times New Roman">garbage</FONT>)可能在<B normal"><FONT face="Times New Roman">value</FONT></B>里结束并且数值可能会不正确的被抛弃。在此情况里,调用程序将得到一个<B normal"><FONT face="Times New Roman">EINVAL</FONT></B>错误而不是<B normal"><FONT face="Times New Roman">EFAULT</FONT></B>错误,这只是一个小缺陷(<FONT face="Times New Roman">bug</FONT>)。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31187</FONT>:注意这一行不会被折磨第<FONT face="Times New Roman">31033</FONT>行相似代码的缺陷所困扰,该行中在最小值和最大值之上进行的并行循环会产生不同步的情况。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">            </FONT>这一行代码能够避免位于<FONT face="Times New Roman">31033</FONT>行的缺陷被暴露出来。正如实际中所进行的,<B normal"><FONT face="Times New Roman">sysctl_intvec</FONT></B>和<B normal"><FONT face="Times New Roman">proc_dointvec_minmax</FONT></B>都总是与同一个<B normal"><FONT face="Times New Roman">ctl_table</FONT></B>条目相关联的。因此,在调用处理例程(<FONT face="Times New Roman">handler routine</FONT>)<B normal"><FONT face="Times New Roman">proc_dointvec_minmax</FONT></B>之前,任何超出允许范围之外的数值将会被策略例程<B normal"><FONT face="Times New Roman">sysctl_intvec</FONT></B>截获。所以,我们知道——在给定内核里所有的<B normal"><FONT face="Times New Roman">ctl_tables</FONT></B>最新定义的情况下——<B normal"><FONT face="Times New Roman">proc_dointvec_minmax</FONT></B>将永远不会遇到超出界限的数值,而那里是唯一可以触发该缺陷的数值种类。某个调用程序或许可以注册一个使用<B normal"><FONT face="Times New Roman">proc_dointvec_minmax</FONT></B>但没有策略例程的<B normal"><FONT face="Times New Roman">ctl_table</FONT></B>,但是尽管这样,在<B normal"><FONT face="Times New Roman">proc_dointvec_ minmax</FONT></B>里的这个缺陷迟早会造成一定损害。</P>< 0cm 0cm 0pt 36pt; TEXT-INDENT: -36pt"><FONT face="Times New Roman">31193</FONT>:返回<FONT face="Times New Roman">0</FONT>表示成功。这里不像在第<FONT face="Times New Roman">31156</FONT>行那样是一个错误,因为<B normal"><FONT face="Times New Roman">sysctl_intvec</FONT></B>并不向<B normal"><FONT face="Times New Roman">table-&gt;data</FONT></B>里写入。从用户空间读出的值只是被读进一个临时变量里作范围检查,然后就被删除;<B normal"><FONT face="Times New Roman">do_sysctl_strategy</FONT></B>将完成那项工作,并只向<B normal"><FONT face="Times New Roman">table-&gt;data</FONT></B>里进行写入。</P>




欢迎光临 数学建模社区-数学中国 (http://www.madio.net/) Powered by Discuz! X2.5