背景
MGLRU 回收一个 lruvec 时,会先把对应的地址空间 pin 住,再去遍历它的页范围。pin 住地址空间用的是 mmget/mmput,也就是抬升 mm->mm_users。
┌─────────────────────────────────┐
│ MGLRU 回收时 pin 住整个地址空间 │
└────────────────┬────────────────┘
▼
┌─────────────────────────────────┐
│ 实际只需稳定的 mm 结构来遍历 │ 多抬了一层 mm_users 引用
└────────────────┬────────────────┘
▼
┌─────────────────────────────────┐ oom-reaper 与 process_mrelease 因引用抬高而
│ 垂死进程内存释放被拖延 │ 跳过
└─────────────────────────────────┘
可回收实际只需要一个稳定的 mm_struct:能拿/放 mmap_read_lock、有一个稳定的 mm->mm_mt 树可遍历。抬升 mm_users 这一层是多余的。
问题
- MGLRU 回收用 mmget 抬升 mm_users,过度 pin 地址空间
- 实际只需稳定 mm_struct 来遍历
- 过度 pin 致垂死进程内存释放延迟
- oom-reaper 与 process_mrelease 因 mm_users 升高误判而跳过
方案
把 mmget/mmput 换成 mmgrab/mmdrop:只 pin mm_struct(抬升 mm_count),不再 pin 地址空间。
旧:pin 地址空间,抬地址空间引用
┌───────────────────────────────┐
│ 回收抬升地址空间引用 │
└───────────────┬───────────────┘
▼
┌───────────────────────────────┐
│ reaper 判定进程未将释放,跳过 │
└───────────────────────────────┘
新:只 pin mm 结构,抬结构引用
┌──────────────────────────────────────┐
│ 回收只稳定 mm 结构,不抬地址空间引用 │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ reaper 正常判定并回收垂死进程 │ 遍历期间持读锁保证页表树稳定
└──────────────────────────────────────┘
mm_mt 就在 mm_struct 内部,mm_struct 稳定它就不会被释放;遍历期间又持有 mmap_read_lock,mm_mt 不会中途变化。所以只 pin mm_struct 足够保证遍历安全,无需把整个地址空间也 pin 住。
一旦不再抬升 mm_users,task_will_free_mem 就能对垂死进程给出正确判定,oom-reaper 与 process_mrelease 也能在 MGLRU 扫描期间照常工作,及时回收垂死进程的内存。
收益
作者未提供量化 benchmark。从机制与真实场景推断的预期收益:
- 垂死进程的内存不再被 MGLRU 回收的过度 pin 拖延,能及时释放
- oom-reaper 与 process_mrelease 在 MGLRU 扫描期间恢复正常工作,被杀进程的内存能被快速 reaped
- Android 上观察到的 process_mrelease 跳过 MM、进程内存数秒不释放的问题得到消除
作者在 Android 生产环境观察到的可见症状是 process_mrelease() 因 mm_users 抬高而跳过被杀进程的 MM,导致进程内存异常长时间(数秒)不释放。