背景

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_lockmm_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,导致进程内存异常长时间(数秒)不释放。