前文回顾: 我们通过 oom_badness 选出了 victim,通过 OOM reaper 异步清除了地址空间,释放了物理内存。但这一切的前提是:进程真的被杀死了——而且不只是被杀死,还得在系统日志中留下足够的诊断信息。本章将揭示最后两道防线:oom_kill_process 的处决流程 和 panic_on_oom 的崩溃策略。
一、完整处决流程:从 out_of_memory 到 SIGKILL
整个流程在 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);
}
}三个关键分支:
进程即将退出:
task_will_free_mem已经返回 true,说明 victim 正在exit_mmap,内核只需标记 + 唤醒 reaper 即可,无需额外处决——这避免了重复杀死一个正在退出的进程。打印日志:
dump_header限速打印系统内存快照(每 5 秒最多 5 条)。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
四、__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);
}处决策略的精妙之处:
find_lock_task_mm双重保险:即使oom_badness选中的是线程组的组老大(group leader),如果组老大已经分离了->mm,find_lock_task_mm会找到子线程中持有->mm的那个线程来执行。若全组都没有 mm,则放弃处决。先发 SIGKILL 再标记:
mark_oom_victim在do_send_sig_info之后调用,确保:进程能收到致命信号,正常退出(而非被信号中断后卡住)
标记后 OOM reaper 可以异步清理地址空间
关键时序:向线程组发送 SIGKILL,再 grant memory reserves 给它,防止进程利用 reserves 做坏事(比如疯狂分配内存)
共享 mm 的线程组全部清除:同一个 mm 主要来自线程组共享地址空间,或者特殊的 CLONE_VM 场景。
process_shares_mm会找到所有这些进程,逐个发送 SIGKILL。不杀 init(PID 1):如果 init 在共享 mm,说明这是一个极其特殊的场景(init 使用 kthread_use_mm),此时标记
MMF_OOM_SKIP并停止 reaper,避免误杀系统核心进程。不杀内核线程(PF_KTHREAD):内核线程不拥有用户空间内存,杀它们没意义。
wake_oom_reaper:处决完成后立即唤醒 OOM reaper 异步清理 victim 的地址空间,防止 reaper 因exit_mmap正在执行而被阻塞。引用计数管理:
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 限制)时 panic2:无论什么情况都 panic

is_sysrq_oom(oc) 检查:当 oc->order == -1(即来自 sysrq-triggered dump 的 OOM 显示)时不触发 panic,防止误操作导致崩溃。
触发位置:check_panic_on_oom(oc) 在 out_of_memory 中 constrained_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 地址空间关键设计哲学:
三步走:先回收,再筛选,最后处决——每一步都有机会避免杀进程
处决前的最后判断:即使选了 victim,
oom_kill_process中还会判断进程是否已经自行退出,避免无意义处决日志为王:
dump_header+dump_tasks+pr_info在每一级都留下丰富的诊断信息异步解绑:杀进程和清内存分离,防止 reaper 阻塞退出路径

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