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中,否则每次缺页异常都会额外产生一次指令缓存未命中。内核在启动时会将向量表对齐到缓存行边界,并锁定关键入口。
ARM64 异常向量表与缺页异常入口 VBAR_EL1 基地址 Sync Current EL (SP0) Sync Current EL (SPx) Sync Lower EL (AArch64) Sync Lower EL (AArch32) Data Abort 入口 Instruction Abort 入口 do_page_fault() 异常向量表偏移:Data Abort 在 Sync Current EL 组内偏移 0x080(SP0)或 0x880(SPx)

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 等
do_page_fault 主流程决策树 do_page_fault() 地址 > TASK_SIZE? 是(内核地址) 否(用户地址) 内核态缺页处理 用户态缺页处理 vmalloc_fault / 内核BUG 查找vma → handle_mm_fault handle_mm_fault() 内核态缺页通常由 vmalloc 延迟分配或内核BUG引起,用户态缺页是主要性能关注点

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 处理路径差异详解

用户态缺页处理路径:

  1. 获取mmap_lock(读锁或写锁,取决于是否修改vma)。
  2. 调用find_vma()查找故障地址所在的vm_area_struct。
  3. 检查访问权限(读/写/执行)与vma权限是否匹配。
  4. 调用handle_mm_fault() → __handle_mm_fault() → 逐级建立页表。
  5. 根据页表项状态分发:匿名页(do_anonymous_page)、文件映射页(do_fault)、写时复制(do_wp_page)、交换页(do_swap_page)。

内核态缺页处理路径:

  1. 检查故障地址是否在vmalloc区域:调用vmalloc_fault()。
  2. 如果是vmalloc区域,直接从init_mm复制页表项到当前进程的内核页表。
  3. 如果不是vmalloc区域,检查是否为内核BUG(如空指针解引用),触发kernel_mode_fault()。
  4. 内核态缺页不获取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查找和页表遍历可能跨越多个缓存行) 较好(内核页表通常已缓存)
内核态 vs 用户态缺页处理路径对比 用户态缺页路径 1. 获取 mmap_lock(读/写锁) 2. find_vma() 查找 vma 3. handle_mm_fault() 建立页表 4. 分发:匿名/文件/COW/交换 内核态缺页路径 1. 检查地址是否在 vmalloc 区域 2. vmalloc_fault() 复制页表项 3. 非 vmalloc → 内核BUG处理 4. 不获取 mmap_lock,无锁路径 关键差异:用户态路径需要获取 mmap_lock 并遍历进程vma树,内核态路径直接检查地址范围并复制页表 用户态缺页是性能优化的主要目标,内核态缺页通常只发生在模块加载或vmalloc延迟分配时

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路径仍需关注。
  • 异常向量表的对齐和入口代码的缓存局部性,是入口延迟优化的基础。