找回密码
 注册
查看: 22710|回复: 22

Linux内核注释

[复制链接]
发表于 2003-10-27 09:28:13 | 显示全部楼层 |阅读模式
Linux内核注释——序

《Linux内核注释》旨在给程序员和学生提供比以前更详细和更易理解的Linux内核
代码注释。作者分析了核心代码,并对重要的函数、系统调用和数据结构提供了大
量的注释。
    对《注释》系列丛书的写作灵感都来源于John Lions所著的大量流行的《
Lions' Commentary on Unix》一书。无数的计算机专业的学生在复制和使用这本
书。这本书对AT&T的Unix操作系统的早期版本的内幕进行了深刻的剖析。
    《Liunx内核注释》同样提供了对流行的功能强大的Liunx操作系统的结构和函
数实现的内幕介绍。本书的主要目标是:
    1.提供一个最新的和完整的服务器版本的完整源代码。(这本书分析的版本是
2.2.5版,也是写这本书时发布的最新版本。)
    2.提供一个对每个子系统功能的一般性概述。
    3.研究各个子系统主要的函数和数据结构。
    4.对开发者应怎样通过修改源代码来改进和扩展内核提出建议。
    本书的最后一项目标—定制--是你学习内核代码的最有说服力的原因。通过理
解内核是怎样工作的,你能够编写自己的代码用以在你的操作系统中实现所需要的
功能。如果允许其他人共享你的改进,你的代码甚至会在官方发行的内核代码中出
现,被全世界数百万计的人们所使用。
    开放源代码是指让开发者研究源代码并实现功能性扩展。Linux是全世界成长
最快的操作系统,开放源代码是其主要的原因之一。从玩游戏,到网上冲浪,到为
大大小小的ISP们提供稳定的Web服务器平台以至解决最庞大的科学难题,Linux都
能胜任全部工作。它之所以能如此强大是因为有像你一样的开发者在研究、学习并
且扩充这个系统。
你能从本书中学到什么
    这本书集中解释了Linux内核源代码的核心中专用代码行是如何运行的。你将
学习到内核最内部的子系统是怎样构造和这种构造能够实现系统功能的理由。
    本书的第一部分以易于阅读和交叉引用的格式复制了一个经过筛选的linux 内
核源代码的子集。在这本书稍后的注释中,无论一行代码在何处被引用,你都会在
这一行前面发现一个小箭头。这个箭头指出了对此行进行注释处的页号。
    源代码后是这本书的第二部分,即注释部分,注释部分对源代码进行了讨论。
注释部分的每一章讨论了一个不同的内核子系统,或者是其它的功能性逻辑组件,
例如系统调用或内存管理。注释部分大量的行号引用为你指明了所讨论代码行的确
切行号。
    在本书正文后的附录部分,简洁地覆盖了自本书主要部分完成以后内核的变化
。在附录中还包含了被内核用做软件许可证的完整的GNU常规公众许可证。最后,本
书为你提供了一个索引。通过该索引你可以查询术语或主题。这将让你更快更有效
的使用这本参考工具书。
本书的使用对象
    本书假设你能阅读C语言的代码,不怕偶尔读一些汇编语言代码。并且你想知道
一个快速的、坚固的、可靠的、健壮的、现代的、实用的操作系统是如何工作的。
一些读者也许是这样的程序员,他们想为前进中的Linux内核发展工作提供他们自己
的改进和添加内容。
如何使用本书
    用最适合你自己的方法放松地去看这本《linux 内核注释》。因为写这本书的
目的是为提供一个参考资料,你不必从头看到尾。因为注释和代码是一一对应的,
你可以从另外一个方向接近内核。
    欢迎你对我的第一本书提出意见。你可以通过e-mail和我联系。地址是:
[email protected]。 勘误表、更新和其它一些有用信息可以通过访问
http://www.ScottMaxwell.org/lckc.html得到。
 楼主| 发表于 2003-10-27 09:29:01 | 显示全部楼层
Linux内核注释——第一章——简介

第1章  Linux简介
让用户很详细地了解现有操作系统的实际工作方式是不可能的,因为大多数操作系
统的源代码都是严格保密的。其例外是一些研究用的系统,另外一些是明确为操作
系统教学而设计的系统。(还有一些系统则是同时出于这两种目的。)尽管研究和
教学这两个目的都很好,但是这类系统很少能够通过对正式操作系统的小部分实现
来体现操作系统的实际功能。对于操作系统的一些特殊问题,这种折衷系统所能够
表现的就更是少得可怜了。

在以实际使用为目标的操作系统中,让任何人都可以自由获取系统源代码,无论目
的是要了解、学习还是改进,这样的现实系统并不多。本书的主题就是这些少数操
作系统中的一个:Linux。
Linux的工作方式类似于Uinx,是免费的,源代码也是开放的,符合标准规范的32
位(在64位CPU上是64位)操作系统。Linux拥有现代操作系统的所具有的内容,例
如:
?   真正的抢先式多任务处理,支持多用户
?   内存保护
?   虚拟内存
?   支持对称多处理机SMP(symmetric multiprocessing),即多个CPU机器,以及
通常的单CPU(UP)机器
?   符合POSIX标准
?   联网
?   图形用户接口和桌面环境(实际上桌面环境并不只一个)
?   速度和稳定性

严格说来,Linux并不是一个完整的操作系统。当我们在安装通常所说的Linux时,
我们实际安装的是很多工具的集合。这些工具协同工作以组成一个功能强大的实用
系统。Linux本身只是这个操作系统的内核,是操作系统的心脏、灵魂、指挥中心
。(整个系统应该称为GNU/Linux,其原因在本章的后续内容中将会给以介绍。)
内核以独占的方式执行最底层任务,保证系统正常运行——协调多个并发进程,管
理进程使用的内存,使它们相互之间不产生冲突,满足进程访问磁盘的请求等等。

在本书中,我们给大家揭示的就是Linux是如何完成这一具有挑战性的工作的。
Linux(和Unix)的简明历史

为了让大家对本书所讨论的内容有更清楚的了解,让我们先来简要回顾一下Linux
的历史。由于Linux是在Unix的基础上发展而来的,我们的话题就从Unix开始。
Unix是由AT&T贝尔实验室的Ken Thompson和Dennis Ritchie于1969年在一台已经废
弃了的PDP-7上开发的;它最初是一个用汇编语言写成的单用户操作系统。不久,
Thompson和Ritchie成功地说服管理部门为他们购买更新的机器,以便该开发小组
可以实现一个文本处理系统,Unix就在PDP-11上用C语言重新编写(发明C语言的部
分目的就在于此)。它果真变成了一个文本处理系统——不久之后。只不过问题是
他们先实现了一个操作系统而已…

最终,他们实现了该文本处理工具,而且Unix(以及Unix上运行的工具)也在
AT&T得到广泛应用。在1973年,Thompson和Ritchie在一个操作系统会议上就这个
系统发表了一篇论文,该论文引起了学术界对Unix系统的极大兴趣。
由于1956年反托拉斯法案的限制,AT&T不能涉足计算机业务,但允许它可以以象征
性的费用发售该系统。就这样,Unix被广泛发布,首先是学术科研用户,后来又扩
展到政府和商业用户。

伯克利(Berkeley)的加州大学是学术用户中的一个。在这里Unix得到了计算机系
统研究小组(CSRG)的广泛应用。并且在这里所进行的修改引发了Unix的一大系列
,这就是广为人知的伯克利软件开发(BSD)Unix。除了AT&T所提供的Unix系列之
外,BSD是最有影响力的Unix系列。BSD在Unix中增加了很多显著特性,例如
TCP/IP网络,更好的用户文件系统(UFS),工作控制,并且改进了AT&T的内存管
理代码。

多年以来,BSD版本的Unix一直在学术环境中占据主导地位,但最终发展成为
System V版本的AT&T的Unix则成为商业领域的主宰。从某种程度上来说,这是有社
会原因的:学校倾向于使用非正式但通常更好用的BSD风格的Unix,而商业界则倾
向于从AT&T获取Unix。

在用户需求驱动和用户编程改进特性的促进下,BSD风格的Unix一般要比AT&T的
Unix更具有创新性,而且改进也更为迅速。但是,在AT&T发布最后一个正式版本
System V Release 4(SVR4)时,System V Unix已经吸收了BSD的大多数重要的优
点,并且还增加了一些自己的优势。这种现象的部分原因在于从1984年开始,
AT&T逐渐可以将Unix商业化,而伯克利Unix的开发工作在1993年BSD4.4版本完成以
后就逐渐收缩以至终止了。然而,BSD的进一步改进由外界开发者延续下来,到今
天还在继续进行。正在进行的Unix系列开发中至少有四个独立的版本是直接起源于
BSD4.4,这还不包括几个厂商的Unix版本,例如惠普的HP-UX,都是部分地或者全
部地基于BSD而发展起来的。

实际上Unix的变种并不止BSD和System V。由于Unix主要使用C语言来编写,这就使
得它相对比较容易地移植到新的机器上,它的简单性也使其相对比较容易重新设计
与开发。Unix的这些特点大受商业界硬件供应商的欢迎,比如Sun、SGI、惠普、
IBM、DEC(数字设备公司)、Amdahl等等;IBM还不止一次对Unix进行了再开发。
厂商们设计开发出新的硬件并简单地将Unix移植到新的硬件上,这样新的硬件一经
发布便具备一定的功能。经过一段时间之后,这些厂商都拥有了自己的专有Unix版
本。而且为了占有市场,这些版本故意以不同的侧重点发布出来以更好的占有用户


版本混乱的状态促进了标准化工作的进行。其中最主要的就是POSIX系列标准,它
定义了一套标准的操作系统接口和工具。从理论上说,POSIX标准代码很容易移植
到任何遵守POSIX标准的操作系统中,而且严格的POSIX测试已经把这种理论上的可
移植性转化为现实。直到今天,几乎所有的正式操作系统都以支持POSIX标准为目
标。

现在让我们回顾一下,在1984年,杰出的电脑黑客Richard Stallman独立开发出一
个类Unix的操作系统,该操作系统具有完全的内核、开发工具和终端用户应用程序
。在GNU(“GNU’s Not Unix”首字母的缩写)计划的配合下,Stallman开发这个
产品有自己的技术理想:他想开发出一个质量高而且自由的操作系统。Stallman使
用了“自由”(free)这个词,不仅意味着用户可以免费的获取软件;而且更重要
的是,它将意味着某种程度的“解放”:用户可以自由使用、拷贝、查询、重用、
修改甚至是分发这份软件,完全没有软件使用协议的限制。这也正是Stallman创建
自由软件基金会(FSF)资助GNU软件开发的本意(FSF也在资助其它科研方面的开
发工作)。

15年以来,GNU工程已经吸收、产生了大量的程序,这不仅包括Emacs,gcc(GNU的
C编译器),bash(shell命令),还有大部分Linux用户所熟知的许多应用程序。
现在正在进行开发的项目是GNU Hurd内核,这是GNU操作系统的最后一个主要部件
(实际上Hurd内核早已能够使用了,不过当前的版本号为0.3的系统在什么时候能
够完成,还是未知数)。

尽管Linux大受欢迎,但是Hurd内核还在继续开发。这种情况的原因有几个方面,
其一是Hurd的体系结构十分清晰的体现了Stallman关于操作系统工作方式的思想,
例如,在运行期间,任何用户都可以部分的改变或替换Hurd(这种替换不是对每个
用户都是可见的,而是只对申请修改的用户可见,而且还必须符合安全规范)。另
一个原因是据介绍Hurd对于多处理器的支持比Linux本身的内核要好。还有一个简
单的原因是兴趣的驱动,因为程序员们希望能够自由地进行自己所喜欢的工作。只
要有人希望为Hurd工作,Hurd的开发就不会停止。如果他们能够如愿以偿,Hurd有
朝一日将成为Linux的强劲对手。不过在今天,Linux还是自由内核王国里无可争议
的主宰。

在GNU发展的中期,也就是1991年,一个名叫Linus Torvalds的芬兰大学生想要了
解Intel的新CPU——80386。他认为比较好的学习方法是自己编写一个操作系统的
内核。出于这种目的,加上他对当时Unix变种版本对于80386类机器的脆弱支持十
分不满,他决定要开发出一个全功能的、支持POSIX标准的、类Unix的操作系统内
核,该系统吸收了BSD和System V的优点,同时摒弃了它们的缺点。Linus(虽然我
知道我应该称他为Torvalds,但是所有人都称他为Linus)独立把这个内核开发到
0.02版,这个版本已经可以运行gcc,bash和很少的一些应用程序。这些就是他开
始的全部工作了。后来,他又开始在因特网络上寻求广泛的帮助。
不到三年,Linus的Unix—Linux—已经升级到1.0版本。它的源代码量也呈指数形
式增长,实现了基本的TCP/IP功能(网络部分的代码后来重写过,而且还可能会再
次重写)。此时Linux就已经拥有大约10万用户了。

现在的Linux内核由150多万行代码组成,Linux也已经拥有了大约1000万用户(由
于Linux可以自由获取和拷贝,获取具体的统计数字是不可能的)。Linux内核
GNU/Linux附同GNU工具已经占据了Unix 50%的市场。一些公司正在把内核和一些应
用程序同安装软件打包在一起,生产出Linux的distribution(发行版本),这些
公司包括Red Hat和Calera prominent公司。现在的GNU/Linux已经备受注目,得到
了诸如Sun、IBM、SGI等公司的广泛支持。SGI最近决定在其基于Intel的Merced的
系列机器上不再搭载自己的Unix变种版本IRIX,而是直接采用GNU/Linux;Linux甚
至被指定为Amiga将要发布的新操作系统的基础。
GNU通用公共许可证

这样一个如此流行大受欢迎的操作系统当然值得我们学习。按照通用公共许可证
(GPL,(General Public License))的规定,Linux的源代码可以自由获取,这使
得我们学习该系统的强烈愿望得以实现。GPL这份非同寻常的软件许可证,充分体
现了上面提到的Stallman的思想:只要用户所做的修改是同等自由的,用户可以自
由地使用、拷贝、查询、重用、修改甚至重新发布这个软件。通过这种方式,GPL
保证了Linux(以及同一许可证保证下的大量其它软件)不仅现在自由可用,而且
以后经过任何修改之后都仍然可以自由使用。

请注意这里的自由并不是说没有人靠这个软件盈利,有一些日益兴起的公司,比如
发行最流行的Linux发行版本的Red Hat,就是一个例子。(Red Hat自从面世以来
,市值已经突破数十亿美元,每年盈利数十万美元,而且这些数字还在不断增长)
。但是任何人都不能限制其它用户涉足本软件领域,而且所作的修改不能减少其自
由程度。

本书的附录B中收录有GNU通用公共许可证协议的全文。


Linux开发过程

如上所述,由于Linux是一款自由软件,它可以免费获取以供学习研究。Linux之所
以值得学习研究,是因为它是相当优秀的操作系统。如果Linux操作系统相当糟糕
,那它就根本不值得被我们使用,也就没有必要去研究相关的书籍。(除非一种可
能,为了追求刺激)。Linux是一款十分优秀的操作系统还在于几个相互关联的原
因。

Linux优秀的原因之一在于它是基于天才的思想开发而成的。在学生时代就开始推
动整个系统开发的Linus Torvads是一个天才,他的才能不仅展现在编程能力方面
,而且组织技巧也相当杰出。Linux的内核是由世界上一些最优秀的程序员开发并
不断完善的,他们通过Internet相互协作,开发理想的操作系统;他们享受着工作
中的乐趣,而且也获得了充分的自豪感。

Linux优秀的另外一个原因在于它是基于一组优秀的概念。Unix是一个简单却非常
优秀的模型。在Linux创建之前,Unix已经有20年的发展历史。Linux从Unix的各个
流派中不断吸取成功经验,模仿Unix的优点,抛弃Unix的缺点。这样做的结果是
Linux 成为了Unix系列中的佼佼者:高速、健壮、完整,而且抛弃了历史包袱。
然而,Linux最强大的生命力还在于其公开的开发过程。每个人都可以自由获取内
核源程序,每个人都可以对源程序加以修改,而后他人也可以自由获取你修改后的
源程序。如果你发现了缺陷(bug),你可以对它进行修正,而不用去乞求不知名
的公司来为你修正。如果你有什么最优化或者新特点的创意,你也可以直接在系统
中增加功能,而不用向操作系统供应商解释你的想法,指望他们将来会增加相应的
功能。当发现一个安全漏洞后,你可以通过编程来弥补这个漏洞,而不用关闭系统
直到你的供应商为你提供修补程序。由于你拥有直接访问源代码的能力,你也可以
直接阅读代码来寻找缺陷,或是效率不高的代码,或是安全漏洞,以防患于未然。

除非你是一个程序员,否则这一点听起来仿佛没有多少吸引力。实际上即使你不是
程序员,这种开发模型也将使你受益匪浅,这主要体现在以下两个方面:
?   可以间接受益于世界各地成千上万的程序员随时进行的改进工作。
?   如果你需要对系统进行修改,你可以雇用程序员为你完成工作。这部分人将根据
你的需求定义单独为你服务。可以设想,这在源程序不公开的操作系统中它将是什
么样子。

Linux这种独特的自由流畅的开发模型已被命名为bazaar(集市模型),它是相对
于cathedral(教堂)模型而言的。在cathedral模型中,源程序代码被锁定在一个
保密的小范围内。只有开发者(很多情况下是市场)认为能够发行一个新版本,这
个新版本才会被推向市场。这些术语在Eric S. Raymond的The Cathedral and the
Bazaar一文中有所介绍,大家可以在http://www.tuxedo.org/~esr/writings/找
到这篇文章。Bazaar开发模型通过重视实验,征集并充分利用早期的反馈,对巨大
数量的脑力资源进行平衡配置,可以开发出更优秀的软件。(顺便说一下,虽然
Linux是最为明显的使用bazaar开发模型的例子,但是它却远不是第一个使用这个
模型的系统。)

为了确保这些无序的开发过程能够有序地进行,Linux采用了双树系统。一个树是
稳定树(stable tree),另一个树是非稳定树(unstable tree)或者开发树(
development tree)。一些新特性、实验性改进等都将首先在开发树中进行。如果
在开发树中所做的改进也可以应用于稳定树,那么在开发树中经过测试以后,在稳
定树中将进行相同的改进。按照Linus的观点,一旦开发树经过了足够的发展,开
发树就会成为新的稳定树,如此周而复始的进行下去。

源程序版本号的形式为x.y.z。对于稳定树来说,y是偶数;对于开发树来说,y比
相应的稳定树大一(因此,是奇数)。截至到本书截稿时,最新的稳定内核版本号
是2.2.10,最新的开发内核的版本号是2.3.12。对2.3树的缺陷修正会回溯影响(
back-propagated)2.2树,而当2.3树足够成熟的时候会发展成为2.4.0。(顺便说
一下,这种开发会比常规惯例要快,因为每一版本所包含的改变比以前更少了,内
核开发人员只需花很短的时间就能够完成一个实验开发周期。)
http://www.kernel.org及其镜像站点提供了最新的可供下载的内核版本,而且同
时包括稳定和开发版本。如果你愿意的话,不需要很长时间,这些站点所提供的最
新版本中就可能包含了你的一部分源程序代码。
回复

使用道具 举报

 楼主| 发表于 2003-10-27 09:30:22 | 显示全部楼层
Linux内核注释——第二章——第2章 代码

本章首先从较高层次介绍Linux内核源程序的概况,这些都是大家关心的一些基本特点。
随后将简要介绍一些实际代码。最后以如何编译内核来检验个人所进行的修
改的讨论来作为本章的收尾。
Linux内核源程序的部分特点
在过去的一段时期,Linux内核同时使用C语言和汇编语言实现的。这两种语言需要
一定的平衡:C语言编写的代码移植性较好、易于维护,而汇编语言编写的程序则
速度较快。一般只有在速度是关键因素或者一些因平台相关特性而产生的特殊要求
(例如直接和内存管理硬件进行通讯)时才使用汇编语言。
正如同实际中所做的,即使内核并未使用C++的对象特性,部分内核也可以在g++(
GNU的C++编译器)下进行编译。同其它面向对象的编程语言相比较,相对而言C++
的开销是较低的,但是对于内核开发人员来说,这已经足够甚至太多了。
内核开发人员不断发展编程风格,形成了Linux代码独有的特色。本节将讨论其中
的一些问题。
gcc特性的使用
Linux内核被设计为必须使用GNU的C编译器gcc来编译,而不是任何一种C编译器都
可以使用。内核代码有时要使用gcc特性,伴随着本书的进程,我们将陆续介绍其
中的一部分。
一些gcc特有代码只是简单地使用gcc语言扩展,例如允许在C(不只是C++)中使用
inline关键字指示内联函数。也就是说,代码中被调用的函数在每次函数调用时都
会被扩充,因而就可以节约实际函数调用的开销。
更为普遍的情况是代码的编写方式比较复杂。因为对于某些类型的输入,gcc能够
产生比其它输入效率更高的执行代码。从理论上讲,编译器可以优化具有相同功能
的两种对等的方法,并且得到相同的结果。因此,代码的编写方式是无关紧要的。
但在实际上,用一些方法编写所产生的代码要比用其它方法编写所产生的代码的执
行速度快得多。内核开发人员清楚如何才能产生更高效的执行代码的方法,而且这
种知识也不断在他们编写的代码中反映出来。
例如,考虑内核中经常使用的goto语句——为了提高速度,内核中经常大量使用这
种一般要避免使用的语句。在本书中所包含的不到40,000行代码中,一共有500多
条goto语句,大约是每80行一个。除汇编文件外,精确的统计数字是接近每72行一
个goto语句。公平的说,这是选择偏向的结果:比例如此高的原因之一是本书中涉
及的是内核源程序的核心,在这里速度比其它因素都需要优先考虑。整个内核的比
例大概是每260行一个goto语句。然而,这仍然是我不再使用Basic进行编程以来见
过的使用goto频率最高的地方。
代码必需受特定编译器限制的特性不仅与普通应用程序的开发有很大不同,而且也
不同于大多数内核的开发。大多数的开发人员使用C语言编写代码来保持较高的可
移植性,即使在编写操作系统时也是如此。这样做的优点是显而易见的,最为重要
的一点是一旦出现更好的编译器,程序员们可以随时进行更换。
内核对于gcc特性的完全依赖使得内核向新的编译器上的移植工作更加困难。最近
Linus对这一问题在有关内核的邮件列表上表明了自己的观点。“记住,编译器只
是一个工具。”这是对依赖于gcc特性的一个很好的基本思想的表述:编译器只是
为了完成工作。如果通过遵守标准还不能达到工作要求,那么就不是工作要求有问
题,而是对于标准的依赖有问题。
在大多数情况下,这种观点是不能够被人所接受的。通常情况下,为了保证和程序
语言标准的一致,开发人员可能需要牺牲某些特性、速度或者其它相关因素。其它
的选择可能会为后期开发造成很大的麻烦。
但是,在这种特定的情况下,Linus是正确的。Linux内核是一个特例,因为其执行
速度要比向其它编译器的可移植性远为重要。如果设计目标是编写一个可移植性好
而不要求快速运行的内核,或者是编写一个任何人都可以使用自己喜欢的编译器进
行编译的内核,那么结论就可能会有所不同了;而这些恰好不是Linux的设计目标
。实际上,gcc几乎可以为所有能够运行Linux的CPU生成代码,因此,对于gcc的依
赖并不是可移植性的严重障碍。
在第3章中我们将对内核设计目标进行详细说明。
内核代码习惯用语
内核代码中使用了一些显著的习惯用语,本节将介绍常用的几个。当你通读源程序
代码时,真正重要的问题是并不在这些习惯用语本身,而是这种类型的习惯用语的
确存在,而且是不断被使用和发展的。如果你需要编写内核代码,你应该注意到内
核中所使用的习惯用语,并把这些习惯用语应用到你的代码中。当通读本书(或者
代码)时,注意你还能找到多少习惯用语。
为了讨论这些习惯用语,我们首先需要对它们进行命名。为了便于讨论,笔者创造
了这些名字。而在实际中,大家不一定非要参考这些用语,它们只是对内核工作方
式的描述而已。
一个普通的习惯用语笔者称之为“资源获取”(resource acquisition idiom)。
在这个用语中,一个函数必须实现一系列资源的获取,包括内存、锁等等(这些资
源的类型未必相同)。只有成功地获取当前所需要的资源之后,才能处理后面的资
源请求。最后,该函数还必须释放所有已经获取的资源,而不必对没有获取的资源
进行考虑。
我采用“错误变量”这一用语(error variable idiom)来辅助说明资源获取用语
,它使用一个临时变量来记录函数的期望返回值。当然,相当多的函数都能实现这
个功能。但是错误变量的不同点在于它通常是用来处理由于速度的因素而变得非常
复杂的流程控制中的问题。错误变量有两个典型的值,0(表示成功)和负数(表
示有错)。
这两个用语结合使用,我们就可以十分自然地得到符合模式的代码如下:

Int f (void)
{
  int err;
  resource *r1, *r2;
  err = -ERR1   /*assume failure*/
  r1=acquire_ resource();
  if (!r1)       /*not aquired*/
     goto out      /*returns  -ERR1*/

  Got resource r1,try for r2.*/
  err = - ERR2;
  r2 = acquire_ resource2();
  if (!r2)       /*not aquired*/
     goto out1      /*returns –ERR2*/

  /*have both r1 and r2.*/
  err = 0;
  
  /* … use r1 and r2 … */

out2:
  release_resource(r2)

out2:
  release_resource(r2)

out:
    return err;
}

  
(注意变量err是使用错误变量的一个明确实例,同样,诸如out之类的标号则指明
了资源获取用语的使用。)
如果执行到标号out2,则都已经获取了r1和r2资源,而且也都需要进行释放。如果
执行到标号out1(不管是顺序执行还是使用goto语句进行跳转到),则r2资源是无
效的(也可能刚被释放),但是r1资源却是有效的,而且必需在此将其释放。同理
,如果标号out能被执行,则r1和r2资源都无效,err所返回的是错误或成功标志。

在这个简单的例子中,对于err的一些赋值是没有必要的。在实践中,实际代码必
须遵守这种模式。这样做的原因主要在于同一行中可能包含有多种测试,而这些测
试应该返回相同的错误代码,因此对错误变量统一赋值要比多次赋值更为简单。虽
然在这个例子中对于这种属性的必要性并不非常迫切,但是我还是倾向于保留这种
特点。有关的实际应用可以参考sys_shmctl(第21654行),在第9章中还将详细介
绍这个例子。
减少#if和#ifdef的使用
现在的Linux内核已经移植到不同的平台上,但是我们还必须解决移植过程中所出
现的问题。大部分支持各种不同平台的代码由于包含许多预处理代码现都已变得非
常不规范,例如:
#if defined(SOLARIS)
/* … do things the solaris way … */
#elif defined(HPUX)
/* … do things the HP-UX way … */
#elif defined(LINUX)
/* … do things the right way … */
#endif

这个例子试图实现操作系统的可移植性,虽然Linux关注的焦点很明显是实现代码
在各种CPU上的可移植性,但是二者的基本原理是一致的。对于这类问题来说,预
处理器是一种错误的解决方式。这些杂乱的问题使得代码晦涩难懂。更为糟糕的是
,增加对新平台的支持有可能要求重新遍历这些杂乱分布的低质量代码段(实际上
你很难能找到这类代码段的全部)。
与现有方式不同的是,Linux一般通过简单函数(或者是宏)调用来抽象出不同平
台间的差异。内核的移植可以通过实现适合于相应平台的函数(或宏)来实现。这
样不仅使代码的主体简单易懂,而且在移植的过程中还可以比较容易地自动检测出
你没有注意到的内容:如引用未声明函数时会出现链接错误。有时用预处理器来支
持不同的体系结构,但这种方式并不常用,而相对于代码风格的变化就更是微不足
道了。
顺便说一下,我们可以注意到这种解决方法和使用用户对象(或者C语言中充满函
数指针的struct结构)来代替离散的switch语句处理不同类型的方法十分相似。在
某些层次上,这些问题和解决方法是统一的。
可移植性的问题并不仅限于平台和CPU的移植,编译器也是一个重要的问题。此处
为了简化,假设Linux只使用gcc来编译。由于Linux只使用同一个编译器,所以就
没有必要使用#if块(或者#ifdef块)来选择不同的编译器。
内核代码主要使用#ifdef来区分需要编译或不需要编译的部分,从而对不同的结构
提供支持。例如,代码经常测试SMP宏是否定义过,从而决定是否支持SMP机。
代码样例
上一节仅仅是一些讨论,了解Linux代码风格最好的方法就是实际研究一下它的部
分代码。即使你不完全理解本节所讨论代码的细节也无关紧要,毕竟本节的主要目
的不是理解代码,一些读者可以只对本节进行浏览。本节的主要目的是让读者对
Linux代码进行初步了解,对今后的工作提供必要基础。而讨论将涉及部分广泛使
用到的内核代码。
printk
printk(25836行)是内核内部消息日志记录函数。在出现诸如内核检测到其数据
结构出现不一致的事件时,内核会使用printk把相关信息打印到系统控制台上。对
于printk的调用一般分为如下几类:
?   紧急事件(emergency)――例如,panic函数(25563行)多次使用了printk。
当内核检测到发生不可恢复的内部错误时就会调用panic函数,然后尽其所能的安
全关闭计算机。这个函数中调用printk以提示用户系统将要关闭。
?   调试――从3816行开始的#ifdef块使用printk来打印SMP逻辑单元(box)中每一
个处理器的相关配置信息,但是此过程只有在使用SMP_DEBUG标志编译代码的情况
下才能够被执行。
?   普通信息――例如,当机器启动时,内核必需估计系统速度以确保设备驱动程序
能够忙等待(busy-waiting)一个精确的极短周期。计算这种估计值的函数名为
calibrate_delay(19654行),它既在19661行使用printk声明马上开始计算,又
在19693行报告计算结果。另外,在第4章将详细的介绍calibrate_delay函数。
如果你已经浏览过这些参照代码,你可能已经注意到printk和printf的参数十分类
似:一个格式化字符串,后跟零个或者多个参数加入字符串中。格式化字符串可能
是以一组“<N>”开始,这里的N是从0到7的数字,包括0和7在内。数字区分了消息
的日志等级(log level),只有当日志等级高于当前控制台定义的日志等级(
console_loglevel,25650行)时,才会打印消息。root可以通过适当减小控制台
的日志等级来过滤不是很紧急的消息。如果内核在格式化字符串中检测不到日志等
级序列,那么就会一直打印消息。(实际上,日志等级序列并不一定要在格式化字
符串中出现,可以在格式化文本中查找到它的代码。)
从14946行开始的#define块说明了这些特殊序列,这些定义可以帮助调用者正确区
分对printk的调用。简单的说,我称日志等级0到4为“紧急事件”,从等级5到等
级6为“普通信息”,等级7自然就是我所说的“调试”。(这种分类方法并不意味
着其它更好的分类方法没有用处,而只是目前我们还不关心它而已。)
在上面讨论的基础上,我们研究一下代码本身。
printk
25836:参数fmt是printf类型的格式化字符串。如果你对“…”部分的内容不熟悉
,那就             需要参阅一本好的C语言参考书(在其索引中查找“变参函数
,variadic function”)。另外,在安装的GNU/Linux中的stdarg帮助里也包含了
一个有关变参函数的简明描述,在这儿只需要敲入“man stdarg”就可以看到。
    简单的说,“…”部分提示编译器fmt后面可能紧跟着数量不定的任何类型的参数
。由于这些参数在编译的时候还没有类型和名字,内核使用由三个宏va_start,
va_arg和va_end组成的特殊组以及一个特殊类型――va_list对它们进行处理。
25842:msg_level记录了当前消息的日志等级。它是静态的,这看起来可能会有些
奇怪――为什么下一次对printk的调用需要记录日志等级呢?问题的答案是只有打
印出新行(\n)或者赋给一个新的日志等级序列以后,当前消息才会结束。这样通
过在包含消息结束的新行里调用printk,就保证了在多个短期冲突的情况下,调用
者只打印唯一一个长消息。
25845:在SMP逻辑单元中,内核可能试图从不同的CPU向控制台同时打印信息。(
有时在单处理机(UP)逻辑单元中也会发生同样问题,但由于中断还未被覆盖掉,
所以问题也并不十分明显。)如果不进行任何协同的话,结果就将处于完全无法让
人了解的杂乱无章的状态,每个消息的各个部分都和其它消息的各个部分混杂交织
在一起。
相反,内核使用旋转锁(spin-lock)来控制对控制台的访问。旋转锁将在第10章
对它进行深入的介绍。
如果你对flags 在传送给spin_lock_irqsave之前为什么不对它初始化感到疑惑,
请不要担心:spin_lock_irqsave(对于不同的版本请分别参看12614行,12637行
,12716行,和12837行)是一个宏,而不是一个函数。该宏实际上是将值写入
flags中,而不是从flags中读出值。(在25895行中,存储在flags中的信息被
spin_lock_irqsave回读,请参看12616行,12639行,12728行和12841行)
25846:初始化变量args,该变量代表printk参数中的“…”部分。
25848:调用内核自身的vsprintf(为节省空间而省略)实现。该函数的功能与标
准vsprintf函数非常相似,向buf中写入格式化文本(25634行)并返回写入字符串
的长度(长度不包括最后一位终止字符0字节)。很快,你将可以看到为什么这种
机制会忽略buf的前三个字符。
(正如25847行的注释中所述)我们应该注意到在这里并没有采取严格的措施来保
证缓冲器不会过载。这里系统假定1024个字符长度的buf已经足够使用(参阅
25634行)。如果内核在这里能够使用vsnprintf函数的话,情况就会好许多。然而
,vsnprintf还有另外一个参数限制了它能够写入缓冲器的字符长度。
25849:计算buf中最近使用的元素,调用va_end终止对“…”参数的处理。
25851:开始格式化消息的循环。其中存在一个内部循环能够处理更多内容(这一
点随后就能看到),因此,每次内循环开始,都开始一个新的打印行。由于通常情
况下printk只用于打印单行,所以在每次调用中这种循环通常只执行一次。
25853:如果预先不知道消息的日志等级,printk会检查当前行是否以日志等级序
列开头。
25860:如果不是,buf中开始未使用的三个字符就能够起作用了。(第一次以后的
每次循环,都会覆盖部分消息文本,但是这样并不会引起问题,因为这里的文本只
是前面行中的一部分,它们已经被打印过,而且以后也不再需要了。)这样,就可
以将日志等级插入buf中。
25866:此处有如下属性:p指向日志等级序列(消息文本紧随其后),msg指向消
息文本——请注意25852行和25865行中对msg的赋值。
    由于已知p用来指示日志等级序列的开头――该日志等级序列可能是由函数自身所
创建的――日志等级可以从p中抽出并存到msg_level中。
25868:没有检测到新行,清空line_feed标志。
25869:这是前面谈到过的内循环,循环将运行到本行结束(也就是检测到新行标
志)或者缓冲器的末尾为止。
25870:除了将消息打印到控制台之外,printk还能够记录最近打印的长度为
LOG_BUF_LEN的字符组。(LOG_BUF_LEN为16K,请参看25632行。)如果在控制台打
开之前,内核就已经调用printk,则显然不能在控制台上正确打印消息,但是这些
消息将被尽可能的存储到log_buf中(25656行)。当控制台打开以后,缓存在
log_buf中的数据就可以转储并在控制台上打印出来,请参看25988行。
    log_buf是一个循环缓冲器,log_start和log_size变量(25657行和25646行)分
别记录当前缓冲器的开始位置和长度。本行中的按位与(AND)操作实际上是快速
求模(%)运算,它的正确性依赖于LOG_BUF_LEN的值是2的幂。
25872:保存变量跟踪记录循环日志的值。显然,日志大小会不断增长,直至达到
LOG_BUF_LEN的值为止。此后,log_size将保持不变,而插入新字符将导致
log_start的增长。
25878:请注意logged_chars(25658行)记录从机器启动之后printk写入的所有字
符的长度,它在每次循环中都会被更新,而不是在循环结束后才改变一次。基于同
样的道理,log_start和log_size的处理方式也是一样。这实际上是一种优化的时
机,但是我们将在结束对函数的介绍之后再对它详细讨论。
25879:消息被分为若干行,这当然要使用新行标志符来进行分割。一旦内核检测
到新行标志符,就写入一个完整行,从而内循环的执行也可以提前终止。
25884:在这里我们先不考虑内部循环是否会提前退出,从msg到p的字符序列是专
门提供给控制台使用的。(这种字符序列我称之为行,但是不要忘了,这里的行可
能并不意味着新行终止,因为buf也许还没有终止。)如果该行的日志等级高于系
统控制台定义的日志等级,而且当前又有控制台可供打印,那么就能够正确打印该
行。(记住,printk可能在所有控制台打开之前就已经被调用过了。)
    如果在该信息块中没有发现日志等级序列,并且在前面的printk调用中也没有对
msg_level赋值,那么本行中的msg_level就是-1。由于console_leglevel总不小于
1(除非root通过sysctl接口锁定),于是总是可以打印这些行。
25886:本行应该能够被打印。printk通过遍历打开的控制台驱动链表告知每一个
控制台驱动去打印当前行。(因为虽然设备驱动在本书的讨论范围之外,但是控制
台驱动代码则并不包含在内。)
25888:请注意这里消息文本的开头使用的是msg而不是p,这样就在没有日志等级
序列的情况下写入消息了。然而,日志等级序列已经被存储到log_buf缓冲器中了
。这样就可以使后来访问log_buf以获取信息日志等级的代码(请参看25998行)能
够正确执行,不会再产生显示混乱信息序列的现象。
25892:如果内层for循环发现一新行,那么buf中的剩余字符(如果有的话)将被
认为是新的消息,因此msg_level会被重置。但是无论怎样,外层循环都会持续到
buf清空为止。
25895:释放在25845行获取的控制台锁(console lock)。
25896:唤醒等待被写入控制台日志的所有进程。注意即使没有文本被实际写入任
何控制台,这个过程也仍然会发生。这样处理是正确的,因为无论是否要往控制台
中写入文本,等待进程实际上都是在等待从log_buf中读出信息。在25748行,进程
被转入休眠状态以等待log_buf的活动。在休眠、唤醒和等待队列中所使用的机制
将在下一节中进行讨论。
25897:返回日志中写入的字符长度。
    如果对于每个字符的处理工作都能减少一点,那么从25869行开始的for循环就能
执行得更快一点。当循环存在时,我们可以通过只在循环退出时将logged_chars更
新一次来稍微提高运行速度。然而我们还可以通过其它努力来提高速度。由于我们
可以预知消息的长度,因此log_size和log_start可以到最后再增长。让我们来实
验一下这样能否提高速度,下面是一段经过理想优化的代码:
    do {
static int wrapped = 0;
const int x = wrapped
  ? log_start
  : log_size;
const int lim = LOG_BUF_LEN – x;
int n = buf_end – p;
if ( n>= lim)
  n = lim;

memcpy(log_buf + x, p, n);
p += n;

if(log_size < LOG_BUF_LEN)
  log_size += n;
else {
  wrapped = 1;
  log_start += n;
  ;og_start &= LOG_BUF_LEN – 1;
}
} while (p < buf_end);
    请注意循环通常只需要执行一次,只有在log_buf末尾写入信息需要折行时才会多
次执行。因而log_size和log_buf只需要更新一次(或者当写入需要换行时是两次
)。
    这时速度的确提高了,但是有两个原因使我们并不能这样做。首先,内核可能有
自己特有的memcpy函数,我们必须确保对memcpy的调用不会再次进入对printk的调
用。(有一部分内核移植版定义了自己特有的速度较快的memcpy函数版本,因此所
有的移植都要在这一点上保持一致。)如果memecpy调用printk来报告失败,那么
就有可能触发无限循环。
    然而在这一点上也并不是真的无药可救。使用这种解决方案的最大问题在于该内
核循环的形式中也要留意新行标志符,因此使用memcpy将整个消息拷贝到log_buf
中是不正确的:如果此处存在新行,我们将无法对其进行处理。
    我们可以试验一个一箭双雕的办法。下面这种替代的尝试虽然可能比前面那种初
步解决方法速度要慢,但是它保持了内核版本的语意:
/* in declaration section:*/
int n;
char *start;
static char *log = log_buf;
/*……*/

for (start = p;p < buf_end;p++) {
  *log++ = *p;
  if  (log >= (log_buf + LOG_BUF_LEN))
log = log_buf ;  /* warp*/

  if (*p == ‘/n’) {
line_feed = 1;
break;
  }
}

/* p - start is number of chars copied. */
n = p – start;
logged_chars += n ;
/*
*exercise for the reader:
*also use n to update log size and log_ start.
*(it’s not as simple as may look.)
*/

    (请注意gcc的优化器十分灵敏,它足以能检测到循环内部的表达式
log_buf+LOG_BUF_LEN并没有改变,因此在上面的循环中试图手工加速计算是没有
任何效果的。)
    不幸的是,这种方法并不能比现在内核版本在速度上快许多,而且那样会使得代
码晦涩难懂(如果你编写过更新log_size和log_start的代码,你就能清楚地了解
这一点)。你可以自己决定这种折衷是否值得。然而无论怎样,我们学到了一些东
西,这是通常的成果:不管成功与否,改进内核代码都可以加深你对内核工作原理
的理解。
等待队列
前一节我们曾简要的提到进程(也就是正在运行的程序)可以转入休眠状态以等待
某个特定事件,当该事件发生时这些进程能够被再次唤醒。内核实现这一功能的技
术要点是把等待队列(wait queue)和每一个事件联系起来。需要等待事件的进程
在转入休眠状态后插入到队列中。当事件发生之后,内核遍历相应队列,唤醒休眠
的任务让它投入运行状态。任务负责将自己从等待队列中清除。
等待队列的功能强大得令人吃惊,它们被广泛应用于整个内核中。更重要的是,实
现等待队列的代码量并不大。
wait_queue结构
18662:简单的数据结构就是等待队列节点,它包含两个元素:
?   tast――指向struct task_struct结构的指针,它代表一个进程。从16325行开
始的struct task_struct结构将在第7章中进行介绍。
?   next――指向队列中下一节点的指针。因而,等待队列实际上是一个单链表。
通常,我们用指向等待队列队首的指针来表示等待队列。作为一个例子,请参看
printk使用的等待队列log_wait(25647行)。
wait_event
16840:通过使用这个宏,内核代码能够使当前执行的进程在等待队列wq中等待直
至给定condition(可能是任何的表达式)得到满足。
16842:如果条件已经为真,当前进程显然也就无需等待了。
16844:否则,进程必须等待给定条件转变为真。这可以通过调用__wait_event来
实现(16824行),我们将在下一节介绍它。由于 __wait_event已经同
wait_event分离,已知条件为假的部分内核代码可以直接调用__wait_queue,而不
用通过宏来进行冗余的(特别是在这些情况下)测试,实际上也没有代码会真正这
样处理。更为重要的是,如果条件已经为真,wait_event会跳过将进程插入等待队
列的代码。
    注意wait_event的主体是用一个比较特殊的结构封闭起来的:
    do {
      /* … */
    } while (0)      
    使我惊奇的是,这个小技巧并没有得到应有的重视。这里的主要思路是使被封闭
的代码能够像一个单句一样使用。考虑下面这个宏,该宏的目的是如果p是一个非
空指针,则调用free:
    #define FREE1(p)  if  (p)  free (p)
除非你在如下所述的情况下使用FREE1,否则所有调用都是正确有效的:
    if  (expression)
        FREE1(p)
    else
        printf(“expression was false.\n”) ;  

FREE1经扩展以后,else就和错误的if――FREE1的if――联系在一起。
    我曾经发现有些程序员通过如下途径解决这种问题:
    #define FREE2(p)  if (p) { free(p); }
    #define FREE3(p)  { if (p) { free(p); } }
这两种方法都不尽人意,程序员在调用宏以后自然而然使用的分号会把扩展信息弄
乱。以FREE2为例,在宏展开之后,为了使编译器能更准确的识别,我们还需要进
行一定的缩进调节,最终代码如下所示:

if (expression)
  if (p) { free(p);}
else
  printf(“expression was false./n”);

这样就会引起语法错误――else和任何一个if都不匹配。FREE3从本质上讲也存在
同样的问题。而且在研究问题产生原因的同时,你也能够明白为什么宏体里是否包
含if是无关紧要的。不管宏体内部内容如何,只要你使用一组括号来指定宏体,你
就会碰到相同的问题。
    这里是我们能够引入do/while(0)技巧的地方。现在我们可以编写FREE4,它能够
克服前面所出现的所有问题。

#define FREE4(P)  \
do {             \
if (p)        \
  free(p);    \
while (0)

将FREE4和其它宏一样插入相同代码之后,宏展开后其代码如下所示(为清晰起见
,我们再次调整了缩进格式):

if (expression)
  do {
if (p)        
  free(p);   
  } while (0);   /* “;” following macro.*/

这段代码当然可以正确执行。编译器能够优化这个伪循环,舍弃循环控制,因此执
行代码并没有速度的损失,我们也从而得到了能够实现理想功能的宏。
    虽然这是一个可以接受的解决方案,但是我们不能不提到的是编写函数要比编写
宏好得多。不过如果你不能提供函数调用所需的开销,那么就需要使用内联函数。
这种情况虽然在内核中经常出现,但是在其它地方就要少得多。(无可否认,当使
用C++,gcc或者任何实现了将要出现的修正版ISO标准C的编译器时,这种方案只是
一种选择,就是最后为C增加内联函数。)
__wait_event
16842:__wait_event使当前进程在等待队列wq中等待直至condition为真。
16829:通过调用add_wait_queue(16791行),局部变量__wait可以被链接到队列
上。注意__wait是在堆栈中而不是在内核堆中分配空间,这是内核中常用的一种技
巧。在宏运行结束之前,__wait就已经被从等待队列中移走了,因此等待队列中指
向它的指针总是有效的。
16830:重复分配CPU给另一个进程直至条件满足,这一点将在下面几节中讨论。
16831:进程被置为TASK_UNINTERRUPTIBLE状态(16190行)。这意味着进程处于休
眠状态,不应被唤醒,即使是信号量也不能打断该进程的休眠。信号量在第6章中
介绍,而进程状态则在第7章中介绍。
16832:如果条件已经满足,则可以退出循环。
    请注意如果在第一次循环时条件就已经满足,那么前面一行的赋值就浪费了(因
为在循环结束之后进程状态会立刻被再次赋值)。__wait_event假定宏开始执行时
条件还没有得到满足。而且,这种对进程状态变量state的延迟赋值也并没有什么
害处。在某些特殊情况下,这种方法还十分有益。例如当__wait_event开始执行时
条件为假,但是在执行到16832行时就为真了。这种变化只有在为有关进程状态的
代码计算condition变量值时才会出现问题。但是在代码中这种情况我一处也没有
发现。
16834:调用schedule(26686行,在第7章中讨论)将CPU转移给另一个进程。直到
进程再次获得CPU时,对schedule的调用才会返回。这种情况只有当等待队列中的
进程被唤醒时才会发生。
16836:进程已经退出了,因此条件必定已经得到了满足。进程重置TASK_RUNNING
的状态(16188行),使其适合CPU运行。
16837:通过调用remove_wait_queue(16814行)将进程从等待队列中移去。
wait_event_interruptible和__wait_event_interruptible(分别参见16868行和
16847)基本上与wait_event和__wait_event相同,但不同的是它们允许休眠的进
程可以被信号量中断。如前所述,信号量将在第6章中介绍。
    请注意wait_event是被如下结构所包含的。
    ({
      /* … */
    })
    和do/while(0)技巧一样,这样可以使被封闭起来的代码能够像一个单元一样运行
。这样的封闭代码就是一个独立的表达式,而不是一个独立的语句。也就是说,它
可以求值以供其它更复杂的表达式使用。发生这种情况的原因主要在于一些不可移
植的gcc特有代码的存在。通过使用这类技巧,一个程序块中的最后一个表达式的
值将定义为整个程序块的最终值。当在表达式中使用wait_event_interruptible时
,执行宏体后赋__ret的值为宏体的值(参看16873行)。对于有Lisp背景知识的程
序员来说,这是个很常见的概念。但是如果你仅仅了解一点C和其它一些相关的过
程性程序设计语言,那么你可能就会觉得比较奇怪。
__wake_up
26829:该函数用来唤醒等待队列中正在休眠的进程。它由wake_up和
wake_up_interruptible调用(请分别参看16612行和16614行)。这些宏提供mode
参数,只有状态满足mode所包含的状态之一的进程才可能被唤醒。
26833:正如将在第10章中详细讨论的那样,锁(lock)是用来限制对资源的访问
,这在SMP逻辑单元中尤其重要,因为在这种情况下当一个CPU在修改某数据结构时
,另一个CPU可能正在从该数据结构中读取数据,或者也有可能两个CPU同时对同一
个数据结构进行修改,等等。在这种情况下,受保护的资源显然是等待队列。非常
有趣的是所有的等待队列都使用同一个锁来保护。虽然这种方法要比为每一个等待
队列定义一个新锁简单得多,但是这就意味着SMP逻辑单元可能经常会发现自己正
在等待一个实际上并不必须的锁。
26838:本段代码遍历非空队列,为队列中正确状态的每一个进程调用
wake_up_process(26356行)。如前所述,进程(队列节点)在此可能并没有从队
列中移走。这在很大程度上是由于即使队列中的进程正在被唤醒,它仍然可能希望
继续存在于等待队列中,这一点正如我们在__wait_event中发现的问题一样。
内核模块(Kernel Modules)
整个内核并不需要同时装入内存。应该确认,为保证系统能够正常运行,一些特定
的内核必须总是驻留在内存中,例如,进程调度代码就必须常驻内存。但是内核其
它部分,例如大部分的设备驱动就应该仅在内核需要的时候才装载,而在其它情况
下则无需占用内存。
举例来说,只有在内核真正和CD-ROM通讯时才需要使用完成内核与CD-ROM通讯的设
备驱动程序,因此内核可以被设置为在和设备通讯之前才装载相应代码。内核完成
和设备的通讯之后可以将这部分代码丢弃。也就是说,一旦代码不再需要,就可以
从内存中移走。系统运行过程中可以增减的这部分内核称为内核模块。
内核模块的优点是可以简化内核自身的开发。假设你购买了一个新的高速CD-ROM驱
动器,但是现有的CD-ROM驱动程序并不支持该设备。你自然就希望增加对这种高速
模式的支持以提高系统光驱设备的性能。如果作为内核模块来编译驱动程序,你的
工作将会方便得多:编译驱动程序,加载到内核,测试,卸载驱动程序,修改驱动
程序,再次加载驱动程序到内核,测试,如此周而复始。如果你的驱动程序是直接
编辑在内核中的,那么你就必须重新编译整个内核并且在每次修改驱动程序之后重
新启动机器。这样慢得很多。
自然,你也必须留意内核模块。对于指明其它内核模块在磁盘上的驻留位置的那些
模块,一定不能从内存中卸载,否则,内核将只能通过访问磁盘来装载处理磁盘访
问的内核模块,这是不可能实现的。这也是我们要选择把部分内核作为模块编译还
是直接编译进内核使其常驻内存的又一个原因。你知道自己系统的设置方式,因而
也就可以自己选择正确使用的方式。(如果为了确保安全,你可以简单的忽略内核
模块系统的优点,而把所有的内容都编译到内核里面。)
内核模块会带来一些速度上的损失,这是因为一些必需的代码现在并不在RAM中,
必需要从磁盘读入。但是整个系统的性能通常会有所提高,这主要是因为通过丢弃
暂时不使用的模块可以释放出额外的RAM供应用程序使用。如果这部分内存被内核
所占用,应用程序将只能更加频繁地进行磁盘交换(swap),而这种磁盘交换会显
著的降低应用程序的性能。(磁盘交换将在第8章中讨论。)
内核模块还会带来因复杂度的增加所造成的开销,这是因为在系统运行的过程中移
进移出部分内核需要额外的代码。然而,正如你将在本节中看到的,复杂度的开销
是可以管理的。通过使用外部程序来代理一些必需的工作还可以更进一步降低复杂
度的开销。(更为确切的说法是,这样做不是减少了复杂度的开销,而是把复杂度
的开销重新分配了一下。)这是对内核模块原理的一个小小的扩展:即使是内核的
支持模块对于内核来说也只是外部的,部分可用的,只有在需要的时候才被装入内
存。
通常用于这种目的程序称为modprobe。有关的modprobe代码超出了本书的范围,但
是在Linux的每个发行版本中都有包含有它。本节的剩余部分将讨论同modprobe协
同工作以装载内核模块的内核代码。
request_module
24432:作为函数说明之前的注释,request_module是一个函数。内核的其它模块
在需要装载其它内核模块的时候,都必须调用这个函数。就像内核处理其它工作一
样,这种调用也是为当前运行的进程进行的。从进程的角度来看,这种调用的请求
通常是隐含的――正在执行进程其它请求的内核可能会发现必须调入一个模块才能
够完成该请求。例如,请参看10070行,这里是一些将在第7章中讨论的代码。
24446:以内核中的一个独立进程的形式执行exec_modprobe函数(24384行,马上
就会讨论到)。这并不能只通过函数的简单调用实现,因为exec_modprobe要继续
调用exec来执行一个程序。因此,对函数exec_modprobe的简单调用将永远不会有
返回。
    这和使用fork以准备exec调用十分类似,你可以认为kernel_thread对内核来说就
是较低版本的fork,虽然两者有很大不同。fork是从指定函数开始执行新的进程,
而不是从调用者的当前位置开始运行。正如fork一样,kernel_thread返回的值是
新进程的进程号。
24448:和fork一样,从kernel_thread返回的负值表示内部错误。
24455:正如函数中论述的一样,大部分的信号量将因当前进程而被暂时阻塞。
24462:等待exec_modprobe执行完毕,同时指出所需要的模块是已经成功装入内存
还是装载失败了。
24465:结束运行,恢复信号量。如果exec_modpro返回错误代码,则打印错误消息

exec_modprobe
24384:exec_modprobe运行为内核增加内核模块的程序。这里的模块名是一个
void*的指针,而不是char*的指针。原因简单说来就是kernel_thread 产生的函数
通常都使用void*指针参数。
24386:设置modprobe的参数列表和环境。Modprobe_path(24363行)用来定位
modprobe程序的位置。它可以通过内核的sysctl特性来修改,这一点将在第11章中
介绍(请参看30388行)。这意味着root可以动态选择不同于/sbin/modprobe的程
序来运行,以适应当modprobe被安装到其它地方或者使用修改过的modprobe替换掉
了原有的modprobe之类的情况。
24400:(正如代码中描述的一样)出于安全性考虑,丢弃所有挂起的信号量和信
号量句柄(handlers)。这里最重要的部分是对flush_signal_handlers的调用(
28041行),它使用内核默认的信号量句柄代替所有用户定义的信号量句柄。如果
在此时有信号量被传送到内核,它将获得默认响应——通常是忽略信号量或杀死进
程。但是不管怎样都不会引起安全风险。由于该函数从触发它的进程中分离出来(
如前所述),所以不管原始进程在此处是否改变其原来分配的信号量句柄都不会产
生任何影响。
24405:关闭调用进程打开的所有文件。最重要的是,这意味着modprobe程序不再
从调用进程中继承标准输入输出和标准错误。这很有可能会引起安全漏洞。(这可
能在替代modprobe的程序中引起的问题,但是modprobe本身实际上并不关心这个差
异。)
24413:modprobe程序作为root运行,它拥有root所拥有的所有权限。和整个内核
中其它地方一样,请注意root使用用户ID号0的假定在这里已经被写入程序。用户
ID号和权能系统(capability system)(在接下来的几行中会用到)将在第7章中
介绍。
24421:试图执行modprobe程序。如果尝试失败,内核将使用printk打印错误消息
并返回错误代码。这里是可能产生printk的缓冲器过载的地点之一。module_name
的长度并没有明确限制,就我们对该调用的看法而言,它可能长达一百万个字符。
为防止printk缓冲器过载,你必需遍历所有对于该函数的调用(实际上是对
request_module的调用)以保证每个调用者使用足够短的不会为printk造成麻烦的
模块名。
24427:当execve成功执行时,它不会返回任何结果,因此本处是不可能执行到的
。但是编译器却并不知道这一点,因此此处使用了return语句以保证gcc不出错。

    对于内核的进一步讨论将超出本章的既定范围,因此在这个问题上我们到此为止
。然而本书中也包括了其它必需的内核代码。在读完第4章和第5章之后,也许你会
希望再次仔细研读一下这部分内容。有关这个问题的两个文件是
include/linux/module.h(从15529行开始)和/kernel/module.c(从24476行开始
)。和sys_create_module(24586行),sys_init_module(24637行),
sys_delete_module(24860行)和sys_query_module(25148行)四个函数需要特
别注意一样,struct module(15581行)也要特别引起注意。这些函数实现了
modprobe以及insmod,lsmod和rmmod所使用的系统调用以完成模块的装载、定位和
卸载。
    内核触发直接回调内核程序的现象看起来很令人奇怪。但是,实际上进行的工作
不止于此。例如,modprobe必须实际访问磁盘以搜寻要装载的模块。而且更为重要
的一点是这种方法赋予root对内核模块系统更多的控制能力。这主要是因为root也
可以运行modprobe以及相关程序。因此,root既可以手工装载、查询、卸载模块,
也可以由内核自动完成。
配置与编译内核
你可能仅仅研读、欣赏而并不修改Linux内核源代码。但是,更普遍的情况是,用
户有强烈的愿望去改进内核代码并完成相应的测试,这样我们就需要知道如何重建
内核。本节就是要告诉你如何实现这一点,而最终则归结于如何把你所做的修改发
行给别人,以使得每个人都能从你的工作中受益。
配置内核
编译内核的第一步就是配置内核,这是增加或者减少对内核特性的支持以及修改内
核的一些特性发挥作用的方式的必要步骤。例如,你可以要求内核为自己的声卡指
定一个不同的DMA通道。如果内核配置和你的需要相同,那么你可以直接跳过本节
,否则请继续阅读以下内容。
为了完成内核的配置,请先切换到root用户,然后转入如下内核源程序目录:
cd /usr/src/linux
接着敲入如下命令组:
make config
make menuconfig
make xconfig
这三条命令都可以让你来配置内核,但它们发挥作用的方式各不相同:
?   make config――三种方法中最简单也是最枯燥的一种。但是最基本的一点是,
它可以适应任何情况。这种方法通过为每一个内核支持的特性向用户提问的方式来
决定在内核中需要包含哪些特性。对于大多数问题,你只要回答y(yes,把该特性
编译进内核中),m(作为模块编译)或者n(no,根本不对该特性提供支持)。在
决定之前用户应该考虑清楚,因为这个过程是不可逆的。如果你在该过程中犯了错
误,就只能按Ctrl+C退出。你也可以敲入?以获取帮助。图2.1显示了这种方法正在
X终端上运行的情况。

图2.1 运行中的make config

幸运的是,这种方法还有一些智能。例如,如果你对SCSI支持回答no,那么系统就
不会再询问你有关SCSI的细节问题了。而且你可以只按回车键以接受缺省的选择,
也就是当前的设置(因此,如果当前内核将对于SCSI的支持编译进了内核,在这个
问题上按回车键就意味着继续把对SCSI的支持编译进内核中)。即使是这样,大部
分用户还是宁愿使用另外的两种方法。
?   make menuconfig―一种基于终端的配置机制,用户拥有通过移动光标来进行浏
览等功能。图2.2显示了在X终端上运行的make menuconfig。虽然在控制台上显示
的是彩色,但是在终端上的显示仍然相当单调。使用menuconfig必须要有相应的
ncurses类库。

图2.2 运行中的make menuconfig

?   make xconfig――这是我最喜欢的一种配置方式。只有你能够在X server上用
root用户身份运行X应用程序时,这种配置方式才可以使用(有些偏执的用户就不
愿意使用这种方式)。你还必须拥有Tcl窗口系统(Tcl windowing system),这
实际上还意味着你必须拥有Tcl,Tk以及一个正在运行的X安装程序。作为补偿,用
户获得的是更漂亮的,基于X系统的以及和menuconfig功能相同的配置方法。图2.
3显示这种方法运行过程中打开“可装载模块支持(Loadable module support)”
子窗口的情况。


图2.3 运行中的make xconfig

如上所述,这三种方法都实现了相同的功能:它们都生成在构建内核时使用的.
config文件。而唯一的区别是在于创建这个文件时的难易程度不同。
构建内核
构建内核要做的工作要比配置内核所做的工作少得多。虽然有几种方式都能实现这
一功能,但是选择哪一种依赖于你希望怎样对系统进行设置。长期以来,我已经形
成了如下的习惯。虽然这种习惯比我所必须要做的略微多一些,但是它包含了所有
基本的问题。首先,如果你还不在内核源程序目录中,请先再次转入这一目录:
cd  /usr/src/linux
现在,切换到root用户,使用下面显示的命令生成内核。现在在shell中敲入下面
的命令,注意make命令因为空间关系分成了两行,但实际上这在shell输入时是一
个只有一行的命令:
    make dep clean zlilo boot
        modules modules_install
当给出了如上多个目标时,除非前面所有的目标都成功了,否则make能够知道没有
必要继续尝试下面的目标。因此,如果make能够运行结束,成功退出,那么这就意
味着所有的目标都正确构建了。现在你可以重新启动机器以运行新的内核。
备份的重要性
当修改(fooling)内核时,你必须准备一个能够启动的备用内核。实现该目的的
一种方式是通过配置Linux加载程序(LILO)以允许用户选择启动的内核映象,其
中之一是从没有修改过的内核的备份(我总是这样做的)。
如果你比较有耐心,那么你就可以使用zdisk目标而不使用zlilo目标;它可以把能
够启动的内核映象写入软盘中。这样你就可以通过在启动时插入软盘的方式启动你
的测试内核;如果没有插入软盘,则启动正常的内核。
但是请注意:内核模块并没有被装载到软盘中,它们实际上是装在硬盘中的(除非
你愿意承担更多的麻烦)。因此,如果你弄乱了内核模块,即使是zdisk目标也救
不了你。实际上,上面提到的这两种方法都存在这个问题。虽然有比较好的解决方
法可用,但是最简单的方法(也就是我所使用的方法)是把备份内核作为严格独立
的内核来编译,而不使用可装载模块的支持。通过这种方法,即使我弄乱了内核而
不得不使用备份启动系统,那么不管问题是实验性内核不正确还是内核模块的原因
都无关紧要。不管怎样,在备份的内核中已经有我需要的所有东西了。
由于用户所作的修改可能导致系统的崩溃,如损坏磁盘上的数据等等,并不仅仅只
是打乱设备驱动程序或文件系统,在测试新内核之前,备份系统的最新数据也是一
个英明的决策。(虽然设备驱动程序的开发不是本书的主题,但是必需指出的是,
设备驱动程序的缺陷可能会引起系统的物理损坏。例如显示器是不能备份的,而且
因价格昂贵而不易替换。)作为一个潜在的内核黑客,你的最佳投资(当然是读过
本书以后)是一个磁带驱动器和充足的磁带。
发行你的改进
下面是有关发行你所做修改的一些基本规则:
?   检查最新发行版本,确保你所处理的不是已经解决了的问题
?   遵守Linux 内核代码编写的风格。简要的说就是8字符缩进以及K&R括号风格(
if,else,for,while,switch或者do后面同一行中紧跟着开括号)。在内核源程
序目录下面的文档编写和代码风格文件给出了完整的规则,不过我们已经介绍了其
中的关键部分。注意本书中包含的源程序代码为节省空间而进行了大量的重新编辑
,在该过程中我可能打破了其中的一些规则。
?   独立发行相对无关的修改。这样,只想使用你所做的某部分修改的人就可以十分
方便地获得想要的东西,而不用一次检验所有的修改内容。
?   让使用你所做修改的用户清楚他们可以从你的修改中获取什么。同样地,你也应
该给出这些问题的可信度。你是15分钟之前才匆匆完成你的修改,甚至还没有时间
对它们进行编译,还是已经在你和你的朋友的系统中从去年3月开始就长期稳定的
运行过这个修改?
假设现在你已经准备好发行自己的修改版本了,那么要做的第一步是建立一个说明
你所做的修改的文件。你可以使用diff程序自动创建这个文件。结果或者被称为
diffs,也或者在Linux中更普遍的被称为补丁(patch)。
发布的过程十分简单。假设原来没有修改过的源程序代码在linux-2.2.5目录下,
而你修改过的源程序代码在linux-my目录下,那么只要进行如下的简单工作就可以
了(只有在链接不存在的情况下才需要执行ln):
ln –s linux-my linux
make –C linux-2.2.5 distclean
make –C linux distclean
diff –urN linux-2.2.5 linux >my.patch
现在,输出文件my.patch包含了其它用户应用这个修改程序时所必须的一切内容。
(警告:如上所述,两个源程序间的所有差别都会包含在这个补丁文件中。Diff不
能区分修改部分之间的关系,所以就把它们都罗列了出来。)如果补丁文件相对较
小,你可以使用邮件直接发往内核邮件列表。如果补丁很大,那么就需要通过FTP
或者Web站点发布。这时发给邮件列表的信件中就只需要包含一个URL。
Linux内核邮件列表的常见问题解答(FAQ)文件位于http://www.ececs.uc.
edu/~rreilova/linux/lkmlfaq.html。该FAQ中包含了邮件列表的订阅,邮件发布
以及阅读邮件列表的注意事项等等。
顺便提一下,如果你想随时了解内核更新开发的进程,我向你强烈推荐下面这个具
有很高价值的内核交流站点Kernel Traffic:http://www.kt.opensrc.org。
回复

使用道具 举报

 楼主| 发表于 2003-10-27 09:31:39 | 显示全部楼层
Linux内核注释—第3章 内核体系结构概述

本章从较高层次上对内核进行说明。从顺序上来说,本章首先介绍内核设计目标,
接下来介绍内核体系结构,最后介绍内核源程序目录结构。
内核设计目标
Linux 的内核展现出了几个相互关联的设计目标,它们依次是:清晰性(clarity
),兼容性(compatibility),可移植性(portability),健壮性(
robustness),安全性(security)和速度(speed)。这些目标有时是互补的,
有时则是矛盾的。但是它们被尽可能的保持在相互一致的状态,内核设计和实现的
特性通常都要回归到这些问题上来。本节接下来的部分将分别讨论这些设计目标,
同时还将对它们之间的取舍与平衡进行简要的说明。
清晰性
稍微过于简化的说,内核目标是在保证速度和健壮性的前提下尽量清晰。这和现在
的大多数应用程序的开发有所区别,后者的目标通常是在保证清晰性和健壮性的基
础上尽量提高速度。因而在内核内部,速度和清晰性经常是一对矛盾。
在某种程度上,清晰性是健壮性的必要补充:一个很容易理解的实现方法比较容易
证明是正确的;或者即使不正确,也能比较容易的找出其问题所在。从而这两个目
标很少会发生冲突。
但是清晰性和速度通常却是一对矛盾。经过仔细手工优化的算法通常都使用了编译
器生成代码的类似技术,很少可能是最清晰的解决方案。当内核中清晰性和速度要
求不一致时,通常都是以牺牲清晰性来保证速度的。即便如此,程序员仍然清楚的
知道清晰性的重要性,而且他们也做了大量完美的工作以使用最清晰的方法保证速
度。
兼容性
正如第1章中所述,Linux最初的编写目的是为了实现一个完整的、与Unix兼容的操
作系统内核。随着开发过程的展开,它也开始以符合POSIX标准为目标。就内核而
言,兼容Unix(至少是同某一现代的Unix实现相兼容)和符合POSIX标准并没有什
么区别,因此我们也不会在这个问题上详细追究。
内核提供了另外一种类型的兼容性。基于Linux 的系统能够提供可选择的对Java.
class文件的本地运行支持。(据说Linux是第一个提供这种支持的操作系统。)尽
管实际负责Java程序解释执行的是另外一个Java虚拟机进程,该虚拟机并没有内置
到内核中。但是内核提供的这种机制可以使得这种支持对用户是透明的。通过内核
本身提供的程度不同的支持(这并不代表大部分工作像Java的解决方式一样能够通
过外部进程实现),对其它可执行文件格式的支持也能够以同样的方式插入内核中
。这方面的内容将在第7章中详细介绍。
另外需要说明的是,GNU/Linux系统作为一个整体通过DOSEMU仿真机器提供了对
DOS可执行程序的支持,而且也通过WINE设计提供了对Windows可执行程序的部分支
持。系统还以同样的方式通过SAMBA提供了对Windows兼容文件和打印服务的支持。
但是这些都不是同内核密切相关的问题,因此在本书中我们不再对它们进行讨论。

兼容性的另外一个方面是兼容异种文件系统,本章中稍后会有更为详细的介绍,但
是大部分内容已经超出了本书的范围。Linux能够支持很多文件系统,例如ext2(
“本地”文件系统),ISO-9660(CD-ROM使用的文件系统),MS-DOS,网络文件系
统(NFS)等许多其它文件系统。如果你有使用其它操作系统格式的磁盘或者一个
网络磁盘服务器,那么Linux将能够和这些不同的文件系统进行交互。
兼容性的另外一个问题是网络,这在当今Internet流行的时代尤为重要。作为
Unix的一个变种,Linux自然从很早就开始提供对TCP/IP的支持。内核还支持其它
许多网络协议,它们包括AppleTalk协议的代码,这使得Linux单元(box)可以和
Macintosh机自由通讯;Novell的网络协议,也就是网络报文交换(IPX),分组报
文交换(SPX),和NetWare核心协议(NCP);IP协议的新版本IPv6;以及其它一
些不太出名的协议。
兼容性考虑的最后一个方面是硬件兼容性。似乎每个不常见的显卡,市场份额小的
网卡,非标准的CD-ROM接口和专用磁带设备都有Linux的驱动程序。(只要它不是
专为特定操作系统设计的专用硬件。)而且只要越来越多的厂商也逐渐认识到
Linux的优势,并能够为更容易地实现向Linux上移植而开放相应的源程序代码,
Linux对硬件支持会越来越好。
这些兼容性必须通过一个重要的子目标:模块度(Modularity)来实现。在可能的
情况下,内核只定义子系统的抽象接口,这种抽象接口可以通过任何方法来实现。
例如,内核对于新文件系统的支持将简化为对虚拟文件系统(VFS)接口的代码实
现。第7章中介绍的是另外一个例子,内核对二进制句柄的抽象支持是实现对诸如
Java之类的新可执行格式的支持的方法。增加新的可执行格式的支持将转变为对相
应的二进制句柄接口的实现。
可移植性
与硬件兼容性相关的设计目标是可移植性(portability),也就是在不同硬件平
台上运行Linux的能力。系统最初是为运行在标准IBM兼容机上的Intel x86 CPU而
设计的,当时根本没有考虑到可移植性的问题。但是情况从那以后已经发生了很大
的变化。现在正式的内核移植包括向基于Alpha,ARM,Motorola 69x0,MIPS,
PowerPC,SPARC以及SPARC-64 CPU系统的移植。因而,Linux可以在Amigas,旧版
或新版的Macintosh,Sun和SGI工作站以及NeXT机等机器上运行。而且这些还只是
标准内核发行版本的移植范围。从老的DEC VAX到3Com掌上系列个人数字助理(例
如Palm III)的非正式的移植工作也在不断进行中。成功的非正式移植版本后来通
常都会变成正式的移植版本,因此这些非正式的移植版本很多最终都会出现在主开
发树中。
广泛平台支持之所以能够成功的部分原因在于内核把源程序代码清晰地划分为体系
结构无关部分和体系结构相关部分。在本章的后续部分将对这个问题进行更深入的
讨论。
健壮性和安全性
Linux必须健壮、稳定。系统自身应该没有任何缺陷,并它还应该可以保护进程(
用户)以防止互相干扰,这就像把整个系统从其它系统中隔离开来加以保护一样。
后一种考虑很大程度上是受信任的用户空间应用程序领域的问题,但是内核至少也
应该提供支撑安全体系的原语(primitive)。健壮性和安全性比任何别的目标都
要重要,包括速度。(系统崩溃的速度很快又有什么好处呢?)
保证Linux健壮性和安全性的唯一一个最重要的因素是其开放的开发过程,它可以
被看作是一种广泛而严格的检查。内核中的每一行代码、每一个改变都会很快由世
界上数不清的程序员检验。还有一些程序员专门负责寻找和报告潜在的缺陷――他
们这样做完全是出于自己的个人爱好,因为他们也希望自己的Linux系统能够健壮
安全。以前检查中所没有发现的缺陷可以通过这类人的努力来定位、修复,而这种
修复又合并进主开发树以使所有的人都能够受益。安全警告和缺陷报告通常在几天
甚至几个小时内就能够得到处理和修复。
Linux可能并不一定是现有的最安全的操作系统(很多人认为这项桂冠应该属于
OpenBSD,它是一个以安全性为主要目标的Unix变种),但是它是一个有力的竞争
者。而且Linux健壮性远没有发展到尽头。
速度
这个术语经常自己就可以说明问题。速度几乎是最重要的衡量标准,虽然其等级比
健壮性、安全性和(在有些时候)兼容性的等级要低。然而它却是代码最直观的几
个方面之一。Linux内核代码经过了彻底的优化,而最经常使用的部分――例如调
度――则是优化工作的重点。几乎在任何时候都有一些不可思议的代码,这是由于
这种方式的执行速度比较快。(这并不总是很明显,但是你经常不得不通过自己的
试验来对这种优化代码进行确认。)虽然有时一些更直接的实现方法速度也很快,
但是我所见过的这种情况屈指可数。
在某些情况下,本书推荐用可读性更好的代码来替代那些以速度的名义而被故意扭
曲了的代码。虽然速度是一个设计目标,但我基本上只在以下两种情况时才会这样
做:a) 在所考虑的问题中,速度明显不是关键问题 b) 没有其它的办法。
内核体系结构初始
图3.1是一种类Unix操作系统的相当标准的视图,实际上,更细致的来说,该图能
够说明所有期望具有平台无关特性的操作系统。它着重强调了内核的下面两个特性

?   内核将应用程序和硬件分离开来。
?   部分内核是体系结构和硬件特有的,而部分内核则是可移植的。


图3.1 内核体系结构基本结构图

第一点我们在前面章节中已经讨论清楚了,在这里没有必要重复说明。第二点,也
就是与体系结构无关和与体系结构相关代码的内容对于我们的讨论比较有意义。内
核通过使用与处理用户应用程序相同的技巧来实现部分可移植性。这也就是说,如
同内核把用户应用程序和硬件分离一样,部分内核将会因为与硬件的联系而同其它
内核分离开来。通过这种分离,用户应用程序和部分内核都成为可移植的。
虽然这通常并不能够使得内核本身更清楚,但是源程序代码的体系结构无关部分通
常定义了与低层,也就是体系结构相关部分(或假定)的接口。作为一个简单的例
子,内存管理代码中的体系结构无关部分假定只要包含特定的头文件就可以获得合
适的PAGE_SIZE 宏(参看10791行)的定义,该宏定义了系统的内存管理硬件用于
分割系统地址空间的内存块的大小(参看第8章)。体系结构无关代码并不关心宏
的确切定义,而把这些问题都留给体系结构相关代码去处理。(顺便一提,这比到
处使用#ifdef/#endif程序块来定义平台相关代码要清晰易懂得多。)
这样,内核向新的体系结构的移植就转变成为确认这些特性以及在新内核上实现它
们的问题。
另外,用户应用程序的可移植性还可以通过它和内核的中间层次――标准C库(
libc)――的协助来实现。应用程序实际上从不和内核直接通讯,而只通过libc来
实现。图3.1中显示应用程序和内核直接通讯的唯一原因在于它们能够和内核通讯
。虽然在实际上应用程序并不同内核直接通讯――这样做是毫无意义的。通过直接
和内核通讯所能处理的问题都可以通过使用libc实现,而且更容易。
Libc和内核通讯的方式是体系结构相关的(这和图中有一点矛盾),libc负责将用
户代码从实现细节中解放出来。有趣的是,甚至大部分libc都不了解这些细节。大
部分的libc,例如atoi和rand的实现,都根本不需要和内核进行通讯。剩余部分的
大部分libc,例如printf函数,在涉及到内核之前或之后就已经处理大量的工作。
(printf必需首先解释格式化字符串,分析相应参数,设定打印方法,在临时内部
缓冲器中记录预期输出。直到此时它才调用底层系统调用write来实际打印该缓冲
区。)其它部分的libc 则只是相应系统调用的简单代理。因而一旦发生函数调用
时,它们会立即调用内核相应函数以完成主要工作。在最低层次上,大部分libc通
过单通道同内核进行交流,而它们所使用的机制将第5章中进行详细介绍。
由于这种设计,所有的用户应用程序,甚至大部分的C库,都是通过体系结构无关
的方式和内核通讯的。
内核体系结构的深入了解
图3.2显示了内核概念化的一种可能方式。该图和区分内核的体系结构无关和体系
结构相关的方法有所不同,它是一种更具有普遍性的结构视图。在“Kernel”框内
的本书中有所涉及的内核部分都用括号注明了相应的章节编号。虽然有关对称多处
理(SMP)的支持也属于本书的范围,但是在这里我们却没有标明章号。部分原因
在于相当多的SMP代码广泛地分布于整个内核中,因此很难将它与某一个模块联系
起来。同样的道理,对于内核初始化的支持也属于本书的范围,但是也没有标明章
号。这样做仅仅是因为从设计的观点上看,该问题并不重要。最后,虽然在图中我
们将第6章和“进程间通讯”框联系在一起,但是该章只涉及一部分进程间通讯的
内容。


图3.2 详细的内核体系结构图

进程和内核的交互通常需要通过如下步骤:
1.  用户应用程序调用系统调用,通常是使用libc。
2.  该调用被内核的system_call函数截获(第5章,171行),此后该函数会将调用
请求转发给另外的执行请求的内核函数。
3.  该函数随即和相关内部代码模块建立通讯,而这些模块还可能需要和其它的代
码模块或者底层硬件通讯。
4.  结果按照同样的路径依次返回。
然而,并不是所有内核和进程间的交互都是由进程发起的。内核有时也会自行决定
同哪个进程交互,例如通过释放信号量或者简单的采用直接杀死进程的方法终止该
进程的执行(如当进程用完所有可用的CPU时间片),以便使其它进程有机会运行
。这些交互过程在该图中并没有表示,主要是因为它们通常都只是内核对自己的内
部数据结构的修改(信号量传递对于这种规则来说是一个例外)。
是层次化(Layered),模块化(Modular)还是其它?
解决复杂性的所有方法都基于一个基本原理:问题分解和各个击破。也就是说,都
是把大型的、难以解决的问题(或系统)分解成一定数量的复杂度较低的子问题(
或子系统),再根据需要重复这一过程直到每一部分都小到可以解决为止,而各种
方法只是这种原理的一些不同运用而已。
计算机科学中有三种经典的方法比较适合于构建大型系统(我首先必须说明的是,
这些定义都是经过我深思熟虑的讨论对象)。
?   层次(Layer)――将解决方案分解成若干部分,在这些部分中存在一个问题域
的最底层,它为上层的抽象层次较高的工作提供基础。较高层建立在其低层基础之
上。OSI和TCP/IP协议堆栈是众所周知的层次化软件设计的成功的例子。操作系统
设计的层次化解决方案可能会包含一个可以直接和硬件通讯的层次、然后在其上提
供为更高层提供抽象支持的层次。这样更高层就可以对磁盘、网卡等硬件进行访问
,而并不需要了解这些设备的具体细节。
层次化设计的一个特征是要逐步构建符号集(vocabulary)。随着层次的升高,符
号集的功能将越来越强大。层次化设计的另外一个特征是完全可以在对其上下层透
明的条件下替换某一层次。在最理想的情况下,移植层次化的操作系统只需要重写
最低层的代码。纯层次化模型实现的执行速度可能会很慢,因为高层必须(间接的
)通过调用一系列连续的低层才能处理完自己的任务――N层调用N-1层,N-1层调
用N-2层,等等,直到实际的工作在0层被处理完成。接着,结果当然是通过同样的
路径反向传递回来。因此,层次化设计通常会包含对某些高层直接和某些低层通讯
的支持;这样虽然提高了速度,但是却使得各个层次的替换工作更加困难(因为不
止一个高层会直接依赖于这个你所希望进行替换的层次)。
?   模块(Module)――模块将具体的一部分功能块隐藏在抽象的接口背后。模块的
最大特点是将接口和其实现分离开来,这样就能够保证一个模块可以在不影响其它
模块的情况下进行改变。这样也将模块之间的依赖关系仅仅限定于接口。模块的范
围是试图反映求解域内一些方面的自然的概念性界限。纯模块化的操作系统因而就
可能有一个磁盘子系统模块,一个内存管理子系统模块,等等。纯模块化和纯层次
化的操作系统之间的主要区别是一个可以由其它模块自由调用,模块间没有上层和
下层的概念。(从这个意义上来说,模块是广义的层次。按照纯粹的观点,层次是
最多可供一个其它模块调用的模块,这个模块也就是它的直接上层模块。)
?   对象(Object)――对象和模块不同,因为对于初学者来说它们具有不同的问题
考虑方式,实现的方法也可能各自独立。但是,就我们当前的目的来说,对象不过
是结构化使用模块的方法。组件(Component)作为对象思想的进一步改进目前还
没有在操作系统设计中广泛使用。即便如此(按照我们的观点),我们也没有足够
的理由将其和模块划分在不同的范畴中。
图3.1强调了内核的层次化的视图,而且是体系结构无关层次位于体系结构相关层
次之上。(更为精确的视图是在顶层增加一个附加的体系结构相关的层次。这是因
为系统调用接口位于应用程序和内核之间,而且是体系结构相关的。)图3.2着重
强调了更加模块化的内核视图。
从合理的表述层次上看,这两种观点都是正确的。但也可以说这两种观点都是错误
的。我可以用大量的图片向你证明内核是遵从所有你所能够指出的设计原则集合的
,因为它就是从众多思想中抽取出来的。简单说来,事实是Linux内核既不是严格
层次化的,也不是严格模块化的,也不是严格意义上的任何类型,而是以实用为主
要依据的。(实际上,如果要用一个词来概括Linux从设计到实现的所有特点,那
么实用就是最确切的。)也许最保守的观点是内核的实现是模块化的,虽然这些模
块有时会为了追求速度而有意跨越模块的界限。
这样,Linux的设计同时兼顾了理论和实际。Linux并没有忽视设计方法;相反,在
Linux的开发基本思想中,设计方法的作用就像是编译器:它是完成工作的有力工
具。选择一个基本的设计原则(例如对象)并完全使用这种原则,不允许有任何例
外,这对于测试该原则的限制,或者构建以说明这些方法为目的的教学系统来说都
是一个不错的方法。但是如果要用它来达到Linux的设计目标则会引起许多问题。
而且Linux的设计目标中也并不包括要使内核成为一个完全纯化的系统。Linux开发
者为了达到设计目标宁愿违背妨碍目标实现的原则。
实际上,如果对于Linux来说是正确的,那么它们对于所有最成功的设计来说都是
正确的。最成功、应用最广泛的实际系统必然是实用的系统。有些开发人员试图寻
找功能强大的可以解决所有问题的特殊方法。他们一旦找到了这种方法,所有的问
题就都迎刃而解了。像Linux内核一样的成功设计通常需要为系统的不同部分和描
述上的不同层次使用不同的方法。这样做的结果可能不是很清晰,也不是很纯粹,
但是这种混合产物比同等功能的纯粹系统要强大而且优秀得多。
Linux大部分都是单内核的
操作系统内核可能是微内核,也可能是单内核(后者有时称之为宏内核
Macrokernel)。按照类似封装的形式,这些术语定义如下:
?   微内核(Microkernel kernel)――在微内核中,大部分内核都作为独立的进程
在特权状态下运行,它们通过消息传递进行通讯。在典型情况下,每个概念模块都
有一个进程。因此,如果在设计中有一个系统调用模块,那么就必然有一个相应的
进程来接收系统调用,并和能够执行系统调用的其它进程(或模块)通讯以完成所
需任务。
在这些设计中,微内核部分经常只不过是一个消息转发站:当系统调用模块要给文
件系统模块发送消息时,消息直接通过内核转发。这种方式有助于实现模块间的隔
离。(某些时候,模块也可以直接给其它模块传递消息。)在一些微内核的设计中
,更多的功能,如I/O等,也都被封装在内核中了。但是最根本的思想还是要保持
微内核尽量小,这样只需要把微内核本身进行移植就可以完成将整个内核移植到新
的平台上。其它模块都只依赖于微内核或其它模块,并不直接直接依赖硬件。
微内核设计的一个优点是在不影响系统其它部分的情况下,用更高效的实现代替现
有文件系统模块的工作将会更加容易。我们甚至可以在系统运行时将开发出的新系
统模块或者需要替换现有模块的模块直接而且迅速的加入系统。另外一个优点是不
需要的模块将不会被加载到内存中,因此微内核就可以更有效的利用内存。
?   单内核(Monolithic kernel)――单内核是一个很大的进程。它的内部又可以
被分为若干模块(或者是层次或其它)。但是在运行的时候,它是一个独立的二进
制大映象。其模块间的通讯是通过直接调用其它模块中的函数实现的,而不是消息
传递。
单内核的支持者声称微内核的消息传递开销引起了效率的损失。微内核的支持者则
认为因此而增加的内核设计的灵活性和可维护性可以弥补任何损失。
我并不想讨论这些问题,但必须说明非常有趣的一点是,这种争论经常会令人想到
前几年CPU领域中RISC和CISC的斗争。现代的成功CPU设计中包含了所有这两种技术
,就像Linux内核是微内核和单一内核的混合产物一样。Linux内核基本上是单一的
,但是它并不是一个纯粹的集成内核。前面一章所介绍的内核模块系统将微内核的
许多优点引入到Linux的单内核设计中。(顺便提一下,我考虑过一种有趣的情况
,就是Linux的内核模块系统可以将系统内核转化成为简单的不传递消息的微内核
设计。虽然我并不赞成,但是它仍然是一个有趣的想法。)
为什么Linux必然是单内核的呢?一个方面是历史的原因:在Linus的观点看来,通
过把内核以单一的方式进行组织并在最初始的空间中运行是相当容易的事情。这种
决策避免了有关消息传递体系结构,计算模块装载方式等方面的相关工作。(内核
模块系统在随后的几年中又进行了不断地改进。)
另外一个原因是充足的开发时间的结果。Linux既没有开发时间的限制,也没有深
受市场压力的发行进度。 所有的限制只有并不过分的对内核的修改与扩充。内核
的单一设计在内部实现了充分的模块化,在这种条件下的修改或增加都并不怎么困
难。而且问题还在于没有必要为了追求尚未证实的可维护性的微小增长而重写
Linux的内核。(Linus曾多次特别强调了如下的观点:为了这点利益而损耗速度是
不值得的。)后面章节中的部分内容将详细的重新考虑充足开发时间的效果。
如果Linux是纯微内核设计,那么向其它体系结构上的移植将会比较容易。实际上
,有一些微内核,如Mach微内核,就已经成功的证明了这种可移植性的优点。实际
的情况是,Linux内核的移植虽然不是很简单,但也绝不是不可能的:大约的数字
是,向一个全新的体系结构上的典型的移植工作需要30,000到60,000行代码,再加
上不到20,000行的驱动程序代码。(并不是所有的移植都需要新的驱动程序代码。
)粗略的计算一下,我估计一个典型的移植平均需要50,000行代码。这对于一个程
序员或者最多一个程序小组来说是力所能及的,可以在一年之内完成。虽然这比微
内核的移植需要更多的代码,但是Linux的支持者将会提出,这样的Linux内核移植
版本比微内核更能够有效的利用底层硬件,因而移植过程中的额外工作是能够从系
统性能的提高上得到补偿的。
这种特殊设计的权衡也不是很轻松就可以达到的,单内核的实现策略公然违背了传
统的看法,后者认为微内核是未来发展的趋势。但是由于单一模式(大部分情况下
)在Linux中运行状态良好,而且内核移植相对来说比较困难,但没有明显地阻碍
程序员团体的工作,他们已经热情高涨地把内核成功的移植到了现存的大部分实际
系统中,更不用说类似掌上型电脑的一些看起来很不实际的目标了。只要Linux的
众多特点仍然值得移植,新的移植版本就会不断涌现。
设计和实现的关系
接下来的部分将介绍一些内核设计和实现之间的关系。本部分最重要的内容是对于
内核源程序目录结构的概述,这一点随后就会提到。本章最后以实现中体系结构无
关代码和体系结构相关代码的相对大小的估算作为总结。
内核源程序目录结构
按照惯例,内核源程序代码安装在/usr/src/linux目录下。在该目录下还有几个其
它目录,每一个都代表一个特定的内核功能性子集(或者非常粗略的说是高层代码
模块)。
Documentation
这个目录下面没有内核代码,只有一套有用的文档。但是这些文档的质量不一。有
一部分内核文档,例如文件系统,在该目录下有相当优秀而且相当完整的文档;而
另外一部分内核,例如进程调度,则根本就没有文档。但是在这里你可以不时的发
现自己所最需要的东西。
arch
arch目录下的所有子目录中都是体系结构相关的代码。每个体系结构特有的子目录
下都又至少包含三个子目录:kernel,存放支持体系结构特有的诸如信号量处理和
SMP之类特征的实现;lib,存放高速的体系结构特有的诸如strlen和memcpy之类的
通用函数的实现;以及mm,存放体系结构特有的内存管理程序的实现。
除了这三个子目录以外,大多数体系结构在必要的情况下还都有一个boot子目录
,该目录中包含有在这种平台上启动内核所使用的部分或全部平台特有代码。这些
启动代码中的部分或全部也可以在平台特有的内核目录下找到。
最后,大部分体系结构所特有的目录还可以根据需要包含了供附加特性或改进的组
织使用的其它子目录。例如,i386目录包含一个math-emu子目录,其中包括了在缺
少数学协处理器(FPU)的CPU上运行模拟FPU的代码。作为另外一个例子,m68k移
植版本中为每一个该移植版本所支持的基于680x0的机器建立了一个子目录,从而
这些机器所特有的代码都有一个自然的根目录。
下面几个是arch目录下的子目录:
?   arch/alpha/――Linux内核到基于DEC Alphs CPU工作站的移植。
?   arch/arm/――Linux到ARM系列CPU 的移植,该类CPU主要用于诸如Corel的
NetWinder和Acorn RiscPC之类的机器。
?   arch/i386/――最接近于Linux内核原始平台或标准平台。这是为Intel的80386
结构使用的,当然包括对同一系列后来的CPU(80486,Pentium等等)的支持。它
还包括了对AMD,Cyrix和IDT等公司的一些兼容产品的支持。
本书基本上将这种体系结构称为“x86”。即使这样,严格说来“x86”对于我们的
目标来说还是要求得过于宽泛。早期的Intel CPU,例如80286,并没有包括Linux
运行所需的所有特性。对于这些机器,Linux也没有正式的支持版本。(顺便提一
下,Linux对这种CPU的独立移植版本是存在的,不过它在功能上有部分损失。)当
本书中提到“x86平台”时,通常是指80386或更新的CPU。
?   arch/m68k/――到Motorola的680x0 CPU系列的移植。该版本可以提供对基于从
68020(只要它同内存管理单元(MMU)68851一起使用)到68060的一切机器的支持
。很多公司在他们的产品中使用680x0系列芯片,例如Commodore(现在是Gateway
)的Amiga,Apple的Macintosh,Atari ST,等等。这些老机器中的很多现在正充
当可靠的Linux工作站。另外,到NeXT工作站和SUN 3工作站的移植也正在进行中。

?   arch/mips/――到MIPS的CPU系列的移植。虽然有其它几个厂商也使用MIPS开发
了一些系统,但是基于这种CPU的最出名的机器是Silicon Graphics(SGI)工作站

?   arch/ppc/――到Motorola/IBM的PowerPC系列CPU的移植。这包括对基于
PowerPC的Macintosh和Amiga以及BeBox、IBM的RS/6000等其它一些机器的支持。
?   arch/sparc/――到32位SPARC CPU的移植。这包括对从Sun SPARC 1到SPARC 20
的全部支持。
?   arch/sparc64/——到基于64位SPARC CPU(UltraSPARC系)系统的移植。这里所
能够支持的机器包括Sun的Ultra 1,Ultra 2和更高配置的机器,直到Sun的最新产
品Enterprise 10000。注意32位和64位的SPARC的移植版本正在合并中。
不幸的是,本书必须将注意力集中在x86上,因此只应用到了arch/i386/目录下的
代码,而其它体系结构所特有的代码将不再涉及了。
drivers
这个目录是内核中非常大的一块。实际上,drivers目录下包含的代码占整个内核
发行版本代码的一半以上。它包括显卡、网卡、SCSI适配器、软盘驱动器,PCI设
备和其它任何你可以说出的Linux支持的外围设备的软件驱动程序。
Drivers目录下的一些子目录是平台特有的,例如,zorro子目录中包含有和Zorro
总线通讯的代码。而Zorro总线只在Amiga中使用过,因此这些代码必然是Amiga特
有的。而其它一些子目录,例如pci子目录,则至少是部分平台无关的。
fs
Linux支持的所有文件系统在fs目录下面都有一个对应的子目录。一个文件系统(
file system)是存储设备和需要访问存储设备的进程之间的媒介。
文件系统可能是本地的物理上可访问的存储设备,例如硬盘或CD-ROM驱动器;在这
两种情况下将分别使用ext2和isofs文件系统。文件系统也可能是可以通过网络访
问的存储设备;这种情况下使用的文件系统是NFS。
还有一些伪文件系统,例如proc文件系统,可以以伪文件的形式提供其它信息(例
如,在proc的情况下是提供内核的内部变量和数据结构)。虽然在底层并没有实际
的存储设备与这些文件系统相对应,但是进程可以像有实际存储设备一样处理(
NFS也可以作为伪文件系统来使用)。
include
这个目录包含了Linux源程序树中大部分的包含(.h)文件。这些文件按照下面的
子目录进行分组:
?   include/asm-*/――这样的子目录有多个,每一个都对应着一个arch的子目录,
例如include/asm-alpha,include/asm-arm,include/asm-i386等等。每个目录下
的文件中包含了支持给定体系结构所必须的预处理器宏和短小的内联函数。这些内
联函数很多都是全部或部分地使用汇编语言实现的,而且在C或者汇编代码中都会
应用到这些文件。
当编译内核时,系统将建立一个从include/asm到目标体系结构特有的目录的符号
链接。结果是体系结构无关内核源程序代码可以使用如下形式的代码来实现所需功
能:
#include <asm/some-file>
这样就能够将适当地体系结构特有的文件包含(#include)进来。
?   include/linux/――内核和用户应用程序请求特定内核服务时所使用的常量和数
据结构在头文件中定义,而该目录中就包含了这些头文件。这些文件大都是平台独
立的。这个目录被全部复制(更多的情况是链接)到/usr/inlude/linux下。这样
用户应用程序就可以使用#include包含这些头文件,而且能够保证所包含进来的头
文件的内容和内核中的定义一致。第9章将会给出有关的一个样例。
?   对这些文件的移植只有对于内核来说才是必须的,对用户应用程序则没有必要。
移植工作可以按照如下的方式封装处理:
/* …  Stuff for user apps and kernel … */
#ifdef  __KERNEL__

    /* … Stuff for kernel only … */
    #endif  /* __KERNEL__ */
?   include/net/――这个目录供与网络子系统有关的头文件使用。
?   include/scsi/――这个目录供与SCSI控制器和SCSI设备有关的头文件使用。
?   include/video/――这个目录供与显卡和帧显示缓存有关的头文件使用。
init
这个目录下面的两个文件中比较重要的一个是main.c,它包含了大部分协调内核初
始化的代码。第4章将详细介绍这部分代码。
ipc
这个目录下的文件实现了System V的进程间通讯(IPC)。在第9章中将会对它们进
行详细介绍。
kernel
这个目录中包含了Linux中最重要的部分:实现平台独立的基本功能。这部分内容
包括进程调度(kernel/sched.c)以及创建和撤销进程的代码(kernel/fork.c和
kernel/exit.c);以上所有的以及其它部分内容将在第7章中有所涉及。但是我并
不想给你留下这样的印象:需要了解的内容都在这个目录下。实际上在其它目录下
也有很多重要的内容。但是,不管怎样说,最重要部分的代码是在这个目录下的。

lib
lib目录包含两部分的内容。lib/inflate.c中的函数能够在系统启动时展开经过压
缩的内核(请参看第4章)。lib目录下剩余的其它文件实现一个标准C库的有用子
集。这些实现的焦点集中在字符串和内存操作的函数(strlen,mmcpy和其它类似
的函数)以及有关sprintf和atoi的系列函数上。
这些文件都是使用C语言编写的,因此在新的内核移植版本中可以立即使用这些文
件。正如本章前面部分说明的那样,一些移植提供了它们独有的高速的函数版本,
这些函数通常是经过手工调整过的汇编程序,在移植后的系统使用这些函数来代替
原来的通用函数。
mm
该目录包含了体系结构无关的内存管理代码。正如我们前面说明的那样,为每个平
台实现最低层的原语的体系结构特有的内存管理程序是存储在arch/platform/mm中
的。大部分平台独立和x86特有的内存管理代码将在第8章中介绍。
net
这个目录包含了Linux应用的网络协议代码,例如AppleTalk,TCP/IP,IPX等等。

scripts
该目录下没有内核代码,它包含了用来配置内核的脚本。当运行make menuconfig
或者make xconfig之类的命令配置内核时,用户就是和位于这个目录下的脚本进行
交互的。
体系结构相关和体系结构无关的代码
现在我们来估计一下体系结构相关和体系结构无关代码的相对大小。我们首先给出
一些数字。完整的2.2.5的内核总共有1,725,645行代码。(顺便一提,请注意本书
只包含了39,000行代码,但是我们仍然努力涵盖了相当部分的核心函数。)其中一
共有392,884行代码在体系结构特有的目录之内,也就是arch/*和include/asm-*下
面。我估计还有超过64,000行的代码是仅供一种体系结构专用的驱动程序。这意味
着大约26%的代码是专用于特定体系结构的。
但是,对于单一一种体系结构,体系结构相关代码比例相对较小。不妨理想一点,
如果某种体系结构所需要的特有代码约有50,000行,而体系结构无关代码则大约有
1,250,000行,那么体系结构相关代码大概只占到4%。当然,在特定的一个内核中
,并不是所有这些体系结构无关代码都会被用到,因此体系结构相关代码在特定内
核中所占的比重与内核的配置有关。但是不管怎样,很显然大部分内核代码是平台
独立的。
回复

使用道具 举报

 楼主| 发表于 2003-10-27 09:32:43 | 显示全部楼层
Linux内核注释——第4章 系统初始化


当你想要运行程序时,你需要把程序的文件名敲入shell――或者更为流行的,在
如GNOME或者KDE等之类桌面环境中点击相应的图标――这样就能将其装载进内核并
运行。但是,首先必须有其它的软件来装载并运行内核;这通常是诸如LOADLIN或
者LILO之类的内核引导程序。更进一步,我们还需要其它的软件来装载运行内核引
导程序――称之“内核引导程序的引导程序”――而且看起来似乎运行内核引导程
序的引导程序也需要内核引导程序的引导程序的引导程序,等等,这个过程是无限
的。
这个无限循环的过程必然最终在某个地方终止,这就是硬件。因此,在最低的层次
上,启动系统的第一步是从硬件中获得帮助。该硬件总是运行一些短小的内置程序
――软件,但是这些软件是被固化在只读存储器中,存储在已知地址中。因此,在
这种情况下就不需要软件引导程序了――它能够运行更大更复杂的程序,直到内核
自身装载成功为止。按照这种方式,系统自己的引导过程(bootstrap)会引发系
统的启动,当然这只是术语“系统引导(booting)”的一个比喻。虽然不同体系
结构的引导过程的具体细节差异很大,但是它们的原则都基本相同。
前面的工作都完成以后,内核就已经成功装载了。随后内核可以初始化自身以及系
统的其它部分。
本章首先将简单介绍基于x86 PC机的典型自启动方式,接着回顾一下每一步工作在
什么时机发生,最后我们还要介绍的是内核的相应部分。
引导PC机
本节简要介绍x86 PC是如何引导的。本节的目的不是让你精通PC是怎样引导的――
这超出了本书的范围――而是向你展示特定体系结构一般的引导方式,为下文中的
内核初始化进行铺垫。
首先,机器中的每个CPU都要自行初始化,接着可能要用几分之一秒的时间来执行
自测试。在多处理器的系统中,这个过程会更复杂些――但是实际上也并不多。在
双处理器的Pentium系统中,一个CPU总是作为主CPU存在,另外一个CPU则是辅CPU
。主CPU执行启动过程中的剩余工作,随后内核才会激活辅CPU。在多处理器的
Pentium Pro系统中,CPU必须根据Intel定义的算法“抢夺标志”――来动态决定
由哪个CPU启动系统。取得标志的CPU启动系统,随后内核激活其它的CPU。无论是
哪种情况,启动程序的剩余部分只与一个CPU有关。这样,在随后的一段时间内,
我们可以认为该系统中只有一个CPU是可用的,而不考虑其它的CPU,或者说这些
CPU被暂时隐藏了。另一方面,内核还需要明确的激活所有其它的CPU――这一点你
可以在本章后续部分看到。
接下来,    CPU从0xfffffff0单元中取得指令并执行,这个地址非常接近于32位CPU
的最后可用的地址。因为大多数PC都没有4GB的RAM,所以通常在这个地址上并没有
实际内存的。内存硬件可以虚拟使用它。对那些确实有4GB内存的机器来说,它们
也只是仅仅损失了供BIOS使用的顶端地址空间末尾的少量内存(实际上BIOS在这里
只保留了64K的空间――这种损失在4GB的机器中是可以忽略的)。
该地址单元中存储的指令是一条跳转指令,这条指令跳转到基本输入输出(BIOS)
代码的首部。BIOS内置在主板中,它主要负责控制系统的启动。请注意CPU实际上
并不真正关心BIOS是否存在,这样就使得在诸如用户定制的嵌入系统之类的非PC体
系结构的计算机中使用Intel的CPU成为可能。CPU执行在目标地址中发现的任何指
令,在这里使用跳转指令转移到BIOS只是PC体系结构的一部分。(实际上,跳转指
令自己是BIOS的一部分,但是这不是考虑这个问题的最方便的方法。)
BIOS使用内置的规则来选择启动设备。通常情况下,这些规则是可以改变的,方法
是在启动过程开始时按下一个键(例如,在我的系统中是Delete键)并通过一些菜
单选项浏览选择。但是,通常的过程是BIOS首先试图从软盘启动,如果失败了,就
再试图从主硬盘上启动。如果又失败了,就再试图从CD-ROM上启动。为了使问题更
具体,这里讨论的情况假定是最普通的,也就是启动设备是硬盘。
从这种启动设备上启动,BIOS读取第一个扇区的信息――首512个字节――称之为
主引导记录(MBR)。接下来发生的内容有赖于Linux是怎样在系统上安装的。为使
讨论形象具体,我们假定LILO是内核的载入程序。在典型的设置中,BIOS检测MBR
中的关键数字(为了确认该数据段的确是MBR)并在MBR中检测引导扇区的位置。这
一扇区包含了LILO的开始部分,然后BIOS将其装入内存,开始执行。
注意我们现在已经实现了从硬件和内置软件的范围到实际软件范围的转变,从有形
范围到无形范围,也就是说从你可以接触的部分到不可接触的部分。
下面就是LILO的责任了。它把自己其余的部分装载进来,在磁盘上找到配置数据,
这些数据指明从什么地方可以得到内核,启动时要通过什么选项。LILO接着装载内
核到内存并跳转到内核。
通常,内核以压缩形式存储,只有少量指令足以完成解压缩的任务,也就是自解压
可执行文件,是以非压缩形式存储的。因此,内核的下一步工作是自解压缩内核镜
像。到这里,内核就已经完成了装载的过程。
下面是到现在进行的步骤地简要描述:
1.  CPU初始化自身,接着在固定位置执行一条指令。
2.  这条指令跳转到BIOS中。
3.  BIOS找到启动设备并获取MBR,该MBR指向LILO。
4.  BIOS装载并把控制权转交给LILO。
5.  LILO装载压缩内核。
6.  压缩内核自解压并把控制权转交给解压的内核
正如你所见到的,引导过程每一步都将你带入更大量更复杂的代码块中,一直到最
后成功地运行了内核为止。
依赖于你计算层次的方式,CPU成为内核引导程序的引导程序的引导程序的引导程
序(CPU装载BIOS,BIOS装载LILO,LILO装载压缩内核,压缩内核装载解压内核;
但是你可以合理的考虑是否这些步骤都满足引导程序的定义)。
初始化Linux内核
在内核成功装入内存(如果需要就解压缩)以及一些关键硬件,例如已经在低层设
置过的内存管理器(MMU,请参看第8章)之后,内核将跳转到start_kernel(
19802行)。这个函数完成其余的系统初始化工作――实际上,几乎所有的初始化
工作都是由这个函数实现的。因此,start_kernel就是本节的核心。
start_kernel
19802:__init标示符在gcc编译器中指定将该函数置于内核的特定区域。在内核完
成自身初始化之后,就试图释放这个特定区域。实际上,内核中存在两个这样的区
域,.text.init和.data.init――第一个是代码初始化使用的,另外一个是数据初
始化使用的。(诸如可以在进程间共享的代码和字符串常量之类的“文本(Text)
”是在可执行程序中的“纯区域”中使用的一个术语。)另外你也可以看到
__initfunc和__initdata标志,前者和__init类似,标志初始化专用代码,后者则
标志初始化专用数据。
19807:如前所述,即使在多处理器系统中,在启动时也只使用一个CPU。Intel称
之为引导程序处理器(Bootstrap Processor,简称为BSP),它在内核代码的某些
地方有时也称之为BP。BSP首次运行这一行时,跳过后面的if语句,并减小
boot_cpu标志,从而当其它CPU运行到此处时,都要运行if语句。等到其它CPU被激
活执行到这里时,BSP已经在idle循环中了(本章稍后会更详细的讨论这个问题)
,initialize_secondary(4355行)负责把其它CPU加入到BSP中。这样,其它CPU
就不用执行start_kernel的剩余部分了――这也是一件好事,因为这意味着不用再
进行对许多硬件进行冗余初始化等工作了。
    顺便说一下,这种奇异的小小的改动只有对于x86是必需的;对于其它平台,调用
smp_init完全可以处理SMP设置的其它部分,这一点马上就会讨论。因此,其它平
台的initialize_secondary的定义都是空的。
19816:打印内核标题信息(20099行),这里显示了有关内核如何编译的信息,包
括在什么机器上编译,什么时间编译,使用什么版本的编译器,等等。如果中间任
何一步发生了错误,在寻找机器不能启动的原因时查明内核的来源是一个有用的线
索。
19817:初始化内核自身的部分组件――内存,硬件中断,调度程序,等等。尤其
是setup_arch函数(19765行)完成体系结构相关的设置,此后在command_line(
传递到内核的参数,在下面讨论)、memory_start和memory_end(内核可用物理地
址范围)中返回结果。下面这些函数都希望驻留在内存低端的;它们使用
memory_start和memory_end来传递该信息。在函数获得所希望的值后,返回值指明
了新的memory_start的值。
19823:分析传给内核的各种选项。parse_options函数(19707行,在随后的分析
内核选项一节中讨论)也设置了argv和envp的初值。
19833:内核运行过程中也可以自行对所进行的工作进行记录,周期性地对所执行
的指令进行抽样,并使用所获得的结果更新表格。这在定时器中断过程中通过调用
x86_do_profile(1896行)来实现,该部分将在第6章中介绍。
    如图4.1中说明的那样,这个表格把内核划分为几个大小相同的范围,并简单跟踪
在一次中断的时间内每个范围中运行多少条指令。这种记录当然是非常粗糙的――
甚至不是依据函数和行号进行划分的,而只是使用近似的地址――但是这样代价很
低、快速、短小,而且有助于专家判断最关键的问题要点。每个表格条目所涉及到
地址的多少――还有问题发生地点的不确定性――可以通过简单修改prof_shift(
26142行)来调节。profile_setup(19076行,在本章中后面讨论)可以让你在启
动的时候设置prof_shift的值,这样比为修改这个数字而重新编译内核要清晰方便
得多。


图4.1 描述用缓存(profiling buffer)

    这个if程序块为记录表格分配内存,并把所有项都清零。注意到如果prof_shift
是0(缺省值),那么记录功能就被关掉了,if程序段不再被执行,也不为表格分
配空间。
19846:内核通过调用sti(13104行是UP版本的――注意该主题在第6章中有更详细
的介绍)开始接收硬件中断。首先需要激活定时器中断,以便后来对
calibrate_delay(19654行)的调用可以计算机器的BogoMIPS的值(在下一节“
BogoMIPS”中介绍)。因为一些设备驱动程序需要BogoMIPS的值,所以内核必需在
大部分硬件、文件系统等等初始化之前计算出这个值来。
19876:测试该CPU的各种缺陷,比如Pentium F00F缺陷(请参看第8章),记录检
测到的缺陷,以便于内核的其它部分以后可以使用它们的工作。(为了节省空间起
见,我们省略掉了check_bugs函数。)
19882:调用smp_init(19787行),它又调用了其它的函数来激活SMP系统中其它
CPU:在x86的平台上,smp_boot_cpus(4614行)初始化一些内核数据结构,这些
数据结构跟踪检测另外的CPU并简单的将其改为保持模式;最后smp_commence(
4195行)使这些CPU继续执行。
19883:把init函数作为内核线程终止,这比较复杂;请参看本章后面有关init的
讨论。
19885:增加idle进程的need_resched标志,这样做的原因在此时可能还比较模糊
。直到读完了第5、6、7章以后,才能有个清楚的概念;但是,在下一个定时器中
断结束之前(在第6章中讨论),system_call(171行,在第5章中讨论)函数中会
注意到idle进程的need_fesched标志增加了,并且调用schedule(26686行,第7章
)释放CPU,并将其赋给更应该获取CPU的进程。
19886:已经完成了内核初始化的工作――或者不管怎样,已经把需要完成的少量
责任传递给了init――所剩余的工作不过是进入idle循环以消耗空闲的CPU时间片
。因此,本行调用cpu_idle(2014行)――idle循环。正如你可以从cpu_idle本身
可以发现的一样,该函数从不返回。然而,当有实际工作要处理时,该函数就会被
抢占。
    注意到cpu_idle只是反复调用idle系统调用(下一章将讨论系统调用),它通过
sys_idle(2064行)实现真正的idle循环――2014行对应UP版本,2044行针对SMP
版本。它们通过执行hlt(对应“halt”)指令把CPU转入低功耗的“睡眠”状态。
只要没有实际的工作处理,CPU都将转入这种状态。
BogoMIPS
BogoMIPS的数字由内核计算并在系统初始化的时候打印。它近似的给出了每秒钟
CPU可以执行一个短延迟循环的次数。在内核中,这个结果主要用于需要等待非常
短周期的设备驱动程序――例如,等待几微秒并查看设备的某些信息是否已经可用

由于没有正确理解BogoMIPS的含义,BogoMIPS在各处都被滥用,就仿佛它可以满足
人类最原始、最深层次的需求:把所有计算机性能的信息简化为一个数字。“
BogoMIPS”中的“Bogo”部分来源于“伪(bogus)”,就正是为了防止这种用法
:虽然这个数字比大多数性能比较有效很多,但是它仍然是不准确的、容易引起误
解的、无用的和不真实的,根本不适合将它用于机器间差别的对比。但是这个数字
仍然非常吸引人,这也正是我们在这里讨论这个问题的原因。(顺便说一下,
BogoMIPS 中“MIPS”部分是“millions of instructions per second(百万条指
令每秒)”的缩写,这是计算机性能对比中的一个常用单位。)
calibrate_delay
19654:calibrate_delay是近似计算BogoMIPS数字的内核函数。
19622:作为第一次估算,calibrate_delay计算出在每一秒内执行多少次__delay
循环(6866行),也就是每个定时器滴答(timer tick)――百分之一秒――内延
时循环可以执行多少次。
19664:计算一个定时器滴答内可以执行多少次循环需要在滴答开始时就开始计数
,或者应该尽可能与它接近。全局变量jiffies(16588行)中存储了从内核开始保
持跟踪时间开始到现在已经经过的定时器滴答数;第6章中将介绍它的实现方式。
jiffies保持异步更新,在一个中断内——每秒一百次,内核暂时挂起正在处理的
内容,更新变量,然后继续刚才的工作。如果不这样处理,下一行的循环就永远不
可能退出。从而,如果jiffies不声明为volatile――简单的说,这个值变化的原
因对于编译器是透明的――gcc仍然可能对该循环进行优化,并引起该循环进入不
能退出的状态。虽然目前的gcc还没有如此高的智能,然而它的维护者应该完全能
够为它实现这种智能。
19669:定时器又前移了一个滴答,因此又产生一个新的滴答。下一步是要等待
loops_per_sec延时循环调用定时器循环,接着检测是否最少有一个完整的滴答已
经完成。如果是这样,就退出首次近似估算循环;如果没有,就把loops_per_sec
的值加倍,然后重新启动这个过程。
    这个循环的正确性依赖于如下的事实:现有的机器在任何地方都不能每秒执行
232次延时循环――对于64位机来说则远低于每秒264次――虽然这只是一个微不足
道的问题。
19677:现在内核已经清楚loops_per_sec循环调用延时循环在这台机器上要花费超
过百分之一秒的时间才能完成,因此,内核将重新开始进行估算。为了提高效率,
内核使用折半查找算法计算loops_per_sec的实际值,我们假定开始的时候,实际
值在现在计算结果和其一半之间――实际值不可能比现在计算值还大,但是可以(
而且可能)稍微小一点。
19681:和前面使用的方式一样,calibrate_delay查看是否这个loops_per_sec已
经减小了的值还是比较大,需要耗费一个完整的定时器间隔。如果还是相当大,实
际值应该小于当前计算值或者就是当前值,因此,使用更小的值继续查询;如果不
够大,就使用一个更大的值继续查询。
19691:内核有一种很好的方法来计算一个定时器滴答中执行延时循环的次数。这
个数字乘以一秒内滴答的数量就得到了每秒内可以执行的延时循环的次数。这种计
算只是一种估算,乘法也累积了误差,因此结果并不能精确到纳秒。但是这个数字
供内核使用已经足够精确了。
19693:为了让用户感到激动,内核打印出这个数字。注意这里明显省略了%f的格
式限定――内核尽量避免浮点数运算。这个计算过程中最有用的常量是500,000;
它是用一百万除以2得来,理由是每秒钟执行一百万条指令,而每个delay循环的核
心是2条指令(decl和一条跳转指令)。
分析内核选项
parse_options函数分析由内核引导程序发送给内核的启动选项,在初始化过程中
按照某些选项运行,并将剩余部分传送给init进程(在本章后面部分提到)。这些
选项可能已经存储在配置文件中了,也可能是由用户在系统启动时敲入的――内核
并不关心这些。类似的细节全部是内核引导程序应该关注的内容。
parse_options
19707:参数已经收集在一条长的命令行中,内核被赋给指向该命令行头部的一个
指针;内核引导程序在前面已经将该行存储在一个指定地址中。
19718:中断下一个参数,保持指向下一个参数的指针以供下一次循环使用。注意
系统使用空格而不是通常的空白来分隔内核参数;制表符并不能把当前参数和下一
个参数分隔开。如果发现了分隔字符空格,下一行就使用字节0覆盖,这样line可
以作为包含有唯一一个内核选项的标准C字符串来使用了。如果没有发现空格,就
该函数关心的内容而言,其余的部分都具有相同的属性――这只有在处理line中最
后一个选项的情况下才会发生,循环就会在下次开始时结束。
    注意该代码不会跳过多个空格。假设line值如下所述(两个空格):
    rw  debug
    这会被当作三个选项:“rw”,“”(空字符串)和“debug”。因为空字符串不
是有效的内核选项,它将会被传递到初始化的过程(这一点随后就可以看到)――
这当然不是用户所希望的。因此,内核引导程序应该负责对多个空格进行压缩。
LILO通过忽略用户多敲的空格,完美的解决了这个问题。
19721:现在开始解释这些选项。最前面的两个选项――ro和rw――指明内核要装
载根文件系统,也就是根目录( / 目录)所在的位置,而分别处于只读和读/写模
式。
19729:第三种可能性,debug,增加了调试信息的数量;这些调试信息要通过调用
do_syslog打印出来(25724行)。
19733:开始几个选项是简单的独立标志,它们并不使用参数。内核也可以辨认形
为option=value的选项。本行就是一个例子,这里内核引导程序定义了一个命令来
代替init运行;它使用init=/some/other/program的形式。这里的代码舍弃了
init= 部分,为随后init的使用而把剩余部分在execute_command中保存起来(
20044行,后面会讨论到)。和其它大部分参数的处理方法不同,本处功能不能在
checksetup(19612行,马上就讨论到)中实现,这是因为它改变了该函数的局部
变量。很快,你就可以看到前面三个选项之所以也在这里处理而不是在
checksetup中处理的原因。
19745:大部分内核选项都是由checksetup函数分析的。如果checksetup处理了某
个选项,就返回真值,循环继续进行。
19750:否则,line中没有已经被辨认的内核选项。在这种情况下,它被作为一个
供init进程使用的选项或者环境变量来处理――如果其形式为envar=value,就作
为环境变量处理;否则,就作为选项处理。如果argv_init和envp_init(分别见
19057和19059行)数组中有足够的空间,选项和环境变量就存储在里面供以后
init函数使用。
    这解释了从19736行开始的注释。字符串auto并不是任何内核选项的前缀,因此它
应该被作为init的一个参数存储在argv_init数组中――这在大多数情况下都是可
行的,因为auto是init可以识别的选项。但是,当使用init=的形式给出内核选项
时,通常是执行shell而不是init,auto会使shell混淆;因此,安全一点的方法是
,parse_options在此处忽略所有与此有关的init参数。
    奇怪的是,当argv_init或者envp_init空间用完时,整个循环就结束了。仅仅因
为argv_init的空间用完了并不意味着line中就不再含有init使用的环境变量,反
之亦然。此外,可能还剩下许多内核选项没有处理。当你考虑到MAX_INIT_ARGX(
19029行)和MAX_INIT_ENVS(19030行)都通过使用#define被预定义为8――这是
一个很容易超过的下限――这种行为就更奇怪了。如果在19752行和19756行的
break改成continue,那么循环可以继续处理内核选项,而不会写入超过
argv_init和envp_init数组界限的空间。如果command_line中仍然包含有并不是为
init而定义的内核选项,那么这一点就是非常重要的。
19760:所有的内核选项都处理完成了。最后一步是要使用NULL填充argv_init和
envp_init数组的末尾,从而使得init可以知道在哪里终止。
checksetup
19612:checksetup函数负责进行大部分内核选项的处理过程。它把这些内核选项
分为三类:一类使用内核普通参数来分析=sign之后的部分;另一类自行分析
=sign之后的部分;还有一类自行分析整个行,包括= sign前面的部分和= sign后
面的部分。第一类被认为是使用“现成”的参数,这与为第二类提供的“原始”参
数相对应。最后一类只由一个IDE驱动程序组成;内核首先在19619行检查并处理这
种情况,以使其不会在随后的处理中造成麻烦。
19625:接下来,checksetup扫描整个raw_params数组(19552行)并判断是否该内
核选项应该不加处理的保留。raw_params中的元素是struct kernel_param类型(
19223行)的,它把内核选项前缀和装载选项时调用的函数联系起来。如果数组中
的某些项的str成员以line为前缀,就会调用line后面的相应函数(也就是前缀之
后的部分),随后checksetup会返回一个非零值以表明它已经对该内核选项进行了
处理。raw_params数组以两个NULL结束,因此在检测到str成员是NULL时,循环就
可以结束了。在这种情况下,显然循环已经到达了raw_params数组的结尾,但是仍
然没有找到匹配的情况。当然,测试setup_func成员也可以取得同样好的效果。
    这个循环说明了一点:与大多数内核非常不同的是,这里的初始化并不需要尽可
能的快。如果内核比从前多用几微秒来启动,这并没有什么实际的损失――毕竟用
户应用程序还没有开始运行,所以他们并没有损失什么东西。
    最终结果是代码效率很低,而且存在很多优化的可能。例如,raw_params数组中
字符串的长度可以在raw_params中暂存,而不用在19626行多次重复计算。更好的
解决方法是,可以把raw_params数组中的项按照字符顺序排序,这样checksetup就
可以进行折半查找。
    在raw_params的情况中实现排序并没有什么障碍,但是这样也可能并不能获得很
大的优势,因为折半查找的优点只有在比较大的数组中才能充分表现出来(所谓比
较大的确切值在不同的环境中也有所不同)。raw_params的姊妹数组
cooked_params(19228行)当然是足够大的,可以显示出折半查找的优势;但是这
样就引发了一个新的问题:对cooked_params进行排序比较难用,因为这可能需要
分隔一些#ifdef程序段――请参看从19268行到19272行的例子。进一步说,因为算
法只是查找前缀,而不使用完全匹配,在遍历数组中的各个项时对遍历次序比较敏
感,所以这种特性在使用不同的查找次序时就很难再保持了。然而,这些问题并不
是不可克服的(程序员可以预先静态地为引导程序建立一颗前缀树),如果性能在
这里是主要因素,那么这种努力也是值得的。但是,由于性能在这里并不是主要问
题,所以简单性才被作为最重要的因素体现出来。
    即使这样,在类似的root_dev_names数组(19085行)中――这个数组把硬件设备
名的前缀映射到它们的主ID号上――开发者仍然可以简单地通过把比较常用的项(
IDE和SCSI磁盘)放在不太常用的项(串口IDE CDs)的前面以节省出一点性能。但
是我在raw_params或cooked_params中并没有发现与之类似的模式。
    另外一件需要注意的事是:现在你可以猜想一下为什么ro,rw和debug选项在
parse_options中测试而不在这里测试――parse_options要检测精确的匹配,但是
checksetup只检测前缀。作为一个特殊的情况,ro选项碰巧正好是root=(19553行
)的前缀,这样如果这三个选项彼此合并,就需要仔细处理了。这似乎仍然是一个
相当无力的原因。考虑一下noinitrd选项(19251行)。这是cooked_params的一个
项,因而只需要匹配前缀,而且与之相关联的设置函数(no_initrd,19902行)将
忽略所有可能已经传递给它们的参数――这正像ro,rw和debug被包含在
cooked_params中时所可能进行的工作一样。
19632:这个循环为cooked_params数组的处理工作和前面一个循环为raw_params数
组的处理工作相同。这两个循环(当然不包括循环使用的数组)间的唯一区别是本
循环在调用设置函数之前,使用get_options(19062行)处理line中=sign后面的
部分。简单的说,get_options使用10个负整数填充ints[1]到ints[10]。ints[0]
中是ints中使用元素的个数――也就是,它记录了存储在ints中的
intsget_options数量。接着这个数组将被传递给设置函数,该设置函数则会按照
自己喜欢的方式对该数组内容进行解释。
19640:返回0,说明line中所包含的内核选项不能被函数理解。
profile_setup
19076:profile_setup是checksetup调用的设置函数的一个完美的例子:这个函数
十分短小,使用ints参数处理了部分内容。而且到目前为止你也应该对它的目的有
了一定了解。正如前面提到的一样,用户可以在启动的时候设置prof_shift的值―
―好,这里正是它的实现方式。当内核启动过程提供profile=选项时,就调用
profile_setup函数。前缀字符串和函数在19235行被联系在一起。注意这是在
cooked_params中,因此profile_setup取得的是处理过的参数。
19079:如果参数中存在profile=的形式,就使用profile=后面的第一个数字作为
prof_shift的新值。选项给出的其它参数都被简单的忽略了。
19081:如果给出了profile=选项,但是没有为它提供参数,prof_shift的缺省值
就是2。这个缺省值有些奇怪,因为我们已经知道,这意味着使用四分之一的内核
可用内存来配置其余部分――这是一个很大的开销。但是另一方面,使用这些内存
有助于更精确的定位问题热点――只有很少的几条指令存在不确定性,这样应该比
较容易地把问题限制在一两行源程序代码内。那张图也并不是像我所画的那样简单
:因为图中只描述了内核代码,这种开销还不到内核所有内存空间的25%,但是对
于所覆盖的代码量来说却并不止25%。
init
init从许多方面看都是一个非常特殊的进程。这是内核运行的第一个用户进程,它
要负责触发其它必需的进程以使系统作为一个整体进入可用的状态。这些工作由
/etc/inittab文件控制,通常包括设置getty进程以接受用户登录;建立网络服务
,例如FTP和HTTP守护进程;等等。如果没有这些进程,用户就不可能完成多少工
作,这样成功启动内核就显得没有多大意义了。
这种设计的另外一个重要的副作用是init是系统中所有进程的祖先。init产生
getty进程,getty进程产生login进程,login进程产生你自己的shell,使用自己
的shell,可以产生每一个你运行的进程。在所有的结果中,这有助于确保内核进
程表中的所有项最终都能够得到处理。进程结束以后将其清除(回收)的工作首先
应由其父进程完成;如果父进程已经退出,那么祖父进程就要担负起这种责任;如
果祖父进程已经退出,那么曾祖父进程就要担负起这种责任,周而复始。通过这种
方式,从不退出的init进程就可能要负责回收其它进程。
因此,为了确保这些重要的工作都能正确执行,内核初始化进程所需要做的最后一
步工作就是创建init进程,接下来就加以描述。
init
20044:unused参数来源于该函数的非常规调用。init函数――不要和init进程搞
混了,后者是它随后要创建的――作为内核线程开始生命周期,一个作为内核的一
部分运行的进程。(如果你编写过多线程的程序,这里的内核线程可能会同你所已
经知道的线程意义有所不同――在那种意义下,它不是一个内核线程。)实际上,
init函数就像是新进程使用的剥离出来了的main函数,unused参数是一个独立的指
针,其值指向为给定进程所提供的信息――这比通常使用argc,argv和envp参数传
递的信息要少得多。init函数碰巧不需要额外的信息,因此这个参数命名为
unused,就是要强调这一点。
    为了确保在这一点上你不会产生困惑,我们在这里再对整个机制进行扼要重复:
init函数是内核的一部分;它在内核中作为内核的一个独立的执行部分运行;也就
是说,无论从哪个方面看它都是内核代码。但是,init进程就不是这样了。在某些
方面,init进程是一个特殊的进程,但是不属于内核本身;其代码存储在磁盘上单
独的可执行映像中,这和其它程序一样。因为init函数后来产生init进程,而它自
己又恰好作为进程运行,这样就很容易产生混淆。
    因为idle进程已经占据了进程ID号(PID)0,init(当然是init)就被赋值为下
一个可用的PID,也就是1。(进程ID在第7章中讨论。)内核重复假定PID为1的进
程是init,因此这种特性在没有充分地相互作用,也就是没有同步地进行修改的情
况下是不能改变的。
20046:调用lock_kernel(17492行对应UP版本;10174行对应SMP版本)执行后续
几行,而不会受到其它会受到随后工作的影响的内核模块的干扰。内核锁随后在
20053行被释放。
20047:调用do_basic_setup(19916行)初始化总线并随同其它工作产生一些其它
内核线程。
20052:内核已完全完成初始化了,因此free_initmen(7620行)可以舍弃内核的
.text.init节的函数和.data.init节的数据。所有使用__initfunc标记过的函数和
使用__initdata标记过的数据现在都不能使用了,它们曾经获得的内存现在也可能
重新用于其它目的了。
20055:如果可能,打开控制台设备,这样init进程就拥有一个控制台,可以向其
中写入信息,也可以从其中读取输入信息。实际上init进程除了打印错误信息以外
,并不使用控制台,但是如果调用的是shell或者其它需要交互的进程,而不是
init,那么就需要一个可以交互的输入源。如果成功执行open,/dev/console就成
为init的标准输入源(文件描述符0)。
20059:调用dup打开/dev/console文件描述符两次,这样,init就也使用它供标准
输出和标准错误使用(文件描述符1和2)。假设20055行的open成功执行(正常情
况),init现在就有三个文件描述符――标准输入、标准输出以及标准错误――全
都加载在系统控制台之上。
20067:如果内核命令行中给出了到init的直接路径(或者别的可替代的程序),
现在就试图执行init。
    因为当execve成功执行目标程序时并不返回,只有当前面的所有处理过程都失败
时,才能执行相关的表达式。接下来的几行在几个地方查找init,按照可能性由高
到低的顺序依次是:首先是/sbin/init,这是init标准的位置;接下来是两个可能
的位置,/etc/init和/bin/init。
20072:这些是init可能出现的所有地方。如果现在还没有出现,init就无法找到
它的这个同名者了,机器可能就崩溃了。因此,它就会试图建立一个交互的shell
(/bin/sh)来代替。现在init最后的希望就是root用户可以修复这种错误并重新
启动机器。(可以肯定,root也正是希望如此。)
20073:init甚至不能创建shell――一定是发生了什么问题!好,按照它们所说的
,当所有其它情况都失败时,调用panic(25563行)。这样内核就会试图同步磁盘
,确保其状态一致,然后暂停进程的执行。如果超过了内核选项中定义的时间,它
也可能会重新启动机器。
回复

使用道具 举报

 楼主| 发表于 2003-10-27 09:33:51 | 显示全部楼层
Linux内核注释—— 第5章 系统调用

大部分介绍Unix内核的书籍都没有仔细说明系统调用,我认为这是一个失误。实际
上,我们实际需要的系统调用现在已经十分完美。因此,从某种意义上来说,研究
系统调用的实现是无意义的——如果你想为Linux内核的改进贡献自己的力量,还
有其它许多方面更值得投入精力。
然而,对于我们来说,仔细研究少量系统调用是十分值得的。这样就有机会初步了
解一些概念,这些概念将随本书发展而进行详细介绍,例如进程处理和内存。这使
得你可以趁机详细了解一下Linux内核编程的特点。这包括一些和你过去在学校里
(或工作中)所学的内容不同的方法。和其它编程任务相比,Linux内核编程的一
个显著特点是它不断同三个成见进行斗争——这三个成见就是速度、正确和清晰—
—我们不可能同时获取这三个方面…至少并不总是能够。
什么是系统调用
系统调用发生在用户进程(比如emacs)通过调用特殊函数(例如open)以请求内
核提供服务的时候。在这里,用户进程被暂时挂起。内核检验用户请求,尝试执行
,并把结果反馈给用户进程,接着用户进程重新启动,随后我们就将详细讨论这种
机制。
系统调用负责保护对内核所管理的资源的访问,系统调用中的几个大类主要有:处
理I/O请求(open,close,read,write,poll等等),进程(fork,execve,
kill,等等),时间(time,settimeofday等等)以及内存(mmap,brk,等等)
的系统调用。几乎所有的系统调用都可以归入这几类中。
然而,从根本上来说,系统调用可能和它表面上有所不同。首先,在Linux中,C库
中对于一些系统调用的实现是建立在其它系统调用的基础之上的。例如,waitpid
是通过简单调用wait4实现的,但是它们两个都是作为独立的系统调用说明的。其
它的传统系统调用,如sigmask和ftime是由C库而不是由Linux内核本身实现的;即
使不是全部,至少大部分是如此。
当然,从技巧的一面来看这是无害的——从应用程序的观点来看,系统调用就和其
它的函数调用一样。只要结果符合预计的情况,应用程序就不能确定是否真正使用
到了内核。(这种处理方式还有一个潜在的优点:用户可以直接触发的内核代码越
少,出现安全漏洞的机会也就越少。)但是,由于使用这种技巧所引起的困扰将会
使我们的讨论更为困难。实际上,系统调用这一术语通常被演讲者用来说明在第一
个Unix版本中的任何对系统的调用。但是在本章中我们只对“真正”的系统调用感
兴趣——真正的系统调用至少包括用户进程对部分内核代码的调用。
系统调用必须返回int的值,并且也只能返回int的值。为了方便起见,返回值如果
为零或者为正,就说明调用成功;为负则说明发生了错误。就像老练的C程序员所
知道的一样,当标准C库中的函数发生错误时会通过设置全局整型变量errno指明发
生错误的属性,系统调用的原理和它相同。然而,仅仅研究内核源程序代码并不能
够获得这种系统调用方式的全部意义。如果发生了错误,系统调用简单返回自己所
期望的负数错误号,其余部分则由标准C库实现。(正常情况下,用户代码并不直
接调用内核系统函数,而是要通过标准C库中专门负责翻译的一个小层次(thin
layer)实现。)我们随便举一个例子,27825行(sys_nanosleep的一部分)返回
-EINVAL指明所提供的值越界了。标准C库中实际处理sys_nanosleep的代码会注意
到返回的负值,从而设置errno和EINVAL,并且自己返回-1给原始的调用者。
在最近的内核版本中,系统调用返回负值偶尔也不一定表示错误了。在目前的几个
系统调用中(例如lseek),即使结果正确也会返回一个很大的负值。最近,错误
返回值是在-1到-4095范围之内。现在,标准C库实现能够以更加成熟和高级的方式
解释系统调用的返回值;当返回值为负时,内核本身就不用再做任何特殊的处理了

中断、内核空间和用户空间
我们将在第6章中介绍中断和在第8章中介绍内存时再次明确这些概念。但是在本章
中,我们只需要粗略地了解一些术语。
第一个术语是中断(interrupt),它来源于两个方面:硬件中断,例如磁盘指明
其中存放一些数据(这与本章无关);和软件中断,一种等价的软件机制。在x86
系列CPU中,软件中断是用户进程通知内核需要触发系统调用的基本方法(出于这
种目的使用的中断号是0x80,对于Intel芯片的研究者来说更为熟悉的是INT 80)
。内核通过system_call(171行)函数响应中断,这一点我们马上就会介绍。
另外两个术语是内核空间(kernel space)和用户空间(user space),它们分别
对应内核保留的内存和用户进程保留的内存。当然,多用户进程也经常同时运行,
而且各个进程之间通常不会共享它们的内存,但是,任何一个用户进程使用的内存
都称为用户空间。内核在某一个时刻通常只和一个用户进程交互,因此实际上不会
引起任何混乱。
由于这些内存空间是相互独立的,用户进程根本不能直接访问内核空间,内核也只
能通过put_user(13278行)和get_user(13254行)宏和类似的宏才可以访问用户
空间。因为系统调用是进程和进程所运行的操作系统之间的接口,所以系统调用需
要频繁地和用户空间交互,因此这些宏也就会不时的在系统调用中出现。在通过数
值传递参数的情况下并不需要它们,但是当用户把指针——内核通过这个指针进行
读写——传递给系统调用时,就需要这些宏了。
如何激活系统调用
系统调用的的激活有两种方法:system_call函数和lcall7调用门(call gate)(
请参看135行)。(你可能听说过还有一种机制,syscall函数,是通过调用
lcall7实现的——至少在x86平台上是如此——因此,它并不是一个特有的方法。
)本节将细致地讨论一下这两种机制。
在阅读的过程中请注意系统调用本身并不关心它们是由system_call还是由lcall7
激活的。这种把系统调用和其实现方式区别开来的方法是十分精巧的。这样,如果
出于某种原因我们不得不增加一种激活系统调用的方法,我们也不必修改系统调用
本身来支持这种方法。
在你浏览这些汇编代码之前要注意这些机器指令中操作数的顺序和普通Intel的次
序相反。虽然还有一些其它的语法区别,但是操作数反序是最令人迷惑的。如果你
还记得Intel的语法:
mov eax, 0
(本句代码的意思是把常数0传送到寄存器EAX中)在这里应该写作:
mov1 $0, %eax
这样你就能够正确通过。(内核使用的语法是AT&T的汇编语法。在GNU汇编文档中
有更多资料。)
system_call
system_call(171行)是所有系统调用的入口点(这是对于内部代码来说的;
lcall7用来支持iBCS2,这一点我们很快就会讨论)。正如前面标题注释中说明的
一样,目的是为普通情况简单地实现直接的流程,不采用跳转,因此函数的各个部
分都是离散的——整体的流量控制已经因为要避免普通情况下的多分支而变得非常
复杂。(分支的避免是十分值得的,因为它们引起的代价非常昂贵。它们可以清空
CPU管道,使现存CPU的并行加速机制失效。)
图5.1  system_call的流程控制

图5.1显示了作为system_call的一部分出现的分支目标标签以及它们之间的流程控
制方向,该图可以在你阅读本部分讨论内容时提供很大的帮助。图中system_call
和restore_all两个标签比其它标签都要大,因为这两处是该函数正常的出口点和
入口点;然而,还有另外两个入口点,这一点在本章的后续内容中很快就可以看到

system_call是由标准C库激活的,该标准C库会把自己希望传递的参数装载到CPU寄
存器中,并触发0x80软件中断。(system_call在这里是一个中断处理程序。)内
核记录了软件中断和6828行的system_call函数的联系(SYSCALL_VECTOR是在1713
行宏定义为0x80的)。
system_call
172:  system_call的第一个参数是所希望激活的系统调用的数目;它存储在EAX
寄存器中。system_call还允许有多达四个的参数和系统调用一起传送。在一些极
其罕见的情况下使用四个参数的限制是负担繁重的,通常可以建立一个指向结构的
指针参数来巧妙地完成同样功能,指针指向的结构中可以包含你所需要的一切信息

    随后可能需要EAX值的一个额外拷贝,因此通过将其压栈而保存起来;这个值就是
218行的ORIG_EAX(%esp)表达式的值。
173:   SAVE_ALL宏是在85行定义的;它把所有寄存器的值压入CPU的堆栈。随后,
就在system_call返回之前,使用RESTALL_ALL(100行)把栈中的值弹出。在这中
间,system_call可以根据需要自由使用寄存器的值。更重要的是,任何它所调用
的C函数都可以从栈中查找到所希望的参数,因为SAVE_ALL已经把所有寄存器的值
都压入栈中了。
    结果栈的结构从26行开始描述。象0(%esp)和4(%esp)一样的表达式指明了堆
栈指针(ESP寄存器)的一种替换形式——分别表示ESP上的0字节,ESP上的4字节
,等等。特别要注意的是在前面一行中压入堆栈的EAX的拷贝已经变成本标题注释
作为orig_eax所描述的内容;它们是由SAVE_ALL压入寄存器之上的堆栈的(
orig_eax之上的寄存器在这里早已就绪了)。
    还需注意:这可能有点令人迷惑——由于我们调用orig_eax时EAX的拷贝已经压入
了堆栈,它是否有可能在其它寄存器下面而不是在其它寄存器上面呢?答案既是肯
定的,也是否定的。x86的堆栈指针寄存器ESP在有数据压入堆栈时会减少——堆栈
会向内存低地址发展。因此,orig_eax逻辑上是在其它值的下面,但是物理上却是
在其它值的上面。
    从51行开始的一系列宏有助于使这些替换更容易理解。例如,EAX(%esp)就和
18(%esp)相同——然而前一种方法通过表达式引用存储在堆栈中的EAX寄存器副
本的决定可以使整个过程更加简单。
174:   从EBX寄存器中取得指向当前任务的指针。完成这个工作的宏GET_CURRENT(
131行)对于在大部分代码中使用的C函数get_current(10277行)来说是一个无限
循环。
    此后,当看到类似于foo(%ebx)或者foo(%esp)的表达式时,这意味着这些的
代码正在引用代表当前进程的结构的字段——16325行的struct task_struct——
这在第7章中将对它进行更详细的介绍。(更确切的描述是, %ebx的置换在struct
task_struct中,%esp的置换在与struct task_struct相关联的struct pt_regs结
构中。但是这些细节在这里都并不重要。)
175:   检查(EAX中的)系统调用的数目是否超过系统调用的最大数量。(此处
EAX为一个无符号数,因此不可能为负值。)如果的确超过了,就向前跳转到
badsys(223行)。
177:   检测系统调用是否正被跟踪。如strace之类的程序为有兴趣的人提供了系统
调用的跟踪工具,或者额外的调试信息:如果能够监测到正在执行的系统调用,那
么你就可以了解到当前程序正在处理的内容。如果系统调用正被跟踪,控制流程就
向前跳转到tracesys(215行)。
179:   调用系统函数。此处有很多工作需要处理。首先,SYSMOL_NAME宏不处理任
何工作,只是简单的为参数文本所替换,因此可以将其忽略。sys_call_table是在
当前文件(arch/i386/kernel/entry.S)的末尾从373行开始定义的。这是一张由
指向实现各种系统调用的内核函数的函数指针组成的表。
    本行中第二对圆括号中包含了三个使用逗号分割开的参数(第一个参数为空);
这里就是实现数组索引的地方。当然,这个数组是以sys_call_table作为索引的,
这称为偏移(displacement)。这三个参数是数组的基地址、索引(EAX,系统调
用的数目)和大小,或者每个数组元素中的字节数——在这里就是4。由于数组基
地址为空,就将其当作0——但是它要和偏移地址,sys_call_table,相加,简单
的说就是sys_call_table被当作数组的基地址。本行基本上等同于如下的C表达式

    /* Call a function in an array of functions. */
    (sys_call_table[eax])();
    然而, C当然还要处理许多繁重的工作,例如为你记录数组元素的大小。不要忘
记,系统调用的参数早已经存储在堆栈中了,这主要由调用者提供给system_call
并使用SAVE_ALL把它们压栈。
180:   系统调用已经返回。它在EAX寄存器中的返回值(这个值同时也是
system_call的返回值)被存储起来。返回值被存储在堆栈中的EAX内,以使得
RESTORE_ALL可以迅速地恢复实际的EAX寄存器以及其它寄存器的值。
182:   接下来的代码仍然是system_call的一部分,它是一个也可以命名为
ret_from_sys_call和ret_from_intr的独立入口点。它们偶尔会被C直接调用,也
可以从system_call的其它部分跳转过来。
185:   接下来的几行检测“下半部分(bottom half)”是否激活;如果激活了,
就跳转到handle_bottom_half标号(242行)并立即开始处理。下半部分是中断进
程的一部分,将在下一章中讨论。
189:   检查该进程是否为再次调度做了标记(记住表达式$0就是常量0的系统简单
表示)。如果的确如此,就跳转到reschedule标号(247行)。
191:   检测是否还有挂起的信号量,如果有的话,下一行就向前跳转到
signal_return(197行)。
193:   restore_all标号是system_call的退出点。其主体就是简单的RESTORE_ALL
宏(100行),该宏将恢复早先由SAVE_ALL存储的参数并返回给system_call的调用
者。
197:   当system_call从系统调用返回前,如果它检测到需要将信号量传送给当前
的进程时,才会执行到signal_return。它通过使中断再次可用开始执行,有关内
容将在第6章中介绍。
199:   如果返回虚拟8086模式(这不是本书的主题),就向前跳转到
v86_signal_return(207行)。
202:   system_call要调用C函数do_signal(3364行,在第6章中讨论)来释放信号
量。do_signal需要两个参数,这两个参数都是通过寄存器传递的;第一个是EAX寄
存器,另一个是EDX寄存器。system_call(在200行)早已把第一个参数的值赋给
了EAX;现在,就把EDX寄存器和寄存器本身进行XOR操作,从而将其清0,这样
do_signal就认为这是一个空指针。
203:   调用do_signal传递信号量,并且跳回到restore_all(193行)结束。
207:   由于虚拟8086模式不是本书的主题,我们将忽略大部分v86_signal_return
。然而,它和signal_return的情况非常类似。
215:   如果当前进程的系统调用正由其祖先跟踪,就像strace程序中那样,那么就
可以执行到tracesys标号。这一部分的基本思想如同179行一样是通过
syscall_table调用系统函数,但是这里把该调用和对syscall_trace函数的调用捆
绑在一起。后面的这个函数在本书中并没有涉及到,它能够中止当前进程并通知其
祖先注意当前进程将要激活一个系统调用。
    EAX操作和这些代码的交错使用最初可能容易令人产生困惑。system_call把存储
在堆栈中的EAX拷贝赋给-ENOSYS,调用syscall_trace,在172行再从所做的备份中
恢复EAX的值,调用实际的系统调用,把系统调用的返回值置入堆栈中EAX的位置,
再次调用syscall_trace。
    这种方式背后的原因是syscall_trace(或者更准确的说是它所要使用到的跟踪程
序)需要知道在它是在实际系统调用之前还是之后被调用的。-ENOSYS的值能够用
来指示它是在实际系统调用执行之前被调用的,因为实际中所有实现的系统调用的
执行都不会返回-ENOSYS。因此,EAX在堆栈中的备份在第一次调用syscall_trace
之前是-ENOSYS,但是在第二次调用syscall_trace之前就不再是了(除非是调用
sys_ni_syscall的时候,在这种情况下,我们并不关心是怎样跟踪的)。218行和
219行中EAX的作用只是找出要调用的系统调用,这和无须跟踪的情况是一致的。
222:   被跟踪的系统调用已经返回;流程控制跳转回ret_from_sys_call(184行)
并以与普通的无须跟踪的情况相同的方式结束。
223:   当系统调用的数目越界时,就可以执行到badsys标号。在这种情况下,
system_call必须返回-ENOSYS(ENOSYS在82行将它赋值为38)。正如前面提到的一
样,调用者会识别出这是一个错误,因为返回值在-1到-4,095之间。
228:   在诸如除零错误(请参看279行)之类的CPU异常中断情况下将执行到
ret_from_exception标号;但是system_call内部的所有代码都不会执行到这个标
号。如果有下半部分是激活的,现在就是它在起作用了。
233:   处理完下半部分之后或者从上面的情况简单的执行下来(虽然没有下半部分
是激活的,但是同样也触发了CPU异常),就执行到了ret_from_intr标号。这是一
个全局符号变量,因此可能在内核的其它部分也会有对它的调用。
237:   被保存的CPU的EFLAGS和CS寄存器在此已经被并入EAX,因而高24位的值(其
中恰好包含了一位在70行定义的非常有用的VM_MASK)来源于EFLAGS,其它低8位的
值来源于CS。该行隐式的同时对这两部分进行测试以判断进程到底返回虚拟8086模
式(这是VM_MASK的部分)还是用户模式(这是3的部分——用户模式的优先等级是
3)。下面是近似的等价C代码:
    /* Mix eflags and cs in eax. */
    eax = eflags & ~0xff;
    eax |= cs & ~0xff
    /* Simultaneously test lower 2 bits
       * and VM_MASK bit. */
    if  (eax & (VM_MASK | 3))
       goto ret_with_reschedule;
    goto restore_all;           
238:   如果这些条件中有一个能得到满足,流程控制就跳转到
ret_with_reschedule(188行)标号来测试在system_call返回之前进程是否需要
再次调度。否则,调用者就是一个内核任务,因此system_call通过跳转到
restore_all (193行)来跳过重新调度的内容。
242:   无论何时system_call使用一个下半部分服务时都可以执行到
handle_bottom_half标号。它简单的调用第6章中介绍的C函数bottom_half(
29126行),然后跳回到ret_from_intrr(233行)。
248:   system_call的最后一个部分在reschedule标号之下。当产生系统调用的进
程已经被标记为需要进行重新调度时,就可以执行到这个标号;典型地,这是因为
进程的时间片已经用完了——也就是说,进程到目前为止已经尽可能的拥有CPU了
,应该给其它进程一个机会来运行了。因此,在必要的情况下就可以调用C函数
schedule(26686行)交出CPU,同时流程控制转回249行。CPU调度是第7章中讨论
的一个主题。
lcall7
Linux支持Intel二进制兼容规范标准的版本2(iBCS2)。(iBCS2中的小写字母i显
然是有意的,但是该标准却没有对此进行解释;这样看来似乎和现实的Intel系列
的CPU例如i386,i486等等是一致的。)iBCS2的规范中规定了所有基于x86的Unix
系统的应用程序的标准内核接口,这些系统不仅包括Linux,而且还包括其它自由
的x86 Unix(例如FreeBSD),也还包括Solaris/x86,SCO Unix等等。这些标准接
口使得为其它Unix系统开发的二进制商业软件在Linux系统中能够直接运行,反之
亦然(而且,近期新开发软件向其它Unix移植的情况越来越多)。例如,Corel公
司的WordPerfect的SCO Unix的二进制代码在还没有Linux的本地版本的
WordPerfect之前就可以使用iBCS2在Linux上良好地运行。
iBCS2标准有很多组成部分,但是我们现在关心的是这些系统调用如何协调一致来
适应这些迥然不同的Unix系统。这是通过lcall7调用门实现的。它是一个相当简单
的汇编函数(尤其是和system_call相比而言更是如此),仅仅定位并全权委托一
个C函数来处理细节。(调用门是x86 CPU的一种特性,通过这种特性用户任务可以
在安全受控的模式下调用内核代码。)这种调用门在6802行进行设定。
lcall7
136: 前面的几行将通过调整处理器堆栈以使堆栈的内容和system_call预期的相
同——system_call中的一些代码将会完成清理工作,这样所有的内容都可以连续
存放了。
145:   基于同样的思想,lcall7把指向当前任务的指针置入EBX寄存器,这一点和
system_call的情况是相同的。但是,它的执行方式却与system_call不同,这就比
较奇怪了。这三行可以等价地按如下形式书写:
    push1 %esp
    GET_CURRENT(%ebx)
    这种实现的执行速度并不比原有的更快;在将宏展开以后,实际上这还是同样的
三条指令以不同的次序组合在一起而已。这样做的优点是可以和文件中的其它代码
更为一致,而且代码也许会更清晰一些。
148:   取得指向当前任务exec_domain域的指针,使用这个域以获取指向其lcall7
处理程序的指针,接着调用这个处理程序。
    本书中并没有对执行域(execution domains)进行详细说明——但是简单说来,
内核使用执行域实现了部分iBCS2标准。在15977行你可以找到struct
exec_domain结构。default_exec_domain(22807行)是缺省的执行域,它拥有一
个缺省的lcall7处理程序。它就是no_lcall7(22820行)。其基本的执行方式类似
于SVR4风格的Unix,如果调用进程没有成功,就传送一个分段违例信号量(
segmentation violation signal)给调用的进程,。
152:   跳转到ret_from_sys_call标号(184行——注意这是在system_call内部的
)清除并返回,就像是正常的系统调用一样。
系统调用样例
现在你已经知道了系统调用是如何激活的,接下来我们将通过几个系统调用例子的
剖析来了解一下它们的工作方式。注意系统调用foo几乎都是使用名为sys_foo的内
核函数实现的,但是在某些情况下该函数也会使用一个名为do_foo的辅助函数。
sys_ni_syscall
29185:sys_ni_syscall的确是最简单的系统调用;它只是简单的返回ENOSYS错误
。最初的时候这可能显得没有什么作用,但是它的确是有用的。实际上,
sys_ni_syscall在sys_call_table中占据了很多位置——而且其原因并不只有一个
。开始的时候,sys_ni_syscall在位置0(374行),因为如果漏洞百出的代码错误
地调用了system_call——例如,没有初始化作为参数传递给system_call的变量—
—在这种偶然的变量定义中,0是最可能的值。如果我们能够避免这种情况,那么
在错误发生时就不用采取象杀掉进程一样的剧烈措施。(当然,只要允许有用工作
的进行,就不可能防止所有的错误。)这种使用表的元素0作为抵御错误的手段在
内核中被作为良好的经验而广泛使用。
    而且,你还会发现sys_ni_syscall在表中明显出现的地方就多达十几处。这些条
目代表了那些已经从内核中移出的系统调用——例如在418行,就代替了已经废弃
了的prof系统调用。我们不能简单地把另外的实际系统调用放在这里,因为老的二
进制代码可能还会使用到这些已经废弃了的系统调用号。如果一个程序试图调用这
些老的系统调用,但是结果却与预期的完全不同,例如打开了一个文件,这会比较
令人感到奇怪的。
    最后,sys_ni_syscall将占据表尾部所有未用的空间;这一点是在从572行到574
行的代码实现的,它根据需要重复使用这些项来填充表。由于sys_ni_syscall只是
简单返回ENOSYS错误号,对它的调用和跳转到system_call中的badsys标号作用是
相同的——也就是说,使用指向这些表项的系统调用号和在表外对整个表进行全部
索引具有相同的作用。因此,我们不用改变NR_syscalls就可以在表中增加(或者
删除)系统调用,但是其效果与我们真的对NR_syscalls进行了修改一样(不管怎
样,这都是由NR_syscalls所建立的限制条件所决定的)。
    到现在也许你已经猜到了,sys_ni_syscall中的“ni”并不是指Monty Python的
“说 ‘Ni’ 的骑士”;而是指“not implemented(没有实现)”这一相较而言
并不太诙谐的短语。
    对于这个简单的函数我们需要研究的另外一个问题是asmlinkage标签。这是为一
些gcc功能定义的一个宏,它告诉编译器该函数不希望从寄存器中(这是一种普通
的优化 )取得任何参数,而希望仅仅从CPU堆栈中取得参数。回忆一下我们前面提
到过system_call使用第一个参数作为系统调用的数目,同时还允许另外四个参数
和系统调用一起传递。system_call通过把其它参数(这些参数是通过寄存器传递
过来的)滞留在堆栈中的方法简单的实现了这种技巧。所有的系统调用都使用
asmlinkage标签作了标记,因此它们都要查找堆栈以获得参数。当然,在
sys_ni_syscall的情况下这并没有任何区别,因为sys_ni_syscall并不需要任何参
数。但是对于其它大部分系统调用来说这就是个问题了。并且,由于在其它很多函
数前面都有asmlinkage标签,我想你也应该对它有些了解。
sys_time
31394:sys_time是包含几个重要概念的简单系统调用。它实现了系统调用time,
返回值是从某个特定的时间点(1970年1月1日午夜UTC)以来经过的秒数。这个数
字被作为全局变量xtime(请参看26095行;它被声明为volatile型的变量,因为它
可以通过中断加以修改,这一点我们在第6章中就会看到)的一部分,通过
CURRENT_TIME宏(请参看16598行)可以访问它。
31400:该函数非常直接的实现了它的简单定义。当前时间首先被存储在局部变量
i中。
31402:如果所提供的指针tloc是非空的,返回值也将被拷贝到指针指向的位置。
该函数的一个微妙之处就在于此;它把i拷贝到用户空间中而不是使用
CURRENT_TIME宏来重新对其进行计算,这基于两个原因:
?   CURRENT_TIME宏的定义以后可能会改变,新的实现方法可能会由于某种原因而速
度比较慢,但是对于i的访问至少应该和CURRENT_TIME宏展开的速度同样快。
?   使用这种方式处理,确保结果的一致性:如果代码刚好执行到31400行和31402行
之间时时间发生了改变,sys_time可能把一个值拷贝到*tloc中,但是在结束之后
却返回另一个值。
    另外还有一个小的方面需要注意,此处的代码不使用&&来编写而是使用两个if,
这可能有一点令人奇怪。内核中采用这些看起来非常特殊的代码的一般原因都是由
于速度的要求,但是gcc为&&版本和两个if版本的代码生成的代码是等同的,因此
这里的原因就不可能是速度的要求——除非这些代码是在早期gcc版本下开发的,
这样才有些意义。
31403:如果sys_time不能访问所提供的位置(一般都是因为tloc无效),它就把
-EFAULT的值赋给i,从而在31405行返回错误代码。
31405:为调用者返回的i或者是当前时间,或者是-EFAULT。
sys_reboot
29298:内核中其他地方可能都没有sys_reboot的实现方法这样先进。其原因是可
以理解为:根据调用的名字我们就可以知道,reboot系统调用可以用来重新启动机
器。根据所提供的参数,它还能够挂起机器,关闭电源,允许或者禁止使用
Ctrl+Alt_Del组合键来重启机器。如果你要使用这个函数编写代码,需要特别注意
它上面的注释标题的警告:首先同步磁盘,否则磁盘缓冲区中的数据可能会丢失。

    由于它可能为系统引发的潜在后果,sys_reboot需要几个特殊参数,这一点马上
就会讨论。
29305:如果调用者不具有CAP_SYS_BOOT(14096行)权能(capability),系统就
会返回EPERM错误。权能在第7章中会详细讨论。现在,简单的说就是:权能是检测
用户是否具有特定权限的方法。
29309:在这里,这种偏执的思想充分发挥了作用。syst_reboot根据从16002到
16005行定义的特殊数字检测参数magic1和magic2。这种思想是如果sys_reboot在
某种程度上是被偶然调用的,那么就不太可能再从由magic1和magic2组成的小集合
中同时提取值。注意这并不意味着这是一个防止粗心的安全措施。
    顺便说一下,这些特殊数字并不是随机选取的。第一个参数的关系是十分明显的
,它是“感受死亡(feel dead)”的双关语。后面的三个参数要用十六进制才能
了解它们全部的意思:它们分别是0x28121969,0x5121996,0x16041998。这似乎
代表Linus的妻子(或者就是Linus自己)和他两个女儿的生日。由此推论,当
Linus和他的妻子养育了更多儿女的时候,重启动需要的特殊参数可能在某种程度
上会增加。不过我想在他们用尽32位可能空间之前,他的妻子就会制止他的行为了

29315:请求内核锁,这样能保证这段代码在某一时间只能由一个处理器执行。使
用lock_kernel/unlock_kernel函数对所保护起来的任何其它代码对其它CPU都同样
是不可访问的。在单处理器的机器中,这只是一个no-op(不处理任何事情);而
详细讨论它在多处理器上的作用则是第10章的内容。
29317:在LINUX_REBOOT_CMD_RESTART的情况中,sys_reboot调用一系列基于
reboot_notifier_list的函数来通知它们系统正在重新启动。正常情况下,这些函
数都是操作系统关闭时需要清除的模块的一部分。这个列表函数似乎并不在内核中
的其它地方使用——至少在标准内核发行版本中是这样,也许此外的其它模块可能
使用这个列表。不管怎样,这个列表的存在可以方便其他人使用。
    LINUX_REBOOT_CMD_RESTART和其它cmd识别出的值从16023行开始通过#define进行
宏定义。这些值并没有潜在的意义,选用它们的简单原因是它们一般不会发生意外
并且相互之间各不相同。(有趣的是,LINUX_REBOOT_CMD_OFF是零,这是在意外情
况下最不可能出现的一个值。但是,由于LINUX_REBOOT_CMD_OFF简单的禁止用户使
用Ctrl+Alt+Del重新启动机器,它就是一种“安全”的意外了。)
29321:打印警告信息以后,sys_reboot调用machine_restart(2185行)重启机器
。正如你从2298行中所看到的一样,machine_restart函数从来不会返回。但是不
管怎样,对于machine_restart的调用后面都跟着一个break语句。
    这仅仅是经典的良好的编程风格吗?的确如此,但是却又不仅仅如此。文件
kernel/sys.c的代码是属于体系结构无关部分的。但是machine_restart,它显然
是体系结构所特有的,属于代码的体系结构特有的部分(
arch/i386/kernel/process.c)。因而对于不同的移植版本也有所不同。我们并不
清楚以后内核的每个移植版本的实现都不会返回——例如,它可能调度底层硬件重
启但是本身要仍然持续运行几分钟,这就需要首先从函数中返回。或者更为确切的
说法是,由于某些特定的原因,系统可能并不总是能够重启;或许某些软件所控制
的硬件根本就不能重启。在这种平台上,machine_restart就应该可以返回,因此
体系结构无关的代码应该对这种可能性有所准备。
    针对这个问题,正式的发行版本中都至少包含一个退出端口,使
machine_restart函数可以从这个端口返回:m68k端口。不同的基于m68k的机器支
持的代码也各不相同,由于本书主要是针对x86的,我不希望花费过多的时间来解
析所有的细节。但是这的确是可能的。(在其它情况下,machine_restart简单进
入一个无限循环——既不重新启动机器,也不返回。但是这里我们担心的是需要返
回的情况。)
    因此,我们毕竟是需要break的。前面看起来只是简单的习惯甚至是偏执的思想在
这里为了内核的移植性已经变成必须的了。
29324:接下来的两种情况分别允许和禁止臭名卓著的Ctrl+Alt+Del组合键(这三
个组合键也被称为“Vulcan神经收缩(Vulcan nerve pinch)”,“黑客之手(
hacker’s claw)”,“三指之礼(three-fingered salute)”,我最喜欢后面
这个)。这些只是简单的设置全局C_A_D标志(在29160行定义,在29378行检测)

29332:这种情况和LINUX_REBOOT_CMD_RESTART类似,但只是暂停系统而不是将其
重新启动。两者之间的一个区别是它调用machine_halt(2304行)——这是x86上
的一条no-op指令,但是在其它平台上却要完成关闭系统的实际工作——而不是调
用machine_restart。并且它会把machine_halt不能使之暂停的机器转入低功耗模
式运行。它使用do_exit(23267行)杀死内核本身。
29340:到现在为止,这已经是一种比较熟悉的模式了。这里,sys_reboot关闭机
器电源,除了为可以使用软件自行关闭电源的系统调用machine_power_off(2307
行)之外,其它的应该和暂停机器情况完全相同。
29348:LINUX_REBOOT_CMD_RESTART2的情况是已建立主题的一个变种。它接收命令
,将其作为ASCII字符串传递,该字符串说明了机器应该如何关闭。字符串不会由
sys_reboot本身来解释,而是使用machine_restart函数来解释;因而这种模式的
意义,如果有的话,就是这些代码是平台相关的。(我使用“如果有”的原因是启
动机器——特别是在x86中——一般只有一种方法,因此其它的信息都可以被
machine_restart忽略。)
29365:调用者传递了一个无法识别的命令。sys_reboot不作任何处理,仅仅返回
一个错误。因此,即使由magic1和magic2传递给sys_reboot正确的magic数值,它
也无须处理任何内容。
29369:一个可识别的命令被传递给sys_reboot。如果流程执行到这里,它可能就
是两个设置C_A_D的命令之一,因为其它情况通常都是停止或者重新启动机器。在
任何情况下,sys_reboot都简单把内核解锁并返回0以表示成功。
sys_sysinfo
24142:一个只能返回一个整型值的系统调用。如果需要返回更多的信息,我们只
需要使用类似于在系统调用中传递多于四个参数时所使用的技巧就可以了:我们通
过一个指向结构的指针将结果返回。收集系统资源使用情况的sysinfo系统调用就
是这种函数的一个样例。
24144:分配并清空一个struct sysinfo结构(15004行)以暂时存储返回值。
sys_sysinfo可以把结构中的每个域都独立地拷贝出来,但是这样会速度很慢、很
不方便,而且必然不容易阅读。
24148:禁止中断。这在第6章中会有详细的介绍;作为目前来说,我们只要说明这
种模式在使用的过程中能够确保sys_sysinfo正在使用的值不会改变就足够了。
24149:struct sysinfo结构的uptime域用来指明系统已经启动并运行了的秒数。
这个值是使用jiffies(26146行)和HZ来计算的。jiffies计算了系统运行过程中
时钟的滴答次数;HZ是系统相关的一个参数,它十分简单,就是每秒内部时钟滴答
的次数。
24151:数组avenrun(27116行)记录了运行队列的平均长度——也就是等待CPU的
平均进程数——在最后的1秒钟,5秒钟和15秒钟。calc_load(27135行)周期性的
重复计算它的值。由于内核中是要严格禁止浮点数运算的,所以只能通过计算变化
的次数这一修正值来计算。
24155:同样记录系统中当前运行的进程数。
24158:si_meminfo(07635行)写入这个结构中的内存相关成员,si_swapinfo(
38544行)写入与虚拟内存相关的部分。
24161:现在整个结构都已经全部填充了。sysinfo试图将其拷贝回用户空间,如果
失败就返回EFAULT,如果成功就返回0。
回复

使用道具 举报

 楼主| 发表于 2003-10-27 09:36:59 | 显示全部楼层
Linux内核注释—— 第6章 信号量,中断

信号量(Signal)是进程间通讯(IPC)的一种形式——是一个进程给另一个进程
发送信息的方法。但是信息不可能很多——一个信号量不可能携带详细的信息,即
使是传送者的身份也不能被传递;唯一能够确定的事实是信号量的确被发送了。(
然而和经典信号量不同,POSIX实时信号量允许传送稍微多一点的信息。)实际上
,信号量对于双向通讯是没有用处的。还有,根据某些限定,信号量的接受者不必
以任何方式作出响应,甚至可以直接忽略大部分信号量。
虽然有这么多的限制,然而信号量仍然是一种功能强大的十分有用的机制——勿庸
置疑,这是Unix IPC中使用最频繁的机制。每当进程退出或者废弃一个空指针时,
每当使用Ctrl+C键终止程序运行时,都要传递信号量。
第9章会更详细的讨论IPC机制。对于本章的讨论来说,信号量的内容就足够讨论了

正如在Linux内核本身的代码注释中所说明的一样,中断(Interrupt)对于内核来
说和信号量是类似的。中断一般都是从磁盘之类的硬件设备送往内核,用以提示内
核该设备需要加以注意。一个重要的硬件中断源就是定时器设备,它周期性地通知
内核已经通过的时间。如同第5章中阐述的一样,中断也可以由用户进程通过软件
产生。
在本章中,我们首先讨论一下Linux中信号量和中断的实现,最后再浏览一下
Linux的时间处理方式。
虽然内核对代码的要求标准非常严格,本章所涉及的代码仍然特别清晰明白。本章
使用的一般方法是首先介绍相关的数据结构和它们之间的关系,接下来讨论操纵和
查询它们的函数。
锁的概述
锁的基本思想是限制对共享资源的访问——共享资源包括共享的文件,共享的内存
片,以及在一次只能由一个CPU执行的代码段。概括的说,在单处理器上运行的
Linux内核并不需要锁,这是因为在编写Linux内核时就已经注意到要尽量避免各种
可能需要锁的情况了。但是,在多处理器机器上,一个处理器有时需要防止其它处
理器对它的有害的介入。
include/asm-i386/spinlock.h文件(从12582行开始)并不使用难看的#ifdef把所
有对锁函数的调用封装起来,它包含一系列对单处理器平台(UP)基本为空的宏,
然而在多处理器平台(SMP)上这些宏将展开成为实际代码。因而内核的其它代码
对UP和SMP(当涉及到这种特性时)都是相同的,但是它们两个的效果却是迥然不
同的。
第10章中涉及SMP的部分会对锁做深入的介绍。但是,由于你在代码中将到处都能
够看到对锁宏的调用,特别是在本章所讨论到的代码中这一点尤为明显,所以你应
该首先对宏的用途有初步了解——以及为什么现在在大多数情况下我们都可以安全
地将其忽略(我们将在讨论的过程中对其中的异常情况进行说明)。
信号量
Linux内核将信号量分为两类:
?   非实时的(Nonrealtime)——大部分是些传统的信号量,例如SIGSEGV,
SIGHUP和SIGKILL。
?   实时的(realtime)——由POSIX 1003.1b标准规定,它们同非实时信号量有细
微的区别。特别是实时信号量具有进程可以配置的意义——就像是非实时信号量
SIGUSR1和SIGUSR2一样——额外的信息能够和这些信号量一起传送。它们也会排队
,因此如果在第一个信号量处理完成之前有多个信号量实例到达,所有的信号量都
能够被正确传送;这对于非实时信号量则是不可能的。
在第7章中我们将会对实时性对于Linux内核的意义进行更详细的介绍——特别是实
时性所不能够说明的内容。
信号量数目的宏定义从12048行开始。实时信号量的数目在SIGRTMIN和SIGRTMAX(
分别在12087行和12088行)所定义的范围之内。
数据结构
本节讨论信号量代码使用的最重要的数据结构。
sigset_t
12035:sigset_t表示信号量的集合。根据使用地点的不同,它的意思也不同——
例如,它可能记录着正在等待某一个进程的信号量(如16425行struct
task_struct的signal成员)的集合,也可能是某个进程已经请求阻塞了的信号量
(如同一行中定义的同一结构的blocked成员)的集合。随着本书的进行,我们会
逐渐看到这些类似的应用。
12036:sigset_t的唯一一个组成部分是一组unsigned long(无符号长整型数),
其中的每一位都代表一个信号量。注意到无符号长整型类型在整个内核代码中是作
为一个字来处理的,这和你所希望的可能有所出入——即使是在当前x86 CPU的讨
论中,有时候字也被用于说明16位类型。由于Linux是一个真32位操作系统,将32
位看作是一个字在绝大多数情况下是正确的。(将Linux称为真32位操作系统也有
一些不准确,因为在64位CPU上它也是一个真64位操作系统。)
    这个数组的大小_NSIG_WORDS在12031行直接计算。(_NSIG_BPW中的“BPW”是“
bits per word(每字位数)”的缩写。)在不同的平台上,_NSIG_WORDS的大小从
1(Alpha平台中)到4(MIPS平台中)不等。如你所见,在x86平台中,该值正好是
2,这意味着在x86平台上2个无符号数就可以包含足够的位数来代表所有Linux使用
的信号量。
struct sigaction
12165:struct sigaction代表信号量到达时进程应该执行的动作。它被封装在
struct k_sigaction(12172行)结构中,而该结构又是被封装在struct
signal_struct 结构中的,后者是struct task_struct结构的sig成员所指向的一
个实例(16424行)。如果这个指针为空,进程就会退出而不必接受任何信号量。
否则,每个进程对于每个信号量数目都需要若干_NSIG struct sigaction结构和一
个struct sigaction结构。
12166:sa_handler(__sighandler_t类型——一个在12148行定义的函数指针类型
)描述了进程希望处理信号量的方式。其值可以是下面中的一个:
?   SIG_DFL(12151行)申请处理信号量的缺省操作,不管该操作是什么——这是由
信号量所决定的。注意它和NULL是等同的。
?   SIG_IGN(12153行)意味着信号量应该忽略。但是,并不是所有的信号量都可以
被忽略的。
?   所有的其它值都是在信号量到达时所需要调用的用户空间函数的地址。
12167:sa_flags进一步调整信号量处理代码所完成的工作。可能的标志集合从
12108行开始定义。这些标志允许用户代码在信号量实例发送以后(或者保留用户
定制的操作时)请求恢复缺省操作,等等。这一点在宏定义块前面的标签注释中已
经说明了。
12168:sa_restorer是本书中所没有涉及的一些信号量处理代码细节所使用的。
12169:sa_mask是一系列其它信号量的集合,进程在处理这些信号量的过程中可能
需要进行锁定。例如,如果一个进程在处理SIGCHLD的时候希望锁定SIGHUP和
    SIGINT,进程的第SIGCHLD个sa_mask就会对与SIGHUP和SIGINT相关的位进行置位

siginfo_t
11851:struct siginfo(也称为siginfo_t)是伴随着信号量,特别是在实时信号
量,所传递的额外信息。
11852:勿庸置疑,si_signo是信号量的数目。
11853:si_errno应该是信号量传递时传送者的errno的值,这样接收者就可以对它
进行检测。内核本身并不关心这个值;当在某些情况下需要设置这个值时,内核将
其设置为0。我推测如果这样,即使调用者没有设置这个值,它们仍然会发现
si_error的值被设为已知状态。
11854:si_code记录了信号量的来源(不是发送者的进程ID号,也就是PID——它
在别处记录)。有效的信号量来源在11915行及其随后部分使用宏进行了定义。
11856:该结构的最后一部分是union类型的;该union类型依赖于si_code的值。
11857:union的第一部分是_pad,它将siginfo_t的长度扩展填充为128*sizeof(
int)字节(在x86平台上一共是512个字节)。留意一下这个数组的大小,也就是
SI_PAD_SIZE(11849行),代表了该结构的前三个成员——如果增加了更多的成员
,SI_PAD_SIZE就需要进行相应修改。
struct signal_queue
17132:struct signal_queue结构用来确保所有的实时信号量都被正确传送了,如
果可能,每一个都包含着额外信息(siginfo_t)。如同后面你将会看到的一样,
内核会为每个进程都设置一个队列,用来存放该进程的挂起的实时信号量。这个队
列类型本身很小,仅仅由一个指向下一个节点的指针和siginfo_t本身组成。
应用函数
有关信号量的一个最重要的数据结构是sigset_t,它是由一系列在
include/linux/signal.h文件中定义的简单函数所操纵的,这些函数的定义从
17123行开始。在x86平台上,这些相同的函数可以——而且已经——使用汇编语言
更加有效的实现了;这些更高效的版本从12204行开始。(m68k端口是唯一一个例
外的端口,它使用体系结构特有的代码实现。)由于平台无关的版本和x86特有的
版本都很重要,我们会对两者都加以介绍。
平台无关的sigset_t函数
配合sigset_t使用的平台无关的函数在include/linux/sigal.h文件中,从17123行
开始。称为“bitops”(位级的操作)的函数将在后面介绍。
sigaddset
17145:sigaddset把一个信号量加入集合——也就是说,它修改了集合中的一位。

17147:为了便于位操作,将基于0的信号量转化为基于1的信号量。
17149:如果信号量中填入一个无符号长整型数,恰当的位就会被设置。
17151:否则,sigaddset就需要绕很多弯路,首先装入恰当的数组元素,接着设置
该元素中相关位。
17148行的代码和该文件中后面的其它代码一样,第一次见到时可能会令人感到有
些困惑。在内核代码中,速度是压倒一切的因素。从而,也许你并不会看到类似于
下面的运行期间进行决定的代码:
    if (_NSIG_WORDS == 1)
       set->sig[0] |=1UL << sig;
    else
       set->sig[sig / NSIGBPW] |= 1UL << (sig % NSIGBPW);
而你看到的是类似于下面的在编译期间决定的代码:
    #if (_NSIG_WORDS == 1)
       set->sig[0] |=1UL << sig;
    #else
       set->sig[sig / NSIGBPW] |= 1UL << (sig % NSIGBPW);
    #endif
难道这样不会运行的更快些吗?不要忘了,if条件是能够在编译期间进行计算的,
因此预处理器可以使系统没有必要在运行期间执行检测工作。
当你认识到优化工作的实现方式时,这也就没有什么神秘的了。gcc的优化器的敏
锐程度足以注意到if表达式只有一个出口,因此它可以把那些不必要的代码移走。
作为内核“运行期间”版本的结果代码和“编译期间”的版本是等同的。
但是在我们使用优化器很糟糕的编译器时,基于预处理器的版本还会更好吗?这一
点并不确定。问题之一是,基于预处理器的(编译期间的)版本有一点更难懂。当
代码的复杂程度比前面的简单例子要高时,可读性的差别就会明显的显示出来。例
如,让我们考虑一下sigemptyset中的从17264行开始的switch。现在的switch类似
于这样:
swithc (_NSIG_WORDS) {
default:
  memset(set, 0, sizeof(sigset_t));
  break;
case 2:  set->sig[1] = 0;
case 1:  set->sig[0] = 0;
  break;
}
(请注意经周密考虑的case 2随case 1连续执行的情况。)为了更好的利用预处理
器而将其重写,它就可能类似于:
#if ((_NSIG_WORDS != 2)) && \ (_NSIG_WORDS != 1)
  memset(set, 0, sizeof(sigset_t));
#else /* (_NSIG_WORDS is 2 or 1). */
#if (_NSIG_WORDS == 2)
  set->sig[1] = 0;
#endif
  set->sog[0] = 0;
#endif /* _NSIG_WORDS test. */
gcc的优化器为两者产生的目标代码是相同的。你更希望读哪一种版本的源程序代
码呢?
另外,即使编译器的优化器并没有这么好——这种优化实在相当简单——那么编译
器就不可能生成很好的代码。不管我们提供多少帮助都注定是不够的,因此我们可
能要编写一些更容易读、更容易维护的代码——这是又一项工程技术的权衡。最后
,就象我们在前面的内容中已经看到而且还要不断看到的那样,使用除gcc之外的
编译器编译内核本身就是个挑战——增加一段gcc特有代码不会引起更多问题的。

sigdelset
17154:这些代码和sigaddset非常类似;区别在于这里从集合中删去了一位——就
是把相应的位设置为关。
sigismember
17163:这些代码和sigaddset也非常类似;这里是要测试某一位是否被设置。注意
到17167行可能和下面的这种写法有同样的好处:
    return set->sig[0] & (1UL << sig);
    这种写法与17169行非常相似。虽然这样能够和其它函数的编写风格更加一致,但
是这并不是什么改进。
    这些修改将对函数的行为方式稍有改动:它现在返回0或1,经过这种修改,就可
以在一个位被设置时返回其它的非0值。但是,这种改变不会终止没有退出的代码
,因为其调用者只关心返回值是否为0(它们并不特别在意是否为1)。
sigfindinword
17172:这个函数返回word中设置的第一个位的位置。函数ffz(在本书中没有涉及
)返回其参数中第一个0位的位置。在将位求补的字中的第一个0的位置——这正是
这个函数搜寻的内容——显然是原始顺序中的第一个1的位置。它从最小位0开始计
算。
sigmask
17177:最后,这个有用的sigmask宏简单的把信号量数目通过一个相应的位集合转
化为一个位掩码。
平台相关的sigset_t函数
即使平台无关的版本已经使用了简单有效的C代码,它也可以通过使用x86 CPU家族
的方便而功能强大的位集指令在x86平台上更加有效地实现,。这些函数中的大部
分都可以减少为单独的机器指令,因此这里的讨论也都很精简。
在x86平台(例如m68k)上平台无关的函数对于编译器甚至是不可见的。17126行包
含进了asm/signal.h文件,在x86上这个文件被分解为include/asm-i386/signal.
h,这都应该归功于设置文件所建立的符号链接。12202行定义了预处理器符号
__HAVE_ARCH_SIG_BITOPS,它消除了这些平台无关的函数的定义(请参看17140行
)。
sigaddset
12204:x86特有的使用btsl指令的sigaddset实现,它仅对操作数的一个位进行设
置。
sigdelset
12210:同样,这是x86特有的使用btrl指令的sigdelset实现,它对操作数的一个
位进行重置(清除)。
sigismember
12233:sigismember根据其sig参数是否是一个编译期常量表达式来选择实现方法
。文档中所没有说明的gcc编译器的强大的特殊参数__builtin_constant_p是一个
编译期操作符(就象sizeof一样),它能够报告是否可以在编译期间计算其参数值

    如果可以,sigismember使用__const_sigismember函数(12216行)完成这项工作
,因为它的大部分表达式都可以在编译期间计算。否则就使用更为普遍的版本
__gen_sigismember函数(12224行)来代替。更普遍的版本中使用的是x86的btl指
令,它需要测试其操作数中的某一位。
    注意到在编译期的常量合并和死锁代码消除通常意味着这样的完整测试只能在编
译期间执行——关键是sigismember要根据需要使用__const_sigismember或者
__gen_sigismember替换,在作为结果的目标代码中甚至完全看不出来根本就没有
对另一部分进行考虑。这样相当精简,难道不是吗?
sigmask
12238:x86特有的sigmask的实现,这与平台无关的版本是等同的。
sigfindinword
12240:最后,x86特有的sigfindinword实现只使用了x86的bsfl指令,它在自己的
操作数中寻找一个设置位。
设置函数
除了前面的那一组函数之外,还有一组对sigset_t执行设置操作的函数和宏。和前
面一组类似,这些函数使用__HAVE_ARCH_SIG_SETOPS预处理器符号保护起来。然而
现在没有一种体系结构能够提供自己独有的这些函数的实现,正因为如此,体系结
构无关的版本是现存的唯一版本。
_SIG_SET_BINOP
17184:我们希望定义的全部三个二进制操作——sigorsets,sigandsets和
signandsets——的实现方式从本质上来说是相同的。这个宏简单的把这三个函数
的共同代码分解出来,从而只给它们提供一个操作和一个名字。当然,这同C++模
版函数类似,不过这样我们不得不自己处理一些记录工作,而不能完全信任编译器
——这是我们使用C工作所付出的一部分代价。
17191:程序开始在sigset_t中全部四个字节的无符号长整型数的循环,同时对这
些操作数进行应用。这个循环是为了速度的原因而展开的——通过减少循环控制开
销来提高速度,这是很出名的一种增加速度的方法。然而,大多数情况下,这个循
环根本就不执行。例如,在x86平台上,编译器可以在运行期间就证实不会执行该
循环体,因为截断取整以后,_NSIG_WORDS/4的结果是0。(回忆一下_NSIG_WORDS
在x86平台上的值为2。)
17201:switch从循环末尾处理剩余工作的这行开始。如果在某些平台上
_NSIG_WORDS正好为6,那么该循环就可以执行一次,而且switch的情况2也可以被
执行。在x86平台上,循环永远不会执行;只有switch的情况2才可能执行。
    顺便说一下,我并不清楚为什么switch不和其类似的_SOG_SET_OP一样使用直接流
程的方式实现。通常情况下,现存的版本可以更充分的利用缓存(如果你试图重新
编写它,那么你就可以清楚的认识到这一点)——但是如果实际原因的确如此,那
么_SIG_SET_OP也应该使用相同的参数。
_SIG_SET_OP
17238:_SIG_SET_OP和_SIG_SET_BINOP类似,但是它使用的是一元操作而不是二元
操作,因此我们并不需要详细地介绍它。但是你应该注意的是,这只能使用一次—
—在17257行生成signotset——这和_SIG_SET_BINOP不同。因此,在某种程度上这
是不需要的——其实现者可以直接编写signotset,而不必借助_SIG_SET_OP,这并
没有产生任何重复代码。然而,二者生成的目标代码是相同的,这样如果我们以后
选择增加一元操作,意义也就不大了。
sigemptyset
17262:sigemptyset清空所提供的集合——要把其中的每一位都清空。(下面一个
函数sigfillset和这个函数功能相同,不过它要设置所有的位而不是清除所有的位
,因此我们就不再详细介绍了。)
17265:普通情况下使用memset把集合中的每一位都置为0。
17268:对于_NSIG_WORDS的一些比较小的值来说,简单的直接设置sigset_t的一两
个元素可能速度更快。在这里采用的就是这种直接流程实现。
sigaddsetmask
17292:该函数和下面的几个函数是更简单快速设置和读取最低的32位(或者根据
字的大小)信号量的一系列函数。sigaddsetmask简单地把mask所指定的位置位,
而不对剩余的位进行任何处理——这是一个集合的联合操作。
siginitset
17310:根据提供的掩码对最低32位(或者是别的)置位,并将其它位设置为0。下
面一个函数siginitsetinv(17323行)正好相反:它根据掩码的补数设置最低32位
(或者别的),并对其余的位置位。
传送信号量
从用户的观点来看,传送信号量相当简单:调用系统调用kill,该调用只需要进程
ID号和一个信号量。但是,正如本节中所显示的那样,其实现要复杂得多。
sys_kill
28768:sys_kill是系统调用kill的一个具有欺骗性的实现样例;真正的实际工作
是在kill_somethig-info中实现的,我们随后就将对这个方面进行研究。
sys_kill的参数是要传递的信号量sig和信号量的目的pid。就象你将看到的那样,
参数pid并不仅是进程ID。(PID和进程的其它概念都在第7章中详细介绍。)
28770:根据提供给sys_kill的信息声明并填充struct siginfo结构。特别要注意
的是si_code是SI_USER(因为只有用户进程才可以调用该系统调用;内核本身是不
会调用系统调用的,它更倾向于使用低层函数)。
28778:传递这些信息给kill_something_info,该函数处理实际的工作。
kill_something_info
28484:该函数的参数和sys_kill类似,但是增加了一项siginfo结构的指针。
28487:如果pid为0,就意味着当前进程希望把信号量传递给整个进程组,该工作
由kill_pg_info(28408行)完成。
28489:如果pid是-1,信号量(几乎)被送往系统中的每一个进程,这在下面的段
落中介绍。
28494:使用for_ech_task宏(在16898行宏定义,第7章中详细介绍)开始循环处
理现存进程列表的每一项。
28496:如果这不是idle进程(或init),就使用send_sig_info(28218行,后面
将会讨论)传递信号量。每次发现合适的任务时count的值都会增加,虽然
kill_something_info并不关心count的实际值,而是在意是否能够发现合适的进程
。如果所有试图发送信号量的努力都失败了,将记录失败的过程以使得
kill_something_info可以在28503行返回错误代码;如果发生了多次错误,则只返
回最后一次失败的情况。
28503:如果发现了合适的候选进程,kill_something_info就返回最近失败的错误
代码,或者成功就返回0。如果没有发现任何合适的候选进程,就返回ESRCH错误。

28504:其它负的pid(是负值,但不是-1)定义了接收信号量的进程组;pid的绝
对值是进程组号。和前面一样,kill_pg_info的使用就是出于这种目的。
28506:其它的所有可能性都已经进行了说明;pid必须为正数。在这种情况下,它
是信号量传送的目的进程的PID。这由kill_proc_info实现(28463行,很快就会讨
论)。
kill_pg_info
28408:这个函数给进程组中的每一个进程发送一个信号量和一个struct siginfo
结构。其函数体和前面介绍的kill_something_info类似,因此我只是简单介绍一
下。
28417:开始循环处理系统中的所有进程。
28418:如果进程在正确的进程组中,那么信号量就发送给它。
28427:如果信号量成功发送给任何进程,retval就设置为0,从而在28430行成功
返回。如果信号量不能被发往任何进程,那么要么是所给的进程组中没有进程,在
这种情况下,reval仍然会在28415行赋值为-ESRCH;或者kill_pg_info发送信号量
给一个或多个进程,但是每次都失败了,在这种情况下retval值为从
send_sig_info得到的最近错误代码。注意它和kill_something_info的细微区别,
后者如果发送信号量失败时就返回错误。但是这里的kill_pg_info即使在某些情况
下出错了,只要信号能成功地传递给任意进程,就会返回成功信息。
28430:在28410行中,如果进程组号无效,retval或者是如前所述的赋值,或者就
是-EINVAL。
kill_proc_info
28463:kill_proc_info是一个相当简单的函数,它把信号量和struct siginfo结
构传递给由PID定义的单个进程。
28469:通过所提供的PID查找相应的进程;如果成功find_task_by_pid(16570行
)返回一个指向该进程的指针,如果没有找到该进程就返回NULL。
28472:如果找到匹配进程,就使用send_sig_info把信号量传送给目的进程。
28474:返回错误指示,或者是在28470行由于没有发现匹配进程而返回-ESRCH,或
者是其它情况下从send_sig_info中返回的值。
send_sig_info
28218: 我们最后看的几个函数中最重要的显然是send_sig_info。这个函数使用
不同的方法装载进程并处理实际的工作。现在应该了解一下实际的工作是如何完成
的。send_sig_info将使用info指针(该指针也可能为NULL)指向额外信息的信号
量sig传送给t指针(调用者应该保证t不会为NULL)指向的进程。
28229:确保sig在范围之内。注意使用的是如下的测试
    sig > _NSIG
    而不是你可能预期的
    sig >= _NSIG
    这是因为信号量的计数是从1开始的,而不是从0开始的。因此虽然不存在对这个
信号量编号的定义,有效信号量的编号的标识符_NSIG本身也是有效的信号量编号

28233:这是另外一个严密性检查——实际上包含多个检验。基本的思想是检测信
号量的传送是否合法。虽然内核本身可以给任何进程传送信号量,但是除了在涉及
SIGCONT的情况之外,除root之外的用户都不能给其它用户的进程传送信号量。总
之,这个长长的if条件说明了如下问题:
?   (28233行)如果不存在补充信息,或者虽然存在补充信息,但是信号量来源于
用户而不是内核,并且…
?   (28235行)…信号量不是SIGCONT,或者虽然信号量是SIGCONT,但是并不是传
送给同一会话过程中的其它进程,并且…
?   (28237行和28238行)…发送者有效的用户ID既不是已经存储了的目标进程的用
户ID,也不是目标进程的当前用户ID,并且…
?   (28239行和28240行)…发送者的当前用户ID既不是已经存储了的目标进程的用
户ID,也不是目标进程的当前用户ID,并且…
?   (28241行)…此处不会允许用户超越普通许可(例如,由于用户是root)…那
么就不应该发送信号量了;可以跳过这段发送信号量的代码。
    对于前面的if条件必须明白两点。首先,当将info映射为无符号长整型数的时候
,如果它为1,这就不是一个实际指向struct siginfo结构的指针。相反的,它是
说明信号量来自于内核的特殊值,但是并没有进一步的附加信息可供使用。内核本
身在最低的页(内存页在第8章中讨论)中并不分配空间,因此在4,096之下的任何
地址(除了0,NULL之外)都可以作为这种特殊值使用。
    其次,在这几种条件的情况中,位XOR运算操作符(^)比不等运算操作符(!=)
使用得更为普遍。在这些情况下,两个操作符意义相同,因为如果两个相比较的正
数之间有一位不同,在XOR运算的结构中就至少有一位被置位,所以结果非空,逻
辑值为真。推测起来,cc的早期版本为^生成的代码比为!=生成的代码更为有效,
但是在现在的编译器版本中就不是这样了。
28248:忽略信号量0并拒绝将信号量传送给僵进程(已经退出但是还尚未从系统的
数据结构中移走的进程;请参看第7章的“进程状态”一节,它讨论了函数exit)

28252:对于一些信号量,在实际发送之前必须进行一些额外的工作。这些工作是
在这里的switch中处理的。
28253:如果正在发送SIGKLL或者SIGCONT,send_sig_info就唤醒进程(也就是说
,如果当前被停止了就允许它再次运行)。
28257:设置进程的返回代码为0——如果进程已经使用SIGSTOP停止了,返回代码
域就被用来在停止等待的信号量和其祖先间建立通讯。
28258:取消任何挂起的SIGSTOP(被调试器阻塞),SIGSTP(由键盘输入的
Ctrl+Z终止),SIGTTIN(试图从TTY中读取信息的后台运行进程),SIGTTOU(试
图向TTY中写入信息的后台运行的进程);这些是所有可能中断进程的条件,也是
SIGCONT或者SIGKILL最可能作为响应出现的情况。
28263:在一些信号量被删除之后,调用recalc_sigpending(16654行,将在后面
讨论)来判断是否还有信号量仍然处于挂起状态以等待进程。
28266:在前面的情况中,如果SIGCONT或者SIGKILL到达了,这四个信号量就会都
被取消。但是看起来有些不太对称,如果这四个信号量有一个到达了,任何挂起等
待的SIGCONT都会被取消。然而SIGKILL却不会被取消,这遵循SIGKILL永远不会被
锁定或者取消的规律。
28281:如果目标进程希望忽略信号量并且允许不接收信号量,那么就跳过了信号
量的接收过程。
28284:非实时信号量并不排队等待,这就意味着如果在进程处理第一个信号量实
例之前,同一信号量的第二个实例就到达了,那么第二个实例就会被忽略。这一点
就是在这里确保的(回想一下struct task_struct结构的singnal成员中保存着一
个进程的当前正在挂起等待的信号量的集合)。
28304:在限制条件的控制下,实时信号量需要排队等待。最重要的限制是可以同
时排队等待的实时信号量总数的可配置限制;这一限制值为max_queued_signal,
它是在28007行定义的,而且可以使用Linux的系统控制特性加以修改。如果有空间
来容纳更多的信号量,就分配struct signal_queue结构来容纳排队等待的信息。

    为什么所要首先限制排队等待的信号量的数目呢?这是为了防止服务拒绝的攻击
:如果没有这个限制,用户可以持续发送实时信号量直到内核内存溢出,这样就会
阻碍内核为其它进程提供该服务及其它服务。
28310:如果一个队列节点已经被分配,现在send_sig_info就必须使有关这个信号
量的信息进入队列。
28311:把信息加入队列是很直接的:send_sig_info把挂起等待的信号量数量(全
局变量)增加1,接着把新的节点增加到目标进程的信号量队列中。
28315:根据提供给send_sig_info的info参数填充队列节点的info成员。
28316:0(NULL)意味着信号量是从用户发送而来的,而且可能使用了从28513行
到28544行定义的向后兼容的信号量发送函数。目标siginfo_t使用相对比较明确的
值来填写。
28323:值1是指示信号量来源于内核的一个特殊值——再一次的使用了向后兼容的
函数。和前面的情况一样,目标siginfo_t使用相对比较明确的值来填写。
28331:正常情况下,send_sig_info得到一个实际的siginfo_t,它可以简单地将
其拷贝到队列节点中。
28334:没有分配队列节点——或者因为系统内存溢出而造成kmem_cache_alloc在
28306行返回NULL;或者因为已经达到了信号量队列的最大值,send_sig_info根本
就没有试图分配节点。不管怎样,send_sig_info所处理的内容是相同的:除非该
信号量是通过内核或者老式的信号量函数(例如kill)发出的,否则
send_sig_info就返回EAGAIN错误,通知调用者现在信号量不能排队等待,但是后
来调用者应该可以再次使用相同的参数成功执行调用。否则,send_sig_info就传
送该信号量但并不将其排入队列中。
28345:最后,send_sig_info从实际上准备好发送信号量。首先,信号量进入该进
程的挂起等待的信号量的集合中。注意即使信号量被锁定了这个过程也要执行,这
可能有点奇怪。但是这样处理是有原因的:内核必须提供sys_sigpending(28981
行,本章中后面部分将讨论),它允许进程查询在锁定时传送进来什么信号量。
28346:如果信号量没有被锁定,那么进程应该被通知有信号量到达了。相应的,
其sigpendig标志被置位。
28370:如果进程正在等待信号量的到达并且有信号量也正在等待它,那么这个进
程就被唤醒(使用wake_up_process,26356行)来处理信号量。
force_sig_info
28386:这个函数被内核用来保证不管进程是否需要,它都确实接收了一个信号量
。例如,在进程释放未用指针时,可以使用这个函数来确保该进程接收了SIGSEGV
(请参看7070行——实际上是调用了向后兼容的函数force_sig,但是force_sig完
全是按照force_sig_info实现的)。force_sig_info的参数和send_sig_info的参
数相同,两者的意义也相同。
28392:如果目标进程是僵进程,即使是内核也不应该给它发送任何信号量;所进
行的尝试将被拒绝。
28397:如果进程将要忽略这个信号量,force_sig_info将通过强制它执行缺省操
作的方式进行纠正。实际上它并不像外表所表现出来的那样无害:在内核使用该函
数的情况下,对这个信号量的缺省操作是杀死进程。
28399:把信号量从t所锁定的集合中移走。
28402:force_sig_info现在已经建立了一些条件使得t必须接收信号量,因此该信
号量就可以使用send_sig_info进行发送。如果send_sig_info的实现改变了,这仍
然可能造成信号量不能发送,因此这两个函数必须保持同步。
recalc_sigpending
16654:这个函数重新计算进程的sigpending标志;当进程的signal或blocked集合
改变时就调用该函数。
16676:在最简单的情况中,recalc_sigpending将信号量和锁定集合求补的结果执
行位AND操作。(对锁定集合求补就是允许的集合。)其它的情况仅仅是这种情况
的泛化。
16679:如果前面操作中的任何一个在ready中遗留下了任何一位,那么挂起等待的
信号量集合中最少有一个信号量现在还被锁定;因此recalc_sigpending将增加
sigpending标志的值。
由于recalc_sigpending所实际需要了解的全部内容只是是否至少有一个信号量在
挂起等待——例如,如果不止一个,也并不需要知道有多少信号量在挂起等待——
非平凡情况下的代码只要发现ready的值被置为非0值就应该停止对其进行修改(例
如,前面16662行通过中断循环)。但是,任何可能来对此优化所产生的效率增进
都必须要同为此而进行的额外测试进行权衡。正是由于这个原因,又加上
_NSIG_WORDS很小(在实际中无论如何都是如此),改进的版本可能要比标准情况
快一点。
ignored_signal
28183:ignored_signal有助于send_sig_info决定是否给一个进程发送信号量。
28189:如果进程正被其祖先跟踪(可能是调试器),或者信号量是在进程锁定的
集合中,那么它就不能被忽略。第二种情况可能是我们过去所没有考虑过的;如果
信号量被锁定了,send_sig_info(还有ignored_signal)难道不应该将其忽略吗
?如果情况的确如此,还真不应该忽略。这个函数通过信号量是否应该被忽略表明
了进程的信号量的signal集合的相应位是否应该被置位。如同前面我们已经看到的
那样,对sigpending系统调用的支持要求如果在锁定过程中有信号量到达,内核就
应该设置相应的位。因此,被锁定信号量不能简单地忽略。
28194:如果进程是一个僵进程,信号量就应该被忽略。这种测试是不必要的,因
为这种情况甚至在28248行的ignored_signal调用之前就会被发现。
28199:在大多数情况下,SIG_DFL(缺省的)操作是处理信号量而不是将其忽略。
你所能看到的例外是SIGCONT,SIGWINCH,SIGCHLD,和SIGURG。
28207:进程允许忽略大部分信号量,但是不能忽略SIGCHLD。对于SIGCHLD,
POSIX赋予SIG_IGN一种特殊的意义,这一点在28831行将会说明。这里所提到的“
automatic child reaping”(自动子进程空间回收)将在3426行执行。
28211:在缺省的情况下,可以假定ignored_signal有一个实际的函数指针,而不
是SIG_DFL或者SIG_IGN两个伪值的一个。这样,信号量就和用户定义的处理句柄联
系起来,这意味着进程希望处理这个信号量。它通过返回0来指明信号量不应该被
忽略。
do_signal
3364: do_signal在信号量到达进程时使用。这个函数在内核中被调用的地方不止
一次——如我们在第5章中看到的从203行和211行,还有从2797行和2827行。通常
所有这些情况都是当前进程希望处理挂起等待的信号量(如果有的话)。
3375: 如果非空,oldset用来返回当前进程锁定的信号量集合。由于do_signal不
会修改锁定的集合,它可以简单的返回一个指向现有锁定集合的指针。
3378: 进入几乎扩展到该函数末尾(3478行)的循环。退出该循环的方法只有两
种:把所有可能的信号量都处理了,或者处理唯一一个信号量。
3382: 使用dequeue_signal使信号量出队列(28060,后面将会介绍)。
dequeue_signal或者返回0,或者返回需要处理的信号量的编号,并且它还会填充
info中的附加信息。
3385: 如果没有信号量处于等待状态,将在这里中断循环。正常情况下,它在循
环第一次执行过程中是不会发生的。
3388: 如果当前进程正在被其祖先跟踪(可能是调试器),而且信号量也并不是
不可锁定的SIGKILL,那么在信号量到达之前,进程的祖先就必须已经得到通知了

3391: 将传递给子孙进程的信号量编号传送到祖先进程中对应子孙进程的
exit_code域;祖先使用sys_wait4收集这些信息(23327行,在第7章中介绍)。
do_signal停止子孙进程的运行,然后使用notify_parent(3393行)给祖先进程发
送SIGCHLD信号量,接着调用调度函数schedule(26686行,第7章中介绍),给其
它进程——尤其是其祖先进程——运行的机会。schedule会把CPU分配给其它进程
,因此直到内核跳转回这个进程才会返回。
3397: 如果调试器取消了信号量,do_signal在这里就不应该处理它;循环继续进
行。
3402: SIGSTOP可能只是由于进程正在被跟踪而产生。这里没有必要处理它;循环
继续进行。
3406: 如果调试器修改了do_signal要处理的信号量编号,do_signal将根据新的
信息填充info。
3415: 正如注释中所说明的一样,如果新的信号量被锁定了,就需要重新排队,
循环继续进行。否则,控制流程将直接执行下面的代码。
3421: 在这里,或者进程未被跟踪,或者进程正被跟踪但是得到了一个SIGKILL信
号量,或者控制流程直接从前面的代码块中执行下来。在任何一种情况中,
do_signal都有一个应该现在处理的信号量。它从获取struct k_sigaction结构开
始,这个结构指明了怎样处理这个信号量编号。
3423: 如果进程试图忽略信号量,那么除非这个信号量是SIGCHLD,否则
do_signal就继续执行循环从而忽略该信号量。为什么这个测试不能同时保证该进
程不会忽略掉SIGKILL这个注定不可忽略也不可锁定的信号量呢?答案在于和
SIGKILL相关的操作永远不会是SIG_IGN的,实际上也不会是除SIG_DFL之外的任何
操作——28807行就保证了这一点(在do_sigaction函数中)。这样,如果操作是
SIG_IGN,那么信号量编号就不可能是SIGKILL。
3426: 如同在从28820行开始的标题注释中说明的一样,POSIX标准说明了忽略
SIGCHLD的操作实际上意味着自动回收其子孙进程。子孙进程是通过使用
sys_wait4来回收的(23327行,在第7章中介绍),此后循环继续运行。
3435: 进程为这个信号量采用缺省操作。专用的初始化进程接收到全部信号量所
对应的缺省操作是把信号量整个忽略掉。
3439: 对信号量SIGCONT、SIGCHLD和SIGWINCH所采取的缺省操作是不加处理,只
是简单地继续执行循环。
3442: 对于信号量SIGSTP,SIGTTIN和SIGTTOU,缺省的操作各自不同。如果该进
程所归属的进程组是孤立的——简单的说就是没有连接到TTY上——那么POSIX规定
对于这些基于终端的信号量的缺省操作是将其忽略。如果进程的进程组不是孤立的
,缺省的操作是停止进程的运行——这和SIGSTOP的情况是相同的,在这种情况下
控制流程直接向下运行。
3447: 在对SIGSTOP的响应中(或者是从前面情况中直接执行下来),do_signal
终止了进程。另外,除非祖先进程已经规定对其子孙进程的终止运行不加理会,否
则祖先进程将会在其子孙进程退出时被通知。和3394行一样,调用schedule交出
CPU给其它某一进程。当内核把CPU再次分配给当前进程的时候,该循环继续运行以
处理队列中的另外一个信号量。
    这不是我们希望的——我认为当schedule返回时,循环应该退出,因为信号量已
经处理完了。其原理在于如果进程被终止了,唤醒进程的最可能的原因是进程又得
到了信号量,可能是SIGCONT,因此该进程现在就可以检测并处理信号量了。
3456: 对于其它信号量的缺省操作是退出进程。它们中的一些将使进程首先清空
内核(详细的介绍请参看第8章),这些信号量就是SIGQUIT,SIGILL,SIGTRRAP,
SIGABRT,SIGFPE和SIGSEGV。如果此二进制格式(详细的介绍请参看第7章)知道
如何清空内核并且成功地清空了内核,那么在进程的返回代码中就会有一位被设置
来指明进程在退出之前就已经清空了内核。接着流程按照default的情况继续执行
,终止进程的运行。注意do_exit(23267行,在第7章中也会有介绍)是从来不会
返回的——因而在3471行中会有“NOTREACHED”注释。
3476: 此处,do_signal从队列中取出一个信号量,该信号量既不和SIG_IGN的操
作有关,也不和SIG_DFL的操作有关。唯一的另外一种可能性是这是用户定义的信
号量处理函数。do_signal调用handle_signal(3314行,本章随后将会更为详细地
讨论)来触发这个信号量处理函数,接着返回1向调用者声明这个信号量已经处理
过了。
3481: 此处,do_signal不能为当前进程从队列中取出信号量。(只有从3386行的
break退出时才能执行到本行。)如果在系统调用的处理过程中间被中断了,
do_signal就要调整寄存器,从而系统调用将可以重新执行。
3490:返回0以通知调用者do_signal没有处理任何信号量。
dequeue_signal
28060:dequeue_signal将信号量从进程信号量队列中移出,同时忽略那些由掩码
说明的信号量。它返回信号量的编号并使用指针参数info返回相关的siginfo_t。

28071:为了避免重复参照而建立一些别名:s是进程的挂起等待的信号量的集合(
记住其中可能包括了一些锁定的信号量),m是掩码的集合。特别要注意的是*s表
达式,在该函数中这个表达式出现了不止一次,但是它只是current->signal.
sig[0]的一种简单写法。
28073:在这个switch条件分支中,sig被设置为第一个挂起等待的信号量。从最简
单的情况入手最容易理解;其它的情况只是这种情况的泛化。
28091:最简单的情况是:它把挂起等待的信号量和掩码求补后的结果进行位AND运
算,结果被存储在临时变量x中;x现在就是掩码不能忽略的挂起等待的信号量的集
合。如果x不为0,那么就存在挂起等待的信号量(x至少有一位被置位);
dequeue_signal使用ffz(本书中没有涉及)取得相应的信号量编号,并将其转化
为从1开始计数的信号量编号,将结果存储在sig中。正如前面所说明的一样,其它
情况只是这种情况的泛化;最重要的结果是sig被置位,如果可能的话在每种情况
下都是如此——此后其它变量(i,s,m或者x)的状态就不难理解了。如果在
switch之后的sig是0,掩码中就没有传递挂起等待的信号量。
28097:如果一个信号量正在挂起等待,那么dequeue_signal应该试图将其从队列
中释放出来。reset跟踪dequeue_signal以判定是否应该把信号量从进程的挂起等
待的信号量队列中删除。将reset初始化为1仅仅是由于在函数处理过程中它可能会
改变的假定。
28107:对于非实时信号量,内核不会保持原始的siginfo_t(如果曾经有过的话)
,因此dequeue_signal应该尽可能的重新组织有关的信息。不幸的是,当前实现方
法中并没有多少信息——只有信号量编号自身而已。info的其它成员都简单地被设
置为0。
28118:在另一种情况,也就是实时信号量情况下,siginfo_t只是一种点缀。
dequeue_signal会在进程的sigqueue中进行扫描以确定其值。
28122:如果找到了siginfo_t,dequeue_signal现在就使其出队列,将siginfo_t
的内容拷贝到info中,并释放为这个队列节点分配的内存。
28129:如果队列中没有这个信号量的更多实例,那么信号量就不会在挂起等待了
。但是为了弄清楚队列中是否还有信号量的实例,dequeue_signal需要遍历整个队
列。因此,该函数需要扫描这个队列的其余元素来查询是否存在相同信号量的其它
实例。如果发现了实例,dequeue_signal就清空reset标志——只有在这种独特的
情况下才会进行的操作。
28142:正在出队的信号量是实时信号量,但是在进程的挂起等待的实时信号量队
列中却没有发现它,其原因在代码中已经进行了阐述。现在,dequeue_signal和它
有非实时信号量的情况相同了——它知道信号量是可以访问的,但是没有方法可以
访问其原始值——并且其响应过程处理的工作也完全相同,仅仅使用信号量编号来
填充info,而没有其它属性值。
28150:除非reset标志被清空了——也就是说除非这是一个实时信号量并且同一个
信号量的其它实例仍然在挂起等待队列中——该信号量已经被处理过;它应该从进
程的挂起等待集合中删除。
28152:信号量脱离队列,因此dequeue_signal应该重新计算进程的sigpending标
志。我认为这里有一个可以进行少量优化的机会:只用当reset为真的时候
dequeue_signal才需要这样处理。recalc_sigpending从进程的锁定集合和挂起等
待集合中计算结果;锁定的集合没有改变,因此只有当挂起等待的集合发生改变时
,dequeue_signal才需要调用recalc_sigpending。如果reset为假,挂起等待的集
合就不会改变,因此对于recalc_sigpending的调用就是不必要的。
28163:switch没有发现信号量,因此没有信号量正在挂起等待。作为内部正确性
的检测,dequeue_signal确保内核不会认为有信号量正在为某任务挂起等待。
28174:返回出队的信号量编号,或者如果没有信号量出队,就返回0。
notify_parent
28548:notify_parent寻找进程的祖先进程并通知它其子孙进程的状态发生了改变
——通常情况是其子孙进程或者被终止了,或者被杀死了。
28553:使用有关信号量发生的上下文的信息填充局部变量info。
28564:如果子孙进程已经退出,why被赋以适当的值以指明其原因是因为它清空了
内核,或者被某信号量将其杀死,或者因为执行了非法操作。
28572:类似地,如果使用信号量终止了进程,对why赋值以说明发生的情况。
28578:前面的情况几乎覆盖了所有的可能性。如果没有,函数打印出警告信息并
继续运行;在这种情况下,系统会在28562行将why的值赋为SI_KERNEL。
28586:给进程的祖先进程发送信号量。下面一行唤醒任何等待这个子孙进程的进
程并为其提供CPU。
handle_signal
3314: handle_signal在需要调用用户定义的信号量处理程序时由do_signal调用

3338: 建立一个用户处理程序可以在其中运行的堆栈帧。如果进程已经请求了内
核所拥有的有关信号量的原始值和其上下文的附加信息,那么堆栈帧就使用
setup_rt_frame(3231行)构建起来;否则就使用setup_frame(3161行)构建。
这两种方法都可以实现构建工作,这样控制流程会返回信号量处理程序。当它返回
时,实际返回的是信号量到达的时候正在执行的代码。
3343: 如果SA_ONESHOT标志被设置,则信号量处理程序应该只执行一次。(注意
sys_signal是signal系统调用的实现,它使用SA_ONESHOT类型的信号量处理程序—
—请参看29063行。)在这种情况中,缺省的操作是立即将其恢复。
3346: SA_NODEFER意味着在执行这个信号量的处理程序时,不应该有其它信号量
被锁定。如果位没有设置,其它的位现在就会被加入进程的锁定的集合中。
其它有关信号量的函数
其它一些有关信号量处理的函数。
sys_sigpending
28981:这个简短的系统调用允许进程询问在信号量锁定期间是否有非实时信号量
到达。通过所提供的指针,该函数返回一个位集以指明它们是哪些信号量。
28987:这个函数的核心是进程的blocked集合和signal集合间的简单位AND操作。
它只对最低32位感兴趣,这些都是非实时信号量。
28992:使用所提供的指针把挂起等待的集合拷贝回用户空间。如果失败就返回
-EFAULT,如果成功就返回0。注意是否有信号量正在挂起等待——也就是说,返回
值是否为空——并不是成功的判据之一。
do_sigaction
28801:do_sigaction实现了系统调用sigaction有意义的部分。(其余部分在
2833行的sys_sigaction中。)sigaction是POSIX中等价于ISO C的函数signal——
它把信号量和操作关联起来,这样进程接收到信号量时就能够执行相应的操作。
28806:健全性检测:确保sig在范围之内并且进程没有试图把SIGKILL或者
SIGCONT和某种操作相关联。进程被简单地剥夺了覆盖这两个信号量的缺省操作的
权力。然而,和signal实现的处理程序不同,使用sigaction实现的处理程序不是
SA_ONESHOT类型的,因此在处理程序被调用的时候就不用每次都将其重新装载。
28811:获取和这个信号量相关的指向k_sigaction结构的指针。
28813:sigaction可以通过一个过去所提供的指针返回旧有的操作。这在以堆栈方
式存在的处理程序中是很有用的,在这里处理程序被临时覆盖,以后再恢复出来。
如果oact指针非空,旧有的操作就会被拷贝到其中。(但是这并不会把信息拷贝会
用户空间;调用者必须执行这样的处理。)
28815:如果do_sigaction被赋予一个需要同信号量相关联的操作,那么二者现在
就相互关联起来。SIGKILL和SIGSTOP也必须被从操作的掩码中删除,为了确保这些
信号量不会被锁定或者覆盖。
28836:正如在28820行开始的标题中注释的一样,为了遵守POSIX标准,下面的几
行代码必须要经过一定变形,并且在必要情况下还会舍弃某些信号量。对于这些细
节情况,我们即使跳过也不会有什么损失。
sys_rt_sigtimedwait
28694:sys_rt_sigtimedwait等待信号量的到达,它可能在经过一段特定的时间间
隔以后超时退出。并不是所有的信号量都会接收;指针uthese所指明的sigset_t说
明了调用者所感兴趣的信号量。
28714:uthese(它已经被拷贝到局部变量these中了)是允许的信号量的集合,于
是内核元语只知道如何锁定信号量。但这样也没有关系:对允许的信号量集合进行
求补运算就得到了应该锁定的信号量,所得到的结果就可以直接使用了。
28717:如果调用者提供了超时时间,该超时时间就将被拷贝到用户空间中,而且
其值也必须经过健全检测。
28726:检查是否已经有信号量正在等待了——如果有,就没有必要为其等待了。
否则,调用者必须等待。
28731:保存原来的锁定信号量集合,然后阻塞由these定义的所有信号量。
28737:如果用户没有提供超时时间,那么超时时间会是MAX_SCHEDULE_TIMEOUT(
宏定义为LONG_MAX,或者是231-1,16228行)。但是并不永远都是如此——超时时
间是以瞬间(jiffy)计数的,它的系统时钟以每秒100次的速度跳动着,因此大约
有248天,超时时间就耗尽了。(在64位机器中,这大约需要三十亿年。)
28739:如果用户确实提供了超时时间,就将其转化为以瞬间计算的值。“+”后面
的表达式是对下一个瞬间进行向上舍入的明智方法——其思想是
timespec_to_jiffies可能已经向下舍入了,但是内核必须是上舍入的,因为它必
须等够用户请求的瞬间个数。它虽然可以检测timespec_to_jiffies(18357行)是
否是下舍入的,但是下面这种方法更为简单:如果用户提供的超时时间不是0就为
其增加一个瞬间,并且认为是对它进行了调整。毕竟Linux不是一个真正的实时操
作系统——当你指定了超时时间时,Linux只能保证至少等待如此长的时间。
28742:设置当前用户的状态为TASK_INTERRUPTIBLE(请参看第7章)。
schedule_timeout(26577行)用来让出CPU;在指定的时间用完以后或者其它事件
到达并唤醒进程(比如接收了一个信号量)时,该进程才可以继续运行。
28746:进程希望被信号量唤醒。sys_rt_sigtimedwait再次尝试从进程的等待信号
量队列中取出信号量并恢复原来锁定的集合。
28752:此处,该函数仍然不知道信号量是否已经到达了——它可能无需等待就可
以得到一个信号量,或者在等待期间可能有另一个信号量到达,也或者该函数一直
在等待但是没有信号量到达。
28753:如果信号量到达,该函数就给用户进程传递信息并且返回信号量编号。
28759:否则,虽然进行了等待,但是没有信号量到达。在这种情况下,该函数或
者返回-EAGAIN(说明用户进程可以再次使用相同的参数尝试),或者返回-EINTR
(说明其等待过程被由于某些原因而不能传递的信号量中断了)。
内核如何区分实时信号量和非实时信号量
简单的说,答案并不复杂。我几乎掩盖了其中的绝大部分区别,这是有一定原因的
:退出语句不多。现在,为了使这一点更加清楚,让我们来看一下系统调用
sigprocmask的两个版本,它允许进程处理自己的锁定信号量的集合——增加,删
除,或者简单地对信号量集合进行设置。
sys_sigprocmask
28931:sys_sigprocmask是这个函数的原始版本,这一版本并不知道或者是不关心
实时信号量。参数how指明了要执行的操作;如果set不为NULL,就是这个操作的操
作数;如果oset是非空的,那么oset返回的就是原始的锁定集合。
28937:如果set为空,那么how的值就没有什么用处了:该操作就没有操作数了,
因此该函数不会处理有关的内容。否则,就继续执行该操作。
28939:在新的锁定集合中的拷贝,其中删除了不可锁定的SIGKILL和SIGSTOP。
28944:为了处理以后将当前锁定集合拷贝回用户空间的需要,在old_set中存储当
前锁定集合的一个备份。由于当前锁定集合在以后的代码中可能会被修改,因此在
它改变之前必须对其值进行存储。
28948:当然是忽略无效的操作。
28951:SIG_BLOCK操作符指明new_set应该解释为要锁定的附加信号量的集合。这
些信号量将被加入该锁定集合中。
28954:SIG_UNBLOCK操作符指明new_set应该解释为要从锁定的信号量的集合移出
的信号量集合。这些信号量现在被移出锁定集合。
28957:SIG_SETMASK操作符指明new_set应该解释为新的锁定集合,简单覆盖该锁
定集合原有的值。因此,sys_sigprocmask正是实现这一点的。注意它只设置了
bloched.set数组的最低的元素——这个元素包含低32位非实时信号量,这是该函
数所关心的内容。
28966:如果调用者已经请求查询锁定的集合的原来的值,执行流程就向前跳到
set_old标号(28970行)。
28968:如果set为空,意味着调用者没有请求对锁定集合进行修改,但是调用者可
能仍然希望了解锁定集合的当前值。
28972:oset非空(set也可能为非空)。不管哪一种情况,old_set都包含一个原
来锁定的集合的备份,在返回之前sys_sigprocmask会试图将其拷贝回用户空间。

sys_rt_sigprocmask
28612:sys_rt_sigprocmask和sys_sigprocmask非常类似,但是它也能够处理新的
实时信号量。由于这两者之间的相似性,在这里我仅仅介绍一下它们之间有趣的区
别。
28638:与如下代码不相类似的是
    /* how sys_sigprocmask does SIG_BLOCK.*/
      new_set =  *set ;      /* line 28938 */
      blocked |= new_set ;    /* line 28952 */

    (作为一个例子采用SIG_BLOCK的情况),实际代码类似如下代码:
    /* how sys_rt_sigprocmask does SIG_BLOCK.*/
      new_set =  *set ;      /* line 28625 */
      new_set |= old_set ;    /* line 28639 */
      blocked |= new_set ;    /* line 28648 */

    我不明白为什么sys_rt_sigprocmask不使用和sys_sigprocmask相同的方式实现,
而且这样还可以节约一点效率。
中断
中断的名字十分形象,因为它们终止了系统正常的处理过程。在前面第5章中你就
已经看到了中断的一个例子:提供系统调用基本机制的软件中断。在本章中,我们
来了解一下硬件中断。
和系统调用中断一样,硬件中断也可能转化为内核模式运行然后返回。如果用户进
程运行时发生了中断,系统就转化为内核模式,并且内核要对中断做出响应。接着
,内核将控制返回给用户进程,用户进程能够从当时离开的位置继续运行。
同系统调用中断的另一个区别是硬件中断可能在内核已经在内核模式下运行时发生
。这在系统调用中很少发生——通常内核不会麻烦地触发系统调用中断,因为它可
以直接调用目标内核函数。如果中断发生时系统处于内核模式,结果就同在用户模
式下的机制相一致——唯一的区别是内核自身所特有的执行过程而不是用户进程的
执行过程暂时地被中断。
如果内核在一段时期内不希望被中断,那么就可以使用cli和sti函数(13105行和
13104行是UP版本;1216行和1229行是SMP版本)屏蔽和开启中断。这些函数根据底
层的x86指令命名:cli代表“清除中断标志”,sti代表“设置中断标志”。其工
作方式和其名称类似:CPU有一个“中断允许”标志,如果对其置位就允许中断,
如果将其清空就禁止中断。因此,你可以使用cli清空这个标志从而禁止中断,也
可以使用sti设置这个标志从而允许中断。在UP代码中,你可以选择调用两个等价
的宏__cli和__sti——分别见13105行和13104行。
当然,把内核移植到非x86平台上会使用不同的底层指令——在这些体系结构中
cli和sti函数的实现都不相同。
IRQs
IRQ,或者中断请求,是从硬件设备发往CPU的中断信号。作为对IRQ的响应,CPU跳
转到某个地址——中断服务例行程序(ISR),更普通的情况是调用中断处理程序
——内核在前面已经对这些处理程序进行了登记。中断处理程序是内核执行的为中
断服务的函数;从中断处理程序中返回就继续执行中断前所在位置的代码。
IRQ是有编号的,每一个硬件设备在系统中都对应一个IRQ号码。例如在IBM PC体系
结构中,IRQ 0就关联着一个每秒产生100次中断的定时器。把IRQ号码和设备关联
起来,使得CPU可以区分每个中断是哪个设备产生的,从而允许它跳转到正确的中
断处理程序。(在某些情况中,在一个系统中一个IRQ号可以被多个设备所共用,
当然这不是非常普遍的情况。)
Bottom Halves
中断处理程序的下半部分(bottom half)是无须立即执行的部分。在某些中断之
后,你甚至可能根本就不需要执行它。
给定的中断处理程序从概念上可以被分为上半部分(top half)和下半部分(
bottom half);在中断发生时上半部分的处理过程立即执行,但是下半部分(如
果有的话)却推迟执行。这是通过把上半部分和下半部分处理为独立的函数并对其
区别对待实现的。总之,上半部分要决定其相关的下半部分是否需要执行。不能推
迟的部分显然不会属于下半部分,但是可以推迟的部分只是可能属于下半部分。
你也许会很奇怪为什么Linux会辛苦地把它们区分开——为什么要延迟呢?一个原
因是要把中断的总延迟时间最小化。Linux内核定义了两种类型的中断,快速的和
慢速的,这两者之间的一个区别是慢速中断自身还可以被中断,而快速中断则不能
。因此,当处理快速中断时,如果有其它中断到达——不管是快速中断还是慢速中
断——它们都必须等待。为了尽可能快地处理这些其它的中断,内核就需要尽可能
地将处理延迟到下半部分执行。
另外一个原因是,在最低层,当内核执行上半部分时,中断控制芯片将被告知禁止
正在服务的这个特殊IRQ(这和CPU级别的中断禁止不同,它把快速中断和慢速中断
区别开来)。我们并不希望这种状态会持续地比需要的时间还长,因此只有上半部
分中时间最为关键的部分才被处理,但是下半部分中其它的工作就要延迟处理了。

区分上下部分还有一个原因是处理程序的下半部分包含有一些中断所不一定非要处
理的操作,只要内核可以在一系列设备中断之后可以从某些地方得到。在这种情况
下,执行对于每个中断的下半部分的处理完全是一种浪费,它可以稍稍延迟并在后
来只执行一次。
最后一段的一个暗示是值得说明的:没有必要每次中断都调用下半部分。相反,是
上半部分(或者也可能是其它代码)简单地标记下半部分,通过设置某一位来指明
下半部分必须执行。如果下半部分已经标记过需要执行了,现在又再次标记,那么
内核就简单地保持这个标记;当情况允许的时候,内核就对它进行处理。如果在内
核有机会运行其下半部分之前给定的设备就已经发生了100次中断,那么内核的上
半部分就运行100次,下半部分运行1次。
下半部分在内核中有时候被认为是“软IRQ”或者“软中断处理程序”,这有助于
你理解今后要遇到的一些文件名和术语。
在本节的剩余内容中,我们将保持下半部分概念的抽象。下一节深入介绍定时器中
断,包括其下半部分的处理,并展示了下半部分概念的一个有趣的滥用现象——我
的意思是一个有趣的变种。
数据结构
同对信号量的处理一样,我们首先介绍一下中断和下半部分使用的重要的数据结构
。图6.1阐述了这些数据类型之间的关系。

图6.1 有关中断的数据结构

我们从这里开始,体系结构无关的头文件linux/interrupt.h定义了struct
irqaction结构(14844行),它代表了内核接收到特定IRQ之后应该采取的操作(
在本章后面的部分中你将看到struct irqaction结构是如何与IRQ关联的)。其成
员如下:
?   handler——指向某一函数的指针,该函数是作为对中断的响应所执行的操作。

?   flags——从与前面已经介绍过了的sa_flags相同的集合中提取出来;这个集合
从12108行开始。该集合中仅仅为此目的而出现的值只有SA_INTERRUPT(使用另外
一个中断来中断这个中断也是可以的),SA_SAMPLE_RANDOM(考虑到这个中断也是
源于物理随机性),和SA_SHIRQ(这个IRQ和其它struct irqaction共享)。
?   mask——在x86或者体系结构无关的代码中不会使用(除非将其设置为0);看起
来只有在SPARC64的移植版本中要跟踪有关软盘的信息时才会使用它。
?   name——生成中断的硬件设备的名字。由于不止一个硬件可以共享一个IRQ,这
在打印人工阅读程序时就有助于区分它们。
?   dev_id——标识硬件类型的一个唯一的ID——Linux支持的所有硬件设备的每一
种类型都有一个由制造厂商定义的在此成员中记录的设备ID。其所有的可能值都是
从一个巨大的集合中抽取出来的,这个集合在本书中没有介绍,因为它包含的内容
是十分繁琐的,而且都是重复的——它仅仅是结构上类似于下面一小段代码的巨大
宏定义块。
#define PCI_DEVICE_ID_S3_868 0x8880
#define PCI_DEVICE_ID_S3_928 0x88b0
#define PCI_DEVICE_ID_S3_864_1 0x88c0
#define PCI_DEVICE_ID_S3_864_2 0x88c2

在你看完这些的一部分之后,也就相当于将其完整的看了一下。可能你已经发现了
,摘录的这部分内容是从包含针对基于S3的PCI显卡的设备ID的文件中选取的。
虽然dev_id是一个指针,可它并不指向任何内容,但若将其解除参照就会引起错误
。能够说明问题的是它的位结构模式。
?   next——如果IRQ是共享的,那么这就是指向队列中下一个struct irqaction结
构的指针。通常情况下,IRQ不是共享的,因此这个成员就为空。
接下来我们感兴趣的两个数据结构存在于体系结构相关的文件
arch/i386/kernel/irq.h中。第一个是struct hw_interrupt_type结构(1673行)
,它是一个抽象的中断控制器。这是一系列的指向函数的指针,这些函数处理控制
器特有的操作:
?   typename——赋给控制器的人工可读的名字。
?   startup——允许从给定的控制器的IRQ所产生的事件。
?   shutdown——禁止从给定的控制器的IRQ所产生的事件。
?   handle——根据提供给该函数的IRQ处理唯一的中断。
?   enable和disable——这两个函数基本上和startup和shutdown相同;存在的差异
对于本书中涉及的代码都不很重要。(实际上,对于本书中包含的所有代码来说,
enable/disable函数对和startup/shutdown函数对都是相同的。)
这个文件中我们感兴趣的另外一个数据结构是irq_desc_t(1698行),它具有如下
成员:
?   status——一个整数,它的位或者为0,或者对应从1685行到1689行定义的集合
中抽取出的标志。这些标志的集合代表了IRQ的状态——IRQ是否被禁止了,有关
IRQ的设备当前是否正被自动检测,等等。
?   handler——指向hw_interrupt_type的指针。
?   action——指向irqaction结构组成的队列的头。如同前面说明的一样,正常情
况下每个IRQ只有一个操作,因此链接列表的正常长度是1(或者0)。但是,如果
IRQ被两个或者多个设备所共享,那么这个队列中就有多个操作了。
?   depth——irq_desc_t的当前用户的个数。主要是用来保证事件正在处理的过程
中IRQ不会被禁止。
irq_desc_t是在irq_desc数组中(733行)积聚起来的。对于每一个IRQ都有一个数
组入口,因此数组把每一个IRQ映射到和它相关的处理程序和irq_desc_t中的其它
信息上。
最后一个需要说明的数据结构集合从29094行开始;这些都与前面所讨论的下半部
分有关:
?   bh_mask_count(29094行)——跟踪为每个下半部分提出的enable/disable请求
嵌套对的数组。这些请求通过调用enable_bh(12575行)和disable_bh(12568行
)实现。每个禁止请求都增加计数器;每个使能请求都减小计数器。当计数器达到
0时,所有未完成的禁止语句都已经被使能语句所匹配了,因此下半部分最终被重
新使能。
?   bh_mask和bh_active(14856行和14857行)——它们共同控制下半部分是否运行
。它们两个都有32位,而每一个下半部分都占用一位。当一个上半部分(或者一些
其它代码)决定其下半部分需要运行时,就通过设置bh_active(12498行中使用
mark_bh)中的一位来标记下半部分。不管是否经过了这样的标记,下半部分可能
会通过清空bh_mask中的相关位来整个跳过——通过调整bh_mask_count入口,
enable_bh和disable_bh完成了这个功能。
因此,对bh_mask和bh_active进行位AND运算就能够表明应该运行哪一个下半部分
。特别是如果位与运算的结果是0,就没有下半部分需要运行。这种技术在内核中
多次使用,例如在宏get_active_bhs(12480行)中就使用了这种技术
?   bh_base(14858行)——这是一组简单的指向下半部分函数的指针。
?   未命名的enum——从14866行开始的未命名的enum为内核使用的每一个下半部分
指定了一个符号名称。例如,为了把计数器的下半部分标记为活动的,你可以这样
的语句:
mark_bh(TIME_BH);
27450行的确就是这样处理的。
操作和IRQ
一个经过仔细选择的小型函数集合处理了操作和IRQ之间的链接和解除链接。本节
就是要讨论这些函数,以及那些从整体上对IRQ系统进行初始化的函数。
init_ IRQ
1597: init_ IRQ初始化IRQ的处理。
1601: 符号CONFIG_X86_ISWS_APIC是为SGI虚拟工作站以及SGI的基于x86的工作站
流水线而设置的。虽然同样基于x86的CPU,虚拟工作站不能和基于IBM PC的体系结
构共享很多其它特性——特别是如同你看到的,它们的中断处理有些不同。我们以
后将忽略虚拟工作站所特有的代码。
1609: 建立中断描述符表,给32项到95项(十进制)赋缺省值。在这个过程中使
用了set_nitr_gate(6647行),该函数很快就会介绍到。
1651: 建立IRQ 2(级联中断)和IRQ13(为FPU使用——请参看955行)。和这两
个IRQ有关的irqaction结构分别是irq2(979行)和irq13(974行)。
init_ISA_irqs
1578: 该函数填充irq_desc数组,为ISA总线类型的机器(也就是所有标准PC)初
始化所有IRQ。虽然该函数没有声明为static类型的,也没有使用__initfunc标签
标记,但是它只会被init_ IRQ调用。因此,只有在内核初始化过程中这个函数才
是必要的。
1583: 对irq_desc中的每一个元素,系统为status,action和depth成员赋与了不
会惹人反对的,也不会使人吃惊的的缺省值。
1589: 原来的(在PCI之前)IRQ使用i8259A_irq_type(723行)处理。
1592: 编号比较高的IRQ初始化为no_irq_type(701行),这是一个必要的空处理
程序。后来它们可能会改变——实际上,如果你使用了PCI卡,就确实会改变,就
象现在的大多数PC一样。
set_intr_gate
6647: set_intr_gate在x86CPU的中断描述符表(IDT)中建立一个项。在基于
x86的系统中发生的每一个软件中断和硬件中断都有一个编号,这个编号被CPU用作
是对这个表的索引。(包括系统调用中断——编号为0x80——在第5章中我们已经
介绍过了。)表中相关的项是中断发生时(内核)函数需要跳转到的地址。
setup_x86_irq
1388:setup_x86_irq给指定的IRQ增加了一个操作(一个struct irqaction结构)
。例如,在6088行使用它来记录定时器的中断。它还可以通过request_irq(1439
行)使用,这在下一节介绍。
1398: Linux使用了几种物理的随机源——例如中断——把一系列不可预知的值提
供给设备/dev/random,这是一个有限却具有很高随机性的数据源,还有
/dev/urandom,这是对应/dev/random的无限的但是随机性较小的对应版本。随机
系统作为一个整体在本书中并没有涉及,但是如果你不知道这个概念,这一大部分
代码就会显得十分神秘。
1412: 如果现存的操作列表非空,setup_x86_irq必须保证现存的操作和新的操作
可以共享这个IRQ。
1414: 验证这个IRQ可以和其上现存的struct irqaction结构共享。这种测试是十
分有效的,它部分是基于我们的一些认识:没有必要遍历执行队列中的所有操作,
也没有必要检测它们可能共享的所有情况。除非这两个操作和第一个操作都允许共
享IRQ,否则不会允许第一个操作后的所有操作都进入队列。因此,如果第一个操
作可以共享IRQ,那么队列中的其它操作也就可以共享IRQ;如果第一个操作不能共
享,那么队列中的其它任何操作也都不能共享IRQ。
1420: IRQ正在被共享。setup_x86_irq利用p向前执行操作队列直到末尾,离开时
p指向队列的最后一个元素的next域。它也会增加shared标志的值,这将会在1429
行中被使用。
1427: p现在指向队列中的最后一个元素的next域,如果要共享IRQ,或者p在不共
享的情况下指向irq_desc[irq].action——指向队列的头节点的指针。不管怎样,
指针现在被设置为新的元素了。
1429: 如果还没有操作和这个IRQ关联,irq_desc[irq]的其它部分也就还没有设
置,在这里就需要对其初始化了。特别要注意1433行中为这个IRQ调用了startup函
数。
request_irq
1439: request_irq从提供的值中创建一个struct irqaction结构,并将其加入对
应给定的IRQ的struct irqaction列表中。(如果你对C++和Java比较熟悉,可以把
它当作是操作的构造函数。)它的实现非常简单明了。
1448: 对一对输入值进行健全性检测。注意没有必要测试irq是否小于0,因为它
是一个无符号数。
1453: 动态分配新的struct irqaction结构。为此目的使用的函数kmalloc在第8
章中简单介绍。
1458: 填充新的操作并使用setup_x86_irq将其加入操作列表。
free_irq
1472: free_irq是request_irq的补数(inverse)。如果request_irq类似于操作
的构造函数,那么这就是操作的析构函数。
1481: 在确保irq在范围内以后,free_irq找到有关的irq_desc项并且开始遍历操
作列表。
1483: 除非它有正确的设备ID,否则就忽略这个队列元素。
1487: 把这个元素从队列中分离出来并且释放其所占用的内存。
1489: 如果现在操作队列为空——也就是如果队列中只有唯一一个元素没有被链
接——设备就会被关闭。
1495: 如果控制流程执行到这里,就意味着free_irq处理了整个操作列表而没有
发现匹配的dev_id。如果发现了匹配对象,1493行的goto语句就已经跳过了本行。
因此,这个试图释放IRQ操作的努力是错误的;在这种情况下free_irq会打印出一
条警告信息对当前状况进行描述。
prove_irq_on
1506: probe_irq_on实现了内核IRQ自动探测的重要的一部分。阅读14889行开始
的标题注释就得到了对整个进程的描述。根据描述我们知道这里要作的工作(只)
是执行步骤三:暂时使能所有没有定义的IRQ,以使得probe_irq_on的调用者可以
检测它们。
1514:  对于除IRQ 0之外的每一个IRQ,如果这个IRQ还没有与之相关的操作,
probe_irq_on会记录下这个IRQ正在自动探测的事实并启动关联设备。顺便说明一
下,我不认为有任何的原因使这个循环向后执行。
1524: 忙等待约十分之一秒时间以允许生成伪中断的设备取消自己。
1530: 循环再次遍历所有的IRQ,这一次要过滤出所有生成伪中断的设备。这个循
环每次重复执行都是从1开始而不是从0开始,这是因为不需要自动检测的IRQ都被
忽略掉了,而IRQ0是从来都不会自动检测的。速度在这里也是一个问题;在十分之
一秒的延时之后——这是很长的一段时间,即使是从慢速的CPU的观点来看也是如
此——一个循环或多或少都是有些不合理的。
1537: 如果设备在1524行的等待过程中触发了中断,这个中断可能就是伪中断:
在此期间系统应该还没有和设备通讯过,因此设备也应该还没有和系统通讯过。因
此自动探测位将被清空,处理程序再次关闭。
1544: 返回特殊数字0x12345678,其原因将在下面的讨论中进行说明。
prove_irq_off
1547: prove_irq_off实现了IRQ自动探测的另外一部分重要的内容。这里的工作
是决定对探测到的哪一个IRQ做出响应,并返回其中的一个IRQ。
1551: 检测名字很容易让人误解的参数unused和probe_irq_on返回的特殊数字是
否相同。调用者假定象下面这样处理:
    magic = prove_irq_on()
    /* … */
    probe_irq_off(magic);
    如果偶然使用了其它的方法调用了probe_irq_off(例如,如果由于其它一些逻辑
调用者偶尔跳过了对probe_irq_on的调用),那么提供的参数可能不会包含正确的
值。可能更重要的是这个参数给正在编写代码使用这个函数的程序员提供了一些信
息:在研究其参数应该是什么的时候,你会发现在调用它的时候一直遵守的规则。
这种规则很容易就被过度使用。
    通过对紧随的错误消息的严格调整,似乎该函数的早期版本中可能已经在其参数
中采用了调用者的地址。如果的确如此,这个测试就具有了第三种目的:把仍然不
正确使用这个函数的调用者检测出来。
1557: 循环遍历所有的IRQ,搜寻响应调用者探测的所有设备。这个循环也可以从
1开始循环,这和前面讨论的probe_irq_on的原因是相同的。
1560: 内核没有试图自动检测这个IRQ上的任何内容;它跳到了下一个IRQ。
1563: IRQ_INPROGRESS标志指明了该IRQ的一个中断已经到达。由于
probe_irq_on可能捕获所有的伪中断,假定这是对探测的真实响应。成功地自动检
测到IRQ的数量因此而增一,同时保存第一次的数字。
1568: 不管是否成功自动探测到IRQ,自动探测标志都要减少,并且再次结束处理
程序。
1573: 如果不止一个IRQ被成功地自动探测到,就通过否定的irq_found来通知调
用者。
1575: 返回irq_found——0,或者(可能是经过求反的)第一个成功地自动探测
到的IRQ号。注意如果发现了设备,则返回值决不会是0,因为内核不会试图自动检
测IRQ 0。因此当没有自动检测到IRQ时,probe_irq_off就返回0。
硬件中断处理程序和下半部分
x86系列的实际中断处理程序是微不足道的;在最低的层次上,这是通过反复使用
BUILD_IRQ宏(1886行)建立了一系列小汇编函数而实现的。BUILD_ IRQ自己被BI
宏调用(866行),这个宏又顺次被BUILD_16_IRQS宏(869行)调用,该宏在878行
到895行的代码中用来建立汇编程序。这一连串的宏调用的目的仅仅是试图减少必
须编写的代码数量和复杂度——我们应该使用256次对BUILD_IRQ的调用,而不是
16次对BUILD_16_IRQS的调用。
汇编程序和如下代码相类似:
IRQ0x00_interrupt:
  push1 0x00-256
  jmp common_interrupt
也就是每一次都简单地把它的IRQ号(减去256,原因在1897行有论述)压入堆栈并
且跳转到正常的中断程序。
正常的中断处理程序是调用common_interrupt,该函数也十分简短。它是使用
BUILD_COMMON_IRQ宏(1871行)建立的,在为do_IRQ进行安排之后简单调用
do_IRQ返回给from_intr(233行)——这是第5章中介绍的系统调用的一部分。随
后将要介绍的do_ IRQ(1362行)负责查看中断是否已经被处理了。
在介绍这些代码之前,从总体上观察一下在处理单个中断时这些部分如何组织在一
起是很有帮助的:
1.  CPU跳转到IRQ0xNN_interrupt程序(其中的NN是中断号),它将其唯一的中断
号压入堆栈并跳转到common_interrupt。
2.  common_interrupt调用do_IRQ并保证当do_IRQ返回时控制流程能够转向
ret_from_intr。
3.  do_IRQ调用中断处理器芯片独有的代码——直接和芯片通讯的代码,如果需要
就用它来处理中断。对于PC体系结构中流行的8259A控制器芯片,处理函数是
do_8259A_IRQ,在这里提到它仅仅是为了举一个例子而已。
4.  do_8259A_IRQ暂时禁止正在处理的特殊IRQ,调用handle_IRQ_event,接着重新
使能这个IRQ。
5.  handle_IRQ_event为慢速IRQ使能中断,或者为处理快速IRQ而将这些中断保持
在禁止状态。接着遍历一个已经和这个IRQ建立联系的函数队列,并依次调用这些
函数。由于中断为慢速IRQ而使能,这里就是慢速IRQ的处理程序可能被其它中断所
中断的地方。在执行完队列中的所有函数之后,handle_IRQ_event禁止中断并返回
控制器所特有的处理函数,而该函数将返回到do_IRQ。
6.  do_IRQ处理所有挂起等待的下半部分,接着返回。如同你已经知道的那样,它
要返回到ret_from_intr。第5章中介绍了从此之后的处理内容。
do_IRQ
1375: 更新内核的一些统计数字并调用与该IRQ相关的处理函数。对于一些老式
PC上的标号较小的IRQ来说,其处理程序是handle_IRQ_event。
1383: 如果下半部分是激活的,内核现在就使用do_bottom_half(29126行)对它
们进行处理。
do_IRQ_event
1292:do_IRQ_event为do_8259A_IRQ(821行)负担了大半重要的工作。本书中没
有涉及到的其它一些代码也会调用这个函数。
1302:与其描述(12094行)正好相反,SA_INTERRUPT标志并不是一个no_op。如果
没有设置该标志,在接下来的代码中就允许中断。这是快速中断和慢速中断之间历
史上遗留下来的区别,这一点我们已经讨论过了。(处理这两种类型中断的代码通
常有很多差异,但是结果是相同的——代码已经被处理得更加出色了。)恰当的说
,这个标志似乎是大多数情况下都为慢速设备使用——顾名思义,就是软盘设备。

1305: 通过调用每一个的处理函数来遍历执行这个IRQ的操作队列(队列头是由调
用者提供的)。
1310: 这里的中断触发是用来为/dev/random和/dev/urandom增加一些随机信息—
—从大体上看来,大部分中断都是随机发生的。
1312: 禁止中断(当条件具备时调用者可以再次允许这些中断)。
do_bottom_half
29126:Linux代码中有三处调用了do_bottom_half:26707行,243行,1384行。(
你会发现,其中的两个是在体系结构特有的文件中的;在非x86平台体系结构特有
的文件中对应部分也会调用这个函数。)因此,下半部分是用来处理如下三种情况
的:
?   当决定随后哪一个进程应该获得CPU时。
?   当从系统调用中返回时。
?   在从do_IRQ中返回之前——也就是说,在每个中断之后。代码中的注释暗示了在
内核的未来版本中不一定总在这里运行下半部分。
29130:下半部分的一个理想特性是在某一时刻只能有一个下半部分处于运行状态
。这种特性在这里,也就是锁定(locking)在UP代码中体现出重要意义的位置之
一,得到了强化。首先调用soft_trylock(UP版本在12559行——第10章中可以查
看所有的SMP版本),只有在local_bh_count[cpu]原来为0时这个函数才把
local_bh_count [cpu]设置为1并返回真值。根据17479行,对于UP来说cpu总是0,
而且你应该注意到softirq_trylock自己是不能被中断的,因为在这里中断已经被
禁止了。softirq_trylock和对应的softirq_endlock(12561行),就是仅仅为了
以下目的而退出的,而不存在其它原因:协助保证这个下半部分不会被其它下半部
分中断(虽然它们可以被上半部分所中断)。
29131:如果成功获得了锁,那么该函数就尝试另一个函数,10736行的
hardirq_trylock。它只报告当前执行进程是否位于hardirq_enter/hardirq_exit
对(10739行和10740行)之间。对于UP,这两者的定义是和irq_enter与irq_exit
(1810行和1811行)两者的定义相同的;后两个函数在handle_IRQ_event中使用,
当然在其它我们所没有讨论到的地方也对它们有所引用。这些宏协同工作来保证
__cli和__sti对能够正确的进行嵌套——由于CPU不会嵌套使用这些宏,我们必须
保证不会使用__sti处理其它的__cli,而这也不是我们所希望的。
29132:没有其它下半部分在运行,而且do_bottom_half有权使能硬件中断。因此
,它就使能硬件中断,运行下半部分,接着再次禁止中断。
29135:释放该函数已经获取的锁并返回。
run_bottom_halves
29110:现在内核能够运行挂起等待的下半部分。
29115:存储当前局部变量active中活动的——也就是被标记过的——下半部分的
集合,并使用clear_active_bh宏(12481行)清空全局变量bh_active中的设置位
。对bh_active中这些位的清除将同时取消对所有下半部分的标记。
现在你就应该可以看到下半部分有时候是批量处理的,这一点在前面已经进行了没
有论证的说明。此处中断是被使能的,因此如果在run_bottom_halves把
bh_active拷贝到active之前就有中断触发并标记已经标记过的下半部分,那么上
半部分就已经运行了两次,然而下半部分只运行了一次。还有,由于这个中断可以
被自己中断,在下半部分运行一次的时候上半部分就已经运行了三次,等等。但是
随着递归调用数量的增长,这很快就会变得不再可能了。
代码有可能忽略某个下半部分吗?假定中断在最坏的时刻发生:在29115行和
29116行之间——也就是在拷贝bh_active之后,但是在清空其中的设置位之前。下
面是三种可能的情况:
?   新的中断没有标记下半部分。这种情况显然不会引起什么问题——中断之后处理
的下半部分集合和前面的集合相同,因此run_bottom_halves仍将运行所有它应该
运行的下半部分。
?   新的中断标记了一个已经标记过了的下半部分。这种情况也不会引起问题——
run_bottom_halves不管怎样都要运行这个下半部分,而且在新中断返回之后就运
行它。
?   新的中断标记了一个前面没有标记过的下半部分。在这种情况下,当
run_bottom_halves遍历执行所有的下半部分时,active就不再和bh_active匹配了
。然而,由于clear_active_bhs只会清空active集合中设置的位,所以29116行不
会清空bh_active中新近标记的位。clear_active_bhs使用atomic_clear_mask(
10262行),后者简单的对active集合中的设置位进行位AND运算,而并不处理其余
部分。因此,当run_bottom_halves执行循环时,就不会立刻对新近标记的下半部
分进行处理;但是由于它的位仍然在bh_active中设置,run_bottom_halves就仍然
会在最后对它进行处理。更为准确的说法是,通过这种方法跳过的下半部分会在处
理随后的一个定时器中断的过程中一起处理,这只是一个瞬间的延迟而已——或者
如果有其它中断首先发生了,那么这段时间延迟会更短。因此,迷途的下半部分通
常的等待时间不会超过百分之一秒,而且根据定义,下半部分毕竟不是时效性要求
非常高的,这种少量延时不会引起任何问题。
29118:同时遍历执行bh_base数组和active中的位。如果active中最低的位被设置
了,就调用相关的下半部分;接着循环推进到下一个bh_base项和active中的下一
位继续执行。
    因为这是一个do/while循环,所以它最少执行一次。部分原因是由于在调用
do_bottom_half之前,调用者都要检测是否有下半部分需要处理,所以这个循环最
少要执行一次。在一些情况下这种检测会执行到do_bottom_half本身中,但是如果
没有下半部分需要运行,在调用之前执行测试就能够节省函数调用的开销。不管怎
样,我们很容易就可以看出即使没有下半部分需要运行,这个循环也可以正确执行
;虽然这会浪费一些时间,但是不这样就会引起错误。
29123:当active中没有任何位被设置时,循环就终止退出。由于在循环执行的过
程中active是不断移位的,这样就同时测试了其余的位,而没有必要对它们的每一
个都进行循环处理。
时间
本节通过观察一个中断的例子——定时器中断——的工作方式来使你能够将中断和
下半部分的知识融会贯通起来。
定时器中断函数timer_interrupt是和6086行的IRQ 0相关的。此处
回复

使用道具 举报

 楼主| 发表于 2003-10-27 09:40:00 | 显示全部楼层
Linux内核注释——第7章 进程和线程  

操作系统的存在归根结底是为了提供一个运行程序的空间。按照Unix的术语,将正
在运行的程序为进程。Linux内核和其它Unix变种一样,都是采用了多任务技术;
它可以在许多进程之间分配时间片从而使这些进程看起来似乎在同时运行一样。这
里通常是内核对有关资源的访问作出仲裁;在这种情况下,资源就是CPU时间。
进程传统上都有唯一的执行程序的上下文——这是说明在某个时刻它正在处理一项
内容的流行的方法。在给定的时刻,我们可以精确地知道代码的哪一部分正在执行
。但是有时我们希望一个进程同时处理多件事情。例如,我们可能希望Web浏览器
获取并显示Web页,同时也要监视用户是否点击停止按钮。只为监视停止按钮而运
行一个全新的程序显然是不必要的,但是对于Web浏览器来说要对其时间进行分隔
也并不总是非常方便——获取一些Web页信息,检测停止按钮,再获取一些Web页信
息,再重新检测停止按钮,等等。
对于这个问题的比较流行的解决方法是线程。从概念上来说,线程是同一个进程中
独立的执行上下文——更简单一点地说,它们为单一进程提供了一种同时处理多件
事情的方法,就像是进程是一个自行控制的微缩化了的多任务操作系统。同一线程
组中的线程共享它们的全局变量并有相同的堆(heap),因此使用malloc给线程组
中的一个线程分配的内存可以被该线程组中的其它线程读写。但是它们拥有不同的
堆栈(它们的局部变量是不共享的)并可以同时在进程代码不同的地方运行。这样
,你的Web浏览器可以让一个线程来获取并显示Web页,同时另外一个线程观测停止
按钮是否被点击,并且在停止按钮被点击时停止第一个线程。
和线程等价的一种观点——这是Linux内核使用的观点——线程只是偶然的共享相
同的全局内存空间的进程。这意味着内核无需为线程创建一种全新的机制,否则必
然会和现在已经编写完成的进程处理代码造成重复,而且有关进程的讨论绝大多数
也都可以应用到线程上。
当然,以上的说明仅仅适用于内核空间的线程。实际中也有用户空间的线程,它执
行相同的功能,但是却是在应用层实现的。用户空间的线程和内核空间的线程相比
有很多优点,也有很多缺点,但是有关这些问题的讨论超出了本书的范围。而使人
更加容易造成混淆是一个名为kernel_thread(2426行)的函数,尽管该函数被赋
予了这样一个名字,但是它实际和内核空间的线程没有任何关系。
部分是由于历史的原因,部分是由于Linux内核并没有真正区分进程和线程这两者
在概念上的不同,在内核代码中进程和线程都使用更通用的名字“任务”来引用。
根据同样的思路,本书中所出现的 “任务”和“进程”具有相同的意义。
调度和时间片
对CPU访问的裁决过程被称为调度(Scheduling)。良好的调度决策要尊重用户赋
予的优先级,这可以建立一种所有进程都在同时运行的十分逼真的假象。糟糕的调
度决策会使操作系统变得沉闷缓慢。这是Linux调度程序必须经过高度优化的一个
原因。
从概念上来说,调度程序把时间分为小片断,并根据一定的原则把这些片断分配给
进程。你可能已经猜到,时间的这些小片断称为时间片。
实时进程
Linux提供了三种调度算法:一种传统的Unix调度程序和两个由POSIX.1b(原名为
POSIX.4)操作系统标准所规定的“实时”调度程序。因此,本书中有时会使用实
时进程(从技术上考虑,系统使用术语“非实时进程(nonrealtime process)”
来作为实时进程的对应,虽然我更倾向于使用另外一个术语unrealtime process)
。不要过分计较“实时”这个术语,虽然——如果从硬件的角度来看待这个问题,
实时意味着你可以得到有关操作系统的某种性能保证,例如有关中断等待时间的承
诺,但是这一点在Linux实时调度规则中并没有提供。相反的,Linux的调度规则是
“软件实时”,也就是说如果实时进程需要,它们就只把CPU分配给实时进程;否
则就把CPU时间让出给非实时进程。
但是如果你真正需要,一些Linux的变种也承诺提供一种“硬实时”。但是,在当
前的Linux内核中——因此也就是在本章中——“实时”仅指“软件实时”。
优先级
非实时进程有两种优先级,一种是静态优先级,另一种是动态优先级。实时进程又
增加了第三种优先级,实时优先级。优先级是一些简单的整数,它代表了为决定应
该允许哪一个进程使用CPU的资源时判断方便而赋予进程的权值——优先级越高,
它得到CPU时间的机会也就越大:
?   静态优先级——被称为“静态”是因为它不随时间而改变,只能由用户进行修改
。它指明了在被迫和其它进程竞争CPU之前该进程所应该被允许的时间片的最大值
。(但是也可能由于其它原因,在该时间片耗尽之前进程就被迫交出了CPU。)
?   动态优先级——只要进程拥有CPU,它就随着时间不断减小;当它小于0时,标记
进程重新调度。它指明了在这个时间片中所剩余的时间量。
?   实时优先级——指明这个进程自动把CPU交给哪一个其它进程:较高权值的进程
总是优先于较低权值的进程。因为如果一个进程不是实时进程,其优先级就是0,
所以实时进程总是优先于非实时进程的。(这并不完全正确;如同后面论述的一样
,实时进程也会明确地交出CPU,而在等待I/O时也会被迫交出CPU。前面的描述仅
限于能够交付CPU运行的进程)
进程ID(PIDs)
传统上每个Unix进程都有一个唯一的标志符,它是一个被称为进程标志符(PID)
的,范围在0到32,767之间的整数。PID 0和PID 1对于系统有特定的意义;其它的
进程标识符都被认为是普通进程。在本章后面对get_pid的讨论中,你会看到PID是
如何生成和赋值的。
在Linux中,PID不一定非要唯一——虽然通常都是唯一的,但是两个任务也可以共
享一个PID。这是Linux对线程支持的一个副作用,这些线程从概念上讲应该共享一
个PID,因为它们是同一个进程的一部分。在Linux中,你可以创建两个任务,并且
共享且仅共享它们的PID——从实际使用角度讲它们不会是线程,但是它们可以使
用同一个PID。这并没有多大的意义,但是如果你希望这样处理,Linux是支持的。

引用计数
引用计数是多个对象之间为共享普通信息而广泛使用的技术。使用更通用的术语来
说,一个或多个“容器对象”携带指向共享数据对象的指针,其中包含了一个称为
“引用计数(Reference Count)”的整数;这个引用计数的值和共享数据的容器
对象的个数相同。希望共享数据的新容器对象将被赋予一个指向同一结构的指针,
并且递增该共享数据对象的引用计数。
当容器对象离开时,就递减共享数据的引用计数,并做到“人走灯熄”——也就是
当引用计数减小到0时,容器对象回收共享对象。图7.1阐述了这种技术。


图7.1 引用计数

就象你随后会看到的那样,Linux通过使用引用计数技术来实现线程间的数据共享

权能
在早期的Unix中,你或者是root用户,或者不是。如果你是root,你几乎可以进行
任何希望进行的操作,即使你的想法实际上十分糟糕,例如删除系统引导盘上的所
有文件。如果你不是root,那么你就不可能对系统造成太大的损害,但是你也不能
执行任何重要的系统管理任务。
不幸的是,很多应用程序的需要都介于这两个安全性极端之间。例如,修改系统时
间是只有root才能执行的操作,因此实现它的程序必须作为root运行。但是因为是
作为root运行的,修改系统时间的进程也就能处理root可以完成的任何事情。对于
编写良好的程序来说并不会造成问题,但是程序仍然会有意无意地把系统搞得一团
糟。(数不清的计算机攻击事件都是欺骗root去运行一些看似值得信任的可执行代
码,造成了一些恶作剧。)
这些问题中有一些可以通过正确使用组和诸如sudo之类的程序而避免,但是有一些
则不行。对于某些重要的操作,虽然你可能只想允许它们执行一两种权限操作,你
也只能给予这些进程普通root访问许可。Linux对于这个问题的解决方法是使用从
现在已经舍弃了的POSIX草案标准中抽取出来的思想:权能。
权能使你可以更精确的定义经授权的进程所允许处理的事情。例如,你可以给一个
进程授予修改系统时间的权力,而没有授予它可以杀掉系统中的其它进程、毁坏你
的文件、并胡乱运行的权力。而且,为了帮助防止意外地滥用其优先级,长时间运
行的进程可以暂时获得权能(如果允许),只要时间足够处理特殊的零碎工作就可
以了,在处理完这个零碎的工作以后再收回权能。
在本书的编写期间,权能仍然处于开发状态。为了完全实现权能的预期功能,开发
者们还必须要实现一些新的特性——例如,目前还没有内核支持将程序的权能附加
到文件本身中。这样所造成的一个后果是Linux有时仍要检测进程是否作为root运
行,而不是检测所进程需要的特殊权能。但是迄今为止已经实现了的内容仍然是十
分有用的。
进程在内核中是如何表示的
内核使用几个数据结构来跟踪进程;其中有一些和进程自身的表示方法是密切相关
的,另外一些则是独立的。图7.2阐述了这些数据结构,随后就会对它们进行详细
介绍。
图7.2 管理任务使用的内核数据结构

16325:表示进程的内核数据结构是struct task_struct。我们暂时向前跳过这个
结构的定义,继续往下看。它相当大,但是可以从逻辑上划分为很多部分。随着本
章讨论的展开,你将会逐渐清楚它们每一部分的意义。在阅读的过程中,要注意这
个结构的很多部分都是指向其它结构的指针;这在子孙进程和祖先进程希望共享指
针所指向的信息时可以灵活运用——很多指针都指向正在被引用计数的信息。
16350:任务本身使用struct task_struct结构的next_task和prev_task成员组成
一个循环的双向链接列表,它被称为任务队列。的确,这忽略了一个事实,它们在
中心数组task(很快就会讨论)中早已存在了。最初这看起来可能有些奇怪,但实
际上这是十分有用的,因为这样允许内核代码可以遍历执行所有现存的任务——也
就是task中所有经过填充的时间片——而无须浪费时间跳过空时间片。实际上对这
个循环的访问是如此频繁,以至于在16898行单独为它定义了一个宏
for_each_task。
    虽然for_each_task是单向的,但是它有一些值得注意的特性。首先,注意到循环
的开始和末尾都是init_task。这是很安全的,因为init_task从来不会退出;因此
,作为标记它一直都是可用的。但是,注意到ini_task本身不是作为循环的一部分
而访问的——这恰好就是你使用这个宏时所需要的东西。还有,作为我们关心的一
小部分,你总是使用next_task成员直接向前遍历执行列表的;不存在相关的向后
执行的宏。也没有必要需要这样一个宏——只有在需要及时把任务从列表中处理清
除时才需要使用prev_task成员。
16351:Linux还保持一个和这个任务列表类似的循环的双向任务列表。这个列表使
用struct task_struct结构的prev_run成员和next_tun成员进行链接,基本上是作
为队列来处理的(这真值得让人举杯庆祝);出于这个原因,这个列表通常被称为
运行队列(run queue)。对于next_task来说,只是因为需要高效地将一个项移出
队列才会使用到prev_run成员;对于这个列表的遍历循环执行通常都是使用
next_run向前的。同样,在这个任务队列中也使用init_task来标记队列的开始和
末尾。
    通过使用add_to_runqueue(26276行)能够将任务加入队列,而使用
del_from_runqueue(26287行)则把任务移出队列。有时候分别使用
move_first_runqueue(26318行)和move_last_runqueue(26300行)把它们强制
移动到队列的开头和末尾。注意这些函数都是局限于kernel/sched.c的,在别的文
件中不会使用prev_run和next_run域(特别是在kernel/fork.c文件中的进程创建
期间);这是十分恰当的,因为只有在调度时才需要运行队列。
16370:首先,任务能够组成一个图,该图的结构表达了任务之间的家族关系;由
于我不清楚这个图所使用的通用术语,我就称它为进程图(process graph)。这
和next_task/prev_task之间的连接根本没有关系,在那里任务的位置是毫无意义
的——只是一个偶然的历史事件而已。每一个struct task_struct中有五个指向进
程图表中自己位置的指针。这五个指针在从16370行到16371行的代码中被定义。
?   p_opptr指向进程的原始祖先;通常和p_pptr类似。
?   p_pptr指向进程的当前祖先。
?   p_cptr指向进程的最年青(最近)子孙。
?   p_ysptr指向进程的下一个最年青(下一个最近)兄弟。
?   p_osptr指向进程的下一个最古老(下一个最远)兄弟。


图7.3  进程图

图7.3说明了它们之间的关系(整个链接集合都以标号为“Me”的节点为核心)。
这个指针的集合还提供了浏览系统中进程集合的另外一种方法;显然,在处理诸如
查找进程祖先或者查找列表中进程子孙时这个指针特别有用。这个指针是由两个宏
维护的:
?   REMOVE_LINKS(16876行)从图中移出指针。
?   SET_LINKS(16887行)向图中插入指针。
这两个宏都可以调整next_task/prev_task的链接。如果你仔细研究一下这两个宏
,你就会发现它们只是增加或者删除叶子进程——而从不对拥有子孙进程的进程进
行处理。
16517:task定义为由指向struct task_struct结构的指针组成的数组。这个数组
中的每一个项代表系统中的一个任务。数组的大小是NR_TASKS(在18320行设置为
512),它规定了系统中可以同时运行的任务数量的上限。由于一共有32,768个可
能的PID,由于数组不够大,要通过它们的PID直接索引系统中所有任务显然是不可
能的。(也就是task未必是由PID i指明的任务。)相反,Linux使用其它的数
据结构来帮助系统管理这种有限的资源。
16519:自由时间片列表tarray_freelist拥有一个说明task数组中自由位置的列表
(实际上是一个堆栈)。它在27966行和27967行初始化,接着被两个在16522行到
16542行定义的内联函数所使用。在SMP平台上,对于tarray_freelist的访问必须
受自旋锁taskslot_lock(23475行)的限制。(自旋锁在第10章中详细讨论。)
16546:pidhash数组有助于把PID映象到指向struct task_struct的指针。
pidhash在27969行和27970行初始化,此后它被一系列在16548行到16580行定义的
宏和内联函数所操纵。这些最终实现了一个普通的哈希表。注意,为了处理hush记
录,维护pidhash的函数使用了struct task_struct结构中的两个成员——
pidhash_next(16374行)和pidhash_pprev(16375行)。通过使用pidhash,内核
可以通过其PID有效地发现任务——虽然这种方式仍然比直接查找要慢。
    仅仅是为了好玩,你可以自己证明这个哈希函数——pid_hashfn,16548行——提
供了一个均匀覆盖其域0到32,767(所有有效的PID)的发行版本。除非你所谓的“
好玩”的概念和我不一样,否则你会同我一样感到有趣。
    这些数据结构提供了有关当前运行系统的很多信息,但是这也需要付出代价:每
当增加或删除进程时这些信息必须能够得到正确维护,否则系统就会变得混乱不堪
。部分出于实现这种正确的维护非常困难的考虑,进程只在一个地方创建(使用
do_fork,后面会讨论),也只在一个地方删除(使用release,也在后面中讨论)

如果我们能把task处理为32,768个struct task_struct结构组成的数组,其中的每
一项代表一个可能的PID,那么至少可以消除一部分这种类型的复杂性。但是这样
处理会大量增加内核对于内存的需求。每一个struct task_struct结构在UP平台上
占用964字节,在SMP平台上占用1,212字节——取整以后,近似的数字是1K。为了
容纳所有这些结构,task会像气球一样迅速膨胀到32,768K,也就是32M!(实际情
况会更糟糕:我们尚未提到的有关任务的额外内存开销会把这个数字增长8倍——
也就是256M——而且不要忘记了,这些开销实际上都还没有运行一个任务。)此外
,x86的内存管理硬件把活动任务的数量限制在4,000左右;这一主题在下一章介绍
。因此,数组中大多数的空间都会不可避免地被浪费了。
在目前的实现中,如果没有进程在运行,task仅仅是512个4字节的指针,总共才
2K。如果我们考虑到那些附加的数据结构会占用一些额外开销,可能有一些超过这
个数字,但是比起32M来还差得远呢。即使是task中的每一项都使用了,而且每个
struct task_struct结构也都分配了,总共使用的内存也才不过大约512K。应用程
序能够忽略这种微小的区别。
进程状态
在一个给定的时间,进程处于下面注释中描述的六种状态中的一种。进程的当前状
态被记录在struct task_struct结构的state成员中(16328行)。
16188:TASK_RUNNING意味着进程准备好运行了。即使是在UP系统中,也有不止一
个任务同时处于TASK_RUNNING状态——TASK_RUNNING并不意味着该进程可以立即获
得CPU(虽然有时候是这样),而是仅仅说明只要CPU一旦可用,进程就可以立即准
备好执行了。
16189:TASK_INTERRUPTIBLE是两种等待状态的一种——这种状态意味着进程在等
待特定事件,但是也可以被信号量中断。
16190:TASK_UNINTERRUPTIBLE是另外一种等待状态。这种状态意味着进程在等待
硬件条件而且不能被信号量中断。
16191:TASK_ZOMBIE意味着进程已经退出了(或者已经被杀掉了),但是其相关的
struct task_struct结构并没有被删除。这样即使子孙进程已经退出,也允许祖先
进程对已经死去的子孙进程的状态进行查询。在本章后面我们会详细介绍这一点。

16192:TASK_STOPPED意味着进程已经停止运行了。一般情况下,这意味着进程已
经接收到了SIGSTOP,SIGSTP,SITTIN或者SIGTTOU信号量中的一个,但是它也可能
意味着当前进程正在被跟踪(例如,进程正在调试器下运行,用户正在单步执行代
码)。
16193:TASK_SWAPPING主要用于表明进程正在执行磁盘交换工作。然而,这种状态
似乎是没有什么用处的——虽然该标志符在整个内核中出现了好几次,但是其值从
来没有被赋给进程的state成员。这种状态正在被逐渐淘汰。
进程来源:fork和_ _clone
传统的Unix实现方法在系统运行以后只给出了一种创建新进程的方法:系统调用
fork。(如果你奇怪第一个进程是哪里来的,实际上该进程是init,在第4章中已
经讨论过。)当进程调用fork时,该进程从概念上被分成了两部分——这就像是道
路中的分支——祖先和子孙可以自由选择不同的路径。在fork之后,祖先进程和其
子进程几乎是等同的——它们所有的变量都有相同的值,它们打开的文件都相同,
等等。但是,如果祖先进程改变了一个变量的值,子进程将不会看到这个变化,反
之亦然。子进程是祖先进程的一个拷贝(至少最初是这样),但是它们并不共享内
容。
Linux保留了传统的fork并增加了一个更通用的函数_ _clone。(前面的两个下划
线有助于强调普通应用程序代码不应该直接调用_ _clone,应该从在_ _clone之上
建立的线程库中调用这个函数。)鉴于fork创建一个新的子孙进程后,子孙进程虽
然是其祖先进程的拷贝,但是它们并不共享任何内容,_ _clone允许你定义祖先进
程和子孙进程所应该共享的内容。如果你没有给_ _clone提供它所能够识别的五个
标志,子孙进程和祖先进程之间就不会共享任何内容,这样它就和fork类似。如果
你提供了全部的五个标志,子孙进程就可以和祖先进程共享任何内容,这就和传统
线程类似。其它标记的不同组合可以使你完成介于两者之间的功能。
顺便提一下,内核使用kernel_thread函数(2426行)为了自己的使用创建了几个
任务。用户从来不会调用这个函数——实际上,用户也不能调用这个函数;它只在
创建例如kswapd(在第8章中介绍)之类的特殊进程时才会使用,这些特殊进程有
效地把内核分为很多部分,为了简单起见也把它们当作任务处理。使用
kernel_thread创建的任务具有一些特殊的性质,这些性质我们在此不再深入介绍
(例如,它们不能被抢占);但是现在主要需要引起注意的是kernel_thread使用
do_fork处理其垃圾工作。因此,即使是这些特殊进程,它们最终也要使用你我所
使用的普通进程的创建方法来创建。
do_fork
23953:do_fork是实现fork和_ _clone的内核程序。
23963:分配struct task_struct结构以代表一个新的进程。
23967:给新的struct task_struct结构赋予初始值,该值直接从当前进程中拷贝
而来。do_fork的剩余工作主要包含为祖先进程和子孙进程不会共享的信息建立新
的拷贝。(在本行和整个内核中你可以看到的current是一个宏,它把一个指针指
向代表当前正在执行的进程的struct task_struct结构。这在10285行定义,但实
际上只是对get_current函数的一个调用,而后者的定义在10277行。)
23981:新到达者需要task数组中的一个项;这个项是使用find_empty_process(
23598行——它严格依赖于16532行的get_free_taskslot)找到的。然而,它工作
的方式有点不明显:task数组没有使用的成员不是设置为空,而是设置为自由列表
的下一个元素(使用add_free_taskslot,16523行)。因此,task中没有使用的项
指向链接列表中另外一个task没有使用的项,而tarray_freelist仅仅指向这个列
表的表头。那么,返回一个自由位置就简单地变成了返回列表头的问题了(当然要
把这个头指针指向下一个元素)。更传统的方法是使用一个独立的数据结构来管理
这些信息,但是在内核中,空间总会显得有些不足。
23999:给新的任务赋PID(其中的细节很快就会介绍)。
24045:本行和下面几行,使用该文件中别处定义的辅助函数,根据所提供的
clone_flags参数的值为子孙进程建立祖先进程的数据结构中子孙进程所选择部分
的拷贝。如果clone_flags指明相关的部分应该共享而不是拷贝,这时辅助函数(
help function)就简单地增加引用计数接着返回;否则,它就创建新进程所独有
的新的拷贝。
24078:到现在为止,所有进程所有的数据结构都已经设置过了,但是大部分跟踪
进程的数据结构还没有被设置。系统将通过把进程增加到进程图表中开始设置它们

24079:通过调用hash_pid把新的进程置入pidhash表中。
24088:通过调用wake_up_process(26356行)把新的进程设置为TASK_RUNNING状
态并将其置入运行队列。
注意到现在不止是struct task_struct结构被设置了,而且所有相关的数据结构—
—自由时间片列表,任务列表、进程图、运行队列和PID hash表——这些都已经为
新的到达者正确地进行了修改。恭喜你,你现在已经得到了一个健康的子孙任务。

PID的分配
PID是使用get_pid函数(23611行)生成的,该函数能够返回一个没有使用的PID。
它从last_pid(23464行)开始——这是最近分配的PID。
内核中使用的get_pid的版本是内核复杂性和速度频繁折中的一个例子;这里速度
更为重要一些。get_pid经过了高度优化——它比直接向前的实现方法要复杂的多
,但是速度也要快的多。最直接的实现方法将遍历执行整个任务列表——典型的情
况可能有几十项,有时候也可能成百上千项——对每一个可能的PID进程检测并找
出适当的值。我们见到的版本有时是必须执行这些步骤的,但是在大多数情况下都
可以跳过。这一结果被用来帮助加速进程创建的操作,它在Unix上慢得臭名卓著。

如果我们所需要的只是要为每一个运行进程都快速计算一个各不相同的整数,那么
这里已经有现实可用的方法:只要取在task数组中进程的索引就可以了。平均说来
,这肯定要比现在的get_pid速度要快——毕竟,这无须遍历任务列表。不幸的是
,很多现存的应用程序都假定在一个PID可以再重用之前都需要等待一段时间。这
种假定在任何情况下都是不安全的,但是在如果为了这些程序的问题而将内核牵涉
进去可能仍然是一个很糟糕的思想。现存的PID分配策略速度仍然很快,并且它偶
尔还有可以暴露这些应用程序中的潜在缺陷的优点,如果有的话(如果你认为这是
一种优点)。
get_pid
23613:next_safe变量是一个为加快系统运行速度而设定的变量;它保持记录了可
能保留的次最低的的候选PID。(更正确的应该把它命名为next_unsafe。)当
last_pid递增并超过这个范围时,系统应该检测整个任务列表来保证这个候选PID
是否仍在被保留着(原来保留这个PID的进程现在可能已经运行完了)。由于遍历
这个任务列表可能会很慢,所以只要可能就应该避免执行这样的操作。因此,在执
行这个遍历的过程中,get_pid要重新计算next_safe——如果有些进程已经死掉了
,这个数字可能现在更大了,因此get_pid可以避免一些将来对任务列表的遍历。
(next_safe是静态的,因此其值在下次get_pid需要分配PID时就会保留下来。)

23616:如果新的进程要和其祖先共享PID,就返回祖先进程的PID。
23620:开始搜寻候选PID寻找未使用的值。位与运算只是通过测试低15位是否置位
来简单测试last_pid的新值是否超过了32,767(最大允许的PID)。我怀疑这些内
核开发者真正需要通过这样做来获得微小的速度优势,但是你永远也不会知道;至
少在这段代码编写期间,gcc还不够敏锐到足以注意到它们的等价性并在生成的代
码中选择稍微快速的形式。
23621:如果last_pid已经超出了允许的最大值,它就会滚动到300。300这个数字
并没有什么魔力——它对于内核并没有特别的意义——这是另外一个加速变量。其
思想是数字比较小的PID通常都属于系统开始运行时就已经创建的,从不会退出的
长时间运行的后台监控程序。由于它们总是占据着数字比较小的PID,所以如果不
考虑对前面几百个值的重用问题,我们将会发现寻找可以使用的PID的过程会快许
多。而且,由于PID的空间是同时允许的任务数(512)的64倍,为了追求速度而损
失一些空间是一种非常值得的。
23622:由于last_pid超出了最大允许的PID,它必然也就超出了next_safe;因此
,后面的if测试也可以跳过。
23624:如果last_pid仍然小于next_safe,其值就可以再用。否则,必须检查任务
列表。
23633:如果取得了last_pid的当前值,它就简单的递增,如果需要就跳转到300,
重新开始循环。初次看的时候,仿佛这个循环会一直运行下去——如果所有的PID
都已经被使用了会出现什么情况呢?但是稍微考虑一下,我们就可以排除这种可能
性:任务列表的最大值和同时并发的任务的最大数是相同的,有效的PID数目要比
这两个数都大得多。因此,循环最终会找到有效的PID;这仅仅是个时间的问题。

23651:get_pid已经发现了一个没有被使用的PID,随后返回该PID。
运行新程序
如果我们能够进行的所有工作只是fork(或者__clone),那么我们就只能一次次
建立同一个进程的拷贝就可以了——这样我们的Linux系统就只能运行在系统中第
一个创建的用户进程init了。Init是很有用的,但是还没有功能如此强大;我们也
还需要处理其它事情。
在我们创建新的进程以后,它通过调用exec就能够变成独立于其它进程的进程了。
(这实际上不止是一个名为exec的函数;而是exec通常用作一个引用一系列函数的
通用术语,所有这些函数基本上都处理相同的事情,但是使用的参数稍微有些不同
。)
因此,创建一个“真正”的新进程——与其祖先不同的程序运行镜像——任务分为
两步,一步是fork,另一步是exec,最后能够得出下面的风格非常熟悉的C代码:

P485 1
(execl是exec家族若干函数中的一个。)
实现所有exec家族函数的底层内核函数是10079行到10141行的do_execve。
do_execve处理三种工作:
?   把一些定义信息从文件读入内存。(do_execve把这个工作交给prepare_binprm
处理。)
?   准备新的参数和环境——这是C应用程序将它作为argc,argv和envp使用的内容

?   装载可以解析可执行文件的二进制处理程序,并让它处理剩余的修改内核数据结
构的工作。
记住这些任务,现在让我们开始仔细研究一下do_execve。
do_execve
10082:代表在使用exec处理进程时所需要记录的全部信息的数据类型是struct
linux_binprm结构(请参看13786行)——我确信binprm是“binary parameters(
二进制参数)”的缩写。do_execve处理自己的工作,并使用这种类型的变量bprm
同那些负责处理其部分工作的函数进行通信。注意到当do_execve返回时bprm就会
被废弃——只有在执行exec时才需要bprm,它并不在该进程的整个生命期中存在。

10087:do_execve通过初始化一个记录新进程参数和环境分配的内存页的微型页表
开始执行。它为这个目的总共需要申请MAX_ARG_PAGES(在13780行宏定义为32)个
页,在x86平台上每一页是4K,因此参数总共可以使用的空间加起来就是
32*4K=128K。作为我个人而言,我很高兴了解到这个内容,因为我偶而会超过这个
限定,通常是在一个具有成百个文件的目录下运行cat*>/tmp/joined之类的东西的
时候——所有这些文件名连接起来可能就超过了128K。我通常是使用xargs程序解
决这个问题,但是我现在也可以通过为MAX_ARG_PAGES重新定义一个比较大的值并
重新编译内核来解决这个问题。至少现在如果这个问题再困扰我,我也知道该如何
增加这一限制了。(可能一些热心的读者会重新编写程序来去掉这段糟糕的限制。
)所以我非常喜欢拥有内核的源代码。
10091:下一步是要打开可执行文件。这不是简单的从文件中读出数据——现在的
焦点是要确保文件存在,这样do_execve就可以清楚是否有必要继续进行处理。如
果这是第一步,而不是首先填充bprm的页表的话,do_execve在执行时有时能够获
得很高的边际效应——如果这样失败了,用来初始化页表的时间就浪费了。然而,
这只在文件不存在时才有用——这不是普通的情况,不值得优化。
10096:继续填充bprm,特别是其argc和envc成员。为了填充这些成员,
do_execve使用count函数(9480行),它通过使用被传递进来的 argv和envp数组
计算非空指针的个数。第一个空指针标志着列表结束,因此在到达空指针时就可以
得到非空指针的个数并将其返回。这开始看起来似乎很可能因此而造成一些效率的
损失:调用do_execve的函数有时早就知道了argv和envp数组的长度。因此可以再
给do_execve增加两个整型参数argc和envc。如果这两个参数都是非负的,那么它
们就可以分别代表两个数组的长度。但是事情并没有这么简单:count同时要检测
它扫描的数组中是否有访问内存的错误发生。强迫(更多的情况是完全信任)
do_execve的调用者来对这些内容进行检测是不正确的。所以目前这样的处理方式
要更好一些。
10115:主要使用copy_strings(9519行)把参数和环境变量拷贝到新进程中。
copy_strings看起来很复杂,但是它要处理的工作十分简单:把字符串拷贝到新进
程的内存空间中,如果需要就给它们分配页。这种复杂性的增长主要出现在对页表
的管理需要和跨越内核/用户空间限制的需要,这一点将在第8章中更详细地介绍。

10126:如果前面的工作可以很好地执行到此处,最后一步是要为新的可执行程序
寻找一个二进制处理程序。如果search_binary_handler成功找到了这种程序,整
个过程就成功运行结束,并返回一个非负值以说明成功。
10134:如果程序运行到了此处,那么前面的几步中肯定发生了错误。系统释放为
新进程的参数和环境分配的所有页,接着必须返回一个负值通知调用者调用过程失
败了。
prepare_binprm
9832: prepare_binprm填写do_execve的重要部分bprm。
9839: 本行开始一些健全性检测,例如要确保执行的是文件而不是目录,并且文
件的可执行位已经设置了。
9858: 如果已经被设置过setuid和setuid位,就根据它们的提示新进程应该把当
前执行的用户作为一个不同的用户(如果setuid被置位)并且/或者把它作为一个
不同组的成员(如果setgid被置位)。
9933: 最后,prepare_binprm从文件中读取前128个字节(而不是像该函数标题注
释里说明的一样是前512个字节)到bprm的buf成员中。
顺便说一下,这里有一个延续已久的争论:在13787行,struct linux_binprm结构
的buf成员被声明为是128字节长,在9933行读入了128字节。但是字面上常量128用
在两个地方——没有宏定义表示有必要保持两个数字的一致;因此,有可能会出现
对其中一个进行改变而不改变相关的另一个的情况,这样就很可能摧毁系统。即使
不从学术上考虑,这种忽略在保证效率的基础上是不能防止的——我不能想象出还
有什么其它理由。
这是一个很好的对内核做点简短却有用的修改的机会:在每处这样使用128的地方
都使用一个#define语句(或者是使用类似于sizeof(bprm->buf)的语句)代替;
存在几个其它实例,我会让你把它们都找到。如果你实验一下,你就会发现在这种
情况下#define为什么比sizeof要好。(把这种重复出现的神奇数字加以定义和修
正对于内核是更好的贡献。但是总体的修正工作要比看起来的困难,这只由于正确
的对所有相关部分进行定位是很困难的;让我们一点一点地开始,最终会将其全部
解决。)
search_binary_handler
二进制处理程序是Linux内核统一处理各种二进制格式的机制,这是我们需要的,
因为不是所有的文件都是以相同的文件格式存储的。一个很合适的例子是Java的.
class文件。Java定义了一种平台无关的二进制可执行格式——无论它们是在什么
平台上运行,它们的文件本身都是相同的——因此这些文件显然应该和Linux特有
的可执行格式一样构建。通过使用适当的二进制处理程序,Linux可以把它们仿佛
当作是自己特有的可执行文件一样处理。
后面我们会详细介绍二进制处理程序,但是现在你应该了解一些有关内容以便理解
do_execve是如何发现匹配的。它把这一工作交给search_binary_handler(9996行
)处理。
10037:开始遍历处理内核的二进制处理程序链接列表,依次将bprm传递给它们。
(我们现在并不关心regs参数。)更确切的说,二进制处理程序的链接列表的每一
个元素都包含一组指向函数的指针,这些函数一起提供了对一种二进制格式的支持
。(13803行定义的struct linux_binfmt结构显示了其中包含的内容:我们感兴趣
的部分是装载二进制的部分load_binary;装载共享库的部分load_shlib;创建内
核转储映象的部分core_dump。)search_binary_handler简单调用每一个
load_binary函数,知道其中一个返回非负值指明它成功识别并装载了文件。
search_binary_handler返回负值指明发生的错误,其中包括不能找到匹配的二进
制处理程序的错误。
10070:如果10037行开始的循环不能找到匹配的二进制处理程序,本行就试图装载
新的二进制格式,它会引起第二次尝试,并应该取得成功。因此整个操作被包含在
从10036行开始的两次执行的循环中。
可执行格式
正如前面一节中说明的一样,不是所有程序都使用相同的文件格式存储,Linux使
用二进制处理程序把它们之间的区别掩盖掉了。
Linux当前“本地的”可执行格式(如果“本地”在系统中可以给各种格式提供良
好支持)是可执行链接格式(ELF)。ELF只是全部替换了原来的称为a.out的格式
,替换之前的格式很难说是灵活的——除了有一些其它缺点以外,a.out还很难适
用于动态链接,这会使得共享库难于实现。Linux仍然为a.out保留了一个二进制处
理程序,但通常是使用ELF。
二进制处理程序通过某种内嵌在文件开头的“magic序列”(一个特殊字节序列)
来识别文件,有时也会通过文件名的一些特性。例如,你会看到的Java处理程序可
以保证文件名以.class结尾并且前四个字节是(以十六进制)0xcafebabe,这是
Java标准所定义的。
下面是2.2版本内核所提供的二进制处理程序(这是在我的Intel系统中的;Linux
的其它平台的移植移植版本,例如PowerPC和SPARC上,需要使用其它的处理程序)

?   a.out(在文件fs/binfmt_aout.c中)——这是为了支持原来风格的Linux二进制
文件。这仍然是为了满足一些系统的向后兼容的需要,但是基本上a.out很快就会
光荣退役了。
?   EFL(在文件fs/binfmt_elf.c中)——只是为了支持现在新风格的Linux二进制
文件。这在可执行文件和共享库中都广泛使用。最新的Linux系统(例如Red Hat
5.2)一般只预装了ELF二进制文件,但是特殊情况下如果你决定装载a.out二进制
文件,那么系统也可以对它提供支持。注意即使ELF被作为惯用的Linux本地格式,
也要和其它格式一样使用二进制处理程序——内核并没有特殊的偏好。避免特殊情
况的惯例能够简化内核代码。
?   EM86(在文件fs/binfmt_em86.c中)——帮你在Alpha机器上运行Intel的Linux
二进制文件,仿佛它们就是Alpha的本地二进制文件。
?   Java(在文件fs/binfmt_java.c中)——使你可以不必每次都麻烦地定义Java字
节码的解释程序就可以执行Java的.class文件。这种机制和脚本中使用的机制类似
;通过把.class文件的文件名作为参数传递,处理程序返回来为你整型字节码处理
程序。从用户的观点来看,Java二进制文件是作为本地可执行文件处理的。在本章
的后面内容中我们会详细介绍这个处理程序。
?   Misc(在文件fs/binfmt_misc.c中)——这是最明智地使用二进制处理程序的方
法,这个处理程序通过内嵌的特征数字或者文件名后缀可以识别出各种二进制格式
——但是其最优秀的特性是它在运行期可以配置,而不是只能在编译器可以配置。
因此,遵守这些限制,你就可以快速的增加对新二进制文件的支持,而不用重新编
译内核,也无须重新启动机器。(这实在太棒了!)源程序文件中的注释建议最终
使用它来取代Java和M86二进制处理程序。
?   脚本(在文件fs/binfmt_script.c中)——对于shell脚本,Perl脚本等提供支
持。宽松一点地说,所有前面两个字符是#!的可执行文件都规由这个二进制处理程
序进行处理。
在上面这些二进制处理程序中,本书中只对Java和ELF处理程序进行了说明(分别
从9083行和7656行开始),因为作为我们关心的基本内容,我们更关心内核如何处
理各种不同格式间的区别,而不是每一种单个二进制处理程序的细节(虽然它自己
也是一个很有趣的主题)。
一个例子:Java二进制处理程序
如同前面你看到的一样,do_execve遍历一个代表二进制处理程序的struct
linux_binfmt结构的链接列表,调用每个结构的load_binary成员指向的函数直到
其中一个成功(当然也或者到已经试验完了所有的格式为止)。但是这些结构又从
何而来呢?函数load_binary是如何实现的?为了寻找这些答案,让我们来看一下
fs/binfmt_java.c文件。
这个模块处理一些不是涉及在Web浏览器上使用java_format(9236行)执行的
Java程序的Java二进制文件和相关的函数。它使用applet_format(9254行)及相
关函数处理Java小程序(Applet)。在本节剩余部分的内容中,我们会集中看一下
对于非Java小程序的支持;对于Java小程序的支持实际上是相同的。
如果重写fs/binfmt_java.c中的函数用来加强Java小程序函数和非Java小程序函数
之间的相同代码的数量就更好了。虽然它注定最终要被“misc”二进制处理程序取
代,但是现在还只是在讨论,尚未实行。
do_load_java
9108: 这是实际处理装载Java的.class文件工作的函数。
9117: 通过检测特征数字0xcafebabe开始,这是因为Java标准规定所有有效的类
文件都使用这个字符序列开始。接着开始执行健全性检测,一直到9147行,确保没
有递归调用而且正在请求执行的可执行文件是以.class结尾的。
9148: 此处,所有的健全性检测已经通过了。现在,do_load_java取得文件的基
本名字,将其和Java字节码解释程序一起放置到程序空间中,并试图执行Java字节
码解释程序。
9165: 使用我们在do_execve中见到的同一个进程执行解释程序。特殊情况下,就
像查询do_load_java的方法一样,使用search_binary_handler为解释程序查询二
进制处理程序。(实际上,虽然它不一定非要是ELF二进制文件,但是它也可能是
。)
记住其它处理程序不会分配新的struct task_struct结构——我们在使用fork的时
候也碰到了这个问题。其它处理程序只是修改现存进程的struct task_struct结构
。如果你希望细致地了解这是如何实现的,你的入手点应该是
do_load_elf_binary(8072行)——我们关心的部分从8273行开始。
load_java
9226:load_java是其它外部对象装载.class文件时所使用的函数。它首先递增内
核模块使用的计数(如果作为内核模块编译),随后又将其递减,但是实际的工作
是由do_load_java(9108行)处理的。
java_format
9236: 通过比较java_format的初始化和struct linux_binfmt结构(13803行)的
定义,你可以看出这个模块没有提供对共享库和内核卸载的支持,只提供了对装载
可执行程序的支持;而且这种支持是通过load_java函数实现的。
init_java_binfmt
9262: 指向这个模块的项是init_java_binfmt,它把两个静态struct
linux_binfmt结构java_format和applet_format的地址压入系统列表中。如果对
Java二进制文件的支持被编译进了内核,就在9355行调用init_java_binfmt,或者
如果Java二进制文件的支持被作为一个内核模块编译进了内核,就使用kmod任务。

调度:了解它们是如何运行的!
在应用程序被装载以后,必须获得对CPU的访问。这是调度程序涉及的领域。操作
系统调度程序基本上划分为两类:
?   复杂调度程序——运行需要花费相当长的时间,但是希望可以全面提高系统性能

?   快餐式(quick-and-dirty)调度程序——只是试图处理一些尽量简单的合理的
工作就退出,从而进程本身将可以尽可能多的获得CPU。
Linux调度程序是后面一种情况。不要把“quick-and-dirty”解释成贬义的词,虽
然实际的情况是:Linux的调度程序在商业和自由领域中都从根本上痛击了其竞争
者。
调度函数和调度策略
内核主要的调度函数经过仔细挑选使用schedule这个名字,该函数从26686行开始
。这实际上是个很简单的函数,比它看起来还要简单,虽然由于它把三种调度策略
合成了一种而其意义显得有些不是很明显。而且对于SMP的支持也增加了一定的复
杂性,这一点将在第10章中详细讨论。
通常情况下使用的调度策略和进程有关。给定进程使用的调度算法称为调度策略,
这在进程的struct task_struct结构的policy成员中有所反映。普通情况下,
policy是SCHED_OTHER、SCHED_FIFO,或者SCHED_RR其中一个的位集。但是它也可
能含有SCHED_YIED位集,如果进程决定交出CPU——例如,通过调用sched_yield系
统调用(请参看sched_yield,27757行)。
SCHED_XXX常量在16196行到16202行宏定义。
16196:SCHED_OTHER意味着传统Unix调度是使用它的——这不是一个实时进程。
16197:SCHED_FIFO意味着这是一个实时进程,这要遵守POSIX.1b标准的FIFO(先
进先出)调度程序。它会一直运行,直到有一个进程在I/O阻塞,因而明确释放
CPU,或者是CPU被另一个具有更高rt_priority的实时进程抢占了。在Linux实现中
,SCHED_FIFO进程拥有时间片——只有当时间片结束时它们才被迫释放CPU。因此
,如同POSIX.1b中规定一样,这样的进程就像没有时间片一样运行。因此进程要保
持对其时间片进行记录的这一事实主要是为了实现的方便,因此我们就不必使用
if(!(current->policy & SCHED_FIFO)) { … }来弄乱这些代码。还有,这样处理
速度可能会快一些——其它实际可行的策略都需要记录时间片,并持续检测是否我
们需要记录时间片会比简单的跟踪它速度更慢。
16198:SCHED_RR意味着这是一个实时进程,要遵守POSIX.1b的RR(循环:
round-robin)调度规则。除了时间片有些不同之外,这和SCHED_FIFO类似。当
SCHED_RR进程的时间片用完后,就使用相同的rt_priority跳转到SCHED_FIFO和
SCHED_RR列表的最后。
16202:SCHED_YIELD并不是一种调度策略,而是截取调度策略的一个附加位。如同
前面说明的一样,如果有其它进程需要CPU,它就提示调度程序释放CPU。特别要注
意的是这甚至会引起实时进程把CPU释放给非实时进程。
schedule
26689:prev和next会被设置为schedule最感兴趣的两个进程:其中一个是在调用
schedule时正在运行的进程(prev),另外一个应该是接着就给予CPU的进程(
next)。记住prev和next可能是相同的——schedule可以重新调度已经获得CPU的
进程。
26706:如同第6章中介绍的一样,这就是中断处理程序的“下半部分”运行的地方

26715:内核实时系统部分的实现,循环调度程序(SCHED_RR)通过移动“耗尽的
”RR进程——已经用完其时间片的进程——到队列末尾,这样具有相同优先级的其
它RR进程就可以获得时间片了。同时这补充了耗尽进程的时间片。重要的是它并不
是为SCHED_FIFO这样处理的,这样和预计的一样,后面的进程在其时间片偶然用完
时就无须释放CPU。
26720:由于代码的其它部分已经决定了进程必须被移进或移出TASK_RUNNING状态
,所以会经常使用schedule——例如,如果进程正在等待的硬件条件已经发生了—
—所以如果必要,这个switch会改变进程的状态。如果进程已经处于
TASK_RUNNING状态,它就无须处理了。如果它是可以中断的(等待信号量)并且信
号量到达了进程,就返回TASK_RUNNING状态。在所有其它情况下(例如,进程已经
处于TASK_UNINTERUPTIBLE状态了),应该从运行队列中将进程移走。
26735:将p初始化为运行队列中的第一个任务;p会遍历队列中的所有任务。
26736:c记录了运行队列中所有进程的最好“goodness”——具有最好“
goodness”的进程是最易获得CPU的进程。(我们很快就会讨论goodness。)
goodness值越高越好,一个进程的goodness值永远不会为负——这是Unix用户经常
见到的一种奇异情况,其中较高的优先级(通常称为较高“niceness”级)意味着
进程会较少地获得CPU时间。(至少这在内核中是有意义的。)
26757:开始遍历执行任务列表,跟踪具有最好goodness的进程。注意只有在当前
记录被破坏而不是当它简单地被约束时它才会改变最好进程的概念。因此,出于对
队列中第一个进程的原因,这种约束就会被打破了。
26758:这个循环中只考虑了唯一一个可以调度的进程。can_schedule宏的SMP版本
在26568行定义;其定义使SMP内核只有任务尚未在CPU上运行才会把调度作为该
CPU上的一个任务。(这样具有完美的意义——在几乎不必要的任务中造成混淆完
全是一种浪费。)UP版本在26573行,它总是真值——换而言之,在UP的情况下,
运行队列中的每一个进程都需要竞争CPU。
26767:值为0的goodness意味着进程已经用完它的时间片或者它已经明确说明要释
放CPU。如果所有运行队列中的所有进程都具有0值的goodness,在循环结束后c的
值就是0。在这种情况下,schedule要重新计算进程计数器;新计数器的值是原来
值的一半加上进程的静态优先级——由于除非进程已经释放CPU,否则原来计数器
的值都是0,schedule通常只是把计数器重新初始化为静态优先级。(中断处理程
序和由另外一个处理器引起的分支在schedule搜寻goodness最大值时都将增加此循
环中的计数器,因此由于这个原因计数器可能不会为0。虽然这有些罕见。)调度
程序不必麻烦地重新计算现在哪一个进程具有最高的goodness值;它只是调度前面
循环中遇到的第一个进程。此时,这个进程是它发现的第一个具有次高goodness值
(0)的进程,因此schedule就能够计算出自己现在和以后所应该运行的任务。(
记住,这就是“quick-and-dirty”的思想。)
26801:如果schedule已经选择了一个不同于前面正在运行的进程来调度,那么它
就必须挂起原来的进程并允许新的进程运行。这是通过后面我们将介绍的
switch_to处理的。switch_to的一个重要结果对于应用程序开发者来说可能显得有
些奇怪:对于schedule的调用并不返回。也就是它不是立即返回的;在系统条件判
断语句返回到当前任务时调用就会返回。作为一个特殊情况,当任务退出而调用
schedule时,对于schedule的调用从不会返回——因为内核不会返回已经退出的任
务。还有另外一种特殊情况,如果schedule不会调度其它进程——也就是说,如果
在schedule结束时next和prev是相同的——那么上下文中的跳转不会执行,
schedule实际上不会立即返回。
26809:schedule末尾的__schedule_tail和reacquire_kernel_lock函数在UP平台
上不执行任何操作,因此现在我们就已经看完了调度程序的内核。顺便说一下,为
了确保你已经正确的理解了这些代码,自己证明下面的性质:如果运行队列为空,
那么下面就会调用idle任务。
switch_to
switch_to处理从一个进程到下一个进程的跳转,称为上下文跳转(
context-switching);这是在不同处理器上会不同处理之间进行的低级特性。有
趣的是,在x86平台上内核开发人员使用软件处理大多数的上下文跳转,这样就忽
略了一些硬件的支持。这种机制背后的原因在__switch_to函数(2638行)上面的
标题注释中有所说明,这个函数和switch_to宏(12939行)一起处理上下文跳转。

由于很多上下文跳转要依赖于对内核处理内存方式的正确理解,这在下一章中才会
详细介绍,本章只是稍微涉及一点。上下文跳转背后的基本思想是记忆当前位置和
将要到达的位置——这是我们必须保存的当前上下文——接着跳转到另外一个前面
已经存储过了的上下文。通过使用一部分汇编代码,switch_to宏保存了后面将要
介绍的上下文的两个重要的部分。
12945:首先,switch_to宏保存ESP寄存器的内容,它指向进程的当前堆栈。堆栈
在下一章中将深入介绍;现在你只需要简单了解堆栈中保存的局部变量和函数调用
信息。switch_to宏也保存EIP寄存器的内容,这是进程的当前指令指针——如果允
许继续运行时所执行的为下一条指令的地址。
12948:把next->tss.eip——保存指令的指针——压入返回堆栈,记录当后面紧跟
的跳转到__switch_to的jmp返回时的返回地址。这样做的最终结果是当
__switch_to返回时,我们又回到了新的进程。
12949:调用__switch_to(2638行),它完成段寄存器和页表的保存和恢复工作。
在你阅读完第8章以后这些特征数字就更有意义了。
12955:tss代表task-state段,这是Intel使用的支持硬件上下文跳转的CPU特性的
术语。虽然内核代码使用软件实现上下文跳转,但是开发人员仍然会使用TSS来记
录进程的状态。struct task_struct结构的tss成员的类型是struct
thread_struct结构,本书中为了节省空间,忽略了它的定义。其成员仅仅对应于
x86的TSS——成员是为EIP和ESP而存在的,如此而已。
计算goodness值
进程的goodness值通过goodness函数(26388行)计算。goodness返回下面两类中
的一个值:1,000以下或者1,000以上。1,000和1,000以上的值只能赋给“实时”进
程,从0到999的值只能赋给“普通”进程。实际上普通进程的goodness值只使用了
这个范围底部的一部分,从0到41(或者对于SMP来说是0到56,因为SMP模式会优先
照顾等待同一个处理器的进程)。无论是在SMP还是在UP上,实时进程的goodness
值的范围都是从1,001到1,099。
有关这两类goodness结果的重要的一点是该值在实时系统中的范围肯定会比非实时
系统的范围要高(因此偏移量(offset)是100而不是1000)。POSIX..1b规定内核
要确保在实时进程和非实时进程同时竞争CPU时,实时进程要优先于非实时进程。
由于调度程序总是选择具有最大goodness值的进程,又由于任何尚未释放CPU的实
时进程的goodness值总是比非实时进程的goodness大,Linux对这一点的遵守是很
容易得到证明的。
尽管在goodness上面的标题注释中有所说明,该函数还是从不会返回-1,000的,也
不会返回其它的负值。由于idle进程的counter值为负,所以如果使用idle进程作
为参数调用goodness,就会返回负值,但这是不会发生的。
goodness只是一个简单的函数,但是它是Linux调度程序必不可少的部分。运行对
立中的每个进程每次执行schedule时都可能调用它,因此其执行速度必须很快。但
是如果一旦它调度失误,那么整个系统都要遭殃了。考虑到这些冲突压力,我想改
进现有的系统是相当困难的。
goodness
26394:如果进程已经释放了CPU,就返回0(在清除SCHED_YIELD位之后,这是因为
进程只可能有一次想释放CPU,现在它已经的确把CPU释放了)。
26402:如果这是一个实时进程,goodness返回的值就属于数值较高的一类;这要
精确地依赖于rt_priority的值。
26411:此处,代码识别出这是一个非实时进程,它把goodness(在这个函数中被
称为weight)初始化为其当前的counter值,这样如果进程已经占用CPU一段时间了
,或者进程开始的优先级比较低,那么进程就不太可能获得CPU。
26412:如果权值weight的值为0,那么进程的计数器就已经被用完了,因此
goodness就不会再增加加权因素。其它进程就可以有机会运行。
26418:尽力优先考虑等待同一个处理器的进程(只在SMP系统中是这样——顺便说
一下,考虑一下运行在一个双处理器的系统中的三个进程的实现情况)。
26423:给相关的当前进程或者当前线程增加了一些优点;这有助于合理使用缓存
以避免使用昂贵的MMU上下文跳转。
26425:增加进程的priority。这样,goodness(和其它类似的调度程序)就对较
高优先级的进程比对较低优先级的进程更感兴趣,即使在前面进程已经部分用完了
它们的时间片也是这样。
26428:返回计算出来的goodness值。
非实时优先级
每个Linux进程都有一个优先级,这是从1到40的一个整数,其值存储在struct
task_struct结构的priority成员中。(对于实时进程,在struct task_struct结
构中还会使用一个成员——rt_priority成员。随后很快就会对它进行更详细的讨
论。)它的范围使用PRIO_MIN(在16094行宏定义为-20)和PRIO_MAX(在16095行
宏定义为20)限定——理论上来说,的确是这样。但是非常令人气恼的是,控制优
先级的函数——sys_setpriority和sys_nice——并没有注意到这些明显的常量,
却相反宁愿使用一些固定的值。(它们也使用最大的完美值19,而不是20。)基于
这个原因,PRIO_MIN和PRIO_MAX两个常量并没有广泛使用。不过这又是一个热心读
者改进代码的机会。
由于已经在文档中说明sys_nice(27562行)为要废弃不用了——可能会使用
sys_setpriority来重新实现——我们就忽略前面一个函数,只讨论后面一个。
sys_setpriority
29213:sys_setpriority使用三个参数——which,who和niceval。which和who参
数提供了一种可以用来指定一个给定用户所拥有的单个进程,一组进程或者所有进
程的方法。who要根据which的值做出不同的解释;它会作为一个进程ID,进程组
ID或者用户ID读取。
29220:这是确保which有效地进行健全性检测。我认为这里的模糊不清是不必要的
。如果我们不使用
    if  ( which > 2 || which > 0 )
    而使用如下语句
    if  ( which != PRIO_PROCESS && wich != PRIO_PGRP && which !=
PRIO_USER )
    或者至少是
    if  ( which > PRIO_USER || which < PRIO_PGRP )
    另外,在29270行也可以使用同样的方法。
29226:niceval是使用用户术语定义的——也就是说,它是在从-20到19的范围中
,而不是象内核中使用的一样,在从1到40的范围中。如同变量名说明的一样,这
是一个完美的值,但不是一个优先级。因此,为了实现这种转化,
sys_setpriority应该跳过一些循环,同时要截断niceval超出允许范围的值。
    我承认自己被这段代码的复杂性所困扰着。使用实际上使用的DEF_PRIORITY的值
——20——以下的简化代码显然可以实现相同的效果:
    if  ( niceval <-19 )
        priority = 40;
    else if  ( niceval > 19 )
        priority = 1;
    else
        priority = 20 - niceval;
    在保持比sys_setpriority中的代码简单的同时,我的实现方法中当然也可以用于
处理DEF_PRIORITY。因此,或者我严重误解了一些内容,或者就象我提出的代码本
身,它根本就不需要这么复杂。
29241:循环遍历系统的任务列表中的所有任务,执行它可以允许修改。proc_sel
(29190行)说明了给定的进程是否对所提供的which和who值满意,可以用它来选
择进程;由于sys_getpriority也要使用这个函数,所以它也是sys_setpriority应
该考虑的一个因素。
对于读取和设置单个进程优先级的普通情况(如果没有其它问题,就通过提早退出
for_each_task循环),sys_setpriority和sys_getpriority(29274行开始的代码
和此处有相似的内部循环)都对它有一点加速作用。sys_setpriority可能不会很
频繁地被调用,但是sys_getpriority却可能被很频繁调用,因而这样努力的是值
得的。
update_process_times
sys_setpriority只会影响进程的priority成员——也就是其静态优先级。回忆一
下进程也是具有动态优先级的,这由counter成员表示,这一点我们在对schedule
和goodness的讨论中就已经清楚地看到了。我们已经可以看出在调度程序发现
counter值为0时,schedule会周期性地根据其静态优先级重新计算每一个进程的动
态优先级。但是我们仍然还没有看到另外一部分困扰我们的问题:counter是在哪
里被递减的?它是怎样达到0的?
对于UP,答案与update_process_times(27382行)有关。(和前面一样,我们把
对于SMP问题的讨论延迟到第10章。)update_process_times是作为update_time(
27412行)的一部分被调用的,它还是第6章中讨论的定时器中断的一部分。作为一
个结果,它被相当频繁地调用——每秒钟100次。(当然,这只是对人类的内力来
说是相当频繁的,对于CPU来说这实在是很慢的。)在每一次调用的时候,它都会
把当前进程的counter值减少从上次以来经过的“滴嗒”的数目(百分之一秒——
请参看第6章)。通常,这只是一次跳动,但是如果内核正忙于处理中断,那么内
核就可能会忽略定时器的跳动。当计数器减小到0以下时,update_process_times
就增加need_resched标志,说明这个进程需要重新调度。
现在,由于进程缺省的优先级(使用内核优先级的术语,而不使用用户空间的完美
值)是20,缺省情况下进程得到一个21次跳动的时间片。(的确这是21次跳动,而
不是20次跳动,因为进程直到其动态优先级减少到0以下时才会为重新调度做出标
记。)一次跳动是百分之一秒,或者是10微秒,因此缺省的时间片就是210微秒—
—大约是五分之一秒——在16466行有确切的描述。
我发现这个结果十分奇怪,因为原来以为理想的反应迅速的系统应该具有小很多的
时间片——实际上我对这一点认识是如此强烈以至于开始的时候我还以为文档的说
明发生了错误。但是,回顾一下,我觉得自己也不应该奇怪。毕竟,进程不会频繁
地耗尽其整个时间片,因为它们经常都会因为I/O的原因而阻塞。在几个进程都绑
定在CPU上时,在它们之间太频繁地跳转是没有必要的。(特别是在诸如x86之类的
CPU上,这里的上下文跳转的代价是相当高的。)最后,我必须承认我从来没有注
意到自己留意Linux逻辑单元的响应的迟缓特性,因此我觉得210微秒的时间片是个
不错的选择——即使这在最初的时候看起来是太长了。
如果由于某些原因你需要时间片比当前最大值还长(410微秒,优先级上长到了40
),你可以简单使用SCHED_FIFO调度策略,在你准备好以后就可以释放CPU(或者
重新编写sys_setpriority和sys_nice)。
实时优先级
Linux的实时进程增加了一级优先级。实时优先级保存在struct task_struct结构
的rt_priority成员中,它是一个从0到99的整数。(值0意味着进程不是实时进程
,在这种情况下其policy成员必须是SCHED_OTHER。)
实时任务仍然使用相同的counter成员作为它们的非实时的计数器部分。实时任务
为了某些目的甚至使用与非实时任务使用的priority成员相同的部分,这是当时间
片用完时用来补充counter值使用的值。为了清晰起见,rt_priority只是用来对实
时进程划分等级以对它们进行区分——否则它们的处理方式就和非实时进程相同了

进程的rt_priority被设定为使用POSIX.1b规定的函数sched_setscheduler和
sched_setparam(通常只有root才可以调用这两个函数,这一点我们在讨论权能时
会看到)设置其调度策略。注意这意味着如果具有修改权限,进程的调度策略在进
程生命期结束以后就可以改变。
实现这些POSIX函数的系统调用sys_sched_setscheduler(27688行)和
sys_sched_setparam(27694行)都会把实际的工作交给setschedular(27618行)
处理,这个函数我们现在就介绍。
setscheduler
27618:这个函数的三个参数是目标进程pid(0意味着当前进程),新的调度策略
policy,和包含附加信息的一个结构param——它记录了rt_priority的新值。
27630:在一些健全性检测之后,setscheduler从用户空间中得到提供的struct
sched_param结构的备份。在16204行定义的struct sched_param结构只有一个成员
sched_priority,它就是调用者为目标进程设计的rt_priority。
27639:使用find_process_by_pid(27608行)找到目标进程,如果pid是0,这个
函数就返回一个指向当前任务的指针;如果存在指向具有给定PID进程,就返回指
向该进程的指针;或者如果不存在具有这个PID的进程,就返回NULL。
27645:如果policy参数为负时,就保留当前的调度策略。否则,如果这是个有效
值,那么现在就可以将其接收。
27657:确保优先级没有越界。这是通过使用一点小技巧来加强的。该行只是第一
步,它被用来确保所提供的值没有大得超出了范围。
27659:现在已经确知新的实时优先级位于0到99的范围之内。如果policy是
SCHED_OTHER,但是新的实时优先级不是0,那么这个测试就失败了。如果policy指
明了一个实时调度程序但是新的实时优先级是0(如果这里它不是0,就应该是从1
到99),测试也会失败。否则,测试就能成功。这虽然并不是很易读,但它确实是
正确的、最小的,(我想)速度也很快。我不确定这里我们是否对速度有所苛求,
但是——到底一个进程需要多长时间需要设置它的调度程序?下面的代码就应该具
有更好的可读性,而且当然也不会太慢:
    P492 1
27663:不是每一个进程都可以设置自己的调度策略和其它进程的调度策略。如果
所有进程都可以设置自己的调度策略,那么任何进程都可以简单地设置自己的调度
策略为SCHED_FIFO并进入一个无限循环来抢占CPU,这样必然会锁定系统。显然,
是不能够允许这种做法的。因此,只有进程拥有这样处理的权能时,setscheduer
才会允许进程设置自己的调度策略。权能在下一节中将比较详细地介绍。
27666:在相同的行中,我们不希望别人可以修改其它用户进程的调度策略;普通
情况下,只允许你修改你自己所有的进程的调度策略。因此,setscheduer要确保
或者用户是设置自己所有的进程的处理程序或者具有修改其它进程的调度策略的权
能。
27672:这里才是setscheduler实际工作的地方,它在目标进程的struct
task_struct结构中设置policy和rt_priority。如果该进程在运行队列中(这通过
检测其next_run成员非空来测试),就将它移到运行队列的顶部——这比较容易令
人感到迷惑;可能这有助于SCHED_FIFO进程实现对CPU的抢占。进程为重新调度做
出标记,setscheduer清空并退出。
遵守限制
内核经常需要决定是否允许进程执行某个操作。进程可能被简单的禁止执行某些操
作,但却被允许在受限的环境中执行一些别的操作;这些操作基本上可以由权能表
示,并且/或者可以从用户ID和组ID中推导出来。在其它期间,允许进程处理一些
操作,但只是在受限的环境中——例如,它对CPU的使用必须受到限制。
权能
在前面一节中,你已经看到了一个检测权能的例子——实际上是有两次相同的权能
。这是CAP_SYS_NICE权能(14104行),它决定是否应该允许进程设置优先级(完
美级别)或调度策略。由于这比仅仅的完美级别要更适用,CAP_SYS_NICE是一个误
用的位——虽然很容易就可以看出设置调度策略和相关的概念是紧密相关的,而且
你一般也不会要一个权能而不要另外一个权能。
每一个进程都有三个权能,它们被存储在进程的struct task_struct结构中(在
16400行到16401行中):
?   cap_effective——有效置位集合
?   cap_permitted——允许位集合
?   cap_inheritable——继承位集合
进程权能的有效位集合是当前可以处理的内容的集合;这是通过广泛使用的
capable函数检测的集合,这个函数在16738行定义。
允许位集合规定进程正常地可以被赋予的权能。这个集合通常不会增加——只有一
种情况例外:如果一个进程具有CAP_SETPCAP权能,那么它就可以将自己的允许位
集合中的任何权能赋给其它进程,即使目标进程还没有拥有这个权能。
如果一个权能在允许位集合中,但是并不在有效位集合中,那么进程现在还没有马
上拥有权能,但是它可以通过请求权能而获得。为什么要麻烦地区别它们呢?在本
章开始我们第一次讨论权能的时候,我们简单地考虑了一个简单的例子:一个长期
运行的进程只是偶然需要权能,而不是所有情况下都需要。为了保证进程不会偶然
缺少权能,进程可以一直等待,直到它需要权能,接着请求权能,执行有权限的操
作,并再次取消权能。这种方法比较安全。
继承位集合不像你想象的那么简单。它不是祖先继承在执行fork的同时传递的权能
集合——实际上,在创建的那一刻(也就是紧随着fork),子孙进程的权能的三个
集合和其祖先的三个权能集合都是相同的。相反,继承位集合在exec运行期间才会
起作用。进程在调用exec之前的继承位集合有助于决定它的允许位集合和继承位集
合,它们在exec执行结束以后也会保留下来——仔细的介绍请参看compute_creds
(9948行)。注意在exec之后权能是否保留要部分依赖于进程的继承位集合;它还
要部分依赖于文件本身中的权能位集合(或者不管怎样,这至少是一个计划——虽
然这种特性还没有完全实现)。
顺便提一下,注意到允许位集合必须总是有效位集合和继承位集合的超集(
superset)(或者和有效位集合相同)。(只有对于有效位集合这才是严格正确的
。一个进程可能会扩展另外一个进程的继承位集合从而它不再是其允许位集合的子
集,但是就我知道的来说,这是无意义的,因此我们从现在就开始忽略这种可能性
。)然而,和你可能希望的相反,有效位集合不一定要是继承位集合的超集(或者
和继承位集合相同)。也就是说,在exec结束以后,进程可能会拥有一个以前不曾
有过的权能(虽然这个权能必须在其允许位集合中——也就是说,这是一个原来进
程自己可能已经得到了的权能)。我认为这种需要只是局部的,这样进程就不需要
暂时获得不需要的权能,而能够获得足以执行exec程序的权能。


图7.4 权能集

图7.4说明了各种可能性。它显示了一个理想进程的三种权能集合,位从左到右计
数。允许进程可以获得CAP_KILL权能,这样就允许它不考虑其它属主而杀掉别的进
程,但是它还没有立即拥有权限,而且也不会在exec执行过程中自动获得。目前它
具有增加和删除内核模块的权能(使用CAP_SYS_MODULE),但是同样也不会在
exec执行过程中自动获得。它可以获得CAP_SYS_NICE权能,但是直到exec执行完后
才会获得(假定文件权能位允许)。最后,它可以立即修改系统时间(
CAP_SYS_TIME),但是也是只有通过exec才能获得这个权能。除非其它具有
CAP_SETPCAP权能的进程提供了这个权能,否则这个进程不能获得这个权能,它可
能执行的其它进程也不可能获得这个权能。
保证这些不同性质的代码主要是在kernel/capability.c中,从22460行开始。两个
主要的函数是读取权能的函数sys_capget(22480行)和设置权能的函数
sys_capset(22592行);它们在下一节中讨论。通过exec继承的权能使用
fs/exec.c的compute_creds(9948行)处理,这一点已经介绍过了。
当然,root肯定拥有所有的权能。内核权能特性给root提供了一种规则的方法来有
所选择地只把需要的权能赋给特定的进程,而不用考虑该进程是否作为root用户运
行。
权能一个有趣的特性是它们可以用来改变系统的“风格”。作为一个简单的例子,
为所有的进程设置CAP_SYS_NICE权能会使所有进程都增加自己的优先级(并设置它
们的调度规则,等等)。如果你修改了系统中每一个进程的运行方式,那么你就改
变了系统本身。自己设想一下发明一种新的可以通过更令人兴奋的方式修补系统的
内核权能。
权能的尚未为人所知的优点是它们使源程序代码非常清晰。当检测当前进程是否允
许设置系统时间时,却反而要检测当前进程是否以root运行,这种方式看起来似乎
有些不很好。权能使我们可以了解它们的意思。权能的存在甚至还能够使查询进程
的用户ID或组ID的代码更为清晰,这是因为这样的处理代码对这个问题的答案比较
感兴趣,而是对从其中可以推导出的结论更感兴趣。否则,代码应该已经使用权能
查询它需要了解的内容了。由于权能更加一致地和Linux内核代码结合起来,这种
特性就变得更加可靠了。
13916:内核可以识别的权能从这里开始。因为这些宏定义的解释已非常详细了,
我们就不再详细介绍其中每一个的内容了。
14153:赋给每一个权能的数字是简单的连续整数,但是由于要使用无符号整数中
的位来编址,所以就使用CAP_TO_MASK宏把它们转化为2的幂。
14154:设置和检测权能的核心只是一系列位操作;从这里到
include/linux/capability.h中定义了用来使位操作更为清晰的宏和内联函数。
sys_capget
22480:sys_capget有两个参数:header和dataptr。header是cap_user_header_t
类型(13878行)的,它是一个指向定义权能使用的版本和目标进程的PID的结构的
指针;dataptr是cap_user_data_t类型(13884行)的,它也是一个指向结构类型
的指针——这个结构包含有效位、允许位和继承位集合。sys_capget通过第二个指
针返回信息。
22492:在版本不匹配的情况下,sys_capget通过header指针返回使用的版本,接
着返回EINVAL错误(或者如果它不能把版本信息拷贝到调用者的空间中就返回
EFAULT)。
22509:定义调用者希望了解其权能的进程;如果pid不是0,也不是当前进程的
PID,sys_capget就要查询它。
22520:如果它能装载目标进程,它就把自己的权能拷贝到临时变量data中。
22530:如果所有工作到目前为止都运行良好,它就把权能拷贝回用户空间中由
dataptr参数提供的地址中。然后,它返回error变量——通常如果一切运行良好,
这就是0;否则就是一个错误号。
sys_capset
22592:sys_capset的参数几乎和sys_capget的参数类似。不同之处是data(不再
称为dataptr了)是常量。
22600:和sys_capget一样,sys_capset确保内核和调用进程使用一致的权能系统
的版本。如果版本不一致,就拒绝尝试请求。
22613:如果pid不是0,就说明调用者希望设置其它进程的权能,在大多数情况下
这种尝试都会遭到拒绝。如果调用者具有CAP_SETPCAP权能,这意味着允许它设置
任何进程的权能,sys_capset就允许这种尝试。这种测试的前面部分有些太受限制
了:如果它和当前进程的pid相等,就接收这个pid。
22616:从用户空间中拷贝新的权能,如果失败就返回错误。
22627:和22509行开始的sys_capget代码类似,sys_capset定义了调用者希望了解
其权能的进程。这就是两者的区别所在,sys_capset为了说明进程组(或者是-1指
明是所有进程)也允许其pid值为负。在这种情况下,target仍然设置为current,
因此当前进程的权能要在后面的计算中使用。
22642:现在它必须保证合法地使用新的权能位集合,而且在内部保持一致。除非
这种新特性在调用者的允许位集合中,否则这种测试会验证出新进程的继承位集合
没有包含任何新鲜的东西。因此,它不会放弃调用者尚未拥有的任何权能。
22650:类似地,sys_capset也要确保除非调用者的允许位中包含新的特性,否则
目标进程的允许位集合也不会包含尚未具有的特性。因此,它也不会放弃调用者尚
未拥有的任何权能。
22658:回想一下进程的有效位集合必须是其允许位集合的一个子集。这种性质在
这里得到了保证。
22666:sys_capset现在已经准备对请求做出修改。负的pid值意味着它正在给不止
一个进程修改权能——如果pid是-1,就是所有的进程;如果pid是其它的负值,就
是一个进程组中的所有进程。在这些情况下,实际工作分别由cap_set_all(
22561行)和cap_set_pg(22539行)完成;这只是通过一些适当的进程集合循环,
按照和单个进程相同的方法覆盖掉集合中的每一个进程的权能位集合。
22676:如果pid是正数(或者是0,表示当前进程),权能位集合只赋给目标进程

用户ID和组ID
尽管权能功能强大、十分有用,但它并不是你实现访问控制的唯一武器。在一些情
况中,我们需要了解哪个用户正在运行一个进程,或者进程是作为哪个用户来运行
。用户使用整型的用户ID来区别,一个用户可以属于一个组或者多个组,每一个都
有自己特有的整型ID。
有两种风格的用户ID和组ID:实际的ID和有效的ID。一般说来,实际用户(或组)
ID为你说明了哪个用户创建了进程,有效用户(或组)ID为你说明在情况改变时进
程作为哪个用户运行。由于访问控制的决定要更多依赖于进程作为哪儿用户运行,
而不是哪个用户创建了这个进程,因此内核会比检测实际用户(和组)ID更加频繁
地检测有效用户(或)ID——在我们现在关心的代码中就是这样处理的。struct
task_struct结构中的相关成员是uid,euid,gid,和egid(16396行到16397行)
。注意用户ID和用户名不同,前者是一个整数,而后者是一个字符串。
/etc/passwd文件把这两者关联起来。
让我们再回到sys_setpriority并看一下前面我们忽略了的从29244行到29245行的
一些代码。sys_setpriority通常执行的操作都是让用户降低自己进程的优先级,
但是不能降低其它用户进程的优先级——除非用户具有CAP_SYS_NICE权能。因此,
if表达式的前面两个术语要检测目标进程的用户ID是否和sys_setpriority的调用
者的实际用户ID或者有效用户ID匹配。如果两个都不匹配,并且SYS_CAP_NICE没有
设置,sys_setpriority就正确地拒绝这种尝试。
如果允许,进程可以使用sys_setuid和sys_setgid(29578行和29445行)和其它一
些函数修改它们的用户ID和组ID。用户ID和组ID也可以通过执行可执行的setuid或
setgid可执行程序进行修改。
资源限制
可以要求内核限制一个进程使用系统中的各种资源,包括内存和CPU时间。这可以
通过sys_setrlimit实现(30057行)。通过浏览struct rusage结构(16068行)你
对支持限制就可以有一个基本的概念。进程特有的限制在struct task_struct结构
中记录——还可能在什么地方?请参看16404行的rlim数组成员。
违反限制的结果根据限制的不同也会有所不同。例如,对于RLIMIT_MPROC(在本书
的源程序代码中没有包括)——有关一个用户可以拥有的进程数目的限制——和你
在23974行中看到的一样,结果仅仅和fork失败一样。超出其它限制的后果对于一
些进程可能比较严重,这样进程会被杀死(请参看27333行)。进程可以使用
sys_getrlimit(30046行)请求特殊限制,或者使用sys_getrusage(30143行)请
求资源使用限制。
在30067行中,注意进程可以随意减少自己的资源限制,但是它增加自己的资源限
制时只能增加到一个最大值,这个值可以根据每一个资源限制进行具体设置。因此
,当前的资源限制和所有的资源限制是分别记录的(使用在16089行定义的
struct rlimit结构的rlin_cur成员和rlim_max成员)。然而具有
CAP_SYS_RESOURCE权能的进程可以覆盖这个最大值。
这和优先级的规则不同:允许进程可以减小自己的优先级,但是为增加其优先级需
要特殊许可,即使是它减少了自己的优先级接着又要马上增加它也是如此。当前资
源限制和最大资源限制这两个相互关联的概念并没有反映在内核优先级的调度中。
还有,注意到一个进程可以改变另一个进程的优先级(当然是假定它有权这样处理
),但是一个进程只能修改自己的资源限制。
所有美好的事物都会结束——这就是它们如何处理的
我们已经看到进程是如何生成的,怎样给它们赋予各自的生存周期。现在我们应该
看一下它们是如何消亡的。
exit
同第6章中介绍的一样,你可以通过给进程发送信号量9强行杀掉进程,但是更普通
的情况是进程自动退出。进程通过调用系统调用exit自动退出,它在内核中是由
sys_exit实现的(23322行)。(顺便说一下,当C程序从它的main部分返回时,就
会潜在调用exit。)当进程退出时,内核释放所有分配给这个进程的资源——内存
、文件,等等——当然,还要停止给它继续使用CPU的机会。
然而内核不能立即回收代表进程的struct task_struct结构,这是因为该进程的祖
先必须能够使用wait系统调用查询其子孙进程的退出状态。wait返回它检测出的死
亡状态的进程的PID,因此如果死亡的子孙进程在祖先进程仍在等待时就已经重新
分配了,那么应用程序就会被搞乱(和其它问题一样,同一个祖先结束时可以有两
个具有相同PID的子孙进程——一个进程是活动的,另一个进程是死亡的——祖先
进程也不知道哪一个已经退出了)。因此,内核必须保留死亡子孙进程的PID直到
wait发生为止——这通过完整地保持其struct task_struct结构来自动实现的;分
配PID的代码就不用再查询它在任务列表中发现的进程是否是活动的。
处于这种在两种状态之间的进程——它既不是活动的,也没有真正死亡——被称为
僵进程(zombies)。那么sys_exit的任务就是把活动进程转化为僵进程。
sys_exit本身的工作很少;它只是简单地把现存退出代码转化为do_exit希望的格
式,接着就会调用do_exit,由它来处理实际的工作。(do_exit也会作为发送信号
量的一部分来调用,这一点我们在第6章中已经讨论过了。)
23267:do_exit把退出代码作为参数处理,在其返回类型之前使用特殊符号
NORET_TYPE。虽然现在NORET_TYPE(14955行)定义为空——因此它也就不起作用
——但是原来它经常被定义为__volatile__,用来提示gcc该函数不会返回。了解
了这一点知识,gcc就执行一些额外的优化工作并取消有关函数不能成功返回的警
告信息。使用其新的定义,NORET_TYPE对于编译器就没有用处了,但是它仍然给我
们人类传递了很多有用的信息。
23285:释放它的信号量和其它System V IPC结构,这一点我们将在第9章中介绍。

23286:释放分配给它的内存,这一点我们在第8章中介绍。
23290:释放分配给它的文件,很快就会讨论。
23291:释放它的文件系统数据,它超出了本书的范围。
23292:释放它的信号量处理程序表,这一点我们在第6章中介绍过了。
23294:剩下的任务是进入TASK_ZOMBIE状态,其退出代码被记录下来以供将来祖先
进程使用。
23296:调用exit_notify(23198行),它会警告当前退出任务的祖先进程和其进
程组中的所有成员该进程正在退出。
23304:调用schedule(26686行)释放CPU。这个对于schedule的调用从来不会返
回,这是因为它跳转到下一个进程的上下文,从来不会再跳转回来,因此这是现在
退出的进程的最后一次拥有CPU的机会。
__exit_files
进程如何和文件交互不是本书的主题。但是我们应该快速浏览一下__exit_files(
23109行),因为这样会有助于我们理解__clone函数,这个函数使祖先进程和子孙
进程可以共享特定的信息。祖先进程和子孙进程可以共享的一种信息是它们打开的
文件列表。和当时说明的一样,Linux使用引用计数器规则来保证进程退出之后可
以正确地处理扫尾工作。这里就有个扫尾工作的很好的例子。
23115:假设进程已经打开了文件(几乎总会是这样的),__exit_files会递减原
来存储在tsk->files->count中的引用计数器。诸如atomic_dec_and_test之类的原
子操作将在第10章详细介绍;知道atomic_dec_and_test(10249行)递减其参数值
并当参数新值是0时返回真值就足够了。因此,如果tsk的对于目标struct
files_struct结构的引用是最后一次时,这就是正确的。(如果这是一个私有拷贝
,没有和其它任何进程共享,那么引用计数器的初始值就是1,当然它被减小为0。

23116:在释放记录进程的打开文件的内存之前,必须把这些文件都关闭,这是通
过调用close_files(23081行)实现的。
23118:释放保留进程的文件描述符数组fd的内存,这个数组是files的一个子域。
打开文件(NR_OPEN,在15067行中定义1,024)的最大数量要加以选择,这样本行
中的if测试就能正确——fd数组必须刚好适合一个内存页的大小。这样做可以使得
内存的分配(或释放)速度快许多;否则,__exit_files只好使用更通用但是速度
却慢得多的内核的内存函数了。下一章会加深你对这种决策的理解。
23122:最后,__exit_files释放files本身。
其它__exit_xxx函数背后的概念是类似的:它们减少了任务自有的对于潜在共享信
息的引用计数器,如果这是最后一次引用,它们要负责执行所有必须的工作来将其
清除。
wait
和exec一样,wait是一组函数,而不是一个函数。(但是和exec不同,wait家族的
函数实际包含一个名为wait的函数。)wait家族中的其它函数最终都是使用一个系
统调用sys_wait4(23327行)实现的,这个系统调用的名字反映出它实现了wait家
族中最通用的函数wait4。标准C库libc的实现必须重新组织对于其它wait函数调用
的参数并调用sys_wait4。(这还不是问题的全部:由于历史的原因,内核到
Alpha的移植也会提供sys_waitpid。但是即使是sys_waitpid也会反过来调用
sys_wait4。)
除了处理一些其它内容,sys_wait4——也只有sys_wait4——最终把僵进程送进坟
墓。然而从应用程序的观点来看,wait和相关函数要检测子孙进程的状态:检测是
否有进程死亡了,如果有,到底是哪一个进程,这个进程是怎样死亡的。
sys_wait4
23327:为了适合作为相当通用的一个函数,sys_wait4有很多参数,其中一些是可
选的。和通常情况一样,pid是目标进程的PID;和你看到的一样,0和负值是特殊
的。如果stat_addr非空,那么它就是所得子孙进程的退出状态应该拷贝到的地址
。options是一些可能定义sys_wait4的操作的标志的集合。如果ru非空,那么它就
回复

使用道具 举报

 楼主| 发表于 2003-10-27 09:45:38 | 显示全部楼层
Linux内核注释——第8章 内存  

内存是内核所管理的最重要的资源之一。某进程区别于其它进程的一个特征是两个
进程存在于逻辑上相互独立的内存空间(与之相反,线程共享内存)。即使进程都
是同一程序的实例,比如,两个xterm或两个Emacse,内核都会为每个进程安排内
存空间,使得它们看起来像是在系统之上运行的唯一进程。当一个进程不可能偶然
或恶意的修改其它进程的执行空间时,系统的安全性和稳定性就会得到增强。
内核也生存在它自己的内存空间之中,即内核空间(kernel space)。与之对应的
是用户空间(user space),它是所有非内核任务所处的内存空间的一个通用术语

虚拟内存
计算机系统包括不同级别的存储器。图8-1说明了这些存储器中最重要的几项,并
且以我自己原有的Linux机器(Linux box)为例标注了一些参数的估计值。当你从
左向右观察该图时,会发现存储器容量越来越大而速度却越来越慢(而且每字节价
格也会更低)。尤其令人注意的是,访问速度跨越了3个数量级(乘数因子为1000
),而容量竟跨越了超过8个数量级(乘数因子为312500000)。(实际上有时速度
的差异是可以被掩盖的,不过这些数字足以很好的说明这一部分讨论的目的。)最
大的差距体现在最后两个:RAM和磁盘上,它们又分别可被称作主存和辅存。
额外附加的存储器空间总是十分诱人的,即使它们也很慢。如果在RAM被用完时,
通过暂时把不用的代码和数据转移到磁盘上以腾出更多空间的方法来使用磁盘代替
RAM的话,那将是很好的一件事情。正如读者可能已经知道的,Linux恰好能够做到
这一点,这被称之为虚拟内存(virtual memory)。
虚拟内存是一种对RAM和磁盘(或称之为:主存和辅存)进行无缝混合访问的技术
。所有这些(虚拟)内存对于应用程序来说就好像它真的存在一样。当然我们知道
它并非真的内存,这正是为什么它被称为是“虚拟的”,但是多亏了内核使得应用
程序无法分辨出它们的区别。对于应用程序来说,就好像真的有很大数量的RAM,
只不过有时候比较慢而已。
术语“虚拟内存”还有另外一层意思,从严格意义来讲是与前述的第一种意思没有
关系的。这里的虚拟内存指的是对进程驻留地址进行欺骗的方法。每个进程都会有
这样一种错觉,认为它的地址是从0开始并由此连续向上发展的。很明显,这一点
同时对所有进程都成立是不可能的,但是在生成代码的时候这个假定(fiction)
却能够带来很大方便,这是由于进程不必知道它们是否真正从0地址开始驻留,而
且它们也不必去关心此事。
这两种意思也不必相关,因为一个操作系统从理论上可以给每个进程分配一个独有
的逻辑地址空间而不用混合使用主存和辅存。然而在所有我已经知道的系统中(对
这两种虚拟内存的实现方式)要么都采纳要么都不采纳,这一点可能会在开始时令
人感到困惑。
为了避免这种意义上的分歧,有人倾向于术语“虚拟内存”代表逻辑地址空间(
logical- address-space)的意义,同时使用“分页(paging)”或“交换”表示
磁盘作为内存使用(disk-as-memory)的含义。尽管这种严格的区分具有充足的理
由,但是我更喜欢普通的用法。除非上下文要求,否则我很少花费精力对它们进行
区分。








图8-1 具有速度和容量的存储级别
交换和分页
早期的虚拟内存(VM)系统仅能够把整个应用程序代码和数据,即完整的进程从磁
盘上移出或移入磁盘。这种技术被称为交换(swapping),因为它是把一个进程同
另一个进程进行了对调。出于这个原因,磁盘上为VM所保留的区域通常被称为交换
空间(swap space),或简称为交换区(swap),尽管如我们所见,现代的系统已
不再使用这种最初意义上的交换技术。与此类似,读者通常会见到的术语是交换设
备(swap device)和交换分区(swap partition),它是磁盘分区的同义词,但
是被专门作为交换空间使用,以及术语交换文件(swap file),这是一个用于交
换的规则的、有固定长度的文件。
交换是很有用的,当然要比根本没有VM好的多,但是它也有一定局限性。首先,交
换需要把整个进程同时调入内存,所以当运行一个需要比系统所有RAM还要大的存
储空间的进程时,交换便于事无补了,即使磁盘有大量空间可供补充。
其次,交换可能会很低效。交换就必须把整个进程同时调出,这就意味着为了2K的
空间你不得不把一个8MB的进程整个调出。同样的道理,即使仅仅需要执行被调进
的应用程序代码的一小部分,你也必须把整个进程同时调进。
分页(paging)是把系统的内存划分成很小的块,即页面,每个页面可以独立的从
磁盘调入或调出磁盘。分页与交换技术相似,但它使用更加细小的粒度(
granularity)。分页比交换有更多的登记(book-keeping)开销,这是因为页面
数远比进程数要多,然而通过分页可以获得更多的灵活性。而且分页也更快一些,
原因之一就是不再需要把整个进程调进调出,而只需要交换必要的页面就足够了。
要记住前述的1000倍的速度差异,所以我们应该尽可能避免磁盘的I/O操作。
传统上特定平台上页面的大小是固定的,比如x86平台为4K,这可以简化分页操作
。不过,大多数CPU为可变大小的页面提供硬件支持,通常能够达到4M或者更大。
可变大小页面可以使分页操作执行更快和更有效,不过要以复杂性为代价。标准发
行的Linux内核不支持可变大小页面,所以我们仍然假定页面大小是4K。(已经有
支持Cyrix可变大小页面机制的补丁程序,但它们不是本书中官方发行版本的部分
。而且据闻由此获得的性能增益也并不非常显著。)
因为分页可以完成交换所能完成的所有工作,而且更加有效,所以类似于Linux一
样的现代操作系统已不再使用交换,严格的说是只使用分页技术。但是术语“交换
”已得到了广泛使用,以至于实际应用中术语“交换”和“分页”已经几乎可以通
用;由于内核使用分页技术,所以本书就遵从这种用法。
Linux能够交换到一个专用磁盘分区、或一个文件,或是分区和文件的不同组合。
Linux甚至允许在系统运行时增加和移去交换空间,当你暂时需要额外大量的交换
空间,或者假如你发现需要额外交换空间而又不想重启系统的时候,这就会很有用
了。另外,与一些Unix的风格(flavors)不同,Linux即使没有任何交换空间也能
运行得很好。
地址空间
地址空间(address space)是一段表示内存位置的地址范围。地址空间有三种:

?   物理地址空间
?   线性地址空间
?   逻辑地址空间,也被称为虚拟地址空间
(需要指出的是,I/O地址能够被看作是第四种地址空间,但是本书中对其不作讨
论。)
物理地址是一个系统中可用的真实的硬件地址。假如一个系统有64M内存,它的合
法地址范围是从0到0x4000000(以十六进制表示)。每个地址都对应于所安装的
SIMMs中的一组晶体管,而且对应于处理器地址总线上的一组特定信号。
分页可以在一个进程的生存期里,把它或它的片段移入或者移出不同的物理内存区
域(或不同物理地址)。这正是进程被分配一个逻辑地址空间的原因之一。就任何
特定的进程来说,从0开始扩展到十六进制地址0xc0000000共3GB的地址空间是绰绰
有余的。即使每个进程有相同的逻辑地址空间,相应进程的物理地址也都是不同的
,因此它们不会彼此重叠。
从内核的角度看来,逻辑和物理地址都被划分成页面。因此,就像我们所说的逻辑
和物理地址一样,可以称它们为逻辑和物理页面:每个合法的逻辑地址恰好处于一
个逻辑页面中,物理地址也是这样的。
与之相反,线性地址通常不被认为是分页的。CPU(实际是下文中的MMU)会以一种
体系结构特有的方式把进程使用的逻辑地址转换成线性地址。在x86平台上,这种
转换是简单地把虚拟地址与另一地址,即进程的段基址相加;因为每个任务的基址
都被设置为0,所以在这种体系结构中,逻辑地址和线性地址是相同的。得到的线
性地址接着被转换成物理地址并与系统的RAM直接作用。
内存管理单元
在逻辑地址和物理地址之间相互转换的工作是由内核和硬件内存管理单元(MMU—
memory management unit)共同完成的。MMU是被集成进现代的CPU里的,它们都是
同一块CPU芯片内的一个部分,但是把MMU当作一个独立的部分仍然非常有益。内核
告诉MMU如何为每个进程把某逻辑页面映射到某特定物理页面,而MMU在进程提出内
存请求时完成实际的转换工作。
当地址转换无法完成时,比如,由于给定的逻辑地址不合法或者由于逻辑页面没有
对应的物理页面的时候,MMU就给内核发出信号。这种情况称为页面错误(page
fault),本章后面会对此进行详细论述。
MMU也负责增强内存保护,比如当一个应用程序试图在它的内存中对一个已标明是
只读的页面进行写操作时,MMU就会通知OS。
MMU的主要好处在于速度。缺少MMU时为了获得同样的效果,OS将不得不使用软件为
每个进程的每一次内存引用进行校验,这种校验同时包括数据和指令在内,而这可
能还包括要用为进程创建其生存所需的虚拟机。(Java所进行的一些工作与此类似
。)这样做的结果将使系统慢得令人无法忍受。但是一个以这种内存访问合法性检
查方式集成在计算机硬件里的MMU却根本不会使系统变慢。在MMU建立起一个进程以
后,内核就只是偶尔参与工作,例如在发生页面错误时,而这与全部内存引用数量
相比是非常少的。
除此而外,MMU还可以协助保护内存自身。没有MMU,内核可能不能够防止一个进程
非法侵入它自己的内存空间或者是其它进程的内存空间。但是如何避免内核也会作
同样的操作呢?在Intel’s 80486或更新的芯片上(不是80386),MMU的内存保护
特性也适用于内核进程。
页目录和页表
在x86体系结构上,把线性地址(或者逻辑地址——记住在Linux上,这二者具有相
同的值)解析(resolving)到物理地址分为两个步骤,整个过程如图8-2所示。提
供给进程的线性地址被分为三个部分:一个页目录索引,一个页表索引和一个偏移
量。页目录(page directory)是一个指向页表的指针数组,页表(page table)
是一个指向页面的指针数组,因此地址解析就是一个跟踪指针链的过程。一个页目
录使你能够确定一个页表,继而得到一个页面,然后页面中的偏移量(offset)能
够指出该页面里的一个地址。
为了进行更详细因而也会更准确的描述:给定页目录索引中的页目录项保存着贮存
在物理内存上的一个页表地址;给定页表索引中的页表项保存着物理内存上相应物
理页面的基地址;然后线性地址的偏移量加到这个物理地址上形成最终物理页面内
的目的地址。
其它CPU使用三级转换方法,如图8-3所示。这在64位体系中尤其有用,以Alpha为
例,其更大的64位的地址空间意味着类似于x86体系的地址转换将要求大量的页目
录、大量页表、大量偏移量,或三者兼有。对于这种情况,Alpha的设计者们向线
性地址模式中引入了另一层次,即Linux所称的页面中间目录(page middle
directory),它位于页目录和页表之间。
这个方案与以前实际是一样的,只不过多增加了一级。这种三级转换方法同样具有
页目录,页目录的每一项包含一个页面中间目录的入口地址,页面中间目录的每一
项包含一个页表的入口地址,而页表也同以前一样每一项包含物理内存中一个页面
的地址,这个地址再加上偏移量就得到了最终的地址。
而使情况更为复杂的是,通过进一步观察可知,三部分地址模式与两级地址转换是
相关联的,而四部分地址模式则与三级地址转换相关联的,这是由于我们通常所说
的“级(或层次levels)”不包括索引到页目录的第一步(我想是因为这一步没有
进行转换的缘故)。
令人奇怪的是内核开发者们决定只用其中一种模式来处理问题。绝大部分的内核代
码对MMU一视同仁,就如同MMU都使用三级转换方法(也就是四部分地址模式)一样
。在x86平台上,通过将页面中间目录定义为1,页面相关的宏可以把三级分解过程
完美地转换到二级分解过程上去。这些宏认为页面中间目录和页目录是几乎可以进
行相互替换的等价品,以至于内核的主要代码可以认为其地址就是由四个部分组成
的。
在x86系统中,32位地址中10位是页目录索引,接下来10位是页表索引,剩下的12
位用作偏移量,这就构成了4K大小的页面(212等于4096个偏移量)。
用于创建和操作每一级项的函数和宏定义在头文件include/asm-i386/page.h(第
10786行)和include/asm-i386/pgtable.h(第10876行)之中。在读者浏览这些函
数和宏的时候,记住PGD通常代表“页目录项(page directory entry)”(不只
是“页目录”),PMD通常代表“页面中间目录项(page middle directory
entry)”(不只是“页面中间目录”),同样PTE也通常代表“页表项”。而且,
正如上面解释中限定词“通常”所暗示的那样,例外是存在的,例如下文将要提到
的pte_alloc就分配页表而不是(如你所可能会认为的)页表项。非常遗憾的是,
由于篇幅所限我们不能对全部例程进行讨论,我们将在后面对其中的一部分进行讨
论。
页表项不仅记录了一个页面的基地址,而且记录了它的保护信息(protections)
,也就是一组指定该页为可读、可写,和/或可执行的标志(这容易让人联想到文
件的保护位)。随着我们对页面保护信息的进一步剖析,读者会看到页表项所包括
的其它页面特有的标志。



转换后备缓存(Translation Lookaside Buffers:TLBs)
如果简单的执行从线性地址到物理地址的转换过程,在跟踪指针链时将会需要几个
内存引用。RAM虽然不像磁盘那么慢,但是仍然比CPU要慢的多,这样就容易形成性
能的瓶颈。为了减少这种开销,最近被执行过的地址转换结果将被存储在MMU的转
换后备缓存(translation lookaside buffers:TLBs)内。除了偶尔会通知CPU,
由于内核的某操作致使TLBs无效之外,Linux不用明确管理TLBs。
在作用于TLB的函数和宏中,我们只研究一下__flush_tlb,在x86平台上,它是其
它大部分函数和宏的基础。
__flush_tlb
10917:CR3(控制寄存器3)是x86CPU寄存器,它保存页目录的基地址。往这个寄
存器送入一个值将会使CPU认为TLBs变成无效,甚至写入与CR3已有值相同的值也是
这样。
       因此,__flush_tlb仅是两条汇编程序指令:它把CR3的值保存在临时变量
tmpreg里,然后立刻把tmpreg的值拷贝回CR3中,整个过程就这么简单!
       注意x86系统也允许使某一个单独的TLB项无效,而并不一定非要使全部项
,这种方法使用invipg指令——参见第10926行它的使用信息。

由于段不是在所有CPU中均可用,所以Linux内核中与体系结构无关的部分不能对段
进行辨识。在不同的CPU体系中,段的(如果段在体系中是可用的)处理方式大相
径庭,这一点是非常重要的。因此,我们在这个问题上不会花费太多时间,不过
x86系统上内核使用段的方式还是值得大概讨论一下的。
段可以被简单的看作是定义内存区域的另一种机制,有些类似于页。这两种机制可
以重叠:地址总是在页面之内,也可能处于段内。与页不同,段的大小可以变化,
甚至在其生存期里能够增长和收缩。与页相同的是,段可以被保护,而且其保护可
由硬件实施;当x86的段保护和同一地址的页保护发生冲突时,段保护优先。
X86系统使用一些寄存器和全局描述表(GDT)和局部描述表(LDT)这两种表来对
段进行跟踪。描述符(descriptor)是段的描述信息,它是用来保存段的大小和基
址以及段的保护信息的8字节的对象。GDT在系统中只有一个,而Linux可以为每个
任务建立一个LDT。
接下来我们将简单解释内核是如何使用这些表来建立段的。内核本身拥有分离的代
码和数据段,它们被记述在GDT的第2和第3行项里。每个任务也有分离的代码和数
据段。当前任务的代码段和数据段不仅在它自己的LDT的第0和第1行项被说明,而
且还被记述在GDT的第4和第5行项里。
在GDT里,每个任务占两行项,一个用来定位它的LDT,一个用来定位它的TSS(前
面章节曾简要提及的任务状态段)。因为x86CPU限制GDT的大小为8192个项,而且
Linux为每个任务占用两行GDT项,因此显而易见的是我们不能拥有超过4096个任务
,这也正是在第7章里提到的限制。事实上,任务的最大数目要稍小一点儿,不过
仍有4090个,这是由于GDT的前12行项被保留用于其它目的。
富有经验的x86程序员可能已经注意到Linux所使用的x86分段机制是采用最低限度
方式的;段的主要使用目的仅是为了避免用户代码出现在内核段中。Linux更倾向
于分页机制。从大的方面来看,对于处理器来说分页或多或少都是相同的,或者说
总的事实就是这样,因此内核越是以分页方式工作,它的可移植性就越好。
最后要提及的是,如果读者对于x86的分段机制很感兴趣的话,不妨阅读一下
Intel体系结构下的软件开发手册第3卷(Intel Architecture Software
Developer’s Manual Volume 3),该书可以从Intel站点上免费得到(
developer.intel.com/design/pentiumii/manuals/243192. htm)。
进程的内存组织
有三个重要的数据结构用于表示进程的内存使用:struct vm_area_struct(第
15113行)、struct vm_operations_struct(第15171行),和struct mm_struct
(第16270行)。我们随后将对这三个数据结构进行逐一介绍。
Struct vm_area_struct
内核使用一个或更多的struct vm_area_struct来跟踪进程使用的内存区域,该结
构体通常缩写为VMAs。每个VMA代表进程地址空间的一块单独连续的区间。对于一
个给定的进程,两个VMAs决不会重叠,一个地址最多被一个VMA所覆盖;进程从未
访问过的的一个地址将不会在任何一个VMA中。
两个VMA之间的区别有两个特征:
?   两个VMA可以不连续(Two VMAs may be discontiguous)——换句话说,一个
VMA的末尾不一定是另一个的开头。
?   两个VMA的保护模式可以不同(Two VMAs may have different protections)—
—例如,一个是可写的而另一个可能是不可写的。即使两个这样的VMA是连在一起
的,它们也必须被分开管理,因为其不同的保护信息。
应注意的一个重点是,一个地址可以被一个VMA所覆盖,即使内核并没有分配一个
页面来存贮这个地址。VMA的一个主要应用就是在页面错误时决定如何作出反应。
我们可以将VMAS看作是一个进程所占用的内存空间以及这些空间的保护模式的总体
视图。内核能够反复重新计算从页表而来的VMA中的大部分信息,不过那样速度会
相当慢。
进程的所有VMA是以一个排序的双向链表方式存储的,并且它们使用自己的指针来
管理该列表。当一个进程有多于avl_min_map_count数目(在第16286行定义为32)
的VAM时,内核也会创建一个AVL数来存储它们,此时仍然是使用VMAs自己的指针对
该树进行管理。AVL树是一个平衡二叉树结构,因此这种方法在VMA数量巨大时查找
效率十分高。不过,即使在AVL树被创建后,线性列表也会被保留以便内核即使不
使用递归也能轻松的遍历一个进程的所有VMA。
Struct_vm_area_struct的两个最重要的元素是它的vm_start和vm_end成员(分别
在第15115行和15116行),它们定义了VMA所覆盖的起止范围,其中vm_start是
VMA中的最小地址,而vm_end是比VMA最大地址大一位的地址。在本章后面我们会反
复提及这些成员。
注意,vm_start和vm_end的类型是unsigned long,而不是读者可能会认为的
void*。由于这个原因,内核在所有表示地址的地方都使用unsigned long类型,而
不用void*类型。采用这种方法的部分原因是可以避免因内核对诸如比特一级的地
址进行计算操作时引起的编译警告,还可能避免由于它们而偶然引起的间接错误。
在引用内核空间的一个数据结构的地址时,内核代码使用指针变量;在对用户空间
的地址进行操作时,内核却频繁的使用unsigned long——实际上,几乎只有本章
中所涉及的代码才是这样。
这样就给用来编译内核的编译器提出了要求。使用unsigned long作为地址类型就
意味着编译器必须使unsigned long的类型长度和void*的一样,尽管实践中对这一
点的要求不是十分严格。对于x86寄存器上的gcc来说,两种类型很自然的都是32位
长。在64位指针长度的体系中,比如Alpha,gcc的unsigned long类型长度通常也
是64位。尽管如此,在将来的体系结构上,gcc的一个端口可能提供与void*不同的
unsigned long类型长度,这是需要内核的移植版本开发人员(kernel porters)
注意的一点。
还要说明的是,除了gcc之外你不需要对编译器的性能有太多担心,因为其它大部
分与gcc相关的特性都已经包括在代码之中了。假如读者试图用某个其它的编译器
来编译内核的话,我想有关unsigned long和void*长度的错误将会占编译错误列表
的绝大多数。
Struct vm_operations_struct
一个VMA可能代表一段平常的内存区间,就像是malloc函数所返回的那样。但是它
也可以是对应于一个文件、共享内存、交换设备,或是其它特别的对象而建立的一
块内存区域;这种对应关系是由本章后面将要涉及的称为mmap的系统调用所确定的

我们不想牵扯太多关于VMA可以被映射的每一种对象的专门知识,这会使对内核代
码的剖析变得凌乱不堪,因为那样就不得不反复决定是否要关闭一个文件、分离共
享内存等等令人非常头疼的事情。与此不同,对象类型struct
vm_operations_struct抽象了各种可能提供给被映射对象的操作,比如打开、关闭
之类。一个struct vm_operations_struct结构体就是一组函数指针,它们之中可
能会是NULL用来表示一个操作对某个被映射对象是不可用的。举例来说,在一个共
享内存没有被映射的情况下,把该共享内存对象的页面与磁盘进行同步是没有意义
的,表示共享内存操作的struct vm_operations_struct里的sync成员就是NULL。

总之,一旦VMA映射为一个对象,那么它的vm_ops成员就会是一个非空的指针,指
向一个表示被映射对象所提供操作的struct vm_operations_struct结构体。对于
VMA可以映射的每一种对象类型,都有一个该VMA可能会在某处指向的静态static
struct vm _ operations_struct结构体。参见第21809行这样的一个例子。
Struct mm_struct
一个进程所保留的所有VMA都是由struct mm_struct结构体来管理的。指向这种结
构类型的指针在struct task_struct中,确切的说,它就是后者的mm成员。这个成
员被前一章中所讨论的goodness(第26388行)应用,来判断是否两个任务是在同
一个线程组中。两个具有相同mm成员(正如我们所见到的)的任务管理同一块全局
内存区域,这也是线程的一个特点。
struct mm_struct结构体的mmap成员(第16271行)就是前述的VMA的链接列表,而
它的mmap_avl成员,如果非空,就是VMA的AVL树。读者可以浏览struct
mm_struct的定义,会发现它还包括相当多的其它成员,它们中的几个会在本章中
涉及到。
VMA的操作
本小节介绍后面要用到的find_vma函数,并捎带简介它的同类函数find_vma_prev
。这将阐明VMA处理操作的一些方面,也为读者将要接触的代码做准备。
find_vma
33460:简单说来,find_vma函数的工作就是找到包含某特定地址的第一个VMA。更
准确   的说,它的工作是找到其vm_end比给定地址大的第一个VMA,这个地址可能
会在该VMA之外,因为它可以比VMA的vm_start要小。这个函数返回指向VMA的指针
,如果没有满足要求的VMA就返回NULL。
33468:首先,通过使用mm的mmap_cache成员,满足进程最近一次请求的同一VMA会
被检查,而mmap_cache正是为此目的而设。我没有亲自测试过,不过这个函数的文
档中说高速缓存的命中率可以达到35%,考虑到高速缓存只由一个VMA组成,那么这
个数字就相当好了。当然,著名的、被称之为“引用的局部性(locality of
reference)”的特性一直在其中提供了很大帮助,这也是软件访问数据(和指令
)时的一条原则,即访问最近使用过的数据(和指令)。由于VMA包含一块连续的
地址区间,引用的局部性就使得所需的地址都在同一个VMA中变为可能,而这样的
VMA就会满足前面的要求。
在修改VMA列表的其它几个地方,这个高速缓存的值被设为空,表明对VMA列表所做
的修改可能会使它失效。至少在一个这种情况中,第33953行,使该高速缓存为空
不总是必要的;这段代码如果能够再聪明一些的话,就可能从本质上改善高速缓存
的命中率。
33471:高速缓存没有命中。假如没有AVL树,find_vma只是搜索列表上的所有VMA
,然后返回第一个符合条件的VMA。回想一下VMA的列表是保持顺序的,所以符合条
件的VMA也就是所有符合条件的VMA中地址最小的一个。假如搜索到列表的末尾都没
有一个匹配,vma就被置为NULL,并被返回。
33476:若有大量VMA,沿树遍历就比沿链表遍历要快;由于AVL树是平衡的,这就
是一种对数时间操作而不是线性时间操作。
树的迭代遍历并不是十分少见的现象,不过一些特征也并不非常明显。首先注意第
33484行的赋值;这个操作一直跟踪当前找到的最好节点,当不能找到更好的时,
它就会被返回。接着的下一行中的if语句是一个最优测试,检测addr是否处于VMA
中(我们已知的一点是addr小于VMA的vm_end)。因为VMA绝对不会彼此覆盖,没有
其它VMA将是一个较贴近的匹配结果,所以树的遍历可以早些结束。
33492:如果在树的遍历或列表搜索过程中找到一个VMA,找到的值就被保存在高速
缓存里以便下一次查找。
33496:在任何情况下,vma都被返回;它的值或者是NULL,或者是满足查找条件的
第一个VMA。
Find_vma_prev
如前所述,这个函数(从第33501行开始)和find_vma函数是一样的,不过它还会
额外的返回一个指向前一个限定的addr(如果有)的VMA的指针。这个函数不仅是
因为它本身的缘故而令人感兴趣,更主要是由于它的出现会告诉我们一些关于内核
程序设计,特别是关于Linux内核程序设计的信息。
应用程序员很可能已经在更加通用的find_vma_prev函数之上写出了find_vma函数
,这只需简单的把指向VMA的指针去掉即可,代码如下:
p504.1
应用程序员这样做的原因是具有代表性的应用程序并不太拘于速度因素。这并非纯
粹是在为铺张浪费找借口,而是由于CPU速度的不断增加使得应用能够更关注于其
它方面,我们现在可以出于可维护性的充分理由而提供一个可以到处使用的额外函
数调用。
与之相反,一个内核程序员可能不会随便增加多余的函数调用;试着减少几个CPU
时钟周期会被认为比负责维护某个近乎是副本的函数要更胜一筹。即使没有其它原
因,我们也可以说内核开发者所持有的这种态度就是为了让应用程序员能相对自由
一些。
为什么这种重复对于源代码相对封闭的操作系统,Linux而言不那么重要,这里是
否有更深层次原因呢?尽管Linux内核必须限制它占用的CPU时间,Linux内核的开
发工作却不受程序员时间的限制。(明确的说,我必须要指出Linux的开发者不必
把他们的时间浪费在会议上的,他们也不必被人工制订的时间表所拘束。)正是由
于这众多的队伍,众人的智慧,才改变了软件开发的规则。
Linux内核的源代码对任何人都是公开的,Linus本人曾说过的一句名言是“…只要
眼睛够多,所有的臭虫(程序错误)都是浅薄的 ”。就算函数find_vma和
find_vma_prev的执行会产生重大差异,在你能想到“重编译”之前,不知什么地
方的某个Linux内核开发者就已经迅速发现并修复了这个问题。实际上,Linux内核
开发者比它的商业对手动作快得多,所得到的代码运行更快而且错误更少,尽管有
时偶然出现的结构会被认为在任何其它环境中都不可维护。
当然,如果没有人负责对这些函数的改进进行维护的话,我认为这也是非常愚蠢的
。内核的下一个发布版本就把它们合并了。但是我仍然对此持怀疑态度,而且即使
我在这个具体问题上所持的态度并不正确,我依然在总体上保持原有态度。 不同
的事还会继续不同,而不同正是Linux之所以为Linux的一方面。
分页
本章前面对分页已作了概要描述,现在我们进一步来研究Linux是如何处理分页的

页面保护详述
正如早先提及的,页表项不仅保存了一个页面的基地址,还有其它一些标志信息,
这些标志指出了该页面上所能进行的操作。现在是仔细研究一下这些标志的时候了

如果页表项只保存一个页面的基地址,并且页面是页对准的(page-aligned),这
个地址的低12位(x86系统),即偏移量部分通常将总是为0。取代这些位置0的作
法是把它们编码作为与页面有关的标志,在获取地址时只需简单的把它们屏蔽掉就
行了。以下就是这12位中的标志位:
?   _PAGE_PRESENT位(第11092行),若置位,当前页面物理存在于RAM中。
?   _PAGE_RW位(第11093行),置为0表示该页面是只读的,置为1表示可读可写。
因此,没有只写的页面。
?   _PAGE_USER位(第11094行),置位表示某页面是用户空间页面,清空表示为内
核空间页面。
?   _PAGE_WT位(第11095行),置为1表示页面高速缓存管理策略是透写,置为0表
示管理策略是回写。透写(writethrough)会立刻把写入高速缓存的数据复制(拷
贝)到主存储器内,即使保存在高速缓存的数据仍是读访问。与之相反,回写(
writeback)具有更高的效率,写入高速缓存的数据仅当其必须为其它数据腾出空
间,而必须移出时才被复制到主存储器内。(这是由硬件,而不是Linux完成的。
)尽管直到本书写作之时,这个标志位在内核中的使用还并不非常普遍,不过这种
情形有望很快改变。有时候,Intel公司的处理器资料中把WT位更多的称为PWT。
?   _PAGE_PCD位(第11096行),关闭页面高速缓存;本书中的代码不总是使用这个
标志位。(缩写“CD”表示“caching disabled”。)如果我们恰好知道一个不经
常使用的页面,那么就不必为它设置高速缓存,这可能会更有效率。这个标志位好
像对于映射内存的I/O设备来说更有用处,尽管我们想确保对表示设备的内存进行
的写操作不被高速缓存缓冲,但是取而代之的作法是立刻把数据直接拷贝到设备之
中。
?   _PAGE_ACCESSED位(第11097行),若置位表示该页面最近曾被访问过。Linux可
以设置或清除这个标志,不过通常这是由硬件完成的。因为清除了该标志的页面已
很久未被使用过,所以它们会在交换时被优先调出主存。
?   _PAGE_DIRTY位(第11098行),若置位,表明该页面的内容自从上次该位被清除
后已发生改变。这就意味着它是一个内容没有保存的页面,就不能简单的为交换而
被删除。当一个页面第一次写入内存时,该标志位由MMU或Linux设置;当这个页面
调出内存时,Linux要读取它的值。
?   _PAGE_PROTNONE位(第11103行),是一个以前的页表项没有使用过的标志位,
用来跟踪当前页面。
_PAGE_4M位和_PAGE_GLOBAL位在同一个#define定义块中出现,但是由于它们不像
其它标志位那样用于页面级的保护,所以我们在此不予讨论。
随后的文件中,上述这些标志位被组合在一个高级宏内。
写拷贝(copy-on-write)
提高效率的一条路就是偷懒——只做最少量的必要工作,而且只在不得不做的时候
才完成。现实生活中这可能是个坏习惯,至少它会导致拖拖拉拉。而在计算机的世
界里,它可能更是一种优点。写拷贝(Copy-On-Write)就是Linux内核一种通过懒
惰来获得效率的方法。其基本思想是把一个页面标记为只读,却把它所含的VMA标
识为可写。任何对页面的写操作都会与页级保护相冲突,然后触发一个页面错误。
页面错误处理程序会注意到这是由页级保护和VMA的保护不一致而导致的,然后它
就会创建一个该页的可写拷贝作为代替。
写拷贝十分有用。进程经常fork并立刻exec,这样为fork而复制的进程页面会造成
浪费,因为exec之后它们会不得不被抛弃。正如读者所见,进程分配大量内存时也
使用同样的机制。所有被分配的页面都与一个单独的空白页面相映射,这就是写拷
贝的原意。向某页面的第一次写操作会触发页面错误,然后空白页面执行复制。用
这种办法,只有页面分配不能再延期时,它才会被分配。
页面错误
到现在为止,本章已几次提到一个页面可以不在RAM里的可能性——毕竟,如果页
面总是在内存里,虚拟内存就没什么必要了。但是我们还没有详细介绍过当某页面
不在RAM中会怎样。当处理器试图访问一个当前不在RAM中的页面时,MMU就会产生
一次页面错误,而内核会尽力解决它。在进程违反页级保护时,页面错误也会产生
,例如进程试图向只读内存区域写入。
因为任何无效内存访问都会导致页面错误,同样的机制支持请求分页。请求分页(
Demand paging)的意思是只有在页面被引用的时候才从磁盘上读取它们——即按
需分配——这是另一种通过懒惰来获得效率的方法。
特别地,请求分页用于实现被请求页面的可执行化。为了达到这个目的,应用程序
第一次被装载时,只有一小部分可执行映象(image)被读入物理内存,然后内核
就依靠页面错误来调入需要的(比如,进程首次跳转到一个子例程时)可执行页面
。除了一些意外的情况,这样做总是要比把所有部分一次读入要快,这是因为磁盘
较慢,而且并不是所有的程序都会用到的。事实上,因为一个大程序运行一次时,
大部分功能特性都不会再用到,所以通常根本不需要全部都读入(这一点对大多数
中小规模的程序也是成立的)。这对于按需分页(demand-paged)的可执行程序稍
有不同——如果你对这种情况进行考虑的话,你就可以知道按需分页还需要二进制
处理程序的支持,而且它是一个具有决定意义的部分。
Do_page_fault
6980: do_page_fault是内核函数,产生页面错误时(在第363行)被调用。当页
面错误产生时,CPU调整进程的寄存器,当解决页面错误时,进程再从引起错误的
指令处开始执行。通过这种方法,在内核使得冲突地内存访问操作完成后,会自动
重试该操作。相反,如果页面错误仍然无法解决,内核就通知引起冲突的进程。当
页面错误是由内核本身导致的时候,所采用的措施是近似的,但并不完全相同。
6992: 控制寄存器2(CR2)是Intel CPU的寄存器,保存引起页面错误的线性地址
。该寄存器内的地址会被直接读入局部变量address。
7004: 函数find_vma(第33460行)返回地址范围末尾在address之后的第一个
VMA。大家知道,这并不能够保证该地址位于VMA的范围内,而仅保证该地址比VMA
的结束地址小,这样它就可能比VMA的初始地址还要小。因此这一点要被检查。假
如通过判断,则address在VMA之内,控制就会向前跳转到标号good_area处(第
7023行);我们随后就会对这一点进行讨论。
7005: 如果find_vma返回空值NULL,那么address就位于进程的所有VMA之后——
换句话说就是超出了由进程引用的所有内存。
7009: vma的开头和结尾都确实超过了address;因此address在VMA低端地址以下
。但是这并不会失去什么。如果VMA是向下扩展的类型——也可以说它是堆栈——
这个堆栈可以简单的向下扩展来适应那个地址。
7011: CPU提供的error_code的测试位2。与监控(内核)模式相比,更多是在用
户模式发生页面错误时设置此位。如果是在用户模式下,do_page_fault函数会保
证给定的地址在为进程建立的堆栈区域内,正如ESP寄存器所定义的那样。(例如
,在代码溢出了被分配的堆栈矩阵时,就会产生这种情况。)如果是在监控(内核
)模式下,就会跳过后一种判断,而简单的假定内核运行正常。
7019: 如果可能,使用expand_stack(行15480)将扩展到包含新的地址。如果成
功,VMA 的vm_start成员将调整到包含address。
7023: 到达good_area标记时,就意味着VMA包含address,或者说要么它已经包括
了address,要么就是堆栈扩展后包括了该地址。
不管那一种方法,包括错误产生原因信息的error_code最低两位现在都可以被测试
了。第0位是存在/保护位:0表示该页不存在;1表示该页存在,但试图的访问操作
与页级保护位冲突。第1位是读/写位:0表示读,1表示写。
7025: switch条件判断语句对于上述两个测试位所组合出的四种可能情况作出相
应处理:
?   case 2或3——检查包括的VMA是否可写。若可写,就是向一个写拷贝页面执行一
次写操作;变量write被增加(设置到1)以便接下来对hand_mm_fault的调用能够
完成写拷贝过程。
?   case 1——这意味着页面错误是由试图从一个存在但不可读的页面中读数据而导
致的;这个尝试会被拒绝。
?   case 0——表示页面错误是由试图从一个不存在的页面中读数据而导致的。如果
涉及的VMA保护指出该区间既不可读也不可写,读页面只不过是浪费时间——如果
再次尝试,将引起另一个页面错误,这样do_page_fault函数会以case 1的结果告
终,即拒绝尝试。否则do_page_fault函数继续执行并从磁盘上读入页面。
7047: 请求handle_mm_fault函数(下面讨论)使该页面变为当前页面。如果失败
,则发出一个SIGBUS错误。
7062: 大多数内核函数的清除代码都不太显著。do_page_fault函数是一个例外;
我们会比较详尽的研究它的清除代码。下列任何情况发生都会跳到bad_area标记处

?   被引用的地址超过了为进程分配的(或保留的)所有内存。
?   被引用的地址位于所有VMA之外,而且可能由于比该地址小的VMA不是堆栈而无法
扩展到这个地址。
?   违反了页面的读/写保护。
7066: 如果用户代码引起以上任何错误,那将发送致命的SIGSEGV信号——一个分
段违例。(注意术语“分段”在这里是历史上的说法而不是字面所表达的意思——
对CPU来说,从技术角度看它是分页违例,不一定是分段违例。)这个信号通常会
像第6章中讨论的那样杀死一个进程。
7075: Intel Pentium CPU(以及它的一些兼容产品)具有一个所谓的f00f缺陷,
它允许任何进程用非法的0xf00fc7C8指令来冻结CPU。Intel所提议的弥补工作就是
在这里实现的:中断描述表(见第6章)的一部分以前是被标识为只读的,因为这
样会使非法指令执行时用产生页面错误代替冻结CPU。在这里,do_page_fault函数
检查导致页面错误的地址是否位于IDT中由非法指令执行而产生的位置上。如果是
这样的,处理器会试着执行“Invalid Opcode”服务中断——CPU的缺陷会使得正
确完成这一步失败,而代码却会通过直接调用do_invalid_op函数而产生正确的结
果。否则,CPU决不会对IDT进行写操作(即使没有标注为只读时也是如此),所以
即使第7080行的检测失败,非法指令也是根本不会被执行的。
7086: 下列情况发生时,标记no_context会被执行:
?   在内核(不是用户)模式里到达bad_area,而且CPU不执行触发f00f缺陷的非法
指令。
?   在一个中断中或没有用户环境(用户任务没有处于正在执行状态)时发生的页面
错误。
?   Handle_mm_fault函数错误并且系统处于内核模式中(我还从未遇到过这种情况
)。
这里的任何一种情形都是内核错误(经常由驱动程序所导致),它不是因为任何用
户代码而造成的页面错误。如果内核(或驱动程序)事先为这种可能准备了错误处
理代码,那么这些错误处理代码一定位于本书讨论范围之外,并在错误发生时可以
通过某种特殊技术跳转过去。
7097: 否则,内核试图访问一个坏页面,do_page_fault函数将不知如何处理它。
这可能也够被考虑到。内核启动代码检查是否MMU写保护工作正常;如果正常,那
就不是一个真正的错误,do_page_fault函数就可以简单的返回了。
7109: 内核访问了一个坏页面,并且do_page_fault函数无法修复这个错误。
do_page_fault函数会在第7129行显示出一些描述错误的信息,然后中止内核本身
。这样整个系统就会被停止,很明显没有任何操作会被执行。不过,如果系统运行
到了这一步,内核也别无选择了。
7134: 最后一个标记是do_sigbus,只有当handle_mm_fault函数无法处理错误时
才会执行到这里。这种情况相对简单;大体上是给违例的进程发送一个SIGBUS错误
信号,如果这是在内核模式里发生的就再跳回到no_context标记处。
Handle_mm_fault
32725:调用者已经检测到了需要一个可用的页面。该页面正是包含address的页面
,这个地址应归入vma中。Handle_mm_fault函数本身相当简洁,但是它建立在其它
几个处理冗长细节问题的函数和宏之上。我们介绍完此函数后将逐一研究那些底层
函数。
32732:查找关联的页目录和页面中间目录入口项(如前所述,这两者在x86平台上
实际是一样的)。
32735:从页面中间目录项得到或定位(如果可能的话)页表。
32737:调用handle_pte_fault函数把页面读入页表项(page table entry);如
果成功,就调用update_mmu _cache函数更新MMU的高速缓存。控制流程到此为止,
一切顺利,handle_mm_fault函数就可以返回一个非零值(1)表示成功了。如果此
过程任何一步出错,控制就转向第32744行,函数返回0值表示失败。
Pgd_offset
11284:这个宏将address除以2PGDIR_SHIFT(第11052行#defined to 32),并对
结果向下舍入,然后把最终结果(移位之前的高端10位)作为提供的struct
mm_struct的pgd数组的一个索引。因此,它的值就是页目录项,相应的页表
address地址就位于该项中。
   这等价于代码
&((mm)->pgd[(address)>>PGDIR_SHIFT]);
而且可能会更高效。
Pmd_alloc
11454:因为x86平台上没有定义页面中间目录,这样就极其简单:它只需返回给定
的pgd指针,并映射为一种不同类型。在其它平台上,该函数与pte_alloc类似,还
要实现更多的工作。
Pte_alloc
11422:Pte_alloc函数有两个参数:一个是指针,指向目标地址所位于的页面中间
目录项,另一个是地址本身。如果我们暂时跳过一部分内容,那么对该函数经过变
形的逻辑的理解就会更容易,所以让我们看一下随后的若干行代码。
11425:用一种几乎无法理解的方式把address转换成PMD内的一个偏移量。
这一行需要详细进行解释。首先,回忆一下PMD中的每项都是一个指针,在x86平台
上它的长度是4个字节(这里的代码是与体系结构相关的,所以我们可以作出这样
的假定)。用C语言来定义就是,

&pmd[middle_10_bits(address)]

(为清晰起见,我在这里引入了假定的pmd数组和middle_10_bits函数)该代码等
价于

pmd+middle_10_bits(address)

这又与如下代码指向的地址相同

((char*)pmd)+middle_10_bits(address)*sizeof(pte_t*)

其技巧是在最后的公式中——或者更准确的说是+号后边的部分——最接近于第
11425行所要计算的实际值。
为了使这一点更为明确,首先可知

4*(PTRS_PER_PTE-1)

就是4092(第11059行PTRS_PER_PTE被定义为1024)。用二进制表示,4092只用占
最低12位,甚至最后2位也用不上。它和只占最低10位的1023左移2位后的值相同。
这样就有

(address>>(PAGE_SHIFT-2))

把address右移10位(第10790行PAGE_SHIFT被定义为12)。这两个表达式结果再逐
位进行与(AND)操作。最终的结果类似于:

((address>>PAGE_SHIFT)&(PTRS_PER_PTE-1))<<2

尽管这仍很复杂,不过它更简单明了:它把address右移12位(为了去掉页面偏移
量部分),屏蔽掉除最低10位的其它位(去掉页目录索引部分,只保留最低10位的
页面中间目录索引),接着把结果左移2位(相当于乘以4,即指针长度的字节数
sizeof(pte_t*))。更直接的方法可能会稍慢一些,但在内核里,我们终归是要尽
力节省时间的。(虽然更直接的方法看来并非明显偏慢:同样版本内核进行两次移
位、两次减法,以及按位与的操作,和进行两次移位、两次按位与的操作,就我的
测试看来实际上是一样快。)
不管采用那一种方法,经过计算之后,把address和PMD的基地址相加(在第11432
行和别的地方执行),就得到了指向与address初值关联的PTE的项指针。
11428:如果PMD项不指向任一个页表,函数向前跳到getnew处分配一个页表。
11435:通过调用get_pte_fast(第11357行)尝试从pte_quicklist中申请一个页
表。这个页表是页表的一个高速缓存,其思想是分配页表(它们本身就是独立的页
面)慢,而从一系列近期释放的页表中指定一个却会稍快一些。所以,代码经常用
free_pte_fast(第11369行)来释放页表,这会把它们放在pte_quicklist里而不
是确实把它们消除掉。
11439:pte_quicklist能够提供一个页表页面。页表可以被送入页面中间目录,并
且函数返回页表中这个页面的偏移值。
11438:pte_quicklist缓存里没有剩下页面,因此pte_alloc需要调用
get_pte_slow函数(第7216行)来分配一个缓慢页面。该函数用__get_free_page
来分配页面,执行过程和一个页面被找到时相似。
11430:如果PMD项不是0,但是是无效的,pte_alloc显示一个警告(通过调用第
7187行的bad_pte)并放弃尝试。
11432:所期待的正常情况:pte_alloc函数返回一个指向包括address地址的PTE的
指针。
Handle_pte_fault
32690:Handle_pte_fault函数试图取回或者创建一个缺少的PTE。
32702:给定的项与物理内存中的任何一个页面都无关联(32700行),而且确实没
有被设置(32701行)。这样,do_no_page(32633行)将被调用以创建一个新的页
面映射。
32704:页面在内存中不存在,但是它有一个映射,所以它一定在交换空间里。函
数do_swap_page(32569行)将被调用来把该页面读回内存。
32708:页面在内存里,所以情况可能是内核正在处理一个页面保护冲突。
Handle_pte_fault首先要用pte_mkyoung(11252行)来把该页面标识为已被访问。

32713:如果是一个写访问操作而页面又不是可写的,Handle_pte_fault就调用
do_wp_page函数(32401行)。这个函数完成真正的写拷贝功能,因此我们要简单
介绍一下。
32715:这是一次对可写页面的写访问。Handle_pte_fault设置该页面的“dirty”
位,表示在它被丢弃之前必须被复制到交换空间。
32720:所需的页面现在可被调用者使用,所以Handle_pte_fault函数返回非零值
(确定为1)以示成功。
Update_mmu_cache
11506:在x86平台上,update_mmu_cache函数是一个空操作。它是一种所谓的“挂
钩(hook)”函数——这种函数要在内核的平台无关部分中适当地点处保证被调用
,以便不同的移植版本都能够在必要的情况下对它进行定义。
Do_wp_page
32401:如前所述,真正的写拷贝操作是在这里实现的,所以我们有必要介绍一下
。tsk试图写入address,这个地址在给定的vma里并由所提供的page_table来控制

32410:调用__get_free_page(15364行,简单的转向第34696行调用
__get_free_pages函数)为进程提供一个新页面,此页面是写保护页面的一个新拷
贝。注意这里可以允许一个任务转换。有趣的是,这里的代码不检查
__get_free_page分配新页面时是否成功——它实际上可能不需要新的页面,因此
到必要时才会去进行检查。
32422:增加“次要(minor)”页面错误,这些错误无需访问磁盘就可被满足。
32438:只有两个页面用户存在,其中一个是交换高速缓存(swap cache),它是
已被交换出但还未被回收的页面的临时缓冲池。该页面被移出交换高速缓存后(利
用37686行的函数delete_from_swap_cache),现在它就只有一个用户了。
32445:要么从交换高速缓存里回收该页面,要么它只有一个开始用户。这个页面
会被标识为可写和“脏”dirty(因为它已被写过)。
32448:如果已分配了一个新页面,它就没有用了:由于该页面只有一个用户,所
以没有必要进行复制。do_wp_page函数释放这个新页面,并返回非零值表示成功。

32454:页面拥有不止一个用户,不能简单的从交换高速缓存里被收回。因此
do_wp_page函数将需要复制一个新页面。如果先前的页面分配失败,现在就是该结
果产生作用的时候了,do_wp_page函数将不得不返回错误。
32459:利用copy_cow_page(31814行)复制页面内容。这通常是调用copy_page宏
(32814行),它是一个memcpy。
32460:利用flush_page_to_ram(10900行)使RAM新旧页面拷贝同步。像
update_mmu _cache函数一样,在x86平台上这是一个空操作。
32463:像以前一样,使得页面可写和“脏”,同时保留从封装的VMA而来的其它页
面保护(比如可执行)。
32466:对函数free_page(在15386行,它只是调用34633行的free_pages函数)的
调用而不会真正释放旧的页面,因为该页面拥有多个用户——它只会减少旧页面被
引用的次数。由于满足了调用者的请求,do_wp_page就返回非零值表示成功。
页面调出
现在读者已经对交换页面调入有所了解,接下来看一看另一方面,交换页面的调出

Try_to_swap_out
38863:try_to_swap_out函数是最低一级交换调出函数,它由内核任务kswapd(见
39272行kswapd函数)周期性地调用(通过一系列其它函数调用)。这个函数用来
写一个页面,该页面是由位于给定任务特定VMA中的一个单独页表项来控制的。
38873:如果内存中缺少该页面,它就不能从内存写回到磁盘,这样
try_to_swap_out函数就返回失败。如果给定的地址明显是不合法的(max_mapnr是
当前系统中物理内存的页面数目;参见7546行),它也会丢弃尝试操作。
38880:如果页面被保留、锁住,或者被一个外设用于直接内存访问时,它就不能
被调出。
38885:如果页面最近被访问过,把它调出可能是不明智的,因为引用的局部性可
能会使该页面不久将再被引用。把该页面标识成“旧的”,这样将来的再一次尝试
就可能把它调出内存——这可能很快就会发生,如果内核不顾一切要这么做的话。
但事实是,页面还没有被调出。
这一行之后的代码注释本身就含有大量信息,所以我们将跳过几段代码而不失完整
性。
38965:减少任务的驻留段长度(注意vma->vm_mm是指向含有vma的struct
mm_struct的指针)。驻留段长度是物理内存中的任务所占页面数目,而且很明显
,这些页面中的一个现在已经不存在了。
38968:因为页面无效,所以try_to_swap_out函数必须通知所有TLB以无效化它们
对该页面的引用。TLB不应该再把地址解析到一个已经不存在了的页面。
try_to_swap_out函数接着把这个页面放入交换缓存。
38977:最后,try_to_swap_out函数通过使用rw_swap_cache(35186行)把旧的页
面写回磁盘,写操作是异步的,以便等待磁盘处理时系统也可以作其它工作。
38979:用__free_page(34621行)来释放页面,并返回非零值表示成功。
交换设备
Linux拥有一个按优先级排序的合法交换设备列表(以及文件,不过为了简单起见
,这一部分通常用“设备”来代替这两者)。当需要分配一个交换页面时,Linux
会在仍然拥有空间的优先级最高的交换设备上来分配它。
Linux也会在所有优先级相同的未满交换设备之间轮转使用,采用的是循环方式,
通过这种在多个磁盘上分布分页请求的方法可以提高交换的性能。在等待第一个请
求被满足时,另一个请求就可以分派到下一个磁盘上。最快的设置是把交换分区分
布在几个相似的磁盘上,并给它们同样的优先级设置;而较慢的磁盘则有稍低一些
的优先级。
不过循环也可能造成交换速度的降低。如果同一磁盘上的多个交换设备有同样的优
先级,那么磁盘的读/写头将不得不在磁盘上来回的反复访问它们;在这种情况下
,臭名卓著的1000倍的速度差异就不容忽视了。幸运的是,系统管理员会合理安排
优先级以避免这种情况。Linux继承了Unix的传统特性,既能让你陷入绝境,也能
使你达成非常良好的目标。最简单的方案是给每个交换设备分配不同的优先级;这
会有助于避免最坏的情况,但可能也不会最好。尽管如此,由于该方法简单且不会
最坏,如果你不指定优先级设置,它将是缺省设置。
交换设备用struct swap_info_struct(17554行)结构体类型来表示。在37834行
定义了这些结构体的一个数组swap_info。好几个文件里的函数都操作和使用
swap_info数组来进行交换管理;很快我们就会对它们进行分析。先来分析一下
struct swap_info_struct的成员,这会使我们能够更清楚的了解这些函数。
?   swap_device——发生交换的设备号;如果struct代表一个文件而不是分区,值
是0。
?   swap_file——struct代表的交换文件或分区
?   swap_map——对交换空间里每个交换页面的用户数进行计数的数组;为0则表示
页面空闲。
?   swap_lockmap——用来跟踪基于磁盘的页面当前是否正被读出或写入磁盘,数组
里的每一位对应一个页面。在I/O过程中页面将被锁定以防止内核同时对同一页面
执行两次I/O操作,或者其它类似的愚蠢操作——需要记住的是,一旦有可能,其
它进程就会与I/O操作相重叠,所以发生这种情况并非难事。
?   lowest_bit和highest_bit——跟踪交换设备里第一个和最后一个可用的页面的
位置。这可以有助于加快寻找空闲页面的循环。设备的第一个页面是一个不允许用
于交换的头部,因此lowest_bit不会是0。
?   cluster_next和cluster_nr——用来对磁盘上的交换页面进行分组以获得更高的
效率。
?   prio——交换设备的优先级。
?   pages——设备上可用的页面数目。
?   max——内核在此设备中所允许的最大页面数目。
?   next——把swap_info数组中的所有struct形成一个单独的链接列表(并保持优
先级顺序)。这样,数组就被逻辑排序,而不是物理排序了。next的值就是列表中
逻辑指向下一个元素的索引,如果到达列表末尾它就是-1。
swap_list在37832行定义,包括列表头(即head成员――参见17627行struct
swap_list_t的定义)的索引;如果列表为空则此索引为-1。它还包括名字很令人
迷惑的next成员,这个成员能够跟踪我们将要在其上尝试页面分配的下一个交换设
备。因此next是一个迭代指针。如果列表为空或者当前没有交换,它的值就是-1。

Get_swap_page
37879:get_swap_page函数从最高优先级的拥有空间的可用交换设备里获得一个页
面;如果找到一个,它就返回一个非零代码描述该项,如果系统没有交换就返回0

37885:从上一次停止的地方继续进行迭代。如果列表是空的或没有剩余交换设备
,函数即刻返回。
37891:否则的话,有理由确信存在交换空间,get_swap_page函数恰恰需要找到它
。这个循环过程一直迭代,直到函数找到一项(很可能的情形)或者扫描了每一个
交换设备但没有一个还有剩余空间(不太可能的情形)为止。
37894:利用scan_swap_map(37838行)扫描当前交换设备的swap_map以寻找一个
空闲单元,如果找到了一项,lowest_bit和highest_bit成员也会被更新。要返回
的offset是0或者是该项。
37897:当前的交换设备能够分配一个页面。get_swap_page函数现在把swap_map的
迭代游标向前推进以便请求能被正确的分布在交换设备上。
如果已经到达交换设备列表的末尾或是下一个交换设备的优先级低于当前设备,迭
代过程就会从列表的头部重新开始。这产生两个重要作用:
?   如果较高优先级设备的交换空间又变得可用,get_swap_page就会在下一次迭代
时从那个设备开始分配交换。如果孤立的观察这些代码,读者会认为当高优先级设
备可用的时候,这个函数仍可以从低优先级设备分配少数页面。然而事实并非如此
,在我们对交换页面是如何被释放进行介绍的时候读者就会看到这一点。
?   如果优先级高的交换设备不可用,那么在下一次内核分配一个交换项时,
get_swap_page函数将沿列表进行迭代直到它找到当前优先级的第一个设备为止,
并试着从那个设备分配交换。因此,在内核转向优先级较低的设备之前,内核会继
续考虑优先级较高的设备直至它们全部耗尽。这就是先前讨论过的循环执行过程。

37910:当前设备没有可用的交换空间,或者当前设备是不可写的(这与我们所说
的是同样的)。跳到下一个设备,这样如果它已经到达末尾但还未曾循环一整圈时
,它就会再从头开始循环。
37916:如果get_swap_page函数到达列表的末尾而且已经循环了一遍,它就已经考
虑了所有交换设备但是没有一个拥有空余的空间。因此,结论是再也没有可用的交
换空间了,函数返回0。
Swap_free
37923:swap_free函数是与get_swap_page函数相对的,它释放一个单独的交换项

37939:通过许多简单而又周密的测试后,swap_free函数检查是否正在释放交换页
面的设备具有比随后将被考虑的设备更高的优先级。如果是,它就把此作为一个线
索以将swap_list的迭代器重新设置在列表头部。这样对get_swap_page函数的下一
次调用就会从列表头部开始并能够检测到新近被释放的高优先级空间。
37944:假如最新被释放的页面处于lowest_bit和highest_bit成员所定义的范围之
外,就要相应的对它们进行调整。你可以看到如果swap_free函数在一个以前已经
耗尽了的设备中释放页面,这通常会引起对lowest_bit或者highest_bit的调整,
但并非都要调整。这会使该区间比所需要的大,交换页面分配也会因此比所需要的
要慢。不过这种情况很少发生。无论如何,交换范围都将调整自己以使更多的交换
页面能够被分配和释放。
37950:对swap_map每一元素的使用计数只维护到一个最大值,即SWAP_MAP_MAX(
17551行定义为32767)。达到这个最大值之后,内核将无法知道真正的计数值有多
大;由此它也无法安全的减少该值。否则的话,swap_free函数将减少使用计数并
增加空闲页面的总数。
Sys_swapoff
38161:sys_swapoff函数在可能情况下从交换设备列表中移去被指明的交换设备。

38178:搜索swap_info_structs的列表以查找匹配的项,设置p指向这个数据项、
type指向该数据项的索引,以及prev指向前一项的索引。如果第一个元素被删除,
prev将是-1。
38195:如果sys_swapoff函数搜索了整个列表但没有找到匹配项,那显然是给定了
一个无效的名字。函数返回错误。
38198:如果prev是负值,sys_swapoff函数将删除列表的第一个元素;它相应的适
当更新swap_list.head。可以证明,这等价于

swap_list.head=
    swap_info[swap_list.head].next

不过速度更快,因为其中所牵扯的间接转换更少。
38203:如果正被移去的设备是内核进行交换尝试的下一个设备,迭代游标会被重
新设置在列表头部。这样下一次分配可能要稍慢一点儿,不过并不显著;无论如何
,实际中这样的情况是相当少见的。
38209:由于设备仍在使用中而不能被释放时,它会被恢复到列表的适当地方。如
果这是数个拥有同样优先级交换设备之中的一个,它可能不会回到同以前一样的相
对位置了——它将是具有同样优先级的设备的第一个而不是最后一个——不过列表
仍然是按照优先级进行排序的。
从交换设备列表上删除一个仍有可能被我们又放回原处的设备,这看起来就象是在
做无用功——为什么不等到可以确信它可以被删除时再删除它呢?
答案在于经由一系列利用swap_list的函数调用后,在前面代码行对try_to_unuse
(38105行)的调用能够结束。 如果正被删除的交换设备那时仍在swap_list里,
那么终止这一切的代码将会给系统造成极大的混乱。
38223:若在一个分区上进行交换,sys_swapoff函数将解除对它的引用。
38244:sys_swapoff函数以使所有数据域无效和释放已分配的内存而告终。特别的
,这行代码清除SWP_USED位,这样内核就会在它再次利用该交换设备时知道它已经
是不可用的了。接下来,sys_swapoff函数清除err指示符并返回成功。
Sys_swapon
38300:sys_swapon函数是sys_swapoff的对应函数,它向系统列表里增加交换设备
或交换文件。
38321:找寻未用的一个项。这里有一些微妙之处。读者可能会从nr_swapfiles的
名字推断出它就是交换文件(或者设备)的数目,但是实际它不是。它是曾被使用
过的swap_info的最大索引值,而且从不会被降低。(它记录着这个数组被使用的
最高峰值。)因此,把swap_info中的这许多项循环一遍的结果是,要么发现未用
的一个项,要么在最后一次循环增量后让p指向第nr_swapfiles项之后。在上述的
后一种情况下,若nr_swapfiles比MAX_ SWAPFILES小,那么所有用过的项恰好会排
在数组的左边,而循环就使得p指向它们右边的一个空位。这样,nr_swapfiles就
会被更新。
    有趣的是,即使nr_swapfiles不是最高峰值而是活动交换设备的计数值,循环也
能正确执行。不过若我们改变了nr_swapfiles的原意,文件里的其它代码就会有问
题了。
38328:在swap_info里找到了一个未用的项;sys_swapon函数开始对其进行填充。
这里所提供的一些值将会发生变化。
38338:若SWAP_FLAGS_PREFER被置位,swap_flags的低端15位就被编码为所需的优
先级。(这里使用的常量和接着的几行代码在17510行进行定义。)否则,就不指
定优先级。如前所述,在此情况下的缺省作法是给每一个设备分派一个逐渐降低的
优先级,其目的是在无须人工干预时也能得到令人满意的交换性能。
38344:保证内核允许交换的文件或设备可以被打开。
38352:检查提供给sys_swapon函数的是一个文件还是一个分区。若S_ISBLK返回为
真,它就是一个块设备,即磁盘分区。在此情形下,sys_swapon函数继续确保能够
打开该块设备而且内核此时没有同它进行交换。
38375:同样的,若给定的不是分区,sys_swapon函数必须确保它是一个普通文件
。若是文件,函数还要确保内核此时没有同该文件进行交换。
38384:如果两项测试均告失败,sys_swapon函数就不会再被请求在磁盘分区或文
件上进行交换;它已经拒绝了该尝试。
38396:从交换设备里把头页面读入swap_header;这是一个在17516行定义的union
swap_ header联合体类型。
38400:检查一串特征字节序列,该序列记述了交换头部的版本信息,它是由
mkswap程序给出的。
38412:交换类型1。此时,该头页面被当作一个大的位映射图,每一位代表设备中
剩下的一个可用页面。同其它页面一样,头页面也是4K字节,即32K比特。由于每
一位表示一个页面,设备就可以拥有32768个页面,也就是每个设备总计128MB。(
实际上要稍小一些,因为头页面的最后10位用于签名,这样我们就不能假定它们对
应的80个页面也是可用的;另外头页面本身也不能用于交换。)如果实际设备比这
个值小,那么头页面中的一些位就不起作用。在38417行,函数进入循环来检查哪
些页面是可用的,并对它正在创建的swap_info_struct的lowest_bit、
highest_bit以及max成员进行设置。
    注意这个头页面位映射图不会永远被保持——当sys_swapon函数结束时它就会被
释放。内核利用交换映射表来跟踪正在使用的页面;该头页面位映射图仅被用来设
置lowest_bit和其它swap_info_struct结构体的成员。
38427:分配交换映射表并把所有使用计数值设置为0。
38440:交换类型为2的交换并没有减轻交换区容量的限制,不过它以一种更自然和
有效的方式贮存头部的信息。在此情形之下,swap_header的info成员就包含了
sys_swapon函数所需的信息。
38451:新的交换头部版本不需要sys_swapon函数把头页面当作一个位映射图来计
算lowest_bit、highest_bit,和max的值——lowest_bit总是1,另外两个值可以
从明确储存在头部的信息在定长时间内计算出来。这要比执行32768次位测试的循
环快的多也简单的多,而且后者的定义语句甚至比前者要多出两倍以上!尽管如此
,这部分以及余下的工作从概念上讲还是与以前十分相似的;sys_swapon函数只不
过是从交换头部直接获取了它所需要的大部分信息,而无须在计算它们而已。
    读者现在可以看出我刚才撒的一个小谎;版本类型为2的交换实际上真正克服了交
换区容量的限制。在这个版本中,文件末尾的80个页面不会由于交换头部签名而不
可利用,因此单独一个设备有可以有320K用于交换。不过上限仍然是大概128MB。

38491:sys_swapon函数忽略读取头部。它把设备交换映射表的第一个元素设置为
SWAP_ MAP_BAD(17552行)以避免内核在头页面上进行交换。
38492:分配加锁映射表并清零。
38499:更新可用的交换页面总数,并对此结果显示一个消息。(在38502行,从移
位计数器里减去10以便输出结果是千字节表示,210就是1K。)
38505:在交换设备的逻辑列表中插入新元素,仍遵循优先级排序的顺序。这里的
代码从功能上是与sys_swapoff函数中相应的代码一样的,所以没理由把它们分离
开来。一个能代替两者的内嵌函数就能简单的解决问题。
38519:进行清理工作,然后结束。
内存映射mmap
mmap是一个重要的系统调用,它允许为不同目的而设置专用的独享内存区域。该内
存可能是一个文件或其它特别对象的代理,在这种情形中,内核将保持内存区域和
潜在对象的一致,或者该内存可能是为一个应用程序所需要的简单的无格式内存。
(应用程序通常不使用mmap来分配无格式内存区,因为此时malloc更符合其目的。

mmap最普遍的使用方法之一是为内核本身通过内存映射(memory-map)形成一个可
执行文件(参见8323行的一个例子)。这是关于二进制处理程序如何同分页机制协
同工作以提供所需要分页的可执行体,这正如本章早些时候所暗示的。可执行体通
过mmap被映射为进程内存空间中的适当区域,然后do_page_fault函数调入执行体
所需的剩余页面。
被mmap分配的内存可能被标识为可执行,其中充满了指令代码,随后系统跳入其中
开始执行;这正是Java Just-In-Time(JIT)编译器的工作方式。更简单的说,可
执行文件能够被直接映射成一个正在运行的进程的内存空间;这项技术用于动态连
接库的执行中。
执行mmap功能的内核函数是do_ mmap。
do_ mmap
33240:do_ mmap函数具有几个参数;它们共同定义应在内存中映射的文件或设备
,并决定将被创建的内存区域的首选地址及其它特性。
33252:TASK_SIZE和在10867行定义的PAGE_OFFSET值相同——即是0xc0000000或
3GB。这是用户进程所能拥有的最大内存,在此基础上代码才有意义:显然,如果
要求do_ mmap函数分配大于3GB的内存,或者在addr之后的3GB内存空间没有足够的
空间,分配请求就必须被放弃。
33275:如果file为NULL,do_ mmap函数将被请求去执行匿名映射(anonymous
mapping)操作,这是一种并不与任何一个文件或其它特别对象连接的映射过程。
否则,映射将被关联到一个文件,接着do_ mmap函数要继续检查为内存区域设置的
标志位是否与用户在文件上允许执行的操作相兼容。举例来说,在33278行,函数
要确保是否内存区可写,因为文件已经被打开并执行写操作了。省略这项判断将可
能使文件打开时所作的检查发生混乱。
33307:允许调用程序强调do_ mmap函数应该或者在要求的地址上提供映射操作,
或者根本没有什么也不做。如果提供地址,do_ mmap函数只需保证提供的地址从一
个页面的边界开始。否则,它将获得在addr处或之后的第一个可用地址(通过调用
开始于33432行的get_unmapped_area函数),然后就使用这个地址。
33323:创建一个VMA并对其进行填写。
33333:如果内存映射着一个可读文件,则内存区域就被设为可读、可写和可执行
。(do_ mmap函数可以很快的取消写许可——这只是假定)另外,如果要求共享该
内存区域,那么现在就可以满足该请求。
33347:若文件不可写,则内存区域也必须不可写。
33351:在此情形中,没有这样的文件,使得do_ mmap函数必须与该文件的打开模
式和许可权限相一致——就允许函数自由运行。因此,函数把内存区域设为可读、
可写和可执行的。
33361:在地址范围建立时,利用do_munmap(很快就会被讨论到)来清除任何旧的
内存映射。因为新的VMA还没有插入进程列表之中(只有do_ mmap函数当前知道它
的存在),所以新VMA不会被此次调用影响。
33406:不会再有错误发生。do_ mmap函数把新VMA插入进程的VMA列表(或是它的
AVL树),合并所有新近相连的段片(接下来会对merge_segments函数进行讨论)
,更新一些统计数字,并返回新映射的地址。
Merge_segments
33892:merge_segments函数是一个有趣的函数,它把相邻的VMA合并成单独的一个
大范围的VMA。换句话说,如果一个VMA所覆盖(有意这样设计)范围是从0x100到
0x200,而另一个VMA的覆盖范围是从0x200到0x300,并且两者保护信息相同,那么
merge_segments函数就会用一个覆盖范围从0x100到0x300的单独VMA来代替它们。
(注意函数名中的“segments”并不暗示此时我们采用CPU分段机制。)
    merge_segments函数的参数是结构体struct mm_struct,它包含了我们该兴趣的
VMA以及可能进行合并的开始地址和终止地址。
33897:find_vma_prev函数将其vm_end定位在给定的start_addr之后的第一个VMA
上——由此,第一个VMA可能会包括start_addr。回忆一下find_vma_prev函数,它
也返回一个指向前一个VMA的指针prevl(如果第一个VMA满足条件则该返回值是
NULL)。
33911:进入处理所有覆盖给定区间的VMA的循环。在该循环过程中,
merge_segments函数将尝试把每一个段片都与其前一个段片进行合并,而前一个段
片的值可以通过prev获得。
33921:绝大部分条件判断都是相对直截了当的,不过最后一个测试就不这么简捷
了。它确保prev和mpnt是连续的——也就是在prev的结尾和mpnt的开头之间没有未
被映射的内存。即使检测结果是一个的vm_end和另一个的vm_start相等,这两块区
域在这一点上也未必一定相互覆盖——回忆一下,vm_end是要比VMA拥有的最后地
址还要大一位的。从33926行到33932行的代码为被映射文件和共享内存坚持了同样
的特性:一块区域的末尾要等于下一块的开头。
33937:merge_segments函数找到了可以合并的VMA。它把mpnt从VMA列表(还可能
是AVL树)里移出,再将它存入prev。要注意的是即使VMA的数目降到了
MIN_MAP_COUNT以下,它都不会拆除AVL树。
33948:如果将要消失的VMA是一个被内存映射的文件的一部分,merge_segments函
数就删除它对该文件的引用。
do_munmap
33689:do_munmap函数明显是do_mmap函数的反作用函数;它从一个进程的内存空
间里废除虚拟内存映射。
33695:如果do_munmap函数被要求取消映射的地址不是页面对准的,或者地址区域
位于进程的内存空间之外,那么很明显它就是无效的,因此请求就会被拒绝。
33699:如果连一个页面也没有被释放,就拒绝尝试。
33707:查找包括给定地址的VMA。令人奇怪的是,do_munmap函数返回的是0——而
不是错误——如果地址不在任何一个VMA之内的话。从某种意义上讲,这是正确无
误的;do_munmap函数被要求用来确保一个进程不再对特定内存区域进行映射,如
果一开始就没有这种映射的话,那就很容易办到。不过这仍颇为奇怪;在调用者看
来这是一个错误而且do_munmap函数也应该报告这个错误。然而,某些调用程序却
希望它如33361行的示例那样执行工作。
33717:如果给出的内存区域整个在单独的一个VMA中,但又不在该VMA的一端,那
么移去这段区域就会在封闭的VMA里生成一个空洞。内核是不会容忍这个空洞的,
因为按照定义,VMA应该是连续的一段内存。因此在这种情况之下,do_munmap函数
就需要创建另一个VMA,使得空洞的两边各有一个VMA。尽管如此,如果内核已经为
该进程创建了所允许的所有VMA,那么函数就不能这样做了,所以此时do_munmap函
数不能满足请求。
33730:标识所有与该区域相交迭或在区域里的VMA为空闲状态,同时把每一个都放
在本地堆栈free里。顺着这个过程,do_munmap函数会把VMA从它们的AVL树中删除
,如果有的话。
33743:do_munmap函数已经建造了要释放的VMA堆栈,现在释放它们。
33748:计算要释放的准确范围,要牢牢记住的是这个范围可能不能以完整的VMA来
度量。假如为min和max的定义适当,这三行可以被写成如下代码:
   
    st  = max ( mpnt -> vm_start, addr );
    end = min ( mpnt -> vm_end,  addr + len );

    由此,st是do_munmap函数实际开始释放区域的开头,end是该区域的结尾。
33765:如果VMA是共享映射的一部分,do_munmap函数通过调用remove_shared_vm_
struct(33140行)来断开mpnt与共享VMAs列表的链接。
33759:更新MMU数据结构,它对应于这个VMA里当前被释放掉的子区域。
33765:调用unmap_fixup函数来修补映射,我们接下来就会对这个函数进行研究。

33773:do_munmap函数已经释放了该范围内由VMA代表的所有映射;最后重要的一
步就是要为同一区域释放页表,这是通过调用free_pgtables(33645行)实现的。

Unmap_fixup
33578:unmap_fixup函数修复给定VMA的映射,这可以或者通过对一端进行调整,
或者通过在中间制造一个空洞,再或者通过把VMA完全删除的方法来完成。
33590:第一种情况比较简单:去掉整个区间的映射。do_munmap函数仅仅需要关闭
底下的文件或其它对象即可,如果它们有的话。读者可以看到,这无须把VMA本身
从current->mm里移出;它已经被调用者删除了。因为VMA的全部范围将被解除映射
,没有什么要向后推移的,所以unmap_fixup函数就此返回。
33599:接下来的两种情况处理把VMA从开头到末尾一块区间移去的问题。这也是比
较简单的;它们的主要工作是要调整VMA的vm_start或vm_end成员。
33608:这是四种情况中最有意思的一种——从一个VMA的中间移去一块区域,从而
会产生一个空洞。函数先开始要复制一份额外生成的VMA的本地拷贝,然后通过将
*extra设置为NULL来通知调用程序该附加VMA已被使用。
33611:图8-4表示了分裂VMA的过程。大部分信息被直接从旧VMA复制到了新VMA,
在此之后,unmap_fixup函数对两个VMA的范围都作了调整以解决空洞问题。原先的
VMA,area,被缩小到了表示低于空洞的子区域,而mpnt则表示高于空洞的子区域

33626:把全部新子区域插入current->mm。
33629:在除了第一种的其它情况里,unmap_fixup函数保持了旧的VMA。它缩小了
,但还未消失,因此它将被插回到VMA的current->mm集合中。


                                   



                                    






                  图 8.4分裂VMA
用户空间和内核空间
动态内存
用户任务和内核本身都经常需要快速分配内存。C程序一般使用著名的malloc和
free函数来完成这项工作;内核也有它自己类似的机制。当然,内核必须至少提供
支持C语言的malloc和free函数的低级操作。
在Linux平台上,就像其它的Unix变种一样,一个进程的数据区分为两个便于使用
的部分,即栈(stack)和堆(heap)。为了避免这两个部分冲突,栈从(准确的
是接近)可用地址空间的顶端开始并向下扩展,而堆从紧靠代码段上方开始并向上
扩展。虽然可以使用mmap在堆和栈之间分配内存,但是这部分空间通常是没有使用
的内存的空白地带。
即使不去研究有关的内核代码(不过我们还是要继续这项工作),读者也能对这些
地址区间所处位置有相当好的了解。下面的短程序显示了几个挑选出来的对象的地
址,它们分处于三种不同内存区域之内。由于种种理由,我们不能保证它可以被移
植到所有平台上,不过它可以在Linux的任何版本下工作,而且也应该可以被移植
到你所尝试的大部分其它平台上。
P515-1 代码
在我的系统上,我得到了如下的数字。你的结果可能会稍有不同,除了所使用的编
译器标志外,它还取决于你的内核及gcc的版本。即使不完全相同,它们也应该与
下面结果相当接近。
P515-2 代码
从这里你不难看出,如果使用大概的数字的话,栈从接近0xC0000000处开始并向下
生长,代码从0x8000000处开始,而堆则如前所述从临近代码上部的地方开始并向
上扩展。
Brk
系统调用brk是一个在C库函数malloc和free底层的原语操作。进程的brk值是一个
位于进程堆空间和它的堆、栈中间未映射区域之间的转折点。从另一个角度看,它
就是进程的最高有效堆地址。
堆位于代码段顶端和brk之间。如果brk底下的可用自由空间不够满足请求,C库函
数malloc就抬高brk;如果被释放的空间位于brk之下,就降低brk。顺便说一句,
Linux是我所知道的唯一的在使用free函数时真正的减少进程内存空间的Unix变体
;其它我所经历过的所有Unix商业版本实际上都是保留该进程的空间的——显然这
是“以防万一”的作法。(其它Unix的自由版本可能同Linux一样,不过我没有使
用过。)另外,对于大量的分配工作,GNU的C库使用mmap和munmap系统调用来执行
malloc和free。
代码、数据,以及栈的关系如图8-5所示。














                     图8.5 代码、数据和栈

Sys_brk
33155:实现brk的函数是sys_brk。它可以修改进程的brk值,还可以返回一个新值
。如果无法修改brk的值,返回的brk值就等于其原值。
33177:如果brk的新值位于代码区域之中,它就明显偏低而必须被抛弃。
33179:通过使用宏PAGE_ALIGN(10842行)把brk参数向上取整到地址更高的下一
个页面。
33180:按页对准进程原有的brk值。这看起来有些多余,因为如果进程的brk只是
在这里被设置,它就一定是按页排列的。但是在初始化一个进程的时候,进程的
brk可以被设置在别的地方,代码并不会把它按页对准排列。不管进程的brk在哪里
被设置,把它按页对准都可能会快一些;允许内核在这里跳过一次页对准操作,而
且由于此处要比别的地方更频繁的对进程的brk进行设置,它应该不会降低执行效
率而且还会少许提高。
33185:brk被降低了,不过还没有进入代码区域,因此尝试被允许。
33192:如果堆的大小有限制,它就要被考虑。图8-5清楚的表明,brk -
mm->end_code是堆的大小。
33197:如果brk扩展到了已被一个VMA所内存映射的(mmapped)区域,它就是不可
利用的,因此这个新brk值要被舍弃。
33201:最后一项必要的检查是察看是否存在足够的自由页面用于空间分配。
33205:使用do_mmap函数(33240行)为新区域分配空间。然后,sys_brk函数更新
进程的brk的位置并返回新值。
Vmalloc和vfree
内核编程中一个有趣的方面是并没有像应用程序编程人员通常所想当然的那样能够
得到很多服务。就拿malloc和free作为例子,它们就是建立在一个内核原语brk之
上的C库函数。
假使内核被修订以使其可以和标准C库连接,并使用它的函数malloc和free,那么
最终结果将是既笨拙又缓慢——这些函数被要求从用户模式调用,所以内核将不得
不切换到用户模式去调用它们,然后它们又不得不掉转回到内核,还必须要对整个
过程进行监控,等等。为了避免这一切,内核有许多十分熟悉的函数的自己的版本
,它们包括malloc和free在内。
的确,内核提供了像malloc和free一样的两对独立的函数。第一对是kmalloc和
kfree,管理在内核段内分配的内存——这是真实地址已知的实际和物理内存块。
第二对是vmalloc和vfree,用于对内核使用的虚拟内存进行分配和释放。由
kmalloc返回的内存更适合于类似设备驱动的程序来使用,因为它在物理内存里而
且是物理连续的。不过,kmalloc要比vmalloc所能使用的资源少,因为vmalloc还
可以处理交换空间。
vmalloc和vfree的一部分也是通过kmalloc和kfree来实现的,因为它们需要一部分
不可交换的内存用于登记操作(bookkeeping)。kmalloc和kfree又依次使用
__get_free_ pages、free_pages,以及其它低级页面操作函数实现的。
在此我不对kmalloc和kfree进行解释,不过本书中提供了相关代码以供读者阅读(
分别见37043和37058行)。我将要讨论的是更有意思的函数vmalloc和vfree。
Vmalloc
38776:vmalloc函数拥有一个参数,即要分配的内存区域的大小。函数返回指向分
配区域的指针,如果无法分配就返回NULL。
    Vmalloc可以分配内存的虚拟地址范围是由常量VMALLOC_START(11081行)和
VMALLOC_END(11084行)决定的。VMALLOC_START从超过物理内存结束地址8MB的地
方开始,以便对任何在这一区域错误的内核内存访问进行截获,VMALLOC_END在接
近可能的最大32位地址4GB的地址处。除非你的系统拥有比我的系统多得多的物理
内存,否则这就意味着几乎整个CPU地址空间都潜在的可为Vmalloc所用。
38781:vmalloc函数首先把要求的区域大小向上取整到地址更高的下一个页面边界
,如果它不在一个页面的边界上的话。(PAGE_ALIGN宏在10842行定义。)如果最
终范围结果太小(0)或明显过大,则请求会被拒绝。
38784:利用get_vm_area来为size大小的块定位一段足够大的内存区域,这个函数
接下来会进行介绍。
38788:通过调用vmalloc_area_pages(38701行)保证能够建立页表映射。
38792:返回被分配的区域。
get_vm_area
38727:get_vm_area函数返回从VMALLOC_START到VMALLOC_END的一段自由内存区间
。通常这就是vmalloc函数的工作;它还被用于我未曾提及的其它少数场合。调用
程序有责任确保参数size是一个非零的页面大小的倍数值。
    vmalloc函数采用所谓的首次适应算法(first-fit algorithm),因为它返回一
个指向定位区域的指针,该区域是它所能找到的第一个满足请求的区域。除此而外
,还有最佳适应算法(best-fit algorithm),该算法选取足够满足需求的最小的
一块可用自由区域进行分配,以及最坏适应算法(worst-fit algorithm),该算
法总是分配最大的一块可用自由区间。每种分配方式都有优点和缺点,不过首次适
应算法在这里对要达到的目的来讲,就已经非常简单、快捷而且足以满足要求了。

38732:分配一个struct vm_struct来代表新的区域。被分配的区域用一个有序链
表,即vmlist(38578行)来维护,该链表是由struct vm_structs构成的。包括
struct vm_struct结构体的头文件被省略以节约空间,不过结构体的定义十分简单

   
struct vm_struct {
        unsigned long flags;
        void* addr;
        unsigned long size;
        struct vm_struct* next;
    };
   
    如图8-6所示,链表的每一个元素都与单独一块已分配了的内存块相关联。形象的
看起来,get_vm_area函数的任务就是在已分配的区域之间找出足够宽的间隔。


图8.6 VMLIST 列表

38737:沿着链表进行循环。循环的结果要么是找到一个足够大的自由区间,要么
是证明这样的区域不存在。它会先从VMALLOC_START开始尝试,然后挨个尝试紧随
着每块被分配区域之后的地址。
38746:链表为空或者循环发现了一个足够大的新块;无论哪种情形,现在addr都
是最小可用地址。填充新的struct vm_struct结构体,它将会被返回。
38747:给保留块增加一个页面的大小(x86平台上是4K),来捕获内核超出的内存
——可能的话还包括下一个更高地址块下方的内存。因为
回复

使用道具 举报

发表于 2003-10-27 09:46:20 | 显示全部楼层
good
回复

使用道具 举报

 楼主| 发表于 2003-10-27 10:01:04 | 显示全部楼层
Linux内核注释——第9章System Ⅴ IPC


Unix从开发的早期就提供了管道的机制,管道在同一机器的两个进程间的双向通信
方面工作的相当出色。后来,BSD(Berkeley Software Development)的Unix版本
又提供了通用的套接字socket,它用来在不同机器的两个进程之间进行通信(或者
是同一机器的)。
Unix System V版本增加了被视为一体的三个机制,现在它们被统称为System V
IPC。像管道一样,这些机制都可以用于同一机器上的进程间通信,不过与管道和
套接字不同的是,System V的IPC特性使得同一机器上的许多进程之间都可以互相
通信,而不是仅限于两个进程。而且,管道——不是套接字——还有一个更大的限
制就是两个通信中的进程必须相关。它们必须有一个建立管道的共同祖先进程——
通常情况下,一个进程是另一个的父进程,或者这两个都是为它们建立管道的父进
程的子进程。System V IPC像套接字一样使得进程间通信(IPC)不需要有共同的
继承关系,只需要一个经过协商的的协议。
组成System V IPC的三个进程间通信机制是:消息队列、信号量和共享内存。
消息队列
System V的消息队列(message queues)是进程之间互相发送消息的一种异步(
asynchronously)方式,在这种情形之下,发送方不必等待接收方检查它的消息—
—即在发送完消息后,发送方就可以从事其它工作了——而接收方也不必一直等待
消息 。对消息进行编码和解码是发送者和接受者进程的工作;消息队列的执行并
不会给它们特别的帮助。这就形成了一个实现起来相对比较简单的通用机制,尽管
是以增加应用程序的复杂度为代价来获得这种简明性的。
这里是一个可能发生在SMP机器上的简单的应用情景。运行在一个CPU上的调度程序
把工作请求发送到一个特定的消息队列上。工作请求可能以各种形式出现:用来破
译代码的一组密码、需要进行计算的在不规则图形里的象素范围、在一个原子系统
里要更新的一部分空间,或者诸如此类的任务。与此同时,工作者进程在其它CPU
上运行,只要它们空闲就从消息队列中检索消息,然后再把结果消息发送到另一个
消息队列上去。
这种体系结构很容易实现,而且假定选择好了每个消息中被请求工作的粒度,就能
极大的提高机器中CPU的利用效率。(还要注意的是,因为调度进程可能不用做许
多工作,所以调度进程的CPU上大部分时间也可以运行一个工作者进程。)以这种
方式,消息队列可以被用作是远程过程调用(RPC)的一种低级形式。
新消息总是加在队列的末尾,不过它们并不总是从排头移出;你将能够在本章中看
到,消息可以从队列的任何地方被移出。在某个方面,消息队列与语音邮件类似:
新消息总是在末尾,不过消息接收方可以从列表的中间接收(以及删除)消息。
消息队列概述
首先对消息队列进行介绍是因为它的实现最简单,不过它仍然体现出了几个所有三
种System V IPC机制都具有的共同结构特征。
给进程提供了四种与队列相关的系统调用:
?   msgget——一个不合时宜的名字:读者可能认为这会得到一个等待的消息。但实
际它不会。调用者提供一个消息队列键标(key),如果存在一个队列,msgget就
用该键标为它返回一个标识号,如果没有队列,就用它为一个新的消息队列返回一
个标识号。因此,msgget所得到的不是一个消息,而是唯一标识一个消息队列的标
识号。
?   msgsnd——向一个消息队列发送一条消息。
?   msgrcv——从一个消息队列中接受一条消息。
?   msgctl——在消息队列上执行一组管理操作——检索关于它的限制的信息(比如
队列所允许的最大消息数据量)、删除一个队列,等等。
Struct msg
15919:struct msg代表在队列中等待的一个消息。它有如下成员:
?   msg_next——指向队列中的下一个消息——当然假如这是最后一个消息就为
NULL。
?   msg_type——用户指定类型编码;它的使用在本章讨论消息如何被接收时再进行
分析。
?   msg_spot——指向消息内容的开头。读者后面将看到,为消息分配的空间总是紧
靠在struct msg的上边,因此msg_spot恰恰指向struct msg末尾之后的位置。
?   msg_stime——记录消息被发送的时间。因为消息以先进先出(FIFO)顺序保存
,所以队列中的消息拥有的msg_stime值就是单调非递减的。
?   msg_ts——记录消息的大小容量(“ts”是“text size”的缩写,尽管消息不
一定非要是人们可以读懂的文本)。一条消息的最大容量是MSGMAX,它在15902行
定义为4056字节。推测一下,这应该是4K(4096字节)减去一个struct msg的结果
。不过b只有20字节,因此还有另外的20字节有待说明。
Struct msqid_ds
15865:msqid_ds代表一个消息队列。它有如下成员:
?   msg_perm——说明哪一个进程可以读写该消息队列。
?   msg_first和msg_last——指向队列中的第一个和最后一个消息。
?   msg_stime和msg_rtime——分别记录消息被发送入队列的最后时间和消息从队列
中读出的最后时间。(一项挑战:什么时候队列中最后一条消息的msg_ stime成员
不等于队列本身的msg_stime成员?至少有两个答案,但是你所掌握的信息现在只
能得出一个——你将不得不仔细阅读代码以寻求另一个解答。)
?   msg_ctime——上一次队列改变的时间——它被创建的时间,或是上一次利用
msgctl系统调用设置参数被确信的时间。
?   wwait——等待写消息队列的进程队列。因为消息发送是异步的,通常进程把一
个消息写入消息队列后就可离开。但是,为了避免拒绝服务(denial-of-service
)的攻击,队列有一个最大容量——若没有这个限制,一个进程就可以不断的向一
个没有读者的队列发送消息,强迫内核为每个消息分配内存直至空间耗尽。因此,
当一个队列达到其最大容量时,想要发送消息给该队列的进程必须等待,直到队列
中有了空间容纳新的消息,或者发送消息的尝试被立刻拒绝为止(读者将看到,进
程能够选择它所希望的执行方式)。wwait队列保留那些决定等待的进程。
?   rwait——与之类似,消息通常可以从消息队列中立刻读出。但是如果没有正等
待被读的消息将怎么办呢?进程再次进行选择:它们要么立刻重获控制(用一个错
误代码表示读消息失败)要么进入休眠等待消息到来。
?   msg_cbytes——当前在队列中的所有消息的总字节总数。
?   msg_qnum——队列中消息的总数。对于能够进入队列的消息数目没有明确的限制
——这也是一个问题,本章随后还要进行解释。
?   msg_qbytes——队列中允许存储的所有消息的最大字节数;把msg_cbytes和
msg_qbytes进行比较来确定是否还有空间容纳新消息。msg_qbytes缺省为MAGMNB,
尽管这个限制可以被有适当权限的用户动态地增加。
MAGMNB在15904行定义为16384。有四个理由说明为什么这个界限被定的这样低。第
一,实际上,你通常不需要把太多的信息包括在一个给定的消息中,所以这个界限
并不是十分苛刻的。第二,如果消息发送方的速度远远领先于接收方,那么让消息
能多包含些信息可能也没有意义——它们还将是接收方要费些时间才能得到的一大
块数据。第三,这个每队列16K的界限可以与潜在的128个队列相乘,总计达2MB。

但是采用这个界限的主要原因还是为了避免先前提及的拒绝服务攻击。然而,没有
什么能防止应用程序发送长度为零的(也就是空的)消息。msg_qbytes不会被这样
的消息影响,而且仍然要给消息头分配内存,因此拒绝服务攻击仍然是可行的。解
决这个问题的一个方案是引入一个独立的、对允许进入队列的消息总数进行限制的
界限;另一个方案是从msg_qbytes中减去整个消息长度——包括消息头。再一种解
决方法当然是不允许有空消息,但这又将同兼容性相抵触。
?   msg_lspid和msg_lrpid——最后消息发送方和最后消息接收方的PID。
Msgque
20129:消息队列实现中的主要数据结构是msgque,一个指向struct msqid_ds的指
针数组。这些指针有一个是MSGMNI(15900行定义为128),它等于128个消息队列
的最大值。为什么不只是用一个的数组而要用一个指针数组呢?一个原因是为了节
省空间:替代一个128个56字节结构体的数组(7168字节,7K),msgque是一个
128个4字节指针的数组(512字节)。在正常情况下,当很少的消息队列投入使用
,这能够节约好几千字节的空间。在最坏的情况时,所有的消息队列都被分配了,
最大的消耗也只是512字节。唯一会引发的真正缺点是附加了一层间接转换,这意
味着速度要有少许损失。
    主要消息队列数结构之间的关系如图9.1所示。









图9.1 消息队列数据结构

Msg_init
20137:msg_init用于消息队列实现时变量的初始化。它的大部分都是不必要,因
为同样的变量已经在函数前面紧挨本段代码的声明中被初始化为同样的值了。
20141:然而这个把msgque的条目设置为IPC_UNUSED的循环是必要的。IPC_UNUSED
不在本书讨论之列,值为-1(能够更好的被映射为 void*);它代表一个没有使用
的消息队列。msgque条目可能接纳的其它特殊值是IPC_NOID(也不在本书讨论之列
)——这只是暂时的,也就是在消息队列被创建的时候。
Real_msgsnd
20149:real_msgsnd实现sys_msgsnd的实质内容,即msgsnd系统调用。这里和内核
的约定有一些偏差,该约定要在命名系统调用的“内脏函数 ”时使用一个“do_”
前缀。
    在20338行调用了real_msgsnd函数,在那里它处于lock_kernel/unlock_kernel函
数对之中。(那两个函数在第10章中讨论——基本上,每次只能有一个CPU对内核
加锁,这与SMP机器有关。)这是一种确保unlock_kernel得到执行的最佳方式——
否则,real_msgsnd复杂的流程控制将因需要在它退出时确保调用unlock_kernel而
变得更加复杂。
    正如读者已经熟悉的,内核大多使用返回代码变量和goto语句来解决这样的问题
。虽然它不能很好的适应每种情况,但是sys_msgsnd函数的方法更加清晰。例如,
当一个函数必须获得多项资源,其中一些只有在以前所有资源请求都成功地被满足
时才能提出请求,考虑这样可能引发什么样的后果。简单扩展的解决方法将需要大
量函数——就像下边代码段所描述的:
P523 —1
    很快,这样的代码将变得臃肿不堪,内核不这样做的原因就在于此。
20158:开始一系列条件判断。假如有了第一个测试,本行三项测试中的第二项就
是不必要的——任何不能通过第二项测试的消息同样也不能通过第一项测试。虽然
以后这种说法可能会不成立,假如MSGMAX的界限增加到足够高的话。(事实上,在
写作本书时,完全消除MSGMAX界限的工作已在开展之中了。)
20164:消息队列标识号对两段信息进行编码:与之对应的msgque元素的索引在低
端7位,一个序列编号(其作用随后就将讨论到)就位于紧挨这7位之前的16位里。
现在所需要全部的就是数组下标部分。
20166:如果指定的数组下标处没有消息队列,或者正在创建一个,那么就没有消
息可以进入队列。
20171:保存在消息队列中的序列编号必须和那个msgque参数里的编码相匹配。其
思想是:在正确的数组下标处有一个消息队列并不代表它就是调用者所需要的消息
队列。自从调用者引用一个队列之后,原先处于那个下标的消息队列可能已经被移
去了而且在同一下标处创建了一个新的消息队列。16位序列编号被周期性的增加,
所以在同一下标处的新队列将有一个和旧队列不同的序列编号。(除非正好先创建
了65535个其它的新队列,这是相当不可能的——或者是131071个其它的新队列,
这就更不可能了。本章随后将对其进行解释,实际情况并非这样简单。)不管怎样
,只要序列编号不匹配,real_msgsnd就返回一个EIDRM错误来指示调用者所需要的
消息队列已经被移出了。
20174:确保调用者有写消息队列的权限。类似的一个方法将在第11章详细介绍;
在这里,可以简单的把它看作是类似于Unix文件权限应用的一个方法。
20177:检查如果提供的消息被写入队列,是否会超过队列所允许的最大容量。接
下来一行代码再次检查同一件事,这显然是当代码从2.0系列的内核版本被转换过
来时留下的一个编辑疏漏。在两次检查之间,曾经有过一些能够释放队列中的部分
空间的代码。
20180:队列中没有空间。如果在msgflg里的IPC_NOWAIT位被设置了,这种情况发
生时调用者就不会等待,这样的结果是返回EAGAIN错误。
20182:进程将要进入休眠状态。real_msgsnd首先检查是否一条消息正在等待该进
程。如果存在等待消息的话,就会用进程的休眠被该消息所中断的方式来处理它(
进程可能已经休眠,就如随后所示的那样)。
20184:假如没有正在等待进程的信号,进程就进入休眠状态,直到有信号到达或
移出队列中的一条消息时它才被唤醒。当进程被唤醒之后,它将再次向读列写入。

20190:为消息队列头(struct msg)和消息体分配足够的空间——正如前面所说
,消息体将紧接在消息头后面存放。消息头的msg_spot直接指向该头部之后消息体
开始的地方。
20196:从用户空间复制消息体。
20202:再次检查消息队列的合法性。Msgque入口可能已经在20184行这个进程休眠
时被其它进程修改过了,因此直到通过检查之前不能认为msg是有效的指针。
    即便如此,这里看起来也有一个潜在的缺陷。如果在当前进程执行到这一步之前
,该消息队列已被删除而另一个消息队列被设置在同一个数组下标的地方那又将怎
样呢?在UP机器上是不会发生这种情况的,因为销毁消息队列的函数,freeque(
20440行),在销毁它之前将唤醒任何休眠于该队列的进程,而且在real_msgsnd完
成之前freeque不会继续进行(本章后面将对freeque进行分析)。然而,在SMP机
器上,这仍然是一个小小的隐患。
    假如发生这种情形,msgque[id]将不是IPC_UNUSED或IPC_NOID,但是msq指向的内
存已经被freeque释放了,因此在20203行将废除无效的指针引用。
20209:填写消息头,将其入队,并更新队列自己相应的统计值(比如消息的总共
大小)。注意只要有可能就推迟填写消息头的工作,所以假如在分配和当前阶段之
间检测到错误时,这样就不会浪费时间。
20226:唤醒所有等待消息到达这个队列的进程,然后返回0以示成功。
Real_msgrcv
20230:同real_msgsnd函数一样,real_msgrcv函数实现msgrcv系统调用。Msgtyp
参数含义灵活,这可以从在20248行开始的标题注释之中看出。Struct msg的
msg_type域在这里发挥作用:在该函数中它要与msgtyp参数相比较。
    另一个与real_msgsnd相同的地方是real_msgrcv函数也是从20349行的
lock_kernel /unlock_kernel函数对内调用的。
20239:从msgid提取msgque下标并确保在那个下标所指的空间中有合法的一项。
20262:这个if/else语句对从队列中选择一个消息。第一种情况最简单:它只需要
得到队列中的第一条消息,使得nmsg或者为NULL或者指向队列的第一个元素。
20266:msgtyp为正值,并且msgflg里的MSG_EXCEPT位(15862行)被设置。real_
  msgrcv函数沿着队列搜索第一个类型和msgtyp不匹配的项。
20272:msgtyp为正值,但是MSG_EXCEPT位未被设置。real_msgrcv函数沿着队列搜
索第一个类型和msgtyp匹配的项。
20279:msgtyp是负值。real_msgrcv函数用最小的msgtyp编号来搜索消息,如果最
小值比msgtyp的绝对值还要小的话。注意20281行在比较时使用<而不是<=,这样队
列中消息的选择就不再有利于第一个消息了。这样的结果不仅令人满意——尽量遵
循FIFO方式是一个好的策略——而且效率也稍有提高,因为这种方式减轻了赋值工
作。如果比较采用<=,每个连接(tie)都将意味一次赋值操作。
20287:此时,如果有消息满足给定的标准,nmsg就指向它。否则,nmsg就是NULL

20288:即使找到一个合适的消息,它也有可能不被返回。如果调用程序的缓冲没
有足够大的空间来容纳整个消息体,调用者通常会得到E2BIG错误。然而,假如
msgflg的MSG_NOERROR位(15860行)被设置,那么这个错误就不会被公布。(我找
不出什么理由可以让一个应用程序去设置MSG_NOERROR标志位,我也找不出任何一
个使用它的应用程序。)
20292:如果msgsz指定了多于消息体的字节数,real_msgrcv函数就把msgsz减少到
消息的实际大小。当程序执行过这里之后,msgsz就是应该被复制到调用者缓冲区
的字节数。
    虽然此处代码的更加传统的写法有时比较慢,不过平均起来还是要更快一些:
   
    if ( msgsz > nmsg -> msg_ts )
        msgsz = nmsg -> msg_ts;

20294:把选中的消息从队列中移出。队列是一个单向链表,不是双向链表,所以
当不是队列中第一个的消息被移出时,real_msgrcv函数必须先要在队列中进行循
环以寻找它的前趋队列节点。
    通过将队列转换为双向链接,前趋节点就能在恒定时间里被找到。这个改变将引
入空间损耗(需要额外的指针),时间损耗(用来更新附加的指针),以及复杂度
的提高(需要增加完成这些工作的代码)。尽管如此,那些代价都是很小的,而在
被移出的消息处于队列中部的情况下,它们可以显著地提高速度。
    不过实际情况中,大部分应用程序从队列中移出的都是第一个消息。其结果是,
额外花费在管理msg _prev 指针(假定我们这样称呼它们)上的时间通常被完全的
浪费了。只在从队列中间移出消息时它才会有所补偿,但应用程序又很少这样做。
结论是为了提高特殊情形时的速度而降低了普遍情况下的效率——这几乎总是一个
坏主意。甚至于确实要移出队列中间节点的应用程序也不会等很长时间,因为通常
消息队列很短,典型情况下最多也就是几十个消息而已,而且平均在循环进行到一
半时就能找到选择的消息了。
    因此,只有当消息队列有成百上千条消息而且应用程序又要移出队列中间的节点
时,应用程序才会经历一次明显的速度减慢过程。考虑到这种情况的罕见程度,开
发者的决定就是正确的。除此而外,如果一个应用程序真的陷入这种困境,而且它
的开发者又不顾一切的需要这额外一点点速度——那么好吧,这就是Linux,他们
可以自己修改内核源代码以满足需要。
20305:处理移去队列中唯一节点的情况。
20308:更新消息队列统计值。
20313:唤醒所有等待写入这个消息队列的进程——也就是所有被real_msgsnd函数
设置为休眠状态的进程。
20314:把消息复制到用户空间并释放队列节点(头部和体部)。
20318:返回正被返回的消息的容量大小——这对可变长度消息来说至关重要,因
为应用程序的消息格式可能无法说明消息在哪里结束。
20320:没有符合调用程序标准的消息。接下来发生的操作将取决于调用者:如果
调用者设置msgflg的IPC_NOWAIT位,那么real_msgrcv函数可以立刻返回一个失败
错误。
20323:否则,调用者宁愿在没有可用的消息时进入休眠状态。如果一个信号正等
待调用进程则返回EINTR错误;否则,调用者进入休眠状态直到一个信号到达或者
别的进程写队列为止。
20329:永远不会执行到这里,但是编译器并不知道这一点。所以,这儿有一个假
return语句,只是为了满足gcc的要求而已。
Sys_msgget
20412:因为sys_msgget的流程控制比sys_msgsnd和sys_msgrcv的要简单,所以就
没有必要把sys_msgget的所有实质操作转移到一个独立的辅助函数上。尽管它确实
有自己的辅助函数,本章随后还将对它进行分析。
20414:跟踪函数所需的返回值的ret不必初始化成-EPERM。Ret会在函数的每一分
支路径上被赋值,所以这一行的赋值就是多余的。然而,gcc的优化器的聪明程度
足以发现并消除这种无效赋值,因此这一点是没有意义的。
20418:特殊键值IPC_PRIVATE(未包括在内——它的值是0)表明调用者需要一个
新队列,无论是否有其它具有相同键值的消息队列存在。在这种情况下,通过使用
newque(20370行)能够立刻创建该队列,随后我们还将对newque进行详细讨论。

20420:否则,key唯一地标识出了调用者需要使用的消息队列。一般地,开发员选
择键值时或多或少带有随机性(或者给用户一种办法来选择一个)而且希望它不会
与任何运行中的应用程序的键值发生冲突。
    这可能听起来耸人听闻,但是临时文件名也存在同样的问题——你只能期望没有
其它应用程序选择了同样的命名方式。实际上,很少出现问题——key_t是int类型
的typedef,所以在32位机上有超过40亿个可能值,而在64位机上超过了9×1018
个!这个键值空间容量的巨大程度有助于降低偶然冲突的机率。而且对于消息队列
键值,或者对于文件,即使偶然发生冲突,一个授权方案也能进一步减小问题发生
的可能性。
    即便如此,难道我们不能做得更好吗?像标准C库函数tmpnam一样的函数可以极大
地帮助产生能够保证在系统范围内唯一的临时文件名,但是却没有类似的办法能够
产生一个消息队列键值而且保证它的唯一性。
    如果对这个问题进行进一步研究的话,这些因素看起来应该是两个不同的问题。
应用程序大体上并不关心临时文件的名字是什么,只要它不是正在使用的文件就可
以。但是应用程序一般需要提前知道应把消息发送到哪一个队列中。如果一个应用
程序动态的选择它的消息队列键值,那么它有时又莫名其妙地需要把被选择的键值
告诉其它应用程序。(等价的,它可以发送msgid来代替键值。)而且,假如被涉
及的应用程序已经有办法来像那样进行彼此之间的发送消息,那么它们还要消息队
列做什么?因此,这可能不是一个值得解决的问题。如果一个应用程序需要一个非
专有(nonprivate)队列的唯一键值,但是它对实际键值是什么并不太关心,那么
它就可以通过尝试键值1来得到一个(记住0就是IPC_PRIVATE)并且可以从那儿逐
步尝试直到成功为止——那只需少量的工作,尽管不太可能需要。
    无论如何,这一行使用findkey(20354行,后边讨论)来查找拥有给定键值的存
在着的一个消息队列。
20421:如果键值没有被使用,那么sys_msgget就可以创建它。如果IPC_CREAT位没
有被设置,则将返回ENOENT错误;否则,newque函数(20370行)创建队列。
20425:键值已被使用。如果调用者把IPC_CREAT和IPC_EXCL位都设置了,那么在这
种情况下调用者就希望能够产生一个错误,因此它就得到了一个。(这是为了故意
与open的O_CREAT和O_EXCL位恰好能够并列。)
    若不加考虑,很难分辨出如原文所写的if判断和下面的等价形式相比那个更快:
P526—1
    两种判断方式都检查是否那两个标志位都被设置了,但是,出于种种原因,你可
能会期望任意一个快于对方。然而,结果是gcc为这两者产生同样的代码,至少是
在优化编译的时候。(如果你对此有兴趣的话,它所选择的方案是把我建议的替代
品直接进行翻译的结果——它的转换方案在内核中看起来就好像是同时测试两个标
志位的代码。)这是一个相当棒的优化过程,而且也是我过去所没有期望得到的。

20428:否则,使用该健值,调用者将接受具有那个健值的存在着的队列。(这是
最普遍的情况。)如果在期望的地方没有消息队列(考虑到findkey的执行,那应
该是决不会出现的情况)或者调用者缺少访问它的权限许可,那么将返回一个错误

20434:序列编号和msgque下标被编码在返回值里。这将成为调用者要传递给
sys_msgsnd、sys_msgrcv,以及sys_msgctl的msgid参数。
    这种编码方案有两个重要特征。更明显的一个特征是如何把序列编号部分和数组
下标部分分离开来:因为id是一个索引msgque的数组下标,它只能具有最大到(但
不包括)msgque中含有元素的数目,即MSGMNI的值。通过把这个值与序列编号相乘
,就可以使低位空出来以保存id了——它很像是一种标准的MSGMNI算法。
    这里还需要注意的是返回值永远不会是负值——这一点是非常重要的,因为C库执
行时可能会把负的返回值当作是一个错误。因为当前值是128,所以数组下标占据
返回值的低端7位。序列编号是16位,因此只有ret的低23位可以被这次赋值设置成
1,而且所有高位应是0。特别地,符号位是0,所以ret是0或正值。
20437:不管ret被如何计算,它现在都将返回。
Sys_msgctl
20468:sys_msgctl函数无疑是消息队列实现中最大的一个函数。这部分上是因为
它要完成许多不同的功能——类似于ioctl函数,它是一个功能联系松散的函数聚
合体。(顺便要说明的是,不要因为此处的混乱而责备Linux的开发者们;他们只
是想要提供与System V那蹩脚的设计相一致的兼容性。)
    msqid参数指定了一个消息队列,cmd指出sys_msgctl函数应该对它如何操作。很
快读者就会看到,需不需要buf取决于cmd,而且即使当它被使用时它的含义也将随
情况的不同而不同。
20477:拒绝明显非法的参数。在不经常出现的,参数无效情况已经是不容置疑时
,在调用lock_kernel函数之前执行本操作能够挽救不必要的内核锁定。(当然,
流程控制将不得不相应作出调整——必须跳过lock_kernel函数)
20481:在IPC_INFO和MSG_INFO情况中,调用者需要有关消息队列实现的属性信息
。它可能要用这些信息来选择消息容量,比如说,在最大消息容量较大的机器上,
调用进程可以提高它自己在每个消息中发送的信息量界限。
    所有清晰地在消息队列实现中定义那些缺省界限的常数都是通过struct msginfo
(15888行)对象复制回来的。假如cmd是MSG_INFO而不是IPC_INFO时,还要包括一
些额外信息,读者可以在20495行看到这一点,不过这两种情况在其它方面是相同
的。
    注意一下调用程序的缓存buf,它被定义成了指向一种不同类型struct msqid_ds
的指针。不过没有关系。复制是由copy_to_user函数(13735行)完成的,它并不
关心它的参数的类型,尽管当被要求向一块不可访问的内存写入时该函数也会产生
错误。如果调用者提供了一个指向一块足够大空间的指针,sys_msgctl函数将把请
求的数据复制到那里;使得类型(或至少是容量)正确是取决于调用程序的。
20505:如果复制成功,sys_msgctl函数返回一个附加的信息段,即max_msqid。注
意这种情况完全忽略了msqid参数。这样做有重要的意义,因为它返回了有关消息
队列执行情况的总体信息,而不是某个特别的消息队列的具体信息。不过,就这种
情况下是否应该拒绝负的msqid值仍是一个各人看法不同的问题。不可否认的是,
即使没有使用msqid时也拒绝一个无效的msqid值一定能够简化代码。
20508:MSG_STAT请求内核对给定消息队列持续作出的统计性信息——它的当前和
最大容量、它的最近的读者和写者的PID,等等。
20512:如果msqid参数不合法,在给定位指处没有队列存在,或者调用者缺少访问
该队列的许可,则返回一个错误。因此,队列上的读许可不仅意味着是对入队消息
的读许可,而且也是对关于队列本身“元数据(metadata)”的读许可。
    顺便提及一下,要注意命令MSG_STAT假定msqid只是msgque下标,并不包括序列编
号。
20521:调用者通过了测试。sys_msgctl函数把请求的信息复制到一个临时变量中
,然后再把临时变量复制回调用者的缓存。
20533:返回“完全的”标识符——序列编号现在已经被编码在其中了(在20520行
完成)。
20535:还剩下三种情况:IPC_SET、IPC_STAT,和IPC_RMID。与读者迄今为止所见
的那些情况有所不同的是,那些情况都在switch语句里被完全的处理了,而剩余的
这三种在此仅进行部分处理。第一种情况,IPC_SET只要确保用户提供的缓冲区非
空,就将它复制到tbuf里以便后面函数的进一步处理。(注意拷贝操作之后在
20540行对err的赋值是不必要的——因为在它使用之前的20550行,err将被再次赋
值。)
20542:剩余三种情况中的第二种,IPC_STAT仅仅执行一次健全性检查——它的真
正工作还在后边的函数体中。最后一种情形,IPC_RMID在这个语句中不工作;它所
有的工作都推迟到后边的函数中完成。
20548:这段代码对所有剩余的情况都是共同的,而且大家现在都应该对它比较熟
悉了:它从msqid里提取出数组下标,确保在指定的下标处存在着一个有效的消息
队列,并验证序列编号的合法性。
20559:处理IPC_STAT命令的剩余部分。假如用户有从队列中读出的许可,
sys_msgctl函数就把统计信息复制进调用者的缓冲区里。如果你认为这与先前
MSG_STAT的情形非常类似,那你就是对的。这两者之间的唯一不同之处在于:正如
读者所见,MSG_STAT期望一个“不完全”的msqid,而IPC_STAT却期望一个“完全
”的msqid(就是说包括序列编号)。
20572:复制统计数据到用户空间。如果按照如下方式重写这三行代码,那么运行
速度或许稍快一些:
P527—1
    毕竟,对于写入用户空间来说成功要肯定比失败更为普遍。基于同样的原因,
MSG_STAT情况下(始于20530行)的相应的代码如果被重写成以下形式也可能更快

        P527—2
    或者,下边的一个甚至可能更快,因为没有一次多余的赋值操作:
        P528—1
    然而和直觉相反的是,我对所有这三种修改都作了测试,结果却发现是内核的版
本执行起来更快。这必然与gcc生成目标代码的方式有关:显然,我的版本中的一
条额外跳转要比内核版本的额外赋值所花费的代价高得多。(从C源代码来考虑额
外的跳转并不直观——你不得不考察gcc的汇编输出代码。)回想前边章节所讨论
过的,跳转会带来明显的性能损失,这是因为它们会使得CPU丧失其内在的并行性
所带来的好处。CPU的设计者们竭尽全力要避免分支造成的性能损失影响,不过很
明显,他们并不总是成功的。
    最终,对gcc优化器的进一步改善可能消除内核版本和我的代码之间的差别。每当
两种形式逻辑相同而一个较快时,假如gcc能够发现这种等价并为两者生成同样的
代码,那将非常令人愉快。不过这个问题是要比看上去难得多的。为了生成最快的
代码,gcc将需要能够猜测哪一次赋值最易发生——另一种情况则涉及了分支。(
对gcc的最近版本所作的工作已为这样的改进打下了基础。)
20576:在IPC_SET情形里,调用者需要设置消息队列的某些参数:它的最大容量、
属主,和模式(mode)。
20578:为了操纵消息队列的参数,调用者必须拥有该队列或者拥有CAP_SYS_ADMIN
权能(14092行)。权能已在第7章中讨论过。
20584:把消息队列中最大字节数的界限提高到正常限制以上,这就类似于提高任
何其它资源的硬界限一样,因此它也需要与之相同的权能,即CAP_SYS_RESOURCE(
14117行)。资源限制在第7章已经讨论过。
20587:调用者应该被允许执行该操作,所以被选择的参数根据调用者提供的tbuf
被设置。
20595:IPC_RMID意味着删除特定的队列——不是队列中的消息,而是队列本身。
假如调用者拥有该队列或者有CAP_SYS_ADMIN权能,这个队列就可以用freeque函数
调用(20440行)来释放。
20605:cmd最终不是经过验证的命令中的一条,所以调用程序得到EINVAL错误。在
这种情况下,在20548行所作的工作原本是可以避免的。假设我们要试图尽早检测
无效的cmd,通过删除switch语句里的default情况并把下列代码附加到函数第
20546行的第一个switch后:
        P528—2
    这样就会改变函数的行为状态。当调用者提供了一个无效cmd和一个无效msqid时
,它将得到一个与现在所得的不同的错误——有了这种改变之后,无效的cmd将先
于无效的msqid而被检查出来。虽然有关msgctl的文档并没有诺许任何一种行为,
但是这样我们就可以自由的来改变它。其结果能够少许提高这种无效cmd情形下的
速度。
    然而,要注意这种解决方案很不幸地需要在第一个switch开关处引入一个空的
IPC_RMID case。没有它,函数将错误的把IPC_RMID也当作一种无效cmd情况而抛弃
掉。这个额外的case减缓了cmd合法这种正常条件下的速度——虽然不很严重,但
情况的确如此。而且,正如你所知道的,用普遍情形的代价来换取特殊情形时速度
的提高从来就不是一个良好的解决办法。因此还是原来的方式更好。
Findkey
20354:findkey函数为sys_msgget系统调用(调用在第20420行)定位具有给定键
值的消息队列。
20359:开始对msgque里所有可能被占据的单元槽进行循环。max_msqid跟踪
msgque里被占据的最大数组元素;在这里使用到了它,并且在很快就要提到的
newque和freeque里将对它进行维护。若没有max_msqid,这个循环将需要在
msgque的所有MSGMNI个元素里反复进行,就算是只有前5个在使用也要如此。
20360:如果当前数组元素值是IPC_NOID,那么就会在那里创建一个消息队列。这
个消息队列可能具有正被搜寻的键值,所以findkey函数将等待该队列的创建工作
完成。(当20385行的kmalloc调用使进程休眠时就会进入这种状态。)
20362:如果该msgque的项目是未被使用的,那么它明显不具有匹配的键值。
20364:若匹配的键值被找到,相应的数组下标就被返回。
20367:如果循环结束仍未找到匹配的键值,就返回-1以示失败。
Newque
20370:newque函数定位一个没有使用的msgque项目,并尝试在那里创建一个新的
消息队列。
20376:循环msgque以查找未用的一项。如果找到了一项,就用IPC_NOID来标记它
,控制随之跳转到20383行的found标记处。
20381:如果循环结束却没有发现未用的项目,msgque就是满的。Newque返回
ENOSPC错误表示表里没有剩余的空间。
20384:分配一个struct msqid_ds来代表新的队列。
20387:如果分配失败,该msgque项目被设置回IPC_UNUSED标志。
20388:一旦发现有IPC_NOID就激活任何已经休眠的findkey。
20391:初始化新队列。
20404:如果这个队列被建立在msgque中原来最高的已用单元槽之后,newque就相
应的增加max_msqid。
20406:在msgque里建立新队列。
20408:唤醒每个可能一直在等待该队列初始化完成的findkey。
20409:返回序列编号和msgque的数组下标。(创建一组宏来处理此处的编码和随
后的解码不会有什么损害。)奇怪的是,没有在这里增加序列编号——它要由接下
来讨论的freeque来完成。如果读者考虑一下,这里的决定是有一定道理的。你并
不需要每个队列都有一个唯一的序列编号——你只是想让每次msgque元素被重用时
有一个不同的序列编号,以便数组下标和序列编号二者的组合(combination)不
可能重复而已。数组下标直到建立在该位置的队列被释放后才能重新使用,所以增
加序列编号的操作也可以推迟到那个时候。
    为了把这个含义说的更明确一些,一个序列编号是可以被两个msgque元素同时使
用的。
Freeque
20440:我们将以freeque函数来结束这次内核消息队列实现的讨论,它的作用是删
除一个队列并释放相应的msgque元素项。
20449:如果正在被释放的是最高的被使用项,freeque函数将尽可能地减低
max_msqid。循环之后,max_msqid将再次成为被使用的msgque项的最高下标值,或
者在所有元素项都没有使用时变成0。要注意的是如果max_msqid是0,则msgque要
么是空,要么就只有一个元素项。
20452:msgque数组的元素被标识成为未使用,尽管此时struct msqid_ds还没有被
释放(在msq里,freeque函数仍然有一个指向该struct msqid_ds的指针)。
20454:假如有某个进程正等待读出或写入这个队列,必须警告它们该队列即将消
失。这里的循环唤醒所有那些进程。每个正等着向该队列发送消息的进程将在第
20171行知道被改变了的序列编号;每个等待从该队列里读取消息的进程也将在第
20254行进行同样的工作。
20458:调用schedule函数(26686行,在第7章讨论过)来赋予被唤醒了的进程运
行的机会。有趣的是,被唤醒了的进程可能还没有得到CPU使用权——当前进程仍
然有最大的优先权。假如这种情况发生,新近被唤醒的进程将不会从各自的等待队
列中被移出;而freeque又会注意到这一点并继续重复以图再次唤醒进程。最终,
执行freeque的进程会因耗尽它的时间片而将(CPU)让出给其它进程。在考虑了这
一切之后,在调用之前明确设置当前进程的SCHED_YIELD标志(16202行)可能是更
好的方法,这样可以给其它进程更好的使用CPU的机会。
20460:没有被挂起的读者和写者,所以该队列和它的消息可以被安全的释放掉。

信号量
信号量(Semaphores)是一种对资源访问进行保护的方式。信号量在通常概念上的
模型是指一种发送信号的标志(名称由此而来),但是我认为更好的象征是一把钥
匙(key)。不要把它与我们已经讲过的整数类型的键值(key)搞混了——在这个
类比中,我所指的意思是你的前门钥匙。
在最简单的情况下,信号量只是悬挂在一扇锁着的门旁吊钩上单独的一把钥匙。为
了穿过这道门,你必须把钥匙从吊钩上拿下来;当你出来时再把钥匙重新放回吊钩
之上。如果你到达时钥匙不再那里,你就不得不等待它的拥有者把它放回原处——
假如你已决定要通过这道门,就必须这样。而作为另一种选择,如果无法立刻得到
钥匙,你也可以因没有耐心等待而就此放弃。
上边描述了某资源每次只能由一个实体(entity)来使用的情形;在这种只有一把
钥匙的情况下,信号量可以被看作是一个二元信号量(binary semaphore)。对于
每次可以被多个实体占用的资源而言,信号量可被看作是计数信号量(counted
semaphores)。这与前边一样,只不过是吊钩上悬挂了更多的钥匙而已。如果资源
同时可供四个用户使用(或者假如有四个等价的可用资源,它们基本上是相同的)
,那么就有四把钥匙。依次可自然的进行类推。
进程使用信号量来协调它们的动作。比如,假设你正在写一个程序而且想保证每次
在给定的机器上最多只有该程序的一个实例可运行。这方面的一个好例子是声音文
件播放器——可能你不会想让它同时播放多个声音文件,因为其结果将是令人烦恼
的一团糟。另一个例子是X服务器。当然偶尔也会有充分的理由使得在同一个机器
上同时运行多个X服务器,但是对于一个X服务器来说禁止这样做也是很合理的,至
少缺省的做法就是如此。
信号量提供了一种解决这个问题的方案。你的音响播放器、或X服务器,或是其它
任何程序都可以定义一个信号量,检查该信号量是否在使用,若没有使用则继续运
行。如果该信号量已被使用,则表明程序的另一个实例在运行之中——你的程序可
以等待信号量被释放(音响播放器可能的行为),只是放弃并退出(X服务器可能
的行为),或者暂时继续其它工作稍候再试信号量。顺便说一句,这样一种信号量
的用法由于显而易见的原因而通常被叫做相互排斥(mutual exclusion);它的通
用简称,互斥(mutex)将在内核源代码中反复出现。
锁文件是获得与二元信号量同样效果的更为普遍的一种方式,至少在某种情况下如
此。锁文件更易使用,而且锁文件的一些实现可工作在网络上;但是信号量则不行
。另一方面,锁文件在超出二元的情况时就不容易使用并推广了。但无论如何,锁
文件都超出了本书的范围。
信号量和消息队列二者的代码是如此相似以至于没有必要再讨论sem_init(20695
行)、findkey(20706行)、sys_semget(20770行)、newary(20722行),以及
freeary(20978行)了,因为它们几乎同它们所对应的消息队列部分是一样的。
Struct sem
16983:struct sem结构体代表一个单独的信号量。它有两个成员:
?   semval——如果是0或为正值,semval +1就是仍然挂在这个信号量吊钩上的钥匙
数目。若为负值,它的绝对值就比正等待访问它的进程数目大一。缺省的信号量是
二元的,但是它们也可以通过使用sys_semctl变为计数型的;信号量的最大值是
SEMVMX(在16971行定义为32767)。
?   Sempid——存储最后一个操作该信号量的进程的PID。
Struct semid_ds
16927:struct semid_ds与struct msqid_ds相对应:它跟踪所有关于单独一个信
号量以及在它上面所执行的一系列操作的信息。我们所感兴趣的、有别于struct
msqid_ds中的成员如下所述:
?   sem_base——指向一个struct sem数组——换句话说,指向一个信号量数组。如
同单独一个struct msqid_ds可以包含多个消息一样,一个struct semid_ds也可以
包含多个信号量——该数组中信号量总和被具有代表性地称为一个信号量集合(
semaphore set)。然而与消息队列不同的是,被一个struct semid_ds所跟踪的信
号量的数目并不在它的生存期里变化。数组的容量大小是固定的。
这些数组中一个的最大长度是SEMMSL,它在第16968行被定义为32。数组的实际长
度记录在struct semid_ds的sem_nsems成员中。
?   sem_pending——跟踪挂起的信号量操作组的一个队列。信号量操作一有可能就
立刻完成,正如读者所预期的那样,所以只有当操作必须等待时,这个队列才会增
加节点。此成员与struct msqid_ds的rwit和wwait成员大致等价。
?   sem_pending_last——跟踪上述同一队列的队尾。它并不直接指向最后一个节点
——它指向一个指向最后节点的指针,这将有利于稍微提高后面代码的速度(尽管
这给理解增加了难度)。(需要顺便提一下的是,我不知道为什么同样的思想没有
被应用于消息队列。)
?   sem_undo——当各个进程退出时所应该执行操作的一个队列。这将在后续章节中
讨论。
Struct sem_queue
16989:struct sem_queue结构体是单个struct semid_ds之上休眠着的操作队列中
的一个节点。它有如下成员:
?   next和prev——队列中的下一个和前一个节点。正如sem_pending_last一样,
prev是指向一个指向前面节点的指针的指针。读者将能够在本章后面章节中看到为
什么系统要这样做的原因。prev永远不会变成NULL;在退化的情况里,即当队列为
空时,prev指向next。
?   sleeper——当某进程必须等待完成一个信号量操作时使用的等待队列。等待队
列在第2章中介绍过。
?   undo——一个将要撤销由sops所暗示的操作的操作数组——用另一种方式表示它
就是sops的反转。
?   pid——尝试完成这个队列节点操作的进程的PID。
?   status——记录一个休眠进程被唤醒的过程。
?   sma——向后指向这个结构体struct所存在的sem_pending队列的struct
semid_ds。
?   sops——指向这个队列节点所代表的一个或多个操作的一个数组;它永远不为
NULL。这个队列节点所描述的工作目的是执行sops里所有的操作。
?   nsops——sops数组的长度。
?   alter——说明是否操作会影响信号量集合里的任何一个信号量。这个问题的回
答看起来总是肯定的,但是要记住等待信号量变成0(即成为可用)并不会影响信
号量本身。
Struct sembuf
16939:struct sembuf结构体表示在信号量上执行的单个操作。它有如下成员:
?   sem_num——是struct semid_ds的sem_base数组的数组下标,该数组由这种操作
适用的信号量构成。因为struct sembuf是struct sem_queue的一部分,而且
struct sem_queue知道它与哪一个struct semid_ds相关联,这样就从不会出现该
操作应使用哪一个struct semid_ds的信号量数组的疑惑了。在其它情况下,一个
struct sembuf数组与一个索引semary的下标组成一对,这也蕴含了一个信号量数
组。
?   sem_op——要执行的信号量操作。通常,它的值是-1、0,或1:-1表示获得(
procure)信号量(从吊钩上取走钥匙),1表示交出(vacate)信号量(把钥匙重
新放回到吊钩上),而0表示等待该信号量变成0。除了这些值以外的值也是有用的
,不过它们只是被翻译为获得或交出更多的信号量值而已——也就是说,取走或放
回吊钩上更多的钥匙。(这段文字里的“获得”和“交出”可能看起来有点儿怪—
—无须担心;这只是普通的信号量术语。)
?   sem_flg——可以修改操作执行方法的一个或多个标志位(在这个short里的每一
个位)。
这些数据结构之间的关系如图9.2所示。

图9.2 信号量数据结构

Struct sem_undo
17014:struct sem_undo含有足够的撤销单个信号量的操作的信息。当一个进程执
行信号量操作同时设置了SEM_UNDO标志位时就创建一个用来撤销该操作的struct
sem _undo。进程的struct sem_undo列表所包含的所有撤销操作在该进程退出时都
会执行。熟悉设计模式(design patterns)的读者可能发现这是命令模式(
Command pattern)的一个实例。
    这个特性保证不管进程如何退出,都将自动为它执行相应的清理工作——这样一
来,就不会意外的让其它进程空等一个永远也不会被释放的信号量了。(除非进程
获得信号量后陷入死循环之中,但是避免这个问题不是内核的工作——在这种情况
下,目的是要提供给进程正确的工作方法,而不是对其进行人工干预。)
    struct sem_undo有如下成员:
?   proc_next——指向固有进程struct sem_undo列表里的下一个struct sem_undo

?   id_next——指向与信号量集合相关联的struct sem_undo列表里的下一个struct
sem_undo。你所看到的是正确的,同一个struct sem_undo确实是同时存在两个不
同的列表之中。读者将在本章的后面看到这两者的作用。
?   semid——标识出这个struct sem_undo所归属的semary元素项。
?   semadj——一个调整器的数组,这些调节将使用在和这个struct sem_undo相关
联的信号量集合中的每个信号量上。这种结构所不知道的信号量在数组中有一个0
——并没有进行调整。
Sys_semop
21244:sys_semop函数实现了semop系统调用。消息队列代码中没有直接同
sys_semop函数对等的函数——它是sys_msgsnd、sys_msgrcv,或者同时是两者的
对应函数,这取决于你如何看待它。在任意一种情况下,它的工作都是在一个或多
个信号量上完成一种或多种操作。它将自动尝试完成所有操作(即无需中断)。假
如无法全部完成,它将不会执行其中的任何一项操作。
21254:同消息队列函数非常类似,这像是在比必要的时机稍微提前一些的时候就
锁住内核。加锁也应该可以被推迟到第21265行左右再执行。
21255:参数的健全性检查。特别注意nsops受到SEMOPM的限制,它是可以被立刻尝
试的信号量操作的最大数目。在第16970行它被定义为32。
21261:把请求的操作描述从用户空间复制到一个临时缓冲区,即sops中。
21265:确保在指定的数组位置存在一项。正如读者所见,同消息队列代码的
msgque对等的是semary(20688行)。还要注意的是数组下标和序列编号以与消息
队列代码相同的方式被打包进了semid参数。当然这里应用的常量稍有不同——
SEMMNI在第16967行定义为128(而巧合的是,MSGMNI也是一样的值)。
21272:开始一个遍历所有特定操作的循环。首先检查在操作中给出的信号量数目
是否超出范围,如果是的话就放弃它。但是令人奇怪的是,这里返回的失败信息是
EFBIG错误(意思是“文件太大”)而不是EINVAL错误(“非法参数”)。尽管这
也是符合文档规范的。
21275:记录设置了SEM_UNDO标志位的操作的数目。undos只是一个标志——重要的
是它是否为0——因此,当条件满足时给它赋值1(或任何非零值)将产生同样的效
果。不过,内核的版本更快一点。而且因为循环重复的循环次数最多是SEMOPM次,
undos就不可能被增加多次以至于回到原点再次变为0。
21277:接下来的几个测试更新两个局部标志:decrease和alter。它们分别用来跟
踪集合里的任何操作是否在减少某个信号量的值以及是否在修改一个信号量的值。
直到循环结束之后,alter才会在第21282行被计算出来——在循环里,它只是跟踪
是否有操作增加信号量的值;这个结果与后边decrease里的信息结合起来最终决定
是否发生了改变。
    要注意这里的代码没有检查是否组合在一起的操作将彼此抵消——可能一个操作
把某个信号量减1,而另一个操作又会把它加1。如果这仅有一个操作,那么从某种
意义上讲,decrease和alter标志的值将是很容易引起误解的。内核可以尝试着优
化这种情况(并得到实现同样内容的更加精致的版本),不过相比较于所花费的时
间和精力,这可能并不值得:一个愚蠢到执行这样一种奇怪得空操作的应用程序就
应该这么慢,而一个聪明的应用程序则不应该这么愚蠢。
21285:确保进程具有在信号量上执行特定操作的许可。如果alter为真,那么修改
信号量的进程就需要写许可;否则,它只是在等待一个或多个信号量的值变为0,
这样该进程就只需要读许可。
21291:包括某些撤销操作的一组操作。如果当前进程已经有了在退出时要在这个
信号量上执行的一组撤销操作,那么新的撤销操作的数据就应该合并到那一组中去
。这个循环查找存在的撤销操作集合,假如有,就使un指向它,若没有,则un为
NULL。
21295:进程还没有这个信号量集合的一个取消集(undo set),所以需要为它分
配一个新的。在读者已经在消息队列代码中熟悉了的一段编码的实践经验之后,为
撤销调节(semadj数组)分配的空间将被安排在紧靠struct sem_undo本身之后,
并作为同一分配的一部分。接着就填入struct sem_undo。
21311:在提供的操作集合里没有撤销操作,所以un被设置为NULL。
21313:调用try_atomic_semop(20838行,后边讨论)来尝试在单个的槽内执行所
有操作。如果有任何引起变化的操作,un即为非空;若失败,就需要利用它来在函
数返回之前取消任何已经完成的部分操作。
21315:try_atomic_semop返回0表示成功,负值表示错误。无论何种情况,控制流
程都向前跳转到第21359行。
21321:否则,try_atomic_semop返回一个正值。这表示此刻无法执行所有操作,
但是该进程希望等待且稍后再试。一个局部struct sem_queue将首先被填写。
21328:代表修改信号量操作的节点被放在队列的末尾;代表等待信号量值归0的操
作的节点位于队列的前边。在本章后边探究update_queue函数时(20900行)读者
将对这种做法的原因有所了解。
    注意在挂起操作的队列中放置了一个局部变量——这很不寻常;这样的数据结构
通常具有以堆形式分配(heap-allocated)的节点。在这种情况下这样做是安全的
,因为节点在函数返回之前将被从队列中移出;而上下文转换部分将负责剩下的工
作。或者进程也可先退出,此时由sem_exit(21379 行)来负责进行收尾工作。
21333:开始一个反复尝试执行这些操作的循环,仅当所有要求的操作都成功完成
或者发生一个错误时该循环才会退出。
21336:一直休眠到被一个信号中断或有某断点(point)被再次尝试为止。
21342:如果进程由于此刻具有成功的机会而被update_queue唤醒,则它重新尝试
进行该操作。
21358:把这个进程从等待修改信号量集合的进程队列中移出。
21360:假如这个操作的集合改变了队列,那么某个其它进程所等待的条件可能就
已经具备。Sys_semop调用update_queue来寻找并唤醒这样的进程。
Sys_semctl
21013:实现semctl系统调用的sys_semctl函数具有与sys_msgctl相似的许多共同
之处。相应的,这里的讨论只涉及那些感兴趣的不同点,比如在sys_msgctl里没有
对应部分的sys_semctl命令(command)。
21093:GETVAL、GETPID、GETNCNT、GETZCNT,以及SETVAL命令对单个信号量、而
不是信号量集合进行操作,所以在这些情形里提供的semnum参数必须首先进行范围
检查。若semnum在范围之内,curr就指向相应的信号量。
21115:几乎是同样的命令集——GETVAL、GETPID、GETNCNT,以及GETZCNT——涉
及了对关于信号量的一段信息进行的阅读和计算。这里就完成那些工作。注意
sempid成员的高位在第21116行被屏蔽掉了——通过后面的讨论你将知道这样做的
原因。
21121:GETALL命令请求这个信号量集合里所有信号量的值。和许多其它命令一样
,此命令的工作并非在一处全部完成;稍后读者将见到其余的命令。
21126:SETVAL命令把信号量的值设置成给定的值——当然,是在规定的限制内。
同样地,此时只完成部分工作——主要是范围检查。
21142:SETALL是SETVAL的一个普遍化结果;它设置集合中所有的信号量值。同
SETVAL类似,在此只完成诸如范围检查一类的准备工作。
21173:GETALL的剩余部分由此开始。
21175:确保进程有读取信号量值的许可。这里的许可检查与第21112行的相重复。

21177:把信号量值复制到局部数组sem_io里,然后再从那里将它们复制到用户空
间。
21183:SETVAL的剩余工作由此开始。
21187:因为信号量取得新值,所以任何有记录的为semnum信号量所进行的取消调
节操作都将变为无效。这个循环通过把它们设置成0以使它们失去作用。
21189:把信号量的值设置成调用者提供的值,并调用update_queue(20900行)来
唤醒那些等待该条件成立的进程。
21220:SETALL的主要部分由此开始。
21224:所有信号量的值都被设置成了调用者提供的值。
21226:与集合内各个信号量相关的所有取消调节操作都被设置成0。当信号量被设
置为它已经拥有的值时,这并没有什么特别的地方——它也不应该有什么特别之处
。如果调用程序需要为除了一个信号量之外的所有信号量都赋予新值,那么就不能
通过设置那个信号量为原值的方法来欺骗它。取而代之的做法是,必须要对不应改
变其值以外的集合中所有信号量施用SETVAL命令。
Sem_exit
21379:sem_exit函数在消息队列代码里没有对应函数。它实现进程在退出时所要
求自动执行的撤销操作。所以,它在进程退出时调用(23285行)。
21389:如果进程的semsleeping成员非空,那么以下二者必有其一成立:要么进程
正在某个sem_queue队列上处于休眠状态,要么它已经从该队列被移出但
semsleeping还未更新。假如是前者,进程将被从休眠队列里移出。
21395:开始遍历当前进程的struct sem_undo列表。轮流对每个条目进行分析然后
在循环的更新部分释放它们。
21397:如果对应于这个撤销结构体的信号量已被释放,就继续循环。struct
semundo的semid域可以被freeary设置成-1,本章随后将对其进行介绍。
21399:类似地,如果相应的semque项不再有效,则继续循环。
21406:与从消息队列中间移出一条消息的情形相当类似,这个循环遍历sma的
struct semundos列表以找寻将被移出的前一个节点。当找到时,sem_exit向前跳
转到第21413行的found标记处。
21411:如果在sma的列表里没有找到撤销结构体,那么就发生了错误。sem_exit显
示一条警告消息并停止外层循环。这种反应看来有点过激,因为可能会有更多的撤
销结构体能够依照这种处理方式进行释放。不应该因一个烂苹果就糟踏整整一桶苹
果。尽管这几乎是“不可能发生”的情形,仅当内核逻辑错误才会导致其发生。我
的推测是这样的,若检测到这样一个错误的话,剩下的数据就不再可信了。
21414:在sma的列表里找到了撤销结构体,unp就指向一个指向其前驱的指针。接
着把un从队列里移出。
21417:执行这个撤销结构体里对所有信号量的调节。
21427:像往常一样,调用update_queue以免被这个函数所执行的操作满足了唤醒
某个休眠进程的条件。
21429:所有的struct sem_undo都已经处理过了——或者在21412行就检测到了错
误并结束了循环。不管哪一种结果,当前进程的队列被设置成为NULL然后函数返回

Append_to_queue
20805:把q附加在sma的sem_pending队列之后。这里的实现很紧凑;通常类似如下
的形式:
P534_1
    真正的优点在于内核的实现方式避免了潜在的代价昂贵的分支。通过使得sem_
pending_last成为指向一个指向队列节点的指针的指针,而不仅仅是一个指向队列
节点的指针,可能会部分的提高执行的效率。
Prepend_to_queue
20812:把q附加在sma的sem_pending队列之前。由于sem_pending不是一个指针的
指针,这种实现就同前面考虑过的简单的实现一样具有相同的形式。
Remove_from_queue
20823:这是struct sem_queue队列上的最后一个原语操作,它把一个节点从队列
中移出。
20826:通过修改前一队列节点的next指针,部分解除q与队列的链接。
20828:如果有下一个节点,还要更新下一个节点的prev指针;或者假如这已经是
队列的最后一个节点,就使用sma->sem_pending_last。要注意的是没有非常清楚
的代码被用于移出队列中唯一的节点——假设你还没有发现原因的话,这就值得你
花些时间研究一下为什么这种情形下代码也可工作。
20831:把已移出节点的prev指针设置成NULL,以便第21350和21390行代码能有效
地发现该节点是否仍在队列之内。
Try_atomic_semop
20838:该函数上方的标题注释说明它被用于测试是否给定的操作集能被全部执行
。该注释没有说明这些操作是否能够被全部被执行,通常情况下它们是能够被执行
的。
20846:开始循环所有通过检查的操作并依次对其进行尝试执行。
20850:sem_op为0表示调用程序希望等待curr->semval变为0。因此,如果
curr->semval不是0,调用程序不得不阻塞(block),这意味着操作无法自动被执
行(由于这个进程被阻塞时要完成其它工作)。
20853:调用程序的PID被暂时保存在curr->sempid的低16位里;从前的PID现在被
移进高16位。
20854:curr->semval依照sem_op所要求的进行调整——还是临时性的。虽然该操
作的结果的范围在随后的代码段中进行了核查,但是不管是在这里还是在其调用者
中sem_op都未进行范围检查。由于sem_op的值过大或者过小都将造成semval回绕(
wrap around),这样将导致意想不到的结果。
20855:如果这条操作的SEM_UNDO标志位被设置了,就表示在进程退出时该操作应
当被自动取消,相应的撤销结构体会被更新。要注意这里假定un是非空的——确保
这一点是调用程序的责任。
20858:对新的semval进行范围检查。
20864:循环即将完成,所有操作都将成功完成。如果调用程序只想知道操作能否
成功,但此刻并不想执行它们,这些操作就可以马上被取消。否则,操作已经被执
行了,所以try_atomic_semop就继续执行下去并返回成功。
20874:当一个操作把semval增加得过大时,跳到out_of_range标记处。函数安排
返回ERANGE错误,并向前跳转到撤销代码。
20878:当进程因为它必须等待信号量归0或者操作不能立刻获得信号量而不得不等
待信号量时,程序将跳至would_block标记处。如果这种情形之下调用程序不愿等
待,就返回EAGAIN错误。否则,函数返回1表示调用者将需要休眠。
20884:在undo标记之后的代码取消所有从第20846行开始for循环里所作的工作。

20888:这一行代码显而易见的部分是用来把在第20853行暂存的值保存在
curr->sempid的低16位中。其隐含的部分是高16位(在此假定是32位平台)没有必
要被设置成0:C标准有意给予编译器用0或用符号位拷贝来填充空余位的自由。在
实际实现中,低级机器指令怎样能最快的工作,编译器就如何工作,结果有时是这
些操作的一种,而有时则是另一种。(C语言标准为什么不拘限于任何一种实现的
原因正在于此。)这样的结果时,高位可以是全0也可以是全1,这也正是在第
21116行里只有低16位被屏蔽的原因。
Update_queue
20900:update_queue函数在信号量的值发生改变时被调用。它完成那些此刻可以
成功(或者将要失败)的挂起操作,并把它们从挂起队列中移出。
20907:如果这个节点的status标志已经被前一次update_queue调用增加过了,那
么与该节点相关的进程就还没有机会把它自己从队列中移出。为了提供其它进程机
会来执行它的挂起操作并从队列脱离,函数返回。
20910:检查是否此刻能够完成当前一组挂起操作。q->alter是最后一个被通过的
参数,所以即将成功的变异(mutating)操作就会自动被取消。这是因为进程将要
继续亲自尝试这些操作,而它们是不应该被执行两次的。
20914:假设错误或者成功状态已经能够被判定(对立于需要继续等待),这个节
点就被从队列中移出,并且与它关联的进程也会被唤醒。否则,节点留在队列中以
便在将来某处被再次尝试。
20917:如果该操作集包括一些变异的操作,标志就被提高以便进程知道唤醒它是
由于现在能够成功了;进程将尝试那些操作并把自己从队列中移出。前边讨论过,
第21342行要对这个标志进行检查。
20920:函数现在返回,以便多个变异进程不会同时尝试进行它们的那些可能并不
互相兼容的改变。回忆一下,非变异的操作位于队列头部,而变异的操作是位于末
尾的。其结果是,所有的非变异进程(它们不会彼此干扰)被首先唤醒,然后最多
唤醒一个变异进程。
20922:否则,将产生一个错误。该错误代码被保存在q->status里,接着队列节点
被移出。
Count_semncnt
20938:count_semncnt函数从21117行被调用来实现sys_semctl内的GETNCNT命令。
它的工作是记录因等待获得信号量而阻塞的任务数目。
20949:这个循环用于执行在sma的挂起队列中等待着的每个任务中的每个挂起操作
。每当找到一个满足的操作时它就递增semncnt——该操作试图获得特定的以及没
有设置IPC_NOWAIT标志的信号量。
Count_semzcnt
20957:count_semzcnt函数在21119行被调用以实现sys_semctl内的GET  ZCNT命令
。它除了要对等待信号量归0的任务(也就是等待信号量变得可用的任务)进行计
数之外,它和count_semncnt函数几乎一样。因此唯一的区别就在第20970行,在那
里它使用等于0而不是小于0来进行测试。
共享内存
共享内存(shared memory)顾名思义就是:一块预留出的内存区域,而且一组进
程均可对其进行访问。因为它涉及IPC和内存管理两方面的内容,这部分讨论将融
合本章及第8章以前的材料进行分析。
截至目前为止,共享内存是本章要介绍的三种IPC机制里最快的一种,而且也是最
简单的一种——对于进程来说,获得共享内存后它和任何其它内存看起来都是一样
的。由一个进程对共享内存所作出的改变对所有其它进程都是立即可见的——它们
只需通过一个指向共享内存空间的指针来读取,然后就轻松的获得了结果。然而,
System V共享内存没有确保互斥的内置方案:一个进程可以向共享内存中的给定地
址写入而同时另一个进程从相同的地址读出,这会导致读者所看到的将是不一致的
数据。这个问题在SMP机器上非常明显,但是它也会发生在UP机器之上——举个例
子,假设正当把某个较大的结构写入共享内存空间时写者被转换出了上下文环境,
而读者又在写者完成操作之前读取了共享内存的时候。
这样的结果是,使用共享内存的进程必须努力确保读操作与写操作的严格分离(考
虑一下,写操作和写操作之间也是如此)。锁和原子操作的相关概念将在下一章详
细论述。但是读者已经了解了保证互斥访问共享内存区域的一种方法:使用信号量
。这种思想是一旦获得信号量就全速访问内存区域,工作一完成后就立即释放该信
号量。
共享内存在一些用到消息队列的情况下也具有同样的帮助作用——一个调度进程可
以把工作请求写入共享内存区域的一部分,同时工作者进程可以把结果写入另一部
分。这就意味着应用程序要预先为请求和结果空间限制界限,但这样的内存分配和
结果写入还是要比使用消息队列快。
对于每个进程来说共享内存区域不必看起来具有相同的地址。如果进程A和进程B都
在使用同一块共享内存区域,那么A可能看到它在一个地址,而B则可能会看它在另
一个地址。当然,共享内存区域中给定的一个页面将最多被映射为一个物理页面。
前一章介绍过的虚拟内存机制只需要为每个进程进行不同的逻辑地址转换即可。
在内核代码中,共享内存区域被称为段(segments),这正是有时会被误用于VMA
的一个术语。为了预先防止任何混淆,这是一个该术语的非正式用法;它与第8章
里讨论过的硬件增强的(MMU)段是不同的。为了避免这种说法所可能引起的迷惑
,我将继续使用区域(regions)这个术语。
共享内存代码从设计到实现都与消息队列及信号量的代码有一些相似之处。因此,
没有必要再介绍shm_init(21482行)和findkey(21493行)函数。出于同样的原
因,剩下的一些函数和数据结构的讨论也会相应缩短。
Struct shmid_ds
17042:多少有点打破了已经建立的模式,struct shmid_ds不是内核用来跟踪共享
内存区域的数据结构。取而代之的是,struct shmid_ds包含这种信息的绝大部分
,而剩下的信息则位于下边要介绍的struct shmid_kernel中。以下是struct
shmid_ds的那些与其对应对象所不同的成员:
?   shm_segsz——这块共享内存区域的大小尺寸,用字节(不是页面)度量。
?   shm_nattch——用典型的术语,是指“附属(attached)”到这块区域的任务数
目——换句话说,就是使用该共享内存区域的任务数。这个成员是一个参考计数(
reference count)。
?   shm_unused、shm_unused2和shm_unused3——从它们的名字就可推断,这些成员
不再用于实现之中;它们的唯一角色看来是为了保持该结构体大小的向后兼容性。
Struct shmid_kernel
17056:struct shmid_kernel用于分离“私有(private)”的共享内存相关信息
和“公有(public)”的信息。struct shmid_ds里那些对用户应用程序可见的部
分还保留在struct shmid _ds之内,而关系到内核的私有信息则位于struct
shmid_kernel之内。用户应用程序需要能够通过struct shmid_ds来进行shmctl系
统调用,所以它的定义必须对它们是可见的,但是内核私有实现的细节就不应该出
现在struct的定义之中。否则,改变内核的执行可能会中断应用程序。struct
shmid_kernel具有如下成员;
?   u——即struct shmid_ds,也就是数据的公共部分。
?   shm_npages——用页面数表示的共享内存区域的容量。它恰为shm_segsz成员除
以PAGE_SIZE(10791行)的结果。
?   shm_pages——用于跟踪这块共享内存区域页面分配的一个“页表”——“页表
”在这里加了引号,是因为它不是一个同前一章里一样真正的、硬件支持的页表。
不过它完成同样的工作。
?   attaches——代表各自进程对这块共享内存区域进行映射的VMA的一个链表。
VMA在第8章里已经介绍过。
Newseg
21511:是与newque和newary相对应的函数。它分配并初始化一个struct
shmid_kernel,然后把它安置在shm_segs数组之中。
21537:分配“页表”。它和紧随struct shmid_kernel之后的对这块内存空间进行
分配的另一个IPC代码一样,它们都是一个大的分配过程中的一部分。不过,
struct shmid_kernel是由kmalloc分配的(在不可交换的内核内存里),然而“页
表”是由vmalloc分配的(在可交换内存里)。
21546:以把页表填零为起点来初始化所有分配了的元素项。
Sys_shmget
21573:这个函数自然是对应于sys_msgget和sys_semget的。唯一新颖的特征是它
对进程struct mm_struct的信号量获取和释放过程。这是一个内核信号量,它与
System V信号量并不相同——内核信号量将在第10章介绍。
Killseg
21610:这个函数对应于freeque和freeary。它的代码也同那些函数的非常相似,
但是有几个特征值得注意。
21616:如果用一个未被占用的shm_segs元素的索引调用killseg函数,它就显示一
条警告并立刻返回。它的两个对应函数中都不存在相似的代码。
21629:如果元素项的shm_pages成员是NULL,那么就在某处有一个逻辑错误。
struct shmid_kernel要么是没有完全构建好,要么就是已经销毁但还没有被从数
组中删除,再或者就是某个类似的看起来“不可能发生”的情况发生了。
21635:释放为页表分配的页面。
21638:如果页表没有映射这个页面,在释放这一项时就无需执行什么操作。
21640:如果页面在物理内存里,则它将被释放回可用页面的缓冲池里,同时递减
驻留页面的数目。
21643:否则,页面位于交换空间,它将从那里被释放。
21648:释放页表本身。
Sys_shmctl
21654:这个函数明显是对应于sys_msgctl和sys_semctl的,而且和它们有许多共
同点。在此只介绍两个共享内存所特有的命令。
21733:SHM_UNLOCK命令是SHM_LOCK的反作用命令,case在第21742行。SHM_LOCK允
许拥有足够权能的进程锁住物理内存里的一整块区域,以防止它被交换出去。而
SHM_UNLOCK则对一块加锁区域进行解锁,使得其中的页面再次可以被用于交换。
    在这两个case里的工作看来不甚相似:它只是确定调用者有合适的权能、要被解
锁的区域当前是加锁的(或反之亦然),然后设置或者清除适当的模式位。但是这
就是所要完成的一切了——其效果会在shm_swap(22172行)中显现出来。
    注意有一个分离的权能用于加锁和解锁共享内存,即CAP_IPC_LOCK(14021行)。

Insert_attach
21823:这个短小的函数只是把一个VMA添加到附属于给定struct shmid_kernel的
VMA列表中。注意该VMA是添加到列表头部的——顺序并不重要,而且这样处理最为
简单。否则的话,attaches的头和尾都将不得不分别被进行跟踪。
Remove_attach
21833:这个函数自然是从附属于给定struct shmid_kernel的列表中移出一个VMA
。关于此函数的奇怪之处是它并不依赖于它的shp参数——该参数是一个指针,指
向存储在VMA列表第一个VMA里的shp的attaches列表,它位于第21829行,而且用于
更新列表的过程同样不考虑是否该VMA为列表里的第一项(如果它是,相应的
attaches也被更新)。
Sys_shmat
21898:这个函数实现了shmat系统调用,调用进程借助它可以同一个共享内存区域
建立联系。
21923:在一些熟悉的准备工作之后,sys_shmat开始对共享内存区域应出现在调用
进程内存空间中的地址进行计算。首先,它要检查调用者传过来的地址。如果它是
NULL,而且SHM_REMAP标志位也未被设置(参见21959行),那么请求必须被抛弃—
—NULL永远不可被读和写。
21929:调用者传入NULL作为目标地址,这意味着sys_shmat应该在该进程的内存空
间里选择一个地址。get_unmapped_area将提供一个候选的地址(33432行),顺便
需要提及的是该函数在前一章已经讨论过。如果它返回0(在所有内核支持的平台
上都等价于NULL),那么就是无法找到足够大的区域。
21932:如果候选的地址不是恰好在一个页面的边界上,它就会被向上舍入到更高
的下一个页面边界,然后用调整过的地址将原先的地址取而代之。
get_unmapped_area返回在给定地址上或超过它的第一个可用地址,因此假如上舍
入的地址是可用的,它将被采用。
    现在解释一下为什么地址要向上舍入而不是向下舍入(那样能够更快和更简单一
些):假如sys_shmat进行向下舍入而所得地址不可用,那么代码将陷入死循环。
下一次调用get_unmapped_area将从下舍入地址处向上搜索并返回到原先未经舍入
的地址处,而它将再次被向下舍入,发现不合适,又传送给get_unmapped_area…

    要注意在这里使用的是SHMLBA(11777行)而不是PAGE_SIZE(10791行)来决定地
址的适宜性。不过,正如你所见到的,SHMLBA恰好被定义为PAGE_SIZE,所以效果
是相同的。
    如果SHMLBA和PAGE_SIZE是一样的,那么二者又为什么要兼有呢?答案在于
SHMLBA在绝大多数平台上就是PAGE_SIZE,但并不是在所有平台上都是如此。在
MIPS上——CPU具有4K的PAGE_SIZE——Linux把SHMLBA定义为非常大的0x40000(
256K),其注释说明选择这样大的值是为了遵守基于MIPS机器的SGI应用程序二进
制接口(ABI——Application Binary Interface)。然而,MIPS ABI的版本2和3
却明确声明了SHMLBA的值“在符合标准的实现上是允许有所不同的”,所以不清楚
为什么内核开发人员认为256K的值是必要的。或许该值是非常早期的ABI版本所要
求的,但是我向回检查ABI一直到1.2版仍没有发现任何这样的要求。
    还有,在SPARC-64上,SHMLBA是PAGE_SIZE的两倍;不幸的是,这个区别没有在代
码中进行解释。
21936:否则,调用者传送一个建议的地址。如果有必要而且是被允许的,该地址
就被向下取整。
21945:确保从被选地址开始的大小为len的内存块在进程的允许内存空间之内。(
len已在几行之前计算出来,21913行。)当调用者提供候选地址时进行检查明显是
必要的,而且粗看起来当sys_shmat用get_unmapped_area来选择一个地址时进行检
查也是必要的。 尽管区域的大小已经被传递给它,get_unmapped_area还是要执行
一个相似的检查,struct shmid_ds的shm_segsz成员不必和len相同——len是
PAGE_SIZE的一个倍数,而shm_segsz则可以不是。
    不过,因为所有被get_unmapped_area使用的地址都是页对准的,所以传递给它的
区域大小是否是页面尺寸的倍数都不会影响它的计算。
21951:正如注释中所说明的,被选区域必须为进程的栈留出一些空间。这个缓冲
区间有四个页面——这个数字并没有什么特别之处,只要达到进程有足够的栈空间
的目的即可。在上一章中曾提到过如果某任务耗尽了它的栈,它将被杀死。综合考
虑起来,让单个系统调用失败可能要比让整个进程被无理的杀死更好一些——进程
可以从前者中逐渐恢复,但是后者却不行。
21959:SHM_REMAP(17075行)的主要作用在此体现:如果SHM_REMAP被设置了而且
调用者提供的区域已在使用,那么就没有错误,这是因为SHM_REMAP用于允许调用
者把一块共享内存区域映射到它自己的内存里——比如是一个全局缓冲区。如果这
个标志没有被设置,被选的地址就一定不能和进程已经拥有的任何内存相互重叠。

21971:如果调用者缺少使用这块内存区域的许可,系统调用失败。如果
SHM_RDONLY(只读)标志被提供,调用者只需要读许可;否则,调用者需要读许可
和写许可。
21991:填充新的VMA。特别注意它的vm_ops成员被初始化为指向shm_vm_ops(
21809行),就像在第8章里讨论过的一样。
22004:增加这块区域的引用计数,以便它不会被过早的销毁。
22005:调用shm_map(21844行)把共享内存页面映射到进程的内存空间里。如果
失败,它就会返回,同时递减引用计数,如果这是第一个和唯一的引用,那么还需
要销毁该区域,然后释放VMA,这样整个工作就结束了。
    注意即使这是最后一个引用,VMA也不必被释放;该区域也必须要用SHM_DEST标志
(17106行)来进行标记。SHM_DEST可以在由调用者来设置的标志位之中;它也可
以在后面sys_shmctl的IPC_RMID情况里被设置——参见21780行。以这样的方式,
一块共享内存区域可以比它所有的附属进程生存更长时间。出于同保留一个检查点
(checkpoint)文件会在某些情况下非常有用相类似的原因,这样的处理方式也是
有用的:你可能会有一个每晚都要运行几个小时的耗时进程,要把它的处理结果保
存在一个即使该进程当前工作完成之后仍然继续存在的共享内存区域。只要通过附
属到剩下的共享内存区域,它就能够恰好在下一个晚上从停下的地方重新开始。(
当然,由于共享内存区域——不同于文件——在计算机关闭后就会消失,所以这种
方案不适用于不能有丢失危险的工作。)
22014:添加到附属于这块区域的VMA列表中,然后更新一些关于每区域统计的数据

22019:返回在调用者空间里真正被选择的共享内存区域地址,然后成功地返回。

Shm_open
22028:shm_open函数像是sys_shmat的一个简化版本(21898行)。它把一个给定
的VMA附加到一个共享内存区域里。提供的VMA是从一个已经附属于目标区域的VMA
复制而来,所以这个VMA本身已经被正确填写了;shm_open函数的工作基本上只是
要完成附属连结。
    正如shm_open上方的注释所陈述的,这个函数是从do_fork(23953行)里被调用
的,该函数在第7章里已经介绍过。更准确的说,这个函数是在dup_mmap里(
23654行)第23692行被调用的。然后,dup_mmap在copy_mm里(23774行)的第
23801行被调用;而copy_mm又是在do_fork里的第24051行被调用的。
22033:从VMA的vm_pte成员里抽取shm_segs下标,然后确保该处有一合法项。注意
下标无需进行范围检查,这是因为和SHM_ID_MADK所进行的按位与操作(11757行)
已强迫它合乎范围了。
22040:添加VMA到区域里并更新区域的统计数字。
Shm_close
22050:shm_close明显是shm_open的反作用函数,它把一个VMA从它附属的共享内
存区域里分离出来。尽管在其它地方内核也可以调用VMA的close操作,但33821行
看来是能结束调用shm_close的唯一之处。这是exit_mmap的一部分(33802行),
而它又是被mmput(23764行)调用、mmput 被__exit_mm(23174行)调用,而
__exit_mm 又被do_exit(23267行)所调用,do_exit函数在第7章就已经讨论过。
要注意还有其它到达shm_close的路径,我们很快就会对其中之一进行介绍。
22056:从VMA的vm_pte成员里抽取shm_segs下标然后把该VMA分离出区域。出于和
shm_open同样的原因,下标不用进行范围检查。还要注意的是shm_close不检查是
否在指示的下标处存在一个合法的shm_segs项。读者已经看到,remove_attach不
依赖于它的shp参数,所以它对此并不关心。然而shm_close剩下部分将假定其它共
享内存代码被正确的使用和执行,所以这种“不可能发生”的情形真的是不可能发
生的。
22058:从共享内存区域分离VMA然后更新区域的统计数字。
22061:减少该区域的引用计数,如果可能的话还需要将其释放。
Sys_shmdt
22068:与sys_shmat相反,sys_shmdt函数把一个进程从一块共享内存区域里分离
出去。
22074:开始对所有代表进程内存的VMA进行循环处理。
22076:如果VMA代表一块共享内存区域(这可以通过检查它的vm_ops成员进行精巧
的测试),而且该VMA始于目标地址,就应解除该VMA的映射。
22079:do_munmap(33689行)调用unmap_fixup(33578行),它又间接的在
33592行调用shm_close。do_munmap和unmap_fixup都在第8章里介绍过。
回复

使用道具 举报

 楼主| 发表于 2003-10-27 10:03:08 | 显示全部楼层
Linux内核注释——第10章 对称多处理(SM

在全书的讨论过程中,我一直在忽略SMP代码,而倾向于把注意力集中在只涉及一
个处理器的相对简单的情况。现在已经到了重新访问读者已经熟悉的一些内容的时
候了,不过要从一个新的角度来审视它:当内核必须支持多于一个CPU的机器时将
发生什么?
在一般情况下,使用多于一个CPU来完成工作被称为并行处理(parallel
processing),它可以被想象成是一段频谱范围,分布式计算(distributed
computing)在其中一端,而对称多处理(SMP—symmetric multiprocessing)在
另一端。通常,当你沿着该频谱从分布式计算向SMP移动时,系统将变得更加紧密
耦合——在CPU之间共享更多的资源——而且更加均匀。在一个典型的分布式系统
中,每个CPU通常都至少拥有它自己的高速缓存和RAM。每个CPU还往往拥有自己的
磁盘、图形子系统、声卡,监视器等等。
在极端的情形下,分布式系统经常不外乎就是一组普通的计算机,虽然它们可能具
有完全不同的体系结构,但是都共同工作在某个网络之上——它们甚至不需要在同
一个LAN里。读者可能知道的一些有趣的分布式系统包括:Beowulf,它是对相当传
统而又极其强大的分布式系统的一个通用术语称谓;SETI@home,它通过利用上百
万台计算机来协助搜寻地外生命的证据,以及distributed.net,它是类似想法的
另一个实现,它主要关注于地球上产生的密码的破解。
SMP是并行处理的一个特殊情况,系统里所有CPU 都是相同的。举例来说,SMP就是
你共同支配两块80486或两块Pentium(具有相同的时钟速率)处理器,而不是一块
80486和一块Pentium,或者一块Pentium和一块PowerPC。在通常的用法中,SMP也
意味着所有CPU都是“在相同处境下的”——那就是它们都在同一个计算机里,通
过特殊用途的硬件进行彼此通信。
SMP系统通常是另一种平常的单一(single)计算机——只不过具有两个或更多的
CPU。因此,SMP系统除了CPU以外每样东西只有一个——一块图形卡、一个声音卡
,等等之类。诸如RAM和磁盘这样以及类似的资源都是为系统的CPU们所共享的。(
尽管现在SMP系统中每个CPU都拥有自己的高存缓存的情况已经变得愈发普遍了。)

分布式配置需要很少的或者甚至不需要来自内核的特殊支持;节点之间的协同是依
靠用户空间的应用程序或者诸如网络子系统之类未经修改的内核组件来处理的。但
是SMP在计算机系统内创建了一个不同的硬件配置,并由此需要特殊用途的内核支
持。比如,内核必须确保CPU在访问它们的共享资源时要相互合作——这是一个读
者在UP世界中所不曾遇到的问题。
SMP的逐渐普及主要是因为通过SMP所获得的性能的提高要比购买几台独立的机器再
把它们组合在一起更加便宜和简单,而且还因为它与等待下一代CPU面世相比要快
的多。
非对称多CPU的配置没有受到广泛支持,这是因为对称配置情况所需的硬件和软件
支持通常较为简单。不过,内核代码中平台无关的部分实际上并不特别关心是否
CPU是相同的——即,是否配置是真正对称的——尽管它也没有进行任何特殊处理
以支持非对称配置。例如,在非对称多处理系统中,调度程序应该更愿意在较快的
而不是较慢的CPU上运行进程,但是Linux内核没有对此进行区别。
谚语说得好,“天下没有白吃的午餐”。对于SMP,为提高的性能所付出的代价就
是内核复杂度的增加和协同开销的增加。CPU必须安排不互相干涉彼此的工作,但
是它们又不能在这种协同上花费太多时间以至于它们显著地耗费额外的CPU能力。

代码的SMP特定部分由于UP机器存在的缘故而被单独编译,所以仅仅因为有了SMP寄
存器是不会使UP寄存器慢下来的。这满足两条久经考验的原理:“为普遍情况进行
优化”(UP机器远比SMP机器普遍的多)以及“不为用不着的东西花钱”。
并行程序设计概念及其原语
具有两个CPU的SMP配置可能是最简单的并行配置,但就算是这最简单的配置也揭
开了未知问题的新领域——即使要两块相同的CPU在一起协调的工作,时常也都像
赶着猫去放牧一样困难。幸运的是,至少30年前以来,就在这个项目上作了大量和
非常熟悉的研究工作。(考虑到第一台电子数字计算机也只是在50年前建造的,那
这就是一段令人惊讶的相当长的时间了。)在分析对SMP的支持是如何影响内核代
码之前,对该支持所基于的若干理论性概念进行一番浏览将能够极大的简化这个问
题。
注意:并非所有这些信息都是针对SMP内核的。一些要讨论的问题甚至是由UP内核
上的并行程序设计所引起的,既要支持中断也要处理进程之间的交互。因此即使你
对SMP问题没有特别的兴趣,这部分的讨论也值得一看。
原子操作
在一个并行的环境里,某些动作必须以一种基本的原子方式(atomically)执行—
—即不可中断。这种操作必须是不可分割的,就象是原子曾经被认为的那样。
作为一个例子,考虑一下引用计数。如果你想要释放你所控制的一份共享资源并要
了解是否还有其它(进程)仍在使用它,你就会减少对该共享资源的计数值并把该
值与0进行对照测试。一个典型的动作顺序可能如下开始:
1.  CPU把当前计数值(假设是2)装载进它的一个寄存器里。
2.  CPU在它的寄存器里把这个值递减;现在它是1。
3.  CPU把新值(1)写回内存里。
4.  CPU推断出:因为该值是1,某个其它进程仍在使用着共享对象,所以它将不会
释放该对象。
对于UP,应不必在此考虑过多(除了某些情况)。但是对于SMP就是另一番景象了
:如果另一个CPU碰巧同时也在作同样的事情应如何处理呢?最坏的情形可能是这
样的:
1.  CPU A把当前计数值(2)装载进它的一个寄存器里。
2.  CPU B把当前计数值(2)装载进它的一个寄存器里。
3.  CPU A在它的寄存器里把这个值递减;现在它是1。
4.  CPU B在它的寄存器里把这个值递减;现在它是1。
5.  CPU A把新值(1)写回内存里。
6.  CPU B把新值(1)写回内存里。
7.  CPU A推断出:因为该值是1,某个其它进程仍在使用着共享对象,所以它将不
会释放该对象。
8.  CPU B推断出:因为该值是1,某个其它进程仍在使用着共享对象,所以它将不
会释放该对象。
内存里的引用计数值现在应该是0,然而它却是1。两个进程都去掉了它们对该共享
对象的引用,但是没有一个能够释放它。
这是一个有趣的失败,因为每个CPU都作了它所应该做的事情,尽管这样错误的结
果还是发生了。当然这个问题就在于CPU没有协调它们的动作行为——右手不知道
左手正在干什么。
你会怎样试图在软件中解决这个问题呢?从任何一个CPU的观点来看待它——比如
说是CPU A。需要通知CPU B它不应使用引用计数值,由于你想要递减该值,所以不
管怎样你最好改变某些CPU B所能见到的信息——也就是更新共享内存位置。举例
来说,你可以为此目的而开辟出某个内存位置,并且对此达成一致:若任何一个
CPU正试图减少引用计数它就包含一个1,如果不是它就为0。使用方法如下:
1.  CPU A从特殊内存位置出取出该值把它装载进它的一个寄存器里。
2.  CPU A检查它的寄存器里的值并发现它是0(如果不是,它再次尝试,重复直到
该寄存器为0为止。)
3.  CPU A把一个1写回特殊内存位置。
4.  CPU A访问受保护的引用计数值。
5.  CPU A把一个0写回特殊内存位置。
糟糕,令人不安的熟悉情况又出现了。以下所发生的问题仍然无法避免:
1.  CPU A从特殊内存位置出取出该值把它装载进它的一个寄存器里。
2.  CPU B从特殊内存位置出取出该值把它装载进它的一个寄存器里。
3.  CPU A检查它的寄存器里的值并发现它是0。
4.  CPU B检查它的寄存器里的值并发现它是0。
5.  CPU A把一个1写回特殊内存位置。
6.  CPU B把一个1写回特殊内存位置。
7.  CPU A访问受保护的引用计数值。
8.  CPU B访问受保护的引用计数值。
9.  CPU A把一个0写回特殊内存位置。
10. CPU B把一个0写回特殊内存位置。
好吧,或许可以再使用一个特殊内存位置来保护被期望保护初始内存位置的那个特
殊内存位置……。
面对这一点吧:我们在劫难逃。这种方案只会使问题向后再退一层,而不可能解决
它。最后,原子性不可能由软件单独保证——必须要有硬件的特殊帮助。
在x86平台上,lock指令正好能够提供这种帮助。(准确地说,lock是一个前缀而
非一个单独的指令,不过这种区别和我们的目的没有利害关系。)lock指令用于在
随后的指令执行期间锁住内存总线——至少是对目的内存地址。因为x86可以在内
存里直接减值,而无需明确的先把它读入一个寄存器中,这样对于执行一个减值原
子操作来说就是万事俱备了:lock内存总线然后立刻对该内存位置执行decl操作。

函数atomic_dec(10241行)正好为x86平台完成这样的工作。LOCK宏的SMP版本在
第10192行定义并扩展成lock指令。(在随后的两行定义的UP版本完全就是空的—
—单CPU不需要保护自己以防其它CPU的干扰,所以锁住内存总线将完全是在浪费时
间。)通过把LOCK宏放在内嵌编译指令的前边,随后的指令就会为SMP内核而被锁
定。如果CPU B在CPU A发挥作用时执行了atomic_dec函数,那么CPU B就会自动的
等待CPU A把锁移开。这样就能够成功了!
这样还只能说是差不多。最初的问题仍然没有被很好的解决。目标不仅是要自动递
减引用计数值,而且还要知道结果值是否是0。现在可以完成原子递减了,可是如
果另一个处理器在递减和结果测试之间又“偷偷的”进行了干预,那又怎么办呢?

幸运的是,解决这个部分问题不需要来自CPU的特殊目的的帮助。不管加锁还是未
锁,x86的decl指令总是会在结果为0时设置CPU的Zero标志位,而且这个标志位是
CPU私有的,所以其它CPU的所为是不可能在递减步骤和测试步骤之间影响到这个标
志位的。相应的,atomic_dec_and_test(10249行)如前完成一次加锁的递减,接
着依据CPU的Zero标志位来设置本地变量c。如果递减之后结果是0函数就返回非零
值(真)。
如同其它定义在一个文件里的函数一样,atomic_dec和atomic_dec_and_test都对
一个类型为atomic_t的(10205行)对象进行操作。就像LOCK,atomic_t对于UP和
SMP也有不同的定义方式——不同之处在于SMP情况里引入了volatile限定词,它指
示gcc不要对被标记的变量做某种假定(比如,不要假定它可以被安全的保存在一
个寄存器里)。
顺便提及一下,读者在这段代码里看到的垃圾代码__atomic_fool_gcc据报告已不
再需要了;它曾用于纠正在gcc的早期版本下代码生成里的一个故障。
Test-And-Set
经典的并行原语是test-and-set。test-and-set操作自动地从一个内存位置读取一
个值然后写入一个新值,并把旧值返回。典型的,该位置可以保存0或者1,而且
test-and-set所写的新值是1——因此是“设置(set)”。与test-and-set对等的
是test-and-clear,它是同样的操作除了写入的是0而不是1。一些test-and-set的
变体既能写入1也可以写入0,因此test-and-set和test-and-clear就能够成为一体
,只是操作数不同而已。
test-and-set原语足以实现任何其它并行安全的操作。(实际上,在某些CPU上
test-and-set是唯一被提供的此类原语。)比如,原本test-and-set是能够用于前
边的例子之中来保护引用计数值的。相似的方法以被尝试——从一个内存位置读取
一个值,检查它是否为0,如果是则写入一个1,然后继续访问受保护的值。这种尝
试的失败并不是因为它在逻辑上是不健全的,而是因为没有可行的方法使其自动完
成。假使有了一个原子的test-and-set,你就可以不通过使用lock来原子化decl的
方法而顺利通过了。
然而,test-and-set也有缺点:
?   它是一个低级的原语——在所有与它打交道时,其它原语都必须在它之上被执行

?   它并不经济——当机器测试该值并发现它已经是1了怎么办呢?这个值在内存里
不会被搞乱,因为只要用同样的值复写它即可。可事实是它已被设置就意味着其它
进程正在访问受保护的对象,所以还不能这样执行。额外需要的逻辑——测试并循
环——会浪费CPU时钟周期并使得程序变得更大一些(它还会浪费高速缓存里的空
间)。
x86的lock指令使高级指令更容易执行,但是你也可以在上执行原子test-and-set
操作。最直接的方式是把lock和btsl指令(位test-and-set)联合起来使用。这种
方法要被本章后边介绍的自旋锁(spinlock)所用到。
另一种在x86上实现的方法是用它的xchg(exchange)指令,它能够被x86自动处理
,就好像它的前面有一个lock指令一样——只要它的一个操作数是在内存里。
xchg要比lock/  btsl组合更为普遍,因为它可以一次交换8、16,或者32位而不仅
仅是1位。除了一个在arch/i386/kernel/entry.S里的使用之外,内核对xchg指令
的使用都隐藏在xchg宏(13052行)之后,而它又是在函数__xchg(13061行)之上
实现的。这样是便于在平台相关的代码里内核代码也可以使用xchg宏;每种平台都
提供它自己对于该宏的等价的实现。
有趣的时,xchg宏是另一个宏,tas(test-and-set——13054行)的基础。然而,
内核代码的任何一个地方都没有用到这个宏。
内核有时候使用xchg宏来完成简单的test-and-set操作(尽管不必在锁变得可用之
前一直循环,如同第22770行),并把它用于其它目的(如同第27427行)。
信号量
第9章中讨论了信号量的基本概念并演示了它们在进程间通信中的用法。内核为达
自己的目的有其特有的信号量实现,它们被特别的称为是“内核信号量”。(在这
一章里,未经修饰的名词“信号量”应被理解为是“内核信号量”。)第9章里所
讨论的基本信号量的概念同样适用于内核信号量:允许一个可访问某资源用户的最
大数目(最初悬挂在吊钩上钥匙的特定数目),然后规定每个申请资源者都必须先
获得一把钥匙才能使用该资源。
到目前为止,你大概应该已经发现信号量如何能够被建立在test-and-set之上并成
为二元(“唯一钥匙”)信号量,或者在像atomic_dec_and_test这样的函数之上
成为计数信号量的过程。内核正好就完成着这样的工作:它用整数代表信号量并使
用函数down(11644行)和up(11714行)以及其它一些函数来递减和递增该整数。
读者将看到,用于减少和增加整数的底层代码和atomic_dec_and_test及其它类似
函数所使用的代码是一样的。
作为相关历史事件的提示,第一位规范信号量概念的研究者,Edsger Dijistra是
荷兰人,所以信号量的基础操作就用荷兰语命名为:Proberen和Verhogen,常缩写
成P和V。这对术语被翻译成“测试(test)”(检查是否还有一把钥匙可用,若是
就取走)和“递增(increment)”(把一个钥匙放回到吊钩之上)。那些词首字
母正是在前一章中所引入的术语“获得(procure)”和“交出(vacate)”的来
源。Linux内核打破了这个传统,用操作down和up的称呼取代了它们。
内核用一个非常简单的类型来代表信号量:定义在11609行的struct semaphore。
他只有三个成员:
?   count——跟踪仍然可用的钥匙数目。如果是0,钥匙就被取完了;如果是负数,
钥匙被取完而且还有其它申请者在等待它。另外,如果count是0或负数,那么其它
申请者的数目就等于count的绝对值。
Sema_init宏(11637行)允许count被初始化为任何值,所以内核信号量可以是二
元的(初始化count为1)也可以是计数型的(赋予它某个更大的初始值)。所有内
核信号量代码都完全支持二元和计数型信号量,前者可作为后者的一个特例。不过
在实践中count总是被初始化为1,这样内核信号量也总是二元类型的。尽管如此,
没有什么能够阻止一个开发者将来增加一个新的计数信号量。
要顺便提及的是,把count初始化为正值而且用递减它来表明你需要一个信号量的
方法并没有什么神秘之处。你也可以用一个负值(或者是0)来初始化计数值然后
增加它,或者遵循其它的方案。使用正的数字只是内核所采用的办法,而这碰巧和
我们头脑中的吊钩上的钥匙模型吻合得相当好。的确,正如你将看到的那样,内核
锁采用的是另一种方式工作——它被初始化为负值,并在进程需要它时进行增加。

?   waking——在up操作期间及之后被暂时使用;如果up正在释放信号量则它被设置
为1,否则是0。
?   wait——因为要等待这个信号量再次变为可用而不得不被挂起的进程队列。
down
11644:down操作递减信号量计数值。你可能会认为它与概念里的实现一样简单,
不过实际上远不是这样简单。
11648:减少信号量计数值——要确保对SMP这是自动完成的。对于SMP来说(当然
也适于UP),除了被访问的整数是在一个不同类型的struct之内以外,这同在
atomic_dec_and_test中所完成的工作本质上是相同的。
    读者可能会怀疑count是否会下溢。它不会:进程总是在递减count之后进入休眠
,所以一个给定的进程一次只能获得一个信号量,而且int具有的负值要比进程的
数目多的多。
11652:如果符号位被设置,信号量就是负值。这意味着甚至它在被递减之前就是
0或者负值了,这样进程无法得到该信号量并因此而应该休眠一直到它变成可用。
接下来的几行代码十分巧妙地完成了这一点。如果符号位被设置则执行js跳转(即
若decl的结果是负的它就跳转),2f标识出跳转的目的地。2f并非十六进制值——
它是特殊的GNU汇编程序语法:2表示跳转到本地符号“2”,f表示向前搜索这个符
号。(2b将表示向后搜索最近的本地符号“2”。)这个本地符号在第11655行。
11653:分支转移没有执行,所以进程得到了信号量。虽然看起来不是这样,但是
这实际已经到达down的末尾。稍后将对此进行解释。
11654:down的技巧在于指令.section紧跟在跳转目标的前面,它表示把随后的代
码汇编到内核的一个单独的段中——该段被称为.text.lock。这个段将在内存中被
分配并标识为可执行的。这一点是由跟在段名之后的ax标志字符串来指定的——注
意这个ax与x86的AX寄存器无关。
    这样的结果是,汇编程序将把11655和11656行的指令从down所在的段里转移到可
执行内核的一个不同的段里。所以这些行生成的目标代码与其前边的行所生成的代
码从物理上不是连续的。这就是为什么说11653行是down的结尾的原因。
11655:当信号量无法得到时跳转到的这一目的行。Pushl $1b并不是要把十六进制
值1b压入栈中——如果要执行那种工作应该使用pushl $0x1b(也可以写成是不带
$的)。正确的解释是,这个1b和前边见到的2f一样都是GNU汇编程序语法——它指
向一个指令的地址;在此情形中,它是向后搜索时碰到的第一个本地标识“1”的
地址。所以,这条指令是把11653行代码的地址压入栈中;这个地址将成为返回地
址,以便在随后的跳转操作之后,执行过程还能返回到down的末尾。
11656:开始跳转到__down_failed(不包括在本书之内)。这个函数在栈里保存几
个寄存器并调用后边要介绍的__down(26932行)来完成等待信号量的工作。一旦
__down返回了,__down_failed就返回到down,而它也随之返回。一直到进程获得
了信号量__down才会返回;最终结果就是只要down返回,进程就得到信号量了,而
不管它是立刻还是经过等待后获得的它。
11657:伪汇编程序指令.previous的作用未在正式文档中说明,但是它的意思肯定
是还原到以前的段中,结束11654行里的伪指令.section的作用效果。
down_interruptible
11664:down_interruptible函数被用于进程想要获得信号量但也愿意在等待它时
被信号中断的情况。这个函数与down的实现非常相似,不过有两个区别将在随后的
两段里进行解释。
11666:第一个区别是down_interruptible函数返回一个int值来指示是否它获得了
信号量或者被一个信号所打断。在前一种情况里返回值(在result里)是0,在后
一种情况里它是负值。这部分上是由11675行代码完成的,如果函数未经等待获得
了信号量则该行把result设置为0。
11679:第二个区别是down_interruptible函数跳转到
__down_failed_interruptible(不包括在本书之内)而不是__down_failed。因循
__down_failed建立起来的模式,__down _failed_interruptible只是调整几个寄
存器并调用将在随后进行研究的__down_interruptible函数(26942行)。要注意
的是11676行为__down_failed_ interruptible设置的返回目标跟在xorl之后,
xorl用于在信号量可以被立刻获得的情况中把result归0。down_interruptible函
数的返回值再被复制进result中。
down_trylock
11687:除了调用__down_failed_trylock函数(当然还要调用26961行的
__down_trylock函数,我们将在后面对它进行检查)之外,down_trylock函数和
down_interruptible函数相同。因此,在这里不必对down_trylock函数进行更多解
释。
DOWN_VAR
26900:这是作为__down和_down_interruptible共同代码因子的三个宏中的第一个
。它只是声明了几个变量。
DOWN_HEAD
26904:这个宏使任务tsk(被DOWN_VAR所声明)转移到task_state给出的状态,然
后把tsk添加到等待信号量的任务队列。最后,它开始一个无限循环,在此循环期
间当__down和__down_interruptible准备退出时将使用break语句结束该循环。
DOWN_TAIL
26926:这个宏完成循环收尾工作,把tsk设置回task_state的状态,为再次尝试获
得信号量做准备。
26929:循环已经退出;tsk已或者得到了信号量或者被一个信号中断了(仅适于
__down_ interruptible)。无论哪一种方式,任务已准备再次运行而不再等待该
信号量了,因此它被转移回TASK_RUNNING并从信号量的等待队列里被注销。
__down
26932:__down和__down_interruptible遵循以下模式:
1.  用DOWN_VAR声明所需的本地变量,随后可能还有补充的本地变量声明。
2.  以DOWN_HEAD开始进入无穷循环。
3.  在循环体内完成函数特定的(function-specific)工作。
4.  重新调度。
5.  以DOWN_TAIL结束。注意对schedule的调用(26686行,在第7章里讨论过)可以
被移进DOWN_TAIL宏中。
6.  完成任何函数特定的收尾工作。
我将只对函数特定的步骤(第3和第6步)进行讨论。
26936:__down的循环体调用waking_non_zero(未包括),它自动检查
sem->waking来判断是否进程正被up唤醒。如果是这样,它将waking归零并返回1(
这仍然是同一个原子操作的一部分);如果不是,它返回0。因此,它返回的值指
示了是否进程获得了信号量。如果它获得了值,循环就退出,接着函数也将返回。
否则,进程将继续等待。
    顺便要说明的是,观察一下__down尝试获得信号量是在调用schedule之前。如果
信号量的计数值已知为负值时,为什么不用另一种相反的方式来实现它呢?实际上
它对于第一遍循环之后的任何一遍重复都是没有影响的,但是去掉一次没有必要的
检查可以稍微加快第一遍循环的速度。如果需要为此提出什么特别的理由的话,那
可能就是因为自从信号量第一次被检查之后的几个微秒内它就应该可以被释放(可
能是在另一个处理器上),而且额外获取标志要比一次额外调度所付出的代价少得
多。因此__down可能还可以在重新调度之前做一次快速检查。
__down_interruptible
26942:__down_interruptible除了允许被信号中断以外,它和__down在本质上是
一样的。
26948:所以,当获取信号量时对waking_non_zero_interruptible(未包括)进行
调用。如果它没能得到信号量就返回0,如果得到就返回1,或者如果它被一个信号
所中断就返回–EINTR。在第一种情况下,循环继续。
26958:否则,__down_interruptible退出,如果它得到信号量就返回0(不是1)
,或者假如被中断则返回–EINTR。
__down_trylock
26961:有时在不能立刻获得信号量的情况下,内核也需要继续运行。所以,
__down_trylock不在循环之内。它仅仅调用waking_nonzero_trylock(未包括),
该函数夺取信号量,如果失败就递增该信号量的count(因为内核不打算继续等待
下去)然后返回。
up
11714:我们已经详尽的分析了内核尝试获得信号量时的情况,也讨论了它失败时
的情况。现在是考察另一面的时候了:当释放一个信号量时将发生什么。这一部分
相对简单。
11721:原子性地递增信号量的计数值。
11722:如果结果小于等于0,就有某个进程正在等待被唤醒。up向前跳转到11725
行。
11724:up采用了down里同样的技巧:这一行进入了内核的单独的一段,而不是在
up本身的段内。up的末尾的地址被压入栈然后up跳转到__up_wakeup(未包括)。
这里完成如同__down_failed一样的寄存器操作并调用下边要讨论的__up函数。
__up
26877:__up函数负责唤醒所有等待该信号量的进程。
26897:调用wake_one_more(未包括在本书中),该函数检查是否有进程在等待该
信号量,如果有,就增加waking成员来通知它们可以尝试获取它了。
26880:利用wake_up宏(16612行),它只是调用__wake_up函数(26829行)来唤
醒所有等待进程。
__wake_up
26829:正如在第2章中所讨论的那样,__wake_up函数唤醒所有传递给它的在等待
队列上的进程,假如它们处于被mode所隐含的状态之一的话。当从wake_up被调用
时,函数唤醒所有处于TASK_UNINTERRUPTIBLE或TASK_INTERRUPTIBLE状态的进程;
当从wake_up_interruptible(16614行)被调用时,它只唤醒处于
TASK_INTERRUPTIBLE状态的任务。
26842:进程用wake_up_process(26356行)被唤醒,该函数曾在以前提到过,它
将在本章随后进行详细介绍。
现在所感兴趣的是唤醒所有进程后的结果。因为__wake_up唤醒所有队列里的进程
,而不仅仅是队列里的第一个,所以它们都要竞争信号量——在SMP里,它们可以
精确的同时做这件事。通常,获胜者将首先获得CPU。这个进程将是拥有最大“
goodness”的进程(回忆一下第7章中26338行对goodness的讨论)。 这一点意义
非常重大,因为拥有更高优先权的进程应该首先被给予继续其工作的机会。(这对
于实时进程尤其重要。)
这种方案的不足之处是有发生饥饿(starvation)的危险,这发生在一个进程永远
不能得到它赖以继续运行的资源时。这里可能会发生饥饿现象:假如两个进程反复
竞争同一个信号量,而第一个进程总是有比第二个更高的优先权,那么第二个进程
将永远不会得到CPU。这种场景同它应该的运行方式存在一定差距——设想一个是
实时进程而另一个以20的niceness运行。我们可以通过只唤醒队列里第一个进程的
方法来避免这种饥饿的危险,可是那样又将意味着有时候会耽误从各个方面来说都
更有资格的进程对CPU的使用。
以前对此没有讨论过,可是Linux的调度程序在适当的环境下也能够使得CPU的一个
进程被彻底饿死。这不完全是一件坏事——只是一种设计决策而已——而且至少应
用于通篇内核代码的原则是一致的,这就很好。还要注意的是使用前边讨论过的其
它机制,饥饿现象也同样会发生。例如说,test-and-set原语就是和内核信号量一
样的潜在饥饿根源。
无论如何,在实际中,饥饿是非常少见的——它只是一个有趣的理论案例。
Spinlocks
这一章里最后一个重要的并行程序设计原语是自旋锁(spinlock)。自旋锁的思想
就是在一个密封的循环里坚持反复尝试夺取一个资源(一把锁)直到成功为止。这
通常是通过在类似test-and-set操作之上进行循环来实现的——即,旋转(
spinning)——一直到获得该锁。
如果这听起来好像是一个二元信号量,那是因为它就是一个二元信号量。自旋锁和
二元信号量唯一的概念区别就是你不必循环等待一个信号量——你可以夺取信号量
,也可以在不能立刻得到它时放弃申请。因此,自旋锁原本是可以通过在信号量代
码外再包裹一层循环来实现的。不过,因为自旋锁是信号量的一个受限特例,它们
有更高效的实现方法。
自旋锁变量——其中的一位被测试和设置——总是spinlock_t类型(12785行)。
只有spinlock_t的最低位被使用;如果锁可用,则它是0,如果被取走,则它是1。
在一个声明里,自旋锁被初始化为值SPIN_LOCK_UNLOCKED(12789行);它也可以
用spin_lock_init函数(12791行)来初始化。这两者都把spinlock_t的lock成员
设置成0——也就是未锁状态。
注意12795行代码简洁地对公平性进行了考虑并最后抛弃了它——公平是饥饿的背
面,正如我们前面已经介绍过的(使得一个CPU或进程饥饿应被认为是“不公平的
”)。
自旋锁的加锁和解锁宏建立在spin_lock_string和sping_unlock_string函数之上
,所以这一小节只对spin_lock_string和sping_unlock_string函数进行详述。其
它宏如果有的话只是增加了IRQ加锁和解锁。
spin_lock_string
12805:这个宏的代码对于所有自旋锁加锁的宏都是相同的。它也被用于x86专用的
lock_ kernel和unlock_kernel版本之中(它们不在本书之列,不过其常规版本则
是包括的——参见10174和10182行)。
12807:尝试测试和设置自旋锁的最低位,这要把内存总线锁住以便对于任何其它
对同一个自旋锁的访问来说这个操作都是原子的。
12808:如果成功了,控制流程就继续向下运行;否则,spin_lock_string函数向
前跳转到第12810行(btsl把这一位的原值放入CPU的进位标志位(Carry flag),
这正是这里使用jc的原因)。同样的技巧我们已经看到过三次了:跳转目标放在内
核的单独一段中。
12811:在封闭的循环里不停地检测循环锁的最低位。注意btsl和testb以不同方式
解释它们第一个操作数——对于btsl,它是一个位状态(bit position),而对于
testb,它是一个位屏蔽(bitmask)。因此,12811行在测试spin_lock_string曾
在12807行已经试图设置(但失败了)的同一位,尽管一个使用$0而另一个使用$1

12813:该位被清除了,所以spin_lock_string应该再次夺取它。函数调转回第
12806行。
    这个代码可以只用加上lock前缀的两条代码加以简化:
    1: lock ; btsl $0, %0
      jc 1b
    不过,使用这个简化版本的话,系统性能将明显受到损害,这因为每次循环重复
内存总线都要被加锁。内核使用的版本虽然长一些,但是它可以使其它CPU运行的
更有效,这是由于该版本只有在它有充分理由相信能够获得锁的时候才会锁住内存
总线。
spin_unlock_string
12816:并不很重要:只是重新设置了自旋锁的锁定位(lock bit)。
读/写自旋锁
自旋锁的一个特殊情况就是读/写自旋锁。这里的思想是这样的:在某些情况中,
我们想要允许某个对象有多个读者,但是当有一个写者正在写入这个对象时,则不
允许它再有其它读者或者写者。
遵循基于spinlock_t的自旋锁的同样模式,读/写自旋锁是用rwlock_t(12853行)
来代表的,它可以在有RW_LOCK_UNLOCKED(12858行)的声明里被初始化。与
rwlock_t一起工作的最低级的宏是read_lock、read_unlock、write_lock,以及
write_unlock,它们在本小节中进行描述。很明显,那些跟随在这些宏之后并建立
在它们之上的宏,自然要在你理解了最初的这四个宏之后在去接触。
正如第12860行注释中所声明的,当写锁(write lock)被占有时,rwlock_t的
lock成员是负值。当既没有读者也没有写者时它为0,当只有读者而没有写者时它
是正值——在这种情况下,lock将对读者的数目进行计数。
read_lock
12867:开始于rwlock_t的lock成员的自动递增。这是推测性的操作——它可以被
撤销。
12868:如果它在增量之后为负,表示某个进程占用了写锁——或者至少是某个进
程正试图得到它。read_lock向前跳到第12870行(注意,在一个不同的内核段里)
。否则,没有写者退出(尽管还有可能有,或者也有可能没有其它读者——这并不
重要),所以可以继续执行读锁定(read-locked)代码。
12870:一个写者出现了。read_lock取消第12867行增值操作的影响。
12871:循环等待rwlock_t的lock变为0或正值。
12873:跳回到第12866行再次尝试。
read_unlock
12878:不太复杂:只是递减该计数值。
write_lock
12883:表示出有一个进程需要写锁:检测并设置lock的符号位并保证lock的值是
负的。
12884:如果符号位已经被设置,则另外有进程占有了写锁;write_lock向前跳转
到第12889行(同以前一样,那是在一个不同的内核段里)。
12885:没有别的进程正试图获得该写锁,可是读者仍可以退出。因为符号位被设
置了,读者不能获得读锁,但是write_lock仍然必须等待正在退出的读者完全离开
。它通过检查低端的31位中是否任何一位被设置过开始,这可以表示lock以前曾是
正值。如果没有,则lock在符号位反转之前曾是0,这意味着没有读者;因而,这
对于写者的继续工作是很安全的,所以控制流程就可以继续向下运行了。不过,如
果低端31位中任何一位被设置过了,也就是说有读者了,这样write_lock就会向前
跳转到第12888行等到它们结束。
12888:该进程是仅有的写者,但是有若干读者。write_lock会暂时清除符号位(
这个宏稍后将再次操纵它)。有趣的是,对符号位进行这样的胡乱操作并不会影响
读者操纵lock的正确性。考虑作为示例的下列顺序事件:
1.  两个读者增加了lock;lock用十六进制表示现在是0x00000002。
2.  一个即将成为写者的进程设置了符号位;lock现在是0x80000002。
3.  读者中的一个离开;lock现在是0x80000001。
4.  写者看到剩余的位不全部是0——仍然有读者存在。这样它根本没有写锁,因此
它就清除符号位;lock现在是0x00000001。
    这样,读和写可以任何顺序交错尝试操作而不会影响结果的正确程度。
12889:循环等待计数值降到0——也就是等待所有读者退出。实际上,0除了表示
所有读者已离开之外,它还表示着没有其它进程获得了写锁。
12891:所有读者和写者都结束了操作;write_lock又从头开始,并再次获得写锁

write_unlock
12896:不太重要:只是重置符号位。
APICs和CPU-To-CPU通信
Intel 多处理规范的核心就是高级可编程中断控制器(Advanced Programmable
Interrupt Controllers——APICs)的使用。CPU通过彼此发送中断来完成它们之
间的通信。通过给中断附加动作(actions),不同的CPU可以在某种程度上彼此进
行控制。每个CPU有自己的APIC(成为那个CPU的本地APIC),并且还有一个I/O
APIC来处理由I/O设备引起的中断。在普通的多处理器系统中,I/O APIC取代了第
6章里提到的中断控制器芯片组的作用。
这里有几个示例性的函数来让你了解其工作方式的风格。
smp_send_reschedule
5019: 这个函数只有一行,其作用将在本章随后进行说明,它仅仅是给其ID以参
数形式给出了的目标CPU发送一个中断。函数用CPU ID和RESCHEDULE_VECTOR向量调
用send_IPI_single函数(4937行)。RESCHEDULE_VECTOR与其它CPU中断向量是一
起在第1723行开始的一个定义块中被定义的。
send_IPI_single
4937: send_IPI_single函数发送一个IPI——那是Intel对处理器间中断(
interprocessor interrupt)的称呼——给指定的目的CPU。在这一行,内核以相
当低级的方式与发送CPU的本地APIC对话。
4949: 得到中断命令寄存器(ICR)高半段的内容——本地APIC就是通过这个寄存
器进行编程的——不过它的目的信息段要被设置为dest。尽管__prepare_ICR2(
4885行)里使用了“2”,CPU实际上只有一个ICR而不是两个。但是它是一个64位
寄存器,内核更愿意把它看作是两个32位寄存器——在内核代码里,“ICR”表示
这个寄存器的低端32位,所以“ICR2”就表示高端32位。我们想要设置的的目的信
息段就在高端32位,即ICR2里。
4950: 把修改过的信息写回ICR。现在ICR知道了目的CPU。
4953: 调用__prepare_ICR(4874行)来设置我们想要发送给目的CPU的中断向量
。(注意没有什么措施能够保证目的CPU不是当前CPU——ICR完全能够发送一个
IPI给它自己的CPU。尽管这样,我还是没有找到有任何理由要这样做。)
4957: 通过往ICR里写入新的配置来发送中断。
SMP支持如何影响内核
既然读者已经学习了能够成功支持SMP的若干原语,那么就让我们来纵览一下内核
的SMP支持吧。本章剩余的部分将局限于对分布在内核之中的那些具有代表性的
SMP代码进行讨论。
对调度的影响
schedule(26686行)正是内核的调度函数,它已在第7章中全面地介绍过了。
schedule的SMP版本与UP的相比有两个主要区别:
?   在schedule里从第26780开始的一段代码要计算某些其它地方所需的信息。
?   在SMP和UP上都要发生的对__schedule_tail的调用(26638行)实际上在UP上并
无作用,因为__schedule_tail完全是为SMP所写的代码,所以从实用的角度来说它
就是SMP所特有的。
schedule
26784:获取当前时间,也就是自从机器开机后时钟流逝的周期数。这很像是检查
jiffies,不过是以CPU周期而不是以时钟滴答作为计时方法的——显然,这要精确
得多。
26785:计算自从schedule上一次在此CPU上进行调度后过去了多长时间,并且为下
一次的计算而记录下当前周期计数。(schedule_data是每个CPU aligned_data数
组的一部分,它在26628行定义。)
26790:进程的avg_slice成员(16342行)记录该进程在其生命周期里占有CPU的平
均时间。可是这并不是简单的平均——它是加权平均,进程近期的活动远比很久以
前的活动权值大。(因为真实计算机的计算是有穷的,“很久以前”的部分在足够
远以后,将逐渐趋近于0。)这将在reschedule_idle中(26221行,下文讨论)被
用来决定是否把进程调入另一个CPU中。因此,在UP的情况下它是无需而且也不会
被计算的。
26797:记录哪一个CPU将运行next(它将在当前的CPU上被执行),并引发它的
has_cpu标志位。
26803:如果上下文环境发生了切换,schedule记录失去CPU的进程——这将在下文
的__schedule_tail中被使用到。
__schedule_tail
26654:如果失去CPU的任务已经改变了状态(这一点在前边的注释里解释过了),
它将被标记以便今后的重新调度。
26664:因为内核已经调度出了这个进程,它就不再拥有CPU了——这样的事实也将
被记录。
reschedule_idle
26221:当已经不在运行队列里的进程被唤醒时,wake_up_process将调用
reschedule_idle,进程是作为p而被传递进reschedule_idle中的。这个函数试图
把新近唤醒的进程在一个不同的CPU上进行调度——即一个空闲的CPU上。
26225:这个函数的第一部分在SMP和UP场合中都是适用的。它将使高优先级的进程
得到占用CPU的机会,同时它也会为那些处于饥饿状态的进程争取同样的机会。如
果该进程是实时的或者它的动态优先级确实比当前占有CPU进程的动态优先级要高
某个量级(强制选定的),该进程就会被标记为重新调度以便它能够争取占用CPU

26263:现在来到SMP部分,它仅仅适用于在上述测试中失败了的那些进程——虽然
这种现象经常发生。reschedule_idle必须确定是否要在另一个CPU上尝试运行该进
程。
    正如在对schedule的讨论中所提到的那样,一个进程的avg_slice成员是它对CPU
使用的加权平均值;因此,它说明了假如该进程继续运行的话是否它可能要控制
CPU一段相对来说较长的时间。
26264:这个if条件判断的第二个子句使用related宏(就在本函数之上的第26218
行)来测试是否CPU都在控制着——或想要控制——内核锁。如果是这样,那么不
管它们生存于何处,都将不大可能同时运行,这样把进程发送到另一个CPU上将不
会全面提高并行的效能。因此,假如这条子句或者前一条子句被满足,函数将不会
考虑使进程在另一CPU上进行调度并简单的返回。
26267:否则,reschedule_idle_slow(接下来讨论)被调用以决定是否进程应当
被删除。
reschedule_idle_slow
26157:正如注释中所说明的,reschedule_idle_slow试图找出一个空闲CPU来贮存
p。这个算法是基于如下观察结果的,即task数组的前n项是系统的空闲进程,机器
的n个CPU中每个都对应一个这样的空闲进程。这些空闲进程当(且仅当)对应CPU
上没有其它进程需要处理器时才会运行。如果可能,函数通常是用hlt指令使CPU进
入低功耗的“睡眠”状态。
    因此,如果有空闲CPU存在的话,对任务数组的前n个进程进行循环是找出一个空
闲CPU所必须的。reschedule_idle_slow函数只需简单的查询每个空闲进程是否此
刻正在运行着;如果是这样,它所在的CPU就一定是空闲的,这就为进程p提供了一
个很好的候选地点来运行。
    当然,这个被选中的明显空闲的CPU完全有可能只是暂时空闲而且必定会被一堆拥
有更高优先级的,CPU绑定的进程所充满,这些进程可能在一纳秒后就会被唤醒并
在该CPU上运行。所以,这并不是完美的解决方法,可是从统计的角度来说它已经
相当好了——要记住,像这样的选择是很符合调度程序“快餐店式(
quick-and-dirty)”的处理方式的。
26180:建立本地变量。best_cpu是此时正在运行的CPU;它是“最佳”的CPU,因
为p在其上会避免缓冲区溢出或其它的开销麻烦。this_cpu是运行
reschedule_idle_slow的CPU。
26182:idle和tsk将沿task数组进行遍历,target_tsk将是所找到的最后一个正在
运行的空闲进程(或者假如没有空闲进程它就为NULL)。
26183:i 从smp_num_cpus(前边被叫作n)开始并且在每一次循环后都递减。
26189:假如这个空闲进程的has_cpu标志被设置,它就正在它的CPU上运行着(我
们将称这样的CPU为“目标(target)CPU”)。如果该标志没有被设置,那么目标
CPU就正被某个其它进程占用着;因而,它也就不是空闲的,这样
reschedule_idle_slow将不会把p发送到那里。刚刚提及问题的反面在这里出现了
:现在仅因为CPU不空闲并不能表示它所有的进程都不会死亡而使其空闲下来。可
是reschedule_idle_slow无法知道这种情形,所以它最好还是假定目标CPU将要被
占用一段时间。无论如何,这都是可能的,就算并非如此,某个其它的进程也将很
快会被调度到另一个空闲CPU上运行。
26190:不过假如CPU目标就是当前CPU,它就会被跳过。这看来很怪,不过无论怎
样这都是“不可能发生”的情况:一个空闲进程的counter是负值,在第26226行的
测试将早已阻止这个函数执行到这一步了。
26192:找到一个可用的空闲CPU;相关的空闲进程被保存在target_tsk中。
    既然已找到了空闲CPU,为什么现在不中断循环呢?这是因为继续循环可能会发现
p当前所在的处理器也是空闲的,在两个CPU都空闲时,维持在当前处理器上运行要
比把它送往另一个好一些。
26193:这一步reschedule_idle_slow检查是否p所在的处理器空闲。如果刚才找到
的空闲CPU就是p所在的,函数将向前跳转到send标记处(26203行)来在那个CPU上
对p进行调度。
26199:函数已经转向另一个CPU;它要递减。
26204:如果循环遍历了所有空闲的CPU,该CPU的空闲任务就被标记为重新调度并
且smp_ send_reschedule(26205行)会给那个CPU发送一个IPI以便它可以重新对
其进程进行调度。
    正如读者所见到的,reschedule_idle_slow是CPU之间协调无需在UP系统中所进行
的工作的典范示例。对于UP机器来说,询问进程应占有哪一个CPU和询问它是否应
拥有系统的唯一的一个CPU或根本不应该占有CPU是等价的。SMP机器必须花费一些
代价来决定系统中哪一个CPU是该进程的最佳栖身之所。当然,换来的速度极大提
高使得这些额外的努力还是相当合算的。
release
22951:release中非SMP特有的部分在第7章中已经介绍过了——在这里,一个僵进
程(zombie)将被送往坟墓,而且其struct task_struct将被释放。
22960:查看是否该进程拥有一个CPU。(拥有它的CPU可能还没有清除这个标志;
但是它马上就将执行这个操作。)如果没有,release退出循环并像往常一样接着
释放struct task_ struct结构体。
22966:否则,release等待进程的has_cpu标志被清除。当它被清除后,release再
次进行尝试。这种貌似奇特的情况——某进程正被删除,然而它仍占有CPU——确
实少见,不过并非不可能。进程可能已经在一个CPU上被杀死,而且这个CPU还没来
得及清除has_cpu标志,但是它的父进程已经正在从另一个CPU对它进行释放了。
smp_local_timer_interrupt
对于UP专有的update_process_times函数(27382行)来说,这个函数就是它在
SMP上的对应。该函数能够完成update_process_times所完成的所有任务——更新
进程和内核在CPU使用方面的统计值——以及其它的一些操作。与众不同的地方在
于拥有这个特性的SMP版本并没有被添加到一个UP函数中去,而是采用了一个具有
同样功能,但却完全分离的功能程序。在浏览了函数之后,我们就能够很容易的知
道这是为什么了——它与UP版本差别甚大到以至于试图将二者融为一体都将是无意
义的。smp_local_timer_interrupt可从两个地方进行调用:
?   从smp_apic_timer_interrupt(5118行)调用,它用于SMP的时钟中断。这是通
过使用在第1856行定义的BUILD_SMP_TIMER_INTERRUPT宏于第919行建立起来的。
?   从第5776行通常的UP时钟中断函数里进行调用。只有当在UP机器上运行SMP内核
时此种调用方式才会发生。
smp_local_timer_interrupt
5059: prof_counter(4610行)用于跟踪到更新进程和内核统计值之前内核应该
等待多长时间;如果该计数器还没有到达0,控制流程会有效地跳转到函数的末尾
。正如代码中所证明的,prof_counter项目从1开始递减计数,除非由根(root)
来增加这个值,因此在缺省情况下每次时钟滴答都要完成此项工作。然后,
prof_counter[cpu]从prof_multiplier[cpu]处被重新初始化。
    明显的这是一个优化的过程:每次时钟滴答都在这个if语句块里完成所有工作将
相当的缓慢,所以我们可能想到以牺牲一些精确度的代价将工作分批完成。因为乘
法器是可调的,所以你可以指定你所需要的速度频率来放松对准确度的要求。
    然而,关于这段代码我总感到有些困惑:确定无疑的是,当
prof_multiplier[cpu]耗尽时,统计值应该被更新,就像prof_multiplier[cpu]的
计数流逝一样——既然它们已经如此。(除了prof_multiplier[cpu]本身刚刚被改
变时,不过这已经偏离了这里讨论的主题。)与此不同的是,这里代码表现出来的
就好像只经过了一次滴答计数。或许其用意是为了以后能把记录下来的滴答数目和
prof_multiplier[cpu]在某个地方相乘,不过现在并没有这样实现。
5068: 当时钟中断被触发时假如系统正在用户模式运行,
smp_local_timer_interrupt会假定全部滴答都是在用户模式里流逝的;否则,它
将假定全部滴答是在系统模式里流逝的。
5073: 用irq_enter(1792行)来夺取全局IRQ锁。这是我们要分批处理这项工作
的另一个原因:并不需要在每次时钟滴答时都要得到全局IRQ锁,这有可能成为
CPU之间争夺的一个重要根源,实际中函数是以较低的频度来争取该锁的。因此,
函数不经常夺取这个锁,可是一旦它获得了锁,就不会再使其被锁。在此我们又一
次以准确度的代价换来了这种效率上的提高。
5074: 不用为保存空闲进程的统计值而操心。这样做只会浪费CPU的周期。总之,
内核会跟踪系统处于空闲的总共时间,对空闲进程的更多细节进行统计价值不大(
比如我们知道它们总是在系统模式下执行的,所以就没有必要再明确计算它们的系
统时间了)。
5075: update_process_times和smp_local_timer_interrupt在这一点上是一致的
:它们都调用update_process_times来完成对单进程CPU使用统计的更新工作。
5077: 减少进程的counter(它的动态优先级),如果它被耗尽就重新调度该进程

5082: 更新内核的统计数字。如在update_process_times中一样,用户时间既可
以用内核的“最优时间”也可以用常规的用户时间来计算,这要取决于进程的优先
级是否低于DEF_PRIORITY。
5094: 重新初始化CPU的prof_counter并释放全局IRQ锁。该工作必须要以这种顺
序完成,当然——若以相反的方式,则可能在prof_counter被重新初始化之前发生
又一次时钟中断。
lock_kernel和unlock_kernel
这两个函数也有专门适应于x86平台的版本;但是在这里只介绍通用版本。
lock_kernel
10174:这个函数相当简单,它获得全局内核锁——在任何一对
lock_kernel/unlock_kernel函数里至多可以有一个CPU。显然这在UP机器上是一个
空操作(no-op)。
10176:进程的lock_depth成员初始为–1(参见24040行)。在它小于0时(若小于
0则恒为-1),进程不拥有内核锁;当大于或等于0时,进程得到内核锁。
    这样,单个进程可以调用lock_kernel,然后在运行到unlock_kernel之前可能又
将调用另一个要使用lock_kernel的函数。在这种情况中,进程将立刻被赋予内核
锁——而这正是我们所期望的。
    其结果是,一旦增加进程的lock_depth就会使lock_depth为0,那么进程以前就是
没有锁的。所以,函数在此情形下获得kernel_flag自旋锁(3587行)。
unlock_kernel
10182:同样的,如果丢弃内核锁就会使lock_depth低于0值,进程退出它所进入的
最后一对lock_kernel/unlock_kernel函数。此时,kernel_flag自旋锁一定要被解
锁以便其它进程可以给内核加锁。通过测试结果的符号位(即使用“<0”而不是“
== -1”)可以使gcc生成更高效的代码,除此之外,这还可能有利于内核在面对不
配对的lock_ kernel/unlock_kernel时可正确执行(或者不能,这取决于具体情况
)。
softirq_trylock
你可能能够回忆起在第6章的讨论中,softirq_trylock的作用是保证对于其它程序
段来说下半部分代码(bottom half)是原子操作——也就是说,保证在任何特定
时段的整个系统范围之内至多只有一个下半部分代码在运行。对于UP来说这相当容
易:内核只不过需要检查或者还要设置一下标志位就可以了。不过对于SMP来说自
然没有这样简单。
softirq_trylock
12528:测试并设置(tests-and-sets)global_bh_count的第0位。尽管读者可能
会从global _bh_count的名字上得到另外一种看法,实际它总是0或者1的——这样
的考虑是适当的,因为至多运行一个下半部分程序代码。不管怎样,如果
global_bh_count已经是1了,那么就已经有一个下半部分代码在运行着,因此控制
流程就跳转到函数末尾。
12529:如果还可得到global_bh_lock,那么下半部分代码就能够在这个CPU上运行
。这种情况与UP机器上使用的双锁系统非常类似。
12533:softirq_trylock无法获取global_bh_lock,因此它的工作失败了。
cli和sti
正如在第6章中解释过的,cli和sti分别用于禁止和启用中断。对于UP这简化为单
个cli或sti指令。而在SMP情况下,这就很不够了,我们不仅需要禁止本地CPU还要
暂时避免其它CPU处理IRQ。因此对于SMP,宏就变成了对__global_cli和
__global_sti函数的调用。
__global_cli
1220: 把CPU的EFLAGS寄存器复制到本地变量flags里。
1221: x86系统里的中断使能标志在EFLAGS寄存器的第9位——在第1205行解释了
EFLAG_IF_SHIFT的定义。它被用来检测是否已经禁止了中断,这样就不再需要去禁
止它们了。
1223: 禁止这个CPU的中断。
1224: 如果该CPU没有正在对IRQ进行处理,__global_cli就调用get_irqlock(
1184行)来获得全局IRQ锁。如果CPU已经在对IRQ进行处理了,那么正如我们马上
要看到的,它已经拥有了该全局IRQ锁。
    现在本CPU已经禁止了中断,而且它也拥有了全局IRQ锁,这样任务就完成了。
__global_sti
1233: 如果CPU没有正在对IRQ进行处理,__global_sti就在__global_cli中通过
release_irqlock(10752行)调用来实现对全局IRQ锁的释放工作。如果CPU已经在
对IRQ进行处理了,那么它已经拥有了该全局IRQ锁,正如在接下来的部分中将要解
释的那样,这个锁将在其它地方被释放掉。  
1235: 再次允许在本CPU上进行中断。
irq_enter和irq_exit
第6章中顺便提及了这两个函数的UP版本。包含在一对irq_enter/irq_exit之中的
代码段都是原子操作,这不仅对于其它这样的代码区域是原子的,而且对于
cli/sti宏对来说也是如此。
irq_enter
1794: 调用hardirq_enter(10761行)自动为本CPU增加全局IRQ计数和本地IRQ计
数。这个函数记录了CPU正在处理一个IRQ的情况。
1795: 执行循环直到这个CPU得到全局IRQ锁为止。这就是为什么我要在前面说明
如果CPU正在处理IRQ,那么它就已经获得了全局IRQ锁的原因:到这个函数退出时
,这两个特性都将被加强。对于内核代码来说,把这两个特性分离出去并没有太大
的意义——它可以直接调用hardirq_enter,而且也不用去争夺全局IRQ锁。函数只
是没有这样作而已。
irq_exit
1802: 这个函数转向hardirq_enter的相反函数hardirq_exit(10767行)。顺便
要提及的是,对irq_enter和irq_exit来说其irq参数都被忽略了——至少在x86平
台上如此。
回复

使用道具 举报

 楼主| 发表于 2003-10-27 10:05:04 | 显示全部楼层
Linux内核注释——第11章 可调内核参数

遵循Unix的BSD 4.4版本所倡导的风格,Linux提供sysctl系统调用以便在系统运行
过程中对它所拥有的某些特性进行检查和重新配置,它并不需要你编辑内核的源代
码、重新编译,然后重启机器。这是对早期Unix版本的一个十分重要的改进,在早
期版本里调整系统经常是令人头痛的琐碎事务。Linux把可以被检查和重新配置的
系统特性有机地组织成了几个种类:常规内核参数、虚拟内存参数、网络参数,等
等。
同样的特性也可以从一个不同的接口进行访问:/proc文件系统。(因为它真正的
是系统的一个透视区(window)而不只是真实文件的一个容器,所以/proc是一个
“伪的文件系统”,不过那是一个蹩脚的词汇,而且无论如何这个区别在此并不重
要。)每种可调内核参数在/proc/sys下都表现为一个子目录,而每个单独的可调
系统参数由某个子目录下的一个文件来代表。这些子目录可能又包含一级子目录,
它们仍然含有更多的代表可调系统参数的文件和子目录,等等,但是这种嵌套级数
从来都不会很深。
/proc/sys绕过了通常的sysctl接口:一个可调内核参数的值可以简单的通过读取
相应的文件来得到,通过写入该文件可以设置它的值。普通Unix文件系统的许可被
应用于这些文件,以便对能够对它们进行读写的用户进行控制。大多数文件对所有
用户是可读的但是只对root(根用户)可写,不过也有例外:比如,
/proc/sys/vm下的文件(虚拟内存参数)只能被root来读写。如果不使用
/proc/sys,检查和调整系统将需要编写程序并使用必须的参数调用sysctl——虽
然不是任务艰巨的劳动,可是也比不上使用/proc/sys来得方便。
struct ctl_table
18274:这是本章涉及的代码中所使用的一个主要数据结构。struct ctl_tables通
常是由数组聚合起来的,每个这样的数组对应于/proc/sys下某处一个单独目录里
的条目。(依我之见,称它为struct ctl_table_entry可能更好。)root_table(
30328行)以及在它之后的数组通过struct ctl_table的child指针连结节点而形成
了一个数组树(child将在下边的列表中介绍)。注意所有这些都是ctl_table的数
组,它只是为struct ctl_table进行typedef;18184行完成这项工作。
    图11.1示意出了数组树间的关系。这幅图显示了由root_table形成的树的一小部
分以及它所指向的树。
    struct ctl_table具有如下成员:
?   ctl_name——是唯一标识表项的一个整数——在它所在的数组中是唯一的;这个
数字在不同的数组中是可以重用的。数组的任何一项都已经存在这样一个唯一的数
字了——就是它的数组下标——可是这个数字不能被用于该目的,因为我们想要维
护不同内核发布版本中的二进制兼容性。与某内核版本里一个数组项相关联的可调
内核参数可能不会出现在将来的内核版本里,所以假如参数是被它们的数组下标定
义的,对数组里废弃项目位置的重新使用将使还没有在新内核版本下编译过的程序
变得混乱。随着时间的推移,为了向后兼容而带上的只浪费空间但没有作用的元素
项将会使数组变得乱七八糟。相反的,这种方法只会“浪费”整数,而整数资源却
无疑是非常丰富的。(另一方面,查找也会更慢,因为一个简单的数组下标还不足
以满足这种方法。)
要注意的是这与有系统调用的情形相当类似:每个系统调用都与一个在系统调用表
里唯一标识它位置的数字相关联。但是在这种情况里使用了一个不同的解决办法,
可能由于速度在此并不重要的缘故。


图11.1 ctl_table 树的一部分

    struct ctl_table具有如下成员:
?   ctl_name——是唯一标识表项的一个整数——在它所在的数组中是唯一的;这个
数字在不同的数组中是可以重用的。数组的任何一项都已经存在这样一个唯一的数
字了——就是它的数组下标——可是这个数字不能被用于该目的,因为我们想要维
护不同内核发布版本中的二进制兼容性。与某内核版本里一个数组项相关联的可调
内核参数可能不会出现在将来的内核版本里,所以假如参数是被它们的数组下标定
义的,对数组里废弃项目位置的重新使用将使还没有在新内核版本下编译过的程序
变得混乱。随着时间的推移,为了向后兼容而带上的只浪费空间但没有作用的元素
项将会使数组变得乱七八糟。相反的,这种方法只会“浪费”整数,而整数资源却
无疑是非常丰富的。(另一方面,查找也会更慢,因为一个简单的数组下标还不足
以满足这种方法。)
要注意的是这与有系统调用的情形相当类似:每个系统调用都与一个在系统调用表
里唯一标识它位置的数字相关联。但是在这种情况里使用了一个不同的解决办法,
可能由于速度在此并不重要的缘故。
?   procname——是用于/proc/sys下的相应项的一个可供我们阅读的简短文件名。

?   data——一个指向与此表项关联的数据的指针。它通常指向一个int或者一个
char(当然,指向char的指针是字符串)。
?   maxlen——可以读取或者写入data的最大字节数。如果data指向一个单精度型的
int,举例来说,maxlen就应该是sizeof(int)。
?   mode——Unix类型的文件许可位,它对应于这一项的/proc文件(或目录)。对
此的解释需要少量文件系统的内容。就像其它Unix的实现一样,Linux使用三个三
元组,其中每一位都记录一个文件许可(在ls -l命令产生的列表里它们表现为r、
w,和x的三组字母)——参见图11.2。它们占据mode的低端9位。文件系统把文件
的mode里剩余的位留作它用,比如用来跟踪是否文件是常规文件(第16位,当它如
此时)、目录(第15位)、setuid或setgid执行程序(第12和11位),等等。不过
就本章的目的来说,那些其它位都不是我们所关心的内容。


图11.2 文件的mode位

这种方式的结果是,读者将经常见到八进制的常数004、002,和001与mode一起使
用——它们分别是在移位mode后可能得到的适当的三位组中检测读(r)、写(w)
,和执行(x)位。这种移位和检查工作基本上是在30544行的test_perm里完成的

注意如果一个表项的maxlen是0,那么不管它的mode是什么,从最终效果上看它都
是既不可读也不可写的。
?   child——如果这是一个目录类型的条目,那么它就是指向子表(child table)
的一个指针。在这样的情况下,因为没有数据与此条目相关联,data将是NULL,而
maxlen则将是0。
?   proc_handler——指针,指向对data成员实际进行读取和写入操作的一个函数;
它在通过/proc文件系统读写数据时被使用。以这种方法,任何类型的数据都可以
通过data来进行指向,而且proc_handler函数会正确的处理对它的工作。
Proc_handler通常指向proc_dostring函数(30820行)或proc_dointvec函数(
30881行);这两个以及其它被普遍适用的函数将在本章后面被讨论。(当然,任
何具有适当原型(prototype)的函数都可以使用。)对于目录类型的条目,proc_
handler是NULL。
?   strategy——指针,指向对data成员实际进行读取和写入操作的另一个函数;它
使用在通过sysctl系统调用进行读写的时候。它通常是sysctl_string(31121行)
,不过也可以是stringctl_intvec(31163行);这两个函数在本章后面进行讨论
。出于种种原因,大多数可调内核参数是通过/proc接口而不是sysctl系统调用进
行调整的,所以这个指针是NULL会比非空更为常见。
?   de——指向struct proc_dir_entry的一个指针,它在/proc文件系统代码中使用
以追踪文件系统里的文件或目录。如果它非空,struct ctl_table就在/proc下的
某处注册过了。
?   extra和extra2——指向在处理这个表元素时所需的任何补充数据。它们当前只
用于指定某些整数参数的最小和最大值。
/proc/sys 支持
不是所有实现用于可调内核参数/proc/sys接口的代码都包括在这本书中——的确
,大部分代码并没有包括在内,因为它们基本上属于/proc文件系统本身。尽管如
此,只要你不关心/proc剩下的部分是如何工作的,就不难理解在kernel/sysctl.
c里的代码,它们与/proc文件系统一起工作用来使/proc下的可调内核参数是可见
的。
register_proc_table
30689:register_proc_table函数在/proc/sys下注册一个ctl_table。注意这里并
不要求所提供的表是根一级的节点(即ctl_table没有双亲)——它本应该是,不
过这取决于调用者是否能够进行保证。
    这个表被直接建立在root之下,它应该对应于/proc/sys或者其下的一个子目录。
(在初次调用时,root总是指向proc_sys_root的,但是在递归调用时它的值改变
了。)
30696:开始在table数组的所有元素中进行循环;在当前元素的clt_name成员为0
时循环结束,表示这是数组的末尾。
30698:如果ctl_table的procname元素是NULL,那么即使同一数组的其它元素都可
以为用户所见,它也不可以在/proc/sys下被用户所见。这样的数组元素会被跳过

30701:如果表项有procname,表明它应该在/proc/sys下被注册,那么它一定还有
一个proc _handler(如果是一个叶子,或文件类型的节点)或者一个child(如果
是一个目录类型的节点)。如果它同时缺少这两者,那么系统将显示一条警告,而
后循环继续进行。
30711:若表项有一个proc_handler,它被标记成常规文件。
30713:否则,正如可从第30701行推断的那样,它一定有一个非空的child,这样
该条目将被看作是一个目录。注意并没有禁止ctl_table同时拥有非空
proc_handler和child这两者——在这种情形下,所有代码将对其一视同仁。
30715:用给定的名字搜索一个存在的子目录,如果找到就让de指向它,如果没找
到则de为NULL。为什么对文件不做类似的检查比较难于理解——这可能是我没有领
会的文件系统的某个细节问题,答案无疑就在那里。
30723:如果指定的子目录已经不存在了,或者假如table对应于一个文件而不是一
个目录,新的文件或者目录就会通过调用create_proc_entry(未包含在本书中)
来创建。
30728:如果表项是一个叶子节点,register_proc_table会告诉文件系统代码使用
由proc_sys_ inode_operations(30295行)定义的文件操作。
proc_sys_inode_operations只定义了两个操作,读和写(不是搜索、内存映射,
或者其它)。这些操作是用proc_readsys和proc_writesys函数(30802和30808行
)来执行的,在本章的后面章节中将对它们进行介绍。
30731:到了这一行,de就不可能是NULL了——它或者已经非空或者在第30723行被
初始化了。
30733:如果增加的条目是目录类型,register_proc_table会被递归调用来增加这
一项的所有子孙。这是内核里不多见的一次递归调用。
unregister_proc_table
30739:unregister_proc_table函数删除ctl_table数组树和/proc文件系统之间的
关联。ctl_  table里的条目以及它们下面所有的“子目录”里的条目也将会从
/proc/sys消失。
30743:同第30396行一样,这一行开始在给定的表项数组上进行循环。
30744:与/proc/sys下任意条目都不关联的表项具有一个为NULL的de成员;显然这
些表项可被忽略。
30748:如果/proc文件系统认为这是一个目录,但表项是一个叶子(非目录),这
两个结构就是不一致的。unregister_proc_table就会显示一条警告并继续循环,
而不会移去这一项。
30752:目录被逐层的进行释放——内核中另一次并不多见的递归过程。
30756:在递归调用结束之后,unregister_proc_table检查是否所有子目录和文件
都被逐层删除了——如果不是,当前元素就不能被安全的移去,接着要继续循环。

30762:这里就是为什么子目录(以及其中的文件)可能还没有被移去的原因:它
们可能当前还正被使用着。如果这个元素正在被使用,循环将继续,这样该元素就
不会被移走。
30765:节点通过proc_unregister(本书不进行介绍)从文件系统里被删除,接着
用于追踪该节点而分配的内存被释放。
do_rw_proc
30771:do_rw_proc实现proc_readsys(30802行)和proc_writesys(30806行)函
数的核心部分,这两个函数被/proc文件系统代码用于对ctl_table执行读取和写入
操作。
30782:确保一个表与/proc/sys下的这一条目相关联。
30785:注意这一行的第一个测试与第30782行的第二个测试是相重复的,这是因为
table是从de->data初始而来。
30788:确保调用进程有适当的读或写权限。
30795:调用该表项的proc_handler来完成真正的读操作或写操作。(要注意第
30785行证实了proc_ handler成员是非空的。)如前所述,proc_handler成员通常
是proc_dostring或proc_ dointvec(30820行和30792行),在随后的几段中我们
将对它们进行讨论。
30799:do_rw_proc返回实际读取或写入的字节数。注意到本地变量res完全是多余
的;它可以被参数count所替代。
proc_dostring
30820:proc_dostring是供文件系统代码调用以对C语言字符串型的内核参数进行
读取或写入操作的函数。
    注意write标志表示调用者正在写表元素,不过这主要是涉及从输入缓冲区里进行
读取——因此,用来写入的代码是受读控制的。类似的,如果write未被设置,调
用者正从该表项读取,这里主要涉及的是写入给定的缓冲区。
    这个函数在第31085行还实现了一个存根程序(stub);这个存根程序在/proc文
件系统被编译出内核时使用。大多数其它函数中的类似存根程序将在这个存根程序
之后被介绍。
30835:从输入缓冲区内读取字符直到一个表示结束的ASCII NUL(0)字节或者发
现新的一行,再或者到达了被允许从该输入缓冲区内读出数据的最大值(被lenp所
指定)为止。(为了不引起混淆,牢记NULL是一个C指针常量,而NUL——只有一个
L——是ASCII用于字符数字0的术语。)
30842:如果从缓冲区读出的字符数超出了可在表项里存储的限度,该数目会被降
低。在循环之前就限制最大输入长度(lenp)可能会更高效,因为不管怎样从
buffer里读取大于table->maxlen字节的数据是无意义的。实际上,循环可能读出
,假设是1024字节,然后降低计数到64,因为表项里只能存储这么多。
30844:该字符串从输入缓冲区里被读出,然后以NUL结束。
30847:内核为每个进程所拥有的每个文件维护一个“当前位置”变量;这就是
struct file的f_pos成员。它是tell系统调用返回的值并由seek系统调用进行设置
。因此,文件的当前位置是由写入的字节数所推进的。
proc_doutsstring
30871:在获得uts_sem信号量后(29975行),proc_doutsstring仅是调用
proc_dostring。这个函数被kern_table(30341行)里的一些条目用来设置
system_utsname结构体的不同部分(20094行)。
do_proc_dointvec
30881:proc_dointvec(30972行)把它的工作委托给了该函数。
do_proc_dointvec读或写一个被table的data成员所指向的int类型数组。要读写的
int类型数目通过lenp传递;它通常是1,所以本函数通常只被用于读写单独一个
int。
    用于int的值是被buffer指定的。这些int是不会被以一个未经加工的int数组传递
的;相反的,它们以ASCII文本给出,而这正是用户写入相关/proc文件的。
30898:在所有要读写的int中循环。left追踪调用者想要读写int的剩余数目,而
vleft追踪table->data里剩余的有效元素数目。在这二者中任何一个到达0,或它
从半途退出时,该循环结束。
    注意如果从循环中去掉第30899行的if语句,可以使整个循环的效率稍微提高一些
,尽管这样做的结果较难维护。取代的代码如下:
        
P556—1
   
这种方式使得并不在循环内改变的write的值将只需被检查一次,而不必在每次循
环重复检查。
30900:向前搜索一个不是空格的字符,它是输入(缓冲区)里下一个数字的开头

30913:从用户空间把一大块数据复制到本地缓冲区buf,然后以NUL结束buf。现在
buf里包含了所有输入缓冲里剩余的ASCII文本——或者是它所能容纳的那些文本。

    这种方法看起来不很有效率,原因在于它可能读取的超出了它所需要的。然而,
因为buf的容量仅为20(TMPBUFLEN,30885行),它就不可能读取比它所需多出许
多的数据。这里的思想可能是读入稍多一些数据要比检查每个字节以确定是否应该
停止读操作所付出的代价要少些。
    计划使buf足够大来包括任何64位整数的ASCII表示,以便这个函数不仅可以支持
32位平台还可以支持64位平台。的确,它只能满足最大的正64位整数,它有19个数
位(使终结的NUL字节是第20个字节)。可是要记住这些是有符号的整数,所以最
小的64位有符号整数,即-9,223,372,032,854,775,808也应是合法输入。这个数字
无法被正确的读取。但是幸运的是,补救方法工作量不大而且也非常明显。
    随后读者就能够看到当这个输入出现时代码将如何对其进行处理。
30919:处理打头的减号(-),如果发现一个减号就跳过它并设置一个标志。
30923:确保从buffer读取的文本(可能是打头的减号之后的部分)至少是以一个
数字开始的,这样它才能顺利的转换为一个整数。若没有这次检查,就不可能分辨
出第30925行调用simple_strtoul返回的0是因为输入就是“0”还是因为函数无法
转换任何文本。
30925:把文本转换为一个整数,用conv参数换算结果。这个换算步骤对于
proc_dointvec _jiffies这样的函数(31077行)比较有用,它用乘以常数HZ的简
单手段把它的输入从秒转换为一段时间值(jiffies)。然而一般情况里,这个比
例因子是1——即没有换算。
30927:如果还要从缓冲区读取更多的文本,而且下一个要读的字符不是分割参数
的(argument-separating)空格,那么整个参数(argument)就无法装进buf。这
样的输入是无效的,所以循环提早结束。(一种可以导致函数处于这种状态的方式
就是前边所描述的,输入表示的是最小的有符号64位整数。)不过,没有错误代码
会被返回,因此调用者可能会错误地认为一切正常。当然这也不完全正确:一个错
误代码将在第31070行被返回,不过这仅当无效参数是在第一次循环重复中被检测
到的时候;如果它在后续的循环里被检测到,错误就不会被注意到。
30929:参数被成功的读取。如果有前导的减号,那么现在就对它进行考虑,其它
的本地变量被调整转移到下一个参数上,然后这个参数通过指针i被存储在表项中

30936:调用者从表项里读取值——由于无需对ASCII文本进行语法分析,这就是一
种更为简单的情形。输出是由tab(制表符)分隔的,所以在除了第一次之外的任
何一次循环里都把一个tab写入临时缓冲区里(在最后一个参数之后也不用写,只
需在参数之间即可)。
30938:接着,当前的整数被conv因子按比例缩减并打印到临时缓冲区里。这段代
码同样会受读者前边已经见到的问题的损害:临时缓冲区buf的大小可能不足以容
纳打印到它里边的全部整数值。在这种情况下,实际问题还会因缓冲区的第一个位
置可以是一个tab制表符而被恶化。这会使buf的可用部分减少一个字符,进一步还
会降低可被正确处理的输入范围。
    在这里过大或过小的整数所造成的结果要比在写入情形里严重的多。在那种情形
中,代码只要抛弃一些本应接受的输入即可。而在这儿,sprintf会越过buf的末尾
继续写下去。
    然而令人惊讶的是,这正是实际工作中可能发生的。在一次典型的执行过程中将
有可能发生如下执行过程:从总体上来说,超过buf的末尾之后还要写入两个额外
的字节(一个是因为它可以写入比预期更长的数字,另一个是tab制表符)。在栈
里p通常是紧跟在buf之后的,所以超出buf末尾写入的部分将会覆盖p。可是由于p
没有先被重新初始化时它是不会再被使用的,因此暂时覆盖它的值并没有危害。
    这是一个看似有趣的事故,但是仅仅通过使buf稍微大一些就能够成为一个更好的
解决方式,这样便于代码为正确的而不是错误的前提(reason)而工作。依照原样
,对于gcc的代码生成器进行完全合法的很小的修改就能够揭示出潜在的缺陷。
30939:把当前int的文本型表示复制进输出缓冲区里——或者和它所能容纳的相等
的文本——接着更新本地变量使其转移到表项的下一个数组元素。
30949:如果调用者刚才在读取,输出就被新的一行结束。if条件语句也保证循环
不会在其第一遍执行而且还有空间来写入新行时就结束。注意输出缓冲区不是用
ASCII NUL字节(读者可能会这样猜测)来结束的,因为它无需如此:调用者能够
利用lenp被写入新值来减少返回字符串的长度。
30954:如果调用者正向表项里写入数值,则略过从输入缓冲区读取的最后参数之
后所有的空格。
30967:更新文件的当前位置和lenp,然后返回0表示成功。
proc_dointvec_minmax
30978:proc_dointvec_minmax函数类似于do_proc_dointvec,区别是这个函数还
把表项的extra1和extra2成员作为可以写入该表项的限制值数组来处理。extra1里
的值是最小限度,而extra2里的值则是最大限度。另一点区别是
proc_dointvec_minmax不使用conv参数。
    因为这两个函数颇为相似,所以这一段里只介绍其不同之处。
31033:最大的区别在于:当写入时,超过被min和max(在extra1和extra2数组上
循环得到)所定义的范围之外的值将悄无声息的被略过。这段代码的目的明显是要
使min和max伴随着val一起继续。当一个数值从输入缓冲里被读取时,它应该被下
一个min和max来检查,然后才能决定被接受或被忽略。可是,这并非是实际所发生
的那样。
    假设从buffer而来的当前值已经进行了语法分析并存入里val,它小于最小值;为
了更具体一些,再假设已是第三遍循环,以便min和max分别指向对应数组中的第三
个元素。然后val将用min来检查并发现它超出了范围(太小),接着循环还要继续
。可是min会作为检查的副作用被更新,而max则没有。现在,min指向它对应数组
的第四个元素了,可是max仍然指向它的数组的第三个元素。这两者不再同步,而
且它们还将保存这种状态,这样在下一个从buffer里读取的值被检验时采用的就是
错误的界限。下列代码是最简单的一种修补程序:
        P558—1
    正如读者将要在本章后边看到的,现在的Linux源代码永远不会暴露出这个缺陷。
(未来发行的版本情况将有所不同,尽管还未曾明确写出。)
sysctl系统调用
用于可调内核参数的另一个接口是sysctl系统调用,以及相关函数。我不很喜欢这
个接口。为什么不呢?对于大部分实际工作目的来说,使用sysctl——不过这种方
法比修改源代码的旧方法来调整内核能够获得更大的性能提高——只会比访问
/proc文件更为笨拙。通过sysctl来进行读写需要C程序(或相似的东西),而
/proc却很容易通过外壳(shell)命令(或等价的通过命令解释程序脚本)来进行
访问。
另一方面,如果你正在C环境下工作,调用sysctl就比打开文件、读取并/或写入,
以及再关闭它要方便的多,所以sysctl在今后也有它的用武之地。与此同时,还是
让我们来看一看它的实现吧。
do_sysctl
30471:do_sysctl实现sys_sysctl(30504行),即sysctl系统调用的主要内容。
注意sys_sysctl还在第31275行出现过——那个版本只是在sysctl系统调用被编译
出内核时所使用的一个简单的存根程序(stub)函数。
    如果oldval非空就用oldval返回内核参数原有的值,而它的新值在newval非空时
从newval来进行设置。oldlenp和newlen分别标识出有多少字节应被写入oldval和
从newval读出,这是在相应的指针不是NULL的时候;当指针为NULL的时候,它们将
被忽略。
    要注意这里的不对称性:函数对旧值的长度使用指针,而对新的长度不使用指针
。这是因为旧的长度既是输入参数也是输出参数;它的输入值是可以通过oldval返
回的最大字节数,而它的输出值是实际返回的字节数。与之相反,新的长度只是一
个输入参数。
30482:如果调用者需要旧的内核参数值,从oldlenp来对old_len进行设置。
30490:开始遍历表树的循环列表。(参见本章随后对register_sysctl_table的讨
论。)
30493:使用parse_table(30560行,在下一段里讨论)来定位可调内核参数,然
后读和/或写它的值。
30495:如果parse_table分配了所有环境信息,它就被释放。很难准确地说出这个
环境信息表示着什么。它不被本书所讨论的任何代码使用——据我所知,它目前甚
至没有被内核里的任何代码所使用。
30497:ENOTDIR错误表示没有在这一棵表树中找到指定的内核参数——它可能在另
一棵还没有查找过的表树中。否则,error将为某个其它的错误代码,或者是代表
成功的0;无论如何,函数应该返回了。
30499:用DLIST_NEXT宏(本书对此不做介绍)来增加循环控制变量的值(loop
iterator)。
30501:返回ENOTDIR错误,报告出指定的内核参数在任何一个表里都未找到。
parse_table
30560:parse_table用于在表树里查找一个条目,其方法类同于在一个目录树里解
析出一个完全合格的文件名的方法。其思想如下:沿着一个int数组(数组name)
进行查找,并在一个ctl_table数组里搜索每个int。当找到一个匹配项时,它对应
的子孙表就被递归查阅(如果匹配项是目录类型的条目),或者该条目被读和/或
写(如果它是文件类型的条目)。
30566:多少有些令人惊讶的是,这一行就开始了对整型数组name内所有元素的循
环。习惯上的方法原本是把从这一行到第30597行所有代码用一个for循环包括起来
,它的开始是这样的:
    for ( ; nlen ; ++name , --nlen , table = table -> child )
    (这个循环还需要删除第30567和30568行代码,并用一个语句来替代从30587直到
30590行的代码。)推测起来,可能是实际使用的版本可以生成更好的目标代码吧

30570:开始循环所有的表项,查找与当前name匹配的一项;当表已被遍历结束(
table->ctl_  name为0了)或者指定的表项已被找到并处理时本循环结束。
30572:把name数组的当前项读入n里,以便它可以与当前表项的ctl_name进行检查
。因为name在内层循环中没有变化,这个读取操作可以放在循环外边(也就是移至
30569行)以提高一点速度。
30574:核查是否当前ctl_table的名字与被找到的名字相匹配,或是否它有特殊的
“通配符(wildcard)”值,即CTL_ANY(17761行)。后者的使用目的还不清楚,
因为现在并没有内核源代码的任何一处使用过CTL_ANY。它可能用于将来的方案中
——我也不认为它是过去版本的一个遗留物,因为CTL_ANY在2.0内核里也没有被用
到,而且整个sysctl接口也只向后兼容到2.0以前的开发树版本。
30576:如果这个表元素有一个孩子,它就是一个“目录”。
30577:遵循标准Unix行为,检查目录的x(可执行)位来判断是否当前进程可被允
许对它进行访问。注意到这与文件系统所实现的工作非常类似,虽然这并不是(
/proc)文件系统接口。这样可以使这两种接口在施用于可调内核参数时能够得到
一致的结果——如果一个用户有通过一种接口来修改某个内核参数的权限而通过另
一种却没有该权限,那么将是非常不可思议的。
30579:如果表项有一个策略(strategy)函数,它可能需要覆盖允许该进程进入
目录的授权。这个策略函数将被访问,如果它返回一个非零值,整个查找就被中止

30587:进入目录。本行有效的继续外层循环,并转移到该名字的下一部分。
30592:这个表节点是一个叶子节点,因此内核参数就被找到了。注意这并不打扰
对name数组是否已到其最后元素的检查(也就是现在nlen是否为1),虽然可以证
明如果不是那样就会有某类型错误产生。不管哪一种情况,do_sysctl_strategy(
30603行)都要负责对当前表元素进行读和/或写操作。
30598:name数组非空,可是它的元素在叶子节点被找到之前均已用完。
parse_table就返回ENOTDIR错误,来表示查找指定节点失败。顺便提及一点,注意
前一行里的分号是多余的。
do_sysctl_strategy
30603:do_sysctl_strategy在单独一个ctl_table里读和/或写数据。计划使用该
表元素里的strategy成员,如果存在的话,来完成读/写工作。如果表元素没有它
自己的strategy例程,某些通用的读/写代码将被替代使用。不过读者将要看到,
它并不完全按照计划工作。
30610:如果oldval非空,调用者将读取旧值,这样r位就会在op里被设置。类似的
,如果newval非空则w位被设置。接着,第30614行核查许可,如果当前进程缺少所
需的授权就返回EPERM错误。
30617:如果表项有它自己的strategy例程,这个例程就要处理读/写请求。如果它
返回负数——一个错误——这个错误就被传送给调用者。如果返回的是正数,0(
成功)就会被传送给调用者。如果是0,strategy例程就拒绝由它自己来处理请求
,取而代之的将是缺省行为。(读者可以设想只返回0的strategy例程,如果它完
成一些其它诸如收集被调用次数的统计数据这样的工作,它仍然是有用处的。)
30630:这里是通用读取代码开始的地方。注意get_user(13254行)的返回值不被
检查。(类似的缺陷发生在第9537和31186行。)
30632:确保不会有多于与该表项的maxlen成员所指定的数值相等的数据被返回。

30634:通过oldval从表里复制所要求的数据,再将真正被写的数据总量存储在
oldlenp中。
30642:类似于oldlenp,要确保写入表项的数据不能多于它的maxlen成员所允许的
值。注意如果copy_from_user在中途的第30644行检测到一个错误,tabel->data可
能会在仅仅被部分更新的情况下就结束。
30648:返回0表示成功。以下三种情况都可以达到这一点:
?   调用者对这个表项既不读也不写。
?   调用者尝试读和/或写这个表项,而且所有步骤都被成功执行。
?   表项没有关联的数据,或者因为它的maxlen是0,所以它是只读的。
三种情形中的第一种有点儿奇怪,而最后一种则更令人奇怪。第一种情况有些奇特
是因为调用sysctl却要求它对指定的表项既不读也不写,这并没有多少意义,所以
可以正当的把它当作一个错误来处理。尽管如此,它要与其它系统调用的内核实现
保持基本一致,那就是把一个无操作请求不看作是一个错误。比如说,在第8章中
介绍的sys_brk(33155行)在由调用者指定的新brk值与旧值相同时并不产生一个
错误信号。
第三种情况要比第一种奇怪一些,因为它可能真的反映着一个错误。例如,调用代
码尝试写入一个maxlen是0的参数,而且由于系统调用返回成功值而认为该尝试已
被完成。看起来事情好像不是这样,因为不管怎样为0的maxlen都会使该条目失效
,不过还真的存在一个maxlen为0的表项——参见第30380行。最终,一切都归结为
sysctl是怎样在文档中描述的,但是man帮助程序中却对此没有任何记载。我仍然
认为do_sysctl_strategy在这种情况下应该返回一个EPERM错误。
register_sysctl_table
30651:把一个新的根已经被给出的ctl_table树插入到其它树所形成的循环链表里

30655:分配一个struct ctl_table_header用来管理新树的信息。
30659:把新的首部(跟踪ctl_tables数组形成的新树)插入到首部组成的链表里

30666:调用register_proc_table(30689行,本章前边讨论过)把新的表树注册
在/proc/sys目录下。如果没有内核在没有/proc文件系统支持的情况下进行编译时
,则这一行将被编译到内核以外。
30688:新分配的首部被返回给调用程序,以便调用程序能够在以后通过把该首部
转递给unregister_sysctl_table(30672行)来删除相应的树。
unregister_sysctl_table
30672:如前所述,这个简单函数只是把一个ctl_table的树从内核里这样的树所组
成的循环链表里删除。如果内核是在支持/proc的情况下编译的,它也用于从
/proc文件系统里删除相应的数据。
    回顾一下第30490和30500行,读者不难发现root_table_header(30256行)——
对应于root_table的列表节点——是在遍历树的循环链表时被用作头和尾节点的。
读者现在能够明白实际上在unregister_sysctl_table函数里没有什么可以避免
root_table_header被从表头列表里删除——它只是还没有这样做而已。
sysctl_string
31121:sysctl_string是ctl_table的策略例程之一。回忆一下,策略例程可以从
第30618行(在do_sysctl_strategy里)被调用来有选择的覆盖一个表项的缺省读
/写代码。(策略例程也可以从第30580行被调用,不过该例程却从不会被调用。)

31127:如果该表没有相关数据,或者如果可访问部分的长度是0,则返回ENOTDIR
错误。这与do_sysctl_strategy的做法是不一致的,在同样的情况里它返回的是成
功。
31138:当前字符串的值被复制到用户空间,然后结果以NUL来结束(这意味着比由
lenp指定值多一个字节的数据可能会被复制——依据文档记录,这可能是一个缺陷
)。因为当前值已经是NUL结束的,这四行代码可以被简化为两行:
    if ( copy_to_user ( oldval , table -> data , len + 1 ) )
        return –EFAULT ;
    这种改变的正确性部分上依赖于当写入table->data时代码剩余部分所遵循的三个
特征:
?   代码剩余部分不能把多于table->maxlen的char数据复制进table->data里。(这
也使得第31136行的测试变得没有必要。即使还需要该测试,那也只用检查>,而不
用检查>=了。)
?   然后table->data以NUL来结束,如果必要就复写最后一个拷贝进来的字节,以便
包括NUL在内的总长度不大于table->maxlen。
?   table->maxlen永不发生变化。
    因为所有三个特征都有效,所以在第31138行len将总是严格小于table->maxlen,
而且结束NUL字节一定会在table->data[len+1]或之前的位置出现。
31146:与前边的情况类似,从用户空间中复制新值,而且结果以NUL来结束。不过
在这种情形下,不从用户空间复制NUL字节是一种正确的做法,因为把它从用户空
间复制进来要比仅仅在data的适当字节安排一个NUL效率低。而且以这种方式,即
使输入不是NUL结束的,table->data也要如此。当然,从newval读出的字符串可能
已经是NUL结束的,在那种情况里第31154行的赋值就是多余的。这还是另一种情况
,直接完成工作比检查需要是否执行它还要快。
31156:返回0表示成功。相反,返回的值应该为正数,以便30618行代码认为结果
是成功的。而又相反,调用代码认为sysctl_string想让缺省处理发生,然后它就
继续从用户空间再次复制多余的数据。
sysctl_intvec
31163:sysctl_intvec是在kernel/sysctl.c里定义的另一个策略例程。它确保假
如调用程序正在写入表项,所有被写的int都应位于某个最小和最大值之间。(顺
便提及一下,sysctl_intvec在这个文件里只被使用了一次——在第30414行——尽
管它被广泛的用于本书所没有包括的内核的其它代码之中。)
31170:如果新的欲写数据总量不符合一个int大小的边界,它就无效,所以尝试被
抛弃。
31173:假如表项没有指定一组最大或最小值,输入的值就永远不可能超出范围,
这样调用程序里的普通写代码(do_sysctl_strategy,30603行)就足够好了。因
此在这种情况里sysctl_intvec返回0。
31184:进行循环以确保所有来自输入数组的值都位于适当范围之内。
31186:这行代码不检查get_user的返回值——没有迫切的需要去这样做。如果当
不能读取一个输入内存位置时,sysctl_intvec返回0(成功),那么当它试图读取
整个数组时do_sysctl_strategy就会注意到这个问题。作为另一选择,假如
get_user无法读取内存位置,无用信息(garbage)可能在value里结束并且数值可
能会不正确的被抛弃。在此情况里,调用程序将得到一个EINVAL错误而不是
EFAULT错误,这只是一个小缺陷(bug)。
31187:注意这一行不会被折磨第31033行相似代码的缺陷所困扰,该行中在最小值
和最大值之上进行的并行循环会产生不同步的情况。
    这一行代码能够避免位于31033行的缺陷被暴露出来。正如实际中所进行的,
sysctl_intvec和proc_dointvec_minmax都总是与同一个ctl_table条目相关联的。
因此,在调用处理例程(handler routine)proc_dointvec_minmax之前,任何超
出允许范围之外的数值将会被策略例程sysctl_intvec截获。所以,我们知道——
在给定内核里所有的ctl_tables最新定义的情况下——proc_dointvec_minmax将永
远不会遇到超出界限的数值,而那里是唯一可以触发该缺陷的数值种类。某个调用
程序或许可以注册一个使用proc_dointvec_minmax但没有策略例程的ctl_table,
但是尽管这样,在proc_dointvec_ minmax里的这个缺陷迟早会造成一定损害。
31193:返回0表示成功。这里不像在第31156行那样是一个错误,因为
sysctl_intvec并不向table->data里写入。从用户空间读出的值只是被读进一个临
时变量里作范围检查,然后就被删除;do_sysctl_strategy将完成那项工作,并只
向table->data里进行写入。
回复

使用道具 举报

 楼主| 发表于 2003-10-27 10:05:43 | 显示全部楼层
Linux内核注释——附录A Linux 2.4
内核的开发人员们并没有因为我在写这本关于Linux的书而暂停他们的工作。(我
是多么希望他们这样做……。)说真的,内核的发展进展神速,就在这本书即将出
版之际,它的最新稳定版本,2.4.0,也应该面世了。尽管在写作本书时要将对内
核的成千种修改都一一涵盖到是不可能的,但是这篇附录却总结了发生在本书所涉
及内核部分的最令人感兴趣的那些改变。这些修改中的绝大部分已经在内核的2.
3.12版本里实现了,它们被包括在附赠的CD-ROM上。
作者十分感谢Joe Pranevich的论文,”Wonderful World of Linux 2.4”(
http://linuxtoday.Com/stories/8191.html),在准备这篇附录时它提供了无价
的帮助。
更少的“惊扰(stampedes )”
请读者回忆一下第2章里,函数__wake_up(26829行)唤醒等待在等待队列上的所
有进程。可以考虑一个诸如Apache一样的Web服务器,它试图通过在同一端口派生
出许多进程监听连接申请以缩短响应时间(正好与等待连接请求到达,并且与只通
过fork派生一个进程来响应的方式相反)。所有这些进程都在一个等待队列里,等
待连接请求到达,而当请求确实到达的时候,它们都会被唤醒。它们当中只有一个
能够为请求提供服务,所以剩下的又都将返回休眠状态。
这种“惊扰”现象会浪费CPU的时钟周期——只唤醒一个等待进程会更好,因为不
管怎样只有一个能够运行。因此,一个新的任务状态位TASK_EXCLUSIVE就被引进;
在一次调用__wake_up里,最多有一个设置了TASK_EXCLUSIVE位的任务会被唤醒。
TASK_EXCLUSIVE位是附加在其他任务状态位上的——它并不代表一个新的任务状态
,它只是为了方便而保存在任务状态信息组里的信息而已。
现在__wake_up要检查它正在唤醒的进程是否设置了TASK_EXCLUSIVE位,并且在它
唤醒该进程之后便不再唤醒其他进程(通过使用break中断循环)。
TASK_EXCLUSIVE位现在只被用在有关网络的等待队列上,而且它在那种环境中效果
也的确很好。对于大多数其他等待队列来说,你需要所有进程对等待着的资源都有
机会进行占用,这样可以避免饥饿现象。不过等待在同一个端口的服务程序通常与
Apache境遇相同:所有等待的任务都是一样的,而且重要的是只要它们中有一个能
够处理请求就可以,即使每次都是同一个任务(也无所谓)。
再见吧,Java
正如第7章里所预言的那样,Java二进制处理程序(binary handler)已经从内核
里消失了,它是因被杂项二进制处理程序所替代而失效的。二元处理程序的常规用
法没有变化,Java执行体仍然可被杂项二进制处理程序的适当配置所完全支持。
ELF权能位
已有非官方的修补程序可以用于(增强)ELF执行体中权能的存储。它们还没有成
为正式内核版本的一部分,部分原因在于是否ELF文件头是保存这些信息的恰当位
置的问题还处于讨论之中。不过,这些修补程序也有可能已经成为正式版本的一部
分了。
调度程序提速
已被高度优化了的schedule函数(26686行)又被作了进一步优化。大多数修改只
是在system_call(171行)系列调用的基础上进行了重新组织——也就是说,通过
允许普通情况直接通过以及把遍布在函数里的大部分if语句体都分散开来的方式提
高代码的(运行)速度。举例来说,第26706和26707行,如果又要运行的代码它们
就运行底下的下半部分(bottom halves),现在采用的是这种形式:
P563—1
这样一来,如果下半部分程序必须运行,控制流程就会跳转到新的handle_bh标记
处,然后在运行完下半部分之后再跳转回去。原有方式在无需运行下半部分时的正
常情况下也要产生一个分支转移,因为在那种情况下所产生的代码不得不跳过对
handle_bottom_halves的调用。在新的版本里,正常情况只需直接通过,不会产生
任何分支。
更多的进程
Linux 2.4几乎消除了对同时可以运行的进程数目的固有限制。唯一剩下来的硬编
码(hard-coded)限制就是PID的最大数目了(不要忘记PID可被共享)。这个改进
使得Linux能够更好的适合于高端(high-end)服务器应用程序,包括经常需要同
时运行大量进程的Web服务。
在第8章中说明过,原有4090个任务的最高限度是受可以同时保存在全局描述表(
GDT)里的任务状态段(TSS)和局部描述表(LDT)的最大数目所影响的:Linux
2.2在GDT里为每个进程存储一个TSS和一个LDT,而且GDT总共允许有最多8192个条
目。Linux 2.4通过存储TSS和LDT本身而不是把它们存储在GDT里的方法回避了这个
限制。现在,GDT只保持每CPU一个TSS和一个LDT,而且把这些条目设置为在每个
CPU上的当前运行任务所需要的信息。
虽然用于追踪每个CPU的空闲进程的init_tasks数组还存在,但是task数组(
26150行)现在却没有了。
日益进步的SMP支持
每一个锁都是一个SMP机器可以被有效地转化为UP机器的地方:正等待一个锁的
CPU对系统的整体性能没有任何贡献。因此,改善SMP性能的主要方法就是通过减小
锁的作用域或者完全消除它们来增加并行程序的运行机会。
Linux 2.0只有一个单独的全局内核锁,所以每次只有一个CPU可以在内核里执行。
Linux 2.2把这个全局内核锁使用的大部分地方都用更小的、子系统专用的锁来替
代了,其中的一些我们在第10章中已经见到过。Linux 2.4继续了这个趋势,它把
可能减小其作用域的锁都作了进一步的分割。比如现在每个等待队列就有一个锁,
而不是所有的等待队列才有唯一的一个锁。
回复

使用道具 举报

 楼主| 发表于 2003-10-27 10:06:47 | 显示全部楼层
Linux内核注释——附录B GNU通用公共

序言
    大多数软件许可证都有意剥夺你的共享和修改软件的自由。相对之下,GNU通
用公共许可证力图保证你的共享和修改自由软件的自由。——保证自由软件对所有
用户都是自由的。GPL适用于大多数自由软件基金会的软件,以及由愿意遵守该许
可证规定的作者所开发的软件。(自由软件基金会的其它一些软件受GNU库通用许
可证的保护)。你也可以将它应用到你的程序中。
    当我们谈到自由软件(free software)时,我们指的是自由而不是价格。我
们的GNU通用公共许可证目的是要保证你有发布自由软件的自由(如果你愿意,你
也可以对此项服务收取一定的费用);保证你能收到源程序或者在你需要时能得到
它;保证你能修改软件或将它的一部分用于新的自由软件;而且还保证你知道你能
做这些事情。
    为了保护你的权利,我们需要作出规定:禁止任何人不承认你的权利,或者要
求你放弃这些权利。如果你自行发布了软件的副本或者对其进行了修改,那么这些
规定就转化为你的责任。
    例如,如果你发布这样一个程序的副本,不管是收费的还是免费的,你必须将
你具有的一切权利给予你的接受者;你必须保证他们能收到或得到源程序;并且将
这些条款给他们看,使他们知道他们有这样的权利。
    我们采取两项措施来保护你的权利。
    (1)给软件以版权保护。
    (2)给你提供许可证。它给你复制,发布和修改这些软件的法律许可。
    同样,为了保护每个作者和我们自己,我们需要清楚地让每个人明白,自由软
件没有担保(no warranty)。如果由于其他某个人修改了软件,并继续加以传播
。我们需要它的接受者明白:他们所得到的并不是原来的自由软件。由其他人引入
的任何问题,不应损害原作者的声誉。
    最后,任何自由软件都不断受到软件专利的威胁。我们希望能够避免这样的风
险,自由软件的再发布者以个人名义获得专利许可证。事实上,将软件变为私有。
为防止这一点,我们必须明确:任何专利必须以允许每个人自由使用为前提,否则
就不准许有专利。
    下面是有关复制,发布和修改的确切的条款和条件。
复制,发布和修改的条款和条件
此许可证适用于任何包含版权所有者声明的程序和其它作品,版权所有者在声明中
明确说明程序和作品可以在本GPL条款的约束下发布。下面提到的“程序”指的是
任何这样的程序或作品。而“基于程序的作品”指的是程序或者任何受版权法约束
的衍生作品:也就是说包含程序或程序的一部分的作品。可以是原封不动的,也可
以是经过修改的和/或翻译成其它语言的(程序)。在下文中,翻译包含在修改的
条款中。每个许可证接受人(licensee)用“你”来称呼。
    许可证条款不适用于复制,发布和修改以外的活动。这些活动超出这些条款的
范围。运行程序的活动不受条款的限制。仅当程序的输出构成基于程序作品的内容
时,这一条款才适用(如果只运行程序就无关)。是否普遍适用取决于程序具体用
来做什么。
1.  只要你在每一副本上明显和恰当地给出版权声明和不承担担保的声明,保持此
许可证的声明和没有担保的声明完整无损,并和程序一起给每个其它的程序接受者
一份许可证的副本,你就可以用任何媒体复制和发布你收到的原始的程序的源代码

    你可以为转让副本的实际行动收取一定费用。你也有权选择提供担保以换取一
定的费用。
2.  你可以修改程序的一个或几个副本或程序的任何部分,以此形成基于程序的作
品。只要你同时满足下面的所有条件,你就可以按前面第一款的要求复制和发布这
一经过修改的程序或作品。
    a) 你必须在修改的文件中附有明确的说明:你修改了这一文件及具体的修改
日期。
    b) 你必须使你发布或出版的作品(它包含程序的全部或一部分,或包含由程
序的全部或部分衍生的作品)允许第三方作为整体按许可证条款免费使用。
    c) 如果修改的程序在运行时以交互方式读取命令,你必须使它在开始进入常
规的交互使用方式时打印或显示声明:包括适当的版权声明和没有担保的声明(或
者你提供担保的声明);用户可以按此许可证条款重新发布程序的说明;并告诉用
户如何看到这一许可证的副本。(例外的情况:如果程序自身以交互方式工作,但
是它并不像通常情况一样打印这样的声明,你的基于程序的作品也就不用打印声明
)。
    这些要求适用于修改了的作品的整体。如果能够确定作品的一部分并非程序的
衍生产品,可以合理地认为这部分是独立的,是不同的作品。当你将它作为独立作
品发布时,它不受此许可证和它的条款的约束。但是当你将这部分作为基于程序的
作品的一部分发布时,作为整体它将受到许可证条款约束。准予其他许可证持有人
的使用范围扩大到整个产品。也就是每个部分,不管它是谁写的。
    因此,本条款的意图不在于索取权利;或剥夺全部由你写成的作品的权利。而
是履行权利来控制基于程序的集体作品或衍生作品的发布。
    此外,将与程序无关的作品和该程序或基于程序的作品一起放在存贮介质或发
布媒体的同一卷上,并不导致将其它作品置于此许可证的约束范围之内。
3.  你可以以目标码或可执行形式复制或发布程序(或符合第2款的基于程序的作
品),只要你遵守前面的第1,2款,并同时满足下列3条中的1条。
    a)在通常用作软件交换的媒体上,和目标码一起附有机器可读的完整的源码
。这些源码的发布应符合上面第1,2款的要求。或者
    b)在通常用作软件交换的媒体上,和目标码一起,附有给第三方提供相应的
机器可读的源码的书面报价。有效期不少于3年,费用不超过完成源程序发布的实
际成本。源码的发布应符合上面的第1,2款的要求。或者
    c)和目标码一起,附有你收到的发布源码的报价信息。(这一条款只适用于
非商业性发布,而且你只收到程序的目标码或可执行代码和按b)款要求提供的报
价)。
    作品的源码指的是对作品进行修改最优先择取的形式。对可执行的作品讲,完
整的源码包括:所有模块的所有源程序,加上有关的接口的定义,加上控制可执行
作品的安装和编译的script。作为特殊例外,发布的源码不必包含任何常规发布的
供可执行代码在上面运行的操作系统的主要组成部分(如编译程序,内核等)。除
非这些组成部分和可执行作品结合在一起。
    如果采用提供对指定地点的访问和复制的方式发布可执行码或目标码,那么,
提供对同一地点的访问和复制源码可以算作源码的发布,即使第三方不强求与目标
码一起复制源码。
4.  除非你明确按许可证提出的要求去做,否则你不能复制、修改、转发许可证和
发布程序。任何试图用其它方式复制、修改、转发许可证和发布程序是无效的。而
且将自动结束许可证赋予你的权利。然而,对那些从你那里按许可证条款得到副本
和权利的人们,只要他们继续全面履行条款,许可证赋予他们的权利仍然有效。

5.  你没有在许可证上签字,因而你没有必要一定要接受这一许可证。然而,没有
任何其它东西赋予你修改和发布程序及其衍生作品的权利。如果你不接受许可证,
这些行为是法律禁止的。因此,如果你修改或发布程序(或任何基于程序的作品)
,你就表明你接受这一许可证以及它的所有有关复制,发布和修改程序或基于程序
的作品的条款和条件。
6.  每当你重新发布程序(或任何基于程序的作品)时,接受者自动从原始许可证
颁发者那里接到受这些条款和条件支配的复制,发布或修改程序的许可证。你不可
以对接受者履行这里赋予他们的权利强加其它限制。你也没有强求第三方履行许可
证条款的义务。
7.  如果由于法院判决或违反专利的指控或任何其它原因(不限于专利问题)的结
果,强加于你的条件(不管是法院判决,协议或其它)和许可证的条件有冲突。他
们也不能用许可证条款为你开脱。在你不能同时满足本许可证规定的义务及其它相
关的义务时,作为结果,你可以根本不发布程序。例如,如果某一专利许可证不允
许所有那些直接或间接从你那里接受副本的人们在不付专利费的情况下重新发布程
序,唯一能同时满足两方面要求的办法是停止发布程序。
    如果本条款的任何部分在特定的环境下无效或无法实施,就使用条款的其余部
分。并将条款作为整体用于其它环境。
    本条款的目的不在于引诱你侵犯专利或其它财产权的要求,或争论这种要求的
有效性。本条款的主要目的在于保护自由软件发布系统的完整性。它是通过通用公
共许可证的应用来实现的。许多人坚持应用这一系统,已经为通过这一系统发布大
量自由软件作出慷慨的供献。作者/捐献者有权决定他/她是否通过任何其它系统
发布软件。许可证持有人不能强制这种选择。
    本节的目的在于明确说明许可证其余部分可能产生的结果。
8.  如果由于专利或者由于有版权的接口问题使程序在某些国家的发布和使用受到
限制,将此程序置于许可证约束下的原始版权拥有者可以增加限止发布地区的条款
,将这些国家明确排除在外。并在这些国家以外的地区发布程序。在这种情况下,
许可证包含的限止条款和许可证正文一样有效。
9.  自由软件基金会可能随时出版通用公共许可证的修改版或新版。新版和当前的
版本在原则上保持一致,但在提到新问题时或有关事项时,在细节上可能存在一定
差别。
    每一版本都有不同的版本号。如果程序指定适用于它的许可证版本号以及“任
何更新的版本”。你有权选择遵循指定的版本或自由软件基金会以后出版的新版本
,如果程序未指定许可证版本,你可选择自由软件基金会已经出版的任何版本。

10.  如果你愿意将程序的一部分结合到其它自由程序中,而它们的发布条件不同
。写信给作者,要求准予使用。如果是自由软件基金会加以版权保护的软件,写信
给自由软件基金会。我们有时会作为例外的情况处理。我们的决定受两个主要目标
的指导。这两个主要目标是:我们的自由软件的衍生作品继续保持自由状态。以及
从整体上促进软件的共享和重复利用。
没有担保
11.  由于程序准予免费使用,在适用法准许的范围内,对程序没有担保。除非另
有书面说明,版权所有者和/或其它提供程序的人们“一样”不提供任何类型的担
保。不论是明确的,还是隐含的。包括但不限于隐含的适销和适合特定用途的保证
。全部的风险,如程序的质量和性能问题都由你来承担。如果程序出现缺陷,你承
担所有必要的服务,修复和改正的费用。
12.  除非适用法或书面协议的要求,在任何情况下,任何版权所有者或任何按许
可证条款修改和发布程序的人们都不对你的损失负有任何责任。包括由于使用或不
能使用程序引起的任何一般的、特殊的、偶然发生的或重大的损失(包括但不限于
数据的损失,或者数据变得不精确,或者你或第三方的持续的损失,或者程序不能
和其它程序协调运行等)。即使版权所有者和其他人提到这种损失的可能性也不例
外。
如何将这些条款用到你的新程序
    如果你开发了新程序,而且你需要它得到公众最大限度的利用。要做到这一点
的最好办法是将它变为自由软件。使得每个人都能在遵守条款的基础上对它进行修
改和重新发布。
    为了做到这一点,给程序附上下列声明。最安全的方式是将它放在每个源程序
的开头,以便最有效地传递拒绝担保的信息。每个文件至少应有“版权所有”行以
及在什么地方能看到声明全文的说明。
         <用一行空间给出程序的名称和它用来做什么的简单说明>
          版权所有(C)         19XX            <作者姓名>
    这一程序是自由软件,你可以遵照自由软件基金会出版的GNU通用公共许可证
条款来修改和重新发布这一程序。或者用许可证的第二版,或者(根据你的选择)
用任何更新的版本。
    发布这一程序的目的是希望它有用,但没有任何担保。甚至没有适合特定目的
的隐含的担保。更详细的情况请参阅GNU通用公共许可证。
    你应该已经和程序一起收到一份GNU通用公共许可证的副本。如果还没有,写
信给:
    The Free Software Foundation, Inc.,  59  Temple Place,  Suite,
    330, Boston, MA02111-1307,  USA

    还应加上如何和你保持联系的信息。

    如果程序以交互方式进行工作,当它开始进入交互方式工作时,使它输出类似
下面的简短声明:
    Gnomovision   第69版,  版权所有(C)  19XX,   作者姓名,
    Gnomovision绝对没有担保。 要知道详细情况,请输入‘show w’。
    这是自由软件,欢迎你遵守一定的条件重新发布它,要知道详细情况,
    请输入‘show c’。

    假设的命令‘show w’和‘show c’应显示通用公共许可证的相应条款。当然
,你使用的命令名称可以不同于‘show w’和‘show c’。根据你的程序的具体情
况,也可以用菜单或鼠标选项来显示这些条款。

    如果需要,你应该取得你的上司(如果你是程序员)或你的学校签署放弃程序
版权的声明。下面只是一个例子,你应该改变相应的名称:
         Yoyodyne公司以此方式放弃James Hacker
         所写的 Gnomovision程序的全部版权利益。
         <Ty coon签名>,1989.4.1
         Ty coon付总裁

    这一许可证不允许你将程序并入专用程序。如果你的程序是一个子程序库。你
可能会认为用库的方式和专用应用程序连接更有用。如果这是你想做的事,使用
GNU库通用公共许可证代替本许可证。
回复

使用道具 举报

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

本版积分规则

GMT+8, 2025-2-15 05:32 , Processed in 0.207181 second(s), 15 queries .

© 2001-2025 Discuz! Team. Powered by Discuz! X3.5.

快速回复 返回顶部 返回列表