4. OOM Killer 概述:什么是 OOM Killer、触发条件、设计目标与核心思想

4.1 什么是 OOM Killer

OOM Killer(Out-Of-Memory Killer)是 Linux 内核内存管理子系统中的一个关键安全机制。当系统物理内存(包括 RAM 和 Swap)被耗尽,且内核无法通过常规回收手段(如 kswapd、直接页面回收)释放足够内存时,OOM Killer 会被激活,强制终止一个或多个进程,以释放内存并恢复系统的可用性。

OOM Killer 并非一个独立的守护进程,而是内嵌在页分配器(Page Allocator)路径中的一段逻辑。它位于 mm/oom_kill.c 文件中,是内核应对内存耗尽危机的最后一道防线。

核心定义: OOM Killer 是内核在内存资源彻底枯竭时,通过牺牲选中的进程来换取系统整体存活的“断尾求生”机制。
类比理解: 想象一艘即将沉没的船,所有救生艇都已满载。船长(内核)必须决定将部分乘客(进程)抛下船,以减轻重量,防止整艘船沉没。OOM Killer 就是这位“船长”的决策算法。
OOM Killer 在内存分配路径中的位置 OOM Killer 在内存分配路径中的位置 用户态进程 malloc() 页分配器分配内存 成功 分配成功 失败 内存回收 (kswapd) 回收不足 OOM Killer 触发 选中进程被终止

4.2 触发条件

OOM Killer 的触发并非偶然,它遵循严格的条件判断。只有当系统内存压力达到临界阈值,且所有常规回收手段均告失败时,内核才会调用 out_of_memory() 函数。

触发条件 详细说明 内核函数/路径
1. 内存分配失败 页分配器在低水位线(Low Watermark)以下无法通过直接回收(Direct Reclaim)或异步回收(kswapd)获得所需内存页。 __alloc_pages_slowpath()
2. 回收机制失效 kswapd 内核线程已尽力回收,但内存压力持续上升;直接页面回收(shrink_node)也无法释放足够内存。 shrink_node()
3. 超过容忍时间 系统持续处于内存不足状态,且分配请求的优先级(Allocation Priority)较高,不允许无限等待。 oom_scan_process_thread()
4. 非原子性上下文 触发 OOM Killer 的分配请求不能处于原子上下文(如中断处理程序),因为原子上下文不允许睡眠等待。 __GFP_ATOMIC 标志检查
5. cgroup 内存限制 当进程所属的 cgroup 内存使用量达到 memory.max 上限,且无法通过回收释放内存时,cgroup OOM 被触发。 mem_cgroup_out_of_memory()
关键误区: 系统存在 Swap 空间并不意味着不会触发 OOM Killer。当 Swap 也被耗尽,或者进程被配置为禁止 Swap(通过 mlock() 或 MADV_DONTFORK 等),OOM Killer 依然会被激活。

4.3 设计目标

OOM Killer 的设计并非为了“惩罚”某个进程,而是为了实现以下四个核心目标:

  1. 系统存活优先: 首要目标是防止内核 panic 或系统完全挂起。通过牺牲一个进程,换取整个操作系统的继续运行。
  2. 内存回收最大化: 选中的进程应能释放尽可能多的物理内存。通常,内存占用大的进程是首选目标。
  3. 系统损害最小化: 避免杀死关键系统进程(如 init、sshd、内核线程),防止系统陷入更糟的状态。
  4. 公平性与可预测性: 提供可配置的评分机制(oom_score_adj),允许管理员和用户影响选择结果,使行为可预测。
OOM Killer 设计目标四象限 OOM Killer 设计目标四象限 系统存活优先 避免内核 panic 保持操作系统可响应 内存回收最大化 优先杀死大内存进程 释放更多物理页框 系统损害最小化 保护 init、内核线程 避免级联故障 公平性与可预测性 oom_score_adj 可调 行为可审计、可复现 OOM

4.4 核心思想:坏蛋评分(Badness Score)

OOM Killer 的核心思想是 “坏蛋评分”(Badness Score)。内核为每个进程计算一个分数,分数越高,代表该进程“罪孽越深重”,越容易被选中杀死。这个评分机制定义在 oom_badness() 函数中。

评分的主要计算因素包括:

  • RSS(Resident Set Size): 进程实际占用的物理内存大小。这是最主要的权重因子,内存占用越大,分数越高。
  • 进程运行时间: 运行时间较短的进程(如刚启动的临时任务)比长期运行的服务更容易被选中。
  • 进程优先级(Nice Value): 低优先级(高 Nice 值)的进程分数更高。
  • 特权进程豁免: 内核线程、init 进程(PID 1)、以及被 oom_score_adj 设置为 -1000 的进程会被豁免。
  • oom_score_adj 调整值: 用户空间可以通过 /proc/[pid]/oom_score_adj 接口手动调整分数,范围从 -1000(完全豁免)到 +1000(极易被杀死)。
评分公式简化模型:
badness = (RSS 页面数) / (sqrt(运行时间秒数) + 1) + (oom_score_adj 调整值)
实际内核实现更为复杂,但 RSS 始终是主导因素。

4.5 选择与执行流程

当 OOM Killer 被触发后,内核按照以下步骤执行:

  1. 加锁与检查: 获取 oom_lock,防止并发 OOM 操作。再次检查内存是否真的耗尽,避免误杀。
  2. 遍历进程树: 扫描所有进程(包括线程组),计算每个进程的 oom_badness 分数。
  3. 选择目标: 选出分数最高的进程。如果多个进程分数相同,优先杀死子进程较少的进程。
  4. 发送 SIGKILL: 向目标进程发送 SIGKILL 信号,该信号不可被捕获、阻塞或忽略。
  5. 等待回收: 内核等待目标进程退出,并回收其占用的内存资源。如果回收后内存仍不足,OOM Killer 会再次触发,杀死下一个进程。
OOM Killer 选择与执行流程图 OOM Killer 选择与执行流程图 1. 获取 oom_lock,确认内存耗尽 2. 遍历进程树,计算 oom_badness 3. 选择分数最高的进程 4. 发送 SIGKILL 信号 5. 等待进程退出,回收内存 内存足够? 是 系统恢复 否 再次触发 OOM

4.6 内核日志与观察

当 OOM Killer 被触发时,内核会在 /var/log/messages 或 /var/log/kern.log 中输出详细的日志信息。通过分析这些日志,可以快速定位问题。

典型的 OOM Killer 日志片段如下:

invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
[ pid ]   uid  tgid total_vm      rss nr_ptes nr_pmds swapents oom_score_adj name
[  123]     0   123    45678     2345      28       3      100             0 my_app
[  456]  1000   456    12345      678      15       2       50          -100 sshd
[  789]     0   789    23456     1234      20       2       80             0 systemd-journal
Out of memory: Killed process 123 (my_app) total-vm:182712kB, anon-rss:9380kB, file-rss:0kB, shmem-rss:0kB, UID:0 pgtables:112kB oom_score_adj:0
日志解读要点:
  • invoked oom-killer: 表明 OOM Killer 被触发。
  • gfp_mask: 显示触发 OOM 的内存分配请求标志。
  • total_vm / rss: 进程的虚拟内存和物理内存占用。
  • oom_score_adj: 用户空间的调整值。
  • Killed process: 最终被选中的进程 PID 和名称。

4.7 本章小结

概念 要点总结
OOM Killer 本质 内核内存耗尽时的最后一道防线,通过杀死进程释放内存。
触发条件 内存分配失败 + 常规回收无效 + 非原子上下文。
设计目标 系统存活 > 内存回收 > 损害最小化 > 可预测性。
核心思想 基于 RSS 的坏蛋评分机制,辅以 oom_score_adj 调整。
执行流程 加锁 → 遍历评分 → 选择目标 → 发送 SIGKILL → 回收内存。