1.3 篇从异常处理的角度看过缺页:数据 abort,EC=0x24/0x25,一路分发.这篇站在内存管理这边,把缺页当作"虚拟内存的发动机"来讲:没有它,按需分配,COW,文件映射全是空谈.实验把各种缺页场景的计数精确到个位数,还抓到两个"和书上说的不一样"的现场.
这篇回答几件事:
- 什么操作触发缺页,什么操作不触发,计数的口径是什么
- 匿名页,写保护,文件页三种缺页各干什么
- minor 和 major 的分界线在哪,怎么造出一个 major
- 零页这个"全系统共享的作弊页"怎么验证
先立框架.缺页 = MMU 查页表失败或权限不符,硬件把地址和原因塞进 FAR/ESR,内核接管.1.4 篇看过"pte 全零"的样子:页还没来.内核按 VMA 合同分流:mmap 登记过的匿名区间,给一页(或零页);文件区间,从 page cache 取或发起 IO;VMA 说只读而你写了,信号;VMA 压根没这段地址,也是信号 — 前三个是正常的发动机,后两个是法律.
计数在 /proc/self/stat 的第 10 和 12 字段,minor 和 major.实测各场景,数字直接说话:
写新页: min+2 (1 真缺页 + 1 测量噪声, 统计函数自己也要内存)
再读同页: min+0 (映射在手, 什么都不发生)
mprotect往返后写: min+0
fork后写共享页: min+2 (COW 写保护缺页)
第二页首写: min+1
第一行先自首:我的计数函数每调用一次 fopen,自己就贡献一次 minor — 个位数级的实验要把测量开销算进去,这行 min+2 里有 1 次是尺子的重量.其余各行干净:同页再读零成本;新页一触一缺.
第三行是第一个"书本 vs 实测".想当然的剧本:mprotect 改成只读再改回可写,写它,应该触发写保护缺页.实测零缺页.原因想通了一钱不值:mprotect 自己就会同步更新页表,权限往返之后 PTE 已经是可写的,写它畅通无阻 — 缺页只在"硬件发现实际权限和预期不符"时发生,内核主动改表不算.真正会触发写保护缺页的是第五行:fork 之后写共享页,PTE 被 COW 改成只读,而 VMA 承诺可写,一写就缺,内核当场发私页副本 — 5.3 篇数过它,一次一页.
零页是这趟最好玩的发现.只读的匿名页,fault 进来的不是一张新页,而是全系统共享的一张全零物理页:
只读匿名页: pfn=0x3115, fork 前后同帧
fork 出一个孩子再看,还是 0x3115 — 因为谁也没写它,大家共享同一张零页.mmap 一个 GB 的匿名内存挨个读,物理内存一分不花,直到某处写了才 COW 出私页.这个设计让"读未初始化的 BSS"几乎免费,而它依赖的正是写保护缺页机制 — 两件事是一枚硬币.
然后是 major 的猎捕,连续翻了两次车,值得完整记录.目标:drop_caches 之后首读一个文件,应该 major.第一次实验 maj 恒 0,排查发现文件放在 /tmp — tmpfs 本来就住在内存里,根本没有"冷"这回事;第二次换到 /root 的真实 eMMC,还是 0,再查,drop 之后我用 cat 预热过它.两坑排掉,终于稳定复现:
热: mmap touch: min+2 maj+0 (cache 命中, 纯 minor)
冷: mmap touch: min+1 maj+1 (真去 eMMC 读 = major)
同一行代码,冷热两读,maj 从 0 到 1,这就是 minor/major 的分界:页是否已在物理内存.顺手还澄清一个口径:普通 read() 冷读 maj 记 0 — read 是系统调用不是缺页,它等 IO 的时间记在别处,拿 maj_flt 衡量"read 慢"是错的工具.性能分析里 maj_flt 只对 mmap 型访问有意义,数据库用 mmap 读文件的场景才会看到它爬升.
内核侧路径收个尾.arm64 的 do_mem_abort 按 ISS 查表(1.3 篇那张 fault_info)进 do_page_fault,权限合法性查完交 handle_mm_fault,它按缺页类型分流:do_anonymous_pages 管匿名(零页或新页),do_wp_page 管写保护(COW 的大本营),do_read_fault 管文件页.major 的计数点埋在 filemap_fault 的 IO 等待路径上 — 所以前面的实验里它和 eMMC 的实际读强相关,不是内核随手标的.
最后留几个容易忽略的细节:
- 缺页处理完返回后,硬件重新执行"肇事指令",所以一次写引发的 COW 缺页对程序完全透明,性能上只贵一次陷核加分配.
- min_flt 在启动期暴涨是正常的(装载 ELF,初始化 libc),运行期持续增长的斜率才是值得看的信号.
- do_user_fault 里有一个 fault_around 优化:文件缺页一次预填周围若干页,减少同一区域连环缺页,对顺序访问友好,对随机访问略浪费.
- KSM 能把不同进程内容相同的匿名页合并成一张,是零页思路的推广版,容器密度场景的大杀器.
- major fault 风暴的系统症状是 CPU 不忙但一切变慢 — iowait 和它一起看,方向就是存储.
- userfaultfd 允许用户态接管缺页处理, live migration 和数据库 buffer pool 全靠它,以后讲调试工具时值得单开小灶.
- 泄漏的映射 (mmap 后 munmap 前进程死了) 由 exit_mm 兜底全回收,缺页机制不产生内存泄漏,泄漏的永远是"分配了不再用"的那部分.