OOM Killer:内核什么时候决定杀进程
上一篇讲了 memcg 如何把全局内存资源划分成层级预算。当预算用尽且回收无力时,最后一道防线是 OOM killer——通过终止进程来释放内存。这不是内存管理的常规路径,而是所有正常手段都失败后的兜底。
核心问题可以压成一句话:
OOM killer 是多轮分配、回收、压缩、写回、swap 都无法满足请求后的兜底路径,不是内存管理的常规目标。
问题从哪里来
内存分配失败的处理有一个基本问题:内核不能简单地对调用者返回"分配失败"。很多内核代码路径不检查分配失败(GFP_KERNEL 分配假设不会失败),即使返回错误,用户空间进程通常也没有合理的 fallback 逻辑。
所以内核的策略是:在返回失败之前,尽可能通过各种手段释放内存。如果所有手段都用尽仍然无法满足分配请求,最后才走 OOM kill——选择一个进程杀掉以释放它占用的内存。
到达 OOM kill 之前的完整路径:
1 | |
这个路径说明:OOM kill 发生时,系统已经经历了多轮回收和压缩尝试。如果在日志中看到 OOM,首先应该追问:回收为什么失败?是所有页面都不可回收(pinned/mlocked/unevictable),还是回收速度跟不上分配速度?
overcommit:为什么分配成功不代表有物理内存
Linux 默认启用内存 overcommit。mmap 和 malloc 返回成功只意味着虚拟地址空间被预留,不保证有物理页支持。物理页在首次访问(page fault)时才分配。
overcommit 策略通过 /proc/sys/vm/overcommit_memory 控制:
| 值 | 行为 |
|---|---|
| 0(默认) | 启发式 overcommit:明显不合理的请求被拒绝 |
| 1 | 永远允许 overcommit:所有 mmap 都成功 |
| 2 | 严格模式:总承诺量不超过 swap + RAM × overcommit_ratio |
在默认模式下,一个进程可以 malloc(100GB) 成功(虚拟地址空间被映射),但实际使用时如果物理内存不够就会触发 OOM。这就是"分配成功 ≠ 有内存"的根源。
overcommit 的合理性:fork 后大多数子进程立即 exec(地址空间被替换),如果 fork 时强制预留物理内存,绝大多数系统无法正常工作。COW 使得 fork 不需要实际复制页面,overcommit 使得这些未触碰的页面不需要物理预留。
victim 选择:oom_score
OOM killer 不是随机杀进程。它用一个评分机制选择"最合适"的 victim:
oom_score(/proc/<pid>/oom_score):内核计算的 OOM 分数,范围 0-1000+。分数越高越可能被选为 victim。基础分数与进程的 RSS(常驻内存集)成正比——使用内存越多,分数越高。
oom_score_adj(/proc/<pid>/oom_score_adj):管理员可调的偏移量,范围 -1000 到 1000。设为 -1000 表示永不被 OOM kill(进程免死)。设为正值表示该进程更容易被选中。
1 | |
实际实现(oom_badness())更复杂:考虑子进程共享页面、硬件资源预留、特权进程等因素。但核心逻辑是:杀掉释放最多内存的进程——因为 OOM kill 的目标是尽快恢复可用内存。
global OOM 与 memcg OOM
两种 OOM 触发场景有本质区别:
| 维度 | global OOM | memcg OOM |
|---|---|---|
| 触发条件 | 系统全局物理内存 + swap 耗尽 | cgroup 达到 memory.max |
| victim 范围 | 全局所有进程(除 oom_score_adj=-1000) | 只限该 cgroup 内的进程 |
| 前提 | 所有 zone 的 kswapd 回收都失败 | 该 cgroup 的 per-memcg 回收失败 |
| 信号 | dmesg “Out of memory” | dmesg “Memory cgroup out of memory” |
| 宿主状态 | 真的没有可用内存 | 宿主可能仍有大量空闲内存 |
memcg OOM 是容器环境中最常见的 OOM 类型。它表示容器的预算用完,不代表物理机出了问题。排查方向完全不同:global OOM 要看全局内存使用和回收效率,memcg OOM 要看容器的 memory.max 设置是否合理以及容器内的内存使用模式。
OOM 日志怎么读
OOM 触发时,内核向 dmesg 输出一段结构化日志。关键字段:
1 | |
各字段含义:
| 字段 | 含义 | 诊断价值 |
|---|---|---|
gfp_mask |
触发 OOM 的分配请求的标志 | 说明是什么类型的分配失败 |
order |
请求的页面阶 | order=0 表示连单页都分配不了;高阶可能是碎片问题 |
oom_score_adj |
触发分配的进程的 adj 值 | 非 victim,是触发者 |
constraint |
OOM 约束类型 | CONSTRAINT_MEMCG=容器限制,CONSTRAINT_NONE=全局 |
total-vm |
victim 的虚拟内存总量 | |
anon-rss |
victim 的匿名页 RSS | 被杀后释放的主要内存来源 |
file-rss |
victim 的文件映射 RSS | page cache,可能被回收 |
日志中还包含系统内存状态快照(MemFree、Active、Inactive、Slab 等),这些信息帮助还原 OOM 发生时的完整现场。
读 OOM 日志的正确顺序:
constraint→ 确定是全局还是 memcg OOMorder→ 0 表示真缺页,高阶可能只是碎片- 内存状态快照 → 回收是否真的无计可施
- victim 选择 → 为什么选了这个进程
防御性配置
几种降低 OOM 影响的策略:
调整 oom_score_adj:为关键服务设置负值(如 -500 到 -999),使其不容易被选为 victim。但设为 -1000(免死)要慎重——如果所有重要进程都免死,OOM killer 可能杀掉不相关的系统进程。
1 | |
合理设置 memcg 限制:memory.high 比 memory.max 更优先使用——前者通过节流避免 OOM,后者直接杀进程。
禁用 overcommit(模式 2):在已知工作集大小的环境中,严格模式可以让分配在虚拟地址阶段就失败,避免后续 OOM。但这大幅降低了内存利用率。
earlyoom / systemd-oomd:用户空间 OOM daemon 在内存压力达到阈值时主动杀进程,比内核 OOM killer 更早介入、决策更灵活(可以根据服务优先级、重启策略等选择目标)。
模式提炼:不可恢复的分配失败触发终止以释放资源
1 | |
| 阶段 | 动作 | 失败后 |
|---|---|---|
| 正常分配 | 从 free_area 取页 | → 唤醒 kswapd |
| 后台回收 | kswapd 扫描 LRU | → direct reclaim |
| 同步回收 | 当前进程回收页面 | → compaction |
| 碎片整理 | 迁移可移动页 | → retry |
| 重试仍然失败 | → OOM kill | |
| OOM kill | SIGKILL victim | → oom_reaper 回收 |
这个逐步升级的策略在系统设计中是经典模式:先用轻量级方法(缓存淘汰),再用重量级方法(同步 I/O、GC STW),最后用破坏性方法(终止连接、丢弃请求)。对应分布式系统中的 circuit breaker 三态:closed → half-open → open。
一个最小实验:在 cgroup 中安全触发 OOM
这个实验在受限 cgroup 中触发 memcg OOM,不影响宿主系统。
1 | |
编译与运行:
1 | |
完成实验后清理:
1 | |
读取实验结果
实验预期行为:
程序在 ~56-64MB 时被杀:前几轮分配顺利。接近 memory.max 时内核开始 per-memcg 回收。由于所有页面都是刚 touch 的匿名页(无法回收),回收失败后触发 memcg OOM,进程收到 SIGKILL。
dmesg 输出:
Memory cgroup out of memory: Killed process <pid> (oom_demo)oom_kill_constraint=CONSTRAINT_MEMCGanon-rss接近 memory.max 的值
memory.events:oom 和 oom_kill 计数器各增加 1。
关键观察:
- 进程不是在"第 8 轮 malloc 时"被杀——是在 page fault 时被杀。mmap 本身几乎总是成功的(只是预留虚拟地址),OOM 发生在 touch 内存(触发物理分配)的时刻。
- 如果给 cgroup 设置
memory.swap.max=0,OOM 来得更快——因为匿名页无处可去。如果允许 swap,内核会先 swap out 再 OOM。 oom_score随 RSS 增长而增加。在单进程 cgroup 中,该进程必然是 victim(没有其他选择)。
模式提炼:OOM 是症状,不是原因
1 | |
看到 OOM 日志时,正确的反应不是"内存不够了",而是追溯回收失败的原因。OOM 是回收链路末端的事件,它的存在意味着前面的每个环节都已经尝试过且失败了。
源码锚点
| 入口 | 读它的目的 |
|---|---|
mm/oom_kill.c |
out_of_memory():OOM 决策入口 |
mm/oom_kill.c |
oom_badness():victim 评分函数 |
mm/oom_kill.c |
oom_reaper:异步回收被杀进程的内存 |
mm/memcontrol.c |
mem_cgroup_oom():memcg OOM 触发路径 |
mm/page_alloc.c |
__alloc_pages_slowpath():分配失败到 OOM 的路径 |
include/linux/oom.h |
OOM 约束类型、标志定义 |
读 oom_badness() 时可以带着一个问题:如果两个进程的 RSS 相同且 oom_score_adj 相同,内核用什么规则打破平局?
研究生迁移表
| Linux 概念 | 一般系统设计 |
|---|---|
| OOM killer | 断路器 open 状态(circuit breaker trip) |
| oom_score | 优先级队列中的权重 |
| oom_score_adj | 管理员配置的服务优先级 |
| overcommit | 超售(航空/云计算/连接池) |
| memcg OOM | per-tenant 熔断(不影响其他租户) |
| oom_reaper | 异步资源清理(GC finalizer) |
| earlyoom/systemd-oomd | 主动熔断(proactive circuit breaker) |
| OOM 日志 | 事后分析 artifact(post-mortem trace) |
OOM 的核心模式——“正常回收手段用尽后通过破坏性操作恢复系统”——在分布式系统中对应 load shedding(过载丢弃请求)、Kubernetes OOMKilled(pod 重启)、数据库连接池满时 reject 新连接。区别在于粒度:Linux OOM 杀进程,K8s 重启 pod,数据库拒绝连接。
常见误解
第一个误解是认为 OOM killer 随机杀进程。实际上 victim 选择完全基于 oom_badness() 评分——RSS 最大且 oom_score_adj 最高的进程最可能被选中。可以通过 /proc/<pid>/oom_score 预判谁会被杀。
第二个误解是认为 OOM 意味着 malloc 返回 NULL。在 overcommit 模式下,malloc(通过 mmap)几乎不会返回 NULL。OOM 发生在 page fault 时——此时内核直接发 SIGKILL,进程没有机会处理。用户空间代码检查 malloc 返回值在 Linux 上几乎不能防御 OOM。
第三个误解是认为 oom_score_adj=-1000 是万能保护。如果系统中所有重要进程都设为 -1000,OOM killer 只能杀 init 或 kernel thread——这通常导致系统挂起而不是恢复。应该只对真正关键的少数进程使用 -1000。
第四个误解是认为增加 swap 能"解决"OOM。swap 推迟 OOM 的发生(匿名页可以换出),但如果内存泄漏是根因,swap 只是延长了到达 OOM 的时间。同时大量 swap 活动导致系统极度缓慢(thrashing),此时进程虽然活着但几乎无法响应。
第五个误解是认为容器中看到 OOM 就要给宿主加内存。memcg OOM 是局部事件——可能只需要调高容器的 memory.max,或者优化容器内应用的内存使用。宿主物理内存可能仍然充裕。
练习
第一,查看当前系统的 overcommit 配置:cat /proc/sys/vm/overcommit_memory 和 /proc/sys/vm/overcommit_ratio。用 /proc/meminfo 中的 CommitLimit 和 Committed_AS 计算当前的承诺比例。
第二,列出系统中 oom_score 最高的 10 个进程:ps -eo pid,comm,rss --sort=-rss | head,然后对比 /proc/<pid>/oom_score。验证分数是否与 RSS 大致成正比。
第三,按文中实验在一个 64MB 的 cgroup 中触发 OOM。对比 memory.events 在 OOM 前后的变化。用 dmesg 找到 OOM 日志,标出 constraint、victim RSS 和 gfp_mask。
第四,阅读 mm/oom_kill.c 中 select_bad_process() 的实现。列出哪些进程会被跳过不杀(除了 oom_score_adj=-1000 外还有哪些条件)。
第五,配置 systemd-oomd(如果系统支持)或研究其配置:它如何在内核 OOM 之前介入?它使用什么指标(PSI memory pressure)判断何时杀进程?与内核 OOM killer 相比,用户空间 OOM daemon 的优势和局限是什么?
系列导航
- 上一篇:cgroup memory:内存从全局资源变成局部预算
- 本文:OOM Killer:内核什么时候决定杀进程
- 下一篇:回到工程:Linux VM 如何改变性能诊断
