4. OOM Killer机制

当系统物理内存耗尽且 Swap 空间也无法满足分配请求时,内核无法再通过常规的页面回收(Page Reclaim)或内存压缩(Memory Compaction)来腾出空间。此时,内核必须启动最后的保护机制——OOM Killer(Out-Of-Memory Killer),选择一个进程杀死以释放内存,保证系统整体继续运行。

核心原则:OOM Killer 不是 Bug,而是内核在极端情况下的“断路器”。理解其触发条件与选择策略,是排查内存泄漏和突发进程被杀问题的关键。

4.1 OOM 触发条件

OOM Killer 的触发并非瞬间发生,而是经过一系列水位检查与重试后的最终动作。主要触发路径如下:

  1. __alloc_pages_slowpath() 失败: 在慢速分配路径中,经过页面回收、Swap、Compaction 等所有尝试后,仍无法满足分配阶数(order)的内存请求。
  2. oom_killer_disable() 被调用: 在某些特定场景(如 cgroup 内存上限触发)下直接进入 OOM。
  3. 内核线程或关键服务分配失败: 系统内存严重碎片化或完全耗尽时,任何 GFP_KERNEL 分配都可能触发。

内核通过 oom_score_adj 和 oom_badness() 函数来决定“谁该被牺牲”。触发前的关键检查点如下表所示:

检查阶段 条件 动作
水位检查 所有内存域(Zone)低于最低水位(min watermark) 启动异步回收(kswapd)
直接回收 异步回收无法满足,进入直接回收(direct reclaim) 同步扫描 LRU 链表,尝试释放页面
Compaction 分配高阶内存(order > 0)时,页面碎片化严重 尝试移动页面以合并连续大块
OOM 判定 以上所有步骤均失败,且分配标志允许 OOM 调用 out_of_memory() 触发 killer
排查提示: 在 /proc/sys/vm/panic_on_oom 设置为 1 时,内核会直接 panic 而非触发 OOM Killer,这在生产环境调试中需要特别注意。
OOM Killer 触发流程 内存分配请求 水位检查 (min watermark) 充足 → 分配成功 直接回收 + Compaction 成功 → 分配 OOM 判定 (out_of_memory) 选择 victim 进程 (badness 评分)

4.2 Badness 评分算法

内核通过 oom_badness() 函数为每个进程计算一个“坏分值”,分值越高,被选为 victim 的可能性越大。评分算法综合考虑以下因素:

  • RSS (Resident Set Size): 进程实际占用的物理内存页数,是评分的主要基础。
  • Swap 占用: 进程占用的 Swap 空间大小,同样计入内存消耗。
  • 进程特权与硬件关联: root 权限进程、直接访问硬件(如 Xorg)的进程会获得较低评分。
  • oom_score_adj 调整值: 用户态可通过 /proc/<pid>/oom_score_adj 手动调整,范围 -1000 到 +1000。设置为 -1000 可完全豁免 OOM。

评分计算公式简化如下:

points = (rss_pages + swap_pages) * (1000 + oom_score_adj) / 1000

其中 rss_pages 和 swap_pages 以物理页框为单位。最终得分越高,越容易被选中。

影响因素 权重方向 说明
RSS + Swap 总量 正相关(越大越危险) 内存占用大户更容易被选中
oom_score_adj 可调(-1000 ~ +1000) 用户态可主动降低或提高被选概率
进程特权 (CAP_SYS_ADMIN) 负相关(降低评分) 系统管理进程受到保护
硬件关联 (CAP_SYS_RAWIO) 负相关(降低评分) 直接操作硬件的进程(如 Xorg)评分降低
子进程数量 正相关(略微增加) 拥有大量子进程的父进程评分略高
常见误区: 很多开发者认为 OOM Killer 总是杀死内存占用最大的进程。实际上,由于特权调整和 oom_score_adj 的存在,一个占用 2GB 的数据库进程可能比一个占用 1.5GB 的普通进程更安全。务必检查 /proc/<pid>/oom_score 和 oom_score_adj。
Badness 评分算法结构 RSS 物理内存 Swap 占用 oom_score_adj 特权/硬件关联 oom_badness() 评分计算引擎 points = (rss+swap) * (1000+adj)/1000 oom_score 最终评分 说明: 评分越高,被选为 victim 的概率越大。oom_score_adj 可设为 -1000 实现完全豁免。 特权进程(如 init、内核线程)评分会被大幅降低,避免误杀关键服务。

4.3 选择 Victim 进程策略

当 out_of_memory() 被调用后,内核遍历所有进程(排除内核线程、已退出进程、被豁免进程),通过 select_bad_process() 选择最终 victim。选择策略遵循以下优先级:

  1. 排除不可杀进程: 内核线程(PF_KTHREAD)、正在执行关键操作的进程(PF_EXITING)、被 oom_score_adj 设为 -1000 的进程直接跳过。
  2. 遍历所有进程: 调用 oom_badness() 计算每个进程的评分,记录当前最高分及其进程描述符。
  3. 选择最高分进程: 如果存在多个同分进程,优先选择子进程数量较少的进程(减少级联影响)。
  4. 发送 SIGKILL: 向选中的 victim 进程发送 SIGKILL 信号,并记录 OOM 日志到内核环形缓冲区。
关键日志查看: 当 OOM Killer 触发时,内核会输出类似以下信息到 dmesg 或 /var/log/kern.log:
[ 1234.567890] oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
[ 1234.567891] [ pid ]   uid  tgid total_vm      rss nr_ptes nr_pmds swapents oom_score_adj name
[ 1234.567892] [ 1234]  1000  1234   456789   234567    123      4     5678             0 myapp
[ 1234.567893] Out of memory: Killed process 1234 (myapp) total-vm:1827156kB, anon-rss:938268kB, file-rss:0kB, shmem-rss:0kB

选择策略的决策树如下表所示:

步骤 检查项 决策
1 进程是否为内核线程 (PF_KTHREAD) 是 → 跳过
2 进程是否正在退出 (PF_EXITING) 是 → 跳过
3 oom_score_adj == -1000 是 → 跳过(完全豁免)
4 计算 oom_badness() 评分 记录最高分进程
5 多个进程同分 选择子进程数最少的
6 最终选定 victim 发送 SIGKILL
生产环境建议: 对于关键业务进程(如数据库、Web 服务器),建议通过 systemd 的 OOMScoreAdjust= 或手动写入 /proc/<pid>/oom_score_adj 设置负值(如 -500),降低被 OOM Killer 选中的概率。同时监控 /proc/<pid>/oom_score 的实时变化。
Victim 进程选择决策树 遍历所有进程 是内核线程或 PF_EXITING? 跳过 (不可杀) oom_score_adj == -1000? 跳过 (豁免) 计算 oom_badness() 记录最高分进程 发送 SIGKILL 杀死进程 同分时优先选择子进程数最少的进程,以减少级联终止的影响。

4.4 实际排查案例

假设某 Java 应用服务器频繁被 OOM Killer 杀死,通过以下步骤定位问题:

  1. 查看 dmesg 日志: 确认被杀进程的 PID 和内存占用(RSS 和 Swap)。
  2. 检查 oom_score: cat /proc/<pid>/oom_score 确认评分是否异常高。
  3. 检查 oom_score_adj: cat /proc/<pid>/oom_score_adj 确认是否被其他服务或 systemd 调整过。
  4. 分析内存泄漏: 使用 top、ps aux 或 /proc/<pid>/smaps 观察 RSS 是否持续增长。
  5. 调整保护策略: 对关键进程设置 oom_score_adj = -500,降低被选概率。
重要提醒: 不要依赖 OOM Killer 作为内存管理的常规手段。频繁触发 OOM 说明系统存在严重的内存泄漏或配置不当。根本解决方案是修复内存泄漏、增加物理内存或优化内存使用策略。