3. 缺页异常处理流程:do_page_fault() 函数调用链与关键数据结构

缺页异常(Page Fault)是虚拟内存管理的核心入口。当 CPU 访问一个虚拟地址,而该地址对应的物理页不在内存中,或权限不匹配时,MMU 会触发缺页异常,操作系统内核通过 do_page_fault() 函数接管处理。本章深入剖析该函数的调用链、关键分支以及核心数据结构。

3.1 缺页异常触发与入口

缺页异常属于 x86 架构的异常向量 14(Page Fault)。CPU 在发生缺页时,会将以下信息压入内核栈:

  • 错误码(Error Code):位于栈顶,指示异常原因(位 0:保护违例;位 1:写操作;位 2:用户态;位 3:保留位;位 4:指令获取)。
  • 触发地址(CR2):CPU 自动将导致缺页的线性地址存入 CR2 寄存器。
  • 返回地址(RIP):发生异常的指令地址。

内核入口汇编代码(如 entry_64.S 中的 page_fault)保存寄存器后,调用 C 函数 do_page_fault()。

性能要点: 缺页异常是开销极大的操作(微秒级),频繁的缺页会严重拖慢系统。性能调优的首要目标就是减少缺页次数,尤其是主缺页(Major Fault)。

3.2 do_page_fault() 核心调用链

do_page_fault() 是缺页处理的总入口,其内部逻辑分为三大阶段:地址空间检查、查找 VMA、分派处理函数。以下为简化调用链:

do_page_fault(regs, error_code, address)
  ├── 1. 地址空间检查
  │     ├── find_vma(mm, address)          // 查找包含 address 的 VMA
  │     └── 若未找到 → 发送 SIGSEGV
  ├── 2. 访问权限检查
  │     ├── 检查 VMA 的 vm_flags 与 error_code
  │     └── 权限不匹配 → 发送 SIGSEGV
  └── 3. 分派处理 (handle_mm_fault)
        ├── handle_pte_fault(vmf)
        │     ├── 匿名页缺页 (do_anonymous_page)
        │     ├── 文件映射缺页 (do_fault)
        │     │     ├── do_read_fault
        │     │     ├── do_cow_fault
        │     │     └── do_shared_fault
        │     ├── 写时复制 (do_wp_page)
        │     └── 交换缺页 (do_swap_page)
        └── 处理完成后更新 TLB
关键路径: 对于匿名页(堆、栈、BSS),最常见的是 do_anonymous_page() 和 do_wp_page()。对于文件映射(mmap 文件),则走 do_fault() 分支。

3.3 关键数据结构全景

缺页处理过程中涉及多个核心数据结构,它们共同描述了进程的虚拟地址空间和物理页状态。

数据结构 所在文件 核心字段 在缺页中的角色
struct mm_struct mm_types.h pgd, mmap, mm_rb, map_count 进程内存描述符,管理所有 VMA 和页表
struct vm_area_struct mm_types.h vm_start, vm_end, vm_flags, vm_ops 描述一个连续的虚拟地址区间及其属性
struct vm_fault memory.h vma, address, flags, pte 缺页上下文,传递异常信息给下层处理函数
pte_t pgtable_types.h 页表项(物理页帧号 + 权限位) 页表项,决定虚拟页到物理页的映射
struct page mm_types.h flags, _refcount, mapping 物理页描述符,管理页的引用、脏页、回写等

3.4 缺页处理流程 SVG 架构图

下图展示了从 CPU 触发缺页到内核完成处理的完整数据流与函数调用关系:

缺页异常处理流程架构图 缺页异常处理流程:do_page_fault() 调用链 CPU 触发缺页异常 (Vector 14) entry_64.S → page_fault 处理 do_page_fault(regs, error_code, address) find_vma(mm, address) 查找 VMA 权限检查 (vm_flags vs error_code) handle_mm_fault → handle_pte_fault(vmf) do_anonymous_page 匿名页缺页 do_fault 文件映射缺页 do_wp_page 写时复制 do_swap_page 交换缺页 do_numa_page NUMA 迁移 最终:分配物理页 / 从磁盘换入 / 复制页 → 更新页表 → 返回用户态重新执行指令

3.5 缺页类型与性能特征

不同类型的缺页异常对性能的影响差异巨大。下表总结了五种主要缺页类型的触发条件与开销等级:

缺页类型 触发条件 处理函数 开销等级 典型场景
匿名页缺页 访问未映射的匿名页(堆、栈、BSS) do_anonymous_page() 低(微秒级) 进程启动、堆扩展
文件映射缺页 访问 mmap 文件但页不在内存 do_fault() 中-高(取决于磁盘 I/O) 文件读取、动态库加载
写时复制缺页 写操作命中只读共享页(fork 后) do_wp_page() 中(需复制页) fork 后写操作
交换缺页 访问被换出到交换分区的页 do_swap_page() 高(磁盘 I/O + 可能唤醒 kswapd) 内存压力大时
NUMA 缺页 访问远端 NUMA 节点页 do_numa_page() 低(仅更新统计) NUMA 自动平衡
调优警示: 交换缺页(Major Fault)是性能杀手。如果系统 sar -B 显示 pgmajfault 持续较高,说明物理内存不足,应优先考虑增加内存或调整 vm.swappiness。

3.6 关键数据结构关系图

以下 SVG 展示了缺页处理过程中核心数据结构之间的引用关系:

缺页关键数据结构关系图 缺页处理关键数据结构关系 mm_struct pgd | mmap | mm_rb vm_area_struct vm_start | vm_end | vm_flags vm_fault vma | address | pte mmap 链表 vma 指针 页表 (PGD/PUD/PMD/PTE) 多级页表结构 pte_t (页表项) 物理页帧号 + 权限位 struct page flags | _refcount | mapping pgd 指向 最后一级 pfn 映射 物理内存 (Physical Memory) 页帧 (Page Frame) 0 ~ max_pfn 管理物理页 缺页处理本质:通过 VMA 找到 vmf → 遍历页表 → 分配/映射物理页 → 更新 pte

3.7 缺页处理中的锁与并发

缺页处理可能发生在多个 CPU 上,内核通过以下机制保证并发安全:

  • mmap_lock(读写信号量):在 do_page_fault() 入口获取,保护 VMA 链表的遍历与修改。读缺页获取读锁,写缺页(如 VMA 分裂)获取写锁。
  • PTE 锁(页表锁):在 handle_pte_fault() 中获取,保护单个页表项的原子更新。使用 pte_offset_map_lock() 获取。
  • LRU 锁:在分配物理页并加入 LRU 链表时使用,防止页面回收与缺页分配冲突。
调优观察点: 使用 perf lock 或 /proc/lock_stat 可以观察 mmap_lock 的竞争情况。如果 mmap_lock 的等待时间过长,说明多线程频繁进行缺页或 VMA 操作,可考虑使用 VMA 缓存 或 THP 减少缺页次数。

3.8 缺页统计与监控

系统管理员和内核开发者可以通过以下接口监控缺页行为:

监控接口 关键指标 说明
/proc/stat pgfault, pgmajfault 系统级缺页计数(总缺页 / 主缺页)
/proc/[pid]/stat minflt, majflt 进程级次缺页 / 主缺页计数
sar -B pgpgin/s, pgpgout/s, majflt/s 系统级缺页与换页统计
perf stat -e page-faults page-faults perf 工具精确采样缺页事件
tracepoint: exceptions:page_fault_user 地址、错误码、进程 ftrace 跟踪每个用户态缺页
实战技巧: 使用 bpftrace 可以动态跟踪缺页处理耗时:
bpftrace -e 'kprobe:handle_mm_fault { @start[tid] = nsecs; } kretprobe:handle_mm_fault /@start[tid]/ { @us[comm] = hist((nsecs - @start[tid]) / 1000); delete(@start[tid]); }'