深入剖析 Linux 内核 do_page_fault() 函数的工作原理
do_page_fault() 是 Linux 内核中处理缺页中断的核心函数,它是 ARMv7-A(及 x86 等)架构下虚拟内存管理的关键枢纽。这个函数负责响应硬件缺页异常,判断错误类型,并执行相应的内存管理操作。
函数原型与关键参数:
void do_page_fault(struct pt_regs *regs, unsigned int esr);
regs: 指向进程在内核态保存的用户态寄存器上下文(包含触发缺页时的 PC、SP 等所有通用寄存器)
esr (Exception Syndrome Register): ARMv7-A 中的 IFSR 寄存器值,包含故障的详细原因代码
处理流程详解(针对 ARMv7-A)
第一步:获取关键硬件信息
unsigned long address = read_cp15_DFAR(); // 获取故障地址 (DFAR)
unsigned int fsr = read_cp15_IFSR(); // 获取故障状态 (IFSR)
address: 导致缺页的虚拟地址 (来自 DFAR - Data Fault Address Register)
fsr: 故障状态码 (来自 IFSR - Instruction Fault Status Register),包含:
FSR[3:0]: 故障类型 (如 0x5=Translation fault, 0x7=Access flag fault)
FSR[5:4]: 域 (Domain)
FSR[6]: 写操作标识 (1=写操作触发)
FSR[10]: 指令中止标记 (0=数据访问)
第二步:检查内核空间缺页
if (unlikely(address >= TASK_SIZE)) {
// 在内核空间发生缺页
if (!user_mode(regs)) {
// 内核自身导致的缺页 (可能严重错误)
vmalloc_fault(address); // 处理内核动态映射
return;
}
// 用户进程错误访问内核空间
bad_area(regs, address);
return;
}
内核态缺页: 处理 vmalloc 动态映射的延迟分配
用户访问内核空间: 立即终止进程 (SIGSEGV)
第三步:查找对应的 VMA (Virtual Memory Area)
vma = find_vma(mm, address);
if (unlikely(!vma)) {
// 虚拟地址不存在于任何 VMA 中
bad_area(regs, address);
return;
}
if (likely(vma->vm_start <= address))
goto good_area; // 成功落在 VMA 范围内
遍历进程的 mm_struct->mmap 链表查找包含该地址的 VMA
未找到说明是非法访问 (SIGSEGV)
第四步:权限检查 (good_area)
good_area:
write = (fsr & FSR_WRITE) != 0;
exec = (fsr & FSR_EXEC) != 0;
// 检查 VMA 权限是否匹配
if (unlikely(access_error(fsr, vma))) {
// 没有权限访问此区域 (如写只读区)
bad_area_access_error(regs, fsr, address, vma);
return;
}
关键检查项:
请求类型 VMA 权限要求 可能处理方式
READ VM_READ 正常处理
WRITE VM_WRITE 正常处理或 COW
EXECUTE VM_EXEC 正常处理或 SIGSEGV
WRITE+无VM_SHARED VM_WRITE COW 处理
第五步:区分多种缺页类型
switch (fsr & 0xF) { // 处理 FSR 的故障类型位
案例 1:首次访问的匿名页(最常见情况)
if (handle_mm_fault(vma, address,
write ? FAULT_FLAG_WRITE : 0) &
VM_FAULT_ERROR) {
// 处理分配失败
oom_kill();
}
触发场景:malloc() 后首次写入内存
操作:调用 handle_mm_fault()
分配清零的物理页 (通过 alloc_page())
建立页表映射
设置 PTE 的 PTE_VALID | PTE_USER | PTE_WRITE
案例 2:写时复制 (Copy-On-Write)
if (write && !(vma->vm_flags & VM_SHARED)) {
if (!(vma->vm_flags & VM_WRITE)) {
// 尝试写入私有只读页 (COW触发点)
handle_cow_fault(vma, address);
}
}
触发场景:写入共享库文本段或 fork() 后的父子共享页
操作:
分配新物理页
复制原始页内容 (copy_pte_range())
将进程 PTE 改为指向新副本并标记可写
案例 3:文件支持的页缺页
if (vma->vm_ops && vma->vm_ops->fault) {
ret = vma->vm_ops->fault(vma, address,
write ? FAULT_FLAG_WRITE : 0);
}
触发场景:首次访问 mmap 映射的文件区域
操作:
调用文件系统的 filemap_fault()
从磁盘读取数据到新页
更新页表指向缓存页
案例 4:换出页的换入
if (pte_swp_swapin_pte(orig_pte)) {
swap_in_page(address, entry);
}
触发场景:访问已被换出到交换区的页面
操作:
分配新物理页
从 swap 分区读回数据
重建页表项并清除 swap 标记
案例 5:访问权限位缺失
if ((fsr & 0xF) == 0x7) { // Access Flag Fault
handle_pte_fault(..., FAULT_FLAG_ACCESS);
}
ARMv7-A 特有:当 L2 PTE 存在但访问位(APX)未设置时触发
操作:仅设置 PTE 的访问位,不需分配物理页
第六步:更新页表和刷新 TLB
flush_tlb_page(vma, address); // 使旧TLB项失效
关键操作:使用协处理器指令 (CP15)
mcr p15, 0, <addr>, c8, c7, 1 (TLBIMVA - 使单个条目失效)
mcr p15, 0, <domain>, c8, c7, 0 (TLBIALL - 全无效)
第七步:错误恢复路径
return; // 正常返回将重执行失败指令
bad_area:
force_sig_fault(SIGSEGV, SEGV_MAPERR, ...);
return;
oom:
pagefault_out_of_memory(); // 触发OOM killer
return;
非法访问:发送 SIGSEGV 信号终止进程
内存不足:调用 OOM killer 选择牺牲进程
ARMv7-A 的特有处理考量
1.L1/L2 转换错误区分:
if (fsr & FSR_L1_PF) handle_l1_translation_fault();
else handle_l2_translation_fault(); // 最常见的缺页情况
2.域访问控制处理:
domain = (fsr >> 4) & 0x0F;
if (domain != DOMAIN_USER) {
// 内核域访问错误处理
}
3.精确的异常类型判断:
FSR[3:0] 错误类型 处理方式
0b0000 Alignment SIGBUS 信号
0b0101 Section trans 一级页表错误处理
0b0111 Page trans 二级页表错误 (主要缺页)
0b1001 Domain section 域权限错误
性能关键路径优化
__do_page_fault():
if (kmem_cache_page_fast_alloc()) // 快速路径分配
goto fast_path;
spin_lock(&mm->page_table_lock); // 慢速路径加锁
// ...详细处理
spin_unlock(&mm->page_table_lock);
do_page_fault() 通过解析硬件状态码,结合进程内存描述,智能地处理了 10+ 种不同类型的页面错误。它不仅是物理内存分配的入口,更是实现写时复制、内存映射文件、交换机制等高级功能的统一网关。对缺页路径的极致优化直接决定了操作系统整体的内存访问性能。