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_oomout_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;
}

这段代码揭示了几个重要细节:

  1. OOM 锁机制oom_lock 是一个互斥锁,保证同一时间只有一个 CPU 执行 OOM。如果另一个 CPU 已经在做 OOM,当前 CPU 只是等待 1 tick,然后重新尝试分配。

  2. 再次尝试分配:进入 __alloc_pages_may_oom 后,先用 ALLOC_WMARK_HIGH(高水位线)再尝试一次分配——这是为了处理并行 OOM 杀进程后可能已经释放了内存的情况。

  3. 多种豁免条件:coredump、高阶分配、允许失败的分配、低 zone 分区、存储挂起等情况会跳过 OOM,直接返回失败。

  4. OOM 成功后的处理:如果 out_of_memory() 返回 true,说明有进程被杀或有内存释放。对于 GFP_NOFAIL 的分配,此时会从保留水位(ALLOC_NO_WATERMARKS)强制分配。

路径 2:mem_cgroup_out_of_memoryout_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_memoryout_of_memory(缺页错误)

用户进程访问了未映射的虚拟地址,触发缺页错误。内核尝试分配并映射页面,但发现内存耗尽。此时 pagefault_out_of_memory() 直接调用 out_of_memory(),将此次缺页错误作为触发源。

注意:这三条路径最终汇聚到同一个 out_of_memory(gfp_mask, order, nodemask) 全局入口,这意味着无论哪种触发,都会经过同一套决策逻辑——但 gfp_mask 不同,影响后续是否允许 swap、允许 IO 等。


二、Memory Reserve 检查链:gfp_pfmemalloc_allowedoom_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 会被三个函数调用:

  1. mem_cgroup_out_of_memory

  2. pagefault_out_of_memory

  3. __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 流程中有两个位置起作用:

  1. 触发 OOM 前的最后一道屏障:在 __alloc_pages_may_oom 中,先检查 oom_reserves_allowed() 是否还有 reserve 页面,有则分配少量紧急页面,没有则 panic。

  2. out_of_memory 之后的"备用资源":即使进入了 out_of_memory 决策链,如果 oom_reserves_allowed 还有剩余,内核仍可用 reserve 分配页面来执行紧急操作,而不是立即崩溃。

也就是说,Memory Reserve 既是触发 panic 的门槛,又是 panic_on_oom 之外的第二重保护机制


五、全景图:OOM 触发链 + Memory Reserve + out_of_memory 决策

整体决策流程图

代码级要点总结

函数

文件

核心逻辑

__alloc_pages_may_oom

mm/page_alloc.c

构造oom_control,加oom_lock,再次分配,检查豁免条件,最终调用out_of_memory

out_of_memory

mm/oom_kill.c

五层防御:disabled→notify→task_will_free→panic_check→select_bad_process→kill

check_panic_on_oom

mm/oom_kill.c

panic_on_oom sysctl:0=不panic,1=无约束时panic,2=强制panic

oom_reserves_allowed

mm/page_alloc.c

检查tsk_is_oom_victim + !MMU时检查TIF_MEMDIE

select_bad_process

mm/oom_kill.c

遍历for_each_process,调用oom_evaluate_task→oom_badness

wake_oom_reaper

mm/oom_kill.c

设置MMF_OOM_REAP_QUEUED,挂入oom_reaper_list,唤醒reaper线程

关键配置参数

参数

默认值

作用

sysctl_panic_on_oom

0

0=不panic,1=无约束时panic,2=强制panic

sysctl_oom_kill_allocating_task

0

1=直接杀触发OOM的进程

/proc/<pid>/oom_score_adj

0

-1000=永不被杀,+1000=优先被杀


下一篇文章:我们将深入分析 oom_badness 死亡名单算法——内核如何给进程打分决定谁该被杀。