当物理内存 + swap 都不够,回收也搞不定时,内核最后一招:选一个进程 SIGKILL。这就是 OOM Killer。它的目标是”花最小代价释放最多内存”。
一、先看图
flowchart TD
ALLOC[页分配失败<br/>回收/compact 都试过] --> OOM[out_of_memory]
OOM --> CG{memcg 限制<br/>还是全局?}
CG -->|memcg| CGOOM[memcg OOM<br/>只杀 cgroup 内进程]
CG -->|global| GOOM[全局 OOM]
GOOM --> SELECT[select_bad_process<br/>遍历 task_struct]
CGOOM --> SELECT
SELECT --> SCORE[计算 oom_score<br/>RSS + adj]
SCORE --> KILL[SIGKILL 最高分者]
KILL --> FREE[释放其内存]
classDef crit fill:#f8514922,stroke:#f85149,color:#adbac7;
classDef warn fill:#f0883e22,stroke:#f0883e,color:#adbac7;
classDef ok fill:#3fb95022,stroke:#3fb950,color:#adbac7;
class ALLOC,OOM crit
class SELECT,SCORE warn
class FREE ok
二、oom_score 算法
内核给每个进程打分 0-1000:
- 基础分 = 进程 RSS / 系统可用内存 × 1000
- 加上子进程共享内存贡献
- 加上
oom_score_adj(-1000 到 1000)
cat /proc/$$/oom_score # 当前分数
cat /proc/$$/oom_score_adj # 调整值分最高的先被杀。
oom_score_adj
- -1000:永不被 OOM Kill(等价于
oom_adj = -17) - 0:默认
- +1000:优先被杀
echo -1000 > /proc/$$/oom_score_adj # 保护关键进程
echo 500 > /proc/$$/oom_score_adj # 牺牲品systemd 用 OOMScoreAdjust= 配置。
三、全局 OOM 流程
__alloc_pages_slowpath
→ __alloc_pages_may_oom
→ out_of_memory
→ select_bad_process # 遍历 for_each_process
→ oom_badness(task) # 打分
→ oom_kill_process
→ do_send_sig_info(SIGKILL)
→ 标记 TIF_MEMDIE # 给其分配 "OOM victim" 特权
TIF_MEMDIE 让被杀进程的分配暂时不受 watermark 限制——给它机会在 SIGKILL 处理中释放资源。
四、memcg OOM
cgroup v2 memory.max 超限触发 memcg 级
OOM:
- 只在 cgroup 内选 victim
memory.oom.group = 1:杀整个 cgroup(而非一个进程)memory.oom.priority(提案中):多 cgroup 竞争时优先杀低优先级
echo 1 > /sys/fs/cgroup/myapp/memory.oom.groupKubernetes pod 的 OOM 通常是 memcg OOM。
五、unkillable 进程
- PID 1(init / systemd):不可被全局 OOM 杀。但 memcg OOM 可以杀容器内 PID 1
- 内核线程:不参与 OOM 评分
oom_score_adj = -1000的进程:跳过
如果所有进程都 -1000,OOM Killer panic(没得杀了 → 系统挂)。
六、用户态 OOM:systemd-oomd
内核 OOM Killer 的问题:
- 介入太晚(物理内存完全耗尽)
- 选择不够智能(只看 RSS)
- 杀完后可能连锁雪崩
systemd-oomd(247+)基于 PSI 提前介入:
# /etc/systemd/oomd.conf
[OOM]
SwapUsedLimit=90%
DefaultMemoryPressureLimit=60%
DefaultMemoryPressureDurationUSec=30s原理: 1. 轮询 cgroup 的 memory.pressure 2.
某 cgroup PSI some > 阈值 + 持续时间 3. 杀掉该 cgroup(发
SIGKILL + memory.oom.group)
好处:在内存完全耗尽之前就介入。
earlyoom
更简单的用户态 OOM daemon:
earlyoom -m 5 -s 5 # 可用内存或 swap < 5% 时杀最大进程比 systemd-oomd 轻量,适合嵌入式/桌面。
七、追查 OOM 事故
7.1 dmesg
[ 123.456] Out of memory: Killed process 1234 (myapp) total-vm:8192000kB ...
OOM 日志包含:
- 触发者(哪个分配触发的)
- 系统内存快照(MemFree/Active/Inactive/Slab/…)
- 每个进程的 RSS、oom_score_adj
- 被杀者的 PID/comm/RSS
7.2 journalctl
journalctl -k --grep="Out of memory"
journalctl --unit=systemd-oomd7.3 cgroup
cat /sys/fs/cgroup/myapp/memory.events
# oom 3 ← 触发了 3 次 OOM
# oom_kill 3 ← 杀了 3 次
# oom_group_kill 17.4 kdump
严重 OOM → panic → kdump 捕获 vmcore。crash 工具可还原当时每个进程的 mm_struct。
八、防御策略
8.1 配额
# 给关键服务设 memcg 限制
systemctl set-property myapp.service MemoryMax=4G MemorySwapMax=2G8.2 保护
echo -1000 > /proc/$(pidof sshd)/oom_score_adj # 保护 sshd8.3 牺牲
echo 1000 > /proc/$(pidof benchmark)/oom_score_adj8.4 overcommit
sysctl vm.overcommit_memory=2 # 严格:malloc 会失败但不会 OOM8.5 预警
# PSI 触发器
echo "some 500000 2000000" > /proc/pressure/memory
# 500ms 窗口内 some stall > 2s 时触发 epoll 通知九、常见误区
A:“OOM = 内存泄漏” ❌
不一定;正常负载超出配额也会。先看
memory.events 和 meminfo。
B:“关 swap 就不 OOM” ❌ 反而更容易 OOM——没有 swap 缓冲。
C:“-1000 所有关键进程” ❌ 不留牺牲品 = panic。
D:“OOM 后只杀一个” 不一定;可能连续触发杀多个。
十、小结
- OOM Killer 是最后手段:选分最高者 SIGKILL
- oom_score_adj 控制优先级
- memcg OOM 只影响 cgroup 内
- systemd-oomd / earlyoom 基于 PSI 提前介入
- 追查靠 dmesg + memory.events + kdump
参考文献
mm/oom_kill.cDocumentation/admin-guide/mm/concepts.rstDocumentation/filesystems/proc.rst(oom_score 描述)- Corbet, J. “Toward more predictable and reliable out-of-memory handling.” LWN.net 2019
- systemd-oomd(8) man page
工具
dmesg、journalctl -k/proc/$$/oom_score、/proc/$$/oom_score_adjmemory.events(cgroup v2)systemd-oomd、earlyoomcrash(vmcore 分析)
文章导航
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
CPU 调度:CFS 的虚拟运行时间与 EEVDF 的虚拟截止期
从比例份额调度的论文谱系出发,对照 Linux v6.6 与 v6.12 fair.c 源码,解释 CFS 的 vruntime、EEVDF 的 lag/eligible/deadline,并用确定性模拟复现延迟与 lag 的差异。
I/O 调度:在寻道、队列深度与公平性之间取舍
从电梯算法到 blk-mq,解释 Linux I/O 调度器为何在 HDD 上排序、在共享设备上保公平、在 NVMe 上常选择 none,并用确定性模拟展示寻道距离与队列尾延迟的取舍。
用户态内存分配器:size class、线程缓存与碎片边界
从 Wilson 分配器综述到 jemalloc、gperftools tcmalloc 与 mimalloc 的源码路径,解释 size class、线程缓存、跨线程释放和 RSS 碎片;用可复现实验说明不能只凭 allocator 名字下结论。
伙伴系统与 SLUB:Linux 物理页和小对象分配的边界
以 Linux v6.6 源码和确定性模拟为准,解释伙伴系统的 split/coalesce、migratetype 反碎片策略,以及 SLUB 的 per-CPU freelist、partial list 与安全加固。