ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Linux内核Slab分配器:原理、SLUB实现与内存问题排查实战

2026/9/11 22:01:46 拓冰建站 浏览量
Linux内核Slab分配器:原理、SLUB实现与内存问题排查实战 1. 项目概述1.1 核心需求解析做Linux性能调优这些年我遇到过不少棘手的场景数据库节点内存莫名其妙被吃光、高并发服务在运行几天后出现卡顿、容器平台频繁报出内存不足但业务进程的内存占用看起来一切正常。这种时候大部分人第一反应是看free、top然后陷入迷茫——用户态内存明明没怎么涨内存到底去哪了答案往往藏在内核态的内存分配里。而说到内核内存分配Slab是绕不开的话题。我在排查实际故障时多次靠分析Slab信息找到了内存增长的真凶比如某个驱动版本存在对象泄漏、某个文件系统缓存的dentry对象数量异常膨胀。可以这么说不懂Slab你对Linux内存管理的理解就停留在用户态的皮毛。这篇文章不是教科书式的源码逐行分析而是把我实际踩坑、定位问题、做性能分析的经验整理出来。核心围绕三件事内核内存分配的整体链路是什么、Slab分配器的设计原理和数据结构、以及拿到一台内存异常的机器后如何用Slab相关的工具和指标快速缩小排查范围。另外会穿插介绍实际项目中遇到过的问题case包括如何通过调整Slab相关的内核参数来优化特定场景的性能。适合这样几类读者已经在用Linux但遇到内存问题无从下手的运维和SRE、刚接触内核开发的工程师、以及做云原生基础设施希望深入理解容器内存隔离底层原理的同学。如果你对内存管理的理解还停留在free和top的层面这篇文章能帮你打开新的一扇门。2. 内核内存分配的整体链路2.1 从伙伴系统到Slab的两级分配架构先理清一个基本框架。Linux内核管理物理内存的核心机制是伙伴系统Buddy System它以页通常是4KB为单位管理物理内存。伙伴系统的设计思想是把空闲内存按2的幂次方分成不同的链表比如order为0的链表挂的是单页块order为1的链表挂的是2页连续块order为2的链表挂的是4页连续块以此类推。分配内存时内核从满足需求的最小order对应的链表里摘一块如果找不到就向更大的order“借”然后逐级拆分。这套机制在分配大块内存时非常高效但它有个明显的短板内核里大量需要分配的对象远小于一个页的大小。比如一个task_struct结构体、一个dentry结构体、一个inode结构体大小从几十字节到几KB不等。如果每次都通过伙伴系统分配一个整页会造成两个问题一是内存内部碎片的浪费可能一个页只用了100字节二是频繁的分配和释放会导致伙伴系统的链表操作开销很大而且每次分配都要走比较重的路径。于是Slab分配器就出现了。它建立在伙伴系统之上充当“二级缓存”的角色。简单理解Slab从伙伴系统申请一批连续页面然后把这些页面划分成大小相等的对象维护一个对象池。需要分配小对象时直接从对象池里取一个释放时归还对象池这样就绕过了频繁的伙伴系统操作分配和释放速度会快得多。2.2 为什么需要单独一套对象缓存机制也许你会问既然伙伴系统之上的Slab本质是缓存分配那为什么不把所有小于一页的分配请求统一交给一个通用内存池而非要为每种对象建立独立的缓存这里就有Slab设计里一个很重要的思想对象类型的区分。不同类型的内核对象它们的生命周期、使用频率、初始化开销差异很大。以dentry为例文件路径查找时需要频繁创建和释放dentry对象。如果每次分配都是“拿一块内存、手动初始化字段、用完后手动销毁”性能会很差。Slab的思路是把“构造”和“析构”也纳入缓存体系——第一次从Slab缓存中拿到对象时做完整的构造函数初始化后续分配和释放时只做字段的清理和复用不再重复执行高成本的初始化操作。这就是kmem_cache_create接口中构造函数参数的用武之地。另一个原因是便于统计和监控。每个Slab缓存都有名字比如kmalloc-64、dentry、filp。通过/proc/slabinfo可以清晰看到每种对象的数量、占用内存这在排查内存泄漏时极其关键。你可以直接定位到某种对象暴涨进而反查是哪个内核子系统在疯狂创建对象这种精确到“对象类型”的可观测性是通用内存池无法提供的。2.3 经典Slab、SLUB与SLOB三种实现说到这儿必须提一个容易混淆的点平时我们说的Slab广义上可能指代三种不同的实现。早期内核用的是经典Slab分配器设计精巧但代码复杂在多核大内存的机器上锁竞争问题比较突出。从内核2.6.23开始SLUB分配器成为默认实现它简化了经典Slab的很多复杂逻辑把per-CPU的本地对象缓存做到更轻量同时保留了Slab的核心思想——每对象缓存。现在的绝大多数发行版你看到的内核里实际运行的其实是SLUB。SLOB则面向内存极度受限的嵌入式场景它把Slab的缓存机制退化掉所有小对象都从连续内存段里紧凑分配核心目的是节省内存而不是追求速度。平时做性能分析和排查几乎遇不到SLOB所以下文说Slab的地方实际上指的都是SLUB实现但在概念层面经典Slab里的缓存着色、对象复用等设计思路依然适用理解它们有助于看懂各种内核文档。我建议在弄懂原理时先按经典Slab的概念去理解对象、Slab缓存、per-CPU部分然后再对照SLUB去理解实际代码这样认知落差不至于太大。经典Slab在mm/slab.cSLUB在mm/slub.c两者在分配路径上的差异主要体现在元数据的管理方式上。3. Slab分配器的核心设计原理3.1 slab、cache、object三个层级理解Slab关键是抓住三个概念struct kmem_cache缓存描述符、slab一个或多个连续页构成的容器、object实际分配的对象。一个kmem_cache对应一种类型的对象它有三种关键状态kmem_cache_cpu当前CPU上的活动slab、kmem_cache_node每个NUMA节点上的部分空slab链表、以及全空和全满的链表。分配路径的核心优化策略是优先从当前CPU的本地活动slab取对象只有本地没有空闲对象时才去节点级链表寻找部分空slab实在不行再从伙伴系统分配新页面。用个生活化的类比想象你是餐厅厨师厨房台面上放着几种常用调料当前CPU的本地slab用完了就去后厨储物柜补货节点级链表储物柜也空了就打电话让供应商送货伙伴系统。你当然不想每次用盐都跑去供应商那里进货Slab的per-CPU缓存就是这份“台面调料”的功劳。对象是Slab管理的原子单位。每个对象在slab中占据固定大小的空间SLUB在对象之间嵌入了一些管理数据freelist指针用来串起空闲对象链表。分配时从freelist头部弹出一个对象释放时把对象重新挂回freelist。这个过程在无竞争的情况下也就是几十纳秒级别的操作非常快。3.2 内存着色与cacheline伪共享问题在经典Slab设计中有个有意思的技术叫缓存着色cache coloringSLUB里也有相关考虑。它的目的是解决什么问题呢硬件CPU的一级和二级缓存cacheline大小通常是64字节。如果所有slab里的对象都从同一个内存对齐偏移开始排列那么不同slab中相同位置的对象会映射到CPU缓存的同一组cache set上。当某个内核路径频繁访问多个slab中相同偏移位置的对象时就会造成缓存行冲突导致cache miss率上升。着色就是让不同的slab里的对象起始地址在页内偏移上做一个错开让它们散布到不同的缓存集合里降低冲突。SLUB里虽然没有完整保留经典Slab的着色机制但它在对象布局上做了一些对齐和redzone处理加之SLUB默认把freelist信息放在对象之间而不是单独管理实际效果上对象地址在页内的分布是比较分散的。现代CPU缓存容量大了很多着色带来的收益不如当年显著但这个设计思路在理解硬件和内存分配器交互时仍然很有价值。还有个相关问题值得注意per-CPU缓存其实也顺便规避了多核竞争下的伪共享问题。每个CPU有自己独立的活动slab不同CPU不会同时操作同一个对象的freelist因此避免了内核对象在多核间乒乓迁移导致的cacheline失效。这一点在高吞吐场景下非常关键也是SLUB能成为默认实现的重要原因之一。3.3 kmalloc系列接口如何走Slab路径Slab不只是给内核内部各类对象使用它还实现了通用的kmalloc接口。你在写驱动或内核模块时常用kmalloc(size, GFP_KERNEL)分配内存这个调用最终走的也是Slab/SLUB的分配路径。内核里预创建了一系列固定大小的kmalloc缓存覆盖从8字节到8MB的范围比如kmalloc-8、kmalloc-16、kmalloc-32、kmalloc-64、kmalloc-128、kmalloc-256、kmalloc-512、kmalloc-1k一直到kmalloc-8k等。kmalloc调用时内核根据请求大小找到不小于该大小的缓存从里面对应分配一个对象。所以分析/proc/slabinfo时你会看到大量以kmalloc-为前缀的缓存。这些缓存里的对象来源很杂可能是某个驱动分配的临时缓冲区也可能是网络层分配的sk_buff数据区无法直接看出是谁分配的。这也是Slab分析相对棘手的地方——kmalloc缓存对象的归属不好追踪。相比之下像dentry、inode、filp这样有明确名字的专用缓存就好排查得多。当然SLUB提供了追踪分配的机制编译内核时开启CONFIG_SLUB_DEBUG后通过slub_debug参数可以记录分配调用栈。不过这在生产环境里开销较大通常是出了问题后专门开启进行复现调查不适合常驻。3.4 GFP标志位与内存回收的关系讨论内核内存分配不能只看分配器本身还得看分配时的GFP标志。GFP_KERNEL和GFP_ATOMIC代表两种截然不同的语义GFP_KERNEL允许分配时睡眠等待内存回收这意味着可以触发页换出、收缩缓存等操作GFP_ATOMIC则用于中断上下文或持有自旋锁的路径不允许睡眠一旦在内存紧张时分配失败就立即返回错误。这跟Slab有什么关系关系很大。Slab分配器需要向伙伴系统申请页面作为新的slab容器时携带的就是调用者的GFP标志。休眠允许与否决定了在内存水位线不足时分配器是等待回收完毕再分配还是直接返回NULL。理解这一点才能在排查莫名分配失败时判断是内存真的耗尽还是因为上下文限制了分配能力。另外一个容易被忽略的点是__GFP_RECLAIMABLE标志。有些Slab缓存创建时明确标注了可回收性例如dentry和inode缓存它们的内存页在内存压力下可以被内核主动收缩释放。这些可回收的页会在内存管理器的LRU列表中有专门归属。因此即使一个系统free显示内存占用很高只要可回收Slab占了大头系统未必真的处于OOM风险中关键要区分“可回收”和“不可回收”两类。提示排查内存问题时先分清楚内存是被可回收缓存占用还是被不可回收对象占用很多时候能帮你快速判断要不要介入处理。如果全是可回收的dentry/page cache而业务本身没有明显的分配失败系统可能还在安全范围内。4. 性能分析与排查实战4.1 分析Slab状态的三个必知命令在真实环境里分析Slab最常用的三个工具是/proc/slabinfo、slabtop和perf。它们各有侧重配合使用效果最好。cat /proc/slabinfo会输出所有Slab缓存的详细信息包含对象数量、活动对象、每对象大小、每个slab的页数等。这是最底层的信息源。格式大致是slabinfo - version: 2.1 # name active_objs num_objs objsize objperslab pagesperslab : tunables limit batchcount sharedfactor : slabdata active_slabs num_slabs sharedavail关键看active_objs和num_objs两者差异越大说明空闲对象越多。如果一个缓存的active_objs持续增长且从不回落大概率存在对象泄漏。slabtop是top风格的交互式工具按对象数量或内存占用排序展示Slab缓存。执行slabtop -o可以按活动对象数排序一眼看到哪类对象涨幅最猛很多内核调优工作中的第一步就是把slabtop的输出记录下来作为基线。perf能派上用场的地方在于定位分配热点。通过perf record采样内核态调用栈再看分配路径上的函数占比可以确认是哪个子系统在频繁分配对象。举个例子如果perf采样里__kmalloc调用占比高且调用栈中频繁出现tcp_alloc_md5sig_pool之类的函数那你就找到了网络栈的分配热点。4.2 /proc/slabinfo字段逐个拆解很多人看到/proc/slabinfo密密麻麻的输出就发怵其实核心字段并不复杂。我挑几个关键的解释一下。objsize是每个对象的大小。objperslab是每个slab能放多少个对象它由slab所占页数和对象大小共同决定。pagesperslab就是这个slab占用的页数。tunables里的limit表示per-CPU队列中最多缓存多少个空闲对象batchcount是一次从节点链表搬运到per-CPU队列的对象数量。这两个参数在经典Slab里影响明显SLUB中逻辑有些变化但概念仍可参考。slabdata部分的active_slabs和num_slabs分别表示正在使用的slab数量和总slab数量两者差值代表完全空闲的slab数量。如果某个缓存中空闲slab很多说明对象被释放后slab容器本身没有归还给伙伴系统处于“留而不放”的状态。这是设计使然目的是应对突发分配但如果这种缓存多且持续不释放也会造成实际内存占用虚高。看一个实际案例。我在一次内存告警中执行slabtop发现kmalloc-192缓存有近200万个对象但活动对象只有几十万也就是好几GB内存几乎全是空闲对象占着。结合业务场景判断这个分配模式来自某个网络中间件在高峰期创建的大量小对象峰值过后对象释放了但SLUB的per-CPU缓存和节点缓存没有及时把空slab归还伙伴系统。这种场景下的处理手法是“等待观察”只要没有持续的对象创建内存会在系统内存压力增大时自动回收不建议手动干预。4.3 使用perf定位Slab分配热点当你想进一步了解到底是哪个模块在频繁分配Slab对象时perf是最直接的武器。假设我在一台压测机器上发现kmalloc-128缓存的对象数量增长异常我可以这样定位perf record -g -a -- sleep 5 perf report --sortsym,callchain在内核调用栈中查找__kmalloc相关的路径然后逐级展开看最频繁的调用来源。我自己实际遇到过一个典型案例某分布式存储节点内存持续上涨slabtop显示bio相关缓存数量非常大用perf采样后发现都来自块设备层的bio_alloc路径再往下关联到文件系统用到了direct IO。最终定位是上层存储服务在做4KB对齐的直写IO时bio创建频率过高而每次bio的释放没有及时归还给Slab缓存导致对象池膨胀。找到热点后有两种优化路线一是优化应用层合并小IO减少bio创建次数二是调整内核参数比如增大/sys/module/block/parameters/...下的某些队列深度参数。路线选择取决于业务形态但定位到具体分配路径是前提。4.4 观察对象数量变化趋势的正确姿势分析Slab状态时单次快照意义有限比较有价值的是看对象数量的变化曲线。我通常在排查内存问题时做这样一组操作记录当前/proc/slabinfo的快照保存一份baseline。等30到60秒后再记录一次对比不同缓存active_objs的变化。重点关注持续增长且增长速率稳定的缓存这类对象大概率存在泄漏或异常膨胀。对疑似缓存通过slabtop -s按缓存名排序重点盯一段时间。这个思路和排查用户态内存泄漏用的是同一套方法论——找增长点区分“该涨”还是“不该涨”。比如文件服务器上dentry对象增长可能是业务在大量遍历目录如果是合理操作就可以接受但如果是绝不应该频繁创建dentry的场景那就要怀疑是不是某些路径没有正确释放dentry引用。注意Slab对象数量短暂上升是正常的尤其是网络高并发和文件密集访问场景。判断是否异常核心看“回落”——正常情况下高峰期过后对象数量会下降不回落就要警惕。5. 常见问题定位与调优笔记5.1 内存泄漏排查对象只增不减怎么办Slab层面的内存泄漏特征非常明显某个缓存的活动对象数量线性增长且业务上找不到对应的正常增长逻辑。排查Skab内存泄漏常见的手法有这么几种。先确认是不是真的泄漏。连续采样几个小时甚至几天看增长趋势是否持续。如果业务量平稳但dentry、kmalloc-*缓存却不断攀升就有问题了。工具方面kmemleak是专门检测内核内存泄漏的利器它通过扫描内存中的指针来发现无人引用的已分配内存块。启用kmemleak需要在编译内核时开启CONFIG_DEBUG_KMEMLEAK使用echo scan /sys/kernel/debug/kmemleak触发扫描然后查看报告。另一种手法是配合slub_debug。通过内核启动参数slub_debugP启用页面完整性检查释放对象时会校验是否有越界写入通过slub_debugF则会在分配和释放时记录调用栈泄漏后可以通过/sys/kernel/slab/cache-name/trace追踪最近的分配历史。这个方法开销大适合临时复现问题。我处理过一次vmalloc相关的Slab泄漏现象是kmalloc-4k缓存对象数持续增长但kmemleak没有报告明显的未被引用内存。后来是通过slub_debugF重新复现并排查调用栈发现是一个第三方驱动在某条错误处理路径中分配了对象却因为异常分支提前返回忘记释放。这属于驱动资源管理的经典错误代码审查时的教训是每个错误分支都要检查资源释放路径是否完整。5.2 内存碎片化对Slab的影响内核内存碎片化是个隐蔽问题。虽然Slab向伙伴系统申请的通常是连续页面但申请较大order的页面比如order 3即连续的8页可能因为伙伴系统里没有足够大的连续块而失败即使系统总空闲内存还很多。这种场景在长期运行且频繁分配释放大内存的服务器上并不少见。伙伴系统的高阶连续内存被大量配置成低阶块而Slab这种“细碎”分配模式恰恰容易加深碎片化程度——因为它会把大页面拆散成小对象长时间运行后想把内存重新聚合回高阶块就很困难了。应对思路有三个层面。第一避免不必要的Slab大对象分配能用vmalloc的场景就用vmalloc它不需要连续的物理内存第二通过drop_caches或compact_memory触发内存规整把分散的页页迁移并聚合起来第三通过代码层面合并分配请求减少对高阶order的需求。# 查看内存碎片化指数 cat /proc/buddyinfo # 手动触发内存规整谨慎操作生产环境建议先在测试机验证 echo 1 /proc/sys/vm/compact_memory/proc/buddyinfo每个order对应列的数字代表该大小的连续块数量。如果你发现低order列数字巨大、高order列好几个都是0并且系统还会出现“分配大块连续内存失败”的告警碎片化就相当严重了。5.3 回收策略与shadow memory开销控制Slab缓存也不是只进不出。内核在内存水位低时会触发内存回收可回收的对象比如文件系统的dentry/inode缓存会被自动收缩。/proc/sys/vm/vfs_cache_pressure控制着内核回收目录项和索引节点缓存的积极程度默认值100值越大回收越积极。把这个值调高可以让内核在内存紧张时更努力回收文件系统缓存释放更多内存。还有一类和Slab相关的开销容易被忽视就是SLUB自身的调试设施带来的额外内存。slub_debug开启后每个对象会附加redzone、track、padding等元数据对象实际占用的内存会增大不少同时速度和启动时间也会受影响。生产环境默认不要开启这些调试选项只有在内核开发或定位疑难问题时才考虑临时开启。另外cgroup的内存限制也会影响Slab。容器场景下memory.kmem.slabinfo可以单独查看某个cgroup的Slab使用量。如果容器频繁触发OOM但业务本身内存不高就要考虑是不是内核对象在cgroup维度占用过多比如容器内频繁的文件操作导致dcache在cgroup里累积。这种情况下适当设置memory.kmem.limit_in_bytes可能有帮助。5.4 与容器内存统计相关的Slab问题容器化环境下Slab问题有个比较坑的地方内核对象的内存属于哪个cgroup统计起来并不总是直观。dentry、inode这类内核缓存在较新的内核中能归属到发起操作的cgroup但也不是百分百精确。我在排查一个Kubernetes节点内存异常时遇到过这样的情况节点上所有pod的RSS加起来离节点总内存占用差了一大截后来查下来发现是宿主机上的文件系统缓存和Slab对象占用了大量内存而这些Slab对象根本没计入任何pod。不同存储插件、节点级Agent都可能导致宿主机级别的Slab增长。这时候要结合/proc/meminfo里的Slab字段和page cache一起判断不要只看pod维度。在容器环境下定位Slab增长我推荐这样的流程先确认节点级Slab占用趋势再用slabtop找出上涨最快的缓存然后结合节点上运行的存储、网络、监控组件判断哪些组件会创建大量此类对象最后再针对组件做配置优化或升级修复。6. 我的实操体会与调优经验6.1 调优前先分清业务形态给Slab做调优不能脱离业务形态空谈。拿文件服务器和数据库服务器举例两者的Slab特征完全不同。文件服务器上dentry和inode缓存的膨胀是常态这些确实是文件访问带来的合理对象。如果vfs_cache_pressure设定过低内核倾向于保留更多dentry缓存高密集的小文件访问场景可能会提速但代价是内存占用会更大。反之数据库服务器通常不太依赖文件缓存适当调高vfs_cache_pressure更合理能避免文件缓存挤占太多内存间接减少换页。我实际调过一台跑MySQL的机器vfs_cache_pressure从默认100调到200后文件缓存回收更积极内存页换出压力降低查询延迟的毛刺少了。这里要反复测试因为代价是文件访问的缓存命中率可能下降。6.2 常用SLUB内核参数的调整指南针对SLUB分配器本身有几个参数值得了解但不建议轻易动slab_min_objects每个slab最少对象数影响slab整体大小的选择对性能影响较小。slab_max_order限制Slab向伙伴系统申请的最大order避免单个slab容器过大导致内存碎片。如果系统碎片化严重适当减小这个值可以降低申请连续大块内存的失败概率。slub_min_order/slub_max_orderSLUB中控制每个slab最小/最大order的参数默认为3即8页一般不需要改。slub_cpu_partial控制每个CPU上partial slab缓存的数量上限增大它可以提升分配性能但会占用更多per-CPU内存。在我自己的服务器上默认参数下绝大多数场景表现良好。除非你通过/proc/buddyinfo看到高阶内存块严重不足或者perf明确显示Slab分配路径是瓶颈否则不建议调整这些参数。盲目调参往往得不偿失。6.3 从内核版本和体系结构看Slab演进最后聊聊趋势。经典Slab到SLUB的演进解决了多核扩展性问题而最近内核版本的更新还引入了更多性能和可观测性方面的优化。比如SLUB的slab_strict_numa参数控制严格的NUMA本地分配策略在NUMA架构下减少跨节点访问延迟。另外一个值得关注的方向是struct slab和struct page的拆分。较新的内核把Slab元数据从页描述符中抽取出来成为独立的struct slab结构这样做的好处是减少内存占用并且让内存管理的语义更清晰。这部分对普通排查影响不大但看内核邮件列表和设计文档时理解这个背景会更容易跟上社区讨论。对我个人而言掌握Slab分析的意义不只是解决几个内存问题更在于建立一种“从分配器视角看系统”的思维习惯。内核开发、驱动调试、底层性能优化很多时候问题表象五花八门但最终都落在“内存到底怎么被使用的”这个根本问题上。搞懂Slab等于多了一把钥匙能从更底层去理解系统的运行逻辑。最后再分享一个小技巧排查内存问题时一定要把/proc/meminfo、/proc/slabinfo、/proc/buddyinfo以及perf的数据串起来看单看任何一个都容易误判。先看总内存水位和回收状态再看Slab对象结构然后用perf定位动态行为最后回到代码里确认根因。这套组合拳打下来绝大多数内核内存相关的问题都能有一个清晰的结论。