├── 第一章
├── 1.png
├── 1_1.png
├── 1_2.png
├── 1_3.png
├── 1_4.png
├── 1_5.png
├── 1_6.png
├── 1_7.png
├── 1_8.png
├── 1_9.png
├── introduction.md
├── section1_7.md
├── section1_8.md
├── exercise1.md
├── section1_4.md
├── section1_2.md
├── section1_3.md
├── section1_5.md
├── section1_1.md
└── section1_6.md
├── 第三章
├── 3_1.png
├── 3_2.png
├── 3_3.png
├── 3_4.png
├── 3_5.png
├── 3_6.png
├── 3_7.png
├── 3_8.png
├── 3_9.png
├── 3_10.png
├── 3_11.png
├── introduction.md
├── section3_8.md
├── exercise3.md
├── section3_7.md
├── section3_5.md
├── section3_6.md
├── section3_2.md
├── section3_3.md
└── section3_1.md
├── 第二章
├── 2_1.png
├── 2_2.png
├── 2_3.png
├── 2_4.png
├── 2_5.png
├── 2_6.png
├── 2_7.png
├── 2_8.png
├── 2_9.png
├── 2_10.png
├── 2_11.png
├── 2_12.png
├── 2_13.png
├── 2_14.png
├── 2_15.png
├── 2_16.png
├── 2_17.png
├── section2_7.md
├── introduction.md
├── exercise2.md
├── section2_6.md
├── section2_4.md
├── section2_3.md
├── section2_2.md
└── section2_1.md
├── 第五章
├── 5_1.png
├── 5_2.png
├── 5_3.png
├── 5_4.png
├── 5_5.png
├── 5_6.png
├── 5_7.png
├── 5_8.png
├── 5_9.png
├── introduction.md
├── section5_6.md
├── exercise5.md
├── section5_1.md
└── section5_2.md
├── 第六章
├── 6_1.png
├── 6_2.png
├── introduction.md
├── section6_7.md
├── exercise6.md
├── section6_2.md
├── section6_4.md
├── section6_1.md
├── section6_5.md
└── section6_3.md
├── 第四章
├── 4_1.png
├── 4_2.png
├── 4_4.png
├── 4_5.png
├── 4_6.png
├── 4_7.png
├── 4_8.png
├── 4_9.png
├── 4_11.png
├── 4_12.png
├── 4_13.png
├── 4_14.png
├── 4_15.png
├── introduction.md
├── section4_7.md
├── exercise4.md
├── section4_3.md
├── section4_1.md
└── section4_5.md
├── 第七章
├── 图7_1.png
├── section7_5.md
├── introduction.md
├── exercise7.md
├── section7_3.md
├── section7_1.md
└── section7_4.md
├── 第九章
├── 图9_1.jpg
├── 图9_2.png
├── 图9_3.png
├── 图9_4.jpg
├── 图9_5.jpg
├── 图9_6.jpg
├── 图9_7.jpg
├── introduction.md
├── section9_6.md
├── exercise9.md
├── section9_1.md
├── section9_3.md
└── section9_2.md
├── 第八章
├── 图8_1.jpg
├── 图8_1.png
├── 图8_10.png
├── 图8_11.jpg
├── 图8_12.jpg
├── 图8_13.PNG
├── 图8_2.jpg
├── 图8_3.PNG
├── 图8_4.PNG
├── 图8_5.jpg
├── 图8_6.png
├── 图8_7.PNG
├── 图8_9.jpg
├── section8_6.md
├── introduction.md
├── exercise8.md
├── section8_3.md
├── section8_4.md
├── section8_1.md
└── section8_5.md
├── README.md
└── SUMMARY.md
/第一章/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第一章/1.png
--------------------------------------------------------------------------------
/第一章/1_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第一章/1_1.png
--------------------------------------------------------------------------------
/第一章/1_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第一章/1_2.png
--------------------------------------------------------------------------------
/第一章/1_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第一章/1_3.png
--------------------------------------------------------------------------------
/第一章/1_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第一章/1_4.png
--------------------------------------------------------------------------------
/第一章/1_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第一章/1_5.png
--------------------------------------------------------------------------------
/第一章/1_6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第一章/1_6.png
--------------------------------------------------------------------------------
/第一章/1_7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第一章/1_7.png
--------------------------------------------------------------------------------
/第一章/1_8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第一章/1_8.png
--------------------------------------------------------------------------------
/第一章/1_9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第一章/1_9.png
--------------------------------------------------------------------------------
/第三章/3_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第三章/3_1.png
--------------------------------------------------------------------------------
/第三章/3_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第三章/3_2.png
--------------------------------------------------------------------------------
/第三章/3_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第三章/3_3.png
--------------------------------------------------------------------------------
/第三章/3_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第三章/3_4.png
--------------------------------------------------------------------------------
/第三章/3_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第三章/3_5.png
--------------------------------------------------------------------------------
/第三章/3_6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第三章/3_6.png
--------------------------------------------------------------------------------
/第三章/3_7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第三章/3_7.png
--------------------------------------------------------------------------------
/第三章/3_8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第三章/3_8.png
--------------------------------------------------------------------------------
/第三章/3_9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第三章/3_9.png
--------------------------------------------------------------------------------
/第二章/2_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第二章/2_1.png
--------------------------------------------------------------------------------
/第二章/2_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第二章/2_2.png
--------------------------------------------------------------------------------
/第二章/2_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第二章/2_3.png
--------------------------------------------------------------------------------
/第二章/2_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第二章/2_4.png
--------------------------------------------------------------------------------
/第二章/2_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第二章/2_5.png
--------------------------------------------------------------------------------
/第二章/2_6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第二章/2_6.png
--------------------------------------------------------------------------------
/第二章/2_7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第二章/2_7.png
--------------------------------------------------------------------------------
/第二章/2_8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第二章/2_8.png
--------------------------------------------------------------------------------
/第二章/2_9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第二章/2_9.png
--------------------------------------------------------------------------------
/第五章/5_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第五章/5_1.png
--------------------------------------------------------------------------------
/第五章/5_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第五章/5_2.png
--------------------------------------------------------------------------------
/第五章/5_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第五章/5_3.png
--------------------------------------------------------------------------------
/第五章/5_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第五章/5_4.png
--------------------------------------------------------------------------------
/第五章/5_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第五章/5_5.png
--------------------------------------------------------------------------------
/第五章/5_6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第五章/5_6.png
--------------------------------------------------------------------------------
/第五章/5_7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第五章/5_7.png
--------------------------------------------------------------------------------
/第五章/5_8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第五章/5_8.png
--------------------------------------------------------------------------------
/第五章/5_9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第五章/5_9.png
--------------------------------------------------------------------------------
/第六章/6_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第六章/6_1.png
--------------------------------------------------------------------------------
/第六章/6_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第六章/6_2.png
--------------------------------------------------------------------------------
/第四章/4_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第四章/4_1.png
--------------------------------------------------------------------------------
/第四章/4_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第四章/4_2.png
--------------------------------------------------------------------------------
/第四章/4_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第四章/4_4.png
--------------------------------------------------------------------------------
/第四章/4_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第四章/4_5.png
--------------------------------------------------------------------------------
/第四章/4_6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第四章/4_6.png
--------------------------------------------------------------------------------
/第四章/4_7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第四章/4_7.png
--------------------------------------------------------------------------------
/第四章/4_8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第四章/4_8.png
--------------------------------------------------------------------------------
/第四章/4_9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第四章/4_9.png
--------------------------------------------------------------------------------
/第七章/图7_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第七章/图7_1.png
--------------------------------------------------------------------------------
/第三章/3_10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第三章/3_10.png
--------------------------------------------------------------------------------
/第三章/3_11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第三章/3_11.png
--------------------------------------------------------------------------------
/第九章/图9_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第九章/图9_1.jpg
--------------------------------------------------------------------------------
/第九章/图9_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第九章/图9_2.png
--------------------------------------------------------------------------------
/第九章/图9_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第九章/图9_3.png
--------------------------------------------------------------------------------
/第九章/图9_4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第九章/图9_4.jpg
--------------------------------------------------------------------------------
/第九章/图9_5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第九章/图9_5.jpg
--------------------------------------------------------------------------------
/第九章/图9_6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第九章/图9_6.jpg
--------------------------------------------------------------------------------
/第九章/图9_7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第九章/图9_7.jpg
--------------------------------------------------------------------------------
/第二章/2_10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第二章/2_10.png
--------------------------------------------------------------------------------
/第二章/2_11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第二章/2_11.png
--------------------------------------------------------------------------------
/第二章/2_12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第二章/2_12.png
--------------------------------------------------------------------------------
/第二章/2_13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第二章/2_13.png
--------------------------------------------------------------------------------
/第二章/2_14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第二章/2_14.png
--------------------------------------------------------------------------------
/第二章/2_15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第二章/2_15.png
--------------------------------------------------------------------------------
/第二章/2_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第二章/2_16.png
--------------------------------------------------------------------------------
/第二章/2_17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第二章/2_17.png
--------------------------------------------------------------------------------
/第八章/图8_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第八章/图8_1.jpg
--------------------------------------------------------------------------------
/第八章/图8_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第八章/图8_1.png
--------------------------------------------------------------------------------
/第八章/图8_10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第八章/图8_10.png
--------------------------------------------------------------------------------
/第八章/图8_11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第八章/图8_11.jpg
--------------------------------------------------------------------------------
/第八章/图8_12.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第八章/图8_12.jpg
--------------------------------------------------------------------------------
/第八章/图8_13.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第八章/图8_13.PNG
--------------------------------------------------------------------------------
/第八章/图8_2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第八章/图8_2.jpg
--------------------------------------------------------------------------------
/第八章/图8_3.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第八章/图8_3.PNG
--------------------------------------------------------------------------------
/第八章/图8_4.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第八章/图8_4.PNG
--------------------------------------------------------------------------------
/第八章/图8_5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第八章/图8_5.jpg
--------------------------------------------------------------------------------
/第八章/图8_6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第八章/图8_6.png
--------------------------------------------------------------------------------
/第八章/图8_7.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第八章/图8_7.PNG
--------------------------------------------------------------------------------
/第八章/图8_9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第八章/图8_9.jpg
--------------------------------------------------------------------------------
/第四章/4_11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第四章/4_11.png
--------------------------------------------------------------------------------
/第四章/4_12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第四章/4_12.png
--------------------------------------------------------------------------------
/第四章/4_13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第四章/4_13.png
--------------------------------------------------------------------------------
/第四章/4_14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第四章/4_14.png
--------------------------------------------------------------------------------
/第四章/4_15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xylinuxer/LKPA/HEAD/第四章/4_15.png
--------------------------------------------------------------------------------
/第三章/introduction.md:
--------------------------------------------------------------------------------
1 | # **第三章 进程**
2 |
3 | 面对计算机系统中少数几个CPU,多个程序在执行时都想占有它并独自在上面运行,但CPU本身并没有分身术,因此互不相让的程序之间可能会厮打起来。作为管理者的操作系统,决不能袖手旁观。于是,操作系统的设计者发明了进程这一概念。
--------------------------------------------------------------------------------
/第七章/section7_5.md:
--------------------------------------------------------------------------------
1 | ## 7.5 小结
2 |
3 | 本章首先介绍了临界区,共享队列,死锁等相关的同步概念,然后给出了内核中常用的三种同步方法,原子操作、自旋锁以及信号量,其中对信号量的实现机制进行了稍微深入的分析。为了加强读者对同步机制的应用能力,本章给出了两大实例,其一是生产者-消费者模型,其二是内核中线程、系统调用以及定时器任务队列的并发执行,通过这个两个例子,让读者深刻体会并发程序编写中如何应用同步机制。
--------------------------------------------------------------------------------
/第八章/section8_6.md:
--------------------------------------------------------------------------------
1 | ## 8.6 小结
2 |
3 | 本章首先介绍了文件系统的基础知识,其中涉及索引节点,软连接,硬链接,文件系统,文件类型以及文件的访问权限等概念。虚拟文件系统机制使得Linux可以支持各种不同的文件系统,其实现中涉及的主要对象有超级块、索引节点、目录项以及文件,对这些数据结构的描述可以使读者深入到细节了解具体字段的含义。然后,简要讨论了文件系统的注册、安装以及卸载,最后给出romfs文件系统的具体实现。
--------------------------------------------------------------------------------
/第九章/introduction.md:
--------------------------------------------------------------------------------
1 | # 第九章设备驱动
2 |
3 | 我们知道,计算机中三个最基本的物质基础是CPU、内存和输入输出(I/O)设备。与I/O设备相比,文件系统是一种逻辑意义上的存在,它只不过使对设备的操作更为方便、有效、更有组织、更接近人类的思维方式。可以说,文件操作是对设备操作的组织和抽象,而设备操作则是对文件操作的最终实现。那么,如何才能使没有感觉的硬件,变得有“灵性”,从而控制设备像操作普通文件一样方便有效,这就是本章要讨论的设备驱动问题。
--------------------------------------------------------------------------------
/第二章/section2_7.md:
--------------------------------------------------------------------------------
1 | ## **2.7 小结**
2 |
3 | 本章从寻址方式的演变入手,给出与操作系统设计密切相关的概念,比如,实模式,保护模式,各种寄存器,物理地址,虚拟地址以及线性地址等。然后对保护模式的分段机制和分页机制给予简要描述,并从Linux设计的角度分析了这些机制的具体落实。接着介绍了Linux中的汇编以及嵌入式汇编,最后给出了Linux系统的地址映射示例,这是在第二章就引入内存寻址的根本目的,就是操作系统如何借助硬件把虚地址转化为物理地址。
4 |
5 |
--------------------------------------------------------------------------------
/第四章/introduction.md:
--------------------------------------------------------------------------------
1 | #第四章 内存管理
2 |
3 | 存储器是一种必须仔细管理的重要资源。在理想的情况下,每个程序员都喜欢无穷大、快速并且内容不易变(即掉电后内容不会丢失)的存储器,同时又希望它是廉价的。但不幸的是,当前技术没有能够提供这样的存储器,因此大部分的计算机都有一个存储器层次结构,即少量的非常快速、昂贵、易变的高速缓存(cache);若干兆字节的中等速度、中等价格、易变的主存储器(RAM);数百兆或数千兆的低速、廉价、不易变的磁盘。这些资源的合理使用与否直接关系着系统的效率。
4 |
5 |
--------------------------------------------------------------------------------
/第三章/section3_8.md:
--------------------------------------------------------------------------------
1 | ## **3.8 小结**
2 |
3 | 本章从进程的引入开始,阐述了进程的各个方面,包括进程上下文、进程层次结构、进程状态,尤其是对进程控制块进行了比较全面的介绍。task_struct结构作为描述Linux进程的核心数据结构,对其熟悉和掌握是深入了解进程的入口点。另外,进程控制块的各种组织方式链表、散列表、队列等数据结构是管理和调度进程的基础。在这些基础上,对核心内容进程调度进行了代码级的描述,并给出了Linux新版本中改进的方法和思路。最后,以进程系统调用的剖析和应用来结束本章。
4 |
5 |
6 |
--------------------------------------------------------------------------------
/第六章/introduction.md:
--------------------------------------------------------------------------------
1 | # 第六章 系统调用
2 |
3 | 操作系统为用户态运行的进程与硬件设备(如CPU、磁盘、打印机等等)进行交互提供了一组接口。在应用程序和硬件之间设置这样一个接口层具有很多优点,首先,这使得编程更加容易,把用户从学习硬件设备的低级编程特性中解放出来。其次,极大地提高了系统的安全性,内核在要满足某个请求之前就可以在接口级检查这种请求的正确性。最后,更重要的是,这些接口使得程序更具有可移植性,因为只要不同操作系统所提供的一组接口相同,那么在这些操作系统之上就可以正确地编译和执行相同的程序。这组接口就是所谓的“系统调用”。
--------------------------------------------------------------------------------
/第九章/section9_6.md:
--------------------------------------------------------------------------------
1 | ## 9.6 小结
2 |
3 | Linux将设备驱动程序纳入文件系统的统一管理之下,也就是让用户以访问文件的方式访问设备,因此,在本章的概述一节中,首先阐述了设备驱动程序在文件系统中所处的位置。接着介绍了驱动程序的通用框架,以及Linux字符驱动的简单实例,让读者对驱动程序有一个初步认识。然后对设备驱动开发中会涉及到的I/O空间进行了比较详细的介绍。在字符设备驱动一节,把内存空间的一片区域看做一个字符设备,并给出了开发这样一个驱动程序的具体步骤和过程,但与实际驱动程序还有一定的而距离。最后,对块设备驱动程序的开发给出了简要描述。
4 |
--------------------------------------------------------------------------------
/第一章/introduction.md:
--------------------------------------------------------------------------------
1 | ## 第一章 概述
2 | 不管是计算机的心脏—CPU,还是记忆的载体—内存,甚至于种类繁多的外围设备—磁盘、打印机、键盘、网卡等输入输出设备,
3 | 它们之所以能有条不紊地协同工作,是因为有一层软件不遗余力地管理着它们。这层软件就是操作系统。
4 |
5 | 操作系统作是一种庞大而复杂的系统软件,为了对这样一个庞然大物有全方位的认识,让我们站在这座大厦的不同侧面给予初步观察。
6 |
7 |
8 |
9 |

10 |
11 |
12 |
--------------------------------------------------------------------------------
/第六章/section6_7.md:
--------------------------------------------------------------------------------
1 | ## 6.7 本章小结
2 |
3 | 系统调用是内核与用户程序进行交互的接口。本章从不同角度对系统调用进行了描述,说明了系统调用与API、系统命令以及内核函数之间的关系。然后,我们分析了Linux内核如何实现系统调用,说明系统调用处理程序以及服务例程在整个系统调用执行过程中的作用。
4 |
5 | 最后,通过两个实例讨论了如何增加系统调用,并给出了从用户空间调用系统调用的简单例子。增加一个系统调用并不难,它有一套比较规范的方法,难点是在实际应用中如何增加合适的系统调用,本章最后一个例子日志收集系统给出完整的过程,以便读者充分认识系统调用的价值并在自己的项目开发中灵活应用。
--------------------------------------------------------------------------------
/第一章/section1_7.md:
--------------------------------------------------------------------------------
1 | ##1.7小结
2 |
3 | 本章首先从不同侧面概要描述了大家熟悉而又陌生的操作系统,使读者从宏观上对操作系统有一个初步认识。之后,简要介绍了Linux的同族同源Unix,从而说明Linux赖以生存的土壤源于30多年Unix的发展。尽管Linux诞生于学生之手,但成长于Internet这片肥沃的土壤,壮大于自由而开放的文化。因为其土壤和文化背景的厚实,决定了其发展的持续性和广阔的前景。
4 |
5 | 为了让读者对Linux有初步了解后动手实践,本章还介绍了Linux内核中的模块编写方法,并以链表为入口点,让读者近距离感知Linux内核代码设计中的精彩和美妙。
6 |
7 |
--------------------------------------------------------------------------------
/第一章/section1_8.md:
--------------------------------------------------------------------------------
1 | **小结**
2 |
3 | 本章首先从不同侧面概要描述了大家熟悉而又陌生的操作系统,使读者从宏观上对操作系统有一个初步认识。之后,简要介绍了Linux的同族同源Unix,从而说明Linux赖以生存的土壤源于30多年Unix的发展。尽管Linux诞生于学生之手,但成长于Internet这片肥沃的土壤,壮大于自由而开放的文化。因为其土壤和文化背景的厚实,决定了其发展的持续性和广阔的前景。
4 |
5 | 为了让读者对Linux有初步了解后动手实践,本章还介绍了Linux内核中的模块编写方法,并以链表为入口点,让读者近距离感知Linux内核代码设计中的精彩和美妙。
6 |
7 |
--------------------------------------------------------------------------------
/第五章/introduction.md:
--------------------------------------------------------------------------------
1 | # 第五章 中断和异常
2 |
3 | 中断控制是计算机发展中一种重要的技术。最初它是为克服对I/O接口控制采用程序查询所带来的处理器低效率而产生的。中断控制的主要优点是只有在I/O需要服务时才能得到处理器的响应,而不需要处理器不断地进行查询。由此,最初的中断全部是对外部设备而言的,即称为外部中断(或硬件中断)。
4 |
5 | 但随着计算机系统结构的不断改进以及应用技术的日益提高,中断的适用范围也随之扩大,出现了所谓的内部中断(或叫异常),它是为解决机器运行时所出现的某些随机事件及编程方便而出现的。因而形成了一个完整的中断系统。本章主要讨论在80x86保护模式下中断机制在Linux中的实现
--------------------------------------------------------------------------------
/第四章/section4_7.md:
--------------------------------------------------------------------------------
1 | ## 4.7 本章小结
2 |
3 | 一个程序编译链接后形成的地址空间本身就是一个虚拟地址空间,对于内核代码而言,它存放在4G虚拟空间的3G以上,对于用户程序而言,存放在3G一下的虚拟地址空间。但是不管是内核还是用户程序,最终运行时还是要被搬到物理内存中。本章的核心就是围绕虚地址到物理地址的转换,由此引发出了各种问题,比如地址映射问题,一方面把可执行映像映射到虚拟地址空间,另一方面把虚地址空间映射到物理地址空间。而在程序执行时,涉及到请页问题,把虚空间中的页真正搬到物理空间,由此要对物理空间进行分配和回收,而在物理内存不够时,又必须进行内外交换,交换的效率直接影响系统的性能,于是缓冲和刷新技术粉墨登场。
4 |
5 | 本章最后一节给出了一个比较完整的例子,说明内存管理在实际中的应用。
--------------------------------------------------------------------------------
/第二章/introduction.md:
--------------------------------------------------------------------------------
1 | # **第二章 内存寻址**
2 |
3 | 我们知道,操作系统是一组软件的集合。但它和一般软件不同,因为它是充分挖掘硬件潜能的软件,也可以说,操作系统是横跨软件和硬件的桥梁。因此,要想深入解析操作系统内在的运作机制,就必须搞清楚相关的硬件机制——尤其是内存寻址的硬件机制。
4 |
5 | 操作系统的设计者必须在硬件相关的代码与硬件无关的代码之间划出清楚的界限,以便于一个操作系统很容易地移植到不同的平台。Linux的设计就做到了这点,它把与硬件相关的代码全部放在arch(architecture一词的缩写,即体系结构相关)的目录下,在这个目录下,可以找到Linux目前版本支持的所有平台,例如,支持的平台有arm、alpha,、i386、m68k、mips等十多种。在这众多的平台中,大家最熟悉的就是i386,即Intel80x86体系结构。因此,我们所介绍的内存寻址也是以此为背景。
--------------------------------------------------------------------------------
/第五章/section5_6.md:
--------------------------------------------------------------------------------
1 | ## 5.6 本章小结
2 |
3 | 本章涵盖了较多的概念:中断和异常,中断向量,IRQ,中断描述符表,中断请求队列,中断的上半部和下半部,时钟中断,时钟节拍,节拍率,定时器等,尽量区分这些概念。
4 |
5 | 中断使得硬件与处理器进行通信,不同的设备对应的中断不同,每个都有一个惟一的数字标识,这就是IRQ;同时,不同的中断具有不同的中断服务程序,其中断处理程序的入口地址存放在中断向量表中。当某个中断发生时,对应的中断服务程序得到执行,在执行期间不接受外界的干扰。为了缓解中断服务程序的压力,内核中引入了中断下半部机制,不管是tasklet机制,工作队列机制,其本质都是推后下半部函数的执行。
6 |
7 | 时钟中断是内核跳动的脉搏,本章引入了时钟节拍,jiffies,节拍率等概念,简要介绍了时钟中断的运行机制,同时给出了定时器的简单应用。
8 |
9 |
--------------------------------------------------------------------------------
/第七章/introduction.md:
--------------------------------------------------------------------------------
1 | # 第七章 内核中的同步
2 |
3 | 如果我们把内核看作不断对各种请求进行响应的服务器,那么,正在CPU上执行的进程、发出中断请求的外部设备等就相当于客户端。正如服务器要随时响应客户的请求一样,内核也会随时响应进程、中断等的请求。我们之所以这样比喻是为了强调内核中的各个任务 [1] 并不是严格按着顺序依次执行的,而是相互交错执行的。
4 |
5 | 对所有内核任务而言,内核中的很多数据 [2] 都是共享资源,这就像高速公路供很多车辆行驶一样。对这些共享资源的访问必须遵循一定的访问规则,否则就可能造成对共享资源的破坏,就如同不遵守交通规则会造成撞车一样。
6 |
7 | [1] 这里所定义的内核任务是指内核态下可以独立执行的内核例程(一个或多个内核函数),每个内核任务运行时都拥有一个独立的程序计数器、栈和一组寄存器。一般来说,内核任务包括内核线程、系统调用、中断服务程序、异常处理程序、下半部等几类。
8 |
9 | [2] 这里的“数据”是广义的概念,包括变量,队列,堆栈等数据结构。
--------------------------------------------------------------------------------
/第九章/exercise9.md:
--------------------------------------------------------------------------------
1 | 习题
2 |
3 | 1. 为什么把设备分为“块设备”和“字符设备”两大类?
4 |
5 | 2. 为什么说设备驱动式文件系统与硬件设备之间的桥梁?
6 |
7 | 3. 什么是设备驱动程序?
8 |
9 | 4. 给出用户进程请求设备服务的流程。
10 |
11 | 5. I/O端口一般包括哪些寄存器?各自功能是什么?CPU用什么命令对其进行读写?
12 |
13 | 6. 基于中断的驱动程序是如何工作的?给出一个包含read()函数和interrupt()函数的驱动程序实例,编写模块上机调试,并对其工作过程给予描述。
14 |
15 | 7. 驱动程序一般包含几部分?并对各部分给予简要说明。
16 |
17 | 8. 如何注册字符驱动程序?画出实现register\_chrdev()函数的流程图。
18 |
19 | 9. 编写一个字符设备驱动程序(包括open(),read(),write(),ioctl(),close()等函数),并进行调试和测试。
20 |
21 | 10. 根据图9.5,对注册块驱动程序的整个过程给出详细描述。
22 |
--------------------------------------------------------------------------------
/第六章/exercise6.md:
--------------------------------------------------------------------------------
1 | 第六章 习题
2 |
3 | 1. 什么是系统调用,为什么要引入系统调用?
4 |
5 | 2. 系统调用与库函数、系统命令及内核函数有什么区别和联系?
6 |
7 | 3. 内核为什么要设置系统调用处理程序,它与服务例程有什么区别?
8 |
9 | 4. system_call()函数为什么要把当前进程的PCB地址保存在ebx寄存器中?
10 |
11 | 5. 画出system_call()的流程图。
12 |
13 | 6. 说明系统调用号的作用?
14 |
15 | 7. 画出系统调用进入内核时内核态堆栈的内容。
16 |
17 | 8. 用KDB跟踪一个系统调用(如read())的执行过程,然后给出执行路径。
18 |
19 | 9. 如何封装系统调用?
20 |
21 | 10. 参照write()系统调用,写出read()系统调用的汇编代码。
22 |
23 | 11. 给出添加一个系统调用的步骤。
24 |
25 | 12. 编写一个新的系统调用,并进行调试。
26 |
27 | 13. 调试日志收集系统,给出调试过程和体会。
28 |
--------------------------------------------------------------------------------
/第八章/introduction.md:
--------------------------------------------------------------------------------
1 | # 第八章 文件系统
2 |
3 | 在使用计算机的过程中,文件是经常被提到的概念,例如可执行文件、文本文件等,这里说的文件是一个抽象的概念,它是存放一切数据化信息的仓库。用户为了保存数据或信息,首先要创建一个文件,然后把数据或信息写入该文件。最终这些数据被保存到文件的载体上,通常情况下是磁盘,对于用户来说,只要给出存放文件的路径和文件名,文件系统就可以在磁盘上找到该文件的物理位置,并把它调入内存供用户使用。在这个过程中文件系统起着举足轻重的作用,通过文件系统我们才能根据路径和文件名访问到文件,以及对文件进行各种操作。
4 |
5 | 从系统角度来看,文件系统是对文件存储器空间进行组织和分配,负责文件的存储并对存入的文件进行保护和检索的系统。具体地说,它负责为用户建立文件,存入、读出、修改、转储文件,控制文件的存取,当用户不再使用时撤销文件等。
6 |
7 | 虚拟文件系统(简称VFS)为用户程序提供了具体文件系统的接口,它对每个具体文件系统的细节进行抽象,使所有文件系统依赖于VFS且可以通过VFS协同工作。要注意VFS与具体文件系统的区别,VFS只存在于内存中,在系统启动时创建,在系统关闭时消亡。
8 |
--------------------------------------------------------------------------------
/第一章/exercise1.md:
--------------------------------------------------------------------------------
1 | **习题**
2 |
3 | 1. 通过阅读本章,谈谈自己对操作系统的认识。
4 | 2. 在操作系统的演变过程中,你认为起推动作用的是什么?
5 | 3. 从硬件的角度谈操作系统的发展,从中得到什么启发?
6 | 4. 从软件设计的角度谈操作系统的发展,从中得到什么启发?
7 | 5. 从Unix/Linux的诞生中,你受到什么启发?
8 | 6. 什么是Posix标准,为什么现代操作系统的设计必须遵循Posix标准?
9 | 7. 什么是GNU?Linux与GNU有什么关系?
10 | 8. 你认为Linux开发模式有何优缺点?
11 | 9. Linux系统由哪些部分组成?Linux内核处于什么位置?
12 | 10. Linux内核由哪几个子系统组成?各个子系统的主要功能是什么?
13 | 11. 访问http://www.kernel.org/,理解Linux的内核的版本树,了解最新内核的特点
14 | 12. 了解Linux内核源代码结构,访问源代码导航网站[http://lxr.linux.no/](http://lxr.linux.no/),说明/kernel目录下包含哪些文件。
15 | 13. 分析[include/linux/list.h](include/linux/list.h)中哈希表的实现,给出分析报告,并编写内核模块,调用其中的函数和宏,实现哈希表的建立和查找。
16 |
17 |
18 |
--------------------------------------------------------------------------------
/第五章/exercise5.md:
--------------------------------------------------------------------------------
1 | 第五章 习题
2 |
3 | 习题
4 |
5 | 1. 什么是中断?什么是异常?二者有何不同?
6 |
7 | 2. 什么是中断向量?Linux是如何分配中断向量的?
8 |
9 | 3. 什么是中断描述符表?什么是门描述符?请描述其格式。
10 |
11 | 4. 门描述符有哪些类型?它们有什么不同?
12 |
13 | 5. Call指令和INT指令有何区别?
14 |
15 | 6. 如何对中断描述符表进行初始化?
16 |
17 | 7. 在中断描述符表中如何插入一个中断门、陷阱门和系统门?
18 |
19 | 8. 内核如何处理异常?
20 |
21 | 9. 画出对中断和异常进行硬件处理的流程图,并说明CPU为什么要进行有效性检查?如何检查?CPU是如何跳到中断或异常处理程序的?
22 |
23 | 10. 中断处理程序和中断服务程序有何区别?Linux如何描述一条共享的中断线?
24 |
25 | 11. 为什么要把中断所执行的操作进行分类?分为哪几类?
26 |
27 | 12. 叙述中断处理程序的执行过程,并给出几个主要函数的调用关系和功能。
28 |
29 | 13. 为什么把中断分为两部分来处理?
30 |
31 | 14. 如何申明和使用一个小任务?
32 |
33 | 15. 实时时钟和操作系统时钟有何不同?
34 |
35 | 16. jiffies表示什么?什么时候对其增加?
36 |
37 | 17. 时钟中断服务程序的主要操作是什么?其主要函数的功能是什么?
38 |
39 | 18. 时钟中断的下半部分主要做什么?
40 |
41 | 19. 阅读时钟中断的源代码。
42 |
43 | 20. 举例说明如何使用定时器。
44 |
--------------------------------------------------------------------------------
/第二章/exercise2.md:
--------------------------------------------------------------------------------
1 | **习题**
2 |
3 | 1. Intel微处理器从4位、8位、16位到32位的演变过程中,什么起了决定作用?其演变过程继承了什么?同时又突破了什么?
4 | 2. 在80X86的寄存器中,哪些寄存器供一般用户使用?哪些寄存器只能操作系统使用?
5 | 3. 什么是物理地址?什么是虚地址?什么又是线性地址?举例说明。
6 | 4. 在保护模式下,MMU如何把一个虚地址转换为物理地址?
7 | 5. 你是如何认识段的?请用C语言描述段描述符表。
8 | 6. 为什么把80X86下的段寄存器叫段选择符?
9 | 7. 保护模式主要保护什么?通过什么进行保护?
10 | 8. Linux是如何利用段机制又巧妙地绕过段机制?在内核代码中如何表示各种段,查找最新源代码进行阅读和分析。
11 | 9. 页的大小是由硬件设计者决定还是操作系统设计者决定?过大或过小会带来什么问题?
12 | 10. 为什么对32位线性地址空间要采用两级页表?
13 | 11. 编写程序,模拟页表的初始化。
14 | 12. 为什么在设计两级页表的线性地址结构时,给页目录和页表各分配10位?如果不是这样,举例说明会产生什么样的结果?
15 | 13. 页表项属性中各个位的定义是由硬件设计者决定还是操作系统设计者决定?如果通过页表项的属性对页表及页中的数据进行保护?
16 | 14. 深入理解图2.12,并结合图叙述线性地址到物理地址的转换?
17 | 15. 假定一个进程分配的线性地址范围为0x00e80000~0xc0000000,又假定这个进程要读取线性地址0x00faf000中的内容,试按分页原理描述其处理过程。
18 | 16. 页面高速缓存起什么作用?如何置换其中的内容,以使其命中率尽可能高?
19 | 17. Linux为什么主要采用分页机制来实现虚拟存储管理?它为什么采用三级分页而不是两级?
20 | 18. Intel的汇编语言与AT&T的汇编语言有何主要区别?
21 | 19. 分析源代码文件system.h中read_cr3()函数和string_32.h中的memcpy()函数。
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LKPA(Linux Kernel Principle and Application)
2 | Linux操作系统原理与应用第二版改编
3 |
4 |
5 | ## 目录结构
6 |
7 | ## Gitbook预览
8 | [Linux操作系统原理与应用(第二版)](https://xylinuxer.gitbooks.io/lkpa/content/)
9 |
10 | ## 约定
11 | * 全文使用Markdown编写
12 | * 文中所有代码都是基于Centos7.3.1611,内核版本3.10.0-514.2.1.el7
13 | * 全文使用缩写LKPA来表示本书
14 | * 全书使用[Gitbook](https://www.gitbook.com/editor)编辑器来编写
15 | * 全书所有的图片统一使用visio2013和MindManage编辑
16 | * 全书所有的代码符合C99标准,使用[Linux kernel coding style](LinuxKernelCodingStyle.md)
17 | * 首行缩进使用` `
18 |
19 | ---
20 |
21 | **内容简介**
22 |
23 |
24 | 本书是一本Linux内核以及动手实践的入门教程。在庞大的Linux内核中,选取最基本的内容-进程管理、中断、内存管理、系统调用、内核同步、文件系统、I/O设备管理等进行阐述。书中各章节从原理出发,基于Linux内核源代码但又不局限于代码,分析原理如何落实到代码,并通过一些简单有效的实例说明如何调用Linux内核提供的函数进行内核级程序的开发,主要章节给出了相对具有实用价值的小型应用,从而让读者在实践中加深对原理的理解和应用。
25 |
26 | 本书对于希望深入Linux操作系统内部、阅读Linux内核源代码以及进行内核级程序开发的读者具有较高的参考价值。本书可作为高等院校计算机相关专业的本科生、研究生的教材,同时,Linux应用开发人员、嵌入式系统开发人员等读者均可从本书的阅读中获益。
27 |
28 |
--------------------------------------------------------------------------------
/第八章/exercise8.md:
--------------------------------------------------------------------------------
1 | 习题
2 |
3 | 1. Linux目录树结构是怎样的?它与Windows的目录树结构有什么区别?为什么Linux的文件系统采用固定的目录形式?
4 |
5 | 2. 什么是软链接和硬链接?二者有何区别?
6 |
7 | 3. 什么是虚拟文件系统?什么是虚拟文件系统界面?
8 |
9 | 4. 以wirte()系统调用为例,说明VFS是如何与具体文件系统(如DOS的FAT)相结合的?
10 |
11 | 5. VFS中有哪些主要对象?其各自存放什么信息?它们的共同特征是什么?
12 |
13 | 6. 一个文件系统对应一个VFS还是整个系统只有一个VFS?
14 |
15 | 7. Super\_block结构中设置u域的意图是什么?
16 |
17 | 8. 什么是索引节点,VFS的索引节点与具体文件系统的索引节点有什么联系?
18 |
19 | 9. 内核如何组织索引节点?为什么要设置多个链表管理索引节点?
20 |
21 | 10. 目录项结构与索引节点有何区别?为什么不把二者合二为一?
22 |
23 | 11. 内核如何组织目录项结构?画图说明。
24 |
25 | 12. 文件对象中含有一个偏移,它给出了文件中的当前位置。如果有两个进程都独立于另一个进程读文件。对于写操作将怎样?如果偏移被放在索引节点中而不是文件对象中将会出现什么情况?
26 |
27 | 13. 假定进程以读方式打开一个文件后,再执行fork,父进程和子进程都将可以读这个文件。这两个进程的读操作和写操作有何关系?
28 |
29 | 14. 结合用户打开表说明什么是文件描述符?
30 |
31 | 15. 结合图8.6,说明图中各个数据结构之间的关系。
32 |
33 | 16. 文件系统的注册和安装有何不同,何时进行?
34 |
35 | 17. 画出mount()系统调用实现的流程图。
36 |
37 | 18. 举例说明借助于页缓冲区如何读取一个页面。
38 |
39 | 19. 从用户发出读请求到最终从磁盘读取数据包括那些步骤?给出读操作的逻辑流。
40 |
41 | 20. 阅读Romfs文件系统的源代码,并画出流程图。
--------------------------------------------------------------------------------
/第三章/exercise3.md:
--------------------------------------------------------------------------------
1 | **习题:**
2 |
3 | 1. 通过一个程序的执行过程说明程序和进程两个概念的区别
4 |
5 | 2. 为什么要引入进程?
6 |
7 | 3. 什么是进程控制块?它包含哪些基本信息。打开源代码,查看sched.h文件中对task_struct的定义,确认一下你已经认识哪些域。
8 |
9 | 4. Linux内核的状态有哪些?请画出状态转换图,查看最新源代码,以确认有哪些状态。
10 |
11 | 5. 自己定义一个进程控制块,其中只包含状态信息、标识符及进程的亲属关系信息,写两个函数,一个函数向进程树中插入一个进程,另一个函数从进程树中删除一个进程。
12 |
13 | 6. Linux的进程控制块如何存放?为什么?假设esp中存放的是栈顶指针,请用三句汇编语句描述如何获得current的PCB的地址。
14 |
15 | 7. PCB的组织方式有哪几种?为什么要采取这些组织方式?
16 |
17 | 8. 请编写内核模块,打印系统中各进程的名字以及pid, 同时统计系统中进程的个数。
18 |
19 | 9. 一个好的调度算法要考虑哪些方面?为什么?
20 |
21 | 10. 查看,2.4内核中shed.c文件中的schedule( )的实现代码 ,画出实现schedule( )的流程图。
22 |
23 | 11. 什么是写时复制技术,这种技术在什么情况下最能发挥其优势?
24 |
25 | 12. 查看fork.c中fork的实现代码,画出实现fork()的流程图。
26 |
27 | 13. 0号进程在什么时候被创建?在什么情况下才被调度执行?
28 |
29 | 14. init内核线程与init进程是一回事吗?它们有什么本质的区别?
30 |
31 | 15. 用fork写一个简单的测试程序,从父进程和子进程中打印信息。信息应该包括父子进程的PID。执行程序若干次,看两个信息是否以同样的次序打印。
32 |
33 | 16. 把wait()和exit()系统调用加到前一个练习中,使子进程返回退出状态给父进程,并将它包含在父进程的打印信息中。执行若干次,观察结果。
34 |
35 | 17. 根据3.7节给出的例子,自己写出一个完整的程序,其中调用了进程相关的系统调用。
36 |
37 |
--------------------------------------------------------------------------------
/第七章/exercise7.md:
--------------------------------------------------------------------------------
1 | 习题
2 |
3 | 1. 什么是临界区?什么是竞争状态?什么是同步?
4 |
5 | 2. 为什么要对共享队列加锁?
6 |
7 | 3. 如何确定要保护的对象?
8 |
9 | 4. 如何避免死锁?
10 |
11 | 5. 内核中造成并发执行的原因是什么?
12 |
13 | 6. 申明一个原子变量v,给其赋初值1,然后对其减1,并测试其结果。
14 |
15 | 7. 申明一个锁定下半部的自旋锁,并给出如何使用它。
16 |
17 | 8. 给出信号量的定义,并说明down()和up()的含义。
18 |
19 | 9. 申明一个信号量,并给出如何使用它。
20 |
21 | 10. 自旋锁和信号量各用在什么情况下。
22 |
23 | 11. 分析给出的并发控制实例,上机调试,并对运行结果进行分析。
24 |
25 | 12. 关于生产者和消费者问题,回答一下问题:
26 |
27 | (1) 在模块的插入函数procon\_init中,为何要对信号量进行如下初始化:
28 | ```c
29 | init_MUTEX(&sem_producer);
30 |
31 | init_MUTEX_LOCKED(&sem_consumer);
32 | ```
33 | 可否将其改为:
34 | ```c
35 | init_MUTEX_LOCKED(&sem_producer);
36 |
37 | init_MUTEX(&sem_consumer);
38 | ```
39 | 为什么?
40 |
41 | (2)
42 | 在上述实例中,由于资金不足,牛奶生产厂家只有一条生产线。随着企业的发展壮大,客户需求日益增大,厂家决定投资多个生产线。上面的程序能否适应“多个生产者,一个消费者,一个公共缓冲区”这种情况?读者可去掉procon\_init函数中注释的语句,重新编译并运行程序,看看程序执行结果是否正确。
43 |
44 | (3)
45 | 由于牛奶产品特别好,所以有多个代理商加入代理销售,上面的程序能否适应“多个生产者,多个消费者,一个公共缓冲区”这种情况?添加多个消费者线程,重新编译运行程序,查看结果是否正确。注意看消费者线程是否可以全部正确退出。
46 |
47 | (4)
48 | 随着企业的扩大,产品需求的增大,厂家决定多建几个仓库。在仓库有空间的情况下,生产线可以继续生产产品放入仓库,直到仓库放满。在仓库有产品的情况下,代理商可以订购产品,直到仓库中没有产品。修改程序,使之适应“一个生产者,一个消费者,多个缓冲区”,“多个生产者,多个消费者,多个缓冲区”这两种情况。
49 |
50 |
--------------------------------------------------------------------------------
/第四章/exercise4.md:
--------------------------------------------------------------------------------
1 | 第四章 习题
2 |
3 | 1. 为什么把进程的地址空间划分为“内核空间”和“用户空间”?
4 |
5 | 2. 为什么说每个进程都拥有3G私有的用户空间?
6 |
7 | 3. 内核空间存放什么内容?如何把其中的一个虚地址转换成物理地址?
8 |
9 | 4. 什么是内核映象?它存放在物理空间和内核空间的什么地方?
10 |
11 | 5. 用户空间划分为哪几部分?用户程序调用malloc()分配的内存属于哪一部分?
12 |
13 | 6. mm_struct结构描述了用户空间的哪些方面?内核对该结构如何组织?查看源代码
14 |
15 | 7. vm_area_struct结构描述了虚存区的哪些方面?内核对该结构如何组织?查看源代码
16 |
17 | 8. 为什么把进程的用户地址空间划分为一个个区间?
18 |
19 | 9. 结合图4.6,叙述task_struct、mm_struct、vm_area_struct及页目录等数据结构之间的关系,并说明哪些数据结构与物理内存和进程的地址空间相关,是如何相关的?
20 |
21 | 10. 进程用户空间何时创建?如何创建?
22 |
23 | 11. 什么是虚存映射?有哪几种类型?
24 |
25 | 12. 一个进程一般包含哪些虚存区?举例说明。
26 |
27 | 13. 说明mmap()系统调用的功能?利用mmap()写一个拷贝文件的程序。
28 |
29 | 14. 对图4.8进程详细描述
30 |
31 | 15. Linux是如何实现“请求调页”的?
32 |
33 | 16. 系统启动后,物理内存的布局如何?动态内存存放什么?
34 |
35 | 17. 试叙述伙伴算法的工作原理,并说明为什么伙伴算法可以消除外碎片?
36 |
37 | 18. 画出物理页面分配和回收的流程图,其中所采用的算法为伙伴算法。
38 |
39 | 19. Linux为什么要采用slab分配机制?
40 |
41 | 20. 举例说明slab专用缓冲区和通用缓冲区的使用。
42 |
43 | 21. 内核的非连续空间位于何处?给出创建非连续区的实现算法。
44 |
45 | 22. vmalloc()和kmalloc()有何区别?编写内核模块程序,调用这两个函数以观察二者所分配空间位于不同的区域。
46 |
47 | 23. 在面交换中,必须考虑哪几个方面的问题?哪些页不应当被换出?
48 |
49 | 24. 对系统空闲时换出页面的策略进行分析,说明“抖动”发生的概率有多大?这种策略在什么情况下实施起来有较好的效果?
50 |
51 | 25. 分析守护进程kswapd的运行时机,你认为怎样换出页面比较合理?
52 |
53 | 26. 分析并调试实例程序。
54 |
55 |
--------------------------------------------------------------------------------
/第六章/section6_2.md:
--------------------------------------------------------------------------------
1 | ## 6.2 系统调用基本概念
2 |
3 | 系统调用实质就是函数调用,只是调用的函数是系统函数,处于内核态而已。用户在调用系统调用时会向内核传递一个系统调用号,然后系统调用处理程序通过此号从系统调用表中找到相应的内核函数执行(系统调用服务例程),最后返回。在这个过程中涉及到系统调用号、系统调用表以及系统调用处理程序等概念,本小节将介绍这些基本概念。
4 |
5 | ### 6.2.1 系统调用号
6 |
7 | Linux系统有几百个系统调用,为了唯一的标识每一个系统调用,Linux为每一个系统调用定义了一个唯一的编号,此编号称为系统调用号。它定义在文件linux/arch/x86/include/asm/unistd_32.h中(注意,在不同的版本中,这个头文件的位置稍有不同):
8 | ```c
9 | #define __NR_restart_syscall 0
10 |
11 | #define __NR_exit 1
12 |
13 | #define __NR_fork 2
14 |
15 | #define __NR_read 3
16 |
17 | .……
18 |
19 | #define __NR_fallocate 324
20 | ```
21 | 由此可见当前系统拥有324个系统调用。系统调用号的另一个目的是作为系统调用表的下标,当用户空间的进程执行一个系统调用的时候,这个系统调用号就被用来指明到底是要执行那个系统调用。系统调用号相当关键,一旦分配好就不能再有任何改变,否则编译好的应用程序就会因为调用到错误的系统调用而导致程序崩溃。
22 |
23 | ### 6.2.2 系统调用表
24 |
25 | 为了把系统调用号与相应的服务例程关联起来,内核利用了一个系统调用表,这个表存放在sys_call_table数组中,它是一个函数指针数组,每一个函数指针都指向了其系统调用的封装例程,有NR_syscalls个表项,第n个表项包含系统调用号为n的服务例程的地址。NR_syscalls宏只是对可实现的系统调用最大个数的静态限制,并不表示实际已实现的系统调用个数。这样我们就可以利用系统调用号作为下标,找到其系统调用的封装例程。此表定义在文件linux/arch/x86/kernel/syscall_table_32.S
26 | ```c
27 | ENTRY(sys_call_table)
28 |
29 | .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
30 |
31 | .long sys_exit
32 |
33 | .long sys_fork
34 |
35 | .long sys_read
36 |
37 | .long sys_write
38 |
39 | .long sys_open /* 5 */
40 |
41 | ..……
42 | ```
43 | ### 6.2.3 系统调用服务例程和系统调用处理程序
44 |
45 | 每一个系统调用bar()在内核态都有一个对应的内核函数sys_bar(),这个内核函数就是系统调用bar()的实现,也就是说在用户态调用bar(),最终会由内核函数sys_bar()为用户服务,这就是系统调用服务例程。
46 |
47 | 系统调用既然最终会由相应的内核函数完成,那么为什么不直接调用内核函数呢?这是因为用户空间的程序无法直接执行内核代码,因为内核驻留在受保护的地址空间上,不允许用户进程在内核地址空间上读写。所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,这种通知内核的机制是靠软中断来实现的,通过引发一个异常来促使系统切换到内核态去执行异常处理程序,此时的异常处理程序就是所谓的系统调用处理程序,下一节会接着介绍此程序。
48 |
--------------------------------------------------------------------------------
/第六章/section6_4.md:
--------------------------------------------------------------------------------
1 | ## 6.4 封装例程
2 |
3 | 上一节讲述了图6.1中当一个系统调用陷入内核时的系统调用处理程序和服务例程。那么libc库中是如何对不同的服务例程进行封装?
4 | Linux的系统调用有200多个,相应的服务例程也这么多,显然,对其一一进行封装是麻烦而不现实的。于是,为了简化对相应的封装例程的声明,Linux定义了从_syscall0到_syscall5的六个宏。之所以定义六个宏,是因为系统调用的参数个数一般不超过六个。
5 |
6 | 每个宏名字的数字0到5对应着系统调用所用的参数个数(系统调用号除外)。显然,不能用这些宏来为超过5个参数的系统调用或产生非标准返回值的系统调用定义封装例程。
7 |
8 | 每个宏严格地需要2+2×n个参数,n是系统调用的参数个数。另外两个参数指明系统调用的返回值类型和名字;每一对参数指明相应的系统调用参数的类型和名字。因此,像fork()系统调用的封装例程可以通过如下语句产生:
9 | ```c
10 | _syscall0(int,fork)
11 | ```
12 | 而write()的封装例程可以通过如下语句生产:
13 | ```c
14 | _syscall3(int,write,int,fd,const char *,buf,unsigned int,count)
15 | ```
16 | 可以把_syscall3()这个宏展开成如下的汇编语言代码:
17 | ```c
18 | write:
19 |
20 | pushl %ebx ; 将ebx 压入栈
21 |
22 | movl 8(%esp), %ebx ;
23 | 将一个参数放入ebx(栈中前两个位置存放的是类型和名字,占8个字节)
24 |
25 | movl 12(%esp), %ecx ; 将第二个参数放入ecx
26 |
27 | movl 16(%esp), %edx ; 将第三个参数放入edx
28 |
29 | movl $4, %eax ; 把系统调用名对应的系统调用号放入 eax
30 |
31 | int $0x80 ; 进行系统调用
32 |
33 | cmpl $-126, %eax ; 检查返回码
34 |
35 | jbe .L1 ; 如无错跳转
36 |
37 | negl %eax ; 求eax的补码
38 |
39 | movl %eax, errno ; 将所求的结果放入 errno变量
40 |
41 | movl $-1, %eax ; 将eax 置为-1
42 |
43 | .L1: popl %ebx ; 从栈中弹出ebx
44 |
45 | ret ;返回到调用程序
46 | ```
47 | 注意write()函数的参数是如何在执行0x80指令前被装入到CPU寄存器中。如果eax中的返回值在-1和-125之间,必须被解释为错误码(内核假定在include/asm-i386/errno.h中定义的最大错误码为125)。如果是这种情况,封装例程在errno中存放-eax的值并返回值-1;否则,返回eax中的值。
48 |
49 | 通过这种封装,在用户态下调用系统调用就变得简单多了,用户既不需要关心系统调用号,也不需要提供复杂的参数,而且还在不知不觉中让内核为自己提供了服务。这里要说明的是,虽然系统调用一般用在用户程序中,但在内核中同样可以调用这种封装了的系统调用。只不过二者有一些区别而已:
50 |
51 | 1. 在用户态进行系统调用时,转换到内核态的系统调用处理程序时要进行用户态堆栈到内核态堆栈的切换,即从“int0x80”指令转换到内核态的“system_call”函数时,要保存寄存器ss、esp;而当“iret”指令从“system_call”返回用户态时要取回ss、esp的值。
52 |
53 | 2. 在内核中进行系统调用时,不用进行堆栈切换,即“int0x80”指令不用切换到内核态“system_call”函数,也不必保存寄存器ss、esp;而当“iret”指令从“system_call”返回,仍然是内核态,所以也不用取回ss、esp的值。
54 |
55 |
--------------------------------------------------------------------------------
/第三章/section3_7.md:
--------------------------------------------------------------------------------
1 | ## **3.7系统调用及应用**
2 |
3 | 以下是用户态下模拟执行命令的一个示例程序。父进程打印控制菜单,并且接受命令,然后创建子进程,让子进程去处理任务,而父进程继续打印菜单并接受命令。
4 |
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | int main(int argc, char *argv[])
13 | {
14 | pid_t pid;
15 | char cmd;
16 | char *arg_psa[] = {"ps", "-a", NULL};
17 | char *arg_psx[] = {"ps", "-x", NULL};
18 |
19 | while (1) {
20 | printf("------------------------------------------------\n");
21 | printf("输入 a 执行'ps -a'命令\n");
22 | printf("输入 x 执行'ps -x'命令\n");
23 | printf("输入 q 退出\n");
24 | cmd = getchar(); /*接收输入命令字符*/
25 | getchar();
26 |
27 | if ((pid = fork()) < 0){//创建子进程
28 | perror("fork error:");
29 | return -1;
30 | }//进程创建成功
31 | if(pid == 0) { /*子进程*/
32 | switch (cmd) {
33 | case 'a':
34 | execve("/bin/ps", arg_psa, NULL);
35 | break;
36 | case 'x':
37 | execve("/bin/ps", arg_psx, NULL);
38 | break;
39 | case 'q':
40 | break;
41 | default:
42 | perror("wrong cmd:\n");
43 | break;
44 | } /*子进程到此结束*/
45 | exit(0); /*此处有意设置子进程提前结束,因为它的任务已经完成
46 | } else if (pid > 0) { /*父进程*/
47 | if ( cmd == 'q' )
48 | break;
49 | }
50 | } /*进进程退出循环*/
51 | while(waitpid(-1,NULL,WNOHANG) > 0);/*父进程等待回收子进程*/
52 | return 0;
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/第二章/section2_6.md:
--------------------------------------------------------------------------------
1 | ## **2.6 Linux系统地址映射举例**
2 |
3 | Linux采用分页存储管理。虚拟地址空间划分成固定大小的“页”,由MMU在运行时将虚拟地址映射(变换)成某个物理页面中的地址。从80X86系列的历史演变过程可知,分段管理在分页管理之前出现,因此,80X86的MMU对程序中的虚拟地址先进行段式映射(虚拟地址转换为线性地址),然后才能进行页式映射(线性地址转换为物理地址)。既然硬件结构是这样设计的,Linux内核在设计时只好服从这种选择,只不过,Linux巧妙地使段式映射实际上不起什么作用。
4 |
5 | 本节通过一个程序的执行来说明地址的映射过程。
6 |
7 | 假定我们有一个简单的C程序Hello.c
8 |
9 | #include
10 | greeting ( )
11 | {
12 | printf(“Hello,world!\n”);
13 | }
14 | main()
15 | {
16 | greeting();
17 | }
18 |
19 | 之所以把这样简单的程序写成两个函数,是为了说明指令的转移过程。我们用gcc和ld对其进行编译和连接,得到可执行代码hello。然后,用Linux的实用程序objdump对其进行反汇编:
20 |
21 | % objdump –d hello
22 |
23 | 得到的主要片段为:
24 |
25 | 08048568 :
26 | 8048568: pushl %ebp
27 | 8048569: movl %esp, %ebp
28 | 804856b: pushl $0x809404
29 | 8048570: call 8048474 <_init+0x84>
30 | 8048575: addl $0x4, %esp
31 | 8048578: leave
32 | 8048579: ret
33 | 804857a: movl %esi, %esi
34 | 0804857c :
35 | 804857c: pushl %ebp
36 | 804857d: movl %esp, %ebp
37 | 804857f: call 8048568
38 | 8048584: leave
39 | 8048585: ret
40 | 8048586: nop
41 | 8048587: nop
42 |
43 | 最左边的数字是连接程序ld分配给每条指令或标识符的虚拟地址,其中分配给greeting()这个函数的起始地址为0x08048568。Linux最常见的可执行文件格式为elf(Executable and Linkable Format)。在elf格式的可执行代码中,ld总是从0x8000000开始安排程序的“代码段”,对每个程序都是这样。至于程序执行时在物理内存中的实际地址,则由内核为其建立内存映射时临时分配,具体地址取决于当时所分配的物理内存页面。
44 |
45 | 假定该程序已经开始运行,整个映射机制都已经建立好,并且CPU正在执行main()中的“call 08048568”这条指令,于是转移到虚地址0x08048568。Linux内核设计的段式映射机制把这个地址原封不动地映射为线性地址,接着就进入页式映射过程。
46 |
47 | 每当调度程序选择一个进程运行时,内核就要为即将运行的进程设置好控制寄存器CR3,而MMU的硬件总是从CR3中取得指向当前页目录的指针。
48 |
49 | 当我们的程序转移到地址0x08048568的时候,进程正在运行中,CR3指向我们这个进程的页目录。根据线性地址0x08048568最高10位,就可以找到相应的目录项。把08048568按二进制展开:
50 |
51 | 0000 1000 0000 0100 1000 0101 0110 1000
52 |
53 | 最高10位为0000 1000 00,即十进制32,这样以32为下标在页目录中找到其目录项。这个目录项中的高20位指向一个页表,CPU在这20位后填12个0就得到该页表的物理地址。
54 |
55 | 找到页表之后,CPU再来找线性地址的中间10位,为0001001000,即十进制72,于是CPU就以此为下标在页表中找到相应的页表项,取出其高20位,假定为0x840,然后与线性地址的最低12位0x568拼接起来,就得到greeting()函数的入口物理地址为0x840568, greeting()的执行代码就存储在这里。
56 |
57 |
--------------------------------------------------------------------------------
/第六章/section6_1.md:
--------------------------------------------------------------------------------
1 | ## 6.1 系统调用与应用编程接口、系统命令以及内核函数之关系
2 |
3 | 程序员或系统管理员并非直接与系统调用打交道,在实际使用中程序员调用的是应用编程接口API(Application Programming Interface),而管理员使用的则是系统命令。
4 |
5 | ### 6.1.1 系统调用与API
6 |
7 | Linux的应用编程接口(API)遵循了在Unix世界中最流行的应用编程接口标准——POSIX标准。POSIX标准是针对API而不是针对系统调用的。判断一个系统是否与POSIX兼容要看它是否提供了一组合适的应用编程接口,而不管对应的函数是如何实现的。事实上,一些非Unix系统被认为是与POSIX兼容的,是因为它们在用户态的库函数中提供了传统Unix能提供的所有服务。
8 |
9 | 应用编程接口(API)其实是一个函数定义,比如常见的read()、malloc()、free()、abs()函数等,这些函数说明了如何获得一个给定的服务;而系统调用是通过软中断向内核发出一个明确的请求。
10 |
11 | API有可能和系统调用的调用形式一致,比如read()函数就和read()系统调用的调用形式一致。但是,情况并不总是这样,这表现在两个方面,一种是几个不同的API其内部实现可能调用了同一个系统调用,例如,Linux的libc库实现了内存分配和释放的函数malloc(
12 | )、calloc( )和free( ),这几个函数的实现都调用了brk(
13 | )系统调用;另一方面,一个API的实现调用了好几个系统调用。更有些API甚至不需要任何系统调用,因为它们不需要内核提供的服务。
14 |
15 | 从编程者的观点看,API和系统调用之间没有什么差别,二者关注的都是函数名、参数类型及返回代码的含义。然而,从设计者的观点看,这是有差别的,因为系统调用实现是在内核完成的,而用户态的函数是在函数库中实现的。
16 |
17 | ### 6.1.2 系统调用与系统命令
18 |
19 | 系统命令相对应用编程接口更高一层,每个系统命令都是一个可执行程序,比如常用的系统命令ls、hostname等,这些命令的实现调用了系统调用。Linux的系统命令多数位于/bin和/sbin目录下。如果通过strace命令查看它们所调用的系统调用,比如
20 | strace ls或strace hostname,就会发现它们调用了诸如open、brk、fstat、ioctl
21 | 等系统调用。
22 |
23 | ### 6.1.3 系统调用与内核函数
24 |
25 | 内核函数与普通函数形式上没有什么区别,只不过前者在内核实现,因此要满足一些内核编程的要求[^1]。系统调用是用户进程进入内核的接口层,它本身并非内核函数,但它是由内核函数实现的,进入内核后,不同的系统调用会找到各自对应的内核函数,这些内核函数被称为系统调用的**“服务例程”**。比如系统调用
26 | getpid实际调用的服务例程为sys_getpid(),或者说系统调用getpid()是服务例程sys_getpid()的“**封装例程”**。下面是sys_getpid()在内核的具体实现:
27 |
28 | [^1]: 内核编程相比用户编程有一些特点,简单地讲内核程序一般不能引用C库函数;缺少内存保护措施;堆栈有限(因此调用嵌套不能过多);而且由于调度关系,必须考虑内核执行路径的连续性,不能有长睡眠等行为。
29 |
30 | ```c
31 | asmlinkage long sys_getpid(void)
32 |
33 | {
34 |
35 | return current->pid;
36 |
37 | }
38 | ```
39 |
40 | 如果想直接调用服务例程,Linux提供了一个syscall()函数,下面我们举例来对比一下调用系统调用和直接调用内核函数的区别。
41 |
42 | ```c
43 | #include
44 |
45 | #include
46 |
47 | #include
48 |
49 | #include
50 |
51 | int main(void)
52 |
53 | {
54 |
55 | long ID1, ID2;
56 |
57 | /*-----------------------------*/
58 |
59 | /* 直接调用内核函数*/
60 |
61 | /*-----------------------------*/
62 |
63 | ID1 = syscall(SYS_getpid);
64 |
65 | printf ("syscall(SYS_getpid)=%ld\n", ID1);
66 |
67 | /*-----------------------------*/
68 |
69 | /* 调用系统调用 */
70 |
71 | /*-----------------------------*/
72 |
73 | ID2 = getpid();
74 |
75 | printf ("getpid()=%ld\n", ID2);
76 |
77 | return(0);
78 |
79 | }
80 | ```
--------------------------------------------------------------------------------
/第九章/section9_1.md:
--------------------------------------------------------------------------------
1 | ## 9.1 概述
2 |
3 | Unix操作系统在最初设计的时候就将所有的设备都看成文件,也就是说,把设备纳入文件系统的范畴来管理。Linux操作系统的设计也遵循这一理念。把设备看成文件,具有以下含义:
4 |
5 | 1. 每个设备都对应一个文件名,在内核中也就对应一个索引节点。应用程序通过设备的文件名寻访具体的设备,而设备则像普通文件一样受到文件系统访问权限控制机制的保护。
6 |
7 | 2. 对文件操作的系统调用大都适用于设备文件。例如,通过open()系统调用可以打开设备文件,也就是说建立起应用程序与目标设备的连接。之后,就可以通过open()、write()、ioctl()等常规的文件操作对目标设备进行操作。
8 |
9 | 3. 从应用程序的角度看,设备文件逻辑上的空间是一个线性空间(起始地址为0,每读取一个字节加1)。从这个逻辑空间到具体设备物理空间(如磁盘的磁道、扇区)的映射则是由内核提供,并被划分为文件操作和设备驱动两个层次。
10 |
11 | 由此可以看出,对于一个具体的设备而言,文件操作和设备驱动是一个事物的不同层次。从这种观点出发,概念上可以把一个系统划分为应用、文件系统和设备驱动三个层次,如图9.1所示。
12 |
13 |
14 |
15 |

16 |
17 |
18 |
19 | 图9.1 设备驱动分层结构示意图
20 |
21 |
22 | Linux将设备分成三大类,就是块设备、字符设备和网络设备。像磁盘那样以块或扇区为单位,成块进行输入/输出的设备,称为 **块设备**,文件系统通常都建立在块设备上;另一类像键盘那样以字符(字节)为单位,逐个字符进行输入/输出的设备,称为 **字符设备**。
23 |
24 | 对于不同的设备,其文件系统层的“厚度”有所不同。对于像磁盘这样结构性很强,并且内容需要进一步组织和抽象的设备来说,其文件系统就很“厚重”,这是由磁盘设备的复杂性决定的。一方面是对磁盘物理空间的立体描述,如柱面、磁道、扇区;另一方面是从物理空间到逻辑空间的抽象,如第一层抽象,也就是把柱面、磁头、扇区这样的三维数据转换为一维线性地址空间中的“块”,比如8柱面,9磁头,10扇区对应的块号可能是798。这样操作者就不必关心读/写的物理位置究竟在哪个磁道,哪个扇区。文件系统则是在第一层抽象的基础上进行第二层抽象,即将块抽象和组织为文件系统。它使得操作者不必关心读/写的内容在哪一个逻辑“块”上。于是,我们把第一层抽象归为设备驱动,而把第二层抽象归为文件系统。另一方面,还有一些像字符终端这样的字符设备,其文件系统就比较“薄”,其设备驱动层也比较简单。
25 |
26 | 在图9.1中,处于应用层的进程通过文件描述符fd与已打开文件的file结构相联系,每个file结构代表着对一个已打开文件操作的上下文。通过这个上下文,进程就可以对各个文件线性逻辑空间中的数据进行操作。对于普通文件,即磁盘文件,文件的逻辑空间在文件系统层内按具体文件系统的结构和规则映射到设备的线性逻辑空间,然后在设备驱动层进一步从设备的逻辑空间映射到其物理空间。这样一共经历了两次映射。或者,可以反过来说,磁盘设备的物理空间经过两层抽象而成为普通文件的线性逻辑空间。而对于设备文件,则文件的逻辑空间通常直接等价于设备的逻辑空间,所以在文件系统层不需要映射。
27 |
28 | 与文件用唯一的索引结点标识相似,一个物理设备也用唯一的索引节点标识,索引节点中记载着与特定设备建立连接所需的信息。这种信息由三部分组成:文件(包括设备)的类型、主设备号和次设备号。其中设备类型和主设备号结合在一起唯一地确定了设备的驱动程序及其接口,而次设备号则说明目标设备是同类设备中的第几个。
29 |
30 | 要使一项设备在系统中成为可见、成为应用程序可以访问的设备,首先要在系统中建立一个代表此设备的设备文件,这是通过mknode命令或者mknode()系统调用实现的。除此之外,更重要的是在设备驱动层要有这种设备的驱动程序。
31 |
32 | Linux 驱动在本质上就是一种软件程序,上层软件可以在不用了解硬件特性的情况下,通过驱动提供的接口与计算机硬件进行通信。
33 |
34 | 系统调用是内核和应用程序之间的接口,而驱动程序是内核和硬件之间的接口,也就是内核和硬件之间的桥梁。它为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,应用程序可以像操作普通文件一样对硬件设备进行操作。
35 |
36 | 那么,系统是如何将设备在用户视野中屏蔽起来的呢?图9.2是对图9.1的进一步抽象,说明了用户进程请求设备进行输入输出的简单流程。
37 |
38 |
39 |

40 |
41 |
42 |
43 | 图9.2 用户进程请求设备服务的流程
44 |
45 |
46 | 首先当用户进程发出输入输出请求时(比如,程序中调用了read()函数),系统把请求处理的权限放在文件系统(例如进入内核调用文件系统的sys\_read()函数),文件系统通过驱动程序提供的接口(例如驱动程序提供的read()函数)将任务下放到驱动程序,驱动程序根据需要对设备控制器进行操作,设备控制器再去控制设备本身(参见8.2.1节)。
47 |
48 | 这样通过层层隔离,对用户进程基本上屏蔽了设备的各种特性,使用户的操作简便易行,不必去考虑具体设备的运作,就像操作文件一样去操作设备。这是因为,在驱动程序向文件系统提供接口时,已经屏蔽掉了设备的电器特性。
49 |
--------------------------------------------------------------------------------
/第一章/section1_4.md:
--------------------------------------------------------------------------------
1 | ## **1.4 Linux内核源代码**
2 |
3 | 在Linux内核官方网站[http://www.kernel.org](http://www.kernel.org),可以随时获取不同版本的Linux源代码。为了深入地了解 Linux的实现机制,有必要阅读Linux的源代码。
4 |
5 | ### **1.4.2 Linux内核版本**
6 |
7 | Linux内核版本从最初的0.01到目前的3.0.x不断发生着变化。Linux的内核具有两种不同的版本号,即实验版本和产品化版本。这种机制使用三个或者四个用“.”分隔的数字来代表不同内核版本。第一个数字是主版本号,第二个数字是从版本号,第三个数字是修订版本号。第四个可选的数字为稳定版本号。从版本号可以反映出该内核是一个产品化版本还是一个处于开发中的实验版本:该数字如果是偶数,那么此内核就是产品化版,如果是奇数,那么它就是实验版。举例来说,版本号为2.6.30.1的内核,它就是一个产品化版。这个内核的主板本号是2,从版本号是6,修订版本号是30,稳定版本号是1。头两个数字在一起描述了“内核系列”—在这个例子中,就是2.6版内核系列。
8 |
9 | Linux的两种版本是相互关联的。实验版本最初是产品化版本的拷贝,然后产品化版本只修改错误,实验版本继续增加新功能,到实验版本测试证明稳定后拷贝成新的产品化版本,不断循环。如图1.4所示:
10 |
11 |
12 |

13 |
14 |
15 |
16 | 这样的组织方式一方面可以方便软件开发人员加入到Linux的开发和测试中来,另一方面又可以让一些用户使用稳定的Linux版本。目前,比较稳定的内核版本是2.6.x,最新版本为3.0.x。
17 |
18 | ### **1.4.3 Linux内核源代码的结构**
19 |
20 | Linux内核源代码位于`/usr/src/linux`目录下,其主要目录结构分布如图1.5所示,
21 |
22 |
23 |

24 |
25 |
26 |
27 |
28 | 下面对每一个目录给予简单描述。
29 |
30 | **- include/** 子目录包含了建立内核代码时所需的大部分包含文件。
31 |
32 | **- init/** 子目录包含了内核的初始化代码,这是内核开始工作的起点。
33 |
34 | **- arch/** 子目录包含了Linux支持的所有硬件结构的内核代码,如图1.5,
35 |
36 | **- arch/** 子目录下有x86、ARM和alpha等针对不同体系结构的代码。
37 |
38 | **- drivers/** 目录包含了内核中所有的设备驱动程序,如字符设备,块设备,scsi 设备驱动程序等。
39 |
40 | **- fs/** 目录包含了所有文件系统的代码,如ext3/ext4, ntfs模块的代码等等。
41 |
42 |
43 | **- net/** 目录包含了内核中关于网络的代码。
44 |
45 | **- mm/** 目录包含了所有的内存管理代码。
46 |
47 | **- ipc/** 目录包含了进程间通信的代码。
48 |
49 | **- kernel/** 目录包含了主内核代码
50 |
51 | ### **1.4.4 Linux内核源代码分析工具**
52 |
53 | Linux的内核组织结构虽然非常有条理,但是,它毕竟是众人合作的结果,在阅读代码的时候要将各个部分结合起来,确实是件非常困难的事情。因为在内核中的代码层次结构肯定分多个层次,那么对一个函数的分析,肯定会涉及到多个函数,而每一个函数可能又有多层的调用,一层层下来,直接在代码文件中查找那些函数会让你失去耐心和兴趣。
54 |
55 | 俗话说:“工欲善其事,必先利其器”。面对Linux这样庞大的源代码,必须有相应工具的支持才能使分析有效地进行下去。 在此介绍两种源代码的分析工具,希望能对感兴趣的读者有所帮助。
56 |
57 | **1. Linux超文本交叉代码检索工具**
58 |
59 | Linux超文本交叉代码检索工具LXR(Linux Cross Reference),是由挪威奥斯陆大学数学系Arne Georg Gleditsch和Per Kristian Gjermshus编写的。这个工具实际上运行在Linux或者Unix平台下,通过对源代码中的所有符号建立索引,从而可以方便的检索任何一个符号,包括函数、外部变量、文件名、宏定义等等。不仅仅是针对Linux源代码,对于C语言的其他大型的项目,都可以建立其lxr站点,以提供开发者查询代码,以及后继开发者学习代码。
60 |
61 | 目前的lxr是专门为Linux下面的Apache服务器设计的,通过运行perl脚本,检索源代码索引文件,将数据发送到网络客户端的Web浏览器上。任何一种平台上的Web浏览器都可以访问,这就方便了习惯在Windows平台下工作的用户。 关于lxr的英文网站为[http://lxr.linux.no/](http://lxr.linux.no/)。
62 |
63 | **2.Windows平台下的源代码阅读工具Source Insight**
64 |
65 | 为了方便地学习Linux源程序,我们不妨回到我们熟悉的window环境下。但是在Window平台上,使用一些常见的集成开发环境,效果也不是很理想,比如难以将所有的文件加进去,查找速度缓慢,对于非Windows平台的函数不能彩色显示。在Windows平台下有一个强大的源代码编辑器,它的卓越性能使得学习Linux内核源代码的难度大大降低,这便是Source Insight,它是一个Windows平台下的共享软件,可以从[http://www.sourceinsight.com/](http://www.sourceinsight.com/)上下载试用版本。由于Source Insight是一个Windows平台的应用软件,所以首先要通过相应手段把Linux系统上的程序源代码移到Windows平台下,这一点可以通过在Linux下将/usr/src目录下的文件拷贝到Windows的分区上,或者从网上或光盘直接拷贝文件到Windows的分区。
66 |
--------------------------------------------------------------------------------
/第二章/section2_4.md:
--------------------------------------------------------------------------------
1 | ## **2.4 Linux中的分页机制**
2 |
3 | 如前所述,Linux主要采用分页机制来实现虚拟存储器管理。这是因为:
4 |
5 | (1)Linux的分段机制使得所有的进程都使用相同的段寄存器值,这就使得内存管理变得简单,也就是说,所有的进程都使用同样的线性地址空间(0~4G)。
6 |
7 | (2)Linux设计目标之一就是能够把自己移植到绝大多数流行的处理器平台。但是,许多RISC处理器支持的段功能非常有限。
8 |
9 | 为了保持可移植性,Linux采用三级分页模式而不是两级,这是因为许多处理器(如康柏的Alpha,Sun的UltraSPARC,Intel的Itanium)都采用64位结构的处理器,在这种情况下,两级分页就不适合了,必须采用三级分页。图2.17为三级分页模式,为此,Linux定义了三种类型的页表:
10 |
11 | - 页总目录PGD(Page Global Directory)
12 | - 页中间目录PMD(Page Middle Derectory)
13 | - 页表PT(Page Table)
14 |
15 |
16 |

17 |
18 |
19 |
20 | 尽管Linux采用的是三级分页模式,但我们的讨论还是以80X86的两级分页模式为主,因此,Linux忽略中间目录层,以后,我们把页总目录就叫页目录。
21 |
22 | 我们将在第四章看到,每一个进程有它自己的页目录和自己的页表集。当进程切换发生时(参见第三章“进程切换”一节),Linux把cr3控制寄存器的内容保存在前一个执行进程的PCB中,然后把下一个要执行进程的PCB的值装入cr3寄存器中。因此,当新进程恢复在CPU上执行时,分页单元指向一组正确的页表。
23 |
24 | 当三级分页模式应用到只有两级页表的奔腾处理器上时会发生什么情况?Linux使用一系列的宏来掩盖各种平台的细节。例如,通过把PMD看作只有一项的表,并把它存放在pgd表项中(通常pgd表项中存放的应该是pmd表的首地址)。页表的中间目录(pmd)被巧妙地“折叠”到页表的总目录(pgd)中,从而适应了二级页表。
25 |
26 |
27 | ### **2.4.1 模拟页表初始化**
28 |
29 | 在了解页表基本原理之后,通过代码来模拟内核初始化页表的过程:
30 |
31 | #define NR_PGT 0x4
32 | #define PGD_BASE (unsigned int*)0x1000
33 | #define PAGE_OFFSET (unsigned int)0x2000
34 |
35 | #define PTE_PRE 0x01 /* Page present */
36 | #define PTE_RW 0x02 /* Page Readable/Writeable */
37 | #define PTE_USR 0x04 /* User Privilege Level*/
38 |
39 | void page_init()
40 | {
41 | int pages = NR_PGT; // 系统初始化创建4个页表
42 |
43 | unsigned int page_offset = PAGE_OFFSET;
44 | unsigned int* pgd = PGD_BASE; // 页目录表位于物理内存的第二个页框内
45 | unsigned int phy_add = 0x0000; // 在物理地址的最低端建立页机制所需的表格
46 |
47 | // 页表从物理内存的第三个页框处开始
48 | // 物理内存的头8KB没有通过页表映射
49 | unsigned int* pgt_entry = (unsigned int*)0x2000;
50 |
51 | while (pages--)// 填充页目录表,这里依次创建4个页目录表
52 | {
53 | *pgd++ = page_offset |PTE_USR|PTE_RW|PTE_PRE;
54 | page_offset += 0x1000;
55 | }
56 |
57 | pgd = PGD_BASE;
58 |
59 |
60 | while (phy_add < 0x1000000) { // 在页表中填写页到物理地址的映射关系,映射了4M大小的物理内存
61 | *pgt_entry++ = phy_add |PTE_USR|PTE_RW|PTE_PRE;
62 | phy_add += 0x1000;
63 | }
64 |
65 |
66 | __asm__ __volatile__ ("movl %0, %%cr3;"
67 | "movl %%cr0, %%eax;"
68 | "orl $0x80000000, %%eax;"
69 | "movl %%eax, %%cr0;"::"r"(pgd):"memory","%eax");
70 | }
71 |
72 | 从代码中可以看出在物理内存的第二个页框设置了页目录,然后的while循环初始化了页目录中的四个页目录项,即四个页表。紧接着的第二个while循环初始化了四个页表中的第一个,其它三个没有用到,映射了4MB的物理内存,至此页表已初始化好,剩下的工作就是将页目录的地址pgd传递给cr3寄存器,这由gcc嵌入式代码部分完成,并且设置了cr0寄存器中的分页允许。最后一句以__asm开头的嵌入式汇编参见2.5.3节。
73 |
74 | 虽然上述代码比较简单,但却描述了页表初始化的过程,以此为模型我们可以更容易理解Linux内核中关于页表的代码。
75 |
76 |
--------------------------------------------------------------------------------
/SUMMARY.md:
--------------------------------------------------------------------------------
1 | #
2 | Summary
3 |
4 | * [Introduction](README.md)
5 | * [1 概述](第一章.md)
6 | * [1.1 认识操作系统](第一章/section1_1.md)
7 | * [1.2 开放源代码的Unix/Linux操作系统](第一章/section1_2.md)
8 | * [1.3 Linux内核](第一章/section1_3.md)
9 | * [1.4 Linux内核源代码](第一章/section1_4.md)
10 | * [1.5 Linux内核模块编程入门](第一章/section1_5.md)
11 | * [1.6 应用程序与内核模块的比较](第一章/section1_6.md)
12 | * [1.7 Linux内核中链表的实现及应用](第一章/section1_7.md)
13 | * [1.8 小结](第一章/section1_8.md)
14 | * [习题](第一章/exercise.md)
15 | * [2 内存寻址](第二章.md)
16 | * [2.1 内存寻址](第二章/section2_1.md)
17 | * [2.2 段机制](第二章/section2_2.md)
18 | * [2.3 分页机制](第二章/section2_3.md)
19 | * [2.4 Linux中的分页机制](第二章/section2_4.md)
20 | * [2.5 Linux中的汇编语言](第二章/section2_5.md)
21 | * [2.6 Linux系统地址映射举例](第二章/section2_6.md)
22 | * [2.7 小结](第二章/section2_7.md)
23 | * [习题](第二章/exercise.md)
24 | * [3 进程](第三章.md)
25 | * [3.1 进程介绍](第三章/section3_1.md)
26 | * [3.2 Linux系统中的进程控制块](第三章/section3_2.md)
27 | * [3.3 Linux系统中进程的组织方式](第三章/section3_3.md)
28 | * [3.4 进程调度](第三章/section3_4.md)
29 | * [3.5 进程的创建](第三章/section3_5.md)
30 | * [3.6 与进程相关的系统调用及其应用](第三章/section3_6.md)
31 | * [3.7 系统调用及应用](第三章/section3_7.md)
32 | * [3.8 小结](第三章/section3_8.md)
33 | * [习题](第三章/exercise.md)
34 | * [4 内存管理](第四章/introduction.md)
35 | * [4.1 Linux的内存管理概述](第四章/section4_1.md)
36 | * [4.2 进程的用户空间管理](第四章/section4_2.md)
37 | * [4.3 请页机制](第四章/section4_3.md)
38 | * [4.4 物理内存分配与回收](第四章/section4_4.md)
39 | * [4.5 交换机制](第四章/section4_5.md)
40 | * [4.6 内存管理实例](第四章/section4_6.md)
41 | * [4.7 本章小结](第四章/section4_7.md)
42 | * [习题](第四章/exercise4.md)
43 | * [5 中断和异常](第五章/introduction.md)
44 | * [5.1 中断是什么](第五章/section5_1.md)
45 | * [5.2 中断描述符表的初始化](第五章/section5_2.md)
46 | * [5.3 中断处理](第五章/section5_3.md)
47 | * [5.4 中断的下半部处理机制](第五章/section5_4.md)
48 | * [5.5 中断应用-时钟中断](第五章/section5_5.md)
49 | * [5.6 本章小结](第五章/section5_6.md)
50 | * [习题](第五章/exercise5.md)
51 | * [6 系统调用](第六章/introduction.md)
52 | * [6.1 系统调用与应用编程接口、系统命令以及内核函数之关系](第六章/section6_1.md)
53 | * [6.2 系统调用基本概念](第六章/section6_2.md)
54 | * [6.3 系统调用实现](第六章/section6_3.md)
55 | * [6.4 封装例程](第六章/section6_4.md)
56 | * [6.5 添加新系统调用](第六章/section6_5.md)
57 | * [6.6 实例-系统调用日志收集系统](第六章/section6_6.md)
58 | * [6.7 本章小结](第六章/section6_7.md)
59 | * [习题](第六章/exercise6.md)
60 | * [7 内核中的同步](第七章/introduction.md)
61 | * [7.1 临界区和竞争状态](第七章/section7_1.md)
62 | * [7.2 内核同步措施](第七章/section7_2.md)
63 | * [7.3 生产者-消费者并发实例](第七章/section7_3.md)
64 | * [7.4 内核多任务并发实例](第七章/section7_4.md)
65 | * [7.5 小结](第七章/section7_5.md)
66 | * [习题](第七章/exercise7.md)
67 | * [8 文件系统](第八章/introduction.md)
68 | * [8.1 Linux文件系统基础](第八章/section8_1.md)
69 | * [8.2 虚拟文件系统](第八章/section8_2.md)
70 | * [8.3 文件系统的注册、安装与卸载](第八章/section8_3.md)
71 | * [8.4 文件的打开与读写](第八章/section8_4.md)
72 | * [8.5 编写一个文件系统](第八章/section8_5.md)
73 | * [8.6 小结](第八章/section8_6.md)
74 | * [习题](第八章/exercise8.md)
75 | * [9 设备驱动](第九章/introduction.md)
76 | * [9.1 概述](第九章/section9_1.md)
77 | * [9.2 设备驱动程序框架](第九章/section9_2.md)
78 | * [9.3 I/O空间的管理](第九章/section9_3.md)
79 | * [9.4 字符设备驱动程序](第九章/section9_4.md)
80 | * [9.5 块驱动程序](第九章/section9_5.md)
81 | * [9.6 小结](第九章/section9_6.md)
82 | * [习题](第九章/exercise9.md)
83 |
--------------------------------------------------------------------------------
/第八章/section8_3.md:
--------------------------------------------------------------------------------
1 | ## 8.3 文件系统的注册、安装与卸载
2 |
3 | ### 8.3.1 文件系统的注册和注销
4 |
5 | 当内核被编译时,就已经确定了可以支持哪些文件系统,这些文件系统在系统引导时,在VFS中进行注册。如果文件系统是作为内核可装载的模块,则在实际安装时进行注册,并在模块卸载时注销。
6 |
7 | 每个文件系统都有一个初始化例程,它的作用就是在VFS中进行注册,即填写一个叫做file\_system\_type 的数据结构,该结构包含了文件系统的名称以及一个指向对应的VFS超级块读取例程的地址。所有已注册的文件系统的file\_system\_type结构形成一个链表,我们把这个链表称为注册链表。图8.7所示就是内核中的file\_system\_type 链表,链表头由 file\_systems 变量指定。
8 |
9 |
10 |

11 |
12 |
13 |
14 | 图8.7 已注册的文件系统形成的链表
15 |
16 |
17 | 图8.7 仅示意性地说明系统中已安装的三个文件系统Ext2、proc 及 iso9660 的 file\_system\_type 结构所形成的链表。当然,系统中实际安装的文件系统要更多。
18 |
19 | file\_system\_type 的数据结构定义如下:
20 | ```c
21 | struct file_system_type {
22 | const char *name;/*文件系统的类型名*/
23 | int fs_flags;/*文件系统的一些特性*/
24 | ...
25 | struct module *owner;/*通常置为宏THIS_MODLUE,用以确定是否把文件系统作为模块来安装*/
26 | struct file_system_type * next;
27 | ...
28 | };
29 | ```
30 | 要对一个文件系统进行注册,就调用 register\_filesystem() 函数。如果不再需要这个文件系统,还可以撤消这个注册,即从注册链表中删除一个file\_system\_type
31 | 结构,此后系统不再支持该种文件系统。unregister\_filesystem() 函数就起这个作用的,它在执行成功后返回0,如果注册链表中本来就没有指定的要删除的结构,则返回-1。
32 |
33 | 我们可以通过8.2.7 节的方法来观察系统现有注册的文件系统,现在动手吧!
34 |
35 | ### 8.3.2 文件系统的安装
36 |
37 | 要使用一个文件系统,仅仅注册是不行的,还必须安装这个文件系统。在安装Linux时,硬盘上已经有一个分区安装了Ext4文件系统,它是作为根文件系统在启动时自动安装的。其实,在系统启动后你所看到的文件系统,都是在启动时安装的。如果你需要自己(一般是超级用户)安装文件系统,则需要指定三种信息:文件系统的名称、包含文件系统的物理块设备、文件系统在已有文件系统中的安装点。
38 |
39 | 把一个文件系统(或设备)安装到一个安装点时要用到的主要数据结构为mount,定义如下:
40 | ```c
41 | struct mount
42 | {
43 | struct list_head mnt_hash;/* 哈希表 */
44 | struct mount *mnt_parent;/*指向上一层安装点的指针*/
45 | struct dentry *mnt_mountpoint;/* 安装点的目录项 */
46 | ...
47 | struct list_head mnt_mounts; /* 子链表 */
48 | struct list_head mnt_child; /* 通过mnt_child进行遍历 */
49 | struct list_head mnt_list;
50 | ...
51 | struct mountpoint *mnt_mp; /*安装点 */
52 | ...
53 | };
54 | ```
55 | 下面对结构中的主要域给予进一步说明:
56 |
57 | 1. 为了对系统中的所有安装点进行快速查找,内核把它们按哈希表来组织,mnt\_hash就是形成哈希表的队列指针。
58 |
59 | 2. mnt\_mountpoint是指向安装点dentry结构的指针。而dentry指针指向安装点所在目录树中根目录的dentry结构。
60 |
61 | 3. mnt\_parent是指向上一层安装点的指针。如果当前的安装点没有上一层安装点(如根设备),则这个指针为NULL。同时,mount结构中还有mnt\_mounts和mnt\_child两个队列头,只要上一层mount结构存在,就把当前mount结构中mnt\_child链入上一层mount结构的mnt\_mounts队列中。这样就形成一颗设备安装的树结构,从一个mount结构的mnt\_mounts队列开始,可以找到所有直接或间接安装在这个安装点上的其他设备。如图8.2。
62 |
63 | 4. mnt\_list是指向vfsmount结构所形成链表的头指针。
64 |
65 | 每个文件系统都有它自己的根目录,如果某个文件系统(如)的根目录是系统目录树的根目录,那么该文件系统称为根文件系统。而其他文件系统可以安装在系统的目录树上,把这些文件系统要插入的目录就称为**安装点**。根文件系统的安装函数为mount\_root()。
66 |
67 | 一旦在系统中安装了根文件系统,就可以安装其他的文件系统。每个文件系统都可以安装在系统目录树中的一个目录上。
68 |
69 | 前面我们介绍了以命令方式来安装文件系统,在用户程序中要安装一个文件系统则可以调用 mount()系统调用,其内核实现函数为 sys\_mount()。安装过程主要工作是创建安装点对象,将其挂接到根文件系统的指定安装点下,然后初始化超级块对象,从而获得文件系统基本信息和相关的操作。
70 |
71 | ### 8.3.3 文件系统的卸载
72 |
73 | 如果文件系统中的文件当前正在使用,该文件系统是不能被卸载的。如果文件系统中的文件或目录正在使用,则VFS 索引节点缓冲区中可能包含相应的 VFS索引节点。内核根据文件系统所在设备的标识符,检查在索引节点缓冲区中否有来自该文件系统的VFS索引节点,如果有且使用计数大于0,则说明该文件系统正在被使用,因此,该文件系统不能被卸载。否则,查看对应的VFS 超级块,如果该文件系统的 VFS超级块标志为“脏”,则必须将超级块信息写回磁盘。上述过程结束之后,对应的 VFS超级块被释放,vfsmount 数据结构将从vfsmntlist链表中断开并被释放。具体的实现代码为fs/super.c中的sys\_umount()函数,在此不再进行详细的讨论。
74 |
--------------------------------------------------------------------------------
/第一章/section1_2.md:
--------------------------------------------------------------------------------
1 | ## **1.2 开放源代码的Unix/Linux操作系统**
2 |
3 | 在操作系统的历史上,Unix的生存周期最长。从Unix年诞生以来,虽然已经使用了40多年,但仍然是现有操作系统中最强大和优秀的系统之一。
4 |
5 | ### **1.2.1 Unix诞生和发展**
6 |
7 | 1965年在美国国防部高级研究计划署ARPA的支持下,麻省理工学院、贝尔实验室和通用电气公司决定开发一种“公用计算服务系统”, 希望能够同时支持整个波士顿所有的分时用户。该系统称作**MULTICS (MULTiplexed Information and Computing Service )。**
8 |
9 | MULTICS设计目标是通过电话线把远程终端接入计算机主机。但是,MULTICS研制难度超出了所有人预料。长期研制工作达不到预期目标,1969年4月贝尔实验室退出,通用电气公司也退出了。 但最终,经过多年的努力,MULTICS 成功地应用了。运行MULTICS的计算机系统在九十年代中陆续被关闭。
10 |
11 | MULTICS引入了许多现代操作系统领域的概念雏形,对随后操作系统特别是Unix的成功有着巨大的影响。
12 |
13 | 1969年,贝尔退出MULTICS研制项目后, Ken Thompson和Dennis M. Ritchie两个研究人员想申请经费购买计算机从事操作系统研究,但多次申请得不到批准。项目无着落,他们边在一台无人用的PDP-7上重新摆弄原先在 MULTICS 项目上设计的“空间旅行”游戏。为了使游戏能够在PDP-7上顺利运行,他们陆续开发了浮点运算软件包、显示驱动软件,设计了文件系统、实用程序、shell 和汇编程序。 1970年, 在一切完成后, 给新系统起了个同 MULTICS发音相近的名字Unix。随后, Unix用C语言全部重写, 自此, Unix诞生了。
14 |
15 | Unix是现代操作系统的代表。Unix运行时的安全性、可靠性以及强大的计算能力赢得广大用户的信赖。促使Unix系统成功的因素有三点, 首先, 由于Unix是用C语言编写, 因此它是可移植的,它可以运行在笔记本计算机、PC机、工作站直至巨型机上;第二, 系统源代码非常有效, 系统容易适应特殊的需求。最后, 也是最重要的一点, 它是一个良好的、通用的、多用户、多任务、分时操作系统。
16 |
17 | 尽管Unix已经不再是一个实验室项目了,但它仍然伴随着操作系统设计技术的进步而继续成长,人们仍然可以把它作为一个通用的操作系统用于研究和演练。不过,因为Unix最终变为一个商业操作系统,只有那些能负担得起许可费的企业才用得起,这限制了它的应用范围。Linux的出现完全改变了这种局面。
18 |
19 | ### **1.2.2 Linux诞生**
20 |
21 | Linux的第一个版本诞生于1991年,它的作者就是Linus Torvalds,这个芬兰小伙最初在做一个调度系统的作业时,福至心灵,他突发灵感开始着手将其改造为一个实用的操作系统。在开发初期,他借助了最负盛名的教育类操作系统Minix的一些思想和成果,但他有自己的雄心,要把自己这个系统变得比Minix更实用、更强健。他决定把自己的系统代码公布于众,并且欢迎任何人来帮助修改和扩充Linux系统,Linux选择了备受推崇的Unix系统接口标准(POSIX标准),由此Linux成为Unix风格操作系统家族中的一员,而且是一个代码完全公开的操作系统。
22 |
23 | Linux的生命力来自于它的开源思想,自Linus公开Linux代码以来,世界各地的软件工程师和爱好者不断积极地对Linux系统进行修改和加强,将其版本从0.1 提高到2.0 、2.2、2.4、2.6,一直到如今的3.0,同时Linux也被从初期的x86平台移植到了PowerPC、Arm,Sparc、MIPS、68K等几乎市面上能找到的所有体系结构上,尤其是建立在Linux之上的Android系统,大大加强了Linux系统的实用性。
24 |
25 | Linux作为开源软件皇冠上的明珠,越来越受到欢迎,毫无疑问地成为人气最旺,最活跃的GNU项目,围绕Linux社区内各种组织雨后春笋般地出现,Linux必将在教育领域、在工业领域取得更大的成功。
26 |
27 | ### **1.2.3操作系统标准POSIX**
28 |
29 | POSIX 表示可移植操作系统接口(Portable Operating System Interface)。该标准由IEEE制订,并由国际标准化组织接受为国际标准。POSIX是在Unix标准化过程中出现的产物。到目前为止,POSIX已成为一个涵盖范围很广的标准体系,己经颁布了20多个相关标准,其中POSIX 1003.1标准定义了一个最小的Unix操作系统接口。也就是说,1003.1标准给出了一组函数的定义,至于如何实现,标准并不关注;或者说POSIX 1003.1提供了一种机制,而具体的策略由实现者决定。
30 |
31 | 任何操作系统只有符合POSIX 1003.1这一标准,就可以运行Unix程序。Linux在设计时遵循这一标准,因此,凡是在Unix上运行的应用程序几乎都可以在Linux上运行,这也是Linux得以流行的原因之一。
32 |
33 | ### **1.2.4 GNU 和 Linux**
34 |
35 | GNU 是 GNU Is Not Unix 的递归缩写,是自由软件基金会的一个项目,该项目的目标是开发一个自由的 Unix 版本,这一 Unix 版本称为 HURD。尽管 HURD 尚未完成,但 GNU 项目已经开发了许多高质量的编程工具,包括 emacs 编辑器、著名的 GNU C 和 C++ 编译器(gcc 和 g++),这些编译器可以在任何计算机系统上运行。所有的 GNU 软件和派生工作均适用 GNU 通用公共许可证,即 GPL。GPL 允许软件作者拥有软件版权,但授予其他任何人以合法复制、发行和修改软件的权利。
36 |
37 | Linux 的开发使用了许多 GNU 工具。Linux 系统上用于实现 POSIX.2 标准的工具几乎都是 GNU 项目开发的,Linux 内核、GNU 工具以及其他一些自由软件组成了人们常说的 Linux系统或Linux发布版:
38 |
39 | 1. 符合 POSIX 标准的操作系统内核、 Shell 和外围工具。
40 | 2. C 语言编译器和其他开发工具及函数库。
41 | 3. X Window 窗口系统。
42 | 4. 各种应用软件,包括字处理软件、图象处理软件等。
43 | 5. 其他各种 Internet 软件,包括 FTP 服务器、WWW 服务器等。
44 | 6. 关系数据库管理系统等。
45 |
46 | ### **1.2.5 Linux的开发模式**
47 |
48 | Linux是一大批广泛分布于世界各地的软件爱好者,以互联网为纽带,通过BBS、新闻组及电子邮件等现代通信方式,同时参与的软件开发项目。Linux的开发模式是开放与协作的,在设计上融合了各方面的优点,也经历了各种各样的测试与考验。它具有一下特点:
49 |
50 | - 开放与协作的开发模式。提供源代码,遵守GPL。
51 | - 发挥集体智慧,减少重复劳动。
52 | - 经历了各种各样的测试与考验,软件的稳定性好。
53 | - 开发人员凭兴趣去开发,热情高,具有创造性。
54 |
--------------------------------------------------------------------------------
/第一章/section1_3.md:
--------------------------------------------------------------------------------
1 | ## **1.3 Linux内核**
2 |
3 |
4 | Linux内核指的是在Linus领导下的开发小组开发出的系统内核,它是所有Linux 发布版本的核心。Linux内核软件开发人员一般在百人以上,任何自由程序员都可以提交自己的修改工作,但是只有领导者Linus和Alan Cox才能够将这些工作合并到正式的核心发布版本中。他们一般采用邮件列表来进行项目管理、交流、错误报告。其好处是软件更新速度和发展速度快,计划的开放性好。由于有大量的用户进行测试,而最终裁决人只有少数非常有经验的程序员,因此正式发布的代码质量高。
5 |
6 | ### **1.3.1 Linux 内核的技术特点**
7 |
8 | Linux是一种是实用性很强的现代操作系统。开发它的中坚力量是经验丰富的软件工程师,他们 多以实用性和效率为出发点,同时还考虑了工业规范和兼容性等因素,因此不同于教学性操作系统单纯追求理论上的先进性,Linux系统内核兼具实用性和高效性。其特色如下:
9 |
10 | 1) Linux内核被设计成单内核(monolithic)结构,这是相对微内核而言的。所谓单内核就是从整体上把内核作为一个大过程来实现,而进程管理、内存管理等是其中的一个个模块,模块之间可以直接调用相关的函数。微内核是一种功能更贴近硬件的核心软件,它一般仅仅包括基本的内存管理、同步原语、进程间通讯机制、I/O操作和中断管理,这样做有利于提高可扩展性和可移植性。但是微内核与诸如文件管理、设备驱动、虚拟内存管理、进程管理等其它上层模块之间需要有较高的通讯开销,所以目前多集中在理论教学领域,对工业应用来说,效率难以保证 ,因此单内核的Linux效率高,紧凑性强。
11 |
12 | 2) 2.6版本前的Linux内核是单线程结构——所谓单线程结构是说同一时间只允许有一个执行线程(内核中函数独立执行)在内核中运行 ,不会被调度程序打断而运行其它任务,这种内核称为非抢占式的,它的好处在于内核中没有并发任务(单处理器而言),因此避免了许多复杂的同步问题,但其不利影响是非抢占特性延迟了系统响应速度,新任务必须等待当前任务在内核执行完毕并自动退出后才能获得运行机会。然而,工业控制领域需要高响应速度,由于Robert love等人的贡献,2.6版本将抢占技术引入了Linux内核,使其变为可以进行内核抢占的操作系统 ——当然,付出的代价是同步变得更复杂。
13 |
14 | 3)Linux内核支持动态加载内核模块。为了保证能方便地支持新设备、新功能,又不会无限地扩大内核规模,Linux系统对设备驱动或新文件系统等采用了模块化的方式,用户在需要时可以现场动态加载,使用完毕可以动态卸载。同时对内核,用户也可以定制,选择适合自己的功能,将不需要的部分剔除出内核。这些都保证了内核的紧凑、可扩展性好。
15 |
16 | 4) Linux内核被动地提供服务。所谓被动是因为它为用户服务的唯一方式是通过系统调用来请求在内核空间执行某种任务。内核本身是一种函数和数据结构的集合,不存在运行着的内核进程为用户提供服务
17 |
18 | 5) Linux内核采用了虚拟内存技术,使得内存空间达到4GB 。其中0-3G属于用户空间,称为用户段,3G-4G属于内核空间,称为内核段。这样,应用程序就可以使用远远大于实际物理内存的存储空间了。
19 |
20 | 6) Linux的文件系统实现了一种抽象文件模型——虚拟文件系统(Virtual Filesystem Switch,VFS),该文件系统属于Unix风格。VFS是Linux的特色之一。通过使用虚拟文件系统,内核屏蔽了各种不同文件系统的内在差别,使得用户可以通过统一的界面访问各种不同格式的文件系统。
21 |
22 | 7) Linux提供了一套很有效的延迟执行机制——下半部分、软中断、tasklet和2.6新引入的工作队列等,这些技术保证了系统可以针对任务的轻重缓急,更细粒度地选择执行时机。
23 |
24 | Linux内核的以上特点,在后续的章节中会逐步体现出来。
25 |
26 | ### **1.3.2 Linux内核的位置**
27 |
28 | Linux内核不是孤立的,必须把它放在整个Linux系统中去研究,图1.2显示了Linux内核在整个系统中的位置:
29 |
30 |
31 |

32 |
33 |
34 |
35 | 由图1.2可以看出,整个系统由四个部分组成:
36 |
37 | 1. 用户进程—用户应用程序是运行在Linux内核之上的一个庞大的软件集合, 当一个用户程序在操作系统之上运行时,它成为操作系统中的一个进程。关于进程更详细的描述参见第三章。
38 | 2. 系统调用接口— 在应用程序中,可通过系统调用来调用操作系统内核中特定的过程,以实现特定的服务。例如,在程序中有一条读取数据的read()系统调用,但是,真正的读取操作是由操作系统内核完成的。所以说,系统调用是内核代码的一部分,更详细内容参看第六章。
39 | 3. Linux内核—内核是操作系统的灵魂,它负责管理内存、磁盘上的文件,负责启动并运行程序,负责从网络上接收和发送数据包等等。内核是本书讨论的重点。
40 | 4. 硬件—这个子系统包括了Linux安装时需要的所有可能的物理设备。例如,CPU、 内存、硬盘、网络硬件等等。
41 |
42 | ### **1.3.3 Linux内核体系结构**
43 |
44 | 虽然Linux内核和Unix系统在具体实现上有很大不同,但是其结构还基本保持一致,Linux内核除系统调调用外,由5个主要的子系统组成,如图1.3
45 |
46 |
47 |

48 |
49 |
50 |
51 | 1. **进程调度(Process Scheduler,SCHED)**-控制着进程对CPU的访问。当需要选择一个进程运行时,由调度程序选择最值得运行的进程。
52 | 2. **内存管理(Memory Manager,MM)**-允许多个进程安全地共享主内存区域 。Linux的内存管理支持虚拟内存,即在计算机中运行的程序,其代码、数据和堆栈的总量可以超过实际内存的大小,操作系统只将当前使用的程序块保留在内存中,其余的程序块则保留在磁盘上。必要时,操作系统负责在磁盘和内存之间交换程序块。
53 | 因为虚拟内存管理需要硬件支持,因此内存管理从逻辑上可以分为硬件无关的部分和硬件相关的部分。详细内容参看第二章和第四章。
54 | 3. **虚拟文件系统(VFS)**-隐藏各种不同硬件的具体细节,为所有设备提供统一的接口。虚拟文件系统支持多达数十种不同的文件系统,这也是Linux较有特色的一部分。
55 | 虚拟文件系统可分为逻辑文件系统和设备驱动程序。逻辑文件系统指Linux所支持的文件系统,如ext2/ext3, ntfs等,设备驱动程序指为每一种硬件控制器所编写的设备驱动程序模块。详细内容参看第八章和第九章。
56 | 4. **网络接口(Network Interface,NET)**-提供了对各种网络标准协议的存取和各种网络硬件的支持。网络子系统可分为网络协议和网络驱动程序两部分。网络协议部分负责实现每一种可能的网络传输协议,网络设备驱动程序负责与硬件设备进行通信,每一种可能的硬件设备都有相应的设备驱动程序。因为这部分内容相对独立和复杂,本书不做详细介绍。
57 | 5. **进程间通信(Inter-Process Communication,IPC)**- 支持进程间各种通信机制,包括共享内存、消息队列及管道等。这部分内容也相对独立,本书不做详细介绍。
58 |
59 | 从图1.3可以看出,处于中心位置的是进程调度,所有其它的子系统都依赖于它,因为每个子系统都需要挂起或恢复进程。一般情况下,当一个进程等待硬件操作完成时,它被挂起;当操作真正完成时,进程恢复执行。例如,当一个进程通过网络发送一条消息时,发送进程被挂起,一直到硬件成功地完成消息的发送。其它子系统(内存管理,虚拟文件系统及进程间通信)以相似的理由依赖于进程调度。
60 |
61 |
--------------------------------------------------------------------------------
/第八章/section8_4.md:
--------------------------------------------------------------------------------
1 | ## 8.4 文件的打开与读写
2 |
3 | 让我们重新考虑一下在本章开始所提到的例子,用户发出一条shell命令:把/floppy/TEST中的MS-DOS文件拷贝到/tmp/test中的Ext4文件中。命令shell调用外部程序(如cp),在实现cp的代码片段中,涉及文件系统常见的三种文件操作,也就是三个系统调用open()、read()和write()。下面就对这三个系统调用的实现及涉及的相关知识给予介绍。
4 |
5 | ### 8.4.1文件打开
6 |
7 | open()系统调用就是打开文件,它返回一个文件描述符。所谓打开文件实质上是在进程与文件之间建立起一种连接,而“文件描述符”唯一地标识着这样一个连接。在文件系统的处理中,每当一个进程打开一个文件时,就建立起一个独立的读/写文件“上下文”,这个“上下文”由file数据结构表示。另外,打开文件,还意味着将目标文件的索引节点从磁盘载入内存,并对其进行初始化。
8 |
9 | open操作在内核中是由do\_sys\_open()函数完成的,其代码如下:
10 | ```c
11 | long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
12 | {
13 | struct open_flags op;
14 | int lookup = build_open_flags(flags, mode, &op);
15 | struct filename *tmp = getname(filename);
16 | int fd = PTR_ERR(tmp);
17 |
18 | if (!IS_ERR(tmp)) {
19 | fd = get_unused_fd_flags(flags);
20 | if (fd >= 0) {
21 | struct file *f = do_filp_open(dfd, tmp, &op, lookup);
22 | if (IS_ERR(f)) {
23 | put_unused_fd(fd);
24 | fd = PTR_ERR(f);
25 | } else {
26 | fsnotify_open(f);
27 | fd_install(fd, f);
28 | }
29 | }
30 | putname(tmp);
31 | }
32 | return fd;
33 | }
34 | ```
35 | 其中,调用参数filename是文件的路径名(绝对路径名或相对路径名);mode表示打开的模式,如“只读”等;而flag则包含许多标志位,用以表示打开模式以外的一些属性和要求。函数通过getname()从用户空间把文件的路径名拷贝到内核空间,并通过get\_unused\_fd\_flags(flags)从当前进程的“打开文件表”中找到一个空闲的表项,该表项的下标即为“文件描述符fd”。然后,通过do\_filp\_open()找到文件名对应索引节点的dentry结构以及inode结构,并找到或创建一个由file数据结构代表的读/写文件的“上下文”。通过fd\_install()函数,将新建的file数据结构的指针“安装”到当前进程的file\_struct结构中,也就是已打开文件指针数组中,其位置即已分配的下标fd。
36 |
37 | 在以上过程中,如果出错,则将分配的文件描述符、file结构收回,inode也被释放,函数返回一个负数以示出错,其中PTR\_ERR()和IS\_ERR()是出错处理函数。
38 |
39 | 由此可以看到,打开文件后,文件相关的“上下文”、索引节点、目录对象等都已经生成,下一步就是实际的文件读写操作了。
40 |
41 | ### 8.4.2文件读写
42 |
43 | 让我们再回到cp例子的代码。open()系统调用返回两个文件描述符,分别存放在inf和outf变量中。然后,程序开始循环。在每次循环中,/floppy/TEST文件的一部分被拷贝到一个缓冲区中,然后,这个缓冲区中的数据又被拷贝到/tmp/test文件。
44 |
45 | read()和write()系统调用非常相似。它们都需要三个参数:一个文件描述符fd、一个内存区的地址buf(该缓冲区包含要传送的数据),以及一个数count(指定应该传送多少字节)。当然,read()把数据从文件传送到缓冲区,而write()执行相反的操作。两个系统调用都返回所成功传送的字节数,或者发一个错误条件的信号并返回-1。
46 |
47 | 简而言之,read()和write()系统调用所对应的内核函数sys\_read() 和sys\_write()执行几乎相同的步骤:
48 |
49 | 1. file=fget(fd),也就是调用fget()从fd获取相应文件对象的地址file,并把引用计数器file-\>f\_count加1。
50 |
51 | 2. 检查file-\>f\_mode中的标志是否允许所请求的访问(读或写操作)。
52 |
53 | 3. 调用locks\_verify\_area()检查对要访问的文件部分是否有强制锁。
54 |
55 | 4. 调用file-\>f\_op-\>read
56 | 或file-\>f\_op-\>write来传送数据。这两个函数都返回实际传送的字节数。另一方面的作用是,文件指针被更新。
57 |
58 | 5. 调用fput()以减少引用计数器file-\>f\_count的值。
59 |
60 | 6. 返回实际传送的字节数。
61 |
62 | 以上概述了文件读写的基本步骤,但是f\_op-\>read或
63 | f\_op-\>write两个方法属于VFS提供的抽象方法,对于具体的文件系统,必须调用针对该具体文件系统的具体方法。而对基于磁盘的文件系统,比如EXT4等,所调用的具体的读写方法都是Linux内核已提供的通用函数generic\_file\_read()或generic\_file\_write()。简单地说,这些通用函数的作用是确定正被访问数据所在物理块的位置,并激活块设备驱动程序开始数据传送,所以基于磁盘的文件系统没必要再实现专用函数了。下面对generic\_file\_read()函数所执行的主要步骤简述如下:
64 |
65 | 第一步:利用给定的文件偏移量和读写字节数计算出数据所在页的逻辑号(index)。
66 |
67 | 第二步: 开始传送数据页。
68 |
69 | 第三步: 更新文件指针,记录时间戳等收尾工作。
70 |
71 | 其中最复杂的是第二步,首先内核会检查数据是否已经驻存在页缓冲区,如果在页缓冲区中发现所需数据而且数据是有效的,那么内核就可以从缓存中快速返回需要的页;否则如果页中的数据是无效的,那么内核将分配一个新页面,然后将其加入到页缓冲区中,随即调用address\_space对象的readpage方法,激活相应的函数进行磁盘到页的I/O数据传送。在进行一定预读,并完成数据传送之后,还要调用file\_read\_actor()方法把页中的数据拷贝到用户态缓冲区,最后进行一些收尾等工作,如更新标志等。
72 |
73 | 总之,从用户发出读请求到最终的从磁盘读取数据,可以概括为以下几步:
74 |
75 | 1. 用户界面层——负责从用户函数经过系统调用进入内核;
76 |
77 | 2. 基本文件系统层——负责调用文件读方法,从缓冲区中搜索数据页,返回给用户。
78 |
79 | 3. I/O调度层——负责对请求排队,从而提高吞吐量。
80 |
81 | 4. I/O传输层——利用任务队列,异步操作设备控制器,完成数据传输。
82 |
83 | 图8.9 给出读操作的逻辑流程。
84 |
85 |
86 |

87 |
88 |
89 |
90 | 图8.9 读操作流程
91 |
92 |
--------------------------------------------------------------------------------
/第六章/section6_5.md:
--------------------------------------------------------------------------------
1 | ## 6.5 添加新系统调用
2 |
3 | 系统调用是用户空间和内核空间交互的一种有效手段。除了系统本身提供的系统调用外,我们也可以添加自己的系统调用。
4 |
5 | 实现一个新的系统调用的第一步是决定它的用途。它要做些什么?每个系统调用都应该有一个明确的用途。在Linux中不提倡采用多用途的(一个系统调用通过传递不同的参数值来选择完成不同的工作)系统调用。
6 |
7 | 新系统调用的参数、返回值和错误码又该是什么呢?系统调用的界面应该力求简洁,参数尽可能少。系统调用的语义和行为非常关键;因为应用程序依赖它们,所以它们应力求稳定,不作改动。
8 |
9 | 设计接口的时候要尽量为将来多做考虑。你是不是对函数做了不必要的限制?系统调用被设计的越通用越好。不要假设这个系统调用现在怎么用将来也一定就是这么用。系统调用的目的可能不变,但它的用法却可能改变。这个系统调用可移植吗?要确保不对系统调用做错误的假设,否则将来这个调用就可能会崩溃。记住Unix的格言:“提供机制而不是策略”。
10 |
11 | 当你写一个系统调用的时候,要时刻注意可移植性和健壮性,不但要考虑当前,还要为将来做打算。基本的Unix系统调用经受住了时间的考验;它们中的很大一部分到现在都还和30年前一样适用和有效。
12 |
13 | 首先我们通过添加一个简单的系统调用说明其实现步骤,然后说明如何添加一个稍微复杂的系统调用。
14 |
15 | 系统调用的实现需要调用内核中的函数,因此,内核版本不同,其内核函数名可能稍有差异,假定我们使用的内核版本为2.6.28,x86平台。内核源代码的默认目录为/usr/src/linux。
16 |
17 | ### 6.5.1 添加系统调用的步骤
18 |
19 | 我们要添加的这个系统调用没有返回值,也不用传递参数,其名取为mysyscall。其功能是使用户的uid等于0。步骤如下:
20 |
21 | #### 1.添加系统调用号
22 |
23 | 系统调用号在unistd.h文件中定义。内核中每个系统调用号都以“__NR_”开头,例如,fork的系统调用号为__NR_fork。于是,我们的系统调用号为__NR_mysyscall。具体在arch/x86/include/asm/unistd_32.h和/usr/include/asm
24 | /unistd_32.h文件中添加如下:
25 | ```c
26 | #define __NR_dup3 330
27 |
28 | #define __NR_pipe2 331
29 |
30 | #define __NR_inotify_init1 332
31 |
32 | #define __NR_mysyscall 333 /* mysyscall 系统调用号添加在这里*/
33 | ```
34 | 添加系统调用号后,系统才能把这个号作为索引去查找系统调用表sys_call_table中的相应表项。
35 |
36 | #### 2.在系统调用表中添加相应表项
37 |
38 | 如前所述,系统调用处理程序system_call会根据eax中的号到系统调用表sys_call_table中查找相应的系统调用服务例程,因此,我们必须把自己的服务例程sys_mysyscall添加到系统调用表中。系统调用表位于汇编语言arch/x86/kernel/syscall_table_32.S中:
39 | ```c
40 | ENTRY(sys_call_table)
41 |
42 | .long sys_restart_syscall /* 0 - old "setup()" system call, used for
43 | restarting */
44 |
45 | .long sys_exit
46 |
47 | .long sys_fork
48 |
49 | .long sys_read
50 |
51 | …
52 |
53 | .long sys_dup3 /* 330 */
54 |
55 | .long sys_pipe2
56 |
57 | .long sys_inotify_init1
58 |
59 | **.long sys_mysyscall /*333 号*/**
60 | ```
61 | 到此为止,内核已经能够正确地找到并且调用sys_mysyscall。接下来,就要实现该例程。
62 |
63 | #### 3.实现系统调用服务例程
64 |
65 | 我们把sys_mysyscall添加在kernel目录下的系统调用文件sys.c中:
66 |
67 | ```c
68 | asmlinkage int sys_mysyscall(void)
69 |
70 | {
71 |
72 | current->uid=0;
73 |
74 | }
75 | ```
76 |
77 | 其中的asmlinkage修饰符是gcc中一个比较特殊的标志。因为gcc常用的一种编译优化方法是使用寄存器传递函数的参数,而加了asmlinkage修饰符的函数必须从堆栈中而不是寄存器中获取参数。内核中所有系统调用的实现都使用了这个修饰符。
78 |
79 | #### 4.重新编译内核
80 |
81 | 通过以上三个步骤,我们要添加一个新系统调用的所有工作已经完成。但是,要使这个系统调用真正在内核运行起来,我还需要对内核进行重新编译。关于内核的编译,请参阅相关资料。
82 |
83 | #### 5.编写用户态程序
84 |
85 | 要测试新添加的系统调用,我们可以编写一个用户程序来调用这个系统调用:
86 |
87 | ```c
88 | #include
89 |
90 | _syscall0(int,mysyscall) /* unistd.h中对这个宏进行了定义*/
91 |
92 | int main()
93 |
94 | {
95 |
96 | printf(“This is my uid:%d.\n”, getuid());
97 |
98 | mysyscall();
99 |
100 | printf(“Now , my uid is changed:%d.\n”, getuid());
101 |
102 | }
103 | ```
104 |
105 | 上面这个例子是把系统调用直接加入内核,因此,需要重新编译内核。下面的例子是把系统调用以模块的形式加载到内核。
106 |
107 | ### 6.5.2 系统调用的调试
108 |
109 | 添加新的系统调用主要是对内核进行修改并编译。如果在用户态无法成功调用所加系统调用,此时,需判断是系统调用没有加进内核还是用户态的测试程序出现问题。下面给出一种解决方法,也就是将源码中的一部分提出来在用户态进行检测,如果没有添加成功,可以根据返回的错误码进行识别并处理。检测程序如下:
110 |
111 | ```c
112 | #include
113 |
114 | #include
115 |
116 | int main()
117 |
118 | {
119 |
120 | unsigned long sys_num=333;/*这里的数值是新添加的系统调用的系统调用号*/
121 |
122 | unsigned long value=0;
123 |
124 | __asm__ ("int $0x80":"=a"(value):"0"((long)(sys_num)));
125 |
126 | printf ("The value is %ld\n", value);
127 |
128 | return value;
129 |
130 | }
131 | ```
132 |
133 | 通过返回值来查看问题所在,如果返回-38则说明没有添加成功,返回-1则说明没有操作的许可权。更多可以查看/usr/include/asm/errno.h
134 |
135 | 另外,在2.6内核中没有宏syscallN()的定义,它的封装机制由libc库的INLINE_SYSCALL来完成。如果不想在libc库中添加它的封装例程,就需要将syscallN()的宏编译进内核,再在用户态程序以_syscallN()的形式对系统调用进行申明。
136 |
--------------------------------------------------------------------------------
/第七章/section7_3.md:
--------------------------------------------------------------------------------
1 | ## 7.3 生产者-消费者并发实例
2 |
3 | 随着人们生活水平的提高,每天早餐基本是牛奶,面包。而在牛奶生产的环节中,生产厂家必须和经销商保持良好的沟通才能使效益最大化,具体说就是生产一批就卖一批,并且只有卖完了,才能生产下一批,这样才能达到供需平衡,否则就有可能造成浪费(供过于求)或者物资短缺(供不应求)。假设现在有一个牛奶生产厂家,他有一个经销商,并且由于资金不足,只有一个仓库。牛奶生产厂家首先生产一批牛奶,并放在仓库里,然后通知经销商来批发。经销商卖完牛奶后,打电话再订购下一批牛奶。牛奶生产厂家接到订单后,才开始生产下一批牛奶。
4 |
5 | ### 7.3.1 问题分析
6 |
7 | 上述问题中,牛奶生产厂家就相当于“生产者”,经销商为“消费者”,仓库则为“公共缓冲区”。
8 | 问题属于单一生产者,单一消费者,单一公共缓冲区。这属于典型的进程同步问题。生产者和消费者为不同的线程,“公共缓冲区“则为临界区。在同一时刻,只能有一个线程访问临界区。
9 |
10 | ### 7.3.2 实现机制
11 |
12 | 1.数据定义
13 |
14 | ```c
15 | #include
16 | #include
17 | #include
18 | #include
19 | #include
20 | #include
21 | #include
22 | #define PRODUCT_NUMS 10
23 |
24 | static struct semaphore sem_producer;
25 | static struct semaphore sem_consumer;
26 | static char product[12];
27 | static atomic_t num;
28 | static int producer(void *product);
29 | static int consumer(void *product);
30 | static int id = 1;
31 | static int consume_num = 1;
32 | ```
33 |
34 | 2.生产者线程
35 |
36 | ```c
37 | static int producer(void *p)
38 | {
39 | char *product=(char *)p;
40 | int i;
41 | atomic_inc(&num);
42 | printk("producer [%d] start... \n", current->pid);
43 | for (i = 0; i < PRODUCT_NUMS; i++) {
44 | down(&sem_producer);
45 | snprintf(product,12,"2015-05-%d",id++);
46 | printk("producer [%d] produce %s\n",current->pid, product);
47 | up(&sem_consumer);
48 | }
49 | printk("producer [%d] exit...\n", current->pid);
50 | return 0;
51 | }
52 | ```
53 |
54 | 该函数代表牛奶生产厂家,负责生产十批牛奶,从代码中可以看出,它的执行受制于sem\_producer信号量,当该信号量无法获取时,它将进入睡眠状态,直到信号量可用,它才能继续执行,并且释放sem\_constumer信号量。
55 |
56 | 3.消费者线程
57 |
58 | ```c
59 | static int consumer(void *p)
60 | {
61 | char *product=(char *)p;
62 | printk("consumer [%d] start...\n", current->pid);
63 | for (;;) {
64 | msleep(100);
65 | down_interruptible(&sem_consumer);
66 | if(consume_num >= PRODUCT_NUMS * atomic_read(&num))
67 | break;
68 | printk("consumer [%d] consume %s\n",current->pid, product);
69 | consume_num++;
70 | memset(product,'\0',12);
71 | up(&sem_producer);
72 | }
73 | printk("consumer [%d] exit...\n", current->pid);
74 | return 0;
75 | }
76 | ```
77 |
78 | 该函数代表牛奶经销商,负责批发并销售牛奶。只有生产厂家生产了牛奶,下发了批发单,经销商才能批发牛奶。批发之后进行零售。当其零售完后,再向牛奶生产厂家下订货单。
79 |
80 | 4.模块插入和删除
81 |
82 | ```c
83 | static int procon_init(void)
84 | {
85 | printk(KERN_INFO"show producer and consumer\n");
86 | sema_init(&sem_producer,1);
87 | sema_init(&sem_consumer,0);
88 | atomic_set(&num, 0);
89 | kthread_run(producer,product,"conpro_1");
90 | kthread_run(consumer,product,"compro_1");
91 | return 0;
92 | }
93 | static void procon_exit(void)
94 | {
95 | printk(KERN_INFO"exit producer and consumer\n");
96 | }
97 |
98 | module_init(procon_init);
99 | module_exit(procon_exit);
100 | MODULE_LICENSE("GPL");
101 | MODULE_DESCRIPTION("producer and consumer Module");
102 | MODULE_ALIAS("a simplest module");
103 | ```
104 |
105 | ### 7.3.3 具体实现
106 |
107 | 对该模块的实际操作步骤如下:
108 |
109 | * make 编译模块
110 |
111 | * insmod procon.ko 加载模块
112 |
113 | * dmesg 观察结果
114 |
115 | * rmmod procon 卸载模块
116 |
117 | 从结果可以看出,生产者线程首先执行生产一批产品,然后等待消费者线程消费产品。只有消费者消费后,生产者才能再进行生产。生产者严格按照生产顺序生产,消费者也严格按照生产顺序消费。
118 |
119 |
--------------------------------------------------------------------------------
/第五章/section5_1.md:
--------------------------------------------------------------------------------
1 | ## 5.1 中断是什么
2 |
3 | 大多数读者可能对16位实地址模式下的中断机制有所了解,例如中断向量、外部I/O中断以及异常,这些内容在32位的保护模式下依然有效。两种模式之间最本质的差别就是在保护模式引入了中断描述符表。
4 |
5 | ### 5.1.1 中断向量
6 |
7 | Intelx86系列微机共支持256种向量中断,为使处理器较容易地识别每种中断源,将它们从0到256编号,即赋以一个中断类型码n,Intel把这个8位的无符号整数叫做一个向量,因此,也叫**中断向量**。所有256种中断可分为两大类:异常和中断。异常又分为**故障(Fault)和陷阱(Trap)**,它们的共同特点是既不使用中断控制器,又不能被屏蔽(异常其实是CPU发出的中断信号)。中断又分为外部**可屏蔽中断**(INTR)和外部**非屏蔽中断**(NMI),所有I/O设备产生的中断请求(IRQ)均引起屏蔽中断,而紧急的事件(如硬件故障)引起的故障产生非屏蔽中断。
8 |
9 | 非屏蔽中断的向量和异常的向量是固定的,而屏蔽中断的向量可以通过对中断控制器的编程来改变。Linux对256个向量的分配如下:
10 |
11 | 1. 从0~31的向量对应于异常和非屏蔽中断。
12 |
13 | 2. 从32~47的向量(即由I/O设备引起的中断)分配给屏蔽中断。
14 |
15 | 3. 剩余的从48~255的向量用来标识软中断。Linux只用了其中的一个(即128或0x80向量)用来实现系统调用。
16 |
17 | 说明,你可以在proc文件系统下的interrupts文件中,查看当前系统中各种外设的IRQ:
18 |
19 | $cat /proc/interrupts
20 |
21 | ### 5.1.2 外设可屏蔽中断
22 |
23 | Intelx86通过两片中断控制器8259A来响应15个外中断源,每个8259A可管理8个中断源。第一级(称主片)的第二个中断请求输入端,与第二级8259A(称从片)的中断输出端INT相连,如图5.1所示。我们把与中断控制器相连的每条线叫做**中断线**,要使用中断线,就要进行中断线的申请,也就是IRQ(Interrupt ReQuirement),因此我们也常把申请一条中断线称为申请一个IRQ或者是申请一个中断号。IRQ线是从0开始顺序编号的;因此,第一条IRQ线通常表示成IRQ0。IRQn的缺省向量是n+32;如前所述,IRQ和向量之间的映射可以通过中断控制器端口来修改。
24 |
25 |

26 |
27 |
28 | 图5.1 级连的 8259A的中断机构
29 |
30 | 并不是每个设备都可以向中断线上发中断信号的,只有对某一条确定的中断线拥有了控制权,才可以向这条中断线上发送信号。由于计算机的外部设备越来越多,所以15条中断线已经不够用了,中断线是非常宝贵的资源,所以只有当设备需要中断的时候才申请占用一个IRQ,或者是在申请IRQ时采用共享中断的方式,这样可以让更多的设备使用中断。
31 |
32 | 对于外部I/O请求的屏蔽可分为两种情况,一种是从CPU的角度,也就是清除eflag的中断标志位(IF),当IF=0时,禁止任何外部I/O的中断请求,即关中断;一种是从中断控制器的角度,因为中断控制器中有一个8位的中断屏蔽寄存器,每位对应8259A中的一条中断线,如果要禁用某条中断线,则把中断屏蔽寄存器相应的位置1,要启用,则置0。
33 |
34 | ### 5.1.3 异常及非屏蔽中断
35 |
36 | 异常就是CPU内部出现的中断,也就是说,在CPU执行特定指令时出现的非法情况。非屏蔽中断就是计算机内部硬件出错时引起的异常情况。从上图可以看出,二者与外部I/O接口没有任何关系。Intel把非屏蔽中断作为异常的一种来处理,因此,后面所提到的异常也包括了非屏蔽中断。在CPU执行一个异常处理程序时,就不再为其他异常或可屏蔽中断请求服务,也就是说,当某个异常被响应后,CPU清除eflag的中IF位,禁止任何可屏蔽中断。但如果又有异常产生,则由CPU锁存(CPU具有缓冲异常的能力),待这个异常处理完后,才响应被锁存的异常。我们这里讨论的异常中断向量在0~31之间,不包括系统调用(中断向量为0x80)。
37 |
38 | Intel x86处理器发布了大约20种异常(具体数字与处理器模式有关)。Linux内核必须为每种异常提供一个专门的异常处理程序。
39 |
40 | ### 5.1.4 中断描述符表
41 |
42 | 在实地址模式中,CPU把内存中从0开始的1K字节作为一个中断向量表。表中的每个表项占四个字节,由两个字节的段地址和两个字节的偏移量组成,这样构成的地址便是相应中断处理程序的入口地址。但是,在保护模式下,由四字节的表项构成的中断向量表显然满足不了要求。这是因为,除了两个字节的段描述符,偏移量必用四字节来表示;要有反映模式切换的信息。因此,在保护模式下,中断向量表中的表项由8个字节组成,如图5.2所示,中断向量表也改叫做中断描述符表IDT(Interrupt
43 | Descriptor Table)。其中的每个表项叫做一个**门描述符**(gate descriptor),“门”的含义是当中断发生时必须先通过这些门,然后才能进入相应的处理程序。
44 |
45 |
46 |

47 |
48 |
49 | 图5.2门描述符的一般格式
50 |
51 | 其中类型占3位,表示门描述符的类型,主要门描述符为:
52 |
53 | (1)中断门(Interrupt gate)
54 |
55 | 其类型码为110,中断门包含了一个中断或异常处理程序所在段的选择符和段内偏移量。当控制权通过中断门进入中断处理程序时,处理器清IF标志,即关中断,以避免嵌套中断的发生。中断门中的**请求特权级**(DPL)为0,因此,用户态的进程不能访问Intel的中断门。所有的中断处理程序都由中断门激活,并全部限制在内核态。
56 |
57 | (2)陷阱门(Trap gate)
58 |
59 | 其类型码为111,与中断门类似,其唯一的区别是,控制权通过陷阱门进入处理程序时维持IF标志位不变,也就是说,不关中断。
60 |
61 | (3)系统门(System gate)
62 |
63 | 这是Linux内核特别设置的,用来让用户态的进程访问Intel的陷阱门,因此,门描述符的DPL为3。系统调用就是通过系统门进入内核的。
64 |
65 | 最后,在保护模式下,中断描述符表在内存的位置不再限于从地址0开始的地方,而是可以放在内存的任何地方。为此,CPU中增设了一个中断描述符表寄存器IDTR,用来存放中断描述符表在内存的起始地址。中断描述符表寄存器IDTR是一个48位的寄存器,其低16位保存中断描述符表的大小,高32位保存中断描述符表的基址,如图5.3所示。
66 |
67 |
68 |

69 |
70 |
71 | 图5.3 中断描述符表寄存器IDTR
72 |
73 | ### 5.1.5 相关汇编指令
74 |
75 | 为了有助于读者对中断实现过程的理解,下面介绍几条相关的汇编指令:
76 |
77 | #### 1. 调用过程指令CALL
78 |
79 | 指令格式: CALL 过程名
80 |
81 | 说明:在取出CALL指令之后及执行CALL指令之前,使指令指针寄存器EIP指向紧接CALL指令的下一条指令。CALL指令先将EIP值压入栈内,再进行控制转移。当遇到RET指令时,栈内信息可使控制权直接回到CALL的下一条指令
82 |
83 | #### 2. 调用中断过程的指令INT
84 |
85 | 指令格式:INT 中断向量
86 |
87 | 说明:EFLAG、CS及EIP寄存器被压入栈内。控制权被转移到由中断向量指定的中断处理程序。在中断处理程序结束时,IRET指令又把控制权送回到刚才执行被中断的地方。
88 |
89 | #### 3. 中断返回指令IRET
90 |
91 | 指令格式:IRET
92 |
93 | 说明:IRET与中断调用过程相反:它将EIP、CS及EFLAGS寄存器内容从栈中弹出,并将控制权返回到发生中断的地方。IRET用在中断处理程序的结束处。
94 |
95 | #### 4. 加载中断描述符表的指令LIDT
96 |
97 | 格式:LIDT 48位的伪描述符
98 |
99 | 说明:LIDT将指令中给定的48位伪描述符装入中断描述符寄存器IDTR。
100 |
--------------------------------------------------------------------------------
/第一章/section1_5.md:
--------------------------------------------------------------------------------
1 | ## **1.5 Linux内核模块编程入门**
2 |
3 | 内核模块是Linux内核向外部提供的一个插口,其全称为动态可加载内核模块(Loadable Kernel Module,LKM),我们简称为模块。Linux内核之所以提供模块机制,是因为它本身是一个单内核(monolithic kernel)。单内核的最大优点是效率高,因为所有的内容都集成在一起,但其缺点是可扩展性和可维护性相对较差,模块机制就是为了弥补这一缺陷。
4 |
5 | ### **1.5.1 什么是模块**
6 |
7 | 模块是具有独立功能的程序,它可以被单独编译,但不能独立运行。它在运行时被链接到内核作为内核的一部分在内核空间运行,这与运行在用户空间的进程是不同的。模块通常由一组函数和数据结构组成,用来实现一种文件系统、一个驱动程序或其他内核上层的功能。
8 |
9 | ### **1.5.2 编写一个简单的模块**
10 |
11 | 模块和内核都在内核空间运行,模块编程在一定意义上说就是内核编程。因为内核版本的每次变化,其中的某些函数名也会相应地发生变化,因此模块编程与内核版本密切相关。我们在本书中所涉及的内核编程,基于的内核为2.6.x,对于其他版本,还需要做一些适当调整。
12 |
13 | **1.程序举例**
14 |
15 | #include
16 | #include
17 | #include
18 |
19 | static int __init lkp_init(void)
20 | {
21 | printk("<1>Hello, World! from the kernel space...\n");
22 | return 0;
23 | }
24 |
25 | static void __exit lkp_exit(void)
26 | {
27 | printk("<1>Goodbye, World! leaving kernel space...\n");
28 | }
29 |
30 | module_init(lkp_init);
31 | module_exit(lkp_exit);
32 | MODULE_LICENSE("GPL");
33 |
34 | **2.说明**
35 |
36 | (1)module.h头文件中包含了对模块的结构定义以及模块的版本控制,任何模块程序的编写都要包含这个头文件; 头文件kernel.h包含了常用的内核函数;而头文件init.h包含了宏\_init和\_exit,宏_init告诉编译程序相关的函数和变量仅用于初始化,编译程序将标有 _init的所有代码存储到特殊的内存段中,初始化结束后就释放这段内存。
37 |
38 | (2)函数lkp\_init ()是模块的初始化函数,函数lkp\_cleanup ()是模块的退出和清理函数。
39 |
40 | (3)我们在这里使用了printk()函数,该函数是由内核定义的,功能与C库中的printf()类似,它把要打印的信息输出到终端或系统日志。字符串中的<1>是输出的级别,表示立即在终端输出。
41 |
42 | (4)函数module\_init()和cleanup\_exit()是模块编程中最基本也是必须的两个函数。module\_init()向内核注册模块所提供的新功能,而cleanup_exit()注销由模块提供的所有功能。
43 |
44 | (5)最后一句告诉内核该模块具有GNU公共许可证。
45 |
46 | **3.编译模块**
47 |
48 | 假定我们给前面的程序起名为“hellomod.c”,只有超级用户才能加载和卸载模块。对于2.6内核的模块,其Makefile文件的基本内容如下:
49 |
50 | #Makefile3.10
51 | obj-m := hellomod.o #产生hellomod 模块的目标文件
52 | CURRENT_PATH := $(shell pwd) #模块所在的当前路径
53 | LINUX_KERNEL := $(shell uname -r) #Linux内核源代码的当前版本
54 | LINUX_KERNEL_PATH := /usr/src/kernels/$(LINUX_KERNEL) #Linux内核源代码的绝对路径
55 | all:
56 | make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules #编译模块
57 | clean:
58 | make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean #清理
59 |
60 | 上面的Makefile中使用了 obj-m := 这个赋值语句,其含义说明要使用目标文件hellomod.o建立一个模块,最后生成的模块名是helloworld.ko,如果你有一个名为module.ko的模块依赖于两个文件 file1.o和file2.o,那么我们可以使用module-obj扩展,如下所示
61 |
62 | obj-m := module.o
63 | module-objs := file1.o file2.o
64 |
65 | 关于Makefile的具体编写方法,请参考相关书籍。
66 |
67 | 最后,用make命令运行Makefile
68 |
69 | **4.运行代码**
70 | 当编译好模块,我们就可以将新的模块插入到内核中,这可以用insmod命令来实现,如下所示:
71 |
72 | insmod hellomod.ko
73 |
74 | 然后,可以用lsmod命令检查模块是否正确插入到内核中了:
75 |
76 | 模块的输出由printk()来产生。该函数默认打印系统文件/var/log/messages的内容。快速浏览这些消息可输入如下命令:
77 |
78 | tail /var/log/messages
79 |
80 | 这一命令打印日志文件的最后10行内容,可以看到我们的初始化信息:.
81 |
82 | ...
83 | Mar 6 10:35:55 lkp1 kernel: Hello,World! from the kernel space...
84 |
85 | 使用rmmod命令,加上我们在insmod中看到的模块名,可以从内核中移除该模块(还可以看到退出时显示的信息)。如下所示:
86 |
87 | rmmod hellomod
88 |
89 | 同样,输出的内容也在日志文件中,如下所示:
90 |
91 | ...
92 | Mar 6 12:00:05 lkp1 kernel: Hello,World! from the kernel space...
93 |
94 | ## **1.5.3 应用程序与内核模块的比较**
95 |
96 | 模块编程属于内核编程,因此,除了对内核相关知识有所了解外,还需要了解与模块相关的知识。
97 |
98 | 为了加深对内核模块的了解,表1.3给出应用程序与内核模块程序的比较。
99 |
100 | **表1.3 应用程序与内核模块程序的比较**
101 |
102 |
103 | | |**C语言应用程序**|**内核模块程序**|
104 | |---- |----- |----|
105 | |使用函数|Libc库|内核函数|
106 | |运行空间|用户空间|内核空间|
107 | |运行权限|普通用户|超级用户|
108 | |入口函数|main()|module_init|
109 | |出口函数|exit()|module_cleanup()|
110 | |编译|gcc-c|mark|
111 | |连接|gcc|insmod|
112 | |运行|直接运行|insmod|
113 | |调试|gdb|kdbug,kdb,kgdb等|
114 |
115 |
116 | 从表1.3我们可以看出,内核模块程序不能调用libc库中的函数,它运行在内核空间,且只有超级用户可以对其运行。另外,模块程序必须通过module()\_init和module()\_cleanup函数来告诉内核“我来了”和“我走了”。
117 |
118 | 在了解内核模块简单编程之后,下面通过对内核中常用数据结构链表的分析,使大家初步了解Linux内核的具体源代码,并编写内核模块对其加以应用。
119 |
--------------------------------------------------------------------------------
/第三章/section3_5.md:
--------------------------------------------------------------------------------
1 | ## **3.5 进程的创建**
2 |
3 | 进程创建是Unix类操作系统中发生最频繁的活动之一。例如,只要用户输入一条命令,shell进程就创建一个新进程,新进程执行shell的另一个拷贝。
4 |
5 | 很多操作系统都提供了产生进程的机制,其采取的方式是首先在新的地址空间里创建进程,然后读可执行文件,最后开始执行。Unix采用了与众不同的实现方式,它把上述步骤分为创建和执行两步,也就是fork()和exec()两个函数。首先,fork()通过拷贝当前进程创建一个子进程。然后,exec()函数负责读取可执行文件并将其载入进程的地址空间开始运行。把这两个函数组合起来使用的效果跟其他系统使用的单一函数的效果类似。
6 |
7 | 传统的fork()系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单并且效率低下。Linux的fork()使用写时复制(Copy-on-write)来实现。也就是在调用fork()时内核并没有把父进程的全部资源给子进程复制一份,而是将这些内容设置为只读状态,当父进程或子进程试图修改某些内容时,内核才在修改之前将被修改的部分进行拷贝。因此,fork()的实际开销就是复制父进程的页表以及给子进程创建惟一的PCB。
8 |
9 | ### **3.5.1 创建进程**
10 |
11 | 新进程是通过克隆父进程(当前进程)而建立的。fork() 和 clone()(用于线程)系统调用可用来建立新的进程。当这两个系统调用结束时,内核在内存中为新的进程分配新的PCB,同时为新进程要使用的堆栈分配物理页。Linux 还会为新进程分配新的进程标识符。然后,新的PCB地址保存在链表中,而父进程的PCB内容被复制到新进程的 PCB中。该部分也是Linux 2.4的内核代码来说明。
12 |
13 | 在克隆进程时,Linux 允许父子进程共享相同的资源。可共享的资源包括文件、信号处理程序和进程地址空间等。当某个资源被共享时,该资源的引用计数值会增加 1,从而只有在两个进程均终止时,内核才会释放这些资源。
14 |
15 | 不管是fork() 还是 clone()系统调用,最终都调用了内核中的do_fork()函数,该函数的主要操作为:
16 |
17 | 1. 调用alloc_task_struct( )函数以获得8KB的union task_union内存区,用来存放进程的PCB和新进程的内核栈。
18 |
19 | 2. 让当前指针指向父进程的PCB,并把父进程PCB的内容拷贝到刚刚分配的新进程的PCB中,此时,子进程和父进程的PCB是完全相同的。
20 |
21 | 3. 检查新创建这个子进程后,当前用户所拥有的进程数目没有超出给他分配的资源的限制。
22 |
23 | 4. 现在, do_fork( )已经获得它从父进程能利用的几乎所有的东西;剩下的事情就是集中建立子进程的新资源,并让内核知道这个新进程已经诞生。
24 |
25 | 5. 接下来,子进程的状态被设置为TASK_UNINTERRUPTIBLE以保证它不会马上投入运行。
26 |
27 | 6. 调用get_pid()为新进程获取一个有效的PID。
28 |
29 | 7. 然后,更新不能从父进程继承的PCB的其他所有域,例如,进程间亲属关系的域。
30 |
31 | 8. 根据传递给clone()的参数标志,拷贝或共享打开的文件、文件系统信息、信号处理函数、进程的虚拟地址空间(参见下一章)等。如果进程包含有线程,则其所有线程共享这些资源,无需拷贝;否则,这些资源对每个进程是不同的,因此被拷贝。
32 |
33 | 9. 把新的PCB插入进程链表,以确保进程之间的亲属关系。
34 |
35 | 10. 把新的PCB插入pidhash哈希表。
36 |
37 | 11. 把子进程PCB的状态域设置成TASK_RUNNING,并调用wake_up_process( )把子进程插入到运行队列链表。
38 |
39 | 12. 让父进程和子进程平分剩余的时间片。
40 |
41 | 13. 返回子进程的PID,这个PID最终由用户态下的父进程读取
42 |
43 | 现在有了处于可运行状态的完整子进程,但是,它还没有实际运行,由调度程序来决定何时把CPU交给这个子进程。在fork()或clone()系统调用结束时,新创建的子进程将开始执行。内核有意选择子进程首先执行,这是因为一般子进程都会马上调用exec()函数,这样可以避免写时复制的额外开销,如果父进程首先执行的话,有可能会开始向地址空间写入。
44 |
45 | 子进程创建结束后,就该从内核态返回用户态了。用户态进程根据fork()的返回值分别安排父子进程执行不同的代码。
46 |
47 | ### **3.5.2线程及其创建**
48 |
49 | 线程是现代编程技术中常用的一种机制。该机制提供了在同一程序内可以运行多个线程,这些线程共享内存地址空间,除此之外还可以共享打开的文件和其他资源。
50 |
51 | Linux实现线程的机制非常独特。从内核的角度来说,它并没有线程这个概念。Linux把所有的线程都当作进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程。相反,线程仅仅被视为一个使用某些共享资源的进程。每个线程都拥有唯一隶属于自己的task_struct,所以在内核中,它看起来就像是一个普通的进程,只是该进程和其它一些进程共享某些资源,如地址空间。
52 |
53 | Linux的内核线程是由kernel_thread( )函数在内核态下创建的,这个函数在内核中的实现是C语言中嵌套着汇编语言,但在某种程度上等价于下面的代码:
54 |
55 | int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
56 | {
57 | pid_t p;
58 | p = clone( 0, flags | CLONE_VM );
59 | if ( p ) /* 父*/
60 | return p;
61 | else { /* 子*/
62 | fn(arg);
63 | exit( );
64 | }
65 | }
66 |
67 | clone()有很多标志,其中CLONE_VM表示父子进程共享地址空间。在kernel_thread()返回时,父线程退出,并返回一个指向子线程的PID。子线程开始运行fn指向的函数,arg是运行时需要用到的参数。
68 |
69 | 一般情况下,内核线程会把在创建时得到的函数永远执行下去(除非系统重起)。该函数通常由一个循环构成,在需要的时候,这个内核线程就会被唤醒和执行,完成了当前任务,它会自行睡眠。
70 |
71 | 内核线程也可以叫内核任务,它们周期性地执行,例如,磁盘高速缓存的刷新,网络连接的维护,页面的换入换出等等。在Linux中,内核线程与普通进程有一些本质的区别,从以下几个方面可以看出二者之间的差异:
72 |
73 | (1) 内核线程执行的是内核中的函数,而普通进程只有通过系统调用才能执行内核中的函数。
74 |
75 | (2) 内核线程只运行在内核态,而普通进程既可以运行在用户态,也可以运行在内核态。
76 |
77 | (3) 因为内核线程只运行在内核态,因此,它只能使用大于PAGE_OFFSET(3G)的地址空间。另一方面,不管在用户态还是内核态,普通进程可以使用4GB的地址空间(参见第四章)。
78 |
79 | 下面描述几个特殊的内核线程
80 |
81 | **1.进程0**
82 |
83 | 内核是一个大程序,它可以控制硬件,并创建、运行、终止及控制所有进程。内核被加载到内存后,首先由完成内核初始化工作的start_kernel函数从无到有的创建一个内核线程swap,并设置其PID为0。因为Linux对进程和线程统一编号,我们也把它叫进程0,又叫闲逛进程(idle process)。进程0执行的是cpu_idle()函数,该函数中只有一条hlt汇编指令,hlt指令在系统闲置时不仅能降低电力的使用还能减少热的产生。如前所述,进程0的PCB叫做init_task,在很多链表中起链表头的作用。当就绪队列没有其他进程时,闲逛进程0就被调度程序选中,以此达到省电的目的。
84 |
85 | **2.进程1**
86 |
87 | 如前所述,init进程是1号进程,实际上,Linux2.6在初始化阶段首先把它建立为一个内核线程kernel_init:
88 | kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
89 |
90 | 参数CLONE_FS | CLONE_FILES | CLONE_SIGHAND表示0号线程和1号线程分别共享文件系统(CLONE_FS)、打开的文件(CLONE_FILES)和信号处理程序(CLONE_SIGHAND)。当调度程序选择到kernel_init内核线程时,kernel_init就开始执行内核的一些初始化函数将系统初始化。
91 |
92 | 那么,kernel_init()内核线程是怎样变为用户进程的呢?实际上,kernel_init()内核函数中调用了execve()系统调用,该系统调用装入用户态下的可执行程序init(/sbin/init)。注意,内核函数kernel_init()和用户态下的可执行文件init是不同的代码,处于不同的位置,也运行在不同的状态,因此,init是内核线程启动起来的一个普通的进程,这也是用户态下的第一个进程。init进程从不终止,因为它创建和监控操作系统外层所有进程的活动。
93 |
94 |
--------------------------------------------------------------------------------
/第四章/section4_3.md:
--------------------------------------------------------------------------------
1 | ## 4.3 请页机制
2 |
3 | 当一个进程运行时,CPU访问的地址是用户空间的虚地址。Linux采用请页机制来节约物理内存,也就是说,它仅仅把当前要使用的用户空间中的少量页装入物理内存。当访问的虚存页面尚未装入物理内存时,处理器将向Linux 报告一个页故障及其对应的故障原因。 页故障的产生有三种原因:
4 |
5 | 1. 程序出现错误,例如,要访问的虚地址在PAGE_OFFSET(3GB)之外,则该地址无效,Linux 将向进程发送一个信号并终止进程的运行;
6 |
7 | 2. 虚地址有效,但其所对应的页当前不在物理内存中,即缺页异常[^1],这时,操作系统必须从磁盘或交换文件(此页被换出)中将其装入物理内存。这是本节要讨论的主要内容。
8 |
9 | 3. 要访问的虚地址被写保护,即保护错误,这时,操作系统必须判断:如果是某个用户进程正在写当前进程的地址空间,则发送一个信号并终止进程的运行。但是,如果错误发生在一旧的共享页上时,则处理方法有所不同,也就是要对这一共享页进行复制,这就是曾经描述过的“写时复制”技术。
10 |
11 | ### 4.3.1 缺页异常处理程序
12 |
13 | 当一个进程执行时,如果CPU访问到一个有效的虚地址,但是这个地址对应的页没有在内存,则CPU产生一个缺页异常,同时将这个虚地址存入CR2寄存器(参见第二章),然后调用缺页异常处理程序do_page_fault()。Linux的缺页异常处理程序必须对产生缺页的原因进行区分:是由编程错误所引起的异常,还是由访问进程用户空间的页但还尚未分配物理页面所引起的异常。
14 |
15 | 下面我们首先给出缺页异常处理程序的总体方案如图4.8所示,随后给出其详细流程图,其中的“地址”指当前进程执行时引起缺页的虚地址,“虚存区”指该地址所处的虚存区。SIGSEGV是当一个进程执行了一个无效的内存引用,或发生段错误时发送给它的信号。
16 |
17 |
18 |

19 |
20 |
21 | 图4.8 缺页异常处理程序的总体方案
22 |
23 | 实际上,缺页异常处理程序必须处理多种更细的特殊情况,它们不宜在总体方案中列出,详细流程图如图4.9。
24 |
25 |

26 |
27 |
28 | 图4.9 缺页异常处理程序流程图
29 |
30 | do_page_fault()函数的首先是读取引起缺页的虚地址。如果没找到,则说明访问了非法虚地址,Linux会发信号终止进程。否则,检查缺页类型,如果是非法类型(越界错误,段权限错误等)同样会发信号终止进程。
31 |
32 | 缺页异常肯定要发生在内核态,如果发生在用户态,则必定是错误的,于是把相关信息保存在进程的PCB中。
33 |
34 | 对有效的虚地址,如果是缺页异常,Linux必须区分页所在的位置,即判断页是在交换文件中,还是在可执行映像中。为此,Linux通过页表项中的信息区分页所在的位置。如果该页的页表项非空,但对应的页不在内存,则说明该页处于交换文件中,操作系统要从交换文件装入页。
35 |
36 | 如果错误由写访问引起,该函数检查这个虚存区是否可写。如果不可写,则对这种错误进行相应的处理;如果可写,则采用“写时复制”技术。
37 |
38 | 如果错误由读或执行访问引起,该函数检查这一页是否已经存在于物理内存中。如果在,错误的发生就是由于进程试图访问用户态下的一个有特权的页面(页面的User/Supervisor标志被清除),因此函数跳到相应的错误处理代码处(实际上这种情况从不发生,因为内核根本不会给用户进程分配有特权的页面)。如果不在物理内存,函数还将检查这个虚存区是否可读或可执行。
39 |
40 | 如果这个虚存区的访问权限与引起缺页异常的访问类型相匹配,则调用handle_mm_fault()函数,该函数确定如何给进程分配一个新的物理页面:
41 |
42 | 1. 如果被访问的页不在内存,也就是说,这个页还没有被存放在任何一个物理页面中,那么,内核分配一个新的页面并适当地初始化;这种技术称为“**请求调页”**。
43 |
44 | 2. 如果被访问的页在内存但是被标为只读,也就是说,它已经被存放在一个页面中,那么,内核分配一个新的页面,并把旧页面的数据拷贝到新页面来初始化它;这种技术称为“**写时复制”**。
45 |
46 | ### 4.3.2 请求调页
47 |
48 | 术语“请求调页”指的是一种动态内存分配技术,它把页面的分配推迟到不能再推迟为止,也就是说,一直推迟到进程要访问的页不在物理内存时为止,由此引起一个缺页异常。
49 |
50 | 请求调页技术的引入主要是因为进程开始运行时并不访问其地址空间中的全部地址;事实上,有一部分地址也许进程永远不使用。此外,程序的局部性原理保证了在程序执行的每个阶段,真正使用的进程页只有一小部分,因此临时用不着的页根本没必要调入内存。相对于全局分配(一开始就给进程分配所需要的全部页面,直到程序结束才释放这些页面)来说,请求调页是首选的,因为它增加了系统中的空闲页面的平均数,从而更好地利用空闲内存。从另一个观点来看,在内存总数保持不变的情况下,请求调页从总体上能使系统有更大的吞吐量。
51 |
52 | 但是,系统为此也要付出额外的开销,这是因为由请求调页所引发的每个“缺页”异常必须由内核处理,这将浪费CPU的周期。幸运的是,局部性原理保证了一旦进程开始在一组页上运行,在接下来相当长的一段时间内它会一直停留在这些页上而不去访问其它的页:这样我们就可以认为“缺页”异常是一种稀有事件。
53 |
54 | 基于以下两种原因,被寻址的页可以不在主存中:
55 |
56 | 1. 进程永远也没有访问到这个页。内核能够识别这种情况,这是因为页表相应的表项被填充为0。宏pte_none(pte是PageTable Entry的缩写)用来判断这种情况,如果页表项为空,则返回1,否则返回0。
57 |
58 | 2. 进程已经访问过这个页,但是这个页的内容被临时保存在磁盘上。内核也能够识别这种情况,这是因为页表相应表项没有被填充为0(然而,由于页面不存在物理内存中,存在位P为0)。
59 |
60 | 在其它情况下,当页从未被访问时则调用do_no_page()函数。有两种方法装入所缺的页,这取决于这个页是否与磁盘文件建立了映射关系。该函数通过检查虚存区描述符的nopage域来确定这一点,如果页与文件建立起了映射关系,则nopage域就指向一个函数,该函数把所缺的页从磁盘装入到内存。因此,可能有两种情况:
61 |
62 | 1. nopage域不为NULL。在这种情况下,说明某个虚存区映射了一个磁盘文件,nopage域指向从磁盘进行读入的函数。这种情况涉及到磁盘文件的低层操作,暂不讨论。
63 |
64 | 2. nopage域为NULL。在这种情况下,虚存区没有映射磁盘文件,也就是说,它是一个匿名映射。因此,do_no_page()调用do_anonymous_page()函数获得一个新的页面。
65 |
66 | do_anonymous_page()函数分别处理写请求和读请求:
67 |
68 | 当处理写访问时,该函数调用__get_free_page()分配一个新的页面,并把新页面填为0。最后,把页表相应的表项置为新页面的物理地址,并把这个页面标记为可写和脏两个标志。
69 |
70 | 相反,当处理读访问时(所访问的虚存区可能是未初始化的数据段bss),因为进程正在对它进行第一次访问,因此页的内容是无关紧要的。给进程一个填充为0的页面要比给它一个由其它进程填充了信息的旧页面更为安全。Linux在请求调页方面做得更深入一些。没有必要立即给进程分配一个填充为零的新页面,我们可以给它一个现有的称为“零页”的页,这样可以进一步推迟页面的分配。“零页”在内核初始化期间被静态分配,并存放在empty_zero_page变量中(一个有1024个长整数的数组,并用0填充);因此页表项被设为零页的物理地址:
71 |
72 | 由于“零页”被标记为不可写,如果进程试图写这个页,则写时复制机制被激活。当且仅当在这个时候,进程才获得一个属于自己的页面并对它进行写。
73 |
74 | ### 4.3.3 写时复制
75 |
76 | 第一代Unix系统实现了一种傻瓜式的进程创建:当发出fork()系统调用时,内核原样复制父进程的整个用户地址空间并把复制的那一份分配给子进程。这种行为是非常耗时的,因为它需要:
77 |
78 | 1. 为子进程的页表分配页面
79 |
80 | 2. 为子进程的页分配页面
81 |
82 | 3. 初始化子进程的页表
83 |
84 | 4. 把父进程的页复制到子进程相应的页中
85 |
86 | 这种创建地址空间的方法涉及许多内存访问,消耗许多CPU周期,并且完全破坏了高速缓存中的内容。在大多数情况下,这样做常常是毫无意义的,因为许多子进程通过装入一个新的程序开始它们的运行,这样就完全丢弃了所继承的地址空间。
87 |
88 | 写时复制(Copy-on-write)是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。有时共享页根本不会被写入,例如,fork()后立即调用exec(),就无需复制父进程的页了。fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的PCB。这种优化可以避免拷贝大量根本就不会使用的数据(地址空间里常常包含数十兆的数据)。
89 |
--------------------------------------------------------------------------------
/第二章/section2_3.md:
--------------------------------------------------------------------------------
1 | ## **2.3 分页机制**
2 |
3 | 分页机制在段机制之后进行,以完成线性——物理地址的转换过程。段机制把虚拟地址转换为线性地址,分页机制进一步把该线性地址再转换为物理地址。
4 |
5 | 如果不允许分页(CR0的最高位置0),那么经过段机制转化而来的32位线性地址就是物理地址。但如果允许分页(CR0的最高位置1),就要将32位线性地址通过一个地址变换机构转化成物理地址。80X86规定,分页机制是可选的,但很多操作系统主要采用分页机制。
6 |
7 | ### **2.3.1 页与页表**
8 |
9 | **1.页、物理页面及页大小**
10 |
11 | 为了效率起见,将线性地址空间划分成若干大小相等的片,称为页(Page),并给各页加以编号,从0开始,如第0页、第1页等。相应地,也把物理地址空间分成与页大小相等的若干存储块,称为(物理)块或页面(Page Frame),也同样为它们加以编号,如0#页面、1#页面等。如图 2.10所示,图中用箭头把线性地址空间中的页,与对应的物理地址空间中的页面联系起来,表示把线性地址空间中若干页将分别装入到多个可以不连续的物理页面中。例如第0页将装入到第2页面,第1页将装入到第0页面,但是第2页也将装入到第2个页面,这似乎是一种错误,但学过内存管理一章后会理解这是一种正常现象。本节只涉及分页机制的一般原理,更多的内容将在第四章内存管理一章讲述。
12 |
13 |
14 |

15 |
16 |
17 |
18 |
19 |
20 | 那么,页的大小应该为多少?页过大或过小都会影响内存的使用率。其大小在设计硬件分页机制时就必须确定下来,例如80X86支持的标准页大小为4KB(也支持4MB),从后面的内容可以看出,选择4KB大小既巧妙又高效。
21 |
22 | **2.页表**
23 |
24 |
25 | 页表是把线性地址映射到物理地址的一种数据结构。参照段描述符表,页表中应当包含如下内容:
26 |
27 | (1)物理页面基地址:线性地址空间中的一个页装入内存后所对应的物理页面的起始地址。
28 |
29 | (2)页的属性:表示页的特性。例如该页是否在内存,是否可被读出或写入等。
30 |
31 | 由于页面的大小为4KB,它的物理页面基地址(32位)必定是4K的倍数,因此其地址的最低12位总是0,那么就可以用这12位存放页的属性,这样用32位完全可以描述页的映射关系,也就是页表中每一项(简称页表项)占4个字节就足够。
32 |
33 | 不过,4 GB的线性空间可以被划分为1M个4K大小的页,每个页表项占4个字节,则1M个页表项的页表就需要占用4 MB空间,而且还要求是连续的,显然这是不现实的。我们可以采用两级页表来解决这个问题。
34 |
35 | **3.两级页表**
36 |
37 | 所谓两级页表就是对页表再进行分页。第一级称为页目录,其中存放的是关于页表的信息。4MB的页表再次分页(4MB/4K)可以分为1K个页,同样对每个页的描述需要4个字节,于是可以算出页目录最多占用4KB个字节,正好是一个页,其示意图如2.11所示。
38 |
39 |
40 |

41 |
42 |
43 |
44 | 页目录共有1K个表项, 于是,线性地址的最高10位(即22位~ 31位)用来产生第一级的索引。两级表结构的第二级称为页表,每个页表也刚好存放在一个4K字节的页中,包含1K个字节的表项。第二级页表由线性地址的中间10位(即21位~ 12位)进行索引,最低12位表示页内偏量。具有两级页表的线性地址结构如图2.12所示。
45 |
46 |
47 |

48 |
49 |
50 |
51 | **4.页表项结构**
52 |
53 |
54 | 不管是页目录还是页表,每个表项占四个字节,其表项结构基本相同,如图2.13所示:
55 |
56 |
57 |

58 |
59 |
60 |
61 | 物理页面基地址: 对页目录而言,指的是页表所在的物理页面在内存的起始地址,对页表而言,指的是页所对应的物理页面在内存的起始物理地址。因为其最低12位全部为0,因此用高20位来描述32位的地址。
62 | 属性包括:
63 |
64 | (1)第0位是P(Present),如果P=1,表示页装入到内存中,如果P=0,表示不在内存中。
65 |
66 | (2)第1位是R/W(Read/Write),第2位是U/S(User/Supervisor)位,这两位为页表或页提供硬件保护。
67 |
68 | (3)第3位是PWT(Page Write-Through)位,表示是否采用写透方式,写透方式就是既写内存(RAM)也写高速缓存,该位为1表示采用写透方式
69 |
70 | (4)第4位是PCD(Page Cache Disable)位,表示是否启用高速缓存,该位为1表示启用高速缓存。
71 |
72 | (5)第5位是访问位,当对相应的物理页面进行访问时,该位置1。
73 |
74 | (6)第7位是Page Size标志,只适用于页目录项。如果置为1,页目录项指的是4MB的页
75 |
76 | (7)第9~11位由操作系统专用,Linux也没有做特殊之用。
77 |
78 | **5.硬件保护机制**
79 |
80 | 对于页表,页的保护是由U/S标志和R/W标志来控制的。当U/S标志为0时,只有处于内核态的操作系统才能对此页或页表进行寻址。当这个标志为1时,则不管在内核态还是用户态,总能对此页进行寻址。
81 |
82 | 此外,与段的三种存取权限(读、写、执行)不同,页的存取权限只有两种(读、写)。如果页目录项或页表项的读写标志为0,说明相应的页表或页是只读的,否则是可读写的。
83 |
84 | ### **2.3.2线性地址到物理地址的转换**
85 |
86 | 当访问线性地址空间的一个操作单元时,如何把32位的线性地址通过分页机制转化成32位物理地址呢?过程如图2.14所示。
87 |
88 |
89 |

90 |
91 |
92 |
93 | 第一步,用32位线性地址的最高10位第31~22位作为页目录项的索引,将它乘以4,与CR3中的页目录的起始地址相加,获得相应目录项在内存的地址。
94 |
95 | 第二步,从这个地址开始读取32位页目录项,取出其高20位,再给低12位补0,形成的32位就是页表在内存的起始地址。
96 |
97 | 第三步,用32位线性地址中的第21~12位作为页表中页表项的索引,将它乘以4,与页表的起始地址相加,获得相应页表项在内存的地址。
98 |
99 | 第四步,从这个地址开始读取32位页表项,取出其高20位,再将线性地址的第11~0位放在低12位,形成最终32位页面物理地址。
100 |
101 | ### **2.3.3 分页举例**
102 |
103 | 下面举一个简单的例子,这将有助于读者理解分页机制是怎样工作的。
104 |
105 | 假如操作系统给一个正在运行的进程分配的线性地址空间范围是0x20000000 到 0x2003ffff。这个空间由64页组成。我们暂且不关心这些页所在的物理页面的地址,只关注页表项中的几个域。
106 |
107 | 我们从分配给进程的线性地址的最高10位(分页硬件机制把它自动解释成页目录域)开始。这两个地址都以2开头,后面跟着0,因此高10位有相同的值,即十六进制的0x080或十进制的128。因此,这两个地址的页目录域都指向进程页目录的第129项。相应的目录项中必须包含分配给进程的页表的物理地址,如图2.15。如果给这个进程没有分配其它的线性地址,则页目录的其余1023项都为0,也就是这个进程在页目录中只占一项。
108 |
109 |
110 |

111 |
112 |
113 |
114 | 中间10位的值(即页表域的值)范围从0到0x03f,或十进制的从0到63。因而只有页表的前64个表项是有意义的,其余960表项填为0。
115 |
116 | 假设进程需要读线性地址0x20021406中的内容。这个地址由分页机制按下面的方法进行处理:
117 |
118 | 1.目录域的0x80用于选择页目录的第0x80目录项,此目录项指向页表。
119 |
120 | 2.页表域的第0x21项用于选择页表的第0x21表项,此表项指向页所对应的内存物理页面。
121 |
122 | 3.最后,偏移量0x406用于在目标物理页面中读偏移量为0x406中的字节。
123 |
124 | 如果页表第0x21表项的Present标志为0,说明此页还没有装入内存中;在这种情况下,分页机制在转换线性地址的同时产生一个缺页异常(参见内存管理一章)。无论何时,当进程试图访问限定在0x20000000到0x2003ffff范围之外的线性地址时,都将产生一个缺页异常,因为这些页表项都填充了0,尤其是,它们的Present标志都为0。
125 |
126 | ### **2.3.4 页面高速缓存**
127 |
128 | 由于在分页情况下,页表是放在内存中的,这使CPU在每次存取一个数据时,都要至少两次访问内存,从而大大降低了访问速度。所以,为了提高速度,在80X86中设置一个最近存取页的高速缓存硬件机制,它自动保持32项处理器最近使用的页表项,因此,可以覆盖128K字节的内存地址。当访问线性地址空间的某个地址时,先检查对应的页表项是否在高速缓存中,如果在,就不必经过两级访问了,如果不在,再进行两级访问。平均来说,页面高速缓存大约有90%的命中率,也就是说每次访问存储器时,只有10%的情况必须访问两级分页机构。这就大大加快了速度,页面高速缓存的作用如图2.16所示。有些书上也把页面高速缓存叫做“联想存储器”或“转换旁视缓冲器(TLB)”。
129 |
130 |
131 |

132 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/第四章/section4_1.md:
--------------------------------------------------------------------------------
1 | ## 4.1 Linux的内存管理概述
2 |
3 | Linux是为多用户多任务设计的操作系统, 所以存储资源要被多个进程有效共享;且由于程序规模的不断膨胀,要求的内存空间比从前大得多。Linux内存管理的设计充分利用了计算机系统所提供的虚拟存储技术,真正实现了虚拟存储器管理。
4 |
5 | 第二章介绍的80X86的段机制和页机制是操作系统实现虚拟存储管理的一种硬件平台。实际上,Linux不仅仅可以运行在Intel系列个人计算机上,还可以运行在Apple、DEC Alpha、MIPS和Motorola 68k等系列上,这些平台都支持虚拟存储器管理,而我们之所以选择80X86,是因为它更具代表性和普遍性。
6 |
7 | 关于内存管理,读者可能对一下问题比较困惑
8 |
9 | 1. 一个源程序编译链接后形成的地址空间是虚地址空间还是物理地址空间,如何管理?
10 | 2. 程序装入内存的过程中,虚地址如何被转换为物理地址?
11 |
12 | 本章将围绕这两大问题展开讨论,在讨论的过程中,会涉及到其他方面的技术问题。
13 |
14 | ### 4.1.1 虚拟内存、内核空间和用户空间
15 |
16 | 从第二章我们知道,Linux简化了分段机制,使得虚地址与线性地址总是一致的。线性空间在32位平台上为4GB的固定大小,也就是Linux的虚拟地址空间也这么大。Linux内核将这4G字节的空间分为两部分。最高的1G字节(从虚地址0xC0000000到0xFFFFFFFF)供内核使用,称为“**内核空间**”。而较低的3G字节(从虚地址0x00000000到0xBFFFFFFF),供各个进程使用,称为“**用户空间**”。因为每个进程可以通过系统调用进入内核,因此,Linux内核空间由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的**虚拟地址空间**(也叫**虚拟内存**)。图4.1 给出了进程虚拟地址空间示意图。
17 |
18 |
19 |

20 |
21 |
22 | 图4.1 进程虚拟地址空间
23 |
24 | 从图中可以看出,每个进程有各自的私有用户空间(0~3G),这个空间对系统中的其他进程是不可见的。最高的1GB内核空间则为所有进程以及内核所共享。另外,进程的“**用户空间**”也叫“**地址空间**”,在后面的叙述中,我们对这两个术语不再区分。
25 |
26 | 图4.1也说明,用户空间不是进程共享的,而是进程隔离的。每个进程最大都可以有3GB的用户空间。一个进程对其中一个地址的访问,与其它进程对于同一地址的访问绝不冲突。比如,一个进程从其用户空间的地址0x1234ABCD处可以读出整数8,而另外一个进程从其用户空间的地址0x1234ABCD处可以读出整数20,这取决于进程自身的逻辑。
27 |
28 | 任意一个时刻,在一个CPU上只有一个进程在运行。所以对于此CPU来讲,在这一时刻,整个系统只存在一个4GB的虚拟地址空间,这个虚拟地址空间是面向此进程的。当进程发生切换的时候,虚拟地址空间也随着切换。由此可以看出,每个进程都有自己的虚拟地址空间,只有此进程运行的时候,其虚拟地址空间才被运行它的CPU所知。在其它时刻,其虚拟地址空间对于CPU来说,是不可知的。所以尽管每个进程都可以有4GB的虚拟地址空间,但在CPU眼中,只有一个虚拟地址空间存在。虚拟地址空间的变化,随着进程切换而变化。
29 |
30 | 从第二章我们知道,一个程序编译连接后形成的地址空间是一个虚拟地址空间,但是程序最终还是要运行在物理内存中。因此,应用程序所给出的任何虚地址最终必须被转化为物理地址,所以,虚拟地址空间必须被映射到物理内存空间中,这个映射关系需要通过硬件体系结构所规定的数据结构来建立。这就是我们第二章所描述的段描述符表和页表,Linux主要通过页表来进行映射。
31 |
32 | 于是,我们得出一个结论,如果给出的页表不同,那么CPU将某一虚拟地址空间中的地址转化成的物理地址就会不同。所以我们为每一个进程都建立其页表,将每个进程的虚拟地址空间根据自己的需要映射到物理地址空间上。既然某一时刻在某一CPU上只能有一个进程在运行,那么当进程发生切换的时候,将页表也更换为相应进程的页表,这就可以实现每个进程都有自己的虚拟地址空间而互不影响。所以,在任意时刻,对于一个CPU来说,只需要有当前进程的页表,就可以实现其虚拟地址到物理地址的转化。
33 |
34 | #### 1. 内核空间到物理内存的映射
35 |
36 | 内核空间对所有的进程都是共享的,其中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据,不管是内核程序还是用户程序,它们被编译和连接以后,所形成的指令和符号地址都是**虚地址**(参见2.5节中的例子),而不是物理内存中的物理地址。
37 |
38 | 虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000)开始的,如图4.2所示,之所以这么规定,是为了在内核空间与物理内存之间建立简单的线性映射关系。其中,3GB(0xC0000000)就是物理地址与虚拟地址之间的位移量,在Linux代码中就叫做PAGE_OFFSET。
39 |
40 |
41 |

42 |
43 |
44 | 图4.2内核的虚拟地址空间到物理地址空间的映射
45 |
46 | 我们来看一下在page.h头文件中对内核空间中地址映射的说明及定义:
47 |
48 | ```c
49 | #ifdef CONFIG_64BIT
50 | #define __PAGE_OFFSET (0x40000000) /* 1GB */
51 | #else
52 | #define __PAGE_OFFSET (0x10000000) /* 256MB */
53 | #endif
54 | #define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET)
55 | /* The size of the gateway page (we leave lots of room for expansion) */
56 | #define GATEWAY_PAGE_SIZE 0x4000
57 | /* The start of the actual kernel binary---used in vmlinux.lds.S
58 | * Leave some space after __PAGE_OFFSET for detecting kernel null
59 | * ptr derefs */
60 | #define KERNEL_BINARY_TEXT_START (__PAGE_OFFSET + 0x100000)
61 | /* These macros don't work for 64-bit C code -- don't allow in C at all */
62 | #ifdef __ASSEMBLY__
63 | # define PA(x) ((x)-__PAGE_OFFSET)
64 | # define VA(x) ((x)+__PAGE_OFFSET)
65 | #endif
66 | #define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
67 | #define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
68 | ```
69 |
70 | 对于内核空间而言,给定一个虚地址x,其物理地址为“x-PAGE_OFFSET”,给定一个物理地址x,其虚地址为“x+ PAGE_OFFSET”。
71 |
72 | 例如,进程的页目录PGD(Page Global Directory)就处于内核空间中。在进程切换时,要将寄存器CR3设置成指向新进程的页目录PGD,而该目录的起始地址在内核空间中是虚地址,但CR3所需要的是物理地址,这时候就要用__pa()进行地址转换:
73 |
74 | ```c
75 | asm volatile(“movl %0,%%cr3”: :”r” (__pa(next->pgd));
76 | ```
77 |
78 | 这是一行嵌入式汇编代码,其含义是将下一个进程的页目录起始地址next_pgd,通过__pa()转换成物理地址,存放在某个寄存器中,然后用movl指令将其写入CR3寄存器中。经过这行语句的处理,CR3就指向新进程next的页目录PGD
79 |
80 | 这里再次说明,宏__pa()仅仅把一个内核空间的虚地址映射到物理地址,而决不适用于用户空间,用户空间的地址映射要复杂得多,它通过分页机制完成。
81 |
82 | #### 2.内核映像
83 |
84 | 在下面的描述中,我们把内核的代码和数据就叫内核映像(kernelimage)。当系统启动时,Linux内核映像被装入在物理地址0x00100000开始的地方,即1MB开始的区间,这第1M用来存放一些与系统硬件相关的代码和数据,如图4.3所示,内核只占用从0x100000开始到start_mem结束的一段区域。从start_mem到end_mem这段区域叫动态内存,是用户程序和数据使用的内存区。
85 |
86 | 0 0x100000 start_mem end_mem
87 |
88 | 图4.3 系统启动后的物理内存布局
89 |
90 | 然而,在正常运行时,整个内核映像应该在虚拟内存的内核空间中,因为连接程序在连接内核映像时,在所有的符号地址上加一个偏移量PAGE_OFFSET,这样,内核映像在内核空间的起始地址就为0xC0100000。
91 |
92 | ### 4.1.2 虚拟内存实现机制间的关系
93 |
94 | Linux虚拟内存的实现需要多种机制的支持,因此,本章我们将围绕以下几种核心机制进行介绍:
95 |
96 | - 地址映射机制
97 |
98 | - 请页机制
99 |
100 | - 内存分配和回收机制
101 |
102 | - 交换机制
103 |
104 | - 缓存和刷新机制
105 |
106 | 这几种机制的关系如图4.4所示。
107 |
108 |
109 |

110 |
111 |
112 | 图4.4 虚拟内存实现机制及之间的关系
113 |
114 | 首先内核通过映射机制把进程的虚拟地址映射到物理地址,在进程运行时,如果内核发现进程要访问的页没有在物理内存时,就发出了请页要求①;如果有空闲的内存可供分配,就请求分配内存②(于是用到了内存的分配和回收),并把正在使用的物理页记录在页缓存中③(使用了缓存机制)。如果没有足够的内存可供分配,那么就调用交换机制,腾出一部分内存④⑤。另外在地址映射中要通过TLB(翻译后援存储器)来寻找物理页⑧;交换机制中也要用到交换缓存⑥,并且把物理页内容交换到交换文件中后也要修改页表来映射文件地址⑦。
--------------------------------------------------------------------------------
/第七章/section7_1.md:
--------------------------------------------------------------------------------
1 | ## 7.1 临界区和竞争状态
2 |
3 | 所谓临界区(critical regions)就是访问和操作共享数据的代码段。多个内核任务并发访问同一个资源通常是不安全的。为了避免对临界区进行并发访问,编程者必须保证临界区代码被原子地执行。也就是说,代码在执行期间不可被打断,就如同整个临界区是一个不可分割的指令一样。如果两个内核任务处于同一个临界区中,这就是一种错误现象。如果这种情况确实发生了,我们就称它是竞争状态。注意竞争状态是小概率事件,因为竞争引起的错误有时出现,有时并不出现,所以调试这种错误会非常困难。避免并发和防止竞争状态则称为同步(synchronization)。
4 |
5 | ### 7.1.1临界区举例
6 |
7 | 为了进一步了解竞争状态,我们首先要明白临界区无处不在。首先,我们考虑一个非常简单的共享资源的例子:一个全局整型变量和一个简单的临界区,其中的操作仅仅是将整型变量的值增加1:
8 |
9 | i++;
10 |
11 | 该操作可以转化成下面三条机器指令序列:
12 |
13 | (1) 得到当前变量i的值并且拷贝到一个寄存器中。
14 |
15 | (2)将寄存器中的值加1。
16 |
17 | (3) 把i的新值写回到内存中。
18 |
19 | 这三条指令形成一个临界区。现在假定有两个内核任务同时进入这个临界区,如果i的初始值是1,那么,我们所期望的结果应该像下面这样:
20 |
21 | | **内核任务1** | **内核任务2** |
22 | | --- | --- |
23 | | 获得i(1) | --- |
24 | | 增加 i(1->2) | --- |
25 | | 写回 i(2) | --- |
26 | | | 获得 i(2) |
27 | | | 增加 i(2->3) |
28 | | | 写回 i(3) |
29 |
30 | 从中可以看出,两个任务分别把i加1,因此i变为3。但是,实际的执行序列却可能如下:
31 |
32 | | **内核任务1** | **内核任务2** |
33 | | --- | --- |
34 | | 获得 i(1) | --- |
35 | | --- | 获得 i(1) |
36 | | 增加 i(1->2) | --- |
37 | | --- | 增加 i(1->2) |
38 | | 写回 i(2) | --- |
39 | | --- | 写回 i(2) |
40 |
41 | 如果两个内核任务都在变量i值增加前读取了它的初值,进而又分别增加变量i的值,最后再保存该值,那么变量i的值就变成了2,也就是说出现了“1+1+1=2”的情况。这是最简单的临界区例子,幸好对这种简单竞争状态的解决方法也同样简单,我们仅仅需要将这些指令作为一个不可分割的整体来执行就可以了。多数处理器都提供了指令来原子地读变量、增加变量然后再写回变量,使用这样的指令就能解决一些问题。内核也提供了一组实现这些原子操作的接口,将在7.2.1节讨论。
42 |
43 | ### 7.1.2 共享队列和加锁
44 |
45 | 现在我们来讨论一个更为复杂的竞争状态。假设有一个需要处理的请求队列,这里假定该队列是一个链表,链表中每个结点的逻辑意义代表一个“请求”。有两个函数可以用来操作此队列:一个函数将新请求添加到队列尾部,另一个函数从队列头删除请求,然后处理它。内核各个部分都会调用这两个函数,所以内核会频繁地在队列中加入请求,从队列中删除请求并对其处理。对请求队列的操作无疑要用多条指令。如果一个任务试图读取队列,而这时正好另一个任务正在处理该队列,那么读取任务就会发现队列此刻正处于不一致状态(与原本要读取的队列不一样了)。很明显,如果允许并发访问队列,就会产生意想不到的错误。当共享资源是一个复杂的数据结构时,竞争状态往往会使该数据结构遭到破坏。
46 |
47 | 对于这种情况,锁机制可以避免竞争状态。这种锁就如同一把门锁,门后的房间可想象成一个临界区。在一个指定时间内,房间里只能有个一个内核任务存在,当一个任务进入房间后,它会锁住身后的房门;当它结束对共享数据的操作后,就会走出房间,打开门锁。如果另一个任务在房门上锁时来了,那么它就必须等待房间内的任务出来并打开门锁后,才能进入房间。
48 |
49 | 前面例子中讲到的请求队列,可以使用一个单独的锁进行保护。每当有一个新请求要加入队列,任务会首先要占住锁,然后就可以安全地将请求加入到队列中,结束操作后再释放该锁;同样当一个任务想从请求队列中删除一个请求时,也需要先占住锁,然后才能从队列中读取和删除请求,而且在完成操作后也必须释放锁。任何要访问队列的其它任务也类似,必须占住锁后才能进行操作。因为在一个时刻只能有一个任务持有锁,所以在一个时刻只有一个任务可以操作队列。由此可见锁机制可以防止并发执行,并且保护队列不受竞争状态影响。
50 |
51 | 任何要访问队列的代码首先都需要占住相应的锁,这样该锁就能阻止来自其它内核任务的并发访问:
52 |
53 | | **任务 1** | **任务2** |
54 | | --- | --- |
55 | | 试图锁定队列 | 试图锁定队列 |
56 | | 成功:获得锁 | 失败:等待… |
57 | | 访问队列… | 等待… |
58 | | 为队列解除锁 | 等待… |
59 | | … | 成功:获得锁 |
60 | | | 访问队列… |
61 | | | 为队列解除锁 |
62 |
63 | 请注意锁的使用是 **自愿的、非强制的**,它完全属于一种编程者自选的编程手段。当然,如果不这么做,无疑会造成竞争状态而破坏队列。
64 |
65 | 锁有多种多样的形式,而且加锁的粒度范围也各不相同,Linux自身实现了几种不同的锁机制。各种锁机制之间的区别主要在于当锁被持有时的行为表现,一些锁被持有时会不断进行循环,等待锁重新可用,而有些锁会使当前任务睡眠,直到锁可用为止。下一节我们将讨论Linux中不同锁之间的行为差别及它们的接口。
66 |
67 | ### 7.1.3 确定保护对象
68 |
69 | 找出哪些数据需要保护是关键所在。由于任何可能被并发访问的代码都需要保护,所以寻找哪些代码不需要保护反而相对更容易些,我们也就从这里入手。内核任务的局部数据仅仅被它本身访问,显然不需要保护,比如,局部自动变量不需要任何形式的锁,因为它们独立存在于内核任务的栈中。类似地,如果数据只会被特定的进程访问,那么也不需要加锁。
70 |
71 | 到底什么数据需要加锁呢?大多数内核数据结构都需要加锁!有一条很好的经验可以帮助我们判断:如果有其它内核任务可以访问这些数据,那么就给这些数据加上某种形式的锁;如果任何其它东西能看到它,那么就要锁住它。
72 |
73 | ### 7.1.4 死锁
74 |
75 | 死锁的产生需要一定条件:要有一个或多个并发执行的内核任务和一个或多个资源,每个任务都在等待其中的一个资源,但所有的资源都已经被占用了。所有任务都在相互等待,但它们永远不会释放已经占有的资源。于是任何任务都无法继续,这便意味着死锁发生了。
76 |
77 | 一个很好的死锁例子是四路交通堵塞问题。如果每一个停止的车都决心等待其它的车开动后自己再启动,那么就没有任何一俩车能启动,于是交通死锁发生了。
78 |
79 | 最简单的死锁例子是 **自死锁**:如果一个执行任务试图去获得一个自己已经持有的锁,它将不得不等待锁被释放,但因为它正在忙着等待这个锁,所以自己永远也不会有机会释放锁,最终结果就是死锁:
80 |
81 | 获得锁
82 |
83 | 再次试图获得锁
84 |
85 | 等待锁重新可用……
86 |
87 | 同样道理,考虑 有n个内核任务和n把锁,如果每个任务都持有一把其它进程需要得到的锁,那么所有的任务都将停下来等待它们希望得到的锁重新可用。最常见的例子是有两个任务和两把锁,它们通常被叫做ABBA死锁。
88 |
89 | **内核任务1 内核任务2**
90 |
91 | 获得锁 A 获得锁B
92 |
93 | 试图获得锁B 试图获得锁 A
94 |
95 | 等待锁 B 等待锁 A
96 |
97 | 每个任务都在等待其它任务持有的锁,但是绝没有一个任务会释放它们一开始就持有的锁。这种类型的死锁也叫做 _**“致命拥抱”**_。
98 |
99 | 预防死锁的发生非常重要,虽然很难证明代码是否隐含着死锁,但是写出避免死锁的代码还是可能的。下面给出的一些简单规则来避免死锁的发生:
100 |
101 | 1. 加锁的顺序是关键。使用嵌套的锁时必须保证以相同的顺序获取锁,这样可以阻止致命拥抱类型的死锁。最好能记录下锁的顺序,以便其他人也能照此顺序使用。
102 |
103 | 2. 防止发生饥饿,试着自问,这个代码的执行是否一定会结束?如果
104 | “张”不发生?“王”要一直等待下去吗?
105 |
106 | 3. 不要重复请求同一个锁。
107 |
108 | 4. 越复杂的加锁方案越有可能造成死锁,因此设计应力求简单
109 |
110 | ### 7.1.5 并发执行的原因
111 |
112 | 用户空间之所以需要同步,是因为用户进程会被调度程序抢占和重新调度。由于用户进程可能在任何时刻被抢占,从而使调度程序完全可能选择另一个高优先级的进程到处理器上执行,所以就有可能在一个进程正处于临界区时,就被非自愿地抢占了,如果新被调度的进程随后也进入同一个临界区,前后两个进程相互之间就会产生竞争。这种类型的并发操作并不是真的同时发生,它们只是相互交叉进行,所以也可称作 **“伪并发”** 执行。
113 |
114 | 如果在对称多处理器的机器上,那么两个进程就可以真正地在临界区中同时执行了,这种类型被称为 **“真并发”** 。虽然真并发和伪并发的原因和含义不同,但它们都同样会造成竞争状态,而且也同样需要保护。
115 |
116 | 那么,内核中造成并发执行的原因有哪些?简单来说有以下几种:
117 |
118 | (1)中断——中断几乎可以在任何时刻异步发生,也就可能随时打断当前正在执行的代码。
119 |
120 | (2) 内核抢占——如果内核具有抢占性,那么内核中的任务可能会被另一任务抢占。
121 |
122 | (3) 睡眠及与用户空间的同步——在内核执行的进程可能会睡眠,这就会唤醒调度程序,从而导致调度一个新的用户进程执行。
123 |
124 | (4)对称多处理——两个或多个处理器可以同时执行代码。
125 |
126 | 对内核开发者来说,必须理解上述这些并发执行的诱因,并且为它们事先做充分准备工作。如果在一段内核代码访问某资源的时候系统产生了一个中断,而该中断的处理程序居然还要访问这一资源,这就存在一个“潜在的错误”;类似地,如果一段内核代码在访问一个共享资源期间可以被抢占,这也存在一个“潜在的错误”;还有,如果内核代码在临界区中睡眠,那就是毫无原则地等待竞争状态的到来。最后还要注意,两个处理器绝对不能同时访问同一共享数据。
127 |
128 | 当我们清楚什么样的数据需要保护时,用锁来保护代码安全也就不难做到。然而,真正困难的是如何发现上述潜在并发执行的可能,并有意识地采取某些措施来防止并发执行。其实,真正用锁来保护共享资源并不困难,只要在设计代码的早期就这么做,事情就会很简单。但是,辨认出真正需要共享的数据和相应的临界区,才是真正有挑战性的地方。这里要说明的是,最开始设计代码的时候就要考虑加入锁。如果代码已经写好了,再在其中找到需要上锁的部分并向其中追加锁,是非常困难的,结果也往往不尽人意。所以,在编写代码的开始阶段就设计恰当的锁是一种基本原则。
129 |
130 |
--------------------------------------------------------------------------------
/第一章/section1_1.md:
--------------------------------------------------------------------------------
1 | ## **1.1 认识操作系统**
2 | 从使用者的角度看,操作系统使得计算机易于使用。从程序员的角度看,操作系统把软件开发人员从与硬件打交道的繁琐事务中解放出来。从设计者的角度看,有了操作系统,就可以方便的对计算机系统中的各种软硬件资源进行有效的管理。
3 |
4 |
5 | ### **1.1.1 从使用者角度看**
6 |
7 | 我们对操作系统的认识一般是从使用开始的。打开计算机,呈现在眼前的首先是操作系统。如果用户打开的是操作系统字符界面,就可以通过命令完成需要的操作,例如在Linux下拷贝一个文件
8 |
9 | cp /home/TEST /mydir/test
10 |
11 | 上述命令可以把/home目录下的TEST文件拷贝到mydir目录下,并更名为test。
12 |
13 | 为什么我们可以这么轻而易举地拷贝文件?操作系统从中做了什么?首先,文件这个概念是从操作系统中衍生出来的。如果没有文件这个实体,我们就必须指明数据存放的物理位置,例如,哪个柱面,哪个扇区。其次,数据搬动过程是复杂的I/O操作,一般用户无法关注这些具体的细节。最后,这个命令的执行还涉及其他复杂的操作,但是,有了操作系统,用户只需要知道文件名,其它繁琐的事务完全由操作系统去处理。
14 |
15 | 如果用户在图形界面下操作,上述处理就更加容易,只需点击鼠标就可以完成需要的操作。实际上,图形界面的本质也是执行各种命令,例如,如果是拷贝一个文件,那么就要调用cp命令,而具体的拷贝操作最终还是由操作系统去完成。
16 |
17 | 因此,不管是敲击键盘或者是点击鼠标,这些简单的操作指挥计算机完成着复杂的处理过程。正是操作系统,把繁琐留给自己,简单留给用户。
18 |
19 | ### **1.1.2 从程序开发者的角度看**
20 |
21 | 从程序开发者的角度看,开发者不用关心如何在内存存放变量、数据,如何从外存存取数据,如何把数据在输出设备上显示出来等等。例如在Linux下实现cp命令的C语言片段为:
22 |
23 | inf = open("/home/TEST", O_RDONLY);
24 | outf = open("/mydir/test", O_WRONLY);
25 | do{
26 | len = read(inf, buf, 4096);
27 | write(outf, buf, len);
28 | } while(len);
29 | close(inf);
30 | close(outf);
31 |
32 | 在这段程序中,涉及到四个函数open(), close(),write()和read(),这些都是C语言函数库中的函数。进一步追究,这些函数都要涉及I/O操作,因此,它们的实现必须调用操作系统所提供的接口,也就是说,打开文件、关闭文件、读写文件的具体实现是由操作系统完成的。这些操作非常繁琐,操作系统不同,其具体实现可能不同。
33 |
34 | ### **1.1.3 从操作系统在整个计算机系统所处位置看**
35 | 如果把操作系统放在整个计算机系统中看,则如图1.1所示:
36 |
37 |
38 |

39 |
40 |
41 | 因为操作系统这个术语越来越大众化,因此许多用户把他们在显示器屏幕上看到的东西理所当然的认为就是操作系统,例如认为图形界面、浏览器、系统工具集等都算操作系统的一部分。但是,本书讨论的操作系统是指内核(Kernel)。用户界面是操作系统的外在表象,内核是操作系统的内在核心,它真正完成用户程序所要求的操作。
42 |
43 | 从图1.1可以看出,**一方面操作系统是上层软件与硬件打交道的窗口和桥梁,另一方面操作系统是其它所有用户程序运行的基础。**
44 |
45 | 下面从一个程序的执行过程,我们看一下操作系统起什么样的作用。一个简单的C程序如下,其名为**test.c**:
46 |
47 | #include
48 | main()
49 | {
50 | printf(" Hello world\n");
51 | return 0;
52 | }
53 | 用户对这个程序编译并连接:
54 |
55 | gcc test.c –o test
56 |
57 | 于是形成一个可执行的二进制文件test,在Linux 下执行该程序./test
58 |
59 | 执行过程简述如下:
60 |
61 | 1. 用户告诉操作系统执行test
62 | 2. 操作系统通过文件名在磁盘找到该程序
63 | 3. 检查可执行代码首部,找出代码和数据存放的地址
64 | 4. 文件系统找到第一个磁盘块
65 | 5. 操作系统建立程序的执行环境
66 | 6. 操作系统把程序从磁盘装入内存,并跳到程序开始处执行
67 | 7. 操作系统检查字符串的位置是否正确
68 | 8. 操作系统找到字符串被送往的设备
69 | 9. 操作系统将字符串送往输出设备窗口系统确定这是一个合法的操作,然后将字符串转换成像素
70 | 10. 窗口系统将像素写入存储映像区
71 | 11. 视频硬件将像素表示转换成一组模拟信号控制显示器(重画屏幕)
72 | 12. 显示器发射电子束。你在屏幕上看到Hello world。
73 |
74 | 从这个简单的例子可以看出,任何一个程序的运行只有借助于操作系统才能得以顺利完成,因此,从本质上说,**操作系统是应用程序的运行环境。**
75 |
76 | ### **1.1.4 从操作系统设计者的角度看**
77 |
78 | 操作系统是一个庞大复杂的系统软件。其设计目标有两个,一是尽可能地方便用户使用计算机,二是让各种软件资源和硬件资源高效而协调地运转起来。
79 |
80 | 笼统地说,计算机的硬件资源包括CPU、存储器和各种外设,其中外设种类繁多,如磁盘、鼠标、网络接口、打印机等,操作系统对外设的操作是通过I/O接口进行的。软件资源主要指存放在存储介质上的文件。
81 |
82 | 假设在一台计算机上有三道程序同时运行,并试图在一台打印机上输出运算结果,这意味着必须考虑以下问题:
83 |
84 | (1)三道程序在内存中如何存放?
85 |
86 | (2)什么时候让某个程序占用CPU?
87 |
88 | (3)怎样有序地输出各个程序的运算结果?
89 |
90 | 对这些问题的解决都必须求助于操作系统,也就是说操作系统必须对内存进行管理,对CPU进行管理,对外设进行管理,对存放在磁盘上的文件更是要精心组织和管理,不仅如此,操作系统对这些资源进行管理的基础上,还要给用户提供良好的接口,以便用户能在某种程度上使用或者操纵这些资源。
91 |
92 | 因此,从操作系统设计者的角度考虑,一个操作系统必须包含以下几部分:
93 |
94 | 1) 操作系统接口
95 |
96 | 2) CPU管理
97 |
98 | 3) 内存管理
99 |
100 | 4) 设备管理
101 |
102 | 5) 文件管理
103 |
104 | 以上这几大管理功能,因具体操作系统不同而稍有取舍,但Linux具备了以上所有的管理功能。
105 |
106 | ### **1.1.5 操作系统组成**
107 |
108 | 尽管我们从不同角度初步认识了操作系统这一概念,但日常应用中,操作系统一词已经有很多不同的内涵。操作系统通常被认为是整个系统中负责完成最基本功能和系统管理的部分。除了内核,这些部分还应当包括启动引导程序、命令行shell或者其他种类的用户界面、基本的文件管理工具和系统工具等。
109 |
110 | 可是,由于大多数最终用户是通过商业途径得到操作系统,他们很少会仅仅购买一个只包含以上功能的软件包。一般地,他们在得到操作系统的同时,更需要的是构架于其上的应用软件,从而完成所需的实际功能。为了满足这种需求,操作系统一般要和应用软件绑定发行和出售。这样的软件包在Linux领域被称作发布版。
111 |
112 | 由此就引起了一些误解,许多用户理所当然地认为发布版就是操作系统。但是,从逻辑结构划分,发布版中的很多应用软件不应该属于操作系统。
113 |
114 | 为了符合大多数人的习惯,在本书中,我们一般用操作系统这个词指代发布版,而用内核表示操作系统本来的逻辑概念。在不引起混淆的情况下,有时也会用操作系统表示内核。
115 |
116 | 操作系统本质上也是大型软件包(从开发者的角度看),因此结构组织也不会与其它大型软件迥然而异:操作系统的设计采取分层结构,越向上层抽象程度越高,越接近用户;相反,越向下层,越靠近硬件,抽象也相对接近硬件。另外,上层软件依靠下层软件提供的服务,而且上层软件本身还提供附加服务,因此,操作系统的结构总体呈现倒金子塔形。
117 |
118 | 不同的操作系统,其组成结构不尽相同。我们选取Unix/Linux操作系统作为背景,至于各种操作系统之间的具体差异,读者可以对比下面的公式之后形成自己的认识。
119 |
120 | 我们用一组简单的公式来描述操作系统的组成要素:
121 |
122 | 操作系统 = 内核 + 系统程序
123 |
124 | 系统程序 = 编译环境 + API(应用程序接口)+AUI(用户接口)
125 |
126 | 编译环境 = 编译程序 + 连接程序 + 装载程序
127 |
128 | API = 系统调用 + 语言库函数(C、C++、Java等等)
129 |
130 | AUI = shell + 系统服务例程(如x服务器等)+ 应用程序(浏览器,字处理,编辑器等)
131 |
132 | 而整个软件系统是:
133 |
134 | 软件系统 = 操作系统 + AUI
135 |
136 | 操作系统最底层的组件是内核,其上层搭建了许多系统程序。
137 |
138 | 系统程序包括三个部分,分别是:编译环境、应用程序接口和用户接口。
139 |
140 | 编译环境包含汇编、C 等低高级语言编译程序,连接程序和装载程序,这些程序负责将文本格式的程序语言转变为机器能识别和装载的机器代码。
141 |
142 | 应用程序接口(API)包含内核提供的系统调用接口和语言库,系统调用是为了能让应用程序使用内核提供的服务,语言库函数则是为了方便应用程序开发,所以将一些常用的基础功能预先编译以供使用,比如对C语言来说有常用的C库等;
143 |
144 | 用户接口(AUI)包括我们熟悉的shell、系统服务程序和常用的应用程序。
145 |
146 | 这是一个典型的结构,但不是一成不变。许多操作系统的发行版中会有所删减,比如应用于嵌入式设备的系统,对X服务器就可能不做要求。但是像内核、系统调用等要素是必不可少的。
147 |
148 | 关于系统软件在此给予进一步说明。系统软件是相对应用软件而言的,应用软件针对最终用户需求编写,完成实际功能,而系统软件则是为了简化应用程序的开发而存在的,比如数据库系统为应用软件提供了有效的数据传输、存储服务;还有编程语言的执行环境(它由C库实现),也属于一种系统程序,它为应用程序开发提供了诸如I/O操作例程,图形库,计算库等等基础服务。可见系统软件范围覆盖很广,只要面向的服务群体不是最终用户的软件都可以划归到系统软件中。
149 |
150 |
--------------------------------------------------------------------------------
/第四章/section4_5.md:
--------------------------------------------------------------------------------
1 | ## 4.5 交换机制
2 |
3 | 当物理内存出现不足时,Linux
4 | 内存管理子系统需要释放部分物理内存页面。这一任务由内核的交换守护进程 kswapd
5 | 完成,该内核守护进程实际是一个内核线程,它在内核初始化时启动,并周期地运行。它的任务就是保证系统中具有足够的空闲页面,从而使内存管理子系统能够有效运行。
6 |
7 | ### 4.5.1 交换的基本原理
8 |
9 | 如前所述,每个进程的可以使用的虚存空间很大(3GB),但实际使用的空间并不大,一般不会超过几MB,大多数情况下只有几十K或几百K。可是,当系统的进程数达到几百甚至上千个时,对存储空间的总需求就很大,在这种情况下,一般的物理内存量就很难满足要求。因此,在计算机技术的发展史上很早就有了把内存的内容与一个专用的磁盘空间交换的技术,在Linux中,我们把用作交换的磁盘空间叫做**交换文件或交换区**。
10 |
11 | 交换技术已经使用了很多年。第一个Unix系统内核就监控空闲内存的数量。当空闲内存数量小于一个固定的极限值时,就执行换出操作。换出操作包括把进程的整个地址空间拷贝到磁盘上。反之,当调度算法选择出一个进程运行时,整个进程又被从磁盘中交换进来。
12 |
13 | 现代的Unix(包括Linux)内核已经摒弃了这种方法,主要是因为当进行换入换出时,上下文切换的代价相当高。在Linux中,交换的单位是页面而不是进程。尽管交换的单位是页面,但交换还是要付出一定的代价,尤其是时间的代价。实际上,在操作系统中,时间和空间是一对矛盾,常常需要在二者之间作出平衡,有时需要以空间换时间,有时需要以时间换空间,页面交换就是典型的以时间换空间。这里要说明的是,页面交换是不得已而为之,例如在时间要求比较紧急的实时系统中,是不宜采用页面交换机制的,因为它使程序的执行在时间上有了较大的不确定性。因此,Linux给用户提供了一种选择,可以通过命令或系统调用开启或关闭交换机制。
14 |
15 | 在页面交换中,页面置换算法是影响交换性能的关键性指标,其复杂性主要与换出有关。具体说来,必须考虑三个主要问题:
16 |
17 | 1. 哪种页面要换出
18 |
19 | 2. 如何在交换区中存放页面
20 |
21 | 3. 如何选择被交换出的页面
22 |
23 | 请注意,我们在这里所提到的页或页面指的是其中存放的数据,因此,所谓页面的换入换出实际上是指页面中数据的换入换出。
24 |
25 | #### 1. 哪种页面被换出
26 |
27 | 实际上,交换的最终目的是页面的回收。并非内存中的所有页面都是可以交换出去的。事实上,只有与用户空间建立了映射关系的物理页面才会被换出去,而内核空间中内核所占的页面则常驻内存。我们下面对用户空间中的页面和内核空间中的页面给出进一步的分类讨论。
28 |
29 | 可以把用户空间中的页面按其内容和性质分为以下几种:
30 |
31 | (1). 进程映像所占的页面,包括进程的代码段、数据段、堆栈段以及动态分配的“存储堆”(参见图4.5)。
32 |
33 | (2). 通过系统调用mmap()把文件的内容映射到用户空间
34 |
35 | (3). 进程间共享内存区
36 |
37 | 对于第1种情况,进程的代码段数据段所占的内存页面可以被换入换出,但堆栈所占的页面一般不被换出,因为这样可以简化内核的设计。
38 |
39 | 对于第2种情况,这些页面所使用的交换区就是被映射的文件本身。
40 |
41 | 对于第3种情况,其页面的换入换出比较复杂。
42 |
43 | 与此相对照,映射到内核空间中的页面都不会被换出。具体来说,内核代码和内核中的全局量所占的内存页面既不需要分配(启动时被装入),也不会被释放,这部分空间是静态的。相比之下,进程的代码段和全局量都在用户空间,所占的内存页面都是动态的,使用前要经过分配,最后都会被释放,中途可能被换出而回收后另行分配。
44 |
45 | 除此之外,内核在执行过程中使用的页面要经过动态分配,但永驻内存,此类页面根据其内容和性质可以分为两类:
46 |
47 | (1). 内核调用kmalloc()或vmalloc()为内核中临时使用的数据结构而分配的页用完立即释放。但是,由于一个页面中存放有多个同种类型的数据结构,所以要到整个页面都空闲时才把该页面释放。
48 |
49 | (2). 内核中通过调用__get_free_pages为某些临时使用和管理目的而分配的页面,例如,每个进程的内核栈所占的两个页面、从内核空间复制参数时所使用的页面等等,这些页面也是一旦使用完毕便无保存价值,所以立即释放。
50 |
51 | 在内核中还有一种页面,虽然使用完毕,但其内容仍有保存价值,因此,并不立即释放。这类页面“释放”之后进入一个LRU(最近最少使用)队列,经过一段时间的缓冲让其“老化”。如果在此期间又要用到其内容了,就又将其投入使用,否则便继续让其老化,直到条件不再允许时才加以回收。这种用途的内核页面大致有以下这些:
52 |
53 | (1). 件系统中用来缓冲存储一些文件目录结构dentry的空间
54 |
55 | (2). 文件系统中用来缓冲存储一些索引节点inode的空间
56 |
57 | (3). 用于文件系统读/写操作的缓冲区
58 |
59 | #### 2. 如何在交换区中存放页面
60 |
61 | 我们知道物理内存被划分为若干页面,每个页面的大小为4KB。实际上,交换区也被划分为块,每个块的大小正好等于一页,我们把交换区中的一块叫做一个**页插槽**(page slot),意思是说,把一个物理页面插入到一个插槽中。当进行换出时,内核尽可能把换出的页放在相邻的插槽中,从而减少在访问交换区时磁盘的寻道时间。这是高效的页面置换算法的物质基础。
62 |
63 | 如果系统使用了多个交换区,事情就变得更加复杂了。快速交换区(也就是存放在快速磁盘中的交换区)可以获得比较高的优先级。当查找一个空闲插槽时,要从优先级最高的交换区中开始搜索。如果优先级最高的交换区不止一个,为了避免超负荷地使用其中一个,应该循环选择相同优先级的交换区。如果在优先级最高的交换区中没有找到空闲插槽,就在优先级次高的交换区中继续进行搜索,依此类推。
64 |
65 | #### 3. 如何选择被交换出的页面
66 |
67 | 页面交换是非常复杂的,其主要内容之一就是如何选择要换出的页面,我们以循序渐进的方式来讨论页面交换策略的选择。
68 |
69 | 策略一,需要时才交换。每当缺页异常发生时,就给它分配一个物理页面。如果发现没有空闲的页面可供分配,就设法将一个或多个内存页面换出到磁盘上,从而腾出一些内存页面来。这种交换策略确实简单,但有一个明显的缺点,这是一种被动的交换策略,需要时才交换,系统势必要付出相当多的时间进行换入换出。
70 |
71 | 策略二,系统空闲时交换。与策略一相比较,这是一种积极的交换策略,也就是,在系统空闲时,预先换出一些页面而腾出一些内存页面,从而在内存中维持一定的空闲页面供应量,使得在缺页中断发生时总有空闲页面可供使用。至于换出页面的选择,一般都采用LRU算法。但是这种策略实施起来也有困难,因为并没有哪种方法能准确地预测对页面的访问,所以,完全可能发生这样的情况,即一个好久没有受到访问的页面刚刚被换出去,却又要访问它了,于是又把它换进来。在最坏的情况下,有可能整个系统的处理能力都被这样的换入/换出所影响,而根本不能进行有效的计算和操作。这种现象被称为页面的“抖动”。
72 |
73 | 策略三,换出但并不立即释放。当系统挑选出若干页面进行换出时,将相应的页面写入磁盘交换区中,并修改相应页表中页表项的内容(把present标志位置为0),但是并不立即释放,而是将其page结构留在一个缓冲(cache)队列中,使其从活跃(active)状态转为不活跃(Inactive)状态。至于这些页面的最后释放,要推迟到必要时才进行。这样,如果一个页面在释放后又立即受到访问,就可以从物理页面的缓冲队列中找到相应的页面,再次为之建立映射。由于此页面尚未释放,还保留着原来的内容,就不需要磁盘读入了。经过一段时间以后,一个不活跃的内存页面一直没有受到访问,那这个页面就需要真正被释放了。
74 |
75 | 策略四,把页面换出推迟到不能再推迟为止。实际上,策略三还有值得改进的地方。首先在换出页面时不一定要把它的内容写入磁盘。如果一个页面自从最近一次换入后并没有被写过(如代码),那么这个页面是“干净的”,就没有必要把它写入磁盘。其次,即使“脏”页面,也没有必要立即写出去,可以采用策略三。至于“干净”页面,可以一直缓冲到必要时才加以回收,因为回收一个“干净”页面花费的代价很小。
76 |
77 | 下面对物理页面的换入换出给出一个概要描述,这里涉及到前面介绍的page结构和free_area结构:
78 |
79 | - 释放页面。如果一个页面变为空闲可用,就把该页面的page结构链入某个空闲队列free_area,同时页面的使用计数count减1。
80 |
81 | - 分配页面。调用___get_free_page()从某个空闲队列分配内存页面,并将其页面的使用计数count置为1。
82 |
83 | - 活跃状态。已分配的页面处于活跃状态,该页面的数据结构page通过其队列头结构lru链入活跃页面队列active_list,并且在进程地址空间中至少有一个页与该页面之间建立了映射关系。
84 |
85 | - 不活跃“脏”状态。处于该状态的页面其page结构通过其队列头结构lru链入不活跃“脏”页面队列inactive_dirty_list,并且原则是任何进程的页面表项不再指向该页面,也就是说,断开页面的映射,同时把页面的使用计数count减1。
86 |
87 | - 将不活跃“脏”页面的内容写入交换区,并将该页面的page结构从不活跃“脏”页面队列inactive_dirty_list转移到不活跃“干净”页面队列,准备被回收。
88 |
89 | - 不活跃“干净”状态。页面page结构通过其队列头结构lru链入某个不活跃“干净”页面队列。
90 |
91 | - 如果在转入不活跃状态以后的一段时间内,页面又受到访问,则又转入活跃状态并恢复映射。
92 |
93 | - 当需要时,就从“干净”页面队列中回收页面,也就是说或者把页面链入到空闲队列,或者直接进行分配。
94 |
95 | 以上是页面换入/换出及回收的基本思想,实际的实现代码还要更复杂一些。
96 |
97 | ### 4.5.2 页面交换守护进程kswapd
98 |
99 | 为了避免在CPU忙碌的时候,也就是在缺页异常发生时,临时搜索可供换出的内存页面并加以换出,Linux内核定期地检查系统内的空闲页面数是否小于预定义的极限,一旦发现空闲页面数太少,就预先将若干页面换出,以减轻缺页异常发生时系统所承受的负担。当然,由于无法确切地预测页面的使用,即使这样做了也还可能出现缺页异常发生时内存依然没有足够的空闲页面。但是,预换出毕竟能减少空闲页面不够用的概率。并且通过选择适当的参数(如每隔多久换出一次,每次换出多少页),可以使临时寻找要换出页面的情况很少发生。为此,Linux内核设置了一个定期将页面换出的守护进程kswapd。
100 |
101 | 从原理上说,kswapd相当于一个进程,它有自己的进程控制块task_struct结构。与其它进程一样受内核的调度。而正因为内核将它按进程来调度,就可以让它在系统相对空闲的时候来运行。不过,与普通进程相比,kswapd有其特殊性。首先,它没有自己独立的地址空间,所以在近代操作系统理论中把它称为“线程”或“守护进程”以与进程相区别。
102 |
103 | 那么,kswapd到底隔多长时间运行一次,这由内核设计时所确定的一个常量HZ决定。HZ决定了内核中每秒时钟中断的次数,用户可以在编译内核前的系统配置阶段改变其值,但是一经编译就确定下来了。在Linux2.4的内核中,每一秒钟kswapd被调用一次。
104 |
105 | Kswapd的执行路线分为两部分,第一部分是发现物理页面已经短缺的情况下才进行的,目的在于预先找出若干页面,且将这些页面的映射断开,使这些物理页面从活跃状态转入不活跃状态,为页面的换出做好准备。第二部分是每次都要执行的,目的在于把已经处于不活跃状态的“脏”
106 | 页面写入交换区,使他们成为不活跃的“干净”页面继续缓冲,或进一步回收这样的页面成为空闲页面。
107 |
108 | 在本章的学习中,有一点需特别向读者强调。在Linux系统中,CPU不能按物理地址访问存储空间,而必须使用虚拟地址。因此,对于Linux内核映像,即使系统启动时将其全部装入物理内存,也要将其映射到虚拟地址空间中的内核空间,而对于用户程序,其经过编译、链接后形成的映像文件最初存于磁盘,当该程序被运行时,先要建立该映像与虚拟地址空间的映射关系,当真正需要物理内存时,才建立地址空间与物理空间的映射关系。
--------------------------------------------------------------------------------
/第七章/section7_4.md:
--------------------------------------------------------------------------------
1 | ## 7.4 内核多任务并发实例
2 |
3 | 内核任务是指在内核态执行的任务,具体包括内核线程、系统调用、中断处理程序、下半部任务等几类。
4 |
5 | ### 7.4.1 内核任务及其并发关系
6 |
7 | 在我们的实例中,涉及三种内核任务,分别是系统调用、内核线程和定时器任务队列。
8 |
9 | (1)系统调用:系统调用是用户程序通过门机制来进入内核执行的内核例程,它运行在内核态,处于进程上下文中,可以认为是代表用户进程的内核任务,因此具有用户态任务的特性。
10 |
11 | (2) 内核线程:内核线程可以理解成在内核中运行的特殊进程,它有自己的“进程上下文”(也就是借用了调用它的用户进程的上下文),所以同样被进程调度程序调度,也可以睡眠。不同之处就在于内核线程运行于内核空间,可访问内核数据,运行期间不能被抢占。
12 |
13 | (3) 定时器任务队列:定时器任务队列属于下半部,在每次产生时钟节拍时得到处理。
14 |
15 | 上述三种内核任务存在如下竞争关系:系统调用和内核线程可能和各种内核任务并发执行,除了中断(定时器任务队列属于软中断范畴)可以抢占它、产生并发外,它们还有可能自发地主动睡眠(比如在一些阻塞性的操作中),放弃处理器,从而其他任务被重新调度,所以系统调用和内核线程除与定时器任务队列发生竞争,也会与其他(包括自己)系统调用和内核线程发生竞争。
16 |
17 | ### 7.4.2 问题描述
18 |
19 | 假设存在这样一个内核共享资源-链表(mine),有多个内核任务并发访问链表:200个内核线程(sharelist)向链表加入新节点;内核定时器(qt\_task)定时删除节点;系统调用(share\_exit)销毁链表。这三种内核任务并发执行时,有可能会破坏链表数据的完整性,所以我们必须对链表进行同步访问保护,以保证数据的一致性。
20 |
21 | ### 7.4.3 实现机制
22 |
23 | 1.变量声明
24 |
25 | ```c
26 | #define NTHREADS 200 /* 线程数 */
27 | struct my_struct {
28 | struct list_head list;
29 | int id;
30 | int pid;
31 | };
32 | static struct work_struct queue; /* 定义工作队列 */
33 | static struct timer_list mytimer; /* 定时器队列 */
34 | static LIST_HEAD(mine); /* sharelist头 */
35 | static unsigned int list_len = 0;
36 | static DECLARE_MUTEX(sem); /* 内核线程进行同步的信号量 */
37 | static spinlock_t my_lock = SPIN_LOCK_UNLOCKED; /* 保护对链表的操作*/
38 | static atomic_t my_count = ATOMIC_INIT(0); /* 以原子方式进行追加*/
39 | static long count = 0; /* 行计数器,每行打印4个信息*/
40 | static int timer_over = 0; /* 定时器结束标志 */
41 | static int sharelist(void *data); /*从共享链表增删节点的线程*/
42 | static void kthread_launcher(struct work_struct *q); /*创建内核线程*/
43 | static void start_kthread(void); /*调度内核线程 */
44 | ```
45 | 2.模块注册函数share\_init
46 | ```c
47 | static int share_init(void)
48 | {
49 | int i;
50 | printk(KERN_INFO"share list enter\n");
51 | INIT_WORK(&queue, kthread_launcher);//初始化工作队列
52 | setup_timer(&mytimer, qt_task, 0); //设置定时器
53 | add_timer(&mytimer); //添加定时器
54 | for (i = 0; i < NTHREADS; i++) //再启动200个内核线程来添加节点
55 | start_kthread();
56 | return 0;
57 | }
58 | ```
59 | 该函数是模块注册函数,也是通过它启动定时器任务和内核线程。它首先初始化定时器任务队列,注册定时器任务qt\_task;然后依次启动200个内核线程start\_kthread()。至此开始对链表进行操作。
60 |
61 | 3.对共享链表操作的内核线程sharelist
62 | ```c
63 | static int sharelist(void *data)
64 | {
65 | struct my_struct *p;
66 | if (count++ % 4 == 0)
67 | printk("\n");
68 | spin_lock(&my_lock); /* 添加锁,保护共享资源 */
69 | if (list_len < 100) {
70 | if ((p = kmalloc(sizeof(struct my_struct), GFP_KERNEL)) == NULL)
71 | return -ENOMEM;
72 | p->id = atomic_read(&my_count); /* 原子变量操作 */
73 | atomic_inc(&my_count);
74 | p->pid = current->pid;
75 | list_add(&p->list, &mine); /* 向队列中添加新节点*/
76 | list_len++;
77 | printk("THREAD ADD:%-5d\t", p->id);
78 | }
79 | else { /* 队列超过定长则删除节点 */
80 | struct my_struct *my = NULL;
81 | my = list_entry(mine.prev, struct my_struct, list);
82 | list_del(mine.prev); /* 从队列尾部删除节点 */
83 | list_len--;
84 | printk("THREAD DEL:%-5d\t", my->id);
85 | kfree(my);
86 | }
87 | spin_unlock(&my_lock);
88 | return 0;
89 | }
90 | ```
91 | 为了防止定时器任务队列抢占执行时造成链表数据不一致,需要在操作链表期间进行同步保护。
92 |
93 | 4.创建内核线程的kthread\_launcher
94 | ```c
95 | void kthread_launcher(struct work_struct *q)
96 | {
97 | kernel_thread(sharelist, NULL, CLONE_KERNEL | SIGCHLD); /*创建内核线程*/
98 | up(&sem);
99 | }
100 | ```
101 | 该函数作用仅仅是通过kernel\_thread方法启动内核线程sharelist。
102 |
103 | 5.调度内核线程的start\_kthread
104 | ```c
105 | static void start_kthread(void)
106 | {
107 | down(&sem);
108 | schedule_work(&queue);/*调度工作队列*/
109 | }
110 | ```
111 | 在模块初始化函数share\_init中,创建内核线程的kthread\_launcher任务挂在工作队列上,也就是说该任务受内核中默认的工作者线程events调度(参见5.4.3一节)。
112 |
113 | > 注意:为了能依次建立且启动内核线程,start\_kthread函数会在任务加入调度队列前利用信号量进行自我阻塞即down(&sem),直到内核线程执行后才解除阻塞即up(&sem),这种信号量同步机制保证了串行地创建内核线程,虽然串行并非必需。
114 |
115 | 6.删除节点的定时器任务qt\_task
116 | ```c
117 | void qt_task(unsigned long data)
118 | {
119 | if (!list_empty(&mine)) {
120 | struct my_struct *i;
121 | if (count++ % 4 == 0)
122 | printk("\n");
123 | i = list_entry(mine.next, struct my_struct, list); /* 取下一个节点 */
124 | list_del(mine.next); /* 删除节点 */
125 | list_len--;
126 | printk("TIMER DEL:%-5d\t", i->id);
127 | kfree(i);
128 | }
129 | mod_timer(&mytimer, jiffies + 1); /*修改定时器时间*/
130 | }
131 | ```
132 | 7.share\_exit
133 | ```c
134 | static void_exit share_exit(void)
135 | {
136 | struct list_head *n, *p = NULL;
137 | struct my_struct *my = NULL;
138 | printk("\nshare list exit\n");
139 | del_timer(&mytimer);
140 | spin_lock(&my_lock); /* 上锁,以保护临界区 */
141 | list_for_each_safe(p, n, &mine) { /* 删除所有节点,销毁链表 */
142 | if (count++ % 4 == 0)
143 | printk("\n");
144 | my = list_entry(p, struct my_struct, list); /* 取下一个节点 */
145 | list_del(p);
146 | printk("SYSCALL DEL: %d\t", my->id);
147 | kfree(my);
148 | }
149 | spin_unlock(&my_lock); /* 开锁*/
150 | printk(KERN_INFO"Over \n");
151 | }
152 | ```
153 | 该函数是模块注销函数,负责销毁链表。由于销毁时内核线程与定时器任务都在运行,所以应该进行同步保护,即锁住链表,这是通过spin\_lock自旋锁达到的,因为自旋锁保证了任务执行的串行化,此刻其他任务就没有机会执行了。当重新打开自旋锁时,其他任务就可以运行。
154 |
155 | 图7.1 给出了这个并发控制实例中各种对象的关系示意图。
156 |
157 |

158 |
159 |
160 | 图 7.1 并发控制实例示意图
161 |
162 |
--------------------------------------------------------------------------------
/第六章/section6_3.md:
--------------------------------------------------------------------------------
1 | ## 6.3 系统调用实现
2 |
3 | 当用户态的进程调用一个系统调用时,CPU从内核态切换到内核态并开始执行一个内核函数。Linux对系统调用的调用必须通过执行int$0x80汇编指令,这条汇编指令产生向量为128的编程异常(参见5.1.3异常及非屏蔽中断)。
4 |
5 | 因为内核实现了很多不同的系统调用,因此进程必须传递一个系统调用号的参数来识别所需的系统调用;eax寄存器就用做此目的。我们将在本章的“参数传递”一节看到,当调用一个系统调用时通常还要传递另外的参数。
6 |
7 | 与其他异常处理程序的结构类似,系统调用处理程序执行下列操作:
8 |
9 | 1. 在内核栈保存大多数寄存器的内容(这个操作对所有的系统调用都是通用的,并用汇编语言编写)。
10 |
11 | 2. 调用所谓系统调用服务例程的相应的C函数来处理系统调用。
12 |
13 | 3. 通过syscall_exit_work( )函数从系统调用返回(这个函数用汇编语言编写)。
14 |
15 | xyz( )系统调用对应的服务例程的名字通常是sys_xyz()。图6.1显示了调用系统调用的应用程序、相应的封装例程、系统调用处理程序及系统调用服务例程之间的关系。箭头表示函数之间的执行流。
16 |
17 |
18 |

19 |
20 |
21 | 图6.1 调用一个系统调用
22 |
23 | ### 6.3.1 初始化系统调用
24 |
25 | 内核初始化期间调用trap_init( )函数建立IDT表中128号向量对应的表项,语句如下:
26 | ```c
27 | set_system_gate(SYSCALL_VECTOR, &system_call);
28 | ```
29 | 其中SYSCALL_VECTOR是一个宏定义,其值为0x80,该调用把下列值装入这个门描述符的相应域(参见第五章“5.2中断描述符表的初始化”一节):
30 |
31 | 段选择子:因为系统调用处理程序属于内核代码,填写内核代码段__KERNEL_CS的段选择子。
32 |
33 | 偏移量:指向system_call( )系统调用处理程序。
34 |
35 | 类型:置为15。表示这个异常是一个陷阱,相应的处理程序不禁止可屏蔽中断。
36 |
37 | DPL(描述符特权级):置为3。这就允许用户态进程调用这个异常处理程序。
38 |
39 | ### 6.3.2 system_call( )函数
40 |
41 | system_call( )函数实现了系统调用处理程序。它首先把系统调用号和这个异常处理程序可以用到的所有CPU寄存器保存到相应的栈中,当然,栈中还有CPU已自动保存的eflags、cs、eip、ss和esp寄存器(参见第五章“异常的硬件处理”一节),也在ds和es中装入内核数据段的段选择子:
42 | ```c
43 | ENTRY(system_call)
44 |
45 | pushl %eax
46 |
47 | SAVE_ALL
48 |
49 | GET_THREAD_INFO(%ebp)
50 | ```
51 | GET_THREAD_INFO()宏把当前进程PCB的地址存放在ebp中;这是通过获得内核栈指针的值并把它取整到8KB的倍数而完成的(参见第三章“3.2.4进程控制块的存放”一节),此宏定义在arch/x86/include/asm/thread_info.h中。然后,对用户态进程传递来的系统调用号进行有效性检查。如果这个号大于或等于NR_syscalls,系统调用处理程序终止:
52 | ```c
53 | cmpl $(nr_syscalls), %eax
54 |
55 | jae syscall_badsys
56 | ```
57 | 如果系统调用号无效,跳转到syscall_badsys处执行,此时就把-ENOSYS值存放在栈中eax[^2]寄存器所在的单元(从当前栈顶开始偏移为24的单元)。然后跳到resume_userspace反回到用户空间。当进程以这种方式恢复它在用户态的执行时,会在eax中发现一个负的返回码。
58 |
59 | [^2]: 2
60 | eax寄存器中既存放系统调用号,也存放系统调用的返回值,前者是一个正数,后者是一个负数。
61 |
62 | 最后, 根据eax中所包含的系统调用号调用对应的特定服务例程:
63 | ```c
64 | call *sys_call_table(0, %eax, 4)
65 | ```
66 | 因为系统调用表中的每一表项占4个字节,因此首先把eax中的系统调用号乘以4再加上sys_call_table系统调用表的起始地址,然后从这个地址单元获取指向相应服务例程的指针,内核就找到了要调用的服务例程。
67 |
68 | 当服务例程执行结束时,system_call( )从eax获得它的返回值,并把这个返回值存放在栈中,让其位于用户态eax寄存器曾存放的位置。然后执行syscall_exit代码段,终止系统调用处理程序的执行(参见“5.4.6从中断返回”一节)。
69 | ```c
70 | movl %eax, 24(%esp)
71 |
72 | syscall_exit:
73 |
74 | ...
75 | ```
76 | 当进程恢复它在用户态的执行时,就可以在eax中找到系统调用的返回码。
77 |
78 | ### 6.3.3 参数传递
79 |
80 | 与普通函数类似,系统调用通常也需要输入/输出参数,这些参数可能是实际的值(例如数值),也可能是函数的地址及用户态进程地址空间的变量。因为system_call()函数是Linux中所有系统调用唯一的人口点,因此每个系统调用至少有一个参数,即通过eax寄存器传递来的系统调用号。例如,如果一个应用程序调用fork()封装例程,在执行int$0x80汇编指令之前就把eax寄存器置为5。因为这个寄存器的设置是由libc中的封装例程进行的,因此程序员通常并不需要关心系统调用号。
81 |
82 | fork()系统调用并不需要其他的参数。不过,很多系统调用确实需要由应用程序明确地传递另外的参数。例如,mmap()系统调用可能需要多达6个参数(除了系统调用号)。
83 |
84 | 普通函数的参数传递是通过把参数值写进活动的程序栈(或者用户态栈或者内核态栈)。但是系统调用的参数通常是传递给系统调用处理程序在CPU中的寄存器,然后再拷贝到内核态堆栈。
85 |
86 | 为什么内核不直接把参数从用户态的栈拷贝到内核态的栈呢?首先,同时操作两个栈是比较复杂的;此外,寄存器的使用使得系统调用处理程序的结构与其他异常处理程序的结构类似。
87 |
88 | 然而,为了用寄存器传递参数,必须满足两个条件:
89 |
90 | 1. 每个参数的长度不能超过寄存器的长度,即32位[^3]。
91 |
92 | 2. 参数的个数不能超过6个(包括eax中传递的系统调用号),因为Intel Pentium寄存器的数量是有限的。
93 |
94 | 第一个条件总能成立,因为根据POSIX标准,不能存放在32位寄存器中的长参数必须通过指定它们的地址来传递。
95 |
96 | 对于第二个条件,确实存在多于6个参数的系统调用:在这样的情况下,用一个单独的寄存器指向进程地址空间中这些参数值所在的一个内存区即可。当然,编程者不用关心这个工作区。与任何C调用一样,当调用libc封装例程时,参数被自动地保存在栈中。封装例程将找到合适的方式把参数传递给内核。
97 |
98 | 存放系统调用参数所用的6个寄存器以递增的顺序为:eax (存放系统调用号)、
99 | ebx、ecx、edx、esi及edi。正如前面看到的那样,system_call()使用SAVE_ALL宏把这些寄存器的值保存在内核态堆栈中。因此,当系统调用服务例程转到内核态堆栈时,就会找到system_call()的返回地址、紧接着是存放在eax中的参数(即系统调用的第一个参数)、存放在ecx中的参数等等。这种栈结构与普通函数调用的栈结构完全相同,因此,服务例程可以很容易地使用一般C语言构造的参数。
100 |
101 | 让我们来看一个例子。处理write( )系统调用的sys_write( )服务例程的声明如下:
102 | ```c
103 | int sys_write (unsigned int fd, const char * buf,unsigned int count)
104 | ```
105 | C编译器产生一个汇编语言函数,该函数可以在栈顶找到fd、buf和count参数,因为这些参数就位于返回地址的下面。
106 |
107 | 在少数情况下,系统调用不使用任何参数,但是相应的服务例程也需要知道在发出系统调用之前CPU寄存器的内容。例如,
108 | 系统调用fork( )没有参数,但其服务例程do_fork()需要知道有关寄存器的值,以便在子进程中使用它们。在这种情况下,一个类型为pt_regs的单独参数允许服务例程访问由SAVE_ALL宏保存在内核态堆栈中的值:
109 | ```c
110 | int sys_fork (struct pt_regs regs)
111 | ```
112 | 服务例程的返回值必须写到eax寄存器中,这是在执行return n指令时由C编译程序自动完成的。
113 |
114 | ### 6.3.4 跟踪系统调用的执行
115 |
116 | 我们可以通过分析getpid系统调用的实际执行过程将上述概念具体化。分析getpid系统调用有两种方法,一种是查看entry.S中的代码细节,阅读相关的源码来分析其运行过程;另外一种是借助一些内核调试工具,动态跟踪执行路径。
117 |
118 | 假设我们的程序源文件名为getpid.c,程序为:
119 |
120 | ```c
121 | #include
122 |
123 | #include
124 |
125 | #include
126 |
127 | #include
128 |
129 | int main(void) {
130 |
131 | long ID;
132 |
133 | ID = getpid();
134 |
135 | printf ("getpid()=%ld\n", ID);
136 |
137 | return(0);
138 |
139 | }
140 | ```
141 |
142 | 将其编译成名为getpid的执行文件:“gcc –o getpid getpid.c”,我们使用KDB来看进入内核后的执行路径(kdb是个内核调试补丁,使用前需要给内核打上该补丁,然后打开调试选项,再重新编译内核)。首先激活KDB(按下pause键),设置内核断点 “bp sys_getpid”,退出kdb。然后执行./getpid。瞬间,进入内核调试状态,执行路径停止在断点sys_getpid处。
143 |
144 | 1. 在KDB>提示符下,执行bt命令观察堆栈,发现调用的嵌套路径,可以看到sys_getpid是在内核函数system_call中被嵌套调用的。
145 |
146 | 2. 在KDB>提示符下,执行rd命令查看寄存器中的数值,可以看到eax中存放的是getpid调用号0x00000014(即十进制20)。
147 |
148 | 3. 在KDB>提示符下,执行ssb(或ss)命令跟踪内核代码执行路径,可以发现sys_getpid执行后,会返回system_call函数,然后接着转入syscall_exit_work例程。
149 |
150 | 结合用户空间的执行路径,该程序的执行大致可归结为以下几个步骤:
151 |
152 | 1. 程序调用libc库的封装函数getpid。该封装函数中将系统调用号_NR_getpid(第20个)压入eax寄存器。
153 |
154 | 2. 调用软中断 int 0x80 进入内核。
155 |
156 | 3. 在内核中首先执行system_call函数,接着根据系统调用号在系统调用表中查找到对应的系统调用服务例程sys_getpid。
157 |
158 | 4. 执行sys_getpid服务例程。
159 |
160 | 5. 执行完毕后,转入syscall_exit_work例程,从系统调用返回。
--------------------------------------------------------------------------------
/第八章/section8_1.md:
--------------------------------------------------------------------------------
1 | ## 8.1 Linux文件系统基础
2 |
3 | 在深入了解文件系统之前,首先介绍文件系统的基本知识。
4 |
5 | ### 8.1.1 Linux文件结构
6 |
7 | 文件结构是文件存放在磁盘等存储设备上的组织方法。主要体现在对文件和目录的组织上。目录提供了管理文件的一个方便而有效的途径。Linux使用标准的目录结构,在Linux安装的时候,安装程序就已经为用户创建了文件系统和完整而固定的目录组成形式,并指定了每个目录的作用和其中的文件类型,如图8.1。
8 |
9 |
10 |

11 |
12 |
13 |
14 | 图8.1 Linux目录树结构
15 |
16 |
17 |
18 | Linux采用的是树型结构。最上层是根目录,其他的所有目录都是从根目录出发而生成的。微软的DOS和Windows也是采用树型结构,但是在DOS和Windows中这样的树型结构的根是磁盘分区的盘符,有几个分区就有几个树型结构,他们之间的关系是并列的。但是在Linux中,无论操作系统管理几个磁盘分区,这样的目录树只有一个。因为Linux是一个多用户系统,因此制定这样一个固定的目录规划有助于对系统文件和不同的用户文件进行统一管理。下面列出了Linux下一些主要目录的功能:
19 |
20 | /bin 二进制可执行命令
21 | /dev 设备特殊文件
22 | /etc 系统管理和配置文件
23 | /home 用户主目录的基点,比如用户user的主目录就是/home/user。
24 | /lib 标准程序设计库,又叫动态链接共享库。
25 | /sbin 系统管理命令,这里存放的是系统管理员使用的管理程序
26 | /tmp 公用的临时文件存储点
27 | /root 系统管理员的主目录
28 | /mnt 用户临时安装其他文件系统的目录。
29 | /proc 虚拟的目录,不占用磁盘空间,是系统内存的映射。可直接访问这个目录来获取系统信息。
30 | /var 某些大文件的溢出区,例如各种服务的日志文件
31 | /usr 最庞大的目录,要用到的应用程序和文件几乎都在这个目录下。
32 |
33 | ### 8.1.2 文件类型
34 |
35 | Linux的文件可以是下列类型之一:
36 |
37 | 1.常规文件
38 |
39 | 计算机用户和操作系统用于存放数据、程序等信息的文件,一般都长期地存放在外存储器(磁盘、磁带等)中。常规文件一般又分为文本文件和二进制文件。
40 |
41 | 2.目录文件
42 |
43 | Linux文件系统将文件索引节点号和文件名同时保存在目录中。所以,目录文件就是将文件的名称和它的索引节点号结合在一起的一张表。目录文件只允许系统进行修改。用户进程可以读取目录文件,但不能对它们进行修改。
44 |
45 | 3.设备文件
46 |
47 | Linux把所有的外设都当作文件来看待。每一种I/O设备对应一个设备文件,存放在/dev目录中,如行式打印机对应/dev/lp文件,第一个软盘驱动器对应/dev/fd0文件。
48 |
49 | 4.管道文件
50 |
51 | 主要用于在进程间传递数据。管道是进程间传递数据的“媒介”。某进程数据写入管道的一端,另一个进程从管道另一端读取数据。Linux对管道的操作与文件操作相同,它把管道做为文件进行处理。管道文件又称先进先出(FIFO)文件。
52 |
53 | 5.链接文件
54 |
55 | 又称符号链接文件,它提供了共享文件的一种方法。在链接文件中不是通过文件名实现文件共享,而是通过链接文件中包含的指向文件的指针来实现对文件的访问。使用链接文件可以访问常规文件,目录文件和其它文件。
56 |
57 | ### 8.1.3 存取权限和文件模式
58 |
59 | 为了保证文件信息的安全,Linux设置了文件保护机制,其中之一就是给文件都设定了一定的访问权限。当文件被访问时,系统首先检验访问者的权限,只有与文件的访问权限相符时才允许对文件进行访问。
60 |
61 | Linux中的每一个文件都归某一个特定的用户所有,而且一个用户一般总是与某个用户组相关。Linux对文件的访问设定了三级权限:文件所有者,与文件所有者同组的用户,其他用户。对文件的访问主要是三种处理操作:读取、写入和执行。三级访问权限和三种处理操作形成了9种情况,如图8.2所示。
62 |
63 |
64 |

65 |
66 |
67 |
68 | 图8.2 文件访问权和访问模式
69 |
70 |
71 | ### 8.1.4 Linux文件系统
72 |
73 | 文件系统指文件存在的物理空间,Linux系统中每个分区都是一个文件系统,都有自己的目录层次结构。Linux会将这些分属不同分区的、单独的文件系统按一定的方式形成一个系统的总的目录层次结构。
74 |
75 | 1.索引节点:
76 |
77 | Linux文件系统使用索引节点来记录文件信息,其作用与Windows的文件分配表类似。索引节点是一个数据结构,它包含文件的长度、创建时间、修改时间、权限、所属关系、磁盘中的位置等信息。每个文件或目录都对应一个索引节点,文件系统把所有的索引节点形成一个数组,系统给每个索引节点分配了一个号码,也就是该节点在数组中的索引号,称为
78 | **索引节点号**。文件系统正是靠这个索引节点号来识别一个文件。可以用ls –i命令查看文件的索引节点:
79 |
80 |
81 | 2.软链接和硬链接
82 |
83 | 可以用链接命令ln(Link)对一个已经存在的文件再建立一个新的链接,而不复制文件的内容。顾名思义,ln是将两个文件名彼此链接起来,使得用户无论使用哪一个文件名都可以访问到同一文件。链接有 **软链接**(也叫 **符号链接** )和 **硬链接**之分。
84 |
85 | **硬链接**(hard link)就是让一个文件对应一个或多个文件名,或者说把我们使用的文件名和文件系统使用的节点号链接起来,这些文件名可以在同一目录或不同目录。一个文件有几个文件名,我们就说该文件的链接数为几。硬链接有两个限制,一是不允许给目录创建硬链接,二是只有在同一文件系统中的文件之间才能创建链接。
86 |
87 | 例如,对已有的文件My.c创建一个硬链接MyHlink.c:
88 | ```
89 | $ln My.c myHlink.c
90 |
91 | $ls –i
92 | ```
93 | 可以看到My.c 和MyHlink.c有相同的索引节点号
94 |
95 | 为了克服硬链接的两个限制,引入 **符号链接** (symbolic link)。符号链接实际上是一种特殊的文件,这种文件包含了另一个文件的任意一个路径名。这个路径名指向位于任意一个文件系统的任意文件,甚至可以指向一个不存在的文件。系统会自动把对符号链接的大部分操作(如读、写等)变为对源文件的操作,但某些操作(如删除等)就会直接在符号链接上完成。
96 |
97 | 例如,对已有的文件My.c创建一个符号链接MySlink.c。
98 |
99 | ```
100 | $ln –s My.c MySlink.c
101 |
102 | $ls –li
103 | ```
104 |
105 | 从显示结果可以看出,My.c和MySlink.c具有不同的索引节点号,也就是说MySlink.c中存放的是My.c的路径。于是在列目录中显示有“MySlink.c-\>My.c”,表示MySlink.c 是符号链接文件,指向的实际文件为My.c
106 |
107 | 3.安装文件系统
108 |
109 | 将一个文件系统的顶层目录挂到另一个文件系统的子目录上,使它们成为一个整体,称为“安装(mount)”。把该子目录称为“安装点(mount
110 | point)”,如图8.2。由于Ext4是Linux的标准文件系统,所以系统把EXT4文件系统的磁盘分区做为系统的根文件系统,EXT4以外的文件系统(如Window的FAT32文件系统)则安装在根文件系统下的某个目录下,成为系统树型结构中的一个分枝。安装一个文件系统用mount命令。
111 |
112 |
113 |

114 |
115 |
116 |
117 | 图 8.3 文件系统的安装
118 |
119 |
120 | 例如:
121 | ```
122 | $ mount -t ntfs /dev/sdd /mnt/usb
123 | ```
124 | 其中,ntfs是u盘的文件系统名称,/dev/sdd是包含文件系统的物理块设备,/mnt/usb就是将要安装到的目录,即安装点。从这个例子可以看出,安装一个文件系统实际上是安装一个物理设备。
125 |
126 | 4.文件系统创建示例
127 |
128 | 为了说明 Linux文件系统层的功能(以及安装的方法),我们在当前文件系统的一个文件中创建一个文件系统(实验环境为EXT4文件系统)。
129 |
130 | 首先用 dd 命令创建一个指定大小的文件(使用/dev/zero 作为源进行文件复制)—— 换句话说,一个用零进行初始化的文件,见清单 1。
131 |
132 | 清单 1. 创建一个经过初始化的文件
133 | ```
134 | $ dd if=/dev/zero of=file.img bs=1k count=10000 //把输入文件/dev/zero拷贝到输出文件file.img中,输入输出的块大小为1k,总共拷贝10000块
135 | 10000+0 records in // 输入块为10000
136 | 10000+0 records out // 输出块为10000
137 | $
138 | ```
139 |
140 | 现在有了一个 10MB 的 file.img文件。使用 losetup 命令将一个循环设备与这个文件关联起来,让它看起来像一个块设备,而不是文件系统中的常规文件:
141 | ```
142 | $ losetup /dev/loop0 file.img
143 | $
144 | ```
145 |
146 |
147 | file.img文件现在作为一个块设备出现(由 /dev/loop0表示)。然后用 mke2fs 在这个设备上创建一个文件系统。这个命令创建一个指定大小的新的ext4 文件系统,见清单2。
148 |
149 | 清单 2. 用循环设备创建 ext4 文件系统
150 | ```
151 | $ mke2fs -c /dev/loop0 10000 //在/dev/loop0块设备上创建大小为10MB的ext4文件系统
152 | ...
153 | $
154 | ```
155 |
156 | 使用 mount 命令将循环设备(/dev/loop0)所表示的 file.img 文件安装到安装点/mnt/point1。注意,文件系统类型指定为 ext4。安装之后,就可以将这个安装点当作一个新的文件系统,比如使用ls 命令,见清单3。
157 |
158 | 清单 3. 创建安装点并通过循环设备安装文件系统
159 | ```
160 | $ mkdir /mnt/point1 //创建安装点
161 | $ mount -t ext4 /dev/loop0 /mnt/point1 //在安装点上安装ext4文件系统
162 | $ ls /mnt/point1 //查看文件系统
163 | lost+found //新文件系统中的默认目录
164 | $
165 | ```
166 |
167 | 如清单4所示,还可以继续这个过程:在刚才安装的文件系统中创建一个新文件,将它与一个循环设备关联起来,再在上面创建另一个文件系统。
168 |
169 | 清单 4. 在循环文件系统中创建一个新的循环文件系统
170 | ```
171 | $ dd if=/dev/zero of=/mnt/point1/file.img bs=1k count=1000
172 | $ losetup /dev/loop1 /mnt/point1/file.img
173 | $ mke2fs -c /dev/loop1 1000
174 | ...
175 | $ mkdir /mnt/point2
176 | $ mount -t ext4 /dev/loop1 /mnt/point2
177 | $ ls /mnt/point2 //查看另一个新的文件系统
178 | lost+found
179 | $ ls /mnt/point1
180 | file.img lost+found $
181 | ```
182 |
183 | 通过这个简单的演示很容易体会到 Linux文件系统(和循环设备)是多么强大。可以按照相同的方法在文件上用循环设备创建加密的文件系统。可以在需要时使用循环设备临时安装文件,这有助于保护数据。
184 |
--------------------------------------------------------------------------------
/第二章/section2_2.md:
--------------------------------------------------------------------------------
1 | ## **2.2 段机制**
2 |
3 | 段是虚拟地址空间的基本单位,段机制必须把虚拟地址空间的一个地址转换为线性地址空间的一个线性地址。
4 |
5 | ### **2.2.1 段描述符**
6 |
7 | 为了实现地址映射,仅仅用段寄存器来确定一个基地址是不够的,至少还得描述段的长度,并且还需要段的一些其他信息,比如访问权之类。所以,这里需要的是一个数据结构,这个结构包括三个方面的内容:
8 |
9 | (1)段的基地址(Base Address):在线性地址空间中段的起始地址。
10 |
11 | (2)段的界限(Limit):在虚拟地址空间中,段内可以使用的最大偏移量。
12 |
13 | (3)段的保护属性(Attribute): 表示段的特性。例如,该段是否可被读出或写入,或者该段是否作为一个程序来执行,以及段的特权级等等。
14 |
15 | 如图2.5所示,虚拟地址空间中偏移量从0到limit范围内的一个段,映射到线性地址空间中就是从Base到Base+Limit。
16 |
17 |
18 |

19 |
20 |
21 |
22 | 把图2.5用一个表描述则如图2.6:
23 |
24 |
25 |

26 |
27 |
28 |
29 | 这样的表就是段描述符表(或叫段表),其中的表项叫做段描述符(Segment Descriptor)。
30 |
31 | 所谓描述符(Descriptor),就是描述段的属性的一个8字节存储单元。在实模式下,段的属性不外乎是代码段、堆栈段、数据段、段的起始地址、段的长度等等,而在保护模式下则复杂一些。将它们结合在一起用一个8字节的数表示,称为描述符 。80x86通用的段描述符的结构如图2.7所示。
32 |
33 |
34 |

35 |
36 |
37 |
38 | 从图可以看出,一个段描述符指出了段的32位基地址和20位段界限(即段长)。
39 |
40 | 第六个字节的G位是粒度位,当G=0时,以节长为单位表示段的长度,即一个段最长可达220(1M)字节。当G=1时,以页(4K)为单位表示段的长度,即一个段最长可达1M×4K=4G字节。D位表示缺省操作数的大小,如果D=0,操作数为16位,如果D=1,操作数为32位。第六个字节的其余两位为0,这是为了与将来的处理器兼容而必须设置为0的位。
41 |
42 | 第5个字节是存取权字节,它的一般格式如图2.8所示:
43 |
44 |
45 |

46 |
47 |
48 |
49 | 第7位P位(Present) 是存在位,表示这个段是否在内存中,如果在内存中。P=1;如果不在内存中,P=0。
50 |
51 | DPL(Descriptor Privilege Level),就是描述符特权级,它占两位,其值为0~3,用来确定这个段的特权级即保护等级。
52 |
53 | S位(System)表示这个段是系统段还是用户段。如果S=0,则为系统段,如果S=1,则为用户程序的代码段、数据段或堆栈段。
54 |
55 | 类型占3位,第三位为E位,表示段是否可执行。当E=0时,为数据段描述符,这时的第2位ED表示扩展方向。当ED=0时,为向地址增大的方向扩展,这时存取数据段中的数据的偏移量必须小于等于段界限,当ED=1时,表示向地址减少的方向扩展,这时偏移量必须大于界限。当表示数据段时,第1位(W)是可写位,当W=0时,数据段不能写,W=1时,数据段可写入。在80x86中,堆栈段也被看成数据段,因为它本质上就是特殊的数据段。当描述堆栈段时,ED=0,W=1,即堆栈段朝地址增大的方向扩展。
56 |
57 |
58 | 在保护模式下,有三种类型的描述符表,分别是全局描述符表GDT(Gloabal Descriptor Table)、中断描述符表IDT(Interrupt Descriptor Table)及局部描述符表LDT(Local Descriptor Table)。为了加快对这些表的访问,Intel设计了专门的寄存器gdtr,ldtr和idtr,以存放这些表的基地址及表的长度界限。各种描述表的具体内容详参见相关参考书。
59 |
60 | 由此可以推断,在保护模式下段寄存器中该存放什么内容了,那就是图2.6中的索引。因为索引表示段描述符在描述符表中位置,因此,把段寄存器也叫选择符,其结构如图2.9所示:
61 |
62 |
63 |

64 |
65 |
66 |
67 |
68 | 可以看出,选择符有三个域。其中,第15~13位是索引域,第2位TI(Table Indicator)为选择域,决定从全局描述符表(TI=0)还是从局部描述符表(TI=1)中选择相应的段描述符。这里我们重点关注的是RPL域,RPL表示请求者的特权级(Requestor Privilege Level)。
69 |
70 | 保护模式提供了四个特权级,用0~3四个数字表示,但很多操作系统(包括Linux)只使用了其中的最低和最高两个,即0表示最高特权级,对应内核态;3表示最低特权级,对应用户态。保护模式规定,高特权级可以访问低特权级,而低特权级不能随便访问高特权级。
71 |
72 | ### **2.2.1 地址转换及保护**
73 |
74 | 程序中的虚拟地址可以表示为“选择符:偏移量”这样的形式,通过以下步骤可以把一个虚拟地址转换为线性地址:
75 |
76 | (1)在段寄存器中装入段选择符,同时把32位地址偏移量装入某个寄存器(比如ESI、EDI等)中。
77 |
78 | (2)根据选择符中的索引值、TI及RPL值,再根据相应描述符表中的段地址和段界限,进行一系列合法性检查(如特权级检查、界限检查),如果该段无问题,就取出相应的描述符放入段描述符高速缓冲寄存器3中。
79 |
80 | (3)将描述符中的32位段基地址和放在ESI、EDI等中的32位有效地址相加,就形成了32位线性地址。
81 |
82 | 注意,在上面的地址转换过程中,从两个方面对段进行了保护:
83 |
84 |
85 | (1)在一个段内,如果偏移量大于段界限,虚拟地址将没有意义,系统将产生异常。
86 |
87 | (2)如果要对一个段进行访问,系统会根据段的保护属性检查访问者是否具有访问权限,如果没有,则产生异常。例如,如果要在只读段中进行写入,系统将根据该段的属性检测到这是一种违规操作,则产生异常。
88 |
89 |
90 | ### **2.2.2 Linux中的段**
91 |
92 | Intel微处理器的段机制是从8086开始提出的,那时引入的段机制解决了从CPU内部16位地址到20位实地址的转换。为了保持这种兼容性,386仍然使用段机制,但比以前复杂得多。因此,Linux内核的设计并没有全部采用Intel所提供的段方案,仅仅有限度地使用了一下分段机制。这不仅简化了Linux内核的设计,而且为把Linux移植到其他平台创造了条件,因为很多RISC处理器并不支持段机制。但是,对段机制相关知识的了解是进入Linux内核的必经之路。
93 |
94 | 从2.2版开始,Linux让所有的进程(或叫任务)都使用相同的逻辑地址空间,因此就没有必要使用局部描述符表LDT。但内核中也用到LDT,那只是在VM86模式中运行Wine,也就是说在Linux上模拟运行Winodws软件或DOS软件的程序时才使用。
95 |
96 |
97 | 在80X86上任意给出的地址都是一个虚拟地址,即任意一个地址都是通过“选择符:偏移量”的方式给出的,这是段机制存访问模式的基本特点。所以在80X86上设计操作系统时无法回避使用段机制。一个虚拟地址最终会通过“段基地址+偏移量”的方式转化为一个线性地址。但是,由于绝大多数硬件平台都不支持段机制,只支持分页机制,所以为了让Linux具有更好的可移植性,我们需要去掉段机制而只使用分页机制。
98 |
99 | 但不幸的是,80X86规定段机制是不可禁止的,因此不可能绕过它直接给出线性地址空间的地址。万般无奈之下,Linux的设计人员干脆让段的基地址为0,而段的界限为4GB,这时任意给出一个偏移量,则等式为“0+偏移量=线性地址”,也就是说“偏移量=线性地址”。另外由于段机制规定“偏移量 < 4GB”,所以偏移量的范围为0H~FFFFFFFFH,这恰好是线性地址空间范围,也就是说虚拟地址直接映射到了线性地址,我们以后所提到的虚拟地址和线性地址指的也就是同一地址。看来,Linux在没有回避段机制的情况下巧妙地把段机制给绕过去了。
100 |
101 | 另外,由于80X86段机制还规定,必须为代码段和数据段创建不同的段,所以Linux必须为代码段和数据段分别创建一个基地址为0,段界限为4GB的段描述符。不仅如此,由于Linux内核运行在特权级0,而用户程序运行在特权级别3,根据80X86的段保护机制规定,特权级3的程序是无法访问特权级为0的段的,所以Linux必须为内核和用户程序分别创建其代码段和数据段。这就意味着Linux必须创建4个段描述符——特权级0的代码段和数据段,特权级3的代码段和数据段。
102 |
103 | Linux在启动的过程中设置了段寄存器的值和全局描述符表GDT的内容,内核代码中可以这样定义段:
104 |
105 | #define __KERNEL_CS 0x10 /*内核代码段,index=2,TI=0,RPL=0*/
106 | #define __KERNEL_DS 0x18 /*内核数据段, index=3,TI=0,RPL=0*/
107 | #define __USER_CS 0x23 /*用户代码段, index=4,TI=0,RPL=3*/
108 | #define __USER_DS 0x2B /*用户数据段, index=5,TI=0,RPL=3*/
109 |
110 | 从定义看出,没有定义堆栈段,实际上,Linux内核不区分数据段和堆栈段,这也体现了Linux内核尽量减少段的使用。因为这几个段都放在GDT中,因此,TI=0 , index就是某个段在GDT表中的下标。内核代码段和数据段具有最高特权,因此其RPL为0,而用户代码段和数据段具有最低特权,因此其RPL为3。可以看出,Linux内核再次简化了特权级的使用,使用了两个特权级而不是4个。
111 |
112 |
113 | 内核代码中可以这样定义全局描述符表:
114 |
115 | ENTRY(gdt_table)
116 | .quad 0x0000000000000000 /* NULL descriptor */
117 | .quad 0x0000000000000000 /* not used */
118 | .quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */
119 | .quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */
120 | .quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */
121 | .quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */
122 | .quad 0x0000000000000000 /* not used */
123 | .quad 0x0000000000000000 /* not used */
124 | …
125 |
126 | 从代码可以看出,GDT放在数组变量gdt_table中。按Intel规定,GDT中的第一项为空,这是为了防止加电后段寄存器未经初始化就进入保护模式而使用GDT的。第二项也没用。从下标2到5共4项对应于前面的4种段描述符值。对照图2.7,从描述符的数值可以得出:
127 |
128 | **·** 段的基地址全部为0x00000000
129 |
130 | **·** 段的上限全部为0xffff
131 |
132 | **·** 段的粒度G为1,即段长单位为4KB
133 |
134 | **·** 段的D位为1,即对这四个段的访问都为32位指令
135 |
136 | **·** 段的P位为1,即四个段都在内存。
137 |
138 | 通过上面的介绍可以看出,Intel的设计可谓周全细致,但Linux的设计者并没有完全陷入这种沼泽,而是选择了简洁而有效的途径,以完成所需功能并达到较好的性能为目标。
139 |
140 | 但是,如果这么定义段,则上一节所说的段保护的第一个作用就失去了,因为这些段使用完全相同的线性地址空间(0~4GB),它们互相覆盖。可以设想,如果不使用分页的话,线性地址空间直接被映射到物理空间,则你修改任何一个段的数据,都会同时修改其它段的数据,段机制所提供的通过“基地址:界限”方式本来将线性地址空间分割,以让段与段之间完全隔离,这种实现段保护的方式根本就不起作用了。那么,这是不是意味着用户可以随意修改内核数据?显然不是的,这是因为,一方面用户段和内核段具有不同的特权级别,另一方面,Linux之所以这么定义段,正是为了实现一个纯的分页,而分页机制会提供给我们所需要的保护。
141 |
142 |
--------------------------------------------------------------------------------
/第二章/section2_1.md:
--------------------------------------------------------------------------------
1 | ## **2.1 内存寻址**
2 |
3 | 曾经有一个叫“阿兰.图灵”的天才1,它设想出了一种简单但运算能力几乎无限发达的理想机器,这不是一个具体的机械设备,而是一个思想模型,可以用来计算能想象得到的所有可计算函数。这个有趣的机器由一个控制器,一个读写头和一条假设两端无限长的带子组成。工作带相当于存储器,被划分成大小相同的格子,每格上可写一个字母,读写头可以在工作带上随意移动,而控制器可以要求读写头读取其下方工作带上的字母。
4 |
5 | 这听起来仅仅是纸上谈兵,但它却是当代冯.诺依曼计算机体系的理论鼻祖。它带来的“数据连续存储和选择读取思想”是目前我们使用的几乎所有机器运行背后的灵魂。计算机体系结构中的核心问题之一就是如何有效地进行内存寻址,因为所有运算的前提都是先要从内存中取得数据,所以内存寻址技术从某种程度上代表了计算机技术。
6 | 据说他16岁开始研究相对论,虽然英年早逝,但才气纵横逻辑学、物理学、数学等多个领域,尤其是数学逻辑上的所作所为奠定了现代计算技术的理论基础 。后来以他名字命名的“图灵奖”被看作计算机学界的最高荣誉。
7 |
8 | ### **2.1.1 Intel X86 CPU寻址的演变**
9 |
10 | 在微处理器的历史上,第一款微处理器芯片4004是由Intel推出的,那是一个4位的微处理器。在4004之后,intel推出了一款8位处理器8080,它有1个主累加器(寄存器A)和6个次累加器(寄存器B,C,D,E,H和L),几个次累加器可以配对(如组成BC, DE或HL)用来访问16位的内存地址,也就是说8080可访问到64K内的地址空间。另外,那时还没有段的概念,访问内存都要通过绝对地址,因此程序中的地址必须进行硬编码(给出具体地址),而且也难以重定位,这就不难理解为什么当时的软件大都是些可控性弱,结构简陋,数据处理量小的工控程序了。
11 |
12 | 几年后,intel开发出了16位的处理器8086,这个处理器标志着Intel X86王朝的开始,这也是内存寻址的第一次飞跃。之所以说这是一次飞跃,是因为8086处理器引入了一个重要概念—段
13 |
14 | 8086处理器的寻址目标是1M大的内存空间,于是它的地址总线扩展到了20位。但是,一个问题摆在了Intel设计人员面前,虽然地址总线宽度是20位的,但是CPU中“算术逻辑运算单元(ALU)”的宽度,即数据总线却只有16位,也就是可直接加以运算的指针长度是16位的。如何填补这个空隙呢?可能的解决方案有多种,例如,可以像一些8位CPU中那样,增设一些20位的指令专用于地址运算和操作,但是那样又会造成CPU内存结构的不均匀。又例如,当时的PDP-11小型机也是16位的,但是其内存管理单元(MMU)可以将16位的地址映射到24位的地址空间。受此启发,Intel设计了一种在当时看来不失为巧妙的方法,即分段的方法。 在微处理器的历史上,第一款微处理器芯片4004是由Intel推出的,那是一个4位的微处理器。在4004之后,intel推出了一款8位处理器8080,它有1个主累加器(寄存器A)和6个次累加器(寄存器B,C,D,E,H和L),几个次累加器可以配对(如组成BC, DE或HL)用来访问16位的内存地址,也就是说8080可访问到64K内的地址空间。另外,那时还没有段的概念,访问内存都要通过绝对地址,因此程序中的地址必须进行硬编码(给出具体地址),而且也难以重定位,这就不难理解为什么当时的软件大都是些可控性弱,结构简陋,数据处理量小的工控程序了。
15 |
16 | 几年后,intel开发出了16位的处理器8086,这个处理器标志着Intel X86王朝的开始,这也是内存寻址的第一次飞跃。之所以说这是一次飞跃,是因为8086处理器引入了一个重要概念—段
17 |
18 | 为了支持分段,Intel在8086 CPU中设置了四个段寄存器:CS、DS、SS和ES,分别用于可执行代码段、数据段、堆栈段及其他段。每个段寄存器都是16位的,对应于地址总线中的高16位。每条“访内”指令中的内部地址也都是16位的,但是在送上地址总线之前,CPU内部自动地把它与某个段寄存器中的内容相加。因为段寄存器中的内容对应于20位地址总线中的高16位(也就是把段寄存器左移4位),所以相加时实际上是内存总线中的高12位与段寄存器中的16位相加,而低4位保留不变,这样就形成一个20位的实际地址,也就实现了从16位内存地址到20位实际地址的转换,或者叫**“映射”**。
19 |
20 | 段式内存管理带来了显而易见的优势,程序的地址不再需要硬编码了,调试错误也更容易定位了,更可贵的是支持更大的内存地址。程序员开始获得了自由。
21 |
22 | 技术的发展不会就此止步。intel的80286处理器于1982年问世了,它的地址总线位数增加到了24位,因此可以访问到16M的内存空间。更重要的是从此开始引进了一个全新理念—**保护模式**。这种模式下内存段的访问受到了限制。访问内存时不能直接从段寄存器中获得段的起始地址了,而需要经过额外转换和检查(从此你不能再随意存取数据段,具体保护和实现我们后面讲述)。
23 |
24 | 为了和过去兼容,80286内存寻址可以有两种方式,一种是先进的保护模式,另一种是老式的8086方式,被成为**实模式**。系统启动时处理器处于实模式,只能访问1M空间,经过处理可进入保护模式,访问空间扩大到16M,但是要想从保护模式返回到实模式,你只有重新启动机器。还有一个致命的缺陷是80286虽然扩大了访问空间,但是每个段的大小还是64k,程序规模仍受到限制。因此这个先天低能儿注定命不会很久。很快它就被天资卓越的兄弟——80386代替了。
25 |
26 | 80386是一个32位的CPU,也就是它的ALU数据总线是32位的,同时它的地址总线与数据总线宽度一致,也是32位,因此,其寻址能力达到4GB。对于内存来说,似乎是足够了。从理论上说,当数据总线与地址总线宽度一致时,其CPU结构应该简洁明了。但是,80386无法做到这一点。作为X86产品系列的一员,80386必须维持那些段寄存器的存在,还必须支持实模式,同时又要能支持保护模式,这给Intel的设计人员带来很大的挑战。
27 |
28 | Intel选择了在段寄存器的基础上构筑保护模式,并且保留段寄存器16位。在保护模式下,它的段范围不再受限于64K,可以达到4G(参见段机制一节)。这一下真正解放了软件工程师,他们不必再费尽心思去压缩程序规模,软件功能也因此迅速提升。
29 |
30 | 从8086的16位到80386的32位处理器,这看起来是处理器位数的变化,但实质上是处理器体系结构的变化,从寻址方式上说,就是从“实模式”到“保护模式”的变化。从80386以后,Intel的CPU经历了80486、Pentium、PentiumII、PentiumIII等型号,虽然它们在速度上提高了好几个数量级,功能上也有不少改进,但基本上属于同一种系统结构的改进与加强,而无本质的变化,所以我们把80386以后的处理器统称为**80x86**。
31 |
32 | ### **2.1.2 80X86寄存器简介**
33 |
34 | 80386作为80X86系列中的一员,必须保证向后兼容,也就是说,既要支持16位的处理器,也要支持32位的处理器。在8086中,所有的寄存器都是16位的,我们来看一下80x86中寄存器有何变化:
35 |
36 |
37 |
38 | - 把16位的通用寄存器、标志寄存器以及指令指针寄存器扩充为32位的寄存器
39 |
40 | - 段寄存器仍然为16位。
41 |
42 | - 增加4个32位的控制寄存器
43 |
44 | - 增加4个系统地址寄存器
45 |
46 | - 增加8个调式寄存器
47 |
48 | - 增加2个测试寄存器
49 |
50 | 下面介绍几种常用的寄存器。
51 |
52 | **1.通用寄存器**
53 |
54 | 8个通用寄存器是8086寄存器的超集,它们分别为:EAX ,EBX ,ECX ,EDX ,EBP ,EBP, ESI及 EDI。这8个通用寄存器中通常保存32位数据,但为了进行16位的操作并与16为机保持兼容,它们的低位部分被当成8个16位的寄存器,即AX、BX…DI。为了支持8位的操作,还进一步把EAX、EBX、ECX、EDX这四个寄存器低位部分的16位,再分为8位一组的高位字节和低位字节两部分,作为8个8位寄存器。这8个寄存器分别被命名为AH、BH、CH、DH和AL、BL、CL、DL。因此,这8个通用寄存器既可以支持1位、8位、16位和32位数据运算,也支持16位和32位存储器寻址。
55 |
56 | **2. 段寄存器**
57 |
58 | 8086中有4个16位的段寄存器:CS、DS、SS、ES,分别用于存放可执行代码的代码段、数据段、堆栈段和其他段的基地址。在80x86中,有6个16位的段寄存器,但是,这些段寄存器中存放的不再是某个段的基地址,而是某个段的选择符(Selector)。因为16位的寄存器无法存放32位的段基地址,段基地址只好存放在一个叫做描述符表(Descriptor)的表中。因此,在80x86中,我们把段寄存器叫做选择符。有关段选择符、描述符表将在段机制一节进行描述。
59 |
60 | **3. 指令指针寄存器和标志寄存器**
61 |
62 | 指令指针寄存器EIP中存放下一条将要执行指令的偏移量(offset ),这个偏移量是相对于目前正在运行的代码段寄存器CS而言的。偏移量加上当前代码段的基地址,就形成了下一条指令的地址。EIP中的低16位可以被单独访问,给它起名叫指令指针IP寄存器,用于16位寻址。标志寄存器EFLAGS存放有关处理器的控制标志,很多标志与16位FLAGS中的标志含义一样。
63 |
64 | **4.控制寄存器**
65 |
66 | 80x86有四个32位的控制寄存器,它们是CR0,CR1,CR2和CR3,主要用于操作系统的分页机制(参见下节)。其结构如图 2.1所示。
67 |
68 |
69 |

70 |
71 |
72 |
73 | 这几个寄存器中保存全局性和任务无关的机器状态。
74 |
75 | CR0中包含了6个预定义标志,这里介绍内核中主要用到的0位和31位。0位是保护允许位PE(Protedted Enable),用于启动保护模式,如果PE位置1,则保护模式启动,如果PE=0,则在实模式下运行。CR0的第31位是分页允许位(Paging Enable),它表示芯片上的分页部件是否被允许工作。由PG位和PE位定义的操作方式如图2.2所示。
76 |
77 |
78 |

79 |
80 |
81 |
82 | 使用以下代码就可以允许分页(AT&T汇编语言参考2.5节):
83 |
84 | movl %cr0, %eax
85 | orl $0x80000000, %eax
86 | movl %eax, %cr0
87 |
88 | CR1是未定义的控制寄存器,供将来的处理器使用。
89 |
90 | CR2是缺页线性地址寄存器,保存最后一次出现缺页的全32位线性地址(将在内存管理一章介绍)。
91 |
92 | CR3是页目录基址寄存器,保存页目录的物理地址,页目录总是放在以4K字节为单位的存储器边界上,因此,其地址的低12位总为0,不起作用,即使写上内容,也不会被理会。
93 |
94 | 这几个寄存器是与分页机制密切相关的,因此,在后面分页机制和第四章虚拟内存管理中会涉及到,读者要记住CR0、CR2及CR3这三个寄存器的作用。
95 | ### **2.1.3 物理地址、虚拟地址及线性地址**
96 |
97 | 在硬件工程师和普通用户看来,内存就是插在或固化在主板上的内存条,它们有一定的容量,比如128MB。但在应用程序员看来中,并不过度关心插在主板上的内存容量,而是他们可以使用的内存空间,比如,他们可以开发一个占用1 GB内存的程序,并让其在操作系统下运行,哪怕这台机器上只有128 MB的物理内存条。而对于操作系统开发者而言,则是介于二者之间,它既需要知道物理内存的地址,也需要提供一套机制,为应用程序员提供另一个内存空间,这个内存空间的大小可以和实际的物理内存大小之间没有多大关系。
98 |
99 | 我们将主板上的物理内存条所提供的内存空间定义为物理内存空间,其中每个内存单元的实际地址就是物理地址;将应用程序员看到的内存空间2定义为虚拟地址空间(或地址空间),其中的地址就叫虚拟地址(或逻辑地址), 一般用“段:偏移量”的形式来描述,比如在8086中A815:CF2D就代表段首地址为A815,段内偏移位为CF2D的虚地址。
100 | > 2 因为高级语言不涉及内存空间,因此,这里指的是从汇编语言的角度看。
101 |
102 | 线性地址空间是指一段连续的,不分段的,范围为0到4GB的地址空间,一个线性地址就是线性地址空间的一个绝对地址。
103 |
104 | 那么,这几种地址之间如何转换?例如,当程序执行“mov ax,[1024]”这样一条指令时,在8086的实模式下,把某一段寄存器(比如ds)左移4位,然后与16位的偏移量(1024)相加后被直接送到内存总线上,这个相加后的地址就是内存单元的物理地址,而程序中的地址(例如ds:1024)就叫虚拟地址。在80X86保护模式下,这个虚拟地址不是被直接送到内存总线,而是被送到内存管理单元(MMU)。MMU由一个或一组芯片组成,其功能是把虚拟地址映射为物理地址,即进行地址转换,如图2.3所示。
105 |
106 |
107 |

108 |
109 |
110 |
111 | 其中,MMU是一种硬件电路,它包含两个部件,一个是分段部件,一个是分页部件,在本书中,我们把它们分别叫做分段机制和分页机制,以利于从逻辑的角度来理解硬件的实现机制。分段机制把一个虚拟地址转换为线性地址;接着,分页机制把一个线性地址转换为物理地址,如图2.4所示。
112 |
113 |
114 |

115 |
116 |
117 |
118 | 下一节对段机制和分页机制进行具体介绍。
119 |
--------------------------------------------------------------------------------
/第三章/section3_6.md:
--------------------------------------------------------------------------------
1 | ## **3.6 与进程相关的系统调用及其应用**
2 |
3 | 以上介绍的是操作系统内核对进程所进行的管理。下面从编程者的角度来说明开发人员如何利用内核提供的系统调用进行程序的开发。这一方面有助于读者对操作系统内部的进一步了解,另一方面有助于读者在应用程序的开发中充分利用系统调用来提升程序的质量。
4 |
5 | 前面我们已经对getpid 、fork、exec等系统调用有初步了解,下面在对这些系统调用进一步要了解的基础上,另外介绍几个系统调用。
6 |
7 | 此外,在这里要说明的是每个系统调用在返回时除了返回正常值外,还要返回错误码。Linux为了防止与正常的返回值混淆,并不直接返回错误码,而是将错误码放入一个名为errno的全局变量中。如果一个系统调用失败,就可以读出errno的值来确定问题所在。errno不同数值所代表的错误消息定义在errno.h中,可以通过命令"man 3 errno"来察看它们。
8 |
9 | ### **3.6.1 fork系统调用**
10 |
11 | 如前所述,fork系统调用的作用是复制一个进程。当一个进程调用它时,就出现两个几乎一模一样的进程,我们也由此得到了一个新进程。据说fork的名字就是来源于这个与叉子的形状颇有几分相似的工作流程。
12 |
13 | 我们回头看2.1.4节的进程举例。再次看到这个程序的时候,必须明确知道,在语句pid=fork()之前,只有一个进程在执行这段代码。当执行到fork()时,就陷入内核,具体说就是执行内核中的do_fork()函数。于是,在这条语句之后,就变成两个进程在执行了。
14 |
15 | fork可能有三种不同的返回值:
16 |
17 | (1) 父进程中,fork返回新创建子进程的进程ID;
18 |
19 | (2) 子进程中,fork返回0;
20 |
21 | (3) 如果出现错误,fork返回一个负值;
22 |
23 | fork出错可能有两种原因:(1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。(2)系统内存不足,这时errno的值被设置为ENOMEM。fork系统调用出错的可能性很小,而且如果出错,一般都为第一种错误。如果出现第二种错误,说明系统已经没有可分配的内存,正处于崩溃的边缘,这种情况对Linux来说是很罕见的。
24 |
25 | ### **3.6.2 exec系统调用**
26 |
27 | 如果调用fork后,子进程和父进程几乎完全一样,而系统中产生新进程唯一的方法就是fork,那岂不是系统中所有的进程都要一模一样吗?那我们要执行新的应用程序时候怎么办?多数情况下,执行完fork后,子进程需要执行与父进程不同的代码。例如,对于一个shell,它首先从终端读取命令,然后创建一个子进程来执行该命令,shell进程等待子进程执行完毕,然后再读取下一条命令。为了等待子进程结束,父进程执行一条wait系统调用。该系统调用使父进程阻塞,直到它的任一个子进程结束。
28 | 现在再来看shell如何使用fork。当键入一条命令时,shell首先创建一个子进程。用户的命令就是由该子进程执行,这是通过调用exec系统调用实现的。一个高度简化的shell框架如下:
29 |
30 | while(TURE) /*TURE为1,无限循环*/
31 | read_command(command, parameters); /*从终端读取命令*/
32 | if (fork()!=0){ /*创建子进程*/
33 | /* Parent code*/
34 | wait(NULL); /*等待子进程结束*/
35 | } else {
36 | /*Child code*/
37 | exec(command, parameters,0); /*执行命令*/
38 | }
39 | }
40 |
41 | wait系统调用等待子进程的结束。Exec有三个参数:待执行的文件名、指向参数数组的指针和指向环境变量的指针。系统提供了若干例程来简化这些参数的使用,包括execl, execv, execle和execve。本书采用exec来泛指所有这些系统调用。
42 |
43 | exec函数族的作用是根据指定的文件名找到可执行文件,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行的脚本文件。
44 |
45 | 与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似“三十六计”中的“金蝉脱壳”。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。
46 |
47 | 现在我们应该明白Linux下是如何执行新程序的,每当有进程认为自己不能为系统和用户做出任何贡献了,他就可以发挥最后一点余热,调用任何一个exec,让自己以新的面貌重生;或者,更普遍的情况是,如果一个进程想执行另一个程序,它就可以fork出一个新进程,然后调用任何一个exec,这样看起来就好像通过执行应用程序而产生了一个新进程一样。
48 |
49 | 事实上第二种情况被应用得如此普遍,以至于Linux专门为其作了优化,这就是我们前面所说的“写时复制"技术,使得fork结束后并不立刻复制父进程的内容,而是到了真正实用的时候才复制,这样如果下一条语句是exec,它就不会白白作无用功了,也就提高了效率。
50 |
51 | ### **3.6.3 wait系统调用**
52 |
53 | 进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,释放其PCB,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
54 |
55 | **1.参数为空**
56 |
57 | wait的函数原型为:pid_t wait(int *status)
58 |
59 | 其中参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就像下面这样:
60 | pid = wait(NULL);
61 |
62 | 如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。
63 |
64 | 下面就让我们用一个例子来实战应用一下wait调用:
65 |
66 | /* wait1.c*/
67 | #include
68 | #include
69 | #include
70 | #include
71 | main()
72 | {
73 | pid_t pc, pr;
74 | pc=fork();
75 | if(pc<0) /* 如果出错 */
76 | printf("error ocurred!\n");
77 | else if(pc==0){ /* 如果是子进程 */
78 | printf("This is child process with pid of %d\n",getpid());
79 | sleep(10); /* 睡眠10秒钟 */
80 | }
81 | else{ /* 如果是父进程 */
82 | pr=wait(NULL); /* 在这里等待 */
83 | printf("I catched a child process with pid of %d\n",pr);
84 | }
85 | exit(0);
86 | }
87 |
88 | 编译并运行该程序:
89 |
90 | $ cc wait1.c -o wait1
91 | $ ./wait1
92 | This is child process with pid of 1508
93 | I catched a child process with pid of 1508
94 |
95 | 运行时可以明显注意到,在第2行结果打印出来前有10秒钟的等待时间,这就是我们设定的让子进程睡眠的时间,只有子进程从睡眠中苏醒过来,它才能正常退出,也就才能被父进程捕捉到。其实这里我们不管设定子进程睡眠的时间有多长,父进程都会一直等待下去,读者如果有兴趣的话,可以试着自己修改一下这个数值,看看会出现怎样的结果。
96 |
97 | 另外,某些时候,父进程要等待子进程算出结果后才进行下一步的运算,或者子进程的功能是为父进程提供了下一步执行的先决条件(例如子进程建立文件,而父进程写入数据),此时父进程就必须在某一个位置停下来,等待子进程运行结束,而如果父进程不等待而直接执行下去的话,可以想见,会出现极大的混乱。这种情况称为进程之间的同步,更准确地说,这是进程同步的一种特例。进程同步就是要协调好两个以上的进程,使之以安排好地次序依次执行。解决进程同步问题有更通用的方法,我们将在以后介绍,但对于我们假设的这种情况,则完全可以用wait系统调用简单地予以解决。
98 |
99 | 前面这段程序还说明,当fork调用成功后,父子进程各做各的事情,但当父进程的工作告一段落,需要用到子进程的结果时,它就调用wait等待,一直到子进程运行结束,然后利用子进程的结果继续执行,这样就圆满地解决了我们提出的进程同步问题。
100 |
101 | **2.参数不为空**
102 |
103 | 如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的(一个进程也可以被其他进程用信号结束),以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作,下面我们来说明其中最常用的两个:
104 |
105 | (1) WIFEXITED(status) :这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。(注意,这里的status为整数,而wait的参数为指向整数的指针)
106 |
107 | (2) WEXITSTATUS(status): 当WIFEXITED返回非零值时,这个宏用来就提取子进程的返回值。
108 |
109 | ### **3.6.4 exit系统调用**
110 |
111 | 从 exit的名字可以看出,这个系统调用是用来终止一个进程的。无论exit在程序中处于什么位置,只要执行到该系统调用就陷入内核,执行该系统调用对应的内核函数do_exit()。该函数回收与进程相关的各种内核数据结构,把进程的状态置为TASK_ZOMBIE,并把其所有的子进程都托付给init进程,最后调用schedule()函数,选择一个新的进程运行。
112 |
113 | exit的函数原型为:`void exit(int status);`
114 |
115 | exit系统调用带有一个整数类型的参数status,我们可以利用这个参数传递进程结束时的状态,比如说,该进程是正常结束的,还是出现某种意外而结束的,一般来说,0表示没有意外的正常结束;其他的数值表示进程非正常结束,出现了错误。我们在实际编程时,可以用wait系统调用接收子进程的返回值,从而针对不同的情况进行不同的处理。
116 |
117 | 这里要说明的是,在一个进程调用了exit之后,该进程并非马上就消失掉,而是仅仅变为僵尸状态。僵尸状态的进程(称其为僵死进程)是非常特殊的,虽然它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,但它的PCB还没有被释放。
118 |
119 | 僵尸进程的PCB中保存着对程序员和系统管理员非常重要的很多信息,比如,这个进程是怎么死亡的?是正常退出呢,还是出现了错误,还是被其它进程强迫退出的?其次,这个进程占用的总系统CPU时间和总用户CPU时间分别是多少?发生缺页中断的次数和收到信号的数目又是多少?这些信息都被存放在其PCB中。试想如果没有僵尸状态的进程,进程一退出,所有与之相关的信息都立刻归于无形,而此时程序员或系统管理员想知道这些信息时就束手无策了。
120 |
121 | 当一个进程调用exit已退出,但其父进程还没有调用系统调用wait对其进行收集之前的这段时间里,它会一直保持僵尸状态,利用这个特点,我们给出一个简单的小程序:
122 |
123 | #include
124 | #include
125 | main()
126 | {
127 | pid_t pid;
128 | pid=fork();
129 | if(pid<0)
130 | printf("error occurred!\n");
131 | else if(pid==0)
132 | exit(0);
133 | else
134 | { sleep(60); /* 睡眠60秒,这段时间里,父进程什么也干不了 */
135 | wait(NULL); /* 收集僵尸进程的信息 */
136 | }
137 | }
138 |
139 | sleep的作用是让进程睡眠指定的秒数,在这60秒内,子进程已经退出,而父进程正忙着睡觉,不可能对它进行收集,这样,我们就能保持子进程60秒的僵尸状态。
140 |
141 | 那么,我们如何收集这些信息,并终结这些僵尸的进程呢?这就要靠我们前面讲到的wait系统调用。其作用就是收集僵尸进程留下的信息,同时使这个进程彻底消失。
142 |
143 | ### **3.6.5进程的一生**
144 |
145 | 下面让我们用一些形象的比喻,来对进程短暂的一生作一个小小的总结:
146 |
147 | 随着一句fork,一个新进程呱呱落地,但这时它只是老进程的一个克隆。然后,随着exec,新进程脱胎换骨,离家独立,开始了独立工作的职业生涯。
148 |
149 | 人有生老病死,进程也一样,它可以是自然死亡,即运行到main函数的最后一个"}",从容地离我们而去;也可以是中途退场,退场有2种方式,一种是调用exit函数,一种是在main函数内使用return,无论哪一种方式,它都可以留下留言,放在返回值里保留下来;甚至它还可能被谋杀,被其它进程通过另外一些方式结束它的生命。
150 |
151 | 进程死掉以后,会留下一个空壳,wait站好最后一班岗,打扫战场,使其最终归于无形。这就是进程完整的一生。
152 |
153 |
--------------------------------------------------------------------------------
/第九章/section9_3.md:
--------------------------------------------------------------------------------
1 | ## 9.3 I/O空间的管理
2 |
3 | 设备通常会提供一组寄存器来控制设备、读写设备以及获取设备的状态。这些寄存器就是控制寄存器、数据寄存器和状态寄存器,它们可能位于I/O空间,也可能位于内存空间。当位于I/O空间时,通常被称为I/O端口,当位于内存空间时,对应的内存空间被称为I/O内存。
4 |
5 | ### 9.3.1 I/O端口和I/O内存
6 |
7 | 系统设计者为了对I/O编程提供统一的方法,每个设备的I/O端口都被组织成如图9.3所示的一组专用寄存器。CPU把要发给设备的命令写入控制寄存器,并从状态寄存器中读出表示设备内部状态的值。CPU还可以通过读取输入寄存器的内容从设备取得数据,也可以通过向输出寄存器中写入字节而把数据输出到设备。
8 |
9 |
10 |

11 |
12 |
13 |
14 | 图9.3 专用I/O端口
15 |
16 |
17 | 一般来说,一个外设的寄存器通常被连续地编址。CPU对外设I/O端口物理地址的编址方式有两种:一种是 **I/O端口**,另一种是 **I/O内存**。而具体采用哪一种则取决于CPU的体系结构。
18 |
19 | 有些体系结构的CPU(如PowerPC、m68k等)通常只实现一个物理地址空间(RAM)。在这种情况下,外设I/O端口的物理地址就被映射到CPU的单一物理地址空间中,而成为内存的一部分。此时,CPU可以象访问一个内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。这就是所谓的“**I/O内存**”。
20 |
21 |
22 | 而另外一些体系结构的CPU(典型地如X86)则为外设专门实现了一个单独地地址空间,称为“I/O端口空间”。这是一个与CPU的内存物理地址空间不同的地址空间,所有外设的I/O端口均在这一空间中进行编址。CPU通过设立专门的I/O指令(如X86的IN和OUT指令)来访问这一空间中的地址单元(也即I/O端口)。这就是所谓的“ **I/O端口**”。与内存物理地址空间相比,I/O地址空间通常都比较小,如x86CPU的I/O空间就只有64KB(0-0xffff)。这是“I/O端口”的一个主要缺点。
23 |
24 | ### 9.3.2 I/O资源管理
25 | Linux将基于I/O端口和I/O内存的映射方式通称为“I/O区域”(I/O Region)。在对I/O区域的管理讨论之前,我们首先来分析一下Linux是如何实现“I/O资源”这一抽象概念的。
26 |
27 | 1.Linux对I/O资源的描述
28 |
29 | Linux设计了一个通用的数据结构resource来描述各种I/O资源(如I/O端口、I/O内存、DMA和IRQ等)。该结构定义在include/linux/ioport.h头文件中:
30 |
31 | ``` c
32 | struct resource {
33 | resource_size_t start; /*资源拥有者的名字*/
34 | resource_size_t end; /*资源范围的开始*/
35 | const char *name; /*资源范围的结束*/
36 | unsigned long flags; /*各种标志*/
37 | struct resource *parent, *sibling, *child; /*指向资源树中父、兄以及孩子的指针*/
38 | };
39 | ```
40 |
41 | 资源表示某个实体的一部分,这部分被互斥地分配给设备驱动程序。所有的同种资源都插入到一个树型数据结构(父亲、兄弟和孩子)中;
42 | 节点的孩子被收集在一个链表中,其第一个元素由child指向,sibling字段指向链表中的下一个节点。
43 |
44 | 为什么使用树?例如,考虑一下IDE硬盘接口所使用的I/O端口地址-比如说从0xf000到0xf00f。那么,start字段为0xf000,end字段为0xf00f的,控制器的名字存放在name字段中,这就是一颗资源树。但是,IDE设备驱动程序需要记住另外的信息,比如IDE主盘使用0xf000到 0xf007的子范围,从盘使用0xf008到0xf00f的子范围。为了做到这点,设备驱动程序把两个子范围对应的孩子插入到从0xf000到0xf00f的整个范围对应的资源下。
45 |
46 | Linux在kernel/resource.c文件中定义了全局变量ioport\_resource和iomem\_resource,它们来分别描述基于I/O端口的整个I/O端口空间和基于I/O内存的整个I/O内存资源空间,其定义如下:
47 | ```c
48 | struct resource ioport_resource = {
49 | .name = "PCI IO",
50 | .start = 0,
51 | .end = IO_SPACE_LIMIT,
52 | .flags = IORESOURCE_IO,
53 | };
54 |
55 | struct resource iomem_resource = {
56 | .name = "PCI mem",
57 | .start = 0,
58 | .end = -1,
59 | .flags = IORESOURCE_MEM,
60 | };
61 | ```
62 |
63 |
64 | 其中,宏IO\_SPACE\_LIMIT表示整个I/O空间的大小,对于X86平台而言,它是0xffff(定义在arch/x86/include/asm/io.h头文件中)。
65 |
66 | 任何设备驱动程序都可以使用下面三个函数申请、分配和释放资源,传递给它们的参数为资源树的根节点和要插入的新资源数据结构的地址:
67 |
68 | request\_resource( ):把一个给定范围分配给一个I/O设备。
69 |
70 | allocate\_resource( ):在资源树中寻找一个给定大小和排列方式的可用范围;若存在,将这个范围分配给一个I/O设备(主要由PCI设备驱动程序使用,可以使用任意的端口号和主板上的内存地址对其进行配置)。
71 |
72 | release\_resource( ):释放以前分配给I/O设备的给定范围。
73 |
74 | 当前分配给I/O设备的所有I/O地址的树都可以从/proc/ioports文件中查看,例如
75 |
76 | ```
77 | $ cat /proc/ioports
78 | ```
79 |
80 | 2.管理I/O 区域资源
81 |
82 | Linux将基于I/O端口和基于I/O内存的资源统称为“I/O区域”。I/O
83 | 区域仍然是一种I/O资源,因此它仍然可以用resource结构类型来描述。Linux在头文件include/linux/ioport.h中定义了三个对I/O区域进行操作的接口函数:
84 |
85 | \_\_request\_region():I/O 区域的分配
86 |
87 | \_\_release\_region():I/O 区域的释放
88 |
89 | \_\_check\_region():检查指定的I/O 区域是否已被占用
90 |
91 |
92 | 3.管理I/O端口资源
93 |
94 | 采用I/O端口的X86处理器为外设实现了一个单独的地址空间,也即“I/O空间”或称为“I/O端口空间”,其大小是64KB(0x0000-0xffff)。Linux在其所支持的所有平台上都实现了“I/O端口空间”这一概念。
95 |
96 |
97 | 由于I/O空间非常小,因此即使外设总线有一个单独的I/O端口空间,却也不是所有的外设都将其I/O端口(指寄存器)映射到“I/O端口空间”中。比如,大多数PCI卡都通过内存映射方式来将其I/O端口或外设内存映射到CPU的内存物理地址空间中。而老式的ISA卡通常将其I/O端口映射到I/O端口空间中。
98 |
99 | Linux是基于“I/O区域”这一概念来实现对I/O端口资源的管理的。对I/O端口空间的操作基于I/O区域的操作函数\_\_xxx\_region(),Linux在头文件include/linux/ioport.h中定义了三个对I/O端口空间进行操作的接口函数:
100 |
101 | request\_region():请求在I/O端口空间中分配指定范围的I/O端口资源。
102 |
103 | check\_region():检查I/O端口空间中的指定I/O端口资源是否已被占用。
104 |
105 | release\_region():释放I/O端口空间中的指定I/O端口资源。
106 |
107 |
108 | 4.管理I/O内存资源
109 |
110 |
111 | 基于I/O区域的操作函数\_\_xxx\_region(),Linux在头文件include/linux/ioport.h中定义了三个对I/O内存资源进行操作的接口:
112 |
113 | request_mem_region():请求分配指定的 I/O 内存资源。
114 |
115 | check_mem_region():检查指定的 I/O 内存资源是否已被占用。
116 |
117 | release_mem_region():释放指定的 I/O 内存资源。
118 |
119 | ### 9.3.3 访问I/O端口空间
120 |
121 | 在驱动程序请求了I/O端口空间中的端口资源后,它就可以通过CPU的IO指令来读写这些I/O端口。在读写I/O端口时要注意的一点就是,大多数平台都区分8位、16位和32位的端口。
122 |
123 | inb() outb() inw() outw() inl() outl()
124 |
125 | inb()的原型为:
126 | ```c
127 | unsigned char inb(unsigned port);
128 | ```
129 |
130 | port参数指定I/O端口空间中的端口地址。在大多数平台上(如x86)它都是unsigned short类型的,其它的一些平台上则是unsigned int类型的。显然,端口地址的类型是由I/O端口空间的大小来决定的。
131 |
132 |
133 | 除了上述这些I/O操作外,某些CPU也支持对某个I/O端口进行连续的读写操作,也即对单个I/O端口读或写一系列字节、字或32位整数,这就是所谓的“串I/O指令”。这种指令在速度上显然要比用循环来实现同样的功能快得多。
134 |
135 | ```
136 | insb() outsb() insw() outw() insl() outsl()
137 | ```
138 |
139 | 另外,在一些平台上(典型地如X86),对于老式总线(如ISA)上的慢速外设来说,如果CPU读写其I/O端口的速度太快,那就可能会发生丢失数据的现象。对于这个问题的解决方法就是在两次连续的I/O操作之间插入一段微小的时延,以便等待慢速外设。这就是所谓的“暂停I/O”。
140 |
141 | 对于暂停I/O,Linux也在io.h头文件中定义了它的I/O读写函数,而且都以XXX\_p命名,比如:inb\_p()、outb\_p()等等。
142 |
143 | ### 9.3.4访问I/O内存资源
144 |
145 | 用于I/O指令的“地址空间”相对来说是很小的。事实上,现在x86的I/O地址空间已经非常拥挤。但是,随着计算机技术的发展,这种只能对外设中的几个寄存器进行操作的方式,已经无法满足实际需要了。而实际上,需求在不断发生变化,例如,在PC上可以插上一块图形卡,有2MB的存储空间,甚至可能还带有ROM,其中装有可执行代码。自从PCI总线出现后,无论CPU的设计采用I/O端口方式,还是I/O内存方式,都必须将外设卡上的存储器映射到内存空间,实际上是采用了虚存空间的手段,这样的映射是通过ioremap()来建立的,该函数的原型为:
146 | ```c
147 | void * ioremap(resource_size_t offset, unsigned long size);
148 | ```
149 | 其中参数的含义为:
150 | offset:I/O设备上的一块物理内存的起始地址;
151 |
152 | size:要映射的空间的大小;
153 |
154 | ioremap()与第四章讲的vmalloc()类似,也需要建立新的页表,但并不进行vmalloc()所执行的内存分配(因为I/O物理内存已存在)。ioremap()返回一个特殊的虚拟地址,该地址可用来存取特定的物理地址范围。通过ioremap()获得的虚拟地址应该被iounmap()函数释放,其原型如下:
155 |
156 | ```c
157 | void iounmap(void *addr);
158 | ```
159 |
160 | 在调用ioremap()之前,首先要调用requset\_mem\_region()函数申请资源,该函数的原型为:
161 |
162 | ```c
163 | struct resource *requset_mem_region(unsigned long start, unsigned long len,char *name);
164 | ```
165 |
166 | 这个函数从内核申请len个内存地址(在3G\~4G之间的虚地址),而这里的start为I/O物理地址,name为设备的名称。(注意,如果分配成功,则返回非NULL,否则,返回NULL。)另外,可以通过/proc/iomem查看系统给各种设备的内存范围。
167 |
168 | 在将I/O内存的物理地址映射成内核虚地址后,理论上讲我们就可以象读写内存那样直接读写I/O内存。但是,由于在某些平台上,对
169 | I/O内存和系统内存有不同的访问处理,因此为了确保跨平台的兼容性,Linux实现了一系列读写I/O内存的函数,这些函数在不同的平台上有不同的实现。但在x86平台上,读写I/O内存与读写内存无任何差别,相关函数如下:
170 |
171 | readb() readw() readl():读I/O内存
172 |
173 | writeb() writew() writel():写I/O内存
174 |
175 | memset\_io() memcpy\_fromio() memcpytoio():拷贝I/O内存
176 |
177 | 为了保证驱动程序的跨平台的可移植性,建议开发者应该使用上面的函数来访问I/O内存。
178 |
--------------------------------------------------------------------------------
/第三章/section3_2.md:
--------------------------------------------------------------------------------
1 | ## **3.2 Linux系统中的进程控制块**
2 |
3 | 操作系统为了对进程管理,就必须对每个进程在其生命周期内涉及的所有事情进行全面的描述。例如,进程当前处于什么状态,它的优先级是什么,它是正在CPU上运行还是因某些事件而被阻塞,给它分配了什么样的地址空间,允许它访问哪个文件等等。所有这些信息在内核中用一个结构体来描述——Linux中把对进程的描述结构叫做task_struct:
4 |
5 | >
6 | > struct task_struct {
7 |
8 | > …
9 | >
10 | > …
11 |
12 | > }
13 |
14 |
15 | 传统上,这样的数据结构被叫做**进程控制块PCB**(process control blaock)。Linux中PCB是一个相当庞大的结构体,我们将它的所有域按其功能可分为一下几类:
16 |
17 | (1) 状态信息-描述进程动态的变化,如就绪态,等待态,僵死态等。
18 |
19 | (2) 链接信息-描述进程的亲属关系,如祖父进程,父进程,养父进程,子进程,兄进程,孙进程等。
20 |
21 | (3) 各种标识符-用简单数字对进程进行标识,如进程标识符,用户标识符等
22 |
23 | (4) 进程间通信信息-描述多个进程在同一任务上协作工作,如管道,消息队列,共享内存,套接字等
24 |
25 | (5) 时间和定时器信息-描述进程在生存周期内使用CPU时间的统计、计费等信息等。
26 |
27 | (6) 调度信息-描述进程优先级、调度策略等信息,如静态优先级,动态优先级,时间片轮转、高优先级优先以及多级反馈队列等调度策略。
28 |
29 | (7) 文件系统信息-对进程使用文件情况进行记录,如文件描述符,系统打开文件表,用户打开文件表等。
30 |
31 | (8) 虚拟内存信息-描述每个进程拥有的地址空间,也就是进程编译连接后形成的空间。
32 |
33 | (9) 处理器环境信息-描述进程的执行环境(处理器的各种寄存器及堆栈等),这是体现进程动态变化最主要的场景。
34 |
35 | 在进程的整个生命周期中,系统(也就是内核)总是通过PCB对进程进行控制的,也就是说,系统是根据进程的PCB而不是别的什么而感知进程的存在的。例如,当内核要调度某进程执行时,要从该进程的PCB中查出其运行状态和优先级;在某进程被选中投入运行时,要从其PCB中取出其处理机环境信息,恢复其运行现场;进程在执行过程中,当需要和与之合作的进程实现同步、通信或访问文件时,也要访问PCB。当进程因某种原因而暂停执行时,又需将其断点的处理机环境保存在PCB中。所以说,PCB是进程存在和运行的唯一标志。
36 |
37 | 当系统创建一个新的进程时,就为它建立一个PCB;进程结束时又收回其PCB,进程随之也消亡。PCB是内核中被频繁读写的数据结构,故应常驻内存。
38 |
39 | 进程的另外一个名字是任务(task)。Linux内核通常把进程也叫任务,在本书中我们交替使用这两个术语。另外,在Linux内核中,对进程和线程也不做明显的区别,也就是说,进程和线程的实现采取了同样的方式。
40 |
41 | 下面主要讨论PCB中进程的状态、标识符和进程间的父/子关系。
42 |
43 | ### **3.2.1 进程状态**
44 |
45 | 从操作系统的原理知道,进程一般有三种基本状态-执行,就绪和等待态,但是在具体操作系统的实现中,设计者根据具体需要可以设置不同的状态。在Linux的设计中,考虑到任一时刻在CPU上运行的进程最多只有一个,而准备运行的进程可能有若干个,为了管理上的方便,把就绪态和运行态合并为一个状态叫就绪态,这样系统把处于就绪态的进程放在一个队列中,调度程序从这个队列中选中一个进程投入运行。而等待态又被划分为两种,除此之外,还有暂停状态和僵死状态,这几个主要状态描述如下:
46 |
47 | **就绪态(TASK_RUNNING):**正在运行或准备运行,处于这个状态的所有进程组成就绪队列。
48 |
49 | **睡眠(或等待)态:**分为浅度睡眠态和深度睡眠态
50 |
51 | **浅度睡眠态(TASK_INTERRUPTIBLE):**进程正在睡眠(被阻塞),等待资源有效时被唤醒,不仅如此,也可以由其他进程通过信号 或时钟中断唤醒。
52 |
53 | **深度睡眠态(TASK_UNINTERRUPTIBLE):** 与前一个状态类似,但其它进程发来的信号和时钟中断并不能打断它的熟睡。
54 |
55 | **暂停状态(TASK_STOPPED):**进程暂停执行,比如,当进程接收到如下信号后,进入暂停状态:
56 |
57 | SIGSTOP-停止进程执行
58 |
59 | SIGTSTP-从终端发来信号停止进程
60 |
61 | SIGTTIN-来自键盘的中断
62 |
63 | SIGTTOU-后台进程请求输出
64 |
65 | **僵死状态(TASK_ZOMBIE):**进程执行结束但尚未消亡的一种状态。此时,进程已经结束且释放大部分资源,但尚未释放其PCB。
66 |
67 | 图3.5 给出Linux进程状态的转换及其所调用的内核函数。
68 |
69 |
70 |

71 |
72 |
73 |
74 | 如图所示,通过fork()创建的进程处于就绪状态,其PCB进入就绪队列。如果调度程序schedule()运行,则从就绪队列中选择一进程投入运行而占有CPU。在进程执行的过程中,因为输入输出等原因调用interruptible_sleep_on()或者sleep_on(),则进程进入浅度睡眠或者深度睡眠。因为进程进入睡眠状态放弃CPU,因此也调用了调度程序schedule()重新从就绪队列中调用一个进程运行。以此类推,读者可以自己理解图中调用其他函数的意义。
75 |
76 | 在taks_struct结构中(定义于shed.h),状态域定义为:
77 |
78 | Struct tast_struct{
79 | volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
80 | …
81 | }
82 |
83 | 其中volatile是一种类型修饰符,它告诉编译程序不必优化,从内存读取数据而不是寄存器,以确保状态的变化能及时地反映出来。
84 |
85 | 对每个具体的状态赋予一个常量,有些状态是在新的内核中增加的:
86 |
87 | #define TASK_RUNNING 0
88 | #define TASK_INTERRUPTIBLE 1
89 | #define TASK_UNINTERRUPTIBLE 2
90 | #define __TASK_STOPPED 4
91 | #define __TASK_TRACED 8/* 由调试程序暂停进程的执行 */
92 | /* in tsk->exit_state */
93 | #define EXIT_ZOMBIE 16
94 | #define EXIT_DEAD 32 /*最终状态,进程将被彻底删除,但需要父进程来回收*/
95 | /* in tsk->state again */
96 | #define TASK_DEAD 64 /*与EXIT_DEAD类似,但不需要父进程回收*/
97 | #define TASK_WAKEKILL 128/*接收到致命信号时唤醒进程,即使深度睡眠*/
98 |
99 | 也可以使用ps命令查看进程的状态
100 |
101 | ### **3.2.2 进程标识符**
102 |
103 | 每个进程有进程标识符、用户标识符、组标识符。
104 |
105 | 不管对内核还是普通用户来说,怎么用一种简单的方式识别不同的进程呢?这就引入了进程标识符(PID),每个进程都有一个唯一的标识符,内核通过这个标识符来识别不同的进程,同时,进程标识符PID也是内核提供给用户程序的接口,用户程序通过PID对进程发号施令。PID是32位的无符号整数,它被顺序编号:新创建进程的PID通常是前一个进程的PID加1。在Linux上允许的最大PID号是由变量pid_max来指定,可以在内核编译的配置界面里配置0x1000和0x8000两种值,即在4096以内或是32768以内。当内核在系统中创建进程的PID大于这个值时,就必须重新开始使用已闲置的PID号。
106 |
107 | #define PID_MAX_DEFAULT (CONFIG_BASE_SMALL ? 0x1000 : 0x8000)
108 | int pid_max = PID_MAX_DEFAULT;
109 |
110 | 这个最大值很重要,因为它实际上就是系统中允许同时存在的进程的最大数目。尽管最大值对于一般的桌面系统足够用了,但是大型服务器可能需要更多进程。这个值越小,转一圈就越快。如果确实需要的话,可以不考虑与老式系统的兼容,由系统管理员通过修改`/proc/sys/kernel/pid_max`来提高上限。可以通过cat命令查看系统pid_max的值。
111 |
112 | $ cat /proc/sys/kernel/pid_max
113 | $32768
114 |
115 | 另外,每个进程都属于某个用户组。task_struct结构中定义有用户标识符UID(User Identifier)和组标识符GID(Group Identifier)。它们同样是简单的数字,这两种标识符用于系统的安全控制。系统通过这两种标识符控制进程对系统中文件和设备的访问。
116 |
117 | ### **3.2.3进程之间的亲属关系**
118 |
119 | 系统创建的进程具有父/子关系。因为一个进程能创建几个子进程,而子进程之间有兄弟关系。在PCB中引入几个域来表示这些关系。如前说述,进程1(init)是所有进程的祖先,系统中的进程形成一颗进程树。为了描述进程之间的父/子及兄弟关系,在进程的PCB中就要引入几个域。假设P表示一个进程,首先要有一个域描述它的父进程(parent);其次,有一个域描述P的子进程,因为子进程不止一个,因此让这个域指向年龄最小的子进程(child);最后,P可能有兄弟,于是用一个域描述P的长兄进程(old sibling),一个域描述P的弟进程(younger sibling)
120 |
121 | 上面通过对进程状态、标识符及亲属关系的描述,我们可以把这些域描述如下:
122 |
123 | struct task_struct{
124 | volatile long state; /*进程状态*/
125 | int pid,uid,gid; /*一些标识符*/
126 | struct task_struct *real_parent; /* 真正创建当前进程的进程,相当于亲生父亲*/
127 | struct task_struct *parent; /* 相当于养父*/
128 | struct list_head children; /* 子进程链表 */
129 | struct list_head sibling; /* 兄弟进程链表 */
130 | struct task_struct *group_leader; /* 线程组的头进程 */
131 | …
132 | }
133 |
134 | 这里说明一点是,一个进程可能有两个父亲,一个为亲生父亲,一个为养父。因为父进程有可能在子进程之前销毁,就得给子进程重新找个养父,但大多数情况下,生父和养父是相同的,如图3.6
135 |
136 |
137 |

138 |
139 |
140 |
141 | ### **3.2.4进程控制块的存放**
142 |
143 | 当创建一个新的进程时,内核首先要为其分配一个PCB(task_struct结构)。那么,这个PCB存放在何处?怎样找到PCB?
144 |
145 | 每当进程从用户态进入内核态后都要使用栈,这个栈叫做进程的内核栈。当进程一进入内核态,CPU就自动设置该进程的内核栈,这个栈位于内核的数据段。为了节省空间,Linux把内核栈和一个紧挨近PCB的小数据结构thread_info放在一起,占用8KB的内存区,如图3.7所示:
146 |
147 |
148 |

149 |
150 |
151 |
152 | 在Intel系统中,栈起始于末端,并朝这个内存区开始的方向增长。从用户态刚切换到内核态以后,进程的内核栈总是空的,因此,堆栈寄存器esp直接指向这个内存区的顶端。 在图3.7中,从用户态切换到内核态后,只要把数据写进栈中,堆栈寄存器的值就超箭头方向递减,p表示thread_info的起始地址。而task是thread_info的第一个数据项,所以只要找到thread_info就很容易找到当前运行的task_struct了。
153 |
154 | C语言使用下列的联合结构表示这样一个混合结构:
155 |
156 | union thread_union {
157 | struct thread_info thread_info;
158 | unsigned long stack[THREAD_SIZE/sizeof(long)];/*大小一般是8KB,但也可以配置为4KB。本书以8KB叙述。*/
159 | };
160 |
161 | 从这个结构可以看出,内核栈占8KB的内存区。实际上,进程的PCB所占的内存是由内核动态分配的,更确切地说,内核根本不给PCB分配内存,而仅仅给内核栈分配8K的内存,并把其中的一部分让给PCB使用。
162 |
163 | 在x86上,其中 thread_info结构在文件中定义如下:
164 |
165 | struct thread_info{
166 | struct task_sturct *task;
167 | struct exec_domain *exec_domain;
168 | …
169 | };
170 |
171 | thread_info结构并不代表线程相关信息,而是和硬件关系更紧密的一些数据。thread_info 和task_struct结构中都有一个域指向对方,因此是一一对应的关系。之所以定义一个thread_info结构,原因之一可能是,进程控制块的所有成员中最被频繁引用的是thread_info。另一个原因可能是,随着Linux版本的变化,进程控制块的内容越来越多,所需空间越来越大,这样就使得留给内核堆栈的空间变小,因此把部分进程控制块的内容移出这个空间,只保留访问频繁的thread_info。
172 |
173 | ### **3.2.5 当前进程**
174 |
175 | 从效率的观点来看,刚才所讲的thread_info结构与内核态堆栈放在一起的最大好处是,内核很容易从esp寄存器的值获得当前在CPU上正在运行的thread_info结构的地址。事实上,如果thread_union 结构长度是8K,则内核屏蔽掉esp的低13位有效位就可以获得thread_info结构的基地址;这由 current_thread_info( )函数来完成,它产生如下一些汇编指令:
176 |
177 | movl $0xffffe000, %ecx
178 | andl %esp, %ecx
179 | movl %ecx, p
180 |
181 | 这三条指令执行以后, p就指向进程的thread_info结构的指针。
182 |
183 | 进程最常用的是其task_struct结构的地址而不是thread_info结构的地址,为了获得当前在CPU上运行进程的PCB指针,内核要调用current宏,该宏本质上等价于`current_thread_info()->task`。
184 |
185 | 可以把current作为全局变量来使用,例如,current->pid返回当前正在执行的进程的标识符。对于当前进程,可以通过下面的代码获得其父进程的PCB
186 |
187 | > struct task_struct *my_parent=current->parent;
188 |
189 |
190 |
--------------------------------------------------------------------------------
/第九章/section9_2.md:
--------------------------------------------------------------------------------
1 | ## 9.2 设备驱动程序框架
2 |
3 | 由于设备种类繁多,相应的设备驱动程序也非常之多。尽管设备驱动程序是内核的一部分,但设备驱动程序的开发往往由很多人来完成,如业余编程高手、设备厂商等。为了让设备驱动程序的开发建立在规范的基础上,就必须在驱动程序和内核之间有一个严格定义和管理的接口,例如SVR4提出了DDI/DDK规范,其含义就是设备与驱动程序接口/设备驱动程序与内核接口(Device-Driver Interface/Driver-Kernel Interface)。通过这个规范,可以规范设备驱动程序与内核之间的接口。
4 |
5 | Linux的设备驱动程序与外接的接口与DDI/DKI规范相似,可以分为三部分:
6 |
7 | 1.驱动程序与内核的接口,这是通过数据结构file\_operations来完成的。
8 |
9 | 2.驱动程序与系统引导的接口,这部分利用驱动程序对设备进行初始化。
10 |
11 | 3.驱动程序与设备的接口,这部分描述了驱动程序如何与设备进行交互,这与具体设备密切相关。
12 |
13 | 其中第一点是驱动程序的核心部分,在此给予具体分析,至于后面两点,在具体的驱动程序中将会涉及到。
14 |
15 | 从上一章虚拟文件系统的介绍可以看出,设备是被纳入到文件系统的框架之下的,如第八章的图8.1。从上图9.1可以看出,用户进程是通过file结构与文件或者设备进行交互的,file结构的简化定义(具体定义于include/linux/fs.h)如下:
16 |
17 | ```c
18 | struct file {
19 | ...
20 | const struct file_operations *f_op;
21 | ...
22 | }
23 | ```
24 |
25 | 其中struct file\_operations是驱动程序主要关注的对象,其结构如下:
26 |
27 | ```c
28 | struct file_operations{
29 | int (*open) (struct inode *, struct file *); /*打开*/
30 | int (*close) (struct inode *, struct file *); /*关闭*/
31 | loff_t (*llseek) (struct file *, loff\_t, int); /*修改文件当前的读写位置*/
32 | ssize_t (*read) (struct file *, char *, size_t, loff_t *);
33 | /*从设备中同步读取数据*/
34 | ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
35 | /*向设备中发送数据*/
36 | int (*mmap) (struct file *, struct vm_area_struct *);
37 | /*将设备的内存映射到进程地址 空间*/
38 | int(*ioctl) (struct inode *, struct file *,unsigned int ,unsigned long);
39 | /*执行设备上的I/O控制命令*/
40 | unsigned int (*poll) (struct file *,struct poll_table_struct *);
41 | /*轮询,判断是否可以进行非阻塞的读取或者写入*/
42 | ...
43 | }
44 | ```
45 |
46 | 可以看出,file\_operations结构中对文件操作的函数只给出了定义,至于实现,就留给具体的驱动程序完成,下面为字符设备驱动程序打开、读、写以及I/O控制函数的模板:
47 |
48 | ```c
49 | static int char_open(struct inode *inode,struct file *file)
50 | {
51 | ...
52 | }
53 |
54 | ssize_t xxx_read(struct file *filp, char _user *buf, size_t
55 | count, loff_t *f_pos)
56 | {
57 | ...
58 | copy_to_user(buf,...,count);
59 | ...
60 | }
61 |
62 | ssize_t xxxwrite(struct file *filp, char _user *buf, size_t
63 | count, loff_t *f_pos)
64 | {
65 | ...
66 | copy_from_user(...,buf,count);
67 | ...
68 | }
69 |
70 | int xxx_ioctl(struct inode *inode,struct file *filp,unsigned int cmd,unsigned
71 | long arg)
72 | {
73 |
74 | switch(cmd)
75 | {
76 | case xxx_cmd1:
77 | ...
78 | break;
79 |
80 | case xxx_cmd2:
81 | ...
82 | break;
83 | default: /*不能支持的命令*/
84 | }
85 | return –enotty;
86 | }
87 | ```
88 |
89 | 在这些设备驱动函数中,filp是文件结构的指针,count是要读的字节数,f\_pos是读的位置相对于文件开头的偏移,buf是用户空间的内存地址,该地址在内核空间不能直接读写,因此要调用copy\_from\_user\(\)和copy\_to\_user\(\)函数进行跨空间的数据拷贝:
90 |
91 | 这两个函数的原型如下:
92 |
93 | ```c
94 | unsigned long copy_from_user(void *to,count void _user *from,unsigned
95 | long count);
96 |
97 | unsigned long copy_to_user(void _user *to,count void *from,unsigned long
98 | count);
99 | ```
100 |
101 | 通过以上的介绍,使读者对驱动程序的框架有一个初步了解,以以上框架为模板,看一个简单的字符驱动程序。
102 |
103 | 例 9-1 简单字符驱动程序mycdev.c
104 |
105 | ```c
106 | #include
107 | #include
108 | #include
109 | #include
110 | #include
111 | #include
112 | #include
113 | #include
114 | #include
115 | #include
116 | #include
117 |
118 | MODULE_LICENSE("GPL");
119 |
120 | #define MYCDEV_MAJOR 231 /*给定的主设备号*/
121 | #define MYCDEV_SIZE 1024
122 |
123 | static int mycdev_open(struct inode *inode, struct file *fp)
124 | {
125 | return 0;
126 | }
127 |
128 | static int mycdev_release(struct inode *inode, struct file *fp)
129 | {
130 | return 0;
131 | }
132 |
133 | static ssize_t mycdev_read(struct file *fp, char __user *buf, size_t size, loff_t *pos)
134 | {
135 | unsigned long p = *pos;
136 | unsigned int count = size;
137 | char kernel_buf[MYCDEV_SIZE] = "This is mycdev!";
138 | int i;
139 | if (p >= MYCDEV_SIZE)
140 | return -1;
141 | if (count > MYCDEV_SIZE)
142 | count = MYCDEV_SIZE - p;
143 | if (copy_to_user(buf, kernel_buf, count) != 0) {
144 | printk("read error!\n");
145 | return -1;
146 | }
147 |
148 | printk("reader: %d bytes was read...\n", count);
149 | return count;
150 | }
151 |
152 | static ssize_t mycdev_write(struct file *fp, const char __user *buf,
153 | size_t size, loff_t *pos)
154 | {
155 | return size;
156 | }
157 |
158 | /* 填充 mycdev的 file operation 结构*/
159 |
160 | static const struct file_operations mycdev_fops =
161 | {
162 | .owner = THIS_MODULE,
163 | .read = mycdev_read,
164 | .write = mycdev_write,
165 | .open = mycdev_open,
166 | .release = mycdev_release,
167 | };
168 |
169 | /*模块初始化函数*/
170 |
171 | static int __init mycdev_init(void)
172 | {
173 | int ret;
174 | printk("mycdev module is staring..\n");
175 | ret=register_chrdev(MYCDEV_MAJOR,"my dev",&mycdev_fops);
176 | /*注册驱动程序*/
177 | if (ret<0){
178 | printk("register failed..\n");
179 | return 0;
180 | }
181 | else{
182 | printk("register success..\n");
183 | }
184 | return 0;
185 | }
186 |
187 | /*模块卸载函数*/
188 |
189 | static void __exit mycdev_exit(void)
190 | {
191 | printk("mycdev module is leaving..\n");
192 | unregister_chrdev(MYCDEV_MAJOR,"my_cdev"); /*注销驱动程序*/
193 | }
194 |
195 | module_init(mycdev_init);
196 |
197 | module_exit(mycdev_exit);
198 | ```
199 |
200 | 上机调试该程序,并按如下步骤进行模块的插入:
201 |
202 | (1) 通过make编译mycdev.c模块,并把mycdev.ko插入到内核;
203 |
204 | (2) 通过cat /proc/devices 查看系统中未使用的字符设备主设备号,比如当前231未使用;
205 |
206 | (3) 创建设备文件结点:sudo mknod /dev/mycdev c 231 0;具体使用方法通过man mknod命令查看;
207 |
208 | (4) 修改设备文件权限:sudo chmod 777 /dev/mycdev;
209 |
210 | (5) 通过dmesg查看日志信息
211 |
212 | (6) 以上成功完成后,编写用户态测试程序;运行该程序查看结果;
213 |
214 | ```c
215 | #include
216 | #include
217 | #include
218 | #include
219 | #include
220 |
221 | int main()
222 | {
223 | int testdev;
224 | int i, ret;
225 | char buf[10];
226 |
227 | testdev = open("/dev/mycdev", O_RDWR);
228 |
229 | if (testdev == -1) {
230 | printf("cannot open file.\n");
231 | exit(1);
232 | }
233 |
234 | if (ret = read(testdev, buf, 10) < 10) {
235 | printf("read error!\n");
236 | exit(1);
237 | }
238 |
239 | for (i = 0; i < 10; i++)
240 | printf("%d\n", buf[i]);
241 | close(testdev);
242 | return 0;
243 | }
244 | ```
245 |
246 |
247 |
248 |
--------------------------------------------------------------------------------
/第一章/section1_6.md:
--------------------------------------------------------------------------------
1 | ## **1.6 Linux 内核中链表的实现及应用**
2 |
3 | 链表是Linux内核中最简单、最常用的一种数据结构。与数组相比,链表中可以动态插入或删除元素,在编译时不必知道要创建的元素个数。也因为链表中每个元素的创建时间各不相同,因此,它们在内存无需占用连续的内存单元,因为单元的不连续,因此各元素需要通过某种方式被链接在一起,于是,每个元素都包含一个指向下一个元素的指针,当有元素加入链表或者从链表中删除元素时,只需要调整下一个节点的指针就可以了。
4 |
5 | ### **1.6.1 链表的演化**
6 |
7 | 在C 语言中,一个基本的双向链表定义如下:
8 |
9 | struct my_list {
10 | void *mydata;
11 | struct my_list *next;
12 | struct my_list *prev;
13 | }
14 |
15 |
16 |
17 |

18 |
19 |
20 |
21 | 图1.6是一双链表,通过前趋(prev)和后继(next)两个指针域,就可以从两个方向遍历双链表,这使得遍历链表的代价减少。如果打乱前驱、后继的依赖关系,就可以构成"二叉树";如果再让首节点的前趋指向链表尾节点、尾节点的后继指向首节点(如图1.6中虚线部分),就构成了循环链表;如果设计更多的指针域,就可以构成各种复杂的树状数据结构。
22 |
23 | 如果减少一个指针域,就退化成单链表,如果只能对链表的首尾进行插入或删除操作,就演变为队结构,如果只能对链表的头进行插入或删除操作,就退化为栈结构。
24 |
25 | ### **1.6.2. 链表的定义和操作**
26 |
27 | 如上所述,在众多数据结构中,选取双向链表作为基本数据结构,并将其嵌入到其他数据结构中,从而可以演化出其他复杂数据结构。Linux内核实现方式与众不同,对链表给出了一种抽象的定义。
28 |
29 | **1. 链表的定义**
30 |
31 | struct list_head {
32 | struct list_head *next, *prev;
33 | };
34 |
35 | 这个不含数据域的链表,可以嵌入到任何结构中,例如可以按如下方式定义含有数据域的链表:
36 |
37 | struct my_list{
38 | void *mydata;
39 | struct list_head list;
40 | };
41 |
42 | 在此,进一步说明几点:
43 |
44 | 1)list域隐藏了链表的指针特性。
45 |
46 | 2)struct list_head可以位于结构的任何位置,可以给其起任何名字
47 |
48 | 3)在一个结构中可以有多个list 域
49 |
50 | 以struct list_head为基本对象,对链表进行插入、删除、合并以及遍历等各种操作。
51 |
52 | **2 链表的声明和初始化宏**
53 |
54 | 实际上, struct list_head只定义了链表节点,并没有专门定义链表头,那么一个链表结构是如何建立起来的?内核代码list.h中定义了两个宏:
55 |
56 | #define LIST_HEAD_INIT(name) { &(name), &(name) } /*仅初始化*/
57 |
58 | #define LIST_HEAD(name) \
59 | struct list_head name = LIST_HEAD_INIT(name) /*声明并初始化*/
60 |
61 | 如果我们要申明并初始化自己的链表头mylist,则直接调用LIST_HEAD:
62 |
63 | LIST_HEAD(mylist_head)
64 |
65 | 调用之后,mylist\_head的next、prev指针都初始化为指向自己,这样,我们就有了一个空链表,如何判断链表是否为空,自己写一下这个简单的函数list_empty ,也就是让头指针的next指向自己。
66 |
67 |
68 | **3 在链表中增加一个结点**
69 |
70 | list.h中增加节点的函数为:
71 |
72 | static inline void __list_add();
73 | static inline void list_add();
74 | static inline void list_add_tail();
75 |
76 | 在内核代码中,函数名前加两个下划线表示内部函数,第一个函数的具体代码如下:
77 |
78 | static inline void __list_add(struct list_head *new,
79 | struct list_head *prev,
80 | struct list_head *next)
81 | {
82 | next->prev = new;
83 | new->next = next;
84 | new->prev = prev;
85 | prev->next = new;
86 | }
87 |
88 | 调用这个内部函数以分别在链表头和尾增加节点:
89 |
90 | static inline void list_add(struct list_head *new, struct list_head *head)
91 | {
92 | __list_add(new, head, head->next);
93 | }
94 |
95 | 该函数向指定链表的head节点后插入new节点。因为链表是循环的,而且通常没有首尾节点的概念,所以可以将任何节点传递给head。但是如果传递最后一个元素传给head,那么该函数可以用来实现一个栈。
96 |
97 |
98 | static inline void list_add_tail(struct list_head *new, struct list_head *head)
99 | {
100 | __list_add(new, head->prev, head);
101 | }
102 |
103 |
104 | 该函数向指定链表的head节点前插入new节点。和list_add()函数类似,因为链表是环形的,而且可以将任何节点传递给head。但是如果传递第一个元素给head那么,该函数可以用来实现一个队列。
105 |
106 | 另外,对函数名前面的staitic inline 关键字给予说明。“static”加在函数前,表示这个函数是静态函数,所谓静态函数,实际上是对函数作用域的限制,指该函数的作用域仅局限于本文件。所以说,static具有信息隐藏的作用。而关键字"inline“加在函数前,说明这个函数对编译程序是可见的,也就是说,编译程序在调用这个函数时就立即展开该函数。所以,关键字inline 必须与函数定义体放在一起才能使函数成为内联。inline函数一般放在头文件中。
107 |
108 | 关于节点的删除,将结合后面的例子给予具体说明。至于节点的搬移和合并,读者自行分析,在此不一一讨论,下面主要分析链表的遍历。
109 |
110 |
111 | **4. 遍历链表**
112 |
113 | list.h中定义了如下遍历链表本的宏:
114 |
115 | #define list_for_each(pos, head) \
116 | for (pos = (head)->next; pos != (head); pos = pos->next)
117 |
118 | 这种遍历仅仅是找到一个个节点在链表中的偏移位置pos,如图1.7所示。
119 |
120 |
121 |

122 |
123 |
124 |
125 | 问题在于,如何通过pos获得节点的起始地址,从而可以引用节点中的域? 于是 list.h中定义了晦涩难懂的list_entry()宏:
126 |
127 | #define list_entry(ptr, type, member) \
128 | container_of(ptr, type, member)
129 |
130 |
131 |
132 | 指针ptr指向结构体type中的成员member;通过指针ptr,返回结构体type的起始地址,也就是list_entry返回指向type类型的指针,如图1.8所示。
133 |
134 |
135 |

136 |
137 |
138 |
139 | 进一步仔细分析list_entry()宏:
140 |
141 | ((unsigned long) &(type *)0)->member)把0地址转化为type结构的指针,然后获取该结构中member域的指针,也就是获得了member在type结构中的偏移量。其中(char *)(ptr)求出的是ptr的绝对地址,二者相减,于是得到type类型结构体的起始地址,如图1.9所示。
142 |
143 |
144 |

145 |
146 |
147 |
148 | 到此,我们对链表的实现机制有初步了解,更多的函数和实现查看[include/linux/list.h ](include/linux/list.h )中的代码。如何应用这些函数,下面举例说明。尽管list.h是内核代码中的头文件,但我们稍加修改后也可以把它移植到用户空间使用。
149 |
150 | ### **1.6.3 链表的应用**
151 |
152 | [linclude/linux/list.h](linclude/linux/list.h)中的函数和宏,是一组精心设计的接口,有比较完整的注释和清晰的思路,请详细阅读该文件中的代码。
153 |
154 | 下面编写一个linux 内核模块,用以创建、增加、删除和遍历一个双向链表。
155 |
156 | #include
157 | #include
158 | #include
159 | #include
160 |
161 | MODULE_LICENSE("GPL");
162 | MODULE_AUTHOR("XIYOU");
163 |
164 | #define N 10 //链表节点
165 | struct numlist {
166 | int num;//数据
167 | struct list_head list;//指向双联表前后节点的指针
168 | };
169 |
170 | struct numlist numhead;//头节点
171 |
172 | static int __init doublelist_init(void)
173 | {
174 | //初始化头节点
175 | struct numlist *listnode;//每次申请链表节点时所用的指针
176 | struct list_head *pos;
177 | struct numlist *p;
178 | int i;
179 |
180 | printk("doublelist is starting...\n");
181 | INIT_LIST_HEAD(&numhead.list);
182 |
183 | //建立N个节点,依次加入到链表当中
184 | for (i = 0; i < N; i++) {
185 | listnode = (struct numlist *)kmalloc(sizeof(struct numlist), GFP_KERNEL); // kmalloc()在内核空间申请内存,类似于malloc,参见第四章
186 | listnode->num = i+1;
187 | list_add_tail(&listnode->list, &numhead.list);
188 | printk("Node %d has added to the doublelist...\n", i+1);
189 | }
190 |
191 | //遍历链表
192 | i = 1;
193 | list_for_each(pos, &numhead.list) {
194 | p = list_entry(pos, struct numlist, list);
195 | printk("Node %d's data:%d\n", i, p->num);
196 | i++;
197 | }
198 |
199 | return 0;
200 | }
201 |
202 | static void __exit doublelist_exit(void)
203 | {
204 | struct list_head *pos, *n;
205 | struct numlist *p;
206 | int i;
207 |
208 | //依次删除N个节点
209 | i = 1;
210 | list_for_each_safe(pos, n, &numhead.list) { //为了安全删除节点而进行的遍历
211 | list_del(pos);//从双链表中删除当前节点
212 | p = list_entry(pos, struct numlist, list);//得到当前数据节点的首地址,即指针
213 | kfree(p);//释放该数据节点所占空间
214 | printk("Node %d has removed from the doublelist...\n", i++);
215 | }
216 | printk("doublelist is exiting..\n");
217 | }
218 |
219 | module_init(doublelist_init);
220 | module_exit(doublelist_exit);
221 |
222 | 说明:关于删除元素的安全性问题
223 |
224 | 在上面的代码中,为什么不调用list\_for\_each()宏而调用 list\_for\_each_safe()进行删除前的遍历?具体看删除函数的源代码:
225 |
226 | static inline void __list_del(struct list_head * prev, struct list_head * next)
227 | {
228 | next->prev = prev;
229 | prev->next = next;
230 | }
231 |
232 | static inline void list_del(struct list_head *entry)
233 | {
234 | __list_del(entry->prev, entry->next);
235 | entry->next = LIST_POISON1;
236 | entry->prev = LIST_POISON2;
237 | }
238 |
239 | 可以看出,当执行删除操作的时候, 被删除的节点的两个指针被指向一个固定的位置(LIST\_POISON1和LIST\_POISON2是内核空间的两个地址)。而list\_for\_each(pos, head)中的pos指针在遍历过程中向后移动,即pos = pos->next,如果执行了list\_del()操作,pos将指向这个固定位置的next, prev,而此时的next, prev没有任何指向了,必然出错。
240 |
241 | 而list\_for\_each_safe(p, n, head) 宏解决了上面的问题:
242 |
243 | #define list_for_each(pos, head) \
244 | for (pos = (head)->next; pos != (head); pos = pos->next)
245 |
246 | 它采用了一个同pos同样类型的指针n 来暂存将要被删除的节点指针pos,从而使得删除操作不影响pos指针!
247 |
248 | 这里要说明的是,哈希表也是链表的一种衍生,在list.h中也有相关的代码,在此不仔细讨论,读者可自行分析。
249 |
250 |
251 |
--------------------------------------------------------------------------------
/第八章/section8_5.md:
--------------------------------------------------------------------------------
1 | ## 8.5编写一个文件系统
2 |
3 | 文件系统比较庞杂,很难编写一个恰当的例子来演示文件系统的实现。如果写一个纯虚的文件系统(没有存储设备),因为虚文件系统不涉及I/O操作,缺少实现文件系统中至关重要的部分,因此必要性不大;如果写一个实际文件系统,但是涉及的东西太多,不容易让读者简明扼要的理解文件系统的实现。幸好,内核中提供的romfs文件系统是个非常理想的实例,它既有实际应用结构,也清晰明了,因此我们以romfs为实例分析文件系统的实现。
4 |
5 | ### 8.5.1 Linux文件系统的实现要素
6 |
7 | 编写新文件系统涉及一些基本对象,具体地说,需要建立“**一个结构四个操作表**”:
8 |
9 | - 文件系统类型结构(file\_system\_type)
10 |
11 | - 超级块操作表(super\_operations)
12 |
13 | - 索引节点操作表(inode\_operations)
14 |
15 | - 页缓冲区表(address\_space\_operations)
16 |
17 | - 文件操作表(file\_operations)
18 |
19 | 对上述几种结构的操作贯穿了文件系统实现的主要过程,理清晰这几种结构之间的关系是编写文件系统的基础,如图8.10所示,下来我们具体分析这几个结构和文件系统实现的要点。
20 |
21 |
22 |

23 |
24 |
25 |
26 | 图8.10 一个结构及四个操作表之间的关系
27 |
28 |
29 | 首先,必须建立一个文件系统类型(file\_system\_type)来描述文件系统,它含有文件系统的名称、类型标志以及get\_sb()等操作。当安装文件系统时,系统会对该文件系统进行注册,即填充file\_system\_type结构,然后调用get\_sb()函数来建立该文件系统的超级块。注意对于基于块的文件系统,如Ext4、romfs等,需要从文件系统的宿主设备读入超级块,然后在内存中建立对应的超级块,如果是虚文件系统(如proc文件系统),则不读取宿主设备的信息(因为它没有宿主设备),而是在现场创建一个超级块,这项任务也由get\_sb()完成。
30 |
31 | 可以说,超级块是一切文件操作的鼻祖,因为超级块是我们寻找索引节点的唯一源头。我们操作文件必然需要获得其对应的索引节点(或从宿主设备读取或现场建立),而获取索引节点是通过超级块操作表提供的read\_inode()函数完成的。同样操作索引节点的底层次任务,如创建一个索引节点、释放一个索引节点,也都是通过超级块操作表提供的有关函数完成的。所以超级块操作表(super\_operations)是第二个需要创建的数据结构。
32 |
33 | 除了派生或释放索引节点等操作是由超级块操作表中的函数完成外,索引节点还需要许多自操作函数,比如lookup()搜索索引节点,建立符号链接等,这些函数都包含在索引节点操作表中,因此索引节点操作表(inode\_operations)是第三个需要创建的数据结构。
34 |
35 | 为了提高文件系统的读写效率,Linux内核设计了I/O缓存机制。所有的数据无论出入都会经过系统管理的缓冲区,不过,对基于非块的文件系统则可跳过该机制。页缓冲区同样提供了页缓冲区操作表(address\_space\_operations),其中包含有readpage()、writepage()等函数负责对页缓冲区中的页进行读写等操作。
36 |
37 | 文件系统最终要和用户交互,这是通过文件操作表(file\_operations)完成的,该表中包含有关用户读写文件、打开、关闭、映射文件等用户接口。
38 |
39 | 一般来说,基于块的文件系统的实现都离不开以上5种数据结构。但根据文件系统的特点(如有的文件系统只可读、有的没有目录),并非要实现操作表中的全部函数,因为有的函数系统已经实现,而有的函数不必实现。
40 |
41 | ### 8.5.2 什么是Romfs文件系统
42 |
43 | Romfs是一种相对简单、占用空间较少的文件系统。
44 |
45 | 空间的节约来自于两个方面:首先内核支持Romfs文件系统比支持
46 | Ext4文件系统需要更少的代码;其次Romfs文件系统相对简单,在建立文件系统超级块(Superblock)需要更少的存储空间。
47 |
48 | Romfs是只读的文件系统,禁止写操作,因此系统同时需要虚拟盘(RAMDISK)支持临时文件和数据文件的存储。
49 |
50 | Romfs是在嵌入式设备上常用的一种文件系统,具备体积小,可靠性好,读取速度快等优点。同时支持目录,符号链接,硬链接,设备文件。但也有其局限性。Romfs是一种只读文件系统,同时由于Romfs本身设计上的原因,使得Romfs支持的最大文件不超过256M。Linux,uclinux都支持Romfs文件系统。除Romfs外,其它常用的嵌入式设备的文件系统还有CRAMFS,JFFS2等,它们各有特色。
51 |
52 | 下面我们来分析它的实现方法,为读者勾勒出编写新文件系统的思路和步骤。
53 |
54 | ### 8.5.3 Romfs文件系统布局与文件结构
55 |
56 | 设计一个文件系统首先要确定它的数据存储方式。不同的数据存储方式对文件系统占用空间,读写效率,查找速度等主要性能有极大影响。Romfs是一种只读的文件系统,它使用顺序存储方式,所有数据都是顺序存放的。因此Romfs中的数据一旦确定就无法修改,这是Romfs只能是一种只读文件系统的原因,它的数据存储方式决定了无法对Romfs进行写操作。由于采用了顺序存放策略,Romfs中每个文件的数据都能连续存放,读取过程中只需要一次寻址操作,进而就可以读入整块数据,因此Romfs中读取数据效率很高。
57 |
58 | 在Linux内核源代码的Document/fs/romfs中介绍了romfs文件系统的布局和文件结构,如图8.11所示。
59 |
60 |
61 |
62 |

63 |
64 |
65 |
66 | 图 8.11 romfs文件系统布局
67 |
68 |
69 | 从图可以看出,文件系统中每部分信息都是16字节对齐的,也就是说存储的偏移量必须最后4位为0,这样作是为了提高访问速度。如果信息不足时,需要填充0以保证所有信息的开始位置都以16来对齐。
70 |
71 | 文件系统的开始8个字节存储文件系统的ASCII形式的名称,在这里是”-rom1fs-”;接着4个字节记录文件大小;然后的4个字节存储的是文件系统开始处512字节的检验和;接下来是卷名;最后是第一个文件的文件头,从这里开始依次存储的就是文件本身的信息了。
72 |
73 | Romfs的文件结构如图8.12所示。
74 |
75 |
76 |
77 |

78 |
79 |
80 |
81 | 图8.12 Romfs的文件结构
82 |
83 |
84 | ### 8.5.4具体实现的对象
85 |
86 | 针对文件系统布局和文件结构,Romfs文件系统定义了一个磁盘超级块结构和文件的inode结构,磁盘超级块的结构定义如下:
87 |
88 | ```c
89 | struct romfs_super_block {
90 | __be32 word0;
91 | __be32 word1;
92 | __be32 size;
93 | __be32 checksum;
94 | char name[0];
95 | };
96 | ```
97 |
98 | 该结构用于识别整个Romfs文件系统,大小为512字节。word0初始值为'-','r','o','m',word1初始值为'-','1','f','s',通过这两个字操作系统确定这是一个Romfs文件系统。size字段用于记录整个文件系统的大小,理论上Romfs大小最多可以达到4G。checksum是前512字节的校验和,用于确认整个文件系统结构数据的正确性。前面4个字段占用了16字节,剩下的都可以用作文件系统的卷名,如果整个首部不足512字节便用0填充,以保证首部符合16字节对齐的规则。
99 | ```c
100 | struct romfs_inode {
101 | __be32 next;
102 | __be32 spec;
103 | __be32 size;
104 | __be32 checksum;
105 | char name[0];
106 | };
107 | ```
108 |
109 | next字段是下一个文件的偏移地址,该地址的后4位是保留的,用于记录文件模式信息,其中前两位为文件类型,后两位则标识该文件是否为可执行文件。因此Romfs用于文件寻址的字段实际上只有28bit,所以Romfs中文件大小不能超过256M。spec字段用于标识该文件类型。目前Romfs支持的文件类型包括普通文件,目录文件,符号链接,块设备和字符设备文件。size是文件大小,checksum是校验和,校验内容包括文件名,填充字段。name是文件名首地址,文件长度必须保证16字节对齐,不足的部分可以用0填充。
110 |
111 | 上述两种结构分别描述了文件系统结构与文件结构,它们将在内核装配超级块对象和索引节点对象时被使用。
112 |
113 | Romfs文件系统首先要定义的对象是文件系统类型romfs\_fs\_type:
114 | ```c
115 | static DECLARE_FSTYPE_DEV(romfs_fs_type, "romfs", romfs_read_super);
116 | ```
117 |
118 | DECLARE\_FSTYPE\_DEV是内核定义的一个宏,用来建立file\_system\_type结构。从上面的申明可以看出,file\_system\_type结构的类型变量为romfs\_fs\_type,文件系统名为”romfs”,读超级块的函数为romfs_read_super()。
119 |
120 | Romfs\_read\_super()从磁盘读取磁盘超级块,主要步骤如下:
121 |
122 | 1.装配超级块,具体步骤为:
123 |
124 | (1)初始化超级块对象某些域。
125 |
126 | (2)从设备中读取磁盘第0块到内存,即调用函数bread(dev,0,ROMBSIZE),其中dev是文件系统安装时指定的设备,0为设备的首块,也就是磁盘超级块,ROMBSIZE是读取的大小。
127 |
128 | (3)检验磁盘超级块中的校验和。
129 |
130 | (4)继续初始化超级块对象某些域。
131 |
132 | 2.给超级块对象的操作表赋值(s-\>s\_op = &romfs\_ops)
133 |
134 | 3.给根目录分配目录项。
135 |
136 | 其中,romfs文件系统在超级块操作表中实现了两个函数:
137 |
138 | ```c
139 | static struct super_operations romfs_ops = {
140 | read_inode: romfs_read_inode,
141 | statfs: romfs_statfs,
142 | };
143 | ```
144 |
145 | 第一个函数romfs\_read\_inode(inode)是从磁盘读取数据填充参数指定的索引节点,主要步骤如下:
146 |
147 | 1.根据inode参数寻找对应的索引节点。
148 |
149 | 2.初始化索引节点某些域
150 |
151 | 3.根据文件类型设置索引节点的相应操作表
152 |
153 | (1)如果是目录文件,则将索引节点表设为i->i_op = &romfs_dir_inode_operations;文件操作表设置为i->i_fop = &romfs_dir_operations; 这两个表分别定义如下:
154 | ```c
155 | static struct inode_operations romfs_dir_inode_operations = {
156 | lookup: romfs_lookup,
157 | };
158 | static struct file_operations romfs_dir_operations = {
159 | read: generic_read_dir,
160 | readdir: romfs_readdir,
161 | };
162 | ```
163 | (2)如果是常规文件,则将文件操作表设置为i->i_fop = &generic_ro_fops;
164 | 对常规文件的操作也只需要使用内核提供的通用函数表,它包含三种基本的常规文件操作:
165 | ```c
166 | struct generic_ro_fops{
167 | llseek: generic_file_llseek,
168 | read: generic_file_read,
169 | mmap: generic_file_mmap,
170 | }
171 | ```
172 |
173 | (3)将页缓冲区表设置为i->i_data.a_ops = &romfs_aops;
174 | ```c
175 | static struct address_space_operations romfs_aops = {
176 | readpage: romfs_readpage
177 | };
178 | ```
179 |
180 | 回忆前面我们描述过的页缓冲区,显然常规文件访问需要经过它,因此有必要实现页缓冲区操作。因为只需要读文件,所以只用实现romfs_readpage函数,这里readpage函数完成将数据从设备读入页缓冲区,该函数根据文件格式从设备读取需要的数据。设备读取操作需要使用bread块I/O例程,它的作用是从设备读取指定的块。
181 |
182 | (4)由于romfs是只读文件系统,它在对常规文件操作时不需要索引节点操作,如mknod,link等,因此使用索引节点的操作表。
183 |
184 | (5)如果是连接文件,则将索引节点操作表设置为:i->i_op=&page_symlink_inode_operations;
185 |
186 | (6)如果是套接字或管道,则进行特殊文件初始化操作init_special_inode(i, ino, nextfh);
187 |
188 | 到此,我们已经遍例了romfs文件系统使用的几种对象结构:romfs_super_block、romfs_inode、romfs_fs_type、super_operations 、address_space_operations、file_operations、romfs_dir_operations、inode_operations、romfs_dir_inode_operations 。实现上述对象中的函数是设计一个新文件系统的最低要求,具体函数的实现请读者阅读源代码。
189 |
190 | 最后要说明的是,为了使得romfs文件系统作为模块安装,需要实现以下两个函数:
191 |
192 | ```c
193 | static int __init init_romfs_fs(void)
194 | {
195 | return register_filesystem(&romfs_fs_type);
196 | }
197 | static void __exit exit_romfs_fs(void)
198 | {
199 | unregister_filesystem(&romfs_fs_type);
200 | }
201 | ```
202 |
203 | 安装和卸载romfs文件系统的例程为:
204 |
205 | ```c
206 | module_init(init_romfs_fs)
207 | module_exit(exit_romfs_fs)
208 | ```
209 |
210 | 到此,介绍了romfs文件系统的主要结构,至于细节,需要读者仔细推敲。Romfs是最简单的基于块的只读文件系统,而且没有访问控制等功能。所以很多访问权限以及写操作相关的方法都不必去实现。
211 |
--------------------------------------------------------------------------------
/第三章/section3_3.md:
--------------------------------------------------------------------------------
1 | ## **3.3 Linux系统中进程的组织方式**
2 |
3 | 在一个系统中,通常可拥有数十个、数百个乃至数千个进程,相应地就有这么多PCB。为了能有效地对它们加以管理,应该用适当的方式将这些PCB组织起来。
4 |
5 | ### **3.3.1进程链表**
6 |
7 | 为了对给定类型的进程(例如在可运行状态的所有进程)进行有效的搜索,内核建立了几个进程链表。每个进程链表由指向进程PCB的指针组成。在task_struct结构中有如下的定义:
8 |
9 | struct task_struct {
10 | …
11 | struct list_head tasks;
12 | char comm[TASK_COMM_LEN];/*可执行程序的名字(带路径)*/
13 | …
14 | }
15 |
16 | 因此,如图3.8的一个双向循环链表把所有进程联系起来,我们叫它为进程链表。
17 |
18 |
19 |

20 |
21 |
22 |
23 | 链表的头和尾都为init_task。init_task是0号进程的PCB,0号这个进程永远不会被撤消,它的PCB被静态地分配到内核数据段中,也就是说init_task的PCB是预先由编译器分配的,在运行的过程中保持不变,而其它PCB是在运行的过程中,由系统根据当前的内存状况随机分配的,撤消时再归还给系统。
24 |
25 | 自己可编写一个内核模块,打印进程的PID和进程名,模块中主要函数的代码如下:
26 |
27 | static int print_pid( void)
28 | {
29 | struct task_struct *task,*p;
30 | struct list_head *pos;
31 | int count=0;
32 | printk("Hello World enter begin:\n");
33 | task=&init_task;
34 | list_for_each(pos,&task->tasks)
35 | {
36 | p=list_entry(pos, struct task_struct, tasks);
37 | count++;
38 | printk("%d--->%s\n",p->pid,p->comm);
39 | }
40 | printk(the number of process is:%d\n",count);
41 | return 0;
42 | }
43 |
44 | 需要注意的是,在一个拥有大量进程的系统中通过重复来遍历所有的进程是非常耗费时的。
45 |
46 | ### **3.3.2哈希表**
47 |
48 | 在有些情况下,内核必须能根据进程的PID导出对应的PCB。顺序扫描进程链表并检查PCB的pid域是可行但相当低效的。为了加速查找,引入了哈希表,于是要有一个哈希函数把PID转换成表的索引,Linux用一个叫做pid_hashfn的宏实现:
49 |
50 | #define pid_hashfn(x) \
51 | ((((x) >> 8) ^ (x)) & (PIDHASH_SZ - 1))
52 |
53 | 其中,PIDHASH_SZ为表中元素的最大个数,通过pid_hashfn这个函数,可以把进程的PID均匀地散列在哈希表中。
54 |
55 | 对于一个给定的pid,可以通过find_task_by_pid()函数快速地找到对应的进程:
56 |
57 | static inline struct task_struct *find_task_by_pid(int pid)
58 | {
59 | struct task_struct *p, **htable = &pidhash[pid_hashfn(pid)];
60 | for(p = *htable; p && p->pid != pid; p = p->pidhash_next);
61 |
62 | return p;
63 | }
64 |
65 | 其中pidhash是哈希表, 其定义为:`struct task_struct *pidhash[PIDHASH_SZ]`
66 |
67 | 在数据结构课程中我们已经了解到,哈希函数并不总能确保PID与表的索引一一对应,两个不同的PID散列到相同的索引称为冲突。
68 |
69 | Linux利用链地址法来处理冲突的PID,也就是说,每一表项是由冲突的PID组成的双向链表, task_struct 结构中有两个域pidhash_next 和 pidhash_pprev域来实现这个链表,同一链表中pid由小到大排列。如图3.9所示。
70 |
71 |
72 |

73 |
74 |
75 |
76 | ### **3.3.3 就绪队列**
77 |
78 | 当内核要寻找一个新的进程在CPU上运行时,必须只考虑处于就绪状态的进程,因为扫描整个进程链表是相当低效的,所以把可运行状态的进程组成一个双向循环链表,也叫就绪(runqueue)。
79 |
80 | 就绪队列容纳了系统中所有准备运行的进程, 在task_struct结构中定义了双向链表。
81 |
82 | struct task_struct{
83 | …
84 | struct list_head run_list;
85 | …
86 | }
87 |
88 |
89 | 就绪队列的定义以及相关操作在/kernel/sched.c文件中:
90 |
91 | static LIST_HEAD(runqueue_head); /*定义就绪队列头指针为runqueue_head*/
92 |
93 | add_to_runqueue()函数向就绪队列中插入进程的PCB。
94 | static inline void add_to_runqueue(struct task_struct * p)
95 | { list_add_tail(&p->run_list, &runqueue_head);
96 | nr_running++; /*就绪进程数加1*/
97 | };
98 |
99 | move_last_runqueue()函数从就绪队列中删除进程的PCB。
100 | static inline void move_last_runqueue(struct task_struct * p)
101 | {
102 | list_del(&p->run_list);
103 | list_add_tail(&p->run_list, &runqueue_head);
104 | };
105 |
106 | 以上讲述的进程组织方式,实际上大都是数据结构中数据的组织方式,因此,读者在阅读本书或者源代码的过程中,首先抓住事物的本质,找出熟悉的知识,然后,再去体会或应用已有的知识解决问题。
107 |
108 | 另外,以上是Linux 2.4中就绪队列简单的组织方式。为了让读者尽可能先掌握原理,因此本章在讨论相关内容的时候将会去繁存简,将其中最核心的调度理论和算法做以阐述。
109 |
110 | ### **3.3.4 等待队列**
111 |
112 | 如前说述,睡眠有两种相关的进程状态:TASK_INTERRUPTIBLE和TASK_ UNINTERRUPTIBLE。它们的唯一区别是处于TASK_UNINTERRUPTIBLE的进程会忽略信号,而处于TASK_INTERRUPTIBLE状态的进程如果接收到一个信号会被提前唤醒并响应该信号。两种状态的进程位于同一个等待队列上,等待某些事件,不能够运行。
113 |
114 | 等待队列在内核中有很多用途,尤其对中断处理、进程同步及定时用处更大。因为这些内容在以后的章节中讨论,我们只在这里说明,进程必须经常等待某些事件的发生,例如,等待一个磁盘操作的终止,等待释放系统资源,或等待时间走过固定的间隔。等待队列实现在事件上的条件等待,也就是说,希望等待特定事件的进程把自己放进合适的等待队列,并放弃控制权。因此,等待队队列是一组睡眠的进程,当某一条件变为真时,由内核唤醒它们。
115 |
116 | **1. 等待队列的数据结构:**
117 |
118 | 在include/linux/wait.h中,对等待队列的定义如下:
119 |
120 | struct __wait_queue {
121 | unsigned int flags;
122 | #define WQ_FLAG_EXCLUSIVE 0x01
123 | void *private;
124 | wait_queue_func_t func;
125 | struct list_head task_list;
126 | };
127 |
128 | typedef struct _ _wait_queue wait_queue_t;
129 |
130 | 在内核代码中,以两个下划线为开头的标识符一般都是内核内部定义的。typefdef对内部定义重新封装。
131 |
132 | 在这个结构中,最主要的域是task_list,它把处于睡眠状态的进程链接成双向链表。睡眠是暂时的,把它唤醒继续运行才是目的。为此,设置了func域,该域指向唤醒函数,用以把等待队列中的进程唤醒:
133 |
134 | typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int flags, void *key);
135 |
136 | 如何唤醒等待队列中的进程,还需进一步根据等待的原因进行归类。比如,因为争夺某个临界资源,有一组进程由此睡眠,那么在唤醒时,是把这一组全部唤醒还是唤醒其中一个?如果全部唤醒,但实际上只能有一个进程使用临界资源,其他进程还得继续回去睡眠,因此仅唤醒等待队列中的一个进程才有意义。结构中的flag域就是为了区分睡眠时的互斥进程和非互斥进程。对于互斥进程,flag的取值为1(#define WQ_FLAG_EXCLUSIVE 0x01),反之,取值为0。还有一个private域,是传递给func函数的参数。
137 |
138 | **2.等待队列头**
139 |
140 | 每个等待队列都有一个等待队列头(wait queue head),定义如下:
141 |
142 | struct _ _wait_queue_head {
143 | spinlock_t lock;
144 | struct list_head task_list;
145 | };
146 | typedef struct _ _wait_queue_head wait_queue_head_t;
147 |
148 | 因为等待队列是由中断处理程序和主要内核函数修改的,因此必须对其双向链表保护以免对其进行同时访问,因为同时访问会导致不可预测的后果(参见第七章)。通过lock自旋锁域进行同步,而task_list域是等待进程链表的头。如图3.10是等待队列以及队列头形成的双向链表。
149 |
150 |
151 |

152 |
153 |
154 | **3. 等待队列的操作**
155 |
156 | 在使用一个等待队列前,首先对等待队列头和等待队列进行初始化,wait.h中定义了如下宏:
157 |
158 |
159 | #define __WAIT_QUEUE_HEAD_INITIALIZER(name) { \
160 | .lock = __SPIN_LOCK_UNLOCKED(name.lock), \
161 | .task_list = { &(name).task_list, &(name).task_list } }
162 |
163 | #define DECLARE_WAIT_QUEUE_HEAD(name) \
164 | wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
165 |
166 | 这两个宏声明初并始化等待队列头name.
167 |
168 | 初始化等待队列中的一个元素,则调用如下函数:
169 |
170 | static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p)
171 | {
172 | q->flags = 0;
173 | q->private = p;
174 | q->func = default_wake_function;
175 | }
176 |
177 | default_wake_function( )唤醒睡眠非互斥进程p,然后从等待队列链表中将其删除。
178 |
179 | 定义了一个等待进程后,必须把它插入等待队列。add_wait_queue( )把一个非互斥进程插入等待队列链表的第一个位置。add_wait_queue_exclusive( )把一个互斥进程插入等待队列链表的最后一个位置。remove_wait_queue( )从等待队列链表中删除一个进程。waitqueue_active( )检查一个给定的等待队列是否为空。
180 |
181 | 如何让等待特定条件的进程去睡眠,内核提供了多个函数。下面介绍最基本的睡眠函数sleep_on():
182 |
183 | void sleep_on(wait_queue_head_t *wq)
184 | {
185 | wait_queue_t wait;
186 | init_waitqueue_entry(&wait, current);
187 | current->state = TASK_UNINTERRUPTIBLE;
188 | add_wait_queue(wq,&wait); /* wq指向当前队列的头 */
189 | schedule( );
190 | remove_wait_queue(wq, &wait);
191 | }
192 |
193 | 该函数把当前进程的状态设置为TASK_UNINTERRUPTIBLE,并把它插入到特定的等待队列。然后,调用调度程序,而调度程序重新调度另一个进程开始执行。当睡眠的进程被唤醒时,调度程序接着执行sleep_on( ),也就是紧接schedule()的remove_wait_queue()函数,把该进程从等待队列中删除。
194 |
195 | 如果要让等待的进程唤醒,就调用唤醒函数wake_up(),它让待唤醒的进程进入TASK_RUNNING状态。内核代码中,wake_up 定义为一个宏,实际上等价于下列代码片段:
196 |
197 | void wake_up(wait_queue_head_t *q)
198 | {
199 | struct list_head *tmp;
200 | wait_queue_t *curr;
201 | list_for_each(tmp, &q->task_list) {
202 | curr = list_entry(tmp, wait_queue_t, task_list);
203 | if (curr->func(curr,
204 | TASK_INTERRUPTIBLE|TASK_UNINTERRUPTIBLE,
205 | 0, NULL) && curr->flags)
206 | break;
207 | }
208 | }
209 |
210 | list_for_each宏扫描双向链表q->task_list中的所有项,即等待队列中的所有进程。对每一项,list_entry宏都计算wait_queue_t型变量(curr)对应的地址。这个变量的func域存放wake_up函数的地址,它试图唤醒由等待队列中的task_list域标识的进程。如果一个进程已经被有效地唤醒(函数返回1)并且进程是互斥的(curr->flags等于1),循环结束。因为所有的非互斥进程总是在双向链表的开始位置,而所有的互斥进程在双向链表的尾部,所以函数总是先唤醒非互斥进程然后再唤醒互斥进程。
211 |
212 |
--------------------------------------------------------------------------------
/第五章/section5_2.md:
--------------------------------------------------------------------------------
1 | ## 5.2 中断描述符表的初始化
2 |
3 | 通过上面的介绍,我们知道了Intel微处理器对中断和异常所做的工作。下面,我们从操作系统的角度来对中断描述符表的初始化给予描述。
4 |
5 | Linux内核在系统的初始化阶段要进行大量的初始化工作,其与中断相关的工作有:初始化可编程控制器8259A;将中断描述符表的起始地址装入IDTR寄存器,并初始化表中的每一项。这些操作的完成将在本节进行具体描述。
6 |
7 | 用户进程可以通过INT指令发出一个中断请求,其中断请求向量在0~255之间。为了防止用户使用INT指令模拟非法的中断和异常,必须对中断描述符表进行谨慎的初始化。其措施之一就是将中断门或陷阱门中的请求特权级DPL域置为0。如果用户进程确实发出了这样一个中断请求,CPU会检查出其**当前特权级**CPL(3)与所请求的特权级DPL(0)有冲突,因此产生一个“通用保护”异常。
8 |
9 | 但是,有时候必须让用户进程能够使用内核所提供的功能(比如系统调用),也就是说从用户态进入内核态,这可以通过把中断门或陷阱门的DPL域置为3
10 | 来达到。
11 |
12 | 当计算机运行在实模式时,中断描述符表被初始化,并由BIOS使用。然而,一旦真正进入了Linux内核,中断描述符表就被移到内存的另一个区域,并为进入保护模式进行预初始化:用汇编指令LIDT对中断向量表寄存器IDTR进行初始化,即把IDTR置为0。把中断描述符表IDT的起始地址装入IDTR
13 |
14 | 用setup\_idt\(\)函数填充中断描述表中的256个表项。在对这个表进行填充时,使用了一个空的中断处理程序。因为现在处于初始化阶段,还没有任何中断处理程序,因此,用这个空的中断处理程序填充每个表项。
15 |
16 | 在对中断描述符表进行预初始化后,内核将在启用分页功能后对IDT进行第二遍初始化,也就是说,用实际的陷阱和中断处理程序替换这个空的处理程序。一旦这个过程完成,对于每个异常,IDT都由一个专门的陷阱门或系统门,而对每个外部中断,IDT都包含专门的中断门。
17 |
18 | ### 5.2.1 IDT表项的设置
19 |
20 | IDT表项的设置是通过\_set\_gaet\(\)函数实现的,在此,我们给出如何调用该函数在IDT表中插入一个门:
21 |
22 | #### 1. 插入一个中断门
23 |
24 | ```c
25 | static inline void set_intr_gate(unsigned int n, void *addr)
26 | {
27 | BUG_ON((unsigned)n > 0xFF);
28 | _set_gate(n, GATE_INTERRUPT, addr, 0, 0, __KERNEL_CS);
29 | }
30 | ```
31 |
32 | 其中,n是中断号,addr是中断处理程序的入口地址,GATE\_INTERRUPT是中断门类型,这样我们能看出来这是一个中断门。
33 |
34 | #### 2. 插入一个陷阱门
35 |
36 | ```c
37 | static inline void set_trap_gate(unsigned int n, void *addr)
38 | {
39 | BUG_ON((unsigned)n > 0xFF);
40 | _set_gate(n, GATE_TRAP, addr, 0, 0, __KERNEL_CS);
41 | }
42 | ```
43 |
44 | 可以看到传入的参数是GATE\_TRAP,所以是一个陷阱门。其他参数如中断门所讲的一样。
45 |
46 | #### 插入一个系统门
47 |
48 | ```c
49 | static inline void set_system_trap_gate(unsigned int n, void *addr)
50 | {
51 | BUG_ON((unsigned)n > 0xFF);
52 | _set_gate(n, GATE_TRAP, addr, 0x3, 0, __KERNEL_CS);
53 | }
54 | ```
55 |
56 | 可以看到传入的参数是GATE\_TRAP,所以是系统门。其他的参数如同中断门讲述的一样。
57 |
58 | ### 5.2.2 对陷阱门、系统门和中断门的初始化
59 |
60 | trap\_init\(\)函数就是设置中断描述符表开头的19个陷阱门,这些中断向量都是CPU保留用于异常处理的:
61 |
62 | ```c
63 | /**
64 | *X86_TRAP_DE = 0, /* 0, Divide-by-zero */
65 | *X86_TRAP_DB, /* 1, Debug */
66 | *X86_TRAP_NMI, /* 2, Non-maskable Interrupt */
67 | *X86_TRAP_BP, /* 3, Breakpoint */
68 | *X86_TRAP_OF, /* 4, Overflow */
69 | *X86_TRAP_BR, /* 5, Bound Range Exceeded */
70 | *X86_TRAP_UD, /* 6, Invalid Opcode */
71 | *X86_TRAP_NM, /* 7, Device Not Available */
72 | *X86_TRAP_DF, /* 8, Double Fault */
73 | *X86_TRAP_OLD_MF, /* 9, Coprocessor Segment Overrun */
74 | *X86_TRAP_TS, /* 10, Invalid TSS */
75 | *X86_TRAP_NP, /* 11, Segment Not Present */
76 | *X86_TRAP_SS, /* 12, Stack Segment Fault */
77 | *X86_TRAP_GP, /* 13, General Protection Fault */
78 | *X86_TRAP_PF, /* 14, Page Fault */
79 | *X86_TRAP_SPURIOUS, /* 15, Spurious Interrupt */
80 | *X86_TRAP_MF, /* 16, x87 Floating-Point Exception */
81 | *X86_TRAP_AC, /* 17, Alignment Check */
82 | *X86_TRAP_MC, /* 18, Machine Check */
83 | *X86_TRAP_XF, /* 19, SIMD Floating-Point Exception */
84 | **/
85 | set_intr_gate(X86_TRAP_DE, ÷_error);
86 | set_intr_gate_ist(X86_TRAP_NMI, &nmi, NMI_STACK);
87 | /* int4 can be called from all */
88 | set_system_intr_gate(X86_TRAP_OF, &overflow);
89 | set_intr_gate(X86_TRAP_BR, &bounds);
90 | set_intr_gate(X86_TRAP_UD, &invalid_op);
91 | set_intr_gate(X86_TRAP_NM, &device_not_available);
92 | #ifdef CONFIG_X86_32
93 | set_task_gate(X86_TRAP_DF, GDT_ENTRY_DOUBLEFAULT_TSS);
94 | #else
95 | set_intr_gate_ist(X86_TRAP_DF, &double_fault, DOUBLEFAULT_STACK);
96 | #endif
97 | set_intr_gate(X86_TRAP_OLD_MF, &coprocessor_segment_overrun);
98 | set_intr_gate(X86_TRAP_TS, &invalid_TSS);
99 | set_intr_gate(X86_TRAP_NP, &segment_not_present);
100 | set_intr_gate(X86_TRAP_SS, stack_segment);
101 | set_intr_gate(X86_TRAP_GP, &general_protection);
102 | set_intr_gate(X86_TRAP_SPURIOUS, &spurious_interrupt_bug);
103 | set_intr_gate(X86_TRAP_MF, &coprocessor_error);
104 | set_intr_gate(X86_TRAP_AC, &alignment_check);
105 | #ifdef CONFIG_X86_MCE
106 | set_intr_gate_ist(X86_TRAP_MC, &machine_check, MCE_STACK);
107 | #endif
108 | set_intr_gate(X86_TRAP_XF, &simd_coprocessor_error);
109 | for (i = 0; i < FIRST_EXTERNAL_VECTOR; i++)
110 | set_bit(i, used_vectors);
111 |
112 | #ifdef CONFIG_IA32_EMULATION
113 | set_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall);
114 | set_bit(IA32_SYSCALL_VECTOR, used_vectors);
115 | #endif
116 |
117 | #ifdef CONFIG_X86_32
118 | set_system_trap_gate(SYSCALL_VECTOR, &system_call);
119 | set_bit(SYSCALL_VECTOR, used_vectors);
120 | #endif
121 |
122 | }
123 | ```
124 |
125 | 其中,“&”之后的名字就是每个异常处理程序的名字。最后一个是对系统调用的设置。
126 |
127 | set\_system\_intr\_gate\(IA32\_SYSCALL\_VECTOR, ia32\_syscall\);这个是对系统中断门进行初始化。
128 |
129 | set\_system\_trap\_gate\(SYSCALL\_VECTOR, &system\_call\);这个是对系统门进行初始化。
130 |
131 | ### 5.2.3 初始化中断
132 |
133 | 内核是在异常和陷阱初始化完成的情况下才会进行中断的初始化,中断的初始化也是处于start\_kernel\(\)函数中,分为两个部分,分别是early\_irq\_init\(\)和init\_IRQ\(\)。early\_irq\_init\(\)是第一步的初始化,其工作主要是跟硬件无关的一些初始化,比如一些变量的初始化,分配必要的内存等。init\_IRQ\(\)是第二步,其主要就是关于硬件部分的初始化了。
134 |
135 | 那让我们来看一下最主要的init\_IRQ\(\)实现了什么。
136 |
137 | ```c
138 | void __init init_IRQ(void)
139 | {
140 | int i;
141 | x86_add_irq_domains();
142 | for (i = 0; i < legacy_pic->nr_legacy_irqs; i++)
143 | per_cpu(vector_irq, 0)[IRQ0_VECTOR + i] = i;
144 |
145 | x86_init.irqs.intr_init();
146 | }
147 | ```
148 |
149 | x86\_init.irqs.intr\_init\(\)是一个函数指针,其指向native\_init\_IRQ\(\);我们可以直接看看native\_init\_IRQ\(\);
150 |
151 | ```c
152 | void __init native_init_IRQ(void)
153 | {
154 | int i;
155 |
156 | x86_init.irqs.pre_vector_init();
157 |
158 | apic_intr_init();
159 |
160 | i = FIRST_EXTERNAL_VECTOR;
161 | for_each_clear_bit_from(i, used_vectors, NR_VECTORS) {
162 | set_intr_gate(i, interrupt[i - FIRST_EXTERNAL_VECTOR]);
163 | }
164 |
165 | if (!acpi_ioapic && !of_ioapic)
166 | setup_irq(2, &irq2);
167 |
168 | #ifdef CONFIG_X86_32
169 | irq_ctx_init(smp_processor_id());
170 | #endif
171 | }
172 | ```
173 |
174 | interrupt\[\]就是中断程序的入口地址,外部中断的门描述的中断处理函数都为interrupt\[i\]。
175 |
176 | ### 5.2.4 中断处理程序的形成
177 |
178 | 由前一节知道,interrupt\[\]为中断处理程序的入口地址,这只是一个笼统的说法。实际上不同的中断处理程序,不仅名字不同,其内容也不同,但是,这些函数又有很多相同之处,因此应当以统一的方式形成其函数名和函数体,于是,内核对该数组的定义如下:
179 |
180 | ```c
181 | static void (*interrupt[NR_IRQS])(void) = {
182 | NULL, NULL, IRQ2_interrupt, IRQ3_interrupt,
183 | IRQ4_interrupt, IRQ5_interrupt, IRQ6_interrupt, IRQ7_interrupt,
184 | IRQ8_interrupt, IRQ9_interrupt, IRQ10_interrupt, IRQ11_interrupt,
185 | IRQ12_interrupt, IRQ13_interrupt, NULL, NULL,
186 | IRQ16_interrupt, IRQ17_interrupt, IRQ18_interrupt, IRQ19_interrupt,
187 | IRQ20_interrupt, IRQ21_interrupt, IRQ22_interrupt, IRQ23_interrupt,
188 | IRQ24_interrupt, IRQ25_interrupt, NULL, NULL, NULL, NULL, NULL,
189 | IRQ31_interrupt
190 | };
191 | ```
192 |
193 | nterrupt\[i\]的每个元素都相同,执行相同的汇编代码,这段汇编代码实际上很简单,它主要工作就是将**中断向量号**和**被中断上下文**
194 |
195 | \(进程上下文或者中断上下文\)保存到栈中,最后调用do\_IRQ函数。 从IRQ2\_interrupt一直到IRQ31\_interupt。那么这些函数名又是如何形成的?我们看如下宏定义:
196 |
197 | ```
198 | #define IRQ_NAME2(nr) nr##_interrupt(void)
199 | #define IRQ_NAME(nr) IRQ_NAME2(IRQ##nr)
200 | ```
201 |
202 | 从这两个宏的定义可以推知,IRQ\_NAME\(n\)就是IRQn\_interrupt\(void\)函数形式,其中随n具体数字不同,则形成不同的IRQn\_interrupt\(\)函数名。接下来,又如何以统一的方式让这些函数拥有内容,也就是说,这些函数的代码是如何形成的?内核定义了BUILD\_IRQ宏。
203 |
204 | BUILD\_IRQ宏是一段嵌入式汇编代码,为了有助于理解,我们把它展开成下面的汇编语言片段:
205 |
206 | ```c
207 | IRQn_interrupt:
208 | pushl $n-256
209 | jmp common_interrupt
210 | ```
211 |
212 | 把中断号减256的结果保存在栈中,这是进入中断处理程序后第一个压入堆栈的值,是一个负数,正数留给系统调用使用。对于每个中断处理程序,唯一不同的就是压入栈中的这个数。然后,所有的中断处理程序都跳到一段相同的代码common\_interrupt。关于这段代码,请参看5.3.3一节中断处理程序IRQn\_interrupt。
213 |
214 |
215 |
216 |
217 |
218 |
--------------------------------------------------------------------------------
/第三章/section3_1.md:
--------------------------------------------------------------------------------
1 | # **第三章 进程**
2 |
3 | 面对计算机系统中少数几个CPU,多个程序在执行时都想占有它并独自在上面运行,但CPU本身并没有分身术,因此互不相让的程序之间可能会厮打起来。作为管理者的操作系统,决不能袖手旁观。于是,操作系统的设计者发明了进程这一概念。
4 |
5 | ## **3.1 进程介绍**
6 |
7 | 现在所有计算机都能同时做几件事情。 例如,用户一边运行浏览器程序上网浏览信息,一边运行字处理程序编辑文档。在一个多程序系统中,CPU由这道程序向那道程序切换,使每道程序运行几十或几百毫秒。然而严格地说,在一个瞬间,CPU只能运行一道程序。但在一段时期内,它却可能轮流运行多个程序,这样就给用户一种并行的错觉。有时人们称其为伪并行——就是指CPU在多道程序之间快速地切换,以此来区分它与多处理机(两个或更多的CPU共享物理存储器)系统真正的硬件并行。由于人们很难对多个并行的活动进行跟踪。因此,经过多年的探索,操作系统的设计者抽象出了进程这样一个逻辑概念,使得并行更容易被理解和处理。
8 |
9 | ### **3.1.1 程序和进程**
10 |
11 | 程序是一个普通文件,是机器代码指令和数据的集合,这些指令和数据存储在磁盘上的一个可执行映象(Executable Image)中。所谓可执行映象就是一个可执行文件的内容,例如,你编写了一个C源程序,最终这个源程序要经过编译、连接成为一个可执行文件后才能运行。源程序中你要定义许多变量,在可执行文件中,这些变量就组成了数据段的一部分;源程序中的许多语句,例如“`i++;for (i=0;i<10;i++) `”等,在可执行文件中,它们对应着许多不同的机器代码指令,这些机器代码指令经CPU执行,就完成了你所期望的工作。可以这么说,程序代表你期望完成某工作的计划和步骤,它还浮在纸面上,等待具体实现。而具体的实现过程就是由进程来完成的,可以认为进程是运行中的程序,它除了包含程序中的所有内容外,还包含一些额外的数据。
12 |
13 | 我们知道,程序装入内存后才得以运行。在程序计数器的控制下,指令被不断地从内存取至CPU中运行。实际上,程序的执行过程可以说是一个执行环境的总和,这个执行环境包括程序中各种指令和数据外,还有一些额外数据,比如寄存器的值、用来保存临时数据(例如传递给某个函数的参数、函数的返回地址、保存的临时变量等)的堆栈、被打开的文件及输入输出设备的状态等等。上述执行环境的动态变化表征了程序的运行。为了对这个动态变化的过程进行描述,程序这个概念已经远远不够,于是就引入了“进程”概念。进程代表程序的执行过程,它是一个动态的实体,随着程序中指令的执行而不断地变化。在某个时刻进程的内容被称为进程映像(Process Image)
14 |
15 | Linux是多任务操作系统,也就是说可以有多个程序同时装入内存并运行,操作系统为每个程序建立一个运行环境即创建进程。从逻辑上说,每个进程拥有它自己的虚拟CPU。当然,实际上真正的CPU在各进程之间来回切换。但如果我们想研究这种系统,而去跟踪CPU如何在程序间来回切换将会是一件相当复杂的事情,于是换个角度,集中考虑在(伪)并行情况下运行的进程集就使问题变得简单、清晰得多。这种快速的切换称作多道程序执行。在一些Unix书籍中,又把“进程切换”(Process Switching)称为“环境切换”或“上下文切换”(Context Switching)。这里“进程的上下文”就是指进程的执行环境。
16 |
17 | 进程运行过程中,还需要其他的一些系统资源,例如,要用CPU来运行它的指令、要用系统的物理内存来容纳进程本身和它的有关数据、要在文件系统中打开和使用文件、并且可能直接或间接的使用系统的物理设备,例如打印机、扫描仪等。由于这些系统资源是由所有进程共享的,所以操作系统必须监视进程和它所拥有的系统资源,使它们可以公平地拥有系统资源以得到运行。
18 |
19 | 由此,我们对进程作一明确定义:所谓进程是由正文段(text)、用户数据段(user segment)以及系统数据段(system segment)共同组成的一个执行环境,如图3.1所示。
20 |
21 |
22 |

23 |
24 |
25 |
26 | (1)正文段(text):存放被执行的机器指令。这个段是只读的,它允许系统中正在运行的两个或多个进程之间能够共享这一代码。例如,有几个用户都在使用文本编辑器,在内存中仅需要该程序指令的一个副本,他们全都共享这一副本。
27 |
28 | (2)用户数据段(user segment):存放进程在执行时直接进行操作的所有数据,包括进程使用的全部变量在内。显然,这里包含的信息可以被改变。虽然进程之间可以共享正文段,但是每个进程需要有它自己的专用用户数据段。例如同时编辑文本的用户,虽然运行着同样的程序—编辑器,但是每个用户都有不同的数据:正在编辑的文本。
29 |
30 | (3)系统数据段(system segment):该段有效地存放程序运行的环境。事实上,这正是程序和进程的区别所在。如前所述,程序是由一组指令和数据组成的静态事物,它们是进程最初使用的正文段和用户数据段。作为动态事物,进程是正文段、用户数据段和系统数据段的信息的交叉综合体,其中系统数据段是进程实体最重要的一部分,之所以说它有效地存放程序运行的环境,是因为这一部分存放有进程的控制信息。系统中有许多进程,操作系统要管理它们、调度它们运行,就是通过这些控制信息。Linux为每个进程建立了task_struct数据结构来容纳这些控制信息。
31 |
32 | 假设有三道程序A、B、C在系统中运行。程序一旦运行起来,我们就称它为进程,因此称它们为三个进程Pa、Pb、Pc。假定进程Pa执行到一条输入语句,因为这时要从外设读入数据,于是进程Pa主动放弃CPU。此时操作系统中的调度程序就要选择一个进程投入运行,假设选中Pc,这就会发生进程切换,从Pa切换到Pc。同理,在某个时刻可能切换到进程Pb。从某一时间段看,三个进程在同时执行,从某一时刻看,只有一个进程在运行,我们把这几个进程的伪并行执行叫做进程的并发执行。
33 |
34 | 在Linux系统中我们还可以使用ps命令来查看当前系统中的进程和进程的一些相关信息。使用这个命令我们就可以查看系统中所有进程的状态。该命令可以确定有哪些进程正在运行和运行的状态、进程是否结束、进程有没有僵死、哪些进程占用了过多地资源等等。例如:ps -e
35 |
36 | $ ps -e
37 | PID TTY TIME CMD
38 | 1 ? 00:00:00 init
39 | 2 ? 00:00:00 kthreadd
40 | 2102 ? 00:04:04 firefox-bin
41 | 2206 pts/0 00:00:00 bash
42 | 2211 pts/0 00:00:05 fcitx
43 | 2809 ? 00:00:01 stardict
44 | 3317 ? 00:00:05 qq
45 | ……
46 |
47 | 这里只是截取了部分进程和部分信息,即进程的进程号、进程相关的终端(?表示进程不需要终端)、进程已经占用CPU的时间和启动进程的程序名。在后面的学习中还要使用这个命令来查看进程的其它信息。
48 |
49 | ### **3.1.2 进程的层次结构**
50 |
51 | 进程是一个动态的实体,它具有生命周期,系统中进程的生生死死随时发生。因此,操作系统对进程的描述模仿人类活动。一个进程不会平白无故的诞生,它总会有自己的父母。在Linux中,通过调用fork系统调用来创建一个新的进程。新创建的子进程同样也能执行fork,所以,有可能形成一颗完整的进程树。注意,每个进程只有一个父进程,但可以有0个、1个、2个或多个子进程。
52 |
53 | 从身边的例子体验进程树的诞生,比如Linux的启动。Linux在启动时就创建一个称为init的特殊进程,顾名思义,它是起始进程,是祖先,以后诞生的所有进程都是它的后代——或是它的儿子,或是它的孙子。init进程为每个终端(tty)创建一个新的管理进程,这些进程在终端上等待着用户的登录。当用户正确登录后,系统再为每一个用户启动一个shell进程,由shell进程等待并接受用户输入的命令信息,如图3.2是一颗进程树。
54 |
55 |
56 |

57 |
58 |
59 |
60 | 此外,init进程还负责管理系统中的“孤儿”进程。如果某个进程创建子进程之后就终止,而子进程还“活着”,则子进程成为孤儿进程。init进程负责“收养”该进程,即孤儿进程会立即成为init进程的儿子,也就说,init进程承担着养父的角色。这是为了保持进程树的完整性。
61 |
62 |
63 | 在Linux系统中可以使用pstree命令来查看系统中的树形结构;pstree将所有进程显示为树状结构,以清楚地表达程序间的相互关系。从该命令的显示结果可以看到,init进程是系统中唯一一个没有父进程的进程,它是系统中的第一个进程,其它进程都是由它和它的子进程产生的。
64 |
65 | 另外ps命令也可以显示进程的树形结构,例如:
66 |
67 | $ ps –ejH
68 |
69 | ### **3.1.3 进程状态**
70 |
71 | 为了对进程从产生到消亡的这个动态变化过程进行捕获和描述,就需要定义进程各种状态并制定相应的状态转换策略,以此来控制进程的运行。
72 |
73 | 因为不同操作系统对进程的管理方式和对进程的状态解释可以不同,所以不同操作系统中描述进程状态的数量和命名也会有所不同,但最基本的进程状态有三种:
74 |
75 | (1) 运行态: 进程占有CPU,并在CPU上运行。
76 |
77 | (2) 就绪态: 进程已经具备运行条件, 但由于CPU忙而暂时不能运行
78 |
79 | (3) 阻塞态(或等待态): 进程因等待某种事件的发生而暂时不能运行。(即使CPU空闲, 进程也不可运行)。
80 |
81 | 进程在生命期内处于且仅处于三种基本状态之一,如图3.3。
82 |
83 |
84 |

85 |
86 |
87 |
88 | 这三种状态之间有四种可能的转换关系:
89 |
90 | ① 运行态阻塞态: 进程发现它不能运行下去时发生这种转换。这是因为进程发生I/O请求或等待某件事情。
91 |
92 | ② 运行态就绪态:在系统认为运行进程占用CPU的时间已经过长,决定让其它进程占用CPU时发生这种转换。这是由调度程序引起的。调度程序是操作系统的一部分,进程甚至感觉不到它的存在。
93 |
94 | ③ 就绪态运行态:运行进程已经用完分给它的CPU时间,调度程序从处于就绪态的进程中选择一个投入运行。
95 |
96 | ④ 阻塞态就绪态:当一个进程等待的一个外部事件发生时(例如输入数据到达),则发生这种转换。如果这时没有其它进程运行,则转换③立即被触发,该进程便开始运行。
97 |
98 | ### **3.1.4进程举例**
99 |
100 | Linux系统中,用户在程序中可以通过调用fork 系统调用来创建进程。调用进程叫父进程(parent),被创建的进程叫子进程(child)。现在举一个简单的C程序forktest.c,说明进程的创建及进程的并发执行。
101 |
102 |
103 | #include
104 | #include
105 | #include
106 |
107 | void do_something(long t)
108 | {
109 | int i = 0;
110 | for(i = 0; i < t; i++)
111 | for(i = 0; i < t; i++)
112 | for(i = 0; i < t; i++)
113 | ;
114 | }
115 |
116 | int main()
117 | {
118 | pid_t pid;
119 |
120 | printf("PID before fork(): %d\n", getpid());
121 | pid=fork();
122 |
123 | pid_t npid = getpid();
124 | if(pid < 0)
125 | perror("fork error\n");
126 | else if(pid == 0) {
127 | while(1) {
128 | printf(“I am child process, PID is %d\n”, getpid());
129 | do_something(10000000);
130 | }
131 | }
132 | else if(pid > 0) {
133 | while(1) {
134 | printf("I am father process, PID is %d\n", getpid());
135 | do_something(10000000);
136 | }
137 | }
138 | return 0;
139 | }
140 |
141 | 在Linux运行的每个进程都有一个唯一的进程标识符PID(Process Identifier)。从进程ID的名字就可以看出,它就是进程的身份证号码,每个人的身份证号码都不会相同,每个进程的进程ID也不会相同。系统调用getpid()就是获得进程标识符。pid_t是用于定义进程PID的一个类型,而实际上就是int型的。
142 |
143 | 先来编译并运行这个程序:
144 |
145 | $ ./fork_test
146 | PID before fork():3991
147 | I am child process, PID is 3992
148 | I am child process, PID is 3992
149 | I am father process, PID is 3991
150 | I am child process, PID is 3992
151 | I am father process, PID is 3991
152 | I am child process, PID is 3992
153 | ....
154 |
155 | 可以看到这里输出了“child proces”和“father process”,它们的PID是不一样的,而且是在“不规则”的交替出现。这其实这就是进程的创建和并发执行了。
156 |
157 | 从概念上讲,fork()就像细胞的裂变,调用fork()的进程就是父进程,而新裂变出的进程就是子进程。新创建的进程与父进程几乎完全相同,只有少量属性必须不同,例如,每个进程的PID必须是唯一的。调用fork()后,子进程被创建,此时父进程和子进程都从这个系统调用内部继续运行。为了区分父/子进程,fork()给两个进程返回不同的值。对父进程,fork()返回新创建子进程的进程标识符(PID),而对子进程,fork()返回值0,这一概念表示在图3.4中。
158 |
159 |
160 |

161 |
162 |
163 |
164 | 当上面那个程序运行时,它会不断的输出信息。第一行将显示fork()被执行前进程的PID,其余的输出行将在fork()执行后由父进程和子进程产生,也就是说,当执行到fork()这个系统调用时,一个进程裂变为两个进程,这两个进程并发执行,到底哪个进程先执行,我们在这里没有控制,不过,系统一般默认子进程先执行。
165 |
166 | 再键入ps命令查看一下目前系统中进程的状态和关系:
167 |
168 | $ ps lf
169 |
170 | > UID PID PPID PRI NI STAT TTY TIME COMMAND
171 | >
172 | 1000 3836 2204 20 0 Ss pts/2 0:00 bash
173 | >
174 | 1000 4008 3836 20 0 R+ pts/2 0:00 \_ ps lf
175 | >
176 | 1000 2206 2204 20 0 Ss pts/0 0:00 bash
177 | >
178 | 1000 3391 2206 20 0 R+ pts/0 0:00 \_ ./fork_test
179 | >
180 | 1000 3392 3391 20 0 R+ pts/0 0:00 \_ ./fork_test
181 | >
182 | > …
183 |
184 | 为了清晰起见,删除了部分列。这里主要说明其中的PID和PPID列,它们分别表示本进程的PID和父进程的PID。可以看到PID为3391的fork_test和PID为3392的fork_test,尽管名字相同,因PID不同实际上是两个不同的进程。3391的父进程是PID为2206的bash进程,3392的父进程就是3391.
185 |
186 | 通过这个简单的例子使读者对进程有初步的认识,尤其是初步感受一下进程的并发执行。对这个例子的进一步理解请看3.6节。
187 |
188 |
--------------------------------------------------------------------------------