缺页是 VM 按需分配的引擎:进程访问 VA → PTE 不 present / 权限不符 → CPU 发 #PF 异常 → 内核进 fault handler → 分配/修复 → 进程无感继续。
一、先看图
flowchart TD
ACC[用户访问 VA] --> PF[#PF 异常]
PF --> DPF[do_page_fault]
DPF --> VMA{找到 VMA?}
VMA -->|否| SEGV[SIGSEGV]
VMA -->|是| PERM{权限匹配?}
PERM -->|写只读非 COW| SEGV
PERM -->|OK| HMM[handle_mm_fault]
HMM --> WALK[walk 页表]
WALK --> PTE{PTE 状态}
PTE -->|none: 首次访问| ALLOC[alloc page<br/>minor fault]
PTE -->|file: 需读盘| FILEIO[read page<br/>major fault]
PTE -->|swap entry| SWAPIN[swap in<br/>major fault]
PTE -->|present + RO + COW| COWBRK[COW break<br/>minor fault]
PTE -->|uffd 注册| UFFD[userfaultfd<br/>通知用户态]
classDef ok fill:#3fb95022,stroke:#3fb950,color:#adbac7;
classDef err fill:#f8514922,stroke:#f85149,color:#adbac7;
classDef io fill:#f0883e22,stroke:#f0883e,color:#adbac7;
class ALLOC,COWBRK ok
class SEGV err
class FILEIO,SWAPIN,UFFD io
二、五种缺页
2.1 Minor fault(软缺页)
PTE 为空但物理页可在内存里立刻分配/找到。不涉及磁盘 I/O。
场景:匿名页首次写、page cache 已驻留的文件页首次映射。
2.2 Major fault(硬缺页)
需要磁盘 I/O:文件页不在 cache 要读盘,或匿名页已 swap-out 要 swap-in。
开销可达 ms 级(HDD 10+ms,SSD 0.1ms)。
2.3 COW fault
fork 后父子共享页标只读。任一方写 → #PF → 检测 COW → 分配新页、复制内容、改 PTE → 写方拿私有。
2.4 Swap fault
PTE 里存的是 swap entry(设备+偏移)而非物理地址。先查 swap cache,miss 就从 swap 读回。
2.5 Userfaultfd
用户态注册了 userfaultfd →
缺页不由内核处理,而是通知用户态程序填充页。用于
CRIU(热迁移)、live migration、postcopy。
三、do_page_fault 调用链
CPU #PF → exc_page_fault()
→ do_user_addr_fault() / do_kern_addr_fault()
→ find_vma() // 定位 VMA
→ check_access() // 权限检查
→ handle_mm_fault() // 核心入口
→ __handle_mm_fault()
→ walk 到 PTE 层
→ handle_pte_fault()
→ do_anonymous_page() // anon 首次
→ do_fault() // file-backed
→ do_swap_page() // swap
→ do_wp_page() // COW write-protect
四、COW 详解
COW break 关键路径:
do_wp_page检测page_count(page) > 1(多进程共享)- 分配新页
alloc_page(GFP_HIGHUSER_MOVABLE) copy_user_highpage(new, old, addr, vma)- 更新 PTE:新页 + 可写
page_remove_rmap(old)→ 老页引用减一- flush TLB
fork+exec 快的秘密:exec 会 exit_mmap
丢弃所有 VMA,绝大部分页根本不会被 COW。
五、file-backed fault
文件映射缺页走 vm_ops->fault:
ext4_file_fault → filemap_fault
→ find_get_page(mapping, offset) // 在 page cache 查
→ 未命中 → page_cache_sync_readahead → submit_bio
→ 等 I/O 完成 → 设 PTE
readahead 机制预读相邻页,减少后续 major fault。
六、hugefault
THP 或 HugeTLB 缺页:
- THP:
do_huge_pmd_anonymous_page或__do_huge_pmd_wp_page(COW) - HugeTLB:
hugetlb_fault→hugetlb_no_page→ 从 HugeTLB pool 取页
THP 的 COW 要复制 2MB,延迟更高;所以数据库倾向 madvise 模式(仅明确标记的区域用 THP)。
七、userfaultfd
int uffd = syscall(SYS_userfaultfd, O_CLOEXEC | O_NONBLOCK);
struct uffdio_api api = {.api = UFFD_API};
ioctl(uffd, UFFDIO_API, &api);
struct uffdio_register reg = {
.range = {.start=addr, .len=len},
.mode = UFFDIO_REGISTER_MODE_MISSING,
};
ioctl(uffd, UFFDIO_REGISTER, ®);
// 用户态 poll(uffd) → 收到 UFFD_EVENT_PAGEFAULT
// → UFFDIO_COPY 或 UFFDIO_ZEROPAGE 填充应用场景:
- CRIU:检查点恢复时 lazy page-in
- QEMU postcopy:虚拟机迁移后按需拉页
- 垃圾回收:Shenandoah/ZGC 用 userfaultfd 做并发压缩(后改用 multi-mapping)
八、fault 计数与观测
# 进程级
ps -o min_flt,maj_flt -p $$
# 系统级
sar -B 1
# pgfault/s, pgmajfault/s
# 实时追踪
perf stat -e page-faults,major-faults ./app
# 精细追踪
bpftrace -e 'tracepoint:exceptions:page_fault_user { @[comm]=count(); }'major fault 高意味着要么 page cache 不够,要么 swap
活跃——查 /proc/meminfo。
九、缺页优化
9.1 MAP_POPULATE
提前把所有页分配好,避免运行时 fault 抖动:
mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_POPULATE, -1, 0);9.2 madvise(MADV_WILLNEED)
文件映射:提前 readahead。比 MAP_POPULATE 更灵活(可按区域)。
9.3 mlock
锁定后不会被换出,就不会有 swap fault:
mlock(addr, size);9.4 huge page
减少 fault 次数(一次 fault 填 2MB 而非 4KB)。
9.5 prefault
内核 mm_populate 在 mmap 返回前批量
fault,用于 MAP_POPULATE。
十、SIGSEGV 路径
VMA 找不到、或权限检查失败、或 stack guard 越界 →
SIGSEGV。
常见场景:
- 空指针解引用(地址 0 附近,低地址 guard)
- 栈溢出(碰到 stack guard VMA)
- JIT 代码执行了 NX 区域
- 写只读映射(不是 COW 场景)
core dump 文件会记录 fault 地址和 si_code(SEGV_MAPERR / SEGV_ACCERR)。
十一、小结
- 缺页是 VM 的运行引擎——按需分配、COW、swap 全靠它
- minor 快(μs)、major 慢(ms)——major 高就查内存压力
- COW 让 fork+exec 以 O(VMA 数量) 而非 O(工作集) 运行
- userfaultfd 把缺页处理推到用户态,是 live migration 核心机制
参考文献
- Bovet & Cesati, “Understanding the Linux Kernel” 3rd §9 “Process Address Space”
mm/memory.c、mm/hugetlb.c- Corbet, J. “Userfaultfd.” LWN.net 2015
- Andrea Arcangeli, “userfaultfd for postcopy live migration.” KVM Forum 2015
Documentation/admin-guide/mm/userfaultfd.rst
工具
ps -o min_flt,maj_fltsar -B、vmstatperf stat -e page-faultsbpftracepage_fault tracepoint
上一篇:mm_struct 与 VMA 下一篇:页缓存深入
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】虚拟内存模型
虚拟内存是现代 OS 最核心的抽象:每个进程都像独占一块连续大内存。本文讲 VM 到底给了什么、代价是什么——VA/PA 映射、保护、隔离、COW、mmap 语义、overcommit、地址空间布局。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。
【操作系统百科】交换
swap 还值得开吗?本文讲 swap area 基础、swap cache、zram 压缩内存、zswap 前端压缩池、swappiness 的真实含义、容器里的 swap 策略,以及为什么现代 Android 全靠 zram 不靠磁盘。
【操作系统百科】Slab/SLUB 分配器
buddy 只管页粒度(4K+),内核大多数对象只有几十到几百字节。slab/SLUB 在 buddy 之上做对象级缓存。本文讲 slab 历史、SLUB 接手、SLOB 退场、kmem_cache、per-CPU cache、KASAN 集成。