从
alloc_pages耗尽到out_of_memory被调用的这条链路,隐藏着内核最后一丝自救的挣扎。本文揭开 OOM 触发机制、Memory Reserve 的 32 页救命钱、以及 out_of_memory 全局入口的决策内幕。
在上一篇中,我们从宏观角度看到了 OOM 全景链:alloc_pages 耗尽 → Memory Reserve + 8 阶段回收 → out_of_memory 进入决策 → panic/reaper/kill 三分支。现在我们深入到每一个环节的实现细节,追踪每一条调用链,看看内核到底是怎么决定"我快死了"的。
零、关键数据结构:struct oom_control
在分析具体函数之前,我们先了解一个核心数据结构——struct oom_control。它贯穿整个 OOM 决策链,负责在函数间传递上下文信息。
从 __alloc_pages_may_oom 的源码可以看到,当触发 OOM 时,内核会构造一个 oom_control 结构体:
// mm/page_alloc.c
static inline struct page *
__alloc_pages_may_oom(gfp_t gfp_mask, unsigned int order,
const struct alloc_context *ac, unsigned long *did_some_progress)
{
struct oom_control oc = {
.zonelist = ac->zonelist, // 本次分配请求的zonelist
.nodemask = ac->nodemask, // 允许访问的NUMA节点掩码
.memcg = NULL, // 当前触发点没有memcg上下文
.gfp_mask = gfp_mask, // 原始GFP分配掩码
.order = order, // 分配的页面等级(order)
};
struct page *page;
...
}这个 oc 会被传递到 out_of_memory(),再传到 select_bad_process()、oom_kill_process() 等函数,贯穿整个 OOM 决策链。其中关键字段:
zonelist/nodemask:内存分配的 NUMA 路由信息gfp_mask:决定了 OOM 过程中是否允许 IO、是否允许文件系统操作constraint:后续在out_of_memory中会根据constrained_alloc()设置,标识这是否是一次受约束的分配(cpuset/mempolicy/memcg)chosen:最终被选中的 OOM victim 进程(task_struct *)
一、三条触发路径:从不同位置引爆 OOM
out_of_memory(gfp_mask, order, nodemask) 这个全局入口被谁调用——只有三条明确的路径:
路径 1:__alloc_pages_may_oom → out_of_memory(最常见)
这是最常见的 OOM 触发方式。当用户进程或内核路径调用 alloc_pages(GFP_KERNEL, order) 时,经历了 8 阶段回收(直接回收 → 唤醒 kswapd → 压缩 → shrink_slab → shrink_page_list)全部失败后,内核走到 __alloc_pages_may_oom(zonelist, gfp_mask, order, nodemask)。
这个函数做的事情就是检查是否还有最后一丝希望。以下是关键的判断逻辑:
// mm/page_alloc.c
static inline struct page *
__alloc_pages_may_oom(gfp_t gfp_mask, unsigned int order,
const struct alloc_context *ac, unsigned long *did_some_progress)
{
struct oom_control oc = { ... }; // 构造 oom_control 上下文
// 1. 获取 OOM 锁(互斥锁)
// 如果获取失败,说明其他CPU正在做OOM处理,等待即可
if (!mutex_trylock(&oom_lock)) {
*did_some_progress = 1;
schedule_timeout_uninterruptible(1);
return NULL;
}
// 2. 再尝试一次分配,保持 HIGH watermark
// 这是为了处理"并行OOM杀进程后释放内存"的情况
page = get_page_from_freelist((gfp_mask | __GFP_HARDWALL) & ~__GFP_DIRECT_RECLAIM,
order, ALLOC_WMARK_HIGH|ALLOC_CPUSET, ac);
if (page)
goto out;
// 3. 以下情况直接退出,不触发OOM:
if (current->flags & PF_DUMPCORE) // coredump会耗尽所有保留内存
goto out;
if (order > PAGE_ALLOC_COSTLY_ORDER) // OOM不帮助高阶分配
goto out;
if (gfp_mask & (__GFP_RETRY_MAYFAIL | __GFP_THISNODE)) // 允许失败的分配
goto out;
if (ac->highest_zoneidx < ZONE_NORMAL) // 低zone不需要OOM
goto out;
if (pm_suspended_storage()) // 存储设备挂起时跳过
goto out;
// 4. 走到这里,所有回收手段已耗尽,触发 OOM
if (out_of_memory(&oc) || WARN_ON_ONCE(gfp_mask & __GFP_NOFAIL)) {
*did_some_progress = 1;
// 如果是GFP_NOFAIL,分配器需要兜底
if (gfp_mask & __GFP_NOFAIL)
page = __alloc_pages_cpuset_fallback(gfp_mask, order, ALLOC_NO_WATERMARKS, ac);
}
out:
mutex_unlock(&oom_lock);
return page;
}这段代码揭示了几个重要细节:
OOM 锁机制:
oom_lock是一个互斥锁,保证同一时间只有一个 CPU 执行 OOM。如果另一个 CPU 已经在做 OOM,当前 CPU 只是等待 1 tick,然后重新尝试分配。再次尝试分配:进入
__alloc_pages_may_oom后,先用ALLOC_WMARK_HIGH(高水位线)再尝试一次分配——这是为了处理并行 OOM 杀进程后可能已经释放了内存的情况。多种豁免条件:coredump、高阶分配、允许失败的分配、低 zone 分区、存储挂起等情况会跳过 OOM,直接返回失败。
OOM 成功后的处理:如果
out_of_memory()返回 true,说明有进程被杀或有内存释放。对于GFP_NOFAIL的分配,此时会从保留水位(ALLOC_NO_WATERMARKS)强制分配。
路径 2:mem_cgroup_out_of_memory → out_of_memory(MemCG 上限)
当某个 cgroup 的内存使用达到 memory.max 限制时,内核会先进入 cgroup 自己的 OOM 处理——调用 mem_cgroup_out_of_memory()。
这个函数会先尝试唤醒 memcg 内进程的 oom_reaper(cgroup 专属的 reaper),通过解除映射释放部分页面。如果解除映射后内存仍然不足,它会进一步调用全局的 out_of_memory(),此时 OOM 决策就扩散到了全系统范围——系统可以选择杀死同组或甚至其他 cgroup 的进程来腾出内存。
路径 3:pagefault_out_of_memory → out_of_memory(缺页错误)
用户进程访问了未映射的虚拟地址,触发缺页错误。内核尝试分配并映射页面,但发现内存耗尽。此时 pagefault_out_of_memory() 直接调用 out_of_memory(),将此次缺页错误作为触发源。
注意:这三条路径最终汇聚到同一个
out_of_memory(gfp_mask, order, nodemask)全局入口,这意味着无论哪种触发,都会经过同一套决策逻辑——但gfp_mask不同,影响后续是否允许 swap、允许 IO 等。
二、Memory Reserve 检查链:gfp_pfmemalloc_allowed 到 oom_reserves_allowed
这是 OOM 流程中最容易被忽略的关键环节。当系统内存耗尽时,内核还会保留每个 NUMA 节点 32 页的物理内存池,称为 OOM Reserve。它的作用是:即使系统已经处于 OOM 状态,内核仍然有能力分配少量页面来执行紧急操作(发送 SIGKILL、解除映射、记录调试信息等)。
Memory Reserve 检查的调用链
oom_reserves_allowed() 被 __gfp_pfmemalloc_flags 调用,而后者又被 gfp_pfmemalloc_allowed 调用——这条链路在内存分配的最后时刻被触发:
gfp_pfmemalloc_allowed()
→ __gfp_pfmemalloc_flags()
→ oom_reserves_allowed()
→ 检查 Reserve 是否耗尽
→ 耗尽 → panic("OOM")
→ 未耗尽 → 返回保留页面具体来说(源码来自 mm/page_alloc.c):
gfp_pfmemalloc_allowed(gfp_t gfp_mask):入口函数,判断 GFP 标志是否需要 Pfmalloc 权限
bool gfp_pfmemalloc_allowed(gfp_t gfp_mask)
{
return !!__gfp_pfmemalloc_flags(gfp_mask);
}__gfp_pfmemalloc_flags(gfp_t gfp_mask):核心逻辑,判断是否可以从 OOM Reserve 分配页面,调用oom_reserves_allowed()
static inline int __gfp_pfmemalloc_flags(gfp_t gfp_mask)
{
if (unlikely(gfp_mask & __GFP_NOMEMALLOC))
return 0;
if (gfp_mask & __GFP_MEMALLOC)
return ALLOC_NO_WATERMARKS;
if (in_serving_softirq() && (current->flags & PF_MEMALLOC))
return ALLOC_NO_WATERMARKS;
if (!in_interrupt()) {
if (current->flags & PF_MEMALLOC)
return ALLOC_NO_WATERMARKS;
else if (oom_reserves_allowed(current))
return ALLOC_OOM;
}
return 0;
}oom_reserves_allowed(struct task_struct *tsk):核心检查,判断当前线程是否有 Pfmalloc 权限
static bool oom_reserves_allowed(struct task_struct *tsk)
{
if (!tsk_is_oom_victim(tsk))
return false;
/*
* !MMU doesn't have oom reaper so give access to memory reserves
* only to the thread with TIF_MEMDIE set
*/
if (!IS_ENABLED(CONFIG_MMU) && !test_thread_flag(TIF_MEMDIE))
return false;
return true;
}调用链分析:gfp_pfmemalloc_allowed 调用 __gfp_pfmemalloc_flags 时传入 GFP 掩码,如果 current 是 OOM 受害者且系统不是无 MMU,则调用 oom_reserves_allowed(current) 检查当前线程是否允许访问 Memory Reserve。如果允许,返回 ALLOC_OOM,系统从保留池中分配少量页面执行紧急操作(如发送 SIGKILL)。
Memory Reserve 的生死意义
32 页保留量:每个节点大约 128KB(32页 × 4KB),虽然不多,但足够执行发送 SIGKILL、解除映射、记录日志这些紧急操作。
耗尽即崩溃:如果所有节点的 Reserve 都耗尽了,内核会直接
panic("OOM")导致整个系统崩溃,不再尝试 kill 任何进程。这是 panic_on_oom 之外的另一种"直接崩溃"机制:即使没有配置
panic_on_oom,只要 Reserve 耗尽,系统一样会崩溃。
三、out_of_memory(gfp_mask, order, nodemask)——全局决策入口
out_of_memory 会被三个函数调用:
mem_cgroup_out_of_memorypagefault_out_of_memory__alloc_pages_may_oom
这三个触发路径汇聚到 out_of_memory() 入口(源码在 mm/oom_kill.c),它接收:
oc->gfp_mask:原始分配请求的 GFP 掩码,决定了后续是否允许 swap、允许 IO 等oc->order:分配的大小等级,影响是否需要压缩内存oc->nodemask:允许访问的 NUMA 节点
完整的 out_of_memory(struct oom_control *oc) 决策链如下:
bool out_of_memory(struct oom_control *oc)
{
unsigned long freed = 0;
if (oom_killer_disabled)
return false;
if (!is_memcg_oom(oc)) {
blocking_notifier_call_chain(&oom_notify_list, 0, &freed);
if (freed > 0)
return true;
}
// 如果当前进程即将释放内存(如正在退出),优先选择它作为 victim
if (task_will_free_mem(current)) {
mark_oom_victim(current);
wake_oom_reaper(current);
return true;
}
// 如果分配请求不允许文件系统操作,且不是 memcg OOM,直接放过
if (oc->gfp_mask && !(oc->gfp_mask & __GFP_FS) && !is_memcg_oom(oc))
return true;
oc->constraint = constrained_alloc(oc);
if (oc->constraint != CONSTRAINT_MEMORY_POLICY)
oc->nodemask = NULL;
check_panic_on_oom(oc); // 第一步:检查是否 panic
// 如果设置了 oom_kill_allocating_task,直接杀死分配者
if (!is_memcg_oom(oc) && sysctl_oom_kill_allocating_task &&
current->mm && !oom_unkillable_task(current) &&
oom_cpuset_eligible(current, oc) &&
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;
}
select_bad_process(oc); // 第三步:选择死亡名单
if (!oc->chosen) {
dump_header(oc, NULL);
pr_warn("Out of memory and no killable processes...\n");
if (!is_sysrq_oom(oc) && !is_memcg_oom(oc))
panic("System is deadlocked on memory\n");
}
if (oc->chosen && oc->chosen != (void *)-1UL)
oom_kill_process(oc, !is_memcg_oom(oc) ? "Out of memory" :
"Memory cgroup out of memory");
return !!oc->chosen;
}进入这个函数后,内核按顺序执行决策分支:
第一步:check_panic_on_oom()——直接崩溃
首先调用 check_panic_on_oom(oc),检查 kernel.panic_on_oom 配置项。如果值为 1,直接调用 panic("OOM"),整个系统立即崩溃,不执行任何后续操作。这条路径适用于高可靠系统,不允许任何 OOM 残留。
static void check_panic_on_oom(struct oom_control *oc)
{
if (likely(!sysctl_panic_on_oom))
return;
if (sysctl_panic_on_oom != 2) {
if (oc->constraint != CONSTRAINT_NONE)
return;
}
/* Do not panic for oom kills triggered by sysrq */
if (is_sysrq_oom(oc))
return;
dump_header(oc, NULL);
panic("Out of memory: %s panic_on_oom is enabled\n",
sysctl_panic_on_oom == 2 ? "compulsory" : "system-wide");
}第二步:wake_oom_reaper() 唤醒解除映射
如果不 panic,内核唤醒 oom_reaper 线程,将当前进程挂入重捕队列。源码(mm/oom_kill.c):
static void wake_oom_reaper(struct task_struct *tsk)
{
/* mm is already queued? */
if (test_and_set_bit(MMF_OOM_REAP_QUEUED, &tsk->signal->oom_mm->flags))
return;
get_task_struct(tsk);
spin_lock(&oom_reaper_lock);
tsk->oom_reaper_list = oom_reaper_list;
oom_reaper_list = tsk;
spin_unlock(&oom_reaper_lock);
trace_wake_reaper(tsk->pid);
wake_up(&oom_reaper_wait);
}通过 oom_reaper → oom_reap_task → oom_reap_task_mm → __oom_reap_task_mm 调用链,对进程解除页表映射。这是不死人的第一道防线,后续由 oom_reaper 线程实际执行解除映射(下一篇文章详述)。
第三步:select_bad_process() + oom_badness() 选死亡名单
选择死亡名单的入口:
static void select_bad_process(struct oom_control *oc)
{
oc->chosen_points = LONG_MIN;
if (is_memcg_oom(oc))
mem_cgroup_scan_tasks(oc->memcg, oom_evaluate_task, oc);
else {
struct task_struct *p;
rcu_read_lock();
for_each_process(p)
if (oom_evaluate_task(p, oc))
break;
rcu_read_unlock();
}
}对每个可杀进程调用 oom_evaluate_task() → oom_badness() 计算"死亡分数"——基于内存占用的权重(RSS / total_mem × 1000)加上用户可调的 oom_score_adj(-1000 到 1000),选出分数最高的作为 victim。
第四步:oom_kill_process 执行杀死
选好 victim 后,oom_kill_process 发送 SIGKILL、kill 整个进程组、处理 memcg 标记、再次唤醒 oom_reaper 解除残留映射,然后调用 dump_header() 打印系统内存快照,最后输出经典日志。
四、Memory Reserve 与 out_of_memory 的关系
Memory Reserve 在 out_of_memory 流程中有两个位置起作用:
触发 OOM 前的最后一道屏障:在
__alloc_pages_may_oom中,先检查oom_reserves_allowed()是否还有 reserve 页面,有则分配少量紧急页面,没有则 panic。out_of_memory之后的"备用资源":即使进入了out_of_memory决策链,如果oom_reserves_allowed还有剩余,内核仍可用 reserve 分配页面来执行紧急操作,而不是立即崩溃。
也就是说,Memory Reserve 既是触发 panic 的门槛,又是 panic_on_oom 之外的第二重保护机制。
五、全景图:OOM 触发链 + Memory Reserve + out_of_memory 决策
整体决策流程图

代码级要点总结
关键配置参数
下一篇文章:我们将深入分析 oom_badness 死亡名单算法——内核如何给进程打分决定谁该被杀。