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 就是这位“船长”的决策算法。
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 的设计并非为了“惩罚”某个进程,而是为了实现以下四个核心目标:
- 系统存活优先: 首要目标是防止内核 panic 或系统完全挂起。通过牺牲一个进程,换取整个操作系统的继续运行。
- 内存回收最大化: 选中的进程应能释放尽可能多的物理内存。通常,内存占用大的进程是首选目标。
- 系统损害最小化: 避免杀死关键系统进程(如 init、sshd、内核线程),防止系统陷入更糟的状态。
- 公平性与可预测性: 提供可配置的评分机制(oom_score_adj),允许管理员和用户影响选择结果,使行为可预测。
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(极易被杀死)。
评分公式简化模型:
实际内核实现更为复杂,但 RSS 始终是主导因素。
badness = (RSS 页面数) / (sqrt(运行时间秒数) + 1) + (oom_score_adj 调整值)实际内核实现更为复杂,但 RSS 始终是主导因素。
4.5 选择与执行流程
当 OOM Killer 被触发后,内核按照以下步骤执行:
- 加锁与检查: 获取
oom_lock,防止并发 OOM 操作。再次检查内存是否真的耗尽,避免误杀。 - 遍历进程树: 扫描所有进程(包括线程组),计算每个进程的
oom_badness分数。 - 选择目标: 选出分数最高的进程。如果多个进程分数相同,优先杀死子进程较少的进程。
- 发送 SIGKILL: 向目标进程发送
SIGKILL信号,该信号不可被捕获、阻塞或忽略。 - 等待回收: 内核等待目标进程退出,并回收其占用的内存资源。如果回收后内存仍不足,OOM Killer 会再次触发,杀死下一个进程。
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 → 回收内存。 |