阅读:5618次   评论:0条   更新时间:2011-07-05    


书名:Linux内核设计与实现(原书第3版)
原书名:Linux Kernel Development (3rd Edition)
作者:(美)Robert Love
译者:陈莉君  康华
ISBN:9787111338291
定价:69.00元

相关网址:【互动网 】【卓越网 】【当当网 】【京东网 】【豆瓣网

内容简介:

     《Linux内核设计与实现(原书第3版)》详细描述了Linux内核的设计与实现。内核代码的编写者、开发者以及程序开发人员都可以通过阅读本书受益,他们可以更好理解操作系统原理,并将其应用在自己的编码中以提高效率和生产率。
本书详细描述了Linux内核的主要子系统和特点,包括Linux内核的设计、实现和接口。从理论到实践涵盖了Linux内核的方方面面,可以满足读者的各种兴趣和需求。
作者Robert Love是一位Linux内核核心开发人员,他分享了在开发Linux 2.6内核过程中颇具价值的知识和经验。本书的主题包括进程管理、进程调度、时间管理和定时器、系统调用接口、内存寻址、内存管理和页缓存、VFS、内核同步、移植性相关的问题以及调试技术。同时本书也涵盖了Linux 2.6内核中颇具特色的内容,包括CFS调度程序、抢占式内核、块I/O层以及I/O调度程序。
《Linux内核设计与实现(原书第3版)》新增内容包括:

  •   增加一章专门描述内核数据结构
  •   详细描述中断处理程序和下半部机制
  •   扩充虚拟内存和内存分配的内容
  •   调试Linux内核的技巧
  •   内核同步和锁机制的深度描述
  •   提交内核补丁以及参与Linux内核社区的建设性建议

 

作者简介

      Robert Love是一位资深的开源社区达人,很早就开始使用Linux。目前他是Google公司高级软件工程师,是开发Android移动平台内核的团队成员;他曾在Novell公司任职Linux桌面系统的首席架构师;他之前也曾是MontaVista和Ximain公司的内核开发工程师。他参与的内核项目包括抢占式内核、进程调度器、内核事件层、通知机制、VM改进,以及设备驱动程序。他是《Linux journal》杂志的编辑。另外他还著有《Linux System Programming》和《Linux in a Nutshell》。

 

译者简介

  陈莉君,西安邮电学院教授,十多年来一直致力于推动Linux在中国的发展,多年从事Linux内核的教学和研究,并积极跟踪Linux内核的发展动向,对Linux内核版本的不断演化有着深刻的理解。著译作品有《Linux操作系统原理与应用》、《Linux操作系统内核分析》、《深入分析Linux内核源代码》、《深入理解Linux内核》和《Linux内核编程》等。

 

读者热评

《Linux内核设计与实现》相对于Daniel P. Bovet和Marco Cesati的内核巨著《Understanding the Linux Kernel》,少了五分细节;相对于实践经典《Linux Device Drivers》,又多了五分说理。可以说,本书填补了Linux内核理论和实践之间的鸿沟,真可谓“一桥飞架南北,天堑变通途”。—译者

 

能够把linux内核的内容在300多页内叙述一遍,本身就是一件高难度的事情。但这本《Linux内核设计与实现》确实做到了。全书很少涉及具体实现,而是把握思想,讲解算法,读者可以学习到linux内核的知识,而不用纠缠于具体细节。—豆瓣读者Googol

《Linux内核设计与实现》很适合系统学习了OS理论之后直接看代码详解又觉得暂且还不够功力的读者,它可以带你由理论学习阶段逐渐过渡到实践阶段。这本书对Linux内核内容的范围和深度把握得恰到好处。—豆瓣读者 纳兰经若

《Linux内核设计与实现》非常不错,内核相关概念及如何使用内核介绍得非常到位。在阅读的过程中结合深入理解Linux内核就更好了。相对来说,本书偏应用,主要强调如何用,以及为了用而需要了解的内核实现。—china-pub读者 Herobpa

《Linux内核设计与实现》不是源码讲解,而是对整体框架的解析,更侧重理解。在学习《操作系统原理》这样的课程时,可以把本书作为一个操作系统例子来加深理解。这本书可以作为研究Linux源码的一个铺垫。这是一本非常好的辅助书,是研究操作系统和Linux内核的必不可少的资料。—china-pub读者 Fatmouse

《Linux内核设计与实现》语言简练,通俗易懂,对源代码的关键部分进行了准确解释,是新手敲开Linux内核大门的最佳选择。—china-pub读者 Zhouzhen26617809

《Linux内核设计与实现》言简意赅,带我走进了Linux内核的世界。—china-pub读者 Zwlane

《Linux内核设计与实现(原书第3版)》前言 Top

      在我刚开始有把自己的内核开发经验集结成册,撰写一本书的念头时,我其实也觉得有点头绪繁多,不知道该从何下手。我实在不想落入传统内核书籍的窠臼,照猫画虎地再写这么一本。不错,前人著述备矣,但我终归是要写出点儿与众不同的东西来,我的书该如何定位,说实话,这确实让人颇费思量。
      后来,灵感终于浮现出来,我意识到自己可以从一个全新的视角看待这个主题。开发内核是我的工作,同时也是我的嗜好,内核就是我的挚爱。这些年来,我不断搜集与内核有关的奇闻轶事,不断积攒关键的开发诀窍,依靠这些日积月累的材料,我可以写一本关于开发内核该做什么—更重要的是—不该做什么的书籍。从本质上说,这本书仍旧是描述Linux内核是如何设计和实现的,但是写法却另辟蹊径,所提供的信息更倾向于实用。通过本书,你就可以做一些内核开发的工作了—并且是使用正确的方法去做。我是一个注重实效的人,因此,这是一本实践的书,它应当有趣、易读且有用。
      我希望读者可以从这本书中领略到更多Linux内核的精妙之处(写出来的和没写出来的),也希望读者敢于从阅读本书和读内核代码开始跨越到开始尝试开发可用、可靠且清晰的内核代码。当然如果你仅仅是兴致所至,读书自娱,那也希望你能从中找到乐趣。
      从第1版到现在,又过了一段时间,我们再次回到本书,修补遗憾。本版比第1版和第2版内容更丰富:修订、补充并增加了新的内容和章节,使其更加完善。本版融合了第2版以来内核的各种变化。更值得一提的是,Linux内核联盟做出决定,近期内不进行2.7版内核的开发,于是,内核开发者打算继续开发并稳定2.6版。这个决定意味深长,而本书从中的最大受益就是在2.6版上可以稳定相当长时间。随着内核的成熟,内核“快照”才有机会能维持得更久远一些。本书可作为内核开发的规范文档,既认识内核的过去,也着眼于内核的未来。


使用这本书
      开发Linux内核不需要天赋异秉,不需要有什么魔法,连Unix开发者普遍长着的络腮胡子都不一定要有。内核虽然有一些有趣并且独特的规则和要求,但是它和其他大型软件项目相比,并没有太大差别。像所有的大型软件开发一样,要学的东西确实不少,但是不同之处在于数量上的积累,而非本质上的区别。
      认真阅读源码非常有必要,Linux系统代码的开放性其实是弥足珍贵的,不要无动于衷地将它搁置一边,浪费了大好资源。实际上就是读了代码还远远不够呢,你应该钻研并尝试着动手改动一些代码。寻找一个bug然后去修改它,改进你的硬件设备的驱动程序。增加新功能,即使看起来微不足道,寻找痛痒之处并解决。只有动手写代码才能真正融会贯通。


内核版本
      本书基于Linux 2.6内核系列。它并不涵盖早期的版本,当然也有一些例外。比如,我们会讨论2.4系列内核中的一些子系统是如何实现的,这是因为简单的实现有助于传授知识。特别说明的是,本书介绍的是最新的Linux 2.6.34内核版本。尽管内核总在不断更新,任何努力也难以捕获这样一只永不停息的猛兽,但是本书力图适合于新旧内核的开发者和用户。
      虽然本书讨论的是2.6.34内核,但我也确保了它同样适用于2.6.32内核。后一个版本往往被各个Linux发行版本奉为“企业版”内核,所以我们可以在各种产品线上见到其身影。该版本确实已经开发了数年(类似的“长线”版本还有 2.6.9、2.6.18和2.6.27等)。

 

读者范围
      本书是写给那些有志于理解Linux内核的软件开发者的。本书并不逐行逐字地注解内核源代码,也不是指导开发驱动程序或是内核API的参考手册(如果存在标准的内核API的话)。本书的初衷是提供足够多的关于Linux内核设计和实现的信息,希望读过本书的程序员能够拥有较为完备的知识,可以真正开始开发内核代码。无论开发内核是为了兴趣还是为了赚钱,我都希望能够带领读者快速走进Linux内核世界。本书不但介绍了理论而且也讨论了具体应用,可以满足不同读者的需要。全书紧紧围绕着理论联系实践,并非一味强调理论或是实践。无论你研究Linux内核的动机是什么,我都希望这本书都能将内核的设计和实现分析清楚,起到抛砖引玉的作用。
      因此,本书覆盖了从核心内核系统的应用到内核设计与实现等各方面的内容。我认为这点很重要,值得花工夫讨论。例如,第8章讨论的是所谓的下半部机制。本章分别讨论了内核下半部机制的设计和实现(核心内核开发者或者学者会感兴趣),随即便介绍了如何使用内核提供的接口实现你自己的下半部(这对设备驱动开发者可能很有用处)。其实,我认为上述两部分内容是相得益彰的,虽然核心内核开发者主要关注的问题是内核内部如何工作,但是也应该清楚如何使用接口;同样,如果设备驱动开发者了解了接口背后的实现机制,自然也会受益匪浅。
      这好比学习某些库的API函数与研究该库的具体实现。初看,好像应用程序开发者仅仅需要理解API—我们被灌输的思想是,应该像看待黑盒子一样看待接口。另外,库的开发者也只关心库的设计与实现,但是我认为双方都应该花时间相互学习。能深刻了解操作系统本质的应用程序开发者无疑可以更好地利用它。同样,库开发者也决不应该脱离基于此库的应用程序,埋头开发。因此,我既讨论了内核子系统的设计,也讨论了它的用法,希望本书能对核心开发者和应用开发者都有用。
      我假设读者已经掌握了C语言,而且对Linux比较熟悉。如果读者还具有与操作系统设计相关的经验和其他计算机科学的概念就更好了。当然,我也会尽可能多地解释这些概念,但如果你仍然不能理解这些知识的话,请看本书最后参考资料中给出的一些关于操作系统设计方面的经典书籍。
      本书很适合在大学中作为介绍操作系统的辅助教材,与介绍操作系统理论的书相搭配。对于大学高年级课程或者研究生课程来说,可直接使用本书作为教材。

 

第3版致谢    

      与其他作者一样,我决非是一个人躲在山洞里孤苦地写出这本书来的(那也是一件美差,因为有熊相伴)。我能最终完成本书原稿是与无数建议和关怀分不开的。仅仅一页纸无法容纳我的感激,但我还是要衷心地感谢所有给予我鼓励、给予我知识和给予我灵感的朋友和同事。
      首先我要对为此书付出辛勤劳作的所有编辑表示感谢,尤其要感谢我的组稿编辑Mark Taber,他为这一版的出版从头到尾倾注了许多心血。还要特别感谢业务开发编辑Michael Thurston和项目组织编辑Tonya Simpso。
      本版的技术编辑Robert P. J. Day也是我需要倍加感谢的人,他独到的洞察力和准确的校对使书稿质量大大提高。尽管他的工作称得上完美,但如果书中仍留有错误,责任由我承担。需要十分感谢的还有Adam Belay、Zack Brown、Martin Pool以及 Chris Rivera,他们对第1版和第2版所做的一切努力我依然记忆犹新。
      许多内核开发者为我提供了大力支持,回答了许多问题,还有那些撰写代码的人——本书正是由于有了这些代码,才有了存在的意义。他们是:Andrea Arcangeli、Alan Cox、Greg Kroah-Hartman、Dave Miller、Patrick Mochel、Andrew Morton、Nick Piggin和Linus Torvalds。
      我要大力感谢我的同事们。他们的创造力和智慧无与伦比,能与他们一起工作其乐无穷。因为篇幅原因,请谅解我不能列出所有人的名字。但不得不提的是:Alan Blount、Jay Crim、Chris Danis、Chris DiBona、Eric Flatt、Mike Lockwood、San Mehat、Brian Rogan、Brian Swetland、Jon Trowbridge和Steve Vinter,感谢他们给予我的支持、知识和友谊!
      还有许多值得尊敬和热爱的人,他们是:Paul Amici、Mikey Babbitt、Keith Barbag、Jacob Berkman、Nat Friedman、Dustin Hall、Joyce Hawkins、Miguel de Icaza、Jimmy Krehl、Doris Love、Linda Love、Brette Luck、Randy O扗owd、Sal Ribaudo和他了不起的妈妈 Chris Rivera、Carolyn Rodon、Joey Shaw、Sarah Stewart、Jeremy VanDoren和他的家人、Luis Villa、Steve Weisberg和他的家人以及 Helen Whisnant等。
      最后,非常感谢我的父母。
      内核黑客万岁!
Robert Love
Boston

《Linux内核设计与实现(原书第3版)》目录 Top

译者序
序 言
前 言
作者简介
第1章 Linux内核简介1
1.1 Unix的历史1
1.2 追寻Linus足迹:Linux简介2
1.3 操作系统和内核简介3
1.4 Linux内核和传统Unix内核的比较5
1.5 Linux内核版本7
1.6 Linux内核开发者社区8
1.7 小结8
第2章 从内核出发10
2.1 获取内核源码10
2.1.1 使用Git10
2.1.1 安装内核源代码10
2.1.3 使用补丁11
2.2 内核源码树11
2.3 编译内核12
2.3.1 配置内核12
2.3.2 减少编译的垃圾信息14
2.3.3 衍生多个编译作业 14
2.3.4 安装新内核14
2.4 内核开发的特点15
2.4.1 无libc库抑或无标准头文件15
2.4.2 GNU C16
2.4.3 没有内存保护机制18
2.4.4 不要轻易在内核中使用浮点数18
2.4.5 容积小而固定的栈18
2.4.6 同步和并发18
2.4.7 可移植性的重要性19
2.5 小结19
第3章 进程管理20
3.1 进程20
3.2 进程描述符及任务结构 21
3.2.1 分配进程描述符22
3.2.2 进程描述符的存放23
3.2.3 进程状态23
3.2.4 设置当前进程状态25
3.2.5 进程上下文25
3.2.6 进程家族树25
3.3 进程创建26
3.3.1 写时拷贝27
3.3.2 fork()27
3.3.3 vfork()28
3.4 线程在Linux中的实现28
3.4.1 创建线程29
3.4.2 内核线程30
3.5 进程终结31
3.5.1 删除进程描述符32
3.5.2 孤儿进程造成的进退维谷32
3.6 小结34
第4章 进程调度35
4.1 多任务35
4.2 Linux 的进程调度36
4.3 策略36
4.3.1 I/O消耗型和处理器消耗型的进程36
4.3.2 进程优先级37
4.3.3 时间片38
4.3.4 调度策略的活动38
4.4 Linux调度算法39
4.4.1 调度器类39
4.4.2 Unix 系统中的进程调度40
4.4.3 公平调度41
4.5 Linux调度的实现42
4.5.1 时间记账42
4.5.2 进程选择44
4.5.3 调度器入口48
4.5.4 睡眠和唤醒49
4.6 抢占和上下文切换51
4.6.1 用户抢占53
4.6.2 内核抢占53
4.7 实时调度策略54
4.8 与调度相关的系统调用54
4.8.1 与调度策略和优先级相关的系统调用55
4.8.2 与处理器绑定有关的系统调用55
4.8.3 放弃处理器时间56
4.9 小结56
第5章 系统调用57
5.1 与内核通信57
5.2 API、POSIX和C库57
5.3 系统调用58
5.3.1 系统调用号59
5.3.2 系统调用的性能59
5.4 系统调用处理程序60
5.4.1 指定恰当的系统调用60
5.4.2 参数传递60
5.5 系统调用的实现61
5.5.1 实现系统调用61
5.5.2 参数验证62
5.6 系统调用上下文64
5.6.1 绑定一个系统调用的最后步骤65
5.6.2 从用户空间访问系统调用67
5.6.3 为什么不通过系统调用的方式实现68
5.7 小结68
第6章 内核数据结构69
6.1 链表69
6.1.1 单向链表和双向链表69
6.1.2 环形链表70
6.1.3 沿链表移动71
6.1.4 Linux 内核中的实现71
6.1.5 操作链表73
6.1.6 遍历链表75
6.2 队列78
6.2.1 kfifo79
6.2.2 创建队列79
6.2.3 推入队列数据79
6.2.4 摘取队列数据80
6.2.5 获取队列长度80
6.2.6 重置和撤销队列80
6.2.7 队列使用举例 81
6.3 映射 81
6.3.1 初始化一个idr82
6.3.2 分配一个新的UID82
6.3.3 查找UID83
6.3.4 删除UID84
6.3.5 撤销idr84
6.4 二叉树84
6.4.1 二叉搜索树84
6.4.2 自平衡二叉搜索树 85
6.5 数据结构以及选择 87
6.6 算法复杂度88
6.6.1 算法88
6.6.2 大o 符号88
6.6.3 大θ符号89
6.6.4 时间复杂度89
6.7 小结 90
第7章 中断和中断处理91
7.1 中断91
7.2 中断处理程序92
7.3 上半部与下半部的对比93
7.4 注册中断处理程序93
7.4.1 中断处理程序标志94
7.4.2 一个中断例子95
7.4.3 释放中断处理程序95
7.5 编写中断处理程序96
7.5.1 共享的中断处理程序97
7.5.2 中断处理程序实例97
7.6 中断上下文99
7.7 中断处理机制的实现100
7.8 /proc/interrupts102
7.9 中断控制103
7.9.1 禁止和激活中断103
7.9.2 禁止指定中断线105
7.9.3 中断系统的状态105
7.10 小结106
第8章 下半部和推后执行的工作107
8.1 下半部107
8.1.1 为什么要用下半部108
8.1.2 下半部的环境108
8.2 软中断110
8.2.1 软中断的实现111
8.2.2 使用软中断113
8.3 tasklet114
8.3.1 tasklet的实现114
8.3.2 使用tasklet116
8.3.3 老的BH机制119
8.4 工作队列120
8.4.1 工作队列的实现121
8.4.2 使用工作队列124
8.4.3 老的任务队列机制126
8.5 下半部机制的选择127
8.6 在下半部之间加锁128
8.7 禁止下半部128
8.8 小结129
第9章 内核同步介绍131
9.1 临界区和竞争条件131
9.1.1 为什么我们需要保护132
9.1.2 单个变量133
9.2 加锁134
9.2.1 造成并发执行的原因135
9.2.2 了解要保护些什么136
9.3 死锁137
9.4 争用和扩展性138
9.5 小结140
第10章 内核同步方法141
10.1 原子操作141
10.1.1 原子整数操作142
10.1.2 64位原子操作144
10.1.3 原子位操作145
10.2 自旋锁147
10.2.1 自旋锁方法148
10.2.2 其他针对自旋锁的操作149
10.2.3 自旋锁和下半部150
10.3 读-写自旋锁150
10.4 信号量152
10.4.1 计数信号量和二值信号量153
10.4.2 创建和初始化信号量154
10.4.3 使用信号量154
10.5 读-写信号量155
10.6 互斥体156
10.6.1 信号量和互斥体158
10.6.2 自旋锁和互斥体158
10.7 完成变量158
10.8 BLK:大内核锁159
10.9 顺序锁160
10.10 禁止抢占161
10.11 顺序和屏障162
10.12 小结165
第11章 定时器和时间管理166
11.1 内核中的时间概念166
11.2 节拍率:HZ167
11.2.1 理想的HZ值168
11.2.2 高HZ的优势169
11.2.3 高HZ的劣势169
11.3 jiffies170
11.3.1 jiffies的内部表示171
11.3.2 jiffies 的回绕172
11.3.3 用户空间和HZ173
11.4 硬时钟和定时器174
11.4.1 实时时钟174
11.4.2 系统定时器174
11.5 时钟中断处理程序174
11.6 实际时间176
11.7 定时器178
11.7.1 使用定时器178
11.7.2 定时器竞争条件180
11.7.3 实现定时器180
11.8 延迟执行181
11.8.1 忙等待181
11.8.2 短延迟182
11.8.3 schedule_timeout()183
11.9 小结185
第12章 内存管理186
12.1 页186
12.2 区187
12.3 获得页189
12.3.1 获得填充为0的页190
12.3.2 释放页191
12.4 kmalloc()191
12.4.1 gfp_mask标志192
12.4.2 kfree()195
12.5 vmalloc()196
12.6 slab层197
12.6.1 slab层的设计198
12.6.2 slab分配器的接口200
12.7 在栈上的静态分配203
12.7.1 单页内核栈203
12.7.2 在栈上光明正大地工作203
12.8 高端内存的映射204
12.8.1 永久映射204
12.8.2 临时映射204
12.9 每个CPU的分配205
12.10 新的每个CPU接口206
12.10.1 编译时的每个CPU数据206
12.10.2 运行时的每个CPU数据207
12.11 使用每个CPU数据的原因208
12.12 分配函数的选择209
12.13 小结209
第13章 虚拟文件系统210
13.1 通用文件系统接口210
13.2 文件系统抽象层211
13.3 Unix文件系统212
13.4 VFS 对象及其数据结构213
13.5 超级块对象214
13.6 超级块操作215
13.7 索引节点对象217
13.8 索引节点操作219
13.9 目录项对象222
13.9.1 目录项状态222
13.9.2 目录项缓存223
13.10 目录项操作224
13.11 文件对象225
13.12 文件操作226
13.13 和文件系统相关的数据结构230
13.14 和进程相关的数据结构232
13.15 小结233
第14章 块I/O层234
14.1 剖析一个块设备234
14.2 缓冲区和缓冲区头235
14.3 bio结构体237
14.3.1 I/O向量238
14.3.2 新老方法对比239
14.4 请求队列240
14.5 I/O调度程序240
14.5.1 I/O调度程序的工作241
14.5.2 Linus 电梯241
14.5.3 最终期限I/O调度程序242
14.5.4 预测I/O调度程序244
14.5.5 完全公正的排队I/O调度程序244
14.5.6 空操作的I/O调度程序245
14.5.7 I/O调度程序的选择245
14.6 小结246
第15章 进程地址空间247
15.1 地址空间247
15.2 内存描述符248
15.2.1 分配内存描述符249
15.2.2 撤销内存描述符250
15.2.3 mm_struct 与内核线程250
15.3 虚拟内存区域251
15.3.1 VMA标志251
15.3.2 VMA 操作253
15.3.3 内存区域的树型结构和内存区域的链表结构254
15.3.4 实际使用中的内存区域254
15.4 操作内存区域255
15.4.1 find_vma()256
15.4.2 find_vma_prev()257
15.4.3 find_vma_intersection()257
15.5 mmap()和do_mmap():创建地址区间258
15.6 mummap()和do_mummap():删除地址区间259
15.7 页表260
15.8 小结261
第16章 页高速缓存和页回写262
16.1 缓存手段262
16.1.1 写缓存262
16.1.2 缓存回收263
16.2 Linux 页高速缓存264
16.2.1 address_space对象264
16.2.2 address_space 操作266
16.2.3 基树267
16.2.4 以前的页散列表268
16.3 缓冲区高速缓存268
16.4 flusher线程268
16.4.1 膝上型计算机模式270
16.4.2 历史上的bdflush、kupdated 和pdflush270
16.4.3 避免拥塞的方法:使用多线程271
16.5 小结271
第17章 设备与模块273
17.1 设备类型273
17.2 模块274
17.2.1 Hello,World274
17.2.2 构建模块275
17.2.3 安装模块277
17.2.4 产生模块依赖性277
17.2.5 载入模块278
17.2.6 管理配置选项279
17.2.7 模块参数280
17.2.8 导出符号表282
17.3 设备模型283
17.3.1 kobject283
17.3.2 ktype284
17.3.3 kset285
17.3.4 kobject、ktype和kset的相互关系285
17.3.5 管理和操作kobject286
17.3.6 引用计数287
17.4 sysfs288
17.4.1 sysfs中添加和删除kobject 290
17.4.2 向sysfs中添加文件291
17.4.3 内核事件层293
17.5 小结294
第18章 调试295
18.1 准备开始295
18.2 内核中的bug296
18.3 通过打印来调试296
18.3.1 健壮性296
18.3.2 日志等级297
18.3.3 记录缓冲区298
18.3.4 syslogd和klogd298
18.3.5 从printf()到printk()的转换298
18.4 oops298
18.4.1 ksymoops300
18.4.2 kallsyms300
18.5 内核调试配置选项301
18.6 引发bug并打印信息301
18.7 神奇的系统请求键302
18.8 内核调试器的传奇303
18.8.1 gdb303
18.8.2 kgdb304
18.9 探测系统304
18.9.1 用UID作为选择条件304
18.9.2 使用条件变量305
18.9.3 使用统计量305
18.9.4 重复频率限制305
18.10 用二分查找法找出引发罪恶的变更306
18.11 使用Git进行二分搜索307
18.12 当所有的努力都失败时:社区308
18.13 小结308
第19章 可移植性309
19.1 可移植操作系统309
19.2 Linux移植史310
19.3 字长和数据类型311
19.3.1 不透明类型313
19.3.2 指定数据类型314
19.3.3 长度明确的类型314
19.3.4 char型的符号问题315
19.4 数据对齐315
19.4.1 避免对齐引发的问题316
19.4.2 非标准类型的对齐316
19.4.3 结构体填补316
19.5 字节顺序318
19.6 时间319
19.7 页长度320
19.8 处理器排序320
19.9 SMP、内核抢占、高端内存321
19.10 小结321
第20章 补丁、开发和社区322
20.1 社区322
20.2 Linux编码风格322
20.2.1 缩进323
20.2.2 switch 语句323
20.2.3 空格324
20.2.4 花括号325
20.2.5 每行代码的长度326
20.2.6 命名规范326
20.2.7 函数326
20.2.8 注释326
20.2.9 typedef327
20.2.10 多用现成的东西328
20.2.11 在源码中减少使用ifdef328
20.2.12 结构初始化328
20.2.13 代码的事后修正329
20.3 管理系统329
20.4 提交错误报告329
20.5 补丁330
20.5.1 创建补丁330
20.5.2 用Git创建补丁331
20.5.3 提交补丁331
20.6 小结332
参考资料333

第一章 Linux内核简介 Top

       第1章将带我们从Unix的历史视角来认识Linux内核与Linux操作系统的前世今生。今天Unix系统业已演化成一个具有相似应用程序编程接口(API),并且基于相似设计理念的操作系统家族。但它又是一个别具特色的操作系统,从萌芽到现在已经有40余年的历史。若要了解Linux,我们必须首先认识Unix系统。

1.1 Unix的历史 Top

       Unix虽然已经使用了40年,但计算机科学家仍然认为它是现存操作系统中最强大和最优秀的系统。从1969年诞生以来,由Dennis Ritchie和Ken Thompson的灵感火花点亮的这个Unix产物已经成为一种传奇,它历经了时间的考验依然声名不坠。
       Unix是从贝尔试验室的一个失败的多用户操作系统Multics中涅槃而生的。Multics项目被终止后,贝尔实验室计算科学研究中心的人们发现自己处于一个没有交互式操作系统可用的境地。在这种情况下,1969年的夏天,贝尔实验室的程序员们设计了一个文件系统原型,而这个原型最终发展演化成了Unix。Thompson首先在一台无人问津的PDP-7型机上实现了这个全新的操作系统。1971年,Unix被移植到PDP-11型机中。1973年,整个Unix操作系统用C语言进行了重写,正是当时这个并不太引人注目的举动,给后来Unix系统的广泛移植铺平了道路。第一个在贝尔实验室以外被广泛使用的Unix版本是第6版,称为V6。
       许多其他的公司也把Unix移植到新的机型上。伴随着这些移植,开发者们按照自己的方式不断地增强系统的功能,并由此产生了若干变体。1977年,贝尔实验室综合各种变体推出了Unix System Ⅲ;1983年AT&T推出了 System V。
       由于Unix系统设计简洁并且在发布时提供源代码,所以许多其他组织和团体都对它进行了进一步的开发。加州大学伯克利分校便是其中影响最大的一个。他们推出的变体叫Berkeley Software Distributions (BSD)。伯克利的第一个Unix演化版是1977年推出的1BSD系统,它的实现基于贝尔实验室的Unix版本,不但在其上加入了许多修正补丁,而且还集成了不少额外的软件;1978年伯克利继续推出了2BSD系统,其中包含我们如今仍在使用的csh 、vi等应用软件。而伯克利真正独立开发的Unix系统是于1979年推出的3BSD系统,该系统引入了一系列令人振奋的新特性,支持虚拟内存便是其一大亮点。在3BSD以后,伯克利又相继推出了4BSD系列,包括4.0BSD、4.1BSD、4.2BSD、4.3BSD等众多分支。这些Unix演化版实现了任务管理、换页机制、TCP/IP等新的特性。 最终伯克利大学在1994年重写了虚拟内存子系统(VM),并推出了伯克利Unix系统的最终官方版,即我们熟知的4.4BSD。现在,多亏了BSD的开放性许可,BSD的开发才得以由Darwin、FreeBSD、NetBSD和OpenBSD继续。
       20世纪80和90年代,许多工作站和服务器厂商推出了他们自己的Unix,这些Unix大部分是在AT&T或伯克利发行版的基础上加上一些满足他们特定体系结构需要的特性。这其中就包括Digital的Tru64、HP的HP-UX、IBM的AIX、Sequent的DYNIX/ptx、SGI的IRIX和Sun的Solaris和SunOS。
由于最初一流的设计和以后多年的创新与逐步提高,Unix系统成为一个强大、健壮和稳定的操作系统。下面的几个特点是使Unix强大的根本原因。首先,Unix很简洁:不像其他动辄提供数千个系统调用并且设计目的不明确的系统,Unix仅仅提供几百个系统调用并且有一个非常明确的设计目的。第二,在Unix中,所有的东西都被当做文件对待。这种抽象使对数据和对设备的操作是通过一套相同的系统调用接口来进行的:open()、read()、write()、lseek()和close()。第三,Unix的内核和相关的系统工具软件是用C语言编写而成——正是这个特点使得Unix在各种硬件体系架构面前都具备令人惊异的移植能力,并且使广大的开发人员很容易就能接受它。第四,Unix的进程创建非常迅速,并且有一个非常独特的fork()系统调用。最后,Unix提供了一套非常简单但又很稳定的进程间通信元语,快速简洁的进程创建过程使Unix的程序把目标放在一次执行保质保量地完成一个任务上,而简单稳定的进程间通信机制又可以保证这些单一目的的简单程序可以方便地组合在一起,去解决现实中变得越来越复杂的任务。正是由于这种策略和机制分离的设计理念,确保了Unix系统具备清晰的层次化结构。
      今天,Unix已经发展成为一个支持抢占式多任务、多线程、虚拟内存、换页、动态链接和TCP/IP网络的现代化操作系统。Unix的不同变体被应用在大到数百个CPU的集群,小到嵌入式设备的各种系统上。尽管Unix已经不再被认为是一个实验室项目了,但它仍然伴随着操作系统设计技术的进步而继续成长,人们仍然可以把它作为一个通用的操作系统来使用。
      Unix的成功归功于其简洁和一流的设计。它能拥有今天的能力和成就应该归功于Dennis Ritchie、Ken Thompson和其他早期设计人员的最初决策,同时也要归功于那些永不妥协于成见,从而赋予Unix无穷活力的设计抉择。

1.2 追寻Linus足迹:Linux简介 Top

      1991年,Linus Torvalds 为当时新推出的、使用Intel 80386微处理器的计算机开发了一款全新的操作系统,Linux由此诞生。那时,作为芬兰赫尔辛基大学的一名学生的Linus,正为不能随心所欲使用强大而自由的Unix系统而苦恼。对Torvalds 而言,使用当时流行的Microsoft的DOS系统,除了玩波斯王子游戏外,别无他用。 Linus热衷使用于Minix,一种教学用的廉价Unix,但是,他不能轻易修改和发布该系统的源代码(由于Minix的许可证),也不能对Minix开发者所作的设计轻举妄动,这让他耿耿于怀并由此对作者的设计理念感到失望。
      Linus像任何一名生机勃勃的大学生一样决心走出这种困境:开发自己的操作系统。他开始写了一个简单的终端仿真程序,用于连接到本校的大型Unix系统上。他的终端仿真程序经过一学年的研发,不断改进和完善。不久,Linus手上就有了虽不成熟但五脏俱全的Unix。1991年年底,他在Internet上发布了早期版本。
      从此Linux便起航了,最初的Linux发布很快赢得了众多用户。而实际上,它成功的重要因素是,Linux很快吸引了很多开发者、黑客对其代码进行修改和完善。由于其许可证条款的约定,Linux迅速成为多人的合作开发项目。
      到现在,Linux早已羽翼丰满,它被广泛移植到Alpha、ARM、PowerPC、SPARC、x86-64等许多其他体系结构之上。如今Linux既被安装在最轻小的消费电子设备上,比如手表,同时也在服务规模最庞大的服务数据中心上,如超级计算机集群。今天,Linux的商业前景也越来越被看好,不管是新成立的Linux专业公司Red Hat还是闻名遐迩的计算巨头IBM,都提供林林总总的解决方案,从嵌入式系统、桌面环境一直到服务器。
      Linux是类Unix系统,但它不是Unix。需要说明的是,尽管Linux借鉴了Unix的许多设计并且实现了Unix的API(由Posix标准和其他Single Unix Specification定义的),但Linux没有像其他Unix变种那样直接使用Unix的源代码。必要的时候,它的实现可能和其他各种Unix的实现大相径庭,但它没有抛弃Unix的设计目标并且保证了应用程序编程接口的一致。
      Linux是一个非商业化的产品,这是它最让人感兴趣的特征。实际上Linux是一个互联网上的协作开发项目。尽管Linus被认为是Linux之父,并且现在依然是一个内核维护者,但开发工作其实是由一个结构松散的工作组协力完成的。事实上,任何人都可以开发内核。和该系统的大部分一样,Linux内核也是自由(公开)软件。当然,也不是无限自由的。它使用GNU的General Public License(GPL)第2版作为限制条款。这样做的结果是,你可以自由地获取内核代码并随意修改它,但如果你希望发布你修改过的内核,你也得保证让得到你的内核的人同时享有你曾经享受过的所有权利,当然,包括全部的源代码。
      Linux用途广泛,包含的东西也名目繁多。Linux系统的基础是内核、C库、工具集和系统的基本工具,如登录程序和Shell。Linux系统也支持现代的X Windows系统,这样就可以使用完整的图形用户桌面环境,如GNOME。可以在Linux上使用的商业和自由软件数以千计。在这本书以后的部分,当我使用Linux这个词时,我其实说的是Linux内核。在容易引起混淆的地方,我会具体说明到底我想说的是整个系统还是内核。一般情况下,Linux这个词汇主要还是指内核。

 

1.3 操作系统和内核简介 Top

      由于一些现行商业操作系统日趋庞杂及其设计上的缺陷,操作系统的精确定义并没有一个统一的标准。许多用户把他们在显示器屏幕上看到的东西理所当然地认为就是操作系统。通常,当然在本书中也这么认为,操作系统是指在整个系统中负责完成最基本功能和系统管理的那些部分。这些部分应该包括内核、设备驱动程序、启动引导程序、命令行Shell或者其他种类的用户界面、基本的文件管理工具和系统工具。这些都是必不可少的东西—别以为只要有浏览器和播放器就行了。系统这个词其实包含了操作系统和所有运行在它之上的应用程序。
      当然,本书的主题是内核。用户界面是操作系统的外在表象,内核才是操作系统的内在核心。系统其他部分必须依靠内核这部分软件提供的服务,像管理硬件设备、分配系统资源等。内核有时候被称作是管理者或者是操作系统核心。通常一个内核由负责响应中断的中断服务程序,负责管理多个进程从而分享处理器时间的调度程序,负责管理进程地址空间的内存管理程序和网络、进程间通信等系统服务程序共同组成。对于提供保护机制的现代系统来说,内核独立于普通应用程序,它一般处于系统态,拥有受保护的内存空间和访问硬件设备的所有权限。这种系统态和被保护起来的内存空间,统称为内核空间。相对的,应用程序在用户空间执行。它们只能看到允许它们使用的部分系统资源,并且只使用某些特定的系统功能,不能直接访问硬件,也不能访问内核划给别人的内存范围,还有其他一些使用限制。当内核运行的时候,系统以内核态进入内核空间执行。而执行一个普通用户程序时,系统将以用户态进入以用户空间执行。
      在系统中运行的应用程序通过系统调用来与内核通信(见图1-1)。应用程序通常调用库函数(比如C库函数)再由库函数通过系统调用界面,让内核代其完成各种不同任务。一些库调用提供了系统调用不具备的许多功能,在那些较为复杂的函数中,调用内核的操作通常只是整个工作的一个步骤而已。举个例子,拿printf()函数来说,它提供了数据的缓存和格式化等操作,而调用write()函数将数据写到控制台上只不过是其中的一个动作罢了。不过,也有一些库函数和系统调用就是一一对应的关系,比如,open()库函数除了调用open()系统调用之外,几乎什么也不做。还有一些C库函数,像strcpy(),根本就不需要直接调用系统级的操作。当一个应用程序执行一条系统调用,我们说内核正在代其执行。如果进一步解释,在这种情况下,应用程序被称为通过系统调用在内核空间运行,而内核被称为运行于进程上下文中。这种交互关系—应用程序通过系统调用界面陷入内核—是应用程序完成其工作的基本行为方式。
      内核还要负责管理系统的硬件设备。现有的几乎所有的体系结构,包括全部Linux支持的体系结构,都提供了中断机制。当硬件设备想和系统通信的时候,它首先要发出一个异步的中断信号去打断处理器的执行,继而打断内核的执行。中断通常对应着一个中断号,内核通过这个中断号查找相应的中断服务程序,并调用这个程序响应和处理中断。举个例子,当你敲击键盘的时候,键盘控制器发送一个中断信号告知系统,键盘缓冲区有数据到来。内核注意到这个中断对应的中断号,调用相应的中断服务程序。该服务程序处理键盘数据然后通知键盘控制器可以继续输入数据了。为了保证同步,内核可以停用中止—既可以停止所有的中断也可以有选择地停止某个中断号对应的中断。许多操作系统的中断服务程序,包括Linux的,都不在进程上下文中执行。它们在一个与所有进程都无关的、专门的中断上下文中运行。之所以存在这样一个专门的执行环境,就是为了保证中断服务程序能够在第一时间响应和处理中断请求,然后快速地退出。
这些上下文代表着内核活动的范围。实际上我们可以将每个处理器在任何指定时间点上的活动必然概括为下列三者之一:

 

 

       运行于用户空间,执行用户进程。
       运行于内核空间,处于进程上下文,代表某个特定的进程执行。
       运行于内核空间,处于中断上下文,与任何进程无关,处理某个特定的中断。
       以上所列几乎包括所有情况,即使边边角角的情况也不例外,例如,当CPU空闲时,内核就运行一个空进程,处于进程上下文,但运行于内核空间。

1.4 Linux内核和传统Unix内核的比较 Top

      由于所有的Unix内核都同宗同源,并且提供相同的API,现代的Unix内核存在许多设计上的相似之处(请看参考目录中我所推荐的关于传统Unix内核设计的相关书籍)。Unix内核几乎毫无例外的都是一个不可分割的静态可执行库。 也就是说,它们必须以巨大、单独的可执行块的形式在一个单独的地址空间中运行。Unix内核通常需要硬件系统提供页机制(MMU)以管理内存。这种页机制可以加强对内存空间的保护,并保证每个进程都可以运行于不同的虚地址空间上。初期的Linux系统也需要MMU支持,但有一些特殊版本并不依赖于此。这无疑是一个简洁的设计,因为它可以使Linux系统运行在没有MMU的小型嵌入系统上。不过现实之中,即便很简单的嵌入系统都开始具备内存管理单元这种高级功能了。 本书中,我们将重点关注支持MMU的Linux系统。
     单内核与微内核设计之比较
     操作系统内核可以分为两大阵营:单内核和微内核(第三阵营是外内核,主要用在科研系统中)。
     单内核是两大阵营中一种较为简单的设计,在1980年之前,所有的内核都设计成单内核。所谓单内核就是把它从整体上作为一个单独的大过程来实现,同时也运行在一个单独的地址空间上。因此,这样的内核通常以单个静态二进制文件的形式存放于磁盘中。所有内核服务都在这样的一个大内核地址空间上运行。内核之间的通信是微不足道的,因为大家都运行在内核态,并身处同一地址空间:内核可以直接调用函数,这与用户空间应用程序没有什么区别。这种模式的支持者认为单模块具有简单和性能高的特点。大多数Unix系统都设计为单模块。
     另一方面,微内核并不作为一个单独的大过程来实现。相反,微内核的功能被划分为多个独立的过程,每个过程叫做一个服务器。理想情况下,只有强烈请求特权服务的服务器才运行在特权模式下,其他服务器都运行在用户空间。不过,所有的服务器都保持独立并运行在各自的地址空间上。因此,就不可能像单模块内核那样直接调用函数,而是通过消息传递处理微内核通信:系统采用了进程间通信(IPC)机制,因此,各个服务器之间通过IPC机制互通消息,互换“服务”。服务器的各自独立有效地避免了一个服务器的失效祸及另一个。同样,模块化的系统允许一个服务器为了另一个服务器而换出。
     因为IPC机制的开销多于函数调用,又因为会涉及内核空间与用户空间的上下文切换,因此,消息传递需要一定的周期,而单内核中简单的函数调用没有这些开销。结果,所有实际应用的基于微内核的系统都让大部分或全部服务器位于内核,这样,就可以直接调用函数,消除频繁的上下文切换。Windows NT内核(Windows XP、Windows Vista和Windows 7等基于此)和Mach(Mac OS X的组成部分)是微内核的典型实例。不管是Windows NT还是Mac OS X,都在其新近版本中不让任何微内核服务器运行在用户空间,这违背了微内核设计的初衷。
     Linux是一个单内核,也就是说,Linux内核运行在单独的内核地址空间上。不过,Linux汲取了微内核的精华:其引以为豪的是模块化设计、抢占式内核、支持内核线程以及动态装载内核模块的能力。不仅如此,Linux还避其微内核设计上性能损失的缺陷,让所有事情都运行在内核态,直接调用函数,无须消息传递。至今,Linux是模块化的、多线程的以及内核本身可调度的操作系统,实用主义再次占了上风。

     当Linus和其他内核开发者设计Linux内核时,他们并没有完全彻底地与Unix诀别。他们充分地认识到,不能忽视Unix的底蕴(特别是Unix的API)。而由于  Linux并没有基于某种特定的Unix,Linus和他的伙伴们对每个特定的问题都可以选择已知最理想的解决方案—在有些时候,当然也可以创造一些新的方案。 Linux 内核与传统的Unix系统之间存在一些显著的差异:
     Linux支持动态加载内核模块。尽管Linux内核也是单内核,可是允许在需要的时候动态地卸除和加载部分内核代码。
     Linux支持对称多处理(SMP)机制,尽管许多Unix的变体也支持SMP,但传统的Unix并不支持这种机制。
     Linux内核可以抢占(preemptive)。与传统的Unix变体不同,Linux内核具有允许在内核运行的任务优先执行的能力。在其他各种Unix产品中,只有Solaris和IRIX支持抢占,但是大多数Unix内核不支持抢占。
     Linux对线程支持的实现比较有意思:内核并不区分线程和其他的一般进程。对于内核来说,所有的进程都一样—只不过是其中的一些共享资源而已。
     Linux提供具有设备类的面向对象的设备模型、热插拔事件,以及用户空间的设备文件系统(sysfs)。
     Linux忽略了一些被认为是设计得很拙劣的Unix特性,像STREAMS,它还忽略了那些难以实现的过时标准。
     Linux体现了自由这个词的精髓。现有的Linux特性集就是Linux公开开发模型自由发展的结果。如果一个特性没有任何价值或者创意很差,没有任何人会被迫去实现它。相反的,针对变革,Linux已经形成了一种值得称赞的态度:任何改变都必须要能通过简洁的设计及正确可靠的实现来解决现实中确实存在的问题。于是,许多出现在某些Unix变种系统中,那些出于市场宣传目的或没有普遍意义的一些特性,如内核换页机制等都被毫不迟疑地摒弃了。
     不管Linux和Unix有多大的不同,它身上都深深地打上了Unix烙印。

1.5 Linux内核版本 Top

       Linux内核有两种:稳定的和处于开发中的。稳定的内核具有工业级的强度,可以广泛地应用和部署。新推出的稳定内核大部分都只是修正了一些Bug或是加入了一些新的设备驱动程序。另一方面处于开发中的内核中许多东西变化得都很快。而且由于开发者不断试验新的解决方案,内核常常发生剧烈的变化。
Linux通过一个简单的命名机制来区分稳定的和处于开发中的内核(见图1-2)。这种机制使用三个或者四个用“.”分隔的数字来代表不同内核版本。第一个数字是主版本号,第二个数字是从版本号,第三个数字是修订版本号,第四个可选的数字为稳定版本号(stable version)。从副版本号可以反映出该内核是一个稳定版本还是一个处于开发中的版本:该数字如果是偶数,那么此内核就是稳定版;如果是奇数,那么它就是开发版。举例来说,版本号为2.6.30.1的内核,它就是一个稳定版。这个内核的主版本号是2,从版本号是6,修订版本号是30,稳定版本号是1。头两个数字在一起描述了“内核系列”—在这个例子中,就是2.6版内核系列。

 

       处于开发中的内核一般要经历几个阶段。最开始,内核开发者们开始试验新的特性,这时候出现错误和混乱是在所难免的。经过一段时间,系统渐渐成熟,最终会有一个特性审定的声明。这时候,Linus就不再接受新的特性了,而对已有特性所进行的后续工作会继续进行。当Linus认为这个新内核确实是趋于稳定后,就开始审定代码。这以后,就只允许再向其中加入修改bug的代码了。在经过一个短暂(希望如此)的准备期之后,Linus会将这个内核作为一个新的稳定版推出。例如,1.3系列的开发版稳定在2.0,而2.5稳定在2.6。
      在一个特定的系列下,Linus会定期发布新内核。每个新内核都是一个新的修订版本。比如2.6内核系列的第一个版本是2.6.0,第二个版本是2.6.1。这些修订版包含了BUG修复、新的驱动和一些新特性。但是,像2.6.3和2.6.4修订版本之间的差异是很微小的。
      这种开发方式一直持续到2004年,当时在受邀参加的Linux开发者峰会上,内核开发者们确定延长2.6内核系列,从而推迟进入到2.7系列的步伐。原因是2.6版本的内核已经被广泛接受、其已经证明了稳定成熟,而那些还不成熟的新特性其实并非人们所需。如今看来2.6版本内核的稳定出色无疑证明了该方针是多么英明。在编写本书时,2.7版本内核仍未提上议程,而且也看不出任何启动迹象。相反,每个2.6系列内核的修订版本发布变得越发长久,每个修订版都伴随有一个最小的开发版系列(称其微缩开发版)。Andrew Morton,Linus的副手,重新定义了他所维护的2.6-mm代码树(它曾经用于内存管理相关改动的测试版本),使其成为一个通用目的的测试版本。任何尚未稳定的修改都将首先进入2.6-mm树中,等其稳定后,再进入某个2.6的微缩开发版。如此策略的结果是:最近几年,每一个2.6系列的修订版本(比如2.6.29)都会较其前身有深刻的变化,也都会经历数月才面世。这种“微缩开发版方式”被证明是可行的、成功的,它更有利于在引入新特性的同时,维持系统的稳定性。想必在近期内开发策略不会改弦易辙,事实上内核开发者们就新版本的发布流程延续目前方式已经达成了一致意见。
      为了解决版本发布周期变长的副作用,内核开发者们引入了上面提到的稳定版本号。这个稳定版本号(如2.6.32.8中的8)包含了关键性bug的修改,并且常会向前移植处于开发版内核(如2.6.33)的重要修改。依靠这种方式,以前版本保证了仍然能将重点放在稳定性上。

1.6 Linux内核开发者社区 Top

      当你开始开发内核代码时,你就成为全球内核开发社区的一分子了。这个社区最重要的论坛是linux kernel mailing list(常缩写为lkml)。你可以在http://vegr.kernel.org上订阅邮件。要注意的是这个邮件列表流量很大,每天有超过几百条的消息,所以其他的订阅者(包括所有的核心开发人员,甚至包括Linus本人)可没有心思听人说废话。这个邮件列表可以给从事内核开发的人提供价值无穷的帮助,在这里,你可以寻找测试人员,接受评论(peer review),向人求zhu。
      后续内容列出了内核开发过程的全景,并详尽地描述了如何成功地加入到内核开发社区中去。但是要明白,在Linux内核邮件列表中潜伏(安静地阅读)是你阅读本书的最好补充。

 

1.7 小结 Top

      这是一本关于Linux内核的书:内核的目标,为达到目标所进行的设计以及设计的实现。这本书侧重实用,同时在讲述工作原理时会结合理论联系实践。我的目标是让你从一个业内人士的视角来欣赏和理解Linux内核的设计和实现之美。力求以一种有趣的方式(伴随着我个人在开发内核过程中收集的种种奇闻逸事和方法技巧)引导你走过跌跌撞撞的起步阶段。无论你是立志于开发内核代码,或者进行驱动开发,甚至只是希望能更好地了解Linux操作系统,你都将从本书受益。
      当你阅读本书时,我希望你有一台装有Linux的机器,我希望你能够看到内核代码。其实,这很理想了,因为这意味着你是一位Linux的使用者,并且早已经开始拿起手术刀对着源代码进行探索了,只不过需要一份结构图以便对整个经脉有个总体把握罢了。相反,你可能没有使用过Linux,只是在好奇心的驱使下希望了解一些内核设计的秘密而已。但是,如果你的目的只是撰写自己的代码,那么,源代码的作用无可替代。而且,你不需要付出任何代价,尽管用吧。
好了,最重要的是,在其中寻找快乐吧。

 

第二章 从内核出发 Top

      在这一章,我们将介绍Linux内核的一些基本常识:从何处获取源码,如何编译它,又如何安装新内核。那么,让我们考察一下内核程序与用户空间程序的差异,以及内核中所使用的通用编程结构。虽然内核在很多方面有其独特性,但从现在来看,它和其他大型软件项目并无多大差别。

2.1 获取内核源码 Top

      登录Linux内核官方网站http://www.kernel.org,可以随时获取当前版本的Linux源代码,可以是完整的压缩形式(使用tar命令创建的一个压缩文件),也可以是增量补丁形式。
      除特殊情况下需要Linux源码的旧版本外,一般都希望拥有最新的代码。kernel.org是源码的库存之处,那些领导潮流的内核开发者所发布的增量补丁也放在这里。

2.1.1 使用Git Top

      在过去的几年中,Linus和他领导的内核开发者们开始使用一个新版本的控制系统来管理Linux内核源代码。Linus创造的这个系统称为Git。与CSV这样的传统的版本控制系统不同,Git是分布式的,它的用法和工作流程对许多开发者来说都很陌生。我强烈建议使用Git来下载和管理Linux内核源代码。
你可以使用Git来获取最新提交到Linus版本树的一个副本:
      $ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
      当下载代码后,你可以更新你的分支到Linus的最新分支:
      $ git pull
      有了这两个命令,就可以获取并随时保持与内核官方的代码树一致。要提交和管理自己的修改,请看第20章。关于Git的全面讨论已经超出了本书的范围,许多在线资源都提供了有效的指导。

 

2.1.2 安装内核源代码 Top

      内核压缩以GNU zip(gzip)和bzip2两种形式发布。bzip2是默认和首选形式,因为它在压缩上比gzip更有优势。以bzip2形式发布的Linux内核叫做linux-x.y.z.tar.bz2,这里x.y.z是内核源码的具体版本。下载了源代码之后,就可以轻而易举地对其解压。如果压缩形式是bzip2,则运行:
      $ tar xvjf linux-x.y.z.tar.bz2
      如果压缩形式是GNU的zip,则运行:
      $ tar xvzf linux-x.y.z.tar.gz
      解压后的源代码位于linux-x.y.z.目录下。如果你是使用git获取和管理内核源代码,那么就不需要下载压缩文件,只要像前面描述的那样运行git clone命令,git就会下载并且解压最新的源代码。
      何处安装并触及源码
      内核源码一般安装在/usr/src/linux目录下。但请注意,不要把这个源码树用于开发,因为编译你的C库所用的内核版本就链接到这棵树。此外,不要以root身份对内核进行修改,而应当是建立自己的主目录,仅以root身份安装新内核。即使在安装新内核时,/usr/src/linux目录都应当原封不动。

2.1.3 使用补丁 Top

      在Linux内核社区中,补丁是通用语。你可以以补丁的形式发布对代码的修改,也可以以补丁的形式接收其他人所做的修改。增量补丁可以作为版本转移的桥梁。你不再需要下载庞大的内核源码的全部压缩,而只需给旧版本打上一个增量补丁,让其旧貌换新颜。这不仅节约了带宽,还省了时间。要应用增量补丁,从你的内部源码树开始,只需运行:
      $ patch -p1 < ../patch-x.y.z
      一般来说,一个给定版本的内核补丁总是打在前一个版本上。
      有关创建和应用补丁更深入的讨论会在后续章节进行。

 

2.2 内核源码树 Top

      内核源码树由很多目录组成,而大多数目录又包含更多的子目录。源码树的根目录及其子目录如表2-1所示。



 

       在源码树根目录中的很多文件值得提及。COPYING文件是内核许可证(GNU GPL v2)。CREDITS是开发了很多内核代码的开发者列表。MAINTAINERS是维护者列表,它们负责维护内核子系统和驱动程序。 Makefile是基本内核的Makefile。

 

2.3 编译内核 Top

      编译内核易如反掌。让人叹为观止的是,这实际上比编译和安装像glibc这样的系统级组伴还要简单。2.6内核提供了一套新工具,使编译内核更加容易,比早期发布的内核有了长足的进步。

2.3.1 配置内核 Top

      因为Linux源码随手可得,那就意味着在编译它之前可以配置和定制。的确,你可以把自己需要的特定功能和驱动程序编译进内核。在编译内核之前,首先你必须配置它。由于内核提供了数不胜数的功能,支持了难以计数的硬件,因而有许多东西需要配置。可以配置的各种选项,以CONFIG_FEATURE形式表示,其前缀为CONFIG。例如,对称多处理器(SMP)的配置选项为CONFIG_SMP。如果设置了该选项,则SMP启用,否则,SMP不起作用。配置选项既可以用来决定哪些文件编译进内核,也可以通过预处理命令处理代码。
这些配置项要么是二选一,要么是三选一。二选一就是yes或no。比如CONFIG_PREEMPT就是二选一,表示内核抢占功能是否开启。三选一可以是yes、no或module。module意味着该配置项被选定了,但编译的时候这部分功能的实现代码是以模块(一种可以动态安装的独立代码段)的形式生成。在三选一的情况下,显然yes选项表示把代码编译进主内核映像中,而不是作为一个模块。驱动程序一般都用三选一的配置项。
配置选项也可以是字符串或整数。这些选项并不控制编译过程,而只是指定内核源码可以访问的值,一般以预处理宏的形式表示。比如,配置选项可以指定静态分配数组的大小。
      销售商提供的内核,像Canonical的Ubuntu或者Red Hat的Fedora,他们的发布版中包含了预编译的内核,这样的内核使得所需的功能得以充分地启用,并几乎把所有的驱动程序都编译成模块。这就为大多数硬件作为独立的模块提供了坚实的内核支持。但是,话又说回来,如果你是一个内核黑客,你应当编译自己的内核,并按自己的意愿决定包括或不包含哪一模块。
内核提供了各种不同的工具来简化内核配置。最简单的一种是一个字符界面下的命令行工具:
      $ make config
      该工具会逐一遍历所有配置项,要求用户选择yes、no或是module(如果是三选一的话)。由于这个过程往往要耗费掉很长时间,所以,除非你的工作是

按小时计费的,否则应该多利用基于ncurse库编制的图形界面工具:
      $ make menuconfig
      或者,是用基于gtk+的图形工具:
      $ make gconfig
      这三种工具将所有配置项分门别类放置,比如按“处理器类型和特点”。你可以按类移动、浏览内核选项,当然也可以修改其值。
      这条命令会基于默认的配置为你的体系结构创建一个配置:
      $ make defconfig
      尽管这些缺省值有点随意性(在i386上,据说那就是Linus的配置),但是,如果你从未配置过内核,那它们会提供一个良好的开端。赶快行动吧,运行这条命令,然后回头看看,确保为你的硬件所配置的选项是启用的。
这些配置项会被存放在内核代码树根目录下的.config文件中。你很容易就能找到它(内核开发者差不多都能找到),并且可以直接修改它。在这里面查找和修改内核选项也很容易。在你修改过配置文件之后,或者在用已有的配置文件配置新的代码树的时候,你应该验证和更新配置:
      $ make oldconfig
      事实上,在编译内核之前你都应该这么做。
      配置选项CONFIG_IKCONFIG_PROC把完整的压缩过的内核配置文件存放在/proc/config.gz下,这样当你编译一个新内核的时候就可以方便地克隆当前的配置。如果你目前的内核已经启用了此选项,就可以从/proc下复制出配置文件并且使用它来编译一个新内核:
      $ zcat /proc/config.gz > .config
      $ make oldconfig
      一旦内核配置好了(不论你是如何配置的),就可以使用一个简单的命令来编译它了:
      $ make
      这跟2.6以前的版本不同,你不用在每次编译内核之间都运行make dep了—代码之间的依赖关系会自动维护。你也无须再指定像老版本中bzImage这样的编译方式或独立地编译模块,默认的Makefile规则会打点这一切。

2.3.2 减少编译的垃圾信息 Top

      如果你想尽量少地看到垃圾信息,却又不希望错过错误报告与警告信息的话,你可以用以下命令来对输出进行重定向:
      $ make > .. /detritus
      一旦你需要查看编译的输出信息,你可以查看这个文件。不过,因为错误和警告都会在屏幕上显示,所以你需要看这个文件的可能性不大。事实上,我只   不过输入如下命令:
      $ make > /dev/null
      就可把无用的输出信息重定向到永无返回值的黑洞/dev/null。

2.3.3 衍生多个编译作业 Top

      make程序能把编译过程拆分成多个并行的作业。其中的每个作业独立并发地运行,这有助于极大地加快多处理器系统上的编译过程,也有利于改善处理器的利用率,因为编译大型源代码树也包括I/O等待所花费的时间(也就是处理器空下来等待I/O请求完成所花费的时间)。
      默认情况下,make只衍生一个作业,因为Makefiles常会出现不正确的依赖信息。对于不正确的依赖,多个作业可能会互相踩踏,导致编译过程出错。当然,内核的Makefiles没有这样的编码错误,因此衍生出的多个作业编译不会出现失败。为了以多个作业编译内核,使用以下命令:
      $ make -jn
      这里,n是要衍生出的作业数。在实际中,每个处理器上一般衍生出一个或者两个作业。例如,在一个16核处理器上,你可以输入如下命令:
      $ make -j32 > /dev/null
      利用出色的distcc或者 ccache工具,也可以动态地改善内核的编译时间。

 

2.3.4 安装新内核 Top

      在内核编译好之后,你还需要安装它。怎么安装就和体系结构以及启动引导工具(boot loader)息息相关了—查阅启动引导工具的说明,按照它的指导将内核映像拷贝到合适的位置,并且按照启动要求安装它。一定要保证随时有一个或两个可以启动的内核,以防新编译的内核出现问题。
例如,在使用grub的x86系统上,可能需要把arch/i386/boot/bzImage拷贝到/boot目录下,像vmlinuz-version这样命名它,并且编辑/etc/grub/grub.conf文件,为新内核建立一个新的启动项 。使用LILO启动的系统应当编辑/etc/lilo.conf,然后运行lilo。
所幸,模块的安装是自动的,也是独立于体系结构的。以root身份,只要运行:
      % make modules_install
      就可以把所有已编译的模块安装到正确的主目录/lib/modules下。
      编译时也会在内核代码树的根目录下创建一个System.map文件。这是一份符号对照表,用以将内核符号和它们的起始地址对应起来。调试的时候,如果需要把内存地址翻译成容易理解的函数名以及变量名,这就会很有用。

2.4 内核开发的特点 Top

       相对于用户空间内应用程序的开发,内核开发有一些独特之处。尽管这些差异并不会使开发内核代码的难度超过开发用户代码,但它们依然有很大不同。
这些特点使内核成了一只性格迥异的猛兽。一些常用的准则被颠覆了,而又必须建立许多全新的准则。尽管有许多差异一目了然(人人都知道内核可以做它想做的任何事),但还是有一些差异晦暗不明。最重要的差异包括以下几种:
       内核编程时既不能访问C库也不能访问标准的C头文件。
       内核编程时必须使用GNU C。
       内核编程时缺乏像用户空间那样的内存保护机制。
       内核编程时难以执行浮点运算。
       内核给每个进程只有一个很小的定长堆栈。
       由于内核支持异步中断、抢占和SMP,因此必须时刻注意同步和并发。
       要考虑可移植性的重要性。
       让我们仔细考察一下这些要点,所有内核开发者必须牢记以上要点。

2.4.1 无libc库抑或无标准头文件 Top

      与用户空间的应用程序不同,内核不能链接使用标准C函数库—或者其他的那些库也不行。造成这种情况的原因有许多,其中就包括先有鸡还是先有蛋这个悖论。不过最主要的原因还是速度和大小。对内核来说,完整的C库—哪怕是它的一个子集,都太大且太低效了。
别着急,大部分常用的C库函数在内核中都已经得到了实现。比如操作字符串的函数组就位于lib/string.c文件中。只要包含<linux/string.h>头文件,就可以使用它们。
      头文件
      当我在本书中谈及头文件时,都指的是组成内核源代码树的内核头文件。内核源代码文件不能包含外部头文件,就像它们不能用外部库一样。
基本的头文件位于内核源代码树顶级目录下的include目录中。例如,头文件<linux/inotify.h>对应内核源代码树的include/linux/inotify.h。
体系结构相关的头文件集位于内核源代码树的arch/<architecture>/include/asm目录下。例如,如果编译的是x86体系结构,则体系结构相关的头文件就是arch/x86/include/asm。内核代码通过以asm/为前缀的方式包含这些头文件,例如<asm/ioctl.h>。

      在所有没有实现的函数中,最著名的就数printf()函数了。内核代码虽然无法调用printf(),但它提供的printk()函数几乎与printf()相同。printk()函数负责把格式化好的字符串拷贝到内核日志缓冲区上,这样,syslog程序就可以通过读取该缓冲区来获取内核信息。printk()的用法很像printf():
printk("Hello world! A string:'%s' and an integer:'%d'\n", str, i);
      printk()和printf()之间的一个显著区别在于,printk()允许你通过指定一个标志来设置优先级。syslogd会根据这个优先级标志来决定在什么地方显示这条系统消息。下面是一个使用这种优先级标志的例子:
      printk(KERN_ERR "this is an error!\n");
      注意 在KERN_ERR和要打印的消息之间没有逗号,这样写是别有用意的。优先级标志是预处理程序定义的一个描述性字符串,在编译时优先级标志就与 要打印的消息绑在一起处理。贯穿整本书,我们会使用printk()。

2.4.2 GNU C Top

      像所有自视清高的Unix内核一样,Linux内核是用C语言编写的。让人略感惊讶的是,内核并不完全符合ANSI C标准。实际上,只要有可能,内核开发者总是要用到gcc提供的许多语言的扩展部分。(gcc是多种GNU编译器的集合,它包含的C编译器既可以编译内核,也可以编译Linux系统上用C语言写的其他代码。)
      内核开发者使用的C语言涵盖了ISO C99标准和GNU C扩展特性。这其中的种种变化把Linux内核推向了gcc的怀抱,尽管目前出现了一些新的编译器如Intel C,已经支持了足够多的gcc扩展特性,完全可以用来编译Linux内核了。最早支持gcc的版本是 3.2,但是推荐使用gcc 4.4或之后的版本。Linux内核用到的ISO C99标准的扩展没有什么特别之处,而且C99作为C语言官方标准的修订本,不可能有大的或是激进的变化。让人感兴趣的,与标准C语言有区别的,通常也是人们不熟悉的那些变化,多数集中在GNU C上。就让我们研究一下内核代码中所使用到的C语言扩展中让人感兴趣的那部分吧,这些变化使内核代码有别于你所熟悉的其他项目。
1. 内联(inline)函数
      C99和GNU C均支持内联函数。inline这个名称就可以反映出它的工作方式,函数会在它所调用的位置上展开。这么做可以消除函数调用和返回所带来的开销(寄存器存储和恢复)。而且,由于编译器会把调用函数的代码和函数本身放在一起进行优化,所以也有进一步优化代码的可能。不过,这么做是有代价的(天下没有免费的午餐),代码会变长,这也就意味着占用更多的内存空间或者占用更多的指令缓存。内核开发者通常把那些对时间要求比较高,而本身长度又比较短的函数定义成内联函数。如果一个函数较大,会被反复调用,且没有特别的时间上的限制,我们并不赞成把它做成内联函数。
定义一个内联函数的时候,需要使用static作为关键字,并且用inline限定它。比如:
static inline void wolf(unsigned long tail_size)
      内联函数必须在使用之前就定义好,否则编译器就没法把这个函数展开。实践中一般在头文件中定义内联函数。由于使用了static作为关键字进行限制,所以编译时不会为内联函数单独建立一个函数体。如果一个内联函数仅仅在某个源文件中使用,那么也可以把它定义在该文件开始的地方。
在内核中,为了类型安全和易读性,优先使用内联函数而不是复杂的宏。
2. 内联汇编
      gcc编译器支持在C函数中嵌入汇编指令。当然,在内核编程的时候,只有知道对应的体系结构,才能使用这个功能。
      我们通常使用asm()指令嵌入汇编代码。例如,下面这条内联汇编指令用于执行x86处理器的rdtsc指令,返回时间戳(tsc)寄存器的值:
      unsigned int low, high;
      asm volatile("rdtsc" : "=a" (low), "=d" (high));
      /* low和high分别包含64位时间戳的低32位和高32位 */
      Linux的内核混合使用了C语言和汇编语言。在偏近体系结构的底层或对执行时间要求严格的地方,一般使用的是汇编语言。而内核其他部分的大部分代码是用C语言编写的。
3. 分支声明
      对于条件选择语句,gcc内建了一条指令用于优化,在一个条件经常出现,或者该条件很少出现的时候,编译器可以根据这条指令对条件分支选择进行优化。内核把这条指令封装成了宏,比如likely()和unlikely(),这样使用起来比较方便。
例如,下面是一个条件选择语句:
if (error) {
        /* ... */
}
如果想要把这个选择标记成绝少发生的分支:
/* 我们认为error绝大多数时间都会为0...*/
if (unlikely(error)) {
        /* ... */
}
相反,如果我们想把一个分支标记为通常为真的选择:
/* 我们认为success通常都不会为0 */
if  (likely(success)) {
   /* ... */
}
      在你想要对某个条件选择语句进行优化之前,一定要搞清楚其中是不是存在这么一个条件,在绝大多数情况下都会成立。这点十分重要:如果你的判断正确,确实是这个条件占压倒性的地位,那么性能会得到提升;如果你搞错了,性能反而会下降。正如上面这些例子所示,通常在对一些错误条件进行判断的时候会用到unlikely()和likely()。你可以猜到,unlikely()在内核中会得到更广泛的使用,因为if语句往往判断一种特殊情况。

2.4.3 没有内存保护机制 Top

      如果一个用户程序试图进行一次非法的内存访问,内核就会发现这个错误,发送 SIGSEGV信号,并结束整个进程。然而,如果是内核自己非法访问了内存,那后果就很难控制了。(毕竟,有谁能照顾内核呢?)内核中发生的内存错误会导致oops,这是内核中出现的最常见的一类错误。在内核中,不应该去做访问非法的内存地址,引用空指针之类的事情,否则它可能会死掉,却根本不告诉你一声—在内核里,风险常常会比外面大一些。
      此外,内核中的内存都不分页。也就是说,你每用掉一个字节,物理内存就减少一个字节。所以,在你想往内核里加入什么新功能的时候,要记住这一点。

2.4.4 不要轻易在内核中使用浮点数 Top

      在用户空间的进程内进行浮点操作的时候,内核会完成从整数操作到浮点数操作的模式转换。在执行浮点指令时到底会做些什么,因体系结构不同,内核的选择也不同,但是,内核通常捕获陷阱并着手于整数到浮点方式的转变。
      与用户空间进程不同,内核并不能完美地支持浮点操作,因为它本身不能陷入。 在内核中使用浮点数时,除了要人工保存和恢复浮点寄存器,还有其他一些琐碎的事情要做。如果要直截了当地回答,那就是:别这么做了,除了一些极少的情况,不要在内核中使用浮点操作。

2.4.5 容积小而固定的栈 Top

      用户空间的程序可以从栈上分配大量的空间来存放变量,甚至巨大的结构体或者是包含数以千计的数据项的数组都没有问题。之所以可以这么做,是因为用户空间的栈本身比较大,而且还能动态地增长(年长的开发者回想一下DOS那个年代,这种低级的操作系统即使在用户空间也只有固定大小的栈)。
      内核栈的准确大小随体系结构而变。在x86上,栈的大小在编译时配置,可以是4KB也可以是8KB。从历史上说,内核栈的大小是两页,这就意味着,32位机的内核栈是8KB,而64位机是16KB,这是固定不变的。每个处理器都有自己的栈。
      关于内核栈的更多内容,会在后面的章节中讨论。

 

2.4.6 同步和并发 Top

       内核很容易产生竞争条件。和单线程的用户空间程序不同,内核的许多特性都要求能够并发地访问共享数据,这就要求有同步机制以保证不出现竞争条件,特别是:
       Linux是抢占多任务操作系统。内核的进程调度程序即兴对进程进行调度和重新调度。内核必须和这些任务同步。

       Linux内核支持对称多处理器系统(SMP)。所以,如果没有适当的保护,同时在两个或两个以上的处理器上执行的内核代码很可能会同时访问共享的同一个资源。
       中断是异步到来的,完全不顾及当前正在执行的代码。也就是说,如果不加以适当的保护,中断完全有可能在代码访问资源的时候到来,这样,中段处理程序就有可能访问同一资源。
       Linux内核可以抢占。所以,如果不加以适当的保护,内核中一段正在执行的代码可能会被另外一段代码抢占,从而有可能导致几段代码同时访问相同的资源。
       常用的解决竞争的办法是自旋锁和信号量。 我们将在后面的章节中详细讨论同步和并发执行。

2.4.7 可移植性的重要性 Top

      尽管用户空间的应用程序不太注意移植问题,然而Linux却是一个可移植的操作系统,并且要一直保持这种特点。也就是说,大部分C代码应该与体系结构无关,在许多不同体系结构的计算机上都能够编译和执行,因此,必须把与体系结构相关的代码从内核代码树的特定目录中适当地分离出来。
诸如保持字节序、64位对齐、不假定字长和页面长度等一系列准则都有助于移植性。对移植性的深度讨论将在后面的章节中进行。

2.5 小结 Top

      毫无疑义,内核有独一无二的特质。它实施自己的规则和奖罚措施,拥有整个系统的最高管理权。当然,Linux内核的复杂性和高门槛与其他大型软件项目并无差异。在内核开发之路上最重要的步骤是要意识到内核并没有那么可怕。陌生是肯定的,但真的就不可逾越?事实并非如此。
      本章和以前的章节为贯穿本书剩余章节所讨论的主题奠定了基础。在后续的每一章中,我们都会涵盖内核的一个具体概念或子系统。在探索的征途中,最重要的是要阅读和修改内核源代码,只有通过实际的阅读和实践才会理解内核。内核源代码是可以免费获取的,直接用就可以了!

第三章 进 程 管 理 Top

      本章引入进程的概念,进程是Unix操作系统抽象概念中最基本的一种。其中涉及进程的定义以及相关的概念,比如线程;然后讨论Linux内核如何管理每个进程:它们在内核中如何被列举,如何创建,最终又如何消亡。我们拥有操作系统就是为了运行用户程序,因此,进程管理就是所有操作系统的心脏所在,Linux也不例外。

 

3.1 进程 Top

      进程就是处于执行期的程序(目标码存放在某种存储介质上)。但进程并不仅仅局限于一段可执行程序代码(Unix称其为代码段,text section)。通常进程还要包含其他资源,像打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程(thread of execution),当然还包括用来存放全局变量的数据段等。 实际上,进程就是正在执行的程序代码的实时结果。内核需要有效而又透明地管理所有细节。
执行线程,简称线程(thread),是在进程中活动的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。在传统的Unix系统中,一个进程只包含一个线程,但现在的系统中,包含多个线程的多线程程序司空见惯。稍后你会看到,Linux系统的线程实现非常特别:它对线程和进程并不特别区分。对Linux而言,线程只不过是一种特殊的进程罢了。
      在现代操作系统中,进程提供两种虚拟机制:虚拟处理器和虚拟内存。虽然实际上可能是许多进程正在分享一个处理器,但虚拟处理器给进程一种假象,让这些进程觉得自己在独享处理器。第4章将详细描述这种虚拟机制。而虚拟内存让进程在分配和管理内存时觉得自己拥有整个系统的所有内存资源。第12章将描述虚拟内存机制。有趣的是, 注意在线程之间可以共享虚拟内存,但每个都拥有各自的虚拟处理器。
      程序本身并不是进程,进程是处于执行期的程序以及相关的资源的总称。实际上,完全可能存在两个或多个不同的进程执行的是同一个程序。并且两个或两个以上并存的进程还可以共享许多诸如打开的文件、地址空间之类的资源。
      无疑,进程在创建它的时刻开始存活。在Linux系统中,这通常是调用fork()系统的结果,该系统调用通过复制一个现有进程来创建一个全新的进程。调用fork()的进程称为父进程,新产生的进程称为子进程。在该调用结束时,在返回点这个相同位置上,父进程恢复执行,子进程开始执行。fork() 系统调用从内核返回两次:一次回到父进程,另一次回到新产生的子进程。
      通常,创建新的进程都是为了立即执行新的、不同的程序,而接着调用exec()这组函数就可以创建新的地址空间,并把新的程序载入其中。在现代Linux内核中,fork()实际上是由clone()系统调用实现的,后者将在后面讨论。
最终,程序通过exit()系统调用退出执行。这个函数会终结进程并将其占用的资源释放掉。父进程可以通过wait4()系统调用查询子进程是否终结,这其实使得进程拥有了等待特定进程执行完毕的能力。进程退出执行后被设置为僵死状态,直到它的父进程调用wait()或waitpid()为止。
       注意 进程的另一个名字是任务(task)。Linux内核通常把进程也叫做任务。本书会交替使用这两个术语,不过我所说的任务通常指的是从内核观点所看到的进程。

3.2 进程描述符及任务结构 Top

      内核把进程的列表存放在叫做任务队列(task list)的双向循环链表中。链表中的每一项都是类型为task_struct、称为进程描述符(process descriptor)的结构,该结构定义在<linux/sched.h>文件中。进程描述符中包含一个具体进程的所有信息。
       task_struct相对较大,在32位机器上,它大约有1.7KB。但如果考虑到该结构内包含了内核管理一个进程所需的所有信息,那么它的大小也算相当小了。进程描述符中包含的数据能完整地描述一个正在执行的程序:它打开的文件,进程的地址空间,挂起的信号,进程的状态,还有其他更多信息(见图3-1)。



 

3.2.1 分配进程描述符 Top

      Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色(cache coloring)(参见第12章)的目的。在2.6以前的内核中,各个进程的task_struct存放在它们内核栈的尾端。这样做是为了让那些像x86那样寄存器较少的硬件体系结构只要通过栈指针就能计算出它的位置,而避免使用额外的寄存器专门记录。由于现在用slab分配器动态生成task_struct,所以只需在栈底(对于向下增长的栈来说)或栈顶(对于向上增长的栈来说)创建一个新的结构struct thread_info(见图3-2)。
      在x86上,struct thread_info在文件<asm/thread_info.h>中定义如下:



       每个任务的thread_info结构在它的内核栈的尾端分配。结构中task域中存放的是指向该任务实际task_struct的指针。

3.2.2 进程描述符的存放 Top

      内核通过一个唯一的进程标识值(process identification value)或PID来标识每个进程。PID是一个数,表示为pid_t隐含类型,实际上就是一个int类型。为了与老版本的Unix和Linux兼容,PID的最大值默认设置为32768(short int短整型的最大值),尽管这个值也可以增加到高达400万(这受<linux/threads.h>中所定义PID最大值的限制)。内核把每个进程的PID存放在它们各自的进程描述符中。
      这个最大值很重要,因为它实际上就是系统中允许同时存在的进程的最大数目。尽管32768对于一般的桌面系统足够用了,但是大型服务器可能需要更多进程。这个值越小,转一圈就越快,本来数值大的进程比数值小的进程迟运行,但这样一来就破坏了这一原则。如果确实需要的话,可以不考虑与老式系统的兼容,由系统管理员通过修改/proc/sys/kernel/pid_max来提高上限。
在内核中,访问任务通常需要获得指向其task_struct的指针。实际上,内核中大部分处理进程的代码都是直接通过task_struct进行的。因此,通过current宏查找到当前正在运行进程的进程描述符的速度就显得尤为重要。硬件体系结构不同,该宏的实现也不同,它必须针对专门的硬件体系结构做处理。有的硬件体系结构可以拿出一个专门寄存器来存放指向当前进程task_struct的指针,用于加快访问速度。而有些像x86这样的体系结构(其寄存器并不富余),就只能在内核栈的尾端创建thread_info结构,通过计算偏移间接地查找task_struct结构。
      在x86系统上,current把栈指针的后13个有效位屏蔽掉,用来计算出thread_info的偏移。该操作是通过current_thread_info()函数来完成的。汇编代码如下:
movl $-8192, %eax
andl %esp, %eax
这里假定栈的大小为8KB。当4KB的栈启用时,就要用4096,而不是8192。
      最后,current再从thread_info的task域中提取并返回task_struct的地址:
current_thread_info()->task;
      对比一下这部分在PowerPC上的实现(IBM基于RISC的现代微处理器),我们可以发现PPC当前的task_struct是保存在一个寄存器中的。也就是说,在PPC上,current宏只需把r2寄存器中的值返回就行了。与x86不一样,PPC有足够多的寄存器,所以它的实现有这样选择的余地。而访问进程描述符是一个重要的频繁操作,所以PPC的内核开发者觉得完全有必要为此使用一个专门的寄存器。

3.2.3 进程状态 Top

      进程描述符中的state域描述了进程的当前状态(见图3-3)。系统中的每个进程都必然处于五种进程状态中的一种。该域的值也必为下列五种状态标志之一:
       TASK_RUNNING(运行)—进程是可执行的;它或者正在执行,或者在运行队列中等待执行(运行队列将会在第4章中讨论)。这是进程在用户空间中执行的唯一可能的状态;这种状态也可以应用到内核空间中正在执行的进程。
       TASK_INTERRUPTIBLE(可中断)—进程正在睡眠(也就是说它被阻塞),等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行。处于此状态的进程也会因为接收到信号而提前被唤醒并随时准备投入运行。
       TASK_UNINTERRUPTIBLE(不可中断)—除了就算是接收到信号也不会被唤醒或准备投入运行外,这个状态与可打断状态相同。这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。由于处于此状态的任务对信号不做响应,所以较之可中断状态,使用得较少。
 __TASK_TRACED—被其他进程跟踪的进程,例如通过ptrace对调试程序进行跟踪。
 __TASK_STOPPED(停止)—进程停止执行;进程没有投入运行也不能投入运行。通常这种状态发生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。


 

3.2.4 设置当前进程状态 Top

     内核经常需要调整某个进程的状态。这时最好使用set_task_state(task, state)函数:
set_task_state(task, state);  /* 将任务task的状态设置为 state */
      该函数将指定的进程设置为指定的状态。必要的时候,它会设置内存屏障来强制其他处理器作重新排序。(一般只有在SMP系统中有此必要。)否则,它等价于:
      task->state = state;
      set_current_state(state)和set_task_state(current, state)含义是等同的。参看<linux/sched.h>中对这些相关函数实现的说明。

3.2.5 进程上下文 Top

       可执行程序代码是进程的重要组成部分。这些代码从一个可执行文件载入到进程的地址空间执行。一般程序在用户空间执行。当一个程序调执行了系统调用(参见第5章)或者触发了某个异常,它就陷入了内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中。在此上下文中current宏是有效的。除非在此间隙有更高优先级的进程需要执行并由调度器做出了相应调整,否则在内核退出的时候,程序恢复在用户空间会继续执行。
       系统调用和异常处理程序是对内核明确定义的接口。进程只有通过这些接口才能陷入内核执行—对内核的所有访问都必须通过这些接口。

3.2.6 进程家族树 Top

      Unix系统的进程之间存在一个明显的继承关系,在Linux系统中也是如此。所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本(initscript)并执行其他的相关程序,最终完成系统启动的整个过程。
      系统中的每个进程必有一个父进程,相应的,每个进程也可以拥有零个或多个子进程。拥有同一个父进程的所有进程被称为兄弟。进程间的关系存放在进程描述符中。每个task_struct都包含一个指向其父进程tast_struct、叫做parent的指针,还包含一个称为children的子进程链表。所以,对于当前进程,可以通过下面的代码获得其父进程的进程描述符:
struct task_struct    *my_parent = current->parent;
同样,也可以按以下方式依次访问子进程:
struct task_struct *task;
struct list_head *list;

list_for_each(list, &current->children) {
         task = list_entry(list, struct task_struct, sibling);
         /* task 现在指向当前的某个子进程 */
}
init进程的进程描述符是作为init_task静态分配的。下面的代码可以很好地演示所有进程之间的关系:
struct task_struct *task;

for (task = current; task != &init_task; task = task->parent)
     ;
/* task 现在指向init */
      实际上,你可以通过这种继承体系从系统的任何一个进程出发查找到任意指定的其他进程。但大多数时候,只需要通过简单的重复方式就可以遍历系统中的所有进程。这非常容易做到,因为任务队列本来就是一个双向的循环链表。对于给定的进程,获取链表中的下一个进程:
list_entry(task->tasks.next, struct task_struct, tasks)
获取前一个进程的方法与之相同:
list_entry(task->tasks.prev, struct task_struct, tasks)
      这两个例程分别通过next_task(task)宏和prev_task(task)宏实现。而实际上,for_each_ process(task)宏提供了依次访问整个任务队列的能力。每次访问,任务指针都指向链表中的下一个元素:
struct task_struct *task;

for_each_process(task) {
         /* 它打印出每一个任务的名称和PID*/
         printk("%s[%d]\n", task->comm, task->pid);
}
      特别提醒 在一个拥有大量进程的系统中通过重复来遍历所有的进程代价是很大的。因此,如果没有充足的理由(或者别无他法),别这样做。

 

3.3 进程创建 Top

      Unix的进程创建很特别。许多其他的操作系统都提供了产生(spawn)进程的机制,首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。Unix采用了与众不同的实现方式,它把上述步骤分解到两个单独的函数中去执行:fork()和exec()。首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID(每个进程唯一)、PPID(父进程的进程号,子进程将其设置为被拷贝进程的PID)和某些资源和统计量(例如,挂起的信号,它没有必要被继承)。exec()函数负责读取可执行文件并将其载入地址空间开始运行。把这两个函数组合起来使用的效果跟其他系统使用的单一函数的效果相似。

3.3.1 写时拷贝 Top

      传统的fork()系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单并且效率低下,因为它拷贝的数据也许并不共享,更糟的情况是,如果新进程打算立即执行一个新的映像,那么所有的拷贝都将前功尽弃。Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。
      只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候才进行。在页根本不会被写入的情况下(举例来说,fork()后立即调用exec())它们就无须复制了。
      fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量根本就不会被使用的数据(地址空间里常常包含数十兆的数据)。由于Unix强调进程快速执行的能力,所以这个优化是很重要的。

 

3.3.2 fork() Top

      Linux通过clone()系统调用实现fork()。这个调用通过一系列的参数标志来指明父、子进程需要共享的资源(关于这些标志更多的信息请参考本章后面3.4节)。fork()、vfork()和__clone ()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork()。
do_fork完成了创建中的大部分工作,它的定义在kernel/fork.c文件中。该函数调用copy_process()函数,然后让进程开始运行。copy_process()函数完成的工作很有意思:
1)调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同。此时,子进程和父进程的描述符是完全相同的。
2)检查并确保新创建这个子进程后,当前用户所拥有的进程数目没有超出给它分配的资源的限制。
3)子进程着手使自己与父进程区别开来。进程描述符内的许多成员都要被清0或设为初始值。那些不是继承而来的进程描述符成员,主要是统计信息。task_struct中的大多数数据都依然未被修改。
4)子进程的状态被设置为TASK_UNINTERRUPTIBLE,以保证它不会投入运行。
5)copy_process()调用copy_flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0。表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
6)调用alloc _pid()为新进程分配一个有效的PID。
7)根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。在一般情况下,这些资源会被给定进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到这里。
8)最后,copy_process()做扫尾工作并返回一个指向子进程的指针。
再回到do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程首先执行。因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能会开始向地址空间写入。

3.3.3 vfork() Top

        除了不拷贝父进程的页表项外,vfork()系统调用和fork()的功能相同。子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程被阻塞,直到子进程退出或执行exec()。子进程不能向地址空间写入。在过去的3BSD时期,这个优化是很有意义的,那时并未使用写时拷贝页来实现fork()。现在由于在执行fork()时引入了写时拷贝页并且明确了子进程先执行,vfork()的好处就仅限于不拷贝父进程的页表项了。如果Linux将来fork()有了写时拷贝页表项,那么vfork()就彻底没用了。另外由于vfork()语意非常微妙(试想,如果exec()调用失败会发生什么),所以理想情况下,系统最好不要调用vfork(),内核也不用实现它。完全可以把vfork()实现成一个普普通通的fork()—实际上,Linux 2.2以前都是这么做的。
vfork()系统调用的实现是通过向clone()系统调用传递一个特殊标志来进行的。
      1)在调用copy_process()时,task_struct的vfor_done成员被设置为NULL。
      2)在执行do_fork()时,如果给定特别标志,则vfork_done会指向一个特定地址。
      3)子进程先开始执行后,父进程不是马上恢复执行,而是一直等待,直到子进程通过vfork_done指针向它发送信号。
      4)在调用mm_release()时,该函数用于进程退出内存地址空间,并且检查vfork_done是否为空,如果不为空,则会向父进程发送信号。
      5)回到do_fork(),父进程醒来并返回。
      如果一切执行顺利,子进程在新的地址空间里运行而父进程也恢复了在原地址空间的运行。这样,开销确实降低了,不过它的实现并不是优良的。

3.4 线程在Linux中的实现 Top

       线程机制是现代编程技术中常用的一种抽象概念。该机制提供了在同一程序内共享内存地址空间运行的一组线程。这些线程还可以共享打开的文件和其他资源。线程机制支持并发程序设计技术(concurrent programming),在多处理器系统上,它也能保证真正的并行处理(parallelism)。
       Linux实现线程的机制非常独特。从内核的角度来说,它并没有线程这个概念。Linux把所有的线程都当做进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属于自己的task_struct,所以在内核中,它看起来就像是一个普通的进程(只是线程和其他一些进程共享某些资源,如地址空间)。
上述线程机制的实现与Microsoft Windows或是Sun Solaris等操作系统的实现差异非常大。这些系统都在内核中提供了专门支持线程的机制(这些系统常常把线程称作轻量级进程(lightweight processes))。“轻量级进程”这种叫法本身就概括了Linux在此处与其他系统的差异。在其他的系统中,相较于重量级的进程,线程被抽象成一种耗费较少资源,运行迅速的执行单元。而对于Linux来说,它只是一种进程间共享资源的手段(Linux的进程本身就够轻量级了)。举个例子来说,假如我们有一个包含四个线程的进程,在提供专门线程支持的系统中,通常会有一个包含指向四个不同线程的指针的进程描述符。该描述符负责描述像地址空间、打开的文件这样的共享资源。线程本身再去描述它独占的资源。相反,Linux仅仅创建四个进程并分配四个普通的task_sturct结构。建立这四个进程时指定他们共享某些资源,这是相当高雅的做法。

 

3.4.1 创建线程 Top

      线程的创建和普通进程的创建类似,只不过在调用clone()的时候需要传递一些参数标志来指明需要共享的资源:
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
上面的代码产生的结果和调用fork()差不多,只是父子俩共享地址空间、文件系统资源、文件描述符和信号处理程序。换个说法就是,新建的进程和它的父进程就是流行的所谓线程。
对比一下,一个普通的fork()的实现是:
clone(SIGCHLD, 0);
而vfork()的实现是:
clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);
传递给clone()的参数标志决定了新创建进程的行为方式和父子进程之间共享的资源种类。表3-1列举了这些clone()用到的参数标志以及它们的作用,这些是在<linux/sched.h>中定义的。



 

 

3.4.2 内核线程 Top

      内核经常需要在后台执行一些操作。这种任务可以通过内核线程(kernel thread)完成—独立运行在内核空间的标准进程。内核线程和普通的进程间的区别在于内核线程没有独立的地址空间(实际上指向地址空间的mm指针被设置为NULL)。它们只在内核空间运行,从来不切换到用户空间去。内核进程和普通进程一样,可以被调度,也可以被抢占。
      Linux确实会把一些任务交给内核线程去做,像flush和ksofirqd这些任务就是明显的例子。在装有Linux系统的机子上运行ps -ef命令,你可以看到内核线程,有很多!这些线程在系统启动时由另外一些内核线程创建。实际上,内核线程也只能由其他内核线程创建。内核是通过从kthreadd内核进程中衍生出所有新的内核线程来自动处理这一点的。在<linux/kthread.h>中申明有接口,于是,从现有内核线程中创建一个新的内核线程的方法如下:

新的任务是由kthread内核进程通过clone()系统调用而创建的。新的进程将运行threadfn函数,给其传递的参数为data。进程会被命名为namefmt,namefmt接受可变参数列表类似于printf()的格式化参数。新创建的进程处于不可运行状态,如果不通过调用wake_up_process()明确地唤醒它,它不会主动运行。创建一个进程并让它运行起来,可以通过调用kthread_run()来达到:

      这个例程是以宏实现的,只是简单地调用了 kthread_create()和wake_up_process():

      内核线程启动后就一直运行直到调用do_exit()退出,或者内核的其他部分调用kthread_stop()退出,传递给kthread_stop()的参数为kthread_create()函数返回的task_struct结构的地址:
int kthread_stop(struct task_struct *k)
我们将在以后的内容中详细讨论具体的内核线程。

3.5 进程终结 Top

      虽然让人伤感,但进程终归是要终结的。当一个进程终结时,内核必须释放它所占有的资源并把这一不幸告知其父进程。
一般来说,进程的析构是自身引起的。它发生在进程调用exit()系统调用时,既可能显式地调用这个系统调用,也可能隐式地从某个程序的主函数返回(其实C语言编译器会在main()函数的返回点后面放置调用exit()的代码)。当进程接受到它既不能处理也不能忽略的信号或异常时,它还可能被动地终结。不管进程是怎么终结的,该任务大部分都要靠do_exit()(定义于kernel/exit.c)来完成,它要做下面这些烦琐的工作:
1)将tast_struct中的标志成员设置为PF_EXITING。
2)调用del_timer_sync()删除任一内核定时器。根据返回的结果,它确保没有定时器在排队,也没有定时器处理程序在运行。
3)如果BSD的进程记账功能是开启的,do_exit()调用acct_update_integrals()来输出记账信息。
4)然后调用exit_mm()函数释放进程占用的mm_struct,如果没有别的进程使用它们(也就是说,这个地址空间没有被共享),就彻底释放它们。
5)接下来调用sem_exit()函数。如果进程排队等候IPC信号,它则离开队列。
6)调用exit_files()和exit_fs(),以分别递减文件描述符、文件系统数据的引用计数。如果其中某个引用计数的数值降为零,那么就代表没有进程在使用相应的资源,此时可以释放。
7)接着把存放在task_struct的exit_code成员中的任务退出代码置为由exit()提供的退出代码,或者去完成任何其他由内核机制规定的退出动作。退出代码存放在这里供父进程随时检索。
8)调用exit_notify()向父进程发送信号,给子进程重新找养父,养父为线程组中的其他线程或者为init进程,并把进程状态(存放在task_struct结构的exit_state中)设成EXIT_ZOMBIE。
9)do_exit()调用schedule()切换到新的进程(参看第4章)。因为处于EXIT_ZOMBIE状态的进程不会再被调度,所以这是进程所执行的最后一段代码。do_exit()永不返回。
      至此,与进程相关联的所有资源都被释放掉了(假设该进程是这些资源的唯一使用者)。进程不可运行(实际上也没有地址空间让它运行)并处于EXIT_ZOMBIE 退出状态。它占用的所有内存就是内核栈、thread_info结构和tast_struct结构。此时进程存在的唯一目的就是向它的父进程提供信息。父进程检索到信息后,或者通知内核那是无关的信息后,由进程所持有的剩余内存被释放,归还给系统使用。

3.5.1 删除进程描述符 Top

      在调用了do_exit()之后,尽管线程已经僵死不能再运行了,但是系统还保留了它的进程描述符。前面说过,这样做可以让系统有办法在子进程终结后仍能获得它的信息。因此,进程终结时所需的清理工作和进程描述符的删除被分开执行。在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct结构才被释放。
wait()这一族函数都是通过唯一(但是很复杂)的一个系统调用wait4()来实现的。它的标准动作是挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回该子进程的PID。此外,调用该函数时提供的指针会包含子函数退出时的退出代码。
当最终需要释放进程描述符时,release_task()会被调用,用以完成以下工作:
1)它调用__exit_signal(),该函数调用_unhash_process(),后者又调用detach_pid()从pidhash上删除该进程,同时也要从任务列表中删除该进程。
2)_exit_signal()释放目前僵死进程所使用的所有剩余资源,并进行最终统计和记录。
3)如果这个进程是线程组最后一个进程,并且领头进程已经死掉,那么release_task()就要通知僵死的领头进程的父进程。
4)release_task()调用put_task_struct()释放进程内核栈和thread_info结构所占的页,并释放tast_struct所占的slab高速缓存。
至此,进程描述符和所有进程独享的资源就全部释放掉了。
3.5.2 孤儿进程造成的进退维谷
如果父进程在子进程之前退出,必须有机制来保证子进程能找到一个新的父亲,否则这些成为孤儿的进程就会在退出时永远处于僵死状态,白白地耗费内存。前面的部分已经有所暗示,对于这个问题,解决方法是给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init做它们的父进程。在do_exit()中会调用exit_notify(),该函数会调用forget_original_parent(),而后者会调用find_new_reaper()来执行寻父过程:

      这段代码试图找到进程所在的线程组内的其他进程。如果线程组内没有其他的进程,它就找到并返回的是init进程。现在,给子进程找到合适的养父进程了,只需要遍历所有子进程并为它们设置新的父进程:

然后调用ptrace_exit_finish()同样进行新的寻父过程,不过这次是给ptraced的子进程寻找父亲。

      这段代码遍历了两个链表:子进程链表和ptrace子进程链表,给每个子进程设置新的父进程。这两个链表同时存在的原因很有意思,它也是2.6内核的一个新特性。当一个进程被跟踪时,它的临时父亲设定为调试进程。此时如果它的父进程退出了,系统会为它和它的所有兄弟重新找一个父进程。在以前的内核中,这就需要遍历系统所有的进程来找这些子进程。现在的解决办法是在一个单独的被ptrace跟踪的子进程链表中搜索相关的兄弟进程—用两个相对较小的链表减轻了遍历带来的消耗。
      一旦系统为进程成功地找到和设置了新的父进程,就不会再有出现驻留僵死进程的危险了。init进程会例行调用wait()来检查其子进程,清除所有与其相关的僵死进程。

3.6 小结 Top

      在本章中,我们考察了操作系统中的核心概念—进程。我们也讨论了进程的一般特性,它为何如此重要,以及进程与线程之间的关系。然后,讨论了Linux如何存放和表示进程(用task_struct 和 thread_info),如何创建进程(通过fork(),实际上最终是clone()),如何把新的执行映像装入到地址空间(通过exec()系统调用族),如何表示进程的层次关系,父进程又是如何收集其后代的信息(通过wait()系统调用族),以及进程最终如何消亡(强制或自愿地调用exit())。进程是一个非常基础、非常关键的抽象概念,位于每一种现代操作系统的核心位置,也是我们拥有操作系统(用来运行程序)的最终原因。
      第4章讨论进程调度,内核以这种微妙而有趣的方式来决定哪个进程运行,何时运行,以何种顺序运行。

  • 大小: 41 KB
  • 大小: 12.4 KB
  • 大小: 32.5 KB
  • 大小: 50.7 KB
  • 大小: 51.1 KB
  • 大小: 54.9 KB
  • 大小: 25.6 KB
  • 大小: 21.4 KB
  • 大小: 52.5 KB
评论 共 0 条 请登录后发表评论

发表评论

您还没有登录,请您登录后再发表评论

文章信息

  • hzbook在2011-07-04创建
  • hzbook在2011-07-05更新
  • 标签: Linux, 内核, 系统, 开发
Global site tag (gtag.js) - Google Analytics