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
关键点:在四级页表架构中,P4D层是x86_64引入48位地址空间时新增的层级。对于只使用39位地址的ARM64(如页大小为4KiB、地址位宽为39位时),P4D和PUD会被“折叠”(folded),即直接跳过,由PGD指向PMD。

3.2 虚拟地址到物理地址的遍历流程

当CPU访问一个虚拟地址时,MMU(内存管理单元)会按照以下顺序遍历页表:

  1. PGD索引:从虚拟地址的bit 47:39提取9位索引,在PGD表中找到对应的页全局目录项。该条目指向下一级页表(P4D)的基地址。
  2. P4D索引:从bit 38:30提取9位索引,在P4D表中找到对应的页四级目录项。该条目指向PUD表的基地址。
  3. PUD索引:从bit 29:21提取9位索引,在PUD表中找到对应的页上级目录项。该条目指向PMD表的基地址。
  4. PMD索引:从bit 20:12提取9位索引,在PMD表中找到对应的页中间目录项。该条目指向PTE表的基地址。
  5. PTE索引:从bit 11:0提取12位页内偏移,在PTE表中找到最终的页表项。该条目包含物理页框号(PFN)和权限位。
  6. 物理地址合成:将PTE中的物理页框号与虚拟地址的页内偏移(bit 11:0)拼接,得到最终的物理地址。
硬件加速:现代CPU的MMU会缓存最近使用的页表项到TLB(Translation Lookaside Buffer)中。如果TLB命中,则直接返回物理地址,无需遍历页表。TLB未命中时,硬件自动执行上述遍历过程(称为“硬件页表遍历”)。

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架构图

下图展示了虚拟地址通过四级页表逐级索引,最终得到物理地址的完整流程:

四级页表硬件遍历流程(48位虚拟地址) 虚拟地址(48位) bit 47:39 (PGD) | bit 38:30 (P4D) | bit 29:21 (PUD) | bit 20:12 (PMD) | bit 11:0 (PTE/偏移) PGD(页全局目录) 索引 bit 47:39 → 512 项 P4D(页四级目录) 索引 bit 38:30 → 512 项 PUD(页上级目录) 索引 bit 29:21 → 512 项 PMD(页中间目录) 索引 bit 20:12 → 512 项 PTE(页表项) 索引 bit 11:0 → 物理页框 + 偏移

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时表示页表项有效
注意:ARM64的PTE布局与x86_64不同,但核心概念一致:高位存储物理地址,低位存储权限和状态标志。Linux内核通过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未命中时的硬件遍历延迟。
内核宏示例:在ARM64 39位地址配置下,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管理以及内核内存管理子系统的基础。