3. 缺页异常处理入口
缺页异常(Page Fault)是MMU与内核交互最频繁的路径之一。当CPU访问的虚拟地址在TLB或页表中缺失有效映射时,MMU会触发一次缺页异常,CPU自动跳转到异常向量表中预定义的入口地址,最终由内核的do_page_fault函数完成处理。理解这一入口路径的硬件与软件协作机制,是分析缺页异常性能开销的第一步。
3.1 缺页异常在异常向量表中的位置
ARM64和x86_64架构的异常向量表设计不同,但缺页异常(或类似的数据/指令中止)都拥有固定的向量号。以ARM64为例,缺页异常对应同步异常中的Data Abort(数据中止)和Instruction Abort(指令中止),向量表基址由VBAR_EL1寄存器指定。
| 架构 | 异常类型 | 向量偏移 | 触发条件 |
|---|---|---|---|
| ARM64 | Data Abort(同步) | 0x200 / 0x280 | 数据加载/存储时页表缺失或权限错误 |
| ARM64 | Instruction Abort(同步) | 0x100 / 0x180 | 指令预取时页表缺失或权限错误 |
| x86_64 | Page Fault (#PF) | 中断向量14 | 线性地址转换失败 |
性能要点:异常向量表的入口地址必须驻留在CPU的L1 I-Cache中,否则每次缺页异常都会额外产生一次指令缓存未命中。内核在启动时会将向量表对齐到缓存行边界,并锁定关键入口。
3.2 do_page_fault 函数核心流程
do_page_fault是缺页异常处理的C语言入口,位于arch/arm64/mm/fault.c(ARM64)或arch/x86/mm/fault.c(x86)。其核心职责是解析异常原因、获取故障地址、判断地址空间合法性,并分发到具体的处理路径。
3.2.1 关键参数与寄存器
- 故障地址(fault_addr):从
FAR_EL1(ARM64)或CR2(x86)寄存器读取。 - 错误码(fault_code):从
ESR_EL1(ARM64)或错误码栈(x86)解析,包含访问类型、权限、是否用户态等信息。 - pt_regs:保存异常发生时的CPU寄存器现场,用于恢复执行。
// ARM64 do_page_fault 简化原型
static int __kprobes do_page_fault(unsigned long far, unsigned int esr,
struct pt_regs *regs)
{
unsigned long addr = far; // 故障虚拟地址
unsigned int mm_flags = FAULT_FLAG_DEFAULT;
struct vm_area_struct *vma;
struct mm_struct *mm = current->mm;
// ... 解析 esr 获取访问类型、权限位 ...
// ... 查找 vma,判断地址合法性 ...
// ... 分发到 handle_mm_fault() ...
}
3.2.2 处理路径分类
| 错误码位 | 含义 | 典型处理路径 |
|---|---|---|
| bit 0(0=无页, 1=保护) | 页表项不存在 vs 权限违规 | do_anonymous_page / do_swap_page |
| bit 1(0=读, 1=写) | 访问类型 | 写时复制(COW)路径 |
| bit 2(0=内核, 1=用户) | 异常发生时的特权级 | 内核态 vs 用户态处理差异 |
| bit 3(保留位/组合) | 指令预取 vs 数据访问 | do_translation_fault 等 |
3.3 内核态与用户态缺页处理差异
内核态和用户态缺页异常在触发场景、处理路径和性能开销上存在本质差异。理解这些差异有助于在性能分析时快速定位瓶颈来源。
3.3.1 触发场景对比
| 对比维度 | 用户态缺页 | 内核态缺页 |
|---|---|---|
| 典型场景 | 进程首次访问堆/栈/文件映射页 | vmalloc 延迟分配、模块加载、ioremap |
| 地址空间 | 用户虚拟地址空间(0 ~ TASK_SIZE) | 内核虚拟地址空间(TASK_SIZE ~ 0xFFFFFFFF) |
| 页表层级 | 进程私有的页表(PGD/P4D/PUD/PMD/PTE) | 内核全局页表(init_mm)或进程内核页表 |
| 锁竞争 | mmap_lock(可能严重竞争) | 通常无锁或使用轻量级RCU |
| 处理频率 | 极高(应用启动、内存分配) | 极低(仅内核内存管理事件) |
3.3.2 处理路径差异详解
用户态缺页处理路径:
- 获取
mmap_lock(读锁或写锁,取决于是否修改vma)。 - 调用
find_vma()查找故障地址所在的vm_area_struct。 - 检查访问权限(读/写/执行)与vma权限是否匹配。
- 调用
handle_mm_fault()→__handle_mm_fault()→ 逐级建立页表。 - 根据页表项状态分发:匿名页(do_anonymous_page)、文件映射页(do_fault)、写时复制(do_wp_page)、交换页(do_swap_page)。
内核态缺页处理路径:
- 检查故障地址是否在
vmalloc区域:调用vmalloc_fault()。 - 如果是vmalloc区域,直接从
init_mm复制页表项到当前进程的内核页表。 - 如果不是vmalloc区域,检查是否为内核BUG(如空指针解引用),触发
kernel_mode_fault()。 - 内核态缺页不获取mmap_lock,因为内核地址空间是全局共享且稳定的。
性能陷阱:用户态缺页处理中,
mmap_lock的竞争是主要性能瓶颈之一。在多线程密集分配内存的场景下,读-写锁的切换可能导致大量上下文切换和缓存颠簸。内核态缺页虽然频率低,但vmalloc_fault路径中的页表复制操作在TLB压力大时可能产生额外开销。
3.3.3 性能指标对比
| 指标 | 用户态缺页(典型值) | 内核态缺页(典型值) |
|---|---|---|
| 单次处理延迟 | 200ns ~ 2μs(取决于页表深度和缓存状态) | 100ns ~ 500ns(路径更短) |
| TLB影响 | 可能刷新本地TLB,影响后续访问 | 通常只更新内核页表,不影响用户TLB |
| 锁开销占比 | 30% ~ 60%(mmap_lock竞争严重时) | < 5%(无锁或RCU) |
| 缓存局部性 | 差(vma查找和页表遍历可能跨越多个缓存行) | 较好(内核页表通常已缓存) |
3.4 异常向量表与性能关键点
异常向量表的设计直接影响缺页异常的入口延迟。以下是与性能相关的几个关键点:
- 向量表对齐:ARM64要求向量表必须2KB对齐,x86_64的IDT也要求页对齐。对齐不足会导致跨页访问,增加L1缓存缺失概率。
- 入口代码大小:每个向量入口通常只有32字节(ARM64)或16字节(x86_64),只能存放跳转指令。跳转目标必须位于同一缓存行内,避免I-Cache miss。
- 中断屏蔽:缺页异常处理期间,CPU会自动屏蔽中断(或部分异常),这会影响系统的实时性。长时间持有mmap_lock会加剧中断延迟。
优化建议:在性能敏感系统中,可以将
do_page_fault的入口函数标记为__kprobes或__always_inline,减少函数调用开销。同时,确保异常向量表所在的物理页被锁定在L2缓存中(通过mapping属性设置)。
3.5 本章小结
- 缺页异常通过异常向量表进入内核,ARM64使用Data/Instruction Abort向量,x86_64使用#PF向量。
do_page_fault是统一的C语言入口,根据故障地址和错误码分发到不同处理路径。- 用户态缺页处理路径长、锁竞争严重,是性能优化的主要目标;内核态缺页路径短、频率低,但vmalloc_fault路径仍需关注。
- 异常向量表的对齐和入口代码的缓存局部性,是入口延迟优化的基础。