4. OOM Killer 机制:OOM 触发条件、badness 评分算法、选择 victim 进程策略
当系统物理内存耗尽且 Swap 空间也无法满足分配请求时,内核无法再通过常规的页面回收(Page Reclaim)或内存压缩(Memory Compaction)来腾出空间。此时,内核必须启动最后的保护机制——OOM Killer(Out-Of-Memory Killer),选择一个进程杀死以释放内存,保证系统整体继续运行。
核心原则:OOM Killer 不是 Bug,而是内核在极端情况下的“断路器”。理解其触发条件与选择策略,是排查内存泄漏和突发进程被杀问题的关键。
4.1 OOM 触发条件
OOM Killer 的触发并非瞬间发生,而是经过一系列水位检查与重试后的最终动作。主要触发路径如下:
- __alloc_pages_slowpath() 失败: 在慢速分配路径中,经过页面回收、Swap、Compaction 等所有尝试后,仍无法满足分配阶数(order)的内存请求。
- oom_killer_disable() 被调用: 在某些特定场景(如 cgroup 内存上限触发)下直接进入 OOM。
- 内核线程或关键服务分配失败: 系统内存严重碎片化或完全耗尽时,任何 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,这在生产环境调试中需要特别注意。
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。
4.3 选择 Victim 进程策略
当 out_of_memory() 被调用后,内核遍历所有进程(排除内核线程、已退出进程、被豁免进程),通过 select_bad_process() 选择最终 victim。选择策略遵循以下优先级:
- 排除不可杀进程: 内核线程(
PF_KTHREAD)、正在执行关键操作的进程(PF_EXITING)、被oom_score_adj设为 -1000 的进程直接跳过。 - 遍历所有进程: 调用
oom_badness()计算每个进程的评分,记录当前最高分及其进程描述符。 - 选择最高分进程: 如果存在多个同分进程,优先选择子进程数量较少的进程(减少级联影响)。
- 发送 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 的实时变化。
4.4 实际排查案例
假设某 Java 应用服务器频繁被 OOM Killer 杀死,通过以下步骤定位问题:
- 查看 dmesg 日志: 确认被杀进程的 PID 和内存占用(RSS 和 Swap)。
- 检查 oom_score:
cat /proc/<pid>/oom_score确认评分是否异常高。 - 检查 oom_score_adj:
cat /proc/<pid>/oom_score_adj确认是否被其他服务或 systemd 调整过。 - 分析内存泄漏: 使用
top、ps aux或/proc/<pid>/smaps观察 RSS 是否持续增长。 - 调整保护策略: 对关键进程设置
oom_score_adj = -500,降低被选概率。
重要提醒: 不要依赖 OOM Killer 作为内存管理的常规手段。频繁触发 OOM 说明系统存在严重的内存泄漏或配置不当。根本解决方案是修复内存泄漏、增加物理内存或优化内存使用策略。