前文回顾: 我们通过 oom_badness 选出了 victim,通过 OOM reaper 异步清除了地址空间,释放了物理内存。但这一切的前提是:进程真的被杀死了——而且不只是被杀死,还得在系统日志中留下足够的诊断信息。本章将揭示最后两道防线:oom_kill_process 的处决流程panic_on_oom 的崩溃策略。


一、完整处决流程:从 out_of_memorySIGKILL

整个流程在 out_of_memory() 末尾的 oom_kill_process(oc) 中收网:

out_of_memory(oc)  // 已在第2章详述
  └─→ oom_kill_process(oc)  // 真正的杀入开始
        ├─ task_will_free_mem  → 若进程即将退出,只标记不做任何事
        ├─ dump_header(oc, victim)  // 打印系统内存状态
        ├─ mem_cgroup_get_oom_group  // 是否要杀整个 cgroup?
        └─ __oom_kill_process(victim, message)
              ├─ find_lock_task_mm  // 找真正有 mm 的线程
              ├─ count_vm_event(OOM_KILL)  // 统计事件
              ├─ do_send_sig_info(SIGKILL, ...)  // 发出 SIGKILL
              ├─ mark_oom_victim(victim)  // 标记 + 唤醒 reaper
              ├─ for_each_process  // 遍历线程组,杀共享 mm 的所有线程
              ├─ wake_oom_reaper(victim)  // 异步清地址空间
              └─ return

这就是整个 OOM 流程的最后一步——把选出来的 victim 处决掉


二、oom_kill_process:最终判断与日志记录

static void oom_kill_process(struct oom_control *oc, const char *message)
{
    struct task_struct *victim = oc->chosen;
    struct mem_cgroup *oom_group;
    static DEFINE_RATELIMIT_STATE(oom_rs, DEFAULT_RATELIMIT_INTERVAL,
                                  DEFAULT_RATELIMIT_BURST);

    /* 如果进程已经正在退出,不做额外操作,直接让 victim 自由释放内存 */
    task_lock(victim);
    if (task_will_free_mem(victim)) {
        mark_oom_victim(victim);
        wake_oom_reaper(victim);
        task_unlock(victim);
        put_task_struct(victim);
        return;
    }
    task_unlock(victim);

    /* 限速防止 spam 日志 */
    if (__ratelimit(&oom_rs))
        dump_header(oc, victim);

    /* 是否需要杀整个内存组? */
    oom_group = mem_cgroup_get_oom_group(victim, oc->memcg);

    /* 执行最终处决 */
    __oom_kill_process(victim, message);

    /* 杀整个 cgroup 中剩余进程 */
    if (oom_group) {
        mem_cgroup_print_oom_group(oom_group);
        mem_cgroup_scan_tasks(oom_group, oom_kill_memcg_member,
                              (void *)message);
        mem_cgroup_put(oom_group);
    }
}

三个关键分支:

  1. 进程即将退出task_will_free_mem 已经返回 true,说明 victim 正在 exit_mmap,内核只需标记 + 唤醒 reaper 即可,无需额外处决——这避免了重复杀死一个正在退出的进程。

  2. 打印日志dump_header 限速打印系统内存快照(每 5 秒最多 5 条)。

  3. cgroup 批量杀:检查是否需要将同一内存组的所有进程全部杀死。


三、dump_header:内存状态的全面记录

当 OOM 触发时,内核会输出大量的内存诊断信息,这对于事后分析至关重要:

static void dump_header(struct oom_control *oc, struct task_struct *p)
{
    /* 谁触发的 OOM?分配请求是什么? */
    pr_warn("%s invoked oom-killer: gfp_mask=%#x(%pGg), order=%d, oom_score_adj=%hd\n",
        current->comm, oc->gfp_mask, &oc->gfp_mask, oc->order,
        current->signal->oom_score_adj);

    if (!IS_ENABLED(CONFIG_COMPACTION) && oc->order)
        pr_warn("COMPACTION is disabled!!!\n");

    dump_stack();  // 打印调用栈

    /* 内存组内存信息或全局内存状态 */
    if (is_memcg_oom(oc))
        mem_cgroup_print_oom_meminfo(oc->memcg);
    else {
        show_mem(SHOW_MEM_FILTER_NODES, oc->nodemask);
        if (should_dump_unreclaim_slab())
            dump_unreclaimable_slab();
    }

    /* 列出所有可杀任务的状态表 */
    if (sysctl_oom_dump_tasks)
        dump_tasks(oc);

    /* 最后输出被选中的 victim 的简略信息 */
    if (p)
        dump_oom_summary(oc, p);
}

输出内容详解:

  • 触发者current->comm 进程名,分配的 gfp_mask(如 __GFP_DIRECT_RECLAIM__GFP_FS),请求的页序(order),以及当前进程的 oom_score_adj

  • dump_stack():调用栈,帮助开发者追溯是哪条分配路径导致的 OOM。

  • 内存信息

    • 内存组 OOM:显示该 cgroup 的内存使用量

    • 全局 OOM:调用 show_mem() 输出所有 NUMA 节点的空闲页数量,若不可回收的 slab 大于用户态内存,额外 dump unreclaimable slab 信息(帮助识别内核泄漏)。

  • dump_tasks:列出所有可杀任务的状态表,包含 pid、uid、tgid、total_vm、rss、pgtables_bytes、swapents、oom_score_adj 和进程名。这就是第3章 oom_badness 打分时看到的"死亡名单"。

  • dump_oom_summary:最终选中的 victim 的简略信息——选择原因、任务名、PID、UID。

示例输出:

Tasks state (memory values in pages):
[  pid  ]   uid  tgid total_vm      rss pgtables_bytes swapents oom_score_adj name
  [  1234 ]     0    1234    92192    85000     458752       0            0 java
  [  5678 ]    100    5678    20480    18000     204800       0            0 nginx
oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0-3,task=java,pid=1234,uid=0

2026/08/halo_8rmkmyz.webp

四、__oom_kill_process:处决执行

这是真正执行杀人的函数,流程极其讲究:

static void __oom_kill_process(struct task_struct *victim, const char *message)
{
    struct task_struct *p;
    struct mm_struct *mm;
    bool can_oom_reap = true;

    /* 查找拥有 mm 的真实线程,若不存在则放弃处决 */
    p = find_lock_task_mm(victim);
    if (!p) {
        pr_info("%s: OOM victim %d (%s) is already exiting. Skip killing\n",
            message, task_pid_nr(victim), victim->comm);
        put_task_struct(victim);
        return;
    } else if (victim != p) {
        get_task_struct(p);
        put_task_struct(victim);
        victim = p;  /* 改用真正的线程 */
    }

    mm = victim->mm;
    mmgrab(mm);

    /* 先计事,再发信号 */
    count_vm_event(OOM_KILL);
    memcg_memory_event_mm(mm, MEMCG_OOM_KILL);

    /* 第一步:发 SIGKILL 给主线程(在 grant access 之前) */
    do_send_sig_info(SIGKILL, SEND_SIG_PRIV, victim, PIDTYPE_TGID);
    mark_oom_victim(victim);
    pr_err("%s: Killed process %d (%s) total-vm:%lukB, anon-rss:%lukB, ...",
        message, task_pid_nr(victim), victim->comm, K(mm->total_vm),
        K(get_mm_counter(mm, MM_ANONPAGES)), K(get_mm_counter(mm, MM_FILEPAGES)),
        K(get_mm_counter(mm, MM_SHMEMPAGES)),
        from_kuid(&init_user_ns, task_uid(victim)),
        mm_pgtables_bytes(mm) >> 10, victim->signal->oom_score_adj);
    task_unlock(victim);

    /* 杀所有共享 victim->mm 的线程组 */
    rcu_read_lock();
    for_each_process(p) {
        if (!process_shares_mm(p, mm))
            continue;
        if (same_thread_group(p, victim))
            continue;
        /* 不杀 init 和内核线程 */
        if (is_global_init(p)) {
            can_oom_reap = false;
            set_bit(MMF_OOM_SKIP, &mm->flags);
            pr_info("oom killer %d (%s) has mm pinned by %d (%s)\n",
                task_pid_nr(victim), victim->comm, task_pid_nr(p), p->comm);
            continue;
        }
        if (unlikely(p->flags & PF_KTHREAD))
            continue;
        do_send_sig_info(SIGKILL, SEND_SIG_PRIV, p, PIDTYPE_TGID);
    }
    rcu_read_unlock();

    if (can_oom_reap)
        wake_oom_reaper(victim);

    mmdrop(mm);
    put_task_struct(victim);
}

处决策略的精妙之处:

  1. find_lock_task_mm 双重保险:即使 oom_badness 选中的是线程组的组老大(group leader),如果组老大已经分离了 ->mmfind_lock_task_mm 会找到子线程中持有 ->mm 的那个线程来执行。若全组都没有 mm,则放弃处决。

  2. 先发 SIGKILL 再标记mark_oom_victimdo_send_sig_info 之后调用,确保:

    • 进程能收到致命信号,正常退出(而非被信号中断后卡住)

    • 标记后 OOM reaper 可以异步清理地址空间

    • 关键时序:向线程组发送 SIGKILL,再 grant memory reserves 给它,防止进程利用 reserves 做坏事(比如疯狂分配内存)

  3. 共享 mm 的线程组全部清除:同一个 mm 主要来自线程组共享地址空间,或者特殊的 CLONE_VM 场景。process_shares_mm 会找到所有这些进程,逐个发送 SIGKILL。

    • 不杀 init(PID 1):如果 init 在共享 mm,说明这是一个极其特殊的场景(init 使用 kthread_use_mm),此时标记 MMF_OOM_SKIP 并停止 reaper,避免误杀系统核心进程。

    • 不杀内核线程(PF_KTHREAD):内核线程不拥有用户空间内存,杀它们没意义。

  4. wake_oom_reaper:处决完成后立即唤醒 OOM reaper 异步清理 victim 的地址空间,防止 reaper 因 exit_mmap 正在执行而被阻塞。

  5. 引用计数管理

    • mmgrab / mmdrop:对 victim 的 mm_struct 持有引用

    • get_task_struct / put_task_struct:对 victim task_struct 持有引用

    • 确保在 __oom_kill_process 返回时所有资源都已正确释放


五、oom_kill_memcg_member:cgroup 批量处理

当 OOM 发生在 cgroup 内部时,可能需要将同组进程全部杀死:

static int oom_kill_memcg_member(struct task_struct *task, void *message)
{
    if (task->signal->oom_score_adj != OOM_SCORE_ADJ_MIN &&
        !is_global_init(task)) {
        get_task_struct(task);
        __oom_kill_process(task, message);
    }
    return 0;
}

条件

  • oom_score_adj 不是 -1000(免疫保护)

  • 不是 init 进程

  • 否则跳过

mem_cgroup_get_oom_group 会检查 victim 所在的 cgroup 或其祖先 cgroup 是否也超过了内存限制,若是,则批量杀死整个 cgroup。


六、panic_on_oom:崩溃作为最后的手段

内核提供了一套 "干脆崩溃" 的应急策略,通过 sysctl_panic_on_oom 控制:

static void check_panic_on_oom(struct oom_control *oc)
{
    if (likely(!sysctl_panic_on_oom))
        return;

    if (sysctl_panic_on_oom != 2) {
        /* mode 1:只在全局约束下 panic */
        if (oc->constraint != CONSTRAINT_NONE)
            return;
    }
    /* 不 panic 来自 sysrq 触发的 OOM  */
    if (is_sysrq_oom(oc))
        return;
    dump_header(oc, NULL);
    panic("Out of memory: %s panic_on_oom is enabled\n",
        sysctl_panic_on_oom == 2 ? "compulsory" : "system-wide");
}

三种模式:

  • 0(默认):不 panic,让 OOM killer 慢慢解决

  • 1:仅在 CONSTRAINT_NONE(全局内存压力,非 cgroup/mempolicy/cpuset 限制)时 panic

  • 2:无论什么情况都 panic

2026/08/halo_flhepyg.webp

is_sysrq_oom(oc) 检查:当 oc->order == -1(即来自 sysrq-triggered dump 的 OOM 显示)时不触发 panic,防止误操作导致崩溃。

触发位置check_panic_on_oom(oc)out_of_memoryconstrained_alloc 之后、select_bad_process 之前被调用。如果开启了 panic 模式且条件匹配,直接 panic() 使内核崩溃——此时系统不再尝试正常杀死进程,而是直接挂起。

适用场景

  • 在生产环境中,OOM 可能意味着系统已不可用(内存泄漏已耗尽资源),继续使用可能导致数据不一致;

  • 配合 systemd 或其他守护进程,内核崩溃后可触发自动重启和崩溃报告;

  • 测试/调试时使用,立即观察到 OOM 是否真的发生。


七、总结:从选择到处决的完整链条

整个 OOM 流程可以概括为:

__alloc_pages 内存不足
  → shrink_zone / shrink_node 尝试回收(第1章)
  → try_to_free_pages 直接回收
  → out_of_memory 触发
    → check_panic_on_oom(崩溃模式,立即挂起)
    → mark_oom_victim 标记当前进程(若当前进程即将退出,直接放过)
    → constrained_alloc 确定约束类型和总内存
    → select_bad_process 通过 oom_badness 选 victim
    → oom_kill_process 处决:
        ├─ dump_header 记录系统状态和死亡名单
        ├─ __oom_kill_process 发 SIGKILL 杀 victim 及其线程组
        ├─ mark_oom_victim + wake_oom_reaper 开始异步清理
        └─ mem_cgroup_scan_tasks 若需要则杀整个 cgroup
  → OOM reaper 异步解除 victim 地址空间

关键设计哲学:

  1. 三步走:先回收,再筛选,最后处决——每一步都有机会避免杀进程

  2. 处决前的最后判断:即使选了 victim,oom_kill_process 中还会判断进程是否已经自行退出,避免无意义处决

  3. 日志为王dump_header + dump_tasks + pr_info 在每一级都留下丰富的诊断信息

  4. 异步解绑:杀进程和清内存分离,防止 reaper 阻塞退出路径

2026/08/halo_xfwwfq7.webp

至此,OOM 系列的六章全部完成:

  1. [linux内存管理] 第 053 篇 OOM 整体流程:从内存耗尽到系统崩溃的完整链路 — 第1章,全景图

  2. [linux内存管理] 第 054 篇 OOM触发 + Memory Reserve + out_of_memory 决策 — 第2章,从触发到决策

  3. [linux内存管理] 第 055 篇 OOM Killer 死亡名单算法— 第3章,打分与筛选

  4. [linux内存管理] 第 056 篇 OOM Reaper:异步解除映射的幕后英雄 — 第4章,异步清理

  5. [linux内存管理] 第 057 章节 OOM Kill 与 panic_on_oom —— 杀人的艺术与崩溃的抉择 — 第5章,处决与崩溃