2. 缺页异常(Page Fault)原理:缺页类型、触发流程与处理路径
缺页异常(Page Fault)是虚拟内存管理的核心触发机制。当 CPU 访问一个虚拟地址,而该地址对应的物理页面不在主存中,或访问权限不匹配时,MMU(内存管理单元)会触发缺页异常,由内核的缺页异常处理程序接管。理解缺页异常的类型、触发流程与处理路径,是进行内存性能调优的基础。
2.1 缺页异常的类型
根据缺页的原因和处理代价,缺页异常主要分为三类:主缺页(Major Page Fault)、次缺页(Minor Page Fault) 和 无效缺页(Invalid Page Fault)。它们的区别在于是否涉及磁盘 I/O 以及是否导致进程终止。
| 缺页类型 | 触发原因 | 是否涉及磁盘 I/O | 性能影响 | 典型场景 |
|---|---|---|---|---|
| 主缺页(Major) | 页面内容不在物理内存中,且不在任何进程的 VMA 中,需要从磁盘(swap 分区或文件映射)加载。 | 是 | 极高(毫秒级延迟) | 程序首次读取大文件、内存不足导致 swap 换入 |
| 次缺页(Minor) | 页面已在物理内存中(如共享库、页面缓存),但页表项无效或未建立映射。 | 否 | 低(微秒级延迟) | 共享库加载、写时复制(COW)、匿名页首次分配 |
| 无效缺页(Invalid) | 访问的虚拟地址未映射到任何 VMA(段错误),或访问权限违反 VMA 权限(如写只读页)。 | 否 | 进程终止(SIGSEGV) | 空指针解引用、栈溢出、访问已释放内存 |
性能调优提示: 在
perf stat 或 /proc/<pid>/status 中观察 majflt 和 minflt 的比值。如果 majflt 占比过高,说明内存压力大或文件访问模式不佳,应考虑增加内存、优化缓存策略或调整 swap 配置。
2.2 缺页异常触发流程
缺页异常的触发是一个硬件与软件协同的过程。以下是典型的触发流程,从 CPU 访问虚拟地址到内核接管处理:
- CPU 发出虚拟地址:程序执行
LOAD或STORE指令,CPU 将虚拟地址发送给 MMU。 - TLB 查找:MMU 首先在 TLB(转译后备缓冲器)中查找虚拟地址对应的物理地址和页表项。如果 TLB 命中且权限允许,直接访问物理内存,流程结束。
- 页表遍历:TLB 未命中,MMU 遍历进程的页表(多级页表结构),查找对应的页表项(PTE)。
- 检查 PTE 的 Present 位:MMU 检查 PTE 的 Present 位(存在位)。
- 如果 Present 位为 1,且权限检查通过,MMU 更新 TLB 并访问物理内存。
- 如果 Present 位为 0,MMU 触发缺页异常,将控制权交给内核的缺页异常处理程序。
- 保存上下文:CPU 自动保存当前进程的上下文(如指令指针、栈指针),并跳转到内核预定义的缺页异常处理入口(如 x86 的
page_fault中断处理函数)。
关键点: 缺页异常是同步异常,由当前指令直接触发。这意味着触发缺页的进程会被阻塞,直到缺页处理完成。这与异步中断(如 I/O 完成中断)不同。
2.3 缺页异常处理路径
内核的缺页异常处理程序(通常为 do_page_fault() 或 handle_mm_fault())根据缺页原因和地址空间信息,选择不同的处理路径。以下是主要处理路径的详细分析:
2.3.1 路径一:次缺页处理(Minor Fault Handler)
当缺页的虚拟地址在进程的 VMA 中,且对应的物理页面已经在内存中(如页面缓存、共享内存、匿名页的写时复制),内核只需建立页表映射即可。处理步骤:
- 查找 VMA:通过
find_vma()确认虚拟地址是否属于某个 VMA。 - 检查权限:验证访问类型(读/写/执行)是否与 VMA 权限一致。
- 获取现有页面:从页面缓存(
page_cache)或 swap 缓存中获取已存在的物理页面。 - 建立页表项:调用
set_pte()或mk_pte()创建新的页表项,并更新 TLB。 - 返回用户态:恢复进程上下文,重新执行触发缺页的指令。
优化点: 次缺页的代价主要在于页表操作和 TLB 刷新。使用大页(Huge Pages)可以减少页表层级和 TLB 压力,从而降低次缺页的开销。
2.3.2 路径二:主缺页处理(Major Fault Handler)
当缺页的物理页面不在内存中,需要从磁盘加载时,内核进入主缺页处理路径。这是最耗时的缺页类型。处理步骤:
- 确认缺页原因:通过 VMA 的
vm_file判断是文件映射缺页还是匿名页缺页(swap 换入)。 - 分配物理页面:调用
alloc_page()从伙伴系统分配一个空闲物理页面。 - 发起磁盘 I/O:
- 对于文件映射:调用
mapping->a_ops->readpage()从文件系统读取数据。 - 对于 swap 换入:调用
swap_readpage()从 swap 分区读取数据。
- 对于文件映射:调用
- 进程睡眠:当前进程被标记为
TASK_UNINTERRUPTIBLE,加入等待队列,直到 I/O 完成。 - I/O 完成唤醒:磁盘 I/O 完成后,中断处理程序唤醒等待的进程。
- 建立页表项:将新加载的物理页面映射到进程的页表中。
- 返回用户态:恢复进程上下文,重新执行指令。
性能陷阱: 主缺页会导致进程长时间阻塞(通常 5-10ms)。如果系统频繁出现主缺页,说明内存严重不足或文件访问模式碎片化。使用
vmstat 1 观察 si(swap in)和 so(swap out)列,如果持续非零,应立即排查内存泄漏或增加物理内存。
2.3.3 路径三:无效缺页处理(Invalid Fault Handler)
当虚拟地址不在任何 VMA 中,或访问权限违反 VMA 权限时,内核无法修复缺页,只能终止进程。处理步骤:
- 查找 VMA 失败:
find_vma()返回 NULL,或找到的 VMA 不包含该地址。 - 检查扩展栈:如果缺页地址接近栈 VMA 的边界,内核尝试扩展栈(
expand_stack())。如果扩展失败,进入下一步。 - 发送 SIGSEGV:内核向当前进程发送
SIGSEGV信号。 - 进程终止:如果进程没有注册
SIGSEGV处理函数,默认行为是终止进程并生成 core dump。
2.4 缺页异常与性能调优的关联
缺页异常是衡量内存子系统健康度的关键指标。以下是几个与性能调优直接相关的观察点:
- 次缺页频率:次缺页本身开销较小,但如果频率极高(如每秒数万次),说明程序的内存访问模式存在大量冷启动或写时复制。可以考虑使用预分配(
MAP_POPULATE)或大页来减少次缺页次数。 - 主缺页频率:主缺页是性能杀手。如果
majflt持续增长,应检查:- 物理内存是否充足(
free -h)。 - swap 使用情况(
swapon --show)。 - 文件映射的缓存命中率(
cachestat或perf stat -e page-faults,major-faults)。
- 物理内存是否充足(
- 无效缺页:无效缺页通常意味着程序有 bug(如空指针、野指针)。在调优前,应先用
valgrind或 AddressSanitizer 修复这些错误。
调优工具命令示例:
# 查看进程缺页统计
cat /proc/<pid>/status | grep -E "VmPeak|VmSize|VmLib|VmPTE|VmSwap|majflt|minflt"
# 使用 perf 统计缺页事件
perf stat -e page-faults,major-faults,minor-faults ./your_program
# 使用 vmstat 观察系统级 swap 活动
vmstat 1 10