3. x86_64 页表结构:PML4、PDPT、PD、PT 的层级关系与索引计算
x86_64 架构采用 4 级页表 将 48 位虚拟地址转换为 52 位物理地址(实际使用 48 位物理地址)。每一级页表负责将虚拟地址的一部分索引转换为下一级页表的物理地址,最终找到物理页框。这种多级结构在节省内存(按需分配页表)和支持大页映射方面具有显著优势。
3.1 四层页表层级总览
从虚拟地址的高位到低位,依次经过以下四级页表:
| 层级 | 英文缩写 | 全称 | 每项大小 | 每表项数 | 覆盖地址范围 |
|---|---|---|---|---|---|
| 第 1 级 | PML4 | Page Map Level 4 | 8 字节 | 512 | 512 GB |
| 第 2 级 | PDPT | Page Directory Pointer Table | 8 字节 | 512 | 1 GB |
| 第 3 级 | PD | Page Directory | 8 字节 | 512 | 2 MB |
| 第 4 级 | PT | Page Table | 8 字节 | 512 | 4 KB |
关键点: 每级页表都包含 512 个条目(2^9),因为虚拟地址中用于索引每级页表的字段宽度为 9 位。每个条目 8 字节,因此每张页表大小为 4 KB(512 × 8 = 4096 字节),恰好与一个物理页框大小一致。
3.2 虚拟地址分解与索引计算
一个 48 位虚拟地址被分解为 5 个部分,从高位到低位依次为:
- PML4 索引(位 47:39):9 位,用于在 PML4 表中定位条目。
- PDPT 索引(位 38:30):9 位,用于在 PDPT 表中定位条目。
- PD 索引(位 29:21):9 位,用于在 PD 表中定位条目。
- PT 索引(位 20:12):9 位,用于在 PT 表中定位条目。
- 页内偏移(位 11:0):12 位,用于在 4 KB 物理页框内寻址。
索引计算非常简单:将虚拟地址右移相应位数,再与 0x1FF(即 511)进行按位与操作。
// 假设 addr 是一个 unsigned long 类型的虚拟地址
unsigned long pml4_idx = (addr >> 39) & 0x1FF;
unsigned long pdpt_idx = (addr >> 30) & 0x1FF;
unsigned long pd_idx = (addr >> 21) & 0x1FF;
unsigned long pt_idx = (addr >> 12) & 0x1FF;
unsigned long offset = addr & 0xFFF;
硬件自动完成: 上述索引计算和页表遍历完全由 CPU 的 MMU(内存管理单元)硬件自动完成,操作系统只需维护好各级页表在物理内存中的结构。Linux 内核通过
pgd_offset()、pud_offset()、pmd_offset()、pte_offset() 等宏来模拟这一遍历过程。
3.3 页表层级关系与遍历流程
下图展示了从虚拟地址到物理地址的完整遍历路径:
3.4 页表条目格式
每个页表条目(PTE)为 8 字节,包含物理地址高位和标志位。以最后一级 PT 条目为例:
| 位域 | 宽度 | 描述 |
|---|---|---|
| 位 63:52 | 12 位 | 保留 / 未来扩展(必须为 0) |
| 位 51:12 | 40 位 | 物理页框号(PFN),对应 52 位物理地址的高 40 位 |
| 位 11:9 | 3 位 | 忽略 / 可用(操作系统可自定义,如用于 NUMA 信息) |
| 位 8 | 1 位 | G(Global):全局页标志,避免 TLB 刷新时被清除 |
| 位 7 | 1 位 | PAT(Page Attribute Table):内存类型扩展 |
| 位 6 | 1 位 | D(Dirty):脏页标志,由硬件在写入时置位 |
| 位 5 | 1 位 | A(Accessed):访问标志,由硬件在读取或写入时置位 |
| 位 4 | 1 位 | PCD(Page Cache Disable):禁用缓存 |
| 位 3 | 1 位 | PWT(Page Write Through):写直达策略 |
| 位 2 | 1 位 | U/S(User/Supervisor):用户态可访问标志 |
| 位 1 | 1 位 | R/W(Read/Write):读写权限,0 表示只读 |
| 位 0 | 1 位 | P(Present):存在位,0 表示未映射 |
注意: 对于非最后一级页表(PML4、PDPT、PD),其条目格式与 PT 条目类似,但位 51:12 指向的是下一级页表的物理基地址,而非最终物理页框。此外,PD 条目中位 7 被用作 PS(Page Size)标志,置 1 时表示该条目映射一个 2 MB 大页,此时不再需要 PT 级。
3.5 Linux 内核中的页表操作宏
Linux 内核通过一组宏来抽象页表遍历,这些宏定义在 arch/x86/include/asm/pgtable.h 中:
// 获取 PML4 条目(pgd)
pgd_t *pgd = pgd_offset(mm, addr);
// 获取 PDPT 条目(pud)
pud_t *pud = pud_offset(pgd, addr);
// 获取 PD 条目(pmd)
pmd_t *pmd = pmd_offset(pud, addr);
// 获取 PT 条目(pte)
pte_t *pte = pte_offset_kernel(pmd, addr);
// 读取物理页框号
unsigned long pfn = pte_pfn(*pte);
性能提示: 每次地址转换都需要 4 次内存访问(每级页表一次),这对性能影响很大。TLB(Translation Lookaside Buffer)正是为了缓存这些转换结果而设计的,下一章将详细讨论 TLB 的结构与刷新策略。
3.6 大页映射:跳过中间层级
x86_64 支持两种大页模式,通过设置非最后一级页表条目中的 PS 位来跳过后续层级:
- 2 MB 大页(HugeTLB):在 PD 条目中设置 PS 位(位 7),此时 PD 条目直接指向 2 MB 物理页框,跳过 PT 级。虚拟地址中 PT 索引字段(位 20:12)变为页内偏移的一部分。
- 1 GB 大页:在 PDPT 条目中设置 PS 位,此时 PDPT 条目直接指向 1 GB 物理页框,跳过 PD 和 PT 两级。虚拟地址中 PD 和 PT 索引字段均变为页内偏移。
大页映射的优势在于:
- 减少页表级数,降低 TLB 未命中率(一个 TLB 条目可覆盖更大内存区域)。
- 减少页表本身的内存占用(无需分配中间级页表)。
- 适用于数据库共享内存、虚拟机内存等大块连续内存场景。
内核接口: Linux 通过
mmap() 的 MAP_HUGETLB 标志或 hugetlbfs 文件系统来使用大页。透明大页(THP)则允许内核自动将匿名内存映射为 2 MB 大页,无需用户显式指定。