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 中存放触发异常的虚拟地址unsignedlongaddress=read_cr2();// 进入核心处理逻辑__do_page_fault(regs,error_code,address);}1.2.缺页异常处理流程
┌─────────────────┐ │ 触发缺页异常 │ │(CR2=故障地址)│ └────────┬────────┘ ▼ ┌─────────────────┐ │ do_page_fault │ │(arch相关入口)│ └────────┬────────┘ ▼ ┌─────────────────────────┐ │ 查找 vma(find_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 的实现细节
- 步骤 1:fork() 时的页表设置
// 在 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;// 如果页是可写的,将其标记为"写保护 + COW"if(pte_write(pte)){ptep_set_wrprotect(src_mm,addr,src_pte);// 父进程页表置只读pte=pte_wrprotect(pte);// 子进程页表也置只读}set_pte_at(dst_mm,addr,dst_pte,pte);}关键:父子进程的 PTE 都被清除了 _PAGE_RW(可写位),但保留了 _PAGE_PRESENT。
步骤 2:写操作触发缺页异常
当子进程(或父进程)尝试写入共享页时:- CPU 检查 PTE,发现 Present=1 但 Writable=0
- 触发 #PF,错误码的 W/R=1(写操作)
- 进入 do_wp_page() —— Write Protect 处理
do_wp_page() 核心逻辑
// mm/memory.cstaticvm_fault_tdo_wp_page(structvm_fault*vmf){structvm_area_struct*vma=vmf->vma;// 1. 获取旧的 pte 和对应的页old_page=vmf->pte_page(*vmf->pte);// 2. 如果页引用计数为 1(只有当前进程在用)// 直接恢复写权限即可,无需复制!if(page_ref_count(old_page)==1){pte_t entry;entry=pte_mkyoung(*vmf->pte);// 标记访问过entry=maybe_mkwrite(pte_mkdirty(entry),vma);// 恢复写权限set_pte_at(vma->vm_mm,vmf->address,vmf->pte,entry);returnVM_FAULT_WRITE;}// 3. 引用计数 > 1,需要真正的 COW 复制// 分配新页,复制数据new_page=alloc_page_vma(GFP_HIGHUSER_MOVABLE,vma,vmf->address);copy_user_highpage(new_page,old_page,vmf->address,vma);// 4. 建立新的映射,赋予写权限entry=mk_pte(new_page,vma->vm_page_prot);entry=maybe_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()──────────┤ │ ↓ │ │ 引用计数=2>1│ │ ↓ │ │ 分配新页 Page X'│ │ 复制 Page X → X'│ │ ↓ │ │ Page X':PTE 可写 │ │ Page X:引用计数=1│ │ ↓ │ │ 继续写入 X'✓ │ │ │ │◄──── X 仍只读共享 ─────►│ ← 子进程仍共享 Page X2.4.关键要点总结
| 要点 | 说明 |
|---|---|
| COW 不是内核"主动"复制 | 而是被动触发——通过写保护页 + 缺页异常实现 |
| 延迟复制 | 只有真正写入时才复制,未写入的页永远共享 |
| 引用计数优化 | 如果引用计数为 1,直接恢复写权限,连复制都省了 |
| 页表级保护 | 利用硬件 MMU 的页表权限位实现,无额外开销 |
| 透明性 | 对应用程序完全透明,进程以为自己拥有独立地址空间 |