前文回顾: 在上一章,我们深入源码剖析了 oom_badness 打分算法,看到内核如何从数百个进程中精准选出那个倒霉的 victim。但仅仅 kill() 该进程,内存并没有真正释放——进程的页表还在,驻留页还在,内核线程仍在持有对该进程的引用。如果等待进程自己 exit_mmap(),可能因进程处于不可中断睡眠而无限拖延,导致 OOM killer 本身也陷入饥饿。OOM reaper 就是内核为此发明的异步解决方案。本章我们将基于源码,逐行揭示这个"清道夫"的工作方式。
一、核心问题:为什么不能等进程自己释放内存?
OOM killer 的工作顺序是:
1. out_of_memory() 选 victim
2. oom_kill_process(victim) 发 SIGKILL
3. 进程被杀,但最终释放内存要等到 exit_mmap() 和 mmput()
问题出在 step 3。一个典型的 OOM victim 进程可能:
正在
__alloc_pages()睡眠(等待内存),exit()后进入不可中断睡眠正在执行磁盘 I/O 等待
被某个内核函数持有引用,无法完成
mmput()
如果必须等待进程完全退出才能回收内存,OOM killer 选中的 victim 可能长时间占用大量物理页,导致整个系统无法恢复。OOM reaper 的诞生就是为了解决这个"中间状态"。

二、整体架构:一个线程,一个列表,一个等待队列
内核通过以下数据结构实现了 OOM reaper:
static struct task_struct *oom_reaper_th; // reaper 线程
static DECLARE_WAIT_QUEUE_HEAD(oom_reaper_wait); // reaper 的等待队列
static struct task_struct *oom_reaper_list; // 待处理进程链表
static DEFINE_SPINLOCK(oom_reaper_lock); // 链表自旋锁
static atomic_t oom_victims = ATOMIC_INIT(0); // 正在处理中的 victims 数
static DECLARE_WAIT_QUEUE_HEAD(oom_victims_wait); // 等待所有 victims 处理的队列
关键设计:
oom_reaper_th:一个独立的内核线程,专门执行 OOM 清理工作oom_reaper_list:通过tsk->oom_reaper_list字段链成链表(复用已有字段)oom_reaper_wait:reaper 线程在此等待有新 victim 时醒来oom_victims/oom_victims_wait:用于oom_killer_disable()等场景,等待所有 victim 处理完

三、Victim 标记与唤醒:mark_oom_victim → wake_oom_reaper
3.1 Victim 标记:mark_oom_victim(tsk)
这个函数在 oom_kill_process() 中调用,完成三件事:
static void mark_oom_victim(struct task_struct *tsk)
{
struct mm_struct *mm = tsk->mm;
WARN_ON(oom_killer_disabled);
/* 双重保险,防止重复标记 */
if (test_and_set_tsk_thread_flag(tsk, TIF_MEMDIE))
return;
/* oom_mm 绑定到 signal_struct 生命周期,用 cmpxchg 原子写入 */
if (!cmpxchg(&tsk->signal->oom_mm, NULL, mm)) {
mmgrab(tsk->signal->oom_mm);
set_bit(MMF_OOM_VICTIM, &mm->flags);
}
__thaw_task(tsk); /* 从冻结状态唤醒,防止 livelock */
atomic_inc(&oom_victims); /* 全局计数 */
trace_mark_victim(tsk->pid);
}
逐行解读:
TIF_MEMDIE标志:这是一个线程标志,标记该线程正处于 OOM victim 的生命周期中。test_and_set是原子操作,防止竞态。tsk->signal->oom_mm:这是核心!将 victim 的mm_struct绑定到 signal 结构上,oom_mm的 lifetime 与 signal 结构一致,不随单个线程退出而消失。cmpxchg确保只写入一次。MMF_OOM_VICTIM:在mm的 flags 中设置标记,mm_is_oom_victim(mm)会检查该位。这个标记用于exit_mmap中识别是否需要特殊处理。__thaw_task(tsk):如果 victim 当时被冻结(freezer cgroup 等),立即解冻。否则 OOM killer 无法释放内存,进程会一直冻结导致 livelock。oom_victims:全局原子计数器,oom_killer_disable()会等待该计数器归零。
3.2 唤醒 Reaper:wake_oom_reaper(tsk)
标记完成后,下一步是告诉 reaper 线程来清理内存:
static void wake_oom_reaper(struct task_struct *tsk)
{
/* mm 已在列表中?防止重复入队 */
if (test_and_set_bit(MMF_OOM_REAP_QUEUED, &tsk->signal->oom_mm->flags))
return;
get_task_struct(tsk); /* 增加引用计数 */
spin_lock(&oom_reaper_lock);
tsk->oom_reaper_list = oom_reaper_list; /* 头插法链入 */
oom_reaper_list = tsk;
spin_unlock(&oom_reaper_lock);
trace_wake_reaper(tsk->pid);
wake_up(&oom_reaper_wait); /* 唤醒 reaper 线程 */
}
关键设计点:
MMF_OOM_REAP_QUEUED标志:防止同一个 mm 被重复入队。只有第一次设置该标志的线程能成功入队。oom_reaper_list头插法:将新 victim 插入链表头部,O(1) 复杂度。wake_up:唤醒在oom_reaper_wait上睡眠的 reaper 线程。
四、Reaper 线程:无限循环,逐个处理
Reaper 线程由 oom_init() 创建(CONFIG_MMU 下):
static int oom_reaper(void *unused)
{
while (true) {
struct task_struct *tsk = NULL;
/* 等待有待处理的 victim */
wait_event_freezable(oom_reaper_wait, oom_reaper_list != NULL);
spin_lock(&oom_reaper_lock);
if (oom_reaper_list != NULL) {
tsk = oom_reaper_list;
oom_reaper_list = tsk->oom_reaper_list;
}
spin_unlock(&oom_reaper_lock);
if (tsk)
oom_reap_task(tsk);
}
return 0;
}
线程循环:
wait_event_freezable:如果没有待处理的 victim,线程进入可冻结的睡眠。freezable让 freezer 可以挂起线程,不影响系统休眠。oom_reaper_list头摘法:从链表头部取出一个 victim,O(1)。oom_reap_task(tsk):真正的清理工作。
五、核心清理逻辑:oom_reap_task + oom_reap_task_mm
5.1 重试机制:最多 10 次
#define MAX_OOM_REAP_RETRIES 10
static void oom_reap_task(struct task_struct *tsk)
{
int attempts = 0;
struct mm_struct *mm = tsk->signal->oom_mm;
/* 重试获取 mmap_lock 最多 10 次 */
while (attempts++ < MAX_OOM_REAP_RETRIES && !oom_reap_task_mm(tsk, mm))
schedule_timeout_idle(HZ/10); /* 休眠 100ms 再重试 */
if (attempts <= MAX_OOM_REAP_RETRIES ||
test_bit(MMF_OOM_SKIP, &mm->flags))
goto done;
/* 10 次重试均失败,print 调试信息 */
pr_info("oom_reaper: unable to reap pid:%d (%s)\n",
task_pid_nr(tsk), tsk->comm);
sched_show_task(tsk);
debug_show_all_locks();
done:
tsk->oom_reaper_list = NULL;
set_bit(MMF_OOM_SKIP, &mm->flags);
put_task_struct(tsk);
}
为什么需要重试?oom_reap_task_mm 需要用 mmap_read_trylock(mm) 获取 mmap 读锁。如果此时进程仍在退出中,mmap_write_lock 可能正被持有(exit_mmap 路径),reaper 尝试会失败。重试 10 次、每次休眠 100ms,给了退出路径释放写锁的时间。总重试窗口约 1 秒。
5.2 解除映射主函数:oom_reap_task_mm
static bool oom_reap_task_mm(struct task_struct *tsk, struct mm_struct *mm)
{
bool ret = true;
/* 尝试获取 mmap 读锁;失败则直接返回 false */
if (!mmap_read_trylock(mm)) {
trace_skip_task_reaping(tsk->pid);
return false;
}
/* MMF_OOM_SKIP 检查:exit_mmap 已接管 */
if (test_bit(MMF_OOM_SKIP, &mm->flags)) {
trace_skip_task_reaping(tsk->pid);
goto out_unlock;
}
trace_start_task_reaping(tsk->pid);
/* 核心解除映射操作 */
ret = __oom_reap_task_mm(mm);
if (!ret)
goto out_finish;
pr_info("oom_reaper: reaped process %d (%s), now anon-rss:%lukB, file-rss:%lukB, shmem-rss:%lukB\n",
task_pid_nr(tsk), tsk->comm,
K(get_mm_counter(mm, MM_ANONPAGES)),
K(get_mm_counter(mm, MM_FILEPAGES)),
K(get_mm_counter(mm, MM_SHMEMPAGES)));
out_finish:
trace_finish_task_reaping(tsk->pid);
out_unlock:
mmap_read_unlock(mm);
return ret;
}
执行路径:
mmap_read_trylock(mm):非阻塞尝试获取 mmap 读锁。这是为了避免与exit_mmap的写锁冲突。MMF_OOM_SKIP检查:如果进程退出路径已设置了该位(exit_mmap接管),直接跳过,防止 reaper 和 exit 路径竞争。__oom_reap_task_mm(mm):核心解除映射操作。记录日志:解除后输出当前的 RSS 值,便于分析效果。

六、解除映射的底层魔法:__oom_reap_task_mm
这是最核心的函数,逐页解除进程的匿名页映射,释放物理内存:
bool __oom_reap_task_mm(struct mm_struct *mm)
{
struct vm_area_struct *vma;
bool ret = true;
/* 设置 MMF_UNSTABLE 标志 */
set_bit(MMF_UNSTABLE, &mm->flags);
for (vma = mm->mmap ; vma; vma = vma->vm_next) {
/* 跳过不可 LRU 管理的 VMA */
if (!can_madv_lru_vma(vma))
continue;
/* 只处理匿名页或非共享文件页 */
if (vma_is_anonymous(vma) || !(vma->vm_flags & VM_SHARED)) {
struct mmu_notifier_range range;
struct mmu_gather tlb;
/* 初始化 TLB 收集器 */
mmu_notifier_range_init(&range, MMU_NOTIFY_UNMAP, 0,
vma, mm, vma->vm_start, vma->vm_end);
tlb_gather_mmu(&tlb, mm);
/* 非阻塞方式发起 MMU 无效通知 */
if (mmu_notifier_invalidate_range_start_nonblock(&range)) {
tlb_finish_mmu(&tlb);
ret = false; /* 通知阻塞了,需重试 */
continue;
}
/* 解除指定范围内的页面映射 */
unmap_page_range(&tlb, vma, range.start, range.end, NULL);
mmu_notifier_invalidate_range_end(&range);
tlb_finish_mmu(&tlb);
}
}
return ret;
}
6.1 第一道过滤:can_madv_lru_vma
static inline bool can_madv_lru_vma(struct vm_area_struct *vma)
{
return !(vma->vm_flags & (VM_LOCKED | VM_HUGETLB | VM_PFNMAP));
}
跳过以下 VMA:
VM_LOCKED:mlock()锁定的页,不能被释放VM_HUGETLB:大页映射,需要特殊管理VM_PFNMAP:直接映射物理内存(如 GPU 共享内存),不能随意解除
6.2 只处理匿名页和非共享文件页
if (vma_is_anonymous(vma) || !(vma->vm_flags & VM_SHARED))
注释中的设计理由(源码原文):
"Only anonymous pages have a good chance to be dropped without additional steps... We do not even care about fs backed pages because all which are reclaimable have already been reclaimed"
解读:
匿名页:没有 backing store,直接映射到物理内存。解除映射后物理页可以立即回收。
非共享文件页:虽然是文件页,但如果是私有映射(非
VM_SHARED),解除映射后可以触发文件页的脏页回写,同样释放物理页。共享文件页:不处理。文件页如果可回收,在之前的 shrink_page_list 阶段已经回收完了。
6.3 解除映射的具体操作
mmu_notifier_range_init:初始化无效化通知范围,标记为 MMU_NOTIFY_UNMAP。
tlb_gather_mmu(&tlb, mm):创建 TLB 收集器,开始收集需要刷新的 TLB 条目。
mmu_notifier_invalidate_range_start_nonblock(&range):通知 MMU notifier 即将解映射,非阻塞模式。如果 notifier 需要阻塞操作,直接返回错误,reaper 稍后重试。
unmap_page_range(&tlb, vma, range.start, range.end, NULL):核心解映射操作! 遍历页表结构,将指定范围的 PTE 置为无效:
void unmap_page_range(struct mmu_gather *tlb,
struct vm_area_struct *vma,
unsigned long addr, unsigned long end,
struct zap_details *details)
{
pgd_t *pgd;
unsigned long next;
BUG_ON(addr >= end);
tlb_start_vma(tlb, vma);
pgd = pgd_offset(vma->vm_mm, addr);
do {
next = pgd_addr_end(addr, end);
if (pgd_none_or_clear_bad(pgd))
continue;
next = zap_p4d_range(tlb, vma, pgd, addr, next, details);
} while (pgd++, addr = next, addr != end);
tlb_end_vma(tlb, vma);
}
遍历 PGD → P4D → PUD → PMD → PTE 四级页表,将 PTE 清零,物理页返回到 buddy allocator。
tlb_finish_mmu(&tlb):完成 TLB 刷新,调用 __tlb_finish_mmu 将收集的页面加入 per-CPU 缓存,最终刷入 TLB。
返回值:如果 unmap_page_range 成功完成所有匿名/非共享页解除映射,返回 true;如果有页面因 mmu notifier 阻塞而跳过了,返回 false,需要下次重试。

七、exit_mmap 中的 OOM Victim 特殊处理
当 OOM victim 进程自己走到 exit_mmap 时,内核需要确保 reaper 不会重复清理:
void exit_mmap(struct mm_struct *mm)
{
/* ... */
if (unlikely(mm_is_oom_victim(mm))) {
/* 手动解除映射,释放尽可能多的内存 */
(void)__oom_reap_task_mm(mm);
/* 设置 MMF_OOM_SKIP,防止 reaper 再次处理 */
set_bit(MMF_OOM_SKIP, &mm->flags);
/* 拿取写锁,确保 reaper 不再运行 */
mmap_write_lock(mm);
mmap_write_unlock(mm);
}
/* ... 继续正常清理 ... */
}
关键逻辑:
(void)__oom_reap_task_mm(mm):既然进程自己要死了,不如自己清理,释放全部内存。MMF_OOM_SKIP:立即设置,防止后续 reaper 线程拿到该 mm 后重复操作。写锁:先拿写锁再释放,确保任何 reaper 线程的
mmap_read_trylock都会失败(因为写锁独占)。
注意: exit_mmap 中的 __oom_reap_task_mm 不会阻塞,因为此时 mmu_notifier 已经被 mmu_notifier_release(mm) 清空了。
八、Reaper 退出与统计
当 reaper 处理完一个 victim 后:
done:
tsk->oom_reaper_list = NULL;
set_bit(MMF_OOM_SKIP, &mm->flags);
put_task_struct(tsk); /* 释放 wake_oom_reaper 加的引用 */
清除
oom_reaper_list字段,防止链表污染。设置
MMF_OOM_SKIP,彻底将 mm 从 OOM killer 视图中隐藏。put_task_struct(tsk):释放wake_oom_reaper中get_task_struct的引用,确保 task_struct 可以安全释放。
九、总结:一个精妙的异步协作机制
OOM reaper 的设计哲学:
分离关注点:OOM killer 只做"谁该死"的决策,reaper 负责"清理尸体"。解耦让两个组件可以独立优化。
非阻塞重试:
mmap_read_trylock+ 最多 10 次重试,避免了死锁,同时给了正常退出路径释放锁的时间窗口。只处理"容易释放的":匿名页和非共享文件页,因为已经无法通过 shrink 回收,而且不会阻塞在其他锁上。
与退出路径协同:通过
MMF_OOM_SKIP和MMF_OOM_VICTIM标志,防止 reaper 和exit_mmap重复操作。不等待进程退出:即使进程仍在运行(如被 SIGKILL 杀死但未退出),reaper 也能独立地解除其地址空间,立即释放物理内存。
整个流程可以总结为:mark_oom_victim → wake_oom_reaper(入队)→ oom_reaper 线程唤醒 → oom_reap_task(重试 10 次)→ oom_reap_task_mm(拿读锁)→ __oom_reap_task_mm(逐页解除映射匿名页)→ put_task_struct(释放)。
下一章我们将完成最后的拼图:OOM Kill + panic_on_oom 机制,探讨内核在 OOM 时如何优雅地杀死进程,以及在极端情况下的崩溃策略。