Linux内核源码详解--缺页异常(Page Fault)处理的核心函数handle_pte_fault
handle_pte_fault 是 Linux 内核中处理缺页异常(Page Fault)的核心函数,负责根据页表项(PTE)的状态和访问权限,分发到不同的子处理逻辑(如匿名页映射、文件页映射、写时复制、NUMA 迁移等)。以下基于代码逻辑和搜索结果详细解析其功能、原理及处理流程。
一、功能概述
handle_pte_fault 在缺页异常处理流程中被调用(通常由 __handle_mm_fault 触发),用于处理以下场景:
1.首次访问未映射的虚拟地址(PTE 为空)。
2.访问已被换出(Swap Out)的页面(PTE 存在但 PRESENT 位为 0)。
3.写保护触发写时复制(COW)。
4.NUMA 内存页迁移优化。
5.权限检查与页表状态更新。
二、代码逻辑分步解析
1. 检查 PMD 状态(大页/透明大页处理)
if (unlikely(pmd_none(*vmf->pmd))) {
vmf->pte = NULL; // 延迟 PTE 分配,避免与透明大页冲突
} else if (pmd_devmap_trans_unstable(vmf->pmd)) {
return 0; // 透明大页不稳定状态,需重试
} else {
vmf->pte = pte_offset_map(vmf->pmd, vmf->address); // 获取 PTE
vmf->orig_pte = *vmf->pte;
barrier();
if (pte_none(vmf->orig_pte)) { // PTE 为空则解除映射
pte_unmap(vmf->pte);
vmf->pte = NULL;
}
}
关键点:
若 PMD 未分配(pmd_none),暂不分配 PTE,避免干扰透明大页(THP)的并发操作。
pmd_devmap_trans_unstable 处理透明大页分裂场景。
2. 处理 PTE 为空的情况(首次映射)
if (!vmf->pte) {
if (vma_is_anonymous(vmf->vma))
return do_anonymous_page(vmf); // 匿名页映射
else
return do_fault(vmf); // 文件页/共享内存映射
}
static inline bool vma_is_anonymous(struct vm_area_struct *vma)
{
return !vma->vm_ops;
}
分发逻辑:
匿名页(vma->vm_ops == NULL):
读操作:映射到零页(Zero Page)以减少物理内存占用。
写操作:分配新物理页并初始化。
文件页(vma->vm_ops != NULL):
触发文件系统缺页处理(如 filemap_fault),从磁盘读取数据到 Page Cache
3. 处理 PTE 存在但 PRESENT 位为 0(已换出)
if (!pte_present(vmf->orig_pte))
return do_swap_page(vmf); // 换回(Swap In)页面
原理:
PTE 存储了 Swap Entry(标识磁盘位置),需调用 do_swap_page 将数据从 Swap 分区读回物理内存
4. 处理 NUMA 迁移优化
if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
return do_numa_page(vmf); // 迁移页面到当前 NUMA 节点
场景:
当物理页位于远端 NUMA 节点时,迁移以提升访问性能。
5. 写操作与写时复制(COW)
if (vmf->flags & FAULT_FLAG_WRITE) {
if (!pte_write(entry))
return do_wp_page(vmf); // 触发写时复制
entry = pte_mkdirty(entry); // 标记脏页
}
COW 机制:
写只读页时,分配新物理页并复制内容,更新 PTE 指向新页(原页引用计数减 1)
若物理页仅被一个进程引用(无共享),则直接设为可写,避免复制。
6. 更新 PTE 与 TLB 刷新
entry = pte_mkyoung(entry); // 标记访问位(PTE_AF)
if (ptep_set_access_flags(vma, address, pte, entry, write)) {
update_mmu_cache(vma, address, pte); // 更新 CPU 缓存
} else if (write) {
flush_tlb_fix_spurious_fault(vma, address); // 刷新 TLB 伪错误
}
关键操作:
ptep_set_access_flags:原子更新 PTE 的访问/脏位。
TLB 刷新仅在权限变更时触发(避免冗余刷新提升性能)。
三、处理流程图解
graph TD
A[handle_pte_fault] --> B{PMD 有效?}
B -- Yes --> C[获取 PTE]
B -- No --> D[延迟 PTE 分配]
C --> E{PTE 为空?}
E -- Yes --> F{匿名页?}
F -- Yes --> G[do_anonymous_page]
F -- No --> H[do_fault]
E -- No --> I{PTE Present?}
I -- No --> J[do_swap_page]
I -- Yes --> K{NUMA 迁移?}
K -- Yes --> L[do_numa_page]
K -- No --> M{写操作且只读?}
M -- Yes --> N[do_wp_page]
M -- No --> O[更新 PTE 标志]
O --> P[刷新 TLB/缓存]
四、关键设计思想
延迟与优化:
延迟 PTE 分配以避免透明大页冲突。
零页映射节省匿名页首次读的内存
分层处理:
按 PTE 状态(空/换出/写保护)分发给专用子函数,确保逻辑清晰。
并发控制:
通过 spin_lock(vmf->ptl) 锁定页表,防止并行修改
性能优化:
减少 TLB 刷新(仅在权限变更时触发)。
区分 major/minor fault(是否涉及磁盘 I/O)
五、典型场景与子函数对照表
场景 触发条件 处理函数 说明
匿名页首次访问 vmf->pte == NULL & 匿名 VMA do_anonymous_page 读:零页;写:分配新页
文件页首次映射 vmf->pte == NULL & 文件 VMA do_fault 读文件到 Page Cache
页面已换出 !pte_present(entry) do_swap_page 从 Swap 分区读回数据
写只读页(COW) write & !pte_write(entry) do_wp_page 复制页面或直接设可写
NUMA 优化迁移 pte_protnone(entry) do_numa_page 迁移页面至本地 NUMA 节点
页表更新 权限变更(如脏页/访问位) ptep_set_access_flags 更新 PTE 并刷新 TLB
六、总结
handle_pte_fault 是 Linux 虚拟内存管理的核心枢纽,通过状态机式的分发逻辑处理各类缺页异常:
匿名/文件页:按需分配物理页或读取文件数据。
COW 机制:平衡内存共享与写操作性能。
Swap 与 NUMA:优化内存不足和跨节点访问场景。
其设计充分体现了 “懒加载”(Lazy Allocation)和 “最小化开销”(如零页、延迟刷新 TLB)的原则,确保高效管理复杂的内存访问需求。