K04 —— 内存耗尽时:OOM Killer 是怎么选择牺牲者的
金句:当内存已经耗尽,内核需要一个”裁判”来决定谁该离开——这就是 OOM Killer。
OOM Killer 的触发条件
当系统内存严重不足时,Linux 会调用 OOM Killer 来释放内存。触发条件不是”内存为零”,而是内存分配请求无法满足且直接回收也失败了。
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(同步回收)失败时才触发。选择牺牲者:oom_score
OOM Killer 选择进程的核心是 oom_score(OOM 评分),分数越高越容易被杀掉。
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(不可捕获)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 仍会工作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 的统一层级
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 1总结
- OOM 触发:direct reclaim 失败 + free_pages < pages_min → out_of_memory()
- oom_score:RSS + nice + oom_score_adj(-1000~+1000),分数最高被选
- oom_score_adj:用户可调,-1000=免疫,+1000=优先杀
- cgroup v1:memory.limit_in_bytes(硬限制)/ soft_limit(软限制),超限触发 cgroup OOM
- cgroup v2:memory.max(上限)/ memory.low(低水位),统一层级,更简单
下篇预告(K05):物理内存是如何被上层抽象一步步使用的?从 page cache 到用户空间的 mmap,再到底层分配器的完整链路。
关注公众号「AI不着急」,回复”资料”获取内存学习路线图。
评论