剖析 Linux 缺页异常处理流程:do_page_fault 源码与写时复制(COW)机制
剖析 Linux 缺页异常处理流程:do_page_fault 源码与写时复制(COW)机制

在操作系统原理中,虚拟内存最精妙的设计之一就是按需调页(Demand Paging)与写时复制(Copy-On-Write, COW)。
当我们在用户态调用 malloc(1GB) 或 mmap 时,Linux 内核并不会立刻在物理内存中为你分配 1GB 的物理页帧,而仅仅是在当前进程的虚拟内存空间(mm_struct)中圈出一块合法的虚拟地址区间(vm_area_struct, VMA)。真正的物理内存分配与页表建立,全部被延迟到了 CPU 第一次真正读写这块虚拟地址的瞬间——由硬件 MMU 触发缺页异常(Page Fault Exception),交由内核的中断处理函数完成。
而在 fork() 创建子进程时,父子进程最初共享所有的物理内存页,并将对应的页表项(PTE)全部标记为“只读”。直到其中某一方试图写入页面时,内核才会真正为子进程拷贝一份全新的物理页帧。
本文将从硬件中断触发、内核源码调用栈一直到 do_wp_page,深度剖析 Linux 缺页异常与写时复制的完整底层执行链路。
一、 硬件异常触发与内核调用全景
当 CPU 执行访存指令遇到未映射的页表项或权限违规时,硬件 MMU 会将发生错误的虚拟地址存入 CR2 控制寄存器,并压入错误码(Error Code),随后触发 14 号中断向量(#PF):
[ CPU 访存指令: mov %rax, (%rbx) ]
│
▼ (MMU 检测到 PTE 存在位为 0,或只读页发生写入)
[ 硬件触发 #PF (14号中断向量) / CR2 写入故障地址 ]
│
▼
[ arch/x86/mm/fault.c: do_page_fault() ]
│
▼
[ mm/memory.c: __do_page_fault() -> handle_mm_fault() ]
│ (遍历 PGD -> P4D -> PUD -> PMD -> PTE)
▼
[ mm/memory.c: handle_pte_fault() ]
│
├─► 匿名页首次访问 ──► do_anonymous_page() (延迟分配物理页)
├─► 文件页未载入 ──► do_fault() (从磁盘读取 Page Cache)
├─► 页面在 Swap 中 ──► do_swap_page() (从交换分区换入)
└─► 写保护页发生写入 ──► do_wp_page() (【核心】写时复制 COW 流程)
二、 核心源码剖析:handle_pte_fault 的分支决策
在 Linux 内核内存子系统的核心文件 mm/memory.c 中,handle_pte_fault() 承担了所有的分流逻辑:
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
pte_t entry;
// 1. 若页表项不存在 (PTE 为空)
if (!vmf->pte) {
if (vma_is_anonymous(vmf->vma))
return do_anonymous_page(vmf); // 首次访问匿名页
else
return do_fault(vmf); // 首次访问文件映射页
}
entry = vmf->orig_pte;
// 2. 若页面不在物理内存中 (PTE 非空但 present 位为 0)
if (!pte_present(entry)) {
if (pte_none(entry))
return do_fault(vmf);
return do_swap_page(vmf); // 页面被换出到 Swap 分区,执行换入
}
// 3. 若页面在内存中,但用户试图写入一个只读页面 (Write-Protection Fault)
if (vmf->flags & FAULT_FLAG_WRITE) {
if (!pte_write(entry))
return do_wp_page(vmf); // 触发写时复制 (COW) !
entry = pte_mkdirty(entry);
}
return 0;
}
三、 写时复制核心实现:do_wp_page 源码拆解
当父进程 fork() 后,父子进程共享物理页,页表项的 _PAGE_RW(读写位)被清 0。当某个进程执行写操作时,触发 do_wp_page:
static vm_fault_t do_wp_page(struct vm_fault *vmf)
__releases(vmf->ptl)
{
struct vm_area_struct *vma = vmf->vma;
struct page *old_page;
// 1. 获取引发异常的旧物理页指针
old_page = vm_normal_page(vma, vmf->address, vmf->orig_pte);
if (!old_page) {
// 特殊映射页处理...
}
// 2. 检查物理页的引用计数 (page_count)
// 如果引用计数为 1 (例如其他共享者已经退出或执行了写操作),说明只有当前进程持有该页!
if (PageAnon(old_page) && !PageKsm(old_page)) {
if (reuse_swap_page(old_page)) {
// 直接将当前页的 PTE 升级为可读写 (_PAGE_RW 置 1),无需发生任何物理内存拷贝!
wp_page_reuse(vmf);
return VM_FAULT_WRITE;
}
}
// 3. 页面被多个进程共享 (引用计数 > 1),必须执行物理内存深度拷贝
return wp_page_copy(vmf);
}
wp_page_copy 的物理拷贝四步曲:
static vm_fault_t wp_page_copy(struct vm_fault *vmf)
{
struct page *old_page = vmf->page;
struct page *new_page;
pte_t entry;
// A. 从伙伴系统申请一个全新的 4KB 物理页帧
new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vmf->vma, vmf->address);
if (!new_page)
return VM_FAULT_OOM;
// B. 将旧物理页的内容完整拷贝到新物理页中 (涉及 CPU 内存拷贝开销)
copy_user_highpage(new_page, old_page, vmf->address, vmf->vma);
// C. 重新获取页表自旋锁并构建新的可写 PTE
entry = mk_pte(new_page, vmf->vma->vm_page_prot);
entry = pte_mkwrite(pte_mkdirty(entry)); // 标记为可读写且为脏页
// D. 更新 MMU 页表:将当前进程的虚拟地址指向新的物理页帧,并使旧物理页引用计数 -1
set_pte_at_notify(vmf->vma->vm_mm, vmf->address, vmf->pte, entry);
page_remove_rmap(old_page, false);
put_page(old_page);
// 刷新当前 CPU 核心的 TLB 缓存
update_mmu_cache(vmf->vma, vmf->address, vmf->pte);
return 0;
}
四、 性能特征与工程避坑指南
写时复制机制让 fork() 的耗时降低了几个数量级,但在高性能服务器与实时系统中,也带来了潜在的性能隐患:
┌──────────────────────────────┐
│ Redis 执行 BGSAVE │
└──────────────┬───────────────┘
│
▼
[ fork() 创建后台 RDB 保存子进程 ]
│
┌──────────────────────────┴──────────────────────────┐
▼ ▼
【只读查询请求】 【高频写入请求】
直接命中共享物理内存 高频触发 do_wp_page / copy_user_highpage
0 额外开销,吞吐极高 系统内存占用瞬间翻倍 (COW 内存暴涨)
CPU 陷入密集内存拷贝,接口延迟剧烈抖动
- Redis 的 COW 内存暴涨问题:
Redis 在执行bgsave或bgrewriteaof时,如果主进程在此期间遭遇大量写请求,内核会为每一个被修改的 Key 所在的 4KB 页触发 COW 复制,导致主机可用内存迅速耗尽。因此,运行 Redis 的宿主机通常需要预留 50% 以上的空闲内存。 - 实时系统关闭 COW 延迟:
在高频量化交易或工业运动控制等确定性延迟场景下,程序通常在初始化阶段调用mlockall(MCL_FUTURE)并主动写入所有申请的内存(Pre-faulting),提前把缺页中断消灭在系统启动期,避免在交易撮合或电机控制的关键路径上触发do_page_fault。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)