3、四级页表概述:Linux内核四级页表(PGD/P4D/PUD/PMD/PTE)的层级结构与设计动机
随着64位处理器(如x86_64、ARMv8)的普及,虚拟地址空间从32位的4GiB扩展到48位(256TiB)甚至52位。为了在巨大的地址空间中高效管理物理内存,同时避免单级页表占用过多连续物理内存,Linux内核引入了四级页表(Four-Level Page Table)架构。本章将详细解析PGD、P4D、PUD、PMD、PTE这五个层级的作用、索引流程以及背后的设计动机。
3.1 四级页表的层级结构
四级页表将虚拟地址划分为5个位段,分别用于索引5个不同层级的页表。每个层级负责将虚拟地址空间进一步细分,最终定位到物理页框。以下是标准48位虚拟地址在四级页表中的划分方式:
| 层级 | 缩写 | 全称 | 位段(48位地址) | 页表项数量 | 覆盖地址范围 |
|---|---|---|---|---|---|
| 第1级 | PGD | Page Global Directory | bit 47:39(9位) | 512 | 256 TiB |
| 第2级 | P4D | Page 4th-level Directory | bit 38:30(9位) | 512 | 512 GiB |
| 第3级 | PUD | Page Upper Directory | bit 29:21(9位) | 512 | 1 GiB |
| 第4级 | PMD | Page Middle Directory | bit 20:12(9位) | 512 | 2 MiB |
| 第5级 | PTE | Page Table Entry | bit 11:0(12位) | 512 | 4 KiB |
3.2 虚拟地址到物理地址的遍历流程
当CPU访问一个虚拟地址时,MMU(内存管理单元)会按照以下顺序遍历页表:
- PGD索引:从虚拟地址的bit 47:39提取9位索引,在PGD表中找到对应的页全局目录项。该条目指向下一级页表(P4D)的基地址。
- P4D索引:从bit 38:30提取9位索引,在P4D表中找到对应的页四级目录项。该条目指向PUD表的基地址。
- PUD索引:从bit 29:21提取9位索引,在PUD表中找到对应的页上级目录项。该条目指向PMD表的基地址。
- PMD索引:从bit 20:12提取9位索引,在PMD表中找到对应的页中间目录项。该条目指向PTE表的基地址。
- PTE索引:从bit 11:0提取12位页内偏移,在PTE表中找到最终的页表项。该条目包含物理页框号(PFN)和权限位。
- 物理地址合成:将PTE中的物理页框号与虚拟地址的页内偏移(bit 11:0)拼接,得到最终的物理地址。
3.3 四级页表设计动机
为什么需要四级页表,而不是三级或五级?其核心动机包括:
- 支持超大虚拟地址空间:48位地址空间需要2^48个虚拟地址。如果使用单级页表,每个进程需要一张包含2^48个条目的页表,每个条目8字节,总大小达2 PiB,完全不可行。四级页表通过分层,将页表拆分为多个小表,按需分配。
- 节省物理内存:大多数进程只使用虚拟地址空间的一小部分。四级页表允许内核只为实际使用的地址范围分配页表。例如,一个进程只使用低2GiB地址,则只需分配PGD、P4D、PUD、PMD和PTE的部分条目,而非全部。
- 支持大页(Huge Pages):在PMD或PUD层级,可以设置“大页”标志,使一个页表项直接映射2MiB(PMD大页)或1GiB(PUD大页)的连续物理内存。这减少了TLB压力,提升了性能。
- 架构兼容性:Linux内核通过
#ifdef和CONFIG_PGTABLE_LEVELS宏,可以在编译时选择3级、4级或5级页表。四级页表是x86_64和ARM64(48位地址)的默认配置,同时为未来扩展到52位地址(五级页表)预留了接口。
3.4 四级页表遍历的SVG架构图
下图展示了虚拟地址通过四级页表逐级索引,最终得到物理地址的完整流程:
3.5 页表项(PTE)结构
每个页表项(PTE)是一个64位的值,包含物理页框号(PFN)和一系列标志位。以下是x86_64架构下PTE的典型布局:
| 位段 | 宽度 | 描述 |
|---|---|---|
| bit 63:52 | 12位 | 保留位(用于未来扩展或软件使用) |
| bit 51:12 | 40位 | 物理页框号(PFN),指向4KiB对齐的物理页 |
| bit 11:9 | 3位 | 忽略位(Ignored) |
| bit 8 | 1位 | 全局位(Global,G) |
| bit 7 | 1位 | 页面大小位(Page Size,PS),用于大页 |
| bit 6 | 1位 | 脏位(Dirty,D) |
| bit 5 | 1位 | 访问位(Accessed,A) |
| bit 4:3 | 2位 | 缓存策略(PWT/PCD) |
| bit 2 | 1位 | 用户/超级用户位(User/Supervisor,U/S) |
| bit 1 | 1位 | 读/写位(Read/Write,R/W) |
| bit 0 | 1位 | 存在位(Present,P),为1时表示页表项有效 |
pte_t结构体抽象了这些差异,使得页表遍历代码在架构间可移植。
3.6 页表层级折叠(Folding)
并非所有架构都使用完整的四级页表。当虚拟地址位宽小于48位时,内核会“折叠”中间的层级。例如,ARM64使用39位地址时,P4D和PUD被折叠:
- PGD → PMD:PGD直接指向PMD,跳过P4D和PUD。
- 代码实现:内核通过
pgtable-nop4d.h和pgtable-nopud.h头文件,将P4D和PUD定义为单条目表,从而在编译时消除这些层级。 - 性能收益:减少页表遍历的深度,降低TLB未命中时的硬件遍历延迟。
CONFIG_PGTABLE_LEVELS=3,此时pgd_offset()宏直接返回pmd_offset(),无需经过P4D和PUD。
3.7 四级页表的内存开销估算
假设一个进程使用了1GiB的虚拟地址空间(连续映射),页大小为4KiB,则各级页表的内存开销如下:
| 层级 | 条目数 | 每项大小 | 总大小 |
|---|---|---|---|
| PGD | 512 | 8字节 | 4 KiB |
| P4D | 1(折叠) | 8字节 | 8字节(实际分配一个页表页) |
| PUD | 1(折叠) | 8字节 | 8字节 |
| PMD | 512 | 8字节 | 4 KiB |
| PTE | 512 × 512 = 262,144 | 8字节 | 2 MiB |
| 总计 | 约2.008 MiB |
如果使用2MiB大页(PMD大页),则PTE层被跳过,PMD直接指向物理页框,总页表开销降至约12 KiB(PGD + P4D + PUD + PMD)。这解释了为什么数据库和HPC应用倾向于使用大页。
3.8 小结
四级页表是Linux内核在64位时代的核心地址转换机制。它通过分层索引,在支持超大地址空间的同时,保持了内存使用的效率。PGD、P4D、PUD、PMD、PTE这五个层级分别对应虚拟地址的不同位段,硬件MMU负责自动遍历。理解这一结构,是后续学习ARM64硬件页表遍历、TLB管理以及内核内存管理子系统的基础。