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 访问虚拟地址到内核接管处理:

  1. CPU 发出虚拟地址:程序执行 LOAD 或 STORE 指令,CPU 将虚拟地址发送给 MMU。
  2. TLB 查找:MMU 首先在 TLB(转译后备缓冲器)中查找虚拟地址对应的物理地址和页表项。如果 TLB 命中且权限允许,直接访问物理内存,流程结束。
  3. 页表遍历:TLB 未命中,MMU 遍历进程的页表(多级页表结构),查找对应的页表项(PTE)。
  4. 检查 PTE 的 Present 位:MMU 检查 PTE 的 Present 位(存在位)。
    • 如果 Present 位为 1,且权限检查通过,MMU 更新 TLB 并访问物理内存。
    • 如果 Present 位为 0,MMU 触发缺页异常,将控制权交给内核的缺页异常处理程序。
  5. 保存上下文:CPU 自动保存当前进程的上下文(如指令指针、栈指针),并跳转到内核预定义的缺页异常处理入口(如 x86 的 page_fault 中断处理函数)。
关键点: 缺页异常是同步异常,由当前指令直接触发。这意味着触发缺页的进程会被阻塞,直到缺页处理完成。这与异步中断(如 I/O 完成中断)不同。
缺页异常触发流程 CPU 发出虚拟地址 TLB 查找 页表遍历 检查 Present 位 触发缺页异常 内核处理程序接管 TLB 命中且权限允许 → 直接访问物理内存 (流程结束)

2.3 缺页异常处理路径

内核的缺页异常处理程序(通常为 do_page_fault() 或 handle_mm_fault())根据缺页原因和地址空间信息,选择不同的处理路径。以下是主要处理路径的详细分析:

2.3.1 路径一:次缺页处理(Minor Fault Handler)

当缺页的虚拟地址在进程的 VMA 中,且对应的物理页面已经在内存中(如页面缓存、共享内存、匿名页的写时复制),内核只需建立页表映射即可。处理步骤:

  1. 查找 VMA:通过 find_vma() 确认虚拟地址是否属于某个 VMA。
  2. 检查权限:验证访问类型(读/写/执行)是否与 VMA 权限一致。
  3. 获取现有页面:从页面缓存(page_cache)或 swap 缓存中获取已存在的物理页面。
  4. 建立页表项:调用 set_pte() 或 mk_pte() 创建新的页表项,并更新 TLB。
  5. 返回用户态:恢复进程上下文,重新执行触发缺页的指令。
优化点: 次缺页的代价主要在于页表操作和 TLB 刷新。使用大页(Huge Pages)可以减少页表层级和 TLB 压力,从而降低次缺页的开销。

2.3.2 路径二:主缺页处理(Major Fault Handler)

当缺页的物理页面不在内存中,需要从磁盘加载时,内核进入主缺页处理路径。这是最耗时的缺页类型。处理步骤:

  1. 确认缺页原因:通过 VMA 的 vm_file 判断是文件映射缺页还是匿名页缺页(swap 换入)。
  2. 分配物理页面:调用 alloc_page() 从伙伴系统分配一个空闲物理页面。
  3. 发起磁盘 I/O:
    • 对于文件映射:调用 mapping->a_ops->readpage() 从文件系统读取数据。
    • 对于 swap 换入:调用 swap_readpage() 从 swap 分区读取数据。
  4. 进程睡眠:当前进程被标记为 TASK_UNINTERRUPTIBLE,加入等待队列,直到 I/O 完成。
  5. I/O 完成唤醒:磁盘 I/O 完成后,中断处理程序唤醒等待的进程。
  6. 建立页表项:将新加载的物理页面映射到进程的页表中。
  7. 返回用户态:恢复进程上下文,重新执行指令。
性能陷阱: 主缺页会导致进程长时间阻塞(通常 5-10ms)。如果系统频繁出现主缺页,说明内存严重不足或文件访问模式碎片化。使用 vmstat 1 观察 si(swap in)和 so(swap out)列,如果持续非零,应立即排查内存泄漏或增加物理内存。

2.3.3 路径三:无效缺页处理(Invalid Fault Handler)

当虚拟地址不在任何 VMA 中,或访问权限违反 VMA 权限时,内核无法修复缺页,只能终止进程。处理步骤:

  1. 查找 VMA 失败:find_vma() 返回 NULL,或找到的 VMA 不包含该地址。
  2. 检查扩展栈:如果缺页地址接近栈 VMA 的边界,内核尝试扩展栈(expand_stack())。如果扩展失败,进入下一步。
  3. 发送 SIGSEGV:内核向当前进程发送 SIGSEGV 信号。
  4. 进程终止:如果进程没有注册 SIGSEGV 处理函数,默认行为是终止进程并生成 core dump。
缺页异常处理路径决策树 缺页异常入口 VMA 查找 VMA 命中,页面在内存 VMA 命中,页面不在内存 VMA 未命中 次缺页处理 主缺页处理 无效缺页处理 建立映射,返回用户态 分配页面,发起 I/O 发送 SIGSEGV 处理路径总结 次缺页:仅更新页表,无 I/O 开销 主缺页:涉及磁盘 I/O,进程阻塞 无效缺页:进程终止(SIGSEGV)

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