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 触发缺页到内核完成处理的完整数据流与函数调用关系:
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 展示了缺页处理过程中核心数据结构之间的引用关系:
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]); }'