前文回顾: 在上一章,我们深入源码剖析了 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 的诞生就是为了解决这个"中间状态"。

2026/08/halo_jz10gax.webp

二、整体架构:一个线程,一个列表,一个等待队列

内核通过以下数据结构实现了 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 处理完

2026/08/halo_wmk6kub.webp


三、Victim 标记与唤醒:mark_oom_victimwake_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;
}

线程循环:

  1. wait_event_freezable:如果没有待处理的 victim,线程进入可冻结的睡眠。freezable 让 freezer 可以挂起线程,不影响系统休眠。

  2. oom_reaper_list 头摘法:从链表头部取出一个 victim,O(1)。

  3. 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;
}

执行路径:

  1. mmap_read_trylock(mm):非阻塞尝试获取 mmap 读锁。这是为了避免与 exit_mmap 的写锁冲突。

  2. MMF_OOM_SKIP 检查:如果进程退出路径已设置了该位(exit_mmap 接管),直接跳过,防止 reaper 和 exit 路径竞争。

  3. __oom_reap_task_mm(mm):核心解除映射操作。

  4. 记录日志:解除后输出当前的 RSS 值,便于分析效果。

2026/08/halo_g3ku2mu.webp

六、解除映射的底层魔法:__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_LOCKEDmlock() 锁定的页,不能被释放

  • 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,需要下次重试。

2026/08/halo_ghsyl9r.webp

七、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);
    }
    /* ... 继续正常清理 ... */
}

关键逻辑:

  1. (void)__oom_reap_task_mm(mm):既然进程自己要死了,不如自己清理,释放全部内存。

  2. MMF_OOM_SKIP:立即设置,防止后续 reaper 线程拿到该 mm 后重复操作。

  3. 写锁:先拿写锁再释放,确保任何 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_reaperget_task_struct 的引用,确保 task_struct 可以安全释放。


九、总结:一个精妙的异步协作机制

OOM reaper 的设计哲学:

  1. 分离关注点:OOM killer 只做"谁该死"的决策,reaper 负责"清理尸体"。解耦让两个组件可以独立优化。

  2. 非阻塞重试mmap_read_trylock + 最多 10 次重试,避免了死锁,同时给了正常退出路径释放锁的时间窗口。

  3. 只处理"容易释放的":匿名页和非共享文件页,因为已经无法通过 shrink 回收,而且不会阻塞在其他锁上。

  4. 与退出路径协同:通过 MMF_OOM_SKIPMMF_OOM_VICTIM 标志,防止 reaper 和 exit_mmap 重复操作。

  5. 不等待进程退出:即使进程仍在运行(如被 SIGKILL 杀死但未退出),reaper 也能独立地解除其地址空间,立即释放物理内存。

整个流程可以总结为:mark_oom_victimwake_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 时如何优雅地杀死进程,以及在极端情况下的崩溃策略。