深入理解Linux内核--缺页异常,写时复制

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

cpp 复制代码
// arch/x86/mm/fault.c
dotraplinkage void notrace
do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
    // CR2 中存放触发异常的虚拟地址
    unsigned long address = read_cr2();
    
    // 进入核心处理逻辑
    __do_page_fault(regs, error_code, address);
}

1.2.缺页异常处理流程

cpp 复制代码
┌─────────────────┐
│  触发缺页异常    │
│  (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() 时的页表设置
cpp 复制代码
// 在 copy_one_pte() 中
static inline unsigned long
copy_one_pte(struct mm_struct *dst_mm, struct mm_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() 核心逻辑

cpp 复制代码
// mm/memory.c
static vm_fault_t do_wp_page(struct vm_fault *vmf)
{
    struct vm_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);
        
        return VM_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);
    
    return VM_FAULT_WRITE;
}

2.3.COW 的完整流程图解

cpp 复制代码
父进程 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 的页表权限位实现,无额外开销
透明性 对应用程序完全透明,进程以为自己拥有独立地址空间
相关推荐
好评1242 小时前
【Linux】网络基础概念
linux·网络
杨云龙UP2 小时前
Toad for Oracle 调整表空间 Datafile 大小:Usage、Used Pct of Max 与 RESIZE 实战
linux·运维·服务器·数据库·oracle·dba·数据库运维
raindayinrain2 小时前
深入理解linux内核--文件页高速缓存,页框回收,性能优化
linux·性能优化·高速缓存·页框
badboy1213 小时前
Xilinx Vitis Core Development Kit 2025.2.1_Win+Linux
linux·fpga开发·xilinx
那年窗外下的雪.3 小时前
Linux 学习笔记
linux·笔记·学习
小张同学a.3 小时前
MooseFS 分布式存储实战指南:部署、运维与 Pacemaker+SBD 高可用落地
linux·运维·分布式·pacemaker·moosefs·sbd
精进之道3 小时前
拆开一个 .rpm 文件,里面到底装了什么?
linux
wuminyu4 小时前
JDK21 FFM异步读取MSG_ZEROCOPY错误队列揭秘
java·linux·c语言·jvm·c++
Eloudy4 小时前
sunshine - ubuntu 22.04, moonlight - mac OS26 远程桌面
linux·运维·ubuntu