引言
在现代操作系统中,虚拟内存是每一个进程独立地址空间的基石。而实现虚拟地址到物理地址转换的核心机制,就是页表(Page Table)。Linux 内核的页表实现极其精巧,它不仅需要处理多级索引、缓存优化(TLB)、大页映射(HugePage),还需要应对现代硬件安全漏洞带来的挑战(如 KPTI)。
本文将从 x86_64 架构的四级页表结构出发,深入剖析 Linux 内核中地址转换的完整路径、TLB 管理机制、HugePage 优化实践,以及近年来重要的安全隔离机制。
1. 虚拟内存与页表基础
1.1 为什么需要虚拟内存
虚拟内存为每个进程提供独立的、连续的地址空间,带来三大核心优势:
- 进程隔离:每个进程拥有独立地址空间,互不干扰
- 内存超分配:通过 swap 机制可使用超过物理内存的空间
- 内存保护:通过页表权限位实现读/写/执行控制
1.2 基本概念
- 虚拟地址(VA):进程看到的地址,范围 0 ~ 2^47-1(x86_64 用户空间)
- 物理地址(PA):实际 DRAM 上的地址
- 页(Page):内存管理的基本单位,通常 4KB
- 页帧(Page Frame):物理内存中被页大小整除的块
- 页表项(PTE):存储虚拟页到物理页帧的映射关系
2. x86_64 四级页表结构
2.1 地址划分
x86_64 架构使用 48 位虚拟地址(256TB 地址空间),4KB 页大小下地址被划分为 5 级索引:
| 级别 | 位数 | 名称 | 说明 |
|---|---|---|---|
| PML4 | 9 位 | Page Map Level 4 | 顶级页表索引 |
| PDPT | 9 位 | Page Directory Pointer Table | 页目录指针表 |
| PD | 9 位 | Page Directory | 页目录 |
| PT | 9 位 | Page Table | 页表 |
| Offset | 12 位 | 页内偏移 | 直接索引页内字节 |
每一级页表占据一个 4KB 页,包含 512 个 8 字节条目(2^9 = 512)。
2.2 地址转换流程
CPU 的 MMU(内存管理单元)按照以下步骤完成地址翻译:
- 从 CR3 寄存器获取 PML4 表基址
- 用虚址的 [47:39] 位索引 PML4 表,得到 PDPT 基址
- 用 [38:30] 位索引 PDPT,得到 PD 基址
- 用 [29:21] 位索引 PD,得到 PT 基址
- 用 [20:12] 位索引 PT,得到最终物理页帧号
- 结合 [11:0] 页内偏移,输出物理地址
整个过程涉及 4 次内存访问,如果每次都从主存读取,一次地址转换的延迟将非常高。这就是 TLB 存在的原因。
2.3 Linux 中的五级页表扩展(LA57)
Intel 第五代至强可扩展处理器(Sapphire Rapids)及更新的 CPU 支持 57 位虚拟地址(5-Level Paging),增加了一级 PML5 表,将地址空间扩展到 128PB。Linux 内核通过 CONFIG_X86_5LEVEL 配置项提供支持,但要求硬件支持 la57 CPUID 标志。
3. 页表项(PTE)详解
3.1 标志位
x86_64 的 64 位 PTE 中,低 12 位为标志位(对应页对齐要求的低位空闲),关键标志包括:
| 标志 | 位 | 含义 |
|---|---|---|
| P | 0 | Present:页面是否在物理内存中 |
| R/W | 1 | Read/Write:0=只读,1=可读写 |
| U/S | 2 | User/Supervisor:特权级控制 |
| PWT/PCD | 3/4 | 缓存策略控制位 |
| A | 5 | Accessed:页面是否被访问过 |
| D | 6 | Dirty:页面是否被写过 |
| PS | 7 | Page Size:为1表示大页映射 |
| G | 8 | Global:全局页,进程切换不刷新 |
| NX | 63 | No-Execute:禁止执行(XD 位) |
3.2 Present=0 时的行为
当 PTE 的 Present 位为 0 时,CPU 触发 Page Fault 异常,Linux 内核的页错误处理程序 do_page_fault() 接管,根据情况执行:
- Demand Paging:文件映射页面首次访问,从磁盘读入
- Copy-on-Write:写时复制,fork 后写共享页面
- Swap In:页面被换出到 swap 分区,需要读回
- SIGSEGV:非法访问,发送段错误信号
4. TLB:地址转换的缓存
4.1 TLB 工作原理
Translation Lookaside Buffer (TLB) 是 MMU 内部的超高速缓存,存储最近使用的虚拟页号到物理页帧号的映射。现代 CPU 通常有独立的指令 TLB(iTLB)和数据 TLB(dTLB)。
典型 TLB 层级(以 Intel Skylake 为例):
| 层级 | 条目数 | 关联度 | 覆盖范围 |
|---|---|---|---|
| L1 dTLB (4K) | 64 路 | 4-way | 256KB |
| L1 iTLB (4K) | 128 路 | 8-way | 512KB |
| L1 TLB (2M/4M) | 32 路 | 4-way | 64MB-128MB |
| L2 TLB (统一) | 1536 路 | 12-way | 6MB |
4.2 TLB Miss 与 Page Walk
当所需映射不在 TLB 中时,发生 TLB Miss,CPU 的 Page Table Walker 硬件自动遍历页表,将结果填入 TLB。这个过程:
- 对 4 级页表最多需要 5 次内存访问(4 级页表 1 次实际数据访问)
- 延迟约 100-300 个 CPU 周期(取决于是否命中各级缓存)
- 优于触发软件 Page Fault,但仍显著慢于 TLB 命中
4.3 TLB Shootdown
在多核系统中,当一个 CPU 修改了页表(如 munmap、mprotect),需要通知其他 CPU 刷新对应 TLB 条目,这个过程称为 TLB Shootdown:
- 发起 CPU 修改页表项
- 通过 IPI(处理器间中断)通知其他 CPU
- 其他 CPU 执行
invlpg指令刷新指定条目 - 所有 CPU 完成后,发起方继续执行
TLB Shootdown 是多核性能的重要瓶颈,特别是在大量使用 mmap/munmap 的场景中。
5. HugePage:解决 TLB Miss 的利器
5.1 问题背景
以 2GB 工作集为例,使用 4KB 页需要 524,288 个 PTE,而 CPU 的 TLB 只能缓存数百到数千页。密集内存访问时 TLB Miss 率极高。HugePage 通过增大单页尺寸减小 TLB 压力。
5.2 x86_64 支持的大页
| 类型 | 尺寸 | 页表级数 | TLB 覆盖率 |
|---|---|---|---|
| 标准小页 | 4KB | 4 级 | 4KB/条目 |
| 大页(HugePage) | 2MB | 3 级 | 2MB/条目 |
| 超大页(GiantPage) | 1GB | 2 级 | 1GB/条目 |
5.3 静态大页(hugetlbfs)
Linux 通过 hugetlbfs 伪文件系统提供静态大页分配:
# 查看可用大页
cat /proc/meminfo | grep HugePages
# 预留 512 个 2MB 大页(即 1GB)
echo 512

发表评论 取消回复