4、缺页异常路径分析:handle_mm_fault函数、fault_in_kernel_space、fault_in_user_space
缺页异常(Page Fault)是MMU与内核交互最频繁的路径之一。当CPU访问的虚拟地址在TLB或页表中不存在有效映射时,MMU触发缺页异常,由内核的缺页异常处理程序接管。本章深入分析核心处理函数 handle_mm_fault 的完整路径,并重点区分内核态缺页(fault_in_kernel_space)与用户态缺页(fault_in_user_space)的差异。
性能要点:缺页异常路径是内存子系统中最耗时的同步路径之一。一次缺页处理可能涉及VMA查找、物理页分配、页表填充、TLB刷新等操作,延迟通常在微秒到毫秒级。理解路径分支是性能优化的前提。
4.1 缺页异常入口与分发
缺页异常在x86架构中由中断向量14(#PF)触发,内核通过 do_page_fault 函数接收异常信息,包括触发地址(address)、错误码(error_code)和寄存器状态。核心分发逻辑如下:
// arch/x86/mm/fault.c (简化示意)
static inline bool fault_in_kernel_space(unsigned long address)
{
return address >= TASK_SIZE_MAX;
}
static noinline void
__do_page_fault(struct pt_regs *regs, unsigned long error_code,
unsigned long address)
{
struct vm_area_struct *vma;
struct mm_struct *mm = current->mm;
unsigned int flags = FAULT_FLAG_DEFAULT;
// 1. 判断地址空间
if (fault_in_kernel_space(address)) {
// 内核空间缺页路径
if (vmalloc_fault(address) >= 0)
return;
// 处理内核空间其他异常
handle_kernel_space_fault(regs, error_code, address);
return;
}
// 2. 用户空间缺页路径
mmap_read_lock(mm);
vma = find_vma(mm, address);
if (!vma || vma->vm_start > address) {
// 无效地址 -> SIGSEGV
mmap_read_unlock(mm);
return;
}
// 3. 进入核心处理
handle_mm_fault(vma, address, flags, regs);
mmap_read_unlock(mm);
}
关键判断:
fault_in_kernel_space 通过比较地址与 TASK_SIZE_MAX(x86_64上通常为0x00007fffffffffff)快速区分地址归属。内核空间地址(>= TASK_SIZE_MAX)走独立路径,用户空间地址走VMA查找路径。
4.2 handle_mm_fault 核心路径
handle_mm_fault 是缺页处理的核心枢纽,负责将缺页请求分发到具体的底层处理函数。其内部包含多个关键阶段:
| 阶段 | 函数 | 主要操作 | 性能开销 |
|---|---|---|---|
| 1. 页表层级遍历 | __handle_mm_fault | 逐级查找/创建页表项(PGD→P4D→PUD→PMD→PTE) | 中等(内存访问+锁) |
| 2. 写时复制(COW) | do_wp_page | 复制物理页、更新页表、TLB刷新 | 高(页分配+拷贝) |
| 3. 匿名页缺页 | do_anonymous_page | 分配零页或新页、映射到用户空间 | 中(页分配+清零) |
| 4. 文件映射缺页 | do_fault | 从文件系统读取页缓存、建立映射 | 高(I/O+页缓存操作) |
| 5. 交换缺页 | do_swap_page | 从交换分区/文件读回物理页 | 非常高(块I/O) |
// mm/memory.c (核心路径简化)
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
pte_t entry = vmf->orig_pte;
if (!pte_present(entry)) {
// 页表项不存在或已换出
if (pte_none(entry)) {
// 匿名页缺页
if (vma_is_anonymous(vmf->vma))
return do_anonymous_page(vmf);
else
return do_fault(vmf); // 文件映射缺页
} else {
// 交换缺页
return do_swap_page(vmf);
}
}
// 页表项存在但权限不匹配
if (vmf->flags & FAULT_FLAG_WRITE) {
if (!pte_write(entry))
return do_wp_page(vmf); // 写时复制
}
return 0;
}
4.3 fault_in_kernel_space 路径分析
内核空间缺页(fault_in_kernel_space)通常由以下场景触发:
- vmalloc区域缺页:vmalloc分配的虚拟地址在首次访问时,页表尚未建立,需要通过
vmalloc_fault同步页表。 - 内核模块代码/数据缺页:模块加载后,其映射的页表可能尚未完全填充。
- fixmap/ioremap区域缺页:设备I/O内存映射的页表缺失。
- 内核栈溢出:当内核栈扩展到guard page时触发。
// arch/x86/mm/fault.c
static noinline int vmalloc_fault(unsigned long address)
{
pgd_t *pgd, *pgd_k;
p4d_t *p4d, *p4d_k;
pud_t *pud, *pud_k;
pmd_t *pmd, *pmd_k;
pte_t *pte_k;
// 获取当前进程的页表
pgd = pgd_offset(current->mm, address);
// 获取内核主页表(init_mm)
pgd_k = pgd_offset_k(address);
if (!pgd_present(*pgd_k))
return -1;
// 逐级同步页表项
p4d = p4d_offset(pgd, address);
p4d_k = p4d_offset(pgd_k, address);
if (!p4d_present(*p4d_k)) return -1;
// ... 类似操作到 pte 级别
return 0;
}
性能陷阱:内核空间缺页虽然发生频率远低于用户空间,但一旦发生,往往意味着内核路径存在设计缺陷(如vmalloc频繁缺页)。每次vmalloc_fault需要遍历内核主页表并同步到进程页表,在多核系统上可能引发TLB shootdown开销。
4.4 fault_in_user_space 路径分析
用户空间缺页是缺页异常的主要来源,占比通常超过99%。其路径包含以下关键步骤:
- VMA查找:通过
find_vma在红黑树中查找地址对应的虚拟内存区域,时间复杂度O(log n)。 - 权限验证:检查VMA的读写执行权限与触发缺页的访问类型是否匹配。
- 页表填充:根据缺页类型(匿名页、文件映射、交换缺页、COW)执行具体操作。
- TLB刷新:在必要时通过
flush_tlb_page或flush_tlb_mm_range刷新TLB。
| 缺页类型 | 触发条件 | 典型延迟 | 优化方向 |
|---|---|---|---|
| 匿名页缺页 | 首次访问堆/栈/BSS段 | ~1-5 µs | 预分配、透明大页 |
| 文件映射缺页 | mmap文件首次访问 | ~10-100 µs (含I/O) | 预读、页缓存预热 |
| 写时复制缺页 | fork后写操作 | ~2-10 µs | 大页、减少fork频率 |
| 交换缺页 | 访问被换出的页 | ~100 µs-10 ms | 减少交换、内存充足 |
// mm/memory.c - 匿名页缺页核心
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
struct page *page;
pte_t entry;
// 1. 分配物理页(GFP_HIGHUSER_MOVABLE)
page = alloc_zeroed_user_highpage_movable(vmf->vma, vmf->address);
if (!page)
return VM_FAULT_OOM;
// 2. 建立页表项
entry = mk_pte(page, vmf->vma->vm_page_prot);
if (vmf->flags & FAULT_FLAG_WRITE)
entry = pte_mkwrite(pte_mkdirty(entry));
// 3. 设置页表
set_pte_at(vmf->vma->vm_mm, vmf->address, vmf->pte, entry);
// 4. 更新LRU和统计
lru_cache_add_inactive_or_unevictable(page, vmf->vma);
return 0;
}
4.5 路径性能对比与优化切入点
从性能角度,缺页路径的优化需要针对不同分支采取差异化策略:
- 减少缺页次数:通过大页(HugeTLB/透明大页)减少页表遍历深度和TLB缺失。
- 加速VMA查找:使用VMA缓存(
vma->vm_rb缓存最近访问的VMA)减少红黑树遍历。 - 页分配优化:使用per-CPU页缓存(pcp list)减少分配锁竞争。
- 批量处理:对于文件映射缺页,通过
filemap_fault的预读机制批量读取多页。 - 避免交换:通过内存规整(compaction)和回收策略优化,减少交换缺页的发生。
性能指标参考:在典型4核x86_64服务器上,一次匿名页缺页处理耗时约1-3 µs,文件映射缺页(页缓存命中)约3-10 µs,交换缺页约100-500 µs。优化目标是将缺页处理延迟控制在10 µs以内。
4.6 路径选择与性能调优建议
在实际性能分析中,可以通过perf工具统计缺页异常的分支分布:
# 统计缺页类型分布
perf stat -e page-faults,minor-faults,major-faults ./workload
# 跟踪缺页事件
perf record -e page-faults:u -ag -- ./workload
perf report --stdio
# 使用bpftrace动态跟踪
bpftrace -e 'kprobe:handle_mm_fault { @[kstack] = count(); }'
优化优先级:首先减少major fault(交换缺页)的发生,其次优化文件映射缺页的预读策略,最后考虑匿名页缺页的批量处理。内核空间缺页应作为异常情况监控,一旦频率异常需立即排查。
理解 handle_mm_fault 的路径分支是缺页性能优化的基础。在实际系统中,通过perf、tracepoint和bpftrace等工具定位热点分支,结合具体业务场景(数据库、Web服务器、HPC等)选择优化策略,才能有效降低缺页处理延迟,提升内存子系统整体性能。