1.缺页异常1.1.缺页异常Page Fault概述1.1.1.什么是缺页异常缺页异常是 x86 架构中的 #PF中断向量 14属于一种同步异常。当 CPU 访问某个虚拟地址时发现该地址对应的页表项PTE存在以下情况之一就会触发缺页异常触发条件说明页不存在PTE 的Present位为 0物理页尚未分配权限不足写只读页、用户态访问内核页、执行不可执行页等保留位被置位页表项格式非法CPU 会将出错虚拟地址存入 CR2 寄存器并在栈上压入错误码Error Code包含P 位0页不存在1权限错误W/R 位0读操作1写操作U/S 位0内核态1用户态I/D 位0数据访问1指令取指1.1.2.内核入口do_page_fault// arch/x86/mm/fault.cdotraplinkagevoidnotracedo_page_fault(structpt_regs*regs,unsignedlongerror_code){// CR2 中存放触发异常的虚拟地址unsignedlongaddressread_cr2();// 进入核心处理逻辑__do_page_fault(regs,error_code,address);}1.2.缺页异常处理流程┌─────────────────┐ │ 触发缺页异常 │ │(CR2故障地址)│ └────────┬────────┘ ▼ ┌─────────────────┐ │ do_page_fault │ │(arch相关入口)│ └────────┬────────┘ ▼ ┌─────────────────────────┐ │ 查找 vmafind_vma │ │ 地址是否在某个vma范围内 │ └────────┬────────────────┘ 否/\ 是 ▼ ▼ ┌──────────┐ ┌────────────────────────┐ │ 非法访问 │ │ 调用 handle_mm_fault │ │(SIGSEGV)│ │ 进入通用缺页处理逻辑 │ └──────────┘ └────────┬───────────────┘ ▼ ┌─────────────────────┐ │ pgd → p4d → pud │ │ → pmd → pte 逐级 │ │ 分配/查找页表项 │ └────────┬────────────┘ ▼ ┌─────────────────────┐ │ handle_pte_fault │ │ 根据pte状态分发 │ └────────┬────────────┘ ▼ ┌───────────┼───────────┐ ▼ ▼ ▼ ┌────────┐ ┌────────┐ ┌────────┐ │pte为空 │ │pte存在 │ │pte存在 │ │(新分配)│ │但不可写│ │且可写 │ └────┬───┘ └────┬───┘ └────┬───┘ ▼ ▼ ▼ do_anonymous do_wp_page 正常访问/do_fault(COW入口)(不应发生)2.写时复制Copy-on-Write, COW机制2.1.COW 的设计动机核心问题fork() 创建子进程时如果立即复制父进程的全部地址空间耗时巨大GB 级内存很多内存子进程根本不会修改如代码段、只读数据COW 解决方案子进程共享父进程的物理页但将共享页标记为只读。当任一进程尝试写入时才触发缺页异常此时才真正复制一份私有副本。2.2.COW 的实现细节步骤 1fork() 时的页表设置// 在 copy_one_pte() 中staticinlineunsignedlongcopy_one_pte(structmm_struct*dst_mm,structmm_struct*src_mm,pte_t*dst_pte,pte_t*src_pte,...){pte_t pte*src_pte;// 如果页是可写的将其标记为写保护 COWif(pte_write(pte)){ptep_set_wrprotect(src_mm,addr,src_pte);// 父进程页表置只读ptepte_wrprotect(pte);// 子进程页表也置只读}set_pte_at(dst_mm,addr,dst_pte,pte);}关键父子进程的 PTE 都被清除了 _PAGE_RW可写位但保留了 _PAGE_PRESENT。步骤 2写操作触发缺页异常当子进程或父进程尝试写入共享页时CPU 检查 PTE发现 Present1 但 Writable0触发 #PF错误码的 W/R1写操作进入 do_wp_page() —— Write Protect 处理do_wp_page() 核心逻辑// mm/memory.cstaticvm_fault_tdo_wp_page(structvm_fault*vmf){structvm_area_struct*vmavmf-vma;// 1. 获取旧的 pte 和对应的页old_pagevmf-pte_page(*vmf-pte);// 2. 如果页引用计数为 1只有当前进程在用// 直接恢复写权限即可无需复制if(page_ref_count(old_page)1){pte_t entry;entrypte_mkyoung(*vmf-pte);// 标记访问过entrymaybe_mkwrite(pte_mkdirty(entry),vma);// 恢复写权限set_pte_at(vma-vm_mm,vmf-address,vmf-pte,entry);returnVM_FAULT_WRITE;}// 3. 引用计数 1需要真正的 COW 复制// 分配新页复制数据new_pagealloc_page_vma(GFP_HIGHUSER_MOVABLE,vma,vmf-address);copy_user_highpage(new_page,old_page,vmf-address,vma);// 4. 建立新的映射赋予写权限entrymk_pte(new_page,vma-vm_page_prot);entrymaybe_mkwrite(pte_mkdirty(entry),vma);set_pte_at_notify(vma-vm_mm,vmf-address,vmf-pte,entry);// 5. 减少旧页引用计数page_ref_dec(old_page);returnVM_FAULT_WRITE;}2.3.COW 的完整流程图解父进程 P 子进程 C │ │ │fork()│ ├─────────────────────────►│ │ │ │ 共享物理页 Page X │ ← 同一物理页 │(PTE:R/O,COW)│(PTE:R/O,COW)│ │ │◄──────── 共享 ──────────►│ │ │ │ 写入 Page X │ │ ↓ │ │ #PF(写保护异常)│ │ ↓ │ ├─do_wp_page()──────────┤ │ ↓ │ │ 引用计数21│ │ ↓ │ │ 分配新页 Page X│ │ 复制 Page X → X│ │ ↓ │ │ Page X:PTE 可写 │ │ Page X:引用计数1│ │ ↓ │ │ 继续写入 X✓ │ │ │ │◄──── X 仍只读共享 ─────►│ ← 子进程仍共享 Page X2.4.关键要点总结要点说明COW 不是内核主动复制而是被动触发——通过写保护页 缺页异常实现延迟复制只有真正写入时才复制未写入的页永远共享引用计数优化如果引用计数为 1直接恢复写权限连复制都省了页表级保护利用硬件 MMU 的页表权限位实现无额外开销透明性对应用程序完全透明进程以为自己拥有独立地址空间