惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tailwind CSS Blog
大猫的无限游戏
大猫的无限游戏
L
LINUX DO - 热门话题
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
雷峰网
雷峰网
aimingoo的专栏
aimingoo的专栏
博客园_首页
MongoDB | Blog
MongoDB | Blog
V
V2EX
GbyAI
GbyAI
量子位
Microsoft Azure Blog
Microsoft Azure Blog
有赞技术团队
有赞技术团队
G
Google Developers Blog
云风的 BLOG
云风的 BLOG
B
Blog
Microsoft Security Blog
Microsoft Security Blog
S
SegmentFault 最新的问题
O
OpenAI News
N
News and Events Feed by Topic
博客园 - Franky
爱范儿
爱范儿
Forbes - Security
Forbes - Security
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V2EX - 技术
V2EX - 技术
Application and Cybersecurity Blog
Application and Cybersecurity Blog
N
News and Events Feed by Topic
N
News | PayPal Newsroom
Schneier on Security
Schneier on Security
Cloudbric
Cloudbric
Security Archives - TechRepublic
Security Archives - TechRepublic
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Recent Commits to openclaw:main
Recent Commits to openclaw:main
人人都是产品经理
人人都是产品经理
P
Privacy International News Feed
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
B
Blog RSS Feed
阮一峰的网络日志
阮一峰的网络日志
D
DataBreaches.Net
Last Week in AI
Last Week in AI
罗磊的独立博客
Spread Privacy
Spread Privacy
Recent Announcements
Recent Announcements
The Cloudflare Blog
Google DeepMind News
Google DeepMind News
AWS News Blog
AWS News Blog
The Register - Security
The Register - Security
Y
Y Combinator Blog
J
Java Code Geeks
I
Intezer

BlogFinder

日常漫步 Vol.24 之漫步前山河 - 雅余 周报 #1-聊聊本周的收获 - Edwin's Blog 我的OpenCode必装插件与Skill Write Something 掌中之物未必在掌握之中 · CRIVU PiliNara,一个更顺手的 PiliPlus 分支 「NekoEcho」:做一个必有回响的猫娘主题博客 2026-05 书影音总结 简化博客主题 - 安迪 我第一次发布 npm 包 拾花小记#45:中考前的二三事 – 小改学习志 黛西花园5月游 #18 枇杷又熟了的五月月报 一些奇奇怪怪的需求?word仿方正书版的几个小操作 - Xiobb's Blog 0419 御温泉之旅 修复了一些bug,网站基本上趋于稳定了 - 新锐博客 又回到四十年前 如何定义成功 迷鹿屋2026已重新上线 科技冰火两重天+一周回顾 ${title} 热度退了,我反而用得更深了-咕咚同学 我到底该不该换个域名? 随身WIFI折腾记 - 安迪 博客撰写体验提升——hexo pro插件 为什么不用相机把屏幕上的接关密码拍下来? 国清寺与天台山 – Ouroboros ★★★★☆《挽救计划》——久违的经济上行感 - Davidの3号基地 删除右键“打开方式”里多余选项 第三周刊_No.53|一切都会被支付两次 安卓APP通话记录与录音上传踩坑记录 - 子舒的博客 天量下跌 inBox 笔记 2.3.8,把工具栏交给了你-咕咚同学 我把小龙虾搬到了微信-咕咚同学 安好 - 响石潭 Compound Engineering Plugin:让每个工程单元都比上一个更容易 MOSS-TTS Family:开源高质量语音与声音生成模型家族深度解析 Crawl4AI:专为 LLM 设计的开源 Web 爬虫与数据抓取工具 Build Your Own X:从零实现你最喜欢的技术——程序员进阶的终极资源清单 Anthropic Skills:用文件夹教 Claude 专业技能的开源框架 1年的去月球(下) - 梅之夏 欢迎回来。 简单讲讲 ASN.1 与 OID DTV - 直播聚合客户端 5.22-5.27 – 不兴江 还没去过鸭川 – 不兴江 张晶晶同学三刷林志颖 关于我 – 不兴江 爱与嫉妒 – 不兴江 港股被持续做空 备案码花了四百块-咕咚同学 一句话生成封面:我给公众号做了4种风格的AI封面生成技能 「官」方認證 再谈费曼学习法 2026-05-28T00:34:11+08:00 2026-05-28T00:28:45+08:00 离谱的英语学习指南:基于AI的英语进阶系统方法论 iii:零集成架构的后端统一运行时 Claude Code Harness:让 Claude Code 工作有迹可循的工程化框架 Heretic:全自动移除大语言模型审查机制的开源工具 MarkItDown:微软开源的万能文档转 Markdown 利器 Harness:让 Claude Code 秒变多智能体协作工厂 这段时间尽折腾AI Agent了,确实极大地提高了效率 近期动态:两个新站点正式上线啦 误判解除!zhouayuan.com 腾讯安全申诉成功 - 周阿源|玩具设计・插画日常・生活随笔 Ralph:让 AI 编码工具自主循环跑完所有 PRD 任务的量产神器 全都违法 – 个人工作记录 关于zhouayuan.com被误判 “含违规信息” 的说明与申诉记录 - 周阿源|玩具设计・插画日常・生活随笔 小米 MiMo v2.5 Pro 白嫖 最大的人间清醒,兜里有钱,但是不花。 夜晚靓歌(12):于文文现场solo - 王志勇的Blog 今日插画:风扬起的倔强 - 周阿源|玩具设计・插画日常・生活随笔 回门习俗 独立网卡 - 忘记了回忆 500亿入股人工智能企业 从命令行到桌面智能体-咕咚同学 第一性原理读书笔记 行者微评论223-加班の守株待兔-博客|政治与时事-风雨行者 ZOZO开源物理接触求解器:GPU加速的可扩展仿真引擎 OpenStock:开源股票市场交易平台技术深度解析 MoneyPrinterTurbo:基于AI的全自动短视频生成工具深度解析 Claude-Mem:为 Claude Code 构建的持久化记忆压缩系统 Twenty:可代码化定制的企业级开源 CRM 平台技术深度解析 2026-05-26T22:59:17+08:00 企业级开源大模型部署平台 GPUStack 实战教程 1年的去月球(上) - 梅之夏 Sevalla - 静态网站托管服务 不用翻墙、不用注册、不用月费,普通人也能用上 Claude Code 装修灯具要注意⚠️ 黄梅天先锋 - 游子微博 公安备案顺利办结,站点备案全部完成 - 周阿源|玩具设计・插画日常・生活随笔 第三次兑换天猫超市卡了宗宗酱-三维狐少儿编程 Don't think, feel. - Rolen's Blog 人这一辈子,到底图个什么 博客迁移 - Edwin's Blog 情感赛道写作模板 再现本轮行情的典型特征 裁员与平常心-咕咚同学 别让“偷懒”,成为隐私泄露的破绽 片刻 - Jdeal | Life is like a Design.
Linux OOM Killer 计分模型源码拆解:进程 adj 权重、内存透支机制与业务无辜被杀根因溯源
qaq卟言,buyan@mail.qaqbuyan.com · 2026-06-21 · via BlogFinder
  • 前言
  • Linux OOM Killer 计分模型源码拆解:进程 adj 权重、内存透支机制与业务无辜被杀根因溯源1.png
  • 声明: 本文所有分析均基于 Linux v6.1 LTS 稳定版 内核原生源码,所引用的代码片段、机制解释及结论仅供学习参考与技术交流。内核版本迭代、架构差异或特定发行版的内核定制改造可能导致行为有所偏差。鉴于内核源码的复杂性与笔者水平有限,文中如有错误、疏漏或表述不当之处,欢迎通过评论或邮箱指正探讨,以期共同完善。请以您所用内核版本的官方源码为准。
  • 本文全部代码基于Linux v6.1 LTS稳定版,为生产环境内核基线,适配物理机、云主机、容器全场景。规避原版仓库反爬校验,统一采用内核官方静态源码镜像溯源
  • 从内核源码结构体维度,计分数据源完全取自task_structmm_struct核心内核结构体
  • 精准绑定进程内核级内存记账数据,不受用户态内存统计工具干扰
  • 这也是用户态监控内存数据与内核OOM判定不一致的核心根源
  • 基于Linux内核稳定版mm/oom_kill.c源码,纯底层机制拆解、零运维操作、零入门科普
  • Linux v6.1 oom_kill.c 永久静态镜像
  • OOM Killer 内核底层本质
  • Linux OOM Killer 计分模型源码拆解:进程 adj 权重、内存透支机制与业务无辜被杀根因溯源2.png
  • Linux OOM Killer并非大众认知的"系统内存耗尽后的兜底杀进程逻辑"
  • 而是内核内存压力博弈下的强制资源收敛机制,属于虚拟内存子系统的核心容错模块
  • 耦合伙伴系统、Slab内存管理器、进程调度结构体与内存记账体系
  • 其核心设计本质并非"回收内存",而是在系统处于内存硬死锁、页分配彻底失败的临界状态下
  • 通过确定性计分算法筛选最优牺牲进程,打破内存分配阻塞僵局,保障内核基础调度链路不崩塌
  • 绝大多数线上业务OOM异常、核心服务无辜被杀、内存充足却触发OOM等疑难问题
  • 均源于开发者对内核计分源码约束、内存权重配比规则、隐性内存透支记账机制的认知盲区
  • 而非简单的业务内存溢出
  • 聚焦内核底层逻辑与线上底层疑难根因
  • OOM 核心计分内核算法源码级深度拆解
  • Linux OOM Killer 计分模型源码拆解:进程 adj 权重、内存透支机制与业务无辜被杀根因溯源3.png
  • OOM Killer所有筛选逻辑的核心载体为oom_badness()内核函数,是进程OOM分数判定的唯一源码入口
  • 所有进程权重、内存占用、特权属性的计算逻辑均封装于此
  • 最终输出以页帧计数的绝对分数,分数越高越优先被杀死(与 oom_score_adj 归一化缩放共同决定最终分数
  • 核心计分源码完整逻辑链路
  • 内核计分不采用单一内存占用维度
  • 而是物理内存(RSS)+交换内存(SWAP)+页表内存(pgtables)+adj权重修正的多维加权模型(注:v6.1 已移除早期版本的 CAP_SYS_ADMIN 特权衰减逻辑
  • 核心源码计算逻辑如下,完全贴合内核原生实现:
  • 基础得分 = 进程RSS物理内存占用 + 交换分区内存占用 + 进程页表占用内存(以页帧计) 最终OOM分数 = 基础得分 + 经totalpages归一化后的oom_score_adj权重修正值
  • 内核原生源码片段(v6.1 稳定版|oom_badness完整实现):
  • 精准对应上述多维加权计分模型,为OOM打分唯一核心实现,无任何二次封装
  • // linux/mm/oom_kill.c (v6.1 stable | oom_badness完整实现)
    long oom_badness(struct task_struct *p, unsigned long totalpages)
    {
            long points;
            long adj;
    
            // 过滤内核线程与全局init进程
            if (oom_unkillable_task(p))
                    return LONG_MIN;
    
            p = find_lock_task_mm(p);
            if (!p)
                    return LONG_MIN;
    
            // oom_score_adj == -1000 直接豁免(先查 adj 可省去后续 points 计算)
            adj = (long)p->signal->oom_score_adj;
            if (adj == OOM_SCORE_ADJ_MIN ||
                            test_bit(MMF_OOM_SKIP, &p->mm->flags) ||
                            in_vfork(p)) {
                    task_unlock(p);
                    return LONG_MIN;     // ← 豁免检查失败,直接退出,不计算 points
            }
    
            // 核心计分:RSS物理内存 + Swap内存 + 页表内存三维度加权
            // 注:执行到此说明豁免检查通过,adj 变量后续将复用为归一化系数
            points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
                     mm_pgtables_bytes(p->mm) / PAGE_SIZE;
            task_unlock(p);
    
            // adj归一化缩放:adj *= totalpages / 1000
            adj *= totalpages / 1000;
            points += adj;
    
            return points;
    }
  • 代码对应核心机制:
  • 1. 三点计分:RSS + SWAPENTS + pgtables_bytes 三维度,物理内存权重最高 2. adj归一化缩放:adj *= totalpages / 1000; points += adj 内嵌于本函数,无独立子函数,解释「大机器adj放大、小容器adj失效」的故障根因 3. 前置过滤:oom_unkillable_task() 过滤内核线程与init进程,adj == OOM_SCORE_ADJ_MIN 直接豁免 4. 注意:v6.1 版本 oom_badness() 已简化为 (p, totalpages) 双参数签名,且 不含 CAP_SYS_ADMIN 特权衰减(该衰减在 v6.1 已移除)
  • 关键内核权重约束机制
  • 普通技术认知仅知晓oom_score_adj可调权重,却完全不懂内核权重归一化底层约束
  • 内核并不会直接叠加adj原始值,而是会基于系统总内存完成归一化计算:
  • adj *= totalpages / 1000
  • 该底层机制决定了:大内存服务器环境下,相同adj数值的修正效果会被放大
  • 小内存容器环境下权重修正会被稀释,这是容器场景核心服务微调adj仍被误杀的核心底层原因
  • adj权重归一化核心实现(v6.1 稳定版):
  • 该逻辑并非独立函数,而是内嵌于oom_badness()函数的尾部(详见上节代码),位于计分计算与task_unlock()之后
  • // linux/mm/oom_kill.c (v6.1 stable | oom_badness() 尾部内联)
    adj = (long)p->signal->oom_score_adj;                     // 读取 adj
    // ... 前置过滤与三维度计分后 ...
    adj *= totalpages / 1000;                                 // totalpages 全局归一化缩放
    points += adj;
    
    return points;
  • 关键差异说明:
  • v6.1 中 不存在 名为 oom_score_adj_update() 的独立函数,adj归一化是 oom_badness() 的内联逻辑; v6.1 已移除 CAP_SYS_ADMIN专属的 points /= 4 特权衰减机制(该机制存在于更早内核版本,v6.1不再适用); totalpages 来源:全局场景来自 totalram_pages() + total_swap_pages(constrained_alloc() 中计算),cgroup场景来自 mem_cgroup_get_max()。
  • 内核原生补充证据——constrained_alloc()totalpages的实际计算逻辑(v6.1 稳定版):
  • totalpagesconstrained_alloc()函数中按约束类型动态赋值,该值直接传递给oom_badness()决定adj归一化的缩放幅度:
  • // linux/mm/oom_kill.c (v6.1 stable | constrained_alloc totalpages 赋值逻辑)
    static enum oom_constraint constrained_alloc(struct oom_control *oc)
    {
            // MEMCG 场景:totalpages = cgroup 内存上限
            if (is_memcg_oom(oc)) {
                    oc->totalpages = mem_cgroup_get_max(oc->memcg) ?: 1;
                    return CONSTRAINT_MEMCG;
            }
    
            // 全局场景:totalpages = 全部物理内存 + 全部 swap
            oc->totalpages = totalram_pages() + total_swap_pages;
    
            // MEMORY_POLICY 场景:totalpages = 指定 NUMA 节点的物理 + swap
            // ...
            // CPUSET 场景:totalpages = cpuset 绑定的节点物理 + swap
            // ...
    }
  • 源码机制溯源:
  • totalpages在不同约束场景取值不同,直接导致adj归一化效果的差异化
  • 物理机totalpages极大导致adj修正被显著放大,cgroup容器totalpages较小导致adj修正被稀释
  • 这是"在物理机上微调 adj 有效、在容器中微调无效"的底层源码根因
  • 该逻辑属于内核硬编码约束,无任何配置开关可关闭
  • 进程虚实内存权重配比底层源码约束
  • Linux OOM Killer 计分模型源码拆解:进程 adj 权重、内存透支机制与业务无辜被杀根因溯源4.png
  • 物理内存(RSS)与虚拟内存权重博弈机制
  • 内核OOM计分的核心权重倾斜规则:
  • 物理内存占用权重远高于虚拟内存,交换分区内存占用具备惩罚性加权
  • 源码中通过get_mm_rss统计进程实际物理驻留内存,通过get_mm_counter统计MM_SWAPENTS交换内存
  • 二者直接计入基础得分,无任何衰减
  • 而进程虚拟内存总占用仅作为辅助判定维度,不参与核心计分
  • 内核源码佐证:虚实内存计分权重差异化约束(v6.1 稳定版
  • // include/linux/mm.h (v6.1 stable | get_mm_rss 实际定义位置)
    // 注意:该函数定义在 mm.h 头文件中,所有包含该头文件的源文件均可使用
    static inline unsigned long get_mm_rss(struct mm_struct *mm)
    {
            return get_mm_counter(mm, MM_FILEPAGES) +
                   get_mm_counter(mm, MM_ANONPAGES);
    }
    
    // include/linux/mm_inline.h (v6.1 stable | MM_SWAPENTS 枚举)
    enum mm_counter_item {
            MM_FILEPAGES,       // 0
            MM_ANONPAGES,       // 1
            MM_SWAPENTS,        // 2
            MM_SHMEMPAGES,      // 3
            NR_MM_COUNTER_ITEMS
    };
  • OOM计分核心仅统计文件页+匿名页物理内存、swap交换内存、页表内存三维度
  • 虚拟内存total_vm不纳入计分,这是「虚存透支、物理落地瞬间OOM」的核心代码根源
  • 内核原生补充证据——dump_task()输出中的total_vm仅用于诊断打印(v6.1 稳定版):
  • dump_task()函数用于OOM触发时打印各进程内存状态(dmesg 中可见的 Tasks state 信息),它确实输出了total_vm
  • 但该值仅用于诊断展示,从未被oom_badness()引用为计分因子:
  • // linux/mm/oom_kill.c (v6.1 stable | dump_task 输出诊断信息)
    static int dump_task(struct task_struct *p, void *arg)
    {
            // ... 前置过滤逻辑 ...
            task = find_lock_task_mm(p);
            // ...
            // 注意:total_vm 在此处仅打印,不参与 oom_badness 计分
            pr_info("[%7d] %5d %5d %8lu %8lu %8ld %8lu %5hd %s\n",
                    task->pid, from_kuid(&init_user_ns, task_uid(task)),
                    task->tgid, task->mm->total_vm, get_mm_rss(task->mm),
                    mm_pgtables_bytes(task->mm),
                    get_mm_counter(task->mm, MM_SWAPENTS),
                    task->signal->oom_score_adj, task->comm);
            task_unlock(task);
            return 0;
    }
  • 源码维度证据链:
  • dump_task明确输出total_vm用于诊断,但回看上节核心计分源码完整逻辑链路oom_badness()计分公式
  • 仅包含get_mm_rss物理内存)+ MM_SWAPENTSswap)+ mm_pgtables_bytes页表
  • total_vm被内核刻意排除在计分之外
  • 这是开发者在dmesg中看到某进程total_vm极大但OOM却杀了另一个进程的底层源码根因
  • 该底层规则直接导致典型线上疑难场景:
  • 大量业务进程仅申请虚拟内存、未落地物理内存,看似内存占用极低,但突发批量物理内存落地时,会瞬间抢占系统内存,触发OOM 而长期占用大量物理内存的沉睡进程,会持续拉高自身OOM分数,成为优先被杀对象,完全违背业务直观认知
  • 页表内存的隐性计分权重盲区
  • 绝大多数开发者完全忽略mm_pgtables_bytes页表内存占用的计分贡献
  • 内核源码会将进程页表占用的内存换算为页帧数量,全额计入基础OOM得分
  • 对于海量连接、多线程业务进程,页表内存会随线程数、文件描述符数线性膨胀
  • 即便业务数据内存占用极低,页表内存也会导致进程OOM分数飙升,引发无辜被杀
  • 内核源码片段:页表内存计分核算逻辑(v6.1 稳定版
  • // linux/mm/memory.c (v6.1 stable | mm_pgtables_bytes 定义)
    size_t mm_pgtables_bytes(const struct mm_struct *mm)
    {
            return mm->pgtable_bytes;
    }
    
    // oom_kill.c (oom_badness 内部) 页表内存全额纳入计分核心逻辑
    points += mm_pgtables_bytes(p->mm) / PAGE_SIZE;
  • 页表内存无豁免、无衰减全额计分,精准解释「轻量高并发多线程业务内存低占用却无辜OOM」的隐蔽故障
  • 多线程进程会创建独立页表映射,pgtable_bytes持续累加
  • 且内核无页表内存豁免规则,是高并发轻量业务进程无辜OOM的核心代码根源
  • Slab 内存占用对 OOM 判定的底层干扰机制(内核级隐蔽漏洞)
  • Linux OOM Killer 计分模型源码拆解:进程 adj 权重、内存透支机制与业务无辜被杀根因溯源5.png
  • Slab作为内核缓存内存管理核心模块,其内存占用不归属任何用户进程OOM统计
  • 但会占用系统全局可用内存,直接压缩系统内存水位
  • 触发全局OOM判定,这是线上最隐蔽的OOM诱因
  • Slab内存的内核记账隔离机制
  • 内核Slab缓存(dentry、inode、buffer_head等)属于内核态全局内存,不计入task_struct进程内存统计
  • 用户态监控工具无法感知其对OOM的影响
  • 当业务频繁创建销毁文件、套接字、目录节点时,Slab缓存会持续膨胀
  • Slab回收存在内核延迟机制,不会随业务进程退出即时释放
  • 内核源码片段:OOM评估过滤内核线程机制(v6.1 稳定版
  • // linux/mm/oom_kill.c (v6.1 stable | oom_evaluate_task 前段过滤逻辑)
    static int oom_evaluate_task(struct task_struct *task, void *arg)
    {
            struct oom_control *oc = arg;
            long points;
    
            // oom_unkillable_task 过滤 init 进程和 PF_KTHREAD 内核线程
            if (oom_unkillable_task(task))
                    goto next;
    
            // cpuset 亲和性过滤
            if (!is_memcg_oom(oc) && !oom_cpuset_eligible(task, oc))
                    goto next;
    
            // 已标记为 OOM victim 且有 MMF_OOM_SKIP 的跳过
            if (!is_sysrq_oom(oc) && tsk_is_oom_victim(task)) {
                    if (test_bit(MMF_OOM_SKIP, &task->signal->oom_mm->flags))
                            goto next;
                    goto abort;
            }
    
            // oom_badness 打分筛选(异常点为 LONG_MAX 时由 oom_task_origin 提前选择)
            points = oom_badness(task, oc->totalpages);
            if (points == LONG_MIN || points < oc->chosen_points)
                    goto next;
    
    select: // 更新最优候选
            if (oc->chosen)
                    put_task_struct(oc->chosen);
            get_task_struct(task);
            oc->chosen = task;
            oc->chosen_points = points;
            return 0;
    next:
            return 0;
    abort:
            if (oc->chosen)
                    put_task_struct(oc->chosen);
            oc->chosen = (void *)-1UL;
            return 1;
    }
  • 注意:
  • struct kmem_cache 结构体定义于 include/linux/slab_def.h,字段远多于 total_pages/free_pages, 博客此处仅为示意Slab内存与用户进程内存分离的概念,不代表实际内核结构体定义。 OOM评估链路通过 oom_unkillable_task() 过滤内核线程(PF_KTHREAD), 但Slab内存消耗的是系统可用物理内存,其本身不归属任何用户进程RSS统计。
  • 源码核心结论:
  • oom_unkillable_task() 天然过滤内核线程与init进程,OOM计分仅针对用户态进程打分,不直接清算内核Slab内存。 Slab内存挤占系统资源后,OOM机制只能杀死用户进程来回收内存,这是"系统内存充足、业务无辜OOM"的底层代码壁垒
  • 内核原生补充证据——Slab检测函数should_dump_unreclaim_slab()v6.1 稳定版):
  • 内核自身内置了专门检测"不可回收 Slab 是否超过全部用户 LRU 页面"的函数
  • 该函数在dump_header()中被调用,是OOM发生时内核主动打印Slab超量信息的源码依据
  • // linux/mm/oom_kill.c (v6.1 stable | should_dump_unreclaim_slab 完整实现)
    /*
     * Check whether unreclaimable slab amount is greater than
     * all user memory(LRU pages).
     * dump_unreclaimable_slab() could help in the case that
     * oom due to too much unreclaimable slab used by kernel.
     */
    static bool should_dump_unreclaim_slab(void)
    {
            unsigned long nr_lru;
    
            nr_lru = global_node_page_state(NR_ACTIVE_ANON) +
                     global_node_page_state(NR_INACTIVE_ANON) +
                     global_node_page_state(NR_ACTIVE_FILE) +
                     global_node_page_state(NR_INACTIVE_FILE) +
                     global_node_page_state(NR_ISOLATED_ANON) +
                     global_node_page_state(NR_ISOLATED_FILE) +
                     global_node_page_state(NR_UNEVICTABLE);
    
            return (global_node_page_state_pages(NR_SLAB_UNRECLAIMABLE_B) > nr_lru);
    }
    
    // dump_header 中调用 should_dump_unreclaim_slab
    if (should_dump_unreclaim_slab())
            dump_unreclaimable_slab();
  • 源码机制溯源:
  • 该函数的存在本身就证明了内核社区已知"不可回收 Slab 超量"OOM的重要诱因
  • 当不可回收Slab占比超过所有用户态LRU页面总量时,OOM日志会主动dump slab信息
  • 这是"内存充足却 OOM——Slab隐性挤占"场景的源码级直接证据,也是线上故障排查时dmesg中出现Slab信息的内核底层原因
  • Slab 干扰OOM判定的核心场景
  • 系统整体内存剩余充足、业务进程内存占用极低,但Slab缓存占用过半系统内存
  • 导致系统可分配空闲内存枯竭,内核触发OOM流程
  • 此时OOM计分模型仅针对用户进程打分,不会清算内核Slab内存
  • 最终随机杀死正常业务进程,形成典型的"内存充足却OOM、核心服务无辜被杀"疑难问题
  • 其本质是内核用户态/内核态内存记账隔离的底层约束
  • 内核内存透支机制源码解析(隐蔽型OOM根源)
  • Linux OOM Killer 计分模型源码拆解:进程 adj 权重、内存透支机制与业务无辜被杀根因溯源6.png
  • Linux内核采用乐观内存分配机制,这是所有内存透支问题的底层根源
  • 内核允许进程申请的虚拟内存总量远超系统物理内存上限,无实时内存总量校验
  • 仅在页表落地、物理内存写入时触发内存校验,形成内存透支
  • 内存透支的内核实现逻辑
  • 伙伴系统仅维护物理内存页帧的分配状态,虚拟内存的mmap申请、brk扩容均为逻辑记账操作,无物理内存占用
  • 内核为提升内存利用率,默认允许超量虚拟内存分配
  • 当多个进程同时触发虚拟内存落地、批量抢占物理内存时
  • 系统物理内存瞬间耗尽,形成突发式内存透支,直接触发OOM Killer强制执行
  • 内核源码相关机制:乐观内存分配与内存透支底层机制(v6.1 稳定版
  • // linux/mm/mmap.c (v6.1 stable | do_mmap 入口示意 - 省略内部复杂校验)
    // do_mmap 仅校验文件映射/匿名映射的地址区间合法性,
    // 不会校验当前系统剩余物理内存是否足够分配,支撑内核乐观分配模型
    
    // linux/mm/oom_kill.c (v6.1 stable | out_of_memory 入口)
    bool out_of_memory(struct oom_control *oc)
    {
            // 检查 oom_killer_disabled
            // 触发 blocking_notifier 通知链(用户态可注册回调)
            // 检查 current 是否即将释放内存
            // 通过 constrained_alloc 确定约束类型
            // 通过 select_bad_process 选择目标
            // 通过 oom_kill_process 执行杀进程
    }
  • 核心机制:
  • do_mmap 虚存分配无物理内存校验,仅做地址空间合法性检查,支撑内核乐观分配模型; out_of_memory 入口触发条件:物理页分配(伙伴系统 __alloc_pages)彻底耗尽即调用该函数,是流量高峰瞬时内存透支击穿、突发OOM的底层源码依据
  • 源码机制溯源:虚拟内存分配无全局阈值校验,物理内存按需落地分配
  • Linux内核内存透支、突发OOM故障的原生设计特性,非bug
  • 线上高频隐蔽内存透支场景
  • 1. 容器场景内存限额透支: 容器cgroup内存限额仅约束物理内存,不限制虚拟内存,容器内进程批量预分配虚拟内存,累计透支远超容器限额,突发物理内存落地触发OOM; 2. 异步批量内存落地: 业务预热阶段批量申请虚拟内存,长期无读写操作,内存处于透支状态,流量高峰瞬时批量写入,物理内存瞬间击穿水位; 3. 子进程内存继承透支: fork子进程继承父进程全部虚拟内存空间,无实时内存拷贝,多进程叠加后虚拟内存透支量指数级增长,触发内核内存压力阈值
  • 内核原生补充证据——cgroup OOM全杀机制oom_kill_memcg_member()v6.1 稳定版):
  • 当容器cgroup触发OOM时,内核不仅仅杀死单个候选进程
  • 而是会扫描整个cgroup内所有非豁免进程,分别执行__oom_kill_process
  • // linux/mm/oom_kill.c (v6.1 stable | oom_kill_memcg_member 完整实现)
    /*
     * Kill provided task unless it's secured by setting
     * oom_score_adj to OOM_SCORE_ADJ_MIN.
     */
    static int oom_kill_memcg_member(struct task_struct *task, void *message)
    {
            if (task->signal->oom_score_adj != OOM_SCORE_ADJ_MIN &&
                !is_global_init(task)) {
                    get_task_struct(task);
                    __oom_kill_process(task, message);
            }
            return 0;
    }
  • 源码维度证据链:容器场景触发OOM时,oom_kill_process()通过mem_cgroup_scan_tasks(oom_group, oom_kill_memcg_member, message) 遍历整个cgroup
  • 所有未设置oom_score_adj=-1000的成员进程都会被逐一__oom_kill_process
  • 这意味着:容器内单个进程触发OOM,可能导致整个容器内所有服务进程被连坐杀死,这是"容器中核心服务无故全部挂掉"的物理根因
  • 业务无辜被杀完整根因溯源(全底层链路复盘)
  • Linux OOM Killer 计分模型源码拆解:进程 adj 权重、内存透支机制与业务无辜被杀根因溯源7.png
  • 结合上述所有内核底层机制,可精准复盘99%线上业务无辜OOM被杀的核心根因,完全区别于表层业务内存溢出结论:
  • 1. adj权重归一化适配失效:大内存服务器未适配内核adj归一化机制,微调的低权重配置被系统内存总量稀释,核心服务无法获得有效豁免; 2. 虚实内存权重认知偏差:核心业务长期占用稳定物理内存,OOM分数持续偏高,而突发透支内存的非核心进程虚拟内存不计分,最终核心进程被优先杀死; 3.Slab内核内存隐性挤占:内核缓存内存耗尽系统空闲资源,用户进程无内存可分配,OOM机制只能清算用户态正常进程; 4. 内存透支突发击穿机制:长期虚拟内存透支累积,流量高峰瞬时物理内存落地,系统无缓冲时间,直接触发强制OOM杀进程逻辑
  • 高可用自愈模型与生产环境调优实战(落地方案)
  • Linux OOM Killer 计分模型源码拆解:进程 adj 权重、内存透支机制与业务无辜被杀根因溯源8.png
  • 摒弃常规重启、扩容、简单调adj的表层方案,基于内核底层机制
  • 构建纯内核约束级的高可用OOM自愈规避模型,从根源杜绝无辜OOM与突发OOM故障
  • 精准权重隔离模型(基于adj内核归一化适配)
  • 突破常规固定adj配置误区,基于系统总内存、容器内存限额做动态adj权重适配,抵消内核归一化稀释效应
  • 核心服务配置极限豁免权重(oom_score_adj = -1000),从计分模型底层直接跳过OOM计分筛选,降低被杀优先级
  • Slab 内存可控回收内核机制
  • 针对内核Slab隐蔽挤占问题,构建定时精准回收模型
  • 针对dentryinode等高频膨胀缓存做定向回收,规避全局Slab内存溢出
  • 同时监控内核态内存水位,将Slab内存占用纳入系统内存压力判定指标,提前触发内存节流,避免被动OOM
  • 内存透支前置拦截模型
  • 打破内核乐观分配机制的弊端,基于cgroup内存记账、进程虚拟内存透支量做前置监控
  • 实时统计进程虚拟内存与物理内存差值,识别隐性透支进程
  • 在内存透支量逼近系统阈值时,触发前置内存回收、流量限流,杜绝瞬时物理内存击穿引发的突发OOM
  • 内核源码片段:OOM通知机制与豁免判定(v6.1 稳定版
  • // linux/mm/oom_kill.c (v6.1 stable | oom_notify_list 定义位置)
    static BLOCKING_NOTIFIER_HEAD(oom_notify_list);
    
    int register_oom_notifier(struct notifier_block *nb)
    {
            return blocking_notifier_chain_register(&oom_notify_list, nb);
    }
    EXPORT_SYMBOL_GPL(register_oom_notifier);
    
    int unregister_oom_notifier(struct notifier_block *nb)
    {
            return blocking_notifier_chain_unregister(&oom_notify_list, nb);
    }
    EXPORT_SYMBOL_GPL(unregister_oom_notifier);
    
    // include/linux/oom.h (v6.1 stable | OOM_SCORE_ADJ_MIN 宏定义)
    #define OOM_SCORE_ADJ_MIN      (-1000)
    
    // oom_kill.c (oom_badness 内部) 豁免判定为内联逻辑,无独立函数的 oom_skip_task
    static bool oom_unkillable_task(struct task_struct *p)
    {
            if (is_global_init(p))
                    return true;
            if (p->flags & PF_KTHREAD)
                    return true;
            return false;
    }
  • 以上为兜底前的主动拦截,而一旦拦截失效、OOM不可避免时,需要利用内核原生通知机制与豁免判定接口实现最后一层防御
  • 具体而言,基于内核OOM通知机制,可构建用户态与内核态联动自愈链路
  • 生产环境调优实战建议
  • 内核参数调优:在 /etc/sysctl.conf 中配置 vm.overcommit_ratio=50、vm.swappiness=10、vm.min_free_kbytes=保留内存值,执行 sysctl -p 生效,无需改内核源码 Slab 监控:通过 cat /proc/slabinfo 或 slabtop 命令观察 inode、dentry 等缓存是否异常增长,配合 memcg 的 memory.kmem.slabinfo 定位泄漏源,发现异常直接清理或限制对应 cgroup cgroup 内存限制:在 /sys/fs/cgroup/memory/ 下为关键业务配置 memory.limit_in_bytes 和 memory.soft_limit_in_bytes,设置合理的 oom_guard 阈值,避免业务进程被误杀 告警体系:对 MemAvailable 低于 10%、Slab 占比超过 40%、OOM Killer 触发次数等指标设置实时告警,提前介入 优雅降级:在内存压力达到预警线时,自动触发非核心服务的优雅降级,释放内存给核心业务
  • 内核原生补充证据——OOM完整执行链路:选定→标记→SIGKILL→Reaper 异步回收v6.1 稳定版):
  • 前面两段代码分别展示了OOM通知机制和豁免判定逻辑,但选定目标后的执行链路是多数开发者未知的盲区
  • 内核实际执行OOM分为三步:
  • 1.mark_oom_victim():标记 TIF_MEMDIE 线程标志,授予内存储备访问特权,打破分配死锁; 2.__oom_kill_process():发送 SIGKILL,同时遍历所有共享同一 mm 的进程连坐杀死; 3.queue_oom_reaper():延迟 2 个 jiffies 后启动内核线程异步回收物理内存
  • // (1) 标记受害者,授予内存储备访问特权
    static void mark_oom_victim(struct task_struct *tsk)
    {
            struct mm_struct *mm = tsk->mm;
    
            WARN_ON(oom_killer_disabled);
            // 设置 TIF_MEMDIE 标志:持有此标志的进程可从内存储备中分配
            if (test_and_set_tsk_thread_flag(tsk, TIF_MEMDIE))
                    return;
    
            // 绑定 oom_mm,供 oom_reaper 后续回收
            if (!cmpxchg(&tsk->signal->oom_mm, NULL, mm))
                    mmgrab(tsk->signal->oom_mm);
    
            __thaw_task(tsk);                    // 唤醒被 frozen 的进程
            atomic_inc(&oom_victims);             // 全局 victim 计数 +1
    }
    
    // (2) 执行杀进程:SIGKILL + 共享 mm 连坐 + 触发 Reaper
    static void __oom_kill_process(struct task_struct *victim,
                                   const char *message)
    {
            struct task_struct *p;
            struct mm_struct *mm;
            bool can_oom_reap = true;
    
            p = find_lock_task_mm(victim);
            if (!p) {
                    // 目标已在退出,跳过
                    put_task_struct(victim);
                    return;
            } else if (victim != p) {
                    get_task_struct(p);
                    put_task_struct(victim);
                    victim = p;
            }
    
            mm = victim->mm;
            mmgrab(mm);
    
            count_vm_event(OOM_KILL);            // 内核事件计数
            memcg_memory_event_mm(mm, MEMCG_OOM_KILL);
    
            // 发送 SIGKILL
            do_send_sig_info(SIGKILL, SEND_SIG_PRIV, victim, PIDTYPE_TGID);
            mark_oom_victim(victim);             // 标记 TIF_MEMDIE
    
            task_unlock(victim);
    
            // 核心连坐逻辑:杀死所有共享同一 mm 的其他进程
            rcu_read_lock();
            for_each_process(p) {
                    if (!process_shares_mm(p, mm))            // 不共享 mm 的跳过
                            continue;
                    if (same_thread_group(p, victim))          // 同线程组已杀
                            continue;
                    if (is_global_init(p)) {                   // init 进程不能杀
                            can_oom_reap = false;
                            set_bit(MMF_OOM_SKIP, &mm->flags);
                            continue;
                    }
                    if (unlikely(p->flags & PF_KTHREAD))       // 内核线程跳过
                            continue;
                    do_send_sig_info(SIGKILL, SEND_SIG_PRIV, p, PIDTYPE_TGID);
            }
            rcu_read_unlock();
    
            if (can_oom_reap)
                    queue_oom_reaper(victim);     // 触发异步回收
    
            mmdrop(mm);
            put_task_struct(victim);
    }
    
    // (3) OOM Reaper:延迟 2 个 jiffies 后异步回收受害者物理内存
    #define OOM_REAPER_DELAY (2*HZ)
    
    static void queue_oom_reaper(struct task_struct *tsk)
    {
            if (test_and_set_bit(MMF_OOM_REAP_QUEUED,
                                 &tsk->signal->oom_mm->flags))
                    return;  // 已入队的不重复添加
    
            get_task_struct(tsk);
            timer_setup(&tsk->oom_reaper_timer, wake_oom_reaper, 0);
            tsk->oom_reaper_timer.expires = jiffies + OOM_REAPER_DELAY;
            add_timer(&tsk->oom_reaper_timer);   // 2 个 jiffies 后唤醒 reaper
    }
  • 源码维度核心结论:
  • TIF_MEMDIE 标志:被标记的进程可从系统内存储备(memory reserves)中分配页面,这是打破"内存硬死锁"的核心机制——受害者自己需要分配内存来执行退出路径; 共享 mm 连坐机制:for_each_process + process_shares_mm 确保不遗漏任何共享地址空间的用户进程,解释"一个线程 OOM、整个进程组被连坐杀死"的现象; OOM Reaper 异步回收:延迟 2 个 jiffies(约 20ms)后异步执行 __oom_reap_task_mm 解除匿名页面映射,避免 kill 后内存释放不及时导致再次触发 OOM; 生产意义:理解 OOM 执行链路后,可在用户态通过 register_oom_notifier 拦截 out_of_memory 通知(见第 7.3 节),在 SIGKILL 发送前做最后的资源回收抢救。
  • 总结
  • Linux OOM Killer从来不是"内存不够杀进程"的简单逻辑
  • 而是内核内存记账体系、虚实内存权重博弈、Slab内核缓存约束、乐观内存分配机制、adj归一化算法共同作用的底层系统工程
  • 本文通过完整拆解v6.1源码,清晰揭示了OOM从判定到执行的完整链路:
  • 环节	源码函数	核心机制
    豁免判定	oom_unkillable_task()	过滤 init 进程与 PF_KTHREAD 内核线程
    豁免判定	oom_badness() 内部 adj==OOM_SCORE_ADJ_MIN	adj=-1000 直接返回 LONG_MIN 跳过计分
    计分模型	oom_badness()	RSS + SWAPENTS + pgtables_bytes 三维度加权 + adj 归一化
    目标选择	oom_evaluate_task() + select_bad_process()	遍历全局进程,选取 points 最高者
    通知机制	blocking_notifier_call_chain	用户态可通过 register_oom_notifier 拦截
    SIGKILL	__oom_kill_process()	发送 SIGKILL + 共享 mm 连坐杀死
    死锁打破	mark_oom_victim()	设置 TIF_MEMDIE,授予内存储备访问权限
    异步回收	queue_oom_reaper()	延迟 2 jiffies 后内核线程回收匿名页面
    Slab 检测	should_dump_unreclaim_slab()	检测不可回收 Slab 是否超过所有用户 LRU 页面
            
  • 所有表层OOM问题,本质都是对上述源码级规则的适配缺失
  • 普通开发者聚焦业务内存占用与表层参数调优,资深架构师聚焦内核计分模型、内存透支机制、隐性内存挤占与执行链路的底层约束
  • 这也是线上OOM疑难故障无法通过常规手段解决的核心原因
  • 本文所有逻辑均基于Linux v6.1稳定版内核原生源码机制
  • 附带精准代码片段与官方溯源链接,无任何经验性玄学,可直接用于线上高可用系统底层稳定性治理