当物理内存 + 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 分析)
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。
【操作系统百科】交换
swap 还值得开吗?本文讲 swap area 基础、swap cache、zram 压缩内存、zswap 前端压缩池、swappiness 的真实含义、容器里的 swap 策略,以及为什么现代 Android 全靠 zram 不靠磁盘。
【操作系统百科】Slab/SLUB 分配器
buddy 只管页粒度(4K+),内核大多数对象只有几十到几百字节。slab/SLUB 在 buddy 之上做对象级缓存。本文讲 slab 历史、SLUB 接手、SLOB 退场、kmem_cache、per-CPU cache、KASAN 集成。
【操作系统百科】用户态分配器
glibc malloc、tcmalloc、jemalloc、mimalloc 各有哲学。本文讲 arena、thread cache、size class、madvise 返还策略、碎片与 RSS 膨胀、如何根据负载选分配器。