Linux内核源代码分析

rickopen

贡献于2011-02-12

字数:0 关键词: Linux

前言 本书着重于对 Linux 系统最新版本(2.4.0)内核源代码进行情景描述和情景分析。 什么是情景描述?什么是情景分析? 不妨以英语的教学为例。大家都知道,有一种很有效的方法是通过“情景会话”学习英语。例如, 去剧院问路要说些什么,去图书馆借书要说些什么,去餐馆吃饭遇上了熟人又说些什么,等等。每一个 这样的“情景”都是一个常见或常用的会话过程。以这样的一些情景为线索,沿着这些线索讲解“这是 被动时态”、“那是习惯用法”,就容易引起学习人的兴趣从而印象深刻,并且每学了这样一个情景就能够 实际运用。另外,由于来自现实生活的情景在语法、语义等方面不是单一的,在学习一个情景的时候通 常都会涉及到该语言种种不同的方面,通过一系列精心安排的情景会话的学习,就能对英语逐步地建立 比较全面的认识。事实上,就英语的学习而言,纯粹的系统化学习方法几乎是不现实的。事实上,很少 有人通过读字典来学单词,而都是结合课文来学,每篇课文实际上也是一个情景。当然,系统化的学习 还是要的,学了情景对话以后还要再系统地学习语法。但是无可否认的是,从情景对话入手学习英语比 从语法入手要有效得多。相信读者会有这方面的体会和经历。 现在来看 Linux 内核的学习。如果以若干经过精心安排的情景为线索,例如,打开一个文件的全过 程,执行一个可执行程序的全过程,从一个进程发送一个报文到另一个进程的过程等等,结合内核源代 码逐个加以讲解,并且在讲解过程中有针对性地介绍所涉及的数据结构和算法,读者就能得到对整个内 核的生动而深刻的理解。本书的宗旨之一就在于引导读者走过许多这样的“情景”,从而建立起对 Linux 内核的全面的认识。至于情景的安排,仍旧按照操作系统的原理分成若干章,例如存储管理、进程管理、 文件系统等等。在每一章中,除了必要的叙述以外,都挑选了若干重要的情景,结合源代码逐个加以讲 解。 本书使用的源代码,刚开始编写初稿时取自当时最新的 Linux 内核 2.3.38 版,后来经历 2.3.98 和 2.4.0 测试版,最后依据 2.4.0 正式版重新修改定稿。读者可以在相关的网站上自行下载该版本的全部源代码。 可以肯定,当读者看到本书时,甚至本书付印时,最新的版本已不再是 2.4.0 了。但不管怎样我们总得要 锁定一个版本,这就是 2.4.0。 一般情况下,分析操作系统源代码的专著和教材习惯上都是这样安排的:以主要数据结构的定义为 核心,以数据结构之间的联系为线索,内容则以对文件、模块和函数的功能描述为主,辅以若干函数中 的代码片段作为实例,以达到介绍、分析各种特定机制的目的。这种思路和安排基本上类似于先讲语法 规则后举一些例句的外语教学方法,它比较适合于只要求对内核和它的原理有粗略了解的读者,但对需 要深入理解内核或实际从事这方面工作的读者就未必合适。其实,这种安排对于初学者也未必是最好的。 不错,要理解一个操作系统的内在机制及其实现机理,当然需要理解主要数据结构的组成,了解数据结 构之间的联系,了解整个内核代码的模块划分、文件划分和功能分解,了解主要函数对有关数据结构操 作的大致逻辑流程。问题在于,怎样才能使读者和学生达到这些要求。根据我们多年来的切身体会,我 们决定从具体、鲜活的源代码入手作情景分析,在分析过程中逐步引入相关的数据结构和互相间的联系, 介绍具体函数的逻辑流程及其物理背景乃至代码作者的某些高超技巧,让读者和作者一起完成必要的抽 象过程,通过读者的思索,最后达到深入而全面的理解。 对于从事系统设计或实现的读者,源代码的阅读和理解是一项重要的基本功。写小说的人大多是读 了许多名著和文学评论以后,而不是读了“小说概论”以后才学到写作技巧,进而写出受读者喜爱的作 品。写程序的人又何尝不是如此。本书的目的之一就是为读者提供一些类似于文学评论的材料。另一方 面,源代码的阅读和理解也是必要的。在某种意义上,源代码本身既是最准确的说明书也是最权威的教 科书,因为由它所构成的系统切切实实在运行。我们自己就有这样的经历:学了一些原理和抽象的流程 就自以为懂了,可是拿到源代码一看却怎么也对不上号。于是下决心钻进去,花了九牛二虎之力才搞懂。 Linux 内核源代码还为计算机行业的工作人员树立了一个参照物。我们在工作中常常看到,人们(包括 我们自己)在碰到问题的时候往往会先想一想:这在 Linux(以前是 Unix)里面是怎样实现的?或者在 Linux 环境中能否实现?再查看一下有关的源代码,便有了主张。有时甚至就在源代码中找几个文件加 以裁减、修改,问题很快就解决了(但须遵守 GPL 中的有关规定)。诚然,Linux 内核源代码的阅读和理解是个艰苦的过程,最好能有些指导,有些帮助,而这正是我们写这本书的目的。 希望读者在每读完一章后能做两个小结。一个是关于数据结构组成和数据结构之间联系的小结,另 一个是关于执行过程以及函数调用关系的小结。读者为了完成这两个小结,可能不得不回过头去再读一 遍甚至几遍前面的内容。从内容的选定和编排的角度来说,最理想的当然是严格遵循“先说明后引用” 的原则,像平面几何那样建立起一个演绎体系。可是,对于一个实际的系统,特别是对于它的源代码, 这种完全线性的叙述和认识过程是不现实的。实际的认识过程是螺旋式的,这也决定了常常需要反复读 几遍才能理解。所以,对于一个操作系统的源代码,读到后面再返回前面,再读到后面又返回前面,这 几乎是必然的过程。真有决心深入了解 Linux 内核的读者应该有这个思想准备。我们相信,读者在读完 全书以后,如果闭目细想,一定会有一种在一个新到的城市中由向导陪同走街串巷,到过了大量的重要 景点,最后到了某个高楼之顶的旋转餐厅鸟瞰整个城市时常会有的那种心情。 由于篇幅的原因,全书分为上下两册。上册包括预备知识、存储管理、中断和系统调用、进程和进 程调度、文件系统以及传统的 Unix 进程间通讯,共六章。下册则分基于 socket 的进程间通讯、设备驱 动、多处理器 SMP 系统结构以及系统引导和初始化,共四章。上下两册不可分割,是一个有机的整体。 本书的题材决定了读者主要是中、高级计算机专业人员,以及大学有关专业的高年级学生和研究生。 但是,我们在写作中也尽量照顾到了非计算机专业的学生和初学者(因此程度高的读者有时候会觉得书 中有些讲解过于啰嗦)。一般而言,读者只要有一些操作系统和计算机系统结构方面的基础知识,并粗通 C 语言,就可以阅读本书。 就像软件免不了有错一样,对软件的理解和诠释也一定会有错误,人们能做的只是尽量减少错误。 我们可以负责任地说,本书付印前在文字中已经没有我们知道而没有改正的错误,更没有故意误导读者 的内容。但是,我们深知错误一定是有的,我们欢迎讨论,欢迎批评。 20 年前,本书的两位作者从不同的单位调入浙江大学计算机系,共事期间曾共同承担过若干计算机 应用项目的开发、研究。后来第一作者去了美国,目前在美国定居,继续从事计算机专业的工作;第二 作者已从学校退休,目前受聘在杭州恒生电子股份有限公司任职。出于难忘的友情和其他一些难以割舍 的情结(包括对 Unix 和 Linux 的共同爱好),两年前通过越洋电话商定要合作写几本有关 Linux 的书, 此刻在读者手上的就是其中的第一本,其余的就要看条件是否允许了。 从成书到出版,曾得到了陈大中、曾抗生、金通洸、俞瑞钊几位教授和其他许多国内朋友的鼓励和 支持,作者感谢他们。特别要提到的是,恩师何志钧教授在过去和现在都给了作者许多的关心和帮助, 令作者终生难忘,本书的出版在某种意义上也是对恩师的一次汇报,同时表示由衷的感谢。 作者还要感谢恒生电子股份有限公司黄大成总裁、彭政纲董事长以及其他领导人对作者,特别是第 二作者开展 Linux 研究的支持。他们为第二作者提供了很好的工作条件,使其在工作之余继 Unix 之后还 能再从事 Linux 技术的研究。作为国内著名软件企业的决策者,他们对软件核心技术的敏锐眼光以及采 用最新技术为我国金融证券行业开发全新的大型应用软件的战略决策,令人钦佩。感谢他们,祝愿他们 取得更大的成功。 最后,还要感谢谢敏、王红女、章西、李清瑜几位小姐,他们在承担公司繁重工作的同时利用业余 时间为本书文稿的录入和整理付出了大量的辛勤劳动。 本书的出版,像任何其他技术专著一样,除了错误之外总还会有许多不尽人意的地方,欢迎国内外 的专家和本书读者给我们指出,以便改进。 毛德操(Decao Mao) 19 Orchard Hill Road, Newtown, CT 06470 USA 胡希明 杭州恒生电子股份有限公司 杭州市解放路 138 号纺织大厦 11F(310009) 2001 年 5 月 1 日目录 1. 预备知识 ........................................................................................................................................................ 7 1.1. Linux内核简介 ........................................................................................................................................ 7 1.2. Intel X86 CPU系列的寻址方式............................................................................................................ 12 1.3. i386的页式内存管理机制..................................................................................................................... 17 1.4. Linux内核源代码中的C语言代码........................................................................................................ 20 1.5. Linux内核源代码中的汇编语言代码................................................................................................... 25 2. 存储管理 ...................................................................................................................................................... 33 2.1. Linux内存管理的基本框架 .................................................................................................................. 33 2.2. 地址映射的全过程................................................................................................................................ 37 2.3. 几个重要的数据结构和函数................................................................................................................ 43 2.4. 越界访问................................................................................................................................................ 55 2.5. 用户堆栈的扩展.................................................................................................................................... 59 2.6. 物理页面的使用和周转........................................................................................................................ 67 2.7. 物理页面的分配.................................................................................................................................... 77 2.8. 页面的定期换出.................................................................................................................................... 90 2.9. 页面的换入.......................................................................................................................................... 124 2.10. 内核缓冲区的管理 ....................................................................................................................... 131 2.11. 外部设备存储空间的地址映射.................................................................................................... 154 2.12. 系统调用brk() ...............................................................................................................................160 2.13. 系统调用mmap()........................................................................................................................... 178 3. 中断、异常和系统调用 ............................................................................................................................ 190 3.1. X86 CPU对中断的硬件支持 .............................................................................................................. 190 3.2. 中断向量表IDT的初始化 ................................................................................................................... 194 3.3. 中断请求队列的初始化...................................................................................................................... 200 3.4. 中断的响应和服务.............................................................................................................................. 206 3.5. 软中断和Bottom Half.......................................................................................................................... 217 3.6. 页面异常的进入和返回...................................................................................................................... 227 3.7. 时钟中断.............................................................................................................................................. 230 3.8. 系统调用.............................................................................................................................................. 237 3.9. 系统调用号和跳转表.......................................................................................................................... 248 4. 进程与进程调度 ........................................................................................................................................ 255 4.1. 进程四要素.......................................................................................................................................... 255 4.2. 进程三步曲:创建、执行与消亡...................................................................................................... 266 4.3. 系统调用fork()、vfork()与clone()...................................................................................................... 270 4.4. 系统调用execve() ................................................................................................................................ 294 4.5. 系统调用exit()与wait4()...................................................................................................................... 323 4.6. 进程的调度与切换.............................................................................................................................. 341 4.7. 强制性调度.......................................................................................................................................... 361 4.8. 系统调用nanosleep()和pause()............................................................................................................ 367 4.9. 内核中的互斥操作.............................................................................................................................. 379 5. 文件系统 .................................................................................................................................................... 396 5.1. 概述...................................................................................................................................................... 396 5.2. 从路径名到目标节点.......................................................................................................................... 410 5.3. 访问权限与文件安全性...................................................................................................................... 446 5.4. 文件系统的安装和拆卸...................................................................................................................... 466 5.5. 文件的打开与关闭.............................................................................................................................. 512 5.6. 文件的写与读...................................................................................................................................... 550 5.7. 其他文件操作...................................................................................................................................... 607 5.8. 特殊文件系统/proc.............................................................................................................................. 620 6. 传统的Unix进程间通信 ............................................................................................................................ 649 6.1. 概述...................................................................................................................................................... 649 6.2. 管道和系统调用pipe() ........................................................................................................................ 650 6.3. 命名管道.............................................................................................................................................. 669 6.4. 信号...................................................................................................................................................... 675 6.5. 系统调用ptrace()和进程跟踪.............................................................................................................. 712 6.6. 报文传递.............................................................................................................................................. 728 6.7. 共享内存.............................................................................................................................................. 755 6.8. 信号量.................................................................................................................................................. 779 7. 基于socket的进程间通信 .......................................................................................................................... 791 7.1. 系统调用socket()................................................................................................................................. 791 7.2. 函数sys_socket()——创建插口.......................................................................................................... 799 7.3. 函数sys_bind()——指定插口地址 ..................................................................................................... 812 7.4. 函数sys_listen()——设定server插口.................................................................................................. 822 7.5. 函数sys_accept()——接收连接请求.................................................................................................. 823 7.6. 函数sys_connect()——请求连接........................................................................................................ 833 7.7. 报文的接收与发送.............................................................................................................................. 849 7.8. 插口的关闭.......................................................................................................................................... 884 7.9. 其他...................................................................................................................................................... 898 8. 设备驱动 .................................................................................................................................................... 900 8.1. 概述...................................................................................................................................................... 900 8.2. 系统调用mknod()................................................................................................................................ 906 8.3. 可安装模块.......................................................................................................................................... 913 8.4. PCI总线................................................................................................................................................ 954 8.5. 块设备的驱动.................................................................................................................................... 1018 8.6. 字符设备驱动概述............................................................................................................................ 1091 8.7. 终端设备与汉字信息处理................................................................................................................ 1094 8.8. 控制台的驱动.................................................................................................................................... 1123 8.9. 通用串行外部总线USB .................................................................................................................... 1171 8.10. 系统调用select()以及异步输入/输出......................................................................................... 1319 8.11. 设备文件系统devfs..................................................................................................................... 1337 9. 多处理器SMP系统结构 .......................................................................................................................... 1354 9.1. 概述.................................................................................................................................................... 1354 9.2. SMP结构中的互斥问题.................................................................................................................... 1357 9.3. 高速缓存与内存的一致性................................................................................................................ 1361 9.4. SMP结构中的中断机制.................................................................................................................... 1368 9.5. SMP结构中的进程调度.................................................................................................................... 1380 9.6. SMP系统的引导................................................................................................................................ 1386 10. 系统引导和初始化 .................................................................................................................................. 1405 10.1. 系统引导过程概述 ..................................................................................................................... 1405 10.2. 系统初始化(第一阶段).......................................................................................................... 1408 10.3. 系统初始化(第二阶段).......................................................................................................... 1423 10.4. 系统初始化(第三阶段).......................................................................................................... 1457 10.5. 系统的关闭和重引导.................................................................................................................. 1473 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 6 页,共 1482 页 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 7 页,共 1482 页 1. 预备知识 1.1. Linux内核简介 在计算机技术的发展史上,Unix 操作系统的出现是一个重要的里程碑。早期的 Unix 曾免费供美国 及一些西方国家的大学和科研机构使用,并且提供源代码。这一方面为高校和科研机构普及使用计算机 提供了条件;另一方面,也是更重要的,为计算机的核心技术“操作系统”的教学和实验提供了条件。 特别是 Unix 第 6 版的源代码,在相当长的一段时间内是大学计算机系高年级学生和研究生使用的教材, 甚至可以说,美国当时整整一代的计算机专业人员都是读着 Unix 的源代码成长的。反过来,这也促进了 Unix 的普及和发展,并且在当时形成了一个 Unix 产业。事实上,回顾硅谷的形成和发展,也可以看到 Unix 起着重要的作用。Unix 两大主流之一的 BSD 就是在加州大学伯客利分校开发的。后来,Unix 成了 商品,其源代码也受到了版权的保护,再说也日益复杂和庞大了,而第 6 版则慢慢变得陈旧了,便逐渐 不再用 Unix 内核的源代码作为教材了(但是直到现在还有在用的)。 在这种情况下,出于教学的需要,荷兰的著名教授 Andrew S. Tanenbaum 编写了一个小型的“类 Unix” 操作系统 Minix,在 PC 机上运行,其源代码在 20 世纪 80 年代后期和 90 年代前期曾被广泛采用。但是, Minix 虽说是“类 Unix”,其实离 Unix 相当远。首先,Minix 是个所谓“微内核”,与 Unix 内核属于不 同的设计,功能上更是不可同日而语。再说 Unix 也不仅仅是内核,还包括了其“外壳”Shell 和许多工 具性的“实用程序”。如果内核提供的支持不完整,就不能与这些成分结合起来形成 Unix 环境。这样, Minix 虽然不失为一个不错的教学工具,却缺乏实用价值。看到 Minix 的这个缺点后,当时的一个芬兰 学生 Linus Torvalds 就萌生了一个念头,即组织一些人,以 Minix 为起点,基本上按照 Unix 的设计,并 且博采各种版本之长,在 PC 机上实现,开发出了一个真正可以实用的 Unix 内核。这样,公众就既有免 费的(现代)Unix 系统,又有系统的源代码,且不存在版权问题。可是,Tanenbaum 教授的目光却完全 盯在教学上,因此并不认为这是一个好主意,没有采纳这个建议。 毕竟是“初生牛犊不怕虎”,加上自身的天赋和勤奋,还有公益心,Linus Torvalds 就自己动手干了 起来。由于所实现的基本上是 Unix,Linus Torvalds 就把它称为 Linux。那时候互联网虽然还不像现在这 么普及,但是大学和公司中已经用得很多了。Linus Torvalds 在基本完成了 Linux 内核的第一个版本以后 就把它放在了互联网上,一来把自己写的代码公诸于众,二来是邀请有兴趣的人也来参与。他的这种做 法很快便引起热烈的反应,并且与美国“自由软件基金会”FSF 的主张正好不谋而合。 当时 FSF 已经有计划要开发一个类 Unix(但又不是 Unix,所以称为 GNU,这是“Gnu is Not Unix” 的缩写)的操作系统和应用环境,而 Linux 的出现正是适得其时,适得其所。于是,由 Linus Torvalds 主持的 Linux 内核的开发、改进与维护,就成了 FSF 的主要项目之一。同时,FSF 的其他项目,如 GNU 的 C 编译 gcc、程序调试工具 gdb,还有各种 Shell 和实用程序,乃至 Web 服务器 Apache、浏览器 Mozilla (实际上就是 Netscape)等等,则正好与之配套成龙。人们普遍认为自由软件的开发是软件领域中的一 个奇迹。这么多志愿者参与,只是通过互联网维持松散的组织,居然能有条不紊地相互配合,开发出高 质量的而且又是难度较大的系统软件,实在令人赞叹。 那么,Linux 与它的前身 Minix 的区别何在呢?简单地说,Minix 是个“微内核”,而 Linux 是个“宏 内核”;Minix 是个类 Unix 的教学用模型,而 Linux 基本上就是 Unix,而且是 Unix 的延续和发展,甚至 是各种 Unix 版本与变种的集大成者。 大家知道,传统意义下的操作系统,其内核应具备多个方面的功能或成分,既包含用于管理属于应 用层的“进程”的成分,如进程管理,也包含为这些进程提供各种服务的成分,如进程间通信、设备驱 动和文件系统等等。内核中提供各种服务的成分与使用这些服务的进程之间实际上就形成了一种典型的 “Client/Server”的关系。其实,这些服务提供者并不一定非得都留在内核中不可,他们本身也可以被设 计并实现为某些“服务进程”,其中必须要留在内核中的成分其实只有进程间通信。如果把这些服务提供 者从内核转移到进程的层次上,那么内核本身的结构就可以大大减小和简化。而各个服务进程,既然已Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 8 页,共 1482 页 从内核中游离出来,便可以单独地设计、实现和调试,更重要的是可以按实际的需要来配置和启动。基 于这种想法,各种“微内核”(Micro-Kernel)便应运而生。特别对于一些专用的系统,主要是实时系统 和“嵌入式”系统(Embedded System),微内核的思想就很有吸引力。究其原因,主要是因为通常这些 系统都不带磁盘,整个系统都必须放在 EPROM 中,常常受到存储空间的限制,而所需要的服务又比较 单一和简单。所以,几乎所有的嵌入式系统和实时系统都采用微内核,如 PSOS,VxWorks 等。当然, 微内核也有缺点,将这些服务的提供都放在进程层次上,再通过进程间通信(通常是报文的传递)提供 服务,势必增加系统的运行开销,降低了效率。 与微内核相对应,传统的内核结构就称为“宏内核”(Macro-Kernel),或称为“一体化内核”(Monolithic Kernel)。通用式系统由于所需的服务面广而量大,一体化内核就更为合适。作为一种通用式系统,Linux 采用一体化内核是很自然的事。 传统的 Unix 内核是“全封闭”的。如果要往内核中加一个设备(增加一个服务),早期一般的做法 是编写这个设备的驱动程序,并变动内核源程序中的某些数据结构(设备表),再重新编译整个内核。并 重新引导整个系统。这样做当然也有好处,如系统的安全性更能得到保证,但其缺点也是很明显的,那 就是太僵化了。在这样的情况下,当某一个公司开发出一种新的外部设备时(比方说,一台彩色扫描仪), 它就不可能随同这新的设备提供一片软盘和光盘给用户,使得用户只要运行一下“setup”就可以把这个 设备安装上了(像对 Dos/Windows 那样)。有能力修改 Unix 内核的设备表,并重新编译内核的用户毕竟 不多。 在 Linux 里,这个问题解决得比较好。Linux 既允许把设备驱动程序在编译时静态地连接在内核中, 一如传统的驱动程序那样;也允许动态地在运行时安装,称为“模块”;还允许在运行状态下当需要用到 某一模块时由系统自动安装。这样的模块仍然在内核中运行,而不是像在微内核中那样作为单独的进程 运行,所以其效率还是可以得到保证。模块,也就是动态安装的设备驱动程序的实现(详见设备驱动程 序一章),是很大的改进。它使 Linux 设备驱动程序的设计、实现、调试以及发布都大大地简化,甚至可 以说是发生了根本性的变化。 Linux 最初是在 Intel 80386“平台”上实现的,但是已经移植到各种主要的 CPU 系列上,包括 Alpha、 M68K、MIPS、SPARC、Power PC 等等(Pentium、Pentium II 等等均属于 i386 系列)。可以说 Linux 内 核是现今覆盖面最广的一体化内核。同时,在同一个系列的 CPU 上,Linux 还支持不同的系统结构,它 既支持常规的单 CPU 结构,也支持多 CPU 结构。不过,本书将专注于 i386 CPU,并且以单 CPU 结构 为主,但是最后有一章专门讨论多 CPU 结构。 在安装好的 Linux 系统中,内核的源代码位于/usr/src/linux。如果是从 GNU 网站下载的 Linux 内核 的 tar 文件,则展开后在一个叫 linux 的子目录中。以后本书中谈到源文件路径时,就总是从 linux 这个 节点开始。Linux 源代码的组成,大体如下所示。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 9 页,共 1482 页 linux ├─COPYING 有关FSF公共许可证制度GPL的具体说明 ├─README Linux内核安装和使用的简要说明 ├─Makefile 重构Linux内核可执行代码的make文件 ├─Documentation 有关Linux内核的文档 ├─arch arch是architecture一词的缩写,内核中与具体CPU和系统结构相 │ │ 关的代码分别放在下一层的子目录中,而相关的.h文件则分别放在 │ │ include/asm目录之下 │ ├─alpha -- 原DEC开发的64位CPU │ ├─i386 -- 包括X86系列中自80386以后的所有32位CPU,包括 │ │ 80486,Pentium,Pentium II,等等,也包括AMD K6等兼 │ │ 容系列 │ ├─m68k -- 由Motorola开发的68000系列 │ ├─mips -- RISC CPU芯片 │ ├─sparc -- RISC CPU芯片,主要用于Sun工作站等机型中 │ ├─s390 -- IBM生产的一种大型计算机 │ └─ia64 -- Intel的IA-64结构64位CPU │ │ 说明:在每个CPU的子目录中,又进一步分解为boot,mm,kernel等子目录, │ 分别包含于系统引导、内存管理、系统调用的进入和返回、中断处理以 │ 及其他内核中依赖于CPU和系统结构的底层代码。这些代码有些是汇 │ 编代码,但主要还是C代码 │ ├─drivers 设备驱动程序,包括各种块设备和字符设备的驱动程序 ├─fs 文件系统,每个目录分别对应一个特定的文件系统,还有一些共 │ 同的源程序则用于"虚拟文件系统"vfs ├─include 包含了所有的.h文件。如arch子树一样,在include中也是为各种CPU │ 都专设一个子目录,而通用的子目录asm则根据系统的配置而"符号连 │ 接"到具体的CPU的专用子目录,如asm i386、asm m68k等。除此之外, │ 还有通用的子目录Linux、net等 ├─init Linux内核的main()及其初始化过程,包括main.c,version.c等文件 ├─ipc Linux内核的进程间通信,包括util.c,sem.c,shm.c,msg.c等文件 ├─kernel 进程管理和调度,包括sched.c,fork.c,exit.c,signal.c,sys.c,time.c │ resource.c,dma.c,softirq.c,itimer.c等文件 ├─lib 通用的工具性子程序,如对出错信息的处理等等 ├─mm 内存管理,即虚存管理,包括swap.c,swapfile.c,page_io.c,page_alloc.c, │ swap_state.c,vmscan.c,kmalloc.c,vmalloc.c,memory.c,mmap.c等文件 ├─net 包含了各种不同网卡和网络规程的驱动程序 └─scripts 用于系统配置的命令文件 值得一提的是,Linux 的源代码看似庞大,其实对于每一个具体的内核而言并不是所有的.c 和.h 文 件都会用到,而是在编译(包括连接)时根据系统的配置有选择地使用。例如,虽然源代码中包含了用 来支持各种不同 CPU 的代码,但编译以后每一个具体的内核都只是针对一种特定 CPU 的。再如,在 net 子目录下包含了各种网卡的驱动程序,但实际上通常只会用到一种网卡,而且各种网卡的驱动程序实际 上大同小异。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 10 页,共 1482 页 在结束本节之前,还要介绍一下有关 Linux 内核版本的一些规定。 通常,在说到 Linux 时,是指它的内核加上运行在内核之上的各种管理程序和应用程序。严格地说, 内核只是操作系统的一部分,即其核心部分。但是,人们往往把 Linux 的内核称为 Linux,所以在讲到 Linux 时有时候是指整个操作系统,有时候是指其内核,要根据上下文加以区分。在本书中,如无特别 说明,则 Linux 通常是指其内核。 Linux 内核的版本在发行上有自己的规则,可以从其版本号加以识别。版本号的格式为“x.yy.zz”。 其中 x 介于 0 到 9 之间,而 yy、zz 则介于 0 到 99 之间。通常数字愈高版本愈新。一些版本号后面有时 会见到 pNN 的字样,NN 是介于 0 到 20 之间的数字。它代表对某一版本的内核“打补丁”或修订的次 数。如 0.99p15,代表这是对版本 0.99 的内核的第 15 次修订。 由于 Linux 源代码的开放性,公众随时都可以从网上下载最新的版本,包括还在开发中、尚未稳定、 因而还不能发行的版本,因此,需要有一套编号的方案,使用户看到一个具体的版本号就可以知道是属 于“发行版”还是“开发版”,所以 Linux 内核的版本编号是有规则的。在版本号 x.yy.zz 中,x 的不同号 码标志着内核在设计或实现上的重大改变,yy 一方面表示版本的变迁,一方面标志着版本的种类,即“发 行版”或“开发版”。如果 yy 为偶数便表示是一个相对稳定、已经发行的版本。开发中的版本一旦通过 测试以及试运行,证明已经稳定下来,就可能会发布一个 yy 的值为偶数的发行版本。之后开发者们又将 创建下一个新的开发版本。但是有时候会在经历了几个开发版本以后才发布一个发行版。至于 zz,则代 表着在内核增加的内容不是很多、改动不是很大时的变迁,只能算是同一个版本。例如,版本由 2.0.34 升级到 2.0.35 只意味着版本 2.0.34 中的一些小缺陷被修复,或者代码有了一些小的改变。“发行版”和“开 发版”的 zz 编号是独立编号的,因此没有固定的对应关系。例如,当开发版 2.3 的版本号达到 2.3.99 时, 相应的发行版还只是 2.2.18。 Linux 内核的 0.0.2 版在 1991 年首次公开发行,2.2 版在 1999 年 1 月发行。Linux 内核的改进是相当 频繁的,几乎每个月都在变。本书最初采用的是 2.3.28 版,最后成书付印时则以正式发行的 2.4.0 版为依 据。 Linux 的内核基本上只有一种来源,那就是由 Linus 主持开发和维护的内核版本。但是有很多公司在 发行 Linux 操作系统不同的发行版(distribution),如 Red Hat、Caldera 等等。虽然不同的发行版本中所 采用的内核在版本上有所不同,但其来源基本一致。各发行版本的不同之处一般表现在安装程序、安装 界面、软件包的多少、软件包的安装和管理方式等方面,在特殊情况下也有对内核代码稍作修改的(如 汉化)。不同的发行版由不同的发行商提供服务。不同的发行商对自己所发行版本的定位也有不同,各厂 商所能提供的售后服务、技术支持也各不相同。由此可见,原则上全世界只有一个 Linux,所谓“某某 Linux”只是它的一种发行版本和修订版本。另外,不要把 Linux 内核的版本与发行商自己的版本(如“Red Hat 6.0”)混淆,例如,Caldera2.2 版的内核是 2.2.5 版。 对于大多数用户,由于发行商提供的这些发行版本起着十分重要的作用。让用户自行配置和生成整 个系统是相当困难的,因为那样用户不但要自己下载内核源程序,自己编译安装,还要从不同的 FTP 站 点下载各种自由软件添加到自己的系统中,还要为系统加入各种有用的工具,等等。而所有这些工作都 是很费时费力的事情。Linux 的发行厂商正是看到了这一点,替用户做了这些工作,在内核之上集成了 大量的应用软件。并且,为了安装软件,发行厂商同时还提供了用于软件安装的工具性软件,以利于用 户安装管理。由于组织新的发行版时并没有一个统一的标准,所以不同的厂商的发行版各有特色也各有 不足。 Linux 内核的终极的来源虽然只有一个,但是可以为其改进和发展作出贡献的志愿者人数却并无限 制。同时,考虑到一些特殊的应用,一些开发商或机构往往对内核加以修改和补充,形成一些针对特殊 环境或要求的变种。例如,针对“嵌入式”系统的要求,有人就开发出 Embedded Linux;针对有“硬实 时”要求的系统,有人就开发了 RT-Linux;针对手持式计算机的要求,有人就开发出了 Baby Linux 等等。 当然,中文 Linux 也是其中的一类。每当有新的 Linux 内核版本发布时,这些变种版本通常也很快就会 推出相应的新版本。根据 FSF 对自由软件版权的规定(GPL),这些变种版本对内核的修改与补充必须 公开源代码。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 11 页,共 1482 页 许多人认为,既然 Linux 是免费的公共软件,那就无所谓版权的问题了。其实不然,Linux 以及 Linux 内核源代码,是有版权保护的,只不过这版权归公众(或者说全人类)所有,由自由软件基金会 FSF 管 理。FSF 为所有的 GNU 软件制定了一个公用许可证制度,称为 GPL(General Public License),也叫 Copyleft,这是与通常所讲的版权即 Copyright 截然不同的制度。Copyright 即通常意义下的版权,保护作 者对其作品及其衍生品的独占权,而 Copyleft 则允许用户对作品进行复制、修改,但要求用户承担 GPL 规定的一些义务。按照 GPL 规定,允许任何人免费地使用 GNU 软件,并且可以用 GNU 软件的源代码 重构可执行代码。进一步,GPL 还允许任何人免费地取得 GNU 软件及其源代码,并且再加以发布甚至 出售,但必须要符合 GPL 的某些条款。简而言之,这些条款规定 GNU 软件以及在 GNU 软件的基础上 加以修改而成的软件,在发布(或转让、出售)时必须要申明该软件出自 GNU(或者源自 GNU),并且 必须要保证让接收者能够共享源代码,能从源代码重构可执行代码。换言之,如果一个软件是在 GNU 源代码的基础上加以修改、扩充而来的,那么这个软件的源代码就也必须对使用者公开(注意产品的出 售与源代码的公开并不一定相矛盾)。通过这样的途径,自由软件的阵容就会像滚雪球一样越滚越大。不 过,如果一个软件只是通过某个 GNU 软件的用户界面(API)使用该软件,则不受 GPL 条款的约束或 限制。总之,GPL 的主要目标是:使自由软件及其衍生产品继续保持开放状态,从整体上促进软件的共 享和重复使用。具体到 Linux 的内核来说,如果你对内核源代码的某些部分作了修改,或者在你的程序 中引用了 Linux 内核中的某些段落,你就必须加以申明并且公开你的源代码。但是,如果你开发了一个 用户应用程序,只是通过系统调用的界面使用内核,则你自己拥有完全的知识产权,不受 GPL 条款的限 制。 应该说,FSF 的构思是很巧妙也是很合理的,其目的也是很高尚的。 说到高尚,此处顺便多说几句。美国曾经出版过两本很有些影响的书,一本叫 Undocumented DOS, 另一本叫 Undocumented Windows,两本书均被列入 DOS/Windows 系统程序员的必备工具书。在这两本 书中,作者们(Andrew Schulman,David Maxey 以及 Matt Pietrek 等)一一列举了经过他们辛勤努力才 破译和总结出来的 DOS/Windows API(应用程序设计界面)实际上提供了但却没有列入 Microsoft 技术 资料的许多有用(而且重要)的功能。作者们认为,Microsoft 没有将这些功能收入其技术资料的原因是 无法用疏忽或遗漏加以解释的,而只能是故意向用户隐瞒。Microsoft 既是操作系统的提供者,同时又是 一个应用程序的开发商,通过向其他的应用程序开发商隐瞒一些操作系统界面上的关键技术,就使那些 开发商无法与 Microsoft 公平竞争,从而使 Microsoft 可以通过对关键技术的垄断达到对 DOS/Windows 应用软件市场的垄断。作者们在书中指责 Microsoft 这样做不仅有道德上的问题,也有法律上的问题。是 否涉及法律问题姑且不论,书中所列的功能确实都是存在的,可以通过实验证实,也确实没有写入 Microsoft 向客户提供的技术资料。 要是将 FSF 与 Microsoft 放在一起,则二者恰好成为鲜明的对比。差别之大,读者不难作出自己的 结论。 GPL 的正文包含在一个叫 COPYING 的文件中。在通过光盘安装的 Linux 系统中,该文件的路径名 为/usr/src/linux/COPYING。而在下载的 Linux 内核 tar 文件中,经过解压后该文件在顶层目录中。有兴趣 或者有需要的读者可以(而且应该)仔细阅读。 1.2. Intel X86 CPU系列的寻址方式 Intel 可以说是资格最老的微处理器芯片制造商了,历史上的第一个微处理器芯片 4004 就是 Intel 制 造的,所谓 X86 系列,是指 Intel 从 16 位微处理器 8086 开始的整个 CPU 芯片系列,系列中的每种型号 都保持与以前的各种型号兼容,主要是 8086、8088、80186、80286、80386、80486 以及以后各种型号的 Pentium 芯片。自从 IBM 选择 8088 用于 PC 个人计算机以后,X86 系列的发展就与 IBM PC 及其兼容机 的发展休戚相关了。其中 80186 并不广为人知,就与 IBM 当初决定停止在 PC 机中使用 80186 有关。限 于篇幅,本书不对这个系列的系统结构作全面的介绍,而只是结合 Linux 内核的存储管理对其寻址方式 作一些简单的说明。 在 X86 系列中,8086 和 8088 是 16 位处理器,而从 80386 开始为 32 位处理器,80286 则是该系列Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 12 页,共 1482 页 从 8088 到 80386,也就是从 16 位到 32 位过渡时的一个中间步骤。80286 虽然仍是 16 位处理器,但是在 寻址方式上开始了从“实地址模式”到“保护模式”的过渡。 当我们说一个 CPU 是“16 位”或“32 位”时,指的是处理器中“算术逻辑单元”(ALU)的宽度。 系统总线中的数据线部分,称为“数据总线”,通常与 ALU 具有相同的宽度(但有例外)。那么“地址 总线”的宽度呢?最自然的地址总线宽度是与数据总线一致的。这是因为从程序设计的角度来说,一个 地址,也就是一个指针,最好是与一个整数的长度一致。但是,如果从 8 位 CPU 寻址能力的角度来考虑, 则实际上是不现实的,因为一个 8 位的地址只能用来寻访 256 个不同的地址单元,这显然太小了。所以, 一般 8 位 CPU 的地址总线都是 16 位的。这也造成了一些 8 位 CPU 在内部结构上的一些不均匀性,在 8 位 CPU 的指令系统中常常会发现一些实际上是 16 位的操作。当 CPU 的技术从 8 位发展到 16 位的时候, 本来地址总线的宽度是可以跟数据总线一致了,但当时人们已经觉得由 16 位地址所决定的地址空间 (64K)还是太小,还应该加大。加到多大呢?结合当时人们所能看到的微型机的应用前景,以及存储 器芯片的价格,Intel 决定采用 1M,也就是说 64K 的 16 倍,那时觉得应该是足够了。确实,1M 字节的 内存空间在当时已经很使一些程序员激动不已了,那时候配置齐全的小型机,甚至大型机也只不过是 4M 字节的内存空间。在计算机的发展史上,几乎每一个技术决策,往往很快就被事后出现的事实证明是估 计不足的。 既然 Intel 决定了在其 16 位 CPU,即 8086 中采用 1M 字节的内存地址空间,地址总线的宽度也就相 应地确定了,那就是 20 位。这样,一个问题就摆在了 Intel 的设计人员面前:虽然地址总线的宽度是 20 位,但 CPU 中 ALU 的宽度却只有 16 位,也就是说可以直接加以运算的指针的长度是 16 位的。如何来 填补这个空隙呢?可能的解决方案有很多种。例如,可以像一些 8 位 CPU 中那样,增设一些 20 位的指 令专用于地址运算和操作,但是那样又会造成 CPU 内部结构的不均匀性。再如,当时的 PDP-11 小型机 也是 16 位的,但是结合其 MMU(内存管理单元)可以将 16 位的地址映射到 24 位的地址空间。结果, Intel 设计了一种当时看来还不失巧妙的方法,即分段的方法。 Intel 在 8086 CPU 中设置了四个“段寄存器”:CS、DS、SS 和 ES,分别用于可执行代码即指令、 数据、堆栈和其他。每个段寄存器都是 16 位的,对应于地址总线中的高 16 位。每条“访内”指令中的 “内部地址”都是 16 位的,但是在送上地址总线之前都在 CPU 内部自动地与某个段寄存器中的内容相 加,形成一个 20 位的实际地址。这样,就实现了从 16 位内部地址到 20 位实际地址的转换,或者“映射”。 这里要注意段寄存器中的内容对应于 20 位地址总线中的高 16 位,所以在相加时实际上是拿内部地址中 的高 12 位与段寄存器中的 16 位相加,而内部地址中的低 4 位保留不变。这个方法与操作系统中的“段 式内存管理”相似,但并不完全一样,主要是没有地址空间的保护机制。对于每个段寄存器的内容确定 的“基地址”,一个进程总是能够访问从次开始的 64K 字节的连续地址空间,而无法加以限制。同时, 可以用来改变段寄存器内容的指令也不是什么“特权指令”,也就是说,通过改变段寄存器的内容,一个 进程可以随心所欲地访问内存中的任何一个单元,而丝毫不受限制。不能对一个进程的内存访问加以限 制,也就谈不上对其他进程以及系统本身的保护。与此相应,一个 CPU 如果缺乏对内存访问的限制,或 者说保护,就谈不上什么内存管理,也就谈不上是现代意义上的中央处理器。由于 8086 的这种内存寻址 方式缺乏对内存空间的保护,所以为了区别于后来出现的“保护模式”,就称为“实地址模式”。 显然,在实地址模式上是无法建造现代意义上的“操作系统”的。 针对 8086 的这种缺陷,Intel 从 80286 开始实现其“保护模式”(Protected Mode,但是早期的 80286 只能从实地址模式转入保护模式,却不能从保护模式转回实地址模式)。同时,不久以后 32 位的 80386 CPU 也开发成功了。这样,从 8088/8086 到 80386 就完成了一次从比较原始的 16 位 CPU 到现代的 32 位 CPU 的飞跃,而 80286 则变成这次飞跃的一个中间步骤。从 80386 以后,Intel 的 CPU 历经 80486、 Pentium、Pentium II 等等型号,虽然在速度上提高了好几个量级,功能上也有不小的改进,但基本上属 于同一种系统结构中的改进与加强,而并无重大的质的改变,所以称为 i386 结构,和 i386 CPU。下面我 们将以 80386 为背景,介绍 i386 系列的保护模式。 80386 是个 32 位 CPU,也就是说它的 ALU 数据总线是 32 位的。我们在前面说过,最自然的地址总 线宽度是与数据线一致。当地址总线宽度达到 32 位时,其寻址能力达到了 4G(4 千兆),对于内存来说Linux 内核源代码情景分析 似乎是足够了。所以,如果新设计一个 32 位 CPU 的话,其结构应该是可以做到很简洁、很自然的。但 是,80386 却无法做到这一点。作为一个产品系列中的一员,80386 必须维持那些段寄存器,还必须支持 实地址模式,在此同时又要能支持保护模式。而保护模式是完全另搞一套,还是建立在段寄存器的基础 上以保持风格上的一致,而且还能节约 CPU 内部的资源呢?这对于 Intel 的设计人员无疑又是一次挑战。 Intel 选择了在段寄存器的基础上构筑保护模式的构思,并且保留段寄存器为 16 位(这样可以利用 原有的四个段寄存器),但是却又添加了两个段寄存器 FS 和 GS。为了实现保护模式,光是用段寄存器 来确定一个基地址是不够的,至少还得要有一个地址段的长度,并且还需要一些其他的信息,如访问权 限之类。所以,这里需要的是一个数据结构,而并非一个单纯的基地址。对此,Intel 设计人员的基本思 路是:在保护模式下改变段寄存器的功能,使其从一个单纯的基地址(变相的基地址)变成指向这样一 个数据结构的指针。这样,当一条内存指令发出一个内存地址时,CPU 就可以这样来归纳出实际上应该 放上数据总线的地址: 1. 根据指令的性质来确定应该使用哪一个段寄存器,例如转移指令中的地址在代码段,而取数据 指令中的地址在数据段。这一点与实地址模式相同。 2. 根据段寄存器的内容,找到相应的“地址段描述结构”。 3. 从地址段描述结构中得到基地址。 4. 将指令发出的地址作为位移,与段描述结构中规定的段长度相比,看看是否越界。 5. 根据指令的性质和段描述符中的访问权限来确定是否越权。 6. 将指令中发出的地址作为位移,与基地址相加而得出实际的“物理地址”。 虽然段描述结构存储在内存中,在实际使用时却将其装载入 CPU 中的一组“影子”结构,而 CPU 在运行时则使用其在 CPU 中的“影子”。从“保护”的角度考虑,在由(指令给出的)内部地址(或者 说“逻辑地址”)转换成物理地址的过程中,必须要在某个环节上对访问权限进行比对,以防止不具备特 权的用户程序通过玩弄某些诡计(例如修改段寄存器的内容,修改段描述符的内容等),得以非法访问其 他进程的空间或系统空间。 明白了这个思路,80386 的段式内存管理机制就比较容易理解了(还是很复杂)。下面就是此机制的 实际实现。 首先,在 80386 CPU 中增设了两个寄存器:一个是全局性的段描述表寄存器 GDTR(global descriptor table register),另一个是局部性的段描述表寄存器 LDTR(local descriptor table register),分别可以用来 指向存储在内存中的一个段描述结构数组,或者称为段描述表。由于这两个寄存器是新增设的,不存在 与原有的指令是否兼容的问题,访问这两个寄存器的专用指令便设计成“特权指令”。 在此基础上,段寄存器的高 13 位(低 3 位另作他用)用作访问段描述表中具体描述结构的下标 (index),如图 1.1 所示。 15 0 RPL 1 T I 23 表示特权级别, 00=最高级,11=最低级 TI=0时使用GDTR =1时使用LDTR 从8192个全局或局部描述 表项中选择一个描述符 图1.1 段寄存器定义 GDTR 或 LDTR 中的段描述表指针和段寄存器中给出的下标结合在一起,才决定了具体的段描述表 项在内存中的什么地方,也可以理解成,将段寄存器内容的低 3 位屏蔽掉以后与 GDTR 和 LDTR 中的基 2006-12-31 版权所有,侵权必究 第 13 页,共 1482 页 Linux 内核源代码情景分析 地址相加得到描述表项的起始地址。因此就无法通过修改描述表项的内容来玩弄诡计,从而起到保护的 作用。每个段描述表项的大小是 8 个字节,每个描述表项含有段的基地址和段的大小,再加上其他一些 信息,其结构如图 1.2 所示。 B31 - B24 L15 - L0 B15 - B0 B23 - B16typeSDPLP L19 - L16ADG 图1.2 8字节段描述表项的定义 结构中的 B31~B24 和 B23~B16 分别是基地址的 bit24~bit31 和 bit16~bit23。而 L19~L16 和 L15~ L0 则为长度(limit)的 bit16~bit19 和 bit0~bit15。其中 DPL 是个 2 位的位段,而 type 是一个 4 位的位 段。它们所在的整个字节分解如图 1.3 所示。 AES RWED/CP A=0本段未被访问 A=1本段已被访问 W=1,可写入 W=0,不能被写入 ED=1,向下伸展(堆栈段) ED=0,向上伸展(数据段) E=0,数据段 R=1,可读 R=0,不可读 C=1,遵循特权级 C=0,忽视特权级 E=1,代码段 S=0,系统描述项 S=1,代码或数据段描述符 P=0,描述项无效 P=1,描述符有效 DPL=00-11,本段特权 DPL 图1.3 段描述表项TYPE字节的定义 我们也可以用一段“伪代码”来说明整个段描述结构: typedef struct { unsigned int base24_31:8; /* 基地址的最高8位 */ unsigned int g:1; /* granularity,表段的长度单位,0表示字节,1表示4KB */ unsigned int d_b:1; /* default operation size,存取方式,0=16位,1=32位 */ 2006-12-31 版权所有,侵权必究 第 14 页,共 1482 页 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 15 页,共 1482 页 unsigned int unused:1; /* 固定设置为0 */ unsigned int avl:1; /* avalable,可供系统软件使用 */ unsigned int seg_limit_16_19:4; /* 段长度的最高4位 */ unsigned int p:1; /* segment present,为0表示该段的内容不在内存中 */ unsigned int dpl:2; /* Descriptor privilege level,访问本段所需权限 */ unsigned int s:1; /* 描述项类型,1表示系统,0表示代码或数据 */ unsigned int type:4; /* 段的类型,与上面的S标志位一起使用 */ unsigned int base_0_23:24; /* 基地址的低24位 */ unsigned int seg_limit_0_15:16; /* 段长度的低16位 */ }段描述项; 以这里的位段 type 为例,“:4”表示其宽度为 4 位。整个数据结构的大小为 64 位,即 8 个字节。 读者一定会问:为什么把段描述项定义成这样一种奇怪的结构?例如,为什么基地址的高 8 位和低 24 位不连在一起?最自然也最合理的解释就是:开始时 Intel 的意图是 24 位地址空间,后来又改成了 32 位地址空间。这也可以从段长度字段也是拆成两节得到印证:当 g 标志位为 1 时,长度的单位为 4KB, 而段长度的 16 位的容量是 64K,所以一个段的最大可能长度为 64K×4K=256M,而这正是 24 位地址空 间的大小。所以,可以看出,Intel 起先意欲使用 24 位地址空间,不久又意识到应该使用 32 位,但是 80286 已经发售出去了,于是只好修修补补。当时的 Intel 确实给人一种“小脚女人走路”的感觉。 每当一个段寄存器的内容改变时(通过 MOV、POP 等指令或发生中断等事件),CPU 就把由这段寄 存器的新内容决定的段描述项装入 CPU 内部的一个“影子”描述项。这样,CPU 中有几个段寄存器就 有几个影子描述项,所以也可以看作是对段寄存器的扩充。扩充后的段寄存器分成两部分,一部分是可 见的(对程序而言),还与原先的段寄存器一样;另一部分是不可见的,就是用来存放影子描述项的空间, 这一部分是专供 CPU 内部使用的。 在 80386 的段式内存管理的基础上,如果把每个段寄存器都指向同一个描述项,而在该描述项中将 基地址设成 0,并将段长度设成最大,这样便形成一个从 0 开始覆盖整个 32 位地址空间的一个整段。由 于基地址为 0,此时的物理地址与逻辑地址相同,CPU 放到地址总线上去的地址就是在指令中给出的地 址。这样的地址有别于由“段寄存器/位移量”构成的“层次式”地址,所以 Intel 称其为“平面(Flat) 地址”。Linux 内核的源代码(更确切地应该说是 gcc)采用平面地址。这里要指出,平面地址的使用并 不意味着绕过了段描述表、段寄存器这一整套内存管理机制,而只是段式内存管理的一种使用特例。 关于 80386 的段式内存管理就先介绍这些,以后随着代码分析的进展视需要再加以补充。读者想要 了解完整的细节可以参阅 Intel 的有关技术资料。 利用 80386 对段式内存管理的硬件支持,可以实现段式虚存管理。如前所述,当一个寄存器的内容 改变时,CPU 要根据新的段寄存器内容以及 GDTR 或 LDTR 的内容找到相应的段描述项并将其装入 CPU 中。在此过程中,CPU 会检查该描述项中的 p 标志位(表示“present”),如果 p 标志位为 0,就表示该 描述项所指向的那一段内容不在内存中(也就是说在磁盘上的某个地方),此时 CPU 会产生一次异常 (exception,类似于中断),而相应的服务程序便可以从磁盘交换区将这一段的内容读入内存的某个地方, 并据此设置描述项中的基地址,再将 p 标志位设置成 1。相应地,内存中暂时不用的存储段则可以写入 磁盘,并将其描述项中 p 标志位改成 0。 对段式内存管理的支持只是 i386 保护模式的一个组成部分。如果没有系统状态和用户状态的分离, 以及特权指令(只允许在系统状态下使用)的设立,那么尽管有了前述的段式内存管理,也还不能起到 保护的效果。前面已经提到过特权指令的设置,如用来装入和存储 GDTR 和 LDTR 的指令 LGDT/LLDT 和 SGDT/SLDT 等就都是特权指令。正是由于这些指令都只能在系统状态(也就是在操作系统的内核中) 使用,才使得用户程序不但不能改变 GDTR 和 LDTR 的内容,还因此既无法知道其段描述表在内存中的 位置,又无法访问其段描述表所在的空间(只能在系统状态下才能访问),从而无法通过修改段描述项来 打破系统的保护机制。那么,80386 怎么来分隔系统状态和用户状态,并且提供在两种状态之间切换的 机制呢? Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 16 页,共 1482 页 80386 并不只是像一般 CPU 通常所做的那样,划分出系统状态和用户状态,而是划分成四个特权级 别,其中 0 级为最高,3 级为最低。每一条指令也都有其适用级别,如前述的 LGDT,就只有在 0 级的 状态下才能使用。而一般的输入/输出指令(IN,OUT)则级别定为 0 级和 1 级。通常,用户的应用程序 都是 3 级。一般程序的当前运行级别由其代码段的局部描述项(即由段寄存器 CS 所指向的局部段描述 项)中的 dpl 字段决定(dpl 表示“descriptor privilege level”)。当然,每个描述项中的 dpl 字段都是在 0 级状态下由内核设定的。而全局段描述的 dpl 字段,则又有所不同,它是表示所需的级别。 前面讲过,16 位的段寄存器中的高 13 位用作下标来访问段描述表,而低 3 位是干什么的呢?我们 还是通过一段伪代码来说明: typedef struct { unsigned short seg_idx:13; /* 13位的段描述项下标 */ unsigned short ti:1; /* 段描述表指示位,0表示GDT,1表示LDT */ unsigned short rpl:2; /* Requested Privilege Level,要求的优先级别 */ }段寄存器; 当段寄存器 CS 中的 ti 位为 0 时,表示要使用全局段描述表,为 1 时,则表示要使用局部段描述表。 而 rpl 则表示所需要的权限。当改变一个段寄存器的内容时,CPU 会加以检查,以确保该段程序的当前 执行权限和段寄存器所指定要求的权限均不低于所要访问的那一段内存的权限 dpl。 至于怎样在不同的执行权限之间切换,我们将在进程调度、系统调用和中断处理的有关章节中讨论。 此外,除了全局段描述表指针 GDTR 和局部段描述表指针 LDTR 两个寄存器外,其实 i386 CPU 中还有 个中断向量表指针寄存器 IDTR、与进程(在 Intel 术语中称为“任务”,Task)有关的寄存器 TR 以及描 述任务状态的“任务状态段”TSS 等,这些都将在其他章节中有需要时再加以介绍。Intel 在实现 i386 保 护模式时将 CPU 的执行状态分成四级,意图是为满足更为复杂的操作系统和运行环境的需要。有些操作 系统,如 OS/2 中,也确实使用了。但是很多人怀疑是否真有必要搞得那么复杂。事实上,几乎所有广 泛使用的 CPU 都没有这么复杂。而且,在 80386 上实现的各种 Unix 版本,包括 Linux,都只用了两个 级别,即 0 级和 3 级,作为系统状态和用户状态。本书在以后的讨论中将沿用 Unix 的传统称之为系统状 态和用户状态。 1.3. i386的页式内存管理机制 学过操作系统原理的读者都知道,内存管理有两种,一种是段式管理,另一种是页式管理,而页式 管理更为先进。从 80 年代中期开始,页式内存管理进入了各种操作系统(以 Unix 为主)的内核,一时 成为操作系统的一个热点。 Intel 从 80286 开始实现其“保护模式”,也即段式内存管理。但是很快就发现,光有段式内存管理 而没有页式内存管理是不够的,那样会使它的 X86 系列逐渐失去竞争力以及作为主流 CPU 产品的地位。 因此,在不久以后的 80386 中就实现了对页式内存管理的支持。也就是说,80386 除了完成并完善从 80286 开始的段式内存管理的同时还实现了页式内存管理。 前面讲过,80386 的段式内存管理机制,是将指令中结合段寄存器使用的 32 位逻辑地址映射(转换 成)同样是 32 位的物理地址。之所以称之为“物理地址”。是因为这是真正放到地址总线上去,并用以 寻访物理上存在着的具体内存单元的地址。但是,段式存储管理机制的灵活性和效率都比较差。一方面 “段”是可变长度的,这就给磁盘交换操作带来了不便;另一方面,如果为了增加灵活性而将一个进程 的空间划分成很多小段时,就势必要求在程序中频繁地改变段寄存器的内容。同时,如果将段分小,虽 然一个段描述表中可以容纳 8192 个描述项(因为有 13 位下标),也未必能保证足够使用。所以,比较好 的办法还是采用页式存储管理。本来,页式存储并不需要建立在段式存储管理的基础上,这是两种不同 的机制。可是,在 80386 中,保护模式的实现是与段式存储密不可分的。例如 CPU 的当前执行权限就是 在有关的代码段描述项中规定的。读过 Unix 早期版本的读者不妨将此与 PDP-11 中的情况作一对比。在Linux 内核源代码情景分析 PDP-11 中 CPU 的当前执行权限存放在一个独立的寄存器 PSW 中,而与任何其他的数据结构没有关系。 因此,在 80386 中,既然决定利用部分已经存在的资源,而不是完全另起炉灶,那就无法绕过段式存储 管理来实现页式存储管理。也就是说,80386 的系统结构决定了它的页式存储管理只能建立在段式存储 管理的基础上。这也意味着,页式存储管理的作用是在由段式存储管理所映射而成的地址上再加上一层 地址映射。由于此时由段式存储管理映射而成的地址不再是“物理地址”了,Intel 就称之为“线性地址”。 于是,段式存储管理先将逻辑地址映射成线性地址,然后由页式存储管理将线性地址映射成物理地址; 或者,当不使用页式存储管理时,就将线性地址直接用作物理地址。 80386 把线性地址空间划分成 4K 字节的页面,每个页面可以被映射至物理存储空间中任意一块 4K 大小的区间(边界必须与 4K 字节对其)。在段式存储管理中,连续的逻辑地址经过映射后在线性地址空 间中还是连续的。但是在页式存储管理中,连续的线性地址空间经过映射后在物理空间却不一定连续(其 灵活性也正在于此)。这里值得指出的是,虽然页式存储管理是建立在段式存储管理的基础上的,但一旦 启用了页式存储管理,所有的线性地址都要经过页式映射,连 GDTR 和 LDTR 中给出的段描述表起始地 址也不例外。 由于页式存储管理的引入,对 32 位的线性地址有了新的解释(以前就是物理地址): typedef struct { unsigned int dir:10; /* 用作页面表目录中的下标,该目录项指向一个页面表 */ unsigned int page:10; /* 用作具体页面表中的下标,该表项指向一个物理页面 */ unsigned int offset:12; /* 在4K字节物理页面内的偏移量 */ }线性地址; 这个结构可以用图 1.4 形象地表示。 页内偏移offset页面表page页面目录dir 01112212231 图1.4 线性地址的格式 可以看出,在页面目录中共有 210=1024 个目录项,每个目录项指向一个页面表,而在每个页面表 中又共有 1024 个页面描述项。类似于GDTR和LDTR,又增加了一个新的寄存器CR3 作为指向当前页面 目录的指针。这样,从线性地址到物理地址的映射过程为: 1. 从 CR3 取得页面目录的基地址。 2. 以线性地址中的 dir 位段为下标,在目录中取得相应页面表的基地址。 3. 以线性地址中的 page 位段为下标,在所得到的页面表中取得相应的页面描述项。将页面描述项 中给出的页面基地址与线性地址中的 offset 位段相加得到物理地址。 上列映射过程可用图 1.5 直观地表示。 2006-12-31 版权所有,侵权必究 第 17 页,共 1482 页 Linux 内核源代码情景分析 偏移量页表目录 目录 …… 页表 …… 内存页面 …… CR3 图1.5 页式映射示意图 那么,为什么要使用两个层次,先找到目录项,再找到页面描述项,而不是像在使用段寄存器时那 样一步到位呢?这是出于空间效率的考虑。如果将线性地址中的 dir 和 page 两个位段合并在一起是 20 位,因此页面表的大小就将是 1K×1K=1M 个表项。由于每个页面的大小为 4K 字节,总的空间大小仍 为 4K×1M=4G,正好是 32 位地址空间的大小。但是,实际上很难想象有一个进程会需要用到 4G 的全 部空间,所以大部分表项势必是空着的。可是,在一个数组中,即使是空着不用的表项也占用空间,这 样就造成了浪费。若分成两层,则页表可以视需要而设置,如果目录中某项为空,就不必设立相应的页 表,从而省下了存储空间。当然,在最坏的情况下,如果一个进程真的要用到全部 4G 的存储空间,那 就不仅不能节省,反而要多消耗一个目录所占用的空间,但那概率基本上是 0。另外,一个页面的大小 是 4K 字节,而每一个页面表项或目录表项的大小是 4 个字节。1024 个表项正好也是 4K 字节,恰好可 以放在一个页面中。而若多于 1024 项就要使目录或页面表跨页面存放了。也正为此,在 64 位的 Alpha CPU 中页面的大小是 8K 字节,因为目录表项和页面表项的大小都变成了 8 个字节。 如前所述,目录项中还有一个指向页面表的指针,而页面表项中则含有一个指向一个页面起始地址 的指针。由于页面表和页面的起始地址都总是在 4K 字节的边界上,这些指针的低 12 位都永远是 0。这 样,在目录项和页表项中都只要 20 位指针就够了,而余下的 12 位则可以用于控制或其他的目的。于是, 目录项的结构为: typedef struct { unsigned int ptba:20; /* 页表基地址的高20位 */ unsigned int avail:3; /* 供系统程序员使用 */ unsigned int g:1; /* global,全局性页面 */ unsigned int ps:1; /* 页面大小,0表示4K字节 */ unsigned int reserved:1; /* 保留,永远是0 */ unsigned int a:1; /* accessed,已被访问过 */ unsigned int pcd:1; /* 关闭(不使用)缓冲存储器 */ unsigned int pwt:1; /* Write Through,用于缓冲存储器 */ unsigned int u_s:1; /* 为0时表示系统(或超级)权限,为1时表示用户权限 */ unsigned int r_w:1; /* 只读或可写 */ unsigned int p:1; /* 为0时表示相应的页面不在内存中 */ 2006-12-31 版权所有,侵权必究 第 18 页,共 1482 页 Linux 内核源代码情景分析 }目录项; 页面表项的直观表示如图 1.6 所示。 页面基地址的高20位 D PX WU P W T P C D A Present Writable User defined Write-through Cache disable Accessed Dirty 图1.6 页面表项示意图 页面表项的结构与目录项的结构基本上相同,但没有“页面大小”位 ps,所以第 8 位保留不用,但 第 7 位(在目录项中保留不用)则为 D(Dirty)标志,表示该页面已经被写过,所以已经“脏”了。当 页面表项或目录表项的最低位 p 为 0 时,表示相应的页面或页面表不在内存,根据其他一些有关寄存器 的设置,CPU 可以产生一个“页面错”(Page Fault)异常(也称为缺页中断,但异常和中断其实是有区 别的)。这样,内核中的有关异常服务程序就可以从磁盘上的页面交换区将相应的页面读入内存,并且相 应地设置表项中的基地址,并将 p 位设置成 1。相反,也可以将内存中暂时不使用的页面写入磁盘的交 换区,然后将相应页面表项的 p 位置为 0。这样,就可以实现页式虚存了。当 p 位为 0 时,表项的其余 各位均无意义,所以可被用来临时存储其他信息,如换出的页面在磁盘上的位置等等。 当目录项中的 ps(page size)位为 0 时,包含在由该目录项所指的页面表中所有页面的大小都是 4K 字节,这也是目前在 Linux 内核中所采用的页面大小。但是,从 Pentium 处理器开始,Intel 引入了 PSE 页面大小扩充机制。当 ps 位为 1 时,页面的大小就成了 4M 字节,而页面表就不再使用了。这时候,线 性地址中的低 22 位全部用作在 4M 字节页面表中的位移。这样,总的寻址能力没有改变,即 1024×4M =4G,但是映射的过程减少了一个层次。随着内存容量和磁盘容量的日益增加,磁盘访问速度的显著提 高,以及对图象处理要求的日益增加,4M 字节的页面大小有可能会成为主流。在这一点上,Intel 倒还 是很有远见的。 最后,i386 CPU 中还有个寄存器 CR0,其最高位 PG 是页式映射机制的总开关。当 PG 位被设置成 1 时,CPU 就开启了页式存储管理的映射机制。 从 Pentium Pro 开始,Intel 又作了扩充。这一次扩充的是物理地址的宽度。Intel 在另一个控制寄存 器 CR4 中又增加了一位 PAE(表示 Physical Address Extention),当 PAE 位设置成 1 时,地址总线的宽度 就变成了 36 位(又增加了 4 位)。与此相应,页式存储管理的映射机制也自然地有所改变。不过大多数 用户都还不需要使用 36 位(64G)物理地址空间,所以这里从略,有兴趣的读者可以参阅 Intel 的有关 技术资料或专著。此外,Intel 已经推出了 64 位的 IA-64 系统结构,Linux 内核也已经支持 IA-64 系统结 构。事实上,Linux 原来就已经在 Alpha CPU 上支持 64 位地址。除存储管理外,80386 还有很强的高速 缓冲存储和流水线功能。但是对于软件,对于操作系统的内核来说,那在很大程度上是透明的,所以本 书将仅在有必要的时候才加以简单的说明,而不在此详述了。 1.4. Linux内核源代码中的C语言代码 Linux 内核的主体是以 GNU 的 C 语言编写的,GNU 为此提供了编译工具 gcc。GNU 对 C 语言本身 2006-12-31 版权所有,侵权必究 第 19 页,共 1482 页 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 20 页,共 1482 页 (在 ANSI C 基础上)做了不少扩充,可能是读者尚未见到过的。另一方面,由于内核代码,往往会用 到一些在应用程序设计中不常见的语言成分或编程技巧,也许使读者感到陌生。本书并非介绍 GNU C 语言的专著,也非技术手册,所以不在这里一一列举和详细讨论这些扩充和技巧。再说,离开具体的情 景和上下文,罗列一大堆规则,对于读者恐怕也没有多大帮助。所以,我们在这里只是对可能会影响读 者阅读 Linux 内核源程序,或使读者感到困惑的一些扩充和技巧先作一些简单的介绍。以后,随着具体 的情景和代码的展开,在需要时还会结合实际加以补充。 首先,gcc 从 C++语言中吸收了“inline”和“const”。其实,GNU 的 C 和 C++是合为一体的,gcc 既是 C 编译又是 C++编译,所以从 C++中吸收一些东西到 C 中是很自然的。从功能上说,inline 函数的 使用与#define 宏定义相似,但更有相对的独立性,也更安全。使用 inline 函数也有利于程序调试。如果 编译时不加优化,则这些 inline 就是普通的、独立的函数,更便于调试。调试好了以后,再采用优化重 新编译一次,这些 inline 函数就像宏操作一样融入了引用处的代码中,有利于提高运行效率。由于 inline 函数的大量使用,相当一部分的代码从.c 文件移入了.h 文件中。 还有,为了支持 64 位的 CPU 结构(Alpha 就是 64 位的),gcc 增加了一种新的基本数据类型“long long int”,该类型在内核代码中常常用到。 许多 C 语言都支持一些“属性描述符”(attribute),如“aligned”、“packed”等等;gcc 也支持不少 这样的描述符。这些描述符的使用等于是在 C 语言中增加了一些新的保留字。可是,在原来的 C 语言(如 ANSI C)中这些词并非保留字,这样就有可能产生一些冲突。例如,gcc 支持保留字 inline,可是由于“inline” 原非保留字(在 C++中是保留字),所以在老的代码中可能已经有一变量名为 inline,这样就产生了冲突。 为了解决这个问题,gcc 允许在作为保留字使用的“inline”前、后都加上“__”(注意,是两个下划线), 因而“__inline__”等价于保留字“inline”。同样的道理,“__asm__”等价于“asm”。这就是我们在代码 中有时候看到“asm”,而有时候又看到“__asm__”的原因。 gcc 还支持一个保留字“attribute”,用来作属性描述。如: struct foo { char a; int x[2]; } __attribute__((packed)); 这里属性描述“packed”表示在字符 a 与整形数组 x 之间不应为了与 32 位长整数边界对齐而留下空 洞。这样“packed”就不会与变量名发生冲突了。 由于在 Linux 的内核中使用了 gcc 对 C 的扩充,很自然地 Linux 的内核就只能用 gcc 编译。不仅如 此,由于 gcc 和 Linux 内核在平行地发展,一旦在 Linux 内核中使用了 gcc,在其较新版本中有了新增加 新扩充,就不能再使用较老的 gcc 来编译。也就是说,Linux 内核的各个版本有着对 gcc 版本的依赖关系。 读者自然会问:“这样,Linux 内核的可移植性是否会受到损害?”回答是:“是的,但这是经过权衡利 弊以后作出的决定。”首先,在可移植性与本身的质量之间,GNU 选择了以质量为优先。再说,将 gcc 移植(其实就是扩充)到新的 CPU 上应非难事。回顾一下 Unix 的历史。最初的 Unix 是以汇编和 B 语 言书写的,正是因为 Unix 的需要才有了 C 语言。所以,C 语言可说是 Unix 的孪生物。Unix 要发展,C 语言自然也要发展。对于 Unix 来说,C 语言不过是工具,而工具当然要服从目的本身的需要。其次,可 移植性问题看似重大,其实并不太严重。如前所述,目前的 Linux 内核源代码已经支持几乎所有重要的、 常用的 CPU,gcc 支持的 CPU 就更多了。而且,gcc 还支持对各种 CPU 的交叉编译。 如前所述,Linux 内核的代码中使用了大量的 inline 函数。不过,这并未消除对宏操作的使用,内核 中仍有许多宏操作定义。人们常常会对内核代码中一些宏操作的定义方式感到迷惑不解,有必要在这里 作一些解释。先看一个实例,取自 fs/proc/kcore.c 第 163 行。 00163: #define DUMP_WRITE(addr,nr) do { memcpy(bufp,addr,nr); bufp += nr; } while(0) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 21 页,共 1482 页 读者想必知道,do-while 循环是先执行后判断循环条件。所以这个定义意味着每次引用这个宏操作 是会执行循环体一次,而且只执行一次,可是,为什么要这样通过一个 do-while 循环来定义呢?这似乎 有点怪。我们不妨看看其他几种可能。首先,能不能改成如下式样? 00163: #define DUMP_WRITE(addr,nr) memcpy(bufp,addr,nr); bufp += nr; 不行。如果有一段程序在一个 if 语句中引用这个宏操作就会出问题,让我们通过一个假想的例子来 说明: if(addr) DUMP_WRITE(addr,nr); else do_something_else(); 经过预处理后,这段代码就会变成这样: if(addr) memcpy(bufp,addr,nr); bufp += nr; else do_something_else(); 编译这段代码时 gcc 会失败,并报告语法错误。因为 gcc 认为 if 语句在 memcpy()以后就结束了,然 后却又碰到了一个 else。如果把 DUMP_WRITE()和 do_something_else()换一下位置,编译倒是可以通过, 问题却更严重了,因为不管条件满足与否 bufp += nr 都会得到执行。 读者马上会想到要在定义中加上括号,成为这样: 00163: #define DUMP_WRITE(addr,nr) {memcpy(bufp,addr,nr); bufp += nr;} 可是,上面那段程序还是通不过编译,因为经过预处理就变成这样: if(addr) {memcpy(bufp,addr,nr); bufp += nr;}; else do_something_else(); 同样,gcc 在碰到 else 前面的“;”时就认为 if 语句已经结束,因而后面的 else 不在 if 语句中。相比 之下,采用 do-while 的定义在任何一种情况下都没有问题。 了解了这一点之后,再来看对“空操作”的定义。由于 Linux 内核的代码要考虑到各种不同的 CPU 和不同的系统配置,所以常常需要在一定的条件下把某些宏操作定义为空操作。例如在 include/asm-i386/system.h 中第 14 行的 prepare_to_switch(): 00014: #define prepare_to_switch() do{} while(0) 内核在调度一个进程运行,进行切换之际,在有些 CPU 上需要调用 prepare_to_switch()作些准备, 而在另一些 CPU 上就不需要,所以要把它定义为空操作。 读者在学习数据结构时一定学习过队列(指双链队列)操作。内核中大量地使用着队列和队列操作, 而这又不是专门属于哪一个方面的内容(如进程管理、文件系统、存储管理等等),所以我们在这里作一 些介绍。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 22 页,共 1482 页 如果我们有一种数据结构 foo,并且需要维持一个这种数据结构的双链队列,最简单的办法、也是 最常用的办法就是在这个数据结构的类型定义中加入两个指针,例如: typedef struct foo { struct foo *prev; struct foo *next; …………………… } foo_t; 然后为这种数据结构写一套用于各种队列操作的子程序。由于用来维持队列的这两个指针的类型是 固定的(都指向 foo 数据结构),这些子程序不能用于其他数据结构的队列操作。换言之,需要维持多少 种数据结构的队列,就得有多少套的队列操作子程序。对于使用队列较少的应用程序或许不是个大问题, 但对于使用大量队列的内核就成了问题了。所以,Linux 内核中采用了一套通用的、一般的、可以用到 各种不同数据结构的队列操作。为此,代码的作者们把指针 prev 和 next 从具体的“宿主”数据结构中抽 象出来成为一种数据结构 list_head,这种数据结构既可以“寄宿”在具体的宿主数据结构内部,成为该 数据结构的一个“连接件”;也可以独立存在而成为一个队列的头。这个数据结构的定义在 include/linux/list.h 中(实际上是数据结构类型的申明,为行文方便,本书采取不那么“学究”,或者说不 那么严格的态度。对“定义”和“申明”,还有对“数据结构类型”和“数据结构”,乃至“结构”这些 词也常常不加严格区分。当然,我们并不鼓励读者这样做)。 00016: struct list_head { 00017: struct list_head *next, *prev; 00018: }; 这里我们把结构名以粗体字排出,目的仅在于醒目,并没有特别的含义。如果需要有某种数据结构 的队列,就把这种结构内部放上一个 list_head 数据结构。以用于内存页面管理的 page 数据结构为例,其 定义为:(见 include/linux/mm.h) 00134: typedef struct page { 00135: struct list_head list; …………………… 00138: struct page *next_hash; …………………… 00141: struct list_head lru; …………………… 00148: } mem_map_t; 可见,在 page 数据结构中寄宿了两个 list_head 结构,或者说有两个队列操作的连接件,所以 page 结构可以同时存在于两个双链队列中。此外,结构中还有个单链指针 next_hash,用来维持一个单链的杂 凑队列,不过我们在这里并不关心。 对于宿主数据结构内部的每个 list_head 数据结构都要加以初始化,可以通过一个宏操作 INIT_LIST_HEAD 进行: 00025: #define INIT_LIST_HEAD(ptr) do { \ 00026: (ptr)->next = (ptr); (ptr)->prev = (ptr); \ 00027: } while(0) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 23 页,共 1482 页 参数 ptr 为指向需要初始化的 list_head 结构。可见初始化后两个指针都指向该 list_head 结构自身。 要将一个 page 结构通过其“队列头”list 链入(有时候我们也说“挂入”)一个队列时,可以使用 list_add(),这是一个 inline 函数,其代码在 include/linux/list.h 中: 00053: static __inline__ void list_add ( struct list_head *new, struct list_head *head) 00054: { 00055: __list_add( new, head, head->next ); 00056: } 参数 new 指向欲链入队列的宿主数据结构内部的 list_head 数据结构。参数 head 则指向链入点,也 是个 list_head 结构,它可以是一个独立的、真正意义上的队列头,也可以是在另一个宿主数据结构(甚 至可以是不同类型的宿主结构)内部。这个 inline 函数调用另一个 inline 函数__list_add()来完成操作: [list_add()>__list_add()] 00029: /* 00030: * Insert a new entry between two known consecutive entries. 00031: * 00032: * This is only for internal list manipulation where we know 00033: * the prev/next entries already! 00034: */ 00035: static __inline__ void __list_add( struct list_head *new, 00036: struct list_head *prev, 00037: struct list_head *next ) 00038: { 00039: next->prev = new; 00040: new->next = next; 00041: new->prev = prev; 00042: prev->next = new; 00043: } 对于辗转调用的函数,为帮助读者随时了解其来龙去脉,本书通常在函数的代码前面用方括号和大 括号列出其调用路径。这种路径通常以一个比较重要或常用的函数为起点,例如这里就是以 list_add()为 起点。不过,读者要注意,对同一个函数的不同调用路径往往有很多,我们列出的只是具体的情景或讨 论中的路径。例如,有些函数也许跳过 list_add()而直接调用__list_add(),而形成另一条不同的路径。至 于__list_add()本身的代码,我们就把它留给读者了。 再来看从队列中脱链的操作 list_del(): 00090: static __inline__ void list_del( struct list_head *entry ) 00091: { 00092: __list_del(entry->prev, entry->next); 00093: } 同样,这里也是调用另外一个 inline 函数__list_del()来完成操作: [list_del()>__list_del()] 00078: static __inline__ void __list_del( struct list_head *prev, 00079: struct list_head *next ) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 24 页,共 1482 页 00080: { 00081: next->prev = prev; 00082: prev->next = next; 00083: } 注意在__list_del()中的操作对象是队列中在 entry 之前和之后的两个 list_head 结构。如果 entry 是队 列中的最后一项,则二者相同,就是队列的头,那也是一个 list_head 结构,不过不在任何宿主结构内部。 读者也许已经等不及要问了:队列操作都是通过 list_head 进行的,但是那不过是个连接件,如果我 们手上有个宿主结构,那当然就知道了它的某个 list_head 在哪里,从而以此为参数调用 list_add()或 list_del();可是,反过来,当我们顺着一个队列取得其中一项的 list_head 结构时,又怎样找到其宿主结 构呢?在 list_head 结构中并没有指向宿主结构的指针啊。毕竟,我们真正关心的是宿主结构,而不是连 接件。 是的,这是个问题。我们还是通过一个实例来看这个问题是怎样解决的。下面是取自 mm/page_alloc.c 中的一行代码: [rmqueue()] 00188: page = memlist_entry(curr, struct page, list); 这里的 memlist_entry()将一个 list_head 指针 curr 换算成其宿主结构的起始地址,也就是取得指向其 宿主 page 结构的指针。读者可能会对 memlist_entry 的实现和调用感到困惑。因为其调用参数 page 是个 类型,而不是具体的数据。如果看一下函数 rmqueue()整个代码,还可以发现在那里 list 竟是无定义的。 事实上,在同一个文件中将 memlist_entry 定义成 list_entry,所以实际引用的是 list_entry(): 00048: #define memlist_entry list_entry 而 list_entry 的定义则在 include/linux/lish.h 中: 00135: /* 00136: * list_entry – get the struct for this entry 00137: * @ptr: the &struct list_head pointer. 00138: * @type: the type of the struct this is embedded in. 00139: * @member: the name of the list_struct within the struct. 00140: */ 00141: #define list_entry(ptr, type, member) \ 00142: ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) 将前面的 188 行与此对照,就可以看出其中的奥秘:经过 C 预处理的文字替换,这一行的内容就成 为: page = ((struct page*)((char*)(curr)-(unsigned long)(&((struct page*)0)->list))); 这里的 curr 是一个 page 结构内部的成分 list 的地址,而我们所需要的却是那个 page 结构本身的地 址,所以要从地址 curr 中减去一个位移量,即成分 list 在 page 内部的位移量,才能达到要求。那么,这 位移量到底是多少呢?&((struct page*)0)->list 就表示当结构 page 正好在地址 0 上时其成分 list 的地址, 这就是位移。 同样的道理,如果是在 page 结构的 lru 队列里,则传下来的 member 为 lru,一样能算出宿主结构的 地址。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 25 页,共 1482 页 可见,这一套操作既普遍适用,又保持了较高的效率。但是,对于阅读代码的人却有个缺点,那就 是光从代码中不容易看出一个 list_head 的宿主结构是什么,而以前只要看一下指针 next 的类型就知道了。 1.5. Linux内核源代码中的汇编语言代码 任何一个用高级语言编写的操作系统,其内核的源代码中总有少部分代码是用汇编语言编写的。读 过 Unix Sys V 源代码的读者都知道,在其约 3 万行的核心代码中用汇编语言编写的代码约 2000 行,分 成不到 20 个扩展名为.s 和.m 的文件,其中大部分是关于中断与异常处理的底层程序,还有就是与初始 化有关的程序以及一些核心代码中调用的公用子程序。 用汇编语言编写核心代码中的部分代码,大体上是出于如下几个方面的考虑: 操作系统内核中的底层程序直接与硬件打交道,需要用到一些专用的指令,而这些指令在 C 语 言中并无对应的语言成分。例如,在 386 系统结构中,对外设的输入/输出指令如 inb、outb 等 均无对应的 C 语言语句。因此,这些底层的操作需要用汇编语言来编写。CPU 中的一些对寄存 器的操作也是一样,例如,要设置一个段寄存器时,也只好用汇编语言来编写。 CPU 中的一些特殊指令也没有对应的 C 语言成分,如关中断,开中断等等。此外,在同一种系 统结构的不同 CPU 芯片中,特别是新开发出来的芯片中,往往会增加一些新的指令,例如 Pentium、Pentium II 和 Pentium MMX,都是在原来的基础上扩充了新的指令,对这些指令的使 用也得用汇编语言。 内核中实现某些操作的过程、程序段或函数,在运行时会频繁地被调用,因此其(时间)效率 就显得很重要。而用汇编语言编写的程序,在算法和数据结构相同的条件下,其效率通常要比 用高级语言编写的高。在此类程序或程序段中,往往每一条汇编指令的使用都需要经过推敲。 系统调用的进入和返回就是一个典型的例子。系统调用的进出是非常频繁用到的过程,每秒钟 可能会用到成千上万次,其时间效率可谓举足轻重。再说,系统调用的进出过程还牵涉到用户 空间和系统空间之间的来回切换,而用于这个目的的一些指令在 C 语言中本来就没有对应的语 言成分,所以,系统调用的进入和返回显然必须用汇编语言来编写。 在某些场合,一段程序的空间效率也会显得非常重要。操作系统的引导程序就是一个例子。系 统的引导程序通常一定要能容纳在磁盘上的第一个扇区中。这时候,哪怕这段程序的大小多出 一个字节也不行,所以就只能以汇编语言编写。 在 Linux 内核的源代码中,以汇编语言编写的程序或程序段,有几种不同的形式。 第一种是完全的汇编代码,这样的代码采用.s 作为文件名的后缀。事实上,尽管是“纯粹”的汇编 代码,现代的汇编工具也吸收了 C 语言预处理的长处,也在汇编之前加上了一趟预处理,而预处理之前 的文件则以.s 为后缀。此类(.s)文件也和 C 程序一样,可以使用#include、#ifdef 等等成分,而数据结 构也一样可以在.h 文件中加以定义。 第二种是嵌入在 C 程序中的汇编语言片段。虽然在 ANSI C 语言标准中并没有关于汇编片段的规定, 事实上各种实际使用的 C 编译中都做了这方面的扩充,而 GNU 的 C 编译 gcc 也在这方面做了很强的扩 充。 此外,内核代码中也有几个 Intel 格式的汇编语言程序,是用于系统引导的。 由于本身专注于 Intel386 系统结构上的 Linux 内核,下面我们只介绍 GNU 对 i386 汇编语言的支持。 对于新接触 Linux 内核源代码的读者,哪怕他比较熟悉 i386 汇编语言,在理解这两种语言的程序或 片段时都会感到困难,有的甚至会望而却步。其原因是:在内核“纯”汇编代码中 GNU 采用了不同于 常用 386 汇编语言的句法;而在嵌入 C 程序的片段中,则更增加了一些指导汇编工具如何分配使用寄存 器、以及如何与 C 程序中定义的变量相结合的语言成分。这些成分使得嵌入 C 程序中的汇编语言片段实 际上变成了一种介乎 386 汇编和 C 语言之间的一种中间语言。 所以,我们先集中介绍一下在内核中这两种情况下使用的 386 汇编语言,以后在具体的情景中涉及 具体的汇编语言代码时还会加以解释。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 26 页,共 1482 页 1.5.1. GNU的386汇编语言 在 Dos/Windows 领域中,386 汇编语言都采用由 Intel 定义的语句(指令)格式,这也是几乎在所有 的有关 386 汇编语言程序设计的教科书或参考书中所使用的格式。可是,在 Unix 领域中,采用的却是由 AT&T 定义的格式。当初,当 AT&T 将 Unix 移植到 80386 处理器上时,根据 Unix 圈内人士的习惯和需 要而定义了这样的格式。Unix 最初是在 PDP-11 机器上开发的,先后移植到 VAX 和 68000 系列的处理器 上。这些机器的汇编语言在风格上、进而在格式上与 Intel 的有所不同。而 AT&T 定义的 386 汇编语言就 比较接近那些汇编语言。后来,在 Unixware 中保留了这种格式。GNU 主要是在 Unix 领域内活动的(虽 然 GNU 是“GNU is Not Unix”的缩写)。为了与先前的各种 Unix 版本与工具有尽可能好的兼容性,由 GNU 开发的各种系统工具自然地继承了 AT&T 的 386 汇编语言格式,而不采用 Intel 的格式。 那么,这两种汇编语言之间的差距到底有多大呢?其实是大同小异。可是有时候小异也是很重要的, 不加重视就会造成困扰。具体讲,主要有下面这么一些差别: 1. 在 Intel 格式中大多使用大写字母,而在 AT&T 格式中都使用小写字母。 2. 在 AT&T 格式中,寄存器名前要加上“%”作为前缀,而在 Intel 格式中则不带前缀。 3. 在 AT&T 的 386 汇编语言中,指令的源操作数与目的操作数的顺序与在 Intel 的 386 汇编语言中正好 相反。在 Intel 格式中是目标在前,源在后;而在 AT&T 格式中则是源在前,目标在后。例如,将寄 存器 eax 的内容送入 ebx,在 Intel 格式中为“MOV EBX, EAX”,而在 AT&T 格式中为“mov %eax, %ebx”。看 来 ,Intel 格式的设计者所想的是“EBX = EAX”,而 AT&T 格式的设计者所想的是“%eax -> %ebx”。 4. 在 AT&T 格式中,访内指令的操作数大小(宽度)由操作码名称的最后一个字母(也就是操作码的 后缀)来决定。用作操作码后缀的字母有 b(表示 8 位),w(表示 16 位)和 l(表示 32 位)。而在 Intel 格式中,则是在表示内存单元的操作数前面加上“BYTE PTR”,“WORD PTR”,或“DWORD PTR”来表示。例如,将 FOO 所指向的内存单元中的字节取入 8 位的寄存器 AL,在两种格式中不 同的表示如下: MOV AL, BYTE PTR FOO (Intel格式) movb FOO, %al (AT&T格式) 5. 在 AT&T 格式中,直接操作数要加上“$”作为前缀,而在 Intel 格式中则不带前缀。所以,Intel 格 式中的“PUSH 4”,在 AT&T 格式中就变为了“pushl $4”。 6. 在 AT&T 格式中,绝对转移或调用指令 jump/call 的操作数(也即转移或调用的目标地址),要加上 “*”作为前缀(读者大概会联想到 C 语言中的指针吧),而在 Intel 格式中则不带。 7. 远程的转移指令和子程序调用指令的操作码名称,在 AT&T 格式中为“ljump”和“lcall”,而 在 Intel 格式中,则为“JMP FAR”和“CALL FAR”。当转移和调用的目标为直接操作数时,两种不同的表 示如下: CALL FAR SECTION:OFFSET (Intel格式) JMP FAR SECTION:OFFSET (Intel格式) lcall $section, $offset (AT&T格式) ljmp $section, $offset (AT&T格式) 与之相应的远程返回指令,则为: RET FAR STACK_ADJUST (Intel格式) lret $stack_adjust (AT&T格式) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 27 页,共 1482 页 8. 间接寻址的一般格式,两者区别如下: SECTION:[BASE+INDEX*SCALE+DISP] (Intel格式) Section:disp(base,index,scale) (AT&T格式) 注意在 AT&T 格式中隐含了所进行的计算。例如,当 SECTION 省略,INDEX 和 SCALE 也省略, BASE 为 EBP,而 DISP(位移)为 4 时,表示如下: [EBP-4] (Intel格式) -4(%ebp) (AT&T格式) 在 AT&T 格式的括号中如果只有一项 base,就可以省略逗号,否则不能省略,所以(%ebp)相当于 (%ebp,,),进一步相当于(%ebp,0,0)。又如,当 INDEX 为 EAX,SCALE 为 4(32 位),DISP 为 foo,其 余均省略,则表示为: [foo+EAX*4] (Intel格式) foo(,%eax,4) (AT&T格式) 这种寻址方式常常用于在数据结构数组中访问特定元素内的一个字段,base 为数组的起始地址,scale 为每个数组元素的大小,index 为下标。如果数组元素为数据结构,则 disp 为具体字段在结构中的位移。 1.5.2. 嵌入C代码中的386汇编语言程序段 当需要在 C 语言的程序中嵌入一段汇编语言程序段时,可以使用 gcc 提供的“asm”语句功能。例 如,在 include/asm-i386/io.h 中有这么一行: 00040: #define __SLOW_DOWN_IO __asm__ __volatile__(“outb %al, $0x80”) 这里,暂时忽略在 asm 和 volatile 前后的两个“__”字符,这也是 gcc 对 C 语言的一种扩充,后面 我们还要讲到。先来看括号里面加上了引号的汇编指令。这是一条 8 位输出指令,如前所述在操作符上 加了后缀“b”以表示这是 8 位的,而 0x80 因为是常数,即所谓“直接操作数”,所以要加上前缀“$”, 而寄存器名 al 也加了前缀“%”。知道了前面所讲 AT&T 格式与 Intel 格式的不同,这就是一条很普通的 汇编指令,很容易理解。 在同一个 asm 语句中也可以插入多行汇编程序。就在同一个文件中,在不同的条件下, __SLOW_DOWN_IO 又有不同的定义: 00038: #define __SLOW_DOWN_IO __asm__ __volatile__(“jmp 1f \nl:\tjmp 1f \n1:”) 这就不怎么直观了。这里,一共插入了三行汇编语句,“\n”就是换行符,而“\t”则表示 TAB 符。 所以 gcc 将之翻译成下面的格式而交给 gas 去汇编: jmp 1f 1: jmp 1f 1: 这里转移指令的目标 1f 表示往前(f 表示 forward)找到第一个标号为 1 的那一行。相应地,如果是 1b 就表示往后找。这也是从早期 Unix 汇编代码中继承下来的,读过 Unix 第 6 版的读者大概都还能记得。 所以,这一小段汇编代码的用意就在于使 CPU 空做两条跳转指令和消耗掉一些时间。既然是要消耗掉一 些时间,而不是要节省一些时间,那么为什么要用汇编语句来实现,而不是在 C 里面来实现呢?原因在Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 28 页,共 1482 页 于想要对此有比较确切的控制。如果用 C 语言来消耗一些时间的话,你常常不能确切地知道经过编译以 后,特别是经过优化的话,最后产生的汇编代码究竟怎样。 如果读者觉得这毕竟还是容易理解的话,那么下面这一段(取自 include/asm/atomic.h)就困难多了: 00029: static __inline__ void atomic_add(int i, atomic_t *v) 00030: { 00031: __asm__ __volatile__( 00032: LOCK "addl %1,%0" 00033: :"=m" (v->counter) 00034: :"ir" (i), "m" (v->counter)); 00035: } 一般而言,往 C 代码中插入汇编语言的代码片段要比“纯粹”的汇编语言代码复杂得多,因为这里 有个怎样分配使用寄存器,怎样与 C 语言代码中的变量结合的问题。为了这个目的,必须对所用的汇编 语言作更多的扩充,增加对汇编工具的指导作用。其结果是其语法实际上变成了既不同于汇编语言,也 不同于 C 语言的某种中间语言。 下面,先介绍一下插入 C 代码中的汇编成分的一般格式,并加以解释。以后,在我们走过各种情景 时碰到具体的代码时还会加以提示。 插入 C 代码中的一个汇编语言代码片段可以分成四部分,以“:”号加以分隔,其一般形式为: 指令部:输出部:输入部:损坏部 注意不要把这些“:”号跟程序标号中所用的(如前面的 1:)混淆。 第一部分就是汇编语句本身,其格式与在汇编语言程序中使用的基本相同,但也有区别,不同之处 下面会讲到。这一部分可以称为“指令部”,是必须有的,而其他各部分则可视具体情况而省略,所以在 最简单的情况下就与常规的汇编语句基本相同,如前面的两个例子那样。 当将汇编语言的代码片段嵌入到 C 代码中时,操作数与 C 代码中的变量如何结合显然是个问题。在 本节开头的两个例子中,汇编指令都没有产生与 C 程序中的变量结合的问题,所以比较简单。当汇编指 令中的操作数需要与 C 程序中的某些变量结合时,情况就复杂多了。这是因为:程序员在编写嵌入的汇 编代码时,按照程序逻辑的要求很清楚应该选用什么指令,但是却无法确切地知道 gcc 在嵌入点的前后 会把哪一个寄存器分配用于哪一个变量,以及哪一个或哪几个寄存器是空闲着的。而且,光是被动地知 道 gcc 对寄存器的分配情况也还是不够,还得有个手段把使用寄存器的要求告知 gcc,反过来影响它对 寄存器的分配。当然,如果 gcc 的功能非常强,那么通过分析嵌入的汇编代码也应该能够归纳出这些要 求,再通过优化,最后也能达到目的。但是,即使那样,所引入的不确定性也还是个问题,更何况要做 到这样还不容易。针对这个问题,gcc 采取了一种折中的办法:程序员只提供具体的指令而对寄存器的 使用则一般只提供一个“样板”和一些约束条件,而把到底如何与变量结合的问题留给 gcc 和 gas 去处 理。 在指令部中,数字加上前缀%,如%0、%1 等等,表示需要使用寄存器的样板操作数。可以使用的 此类操作数的总数取决于具体 CPU 中通用寄存器的数量。这样,指令部中用到了几个不同的这种操作数, 就说明有几个变量需要与寄存器结合,由 gcc 和 gas 在编译和汇编时根据后面的约束条件自行变通处理。 由于这些样板操作数也使用“%”前缀,在涉及到具体的寄存器时就要在寄存器名前面加上两个“%” 符,以免混淆。 那么,怎样表达对变量结合的约束条件呢?这就是其余几个部分的作用。紧接在指令部后面的是“输 出部”,用以规定对输出变量,即目标操作数如何结合的约束条件。每个这样的条件称为一个“约束” (constraint)。必要时输出部中可以有多个约束,互相以逗号分隔。每个输出约束以“=”号开头,然后 是一个字母表示对操作数类型的说明,然后是关于变量结合的约束。例如,在上面的例子中,输出部为 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 29 页,共 1482 页 :"=m" (v->counter) 这里只有一个约束条件,“=m”表示相应的目标操作数(指令部中的%0)是一个内存单元 v->counter。 凡是与输出部中说明的操作数相结合的寄存器或操作数本身,在执行嵌入的汇编代码以后均不保留执行 之前的内容,这就给 gcc 提供了调度使用这些寄存器的依据。 输出部后面是“输入部”。输入约束的格式与输出约束条件相似,但不带“=”号。在前面的例子中 的输入部有两个约束。第一个为”ir”(i),表示指令中的%1 可以是一个寄存器中的“直接操作数”(i 表示 immediate),并且该操作数来自于 C 代码中的变量名(这里是调用参数)i。第二个约束为"m" (v->counter), 意义与输出约束中相同。如果一个输入约束要求使用寄存器,则在预处理时 gcc 会为之分配一个寄存器, 并自动插入必要的指令将操作数即变量的值装入该寄存器。在输入部中说明的操作数结合的寄存器或操 作数本身,在执行嵌入的汇编代码后也不保留执行之前的内容。例如,这里的%1 要求使用寄存器,所 以 gcc 会为其分配一个寄存器,并自动插入一条 movl 指令把参数 i 的数值装入该寄存器,可是这个寄存 器原来的内容就不复存在了。如果这个寄存器本来就是空闲的,那倒无所谓,可是如果所有的寄存器都 在使用,而只好暂时借用一个,那就得保证在使用以后恢复其原有的内容。此时,gcc 会自动在开头处 插入一条 pushl 指令,将该寄存器原来的内容保存在堆栈中,而在结束以后插入一条 popl 指令,恢复寄 存器的内容。 在有些操作中,除用于输入操作数和输出操作数的寄存器以外,还要将若干个寄存器用于计算或操 作的中间结果。这样,这些寄存器原有的内容就损坏了,所以要在损坏部对操作的副作用加以说明,让 gcc 采取相应的措施。不过,有时候就直接把这些说明放在输出部了,那也并无不可。 操作数的编号从输出部的第一个约束(序号为 0)开始,顺序数下来,每个约束计数一次,在指令 部中引用这些操作数或分配用于这些操作数的寄存器时,就在序号前面加上一个“%”号。在指令部中 引用一个操作数时总是把它当成一个 32 位的“长字”,但是对其实施的操作,则根据需要也可以是字节 操作或字(16 位)操作。对操作数进行的字节操作默认为对其最低字节的操作,字操作也是一样。不过, 在一些特殊的操作中,对操作数进行字节操作时也允许明确指出是对哪一个字节操作,此时在%与序号 之间插入一个“b”表示最低字节,插入一个“h”表示次低字节。 表示约束条件的字母有很多。主要有: “m”,“v”和“o” —— 表示内存单元; “r” —— 表示任何寄存器; “q” —— 表示寄存器 eax,ebx,ecx,edx 之一; “i”和“h” —— 表示直接操作数; “E”和“F” —— 表示浮点数; “g” —— 表示“任意”; “a”,“b”,“c”,“d” —— 分别表示要求使用寄存器 eax,ebx,ecx 或 edx; “S”,“D” —— 分别表示要求使用寄存器 esi 或 edi; “I” —— 表示常数(0 至 31)。 此外,如果一个操作数要求与前面某个约束中所要求的是同一个寄存器,那就把与那个约束对应的 操作数编号放在约束条件中。在损坏部常常会以“memory”为约束条件,表示操作完成后内存中的内容 已有改变,如果原来某个寄存器(也许在本次操作中并未用到)的内容来自内存,则现在可能已经不一 致。 还要注意,当输出部为空,即没有输出约束时,如果有输入约束存在,则须保留分隔标记“:”号。 回到上面的例子,读者现在应该可以理解这段代码的作用是将参数 i 的值加到 v->counter 上。代码 中的关键字 LOCK 表示在执行 addl 指令时要把系统的总线锁住,不让别的 CPU(如果系统中有不只一 个 CPU)打扰。读者也许要问,将两个数相加是很简单的操作,C 语言中明明有相应的语言成分,例如 “v->counter += i;”,为什么要用汇编呢?原因就在于,这里要求整个操作只由一条指令完成,并且要将 总线锁住,以保证操作的“原子性(atomic)”。相比之下,上述的 C 语句在编译之后到底有几条指令是 没有保证的,也无法要求在计算过程中对总线加锁。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 30 页,共 1482 页 再看一段嵌入汇编代码,这一次取自 include/asm-i386/bitops.h: 00018: #ifdef CONFIG_SMP 00019: #define LOCK_PREFIX "lock ; " 00020: #else 00021: #define LOCK_PREFIX "" 00022: #endif 00023: 00024: #define ADDR (*(volatile long *) addr) 00025: 00026: static inline void set_bit(int nr, volatile unsigned long * addr) 00027: { 00028: __asm__ __volatile__( LOCK_PREFIX 00029: "btsl %1,%0" 00030: :"=m" (ADDR) 00031: :"Ir" (nr)); 00032: } 这里的指令 btsl 将一个 32 位操作数中的某一位设置成 1。参数 nr 表示该位的位置。现在读者应该 不感到困难,也明白为什么要用汇编语言的原因了。 再来看一个复杂一点的例子,取自 include/asm-i386/string.h: 00199: static inline void * __memcpy(void * to, const void * from, size_t n) 00200: { 00201: int d0, d1, d2; 00202: __asm__ __volatile__( 00203: "rep ; movsl\n\t" 00204: "testb $2,%b4\n\t" 00205: "je 1f\n\t" 00206: "movsw\n" 00207: "1:\ttestb $1,%b4\n\t" 00208: "je 2f\n\t" 00209: "movsb\n" 00210: "2:" 00211: : "=&c" (d0), "=&D" (d1), "=&S" (d2) 00212: :"0" (n/4), "q" (n),"1" ((long) to),"2" ((long) from) 00213: : "memory"); 00214: return (to); 00215: } 读者也许知道 memcpy()。这里的__memcpy 就是内核中对 memcpy()的底层实现,用来复制一块内存 空间的内容,而忽略其数据结构。这是使用非常频繁的一个函数,所以其运行效率十分重要。 先看约束条件和变量与寄存器的结合。输出部有三个约束,对应操作数%0 至%2。其中变量 d0 为操 作数%0,必须放在寄存器 ecx 中,原因等一下就会明白。同样,d1 即%1 必须放在寄存器 edi 中;d2 即 %2 必须放在寄存器 esi 中。再看输入部,这里有四个约束,对应于操作数%3 至%6。其中操作数%3 与 操作数%0 使用同一个寄存器,所以也必须是寄存器 ecx;并且要求由 gcc 自动插入必要的指令,事先将Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 31 页,共 1482 页 其设置成 n/4,实际上是将复制长度从字节个数 n 换算成长字个数 n/4。至于 n 本身,则要求 gcc 任意分 配一个寄存器存放。操作数%5 与%6,即参数 to 与 from,分别与%1 和%2 使用相同的寄存器,所以也 必须是寄存器 edi 和 esi。 再看指令部,读者马上就能看到这里似乎只用了%4。为什么那么多的操作数似乎都没有用到呢?读 完这些指令就明白了。 第一条指令是“rep”,表示下一条指令 movl 要重复执行,每重复一遍就把寄存器 ecx 中的内容减 1, 直到变成 0 为止。所以,在这段代码中一共执行 n/4 次。那么,movl 又干些什么呢?它从 esi 所指向的 地方复制一个长字到 edi 所指的地方,并使 esi 和 edi 分别加 4。这样,当代码中的 203 行执行完毕,到 达 204 行时,所有的长字都已复制好,最多只剩下了三个字节了。在这个过程中,实际上使用了 ecx、 esi 以及 edi 三个寄存器,即%0(同时也是%3)、%2(同时也是%6)以及%1(同时也是%5)三个操作 数,这些都隐含在指令中,从字面上看不出来。同时,这也说明了为什么这些操作数必须存放在指定的 寄存器中。 接着就是处理剩下的三个字节了,先通过 testb 测试操作数%4,即复制长度 n 的最低字节中的 bit2, 如果这一位为 1 就说明还有至少两个字节,所以通过指令 movsw 复制一个短字(esi 和 edi 则分别加 2), 否则就把它跳过。再通过 testb(注意它前面时\t,表示在预处理后的汇编代码中插入一个 TAB 字符)测 试操作数%4 的 bit1,如果这一位为 1 就说明还剩下一个字节,所以通过 movsb 再复制一个字节,否则 就把它跳过。到达标号 2 的时候,执行就结束了。读者不妨自己写一段 C 代码来实现这个函数,编译以 后用 objdump 看它的实现,并与此作一比较,相信就能体会到为什么这里要采用汇编语言。 作为读者的复习材料,下面时 strcmp()的代码,不熟悉 i386 指令的读者可以找一本 Intel 的指令手册 对照着阅读。 00127: static inline int strncmp(const char * cs,const char * ct,size_t count) 00128: { 00129: register int __res; 00130: int d0, d1, d2; 00131: __asm__ __volatile__( 00132: "1:\tdecl %3\n\t" 00133: "js 2f\n\t" 00134: "lodsb\n\t" 00135: "scasb\n\t" 00136: "jne 3f\n\t" 00137: "testb %%al,%%al\n\t" 00138: "jne 1b\n" 00139: "2:\txorl %%eax,%%eax\n\t" 00140: "jmp 4f\n" 00141: "3:\tsbbl %%eax,%%eax\n\t" 00142: "orb $1,%%al\n" 00143: "4:" 00144: :"=a" (__res), "=&S" (d0), "=&D" (d1), "=&c" (d2) 00145: :"1" (cs),"2" (ct),"3" (count)); 00146: return __res; 00147: } Linux 内核源代码情景分析 2. 存储管理 2.1. Linux内存管理的基本框架 在上一章,我们介绍了 i386 CPU,包括 Pentium,在硬件层次上对内存管理所提供的支持。内存管 理最终的实现当然要由软件完成。 我们前面谈到过,i386 CPU 中的页式存储管理的基本思路是:通过页面目录和页面表分两个层次实 现从线性地址到物理地址的映射。这种映射模式在大多数情况下可以节省页面表所占用的空间。因为大 多数进程不会用到整个虚存空间,在虚存空间中通常都留有很大的“空洞”。采用两层的方式,只要一个 目录项所对应的那部分空间是个空洞,就可以把该目录项设置成“空”,从而省下了与之对应的页面表 (1024 个页面描述项)。当地址的宽度为 32 位时,两层映射机制比较有效也比较合理。但是,当地址的 宽度大于 32 位时,两层映射就显得不尽合理,不够有效了。 Linux 内核的设计要考虑到在各种不同 CPU 上的实现,还要考虑到在 64 位 CPU(如 Alpha)上的实 现,所以不能仅仅针对 i386 结构来设计它的映射机制,而要以一种假想的、虚拟的 CPU 和 MMU(内存 管理单元)为基础,设计出一种通用的模型,再把它分别落实到各种具体的 CPU 上。因此,Linux 内核 的映射机制设计成三层,在页面目录和页面表中间增设了一层“中间目录”。在代码中,页面目录称为 PGD,中间目录称为 PMD,而页面表则称为 PT。PT 中的表项则称为 PTE,PTE 是“Page Table Entry” 的缩写。PGD、PMD 和 PT 三者均为数组。相应地,在逻辑上也把线性地址从高位到低位划分成 4 个位 段,各占若干位,分别用作在目录 PGD 中的下标、中间目录 PMD 中的下标、页面表中的下标以及物理 页面内的位移。这样,对线性地址的映射就分成如图 2.1 所示的四步。 具体一点说,对于 CPU 发出的线性地址,虚拟的 Linux 内存管理单元分成如下四步完成从线性地址 到物理地址的映射: 1. 用线性地址中最高的那一个位段为下标在 PGD 中找到相应的表项,该表项指向相应的中间目录 PMD。 2. 用线性地址中的第二个位段为下标在此 PMD 中找到相应的表项,该表项指向相应页面表。 3. 用线性地址中的第三个位段作为下标在页面表中找到相应的表项 PTE,该表项中存放的就是指 向物理页面的指针。 4. 线性地址中的最后位段为物理页面内的相对位移量,将此位移量与目标物理页面的起始地址相 加便得到相应的物理地址。 PGD 基地址 PGD表项下标 PMD表项下标 PT表项下标 位移 PGD . . . PMD . . . PT . . . 物理页面 . . . 图2.1 三层地址映射示意图 2006-12-31 版权所有,侵权必究 第 32 页,共 1481 页 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 33 页,共 1481 页 但是,这个虚拟的映射模式必须落实到具体的 CPU 和 MMU 的物理映射机制。就以 i386 来说,CPU 实际上不是按三层而是按两层的模型进行地址映射的。这就需要将虚拟的三层映射落实到具体的两层映 射,跳过中间的 PMD 层次。另一方面,从 Pentium Pro 开始,Intel 引入了物理地址扩充功能 PAE,允许 地址宽度从 32 位提高到 36 位,并且在硬件上支持三层映射模型。这样,在 Pentium Pro 及以后的 CPU 上,只要将 CPU 的内存管理设置成 PAE 模式,就能使虚存的映射变成三层模式。 那么,具体对于 i386 结构的 CPU,Linux 内核是怎样实现这种映射机制的呢?首先让我们来看 include/asm-i386/pgtable.h 中的一段定义: 00106: #if CONFIG_X86_PAE 00107: # include 00108: #else 00109: # include 00110: #endif 根据在编译 Linux 内核之前的系统配置(config)过程中的选择,编译的时候会把目录 include/asm 符号连接到具体 CPU 专用的文件目录。对于 i386 CPU,该目录被符号连接到 include/asm-i386。同时, 在配置系统时还有一个选项是关于 PAE 的,如果所使用的 CPU 是 Pentium Pro 或以上时,并且决定采用 36 位地址,则在编译时选择项 CONFIG_X86_PAE 为 1,否则为 0。根据此项选择,编译时从 pgtable-3level.h 或 pgtable-2level.h 中二者选一,前者用于 36 位地址的三层映射,而后者则用于 32 位地址的二层映射。 这里,我们将集中讨论 32 位地址的二层映射。在弄清了 32 位地址的二层映射以后,读者可以自行阅读 有关 36 位地址的三层映射的代码。 文件 pgtable-2level.h 中定义了二层映射中 PGD 和 PMD 的基本结构: 00004: /* 00005: * traditional i386 two-level paging structure: 00006: */ 00007: 00008: #define PGDIR_SHIFT 22 00009: #define PTRS_PER_PGD 1024 00010: 00011: /* 00012: * the i386 is two-level, so we don't really have any 00013: * PMD directory physically. 00014: */ 00015: #define PMD_SHIFT 22 00016: #define PTRS_PER_PMD 1 00017: 00018: #define PTRS_PER_PTE 1024 这里 PGDIR_SHIFT 表示线性地址中 PGD 下标位段的起始位置,文件中将其定义为 22,也即 bit22 (第 23 位)。由于 PGD 是线性地址中最高的位段,所以该位段是从第 23 位到第 32 位,一共是 10 位。 在文件 pgtable.h 中定义了另一个常数 PGDIR_SIZE 为: 00117: #define PGDIR_SIZE (1UL << PGDIR_SHIFT) 也就是说,PGD中的每一个表项所代表的空间(并不是PGD本身所占用的空间)大小是 1×222。同 时,pgtable-2level.h中又定义了PTRS_PER_PGD,也就是每个PGD表中指针的个数为 1024。显然,这是 与线性地址中PGD位段的长度(10 位)相符的,因为 210=1024。这两个常量的定义完全是针对i386 CPULinux 内核源代码情景分析 及其MMU的,因为非PAE模式的i386 MMU用线性地址中的最高 10 位作为目录中的下标,而目录的大小 为 1024。不过,在 32 位的系统中每个指针的大小为 4 个字节,所以PGD表的大小为 4KB。 对PMD的定义就很有意思了。PMD_SHIFT也定义为 22,与PGD_SHIFT相同,表示PMD位段的长度 为 0,一个PMD表项所代表的空间与PGD表项所代表的空间是一样大的。而PMD表中指针的个数 PTRS_PER_PMD则定义为 1,表示每个PMD表中只有一个表项。同样,这也是针对i386 CPU及其MMU 而定义的,因为要将Linux逻辑上的三层映射模型落实到i386 结构物理上的二层映射,就要从线性地址逻 辑上的 4 个虚拟位段中把PMD抽去,使它的长度为 0,所以逻辑上的PMD表的大小就成为 1(20=1)。 这样,上述的 4 步映射过程对于内核(软件)和 i386 MMU 就成为: 1. 内核为 MMU 设置好映射目录 PGD,MMU 用线性地址中最高的那一个位段(10 位)作为下标 在 PGD 中找到相应的表项。该表项逻辑上指向一个中间目录 PMD,但是物理上直接指向相应 的页面表,MMU 并不知道 PMD 的存在。 2. PMD 只是逻辑上存在,即对内核软件在概念上存在,但在表中只有一个表项,而所谓的映射就 是保持原值不变,现在一转手却指向页面表了。 3. 内核为 MMU 设置好了所有的页面表,MMU 用线性地址中的 PT 位段作为下标在相应页面表中 找到相应的表项 PTE,该表项中存放的就是指向物理页面的指针。 4. 线性地址中的最后位段为物理页面内的相对位移量,MMU 将此位移量与目标物理页面的起始 地址相加便得到相应的物理地址。 这样,逻辑上的三层映射对于 i386 CPU 和 MMU 就变成了二层映射,把中间目录 PMD 这一层跳过 了,但是软件的结构却还保持着三层映射的框架。 具体的映射因空间的性质而异,但是后面读者将会看到(除用来模拟 80286 的 VM86 模式外),其 段式映射基地址总是 0,所以线性地址与虚拟地址总是一致的。在以后的讨论中,我们常常对二者不加 区分。 32 位地址意味着 4G 字节的虚存空间,Linux 内核将这 4G 字节的空间分成两部分。将最高的 1G 字 节(从虚地址 0xC0000000 至 0xFFFFFFFF),用于内核本身,称为“系统空间”。而将较低的 3G 字节(从 虚地址 0x00000000 至 0xBFFFFFFF),用作各个进程的“用户空间”。这样理论上每个进程可以使用的用 户空间都是 3G 字节。当然,实际的空间大小受到物理存储器(包括内存以及磁盘交换区或交换文件) 大小的限制。虽然各个进程拥有其自己的 3G 字节用户空间。系统空间却由所有的进程共享。每当一个 进程通过系统调用进入了内核,该进程就在共享的系统空间中运行,不再有其自己的独立空间。从具体 进程的角度看,则每个进程都拥有 4G 字节的虚存空间,较低的 3G 字节为自己的用户空间,最高的 1G 字节则为与所有进程以及内核共享的系统空间,如图 2.2 所示。 进程1的 虚拟 用户空间 3GB 进程N的 虚拟 用户空间 3GB 进程2的 虚拟 用户空间 3GB 虚拟系统空间 1GB ………… 图2.2 进程虚存空间示意图 虽然系统空间占据了每个虚存空间中最高的 1G 字节,在物理的内存中却总是从最低的地址(0)开 始。所以,对于内核来说,其地址的映射是很简单的线性映射,0xC0000000 就是两者之间的位移量。因 此,在代码中将此位移称为 PAGE_OFFSET 而定义在 page.h 中: 2006-12-31 版权所有,侵权必究 第 34 页,共 1481 页 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 35 页,共 1481 页 00068: /* 00069: * This handles the memory map.. We could make this a config 00070: * option, but too many people screw it up, and too few need 00071: * it. 00072: * 00073: * A __PAGE_OFFSET of 0xC0000000 means that the kernel has 00074: * a virtual address space of one gigabyte, which limits the 00075: * amount of physical memory you can use to about 950MB. 00076: * 00077: * If you want more physical memory than this then see the CONFIG_HIGHMEM4G 00078: * and CONFIG_HIGHMEM64G options in the kernel configuration. 00079: */ 00080: 00081: #define __PAGE_OFFSET (0xC0000000) 00082: …………………… 00113: 00114: #define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET) 00115: #define __pa(x) ((unsigned long)(x)-PAGE_OFFSET) 00116: #define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET)) 也就是说:对于系统空间而言,给定一个虚地址 x,其物理地址是 x - PAGE_OFFSET;相应地,给 定一个物理地址 x,其虚拟地址是 x + PAGE_OFFSET。 同时,PAGE_OFFSET 也代表着用户空间的上限,所以常数 TASK_SIZE 就是通过它定义的 (processor.h): 00258: /* 00259: * User space process size: 3GB (default). 00260: */ 00261: #define TASK_SIZE (PAGE_OFFSET) 这是因为在谈论一个用户进程的大小时,并不包括此进程在系统空间中共享的资源。 当然,CPU 并不是通过这里所说的计算方法进行地址映射的,__pa()只是为内核代码中当需要知道 与一个虚拟地址对应的物理地址时提供方便。例如,在切换进程的时候要将寄存器 CR3 设置成指向新进 程的页面目录 PGD,而该目录的起始地址在内核代码中是虚地址,但 CR3 所需要的是物理地址,这时 候就要用到__pa()了。这行语句在文件 mmu_context.h 中: 00043: /* Re-load page tables */ 00044: asm volatile("movl %0,%%cr3": :"r" (__pa(next->pgd))); 这是一行汇编代码,说的是将 next->pgd,即下一个进程的页面目录起始地址,通过__pa()转换成物 理地址(存放在某个寄存器),然后用 mov 指令将其写入寄存器 CR3。经过这条指令以后,CR3 就指向 新进程 next 的页面目录表 PGD 了。 前面讲过,每个进程的局部段描述表 LDT 都作为一个独立的段而存在,在全局描述表 GDT 中要有 一个表项指向这个段的起始地址,并说明该段的长度以及其他一些参数。除此之外,每个进程还有一个 TSS 结构(任务状态段)也是一样。(关于 TSS 以后还会加以讨论)所以,每个进程都要在全局段描述 表中占据两个表项。那么,GDT 的容量有多大呢?段寄存器中用作 GDT 表下标的位段宽度为 13 位,所Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 36 页,共 1481 页 以 GDT 中可以有 8192 个描述项。除一些系统的开销外(例如 GDT 中的第 2 项和第 3 项分别用于内核 的代码段和数据段,第 4 项和第 5 项永远用于当前进程的代码段和数据段,第 1 项永远是 0 等等)以外, 尚有 8180 个表项可供使用,所以理论上系统中最大的进程数量是 4090。 2.2. 地址映射的全过程 Linux 内核采用页式存储管理。虚拟地址空间划分成固定大小的“页面”,由 MMU 在运行时将虚拟 地址“映射”成(或者说变换成)某个物理内存页面中的地址。与段式存储管理相比,页式存储管理有 很多好处。首先,页面都是固定大小的,便于管理。更重要的是,当要将一部分物理空间中的内容换出 到磁盘上的时候,在段式存储管理中要将整个段(通常都很大)都换出,而在页式存储管理中则是按页 进行,效率显然要高得多。页式存储管理与段式存储管理所要求的硬件支持不同,一种 CPU 既然支持页 式存储管理,就无需再支持段式存储管理。但是,我们前面讲过,i386 的情况是特殊的。由于 i386 系列 的历史演变过程,它对页式存储管理的支持是在其段式存储管理已经存在了相当长的时间以后才发展起 来的。所以,不管程序是怎样写的,i386 CPU 一律对程序中使用的地址先进行段式映射,然后才能进行 页式映射。既然 CPU 的硬件结构是这样,Linux 内核也只好服从 Intel 的选择。这样的双重映射其实是毫 无必要的,也使映射的过程变得不容易理解,以至有人还得出了 Linux 采用“段页式”存储管理技术这 样一种似是而非的结论。下面读者将会看到,Linux 内核所采取的办法是使段式映射的过程实际上不起 作用(除特殊的 VM86 模式外,那是用来模拟 80286 的)。也就是说,“你有政策,我有对策”,惹不起 就躲着走。本节将通过一个情景,看看 Linux 内核在 i386 CPU 上运行时地址映射的全过程。这里要指出, 这个过程仅是对 i386 处理器而言的。对于其他的处理器,比如说 M68K、Power PC 等,就根本不存在段 式映射这一层了。反之,不管是什么操作系统(例如 UNIX),只要是在 i386 上实现,就必须至少在形 式上要先经过段式映射,然后才可以实现其本身的设计。 假定我们写了这么一个程序: #include greeting() { printf(“Hello, would!\n”); } main() { greeting(); } 读者一定很熟悉。这个程序与大部分人写的第一个 C 程序只有一点不同,我们故意让 main()调用 greeting()来显示或打印“Hello, would!”。 经过编译以后,我们得到可执行代码 hello。先来看看 gcc 和 ld(编译和连接)执行后的结果。Linux 有一个实用程序 objdump 是非常有用的,可以用来反汇编一段二进制代码。通过命令: % objdump -d hello 可以得到我们所关心的那部分结果,输出的片段(反汇编的结果)为: 08048568 : 8048568: 55 pushl %ebp 8048569: 89 e5 movl %esp,%ebp 804856b: 68 04 94 04 08 pushl $0x8049404 8048570: e8 ff fe ff ff call 8048474 Linux 内核源代码情景分析 8048575: 83 c4 04 addl $0x4,%esp 8048578: c9 leave 8048579: c3 ret 804857a: 89 f6 movl %esi,%esi 0804857c
: 804857c: 55 pushl %ebp 804857d: 89 e5 movl %esp,%ebp 804857f: e8 e4 ff ff ff call 8048568 8048584: c9 leave 8048585: c3 ret 8048586: 90 nop 8048587: 90 nop 从上列结果可以看到,ld 给 greeting()分配的地址为 0x08048568。在 elf 格式的可执行代码中,ld 总 是从 0x08000000 开始安排程序的“代码段”,对每个程序都是这样。至于程序在执行时在物理内存中的 实际位置则就要由内核在为其建立内存映射时临时作出安排,具体地址则取决于当时所分配到的物理内 存页面。 假定该程序已经在运行,整个映射机制都已经建立好了,并且 CPU 正在等待执行 main()中的“call 08048568”这条指令,要转移到虚拟地址 0x08048568 去。接下来就请读者耐着性子跟随我们一步一步地 走过这个地址的映射过程。 首先是段式映射阶段。由于地址 0x08048568 是一个程序的入口,更重要的是在执行的过程中是由 CPU 中的“指令寄存器”EIP 所指向的,所以在代码段中。因此,i386 CPU 使用代码段寄存器 CS 的当 前值来作为段式映射的“选择码”,也就是用它作为在段描述表中的下标。哪一个段描述表呢?是全局段 描述表还是局部段描述表 LDT?那就要看 CS 中的内容了。先重温一下保护模式下段寄存器的格式,见 图 2.3。 Index 15 0 RPL 1 T I 23 Request Privilege Level Table Indicator,0=GDT,1=LDT 图2.3 段寄存器格式定义 也就是说,当 bit2 为 0 时表示用 GDT,为 1 时表示用 LDT。Intel 的设计意图是内核用 GDT 而各个 进程都用自己的 LDT。最低两位 RPL 为所要求的特权级别,共分为 4 级,0 为最高。 现在,可以来看看 CS 的内容了。内核在建立一个进程时都要将其段寄存器设置好(在进程管理一 章中要讲到这个问题),有关代码在 include/asm-i386/processor.h 中: 00408: #define start_thread(regs, new_eip, new_esp) do { \ 00409: __asm__("movl %0,%%fs ; movl %0,%%gs": :"r" (0)); \ 00410: set_fs(USER_DS); \ 00411: regs->xds = __USER_DS; \ 00412: regs->xes = __USER_DS; \ 00413: regs->xss = __USER_DS; \ 2006-12-31 版权所有,侵权必究 第 37 页,共 1481 页 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 38 页,共 1481 页 00414: regs->xcs = __USER_CS; \ 00415: regs->eip = new_eip; \ 00416: regs->esp = new_esp; \ 00417: } while (0) 这里 regs->xds 是段寄存器 DS 的映像,余类推。这里已经可以看到一个有趣的事,就是除 CS 被设 置成 USER_CS 外,其他所有的段寄存器都设置成 USER_DS。这里特别值得注意的是堆栈寄存器 SS, 它也被设置成 USER_DS。就是说,虽然 Intel 的意图是将一个进程的映像分成代码段、数据段和堆栈段, Linux 内核却并不买这个帐。在 Linux 内核中堆栈段和数据段是不分的。 再来看看 USER_CS 和 USER_DS 到底是什么。那是在 include/asm-i386/segment.h 中定义的: 00004: #define __KERNEL_CS 0x10 00005: #define __KERNEL_DS 0x18 00006: 00007: #define __USER_CS 0x23 00008: #define __USER_DS 0x2B 也就是说,Linux 内核中只使用四种不同的段寄存器数值,两种用于内核本身,两种用于所有的进 程。现在,我们将这四种数值用二进制展开并与段寄存器的格式相对照: index TI RPL ----------------------------------------------------------- __KERNEL_CS 0x10 0 0 0 0 0 0 0 0 0 0 0 1 0 | 0 | 0 0 __KERNEL_DS 0x18 0 0 0 0 0 0 0 0 0 0 0 1 1 | 0 | 0 0 __USER_CS 0x23 0 0 0 0 0 0 0 0 0 0 1 0 0 | 0 | 1 1 __USER_DS 0x28 0 0 0 0 0 0 0 0 0 0 1 0 1 | 0 | 1 1 ----------------------------------------------------------- 一对照就清楚了,那就是: __KERNEL_CS: index = 2, TI = 0, RPL = 0 __KERNEL_DS: index = 3, TI = 0, RPL = 0 __USER_CS: index = 4, TI = 0, RPL = 3 __USER_DS: index = 5, TI = 0, RPL = 3 首先,TI 都是 0,也就是说全部都使用 GDT。这就与 Intel 的设计意图不一致了。实际上,在 Linux 内核中基本上不使用局部段描述表 LDT。LDT 只是在 VM86 模式中运行 wine 以及其他在 Linux 上模拟 Windows 软件或 DOS 软件的程序中才使用。 再看 RPL,只用了 0 和 3 两级,内核为 0 级而用户(进程)为 3 级。 回到我们的程序中。我们的程序显然不属于内核,所以在进程的用户空间中运行,内核在调度该进 程进入运行时,把 CS 设置成__USER_CS,即 0x23。所以,CPU 以 4 为下标,从全局段描述表 GDT 中 找到对应的段描述项。 初始的 GDT 内容是在 arch/i386/kernel/head.s 中定义的,其主要内容在运行中并不改变: 00444: /* 00445: * This contains typically 140 quadwords, depending on NR_CPUS. 00446: * 00447: * NOTE! Make sure the gdt descriptor in head.S matches this if you 00448: * change anything. 00449: */ Linux 内核源代码情景分析 00450: ENTRY(gdt_table) 00451: .quad 0x0000000000000000 /* NULL descriptor */ 00452: .quad 0x0000000000000000 /* not used */ 00453: .quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */ 00454: .quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */ 00455: .quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */ 00456: .quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */ 00457: .quad 0x0000000000000000 /* not used */ 00458: .quad 0x0000000000000000 /* not used */ GDT 中的第一项(下标为 0)是不用的,这是为了防止在加电后段寄存器未经初始化就进入保护模 式并使用 GDT,这也是 Intel 的规定。第二项也不用。从下标 2 至 5 共 4 项对应于前面的四种段寄存器 数值。为了便于对照,下面再次在图 2.4 中给出段描述项的格式。 AES RWED/CDPLP A=0本段未被访问 A=1本段已被访问 W=1,可写入 W=0,不能被写入 ED=1,向下伸展(堆栈段) ED=0,向上伸展(数据段) E=0,数据段 R=1,可读 R=0,不可读 C=1,遵循特权级 C=0,忽视特权级 E=1,代码段 S=0,系统描述项 S=1,代码或数据段描述符 P=0,描述项无效 P=1,描述符有效 DPL=00-11,本段特权 G D/B O AV 5255 可由软件使用,CPU忽略该位 永远为0 =1,表示对该段的访问为32位指令;=0,为16位指令 =1,段长以4K字节为单位;=0,以字节为单位 L19 - L16 48515255 B15 - B0 1631 B31 - B24 5663 4047 B23 - B16 3239 L15 - L0 015 78 图2.4 段描述项定义 同时,将 4 个段描述项的内容按二进制展开如下: K_CS: 0000 0000 1100 1111 1001 1010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 2006-12-31 版权所有,侵权必究 第 39 页,共 1481 页 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 40 页,共 1481 页 K_DS: 0000 0000 1100 1111 1001 0010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 U_CS: 0000 0000 1100 1111 1111 1010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 U_DS: 0000 0000 1100 1111 1111 0010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 读者结合图 2.4 段描述项的定义仔细对照,可以得出如下结论: (1)四个段描述项的下列内容都是相同的。 B0-B15、B16-B31 都是 0 —— 基地址全为 0; L0-L15、L16-L19 都是 1 —— 段的上限全是 0xfffff; G 位都是 1 —— 段长单位均为 4KB; D 位都是 1 —— 对四个段的访问都是 32 位指令; P 位都是 1 —— 四个段都在内存。 结论:每个段都是从 0 地址开始的整个 4GB 虚存空间,虚地址到线性地址的映射保持原值不变。因 此,讨论或理解 Linux 内核的页式映射时,可以直接将线性地址当作虚拟地址,二者完全一致。 (2)有些区别的地方只是在 bit40~bit46,对于描述项中的 type 以及 S 标志和 PDL 位段。 对 KERNEL_CS:DPL=0,表示 0 级;S 位为 1,表示代码段或数据段;type 为 1010,表示代 码段,可读,可执行,尚未受到访问。 对 KERNEL_DS:DPL=0,表示 0 级;S 位为 1,表示代码段或数据段;type 为 0010,表示数 据段,可读,可写,尚未受到访问。 对 USER_CS:DPL=3,表示 3 级;S 位为 1,表示代码段或数据段;type 为 1010,表示代码段, 可读,可执行,尚未受到访问。 对 USER_DS:DPL=3,表 示 3 级;S 位为 1,表示代码段或数据段;type 为 0010,表示数据段, 可读,可写,尚未受到访问。 有区别的其实只有两个地方:一是 DPL,内核为最高的 0 级,用户为最低的 3 级;另一个是段的类 型,或为代码,或为数据。这两项都是 CPU 在映射过程中要加以检查核对的。如果 DPL 为 0 级,而段 寄存器 CS 中的 DPL 为 3 级,那就不允许了,因为那说明 CPU 的当前运用级别比想要访问的区段要低。 或者,如果段描述项说是数据段,而程序中通过 CS 来访问,那也不允许。实际上,这里所作的检查比 对在页式映射的过程中还要进行,所以既然用了页式映射,这里的检查比对就是多余的。要不是 i386 CPU 中的 MMU 要作这样的检查比对,那就根本不需要段描述项和段寄存器了。所以,这里 Linux 内核只不 过是装模作样地糊弄 i386 CPU,对付其检查比对而已。 读者也许会问:如此说来,怀有恶意的程序员岂不是可以通过设置寄存器 CS 或 DS,甚至连这也不 用,就可以打破 i386 的段式保护机制吗?是的,但是不要忘记,Linux 内核之所以这样安排,原因在于 它采用的是页式存储管理,这里只不过是在对付本来就毫无必要却又非得如此的例行公事而已。真正重 要的是页式映射阶段的保护机制。 所以,Linux 内核设计的段式映射机制把地址 0x08048568 映射到了其自身,现在作为线性地址出现 了。下面才进入了页式映射的过程。 与段式映射过程中所有进程全都共用一个 GDT 不一样,现在可是动真格的了,每个进程都有其自 己的页面目录 PGD,指向这个目录的指针保持在每个进程的 mm_struct 数据结构中。每当调度一个进程 进入运行的时候,内核都要为即将运行的进程设置好控制寄存器 CR3,而 MMU 的硬件则总是从 CR3 中取得指向当前页面目录的指针。不过,CPU 在执行程序时使用的是虚存地址,而 MMU 硬件在进行映 射时所用的则是物理地址。这是在 inline 函数 switch_mm() 中完成的,其代码见 include/asm-i386/mmu_context.h。但是我们在此关心的只是其中关键的一行: 00028: static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk, unsigned cpu) 00029: { …………………… Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 41 页,共 1481 页 00044: asm volatile("movl %0,%%cr3": :"r" (__pa(next->pgd))); …………………… 00059: } 我们以前曾用这行代码说明__pa()的用途,这里将下一个进程的页面目录 PGD 的物理地址装入寄存 器%%cr3,也即 CR3。细心的读者可能会问:这样,在这一行以前和以后 CR3 的值不一样,也就是使用 不同的页面目录,不会使程序的执行不能连续了吗?答案是,这是在内核中。不管什么进程,一旦进入 内核就进了系统空间,都有相同的页面映射,所以不会有问题。 当我们在程序中要转移到地址 0x08048568 去的时候,进程正在运行中,CR3 早已设置好,指向我 们这个进程的页面目录了。先将线性地址 0x08048568 按二进制展开: 0000 1000 0000 0100 1000 0101 0110 1000 对照线性地址的格式,可见最高 10 位为二进制的 0000100000,也就是十进制的 32,所以 i386 CPU (确切地说是 CPU 中的 MMU,下同)就以 32 为下标去页面目录中找到其目录项。这个目录项中的高 20 位指向一个页面表。CPU 在这 20 位后边添加上 12 个 0 就得到该页面表的指针。以前我们讲过,每个 页面表占一个页面,所以自然就是 4K 字节边界对齐的,其起始地址的低 12 位一定是 0。正因为如此, 才可以把 32 位目录项中的低 12 位挪作他用,其中的最低位为 P 标志位,为 1 时表示该页面表在内存中。 找到页面表以后,CPU 再看线性地址中的中间 10 位。线性地址 0x08048568 的第二个 10 位为 0001001000,即十进制的 72。于是 CPU 就以此为下标在已经找到的页表中找到相应的表项。与目录项 相似,当页面表项的 P 标志位为 1 时表示所映射的物理页面在内存中。32 位的页面表项中的高 20 位指 向一个物理页面,在后边添上 12 个 0 就得到这物理内存页面的起始地址。所不同的是,这一次指向的不 再是一个中间结构,而是映射的目标页面了。在其起始地址上加上线性地址中的最低 12 位,就得到了最 终的物理内存地址。这时这个线性地址的最低 12 位为 0x568。所以,如果目标页面的起始地址为 0x00740000 的话(具体取决于内核中的动态分配),那么 greeting 入口的物理地址就是 0x00740568, greeting()的执行代码就存储在这里。 读者可能已经注意到,在页面映射的过程中,i386 CPU 要访问内存三次。第一次是页面目录,第二 次是页面表,第三次才是访问真正的目标。所以虚存的高效实现有赖于高速缓存(cache)的实现。有了 高速缓存,虽然在第一次用到具体的页面目录和页面表时要到内存中去读取,但一旦装入了高速缓存以 后,一般都可以在高速缓存中找到,而不需要再到内存中去读取了。另一方面,这整个过程是由硬件来 实现的,所以速度很快。 除常规的页式映射之外,为了能在 Linux 内核上仿真运行采用段式存储管理的 Windows 或 DOS 软 件,还提供了两个特殊的、与段式存储管理有关的系统调用。 2.2.1. modify_ldt(int func, void *ptr, unsigned long bytecount) 这个系统调用可以用来改变当前进程的局部段描述表。在自由软件基金会 FSF 下面,除 Linux 以外 还有许多项目在进行。其中有一个叫“WINE”,其名字来自“Windows Emulation”,目的是在 Linux 上 仿真运行 Windows 的软件。多年来,有些 Windows 软件已经广泛地为人们所接受和熟悉(如 MS Word 等),而在 Linux 上没有相同的软件往往成了许多人不愿意转向 Linux 的原因。所以,在 Linux 上建立一 个环境,使得用户可以在上面运行 Windows 的软件,就成了一个开拓市场的举措。而系统调用 modify_ldt() 就是因开发 WINE 的需要而设置的。当 func 参数的值为 0 时,该调用返回本进程局部段描述表的实际大 小,而表的内容就在用户通过 ptr 提供的缓冲区中。当 func 参数的值为 1 时,ptr 应指向一个结构 modify_ldt_ldt_s 。而 bytecount 则为 sizeof(struct modify_ldt_ldt_s) 。该数据结构的定义见于 include/asm-i386/ldt.h: 00015: struct modify_ldt_ldt_s { 00016: unsigned int entry_number; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 42 页,共 1481 页 00017: unsigned long base_addr; 00018: unsigned int limit; 00019: unsigned int seg_32bit:1; 00020: unsigned int contents:2; 00021: unsigned int read_exec_only:1; 00022: unsigned int limit_in_pages:1; 00023: unsigned int seg_not_present:1; 00024: unsigned int useable:1; 00025: }; 其中 entry_number 是想要改变的表项的序号,即下标。而结构中其余的成分则给出要设置到各个位 段中去的值。 读者可能会要问:这样岂不是在内存管理机制上挖了个洞?既然一个进程可以改变它的局部段描述 表,它岂不就可设法侵犯到其他进程或内核的空间中去?这要从两方面来看。一方面它确实是在内存管 理机制上开了一个小小的缺口,但另一方面它的背后仍然是 Linux 内核的页式存储管理,只要不让用户 进程掌握修改页面目录和页面表的手段,系统就还是安全的。 2.2.2. vm86(struct vm86_struct *info) 与 modify_ldt()相类似,还有一个系统调用 vm86(),用来在 Linux 上模拟运行 DOS 软件。i386 CPU 专门提供了一种寻址方式 VM86,用来在保护模式下模拟运行实址模式(real-mode)的软件。其目的是 为采用保护模式的系统(如 Windows,OS/2 等)提供与实模式软件(常常是 DOS 软件)的兼容性。事 至如今,需要加以模拟运行 DOS 软件已经很少了,或者干脆已经绝迹了。所以本书在 80386 的寻址方式 一节中略去了 VM86 模式的内容,有兴趣的读者可以参照 Intel 的技术资料,自行阅读内核中有关的源代 码,主要有 include/asm-i386/vm86.h 和 arch/kernel/vm86.c。 显然,这两个系统调用以及由此实现的功能实际上并不属于 Linux 内核本身的存储管理框架,而是 为了与 Windows 软件和 DOS 软件兼容而采取的权宜之计。 2.3. 几个重要的数据结构和函数 从硬件的角度来说,Linux 内核只要能为硬件准备好页面目录 PGD、页面表 PT 以及全局段描述表 GDT 和局部段描述表 LDT,并正确地设置有关的寄存器,就完成了内存管理机制中地址映射部分的准备 工作。虽然最终的目的是地址映射,但是实际上内核所需要做的工作却要复杂得多。在与内存管理有关 的内核代码中,有几个数据结构是很重要的,这些数据结构及其使用构成了代码中内存管理的基本框架。 页面目录 PGD、中间目录 PMD 和页面表 PT 分别是由表项 pgd_t、pmd_t 以及 pte_t 构成的数组,而 这些表项又都是数据结构,定义在 include/asm-i386/page.h 中: 00036: /* 00037: * These are used to make use of C type-checking… 00038: */ 00039: #if CONFIG_X86_PAE 00040: typedef struct { unsigned long pte_low, pte_high; } pte_t; 00041: typedef struct { unsigned long long pmd; } pmd_t; 00042: typedef struct { unsigned long long pgd; } pgd_t; 00043: #define pte_val(x) ((x).pte_low | ((unsigned long long)(x).pte_high << 32)) 00044: #else 00045: typedef struct { unsigned long pte_low; } pte_t; 00046: typedef struct { unsigned long pmd; } pmd_t; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 43 页,共 1481 页 00047: typedef struct { unsigned long pgd; } pgd_t; 00048: #define pte_val(x) ((x).pte_low) 00049: #endif 00050: #define PTE_MASK PAGE_MASK 可见,当采用 32 位地址时,pgd_t、pmd_t 和 pte_t 实际上就是长整数,而当采用 36 位地址时则是 long long 整数。之所以不直接定义成长整数的原因在于这样可以让 gcc 在编译时加以严格的类型检查。 同时,代码中又定义了几个简单的函数来访问这些数据结构的成分,如 pte_val()、pgd_val()等(难怪有 人说 Linux 内核的代码吸收了面向对象的程序设计手法)。但是,如我们以前说过的那样,表项 PTE 作 为指针实际上需要它的高 20 位。同时,所有的物理页面都是跟 4K 字节的边界对齐的,因而物理页面起 始地址的高 20 位又可看作是物理页面的序号。所以,pte_t 中的低 12 位用于页面的状态信息和访问权限。 在内核代码中并没有在 pte_t 等结构中定义有关的位段,而是在 page.h 中另行定义了一个用来说明页面 保护的结构 pgprot_t: 00052: typedef struct { unsigned long pgprot; } pgprot_t; 参数 pgprot 的值与 i386 MMU 的页面表项的低 12 位相对应,其中 9 位是标志位,表示所映射页面 的当前状态和访问权限(详见的第 1 章)。内核代码中作了相应的定义(include/asm-i386/pgtable.h): 00148: #define _PAGE_PRESENT 0x001 00149: #define _PAGE_RW 0x002 00150: #define _PAGE_USER 0x004 00151: #define _PAGE_PWT 0x008 00152: #define _PAGE_PCD 0x010 00153: #define _PAGE_ACCESSED 0x020 00154: #define _PAGE_DIRTY 0x040 00155: #define _PAGE_PSE 0x080 /* 4 MB (or 2MB) page, Pentium+, if present.. */ 00156: #define _PAGE_GLOBAL 0x100 /* Global TLB entry PPro+ */ 00157: 00158: #define _PAGE_PROTNONE 0x080 /* If not present */ 注意这里的_PAGE_PROTNONE 对应于页面表项的 bit7,在 Intel 的手册中说这一位保留不用,所以 对 MMU 不起作用。 在实际使用中,pgprot 的数值总是小于 0x1000,而 pte 中的指针部分则总是大于 0x1000,将二者合 在一起就得到实际应用于页面表中的表项。具体的计算是由 pgtable.h 中定义的宏操作 mk_pte 完成的: 00061: #define __mk_pte(page_nr,pgprot) \ __pte(((page_nr) << PAGE_SHIFT) | pgprot_val(pgprot)) 这里将页面的序号左移 12 位,再与页面的控制/状态位段相或,就得到了表项的值。这里引用的两 个宏操作均定义于 include/asm-i386/page.h 中: 00056: #define pgprot_val(x) ((x).pgprot) 00057: 00058: #define __pte(x) ((pte_t) { (x) } ) 内核中有个全局变量 mem_map,是一个指针,指向一个 page 数据结构的数组(下面会讨论 page 结Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 44 页,共 1481 页 构),每个 page 数据结构代表着一个物理页面,整个数组就代表着系统中的全部物理页面。因此,页面 表项的高 20 位对于软件和 MMU 有着不同的意义。对于软件,这是一个物理页面的序号,将这个序号用 作下标就可以从 mem_map 找到代表这个物理页面的 page 数据结构。对于硬件,则(在低位补上 12 个 0) 就是物理页面的起始地址。 还有一个常用的宏操作 set_pte(),用来把一个表项的值设置到一个页面表项中,这个宏操作定义于 include/asm-i386/pgtable-2level.h 中: 00042: #define set_pte(pteptr, pteval) (*(pteptr) = pteval) 在映射的过程中,MMU 首先检查的是 P 标志位,就是上面的_PAGE_PRESENT,它指示着所映射 的页面是否在内存中。只有在 P 标志位为 1 的时候 MMU 才会完成映射的全过程;否则就会因为不能完 成映射而产生一次缺页异常,此时表项中的其他内容对 MMU 就没有任何意义了。除 MMU 硬件根据页 面表项的内容进行页面映射外,软件也可以设置或检测页面表项的内容,上面的 set_pte()就是用来设置 页面表项。内核中还为检测页面表项的内容定义了一些工具性的函数或宏操作,其中最重要的有: 00060: #define pte_none(x) (!(x).pte_low) 00248: #define pte_present(x) ((x).pte_low & (_PAGE_PRESENT | _PAGE_PROTNONE)) 对软件来说,页面表项为 0 表示尚未为这个表项(所代表的虚存页面)建立映射,所以还是空白; 而如果页面表项不为 0,但 P 标志位为 0,则表示映射已经建立,但是所映射的物理页面不在内存中(已 经交换出到交换设备上,详见后面的页面交换)。 00269: static inline int pte_dirty(pte_t pte) { return (pte).pte_low & _PAGE_DIRTY; } 00270: static inline int pte_young(pte_t pte) { return (pte).pte_low & _PAGE_ACCESSED; } 00271: static inline int pte_write(pte_t pte) { return (pte).pte_low & _PAGE_RW; } 当然,这些标志位只有在 P 标志位为 1 时才有意义。 如前所述,当页面表项的 P 标志位为 1 时,其高 20 位为相应的物理页面起始地址的高 20 位,由于 物理页面的起始地址必然是与页面边界对齐的,所以低 12 位一定是 0。如果把整个物理内存看成一个物 理页面的“数组”,那么这高 20 位( 右 移 12 位以后)就是数组的下标,也就是物理页面的序号。相应地, 用这个下标,就可以在上述的 page 结构数组中找到代表目标物理页面的数据结构。代码中为此也定义了 一个宏操作(include/asm-i386/pgtable.h): 00059: #define pte_page(x) (mem_map+((unsigned long)(((x).pte_low >> PAGE_SHIFT)))) 由于 mem_map 是 page 结构的指针,操作的结果也是个 page 结构指针,mem_map+x 与&mem_map[x] 是一样的。在内核的代码中,还常常需要根据虚存地址找到相应物理页面的 page 数据结构,所以还为此 也定义了一个宏操作(include/asm-i386/page.h): 00117: #define virt_to_page(kaddr) (mem_map + (__pa(kaddr) >> PAGE_SHIFT)) 代表物理页面的 page 数据结构是在文件 include/linux/mm.h 中定义的: 00126: /* 00127: * Try to keep the most commonly accessed fields in single cache lines 00128: * here (16 bytes or greater). This ordering should be particularly 00129: * beneficial on 32-bit processors. Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 45 页,共 1481 页 00130: * 00131: * The first line is data used in page cache lookup, the second line 00132: * is used for linear searches (eg. clock algorithm scans). 00133: */ 00134: typedef struct page { 00135: struct list_head list; 00136: struct address_space *mapping; 00137: unsigned long index; 00138: struct page *next_hash; 00139: atomic_t count; 00140: unsigned long flags; /* atomic flags, some possibly updated asynchronously */ 00141: struct list_head lru; 00142: unsigned long age; 00143: wait_queue_head_t wait; 00144: struct page **pprev_hash; 00145: struct buffer_head * buffers; 00146: void *virtual; /* non-NULL if kmapped */ 00147: struct zone_struct *zone; 00148: } mem_map_t; 内核中用来表示这个数据结构的变量名常常是 page 或 map。 当页面的内容来自一个文件时,index 代表着该页面在文件中的序号;当页面的内容被换出到交换设 备上,但还保留着内容作为缓冲时,则 index 指明了页面的去向。结构中各个成分的次序是有讲究的, 目的是尽量使得联系紧密的若干成分在执行时被装填入高速缓存的同一缓冲线(16 个字节)中。 系统中的每一个物理页面都有一个 page 结构(或 mem_map_t)。系统初始化时根据物理内存的大小 建立起一个 page 结构数组 mem_map,作为物理页面的“仓库”,里面的每个 page 数据结构都代表着一 个物理页面。每个物理页面的 page 结构在这个数组里的下标就是该物理页面的序号。“仓库”里的物理 页面划分成 ZONE_DMA 和 ZONE_NORMAL 两个管理区(根据系统配置,还可能有第三个管理区 ZONE_HIGHMEM,用于物理地址超过 1GB 的存储空间)。 管理区 ZONE_DMA 里的页面是专供 DMA 使用的。为什么供 DMA 使用的页面要单独加以管理呢? 首先,DMA 使用的页面是磁盘 I/O 所必需的,如果把仓库中所有的物理页面都分配光了,那就无法使用 页面与盘区的交换了。此外,还有特殊的原因。在 i386 CPU 中,页式存储管理的硬件支持是在 CPU 内 部实现的,而不是像另有些 CPU 那样由一个单独的 MMU 提供,所以 DMA 不经过 MMU 提供的地址映 射。这样,外部设备就要直接提供访问物理页面的地址,可是有些外设(特别是插在 ISA 总线上的外设 接口卡)在这方面往往有些限制,要求用于 DMA 的物理地址不能过高。另一方面,正因为 DMA 不经 过 MMU 提供的地址映射,当 DMA 所需的缓冲区超过一个物理页面的大小时,就要求两个页面在物理 上连续,因为此时 DMA 控制器不能依靠 CPU 内部的 MMU 将连续的虚存页面映射到物理上不连续的页 面。所以,用于 DMA 的物理页面是要单独加以管理的。 每个管理区都有一个数据结构,即zone_struct数据结构。在zone_struct数据结构中有一组“空闲区” (free_area_t)队列。为什么是“一组”队列,而不是“一个”队列呢?这也是因为常常需要成“块”地 分配在物理空间内连续的多个页面,所以要按块的大小分别加以管理。因此,在管理区数据结构中既要 有一个队列来保持一些离散(连续长度为 1)的物理页面,还要有一个队列来保存一些连续长度为 2 的 页面块以及连续长度为 4、8、16、…、直至 2MAX_ORDER的页面块。常数MAX_ORDER定义为 10,也就 是说最大的连续页面块可以达到 210=1024 个页面,即 4M字节。这两个数据结构以及几个常数都是在文 件include/linux/mmzone.h中定义的: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 46 页,共 1481 页 00011: /* 00012: * Free memory management - zoned buddy allocator. 00013: */ 00014: 00015: #define MAX_ORDER 10 00016: 00017: typedef struct free_area_struct { 00018: struct list_head free_list; 00019: unsigned int *map; 00020: } free_area_t; 00021: 00022: struct pglist_data; 00023: 00024: typedef struct zone_struct { 00025: /* 00026: * Commonly accessed fields: 00027: */ 00028: spinlock_t lock; 00029: unsigned long offset; 00030: unsigned long free_pages; 00031: unsigned long inactive_clean_pages; 00032: unsigned long inactive_dirty_pages; 00033: unsigned long pages_min, pages_low, pages_high; 00034: 00035: /* 00036: * free areas of different sizes 00037: */ 00038: struct list_head inactive_clean_list; 00039: free_area_t free_area[MAX_ORDER]; 00040: 00041: /* 00042: * rarely used fields: 00043: */ 00044: char *name; 00045: unsigned long size; 00046: /* 00047: * Discontig memory support fields. 00048: */ 00049: struct pglist_data *zone_pgdat; 00050: unsigned long zone_start_paddr; 00051: unsigned long zone_start_mapnr; 00052: struct page *zone_mem_map; 00053: } zone_t; 00054: 00055: #define ZONE_DMA 0 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 47 页,共 1481 页 00056: #define ZONE_NORMAL 1 00057: #define ZONE_HIGHMEM 2 00058: #define MAX_NR_ZONES 3 管理区结构中的 offset 表示该分区在 mem_map 中的起始页面号。一旦建立起了管理区,每个物理页 面便永久地属于某一个管理区,具体取决于页面的起始地址,就好像一幢建筑物属于哪一个派出所管辖 取决于其地址一样。空闲区 free_area_struct 结构中用来维持双向链队列的结构 list_head 是一个通用的数 据结构,Linux 内核中需要使用双向链队列的地方都使用这种数据结构。结构很简单,就是 prev 和 next 两个指针。回到上面的 page 结构,其中的第一个成分就是一个 list_head 结构,物理页面的 page 结构正 是通过它进入 free_area_struct 结构中的双向链队列的。在“物理页面的分配”一节中,我们将讲述内核 是怎样从它的仓库中分配一块物理空间,即若干连续的物理页面。 在传统的计算机结构中,整个物理空间都是均匀一致的,CPU 访问这个空间中的任何一个地址所需 要的时间都相同,所以称为“均质存储结构”(Uniform Memory Architecture),简称 UMA。可是,在一 些新的系统结构中,特别是在多 CPU 结构的系统中,物理内存空间在这方面的一致性却成了问题。试想 有这么一种系统结构: 系统的中心是一条总线,例如 PCI 总线。 有多个 CPU 模块连接在系统总线上,每个 CPU 模块都有本地的物理内存,但是也可以通过系 统总线访问其他 CPU 模块上的内存。 系统总线上还连接着一个公用的存储模块,所有的 CPU 模块都可以通过系统总线来访问它。 所有这些物理内存的地址互相连续而形成一个连续的物理地址空间。 显然,就某个特定的 CPU 而言,访问其本地的存储器是速度最快的,而穿过系统总线访问公用存储 模块或其他 CPU 模块上的存储器就比较慢,而且还面临因可能的竞争而引起的不确定性。也就是说,在 这样的系统中,其物理内存空间虽然连续,“质地”却不一致,所以称为“非均匀存储结构”(Non-Uniform Memory Architecture),简称 NUMA。在 NUMA 结构的系统中,分配连续的若干物理页面时一般都要求 分配在质地相同的区间(称为 node,即“节点”)。举例来说,要是 CPU 模块 1 要求分配 4 个物理页面, 可是由于本模块上的空间已经不够,所以前 3 个页面分配在本模块,而最后一个页面却分配到了 CPU 模 块 2 上,那显然是不合适的。在这样的情况下,将 4 个页面都分配在公用模块上显然要好得多。 事实上,严格意义上的 UMA 结构几乎是不存在的。就拿配置最简单的单 CPU 的 PC 来说,其物理 存储空间就包括了 RAM、ROM(用于 BIOS),还有图形卡上的静态 RAM。但是在 UMA 结构中,除“主 存”RAM 以外的存储器都很小,所以把它们放在特殊的地址上成为小小的“孤岛”,再在编程时特别加 以注意就可以了。然而,在典型的 NUMA 结构中就需要来自内核中内存管理机制的支持了。由于多处 理器结构的系统日益广泛的应用,Linux 内核 2.4.0 版提供了对 NUMA 的支持(作为一个编译可选项)。 由于 NUMA 结构的引入,对于上述的物理页面管理机制也作了相应的修改。管理区不再是属于最 高层的机构,而是在每个存储节点中都有至少两个管理区。而且前述的 page 结构数组也不再是全局性的, 而是从属于具体的节点了。从而,在 zone_struct 结构(以及 page 结构数组)之上又有了一层代表着存储 节点的 pglist_data 数据结构,定义于 include/linux/mmzone.h 中: 00079: typedef struct pglist_data { 00080: zone_t node_zones[MAX_NR_ZONES]; 00081: zonelist_t node_zonelists[NR_GFPINDEX]; 00082: struct page *node_mem_map; 00083: unsigned long *valid_addr_bitmap; 00084: struct bootmem_data *bdata; 00085: unsigned long node_start_paddr; 00086: unsigned long node_start_mapnr; 00087: unsigned long node_size; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 48 页,共 1481 页 00088: int node_id; 00089: struct pglist_data *node_next; 00090: } pg_data_t; 显然,若干存储节点的 pglist_data 数据结构可以通过指针 node_next 形成一个单链队列。每个结构 中的指针 node_mem_map 指向具体节点的 page 数据数组,而数组 node_zones[]就是该节点的最多三个页 面管理区。反过来,在 zone_struct 结构中也有一个指针 zone_pgdat,指向所属节点的 pglist_data 数据结 构。 同时,又在 pglist_data 结构里设置了一个数组 node_zonelists[],其类型定义也在同一文件中: 00071: typedef struct zonelist_struct { 00072: zone_t * zones [MAX_NR_ZONES+1]; // NULL delimited 00073: int gfp_mask; 00074; } zonelist_t; 这里的 zones[]是个指针数组,各个元素按特定的次序指向具体的页面管理区,表示分配页面时先试 zone[0]所指向的管理区,如不能满足要求就试 zones[1]所指向的管理区,等等。这些管理区可以属于不 同的存储节点。这样针对上面所举的例子就可以规定:先试本节点,即 CPU 模块 1 的 ZONE_DMA 管理 区,若不够 4 个页面就全部从公用模块的 ZONE_DMA 管理区中分配。就是说每个 zonelist_t 规定了一种 分配策略。然而,每个存储节点不应该只有一种分配策略,所以在 pglist_data 结构中提供的是一个 zonelist_t 数组,数组的大小为 NR_GFPINDEX,定义为: 00076: #define NR_GFPINDEX 0x100 就是说,最多可以规定 256 种不同的策略。要求分配页面时,要说明采用哪一种分配策略。 前面几个数据结构都是用于物理空间管理的,现在来看看虚拟空间的管理,也就是虚存页面的管理。 虚存空间的管理不像物理空间的管理那样有一个总的物理页面仓库,而是以进程为基础的,每个进程都 有各自的虚存(用户)空间。不过,如前所述,每个进程的“系统空间”是统一为所有进程所共享的。 以后我们对进程的“虚存空间”和“用户空间”这两个词常常会不加区分。 如果说物理空间是从“供”的角度来管理的,也就是:“仓库中还有些什么”;则虚存空间的管理是 从“需”的角度来管理的,就是“我们需要用虚存空间中的哪些部分”。拿虚存空间中的“用户空间”部 分来说,大概没有一个进程会真的需要使用全部的 3G 字节的空间。同时,一个进程所需要使用的虚存 空间中的各个部位又未必是连续的,通常形成若干离散的虚存“区间”。很自然地,对虚存区间的抽象是 一个重要的数据结构。在 Linux 内核中,这就是 vm_area_struct 数据结构,定义于 include/linux/mm.h 中: 00035: /* 00036: * This struct defines a memory VMM memory area. There is one of these 00037: * per VM-area/task. A VM area is any part of the process virtual memory 00038: * space that has a special rule for the page-fault handlers (ie a shared 00039: * library, the executable area etc). 00040: */ 00041: struct vm_area_struct { 00042: struct mm_struct * vm_mm; /* VM area parameters */ 00043: unsigned long vm_start; 00044: unsigned long vm_end; 00045: 00046: /* linked list of VM areas per task, sorted by address */ Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 49 页,共 1481 页 00047: struct vm_area_struct *vm_next; 00048: 00049: pgprot_t vm_page_prot; 00050: unsigned long vm_flags; 00051: 00052: /* AVL tree of VM areas per task, sorted by address */ 00053: short vm_avl_height; 00054: struct vm_area_struct * vm_avl_left; 00055: struct vm_area_struct * vm_avl_right; 00056: 00057: /* For areas with an address space and backing store, 00058: * one of the address_space->i_mmap{,shared} lists, 00059: * for shm areas, the list of attaches, otherwise unused. 00060: */ 00061: struct vm_area_struct *vm_next_share; 00062: struct vm_area_struct **vm_pprev_share; 00063: 00064: struct vm_operations_struct * vm_ops; 00065: unsigned long vm_pgoff; /* offset in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */ 00066: struct file * vm_file; 00067: unsigned long vm_raend; 00068: void * vm_private_data; /* was vm_pte (shared mem) */ 00069: }; 在内核的代码中,用于这个数据结构的变量名常常是 vma。 结构中的 vm_start 和 vm_end 决定了一个虚存区间。vm_start 是包含在区间内的,而 vm_end 则不包 含在区间内。区间的划分并不仅仅取决于地址的连续性,也取决于区间的其他属性,主要是针对虚存页 面的访问权限。如果一个地址范围内的前一半页面和后一半页面有不同的访问权限或其他属性,就要分 成两个区间。所以,包含在同一个区间里的所有页面都应有相同的访问权限(或者说保护属性)和其他 一些属性,这就是结构中的成分 vm_page_prot 和 vm_flags 的用途。属于同一个进程的所有区间都要按虚 存地址的高低次序链接在一起,结构中的 vm_next 指针就是用于这个目的。由于区间的划分并不仅仅取 决于地址的连续性,一个进程的虚存(用户)空间很可能会被划分成大量的区间。内核中给定一个虚拟 地址而要找出其所属的区间是一个频繁用到的操作,如果每次都要顺着 vm_next 在链中作线性搜索的话, 势必会显著地影响到内核的效率。所以,除了通过 vm_next 指针把所有区加串成一个线性队列以外,还 可以在区间数量较大时为之建立一个 AV L(Adelson_Velskii and Landis)树 。AV L 树是一种平衡的树结 构,读者从有关的数据结构专著中可以了解到,在 AV L 树中搜索的速度快而代价是 O(lg n),即与树的 大小的对数(而不是树的大小)成比例。虚存结构 vm_area_struct 中的 vm_avl_height、vm_avl_left 以及 vm_avl_right 三个成分就是用于 AV L 树,表示本区间在 AV L 树中的位置的。 在两种情况下虚存页面(或区间)会跟磁盘文件发生关系。一种是盘区交换(swap),当内存页面 不够分配时,一些久未使用的页面可以被交换到磁盘上去,腾出物理页面以供更急需的进程使用,这就 是大家所知道的一般意义上的“按需调度”页式虚存管理(demand paging)。另一种情况则是将一个磁 盘文件映射到一个进程的用户空间中。Linux 提供了一个系统调用 mmap()(实际上是从 Unix Sys V R4.2 开始的),使一个进程可以将一个已经打开的文件映射到其用户空间中,此后就可以像访问内存中一个字 符数组那样来访问这个文件的内容,而不必通过 lseek()、read()或 write()等进行文件操作。 由于虚存区间(最终是页面)与磁盘文件的这种联系,在 vm_area_struct 结构中相应地设置了一些Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 50 页,共 1481 页 成分,如 mapping、vm_next_share、vm_pprev_share、vm_file 等,用以记录和管理此种联系。我们将在 以后结合具体的情景介绍这些成分的使用。 虚存空间结构中另一个重要的成分是 vm_ops,这是指向一个 vm_operation_struct 数据结构的指针。 这种数据结构也是在 include/linux/mm.h 中定义的: 00115: /* 00116: * These are the virtual MM functions - opening of an area, closing and 00117: * unmapping it (needed to keep files on disk up-to-date etc), pointer 00118: * to the functions called when a no-page or a wp-page exception occurs. 00119: */ 00120: struct vm_operations_struct { 00121: void (*open)(struct vm_area_struct * area); 00122: void (*close)(struct vm_area_struct * area); 00123: struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, int write_access); 00124: }; 结构中全是函数指针。其中 open、close、nopage 分别用于虚存空间的打开、关闭和建立映射。为什 么要有这些函数呢?这是因为对于不同的虚存空间可能会需要一些不同的附加操作。函数指针 nopage 指示当因(虚存)页面不在内存中而引起的“页面出错”(page fault)异常(见第 3 章)时所应调用的函 数。 最后,vm_area_struct 中还有一个指针 vm_mm,该指针指向一个 mm_struct 数据结构,那是在 include/linux/sched.h 中定义的: 00203: struct mm_struct { 00204: struct vm_area_struct * mmap; /* list of VMAs */ 00205: struct vm_area_struct * mmap_avl; /* tree of VMAs */ 00206: struct vm_area_struct * mmap_cache; /* last find_vma result */ 00207: pgd_t * pgd; 00208: atomic_t mm_users; /* How many users with user space? */ 00209: atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */ 00210: int map_count; /* number of VMAs */ 00211: struct semaphore mmap_sem; 00212: spinlock_t page_table_lock; 00213: 00214: struct list_head mmlist; /* List of all active mm's */ 00215: 00216: unsigned long start_code, end_code, start_data, end_data; 00217: unsigned long start_brk, brk, start_stack; 00218: unsigned long arg_start, arg_end, env_start, env_end; 00219: unsigned long rss, total_vm, locked_vm; 00220: unsigned long def_flags; 00221: unsigned long cpu_vm_mask; 00222: unsigned long swap_cnt; /* number of pages to swap on next pass */ 00223: unsigned long swap_address; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 51 页,共 1481 页 00224: 00225: /* Architecture-specific MM context */ 00226: mm_context_t context; 00227: }; 在内核的代码中,用于这个数据结构(指针)的变量名常常是 mm。 显然,这是比 vm_area_struct 更高层次上使用的数据结构。事实上,每个进程只有一个 mm_struct 结构,在每个进程的“进程控制块”,即 task_struct 结构中,有一个指针指向该进程的 mm_struct 结构。 可以说,mm_struct 数据结构是进程整个用户空间的抽象,也是总的控制结构。结构中的头三个指针都 是关于虚存区间的。第一个 mmap 用来建立一个虚存区间结构的单链线性队列。第二个 mmap_avl 用来 建立一个虚存区间结构的 AV L 树,这在前面已经谈过。第三个指针 mmap_cache,用来指向最近一次用 到的那个虚存区间结构:这是因为程序中用到的地址常常带有局部性,最近一次用到的区间很有可能就 是下一次要用到的区间,这样就可以提高效率。另一个成分 map_count,则说明在队列中(或在 AV L 树 中)有几个虚存区间结构,也就是说该进程有几个虚存区间。指针 pgd 显而易见是指向该进程的页面目 录的,当内核调度一个进程进入运行时,就将这个指针转换成物理地址,并写入控制寄存器 CR3,这在 前面已经看到过了。另一方面,由于 mm_struct 结构及其下属的 vm_area_struct 结构都有可能在不同的上 下文中受到访问,而这些结构又必须互斥,所以在结构中设置了用于 P、V 操作的信号量(semaphore), 即 mmap_sem。此外,page_table_lock 也是为类似的目的而设置的。 虽然一个进程只使用一个 mm_struct 结构,反过来一个 mm_struct 结构却可能为多个进程所共享。 最简单的例子就是,当一个进程创建(vfork()或 clone(),见第 4 章)一个子进程时,其子进程就可能与 父进程共享一个 mm_struct 结构。所以,在 mm_struct 结构中还为此设置了计数器 mm_users 和 mm_counter。类型 atomic_t 实际上就是整数,但是对这种类型的整数进行的操作必须是“原子”的,也 就是不允许因中断或其他原因而受到干扰。 指针 segments 指向该进程的局部段描述表 LDT。不过,一般的进程是不用局部段描述表的,只有在 VM86 模式下才会有 LDT。 结构中其他成分的用途比较显而易见,如 start_code、end_code、start_data、end_data 等等就是该进 程映像中代码段、数据段、存储堆以及堆栈段的起点和终点,这里就不多说了。注意,不要把进程映像 中的这些“段”跟“段式存储管理”中的“段”相混淆。 如前所述,mm_struct 结构及其属下的各个 vm_area_struct 只是表明了对虚存空间的需求。一个虚拟 地址有相应的虚存区间存在,并不保证该地址所在的页面已经映射某一个物理(内存或盘区)页面。更 不保证该页面就在内存中。当一个未经映射的页面受到访问时,就会产生一个“Page Fault”异常(也称 缺页异常、缺页中断),那时候 Page Fault 异常的服务程序就会来处理这个问题。所以,从这个意义上, mm_struct 和 vm_area_struct 说明了对页面的需求;前面的 page、zone_struct 等结构则说明了对页面的供 应;而页面目录、中间目录以及页面表则是二者中间的桥梁。 图 2.5 是个示意图,图中说明了用于虚存管理的各种数据结构之间的联系。 Linux 内核源代码情景分析 mm task_struct pgd mm_struct mm vm_next vm_area_struct vm_op 页面目录pgd_t 页面映射表pte_t 页面映射表pte_t vm_operations_struct 其他vm_area_struct 图2.5 虚存管理数据结构联系图 前面讲过,在内核中经常要用到这样的操作:给定一个属于某个进程的虚拟地址,要求找到其所属 的区间以及相应的 vm_area_struct 结构。这是由 find_vma()来实现的,其代码在 mm/mmap.c 中: 00404: /* Look up the first VMA which satisfies addr < vm_end, NULL if none. */ 00405: struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr) 00406: { 00407: struct vm_area_struct *vma = NULL; 00408: 00409: if (mm) { 00410: /* Check the cache first. */ 00411: /* (Cache hit rate is typically around 35%.) */ 00412: vma = mm->mmap_cache; 00413: if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) { 00414: if (!mm->mmap_avl) { 00415: /* Go through the linear list. */ 00416: vma = mm->mmap; 00417: while (vma && vma->vm_end <= addr) 00418: vma = vma->vm_next; 00419: } else { 00420: /* Then go through the AVL tree quickly. */ 00421: struct vm_area_struct * tree = mm->mmap_avl; 00422: vma = NULL; 00423: for (;;) { 00424: if (tree == vm_avl_empty) 2006-12-31 版权所有,侵权必究 第 52 页,共 1481 页 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 53 页,共 1481 页 00425: break; 00426: if (tree->vm_end > addr) { 00427: vma = tree; 00428: if (tree->vm_start <= addr) 00429: break; 00430: tree = tree->vm_avl_left; 00431: } else 00432: tree = tree->vm_avl_right; 00433: } 00434: } 00435: if (vma) 00436: mm->mmap_cache = vma; 00437: } 00438: } 00439: return vma; 00440: } 当我们说到一个特定的用户虚拟地址时,必须说明是哪一个进程的虚存空间中的地址,所以函数的 参数有两个,一个是地址,一个是指向该进程的 mm_struct 结构的指针。首先看一下这地址是否恰好在 上一次(最近一次)访问过的同一个区间中。根据代码作者所加的注释,命中率一般可达到 35%,这也 正是 mm_struct 结构中设置一个 mmap_cache 指针的原因。如果没有命中的话,那就要搜索了。最后, 如果找到的话,那就把 mmap_cache 指针设置成指向所找到的 vm_area_struct 结构。函数的返回值为零 (NULL),表示该地址所属的区间还未建立。此时通常就得要建立起一个新的虚存区间结构,再调用 insert_vm_struct()将其插入到 mm_struct 中的线性队列或 AV L 树中去。函数 insert_vm_struct()的源代码在 同一文件中: 00961: void insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vmp) 00962: { 00963: lock_vma_mappings(vmp); 00964: spin_lock(¤t->mm->page_table_lock); 00965: __insert_vm_struct(mm, vmp); 00966: spin_unlock(¤t->mm->page_table_lock); 00967: unlock_vma_mappings(vmp); 00968: } 将一个 vm_area_struct 数据结构插入队列的实际操作是由__insert_vm_struct()完成的,但是这个操作 绝对不允许受到干扰,所以要对操作加锁。这里加了两把锁。第一把加在代表新区间的 vm_area_struct 数据结构中,第二把加在代表着整个虚存空间的 mm_struct 数据结构中,使得在操作过程中不让其他进 程能够在中途插进来,也对这两个数据结构进行队列操作。下面是__insert_vm_struct()的主体,我们略去 了与文件映射有关的部分代码。由于与 find_vma()很相似,这里就不加说明了,留给读者自行阅读。对 AV L 缺乏了解的读者只阅读不采用 AV L 树,即 mm->mmap_alv 为 0 的那一部分代码。 00913: /* Insert vm structure into process list sorted by address 00914: * and into the inode's i_mmap ring. If vm_file is non-NULL 00915: * then the i_shared_lock must be held here. 00916: */ Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 54 页,共 1481 页 00917: void __insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vmp) 00918: { 00919: struct vm_area_struct **pprev; 00920: struct file * file; 00921: 00922: if (!mm->mmap_avl) { 00923: pprev = &mm->mmap; 00924: while (*pprev && (*pprev)->vm_start <= vmp->vm_start) 00925: pprev = &(*pprev)->vm_next; 00926: } else { 00927: struct vm_area_struct *prev, *next; 00928: avl_insert_neighbours(vmp, &mm->mmap_avl, &prev, &next); 00929: pprev = (prev ? &prev->vm_next : &mm->mmap); 00930: if (*pprev != next) 00931: printk("insert_vm_struct: tree inconsistent with list\n"); 00932: } 00933: vmp->vm_next = *pprev; 00934: *pprev = vmp; 00935: 00936: mm->map_count++; 00937: if (mm->map_count >= AVL_MIN_MAP_COUNT && !mm->mmap_avl) 00938: build_mmap_avl(mm); 00939: …………………… 00959: } 当一个虚存空间中区间的数量较小时,在线性队列中搜索的效率并不成为问题,所以不需要为之建 立 AV L 树。而当区间的数量增大到 AVL_MIN_MAP_COUNT,即 32 时,就需要通过 build_mmap_avl() 建立 AV L 树,以提高搜索效率了。 2.4. 越界访问 页式存储管理机制通过页面目录和页面表将每个线性地址(也可以理解为虚拟地址)转换成物理地 址。如果在这个过程中遇到某种阻碍而使 CPU 无法最终访问到相应的物理内存单元,映射便失败了,而 当前的指令也就不能执行完成。此时 CPU 会产生一次页面出错(Page Fault)异常(Exception)(也称缺 页中断),进而执行预定的页面异常处理程序,使应用程序得以从因映射失败而暂停的指令处开始恢复执 行,和进行一些善后处理。这里所说的阻碍可以有以下几种情况: 相应的页面目录或页面表项为空,也就是该线性地址与物理地址的映射关系尚未建立,或者已 经撤销。 相应的物理页面不在内存中。 指令中规定的访问方式与页面的权限不符,例如企图写一个“只读”的页面。 在这个情景里,我们假定一段用户程序曾经将一个已打开文件通过 mmap()系统调用映射到内存,然 后又已经将映射撤销(通过 munmap()系统调用)。在撤销一个映射区间时,常常会在虚存地址空间中留 下一个孤立的空洞,而相应的地址则不应继续使用了。但是,在用户程序中往往会有错误,以至在程序 中某个地方还再次访问这个已经撤销的区域(程序员们一定会同意,这是不足为奇的)。这时候,一次因 越界访问一个无效地址(Invalid Address)而引起映射失败,从而产生一次页面出错异常。中断请求以及Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 55 页,共 1481 页 异常的响应机制将在“中断和异常”一章中集中介绍,读者在那里可以找到从发生异常到进入内核相应 服务程序的全过程。这里假定 CPU 的运行已经到达了页面异常服务程序的主体 do_page_fault()的入口处。 函数 do_page_fault()的代码在文件 arch/i386/mm/fault.c 中。这个函数的代码比较长,我们将随着情 景的进展按需要来展示其有关的片段。这里先来看开头几行代码: 00106: asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code) 00107: { 00108: struct task_struct *tsk; 00109: struct mm_struct *mm; 00110: struct vm_area_struct * vma; 00111: unsigned long address; 00112: unsigned long page; 00113: unsigned long fixup; 00114: int write; 00115: siginfo_t info; 00116: 00117: /* get the address */ 00118: __asm__("movl %%cr2,%0":"=r" (address)); 00119: 00120: tsk = current; 00121: 00122: /* 00123: * We fault-in kernel-space virtual memory on-demand. The 00124: * 'reference' page table is init_mm.pgd. 00125: * 00126: * NOTE! We MUST NOT take any locks for this case. We may 00127: * be in an interrupt or a critical region, and should 00128: * only copy the information from the master page table, 00129: * nothing more. 00130: */ 00131: if (address >= TASK_SIZE) 00132: goto vmalloc_fault; 00133: 00134: mm = tsk->mm; 00135: info.si_code = SEGV_MAPERR; 00136: 00137: /* 00138: * If we're in an interrupt or have no user 00139: * context, we must not take the fault.. 00140: */ 00141: if (in_interrupt() || !mm) 00142: goto no_context; 00143: 00144: down(&mm->mmap_sem); 00145: 00146: vma = find_vma(mm, address); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 56 页,共 1481 页 00147: if (!vma) 00148: goto bad_area; 00149: if (vma->vm_start <= address) 00150: goto good_area; 00151: if (!(vma->vm_flags & VM_GROWSDOWN)) 00152: goto bad_area; 首先是一行汇编代码。为什么要用汇编呢?当 i386 CPU 产生“页面错”异常时,CPU 将导致映射 失败的线性地址放在控制寄存器 CR2 中,而这显然是相应的服务程序所必需的信息。可是 C 语言中并没 有相应的语言成分可以用来读取 CR2 的内容,所以只能用汇编代码。这行汇编代码只有输出部而没有输 入部,它将%0 与变量 address 相结合,并说明该变量应该被分配在一个寄存器中。 同时,内核的中断/异常响应机制还传过来两个参数。一个是 pt_regs 结构指针 regs,它指向异常发 生前夕 CPU 中各寄存器内容的一份副本,这是由内核的中断响应机制保存下来的“现场”,而 error_code 则进一步指明映射失败的具体原因。 然后是获取当前进程的 task_struct 数据结构。在内核中,可以通过一个宏操作 current 取得当前进程 (当前正在运行的进程)的 task_struct 结构的地址。在每个进程的 task_struct 结构中有一个指针,指向 其 mm_struct 数据结构,而跟虚存管理和映射有关的信息都在那个结构中。这里要指出,CPU 实际进行 的映射并不涉及 mm_struct 结构,而是像以前讲过的那样通过页面目录和页面表进行,但是 mm_struct 结构反映了,或者说描述了这种映射。 接下来,需要检测两个特殊情况。一个特殊情况是 in_interrupt()返回非 0,说明映射的失败发生在某 个中断服务程序中,因而与当前的进程毫无关系。另一个特殊情况是当前进程的 mm 指针为空,也就是 说该进程的映射尚未建立,当然也就不可能与进程有关。可是,不跟当前进程有关,in_interrupt()又返回 0,那这次异常发生在什么地方呢?其实还是在某个中断/异常服务程序中,只不过不在 in_interrupt()能检 测到的范围中而已。如果发生这些特殊情况,控制就通过 goto 语句转到标号 no_context 处,不过那与我 们这种情景无关,所以我们略去对那段代码的讨论。 以下的操作有互斥的要求,也就是不容许别的进程来打扰,所以要有对信号量的 P/V 操作,即 down()/up()操作来保证。为了这个目的,在 mm_struct 结构中还设置了所需的信号量 mmap_sem。这样, 从 down()返回以后,就不会有别的进程来打扰了。 可以想像,在知道了发生映射失败的地址以及所属的进程以后,接下来应该要搞清楚的是这个地址 是否落在某个已经建立起映射的区间,或者进一步具体指出在哪个区间。事实上正是这样,这就是 find_vma()所要做的事情。以前讲过,find_vma()试图在一个虚存空间中找出结束地址大于给定地址的第 一个区间。如果找不到的话,那本次页面异常就必定是因越界访问而引起。那么,在什么情况下会找不 到呢?回忆一下内核对用户虚存空间的使用,堆栈在用户区,堆栈在用户区的顶部,从上向下伸展,而 进程的代码和数据都是自底向上分配空间。如果没有一个区间的结束地址高于给定的地址,那就是说明 这个地址是在堆栈之上,也就是 3G 字节以上了。要从用户空间访问属于系统的空间,那当然是越界了, 然后就转向 bad_area,不过我们这个情景所说的不是这个情况。 如果找到了这么一个区间,而且其起始地址又不高于给定的地址(见程序 148 行),那就说明给定的 地址恰好落在这个区间。这样,映射肯定已经建立,所以就转向 good_area 去进一步检查失败的原因。 这也不是我们这个情景所要说的。 除了这两种情况,剩下的就是给定地址正好落在两个区间当中的空洞里,也就是该地址所在页面的 映射尚未建立或已经撤销。在用户虚存空间中,可能有两种不同的空洞。第一种空洞只能有一个,那就 是在堆栈区以下的那个大空洞,它代表着供动态分配(通过系统调用 brk())而仍未分配出去的空间。当 映射失败的地址落在这个空洞里时,还有个特殊情况要考虑,我们将在下一个情景中讨论。但是,怎样 才能知道这地址是落在这个空洞里呢?请看程序 151 行。我们知道,堆栈区是向下伸展的,如果 find_vma() 找到的区间是堆栈区间,那么它的 vm_flags 中应该有个标志位 VM_GROWSDOWN。要是该标志位为 0 的话,那就说明空洞上方的区间并非堆栈区,说明这个空洞是因为一个映射区间被撤销而留下的,或者Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 57 页,共 1481 页 在建立时跳过了一块地址。这就是第二种可能,也就是我们这个情景所说的情况。所以,我们就随着这 里的 goto 语句转向 bad_area,那是在 224 行: [do_page_fault()] 00220: /* 00221: * Something tried to access memory that isn't in our memory map.. 00222: * Fix it, but check if it's kernel or user first.. 00223: */ 00224: bad_area: 00225: up(&mm->mmap_sem); 00226: 00227: bad_area_nosemaphore: 00228: /* User mode accesses just cause a SIGSEGV */ 00229: if (error_code & 4) { 00230: tsk->thread.cr2 = address; 00231: tsk->thread.error_code = error_code; 00232: tsk->thread.trap_no = 14; 00233: info.si_signo = SIGSEGV; 00234: info.si_errno = 0; 00235: /* info.si_code has been set above */ 00236: info.si_addr = (void *)address; 00237: force_sig_info(SIGSEGV, &info, tsk); 00238: return; 00239: } 首先,当控制流到达这里时,已经不再需要互斥(因为不再对 mm_struct 结构进行操作),所以通过 up()退出临界区。代码的作者为此加了注解: 00096: /* 00097: * This routine handles page faults. It determines the address, 00098: * and the problem, and then passes it off to one of the appropriate 00099: * routines. 00100: * 00101: * error_code: 00102: * bit 0 == 0 means no page found, 1 means protection fault 00103: * bit 1 == 0 means read, 1 means write 00104: * bit 2 == 0 means kernel, 1 means user-mode 00105: */ 当 error_code 的 bit2 为 1 时,表示失败是当 CPU 处于用户模式时发生的,这正与我们情景相符,所 以控制将进入 229 行。在那里,对当前进程的 task_struct 结构内的一些成分进行一些设置以后,就向该 进程发出一个强制的“信号”(或称“软中断”)SIGSEGV。至此,本次例外服务就结束了。 读者大概会问:“这样就完了?”是的,完了。接下来的详情,读者在看了有关中断处理和信号的章 节以后就会明白。每次从中断/异常返回之前,都要检查当前进程是否有悬而未决的信号需要处理,在我 们这个情景里当然是有的,其中至少有一个就是 SIGSEGV。然后,内核根据这些待处理信号的性质以及 进程本身的选择决定怎么办。对有些软中断的处理是自愿的,有些则是强制的。而对于 SIGSEGV 的反Linux 内核源代码情景分析 应,那是强制的,其后果是在该进程的显示屏上显示程序员们怕见到却又经常见到的“Segment Fault” 提示,然后使进程流产(撤销)。至于从异常处理返回用户空间后的地址,在这种情况下并无意义,因为 本来就不会回去了。 我们在这里跳过了 do_page_fault()中的许多代码,因为那些代码与我们眼下这个特定的情景无关。 不过,以后在其他的情景里我们还会回到这些代码中来。 2.5. 用户堆栈的扩展 在上一个情景中,我们“游览参观”了一次因越界访问而造成映射失败从而引起进程流产的过程。 但是,读者也许会感到惊奇,越界访问有时候是正常的。不过这只发生在一种情况下。现在我们就来看 看当用户堆栈过小,但是因越界访问而“因祸得福”得以伸展的情况。在阅读本情景之前,读者应该先 温习一下前一个情景。 假设在进程运行的过程中,已经用尽了为本进程分配的堆栈空间,也就是从堆栈的“顶部”开始(记 住,堆栈是从上向下伸展的),已经到达了已映射的堆栈区间的下沿。或者说,CPU 中的堆栈指针%esp 已经指向堆栈区间的起始地址,见图 2.6。 堆栈区间 空洞 数据和 代码区间 系统空间 用户空间 %esp 图2.6 进程地址空间示意图 假定现在需要调用某个子程序,因此 CPU 需将返回的地址压入堆栈,也就是要将返回地址写入虚存 空间中地址为(%esp-4)的地方。可是,在我们这个情景中地址(%esp-4)落入了空洞中,这是尚未映 射的地址,因此必然要引起一次页面错异常。让我们顺着上一个情景中已经走过的路线到达文件 arch/i386/mm/fault.c 的第 151 行。 [do_page_fault()] 00151: if (!(vma->vm_flags & VM_GROWSDOWN)) 00152: goto bad_area; 00153: if (error_code & 4) { 00154: /* 00155: * accessing the stack below %esp is always a bug. 00156: * The "+ 32" is there due to some instructions (like 00157: * pusha) doing post-decrement on the stack and that 00158: * doesn't show up until later.. 2006-12-31 版权所有,侵权必究 第 58 页,共 1481 页 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 59 页,共 1481 页 00159: */ 00160: if (address + 32 < regs->esp) 00161: goto bad_area; 00162: } 00163: if (expand_stack(vma, address)) 00164: goto bad_area; 这一次,导致映射失败的线性地址上方的区间是堆栈区间,其 VM_GROWSDOWN 标志位为 1,所 以 CPU 就继续往前执行。当映射失败发生在用户空间(bit2 为 1)时,因堆栈操作引起的越界是作为特 殊情况对待的,所以还需要检查发生异常时的地址是否紧挨着堆栈指针所指的地方。在我们这个情景中, 那是%esp-4,当然是紧挨着的。但是如果是%esp-40 呢?那就不会是因为正常的堆栈操作而引起的,而 是货真价实的非法越界访问了。可是,怎样来判定“正常”或不正常呢?通常,一次压入堆栈的是 4 个 字节,所以该地址应该是%esp-4。但是 i386 CPU 有一条 pusha 指令,可以一次将 32 个字节(8 个 32 位 寄存器的内容)压入堆栈。所以,检查的准则是%esp-32。超出这个范围就一定是错的了,所以跟在前面 一个情景中一样,转向 bad_area。而在我们现在这个情景中,这个测试应是顺利通过了。 既然是属于正常的堆栈扩展要求,那就应该从空洞的顶部开始分配若干页面建立映射,并将之并入 堆栈区间,使其得以扩展。所以就要调用 expand_stack(),这是在文件 include/linux/mm.h 中定义的一个 inline 函数: [do_page_fault() > expand_stack()] 00487: /* vma is the first one with address < vma->vm_end, 00488: * and even address < vma->vm_start. Have to extend vma. */ 00489: static inline int expand_stack(struct vm_area_struct * vma, unsigned long address) 00490: { 00491: unsigned long grow; 00492: 00493: address &= PAGE_MASK; 00494: grow = (vma->vm_start - address) >> PAGE_SHIFT; 00495: if (vma->vm_end - address > current->rlim[RLIMIT_STACK].rlim_cur || 00496: ((vma->vm_mm->total_vm + grow) << PAGE_SHIFT) > current->rlim[RLIMIT_AS].rlim_cur) 00497: return -ENOMEM; 00498: vma->vm_start = address; 00499: vma->vm_pgoff -= grow; 00500: vma->vm_mm->total_vm += grow; 00501: if (vma->vm_flags & VM_LOCKED) 00502: vma->vm_mm->locked_vm += grow; 00503: return 0; 00504: } 参数 vma 指向一个 vm_area_struct 数据结构,代表着一个区间,在这里就是代表着用户空间堆栈所 在的区间。首先,将地址按页面边界对齐,并计算需要增长几个页面才能把给定的地址包括进去(通常 就是一个)。这里还有一个问题,堆栈的这种扩展是否不受限制,直到把空间中的整个空洞用完为止呢? 不是的,每个进程的 task_struct 结构中都有个 rlim 结构数组,规定了对每种资源分配使用的限制,而 RLIMIT_STACK 就是对用户空间堆栈大小的限制。所以,这里就要进行这样的检查。如果扩展以后的区 间大小超过了可用于堆栈的资源,或者使动态分配的页面总量超过了可用于该进程的资源限制,那就不Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 60 页,共 1481 页 能扩展了,就会返回一个负的出错代码-ENOMEM,表示没有存储空间可以分配了;否则就应返回 0。 当 expand_stack()返回值为非 0,也即-ENOMEM 时,在 do_page_fault()中也会转向 bad_area,其结果就 与前一个情景一样了。不过一般情况下都不至于用尽资源,所以 expand_stack()一般都是正常返回的。但 是我们已经看到,expand_stack()只是改变了堆栈区的 vm_area_struct 结构,而并未建立起新扩展的页面 对物理内存的映射。这个任务由接下去的 good_area 完成: [do_page_fault()] 00165: /* 00166: * Ok, we have a good vm_area for this memory access, so 00167: * we can handle it.. 00168: */ 00169: good_area: 00170: info.si_code = SEGV_ACCERR; 00171: write = 0; 00172: switch (error_code & 3) { 00173: default: /* 3: write, present */ 00174: #ifdef TEST_VERIFY_AREA 00175: if (regs->cs == KERNEL_CS) 00176: printk("WP fault at %08lx\n", regs->eip); 00177: #endif 00178: /* fall through */ 00179: case 2: /* write, not present */ 00180: if (!(vma->vm_flags & VM_WRITE)) 00181: goto bad_area; 00182: write++; 00183: break; 00184: case 1: /* read, present */ 00185: goto bad_area; 00186: case 0: /* read, not present */ 00187: if (!(vma->vm_flags & (VM_READ | VM_EXEC))) 00188: goto bad_area; 00189: } 00190: 00191: /* 00192: * If for any reason at all we couldn't handle the fault, 00193: * make sure we exit gracefully rather than endlessly redo 00194: * the fault. 00195: */ 00196: switch (handle_mm_fault(mm, vma, address, write)) { 00197: case 1: 00198: tsk->min_flt++; 00199: break; 00200: case 2: 00201: tsk->maj_flt++; 00202: break; 00203: case 0: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 61 页,共 1481 页 00204: goto do_sigbus; 00205: default: 00206: goto out_of_memory; 00207: } 在这里的 switch 语句中,内核根据由中断响应机制传过来的 error_code 来进一步确定映射失败的原 因并采取相应的对策(error_code 最低三位的定义已经在前节中列出)。就现在这个情景而言,bit0 为 0, 表示没有物理页面,而 bit1 为表示写操作。所以,最低两位的值为 2,既然是写操作,当然要检查相应 的区间是否允许写入,而堆栈是允许写入的。于是,就到达了 196 行,调用虚存管理 handle_mm_fault() 了。该函数定义于 mm/memory.c 中: [do_page_fault() > handle_mm_fault()] 01189: /* 01190: * By the time we get here, we already hold the mm semaphore 01191: */ 01192: int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma, 01193: unsigned long address, int write_access) 01194: { 01195: int ret = -1; 01196: pgd_t *pgd; 01197: pmd_t *pmd; 01198: 01199: pgd = pgd_offset(mm, address); 01200: pmd = pmd_alloc(pgd, address); 01201: 01202: if (pmd) { 01203: pte_t * pte = pte_alloc(pmd, address); 01204: if (pte) 01205: ret = handle_pte_fault(mm, vma, address, write_access, pte); 01206: } 01207: return ret; 01208: } 根据给定的地址和代表着具体虚存空间的 mm_struct 数据结构,由宏操作 pgd_offset()计算出指向该 地址所属页面目录项的指针。这是在 include/asm-i386/pgtable.h 中定义的: 00311: /* to find an entry in a page-table-directory. */ 00312: #define pgd_index(address) ((address >> PGDIR_SHIFT) & (PTRS_PER_PGD-1)) …………………… 00316: #define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address)) 至于下面的 pmd_alloc(),本来是应该分配(或者找到)一个中间目录项的。由于 i386 只使用两层映 射,所以在 include/asm-i386/pgtable_2level.h 中将其定义为“return (pmd_t *)pgd”,也就是说,在 i386 CPU 中,把具体的目录项当成一个只含一个表项(表的大小为 1)的中间目录。所以,对于 i386 CPU 而言, pmd_alloc()是绝对不会失败的,所以这里的 pmd 不可能为 0。读者不妨顺着线性地址的映射过程想想, 接下来需要做些什么?页面目录总是在的,相应的目录项也许已经指向一个页面表,此时需要根据给定 的地址在表中找到相应的页面表项。或者,目录项也可能还是空的,那样的话就需要先分配一个页面表,Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 62 页,共 1481 页 再在页面表中找到相应的表项。这样,才可以为下面分配物理内存页面并建立映射做好准备。这是通过 pte_alloc()完成的,其代码在 include/asm-i386/pgalloc.h 中: [do_page_fault() > handle_mm_fault() > pte_alloc()] 00120: extern inline pte_t * pte_alloc(pmd_t * pmd, unsigned long address) 00121: { 00122: address = (address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1); 00123: 00124: if (pmd_none(*pmd)) 00125: goto getnew; 00126: if (pmd_bad(*pmd)) 00127: goto fix; 00128: return (pte_t *)pmd_page(*pmd) + address; 00129: getnew: 00130: { 00131: unsigned long page = (unsigned long) get_pte_fast(); 00132: 00133: if (!page) 00134: return get_pte_slow(pmd, address); 00135: set_pmd(pmd, __pmd(_PAGE_TABLE + __pa(page))); 00136: return (pte_t *)page + address; 00137: } 00138: fix: 00139: __handle_bad_pmd(pmd); 00140: return NULL; 00141: } 先将给定的地址转换成其所属页面表中的下标。在我们这个情景中,假定指针 pmd 所指向的目录项 为空,所以需要转到标号 get_new()处分配一个页面表。一个页面表所占的空间恰好是一个物理页面。内 核中对页面表的分配做了些优化,当释放一个页面表时,内核将释放的页面表先保存在一个缓冲池中, 而先不将其物理内存页面释放。只有在缓冲池已满的情况下才真的将页面表所占的物理内存页面释放。 这样,在要分配一个页面表时,就可以先看一下缓冲池,这就是 get_pte_fast()。要是缓冲池已经空了, 那就只好通过 get_pte_slow()来分配了。读者也许会想,分配一个物理内存页面用作页面表就那么麻烦吗, 为什么是“slow”呢?回答是有时候可能会很慢。只要想一下物理内存页面有可能已经用完,需要把内 存中已经占用的页面交换到磁盘上去,就可以明白了。分配到一个页面以后,就通过 set_pmd()将其起始 地址连同一些属性标志位一起写入中间目录项 pmd 中,而对 i386 却实际上写入到了页面目录项 pgd 中。 这样,映射所需的“基础设施”都已经齐全了,但页面表项 pte 还是空的。剩下的就是物理内存页面本 身了,那是由 handle_pte_fault()完成的。该函数定义于 mm/memory.c 内: [do_page_fault() > handle_mm_fault() > handle_pte_fault()] 01135: /* 01136: * These routines also need to handle stuff like marking pages dirty 01137: * and/or accessed for architectures that don't do it in hardware (most 01138: * RISC architectures). The early dirtying is also good on the i386. 01139: * 01140: * There is also a hook called "update_mmu_cache()" that architectures Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 63 页,共 1481 页 01141: * with external mmu caches can use to update those (ie the Sparc or 01142: * PowerPC hashed page tables that act as extended TLBs). 01143: * 01144: * Note the "page_table_lock". It is to protect against kswapd removing 01145: * pages from under us. Note that kswapd only ever _removes_ pages, never 01146: * adds them. As such, once we have noticed that the page is not present, 01147: * we can drop the lock early. 01148: * 01149: * The adding of pages is protected by the MM semaphore (which we hold), 01150: * so we don't need to worry about a page being suddenly been added into 01151: * our VM. 01152: */ 01153: static inline int handle_pte_fault(struct mm_struct *mm, 01154: struct vm_area_struct * vma, unsigned long address, 01155: int write_access, pte_t * pte) 01156: { 01157: pte_t entry; 01158: 01159: /* 01160: * We need the page table lock to synchronize with kswapd 01161: * and the SMP-safe atomic PTE updates. 01162: */ 01163: spin_lock(&mm->page_table_lock); 01164: entry = *pte; 01165: if (!pte_present(entry)) { 01166: /* 01167: * If it truly wasn't present, we know that kswapd 01168: * and the PTE updates will not touch it later. So 01169: * drop the lock. 01170: */ 01171: spin_unlock(&mm->page_table_lock); 01172: if (pte_none(entry)) 01173: return do_no_page(mm, vma, address, write_access, pte); 01174: return do_swap_page(mm, vma, address, pte, pte_to_swp_entry(entry), write_access); 01175: } 01176: 01177: if (write_access) { 01178: if (!pte_write(entry)) 01179: return do_wp_page(mm, vma, address, pte, entry); 01180: 01181: entry = pte_mkdirty(entry); 01182: } 01183: entry = pte_mkyoung(entry); 01184: establish_pte(vma, address, pte, entry); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 64 页,共 1481 页 01185: spin_unlock(&mm->page_table_lock); 01186: return 1; 01187: } 在我们这个情景里,不管页面表是新分配的还是原来就有的,相应的页面表项却一定是空的。这样, 程序一开头的 if 语句的条件一定能满足,因为 pte_present()测试一个表项所映射的页面是否在内存中, 而我们的物理内存页面还没有分配。进一步,pte_none()所测试的条件也一定能满足,因为它测试一个表 项是否为空。所以,就必定会进入 do_no_page()(否则就是 do_swap_page())。顺便讲一下,如果 pte_present() 的测试结果是该表项所映射的页面确实在内存中,那么问题一定出在访问权限,或者根本就没有问题了。 函数 do_no_page()也是在 mm/memory.c 中定义的。这里先简要地介绍一下,然后再来看代码。 以前我们曾经提起过,在虚存区间结构 vm_area_struct 中有个指针 vm_ops ,指向一个 vm_operations_struct 数据结构。这个数据结构实际上是一个函数跳转表,结构中通常是一些与文件操作 有关的函数指针。其中有一个函数指针就是用于物理内存页面的分配。物理内存页面的分配为什么与文 件操作有关呢?因为这对于可能的文件共享是很有意义的。当多个进程将同一个文件映射到各自的虚存 空间中时,内存中通常只要保存一份物理页面就可以了。只有当一个进程需要写入该文件时才有必要另 外复制一份独立的副本,称为“copy on write”或者 COW。关于 COW 我们在进程一章中讲到 fork()时还 要作较详细的介绍。这里,当通过 mmap()将一块虚存空间跟一个已打开文件(包括设备)建立起映射后, 就可以通过对这些函数的调用将内存的操作转化成对文件的操作,或者进行一些必要的对文件的附加操 作。另一方面,物理页面的盘区交换显然也是跟文件操作有关的。所以,为特定的虚存空间预先指定一 些特定的操作常常是很有必要的。于是,如果已经预先为一个虚存空间 vma 指定了分配物理页面的操作 的话,那就是 vma->vm_ops->nopage()。但是,vma->vm_ops 和 vma->vm_ops->nopage 都有可能是空, 那就表示没有为之指定具体的 nopage()操作,或者根本就没有配备一个 vm_operation_struct 结构。当没 有指定的 nopage()操作时,内核就调用一个函数 do_anonymous_page()来分配物理内存页面。 现在来看看 do_no_page()的开头几行: [do_page_fault() > handle_mm_fault() > handle_pte_fault() > do_no_page()] 01080: /* 01081: * do_no_page() tries to create a new page mapping. It aggressively 01082: * tries to share with existing pages, but makes a separate copy if 01083: * the "write_access" parameter is true in order to avoid the next 01084: * page fault. 01085: * 01086: * As this is called only for pages that do not currently exist, we 01087: * do not need to flush old virtual caches or the TLB. 01088: * 01089: * This is called with the MM semaphore held. 01090: */ 01091: static int do_no_page(struct mm_struct * mm, struct vm_area_struct * vma, 01092: unsigned long address, int write_access, pte_t *page_table) 01093: { 01094: struct page * new_page; 01095: pte_t entry; 01096: 01097: if (!vma->vm_ops || !vma->vm_ops->nopage) 01098: return do_anonymous_page(mm, vma, page_table, write_access, address); …………………… Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 65 页,共 1481 页 01133: } 对于我们这个情景来说,所涉及的虚存区间是供堆栈用的,跟文件系统或页面共享没有什么关系, 不会有指定的 nopage()操作,所以进入 do_anonymous_page()。 [do_page_fault() > handle_mm_fault() > handle_pte_fault() > do_no_page() > do_anonymous_page()] 01058: /* 01059: * This only needs the MM semaphore 01060: */ 01061: static int do_anonymous_page(struct mm_struct * mm, struct vm_area_struct * vma, pte_t *page_table, int write_access, unsigned long addr) 01062: { 01063: struct page *page = NULL; 01064: pte_t entry = pte_wrprotect(mk_pte(ZERO_PAGE(addr), vma->vm_page_prot)); 01065: if (write_access) { 01066: page = alloc_page(GFP_HIGHUSER); 01067: if (!page) 01068: return -1; 01069: clear_user_highpage(page, addr); 01070: entry = pte_mkwrite(pte_mkdirty(mk_pte(page, vma->vm_page_prot))); 01071: mm->rss++; 01072: flush_page_to_ram(page); 01073: } 01074: set_pte(page_table, entry); 01075: /* No need to invalidate - it was non-present before */ 01076: update_mmu_cache(vma, addr, entry); 01077: return 1; /* Minor fault */ 01078: } 首先我们注意到,如果引起页面异常的是一次读操作,那么由 mk_pte()构筑的映射表项要通过 pte_wrprotect()加以修正;而如果是写操作(参数 write_access 为非 0),则通过 pte_mkwrite()加以修正。 这二者有什么不同呢?见 include/asm-i386/pgtable.h: 00277: static inline pte_t pte_wrprotect(pte_t pte) \ { (pte).pte_low &= ~_PAGE_RW; return pte; } 00271: static inline int pte_write(pte_t pte) \ { return (pte).pte_low & _PAGE_RW; } 对比一下,就可看出,在 pte_wrprotect()中,把_PAGE_RW 标志位设成 0,表示这个物理页面只允 许读;而在 pte_write()却把这个标志位设成 1。同时,对于读操作,所映射的物理页面总是 ZERO_PAGE, 这个页面是在 include/asm-i386/pgtable.h 中定义的: 00091: /* 00092: * ZERO_PAGE is a global shared page that is always zero: used 00093: * for zero-mapped memory areas etc.. 00094: */ Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 66 页,共 1481 页 00095: extern unsigned long empty_zero_page[1024]; 00096: #define ZERO_PAGE(vaddr) (virt_to_page(empty_zero_page)) 就是说,只要是“只读”(也就是写保护)的页面,开始时都一律映射到同一个物理内存页面 empty_zero_page,而不管其虚拟地址是什么。实际上,这个页面的内容为全 0,所以映射之初若从该页 面读出就读得 0。只有可写的页面,才通过 alloc_page()为其分配独立的物理内存。在我们这个情景里, 所需的页面是在堆栈区,并且是由写操作才引起异常的,所以要通过 alloc_page()为其分配一个物理内存 页面,并将分配到的物理页面连同所有的状态及标志位(见程序 1115 行),一起通过 set_pte()设置进指 针 page_table 所指的页面表项。至此,从虚存页面到物理内存页面的映射终于建立了。这里的 update_mmu_cache()对 i386 CPU 是个空函数(见 include/asm-i386/pgtable.h),因为 i386 的 MMU(内存 管理单元)是实现在 CPU 内部,而并没有独立的 MMU。 映射既已建立,下面就是逐层返回了。由于映射成功,各个层次中的返回值都是 1,直至 do_page_fault()。在函数 do_page_fault()中,还要处理一个与 VM86 模式以及 VGA 的图象存储区有关的 特殊情况,但是那与我们这个情景已经没有关系了: [do_page_fault()] 00209: /* 00210: * Did it hit the DOS screen memory VA from vm86 mode? 00211: */ 00212: if (regs->eflags & VM_MASK) { 00213: unsigned long bit = (address - 0xA0000) >> PAGE_SHIFT; 00214: if (bit < 32) 00215: tsk->thread.screen_bitmap |= 1 << bit; 00216: } 00217: up(&mm->mmap_sem); 00218: return; 最后,特别要指出,当 CPU 从一次页面错异常处理返回到用户空间时,将会先重新执行因映射失败 而中途夭折的那条指令,然后才继续往下执行,这是异常处理的特殊性。学过有关课程的读者都知道, 中断以及自陷(trap 指令)发生时,CPU 都会将下一条指令,也就是接下去本来应该执行的指令的地址 压栈作为中断服务的返回地址。但是异常处理却不同。当异常发生时,CPU 将因无法完成(例如除以 0, 映射失败,等等)而夭折的指令本身的地址(而不是下一条指令的地址)压入堆栈。这样,就可以在从 异常处理返回时完成未竟的事业。这个特殊性是在 CPU 的内部电路中实现的,而不需由软件干预。从这 个意义上讲,所谓“缺页中断”是不对的,应该叫“缺页异常”才对。在我们这个情景中,当初是因为 在一条指令中要压栈,但是越出了已经为堆栈区分配的空间而引起的。那条指令在当时已经中途夭折了, 并没有产生什么效果(例如堆栈指针%esp 还是指向原来的位置)。现在,从异常处理返回以后,堆栈区 已经扩展了,再重新执行一遍以前夭折的那条压栈指令,然后就可以继续往下执行了。对于用户程序来 说,这整个过程都是“透明”的,就像什么事也没有发生过,而堆栈区间就仿佛从一开始就已经分配好 了足够大的空间一样。 2.6. 物理页面的使用和周转 除 CPU 之外,对于像 Linux 这样的现代操作系统来说,物理存储页面可以说是最基本、最重要的资 源了。物理存储器页面在系统中的使用和周转就好像资金在企业中的使用和周转一样重要。因此,读者 对此最好能有更多一些了解。 首先要澄清本书中使用的几个术语。“虚存页面”是指在虚拟地址空间中一个固定大小,边界与页面 大小(4KB)对齐的区间及其内容。虚拟页面最终要落实到,或者说要映射到某种物理存储介质上,那Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 67 页,共 1481 页 就是“物理页面”。根据具体介质的不同,一个物理页面可以在内存中,也可以在磁盘上。为了区分这两 种情况,本书将分别称之为“(物理)内存页面”和“盘上(物理)页面”。此外,在某项外部设备上, 例如在网络接口卡上,用来存储一个页面内容的那部分介质,也称为一个物理页面。所以,当我们在谈 及物理内存页面的分配和释放的时候,指的仅是物理介质,而在谈及页面的换入和换出时则指的是其内 容。读者,特别是非计算机专业的读者,一定要清楚并记住这一点。 如前所述,每个进程的虚存空间是很大的(用户空间为 3GB)。不过,每个进程实际上使用的空间 则要小得多,一般不会超过几个 MB。特别地,传统的 Linux(以及 Unix)可执行程序通常都是比较小 的,例如几十 KB 或一二百 KB。可是,系统中有几百个、上千个进程同时存在的时候,对存储空间的 需求总量就很大了。在这样的情况下,要为系统配备足够的内存就很难。所以,在计算机技术的发展史 上很早就有了把内存的内容与一个专用的磁盘空间“交换”的技术,即把暂时不用的信息(内容)存放 到磁盘上,为其他急用的信息腾出空间,到需要时再从磁盘上读进来的技术。早期的盘区交换技术是建 立在段式存储管理的基础上的,当一个进程暂时不运行的时候就可以把它(代码段和数据段等)交换出 去(把其他进程换进来,故曰“交换”),到调度这个进程运行时再交换回来。显然,这样的盘区交换是 很粗糙的,对系统性能的影响也比较大,所以后来发展起了建立在页式存储管理基础上的“按需页面交 换”技术。 在计算机技术中,时间和空间是一对矛盾,常常需要在二者之间折中权衡,有时候是以空间换时间, 有时候是以时间换空间。而页面的交换,则是典型的以时间换空间。必须指出,这只是不得已而为之。 特别是在有实时要求的系统中,是不宜采用页面交换的,因为它使程序的执行在时间上有了较大的不确 定性。因此,Linux 提供了用来开启和关闭页面交换机制的系统调用,不过我们在本章的叙述中假定它 是打开的。 在介绍页面周转的策略之前,先要对物理页面、特别是磁盘页面的抽象描述作一个简要的说明。 前面已经简略地介绍过,为了方便(物理)内存页面的管理,每一个内存页面都对应一个 page 数据 结构。每一个物理内存页面之有 page 数据结构(以及每个进程之有其 task_struct 结构),就好像每个人 之有“户口”或者“档案”一样。一个物理上存在的人,如果没有户口,从管理的角度来说便是不存在 的。同样,一个物理上存在的内存页面,如果没有一个相应的 page 结构,就根本不会被系统“看到”。 在系统的初始化阶段,内核根据检测到的物理内存的大小,为每一个页面都建立一个 page 结构,形成一 个 page 结构的数组,并使一个全局量 mem_map 指向这个数组。同时,又按需要将这些页面拼合成物理 地址连续的许多内存页面“块”,再根据块的大小建立起若干“管理区”(zone),而在每个管理区中则设 置一个空闲块队列,以便物理内存页面的分配使用。这一些,读者已经在前面看到过了。 与此类似,交换设备(通常是磁盘,也可以是普通文件)的每个物理页面也要在内存中有个相应的 数据结构(或者说“户口”),不过那样要简单得多,实际上只是一个计数,表示该页面是否已被分配使 用,以及有几个用户在共享这个页面。对盘上页面的管理是按文件或磁盘设备来进行的。内核中定义了 一个 swap_info_struct 数据结构,用以描述和管理用于页面交换的文件和设备。它的定义包含在 include/linux/swap.h 中: 00049: struct swap_info_struct { 00050: unsigned int flags; 00051: kdev_t swap_device; 00052: spinlock_t sdev_lock; 00053: struct dentry * swap_file; 00054: struct vfsmount *swap_vfsmnt; 00055: unsigned short * swap_map; 00056: unsigned int lowest_bit; 00057: unsigned int highest_bit; 00058: unsigned int cluster_next; 00059: unsigned int cluster_nr; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 68 页,共 1481 页 00060: int prio; /* swap priority */ 00061: int pages; 00062: unsigned long max; 00063: int next; /* next entry on swap list */ 00064: }; 其中的指针 swap_map 指向一个数组,该数组中的每一个无符号短整数即代表盘上(或者普通文件 中)的一个物理页面,而数组的下标则决定了该页面在盘上或文件中的位置。数组的大小取决于 pages, 它表示该页面交换设备或文件的大小。设备上(或文件中,设备也是一种文件,下同)的第一个页面, 也即 swap_map[0]所代表的那个页面是不用于页面交换的,它包含了该设备或文件自身的一些信息以及 一个表明哪些页面可供使用的位图。这些信息最初是在把该设备格式化成页面交换区时设置的。根据不 同的页面交换格式(以及版本),还有一些其他的页面也不供页面交换使用。这些页面都集中在开头和结 尾两个地方,所以 swap_info_struct 结构中的 lowest_bit 和 highest_bit 就说明文件中从什么地方开始到什 么地方为止是供页面交换使用的。另一个字段 max 则表示该设备或文件中最大的页号,也就是设备或文 件的物理大小。 由于存储介质是移动的磁盘,将地址连续的页面存储在连续的磁盘扇区中不见得是最有效的方法, 所以在分配盘上页面空间时尽可能按集群(cluster)方式进行,而字段 cluster_next 和 cluster_nr 就是为 此而设置的。 Linux 内核允许使用多个页面交换设备(或文件),所以在内核中建立了一个 swap_info_struct 结构 的阵列(数组)swap_info,这是在 mm/swapfile.c 中定义的: 00025: struct swap_info_struct swap_info[MAX_SWAPFILES]; 同时,还设立了一个队列 swap_list,将各个可以分配物理页面的磁盘设备或文件的 swap_info_struct 结构按优先级高低连接在一起: 00023: struct swap_list_t swap_list = {-1, -1}; 这里的 swap_list_t 数据结构是在 include/linux/swap.h 中定义的: 00153: struct swap_list_t { 00154: int head; /* head of priority-ordered swapfile list */ 00155: int next; /* swapfile to be used next */ 00156: }; 开始时队列为空,所以 head 和 next 均为-1。当系统调用 swap_on()指定将一个文件用于页面交换时, 就将该文件的 swap_info_struct 结构链入队列中。 就像通过 pte_t 数据结构(页面表项)将物理内存页面与虚存页面建立联系一样,盘上页面也有这么 一个 swp_entry_t 数据结构,这是在 include/linux/shmem_fs.h 中定义的: 00008: /* 00009: * A swap entry has to fit into a “unsigned long”, as 00010: * the entry is hidden in the “index” field of the 00011: * swapper address space. 00012: * 00013: * We have to move it here, since not every user of fs.h is including 00014: * mm.h, but m.h is including fs.h via sched.h :-/ Linux 内核源代码情景分析 00015: */ 00016: typedef struct { 00017: unsigned long val; 00018: }swp_entry_t; 可见,一个 swp_entry_t 结构实际上只是一个 32 位无符号整数。但是,这个 32 位整数实际上分成三 个部分,见图 2.7。 offset 24位 type 7位 最低位永远是0 图2.7 页面交换项结构示意图 文件 include/asm-i386/pgtable.h 中还为 type 和 offset 两个位段的访问以及与 pte_t 结构之间的关系定 义了几个宏操作: 00336: /* Encode and de-code a swap entry */ 00337: #define SWP_TYPE(x) (((x).val >> 1) & 0x3f) 00338: #define SWP_OFFSET(x) ((x).val >> 8) 00339: #define SWP_ENTRY(type, offset) ((swp_entry_t) { ((type) << 1) | ((offset) << 8) }) 00340: #define pte_to_swp_entry(pte) ((swp_entry_t) { (pte).pte_low }) 00341: #define swp_entry_to_pte(x) ((pte_t) { (x).val }) 这里 offset 表示页面在一个磁盘设备或文件中的位置,也就是文件中的逻辑页面号;而 type 则是指 该页面在哪一个文件中,是个序号。这个位段的命名很容易引起读者的误解,明明是指交换设备或文件 的序号(一共可以容纳 127 个这样的文件,但实际上则视系统的配置而定,远小于 127),为什么称之为 type 呢?估计这是从 pte_t 结构中过来的。读者可能记得,pte_t 实际上也是一个 32 位无符号整数,其中 最高的 20 位为物理页面起始地址的高 20 位(物理页面起始地址的低 12 位永远是 0,因为页面都是 4KB 边界对齐的),而与这 7 位相对应的则都是些表示页面各种性质的标志位,如 R/W,U/S,等等,所以称 之为 type 位段。而 swp_entry_t 与 pte_t 两种数据结构大小相同,关系非常密切。当一个页面在内存中时, 页面表中的表项 pte_t 的最低位 P 标志为 1,表示页面在内存中,而其余各位指明物理内存页面的地址及 页面的属性。而当一个页面在磁盘上时,则相应的页面表项不再指向一个物理内存页面,而是变成了一 个 swp_entry_t“表项”,指示着这个页面的去向。由于此时其最低位为 0,表示页面不在内存,所以 CPU 中的 MMU 单元对其余各位都忽略不顾,而留待系统软件自己来加以解释。在 Linux 内核中,就用它来 唯一地确定一个页面在盘上的位置,包括在哪一个文件或设备,以及页面在此文件中的相对位置。 所以,当页面在内存时,页面表中的相应表项确定了地址的映射关系;而当页面不在内存时,则指 明了物理页面的去向和所在。读者在阅读内核的源程序时,不妨将 SWP_TYPE(entry)想像成 SWP_FILE(entry)。 下面转入本节标题所说对物理页面周转的介绍。我们还是通过一些函数的代码来帮助读者理解。 先介绍一下用来释放一个磁盘页面的函数__swap_free()。通过这个函数的阅读,读者可以加深对上 面一段说明的理解。此函数的代码在文件 mm/swapfile.c 中。而分配磁盘页面的函数__get_swap_page() 也在同一文件中,读者不妨自行阅读。 先来看__swap_free()的开头几行: 00141: /* 2006-12-31 版权所有,侵权必究 第 69 页,共 1481 页 00142: * Caller has made sure that the swapdevice corresponding to entry Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 70 页,共 1481 页 00143: * is still around or has not been recycled. 00144: */ 00145: void __swap_free(swp_entry_t entry, unsigned short count) 00146: { 00147: struct swap_info_struct * p; 00148: unsigned long offset, type; 00149: 00150: if (!entry.val) 00151: goto out; 00152: 00153: type = SWP_TYPE(entry); 00154: if (type >= nr_swapfiles) 00155: goto bad_nofile; 00156: p = & swap_info[type]; 00157: if (!(p->flags & SWP_USED)) 00158: goto bad_device; 如果 entry.val 为 0,就显然不需要做任何事,因为在任何页面交换设备或文件中页面 0 是不用于页 面交换的。接着,如前所述,SWAP_TYPE 所返回的实际上是页面交换设备的序号,即其 swap_info_struct 结构在 swap_info[] 数组中的下标。所以 156 行以此为下标从 swap_info[] 中取得具体文件的 swap_info_struct 结构。文件找到以后,下面就来看具体的页面了: 00159: offset = SWP_OFFSET(entry); 00160: if (offset >= p->max) 00161: goto bad_offset; 00162: if (!p->swap_map[offset]) 00163: goto bad_free; 00164: swap_list_lock(); 00165: if (p->prio > swap_info[swap_list.next].prio) 00166: swap_list.next = type; 00167: swap_device_lock(p); 00168: if (p->swap_map[offset] < SWAP_MAP_MAX) { 00169: if (p->swap_map[offset] < count) 00170: goto bad_count; 00171: if (!(p->swap_map[offset] -= count)) { 00172: if (offset < p->lowest_bit) 00173: p->lowest_bit = offset; 00174: if (offset > p->highest_bit) 00175: p->highest_bit = offset; 00176: nr_swap_pages++; 00177: } 00178: } 00179: swap_device_unlock(p); 00180: swap_list_unlock(); 00181: out: 00182: return; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 71 页,共 1481 页 如前所述,offset 是页面在文件中的位置,当然不能大于文件本身所提供的最大值。而 p->swap_map[offset]是该页面的分配(和使用)计数,如为 0 就表明尚未分配。同时,分配计数也不应 大于 SWAP_MAP_MAX。函数的调用参数 count 表示有几个使用者释放该页面,所以从计数器减去 count。 当技术达到 0 时,这个页面就真正变成空闲的了。此时,如果页面落在当前可供分配的范围之外,就要 相应地调整这个范围的边界 lowest_bit 或 highest_bit,同时,可供分配的盘上页面的数量 nr_swap_pages 也增加了。值得注意的是,释放磁盘页面的操作实际上并不涉及磁盘操作,而只是在内存中“帐面”上 的操作,表示磁盘上那个页面的内容已经作废。所以花费的代价是极小的。 知道了内核怎样管理内存页面和盘上页面以后,就可以来看看内存页面的周转了。当一个内存页面 空闲,也就是留在某一个空闲页面管理区的空闲队列中时,其 page 结构中的计数 count 为 0,而在分配 页面时将其设置成 1。这是在函数 rmqueue()中通过 set_page_count()设置的,我们在前面已经看到过。 所谓内存页面的周转有两方面的意思。其一是页面的分配、使用和回收,并不一定涉及页面的盘区 交换。其二才是盘区交换,而交换的目的最终也是页面的回收。并非所有的内存页面都是可以交换出去 的。事实上,只有映射到用户空间的页面才会被换出,而内核,即系统空间的页面则不在此列。这里要 说明一下,在内核中可以访问所有的物理页面,换言之所有的物理页面在系统空间中都是有映射的。所 谓“用户空间的页面”,是指在至少一个进程的用户空间中有映射的页面,反之则为(只能由)内核使用 的页面。 按页面的内容和性质,用户空间的页面有下面几种: 普通的用户空间页面,包括进程的代码段、数据段、堆栈段,以及动态分配的“存储堆”。其中 有些页面从用户程序即进程的角度看是静态的(如代码段),但从系统的角度看仍是动态分配的。 通过系统调用 mmap()映射到用户空间的已打开文件的内容。 进程间的共享内存区。 这些页面既涉及分配、使用和回收,也涉及页面的换出/换入。 凡是映射到系统空间的页面都不会被换出,但还是可以按使用和周转的不同而大致上分成几类。首 先,内核代码和内核中全局变量所占的内存页面既不需要经过分配也不会被释放,这部分空间是静态的 (相比之下,进程的代码段和全局量都在用户空间,所占的内存页面都是动态的,使用前要经过分配, 最后都会被释放,并且中途可能被换出而回收后另行分配) 除此之外,内核中使用的内存页面也要经过动态分配,但永远都保留在内存中,不会被交换出去。 此类常驻内存的页面根据其内容的性质可以分成两类。 一类是一旦使用完毕便无保存价值,所以立即便可释放、回收。这类页面的周转很简单,就是空闲 ->(分配)->使用->(释放)->空闲。这种用途的内核页面大致上有这样一些: 内核中通过 kmalloc()或 vmalloc()分配、用作某些临时性使用和为管理目的而设的数据结构,如 vma_area_struct 数据结构等等。这些数据结构一旦使用完毕便无保存价值,所以立即便可释放。 不过由于一个页面中往往有多个同种数据结构,所以要在整个页面都空闲时才能把页面释放。 内核中通过 alloc_pages()分配,用作某些临时性使用和为管理目的的内存页面,如每个进程的 系统堆栈所在的两个页面,以及从系统空间复制参数时使用的页面等等。这些页面也是一旦使 用完便无保存价值,所以立即便可释放。 另一类是虽然使用完毕了,但是其内容仍有保存的价值。只要条件允许,把这些页面“养起来”也 许可以提高以后的操作效率。这类页面(或数据结构)在释放之后要放入一个 LRU 队列,经过一段时间 的缓冲让其“老化”;如果在此期间忽然又要用到其内容了,便直接将页面连内容分配给“用户”;否则 便继续老化,直到条件不再允许时才加以回收。这种用途的内核页面大致上有下面这些: 在文件系统操作中用来缓冲存储一些文件目录结构 dentry 的空间。 在文件系统操作中用来缓冲存储一些 inode 结构的空间。 用于文件系统读/写操作的缓冲区。 这些页面的内容是从文件系统中直接读入或经过综合取得的,释放后立即回收另作他用也并无不可, 但是那样以后要用时就又要付出代价了。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 72 页,共 1481 页 相比之下,页面交换是最复杂的,所以我们将花较大的篇幅来介绍。 显然,最简单的页面交换策略就是:每当缺页异常时便分配一个内存页面,并把在磁盘上的页面读 入到分配得到的内存页面中。如果没有空闲页面可供分配,就设法将一个或几个内存页面交换出到磁盘 上,从而腾出一些内存页面来。但是这种完全消极的页面交换策略有个缺点,就是页面的交换总是“临 阵磨枪”,发生在系统忙碌的时候而没有调度的余地。比较积极的办法是定期地,最好是在系统相对空闲 时,挑选一些页面预先换出腾出一些内存页面,从而在系统中维持一定的空闲页面供应量,使得在缺页 异常发生时总是有空闲内存页面可供分配。至于挑选的准则,一般都是 LRU,即挑选“最近最少用到” 的页面。但是,这种积极的页面交换策略实行起来也有问题,因为实际上并不存在一种方法可以准确地 预测对页面地访问。所以,完全有可能发生这样的现象,就是一个页面已经好久没有受到访问了,但是 刚把它换出到磁盘上,却又有访问了,于是只好又赶快把它换进来。在最坏的情况下,有可能整个系统 的处理能力都被这样的换入/换出所饱和,而实际上根本不能进行有效的运算和操作。有人把此种现象称 为(页面的)“抖动”。 为了防止这种情况的发生,可以将页面的换出和内存页面的释放分成两步来做。当系统挑选出若干 内存页面准备交换出时,将这些页面的内容写入相应的磁盘页面种,并且将相应页面表项的内容改成指 向盘上页面(P 标志位为 0,表示页面不在内存),但是所占据的内存页面却并不立即释放,而是将其 page 结构留在一个“暂存”(cache)队列(或称缓冲队列)中,只是使其从“活跃状态”转入了“不活跃状 态”,就像军人从“现役”转入了“预备役”。至于内存页面的“退役”,即最后释放,则推迟到以后有条 件地进行。这样,如果一个页面被换出以后立即又受到访问而发生缺页异常,就可以从物理页面的暂存 队列中找回相应的页面,再次为之建立映射。由于此页面尚未释放,还保留着其原来的内容,就不需要 从盘上读之了。反之,如果经过一段时间以后,一个不活跃的内存页面,即还留在暂存队列却已不再有 (用户空间)映射的页面,还是没有受到访问,那就到了最后退役的时候了。如果留在暂存队列中的页 面又受到访问,确切地说时发生了以此页面为目标的页面异常,那么只要恢复这个页面的映射并使其脱 离暂存队列就可以了,此时该页面又回到了活跃状态。 这种策略显然可以减小抖动的可能,并且减少系统在页面交换上的花费。可是,如果更深入地考察 这个问题,就可以看出其实还可以改进。首先,在准备换出一个页面时并不一定要把它的内容写入磁盘。 如果从最近一次换入该页面以后从未写过这个页面,那么这个页面是“干净”的,也就是与盘上页面的 内容相一致,这样的页面当然不用写出去。其次,即使是“脏”的页面,也不必立刻就写出去,而可以 先从页面映射表断开,经过一段时间的“冷却”或“老化”后再写出去,从而变成“干净”页面。至于 “干净”页面,则可以继续缓冲到真有必要时才加以回收,因为回收一个“干净”页面的花费是很小的。 综上所述,物理内存页面的换入/换出的周转要点如下: 1. 空闲。页面的 page 数据结构通过其队列头结构 list 链入某个页面管理区(zone)的空闲区队列 free_area。页面的使用计数 count 为 0。 2. 分配。通过函数__alloc_pages()或__get_free_page()从某个空闲队列中分配内存页面,并将所分 配页面的使用计数 count 置成 1,其 page 数据结构的队列头 list 结构则变成空闲。 3. 活跃状态。页面的 page 数据结构通过其队列头结构 lru 链入活跃页面队列 active_list,并且至少 又一个进程的(用户空间)页面表项指向该页面。每当为页面建立或恢复映射时,都使页面的 使用计数 count 加 1。 4. 不活跃状态(脏)。页面的 page 数据结构通过其队列头 lru 链入不活跃“脏”页面队列 inactive_dirty_list,但是原则上不再有任何进程的页面表项指向该页面。每当断开页面表的映射 时都使页面的使用计数 count 减 1。 5. 将不活跃“脏”页面的内容写入交换设备,并将页面的 page 数据结构从不活跃“脏”页面队列 inactive_dirty_list 转移到某个不活跃“干净”页面队列中。 6. 不活跃状态(干净)。页面的 page 数据结构通过其队列头结构 lru 链入某个不活跃“干净”页面 队列,每个页面管理区都有一个不活跃“干净”页面队列 inactive_clean_list。 7. 如果在转入不活跃状态以后的一段时间内页面受到访问,则又转入活跃状态并恢复映射。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 73 页,共 1481 页 8. 当有需要时,就从“干净”页面队列中回收页面,或退回到空闲队列中,或直接另行分配。 当然,实际的实现还要更复杂一些。 为了实现这种策略,在 page 数据结构中设置了所需的各种成分,并在内核中设置了全局性的 active_list 和 inactive_dirty_list 两个 LRU 队列,还在每个页面管理区中设置了一个 inactive_clean_list。根 据页面的 page 结构在这些 LRU 队列中的位置,就可以知道这个页面转入不活跃状态后时间的长短,为 回收页面提供参考。同时,还通过一个全局的 address_space 数据结构 swapper_space,把所有可交换内 存页面管理起来,每个可交换内存页面的 page 数据结构都通过其队列头结构 list 链入其中的一个队列。 此外,为加快在暂存队列中的搜索,又设置了一个杂凑表 page_hash_table。 让我们来看看内核是怎样将一个内存页面链入这些队列的。内核在为某个需要换入的页面分配了一 个空闲内存页面后,就通过 add_to_swap_cache()将其 page 结构链入相应的队列,这个函数的代码在 mm/swap_state.c 中: 00054: void add_to_swap_cache(struct page *page, swp_entry_t entry) 00055: { 00056: unsigned long flags; 00057: 00058: #ifdef SWAP_CACHE_INFO 00059: swap_cache_add_total++; 00060: #endif 00061: if (!PageLocked(page)) 00062: BUG(); 00063: if (PageTestandSetSwapCache(page)) 00064: BUG(); 00065: if (page->mapping) 00066: BUG(); 00067: flags = page->flags & ~((1 << PG_error) | (1 << PG_arch_1)); 00068: page->flags = flags | (1 << PG_uptodate); 00069: add_to_page_cache_locked(page, &swapper_space, entry.val); 00070: } 在调用这个函数之前要先将页面锁住,以免受到干扰。因为是刚分配的空闲页面,其 PG_swap_cache 标志位必须为 0,指针 mapping 也必须为 0。同时,页面的内容是刚从交换设备读入的,当然与盘上页面 一致,所以把 PG_uptodate 标志位设置成 1。函数__add_to_page_cache()的定义见 mm/filemap.c: 00476: /* 00477: * Add a page to the inode page cache. 00478: * 00479: * The caller must have locked the page and 00480: * set all the page flags correctly.. 00481: */ 00482: void add_to_page_cache_locked(struct page * page, struct address_space *mapping, unsigned long index) 00483: { 00484: if (!PageLocked(page)) 00485: BUG(); 00486: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 74 页,共 1481 页 00487: page_cache_get(page); 00488: spin_lock(&pagecache_lock); 00489: page->index = index; 00490: add_page_to_inode_queue(mapping, page); 00491: add_page_to_hash_queue(page, page_hash(mapping, index)); 00492: lru_cache_add(page); 00493: spin_unlock(&pagecache_lock); 00494: } 调用参数 mapping 是一个 address_space 结构指针,就是&swapper_space。这种数据结构的定义见 include/linux/fs.h: 00365: struct address_space { 00366: struct list_head clean_pages; /* list of clean pages */ 00367: struct list_head dirty_pages; /* list of dirty pages */ 00368: struct list_head locked_pages; /* list of locked pages */ 00369: unsigned long nrpages; /* number of total pages */ 00370: struct address_space_operations *a_ops; /* methods */ 00371: struct inode *host; /* owner: inode, block_device */ 00372: struct vm_area_struct *i_mmap; /* list of private mappings */ 00373: struct vm_area_struct *i_mmap_shared; /* list of shared mappings */ 00374: spinlock_t i_shared_lock; /* and spinlock protecting it */ 00375: }; 结构中有三个队列头,前两个分别用于“干净”的和“脏”的页面(需要写出),另一个队列头 locked_pages 用于需要暂时锁定在内存不让换出的页面。数据结构 swapper_space 的定义见于 mm/swap_state.c: 00031: struct address_space swapper_space = { 00032: LIST_HEAD_INIT(swapper_space.clean_pages), 00033: LIST_HEAD_INIT(swapper_space.dirty_pages), 00034: LIST_HEAD_INIT(swapper_space.locked_pages), 00035: 0, /* nrpages */ 00036: &swap_aops, 00037: }; 结构中的最后一个成分指向另一个数据结构 swap_aops,里面包含了各种 swap 操作的函数指针。 从函数 add_to_page_cache_locked()中可以看到,页面 page 被加入到三个队列中。下面读者会看到, page 结构通过其队列头 list 链入暂存队列 swapper_space,通过指针 next_hash 和双重指针 pprev_hash 链 入某个杂凑队列,并通过其队列头 lru 链入 LRU 队列 active_list。 代码中的 page_cache_get 在 pagemap.h 中定义为 get_page(page),实际上只是将页面的使用计数 page->count 加 1。这是在 include/linux/mm.h 中定义的: 00150: #define get_page(p) atomic_inc(&(p)->count) 00031: #define page_cache_get(x) get_page(x) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 75 页,共 1481 页 先将给定的 page 结构通过 add_to_inode_queue()加入到 swapper_space 中的 clean_pages 队列,其代 码在 mm/filemap.c 中: 00072: static inline void add_page_to_inode_queue(struct address_space *mapping, struct page * page) 00073: { 00074: struct list_head *head = &mapping->clean_pages; 00075: 00076: mapping->nrpages++; 00077: list_add(&page->list, head); 00078: page->mapping = mapping; 00079: } 可见,链入的是 swapper_space 中的 clean_pages 队列,刚从交换设备读入的页面当然是“干净”页 面。为什么这个函数叫 add_page_to_inode_queue 呢?这是因为页面的缓冲不光是为页面交换而设的,文 件的读/写也要用到这种缓冲机制。通常来自同一个文件的页面就通过一个 address_space 数据结构来管 理,而代表着一个文件的 inode 数据结构中有个成分 i_data,那就是一个 address_space 数据结构。从这 个意义上说。用来管理可交换页面的 address_space 数据结构 swapper_space 只是个特例。 然后通过__add_page_to_hash_queue()将其链入到某个杂凑队列中,其代码也在 mm/filemap.c 中: 00058: static void add_page_to_hash_queue(struct page * page, struct page **p) 00059: { 00060: struct page *next = *p; 00061: 00062: *p = page; 00063: page->next_hash = next; 00064: page->pprev_hash = p; 00065: if (next) 00066: next->pprev_hash = &page->next_hash; 00067: if (page->buffers) 00068: PAGE_BUG(page); 00069: atomic_inc(&page_cache_size); 00070: } 链入的具体队列取决于杂凑值(定义在 include/linux/pagemap.h 中): 00068: #define page_hash(mapping,index) (page_hash_table+_page_hashfn(mapping,index)) 最后将页面的 page 数据结构通过 lru_cache_add()链入到内核中的 LRU 队列 active_list 中,其代码在 mm/swap.c 中: 00226: /** 00227: * lru_cache_add: add a page to the page lists 00228: * @page: the page to add 00229: */ 00230: void lru_cache_add(struct page * page) 00231: { Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 76 页,共 1481 页 00232: spin_lock(&pagemap_lru_lock); 00233: if (!PageLocked(page)) 00234: BUG(); 00235: DEBUG_ADD_PAGE 00236: add_page_to_active_list(page); 00237: /* This should be relatively rare */ 00238: if (!page->age) 00239: deactivate_page_nolock(page); 00240: spin_unlock(&pagemap_lru_lock); 00241: } 这里的 add_page_to_active_list()是个宏操作,定义于 include/linux/swap.h 内: 00209: #define add_page_to_active_list(page) { \ 00210: DEBUG_ADD_PAGE \ 00211: ZERO_PAGE_BUG \ 00212: SetPageActive(page); \ 00213: list_add(&(page)->lru, &active_list); \ 00214: nr_active_pages++; \ 00215: } 由于 page 数据结构可以通过其同一个队列头结构 lru 链入不同的 LRU 队列,所以需要有 PG_active、 PG_inactive_dirty 以及 PG_inactive_clean 等标志位来表明目前是在哪一个队列中。以后读者将看到页面 在这些队列间的转移。 存储管理不完全是内核的事,用户进程可以在相当程度上参与对内存的管理,可以在一定的范围内 对于其本身的内存管理向内核提出一些要求,例如通过系统调用 mmap()将一个文件映射到它的用户空 间。特别是特权用户进程,还掌握着对换入/换出机制的全局性控制权,这就是系统调用 swapon()和 swapoff()。调用界面为: swapon(const char *path, int swapflags) swapoff(const char *path) 这两个系统调用是为特权用户进程设置的,用以开始或中止把某个特定的盘区或文件文件用于页面 的换入和换出。当所有的盘区和文件都不再用于页面交换时,存储管理的机制就退化到单纯的地址映射 和保护。在实践中,这样做有时候是必要的。一些“嵌入式”系统,常常用 Flash Memory(内存)来代 替磁盘介质。对 Flash Memory 的写操作是很麻烦费时的,需要将存储器中的内容先抹去,然后才写入, 而抹去的过程又很慢(与磁盘读写相比较)。显然,Flash Memory 是不适合用作页面交换的。所以在这 样的系统中应将盘区交换关闭。事实上,在 Linux 内核刚引导进来之初,所有的页面交换都是关闭的, 内核在初始化期间要执行/etc/rc.d/rc.S 命令文件,而这个文件中的命令行之一就是与系统调用 swapon() 相应的实用程序 swapon。只要把这个命令行从文件中拿掉就没有页面交换了。 此外,还有几个用于共享内存的系统调用,也是与存储管理有关的。由于习惯上将共享内存归入进 程间通讯的范畴,对这几个系统调用将在进程间通讯一章中另行介绍。 2.7. 物理页面的分配 上一节中曾经提到,当需要分配若干个内存页面时,用于 DMA 的内存页面必须是连续的。其实, 为便于管理,特别是出于对物理存储空间“质地”一致性的考虑,即是不是用于 DMA 的内存页面也是Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 77 页,共 1481 页 连续分配的。 当一个进程需要分配若干个连续的物理页面时,可以通过 alloc_pages()来完成。Linux 内核 2.4.0 的 代码中有两个 alloc_pages(),一个在 mm/numa.c 中,另一个在 mm/page_alloc.c 中,编译时根据所定义的 条件编译选项 CONFIG_DISCONTIGMEM 决定取舍。为什么呢?这就是出于前一节中所述对物理存储空 间“质地”一致性的考虑。 我们先来看看用于 NUMA 结构的 alloc_pages(),其代码在 mm/numa.c 中: 00043: #ifdef CONFIG_DISCONTIGMEM …………………… 00091: /* 00092: * This can be refined. Currently, tries to do round robin, instead 00093: * should do concentratic circle search, starting from current node. 00094: */ 00095: struct page * alloc_pages(int gfp_mask, unsigned long order) 00096: { 00097: struct page *ret = 0; 00098: pg_data_t *start, *temp; 00099: #ifndef CONFIG_NUMA 00100: unsigned long flags; 00101: static pg_data_t *next = 0; 00102: #endif 00103: 00104: if (order >= MAX_ORDER) 00105: return NULL; 00106: #ifdef CONFIG_NUMA 00107: temp = NODE_DATA(numa_node_id()); 00108: #else 00109: spin_lock_irqsave(&node_lock, flags); 00110: if (!next) next = pgdat_list; 00111: temp = next; 00112: next = next->node_next; 00113: spin_unlock_irqrestore(&node_lock, flags); 00114: #endif 00115: start = temp; 00116: while (temp) { 00117: if ((ret = alloc_pages_pgdat(temp, gfp_mask, order))) 00118: return(ret); 00119: temp = temp->node_next; 00120: } 00121: temp = pgdat_list; 00122: while (temp != start) { 00123: if ((ret = alloc_pages_pgdat(temp, gfp_mask, order))) 00124: return(ret); 00125: temp = temp->node_next; 00126: } 00127: return(0); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 78 页,共 1481 页 00128: } 首先,对 NUMA 的支持是通过条件编译作为可选项提供的,所以这段代码仅在可选项 CONFIG_DISCONTIGMEM 有定义时才得到编译。不过,这里用来作为条件的是“不连续存储空间”, 而不是 CONFIG_NUMA。其实,不连续的物理内存存储空间是一种广义的 NUMA,因为那说明在最低 物理地址和最高物理地址之间存在着空洞,而有空洞的空间当然是非均质的。所以在地址不连续的物理 空间也要像在质地不均匀的物理空间那样划分出若干连续(而且均匀)的“节点”。所以,在存储空间不 连续的系统中,每个模块都有若干个节点,因而都有个 pg_data_t 数据结构的队列。 调用时有两个参数。第一个参数gfp_mask是个整数,表示采用哪一种分配策略;第二个参数order表 示所需要的物理块大小,可以是 1、2、4、…、直到 2MAX_ORDER个页面。 在 NUMA 结构的系统中,可以通过宏操作 NUMA_DATA 和 numa_node_id()找到 CPU 所在节点的 pg_data_t 数据结构队列。而在不连续的 UMA 结构中,则也有个 pg_data_t 数据结构的队列 pgdat_list, 分配时轮流从各个节点开始,以求各节点负荷的平衡。 函数中主要的操作在于两个 while 循环,它们分两截(先是从 temp 开始到队列的末尾,然后回头从 第一个节点到最初开始的地方)扫描队列中所有的节点,直至在某个节点内分配成功,或彻底失败而返 回 0。对于每个节点,调用 alloc_pages_pgdat()试图分配所需的页面,这个函数的代码在 mm/numa.c 中: 00085: static struct page * alloc_pages_pgdat(pg_data_t *pgdat, int gfp_mask, 00086: unsigned long order) 00087: { 00088: return __alloc_pages(pgdat->node_zonelists + gfp_mask, order); 00089: } 可见,参数 gfp_mask 在这里用作给定结点中数组 node_zonelists[]的下标,决定具体的分配策略。把 这段代码与下面用于连续空间 UMA 结构的 alloc_pages()对照一下,就可以看出区别:在连续空间 UMA 结构中只有一个节点 contig_page_data,而在 NUMA 结构或不连续空间 UMA 结构中则有多个。 连续空间 UMA 结构的 alloc_pages()是在文件 include/linux/mm.h 中定义的: 00343: #ifndef CONFIG_DISCONTIGMEM 00344: static inline struct page * alloc_pages(int gfp_mask, unsigned long order) 00345: { 00346: /* 00347: * Gets optimized away by the compiler. 00348: */ 00349: if (order >= MAX_ORDER) 00350: return NULL; 00351: return __alloc_pages(contig_page_data.node_zonelists+(gfp_mask), order); 00352: } 与 NUMA 结构的 alloc_pages()相反,这个函数仅在 CONFIG_DISCONTIGMEM 无定义时才得到编 译。所以这两个同名的函数只有一个会得到编译。 具体的页面分配由函数__alloc_pages()完成,其代码在 mm/page_alloc.c 中,我们分段阅读: [alloc_pages() > __alloc_pages()] 00270: /* 00271: * This is the 'heart' of the zoned buddy allocator: 00272: */ Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 79 页,共 1481 页 00273: struct page * __alloc_pages(zonelist_t *zonelist, unsigned long order) 00274: { 00275: zone_t **zone; 00276: int direct_reclaim = 0; 00277: unsigned int gfp_mask = zonelist->gfp_mask; 00278: struct page * page; 00279: 00280: /* 00281: * Allocations put pressure on the VM subsystem. 00282: */ 00283: memory_pressure++; 00284: 00285: /* 00286: * (If anyone calls gfp from interrupts nonatomically then it 00287: * will sooner or later tripped up by a schedule().) 00288: * 00289: * We are falling back to lower-level zones if allocation 00290: * in a higher zone fails. 00291: */ 00292: 00293: /* 00294: * Can we take pages directly from the inactive_clean 00295: * list? 00296: */ 00297: if (order == 0 && (gfp_mask & __GFP_WAIT) && 00298: !(current->flags & PF_MEMALLOC)) 00299: direct_reclaim = 1; 00300: 00301: /* 00302: * If we are about to get low on free pages and we also have 00303: * an inactive page shortage, wake up kswapd. 00304: */ 00305: if (inactive_shortage() > inactive_target / 2 && free_shortage()) 00306: wakeup_kswapd(0); 00307: /* 00308: * If we are about to get low on free pages and cleaning 00309: * the inactive_dirty pages would fix the situation, 00310: * wake up bdflush. 00311: */ 00312: else if (free_shortage() && nr_inactive_dirty_pages > free_shortage() 00313: && nr_inactive_dirty_pages >= freepages.high) 00314: wakeup_bdflush(0); 00315: 调用时有两个参数。第一个参数 zonelist 指向代表着一个具体分配策略的 zonelist_t 数据结构。另一 个参数 order 则与前面 alloc_pages()中的相同。全局量 memory_pressure 表示内存页面管理所受的压力,Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 80 页,共 1481 页 分配页面时递增,归还时则递减。这里的局部变量 gfp_mask 来自代表着具体分配策略的数据结构,是一 些用于控制目的的标志位。如果要求分配的只是单个页面,而且要等待分配完成,有不是用于管理目的, 则把一个局部量 direct_reclaim 设成 1,表示可以从相应页面管理区的“不活跃干净页面”缓冲队列中回 收。这些页面的内容都已经写出至页面交换设备或文件中,只是还保存着页面的内容,使得在需要这个 页面的内容时无需再从设备或文件读入,但是当空闲页面短缺时,就顾不得那么多了。由于一般而言这 些页面不一定能像真正的空闲页面那样连成块,所以仅在要求分配单个页面时才能从这些页面中回收。 此外,当发现可分配页面短缺时,还要唤醒 kswapd 和 bdflush 两个内核线程,让它们设法腾出一些内存 页面来(详见“页面的定期换出”)。我们继续往下看: [alloc_pages() > __alloc_pages()] 00316: try_again: 00317: /* 00318: * First, see if we have any zones with lots of free memory. 00319: * 00320: * We allocate free memory first because it doesn't contain 00321: * any data ... DUH! 00322: */ 00323: zone = zonelist->zones; 00324: for (;;) { 00325: zone_t *z = *(zone++); 00326: if (!z) 00327: break; 00328: if (!z->size) 00329: BUG(); 00330: 00331: if (z->free_pages >= z->pages_low) { 00332: page = rmqueue(z, order); 00333: if (page) 00334: return page; 00335: } else if (z->free_pages < z->pages_min && 00336: waitqueue_active(&kreclaimd_wait)) { 00337: wake_up_interruptible(&kreclaimd_wait); 00338: } 00339: } 00340: 这是对一个分配策略中规定的所有页面管理区的循环。循环中依次考察各个管理区中空闲页面的总 量,如果总量尚在“低水位”以上,就通过 rmqueue()试图从该管理区中分配。要是发现管理区中的空闲 页面总量已经降到了最低点,而且由进程(实际上只能是内核线程 kreclaimd)在一个等待队列 kreclaimd_wait 中睡眠,就把它唤醒,让它帮助回收一些页面备用。函数 rmqueue()试图从一个页面管理 区分配若干个连续的内存页面,其代码在 mm/page_alloc.c 中: [alloc_pages() > __alloc_pages() > rmqueue()] 00172: static struct page * rmqueue(zone_t *zone, unsigned long order) 00173: { 00174: free_area_t * area = zone->free_area + order; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 81 页,共 1481 页 00175: unsigned long curr_order = order; 00176: struct list_head *head, *curr; 00177: unsigned long flags; 00178: struct page *page; 00179: 00180: spin_lock_irqsave(&zone->lock, flags); 00181: do { 00182: head = &area->free_list; 00183: curr = memlist_next(head); 00184: 00185: if (curr != head) { 00186: unsigned int index; 00187: 00188: page = memlist_entry(curr, struct page, list); 00189: if (BAD_RANGE(zone,page)) 00190: BUG(); 00191: memlist_del(curr); 00192: index = (page - mem_map) - zone->offset; 00193: MARK_USED(index, curr_order, area); 00194: zone->free_pages -= 1 << order; 00195: 00196: page = expand(zone, page, index, order, curr_order, area); 00197: spin_unlock_irqrestore(&zone->lock, flags); 00198: 00199: set_page_count(page, 1); 00200: if (BAD_RANGE(zone,page)) 00201: BUG(); 00202: DEBUG_ADD_PAGE 00203: return page; 00204: } 00205: curr_order++; 00206: area++; 00207: } while (curr_order < MAX_ORDER); 00208: spin_unlock_irqrestore(&zone->lock, flags); 00209: 00210: return NULL; 00211: } 以前讲过,代表物理页面的 page 数据结构,以双向链表的形式链接在管理区的某个空闲队列中,分 配页面时当然要把它从队列中摘链,而摘链的过程是不容许其他的进程、其他的处理器(如果有的话) 来打扰的。所以要用 spin_lock_irqsave()将相应的分区加上锁,不容许打扰。管理区结构中的空闲区 zone->free_area 是个结构数组,所以 zone->free_area+order 就指向链接所需大小的物理内存块的队列头。 主要的操作是在一个 do-while 循环中进行。它首先在恰好满足大小要求的队列里分配,如果不行的话就 试试更大的(指物理内存块)队列中分配,成功的话,就把分配到的大块中剩余的部分分解成小块而链 入相应的队列(通过 196 行的 expand())。 第 188 行中的 memlist_entry()从一个非空的队列里取第一个结构 page 元素,然后通过 memlist_del()Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 82 页,共 1481 页 将其从队列中摘除。对此,我们已经在第 1 章中做过解释。 函数 expand()是在同一个文件(mm/page_alloc.c)中定义的: [alloc_pages() > __alloc_pages() > rmqueue() > expand()] 00150: static inline struct page * expand (zone_t *zone, struct page *page, 00151: unsigned long index, int low, int high, free_area_t * area) 00152: { 00153: unsigned long size = 1 << high; 00154: 00155: while (high > low) { 00156: if (BAD_RANGE(zone,page)) 00157: BUG(); 00158: area--; 00159: high--; 00160: size >>= 1; 00161: memlist_add_head(&(page)->list, &(area)->free_list); 00162: MARK_USED(index, high, area); 00163: index += size; 00164: page += size; 00165: } 00166: if (BAD_RANGE(zone,page)) 00167: BUG(); 00168: return page; 00169: } 调用参数表中的 low 对应于表示所需物理块大小的 order,而 high 则对应于表示当前空闲空闲队列 (也就是从中得到能满足要求的物理块的队列)的 curr_order。当两者相符时,从 155 行开始的 while 循 环就被跳过了。若是分配到的物理块大于所需的大小(不可能小于所需的大小),那就将该物理块链入低 一档也就是物理块大小减半的空闲块队列中去,并相应设置该空闲区队列的位图,这是在第 158 行指 162 行中完成的。然后从该物理块中切去一半,而以其后半部作为一个新的物理块(第 163 和 164 行),而后 开始下一轮循环也就是处理更低一档的空闲块队列。这样,最后必有 high 与 low 两者相等,也就是实际 上下的物理块与要求恰好相符的时候,循环就结束了。 就这样,rmqueue()一直往上扫描,知道成功或者最终失败。如果 rmqueue()失败,则__alloc_pages() 通过其 for 循环降格以求,接着试分配策略中规定的下一个管理区,直到成功,或者碰到了空指针而最 终失败(见 327 行)。如果分配成功了,则__alloc_pages()返回一个 page 结构指针,指向页面块中第一个 页面的 page 结构,并且该 page 结构中的使用计数 count 为 1。如果每次分配的都是单个页面(order 为 0), 则自然每个页面的使用计数都是 1。 要是给定分配策略中所有的页面管理区都失败了,那就只好“加大力度”再试,一是降低对页面管 理区中保持“水位”的要求,二是把缓冲在管理区中的“不活跃干净页面”也考虑进去。我们再往下看 __alloc_pages()的代码(mm/page_alloc.c)。 [alloc_pages() > __alloc_pages()] 00341: /* 00342: * Try to allocate a page from a zone with a HIGH 00343: * amount of free + inactive_clean pages. 00344: * Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 83 页,共 1481 页 00345: * If there is a lot of activity, inactive_target 00346: * will be high and we'll have a good chance of 00347: * finding a page using the HIGH limit. 00348: */ 00349: page = __alloc_pages_limit(zonelist, order, PAGES_HIGH, direct_reclaim); 00350: if (page) 00351: return page; 00352: 00353: /* 00354: * Then try to allocate a page from a zone with more 00355: * than zone->pages_low free + inactive_clean pages. 00356: * 00357: * When the working set is very large and VM activity 00358: * is low, we're most likely to have our allocation 00359: * succeed here. 00360: */ 00361: page = __alloc_pages_limit(zonelist, order, PAGES_LOW, direct_reclaim); 00362: if (page) 00363: return page; 00364: 这里先以参数 PAGES_HIGH 调用__alloc_pages_limit() ;如果还不行就再加大力度,改以 PAGES_LOW 再调用一次。函数__alloc_pages_limit()的代码也在 mm/page_alloc.c 中: [alloc_pages() > __alloc_pages() > __alloc_pages_limit()] 00213: #define PAGES_MIN 0 00214: #define PAGES_LOW 1 00215: #define PAGES_HIGH 2 00216: 00217: /* 00218: * This function does the dirty work for __alloc_pages 00219: * and is separated out to keep the code size smaller. 00220: * (suggested by Davem at 1:30 AM, typed by Rik at 6 AM) 00221: */ 00222: static struct page * __alloc_pages_limit(zonelist_t *zonelist, 00223: unsigned long order, int limit, int direct_reclaim) 00224: { 00225: zone_t **zone = zonelist->zones; 00226: 00227: for (;;) { 00228: zone_t *z = *(zone++); 00229: unsigned long water_mark; 00230: 00231: if (!z) 00232: break; 00233: if (!z->size) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 84 页,共 1481 页 00234: BUG(); 00235: 00236: /* 00237: * We allocate if the number of free + inactive_clean 00238: * pages is above the watermark. 00239: */ 00240: switch (limit) { 00241: default: 00242: case PAGES_MIN: 00243: water_mark = z->pages_min; 00244: break; 00245: case PAGES_LOW: 00246: water_mark = z->pages_low; 00247: break; 00248: case PAGES_HIGH: 00249: water_mark = z->pages_high; 00250: } 00251: 00252: if (z->free_pages + z->inactive_clean_pages > water_mark) { 00253: struct page *page = NULL; 00254: /* If possible, reclaim a page directly. */ 00255: if (direct_reclaim && z->free_pages < z->pages_min + 8) 00256: page = reclaim_page(z); 00257: /* If that fails, fall back to rmqueue. */ 00258: if (!page) 00259: page = rmqueue(z, order); 00260: if (page) 00261: return page; 00262: } 00263: } 00264: 00265: /* Found nothing. */ 00266: return NULL; 00267: } 这个函数的代码与前面__alloc_pages()中的 for 循环在逻辑上只是稍有不同,我们把它留给读者。其 中 reclaim_page()从页面管理区的 inactive_clean_list 队列中回收页面,其代码在 mm/vmscan.c 中,我们把 它列出在“页面的定期换出”一节的末尾,读者可以在学习了页面的换出和换入以后自己阅读。注意调 用这个函数的条件是参数 direct_reclaim 非 0,所以要求分配的一定是单个页面。 还是不行的话,那就说明这些管理区中的页面已经严重短缺了,让我们看看__alloc_pages()是如何对 付的: [alloc_pages() > __alloc_pages()] 00365: /* 00366: * OK, none of the zones on our zonelist has lots 00367: * of pages free. Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 85 页,共 1481 页 00368: * 00369: * We wake up kswapd, in the hope that kswapd will 00370: * resolve this situation before memory gets tight. 00371: * 00372: * We also yield the CPU, because that: 00373: * - gives kswapd a chance to do something 00374: * - slows down allocations, in particular the 00375: * allocations from the fast allocator that's 00376: * causing the problems ... 00377: * - ... which minimises the impact the "bad guys" 00378: * have on the rest of the system 00379: * - if we don't have __GFP_IO set, kswapd may be 00380: * able to free some memory we can't free ourselves 00381: */ 00382: wakeup_kswapd(0); 00383: if (gfp_mask & __GFP_WAIT) { 00384: __set_current_state(TASK_RUNNING); 00385: current->policy |= SCHED_YIELD; 00386: schedule(); 00387: } 00388: 00389: /* 00390: * After waking up kswapd, we try to allocate a page 00391: * from any zone which isn't critical yet. 00392: * 00393: * Kswapd should, in most situations, bring the situation 00394: * back to normal in no time. 00395: */ 00396: page = __alloc_pages_limit(zonelist, order, PAGES_MIN, direct_reclaim); 00397: if (page) 00398: return page; 00399: 首先是唤醒内核线程 kswapd,让它设法换出一些页面。如果分配策略表明对于要求分配的页面是志 在必得,分配不到时宁可等待,就让系统来一次调度,并且让当前进程为其他进程让一下路。这样,以 来让 kswapd 有可能立即被调度运行,二来其他进程也有可能会释放出一些页面,再说页可减缓了要求 分配页面的速度,减轻了压力。当请求分配页面的进程再次被调度运行时,或者分配策略表明不允许等 待时,就以参数 PAGES_MIN 再调用一次__alloc_pages_limit(),可是,要是再失败呢?这时候就要看是 谁在要求分配内存页面了。如果要求分配页面的进程(或线程)是 kswapd 或 kreclaimd,本身就是“内 存分配工作者”,要求分配内存页面的目的是执行公务,是要更好地分配内存页面,这当然比一般的进程 更重要。这些进程的 task_struct 结构中 flags 字段的 PF_MEMALLOC 标志位为 1。我们先看对于一般进 程,即 PF_MEMALLOC 标志为 0 的进程的对策。 [alloc_pages() > __alloc_pages()] 00400: /* 00401: * Damn, we didn't succeed. Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 86 页,共 1481 页 00402: * 00403: * This can be due to 2 reasons: 00404: * - we're doing a higher-order allocation 00405: * --> move pages to the free list until we succeed 00406: * - we're /really/ tight on memory 00407: * --> wait on the kswapd waitqueue until memory is freed 00408: */ 00409: if (!(current->flags & PF_MEMALLOC)) { 00410: /* 00411: * Are we dealing with a higher order allocation? 00412: * 00413: * Move pages from the inactive_clean to the free list 00414: * in the hope of creating a large, physically contiguous 00415: * piece of free memory. 00416: */ 00417: if (order > 0 && (gfp_mask & __GFP_WAIT)) { 00418: zone = zonelist->zones; 00419: /* First, clean some dirty pages. */ 00420: current->flags |= PF_MEMALLOC; 00421: page_launder(gfp_mask, 1); 00422: current->flags &= ~PF_MEMALLOC; 00423: for (;;) { 00424: zone_t *z = *(zone++); 00425: if (!z) 00426: break; 00427: if (!z->size) 00428: continue; 00429: while (z->inactive_clean_pages) { 00430: struct page * page; 00431: /* Move one page to the free list. */ 00432: page = reclaim_page(z); 00433: if (!page) 00434: break; 00435: __free_page(page); 00436: /* Try if the allocation succeeds. */ 00437: page = rmqueue(z, order); 00438: if (page) 00439: return page; 00440: } 00441: } 00442: } 00443: /* 00444: * When we arrive here, we are really tight on memory. 00445: * 00446: * We wake up kswapd and sleep until kswapd wakes us Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 87 页,共 1481 页 00447: * up again. After that we loop back to the start. 00448: * 00449: * We have to do this because something else might eat 00450: * the memory kswapd frees for us and we need to be 00451: * reliable. Note that we don't loop back for higher 00452: * order allocations since it is possible that kswapd 00453: * simply cannot free a large enough contiguous area 00454: * of memory *ever*. 00455: */ 00456: if ((gfp_mask & (__GFP_WAIT|__GFP_IO)) == (__GFP_WAIT|__GFP_IO)) { 00457: wakeup_kswapd(1); 00458: memory_pressure++; 00459: if (!order) 00460: goto try_again; 00461: /* 00462: * If __GFP_IO isn't set, we can't wait on kswapd because 00463: * kswapd just might need some IO locks /we/ are holding ... 00464: * 00465: * SUBTLE: The scheduling point above makes sure that 00466: * kswapd does get the chance to free memory we can't 00467: * free ourselves... 00468: */ 00469: } else if (gfp_mask & __GFP_WAIT) { 00470: try_to_free_pages(gfp_mask); 00471: memory_pressure++; 00472: if (!order) 00473: goto try_again; 00474: } 00475: 00476: } 00477: 分配内存页面失败的原因可能是两方面的,一种可能是可分配页面的总量是在已经太少了;另一种 是总量其实还不少,但是所要求的页面块大小却不能满足,此时往往有不少单个的页面在管理区的 inactive_clean_pages 队列中,如果加以回收就有可能拼装起较大的页面块。同时,可能还有些“脏”页 面在全局的 inactive_dirty_pages 队列中,把脏页面的内容写出到交换设备上或文件中,就可以使它们变 成“干净”页面而加以回收。所以,针对第二种可能,代码中通过 page_launder()把“脏”页面“洗净” (详见“页面的定期换出”),然后通过一个 for 循环在各个页面管理区中回收和释放“干净”页面。具 体的回收和释放是通过一个 while 循环完成的。在通过__free_page()释放页面时会把空闲页面拼装起尽可 能大的页面块,所以在每回收了一个页面以后都要调用 rmqueue()试一下,看看是否已经能满足要求。值 得注意的是,这里在调用 page_launder()期间把当前进程的 PF_MEMALLOC 标志位设成 1,使其有了“执 行公务”时的特权。为什么要这样做呢?这是因为在 page_launder()中也会要求分配一些临时行的工作页 面,不把 PF_MEMALLOC 标志位设成 1 就可能递归地进入这里的 409~476 行。 如果回收了这样的页面以后还是不行,那就是可分配页面的总量不够了。这时候一种办法是唤醒 kswapd,而要求分配页面的进程则睡眠等待,由 kswapd 在完成了一轮运行之后再反过来唤醒要求分配 页面的进程。然后,如果要求分配的是单个页面,就通过 goto 语句转回__alloc_pages()开头出的标号Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 88 页,共 1481 页 try_again 处。另一种办法是直接调用 try_to_free_pages(),这个函数本来是由 kswapd 调用的。 那么,如果是“执行公务”呢?或者,虽然不是执行公务,但已想尽了一切办法,采取了一切措施, 只不过因为要求分配的是成块的页面才没有转向前面的标号 try_again 处。 前面我们看到,一次次加大力度调用__alloc_pages_limit()时,实际上还是有所保留的。例如,最后 一次以 PAGES_MIN 为参数,此时判断是否可以分配的准则是管理区中可分配页面的“水位”高于 z->pages_min。之所以还留着一点“老本”,是为了应付紧急状况,而现在已到了“不惜血本”的时候了。 我们继续往下看__alloc_pages()的代码。 [alloc_pages() > __alloc_pages()] 00478: /* 00479: * Final phase: allocate anything we can! 00480: * 00481: * Higher order allocations, GFP_ATOMIC allocations and 00482: * recursive allocations (PF_MEMALLOC) end up here. 00483: * 00484: * Only recursive allocations can use the very last pages 00485: * in the system, otherwise it would be just too easy to 00486: * deadlock the system... 00487: */ 00488: zone = zonelist->zones; 00489: for (;;) { 00490: zone_t *z = *(zone++); 00491: struct page * page = NULL; 00492: if (!z) 00493: break; 00494: if (!z->size) 00495: BUG(); 00496: 00497: /* 00498: * SUBTLE: direct_reclaim is only possible if the task 00499: * becomes PF_MEMALLOC while looping above. This will 00500: * happen when the OOM killer selects this task for 00501: * instant execution... 00502: */ 00503: if (direct_reclaim) { 00504: page = reclaim_page(z); 00505: if (page) 00506: return page; 00507: } 00508: 00509: /* XXX: is pages_min/4 a good amount to reserve for this? */ 00510: if (z->free_pages < z->pages_min / 4 && 00511: !(current->flags & PF_MEMALLOC)) 00512: continue; 00513: page = rmqueue(z, order); 00514: if (page) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 89 页,共 1481 页 00515: return page; 00516: } 00517: 00518: /* No luck.. */ 00519: printk(KERN_ERR "__alloc_pages: %lu-order allocation failed.\n", order); 00520: return NULL; 00521: } 如果连这也失败,那一定是系统有问题了。 读者也许会说:好家伙,分配一个(或几个)内存页面有这么麻烦,那 CPU 还有多少时间能用于实 质性的计算呢?要知道我们这里是假定分配页面的努力“屡战屡败”,而又“屡败屡战”,这才有这么多 艰苦卓绝的努力。实际上,绝大多数的分配页面操作都是在分配策略所规定的第一个页面管理区中就成 功了。不过,从这里我们可以看到设计一个系统需要何等周密的考虑。 2.8. 页面的定期换出 这个情景比较长,读者得有点耐心。 为了避免总是在 CPU 忙碌的时候,也就是在缺页异常发生的时候,临时再来搜索可供换出的内存页 面并加以换出,Linux 内核定期地检查并且预先将若干页面换出,腾出空间,以减轻系统在缺页异常发 生时的负担。当然,由于无法确切地预测页面的使用,即使这样做了也还是不能完全杜绝在缺页异常发 生时内存没有空闲页面,而只好临时寻找可换出页面的可能。但是,这样毕竟可以减少其发生的概率。 并且,通过选择适当的参数,例如每个多久换一次,每次换出多少页面,可以使得在缺页异常发生时必 须临时寻找页面换出的情况实际上很少发生。为此,在 Linux 内核中设置了一个专司定期将页面换出的 “守护神”kswapd。 从原理上说,kswapd 相当于一个进程,有其自身的进程控制块 task_struct 结构,跟其他进程一样受 内核的调度。而正因为内核将它按进程来调度,就可以让它在系统相对空闲的时候运行。不过,与普通 进程相比,kswapd 还是有其特殊性。首先,它没有自己独立的地址空间,所以在近代操作系统理论中称 为“线程”(thread)以示区别。那么,kswapd 使用谁的地址空间呢?它使用的是内核的空间。在这一点 上,它与中断服务程序相似。其次,它的代码是静态地链接在内核中的,可以直接调用内核中的各种子 程序,而不想普通的进程那样只能通过调用,使用预先定义好的一组功能。 本节讲述 kswapd 受内核调度而运行并走完一条例行路线的全过程。 线程 kswapd 的源代码基本上都在 mm/vmscan.c 中。先来看它的建立: 01146: static int __init kswapd_init(void) 01147: { 01148: printk("Starting kswapd v1.8\n"); 01149: swap_setup(); 01150: kernel_thread(kswapd, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL); 01151: kernel_thread(kreclaimd, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL); 01152: return 0; 01153: } 函数 kswapd_init()是在系统初始化期间受到调用的,它主要做两件事。第一件是在 swap_setup()中根 据物理内存的大小设定一个全局量 page_cluster: [kswapd_init() > swap_setup()] 00293: /* Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 90 页,共 1481 页 00294: * Perform any setup for the swap system 00295: */ 00296: void __init swap_setup(void) 00297: { 00298: /* Use a smaller cluster for memory <16MB or <32MB */ 00299: if (num_physpages < ((16 * 1024 * 1024) >> PAGE_SHIFT)) 00300: page_cluster = 2; 00301: else if (num_physpages < ((32 * 1024 * 1024) >> PAGE_SHIFT)) 00302: page_cluster = 3; 00303: else 00304: page_cluster = 4; 00305: } 这是一个跟磁盘设备驱动有关的参数。由于读磁盘时先要经过寻道,并且寻道是个比较费时间的操 作,所以如果每次只读一个页面是不经济的。比较好的办法是既然读了就干脆多读几个页面,称为“预 读”。但是预读意味着每次需要暂存更多的内存页面,所以需要决定一个适当的数量,而根据物理内存本 身的大小来确定这个参数显然是合理的。第二件事就是创建线程 kswapd,这是由 kernel_thread()完成的。 这里还创建了另一个线程 kreclaimd,也是跟存储管理有关,不过不像 kswapd 那么复杂和重要,所以我 们暂且把它放在一边。关于建立线程详情请参阅进程管理一章,这里暂且假定线程 kswapd 就此建立了, 并且从函数 kswapd()开始执行。其代码在 mm/vmscan.c 中: 00947: /* 00948: * The background pageout daemon, started as a kernel thread 00949: * from the init process. 00950: * 00951: * This basically trickles out pages so that we have _some_ 00952: * free memory available even if there is no other activity 00953: * that frees anything up. This is needed for things like routing 00954: * etc, where we otherwise might have all activity going on in 00955: * asynchronous contexts that cannot page things out. 00956: * 00957: * If there are applications that are active memory-allocators 00958: * (most normal use), this basically shouldn't matter. 00959: */ 00960: int kswapd(void *unused) 00961: { 00962: struct task_struct *tsk = current; 00963: 00964: tsk->session = 1; 00965: tsk->pgrp = 1; 00966: strcpy(tsk->comm, "kswapd"); 00967: sigfillset(&tsk->blocked); 00968: kswapd_task = tsk; 00969: 00970: /* 00971: * Tell the memory management that we're a "memory allocator", Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 91 页,共 1481 页 00972: * and that if we need more memory we should get access to it 00973: * regardless (see "__alloc_pages()"). "kswapd" should 00974: * never get caught in the normal page freeing logic. 00975: * 00976: * (Kswapd normally doesn't need memory anyway, but sometimes 00977: * you need a small amount of memory in order to be able to 00978: * page out something else, and this flag essentially protects 00979: * us from recursively trying to free more memory as we're 00980: * trying to free the first piece of memory in the first place). 00981: */ 00982: tsk->flags |= PF_MEMALLOC; 00983: 00984: /* 00985: * Kswapd main loop. 00986: */ 00987: for (;;) { 00988: static int recalc = 0; 00989: 00990: /* If needed, try to free some memory. */ 00991: if (inactive_shortage() || free_shortage()) { 00992: int wait = 0; 00993: /* Do we need to do some synchronous flushing? */ 00994: if (waitqueue_active(&kswapd_done)) 00995: wait = 1; 00996: do_try_to_free_pages(GFP_KSWAPD, wait); 00997: } 00998: 00999: /* 01000: * Do some (very minimal) background scanning. This 01001: * will scan all pages on the active list once 01002: * every minute. This clears old referenced bits 01003: * and moves unused pages to the inactive list. 01004: */ 01005: refill_inactive_scan(6, 0); 01006: 01007: /* Once a second, recalculate some VM stats. */ 01008: if (time_after(jiffies, recalc + HZ)) { 01009: recalc = jiffies; 01010: recalculate_vm_stats(); 01011: } 01012: 01013: /* 01014: * Wake up everybody waiting for free memory 01015: * and unplug the disk queue. 01016: */ Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 92 页,共 1481 页 01017: wake_up_all(&kswapd_done); 01018: run_task_queue(&tq_disk); 01019: 01020: /* 01021: * We go to sleep if either the free page shortage 01022: * or the inactive page shortage is gone. We do this 01023: * because: 01024: * 1) we need no more free pages or 01025: * 2) the inactive pages need to be flushed to disk, 01026: * it wouldn't help to eat CPU time now ... 01027: * 01028: * We go to sleep for one second, but if it's needed 01029: * we'll be woken up earlier... 01030: */ 01031: if (!free_shortage() || !inactive_shortage()) { 01032: interruptible_sleep_on_timeout(&kswapd_wait, HZ); 01033: /* 01034: * If we couldn't free enough memory, we see if it was 01035: * due to the system just not having enough memory. 01036: * If that is the case, the only solution is to kill 01037: * a process (the alternative is enternal deadlock). 01038: * 01039: * If there still is enough memory around, we just loop 01040: * and try free some more memory... 01041: */ 01042: } else if (out_of_memory()) { 01043: oom_kill(); 01044: } 01045: } 01046: } 在一些简单的初始化操作以后,程序便进入一个无限循环。在每次循环的末尾一般都会调用 interruptible_sleep_on_timeout()进入睡眠,让内核自由地调度别的进程运行。但是内核在一定时间以后又 会唤醒并调度 kswapd 继续运行,这时候 kswapd 就又回到这无限循环开始的地方。那么,这“一定时间” 是多长呢?这就是常数 HZ。HZ 决定了内核中每秒钟有多少次时钟中断。用户可以在编译内核前的系统 配置阶段改变其数值,但是已经编译就定下来了。所以在调用 interruptible_sleep_on_timeout()时的参数为 HZ,表示 1 秒钟以后又要调度 kswapd 继续运行。换言之,对 interruptible_sleep_on_timeout()的调用一进 去就得 1 秒钟以后才回来。但是在有些情况下内核也会在不到 1 秒钟时就把它唤醒,那样 kswapd 就会 提前返回而开始新的一轮循环。所以,这个循环至少每个 1 秒钟执行一遍,这就是 kswapd 的例行路线。 那么,kswapd 在这至少每秒一次的例行路线中做些什么呢?可以把它分成两部分。第一部分是在发 现物理页面已经短缺的情况下才进行的,目的在于预先找出若干页面,且将这些页面的映射断开,使这 些物理页面从活跃状态转入不活跃状态,为页面的换出作好准备。第二部分是每次都要执行的,目的在 于把已经出于不活跃状态的“脏”页面写入交换设备,使它们成为不活跃“干净”页面继续缓冲,或进 一步回收一些这样的页面成为空闲页面。 先看第一部分,首先检查内存中可供分配或周转的物理页面是否短缺: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 93 页,共 1481 页 [kswapd() > inactive_shortage()] 00805: /* 00806: * How many inactive pages are we short? 00807: */ 00808: int inactive_shortage(void) 00809: { 00810: int shortage = 0; 00811: 00812: shortage += freepages.high; 00813: shortage += inactive_target; 00814: shortage -= nr_free_pages(); 00815: shortage -= nr_inactive_clean_pages(); 00816: shortage -= nr_inactive_dirty_pages; 00817: 00818: if (shortage > 0) 00819: return shortage; 00820: 00821: return 0; 00822: } 系统中应该维持的物理页面供应量由两个全局量确定,那就是freepages.high和inactive_target,分别 为空闲页面的数量和不活跃页面的数量,二者之和为正常情况下潜在的供应量。而这些内存页面的来源 由三方面。一方面是当前尚存的空闲页面,这是立即就可以分配的页面。这些页面分散在各个页面管理 区中,并且合并成地址连续、大小为 2、4、8、…2N个页面的页面块,其数量由nr_free_pages()加以统计。 另一方面是现在的不活跃“干净”页面,这些页面本质上也是马上就可以分配的页面,但是页面中的内 容可能还会用到,所以多保留一些这样的页面有助于减少从交换设备的读入。这些页面页分散在各个页 面管理区中,但并不合并成块,其数量由nr_inactive_clean_pages()加以统计。最后是现在不活跃的“脏” 页面,这些页面要先加以“净化”,即写入交换设备以后才能投入分配。这种页面全都在同一个队列中, 内核中的全局量nr_inactive_dirty_pages记录着当前此类页面的数量。上述两个函数的代码都在 mm/page_alloc.c中,也都比较简单,读者可以自己阅读。 不过,光维持潜在的物理页面供应总量还不够,还要通过 free_shortage()检查是否有某个具体管理区 中有严重的短缺,即直接可供分配的页面数量(除不活跃“脏”页面以外)是否小于一个最低限度。这 个函数的代码在 mm/vmscan.c 中,我们也把它留给读者。 如果发现可供分配的内存页面短缺,那就要设法释放和换出若干页面,这是通过 do_try_to_free_pages()完成的。不过在此之前还要调用 waitqueue_active(),看 看 kswapd_done 队列中是否 有函数在等待执行,并把查看的结果作为参数传递给 do_try_to_free_pages()。在第 3 章中,读者将看到 内核中有几个特殊的队列,内核中各个部分(主要是设备驱动)可以把一些底层函数挂入这样的队列, 使得这些函数在某种事件发生时就能得到执行。而 kswapd_done,就是这样的一个队列。凡是挂入这个 队列的函数,在 kswapd 每完成一趟例行的操作时就能得到执行。这里的 inline 函数 waitqueue_active() 就是查看是否有函数在这个队列中等待执行。其定义在 include/linux/wait.h 中: [kswapd() > waitqueue_active()] 00152: static inline int waitqueue_active(wait_queue_head_t *q) 00153: { 00154: #if WAITQUEUE_DEBUG 00155: if (!q) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 94 页,共 1481 页 00156: WQ_BUG(); 00157: CHECK_MAGIC_WQHEAD(q); 00158: #endif 00159: 00160: return !list_empty(&q->task_list); 00161: } 下面就是调用 do_try_to_free_pages(),试图腾出一些内存页面。其代码在 vmscan.c 中: [kswapd() > do_try_to_free_pages()] 00907: static int do_try_to_free_pages(unsigned int gfp_mask, int user) 00908: { 00909: int ret = 0; 00910: 00911: /* 00912: * If we're low on free pages, move pages from the 00913: * inactive_dirty list to the inactive_clean list. 00914: * 00915: * Usually bdflush will have pre-cleaned the pages 00916: * before we get around to moving them to the other 00917: * list, so this is a relatively cheap operation. 00918: */ 00919: if (free_shortage() || nr_inactive_dirty_pages > nr_free_pages() + 00920: nr_inactive_clean_pages()) 00921: ret += page_launder(gfp_mask, user); 00922: 00923: /* 00924: * If needed, we move pages from the active list 00925: * to the inactive list. We also "eat" pages from 00926: * the inode and dentry cache whenever we do this. 00927: */ 00928: if (free_shortage() || inactive_shortage()) { 00929: shrink_dcache_memory(6, gfp_mask); 00930: shrink_icache_memory(6, gfp_mask); 00931: ret += refill_inactive(gfp_mask, user); 00932: } else { 00933: /* 00934: * Reclaim unused slab cache memory. 00935: */ 00936: kmem_cache_reap(gfp_mask); 00937: ret = 1; 00938: } 00939: 00940: return ret; 00941: } Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 95 页,共 1481 页 将活跃页面的映射断开,使之转入不活跃状态,甚至进而换出到交换设备上,是不得已而为之,因 为谁也不能精确地预测到底哪一些页面是合适的换出对象,虽然一般而言“最近最少用到”是个有效的 准则,但也并不是“放诸四海皆准”。所以,能够不动“现役”页面是最理想的。基于这样的考虑,这里 所作的是先易后难,逐步加强力度。首先是调用 page_launder(),试图把已经转入不活跃状态的“脏”页 面“洗净”,使它们变成立即可以分配的页面。函数名中的“launder”,就是“洗衣工”的意思。这个函 数一方面(基本上)定期地受到 kswapd()的调用,一方面在每当需要分配内存页面,而又无页面可供分 配时,临时地受到调用。其代码在 mm/vmscan.c 中: [kswapd() > do_try_to_free_pages() > page_launder()] 00465: /** 00466: * page_launder - clean dirty inactive pages, move to inactive_clean list 00467: * @gfp_mask: what operations we are allowed to do 00468: * @sync: should we wait synchronously for the cleaning of pages 00469: * 00470: * When this function is called, we are most likely low on free + 00471: * inactive_clean pages. Since we want to refill those pages as 00472: * soon as possible, we'll make two loops over the inactive list, 00473: * one to move the already cleaned pages to the inactive_clean lists 00474: * and one to (often asynchronously) clean the dirty inactive pages. 00475: * 00476: * In situations where kswapd cannot keep up, user processes will 00477: * end up calling this function. Since the user process needs to 00478: * have a page before it can continue with its allocation, we'll 00479: * do synchronous page flushing in that case. 00480: * 00481: * This code is heavily inspired by the FreeBSD source code. Thanks 00482: * go out to Matthew Dillon. 00483: */ 00484: #define MAX_LAUNDER (4 * (1 << page_cluster)) 00485: int page_launder(int gfp_mask, int sync) 00486: { 00487: int launder_loop, maxscan, cleaned_pages, maxlaunder; 00488: int can_get_io_locks; 00489: struct list_head * page_lru; 00490: struct page * page; 00491: 00492: /* 00493: * We can only grab the IO locks (eg. for flushing dirty 00494: * buffers to disk) if __GFP_IO is set. 00495: */ 00496: can_get_io_locks = gfp_mask & __GFP_IO; 00497: 00498: launder_loop = 0; 00499: maxlaunder = 0; 00500: cleaned_pages = 0; 00501: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 96 页,共 1481 页 00502: dirty_page_rescan: 00503: spin_lock(&pagemap_lru_lock); 00504: maxscan = nr_inactive_dirty_pages; 00505: while ((page_lru = inactive_dirty_list.prev) != &inactive_dirty_list && 00506: maxscan-- > 0) { 00507: page = list_entry(page_lru, struct page, lru); 00508: 00509: /* Wrong page on list?! (list corruption, should not happen) */ 00510: if (!PageInactiveDirty(page)) { 00511: printk("VM: page_launder, wrong page on list.\n"); 00512: list_del(page_lru); 00513: nr_inactive_dirty_pages--; 00514: page->zone->inactive_dirty_pages--; 00515: continue; 00516: } 00517: 00518: /* Page is or was in use? Move it to the active list. */ 00519: if (PageTestandClearReferenced(page) || page->age > 0 || 00520: (!page->buffers && page_count(page) > 1) || 00521: page_ramdisk(page)) { 00522: del_page_from_inactive_dirty_list(page); 00523: add_page_to_active_list(page); 00524: continue; 00525: } 00526: 00527: /* 00528: * The page is locked. IO in progress? 00529: * Move it to the back of the list. 00530: */ 00531: if (TryLockPage(page)) { 00532: list_del(page_lru); 00533: list_add(page_lru, &inactive_dirty_list); 00534: continue; 00535: } 00536: 00537: /* 00538: * Dirty swap-cache page? Write it out if 00539: * last copy.. 00540: */ 00541: if (PageDirty(page)) { 00542: int (*writepage)(struct page *) = page->mapping->a_ops->writepage; 00543: int result; 00544: 00545: if (!writepage) 00546: goto page_active; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 97 页,共 1481 页 00547: 00548: /* First time through? Move it to the back of the list */ 00549: if (!launder_loop) { 00550: list_del(page_lru); 00551: list_add(page_lru, &inactive_dirty_list); 00552: UnlockPage(page); 00553: continue; 00554: } 00555: 00556: /* OK, do a physical asynchronous write to swap. */ 00557: ClearPageDirty(page); 00558: page_cache_get(page); 00559: spin_unlock(&pagemap_lru_lock); 00560: 00561: result = writepage(page); 00562: page_cache_release(page); 00563: 00564: /* And re-start the thing.. */ 00565: spin_lock(&pagemap_lru_lock); 00566: if (result != 1) 00567: continue; 00568: /* writepage refused to do anything */ 00569: set_page_dirty(page); 00570: goto page_active; 00571: } 00572: 00573: /* 00574: * If the page has buffers, try to free the buffer mappings 00575: * associated with this page. If we succeed we either free 00576: * the page (in case it was a buffercache only page) or we 00577: * move the page to the inactive_clean list. 00578: * 00579: * On the first round, we should free all previously cleaned 00580: * buffer pages 00581: */ 00582: if (page->buffers) { 00583: int wait, clearedbuf; 00584: int freed_page = 0; 00585: /* 00586: * Since we might be doing disk IO, we have to 00587: * drop the spinlock and take an extra reference 00588: * on the page so it doesn't go away from under us. 00589: */ 00590: del_page_from_inactive_dirty_list(page); 00591: page_cache_get(page); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 98 页,共 1481 页 00592: spin_unlock(&pagemap_lru_lock); 00593: 00594: /* Will we do (asynchronous) IO? */ 00595: if (launder_loop && maxlaunder == 0 && sync) 00596: wait = 2; /* Synchrounous IO */ 00597: else if (launder_loop && maxlaunder-- > 0) 00598: wait = 1; /* Async IO */ 00599: else 00600: wait = 0; /* No IO */ 00601: 00602: /* Try to free the page buffers. */ 00603: clearedbuf = try_to_free_buffers(page, wait); 00604: 00605: /* 00606: * Re-take the spinlock. Note that we cannot 00607: * unlock the page yet since we're still 00608: * accessing the page_struct here... 00609: */ 00610: spin_lock(&pagemap_lru_lock); 00611: 00612: /* The buffers were not freed. */ 00613: if (!clearedbuf) { 00614: add_page_to_inactive_dirty_list(page); 00615: 00616: /* The page was only in the buffer cache. */ 00617: } else if (!page->mapping) { 00618: atomic_dec(&buffermem_pages); 00619: freed_page = 1; 00620: cleaned_pages++; 00621: 00622: /* The page has more users besides the cache and us. */ 00623: } else if (page_count(page) > 2) { 00624: add_page_to_active_list(page); 00625: 00626: /* OK, we "created" a freeable page. */ 00627: } else /* page->mapping && page_count(page) == 2 */ { 00628: add_page_to_inactive_clean_list(page); 00629: cleaned_pages++; 00630: } 00631: 00632: /* 00633: * Unlock the page and drop the extra reference. 00634: * We can only do it here because we ar accessing 00635: * the page struct above. 00636: */ Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 99 页,共 1481 页 00637: UnlockPage(page); 00638: page_cache_release(page); 00639: 00640: /* 00641: * If we're freeing buffer cache pages, stop when 00642: * we've got enough free memory. 00643: */ 00644: if (freed_page && !free_shortage()) 00645: break; 00646: continue; 00647: } else if (page->mapping && !PageDirty(page)) { 00648: /* 00649: * If a page had an extra reference in 00650: * deactivate_page(), we will find it here. 00651: * Now the page is really freeable, so we 00652: * move it to the inactive_clean list. 00653: */ 00654: del_page_from_inactive_dirty_list(page); 00655: add_page_to_inactive_clean_list(page); 00656: UnlockPage(page); 00657: cleaned_pages++; 00658: } else { 00659: page_active: 00660: /* 00661: * OK, we don't know what to do with the page. 00662: * It's no use keeping it here, so we move it to 00663: * the active list. 00664: */ 00665: del_page_from_inactive_dirty_list(page); 00666: add_page_to_active_list(page); 00667: UnlockPage(page); 00668: } 00669: } 00670: spin_unlock(&pagemap_lru_lock); 代码中的局部变量 cleaned_pages 用来累计被“清洗”的页面数量。另一个局部变量 launder_loop 用 来控制扫描不活跃“脏”页面队列的次数。在第一趟扫描时 launder_loop 为 0,如果有必要进行第二趟 扫描,则将其设成 1 并转回到标号 dirty_page_rescan 处(502 行),开始又一次扫描。 对不活跃“脏”页面队列的扫描是通过一个 while 循环(505 行)进行的。由于在循环中会把有些页 面从当前位置移到队列的尾部,所以除沿着链接指针扫描外还要对数量加以控制,才能避免重复处理同 一个页面,甚至陷入死循环,这就是变量 maxscan 的作用。 对于队列中的每一个页面,首先要检查它的 PG_inactive_dirty 标志位为 1,否则就根本不因该出现 在这个队列中,这一定是出了什么毛病,所以把它从队列中删除(见 512 行)。除此之外,对于正常的不 活跃“脏”页面,则要依次作下述的检查并作相应的处理。 1. 有些页面虽然已经进入不活跃“脏”页面队列,但是由于情况已经变化,或者当初进入这个队 列本来就是“冤假错案”,因而需要回到活跃页面队列中(519~525 行)。这样的页面有: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 100 页,共 1481 页 页面在进入了不活跃“脏”页面队列中后又受到了访问,即发生了以此页面为目标的缺页 异常,从而恢复了该页面的映射。 页面的“寿命”还未耗尽。页面的 page 结构中有个字段 age,其数值与页面受访问的频繁 程度有关。后面我们还要回到这个话题。 页面并不用作读/写文件的缓冲,而页面的使用计数却又大于 1。这说明页面在至少一个进 程的映射表中有映射。如前所述,一个页面的使用计数在分配时设成 1,以后对该页面的 每一次使用都使这个计数加 1,包括将页面用作读/写文件的缓冲。如果一个页面没有用作 读/写文件的缓冲区,那么只要计数大于 1 就必定还有进程在使用这个页面。 页面在受到进程用户空间映射的同时又用于 ramdisk,即用内存空间来模拟磁盘,这种页面 当然不应该换出到磁盘上。 2. 页面已经被锁住(531 行),所以 TryLockPage()返回 1,这表明正在对此页面进行操作,如输入 /输出,这样的页面应该留在不活跃“脏”页面队列中,但是把它移到队列的尾部。注意,对于 未被锁住的页面,现在已经锁上了。 3. 如果页面仍是“脏”的(541 行),即 page 结构中的 PG_dirty 标志位为 1,则原则上要将其写 出到交换设备上,但还有些特殊情况要考虑(541~571 行)。首先,所属的 address_space 数据 结构必须提供页面写出操作的函数,否则就只好转到 page_active 处,将页面送回活跃页面队列 中。对于一般的页面交换,所属的 address_space 数据结构为 swapper_space ,其 address_space_operations 结构为 swap_aops,所提供的页面写出操作为 swap_writepage(),过这 一“关”是没有问题的。在第一趟扫描中,只是把页面移到同一队列的尾部,而并不写出页面 (531~535 行)。如果进行第二趟扫描的话,那就真的要把页面写出去了。写之前通过 ClearPageDirty()把页面的 PG_dirty 标志位清成 0,然后通过由所属 address_space 数据结构所提 供的函数把页面写出去。根据页面的不同使用目的,例如普通的用户空间,或者通过 mmap() 建立的文件映射以及文件系统的读/写缓冲,具体的操作也不一样。这个写操作可能是同步的(当 前进程睡眠,等待写出完成),也可能是异步的,但总是需要一定的时间才能完成,在此期间内 核可能再次进入page_launder(),所以需要防止把这个页面再写出一次。这就是把页面的PG_dirty 标志位清成 0 的目的。这样就不会把同一个页面写出两次了(见 541 行)。此外,还要考虑页面 写出失败的可能,具体的函数在写出失败时应该返回 1,使 page_launder()可以恢复页面的 PG_dirty 标志位并将其退还给活页页面队列中(569~570 行)。顺便提一下,这里在调用具体 的 writepage 函数时先通过 page_cache_get()递增页面的使用计数,从这个函数返回后再通过 page_cache_release()递减这个计数,表示在把页面写出的期间多了一个“用户”。注意这里并没 有立即把写出的页面转移到不活跃“干净”页面队列中,而只是把它的 PG_dirty 标志位清成了 0。还要注意,如果 CPU 到达了代码中的 582 行,则页面的 PG_dirty 标志位必定为 0,这个页 面一定是在以前的扫描中写出而变“干净”的。 4. 如果页面不再是“脏”的,并且又是用作文件读/写缓冲的页面(582~647 行),则先使它脱离 不活跃“脏”页面队列,再通过 try_to_free_buffers()试图将页面释放。如果不能释放则根据返 回值将其退回不活跃“脏”页面队列,或者链入活跃页面队列,或者不活跃“干净”页面队列。 如果释放成功,则页面的使用计数已经在 try_to_free_buffers() 中减 1 , 638 行的 page_cache_release()再使其减 1 就达到了 0,从而最终将页面释放回到空闲页面队列中。如果成 功地释放了一个页面,并且发现系统中的空闲页面已经不再短缺,那么扫描就可以结束了(见 644 和 645 行)。否则继续扫描。函数 try_to_free_buffers()的代码在 fs/buffer.c 中,读者可以在学 习了“文件系统”一章以后自行阅读。 5. 如果页面不再是“脏”的,并且在某个 address_space 数据结构的队列中,这就是已经“洗清” 了的页面,所以把它转移到所属区间的不活跃“干净”页面队列中。 6. 最后,如果不属于上述任何一种情况(658 行),那就是无法处理的页面,所以把它退回活跃页 面队列中。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 101 页,共 1481 页 完成了一趟扫描以后,还要根据系统中空闲页面是否短缺、以及调用参数 gfp_mask 中的__GFP_IO 标志位是否为 1,来决定是否进行第二趟扫描。 [kswapd() > do_try_to_free_pages() > page_launder()] 00671: 00672: /* 00673: * If we don't have enough free pages, we loop back once 00674: * to queue the dirty pages for writeout. When we were called 00675: * by a user process (that /needs/ a free page) and we didn't 00676: * free anything yet, we wait synchronously on the writeout of 00677: * MAX_SYNC_LAUNDER pages. 00678: * 00679: * We also wake up bdflush, since bdflush should, under most 00680: * loads, flush out the dirty pages before we have to wait on 00681: * IO. 00682: */ 00683: if (can_get_io_locks && !launder_loop && free_shortage()) { 00684: launder_loop = 1; 00685: /* If we cleaned pages, never do synchronous IO. */ 00686: if (cleaned_pages) 00687: sync = 0; 00688: /* We only do a few "out of order" flushes. */ 00689: maxlaunder = MAX_LAUNDER; 00690: /* Kflushd takes care of the rest. */ 00691: wakeup_bdflush(0); 00692: goto dirty_page_rescan; 00693: } 00694: 00695: /* Return the number of pages moved to the inactive_clean list. */ 00696: return cleaned_pages; 00697: } 如果决定进行第二趟扫描,就转回到 502 行标号 dirty_page_rescan 处。注意这里把 launder_loop 设 成了 1,以后就不可能再回过去又扫描一次了。所以每次调用 page_launder()最多是作两趟扫描。 回到 do_try_to_free_pages()的代码中,经过 page_launder()以后,如果可分配的物理页面数量仍然不 足,那就要进一步设法回收页面了。不过,也并不是单纯地从各个进程的用户空间所映射的物理页面中 回收,而是从四个方面回收,这就是这里所调用三个函数(shrink_dcache_memory() 、 shrink_icache_memory()、refill_inactive()),以及等一下将会看到的 kmem_cache_reap()的意图。在“文件 系统”一章中,读者将会看到,在打开文件的过程中要分配和使用代表着目录项的 dentry 数据结构,还 有代表着文件索引节点的 inode 数据结构。这些数据结构在文件关闭以后并不立即释放,而是放在 LRU 队列中作为后备,以防在不久将来的文件操作中又要用到。这样,经过一段时间以后,就有可能积累起 大量的 dentry 数据结构和 inode 数据结构,占用数量可观的物理页面。这时,就要通过 shrink_dcache_memory()和 shrink_icache_memory()适当加以回收,以维持这些数据结构与物理页面间的 “生态平衡”。另一方面,除此以外,内核在运行中也需要动态地分配使用很多数据结构,内核中对此采 用了一种称为“slab”的管理机制。以后读者会看到,这种机制就好像是向存储管理“批发”物理页面, 然后切割成小块“零售”。随着系统的运行,对这种物理页面的实际需求也在动态地变化。但是 slab 管Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 102 页,共 1481 页 理机制也是倾向于分配和保持更多的空闲物理页面,而不热衷于退还这些页面,所以过一段时间就要通 过 kmem_cache_reap()来“收割”。读者可以在学习了“文件系统”后回过来自己阅读前两个函数的代码, 我们在这里则集中关注 refill_inactive(),其代码在 mm/vmscan.c 中: [kswapd() > do_try_to_free_pages() > refill_inactive()] 00824: /* 00825: * We need to make the locks finer granularity, but right 00826: * now we need this so that we can do page allocations 00827: * without holding the kernel lock etc. 00828: * 00829: * We want to try to free "count" pages, and we want to 00830: * cluster them so that we get good swap-out behaviour. 00831: * 00832: * OTOH, if we're a user process (and not kswapd), we 00833: * really care about latency. In that case we don't try 00834: * to free too many pages. 00835: */ 00836: static int refill_inactive(unsigned int gfp_mask, int user) 00837: { 00838: int priority, count, start_count, made_progress; 00839: 00840: count = inactive_shortage() + free_shortage(); 00841: if (user) 00842: count = (1 << page_cluster); 00843: start_count = count; 00844: 00845: /* Always trim SLAB caches when memory gets low. */ 00846: kmem_cache_reap(gfp_mask); 00847: 00848: priority = 6; 00849: do { 00850: made_progress = 0; 00851: 00852: if (current->need_resched) { 00853: __set_current_state(TASK_RUNNING); 00854: schedule(); 00855: } 00856: 00857: while (refill_inactive_scan(priority, 1)) { 00858: made_progress = 1; 00859: if (--count <= 0) 00860: goto done; 00861: } 00862: 00863: /* 00864: * don't be too light against the d/i cache since Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 103 页,共 1481 页 00865: * refill_inactive() almost never fail when there's 00866: * really plenty of memory free. 00867: */ 00868: shrink_dcache_memory(priority, gfp_mask); 00869: shrink_icache_memory(priority, gfp_mask); 00870: 00871: /* 00872: * Then, try to page stuff out.. 00873: */ 00874: while (swap_out(priority, gfp_mask)) { 00875: made_progress = 1; 00876: if (--count <= 0) 00877: goto done; 00878: } 00879: 00880: /* 00881: * If we either have enough free memory, or if 00882: * page_launder() will be able to make enough 00883: * free memory, then stop. 00884: */ 00885: if (!inactive_shortage() || !free_shortage()) 00886: goto done; 00887: 00888: /* 00889: * Only switch to a lower "priority" if we 00890: * didn't make any useful progress in the 00891: * last loop. 00892: */ 00893: if (!made_progress) 00894: priority--; 00895: } while (priority >= 0); 00896: 00897: /* Always end on a refill_inactive.., may sleep... */ 00898: while (refill_inactive_scan(0, 1)) { 00899: if (--count <= 0) 00900: goto done; 00901: } 00902: 00903: done: 00904: return (count < start_count); 00905: } 参数 user 是从 kswapd 传下来的,表示是否有函数在 kswapd_done 队列中等待执行,这个因素决定 回收物理页面的过程是否可以慢慢来,所以对本次要回收的页面数量有影响。 首先通过 kmem_cache_reap()“收割”由 slab 机制管理的空闲物理页面,相对而言这是动作最小的, 读者可以在学习了“内核缓冲区的管理”一节以后自行阅读这个函数的代码。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 104 页,共 1481 页 然后,就是一个 do-while 循环。循环从优先级最低的 6 级开始,逐步加大“力度”直到 0 级,结果 或者达到了目标,回收的数量够了;或者在最高优先级时还是达不到目标,那也只好算了(到缺页异常 真的发生时情况也许有了改变)。 在循环中,每次开头都要检查一下当前进程的 task_struct 结构中的 need_resched 是否为 1。如果是, 就说明某个中断服务程序要求调度,所以调用 schedule()让内核进行一次调度,但是在此之前把本进程的 状态设置成 TASK_RUNNING,表达要继续运行的意愿。读者在第 4 章中将会看到,task_struct 结构中的 need_resched 是为强制调度而设置的,每当 CPU 结束了一次系统调用或中断服务、从系统空间返回用户 空间时就会检查这个标志。可是,kswapd 是个内核线程,永远不会“返回用户空间”,这样就有可能绕 过这个机制而占住 CPU 不放,所以只能靠它“自律”,自己在可能需要较长时间的操作之前检查这个标 志并调用 schedule()。 那么,在循环中做些什么呢?主要是两件事。一件是通过 refill_inactive_scan()扫描活跃页面队列, 试图从中找到可以转入不活跃状态的页面;另一件是通过 swap_out()找出一个进程,然后扫描其映射表, 从中找到可以转入不活跃状态的页面。此外,还要再试试用于 dentry 结构和 inode 结构的页面。先看 refill_inactive_scan()的代码,这个函数在 mm/vmscan.c 中: 00699: /** 00700: * refill_inactive_scan - scan the active list and find pages to deactivate 00701: * @priority: the priority at which to scan 00702: * @oneshot: exit after deactivating one page 00703: * 00704: * This function will scan a portion of the active list to find 00705: * unused pages, those pages will then be moved to the inactive list. 00706: */ 00707: int refill_inactive_scan(unsigned int priority, int oneshot) 00708: { 00709: struct list_head * page_lru; 00710: struct page * page; 00711: int maxscan, page_active = 0; 00712: int ret = 0; 00713: 00714: /* Take the lock while messing with the list... */ 00715: spin_lock(&pagemap_lru_lock); 00716: maxscan = nr_active_pages >> priority; 00717: while (maxscan-- > 0 && (page_lru = active_list.prev) != &active_list) { 00718: page = list_entry(page_lru, struct page, lru); 00719: 00720: /* Wrong page on list?! (list corruption, should not happen) */ 00721: if (!PageActive(page)) { 00722: printk("VM: refill_inactive, wrong page on list.\n"); 00723: list_del(page_lru); 00724: nr_active_pages--; 00725: continue; 00726: } 00727: 00728: /* Do aging on the pages. */ 00729: if (PageTestandClearReferenced(page)) { Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 105 页,共 1481 页 00730: age_page_up_nolock(page); 00731: page_active = 1; 00732: } else { 00733: age_page_down_ageonly(page); 00734: /* 00735: * Since we don't hold a reference on the page 00736: * ourselves, we have to do our test a bit more 00737: * strict then deactivate_page(). This is needed 00738: * since otherwise the system could hang shuffling 00739: * unfreeable pages from the active list to the 00740: * inactive_dirty list and back again... 00741: * 00742: * SUBTLE: we can have buffer pages with count 1. 00743: */ 00744: if (page->age == 0 && page_count(page) <= 00745: (page->buffers ? 2 : 1)) { 00746: deactivate_page_nolock(page); 00747: page_active = 0; 00748: } else { 00749: page_active = 1; 00750: } 00751: } 00752: /* 00753: * If the page is still on the active list, move it 00754: * to the other end of the list. Otherwise it was 00755: * deactivated by age_page_down and we exit successfully. 00756: */ 00757: if (page_active || PageActive(page)) { 00758: list_del(page_lru); 00759: list_add(page_lru, &active_list); 00760: } else { 00761: ret = 1; 00762: if (oneshot) 00763: break; 00764: } 00765: } 00766: spin_unlock(&pagemap_lru_lock); 00767: 00768: return ret; 00769: } 就像对“脏”页面队列的扫描一样,这里也通过一个局部量 maxscan 来控制扫描的页面数量,不过 这里扫描的不一定是整个活跃页面队列,而是根据调用参数 priority 的值扫描其中一部分,只有在 priority 为 0 时才扫描整个队列(见 716 行)。对于所扫描的页面,首先也要验证确实属于活跃页面(见 721 行)。 然后,根据页面是否受到了访问(见 729 行),决定增加或减少页面的寿命。不过,光是耗尽了寿命还不 足以把页面从活跃状态转入不活跃状态,还得要看是否还有用户空间映射。如果页面并不用作文件系统Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 106 页,共 1481 页 的读/写缓冲,那么只要页面的使用计数大于 1 就说明还有用户空间映射,还不能转入不活跃状态(见 744 行),这样的页面在通过 swap_out()扫描相应进程的映射表时才能转入不活跃状态,对于还不能转入 不活跃状态的页面,要将其从队列中的当前位置移到队列的尾部。反之,如果成功地将一个页面转入了 不活跃状态,则根据参数 oneshot 的值决定是否继续扫描。一般来说,在活跃页面队列中的页面使用计 数都大于 1。而当 swap_out()断开一个页面的映射而使其转入不活跃状态时,则已经将页面转入不活跃页 面队列,因而不在这个队列中了。可是,就如代码中的注释所言,确实存在着特殊的情况,在“页面的 换入”中就可以看到。 再看 swap_out(),那是在 mm/vmscan.c 中定义的: [kswapd() > do_try_to_free_pages() > refill_inactive() > swap_out()] 00297: /* 00298: * Select the task with maximal swap_cnt and try to swap out a page. 00299: * N.B. This function returns only 0 or 1. Return values != 1 from 00300: * the lower level routines result in continued processing. 00301: */ 00302: #define SWAP_SHIFT 5 00303: #define SWAP_MIN 8 00304: 00305: static int swap_out(unsigned int priority, int gfp_mask) 00306: { 00307: int counter; 00308: int __ret = 0; 00309: 00310: /* 00311: * We make one or two passes through the task list, indexed by 00312: * assign = {0, 1}: 00313: * Pass 1: select the swappable task with maximal RSS that has 00314: * not yet been swapped out. 00315: * Pass 2: re-assign rss swap_cnt values, then select as above. 00316: * 00317: * With this approach, there's no need to remember the last task 00318: * swapped out. If the swap-out fails, we clear swap_cnt so the 00319: * task won't be selected again until all others have been tried. 00320: * 00321: * Think of swap_cnt as a "shadow rss" - it tells us which process 00322: * we want to page out (always try largest first). 00323: */ 00324: counter = (nr_threads << SWAP_SHIFT) >> priority; 00325: if (counter < 1) 00326: counter = 1; 00327: 00328: for (; counter >= 0; counter--) { 00329: struct list_head *p; 00330: unsigned long max_cnt = 0; 00331: struct mm_struct *best = NULL; 00332: int assign = 0; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 107 页,共 1481 页 00333: int found_task = 0; 00334: select: 00335: spin_lock(&mmlist_lock); 00336: p = init_mm.mmlist.next; 00337: for (; p != &init_mm.mmlist; p = p->next) { 00338: struct mm_struct *mm = list_entry(p, struct mm_struct, mmlist); 00339: if (mm->rss <= 0) 00340: continue; 00341: found_task++; 00342: /* Refresh swap_cnt? */ 00343: if (assign == 1) { 00344: mm->swap_cnt = (mm->rss >> SWAP_SHIFT); 00345: if (mm->swap_cnt < SWAP_MIN) 00346: mm->swap_cnt = SWAP_MIN; 00347: } 00348: if (mm->swap_cnt > max_cnt) { 00349: max_cnt = mm->swap_cnt; 00350: best = mm; 00351: } 00352: } 00353: 00354: /* Make sure it doesn't disappear */ 00355: if (best) 00356: atomic_inc(&best->mm_users); 00357: spin_unlock(&mmlist_lock); 00358: 00359: /* 00360: * We have dropped the tasklist_lock, but we 00361: * know that "mm" still exists: we are running 00362: * with the big kernel lock, and exit_mm() 00363: * cannot race with us. 00364: */ 00365: if (!best) { 00366: if (!assign && found_task > 0) { 00367: assign = 1; 00368: goto select; 00369: } 00370: break; 00371: } else { 00372: __ret = swap_out_mm(best, gfp_mask); 00373: mmput(best); 00374: break; 00375: } 00376: } 00377: return __ret; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 108 页,共 1481 页 00378: } 这个函数的主体是一个 for 循环,循环的次数取决于 counter,而 counter 又是根据内核中进程(包括 线程)的个数和调用 swap_out()时优先级(最初为 6 级,逐次上升至 0 级)计算而得的。当优先级为 0 时,counter 就等于(nr_threads<swap_cnt 为最大的进程。每个 mm_struct 结构中的这个数值, 是在把所有进程的页面资源都处理了一遍,从而每个 mm_struct 结构中的这个数值都变成了 0 的时候设 置好了的,反映了当时该进程占用内存页面的数量 mm->rss。这就好像一次“人口普查”。随后,每次 考察和处理了这个进程的一个页面,就将其 mm->swap_cnt 减 1,直至最后变成 0。所以,mm->rss 反 映了一个进程占用的内存页面数量。只要在这一轮中至少还有一个进程的页面尚未受到考察,就一定能 找到一个“最佳对象”。一直到所有进程的 mm->swap_cnt 都变成 0,从而扫描下来竟找不到一个“best” 时(439~444 行),再把这里的局部量 assign 置成 1,再扫描一遍。这一次讲每个进程当前的 mm->rss 拷贝到 mm->swap_cnt 中,然后再从最富有的进程开始。但是,所谓尚未受到考察的页面数量并不包括 最近一次“人口普查”以后因页面异常而换入(或恢复映射)的页面,这些页面的数量要到下一次“人 口普查”以后才会反映出来。就每个进程的角度而言,对内存页面的占用存在着两个方向上的运动:一 个方向时因页面异常而有更多的页面建立起或恢复起映射;另一个方向则是周期性地受到 swap_out()的 考察而被切断若干页面的映射。这两个运动的结合决定了一个进程在特定时间内对内存页面的占用。 找到一个“最佳对象”best 以后,就要一次考察该进程的映射表,将符合条件的页面换出去。 页面的换出具体是由 swap_out_mm()来完成的。当 swap_out_mm()成功地换出一个页面时返回 1,否 则返回 0,返回负数则为异常。在操作之前先通过 356 行的 atomic_inc()递增 mm_struct 结构中的使用计 数 mm_users,待完成以后再由 373 行的 mmput()将其还原,使这个数据结构在操作的期间多了一个用户, 从而不会在中途被释放。 函数 swap_out_mm()的代码也在 vmscan.c 中: [kswapd() > do_try_to_free_pages() > refill_inactive() > swap_out() > swap_out_mm()] 00257: static int swap_out_mm(struct mm_struct * mm, int gfp_mask) 00258: { 00259: int result = 0; 00260: unsigned long address; 00261: struct vm_area_struct* vma; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 109 页,共 1481 页 00262: 00263: /* 00264: * Go through process' page directory. 00265: */ 00266: 00267: /* 00268: * Find the proper vm-area after freezing the vma chain 00269: * and ptes. 00270: */ 00271: spin_lock(&mm->page_table_lock); 00272: address = mm->swap_address; 00273: vma = find_vma(mm, address); 00274: if (vma) { 00275: if (address < vma->vm_start) 00276: address = vma->vm_start; 00277: 00278: for (;;) { 00279: result = swap_out_vma(mm, vma, address, gfp_mask); 00280: if (result) 00281: goto out_unlock; 00282: vma = vma->vm_next; 00283: if (!vma) 00284: break; 00285: address = vma->vm_start; 00286: } 00287: } 00288: /* Reset to 0 when we reach the end of address space */ 00289: mm->swap_address = 0; 00290: mm->swap_cnt = 0; 00291: 00292: out_unlock: 00293: spin_unlock(&mm->page_table_lock); 00294: return result; 00295: } 首先,mm->swap_address 表示在执行的过程中要接着考察的页面地址。最初时该地址为 0,到所有 的页面都已经考察了一遍的时候就又清成 0(见 289 行)。程序在一个 for 循环中根据当前的这个地址找 到其所在的虚存区域。就这样一层一层地往下调用,经过 swap_out_vma()、 swap_out_pgd()、 swap_out_pmd(),一直到 try_to_swap_out(),试图换出由一个页面表项 pte 所指向的内存页面。中间这几 个函数都在同一个文件中,读者可以自行阅读。这里我们直接来看 try_to_swap_out(),因为这是关键所 在。下面,我们一步一步来看它的各个片断: [kswapd() > do_try_to_free_pages() > refill_inactive() > swap_out() > swap_out_mm() > swap_out_vma() > swap_out_pgd() > swap_out_pmd() > try_to_swap_out()] 00027: /* 00028: * The swap-out functions return 1 if they successfully Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 110 页,共 1481 页 00029: * threw something out, and we got a free page. It returns 00030: * zero if it couldn't do anything, and any other value 00031: * indicates it decreased rss, but the page was shared. 00032: * 00033: * NOTE! If it sleeps, it *must* return 1 to make sure we 00034: * don't continue with the swap-out. Otherwise we may be 00035: * using a process that no longer actually exists (it might 00036: * have died while we slept). 00037: */ 00038: static int try_to_swap_out(struct mm_struct * mm, struct vm_area_struct* vma, unsigned long address, pte_t * page_table, int gfp_mask) 00039: { 00040: pte_t pte; 00041: swp_entry_t entry; 00042: struct page * page; 00043: int onlist; 00044: 00045: pte = *page_table; 00046: if (!pte_present(pte)) 00047: goto out_failed; 00048: page = pte_page(pte); 00049: if ((!VALID_PAGE(page)) || PageReserved(page)) 00050: goto out_failed; 00051: 00052: if (!mm->swap_cnt) 00053: return 1; 00054: 00055: mm->swap_cnt--; 00056: 首先要说明,参数 page_table 实际上指向一个页面表项、而不是页面表,参数名 page_table 有些误 导。把这个表项的内容赋给变量 pte 以后,就通过 pte_present()来测试该表项所指的物理页面是否在内存 中,如果不在内存中就转向 out_failed,本次操作就失败了: 00106: out_failed: 00107: return 0; 当 try_to_swap_out()返回 0 时,其上一层的程序就会跳过这个页面,而试着换出同一页面表中映射 的下一个页面。如果一个页面已经穷尽,就再往上退一层试下一个页面表。 反之,如果物理页面确在内存中,就通过 ptr_page()将页面表项的内容换算成指向该物理内存页面的 page 结构的指针。由于所有的 page 结构都在 mem_map 数组中,所以(page – mem_map)就是该页面的 序号(数组中的下标)。要是这个序号大于最大的物理内存页面序号 max_mapnr,那就不是一个有效的 物理页面,这种情况下通常是因为物理页面的外部设备(例如网络接口卡)上,所以也跳过这一项。 00118: #define VALID_PAGE(page) ((page - mem_map) < max_mapnr) 此外,对于保留在内存中不允许换出的物理页面也要跳过。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 111 页,共 1481 页 跳过了这两种特殊情况,就要具体地考察一个页面了,所以将 mm->swap_cnt 减 1。继续往下看 try_to_swap_out()的代码: [kswapd() > do_try_to_free_pages() > refill_inactive() > swap_out() > swap_out_mm() > swap_out_vma() > swap_out_pgd() > swap_out_pmd() > try_to_swap_out()] 00057: onlist = PageActive(page); 00058: /* Don't look at this pte if it's been accessed recently. */ 00059: if (ptep_test_and_clear_young(page_table)) { 00060: age_page_up(page); 00061: goto out_failed; 00062: } 00063: if (!onlist) 00064: /* The page is still mapped, so it can't be freeable... */ 00065: age_page_down_ageonly(page); 00066: 00067: /* 00068: * If the page is in active use by us, or if the page 00069: * is in active use by others, don't unmap it or 00070: * (worse) start unneeded IO. 00071: */ 00072: if (page->age > 0) 00073: goto out_failed; 00074: 内存页面的 page 结构中,字段 flags 中的各种标志位反映着页面的当前状态,其中的 PG_active 标志 位表示当前这个页面是否“活跃”,即是否仍在 active_list 队列中: 00230: #define PageActive(page) test_bit(PG_active, &(page)->flags) 一个可交换的物理页面一定在某个 LRU 队列中,不在 active_list 队列中就说明一定在 inactive_dirty_list 中或某个 inactive_clean_list 中,等一下就要使用测试的结果。 一个映射中的物理页面是否应该换出,取决于这个页面最近是否受到了访问。这是通过 inline 函数 ptep_test_and_clear_young()测试(并清 0)的,其定义在 include/asm-i386/pgtable.h 中: 00285: static inline int ptep_test_and_clear_young(pte_t *ptep) \ { return test_and_clear_bit(_PAGE_BIT_ACCESSED, ptep); } 如前所述,页面表项中有个_PAGE_ACCESSED 标志位。当 i386 CPU 的内存映射机制在通过一个页 面表项将一个虚拟地址映射成一个物理地址,进而访问这个物理地址时,就会自动将该表项的 _PAGE_ACCESSED 标志位设成 1。所以,如果 pte_young()返回 1,就表示从上一次对同一个页面表项 调用 try_to_swap_out()至今,该页面至少已经被访问过一次,所以说页面还“年轻”。一般而言,最近受 到访问就预示着在不久的将来也会受到访问,所以不宜将其换出。取得了此项信息以后,就将页面表项 中的_PAGE_ACCESSED 标志位清成 0,再把它写回页面表项,为下一次再来测试这个标志位做好准备。 如果页面还“年轻”,那就肯定不是要加以还出的对象,所以也要转到 out_failed。不过,在转到 out_failed 之前还要作一点事情:如果页面还活跃,就要通过 SetPageReference()将 page 数据结构中的 PG_referenced 标志位置成 1。也就是说,将页面表项中表示受到过访问的信息转移至页面的数据结构中。 而要是页面不在活跃页面队列中,则通过 age_page_up()增加页面可以留下来“以观后效”的时间,因为Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 112 页,共 1481 页 毕竟这个页面最近已受到过访问。 [kswapd() > do_try_to_free_pages() > refill_inactive() > swap_out() > swap_out_mm() > swap_out_vma() > swap_out_pgd() > swap_out_pmd() > try_to_swap_out() > age_page_up()] 00125: void age_page_up(struct page * page) 00126: { 00127: /* 00128: * We're dealing with an inactive page, move the page 00129: * to the active list. 00130: */ 00131: if (!page->age) 00132: activate_page(page); 00133: 00134: /* The actual page aging bit */ 00135: page->age += PAGE_AGE_ADV; 00136: if (page->age > PAGE_AGE_MAX) 00137: page->age = PAGE_AGE_MAX; 00138: } 转到 out_failed 以后,就在那里返回 0,让更高层的程序跳过这个页面。这样,到下一轮又轮到这个 进程和这个页面时,如果同一页面表项 pte 中的_PAGE_ACCESSED 标志位仍然为 0,那就表示不再“年 轻”了。读者也许会问,既然这个页面是有映射的(否则不会出现在目标进程的映射表中并且在内存中), 怎么又会不在活跃页面队列中呢?以后读者就会在 do_swap_page()中看到,当因页面异常而恢复一个不 活跃页面的映射时,并不立即把它转入活跃页面队列,而把这项工作留给前面看到的 page_launder(),让 其在系统比较空闲时再来处理,所以这样的页面可能不在活跃队列中。 如果页面已不“年轻”,那就要进一步考察了。当然,也不能因为这个页面在过去一个周期中未受到 访问就马上把它换出去,还要给他一个“留职查看”的机会。查看多久呢?那就是 page->age 的值,即 页面的寿命。如果页面不在活跃队列中则还要先通过 age_page_down_ageonly()减少其寿命(mm/swap.c): 00103: /* 00104: * We use this (minimal) function in the case where we 00105: * know we can't deactivate the page (yet). 00106: */ 00107: void age_page_down_ageonly(struct page * page) 00108: { 00109: page->age /= 2; 00110: } 只要 page->age 尚未达到 0,就不能将此页面换出,所以也要转到 out_failed。 经过上面这些筛选,这个页面原则上已经是可以换出的对象了。我们继续往下看代码: [kswapd() > do_try_to_free_pages() > refill_inactive() > swap_out() > swap_out_mm() > swap_out_vma() > swap_out_pgd() > swap_out_pmd() > try_to_swap_out()] 00075: if (TryLockPage(page)) 00076: goto out_failed; 00077: 00078: /* From this point on, the odds are that we're going to Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 113 页,共 1481 页 00079: * nuke this pte, so read and clear the pte. This hook 00080: * is needed on CPUs which update the accessed and dirty 00081: * bits in hardware. 00082: */ 00083: pte = ptep_get_and_clear(page_table); 00084: flush_tlb_page(vma, address); 00085: 00086: /* 00087: * Is the page already in the swap cache? If so, then 00088: * we can just drop our reference to it without doing 00089: * any IO - it's already up-to-date on disk. 00090: * 00091: * Return 0, as we didn't actually free any real 00092: * memory, and we should just continue our scan. 00093: */ 00094: if (PageSwapCache(page)) { 00095: entry.val = page->index; 00096: if (pte_dirty(pte)) 00097: set_page_dirty(page); 00098: set_swap_pte: 00099: swap_duplicate(entry); 00100: set_pte(page_table, swp_entry_to_pte(entry)); 00101: drop_pte: 00102: UnlockPage(page); 00103: mm->rss--; 00104: deactivate_page(page); 00105: page_cache_release(page); 00106: out_failed: 00107: return 0; 00108: } 下面对 page 数据结构的操作涉及需要互斥,或者说独占条件下进行的操作,所以这里通过 TryLockPage()将 page 数据锁住(include/linux/mm.h): 00183: #define TryLockPage(page) test_and_set_bit(PG_locked, &(page)->flags) 如果返回 1,即表示 PG_locked 标志位原来就已经是 1,已经被别的进程先锁住了,此时就不能继续 处理这个 page 数据结构,而只好失败返回。 加锁成功以后,就可以根据页面的不同情况作换出的准备了。 首先通过 ptep_get_and_clear()再读一次页面表项的内容,并把表项的内容清成 0,暂时撤销该页面的 映射。前面在 45 行已经读了一次页面表项的内容,为什么现在还要再读一次,而不仅仅是把表项清 0 呢?在多处理器系统中,目标进程有可能正在另一个 CPU 上运行,所以其映射表项的内容有可能已经改 变。 如果页面的 page 数据结构已经在为页面换入/换出而设置的队列中,即数据结构 swapper_space 内的 队列中,那么页面的内容已经在交换设备上,只要把映射暂时断开,表示目标进程已经同意释放这个页 面,就可以了。不过,为页面换入/换出而设置的队列也分为“干净”和“脏”两个,所以如果页面已经 受过写访问就要通过 set_page_dirty()将其转入“脏”页面队列。宏操作 PageSwapCache()的定义为 (include/linux/mm.h): Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 114 页,共 1481 页 00217: #define PageSwapCache(page) test_bit(PG_swap_cache, &(page)->flags) 标志位 PG_swap_cache 为 1 表示 page 结构在 swapper_space 队列中,也说明相应的页面是个普通的 换入/换出页面。此时,page 结构中的 index 字段是一个 32 位的索引项 swp_entry_t,实际上是指向页面 在交换设备上的映象的指针。函数 swap_duplicate()的作用,一者是要对索引项的内容作一些检验,二者 是要递增相应盘上页面的共享计数,其代码在 mm/swapfile.c 中: [kswapd() > do_try_to_free_pages() > refill_inactive() > swap_out() > swap_out_mm() > swap_out_vma() > swap_out_pgd() > swap_out_pmd() > try_to_swap_out() > swap_duplicate()] 00820: /* 00821: * Verify that a swap entry is valid and increment its swap map count. 00822: * Kernel_lock is held, which guarantees existance of swap device. 00823: * 00824: * Note: if swap_map[] reaches SWAP_MAP_MAX the entries are treated as 00825: * "permanent", but will be reclaimed by the next swapoff. 00826: */ 00827: int swap_duplicate(swp_entry_t entry) 00828: { 00829: struct swap_info_struct * p; 00830: unsigned long offset, type; 00831: int result = 0; 00832: 00833: /* Swap entry 0 is illegal */ 00834: if (!entry.val) 00835: goto out; 00836: type = SWP_TYPE(entry); 00837: if (type >= nr_swapfiles) 00838: goto bad_file; 00839: p = type + swap_info; 00840: offset = SWP_OFFSET(entry); 00841: if (offset >= p->max) 00842: goto bad_offset; 00843: if (!p->swap_map[offset]) 00844: goto bad_unused; 00845: /* 00846: * Entry is valid, so increment the map count. 00847: */ 00848: swap_device_lock(p); 00849: if (p->swap_map[offset] < SWAP_MAP_MAX) 00850: p->swap_map[offset]++; 00851: else { 00852: static int overflow = 0; 00853: if (overflow++ < 5) 00854: printk("VM: swap entry overflow\n"); 00855: p->swap_map[offset] = SWAP_MAP_MAX; 00856: } 00857: swap_device_unlock(p); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 115 页,共 1481 页 00858: result = 1; 00859: out: 00860: return result; 00861: 00862: bad_file: 00863: printk("Bad swap file entry %08lx\n", entry.val); 00864: goto out; 00865: bad_offset: 00866: printk("Bad swap offset entry %08lx\n", entry.val); 00867: goto out; 00868: bad_unused: 00869: printk("Unused swap offset entry in swap_dup %08lx\n", entry.val); 00870: goto out; 00871: } 以前讲过,数据结构 swp_entry_t 实际上是 32 位无符号整数,其内容不可能全是 0,但是最低有效 位却一定是 0,最高的(24 位)位段 offset 为设备上的页面序号,其余的(7 位)位段 type 则其实是交 换设备本身的序号。以前还讲过,其中的位段 type 实际上与“类型”毫无关系,而是代表着交换设备的 序号。以此为下标,就可在内核中的数组 swap_info 中找到相应交换设备的 swap_info_struct 数据结构。 这个数据结构中的数组 swap_map[],则记录着交换设备上各个页面的共享计数。由于正在处理中的页面 原来就已经在交换设备上,其计数显然不应为 0,否则就错了;另一方面,递增以后也不应达到 SWAP_MAP_MAX。递增盘上页面的共享计数表示这个页面现在多了一个用户。 回到 try_to_swap_out()的代码中,100 行调用 set_pte(),把这个指向盘上页面的索引项置入相应的页 面表项,原先对内存页面的映射就变成了对盘上页面的映射。这样,当执行到标号 drop_pte 的地方,目 标进程的驻内页面集合 rss 中就减少了一个页面。由于我们这个物理页面断开了一个映射,很可能已经 满足了变成不活跃页面的条件,所以在调用 deactivate_page()时有条件地将其设置成不活跃状态,并将页 面的 page 结构从活跃页面队列转移到某个不活跃页面队列(mm/swap.c): [kswapd() > do_try_to_free_pages() > refill_inactive() > swap_out() > swap_out_mm() > swap_out_vma() > swap_out_pgd() > swap_out_pmd() > try_to_swap_out() > deactivate_page()] 00189: void deactivate_page(struct page * page) 00190: { 00191: spin_lock(&pagemap_lru_lock); 00192: deactivate_page_nolock(page); 00193: spin_unlock(&pagemap_lru_lock); 00194: } [kswapd() > do_try_to_free_pages() > refill_inactive() > swap_out() > swap_out_mm() > swap_out_vma() > swap_out_pgd() > swap_out_pmd() > try_to_swap_out() > deactivate_page() > deactivate_page_nolock()] 00154: /** 00155: * (de)activate_page - move pages from/to active and inactive lists 00156: * @page: the page we want to move 00157: * @nolock - are we already holding the pagemap_lru_lock? 00158: * 00159: * Deactivate_page will move an active page to the right Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 116 页,共 1481 页 00160: * inactive list, while activate_page will move a page back 00161: * from one of the inactive lists to the active list. If 00162: * called on a page which is not on any of the lists, the 00163: * page is left alone. 00164: */ 00165: void deactivate_page_nolock(struct page * page) 00166: { 00167: /* 00168: * One for the cache, one for the extra reference the 00169: * caller has and (maybe) one for the buffers. 00170: * 00171: * This isn't perfect, but works for just about everything. 00172: * Besides, as long as we don't move unfreeable pages to the 00173: * inactive_clean list it doesn't need to be perfect... 00174: */ 00175: int maxcount = (page->buffers ? 3 : 2); 00176: page->age = 0; 00177: ClearPageReferenced(page); 00178: 00179: /* 00180: * Don't touch it if it's not on the active list. 00181: * (some pages aren't on any list at all) 00182: */ 00183: if (PageActive(page) && page_count(page) <= maxcount && !page_ramdisk(page)) { 00184: del_page_from_active_list(page); 00185: add_page_to_inactive_dirty_list(page); 00186: } 00187: } 在物理页面的 page 结构中有个计数器 count,空闲也面的这个计数为 0,在分配页面时将其设为 1 (见__alloc_pages()和 rmqueue()的代码),此后每当页面增加一个“用户”,如建立或恢复一个映射时, 就使 count 加 1。这样,如果这个计数器的值为 2,就说明刚断开的映射已经是该物理页面的最后一个映 射。既然最后的映射已经断开,着页面当然是不活跃的了。所以把小于等于 2 作为一个判断的准则,就 是这里的 maxcount。但是,这里还要考虑一种特殊情况,就是当这个页面是通过 mmap()映射到普通文 件,而这个文件又已经被打开,按常规的文件操作访问,因为这个页面又同时用作读/写文件的缓冲。此 时页面划分成若干干缓冲区,其 page 结构中的指针 buffers 指向一个 buffer_head 数据结构队列,而这个 队列则成了该页面的另一个“用户”。所以,当 page->buffer 非 0 时,maxcount 为 3 说明刚断开的映射是 该内存也面的最后一个映射。此外,内存页面也有可能用作 ramdisk,即以一部分内存物理页面空间来模 拟硬盘,这样的页面永远不会变成不活跃。这样,判断的准则一共有三条,只有满足了这三条准则时才 真的可以将页面转入不活跃队列。多数有用户空间映射的内存页面都只有一个映射,此时就转入了不活 跃状态。同时,从代码中也可看出,对在不活跃队列中的页面再调用一次 deactivate_page_nolock()并无 害处。 将一个活跃页面变成不活跃时,要把该页面的 page 结构从活跃页面的 LRU 队列 active_list 中转移到 一个不活跃队列中去。可是,系统中有两种不活跃页面队列。一种是“dirty”,即可能最近已被写过,因 而跟交换设备上的页面不一致的“脏”页面队列,这样的页面不能马上就拿来分配,因为还需要把它写 出去才能把它“洗净”。另一种是“clean”,即肯定跟交换设备上的页面一致的“干净”页面队列,这样Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 117 页,共 1481 页 的页面原则上已可作为空闲页面分配,只是因为页面中的内容还可能有用,因而再予以保存一段时间。 不活跃“脏”页面队列只有一个,那就是 inactive_dirty_list;而不活跃“干净”页面队列则有很多,每 个页面管理区中都有个 inactive_clean_list 队列。那么,当一个原来活跃的页面变成不活跃时,应该把它 转移到哪一个队列中去呢?第一步总是把它转入“脏”页面队列。将一个 page 结构从活跃队列脱链是由 宏操作 del_page_from_active_list()完成的,其定义在 include/linux/swap.h 中: 00234: #define del_page_from_active_list(page) { \ 00235: list_del(&(page)->lru); \ 00236: ClearPageActive(page); \ 00237: nr_active_pages--; \ 00238: DEBUG_ADD_PAGE \ 00239: ZERO_PAGE_BUG \ 00240: } 将一个 page 结构链入不活跃队列,则由 add_page_to_inactive_dirty_list()完成: 00217: #define add_page_to_inactive_dirty_list(page) { \ 00218: DEBUG_ADD_PAGE \ 00219: ZERO_PAGE_BUG \ 00220: SetPageInactiveDirty(page); \ 00221: list_add(&(page)->lru, &inactive_dirty_list); \ 00222: nr_inactive_dirty_pages++; \ 00223: page->zone->inactive_dirty_pages++; \ 00224: } 这里的 ClearPageActive()和 SetPageInactiveDirty()分别将 page 结构中的 PG_active 标志位清成 0 和将 PG_inactive_dirty 标志位设成 1。注意在这个过程中 page 结构中的使用计数并未改变。 又回到 try_to_swap_out()的代码中,既然断开了对一个内存页面的映射,就要递减对这个页面的使 用计数,这是由宏操作 page_cache_release()、实际上是由__free_pages()完成的。 00034: #define page_cache_release(x) __free_page(x) 00379: #define __free_page(page) __free_pages((page), 0) 00549: void __free_pages(struct page *page, unsigned long order) 00550: { 00551: if (!PageReserved(page) && put_page_testzero(page)) 00552: __free_pages_ok(page, order); 00553: } 00152: #define put_page_testzero(p) atomic_dec_and_test(&(p)->count) 这个函数通过 put_page_testzero(),将 page 结构中 count 的值减 1,然后测试是否达到了 0,如果达 到了 0 就通过__free_pages_ok()将该页面释放。在这里,由于页面还在不活跃页面队列中尚未释放,至少 还有这么一个引用,所以不会达到 0。 至此,对这个页面的处理就完成了,于是又到了标号 out_failed 处而返回 0。为什么又是到达 outfailed 处呢?其实,try_to_swap_out()仅在一种情况下返回 1,那就是当 mm->swap_cnt 达到了 0 的时候(见 52Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 118 页,共 1481 页 行)。正是这样,才使 swap_out_mm()能够依次考察一个进程的所有页面。 要是页面的 page 结构不在 swapper_space 的队列中呢?这说明尚未为该页面在交换设备上建立起映 象,或者页面来自一个文件。读者可以回顾一下,在因页面无映射而发生缺页异常时,具体的处理取决 于页面所在的区间是否提供了一个 vm_operations_struct 数据结构,并且通过这个数据结构中的函数指针 nopage 提供了特定的操作。如果提供了 nopage 操作,就说明该区间的页面来自一个文件(而不是交换 设备),此时根据虚存地址可以计算出在文件中的页面位置。否则就是普通的页面,但尚未建立相应的盘 上页面(因为页面表项为 0),此时先把它映射到空白页面,以后需要写的时候才为之另行分配一个页面。 我们继续往下看 try_to_swap_out()的代码,下面一段就是对这种页面的处理: [kswapd() > do_try_to_free_pages() > refill_inactive() > swap_out() > swap_out_mm() > swap_out_vma() > swap_out_pgd() > swap_out_pmd() > try_to_swap_out()] 00110: /* 00111: * Is it a clean page? Then it must be recoverable 00112: * by just paging it in again, and we can just drop 00113: * it.. 00114: * 00115: * However, this won't actually free any real 00116: * memory, as the page will just be in the page cache 00117: * somewhere, and as such we should just continue 00118: * our scan. 00119: * 00120: * Basically, this just makes it possible for us to do 00121: * some real work in the future in "refill_inactive()". 00122: */ 00123: flush_cache_page(vma, address); 00124: if (!pte_dirty(pte)) 00125: goto drop_pte; 00126: 00127: /* 00128: * Ok, it's really dirty. That means that 00129: * we should either create a new swap cache 00130: * entry for it, or we should write it back 00131: * to its own backing store. 00132: */ 00133: if (page->mapping) { 00134: set_page_dirty(page); 00135: goto drop_pte; 00136: } 00137: 00138: /* 00139: * This is a dirty, swappable page. First of all, 00140: * get a suitable swap entry for it, and make sure 00141: * we have the swap cache set up to associate the 00142: * page with that swap entry. 00143: */ 00144: entry = get_swap_page(); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 119 页,共 1481 页 00145: if (!entry.val) 00146: goto out_unlock_restore; /* No swap space left */ 00147: 00148: /* Add it to the swap cache and mark it dirty */ 00149: add_to_swap_cache(page, entry); 00150: set_page_dirty(page); 00151: goto set_swap_pte; 00152: 00153: out_unlock_restore: 00154: set_pte(page_table, pte); 00155: UnlockPage(page); 00156: return 0; 00157: } 这里的 pte_dirty()是一个 inline 函数,定义于 include/asm-i386/pgtable.h: 00269: static inline int pte_dirty(pte_t pte) { return (pte).pte_low & _PAGE_DIRTY; } 在页面表项中有一个“D”标志位(_PAGE_DIRTY),如果 CPU 对表项所指的内存页面进行了写操 作,就自动把该标志位设置成 1,表示该内存页面已经“脏”了。如果此标志位为 0,就表示相应内存页 面尚未被写过。对这样的页面,如果很久没有受到写访问,就可以把映射解除(而不是暂时断开)。这是 因为:如果页面的内容是空白,那么以后需要时可以再来建立映射;或者,如果页面来自通过 mmap() 建立起的文件映射,则在需要时可以根据虚拟地址计算出页面在文件中的位置(相比之下,交换设备上 的页面位置不能通过计算得到,所以必须把页面的去向存储在页面表项中)。所以,这里转到前面的标号 drop_pte 处。注意在这种情况下前面的 deactivate_page()实际上就不起作用,特别时页面表项已在前面 83 行清 0,而 page_cache_release()则只是递减对空白页面的引用计数。 如果考察的页面是来自通过 mmap()建立起的文件映射,则其 page 结构中的指针 mapping 指向相应 的 address_space 数据结构。对于这样的页面,如果决定解除映射,而页面表项中的_PAGE_DIRTY 标志 位为 1,就要在转到 drop_pte 处之前,先把 page 结构中的 PG_dirty 标志位设成 1,并把页面转移到该文 件映射的“脏”页面队列中。有关的操作 set_page_dirty()定义于 include/linux/mm.h 以及 mm/filemap.c: [kswapd() > do_try_to_free_pages() > refill_inactive() > swap_out() > swap_out_mm() > swap_out_vma() > swap_out_pgd() > swap_out_pmd() > try_to_swap_out() > set_page_dirty()] 00187: static inline void set_page_dirty(struct page * page) 00188: { 00189: if (!test_and_set_bit(PG_dirty, &page->flags)) 00190: __set_page_dirty(page); 00191: } 00134: /* 00135: * Add a page to the dirty page list. 00136: */ 00137: void __set_page_dirty(struct page *page) 00138: { 00139: struct address_space *mapping = page->mapping; 00140: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 120 页,共 1481 页 00141: spin_lock(&pagecache_lock); 00142: list_del(&page->list); 00143: list_add(&page->list, &mapping->dirty_pages); 00144: spin_unlock(&pagecache_lock); 00145: 00146: mark_inode_dirty_pages(mapping->host); 00147: } 再往下看 try_to_swap_out()的代码。当程序执行到这里时,所考察的页面必然是个很久没有受到访 问,又不在 swapper_space 的换入/换出队列中,也不属于文件映射,但却是个受到过写访问的“脏”页 面。对于这样的页面必需要为之分配一个盘上页面,并将其内容写到盘上页面中去。首先通过 get_swap_page()分配一个盘上页面,这是个宏操作: 00150: #define get_swap_page() __get_swap_page(1) 就是说,通过__get_swp_page(1)从交换设备上分配一个页面。其代码在 mm/swapfile.c 中,由于比较 简单,我们把它留给读者。盘上页面的使用计数在分配时设置成 1,以后每当有进程参与共享同一内存 页面时就通过 swap_duplicate()递增,此外在有进程断开对此页面的映射时也要递增(见 99 行);反之则 通过 swap_free()递减。如果分配盘上页面失败,就转到 out_unlock_restore 处恢复原有的映射。 分配了盘上页面以后,就通过 add_to_swap_cache()将页面链入 swapper_space 的队列中,以及活跃 页面队列中,这个函数的代码以前已经看到过了。然后,再通过 set_page_dirty()将页面转到不活跃“脏” 页面队列中。至于实际的写出,则前面已经看到是 page_launder()的事。 至此,对一个进程的用户空间页面的扫描处理就完成了。swap_out()是在一个 for 循环中调用 swap_out_mm()的,所以每次调用 swap_out()都会换出若干进程的若干页面,而 refill_inactive()又是再嵌 套的 while 循环中调用 swap_out()的,一直要到系统中可供分配的页面,包括潜在可供分配的页面在内不 再短缺时为止。到那时,do_try_to_free_pages()就结束了。回到 kswapd()的代码中,此时活跃页面队列的 情况可能已经有了较大的改变,所以还要再调用一次 refill_inactive_scan()。这样,kswapd()的一次例行 路线就基本走完了。如前所述,kswapd()除定期的执行外,也有可能是被其他进程唤醒的,所以可能有 进程正在睡眠中等待其完成,因此通过 wake_up_all()唤醒这些进程。 读者也许在想,通过 swap_out_mm()对每个进程页面表的扫描并不保证一定能有页面转入活跃状态, 这样 refill_inactive()岂不是要无穷无尽地循环下去?事实上,以来程序中对循环的次数有个限制,二来 对页面表的扫描是个自适应的过程。如果在对多有进程的一轮扫描后转入不活跃状态的页面数量不足, 那么 refill_inactive()就会又回过头来开始第二轮扫描。而扫描次数的增加会使页面老化的速度也增加, 因为页面的寿命实际上是以扫描的次数为单位的。这样,在第一轮扫描中不符合条件的页面在第二轮扫 描中就可能符合条件了。最后,在很特殊的情况下,可能最终还是达不到要求,此时就调用 oom_kill() 从系统中杀掉一个进程,通过牺牲局部来保障全局。 最后,再来看看线程 kreclaimd 的代码,这是在 mm/vmscan.c 中: 01095: DECLARE_WAIT_QUEUE_HEAD(kreclaimd_wait); 01096: /* 01097: * Kreclaimd will move pages from the inactive_clean list to the 01098: * free list, in order to keep atomic allocations possible under 01099: * all circumstances. Even when kswapd is blocked on IO. 01100: */ 01101: int kreclaimd(void *unused) 01102: { Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 121 页,共 1481 页 01103: struct task_struct *tsk = current; 01104: pg_data_t *pgdat; 01105: 01106: tsk->session = 1; 01107: tsk->pgrp = 1; 01108: strcpy(tsk->comm, "kreclaimd"); 01109: sigfillset(&tsk->blocked); 01110: current->flags |= PF_MEMALLOC; 01111: 01112: while (1) { 01113: 01114: /* 01115: * We sleep until someone wakes us up from 01116: * page_alloc.c::__alloc_pages(). 01117: */ 01118: interruptible_sleep_on(&kreclaimd_wait); 01119: 01120: /* 01121: * Move some pages from the inactive_clean lists to 01122: * the free lists, if it is needed. 01123: */ 01124: pgdat = pgdat_list; 01125: do { 01126: int i; 01127: for(i = 0; i < MAX_NR_ZONES; i++) { 01128: zone_t *zone = pgdat->node_zones + i; 01129: if (!zone->size) 01130: continue; 01131: 01132: while (zone->free_pages < zone->pages_low) { 01133: struct page * page; 01134: page = reclaim_page(zone); 01135: if (!page) 01136: break; 01137: __free_page(page); 01138: } 01139: } 01140: pgdat = pgdat->node_next; 01141: } while (pgdat); 01142: } 01143: } 对照一下 kswap()的代码,就可以看出二者的初始化部分是一样的,程序的结构也相似。注意二者都 把其 task_struct 结构中 flags 字段的 PF_MEMALLOC 标志位设成 1,表示这两个内核线程都是页面管理 机制的维护者。事实上,在以前的版本中只有一个线程 kswapd,在 2.4 版中才把其中的一部分独立出来 成为一个线程。不过,这一次是通过 reclaim_page()扫描各个页面管理区中的不活跃“干净”页面队列,Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 122 页,共 1481 页 从中回收页面加以释放。这个函数的代码在 mm/vmscan.c 中,我们把它留给读者自己阅读。在阅读了上 面这些代码后,读者已经不至于感到困难了。 [kreclaimd() > reclaimd_page()] 00381: /** 00382: * reclaim_page - reclaims one page from the inactive_clean list 00383: * @zone: reclaim a page from this zone 00384: * 00385: * The pages on the inactive_clean can be instantly reclaimed. 00386: * The tests look impressive, but most of the time we'll grab 00387: * the first page of the list and exit successfully. 00388: */ 00389: struct page * reclaim_page(zone_t * zone) 00390: { 00391: struct page * page = NULL; 00392: struct list_head * page_lru; 00393: int maxscan; 00394: 00395: /* 00396: * We only need the pagemap_lru_lock if we don't reclaim the page, 00397: * but we have to grab the pagecache_lock before the pagemap_lru_lock 00398: * to avoid deadlocks and most of the time we'll succeed anyway. 00399: */ 00400: spin_lock(&pagecache_lock); 00401: spin_lock(&pagemap_lru_lock); 00402: maxscan = zone->inactive_clean_pages; 00403: while ((page_lru = zone->inactive_clean_list.prev) != 00404: &zone->inactive_clean_list && maxscan--) { 00405: page = list_entry(page_lru, struct page, lru); 00406: 00407: /* Wrong page on list?! (list corruption, should not happen) */ 00408: if (!PageInactiveClean(page)) { 00409: printk("VM: reclaim_page, wrong page on list.\n"); 00410: list_del(page_lru); 00411: page->zone->inactive_clean_pages--; 00412: continue; 00413: } 00414: 00415: /* Page is or was in use? Move it to the active list. */ 00416: if (PageTestandClearReferenced(page) || page->age > 0 || 00417: (!page->buffers && page_count(page) > 1)) { 00418: del_page_from_inactive_clean_list(page); 00419: add_page_to_active_list(page); 00420: continue; 00421: } 00422: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 123 页,共 1481 页 00423: /* The page is dirty, or locked, move to inactive_dirty list. */ 00424: if (page->buffers || PageDirty(page) || TryLockPage(page)) { 00425: del_page_from_inactive_clean_list(page); 00426: add_page_to_inactive_dirty_list(page); 00427: continue; 00428: } 00429: 00430: /* OK, remove the page from the caches. */ 00431: if (PageSwapCache(page)) { 00432: __delete_from_swap_cache(page); 00433: goto found_page; 00434: } 00435: 00436: if (page->mapping) { 00437: __remove_inode_page(page); 00438: goto found_page; 00439: } 00440: 00441: /* We should never ever get here. */ 00442: printk(KERN_ERR "VM: reclaim_page, found unknown page\n"); 00443: list_del(page_lru); 00444: zone->inactive_clean_pages--; 00445: UnlockPage(page); 00446: } 00447: /* Reset page pointer, maybe we encountered an unfreeable page. */ 00448: page = NULL; 00449: goto out; 00450: 00451: found_page: 00452: del_page_from_inactive_clean_list(page); 00453: UnlockPage(page); 00454: page->age = PAGE_AGE_START; 00455: if (page_count(page) != 1) 00456: printk("VM: reclaim_page, found page with count %d!\n", 00457: page_count(page)); 00458: out: 00459: spin_unlock(&pagemap_lru_lock); 00460: spin_unlock(&pagecache_lock); 00461: memory_pressure++; 00462: return page; 00463: } 2.9. 页面的换入 在 i386 CPU 将一个线性地址映射成一个物理地址的过程中,如果该地址的映射关系已经建立,但是 发现相应页面或目录表项中的 P(Present)标志位为 0,则表示相应的物理页面不在内存,从而无法完成Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 124 页,共 1481 页 本次内存访问。从理论上说,也许应该把这种情况成为“受阻”而不是“失败”,因为映射的关系毕竟已 经建立,理应与尚未建立映射的情况有所区别,所以我们称之为“断开”。但是 CPU 的 MMU 硬件并不 区分这两种不同的情况,只要 P 标志位为 0 就都认为是页面映射失败,CPU 就会产生一次“页面异常” (Page Fault)。事实上,CPU 在映射过程中首先看的就是页面表项或目录项中的 P 标志位。只要 P 标志 位为 0,其余各个位段的值就无意义了。至于当一个页面不在内存中时,利用页面表项指向一个盘上页 面,那是软件的事。所以,区分失败的原因到底是因为页面不在内存,还是因为映射尚未建立,乃是软 件,也就是页面异常处理程序的事。在“越界访问”的情景中,我们曾看到在函数 handle_pte_fault()中 的头几行: [do_page_fault() > handle_mm_fault() > handle_pte_fault()] 01153: static inline int handle_pte_fault(struct mm_struct *mm, 01154: struct vm_area_struct * vma, unsigned long address, 01155: int write_access, pte_t * pte) 01156: { 01157: pte_t entry; 01158: 01159: /* 01160: * We need the page table lock to synchronize with kswapd 01161: * and the SMP-safe atomic PTE updates. 01162: */ 01163: spin_lock(&mm->page_table_lock); 01164: entry = *pte; 01165: if (!pte_present(entry)) { 01166: /* 01167: * If it truly wasn't present, we know that kswapd 01168: * and the PTE updates will not touch it later. So 01169: * drop the lock. 01170: */ 01171: spin_unlock(&mm->page_table_lock); 01172: if (pte_none(entry)) 01173: return do_no_page(mm, vma, address, write_access, pte); 01174: return do_swap_page(mm, vma, address, pte, pte_to_swp_entry(entry), write_access); 01175: } …………………… 这里,首先区分的是 pte_present(),也就是检查表项中的 P 标志位,看看物理页面是否在内存中。 如果不在,则进而通过 pte_none()检查表项是否为空,即全 0。如果为空就说明映射尚未建立,所以要 do_no_page()。这在以前的情景中已经看到过了。反之,如果非空,就说明映射已经建立,只是物理页 面不在内存中,所以要通过 do_swap_page(),从交换设备上还入这个页面。本情景在 handle_pte_fault() 之前的处理以及执行路线都与越界访问的情景相同,所以我们直接进入 do_swap_page()。这个函数的代 码在 mm/memory.c 中: [do_page_fault() > handle_mm_fault() > handle_pte_fault() > do_swap_page()] 01018: static int do_swap_page(struct mm_struct * mm, 01019: struct vm_area_struct * vma, unsigned long address, Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 125 页,共 1481 页 01020: pte_t * page_table, swp_entry_t entry, int write_access) 01021: { 01022: struct page *page = lookup_swap_cache(entry); 01023: pte_t pte; 01024: 01025: if (!page) { 01026: lock_kernel(); 01027: swapin_readahead(entry); 01028: page = read_swap_cache(entry); 01029: unlock_kernel(); 01030: if (!page) 01031: return -1; 01032: 01033: flush_page_to_ram(page); 01034: flush_icache_page(vma, page); 01035: } 01036: 01037: mm->rss++; 01038: 01039: pte = mk_pte(page, vma->vm_page_prot); 01040: 01041: /* 01042: * Freeze the "shared"ness of the page, ie page_count + swap_count. 01043: * Must lock page before transferring our swap count to already 01044: * obtained page count. 01045: */ 01046: lock_page(page); 01047: swap_free(entry); 01048: if (write_access && !is_page_shared(page)) 01049: pte = pte_mkwrite(pte_mkdirty(pte)); 01050: UnlockPage(page); 01051: 01052: set_pte(page_table, pte); 01053: /* No need to invalidate - it was non-present before */ 01054: update_mmu_cache(vma, address, pte); 01055: return 1; /* Minor fault */ 01056: } 先看看调用时传过来的参数是些什么。建议读者先回到前面通过越界访问扩充堆栈的情景中,顺着 CPU 的执行路线走一遍,搞清楚这些参数的来龙去脉。参数表中的 mm、vma 还有 address 是一目了然的, 分别是指向当前进程的 mm_struct 结构的指针、所属虚存区间的 vm_area_struct 结构的指针以及映射失败 的线性地址。 参数 page_table 指向映射失败的页面表项,而 entry 则为该表项的内容。我们以前说过,当物理页面 在内存中时,页面表项是一个 pte_t 结构,指向一个内存页面;而当物理页面不在内存中时,则是一个 swap_entry_t 结构,指向一个盘上页面。二者实际上都是 32 位无符号整数。这里要指出,所谓“不在内 存中”是逻辑意义上的,是对 CPU 的页面映射硬件而言,实际上这个页面很可能在不活跃页面队列中,Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 126 页,共 1481 页 甚至在活跃页面队列中。 还有一个参数 write_access,表示当映射失败时所进行的访问种类(读/写),这是在 do_page_fault() 的 switch 语句中(见 arch/i386/fault.c)根据 CPU 产生的出错代码 error_code 的 bit1 决定的(注意,在那 个 switch 语句中,“default:”与“case 2:”之间没有 break 语句)。此后便逐层传了下来。 由于物理页面不在内存,所以 entry 是指向一个盘上页面的类似于指针的索引项(加上若干标志位)。 该指针逻辑上分成两部分:第一部分是页面交换设备(或文件)的序号;第二部分是页面在这个设备上 (或文件中,下同)的位移,其实也就是页面序号。两部分合在一起就唯一地确定了一个盘上页面。供 页面交换的设备上第一个页面(序号为 0)是保留不用的,所以 entry 的值不可能为全 0。这样才能与映 射尚未建立时的页面表项相区别。 处理一次因缺页而引起的页面异常时,首先要看看相应的内存页面是否还留在 swapper_space 的换 入/换出队列中尚未最后释放。如果是的话那就省事了。所以,要先调用 lookup_swap_cache()。这个函数 是在 swap_state.c 中定义的,我们把它留给读者自己阅读。 如果没有找到,就是说以前用于这个虚存页面的内存页面已经释放,现在其内容仅存在于盘上了, 那就要通过 read_swap_cache()分配一个内存页面,并且从盘上将其内容读进来。为什么在此之前要先调 用 swapin_readahead()呢?当从磁盘上读的时候,每次仅仅读一个页面是不经济的,因为每次读盘都要经 过在磁盘上寻道使磁头定位,而寻道所需的时间实际上比磁头到位以后读一个页面所需的时间要长得多。 所以,比较经济的办法是:既然必须经过寻道,就干脆一次多读几个页面进来,称为一个页面集群 (cluster)。由于此时并非每个读入的页面都是立即需要的,所以是“预读”(read ahead)。预读进来的页 面都暂时链入活跃页面队列以及 swapper_space 的换入/换出队列中,如果实际上确实不需要就会由进程 kswapd 和 kreclaimd 在一段时间以后加以回收。这样,当调用 read_swap_cache()时,通常所需的页面已 经在活跃页面队列中而只需要把它找到就行了。但是也有可能预读时因为分配不到足够的内存页面而失 败,那样就真的要再来读一次,而这一次却真是只读入一个页面了。细心的读者可能会问,这两行程序 是紧挨着的,为什么在前一行语句中因分配不到足够的内存页面而失败,到紧接着的下一行就有可能成 功呢?这是因为,在分配内存页面失败时,内核可能会调度其他进程先运行,而被调度运行的进程可能 会释放出一些内存页面,甚至被调度运行的进程可能恰好就是 kswapd。因此,第一次分配内存页面失败 并不说明紧接着的第二次也会失败。要明白这一点,我们可以再来看一下函数__alloc_pages()中的一个片 段: 00382: wakeup_kswapd(0); 00383: if (gfp_mask & __GFP_WAIT) { 00384: __set_current_state(TASK_RUNNING); 00385: current->policy |= SCHED_YIELD; 00386: schedule(); 00387: } 无论是 swapin_readahead()还是 read_swap_cache(),在申请分配内存页面时都把调用参数 gfp_mask 中的__GFP_WAIT 标志位设置成 1,所以当分配不到内存页面时都会自愿暂时礼让,让内核调度其他进 程先运行。由于在此之前先唤醒了 kswapd,当本进程被调度恢复运行时,也就是从 schedule()返回时, 再次试图分配页面已有可能成功了。即使在 swapin_readahead()中又失败了,在 read_swap_cache()中再来 一次,也还是有可能(而且多半能够)成功。当然,也有可能二者都失败了,那样 do_swap_page()也就 失败了,所以在 1031 行返回-1。这里,我们就不深入到 swapin_readahead()中去了,读者可以自行阅读。 而 read_swap_cache()实际上是 read_swap_cache_async(),只是把调用参数 wait 设成 1,表示要等待读入 完成(所以实际上是同步的读入)。 00125: #define read_swap_cache(entry) read_swap_cache_async(entry, 1); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 127 页,共 1481 页 函数 read_swap_cache_async()的代码在 mm/swap_state.c 中: [do_page_fault() > handle_mm_fault() > handle_pte_fault() > do_swap_page() > read_swap_cache_async()] 00204: /* 00205: * Locate a page of swap in physical memory, reserving swap cache space 00206: * and reading the disk if it is not already cached. If wait==0, we are 00207: * only doing readahead, so don't worry if the page is already locked. 00208: * 00209: * A failure return means that either the page allocation failed or that 00210: * the swap entry is no longer in use. 00211: */ 00212: 00213: struct page * read_swap_cache_async(swp_entry_t entry, int wait) 00214: { 00215: struct page *found_page = 0, *new_page; 00216: unsigned long new_page_addr; 00217: 00218: /* 00219: * Make sure the swap entry is still in use. 00220: */ 00221: if (!swap_duplicate(entry)) /* Account for the swap cache */ 00222: goto out; 00223: /* 00224: * Look for the page in the swap cache. 00225: */ 00226: found_page = lookup_swap_cache(entry); 00227: if (found_page) 00228: goto out_free_swap; 00229: 00230: new_page_addr = __get_free_page(GFP_USER); 00231: if (!new_page_addr) 00232: goto out_free_swap; /* Out of memory */ 00233: new_page = virt_to_page(new_page_addr); 00234: 00235: /* 00236: * Check the swap cache again, in case we stalled above. 00237: */ 00238: found_page = lookup_swap_cache(entry); 00239: if (found_page) 00240: goto out_free_page; 00241: /* 00242: * Add it to the swap cache and read its contents. 00243: */ 00244: lock_page(new_page); 00245: add_to_swap_cache(new_page, entry); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 128 页,共 1481 页 00246: rw_swap_page(READ, new_page, wait); 00247: return new_page; 00248: 00249: out_free_page: 00250: page_cache_release(new_page); 00251: out_free_swap: 00252: swap_free(entry); 00253: out: 00254: return found_page; 00255: } 读者也许注意到了,这里两次调用了 lookup_swap_cache() 。第一次是很好理解的,因为 swapin_readahead()也许已经把目标页面读进来了,所以要先从 swapper_sapce 队列中寻找一次。这一方 面是为了节省一次从设备读入;另一方面,更重要的是防止同一个页面在内存中有两个副本。可是为什 么找不到、因而为此分配了一个内存页面以后又来寻找一次呢?这是因为分配内存页面的过程有可能受 阻,如果一时分配不到页面,当前进程就会睡眠等待,让别的进程先运行。而当这个进程再次被调度运 行,并成功地分配到物理页面从__get_free_page()返回时,也许另一个进程已经先把这个页面读进来了, 所以要再检查一次。如果确实需要从交换设备读入,则通过 add_to_swap_cache()将新分配的屋里页面(确 切地说是它的 page 数据结构)挂入 swapper_space 队列以及 active_list 队列中,这个函数的代码读者已 经看到过了。至于 rw_swap_page(),读者可以在学习了块设备驱动一章以后回过来阅读。调用 read_swap_cache()成功以后,所要的页面肯定已经在 swapper_space 队列以及 active_list 队列中了,并且 马上就要恢复映射。 这里要着重注意一下对盘上页面的共享计数。首先,一开始时在 221 行就通过 swap_duplicate()递增 了盘上页面的共享计数。如果在缓冲队列中找到了所需的页面而无需从交换设备读入,则在 252 行通过 swap_free()抵消对共享计数的递增。反之,如果需要从交换设备读入页面,则不调用 swap_free(),所以 盘上页面的共享计数加了 1。可是,回到 do_swap_page()以后,在 1047 行又调用了一次 swap_free(),使 盘上页面的共享计数减 1。这么一来,情况就变成了这样:如果从交换设备读入页面,则盘上页面的共 享计数保持不变;而如果在缓冲队列中找到了所需要的页面,则共享计数减 1。对此,读者不妨回过去 看一下 try_to_swap_out()中的 99 行。在那里,当断开一个页面的映射时,通过 swap_duplicate()递增了盘 上页面的共享计数。而现在恢复映射则使共享计数减 1,二者使互相对应的。 还要注意对内存页面,即其 page 结构的使用计数。首先,在分配一个内存页面时把这个计数设成 1。 然后,在通过 add_to_swap_cache()将其链入换入/换出队列(或文件映射队列)和 LRU 队列 active_list 时,又在 add_to_page_cache_locked()中通过 page_cache_get()递增了这个计数,所以当有、并且只有一个 进程映射到这个换入/换出页面时,其使用计数为 2。如果页面来自文件映射,则由于同时又与文件读/ 写缓冲区相联系,又多了一个“用户”,所以使用计数为 3。但是,还有一种特殊情况,那就是通过 swapin_readahead()预读进来的页面。 [do_page_fault() > handle_mm_fault() > handle_pte_fault() > do_swap_page() > swapin_readahead()] 00984: /* 00985: * Primitive swap readahead code. We simply read an aligned block of 00986: * (1 << page_cluster) entries in the swap area. This method is chosen 00987: * because it doesn't cost us any seek time. We also make sure to queue 00988: * the 'original' request together with the readahead ones... 00989: */ 00990: void swapin_readahead(swp_entry_t entry) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 129 页,共 1481 页 00991: { 00992: int i, num; 00993: struct page *new_page; 00994: unsigned long offset; 00995: 00996: /* 00997: * Get the number of handles we should do readahead io to. Also, 00998: * grab temporary references on them, releasing them as io completes. 00999: */ 01000: num = valid_swaphandles(entry, &offset); 01001: for (i = 0; i < num; offset++, i++) { 01002: /* Don't block on I/O for read-ahead */ 01003: if (atomic_read(&nr_async_pages) >= pager_daemon.swap_cluster 01004: * (1 << page_cluster)) { 01005: while (i++ < num) 01006: swap_free(SWP_ENTRY(SWP_TYPE(entry), offset++)); 01007: break; 01008: } 01009: /* Ok, do the async read-ahead now */ 01010: new_page = read_swap_cache_async(SWP_ENTRY(SWP_TYPE(entry), offset), 0); 01011: if (new_page != NULL) 01012: page_cache_release(new_page); 01013: swap_free(SWP_ENTRY(SWP_TYPE(entry), offset)); 01014: } 01015: return; 01016: } 在 swapin_readahead()中,循环地调用 read_swap_cache_async()分配和读入若干页面,因而在从 read_swap_cache_async() 返回时,每个页面的使用计数都是 2 。但是,在循环中马上又通过 page_cache_release()递减这个计数,因为预读进来的页面并没有进程在使用。于是,这些页面就成了特 殊的页面,它们在 active_list 中,而使用计数却是 1。以后,这些页面或者是被某个进程“认领”,从而 使使用计数变成 2;或者是在一段时间以后仍无进程认领,最后被 refill_inactive_scan()移入不活跃队列 (见 mm/vmscan.c 的 744 行),那才是使用计数为 1 的页面该呆的地方。 回到 do_swap_page()的代码中,这里的 flush_page_to_ram()和 flush_icache_page()对于 i386 处理器均 为空操作。代码中通过 pte_mkdirty()将页面表项中的 D 标志位置成 1,表示该页面已经“脏”了,并且 通过 pte_mkwrite()将页面表项中的_PAGE_RW 标志位也置成 1。读者也许会问:怎么可以凭着当前的访 问是一次写访问就把页面表项设置成允许写?万一本来就应该有写保护的呢?答案是,如果那样的话就 根本到达不了这个地方。读者不妨回过头去看看 do_page_fault()中 switch 语句的 case 2。在那里,如果页 面所属的区间不允许写的话(VM_WRITE 标志位为 0),就转到 bad_area 去了。还要注意,区间的可写 标志 VM_WRITE 与页面的可写标志_PAGE_RW 是不同的。VM_WRITE 是个相对静态的标志位;而 _PAGE_RW 则更为动态,只表示当前这一物理页面是否允许写访问。只有在 VM_WRITE 为 1 的前提下, _PAGE_RW 才有可能为 1,但却并不一定为 1。所以,在 1039 行中,根据 vma->vm_page_prot 构筑一 个页面表项时,表项的_PAGE_RW 标志位为 0(注意 VM_WRITE 是 vma->vm_flags 而不是 vma->vm_page_prot 中的一位)。读者还可能会问,那样一来,要是当前的访问恰好是读访问,这个页面 不就永远不允许写了吗?不要紧,发生写访问时会因为访问权限不符而引起另一次页面异常。那时,就 会在 handle_pte_fault()中调用 do_wp_page(),将页面的访问权限作出改变(如果需要 cow,即 copy_on_writeLinux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 130 页,共 1481 页 的话,也是在那里处理的)。我们将 do_wp_page()留给读者,一来因为篇幅的关系,二来读者现在对存 储管理已经比较熟悉,应该不会有太大的困难了。 至于紧接着的 update_mmu_cache(),对于 i386 CPU 只是个空操作,因为 i386 的 MMU 是与 CPU 汇 成一体的。 2.10. 内核缓冲区的管理 可想而知,内核在运行中常常会需要使用一些缓冲区。例如,当要建立一个新的进程时就要增加一 个 task_struct 结构,而当这些进程撤销时就要释放本进程的 task 结构。这些小块存储空间的使用并不局 限于某一个子程序,否则就可以作为这个子程序的局部变量而使用堆栈空间了。另外,这些小块存储空 间又是动态变化的,不像用于内存页面管理的 page 结构那样,有多大的内存就有多少个 page 结构,构 成一个静态的阵列。由于事先根本无法预测运行中各种不同数据结构对缓冲区的要求,不适合为每一种 可能用到的数据结构建立一个“缓冲池”,因为那样的话很可能会出现有些缓冲池已经用尽而有些缓冲池 中却有大量缓冲区空间的局面。因此,只能采用更具全局性的方法。 那么,用什么样的方法呢?如果采用像用户空间中的 malloc()那样的动态分配办法,从一个统一的 存储空间“堆”(heap)中,需要用多少就切下多大一块,不用了就归还,则有几个缺点需要考虑改进: 久而久之,会使存储堆“碎片化”,以至虽然存储堆中空闲空间的总量足够大、却无法分配所需 大小的连续空间。为此,一般采用按 2n的大小来分配空间,以缓解碎片化。 每次分配得到所需大小的缓冲区以后,都要进行初始化。内核中频繁地使用一些数据结构,这 些数据结构中相当一部分成分需要某些特殊的初始化(例如队列头部等)而并非简单地清成全 0。如果释放的数据结构可以在下次分配时“重用”而无需初始化,那就可以提高内核的效率。 缓冲区的组织和管理是密切相关的。在有高速缓存的情况下,这些缓冲区的组织和管理方式直 接影响到高速缓存中的命中率,进而影响到运行时的效率。试想,假定我们运用最先符合(first fit)的方法,从一个由存储空间片段构成的队列中分配缓冲区。在这样的过程中,当一个片段 不能满足要求而顺着指针往下看下一个片段的数据结构时,如果该数据结构每次都在不同的页 面中,因而每次都不能命中,而要从内存装入到高速缓存,那么可想而知,其效率显然就要打 折扣了。 不适合多处理器公用内存的情况。 实际上,如何有效地管理缓冲空间,很久以来就是一个热门的研究课题。在 90 年代前期,在 solaris 2.4 操作系统(Unix 的一个变种)中,采用了一种称为“slab”的缓冲区分配和管理方法(slab 的原意是 大块的混凝土),在相当程度上克服了上述的缺点。而 Linux,也在其内核中采用了这种方法,并做了改 进。 从存储器分配的角度讲,slab 与各种数据结构分别建立缓冲池相似,也与以前我们看到过的按大小 划分管理区(zone)的方法相似,但是也有重要的不同。 在 slab 方法中,每种重要的数据结构都有自己专用的缓冲区队列,每种数据结构都有相应的“构造” (constructor)和“拆除”(destructor)函数。同时,还借用面向对象程序设计技术中的名词,不再称“结 构”而称为“对象”(object)。缓冲区队列中的各个对象在建立时用其“构造”函数进行初始化,所以一 经分配立即就能使用,而在释放时则恢复成原状。例如,对于其中的队列头成分来说(读者可参看 page 数据结构的定义,结构中有两个 struct list_head 成分),当将其从队列中摘除时自然就恢复成了原状。每 个队列中对象的个数时动态变化的,不够时可以增添。同时又定期地检查,将有富余的队列加以精简。 我们在 kswapd 的 do_try_to_free_pages()中曾经看到,调用函数 kmem_cache_reap(),为的就是从富余的 队列回收物理页面,只是当时我们没有细讲。其实,定期地检查和处理这些缓冲区队列,也是 kswapd 的一项功能。 此外,slab()管理方法还有一个特点,每种对象的缓冲区队列并非由各个对象直接构成,而是由一连 串的“大块”(slab)构成,而每个大块中则包含了若干同种的对象。一般而言,对象分两种,一种是大 对象,一种是小对象。所谓小对象,是指在一个页面中可以容纳下好几个对象的那一种,例如,一个 inodeLinux 内核源代码情景分析 的大小约 300 多个字节,因此一个页面中可以容纳 8 个以上的 inode,所以 inode 是小对象。内核中使用 的大多数数据结构都是这样的小对象。所以,我们先来看对小对象的组织和管理以及相应的 slab 结构。 先看用于某种假想小对象的一个 slab 块的结构示意图(图 2.8): 着色补偿区 对象区 对象链接数组 Slab描述结构slab_t 着色区 nextprev 图2.8 小对象slab结构示意图 此处先对上列示意图作几点说明,详细情况则随着代码的阅读再逐步深入: 一个 slab 可能由 1 个、2 个、4 个、…最多 32 个连续的物理页面构成。slab 的具体大小因对象 的大小而异,初始化时通过计算得出最合适的大小。 在每个 slab 的前端是该 slab 的描述结构 slab_t。用于同一种对象的多个 slab 通过描述结构中的 队列头形成一条双向链队列。每个 slab 双向链队列在逻辑上分成三截,第一截是各个 slab 上所 有的对象都已分配使用的;第二截是各个 slab 上的对象已经部分地分配使用;最后一截是各个 slab 上的全部对象都处于空闲状态。 每个 slab 上都有一个对象区,这是个对象数据结构的数组,以对象的序号为下标就可得到具体 对象的起始地址。 每个 slab 上还有个对象链接数组,用来实现一个空闲对象链。 同时,每个 slab 的描述结构中都有一个字段,表明该 slab 上的第一个空闲对象。这个字段与对 象链接数组结合在一起形成了一个空闲对象链。 在 slab 描述结构中还有一个已经分配使用的对象的计数器,当将一个空闲的对象分配使用时, 就将 slab 控制结构中的计数器加 1,并将该对象从空闲队列中脱链。 当释放一个对象时,只需要调整链接数组中的相应元素以及 slab 描述结构中的计数器,并且根 据该 slab 的使用情况而调整其在 slab 队列中的位置(例如,如果 slab 上所有的对象都已分配使 用,就要将该 slab 从第二截转移到第一截去)。 每个 slab 的头部有一个小小的区域是不使用的,称为“着色区”(coloring area)。着色区的大小 使 slab 中的每个对象的起始地址都按高速缓存中的“缓冲行”(cache line)大小(80386 的一级 高速缓存中缓存行大小为 16 个字节,Pentium 为 32 个字节)对齐。每个 slab 都是从一个页面 2006-12-31 版权所有,侵权必究 第 131 页,共 1481 页 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 132 页,共 1481 页 边界开始的,所以本来就自然按高速缓存的缓冲行对齐,而着色区的设置只是将第一个对象的 起始地址往后推到另一个与缓冲行对齐的边界。同一个对象的缓冲队列中的各个 slab 的着色区 的大小尽可能地安排成不同的大小,使得不同 slab 上同一相对位置的对象的起始地址在高速缓 存中互相错开,这样可以改善高速缓存的效率。 每个 slab 上最后一个对象以后也有一个小小的废料区是不用的,这是对着色区大小的补偿,其 大小取决于着色区的大小以及 slab 与其每个对象的相对大小。但该区域与着色区的总和对于同 一种对象的 slab 是个常熟。 每个对象的大小基本上是所需数据结构的大小。只有当数据结构的大小不与高速缓存中的缓冲 行对齐时,才增加若干字节使其对齐。所以,一个 slab 上的所有对象的起始地址都必然是按高 速缓存中的缓冲行对齐的。 下面就是 slab 描述结构 slab_t 的定义,在 mm/slab.c 中: 00138: /* 00139: * slab_t 00140: * 00141: * Manages the objs in a slab. Placed either at the beginning of mem allocated 00142: * for a slab, or allocated from an general cache. 00143: * Slabs are chained into one ordered list: fully used, partial, then fully 00144: * free slabs. 00145: */ 00146: typedef struct slab_s { 00147: struct list_head list; 00148: unsigned long colouroff; 00149: void *s_mem; /* including colour offset */ 00150: unsigned int inuse; /* num of objs active in slab */ 00151: kmem_bufctl_t free; 00152: } slab_t; 这里的队列头 list 用来将一块 slab 链入一个专用缓冲区队列,colouroff 为本 slab 上着色区的大小, 指针 s_mem 指向对象区的起点,inuse 是已分配对象的计数器。最后,free 的值指明了空闲对象链中的 第一个对象,其实是个整数: 00110: /* 00111: * kmem_bufctl_t: 00112: * 00113: * Bufctl's are used for linking objs within a slab 00114: * linked offsets. 00115: * 00116: * This implementaion relies on "struct page" for locating the cache & 00117: * slab an object belongs to. 00118: * This allows the bufctl structure to be small (one int), but limits 00119: * the number of objects a slab (not a cache) can contain when off-slab 00120: * bufctls are used. The limit is the size of the largest general cache 00121: * that does not use off-slab slabs. 00122: * For 32bit archs with 4 kB pages, is this 56. 00123: * This is not serious, as it is only for large objects, when it is unwise Linux 内核源代码情景分析 00124: * to have too many per slab. 00125: * Note: This limit can be raised by introducing a general cache whose size 00126: * is less than 512 (PAGE_SIZE<<3), but greater than 256. 00127: */ 00128: 00129: #define BUFCTL_END 0xffffFFFF 00130: #define SLAB_LIMIT 0xffffFFFE 00131: typedef unsigned int kmem_bufctl_t; 在空闲对象链接数组中,链内每一个对象所对应元素的值为下一个对象的序号,最后一个对象所对 应元素的值为 BUFCTL_END。 为每种对象建立的 slab 队列都有个队头,其控制结构为 kmem_cache_t。该数据结构中除用来维护 slab 队列的各种指针外,还记录了适用于队列中每个 slab 的各种参数,以及两个函数指针:一个是对象 的构筑函数(constructor),另一个是拆除函数(destructor)。有趣的是,像其他数据结构一样,每种对象 的 slab 对头也是在 slab 上。系统中有个总的 slab 队列,其对象是各个其他对象的 slab 队头,其队头则也 是一个 kmem_cache_t 结构,称为 cache_cache。 这样就形成了一种层次式的树形结构: 总根 cache_cache 是一个 kmem_cache_t 结构,用来维持第一层 slab 队列,这些 slab 上的对象都 是 kmem_cache_t 数据结构。 每个第一层 slab 上的每个对象,即 kmem_cache_t 数据结构都是队头,用来维持一个二层 slab 队列。 第二层 slab 队列基本上都是为某种对象,即数据结构专用的。 每个第二层 slab 上都维持着一个空闲对象队列。 总体的组织结构入下页图 2.9 所示。 slab_t slab slab_t slab slab_t slab slab_t slab kmem_cache_t cache_cache kmem_cache_t vm_area_struct slab list inode slab list mm_struct slab list 图2.9 小对象缓冲区结构示意图 从图 2.9 中可以看出,最高的层次是 slab 队列 cache_cache,队列的每个 slab 载有若干个 kmem_cache_t 数据结构。而每个这样的数据结构又是某种数据结构(例如 inode、vm_area_struct、mm_struct,乃至于 2006-12-31 版权所有,侵权必究 第 133 页,共 1481 页 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 134 页,共 1481 页 IP 网络信息保等等)缓冲区的 slab 队列的头部。这样,当要分配一个某种数据结构的缓冲区时,就只要 指明是从哪一个队列中分配,而不需要说明缓冲区的大小,并且不需要初始化了。具体的函数是: void * kmem_cache_alloc (kmem_cache_t *cachep, int flags); void kmem_cache_free (kmem_cache_t *cachep, void *objp); 所以,当需要分配一个具有专用 slab 队列的数据结构时,应该通过 kmem_cache_alloc()分配。例如, 我们在本章中看到过的 mm_struct、vm_area_struct、file、dentry、inode 等常用的数据结构,就都有专用 的 slab 队列,而应通过 kmem_cache_alloc()分配。 当数据结构比较大,因而不属于“小对象”时,slab 的结构略有不同。不同之处是不将 slab 的控制 结构放在它所代表的 slab 上,而是将其游离出来,集中放在另外的 slab 上。由于在 slab 的控制结构中有 一个指针指向相应 slab 上的第一个对象,所以逻辑上是一样的。其实,这酒是将控制结构与控制对象相 分离的一般模式。打个比方,载有“小对象”的 slab 就好像是随身携带着的户口本,而载有“大对象” 的 slab 就好像是将户口本集中存放在派出所里或者是某个代理机构里。此时,当对象的大小恰好是物理 页面的 1/2、1/4 或 1/8 时,将依附于每个对象的链接指针紧挨着放在一起会造成 slab 空间上的重大浪费, 所以在特殊情况下,将链接指针也从 slab 上游离出来集中存放,以提高 slab 的空间使用率。 不过,并非内核中使用的所有数据结构都有必要拥有专用的缓冲区队列,一些不太常用、初始化开 销也不大的数据结构还是可以合用一个通用的缓冲区分配机制。所以,Linux 内核中还有一种既类似于 物理页面分配种采用的按大小分区,又采用 slab 方式管理的通用缓冲池,称为“slab_cache”。slab_cachede 的结构与 cache_cache 大同小异,只不过其顶层不是一个队列而是一个结构数组(这是由于 slab_cache 相对来说比较静态),数组中的每一个元素指向一个不同的 slab 队列。这些 slab 队列的不同之处仅在于 所载对象的大小。最小的是 32,然后依次是 64、128、直至 128K(也就是 32 个页面)。从通用缓冲池中 分配和释放缓冲区的函数为: void * kmalloc (size_t size, int flags); void kfree (const void *objp); 所以,当需要分配一个不具有专用 slab 队列的数据结构而又不必为之使用整个页面时,就因该用过 kmalloc()分配。这一般都是些细小而又不常用的数据结构,例如第 5 章中安装文件系统时使用的 vfsmount() 数据结构就是这样。如果数据结构的大小接近于一个页面,则也可以干脆就通过 alloc_pages()为之分配 一个页面。 顺便提一下,内核中还有一组与内存分配有关的函数 vmalloc()和 vfree(): static inline void * vmalloc (unsigned long size); void vfree(void * addr); 函数 vmalloc()从内核的虚存空间(3GB 以上)分配一块虚存以及相应的物理内存,类似于系统调用 brk()。不过 brk()是由进程在用户空间启动并从用户空间中分配的,而 vmalloc()则是在系统空间,也就是 内核中启动,从内核空间中分配的。由 vmalloc()分配的页面空间不会被 kswapd 换出,因为 kswapd 只扫 描各个进程的用户空间,而根本就看不到通过 vmalloc()分配的页面表项。至于通过 kmalloc()分配的数据 结构,则 kswapd 只是从各个 slab 队列中寻找和收集空闲不用的 slab,并释放所占用的页面,但是不会 将尚在使用中的 slab 所占据的页面换出。由于 vmalloc()与我们后面要讲到的 ioremap()非常相似,这里就 不讲了。 在讲解内核缓冲区的分配之前,我们先接手缓冲区队列的建立。 2.10.1. 专用缓冲区队列的建立 本来,虚存空间结构 vm_area_struct 的专用缓冲区队列就是一个很好的实例,读者都已经熟悉了这Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 135 页,共 1481 页 个数据结构的使用。但是,到现在为止,Linux 内核中多数专用缓冲区的建立都用 NULL 作为构造函数 (constructor)的指针,也就是说并没有充分利用 slab 管理机制所提供的好处(相对来说,slab 是比较新 的计数),似乎不够典型。所以,我们从内核的网络驱动子系统中选择了一个例子,这是在 net/core/skbuff.c 中定义的: 00473: void __init skb_init(void) 00474: { 00475: int i; 00476: 00477: skbuff_head_cache = kmem_cache_create("skbuff_head_cache", 00478: sizeof(struct sk_buff), 00479: 0, 00480: SLAB_HWCACHE_ALIGN, 00481: skb_headerinit, NULL); 00482: if (!skbuff_head_cache) 00483: panic("cannot create skbuff cache"); 00484: 00485: for (i=0; i kmem_cache_alloc()] 01506: void * kmem_cache_alloc (kmem_cache_t *cachep, int flags) 01507: { 01508: return __kmem_cache_alloc(cachep, flags); 01509: } [alloc_skb() > kmem_cache_alloc() > __kmem_cache_alloc()] 01291: static inline void * __kmem_cache_alloc (kmem_cache_t *cachep, int flags) 01292: { 01293: unsigned long save_flags; 01294: void* objp; 01295: 01296: kmem_cache_alloc_head(cachep, flags); 01297: try_again: 01298: local_irq_save(save_flags); 01299: #ifdef CONFIG_SMP 01300: { 01301: cpucache_t *cc = cc_data(cachep); 01302: 01303: if (cc) { 01304: if (cc->avail) { 01305: STATS_INC_ALLOCHIT(cachep); 01306: objp = cc_entry(cc)[--cc->avail]; 01307: } else { 01308: STATS_INC_ALLOCMISS(cachep); 01309: objp = kmem_cache_alloc_batch(cachep,flags); 01310: if (!objp) 01311: goto alloc_new_slab_nolock; 01312: } 01313: } else { 01314: spin_lock(&cachep->spinlock); 01315: objp = kmem_cache_alloc_one(cachep); 01316: spin_unlock(&cachep->spinlock); 01317: } 01318: } 01319: #else 01320: objp = kmem_cache_alloc_one(cachep); 01321: #endif 01322: local_irq_restore(save_flags); 01323: return objp; 01324: alloc_new_slab: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 139 页,共 1481 页 01325: #ifdef CONFIG_SMP 01326: spin_unlock(&cachep->spinlock); 01327: alloc_new_slab_nolock: 01328: #endif 01329: local_irq_restore(save_flags); 01330: if (kmem_cache_grow(cachep, flags)) 01331: /* Someone may have stolen our objs. Doesn't matter, we'll 01332: * just come back here again. 01333: */ 01334: goto try_again; 01335: return NULL; 01336: } 首先,alloc_skb()中的指针 skbuff_head_cache 是个全局变量,指向相应的 slab 队列(这里是 sk_buff 结构的 slab 队列)的队头,因而这里的参数 cachep 也指向这个队列。 程序中的 kmem_cache_alloc_head()是为调试而设计的,在实际运行的系统中是空函数。我们在这里 也不关心多处理器 SMP 结构,所以这里的关键性的操作就是 kmem_cache_alloc_one(),这是一个宏操作, 其定义为: 01246: /* 01247: * Returns a ptr to an obj in the given cache. 01248: * caller must guarantee synchronization 01249: * #define for the goto optimization 8-) 01250: */ 01251: #define kmem_cache_alloc_one(cachep) \ 01252: ({ \ 01253: slab_t *slabp; \ 01254: \ 01255: /* Get slab alloc is to come from. */ \ 01256: { \ 01257: struct list_head* p = cachep->firstnotfull; \ 01258: if (p == &cachep->slabs) \ 01259: goto alloc_new_slab; \ 01260: slabp = list_entry(p,slab_t, list); \ 01261: } \ 01262: kmem_cache_alloc_one_tail(cachep, slabp); \ 01263: }) 上面__kmem_cache_alloc()的代码一定要和这个宏操作结合起来看才能明白。从定一种可以看到,第 一步是通过 slab 队列头中的指针 firstnotfull,找到该队列中第一个含有空闲对象的 slab。如果这个指针指 向 slab 队列的链头(不是链中的第一个 slab),那就表示队列中已经没有含有空闲对象的 slab,所以就转 到__kmem_cache_alloc()中的标号 alloc_new_slab 处(1324 行),进一步扩充该 slab 队列。 如果找到了含有空闲对象的 slab,就 调 用 kmem_cache_alloc_one_tail()分配一个空闲对象并返回其指 针: [alloc_skb() > kmem_cache_alloc() > __kmem_cache_alloc() > kmem_cache_alloc_one_tail()] Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 140 页,共 1481 页 01211: static inline void * kmem_cache_alloc_one_tail (kmem_cache_t *cachep, 01212: slab_t *slabp) 01213: { 01214: void *objp; 01215: 01216: STATS_INC_ALLOCED(cachep); 01217: STATS_INC_ACTIVE(cachep); 01218: STATS_SET_HIGH(cachep); 01219: 01220: /* get obj pointer */ 01221: slabp->inuse++; 01222: objp = slabp->s_mem + slabp->free*cachep->objsize; 01223: slabp->free=slab_bufctl(slabp)[slabp->free]; 01224: 01225: if (slabp->free == BUFCTL_END) 01226: /* slab now full: move to next slab for next alloc */ 01227: cachep->firstnotfull = slabp->list.next; 01228: #if DEBUG …………………… 01242: #endif 01243: return objp; 01244: } 如前所述,数据结构 slab_t 中的 free 记录着下一次可以分配的空闲对象的序号,而 s_mem 则指向 slab 中的对象区,所以根据这些数据和本专用队列的对象的大小,就可以计算出该空闲对象的起始地址。然 后,就通过宏操作 slab_bufctl()改变字段 free 的值,使它指明下一个空闲对象的序号。 00154: #define slab_bufctl(slabp) \ 00155: ((kmem_bufctl_t *)(((slab_t*)slabp)+1)) 这个宏操作返回一个 kmem_bufctl_t 数组的地址,这个数组就在 slab 中数据结构 slab_t 的上方,紧 挨着数据结构 slab_t。该数组以当前对象的序号为下标,而数组元素的值则表明下一个空闲对象的序号。 改变了 slab_t 中的 free 字段的值,就因含着当前对象已被分配。 如果达到了 slab 的末尾 BUFCTL_END,就要调整该 slab 队列的指针 firstnotfull,使它指向队列中的 下一个 slab。 不过,我们假定 slab 队列中已经不存在含有空闲对象的 slab,所以要转到前面代码中的标号 alloc_new_slab 处,通过 kmem_cache_grow()来分配一块新的 slab,使缓冲区队列“生长”起来。函数 kmem_cache_grow()的代码也在 mm/slab.c 中: [alloc_skb() > kmem_cache_alloc() > __kmem_cache_alloc() > kmem_cache_grow()] 01066: /* 01067: * Grow (by 1) the number of slabs within a cache. This is called by 01068: * kmem_cache_alloc() when there are no active objs left in a cache. 01069: */ 01070: static int kmem_cache_grow (kmem_cache_t * cachep, int flags) 01071: { Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 141 页,共 1481 页 01072: slab_t *slabp; 01073: struct page *page; 01074: void *objp; 01075: size_t offset; 01076: unsigned int i, local_flags; 01077: unsigned long ctor_flags; 01078: unsigned long save_flags; 01079: 01080: /* Be lazy and only check for valid flags here, 01081: * keeping it out of the critical path in kmem_cache_alloc(). 01082: */ 01083: if (flags & ~(SLAB_DMA|SLAB_LEVEL_MASK|SLAB_NO_GROW)) 01084: BUG(); 01085: if (flags & SLAB_NO_GROW) 01086: return 0; 01087: 01088: /* 01089: * The test for missing atomic flag is performed here, rather than 01090: * the more obvious place, simply to reduce the critical path length 01091: * in kmem_cache_alloc(). If a caller is seriously mis-behaving they 01092: * will eventually be caught here (where it matters). 01093: */ 01094: if (in_interrupt() && (flags & SLAB_LEVEL_MASK) != SLAB_ATOMIC) 01095: BUG(); 01096: 01097: ctor_flags = SLAB_CTOR_CONSTRUCTOR; 01098: local_flags = (flags & SLAB_LEVEL_MASK); 01099: if (local_flags == SLAB_ATOMIC) 01100: /* 01101: * Not allowed to sleep. Need to tell a constructor about 01102: * this - it might need to know... 01103: */ 01104: ctor_flags |= SLAB_CTOR_ATOMIC; 01105: 01106: /* About to mess with non-constant members - lock. */ 01107: spin_lock_irqsave(&cachep->spinlock, save_flags); 01108: 01109: /* Get colour for the slab, and cal the next value. */ 01110: offset = cachep->colour_next; 01111: cachep->colour_next++; 01112: if (cachep->colour_next >= cachep->colour) 01113: cachep->colour_next = 0; 01114: offset *= cachep->colour_off; 01115: cachep->dflags |= DFLGS_GROWN; 01116: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 142 页,共 1481 页 01117: cachep->growing++; 01118: spin_unlock_irqrestore(&cachep->spinlock, save_flags); 01119: 01120: /* A series of memory allocations for a new slab. 01121: * Neither the cache-chain semaphore, or cache-lock, are 01122: * held, but the incrementing c_growing prevents this 01123: * cache from being reaped or shrunk. 01124: * Note: The cache could be selected in for reaping in 01125: * kmem_cache_reap(), but when the final test is made the 01126: * growing value will be seen. 01127: */ 01128: 01129: /* Get mem for the objs. */ 01130: if (!(objp = kmem_getpages(cachep, flags))) 01131: goto failed; 01132: 01133: /* Get slab management. */ 01134: if (!(slabp = kmem_cache_slabmgmt(cachep, objp, offset, local_flags))) 01135: goto opps1; 01136: 01137: /* Nasty!!!!!! I hope this is OK. */ 01138: i = 1 << cachep->gfporder; 01139: page = virt_to_page(objp); 01140: do { 01141: SET_PAGE_CACHE(page, cachep); 01142: SET_PAGE_SLAB(page, slabp); 01143: PageSetSlab(page); 01144: page++; 01145: } while (--i); 01146: 01147: kmem_cache_init_objs(cachep, slabp, ctor_flags); 01148: 01149: spin_lock_irqsave(&cachep->spinlock, save_flags); 01150: cachep->growing--; 01151: 01152: /* Make slab active. */ 01153: list_add_tail(&slabp->list,&cachep->slabs); 01154: if (cachep->firstnotfull == &cachep->slabs) 01155: cachep->firstnotfull = &slabp->list; 01156: STATS_INC_GROWN(cachep); 01157: cachep->failures = 0; 01158: 01159: spin_unlock_irqrestore(&cachep->spinlock, save_flags); 01160: return 1; 01161: opps1: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 143 页,共 1481 页 01162: kmem_freepages(cachep, objp); 01163: failed: 01164: spin_lock_irqsave(&cachep->spinlock, save_flags); 01165: cachep->growing--; 01166: spin_unlock_irqrestore(&cachep->spinlock, save_flags); 01167: return 0; 01168: } 函数 kmem_cache_grow()根据队列头中的参数 gfporder 分配若干连续的物理内存页面,并将这些页 面构造成 slab,链入给定的 slab 队列。最参数进行了一些检查以后,就计算出下一块 slab 应有的着色区 大小。然后,通过 kmem_getpages()分配用于具体对象缓冲区的页面,这个函数最终调用 alloc_pages()分 配空闲页面。分配了用于对象本身的内存页面后,还要通过 kmem_cache_slabmgmt()建立起 slab 的管理 信息。其代码在 mm/slab.c 中: [alloc_skb() > kmem_cache_alloc() > __kmem_cache_alloc() > kmem_cache_grow() > kmem_cache_slabmgmt()] 00996: /* Get the memory for a slab management obj. */ 00997: static inline slab_t * kmem_cache_slabmgmt (kmem_cache_t *cachep, 00998: void *objp, int colour_off, int local_flags) 00999: { 01000: slab_t *slabp; 01001: 01002: if (OFF_SLAB(cachep)) { 01003: /* Slab management obj is off-slab. */ 01004: slabp = kmem_cache_alloc(cachep->slabp_cache, local_flags); 01005: if (!slabp) 01006: return NULL; 01007: } else { 01008: /* FIXME: change to 01009: slabp = objp 01010: * if you enable OPTIMIZE 01011: */ 01012: slabp = objp+colour_off; 01013: colour_off += L1_CACHE_ALIGN(cachep->num * 01014: sizeof(kmem_bufctl_t) + sizeof(slab_t)); 01015: } 01016: slabp->inuse = 0; 01017: slabp->colouroff = colour_off; 01018: slabp->s_mem = objp+colour_off; 01019: 01020: return slabp; 01021: } 如前所述,小对象的 slab 控制结构 slab_t 与对象本身共存于同一 slab 上,而大对象的控制结构则游 离于 slab 之外。但是,大对象的控制结构也是 slab_t,存在于为这种数据结构专设的 slab 上,也有其专 用的 slab 队列。所以,如果使大对象就通过 kmem_cache_alloc()分配一个 slab_t,否则就用小对象 slabLinux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 144 页,共 1481 页 低端的一部分空间用作其控制结构,不过在此之前要空出一小块着色区。注意这里 1012 行和 1017 行所 引用的 colour_off 不是用一个数值,这个变量的值在 1013 行做了调整,在原来的数之上增加了对象链接 数组的大小以及控制结构 slab_t 的大小。所以,slab->s_mem 总是指向 slab 上对象区的起点。 对分配用于 slab 的每个页面的 page 数据结构,要通过宏操作 SET_PAGE_CACHE 和 SET_PAGE_SLAB,设置其链接指针 prev 和 next,使它们分别指向所属的 slab 和 slab 队列。同时,还要 把 page 结构中的 PG_slab 标志位设成 1,以表明该页面的用途。 最后,通过 kmem_cache_init_objs()进行 slab 的初始化: [alloc_skb() > kmem_cache_alloc() > __kmem_cache_alloc() > kmem_cache_grow() > kmem_cache_init_objs()] 01023: static inline void kmem_cache_init_objs (kmem_cache_t * cachep, 01024: slab_t * slabp, unsigned long ctor_flags) 01025: { 01026: int i; 01027: 01028: for (i = 0; i < cachep->num; i++) { 01029: void* objp = slabp->s_mem+cachep->objsize*i; 01030: #if DEBUG 01031: if (cachep->flags & SLAB_RED_ZONE) { 01032: *((unsigned long*)(objp)) = RED_MAGIC1; 01033: *((unsigned long*)(objp + cachep->objsize - 01034: BYTES_PER_WORD)) = RED_MAGIC1; 01035: objp += BYTES_PER_WORD; 01036: } 01037: #endif 01038: 01039: /* 01040: * Constructors are not allowed to allocate memory from 01041: * the same cache which they are a constructor for. 01042: * Otherwise, deadlock. They must also be threaded. 01043: */ 01044: if (cachep->ctor) 01045: cachep->ctor(objp, cachep, ctor_flags); 01046: #if DEBUG 01047: if (cachep->flags & SLAB_RED_ZONE) 01048: objp -= BYTES_PER_WORD; 01049: if (cachep->flags & SLAB_POISON) 01050: /* need to poison the objs */ 01051: kmem_poison_obj(cachep, objp); 01052: if (cachep->flags & SLAB_RED_ZONE) { 01053: if (*((unsigned long*)(objp)) != RED_MAGIC1) 01054: BUG(); 01055: if (*((unsigned long*)(objp + cachep->objsize - 01056: BYTES_PER_WORD)) != RED_MAGIC1) 01057: BUG(); 01058: } Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 145 页,共 1481 页 01059: #endif 01060: slab_bufctl(slabp)[i] = i+1; 01061: } 01062: slab_bufctl(slabp)[i-1] = BUFCTL_END; 01063: slabp->free = 0; 01064: } 这里的初始化包括了对具体对象构造函数的调用。对于 sk_buff 数据结构,这个函数就是 skb_headerinit()。此处,代码中的 1060 行是对链接数组中各个元素的初始化。 缓冲区队列“成长”了一些以后,就一定有空闲缓冲区可供分配了。所以转回标号 try_again 处再试 一遍(见__kmem_cache_alloc()中的 1334 行)。 这样,就构成了一个多层次的缓冲机制。位于最高层的是缓冲区的分配,在我们这个情景中就是 alloc_skb(),具体则是先通过 skb_head_from_pool(),从缓冲池,即已经分配的 slab 块中分配。如果失败 的话,就往下跑一层从 slab 队列中通过 kmem_cache_alloc()分配。要是 slab 队列中已经没有载有空闲缓 冲区的 slab,那就再往下跑一层,通过 kmem_cache_grow(),分配若干页面而构筑出一个 slab 块。 那么,缓冲区队列是否单调地成长而不缩小呢?我们在以前提到过,kswapd 定时地调用 kmem_cache_reap()来“收割”。也就是说,依次检查若干专用缓冲区 slab 队列,看看是否由完全空闲的 slab 存在。有的话就将这些 slab 占用的内存页面释放。 再来看专用缓冲区的释放,只是由 kmem_cache_free()完成的。其代码在 mm/slab.c 中: 01554: void kmem_cache_free (kmem_cache_t *cachep, void *objp) 01555: { 01556: unsigned long flags; 01557: #if DEBUG …………………… 01561: #endif 01562: 01563: local_irq_save(flags); 01564: __kmem_cache_free(cachep, objp); 01565: local_irq_restore(flags); 01566: } 显然,操作的主体是__kernel_cache_free(),这里只是在操作期间把中断暂时关闭。 [kmem_cache_free() > __kmem_cache_free()] 01466: /* 01467: * __kmem_cache_free 01468: * called with disabled ints 01469: */ 01470: static inline void __kmem_cache_free (kmem_cache_t *cachep, void* objp) 01471: { 01472: #ifdef CONFIG_SMP 01473: cpucache_t *cc = cc_data(cachep); 01474: 01475: CHECK_PAGE(virt_to_page(objp)); 01476: if (cc) { Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 146 页,共 1481 页 01477: int batchcount; 01478: if (cc->avail < cc->limit) { 01479: STATS_INC_FREEHIT(cachep); 01480: cc_entry(cc)[cc->avail++] = objp; 01481: return; 01482: } 01483: STATS_INC_FREEMISS(cachep); 01484: batchcount = cachep->batchcount; 01485: cc->avail -= batchcount; 01486: free_block(cachep, 01487: &cc_entry(cc)[cc->avail],batchcount); 01488: cc_entry(cc)[cc->avail++] = objp; 01489: return; 01490: } else { 01491: free_block(cachep, &objp, 1); 01492: } 01493: #else 01494: kmem_cache_free_one(cachep, objp); 01495: #endif 01496: } 我们在这里不关心多处理器 SMP 结构,而函数 kmem_cache_free_one()的代码也在同一个文件中: [kmem_cache_free() > __kmem_cache_free() > kmem_cache_free_one()] 01367: static inline void kmem_cache_free_one(kmem_cache_t *cachep, void *objp) 01368: { 01369: slab_t* slabp; 01370: 01371: CHECK_PAGE(virt_to_page(objp)); 01372: /* reduces memory footprint 01373: * 01374: if (OPTIMIZE(cachep)) 01375: slabp = (void*)((unsigned long)objp&(~(PAGE_SIZE-1))); 01376: else 01377: */ 01378: slabp = GET_PAGE_SLAB(virt_to_page(objp)); 01379: 01380: #if DEBUG 01381: if (cachep->flags & SLAB_DEBUG_INITIAL) 01382: /* Need to call the slab's constructor so the 01383: * caller can perform a verify of its state (debugging). 01384: * Called without the cache-lock held. 01385: */ 01386: cachep->ctor(objp, cachep, SLAB_CTOR_CONSTRUCTOR|SLAB_CTOR_VERIFY); 01387: 01388: if (cachep->flags & SLAB_RED_ZONE) { Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 147 页,共 1481 页 01389: objp -= BYTES_PER_WORD; 01390: if (xchg((unsigned long *)objp, RED_MAGIC1) != RED_MAGIC2) 01391: /* Either write before start, or a double free. */ 01392: BUG(); 01393: if (xchg((unsigned long *)(objp+cachep->objsize - 01394: BYTES_PER_WORD), RED_MAGIC1) != RED_MAGIC2) 01395: /* Either write past end, or a double free. */ 01396: BUG(); 01397: } 01398: if (cachep->flags & SLAB_POISON) 01399: kmem_poison_obj(cachep, objp); 01400: if (kmem_extra_free_checks(cachep, slabp, objp)) 01401: return; 01402: #endif 01403: { 01404: unsigned int objnr = (objp-slabp->s_mem)/cachep->objsize; 01405: 01406: slab_bufctl(slabp)[objnr] = slabp->free; 01407: slabp->free = objnr; 01408: } 01409: STATS_DEC_ACTIVE(cachep); 01410: 01411: /* fixup slab chain */ 01412: if (slabp->inuse-- == cachep->num) 01413: goto moveslab_partial; 01414: if (!slabp->inuse) 01415: goto moveslab_free; 01416: return; 01417: 01418: moveslab_partial: 01419: /* was full. 01420: * Even if the page is now empty, we can set c_firstnotfull to 01421: * slabp: there are no partial slabs in this case 01422: */ 01423: { 01424: struct list_head *t = cachep->firstnotfull; 01425: 01426: cachep->firstnotfull = &slabp->list; 01427: if (slabp->list.next == t) 01428: return; 01429: list_del(&slabp->list); 01430: list_add_tail(&slabp->list, t); 01431: return; 01432: } 01433: moveslab_free: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 148 页,共 1481 页 01434: /* 01435: * was partial, now empty. 01436: * c_firstnotfull might point to slabp 01437: * FIXME: optimize 01438: */ 01439: { 01440: struct list_head *t = cachep->firstnotfull->prev; 01441: 01442: list_del(&slabp->list); 01443: list_add_tail(&slabp->list, &cachep->slabs); 01444: if (cachep->firstnotfull == &slabp->list) 01445: cachep->firstnotfull = t->next; 01446: return; 01447: } 01448: } 代码中的 CHECK_PAGE 只用于程序调试,在实际运行的系统中为空语句。根据待释放对象的地址 可以计算出其所在的页面。进一步,如前所述(见 kmem_cache_grow()中的 1142 行),页面的 page 结构 中链头 list 内,原用于队列链接的指针 prev,指向页面所属的 slab,所以通过宏操作 GET_PAGE_SLAB 就可以得到这个 slab 的指针。找到了对象所在的 slab,就可以通过其链接数组释放给定对象了(见 1404~ 1407 行)。同时,还要递减所属 slab 队列控制结构中非空闲对象的计数。递减以后有三种可能: 原来 slab 上没有空闲对象,而现在有了,所以要转到 moveslab_partial 处,把 slab 从队列中原 来的位置移到队列的第二截,即由指针 firstnotfull 所指的地方。 原来 slab 上就有空闲对象,而现在多有对象都空闲了,所以要转到 moveslab_free 处,把 slab 从队列中原来的为之移到队列的第三截,即队列的末尾。 原来 slab 上就有空闲对象,现在只不过是多了一个,但是也并没有全部空闲,所以不需要任何 改动。 可见,分配和释放专用缓冲区的开销都是很小的。这里还要指出,缓冲区的释放并不导致 slab 的释 放,空闲 slab 的释放是由 kswapd 等内核县城周期地调用 kmem_cache_reap()完成的。 看完了专用缓冲区的分配和释放,再看看通用缓冲区的分配。前面讲过,除各种专用的缓冲区队列 外,内核中还有一个通用的缓冲池 cache_size,里面根据缓冲区的大小而分成若干队列。用于通用缓冲 区分配的函数 kmalloc()是在 mm/slab.c 中定义的: 01511: /** 01512: * kmalloc - allocate memory 01513: * @size: how many bytes of memory are required. 01514: * @flags: the type of memory to allocate. 01515: * 01516: * kmalloc is the normal method of allocating memory 01517: * in the kernel. The @flags argument may be one of: 01518: * 01519: * %GFP_BUFFER - XXX 01520: * 01521: * %GFP_ATOMIC - allocation will not sleep. Use inside interrupt handlers. 01522: * 01523: * %GFP_USER - allocate memory on behalf of user. May sleep. Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 149 页,共 1481 页 01524: * 01525: * %GFP_KERNEL - allocate normal kernel ram. May sleep. 01526: * 01527: * %GFP_NFS - has a slightly lower probability of sleeping than %GFP_KERNEL. 01528: * Don't use unless you're in the NFS code. 01529: * 01530: * %GFP_KSWAPD - Don't use unless you're modifying kswapd. 01531: */ 01532: void * kmalloc (size_t size, int flags) 01533: { 01534: cache_sizes_t *csizep = cache_sizes; 01535: 01536: for (; csizep->cs_size; csizep++) { 01537: if (size > csizep->cs_size) 01538: continue; 01539: return __kmem_cache_alloc(flags & GFP_DMA ? 01540: csizep->cs_dmacachep : csizep->cs_cachep, flags); 01541: } 01542: BUG(); // too big size 01543: return NULL; 01544: } 这里通过一个 for 循环,在 cache_size 结构数组中由小到大扫描,找到第一个能满足缓冲区大小要求 的队列,然后就调用函数__kmem_cache_alloc()从该队列中分配一个缓冲区。而 kmem_cache_alloc()的作 用我们在前面已经简要地介绍了。 最后,我们来看看空闲 slab 的“收割”,即对构成空闲 slab 的页面的回收。以前我们看到过,内核 线程 kswapd 在周期性的运行中会调用 kmem_cache_reap()会后这些页面。这个函数的代码在 mm/slab.c 中: 01701: /** 01702: * kmem_cache_reap - Reclaim memory from caches. 01703: * @gfp_mask: the type of memory required. 01704: * 01705: * Called from try_to_free_page(). 01706: */ 01707: void kmem_cache_reap (int gfp_mask) 01708: { 01709: slab_t *slabp; 01710: kmem_cache_t *searchp; 01711: kmem_cache_t *best_cachep; 01712: unsigned int best_pages; 01713: unsigned int best_len; 01714: unsigned int scan; 01715: 01716: if (gfp_mask & __GFP_WAIT) 01717: down(&cache_chain_sem); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 150 页,共 1481 页 01718: else 01719: if (down_trylock(&cache_chain_sem)) 01720: return; 01721: 01722: scan = REAP_SCANLEN; 01723: best_len = 0; 01724: best_pages = 0; 01725: best_cachep = NULL; 01726: searchp = clock_searchp; 01727: do { 01728: unsigned int pages; 01729: struct list_head* p; 01730: unsigned int full_free; 01731: 01732: /* It's safe to test this without holding the cache-lock. */ 01733: if (searchp->flags & SLAB_NO_REAP) 01734: goto next; 01735: spin_lock_irq(&searchp->spinlock); 01736: if (searchp->growing) 01737: goto next_unlock; 01738: if (searchp->dflags & DFLGS_GROWN) { 01739: searchp->dflags &= ~DFLGS_GROWN; 01740: goto next_unlock; 01741: } 01742: #ifdef CONFIG_SMP 01743: { 01744: cpucache_t *cc = cc_data(searchp); 01745: if (cc && cc->avail) { 01746: __free_block(searchp, cc_entry(cc), cc->avail); 01747: cc->avail = 0; 01748: } 01749: } 01750: #endif 01751: 01752: full_free = 0; 01753: p = searchp->slabs.prev; 01754: while (p != &searchp->slabs) { 01755: slabp = list_entry(p, slab_t, list); 01756: if (slabp->inuse) 01757: break; 01758: full_free++; 01759: p = p->prev; 01760: } 01761: 01762: /* Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 151 页,共 1481 页 01763: * Try to avoid slabs with constructors and/or 01764: * more than one page per slab (as it can be difficult 01765: * to get high orders from gfp()). 01766: */ 01767: pages = full_free * (1<gfporder); 01768: if (searchp->ctor) 01769: pages = (pages*4+1)/5; 01770: if (searchp->gfporder) 01771: pages = (pages*4+1)/5; 01772: if (pages > best_pages) { 01773: best_cachep = searchp; 01774: best_len = full_free; 01775: best_pages = pages; 01776: if (full_free >= REAP_PERFECT) { 01777: clock_searchp = list_entry(searchp->next.next, 01778: kmem_cache_t,next); 01779: goto perfect; 01780: } 01781: } 01782: next_unlock: 01783: spin_unlock_irq(&searchp->spinlock); 01784: next: 01785: searchp = list_entry(searchp->next.next,kmem_cache_t,next); 01786: } while (--scan && searchp != clock_searchp); 01787: 01788: clock_searchp = searchp; 01789: 01790: if (!best_cachep) 01791: /* couldn't find anything to reap */ 01792: goto out; 01793: 01794: spin_lock_irq(&best_cachep->spinlock); 01795: perfect: 01796: /* free only 80% of the free slabs */ 01797: best_len = (best_len*4 + 1)/5; 01798: for (scan = 0; scan < best_len; scan++) { 01799: struct list_head *p; 01800: 01801: if (best_cachep->growing) 01802: break; 01803: p = best_cachep->slabs.prev; 01804: if (p == &best_cachep->slabs) 01805: break; 01806: slabp = list_entry(p,slab_t,list); 01807: if (slabp->inuse) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 152 页,共 1481 页 01808: break; 01809: list_del(&slabp->list); 01810: if (best_cachep->firstnotfull == &slabp->list) 01811: best_cachep->firstnotfull = &best_cachep->slabs; 01812: STATS_INC_REAPED(best_cachep); 01813: 01814: /* Safe to drop the lock. The slab is no longer linked to the 01815: * cache. 01816: */ 01817: spin_unlock_irq(&best_cachep->spinlock); 01818: kmem_slab_destroy(best_cachep, slabp); 01819: spin_lock_irq(&best_cachep->spinlock); 01820: } 01821: spin_unlock_irq(&best_cachep->spinlock); 01822: out: 01823: up(&cache_chain_sem); 01824: return; 01825: } 这个函数扫描 slab 队列的队列 cache_cache,从中发现可供“收割”的 slab 队列。不过,并不是每 次都扫描整个 cache_cache,而只是扫描其中的一部分 slab 队列,所以需要有个全局量来记录下一次扫描 的起点,这就是 clock_searchp: 00360: /* Place maintainer for reaping. */ 00361: static kmem_cache_t *clock_searchp = &cache_cache; 找到了可以“收割”的 slab 队列,也不是把它所有空闲的 slab 都全部回收,而是回收其中的大约 80%。 对于要回收的 slab,调用 kmem_slab_destroy()释放其各个页面,我们把这个函数留给读者自己阅读。 [kmem_cache_reap() > kmem_slab_destroy()] 00540: /* Destroy all the objs in a slab, and release the mem back to the system. 00541: * Before calling the slab must have been unlinked from the cache. 00542: * The cache-lock is not held/needed. 00543: */ 00544: static void kmem_slab_destroy (kmem_cache_t *cachep, slab_t *slabp) 00545: { 00546: if (cachep->dtor 00547: #if DEBUG 00548: || cachep->flags & (SLAB_POISON | SLAB_RED_ZONE) 00549: #endif 00550: ) { 00551: int i; 00552: for (i = 0; i < cachep->num; i++) { 00553: void* objp = slabp->s_mem+cachep->objsize*i; 00554: #if DEBUG 00555: if (cachep->flags & SLAB_RED_ZONE) { Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 153 页,共 1481 页 00556: if (*((unsigned long*)(objp)) != RED_MAGIC1) 00557: BUG(); 00558: if (*((unsigned long*)(objp + cachep->objsize 00559: -BYTES_PER_WORD)) != RED_MAGIC1) 00560: BUG(); 00561: objp += BYTES_PER_WORD; 00562: } 00563: #endif 00564: if (cachep->dtor) 00565: (cachep->dtor)(objp, cachep, 0); 00566: #if DEBUG 00567: if (cachep->flags & SLAB_RED_ZONE) { 00568: objp -= BYTES_PER_WORD; 00569: } 00570: if ((cachep->flags & SLAB_POISON) && 00571: kmem_check_poison_obj(cachep, objp)) 00572: BUG(); 00573: #endif 00574: } 00575: } 00576: 00577: kmem_freepages(cachep, slabp->s_mem-slabp->colouroff); 00578: if (OFF_SLAB(cachep)) 00579: kmem_cache_free(cachep->slabp_cache, slabp); 00580: } 2.11. 外部设备存储空间的地址映射 任何系统都免不了要有输入/输出,所以对外部设备的访问是 CPU 设计中的一个重要问题。一般来 说,对外部设备的访问有两种不同的形式,一种叫内存映射式(memory mapped),另一种叫 I/O 映射式 (I/O mapped)。在采用内存映射方式的 CPU 中,外部设备的存储单元,如控制寄存器、状态寄存器、 数据寄存器等等,是作为内存的一部分出现在系统中的。CPU 可以像访问一个内存单元一样的访问外部 设备的存储单元,所以不需要专门设立用于外设 I/O 的指令。从前的 PDP-11、后来的 M68K、PowerPC 等 CPU 都采用这种方式。而在采用 I/O 映射方式的系统中则不同,外部设备的存储单元与内存分属两个 不同的体系。访问内存的指令不能用来访问外部设备的存储单元,所以在 X86 CPU 中设立了专门的 IN 和 OUT 指令,但是用于 I/O 指令的“地址空间”相对来说是很小的。事实上,现在 X86 的 I/O 地址空间 已经非常拥挤。 但是,随着计算机技术的发展,人们发现单纯的 I/O 映射方式是不能满足要求的。此种方式只适合 于早期的计算机技术,那时候一个外设通常都只有几个寄存器,通过这几个寄存器就可以完成对外设的 所有操作了。而现在的情况却大不一样。例如,在 PC 机上可以插上一块图像卡,带有 2MB 的存储器, 甚至还可能带有一块 ROM,里面装有可执行代码。自从 PCI 总线出现以后,这个问题就更突出了。所 以,不管 CPU 的设计采用 I/O 映射或是存储器映射,都必须要有将外设卡上的存储器映射到内存空间, 实际上是虚拟空间的手段。在 Linux 内核种,这样的映射是通过函数 ioremap()来建立的。 对于内存页面的管理,通常我们都是先在虚存空间分配一个虚存区间,然后为此区间分配相应的物 理内存页面并建立起映射。而且这样的映射也并不是一次就建立完毕,可以在访问这些虚存页面引起页 面异常时逐步地建立。但是,ioremap()则不同,首先,我们先有一个物理存储区间,其地址就是外设卡Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 154 页,共 1481 页 上的存储器出现在总线上的地址。这地址未必就是这些存储单元在外设卡上局部的物理地址,而是在总 线上由 CPU 所“看到”的地址,这中间很可能已经经历了一次地址映射,但这种映射对于 CPU 来说是 透明的。所以有时把这种地址称为“总线地址”。举个例来说,如果有一块“智能图形卡”,卡上有个微 处理器。对于卡上的微处理器来说,卡上的存储器是从地址 0 开始的,这酒是卡上局部的物理地址。但 是将这块图形卡插到 PC 的一个 PCI 总线插槽上,由 PC 的 CPU 所看到的这片物理存储区间的地址可能 是从 0x 0000 f000 0000 0000 开始的,这中间已经有了一次映射。可是,从系统(PC)的 CPU 的角度说, 它只知道这片物理存储区间是从 0x 0000 f000 0000 0000 开始的,这酒是该区间的物理地址,或者说“总 线地址”。在 Linux 系统中,CPU 不能按物理地址来访问存储空间,而必须使用虚拟地址,所以必须“反 向”地从物理地址出发找到一片虚存空间并建立起映射。其次,这样的需求只发生于对外部设备的操作, 而这是内核的事,所以相应的虚存空间是在系统空间(3GB 以上)。在以前的 Linux 内核版本中,这个 函数称为 vremap(),后来改成了 ioremap(),也突出地反映了这一点。还有,这样的页面当然不服从动态 的物理内存页面分配,也不服从 kswapd 的换出。 先看 ioremap(),这是一个 inline 函数,定义于 include/asm-i386/io.h: 00140: extern inline void * ioremap (unsigned long offset, unsigned long size) 00141: { 00142: return __ioremap(offset, size, 0); 00143: } 实际的操作由__ioremap()完成,是在 arch/i386/mm/ioremap.c 中定义的: [ioremap() > __ioremap()] 00092: /* 00093: * Remap an arbitrary physical address space into the kernel virtual 00094: * address space. Needed when the kernel wants to access high addresses 00095: * directly. 00096: * 00097: * NOTE! We need to allow non-page-aligned mappings too: we will obviously 00098: * have to convert them into an offset in a page-aligned mapping, but the 00099: * caller shouldn't need to know that small detail. 00100: */ 00101: void * __ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags) 00102: { 00103: void * addr; 00104: struct vm_struct * area; 00105: unsigned long offset, last_addr; 00106: 00107: /* Don't allow wraparound or zero size */ 00108: last_addr = phys_addr + size - 1; 00109: if (!size || last_addr < phys_addr) 00110: return NULL; 00111: 00112: /* 00113: * Don't remap the low PCI/ISA area, it's always mapped.. 00114: */ 00115: if (phys_addr >= 0xA0000 && last_addr < 0x100000) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 155 页,共 1481 页 00116: return phys_to_virt(phys_addr); 00117: 00118: /* 00119: * Don't allow anybody to remap normal RAM that we're using.. 00120: */ 00121: if (phys_addr < virt_to_phys(high_memory)) { 00122: char *t_addr, *t_end; 00123: struct page *page; 00124: 00125: t_addr = __va(phys_addr); 00126: t_end = t_addr + (size - 1); 00127: 00128: for(page = virt_to_page(t_addr); page <= virt_to_page(t_end); page++) 00129: if(!PageReserved(page)) 00130: return NULL; 00131: } 00132: 00133: /* 00134: * Mappings have to be page-aligned 00135: */ 00136: offset = phys_addr & ~PAGE_MASK; 00137: phys_addr &= PAGE_MASK; 00138: size = PAGE_ALIGN(last_addr) - phys_addr; 00139: 00140: /* 00141: * Ok, go for it.. 00142: */ 00143: area = get_vm_area(size, VM_IOREMAP); 00144: if (!area) 00145: return NULL; 00146: addr = area->addr; 00147: if (remap_area_pages(VMALLOC_VMADDR(addr), phys_addr, size, flags)) { 00148: vfree(addr); 00149: return NULL; 00150: } 00151: return (void *) (offset + (char *)addr); 00152: } 首先是一些例行检查,常常称为“sanity check”,或者说“健康检查”、“卫生检查”。其中 109 行检 查的是区间的大小既不为 0,也不能太大而越出了 32 位地址空间的限制。物理地址 0xa000 至 0x100000 用于 VGA 卡和 BIOS,这是系统初始化时就映射好了的,不能侵犯到这个区间中去。121 行中的 high_memory 是在初始化时,根据检测到的物理内存的大小而设置的物理内存地址的上限(所对应的虚 拟地址)。如果所要求的 phys_addr 小于这个上限的话,就表示与系统的物理内存有冲突了,除非相应的 物理页面原来就是保留着的空洞。在通过了这些检查以后,还要保证该物理地址是按页面边界对齐的 (136~138 行)。 完成了这些准备以后,这才“言归正传”。首先是要找到一片虚存地址空间。前面讲过,这片区间属Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 156 页,共 1481 页 于内核,而不属于任何一个特定的进程,所以不是在某个进程的 mm_struct 结构中的虚存区间队列中去 寻找,而是从属于内核的虚存区间队列中去寻找。函数 get_vm_area()是在 mm/vmalloc.c 中定义的: [ioremap() > __ioremap() > get_vm_area()] 00168: struct vm_struct * get_vm_area(unsigned long size, unsigned long flags) 00169: { 00170: unsigned long addr; 00171: struct vm_struct **p, *tmp, *area; 00172: 00173: area = (struct vm_struct *) kmalloc(sizeof(*area), GFP_KERNEL); 00174: if (!area) 00175: return NULL; 00176: size += PAGE_SIZE; 00177: addr = VMALLOC_START; 00178: write_lock(&vmlist_lock); 00179: for (p = &vmlist; (tmp = *p) ; p = &tmp->next) { 00180: if ((size + addr) < addr) { 00181: write_unlock(&vmlist_lock); 00182: kfree(area); 00183: return NULL; 00184: } 00185: if (size + addr < (unsigned long) tmp->addr) 00186: break; 00187: addr = tmp->size + (unsigned long) tmp->addr; 00188: if (addr > VMALLOC_END-size) { 00189: write_unlock(&vmlist_lock); 00190: kfree(area); 00191: return NULL; 00192: } 00193: } 00194: area->flags = flags; 00195: area->addr = (void *)addr; 00196: area->size = size; 00197: area->next = *p; 00198: *p = area; 00199: write_unlock(&vmlist_lock); 00200: return area; 00201: } 内核为自己保持一个虚存区间队列 vmlist,这是由一串 vm_struct 数据结构组成的一个单链队列。这 里的 vm_struct 和 vmlist 都是由内核专用的。vm_struct 从概念上说类似于供进程使用的 vm_area_struct, 但要简单得多,定义于 include/linux/vmalloc.h 和 mm/vmalloc.c 中: 00014: struct vm_struct { 00015: unsigned long flags; 00016: void * addr; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 157 页,共 1481 页 00017: unsigned long size; 00018: struct vm_struct * next; 00019: }; 00018: struct vm_struct *vmlist; 以前讲过,内核使用的系统空间虚拟地址与物理地址间存在着一种简单的映射关系,只要在物理地 址上加上一个 3GB 的偏移量就得到了内核的虚拟地址。而变量 high_memory 标志着具体物理内存的上限 所对应的虚拟地址,这是在系统初始化时设置好的。当内核需要一片虚存地址空间时,就从这个地址以 上 8MB 处分配。为此,在 include/asm-i386/pgtable.h 中定义了 VMALLOC_START 等有关的常数: 00132: /* Just any arbitrary offset to the start of the vmalloc VM area: the 00133: * current 8MB value just means that there will be a 8MB "hole" after the 00134: * physical memory until the kernel virtual memory starts. That means that 00135: * any out-of-bounds memory accesses will hopefully be caught. 00136: * The vmalloc() routines leaves a hole of 4kB between each vmalloced 00137: * area for the same reason. ;) 00138: */ 00139: #define VMALLOC_OFFSET (8*1024*1024) 00140: #define VMALLOC_START (((unsigned long) high_memory + 2*VMALLOC_OFFSET-1) & \ 00141: ~(VMALLOC_OFFSET-1)) 00142: #define VMALLOC_VMADDR(x) ((unsigned long)(x)) 00143: #define VMALLOC_END (FIXADDR_START) 源代码中的注释对于为什么要留下一个 8MB 的空洞,以及在每次分配虚存空间时也要留下一个页 面的空洞(见 132 行)解释得很清楚:是为了便于捕获可能的越界访问。 这里读者可能会有个问题,185 行的 if 语句检查的是当前的起始地址加上区间大小须小于下一个区 间的起始地址,这是很好理解的。可是 176 行在区间大小上又加了一个页面作为空洞。这个空洞页面难 道不可能与下一个区间的起始地址冲突吗?这里的奥妙在于 185 行判定的条件是“<”而不是“<=”,并 且 size 和 addr 都是按页面边界对齐的,所以 185 行的条件已经隐含着其中有一个页面的空洞。从 get_vm_area()成功返回时,就标志着所需要的一片虚存空间已经分配好了,从返回的数据结构可以得到 这片空间的起始地址。下面就是建立映射的事了。 宏定义 VMALLOC_VMADDR 我们已经在前面看到过了,实际上不做什么事情,只是类型转换。函 数 remap_area_pages()的代码也在 arch/i386/mm/ioremap.c 中: [ioremap() > __ioremap() > remap_area_pages()] 00062: static int remap_area_pages(unsigned long address, unsigned long phys_addr, 00063: unsigned long size, unsigned long flags) 00064: { 00065: pgd_t * dir; 00066: unsigned long end = address + size; 00067: 00068: phys_addr -= address; 00069: dir = pgd_offset(&init_mm, address); 00070: flush_cache_all(); 00071: if (address >= end) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 158 页,共 1481 页 00072: BUG(); 00073: do { 00074: pmd_t *pmd; 00075: pmd = pmd_alloc_kernel(dir, address); 00076: if (!pmd) 00077: return -ENOMEM; 00078: if (remap_area_pmd(pmd, address, end - address, 00079: phys_addr + address, flags)) 00080: return -ENOMEM; 00081: address = (address + PGDIR_SIZE) & PGDIR_MASK; 00082: dir++; 00083: } while (address && (address < end)); 00084: flush_tlb_all(); 00085: return 0; 00086: } 我们讲过,每个进程的 task_struct 结构中都有一个指针指向 mm_struct 结构,从中可以找到相应的 页面目录。但是,内核空间不属于任何一个特定的进程,所以单独设置了一个内核专用的 mm_struct, 称为 init_mm。当然,内核也没有代表它的 task_struct 结构,所以 69 行根据起始地址从 init_mm 中找到 所属的目录项,然后就根据区间的大小走遍所有涉及的目录项。这里的 68 行看似奇怪。从物理地址中减 去虚拟地址得出一个负的位移量,这个位移量在 78~79 行又与虚拟地址相加,仍旧得到物理地址。由于 在循环中虚拟地址 address 在变(见 81 行),物理地址也就相应而变。第 75 行的 pmd_alloc_kernel()对于 i386 CPU 就是 pmd_alloc(),定义于 include/asm-i386/pgalloc.h: 00151: #define pmd_alloc_kernel pmd_alloc 而 inline 函数 pmd_alloc()的定义则有两个,分别用于二级和三级映射。对于二级映射这个定义为(见 include/asm-i386/pgtable_2level.h): [ioremap() > __ioremap() > remap_area_pages() > pmd_alloc()] 00016: extern inline pmd_t * pmd_alloc(pgd_t *pgd, unsigned long address) 00017: { 00018: if (!pgd) 00019: BUG(); 00020: return (pmd_t *) pgd; 00021: } 可见,对于 i386 的二级页式映射,只是把页面目录项当成中间目录项而已,与“分配”实际上毫无 关系。即使对于采用物理地址扩充(PAE)的 Pentium CPU,虽然实现三级映射,起作用也只是“找到” 中间目录项而已,只有在中间目录项为空时才真的分配一个。 这样,remap_area_pages()中从 73 行开始的 do-while 循环,对涉及到的每个页面目录表项调用 remap_area_pmd()。而 remap_area_pmd()几乎完全一样,对涉及的每个页面表(对 i386 的二级映射,每 个中间目录项实际上就是一个页面表项,也可以理解为中间目录表的大小为 1)调用 remap_area_pte(), 这也是在 arch/i386/mm/ioremap.c 中定义的: [ioremap() > __ioremap() > remap_area_pages() > remap_area_pmd() > remap_area_pte()] 00015: static inline void remap_area_pte(pte_t * pte, unsigned long address, Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 159 页,共 1481 页 00016: unsigned long size, unsigned long phys_addr, unsigned long flags) 00017: { 00018: unsigned long end; 00019: 00020: address &= ~PMD_MASK; 00021: end = address + size; 00022: if (end > PMD_SIZE) 00023: end = PMD_SIZE; 00024: if (address >= end) 00025: BUG(); 00026: do { 00027: if (!pte_none(*pte)) { 00028: printk("remap_area_pte: page already exists\n"); 00029: BUG(); 00030: } 00031: set_pte(pte, mk_pte_phys(phys_addr, __pgprot(_PAGE_PRESENT | _PAGE_RW | 00032: _PAGE_DIRTY | _PAGE_ACCESSED | flags))); 00033: address += PAGE_SIZE; 00034: phys_addr += PAGE_SIZE; 00035: pte++; 00036: } while (address && (address < end)); 00037: } 这里只是简单地在循环中设置页面表中所有涉及的页面表项(31 行)。每个表项都被预设成 _PAGE_DIRTY、_PAGE_ACCESSED 和_PAGE_PRESENTED。 在 kswapd 换出页面的情景中,我们已经看到 kswapd 定期地、循环地、依次地从 task 结构队列中找 出占用内存页面最多的进程,然后就对该进程调用 swap_out_mm()换出一些页面。而内核的 mm_struct 结构 init_mm 是单独的,从任何一个进程的 task 结构中都到达不了 init_mm。所以,kswapd 根本就看不 到 init_mm 中的虚存空间,这些区间的页面就自然不会被换出而长驻于内存。 2.12. 系统调用brk() 尽管“可见度”不高,brk()也许是最常使用的系统调用了,用户通过它向内核申请空间。人们常常 并不意识到在调用 brk(),原因在于很少会有人直接使用系统调用 brk()向系统申请空间,而总是通过像 malloc()一类的 C 语言库函数(或语言成分,如 C++中的 new)间接地用到 brk()。如果把 malloc()想像成 零售,brk()则是批发。库函数 malloc()为用户进程(malloc 本身就是该进程的一部分)维持一个小仓库, 当进程需要使用更多的内存空间时就向小仓库要,小仓库中存量不足时就通过 brk()向内核批发。 前面讲过,每个进程拥有 3G 字节的用户虚拟空间。但是,这并不意味着用户进程在这 3G 字节的范 围里可以任意使用,因为虚存空间最终得映射到某个物理存储空间(内存或磁盘空间),才真正可以使用, 而这种映射的建立和管理则由内核处理。所谓向内核申请一块空间,是指请求内核分配一块虚存空间和 相应的若干物理页面,并建立起映射关系。由于每个进程的虚存空间都很大(3G),而实际需要使用的 又很小,内核不可能在创建进程时就为整个虚存空间都分配好相应的物理空间并建立映射,而只能时需 要用多少才“分配”多少。 那么,内核怎样管理每个进程的 3G 字节虚存空间呢?粗略地说,用户程序经过编译、连接形成的 映象文件中有一个代码段和数据段(包括 data 段和 bss 段),其中代码段在下,数据段在上。数据段中包 括了所有静态分配的数据空间,包括全局变量和说明为 static 的局部变量。这些空间是进程所必须的基Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 160 页,共 1481 页 本要求,所以内核在建立一个进程的运行映象时就分配好这些空间,包括虚存地址空间和物理页面,并 建立好二者间的映射。除此之外,堆栈使用的空间也属于基本要求,所以也是在建立进程时就分配好的 (但可以扩充)。所不同的是,堆栈空间安置在虚存空间的顶部,运行时由顶向下延伸;代码段和数据段 则在底部(注意,不要与 X86 系统结构中由段寄存器建立的“代码段”及“数据段”相混淆),在运行 时并不向上伸展。而从数据段的顶部 end_data 到堆栈段地址的下沿这个中间区域则是一个巨大的空洞, 这酒是可以在运行时动态分配的空间。最初,这个动态分配空间是从进程的 end_data 开始的,这个地址 为内核和进程所共知。以后,每次动态分配一块“内存”,这个边界就往上推进一段距离,同时内核和进 程都要记下当前的边界在哪里。在进程这一边由 malloc()或类似的库函数管理,而在内核中则将当前的 边界记录在进程的 mm_struct 结构中。具体地说,mm_struct 结构中有一个成分 brk,表示动态分配区前 的部分。当一个进程需要分配内存时,将要求的大小与其当前的动态分配区底部边界相加,所得的就是 所要求的新边界,也就是 brk()调用时的参数 brk。当内核能满足要求时,系统调用 brk()返回 0,此后新 旧两个边界之间的虚存地址就都可以使用了。当内核发现无法满足要求(例如物理空间已经分配完),或 者发现新的边界已经过于逼近设于顶部的堆栈时,就拒绝分配而返回-1。 系统调用 brk()在内核中的实现为 sys_brk(),其代码在 mm/mmap.c 中。这个函数既可以用来分配空 间,即把动态分配区底部的边界往上推;也可以用来释放,即归还空间。因此,它的代码也大致上可以 分成两部分。我们先读第一部分: [sys_brk()] 00113: /* 00114: * sys_brk() for the most part doesn't need the global kernel 00115: * lock, except when an application is doing something nasty 00116: * like trying to un-brk an area that has already been mapped 00117: * to a regular file. in this case, the unmapping will need 00118: * to invoke file system routines that need the global lock. 00119: */ 00120: asmlinkage unsigned long sys_brk(unsigned long brk) 00121: { 00122: unsigned long rlim, retval; 00123: unsigned long newbrk, oldbrk; 00124: struct mm_struct *mm = current->mm; 00125: 00126: down(&mm->mmap_sem); 00127: 00128: if (brk < mm->end_code) 00129: goto out; 00130: newbrk = PAGE_ALIGN(brk); 00131: oldbrk = PAGE_ALIGN(mm->brk); 00132: if (oldbrk == newbrk) 00133: goto set_brk; 00134: 00135: /* Always allow shrinking brk. */ 00136: if (brk <= mm->brk) { 00137: if (!do_munmap(mm, newbrk, oldbrk-newbrk)) 00138: goto set_brk; 00139: goto out; 00140: } Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 161 页,共 1481 页 00141: 参数 brk 表示所要求的新边界,这个边界不能低于代码段的终点,并且必须与页面大小对其。如果 新边界低于老边界,那就不是申请分配空间,而是释放空间,所以通过 do_munmap()接触一部分区间的 映射,这是个重要的函数。其代码在 mm/mmap.c 中: [sys_brk() > do_munmap()] 00664: /* Munmap is split into 2 main parts -- this part which finds 00665: * what needs doing, and the areas themselves, which do the 00666: * work. This now handles partial unmappings. 00667: * Jeremy Fitzhardine 00668: */ 00669: int do_munmap(struct mm_struct *mm, unsigned long addr, size_t len) 00670: { 00671: struct vm_area_struct *mpnt, *prev, **npp, *free, *extra; 00672: 00673: if ((addr & ~PAGE_MASK) || addr > TASK_SIZE || len > TASK_SIZE-addr) 00674: return -EINVAL; 00675: 00676: if ((len = PAGE_ALIGN(len)) == 0) 00677: return -EINVAL; 00678: 00679: /* Check if this memory area is ok - put it on the temporary 00680: * list if so.. The checks here are pretty simple -- 00681: * every area affected in some way (by any overlap) is put 00682: * on the list. If nothing is put on, nothing is affected. 00683: */ 00684: mpnt = find_vma_prev(mm, addr, &prev); 00685: if (!mpnt) 00686: return 0; 00687: /* we have addr < mpnt->vm_end */ 00688: 00689: if (mpnt->vm_start >= addr+len) 00690: return 0; 00691: 00692: /* If we'll make "hole", check the vm areas limit */ 00693: if ((mpnt->vm_start < addr && mpnt->vm_end > addr+len) 00694: && mm->map_count >= MAX_MAP_COUNT) 00695: return -ENOMEM; 00696: 函数 find_vma_prev()的作用与以前的“几个重要的数据结构和函数”一节中读过的 find_vma()基本 相同,它扫描当前进程用户空间的 vm_area_struct 结构链表或 AVL 树,试图找到结束地址高于 addr 的第 一个区间,如果找到,则函数返回该区间的 vm_area_struct 结构指针。不同的是,它同时还通过参数 prev 返回其前一区间结构的指针。等一下我们就将看到为什么需要这个指针。如果返回的指针为 0,或者该 区间的起始地址也高于了 addr+len,那就表示想要解除映射的那部分空间原来就没有映射,所以直接返Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 162 页,共 1481 页 回 0。如果这部分空间落在某个区间的中间,则在接触这部分空间的映射以后会造成一个空洞而使原来 的区间一分为二。可是,一个进程可以拥有的虚存区间的数量是有限制的,所以若这个数量达到了上限 MAX_MAP_COUNT,就不再允许这样的操作。 我们继续往下看: [sys_brk() > do_munmap()] 00697: /* 00698: * We may need one additional vma to fix up the mappings ... 00699: * and this is the last chance for an easy error exit. 00700: */ 00701: extra = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL); 00702: if (!extra) 00703: return -ENOMEM; 00704: 00705: npp = (prev ? &prev->vm_next : &mm->mmap); 00706: free = NULL; 00707: spin_lock(&mm->page_table_lock); 00708: for ( ; mpnt && mpnt->vm_start < addr+len; mpnt = *npp) { 00709: *npp = mpnt->vm_next; 00710: mpnt->vm_next = free; 00711: free = mpnt; 00712: if (mm->mmap_avl) 00713: avl_remove(mpnt, &mm->mmap_avl); 00714: } 00715: mm->mmap_cache = NULL; /* Kill the cache. */ 00716: spin_unlock(&mm->page_table_lock); 00717: 由于解除一部分空间的映射有可能使原来的区间一分为二,所以这里先分配好一个空白的 vm_area_struct 结构 extra。另一方面,要解除映射的那部分空间也有可能跨越好几个区间,所以通过一 个 for 循环把所有涉及的区间都转移到一个临时队列 free 中,如果建立了 AV L 树,则也要把这些区间的 vm_area_struct 结构从 AV L 树中删除。以前讲过,mm_struct 结构中的指针 mmap_cache 指向上一次 find_vma()操作的对象,因为对虚存区间的操作往往是连续性的(见 find_vma()的代码),而现在用户空 间的结构有了变化,多半已经打破了这种连续性,所以把它清成 0。至此,已经完成了所有的准备,下 面就要具体解除映射了。 [sys_brk() > do_munmap()] 00718: /* Ok - we have the memory areas we should free on the 'free' list, 00719: * so release them, and unmap the page range.. 00720: * If the one of the segments is only being partially unmapped, 00721: * it will put new vm_area_struct(s) into the address space. 00722: * In that case we have to be careful with VM_DENYWRITE. 00723: */ 00724: while ((mpnt = free) != NULL) { 00725: unsigned long st, end, size; 00726: struct file *file = NULL; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 163 页,共 1481 页 00727: 00728: free = free->vm_next; 00729: 00730: st = addr < mpnt->vm_start ? mpnt->vm_start : addr; 00731: end = addr+len; 00732: end = end > mpnt->vm_end ? mpnt->vm_end : end; 00733: size = end - st; 00734: 00735: if (mpnt->vm_flags & VM_DENYWRITE && 00736: (st != mpnt->vm_start || end != mpnt->vm_end) && 00737: (file = mpnt->vm_file) != NULL) { 00738: atomic_dec(&file->f_dentry->d_inode->i_writecount); 00739: } 00740: remove_shared_vm_struct(mpnt); 00741: mm->map_count--; 00742: 00743: flush_cache_range(mm, st, end); 00744: zap_page_range(mm, st, size); 00745: flush_tlb_range(mm, st, end); 00746: 00747: /* 00748: * Fix the mapping, and free the old area if it wasn't reused. 00749: */ 00750: extra = unmap_fixup(mm, mpnt, st, size, extra); 00751: if (file) 00752: atomic_inc(&file->f_dentry->d_inode->i_writecount); 00753: } 00754: 00755: /* Release the extra vma struct if it wasn't used */ 00756: if (extra) 00757: kmem_cache_free(vm_area_cachep, extra); 00758: 00759: free_pgtables(mm, prev, addr, addr+len); 00760: 00761: return 0; 00762: } 这里通过一个 while 循环逐个处理所涉及的区间,这些区间的 vm_area_struct 结构都链接在一个临时 的队列 free 中。在下一节中读者将看到,一个进程可以通过系统调用 mmap()将一个文件的内容映射到其 用户空间的某个区间,然后就像访问内存一样来访问这个文件。但是,如果这个文件同时又被别的进程 打开,并通过常规的文件操作访问,则在二者对此文件的两种不同形式的写操作之间要加以互斥。如果 要解除映射的只是这样的区间的一部分(735~737 行),那就相当于对此区间的写操作,所以要递减该 文件的 inode 结构中的一个计数器 i_writecount,以保证互斥,到操作完成以后再予恢复(751~752 行)。 同时,还要通过 remove_shared_vm_struct()看看处理的区间是否是这样的区间,如果是,就将其 vm_area_struct 结构从目标文件的 inode 结构内的 i_mapping 队列中脱链。 代码中的 zap_page_range()解除若干连续页面的映射,并且释放所映射的内存页面,或对交换设备上Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 164 页,共 1481 页 物理页面的引用,这才是我们在这里关心的。其代码在 mm/memory.c 中: [sys_brk() > do_munmap() > zap_page_range()] 00348: /* 00349: * remove user pages in a given range. 00350: */ 00351: void zap_page_range(struct mm_struct *mm, unsigned long address, unsigned long size) 00352: { 00353: pgd_t * dir; 00354: unsigned long end = address + size; 00355: int freed = 0; 00356: 00357: dir = pgd_offset(mm, address); 00358: 00359: /* 00360: * This is a long-lived spinlock. That's fine. 00361: * There's no contention, because the page table 00362: * lock only protects against kswapd anyway, and 00363: * even if kswapd happened to be looking at this 00364: * process we _want_ it to get stuck. 00365: */ 00366: if (address >= end) 00367: BUG(); 00368: spin_lock(&mm->page_table_lock); 00369: do { 00370: freed += zap_pmd_range(mm, dir, address, end - address); 00371: address = (address + PGDIR_SIZE) & PGDIR_MASK; 00372: dir++; 00373: } while (address && (address < end)); 00374: spin_unlock(&mm->page_table_lock); 00375: /* 00376: * Update rss for the mm_struct (not necessarily current->mm) 00377: * Notice that rss is an unsigned long. 00378: */ 00379: if (mm->rss > freed) 00380: mm->rss -= freed; 00381: else 00382: mm->rss = 0; 00383: } 这个函数解除一块虚存区间的页面映射。首先通过 pgd_offset()在第一层页面目录中找到起始地址所 属的目录项,然后就通过一个 do-while 循环从这个目录项开始处理涉及的所有目录项。 00312: #define pgd_index(address) ((address >> PGDIR_SHIFT) & (PTRS_PER_PGD-1)) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 165 页,共 1481 页 00316: #define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address)) 对于涉及的每一个目录项,通过 zap_pmd_range()处理第二层的中间目录表。 [sys_brk() > do_munmap() > zap_page_range() > zap_pmd_range()] 00321: static inline int zap_pmd_range(struct mm_struct *mm, pgd_t * dir, unsigned long address, unsigned long size) 00322: { 00323: pmd_t * pmd; 00324: unsigned long end; 00325: int freed; 00326: 00327: if (pgd_none(*dir)) 00328: return 0; 00329: if (pgd_bad(*dir)) { 00330: pgd_ERROR(*dir); 00331: pgd_clear(dir); 00332: return 0; 00333: } 00334: pmd = pmd_offset(dir, address); 00335: address &= ~PGDIR_MASK; 00336: end = address + size; 00337: if (end > PGDIR_SIZE) 00338: end = PGDIR_SIZE; 00339: freed = 0; 00340: do { 00341: freed += zap_pte_range(mm, pmd, address, end - address); 00342: address = (address + PMD_SIZE) & PMD_MASK; 00343: pmd++; 00344: } while (address < end); 00345: return freed; 00346: } 同样,先通过 pmd_offset(),在第二层目录表中找到起始目录项。对于采用二级映射的 i386 结构, 中间目录表这一层是空的。pmd_offset()的定义在 include/asm-i386/pgtable-2level.h 中: 00053: extern inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address) 00054: { 00055: return (pmd_t *) dir; 00056: } 可见,pmd_offset()把指向第一层目录项的指针原封不动地作为指向中间目录的指针返回来了,也就 是说把第一层目录当成了中间目录。所以,对于二级映射,zap_pmd_range()在某种意义上只是把 zap_page_range()所做的事重复了一遍。不过,这一次重复调用的是 zap_pte_range(),处理的是底层的页 面映射表了。 [sys_brk() > do_munmap() > zap_page_range() > zap_pmd_range() > zap_pte_range()] Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 166 页,共 1481 页 00289: static inline int zap_pte_range(struct mm_struct *mm, pmd_t * pmd, unsigned long address, unsigned long size) 00290: { 00291: pte_t * pte; 00292: int freed; 00293: 00294: if (pmd_none(*pmd)) 00295: return 0; 00296: if (pmd_bad(*pmd)) { 00297: pmd_ERROR(*pmd); 00298: pmd_clear(pmd); 00299: return 0; 00300: } 00301: pte = pte_offset(pmd, address); 00302: address &= ~PMD_MASK; 00303: if (address + size > PMD_SIZE) 00304: size = PMD_SIZE - address; 00305: size >>= PAGE_SHIFT; 00306: freed = 0; 00307: for (;;) { 00308: pte_t page; 00309: if (!size) 00310: break; 00311: page = ptep_get_and_clear(pte); 00312: pte++; 00313: size--; 00314: if (pte_none(page)) 00315: continue; 00316: freed += free_pte(page); 00317: } 00318: return freed; 00319: } 还是先找到在给定页面表中的起始表项,与 pte_offset 有关的定义在 include/asm-i386/pgtable.h 中: 00324: /* Find an entry in the third-level page table.. */ 00325: #define __pte_offset(address) \ 00326: ((address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1)) 00327: #define pte_offset(dir, address) ((pte_t *) pmd_page(*(dir)) + \ 00328: __pte_offset(address)) 然后就是在一个 for 循环中,对需要解除映射的页面调用 ptep_get_and_clear()将页面表项清成 0: 00057: #define ptep_get_and_clear(xp) __pte(xchg(&(xp)->pte_low, 0)) 最后通过 free_pte()解除对内存页面以及盘上页面的使用,这个函数的代码在 mm/memory.c 中: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 167 页,共 1481 页 [sys_brk() > do_munmap() > zap_page_range() > zap_pmd_range() > zap_pte_range() > free_pte()] 00259: /* 00260: * Return indicates whether a page was freed so caller can adjust rss 00261: */ 00262: static inline int free_pte(pte_t pte) 00263: { 00264: if (pte_present(pte)) { 00265: struct page *page = pte_page(pte); 00266: if ((!VALID_PAGE(page)) || PageReserved(page)) 00267: return 0; 00268: /* 00269: * free_page() used to be able to clear swap cache 00270: * entries. We may now have to do it manually. 00271: */ 00272: if (pte_dirty(pte) && page->mapping) 00273: set_page_dirty(page); 00274: free_page_and_swap_cache(page); 00275: return 1; 00276: } 00277: swap_free(pte_to_swp_entry(pte)); 00278: return 0; 00279: } 如果页面表项表明在解除映射之前页面就已不在内存,则当前进程对该内存页面的使用已经解除, 所以只需调用 swap_free()解除对交换设备上的“盘上页面”的使用。当然,swap_free()首先是递减盘上 页面的使用计数,只有当这个计数达到 0 时才真正地释放了这个盘上页面。如果当前进程是这个盘上页 面的最后一个用户(或唯一用户),则该计数递减为 0。反之,则要通过 free_page_and_swap_cache()解除 对盘上页面和内存页面二者的使用。此外,如果页面在最近一次 try_to_swap_out()以后已被写过,则还 要通过 set_page_dirty()设置该页面 page 结构中的 PG_dirty 标志位,并在相应的 address_space 结构中将 其移入 dirty_pages 队列。函数 free_page_and_swap_cache()的代码在 mm/swap_state.c 中: [sys_brk() > do_munmap() > zap_page_range() > zap_pmd_range() > zap_pte_range() > free_pte() > free_page_swap_cache()] 00133: /* 00134: * Perform a free_page(), also freeing any swap cache associated with 00135: * this page if it is the last user of the page. Can not do a lock_page, 00136: * as we are holding the page_table_lock spinlock. 00137: */ 00138: void free_page_and_swap_cache(struct page *page) 00139: { 00140: /* 00141: * If we are the only user, then try to free up the swap cache. 00142: */ 00143: if (PageSwapCache(page) && !TryLockPage(page)) { 00144: if (!is_page_shared(page)) { 00145: delete_from_swap_cache_nolock(page); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 168 页,共 1481 页 00146: } 00147: UnlockPage(page); 00148: } 00149: page_cache_release(page); 00150: } 以前讲过,一个有用户空间映射、可换出的内存页面(确切地说是它的 page 数据结构),同时三个 队列中。一是通过其队列头 list 链入某个换入/换出队列,即相应 address_space 结构中的 clean_pages、 ditry_pages 以及 locked_pages 三个队列之一;二是通过其队列头 lru 链入某个 LRU 队列,即 active_list、 inactive_dirty_list 或者某个 inactive_clean_list 之一;最后就是通过指针 next_hash 链入一个杂凑队列。当 一个页面在换入/换出队列中时,其 page 结构中的 PG_swap_cache 标志位为 1,如果当前进程是这个页 面的最后一个用户(或唯一用户),此时便要调用 delete_from_swap_cache_nolock()将页面从上述队列中 脱离出来。 [sys_brk() > do_munmap() > zap_page_range() > zap_pmd_range() > zap_pte_range() > free_pte() > free_page_swap_cache() > delete_from_swap_cache_nolock()] 00103: /* 00104: * This will never put the page into the free list, the caller has 00105: * a reference on the page. 00106: */ 00107: void delete_from_swap_cache_nolock(struct page *page) 00108: { 00109: if (!PageLocked(page)) 00110: BUG(); 00111: 00112: if (block_flushpage(page, 0)) 00113: lru_cache_del(page); 00114: 00115: spin_lock(&pagecache_lock); 00116: ClearPageDirty(page); 00117: __delete_from_swap_cache(page); 00118: spin_unlock(&pagecache_lock); 00119: page_cache_release(page); 00120: } 先通过 block_flushpage()把页面的内容“冲刷”到块设备上,不过实际上这种冲刷仅在页面来自一 个映射到用户空间的文件时才进行,因为对于交换设备上的页面,此时的内容已经没有意义了。完成了 冲刷以后,就通过 lru_cache_del() 将页面从其所在的 LRU 队列中脱离出来。然后,再通过 __delete_from_swap_cache(),使页面脱离其他两个队列。 [sys_brk() > do_munmap() > zap_page_range() > zap_pmd_range() > zap_pte_range() > free_pte() > free_page_swap_cache() > delete_from_swap_cache_nolock() > __delete_from_swap_cache()] 00086: /* 00087: * This must be called only on pages that have 00088: * been verified to be in the swap cache. 00089: */ Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 169 页,共 1481 页 00090: void __delete_from_swap_cache(struct page *page) 00091: { 00092: swp_entry_t entry; 00093: 00094: entry.val = page->index; 00095: 00096: #ifdef SWAP_CACHE_INFO 00097: swap_cache_del_total++; 00098: #endif 00099: remove_from_swap_cache(page); 00100: swap_free(entry); 00101: } 这里的 remove_from_swap_cache()将页面的 page 结构从换入/换出和杂凑队列中脱离出来。然后,也 是通过 swap_free()释放盘上页面,回到 delete_from_swap_cache_nolock()。最后是 page_cache_release(), 即递减 page 结构中的使用计数。由于当前进程是页面的最后一个用户,并且在接触映射之前页面在内存 中(见上面 free_pte()中的 264 行),所以页面的使用计数应该是 2,这里(119 行)调用了一次 page_cache_release()就使其变成了 1。再返回到 free_page_and_swap_cache()中,这里(149 行)又调用了 一次 page_cache_release(),这一次就使其变成了 0,于是就最终把页面释放,让它回到了空闲页面队列 中。 当回到 do_munmap()中的时候,已经完成了对一个虚存区间的操作。此时,一方面要对虚存区间的 vm_area_struct 数据结构和进程的 mm_struct 数据结构作出调整,以反映已经发生的变化,如果整个区间 都解除了映射,则要释放原有的 vm_area_struct 数据结构;另一方面原来的区间还可能要一分为二,因 而需要插入一个新的 vm_area_struct 数据结构。这些操作是由 unmap_fixup()完成的,其代码在 mm/mmap.c 中: [sys_brk() > do_munmap() > unmap_fixup()] 00516: /* Normal function to fix up a mapping 00517: * This function is the default for when an area has no specific 00518: * function. This may be used as part of a more specific routine. 00519: * This function works out what part of an area is affected and 00520: * adjusts the mapping information. Since the actual page 00521: * manipulation is done in do_mmap(), none need be done here, 00522: * though it would probably be more appropriate. 00523: * 00524: * By the time this function is called, the area struct has been 00525: * removed from the process mapping list, so it needs to be 00526: * reinserted if necessary. 00527: * 00528: * The 4 main cases are: 00529: * Unmapping the whole area 00530: * Unmapping from the start of the segment to a point in it 00531: * Unmapping from an intermediate point to the end 00532: * Unmapping between to intermediate points, making a hole. 00533: * 00534: * Case 4 involves the creation of 2 new areas, for each side of Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 170 页,共 1481 页 00535: * the hole. If possible, we reuse the existing area rather than 00536: * allocate a new one, and the return indicates whether the old 00537: * area was reused. 00538: */ 00539: static struct vm_area_struct * unmap_fixup(struct mm_struct *mm, 00540: struct vm_area_struct *area, unsigned long addr, size_t len, 00541: struct vm_area_struct *extra) 00542: { 00543: struct vm_area_struct *mpnt; 00544: unsigned long end = addr + len; 00545: 00546: area->vm_mm->total_vm -= len >> PAGE_SHIFT; 00547: if (area->vm_flags & VM_LOCKED) 00548: area->vm_mm->locked_vm -= len >> PAGE_SHIFT; 00549: 00550: /* Unmapping the whole area. */ 00551: if (addr == area->vm_start && end == area->vm_end) { 00552: if (area->vm_ops && area->vm_ops->close) 00553: area->vm_ops->close(area); 00554: if (area->vm_file) 00555: fput(area->vm_file); 00556: kmem_cache_free(vm_area_cachep, area); 00557: return extra; 00558: } 00559: 00560: /* Work out to one of the ends. */ 00561: if (end == area->vm_end) { 00562: area->vm_end = addr; 00563: lock_vma_mappings(area); 00564: spin_lock(&mm->page_table_lock); 00565: } else if (addr == area->vm_start) { 00566: area->vm_pgoff += (end - area->vm_start) >> PAGE_SHIFT; 00567: area->vm_start = end; 00568: lock_vma_mappings(area); 00569: spin_lock(&mm->page_table_lock); 00570: } else { 00571: /* Unmapping a hole: area->vm_start < addr <= end < area->vm_end */ 00572: /* Add end mapping -- leave beginning for below */ 00573: mpnt = extra; 00574: extra = NULL; 00575: 00576: mpnt->vm_mm = area->vm_mm; 00577: mpnt->vm_start = end; 00578: mpnt->vm_end = area->vm_end; 00579: mpnt->vm_page_prot = area->vm_page_prot; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 171 页,共 1481 页 00580: mpnt->vm_flags = area->vm_flags; 00581: mpnt->vm_raend = 0; 00582: mpnt->vm_ops = area->vm_ops; 00583: mpnt->vm_pgoff = area->vm_pgoff + ((end - area->vm_start) >> PAGE_SHIFT); 00584: mpnt->vm_file = area->vm_file; 00585: mpnt->vm_private_data = area->vm_private_data; 00586: if (mpnt->vm_file) 00587: get_file(mpnt->vm_file); 00588: if (mpnt->vm_ops && mpnt->vm_ops->open) 00589: mpnt->vm_ops->open(mpnt); 00590: area->vm_end = addr; /* Truncate area */ 00591: 00592: /* Because mpnt->vm_file == area->vm_file this locks 00593: * things correctly. 00594: */ 00595: lock_vma_mappings(area); 00596: spin_lock(&mm->page_table_lock); 00597: __insert_vm_struct(mm, mpnt); 00598: } 00599: 00600: __insert_vm_struct(mm, area); 00601: spin_unlock(&mm->page_table_lock); 00602: unlock_vma_mappings(area); 00603: return extra; 00604: } 我们把这段代码留给读者。最后,当循环结束之时,由于已经解除了一些页面的映射,有些页面映 射可能整个都已经空白,对于这样的页面表(所占的页面)也要加以释放。这是由 free_pgtables()完成的。 我们也把它的代码留给读者(mm/mmap.c)。 [sys_brk() > do_munmap() > free_pgtables()] 00606: /* 00607: * Try to free as many page directory entries as we can, 00608: * without having to work very hard at actually scanning 00609: * the page tables themselves. 00610: * 00611: * Right now we try to free page tables if we have a nice 00612: * PGDIR-aligned area that got free'd up. We could be more 00613: * granular if we want to, but this is fast and simple, 00614: * and covers the bad cases. 00615: * 00616: * "prev", if it exists, points to a vma before the one 00617: * we just free'd - but there's no telling how much before. 00618: */ 00619: static void free_pgtables(struct mm_struct * mm, struct vm_area_struct *prev, 00620: unsigned long start, unsigned long end) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 172 页,共 1481 页 00621: { 00622: unsigned long first = start & PGDIR_MASK; 00623: unsigned long last = end + PGDIR_SIZE - 1; 00624: unsigned long start_index, end_index; 00625: 00626: if (!prev) { 00627: prev = mm->mmap; 00628: if (!prev) 00629: goto no_mmaps; 00630: if (prev->vm_end > start) { 00631: if (last > prev->vm_start) 00632: last = prev->vm_start; 00633: goto no_mmaps; 00634: } 00635: } 00636: for (;;) { 00637: struct vm_area_struct *next = prev->vm_next; 00638: 00639: if (next) { 00640: if (next->vm_start < start) { 00641: prev = next; 00642: continue; 00643: } 00644: if (last > next->vm_start) 00645: last = next->vm_start; 00646: } 00647: if (prev->vm_end > first) 00648: first = prev->vm_end + PGDIR_SIZE - 1; 00649: break; 00650: } 00651: no_mmaps: 00652: /* 00653: * If the PGD bits are not consecutive in the virtual address, the 00654: * old method of shifting the VA >> by PGDIR_SHIFT doesn't work. 00655: */ 00656: start_index = pgd_index(first); 00657: end_index = pgd_index(last); 00658: if (end_index > start_index) { 00659: clear_page_tables(mm, start_index, end_index - start_index); 00660: flush_tlb_pgtables(mm, first & PGDIR_MASK, last & PGDIR_MASK); 00661: } 00662: } 回到 sys_brk()的代码中,我们已经完成了通过 sys_brk()释放空间的情景分析。 如果新边界高于老边界,就表示要分配空间,这就是 sys_brk()的后一部分。我们继续往下看 (mm/mmap.c): Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 173 页,共 1481 页 [sys_brk()] 00142: /* Check against rlimit.. */ 00143: rlim = current->rlim[RLIMIT_DATA].rlim_cur; 00144: if (rlim < RLIM_INFINITY && brk - mm->start_data > rlim) 00145: goto out; 00146: 00147: /* Check against existing mmap mappings. */ 00148: if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE)) 00149: goto out; 00150: 00151: /* Check if we have enough memory.. */ 00152: if (!vm_enough_memory((newbrk-oldbrk) >> PAGE_SHIFT)) 00153: goto out; 00154: 00155: /* Ok, looks good - let it rip. */ 00156: if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk) 00157: goto out; 00158: set_brk: 00159: mm->brk = brk; 00160: out: 00161: retval = mm->brk; 00162: up(&mm->mmap_sem); 00163: return retval; 00164: } 首先检查对进程的资源限制,如果所要求的新边界使数据段的大小超过了对当前进程的限制,就拒 绝执行。此外,还要通过 find_vma_intersection(),检查所要求的那部分空间是否与已经存在某一区间相 冲突,这个 inline 函数的代码在 include/linux/mm.h 中: [sys_brk() > find_vma_intersection()] 00511: /* Look up the first VMA which intersects the interval start_addr..end_addr-1, 00512: NULL if none. Assume start_addr < end_addr. */ 00513: static inline struct vm_area_struct * find_vma_intersection( struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr) 00514: { 00515: struct vm_area_struct * vma = find_vma(mm,start_addr); 00516: 00517: if (vma && end_addr <= vma->vm_start) 00518: vma = NULL; 00519: return vma; 00520: } 这里的 start_addr 是老的边界,如果 find_vma()返回一个非 0 指针,就表示在它之上已经有了一个已 映射区间,因此有冲突的可能。此时新的边界 end_addr 必须落在这个区间的起点之下,也就是让从 start_addr 到 end_addr 这块空间落在空洞中,否则便是有了冲突。在查明了不存在冲突以后,还要通过 vm_enough_memory()看看系统中是否有足够的空闲内存页面。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 174 页,共 1481 页 [sys_brk() > vm_enough_memory()] 00041: /* Check that a process has enough memory to allocate a 00042: * new virtual mapping. 00043: */ 00044: int vm_enough_memory(long pages) 00045: { 00046: /* Stupid algorithm to decide if we have enough memory: while 00047: * simple, it hopefully works in most obvious cases.. Easy to 00048: * fool it, but this should catch most mistakes. 00049: */ 00050: /* 23/11/98 NJC: Somewhat less stupid version of algorithm, 00051: * which tries to do "TheRightThing". Instead of using half of 00052: * (buffers+cache), use the minimum values. Allow an extra 2% 00053: * of num_physpages for safety margin. 00054: */ 00055: 00056: long free; 00057: 00058: /* Sometimes we want to use more memory than we have. */ 00059: if (sysctl_overcommit_memory) 00060: return 1; 00061: 00062: free = atomic_read(&buffermem_pages); 00063: free += atomic_read(&page_cache_size); 00064: free += nr_free_pages(); 00065: free += nr_swap_pages; 00066: return free > pages; 00067: } 通过了这些检查,接着就是操作的主体 do_brk()了。这个函数的代码在 mm/mmap.c 中: [sys_brk() > do_brk()] 00775: /* 00776: * this is really a simplified "do_mmap". it only handles 00777: * anonymous maps. eventually we may be able to do some 00778: * brk-specific accounting here. 00779: */ 00780: unsigned long do_brk(unsigned long addr, unsigned long len) 00781: { 00782: struct mm_struct * mm = current->mm; 00783: struct vm_area_struct * vma; 00784: unsigned long flags, retval; 00785: 00786: len = PAGE_ALIGN(len); 00787: if (!len) 00788: return addr; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 175 页,共 1481 页 00789: 00790: /* 00791: * mlock MCL_FUTURE? 00792: */ 00793: if (mm->def_flags & VM_LOCKED) { 00794: unsigned long locked = mm->locked_vm << PAGE_SHIFT; 00795: locked += len; 00796: if (locked > current->rlim[RLIMIT_MEMLOCK].rlim_cur) 00797: return -EAGAIN; 00798: } 00799: 00800: /* 00801: * Clear old maps. this also does some error checking for us 00802: */ 00803: retval = do_munmap(mm, addr, len); 00804: if (retval != 0) 00805: return retval; 00806: 00807: /* Check against address space limits *after* clearing old maps... */ 00808: if ((mm->total_vm << PAGE_SHIFT) + len 00809: > current->rlim[RLIMIT_AS].rlim_cur) 00810: return -ENOMEM; 00811: 00812: if (mm->map_count > MAX_MAP_COUNT) 00813: return -ENOMEM; 00814: 00815: if (!vm_enough_memory(len >> PAGE_SHIFT)) 00816: return -ENOMEM; 00817: 00818: flags = vm_flags(PROT_READ|PROT_WRITE|PROT_EXEC, 00819: MAP_FIXED|MAP_PRIVATE) | mm->def_flags; 00820: 00821: flags |= VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC; 00822: 00823: 00824: /* Can we just expand an old anonymous mapping? */ 00825: if (addr) { 00826: struct vm_area_struct * vma = find_vma(mm, addr-1); 00827: if (vma && vma->vm_end == addr && !vma->vm_file && 00828: vma->vm_flags == flags) { 00829: vma->vm_end = addr + len; 00830: goto out; 00831: } 00832: } 00833: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 176 页,共 1481 页 00834: 00835: /* 00836: * create a vma struct for an anonymous mapping 00837: */ 00838: vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL); 00839: if (!vma) 00840: return -ENOMEM; 00841: 00842: vma->vm_mm = mm; 00843: vma->vm_start = addr; 00844: vma->vm_end = addr + len; 00845: vma->vm_flags = flags; 00846: vma->vm_page_prot = protection_map[flags & 0x0f]; 00847: vma->vm_ops = NULL; 00848: vma->vm_pgoff = 0; 00849: vma->vm_file = NULL; 00850: vma->vm_private_data = NULL; 00851: 00852: insert_vm_struct(mm, vma); 00853: 00854: out: 00855: mm->total_vm += len >> PAGE_SHIFT; 00856: if (flags & VM_LOCKED) { 00857: mm->locked_vm += len >> PAGE_SHIFT; 00858: make_pages_present(addr, addr + len); 00859: } 00860: return addr; 00861: } 参数 addr 为需要建立映射的新区间的起点,len 则为区间的长度。前面我们已经看到 find_vma_intersection()对冲突的检查,可是不知读者是否注意到,实际上检查的只是新区间的高端,对 于其低端的冲突则并未检查。例如,老的边界是否恰好是一个已映射区间的终点呢?如果不是,那说明 在低端有了冲突。不过对于低端的冲突是允许的,解决的方法是以新的映射为准,先通过 do_munmap() 把原有的映射解除(见 803 行),再来建立新的映射。读者大概要问了,为什么对新区间的高端和低端有 如此不同的容忍度和对待呢?读者最好先想一想,然后再往下看。 以前说过,用户空间的顶端是进程的用户空间堆栈。不管什么进程,在那里总是有一个已映射区间 存在着的,所以 find_vma_intersection()中的 find_vma()其实不会返回 0,因为至少用于堆栈的那个区间总 是存在的。当然,在堆栈以下也可能还有通过 mmap()或 ioremap()建立的映射区间。所以,如果新区间 的高端有冲突,那就可能是与堆栈的冲突,而低端的冲突则只能是与数据段的冲突。所以,对于低端可 以让进程自己对可能的错误负责,而对于堆栈可就不能采取把原有的映射解除,另行建立新的映射这种 方法了。 建立新的映射时,先看看是否可以根原有的区间合并,即通过扩展原有区间来覆盖新增的区间(826~ 831 行)。如果不行就得另行建立一个区间(838~852 行)。 最后,通过 make_pages_present(),为新增的区间建立起对内存页面的映射。其代码见 mm/memory.c: [sys_brk() > do_brk() > make_pages_present()] Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 177 页,共 1481 页 01210: /* 01211: * Simplistic page force-in.. 01212: */ 01213: int make_pages_present(unsigned long addr, unsigned long end) 01214: { 01215: int write; 01216: struct mm_struct *mm = current->mm; 01217: struct vm_area_struct * vma; 01218: 01219: vma = find_vma(mm, addr); 01220: write = (vma->vm_flags & VM_WRITE) != 0; 01221: if (addr >= end) 01222: BUG(); 01223: do { 01224: if (handle_mm_fault(mm, vma, addr, write) < 0) 01225: return -1; 01226: addr += PAGE_SIZE; 01227: } while (addr < end); 01228: return 0; 01229: } 这里所用的方法很有趣,那就是对新区间中的每一个页面模拟一次缺页异常。读者不妨想想,当从 do_brk()返回,进而从 sys_brk()返回之时,这些页面表项的映射是怎样的?如果进程从新分配的区间中 读,读出的内容该是什么?往里面写,情况又会怎样? 2.13. 系统调用mmap() 一个进程可以通过系统调用 mmap(),将一个已打开文件的内容映射到它的用户空间,其用户界面为: mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset) 参数 fd 代表着一个已打开文件,offset 为文件中的起点,而 start 为映射到用户空间中的起始地址, length 则为长度。还有两个参数 prot 和 flags,前者用于对所映射区间的访问模式,如可写、可执行等等; 后者则用于其他控制目的。从应用程序设计的角度来说,比之常规的文件操作,如 read()、write()、lseek() 等等,将文件映射到用户空间后像访问内存一样地访问文件显然要方便得多(读者不妨设想一下对数据 库文件的访问)。 在阅读本节之前,读者应先看一下前一节 sys_brk()的代码和有关说明,并且在阅读的过程中注意与 sys_brk()互相参照比较。有些内容可能要到阅读了后面几章,特别是“文件系统”以后,再回过来阅读 才能弄懂。 在 2.4.0 版的内核中实现这个调用的函数为 sys_mmap2(),但是在老一些的版本中另有一个函数 old_mmap(),这两个函数对应着不同的系统调用号。为保持对老版本的兼容,2.4.0 版中仍保留老的系统 调用号和 old_mmap(),由不同版本的 C 语言库程序决定采用哪一个系统调用号。二者的代码都在 arch/i386/kernel/sys_i386.c 中: 00068: asmlinkage long sys_mmap2(unsigned long addr, unsigned long len, 00069: unsigned long prot, unsigned long flags, 00070: unsigned long fd, unsigned long pgoff) 00071: { 00072: return do_mmap2(addr, len, prot, flags, fd, pgoff); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 178 页,共 1481 页 00073: } 00091: asmlinkage int old_mmap(struct mmap_arg_struct *arg) 00092: { 00093: struct mmap_arg_struct a; 00094: int err = -EFAULT; 00095: 00096: if (copy_from_user(&a, arg, sizeof(a))) 00097: goto out; 00098: 00099: err = -EINVAL; 00100: if (a.offset & ~PAGE_MASK) 00101: goto out; 00102: 00103: err = do_mmap2(a.addr, a.len, a.prot, a.flags, a.fd, a.offset >> PAGE_SHIFT); 00104: out: 00105: return err; 00106: } 可见,二者的区别仅在于传递参数的方式,它们的主体都是 do_mmap2(),其代码在同一文件中: [sys_mmap2() > do_mmap2()] 00042: /* common code for old and new mmaps */ 00043: static inline long do_mmap2( 00044: unsigned long addr, unsigned long len, 00045: unsigned long prot, unsigned long flags, 00046: unsigned long fd, unsigned long pgoff) 00047: { 00048: int error = -EBADF; 00049: struct file * file = NULL; 00050: 00051: flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE); 00052: if (!(flags & MAP_ANONYMOUS)) { 00053: file = fget(fd); 00054: if (!file) 00055: goto out; 00056: } 00057: 00058: down(¤t->mm->mmap_sem); 00059: error = do_mmap_pgoff(file, addr, len, prot, flags, pgoff); 00060: up(¤t->mm->mmap_sem); 00061: 00062: if (file) 00063: fput(file); 00064: out: 00065: return error; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 179 页,共 1481 页 00066: } 一般而言,系统调用 mmap()将已打开文件映射到用户空间。但是有个例外,那就是可以在调用参数 flags 中把标志位 MAP_ANNONYMOUS 设成 1,表示没有文件,实际上只是用来“圈地”,即在指定的 位置上分配空间。除此之外,操作的主体就是 do_mmap_pgoff()。 内核中还有个 inline 函数 do_mmap(),是供内核自己用的,它也是将已打开的文件映射到当前进程 的用户空间。以后,在阅读系统调用 sys_execve()的代码时,在函数 load_aout_binary()中可以看到通过 do_mmap()将可执行程序(二进制代码)映射到当前进程的用户空间。此外,do_mmap()还用来创建作为 进程间通信手段的“共享内存区”。这个 inline 函数是在 include/linux/mm.h 中定义的: 00428: static inline unsigned long do_mmap(struct file *file, unsigned long addr, 00429: unsigned long len, unsigned long prot, 00430: unsigned long flag, unsigned long offset) 00431: { 00432: unsigned long ret = -EINVAL; 00433: if ((offset + PAGE_ALIGN(len)) < offset) 00434: goto out; 00435: if (!(offset & ~PAGE_MASK)) 00436: ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT); 00437: out: 00438: return ret; 00439: } 与 do_mmap2()作一比较,就可以发现二者基本上相同,都是通过 do_mmap_pgoff()完成操作。不同 的只是 do_mmap()不支持 MAP_ANNONYMOUS;另一方面由于在进入 do_mmap()之前已经在临界区之 内,所以也不再需要通过信号量操作 down()和 up()加以保护。 函数 do_mmap_pgoff()的代码在 mm/mmap.c 中: [sys_mmap2() > do_mmap2() > do_mmap_pgoff()] 00188: unsigned long do_mmap_pgoff(struct file * file, unsigned long addr, unsigned long len, 00189: unsigned long prot, unsigned long flags, unsigned long pgoff) 00190: { 00191: struct mm_struct * mm = current->mm; 00192: struct vm_area_struct * vma; 00193: int correct_wcount = 0; 00194: int error; 00195: 00196: if (file && (!file->f_op || !file->f_op->mmap)) 00197: return -ENODEV; 00198: 00199: if ((len = PAGE_ALIGN(len)) == 0) 00200: return addr; 00201: 00202: if (len > TASK_SIZE || addr > TASK_SIZE-len) 00203: return -EINVAL; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 180 页,共 1481 页 00204: 00205: /* offset overflow? */ 00206: if ((pgoff + (len >> PAGE_SHIFT)) < pgoff) 00207: return -EINVAL; 00208: 00209: /* Too many mappings? */ 00210: if (mm->map_count > MAX_MAP_COUNT) 00211: return -ENOMEM; 00212: 00213: /* mlock MCL_FUTURE? */ 00214: if (mm->def_flags & VM_LOCKED) { 00215: unsigned long locked = mm->locked_vm << PAGE_SHIFT; 00216: locked += len; 00217: if (locked > current->rlim[RLIMIT_MEMLOCK].rlim_cur) 00218: return -EAGAIN; 00219: } 00220: 00221: /* Do simple checking here so the lower-level routines won't have 00222: * to. we assume access permissions have been handled by the open 00223: * of the memory object, so we don't do any here. 00224: */ 00225: if (file != NULL) { 00226: switch (flags & MAP_TYPE) { 00227: case MAP_SHARED: 00228: if ((prot & PROT_WRITE) && !(file->f_mode & FMODE_WRITE)) 00229: return -EACCES; 00230: 00231: /* Make sure we don't allow writing to an append-only file.. */ 00232: if (IS_APPEND(file->f_dentry->d_inode) && (file->f_mode & FMODE_WRITE)) 00233: return -EACCES; 00234: 00235: /* make sure there are no mandatory locks on the file. */ 00236: if (locks_verify_locked(file->f_dentry->d_inode)) 00237: return -EAGAIN; 00238: 00239: /* fall through */ 00240: case MAP_PRIVATE: 00241: if (!(file->f_mode & FMODE_READ)) 00242: return -EACCES; 00243: break; 00244: 00245: default: 00246: return -EINVAL; 00247: } 00248: } Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 181 页,共 1481 页 00249: 首先对文件和区间两方面都作一些检查,包括起始地址与长度、已经映射的次数等等。自振 file 非 0 表示映射的是具体的文件(而不是 MAP_ANONYMOUS),所以相应的 file 结构中的指针 f_op 必须指 向一个 file_operation 数据结构,其中的函数指针 mmap 又必须指向具体文件系统所提供的 mmap 操作(详 见第 5 章“文件系统”)。从某种意义上说,do_mmap()和 do_mmap2()提供的只是一个高层的框架,低层 的文件操作是由具体的文件系统提供的。 此外,还要对文件和区间的访问权限进行检查,二者必须相符。读者可以在阅读了第 5 章以后回过 来仔细看这些代码。这里我们继续往下看: [sys_mmap2() > do_mmap2() > do_mmap_pgoff()] 00250: /* Obtain the address to map to. we verify (or select) it and ensure 00251: * that it represents a valid section of the address space. 00252: */ 00253: if (flags & MAP_FIXED) { 00254: if (addr & ~PAGE_MASK) 00255: return -EINVAL; 00256: } else { 00257: addr = get_unmapped_area(addr, len); 00258: if (!addr) 00259: return -ENOMEM; 00260: } 00261: 调用 do_mmap_pgoff()时的参数基本上就是系统调用 mmap()的参数,如果参数 flags 中的标志位 MAP_FIXED 为 0,就表示指定的映射地址只是个参考值,不能满足时可以由内核给分配一个。所以就 通过 get_unmapped_area()在当前进程的用户空间中分配一个起始地址。其代码在 mm/mmap.c 中: [sys_mmap2() > do_mmap2() > do_mmap_pgoff() > get_unmapped_area()] 00374: /* Get an address range which is currently unmapped. 00375: * For mmap() without MAP_FIXED and shmat() with addr=0. 00376: * Return value 0 means ENOMEM. 00377: */ 00378: #ifndef HAVE_ARCH_UNMAPPED_AREA 00379: unsigned long get_unmapped_area(unsigned long addr, unsigned long len) 00380: { 00381: struct vm_area_struct * vmm; 00382: 00383: if (len > TASK_SIZE) 00384: return 0; 00385: if (!addr) 00386: addr = TASK_UNMAPPED_BASE; 00387: addr = PAGE_ALIGN(addr); 00388: 00389: for (vmm = find_vma(current->mm, addr); ; vmm = vmm->vm_next) { 00390: /* At this point: (!vmm || addr < vmm->vm_end). */ Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 182 页,共 1481 页 00391: if (TASK_SIZE - len < addr) 00392: return 0; 00393: if (!vmm || addr + len <= vmm->vm_start) 00394: return addr; 00395: addr = vmm->vm_end; 00396: } 00397: } 00398: #endif 读者自行阅读这段程序应该不会有困难。常数 TASK_UNMAPPED_BASE 是在 include/asm-i386/processor.c 中定义的: 00263: /* This decides where the kernel will search for a free chunk of vm 00264: * space during mmap's. 00265: */ 00266: #define TASK_UNMAPPED_BASE (TASK_SIZE / 3) 也就是说,当给定的目标地址为 0 时,内核从(TASK_SIZE/3)即 1GB 处开始向上在当前进程的虚存 空间中寻找一块足以容纳给定长度的区间。而给定的目标地址不为 0 时,则从给定的地址开始向上寻找。 函数 find_vma()在当前进程已经映射的虚存空间中找到第一个满足 vma->vm_end 大于给定地址的区间。 如果找不到这么一个区间,那就说明给定的地址尚未映射,因而可以使用。 至此,只要返回的地址非 0,addr 就已经时一个符合各种要求的虚存地址了。我们回到 do_mmap_pgoff()中继续往下看(mm/mmap.c): [sys_mmap2() > do_mmap2() > do_mmap_pgoff()] 00262: /* Determine the object being mapped and call the appropriate 00263: * specific mapper. the address has already been validated, but 00264: * not unmapped, but the maps are removed from the list. 00265: */ 00266: vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL); 00267: if (!vma) 00268: return -ENOMEM; 00269: 00270: vma->vm_mm = mm; 00271: vma->vm_start = addr; 00272: vma->vm_end = addr + len; 00273: vma->vm_flags = vm_flags(prot,flags) | mm->def_flags; 00274: 00275: if (file) { 00276: VM_ClearReadHint(vma); 00277: vma->vm_raend = 0; 00278: 00279: if (file->f_mode & FMODE_READ) 00280: vma->vm_flags |= VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC; 00281: if (flags & MAP_SHARED) { 00282: vma->vm_flags |= VM_SHARED | VM_MAYSHARE; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 183 页,共 1481 页 00283: 00284: /* This looks strange, but when we don't have the file open 00285: * for writing, we can demote the shared mapping to a simpler 00286: * private mapping. That also takes care of a security hole 00287: * with ptrace() writing to a shared mapping without write 00288: * permissions. 00289: * 00290: * We leave the VM_MAYSHARE bit on, just to get correct output 00291: * from /proc/xxx/maps.. 00292: */ 00293: if (!(file->f_mode & FMODE_WRITE)) 00294: vma->vm_flags &= ~(VM_MAYWRITE | VM_SHARED); 00295: } 00296: } else { 00297: vma->vm_flags |= VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC; 00298: if (flags & MAP_SHARED) 00299: vma->vm_flags |= VM_SHARED | VM_MAYSHARE; 00300: } 00301: vma->vm_page_prot = protection_map[vma->vm_flags & 0x0f]; 00302: vma->vm_ops = NULL; 00303: vma->vm_pgoff = pgoff; 00304: vma->vm_file = NULL; 00305: vma->vm_private_data = NULL; 00306: 00307: /* Clear old maps */ 00308: error = -ENOMEM; 00309: if (do_munmap(mm, addr, len)) 00310: goto free_vma; 00311: 00312: /* Check against address space limit. */ 00313: if ((mm->total_vm << PAGE_SHIFT) + len 00314: > current->rlim[RLIMIT_AS].rlim_cur) 00315: goto free_vma; 00316: 00317: /* Private writable mapping? Check memory availability.. */ 00318: if ((vma->vm_flags & (VM_SHARED | VM_WRITE)) == VM_WRITE && 00319: !(flags & MAP_NORESERVE) && 00320: !vm_enough_memory(len >> PAGE_SHIFT)) 00321: goto free_vma; 00322: 每个逻辑区间都要有个 vm_area_struct 数据结构,所以通过 kmem_cache_alloc()为待映射的区间分配 一个,并加以设置。我们不妨与前一节中 do_brk()的代码作一比较,在那里只是在新增的区间不能与已 有的区间合并时,才分配了一个 vm_area_struct 数据结构,而这里是无条件的。以前我们提到过,属性 不同的区段不能共存于一个逻辑区域中,而映射到一个特定的文件也是一种属性,所以总是要为之单独 建立一个逻辑区间。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 184 页,共 1481 页 如果调用 do_mmap_pgoff()时的 file 结构指针为 0,则目的仅在于创建虚存区间,或者说仅在于建立 从物理空间到虚存区间的映射。而如果目的在于建立从文件到虚存区间的映射,那就要把为文件设置的 访问权限考虑进去(间 275~296 行)。 注意代码中的 303 行将参数 pgoff 设置到 vm_area_struct 数据结构中的 vm_pgoff 字段。这个参数代 表着所映射内容在文件中的起点。有了这个起点,发生缺页异常时就可以根据虚存地址计算出相应页面 在文件中的位置。所以当断开映射时,对于文件映射页面不需要像普通换入/换出页面那样在页面表项中 指明其去向。另一方面,这也说明了为什么这样的区间必须是独立的。 至此,代表着我们所需虚存区间的数据结构已经创建了,只是尚未插入代表当前进程虚存空间的 mm_struct 结构中。可是,在某些条件下却还不得不将它撤销。为什么呢?这里调用了一个函数 do_munmap()。它检查目标地址是在当前进程的虚存空间是否已经在使用,如果已经在使用就要将老的 映射撤销。要是这个操作失败,那当然不能重复映射同一个目标地址,所以就得转移到 free_vma,把已 经分配的 vm_area_struct 数据结构撤销。我们已经在前一节中读过 do_munmap()的代码。也许读者会感 到奇怪,这个区间不是在前面调用 get_unmapped_area()找到的吗?怎么会原来就已映射呢?回过头去注 意看一下就可知道,那只是当调用参数 flags 中的标志位 MAP_FIXED 为 0 时,而当标志位为 1 时则尚 未对此加以检查。除此之外,还有两个情况也会导致撤销已经分配的 vm_area_struct 数据结构:一个是 如果当前的如果当前进程对虚存空间的使用超出了为其设置的上限;另一个是在要求建立由当前进程专 用的可写区间,而物理页面的数量已经(暂时)不足。 读者也许还要问:为什么不把对所有条件的检验放在分配 vm_area_struct 数据结构之前呢?问题在 于,在通过 kmem_cache_alloc()分配 vm_area_struct 数据结构的过程中,有可能会发生供这种数据结构专 用的 slab 已经用完,而不得不分配更多物理页面的情形。而分配物理页面的过程,则又有可能因一时不 能满足要求而只好先调度别的进程运行。这样,由于可能已经有别的进程和线程,特别是由本进程 clone() 出来的线程(见第 4 章)运行了,就不能排除这些条件已经改变的可能。所以,读者在内核中常常可以 看到先分配某项资源,然后检查条件,如果条件不符再将资源释放(而不是先检测条件,后分配资源) 的情景。关键就在于分配资源的过程中是有可能发生调度,以及其他进程和线程的运行有否可能改变这 些条件。以这里的第三个条件为例,如果发生调度,那就明显是可能改变的。 继续往下看 do_mmap_pgoff()的代码(mm/mmap.c): [sys_mmap2() > do_mmap2() > do_mmap_pgoff()] 00323: if (file) { 00324: if (vma->vm_flags & VM_DENYWRITE) { 00325: error = deny_write_access(file); 00326: if (error) 00327: goto free_vma; 00328: correct_wcount = 1; 00329: } 00330: vma->vm_file = file; 00331: get_file(file); 00332: error = file->f_op->mmap(file, vma); 00333: if (error) 00334: goto unmap_and_free_vma; 00335: } else if (flags & MAP_SHARED) { 00336: error = shmem_zero_setup(vma); 00337: if (error) 00338: goto free_vma; 00339: } 00340: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 185 页,共 1481 页 00341: /* Can addr have changed?? 00342: * 00343: * Answer: Yes, several device drivers can do it in their 00344: * f_op->mmap method. -DaveM 00345: */ 00346: flags = vma->vm_flags; 00347: addr = vma->vm_start; 00348: 00349: insert_vm_struct(mm, vma); 00350: if (correct_wcount) 00351: atomic_inc(&file->f_dentry->d_inode->i_writecount); 00352: 00353: mm->total_vm += len >> PAGE_SHIFT; 00354: if (flags & VM_LOCKED) { 00355: mm->locked_vm += len >> PAGE_SHIFT; 00356: make_pages_present(addr, addr + len); 00357: } 00358: return addr; 00359: 00360: unmap_and_free_vma: 00361: if (correct_wcount) 00362: atomic_inc(&file->f_dentry->d_inode->i_writecount); 00363: vma->vm_file = NULL; 00364: fput(file); 00365: /* Undo any partial mapping done by a device driver. */ 00366: flush_cache_range(mm, vma->vm_start, vma->vm_end); 00367: zap_page_range(mm, vma->vm_start, vma->vm_end - vma->vm_start); 00368: flush_tlb_range(mm, vma->vm_start, vma->vm_end); 00369: free_vma: 00370: kmem_cache_free(vm_area_cachep, vma); 00371: return error; 00372: } 如果要建立的是从文件到虚存区间的映射,而在调用 do_mmap() 时的参数 flags 中的 MAP_DENYWRITE 标志位为 1(这个标志位在前面 273 行引用的宏操作 vm_flags()中转换成 VM_DENYWRITE),那就表示不允许通过常规的文件操作访问该文件,所以要调用 deny_write_access() 排斥常规的文件操作,详见“文件系统”一章中的有关内容。至于 get_file(),其作用只是递增 file 结构 中的共享计数。 我们在这里暂不关心为共享内存区而建立的映射,所以跳过 335~339 行,将来在讲到共享内存区时, 还要回过头来看 shmem_zero_setup()的代码。 每个文件系统都有个 file_operations 数据结构,其中的函数指针 mmap 提供了用来建立从该类文件到 虚存区间的映射的操作。那么,具体到 Linux 的 Ext2 文件系统,这个函数是什么呢?我们来看 Ext2 文 件系统的 file_operations 数据结构(fs/ext2/file.c): 00100: struct file_operations ext2_file_operations = { …………………… Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 186 页,共 1481 页 00105: mmap: generic_file_mmap, …………………… 00109: }; 当打开一个文件时,如果所打开的文件在一个 Ext2 文件系统中,内核就会将 file 结构中的指针 f_op 设置成指向这个数据结构,所以上面 332 行的 file->f_op->mmap 就指向 generic_file_mmap()。这个函数 的代码在 mm/filemap.c 中: [sys_mmap2() > do_mmap2() > do_mmap_pgoff() > generic_file_mmap()] 01705: /* This is used for a general mmap of a disk file */ 01706: 01707: int generic_file_mmap(struct file * file, struct vm_area_struct * vma) 01708: { 01709: struct vm_operations_struct * ops; 01710: struct inode *inode = file->f_dentry->d_inode; 01711: 01712: ops = &file_private_mmap; 01713: if ((vma->vm_flags & VM_SHARED) && (vma->vm_flags & VM_MAYWRITE)) { 01714: if (!inode->i_mapping->a_ops->writepage) 01715: return -EINVAL; 01716: ops = &file_shared_mmap; 01717: } 01718: if (!inode->i_sb || !S_ISREG(inode->i_mode)) 01719: return -EACCES; 01720: if (!inode->i_mapping->a_ops->readpage) 01721: return -ENOEXEC; 01722: UPDATE_ATIME(inode); 01723: vma->vm_ops = ops; 01724: return 0; 01725: } 这个函数很简单,实质性的操作就是 1723 行将虚存区间控制结构中的指针 vm_ops 设置成 ops。至 于 ops,则根据映射为专有和共享而分别指向数据结构 file_private_mmap 或 file_shared_mmap。这 两 个 结 构均定义于 mm/filemap.c: 01686: /* 01687: * Shared mappings need to be able to do the right thing at 01688: * close/unmap/sync. They will also use the private file as 01689: * backing-store for swapping.. 01690: */ 01691: static struct vm_operations_struct file_shared_mmap = { 01692: nopage: filemap_nopage, 01693: }; 01694: 01695: /* 01696: * Private mappings just need to be able to load in the map. Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 187 页,共 1481 页 01697: * 01698: * (This is actually used for shared mappings as well, if we 01699: * know they can't ever get write permissions..) 01700: */ 01701: static struct vm_operations_struct file_private_mmap = { 01702: nopage: filemap_nopage, 01703: }; 数据结构的初始化也是 gcc 对 C 语言所作改进之一。这里表示具体 vm_operations_struct 结构中除 nopage 以外,所有成分的初始值均为 0 或 NULL,而 nopage 的初始值则为 filemap_nopage。相比之下, 在老版本中则必须写成{NULL, NULL, filemap_nopage},那样,一来麻烦,二来结构中各个字段与其初 始值的对应关系也不直观。 两个结构起始是一样的,都只是为缺页异常提供了 nopage 操作。此外,在 generic_file_mmap()中还 检验了用于页面读/写的函数是否存在(见 1714 和 1720 行)。这两个函数应该由文件的 inode 数据结构间 接地提供。在 inode 结构中有个指针 i_mapping,它指向一个 address_space 数据结构,读者应该回到“物 理页面的使用和周转”一节中看一下它的定义。我们在这里关心的是 address_space 结构中的指针 a_ops, 它指向一个 address_space_operations 数据结构。不同的文件系统(页面交换设备可以看作是一种特殊的 文件系统)有不同的 address_space_operations 结构。对于 Ext2 文件系统是 ext2_aops,定义于 fs/ext2/inode.c 中: 00669: struct address_space_operations ext2_aops = { 00670: readpage: ext2_readpage, 00671: writepage: ext2_writepage, 00672: sync_page: block_sync_page, 00673: prepare_write: ext2_prepare_write, 00674: commit_write: generic_commit_write, 00675: bmap: ext2_bmap 00676: }; 这个数据结构提供了用来读/写 ext2 文件页面的函数 ext2_readpage()和 ext2_writepage()。这些有关的 数据结构和指针也是在打开文件时设置好了的。 完成了这些检查和处理,把新建立的 vm_area_struct 结构插入到当前进程的 mm_struct 结构中,就基 本完成了 do_mmap_pgoff()的操作,仅在要求对区间加锁时才调用 make_pages_present(),建立起初始的 页面映射,这个函数的代码已经在前一节中看到过了。 读者也许感到困惑,在文件与虚存区间之间建立映射难道就这么简单?而且我们根本就没有看到页 面映射的建立!其实,具体的映射是非常动态、经常在变的。所谓文件与虚存区间之间的映射包含着两 个环节,一是物理页面文件映象之间的换入/换出,二是物理页面与虚存页面之间的映射。这二者都是动 态的。所以,重要的并不是建立起一个特定的映射,而是建立起一套机制,使得一旦需要时就可以根据 当时的具体情况建立起新的映射。另一方面,在计算机技术中有一个称为“lazy computation”的概念, 就是说有些为将来做某种准备而进行的操作(计算)可能并无必要,所以应该推迟到真正需要时才进行。 这是因为实际运行中的情况千变万化,有时候花了老大的劲才完成了准备,实际上却根本没有用到或者 只用到了很小一部分,从而造成了浪费。就以这里的文件映射来说,也许映射了 100 个页面,而实际上 在相当长的时间里只用到了其中的一个页面,而映射 99 个页面的开销却是不能忽略不计的。何况,长期 不用的页面还得费劲把它们换出哩。考虑到这些因素,还不如到真正需要用到一个页面时再来建立该页 面的映射,用到几个页面就映射几个页面。当然,那样很可能会因为分散处理而使具体映射每一个页面 开销增加。所以这里有个利弊权衡的问题,具体的决定往往要建立在统计数据的基础上。这里正是运用Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 188 页,共 1481 页 了这个概念,把具体页面的映射推迟到真正需要的时候才进行。具体地,就是为映射的建立、物理页面 的换入和换出(以及映射的拆除)分别准备一些函数,这就是 filemap_nopage()、ext2_readpage()以及 ext2_writepage()。 那么,什么时候,由谁来调用这些函数呢? 1. 首先,当这个区间中的一个页面首次受到访问时,会由于页面无映射而发生缺页异常,相应的 异常处理程序为 do_no_page()。对 于 Ext2 文件系统,do_no_page()会通过 ext2_readpage()分配一 个空闲内存页面并从文件读入相应的页面,然后建立起映射。 2. 建立起映射以后,对页面的操作时页面变“脏”,但是页面的内容并不立即写回文件中,而由内 核线程 bdflush()周期性运行时通过 page_launder()间接地调用 ext2_writepage(),将页面的内容写 入文件。如果页面很长时间没有受到访问,则页面会耗尽它的寿命,从而在一次 try_to_swap_out() 中被解除映射而转入不活跃状态。如果页面是“脏”的,则也会在 page_launder()中调用 ext2_writepage()。我们在 try_to_swap_out()的代码中曾看到,对用于文件映射的页面与普通的换 入/换出页面有不同的处理。对于前者是解除页面映射,把页面表项设置成 0;而对后者是断开 页面映射,是页面表项指向盘上页面。 3. 解除了映射的页面在再次受到访问时又会发生缺页异常,仍旧因页面无映射而进入 do_no_page(),而不像换入/换出页面那样进入 do_swap_page()。 我们把这些情景留给读者作为“家庭作业”。 除 mmap()以外,Linux 内核还提供了几个与之有关的系统调用,作为对 mmap()的补充。限于篇幅, 我们只把它们列出于下,有兴趣或需要的读者可自行阅读这些函数的源代码。 munmap(void *start, size_t length) 解除由 mmap()所建立的文件映射。 mremap(void *old_address, size_t old_size, size_t new_size, unsigned long flags) 这是 Linux 所特有的,用来扩大或缩小已经映射的一块空间。 msync(const void *start, size_t length, int flags) 把一个打开的文件映射到进程的虚存空间并进行读写之后,可以用 msync()将从地址 start 开始 的 length 个字节“冲刷”到实际的文件中,使得文件的内容与内存中的内容一致。参数 flag 中 有三个标志位,分别为 MS_SYNC、MS_ASYNS 和 MS_INVALIDATE。MS_SYNC 表示冲刷立 刻执行,并且系统调用应该等冲刷完成时才返回。MS_ASYNC 则表示冲刷可以异步地完成, 系统调用应立即返回,内核可以在适当的时机进行冲刷。而 MS_INVALIDATE,那是为同一个 文件被多次(由多个进程)映射的情况而设置的,表示同一文件的其他映象应被视作无效而应 加以刷新。 mlock(const void *addr, size_t len) 虚存空间被映射到物理空间以后,一般而言是由内核运用 LRU 算法决定页面的换入或换出。但 有时候某些进程因运行效率的考虑要将某些页面“锁定”在内存中,这时候就可以用 mlock() 将虚存中从 addr 开始的 len 个字节,实际上是这些字节所在的页面锁定在内存中,不允许换出。 mprotect(const void *addr, size_t len, int prot) 最后,mprotect()用来改变一段虚存空间的保护属性。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 189 页,共 1481 页 3. 中断、异常和系统调用 我们假定本书的读者已经具备了计算机系统结构方面的基础知识,所以本章对中断以及异常 (exception)处理的原理和机制不作深入的介绍。缺乏这方面基础的读者不妨先阅读一些微处理器方面 的有关材料。不过,我们也并不要求读者对相关内容已经具备了很深入的理解。事实上,随着我们的介 绍和分析,特别是随着各个情景的发展和代码的阅读,读者会逐步地加深理解。 先简要提一下,中断有两种,一种是由 CPU 外部产生的,另一种是由 CPU 本身在执行程序的过程 中产生的。 外部中断,就是通常所讲的“中断”(interrupt)。对于执行中的软件来说,这种中断的发生完全是“异 步”的,根本无法预测此类中断会在什么时候发生。因此,CPU(或者软件)对外部中断的响应完全是 被动的。不过,软件可以通过“关中断”指令关闭对中断的响应,把它“反映情况”的途径掐断,这样 就可以眼不见心不烦了(这里不考虑“不可屏蔽中断”)。 由软件产生的中断则不同,它是由专设的指令,如 X86 中的“INT n”,在程序中有意地产生的,所 以是主动的,“同步”的。只要 CPU 执行了一条 INT 指令,就知道在开始执行下一条指令之前一定要先 进入中断服务程序。这种主动的中断称为“陷阱”(trap)。 此外,还有一种与中断相似的机制称为“异常”(exception),一般也是异步的,多半由于“不小心” 犯了规才发生。例如,当你在程序中发出一条除法指令 DIV,而除数为 0 时,就会发生一次异常。这多 半是因为不小心,而不是故意的,所以也是被动的。当然,也不排除故意的可能性。我们在第 2 章中看 到过通过页面异常扩展堆栈区间的情景,那就是故意安排的。 这样,一共就有三种类似的机制,机中断、陷阱以及异常。 但是,不管是外部产生的中断还是陷阱,或者异常,不管是无意的、被动的,还是故意的、主动的, CPU 的响应过程却基本上一致。这就是:在执行完当前指令以后,或者在执行当前指令的中途,就根据 中断源所提供的“中断向量”,在内存中找到相应的服务程序入口并调用该服务程序。外部中断的向量是 由软件或硬件设置好了的,陷阱的向量是“自陷”指令中发出的(INT n 中的 n),而各种异常的向量则 是 CPU 的硬件结构中预先定好的。这样,这些不同的情况就因中断向量的不同而互相区分开了。因此, 在实践中常常将这些不同的情况作为一种统一的模式加以考虑和实现,而且常常统称为“中断”。至于系 统调用,一般都是通过 INT 指令实现的,所以也与中断密切相关。 本章前一部分内容讲中断,包括中断的硬件支持、软件处理以及中断响应和服务的过程;后一部分 则介绍系统调用的有关内容。 3.1. X86 CPU对中断的硬件支持 本节不讨论严格意义上的中断相应全过程(比如说,怎样获得中断向量),而是着重讨论 CPU 在响 应中断时,即在得到了中断向量以后,怎样进入相应的中断服务程序的过程。这是从操作系统的角度需 要关心的问题。Intel X86 CPU 支持 256 个不同的中断向量,这一点至今未变。可是,早期 X86 CPU 的 中断响应机制是非常原始、非常简单的。在实地址模式中,CPU 把内存中从 0 开始的 1K 字节作为一个 中断向量表。表中的每个表项占四个字节,由两个字节的段地址和两个字节的位移组成。这样构成的地 址便是相应中断服务程序的入口地址。这与 16 位实地址模式中的寻址方式也是一致的。但是,在这样的 机制上是不能构筑现代意义的操作系统的,即使把 16 位寻址模式改成 32 位寻址,即使实现了页式存储 管理,也还是无济于事。原因在于,这个机制中并没有提供空间切换,或者说运行模式切换的手段。为 了理解这一点,让我们来看看其他的 CPU 是怎么做的。读者也许知道,早期的 UNIX 是在 PDP-11 上实 现的。PDP-11 的 CPU 中有一个与 X86 的 FLAGS 寄存器相类似的控制状态寄存器,称为 PSW(处理器 状态字)。PSW 中有一个位段决定了 CPU 的当前运行优先级和模式(系统或用户)。在用户程序中是不 能通过直接修改 PSW 来达到调高优先级的目的的。在 PDP-11 的中断向量表中,每个表项由两部分组成, 一部分是相应中断服务程序的入口地址,另一部分就是当 CPU 进入中断服务程序后的 PSW。当然,中Linux 内核源代码情景分析 断向量表的内容只有当 CPU 处于系统模式时才能改变。当中断发生时,CPU 从向量表将 PSW 装入其控 制状态寄存器,而将中断服务程序的入口地址装入程序计数器,从而达到既转入相应的中断服务程序, 又从一种运行模式切换到另一种运行模式(或优先级别)的双重目的。至于原来的 PSW 则随中断返回 地址一起被压入堆栈,以便 CPU 从中断服务程序返回时能回到原来的运行模式。这样,就很自然地实现 了运行状态的切换。CPU 平时处于用户状态,无论是因为外部中断还是系统调用(由软件产生的中断), 或是某种异常,都会通过中断向量表进入系统状态,执行完中断服务程序后返回时便又恢复原状,回到 用户状态。相比之下,我们可以清楚地看到,X86 实地址模式下的中断响应过程所缺少的就是类似于 PDP-11 对 PSW 的处理。 因此,Intel 在实现保护模式时,对 CPU 的中断响应机制作了大幅度的修改。 首先,中断向量表中的表项从单纯的入口地址改成了类似于 PSW 加入口地址并且更为复杂的描述 项,称为“门”(gate),意思是当中断发生时必须先通过这些门,才能进入相应的服务程序。但是,这 样的门并不光是为中断而设的,只要想切换 CPU 的运行状态,即其优先级别,例如从用户的 3 级进入系 统的 0 级,就都要通过一道门。而从用户状态进入系统状态的途径也并不只限于中断(或异常,或陷阱), 孩可以通过子程序调用指令 CALL 和转移指令 JMP 来达到目的。而且,当中断发生时不但可以切换 CPU 的运行状态并转入中断服务程序,还可以安排进行一次任务切换(所谓“上下文切换”),立即切换到另 一个进程。因此在操作系统中可以设立一个“中断服务程序(任务)”,每当中断发生时就切换到该进程。 按不同的用途和目的,CPU 中一共有四种门,即任务门(task gate)、中断门(interrupt gate)、陷阱 门(trap gate)以及调用门(call gate)。其中除任务门外其他三种门的结构基本相同,不过调用门并不是 与中断向量表相联系的。 先看任务门,其大小为 64 位,结构如图 3.1 所示。 TSS段选择码00101P DPL 16位 16位16位8位5位2位1位 类型码101,表示任务门 DPL,描述项优先级别 P标志位,为1时表示在内存中 (阴影部分空闲不用) 图3.1 任务门结构图 TSS 段选择码的作用和段寄存器 CS、DS 的等相似,通过 GDT 或 LDT 指向特殊的“系统段”中的 一种,称为“任务状态段”(task state segment)TSS。TSS 实际上是一个用来保存任务运行“现场”的数 据结构,其中包括 CPU 中所有具体进程有关的寄存器的内容(包含页面目录指针 CR3),还包括了三个 堆栈指针。中断发生时,CPU 在中断向量表中找到相应的表项。如果此表项是一个任务门,并且通过了 优先级别的检查,CPU 就会将当前任务的运行现场保存在相应的 TSS 中,并将任务门所指向的 TSS 作 为当前任务,将其内容装入 CPU 中的各个寄存器,从而完成了一次任务的切换。为此目的,CPU 中又 增设了一个“任务寄存器”TR,用来指向当前任务的 TSS。在 Linux 内核中,一个任务就是一个进程, 但是进程的“控制块”,即 task_struct 结构中需要存放更多的信息。所以,从这个意义上讲,Linux 的进 程又并不完全是 Intel 设计意图中的任务。读者后面会看到,Linux 内核并不采用任务门作为进程切换的 手段。通过任务门切换到一个新的任务并不是唯一的途径,例如在程序中也可以用 CALL 指令或 JMP 指令通过调用门达到同样的目的。DPL 位段的作用后面还要讨论。 除任务门外,其余三种门的结构基本相同,每个门的大小也都是 64 位,见图 3.2。 2006-12-31 版权所有,侵权必究 第 190 页,共 1481 页 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 191 页,共 1481 页 位移的高16位 段选择码 位移的低16位P DPL 16位 16位16位8位3位 类型码:110中断门,111陷阱门,100调用门 DPL,描述项优先级别 P标志位,为1时表示在内存中 0 D D标志位,1=32位,0=16位 永远为0 图 3.2 中断门、陷阱门和调用门结构图 三种门之间的不同之处在于 3 位的类型码。中断门的类型码是 110,陷阱门的类型码是 111,而调用 门的类型码是 100。与任务门相比,不同之处主要在于:在任务门中不需要使用段内位移,因为任务门 并不指向某一个子程序的入口,TSS 本身是作为一个段来对待的,而中断门、陷阱门和调用门则都是要 指向一个子程序,所以必须结合使用段选择码和段内位移。此外,任务门中相对于 D 标志位的位置上永 远是 0。 中断门和陷阱门在使用上的区别不在于中断是外部产生的或是由 CPU 本身产生的,而是在于通过中 断门进入中断服务程序时 CPU 会自动将中断关闭,也就是将 CPU 中 EFLAGS 寄存器的 IF 标志位清成 0, 以防嵌套中断的发生;而在通过陷阱门进入服务程序时则维持 IF 标志位不变。这就是中断门和陷阱门的 唯一区别。 不管是什么门,都通过段选择码指向一个存储段。段选择码的作用于普通的段寄存器一样。我们在 第 2 章中讲过,在保护模式下段寄存器的内容并不直接指向一个段的起始地址,而是指向由 GDTR 或 LDTR 决定的某个段描述表中的一个表项,所以才又称为“段选择码”。至于到底是由 GDTR 还是由 LDTR 所指向的段描述表,则取决于段选择码中的一个 TI 标志位。在 Linux 内核中,实际上只使用全局段描述 表 GDT,而局部段描述表 LDT 只是在特殊应用中(主要是 WINE)才使用。对于中断门、陷阱门和调 用门来说,段描述表中的相应表项显然应该是一个代码段描述项。而任务门所指向的描述项,则是专门 为 TSS 而设的 TSS 描述项。TSS 描述项的结构于我们在第 2 章中所讲的基本上是相同的,但是 bit44 的 S 标志位为 0,表示不是一般的代码段或数据段。 每个段描述项中都有一个 DPL 位段,即“描述项优先级别”位段。当 CPU 通过中断门找到一个代 码段描述项,并进而转入相应的服务程序时,就把这个代码段描述项装入 CPU 中,而描述项的 DPL 就 变成 CPU 的当前运行级别,称为 CPL。这与我们在前面所说的 PDP-11 在中断时从向量表中同时装入 PSW 和服务程序入口地址是一致的。可是,在中断门中也有一个 DPL,那是干什么用的呢?这就是要讲到 i386 的保护模式中对运行和访问级别进行检查比对的机制了。 Intel 在 i386 CPU 中实现了一套可谓复杂得出奇的优先级别检验机制。我们在这里只根据 Linux 内核 的实现介绍其中一部分。由于 Linux 内核避开了这套机制中最复杂的部分,例如不使用任务门,基本上 也不使用调用门(不过为了兼容性的要求确实支持通过调用门来进入系统调用,但不是主流),再说在这 里我们只关心对代码段的访问,所以剩下的就不太复杂了。 当通过一条 INT 指令进入一个中断服务程序时,在指令中给出一个中断向量。CPU 先根据该向量在 中断向量表中找到一扇门(描述项),在这种情况下一般总是中断门。然后,就要讲这个门的 DPL 与 CPU 的 CPL 相比,CPL 必须小于或等于 DPL,也就是优先级别不低于 DPL,才能穿过这扇门。不过,如果 中断是由外部产生或是因 CPU 异常而产生的话,那就免去了这一层检验。穿过了中断门之后,还要进一Linux 内核源代码情景分析 步将目标代码段描述项中的 DPL 与 CPL 比较,目标段的 DPL 必须小于或等于 CPL。也就是说,通过中 断门时只允许保持或提升 CPU 的运行级别,而不允许降低其运行级别。这两个环节中的任何一个失败都 会产生一次全面保护异常(general protection exception)。 进入中断服务程序时,CPU 要将当前 EFLAGS 寄存器的内容以及返回地址压入堆栈,返回地址是由 段寄存器 CS 的内容和取指令指针 EIP 的内容共同组成的。如果中断是由异常引起的,则还要将一个表 示异常原因的出错代码也压入堆栈。进一步,如果中断服务程序的运行级别,也就是目标代码段的 DPL, 与中断发生时的 CPL 不同,那就要引起更换堆栈。前面提到过,TSS 结构中除所有常规的寄存器内容(包 括当前的 SS 和 ESP)外,还有三个额外的堆栈指针(SS 加 ESP)。这三个额外的堆栈指针分别用于当 CPU 在目标代码段中的运行级别为 0,1 以及 2 时。所以,CPU 根据寄存器 TR 的内容找到当前 TSS 结 构,并根据目标代码段的 DPL,从这 TSS 结构中取出新的堆栈指针(SS 加 ESP),并装入其堆栈寄存器 SS 和堆栈指针(寄存器)ESP,达到更换堆栈的目的。在这种情况下,CPU 不但要将 EFLAGS、返回地 址以及出错代码压入堆栈,还要将原来的堆栈指针也压入堆栈(新堆栈)。示意图 3.3 也许有助于理解。 ERROR CODE EIP CS EFLAGS 中断发生前夕的ESP 转入中断服务程序时的ESP 被中断进程与服务程序使用同一堆栈 ①运行级别不变 SS ERROR CODE EIP CS EFLAGS ESP中断发生前夕的SS:ESP 转入中断服务程序时的ESP ②运行级别改变 被中断进程的堆栈 中断服务程序的堆栈 图3.3 中断服务程序堆栈示意图 具体到 Linux 内核。当中断发生在用户状态、也就是 CPU 在用户空间运行时,由于用户态的运行级 别为 3,而在内核中的中断服务程序的运行级别为 0,所以会引起堆栈的更换。也就是说,从用户堆栈切 换到系统堆栈。而当中断发生在系统状态时,也就是当 CPU 在内核中运行时,则不会更换堆栈。 最后,在保护模式中,中断向量表在内存中的位置也不再限于从地址 0 开始的地方,而是像 GDT 和 LDT 那样可以放在内存中的任何地方。为此目的,在 CPU 中又增设了一个寄存器 IDTR,指向当前中 断向量表 IDT,或者说当前中断描述表。 图 3.4 的示意说明了 i386 保护模式下的中断机制在采用中断门或陷阱门时的结构。 实际的 i386 系统结构中的有关机制比上面讲的还要复杂,我们略去了其中与 Linux 内核实现无关的 内容。这也从另一个角度说明,对于像 Linux 这样的操作系统(事实证明是功能最强,而且最稳定的系 统之一)来说,i386 系统结构中的许多内容是不必要的,甚至是画蛇添足的,难怪有些学者批评 Intel 2006-12-31 版权所有,侵权必究 第 192 页,共 1481 页 Linux 内核源代码情景分析 将 i386 的系统结构过于复杂化了。当然,也有可能将来会出现一些新的技术,从而证明 Intel 是有远见 的,我们拭目以待。如果说,在能达到相同目标的前提下简单就是美,那么 i386 系统结构显然是不美的。 而相比之下,Linux 内核的实现倒确实是一种美。当然,不管怎么说,i386 的系统结构能够满足像 Linux 这样的现代操作系统的需要,却是毫无疑义的。 中断门 段描述项 IDT GDT/LDT (段选择码) 中断服务程序 代码段 位移 位移 GDTR或 LDTR IDTR 中断向量V V 图 3.4 中断机制示意图 3.2. 中断向量表IDT的初始化 Linux 内核在初始化阶段完成了对页式虚存管理的初始化以后,便调用 trap_int()和 init_IRQ()两个函 数进行中断机制的初始化。其中 trap_init()主要是对一些系统保留的中断向量的初始化,而 init_IRQ()则 主要是用于外设的中断。 函数 trap_init()是在 arch/i386/kernel/traps.c 中定义的: 00949: void __init trap_init(void) 00950: { 00951: #ifdef CONFIG_EISA 00952: if (isa_readl(0x0FFFD9) == 'E'+('I'<<8)+('S'<<16)+('A'<<24)) 00953: EISA_bus = 1; 00954: #endif 00955: 00956: set_trap_gate(0,÷_error); 00957: set_trap_gate(1,&debug); 00958: set_intr_gate(2,&nmi); 00959: set_system_gate(3,&int3); /* int3-5 can be called from all */ 00960: set_system_gate(4,&overflow); 00961: set_system_gate(5,&bounds); 2006-12-31 版权所有,侵权必究 第 193 页,共 1481 页 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 194 页,共 1481 页 00962: set_trap_gate(6,&invalid_op); 00963: set_trap_gate(7,&device_not_available); 00964: set_trap_gate(8,&double_fault); 00965: set_trap_gate(9,&coprocessor_segment_overrun); 00966: set_trap_gate(10,&invalid_TSS); 00967: set_trap_gate(11,&segment_not_present); 00968: set_trap_gate(12,&stack_segment); 00969: set_trap_gate(13,&general_protection); 00970: set_trap_gate(14,&page_fault); 00971: set_trap_gate(15,&spurious_interrupt_bug); 00972: set_trap_gate(16,&coprocessor_error); 00973: set_trap_gate(17,&alignment_check); 00974: set_trap_gate(18,&machine_check); 00975: set_trap_gate(19,&simd_coprocessor_error); 00976: 00977: set_system_gate(SYSCALL_VECTOR,&system_call); 00978: 00979: /* 00980: * default LDT is a single-entry callgate to lcall7 for iBCS 00981: * and a callgate to lcall27 for Solaris/x86 binaries 00982: */ 00983: set_call_gate(&default_ldt[0],lcall7); 00984: set_call_gate(&default_ldt[4],lcall27); 00985: 00986: /* 00987: * Should be a barrier for any external CPU state. 00988: */ 00989: cpu_init(); 00990: 00991: #ifdef CONFIG_X86_VISWS_APIC 00992: superio_init(); 00993: lithium_init(); 00994: cobalt_init(); 00995: #endif 00996: } 程序中先设置中断向量表开头的 19 个陷阱门,这些中断向量都是 CPU 用于异常处理的。例如,中 断向量 14 就是为页面异常保留的,CPU 硬件在页面映射及访问的过程中发生问题(如缺页),就会产生 一次以 14(0x0e)为中断向量的异常。操作系统的设计和实现必须遵循这些规定。 然后是对系统调用向量的初始化,常数 SYSCALL_VECTOR 在 include/asm_i386/hw_irq.h 中定义为 0x80,所以执行一条“INT 0x80”指令就是进行一次系统调用。 Linux 操作系统本身并不使用调用门,但是有些 Unix 变种已经用了调用门来实现系统调用,如注释 所说的 iBCS 和 Solaris/X86。为了与这些系统上编译的应用程序可执行代码相兼容,Linux 内核也相应设 置了两个调用门,983 行和 984 行就是对这两个调用门的初始化。由于我们在这里并不关心 SGI 公司的 特殊工作站显示设备,所以就略去了从 991 行开始的几行条件编译代码。 从程序中可以看到,这里用了三个函数来进行这些表项的初始化,那就是 set_trap_gate()、Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 195 页,共 1481 页 set_system_gate()以及 set_call_gate()。还有一个用于外设中断的 set_intr_gate(),这里虽然没有用到,但是 也属于同一组函数。这些函数都是在文件 arch/i386/kernel/traps.c 中定义的: 00808: void set_intr_gate(unsigned int n, void *addr) 00809: { 00810: _set_gate(idt_table+n,14,0,addr); 00811: } 00812: 00813: static void __init set_trap_gate(unsigned int n, void *addr) 00814: { 00815: _set_gate(idt_table+n,15,0,addr); 00816: } 00817: 00818: static void __init set_system_gate(unsigned int n, void *addr) 00819: { 00820: _set_gate(idt_table+n,15,3,addr); 00821: } 00822: 00823: static void __init set_call_gate(void *a, void *addr) 00824: { 00825: _set_gate(a,12,3,addr); 00826: } 这些函数都调用同一个子程序_set_gate(),设置中断描述表 idt_table 中的第 n 项,所不同的是参数表 中的第 2 个、第 3 个参数。第 2 个参数对应于中断门或陷阱门格式中的 D 标志位加上类型位段。参数 14 表示 D 标志位为 1 而类型为 110,所以 set_intr_gate()设置的是中断门。第 3 个参数则对应于 DPL 位段。 中断门的 DPL 一律设置成 0 是有讲究的。当中断是由外部产生或是 CPU 异常产生时,中断门的 DPL 是 被忽略不顾的,所以总能穿过该中断门。可是,要是用户进程在用户空间试着用一条“INT 2”来进入不 可屏蔽中断的服务程序时,由于用户状态的运行级别为 3,而中断门的 DPL 为 0(级别最高),由软件产 生的中断就会被拒之门外(CPU 会产生一次异常),因此不能得逞。同样,set_trap_gate()也将 DPL 设成 0,所不同的是调用_set_gate()时的第 2 个参数为 15,也即为 111,表示所设置的是陷阱门。我们在前面 已经讲过,陷阱门与中断门的不同仅在于通过中断门进入服务程序时自动关中断,而通过陷阱门进入服 务程序时则维持不变。所以,例如说,因 CPU 的页面异常而进入服务程序时,中断多半是开着的,我们 在第 2 章中看到过的那些程序,如 handle_mm_fault()等等,都是可中断的。但是 DPL 为 3,因为系统调 用是在用户空间通过“INT 0x80”进行的,只有将陷阱门的 DPL 设成 3 才能让系统调用顺利穿过,否则 就会把系统调用拒之门外了。 进一步看看,这些 IDT 表项到底怎么设置。_set_gate()也在同一文件(traps.c)中定义: 00788: #define _set_gate(gate_addr,type,dpl,addr) \ 00789: do { \ 00790: int __d0, __d1; \ 00791: __asm__ __volatile__ ("movw %%dx,%%ax\n\t" \ 00792: "movw %4,%%dx\n\t" \ 00793: "movl %%eax,%0\n\t" \ 00794: "movl %%edx,%1" \ 00795: :"=m" (*((long *) (gate_addr))), \ 00796: "=m" (*(1+(long *) (gate_addr))), "=&a" (__d0), "=&d" (__d1) \ 00797: :"i" ((short) (0x8000+(dpl<<13)+(type<<8))), \ Linux 内核源代码情景分析 00798: "3" ((char *) (addr)),"2" (__KERNEL_CS << 16)); \ 00799: } while (0) 首先,do{}while(0)决定了它的循环体,也就是 790 行至 798 行,一定会被执行一遍,并且只执行一 遍。特别是在编译时不管在什么情况下都不会有问题(见第 1 章)。从 795 行的第一个“:”到 797 行的 第 2 个“:”之间为输出部,其中说明了有四个变量会被改变,分别与%0、%1、%2 和%3 相结合。其中 %0 与参数 gate_addr 结合,%1 与(gate_addr+1)结合,二者都是内存单元;%2 与局部变量__d0 结合,存 放在寄存器%%eax 中,而%3 与局部变量__dl 结合,存放在寄存器%%edx 中。从 797 行至 798 行则为输 入部。由于输出部已经定义定义了%0~%3,输入部中的第一个变量便为%4,而后面还有两个变量分别 等价于输出部中的%3 和%2。输入部中说明的各输入变量的值,包括%3 和%2 的值,都会在引用这些变 量之前设置好。 为了方便,我们把所要求的中断门(或陷阱门)的格式再表示在图 3.5。 入口地址的高16位 段选择码 入口地址的低16位 P DPL 0DXXX 000 5位 8位 图3.5 中断门和陷阱门的格式定义 由于 791 行要用到%%dx 和%%ax,所以编译(以及汇编)以后的代码会按输入部的说明先将%%edx 设成 addr,而%%ax 设成(__KERNEL_CS<<16)。而 791 行将%%dx 的低 16 位移入%%ax 的低 16 位(注 意%%dx 与%%edx 的区别)。这样,在%%eax 中就形成了所需要的中断门的第一个长整数,其高 16 位 为__KERNEL_CS,而低 16 位为 addr 的低 16 位。接着,在 792 行中将(0x8000+(dpl<<3)+(type<<8))装入 %%edx 的低 16 位。这样,%%edx 中高 16 位为 addr 的高 16 位,而低 16 位的 P 位为 1(因为是 0x8000), DPL 位段为 dpl(因为 dpl<<3),而 D 位加上类型位段则为 type(因为 type<<8),其余各位皆为 0。这就 形成了中断门中的第 2 个长整数。然后,793 行将%%eax 写入*gate_addr,而 794 行则将%%edx 写入 *(gate_addr+1)。读者不妨试试,看看能否写出效率更高的代码!当然,这种高效率是以牺牲可读性为代 价的。对于像设置 IDT 表项一类并不是频繁发生的操作,这样做是否值得,大可商榷。不过,这毕竟是 在内核中,而且是很底层的东西,一般也不会有很多人去读、去维护的。 系统初始化时,在 trap_init()中设置了一些为 CPU 保留专用的 IDT 表项以及系统调用所用的陷阱门 以后,就要进入 init_IRQ() 设置大量用于外设的通用中断门了。函数 init_IRQ() 的代码在 arch/i386/kernel/i8259.c 中: 00438: void __init init_IRQ(void) 00439: { 00440: int i; 00441: 00442: #ifndef CONFIG_X86_VISWS_APIC 00443: init_ISA_irqs(); 00444: #else 00445: init_VISWS_APIC_irqs(); 00446: #endif 00447: /* 00448: * Cover the whole vector space, no vector can escape 2006-12-31 版权所有,侵权必究 第 196 页,共 1481 页 00449: * us. (some of these will be overridden and become Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 197 页,共 1481 页 00450: * 'special' SMP interrupts) 00451: */ 00452: for (i = 0; i < NR_IRQS; i++) { 00453: int vector = FIRST_EXTERNAL_VECTOR + i; 00454: if (vector != SYSCALL_VECTOR) 00455: set_intr_gate(vector, interrupt[i]); 00456: } 00457: 首先是在 init_ISA_irq()中对 PC 的中断控制器 8259A 进行初始化,并且初始化一个结构数组 irq_desc[]。为什么要有这么一个结构数组呢?我们知道,i386 的系统结构支持 256 个中断向量,还要扣 除一些为 CPU 本身保留的向量。但是,作为一个通用的操作系统,很难说剩下的这些中断向量是否够用。 而且,很多外部设备由于种种原因可能本来就不得不共用中断向量。所以,在像 Linux 这样的系统中, 限制每个中断源都必须独占使用一个中断向量是不现实的。解决的方法是为共用中断向量提供一种手段。 因此,系统中为每个中断向量设置一个队列,而根据每个中断源所使用(产生)的中断向量,将其中断 服务程序挂到相应的队列中去,而数组 irq_desc[]中的每个元素则是这样一个队列的头部以及控制结构。 当中断发生时,首先执行与中断向量相对应的一段总服务程序,根据具体中断源的设备号在其所属队列 中找到特定的服务程序加以执行。这个过程我们将在以后详细介绍,这里只要知道需要有这么一个结构 数组就行了。 接着,从 FIRST_EXTERNAL_VECTOR 开始,设立 NR_IRQS 个中断向量的 IDT 表项。常数 FIRST_EXTERNAL_VECTOR 在 include/asm-i386/hw_irq.h 中定义为 0x20,而 NR_IRQS 则为 224,那是 在 include/asm-i386/irq.h 中定义的。不过,要跳过用于系统调用的向量 0x80,那已经在前面设置好了。 这里设置的服务程序入口地址都来自一个函数指针数组 interrupt[]。 函数指针数组 interrupt[]的内容也是在 arch/i386/kernel/i8259.c 中定义的: 00098: #define IRQ(x,y) \ 00099: IRQ##x##y##_interrupt 00100: 00101: #define IRQLIST_16(x) \ 00102: IRQ(x,0), IRQ(x,1), IRQ(x,2), IRQ(x,3), \ 00103: IRQ(x,4), IRQ(x,5), IRQ(x,6), IRQ(x,7), \ 00104: IRQ(x,8), IRQ(x,9), IRQ(x,a), IRQ(x,b), \ 00105: IRQ(x,c), IRQ(x,d), IRQ(x,e), IRQ(x,f) 00106: 00107: void (*interrupt[NR_IRQS])(void) = { 00108: IRQLIST_16(0x0), 00109: 00110: #ifdef CONFIG_X86_IO_APIC 00111: IRQLIST_16(0x1), IRQLIST_16(0x2), IRQLIST_16(0x3), 00112: IRQLIST_16(0x4), IRQLIST_16(0x5), IRQLIST_16(0x6), IRQLIST_16(0x7), 00113: IRQLIST_16(0x8), IRQLIST_16(0x9), IRQLIST_16(0xa), IRQLIST_16(0xb), 00114: IRQLIST_16(0xc), IRQLIST_16(0xd) 00115: #endif 00116: }; 00117: 00118: #undef IRQ 00119: #undef IRQLIST_16 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 198 页,共 1481 页 数组的第一部分内容定义于 107 行,顺着 IRQLIST_16(x)和 IRQ(x,y)的定义到 98 行,可知关于函数 指针的文字是由 gcc 的预处理自动产生的,因为符号##的作用是将字符串连接在一起。例如,当 108 行 以参数 0x0(作为字符串)调用 IRQLIST_16()时,102 行中的 IRQ(x,0)就会在预处理阶段被替换成 IRQ0x00_interrupt。后面依次为 IRQ0x01_interrupt、IRQ0x02_interrupt、…一直到 IRQ0xff_interrupt。这 样,就利用 gcc 的预处理自动生成了所需的文字,而避免了枯燥繁琐的文字录入和编辑。所以,这一部 分给出了 interrupt[]中的开头 16 个函数指针。对于单 CPU 系统结构,后面的指针就都是 NULL 了。如果 是多处理器 SMP 结构,则后面还有 IRQ0x10 至 IRQ0xdf 等 208 个函数指针。 那么,从 IRQ0x00_interrupt 到 IRQ0x0f_interrupt 这 16 个函数本身是在哪儿定义的呢?请看 i8259.c 中的另外几行: 00038: #define BI(x,y) \ 00039: BUILD_IRQ(x##y) 00040: 00041: #define BUILD_16_IRQS(x) \ 00042: BI(x,0) BI(x,1) BI(x,2) BI(x,3) \ 00043: BI(x,4) BI(x,5) BI(x,6) BI(x,7) \ 00044: BI(x,8) BI(x,9) BI(x,a) BI(x,b) \ 00045: BI(x,c) BI(x,d) BI(x,e) BI(x,f) 00046: 00047: /* 00048: * ISA PIC or low IO-APIC triggered (INTA-cycle or APIC) interrupts: 00049: * (these are usually mapped to vectors 0x20-0x2f) 00050: */ 00051: BUILD_16_IRQS(0x0) 可见,51 行的宏定义 BUILD_16_IRQS(0x0)在预处理阶段会被展开成从 BUILD_IRQS(0x00)至 BUILD_IRQS(0x0f)共 16 项宏定义的引用。而 BUILD_IRQS()则是在 include/asm-i386/hw_irq.h 中定义的: 00172: #define BUILD_IRQ(nr) \ 00173: asmlinkage void IRQ_NAME(nr); \ 00174: __asm__( \ 00175: "\n"__ALIGN_STR"\n" \ 00176: SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" \ 00177: "pushl $"#nr"-256\n\t" \ 00178: "jmp common_interrupt"); 经过 gcc 的预处理以后,便会展开成一系列如下式样的代码: asmlinkage void IRQ0x01_interrupt(); __asm__( \ “\n” \ “IRQ0x01_interrupt: \n\t” \ “pushl $0x01 – 256\n\t” \ “jmp common_interrupt”); 由此可以看出,实际上由外设产生的中断处理全都进入一段公共的程序 common_interrupt 中,而在 此之前分别跑到 IRQ0x01_interrupt 或者 IRQ0x02_interrupt 等等的目的,只在于由此得到一个与中断向量 相关的数值(压入堆栈中)。对应于 IRQ0x00_interrupt 到 IRQ0x0f_interrupt,该数值分别为 0x0fffff00 至 0x0fffff0f ,余类推。至于 common_interrupt ,那也是由 gcc 的预处理展开一个宏定义 BUILD_COMMON_IRQ()而生成的,这段程序我们在后面的情景中还要讲,这里先从略。 回到 init_IRQ()中继续往下看(i8259.c): Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 199 页,共 1481 页 00458: #ifdef CONFIG_SMP ………… 00485: #endif 00486: 00487: /* 00488: * Set the clock to HZ Hz, we already have a valid 00489: * vector now: 00490: */ 00491: outb_p(0x34,0x43); /* binary, mode 2, LSB/MSB, ch 0 */ 00492: outb_p(LATCH & 0xff , 0x40); /* LSB */ 00493: outb(LATCH >> 8 , 0x40); /* MSB */ 00494: 00495: #ifndef CONFIG_VISWS 00496: setup_irq(2, &irq2); 00497: #endif 00498: 00499: /* 00500: * External FPU? Set up irq13 if so, for 00501: * original braindamaged IBM FERR coupling. 00502: */ 00503: if (boot_cpu_data.hard_math && !cpu_has_fpu) 00504: setup_irq(13, &irq13); 00505: } 由于我们在这里既不关心多处理器 SMP 结构,也不考虑 SG1 工作站的特殊处理,剩下的就只是对 系统时钟的初始化了。代码中有个注解,说我们已经有了个中断向量,实际上指的是 IRQ0x00_interrupt。 但是要注意,虽然该中断服务的入口地址已经设置到中断向量表中,但实际上我们还没有把具体的时钟 中断服务程序挂到 IRQ0 的队列中去。这个时候,这些 irq 队列都还是空的,所以即使开了中断,并且产 生了时钟中断,也只不过是让它在 common_interrupt 中空跑一趟。读者以后将看到,时钟中断和对时钟 中断的服务,就好像是动物的心跳,脉搏。而现在内核的脉搏尚未开始。为什么还不让它开始呢?这是 因为系统在这个时候还没有完成对进程调度机制的初始化,而一旦时钟中断开始,进度调度也就要随之 开始。所以,一定要等完成了对进程调度的初始化,做好了准备以后才能让脉搏开始跳动。 由此可见,设计一个真正实用的操作系统,有多少事情需要周到精细的考虑! 3.3. 中断请求队列的初始化 在前一节中,我们讲到中断向量表(更准确地,应该说“中断描述表”)IDT 中有两种表项,一种是 为保留专用于 CPU 本身的中断门,主要用于由 CPU 产生的异常,如“除数为 0”、“页面错”等等,以 及由用户程序通过 INT 指令产生的中断(或称“陷阱”),主要用来产生系统调用(另外还有个用于 debug 的 INT 3)。这些中断门的向量除用于系统调用的 0x80 外都在 0x20 以下。从 0x20 开始就是第 2 种表项, 共 224 项,都是用于外设的通用中断门。这二者的区别在于通用中断门可以为多个中断源所共享,而专 用中断门则是为特定的中断源所专用。 由于通用中断门是让多个中断源共用的,而且允许这种共用的结构在系统运行的过程中动态地变化, 所以在 IDT 的初始化阶段只是为每个中断向量,也即每个表项准备下一个“中断请求队列”,从而形成 一个中断请求队列的数组,这就是数组 irq_desc[]。中断请求队列头部的数据结构是在 include/linux/irq.h 中定义的: 00023: /* Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 200 页,共 1481 页 00024: * Interrupt controller descriptor. This is all we need 00025: * to describe about the low-level hardware. 00026: */ 00027: struct hw_interrupt_type { 00028: const char * typename; 00029: unsigned int (*startup)(unsigned int irq); 00030: void (*shutdown)(unsigned int irq); 00031: void (*enable)(unsigned int irq); 00032: void (*disable)(unsigned int irq); 00033: void (*ack)(unsigned int irq); 00034: void (*end)(unsigned int irq); 00035: void (*set_affinity)(unsigned int irq, unsigned long mask); 00036: }; 00037: 00038: typedef struct hw_interrupt_type hw_irq_controller; 00039: 00040: /* 00041: * This is the "IRQ descriptor", which contains various information 00042: * about the irq, including what kind of hardware handling it has, 00043: * whether it is disabled etc etc. 00044: * 00045: * Pad this out to 32 bytes for cache and indexing reasons. 00046: */ 00047: typedef struct { 00048: unsigned int status; /* IRQ status */ 00049: hw_irq_controller *handler; 00050: struct irqaction *action; /* IRQ action list */ 00051: unsigned int depth; /* nested irq disables */ 00052: spinlock_t lock; 00053: } ____cacheline_aligned irq_desc_t; 00054: 00055: extern irq_desc_t irq_desc [NR_IRQS]; 每个队列头部中除指针 action 用来维持一个由中断服务程序描述项构成的单链队列外,还有个指针 handler 指向另一个数据结构,即 hw_interrupt_type 数据结构。那里主要是一些函数指针,用于该队列, 或者说该共用“中断通道”的控制(通常是 i8259A)。例如,函数指针 enable 和 disable 用来开启和关断 其所属的通道,ack 用于对中断控制器的响应,而 end 则用于每次中断服务返回的前夕。这些函数都是 在 init_IRQ()中调用 init_ISA_irqs()设置好的,见 i8259.c: 00413: void __init init_ISA_irqs (void) 00414: { 00415: int i; 00416: 00417: init_8259A(0); 00418: 00419: for (i = 0; i < NR_IRQS; i++) { 00420: irq_desc[i].status = IRQ_DISABLED; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 201 页,共 1481 页 00421: irq_desc[i].action = 0; 00422: irq_desc[i].depth = 1; 00423: 00424: if (i < 16) { 00425: /* 00426: * 16 old-style INTA-cycle interrupts: 00427: */ 00428: irq_desc[i].handler = &i8259A_irq_type; 00429: } else { 00430: /* 00431: * 'high' PCI IRQs filled in on demand 00432: */ 00433: irq_desc[i].handler = &no_irq_type; 00434: } 00435: } 00436: } 程序先调用 init_8259A()对 8259A 中断控制器进行初始化(其代码也在 i8259.c 中),然后将开头 16 个中断请求队列的 handler 指针设置成指向数据结构 i8259A_irq_type,那也是在 i8259.c 中定义的: 00148: static struct hw_interrupt_type i8259A_irq_type = { 00149: "XT-PIC", 00150: startup_8259A_irq, 00151: shutdown_8259A_irq, 00152: enable_8259A_irq, 00153: disable_8259A_irq, 00154: mask_and_ack_8259A, 00155: end_8259A_irq, 00156: NULL 00157: }; 用于具体中断服务程序描述项的数据结构 irqaction,则是在 include/linux/interrupt.h 中定义的: 00014: struct irqaction { 00015: void (*handler)(int, void *, struct pt_regs *); 00016: unsigned long flags; 00017: unsigned long mask; 00018: const char *name; 00019: void *dev_id; 00020: struct irqaction *next; 00021: }; 其中最主要的就是函数指针 handler,指向具体的中断服务程序。 在 IDT 表的初始化完成之初,每个中断服务队列都是空的。此时即使打开中断并且某个外设中断真 的发生了,也得不到实际的服务。虽然从中断源的硬件以及中断控制器的角度来看似乎已经得到服务了, 因为形式上 CPU 确实通过中断门进入了某个中断向量的总服务程序,例如 IRQ0x01_interrupt(),并且按 照要求执行了对中断控制器的 ack()以及 end(),然后执行 iret 指令从中断返回。但是,从逻辑的角度、功 能的角度来看,则其实并没有得到实质的服务,因为并没有执行具体的中断服务程序。所以,真正的中 断服务要到具体设备的初始化程序将其中断服务程序通过 request_irq()向系统“登记”,挂入某个中断请 求队列以后才会发生。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 202 页,共 1481 页 函数 request_irq()的代码在 arch/i386/kernel/irq.c 中: 00630: /** 00631: * request_irq - allocate an interrupt line 00632: * @irq: Interrupt line to allocate 00633: * @handler: Function to be called when the IRQ occurs 00634: * @irqflags: Interrupt type flags 00635: * @devname: An ascii name for the claiming device 00636: * @dev_id: A cookie passed back to the handler function 00637: * 00638: * This call allocates interrupt resources and enables the 00639: * interrupt line and IRQ handling. From the point this 00640: * call is made your handler function may be invoked. Since 00641: * your handler function must clear any interrupt the board 00642: * raises, you must take care both to initialise your hardware 00643: * and to set up the interrupt handler in the right order. 00644: * 00645: * Dev_id must be globally unique. Normally the address of the 00646: * device data structure is used as the cookie. Since the handler 00647: * receives this value it makes sense to use it. 00648: * 00649: * If your interrupt is shared you must pass a non NULL dev_id 00650: * as this is required when freeing the interrupt. 00651: * 00652: * Flags: 00653: * 00654: * SA_SHIRQ Interrupt is shared 00655: * 00656: * SA_INTERRUPT Disable local interrupts while processing 00657: * 00658: * SA_SAMPLE_RANDOM The interrupt can be used for entropy 00659: * 00660: */ 00661: 00662: int request_irq(unsigned int irq, 00663: void (*handler)(int, void *, struct pt_regs *), 00664: unsigned long irqflags, 00665: const char * devname, 00666: void *dev_id) 00667: { 00668: int retval; 00669: struct irqaction * action; 00670: 00671: #if 1 00672: /* 00673: * Sanity-check: shared interrupts should REALLY pass in Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 203 页,共 1481 页 00674: * a real dev-ID, otherwise we'll have trouble later trying 00675: * to figure out which interrupt is which (messes up the 00676: * interrupt freeing logic etc). 00677: */ 00678: if (irqflags & SA_SHIRQ) { 00679: if (!dev_id) 00680: printk("Bad boy: %s (at 0x%x) called us without a dev_id!\n", devname, (&irq)[-1]); 00681: } 00682: #endif 00683: 00684: if (irq >= NR_IRQS) 00685: return -EINVAL; 00686: if (!handler) 00687: return -EINVAL; 00688: 00689: action = (struct irqaction *) 00690: kmalloc(sizeof(struct irqaction), GFP_KERNEL); 00691: if (!action) 00692: return -ENOMEM; 00693: 00694: action->handler = handler; 00695: action->flags = irqflags; 00696: action->mask = 0; 00697: action->name = devname; 00698: action->next = NULL; 00699: action->dev_id = dev_id; 00700: 00701: retval = setup_irq(irq, action); 00702: if (retval) 00703: kfree(action); 00704: return retval; 00705: } 参数 irq 为中断请求队列的序号,也就是人们通常所说的“中断请求号”,对应于中断控制器中的一 个通道,有时候要在接口卡上通过微型开关或跳线来设置。但是要注意,这样的中断请求号与 CPU 所用 的“中断号”或“中断向量”是不同的,中断请求号 IRQ0 相当于中断向量 0x20。也许,可以把这种中 断请求号看成“逻辑”中断向量,而后者则为“物理”中断向量。通常,前 16 个中断请求通道 IRQ0 至 IRQ15 是由中断控制器 i8259A 控制的。参数 ireflags 是一些标志位,其中的 SA_SHIRQ()标志表示与其 他中断源公用该中断请求通道。此时必须提供一个非零的 dev_id 以供区别。当中断发生时,参数 dev_id 会被作为调用参数传回所指定的服务程序。至于这 dev_id 到底是什么,request_irq()和中断服务的总控并 不在乎,只要各个具体的中断服务程序自己能够辨识和使用即可,所以这里 dev_id 的类型为 void*。而 request_irq()中则对此进行检查。顺便提一下,printk()产生一个出错信息,通常是写入文件/var/log/messages 或者在屏幕上显示,取决于“守护神”syslogd 和 klogd 是否已经在运行。这里有趣的是语句中的参数 (&irq)[-1]。这里 irq 是第一个调用参数,所以是最后压入堆栈的,&irq 就是参数 irq 在堆栈中的位置。那 么,在&irq 下面的是什么呢?那就是函数的返回地址。所以,这个 printk()语句显示该 request_irq()函数Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 204 页,共 1481 页 是从什么地方调用的,使程序员可以根据这个地址发现是在哪一个函数中调用的。 在分配并设置了一个 irqaction 数据结构 action 以后,便调用 setup_irq(),将其链入相应的中断请求 队列。其代码在同一文件(irq.c)中: 00958: /* this was setup_x86_irq but it seems pretty generic */ 00959: int setup_irq(unsigned int irq, struct irqaction * new) 00960: { 00961: int shared = 0; 00962: unsigned long flags; 00963: struct irqaction *old, **p; 00964: irq_desc_t *desc = irq_desc + irq; 00965: 00966: /* 00967: * Some drivers like serial.c use request_irq() heavily, 00968: * so we have to be careful not to interfere with a 00969: * running system. 00970: */ 00971: if (new->flags & SA_SAMPLE_RANDOM) { 00972: /* 00973: * This function might sleep, we want to call it first, 00974: * outside of the atomic block. 00975: * Yes, this might clear the entropy pool if the wrong 00976: * driver is attempted to be loaded, without actually 00977: * installing a new handler, but is this really a problem, 00978: * only the sysadmin is able to do this. 00979: */ 00980: rand_initialize_irq(irq); 00981: } 00982: 00983: /* 00984: * The following block of code has to be executed atomically 00985: */ 00986: spin_lock_irqsave(&desc->lock,flags); 00987: p = &desc->action; 00988: if ((old = *p) != NULL) { 00989: /* Can't share interrupts unless both agree to */ 00990: if (!(old->flags & new->flags & SA_SHIRQ)) { 00991: spin_unlock_irqrestore(&desc->lock,flags); 00992: return -EBUSY; 00993: } 00994: 00995: /* add new interrupt at end of irq queue */ 00996: do { 00997: p = &old->next; 00998: old = *p; 00999: } while (old); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 205 页,共 1481 页 01000: shared = 1; 01001: } 01002: 01003: *p = new; 01004: 01005: if (!shared) { 01006: desc->depth = 0; 01007: desc->status &= ~(IRQ_DISABLED | IRQ_AUTODETECT | IRQ_WAITING); 01008: desc->handler->startup(irq); 01009: } 01010: spin_unlock_irqrestore(&desc->lock,flags); 01011: 01012: register_irq_proc(irq); 01013: return 0; 01014: } 计算机系统在使用中常常有产生随机数的要求,但是要产生真正的随机数不可能的(所以由计算机 产生的随机数称为“伪随机数”)。为了达到尽可能的随机,需要在系统的运行中引入一些随机的因素, 称为“熵”(entropy)。由各种中断源产生的中断请求在时间上大多使相当随机的,可以用来作为这样的 随机因素。所以 Linux 内核提供了一种手段,使得可以根据中断发生的时间来引入一点随机性。需要在 某个中断请求队列,或者说中断请求通道中引入这种随机性时,可以在调用参数 irqflags 中将标志位 SA_SAMPLE_RANDOM 设成 1。而这里调用的 rand_initialize_irq()就据此为该中断请求队列初始化一个 数据结构,用来记录该中断的时序。 可想而知,对于中断请求队列的操作当然不允许受到干扰,必须要在临界区内进行,不光中断要关 闭,还要防止可能来自其他处理器的干扰。代码 986 行的 spin_lock_irqsave()就使 CPU 进入了这样的临 界区。我们将在本书下册“多处理器 SMP 结构”一章中介绍和讨论 spin_lock_irqsave(),与之相对应的 spin_lock_irqrestore()则是临界区的出口。 对第一个加入队列的 irqaction 结构的处理比较简单(1003 行),不过此时要对队列的头部进行一些 初始化(1006~1008 行),包括调用本队列的 startup 函数。对于后来加入队列的 irqaction 结构则要稍加 检查,检查的内容为是否允许共用一个中断通道,只有在新加入的结构以及队列中的第一个结构都允许 共用时才将其链入队列的尾部。 在内核中,设备驱动程序一般都要通过 request_irq()向系统登记其中断服务程序。 3.4. 中断的响应和服务 搞清了 i386 CPU 的中断机制和内核中有关的初始化以后,我们就可以从中断请求的发生到 CPU 的 响应,再到中断服务程序的调用与返回,沿着 CPU 所经过的路线走一遍。这样,即可以弄清和理解 Linux 内核对中断响应和服务的总体的格局和安排,还可以顺着这个过程介绍内核中的一些相关的“基础设施”。 对此二者的了解和理解,有助于读者对整个内核的理解。 这里,我们假定外设的驱动程序都已经完成了初始化,并且已把相应的中断服务程序挂入到特定的 中断请求队列中,系统正在用户空间正常运行(所以中断必然是开着的),并且某个外设已经产生了一次 中断请求。该请求通过中断控制器 i8259A 到达了 CPU 的“中断请求”引线 INTR。由于中断是开着的, 所以 CPU 在执行完当前指令后就来响应该次中断请求。 CPU 从中断控制器取得中断向量,然后根据具体的中断向量从中断向量表 IDT 中找到相应的表项, 而该表项应该是一个中断门。这样,CPU 就根据中断门的设置而到达了该通道的总服务程序的入口,假 定为 IRQ0x03_interrupt。由于中断是当 CPU 在用户空间中运行时发生的,当前的运行级别 CPL 为 3;而 中断服务程序属于内核,其运行级别 DPL 为 0,二者不同。所以,CPU 要从寄存器 TR 所指的当前 TSSLinux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 206 页,共 1481 页 中取出用于内核(0 级)的堆栈指针,并把堆栈切换到内核堆栈,即当前进程的系统空间堆栈。应该指 出,CPU 每次使用内核堆栈时对堆栈所作的操作总是均衡的,所以每次从系统空间返回到用户空间时堆 栈指针一定回到其原点,或曰“堆栈底部”。也就是说,当 CPU 从 TSS 中取出内核堆栈指针并切换到内 核堆栈时,这个堆栈一定是空的。这样,当 CPU 进入 IRQ0x03_interrupt 时,堆栈中除寄存器 EFLAGS 的内容以及返回地址外就一无所有了。另外,由于穿过的是中断门(而不是陷阱门),所以中断已被关断, 在重新开启中断之前再没有其他的中断可以发生了。 中断服务的总入口 IRQ0xYY_interrupt 的代码以前已经见到过了,但为方便起见再把它列出在这里。 再说,我们现在的认识也可以更深入一些了。 如前所述,所有公用中断请求的服务程序总入口是由 gcc 的预处理阶段生成的,全部都具有相同的 模式: asmlinkage void IRQ0x03_interrupt(); __asm__( \ “\n” \ “IRQ0x01_interrupt: \n\t” \ “pushl $0x03 – 256\n\t” \ “jmp common_interrupt”); 这段程序的目的在于将一个与中断请求号相关的数值压入堆栈,使得在 common_interrupt 中可以通 过这个数值来确定该次中断的来源。可是为什么要从中断请求号 0x03 中减去 256 使其变成负数呢?就用 数值 0x03 不是更直截了当吗?这是因为,系统堆栈中的这个位置在因系统调用而进入内核时要用来存放 系统调用号,而系统调用又与中断服务共用一部分子程序。这样,就要有个手段来加以区分。当然,要 区分系统调用号和中断请求号并不非得把其中之一变成负数不可。例如,在中断请求号上加上一个常数, 比方说 0x1000,也可以达到目的。但是如果考虑到运行时的效率,那么把其中之一变成负数无疑是效率 最高的。将一个整数装入到一个通用寄存器之后,要判断它是否大于等于 0 是很方便的,只要一条寄存 器指令就可以了,如“orl %%eax, %%eax”或“testl %%ecx, %%ecx”都可以达到目的。而如果要与另一 个常数相比较,那就要多访问一次内存。从这个例子也可以看出,内核中的有些代码看似简单,好像只 是作者随意的决定,但实际上却是经过精心推敲的。 公共的跳转目标 common_interrupt()是在 include/asm-i386/hw_irq.h 中定义的: [IRQ0x03_interrupt -> common_interrupt] 00152: #define BUILD_COMMON_IRQ() \ 00153: asmlinkage void call_do_IRQ(void); \ 00154: __asm__( \ 00155: "\n" __ALIGN_STR"\n" \ 00156: "common_interrupt:\n\t" \ 00157: SAVE_ALL \ 00158: "pushl $ret_from_intr\n\t" \ 00159: SYMBOL_NAME_STR(call_do_IRQ)":\n\t" \ 00160: "jmp "SYMBOL_NAME_STR(do_IRQ)); 00161: 这里主要的操作是宏操作 SAVE_ALL,就是所谓“保存现场”,把中断发生前夕所有寄存器的内容 都保存在堆栈中,待中断服务完毕要返回之前再来“恢复现场”。 SAVE_ALL 的定义在 arch/i386/kernel/entry.S 中: 00086: #define SAVE_ALL \ 00087: cld; \ 00088: pushl %es; \ 00089: pushl %ds; \ Linux 内核源代码情景分析 00090: pushl %eax; \ 00091: pushl %ebp; \ 00092: pushl %edi; \ 00093: pushl %esi; \ 00094: pushl %edx; \ 00095: pushl %ecx; \ 00096: pushl %ebx; \ 00097: movl $(__KERNEL_DS),%edx; \ 00098: movl %edx,%ds; \ 00099: movl %edx,%es; 这里要指出两点:第一是标志位寄存器 EFLAGS 的内容并不是在 SAVE_ALL 中保存的,这是因为 CPU 在进入中断服务时已经把它的内容连同返回地址一起压入堆栈了。第二是段寄存器 DS 和 ES 原来 的内容被保存在堆栈中,然后就被改成指向用于内核的__KERNEL_DS。我们在第 2 章中讲过, __KERNEL_DS 和__USER_DS 都是指向从 0 开始的空间,所不同的只是运行级别 DPL 一个为 0 级,另 一个为 3 级。至于原来的堆栈段寄存器 SS 和堆栈指针 SP 的内容,则或者已被压入堆栈(如果更换堆栈), 或者继续使用而无须保存(如果不更换堆栈)。这样,在 SAVE_ALL 以后,堆栈中的内容就成为图 3.6 形式。 EAX EDI ESI EBX ECX EDX EBP 用户堆栈的SS 用户堆栈的ESP EFLAGS 用户空间的CS EIP (中断号 - 256) ES DS 系统堆栈指针 +0x04 +0x08 +0x0C +0x10 +0x14 +0x18 +0x1C +0x20 +0x24 +0x28 +0x2C +0x30 +0x34 +0x38 用户堆栈指针 返回地址 称为orig_eax(original eax之意) 图3.6 进入中断服务程序时系统堆栈示意图 此时系统堆栈中各项相对于堆栈指针的位置如图 3.6 所示,而 arch/i386/kernel/entry.S 中也根据这些 关系定义了一些常数: 00050: EBX = 0x00 00051: ECX = 0x04 00052: EDX = 0x08 00053: ESI = 0x0C 00054: EDI = 0x10 00055: EBP = 0x14 00056: EAX = 0x18 2006-12-31 版权所有,侵权必究 第 207 页,共 1481 页 00057: DS = 0x1C Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 208 页,共 1481 页 00058: ES = 0x20 00059: ORIG_EAX = 0x24 00060: EIP = 0x28 00061: CS = 0x2C 00062: EFLAGS = 0x30 00063: OLDESP = 0x34 00064: OLDSS = 0x38 这里的 EAX,举例来说,当出现在 entry.S 的代码中时并不是表示寄存器%%eax,而是表示该寄存 器的内容在系统堆栈中的位置相对于此时的堆栈指针的位移。前面在转入 common_interrupt 之前压入堆 栈的(中断调用号 - 256)所在的位置称为 ORIG_EAX,对中断服务程序而言它代表着中断请求号。 回到 common_interrupt 的代码。在 SAVE_ALL 以后,又将一个程序标号(入口)ret_from_intr 压入 堆栈,并通过 jmp 指令转入另一段程序 do_IRQ()。读者可能已注意到,IRQ0x03_interrupt 和 common_interrupt 本质上都不是函数,它们都没有与 return 相当的指令,所以从 common_interrupt 不能 返回到 IRQ0x03_interrupt,而 从 IRQ0x03_interrupt 也不能执行中断返回。可是,do_IRQ()却是一个函数。 所以在通过 jmp 指令转入 do_IRQ()之前将返回地址 ret_from_intr 压入堆栈就模拟了一次函数调用,仿佛 对 do_IRQ()的调用就发生在 CPU 进入 ret_from_intr 的第一条指令前夕一样。这样,当从 do_IRQ()返回 时就会“返回”到 ret_from_intr 继续执行。do_IRQ()是在 arch/i386/kernel/irq.c 中定义的,我们先来看开 头几行: [IRQ0x03_interrupt -> common_interrupt -> do_IRQ()] 00543: /* 00544: * do_IRQ handles all normal device IRQ's (the special 00545: * SMP cross-CPU interrupts have their own specific 00546: * handlers). 00547: */ 00548: asmlinkage unsigned int do_IRQ(struct pt_regs regs) 00549: { 00550: /* 00551: * We ack quickly, we don't want the irq controller 00552: * thinking we're snobs just because some other CPU has 00553: * disabled global interrupts (we have already done the 00554: * INT_ACK cycles, it's too late to try to pretend to the 00555: * controller that we aren't taking the interrupt). 00556: * 00557: * 0 return value means that this irq is already being 00558: * handled by some other CPU. (or is disabled) 00559: */ 00560: int irq = regs.orig_eax & 0xff; /* high bits used in ret_from_ code */ 00561: int cpu = smp_processor_id(); 00562: irq_desc_t *desc = irq_desc + irq; 00563: struct irqaction * action; 00564: unsigned int status; 00565: 函数的调用参数是一个 pt_regs 数据结构。注意,这是一个数据结构,而不是指向数据结构的指针。 也就是说,在堆栈中的返回地址以上的位置上应该是一个数据结构映象。数据结构 struct pt_regs 是在 include/asm-i386/ptrace.h 中定义的: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 209 页,共 1481 页 00023: /* this struct defines the way the registers are stored on the 00024: stack during a system call. */ 00025: 00026: struct pt_regs { 00027: long ebx; 00028: long ecx; 00029: long edx; 00030: long esi; 00031: long edi; 00032: long ebp; 00033: long eax; 00034: int xds; 00035: int xes; 00036: long orig_eax; 00037: long eip; 00038: int xcs; 00039: long eflags; 00040: long esp; 00041: int xss; 00042: }; 相信读者一定会联想到前面讲过的系统堆栈的内容并且恍然大悟:原来前面所作的一切,包括 CPU 在进入中断时自动做的,实际上都是在为 do_IRQ()建立一个模拟的子程序调用环境,使得在 do_IRQ() 中既可以方便地知道进入中断前夕各个寄存器的内容,又可以在执行完毕后返回到 ret_from_intr,并且 从那里执行中断返回。可想而知,当 do_IRQ()调用具体的中断服务程序时也一定会把 pt_regs 数据结构 的内容传下去,不过那时只要传一个指针就够了。读者不妨回顾一下我们在第 2 章中讲过的页面异常服 务程序 do_page_fault(),其调用参数表为: asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code); 第一个参数就是指向 struct pt_regs 的指针,实际上就是指向系统堆栈中的那块地方。当时我们无法 将这一点讲清楚,所以略了过去。而现在结合进入中断的过程一看就清楚了。不过,页面异常并不属于 通用的中断请求,而是为 CPU 保留专用的,所以中断发生时并不经过 do_IRQ()这条路线,但是对于系 统堆栈的这种安排基本上是一致的。 以后读者还会看到,对系统堆栈的这种安排不光用于中断,还用于系统调用。 前面讲过,在 IRQ0x03_interrupt 中把数值(0x03 - 256)压入堆栈的目的是使得在公共的中断处 理程序中可以知道中断的来源,现在进入 do_IRQ()以后的第一件事情就是要弄清楚这一点。以 IRQ3 为 例,压入堆栈的数值为 0xffffff03,现在通过 regs.orig_eax 读回来并且把高位屏蔽掉,就又得到 0x03。由 于 do_IRQ()仅用于中断服务,所以不需要顾及系统调用时的情况。 代码中 561 行的 smp_processor_id()是为多处理器 SMP 结构而设的,在单处理器系统中总是返回 0。 现在,既然中断请求号已经恢复,从数组 irq_desc[]中找到相应的中断请求队列当然是轻而易举的了(562 行)。下面就是对具体中断请求队列的操作了。我们继续在 do_IRQ()中往下看: [IRQ0x03_interrupt -> common_interrupt -> do_IRQ()] 00566: kstat.irqs[cpu][irq]++; 00567: spin_lock(&desc->lock); 00568: desc->handler->ack(irq); 00569: /* 00570: REPLAY is when Linux resends an IRQ that was dropped earlier Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 210 页,共 1481 页 00571: WAITING is used by probe to mark irqs that are being tested 00572: */ 00573: status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING); 00574: status |= IRQ_PENDING; /* we _want_ to handle it */ 00575: 00576: /* 00577: * If the IRQ is disabled for whatever reason, we cannot 00578: * use the action we have. 00579: */ 00580: action = NULL; 00581: if (!(status & (IRQ_DISABLED | IRQ_INPROGRESS))) { 00582: action = desc->action; 00583: status &= ~IRQ_PENDING; /* we commit to handling */ 00584: status |= IRQ_INPROGRESS; /* we are handling it */ 00585: } 00586: desc->status = status; 00587: 当通过中断门进入中断服务时,CPU 的中断响应机制就自动被关闭了。既然已经关闭中断,为什么 567 行还要调用 spin_lock()加锁呢?这是为多处理器的情况而设置的,我们将在“多处理器 SMP 系统结 构”一章中讲述,这里暂且只考虑单处理器结构。 中断处理器(如 i8259A)在将中断请求“上报”到 CPU 以后,期待 CPU 给它一个确认(ACK), 表示“我已经在处理”,这里的 568 行就是做这件事。对函数指针 desc->handle->ack 的设置前面已经讲 过。从 569 行至 586 行主要是对 desc->status,即中断通道状态的处理和设置,关键在于将其 IRQ_INPROGRESS 标志位设成 1,而将 IRQ_PENDING 标志位清 0。其中 IRQ_INPROGRESS 主要是为 多处理器设置的,而 IRQ_PENDING 的作用下面就会看到: [IRQ0x03_interrupt -> common_interrupt -> do_IRQ()] 00588: /* 00589: * If there is no IRQ handler or it was disabled, exit early. 00590: Since we set PENDING, if another processor is handling 00591: a different instance of this same irq, the other processor 00592: will take care of it. 00593: */ 00594: if (!action) 00595: goto out; 00596: 00597: /* 00598: * Edge triggered interrupts need to remember 00599: * pending events. 00600: * This applies to any hw interrupts that allow a second 00601: * instance of the same irq to arrive while we are in do_IRQ 00602: * or in the handler. But the code here only handles the _second_ 00603: * instance of the irq, not the third or fourth. So it is mostly 00604: * useful for irq hardware that does not mask cleanly in an 00605: * SMP environment. 00606: */ Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 211 页,共 1481 页 00607: for (;;) { 00608: spin_unlock(&desc->lock); 00609: handle_IRQ_event(irq, ®s, action); 00610: spin_lock(&desc->lock); 00611: 00612: if (!(desc->status & IRQ_PENDING)) 00613: break; 00614: desc->status &= ~IRQ_PENDING; 00615: } 00616: desc->status &= ~IRQ_INPROGRESS; 00617: out: 00618: /* 00619: * The ->end() handler has to deal with interrupts which got 00620: * disabled while the handler was running. 00621: */ 00622: desc->handler->end(irq); 00623: spin_unlock(&desc->lock); 如果某一个中断请求队列的服务是关闭着的(IRQ_DISABLE 标志位为 1),或者 IRQ_INPROGRESS 标志位为 1,或者队列是空的,那么指针 action 为 NULL(见 580 和 582 行),无法往下执行了,所以只 好返回。但是,在这集中情况下 desc->status 中的 IRQ_PENDING 标志位为 1(见 574 和 583 行)。这样, 以后当 CPU(在多处理器系统结构中有可能有另一个 CPU)开启该队列的服务时,会看到这个标志位而 补上一次中断服务,称为“IRQ_REPLAY”。而如果队列是空的,那么整个通道也必然是关着的,因为 这是在将第一个服务程序挂入队列时才开启的。所以,这两种情形实际上相同。最后一种情况是服务已 经开启,队列也不是空的,可是 IRQ_INPROGRESS 标志为 1。这只有在两种情形下才会发生。一种情 形是在多处理器 SMP 系统结构中,一个 CPU 正在中断服务,而另一个 CPU 又进入了 do_IRQ(),这时 候由于队列的 IRQ_INPROGRESS 标志为 1 而经 595 行返回,此时 desc->status 中的 IRQ_PENDING 标志 位也是 1。第 2 种情形是在单处理器系统中 CPU 已经在中断服务程序中,但是因为某种原因又将中断开 启了,而且在同一个中断通道中又产生了一次中断。在这种情形下后面发生的那次中断也会因为 IRQ_INPROGRESS 标志为 1 而经 595 行返回,但也是将 desc->status 的 IRQ_PENDING 置成 1。总之, 这两种情形下最后的结构也是一样的,即 desc->status 中 IRQ_PENDING 标志位为 1。 那么,IRQ_PENDING 标志位到底是怎样其作用的呢?请看 612 和 613 行。这是在一个无限 for 循 环中,具体的中断服务是在 609 行的 handle_IRQ_event()中进行的。在进入 609 行时,desc->status 中的 IRQ_PENDING 标志位必然为 0。当 CPU 完成了具体的中断服务返回到 610 行以后,如果这个标志位仍 然为 0,那么循环就在 613 行结束了。而如果变成 1,那就说明已经发生过前述的某种情况,所以又循环 回到 609 行再服务一次。这样,就把本来可能发生的在同一通道上(甚至可能来自同一中断源)的中断 嵌套化解成为一个循环。 这样,同一个中断通道上的中断处理就得到了严格的“串行化”。也就是说,对于同一个 CPU 而言 不允许中断服务嵌套,而对不同的 CPU 则不允许并发地进入同一个中断服务程序。如果不是这样处理的 话,那就要求所有的中断服务程序都必须是“可重入”的“纯代码”,那样就使中断服务程序的设计和实 现复杂化了。这么一套机制的设计和实现,不能不说是非常周到、非常巧妙的。而 Linux 的稳定性和可 靠性也正是植根于这种从 Unix 时代继承下来、并经过时间考验的设计中。当然,在极端的情况下,也有 可能发生这样的情景:中断服务程序中总是把中断打开,而中断源又不断地产生中断请求,使得 CPU 每 次从 handle_IRQ_event()返回时 IRQ_PENDING 标志位永远是 1,从而使 607 行的 for 循环变成一个真正 的“无限”循环。如果真的发生这种情况而得不到纠正的话,那么该中断服务程序的作者应该另请高就 了。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 212 页,共 1481 页 还要指出,对 desc->status 的任何改变都是在加锁的情况下进行的,这也是出于对多处理器 SMP 系 统结构的考虑。 最后,在循环结束以后,只要本队列的中断服务程序还是开着的,就要对中断控制器执行一次“结 束中断服务”操作(622 行),具体取决于中断控制器硬件的要求,所调用的函数也是在队列初始化时设 置好的。 再看上面 for 循环中调用的 handle_IRQ_event(),这个函数一次执行队列中的各个中断服务程序,让 它们辨认本次中断请求是否来自各自的服务对象,即中断源,如果是就进而提供相应的服务。其代码也 在 irq.c 中: [IRQ0x03_interrupt -> common_interrupt -> do_IRQ() -> handle_IRQ_event()] 00418: /* 00419: * This should really return information about whether 00420: * we should do bottom half handling etc. Right now we 00421: * end up _always_ checking the bottom half, which is a 00422: * waste of time and is not what some drivers would 00423: * prefer. 00424: */ 00425: int handle_IRQ_event(unsigned int irq, struct pt_regs * regs, struct irqaction * action) 00426: { 00427: int status; 00428: int cpu = smp_processor_id(); 00429: 00430: irq_enter(cpu, irq); 00431: 00432: status = 1; /* Force the "do bottom halves" bit */ 00433: 00434: if (!(action->flags & SA_INTERRUPT)) 00435: __sti(); 00436: 00437: do { 00438: status |= action->flags; 00439: action->handler(irq, action->dev_id, regs); 00440: action = action->next; 00441: } while (action); 00442: if (status & SA_SAMPLE_RANDOM) 00443: add_interrupt_randomness(irq); 00444: __cli(); 00445: 00446: irq_exit(cpu, irq); 00447: 00448: return status; 00449: } 其中 430 行的 irq_enter()和 446 行的 irq_exit()只是对一个计数器进行操作,二者均定义于 include/asm-i386/hardirq.h: 00034: #define irq_enter(cpu, irq) (local_irq_count(cpu)++) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 213 页,共 1481 页 00035: #define irq_exit(cpu, irq) (local_irq_count(cpu)--) 当这个计数器的值为非 0 时就表示 CPU 正处于具体的中断服务程序中,以后读者会看到有些操作是 不允许在此期间进行的。 一般来说,中断服务程序都是在关闭中断(不包括“不可屏蔽中断”NMI)的条件下执行的,这也 就是 CPU 在穿越中断门时自动关中断的原因。但是,关中断是个既不可不用,又不可滥用的手段,特别 是当中断服务程序较长,操作比较复杂时,就有可能因关闭中断的时间持续太长而丢失其他的中断。经 验表明,允许中断在同一个中断源或同一个中断通道嵌套是应该避免的,因此内核在 do_IRQ()中通过 IRQ_PENDING 标志位的运用来保证了这一点。可是,允许中断在不同的通道上嵌套,则只要处理得当 就还是可行的。当然,必须十分小心。所以,在调用 request_irq()将一个中断服务程序挂入某个中断服务 队列时,允许将参数 irqflags 中的一个标志位 SA_INTERRUPT 置成 0,表示该服务程序应该在开启中断 的情况下执行。这里的 434~435 行和 444 行就是为此而设的(_sti()为开中断,cli()为关中断)。 然后,从 437 行至 441 行的 do_while 循环就是实质性的操作了。它依次调用队列中的每一个中断服 务程序。调用的参数有三:irq 为中断请求号;action->dev_d 是一个 void 指针,由具体的服务程序自行 解释和运用,这是由设备驱动程序在调用 request_irq()时自己规定的;最后一个就是前述的 pt_regs 数据 结构的指针 regs 了。至于具体的中断服务程序,那是设备驱动范畴内的东西,这里就不讨论了。 读者也许会问,如果中断请求队列中有多个服务程序存在,每次有来自这个通道的中断请求时就要 把队列中的所有服务程序依次执行一遍,岂非使效率大降?回答是:确实会有所下降,但不会严重。首 先,在每个具体的中断服务程序中都应该(通常都确实是)一开始就检查各自的中断源,一般是读相应 设备(接口卡上)的中断状态寄存器,看是否有来自该设备的中断请求,如没有就马上返回了,这个过 程一般只需要几条机器指令;其次,每个队列中服务程序的数量一般也不会太大。所以,实际上不会有 显著的影响。 最后,在 442 至 443 行,如果队列中的某个服务程序要为系统引入一些随机性的话,就调用 add_interrupt_randomness()来实现。有关详情在设备驱动一章中还会讲到。 从 handle_IRQ_event()返回的 status 的最低位必然为 1,这是在 432 行设置的。代码中还为此加了些 注解(418~424 行),其作用在看了下面这一段以后就会明白。我们随着 CPU 回到 do_IRQ()中继续往下 看: [IRQ0x03_interrupt -> common_interrupt -> do_IRQ()] 00625: if (softirq_active(cpu) & softirq_mask(cpu)) 00626: do_softirq(); 00627: return 1; 00628: } 到 624 行以后,从逻辑的角度说对中断请求的服务似乎已经完毕,可以返回了。可是 Linux 内核在 这里有个特殊的考虑,这就是所谓 softirq,即“(在时间上)软性的中断请求”,以前称为“bottom half”。 在 Linux 中,设备驱动程序的设计人员可以将中断服务分成两“半”,其实是两“部分”,而并不一定是 两“半”。第一部分是必须立即执行,一般是在关中断条件下执行的,并且必须是对每次请求都单独执行 的。而另一部分,即“后半” 部分,是可以稍后在开中断条件下执行的,并且往往可以将若干次中断服 务程序中剩下来的部分合并起来执行。这些操作往往是比较费时的,因而不适宜在关中断条件下执行, 或者不适宜一次占据 CPU 时间太长而影响对其他中断请求的服务。这就是所谓的“后半”(bottom half), 在内核代码中常简称为 bh。作为一个比喻,读者不妨想象在“cooked mode”下从键盘输入字符串的过 程(详见设备驱动),每当按一个键的时候,首先要把字符读进来,这要放在“前半”中执行;而进一步 检查所按的是否“回车”键,从而决定是否完成了一个字符串的输入,并进一步把睡眠中的进程唤醒, 则可以放在“后半”中执行。 执行 bh 的机制是内核中的一项“基础设施”,所以我们在下一节单独加以介绍。这里,读者暂且只 要知道有这么回事就行了。 在 do_softirq()中执行完相关的 bh 函数(如果有的话)以后,就到了从 do_IRQ()返回的时候了。返Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 214 页,共 1481 页 回到哪里?entry.S 中的标号 ret_from_intr 处,这是内核中处心积虑安排好了的。其代码在 arch/i386/kernel/entry.S 中: [IRQ0x03_interrupt -> common_interrupt -> … -> ret_from_intr] 00273: ENTRY(ret_from_intr) 00274: GET_CURRENT(%ebx) 00275: movl EFLAGS(%esp),%eax # mix EFLAGS and CS 00276: movb CS(%esp),%al 00277: testl $(VM_MASK | 3),%eax # return to VM86 mode or non-supervisor? 00278: jne ret_with_reschedule 00279: jmp restore_all 00280: 这里的 GET_CURRENT(%ebx)将指向当前进程的 task_struct 结构的指针置入寄存器 EBX。275 行和 276 行则在寄存器 EAX 中拼凑起由中断前夕寄存器 EFLAGS 的高 16 位和代码段寄存器 CS 的(16 位)内容构成的 32 位长整数。其目的是要检验: 中断前夕 CPU 是否运行于 VM86 模式。 中断前夕 CPU 运行于用户空间还是系统空间。 VM86 模式是为在 i386 保护模式下模拟运行 DOS 软件而设置的。在寄存器 EFLAGS 的高 16 位中有 个标志位表示 CPU 正在 VM86 模式中运行,我们对 VM86 模式不感兴趣,所以不予深究。而 CS 的最低 两位,那就有文章了。这两位代表着中断发生时 CPU 的运行级别 CPL。我们知道 Linux 虽然只采用两种 运行级别,系统为 0,用户为 3。所以,若是 CS 的最低两位为非 0,那就说明中断发生于用户空间。 顺便说一下,275 行的 EFLAGS(%esp)表示地址为堆栈指针%esp 的当前值加上常数 EFLAGS 处 的内容,这就是保存在堆栈中的中断前夕寄存器%eflags 的内容,常数 EFLAGS 我们已经在前面介绍过, 其值为 0x30。276 行中的 CS(%esp)也是一样。 如果中断发生于系统空间,控制就直接转移到 restore_all,而如果发生于用户空间(或 VM86 模式) 则转移到 ret_with_reschedule。这里我们假定中断发生于用户空间,因为从 ret_with_reschedule 最终还会 到达 restore_all。这段程序在同一文件(entry.S)中: [IRQ0x03_interrupt -> common_interrupt -> … -> ret_from_intr -> ret_with_reschedule] 00217: ret_with_reschedule: 00218: cmpl $0,need_resched(%ebx) 00219: jne reschedule 00220: cmpl $0,sigpending(%ebx) 00221: jne signal_return 00222: restore_all: 00223: RESTORE_ALL 00224: 00225: ALIGN 00226: signal_return: 00227: sti # we can get here from an interrupt handler 00228: testl $(VM_MASK),EFLAGS(%esp) 00229: movl %esp,%eax 00230: jne v86_signal_return 00231: xorl %edx,%edx 00232: call SYMBOL_NAME(do_signal) 00233: jmp restore_all 这里,首先检查是否需要进行一次进程调度。上面我们已经看到,寄存器 EBX 中的内容就是当前进Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 215 页,共 1481 页 程的 task_struct 结构指针,而 need_resched(%ebx)就表示该 task_struct 结构中位移为 need_resched 处 的内容。220 行的 sigpending(%ebx)也是一样。常数 need_resched 和 sigpending 的定义为(见 entry.S): 00071: /* 00072: * these are offsets into the task-struct. 00073: */ 00074: state = 0 00075: flags = 4 00076: sigpending = 8 00077: addr_limit = 12 00078: exec_domain = 16 00079: need_resched = 20 如果当前进程的 task_struct 结构中的 need_resched 字段为非 0,即表示需要进行调度,reschedule 也 在 arch/i386/kernel/entry.S 中: [IRQ0x03_interrupt -> common_interrupt -> … -> ret_from_intr -> ret_with_reschedule -> reschedule] 00287: reschedule: 00288: call SYMBOL_NAME(schedule) # test 00289: jmp ret_from_sys_call 程序在这里调用一个函数 schedule()进行调度,然后又转移到 ret_from_sys_call。我们将在系统调用 一节中再加以讨论。至于 schedule()则在进程一章中介绍,这里我们暂且假定不需要调度。读者以后会看 到,如果要调度的话,从 ret_from_sys_call 处经过一段略为曲折的道路最终也会到达 restore_all。 同样,如果当前进程的 task_struct 结构中的 sigpending 字段为非 0,就表示该进程有“信号”等待处 理,要先处理了这些待处理的信号才最后从中断返回,所以先转移到 226 行。在 228 行处先区分是否 VM86 模式,然后将寄存器%edx 的内容清 0(231 行)再调用 do_signal()。“信号(signal)”基本上是一种进程 间通信的手段,我们将在“进程间通信”一章中加以介绍。处理完信号以后,控制还是回到 222 行的 restore_all。实际上,ret_from_sys_call 最后还回到 ret_from_intr,最终殊途同归都会到达 restore_all,并 从那里执行中断返回。宏操作 RESTORE_ALL 的定义也在同一文件(entry.S)中: 00101: #define RESTORE_ALL \ 00102: popl %ebx; \ 00103: popl %ecx; \ 00104: popl %edx; \ 00105: popl %esi; \ 00106: popl %edi; \ 00107: popl %ebp; \ 00108: popl %eax; \ 00109: 1: popl %ds; \ 00110: 2: popl %es; \ 00111: addl $4,%esp; \ 00112: 3: iret; \ 显然,这是与进入内核时执行的宏操作 SAVE_ALL 遥相对应的。为方便读者加以对照,我们再把 SAVE_ALL(entry.S)列出在这里: 00086: #define SAVE_ALL \ 00087: cld; \ 00088: pushl %es; \ 00089: pushl %ds; \ Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 216 页,共 1481 页 00090: pushl %eax; \ 00091: pushl %ebp; \ 00092: pushl %edi; \ 00093: pushl %esi; \ 00094: pushl %edx; \ 00095: pushl %ecx; \ 00096: pushl %ebx; \ 00097: movl $(__KERNEL_DS),%edx; \ 00098: movl %edx,%ds; \ 00099: movl %edx,%es; 00100: 为什么在 RESTORE_ALL 的 111 行要将堆栈指针的当前值加 4?这是为了跳过 ORIG_EAX,那是在 进入中断之初压入堆栈的中断请求号(经过变形)。我们已经看到在 do_IRQ()中的第一件事就是从中取 出其最低 8 位,然后以此为下标从 irq_desc[]中找到相应的中断服务描述结构。以后在讲述系统调用和异 常处理时读者会进一步看到其作用。读者也许会问:那为什么不像对堆栈中的其他内容一样也使用 popl 指令呢?是的,在正常情况下确实应该使用 popl 指令,但是 popl 指令一定是与一个寄存器相联系的, 现在所有的寄存器都已占满了,还能 popl 到哪儿去呢? 这样,当 CPU 到达 112 行的 iret 指令时,系统堆栈又恢复到刚进入中断门时的状态,而 iret 则使 CPU 从中断返回。根进入中断时相对应,如果是从系统态返回到用户态就会将当前堆栈切换到用户堆栈。 3.5. 软中断和Bottom Half 中断服务一般是在将中断请求关闭的条件下执行的,以避免嵌套而使控制复杂化。可是,如果关中 断的时间持续太长就可能因为 CPU 不能及时响应其他的中断请求而使中断(请求)丢失,为此,内核允 许在将具体的中断服务程序挂入中断请求队列时将 SA_INTERRUPT 标志置成 0,使这个中断服务程序 在开中断的条件下执行。然而,实际的情况往往是:若在服务的全过程关中断则“扩大打击面”,而全过 程开中断则又造成“不安定因素”,很难取舍。一般来说,一次中断服务的过程常常可以分成两部分。开 头的部分往往是必须在关中断条件下执行的。这样才能在不受干扰的条件下“原子”地完成一些关键性 操作。同时,这部分操作的时间性又往往很强,必须在中断请求发生后“立即”或至少是在一定的时间 限制中完成,而且相继的多次中断请求也不能合并在一起来处理。而后半部分,则通常可以、而且应该 在开中断条件下执行,这样才不至于因将中断关闭过久而造成其他中断的丢失。同时,这些操作常常允 许延迟到稍后才来执行,而且有可能将多次中断服务中的相关部分合并在一起处理。这些不同的性质常 常使中断服务的前后两半明显地区分开来,可以、而且应该分别加以不同的实现。这里的后半部分就称 为“bottom half”,在内核代码中常常缩写为 bh。这个概念在相当程度上来自 RISC 系统结构。在 RISC 的 CPU 中,通常都有大量的寄存器。当中断发生时,要将所有这些寄存器的内容都压入堆栈,并在返回 时加以恢复,为此而付出很高的代价。所以,在 RISC 结构的系统中往往把中断服务分成两部分。第一 部分只保存为数不多的寄存器(内容),并利用这为数不多的寄存器来完成有限的关键性的操作,称为“轻 量级中断”。而另一部分,那就相当于这里的 bh 了。虽然 i386 的结构主要是 CISC 的,面临的问题不尽 相同,但前述的问题已经使 bh 的必要性在许多情况下变得很明显了。 Linux 内核为将中断服务分成两半提供了方便,并设立了相应的机制。在以前的内核中,这个机制 就称为 bh。但是,在 2.4 版(确切地说是 2.3.43)中有了新的发展和推广。 以前的内核中设置了一个函数指针数组 bh_base[],其大小为 32,数组中的每个指针可以用来指向一 个具体的 bh 函数。同时,又设置了两个 32 位无符号整数 bh_active 和 bh_mask,每个无符号整数中的 32 位对应着数组 bh_base[]中的 32 个元素。 我们可以在中断与 bh 二者之间建立起一种类比。 1) 数组 bh_base[]相当于硬件中断机制中的数组 irq_desc[]。不过 irq_desc[]中的每个元素代表着一Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 217 页,共 1481 页 个中断通道,所以是一个中断服务程序队列。而 bh_base[]中的每个元素却最多只能代表一个 bh 函数。但是,尽管如此,二者在概念上还是相同的。 2) 无符号整数 bh_active 在概念上相当于硬件的“中断请求寄存器”,而 bh_mask 则相当于“中断 屏蔽寄存器”。 3) 需要执行一个 bh 函数时,就通过一个函数 mark_bh()将 bh_active 中的某一位设成 1,相当于中 断源发出了中断请求,而所设置的具体标志位则类似于“中断向量”。 4) 如果相当于“中断屏蔽寄存器”的 bh_mask 中的相应位也是 1,即系统允许执行这个 bh 函数, 那么就会在每次执行完 do_IRQ()中的中断服务程序以后,以及每次系统调用结束之时,在一个 函数 do_bottom_half()中执行相应的 bh 函数。而 do_bottom_half(),则类似于 do_IRQ()。 为了简化 bh 函数的设计,在 do_bottom_half()中也像 do_IRQ()中一样,把 bh 函数的执行严格地“串 行化”了。这种串行化有两方面的考虑和措施。 一方面,bh 函数的执行不允许嵌套。如果在执行 bh 函数的过程中发生中断,那么由于每次中断服 务以后在 do_IRQ()中都要检查和处理 bh 函数的执行,就有可能嵌套。为此,在 do_bottom_half()中针对 同一 CPU 上的嵌套执行了加锁。这样,如果进入 do_bottom_half()以后发现已经上了锁,就立即返回。 因为这说明 CPU 在本次中断发生之前已经在这个函数中了。 另一方面,是在多 CPU 系统中,在同一时间内最多允许一个 CPU 执行 bh 函数,以防止有两个甚至 更多个 CPU 同时来执行 bh 函数而互相干扰。为此在 do_bottom_half()中针对不同 CPU 同时执行 bh 函数 也加了锁。这样,如果进入 do_bottom_half()以后发现这个锁已经锁上,就说明已经有 CPU 在执行 bh 函 数,所以也立即返回。 这两条措施,特别是第二条措施,保证了从单 CPU 结构到多 CPU SMP 结构的平稳过渡。可是,在 当时的 Linux 内核中可以在多 CPU SMP 结构上稳定运行以后,就慢慢发现这样的处理对于多 CPU SMP 结构的性能有不利的影响。原因就在于上述的第二个措施使 bh 函数的执行完全串行化了。当系统中有很 多 bh 函数需要执行时,虽然系统中有多个 CPU 存在,却只有一个 CPU 这么一个“独木桥”。跟 do_IRQ() 作一比较就可以发现,在 do_IRQ()中的串行化只是针对一个具体中断通道的,而 bh 函数的串行化却是 全局性的,所以是“防卫过当”了。既然如此,就应该考虑放宽上述的第二条措施。但是,如果放宽了 这一条,就要对 bh 函数本身的设计和实现有更高的要求(例如对全局变量的互斥),而原来已经存在的 bh 函数显然不符合这些要求。所以,比较好的办法是保留 bh,另外再增设一种或几种机制,并把它们纳 入一个统一的框架中。这就是 2.4 版中的“软中断”(softirq)机制。 从字面上说 softirq 就是软中断,可是“软中断”这个词(尤其是在中文里)已经被用作“信号”(signal) 的代名词,因为信号实际上就是“以软件手段实现的中断机制”。但是,另一方面,把类似于 bh 的机制 称为“软中断”又却是很贴切。这一方面反映了上述 bh 函数与中断之间的类比,另一方面也反映了这是 一种在时间要求上更为软性的中断请求。实际上,这里所体现的是层次的不同。如果说“硬中断”通常 是外部设备对 CPU 的中断,那么 softirq 通常是“硬中断服务程序”对内核的中断,而“信号”则是由 内核(或其他进程)对某个进程的中断。后面这二者都是由软件产生的“软中断”。所以,对“软中断” 这个词的含意要根据上下文加以区分。 下面,我们以 bh 函数为主线,通过阅读代码来叙述 2.4 版内核的软中断(softirq)机制。 系统在初始化时通过函数 softirq_init()对内核的软中断机制进行初始化。其代码在 kernel/softirq.c 中: 00281: void __init softirq_init() 00282: { 00283: int i; 00284: 00285: for (i=0; i<32; i++) 00286: tasklet_init(bh_task_vec+i, bh_action, i); 00287: 00288: open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 218 页,共 1481 页 00289: open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL); 00290: } 软中断本身是一种机制,同时也是一个框架。在这个框架里有 bh 机制,这是一种特殊的软中断,也 可以说是设计最保守的,但却是最简单、最安全的软中断。除此之外,还有其他的软中断,定义于 include/linux/interrupt.h: 00056: enum 00057: { 00058: HI_SOFTIRQ=0, 00059: NET_TX_SOFTIRQ, 00060: NET_RX_SOFTIRQ, 00061: TASKLET_SOFTIRQ 00062: }; 这里最值得注意的是 TASKLET_SOFTIRQ,代表着一种称为 tasklet 的机制。也许采用 tasklet 这个词 的原意在于表示这是一片小小的“任务”,但是这个词容易使人联想到“task”即进程而引起误会,其实 二者毫无关系。显然,NET_TX_SOFTIRQ 和 NET_RX_SOFTIRQ 两种软中断是专为网络操作而设的, 所以在 softirq_init()中只对 TASKLET_SOFTIRQ 和 HI_SOFTIRQ 两种软中断进行初始化。 先看 bh 机制的初始化。内核中为 bh 机制设置了一个数据结构 bh_task_vec[],这是 tasklet_struct 数 据结构的数组。这种数据结构的定义也在 interrupt.h 中: 00097: /* Tasklets --- multithreaded analogue of BHs. 00098: 00099: Main feature differing them of generic softirqs: tasklet 00100: is running only on one CPU simultaneously. 00101: 00102: Main feature differing them of BHs: different tasklets 00103: may be run simultaneously on different CPUs. 00104: 00105: Properties: 00106: * If tasklet_schedule() is called, then tasklet is guaranteed 00107: to be executed on some cpu at least once after this. 00108: * If the tasklet is already scheduled, but its excecution is still not 00109: started, it will be executed only once. 00110: * If this tasklet is already running on another CPU (or schedule is called 00111: from tasklet itself), it is rescheduled for later. 00112: * Tasklet is strictly serialized wrt itself, but not 00113: wrt another tasklets. If client needs some intertask synchronization, 00114: he makes it with spinlocks. 00115: */ 00116: 00117: struct tasklet_struct 00118: { 00119: struct tasklet_struct *next; 00120: unsigned long state; 00121: atomic_t count; 00122: void (*func)(unsigned long); 00123: unsigned long data; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 219 页,共 1481 页 00124: }; 代码的作者加了详细的注释,说 tasklet 是“多序”(不是“多进程”或“多线程”!)的 bh 函数。为 什么这么说呢?因为对 tasklet 的串行化不像对 bh 函数那样严格,所以允许在不同的 CPU 上同时执行 tasklet,但必须是不同的 tasklet。一个 tasklet_struct 数据结构就代表着一个 tasklet,结构中的函数指针 func 指向其服务程序。那么,为什么在 bh 机制中要使用这种数据结构呢?这是因为 bh 函数的执行(并不是 bh 函数本身)就是作为一个 tasklet 来实现的,在此基础上再加上更严格的限制,就成了 bh。 函数 tasklet_init()的代码再 kernel/softirq.c 中: [softirq_init() > tasklet_init()] 00203: void tasklet_init(struct tasklet_struct *t, 00204: void (*func)(unsigned long), unsigned long data) 00205: { 00206: t->func = func; 00207: t->data = data; 00208: t->state = 0; 00209: atomic_set(&t->count, 0); 00210: } 在 soft_init()中,对用于 bh 的 32 个 tasklet_struct 结构调用 tasklet_init()以后,它们的函数指针 func 全都指向 bh_action()。 对其他软中断的初始化是通过 open_softirq()完成的,其代码也在同一文件中: [softirq_init() > open_softirq()] 00103: static spinlock_t softirq_mask_lock = SPIN_LOCK_UNLOCKED; 00104: 00105: void open_softirq(int nr, void (*action)(struct softirq_action*), void *data) 00106: { 00107: unsigned long flags; 00108: int i; 00109: 00110: spin_lock_irqsave(&softirq_mask_lock, flags); 00111: softirq_vec[nr].data = data; 00112: softirq_vec[nr].action = action; 00113: 00114: for (i=0; i tasklet_hi_schedule()] Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 222 页,共 1481 页 00171: static inline void tasklet_hi_schedule(struct tasklet_struct *t) 00172: { 00173: if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) { 00174: int cpu = smp_processor_id(); 00175: unsigned long flags; 00176: 00177: local_irq_save(flags); 00178: t->next = tasklet_hi_vec[cpu].list; 00179: tasklet_hi_vec[cpu].list = t; 00180: __cpu_raise_softirq(cpu, HI_SOFTIRQ); 00181: local_irq_restore(flags); 00182: } 00183: } 这里的 smp_processor_id()返回当前进程所在 CPU 的编号,然后以次为下标从 tasklet_hi_vec[]中找到 该 CPU 的队列头,把参数 t 所指的 tasklet_struct 数据结构链入这个队列。由此可见,对执行 bh 函数的 要求是在哪一个 CPU 上提出的,就把它“调度”在哪一个 CPU 上执行,函数名中的“schedule”就是这 个意思,而与“进程调度”毫无关系。另一方面,一个 tasklet_struct 代表着对 bh 函数的一次执行,在同 一时间内只能把它链入一个队列中,而不可能同时出现在多个队列中。对于同一个 tasklet_struct 数据结 构,如果已经对齐调用了 tasklet_hi_schedule(),而尚未得到执行,就不允许再将其链入队列,所以在数 据结构中设置了一个标志位 TASKLET_STATE_SCHED 来保证这一点。最后,还要通过 __cpu_raise_softirq()正式发出软中断请求。 [mark_bh() > tasklet_hi_schedule() > __cpu_raise_softirq()] 00077: static inline void __cpu_raise_softirq(int cpu, int nr) 00078: { 00079: softirq_active(cpu) |= (1<action(h); 00079: h++; 00080: active >>= 1; 00081: } while (active); 00082: 00083: local_irq_disable(); 00084: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 224 页,共 1481 页 00085: active = softirq_active(cpu); 00086: if ((active &= mask) != 0) 00087: goto retry; 00088: } 00089: 00090: local_bh_enable(); 00091: 00092: /* Leave with locally disabled hard irqs. It is critical to close 00093: * window for infinite recursion, while we help local bh count, 00094: * it protected us. Now we are defenceless. 00095: */ 00096: return; 00097: 00098: retry: 00099: goto restart; 00100: } 软中断服务程序既不允许在一个硬件中断服务程序内部执行,也不允许在一个软中断服务程序内部 执行,所以要通过一个宏操作 in_interrupt()加以检测,这是在 include/asm-i386/hardirq.c 中定义的: 00020 /* 00021 * Are we in an interrupt context? Either doing bottom half 00022 * or hardware interrupt processing? 00023 */ 00024 #define in_interrupt() ({ int __cpu = smp_processor_id(); \ 00025 (local_irq_count(__cpu) + local_bh_count(__cpu) != 0); }) 显然,这个测试防止了软中断服务程序的嵌套,这就是前面讲的第一条串行化措施。与 local_bh_disable()有关的定义在 include/asm-i386/softirq.h 中: 00007: #define cpu_bh_disable(cpu) do { local_bh_count(cpu)++; barrier(); } while (0) 00008: #define cpu_bh_enable(cpu) do { barrier(); local_bh_count(cpu)--; } while (0) 00009: 00010: #define local_bh_disable() cpu_bh_disable(smp_processor_id()) 00011: #define local_bh_enable() cpu_bh_enable(smp_processor_id()) 从 do_softirq()的代码中可以看出,使 CPU 不能执行软中断服务程序的“关卡”只有一个,那就是 in_interrupt(),所以对软中断服务程序的执行并没有采取前述的第二条措施。这就是说,不同的 CPU 可 以同时进入对软中断服务程序的执行(见 78 行),分别执行各自所请求的软中断服务。从这个意义上, 软中断服务程序的执行是“并发”的、多序的。但是,这些软中断服务程序的设计和实现必须十分小心, 不能让它们互相干扰(例如通过共享的全局量)。至于 do_softirq()中其他的代码,则读者不会感到困难, 我们就不多说了。 在我们这个情景中,如前所述,执行的服务程序为 bh_action(),其代码在 kernel/softirq.c 中: [do_softirq() > bh_action()] 00235: /* BHs are serialized by spinlock global_bh_lock. 00236: 00237: It is still possible to make synchronize_bh() as 00238: spin_unlock_wait(&global_bh_lock). This operation is not used 00239: by kernel now, so that this lock is not made private only 00240: due to wait_on_irq(). Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 225 页,共 1481 页 00241: 00242: It can be removed only after auditing all the BHs. 00243: */ 00244: spinlock_t global_bh_lock = SPIN_LOCK_UNLOCKED; 00245: 00246: static void bh_action(unsigned long nr) 00247: { 00248: int cpu = smp_processor_id(); 00249: 00250: if (!spin_trylock(&global_bh_lock)) 00251: goto resched; 00252: 00253: if (!hardirq_trylock(cpu)) 00254: goto resched_unlock; 00255: 00256: if (bh_base[nr]) 00257: bh_base[nr](); 00258: 00259: hardirq_endlock(cpu); 00260: spin_unlock(&global_bh_lock); 00261: return; 00262: 00263: resched_unlock: 00264: spin_unlock(&global_bh_lock); 00265: resched: 00266: mark_bh(nr); 00267: } 这里对具体 bh 函数的执行(见 257 行)又设置了两道关卡。一道是 hardirq_trylock(),其定义为: 00031: #define hardirq_trylock(cpu) (local_irq_count(cpu) == 0) 与前面的 in_interrupt()比较一下就可看出,这还是在防止从一个硬中断服务程序内部调用 bh_action()。而另一道关卡 spin_trylock()就不同了,它的代码在 include/linux/spinlock.h 中: 00074: #define spin_trylock(lock) (!test_and_set_bit(0,(lock))) 这把“锁”就是全局变量 global_bh_lock,只要有一个 CPU 在 253 行至 260 行之间运行别的 CPU 就 不能进入这个区间,所以在任何时间最多只有一个 CPU 在执行 bh 函数。这就是前述的第二条串行化措 施。至于根据 bh 函数编号执行相应的函数,那就很简单了。在我们这个情景中,具体的 bh 函数是 timer_bh(),我们将在“时钟中断”一节中阅读这个函数的代码。 作为对比,我们列出另一个软中断服务程序 tasklet_action()的代码,读者可以把它与 bh_action()比较, 看看有哪些重要的区别。这个函数的代码在 kernel/softirq.c 中: 00122: struct tasklet_head tasklet_vec[NR_CPUS] __cacheline_aligned; 00123: 00124: static void tasklet_action(struct softirq_action *a) 00125: { 00126: int cpu = smp_processor_id(); 00127: struct tasklet_struct *list; 00128: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 226 页,共 1481 页 00129: local_irq_disable(); 00130: list = tasklet_vec[cpu].list; 00131: tasklet_vec[cpu].list = NULL; 00132: local_irq_enable(); 00133: 00134: while (list != NULL) { 00135: struct tasklet_struct *t = list; 00136: 00137: list = list->next; 00138: 00139: if (tasklet_trylock(t)) { 00140: if (atomic_read(&t->count) == 0) { 00141: clear_bit(TASKLET_STATE_SCHED, &t->state); 00142: 00143: t->func(t->data); 00144: /* 00145: * talklet_trylock() uses test_and_set_bit that imply 00146: * an mb when it returns zero, thus we need the explicit 00147: * mb only here: while closing the critical section. 00148: */ 00149: #ifdef CONFIG_SMP 00150: smp_mb__before_clear_bit(); 00151: #endif 00152: tasklet_unlock(t); 00153: continue; 00154: } 00155: tasklet_unlock(t); 00156: } 00157: local_irq_disable(); 00158: t->next = tasklet_vec[cpu].list; 00159: tasklet_vec[cpu].list = t; 00160: __cpu_raise_softirq(cpu, TASKLET_SOFTIRQ); 00161: local_irq_enable(); 00162: } 00163: } 最后,软中断服务程序,包括 bh 函数,与常规中断服务程序的分离并不是强制性的,要根据设备驱 动的具体情况(也许还有设计人员的水平)来决定。 3.6. 页面异常的进入和返回 我们在第 2 章中介绍内核对页面异常处理时,是从 do_page_fault()开始的。当时因为尚未介绍 CPU 的中断和异常机制,所以暂时跳过了对页面异常的响应过程,也就是从发生异常至 CPU 到达 do_page_fault()之间的那一段路程,以及从 do_page_fault()返回之后到 CPU 返回到用户空间这一段路程。 现在,我们可以来补上这个缺口了。 与外设中断不同,各种异常都有为其保留的专用中断向量,因此相应的初始化也是直截了当的,这 一点我们已经在初始化一节中看到了。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 227 页,共 1481 页 为页面异常设置的中断门指向程序入口 page_fault(见 IDT 初始化一节中所引 trap_init()中的 970 行), 所以当发生页面异常时,CPU 穿过中断门以后就直接到达了 page_fault()。CPU 因异常而穿过中断门的 过程,包括堆栈的变化,与因外设中断而引起的过程基本上是一样的,读者可以参阅外设中断一节。但 是有一点很重要的不同。当中断发生时,CPU 将寄存器的 EFLAGS 的内容,以及代表返回地址的 CS 和 EIP 两个寄存器的内容压入堆栈。如果 CPU 的运行级别发生变化,则在此之前还要发生堆栈的切换,并 且要把代表老堆栈指针的 SS 和 ESP 的内容压入堆栈。这一点,我们已经在前面介绍过了。当异常发生 时,在上述这些操作之后,还要加上附加的操作。那就是:如果所发生的异常产生出错代码的话,就把 这个出错代码也压入堆栈。并非所有的异常都产生出错代码,有关详情可参考 Intel 的技术资料或相关专 著,但是绝大多数异常,包括我们这里所关心的页面异常是会产生出错代码的。而且,实际上我们在第 2 章中已经看到 do_page_fault()如何通过这个出错代码识别发生异常的原因。可是 CPU 只是在进入异常 时才知道是否应该把出错代码压入堆栈。而从异常处理通过 iret 指令返回时已经时过境迁,CPU 已经无 从知道当初发生异常的原因,因此不会自动跳过堆栈中的这一项,而要靠相应的异常处理程序对堆栈加 以调整,使得在 CPU 开始执行 iret 指令时堆栈顶部是返回地址。由于这个不同,对异常的处理和对中断 的处理在代码上也要有所不同。 页面异常处理的入口 page-fault 是在 arch/i386/entry.S 中定义的: 00410: ENTRY(page_fault) 00411: pushl $ SYMBOL_NAME(do_page_fault) 00412: jmp error_code 这里的跳转目标 error_code 就好像外设中断处理中的 common_interrupt 一样,是各种异常处理所共 用的程序入口。而将服务程序 do_page_fault()的地址压入堆栈,则为具体的服务程序做好了准备。程序 入口 error_code 的代码也在同一文件(entry.S)中: 00295: error_code: 00296: pushl %ds 00297: pushl %eax 00298: xorl %eax,%eax 00299: pushl %ebp 00300: pushl %edi 00301: pushl %esi 00302: pushl %edx 00303: decl %eax # eax = -1 00304: pushl %ecx 00305: pushl %ebx 00306: cld 00307: movl %es,%ecx 00308: movl ORIG_EAX(%esp), %esi # get the error code 00309: movl ES(%esp), %edi # get the function address 00310: movl %eax, ORIG_EAX(%esp) 00311: movl %ecx, ES(%esp) 00312: movl %esp,%edx 00313: pushl %esi # push the error code 00314: pushl %edx # push the pt_regs pointer 00315: movl $(__KERNEL_DS),%edx 00316: movl %edx,%ds 00317: movl %edx,%es 00318: GET_CURRENT(%ebx) Linux 内核源代码情景分析 00319: call *%edi 00320: addl $8,%esp 00321: jmp ret_from_exception 读者也许注意到了,这里并不像进入中断响应时那样引用 SAVE_ALL。让我们来看看有什么区别, 以及为什么。观察图 3.7,我们把 CPU 执行到这里的 307 行时的堆栈(左边)与 CPU 在外设中断时 SAVE_ALL 以后的堆栈(右边)作一比较。 顺便提一下,系统调用时的堆栈在执行完 SAVE_ALL 以后与图 3.7 的右边(中断)几乎完全一样, 只是在 ORIG_EAX 位置上是系统调用号而不是中断请求号。 EAX EDI ESI EBX ECX EDX EBP SS ESP EFLAGS CS EIP ORIG_EAX ES DS 堆栈指针 EAX EDI ESI EBX ECX EDX EBP SS ESP EFLAGS CS EIP 出错代码 do_page_fault DS 堆栈指针 (中断请求号) 图3.7 异常处理和中断处理系统堆栈对照图 比较之后,可以看到其实也只有在两个位置上不同。一个是与 ORIG_EAX 对应的位置上,现在是 CPU 在发生异常时压入堆栈的出错代码。另一个是在与 ES 相应的位置上,现在是 do_page_fault()的入口 地址。其他就都一样了。可是,下面会将堆栈中对应于 ORIG_EAX 位置上的内容转移到寄存器%esi 中, 并将其替换成%eax 中的内容。这样一来,出错代码就到了%esi 中,而堆栈中的 ORIG_EAX 就变成了-1 (见 298 行和 303 行)。同时,又以寄存器%ecx 的内容替换堆栈中 ES 处的函数指针,而把函数指针转 移到寄存器%edi 中。在此之前的 307 行已经将%es 的内容装入了%ecx,所以 在 311 行以后,堆栈的内容 于中断或系统调用时就完全一样了,只是 ORIG_EAX 的位置上为-1。这么一来,堆栈就调整好了。我们 在中断一节中已经看到将来返回时在 RESTORE_ALL 中会把 ORIG_EAX 跳过去。 读者也许会问:那么,对于不产生出错代码的异常又怎样处理呢?很简单,在进入 error_code 之前 补上一个就是了。请看,同一源文件(entry.S)中因协处理器(coprocessor)出错而导致的异常 coprocessor_error: 00323: ENTRY(coprocessor_error) 00324: pushl $0 00325: pushl $ SYMBOL_NAME(do_coprocessor_error) 00326: jmp error_code 这里多了一行“pushl $0”,将 0 压入堆栈中与出错代码相应的地方,此后就都一样了。 回到前面 error_code 的代码中,第 313 行和 314 行先后把%esi 和%edx 的内容压入堆栈。我们知道, %esi 中是出错代码,而 312 行已经把堆栈指针的当前内容拷贝到%edx 中。在中断一节中我们已经讲过, 内核将 SAVE_ALL 以后堆栈中的内容视同一个 pt_regs 数据结构,而当时的堆栈指针指向该数据结构的 2006-12-31 版权所有,侵权必究 第 228 页,共 1481 页 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 229 页,共 1481 页 起点。所以,这二者一项是出错代码而另一项便是 pt_regs 结构指针,这正是 do_page_fault()的两个调用 参数。把调用参数压栈以后,就为 319 行的函数调用做好了准备。其他一些准备工作读者在中断响应中 都已看到过,这里就不重复了。 从调用的函数,在这里就是 do_page_fault()返回以后,CPU 就转入 ret_from_exception。由于 do_page_fault()的类型是 void,所以没有返回值。ret_from_exception 的代码也在 entry.S 中: [page_fault -> error_code -> … -> ret_from_exception] 00260: ret_from_exception: 00261: #ifdef CONFIG_SMP 00262: GET_CURRENT(%ebx) 00263: movl processor(%ebx),%eax 00264: shll $CONFIG_X86_L1_CACHE_SHIFT,%eax 00265: movl SYMBOL_NAME(irq_stat)(,%eax),%ecx # softirq_active 00266: testl SYMBOL_NAME(irq_stat)+4(,%eax),%ecx # softirq_mask 00267: #else 00268: movl SYMBOL_NAME(irq_stat),%ecx # softirq_active 00269: testl SYMBOL_NAME(irq_stat)+4,%ecx # softirq_mask 00270: #endif 00271: jne handle_softirq 00272: 00273: ENTRY(ret_from_intr) 00274: GET_CURRENT(%ebx) 00275: movl EFLAGS(%esp),%eax # mix EFLAGS and CS 00276: movb CS(%esp),%al 00277: testl $(VM_MASK | 3),%eax # return to VM86 mode or non-supervisor? 00278: jne ret_with_reschedule 00279: jmp restore_all 如果没有软中断请求需要处理,就直接进入 ret_from_intr。后面这些代码读者已经很熟悉了,要是 还有困难可以回到前几节再看看。 3.7. 时钟中断 在所有的外部中断中,时钟中断起着特殊的作用,其作用原非单纯的计时所能相比。当然,即使是 单纯的计时也已经足够重要了。别的不说,没有正确的时间关系,你用来重建内核的工具 make 就不能 正常运行了,因为 make 是靠时间标记来确定是否需要重新编译以及连接的。可是时钟中断的重要性还 远不止于此。 我们在中断一节中看到,内核在每次中断(以及系统调用和异常)服务完毕返回用户空间之前都要 检查是否需要调度,若有需要就进行进程调度。事实上,调度只有当 CPU 在内核中运行时才可能发生。 在进程一章中,读者将会看到进程调度发生在两种情况下。一种是“自愿”的,通过像 sleep()之类的系 统调用实现;或者是在通过其他系统调用进入内核以后因某种原因受阻需要等待,而“自愿”让内核调 度其他进程先进行运行。另一种是“强制”的,当一个进程连续运行的时间超过一定限度时,内核就会 强制地调度其他进程来运行。如果没有了时钟,内核就失去了与时间有关的强制调度的依据和时机,而 只能依赖于各个进程的“思想觉悟”了。试想,如果有一个进程在用户空间中陷入了死循环,而在循环 体内也没有作任何系统调用,并且也没有发生外设中断,那么,要是没有时钟中断,整个系统就在原地 打转什么事也不能做了。这是因为,在这种情况下永远不会有调度,而死抓住 CPU 不放的进程则陷在死 循环中。退一步讲,即使我们还有其他的准则(例如进程的优先级)来决定是否应该调度,那也得要有 中断、异常或系统调用使 CPU 进入内核运行才能发生调度。而唯一可以预测在一定时间内必定会发生的,Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 230 页,共 1481 页 就是“时钟中断”。所以,对于像 Linux 这样的“分时系统”来说,时钟中断是维护“生命”的必要条件, 难怪人们称时钟中断为“heart beat”,也即“心跳”。 在初始化阶段。在对外部中断的基础设施,也就是 IRQ 队列的初始化,以及对调度机制的初始化完 成后,就轮到时钟中断的初始化。请看 init/main.c 中 start_kernel()的片段: 00534: trap_init(); 00535: init_IRQ(); 00536: sched_init(); 00537: time_init(); 从这里我们也可以看出,时钟中断和调度是密切联系在一起的。以前也讲到过,一旦开始有时钟中 断就可能要进行调度,所以要先完成对调度机制的初始化,做好准备。函数 time_init()的代码在 arch/i386/kernel/time.c 中: 00626: void __init time_init(void) 00627: { 00628: extern int x86_udelay_tsc; 00629: 00630: xtime.tv_sec = get_cmos_time(); 00631: xtime.tv_usec = 0; ……………… 00704: setup_irq(0, &irq0); ……………… 00706: } 当我们提及“系统时钟”时,实际上是指着内核中的两个全局变量之一。一个是数据结构 xtime, 其类型为 struct timeval,是在 include/linux/time.h 中定义的: 00088: struct timeval { 00089: time_t tv_sec; /* seconds */ 00090: suseconds_t tv_usec; /* microseconds */ 00091: }; 数据结构中记载的是从历史上某一时刻开始的时间的“绝对值”,其数值来自计算机中一个 CMOS 晶片,常常称为“实时时钟”。这块 CMOS 晶片是由电池供电的,所以即使机器断了电也还能维持正确 的时间。上面的 630 行就是通过 get_cmos_time()从 CMOS 时钟晶片中把当时的实际时间读入 xtime,时 间的精度为秒,而时钟中断,则是由另一个晶片产生的。 另一个全局量是个无符号整数,叫 jiffies,记录着从开机以来时钟中断的次数。每个 jiffy 的长度就 是时钟中断的周期,有时候也称为一个 tick,取决于系统中的一个常数 HZ,这个常数定义于 include/asm-i386/param.h 中。以后读者也许会看到,在内核中 jiffies 远远比 xtime 重要,是个经常要用到 的变量。 系统中有很多因素会影响到时钟中断在时间上的精确度,所以要通过好多手段来加以校正。在比较 新的 i386 CPU 中(主要是 Pentium 及以后),还设置了一个特殊的 64 位寄存器,称为“时间印记计数器” (Time Stamp Counter)TSC。这个计数器对驱动 CPU 的时钟脉冲进行计数,例如要是 CPU 的时钟脉冲 频率位 500MHz,则 TSC 的计时精度为 2ns。由于 TSC 是个 64 位的计数器,其计数要经过连续运行上 千年才会溢出。显然,可以利用 TSC 的读数来改善时钟中断的精度。不过,我们在这里并不关心时间的 精度,所以跳过了代码中的有关部分,而只关注带有本质性的部分。 读者在中断一节中看到过 setup_irq(),可以回过头去看一下。这里的第一个参数为中断请求号,时 钟中断的请求号为 0。第二个参数是指向一个 irqaction 数据结构 irq0 的指针。irq0 也是在 time.c 中定义 的: 00547: static struct irqaction irq0 = { timer_interrupt, SA_INTERRUPT, Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 231 页,共 1481 页 0, "timer", NULL, NULL}; 可见,时钟中断的服务程序为 timer_interrupt();中断请求 0 为时钟中断专用,因为 irq0.falgs 中标志 位 SA_SHIRQ 为 0;而且在执行 timer_interrupt()的过程中不容许中断,因为标志位 SA_INTERRUPT 为 1。服务程序 timer_interrupt()的代码在同一个文件(time.c)中: 00454: /* 00455: * This is the same as the above, except we _also_ save the current 00456: * Time Stamp Counter value at the time of the timer interrupt, so that 00457: * we later on can estimate the time of day more exactly. 00458: */ 00459: static void timer_interrupt(int irq, void *dev_id, struct pt_regs *regs) 00460: { 00461: int count; 00462: 00463: /* 00464: * Here we are in the timer irq handler. We just have irqs locally 00465: * disabled but we don't know if the timer_bh is running on the other 00466: * CPU. We need to avoid to SMP race with it. NOTE: we don' t need 00467: * the irq version of write_lock because as just said we have irq 00468: * locally disabled. -arca 00469: */ 00470: write_lock(&xtime_lock); 00471: 00472: if (use_tsc) 00473: { 00474: /* 00475: * It is important that these two operations happen almost at 00476: * the same time. We do the RDTSC stuff first, since it's 00477: * faster. To avoid any inconsistencies, we need interrupts 00478: * disabled locally. 00479: */ 00480: 00481: /* 00482: * Interrupts are just disabled locally since the timer irq 00483: * has the SA_INTERRUPT flag set. -arca 00484: */ 00485: 00486: /* read Pentium cycle counter */ 00487: 00488: rdtscl(last_tsc_low); 00489: 00490: spin_lock(&i8253_lock); 00491: outb_p(0x00, 0x43); /* latch the count ASAP */ 00492: 00493: count = inb_p(0x40); /* read the latched count */ 00494: count |= inb(0x40) << 8; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 232 页,共 1481 页 00495: spin_unlock(&i8253_lock); 00496: 00497: count = ((LATCH-1) - count) * TICK_SIZE; 00498: delay_at_last_interrupt = (count + LATCH/2) / LATCH; 00499: } 00500: 00501: do_timer_interrupt(irq, NULL, regs); 00502: 00503: write_unlock(&xtime_lock); 00504: 00505: } 在这里我们并不关心多处理器 SMP 结构,也不关心时间的精度,所以实际上只剩下 501 行的 do_timer_interrupt(): [timer_interrupt() > do_timer_interrupt()] 00380: /* 00381: * timer_interrupt() needs to keep up the real-time clock, 00382: * as well as call the "do_timer()" routine every clocktick 00383: */ 00384: static inline void do_timer_interrupt(int irq, void *dev_id, struct pt_regs *regs) 00385: { 00386: #ifdef CONFIG_X86_IO_APIC …………………… 00400: #endif 00401: 00402: #ifdef CONFIG_VISWS 00403: /* Clear the interrupt */ 00404: co_cpu_write(CO_CPU_STAT,co_cpu_read(CO_CPU_STAT) & ~CO_STAT_TIMEINTR); 00405: #endif 00406: do_timer(regs); 00407: /* 00408: * In the SMP case we use the local APIC timer interrupt to do the 00409: * profiling, except when we simulate SMP mode on a uniprocessor 00410: * system, in that case we have to call the local interrupt handler. 00411: */ 00412: #ifndef CONFIG_X86_LOCAL_APIC 00413: if (!user_mode(regs)) 00414: x86_do_profile(regs->eip); 00415: #else 00416: if (!smp_found_config) 00417: smp_local_timer_interrupt(regs); 00418: #endif 00419: 00420: /* 00421: * If we have an externally synchronized Linux clock, then update 00422: * CMOS clock accordingly every ~11 minutes. Set_rtc_mmss() has to be Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 233 页,共 1481 页 00423: * called as close as possible to 500 ms before the new second starts. 00424: */ 00425: if ((time_status & STA_UNSYNC) == 0 && 00426: xtime.tv_sec > last_rtc_update + 660 && 00427: xtime.tv_usec >= 500000 - ((unsigned) tick) / 2 && 00428: xtime.tv_usec <= 500000 + ((unsigned) tick) / 2) { 00429: if (set_rtc_mmss(xtime.tv_sec) == 0) 00430: last_rtc_update = xtime.tv_sec; 00431: else 00432: last_rtc_update = xtime.tv_sec - 600; /* do it again in 60 s */ 00433: } 00434: 00435: #ifdef CONFIG_MCA …………………… 00449: #endif 00450: } 同样,我们在这里并不关心多处理器 SMP 结构中采用 APIC 时的特殊处理,也不关心 SGI 工作站(402 行~405 行)和 PS/2 的“Micro chanel”(435~449 行)的特殊情况,此外,我们在这里也不关心时钟的 精度(420~433 行)。 这样,就只剩下了两件事。一件事是 do_timer(),另一件是 x86_do_profile()。其中 x86_do_profile() 的目的在于积累统计信息,也不是我们关心的重点。最后只剩下 do_timer()了,那是在 kernel/timer.c 中: [timer_interrupt() > do_timer_interrupt() > do_timer()] 00674: void do_timer(struct pt_regs *regs) 00675: { 00676: (*(unsigned long *)&jiffies)++; 00677: #ifndef CONFIG_SMP 00678: /* SMP process accounting uses the local APIC timer */ 00679: 00680: update_process_times(user_mode(regs)); 00681: #endif 00682: mark_bh(TIMER_BH); 00683: if (TQ_ACTIVE(tq_timer)) 00684: mark_bh(TQUEUE_BH); 00685: } 这里的 676 行使 jiffies 加 1。细心的读者可能会问,为什么这里不用简单的“jiffies++”,而要使用 这么一种奇怪的方式呢?这是因为代码的作者要将递增 jiffies 的操作在一条指令中实现,成为一个“原 子”的操作。Gcc 将这条语句翻译成一条对内存单元的 INC 指令。而若采用“jiffies++”,则有可能会被 编译成先将 jiffies 的内容 MOV 至寄存器 EAX,然后递增,再 MOV 回去。二者所耗费的 CPU 时钟周期 几乎是相同的,但前者保证了操作的“原子”性。 函数 update_process_times()就与进程的调度有关了,我们将在进程调度一节中再来介绍。但是,从 函数的名字也可以看出,它处理的是当前进程与时间有关的变量,一方面是为统计的目的,另一方面也 是为调度的目的。对用于计时和统计的这些变量的操作可说是时钟中断的“前半”,可是 682 行和 684 行为时钟中断安排的“后半”和“第二职业”,却要耗费多得多的精力。 我们在前几节中已经介绍过中断服务程序的“后半”,即 bh。CPU 在从中断返回之前都要检查是否 有某个 bh 队列中还有事等着要处理。而这里的 682 行就通过 mark_bh()将 bh_task_vec[TIMER_BH]挂入Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 234 页,共 1481 页 tasklet_hi_vec 的队列中,使 CPU 在中断返回之前执行与 TIMER_BH 对应的函数 timer_bh(),这是事先设 置好了的。对此,在 kernel/sched.c 的 sched_init()中有三行重要的代码: 01260: init_bh(TIMER_BH, timer_bh); 01261: init_bh(TQUEUE_BH, tqueue_bh); 01262: init_bh(IMMEDIATE_BH, immediate_bh); 这里初始化了三个 bh。第一个显然是在每次时钟中断结束之前都要执行的,用来完成逻辑上属于是 中中断服务、但又不是那么紧急,或者可以在更宽松的环境(开中断)下完成的操作,其相应的函数为 timer_bh()。而 TQUEUE_BH 和 IMMEDIATE_BH,则又是内核中两项重要的基础设施。我们以前讲过, Linux 内核中可能的 bh 的数量是 32。读者心里可能已经在想,32 个 bh 够吗?如果需要更多怎么办?还 有,更重要地,在实践中常常会有要求让某些操作跟某个已经存在的中断服务动态地挂上钩,是一些操 作按运行时的需要“挂靠”在某种中断甚至某种其他的事件中。举例来说,如果我们要为一个外部设备 写驱动程序,该设备要求每 20ms 读一次它的状态寄存器,再根据读入的信息进行某些计算,并把计算 结果写入它的控制寄存器以驱动一台步进马达,而该设备并不具备产生中断的功能。其实,由于这个外 设的控制完全是周期性的,本来就不必使用独立的中断,所需要解决的只是怎样与系统的时钟中断挂上 钩。前面讲过,Linux 系统时钟的频率是由一个常数 HZ 决定的,定义于 include/asm-i386/param.h。通常 HZ 定义为 100,也即每 10ms 一次时钟中断,跟需要的 20ms 正好是整数倍关系。所以,如果写个程序, 并且能在每次时钟中断都调用它一次。而在程序中则设置一个计数器,使得每当计数为偶数时就采集数 据,为奇数时就计算并输出。这样就可以解决问题了。可是,怎样让时钟中断每次都来调用它呢? TQUEUE_BH 就是为这种需要而设置的。全局量 tq_timer 指向一个队列,想要让系统在每次时钟中断时 都来调用某个函数(当然是在系统空间),就将其挂入该队列里。而这里的 683 行则检查 tq_timer 是否为 空。如果不空就通过 mark_bh()把 bh_task_vec[TQUEUE_BH]也挂入 tasklet_hi_vec 的队列中,这样内核 就会在执行 bh 时通过 tqueue_bh()来将该队列中所有的函数都调用一遍。由此可见,TQUEUE_BH 确实 是一项很重要的基础设施。除与时钟中断挂钩的 tq_timer 队列外,还有其他一些 bh 和相应的队列, IMMEDIATE_BH 是其中之一。有关详情我们将在“进程”和“设备驱动”有关章节中介绍。如果说, 时钟中断的“前半”timer_interrupt()和“后半”timer_bh()还是它的“正业”的话,那么 tqueue_bh()的执 行便是“第二职业”了。 在做好这些准备以后,时钟中断服务的“前半”就完成了。可是读者在中断一节中已经看到,CPU 在返回途中,却在离开 do_IRQ()之前,先折入 do_softirq()去干它的“后半”和“第二职业”。在我们这 个情景中,timer_bh()肯定会得到执行,而 tqueue_bh()则在 tq_timer 队列非空时会得到执行。读者也许还 会问,既然 timer_bh()肯定是要执行的,为什么不干脆把它也放在 do_timer()中执行,而要费这么些周折 呢?首先,前面我们已经看到,执行 timer_interrupt()的整个过程中中断是关闭的(见前面的 SA_INTERRUPT 标志位);而 timer_bh()的执行则没有这么多严格的要求。其次,在 do_IRQ()的代码中 可以看出,对具体中断服务程序的执行与对 do_softirq()的执行不是一对一的关系。对具体中断服务程序 的执行是在一个循环中进行的,而 do_softirq()只执行一次。这样,当同一中断通道内紧接着发生了好几 次中断时,对 do_softirq(),从而对 timer_bh 的执行就推迟并且合并了。 与 TIMER_BH 对应的 timer_bh()在 kernel/timer.c 中: 00668: void timer_bh(void) 00669: { 00670: update_times(); 00671: run_timer_list(); 00672: } 先看同一文件(timer.c)中的 update_times(): [timer_bh() > update_times()] 00643: /* 00644: * This spinlock protect us from races in SMP while playing with xtime. -arca Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 235 页,共 1481 页 00645: */ 00646: rwlock_t xtime_lock = RW_LOCK_UNLOCKED; 00647: 00648: static inline void update_times(void) 00649: { 00650: unsigned long ticks; 00651: 00652: /* 00653: * update_times() is run from the raw timer_bh handler so we 00654: * just know that the irqs are locally enabled and so we don't 00655: * need to save/restore the flags of the local CPU here. -arca 00656: */ 00657: write_lock_irq(&xtime_lock); 00658: 00659: ticks = jiffies - wall_jiffies; 00660: if (ticks) { 00661: wall_jiffies += ticks; 00662: update_wall_time(ticks); 00663: } 00664: write_unlock_irq(&xtime_lock); 00665: calc_load(ticks); 00666: } 这里做了两件事。第一件事是 update_wall_time(),目的是处理所谓“实时时钟”或者说“挂钟”xtime 中的数值,包括计数、进位、以及为精度目的而作的校正。所涉及的主要也是数值的计算和处理,我们 就不深入进去了。这里的 wall_jiffies 也像 jiffies 一样是个全局量,它代表着与当前 xtime 中的数值相对 应的 jiffies 值,表示“挂钟”当前的读数已经校准到了时轴上的哪一点。 第二件事是 calc_load(),目的是计算和积累关于 CPU 负荷的统计信息。内核每个 5 秒钟计算、累计 和更新一次系统在过去的 15 分钟、10 分钟以及 1 分钟内平均有多少个进程处于可执行状态,作为衡量 系统负荷轻重的指标。由于涉及的主要是数值计算,所以我们也不深入进去了。 从 update_times()返回后,就是 timer_bh()的主体部分 run_timer_list()了。它检查系统中已经设置的各 个“定时器”(timer),如果某个定时器已经“到点”就执行为之预定的函数(这就是该定时器的 bh 函 数)。我们将在“进程与进程调度”一章中讲述定时器的设置,到那时再回过头来阅读 run_timer_list()的 代码。 每个定时器都由一个 timer_list 数据结构代表,定义于 include/linux/timer.h 中: 00020: struct timer_list { 00021: struct list_head list; 00022: unsigned long expires; 00023: unsigned long data; 00024: void (*function)(unsigned long); 00025: }; 这是一个用于链表的数据结构,链表的长度是动态的而不受限制,因此系统中可以设置的定时器数 量也不受限制(早期的实现采用数组,因而受到数组大小的限制)。每个定时器都有个到点时间 expires。 结构中的函数指针 function 指向预定在到点时执行的 bh 函数,并且可以带一个参数 data(早期的实现中 不能带参数)。如前所述,在执行 bh 函数时中断是打开的。 可见,在整个时钟中断服务的期间,大部分的操作是在“后半”,即 bh 函数中完成的。真正在关中Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 236 页,共 1481 页 断状态下执行的只是少量关键性的操作,而大量的操作尽可能要放在比较宽松的环境下,即开中断的条 件下,以及允许在时间上有所伸缩的条件下完成,这样才能将对系统的影响减至最小。一方面,这是应 该成为系统程序设计(特别是设备驱动程序)的一项准则;另一方面,这也对设计和开发的人员提出了 很高的要求,因为要区分一项操作是否必须在“前半”中执行,以及是否必须关中断,需要对系统有深 刻的理解。 3.8. 系统调用 如果说外部中断是使 CPU 被动地、异步地进入系统空间的一种手段,那么系统调用就是 CPU 主动 地、同步地进入系统空间的手段。这里所谓“主动”,是指 CPU“自愿”的、事先计划好了的行为。而 “同步”则是说,CPU(实际上是软件的设计人员)确切地知道在执行哪一条指令以后就一定会进入系 统空间。相比之下,中断的发生带有很大的不可预测性。但是,尽管有着这样的区别,二者之间还是有 很大的共性。这是因为,在使 CPU 的运行状态从用户态转入系统态,也就是从用户空间转入系统空间, 这一点上二者是一致的。当然,中断有可能发生在 CPU 已经运行在系统空间的时候,而系统调用却只发 生于用户空间,这又是二者不同的地方。这里,关键是 CPU 运行状态的改变,没有了这样的手段,也就 无所谓“保护模式”了。相比之下,在不分“用户态”和“系统态”的操作系统中,例如 DOS,所谓系 统调用实际上只不过是动态连接的库函数调用而已。虽然在 DOS 里面系统调用也是通过中断指令 INT 来实现的,但是跟预先规定好各种库函数入口地址的普通函数调用没有多大不同。如果用户程序知道具 体函数的入口地址,就可以绕过“系统调用”而直接调用这些函数。 Linux 的系统调用是通过中断指令“INT 0x80”实现的。我们已经在前面几节中讨论过进程通过“陷 阱门”或“中断门”进入系统空间的机制,以及 IDT 表中陷阱门的初始化。本节将着重介绍进程在系统 调用中进入系统空间,以及在完成了所需的服务以后从系统空间返回的过程。这个过程并不局限于某个 特定的调用,而是所有的系统调用都要经历的共同的过程。虽然我们选择了一个具体的调用作为例子, 但并不从功能的角度来关心具体的调用,而是着眼于这个公共的过程。系统调用是内核所提供的最根本 的、最重要的基础设施。由于系统调用与中断的共同性,读者在阅读本节时应该与前几节,特别是中断 过程一节结合阅读。事实上,有些代码就是二者共用的,凡是以前已经介绍过的本节不再重复。 由于我们并不关心内核在具体系统调用中所提供的服务,所以选择了一个非常简单的调用 sethostname()作为情景,通过对 CPU 在这个系统调用全过程中所走过的路线的分析,介绍内核的系统调 用机制。 系统调用 sethostname()的功能非常简单,就是设置计算机(在网络中的)“主机名”,其使用也很简 单: int sethostname(const char *name, size_t len); 参数 name 就是要设置的主机名,而 len 则为该字符串的长度。调用结束后返回 0 表示成功,-1 则表 示失败。失败时用户程序中的全局变量 errno 含有具体的出错代码。从程序设计的观点来看,Linux 的系 统调用可以分成两类:一类比较接近于真正意义上的“函数”,调用的结果就是函数值,例如 getpid()就 是这样;而另一类就是像 sethostname()这样的,返回的值实际上只是一个是否成功的标志,而调用的目 的时通过“副作用”来体现的。但是,在 C 语言中把所有可以通过调用指令来调用的程序段,也就是带 有 ret 指令的程序段都称作“函数”。而中断服务程序和系统调用,由于 ret(实际上是 iret)指令的存在 也就成了“函数”。我们在讨论中也将遵循 C 语言的规定和传统一概称之为函数。 为了帮助读者更好地理解系统调用的全过程,我们从用户空间对函数 sethostname()的调用开始我们 的情景分析。其实,sethostname()是一个库函数(在 usr/lib/libc.a 中),而实际的系统调用就是在那个函 数中发出的。GNU 的 C 语言库函数的源代码也是公开的,可以从 GNU 的网站下载。但是,我们在这里 采用从 libc.a 反汇编得到的代码。原因是,一来方便,“得来全不费工夫”;二来,读者多接触一些汇编 代码也是有好处的。特别是对于系统程序员来说,阅读和使用汇编语言也是一种有用的技能。 00030: sethostname.o: file format elf32-i386 00031: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 237 页,共 1481 页 00032: Disassembly of section .text: 00033: 00034: 00000000 : 00035: 0: 89 da movl %ebx, %edx 00036: 2: 8b 4c 24 08 movl 0x8(%esp,1), %ecx 00037: 6: 8b 5c 24 04 movl 0x4(%esp,1), %ebx 00038: a: b8 4a 00 00 00 movl $0x4a, %eax 00039: f: cd 80 int $0x80 00040: 11: 89 d3 movl %edx, %ebx 00041: 13: 3d 01 f0 ff ff cmpl $0xfffff001, %eax 00042: 18: 0f 83 fc ff ff jae la 00043: 1d: ff 00044: 1a: R_386_PC32 __syscall_error 00045: 1e: c3 ret 进入函数 sethostname()以后,堆栈指针%esp 指向返回地址,而在堆栈指针的内容加 4 的地方则是调 用该函数时的第一个参数(name),加 8 的地方为第二个参数 len,依此类推。由于 i386 运行于 32 位模 式,所有的参数都是按 32 位长整数压入堆栈的。指令“movl 0x8(%esp,1), %ecx”表示将相对于寄存器 %esp 的位移位 0x8(位移单位为 1)处的内容(在我们这个情景中就是参数 len)存入寄存器%ecx。然 后,又将参数 name 从堆栈中存入寄存器%ebx。最后是将代表 sethostname()的系统调用号 0x4a 存入寄存 器%eax,接着就是中断指令“int $0x80”。这里,读者已经看到,Linux 内核在系统调用时是通过寄存器 而不是通过堆栈传的参数的。 为什么要用寄存器传递参数?读者也许还记得:当 CPU 穿越陷阱门,从用户空间进入系统空间时, 由于运行级别的变动,要从用户堆栈切换到系统堆栈。如果在 INT 指令之前把参数压入堆栈,那是在用 户堆栈中,而进入系统空间以后就换成了系统堆栈。虽然进入系统空间之后也还可以从用户堆栈中读取 这些参数,但毕竟比较费事了。而通过寄存器来传递参数,则读者下面会看到,是个巧妙的安排。我们 暂且不随着 CPU 进入内核,而先看一下从系统调用返回以后的情况。首先是从%edx 中恢复%ebx 原先的 内容,那是在系统调用之前保存在%edx 中的(%edx 中原先的内容就丢失了,这是一种约定,gcc 在使 用寄存器时会遵守这个约定)。然后就是检查系统调用的返回值,那是在寄存器%eax 中。如果%eax 中的 内容是 0xfffff001 与 0xffffffff 之间,也就是-1 至-4095 之间,那就是出错了,就要转向__syscall_error()并 从那里返回。这里的 la: R_386_PC32 表示地址 sethostname+0x1a 处为重定位信息,在连接时会把地址 __syscall_error()填入该处。函数__syscall_error()也在 libc.a 中: 00001: sysdep.o file format elf32-i386 00002: 00003: Disassembly of section .text: 00004: 00005: 00000000 <__syscall_error>: 00006: 0: f7 d8 negl %eax 00007: 00008: 00000002 <__syscall_error_1>: 00009: 2: 50 pushl %eax 00010: 3: e8 fc ff ff ff call 4<__syscall_error_1+0x2> 00011: 4: R_386_PC32 __errno_location 00012: 8: 59 popl %ecx 00013: 9: 89 08 movl %ecx, (%eax) 00014: b: b8 ff ff ff ff movl $0xffffffff, %eax Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 238 页,共 1481 页 00015: 10: c3 ret 00016: 00017: 00018: errno-loc.o: file format elf32-i386 00019: 00020: Disassembly of section .text: 00021: 00022: 00000000 <__errno_location>: 00023: 0: 55 pushl %ebp 00024: 1: 89 e5 movl %esp, %ebp 00025: 3: b8 00 00 00 00 movl $0x0, %eax 00026: 4: R_386_32 errno 00027: 8: 89 ec movl %ebp, %esp 00028: a: 5d popl %ebp 00029: b: c3 ret 在__syscall_error 中,先取出%eax 的内容的负值,使其数值变成 1~4095 之间,这就是出错代码, 并将其压入堆栈。接着,又调用__errno_location(),将全局量 errno 的地址取入%eax。然后从堆栈中抛出 出错代码至%ecx、并将其写入全局量 errno。最后,在返回之前,将%eax 的内容改成-1。这样,通过寄 存器%eax 返回到用户进程的数值便是-1,而 errno 则含有具体的出错代码。这是对大部分系统调用(返 回整数的调用)返回值的约定。 搞清了发生在用户空间的过程,我们就进入内核,也就是系统空间中去了。CPU 穿过陷阱门的过程 与发生中断时穿过中断门的过程相同,这里就不重复了。不过,还是要指出,因外部中断而穿过中断门 时是不检查中断门与所规定的准入级别的,而在通过 INT 指令穿越中断门或陷阱门时,则要核对所规定 的准入级别与 CPU 的当前运行级别。为系统调用设置的陷阱门的准入级别 DPL 为 3。寄存器 IDTR 指向 当前的中断向量表 IDT,而 IDT 表中对应于 0x80 的表项就是为 INT 0x80 设置的陷阱门,其中的函数指 针指向 system_call()。当 CPU 到达 system_call()时,已经从用户态切换到了系统态,并且从用户堆栈换 成了系统堆栈,相当于 CPU 在发生于用户空间的外部中断过程中到达 IRQ0xYY_interrupt 时的状态,读 者不妨先回过头去重温一下。 如前所述,CPU 在穿越陷阱门进入系统内核时并不自动关中断,所以系统调用的过程是可中断的。 函数 system_call()的代码在 arch/i386/kernel/entry.S 中: 00195: ENTRY(system_call) 00196: pushl %eax # save orig_eax 00197: SAVE_ALL 00198: GET_CURRENT(%ebx) 00199: cmpl $(NR_syscalls),%eax 00200: jae badsys 00201: testb $0x02,tsk_ptrace(%ebx) # PT_TRACESYS 00202: jne tracesys 00203: call *SYMBOL_NAME(sys_call_table)(,%eax,4) 00204: movl %eax,EAX(%esp) # save the return value 00205: ENTRY(ret_from_sys_call) …………………… 首先是将寄存器%eax 的内容压入堆栈。系统堆栈中的这个位置在代码中称为 orig_ax,在外部中断 过程中用来保存(经过变形的)中断请求号,而在系统调用中则用来保存系统调用号。SAVE_ALL 我们 已经在中断过程一节中看到过了。但是,这里要指出,对于压入堆栈中的寄存器内容的使用方式是不一Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 239 页,共 1481 页 样的。在中断过程中,SAVE_ALL 以后,当调用具体的中断服务程序时已经保存在堆栈中的内容是作为 一个 pt_regs 数据结构,当成参数传递给 do_IRQ(),然后又传递给具体的服务程序的,这一点读者在中 断服务一节中已经看到。可是,在系统调用中就不同了,这里堆栈中每个寄存器的内容可以根据需要作 为独立的参数传递给具体的服务程序。以 sethostname()为例,需要传递的参数是两个,分别在%ebx 和 %ecx 中。在 SAVE_ALL 中%ebx 是最后压入堆栈的,%ecx 次之。所以堆栈中%ebx 的内容就成为参数 1, 而%ecx 的内容就是参数 2 了。回到 SAVE_ALL 去看一下,可以看到被压入堆栈的寄存器依次为:%es、 %ds、%eax、%ebp、%edi、%esi、%edx、%ecx 和%ebx。这里的%eax 持有系统调用号(与 orig_ax 相同), 显然不能用来传的参数;而%rbp 是用作子程序调用过程中的“帧”(frame)指针的,也不能用来传的参 数。这样,实际上就只有最后 5 个寄存器可以用来传递参数,所以,在系统调用中独立传递的参数不能 超过 5 个。从这里也可以看出,SAVE_ALL 中将寄存器压入堆栈的次序并不是随意决定的,而有其特殊 的考虑。 宏调用GET_CURRENT(%ebx)使寄存器%ebx 指向当前进程的task_struct结构(关于 GET_CURRENT 我们将在进程一章中介绍)。然后,就检查寄存器%eax 中的系统调用号是否超出了范围。在 task_struct 数据结构中有个成分 flags,其中有个标志位叫 PT_TRACESYS。一个进程可以通过系统调用 ptrace(), 将一个子进程的 PT_TRACESYS 标志位设成 1,从而跟踪该子进程的系统调用。Linux 系统中有一条命 令 strace 就是干这件事的,是一个很有用的工具。这里 system_call()中的第 201 行就是在检查当前进程的 PT_TRACESYS 是否为 1。注意,flags(%ebx)并不是一个函数调用,而是表示相对于%ebx 的内容,也就 是当前进程的 task_struct 结构指针、位移为 flags 处的地址,而 flags 在 entry.S 中的 75 行定义为 4。这一 点以前已经讲过,这里再提醒一下。 当 PT_TRACESYS 标志位(0x20)为 1 时,就要转入 tracesys,其代码也在 entry.S 中: 00244: tracesys: 00245: movl $-ENOSYS,EAX(%esp) 00246: call SYMBOL_NAME(syscall_trace) 00247: movl ORIG_EAX(%esp),%eax 00248: cmpl $(NR_syscalls),%eax 00249: jae tracesys_exit 00250: call *SYMBOL_NAME(sys_call_table)(,%eax,4) 00251: movl %eax,EAX(%esp) # save the return value 00252: tracesys_exit: 00253: call SYMBOL_NAME(syscall_trace) 00254: jmp ret_from_sys_call 将这一段程序与前面正常执行时的 203 行作一比较,就可以看出不同之处在于:当 PT_TRACESYS 为 1 时,在调用具体的服务程序之前和之后都要调用一下函数 syscall_trace(),向父进程报告具体系统调 用的进入和返回。我们将在讲述进程间通信时再深入到 syscall_trace()中去,但是有兴趣的读者不妨先自 己看看。现在回到 system_call 继续看那里的 203 行。这是一条 call 指令,所 call 的地址在一个函数指针 中,而这个函数指针在数组 sys_call_table[]中以%eax 的内容为下标、单位为 4 个字节的元素中。表达式 (, %eax, 4)的第一个逗号前面为空,表示在%eax 的基础上并没有其他的位移,而 4 则表示计算位移(%eax 相对于 sys_call_table)时的单位为 4 字节。系统调用跳转表 sys_call_table[]是一个函数指针数组,由于篇 幅较大,我们把它单独作为一节,附在本章之后。 表中凡是内核不支持的系统调用号全部都指向 sys_ni_call(),这个函数只是返回一个出错代码 -ENOSYS,表示该系统调用尚未实现。结合前面讲过的 libc.a 中的处理,可知此时用户程序会得到返回 值-1,而全局量 errno 的值为 ENOSYS。 跳转表中位移为 0x4a,也就是 74 处的函数指针(见后面跳转表中的 500 行)为 sys_sethostname, 所以在我们这个情景中就进入了 sys_sethostname(),这也是在 kernel/sys.c 中定义的: 00971: asmlinkage long sys_sethostname(char *name, int len) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 240 页,共 1481 页 00972: { 00973: int errno; 00974: 00975: if (!capable(CAP_SYS_ADMIN)) 00976: return -EPERM; 00977: if (len < 0 || len > __NEW_UTS_LEN) 00978: return -EINVAL; 00979: down_write(&uts_sem); 00980: errno = -EFAULT; 00981: if (!copy_from_user(system_utsname.nodename, name, len)) { 00982: system_utsname.nodename[len] = 0; 00983: errno = 0; 00984: } 00985: up_write(&uts_sem); 00986: return errno; 00987: } 可想而知,sethostname 应该是只有特权用户才可以进行的操作,所以一上来就先检查这一点。函数 capable(CAP_SYS_ADMIN)检查当前进程是否享有 CAP_SYS_ADMIN 的授权。如果没有的话就返回负 的出错代码 EPERM。然后,又对字符串的长度进行检查以保证安全。 在多处理器系统中,同时可以有多个进程在不同的 CPU 上运行。这样,就有可能发生两个进程同时 调用 sethostname(),而形成这样的现象: 1. 进程 A 调用 sethostname(),要把主机名设成“AB”。 2. 进程 C 在另一个 CPU 上运行,也调用 sethostname(),要把主机名设成“CD”。 3. 进程 A 先进入内核,并且已经在 sys_sethostname() 中将“A ”写入了内核中的 system_utsname.nodename,可是还没来得及写“B”之前发生了中断,而 C 在这个时候插了进 来。 4. 进程 C 进入内核,并且完成了对 sethostname() 的调用,成功地将内核中的 system_utsname.nodename 设置成“CD”。 5. 稍后,进程 A 恢复运行,继续把“B”写入 system_utsname.nodename。 6. 当进程 A 完成对 sethostname()的调用而“成功”返回时,内核中 system_utsname.nodename 的 内容却是“CB”。 在操作系统理论中,这种现象称为“race condition”(抢道)。为了防止这种情况发生,就要将对 system_utsname.nodename 的操作放在受到“信号量”(semaphore)保护的“临界区”中,而 sys_sethostname() 中 979 行的 down_write()和 985 行的 up_write()所实现的正是这样的保护机制。有了这种保护,上述过程 中当进程到达 979 行时会发现已经有个进程正在里面操作,“请勿打扰”,而自愿暂缓,让别的进程先运 行,从而避免了互相抢道。 下面,就是本次系统调用所要完成的实质性的操作了,这就是将参数 name 所指向的字符串写入内 核中的 system_utsname.nodename。这个操作的源在用户空间中,而目标在系统空间中,所以要通过一个 宏操作 copy_from_user()来完成复制。如前所述,系统调用时时通过寄存器传递参数的,能够通过寄存器 传递的信息量显然不大,所以传递的参数大多是指针,这样才能通过指针找到更大块的数据。因此,对 于系统调用的实现,类似于 copy_from_user()这样的用户空间和系统空间之间复制数据的操作是很重要、 也很常用的。对于 i386 CPU,宏操作 copy_from_user()是在 asm-i386/uaccess.h 中定义的: 00567: #define copy_from_user(to,from,n) \ 00568: (__builtin_constant_p(n) ? \ 00569: __constant_copy_from_user((to),(from),(n)) : \ Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 241 页,共 1481 页 00570: __generic_copy_from_user((to),(from),(n))) 当复制的长度为一些特殊的常数,例如 4、8、…、512 等等时,具体的操作要略为简单一些,而在 一般情况下则通过__generic_copy_from_user()来完成。其代码在 arch/i386/lib/usercpoy.c 中: 00050: unsigned long 00051: __generic_copy_from_user(void *to, const void *from, unsigned long n) 00052: { 00053: if (access_ok(VERIFY_READ, from, n)) 00054: __copy_user_zeroing(to,from,n); 00055: return n; 00056: } 对于读操作,access_ok()只是检查参数 from 和 n 的合理性,例如(from + n)是否超出了用户空间的上 限,而并不检查该区间是否已经映射。然后,就通过另一个宏操作__copy_user_zeroing()从用户空间复制。 这里__copy_user_zeroing()的代码可以说是一块“硬骨头”。可是,这个操作对于系统调用又是很重要的。 而且还有一些其他的类似操作,例如 copy_to_user()中调用的__copy_user(),以及__constant_copy_user(), 还有__do_strncpy_from_user(),get_user()等等都与此相似,所以还是值得“啃”一下的。另一方方面, 我们在第 2 章中讲述 do_page_fault() 时留下了一个尾巴,正是跟这些操作有关的。宏操作 __copy_user_zeroing()的定义在 include/asm-i386/uaccess.h 中: 00263: #define __copy_user_zeroing(to,from,size) \ 00264: do { \ 00265: int __d0, __d1; \ 00266: __asm__ __volatile__( \ 00267: "0: rep; movsl\n" \ 00268: " movl %3,%0\n" \ 00269: "1: rep; movsb\n" \ 00270: "2:\n" \ 00271: ".section .fixup,\"ax\"\n" \ 00272: "3: lea 0(%3,%0,4),%0\n" \ 00273: "4: pushl %0\n" \ 00274: " pushl %%eax\n" \ 00275: " xorl %%eax,%%eax\n" \ 00276: " rep; stosb\n" \ 00277: " popl %%eax\n" \ 00278: " popl %0\n" \ 00279: " jmp 2b\n" \ 00280: ".previous\n" \ 00281: ".section __ex_table,\"a\"\n" \ 00282: " .align 4\n" \ 00283: " .long 0b,3b\n" \ 00284: " .long 1b,4b\n" \ 00285: ".previous" \ 00286: : "=&c"(size), "=&D" (__d0), "=&S" (__d1) \ 00287: : "r"(size & 3), "0"(size / 4), "1"(to), "2"(from) \ 00288: : "memory"); \ 00289: } while (0) 首先来看__copy_user_zeroing()代码中常规的部分,这些代码是在操作顺利,一切都正常的情况下执Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 242 页,共 1481 页 行的。这一部分实质上只有 267~270 行,加上 286~288 三行。286 行为“输出部”,共说明了三个变量, 分别为%0、%1 以及%2。其中%0 对应于参数 size,与寄存器%%ecx 结合;%1 对应于局部变量__d0, 与寄存器%%edi 结合;而%2 则对应于局部变量__d1,与寄存器%%esi 结合。287 行为“输入部”,说明 了四个变量。第一个为%3,是一个寄存器变量,初值为(size&3),而后面两个则分别等价于%1,%2 和 %3,分别应该置初值为(size/4),参数 to,以及参数 from。完成了输入部所规定的初始化以后,就开始 执行 267~270 行的汇编语言程序。程序中利用 X86 处理器的 REP 和 MOVS 指令进行成串 MOVE,寄 存器%%ecx 为计数器,%%esi 为源指针,%%edi 为目标指针。先按长整数进行,然后对剩余的部分(不 超过 3 个)字节按字节进行。如果用 C 语言来写这段程序,那就相当于: __copy_user_zeroing(void *to, void *from, int size) { int r; r = size & 3; size = size / 4; while( size-- ) *((int *)to) ++ = *((int *)from)++; while( r-- ) *((char *)to)++ = *((char *)from)++; } 显然,二者的效率是不能相比的。读者在前几节中已经看到过类似的代码,所以这一部分代码是容 易理解的。 可是,为什么要有从 271 行至 280 行这些代码呢?代码的作者特地写了个说明,就是文件 “ Documentation/exception.txt ”,解释其原因(如果读者的计算机安装了 Linux ,可以在 /usr/src/linux/Documentation 目录中找到这个文件)。不过读者在阅读那篇说明时可能还会感到困难,所以 我们结合本节的情景分析加以补充说明。当内核从一个进程得到从用户空间传递进来的指针时,就像这 个情景中的 name,是很难保证这个指针的“合法”性的,更难保证在长度为 len 的整个区间都是“合法” 的。所以,为安全起见应该先检查这个区间的合法性,看看由指针和长度两个参数所决定的虚存空间是 否已经建立映射。每个进程都有个代表它的虚存空间的 mm_struct 数据结构,记录着该进程在用户空间 所有已经建立映射的区间。只要搜索这个数据结构中的链表,就可以发现从 name 开始,长度为 len 的区 间是否已经建立映射,并且是否允许所需的操作(读或写)。内核中专门有个函数 verify_area()用于这个 目的。而 Linux 内核老一些的版本中确实是这样做的。但是,每次从用户区读或写时都要进行这样的检 查实在是个负担,测试表明这个负担在典型的应用中确实显著地影响了效率。在实际应用中,虽然指针 由问题的可能性也是有的,甚至可能还不小,但毕竟总是少数,也许可以说“百分之九十五以上的指针 是好的”,实在犯不着为少数的坏指针而“打击一大片”,致使总体效率下降。所以,新版本就决定把对 指针合法性的检查取消了。万一碰上了坏指针,那就让页面异常发生吧,内核可以在页面异常的服务程 序中个别地处理这个问题。 现在,我们再回过头去看看 do_page_fault()。当碰上了坏指针而页面异常真的发生时,在 do_page_fault()中,首先就是通过 find_vma()搜索当前进程的虚存区间链表,如果搜索失败就转入 bad_area。在第 2 章中,我们对于 bad_area 只讲了当异常发生于 CPU 运行在用户空间时的情况。而在我 们现在这个情景中,则异常发生于当 CPU 运行在系统空间的时候。虽然访问失败的目标地址在用户空间 中,但是 CPU 的“执行地址”却是在系统空间中。为方便起见,我们再列出 do_page_fault()中有关的几 行代码: [do_page_fault()] 00299: do_sigbus: …………………… 00315: /* Kernel mode? Handle exceptions or die */ Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 243 页,共 1481 页 00316: if (!(error_code & 4)) 00317: goto no_context; 00318: return; 00255: no_context: 00256: /* Are we prepared to handle this kernel fault? */ 00257: if ((fixup = search_exception_table(regs->eip)) != 0) { 00258: regs->eip = fixup; 00259: return; 00260: } 就是说,如果内核能够在一个“异常表”中找到发生异常的指令所在的地址,并得到相应的“修复” 地址 fixup,就 将 CPU 在异常返回后将要重新执行的地址替换成这个“修复”地址。为什么要这样做呢? 因为在这种情况下内核不能为当前进程补上一个页面(那样的话 name 所指的字符串就变成空白了)。如 果任其自然的话,则从异常返回以后,当前进程必然会接连不断地因执行同一条指令而产生新的异常, 落入“万劫不复”的地步。所以,必须把它“从泥坑里拉出来”。函数 search_exception_table()是在 arch/i386/mm/extable.c 中定义的: [do_page_fault() > search_exception_table()] 00033: unsigned long 00034: search_exception_table(unsigned long addr) 00035: { 00036: unsigned long ret; 00037: 00038: #ifndef CONFIG_MODULES 00039: /* There is only the kernel to search. */ 00040: ret = search_one_table(__start___ex_table, __stop___ex_table-1, addr); 00041: if (ret) return ret; 00042: #else 00043: /* The kernel is the last "module" -- no need to treat it special. */ 00044: struct module *mp; 00045: for (mp = module_list; mp != NULL; mp = mp->next) { 00046: if (mp->ex_table_start == NULL) 00047: continue; 00048: ret = search_one_table(mp->ex_table_start, 00049: mp->ex_table_end - 1, addr); 00050: if (ret) return ret; 00051: } 00052: #endif 00053: 00054: return 0; 00055: } 不管 38 行的 CONFIG_MODULES 是否有定义,即是否支持“可安装模块”(取决于系统配置),最 终总是要调用 search_one_table()。那也是在同一个源文件(extable.c)中: [do_page_fault() > search_exception_table() > search_one_table()] 00012: static inline unsigned long Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 244 页,共 1481 页 00013: search_one_table(const struct exception_table_entry *first, 00014: const struct exception_table_entry *last, 00015: unsigned long value) 00016: { 00017: while (first <= last) { 00018: const struct exception_table_entry *mid; 00019: long diff; 00020: 00021: mid = (last - first) / 2 + first; 00022: diff = mid->insn - value; 00023: if (diff == 0) 00024: return mid->fixup; 00025: else if (diff < 0) 00026: first = mid+1; 00027: else 00028: last = mid-1; 00029: } 00030: return 0; 00031: } 显然,这里所实现的是在一个 exception_table_entry 数据结构中进行的对分搜索。数据结构 struct exception_table_entry 又是在 include/asm-i386/uaccess.h 中定义的: 00067: /* 00068: * The exception table consists of pairs of addresses: the first is the 00069: * address of an instruction that is allowed to fault, and the second is 00070: * the address at which the program should continue. No registers are 00071: * modified, so it is entirely up to the continuation code to figure out 00072: * what to do. 00073: * 00074: * All the routines below use bits of fixup code that are out of line 00075: * with the main instruction path. This means when everything is well, 00076: * we don't even have to jump over them. Further, they do not intrude 00077: * on our cache or tlb entries. 00078: */ 00079: 00080: struct exception_table_entry 00081: { 00082: unsigned long insn, fixup; 00083: }; 结构中的 insn 表示可能产生异常的指令所在的地址,而 fixup 则为用来替换的“修复”地址。读者 会问:可能发生问题的指令有那么多,怎么能为每一条可能发生问题的指令都建立这样一个数据结构呢? 回答是:首先,可能发生问题的指令其实并不像想像的那么多;其次,由谁来为这些指令建立这样的数 据结构呢?很简单,就是“谁使用,谁负责”。例如,我们这里的__copy_user_zeroing()要从用户空间拷 贝,可能发生问题,它就应该负责在异常表中为其可能发生问题的指令建立起这样的数据结构。 现在我们可以回到__copy_user_zeroing()的代码中了。首先,在这里可能发生问题的指令其实只有两 条,一条是 267 行标号为 0 的 movsl,另一条则是 269 行标号为 1 的 movsb。所以应该建立两个表项,Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 245 页,共 1481 页 这就是 282 行至 284 行所说明的,关键之处在 283 行和 284 行。283 行表示,如果异常发生在前面标号 为 0 处的地址,也就是指令 movsl 所在的地址,那么其“修复地址”fixup 为前面标号为 3 处的地址,也 就是指令 lea 所在的地址。这时,CPU 从“修复地址”开始做些什么修复呢?在这里是通过 stosb 把 system_utsname.nodename 中剩余的部分设成 0(当然也可以什么都不做)。然后,就通过 279 行的 JMP 指令跳转到前面标号为 2 处,也就是结束的地方。这样,虽然从用户空间拷贝的目的没有达到,却避免 了陷入在“异常-重执”之间可能发生的无限循环。 大家知道,程序经编译(或汇编)连接以后,其可执行代码分成 text 和 data 两个段。但是,其实 GNU 的 gcc 和 ld 还支持另外两个段。一个是 fixup,专门用于异常发生后的修复,实际上跟 text 段没有 太大区别。另一个是__ex_table,专门用于异常地址表。而__copy_user_zeroing()中的 271 行和 281 行就 是告诉 gcc 应该把相应的代码分别放在 fixup 和__ex_table 段中,连接时 ld 会按地址排序将这些表项装入 异常地址表中。 实际上,不光是像__copy_user_zeroing()这样的函数要准备好“修复地址”,任何在内核中运行时可 能发生问题的都要有所准备,其中还包括我们在前一节中看到过的 RESTORE_ALL。当时为了让读者把 注意力集中在中断的基本机制上而没有讲述有关的内容,我们在下面降到从系统调用返回时会加以补充。 这里,读者还应该注意一下函数__generic_copy_from_user()的返回值。从代码中可以看到,返回的是调 用参数,也就是从用户空间拷贝的长度。这是怎么回事呢?这是因为__copy_user_zeroing()不是一个函数, 而是一个宏定义。在执行的过程中,n 随着复制而减小,一直到 0 为止。如果中途失败的话,则 n 代表 了剩下未完成部分的大小。回头看一下__copy_user_zeroing()中的第 273 行,这里的%0 就是参数 size, 因而也就是 n。同时,它就是寄存器%%ecx。在 movsl 或 movsb 执行的过程中,%%ecx 的值一定是非 0。 可是下面在 276 行还要用%%ecx,所以先把它保存在堆栈中,而到 278 行再来恢复。所以,最后在 __generic_copy_from_user 中返回的 n 表示还有几个字节尚未完成。而在 sys_sethostname()中,则根据这 个返回值来判断 copy_from_user()是否成功。当返回值为 0 时,就把 errno 也设成 0。这样最后 sys_sethostname()返回 0 表示成功,而若在 copy_from_user()过程中失败则返回-EFAULT。 由于 sys_sethostname()本身很简单,现在回到本节开头的 system_call()。CPU 从具体系统调用的服务 程序返回时,由服务程序准备好的返回值在寄存器%eax 中,所以在第 204 行将它写入到堆栈中与%eax 对应的地方,这样在 RESTORE_ALL 以后,这个返回值仍通过%eax 传回用户空间。这以后,CPU 就到 达了 ret_from_sys_call。 [system_call -> ret_from_sys_call] 00205: ENTRY(ret_from_sys_call) 00206: #ifdef CONFIG_SMP 00207: movl processor(%ebx),%eax 00208: shll $CONFIG_X86_L1_CACHE_SHIFT,%eax 00209: movl SYMBOL_NAME(irq_stat)(,%eax),%ecx # softirq_active 00210: testl SYMBOL_NAME(irq_stat)+4(,%eax),%ecx # softirq_mask 00211: #else 00212: movl SYMBOL_NAME(irq_stat),%ecx # softirq_active 00213: testl SYMBOL_NAME(irq_stat)+4,%ecx # softirq_mask 00214: #endif 00215: jne handle_softirq 00216: 00217: ret_with_reschedule: 00218: cmpl $0,need_resched(%ebx) 00219: jne reschedule 00220: cmpl $0,sigpending(%ebx) 00221: jne signal_return Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 246 页,共 1481 页 00222: restore_all: 00223: RESTORE_ALL …………………… 00282: handle_softirq: 00283: call SYMBOL_NAME(do_softirq) 00284: jmp ret_from_intr 读者已经读过从中断返回时的代码,对上面这些代码应该不会有问题了。 需要补充的是,在 RESTORE_ALL 中有三条指令可能会引起异常,所以需要为之准备“修复”。这 三条指令是:popl %ds,popl %es 以及 iret。我们先看代码(entry.S),在加以讨论: 00101: #define RESTORE_ALL \ 00102: popl %ebx; \ 00103: popl %ecx; \ 00104: popl %edx; \ 00105: popl %esi; \ 00106: popl %edi; \ 00107: popl %ebp; \ 00108: popl %eax; \ 00109: 1: popl %ds; \ 00110: 2: popl %es; \ 00111: addl $4,%esp; \ 00112: 3: iret; \ 00113: .section .fixup,"ax"; \ 00114: 4: movl $0,(%esp); \ 00115: jmp 1b; \ 00116: 5: movl $0,(%esp); \ 00117: jmp 2b; \ 00118: 6: pushl %ss; \ 00119: popl %ds; \ 00120: pushl %ss; \ 00121: popl %es; \ 00122: pushl $11; \ 00123: call do_exit; \ 00124: .previous; \ 00125: .section __ex_table,"a";\ 00126: .align 4; \ 00127: .long 1b,4b; \ 00128: .long 2b,5b; \ 00129: .long 3b,6b; \ 00130: .previous 这里准备了三个“修复”地址,分别在 127~129 行;而可能出现问题的指令则分别在 109 行、110 行和 112 行。那么,为什么从堆栈中恢复%ds 会有可能发生问题呢?读者也许还记得,每当装入一个段 寄存器时,CPU 都要根据这新的段选择码以及 GDTR 或 LDTR 的内容在相应的段描述表中找到所选择 的段描述项,并加以检查。如果描述项与选择码都有效并且相符,就将描述项装入到 CPU 中段寄存器的 “不可见”部分,使得以后不必每次都要到内存中去访问该描述项。可是,如果因为不管什么原因而使 得选择码或描述项无效或不符时,CPU 就会产生一次“全面保护”(General Protection)异常(称为 GPLinux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 247 页,共 1481 页 异常)。当这样的异常发生于系统空间时,就要为之准备好修复手段。在这里,为“popl %ds”准备的修 复手段是从标号为 4 处,即 114 行的“move $0, (%esp)”指令开始的程序段,实际上只有两行。这条指 令将%ds 在堆栈中的副本先清成 0,然后在 115 行转回 109 行重新执行“popl %ds”。为什么这样就能“修 复”呢?其实并不是真的修复,而只是避免进一步的 GP 异常。以 0 作为段选择码称为“空选择码”。将 空选择码装入一个段寄存器(除 CS 和 SS 以外)本身不会引起 GP 异常,而要到以后企图通过这个空选 择码访问内存时才会引起异常,但那是回到用户空间以后的事了。在用户空间发生异常,最多也不过是 把这进程“杀”了,而不会在系统一级上产生问题。所以,这里的修复手段实际上是把问题往下推、往 后推而已。110 行的“popl %es”与此相同。 最后,为什么“iret”也可能发生问题,又怎样“修复”呢?当 i386 CPU 从系统空间中断返回到用 户空间时,要从系统堆栈中恢复用户堆栈的指针,包括堆栈寄存器的内容,并从系统堆栈中恢复在用户 空间的返回地址,包括代码段寄存器的内容。与数据段寄存器%ds 类似,这两个步骤都有可能发生问题 而产生 GP 异常,使 CPU 回不到用户空间中去。那么,怎样修复呢?对 CS 和 SS 不能通过使用空选择 码的“瞒天过海”手段,因为 CS 和 SS 根本不接受空选择码(会产生 GP 异常)。所以,问题比“popl %ds” 所可能发生的问题更为严重。而解决的办法,则只好通过 do_exit()(详见“进程与进程调度”一章),将 当前进程“丢卒保车”杀掉算了(见 118~123 行)。把当前进程杀了以后,内核会调度另一个进程称为 当前进程。所以,当再要从系统空间返回用户空间时,是返回到另一个进程的用户空间中去,那时候要 从系统堆栈中恢复的寄存器副本也是另一个进程的副本了。 系统调用 sethostname()的实现虽然简单,但是从内核中的入口 system_call 到进入 sys_sethostname() 前的这一段代码,以及从 sys_sethostname()返回后直到完成 RESTORE_ALL 中的 iret 指令这一段代码, 则是所有系统调用所共用的。不管什么系统调用,其进入内核以及退出内核的过程都是相同的。以后, 当我们谈到系统调用时,就直接从内核中的实现,如 sys_sethostname()那样开始。 最后,还要指出一个读者已经看到但是未必清楚地意识到的事实,那就是从内核中可以直接访问当 前进程的用户空间,所使用的虚拟地址也与当进程处于用户空间时的地址完全相同。当然,反过来就不 可以了。 3.9. 系统调用号和跳转表 文件 include/asm/unistd.h 为每个系统调用定义了一个唯一的编号,称为系统调用号。部分编号如下 所示: 00008: #define __NR_exit 1 00008: #define __NR_fork 2 00008: #define __NR_read 3 00008: #define __NR_write 4 00008: #define __NR_open 5 00008: #define __NR_close 6 00008: #define __NR_waitpid 7 00008: #define __NR_creat 8 00008: #define __NR_link 9 00008: #define __NR_unlink 10 00008: #define __NR_execve 11 00008: #define __NR_chdir 12 00008: #define __NR_time 13 00008: #define __NR_mknod 14 …………………… 系统调用的跳转表是一个函数指针数组,跳转时以系统调用号为下标在数组中找到相应的函数指针。 该数组是在 arch/i386/kernel/entry.S 中定义的。数组的大小由常数 NR_syscalls 决定,该常数在Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 248 页,共 1481 页 include/linux/sys.h 中定义为 256。目前 Linux 共定义了 221 个系统调用,其余的 30 余项可供用户自行添 加。数组中对凡是没有定义的下标(系统调用号)都放上一个函数指针,指向 sys_ni_syscall(),其代码 在 kernel/sys.c 中: 00169: asmlinkage long sys_ni_syscall(void) 00170: { 00171: return -ENOSYS; 00172: } 下面即为 entry.S 中数组 sys_call_table 的汇编代码。第 656 行处的.rept NR_syscalls-221 系 gcc 预处理 命令。文件经预处理后就会将后面的 657 行重复(NR_syscalls-221)次,也即 35 次。 00425: ENTRY(sys_call_table) 00426: .long SYMBOL_NAME(sys_ni_syscall) /* 0 - old "setup()" system call*/ 00427: .long SYMBOL_NAME(sys_exit) 00428: .long SYMBOL_NAME(sys_fork) 00429: .long SYMBOL_NAME(sys_read) 00430: .long SYMBOL_NAME(sys_write) 00431: .long SYMBOL_NAME(sys_open) /* 5 */ 00432: .long SYMBOL_NAME(sys_close) 00433: .long SYMBOL_NAME(sys_waitpid) 00434: .long SYMBOL_NAME(sys_creat) 00435: .long SYMBOL_NAME(sys_link) 00436: .long SYMBOL_NAME(sys_unlink) /* 10 */ 00437: .long SYMBOL_NAME(sys_execve) 00438: .long SYMBOL_NAME(sys_chdir) 00439: .long SYMBOL_NAME(sys_time) 00440: .long SYMBOL_NAME(sys_mknod) 00441: .long SYMBOL_NAME(sys_chmod) /* 15 */ 00442: .long SYMBOL_NAME(sys_lchown16) 00443: .long SYMBOL_NAME(sys_ni_syscall) /* old break syscall holder */ 00444: .long SYMBOL_NAME(sys_stat) 00445: .long SYMBOL_NAME(sys_lseek) 00446: .long SYMBOL_NAME(sys_getpid) /* 20 */ 00447: .long SYMBOL_NAME(sys_mount) 00448: .long SYMBOL_NAME(sys_oldumount) 00449: .long SYMBOL_NAME(sys_setuid16) 00450: .long SYMBOL_NAME(sys_getuid16) 00451: .long SYMBOL_NAME(sys_stime) /* 25 */ 00452: .long SYMBOL_NAME(sys_ptrace) 00453: .long SYMBOL_NAME(sys_alarm) 00454: .long SYMBOL_NAME(sys_fstat) 00455: .long SYMBOL_NAME(sys_pause) 00456: .long SYMBOL_NAME(sys_utime) /* 30 */ 00457: .long SYMBOL_NAME(sys_ni_syscall) /* old stty syscall holder */ 00458: .long SYMBOL_NAME(sys_ni_syscall) /* old gtty syscall holder */ 00459: .long SYMBOL_NAME(sys_access) 00460: .long SYMBOL_NAME(sys_nice) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 249 页,共 1481 页 00461: .long SYMBOL_NAME(sys_ni_syscall) /* 35 */ /* old ftime syscall holder */ 00462: .long SYMBOL_NAME(sys_sync) 00463: .long SYMBOL_NAME(sys_kill) 00464: .long SYMBOL_NAME(sys_rename) 00465: .long SYMBOL_NAME(sys_mkdir) 00466: .long SYMBOL_NAME(sys_rmdir) /* 40 */ 00467: .long SYMBOL_NAME(sys_dup) 00468: .long SYMBOL_NAME(sys_pipe) 00469: .long SYMBOL_NAME(sys_times) 00470: .long SYMBOL_NAME(sys_ni_syscall) /* old prof syscall holder */ 00471: .long SYMBOL_NAME(sys_brk) /* 45 */ 00472: .long SYMBOL_NAME(sys_setgid16) 00473: .long SYMBOL_NAME(sys_getgid16) 00474: .long SYMBOL_NAME(sys_signal) 00475: .long SYMBOL_NAME(sys_geteuid16) 00476: .long SYMBOL_NAME(sys_getegid16) /* 50 */ 00477: .long SYMBOL_NAME(sys_acct) 00478: .long SYMBOL_NAME(sys_umount) /* recycled never used phys() */ 00479: .long SYMBOL_NAME(sys_ni_syscall) /* old lock syscall holder */ 00480: .long SYMBOL_NAME(sys_ioctl) 00481: .long SYMBOL_NAME(sys_fcntl) /* 55 */ 00482: .long SYMBOL_NAME(sys_ni_syscall) /* old mpx syscall holder */ 00483: .long SYMBOL_NAME(sys_setpgid) 00484: .long SYMBOL_NAME(sys_ni_syscall) /* old ulimit syscall holder */ 00485: .long SYMBOL_NAME(sys_olduname) 00486: .long SYMBOL_NAME(sys_umask) /* 60 */ 00487: .long SYMBOL_NAME(sys_chroot) 00488: .long SYMBOL_NAME(sys_ustat) 00489: .long SYMBOL_NAME(sys_dup2) 00490: .long SYMBOL_NAME(sys_getppid) 00491: .long SYMBOL_NAME(sys_getpgrp) /* 65 */ 00492: .long SYMBOL_NAME(sys_setsid) 00493: .long SYMBOL_NAME(sys_sigaction) 00494: .long SYMBOL_NAME(sys_sgetmask) 00495: .long SYMBOL_NAME(sys_ssetmask) 00496: .long SYMBOL_NAME(sys_setreuid16) /* 70 */ 00497: .long SYMBOL_NAME(sys_setregid16) 00498: .long SYMBOL_NAME(sys_sigsuspend) 00499: .long SYMBOL_NAME(sys_sigpending) 00500: .long SYMBOL_NAME(sys_sethostname) 00501: .long SYMBOL_NAME(sys_setrlimit) /* 75 */ 00502: .long SYMBOL_NAME(sys_old_getrlimit) 00503: .long SYMBOL_NAME(sys_getrusage) 00504: .long SYMBOL_NAME(sys_gettimeofday) 00505: .long SYMBOL_NAME(sys_settimeofday) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 250 页,共 1481 页 00506: .long SYMBOL_NAME(sys_getgroups16) /* 80 */ 00507: .long SYMBOL_NAME(sys_setgroups16) 00508: .long SYMBOL_NAME(old_select) 00509: .long SYMBOL_NAME(sys_symlink) 00510: .long SYMBOL_NAME(sys_lstat) 00511: .long SYMBOL_NAME(sys_readlink) /* 85 */ 00512: .long SYMBOL_NAME(sys_uselib) 00513: .long SYMBOL_NAME(sys_swapon) 00514: .long SYMBOL_NAME(sys_reboot) 00515: .long SYMBOL_NAME(old_readdir) 00516: .long SYMBOL_NAME(old_mmap) /* 90 */ 00517: .long SYMBOL_NAME(sys_munmap) 00518: .long SYMBOL_NAME(sys_truncate) 00519: .long SYMBOL_NAME(sys_ftruncate) 00520: .long SYMBOL_NAME(sys_fchmod) 00521: .long SYMBOL_NAME(sys_fchown16) /* 95 */ 00522: .long SYMBOL_NAME(sys_getpriority) 00523: .long SYMBOL_NAME(sys_setpriority) 00524: .long SYMBOL_NAME(sys_ni_syscall) /* old profil syscall holder */ 00525: .long SYMBOL_NAME(sys_statfs) 00526: .long SYMBOL_NAME(sys_fstatfs) /* 100 */ 00527: .long SYMBOL_NAME(sys_ioperm) 00528: .long SYMBOL_NAME(sys_socketcall) 00529: .long SYMBOL_NAME(sys_syslog) 00530: .long SYMBOL_NAME(sys_setitimer) 00531: .long SYMBOL_NAME(sys_getitimer) /* 105 */ 00532: .long SYMBOL_NAME(sys_newstat) 00533: .long SYMBOL_NAME(sys_newlstat) 00534: .long SYMBOL_NAME(sys_newfstat) 00535: .long SYMBOL_NAME(sys_uname) 00536: .long SYMBOL_NAME(sys_iopl) /* 110 */ 00537: .long SYMBOL_NAME(sys_vhangup) 00538: .long SYMBOL_NAME(sys_ni_syscall) /* old "idle" system call */ 00539: .long SYMBOL_NAME(sys_vm86old) 00540: .long SYMBOL_NAME(sys_wait4) 00541: .long SYMBOL_NAME(sys_swapoff) /* 115 */ 00542: .long SYMBOL_NAME(sys_sysinfo) 00543: .long SYMBOL_NAME(sys_ipc) 00544: .long SYMBOL_NAME(sys_fsync) 00545: .long SYMBOL_NAME(sys_sigreturn) 00546: .long SYMBOL_NAME(sys_clone) /* 120 */ 00547: .long SYMBOL_NAME(sys_setdomainname) 00548: .long SYMBOL_NAME(sys_newuname) 00549: .long SYMBOL_NAME(sys_modify_ldt) 00550: .long SYMBOL_NAME(sys_adjtimex) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 251 页,共 1481 页 00551: .long SYMBOL_NAME(sys_mprotect) /* 125 */ 00552: .long SYMBOL_NAME(sys_sigprocmask) 00553: .long SYMBOL_NAME(sys_create_module) 00554: .long SYMBOL_NAME(sys_init_module) 00555: .long SYMBOL_NAME(sys_delete_module) 00556: .long SYMBOL_NAME(sys_get_kernel_syms) /* 130 */ 00557: .long SYMBOL_NAME(sys_quotactl) 00558: .long SYMBOL_NAME(sys_getpgid) 00559: .long SYMBOL_NAME(sys_fchdir) 00560: .long SYMBOL_NAME(sys_bdflush) 00561: .long SYMBOL_NAME(sys_sysfs) /* 135 */ 00562: .long SYMBOL_NAME(sys_personality) 00563: .long SYMBOL_NAME(sys_ni_syscall) /* for afs_syscall */ 00564: .long SYMBOL_NAME(sys_setfsuid16) 00565: .long SYMBOL_NAME(sys_setfsgid16) 00566: .long SYMBOL_NAME(sys_llseek) /* 140 */ 00567: .long SYMBOL_NAME(sys_getdents) 00568: .long SYMBOL_NAME(sys_select) 00569: .long SYMBOL_NAME(sys_flock) 00570: .long SYMBOL_NAME(sys_msync) 00571: .long SYMBOL_NAME(sys_readv) /* 145 */ 00572: .long SYMBOL_NAME(sys_writev) 00573: .long SYMBOL_NAME(sys_getsid) 00574: .long SYMBOL_NAME(sys_fdatasync) 00575: .long SYMBOL_NAME(sys_sysctl) 00576: .long SYMBOL_NAME(sys_mlock) /* 150 */ 00577: .long SYMBOL_NAME(sys_munlock) 00578: .long SYMBOL_NAME(sys_mlockall) 00579: .long SYMBOL_NAME(sys_munlockall) 00580: .long SYMBOL_NAME(sys_sched_setparam) 00581: .long SYMBOL_NAME(sys_sched_getparam) /* 155 */ 00582: .long SYMBOL_NAME(sys_sched_setscheduler) 00583: .long SYMBOL_NAME(sys_sched_getscheduler) 00584: .long SYMBOL_NAME(sys_sched_yield) 00585: .long SYMBOL_NAME(sys_sched_get_priority_max) 00586: .long SYMBOL_NAME(sys_sched_get_priority_min) /* 160 */ 00587: .long SYMBOL_NAME(sys_sched_rr_get_interval) 00588: .long SYMBOL_NAME(sys_nanosleep) 00589: .long SYMBOL_NAME(sys_mremap) 00590: .long SYMBOL_NAME(sys_setresuid16) 00591: .long SYMBOL_NAME(sys_getresuid16) /* 165 */ 00592: .long SYMBOL_NAME(sys_vm86) 00593: .long SYMBOL_NAME(sys_query_module) 00594: .long SYMBOL_NAME(sys_poll) 00595: .long SYMBOL_NAME(sys_nfsservctl) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 252 页,共 1481 页 00596: .long SYMBOL_NAME(sys_setresgid16) /* 170 */ 00597: .long SYMBOL_NAME(sys_getresgid16) 00598: .long SYMBOL_NAME(sys_prctl) 00599: .long SYMBOL_NAME(sys_rt_sigreturn) 00600: .long SYMBOL_NAME(sys_rt_sigaction) 00601: .long SYMBOL_NAME(sys_rt_sigprocmask) /* 175 */ 00602: .long SYMBOL_NAME(sys_rt_sigpending) 00603: .long SYMBOL_NAME(sys_rt_sigtimedwait) 00604: .long SYMBOL_NAME(sys_rt_sigqueueinfo) 00605: .long SYMBOL_NAME(sys_rt_sigsuspend) 00606: .long SYMBOL_NAME(sys_pread) /* 180 */ 00607: .long SYMBOL_NAME(sys_pwrite) 00608: .long SYMBOL_NAME(sys_chown16) 00609: .long SYMBOL_NAME(sys_getcwd) 00610: .long SYMBOL_NAME(sys_capget) 00611: .long SYMBOL_NAME(sys_capset) /* 185 */ 00612: .long SYMBOL_NAME(sys_sigaltstack) 00613: .long SYMBOL_NAME(sys_sendfile) 00614: .long SYMBOL_NAME(sys_ni_syscall) /* streams1 */ 00615: .long SYMBOL_NAME(sys_ni_syscall) /* streams2 */ 00616: .long SYMBOL_NAME(sys_vfork) /* 190 */ 00617: .long SYMBOL_NAME(sys_getrlimit) 00618: .long SYMBOL_NAME(sys_mmap2) 00619: .long SYMBOL_NAME(sys_truncate64) 00620: .long SYMBOL_NAME(sys_ftruncate64) 00621: .long SYMBOL_NAME(sys_stat64) /* 195 */ 00622: .long SYMBOL_NAME(sys_lstat64) 00623: .long SYMBOL_NAME(sys_fstat64) 00624: .long SYMBOL_NAME(sys_lchown) 00625: .long SYMBOL_NAME(sys_getuid) 00626: .long SYMBOL_NAME(sys_getgid) /* 200 */ 00627: .long SYMBOL_NAME(sys_geteuid) 00628: .long SYMBOL_NAME(sys_getegid) 00629: .long SYMBOL_NAME(sys_setreuid) 00630: .long SYMBOL_NAME(sys_setregid) 00631: .long SYMBOL_NAME(sys_getgroups) /* 205 */ 00632: .long SYMBOL_NAME(sys_setgroups) 00633: .long SYMBOL_NAME(sys_fchown) 00634: .long SYMBOL_NAME(sys_setresuid) 00635: .long SYMBOL_NAME(sys_getresuid) 00636: .long SYMBOL_NAME(sys_setresgid) /* 210 */ 00637: .long SYMBOL_NAME(sys_getresgid) 00638: .long SYMBOL_NAME(sys_chown) 00639: .long SYMBOL_NAME(sys_setuid) 00640: .long SYMBOL_NAME(sys_setgid) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 253 页,共 1481 页 00641: .long SYMBOL_NAME(sys_setfsuid) /* 215 */ 00642: .long SYMBOL_NAME(sys_setfsgid) 00643: .long SYMBOL_NAME(sys_pivot_root) 00644: .long SYMBOL_NAME(sys_mincore) 00645: .long SYMBOL_NAME(sys_madvise) 00646: .long SYMBOL_NAME(sys_getdents64) /* 220 */ 00647: .long SYMBOL_NAME(sys_fcntl64) 00648: .long SYMBOL_NAME(sys_ni_syscall) /* reserved for TUX */ 00649: 00650: /* 00651: * NOTE!! This doesn't have to be exact - we just have 00652: * to make sure we have _enough_ of the "sys_ni_syscall" 00653: * entries. Don't panic if you notice that this hasn't 00654: * been shrunk every time we add a new system call. 00655: */ 00656: .rept NR_syscalls-221 00657: .long SYMBOL_NAME(sys_ni_syscall) 00658: .endr Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 254 页,共 1481 页 4. 进程与进程调度 4.1. 进程四要素 要给进程下一个确切的定义不是一件容易的事。不过,一般来说 Linux 系统中的进程都具备下列诸 要素: 1. 有一段程序供其执行,就好像一场戏要有个剧本一样。这段程序不一定是进程所专有的,可以 与其他进程共用,就好像不同剧团的许多场演出可以共用一个剧本一样。 2. 有起码的“私有财产”,这就是进程专用的系统堆栈空间。 3. 有“户口”,这就是在内核中的一个 task_struct 数据结构,操作系统教科书中常称为“进程控制 块”。有了这个数据结构,进程才能成为内核调度的一个基本单位接受内核的调度。同时,这个 结构又是进程的“财产登记卡”,记录着进程所占用的各项资源。 4. 有独立的存储空间,意味着拥有专有的用户空间:进一步,还意味着除前述的系统空间堆栈外 还有其专用的用户空间堆栈。注意,系统空间是不能独立的,任何进程都不可能直接(不通过 系统调用)改变系统空间的内容(除其本身的系统空间堆栈以外)。 这四条都是必要条件,缺了其中任何一条就不成其为“进程”。如果只具备了前面三条而缺第四条, 那就称为“线程”。特别地,如果完全没有用户空间,就称为“内核线程”(kernel thread);而如果共享 用户空间则就称为“用户线程”。在不致引起混淆的场合,二者也都往往简称为“线程”。读者在第 2 章 中看到过的 kswapd,就是一个内核线程。读者要注意,不要把这里的“线程”与有些系统中在用户空间 的同一进程内实现的“线程”相混淆。那种线程显然不拥有独立、专用的系统堆栈,也不作为一个调度 单位直接接受内核调度。而且,既然 Linux 内核提供了对线程的支持,一般也就没有必要再在进程内部, 即用户空间中自行实现线程。 另一方面,进程与线程的区分也不是十分严格的,一般在讲到进程时常常也包括了线程。事实上, 在 Linux(以及 Unix)系统中,许多进程在“诞生”之初都与其父进程共用同一个存储空间,所以严格 说来还是线程;但是自进程可以建立其自己的存储空间,并与父进程分道扬镳,称为真正意义上的进程。 再说,线程也有“pid”,也有 task_struct 结构,所以这两个词在使用中有时候并不严格加以区分,要根 据上下文理解其含义。 还有,在 Linux 系统中“进程”(process)和“任务”(task)是同一个意思,在内核的代码中也常常 混用这两个名次和概念。例如,每一个进程都要有一个 task_struct 数据结构,而其号码却又是 pid;唤醒 一个睡眠进程的函数名为 wake_up_process()。之所以有这样的情况是因为 Linux 源自 Unix 和 i386 系统 结构,而 Unix 中的进程在 Intel 的技术资料中则称为“任务”(严格来说有点区别,但是对 Linux 和 Unix 的实现来说是一码事)。 Linux 系统运行时的第一个进程是在初始化阶段“捏造”出来的。而此后的进程或线程则都是由一 个业已存在的进程像细胞分裂那样通过系统调用复制出来的,称为“fork”(分叉)或“clone”(克隆)。 除上述最起码的“财产”,即 task_struct 数据结构和系统堆栈之外,一个进程还要有些附加的资源。 例如上面说过,“独立”的存储空间意味着进程拥有用户空间,因此就要有用于蓄存管理的 mm_struct 数据结构以及下属的 vm_area 数据结构,以及相应的页面目录项和页面表。但那些都是第二位的,从属 于 task_struct 的资源,而 task_struct 数据结构则在这方面起着登记卡的作用。至于进程的具体实现,则 在相当程度上取决于宿主 CPU 的系统结构。 在转入详细介绍进程的各个要素之前,我们先讲一下 i386 系统结构所提供的进程管理机制以及 Linux 内核对这种机制的特殊运用和处理。读者可以结合第 2 章中的有关内容阅读。 Intel 在 i386 系统结构的设计中考虑到了进程(任务)的管理和调度,并从硬件上支持任务间的切换。 为此目的,Intel 在 i386 系统结构中增设了另一种新的段,叫做“任务状态段”TSS。一个 TSS 虽然说像 代码段、数据段等一样,也是一个“段”,实际上却只是一个 104 字节的数据结构、或曰控制块,用以纪Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 255 页,共 1481 页 录一个任务的关键性的状态信息,包括: 任务切换前夕(也就是切入点上)该任务各通用寄存器的内容。 任务切换前夕(切入点上)该任务各个段寄存器(包括 ES、CS、SS、DS、FS 和 ES)的内容。 任务切换前夕(切入点上)该任务 EFLAGS 寄存器的内容。 任务切换前夕(切入点上)该任务指令地址寄存器 EIP 的内容。 指向前一个任务的 TSS 结构的段选择码。当前任务执行 IRET 指令时,就返回到由这个段选择 码所指的(TSS 所代表的)任务(返回地址则由堆栈决定)。 该任务的 LDT 段选择码,它指向任务的 LDT。 控制寄存器 CR3 的内容,它指向任务的页面目录。 三个堆栈指针,分别为当任务运行于 0 级、1 级和 2 级时的堆栈指针,包括堆栈段寄存器 SS0、 SS1 和 SS2,以及 ESP0、ESP1 和 ESP2 的内容。注意,在 CPU 中只有一个 SS 和一个 ESP 寄 存器,但是 CPU 在进入新的运行级别时会自动从当前任务的 TSS 中装入相应 SS 和 ESP 的内容, 实现堆栈的切换。 一个用于程序跟踪的标志位 T。当 T 标志位为 1 时,CPU 就会在切入该进程时产生一次 debug 异常,这样就可以在 debug 异常的服务程序中安排所需的操作,如加以记录、显示等等。 在一个 TSS 段中,除了基本的 104 字节的 TSS 结构以外,还可以有一些附加的信息。其中之一 时表示 I/O 权限的位图。I386 系统结构允许 I/O 指令在比 0 级为低的状态下执行,也就是说可 以将外设驱动实现于一个既非内核(0 级)也非用户(3 级)的空间中,这个位图就是用于这个 目的。另一个是“中断重定向位图”,用于 vm86 模式。 像其他的“段”一样,TSS 也要在段描述表中有个表项。不过 TSS 的描述项只能在 GDT 中,而不 能放在任何一个 LDT 中或 IDT 中。如果通过一个段选择项访问一个 TSS,而选择项中的 T1 标志位为 1 (表示使用 LDT),就会产生一次“总保护”GP 异常。TSS 描述项的结构与其他的段描述项基本相同(参 看第 2 章),但有一个 B(Busy)标志位,表示相应 TSS 所代表的任务是否正在运行或者正被中断。 另外,CPU 中还增设了一个“任务寄存器”TR,指向当前任务的 TSS。相应地,还增设了一条指令 LTR,对 TR 寄存器进行装入操作。像 CS 和 DS 一样,TR 也有一个不可见的部分,每当将一个段寄存 器选择码装入到 TR 中时,CPU 就自动找到所选择的 TSS 描述项并将其装入到 TR 的不可见部分,以加 速以后对该 TSS 段的访问。 还有,在 IDT 表中,除中断门、陷阱门和调用门外,还定义了一种“任务门”。任务门中包含着一 个 TSS 段选择码。当 CPU 因中断而穿过一个任务门时,就会将任务门中的段选择码自动装入 TR,使 TR 指向新的 TSS,并完成任务切换。CPU 还可以通过 JMP 和 CALL 指令实现任务切换,当跳转或调用 的目标段(代码段)实际上指向 GDT 表中的一个 TSS 描述项时,就会引起一次任务切换。 Intel 的这种设计确实很周到,也为任务切换提供了一个非常简洁的机制。但是,请读者注意,由 CPU 自动完成的这种任务切换并不是像读者可能误以为的那样只相当于“一条指令”。实际上,i386 的系统 结构基本上是 CISC 的,而通过 JMP 指令或 CALL 指令(或中断)完成任务切换的过程可以说是典型的、 甚至是极端的“复杂指令”执行过程,其执行过程长达 300 多个 CPU 时钟周期(一条 POP 指令占 12 个 CPU 时钟周期)。在执行的过程中,CPU 实际上做了所有可能需要做的事,而其中有的事在一定的条件 下本来是可以简化的,有的事则可能在一定的条件下应该按不同的方式组合。所以,i386 CPU 所提供的 这种任务切换机制就好像是一种“高级语言”的成分。你固然可以用它,但对于操作系统的设计和实现 而言,你往往会选择“汇编语言”来实现这个机制,以达到更高的效率和更大的灵活性。重要的是,任 务的切换往往不是孤立的,常常跟其他的操作联系在一起。例如,在 Unix 和 Linux 系统中,任务切换就 只发生于系统空间,因而与系统调用和中断密切联系在一起,并且有许多操作可以合并。 就如对 i386 所提供的许多其他功能一样,读者将会看到,Linux 内核实际上并不使用 i386 CPU 硬件 提供的任务切换机制。不过,由于 i386 CPU 要求软件设置 TR 及 TSS,内核中便只好“走过场”地设置 好 TR 及 TSS 以满足 CPU 的要求。但是,内核中并不使用任务门、也不允许使用 JMP 或 CALL 指令实 施任务切换。内核只在初始化阶段设置 TR,使之指向一个 TSS,从此以后就再不改变 TR 的内容了。也Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 256 页,共 1481 页 就是说,每个 CPU(如果有多个 CPU 的话)在初始化以后的全部运行过程中永远各自使用同一个 TSS。 同时,内核也不依靠 TSS 保存每个进程切换时的寄存器副本,而是将这些寄存器的副本保存在各个进程 自己的系统空间堆栈中,就如读者在第 3 章中所看到的那样。 这样一来,TSS 中的绝大部分内容已经失去了原来的意义。可是,在第 3 章中讲过,当 CPU 因中断 或系统调用而从用户空间进入系统空间时,会由于运行级别的变化而自动更换堆栈。而新的堆栈指针, 包括堆栈段寄存器 SS 的内容和堆栈指针寄存器 ESP 的内容,则取自“当前”任务的 TSS。由于在 Linux 中只使用两个运行级别,即 0 级和 3 级,所以 TSS 中为另两个级别(即 1 级和 2 级)设置的堆栈指针副 本也失去了意义。于是,对于 Linux 内核来说,TSS 中有意义的就只剩下了 0 级的堆栈指针,也就是 SS0 和 ESP0 两项了。Intel 原来的意图是让 TR 的内容,随着不同的 TSS,随着任务的切换而走马灯似地转。 可是在 Linux 内核中却变成了“铁打的营盘流水的兵”:就一个 TSS,像一座营盘,一经建立就再也不动 了。而里面的内容,也就是当前任务的系统堆栈指针,则随着进程的调度切换而流水似地变动。这里的 原因在于:改变 TSS 中的 SS0 和 ESP0 所花的开销比通过装入 TR 以更换一个 TSS 要小得多。因此,在 Linux 内核中,TSS 并不是属于某个进程的资源,而是全局性的公共资源。在多处理器的情况下,尽管 内核中确实有多个 TSS,但是每个 CPU 仍旧只有一个 TSS,一经装入就不再变了。 那么,这个 TSS 是什么样的呢?请看 include/asm-i386/processor.h 中对 INIT_TSS 的定义: 00392: #define INIT_TSS { \ 00393: 0,0, /* back_link, __blh */ \ 00394: sizeof(init_stack) + (long) &init_stack, /* esp0 */ \ 00395: __KERNEL_DS, 0, /* ss0 */ \ 00396: 0,0,0,0,0,0, /* stack1, stack2 */ \ 00397: 0, /* cr3 */ \ 00398: 0,0, /* eip,eflags */ \ 00399: 0,0,0,0, /* eax,ecx,edx,ebx */ \ 00400: 0,0,0,0, /* esp,ebp,esi,edi */ \ 00401: 0,0,0,0,0,0, /* es,cs,ss */ \ 00402: 0,0,0,0,0,0, /* ds,fs,gs */ \ 00403: __LDT(0),0, /* ldt */ \ 00404: 0, INVALID_IO_BITMAP_OFFSET, /* tace, bitmap */ \ 00405: {~0, } /* ioperm */ \ 00406: } 这里把系统中第一个进程的 SS0 设置成__KERNEL_DS,而把 ESP0 设置成指向&init_stack 的顶端。 对 INIT_TSS 的引用则在 kernel/init_task.c 中给出: 00026: /* 00027: * per-CPU TSS segments. Threads are completely 'soft' on Linux, 00028: * no more per-task TSS's. The TSS size is kept cacheline-aligned 00029: * so they are allowed to end up in the .data.cacheline_aligned 00030: * section. Since TSS's are completely CPU-local, we want them 00031: * on exact cacheline boundaries, to eliminate cacheline ping-pong. 00032: */ 00033: struct tss_struct init_tss[NR_CPUS] __cacheline_aligned = { [0 ... NR_CPUS-1] = INIT_TSS }; 结构数组 init_tss 的大小为 NR_CPUS,即系统中 CPU 的个数。每个 TSS 的内容都相同,都由 INIT_TSS 定义。此外,每个 TSS 的起始地址都与高速缓存中的缓冲行对齐。 数据结构 tss_struct 是在 processor.h 中定义的,它反映了 TSS 段的结构: 00327: struct tss_struct { Linux 内核源代码情景分析 00328: unsigned short back_link,__blh; 00329: unsigned long esp0; 00330: unsigned short ss0,__ss0h; 00331: unsigned long esp1; 00332: unsigned short ss1,__ss1h; 00333: unsigned long esp2; 00334: unsigned short ss2,__ss2h; 00335: unsigned long __cr3; 00336: unsigned long eip; 00337: unsigned long eflags; 00338: unsigned long eax,ecx,edx,ebx; 00339: unsigned long esp; 00340: unsigned long ebp; 00341: unsigned long esi; 00342: unsigned long edi; 00343: unsigned short es, __esh; 00344: unsigned short cs, __csh; 00345: unsigned short ss, __ssh; 00346: unsigned short ds, __dsh; 00347: unsigned short fs, __fsh; 00348: unsigned short gs, __gsh; 00349: unsigned short ldt, __ldth; 00350: unsigned short trace, bitmap; 00351: unsigned long io_bitmap[IO_BITMAP_SIZE+1]; 00352: /* 00353: * pads the TSS to be cacheline-aligned (size is 0x100) 00354: */ 00355: unsigned long __cacheline_filler[5]; 00356: } 前面讲过,每个进程都有一个 task_struct 数据结构和一片用作系统空间堆栈的存储空间。这二者缺 一不可,又有紧密的联系,所以在物理存储空间中也连在一起。内核在为每个进程分配一个 task_struct 结构时,实际上分配两个连续的物理页面(供 8192 字节)。这两个页面的底部用作进程的 task_struct 结 构,而在结构的上面就用作进程的系统空间堆栈,见图 4.1。 两个连续的物理页面 (8KB) 系统空间堆栈 (大约7KB) struct task_struct (大约1KB) 图4.1 进程系统堆栈示意图 数据结构 task_struct 的大小约 1K 字节,所以进程系统空间堆栈的大小约为 7K 字节。注意,系统空 间堆栈的空间不像用户空间堆栈那样可以在运行时动态地扩展(见第 2 章),而是静态地确定了的。所以, 在中断服务程序、内核软中断服务程序以及其他设备驱动程序的设计中,应注意不能让这些函数嵌套得 2006-12-31 版权所有,侵权必究 第 257 页,共 1481 页 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 258 页,共 1481 页 太深,同时,在这些函数中也不宜使用太多、太大得局部变量。像下面程序中这样的局部变量就应该避 免: int something() { char buf[1024]; …………………… } 这里的 buf 是局部变量,因为是在堆栈中,它一下子就耗去了 1K 字节,显然是不合适的。 进程 task_struct 结构以及系统空间堆栈的这种特殊安排,决定了内核中一些宏操作的定义 (processor.h): 00446: #define THREAD_SIZE (2*PAGE_SIZE) 00447: #define alloc_task_struct() ((struct task_struct *) __get_free_pages(GFP_KERNEL,1)) 00448: #define free_task_struct(p) free_pages((unsigned long) (p), 1) THREAD_SIZE 定义为两个页面,表示每个内核线程(一个进程必定同时又是一个内核线程)的这 两项基本资源所占用的物理存储空间大小。至于 alloc_task_struct()的实现,读者也许会想像成这样: struct task_struct *t = kmalloc(sizeof(struct task_struct)); 实际上却不是,这是因为所分配的并不仅仅是task_struct数据结构的大小,而是连同系统空间堆栈所需要 的空间一起分配。注意,__get_free_pages()中第二个参数的值 1 表示 21,也就是两个页面。 当进程在系统空间运行时,常常需要访问当前进程自身的 task_struct 数据结构。为此目的,内核中 (current.h)定义了一个宏操作 current,提供指向当前进程 task_struct 结构的指针: 00006: static inline struct task_struct * get_current(void) 00007: { 00008: struct task_struct *current; 00009: __asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL)); 00010: return current; 00011: } 00012: 00013: #define current get_current() 第 9 行通过将当前的堆栈指针寄存器 ESP 的内容与 8191UL(0xfffffe00)相“与”而得到当前进程 task_struct 结构的起始地址(汇编代码的解释可参看第 2 章和第 3 章中的几个例子)。结合前面的图 4.1 和说明。读者应该不难理解为什么这样就可以得到所需的地址。 那么,为什么不把这个地址放在一个全局量中,使得每次调度一个新的进程运行时就将该进程的 task_struct 结构的起始地址写入这个变量,以后随时可用,这样不是更有效吗?答案恰恰相反。一条 AND 指令的执行只需要 4 个 CPU 时钟周期,而一条从寄存器到寄存器的 MOV 指令也才 2 个 CPU 时钟周期, 所以,像这样的需要时才临时把它计算出来反而效率更高。读者从这里也可以看出,高水平的系统程序 员的“抠门”真是到了极点。 与此相类似的,还有在进入中断和系统调用时所引用的宏操作 GET_CURRENT,那是在 include/asm-i386/hw_irq.h 中定义的: 00113: #define GET_CURRENT \ 00114: "movl %esp, %ebx\n\t" \ 00115: "andl $-8192, %ebx\n\t" 我们在第 2 章中跳过了对这段程序的解释,因为那时还没有讲到进程的系统空间堆栈与其 task_struct 结构之间的关系。 task_struct 的定义在 include/linux/sched.h 中给出: 00277: struct task_struct { Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 259 页,共 1481 页 00278: /* 00279: * offsets of these are hardcoded elsewhere - touch with care 00280: */ 00281: volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ 00282: unsigned long flags; /* per process flags, defined below */ 00283: int sigpending; 00284: mm_segment_t addr_limit; /* thread address space: 00285: 0-0xBFFFFFFF for user-thead 00286: 0-0xFFFFFFFF for kernel-thread 00287: */ 00288: struct exec_domain *exec_domain; 00289: volatile long need_resched; 00290: unsigned long ptrace; 00291: 00292: int lock_depth; /* Lock depth */ 00293: 00294: /* 00295: * offset 32 begins here on 32-bit platforms. We keep 00296: * all fields in a single cacheline that are needed for 00297: * the goodness() loop in schedule(). 00298: */ 00299: long counter; 00300: long nice; 00301: unsigned long policy; 00302: struct mm_struct *mm; 00303: int has_cpu, processor; 00304: unsigned long cpus_allowed; 00305: /* 00306: * (only the 'next' pointer fits into the cacheline, but 00307: * that's just fine.) 00308: */ 00309: struct list_head run_list; 00310: unsigned long sleep_time; 00311: 00312: struct task_struct *next_task, *prev_task; 00313: struct mm_struct *active_mm; 00314: 00315: /* task state */ 00316: struct linux_binfmt *binfmt; 00317: int exit_code, exit_signal; 00318: int pdeath_signal; /* The signal sent when the parent dies */ 00319: /* ??? */ 00320: unsigned long personality; 00321: int dumpable:1; 00322: int did_exec:1; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 260 页,共 1481 页 00323: pid_t pid; 00324: pid_t pgrp; 00325: pid_t tty_old_pgrp; 00326: pid_t session; 00327: pid_t tgid; 00328: /* boolean value for session group leader */ 00329: int leader; 00330: /* 00331: * pointers to (original) parent process, youngest child, younger sibling, 00332: * older sibling, respectively. (p->father can be replaced with 00333: * p->p_pptr->pid) 00334: */ 00335: struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr; 00336: struct list_head thread_group; 00337: 00338: /* PID hash table linkage. */ 00339: struct task_struct *pidhash_next; 00340: struct task_struct **pidhash_pprev; 00341: 00342: wait_queue_head_t wait_chldexit; /* for wait4() */ 00343: struct semaphore *vfork_sem; /* for vfork() */ 00344: unsigned long rt_priority; 00345: unsigned long it_real_value, it_prof_value, it_virt_value; 00346: unsigned long it_real_incr, it_prof_incr, it_virt_incr; 00347: struct timer_list real_timer; 00348: struct tms times; 00349: unsigned long start_time; 00350: long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS]; 00351: /* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */ 00352: unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap; 00353: int swappable:1; 00354: /* process credentials */ 00355: uid_t uid,euid,suid,fsuid; 00356: gid_t gid,egid,sgid,fsgid; 00357: int ngroups; 00358: gid_t groups[NGROUPS]; 00359: kernel_cap_t cap_effective, cap_inheritable, cap_permitted; 00360: int keep_capabilities:1; 00361: struct user_struct *user; 00362: /* limits */ 00363: struct rlimit rlim[RLIM_NLIMITS]; 00364: unsigned short used_math; 00365: char comm[16]; 00366: /* file system info */ Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 261 页,共 1481 页 00367: int link_count; 00368: struct tty_struct *tty; /* NULL if no tty */ 00369: unsigned int locks; /* How many file locks are being held */ 00370: /* ipc stuff */ 00371: struct sem_undo *semundo; 00372: struct sem_queue *semsleeping; 00373: /* CPU-specific state of this task */ 00374: struct thread_struct thread; 00375: /* filesystem information */ 00376: struct fs_struct *fs; 00377: /* open file information */ 00378: struct files_struct *files; 00379: /* signal handlers */ 00380: spinlock_t sigmask_lock; /* Protects signal and blocked */ 00381: struct signal_struct *sig; 00382: 00383: sigset_t blocked; 00384: struct sigpending pending; 00385: 00386: unsigned long sas_ss_sp; 00387: size_t sas_ss_size; 00388: int (*notifier)(void *priv); 00389: void *notifier_data; 00390: sigset_t *notifier_mask; 00391: 00392: /* Thread group tracking */ 00393: u32 parent_exec_id; 00394: u32 self_exec_id; 00395: /* Protection of (de-)allocation: mm, files, fs, tty */ 00396: spinlock_t alloc_lock; 00397: } 先把结构中几个特别重要的成分介绍一下,其余则留待以后再来介绍。这些成分大体可以分成状态、 性质、资源和组织等几大类。 第 281 行的 state 表示进程当前的运行状态,具体定义见 sched.h: 00084: #define TASK_RUNNING 0 00085: #define TASK_INTERRUPTIBLE 1 00086: #define TASK_UNINTERRUPTIBLE 2 00087: #define TASK_ZOMBIE 4 00088: #define TASK_STOPPED 8 状态 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 均表示进程处于睡眠状态。但是, TASK_UNINTERRUPTIBLE 表示进程处于“深度睡眠”而不受“信号”(signal,也称“软中断”)的打 扰,而 TASK_INTERRUPTIBLE 则可以因“信号”的到来而被唤醒。内核中提供了不同的函数,让一个 进程进入不同深度的睡眠或将进程从睡眠中唤醒。具体地说,函数 sleep_on()和 wake_up()用于深度睡眠, 而 interruptible_sleep_on()和 wake_up_interruptible()则用于浅度睡眠。深度睡眠一般只用于临界区和关键 性的部位,而“可中断”的睡眠那就是通用的了。特别,当进程在“阻塞性”(blocking)的系统调用中Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 262 页,共 1481 页 等待某一事件发生时,应该进入“可中断”睡眠而不应深度睡眠。例如,当进程等待操作人员按某个键 的时候,就不应该进入深度睡眠,否则就不能对别的事件作出反应,别的进程就不能通过发一个信号来 “杀”掉这个进程了。还应该注意,这里的 INTERRUPTIBLE 或 UNINTERRUPTIBLE 跟“中断”毫无 关系,而只是说睡眠能否因其他事件而中断,即唤醒。不过,所谓其他事件主要是“信号”,而信号的概 念实际上与中断的概念是相同的,所以这里所谓 INTERRUPTIBLE 也是指这种“软中断”而言。 TASK_RUNNING 状态并不是表示一个进程正在执行中,或者说这个进程就是“当前进程”,而是表 示这个进程可以被调度执行而成为当前进程。当进程处于这样的可执行(或就绪)状态时,内核就将该 进程的 task_struc 结构通过其队列头 run_list(见 309 行)挂入一个“运行队列”。 TASK_ZOMBIE 状态表示进程已经“去世”(exit)而“户口”尚未注销。 TASK_STOPPED 主要用于调试目的。进程接收到一个 SIGSTOP 信号后就将运行状态改成 TASK_STOPPED 而进入“挂起”状态,然后在接收到一个 SIGCONT 信号时又恢复继续运行。 在本章“进程的调度与切换”一节中有一个进程的状态转换示意图(图 4.4),读者不妨先翻过去看 一下。 第 282 行中的 flags 也是反映进程状态的信息,但并不是运行状态,而是与管理有关的其他信息。这 些标志位也是在 sched.h 中定义的: 00399: /* 00400: * Per process flags 00400: */ 00400: #define PF_ALIGNWARN 0x00000001 /* Print alignment warning msgs */ 00400: /* Not implemented yet, only for 486*/ 00400: #define PF_STARTING 0x00000002 /* being created */ 00400: #define PF_EXITING 0x00000004 /* getting shut down */ 00400: #define PF_FORKNOEXEC 0x00000040 /* forked but didn't exec */ 00400: #define PF_SUPERPRIV 0x00000100 /* used super-user privileges */ 00400: #define PF_DUMPCORE 0x00000200 /* dumped core */ 00400: #define PF_SIGNALED 0x00000400 /* killed by a signal */ 00400: #define PF_MEMALLOC 0x00000800 /* Allocating memory */ 00400: #define PF_VFORK 0x00001000 /* Wake up parent in mm_release */ 00400: 00400: #define PF_USEDFPU 0x00100000 /* task used FPU this quantum (SMP) */ 代码作者所加的注释已经说明了各个标志位的作用,这里就不多说了。 除了上述的 state 和 flags 以外,反映当前进程状态的成分还有下面这么一些: sigpending——表示进程收到了“信号”但尚未处理。与这个标志相联系的是与信号队列有关 的 sigqueue、sigqueue_tail、sig 等指针以及 sigmask_lock、signal、blocked 等成分。请详见“进 间通信”中的“信号”一节,以及本章中的有关叙述。 count——与调度有关,详见“进程的调度与切换”一节。 need_sched——与调度有关,表示 CPU 从系统空间返回用户空间前夕要进行一次调度。 上列当前状态都反映了进程的动态特征,还有一些则反映静态特征: add_limit——虚存地址空间的上限。对进程而言是其用户空间的上限,所以是 0xbfffffff;对内 核线程而言则是系统空间的上限,所以是 0xffffffff。 personality——由于 Unix 有许多不同的版本和变种,应用程序也就有了适用范围,例如 Unix SVR4 的应用程序就未必与 Linux 开发的其他软件完全兼容,所以根据执行程序的不同,每个 进程都有其“个性”。文件 include/linux/personality.h 中定义了有关的常数: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 263 页,共 1481 页 00008: /* Flags for bug emulation. These occupy the top three bytes. */ 00009: #define STICKY_TIMEOUTS 0x4000000 00010: #define WHOLE_SECONDS 0x2000000 00011: #define ADDR_LIMIT_32BIT 0x0800000 00012: 00013: /* Personality types. These go in the low byte. Avoid using the top bit, 00014: * it will conflict with error returns. 00015: */ 00016: #define PER_MASK (0x00ff) 00017: #define PER_LINUX (0x0000) 00018: #define PER_LINUX_32BIT (0x0000 | ADDR_LIMIT_32BIT) 00019: #define PER_SVR4 (0x0001 | STICKY_TIMEOUTS) 00020: #define PER_SVR3 (0x0002 | STICKY_TIMEOUTS) 00021: #define PER_SCOSVR3 (0x0003 | STICKY_TIMEOUTS | WHOLE_SECONDS) 00022: #define PER_WYSEV386 (0x0004 | STICKY_TIMEOUTS) 00023: #define PER_ISCR4 (0x0005 | STICKY_TIMEOUTS) 00024: #define PER_BSD (0x0006) 00025: #define PER_SUNOS (PER_BSD | STICKY_TIMEOUTS) 00026: #define PER_XENIX (0x0007 | STICKY_TIMEOUTS) 00027: #define PER_LINUX32 (0x0008) 00028: #define PER_IRIX32 (0x0009 | STICKY_TIMEOUTS) /* IRIX5 32-bit */ 00029: #define PER_IRIXN32 (0x000a | STICKY_TIMEOUTS) /* IRIX6 new 32-bit */ 00030: #define PER_IRIX64 (0x000b | STICKY_TIMEOUTS) /* IRIX6 64-bit */ 00031: #define PER_RISCOS (0x000c) 00032: #define PER_SOLARIS (0x000d | STICKY_TIMEOUTS) exec_domain——除了 personality 以外,应用程序还有一些其他的版本间的差异,从而形成了不 同的“执行域”。这个指针就是指向描述本进程所属执行域的数据结构。 binfmt——应用程序的文件格式,如 a.out、elf 等。详见“系统调用 exec()”一节。 exit_code、exit_signal、pdeath_signal——详见“系统调用 exit()与 wait4()”。 pid——进程号。 pgrp、session、leader——当一个用户登录到系统时,就开始了一个进程组(session),此后创 建的进程都属于这同一个 session。此外,若干进程可以通过“管道”组合在一起,如“ls | wc -l”, 从而形成进程组。详见“系统调用 exec”一节。 priority、rt_priority——优先级别以及“实时”优先级别,详见“进程的调度与切换”。 policy——适用于本进程的调度政策,详见“进程的调度与切换”。 parent_exec_id、self_exec_id——与进程组(session)有关,见“系统调用 exit()与 wait4()”。 uid、euid、suid、fsuid、gid、egid、sgid、fsgid——主要与文件操作权限有关,见“文件系统” 一章。 cap_effective、cap_inheritable、cap_permitted——一般进程都不能“为所欲为”,而是各自被 赋予了各种不同的权限。例如,一个进程是否可以通过系统调用 ptrace()跟踪另一个进程,就是 由该进程是否具有 CAP_SYS_PTRACE 授权决定的;一个进程是否有权重新引导操作系统,则 取决于改进程是否具有 CAP_SYS_BOOT 授权。这样,就把进程的各种权限分细了,而不再是 笼统地取决于一个进程是否是“特权用户”进程。文件 include/linux/capability.h 中定义了许多 这样的权限,代码的作者还加了相当详细的注解(详见“文件系统”一章)。每一种权限都由一 个标志位代表,内核中提供了一个 inline 函数 capable(),用来检查当前进程是否具有某种权限。Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 264 页,共 1481 页 如 capable(CAP_SYS_BOOT),就是检查当前进程是否由权重引导操作系统(返回非 0 表示有 权)。值得注意的是,对操作权限的这种划分与文件访问权限结合在一起,形成了系统安全的基 础。在现今的网络时代,这种安全性正在变得愈来愈重要,而这方面的研究与发展也是一个重 要的课题。 user——指向一个 user_struct 结构,该数据结构代表着进程所属的用户。注意这跟 Unix 内核中 每个进程的 user 结构是两码事。Linux 内核中的 user 结构是非常简单的,详见“系统调用 fork()” 一节。 rlim——这是一个结构数组,表明进程对各种资源的使用数量所受的限制。读者在“存储管理” 一章中已经看到过其应用。数据结构 rlimit 是在 include/linux/resource.h 中定义的: 00040: struct rlimit { 00041: unsigned long rlim_cur; 00042: unsigned long rlim_max; 00043: }; 对 i386 环境而言,进程可用资源共有 RLIMIT_NLIMITS 项,即 10 项。每种资源的限制在文件 linux/include/asm/resource.h 中给出: 00004: /* 00005: * Resource limits 00006: */ 00007: 00008: #define RLIMIT_CPU 0 /* CPU time in ms */ 00009: #define RLIMIT_FSIZE 1 /* Maximum filesize */ 00010: #define RLIMIT_DATA 2 /* max data size */ 00011: #define RLIMIT_STACK 3 /* max stack size */ 00012: #define RLIMIT_CORE 4 /* max core file size */ 00013: #define RLIMIT_RSS 5 /* max resident set size */ 00014: #define RLIMIT_NPROC 6 /* max number of processes */ 00015: #define RLIMIT_NOFILE 7 /* max number of open files */ 00016: #define RLIMIT_MEMLOCK 8 /* max locked-in-memory address space */ 00017: #define RLIMIT_AS 9 /* address space limit */ 00018: #define RLIMIT_LOCKS 10 /* maximum file locks held */ 00019: 00020: #define RLIM_NLIMITS 11 还有一些成分代表着进程所占有和使用的资源,如 mm、active_mm、fs、files、tty、real_timer、times、 it_real_value 等,对这些成分都有专门的章节加以介绍,这里就不重复了。 至于统计信息,则主要有 per_cpu_utime[]和 per_cpu_stime[]两个数组,表示该进程在各个处理器上 (在多处理器 SMP 结构中,一个进程可以受调度在不同的处理器上运行)运行于用户空间和系统空间的 累计时间。而数据结构 times 中则是对这些时间的汇总。此外,还有发生页面异常次数的统计 min_flt、 maj_flt 以及换入/换出的次数 nswap 等。当一个进程通过 do_exit()结束其生命时,该进程的有关统计信息 要合并到父进程中,所以对每项统计信息都有一项相应的“总计”信息,如相对于 min_flt 有 cmin_flt, 在数据结构 times 中相对于 tms_utime 有 tms_cutime 等。 最后,每一个进程都不是孤立地存在于系统中,而总是根据不同的目的、关系和需要与其他的进程 项联系。从内核的角度看,则是要按不同的目的和性质将每个进程纳入不同的组织中。第一个组织是由 每个进程的“家庭与社会关系”形成的“宗族”或“家谱”。这是一种树型的组织,通过指针 p_opptr、 p_pptr、p_cptr、p_ysptr 和 p_osptr 构成。其中 p_opptr 和 p_pptr 指向父进程的 task_struct 结构,p_cptr 指 向最“年轻”的子进程,而 p_ysptr 和 p_osptr 则分别指向其“哥哥”和“弟弟”,从而形成一个子进程链。Linux 内核源代码情景分析 这些指针确定了一个进程在其“宗族”中的上、下、左、右关系,详见本章中对 fork()和 exit()的叙述。 图 4.2 就是这个进程“家谱”的示意图。 父进程 子进程最年轻的子进程 最老的子进程 p_pptr p_pptr p_pptrp_cptr p_osptr p_osptr p_ysptr p_ysptr 图4.2 进程家谱示意图 这个组织虽然确定了每个进程的“宗族” 关系,涵盖了系统中所有的进程,但是,要在这个组织中 根据进程号 pid 找到一个进程却非易事。进程号的分配是相当随机的,在进程号中并不包含任何可以用 来找到一个进程的路径信息,而给定一个进程号要求找到给进程的 task_struct 结构却又是常常要用到的 一种操作。于是,就有了第二个组织,那就是一个以杂凑表为基础的进程队列的阵列。当给定一个 pid 要找到该进程时,先对 pid 施行杂凑计算,以计算的结果为下标在杂凑表中找到一个队列,再顺着该队 列就可以较容易地找到特定的进程了。杂凑表 pidhash 是在 kernel/fork.c 中定义的: 00035: struct task_struct *pidhash[PIDHASH_SZ]; 杂凑表的大小 PIDHASH_SZ 则在 include/linux/sched.h 中定义: 00485: #define PIDHASH_SZ (4096 >> 2) 杂凑表的大小为 1024。由于每个指针的大小是 4 个字节,所以整个杂凑表(不包括各个队列)正好 占一个页面。每个进程的 task_struct 数据结构都其 pidhash_next 和 pidhash_pprev 两个指针链入到杂凑表 中的某个队列中,同一队列中所有进程的 pid 都具有相同的杂凑值。由于杂凑表的使用,要找到 pid 为 某个给定值的进程就很迅速了。 当内核需要对每一个进程做点事情时,还需要将系统中所有的进程都组织成一个先行的队列,这样 就可以通过一个简单的 for 循环或 while 循环遍历所有进程的 task_struct 结构。所以,第三个组织就是这 么一个线性队列。系统中第一个建立的进程为 init_task,这个进程就是所有进程的总根,所以这个线性 队列就是以 init_task 为起点(也可把它看成是一个队头),后继每创建一个进程就通过其 task_struct 结构 中的 next_task 和 prev_task 两个指针链入这个线性队列中。 每个进程都必然同时身处这三个队列之中,直到进程消亡时才从这三个队列中摘除,所以这三个队 列都是静态的。 在运行的过程中,一个进程还可以动态地链接进“可执行队列”接受系统的调度。实际上,这是最 重要的队列,一个进程只有在可执行队列中才有可能受到调度而投入运行。与前几个队列不同的是,一 个进程的 task_struct 是通过其 list_head 数据结构 run_list、而不是个别的指针,链接进可执行队列的。以 前说过,这是用于双向链接的通用数据结构,具有一些与之配套的函数或宏操作,处理的效率比较高, 也使代码得以简化。可执行队列的变化是非常频繁的,一个进程进入睡眠时就从队列中脱链,被唤醒时 则又链入到该队列中,在调度的过程中也有可能会改变一个进程在此队列中的位置。详见本章“进程调 度与进程切换”以及“系统调用 nanosleep()”中的有关叙述。 4.2. 进程三步曲:创建、执行与消亡 就像世上万物都有产生、发展与消亡的过程一样,每个进程也有被创建、执行某段程序以及最后消 亡的过程。在 Linux 系统中,第一个进程是系统固有的、与生俱来的或者说是由内核的设计者安排好了 的。内核在引导并完成了基本的初始化以后,就有了系统的第一个进程(实际上是内核线程)。除此之外, 2006-12-31 版权所有,侵权必究 第 265 页,共 1481 页 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 266 页,共 1481 页 所有其他的进程和内核线程都由这个原是进程或其子孙进程所创建,都是这个原始进程的“后代”。在 Linux 系统中,一个新的进程一定要由一个已经存在的进程“复制”出来,而不是“创造”出来(而所 谓“创建”实际就是复制)。所以,Linux 系统(Unix 也一样)并不向用户(即进程)提供类似这样的系 统调用: int creat_proc(int (*fn)(void *), void *arg, unsigned long options); 可是在很多操作系统(包括一些 Unix 的变种)中都采用了“一揽子”的方法。它“创造”出一个进 程,并是该进程从函数指针 fn 所指的地方开始执行。根据不同的情况和设计,参数 fn 也可以换成一个 可执行程序的文件名。这里所谓“创造”,包括为进程分配所需的资源、包括最低限度的 task_struct 数据 结构和系统空间堆栈,并初始化这些资源;还要设置其系统空间堆栈,使得这个新进程看起来就好像一 个本来就已经存在而正在睡眠的进程。当这个进程被调度运行的时候,其“返回地址”,也就是“恢复” 运行时的下一条指令,则就在 fn 所指的地方。这个“子进程”生下来时两手空空,却可以完全独立,并 不与其父进程共享资源。 但是,Linux(以及 Unix)采用的方法却不同。 Linux 将进程的创建与目标程序的执行分成两步。第一步是从已经存在的“父进程”中像细胞分裂 一样地复制出一个“子进程”。这里所谓像“细胞分裂一样”,只是打个比方,实际上,复制出来的子进 程有自己的 task_struct 结构和系统空间堆栈,但与父进程共享其他所有资源。例如,要是父进程打开了 五个文件,那么子进程也有五个打开的文件,而且这些文件的当前读写指针也停在相同的地方。所以, 这一步所做的是“复制”。Linux 为此提供了两个系统调用,一个是 fork(),另一个是 clone()。两者的区 别在于 fork()是全部复制,父进程所有的资源全部都通过数据结构的复制“遗传”给子进程。而 clone() 则可以将资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享。在极端 的情况下,一个进程可以 clone()出一个线程。所以,系统调用 fork()是无参数的,而 clone()则带有参数。 读者也许已经意识到,fork()其实比 clone()更接近本来意义上的“克隆”。确实是这样,原因在于 fork() 从 Unix 的初期即已存在,那时候“克隆”这个词还不像现在这么流行,而既然业已存在,就不宜更改了。 否则,也许应该互换一下名字。后来,又增设了一个系统调用 vfork(),也不带参数,但是除 task_struct 结构和系统空间堆栈以外的资源全都通过数据结构指针的复制“遗传”,所以 vfork()出来的是线程而不 是进程。读者将会看到,vfork()主要是出于效率的考虑而设计并提供的。 第二步是目标程序的执行。一般来说,创建一个新的进程是因为有不同的目标程序要让新的进程去 执行(但也不一定),所以,复制完成以后,子进程通常要与父进程分道扬镳,走自己的路。Linux 为此 提供了一个系统调用 execve(),让一个进程执行以文件形式存在的一个可执行程序的映象。 读者也许要问:这两种方案到底哪一种好呢?应该说是各有利弊。但是更应该说,Linux 从 Unix 继 承下来的这种分两步走,并且在第一步中采取复制的方案,利远大于弊。从效率的角度看,分两步走很 有好处。所谓复制,只是进程的基本资源的复制,如 task_struct 数据结构、系统空间堆栈、页面表等等, 对父进程的代码及全局量则并不需要复制,而只是通过只读访问的形式实现共享,仅在需要写的时候才 通过 copy_on_write 的手段为所涉及的页面建立一个新的副本。所以,总的来说复制的代价是很低的,但 是通过复制而继承下来的资源则往往对子进程很有用。读者以后会看到,在计算机网络的实现中,以及 在 client/server 系统中的 server 一方的实现中,fork()或 clone()常常是最自然、最有效、最适宜的手段。 笔者有时候简直怀疑,到底是先有 fork()还是先有 client/server,因为 fork()似乎就是专门为此而设计的。 更重要的好处是,这样有利于父、子进程间通过 pipe 来建立起一种简单有效的进程间通信管道,并且从 而产生了操作系统的用户界面即 shell 的“管道”机制。这一点,对于 Unix 的发展和推广应用,对于 Unix 程序设计环境的形成,对于 Unix 程序设计风格的形成,都有着非常深远的影响。可以说,这是一项天才 的发明,它在很大程度上改变了操作系统的发展方向。 当然,从另一个角度,也就是从程序设计界面的角度来看,则“一揽子”的方案更为简洁。不过 fork() 加 execve()的方案也并不复杂很多。进一步说,这也像练武或演戏一样有个固定的“招式”,一旦掌握了 以后就不觉得复杂,也很少变化了。再说,如果有必要也可以通过程序库提供一个“一揽子”的库函数, 将这两步包装在一起。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 267 页,共 1481 页 创建了子进程以后,父进程有三个选择。第一是继续走自己的路,与子进程分道扬镳。只是如果子 进程先于父进程“去世”,则由内核给父进程发一个报丧的信号。第二是停下来,也就是进入睡眠状态, 等待子进程完成其使命而最终去世,然后父进程再继续运行。Linux 为此提供了两个系统调用,wait4() 和 wait3()。两个系统调用基本相同,wait4()等待某个特定的子进程去世,而 wait3()则等待任何一个子进 程去世。第三个选择是“自行退出历史舞台”,结束自己的生命。Linux 为此设置了一个系统调用 exit()。 这里的第三个选择其实不过是第一个选择的一种特例,所以从本质上说是两种选择:一种是父进程不受 阻的(non_blocking)方式,也称为“异步”的方式;另一种是父进程受阻的(blocking)方式,或者也 称为“同步”的方式。 下面是一个用来演示进程的这种“生命周期”的简单程序: 00001: include 00002: 00003: int main() 00004: { 00005: int child; 00006: char *args[] = {“/bin/echo”, “Hello”, “World!”, NULL}; 00007: 00008: if (!(child = fork())) 00009: { 00010: /* child */ 00011: printf(“pid %d: %d is my father\n”, getpid(), getppid()); 00012: execve(“/bin/echo”, args, NULL); 00013: printf(“pid %d: I am back, something is wrong!\n”, getpid()); 00014: } 00015: else 00016: { 00017: int myself = getpid(); 00018: printf(“pid %d: %d is my son\n”, myself, child); 00019: wait4(child, NULL, 0, NULL); 00020: printf(“pid %d: done\n”, myself); 00021: } 00022: return 0; 00023: } 这里,进入 main()的进程为父进程,它在第 8 行执行了系统调用 fork()创建了一个子进程,也就是复 制了一个子进程。子进程复制出来以后,就像其父进程一样地接受内核的调度,而且具有相同的返回地 址。所以,当父进程和子进程接受调度继续运行而从内核空间返回时都返回到同一点上。以前的代码执 行只有一个进程执行,而从这一点开始却有两个进程在执行了。复制出来的子进程全面地继承了父进程 的所有资源和特征,但还是有一些细微的却重要的区别。首先,子进程有一个不同于父进程的进程号 pid, 而且子进程的 task_struct 中有几个字段说明谁是它的父亲,就像人们的户口或档案中也有相应的栏目一 样。其次,也许是更为重要的是,二者从 fork()返回时所具有的返回值不一样。当子进程从 fork()“返回” 时,其返回值为 0;而父进程从 fork()返回时的返回值却是子进程的 pid,这是不可能为 0 的。这样,第 8 行的 if 语句就可以根据这个特征把二者区分开来,使两个进程各自知道“我是谁”。然后,第 10~12 行属于子进程,而 16~19 行属于父进程,虽然两个进程具有相同的视野,都能“看到”对方所要执行的 代码,但是 if 语句将它们各自的执行路线分开了。在这个程序中,我们选择了让父进程停下来等待,所 以父进程执行 wait4();而子进程则通过 execve()执行“bin/echo”。子进程在执行 echo 以后不会回到这里 的第 13 行,而是“壮士一去不复返”。这是因为在/bin/echo 中必定有一个 exit()调用,使子进程结束它的Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 268 页,共 1481 页 生命。对 exit()的调用是每一个可执行程序映象必有的,虽然在我们这个程序中并没有调用它,而是以 return 语句从 main()返回,但是 gcc 在编译和连接时会自动加上,所以谁也逃不过这一关。 由于子进程与父进程一样接受内核调度,而每次系统调用都有可能引起调度,所以二者返回的先后 次序是不定的,也不能根据返回的先后来确定谁是父进程谁是子进程。 还要指出,Linux 内核中确实有个貌似“一揽子”创建内核线程的函数(常常称为“原语”) kernel_thread(),供内核线程调用。但是,实际上这只是对 clone()的包装,它并不能像调用 execve()时那 样执行一个可执行映象文件,而只是执行内核中的某一个函数。我们不妨看一下它的代码,这是在 arch/i386/kernel/process.c 中给出的: 00436: /* 00437: * Create a kernel thread 00438: */ 00439: int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags) 00440: { 00441: long retval, d0; 00442: 00443: __asm__ __volatile__( 00444: "movl %%esp,%%esi\n\t" 00445: "int $0x80\n\t" /* Linux/i386 system call */ 00446: "cmpl %%esp,%%esi\n\t" /* child or parent? */ 00447: "je 1f\n\t" /* parent - jump */ 00448: /* Load the argument into eax, and push it. That way, it does 00449: * not matter whether the called function is compiled with 00450: * -mregparm or not. */ 00451: "movl %4,%%eax\n\t" 00452: "pushl %%eax\n\t" 00453: "call *%5\n\t" /* call fn */ 00454: "movl %3,%0\n\t" /* exit */ 00455: "int $0x80\n" 00456: "1:\t" 00457: :"=&a" (retval), "=&S" (d0) 00458: :"0" (__NR_clone), "i" (__NR_exit), 00459: "r" (arg), "r" (fn), 00460: "b" (flags | CLONE_VM) 00461: : "memory"); 00462: return retval; 00463: } 这里 445 行和 455 行的指令“int $0x80”就是系统调用。那么系统调用号是在哪里设置的呢?请看 457 行的输出部,这里寄存器 EAX 与变量 retval 相结合作为%0,而在 458 行开始的输入部里又规定, %0 应事先赋值为__NR_clone。所以,在进入 454 行时寄存器 EAX 已经被设置成__NR_clone,即 clone() 的系统调用号。从 clone()返回以后,这里采用了一种不同的方法区分父进程与子进程,就是将返回时的 堆栈指针与保存在寄存器 ESI 中的父进程的堆栈指针进行比较。由于每一个内核线程都有自己的系统空 间堆栈,子进程的堆栈指针必然与父进程不同。那么,为什么不采用像 fork()返回时所用的方法呢?这 是因为 clone()所产生的子线程可以具有与父线程相同的 pid,如果 pid 为 0 的内核线程再 clone()一个子线 程,则子线程的 pid 就也有可能是 0。所以,这里采用的比较堆栈指针的方法是更为可靠的。当然,这 个方法只有对内核线程才适用,因为普通的进程都在用户空间,根本就不知道其系统空间堆栈到底在哪Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 269 页,共 1481 页 里。 前面讲过,内核线程不能像进程一样执行一个可执行映象文件,而只能执行内核中的一个函数。453 行的 call 指令就是对这个函数的调用。函数指针%5 是什么呢?从 457 行的输出部开始数一下,就可以 知道%5 与变量 fn 相结合,而那正是 kernel_thread()的第一个参数,内核线程与进程在执行目标程序的方 式上的这种不同,又引发另一个重要的不同,那就是进程在调用 execve()之后不再返回,而是“客死他 乡”,在所执行的程序中去世。可是内核线程只不过是调用一个目标函数,当然要是从那个函数返回。所 以,这里在 455 行又进行一次系统调用,而这次的系统调用号在%3 中,那是 NR_exit。 以后,我们将围绕着前面的那个程序来介绍系统调用 fork()、clone()、execve()、wait4()以及 exit()的 实现,使读者对进程的创建、执行以及消亡有更深入的理解。 4.3. 系统调用fork()、vfork()与clone() 前面已经介绍过 fork()与 clone()二者的作用和区别。这里先来看一下二者在程序设计接口上的不同: pid_t fork(void); int clone(int (*fn)(void *arg), void *child_stack, int flags, void *arg); 系统调用__clone()的主要用途是创建一个线程,这个线程可以是内核线程,也可以是用户线程。创 建用户空间线程时,可以给定子线程用户空间堆栈的位置,还可以指定子进程运行的起点。同时,也可 以用__clone()创建进程,有选择地复制父进程的资源。而 fork(),则是全面地复制。还有一个系统调用 vfork(),其作用也是创建一个线程,但主要只是作为创建进程的中间步骤,目的在于提高创建时的效率, 减少系统开销,其程序设计接口则与 fork()相同。 这几个系统调用的代码都在 arch/i386/kernel/process.c 中: 00690: asmlinkage int sys_fork(struct pt_regs regs) 00691: { 00692: return do_fork(SIGCHLD, regs.esp, ®s, 0); 00693: } 00694: 00695: asmlinkage int sys_clone(struct pt_regs regs) 00696: { 00697: unsigned long clone_flags; 00698: unsigned long newsp; 00699: 00700: clone_flags = regs.ebx; 00701: newsp = regs.ecx; 00702: if (!newsp) 00703: newsp = regs.esp; 00704: return do_fork(clone_flags, newsp, ®s, 0); 00705: } 00706: 00707: /* 00708: * This is trivial, and on the face of it looks like it 00709: * could equally well be done in user mode. 00710: * 00711: * Not so, for quite unobvious reasons - register pressure. 00712: * In user mode vfork() cannot have a stack frame, and if 00713: * done by calling the "clone()" system call directly, you 00714: * do not have enough call-clobbered registers to hold all Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 270 页,共 1481 页 00715: * the information you need. 00716: */ 00717: asmlinkage int sys_vfork(struct pt_regs regs) 00718: { 00719: return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0); 00720: } 可见,三个系统调用的实现都是通过 do_fork()来完成的,不同的只是对 do_fork()的调用参数。关于 这些参数所起的作用,读了 do_fork()的代码以后就会清楚。注意 sys_clone()中的 regs.ecx,就是调用__clone 时的参数 child_stack,读者如果还不清楚,可以回到第 3 章“系统调用”一节顺着代码再走一遍。调用 __clone()时可以为子进程设置一个独立的用户空间堆栈(在同一个用户空间中),如果 child_stack 为 0, 就表示使用父进程的用户空间堆栈。这三个系统调用的主体部分 do_fork()是在 kernel/fork.c 中定义的。 这个函数比较大,让我们逐段往下看: [sys_fork() > do_fork()] 00546: /* 00547: * Ok, this is the main fork-routine. It copies the system process 00548: * information (task[nr]) and sets up the necessary registers. It also 00549: * copies the data segment in its entirety. The "stack_start" and 00550: * "stack_top" arguments are simply passed along to the platform 00551: * specific copy_thread() routine. Most platforms ignore stack_top. 00552: * For an example that's using stack_top, see 00553: * arch/ia64/kernel/process.c. 00554: */ 00555: int do_fork(unsigned long clone_flags, unsigned long stack_start, 00556: struct pt_regs *regs, unsigned long stack_size) 00557: { 00558: int retval = -ENOMEM; 00559: struct task_struct *p; 00560: DECLARE_MUTEX_LOCKED(sem); 00561: 00562: if (clone_flags & CLONE_PID) { 00563: /* This is only allowed from the boot up thread */ 00564: if (current->pid) 00565: return -EPERM; 00566: } 00567: 00568: current->vfork_sem = &sem; 00569: 00570: p = alloc_task_struct(); 00571: if (!p) 00572: goto fork_out; 00573: 00574: *p = *current; 第 560 行的宏操作 DECLARE_MUTEX_LOCKED()定义和创建了一个用于进程间互斥和同步的信号 量,其定义和实现见第 6 章“进程间通信”。 参数 clone_flags 由两部分组成,其最低的字节为信号类型,用以规定子进程去世时应该向该父进程Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 271 页,共 1481 页 发出的信号。我们已经看到,对于 fork()和 vfork()这个信号就是 SIGCHLD,而对__clone()则该段可由调 用者决定。第二部分是一些表示资源和特性的标志位,这些标志位是在 include/linux/sched.h 中定义的: 00030: /* 00031: * cloning flags: 00032: */ 00033: #define CSIGNAL 0x000000ff /* signal mask to be sent at exit */ 00034: #define CLONE_VM 0x00000100 /* set if VM shared between processes */ 00035: #define CLONE_FS 0x00000200 /* set if fs info shared between processes */ 00036: #define CLONE_FILES 0x00000400 /* set if open files shared between processes */ 00037: #define CLONE_SIGHAND 0x00000800 /* set if signal handlers and blocked signals shared */ 00038: #define CLONE_PID 0x00001000 /* set if pid shared */ 00039: #define CLONE_PTRACE 0x00002000 /* set if we want to let tracing continue on the child too */ 00040: #define CLONE_VFORK 0x00004000 /* set if the parent wants the child to wake it up on mm_release */ 00041: #define CLONE_PARENT 0x00008000 /* set if we want to have the same parent as the cloner */ 00042: #define CLONE_THREAD 0x00010000 /* Same thread group? */ 00043: 00044: #define CLONE_SIGNAL (CLONE_SIGHAND | CLONE_THREAD) 对于 fork(),这一部分为全 0,表示对有关的资源都要复制而不是通过指针共享。而对 vfork(),则为 CLONE_VFORK | CLONE_VM,表示父、子进程共用(用户)虚存区间,并且当子进程释放其虚存区间 时要唤醒父进程。至于__clone(),则这一部分完全由调用者设定而作为参数传递下来。其中标志位 CLONE_PID 有特殊的作用,当这个标志位为 1 时,父、子进程(线程)共用同一个进程号,也就是说, 则进程虽然有其自己的 task_struct 数据结构,却使用父进程的 pid。但是,只有 0 号进程,也就是系统中 的原是进程(实际上是线程),才允许这样来调用__clone(),所以 564 行对此加以检查。 接着,通过 alloc_task_struct()为子进程分配两个连续的物理页面,低端用作子进程的 task_struct 结 构,高端则用作其系统空间堆栈。 注意 574 行的赋值为整个数据结构的赋值。这样,父进程的整个 task_struct 就被复制到了子进程的 数据结构中。经编译以后,这样的赋值是用 memcpy()实现的,所以效率很高。 接着看下一段(fork.c): [sys_fork() > do_fork()] 00576: retval = -EAGAIN; 00577: if (atomic_read(&p->user->processes) >= p->rlim[RLIMIT_NPROC].rlim_cur) 00578: goto bad_fork_free; 00579: atomic_inc(&p->user->__count); 00580: atomic_inc(&p->user->processes); 00581: 00582: /* 00583: * Counter increases are protected by 00584: * the kernel lock so nr_threads can't 00585: * increase under us (but it may decrease). 00586: */ 00587: if (nr_threads >= max_threads) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 272 页,共 1481 页 00588: goto bad_fork_cleanup_count; 00589: 00590: get_exec_domain(p->exec_domain); 00591: 00592: if (p->binfmt && p->binfmt->module) 00593: __MOD_INC_USE_COUNT(p->binfmt->module); 00594: 00595: p->did_exec = 0; 00596: p->swappable = 0; 00597: p->state = TASK_UNINTERRUPTIBLE; 00598: 00599: copy_flags(clone_flags, p); 00600: p->pid = get_pid(clone_flags); 00601: 在 task_struct 结构中有个指针 user,用来指向一个 user_struct 结构。一个用户常常有许多个进程, 所以有关用户的一些信息并不专属于某一个进程。这样,属于同一用户的进程就可以通过指针 user 共享 这些信息。显然,每个用户有且只有一个 user_struct 结构。结构中有个计数器 count,对属于该用户的进 程数量计数。可想而知,内核线程并不属于某个用户,所以其 task_struct 中的 user 指针为 0。这个数据 结构的定义在 include/linux/sched.h 中: 00256: /* 00257: * Some day this will be a full-fledged user tracking system.. 00258: */ 00259: struct user_struct { 00260: atomic_t __count; /* reference count */ 00261: atomic_t processes; /* How many processes does this user have? */ 00262: atomic_t files; /* How many open files does this user have? */ 00263: 00264: /* Hash table maintenance information */ 00265: struct user_struct *next, **pprev; 00266: uid_t uid; 00267: }; 熟悉 Unix 内核的读者要注意,不要把 Unix 的进程控制结构中的 user 去与这里的 user_struct 结构相 混淆,二者是截然不同的概念。在 kernel/user.c 中还定义了一个 user_struct 结构指针的数组 uidhash: 00019: #define UIDHASH_BITS 8 00020: #define UIDHASH_SZ (1 << UIDHASH_BITS) 00026: static struct user_struct *uidhash_table[UIDHASH_SZ]; 这是一个杂凑(hash)表。对用户名施以杂凑运算,就可以计算出一个下标而找到该用户的 user_struct 结构。 各进程的 task_struct 结构中还有个数组 rlim,对该进程占用各种资源的数量作出限制,而 rlim[RLIMIT_NPROC]就规定了该进程所属的用户可以拥有的进程数量。所以,如果当前进程是一个用 户进程,并且该用户拥有的进程数量已经达到了规定的限制值,就不再允许它 fork()了。那么,对于不 属于任何用户的内核线程怎么办呢?587 行中的两个计数器就是为进程的总量而设的。 一个进程除了属于某一个用户之外,还属于某个“执行域”。总的来说,Linux 是 Unix 的一个变种, 并且符合 POSIX 的规定。但是,有很多版本的操作系统同样是 Unix 变种,同样符合 POSIX 规定,互相Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 273 页,共 1481 页 之间在实现细节上却仍有明显的不同。例如,AT&T 的 Sys V 和 BSD 4.2 就有相当的不同,而 Sun 的 Solaris 又有区别,这就形成了不同的执行域。如果一个进程所执行的程序是为 Solaris 开发的,那么这个进程就 属于 Solaris 执行域 PER_SOLARIS。当然,在 Linux 上运行的绝大多数程序都属于 Linux 执行域。在 task_struct 结构中有一个指针 exec_domain ,可以指向一个 exec_domain 数据结构。那是在 include/linux/personality.h 中定义的: 00038: /* Description of an execution domain - personality range supported, 00039: * lcall7 syscall handler, start up / shut down functions etc. 00040: * N.B. The name and lcall7 handler must be where they are since the 00041: * offset of the handler is hard coded in kernel/sys_call.S. 00042: */ 00043: struct exec_domain { 00044: const char *name; 00045: lcall7_func handler; 00046: unsigned char pers_low, pers_high; 00047: unsigned long * signal_map; 00048: unsigned long * signal_invmap; 00049: struct module * module; 00050: struct exec_domain *next; 00051: }; 函数指针 handler,用于通过调用门实现系统调用,我们并不关心。字节 per_low 为某种域的代码, 有 PER_LINUX、PER_SVR4、PER_BSD 和 PER_SOLARIS 等等。 我们在这里主要关心的结构成分是 module,这是指向某个 module 数据结构的指针。读者在有关文 件系统和设备驱动的章节中将会看到,在 Linux 系统中设备驱动程序可以设计并实现成“动态安装模块” module,使其在运行时动态地安装和拆除。这些“动态安装模块”与运行中的进程的执行域有密切的关 系。例如,一个属于 Solaris 执行域的进程就很可能要用到专门为 Solaris 设置的一些模块,只要还有一 个这样的进程在运行,这些为 Solaris 所需的模块就不能拆除。所以,在描述每个已安装模块的数据结构 中都有一个计数器,表明有几个进程需要使用这个模块。因此,do_fork()中通过 590行的 get_exec_domain() 递增具体模块的数据结构中的计数器(定义在 include/linux/personality.h 中)。 00059: #define get_exec_domain(it) \ 00060: if (it && it->module) __MOD_INC_USE_COUNT(it->module); 同样的道理,每个进程所执行的程序属于某种可执行影像格式,如 a.out 格式、elf 格式、甚至 java 虚拟机格式。对这些不同格式的支持通常是通过动态安装的驱动模块来实现的。所以 task_struct 结构中 还有一个指向 linux_binfmt 数据结构的指针 binfmt,而 do_fork()中 593 行的__MOD_INC_USE_COUNT() 就是对有关模块的使用计数器进行操作。 为什么要在 597 行把状态设成 TASK_UNINTERRUPTIBLE 呢?这是因为在 get_pid()中产生一个新 的 pid 的操作必须是独占的,当前进程可能会因为一时进不了临界区而只好暂时进入睡眠状态等待,所 以才事先把状态设成 UNINTERRUPTIBLE。函数 copy_flags()将参数 clone_flags 中的标志位略加补充和 变换,然后写入 p->flags。这个函数的代码也在 fork.c 中。读者可以自己阅读。 至于 600 行的 get_pid(),则根据 clone_flags 中标志位 CLONE_PID 的值,或返回父进程(当前进程) 的 pid,或返回一个新的 pid 放在子进程的 task_struct 中。函数 get_pid()的代码也在 fork.c 中: [sys_fork() > do_fork() > get_pid()] 00082: static int get_pid(unsigned long flags) 00083: { 00084: static int next_safe = PID_MAX; 00085: struct task_struct *p; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 274 页,共 1481 页 00086: 00087: if (flags & CLONE_PID) 00088: return current->pid; 00089: 00090: spin_lock(&lastpid_lock); 00091: if((++last_pid) & 0xffff8000) { 00092: last_pid = 300; /* Skip daemons etc. */ 00093: goto inside; 00094: } 00095: if(last_pid >= next_safe) { 00096: inside: 00097: next_safe = PID_MAX; 00098: read_lock(&tasklist_lock); 00099: repeat: 00100: for_each_task(p) { 00101: if(p->pid == last_pid || 00102: p->pgrp == last_pid || 00103: p->session == last_pid) { 00104: if(++last_pid >= next_safe) { 00105: if(last_pid & 0xffff8000) 00106: last_pid = 300; 00107: next_safe = PID_MAX; 00108: } 00109: goto repeat; 00110: } 00111: if(p->pid > last_pid && next_safe > p->pid) 00112: next_safe = p->pid; 00113: if(p->pgrp > last_pid && next_safe > p->pgrp) 00114: next_safe = p->pgrp; 00115: if(p->session > last_pid && next_safe > p->session) 00116: next_safe = p->session; 00117: } 00118: read_unlock(&tasklist_lock); 00119: } 00120: spin_unlock(&lastpid_lock); 00121: 00122: return last_pid; 00123: } 这里的常数 PID_MAX 定义为 0x8000。可见,进程号的最大值是 0x7fff,即 32767。进程号 0~299 是为系统进程(包括内核线程)保留的,主要用于各种“保护神”进程。以上这段代码的逻辑并不复杂, 我们就不多加解释了。 回到 do_fork()中再往下看(fork.c): [sys_fork() > do_fork()] 00602: p->run_list.next = NULL; 00603: p->run_list.prev = NULL; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 275 页,共 1481 页 00604: 00605: if ((clone_flags & CLONE_VFORK) || !(clone_flags & CLONE_PARENT)) { 00606: p->p_opptr = current; 00607: if (!(p->ptrace & PT_PTRACED)) 00608: p->p_pptr = current; 00609: } 00610: p->p_cptr = NULL; 00611: init_waitqueue_head(&p->wait_chldexit); 00612: p->vfork_sem = NULL; 00613: spin_lock_init(&p->alloc_lock); 00614: 00615: p->sigpending = 0; 00616: init_sigpending(&p->pending); 00617: 00618: p->it_real_value = p->it_virt_value = p->it_prof_value = 0; 00619: p->it_real_incr = p->it_virt_incr = p->it_prof_incr = 0; 00620: init_timer(&p->real_timer); 00621: p->real_timer.data = (unsigned long) p; 00622: 00623: p->leader = 0; /* session leadership doesn't inherit */ 00624: p->tty_old_pgrp = 0; 00625: p->times.tms_utime = p->times.tms_stime = 0; 00626: p->times.tms_cutime = p->times.tms_cstime = 0; 00627: #ifdef CONFIG_SMP 00628: { 00629: int i; 00630: p->has_cpu = 0; 00631: p->processor = current->processor; 00632: /* ?? should we just memset this ?? */ 00633: for(i = 0; i < smp_num_cpus; i++) 00634: p->per_cpu_utime[i] = p->per_cpu_stime[i] = 0; 00635: spin_lock_init(&p->sigmask_lock); 00636: } 00637: #endif 00638: p->lock_depth = -1; /* -1 = no lock */ 00639: p->start_time = jiffies; 00640: 我们在前一节中提到过 wait4()和 wait3(),一个进程可以停下来等待其子进程完成使命。为此,在 task_struct 中设置了一个队列头部 wait_childexit,前面在复制 task_struct 结构时把这也照抄过来,而子进 程此时尚未“出生”,当然谈不上子进程的等待队列,所以要在 611 行中加以初始化。 类似地,对各种信息量也要加以初始化。这里 615 和 616 行是对子进程的待处理信号队列以及有关 结构成分的初始化。对这些与信号有关的结构成分我们将在“进程间通信”的信号一节中详细介绍。接 下来是对 task_struct 结构中各种计时变量的初始化,我们将在“进程调度”一节中介绍这些变量。在这 里我们并不关心对多处理器 SMP 结构的特殊考虑,所以也跳过 627~637 行。 最后,task_struct 结构中的 start_time 表示进程创建的时间,而全局变量 jiffies 的数值就是以时钟中Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 276 页,共 1481 页 断周期为单位的从系统初始化开始至此时的时间。 至此,对 task_struct 数据结构的复制与初始化就基本完成了。下面就轮到其他的资源了: [sys_fork() > do_fork()] 00641: retval = -ENOMEM; 00642: /* copy all the process information */ 00643: if (copy_files(clone_flags, p)) 00644: goto bad_fork_cleanup; 00645: if (copy_fs(clone_flags, p)) 00646: goto bad_fork_cleanup_files; 00647: if (copy_sighand(clone_flags, p)) 00648: goto bad_fork_cleanup_fs; 00649: if (copy_mm(clone_flags, p)) 00650: goto bad_fork_cleanup_sighand; 00651: retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs); 00652: if (retval) 00653: goto bad_fork_cleanup_sighand; 00654: p->semundo = NULL; 00655: 函数copy_files()有条件地复制已打开文件的控制结构,这种复制只有在clone_flags中CLONE_FILES 标志位为 0 时才真正进行,否则就只是共享父进程的已打开文件。当一个进程有已打开文件时,task_struct 结构中的指针 files 指向一个 files_struct 数据结构,否则为 0。所有与终端设备 tty 相联系的用户进程的头 三个文件,即 stdin、stdout 及 stderr,都是预先打开的,所以指针一般不会是 0。数据结构 files_struct 是 在 include/linux/sched.h 中定义的(详见“文件系统”一章),copy_files()的代码则还是在 fork.c 中: [sys_fork() > do_fork() > copy_files()] 00408: static int copy_files(unsigned long clone_flags, struct task_struct * tsk) 00409: { 00410: struct files_struct *oldf, *newf; 00411: struct file **old_fds, **new_fds; 00412: int open_files, nfds, size, i, error = 0; 00413: 00414: /* 00415: * A background process may not have any files ... 00416: */ 00417: oldf = current->files; 00418: if (!oldf) 00419: goto out; 00420: 00421: if (clone_flags & CLONE_FILES) { 00422: atomic_inc(&oldf->count); 00423: goto out; 00424: } 00425: 00426: tsk->files = NULL; 00427: error = -ENOMEM; 00428: newf = kmem_cache_alloc(files_cachep, SLAB_KERNEL); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 277 页,共 1481 页 00429: if (!newf) 00430: goto out; 00431: 00432: atomic_set(&newf->count, 1); 00433: 00434: newf->file_lock = RW_LOCK_UNLOCKED; 00435: newf->next_fd = 0; 00436: newf->max_fds = NR_OPEN_DEFAULT; 00437: newf->max_fdset = __FD_SETSIZE; 00438: newf->close_on_exec = &newf->close_on_exec_init; 00439: newf->open_fds = &newf->open_fds_init; 00440: newf->fd = &newf->fd_array[0]; 00441: 00442: /* We don't yet have the oldf readlock, but even if the old 00443: fdset gets grown now, we'll only copy up to "size" fds */ 00444: size = oldf->max_fdset; 00445: if (size > __FD_SETSIZE) { 00446: newf->max_fdset = 0; 00447: write_lock(&newf->file_lock); 00448: error = expand_fdset(newf, size); 00449: write_unlock(&newf->file_lock); 00450: if (error) 00451: goto out_release; 00452: } 00453: read_lock(&oldf->file_lock); 00454: 00455: open_files = count_open_files(oldf, size); 00456: 00457: /* 00458: * Check whether we need to allocate a larger fd array. 00459: * Note: we're not a clone task, so the open count won't 00460: * change. 00461: */ 00462: nfds = NR_OPEN_DEFAULT; 00463: if (open_files > nfds) { 00464: read_unlock(&oldf->file_lock); 00465: newf->max_fds = 0; 00466: write_lock(&newf->file_lock); 00467: error = expand_fd_array(newf, open_files); 00468: write_unlock(&newf->file_lock); 00469: if (error) 00470: goto out_release; 00471: nfds = newf->max_fds; 00472: read_lock(&oldf->file_lock); 00473: } Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 278 页,共 1481 页 00474: 00475: old_fds = oldf->fd; 00476: new_fds = newf->fd; 00477: 00478: memcpy(newf->open_fds->fds_bits, oldf->open_fds->fds_bits, open_files/8); 00479: memcpy(newf->close_on_exec->fds_bits, oldf->close_on_exec->fds_bits, 00408: open_files/8); 00480: 00481: for (i = open_files; i != 0; i--) { 00482: struct file *f = *old_fds++; 00483: if (f) 00484: get_file(f); 00485: *new_fds++ = f; 00486: } 00487: read_unlock(&oldf->file_lock); 00488: 00489: /* compute the remainder to be cleared */ 00490: size = (newf->max_fds - open_files) * sizeof(struct file *); 00491: 00492: /* This is long word aligned thus could use a optimized version */ 00493: memset(new_fds, 0, size); 00494: 00495: if (newf->max_fdset > open_files) { 00496: int left = (newf->max_fdset-open_files)/8; 00497: int start = open_files / (8 * sizeof(unsigned long)); 00498: 00499: memset(&newf->open_fds->fds_bits[start], 0, left); 00500: memset(&newf->close_on_exec->fds_bits[start], 0, left); 00501: } 00502: 00503: tsk->files = newf; 00504: error = 0; 00505: out: 00506: return error; 00507: 00508: out_release: 00509: free_fdset (newf->close_on_exec, newf->max_fdset); 00510: free_fdset (newf->open_fds, newf->max_fdset); 00511: kmem_cache_free(files_cachep, newf); 00512: goto out; 00513: } 读者可以在学习了“文件系统”一章以后再回过头来仔细阅读这段代码,我们再这里先作一些解释。 先看复制的方向。因为是当前进程在创建子进程,是从当前进程复制到子进程,所以把当前进程的 task_struct 结构中的 files_struct 结构指针作为 oldf。 再看复制的条件。如果参数 clone_flags 中的 CLONE_FILES 标志位为 1,就只通过 atomic_inc()递增Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 279 页,共 1481 页 当前进程的 files_struct 结构中的共享计数,表示这个数据结构现在多了一个“用户”,就返回了。由于在 此之前已通过数据结构赋值将当前进程的整个 task_struct 结构都复制给了子进程,结构中的指针 files 自 然也复制到了子进程的 task_struct 结构中,使子进程通过这个指针共享当前进程的 files_struct 数据结构。 否则,如果 CLONE_FILES 标志位为 0,那就要复制了。首先通过 kmem_cache_alloc()为子进程分配一个 files_struct 数据结构为 newf,然后从 oldf 把内容复制到 newf。在 files_struct 数据结构中有三个主要的“部 件”。其一是个位图,名为 close_on_exec_init;其二也是位图,名为 open_fds_init;其三则是 file 结构数 组 fd_array[]。这三个部件都是固定大小的,如果打开的文件数量超过其容量,就得通过 expand_fdset() 和 expand_fd_array()在 files_struct 数据结构以外另行分配空间最为替换。不管是采用 files_struct 数据结 构内部的这三个部件或是采用外部的替换空间,指针 close_on_exec、open_fds 和 fd 总是分别指向这三组 信息。所以,如果复制取决于已打开文件的数量。 显而易见,共享不复制要简单得多。那么二者在效果上到底有什么区别呢?如果共享就可以达到目 的,为什么还要不辞辛劳地复制呢?区别在于子进程(以及父进程本身)是否能“独立自主”。当复制完 成之初,子进程有了一份“副本”,它的内容与父进程的“正本”在内容上基本上是相同的,在这一点上 似乎与共享没有什么区别。可是,随后区别就来了。在共享的情况下,两个进程是互相牵制的。如果子 进程对某个已打开文件调用了一次 lseek(),则父进程对这个文件的读写位置也随着改变了,因为两个进 程共享着对文件的同一个读写上下文。而在复制的情况下就不一样了,由于子进程有自己的副本,就有 了对同一文件的另一个读写上下文,以后就可以各走各的路,互不干扰了。 除 files_struct 数据结构外,还有个 fs_struct 数据结构也是与文件系统有关的,也要通过共享或复制 遗传给子进程。类似地,copy_fs()也是只有在 clone_flags 中 CLONE_FS 标志位为 0 时才加以复制。 task_struct 结构中的指针指向一个 fs_struct 数据结构,结构中记录的是进程的根目录 root、当前工作目录 pwd、一个用于文件操作权限管理的 umask,还有一个计数器,其定义在 include/linux/fs_struct.h 中(详 见“文件系统”一章)。函数 copy_fs()连同几个有关低层函数的代码也在 fork.c 中。我们把这些代码留给 读者: [sys_fork() > do_fork() > copy_fs()] 00383: static inline int copy_fs(unsigned long clone_flags, struct task_struct * tsk) 00384: { 00385: if (clone_flags & CLONE_FS) { 00386: atomic_inc(¤t->fs->count); 00387: return 0; 00388: } 00389: tsk->fs = __copy_fs_struct(current->fs); 00390: if (!tsk->fs) 00391: return -1; 00392: return 0; 00393: } [sys_fork() > do_fork() > copy_fs() > copy_fs_struct()] 00378: struct fs_struct *copy_fs_struct(struct fs_struct *old) 00379: { 00380: return __copy_fs_struct(old); 00381: } [sys_fork() > do_fork() > copy_fs() > copy_fs_struct() > __ copy_fs_struct()] 00353: static inline struct fs_struct *__copy_fs_struct(struct fs_struct *old) 00354: { Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 280 页,共 1481 页 00355: struct fs_struct *fs = kmem_cache_alloc(fs_cachep, GFP_KERNEL); 00356: /* We don't need to lock fs - think why ;-) */ 00357: if (fs) { 00358: atomic_set(&fs->count, 1); 00359: fs->lock = RW_LOCK_UNLOCKED; 00360: fs->umask = old->umask; 00361: read_lock(&old->lock); 00362: fs->rootmnt = mntget(old->rootmnt); 00363: fs->root = dget(old->root); 00364: fs->pwdmnt = mntget(old->pwdmnt); 00365: fs->pwd = dget(old->pwd); 00366: if (old->altroot) { 00367: fs->altrootmnt = mntget(old->altrootmnt); 00368: fs->altroot = dget(old->altroot); 00369: } else { 00370: fs->altrootmnt = NULL; 00371: fs->altroot = NULL; 00372: } 00373: read_unlock(&old->lock); 00374: } 00375: return fs; 00376: } 代码中的 mntget()和 dget()都是用来递增相应数据结构共享计数的,因为这些数据结构现在多了一个 用户。注意,在这里要复制的是 fs_struct 数据结构,而并不复制更深层次的数据结构。复制了 fs_struct 数据结构,就在这一层上有了自主性,至于对更深层次的数据结构则还是共享,所以需要递增它们的共 享计数。 接着是关于对信号的处理方式。是否复制父进程对信号的处理是由标志位 CLONE_SIGHAND 控制 的。信号基本上是一种进程间通信手段,信号之于一个进程就好像中断之于一个处理器。进程可以为各 种信号设置用于该信号的处理程序,就好像系统可以为各个中断源设置相应的中断服务程序一样。如果 一个进程设置了信号处理程序,其 task_struct 结构的指针 sig 就指向一个 signal_struct 数据结构。这种结 构是在 include/linux/sched.h 中定义的: 00243: struct signal_struct { 00244: atomic_t count; 00245: struct k_sigaction action[_NSIG]; 00246: spinlock_t siglock; 00247: }; 其中的数组 action[]确定了一个进程对各种信号(以信号的数值为下标)的反应和处理,子进程可以 通过复制或共享把它从父进程继承下来。函数 copy_sighand()的代码如下(fork.c): [sys_fork() > do_fork() > copy_sighand()] 00515: static inline int copy_sighand(unsigned long clone_flags, struct task_struct * tsk) 00516: { 00517: struct signal_struct *sig; 00518: 00519: if (clone_flags & CLONE_SIGHAND) { 00520: atomic_inc(¤t->sig->count); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 281 页,共 1481 页 00521: return 0; 00522: } 00523: sig = kmem_cache_alloc(sigact_cachep, GFP_KERNEL); 00524: tsk->sig = sig; 00525: if (!sig) 00526: return -1; 00527: spin_lock_init(&sig->siglock); 00528: atomic_set(&sig->count, 1); 00529: memcpy(tsk->sig->action, current->sig->action, sizeof(tsk->sig->action)); 00530: return 0; 00531: } 像 copy_files()和 copy_fs()一样,copy_sighand()也只有在 CLONE_SIGHAND 为 0 时才真正进行;否 则就共享父进程的 sig 指针,并将父进程的 signal_struct 中的共享计数加 1。 然后是用户空间的继承。进程的 task_struct 结构中有个指针 mm,读者已经相当熟悉了,它指向一 个代表着进程的用户空间的 mm_struct 数据结构。由于内核线程并不拥有用户空间,所以在内核线程的 task_struct 结构中该指针为 0。有关 mm_struct 及其下属的 vm_area_struct 等数据结构已经在第 2 章中介 绍过,这里不再重复。函数 copy_mm()的代码还是在 fork.c 中: [sys_fork() > do_fork() > copy_mm()] 00279: static int copy_mm(unsigned long clone_flags, struct task_struct * tsk) 00280: { 00281: struct mm_struct * mm, *oldmm; 00282: int retval; 00283: 00284: tsk->min_flt = tsk->maj_flt = 0; 00285: tsk->cmin_flt = tsk->cmaj_flt = 0; 00286: tsk->nswap = tsk->cnswap = 0; 00287: 00288: tsk->mm = NULL; 00289: tsk->active_mm = NULL; 00290: 00291: /* 00292: * Are we cloning a kernel thread? 00293: * 00294: * We need to steal a active VM for that.. 00295: */ 00296: oldmm = current->mm; 00297: if (!oldmm) 00298: return 0; 00299: 00300: if (clone_flags & CLONE_VM) { 00301: atomic_inc(&oldmm->mm_users); 00302: mm = oldmm; 00303: goto good_mm; 00304: } 00305: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 282 页,共 1481 页 00306: retval = -ENOMEM; 00307: mm = allocate_mm(); 00308: if (!mm) 00309: goto fail_nomem; 00310: 00311: /* Copy the current MM stuff.. */ 00312: memcpy(mm, oldmm, sizeof(*mm)); 00313: if (!mm_init(mm)) 00314: goto fail_nomem; 00315: 00316: down(&oldmm->mmap_sem); 00317: retval = dup_mmap(mm); 00318: up(&oldmm->mmap_sem); 00319: 00320: /* 00321: * Add it to the mmlist after the parent. 00322: * 00323: * Doing it this way means that we can order 00324: * the list, and fork() won't mess up the 00325: * ordering significantly. 00326: */ 00327: spin_lock(&mmlist_lock); 00328: list_add(&mm->mmlist, &oldmm->mmlist); 00329: spin_unlock(&mmlist_lock); 00330: 00331: if (retval) 00332: goto free_pt; 00333: 00334: /* 00335: * child gets a private LDT (if there was an LDT in the parent) 00336: */ 00337: copy_segments(tsk, mm); 00338: 00339: if (init_new_context(tsk,mm)) 00340: goto free_pt; 00341: 00342: good_mm: 00343: tsk->mm = mm; 00344: tsk->active_mm = mm; 00345: return 0; 00346: 00347: free_pt: 00348: mmput(mm); 00349: fail_nomem: 00350: return retval; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 283 页,共 1481 页 00351: } 显然,对 mm_struct 的复制也是只在 clone_flags 中 CLONE_VM 标志为 0 时才真正进行,否则就只 是通过已经复制的指针共享父进程的用户空间。对 mm_struct 的复制就不只是局限于这个数据结构本身 了,也包括了对更深层次数据结构的复制。其中最重要的是 vm_area_struct 数据结构和页面映射表,这 就是由 dup_mmap()复制的。函数 dup_mmap()的代码也在 fork.c 中。读者在认真读过本书第 2 章以后, 阅读这段程序时应该不会感到困难,同时也是一次很好的练习。 [sys_fork() > do_fork() > copy_mm() > dup_mmap()] 00125: static inline int dup_mmap(struct mm_struct * mm) 00126: { 00127: struct vm_area_struct * mpnt, *tmp, **pprev; 00128: int retval; 00129: 00130: flush_cache_mm(current->mm); 00131: mm->locked_vm = 0; 00132: mm->mmap = NULL; 00133: mm->mmap_avl = NULL; 00134: mm->mmap_cache = NULL; 00135: mm->map_count = 0; 00136: mm->cpu_vm_mask = 0; 00137: mm->swap_cnt = 0; 00138: mm->swap_address = 0; 00139: pprev = &mm->mmap; 00140: for (mpnt = current->mm->mmap ; mpnt ; mpnt = mpnt->vm_next) { 00141: struct file *file; 00142: 00143: retval = -ENOMEM; 00144: if(mpnt->vm_flags & VM_DONTCOPY) 00145: continue; 00146: tmp = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL); 00147: if (!tmp) 00148: goto fail_nomem; 00149: *tmp = *mpnt; 00150: tmp->vm_flags &= ~VM_LOCKED; 00151: tmp->vm_mm = mm; 00152: mm->map_count++; 00153: tmp->vm_next = NULL; 00154: file = tmp->vm_file; 00155: if (file) { 00156: struct inode *inode = file->f_dentry->d_inode; 00157: get_file(file); 00158: if (tmp->vm_flags & VM_DENYWRITE) 00159: atomic_dec(&inode->i_writecount); 00160: 00161: /* insert tmp into the share list, just after mpnt */ 00162: spin_lock(&inode->i_mapping->i_shared_lock); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 284 页,共 1481 页 00163: if((tmp->vm_next_share = mpnt->vm_next_share) != NULL) 00164: mpnt->vm_next_share->vm_pprev_share = 00165: &tmp->vm_next_share; 00166: mpnt->vm_next_share = tmp; 00167: tmp->vm_pprev_share = &mpnt->vm_next_share; 00168: spin_unlock(&inode->i_mapping->i_shared_lock); 00169: } 00170: 00171: /* Copy the pages, but defer checking for errors */ 00172: retval = copy_page_range(mm, current->mm, tmp); 00173: if (!retval && tmp->vm_ops && tmp->vm_ops->open) 00174: tmp->vm_ops->open(tmp); 00175: 00176: /* 00177: * Link in the new vma even if an error occurred, 00178: * so that exit_mmap() can clean up the mess. 00179: */ 00180: *pprev = tmp; 00181: pprev = &tmp->vm_next; 00182: 00183: if (retval) 00184: goto fail_nomem; 00185: } 00186: retval = 0; 00187: if (mm->map_count >= AVL_MIN_MAP_COUNT) 00188: build_mmap_avl(mm); 00189: 00190: fail_nomem: 00191: flush_tlb_mm(current->mm); 00192: return retval; 00193: } 这里通过 140~185 行的 for 循环对同一用户空间中的各个区间进行复制。对于通过 mmap()映射到 某个文件的区间,155~169 行是一些特殊的附加处理。172 行的 copy_page_range()是关键所在,这个函 数逐层处理页面目录项和页面表,其代码在 mm/memory.c 中: [sys_fork() > do_fork() > copy_mm() > dup_mmap() > copy_page_range()] 00144: /* 00145: * copy one vm_area from one task to the other. Assumes the page tables 00146: * already present in the new task to be cleared in the whole range 00147: * covered by this vma. 00148: * 00149: * 08Jan98 Merged into one routine from several inline routines to reduce 00150: * variable count and make things faster. -jj 00151: */ 00152: int copy_page_range(struct mm_struct *dst, struct mm_struct *src, 00153: struct vm_area_struct *vma) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 285 页,共 1481 页 00154: { 00155: pgd_t * src_pgd, * dst_pgd; 00156: unsigned long address = vma->vm_start; 00157: unsigned long end = vma->vm_end; 00158: unsigned long cow = (vma->vm_flags & (VM_SHARED | VM_MAYWRITE)) == VM_MAYWRITE; 00159: 00160: src_pgd = pgd_offset(src, address)-1; 00161: dst_pgd = pgd_offset(dst, address)-1; 00162: 00163: for (;;) { 00164: pmd_t * src_pmd, * dst_pmd; 00165: 00166: src_pgd++; dst_pgd++; 00167: 00168: /* copy_pmd_range */ 00169: 00170: if (pgd_none(*src_pgd)) 00171: goto skip_copy_pmd_range; 00172: if (pgd_bad(*src_pgd)) { 00173: pgd_ERROR(*src_pgd); 00174: pgd_clear(src_pgd); 00175: skip_copy_pmd_range: address = (address + PGDIR_SIZE) & PGDIR_MASK; 00176: if (!address || (address >= end)) 00177: goto out; 00178: continue; 00179: } 00180: if (pgd_none(*dst_pgd)) { 00181: if (!pmd_alloc(dst_pgd, 0)) 00182: goto nomem; 00183: } 00184: 00185: src_pmd = pmd_offset(src_pgd, address); 00186: dst_pmd = pmd_offset(dst_pgd, address); 00187: 00188: do { 00189: pte_t * src_pte, * dst_pte; 00190: 00191: /* copy_pte_range */ 00192: 00193: if (pmd_none(*src_pmd)) 00194: goto skip_copy_pte_range; 00195: if (pmd_bad(*src_pmd)) { 00196: pmd_ERROR(*src_pmd); 00197: pmd_clear(src_pmd); 00198: skip_copy_pte_range: address = (address + PMD_SIZE) & PMD_MASK; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 286 页,共 1481 页 00199: if (address >= end) 00200: goto out; 00201: goto cont_copy_pmd_range; 00202: } 00203: if (pmd_none(*dst_pmd)) { 00204: if (!pte_alloc(dst_pmd, 0)) 00205: goto nomem; 00206: } 00207: 00208: src_pte = pte_offset(src_pmd, address); 00209: dst_pte = pte_offset(dst_pmd, address); 00210: 00211: do { 00212: pte_t pte = *src_pte; 00213: struct page *ptepage; 00214: 00215: /* copy_one_pte */ 00216: 00217: if (pte_none(pte)) 00218: goto cont_copy_pte_range_noset; 00219: if (!pte_present(pte)) { 00220: swap_duplicate(pte_to_swp_entry(pte)); 00221: goto cont_copy_pte_range; 00222: } 00223: ptepage = pte_page(pte); 00224: if ((!VALID_PAGE(ptepage)) || 00225: PageReserved(ptepage)) 00226: goto cont_copy_pte_range; 00227: 00228: /* If it's a COW mapping, write protect it both in the parent and the child */ 00229: if (cow) { 00230: ptep_set_wrprotect(src_pte); 00231: pte = *src_pte; 00232: } 00233: 00234: /* If it's a shared mapping, mark it clean in the child */ 00235: if (vma->vm_flags & VM_SHARED) 00236: pte = pte_mkclean(pte); 00237: pte = pte_mkold(pte); 00238: get_page(ptepage); 00239: 00240: cont_copy_pte_range: set_pte(dst_pte, pte); 00241: cont_copy_pte_range_noset: address += PAGE_SIZE; 00242: if (address >= end) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 287 页,共 1481 页 00243: goto out; 00244: src_pte++; 00245: dst_pte++; 00246: } while ((unsigned long)src_pte & PTE_TABLE_MASK); 00247: 00248: cont_copy_pmd_range: src_pmd++; 00249: dst_pmd++; 00250: } while ((unsigned long)src_pmd & PMD_TABLE_MASK); 00251: } 00252: out: 00253: return 0; 00254: 00255: nomem: 00256: return -ENOMEM; 00257: } 代码中 163 行的 for 循环是对页面目录项的循环,188 行的 do 循环是对中间目录项的循环,211 的 do 循环则是对页面表项的循环。我们把注意力集中在 211~246 行对页面表项的 do-while 循环。 循环中检查父进程一个页面表中的每个表项,根据表项的内容决定具体的操作。而表项的内容,则 无非有下面这么一些可能: 1. 表项的内容为全 0,所以 pte_none()返回 1。说明该页面的映射尚未建立,或者说是个“空洞”, 因此不需要做任何事。 2. 表项的最低位,即_PAGE_PRESENT 标志位为 0,所以 pte_present()返回 1。说明映射已建立, 但是该页面目前不在内存中,已经被调出到交换设备上。此时表项的内容指明“盘上页面”的 地点,而现在该盘上页面多了一个“用户”,所以要通过 swap_duplicate()递增它的共享计数。 然后,就转到 cont_copy_pte_range 将此表项复制到子进程的页面表中。 3. 映射已建立,但是物理页面不是一个有效的内存页面,所以 VALID_PAGE()返回 0。读者可以 回顾一下,我们以前讲过有些物理页面在外设接口卡上,相应的地址称为“总线地址”,而并不 是内存页面。这样的页面、以及虽是内存页面但有内核保留的页面,是不属于页面换入/换出机 制管辖的,实际上也不消耗动态分配的内存页面,所有也转到 cont_copy_pte_range 将此表项复 制到子进程的页面表中。 4. 需要从父进程复制的可写页面。本来,此时应该分配一个空闲的内存页面,再从父进程的页面 把内容复制出来,并为之建立映射。显然,这个操作的代价是不小的。然而,对这么辛辛苦苦 复制下来的页面,子进程是否一定会用呢?特别是会有写访问呢?如果只是读访问,则只要父 进程从此不再写这个页面,就完全可以通过固执指针来共享这个页面,那不知要省事多少了。 所以,Linux 内核采用了一种称为“copy on write”的计数,先通过复制页面表暂时共享这个页 面,到子进程(或父进程)真的要写这个页面时再来分配页面和复制。代码中的局部变量 cow 是在前面 158 行定义的,便两面 cow 是“copy on write”的缩写。只要一个虚存区间的性质是 可写(VM_MAYWRITE 为 1)而又不是共享(VM_SHARED 为 0),就属于 copy_on_write 区 间。实际上,对于绝大多数的可写虚存区间,cow 都是 1。在通过复制页面表项暂时共享一个 页面表项时要做两件重要的事情,首先要在 230 和 231 行将父进程的页面表项改成写保护,然 后在 236 行把已经改成写保护的表项设置到子进程页面表中。这样一来,相应的页面在两个进 程中都变成了“只读”了,当不管是父进程或是子进程企图写入该页面时,都会引起一次页面 异常。而页面异常处理程序对此的反应则是另行分配一个物理页面,并把内容真正地“复制” 到新的物理页面中,让父、子进程各自拥有自己的物理页面,然后将两个页面表中相应的表项 改成可写。所以,Linux 内核之所以可以很迅速地“复制”一个进程,完全依赖于“copy on write”Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 288 页,共 1481 页 (否则,在 fork()一个进程时就得要复制每一个物理页面了)。可是,copy_on_write 只有在父、 子进程各自拥有自己的页面表项时才能实现。当 CLONE_VM 标志位为 1,因而父、子进程通 过指针共享用户空间时,copy_on_write 就用不上了。此时,父、子进程是在真正意义上共享用 户空间,父进程写入其用户空间的内容同时也“写入”子进程的用户空间。 5. 父进程的只读页面。这种页面本来就不需要复制。因而可以复制页面表项共享物理页面。 可见,名为 copy_page_range(),实际上却连一个页面也没有真正地“复制”,这就是 Linux 内核能够 很迅速地 fork()或 clone()一个进程的秘密。 回到 copy_mm()的代码中。函数 copy_segments()处理的是进程可能具有的局部段描述表 LDT。我们 在第 2 章中讲过,只有在 VM86 模式中运行的进程才会有 LDT。虽然我们不关心 VM86 模式,但是有兴 趣的读者也不妨自己看看它是怎样复制的。Copy_segments()的代码在 arch/i386/kernel/process.c 中: [sys_fork() > do_fork() > copy_mm() >copy_segments()] 00499: /* 00500: * we do not have to muck with descriptors here, that is 00501: * done in switch_mm() as needed. 00502: */ 00503: void copy_segments(struct task_struct *p, struct mm_struct *new_mm) 00504: { 00505: struct mm_struct * old_mm; 00506: void *old_ldt, *ldt; 00507: 00508: ldt = NULL; 00509: old_mm = current->mm; 00510: if (old_mm && (old_ldt = old_mm->context.segments) != NULL) { 00511: /* 00512: * Completely new LDT, we initialize it from the parent: 00513: */ 00514: ldt = vmalloc(LDT_ENTRIES*LDT_ENTRY_SIZE); 00515: if (!ldt) 00516: printk(KERN_WARNING "ldt allocation failed\n"); 00517: else 00518: memcpy(ldt, old_ldt, LDT_ENTRIES*LDT_ENTRY_SIZE); 00519: } 00520: new_mm->context.segments = ldt; 00521: } 回到 copy_mm()的代码。对于 i386 CPU 来说,copy_mm()中 39 行处的 init_new_context()是个空语句。 当 CPU 从 copy_mm()回到 do_fork()中时,所有需要有条件复制的资源都已经处理完了。读者不妨回 顾一下,当系统调用 fork()通过 sys_fork()进入 do_fork()时,其 clone_flags 为 SIGCHLD,也就是说所有 的标志位均为 0,所以 copy_files()、copy_fs()、copy_sighand()以及 copy_mm()全部真正执行了,这四项 资源全都复制了。而当 vfork()经过 sys_vfork()进入 do_fork()时,则其 clone_flags 为 CLONE_VFORK | CLONE_VM | SIGCHLD,所以只执行了 copy_files()、copy_fs()以及 copy_sighand();而 copy_mm(),则 因标志位 CLONE_VM 为 1,只是通过指针共享其父进程的 mm_struct,并没有一份自己的副本。这也就 是说,经 vfork()复制的是个线程,只能靠共享其父进程的存储空间度日,包括用户空间堆栈在内。至于 __clone(),则取决于调用时的参数。当然,最终还得取决于父进程具有什么资源,要是父进程没有打开 的文件,那么即使执行了 copy_files(),也还是空的。 回到do_fork()的代码中。前面已通过alloc_task_struct()分配了两个连续的页面,其低端用作task_structLinux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 289 页,共 1481 页 结构,已经基本上复制好了;而用作系统空间堆栈的高端,却还没有复制。现在就由 copy_thread()来做 这件事。这个函数的代码在 arch/i386/kernel/process.c 中: [sys_fork() > do_fork() > copy_thread()] 00523: /* 00524: * Save a segment. 00525: */ 00526: #define savesegment(seg,value) \ 00527: asm volatile("movl %%" #seg ",%0":"=m" (*(int *)&(value))) 00528: 00529: int copy_thread(int nr, unsigned long clone_flags, unsigned long esp, 00530: unsigned long unused, 00531: struct task_struct * p, struct pt_regs * regs) 00532: { 00533: struct pt_regs * childregs; 00534: 00535: childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) - 1; 00536: struct_cpy(childregs, regs); 00537: childregs->eax = 0; 00538: childregs->esp = esp; 00539: 00540: p->thread.esp = (unsigned long) childregs; 00541: p->thread.esp0 = (unsigned long) (childregs+1); 00542: 00543: p->thread.eip = (unsigned long) ret_from_fork; 00544: 00545: savesegment(fs,p->thread.fs); 00546: savesegment(gs,p->thread.gs); 00547: 00548: unlazy_fpu(current); 00549: struct_cpy(&p->thread.i387, ¤t->thread.i387); 00550: 00551: return 0; 00552: } 名为 copy_thread(),实际上却只是复制父进程的系统空间堆栈。堆栈中的内容说明了父进程从通过 系统调用进入系统空间开始到进入 copy_thread()的来历,子进程将要循相同的路线返回,所以要把它复 制给子进程。但是,如果子进程的系统空间堆栈与父进程的完全相同,那返回以后就无从区分谁是子进 程了,所以复制以后还要略作调整。这是一段很有趣的程序,我们先来看 535 行。在第 3 章中,读者已 经看到当一个进程因系统调用或中断而进入内核时,其系统空间堆栈的顶部保存着 CPU 进入内核前夕各 个寄存器的内容,并形成一个 pt_regs 数据结构。这里 535 行中的 p 为子进程的 task_struct 指针,指向两 个连续物理页面的起始地址;而 THREAD_SIZE + (unsigned long)p 则指向这两个页面的顶端。将其变换 成 struct pt_regs*,再从中减 1,就指向了子进程系统空间堆栈中的 pt_regs 结构,如图 4.3 所示。 Linux 内核源代码情景分析 pt_regs task_struct 系统空间堆栈 P THREAD_SIZE 页面2 页面1 (THREAD_SIZE + (unsigned long)P) ((struct pt_regs*)(THREAD_SIZE + (unsigned long)P)-1) 图4.3 子进程系统空间堆栈示意图 得到了指向子进程系统空间堆栈中 pt_regs 结构的指针 childregs 以后,就先将当前进程系统空间堆 栈中的 pt_regs 结构复制过去,再来做少量的调整。什么样的调整呢?首先,将该结构中的 eax 置成 0。 当子进程接受调度而“恢复”运行,从系统调用“返回”时,这就是返回值。如前所述,子进程的返回 值为 0。其次,还要将结构中的 esp 置成这里的参数 esp,它决定了进程在用户空间的堆栈位置。在__clone() 调用中,这个参数是由调用这给定的。而在 fork()和 vfork()中,则来自调用 do_fork()前夕的 regs.esp,所 以实际上并没有改变,还是指向父进程原来在用户空间的堆栈。 在进程的 task_struct 结构中有个重要的成分 thread,它本身是一个数据结构 thread_struct,里面记录 着进程在切换时的(系统空间)堆栈指针、取指令地址(也就是“返回地址”)等关键性的信息。在复制 task_struct 数据结构的时候,这些信息也原封不动地复制了过来。可是,子进程有自己的系统空间堆栈, 所以也要相应加以调整。具体地说,540 行将 p->thread.esp 设置成子进程系统空间堆栈中 pt_regs 结构的 起始地址,就好像这个子进程以前曾经运行过,而在进入内核以后正要返回用户空间时被切换了一样。 而 p->thread.esp0 则应该指向子进程的系统堆栈的顶端。当一个进程被调度运行时,内核会将这个变量的 值写入 TSS 的 esp0 字段,表示当这个进程进入 0 级运行时其堆栈的位置。此外,p->thread.eip 的值表示 当进程下一次被切换进入运行时的切入点,类似于函数调用或中断的返回地址。将此地址设置成 ret_from_fork,使创建的子进程在首次被调度运行时就从那儿开始,这一点以后在阅读有关进程切换的 代码时还要讲到。545 行和 546 行的 savesegment 是个宏操作,其定义就在 526 行。所以,545 行在 gcc 预处理以后就会变成: asm volatile (“movl %%fs, %0” : ” = m ” (*(int*)&p->thread.fs)) 也就是把当前的段寄存器 fs 的值保存在 p->thread.fs 中。546 行与此类似。548 行和 549 行是为 i387 浮点处理器而设的,那就不是我们所关心的了。 回到 do_fork(),再往下看: [sys_fork() > do_fork()] 00656: /* Our parent execution domain becomes current domain 00657: These must match for thread signalling to apply */ 00658: 00659: p->parent_exec_id = p->self_exec_id; 00660: 00661: /* ok, now we should be set up.. */ 00662: p->swappable = 1; 00663: p->exit_signal = clone_flags & CSIGNAL; 00664: p->pdeath_signal = 0; 00665: 00666: /* 00667: * "share" dynamic priority between parent and child, thus the 2006-12-31 版权所有,侵权必究 第 290 页,共 1481 页 00668: * total amount of dynamic priorities in the system doesnt change, Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 291 页,共 1481 页 00669: * more scheduling fairness. This is only important in the first 00670: * timeslice, on the long run the scheduling behaviour is unchanged. 00671: */ 00672: p->counter = (current->counter + 1) >> 1; 00673: current->counter >>= 1; 00674: if (!current->counter) 00675: current->need_resched = 1; 00676: 00677: /* 00678: * Ok, add it to the run-queues and make it 00679: * visible to the rest of the system. 00680: * 00681: * Let it rip! 00682: */ 00683: retval = p->pid; 00684: p->tgid = retval; 00685: INIT_LIST_HEAD(&p->thread_group); 00686: write_lock_irq(&tasklist_lock); 00687: if (clone_flags & CLONE_THREAD) { 00688: p->tgid = current->tgid; 00689: list_add(&p->thread_group, ¤t->thread_group); 00690: } 00691: SET_LINKS(p); 00692: hash_pid(p); 00693: nr_threads++; 00694: write_unlock_irq(&tasklist_lock); 00695: 00696: if (p->ptrace & PT_PTRACED) 00697: send_sig(SIGSTOP, p, 1); 00698: 00699: wake_up_process(p); /* do this last */ 00700: ++total_forks; 00701: 00702: fork_out: 00703: if ((clone_flags & CLONE_VFORK) && (retval > 0)) 00704: down(&sem); 00705: return retval; 00706: 代码中的 parent_exec_id 表示父进程的执行域,self_exec_id 为本进程的执行域,swappable 表示本进 程的存储页面可以被换出,exit_signal 为本进程执行 exit()时应向父进程发出的信号,pdeath_signal 为要 求父进程在执行 exit()时向本进程发出的信号。此外,task_struct 结构中 counter 字段的值就是进程的运行 时间配额,这里将父进程的时间配额分成两半,让父、子进程各自有原值的一半。如果创建的是线程, 则还要通过 task_struct 结构中的队列头 thread_group 与父进程链接起来,形成一个“线程组”。接着,就 要让子进程进入它的关系网了。先通过 SET_LINKS(p)将子进程的 task_struct 结构链入内核的进程队列, 然后又通过 hash_pid()将其链入按其 pid 计算得的杂凑队列。有关这些队列的详情可参看“进程”以及“进Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 292 页,共 1481 页 程的调度与切换”两节中的有关叙述。最后,通过 wake_up_process()将子进程“唤醒”,也就是将其挂 入可执行进程队列等待调度。有关详情可参看“进程的睡眠与唤醒”一节。 至此,新进程的创建已经完成了,并且已经挂入了可运行进程的队列接受调度。子进程与父进程在 用户空间中具有相同的返回地址,然后才会因用户空间中程序的安排而分开。同时,由于当父进程(当 前进程)从系统调用返回的前夕可能会接受调度,所以,到底谁会先返回到用户空间是不确定的。不过, 一般而言,由于父、子进程适用相同的调度政策,而父进程在可执行进程队列中排在子进程前面,所有 父进程先运行的可能较大。 还有一种特殊情况要考虑。当调用 do_fork()的参数中 CLONE_VFORK 标志位为 1 时,一定要保证 让子进程先运行,一直到子进程通过系统调用 execv()执行一个新的可执行程序或者通过系统调用 exit() 退出系统时,才可以恢复父进程的运行。为什么呢?这要从用户空间的复制或共享这个问题说起。前面 读者已经看到,在创建子进程时,对于父进程的用户空间可以通过复制父进程的 mm_struct 及其下属的 各个 vm_area_struct 数据结构,再加上父进程的页面目录和页面表来继承;也可以简单地复制父进程的 task_struct 结构中指向其 mm_struct 结构的指针来共享,具体取决于 CLONE_VM 标志位的值。当 CLONE_VM 标志位为 1,因而父、子进程通过指针共享用户空间时,父、子进程是在真正意义上共享用 户空间,父进程写入其用户空间的内容同时也“写入”子进程的用户空间,反之亦然。如果说,在这种 情况下父、子进程各自对其数据区的写入可能会引起问题的话,那么对堆栈区的写入可就是致命的了。 而每次对子程序的调用都是对堆栈区的写入!由此可见,在这样的情况下决不能让两个进程都回到用户 空间并发地运行;否则,必然时两个进程最终都乱来一气或者因非法越界访问而死亡。解决定办法只能 是“扣留”其中一个进程,而只让一个进程回到用户空间,直到两个进程不再共享它们的用户空间或其 中一个进程(必然是回到用户空间运行的那个进程)消亡为止。 所以,do_fork()中的 703 行和 704 行在 CLONE_VFORK 标志位为 1 并且 fork 子进程成功的情况下, 通过让当前进程(父进程)在一个信号量上执行一次 down()操作,以达到扣留父进程的目的。我们来看 看具体时怎样实现的。 首先,信号量 sem 是在函数开头时的 560 行定义的一个局部变量(名曰 DECLARE,实际上为之分 配了空间): [sys_fork() > do_fork()] 00560: DECLARE_MUTEX_LOCKED(sem); 这儿 DECLARE_MUTEX_LOCKED 是在 include/asm-i386/semaphore.h 中定义的: 00070: #define DECLARE_MUTEX(name) __DECLARE_SEMAPHORE_GENERIC(name,1) 00071: #define DECLARE_MUTEX_LOCKED(name) __DECLARE_SEMAPHORE_GENERIC(name,0) 将 DECLARE_MUTEX_LOCKED 与 DECLARE_MUTEX 作一比较,可以看出正常情况下信号量中 资源的数量为 1,而现在这个信号量中资源的数量为 0。当资源数量为 1 时,第一个执行 down()操作的 进程进入临界区,而使资源数量变成了 0,以后执行 down()操作的进程便会因为资源数量为 0 而被拒之 门外进入睡眠,知道第一个进程归还资源离开临界区时才被唤醒。而现在这个信号量的资源从一开始就 是 0,所以第一个对此信号量执行 down()操作的进程就会进入睡眠,一直要到某个进程往这个信号量中 投入资源,也就是执行一次 up()操作才会被唤醒。 那么,谁来投入资源呢?在“系统调用 execv()”一节中读者将会看到,子进程在通过 execv()执行一 个新的可执行程序时会做这件事。此外,子进程在通过 exit()退出系统时也会做这件事。这里还要指出, 这个信号量是 do_fork()的一个局部变量,所以在父进程的系统空间堆栈中,而子进程在其 task_struct 结 构中有指向这个信号量的指针(即 vfork_sem,见 do_fork()的第 554 行和 560 行),既然父进程一直要睡 眠到子进程使用这个信号量以后,信号量所在的空间就不会受到打扰。还应指出,CLONE_VM 要与 CLONE_VFORK 结合使用,否则就会发生前述的问题,除非在用户程序中采取了特殊的预防措施。 不管怎样,子进程的创建终于完成了,让我们祝福这新的生命!可是,如果子进程只具有与父进程 相同的可执行程序和数据,只是父进程的“影子”,那又有什么意义呢?子进程必须走自己的路,这就是 下一节“系统调用 execv()”所要讲述的内容了。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 293 页,共 1481 页 4.4. 系统调用execve() 读者在前一节中已经看到,进程通常是按其父进程的原样复制出来的,在多数情况下,如果复制出 来的子进程不能与父进程分道扬镳,“走自己的路”,那就没有多大意义。所以,执行一个新的可执行程 序是进程生命历程中关键性的一步。Linux 为此提供了一个系统调用 execv(),而在 C 语言的程序库中则 又在此基础上向应用程序提供了一整套的库函数,包括 execl()、execlp()、execle()、execv()和 execvp()。 此外,还有库函数 system(),也与 execv()有关,不过 system()是 fork()、execve()、wait4()的组合。我们 已经在本章第 2 节介绍过应用程序怎样调用 execve(),现在我们就来介绍 execve()的实现。 系统调用 execve()内核入口是 sys_execve(),代码见 arch/i386/kernel/process.c: 00722: /* 00723: * sys_execve() executes a new program. 00724: */ 00725: asmlinkage int sys_execve(struct pt_regs regs) 00726: { 00727: int error; 00728: char * filename; 00729: 00730: filename = getname((char *) regs.ebx); 00731: error = PTR_ERR(filename); 00732: if (IS_ERR(filename)) 00733: goto out; 00734: error = do_execve(filename, (char **) regs.ecx, (char **) regs.edx, ®s); 00735: if (error == 0) 00736: current->ptrace &= ~PT_DTRACE; 00737: putname(filename); 00738: out: 00739: return error; 00740: } 以前讲过,系统调用进入内核时,regs.ebx 中的内容为应用程序中调用相应库函数时的第一个参数。 在本章第 2 节所举的例子中,这个参数为指向字符串“/bin/echo”的指针。现在,指针存放在 regs.ebx 中,但字符串本身还在用户空间中,所以 730 行的 getname()要把这个字符串从用户空间拷贝到系统空间, 在系统空间中建立起一个副本。让我们看看具体是怎么做的。函数 getname()的代码在 fs/namei.c 中: [sys_execve() > getname()] 00129: char * getname(const char * filename) 00130: { 00131: char *tmp, *result; 00132: 00133: result = ERR_PTR(-ENOMEM); 00134: tmp = __getname(); 00135: if (tmp) { 00136: int retval = do_getname(filename, tmp); 00137: 00138: result = tmp; 00139: if (retval < 0) { 00140: putname(tmp); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 294 页,共 1481 页 00141: result = ERR_PTR(retval); 00142: } 00143: } 00144: return result; 00145: } 先通过__getname()分配一个物理页面作为缓冲区,然后调用 do_getname()从用户空间拷贝字符串。 那么,为什么要专门为此分配一个物理页面作为缓冲区呢?首先,这个字符串有可能相当长,因为这是 一个绝对路径名。其次,我们以前讲过,进程系统空间堆栈的大小是大约 7KB,不能滥用,不宜在 getname() 中定义一个局部的 4KB 的字符数组(注意,局部变量所占据的空间是在堆栈中分配的)。函数 do_getname() 的代码也在文件 fs/namei.c 中: [sys_execve() > getname() > do_getname()] 00102: /* In order to reduce some races, while at the same time doing additional 00103: * checking and hopefully speeding things up, we copy filenames to the 00104: * kernel data space before using them.. 00105: * 00106: * POSIX.1 2.4: an empty pathname is invalid (ENOENT). 00107: */ 00108: static inline int do_getname(const char *filename, char *page) 00109: { 00110: int retval; 00111: unsigned long len = PATH_MAX + 1; 00112: 00113: if ((unsigned long) filename >= TASK_SIZE) { 00114: if (!segment_eq(get_fs(), KERNEL_DS)) 00115: return -EFAULT; 00116: } else if (TASK_SIZE - (unsigned long) filename < PAGE_SIZE) 00117: len = TASK_SIZE - (unsigned long) filename; 00118: 00119: retval = strncpy_from_user((char *)page, filename, len); 00120: if (retval > 0) { 00121: if (retval < len) 00122: return 0; 00123: return -ENAMETOOLONG; 00124: } else if (!retval) 00125: retval = -ENOENT; 00126: return retval; 00127: } 如果指针 filename 的值大于等于 TASK_SIZE,就 表 示 filename 实际上在系统空间中。读者应该还记 得 TASK_SIZE 的值是 3GB 。具体的拷贝是通过 strncpy_from_user() 进行的,代码见 arch/i386/lib/usercopy.c: [sys_execve() > getname() > do_getname() > strncpy_from_user()] 00100: long 00101: strncpy_from_user(char *dst, const char *src, long count) 00102: { 00103: long res = -EFAULT; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 295 页,共 1481 页 00104: if (access_ok(VERIFY_READ, src, 1)) 00105: __do_strncpy_from_user(dst, src, count, res); 00106: return res; 00107: } 这个函数的主体 strncpy_from_user()是一个宏操作,也在同一个源文件 usercopy.c 中,与第 3 章中介 绍过的_generic_copy_from_user()很相似,读者可以自行对照阅读。 在系统空间中建立起一份可执行文件的路径名副本以后,sys_execve()就调用 do_execve(),以完成其 主体部分的工作。当然,完成以后还要通过 putname()将所分配的物理页面释放。函数 do_execve()的代 码在 fs/exec.c 中,我们逐段地往下看: [sys_execve() > do_execve()] 00835: /* 00836: * sys_execve() executes a new program. 00837: */ 00838: int do_execve(char * filename, char ** argv, char ** envp, struct pt_regs * regs) 00839: { 00840: struct linux_binprm bprm; 00841: struct file *file; 00842: int retval; 00843: int i; 00844: 00845: file = open_exec(filename); 00846: 00847: retval = PTR_ERR(file); 00848: if (IS_ERR(file)) 00849: return retval; 00850: 显然,先要将给定的可执行程序文件找到并打开,open_exec()就是为此而调用的,其代码也在 exec.c 中,读者可以结合“文件系统”一章中有关打开文件操作的内容,特别是 path_walk()的代码自行阅读。 假定目标文件已经打开,下一步就要从文件中装入可执行程序了。内核中为可执行程序的装入定义 了一个数据结构 linux_binprm,以便将运行一个可执行文件时所需的信息组织在一起,这是在 include/linux/binfmts.h 定义的: 00019: /* 00020: * This structure is used to hold the arguments that are used when loading binaries. 00021: */ 00022: struct linux_binprm{ 00023: char buf[BINPRM_BUF_SIZE]; 00024: struct page *page[MAX_ARG_PAGES]; 00025: unsigned long p; /* current top of mem */ 00026: int sh_bang; 00027: struct file * file; 00028: int e_uid, e_gid; 00029: kernel_cap_t cap_inheritable, cap_permitted, cap_effective; 00030: int argc, envc; 00031: char * filename; /* Name of binary */ 00032: unsigned long loader, exec; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 296 页,共 1481 页 00033: }; 其中各个成分的作用读了下面的代码就会清楚。我们继续在 do_execve()中往下看: [sys_execve() > do_execve()] 00851: bprm.p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *); 00852: memset(bprm.page, 0, MAX_ARG_PAGES*sizeof(bprm.page[0])); 00853: 00854: bprm.file = file; 00855: bprm.filename = filename; 00856: bprm.sh_bang = 0; 00857: bprm.loader = 0; 00858: bprm.exec = 0; 00859: if ((bprm.argc = count(argv, bprm.p / sizeof(void *))) < 0) { 00860: allow_write_access(file); 00861: fput(file); 00862: return bprm.argc; 00863: } 00864: 00865: if ((bprm.envc = count(envp, bprm.p / sizeof(void *))) < 0) { 00866: allow_write_access(file); 00867: fput(file); 00868: return bprm.envc; 00869: } 00870: 00871: retval = prepare_binprm(&bprm); 00872: if (retval < 0) 00873: goto out; 00874: 00875: retval = copy_strings_kernel(1, &bprm.filename, &bprm); 00876: if (retval < 0) 00877: goto out; 00878: 00879: bprm.exec = bprm.p; 00880: retval = copy_strings(bprm.envc, envp, &bprm); 00881: if (retval < 0) 00882: goto out; 00883: 00884: retval = copy_strings(bprm.argc, argv, &bprm); 00885: if (retval < 0) 00886: goto out; 00887: 代码中的 linux_binprm 数据结构 bprm 是个局部量。函数 open_exec()返回一个 file 结构指针,代表 着读入可执行文件的上下文,所以将其保存在数据结构 bprm 中。变量 bprm.sh_bang 的值说明可执行文 件的性质,当可执行文件是一个 Shell 过程(Shell Script,用 Shell 语言编写的命令文件,由 Shell 解释执 行)时设置为 1。而现在还不知道,所以暂且将其置为 0,也就是先假定为二进制文件。数据结构中的其 他两个变量也暂时设置成 0。接着就处理可执行文件的参数和环境变量。 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 297 页,共 1481 页 与可执行文件路径名的处理方法一样,每个参数的最大长度也定为一个物理页面,所以 bprm 中有 一个页面指针数组,数组的大小为允许的最大参数个数 MAX_ARG_PAGES,目前这个常数定义为 32。 前面已通过 memset()将这个指针数组初始化成全 0。现在将 bprm.p 设置成这些页面的总和减去一个指针 的大小,因为第 0 个参数也就是 argv[0]时可执行程序本身的路径名。函数 count()是在 exec.c 中定义的, 这里用它对字符串指针数组 argv[]中参数的个数进行计数,而 bprm.p/sizeof(void *)表示允许的最大值。 同样,对作为参数传过来的环境变量也要通过 count()计数。注意这里的数组 argv[]和 envp[]是在用户空 间而不在系统空间,所以计数的操作并不那么简单。函数 count()的代码在 fs/exec.c 中,它本身的代码很 简单,但是引用的宏定义 get_user()却颇有些挑战性,值得一读。它也与第 3 章中介绍过的 _generic_copy_from_user()相似,我们把它留给读者作为练习。有关的代码在 include/asm-i386/uaccess.h 和 arch/i386/lib/getuser.S 中,调用的路径为[count() > get_user() > getuser() > _get_user_4()]。如果 count() 失败,即返回负值,则要对目标文件执行一次 allow_write_access()。这个函数是与 deny_write_access() 配对使用的,目的在于防止其他进程(可能在另一个 CPU 上运行)在读入可执行文件期间通过内存映射 改变它的内容(详见“文件系统”以及系统调用 mmap())。与其配对的 deny_write_access()是在打开可执 行文件时在 open_exec()中调用的。 完成了对参数和环境变量的计数以后,do_execve()又调用 prepare_binprm(),进一步做数据结构 bprm 的准备工作,从可执行文件中读入开头的 128 个字节到 linux_binprm 结构 bprm 中的缓冲区。当然,在 读之前还要先检验当前进程是否有这个权利,以及该文件是否有可执行属性。如果可执行文件具有“set uid”特性则要作相应的设置。这个函数的代码也在 exec.c 中。由于涉及文件操作的细节,我们建议读者 在学习了“文件系统”以后再回过来自行阅读。此处先说明为什么只是先读 128 个字节。这是因为,不 管目标文件是 elf 格式还是 a.out 格式,或者别的格式,在开头 128 个字节中都包括了关于可执行文件属 性的必要而充分的信息。等一下读者就会看到这些信息的用途。 最后的准备工作就是把执行的参数,也就是 argv[],以及运行的环境,也就是 envp[],从用户空间 拷贝到数据结构 bprm 中。其中的第 1 个参数 argv[0]就是可执行文件的路径名,已经在 bprm.filename 中 了,所以用 copy_strings_kernel()从系统空间中拷贝,其他的就要用 copy_string()从用户空间拷贝。 至此,所有的准备工作都已经完成,所有必要的信息都已经搜集到了 linux_binprm 结构 bprm 中, 接下来就要装入并运行目标程序了(exec.c): [sys_execve() > do_execve()] 00888: retval = search_binary_handler(&bprm,regs); 00889: if (retval >= 0) 00890: /* execve success */ 00891: return retval; 00892: 00893: out: 00894: /* Something went wrong, return the inode and free the argument pages*/ 00895: allow_write_access(bprm.file); 00896: if (bprm.file) 00897: fput(bprm.file); 00898: 00899: for (i = 0 ; i < MAX_ARG_PAGES ; i++) { 00900: struct page * page = bprm.page[i]; 00901: if (page) 00902: __free_page(page); 00903: } 00904: 00905: return retval; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 298 页,共 1481 页 00906: } 显然,这里的关键是 search_binary_handler()。在深入到这个函数内部之前,先介绍一个大概。内核 中有一个队列,叫 formats,挂在此队列中的成员是代表着各种可执行文件格式的“代理人”,每个成员 只认识并且处理一种特定格式的可执行文件的运行。在前面的准备阶段中,已经从可执行文件头部读入 了 128 个字节存放在 bprm 的缓冲区,而且运行所需的参数和环境变量也已经收集在 bprm 中。现在就由 formats 队列中的成员逐个来认领,谁要是辨认到了它所代表的可执行文件格式,运行的事就交给它。要 是都不认识呢?那就根据文件头部的信息再找找看,是否有为此种格式设计,但是作为可动态安装模块 实现的“代理人”存在于文件系统中。如果有的话就把这模块安装进来并且将其挂入到 formats 队列中, 然后让 formats 队列中的各个“代理人”再来试一次。 函数 search_binary_handler()的代码也在 exec.c 中,其中有一段是专门针对 alpha 处理器的条件编译, 在下列的代码中跳过了这段条件编译语句: [sys_execve() > do_execve() > search_binary_handler()] 00747: /* 00748: * cycle the list of binary formats handler, until one recognizes the image 00749: */ 00750: int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs) 00751: { 00752: int try,retval=0; 00753: struct linux_binfmt *fmt; 00754: #ifdef __alpha__ …………………… 00785: #endif 00786: for (try=0; try<2; try++) { 00787: read_lock(&binfmt_lock); 00788: for (fmt = formats ; fmt ; fmt = fmt->next) { 00789: int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary; 00790: if (!fn) 00791: continue; 00792: if (!try_inc_mod_count(fmt->module)) 00793: continue; 00794: read_unlock(&binfmt_lock); 00795: retval = fn(bprm, regs); 00796: if (retval >= 0) { 00797: put_binfmt(fmt); 00798: allow_write_access(bprm->file); 00799: if (bprm->file) 00800: fput(bprm->file); 00801: bprm->file = NULL; 00802: current->did_exec = 1; 00803: return retval; 00804: } 00805: read_lock(&binfmt_lock); 00806: put_binfmt(fmt); 00807: if (retval != -ENOEXEC) 00808: break; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 299 页,共 1481 页 00809: if (!bprm->file) { 00810: read_unlock(&binfmt_lock); 00811: return retval; 00812: } 00813: } 00814: read_unlock(&binfmt_lock); 00815: if (retval != -ENOEXEC) { 00816: break; 00817: #ifdef CONFIG_KMOD 00818: }else{ 00819: #define printable(c) (((c)=='\t') || ((c)=='\n') || (0x20<=(c) && (c)<=0x7e)) 00820: char modname[20]; 00821: if (printable(bprm->buf[0]) && 00822: printable(bprm->buf[1]) && 00823: printable(bprm->buf[2]) && 00824: printable(bprm->buf[3])) 00825: break; /* -ENOEXEC */ 00826: sprintf(modname, "binfmt-%04x", *(unsigned short *)(&bprm->buf[2])); 00827: request_module(modname); 00828: #endif 00829: } 00830: } 00831: return retval; 00832: } 程序中有两层嵌套的 for 循环。内层是对 formats 队列中的每个成员循环,让队列中的成员逐个试试 它们的 load_binary()函数,看看能否对上号。如果对上了号,那就把目标文件装入并将其投入运行,再 返回一个正数或 0。当 CPU 从系统调用返回时,该目标文件的执行就真正开始了。否则,如果不能辨识, 或者在处理的过程中出了错,就返回一个负数。出错代码-ENOEXEC 表示只是对不上号,而没有发生其 他的错误,所以循环回去,让队列中的下一个成员再来试试。但是如果出了错而又并不是-ENOEXEC, 那就表示对上了号但出了其他的错,这就不用再让其他的成员来试了。 内层循环结束以后,如果失败的原因是-ENOEXEC,就说明队列中所有的成员都不认识目标文件的 格式。这时候,如果内核支持动态安装模块(取决于编译选择项 CONFIG_KMOD),就根据目标文件的 第 2 和第 3 个字节生成一个 binfmt 模块名,通过 request_module()试着将相应的模块装入(见本书“文件 系统”和“设备驱动”两章中的有关内容)。外层的 for 循环共进行了两次,正是为了安装模块以后再来 试一次。 能在Linux系统上运行的可执行程序的头几个字节,特别是开头4个字节,往往构成一个所谓的magic number,如果把它拆开成字节,则往往又是说明文件格式的字符。例如,elf 格式的可执行文件的头四个 字节为“0x7F”、“e”、“1”和“f”;而 java 的可执行文件头部四个字节则为“c”、“a”、“f”和“e”。如 果可执行文件为 Shell 过程或 perl 文件,即第一行的格式为#!/bin/sh 或#!/usr/bin/perl,此时第一个字符为 “#”,第二个字符为“!”,后面是相应解释程序的路径名。 数据结构 linux_binfmt 定义于 include/linux/binfmts.h 中,前面已经看到过了。结构中有三个函数指 针,load_binary 用来装入可执行程序,load_shlib 用来装入动态安装的公用库程序,而 core_dump 的作用 则不言自明。显然,这里最根本的是 load_binary。同时,如果搞不清楚具体的装载程序格式怎样工作, 就很难对 execve()、进而 Linux 进程的运行有深刻的理解。下面我们以 a.out 格式为例,讲述装入并启动 执行目标程序的过程。其实,a.out 格式的可执行文件已经渐渐被淘汰了,取而代之的是 elf 格式。但是,Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 300 页,共 1481 页 a.ou 格式要简单得多,并且方便我们通过它来讲述目标程序的装载和投入运行的过程,所以从篇幅考虑 我们选择了 a.out。读者在搞清楚了 a.out 格式的装载和投入运行过程以后,可以自行阅读有关 elf 格式的 相关代码。 4.4.1. a.out格式目标文件的装载和投入运行 与 a.out 格式可执行文件有关的代码都在 fs/binfmt_aout.c 中。先来看看 a.out 格式的 linux_binfmt 数 据结构,这个数据结构就是在 formats 队列中代表 a.out 格式的: 00038: static struct linux_binfmt aout_format = { 00039: NULL, THIS_MODULE, load_aout_binary, load_aout_library, aout_core_dump, PAGE_SIZE 00040: }; 读者可以将它与前面的数据结构的定义相对照。装载和投入运行 a.out 格式目标文件的函数为 load_aout_binary()。可以想像,这是个比较复杂的过程,函数也比较大。我们还是老办法,一段一段往 下看。其代码在 binfmt_aout.c 中: [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary()] 00249: /* 00250: * These are the functions used to load a.out style executables and shared 00251: * libraries. There is no binary dependent code anywhere else. 00252: */ 00253: 00254: static int load_aout_binary(struct linux_binprm * bprm, struct pt_regs * regs) 00255: { 00256: struct exec ex; 00257: unsigned long error; 00258: unsigned long fd_offset; 00259: unsigned long rlim; 00260: int retval; 00261: 00262: ex = *((struct exec *) bprm->buf); /* exec-header */ 00263: if ((N_MAGIC(ex) != ZMAGIC && N_MAGIC(ex) != OMAGIC && 00264: N_MAGIC(ex) != QMAGIC && N_MAGIC(ex) != NMAGIC) || 00265: N_TRSIZE(ex) || N_DRSIZE(ex) || 00266: bprm->file->f_dentry->d_inode->i_size < ex.a_text+ex.a_data+N_SYMSIZE(ex)+N_TXTOFF(ex)) { 00267: return -ENOEXEC; 00268: } 首先是检查目标文件的格式,看看是否对上号。所有 a.out 格式可执行文件(二进制代码)的开头都 应该是一个 exec 数据结构,这是在 include/asm-i386/a.out.h 中定义的: 00004: struct exec 00005: { 00006: unsigned long a_info; /* Use macros N_MAGIC, etc for access */ 00007: unsigned a_text; /* length of text, in bytes */ 00008: unsigned a_data; /* length of data, in bytes */ 00009: unsigned a_bss; /* length of uninitialized data area for file, in bytes */ 00010: unsigned a_syms; /* length of symbol table data in file, in bytes */ 00011: unsigned a_entry; /* start address */ Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 301 页,共 1481 页 00012: unsigned a_trsize; /* length of relocation info for text, in bytes */ 00013: unsigned a_drsize; /* length of relocation info for data, in bytes */ 00014: }; 00015: 00016: #define N_TRSIZE(a) ((a).a_trsize) 00017: #define N_DRSIZE(a) ((a).a_drsize) 00018: #define N_SYMSIZE(a) ((a).a_syms) 结构中的第一个无符号长整数 a_info 在逻辑上分成两部分:其高 16 位是一个代表目标 CPU 类型的 代码,对于 i386 CPU 这部分的值为 100(0x64);而 低 16 为就是 magic number。不过,a.out 文件的 magic number 并不像在有的格式中那样是可打印字符,而是表示某些属性的编码,一共有四种,即 ZMAGIC、 OMAGIC、QMAGIC 以及 NMAGIC,这是在 include/linux/a.out.h 中定义的: 00060: /* Code indicating object file or impure executable. */ 00061: #define OMAGIC 0407 00062: /* Code indicating pure executable. */ 00063: #define NMAGIC 0410 00064: /* Code indicating demand-paged executable. */ 00065: #define ZMAGIC 0413 00066: /* This indicates a demand-paged executable with the header in the text. 00067: The first page is unmapped to help trap NULL pointer references */ 00068: #define QMAGIC 0314 00069: 00070: /* Code indicating core file. */ 00071: #define CMAGIC 0421 如果 magic number 不符,或者 exec 结构中提供的信息与实际不符,那就不能认为这个目标文件是 a.out 格式的,所以返回-ENOEXEC。 继续在 binfmt_aout.c 中往下看: [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary()] 00270: fd_offset = N_TXTOFF(ex); 00271: 00272: /* Check initial limits. This avoids letting people circumvent 00273: * size limits imposed on them by creating programs with large 00274: * arrays in the data or bss. 00275: */ 00276: rlim = current->rlim[RLIMIT_DATA].rlim_cur; 00277: if (rlim >= RLIM_INFINITY) 00278: rlim = ~0; 00279: if (ex.a_data + ex.a_bss > rlim) 00280: return -ENOMEM; 00281: 00282: /* Flush all traces of the currently running executable */ 00283: retval = flush_old_exec(bprm); 00284: if (retval) 00285: return retval; 00286: 00287: /* OK, This is the point of no return */ Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 302 页,共 1481 页 各种 a.out 格式的文件因目标代码的特征不同,其正文的起始位置也就不同。为此提供了一个宏操作 N_TXTOFF(),以便根据代码的特征取得正文在目标文件中的起始位置,这是在 include/linux/a.out.h 中定 义的: 00080: #define _N_HDROFF(x) (1024 - sizeof (struct exec)) 00081: 00082: #if !defined (N_TXTOFF) 00083: #define N_TXTOFF(x) \ 00084: (N_MAGIC(x) == ZMAGIC ? _N_HDROFF((x)) + sizeof (struct exec) : \ 00085: (N_MAGIC(x) == QMAGIC ? 0 : sizeof (struct exec))) 00086: #endif 以前曾经讲过,每个进程的 task_struct 结构中有个数组 rlim,规定了该进程使用各种资源的限制, 其中也包括对用于数据的内存空间的限制。所以,目标文件所确定的 data 和 bss 两个“段”的总和不能 超出这个限制。 顺利通过了这些检验就表示具备了执行该目标文件的条件,所以就到了“与过去告别”的时候。这 种“告别过去”意味着放弃从父进程“继承”下来的全部用户空间,不管是通过复制还是通过共享继承 下来的。不过,下面读者会看到,这种告别也并非彻底的决裂。 函数 flush_old_exec()的代码也在 exec.c 中: [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary() > flush_old_exec()] 00523: int flush_old_exec(struct linux_binprm * bprm) 00524: { 00525: char * name; 00526: int i, ch, retval; 00527: struct signal_struct * oldsig; 00528: 00529: /* 00530: * Make sure we have a private signal table 00531: */ 00532: oldsig = current->sig; 00533: retval = make_private_signals(); 00534: if (retval) goto flush_failed; 00535: 00536: /* 00537: * Release all of the old mmap stuff 00538: */ 00539: retval = exec_mmap(); 00540: if (retval) goto mmap_failed; 00541: 00542: /* This is the point of no return */ 00543: release_old_signals(oldsig); 00544: 00545: current->sas_ss_sp = current->sas_ss_size = 0; 00546: 00547: if (current->euid == current->uid && current->egid == current->gid) 00548: current->dumpable = 1; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 303 页,共 1481 页 00549: name = bprm->filename; 00550: for (i=0; (ch = *(name++)) != '\0';) { 00551: if (ch == '/') 00552: i = 0; 00553: else 00554: if (i < 15) 00555: current->comm[i++] = ch; 00556: } 00557: current->comm[i] = '\0'; 00558: 00559: flush_thread(); 00560: 00561: de_thread(current); 00562: 00563: if (bprm->e_uid != current->euid || bprm->e_gid != current->egid || 00564: permission(bprm->file->f_dentry->d_inode,MAY_READ)) 00565: current->dumpable = 0; 00566: 00567: /* An exec changes our domain. We are no longer part of the thread 00568: group */ 00569: 00570: current->self_exec_id++; 00571: 00572: flush_signal_handlers(current); 00573: flush_old_files(current->files); 00574: 00575: return 0; 00576: 00577: mmap_failed: 00578: flush_failed: 00579: spin_lock_irq(¤t->sigmask_lock); 00580: if (current->sig != oldsig) 00581: kfree(current->sig); 00582: current->sig = oldsig; 00583: spin_unlock_irq(¤t->sigmask_lock); 00584: return retval; 00585: } 首先是进程的信号(软中断)处理表。我们讲过,一个进程的信号处理标就好像一个系统中的中断 向量表,虽然运用的层次不同,其概念是相似的。当子进程被创建出来时,父进程的信号处理表可能已 经复制过来,但也有可能只是把父进程的信号处理表指针复制了过来,而通过这指针来共享父进程的信 号处理表。现在,子进程最重要“自立门户”了,所以要看一下,如果还在共享父进程的信号处理表的 话,就要把它复制过来。正因为这样,make_private_signals()的代码与 do_fork()中调用的 copy_sighand() 基本相同。 [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary() >flush_old_exec() > make_private_signals()] Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 304 页,共 1481 页 00429: /* 00430: * This function makes sure the current process has its own signal table, 00431: * so that flush_signal_handlers can later reset the handlers without 00432: * disturbing other processes. (Other processes might share the signal 00433: * table via the CLONE_SIGNAL option to clone().) 00434: */ 00435: 00436: static inline int make_private_signals(void) 00437: { 00438: struct signal_struct * newsig; 00439: 00440: if (atomic_read(¤t->sig->count) <= 1) 00441: return 0; 00442: newsig = kmem_cache_alloc(sigact_cachep, GFP_KERNEL); 00443: if (newsig == NULL) 00444: return -ENOMEM; 00445: spin_lock_init(&newsig->siglock); 00446: atomic_set(&newsig->count, 1); 00447: memcpy(newsig->action, current->sig->action, sizeof(newsig->action)); 00448: spin_lock_irq(¤t->sigmask_lock); 00449: current->sig = newsig; 00450: spin_unlock_irq(¤t->sigmask_lock); 00451: return 0; 00452: } 读者也许要问:既然最终还是要把它复制过来,何不在当初一步就把它复制好了?这就是所谓“lazy computer”的概念:一件事情只有在非做不可时才做。虽然新创建的进程一般都会执行 execve(),“走自 己的路”,但这是没有保证的。如果创建的时线程那就不一定会执行 execve(),如果一律在创建时就复制 就可能造成浪费而且不符合要求。再说,检查一下是否还在与父进程共享信号处理表(通过检查共享计 数)所花费的代价是很小的。当然,如果子进程是通过 fork()创建出来的话(而不是 vfork()或__clone()), 那就一定都已经复制好了,这里的 make_private_signals()只不过是检查一下共享计数就马上回来了。 相比之下,exec_mmap()是更为关键的行动,从父进程继承下来的用户空间就是在这里放弃的。其代 码在同一文件(exec.c)中: [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary() >flush_old_exec() > exec_mmap()] 00385: static int exec_mmap(void) 00386: { 00387: struct mm_struct * mm, * old_mm; 00388: 00389: old_mm = current->mm; 00390: if (old_mm && atomic_read(&old_mm->mm_users) == 1) { 00391: flush_cache_mm(old_mm); 00392: mm_release(); 00393: exit_mmap(old_mm); 00394: flush_tlb_mm(old_mm); 00395: return 0; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 305 页,共 1481 页 00396: } 00397: 00398: mm = mm_alloc(); 00399: if (mm) { 00400: struct mm_struct *active_mm = current->active_mm; 00401: 00402: if (init_new_context(current, mm)) { 00403: mmdrop(mm); 00404: return -ENOMEM; 00405: } 00406: 00407: /* Add it to the list of mm's */ 00408: spin_lock(&mmlist_lock); 00409: list_add(&mm->mmlist, &init_mm.mmlist); 00410: spin_unlock(&mmlist_lock); 00411: 00412: task_lock(current); 00413: current->mm = mm; 00414: current->active_mm = mm; 00415: task_unlock(current); 00416: activate_mm(active_mm, mm); 00417: mm_release(); 00418: if (old_mm) { 00419: if (active_mm != old_mm) BUG(); 00420: mmput(old_mm); 00421: return 0; 00422: } 00423: mmdrop(active_mm); 00424: return 0; 00425: } 00426: return -ENOMEM; 00427: } 同样,子进程的用户空间可能是父进程用户空间的复制品,也可能只是通过一个指针来共享父进程 的用户空间,这一点只要检查一下对用户空间,也就是 current->mm 的共享计数就可清楚。当共享计数 为 1 时,表明对此空间的使用是独占的,也就是说这是从父进程复制过来的,那就要先释放 mm_struct 数据结构以下的所有 vm_area_struct 数据结构(但是不包括 mm_struct 结构本身),并且将页面表中的表 项都设置成 0。具体地这是由 exit_mmap()完成的,其代码在 mm/mmap.c 中,读者可自行阅读。在调用 exit_mmap()之前还调用了一个函数 mm_release(),对此我们将在稍后加以讨论,因为在后面也调用了这 个函数。至于 flush_cache_mm()和 flush_tlb_mm(),那只是使高速缓存与内存相一致,不在我们现在关心 之列,而且前者对 i386 处理器而言根本就是空语句。这里倒是要问一句,在父进程 fork()子进程的时候, 辛辛苦苦地复制了代表用户空间的所有数据结构,难道目的就在于稍后在执行 execve()时又辛辛苦苦把 它们全部释放?既有今日,何必当初?是的,这确实不合理。这就是在有了 fork()系统调用以后又增加 了一个 vfork()系统调用(从 BSD Unix 开始)的原因。让我们回顾一下 sys_fork()与 sys_vfork()在调用 do_fork()时的不同(process.c): 00690: asmlinkage int sys_fork(struct pt_regs regs) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 306 页,共 1481 页 00691: { 00692: return do_fork(SIGCHLD, regs.esp, ®s, 0); 00693: } 00717: asmlinkage int sys_vfork(struct pt_regs regs) 00718: { 00719: return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0); 00720: } 可见,sys_vfork()在调用 do_fork()时比 sys_fork()多了两个标志位,一个是 CLONE_VFORK,另一 个是 CLONE_VM。当 CLONE_VM 标志位为 1 时,内核并不将父进程的用户空间(数据结构)复制给 子进程,而只是将指向 mm_struct 数据结构的指针复制给子进程,让子进程通过这个指针来共享父进程 的用户空间。这样,创建子进程时可以免去复制用户空间的麻烦。而当子进程调用 execve()时就可以跳 过释放用户空间这一步,直接就为子进程分配新的用户空间。但是,这样一来省事是省事了,却可能带 来新的问题。以前讲过,fork()以后,execve()之前,子进程虽然有它自己的一整套代表用户空间的数据 结构,但是最终在物理上还是与父进程共用相同的页面。不过,由于子进程有其独立的页面目录与页面 表,可以在子进程的页面表里把对所有页面的访问权限都设置成“只读”。这样,当子进程企图改变某个 页面的内容时,就会因权限不符而导致页面异常,在页面异常的处理程序中为子进程复制所需的物理页 面,这就叫“copy_on_write”。相比之下,如果子进程与父进程共享用户空间,也就共享包括页面表在内 的所有数据结构,那就无法实施“copy_on_write”了。此时子进程写入的内容就真正意义进入了父进程 的空间中。我们知道,当一个进程在用户空间中运行时,其堆栈也在用户空间。这意味着在这种情况下 子进程可以改变父进程的堆栈,反过来父进程也可以改变子进程的堆栈!因为这个原因,vfork()的使用 是很危险的,在子进程尚未放弃对父进程用户空间的共享之前,绝对不能让两个进程都进入用户空间运 行。所以,在 sys_vfork()调用 do_fork()时结合使用了另一个标志位 CLONE_VFORK。当这个标志位为 1 时,父进程在创建了子进程以后就进入睡眠状态,等候子进程通过 execve()执行另一个目标程序,或者 通过 exit()寿终正寝。在这两种情况下子进程都会释放其共享的用户空间,使父进程可以安全地继续运行。 即便如此,也还是有危险,子进程绝对不能从调用 vfork()的那个函数中返回,否则还是可能破坏父进程 的返回地址。所以,vfork()实际上是建立在子进程在创建以后立即就会调用 execve()这个前提之上的。 那么,怎样使父进程进入睡眠而等待子进程调用 execve()或 exit()呢?当然可以有不同的实现。读者 已经在 do_fork()的代码中看到了内核让父进程在一个 0 资源的“信号量”上执行一次 down()操作而进入 睡眠的安排,这里的 mm_release()则让子进程在此信号量上执行一次 up()操作将父进程唤醒。函数 mm_release()的代码在 fork.c 中: [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary() >flush_old_exec() > exec_mmap() > mm_release()] 00255: /* Please note the differences between mmput and mm_release. 00256: * mmput is called whenever we stop holding onto a mm_struct, 00257: * error success whatever. 00258: * 00259: * mm_release is called after a mm_struct has been removed 00260: * from the current process. 00261: * 00262: * This difference is important for error handling, when we 00263: * only half set up a mm_struct for a new process and need to restore 00264: * the old one. Because we mmput the new mm_struct before 00265: * restoring the old one. . . 00266: * Eric Biederman 10 January 1998 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 307 页,共 1481 页 00267: */ 00268: void mm_release(void) 00269: { 00270: struct task_struct *tsk = current; 00271: 00272: /* notify parent sleeping on vfork() */ 00273: if (tsk->flags & PF_VFORK) { 00274: tsk->flags &= ~PF_VFORK; 00275: up(tsk->p_opptr->vfork_sem); 00276: } 00277: } 回到 exec_mmap()中,如果子进程的用户空间是通过指针共享而不是复制的,或者根本就没有用户 空间,那就不需要调用 exit_mmap()释放代表用户空间的那些数据结构了。但是,此时要为子进程分配一 个 mm_struct 数据结构以及页面目录,使得稍后可以在此基础上建立起子进程的用户空间。对于 i386 结 构的 CPU,这里的 init_new_context()是空操作,永远返回 0,所以把它跳过。把当前进程的 task_struct 结构中的指针 mm 和 active_mm 设置成指向新分配的 mm_struct 数据结构以后,就要通过 activate_mm() 切换到这个新的用户空间。这是一个宏操作,定义于 include/asm-i386/mmu_context.h: 00061: #define activate_mm(prev, next) \ 00062: switch_mm((prev),(next),NULL,smp_processor_id()) 我们将在“进程的调度与切换”一节中阅读 switch_mm()的代码,在这里只要知道当前进程的用户 空间切换到了由新分配 mm_struct 数据结构所代表的空间就可以了。还要指出,现在新的“用户空间” 实际上只是一个框架,一个“空壳”,里面一个页面也没有。另一方面,现在是在内核中运行,所以用户 空间的切换对目前的运行并无影响。 可是,原来的用户空间则从此与当前进程无关了。也就是说,当前进程最终放弃了对原来用户空间 的共享。当然,此时要执行 mm_release()将父进程唤醒。实际上,CLONE_VFORK 通常都是与 CLONE_VM 标志相联系的,所以这里对 mm_release()的调用更为关键,而前面的 mm_release()则只是“以防万一”而 已。那么,对父进程的用户空间呢?当然要减少它的共享计数。此外,如果将它的共享计数减 1 以后达 到了 0,则还要将其下属的数据结构释放,因为此时已经没有进程还在使用这个空间了。这是由 mmput() 完成的,其代码在 fork.c 中: [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary() >flush_old_exec() > exec_mmap() > mmput()] /* * Decrement the use count and release all resources for an mm. */ void mmput(struct mm_struct *mm) { if (atomic_dec_and_lock(&mm->mm_users, &mmlist_lock)) { list_del(&mm->mmlist); spin_unlock(&mmlist_lock); exit_mmap(mm); mmdrop(mm); } } 就是说,将 mm->mm_users 减 1,如果减 1 以后变成了 0,就对 mm 执行 exit_mmap()和 mmdrop()。 我们已经介绍过 exit_mmap()的作用,它释放 mm_struct 下面的所有 vm_area_struct 数据结构,并且将页Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 308 页,共 1481 页 面表中与用户空间相对应的表项都设置成 0,使整个“用户空间”成为一个“空壳”。而 mmdrop(),则 进一步将这空壳,也就是页面表和页面目录以及 mm_struct 数据结构本身,也全都释放了。不过,这只 是在将父进程的 mm->mm_users 减 1 以后变成了 0 这种特殊情况下才发生。而在我们现在这个情景中, 既然子进程通过指针共享父进程的用户空间,则父进程应该睡眠等待,所以子进程释放对空间的共享时 不会使共享计数达到 0。 回到前面的 exec_mmap()的代码中,最后还有一个特殊情况要考虑,那就是当子进程进入 exec_mmap() 时,其 task_struct 结构中的 mm_struct 结构指针 mm 为 0,也就是没有用户空间(所以是内核线程)。但 是,另一个 mm_struct 结构指针 active_mm 却不为 0,这是因为在进程切换时的一个特殊要求引起的。进 程的 task_struct 中有两个 mm_struct 结构指针:一个是 mm,指向进程的用户空间,另一个是 active_mm。 对于具有用户空间的进程这两个指针始终是一致的。但是,当一个不具备用户空间的进程(内核线程) 被调度运行时,要求它的 active_mm 一定要指向某个 mm_struct 结构,所以只好暂借一个。在这种情况, 内核将其 active_mm 设置成与在其之前运行的那个进程的 active_mm 相同,而在调度其停止运行时又将 该指针设置成 0。也就是说,一个内核线程在受调度运行时要“借用”在它之前运行的那个进程的 active_mm(详见“进程的调度与切换”),因而要递增这个 mm_struct 结构的使用计数。而现在,已经为 这内核线程分配了它自己的 mm_struct 结构,使其升格成为了进程,就不再使用借来的 active_mm 了。 所以,要调用 mmdrop(),递减其使用计数。这是一个 inline 函数,其代码在 include/linux/sched.c 中: [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary() > flush_old_exec() > exec_mmap() > mmdrop()] 00709: /* mmdrop drops the mm and the page tables */ 00710: extern inline void FASTCALL(__mmdrop(struct mm_struct *)); 00711: static inline void mmdrop(struct mm_struct * mm) 00712: { 00713: if (atomic_dec_and_test(&mm->mm_count)) 00714: __mmdrop(mm); 00715: } 而__mmdrop()的代码则在 fork.c 中: [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary() > flush_old_exec() > exec_mmap() > mmdrop() > __mmdrop()] 00229: /* 00230: * Called when the last reference to the mm 00231: * is dropped: either by a lazy thread or by 00232: * mmput. Free the page directory and the mm. 00233: */ 00234: inline void __mmdrop(struct mm_struct *mm) 00235: { 00236: if (mm == &init_mm) BUG(); 00237: pgd_free(mm->pgd); 00238: destroy_context(mm); 00239: free_mm(mm); 00240: } 可见,mmdrop()在将一个 mm_struct 数据结构释放之前也要递减并检查其使用计数 mm_count,只 有 在递减后变成 0 才会将其释放。注意两个计数器,即 mm_users 与 mm_count 的区别,在 mm_struct 结构 分配之初二者都设为 1,然后 mm_users 随子进程对用户空间的共享递减,而 mm_count 则因内核中对该 mm_struct 数据结构的使用而递减。 从 exec_mmap()返回到 flush_old_exec()时,子进程从父进程继承的用户空间已经释放,其用户空间Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 309 页,共 1481 页 变成了一个独立的“空壳”,也就是一个大小为 0 的独立的用户空间。这时候的进程已经是“义无反顾” 了,回不到原来的用户空间中去了(见代码中的注解)。前面讲过,当前进程(子进程)原来可能是通过 指针共享父进程的信号处理表的,而现在有了自己的独立的信号处理表,所以也要递减父进程信号处理 表的共享计数,并且如果递减后为 0 就要将其所占用的空间释放,这就是 release_old_signals()所做的事 情。此外,进程的 task_struct 结构中有一个字符数组 comm[],用来保存进程所执行的程序名,所以还要 把 bprm->filename 的目标程序路径名中的最后一段抄过去。接着的 flush_thread()只是处理与 debug 和 i387 协处理器有关的内容,不是我们所关心的。 如果“当前进程”原来只是一个线程,那么它的 task_struct 结构通过结构中的队列头 thread_group 挂入由其父进程为首的“线程组”队列。现在,它已经在通过 execve()升级为进程,放弃了对父进程用 户空间的共享,所以就要通过 de_thread()从这个线程组中脱离出来。这个函数的代码在 fs/exec.c 中: [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary() >flush_old_exec() > de_thread()] 00502: /* 00503: * An execve() will automatically "de-thread" the process. 00504: * Note: we don't have to hold the tasklist_lock to test 00505: * whether we migth need to do this. If we're not part of 00506: * a thread group, there is no way we can become one 00507: * dynamically. And if we are, we only need to protect the 00508: * unlink - even if we race with the last other thread exit, 00509: * at worst the list_del_init() might end up being a no-op. 00510: */ 00511: static inline void de_thread(struct task_struct *tsk) 00512: { 00513: if (!list_empty(&tsk->thread_group)) { 00514: write_lock_irq(&tasklist_lock); 00515: list_del_init(&tsk->thread_group); 00516: write_unlock_irq(&tasklist_lock); 00517: } 00518: 00519: /* Minor oddity: this might stay the same. */ 00520: tsk->tgid = tsk->pid; 00521: } 前面说过,进程的信号处理表就好像是个中断向量表。但是,这里还有个重要的不同,就是中断向 量表中的表项要么指向一个服务程序,要么就没有;而信号处理表中则还可以有对各种信号预设的 (default)响应,并不一定非要指向一个服务程序。当把信号处理表从父进程复制过来时,其中每个表 项的值有三种可能:一种可能是 SIG_IGN,表示不理睬;第二种是 SIG_DFL,表示采取预设的响应方式 (例如收到 SIGOUT 就 exit());第三种就是指向一个用户空间的子程序。可是,现在整个用户空间都已 经放弃了,怎么还能让信号处理表的表项指向用户空间的子程序呢?所以还得检查一遍,将指向服务程 序的表项改成 SIG_DFL。这是由 flush_signal_handler()完成的,代码在 kernel/signal.c 中: [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary() >flush_old_exec() > flush_signal_handlers()] 00127: /* 00128: * Flush all handlers for a task. 00129: */ 00130: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 310 页,共 1481 页 00131: void 00132: flush_signal_handlers(struct task_struct *t) 00133: { 00134: int i; 00135: struct k_sigaction *ka = &t->sig->action[0]; 00136: for (i = _NSIG ; i != 0 ; i--) { 00137: if (ka->sa.sa_handler != SIG_IGN) 00138: ka->sa.sa_handler = SIG_DFL; 00139: ka->sa.sa_flags = 0; 00140: sigemptyset(&ka->sa.sa_mask); 00141: ka++; 00142: } 00143: } 最后,是对原有已打开文件的处理,这是由 flush_old_files()完成的。进程的 task_struct 结构中有一 个指向一个 file_struct 结构的指针“files”,所指向的数据结构中保存着已打开文件的信息。在 file_struct 结构中有个位图 close_on_exec,里面存储着表示哪些文件在执行一个新目标程序时应予关闭的信息。而 flush_old_files()要做的就是根据这个位图的指示将这些文件关闭,并且将此位图清成全 0。其代码在 exec.c 中: [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary() >flush_old_exec() > flush_old_files()] 00469: /* 00470: * These functions flushes out all traces of the currently running executable 00471: * so that a new one can be started 00472: */ 00473: 00474: static inline void flush_old_files(struct files_struct * files) 00475: { 00476: long j = -1; 00477: 00478: write_lock(&files->file_lock); 00479: for (;;) { 00480: unsigned long set, i; 00481: 00482: j++; 00483: i = j * __NFDBITS; 00484: if (i >= files->max_fds || i >= files->max_fdset) 00485: break; 00486: set = files->close_on_exec->fds_bits[j]; 00487: if (!set) 00488: continue; 00489: files->close_on_exec->fds_bits[j] = 0; 00490: write_unlock(&files->file_lock); 00491: for ( ; set ; i++,set >>= 1) { 00492: if (set & 1) { 00493: sys_close(i); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 311 页,共 1481 页 00494: } 00495: } 00496: write_lock(&files->file_lock); 00497: 00498: } 00499: write_unlock(&files->file_lock); 00500: } 一般来说,进程开头三个文件,即 fd 为 0、1 和 2(或 stdin、stdout 以及 stderr)的已打开文件是不 关闭的;其他的已打开文件则都应关闭,但是也可以通过 ioctl()系统调用来加以改变。 从 flush_old_exec()返回到 load_aout_binary()中时,当前进程已经完成了与过去告别,准备迎接新的 使命了。我们继续沿着 binfmt_aout.c 往下看(但是跳过针对 sparc 处理器的条件编译): [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary()] 00287: /* OK, This is the point of no return */ 00288: #if !defined(__sparc__) 00289: set_personality(PER_LINUX); 00290: #else 00291: set_personality(PER_SUNOS); 00292: #if !defined(__sparc_v9__) 00293: memcpy(¤t->thread.core_exec, &ex, sizeof(struct exec)); 00294: #endif 00295: #endif 00296: 00297: current->mm->end_code = ex.a_text + 00298: (current->mm->start_code = N_TXTADDR(ex)); 00299: current->mm->end_data = ex.a_data + 00300: (current->mm->start_data = N_DATADDR(ex)); 00301: current->mm->brk = ex.a_bss + 00302: (current->mm->start_brk = N_BSSADDR(ex)); 00303: 00304: current->mm->rss = 0; 00305: current->mm->mmap = NULL; 00306: compute_creds(bprm); 00307: current->flags &= ~PF_FORKNOEXEC; 这里是对新的 mm_struct 数据结构中的一些变量进行初始化,为以后分配存储空间并读入可执行代 码的映象做好准备。目标代码的映象分成 text、data 以及 bss 三段,mm_struct 结构中为每个段都设置了 start 和 end 两个指针。每段的其实地址定义于 inlcue/linux/a.out.h: 00108: /* Address of text segment in memory after it is loaded. */ 00109: #if !defined (N_TXTADDR) 00110: #define N_TXTADDR(x) (N_MAGIC(x) == QMAGIC ? PAGE_SIZE : 0) 00111: #endif 00141: #define _N_SEGMENT_ROUND(x) (((x) + SEGMENT_SIZE - 1) & ~(SEGMENT_SIZE - 1)) 00142: 00143: #define _N_TXTENDADDR(x) (N_TXTADDR(x)+(x).a_text) 00144: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 312 页,共 1481 页 00145: #ifndef N_DATADDR 00146: #define N_DATADDR(x) \ 00147: (N_MAGIC(x)==OMAGIC? (_N_TXTENDADDR(x)) \ 00148: : (_N_SEGMENT_ROUND (_N_TXTENDADDR(x)))) 00149: #endif 00150: 00151: /* Address of bss segment in memory after it is loaded. */ 00152: #if !defined (N_BSSADDR) 00153: #define N_BSSADDR(x) (N_DATADDR(x) + (x).a_data) 00154: #endif 可见,装入内存以后的程序映象从正文段(代码段)开始,其起始地址为 0 或 PAGE_SIZE,取决于 具体的格式。正文段上面是数据段;然后是 bss 段,那就是不加初始化的数据段。再往上就是动态分配 的内存“堆”以及用户空间的堆栈了。 然后,通过 compute_creds()确定进程在开始执行新的目标代码以后所具有的权限,这是根据 bprm 中的内容和当前的权限确定的。其代码在 exec.c 中,读者可以自行阅读。 接下来,就取决于特殊 a.out 格式可执行代码的特性了(binfmt_aout.c): [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary()] 00308: #ifdef __sparc__ 00309: if (N_MAGIC(ex) == NMAGIC) { 00310: loff_t pos = fd_offset; 00311: /* Fuck me plenty... */ 00312: /* */ 00313: error = do_brk(N_TXTADDR(ex), ex.a_text); 00314: bprm->file->f_op->read(bprm->file, (char *) N_TXTADDR(ex), 00315: ex.a_text, &pos); 00316: error = do_brk(N_DATADDR(ex), ex.a_data); 00317: bprm->file->f_op->read(bprm->file, (char *) N_DATADDR(ex), 00318: ex.a_data, &pos); 00319: goto beyond_if; 00320: } 00321: #endif 00322: 00323: if (N_MAGIC(ex) == OMAGIC) { 00324: unsigned long text_addr, map_size; 00325: loff_t pos; 00326: 00327: text_addr = N_TXTADDR(ex); 00328: 00329: #if defined(__alpha__) || defined(__sparc__) 00330: pos = fd_offset; 00331: map_size = ex.a_text+ex.a_data + PAGE_SIZE - 1; 00332: #else 00333: pos = 32; 00334: map_size = ex.a_text+ex.a_data; 00335: #endif Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 313 页,共 1481 页 00336: 00337: error = do_brk(text_addr & PAGE_MASK, map_size); 00338: if (error != (text_addr & PAGE_MASK)) { 00339: send_sig(SIGKILL, current, 0); 00340: return error; 00341: } 00342: 00343: error = bprm->file->f_op->read(bprm->file, (char *)text_addr, 00344: ex.a_text+ex.a_data, &pos); 00345: if (error < 0) { 00346: send_sig(SIGKILL, current, 0); 00347: return error; 00348: } 00349: 00350: flush_icache_range(text_addr, text_addr+ex.a_text+ex.a_data); 00351: } else { 前面讲过,a.out 格式目标代码中的 magic number 表示着代码的特性,或者说类型。当 magic number 为 OMAGIC 时,表示该文件中的可执行代码并非“纯代码”。对于这样的代码,先通过 do_brk()为正文 段和数据段合在一起分配空间,然后就把这两部分从文件中读进来。函数 do_brk()我们已经在第 2 章中 介绍过,而从文件读入则在“文件系统”和“块设备驱动”两章中有详细叙述,读者可以参阅,这里就 不重复了。不过要指出,读入代码时是从文件中位移为 32 的地方开始,读入到进程用户空间中从地址 0 开始的地方,读入的总长度为 ex.a_text+ex.a_data。对 于 i386 CPU 而言,flush_icache_range()为一空语句。 至于 bss 段,则无需从文件读入,只要分配空间就可以了,所以放在后面再处理。对于 OMAGIC 类型的 a.out 可执行文件而言,装入程序的工作就基本完成了。 可是,如果不是 OMAGIC 类型呢?请接着往下看(binfmt_aout.c): [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary()] 00351: } else { 00352: static unsigned long error_time, error_time2; 00353: if ((ex.a_text & 0xfff || ex.a_data & 0xfff) && 00354: (N_MAGIC(ex) != NMAGIC) && (jiffies-error_time2) > 5*HZ) 00355: { 00356: printk(KERN_NOTICE "executable not page aligned\n"); 00357: error_time2 = jiffies; 00358: } 00359: 00360: if ((fd_offset & ~PAGE_MASK) != 0 && 00361: (jiffies-error_time) > 5*HZ) 00362: { 00363: printk(KERN_WARNING 00364: "fd_offset is not page aligned. Please convert program: %s\n", 00365: bprm->file->f_dentry->d_name.name); 00366: error_time = jiffies; 00367: } 00368: 00369: if (!bprm->file->f_op->mmap||((fd_offset & ~PAGE_MASK) != 0)) { Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 314 页,共 1481 页 00370: loff_t pos = fd_offset; 00371: do_brk(N_TXTADDR(ex), ex.a_text+ex.a_data); 00372: bprm->file->f_op->read(bprm->file,(char *)N_TXTADDR(ex), 00373: ex.a_text+ex.a_data, &pos); 00374: flush_icache_range((unsigned long) N_TXTADDR(ex), 00375: (unsigned long) N_TXTADDR(ex) + 00376: ex.a_text+ex.a_data); 00377: goto beyond_if; 00378: } 00379: 00380: down(¤t->mm->mmap_sem); 00381: error = do_mmap(bprm->file, N_TXTADDR(ex), ex.a_text, 00382: PROT_READ | PROT_EXEC, 00383: MAP_FIXED | MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE, 00384: fd_offset); 00385: up(¤t->mm->mmap_sem); 00386: 00387: if (error != N_TXTADDR(ex)) { 00388: send_sig(SIGKILL, current, 0); 00389: return error; 00390: } 00391: 00392: down(¤t->mm->mmap_sem); 00393: error = do_mmap(bprm->file, N_DATADDR(ex), ex.a_data, 00394: PROT_READ | PROT_WRITE | PROT_EXEC, 00395: MAP_FIXED | MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE, 00396: fd_offset + ex.a_text); 00397: up(¤t->mm->mmap_sem); 00398: if (error != N_DATADDR(ex)) { 00399: send_sig(SIGKILL, current, 0); 00400: return error; 00401: } 00402: } 在 a.out 格式的可执行文件中,除 OMAGIC 以外其他三种均为纯代码,也就是所谓的“可重入”代 码。在此类代码中,不但其正文段的执行代码在运行时不会改变,其数据段的内容也不会在运行时改变。 凡是要在运行过程中改变内容的东西都在堆栈中(局部变量),要不然就在动态分配的缓冲区。所以内核 干脆将可执行文件映射到了进程的用户空间中,这样连通常 swap 所需的盘上空间也省去了。在这三种 类型的可执行文件中,除 NMAGIC 以外都要求正文段及数据段的长度与页面大小对齐。如果发现没有 对齐就要通过 printk()发出警告信息。但是,发出警告信息太频繁也不好,所以设置了一个静态变量 error_time2,使警告信息之间的间隔不小于 5 秒。接下来的操作取决于具体的文件系统是否提供 mmap, 就是将一个已打开文件映射到虚存空间的操作,以及正文段及数据段的长度是否与页面大小对齐。如果 不满足映射的条件,就分配空间并且将正文段和数据段一起读入至进程的用户空间,这次是从文件中位 移为 fd_offset,即 N_TXTOFF(ex)的地方开始,读入到由文件的头部所指定的地址 N_TXTADDR(ex),长 度为两段的总和。如果满足映射的条件,那就更好了,那就通过 do_mmap()分别将文件中的正文段和数 据段映射到进程的用户空间中,映射的地址则与装入的地址一致。调用 mmap()之前无需分配空间,那已Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 315 页,共 1481 页 经包含在 mmap()之中了。 至此,正文段和数据段都已经装入就绪了,接下来就是 bss 段和堆栈段了(binfmt_aout.c): [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary()] 00403: beyond_if: 00404: set_binfmt(&aout_format); 00405: 00406: set_brk(current->mm->start_brk, current->mm->brk); 00407: 00408: retval = setup_arg_pages(bprm); 00409: if (retval < 0) { 00410: /* Someone check-me: is this error path enough? */ 00411: send_sig(SIGKILL, current, 0); 00412: return retval; 00413: } 00414: 00415: current->mm->start_stack = 00416: (unsigned long) create_aout_tables((char *) bprm->p, bprm); 函数 set_binfmt()的操作很简单(fs/exec.c): [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary() > set_binfmt()] 00908: void set_binfmt(struct linux_binfmt *new) 00909: { 00910: struct linux_binfmt *old = current->binfmt; 00911: if (new && new->module) 00912: __MOD_INC_USE_COUNT(new->module); 00913: current->binfmt = new; 00914: if (old && old->module) 00915: __MOD_DEC_USE_COUNT(old->module); 00916: } 如果当前进程原来执行的代码格式与新的代码格式都不是由可安装模块支持,则实际上只剩下一行 语句,那就是设置 current->binfmt。 函数 set_brk()为可执行代码的 bss 段分配空间并建立起页面映射,其代码在同一文件中 (binfmt_aout.c): [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary() > set_brk()] 00042: static void set_brk(unsigned long start, unsigned long end) 00043: { 00044: start = PAGE_ALIGN(start); 00045: end = PAGE_ALIGN(end); 00046: if (end <= start) 00047: return; 00048: do_brk(start, end - start); 00049: } 读者在第 2 章中读过 do_brk()的代码,应该理解为什么 bss 段中内容的初始值为全 0。接着,还要在 用户空间的堆栈区顶部为进程建立起一个虚存区间,并将执行参数以及环境变量所占的物理页面与此虚 存区间建立起映射。这是由 setup_arg_pages()完成的,其代码在 exec.c 中: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 316 页,共 1481 页 [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary() > setup_arg_pages()] 00288: int setup_arg_pages(struct linux_binprm *bprm) 00289: { 00290: unsigned long stack_base; 00291: struct vm_area_struct *mpnt; 00292: int i; 00293: 00294: stack_base = STACK_TOP - MAX_ARG_PAGES*PAGE_SIZE; 00295: 00296: bprm->p += stack_base; 00297: if (bprm->loader) 00298: bprm->loader += stack_base; 00299: bprm->exec += stack_base; 00300: 00301: mpnt = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL); 00302: if (!mpnt) 00303: return -ENOMEM; 00304: 00305: down(¤t->mm->mmap_sem); 00306: { 00307: mpnt->vm_mm = current->mm; 00308: mpnt->vm_start = PAGE_MASK & (unsigned long) bprm->p; 00309: mpnt->vm_end = STACK_TOP; 00310: mpnt->vm_page_prot = PAGE_COPY; 00311: mpnt->vm_flags = VM_STACK_FLAGS; 00312: mpnt->vm_ops = NULL; 00313: mpnt->vm_pgoff = 0; 00314: mpnt->vm_file = NULL; 00315: mpnt->vm_private_data = (void *) 0; 00316: insert_vm_struct(current->mm, mpnt); 00317: current->mm->total_vm = (mpnt->vm_end - mpnt->vm_start) >> PAGE_SHIFT; 00318: } 00319: 00320: for (i = 0 ; i < MAX_ARG_PAGES ; i++) { 00321: struct page *page = bprm->page[i]; 00322: if (page) { 00323: bprm->page[i] = NULL; 00324: current->mm->rss++; 00325: put_dirty_page(current,page,stack_base); 00326: } 00327: stack_base += PAGE_SIZE; 00328: } 00329: up(¤t->mm->mmap_sem); 00330: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 317 页,共 1481 页 00331: return 0; 00332: } 进程的用户空间中地址最高处为堆栈区,这里的常数 STACK_TOP 就是 TASK_SIZE,也就是 3GB (0xC0000000)。堆栈区的顶部为一个数组,数组中的每一个元素都是一个页面。数组的大小为 MAX_ARG_PAGES,而实际映射的页面数量则取决于这些执行参数和环境变量的数量。 然后,在这些页面的下方,就是进程的用户空间堆栈了。另一方面,大家知道任何用户程序的入口 都是 main(),而 main()有两个参数 argc 和 argv[]。其中参数 argv[]是字符指针数组,argc 则为数组的大小。 但是实际上还有个隐藏着的字符指针数组 envp[]用来传递环境变量,只是不在用户程序的“视野”之内 而已。所以,用户空间堆栈从一开始就要设置好三项数据,即 envp[]、argv[]以及 argc。此外,还要将保 存着的(字符串形式的)参数和环境变量复制到用户空间的顶端。这都是由 create_aout_tables()完成的, 其代码也在同一个文件(binfmt.aout.c)中: [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary() > create_aout_tables()] 00187: /* 00188: * create_aout_tables() parses the env- and arg-strings in new user 00189: * memory and creates the pointer tables from them, and puts their 00190: * addresses on the "stack", returning the new stack pointer value. 00191: */ 00192: static unsigned long * create_aout_tables(char * p, struct linux_binprm * bprm) 00193: { 00194: char **argv, **envp; 00195: unsigned long * sp; 00196: int argc = bprm->argc; 00197: int envc = bprm->envc; 00198: 00199: sp = (unsigned long *) ((-(unsigned long)sizeof(char *)) & (unsigned long) p); 00200: #ifdef __sparc__ …………………… 00204: #endif 00205: #ifdef __alpha__ …………………… 00217: #endif 00218: sp -= envc+1; 00219: envp = (char **) sp; 00220: sp -= argc+1; 00221: argv = (char **) sp; 00222: #if defined(__i386__) || defined(__mc68000__) || defined(__arm__) 00223: put_user((unsigned long) envp,--sp); 00224: put_user((unsigned long) argv,--sp); 00225: #endif 00226: put_user(argc,--sp); 00227: current->mm->arg_start = (unsigned long) p; 00228: while (argc-->0) { 00229: char c; 00230: put_user(p,argv++); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 318 页,共 1481 页 00231: do { 00232: get_user(c,p++); 00233: } while (c); 00234: } 00235: put_user(NULL,argv); 00236: current->mm->arg_end = current->mm->env_start = (unsigned long) p; 00237: while (envc-->0) { 00238: char c; 00239: put_user(p,envp++); 00240: do { 00241: get_user(c,p++); 00242: } while (c); 00243: } 00244: put_user(NULL,envp); 00245: current->mm->env_end = (unsigned long) p; 00246: return sp; 00247: } 读者应该能看明白,这是在堆栈的顶端构筑 envp[]、argv[]和 argc。请读者注意看一下这段代码中的 228 至 234 行(以及 237 至 243 行),然后回答一个问题:为什么是 get_user(c, ptt)而不是 get_user(&c, ptt)? 以前我们曾经讲过,get_user()是一段颇具挑战性的代码,并建议读者自行阅读。现在简单地介绍一下, 看看你是否读懂了。这是在 include/asm-i386/uaccess.h 中定义的一个宏: 00089: /* 00090: * These are the main single-value transfer routines. They automatically 00091: * use the right size if we just have the right pointer type. 00092: * 00093: * This gets kind of ugly. We want to return _two_ values in "get_user()" 00094: * and yet we don't want to do any pointers, because that is too much 00095: * of a performance impact. Thus we have a few rather ugly macros here, 00096: * and hide all the uglyness from the user. 00097: * 00098: * The "__xxx" versions of the user access functions are versions that 00099: * do not verify the address space, that must have been done previously 00100: * with a separate "access_ok()" call (this is used when we do multiple 00101: * accesses to the same area of user memory). 00102: */ 00103: 00104: extern void __get_user_1(void); 00105: extern void __get_user_2(void); 00106: extern void __get_user_4(void); 00107: 00108: #define __get_user_x(size,ret,x,ptr) \ 00109: __asm__ __volatile__("call __get_user_" #size \ 00110: :"=a" (ret),"=d" (x) \ 00111: :"0" (ptr)) 00112: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 319 页,共 1481 页 00113: /* Careful: we have to cast the result to the type of the pointer for sign reasons */ 00114: #define get_user(x,ptr) \ 00115: ({ int __ret_gu,__val_gu; \ 00116: switch(sizeof (*(ptr))) { \ 00117: case 1: __get_user_x(1,__ret_gu,__val_gu,ptr); break; \ 00118: case 2: __get_user_x(2,__ret_gu,__val_gu,ptr); break; \ 00119: case 4: __get_user_x(4,__ret_gu,__val_gu,ptr); break; \ 00120: default: __get_user_x(X,__ret_gu,__val_gu,ptr); break; \ 00121: } \ 00122: (x) = (__typeof__(*(ptr)))__val_gu; \ 00123: __ret_gu; \ 00124: }) 先看一下 122 行,它回答了为什么引用时的第一个参数是 c 而不是&c 的问题。其次,经过 gcc 的预 处理以后,__get_user_x()就变成了__get_user_1(),_get_user_2()或__get_user_4(),分别用于从用户空间 读取一个字节、一个短整数或一个长整数。宏操作 get_user 根据第 2 个参数的类型确定目标的大小而分 别调用__get_user_1(),_get_user_2()或__get_user_4()。调用时目标地址(ptr)在寄存器 EAX 中;而返回 时 EAX 中为返回的函数值(出错代码),EDX 中为从用户空间读过来的数值。这几个函数的代码都在 arch/i386/lib/getusr.S 中,以__get_user_1()为例: 00024: addr_limit = 12 00025: 00026: .text 00027: .align 4 00028: .globl __get_user_1 00029: __get_user_1: 00030: movl %esp,%edx 00031: andl $0xffffe000,%edx 00032: cmpl addr_limit(%edx),%eax 00033: jae bad_get_user 00034: 1: movzbl (%eax),%edx 00035: xorl %eax,%eax 00036: ret …………………… 00064: bad_get_user: 00065: xorl %edx,%edx 00066: movl $-14,%eax 00067: ret 00068: 这里的第 30 和 31 行将当前进程的系统空间堆栈指针与 8K,即两个页面的边界对齐,从而取得当前 进程的 task_struct 结构指针。在 task_struct 结构中位移为 12 处为当前进程用户空间地址的上限,所以作 为参数传过来的地址不得高于这个上限。这也说明,对 task_struct 结构的定义(开头几个成分)是不能 随意更改的。如果地址没有超出范围就从用户空间把其内容读入寄存器 DX,并将 EAX 清 0 作为函数的 返回值。 另一个宏操作 pur_user()与此相反,只是方向相反。 当 CPU 从 create_aout_tables()返回到 do_load_aout_binary()时,堆栈顶端的 argv[]和 argc 都已经准备Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 320 页,共 1481 页 好。我们再继续往下看(binfmt_aout.c): [sys_execve() > do_execve() > search_binary_handler() > load_aout_binary()] 00417: #ifdef __alpha__ 00418: regs->gp = ex.a_gpvalue; 00419: #endif 00420: start_thread(regs, ex.a_entry, current->mm->start_stack); 00421: if (current->ptrace & PT_PTRACED) 00422: send_sig(SIGTRAP, current, 0); 00423: return 0; 00424: } 这里只剩下最后一个关键性的操作了,那就是 start_thread() 。这是个宏操作,定义于 include/asm-i386/process.h 中: 00408: #define start_thread(regs, new_eip, new_esp) do { \ 00409: __asm__("movl %0,%%fs ; movl %0,%%gs": :"r" (0)); \ 00410: set_fs(USER_DS); \ 00411: regs->xds = __USER_DS; \ 00412: regs->xes = __USER_DS; \ 00413: regs->xss = __USER_DS; \ 00414: regs->xcs = __USER_CS; \ 00415: regs->eip = new_eip; \ 00416: regs->esp = new_esp; \ 00417: } while (0) 读者对这里的 regs 指针已经很熟悉,它指向保留在当前进程系统空间堆栈中的各个寄存器副本。当 进程从系统调用返回时,这些数值就会被“恢复”到 CPU 的各个寄存器中。所以,那时候的堆栈指针将 是 current->mm->start_stack;而返回地址,也就是 EIP 的内容,则将是 ex.a_entry。显然,这正是我们所 需要的。 至此,可执行代码的装载和投入运行已经完成。而 do_execve()在调用了 search_binary_handler()以后 也就结束了。当 CPU 从系统调用返回到用户空间时,就会从由 ex.a_entry 确定的地址开始执行。 4.4.2. 文字形式可执行文件的执行 前面介绍了 a.out 格式可执行文件的装入和投入运行过程,我们把这作为二进制可执行文件的代表。 现在,再来简单地看一下字符形式的可执行文件(为 shell 过程或 perl 文件)的执行。有关的代码都在 binfmt_script.c 中。由于已经比较详细地阅读了二进制可执行文件的处理,读者在阅读下面的代码时应该 比较轻松了,所以我们只作一些简要的提示(binfmt_script.c): 00095: struct linux_binfmt script_format = { 00096: NULL, THIS_MODULE, load_script, NULL, NULL, 0 00097: }; 以前我们提到过,Script 文件的开头两个字符应为“#!”,然后是解释程序的路径名,如/bin/sh, /usr/bin/perl 等等,后面还可以有参数。但是,第一行的长度不得长于 127 个字符。我们来看 Script 文件 的装载,这是由 load_script()完成的(binfmt_script.c): [sys_execve() > do_execve() > search_binary_handler() > load_script()] 00017: static int load_script(struct linux_binprm *bprm,struct pt_regs *regs) 00018: { 00019: char *cp, *i_name, *i_arg; 00020: struct file *file; 00021: char interp[BINPRM_BUF_SIZE]; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 321 页,共 1481 页 00022: int retval; 00023: 00024: if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!') || (bprm->sh_bang)) 00025: return -ENOEXEC; 00026: /* 00027: * This section does the #! interpretation. 00028: * Sorta complicated, but hopefully it will work. -TYT 00029: */ 00030: 00031: bprm->sh_bang++; 00032: allow_write_access(bprm->file); 00033: fput(bprm->file); 00034: bprm->file = NULL; 00035: 00036: bprm->buf[BINPRM_BUF_SIZE - 1] = '\0'; 00037: if ((cp = strchr(bprm->buf, '\n')) == NULL) 00038: cp = bprm->buf+BINPRM_BUF_SIZE-1; 00039: *cp = '\0'; 00040: while (cp > bprm->buf) { 00041: cp--; 00042: if ((*cp == ' ') || (*cp == '\t')) 00043: *cp = '\0'; 00044: else 00045: break; 00046: } 00047: for (cp = bprm->buf+2; (*cp == ' ') || (*cp == '\t'); cp++); 00048: if (*cp == '\0') 00049: return -ENOEXEC; /* No interpreter name found */ 00050: i_name = cp; 00051: i_arg = 0; 00052: for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++) 00053: /* nothing */ ; 00054: while ((*cp == ' ') || (*cp == '\t')) 00055: *cp++ = '\0'; 00056: if (*cp) 00057: i_arg = cp; 00058: strcpy (interp, i_name); 得到了解释程序的路径名以后,问题就转化成了对解释程序的装入,而 script 文件本身则转化成了 解释程序的运行参数。虽然 script 文件本身并不是二进制格式的可执行文件,解释程序的映象却是一个 二进制的可执行文件。还是在 binfmt_script.c 文件中往下看: [sys_execve() > do_execve() > search_binary_handler() > load_script()] 00059: /* 00060: * OK, we've parsed out the interpreter name and 00061: * (optional) argument. 00062: * Splice in (1) the interpreter's name for argv[0] Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 322 页,共 1481 页 00063: * (2) (optional) argument to interpreter 00064: * (3) filename of shell script (replace argv[0]) 00065: * 00066: * This is done in reverse order, because of how the 00067: * user environment and arguments are stored. 00068: */ 00069: remove_arg_zero(bprm); 00070: retval = copy_strings_kernel(1, &bprm->filename, bprm); 00071: if (retval < 0) return retval; 00072: bprm->argc++; 00073: if (i_arg) { 00074: retval = copy_strings_kernel(1, &i_arg, bprm); 00075: if (retval < 0) return retval; 00076: bprm->argc++; 00077: } 00078: retval = copy_strings_kernel(1, &i_name, bprm); 00079: if (retval) return retval; 00080: bprm->argc++; 00081: /* 00082: * OK, now restart the process with the interpreter's dentry. 00083: */ 00084: file = open_exec(interp); 00085: if (IS_ERR(file)) 00086: return PTR_ERR(file); 00087: 00088: bprm->file = file; 00089: retval = prepare_binprm(bprm); 00090: if (retval < 0) 00091: return retval; 00092: return search_binary_handler(bprm,regs); 00093: } 可见,Script 文件的使用在装入运行的过程中引入了递规性,load_script() 最后又调用 search_binary_handler()。不管递规有多深,最终执行的一定是个二进制可执行文件,例如/bin/sh、 /usr/bin/perl 等解释程序。在递规的过程中,逐层的可执行文件路径名形成一个参数堆栈,传递给最终的 解释程序。 4.5. 系统调用exit()与wait4() 系统调用 exit()与 wait4()的代码基本上都在 kernel/exit.c 中,下面我们在引用代码时凡不特别说明出 处的均来自这个文件。 先来看 exit()的实现(exit.c): 00482: asmlinkage long sys_exit(int error_code) 00483: { 00484: do_exit((error_code&0xff)<<8); 00485: } 显然,其主体为 do_exit()。先看它的前半部: Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 323 页,共 1481 页 [sys_exit() > do_exit()] 00421: NORET_TYPE void do_exit(long code) 00422: { 00423: struct task_struct *tsk = current; 00424: 00425: if (in_interrupt()) 00426: panic("Aiee, killing interrupt handler!"); 00427: if (!tsk->pid) 00428: panic("Attempted to kill the idle task!"); 00429: if (tsk->pid == 1) 00430: panic("Attempted to kill init!"); 00431: tsk->flags |= PF_EXITING; 00432: del_timer_sync(&tsk->real_timer); 00433: 首先,在函数的类型 void 前面还有个说明 NORET_TYPE。在 include/linux/kernel.h 中 NORET_TYPE 定义为“/**/”,所以对编译毫无影响,但起到了提醒读者的作用。CPU 在进入 do_exit()以后,当前进程 就在中途寿终正寝,不会从这个函数返回。所谓不会从这个函数返回到底是怎么回事,又是什么原因, 读者在读了下面的代码以后就明白了。这里只指出,既然 CPU 不会从 do_exit()中返回,也就不会从 sys_exit()中返回,从而也就不会从系统调用 exit()返回。也只有这样,才能达到“exit”,即从系统退出的 目的。另一方面,所谓 exit,只有进程(或线程)才谈得上。中断服务程序根本就不应该调用 do_exit(), 不管是直接还是间接调用。所以,这里首先通过 in_interrupt()对此加以检查,如发现这是在某个中断服 务程序调用的,那就一定是出了问题。 那么,怎么知道是否在中断服务程序中呢?让我们来看看在 include/asm-i386/hardirq.h 中定义的 in_interrupt(): 00020: /* 00021: * Are we in an interrupt context? Either doing bottom half 00022: * or hardware interrupt processing? 00023: */ 00024: #define in_interrupt() ({ int __cpu = smp_processor_id(); \ 00025: (local_irq_count(__cpu) + local_bh_count(__cpu) != 0); }) 在单 CPU 的系统中,__cpu 一定是 0。在第 3 章中讲过函数 handle_IRQ_event(),在其入口处和出口 处个有一个函数调用 irq_enter()和 irq_exit(),就分别递增和递减计数器 local_irq_count[__cpu]。所以,只 要这个计数器非 0,就说明 CPU 在 handle_IRQ_event()中。类似地,只要计数器 local_bh_count[__cpu] 为非 0,就说明 CPU 正在执行某个 bh 函数,这也跟中断服务程序一样。反之,只要不是在中断服务的 上下文中,那就一定是在某个进程(或线程)的上下文中了。但是,0 号进程和 1 号进程,也就是“空 转”(idle)进程和“初始化”(init)进程,是不允许退出的,所以接着要对当前进程的 pid 加以检查。 进程在决定退出之前可能已经设置了实时定时器,也就是将其 task_struct 结构中的成员 real_timer 挂入了内核中的定时器队列。现在进程即将退出系统,一来是这个定时器已经没有了存在的必要,二来 进程的 task_struct 结构将撤销,作为其成员的 real_timer 也将“皮之不存,毛将焉附”,当然要先将它从 队列中脱离。所以,要通过 del_timer_sync()将当前进程从定时器队列中脱离出来。 继续往下看(exit.c): [sys_exit() > do_exit()] 00434: fake_volatile: 00435: #ifdef CONFIG_BSD_PROCESS_ACCT 00436: acct_process(code); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 324 页,共 1481 页 00437: #endif 00438: __exit_mm(tsk); 00439: 00440: lock_kernel(); 00441: sem_exit(); 00442: __exit_files(tsk); 00443: __exit_fs(tsk); 00444: exit_sighand(tsk); 00445: exit_thread(); 00446: 00447: if (current->leader) 00448: disassociate_ctty(1); 00449: 00450: put_exec_domain(tsk->exec_domain); 00451: if (tsk->binfmt && tsk->binfmt->module) 00452: __MOD_DEC_USE_COUNT(tsk->binfmt->module); 00453: 00454: tsk->exit_code = code; 00455: exit_notify(); 00456: schedule(); 00457: BUG(); 00458: /* 00459: * In order to get rid of the "volatile function does return" message 00460: * I did this little loop that confuses gcc to think do_exit really 00461: * is volatile. In fact it's schedule() that is volatile in some 00462: * circumstances: when current->state = ZOMBIE, schedule() never 00463: * returns. 00464: * 00465: * In fact the natural way to do all this is to have the label and the 00466: * goto right after each other, but I put the fake_volatile label at 00467: * the start of the function just in case something /really/ bad 00468: * happens, and the schedule returns. This way we can try again. I'm 00469: * not paranoid: it's just that everybody is out to get me. 00470: */ 00471: goto fake_volatile; 00472: } 可想而知,进程在结束生命退出系统之前要释放其所有的资源。我们在前一节的 do_fork()中看到从 父进程“继承”的资源有存储空间、已打开文件、工作目录、信号处理表等等。相应地,这里就有 __exit_mm()、exit_files()、__exit_fs()以及__exit_signals()。可是,还有一种资源是不“继承”的,所以在 do_fork()中不会看到,那就是进程在用户空间建立和使用的“信号量”(semaphore)。这是一种用于进程 间通讯的资源,如果在调用 exit()之前还有信号量尚未撤销,那就也要把它撤销。这里有一个简单的准则, 就是看 task_struct 数据结构中的各个成分,如果一个成分是指针,在进程创建时以及运行过程中要为其 在内核中分配一个数据结构或缓冲区,而且这个指针又是通向这个数据结构或缓冲区的唯一途径,那就 一定要把它释放,否则就会造成内核的存储空间“泄漏”。例如,指针 sig 指向进程的信号处理表,这个 表所占的空间是专为 sig 分配的,指针 sig 就是进入这个表的唯一途径,所以必须释放。而指针 p_pptrLinux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 325 页,共 1481 页 指向父进程的 task_struct 结构,可是父进程的 task_struct 结构却并不是专门为子进程的 p_pptr 而分配的, 这个 p_pptr 并不是进入其父进程的 task_struct 的唯一途径,所以不能把这个数据结构也释放掉,否则其 他指向这个数据结构的指针就都“悬空”了。具体到用户空间信号量,当进程在用户空间创建和使用信 号量时,内核为为进程 task_struct 结构中的两个指针 semundo 和 semsleeping 分配缓冲区(sem_undo 数 据结构和 sem_queue 数据结构,详见“进程间通信”)。而且这两个指针就是进入这些数据结构的唯一途 径,所以必须把它们释放。函数 sem_exit()的代码在 ipc/sem.c 中: [sys_exit() > do_exit() > sem_exit()] 00966: /* 00967: * add semadj values to semaphores, free undo structures. 00968: * undo structures are not freed when semaphore arrays are destroyed 00969: * so some of them may be out of date. 00970: * IMPLEMENTATION NOTE: There is some confusion over whether the 00971: * set of adjustments that needs to be done should be done in an atomic 00972: * manner or not. That is, if we are attempting to decrement the semval 00973: * should we queue up and wait until we can do so legally? 00974: * The original implementation attempted to do this (queue and wait). 00975: * The current implementation does not do so. The POSIX standard 00976: * and SVID should be consulted to determine what behavior is mandated. 00977: */ 00978: void sem_exit (void) 00979: { 00980: struct sem_queue *q; 00981: struct sem_undo *u, *un = NULL, **up, **unp; 00982: struct sem_array *sma; 00983: int nsems, i; 00984: 00985: /* If the current process was sleeping for a semaphore, 00986: * remove it from the queue. 00987: */ 00988: if ((q = current->semsleeping)) { 00989: int semid = q->id; 00990: sma = sem_lock(semid); 00991: current->semsleeping = NULL; 00992: 00993: if (q->prev) { 00994: if(sma==NULL) 00995: BUG(); 00996: remove_from_queue(q->sma,q); 00997: } 00998: if(sma!=NULL) 00999: sem_unlock(semid); 01000: } 01001: 01002: for (up = ¤t->semundo; (u = *up); *up = u->proc_next, kfree(u)) { 01003: int semid = u->semid; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 326 页,共 1481 页 01004: if(semid == -1) 01005: continue; 01006: sma = sem_lock(semid); 01007: if (sma == NULL) 01008: continue; 01009: 01010: if (u->semid == -1) 01011: goto next_entry; 01012: 01013: if (sem_checkid(sma,u->semid)) 01014: goto next_entry; 01015: 01016: /* remove u from the sma->undo list */ 01017: for (unp = &sma->undo; (un = *unp); unp = &un->id_next) { 01018: if (u == un) 01019: goto found; 01020: } 01021: printk ("sem_exit undo list error id=%d\n", u->semid); 01022: goto next_entry; 01023: found: 01024: *unp = un->id_next; 01025: /* perform adjustments registered in u */ 01026: nsems = sma->sem_nsems; 01027: for (i = 0; i < nsems; i++) { 01028: struct sem * sem = &sma->sem_base[i]; 01029: sem->semval += u->semadj[i]; 01030: if (sem->semval < 0) 01031: sem->semval = 0; /* shouldn't happen */ 01032: sem->sempid = current->pid; 01033: } 01034: sma->sem_otime = CURRENT_TIME; 01035: /* maybe some queued-up processes were waiting for this */ 01036: update_queue(sma); 01037: next_entry: 01038: sem_unlock(semid); 01039: } 01040: current->semundo = NULL; 01041: } 如果当前进程正在睡眠(等待进入某个临界区),则其 task_struct 结构中的指针 semsleeping 指向所 在的队列。显然,现在不需要再等待了,所以把当前进程从这个队列中脱链。接着是一个 for 循环,料 理那些正在由当前进程所创建的用户空间信号量(即临界区)上操作的进程,告诉它们:信号量已经撤 销,临界区已经要“清场”并“关门大吉”,大家请回吧。建议读者在学习了“进程间通信”的有关内容 以后再回过来自己读一下这段代码。 再看__exit_mm()的代码(exit.c): [sys_exit() > do_exit() > __exit_mm()] Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 327 页,共 1481 页 00297: /* 00298: * Turn us into a lazy TLB process if we 00299: * aren't already.. 00300: */ 00301: static inline void __exit_mm(struct task_struct * tsk) 00302: { 00303: struct mm_struct * mm = tsk->mm; 00304: 00305: mm_release(); 00306: if (mm) { 00307: atomic_inc(&mm->mm_count); 00308: if (mm != tsk->active_mm) BUG(); 00309: /* more a memory barrier than a real lock */ 00310: task_lock(tsk); 00311: tsk->mm = NULL; 00312: task_unlock(tsk); 00313: enter_lazy_tlb(mm, current, smp_processor_id()); 00314: mmput(mm); 00315: } 00316: } 实际的存储空间释放是调用 mmput()完成的(代码在 fork.c 中),我们已经在前一节中读过它的代码, 这里要提醒读者的是这里对 mm_release()的调用。在 fork()和 execve()两节中,读者已经看到,当 do_fork() 时标志位 CLONE_VFORK 为 1 时,父进程在睡眠,等待子进程在一个信号量上执行一次 up()操作以后 才能回到用户空间运行,而子进程必须在释放其用户存储空间时执行这个操作,所以这里要通过 mm_release(),在这个信号量上执行一次 up()操作唤醒睡眠中的父进程。其代码已列出在 execve()一节中, 这里不再重复。 将一个进程的 task_struct 结构中的指针 mm 清成 0,这个进程便不再有用户空间了。 回到 do_exit()的代码中,其他几个用于释放资源的函数读者可自行阅读。对于 i386 处理器 exit_thread() 是个空函数。 接着,当前进程的状态就改成了 TASK_ZOMBIE,表示进程的生命已经结束,从此不再接受调度。 但是当前进程的残骸仍旧占用着最低限度的资源,包括其 task_struct 数据结构和系统空间堆栈所在的两 个页面。什么时候释放这两个页面呢?当前进程自己并不释放这两个页面,就像人们自己并不在临终前 注销自己的户口一样,而是调用 exit_notify()通知其父进程,让父进程料理后事。 为什么要这样安排,而不是让当前进程,也就是子进程自己照料这一切呢?有两个原因。首先,在 子进程的 task_struct 数据结构中还有不少有用的统计信息,让父进程来料理后事可以将这些统计信息并 入父进程的统计信息中而不会使这些信息丢失。其次,也许更重要的是,系统一旦进入多进程状态以后, 任何一刻都需要有个“当前进程”存在。读者在第 3 章中看到了,在中断服务程序以及异常处理程序中 都要用到当前进程的系统空间堆栈。如果子进程在系统调度另一个进程投入运行之前就把它的 task_struct 结构和系统空间堆栈释放,那就会造成一个空隙,如果恰好有一次中断或者异常在此空隙中发生就会造 成问题。诚然,中断是可以关闭的,可是异常却不能通过关中断来防止其发生,更何况还有“不可屏蔽 中断”哩。所以,子进程的 task_struct 结构和系统空间堆栈必须保存到另一个进程开始运行之后才能释 放。这样,让父进程料理后事就是一个合理的安排了。此外,这样安排也有利于使程序简化,否则的话 调度程序 schedule()就得要多考虑一些特殊情况了。让我们来看看 exit.c 中函数 exit_notify()的源代码: [sys_exit() > do_exit() > exit_notify()] 00323: /* Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 328 页,共 1481 页 00324: * Send signals to all our closest relatives so that they know 00325: * to properly mourn us.. 00326: */ 00327: static void exit_notify(void) 00328: { 00329: struct task_struct * p, *t; 00330: 00331: forget_original_parent(current); 就像人一样,所谓父进程也有“生父”和“养父”之分。在 task_struct 结构中有个指针 p_opptr 指向 其“original parent”也即生父,另外还有个指针 p_pptr 则指向其养父。一个进程在创建之初其生父和养 父是一致的,所以两个指针指向同一个父进程。但是,在运行中 p_pptr 可以暂时地改变。这种改变发生 在一个进程通过系统调用 ptrace()来跟踪另一个进程的时候,这时候被跟踪进程的 p_pptr 指针被设置成指 向正在跟踪它的进程,那个进程就暂时成了被跟踪进程的“养父”。而被跟踪进程的 p_opptr 指针却不变, 仍旧指向其生父。如果一个进程在其子进程之前“去世”的话,就要把它的子进程托付给某个进程。托 付给谁呢?如果父进程是一个线程,那就托付给同一线程组中的下一个线程,使子进程的 p_opptr 指向 这个线程。否则,就只好托付给系统中的 init 进程,所以这 init 进程就好像是孤儿院。由此可见,所谓 “original parent”也不是永远不变的,原因在于系统中的进程号 pid 以及用作 task_struct 结构的页面都是 周转使用的,所以实际上一来并没有保留这个记录的意义,二来技术上也有困难,现在,当前进程要 exit() 了,所以要将其所有的子进程都送进“孤儿院”,要不然到它们也要 exit()的时候就没有父进程来料理它 们的后事了。这酒是 331 行调用 forget_original_parent()的目的(exit.c)。 [sys_exit() > do_exit() > exit_notify() > forget_original_parent()] 00147: /* 00148: * When we die, we re-parent all our children. 00149: * Try to give them to another thread in our process 00150: * group, and if no such member exists, give it to 00151: * the global child reaper process (ie "init") 00152: */ 00153: static inline void forget_original_parent(struct task_struct * father) 00154: { 00155: struct task_struct * p, *reaper; 00156: 00157: read_lock(&tasklist_lock); 00158: 00159: /* Next in our thread group */ 00160: reaper = next_thread(father); 00161: if (reaper == father) 00162: reaper = child_reaper; 00163: 00164: for_each_task(p) { 00165: if (p->p_opptr == father) { 00166: /* We dont want people slaying init */ 00167: p->exit_signal = SIGCHLD; 00168: p->self_exec_id++; 00169: p->p_opptr = reaper; 00170: if (p->pdeath_signal) send_sig(p->pdeath_signal, p, 0); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 329 页,共 1481 页 00171: } 00172: } 00173: read_unlock(&tasklist_lock); 00174: } 这段程序中的 for_each_task 在 sched.h 中定义为: 00824: #define for_each_task(p) \ 00825: for (p = &init_task ; (p = p->next_task) != &init_task ; ) 就是说,搜索所有的 task_struct 数据结构,凡发现“生父”为当前进程者就将其 p_opptr 指针改成指 向 child_reaper,即 init 进程,并嘱其将来 exit()是要发一个 SIGCHLD 信号给 child_reaper,并根据当前 进程的 task_struct 结构中的 pdeath_signal 的设置向其发一个信号,告知生父的“噩耗”。 回到 exit_notify()中,下面就来处理由指针 p_pptr 所指向的“养父”进程了。这个父进程就好像是当 前进程的“法定监护人”,扮演着重要的角色(exit.c): [sys_exit() > do_exit() > exit_notify()] 00332: /* 00333: * Check to see if any process groups have become orphaned 00334: * as a result of our exiting, and if they have any stopped 00335: * jobs, send them a SIGHUP and then a SIGCONT. (POSIX 3.2.2.2) 00336: * 00337: * Case i: Our father is in a different pgrp than we are 00338: * and we were the only connection outside, so our pgrp 00339: * is about to become orphaned. 00340: */ 00341: 00342: t = current->p_pptr; 00343: 00344: if ((t->pgrp != current->pgrp) && 00345: (t->session == current->session) && 00346: will_become_orphaned_pgrp(current->pgrp, current) && 00347: has_stopped_jobs(current->pgrp)) { 00348: kill_pg(current->pgrp,SIGHUP,1); 00349: kill_pg(current->pgrp,SIGCONT,1); 00350: } 00351: 00352: /* Let father know we died 00353: * 00354: * Thread signals are configurable, but you aren't going to use 00355: * that to send signals to arbitary processes. 00356: * That stops right now. 00357: * 00358: * If the parent exec id doesn't match the exec id we saved 00359: * when we started then we know the parent has changed security 00360: * domain. 00361: * 00362: * If our self_exec id doesn't match our parent_exec_id then 00363: * we have changed execution domain as these two values started Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 330 页,共 1481 页 00364: * the same after a fork. 00365: * 00366: */ 00367: 00368: if(current->exit_signal != SIGCHLD && 00369: ( current->parent_exec_id != t->self_exec_id || 00370: current->self_exec_id != current->parent_exec_id) 00371: && !capable(CAP_KILL)) 00372: current->exit_signal = SIGCHLD; 00373: 00374: 00375: /* 00376: * This loop does two things: 00377: * 00378: * A. Make init inherit all the child processes 00379: * B. Check to see if any process groups have become orphaned 00380: * as a result of our exiting, and if they have any stopped 00381: * jobs, send them a SIGHUP and then a SIGCONT. (POSIX 3.2.2.2) 00382: */ 00383: 00384: write_lock_irq(&tasklist_lock); 00385: current->state = TASK_ZOMBIE; 00386: do_notify_parent(current, current->exit_signal); 00387: while (current->p_cptr != NULL) { 00388: p = current->p_cptr; 00389: current->p_cptr = p->p_osptr; 00390: p->p_ysptr = NULL; 00391: p->ptrace = 0; 00392: 00393: p->p_pptr = p->p_opptr; 00394: p->p_osptr = p->p_pptr->p_cptr; 00395: if (p->p_osptr) 00396: p->p_osptr->p_ysptr = p; 00397: p->p_pptr->p_cptr = p; 00398: if (p->state == TASK_ZOMBIE) 00399: do_notify_parent(p, p->exit_signal); 00400: /* 00401: * process group orphan check 00402: * Case ii: Our child is in a different pgrp 00403: * than we are, and it was the only connection 00404: * outside, so the child pgrp is now orphaned. 00405: */ 00406: if ((p->pgrp != current->pgrp) && 00407: (p->session == current->session)) { 00408: int pgrp = p->pgrp; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 331 页,共 1481 页 00409: 00410: write_unlock_irq(&tasklist_lock); 00411: if (is_orphaned_pgrp(pgrp) && has_stopped_jobs(pgrp)) { 00412: kill_pg(pgrp,SIGHUP,1); 00413: kill_pg(pgrp,SIGCONT,1); 00414: } 00415: write_lock_irq(&tasklist_lock); 00416: } 00417: } 00418: write_unlock_irq(&tasklist_lock); 00419: } 代码的作者在程序中加了不少注解,代码本身也并不复杂,所以我们基本上把它留给读者自己阅读, 不过要给予一些提示。 一个用户 login 到系统中以后,可能会启动许多不同的进程,所有这些进程都使用同一个控制终端 (或用来模拟一个终端的窗口)。这些使用同一个控制终端的进程属于同一个 session。此外,用户可以 在同一条 shell 命令或执行程序中启动多个进程,例如在命令“ls | wc -l”中就同时启动了两个进程,这 些进程形成一个“组”(session 与组是两个不同的概念)。每个 session 或进程组中都有一个为主的、最 早创建的进程,这个进程的 pid 就成为 session 和进程组的代号。如果当前进程与父进程属于不同的 session,不同的组,同时又是其所在的组与父进程之间唯一的纽带,那么一旦当前进程不存在以后,这 整个组就成了“孤儿”。在这样的情况下,按 POSIX 3.2.2.2 的规定要给这个进程组中所有的进程都先发 一个 SIGHUP 信号,然后再发一个 SIGCONT 信号,这是由 kill_pg()完成的。 我们讲过,exit_notify()最主要的目的是要给父进程发一个信号,让其知道子进程的生命已经结束而 来料理子进程的后事,这是通过 do_notify_parent()来完成的。其代码再 kernel/signal.c 中,程序很简单, 读者可自行阅读: [sys_exit() > do_exit() > exit_notify() > do_notify_parent()] 00732: /* 00733: * Let a parent know about a status change of a child. 00734: */ 00735: 00736: void do_notify_parent(struct task_struct *tsk, int sig) 00737: { 00738: struct siginfo info; 00739: int why, status; 00740: 00741: info.si_signo = sig; 00742: info.si_errno = 0; 00743: info.si_pid = tsk->pid; 00744: info.si_uid = tsk->uid; 00745: 00746: /* FIXME: find out whether or not this is supposed to be c*time. */ 00747: info.si_utime = tsk->times.tms_utime; 00748: info.si_stime = tsk->times.tms_stime; 00749: 00750: status = tsk->exit_code & 0x7f; 00751: why = SI_KERNEL; /* shouldn't happen */ Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 332 页,共 1481 页 00752: switch (tsk->state) { 00753: case TASK_STOPPED: 00754: /* FIXME -- can we deduce CLD_TRAPPED or CLD_CONTINUED? */ 00755: if (tsk->ptrace & PT_PTRACED) 00756: why = CLD_TRAPPED; 00757: else 00758: why = CLD_STOPPED; 00759: break; 00760: 00761: default: 00762: if (tsk->exit_code & 0x80) 00763: why = CLD_DUMPED; 00764: else if (tsk->exit_code & 0x7f) 00765: why = CLD_KILLED; 00766: else { 00767: why = CLD_EXITED; 00768: status = tsk->exit_code >> 8; 00769: } 00770: break; 00771: } 00772: info.si_code = why; 00773: info.si_status = status; 00774: 00775: send_sig_info(sig, &info, tsk->p_pptr); 00776: wake_up_parent(tsk->p_pptr); 00777: } 参数 tsk 指向当前进程的 task_struct 结构,只有当进程处于 TASK_ZOMBIE(正在 exit())或 TASK_STOPPED(被跟踪)时才允许调用 do_notify_parent()。从代码中可见,这里的所谓 parent 是指当 前进程的“养父”而不是“生父”,也就是由指针 p_pptr 所指而不是 p_opptr 所指的进程。在前面的 forget_original_parent()中已经把每个子进程的 p_opptr 改成了指向 child_reaper,而 notify_parent()中却是 向 p_pptr 所指出的进程发信号;那样,将来当子进程要 exit()时岂不是要向一个已经不存在了的父进程 发信号吗?不要紧,exit_notify()的代码中随后(392 行)就把子进程的 p_pptr 设置成与 p_opptr 相同。 进程之间都通过亲缘关系连接在一起形成“关系网”,所用的指针除 p_opptr 和 p_pptr 外,还有: p_cptr,指向子进程,这里的 c 表示“child”。p_cptr 与 p_pptr 是相对应的。当一个进程有多个子进 程时,p_cptr 指向其“最年轻的”,也就是最近创建的那个子进程。 p_ysptr,指向当前进程的“弟弟”,这里的 y 表示“younger”,而 s 表示“sibling”。 p_osptr,指向当前进程的“哥哥”,这里的 o 表示“older”。 这样,当前进程的所有子进程都通过 p_ysptr 和 p_osptr 连接在一起形成一个双链队列。队列中每一 个进程的 p_pptr 都指向当前进程,而当前进程的 p_optr 则指向队列中最后创建的子进程。有趣的是,子 进程在行事时只认其“养父”,而 p_opptr 所指的“生父”倒似乎无关紧要。当然,一个进程除身处这个 有亲属关系形成的队列中之外,同时也身处其他的队列中,所以 task_struct 结构中还有其他的 task_struct 指针,从而形成一个并不简单的“关系树”。进程是在创建的时候在 do_fork()中通过 SET_LINKS 进入这 个关系网的。SET_LINKS 的定义在 sched.h 中: 00813: #define SET_LINKS(p) do { \ 00814: (p)->next_task = &init_task; \ Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 333 页,共 1481 页 00815: (p)->prev_task = init_task.prev_task; \ 00816: init_task.prev_task->next_task = (p); \ 00817: init_task.prev_task = (p); \ 00818: (p)->p_ysptr = NULL; \ 00819: if (((p)->p_osptr = (p)->p_pptr->p_cptr) != NULL) \ 00820: (p)->p_osptr->p_ysptr = p; \ 00821: (p)->p_pptr->p_cptr = p; \ 00822: } while (0) 现在,是退出这个关系网的时候了。当 CPU 从 do_notify_parent()返回到 exit_notify()中时,所有子 进程的 p_opptr 都已指向 child_reaper,而 p_pptr 仍指向当前进程。随后的 while 循环将子进程队列中的 每个进程都转移到 child_reaper 的子进程队列中去,并使其 p_pptr 也指向 child_reaper。同时,对每个子 进程也要检查其所属的进程组是否成了“孤岛”。 如果当前进程是一个 session 中的主进程(current->leader 非 0),那就还要将整个 session 与其主控终 端的联系切断,并将该 tty 释放(注意,进程的 task_struct 结构中有个指针 tty 指向其主控终端)。函数 disassociate_ctty()的代码在 drivers/char/tty_io.c 中: [sys_exit() > do_exit() > exit_notofy() > disassociate_ctty()] 00560: /* 00561: * This function is typically called only by the session leader, when 00562: * it wants to disassociate itself from its controlling tty. 00563: * 00564: * It performs the following functions: 00565: * (1) Sends a SIGHUP and SIGCONT to the foreground process group 00566: * (2) Clears the tty from being controlling the session 00567: * (3) Clears the controlling tty for all processes in the 00568: * session group. 00569: * 00570: * The argument on_exit is set to 1 if called when a process is 00571: * exiting; it is 0 if called by the ioctl TIOCNOTTY. 00572: */ 00573: void disassociate_ctty(int on_exit) 00574: { 00575: struct tty_struct *tty = current->tty; 00576: struct task_struct *p; 00577: int tty_pgrp = -1; 00578: 00579: if (tty) { 00580: tty_pgrp = tty->pgrp; 00581: if (on_exit && tty->driver.type != TTY_DRIVER_TYPE_PTY) 00582: tty_vhangup(tty); 00583: } else { 00584: if (current->tty_old_pgrp) { 00585: kill_pg(current->tty_old_pgrp, SIGHUP, on_exit); 00586: kill_pg(current->tty_old_pgrp, SIGCONT, on_exit); 00587: } 00588: return; Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 334 页,共 1481 页 00589: } 00590: if (tty_pgrp > 0) { 00591: kill_pg(tty_pgrp, SIGHUP, on_exit); 00592: if (!on_exit) 00593: kill_pg(tty_pgrp, SIGCONT, on_exit); 00594: } 00595: 00596: current->tty_old_pgrp = 0; 00597: tty->session = 0; 00598: tty->pgrp = -1; 00599: 00600: read_lock(&tasklist_lock); 00601: for_each_task(p) 00602: if (p->session == current->session) 00603: p->tty = NULL; 00604: read_unlock(&tasklist_lock); 00605: } 那么,进程与主控终端的这种联系最初是怎样,以及在什么时候建立的呢?显然,在创建子进程时, 将父进程的 task_struct 结构复制给子进程的过程中把结构中的 tty 指针也复制了下来,所以子进程具有与 其父进程相同的主控终端。但是子进程可以通过 ioctl()系统调用来改变主控终端,也可以先将当前的主 控终端关闭然后再打开另一个 tty。不过,在此之前先得通过 setid()系统调用来建立一个新的人机交互分 组(session),并使得作此调用的进程成为该 session 的主进程(leader)。一个 session 的主进程与其主控 终端断绝关系意味着整个 session 中的进程都与之断绝了关系,所以要给同一 session 中的进程发出信号。 从此以后,这些进程就没有主控终端,成了“后台进程”。 再回到 do_exit()的代码中。当 CPU 完成了 exit_notify(),回到 do_exit()中时,剩下的大事只有一件 了,那就是 schedule(),即进程调度。前面讲过,do_exit()是不返回的,实际上使 do_exit()不返回的正是 这里的 schedule()。换言之,在这里的对 schedule()调用是不返回的。当然,在正常条件下对 schedule() 的调用是返回的,只不过返回的时机要延迟到本进程再次被调度而进入运行的时候。函数 schedule()按照 一定的准则从系统中挑选一个最适合的进程进入运行。这个进程有可能就是正在运行的进程本身,也可 能是另一个进程。如果不同的话,那就要进行切换。而当前进程虽然被剥夺了运行权,却维持其“运行 状态”,即 task->state 不变,等待下一次又在 schedule()中(由另一个进程引起,或者因为中断进入内核 后从系统空间返回用户空间之前)被选中时再继续运行,从而从 schedule()中返回。所以,什么时候从 schedule()返回取决于什么时候被进程调度选中而得以继续运行。可是,在这里,当前进程的 task->state 已经变成了 TASK_ZOMBIE,这个条件使它在 schedule()中永远不会被选中,所以就“黄鹤一去不复返 了”。而这里对 schedule()的调用,实际上(从 CPU 的角度看)也是返回的,只不过时返回到另一个进程 中去了,只是从当前进程的角度来看没有返回而已。不过,至此为止,当前进程还只是因为不会被选中 而不能返回,从理论上说只是无限推迟而已,其 task_struct 结构还是存在的。到父进程收到子进程发来 的信号而来料理后事,将子进程的 task_struct 结构释放之时,子进程就最终从系统中消失了。在我们这 个情景中,父进程正在 wait4()中等着哩。 像其他系统调用一样,wait4()在内核中的入口是 sys_wait4(),见 exit.c 中的代码: 00487: asmlinkage long sys_wait4(pid_t pid,unsigned int * stat_addr, int options, struct rusage * ru) 00488: { 00489: int flag, retval; 00490: DECLARE_WAITQUEUE(wait, current); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 335 页,共 1481 页 00491: struct task_struct *tsk; 00492: 00493: if (options & ~(WNOHANG|WUNTRACED|__WNOTHREAD|__WCLONE|__WALL)) 00494: return -EINVAL; 00495: 00496: add_wait_queue(¤t->wait_chldexit,&wait); 参数 pid 为某一个子进程的进程号。 首先,在当前进程的系统空间堆栈中通过 DECLARE_WAITQUEUE 分配空间并建立一个 wait_queue_t 数据结构。有关的宏定义和数据结构都是在 include/linux/wait.h 中定义的: 00046: struct __wait_queue { 00047: unsigned int flags; 00048: #define WQ_FLAG_EXCLUSIVE 0x01 00049: struct task_struct * task; 00050: struct list_head task_list; 00051: #if WAITQUEUE_DEBUG 00052: long __magic; 00053: long __waker; 00054: #endif 00055: }; 00056: typedef struct __wait_queue wait_queue_t; 00092: struct __wait_queue_head { 00093: wq_lock_t lock; 00094: struct list_head task_list; 00095: #if WAITQUEUE_DEBUG 00096: long __magic; 00097: long __creator; 00098: #endif 00099: }; 00100: typedef struct __wait_queue_head wait_queue_head_t; 00101: 00102: #if WAITQUEUE_DEBUG 00103: # define __WAITQUEUE_DEBUG_INIT(name) \ 00104: , (long)&(name).__magic, 0 00105: # define __WAITQUEUE_HEAD_DEBUG_INIT(name) \ 00106: , (long)&(name).__magic, (long)&(name).__magic 00107: #else 00108: # define __WAITQUEUE_DEBUG_INIT(name) 00109: # define __WAITQUEUE_HEAD_DEBUG_INIT(name) 00110: #endif 00111: 00112: #define __WAITQUEUE_INITIALIZER(name,task) \ 00113: { 0x0, task, { NULL, NULL } __WAITQUEUE_DEBUG_INIT(name)} 00114: #define DECLARE_WAITQUEUE(name,task) \ 00115: wait_queue_t name = __WAITQUEUE_INITIALIZER(name,task) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 336 页,共 1481 页 也就是说,sys_wait4()一开头就在当前进程的系统堆栈上分配一个 wait_queue_t 数据结构(名为 wait),结构中的 compiler_warning 为 0x1234567,指针 task 指向当前进程的 task_struct,而 list_head 结 构 task_list 中的两个指针均为 NULL。由于这个数据结构建立在当前进程的系统空间堆栈中,一旦从 sys_wait4()返回,这个数据结构就不复存在了。与此相应,在进程的 task_struct 中有个 wait_queue_head_t 数据结构 wait_chldexit 用于这个目的。 然后,通过 add_wait_queue()将这个数据结构(wait)加入到当前进程的 wait_chldexit 队列中。这样 做的作用在下面重温了 do_notify_parent()的代码以后就会清楚。接着,就进入了一个循环,这是一个不 小的循环(exit.c:sys_wait4()): [sys_wait4()] 00497: repeat: 00498: flag = 0; 00499: current->state = TASK_INTERRUPTIBLE; 00500: read_lock(&tasklist_lock); 00501: tsk = current; 00502: do { 00503: struct task_struct *p; 00504: for (p = tsk->p_cptr ; p ; p = p->p_osptr) { 00505: if (pid>0) { 00506: if (p->pid != pid) 00507: continue; 00508: } else if (!pid) { 00509: if (p->pgrp != current->pgrp) 00510: continue; 00511: } else if (pid != -1) { 00512: if (p->pgrp != -pid) 00513: continue; 00514: } 00515: /* Wait for all children (clone and not) if __WALL is set; 00516: * otherwise, wait for clone children *only* if __WCLONE is 00517: * set; otherwise, wait for non-clone children *only*. (Note: 00518: * A "clone" child here is one that reports to its parent 00519: * using a signal other than SIGCHLD.) */ 00520: if (((p->exit_signal != SIGCHLD) ^ ((options & __WCLONE) != 0)) 00521: && !(options & __WALL)) 00522: continue; 00523: flag = 1; 00524: switch (p->state) { 00525: case TASK_STOPPED: 00526: if (!p->exit_code) 00527: continue; 00528: if (!(options & WUNTRACED) && !(p->ptrace & PT_PTRACED)) 00529: continue; 00530: read_unlock(&tasklist_lock); 00531: retval = ru ? getrusage(p, RUSAGE_BOTH, ru) : 0; 00532: if (!retval && stat_addr) Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 337 页,共 1481 页 00533: retval = put_user((p->exit_code << 8) | 0x7f, stat_addr); 00534: if (!retval) { 00535: p->exit_code = 0; 00536: retval = p->pid; 00537: } 00538: goto end_wait4; 00539: case TASK_ZOMBIE: 00540: current->times.tms_cutime += p->times.tms_utime + p->times.tms_cutime; 00541: current->times.tms_cstime += p->times.tms_stime + p->times.tms_cstime; 00542: read_unlock(&tasklist_lock); 00543: retval = ru ? getrusage(p, RUSAGE_BOTH, ru) : 0; 00544: if (!retval && stat_addr) 00545: retval = put_user(p->exit_code, stat_addr); 00546: if (retval) 00547: goto end_wait4; 00548: retval = p->pid; 00549: if (p->p_opptr != p->p_pptr) { 00550: write_lock_irq(&tasklist_lock); 00551: REMOVE_LINKS(p); 00552: p->p_pptr = p->p_opptr; 00553: SET_LINKS(p); 00554: do_notify_parent(p, SIGCHLD); 00555: write_unlock_irq(&tasklist_lock); 00556: } else 00557: release_task(p); 00558: goto end_wait4; 00559: default: 00560: continue; 00561: } 00562: } 00563: if (options & __WNOTHREAD) 00564: break; 00565: tsk = next_thread(tsk); 00566: } while (tsk != current); 00567: read_unlock(&tasklist_lock); 00568: if (flag) { 00569: retval = 0; 00570: if (options & WNOHANG) 00571: goto end_wait4; 00572: retval = -ERESTARTSYS; 00573: if (signal_pending(current)) 00574: goto end_wait4; 00575: schedule(); Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 338 页,共 1481 页 00576: goto repeat; 00577: } 00578: retval = -ECHILD; 00579: end_wait4: 00580: current->state = TASK_RUNNING; 00581: remove_wait_queue(¤t->wait_chldexit,&wait); 00582: return retval; 00583: } 这个由 goto 实现的循环要到当前进程被调度运行,并且下列条件之一得到满足时才结束(见代码中 的“goto end_wait4”语句): 所等待的子进程的状态变成 TASK_STOPPED 或 TASK_ZOMBIE; 所等待的子进程存在,可是不在上列两个状态,而调用参数 options 中的 WNOHANG 标志位为 1,或者当前进程收到了其他的信号; 进程号为 pid 的那个进程根本不存在,或者不是当前进程的子进程。 否则,当前进程将其自身的状态设成 TASK_INTERRUPTIBLE(见 499 行)并在 575 行调用 schedule() 进入睡眠让别的进程先运行。当该进程因收到信号而被唤醒,并且受到调度从 schedule()返回时,就又经 由 576 行的 goto 语句转回 repeat,再次通过一个 for 循环扫描其子进程队列,看看所等待的子进程的状 态是否满足条件。这里的 for 循环扫描一个进程的所有子进程,从最年轻的子进程开始沿着由各个 task_struct 结构中的指针 p_osptr 所形成的链扫描,找寻与所等待对象的 pid 相符的子进程、或符合其他 一些条件的子进程。这个 for 循环又嵌套在一个 do_while 循环中。为什么要有这个外层的 do_while 循环 呢?这是因为当前进程可能是一个线程,而所等待的对象实际上是由同一个进程克隆出来的另一个线程 的子进程,所以要通过这个 do_while 循环来检查同一个 thread_group 中所有线程的子进程。代码中的 next_thread()从同一个 thread_group 队列中找到下一个线程的 task_struct 结构,并使局部量 tsk 指向这个 结构。在我们这个情景中,当父进程调用 wait4()而第一次扫描其其进程队列时,该子进程尚未在运行, 所以通过 schedule()进入睡眠。当子进程 exit()时,会向父进程发一个信号,从而将其唤醒。怎么唤醒呢? 我们在前面看到,子进程在 exit_notify()中通过 do_notify_parent()向父进程发送信号。这个函数准备下一 个 siginfo 数据结构,然后调用 send_sig_info()将其发送给父进程,并调用 wake_up_process()将父进程唤 醒。对 send_sig_info()的代码我们将在“进程间通信”的信号一节中介绍。而 wake_up_process(),则把 父进程的状态从 TASK_INTERRUPTIBLE 改成 TASK_RUNNING,并将其转移到可执行队列中,使 schedule()能够“看”到父进程而可以调度其运行。 当父进程因子进程在 exit()向其发送信号而被唤醒时,就转回到前面 sys_wait4()中的 repeat 处,又一 次扫描其子进程队列。这一次,子进程的状态已经改成 TASK_ZOMBIE 了,所以父进程在将子进程的用 户空间运行的时间和系统空间运行的时间两项统计数据合并入其自身的统计数据中。然后,在典型的条 件下,就调用 release_task()将子进程残存的资源,就是其 task_struct 结构和系统空间堆栈,全都释放 (exit.c): [sys_wait4() > release_task()] 00025: static void release_task(struct task_struct * p) 00026: { 00027: if (p != current) { 00028: #ifdef CONFIG_SMP 00029: /* 00030: * Wait to make sure the process isn't on the 00031: * runqueue (active on some other CPU still) 00032: */ 00033: for (;;) { Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 339 页,共 1481 页 00034: task_lock(p); 00035: if (!p->has_cpu) 00036: break; 00037: task_unlock(p); 00038: do { 00039: barrier(); 00040: } while (p->has_cpu); 00041: } 00042: task_unlock(p); 00043: #endif 00044: atomic_dec(&p->user->processes); 00045: free_uid(p->user); 00046: unhash_process(p); 00047: 00048: release_thread(p); 00049: current->cmin_flt += p->min_flt + p->cmin_flt; 00050: current->cmaj_flt += p->maj_flt + p->cmaj_flt; 00051: current->cnswap += p->nswap + p->cnswap; 00052: /* 00053: * Potentially available timeslices are retrieved 00054: * here - this way the parent does not get penalized 00055: * for creating too many processes. 00056: * 00057: * (this cannot be used to artificially 'generate' 00058: * timeslices, because any timeslice recovered here 00059: * was given away by the parent in the first place.) 00060: */ 00061: current->counter += p->counter; 00062: if (current->counter >= MAX_COUNTER) 00063: current->counter = MAX_COUNTER; 00064: free_task_struct(p); 00065: } else { 00066: printk("task releasing itself\n"); 00067: } 00068: } 这里通过 unhash_process()把子进程的 task_struct 结构从杂凑表队列中摘除,然后把子进程的其他几 项统计信息也合并入父进程。至于 release_thread()只是检查进程的 LDT(如果有的话)是否确已释放。 最后,就调用 free_task_struct()将 task_struct 结构和系统空间堆栈所占据的两个物理页面释放。 在 sys_wait4()中还有个特殊情况需要考虑,那就是万一子进程的 p_opptr 与 p_pptr 不同,也就是说 其“养父”与“生父”不同。如前所述,进程在 exit()时,do_notify_parent()的对象是其“养父”,但是当 “生父”与“养父”不同时,其“生父”可能也在等待,所以将子进程的 p_pptr 指针设置成与 p_opptr 相同,并通过 REMOVE_LINKS 将其 task_struct 从其“养父”的队列中脱离出来,再通过 SET_LINKS 把它归还给“生父”,重新挂入其“生父”的队列。然后,给其“生父”发一信号,让它自己来处理。 此外,根据当前进程在调用 wait4()时的要求,还可能要把一些状态信息和统计信息通过 put_user() 复制到用户空间中。如果复制失败的话,那暂时就不能将子进程的 task_struct 结构是放了(这里的“goto Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 340 页,共 1481 页 end_wait4”跳过了对 release_task()的调用)。在这种情况下,系统中会留下子进程的“尸体”,用户通过 “ps”命令来观察系统中的进程状态时,会看到有个进程的状态为“ZOMBIE”。读者在前面看到:在 exit_notify()中,当父进程要结束生命前为其子进程“托孤”时,还要看一下子进程的状态是否 TASK_ZOMBIE,若是的话,就要替它调用 do_notify_parent()给新的“养父”发一信息,就是这个原因。 至此,在执行了 release_task()以后,子进程就最终“灰飞烟灭”,从系统中消失了。 可是,要是父进程不在 wait4()中等待呢?那也不要紧。读者在第 3 章中已经看到,每当进程从系统 调用、中断或异常返回时,都要检查一下是否有信号等待处理,如有的话就转入 entry.S 中的 signal_return 处调用 do_signal()。而 do_signal()中有一个片段为(在 arch/i386/kernel/signal.c 中): 00643: ka = ¤t->sig->action[signr-1]; 00644: if (ka->sa.sa_handler == SIG_IGN) { 00645: if (signr != SIGCHLD) 00646: continue; 00647: /* Check for SIGCHLD: it's special. */ 00648: while (sys_wait4(-1, NULL, WNOHANG, NULL) > 0) 00649: /* nothing */; 00650: continue; 00651: } 可见父进程在收到 SIGCHLD 信号后还会被动地来调用 sys_wait4(),此时的调用参数 pid 为-1,表 示 同一个进程组中的任何一个子进程都在处理之列(见 sys_wait4()的 for 循环中对参数 pid 的比对)。当 然 , 如果父进程已经为 SIGCHLD 信号设置了其他的处理程序,那就另作别论了。 读者也许还会问,怎样才能保证一定会有系统调用、中断或异常来迫使其父进程执行 do_signal()呢? 万一父进程在运行时既不作系统调用,也不访问外设,更没有任何操作引起异常呢?别忘记时钟中断是 周期性地发生的,要不然就连调度也有可能不会发生了,正因为如此,时钟中断才被看作是系统的“心 跳”。 4.6. 进程的调度与切换 在多进程的操作系统中,进程调度是一个全局性的、关键性的问题,它对系统的总体设计、系统的 实现、功能设置以及各方面的性能都有着决定性的影响。根据调度结果所作的进程切换的速度,也是衡 量一个操作系统性能的重要指标。进程调度机制的设计,还对系统复杂性有着极大的影响,常常会由于 实现的复杂程度而在功能与性能方面作出必要的权衡和让步。一个好的系统的进程调度机制,要兼顾三 种不同应用的需要: 交互式应用。在这种应用中,着重于系统的响应速度,使共用一个系统的各个用户(以及各个 应用程序)都能感觉到自己是在独占地使用一个系统。特别是,当系统中有大量的进程共存时, 仍要能保证每个用户都有可以接受的响应速度而并不感到明显的延迟。根据测定,当这种延迟 超过 150 毫秒时,使用者就会明显地感觉到。 批处理应用。批处理应用往往是作为“后台作业”运行的,所以对响应速度并无要求,但是完 成一个作业所需的时间仍是一个重要的因素,考虑的是“平均速度”。 实时应用。这是时间性最强的应用,不但要考虑进程执行的平均速度,还要考虑“即时速度”; 不但要考虑响应速度(即从某个事件发生到系统对此作出反应并开始执行有关程序之间所需的 时间),还要考虑有关程序(常常在用户空间)能否在规定的时间内执行完。在实时应用中,注 重的是对程序执行的“可预测性”。 另外,进程调度的机制还要考虑到“公证性”,让系统中的所有进程都有机会向前推进,尽管其进度 各有不同,并最终受到 CPU 速度和负载的影响。更重要的是,还要防止“死锁”的发生,以及防止对 CPU 能力的不合理使用,也就是说要防止 CPU 尚有能力且有进程等着执行,却由于某种原因而长时间 得不到执行的情况。一旦这些情况发生时,调度机制还应能识别与化解。可以说,关于进程调度的研究Linux 内核源代码情景分析 是整个操作系统理论的核心。不过,本书的目的在于对 Linux 内核的剖析和解释,而不在于理论方面的 深入探讨,有兴趣的读者可以阅读操作系统方面的专著。 为了满足上述的目标,在设计一个进程调度机制时要考虑的具体问题主要有: 1. 调度的时机:在什么情况下、什么时候进行调度。 2. 调度的“政策”(policy):根据什么准则挑选下一个进入运行的进程。 3. 调度的方式:是“可剥夺”(preemptive)还是“不可剥夺”(nonpreemptive)。当正在运行的进 程并不自愿暂时放弃对 CPU 的“使用权”时,是否可以强制性地暂时剥夺其使用权,停止其运 行而给其他的进程一个机会。如果是可剥夺,那么是否在任何条件下都可剥夺,有没有例外? 这三个问题,特别是第一和第三个问题,是紧密结合在一起的。例如,如果调度的性质是绝对地不 可剥夺,也就是说坚持完全自愿的原则,那么调度的时机也就基本上决定了,只能在当前进程自愿调度 的时候才能进行调度。相应地,就要设计一个“原语”,即系统调用,让进程可以表达自己的这个意愿。 同时,还要考虑,如果一个进程因陷入了死循环而抓住 CPU 不放该怎么办。 这里要说明一下,在中文里也许应该把是否可以剥夺称为“政策”,但是在英文的书刊中已经把调度 准则或标准称为“policy”,所以我们只好把这称为“方式”,以免引起不必要的混淆。 进一步,如果调度的性质是有条件地可剥夺,那么,在什么情况下可剥夺就成了重要的问题。例如, 可以把时间划分成时间片,每个时间片来一次时钟中断,而调度可以在时间片中断时进行。按进程的优 先级别的高低进行调度,每个时间片一次,除此之外就只能在进程自愿时才可进行调度。这样,只要时 间片划分得当,交互式应用的要求就可以满足了。但是,这样的系统显然不适合实时的应用。因为,有 可能发生“急惊风遇上慢郎中”的情况,优先级别高的进程急着要运行,而正在运行中的进程偏偏“觉 悟不高”,不懂得“先人后己”,别的进程只好干等着它把时间片用完而坐失良机。从另一个角度讲,这 也取决于技术的发展,特别是 CPU 的速度。例如,就在这么一个系统中,如果可以把时间片分小到 0.5 毫秒,而 CPU 仍能在这么短的时间里作足够多的事,那么对一般的实时应用来说可能还是能满足要求的, 虽然从整体上讲 CPU 用在调度与切换上的开销所占的比例上升了。 那么,Linux 内核的调度机制到底是什么样的呢?我们还是分成三个方面来回答这个问题。 在往下叙述之前,此处先给出一个进程的状态转换关系示意图(图 4.4)。 TASK_RUNNING 就绪 TASK_UNINTERRUPTIBLE 深度睡眠 占有CPU 执行 TASK_INTERRUPTIBLE 浅度睡眠 TASK_STOPPED 暂停 TASK_ZOMBIE 死亡但户口未注销 fork() schedule() 时间片到 schedule() ptrace() do_exit() schedule() schedule() sleep_on() interruptible_sleep_on() 等待资源到位 等待资源到位 等待资源到位 wake_up() 等待资源到位 wake_up_interruptible() 或收到信号 wake_up() 收到信号SIG_CONT wake_up() 图4.4 进程状态转换图 先看调度的时机。 2006-12-31 版权所有,侵权必究 第 341 页,共 1481 页 Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 342 页,共 1481 页 首先,自愿的调度随时可以进行。在内核里面,一个进程可以通过 schedule()启动一次调度,当然也 可以在调用 schedule() 之前,将本进程的状态设置成为 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE,暂时放弃运行而进入睡眠。在用户空间中,则可以通过系统调用 pause() 来达到同样的目的。也可以为这种自愿的暂时放弃运行加上时间限制。在内核中有 schedule_timeout()用 于此项目的;相应地,在用户空间则可以通过系统调用 nanosleep()而达到目的(注意,sleep()是库函数, 不是系统调用,但最终要通过系统调用来完成)。这里要指出:从应用的角度看,只有在用户空间自愿放 弃运行这一举动是可见的;而在内核中自愿放弃运行则是不可见的,它隐藏在其他可能受阻的系统调用 中。几乎所有涉及到外设的系统调用,如 open()、read()、write()和 select()等,都是可能受阻的。 除此之外,调度还可以非自愿地,即强制地发生在每次从系统调用返回的前夕,以及每次从中断或 异常处理返回到用户空间的前夕。注意,这里“返回到用户空间”几个字是关键性的,因为这意味着只 有在用户空间(当 CPU 在用户空间运行时)发生的中断或异常才会引起调度。关于这一点我们在第 3 章中讲述中断返回时提到过,但是有必要在此加以强调,并重温 entry.S 中的两个片段: 00260: ret_from_exception: 00261: #ifdef CONFIG_SMP …………………… 00267: #else 00268: movl SYMBOL_NAME(irq_stat),%ecx # softirq_active 00269: testl SYMBOL_NAME(irq_stat)+4,%ecx # softirq_mask 00270: #endif 00271: jne handle_softirq 00272: 00273: ENTRY(ret_from_intr) 00274: GET_CURRENT(%ebx) 00275: movl EFLAGS(%esp),%eax # mix EFLAGS and CS 00276: movb CS(%esp),%al 00277: testl $(VM_MASK | 3),%eax # return to VM86 mode or non-supervisor? 00278: jne ret_with_reschedule 00279: jmp restore_all 277 行中寄存器 EAX 的内容有两个来源,其最低的字节来自保存在堆栈中的进入中断前夕段寄存器 CS 的内容,最低的两位表示当时的运行级别。从代码中可以看到,转入 ret_with_reschedule 的条件为中 断(或异常)发生前 CPU 的运行级别为 3,即用户状态(我们在这里不关心 VM_MASK,那是为 VM86 模式而设置的)。这一点对于系统的设计和实现有很重要的意义。因为那意味着当 CPU 在内核中运行时 无需考虑强制调度的可能性。发生在系统空间的中断或异常当然是可能的,但是这种中断或异常不会引 起调度。这就使内核的实现简化了,早期的 Unix 内核正是靠这个前提来简化其设计与实现的。否则的话, 内核中所有可能为一个以上进程共享的变量和数据结构就全都要通过互斥机制(如信号量)加以保护, 或者说都要放在临界区中。不过,随着多处理器 SMP 系统结构的出现以及日益广泛的采用,这种简化正 在失去重要性。在多处理器 SMP 系统中(见“多处理器 SMP 系统结构”一章),尽管在内核中由于不会 发生调度而无需考虑互斥,但却不能不考虑在另一个处理器上运行的进程访问共享资源的可能性。这样, 不管在同一个 CPU 上是否可能在内核中发生调度,所有可能为多个进程(可能在不同的 CPU 上运行) 共享的变量和数据结构,都得保护起来。这就是读者在阅读代码时看到那么多 up()、down()等信号量操 作或加锁操作的原因。Linux 内核中一般将用于多处理器 SMP 结构的代码放在条件编译“#ifdef CONFIG_SMP”中,但是却没有把这些用于互斥保护的操作也放在条件编译中。究其原因,一来可能是 太多了,加不胜加,再说在单处理器条件下的运行时开销也不大;二来也是为日后对调度机制的改进奠 定基础。 那么 Linux 现行的这种调度机制有什么缺点或不足,为什么可能会有日后的改进呢?例如:在实时Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 343 页,共 1481 页 的应用中,某个中断的发生可能不但要求迅速的中断服务,还要求迅速地调度有关进程进入运行,以便 在较高的层次上,也就是在用户空间中对事件进行及时的处理。可是,如果这样的中断发生在内核中时, 本次中断返回是不会引起调度的,而要到最初使 CPU 从用户空间进入内核的那次系统调用或中断(或异 常)返回时才会发生调度。倘若内核中的这段代码恰好需要较长时间完成的话,或者连续又发生几次中 断的话,就可能将调度过分地推迟了。良好的内核代码可以减轻这个问题,但并不能从根本上解决问题。 所以这是个设计问题而不是实现问题。只是,随着 CPU 速度变得越来越快,这个问题渐渐地变得不那么 重要了。 注意,“从系统空间返回到用户空间”只是发生调度的必要条件,而不是充分条件。具体是否发生调 度还要看有无此种要求,看一下 entry.S 中的这一段代码: 00217: ret_with_reschedule: 00218: cmpl $0,need_resched(%ebx) 00219: jne reschedule 00220: cmpl $0,sigpending(%ebx) 00221: jne signal_return 00222: restore_all: 00223: RESTORE_ALL …………………… 00287: reschedule: 00288: call SYMBOL_NAME(schedule) # test 00289: jmp ret_from_sys_call 可见,只有在当前进程的 task_struct 结构中的 need_resched 字段为非 0 时才会转到 reschedule 处调 用 schedule()。那么,谁来设置这个字段呢?当然是内核,从用户空间是访问不到进程的 task_struct 结构 的。可是,内核在什么情况下设置这个字段呢?除当前进程通过系统调用自愿让出运行以及在系统调用 中因某种原因受阻以外,主要就是当因某种原因唤醒一个进程的时候,以及在时钟中断服务程序发现当 前进程已经连续运行太久的时候。 再看调度的方式。 Linux 内核的调度方式可以说是“有条件的可剥夺”方式。当进程在用户空间运行时,不管自愿不 自愿,一旦有必要(例如已经运行了足够长的时间),内核就可以暂时剥夺其运行而调度其他进程进入运 行。可是,一旦进程进入了内核空间,或者说进入了“长官”(supervisor)模式,那就好像是进入了“高 层”而“刑不上大夫”了。这时候,尽管内核知道应该要调度了,但实际上却不会发生,一直要到该进 程即将“下台”,也就是回到用户空间的前夕才能剥夺其运行。所以,Linux 的调度方式从原则上来说是 可剥夺的,可是实际运行中由于调度时机的限制而变成了有条件的。正因为这样,有的书说 Linux 的调 度是可剥夺的,有的却说是不可剥夺的,甚至同一本书中有时候说是可剥夺的,有时候又说是不可剥夺 的,其原因盖出于此。 那么,剥夺式的调度发生在什么时候呢?同样也是发生在进程从系统空间(包括因系统调用进入内 核)返回用户空间的前夕。 至于调度政策,基本上是从 Unix 继承下来的以优先级为基础的调度。内核为系统中每个进程计算出 一个反映其运行“资格”的权值,然后挑选权值最高的进程投入运行。在运行过程中,当前进程的资格 随时间而递减,从而在下一次调度的时候原来资格较低的进程可能就更有资格运行了。到所有进程的资 格都变成了 0 时,就重新计算一次所有进程的资格。资格的计算主要是以优先级为基础的,所以说是以 优先级为基础的调度。 但是,为了适应各种不同应用的需要,内核在此基础上实现了三种不同的政策:SCHED_FIFO、 SCHED_RR 和 SCHED_OTHER。每个进程都有自己适用的调度政策,并且,进程还可以通过系统调用 sched_setscheduler()设定自己适用的调度政策。其中 SCHED_FIFO 适合于时间性要求比较强、但每次运 行所需的时间比较短的进程,实时的应用大都具有这样的特点。SCHED_RR 中的“RR”表示“Round Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 344 页,共 1481 页 Robin”,是轮流的意思,这种政策适合比较大、也就是每次运行需时较长的进程。而除此二者之外的 SCHED_OTHER,则为传统的调度政策,比较适合于交互式的分时应用。 既然每个进程都有自己的适用调度政策,内核怎样在适用不同调度政策的进程之间决定取舍呢?实 际上最后还是都归结到各个进程的权值,只不过是在计算资格时把适用政策也考虑进去,就好像考大学 时符合某些特殊条件的考生可以获得加分一样。同时,对于适用不同政策的进程的优先级别也加了限制。 我们将结合代码更深入地讨论这些政策间的差异和作用。 下面,我们就结合代码深入到调度和切换的过程中去。在本节中我们先看一个主动调度,也就是当 前进程自愿调用 schedule()暂时放弃运行的情景。在 exit()一节中,读者已经看到一个正在结束生命的进 程在 do_exit()中的最后一件事就是调用 schedule(),我们就从这里接着往下看,深入到 schedule()里面去, 其代码在 kernel/sched.c 中: 00498: /* 00499: * 'schedule()' is the scheduler function. It's a very simple and nice 00500: * scheduler: it's not perfect, but certainly works for most things. 00501: * 00502: * The goto is "interesting". 00503: * 00504: * NOTE!! Task 0 is the 'idle' task, which gets called when no other 00505: * tasks can run. It can not be killed, and it cannot sleep. The 'state' 00506: * information in task[0] is never used. 00507: */ 00508: asmlinkage void schedule(void) 00509: { 00510: struct schedule_data * sched_data; 00511: struct task_struct *prev, *next, *p; 00512: struct list_head *tmp; 00513: int this_cpu, c; 00514: 00515: if (!current->active_mm) BUG(); 00516: need_resched_back: 00517: prev = current; 00518: this_cpu = prev->processor; 00519: 00520: if (in_interrupt()) 00521: goto scheduling_in_interrupt; 00522: 00523: release_kernel_lock(prev, this_cpu); 00524: 00525: /* Do "administrative" work here while we don't hold any locks */ 00526: if (softirq_active(this_cpu) & softirq_mask(this_cpu)) 00527: goto handle_softirq; 00528: handle_softirq_back: 00529: 这个函数中使用了许多 goto 语句。对于这么一个非常频繁地执行的函数,把运行效率放在第一位是 可以理解的,只是给阅读和理解带来了一些困难。 以前我们讲过,在 task_struct 结构中有两个 mm_struct 指针。一个是 mm,只想代表着进程的虚存(用Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 345 页,共 1481 页 户)空间的数据结构。如果当前线程实际上是个内核线程,那就没有用户空间,所以其 mm 指针为 0, 运行时就要暂时借用在它之前运行的那个进程的 active_mm。所以正在运行中的进程,也即当前进程, 在进入 schedule()时其 active_mm 一定不能是 0(见 515 行)。后面我们还要回到这个话题上。 以前讲过,对 schedule()只能由进程在内核中主动调用,或者在当前进程从系统空间返回用户空间的 前夕被动地发生,而不能在一个中断服务程序的内部发生。即使一个中断服务程序有调度的要求,也只 能通过把当前进程的 need_resched 字段设成 1 来表达这种要求,而不能直接调用 schedule()。读者也许会 问,我们在第 3 章中看到,在执行中断服务程序的时候是允许开中断的,如果在执行过程中发生了嵌套 中断,那么当从嵌套的中断返回时不是也要调用 schedule()吗?那不就等于是在中断服务程序的内部调用 了这个函数吗?其实,从嵌套的中断返回时不会调用 schedule(),因为此时的中断返回并不是返回到用户 空间。还要注意:因中断而进入内核并不等于已经进入了某个中断服务程序,而当 CPU 要从系统空间返 回用户空间之时则已经离开了具体的中断服务程序,详见第 3 章。所以,如果在某个中断服务程序内部 调用 schedule(),那一定是有问题了,所以转向 scheduling_in_interrupt。接着看 sched.c: [schedule()] 00686: scheduling_in_interrupt: 00687: printk("Scheduling in interrupt\n"); 00688: BUG(); 00689: return; 内核对此的反应是显示或者在/var/log/messgaes 文件末尾添加一条出错信息,然后执行一个宏操作 BUG,这是在 include/asm-i386/page.h 中定义的: 00085: /* 00086: * Tell the user there is some problem. Beep too, so we can 00087: * see^H^H^Hhear bugs in early bootup as well! 00088: */ 00089: #define BUG() do { \ 00090: printk("kernel BUG at %s:%d!\n", __FILE__, __LINE__); \ 00091: __asm__ __volatile__(".byte 0x0f,0x0b"); \ 00092: } while (0) 这里的奥妙之处是在 91 行中准备下了两个字节 0x0f 和 0x0b,让 CPU 当作指令去执行。可是由这 两个字节构成的是非法指令,因而会产生一次“非法指令(invalid_op)”异常,使 CPU 执行 do_invalid_op()。 当然,在实际运行中这样的错误(在中断服务程序或 bf 函数的内部调用 schedule())是不会发生的,除 非正在调试用户自己编写的中断服务程序。 我们回过头来继续往下看 schedule(),这里的 523 行的 release_kernel_lock()对于 i386 单处理器系统 为空语句,所以接着就是检查是否有内核软中断服务请求在等待(见第3章)。如果有就转入handle_softirq() 为这些请求服务: [schedule()] 00675: handle_softirq: 00676: do_softirq(); 00677: goto handle_softirq_back; 从执行 softirq()队列完毕以后继续往下看: [schedule()] 00528: handle_softirq_back: 00529: 00530: /* 00531: * 'sched_data' is protected by the fact that we can run 00532: * only one process per CPU. Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 346 页,共 1481 页 00533: */ 00534: sched_data = & aligned_data[this_cpu].schedule_data; 00535: 00536: spin_lock_irq(&runqueue_lock); 00537: 00538: /* move an exhausted RR process to be last.. */ 00539: if (prev->policy == SCHED_RR) 00540: goto move_rr_last; 00541: move_rr_back: 指针指向一个 schedule_data 数据结构,用来保存供下一次调度时使用的信息(sched.c): 00091: /* 00092: * We align per-CPU scheduling data on cacheline boundaries, 00093: * to prevent cacheline ping-pong. 00094: */ 00095: static union { 00096: struct schedule_data { 00097: struct task_struct * curr; 00098: cycles_t last_schedule; 00099: } schedule_data; 00100: char __pad [SMP_CACHE_BYTES]; 00101: } aligned_data [NR_CPUS] __cacheline_aligned = { {{&init_task,0}}}; 这里的类型 cycles_t 实际上是无符号整数,用来记录调度发生的时间。这个数据结构是为多处理器 SMP 结构而设的,所以我们在这里并不关心。数组中的第一个元素,即 CPU 0 的 schedule_data 结构初 始化成{&inti_task, 0},其余的则全为{0, 0}。代码中__cacheline_aligned 表示数据结构的起点应与高速缓 存中的缓冲线对其。 下面就要涉及可执行进程队列了,所以先将这个队列锁住,以防止来自其他处理器的干扰。如果当 前进程 prev 的调度政策为 SCHED_RR,即轮换调度,那就要先进行一点特殊的处理。SCHED_RR 和 SCHED_FIFO 都是基于优先级的调度政策,可是在怎样调度具有相同优先级的进程这个问题上二者有区 别。调度政策为 SCHED_FIFO 的进程一旦受到调度而开始运行之后,就要一直运行到自愿让出或者被优 先级更高的进程剥夺为止。对于每次受到调度时要求运行时间不长的进程,这样并没有什么不妥。可是, 如果在受到调度之后可能会长时间运行的进程,那样就不公平了。这种不公平性是对具有相同优先级的 进程而言的。因为具有更高优先级的进程可以剥夺它的运行,而优先级更低的进程则本来就没有机会运 行。但是,这样对具有相同优先级的其他进程就不公平了。所以,对这样的进程应该实行 SCHED_RR 调度政策,这种政策在相同的优先级上实行轮换调度。也就是说,对调度政策为 SCHED_RR 的进程有 个时间配额,用完了这个配额就要让具有相同优先级的其他就绪进程先运行。这里,就是对调度政策为 SCHED_RR 的当前进程的这种处理(sched.c): [schedule()] 00679: move_rr_last: 00680: if (!prev->counter) { 00681: prev->counter = NICE_TO_TICKS(prev->nice); 00682: move_last_runqueue(prev); 00683: } 00684: goto move_rr_back; 00685: 这是什么意思呢?这里的 prev->counter 代表着当前进程的运行时间配额,其数值在每次时钟中断时Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 347 页,共 1481 页 都要递减。这是在一个函数 update_process_times()中进行的,详见下一节。不管一个进程的时间配额有 多高,随着运行时间的积累最终会递减到 0。对于调度策略为 SCHED_RR 的进程,一旦其时间配额降到 了 0,就要从可执行进程队列 runqueue 中当前的位置上移到队列的末尾,同时恢复其最初的时间配额。 对于具有相同优先级的进程,调度的时候排在前面的进程优先,所以这使队列中具有相同优先级的其他 进程有了优势。宏操作 NICE_TO_TICKS 根据系统时钟的精度将进程的优先级别还算成可以运行的时间 配额,这也是在 sched.c 中定义的: 00044: /* 00045: * Scheduling quanta. 00046: * 00047: * NOTE! The unix "nice" value influences how long a process 00048: * gets. The nice value ranges from -20 to +19, where a -20 00049: * is a "high-priority" task, and a "+10" is a low-priority 00050: * task. 00051: * 00052: * We want the time-slice to be around 50ms or so, so this 00053: * calculation depends on the value of HZ. 00054: */ 00055: #if HZ < 200 00056: #define TICK_SCALE(x) ((x) >> 2) 00057: #elif HZ < 400 00058: #define TICK_SCALE(x) ((x) >> 1) 00059: #elif HZ < 800 00060: #define TICK_SCALE(x) (x) 00061: #elif HZ < 1600 00062: #define TICK_SCALE(x) ((x) << 1) 00063: #else 00064: #define TICK_SCALE(x) ((x) << 2) 00065: #endif 00066: 00067: #define NICE_TO_TICKS(nice) (TICK_SCALE(20-(nice))+1) 将一个进程的 task_struct 结构从可执行队列中的当前位置移到队列的末尾是由 move_last_runqueue() 完成的: [schedule() > move_last_runqueue()] 00309: static inline void move_last_runqueue(struct task_struct * p) 00310: { 00311: list_del(&p->run_list); 00312: list_add_tail(&p->run_list, &runqueue_head); 00313: } 把进程移到可执行进程队列的末尾意味着:如果队列中没有资格更高的进程,但是有一个资格与之 相同的进程存在,那么,那个资格虽然相同而排在前面的进程就会被选中。继续在 schedule()中往下看 (sched.c): [schedule()] 00541: move_rr_back: 00542: 00543: switch (prev->state) { Linux 内核源代码情景分析 2006-12-31 版权所有,侵权必究 第 348 页,共 1481 页 00544: case TASK_INTERRUPTIBLE: 00545: if (signal_pending(prev)) { 00546: prev->state = TASK_RUNNING; 00547: break; 00548: } 00549: default: 00550: del_from_runqueue(prev); 00551: case TASK_RUNNING: 00552: } 00553: prev->need_resched = 0; 当前进程就是正在运行中的进程,可是当进入 schedule()时其状态却不一定是 TASK_RUNNING。例 如,在我们这个情景中,当前进程已在 do_exit()中将其状态改成了 TASK_ZOMBIE。又例如,前一节中 我们看到当前进程在 sys_wait4()中调用 schedule()时的状态为 TASK_INTERRUPTIBLE。所以,这里的 prev->state 与其说是当前进程的状态还不如说是其意愿。正因为这样,当其意愿既不是继续运行也不是 可中断的睡眠时,就要通过 del_from_runqueue()把这个进程从可执行队列中撤下来。另一方面,也可以 看出 TASK_INTERRUPTIBLE 与 TASK_UNINTERRUPTIBLE 两种睡眠状态之间的区别,前者在进程有 信号等待处理时要将其改成 TASK_RUNNING,让其处理完这些信号再说,而后者则不受信号的影响。 请注意,在 548 行与 549 行之间并无 break 语句,所以当没有信号等待处理时就落入了 default 的情形, 同样要讲进程从可执行队列中撤下来。反之,如果当前进程的意思是 TASK_RUNNING,即继续运行(见 551 行),那在这里就不需要有什么特殊处理。 然后,将 prev->need_resched 恢复成 0,因为所需求的调度已经在进行。下面就要挑选一个进程来运 行了(sche