找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

581

积分

0

好友

75

主题
发表于 5 天前 | 查看: 23| 回复: 0

上一篇文章我们总结了 OOM Killer 的各种操作用法,但会用不等于深入理解。本次我们就深入 Linux 内核源码,来好好研究一下这个“斩杀线” OOM 机制究竟是如何设计的。

接下来我们以 5.10 内核版本为主要分析对象。

分析的起点,自然是走到触发 OOM Killer 的核心函数 out_of_memory。这个函数定义了当系统检测到超出内存资源限制时,应该如何应对。

注意:以下分析基于简化后的类伪代码,旨在阐明核心逻辑,并会用 *** 标注其对应上一篇文章中提到的哪一个调优项。

让我们先看看 out_of_memory 函数的主要逻辑框架:

// 位于 mm/oom_kill.c
bool out_of_memory(struct oom_control *oc)
{
    // 规则1:如果全局禁用了OOM Killer,则直接返回
    if (oom_killer_disabled)
        return false;

    // 规则2:检查进程是否在允许被杀的候选集内(涉及cpuset/mempolicy)
    if (!is_memcg_oom(oc) && !oom_cpuset_eligible(task, oc)) {
        // 如果进程不在候选集内,则跳过它
        mem_cgroup_account_oom_skip(task, oc);
        return 0;
    }

    // 规则3:一个特殊策略——谁触发,杀谁
    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) {
        // *** 对应上篇文章表中第五项:oom_kill_allocating_task ***
        // 条件满足时,直接选择当前触发OOM的进程进行斩杀
        get_task_struct(current);
        oc->chosen = current;
        oom_kill_process(oc, “Out of memory (oom_kill_allocating_task)”);
        return true;
    }

    // 如果上述快速路径都没走通,则进入常规选择流程:挑选“最坏”的进程
    select_bad_process(oc);

    // ... 后续逻辑:斩杀被选中的进程
    oom_kill_process(oc, ...);
}

从框架可以看出,OOM Killer 并非盲目杀戮,而是有一套清晰的优先级判断。在常规选择流程中,核心函数是 select_bad_process,它负责遍历进程并打分,选出那个“最该死”的。

select_bad_process 函数会根据是否是由 Cgroup 内存限制触发的 OOM(即 memcg oom)而走不同的分支:

// 位于 mm/oom_kill.c
static void select_bad_process(struct oom_control *oc)
{
    if (cgroup mem oom) {
        // Cgroup OOM 路径
        mem_cgroup_select_bad_process(oc); // 实现在 mm/memcontrol.c
        [
            // *** 对应上篇文章表中第一项:cgroup优先级策略 ***
            if (oc->use_priority_oom) {
                // 选择优先级最低的 Cgroup
                victim_memcg = mem_cgroup_select_victim_cgroup(...);
                [
                    if (!memcg->use_hierarchy)
                        return memcg;
                    // 否则,迭代遍历子Cgroup,选择其中优先级(oom.priority)最低的一个
                    return 优先级最低的 memcg;
                ]
            }
            // 在选中的 Cgroup 内,遍历其下的任务并计算分数,逻辑与非Cgroup OOM一致
            mem_cgroup_scan_tasks(victim_memcg, oom_evaluate_task, oc);
        ]
    } else {
        // 全局 OOM 路径
        for_each_process(p) {
            if (oom_evaluate_task(p, oc)) {
                // 找到一个候选进程,可能直接选中或参与评分竞争
                break;
            }
        }
    }
}

无论是哪种路径,最终为单个进程打分的核心函数都是 oom_badness,它决定了进程的“坏”程度(分数越高,越容易被杀)。

// 位于 mm/oom_kill.c
long oom_badness(struct task_struct *p, unsigned long totalpages)
{
    // *** 对应上篇文章表中第三项:oom_score_adj ***
    long adj = (long)p->signal->oom_score_adj;
    long points;

    // 规则1:不可杀进程(如内核线程、init进程)直接返回最小分
    if (task_is_unkillable(p) || task_is_locked_memory(p))
        return LONG_MIN;

    // 规则2:如果 oom_score_adj 被设为 -1000,则免疫OOM Kill
    if (adj == OOM_SCORE_ADJ_MIN) // OOM_SCORE_ADJ_MIN = -1000
        return LONG_MIN;

    // 计算基础分:主要基于物理内存(RSS)、页表、Swap使用量
    points = 常驻内存集(rss) + 页表占用 + swap内存比例;

    // 调整分数:oom_score_adj 的加权影响
    adj *= totalpages / 1000;
    points += adj;

    return points;
}

此外,文中提到了两个至关重要的筛选策略,它们决定了哪些进程有资格进入“候选名单”。这主要取决于进程的内存分配策略(mempolicy)或 CPU 亲和性集(cpuset)是否与当前触发 OOM 的上下文有交集。这个判断在函数 oom_cpuset_eligible 中完成。

// 位于 mm/oom_kill.c
bool oom_cpuset_eligible(struct task_struct *start, struct oom_control *oc)
{
    const nodemask_t *mask = oc->nodemask;
    ...
    // 遍历线程组
    for_each_thread(start, tsk) {
        if (mask) {
            // 情况A:由内存策略(mempolicy)约束的OOM
            // *** 对应上篇文章表中第四项 ***
            ret = mempolicy_nodemask_intersects(tsk, mask);
            [
                // 检查任务的内存策略掩码与触发OOM的掩码是否有交集
                struct mempolicy *mempolicy = tsk->mempolicy;
                if (!mempolicy) // 默认策略,允许在任何节点分配,视为有交集
                    return true;
                switch (mempolicy->mode) {
                    // ... 处理 bind, interleave, preferred 等策略
                    case MPOL_BIND:
                    case MPOL_INTERLEAVE:
                        // 关键判断:位图是否有交集
                        nodes_intersects(mempolicy->v.nodes, *mask);
                        [
                            // 底层使用 bitmap_intersects() 判断
                            // 两个位图中是否有至少一个相同的位被设置为1
                        ]
                }
            ]
        } else {
            // 情况B:非mempolicy约束的全局或cpuset OOM
            // *** 对应上篇文章表中第二项:cpuset内存交集判断 ***
            ret = cpuset_mems_allowed_intersects(current, tsk);
            [
                // 判断当前触发OOM的进程(‘current’)与待检查进程(‘tsk’)
                // 它们的 cpuset 所允许的内存节点(mems_allowed)是否有交集。
                // 原理:获取两者的 mems_allowed 掩码,进行按位与(AND)操作。
                // 结果非零表示有交集,该进程有资格被选中。
            ]
        }
        if (ret) // 只要线程组中有一个线程符合条件,即认为该任务符合条件
            break;
    }
    return ret;
}

总结

通过源码分析,我们可以看到 Linux OOM Killer 的“制度设计”是一个多层次的筛选与决策过程:

  1. 全局开关检查:首先确认 OOM Killer 未被禁用。
  2. 资格预审:通过 oom_cpuset_eligible 检查进程的内存访问范围(由 cpusetmempolicy 定义)是否与当前出问题的内存区域有交集,无交集者直接豁免。
  3. 快速通道:若启用 oom_kill_allocating_task 且条件满足,则直接斩杀触发 OOM 的进程,无需遍历评分。
  4. 分组策略:对于 Cgroup OOM,可配置优先级策略,先挑出优先级最低的 Cgroup。
  5. 精细打分:在候选进程集合中,通过 oom_badness 函数计算每个进程的“坏”分数。分数综合考虑了物理内存使用量、交换区使用量,并最终由 oom_score_adj 这个用户可调参数进行强力加权(可正可负,甚至设置为-1000实现完全免疫)。
  6. 最终执行:选择分数最高的进程(或通过快速通道选定的进程)执行 oom_kill_process

理解这套机制,对于在复杂部署环境(尤其是容器和混合负载场景)下进行有效的内存风险控制和进程保活至关重要。它不仅仅是内存耗尽的应急响应,更是系统资源管理策略的一个关键组成部分。如果你想深入探讨更多内核或计算机系统底层原理,欢迎在 云栈社区 的技术板块,如计算机基础等,与更多开发者交流学习。




上一篇:深度解析Claude Agent Skills:从概率预测到确定性输出的智能体核心能力重塑
下一篇:Google Opal实战:两分钟创建AI驱动的高端PPT自动化工作流
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-24 02:54 , Processed in 0.313712 second(s), 38 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表