K04 —— 内存耗尽时:OOM Killer 是怎么选择牺牲者的

金句:当内存已经耗尽,内核需要一个”裁判”来决定谁该离开——这就是 OOM Killer。


OOM Killer 的触发条件

当系统内存严重不足时,Linux 会调用 OOM Killer 来释放内存。触发条件不是”内存为零”,而是内存分配请求无法满足且直接回收也失败了。

Bash
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 评分),分数越高越容易被杀掉。

Bash
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)

Bash
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 的实际应用

Bash
调整 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 的内存限制

Bash
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 的统一层级

Bash
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不着急」,回复”资料”获取内存学习路线图。

最后修改: 2024年6月15日

作者

评论

发表评论

您的邮箱地址不会被公开。