ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux内核内存管理:SLAB、SLUB与SLOB分配器解析与排障指南

Linux内核内存管理:SLAB、SLUB与SLOB分配器解析与排障指南 搞内核这段时间我最大的一个感受是内存管理这摊水表面看是伙伴系统加页表的事但真正把内核日常跑起来的其实是 slab 那套小对象机制。你打开的每个文件、创建的每个进程、发的每个网络包背后都有各种内核对象在 slab 缓存里反复进出。SLAB、SLUB、SLOB 这三个名字学内核的朋友基本都听过可真到了项目里很多人一聊就含糊现在默认用的到底是哪个三者什么关系能随便切换吗为什么我服务器的 Slab 内存一直涨这篇文章我打算把这些事一次说透。不会去逐行粘贴源码而是把三种分配器的设计思路、取舍逻辑、实际配置和排障手法串起来。适合刚接触内核内存管理的人也适合那些已经会敲 slabtop、但在问题面前不知道往哪查的工程师。1. 三种分配器要解决的同一个问题在聊 SLAB、SLUB、SLOB 之前得先搞清楚一个前提它们解决的是同一个问题只是解题思路完全不同。换个说法这三兄弟争的是同一块地盘只是有的选择“精装修”有的选择“极简风”。1.1 从伙伴系统说起为什么内核非要搞一套小对象机制Linux 物理内存管理的地基是伙伴系统分配物理内存的最小单位是一个页通常 4KB。伙伴系统很适合分配整页内存因为它按 2 的幂次拆分合并外部碎片控制得不错而且分配路径非常直接。问题在于内核里真正高频分配的内存对象并不是整页而是几十字节、几百字节的小结构体。随便举几个例子task_struct 进程描述符、dentry 目录项、inode 文件索引节点、socket 结构、文件描述符这些对象会随着文件操作和进程调度疯狂创建和销毁。如果每次都向伙伴系统要一个 4KB 页哪怕对象本身只有几百字节剩下的空间也全部浪费掉。而且每次分配页、释放页都要走页表映射、zone 锁、页标志位维护这个开销即使是机器也很难扛住。更关键的是初始化成本。很多内核对象分配之后不是直接用而是要执行构造函数去设置初始值或者至少要把关键字段清一遍。如果每次都从伙伴系统拿到一个全新页面那么这个初始化操作也得跟着做一次。可如果对象被放到一个缓存里分配几次之后对象已经被初始化过了后边的分配就可以跳过构造阶段只需要把对象从空闲链表上摘下来就行。这就是 slab 分配器存在的根本原因把同尺寸、同类别的对象集中缓存在一起减少对伙伴系统的打扰同时降低初始化和内存碎片成本。1.2 直接感受一下不做缓存会亏多少我举个直观的例子帮助你理解。假设一个内核对象真实大小是 180 字节如果不经过 slab 层直接从伙伴系统分配内存你拿到的是一整页 4KB180 字节用掉剩下 4012 字节全浪费。换句话说这一页的内存利用率只有 4% 出头。但如果你建了一个 dedicated cache按 180 字节去切分这个页一页里能切出 22 个对象利用率立刻升到 98% 左右。再加上内部碎片控制、per-CPU 缓存、对象复用整体的内存效率和分配吞吐完全是另一个数量级。这其实就是 slab 类分配器最底层的收益公式大内存伙伴系统负责“批发”小对象缓存负责“零售”。内核不可能每次买一斤米都开一辆卡车去粮库slab 层相当于在楼下开了个小卖部。1.3 三兄弟定位对比一张表说清在往下深入之前先摆一张对比表把 SLAB、SLUB、SLOB 的整体气质说清楚。后面每个我都会单独展开。维度SLABSLUBSLOB设计哲学功能完善、精细优化简洁高效、去复杂化极致轻量、省内存优先内存开销中等偏高低极低分配性能优秀但锁竞争在高并发下偏重高尤其多核扩展性好差分配路径是线性扫描硬件缓存优化有 cache coloring、硬件对齐基本不做传统着色无调试能力不错非常强支持调用栈追踪几乎为零NUMA 适配支持但复杂支持且实现更简单不支持或极弱默认程度历史上长期默认现代内核默认仅嵌入式小内存场景适用场景老牌服务器、强调兼容与调参绝大多数现代服务器、桌面几 MB 到几十 MB 内存的嵌入式设备我个人的看法SLAB 像一个精装修的老房子每个细节都打磨过但管线复杂SLUB 是推倒重来后的现代公寓省掉了很多花哨设计结果住起来反而更舒服SLOB 则是个临时板房能遮风挡雨但别指望它有多舒服。2. SLAB 分配器传统派的精细与代价SLAB 是最早进入 Linux 内核的通用对象缓存方案设计思想来自 Solaris。在很长一段时间里它就是 Linux 内核的默认分配器很多老工程师提到 “slab” 时其实指的就是这个 SLAB 实现本身。2.1 三个队列加 per-CPU 缓存SLAB 的核心调度SLAB 里一个 kmem_cache 对应一类对象。每个 cache 下面挂着若干 slab每一个 slab 本质上就是一块连续物理页被切成多个大小相同的对象。SLAB 给每个 cache 维护了三类 slab 队列完全空闲的 empty 队列、部分使用的 partial 队列、完全用光的 full 队列。分配对象时优先从 partial 里拿拿不到再考虑新建 slab释放对象时如果 slab 变为 empty则根据水位策略决定是归还给伙伴系统还是继续留在 cache 里备用。这套三队列本身没什么稀奇的真正精妙的是它加了 per-CPU 缓存。每个 CPU 都有一个 array_cache里面缓存了一批刚刚释放出来的对象。分配时先看本 CPU 的 array_cache有就直接拿完全不碰全局锁没有再去 partial 队列里批量搬一批到本 CPU 缓存。释放时同理对象先回到 array_cache积累到一定水位再批量还给 slab。你看到这应该就明白了SLAB 在该省锁的地方非常舍得花心思。array_cache 的存在让同一个 CPU 上的分配释放操作绝大部分时间不需要跨核通信也不会触发锁竞争。这在单核、小规模多核时代简直是降维打击。2.2 cache coloring为了硬件缓存命中玩的偏移手段SLAB 一个容易被忽略但很有意思的设计是着色。这里说的着色不是给 slab 刷颜色而是为了躲开 CPU 硬件 cache 的 set 冲突。现代 CPU 的硬件缓存尤其是 L2/L3并不是简单的全关联。它把内存地址映射到固定的 cache set 上不同物理地址如果落进同一个 set那么它们会互相驱逐。如果内核把多个 slab 都从页面的同一偏移开始放第一个对象这些 slab 的第一个对象很可能映射到同一个 cache set你在一个 slab 上频繁访问对象时另一个 slab 上的热点数据就会被反复挤出硬件缓存造成性能抖动。SLAB 的做法是给不同的 slab 设置不同的首个对象偏移量也就是一组“颜色”。每个新的 slab 从颜色表里取一个颜色让对象在物理页面里错开位置尽量分散硬件缓存的集合冲突。这个优化在远古 CPU 缓存很小、关联度很低的年代效果很显著。但现代 CPU 缓存关联度已经高了很多着色带来的收益越来越弱SLUB 最终干脆放弃了这个功能。2.3 构造器、回收和 shrink 机制SLAB 的另一个传统是支持对象构造函数。创建缓存时可以通过 kmem_cache_create 传入构造函数之后每次分配对象时执行销毁对象时也可以执行析构函数。听起来很美好但在现代内核里构造函数用得很少了因为内核普遍倾向于“用哪个字段就初始化哪个字段”没必要让每个对象都经过一遍全量构造。SLUB 虽然也保留了这个参数但大多数情况传入 NULL。SLAB 对内存回收也做了不少工作。部分缓存会被标记为可回收比如 dentry、inode 这类缓存。内核在内存紧张时会优先扫描这些可回收 slab把它们占用的内存让给更紧迫的用途。同时 SLAB 还提供了 kmem_cache_shrink 这样的接口用于压缩缓存里空闲的 slab。实际运维中你要注意看到/proc/meminfo里 Slab 数值偏高不一定是内存泄漏也可能是正常的文件系统缓存只是没有达到回收阈值。3. SLUB 分配器一场教科书级的去复杂化实践SLUB 是在 2.6.23 前后合入内核的设计初衷非常直接嫌 SLAB 太复杂。它保留了 SLAB 的所有核心收益但把数据结构和管理路径大幅简化。现代主流发行版基本都是 SLUB你要在 2020 年之后的服务器上跑 uname 看内核十有八九用的就是它。3.1 SLUB 到底简化了什么SLAB 里每个 slab 的状态必须靠复杂的队列管理还要维护空闲对象链表、着色偏移、per-CPU array_cache 等一整套状态。SLUB 最大的变化在于它把“slab 本身的元数据”直接塞到了页结构里不再需要额外维护一套 slab 描述符的链表和状态机。分配时SLUB 从页结构里拿到空闲对象链表的头指针直接摘一个对象出来释放时把对象重新挂回空闲链表。一个 slab 要么是 partial要么是 full不再有 SLAB 那么复杂的 empty/full/partial 三态管理。当一个 slab 的所有对象都被释放后页面直接归还给伙伴系统没有那么多过渡状态需要维护。这种设计带来的收益很实在代码路径更短锁更少数据结构占用的内存更小。尤其是对大内存、多核、NUMA 架构的服务器来说SLUB 的扩展性比 SLAB 明显好。你想象一下一个大型数据库服务器上有几十个核同时做内存分配SLUB 的锁开销比 SLAB 低很多最终体现出来的就是吞吐量和延迟的差距。3.2 per-CPU partial 列表与远程释放的巧妙处理SLUB 虽然砍掉了 SLAB 的 array_cache但它引入了 per-CPU partial 列表。每个 CPU 持有一部分 partial slab只属于它自己别的 CPU 不会来抢。这样本 CPU 的对象分配和释放很多情况下连一个全局锁都不用碰只有在本 CPU partial 列表耗尽或者过多时才会去访问节点级的 partial 列表。这里有个很容易被忽略的细节远程释放。假设 CPU0 分配了一个对象后来 CPU2 把它释放了。SLUB 的处理方式是先看看这个对象原来属于哪个 slab 的 freelist根据 slab 的归属关系来决定放到哪个节点列表。如果非要跨节点访问则走锁路径。但因为有 per-CPU partial 列表兜底大多数释放操作都不需要跨 CPU 找锁这在多核场景下威力巨大。SLUB 还引入了几个重要的调控参数slub_min_order、slub_max_order、slub_min_objects、cpu_partial。比如 slub_min_objects 表示每个 slab 至少要放多少个对象系统计算 slab 的阶数时会拿这个值去换算避免为了凑一个大的 slab 而浪费内存。cpu_partial 则限制每个 CPU 上最多暂存多少个 partial slab防止某个 CPU 上对象堆积过多。这些参数都可以作为内核启动参数传入生产环境中一般不需要动但调优时它们就是最直接的抓手。3.3 SLUB 的调试能力比想象中强得多很多人以为 SLAB 调试功能丰富SLUB 简化了所以调试差事实正好相反。SLUB 在保持简单结构的同时提供了非常强大的排障手段。内核编译时如果开启 CONFIG_SLUB_DEBUG重启时可以在引导参数里加 slub_debug。常见选项有 F做完整性检查、Z红区保护探测越界写、P毒化对象释放后写入固定字节再次分配时如果值不对就说明有野指针、O对象跟踪记录每个对象的分配与释放调用栈、U用户态调用栈跟踪、Ttrace。组合起来常用 slub_debugFZPOU相当于把 slab 调试的常见手段全开。启动之后/sys/kernel/slab 下面会出现每个缓存的目录。你可以看到一个缓存里有多少对象、每个对象多大、每个 slab 里放多少对象、使用的 order 是多少、per-CPU partial 数量等。像 kmalloc-256 这种通用缓存也能在它的 alloc_calls、free_calls 文件里看到造对象时内核栈的统计信息。我遇到过一个内存泄露问题就是靠 SLUB 的 alloc_calls / free_calls 定位到是某个网卡驱动在收包路径上反复分配 skb 但不释放。SLAB 时代想做到这种粒度你得自己打 patchSLUB 直接把工具给你备齐了。4. SLOB 分配器极限小内存场景的取舍SLOB 全称是 Simple List Of Blocks它对标的场景和 SLAB/SLUB 完全不一样。SLAB 和 SLUB 是为了解决性能和碎片问题而 SLOB 的目标非常单纯尽量少占内存。它存在的意义就是把整个 slab 机制本身的内存开销降到最低。4.1 工作原理抛弃缓存回归最原始的链表SLOB 不按对象大小划分独立的 slab 缓存至少不像 SLAB/SLUB 那样为每个对象类别维护一套复杂状态。它直接把物理页面当成一块块内存块按实际请求的大小区分塞进一个简单链表里。分配时采用 next-fit 策略沿着链表找第一个足够大的块拆出来给请求方释放时把内存块放回链表必要时做前后合并。这种设计几乎没有任何额外开销。没有 kmem_cache 描述符没有 per-CPU 缓存没有 slab 状态机没有着色。内核里的对象直接从一个共享内存池里切你要 70 字节它就给你切一个 70 字节的块多了也不管理。这样一来SLOB 占用的元数据内存被压缩到极致非常适合内存只有几 MB 到十几 MB 的嵌入式设备比如老式路由器、开发板、微控制器系统。4.2 省了内存代价是什么SLOB 省内存省得痛快但绝不是免费的午餐。它最大的问题有两个碎片和性能。因为没有按对象大小做隔离各种各样的对象会互相穿插地出现在同一个页面上。今天分配一个 task_struct明天释放一个 socket 缓冲区后天再分配一个 skb这块内存就会被打得七零八落产生大量没法利用的外部碎片。碎片积累到一定程度可能连一个连续大块都分配不出来触发内核回收、甚至直接分配失败。性能问题更明显。next-fit 策略在对象数量少的时候无所谓可一旦缓存里对象成千上万每次分配都要在链表里走一圈复杂度就是 O(n)。在服务器上随便跑一个并发稍高的服务每秒分配的 skb 和 dentry 数以万计SLOB 的线性扫描直接能成为新的性能瓶颈。我在一个 64MB 内存的 MIPS 小板上试过 SLOB纯粹内存占用确实低但一旦跑起业务流量CPU 占用立刻飙升得不偿失。4.3 什么场景下 SLOB 才值得用SLOB 的适用面非常窄只适合那种内存小到 SLUB 的开销都嫌浪费、且对性能要求不高的场景。比如一个 8MB RAM 的工业控制器、一个跑 busybox 的极简系统。在这种环境里你省下的那几百 KB 内核内存可能比什么都珍贵。但我要提醒一句如果你的设备内存已经能到 32MB、64MB我都建议直接上 SLUB别用 SLOB。省下的内存有限调试能力却几乎归零以后出了问题你会非常痛苦。SLOB 连 /proc/slabinfo 都提供不了你想查每个对象占了多少内存都无从下手。5. 如何查看当前用的是哪种分配器聊了这么多理论该落地了。实际工作中你遇到的第一件事往往是我这台机器到底是 SLAB 还是 SLUB甚至是不是 SLOB判断方式其实很简单。5.1 一条命令快速判断分配器类型最直接的方法就是看内核编译配置。如果内核开启了 CONFIG_IKCONFIG_PROC可以直接看压缩配置zcat /proc/config.gz | grep CONFIG_SLUB大多数发行版没有开放这个接口那就去 /boot 下看grep -E CONFIG_(SLAB|SLUB|SLOB) /boot/config-$(uname -r)输出结果里通常只有一个 Y。比如CONFIG_SLUBy就说明当前内核编译时选的是 SLUB。如果没开 CONFIG_SLUB_DEBUG你还可以用文件系统特征来判断。SLUB 会在 /sys/kernel/slab 下导出缓存目录而 SLOB 系统里通常没有 /proc/slabinfo。所以ls /sys/kernel/slab | head -20如果这个目录存在且内容很多基本就是 SLUB如果你在 /proc/slabinfo 能看到内容说明至少是 SLAB 或 SLUB如果 /proc/slabinfo 根本没有那大概率是 SLOB。5.2 slabtop 应该怎么看确认分配器之后日常监控用 slabtop 就够了。它的输出是从 /proc/slabinfo 读取并处理后展示出来的。slabtop -o你会看到一个表格默认按对象数量排序列出缓存名、活动对象数、总对象数、对象大小、每 slab 对象数、页数、内存占比等信息。实际看的过程中我建议先按内存占用排序比如slabtop -s c其中 -s c 表示按缓存大小排序。看到 dentry、inode、kmalloc-*、skbuff_head_cache 这类缓存排在前面是正常现象。但如果发现某个专用缓存占用异常高、对象数持续增长不回落就要警惕了这往往是泄漏或异常扩大化的信号。配合 /proc/meminfo 里的 Slab 字段你能快速判断内核对象缓存占用的总内存grep Slab /proc/meminfo5.3 内核选型不是随便改的SLAB、SLUB、SLOB 是编译期决定的不是运行时可以热切换的配置项。内核编译时在 Kconfig 里选择对应的配置项是CONFIG_SLABCONFIG_SLUBCONFIG_SLOB如果你真的想切换得重新编译内核。现在主流发行版默认都是 SLUB我个人不建议你为了“试试 SLAB 的效果”去随便切换。SLUB 的简单路线在绝大多数场景下都更有优势SLAB 的复杂度换来不了多少额外收益。只有在你明确复现某个 SLUB 行为异常、且确认 SLAB 能规避时才值得去编译测试。6. 实战排障Slab 内存膨胀与定位理论知识再多最终都要落到排障上。这些年处理了不少 slab 相关的问题最常见的就两类一类是 Slab 内存持续上涨看着像泄漏另一类是明确怀疑某个内核对象类型在异常分配。下面分享几个我实操中反复用到的思路。6.1 场景一Slab 总内存居高不下这大概是群里问得最多的问题“服务器 /proc/meminfo 里 Slab 占了几个 G是不是内存泄漏了”很多朋友一看到 Slab 大就慌了其实要先判断是正常缓存还是异常增长。第一步做快照对比。间隔一分钟分别执行两次slabtop -o /tmp/slab_1.log sleep 60 slabtop -o /tmp/slab_2.log diff /tmp/slab_1.log /tmp/slab_2.log如果绝大多数缓存的名字没变总内存也基本稳定那就说明只是缓存水位高不是泄漏。dentry、inode 这类缓存大往往是因为系统里文件总数多、目录层级深容量本身是物理内存的反映。如果确认某个缓存的对象数持续单调上涨就进入下一步。先看这个缓存的 active_objs 和 num_objs 比例。active_objs 是正在被使用的对象数num_objs 是缓存里总对象数。如果 active_objs 很小num_objs 很大说明空闲对象积压可能只是缓存没收缩如果 active_objs 也和 num_objs 一起涨那就有真实对象没释放。常见原因是文件系统相关。比如 dentry 缓存异常你可以尝试触发回收sync echo 2 /proc/sys/vm/drop_caches这会清掉 dentry 和 inode 缓存。注意生产环境上执行前必须确认业务能接受缓存被清空带来的短时性能抖动尤其是文件服务场景。6.2 场景二定位对象泄漏的源头如果确认不是单纯缓存水位问题对象在真实泄漏就要上调试手段了。SLUB 环境下最有效的方案就是用 slub_debug 重启。在引导参数里加上slub_debugFZPOU然后等问题复现。复现后去 /sys/kernel/slab/出问题的缓存/ 目录查看 alloc_calls 和 free_calls。前者会告诉你这些对象都是在内核栈的哪些路径上被分配的后者告诉你释放路径。如果 alloc_calls 里某个函数路径占比异常高、free_calls 里几乎不出现对应路径那基本就是泄漏点。比如我曾经定位一个问题看到 alloc_calls 里全是 tcp_sendmsg 路径free_calls 却空空如也最后发现是某个版本内核 TCP 错误路径里没有释放 skb导致发送队列里的对象全部累积。这个结论拿到后问题解决窗口直接缩小到那个错误分支。另一个可用的工具是 kmemleak。需要内核开启 CONFIG_DEBUG_KMEMLEAKecho scan /sys/kernel/debug/kmemleak cat /sys/kernel/debug/kmemleak它会扫描内存中孤儿对象报告疑似未释放的内核内存块。kmemleak 的缺点是误报不少但它给出的调用栈信息在做第一轮粗筛时非常有价值。6.3 场景三SLOB 系统下没法用 slabinfo 怎么办如果你管理的是嵌入式设备而且内核确实用的是 SLOB那么很多 slab 查询手段都失效了。因为 SLOB 没有 /proc/slabinfo也没有 /sys/kernel/slab 目录。你只能从整体内存增长趋势去推断非常被动。这时候我建议你重新评估内核配置。如果设备内存还够尽量切到 SLUB。SLUB 虽然多一点元数据开销但换来的是可观测性。嵌入式系统调试本来就难再砍掉 slab 调试手段出了问题只能抓瞎。有一些 buildroot 老工程喜欢默认选 SLOB作为一种“省内存”的软配置这里我得泼盆冷水省的那点内存可能还没你一次内存泄漏浪费的多。6.4 常见问题速查表现象可能原因推荐排查手段Slab 总内存高但对象数稳定正常文件缓存 / 对象缓存堆积对比两次 slabtop 快照确认无持续增长某个缓存 active_objs 持续增长对象泄漏或异常持有slub_debugFZPOU alloc_calls/free_callsnum_objs 很大但 active_objs 很小空闲对象积压缓存未收缩drop_caches 触发回收或检查 shrink 接口kmalloc-* 缓存整体膨胀无法匹配专有缓存的大量小对象分配slub_debugU 追踪用户态调用栈dentry/inode 缓存巨大文件量多或路径解析压力大观察系统文件操作频率必要时调 vm.vfs_cache_pressure设备上 /proc/slabinfo 不存在内核用的 SLOB检查 CONFIG_SLOB考虑切回 SLUB 提升可观测性一开 slub_debug 系统变慢很多调试本身有开销尤其在热路径上缩小跟踪范围只对可疑缓存启用如 kmalloc-2567. 几个值得记住的底层细节很多朋友在看完前面这些内容后可能会问那我到底该记住哪些东西我觉得有几条经验是可以长期用的。第一slab 不是只有 SLAB。SLAB 是具体的一种实现slab 这个词已经泛化成“内核对象缓存机制”的意思。你日常说“查一下 slab”大概率在查的是 SLUB 分配器还在正常工作的状态。第二per-CPU 缓存是现代内核高性能分配的灵魂。无论 SLAB 的 array_cache 还是 SLUB 的 per-CPU partial本质上都是把分配频率高的对象留在本 CPU 手里尽量避免锁和缓存一致性开销。理解这一点你就能看懂为什么某些场景下绑核之后分配性能会提高。第三slab 可回收不代表会及时回收。内核有个 vfs_cache_pressure 参数控制 dentry/inode 缓存的回收倾向默认值 100。如果你的系统内存紧张但文件缓存还占着一堆内存可以适当调高这个参数比如 200。但别调到太高否则每次文件操作都会频繁重建缓存性能反而下降。还有一个我踩过的坑不要在生产环境轻易开启 slub_debugFZPOU 长期运行。开启后每个对象都会加红区、毒化、调用栈记录内存开销和 CPU 开销都不可忽略。我曾经在一台 128GB 内存的机器上开全量 SLUB 调试跑了一周Slab 直接多占了接近 2GB 内存业务延迟也有明显抖动。正确做法是让问题先复现再开调试抓现场问题定位完立刻恢复正常内核启动。这套东西其实没有太多玄学。它背后就是一个朴素的道理内核把最常用的工具放在最顺手的地方尽量少打扰别人。SLUB 之所以能成为默认选择靠的也正是这种“少做无用功”的克制。如果你正在排查一个诡异的内存问题别急着怀疑上层应用先到 slab 里看一眼很多时候答案就写在对象数量曲线里。
返回列表