K04 — When Memory Is Exhausted: How the OOM Killer Chooses Its Victims
Key Insight: When memory is exhausted, the kernel needs a “referee” to decide who should leave. That is the OOM Killer.
OOM Killer Trigger Conditions
When system memory is critically low, Linux invokes the OOM Killer. The trigger is not “memory is zero”, but that an allocation request cannot be satisfied and direct reclaim has also failed.
OOM Killer 的触发流程:
1. 内存分配请求(GFP_KERNEL 等)失败
2. 调用直接回收(direct reclaim)
3. 直接回收释放的内存仍然不够
4. 分配请求返回 ENOMEM
5. 内核检查:是否应该调用 OOM Killer?
关键函数:
- out_of_memory() // 决定是否触发 OOM
- select_bad_process() // 选择最"坏"的进程
- oom_kill() // 发送 SIGKILL
触发条件(检查 watermark):
- 如果 free_pages < pages_min(最低水位)
- 并且 direct reclaim 失败
- 则触发 OOM
注意:kswapd 异步回收时可能不会触发 OOM,
只有 direct reclaim(同步回收)失败时才触发。Choosing the Victim: oom_score
The core of OOM Killer’s process selection is oom_score. Higher scores make a process more likely to be killed.
oom_score 的计算因素:
1. 内存消耗(RSS + Swap)
- 消耗越多,分数越高
- 这是最重要的因素
2. oom_score_adj(用户可调)
- /proc/PID/oom_score_adj
- 范围:-1000 到 +1000
- 调整方法:
$ echo -500 > /proc/1234/oom_score_adj // 降低 500 分
- -1000 = 完全免疫(不会被选中)
- +1000 = 必定被选中(优先杀掉)
3. 进程nice值
- nice 值越低(优先级越高),越不容易被选
- nice 值越高(优先级越低),越容易被选
4. 其他因素
- 进程是 root 用户还是普通用户
- 进程存活时间(老进程可能被降低分数)
- 进程是否刚刚被 OOM 过(cooldown)oom_score 计算公式(简化):
points = RSS / PAGE_SIZE; // 基本分数 = 内存页数
if (is_root_process) points /= 2; // root 用户减半
if (is_kernel_thread) points = 0; // 内核线程不选
points += oom_score_adj; // 加上用户调整
限制:
- 最小分数:1(至少要有点分数)
- 最大分数:1000 * 1000(防止溢出)
最终选择:
- 选择 points 最高的进程
- 发送 SIGKILL(不可捕获)Practical Applications of oom_score_adj
调整 OOM 评分的场景:
场景 1:保护重要服务
Web 服务器(nginx):
$ echo -1000 > /proc/$(pidof nginx)/oom_score_adj // 免疫
场景 2:让某个进程更容易被选
测试程序,故意让它被 OOM:
$ echo 1000 > /proc/$(pidof test-prog)/oom_score_adj
场景 3:容器(container)环境
Docker 默认把 oom_score_adj 设为 0
但容器有自己的 cgroup 内存限制
- 当 cgroup 内存耗尽,cgroup 自己的 OOM Killer 会选进程
Docker 的 OOM 处理:
docker run --oom-kill-disable=true ...
→ 禁用容器的 OOM Killer
→ 但如果主机内存耗尽,主机 OOM Killer 仍会工作Memory Limits in cgroup v1
cgroup v1 内存限制:
控制组(cgroup)可以限制进程组的内存使用:
# 创建 memory cgroup
mkdir /sys/fs/cgroup/memory/myapp
echo 1G > /sys/fs/cgroup/memory/myapp/memory.limit_in_bytes
echo 1G > /sys/fs/cgroup/memory/myapp/memory.soft_limit_in_bytes
# 加入进程
echo $PID > /sys/fs/cgroup/memory/myapp/tasks
限制参数:
memory.limit_in_bytes:硬限制(超过触发 OOM)
memory.soft_limit_in_bytes:软限制(kswapd 回收)
memory.swappiness:换出策略
memory.oom_control:OOM 行为配置
当 cgroup 内存超限时:
- 先尝试 soft limit + kswapd 回收
- 如果超过 hard limit,触发 cgroup 内的 OOM Killer
- cgroup 内的 OOM Killer 只杀该 cgroup 内的进程cgroup v2 Unified Hierarchy
cgroup v2(统一层级)内存限制:
cgroup v2 使用统一的树形结构:
/sys/fs/cgroup/
├── myapp/ // 控制组
│ ├── memory.max // 内存上限
│ ├── memory.low // 低水位(kswapd 优先回收)
│ └── memory.current // 当前使用
└── system.slice/ // 系统服务
设置内存限制:
echo 1G > /sys/fs/cgroup/myapp/memory.max
echo 100M > /sys/fs/cgroup/myapp/memory.low
cgroup v2 的 OOM 行为:
- memory.oom_group = 1(整个 cgroup 一起 OOM)
- 或者单独每个进程 OOM(默认)
查看 OOM 状态:
$ cat /sys/fs/cgroup/myapp/memory.events
low 0
max 1234
oom 567
oom_kill 1Summary
- OOM Trigger: direct reclaim failure + free_pages < pages_min → out_of_memory()
- oom_score: RSS + nice + oom_score_adj (-1000 to +1000); highest score is selected
- oom_score_adj: User-adjustable; -1000 = immune; +1000 = priority kill
- cgroup v1: memory.limit_in_bytes (hard limit) / soft_limit (soft limit); exceeding triggers cgroup OOM
- cgroup v2: memory.max (upper limit) / memory.low (low watermark); unified hierarchy; simpler
Next (K05): How is physical memory used step by step through upper-layer abstractions? From page cache to user-space mmap to underlying allocators.
Comments