1. 页表基础概念
页表是操作系统内存管理子系统的核心数据结构,负责将进程看到的连续虚拟地址空间映射到物理内存中离散的物理页帧上。没有页表,多进程隔离、按需分页、共享内存、写时复制等现代 OS 特性将无法实现。
核心认知:页表是虚拟内存系统的“翻译引擎”——每次 CPU 发出的虚拟地址都必须经过页表查询才能得到真实的物理地址。页表配置错误或 TLB 缺失频繁,会直接导致性能悬崖。
1.1 页表的作用
页表承担以下三项关键职责:
- 地址翻译:将虚拟页号(VPN)转换为物理页帧号(PFN),加上页内偏移得到完整物理地址。
- 权限控制:每个页表项(PTE)记录读/写/执行权限,阻止用户态访问内核空间。
- 状态管理:标记页面是否在内存中(有效位)、是否被修改(脏位)、是否被访问(访问位),支撑页面置换和写回策略。
| 作用 | 硬件支撑 | 操作系统行为 |
|---|---|---|
| 地址翻译 | MMU + TLB | 建立/销毁页表映射 |
| 权限控制 | PTE 中的 U/S、R/W、NX 位 | 设置页表项权限位 |
| 状态管理 | PTE 中的 Present、Dirty、Accessed 位 | 缺页处理、页面换出、写回 |
1.2 虚拟地址到物理地址的映射原理
以典型的 4KB 页面、两级页表(x86 32-bit 经典模型)为例,地址翻译过程分为三步:
- 拆分地址:32 位虚拟地址被拆分为
页目录索引(10位) | 页表索引(10位) | 页内偏移(12位)。 - 查页目录:MMU 从 CR3 寄存器获取页目录基址,加上页目录索引 × 4 得到页目录项(PDE),从中读出页表基址。
- 查页表:用页表索引 × 4 在页表中找到页表项(PTE),PTE 中包含物理页帧号(PFN)。
- 合成物理地址:
物理地址 = PFN × 页面大小 + 页内偏移。
性能要点:每次地址翻译需要两次内存访问(页目录 + 页表),TLB 正是为了消除这两次访问而设计的硬件缓存。多级页表虽然节省了页表内存,但增加了翻译路径长度。
1.3 页表项(PTE)结构
页表项是页表的基本单元,每个 PTE 对应一个虚拟页到物理页的映射。以 x86-64 架构的 4KB 页面为例,每个 PTE 占 8 字节(64 位),其关键字段如下:
| 位域 | 宽度 | 名称 | 说明 |
|---|---|---|---|
| 0 | 1 bit | Present (P) | 页面是否在物理内存中;0 表示缺页 |
| 1 | 1 bit | Read/Write (R/W) | 0=只读,1=可读写 |
| 2 | 1 bit | User/Supervisor (U/S) | 0=仅内核可访问,1=用户态也可访问 |
| 3 | 1 bit | Page-Level Write-Through (PWT) | 控制 Cache 写策略 |
| 4 | 1 bit | Page-Level Cache Disable (PCD) | 禁止该页被缓存 |
| 5 | 1 bit | Accessed (A) | 硬件置位,表示该页被访问过 |
| 6 | 1 bit | Dirty (D) | 硬件置位,表示该页被写入过 |
| 7 | 1 bit | Page Attribute Table (PAT) | 与 PWT/PCD 配合控制内存类型 |
| 8 | 1 bit | Global (G) | 全局页,TLB 刷新时不被清除 |
| 9–11 | 3 bits | Ignored / Available | 供 OS 自定义使用(如记录页面状态) |
| 12–51 | 40 bits | Physical Page Number (PFN) | 物理页帧号,指向 4KB 对齐的物理页 |
| 52–62 | 11 bits | Reserved / Ignored | 硬件保留或供未来扩展 |
| 63 | 1 bit | No-Execute (NX) | 禁止执行该页上的代码(DEP 保护) |
调试陷阱:PTE 中的 Present 位为 0 时,MMU 会触发缺页异常(#PF)。但 OS 可能利用 Present=0 的 PTE 存储交换分区信息(如磁盘块号),此时 PTE 的剩余位被 OS 重新解释。调试时务必区分“真正的缺页”和“页面已换出”两种情况。
1.4 页表配置对性能的影响
页表配置直接影响 TLB 缺失率和地址翻译开销,以下是三个关键维度:
- 页面大小:大页面(2MB/1GB)能覆盖更大虚拟地址范围,减少 TLB 项数需求,降低缺失率。但大页面可能导致内部碎片。
- 页表层级:层级越多,TLB 缺失后遍历页表的代价越高(每次遍历需访问内存)。x86-64 的 4 级页表在 TLB 缺失时最多需要 4 次内存访问。
- TLB 刷新策略:进程切换时刷新 TLB(如 x86 的 INVLPG、MOV CR3)会清空缓存,导致后续大量 TLB 缺失。使用 PCID(Process-Context IDentifiers)可避免全局刷新。
| 页面大小 | TLB 项数(典型) | TLB 覆盖范围 | 适用场景 |
|---|---|---|---|
| 4 KB | 64 | 256 KB | 通用桌面/服务器 |
| 2 MB | 32 | 64 MB | 数据库、HPC、大内存应用 |
| 1 GB | 4 | 4 GB | 虚拟化、大型内存数据库 |
性能优化提示:在性能热点代码中,如果发现大量 TLB 缺失(通过 perf stat -e dTLB-load-misses 观察),优先考虑启用透明大页(THP)或手动使用 mmap 的 MAP_HUGETLB 标志。对于数据库和搜索引擎等大内存工作负载,2MB 大页通常能降低 30%–50% 的 TLB 缺失率。