当你在日志里看到
Out of memory: Killed process 1234 (java)时,内核其实已经走完了一条相当漫长的“求生链路”:分配失败、直接回收、唤醒 kswapd、内存规整、再次重试,直到最后不得不选择一个进程作为牺牲品。本文就沿着这条链路,把 Linux OOM 从触发到 kill、从 memcg OOM 到系统 panic 的整体流程串起来。
在前面几篇文章里,我们已经拆过内存回收的核心路径:shrink_lruvec()、shrink_page_list()、pageout()、try_to_unmap()、__remove_mapping()……这些机制的目标都很明确:尽量从系统中回收出足够的物理页,让下一次分配能够成功。
但如果这些努力全部失败呢?
如果直接回收回不来,kswapd 也来不及救场,slab 能缩的已经缩了,LRU 上能扫的页也扫过了,内存规整也没能拼出足够的连续页,那么内核就会进入最后阶段:OOM(Out of Memory)处理。
OOM 不是普通回收路径的自然延续,而是回收失败后的兜底策略。它的核心问题不再是“还能不能温和地腾出一点内存”,而是更残酷的:
到底要不要杀进程?杀谁?如果连杀进程都不可信,是否直接让系统 panic?
一、OOM 的定位:回收走不通后的最后决策
首先要明确一个边界:OOM killer 不是内存回收算法本身。
内存回收负责“尽量不伤人地释放页面”,比如:
扫描 LRU 链表;
回收 clean file page;
回写 dirty page;
将匿名页写入 swap;
解除页表映射;
释放 slab cache;
做内存规整 compaction;
唤醒 kswapd 在后台继续回收。
这些都还属于“正常自救”。只有当分配路径发现这些手段都不足以满足当前分配请求时,才会进入 OOM 决策。
典型入口在页分配慢路径中:
__alloc_pages_slowpath()
...
__alloc_pages_may_oom()
out_of_memory()
也就是说,OOM 不是内核一看到空闲内存低就立刻触发,而是在分配路径已经多次尝试、回收和重试之后,才被迫启动的最后机制。
二、OOM 的主要触发场景
OOM 看起来都是“内存不够”,但在内核里,不同来源的 OOM 约束范围并不一样。最常见的可以分成三类。
1. 全局分配失败:系统级 OOM
这是最典型的 OOM 场景。
某个进程或内核路径调用 alloc_pages() 分配物理页,页分配器进入 slowpath,尝试直接回收、唤醒 kswapd、做 compaction、降低水位线重试等一系列动作。如果最终仍然失败,就可能通过 __alloc_pages_may_oom() 进入:
out_of_memory(&oc)
这里的 OOM 是全局意义上的:系统层面的可用内存已经无法满足当前请求。后续 victim 选择也会在全局范围内寻找合适的可杀进程,但仍会受到 cpuset、mempolicy、nodemask 等约束影响。
2. MemCG OOM:cgroup 范围内的 OOM
第二类是 memcg OOM。
比如某个容器或 cgroup 设置了 memory.max,组内进程的内存使用超过了这个上限。此时系统整体可能还有大量空闲内存,但对这个 cgroup 来说,它已经“没钱了”。
这种情况下,内核会进入 memcg OOM 路径:
mem_cgroup_out_of_memory()
out_of_memory(&oc)
注意,这里虽然也调用 out_of_memory(),但传入的 oom_control 中带着 memcg 上下文。也就是说,memcg OOM 使用的是同一套 OOM 决策框架,但 victim 的选择范围会被限制在对应 memcg 及其相关层级内。
它不是“扩散到全系统随便杀其他 cgroup 的进程”。如果配置了 memory.oom.group,内核还可能按 cgroup 粒度杀掉一组进程,而不是只杀单个 task。
3. Page Fault 场景:缺页路径感知 OOM
第三类和缺页异常有关。
用户进程访问某个尚未建立映射的虚拟地址时,内核会进入 page fault handler,尝试为它分配物理页并建立页表。如果底层分配失败,缺页路径可能看到 VM_FAULT_OOM。
不过这里要稍微小心:在现代内核中,pagefault_out_of_memory() 主要负责同步和处理 memcg OOM 状态;真正的全局 OOM 通常还是由页分配慢路径触发。也就是说,缺页路径可以“感知” OOM,但全局 OOM killer 的主要入口仍然在 allocator slowpath。
这点很重要,否则容易误以为所有 page fault 分配失败都会直接调用全局 out_of_memory()。
三、进入 out_of_memory():先判断是否必须 panic
当内核进入 out_of_memory() 时,并不是马上开始杀进程。它首先要判断:这个 OOM 场景是否应该直接让系统 panic。
关键配置是:
/proc/sys/vm/panic_on_oom
它大致有三种含义:
0:默认行为,尽量通过 OOM killer 杀进程自救;1:对系统级 OOM 可以 panic,但对 memcg、cpuset、mempolicy 等受限 OOM 场景不一定 panic;2:更强硬,遇到 OOM 时倾向于直接 panic。
所以 panic_on_oom=1 并不等于“任何 OOM 都立刻崩溃”。内核会结合当前 OOM 的约束类型判断,只有满足条件时才会走:
check_panic_on_oom()
panic("Out of memory")
如果没有选择 panic,OOM killer 才会继续向下,进入 victim 选择流程。
四、特殊快速路径:当前进程已经在退出或即将被杀
在真正扫描全系统选 victim 之前,内核还会看一些特殊情况。
比如当前触发 OOM 的进程已经收到了 SIGKILL,或者正在退出路径上,那么它本身马上就会释放内存。此时内核没有必要再额外杀一个进程,而是可以把当前进程标记为 OOM victim,让它优先完成退出。
相关动作包括:
mark_oom_victim(current)
queue_oom_reaper(current)
这里的 mark_oom_victim() 很关键。被标记为 OOM victim 的任务,在退出过程中可以获得一些特殊待遇,比如更容易访问少量内存保留资源,从而避免“想退出释放内存,却连退出所需的一点内存都分配不到”的死局。
这类保留资源不建议简单理解成“每个 NUMA 节点固定 32 页 OOM Reserve”。更准确的说法是:内核会通过 OOM victim 标记和页分配器的 reserve 机制,尽量保证被选中的牺牲进程能够顺利退出并释放内存。
五、选择 victim:谁最该被杀?
如果没有直接 panic,也没有现成的退出进程可以利用,内核就必须面对 OOM killer 最核心的问题:
杀谁?
这个逻辑主要由下面的函数完成:
select_bad_process()
oom_evaluate_task()
oom_badness()
select_bad_process() 会遍历候选进程,对每个进程计算一个 badness 分数。分数越高,越可能成为 OOM victim。
oom_badness() 的基本思路
oom_badness() 不是单纯看谁占用内存最多,而是综合几个因素:
进程占用的内存越多,分数越高;
oom_score_adj越大,越容易被杀;oom_score_adj = -1000的进程通常被视为不可杀;内核线程、不可回收或不合适的任务会被跳过;
在 memcg OOM 中,候选范围会受 memcg 限制。
用户最常接触的是:
/proc/<pid>/oom_score
/proc/<pid>/oom_score_adj
oom_score 是内核计算出的当前分数,oom_score_adj 是用户或系统服务可以设置的调节值,范围是 -1000 到 1000。
例如:
echo -1000 > /proc/<pid>/oom_score_adj
可以尽量保护某个关键进程不被 OOM killer 选中。
而:
echo 1000 > /proc/<pid>/oom_score_adj
则表示这个进程在 OOM 时可以优先牺牲。
很多服务管理器、容器运行时、Android low memory 策略,都会围绕这个接口做进程优先级管理。
六、执行 kill:oom_kill_process() 做了什么?
一旦 victim 被选中,内核就会进入:
oom_kill_process()
这一步不是简单地发一个 SIGKILL 就结束。它通常会做几类事情。
1. 打印 OOM 信息
内核会打印 OOM 上下文,包括当前触发者、gfp mask、order、nodemask、cpuset、memcg 信息等,并通过 dump_header() 输出系统内存快照。
这些日志是排查 OOM 的黄金线索。
2. 标记 victim
被选中的进程会被标记为 OOM victim:
mark_oom_victim(victim)
这个标记的意义是告诉系统:它就是这次 OOM 的牺牲对象,请尽快让它退出、释放内存,并在必要时给它一点“退出通道”。
3. 发送 SIGKILL
随后内核会向 victim 发送不可捕获的 kill 信号:
do_send_sig_info(SIGKILL, ...)
日志里常见的这一行就来自这里附近:
Out of memory: Killed process 1234 (java) total-vm:8388608kB, anon-rss:1048576kB, file-rss:0kB, shmem-rss:0kB
其中几个字段很有用:
total-vm:进程总虚拟地址空间;anon-rss:匿名页常驻内存;file-rss:文件页常驻内存;shmem-rss:共享内存常驻内存。
真正分析问题时,anon-rss 往往比 total-vm 更值得关注,因为虚拟地址空间大不代表实际占用了同样多的物理内存。
4. 处理线程组和 memcg group kill
如果 victim 是多线程进程,内核会确保相关线程正确退出。
如果是 memcg OOM,并且开启了 memory.oom.group,内核可能会将整个 cgroup 作为牺牲对象处理,而不是只杀其中一个进程。这对容器场景很重要:与其只杀容器里的一个进程导致服务半死不活,不如按组终止,让上层编排系统重新拉起。
七、oom_reaper:不是“不杀人的替代方案”,而是 victim 的异步回收器
很多 OOM 文章会把 oom_reaper 写成一个“杀进程之前先试试解除映射”的温和机制,但这个说法容易误导。
更准确地说:
oom_reaper 是 OOM victim 的异步内存回收加速器。
它的目标不是随便找一个进程剥夺内存,也不是替代 kill。它主要服务于已经被选中、已经被标记,或者正在退出的 OOM victim。
典型链路是:
oom_kill_process()
mark_oom_victim()
do_send_sig_info(SIGKILL, ...)
queue_oom_reaper()
oom_reaper()
oom_reap_task()
oom_reap_task_mm()
__oom_reap_task_mm()
为什么需要它?
因为进程收到 SIGKILL 后,并不会瞬间释放所有内存。它还要调度运行、进入退出路径、释放 mm、清理页表。如果 victim 卡在某个锁上,或者退出过程迟迟无法推进,系统可能会继续处在 OOM 状态,甚至反复触发 OOM killer。
oom_reaper 的作用就是提前介入,尽快解除 victim 的部分用户态映射,让相关匿名页可以更快回到系统中。
它主要处理的是适合异步回收的 VMA,例如匿名私有映射;对一些不安全或不能随便拆的映射,会选择跳过。这样做的目标是:尽快从 victim 身上榨出可释放内存,同时避免卡在复杂的退出路径上。
所以,oom_reaper 不是“先不杀人试试看”的第一道防线,而是“既然已经选了牺牲者,就尽快让它释放内存”的加速器。
八、全局 OOM 的主流程图
把前面的内容串起来,一个典型的全局 OOM 大致是这样:

这个图里最重要的点是:OOM killer 的核心主线是 panic 判断 → victim 选择 → kill → oom_reaper 加速释放,而不是 panic、reaper、kill 三个完全并行的分叉。
九、MemCG OOM 的主流程图
memcg OOM 和全局 OOM 共享很多代码,但约束范围不同:

这解释了一个常见现象:容器里进程被 OOM kill,并不代表宿主机全局内存真的耗尽了。它可能只是这个容器自己的 memcg 配额耗尽。
排查这类问题时,不能只看 free -h,还要看 cgroup 的内存限制和事件计数,例如:
memory.current
memory.max
memory.events
memory.oom.group
十、如何从日志判断一次 OOM?
一次典型 OOM 日志通常包含几类信息。
1. 触发上下文
比如:
invoked oom-killer: gfp_mask=..., order=..., oom_score_adj=...
这里可以看出是谁触发了 OOM、当时想分配什么类型的内存、分配阶数是多少。
如果 order 很大,问题可能不只是“总内存不足”,还可能是连续物理页不足,也就是碎片化严重。
2. 系统内存快照
dump_header() 会打印大量内存状态,包括 active/inactive、file/anon、slab、swap、各 zone 水位线等。
这里要重点看:
free page 是否真的很低;
file cache 是否还有可回收空间;
swap 是否耗尽;
slab 是否异常膨胀;
某个 zone 是否低于水位线;
是否是高阶分配失败。
3. 被杀进程
最后会看到:
Out of memory: Killed process 1234 (java) ...
这行告诉你 victim 是谁,但它不一定等于“罪魁祸首”。OOM killer 选择的是当时最适合杀的进程,而不是一定最早泄漏内存的进程。
这也是排查 OOM 时最容易踩坑的地方:被杀者不一定是肇事者。
十一、总结:OOM 是内核最后的秩序维护
从 alloc_pages() 分配失败,到最终打印 Out of memory: Killed process ...,中间并不是一条简单的“内存不够 → 杀进程”直线。
更完整的链路是:
页分配进入 slowpath;
尝试直接回收、slab 回收、kswapd、compaction;
多次重试仍然失败;
进入
out_of_memory();判断是否需要 panic;
如果当前进程已经在退出,尽量让它完成释放;
否则通过
oom_badness()选择 victim;oom_kill_process()标记并杀死 victim;oom_reaper 异步解除 victim 映射,加快内存释放;
系统重新获得可用内存,分配路径得以继续。
OOM killer 的本质不是“暴力杀进程”,而是在系统已经没有温和选择时,尽量用一个最小代价维持整体可用性。
它像内存管理系统最后的执法者:平时不出现,一旦出现,说明前面的回收、防抖、规整、限额机制都已经顶不住了。
接下去的OOM系列文章,我们可以继续深入拆解三个关键细节:
out_of_memory()的源码主线;oom_badness()的打分细节;oom_reaper如何安全地异步拆除 victim 的页表映射。