QQ登录

只需要一步,快速开始

 注册地址  找回密码
查看: 11228|回复: 18
打印 上一主题 下一主题

[分享]《Linux内核注释》

[复制链接]
字体大小: 正常 放大
ilikenba 实名认证       

1万

主题

49

听众

2万

积分

  • TA的每日心情
    奋斗
    2024-6-23 05:14
  • 签到天数: 1043 天

    [LV.10]以坛为家III

    社区QQ达人 新人进步奖 优秀斑竹奖 发帖功臣

    群组万里江山

    群组sas讨论小组

    群组长盛证券理财有限公司

    群组C 语言讨论组

    群组Matlab讨论组

    跳转到指定楼层
    1#
    发表于 2005-3-4 21:40 |只看该作者 |正序浏览
    |招呼Ta 关注Ta
    <  align=center><B >介  绍<p></p></B></P>
    3 j8 _% u2 q% F<  align=left><FONT size=3>    《Linux内核注释》旨在给程序员和学生提供比以前更详细和更易理解的Linux内核代码注释。作者分析了核心代码,并对重要的函数、系统调用和数据结构提供了大量的注释。<p></p></FONT></P>
    ) }3 z: N3 T# m) U# i3 M<  align=left><FONT size=3>    对《注释》系列丛书的写作灵感都来源于John Lions所著的大量流行的《Lions' Commentary on Unix》一书。无数的计算机专业的学生在复制和使用这本书。这本书对AT&amp;T的Unix操作系统的早期版本的内幕进行了深刻的剖析。<p></p></FONT></P>. Y: W: Q! \% K# x; G5 K
    <  align=left><FONT size=3>    《Liunx内核注释》同样提供了对流行的功能强大的Liunx操作系统的结构和函数实现的内幕介绍。本书的主要目标是:<p></p></FONT></P>: b. f4 K9 l! p
    <  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>$ z1 R8 m# P8 x3 Z
    <  align=left><FONT size=3>    2.提供一个对每个子系统功能的一般性概述。<p></p></FONT></P>4 D" s' R. S7 G% h
    <  align=left><FONT size=3>    3.研究各个子系统主要的函数和数据结构。<p></p></FONT></P>. y& e& z9 {" {( ?8 G
    <  align=left><FONT size=3>    4.对开发者应怎样通过修改源代码来改进和扩展内核提出建议。<p></p></FONT></P>8 M( n3 A! s9 j- g7 P
    <  align=left><FONT size=3>    本书的最后一项目标<FONT face="Times New Roman">—</FONT>定制--是你学习内核代码的最有说服力的原因。通过理解内核是怎样工作的,你能够编写自己的代码用以在你的操作系统中实现所需要的功能。如果允许其他人共享你的改进,你的代码甚至会在官方发行的内核代码中出现,被全世界数百万计的人们所使用。<p></p></FONT></P>, Q# O6 h& O' n
    <  align=left><FONT size=3>    开放源代码是指让开发者研究源代码并实现功能性扩展。Linux是全世界成长最快的操作系统,开放源代码是其主要的原因之一。从玩游戏,到网上冲浪,到为大大小小的ISP们提供稳定的Web服务器平台以至解决最庞大的科学难题,Linux都能胜任全部工作。它之所以能如此强大是因为有像你一样的开发者在研究、学习并且扩充这个系统。<p></p></FONT></P>: ^% H) r+ Z8 o9 d- m! D
    <  align=left><B >你能从本书中学到什么</B><B ><p></p></B></P>
    + @: {5 n8 }) J% _) U. x<  align=left><FONT size=3>    这本书集中解释了Linux内核源代码的核心中专用代码行是如何运行的。你将学习到内核最内部的子系统是怎样构造和这种构造能够实现系统功能的理由。<p></p></FONT></P>
    ; Y- c4 {- T  w  B) [: \, S<  align=left><FONT size=3>    本书的第一部分以易于阅读和交叉引用的格式复制了一个经过筛选的linux 内核源代码的子集。在这本书稍后的注释中,无论一行代码在何处被引用,你都会在这一行前面发现一个小箭头。这个箭头指出了对此行进行注释处的页号。<p></p></FONT></P>" U1 K/ ]) n+ o' P! G1 C( b
    <  align=left><FONT size=3>    源代码后是这本书的第二部分,即注释部分,注释部分对源代码进行了讨论。注释部分的每一章讨论了一个不同的内核子系统,或者是其它的功能性逻辑组件,例如系统调用或内存管理。注释部分大量的行号引用为你指明了所讨论代码行的确切行号。<p></p></FONT></P>
    ' H6 c$ S. n/ ]) I. I<  align=left><FONT size=3>    在本书正文后的附录部分,简洁地覆盖了自本书主要部分完成以后内核的变化。在附录中还包含了被内核用做软件许可证的完整的GNU常规公众许可证。最后,本书为你提供了一个索引。通过该索引你可以查询术语或主题。这将让你更快更有效的使用这本参考工具书。<p></p></FONT></P>
    ( |  G: U! C2 S  D$ K1 r9 K) I6 u<  align=left><B >本书的使用对象</B><p></p></P>
    # P# q% _) u- T0 ]% T: k$ U0 }<  align=left><FONT size=3>    本书假设你能阅读C语言的代码,不怕偶尔读一些汇编语言代码。并且你想知道一个快速的、坚固的、可靠的、健壮的、现代的、实用的操作系统是如何工作的。一些读者也许是这样的程序员,他们想为前进中的Linux内核发展工作提供他们自己的改进和添加内容。<p></p></FONT></P>& m, ?; }# Q3 C" L; H
    <  align=left><B >如何使用本书</B><p></p></P>- d# o% R; D) U  k
    <  align=left><FONT size=3>    用最适合你自己的方法放松地去看这本《linux 内核注释》。因为写这本书的目的是为提供一个参考资料,你不必从头看到尾。因为注释和代码是一一对应的,你可以从另外一个方向接近内核。<p></p></FONT></P>
    ! i" b: w) u! |& o' P<  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>; V5 o1 ~9 @  i7 n
    <  align=left><p><FONT face="Times New Roman" size=3> </FONT></p></P>
    zan
    转播转播0 分享淘帖0 分享分享0 收藏收藏0 支持支持0 反对反对0 微信微信
    ilikenba 实名认证       

    1万

    主题

    49

    听众

    2万

    积分

  • TA的每日心情
    奋斗
    2024-6-23 05:14
  • 签到天数: 1043 天

    [LV.10]以坛为家III

    社区QQ达人 新人进步奖 优秀斑竹奖 发帖功臣

    群组万里江山

    群组sas讨论小组

    群组长盛证券理财有限公司

    群组C 语言讨论组

    群组Matlab讨论组

    < 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>
    回复

    使用道具 举报

    ilikenba 实名认证       

    1万

    主题

    49

    听众

    2万

    积分

  • TA的每日心情
    奋斗
    2024-6-23 05:14
  • 签到天数: 1043 天

    [LV.10]以坛为家III

    社区QQ达人 新人进步奖 优秀斑竹奖 发帖功臣

    群组万里江山

    群组sas讨论小组

    群组长盛证券理财有限公司

    群组C 语言讨论组

    群组Matlab讨论组

    <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 实名认证       

    1万

    主题

    49

    听众

    2万

    积分

  • TA的每日心情
    奋斗
    2024-6-23 05:14
  • 签到天数: 1043 天

    [LV.10]以坛为家III

    社区QQ达人 新人进步奖 优秀斑竹奖 发帖功臣

    群组万里江山

    群组sas讨论小组

    群组长盛证券理财有限公司

    群组C 语言讨论组

    群组Matlab讨论组

    < 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 实名认证       

    1万

    主题

    49

    听众

    2万

    积分

  • TA的每日心情
    奋斗
    2024-6-23 05:14
  • 签到天数: 1043 天

    [LV.10]以坛为家III

    社区QQ达人 新人进步奖 优秀斑竹奖 发帖功臣

    群组万里江山

    群组sas讨论小组

    群组长盛证券理财有限公司

    群组C 语言讨论组

    群组Matlab讨论组

    <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 实名认证       

    1万

    主题

    49

    听众

    2万

    积分

  • TA的每日心情
    奋斗
    2024-6-23 05:14
  • 签到天数: 1043 天

    [LV.10]以坛为家III

    社区QQ达人 新人进步奖 优秀斑竹奖 发帖功臣

    群组万里江山

    群组sas讨论小组

    群组长盛证券理财有限公司

    群组C 语言讨论组

    群组Matlab讨论组

    < 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 实名认证       

    1万

    主题

    49

    听众

    2万

    积分

  • TA的每日心情
    奋斗
    2024-6-23 05:14
  • 签到天数: 1043 天

    [LV.10]以坛为家III

    社区QQ达人 新人进步奖 优秀斑竹奖 发帖功臣

    群组万里江山

    群组sas讨论小组

    群组长盛证券理财有限公司

    群组C 语言讨论组

    群组Matlab讨论组

    < 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 实名认证       

    1万

    主题

    49

    听众

    2万

    积分

  • TA的每日心情
    奋斗
    2024-6-23 05:14
  • 签到天数: 1043 天

    [LV.10]以坛为家III

    社区QQ达人 新人进步奖 优秀斑竹奖 发帖功臣

    群组万里江山

    群组sas讨论小组

    群组长盛证券理财有限公司

    群组C 语言讨论组

    群组Matlab讨论组

    <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 实名认证       

    1万

    主题

    49

    听众

    2万

    积分

  • TA的每日心情
    奋斗
    2024-6-23 05:14
  • 签到天数: 1043 天

    [LV.10]以坛为家III

    社区QQ达人 新人进步奖 优秀斑竹奖 发帖功臣

    群组万里江山

    群组sas讨论小组

    群组长盛证券理财有限公司

    群组C 语言讨论组

    群组Matlab讨论组

    < 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 实名认证       

    1万

    主题

    49

    听众

    2万

    积分

  • TA的每日心情
    奋斗
    2024-6-23 05:14
  • 签到天数: 1043 天

    [LV.10]以坛为家III

    社区QQ达人 新人进步奖 优秀斑竹奖 发帖功臣

    群组万里江山

    群组sas讨论小组

    群组长盛证券理财有限公司

    群组C 语言讨论组

    群组Matlab讨论组

    < 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行),它决定是否应该允许进程设置优先级(完美级别)
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 注册地址

    qq
    收缩
    • 电话咨询

    • 04714969085
    fastpost

    关于我们| 联系我们| 诚征英才| 对外合作| 产品服务| QQ

    手机版|Archiver| |繁體中文 手机客户端  

    蒙公网安备 15010502000194号

    Powered by Discuz! X2.5   © 2001-2013 数学建模网-数学中国 ( 蒙ICP备14002410号-3 蒙BBS备-0002号 )     论坛法律顾问:王兆丰

    GMT+8, 2025-7-28 17:19 , Processed in 1.274637 second(s), 97 queries .

    回顶部