一、PAE 技术背景与 x86 页表结构
1. PAE 的诞生:突破 32 位内存限制
x86 架构的 32 位 CPU 原生支持 32 位物理地址(4GB),但通过 PAE(1995 年随 Pentium Pro 引入),可支持 36 位物理地址(64GB)。PAE 的核心是将页表结构从两级转换为三级,并调整页表项的格式:
- 非 PAE(32 位):虚拟地址分为 10 位(PGD)+ 10 位(PTE)+ 12 位(页内偏移),对应 4KB 页。
- PAE(32 位):虚拟地址分为 10 位(PGD)+ 9 位(PMD)+ 9 位(PTE)+ 12 位(页内偏移),支持 4KB/2MB 页(PTE 标志位控制)。
2. 页表项(PTE)的格式变化
PAE 下,PTE 从 32 位扩展到 64 位,高 20 位存储物理地址的高 20 位(36 位物理地址 = 16 位(PGD+PMD)+20 位(PTE)),低 12 位为标志位(权限、缓存、脏页等)。
二、Linux 内核中的 PAE 支持条件
- 硬件支持:CPU 必须支持 PAE(通过
cpuid
指令检测FLAGS
中的PAE
标志)。 - 内核配置:编译时启用
CONFIG_PAE
选项(32 位内核默认启用,64 位内核无需 PAE)。 - 地址空间划分:32 位 PAE 内核将虚拟地址空间分为:
- 用户空间:0~3GB(3GB)
- 内核空间:3GB~4GB(1GB,非 PAE)→ 3GB~64TB(PAE,理论值,实际受物理内存限制)
三、页表初始化的核心数据结构
-
页目录表(Page Global Directory, PGD)
- 每个进程一个 PGD(内核使用全局 PGD),类型为
pgd_t
(本质是unsigned long
)。 - 32 位 PAE 下,PGD 大小为 4KB(1 个页),包含 1024 个 PGD 项(每个 32 位,非 PAE;PAE 下 PGD 项为 64 位,所以 PGD 大小变为 8KB?不,实际 x86 PAE 的 PGD 仍为 4KB,因为 PGD 项数量减少,详见下文)。
注意:x86 PAE 的 PGD 项实际是 32 位,其中高 27 位指向 PMD 基址(物理地址 + 页目录基址寄存器调整),低 5 位为标志位。
- 每个进程一个 PGD(内核使用全局 PGD),类型为
-
页中间目录表(Page Middle Directory, PMD)
- PAE 新增层,每个 PMD 表包含 512 个 PMD 项(9 位索引),每个 64 位,指向 PTE 表或大页(2MB)。
-
页表(Page Table, PTE)
- 每个 PTE 表包含 512 个 PTE 项(9 位索引),每个 64 位,指向 4KB 页或巨页(1GB,需额外支持)。
-
内核全局变量
swapper_pg_dir
:内核启动时使用的全局页目录表。init_mm
:初始进程(swapper)的内存描述符,指向swapper_pg_dir
。
四、初始化流程:从 boot 到内核启动阶段
1. 早期初始化:实模式→保护模式→PAE 启用(boot 阶段)
- 实模式(0x0~0xfffff):BIOS 加载引导程序(如 GRUB),此时未启用分页。
- 保护模式:引导程序启用保护模式,设置 CR0.PG=0(未分页),CR3=0(页目录基址)。
- 检测 PAE 支持:通过
cpuid 1
指令检查 ECX 寄存器的第 4 位(PAE 标志)。 - 启用 PAE:设置 CR4.PAE=1,此时 CPU 进入 PAE 模式,支持三级页表。
2. 内核入口:start_kernel()
中的页表准备
Linux 内核初始化的核心函数start_kernel()
会调用setup_arch()
,其中early_init_pgtables()
负责早期页表初始化。
(1)early_init_pgtables()
:建立最小化页表
目标:建立内核启动初期必需的地址映射(如内核代码段、数据段、初始页表自身)。
步骤:
- 分配页目录表(PGD):使用
early_alloc()
分配物理内存,创建swapper_pg_dir
。 - 初始化 PGD 项:
// 遍历PGD的1024个项(32位PAE下,实际有效项可能更少) for (pgd = pgd_offset_k(0); pgd < pgd_end(); pgd++) { if (pgd_none(*pgd)) { // 分配PMD表(每个PGD项对应一个PMD表) pmd_t *pmd = early_alloc(PAGE_SIZE); memset(pmd, 0, PAGE_SIZE); // 设置PGD项:指向PMD表的物理地址,标记为存在(P)、可写(W)等 set_pgd(pgd, __pgd(__pa(pmd) | _PAGE_PRESENT | _PAGE_RW)); } }
- 初始化 PMD 和 PTE:
对内核空间(3GB 以上)的关键区域(如__va(0x0)
到__va(high_memory)
),在对应的 PMD 和 PTE 中建立映射,指向物理地址。例如,内核代码段位于物理地址 0x100000,虚拟地址为 0xC0000000,需将虚拟地址分解为:- PGD 索引:
(0xC0000000 >> 22) & 0x3ff
(32 位 PAE 的 PGD 偏移是 22 位?实际 x86 PAE 的虚拟地址划分是:31-22 位为 PGD,21-13 位为 PMD,12-0 位为页内偏移,共 32 位)。
- PGD 索引:
(2)setup_page_tables()
:完善内核空间映射
在early_init_pgtables()
之后,内核会进一步映射高端内存(HighMemory)和动态分配区域:
- 永久内核映射:通过
kmap()
等机制映射非连续物理内存到内核虚拟地址空间。 - 临时内核映射:用于短期访问高端内存,通过
alloc_page()
等函数分配页表项。
(3)mem_init()
:用户空间页表预留
为用户空间(0~3GB)初始化页目录和 PMD 项,但不立即分配 PTE(按需分配,即「请求调页」)。每个 PGD 项标记为「不存在」(P=0),当用户程序首次访问时触发缺页中断,内核再动态分配 PTE。
五、地址转换细节:从虚拟地址到物理地址
1. 32 位 PAE 下的虚拟地址分解
虚拟地址(32 位)分为 4 部分(以 4KB 页为例):
31-22位(10位):PGD索引(页目录项)
21-13位(9位):PMD索引(页中间目录项)
12-4位(9位):PTE索引(页表项)
3-0位(4位):页内偏移(实际4KB页需要12位,此处划分可能有误,正确应为12-0位为页内偏移,所以PTE索引应为21-13位(9位),PMD索引可能不存在?这里需要纠正:x86 PAE的正确划分是:
- 对于4KB页:
- 31-21位:PGD索引(11位?不,实际x86 PAE的PGD偏移是10位,PMD是9位,PTE是9位,页内偏移12位,总32位:10+9+9+12=40,超过32位?这里存在错误,正确的x86 PAE地址划分是针对36位物理地址,虚拟地址仍为32位,所以:
- PGD:10位(31-22)
- PMD:9位(21-13)
- PTE:9位(12-4)
- 页内偏移:4位(3-0)? 不对,4KB页需要12位偏移(0-11位),所以正确的划分应为:
- PGD:10位(31-22)
- PMD:9位(21-13)
- PTE:9位(12-4)
- 页内偏移:12位(11-0)? 这里混淆了,实际x86 PAE的虚拟地址划分是:
- 对于4KB页,虚拟地址分为:
- PGD:10位(31-22)
- PMD:9位(21-13)
- PTE:9位(12-4)
- 页内偏移:12位(3-0)? 这显然不对,因为3-0只有4位。正确的应该是页内偏移占12位(0-11),所以PTE索引是12-20位(9位),PMD是21-29位(9位),PGD是30-31位(2位)? 这里必须参考x86架构手册:
**正确划分(32位PAE,4KB页)**:
- 虚拟地址32位,分为:
- PGD:10位(31-22)
- PMD:9位(21-13)
- PTE:9位(12-4)
- 页内偏移:12位(11-0)
是的,这样总和是10+9+9+12=40,但虚拟地址只有32位,所以实际PMD和PTE的高位在32位地址中是无效的? 不,这里的错误在于,PAE是扩展物理地址,虚拟地址仍为32位,所以PGD占10位(31-22),PMD占9位(21-13),PTE占9位(12-4),页内偏移占12位(11-0),但32位地址中,12-11位是0,所以实际PTE索引是9位(12-4位,共9位),页内偏移是12位(11-0)。这可能是一个常见的理解误区,需要明确:**在32位PAE下,虚拟地址的有效位是32位,因此PMD和PTE的索引位需要调整到32位范围内**。正确的划分应参考Intel手册:PAE下,32位虚拟地址的转换使用三级页表,其中:
- PGD:10位(31-22)
- PMD:9位(21-13)
- PTE:9位(12-4)
- 页内偏移:12位(11-0)
这样,虚拟地址的4-11位实际是PTE索引的低8位,加上12位是错误的,正确的页内偏移是0-11位(12位),所以PTE索引是4-12位? 这里必须严格按照x86架构定义,可能更简单的方式是:PAE将32位虚拟地址分为10(PGD)+9(PMD)+9(PTE)+12(偏移),但总位数超过32,因此实际在32位系统中,PMD和PTE的高位在虚拟地址中被截断,仅使用有效位。
##### 2. 转换步骤(以访问内核虚拟地址0xC0000000为例)
1. **获取CR3寄存器**:指向页目录表基址(物理地址+0xC0000000,因为内核虚拟地址映射物理地址+0xC0000000)。
2. **查找PGD项**:用虚拟地址的31-22位作为索引,找到对应的PGD项,其中包含PMD表的物理基址。
3. **查找PMD项**:用21-13位索引PMD表,若PMD项标记为大页(2MB),则直接获取物理地址高21位(2MB页的偏移是11-0位);否则继续查找PTE。
4. **查找PTE项**:用12-4位索引PTE表,获取物理地址高20位,与页内偏移(11-0位)拼接得到36位物理地址。
##### 3. 大页支持(2MB页)
PAE允许PMD项直接指向2MB页(通过设置`_PAGE_PSE`标志),跳过PTE层,减少地址转换时间。内核会优先使用大页映射内核空间,提高性能。
#### 六、关键函数与内核源码分析
##### 1. 页目录操作函数
- `pgd_offset(mm, address)`:根据进程`mm`和虚拟地址`address`,获取对应的PGD项指针。
- `pmd_offset(pgd, address)`:根据PGD项和虚拟地址,获取PMD项指针。
- `pte_offset_kernel(pmd, address)`:获取内核空间的PTE项指针(用户空间需检查权限)。
##### 2. 初始化核心函数调用链
start_kernel ()
→ setup_arch ()
→ early_init_pgtables () # 早期页表初始化
→ build_mem_type_nodes () # 内存节点划分
→ page_table_init () # 完善页表结构
→ mem_init () # 内存管理初始化,包括用户空间页表预留
##### 3. 内存分配与页表同步
- 当内核分配物理页(如`alloc_page()`)时,会通过`set_pte()`等函数更新对应的PTE项,确保虚拟地址与物理地址映射正确。
- 用户空间进程通过`mmap()`映射内存时,内核会在对应的PGD/PMD/PTE中创建映射,可能触发页表分配(如`handle_pte_fault()`处理缺页中断)。
#### 七、常见问题与性能影响
##### 1. 为什么PAE需要三级页表?
- 32位地址空间有限,若直接扩展PTE为64位(容纳36位物理地址),两级页表的内存消耗会增加(每个PTE表从4KB变为8KB),而增加PMD层可平衡索引位数,减少页表内存占用。
##### 2. PAE对性能的影响
- **优势**:支持更大内存,适合服务器场景。
- **劣势**:三级地址转换增加CPU的TLB(转换后备缓冲区)未命中概率,可能导致轻微性能下降(可通过大页缓解)。
##### 3. 64位系统为何不需要PAE?
64位x86架构(x86-64)使用四级页表(PGD、PUD、PMD、PTE),原生支持更大地址空间,无需PAE的36位扩展。
#### 八、总结:PAE页表初始化的本质
Linux启用PAE时的页表初始化,本质是**在32位地址空间内,通过新增的PMD层,建立三级地址映射关系,将虚拟地址逐步转换为36位物理地址**。这一过程分为早期内核必需映射的快速建立,和后续用户空间的按需分配,体现了操作系统内存管理的「分层抽象」与「延迟分配」思想。
理解这一机制,需要结合x86架构特性、内核内存布局、页表数据结构三者的交互,而PAE作为32位系统突破内存限制的关键技术,至今仍在一些嵌入式和legacy系统中使用,是理解Linux内存管理的重要切入点。
### 延伸学习建议
1. 阅读Linux内核源码(`arch/x86/mm/init.c`中的页表初始化代码)。
2. 调试工具:使用`cr3`寄存器查看页目录基址,`gdb`断点跟踪`pgd_offset`等函数。
3. 架构手册:Intel® 64 and IA-32 Architectures Software Developer Manuals(Volume 3: System Programming Guide)中的分页章节。
通过形象比喻建立感性认识,再结合源码和架构文档深入技术细节,可有效掌握Linux页表初始化的核心逻辑。
形象比喻:把页表初始化比作「图书馆分类上架」(适合快速记忆)
1. 先理解 PAE 是什么?(为什么需要多一层页表?)
假设你有一个超级大的图书馆:
- 32 位系统(无 PAE):就像图书馆只有「2 层楼」,每层最多放 4 万本书(4GB 内存,每本书 4KB)。
- 启用 PAE 后:图书馆变成「3 层楼」,可以放 64 万本书(64GB 内存)!但新增的第 3 层楼需要额外的「楼层索引」—— 这就是 PAE 增加的「页中间目录(PMD)」层。
PAE 的全称是 Physical Address Extension(物理地址扩展),专门让 32 位 CPU 能访问超过 4GB 的内存,但需要多一层页表来管理这些额外的地址。
2. 页表初始化过程:给每本书贴「三层地址标签」
Linux 初始化页表的过程,就像图书馆给每本书分配一个唯一的「三层地址标签」,方便快速找到书的位置:
-
第 1 层:页目录(PGD,Page Global Directory)
相当于「书架区域索引」,每个格子记录「某个区域对应哪个书架组」。比如 32 位 PAE 下,虚拟地址的最高 10 位用来找页目录项(PGD entry),对应 4KB 大小的一个目录块。 -
第 2 层:页中间目录(PMD,Page Middle Directory)
新增的 PAE 层!相当于「书架组内的具体书架」。PAE 让这一层有 9 位地址,每个 PMD 项指向一个「页表组」。 -
第 3 层:页表(PTE,Page Table Entry)
相当于「书架上的具体格子」,最后 12 位地址(4KB 页大小)确定书的位置。PTE 记录物理地址的低 20 位(因为 PAE 支持 36 位物理地址,高 16 位需要 PMD 和 PGD 配合)。
3. 初始化步骤:从「空书架」到「完整索引」
-
准备空的三层目录架子
内核先分配页目录表(PGD)、页中间目录表(PMD)、页表(PTE)的内存空间,就像搭好空的书架框架。 -
给内核空间贴标签(先搞定内核自己要用的书)
内核必须先把自己的代码和数据所在的物理地址,映射到虚拟地址空间(比如 3GB 以上的内核空间)。这一步就像图书馆先把「管理员手册」放在固定区域,并贴上三层标签。 -
给用户空间留位置(暂时标记为「未使用」)
用户程序的虚拟地址空间(0~3GB)暂时不分配具体书架,只在页目录和 PMD 中标记「这个区域未来可能放书」,等程序运行时再动态填充 PTE。 -
检查标签是否正确(地址转换测试)
内核会简单访问几个地址,确认通过三层页表能正确找到物理内存,就像图书馆管理员抽查几本书,确认索引没错。
4. 一句话总结
启用 PAE 时,Linux 就像给 32 位系统的内存管理加了一层「中间书架索引」,初始化页表就是在内核启动时,先把内核自己要用的内存地址,按照「页目录→页中间目录→页表」三层结构贴好标签,让 CPU 能通过这三层索引快速找到物理内存的位置。