背景
mincore(2) 查询调用进程一段地址范围内每个页是否驻留内存:内核按页把结果写进用户传入的向量,页在内存中则对应字节最低位置 1。应用程序借此判断哪些页已被换出或尚未加载,比如数据库用它来确认缓存页是否还在内存。
内核的实现是找到起始地址所在的 VMA,然后走这个 VMA 的页表,按 PTE 的 present 位与 page cache 状态逐页判断。走页表期间 VMA 与页表都必须稳定,传统做法是持有 mmap_lock 读锁。mmap_lock 是进程地址空间的全局读写锁:所有线程的缺页与各种查询共用它的读锁,mmap、munmap、mprotect 这类修改操作则要拿写锁。读写信号量讲究公平,一旦有写者排队,后来的读者也得跟着排队。
此外 mincore 的临时结果缓冲区只有一页大小,每轮迭代最多处理 PAGE_SIZE 个页。查询一大片区域时,循环每轮都要重新申请与释放 mmap 读锁。
mincore 原有执行流程
┌──────────────────────────────────────┐
│ 每轮迭代先申请 mmap_lock 读锁,锁定 │
│ 整个地址空间 │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ 锁内查找目标 VMA,走它的页表逐页判断 │
│ 驻留 │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ 结果拷给用户后放锁,进入下一轮迭代 │
└──────────────────────────────────────┘
问题
- mincore 每轮迭代都申请进程全局的 mmap_lock 读锁
- mmap_lock 有写者排队时,mincore 只读查询也被迫等待
- do_mincore() 只访问单个 VMA,锁范围远超实际需要
方案
per-VMA 锁给每个 VMA 各配一把锁,不持有 mmap_lock 也能锁定单个 VMA:读者加锁即原子增加 VMA 的引用计数,写者修改 VMA 前先挡住新读者并等已有读者退出,读锁持有期间 VMA 保证不会被修改或释放。缺页处理与 /proc 的多个接口早已用它替代 mmap_lock 读锁,mincore 这次跟进。mincore 是只读查询,天然适合这把锁。
锁定范围对比
┌──────────────────────────────────────┐ ┌──────────────────────────────┐
│ mmap_lock 读锁,一次锁住进程全部 VMA ├───│ per-VMA 读锁,只锁住目标 VMA │
└──────────────────────────────────────┘ └──────────────────────────────┘
改动围绕两点。其一,页表遍历声明的锁模式从 PGWALK_RDLOCK 换成 PGWALK_VMA_RDLOCK_VERIFY:前者要求调用者持有 mmap 读锁,后者告诉遍历框架调用者已持有目标 VMA 的读锁,遍历时校验即可,不再依赖 mmap_lock。其二,系统调用循环不再用 mmap_read_lock() 包住 do_mincore(),改用新接口 vma_start_read_unlocked() 按地址查找 VMA 并直接加好 per-VMA 读锁,处理完用 vma_end_read() 释放。查找与加锁一体完成,不留查到之后 VMA 被并发修改的窗口。地址没有 VMA 覆盖时它返回 NULL,mincore 照旧返回 -ENOMEM,错误语义不变。do_mincore() 相应改为直接接收已锁定的 VMA,不再自己查找。
改用 per-VMA 读锁后的执行流程
┌─────────────────────────────────────┐
│ 按地址找到目标 VMA,只给它加读锁 │
└──────────────────┬──────────────────┘
▼
┌─────────────────────────────────────┐
│ 走这个 VMA 的页表,遍历框架校验 VMA │
│ 已锁定 │
└──────────────────┬──────────────────┘
▼
┌─────────────────────────────────────┐
│ 释放该 VMA 的读锁,进入下一轮迭代 │
└─────────────────────────────────────┘
收益
作者未提供性能数据,从代码逻辑推断的预期收益:
- mincore 不再触碰进程全局 mmap_lock,与缺页、munmap 等操作不再在同一把锁上竞争
- mmap_lock 有写者排队时,mincore 不再因读写信号量的公平性规则跟着等待
- 只读查询的锁定范围从整个地址空间缩小到目标 VMA