深入理解Linux内核--缺页异常,写时复制
2026/8/24 20:04:44 网站建设 项目流程

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 X

2.4.关键要点总结

要点说明
COW 不是内核"主动"复制而是被动触发——通过写保护页 + 缺页异常实现
延迟复制只有真正写入时才复制,未写入的页永远共享
引用计数优化如果引用计数为 1,直接恢复写权限,连复制都省了
页表级保护利用硬件 MMU 的页表权限位实现,无额外开销
透明性对应用程序完全透明,进程以为自己拥有独立地址空间

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询