[内核学习]缺页异常

/ 0评 / 0赞

1.3 篇从异常处理的角度看过缺页:数据 abort,EC=0x24/0x25,一路分发.这篇站在内存管理这边,把缺页当作"虚拟内存的发动机"来讲:没有它,按需分配,COW,文件映射全是空谈.实验把各种缺页场景的计数精确到个位数,还抓到两个"和书上说的不一样"的现场.

这篇回答几件事:

先立框架.缺页 = 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 的实际读强相关,不是内核随手标的.

最后留几个容易忽略的细节:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注