4. 匿名页缺页处理:匿名映射、COW(写时复制)机制与缺页处理
匿名页缺页是 Linux 内核内存管理中最核心的缺页类型之一。与文件页缺页不同,匿名页没有磁盘上的后备文件,其物理页面的分配、共享与回收完全由内核的 匿名映射 机制管理。本章深入剖析匿名页缺页的完整路径,重点讲解写时复制(Copy-on-Write, COW)技术的实现原理与性能影响。
4.1 匿名映射与文件映射的本质区别
在理解匿名页缺页之前,必须首先区分两种映射类型:
| 特性 | 文件映射(File-backed Mapping) | 匿名映射(Anonymous Mapping) |
|---|---|---|
| 后备存储 | 磁盘上的文件(如 ELF 二进制、共享库) | 交换分区(swap)或物理内存 |
| 缺页来源 | 文件内容未读入页缓存 | 首次访问或 COW 分裂 |
| 页回收 | 可写回文件(脏页)或直接丢弃(干净页) | 必须换出到 swap 分区 |
| 典型场景 | 程序代码段、mmap 文件 I/O | 堆(heap)、栈(stack)、mmap(MAP_ANONYMOUS) |
| VMA 标志 | VM_FILE | VM_ANON |
性能提示: 匿名页缺页的代价通常低于文件页缺页(无需磁盘 I/O),但 COW 缺页涉及页表复制和物理页分配,在 fork 密集场景下可能成为瓶颈。
4.2 匿名页缺页处理流程
当进程访问一个尚未建立物理页映射的匿名 VMA 时,内核进入 do_anonymous_page() 函数。该函数是匿名页缺页的入口,其核心流程如下:
- 检查 VMA 标志:确认当前 VMA 是匿名映射(VM_ANON),且缺页地址在 VMA 范围内。
- 判断写权限:根据页表项中的写权限位,决定是分配新页还是触发 COW。
- 分配零页(读缺页):如果是只读缺页,映射到全局零页(ZERO_PAGE),实现共享只读。
- 分配物理页(写缺页):如果是写缺页,通过
alloc_page_vma()分配一个全新的物理页,并清零。 - 建立页表映射:将新分配的物理页或零页插入进程页表,设置相应的权限位。
- 统计与记账:更新 RSS(Resident Set Size)计数,触发 cgroup 内存记账。
关键代码路径:
// mm/memory.c
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
// 1. 检查 VMA 是否为匿名映射
if (vma_is_anonymous(vma)) {
// 2. 读缺页:使用零页
if (!(vmf->flags & FAULT_FLAG_WRITE)) {
entry = pte_mkspecial(pfn_pte(my_zero_pfn(), vma->vm_page_prot));
// 映射零页到页表
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
return 0;
}
// 3. 写缺页:分配新页
page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
if (!page)
return VM_FAULT_OOM;
// 4. 建立写权限映射
entry = mk_pte(page, vma->vm_page_prot);
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
// 5. 更新 RSS
update_mmu_cache(vma, vmf->address, vmf->pte);
}
return 0;
}
4.3 写时复制(COW)机制
COW 是 Linux 内核实现 fork() 系统调用的核心优化技术。父进程调用 fork() 创建子进程时,内核并不立即复制父进程的所有物理页,而是让父子进程共享相同的物理页,并将这些页的页表项标记为 只读。当其中任一进程尝试写入共享页时,触发写保护缺页,内核在缺页处理中完成物理页的复制。
4.3.1 COW 缺页处理流程
- 触发写保护缺页:进程写入一个标记为只读的共享页,CPU 触发缺页异常,缺页原因码包含
FAULT_FLAG_WRITE。 - 进入 do_wp_page():内核识别出这是写保护缺页,调用
do_wp_page()函数。 - 判断页是否可共享:检查页的引用计数(
page_count())。如果引用计数为 1,说明只有当前进程拥有该页,直接修改页表权限为可写即可(优化路径)。 - 复制物理页:如果引用计数大于 1,说明页被多个进程共享。内核分配一个新物理页,将原页内容复制到新页。
- 更新页表:将当前进程的页表项指向新分配的物理页,并设置写权限。
- 减少原页引用计数:原物理页的引用计数减 1,如果减到 0,则释放原页。
性能陷阱: COW 缺页虽然避免了 fork 时的全量复制,但在大量写操作场景下(如大型数组初始化),频繁的 COW 缺页会导致严重的性能下降。此时应考虑使用
vfork() 或 clone() 的 CLONE_VM 标志。
4.3.2 COW 与 KSM 的交互
KSM(Kernel Same-page Merging)与 COW 机制存在紧密的交互关系。KSM 扫描匿名页并合并内容相同的物理页,合并后的页被标记为写保护。当进程尝试写入 KSM 合并页时,同样触发 COW 缺页,内核会分配一个新页并将内容从合并页复制出来。
| 场景 | 触发条件 | 处理函数 | 性能影响 |
|---|---|---|---|
| fork 后首次写入 | 父子进程共享页,页表只读 | do_wp_page() | 分配+复制,延迟高 |
| KSM 合并后写入 | KSM 合并页被写保护 | do_wp_page() + KSM 解除 | 额外 KSM 解除开销 |
| 匿名页首次写缺页 | VMA 可写,但页表无映射 | do_anonymous_page() | 仅分配,无复制 |
| 只读匿名页缺页 | VMA 只读,页表无映射 | do_anonymous_page() | 映射零页,零开销 |
4.4 匿名页缺页性能调优要点
- 减少 fork 频率:使用线程(pthread)替代进程,避免 COW 缺页的累积开销。
- 预分配大页:对于大内存应用,使用
mmap(MAP_ANONYMOUS | MAP_HUGETLB)分配 2MB 或 1GB 大页,减少缺页次数。 - 调整 KSM 扫描参数:在虚拟机场景下,合理设置
/sys/kernel/mm/ksm/pages_to_scan和sleep_millisecs,平衡内存节省与 CPU 开销。 - 监控缺页统计:使用
perf stat -e page-faults,minor-faults,major-faults定位缺页热点。
4.5 匿名页缺页与 COW 流程图
4.6 实战:使用 ftrace 追踪匿名页缺页
以下命令可以实时追踪系统中的匿名页缺页事件,帮助定位性能瓶颈:
# 追踪所有缺页事件
echo 'do_anonymous_page' > /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
cat /sys/kernel/debug/tracing/trace_pipe
# 追踪 COW 缺页
echo 'do_wp_page' > /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
cat /sys/kernel/debug/tracing/trace_pipe
调优建议: 如果观察到大量
do_wp_page 调用,说明系统中存在频繁的 COW 操作。对于数据库、虚拟机等写密集型应用,建议使用 MAP_SHARED 或调整应用架构以减少 fork 调用。
4.7 本章小结
- 匿名页缺页分为读缺页(映射零页)和写缺页(分配新页)两种类型。
- COW 机制通过写保护页表项延迟物理页复制,是 fork 性能优化的基石。
- COW 缺页处理包含优化路径(引用计数为1时直接改权限)和复制路径(分配+拷贝)。
- KSM 合并页的写入同样触发 COW 缺页,带来额外的解除合并开销。
- 通过 ftrace 和 perf 工具可以精确追踪缺页事件,指导性能调优。