跳转到内容

Linux内核完全注释

英文名:Linux kernel Source Code fully comments Based On Kernel V0.12

修正版 V5.0 2019-01-24

赵 炯 [email protected]

堪称伟大的开源中文书籍。目标:阅读不下三遍。

官网资源:

本书对 Linux 操作系统早期内核(V0.12)全部源代码文件进行了详细全面的注释和说明,旨在让读者能够在尽量短的时间内对 Linux 的工作机理获得全面而深刻的理解,为进一步学习和研究现代 Linux 系统打下坚实的基础。虽然分析的版本很低,但该内核已能够正常编译运行,并且其中已包括了 Linux 工作原理的精髓。书中首先概要地介绍了 Linux 内核发展历史,说明了各内核版本之间的主要区别和改进方面,给出了选择 0.12 版内核源代码作为研究对象的原因。然后给出了阅读内核源代码所需的相关基础知识,概要介绍了运行 Linux 系统的 PC 机硬件组成结构、编制内核使用的汇编语言和 C 语言扩展部分,并且重点说明了80X86 处理器在保护模式下运行的编程方法。接着介绍了内核代码概况,给出了内核源代码目录树结构,并依据该组织结构对所有内核程序和文件进行了详细描述和说明。为了加深读者对内核工作原理的理解,书中最后一章给出了多个相关运行调试试验。书中所有相关资料和信息均可从网站 www.oldlinux.org 下载。本书适合作为高校计算机专业学生学习操作系统课程的辅助和实践教材,也适合 Linux 爱好者作为学习内核工作原理的自学参考书籍,还可作为一般技术人员开发嵌入式系统时的参考书。

序言

阅读早期内核的好处:

通过阅读Linux早期内核版本的源代码,的确是学习Linux系统的一种行之有效的途径,并且对研究和应用Linux嵌入式系统也有很大的帮助。

当然,使用早期内核作为学习的对象也有不足之处。所选用的Linux早期内核版本不包含对虚拟文件系统VFS的支持、对网络系统的支持、仅支持a.out执行文件和对其他一些现有内核中复杂子系统的说明。但由于本书是作为 Linux 内核工作机理实现的入门教材,因此这也正是选择早期内核版本的优点之一。通过学习本书,可以为进一步学习这些高级内容打下扎实的基础。

阅读完整源代码的重要性和必要性:

正如Linux系统的创始人在一篇新闻组投稿上所说的,要理解一个软件系统的真正运行机制,一定要阅读其源代码(RTFSC – Read The Fucking Source Code)。系统本身是一个完整的整体,具有很多看似不重要的细节存在,但是若忽略这些细节,就会对整个系统的理解带来困难,并且不能真正了解一个实际系统的实现方法和手段。

只有在详细阅读过完整的内核源代码之后,才会对系统有一种豁然开朗的感觉,对整个系统的运作过程有深刻的理解。以后再选择最新的或较新内核源代码进行学习时,也不会碰到大问题,基本上都能顺利地理解新代码的内容。

4.20版内核代码行数已超过2500万行,而0.12版内核不超过2万行代码量,因此完全可以在一本书中解释和注释清楚。麻雀虽小,五脏俱全。为了对所研究的系统有感性的了解,并能利用实验来加深对原理的理解,作者还专门重建了基于该内核的可运行的Linux 0.12系统。由于其中含有GNU gcc编译环境,因此使用该系统也能做一些简单的开发工作。另外,使用该版本可以避免使用现有较新内核版本中已经变得越来越复杂得各子系统部分的研究(如虚拟文件系统 VFS、ext2 或 ext3 文件系统、网络子系统、新的复杂的内存管理机制等)。

由于早期内核源代码量短小而精干,因此利用本书学习 Linux 内核会有极高的学习效率,能够做到事半功倍,快速入门。并且对继续进一步选择新内核部分源代码的学习打下坚实的基础。

在完整阅读完本书之后,相信您定会发出这样的感叹:“对于 Linux 内核系统,我现在终于入门了!”。此时,您应该有足够的把握去进一步学习最新 Linux 内核中各部分的工作原理和过程了。

1 概述

Linux 操作系统的诞生、发展和成长过程依赖于以下五个重要支柱:UNIX 操作系统、MINIX 操作 系统、GNU 计划、POSIX 标准和 Internet 网络。

  • UNIX 操作系统 -- UNIX 于 1969 年诞生在 Bell 实验室。Linux 就是 UNIX 的一种克隆系统。UNIX 的重要性就不用多说了。

  • MINIX 操作系统 -- MINIX 操作系统也是 UNIX 的一种克隆系统,它于 1987 年由著名计算机教授 Andrew S. Tanenbaum 开发完成。由于 MINIX 系统的出现并且提供源代码(只能免费用于大学内)在全世 界的大学中刮起了学习 UNIX 系统旋风。Linux 刚开始就是参照 MINIX 系统于 1991 年才开始开发。

  • GNU 计划-- 开发 Linux 操作系统,以及 Linux 上所用大多数软件基本上都出自 GNU 计划。Linux 只是操作系统的一个内核,没有 GNU 软件环境(比如说 bash shell),则 Linux 将寸步难行。

  • POSIX 标准 -- 该标准在推动 Linux 操作系统以后朝着正规路上发展起着重要的作用。是 Linux 前 进的灯塔。

  • INTERNET -- 如果没有 Intenet 网,没有遍布全世界的无数计算机黑客的无私奉献,那么 Linux 最多 只能发展到 0.13(0.95)版的水平。

Linux-0.12 版本发布于 1992 年 1 月 15 日。在发布时包括以下文件:

  • bootimage-0.12.Z - 具有美国键盘代码的压缩启动映像文件;
  • rootimage-0.12.Z - 以 1200kB 压缩的根文件系统映像文件;
  • linux-0.12.tar.Z - 内核源代码文件。大小为 130KB,展开后也仅有 463KB;
  • as86.tar.Z - Bruce Evans'二进制执行文件。是 16 位的汇编程序和装入程序;
  • INSTALL-0.11 - 更新过的安装信息文件。

Linus 在最初开发 Linux 操作系统内核时,主要参考了 3 本书。一本是 M. J. Bach 著的《UNIX 操作 系统设计》,该书描述了 UNIX System V 内核的工作原理和数据结构。Linus 使用了该书中很多函数的算 法,Linux 内核源代码中很多重要函数的名称都取自该书。因此,在阅读本书时,这是一本必不可少的 内核工作原理方面的参考书籍。另一本是 John H. Crawford 等编著的《Programming the 80386》,是讲解 80x86 下保护模式编程方法的好书。还有一本就是 Andrew S.Tanenbaum 著的《MINIX 操作系统设计与实 现》一书的第 1 版。Linus 主要使用了该书中描述的 MINIX 文件系统 1.0 版,而且在早期的 Linux 内核 中也仅支持该文件系统,所以在阅读本书有关文件系统一章内容时,文件系统的工作原理方面的知识完 全可以从 Tanenbaum 的书中获得。

最后要说明的是当你已经完全理解了本文所解说的一切时,并不代表你已经成为一个 Linux 行家了, 你只是刚刚踏上 Linux 的征途,具有了一定的成为一个 Linux 内核高手的初步知识。这时你应该去阅读更多的源代码,最好是循序渐进地从 1.0 版本开始直到最新的正在开发中的奇数编号的版本。

2 微型计算机组成结构

CPU 通过地址线、数据线和控制信号线组成的本地总线(或称为内部总线) 与系统其他部分进行数据通信。地址线用于提供内存或 I/O 设备的地址,即指明需要读/写数据的具体位置。 数据线用于在 CPU 和内存或 I/O 设备之间提供数据传输的通道,而控制线则负责指挥执行的具体读/写操作。对于使用 80386 CPU 的 PC 机,其内部地址线和数据线都分别有 32 根,即都是 32 位的。因此地址寻址空间范围有 2 ^ 32 字节,从 0 到 4GB

PC组成框图:

控制器和存储器接口通常都集成在计算机主板上,这些控制器分别都是以一块大规模集成电路芯片为主组成的功能电路。例如,中断控制器由 Intel 8259A 或其兼容芯片构成;DMA 控制器通常采用Intel 8237A 芯片构成;定时计数器的核心则是 Intel 8253/8254 定时芯片;键盘控制器使用的是 Intel 8042 芯片来与键盘中的扫描电路进行通信。

图中下方的控制卡(或者称为适配器)则是通过扩展插槽与主板上系统总线连接。总线插槽是系统地址总线、数据总线和控制线的与扩展设备控制器的标准连接接口。最初的 80386 机器上只有 ISA 总线,因此系统与外部 I/O 设备最多只能使用 16 位数据线进行数据传输。

现代PC组成框图:

虽然总线接口发生了很大变化,甚至今后北桥和南桥芯片都将会合二为一,但是对于我们编程人员来说,这些变化仍然与传统的 PC 机结构兼容。因此为传统 PC 机硬件结构编制的程序仍然能运行于现在的 PC 机上。这从 Intel 的开发手册上可以证实这个结论。所以为了便于入门学习,我们仍然以传统 PC 机结 构为框架来讨论和学习 PC 的组成和编程方法,当然这些方法仍然适合于现代 PC 机结构。

I/O端口寻址:

CPU 为了访问 I/O 接口控制器或控制卡上的数据和状态信息,需要首先指定它们的地址。这种地址就 称为 I/O 端口地址或者简称端口。通常一个 I/O 控制器包含访问数据的数据端口、输出命令的命令端口和 访问控制器执行状态的状态端口。端口地址的设置方法一般有两种:统一编址和独立编址。

端口统一编址的原理是把 I/O 控制器中的端口地址归入存储器寻址地址空间范围内。因此这种编址方 式也成为存储器映像编址。CPU 访问一个端口的操作与访问内存的操作一样,也使用访问内存的指令。端 口独立编址的方法是把 I/O 控制器和控制卡的寻址空间单独作为一个独立的地址空间对待,称为 I/O 地址 空间。每个端口有一个 I/O 地址与之对应,并且使用专门的 I/O 指令来访问端口。

IBM PC 及其兼容微机主要使用独立编址方式,采用了一个独立的 I/O 地址空间对控制设备中的寄存器进行寻址和访问。。使用 ISA 总线结构的传统 PC 机其 I/O 地址空间范围是 0x000 -- 0x3FF,有 1024 个 I/O 端口地址可供使用。各个控制器和控制卡所默认分配使用的端口地址范围见表 2-1 所示。

另外,IBM PC 机也部分地使用了统一编址方式。例如,CGA 显示卡上显示内存的地址就直接占用了存储器地址空间 0xB800 -- 0xBC00 范围。因此若要让一个字符显示在屏幕上,可以直接使用内存操作指令 往这个内存区域执行写操作。

对于使用 EISA 或 PCI 等总线结构的现代 PC 机,有 64KB 的 I/O 地址空间可供使用。在普通 Linux 系 统下通过查看/proc/ioports 文件可以得到相关控制器或设置使用的 I/O 地址范围。

典型 PC 机上通常含有三种类型的存储器,一种是用来运行程序和临时保存数据的内存存储器,一种 是存放着系统开机诊断和初始化硬件程序的 ROM,另一种是用来存放存计算机实时时钟信息和系统硬件 配置信息的少量 CMOS 存储器。

PC/AT 机内存使用区域图:

存放在 ROM 中的系统 BIOS 程序主要用于计算机开机时执行系统各部分的自检,建立起操作系统需要使用的各种配置表,例如中断向量表、硬盘参数表。并且把处理器和系统其余部分初始化到一个已知状态,而且还为 DOS 等操作系统提供硬件设备接口服务。但是由于 BIOS 提供的这些服务不具备可重入性(即 其中程序不可并发运行),并且从访问效率方面考虑,因此除了在初始化时会利用 BIOS 提供一些系统参数以外,Linux 操作系统在运行时并不使用 BIOS 中的功能。

当计算机系统上电开机或者按了机箱上的复位按钮时,CPU 会自动把代码段寄存器 CS 设置为 0xF000, 其段基地址则被设置为 0xFFFF0000,段长度设置为 64KB。而 IP 被设置为 0xFFF0,因此此时 CPU 代码指针指向 0xFFFFFFF0 处,即 4G 空间最后一个 64K 的最后 16 字节处。由上图可知,这里正是系统 ROM BIOS 存放的位置。并且 BIOS 会在这里存放一条跳转指令 JMP 跳转到 BIOS 代码中 64KB 范围内的某一条指令开始执行。

此后,BIOS 在执行了一些列硬件检测和初始化操作之后,就会把与原来 PC 机兼容的 64KB BIOS 代码和数据复制到内存低端 1M 末端的 64K 处,然后跳转到这个地方并让 CPU 真正运行在实地址模式下,见图 2-5 所示。最后 BIOS 就会从硬盘或其他块设备把操作系统引导程序加载到内存 0x7c00 处,并跳转到这个地方继续执行引导程序。

在 PC/AT 机中,除需要使用内存和 ROM BIOS 以外,还使用只有很少存储容量的(只有 64 或 128 字 节)CMOS(Complementary Metal Oxide Semiconductor,互补金属氧化物半导体)存储器来存放计算机的实时时钟信息和系统硬件配置信息。这部分内存通常和实时时钟芯片(Real Time Chip)做在一块集成块中。 CMOS 内存的地址空间在基本内存地址空间之外,需要使用 I/O 指令来访问。

DMA 控制器的主要功能是通过让外部设备直接与内存传输数据来增强系统的性能。通常它由机器上的 Intel 8237 芯片或其兼容芯片实现。通过对 DMA 控制器进行编程,外设与内存之间的数据传输能在不受 CPU 控制的条件下进行。因此在数据传输期间,CPU 可以做其他事情。

IBM PC/AT 及其兼容计算机可以使用彩色和单色显示卡。IBM 最早推出的 PC 机视频系统标准有单色 MDA 标准和彩色 CGA 标准以及 EGA 和 VGA 标准。以后推出的所有高级显示卡(包括现在的 AGP 显示卡)虽然都具有极高的图形处理速度和智能加速处理功能,但它们还是都支持这几种最初的标准。Linux 0.1x 操作系统仅使用了这几种标准都支持的文本显示方式。

由于硬盘具有很大的存储容量,并且读写速度很快,因此它是 PC 机中最大容量的外部存储设备,通常也被称为外存。

3 内核编程语言和环境

语言编译过程就是把人类能理解的高级语言转换成计算机硬件能理解和执行的二进制机器指令的过程。这种转换过程通常会产生一些效率不是很高的代码,所以对一些运行效率要求高或性能影响较大的部分代码通常就会直接使用低级汇编语言来编写,或者对高级语言编译产生的汇编程序再进行人工修改优化处理。

本章主要描述 Linux 0.12 内核中使用的编程语言、目标文件格式和编译环境,主要目标是提供阅读 Linux 0.12 内核源代码所需要的汇编语言和 GNU C 语言扩展知识。首先比较详细地介绍了 as86 和 GNU as 汇编程序的语法和使用方法,然后对 GNU C 语言中的内联汇编、语句表达式、寄存器变量以及内联函数 等内核源代码中常用的 C 语言扩展内容进行了介绍,同时详细描述了 C 和汇编函数之间的相互调用机制。

as86 和 ld86 是由 MINIX-386 的主要开发者之一 Bruce Evans 编写的 Intel 8086、80386 汇编编译程序和链接程序。在刚开始开发 Linux 内核时 Linus 就已经把它移植到了 Linux 系统上。它虽然可以为 80386 处 理器编制 32 位代码,但是 Linux 系统仅用它来创建 16 位的启动引导扇区程序 boot/bootsect.s 和实模式下初始设置程序 boot/setup.s 的二进制执行代码。该编译器快速小巧,并具有一些 GNU as 所没有的特性, 例如宏以及更多的错误检测手段。不过该编译器的语法与 GNU as 汇编编译器的语法不兼容而更近似于微 软的 MASM、Borland 公司的 Turbo ASM 和 NASM 等汇编器的语法。这些汇编器都使用了 Intel 的汇编语言语法(如操作数的次序与 GNU as 的相反等)。

现代 Linux 系统上可以直接安装包含 as86/ld86 的 RPM 软件包,例如 dev86-0.16.3-8.i386.rpm。由于 Linux 系统仅使用 as86 和 ld86 编译和链接上面提到的两个 16 位汇编程序 bootsect.s 和 setup.s,因此这里仅介绍 这两个程序中用到的一些汇编程序语法和汇编命令(汇编指示符)的作用和用途。

4 80X86 保护模式及其编程