4. 4级页表结构总览
在 x86_64 长模式下,Linux 内核使用 4 级分页 机制将 48 位线性地址转换为 52 位物理地址。这 4 级页表分别为:PML4(Page Map Level 4)、PDPT(Page Directory Pointer Table)、PD(Page Directory)和 PT(Page Table)。每一级负责从线性地址中提取对应的索引,逐级查找,最终定位到物理页框。
核心要点: 4 级页表本质上是一个 多级索引数组。线性地址被切分为 5 个部分:4 个索引(每级 9 位)和 1 个页内偏移(12 位)。CPU 通过 CR3 寄存器找到 PML4 表基址,然后逐级索引,最终获得物理地址。
4.1 线性地址的划分
一个 48 位线性地址被划分为 5 个字段,从高位到低位依次为:
| 字段 | 位范围 | 位数 | 作用 |
|---|---|---|---|
| PML4 索引 | 47:39 | 9 | 在 PML4 表中索引一个条目(PML4E) |
| PDPT 索引 | 38:30 | 9 | 在 PDPT 表中索引一个条目(PDPTE) |
| PD 索引 | 29:21 | 9 | 在 PD 表中索引一个条目(PDE) |
| PT 索引 | 20:12 | 9 | 在 PT 表中索引一个条目(PTE) |
| 页内偏移 | 11:0 | 12 | 在物理页框内的字节偏移 |
每个索引占用 9 位,因此每级页表最多包含 512 个条目(2^9)。每个条目占用 8 字节(64 位),因此每级页表的大小为 4 KB(512 × 8),恰好与一个物理页框大小相同。
4.2 4级页表层级关系与索引流程
下图展示了从线性地址到物理地址的完整转换路径:
4.3 索引方式详解
地址转换的每一步都遵循相同的模式:
- 获取表基址: 从 CR3 寄存器(PML4 基址)或上一级页表条目中读取下一级页表的物理基址。
- 提取索引: 从线性地址中提取对应级别的 9 位索引。
- 计算条目地址: 条目地址 = 表基址 + 索引 × 8。
- 读取条目: 从内存中读取 8 字节的页表条目。
- 检查权限: 验证 Present、R/W、U/S 等标志位。
- 获取下一级基址: 从条目中提取物理地址的高 40 位(页框号),作为下一级页表的基址。
关键公式: 对于线性地址
LA,其 PML4 索引为 (LA >> 39) & 0x1FF,PDPT 索引为 (LA >> 30) & 0x1FF,PD 索引为 (LA >> 21) & 0x1FF,PT 索引为 (LA >> 12) & 0x1FF,页内偏移为 LA & 0xFFF。
4.4 页表条目结构
每个页表条目(PML4E、PDPTE、PDE、PTE)都是 64 位,其通用结构如下:
| 位范围 | 字段 | 说明 |
|---|---|---|
| 63:52 | 保留 / NX | 第 63 位为 NX(不可执行)位(如果启用 NX 支持) |
| 51:12 | 物理页框号 | 下一级页表或物理页的基址(高 40 位) |
| 11:9 | AVL | 软件可用位(操作系统可自定义) |
| 8 | G | 全局页(Global) |
| 7 | PS | 页大小(Page Size),仅在 PDE 和 PDPTE 中有效 |
| 6 | D | 脏位(Dirty),仅在 PDE 和 PTE 中有效 |
| 5 | A | 访问位(Accessed) |
| 4 | PCD | 页级缓存禁用(Page-level Cache Disable) |
| 3 | PWT | 页级写透(Page-level Write-Through) |
| 2 | U/S | 用户/超级用户(User/Supervisor) |
| 1 | R/W | 读/写(Read/Write) |
| 0 | P | 存在位(Present) |
注意: 在 PML4 和 PDPT 级别,PS 位(位 7)用于指示是否使用 1 GB 大页(PDPTE)或 2 MB 大页(PDE)。如果 PS 位被置位,则地址转换在该级终止,不再继续查找下一级页表。
4.5 4级页表的内存占用
每个进程拥有独立的页表层次结构。下表展示了 4 级页表在不同情况下的内存占用:
| 页表级别 | 条目数 | 每个条目大小 | 表大小 | 覆盖的虚拟地址范围 |
|---|---|---|---|---|
| PML4 | 512 | 8 字节 | 4 KB | 整个 48 位地址空间(256 TB) |
| PDPT | 512 | 8 字节 | 4 KB | 512 GB(每个 PML4E) |
| PD | 512 | 8 字节 | 4 KB | 1 GB(每个 PDPTE) |
| PT | 512 | 8 字节 | 4 KB | 2 MB(每个 PDE) |
对于典型的 4 KB 页,一个完整的 4 级页表层次结构理论上需要 4 个页框(PML4、PDPT、PD、PT 各一个),共 16 KB。然而,由于大多数进程只使用地址空间的一小部分,内核会按需分配页表,实际内存占用远小于理论最大值。
4.6 实战:手动计算地址转换
假设线性地址为 0x7F3A_5B1C_8D20,我们手动计算其各级索引:
线性地址: 0x7F3A5B1C8D20
二进制: 0111 1111 0011 1010 0101 1011 0001 1100 1000 1101 0010 0000
PML4索引: 位 [47:39] = 0xFE (254)
PDPT索引: 位 [38:30] = 0x74 (116)
PD索引: 位 [29:21] = 0x2D (45)
PT索引: 位 [20:12] = 0x1C (28)
页内偏移: 位 [11:0] = 0xD20 (3360)
转换过程如下:
- 从 CR3 获取 PML4 表基址。
- 在 PML4 表中索引第 254 个条目,读取 PML4E,获取 PDPT 表基址。
- 在 PDPT 表中索引第 116 个条目,读取 PDPTE,获取 PD 表基址。
- 在 PD 表中索引第 45 个条目,读取 PDE,获取 PT 表基址。
- 在 PT 表中索引第 28 个条目,读取 PTE,获取物理页框基址。
- 物理地址 = 物理页框基址 + 0xD20。
实战建议: 在 Linux 内核中,可以使用
pgd_offset()、pud_offset()、pmd_offset() 和 pte_offset() 宏来遍历当前进程的页表。这些宏封装了索引计算和条目读取的逻辑,是理解 4 级页表的最佳入口。