在 Linux 系统中,当系统内存资源极度紧张时,内核会触发一种极端的回收机制——OOM(Out Of Memory)机制,选择并终止一个或多个进程以释放内存,从而保证系统核心功能的正常运行。本文将深入剖析 Linux 内核中 OOM 的触发条件、决策流程、核心参数配置,并结合源码与日志实例,为你提供一套完整的 OOM 问题分析与调优思路。
一、OOM机制概述
Linux 内核为了提高内存的使用效率,采用了过度分配内存(over-commit memory)的策略。这使得应用程序可以申请比物理内存总量更多的虚拟内存。当物理内存真的被消耗殆尽,且所有常规内存回收手段(如 kswapd、直接回收、内存规整)均告失败时,便会触发 OOM 机制。
该机制的核心任务是:监控并挑选出系统中占用内存过大(尤其是瞬间消耗大量内存)的“坏”进程,通过强制终止它来回收内存,防止整个系统因内存耗尽而僵死。
其基本工作流程如下:
- 触发:在内存分配路径上检查到内存不足,触发
out_of_memory()。
- 评选:对系统所有非特权进程进行“坏”程度打分(通过
oom_badness() 函数),选出得分最高的进程。
- 处决:杀死选中的进程及其相关线程。
- 善后:唤醒
oom_reaper 内核线程进行内存收割(如释放匿名页)。
- 控制:内核提供了丰富的 sysfs 接口(/proc/sys/vm/)和进程级参数(/proc/\<pid\>/)来精细控制 OOM 行为。
下面我们将从几个关键问题展开分析:
- OOM 是在什么条件下被触发的?
- 有哪些内核参数可以影响 OOM 的行为?
- 内核源码中的 OOM 流程是怎样的?
- 如何解读一个真实的 OOM 内核日志?
- 如何通过配置来保护关键进程或调整 OOM 策略?
二、OOM的触发路径
OOM 并非内存不足时的首选方案,它是最后一道防线。其触发主要源于内存分配失败。
1. 页面分配路径触发
这是最常见的触发场景。当 __alloc_pages_nodemask 分配物理页失败,进入慢速路径 __alloc_pages_slowpath,在尝试了内存回收、内存规整等手段后依然无法获得所需内存时,便会调用 __alloc_pages_may_oom 进入 OOM 处理流程。
alloc_pages
-> _alloc_pages
-> __alloc_pages_nodemask
-> __alloc_pages_slowpath # 内存回收/规整失败
-> __alloc_pages_may_oom # OOM入口检查
-> out_of_memory # OOM核心逻辑
-> select_bad_process # 选择“最坏”进程
-> oom_scan_process_thread
-> oom_badness # 计算进程“坏”程度
-> oom_kill_process # 杀死选中的进程
2. 缺页异常路径触发
另一种情况发生在处理缺页异常时。在 do_page_fault() 函数中,如果 handle_mm_fault() 返回了 VM_FAULT_OOM 错误,表明在处理缺页时遇到了内存不足,此时会跳转到 pagefault_out_of_memory(),并最终调用 out_of_memory()。
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long write, unsigned long mmu_meh)
{
...
good_area:
...
fault = handle_mm_fault(vma, address, write ? FAULT_FLAG_WRITE : 0);
if (unlikely(fault & VM_FAULT_ERROR)) {
if (fault & VM_FAULT_OOM) // 缺页处理中遇到OOM
goto out_of_memory;
else if (fault & VM_FAULT_SIGBUS)
goto do_sigbus;
else if (fault & VM_FAULT_SIGSEGV)
goto bad_area;
BUG();
}
...
out_of_memory:
pagefault_out_of_memory();
return;
...
}
三、影响OOM行为的内核参数
Linux 通过系统级 sysctl 参数和进程级 proc 参数两个维度,精细控制 OOM 的触发、评选和处决行为。
3.1 系统级核心行为控制参数
这类参数直接影响 OOM 触发后的宏观行为。
vm.panic_on_oom (/proc/sys/vm/panic_on_oom)
控制 OOM 发生时内核是否直接 panic(崩溃),而非杀进程。
0 (默认):正常执行进程查杀流程。
1:仅在全局 OOM时触发 panic。
2:无论全局或局部 OOM(如 cgroup 限制),均触发 panic。
其判断逻辑在内核的 check_panic_on_oom() 函数中:
static void check_panic_on_oom(struct oom_control *oc, enum oom_constraint constraint)
{
if (likely(!sysctl_panic_on_oom)) // panic_on_oom为0,直接退出,不触发panic
return;
if (sysctl_panic_on_oom != 2) { // panic_on_oom为1,仅处理全局OOM场景
if (constraint != CONSTRAINT_NONE)
return;
}
if (is_sysrq_oom(oc)) // sysrq手动触发的OOM不触发panic
return;
dump_header(oc, NULL);
panic("Out of memory: %s panic_on_oom is enabled\n",
sysctl_panic_on_oom == 2 ? "compulsory" : "system-wide");
}
vm.oom_kill_allocating_task (/proc/sys/vm/oom_kill_allocating_task)
当设置为非 0 时,OOM 会优先杀死触发本次内存分配的当前进程,跳过全系统进程扫描。这可以加快回收速度,但可能误杀“凶手”进程(它可能只是在执行一个必要的临时操作)。
bool out_of_memory(struct oom_control *oc)
{
...
if (!is_memcg_oom(oc) && sysctl_oom_kill_allocating_task &&
current->mm && !oom_unkillable_task(current, NULL, oc->nodemask) &&
current->signal->oom_score_adj != OOM_SCORE_ADJ_MIN) {
get_task_struct(current);
oc->chosen = current;
oom_kill_process(oc, "Out of memory (oom_kill_allocating_task)");
return true;
}
...
}
vm.oom_dump_tasks (/proc/sys/vm/oom_dump_tasks)
默认值为 1(开启)。开启时,OOM 现场会调用 dump_tasks() 打印系统所有可杀进程的详细信息(PID、UID、内存占用、oom_score_adj 等),是问题定位的关键。关闭可减少日志输出,适用于嵌入式等资源受限环境。
3.2 影响OOM触发时机的内存管理参数
这类参数决定了内核何时判定“内存不足”。
vm.overcommit_memory (/proc/sys/vm/overcommit_memory)
控制内存过度提交策略,是 OOM 是否频繁触发的根源。
0 (默认):启发式过度提交。内核会粗略检查,拒绝明显过分的申请。
1:总是允许过度提交。内存分配永不失败,完全依赖 OOM 来收拾残局,OOM 触发概率极高。
2:禁止过度提交。严格限制分配总量(swap + 物理内存 * overcommit_ratio/100),超限直接返回 ENOMEM,不会触发 OOM。
vm.min_free_kbytes (/proc/sys/vm/min_free_kbytes)
定义内核必须保留的最小空闲内存。当系统空闲内存低于由此计算出的 min 水位线时,会触发直接内存回收,失败则可能进入 OOM。调大此值会让内核更“警觉”,更早触发回收/OOM;调小则反之。
vm.watermark_scale_factor (/proc/sys/vm/watermark_scale_factor)
默认 10,范围 1-1000。它决定了 low 和 high 内存水位线与 min 水位线的间隔比例。调大该值会让后台回收线程 kswapd 更早启动工作,有助于平滑内存回收,降低突发 OOM 的概率。
3.3 进程级OOM优先级控制参数
位于 /proc/<pid>/ 目录下,用于调整单个进程在 OOM 评选中被选中的概率。
/proc/<pid>/oom_score_adj
这是核心调整参数,范围 -1000 到 1000,直接影响 oom_badness() 打分结果。
- -1000 (
OOM_SCORE_ADJ_MIN):进程完全免疫,不会被 OOM 杀死。
- 负值:降低进程得分,越负越不容易被杀。
- 正值:增加进程得分,越正越容易被杀。
- 0:默认值。
其作用体现在打分函数 oom_badness() 中:
unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg, const nodemask_t *nodemask, unsigned long totalpages)
{
...
adj = (long)p->signal->oom_score_adj; // 获取进程oom_score_adj
if (adj == OOM_SCORE_ADJ_MIN || // 取值-1000,直接返回0分,不参与评选
test_bit(MMF_OOM_SKIP, &p->mm->flags) ||
in_vfork(p)) {
task_unlock(p);
return 0;
}
...
adj *= totalpages / 1000; // 将adj归一化为系统总内存比例
points += adj; // 叠加到进程基础得分中
...
}
计算公式可简化为:最终得分 = 进程内存占用基础得分 + oom_score_adj * 系统总内存页数 / 1000。因此,oom_score_adj 每调整 1,相当于让进程多占用或少占用系统总内存的 1/1000。
/proc/<pid>/oom_adj
旧版参数,范围 -17 到 15,仅为兼容性保留。内核会自动按 oom_score_adj = oom_adj * 1000 / 17 进行换算。
/proc/<pid>/oom_score (只读)
该文件显示由 oom_badness() 计算出的进程当前 OOM 得分(归一化到 0-1000 之间)。得分越高,在 OOM 时越容易被选中。
四、OOM核心代码流程分析
4.1 数据结构与入口
OOM 控制信息封装在 struct oom_control 中:
struct oom_control {
struct zonelist *zonelist;
nodemask_t *nodemask;
struct mem_cgroup *memcg; // 发生OOM的内存控制组,NULL表示全局OOM
const gfp_t gfp_mask; // 触发OOM时分配内存的掩码
const int order; // 申请内存的order大小
unsigned long totalpages; // 系统总页数(用于计算)
struct task_struct *chosen; // 被选中的进程
unsigned long chosen_points; // 被选中进程的得分
};
OOM 的入口函数 __alloc_pages_may_oom 会进行一系列检查(如是否允许失败、order 是否过大等),最终调用核心函数 out_of_memory()。
4.2 评选“最坏”进程:select_bad_process() 与 oom_badness()
out_of_memory() 会调用 select_bad_process()。该函数遍历所有进程,通过 oom_evaluate_task() 对每个进程调用 oom_badness() 进行打分,并记录得分最高的进程。
oom_badness() 是 OOM 机制的灵魂。它计算一个进程有多“坏”:
- 基础分:基于进程实际消耗的内存。
- 包括 RSS(驻留内存)、Swap 占用、页表(PTE/PMD)占用。
- 计算:
points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) + atomic_long_read(&p->mm->nr_ptes) + mm_nr_pmds(p->mm);
- 特权修正:root 进程获得 3% 的“优惠”(
points -= (points * 3) / 100;)。
- 权重调整:加上由
oom_score_adj 决定的影响值(points += adj;)。
因此,一个进程的 OOM 得分直观反映了它对系统内存资源的“负担”,并可通过 oom_score_adj 进行人工加权。
4.3 处决进程与内存收割:oom_kill_process() 与 oom_reaper
选中进程后,oom_kill_process() 负责执行“死刑”:
- 通过
dump_header() 打印详细的 OOM 现场信息(触发进程、内存状态、所有进程列表)。
- 向选中的进程(
victim)发送 SIGKILL 信号。
- 标记进程为
TIF_MEMDIE,并唤醒 oom_reaper 内核线程。
oom_reaper 会异步地、快速地释放被杀死进程占用的匿名内存等资源,加速内存回收。
五、实战:解读一个OOM内核日志
以下是来自一个真实系统的 OOM 日志片段,我们结合上文知识进行分析:
1. [19174.926798] copy invoked oom-killer: gfp_mask=0x24200c8(GFP_USER|__GFP_MOVABLE), nodemask=0, order=0, oom_score_adj=0
解读:进程 copy 在申请内存时触发了 OOM。分配掩码包含 GFP_USER 和 __GFP_MOVABLE,申请 order 为 0(即 1 个页),该进程的 oom_score_adj 为默认值 0。
2. [19174.937586] CPU: 0 PID: 163 Comm: copy Not tainted 4.9.56 #1
[19174.943274] Call Trace:
[<802f63c2>] dump_stack+0x1e/0x3c
[<80132224>] dump_header.isra.6+0x84/0x1a0
[<800f2d68>] oom_kill_process+0x23c/0x49c
[<800f32fc>] out_of_memory+0xb0/0x3a0
[<800f7834>] __alloc_pages_nodemask+0xa84/0xb5c
[<801306b8>] alloc_migrate_target+0x34/0x6c
[<8012f30c>] migrate_pages+0x108/0xbe4
[<800f8a0c>] alloc_contig_range+0x188/0x378
[<80130c54>] cma_alloc+0x100/0x220
...
解读:调用栈显示,OOM 的触发源于 CMA(连续内存分配器)的分配请求 cma_alloc(),这通常是为特定硬件(如 GPU、DSP)预留大块连续物理内存时发生的。
3. [19175.001223] Mem-Info:
active_anon:99682 inactive_anon:12 isolated_anon:1
active_file:55 inactive_file:75 isolated_file:0
unevictable:0 dirty:0 writeback:0 unstable:0
slab_reclaimable:886 slab_unreclaimable:652
mapped:2 shmem:91862 pagetables:118 bounce:0
free:592 free_pcp:61 free_cma:0
Node 0 active_anon:398728kB ... shmem:367448kB ...
Normal free:2368kB min:2444kB low:3052kB high:3660kB ...
解读:系统内存状态告急。关键数据:
free:592 (页) ≈ 2.3MB,空闲内存极少。
active_anon:398728kB ≈ 389MB,活跃匿名内存很多。
shmem:367448kB ≈ 359MB,共享内存(tmpfs/shmem)占用巨大。这是重要的线索,可能是有进程往 /dev/shm 或内存文件系统写入了大量数据。
4. [19175.125942] [ pid ] uid tgid total_vm rss nr_ptes nr_pmds swapents oom_score_adj name
[19175.134514] [ 135] 0 135 1042 75 4 0 0 -1000 sshd
[19175.143070] [ 146] 0 146 597 141 3 0 0 0 autologin
[19175.152057] [ 147] 0 147 608 152 4 0 0 0 sh
[19175.160434] [ 161] 0 161 109778 7328 104 0 0 0 xxxxx
解读:系统进程内存快照。注意进程 161 (xxxxx) 的 total_vm 很大(约 429MB),rss 约为 28.6MB,且 oom_score_adj=0。
5. [19175.169068] Out of memory: Kill process 161 (xxxxx) score 39 or sacrifice child
6. [19175.176439] Killed process 161 (xxxxx) total-vm:439112kB, anon-rss:29304kB, file-rss:8kB, shmem-rss:0kB
解读:OOM 机制最终选择杀死了进程 161 (xxxxx),其得分为 39。被杀后显示其内存占用细节:匿名内存约 28.6MB,文件缓存几乎无,共享内存为 0。
分析总结:
这个案例中,虽然进程 161 的 RSS 并非最高,但结合其 total_vm 较大、oom_score_adj 为 0 以及其他进程(如 sshd)被设置为 -1000 免疫,它成为了 OOM 的牺牲品。根本原因可能是系统大量使用共享内存(shmem 高达 359MB),导致物理内存被快速耗尽。解决方案可能需要:1) 优化使用共享内存的应用程序;2) 适当调整触发 OOM 的内存水位线参数;3) 为更重要的服务进程设置 oom_score_adj = -1000 进行保护。
六、OOM调优与防护配置
6.1 系统级调优
- 调整水位线:适当增加
vm.min_free_kbytes 和 vm.watermark_scale_factor,让内核更早启动后台回收,避免突然陷入 OOM。
- 谨慎使用 overcommit:生产环境慎用
vm.overcommit_memory = 1,推荐默认值 0 或严格模式 2(需合理设置 overcommit_ratio)。
- 控制日志输出:内存紧张环境下,可设置
vm.oom_dump_tasks = 0 减少 OOM 时的日志 I/O 压力。
6.2 进程级防护
理解 Linux 的 内存管理 机制是进行有效调优的基础。OOM 虽然是最后的保障,但通过合理的配置和监控,我们可以将其对系统稳定性的冲击降到最低,确保关键业务持续运行。
原文信息
原作者:ArnoldLu
原文地址:https://www.cnblogs.com/arnoldlu/p/8567559.html