资讯详情

Linux内核内存管理实战:从伙伴系统到OOM Killer排查调优

📅 2026/9/9 11:04:52 | 华诺云谱 👁 阅读
Linux内核内存管理实战:从伙伴系统到OOM Killer排查调优
Linux 内核内存管理原理与实践从伙伴系统到 OOM Killer 的实战笔记内存管理是 Linux 内核里最核心、也最容易让人犯迷糊的模块之一。不管是做服务器运维、嵌入式开发还是日常排查应用崩溃最终都会落到“内存到底怎么回事”这个问题上。我这些年看过不少内核源码也排过很多内存相关的线上事故最大的感受就是内存管理这块知识光背命令没用而是要把“虚拟内存、分页、分配器、回收机制”这条链路串起来才能真正看懂现象背后的原理。这篇文章不打算写成教科书我尽量用做项目、调问题时的实际视角把这套东西讲透。先解释核心概念和设计思路再讲关键机制的实现细节然后给出一套可以直接照着做的排查和调优实操最后把我踩过的一些坑和速查表分享出来。适合三类人看刚接触内核开发的嵌入式工程师、做 Linux 服务器运维的同学、以及面试前想系统梳理内存管理知识的后端开发者。1. 内容整体设计与思路拆解1.1 为什么内存管理是内核的“地基”先问一个问题为什么所有操作系统课程都要花大篇幅讲内存管理答案很简单因为 CPU 的执行效率和内存的访问效率直接决定了系统的整体性能。而现代操作系统里CPU 并不是直接访问物理内存的中间隔了一层叫“虚拟内存”的东西。这层抽象解决了几个世纪难题第一多个进程同时运行各自需要独立的地址空间谁也不能随便踩到别人的数据第二物理内存有限需要让“看起来很大的内存”跑在“实际上很小”的物理内存上第三内存碎片问题——频繁的分配和释放会让物理内存产生大量不连续的小块导致明明有空间却分配不出大块内存。Linux 内核解决这些问题的方式很有意思。它不像有些系统那样用复杂的段页式加上各种花哨策略而是把“分页机制”和“伙伴系统”组合起来再加上一个叫 slab 的缓存层把整条内存分配链路做得既简洁又高效。所以理解内存管理建议按“虚拟地址空间 - 页表映射 - 物理页分配 - 内核对象缓存 - 回收与压力管理”这条线去学而不是零散地看命令。1.2 内存管理系统模块地图我画了一张脑图帮你在脑海里建立一个整体框架后续所有内容都可以挂在这张图下面用户态视角每个进程拥有独立的虚拟地址空间通过 malloc/ mmap 向内核要内存。内核态核心模块缺页异常处理page fault、页表管理、伙伴系统buddy system、slab 分配器、页面回收page reclaim、OOM Killer。硬件协作单元MMU内存管理单元、TLB页表缓存、多级页表结构。核心数据结构struct page、struct mm_struct、struct vm_area_structVMA、pgdat/zone 等。这么多名字第一次看容易懵。别急后面每一块我都会拆开讲。你只要先记住两件事第一虚拟内存到物理内存的翻译靠页面映射第二物理内存的分配和释放主要靠伙伴系统和 slab。理解了这两条主线剩下都是围绕它们展开的旁支。2. 核心细节解析与实操要点2.1 虚拟内存与分页机制一切的地基先从最基础的虚拟内存说起。每个进程都有一个完整的虚拟地址空间比如 64 位系统上是 128TB 的地址空间。进程看到的是“连续”的地址实际上这些地址对应的物理内存很可能东一块西一块甚至根本不在物理内存里。CPU 访问内存时MMU 会拿虚拟地址去查页表找到对应的物理页帧号然后拼出物理地址。为了加快查找每级页表都有一定规则64 位 Linux 通常使用 4 级页表PGDPage Global Directory、P4DPage 4K Directory、PUDPage Upper Directory、PMDPage Middle Directory、PTEPage Table Entry。每级索引都从虚拟地址中取对应位段。这里用一个生活类比虚拟地址就像“门牌号”页表就像“小区物业登记表”你报出门牌号物业帮你查表告诉你具体楼栋和房间。查一次表可能不慢但每次访问内存都要查表就太累了所以 CPU 里放了一个高速缓存叫 TLB专门存最近用过的虚拟页到物理页的映射。这就是为什么大页HugePage能提升性能——它让单条 TLB 项能覆盖更大的内存区域。实操要点想感受一下页表和 TLB 的存在可以用perf stat观察程序的dTLB-loads和dTLB-load-misses。如果 miss 率很高说明程序局部性不好或者页太小。这种场景下换大页如madvise配合 THP往往有奇效。2.2 伙伴系统物理页分配的核心算法物理页分配这块Linux 用的经典算法叫伙伴系统Buddy System。它的核心思想是把物理内存按 2 的幂次方划分成不同大小的块order 0 是 1 页order 1 是 2 页依此类推每个 order 对应一个空闲链表。分配时从合适的 order 里拿块如果没有就向更大的 order 借一块然后一分为二直到刚好满足需求。释放时如果相邻的块也是空闲的就把它们合并成更大的块这就是“伙伴”的由来。这个设计妙在哪里分配和释放都是 O(1) 量级的操作只要操作链表头部就行。而且通过合并机制能有效缓解外部碎片问题。当然它也有缺点内部碎片——比如你要分配 3 页系统只能给你 4 页order 2多出来的 1 页就浪费了。所以内核才需要 slab专门用来解决小对象分配问题。实操要点查看系统当前各 order 的空闲页情况可以读/proc/buddyinfo。你会在里面看到各 zone 对应 order 0 到 order 10 的空闲块数量。如果高阶 order 全是 0说明内存碎片很严重这种时候即使 total 内存看似够用也分配不出大块连续内存。这也是为什么线上环境经常要打内存碎片预防补丁的原因。2.3 slab 与 slub小对象分配的战马伙伴系统最小单位是一页通常 4KB但内核里大量对象可能只是几十字节比如 inode、dentry、task_struct。如果每次都调伙伴系统浪费又慢。于是有了 slab 分配器后来演进为 SLUB默认和 SLOB嵌入式场景。slab 的思路是为每种常用对象类型预先分配一块内存切成等大小的对象槽位用链表管理空闲槽。分配时从槽位拿释放时还回去还要复用同一个 cache。这样就避免了伙伴系统的频繁拆分合并。有个细节值得注意/proc/slabinfo里可以看到每个 slab cache 的使用情况。有一年我们做嵌入式优化发现 dentry cache 占用异常高排查下来是频繁创建删除文件导致 dentry 回收延迟。这类问题如果不知道 slab 机制很容易误判成内存泄漏。实操要点如果怀疑内核对象缓存异常增长用slabtop命令实时观察。重点看ACTIVE和OBJS的数值。如果OBJS很高但ACTIVE很低说明大量空闲对象还没被回收可以尝试echo 2 /proc/sys/vm/drop_caches触发回收注意生产环境谨慎操作或者检查是不是有内核 bug 导致对象不归还。2.4 缺页异常懒加载的核心实现你可能会问进程申请内存时内核真的会立刻分配物理页吗答案是否定的这也是 Linux 内存管理最精彩的地方之一——惰性分配。当你调用malloc时用户态的内存分配器glibc 的 ptmalloc 等只是通过brk或mmap系统调用在内核里创建一个 VMA虚拟内存区域。此时只“记账”不实际分配物理页。直到进程真正访问这块地址时CPU 触发缺页异常page fault内核才在异常处理里分配物理页并建立页表映射。所以你会看到一个诡异现象一个进程申请了 10GB 内存RES很小但VIRT很大。不写不读就不占物理内存。这在场景化分配如大数组预分配但只用一部分里特别常见。而缺页异常也分两类一个是“minor fault”仅缺页表项物理页不用从磁盘读一个是“major fault”需要从磁盘换入比如 mmap 文件的页不在内存里。major fault 的性能代价是几十倍的差距优化程序时要尽量减少 major fault 次数。实操要点用ps -o min_flt,maj_flt,cmd -p PID可以看进程的缺页次数。如果 maj_flt 增长很快说明程序在做大量磁盘 I/O 换页这时候要考虑增加内存、调整文件读缓存策略或者优化访问模式。3. 实操过程与核心环节实现3.1 查看内存现状从 free 到 /proc/meminfo实践部分我先带你过一遍查看内存信息的常用命令这既是排查问题的第一步也是理解内核内存行为的窗口。free -h是最常用的。但你要注意看available这一列它和free的含义完全不同。free是真正未被使用的物理内存而available估算的是“在不触发 swap 的情况下还能分配多少内存”——它把可回收的 page cache 也算进去了。所以服务器上free很小但available很大是很正常的别看到 free 小就慌。/proc/meminfo则是一手内核数据。里面有MemTotal、MemFree、MemAvailable、Buffers、Cached、SwapTotal、SwapFree、Slab、SReclaimable、SUnreclaim等字段。其中SReclaimable是可以回收的内核对象内存SUnreclaim是回收不了的部分——它如果持续增长大概率有内核对象泄漏问题。实操记录我之前排查过一个 Java 服务内存告警的问题。free -g显示用了 30GB但 Java 堆只配了 16GB差了 14GB。后来用cat /proc/meminfo | grep -E Slab|SReclaimable|SUnreclaim发现 SReclaimable 占了 8GB再配合slabtop定位是 dentry 缓存。最终用sysctl vm.vfs_cache_pressure200提高了回收优先级问题就缓解了。整个过程就是靠逐层查看。3.2 定位内存占用Top 命令常用的 3 个坑很多人在服务器上排内存问题第一反应就是top。但top里的RES常驻物理内存和VIRT虚拟内存经常误导人。先明确一下RES 是进程实际占用的物理内存但它包含了共享库、共享内存等可能多个进程共同使用的部分。真正反映“私有内存”的指标是PSSProportional Set Size和USSUnique Set Size这需要用smem或者读/proc/PID/smaps来看。其次VIRT巨大不代表真的吃了这么多内存它可能是 mmap 了大文件、GPU 头显内存预留等只建了映射没实际使用。第三top 里按内存排序默认是 RES但如果你想找“谁在快速增长内存”光看 RES 不够建议用pidstat -r 1按刷新率观察 miss 和 rss 变化。实操要点一条很有价值的组合命令是for i in $(ls /proc | grep -E ^[0-9]$); do awk /VmRSS/ {rss[$i]$2} /VmSwap/ {swap[$i]$2} END {} /proc/$i/status; done再结合ps -o pid,cmd --sort-rss | head可以快速找出 TOP 10 内存消耗进程。如果进程退出过这里看不到再用dmesg查 OOM 日志。3.3 OOM Killer 为什么要杀你的进程当系统内存严重不足页面回收也腾不出空间时内核会祭出最后手段OOM Killer。它按oom_score选进程杀掉释放内存。oom_score的计算逻辑是基础分加上内存占用修正分数越高越容易被杀还要考虑oom_adj和oom_score_adj可以手动调权。/proc/PID/oom_score可以查看实际分数/proc/PID/oom_score_adj可以设置范围 -1000 到 1000-1000 表示禁止被 OOM 杀死。实际调试 OOM 时首先要看dmesg | tail里的 OOM 日志。日志里会列出当时触发 OOM 的进程、系统的内存统计、各进程的 oom_score、被杀进程的位置进程名称和调用栈。特别关注 “Total pages” 和 “Free pages” 的数值如果 Total 远大于物理内存说明 overcommit 开启且吃满了虚拟空间。实操记录我碰到过一次 MySQL 实例被 OOM 的问题日志显示是另一个使用mmap预分配了 30GB 的离线任务在高峰期触发全局 OOM。最终方案是给 MySQL 设置oom_score_adj-800同时给离线任务设置 cgroup 内存限制。这样既保证了核心业务不被误杀又把离线任务约束在可控范围内。3.4 调优参数实战从 swappiness 到 overcommit内存调优是最容易翻车的地方因为参数太多了而且“最优值”因场景而异。这里我只讲几个真正影响大的参数并给出适用场景。第一个是vm.swappiness。它控制的是“内核在内存压力下更倾向回收匿名页进程内存可能触发 swap还是文件页page cache直接丢弃”。默认值通常是 60范围 0-200。如果你希望减少 swap 使用比如跑数据库可以调低到 10如果你希望尽量缓存文件、能 swap 就 swap就调高。不过要特别注意swappiness0并不等于完全禁用 swap它只是表示“极端才会换出匿名页”。想禁用 swap得用swapoff。第二个是vm.overcommit_memory。默认是 0启发式简单说就是内核自己判断是否允许超额分配。设为 1 表示总是允许超额分配适合内存吃紧但运行特殊程序的场景设为 2 表示禁止超额分配配合overcommit_ratio指定可超配百分比。生产环境我一般倾向保持默认 0因为调成 1 容易在极端情况下直接触发 OOM调成 2 又可能导致 malloc 频繁失败。第三个是vm.min_free_kbytes。它控制各 zone 保留的空闲低水位。适当调大可以在内存压力到来时给内核保留足够的紧急分配储备防止出现“无法分配内存来执行回收内存”的死锁。但调太大会白白浪费内存。经验值一般 4GB 内存的机器设置 64MB16GB 内存设置 128MB也可以结合zone_reclaim_mode一起调。实操要点任何sysctl修改都要先确认是否持久化到/etc/sysctl.conf或/etc/sysctl.d/。线上环境最好先在测试机验证一两天再全量推送避免因为参数不适配导致新的问题。4. 常见问题与排查技巧实录4.1 常见问题速查表我结合多年的实际经历把 Linux 内存管理高频问题整理成一张速查表方便你遇到现象时直接对照排查。现象可能原因快速排查命令解决办法系统内存看着快满但服务正常page cache 被文件读写占满free -h看 availablecat /proc/meminfo不用处理必要时vfs_cache_pressure调回收倾向进程 RSS 很高但实际业务占用不高共享库/share memory 重复计算smem -k、cat /proc/PID/smaps用 PSS/USS 指标评估突然出现 OOM 且杀掉的是核心服务oom_score 过高、cgroup 限制不严dmesg -T、查看被杀进程给核心进程调 oom_score_adj给任务设置 cgroup 内存上限Slab 内存持续增长内核对象泄漏或回收不及时slabtop、cat /proc/slabinfo检查相关文件系统/驱动临时 drop_cachesswap 使用率高系统卡顿内存压力过大或 swappiness 过高vmstat 1观察 si/so、free看 swap逻辑上扩容内存降 swappiness排查内存泄漏大块内存分配失败内存碎片或 overcommit 策略cat /proc/buddyinfo、dmesg看分配失败日志开启动态大页 THP调整 min_free_kbytes这张表不是万能的但它能帮你建立“现象 - 可能原因 - 排查命令”的思维路径。4.2 一次典型的线上内存泄漏排查实录说一个我印象很深的案例。某个后台服务每次上线跑 3-5 天内存就会涨满然后被 OOM 杀掉重启。第一次遇到这种问题很多人的第一反应是“看是不是代码泄漏了”但我们用 valgrind 和 ASAN 跑了很久也没发现明显泄漏。后来我决定从内核视角切入。先用ps -o pid,rss,vsz,cmd --sort-rss | head -5找出可疑进程再用pidstat -r 1 -p PID观察minflt/s和majflt/s。发现minflt高得离谱而且 RSS 在缓慢上升。接着用cat /proc/PID/smaps查看各部分 VMA定位到有一个小的 mmap 区域大小没变但 RSS 在涨。最后用perf record -e page-faults -p PID抓了一下发现是某个库在每次请求时调用posix_memalign分配了较大内存但只释放了一部分。虽然单次泄漏量不大架不住请求量大5 天就耗光了。这个案例给我的经验是排查内存泄漏不能只盯着用户态代码一定要结合内核的缺页统计、VMA 分析和性能采样工具才能快速定位。4.3 一些实用的独家避坑技巧接下来分享几个常规文档里不会写的经验技巧都是我在实际项目中踩过坑后总结的。第一别在内存压力大的机器上用echo 3 /proc/sys/vm/drop_caches这种命令。drop_caches 只清理 page cache但瞬间清空所有文件缓存会让后续所有读操作直接打回磁盘IO 延迟飙升。如果实在要清可以先用sync并且选择1或2而不是3或者在业务低峰期执行。第二要重视cgroup的内存限制。在容器环境里cgroup v2的memory.max是限制真实内存的。很多集群问题其实是某些容器超过 limit 后会直接触发 OOM而不是整个宿主内存耗尽。用systemd-cgls和cat /sys/fs/cgroup/.../memory.events可以快速定位到底是谁在超限远比看宿主机 free 来得精准。第三嵌入式场景里如果内存非常紧张要关注SLOB和SLUB的选择。小内存设备用 SLOB 能省很多静态开销但性能略差主流设备默认 SLUB 更合适。换内存分配器不只是改内核配置还要跑一轮长稳测试看看碎片率是不是真的下降了。4.4 大页机制HugePage 与 THP 的取舍最后说一个和高性能场景强相关的话题大页。传统 4KB 页在大量内存访问时TLB miss 会很严重。HugePage 提供 2MB 或 1GB 的大页让单条 TLB 项覆盖更大的区域这对数据库、JVM、大数据引擎都有明显效果。代价是大页内存分配是连续的外部碎片稍多且传统 HugePage 需要在应用里显式用mmapMAP_HUGETLB或者libhugetlbfs来支持。HugePage 一旦分配就不能像普通内存那样按页回收灵活性差。THPTransparent Huge Page解决的正是这个问题内核在缺页时自动尝试分配 2MB 大页对应用透明不需要改代码。听起来很美但它也有暗坑如果系统内存压力大THP 的 khugepaged 后台线程会频繁整理页表甚至触发更高的内存分配和回收反而引入毛刺和延迟。在数据库这类延迟敏感型业务里默认开启 THP 经常被建议关闭。此时可以设置never或madvise只有用madvise(MADV_HUGEPAGE)的 VMA 才启用 THP。实操要点查看系统 HugePage 配置用cat /proc/meminfo | grep Huge查看/配置sysctl vm.nr_hugepages。想要调整 THP写/sys/kernel/mm/transparent_hugepage/enabled可选值是always、madvise、never。RHEL/CentOS 系的数据库服务器我一般设成never或者madvise并开启vm.nr_hugepages让指定应用用经典大页。结尾一点个人体会在 Linux 内存管理这个领域摸爬滚打这么多年我的真实体会是积累了越多问题排查经验越会发现核心原理比命令技巧重要。命令是招式原理是内功。比如你知道了伙伴系统和 slab 的关系就不会在slabtop显示 dentry 缓存占用高时手足无措你理解了缺页异常和惰性分配就明白为什么malloc之后RES不涨是正常的你吃透了 OOM Killer 的打分机制就不会傻傻地在生产环境把oom_score_adj调到 -1000导致内核宁可杀别的进程也要保一个注定崩的服务。最后再分享一个小技巧每次遇到诡异的内存问题先别急着上网搜答案而是按这个顺序自查——先看/proc/meminfo总量和可用量再看进程级 RSS 和缺页统计最后落在dmesg和/proc/slabinfo上。把现象量化了再回到原理推演原因往往一分钟就有点子了。这比拿一堆监控图瞎猜省力得多。希望这篇长文能帮你真正打通 Linux 内存管理的任督二脉下次再遇到问题可以从容应对。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。