《深入分析Linux内核源代码》陈莉君 编著
Linux 内核具有下列基本特征:
- Linux 内核的组织形式为整体式结构
- Linux 的进程调度方式简单而有效
- Linux 支持内核线程(或称守护进程)
- Linux 支持多种平台的虚拟内存管理
- Linux 内核另一个独具特色的部分是虚拟文件系统(VFS Virtul File Systen)
- Linux 的模块机制使得内核保持独立而又易于扩充
- 增加系统调用以满足特殊的需求
- 网络部分面向对象的设计思想使得 Linux 内核支持多种协议、多种网卡驱动程序变得容易
本书所分析的 Linux 内核版本是 2.4 版的 2.4.16 版。
Linux 操作系统由 4 个部分组成:
- 用户进程:用户应用程序是运行在 Linux 操作系统最高层的一个庞大的软件集合
- 系统调用接口:在应用程序中,可通过系统调用来调用操作系统内核中特定的过程,以实现特定的服务
- Linux 内核:内核实际是抽象的资源操作到具体硬件操作细节之间的接口
- 硬件:包括了 Linux 安装时需要的所有可能的物理设备
其中,Linux 内核由 5 个主要的子系统组成
- 进程调度(SCHED)控制着进程对 CPU 的访问
- 内存管理(MM)允许多个进程安全地共享主内存区域
- 虚拟文件系统(Virtul File System,VFS)隐藏了各种不同硬件的具体细节
- 网络接口(NET)提供了对各种网络标准协议的存取和各种网络硬件的支持
- 进程间通信(IPC) 支持进程间各种通信机制
各个子系统之间相互依赖。
操作系统是横跨软件和硬件的桥梁。
与硬件相关的代码全部放在 arch(architecture 一词的缩写,即体系结构相关)目录下。
- 通用寄存器:8 个通用寄存器
- 段寄存器:6 个 16 位的段寄存器
- 状态和控制寄存器:标志寄存器(EFLAGS)、指令指针(EIP)和 4 个控制寄存器组成
- 系统地址寄存器: 4 个系统地址寄存器
- 调试寄存器: 8 个 32 位的调试寄存器 DR0~DR7
- 测试寄存器:两个 32 位的测试寄存器 TR6 和 TR7
在 8086 的实模式下,把某一段寄存器左移 4 位,然后与地址 ADDR 相加后被直接送到内存总线上,这个相加后的地址就是内存单元的物理地址
,而程序中的这个地址就叫逻辑地址
(或叫虚地址)。在 80386 的保护模式下,这个逻辑地址不是被直接送到内存总线,而是被送到内存管理单元
(MMU)。
区分以下 3 种不同的地址:
- 逻辑地址:机器语言指令仍用这种地址指定一个操作数的地址或一条指令的地址
- 线性地址:线性地址是一个 32 位的无符号整数,可以表达高达 2^32(4GB)的地址
- 物理地址: 物理地址是内存单元的实际地址,用于芯片级内存单元寻址
在 80386 的段机制中,逻辑地址由两部分组成,即段部分(选择符)及偏移部分。
- 段是形成逻辑地址到线性地址转换的基础,包含:
- 段的基地址(Base Address)
- 段的界限(Limit)
- 段的属性(Attribute)
描述符(Descriptor):描述段的属性的一个 8 字节存储单元。
- 用户段描述符
- 系统段描述符
门也是一种描述符,有调用门、任务门、中断门和陷阱门 4 种门描述符。
各种各样的用户描述符和系统描述符,都放在对应的描述符表中。描述符表(即段表)定义了 386 系统的所有段的情况。
- 全局描述符表(GDT)
- 中断描述符表(IDT)
- 局部描述符表(LDT)
在实模式下,段寄存器存储的是真实的段地址,在保护模式下,16 位的段寄存器无法放下 32 位的段地址,因此,它们被称为选择符,即段寄存器的作用是用来选择描述符。
选择符有 3 个域:
- 索引域:用于指向全局描述符表中相应的描述符
- 选择域:局部还是全局
- 特权级:请求者特权级 RPL
386 的每一个段选择符都有一个程序员不可见(也就是说程序员不能直接操纵)的 88 位宽的段描述符高速缓冲寄存器与之对应。当选择符的值改变时,处理器自动装载不可见部分。
在没有分页操作时,寻址一个存储器操作数的步骤:
- 在段选择符中装入 16 位数,同时给出 32 位地址偏移量(比如在 ESI、EDI 中等)。
- 根据段选择符中的索引值、TI 及 RPL 值,再根据相应描述符表寄存器中的段地址和段界限,进行一系列合法性检查(如特权级检查、界限检查),该段无问题,就取出相应的描述符放入段描述符高速缓冲寄存器中。
- 将描述符中的 32 位段基地址和放在 ESI、EDI 等中的 32 位有效地址相加,就形成了 32 位物理地址。
在保护模式下,32 位段基地址不必向左移 4 位,而是直接和偏移量相加形成 32位物理地址(只要不溢出)
描述符投影寄存器:
每个段寄存器都有与之相联系的描述符投影寄存器。在这些寄存器中,容纳有由段寄存器中的选择符确定的段的描述符信息。段寄存器对编程人员是可见的,而与之相联系的容纳描述符的寄存器,则对编程人员是不可见的,故称之为投影寄存器。
Linux 中的段:
- Linux 内核的设计并没有全部采用 Intel 所提供的段方案,仅仅有限度地使用了一下分段机制。
- Linux 让所有的进程(或叫任务)都使用相同的逻辑地址空间,因此就没有必要使用局部描述符表 LDT。
- Linux 在启动的过程中设置了段寄存器的值和全局描述符表 GDT 的内容
- Linux 内核不区分数据段和堆栈段
- 内核代码段和数据段具有最高特权,因此其 RPL为 0,而用户代码段和数据段具有最低特权,因此其 RPL 为 3
GDT 放在数组变量 gdt_table 中
ENTRY(gdt_table)
.quad 0x0000000000000000 /* NULL descriptor */
.quad 0x0000000000000000 /* not used */
.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */
.quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */
.quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */
.quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */
.quad 0x0000000000000000 /* not used */
.quad 0x0000000000000000 /* not used */
/*
* The APM segments have byte granularity and their bases
* and limits are set at run time.
*/
.quad 0x0040920000000000 /* 0x40 APM set up for bad BIOS's */
.quad 0x00409a0000000000 /* 0x48 APM CS code */
.quad 0x00009a0000000000 /* 0x50 APM CS 16 code (16 bit) */
.quad 0x0040920000000000 /* 0x58 APM DS data */
.fill NR_CPUS*4,8,0 /* space for TSS's and LDT's */
- 段的基地址全部为 0x00000000;
- 段的上限全部为 0xffff;
- 段的粒度 G 为 1,即段长单位为 4KB;
- 段的 D 位为 1,即对这 4 个段的访问都为 32 位指令;
- 段的 P 位为 1,即 4 个段都在内存。
这样 Linux 巧妙地绕过了逻辑地址到线性地址的映射
分页机制在段机制之后进行,以完成线性—物理地址的转换过程。
- 分页机制由 CR0 中的 PG 位启用。如 PG=1,启用分页机制
- 分页机制管理的对象是固定大小的存储块,称之为页(page)
- 80386 使用 4K 字节大小的页
线性—物理地址的转换,可将其意义扩展为允许将一个线性地址标记为无效:
- 线性地址是操作系统不支持的地址
- 在虚拟存储器系统中,线性地址对应的页存储在磁盘上
分页机构:
两级页表结构,第一级称为页目录,第二级称为页表
页目录项:
- 第 31~12 位是 20 位页表地址
- 第 0 位是存在位
- 第 1 位是读/写位
- 第 2 位是用户/管理员位
- 第 3 位是 PWT(Page Write-Through)位
- 第 4 位是 PCD(Page Cache Disable)位
- 第 5 位是访问位
- 第 7 位是 Page Size 标志,只适用于页目录项
页面项:
第 6 位是页面项独有的,当对涉及的页面进行写操作时,D 位被置 1
线性地址到物理地址的转换:
- 第一步,CR3 包含着页目录的起始地址,用 32 位线性地址的最高 10 位 A31~A22 作为页目录的页目录项的索引,将它乘以 4,与 CR3 中的页目录的起始地址相加,形成相应页表的地址。
- 第二步,从指定的地址中取出 32 位页目录项,它的低 12 位为 0,这 32 位是页表的起始地址。用 32 位线性地址中的 A21~A12 位作为页表中的页面的索引,将它乘以 4,与页表的起始地址相加,形成 32 位页面地址。
- 第三步,将 A11~A0 作为相对于页面地址的偏移量,与 32 位页面地址相加,形成 32 位物理地址。
扩展分页:它允许页的大小为 4MB
页面高速缓存:
为了提高速度,在 386 中设置一个最近存取页面的高速缓存硬件机制,它自动保持 32 项处理器最近使用的页面地址,因此,可以覆盖 128K 字节的存储器地址。有些书上也把页面高速缓存叫做 “联想存储器” 或 “转换旁路缓冲器(TLB)”
Linux 主要采用分页机制来实现虚拟存储器管理,原因如下:
- Linux 设计目标之一就是能够把自己移植到绝大多数流行的处理器平台
- Linux 的分段机制使得所有的进程都使用相同的段寄存器值
为了保持可移植性,Linux 采用三级分页模式而不是两级,为此,Linux定义了 3 种类型的页表:
- 总目录 PGD(Page Global Directory)
- 中间目录 PMD(Page Middle Derectory)
- 页表 PT(Page Table)
尽管 Linux 采用的是三级分页模式,但我们的讨论还是以 Intel 奔腾处理器的两级分页模式为主,因此,Linux 忽略中间目录层,以后,我们把总目录就叫页目录。
与页相关的数据结构及宏的定义:
-
表项的定义
PGD、PMD 及 PT 表的表项:
#if CONFIG_X86_PAE typedef struct { unsigned long pte_low, pte_high; } pte_t; typedef struct { unsigned long long pmd; } pmd_t; typedef struct { unsigned long long pgd; } pgd_t;
Linux 没有把这几个类型直接定义长整数而是定义为一个结构,这是为了让 gcc 在编译时进行更严格的类型检查。定义了几个宏来访问这些结构的成分:
#define pte_val(x) ((x).pte_low) #define pmd_val(x)((x).pmd) #define pgd_val(x) ((x).pgd)
一个页面保护结构 pgprot_t 和一些宏,字段 pgprot 的值与图 2.24 页面项的低 12 位相对应,其中的 9 位对应 0~9 位:
typedef struct { unsigned long pgprot; } pgprot_t; #define pgprot_val(x) ((x).pgprot)
#define _PAGE_PRESENT 0x001 #define _PAGE_RW 0x002 #define _PAGE_USER 0x004 #define _PAGE_PWT 0x008 #define _PAGE_PCD 0x010 #define _PAGE_ACCESSED 0x020 #define _PAGE_DIRTY0x040 #define _PAGE_PSE 0x080 /* 4 MB (or 2MB) page, Pentium+, if present.. */ #define _PAGE_GLOBAL 0x100 /* Global TLB entry PPro+ */
页目录表及页表在 pgtable.h 中定义。
-
线性地址域的定义
其中 PAGE_SHIFT 宏定义了偏移量的位数为 12,因此页大小 PAGE_SIZE 为 212=4096 字节;PTRS_PER_PTE 为页表的项数;最后 PAGE_MASK 值定义为 0xfffff000,用以屏蔽掉偏移量域的所有位(12 位)。
GDIR_SHIFT 是页表所能映射区域线性地址的位数,它的值为 22(12 位的偏移量加上10 位的页表);PTRS_PER_PGD 为页目录目录项数;PGDIR_SIZE 为页目录的大小,为 222,即 4MB;PGDIR_MASK 为 0xffc00000,用于屏蔽偏移量位与页表域的所有位。
PMD_SHIFT 为中间目录表映射的地址位数,其值也为 22。
#define PAGE_SHIFT 12 #define PAGE_SIZE (1UL << PAGE_SHIFT) #define PTRS_PER_PTE 1024 #define PAGE_MASK (~(PAGE_SIZE-1)) #define PGDIR_SHIFT22 #define PTRS_PER_PGD 1024 #define PGDIR_SIZE (1UL << PGDIR_SHIFT) #define PGDIR_MASK (~(PGDIR_SIZE-1)) #define PMD_SHIFT 22 #define PTRS_PER_PMD 1
-
对页目录及页表的处理
在 page.h,pgtable.h 及 pgtable-2level.h3 个文件中还定义有大量的宏,用以对页目录、页表及表项的处理
- pgd_none()函数直接返回 0,表示尚未为这个页目录建立映射,所以页目录项为空。
- pgd_present()函数直接返回 1,表示映射虽然还没有建立,但页目录所映射的页表肯定存在于内存(即页表必须一直在内存)
- pte_present 宏的值为 1 或 0,表示 P 标志位。
- pgd_clear 宏实际上什么也不做
- pte_clear 就是把 0 写到页表表项中
- 对页表表项标志值进行操作的宏:这些宏的代码在 pgtable.h 文件中
AT&T的 386 汇编语言
- 在 AT&T 中,寄存器前冠以“%”,而立即数前冠以“$”
- 在 AT&T 中,十六进制立即数前冠以“0x“
- Intel 与 AT&T 操作数的方向正好相反,在 AT&T 中,第一个数是源操作数,第二个数是目的操作数。
- 在 AT&T 中,内存单元操作数用“()”括起来。
- AT&T 间接寻址方式可能更晦涩难懂一些:%segreg:disp(base,index,scale)。这种寻址方式常常用在访问数据结构数组中某个特定元素内的一个字段,其中,base 为数组的起始地址,scale 为每个数组元素的大小,index 为下标。如果数组元素还是一个结构,则 disp 为具体字段在结构中的位移。
- AT&T 的操作码后面有一个后缀,其含义就是指出操作码的大小。
- 以.S 为扩展名的文件是“纯”汇编语言的文件。
一些 AT&T 汇编语言的相关:
- GNU 汇编程序 GAS(GNU Assembly)和连接程序
- AT&T 中的节(Section):至少需要有以下 3 种节
- section .data
- .section .bss
- section .text
- 汇编程序指令(Assembler Directive):以句点(.)为开头,后跟指令名(小写字母)
- .ascii "string"...
- .byte 表达式
- .fill 表达式
- .globl symbol
- .quad bignums
- .rept count
- .space size , fill
- .word expressions
- .long expressions
- .org new-lc , fill
- gcc 嵌入式汇编:
__asm__ __volatile__ ("<asm routine>" : output : input : modify);
"<asm routine>"
为汇编指令部分- 输出部分(output),用以规定对输出变量(目标操作数)如何与寄存器结合的约束(constraint)
- 输入部分(Input):输入部分与输出部分相似,但没有“=”。
- 修改部分(modify):这部分常常以“memory”为约束条件,以表示操作完成后内存中的内容已有改变
- 指令部分为必选项,而输入部分、输出部分及修改部分为可选项
- 一些常用的 386 汇编指令及其功能:
- 位操作指令
- 控制转移类指令
- 数据传输指令
- 标志控制类指令
- 逻辑类指令
- 串操作指令
- 多段类操作指令
- 操作系统类指令