尧图精选

内核内存管理:Slab/Slub/Slob分配器原理与调优实战

🕒 发布时间:2026/10/2 11:56:51 📁 来源:尧图网络
刚接触内核内存管理的时候我第一个绕不开的坑就是Slab分配器。每次看内核文档都看到Slab、Slub、Slob三个词并排出现初看以为只是同一机制的三个命名结果一深究才发现它们是完全不同的三套实现甚至在不同内核版本、不同硬件平台上你遇到的可能是其中任何一个。更让我头疼的是很多教程只讲Slab的原理图但实际生产环境里默认用的早已是Slub而嵌入式场景里还藏着一个Slob。这篇文章我就把自己一路踩坑、翻源码、调参数积累下来的东西捋清楚从设计思路到分配流程再到调优和排障一次性说透。这篇文章适合正在读内核源码、被内存分配器绕晕的读者也适合那些在做内核性能调优、或者需要在银河麒麟这类国产系统上更换内核版本、编译定制内核的人。我会把三个分配器各自的设计取舍、核心数据结构、分配释放流程、对应启动参数以及常见故障排查方法全部拆开讲内容尽量贴近实操少讲空泛理论。1. 三个分配器同一使命不同性格1.1 为什么内核需要专用小块内存分配器先搞清楚一个基础问题内核已经有一个基于伙伴系统的页分配器为什么还要再搞一套Slab/Slub/Slob这是因为伙伴系统按页粒度管理内存最小分配单位是4KB默认页面大小。但内核内部有大量小于一页的小对象比如task_struct、inode、dentry、buffer_head它们动辄几十字节到几百字节。如果每次都向伙伴系统直接申请一页然后自己切成小对象会产生两个问题一是会大量浪费内存因为小对象不满一页时剩余空间就浪费了二是频繁申请和释放相同类型对象时内存反复分配、回收页表刷新和TLB压力都很大性能受影响。所以Slab类型分配器的核心思想就是为每种频繁创建销毁的对象类型建立独立的缓存池每个缓存池由若干个完整页组成的slab或slub中的slab构成每个slab被划分成多个等大小的对象。下次你再创建对象时直接从对应缓存里取一个已初始化或空闲的对象用完了再还回去。这样避免了频繁和伙伴系统打交道也避免了不同大小对象互相干扰导致的外部碎片。这个思路其实很像我们在用户态写代码时用的对象池。你要反复创建某个结构体与其每次malloc、free不如维护一个复用池用完之后把指针放回池子下次直接取用。Slab分配器就是内核层面最经典的对象池实现而且它做得更彻底不仅缓存对象本身还缓存对象初始化所需的部分状态比如构造函数这样拿出来的对象可以近乎零成本地复用。1.2 Slab、Slub、Slob的出身与定位三个分配器的出身完全不同。Slab是1980年代SunOS中引入的设计后来被Linux采纳并成为内核默认分配器。它的特点是每个缓存cache维护三个链表slabs_full所有对象都已被分配、slabs_partial部分对象被分配、slabs_empty所有对象都空闲。每次分配时优先从slabs_partial里找空闲对象否则从slabs_empty里面调一个新slab释放时则把对象还回原slab并调整链表归属。Slub是Slab的改进版2007年左右由Christoph Lameter重写从Linux 2.6.23开始成为默认分配器。它的名字是SLUB没太多深意就是区别于原有Slab。Slub的设计目标是减少Slab里的复杂链表管理和per-cpu队列开销把数据结构简化突出无锁或低锁竞争的per-cpu操作。它之所以成为默认一个关键原因是它在NUMA架构上的可扩展性更好因为每个CPU都有独立的freelist不需要频繁访问全局链表。Slob则主打极简和小内存占用主要用在嵌入式系统或内存极小几十MB级别的环境中。它没有复杂的slab结构而是用简单的对象链表配合几个内存区域的空闲块列表来管理。分配时采用首次适配的近似策略碎片率可能高一些但代码量少得多本身占用的静态内存也极小。从定位上讲追求极致性能、多核扩展、NUMA友好的服务器和普通桌面选Slub兼容旧内核接口、研究经典算法、或者需要跟踪调试旧代码选Slab内存极度有限、无MMU、或者说不希望分配器自身占用太多内存选Slob1.3 三者的核心数据结构对比我自己读源码时最大的痛苦在于三个分配器命名相近但结构不同。先把关键数据结构理清楚。Slab的核心结构是kmem_cache和struct slab。kmem_cache描述一个对象类型缓存包含对象大小object_size对齐要求每slab的页面数或对象数三个slab链表的管理头per-cpu的cpu_cache数组用来缓存少量空闲对象struct slab则描述一个实际的slab块它有一个struct list_head list挂到前述三个链表之一还有一个freelist指针指向空闲对象链表。对象存储区域紧跟在slab描述符之后如果是内置slab描述符则描述符本身也存放在同一页内如果是外部描述符则需要单独分配内存。这个细节对理解slab的额外内存开销很重要。Slub的核心结构同样是kmem_cache但字段跟Slab差异很大。最关键的是cpu_slab这是一个per-cpu变量里面保存freelist指向当前CPU上第一个空闲对象的指针page当前正在使用的slab页框partial部分空闲slab链表Slub的slab结构本身用的是struct page的复用字段比如slab_freelist、counters而不是单独定义一个struct slab。它把每个对象的大小写入页的slab_cache相关字段通过page-slab_cache找到所属缓存。这种设计大幅减少了元数据量。Slob的核心结构更简单kmem_cache内部维护一个free_blocks链表然后每个分配出来的slab块用一个小头记录空闲块信息和对象大小。sp_slabslob_page等结构来组织页面内对象。它没有对象大小一致的假设所以可以像普通堆一样从一个块里切出任意大小对象这也是它实现简单但碎片化的原因。特性SlabSlubSlob引入时间早期核心2.6.23后默认早期嵌入式元数据结构复杂slab描述符链表复用page字段精简极小块头链表每CPU缓存cpu_cache数组cpu_slab无锁队列无简单全局锁适用环境普通/研究、调试NUMA、多核、默认极小内存、无MMU碎片控制较好较好较差代码复杂度高中低2. 分配流程逐层拆解从kmalloc到cache2.1 kmalloc如何选择cache我们在驱动里写kmalloc(128, GFP_KERNEL)这背后是怎么变成Slub里的一次cache分配的实际上内核维护了一系列预先定义好的kmalloc cache覆盖2的幂次大小或某些特殊大小例如96、192等具体看kmalloc_info数组。当你调用kmalloc(size, flags)时内核先做指针合法性检查然后通过kmalloc_slab(size)找对应的cache。这一步的关键是size不是精确匹配而是向上对齐到最近的可用规格。比如你要140字节如果可用规格里有128和192那就会落在192字节的cache里。有些内核配置会把96字节和192字节加入kmalloc cache用来减少因为2次幂对齐带来的浪费。这个“向上对齐”的逻辑在64位系统上由于指针大小和调试选项会有些变化但总体思路不变。找到cache后调用kmem_cache_alloc(cache, flags)这才进入真正的分配器内部。对于普通GFP_KERNEL请求分配器还会区分是否允许睡眠、是否允许回收等这些通过flags传给底层页分配器。如果你拿GFP_ATOMIC去申请那么分配过程不能睡眠如果当前CPU的freelist为空就会走紧急路径甚至直接尝试从伙伴系统拿页但不允许做耗时的内存规整。2.2 Slab分配的完整路径虽然是历史方案但理解Slab路径能帮你对比出Slub的改进点。Slab的kmem_cache_alloc先拿到当前CPU的kmem_cache_cpu这个结构里有一个freelist指向当前CPU已缓存的一个空闲对象如果非空直接取用并更新指针这就是最快路径。如果当前CPU缓存为空则进入“慢路径”从缓存池的slabs_partial或slabs_empty链表中取一个slab。此时需要加cachep-spinlock因为全局链表是需要互斥访问的。拿到slab后从它的freelist里取对象同时把该slab重新挂载到合适的链表如果还剩空闲则挂slabs_partial如果满了则挂slabs_full。如果所有slab链表都空就需要让伙伴系统分配新页创建一个新的slab然后切分对象。这个流程的竞争点主要有两个一是per-cpu缓存耗尽后访问全局链表需要锁二是每个CPU的cpu_cache大小是有限的默认可缓存若干个对象当对象分配释放频繁跨越CPU时会出现缓存失效需要从其他CPU的缓存中“偷取”对象。2.3 Slub的cpu partial机制Slub把“尽量在per-cpu路径上完成一切”发挥到极致。它不再维护slabs_full/slabs_partial/slabs_empty三个全局链表而是维护一个partial链表同时每个CPU还有一个cpu_slab对象。分配时首选cpu_slab-freelist。如果非空就直接弹出对象。如果为空说明当前CPU正在使用的slab可能已经满了这时Slub会尝试从cpu_slab-partial获取一个之前的slab这个partial链表是per-cpu的不需要全局锁把它作为新的活动slab再从中取对象。只有当本CPU的partial也空了Slub才会去全局partial链表kmem_cache-partial取这个操作需要锁。如果全局也空就调用__slab_alloc进入更深的慢路径最终可能要new_slab()向伙伴系统请求新页。释放时也一样如果对象属于当前cpu活动slab直接归还freelist如果不是例如对象在另一个CPU创建的slab上则需要通过页面所属node找到其所属缓存然后根据情况把原子归还到空闲列表或把slab放入当前CPU partial。这里有一个很关键的“冷冻”逻辑slab的frozen标志表示它是否已被某个CPU锁定。只有非冻结的slab才能被放入partial链表。Slub把大量操作都解耦成优先无锁读写当前CPU变量其次才触碰全局。这个设计对多核、NUMA场景非常友好。实测中我们在多个CPU同时高频分配释放对象时Slub的per-cpu路径命中率非常高锁竞争远低于Slab。2.4 Slob的简单链表Slob的设计目标就是最小化代码和内存开销。它维护slob_free空闲块链表每次分配时在空闲块里找到一个大小足够的块采用first-fit策略然后把块切成所需大小剩余部分重新插入空闲链表。如果找不到合适的空闲块就向页分配器申请新页并把整页放入空闲链表管理。因为采用first-fit且没有对象池概念Slob不需要为每种对象大小建独立cache所有普通kmalloc请求共享一套堆。这节省了caches的元数据开销副作用是Heap碎片化会更严重而且多核并发时需要通过全局锁保护链表的操作CPU扩展性很差。所以Slob只适合那种基本不考虑扩展性、内存量极小并且运行线程很少的场景。3. 关键参数与调优实操中必须知道的事3.1 /proc/slabinfo解读无论哪种分配器内核都会通过/proc/slabinfo暴露kmem_cache信息。但是Slab和Slub的字段含义有些不同尤其active_objs和num_objs的统计口径你需要仔细确认。典型的/proc/slabinfo输出长这样slabinfo - version: 2.1 # name active_objs num_objs objsize objperslab pagesperslab : tunables limit batchcount sharedfactor : slabdata active_slabs num_slabs sharedavail kmalloc-512 512 640 512 64 8 : tunables 0 0 0 : slabdata 10 10 0第一行是header后面每行表示一个cache。列中active_objs表示当前正在被使用未归还的对象数num_objs表示这个cache里所有对象总数两者相减就是空闲对象数量。如果num_objs远远大于active_objs说明这个缓存里堆积了大量空闲slab可能是不正常泄漏或者只是单纯的高峰遗留。objperslab和pagesperslab表示每个slab如何切分。在Slub中objperslab可能根据对象大小自动计算但也可以通过启动参数强制指定。tunables列在Slub下通常是0因为Slub不再使用limit、batchcount等传统Slab调优项。如果你用SysRq或者通过/sys/kernel/slab/cache/下的节点可以看到更详细的参数甚至能实时改变某个cache的cpu_partial、min_partial等值。记得在改之前先备份并在测试环境验证。3.2 slub_debug、slub_min_order等启动参数内核启动时可以通过内核命令行参数对Slub进行配置这些参数在内核文档的admin-guide/kernel-parameters.txt里都有记录但很多人不读。我总结几个关键的slub_debugP开启Slub的调试功能。常用的组合还有slub_debugU跟踪未初始化内存、slub_debugF跟踪释放后使用的内存、slub_debugZ把对象用特定模式填充以检测修改、slub_debugA打开全部检查。开启后每个slab对象在分配和释放时会附加红区redzone、毒化字节和跟踪信息变相加大对象尺寸和操作开销所以生产环境不要开排查问题时才开。slub_min_order、slub_max_order和slub_min_objects控制Slub分配页面的阶数order。slub_min_objects表示每个slab至少要放几个对象slub_min_order是最少页面阶slub_max_order是最大页面阶。默认情况下Slub会尽量让每个slab包含少量对象以减小浪费但对大对象比如几百KB会允许更高的order。如果内存碎片让你在分配大对象时频繁失败可以降低slub_max_order强制用更小的连续页块。slub_memcg...slub_nomergeslub_nomerge可以禁用kmem_cache的合并。内核默认会把大小相同或兼容、flags也兼容的kmem_cache合并到一个cache里以节省内存。但合并后会导致某些调试和审计信息丢失如果需要单独跟踪某个类别的对象可以在启动参数加slub_nomerge。slab_merge、slab_nomerge同理适用于Slab/Cache合并。需要注意这些参数在Slab分配器下的对应项不同Slab有slab_min_order、slab_max_order等另外slab_nomerge名称相同。启动参数选错时内核可能直接忽略不会有明显报错。3.3 真实调优案例减少内存碎片/提升性能我之前调试过一个网络转发模块在长时间运行后发现分配sk_buff相关的cache占据了大量内存/proc/slabinfo里skbuff_head_cache的num_objs持续增长但active_objs增长不明显。这是因为网络收发路径上的对象创建/释放频率极高Slub的partial链表里积累了太多半空的slab它们的页面无法归还伙伴系统。当时我用cat /proc/slabinfo观察又通过slabtop -s c按活性排序发现很多cache的空闲率大于50%。解决办法不是盲目重启而是先用“收缩cache”的方法echo 1 /sys/kernel/slab/skbuff_head_cache/shrink。这会主动回收部分空slab。如果系统中很多cache都膨胀还可以用echo 1 /proc/sys/vm/drop_caches只影响页缓存对slab不一定有效但通过sync和echo 3会间接回收一部分。更彻底的工具是slabtop命令能实时看到各Cache的大小和活性适合日常巡检。另一个性能相关的案例在某个多路服务器上我发现某些进程的kmalloc-64cache的锁竞争非常高。用perf lock观察锁热点后发现是任务创建的线程频繁用kmalloc(64)分配小对象而kmalloc-64在Slub里被合并到其他cache导致cache上存在跨CPU的partial对象传递。当时我配合slab_nomerge和调整slub_min_objects、cpu_partial参数手动设置了/sys/kernel/slab/kmalloc-64/cpu_partial为较小值减少了单个CPU累计过度的partial空闲对象锁竞争有了明显下降。这里的核心思想是Slub的per-cpu路径虽快但要让它快就得保证每个CPU都能尽量自给自足不要让对象频繁“串门”。如果你的业务模式是多线程多CPU高频分配释放同一类对象那么适当减少cpu_partial有时反而能减少后续的全局partial操作因为对象不会过多滞留在某个CPU上而是尽快现死缓存。4. 常见问题与排查技巧实录4.1 内存泄漏定位slabinfo泄漏检测很多内核内存泄漏并不像用户态那样直接表现为进程RSS增长而是某一类kmem_cache的对象数量只增不减。定位的第一步就是用slabtop或者cat /proc/slabinfo观察哪个cache的active_objs持续增长。有一次我怀疑驱动里忘了释放buffer_head结果看到buffer_headcache的对象数在几分钟内翻了几倍。为了进一步确认我打开了slub_debugU检测未初始化和slub_debugF释放后使用重启内核通过dmesg里的辅助信息定位到具体调用栈。这里有个技巧slub_debug开启以后对象里会填充特殊字节0x6b等当你看到某个指针内容全是0x6b时说明这块内存已被释放但你又访问了这是典型的use-after-free。如果你不想重启也可以借助kmemleak。内核的kmemleak通过扫描内存中的指针来猜测孤立内存块虽然对Slub分配的对象也能发现但准确率取决于扫描时机。我建议在测试环境下并行使用kmemleak和slub_debug先快速定位再用slabtrace或ftrace跟踪分配释放路径。常见的这种“cache膨胀”并不都是泄漏也可能是有意缓存。例如filpcache会缓存关闭的文件对象dentrycache也有自己的回收机制。所以不要看到active_objs高就判死刑要看它是否随业务回落。如果一个本来应该波动的cache连续多个采样周期都单调上升那才有较大嫌疑。4.2 Slab corruption排查如果你在驱动里写越界比如给一个kmalloc(64)的对象写入70字节缓冲区溢出会破坏邻居对象或者破坏slab自身的元数据。此时最明显的现象是kernel BUG at mm/slub.c:xxxx或general protection fault伴随list_del corruption之类的报错。此时第一时间开启slub_debugFZPU组合F检测释放后使用Z检测redzone对象尾部的特殊填充用于探测越界P检测page poisoning。开启后内核会在分配对象时写入特定模式释放时检查模式是否被改动。如果发现redzone字节被写坏会打印“Redzone overwritten”以及调用栈。这个调用栈是最后一次合法分配或释放的函数栈能帮你大致定位越界发生的位置。我还遇到过一种情况不是越界而是并发访问导致两个CPU同时操作同一个空闲链表。典型表现是freelist指针被改写得混乱内核报slub double free或者Freepointer corrupt。这多半是驱动自旋锁使用不当或对象的引用计数错误导致同一对象被并发释放两次。这类问题的排查比较折磨建议配合KASAN如果内核开了CONFIG_KASAN来检测堆越界和UAF。KASAN在编译期插桩运行期对每个内存访问做校验定位精度远高于纯slub_debug代价是内存消耗大量增加和性能下降适合在开发环境使用。4.3 性能问题cache thrashing有一种典型性能问题叫“cache thrashing”意思是在多核系统上对象频繁从一个CPU的缓存被迁移到另一个CPU导致大量partial slab在node之间转移。表现出来就是cat /proc/slabinfo时看到某cache的num_slabs很大但实际每个slab都只用了少数对象利用率极低。这种问题的诱因通常是业务线程的迁移或者中断处理在不同CPU上交替。比如网卡收包队列使用RPS把包分发到多个CPU每个CPU都要分配sk_buff和skb-head如果队列频繁切换本来应该由同一个CPU复用的sk_buffcache就会散布到多个CPU上。解决办法有几种让业务线程或中断绑核尽量保持同CPU操作同类型对象。调整cpu_partial增大或减小每个CPU上缓存的最大partial slab数量。如果CPU之间迁移频繁适当提高cpu_partial可以让对象在迁移前尽可能留在这个CPU的slab里被复用。通过echo 0 /sys/kernel/slab/cache/remote_node_defrag_ratio来控制节点间回收或迁移。这些调整没有绝对标准我一般会在压测试验中连续改变参数观察perf里的cache-miss率、锁竞争以及业务QPS变化。多尝试每次只动一个变量。4.4 与虚拟化/容器场景的适配内核虚拟化比如KVM虚拟机和容器场景对Slub也有影响。一个常见现象是大量容器在同一个宿主机上频繁创建销毁每次创建容器要创建大量的task_struct、mm_struct、pid等内核对象这些缓存如果受限会发现容器启动速度变慢甚至触发OOM。这里有个容易被忽略的参数/sys/fs/cgroup/memory/group/memory.kmem.slabinfocgroup v1有类似基于kmem的机制。通过限制某个cgroup的slab内存总量可以避免一个容器疯狂创建线程导致整个宿主机的slab内存耗尽。不过cgroup v2下slab内存计入memory.current你可以通过memory.high或memory.max来约束。但这里要小心限制过严会导致容器内内核内存分配频繁回收甚至触发不可预期的问题。另一个虚拟化场景是VM热迁移时由于不同宿主机的内核版本和分配器配置不同源端缓存的部分slab状态不会迁移目标端需要重新分配这可能导致短暂的性能抖动。所以生产上最好保持宿主机内核版本和slub参数一致别一台机器开了slub_debug另一台没开否则迁移后两个环境的内存分配行为差异很大。5. 编译内核时如何选择分配器结合银河麒麟/通用Linux5.1 Kconfig与编译选项编译内核时分配器的选择在kernel/Kconfig中的SLAB、SLUB、SLOB选项。你需要进入Kernel hacking或General setup的子菜单找到Choose SLAB allocator不同版本菜单名略有不同。选择后对应的CONFIG_SLAB、CONFIG_SLUB或CONFIG_SLOB会被置为y。一般来说服务器和桌面默认应该选SLUB如果要用旧式调试或研究/proc/slabinfo的传统字段选SLAB如果编译目标是内存很小的嵌入式系统选SLOB。但注意SLOB通常只适合无MMU或内存64MB的场合大多数带有MMU的环境哪怕内存128MB也建议用SLUB。以银河麒麟v10桌面版为例它基于Linux 4.19内核大部分官方内核选用的就是SLUB而且开启了CONFIG_SLUB_DEBUG和CONFIG_SLUB_DEBUG_PANIC_ON可能未开启。如果你需要更换内核版本比如从4.19升级到5.x或6.x需要确认新内核的分配器配置是否和原有业务兼容。新内核默认仍是SLUB接口基本兼容但/sys/kernel/slab下的有些节点名或属性可能变化比如某些kmalloc-缓存尺寸的编号变了96、192等的引入与架构相关。如果你们有监控脚本依赖固定的cache名升级前要重点核对。5.2 切换分配器的注意事项在编译时从SLUB切换到SLAB或者反向切换不是改个Kconfig那么简单。实际影响包括所有内核模块的二进制需要重新编译吗通常不需要因为kmalloc、kmem_cache_alloc都是GPL导出的符号ABI基本一致。但是如果你使用了kmem_cache_create的某些高级flags或者直接操作/sys/kernel/slab下的节点这些在两种分配器下的表现不同。/proc/slabinfo的字段细节不一样监控脚本要适配。启动参数不一样比如SLUB的slub_debug在SLAB下不生效SLAB的slabinfo...在SLUB下也不一定有效。内存占用和性能特征有差异需要重新压测。我建议你用一个全新的内核版本在Kconfig里明确选SLUB同时打开CONFIG_SLUB_DEBUG和CONFIG_KASAN调试内核用发布版关闭。生产内核不要开slub_debug也不要开init_on_alloc1或init_on_free1虽然这两个有安全价值但会显著影响分配和释放性能除非你的业务痛点是未初始化内存泄漏否则不推荐默认开启。另外如果你的系统是银河麒麟等基于RPM/DEB发行版更换内核时建议保留厂商的abiver和signature避免安全启动校验失败。动手前先准备好grub-mkconfig能引导回旧内核避免新内核起不来的时候系统变砖。5.3 从4.19内核升级到新内核时分配器行为变化我实际对比过Linux 4.19和5.15、6.x的SLUB实现大的路径没有变但有些细节值得注意5.x以后kmalloc(size)对中间尺寸的支持更完善在64位系统上加入了更多非2次幂大小cache如96、128、192、256等减少了内存浪费。如果你之前的用户态模块固定用kmalloc(100)升级前分配在128 cache升级后可能落在96 cache行为不一致但更高效。5.9开始引入了CONFIG_SLAB_BUCKET其实没有那是针对SLAB的维护模式。真正变化比较大的是5.17前后引入了struct slab的独立定义把原本在struct page上的slab字段移到struct slab目的是减少struct page的尺寸膨胀。这会影响某些直接操作page_to_slab等宏的驱动但其实很多驱动不涉及。6.x中加入了kmem_cache_alloc_bulk和kmem_cache_free_bulk的进一步优化以及slab_free路径中更细粒度的RCU延迟释放。如果你的驱动大量批量分配对象升级后性能有提升。所以升级时我的建议是编译前先查阅目标内核的mm/slub.c和include/linux/slub_def.h看有没有你依赖的宏变化。更简单的做法是测试环境装好新内核跑一遍你所有的驱动模块用dmesg和perf stat采集异常。5.4 与eventfd唤醒机制、内核虚拟化的实际关联有热词提到linux内核eventfd唤醒机制这里也可以顺带说一句内在联系。eventfd是内核用来做事件通知的轻量文件描述符它与poll/epoll配合时常用来在用户态和内核态之间传递唤醒信号。而在内核虚拟化场景中vhost、KVM的ioeventfd/irqfd实现里会有大量eventfd_ctx对象的分配与释放这些对象走的就是内核分配器。如果你发现eventfd_ctxcache在频繁暴涨说明有大量事件上下文被创建而未释放或者epoll机制存在泄漏。这就是分配器与具体模块相关联的一个经典案例。调优Slub不能只看分配器本身得往上追到你关注的业务模块。比如eventfd相关的cache叫做eventfd_ctx_cache在/proc/slabinfo里能看到。如果它在虚拟机规模扩大时增长率正常说明分配器工作正常如果异常增长应该检查eventfd的引用计数或是否忘记调用eventfd_ctx_put。虚拟化和容器场景下由于需要创建大量fd、线程、内存映射对files_cache、signal_cache、mm_struct等cache的需求会突然增大。一个合理的经验值是当cgroup的memory.kmem.slabinfov1接近memory.limit_in_bytes时应提高/proc/sys/vm/vfs_cache_pressure来加快回收可回收slab否则可能导致某些模块分配失败。不过这个值通常默认100调太高会让dentry/inode被过早回收反而增加IO和CPU开销需要权衡。6. 实操中的几个独门技巧这个部分是纯经验分享没有官方文档会帮你写出来。第一个技巧是监控slab不要只盯总数要看增量。很多人用slabtop看一眼就完事但排查泄漏时应该写一个循环脚本每10秒采一次/proc/slabinfo对每个cache做差分。脚本逻辑很简单解析第二列active_objs每隔固定间隔计算差值输出TOP20的增长量。我就是靠这个脚本在一次生产故障里快速锁定了增长最快的kmalloc-128然后顺着faddr2line找到了泄漏点。第二个技巧是临时生效的参数别写进grub先用sysfs试。Slub很多参数可以在/sys/kernel/slab/cache/下在线调整例如cpu_partial、min_partial、order。你完全可以在压测时先用echo尝试找到最优值再固化到内核命令行或sysctl里。但要注意/sys/kernel/slab/cache/下的节点在一个cache被合并后可能不存在所以需要slab_nomerge才能看到独立节点。第三个技巧是利用slabinfo -v和slabtop -s排序。slabtop命令默认按缓存占用量排序但你也可以用-s c按active objects、-s a按active slabs等不同维度排序。排查性能抖动时我会用-s c找出对象数剧增但活跃数不高的“僵尸缓存”这类缓存通常可以通过shrink回收。第四个技巧小心内存回收器对slab的处理。shrink_slab()会回调每个kmem_cache的shrink接口但并不是所有cache都能收缩。像kmalloc-*这类通用缓存每个slab对象都是独立使用的除非整个slab空闲否则不能剥离单个对象。所以如果你看到一个kmalloc cache占用很多内存但每个slab都只有少量对象在用靠收缩机制是没法有效回收的只能靠Slub内部的cpu_partial/min_partial去控制它保留的空slab数量。因此某些情况下重启业务比调参更高效。还有一个很多人忽略的细节开启init_on_alloc和init_on_free后所有slab对象都会清零这能让未初始化数据和释放后残留数据带来的安全风险大幅降低但代价是分配和释放时的CPU消耗明显增加。对于性能敏感的路径我不建议开启除非你明确要防内核信息泄露。实际上很多主流发行版默认开启init_on_alloc1从安全角度能接受但如果你的业务非常吃分配性能可以在内核命令行里显式设init_on_alloc0。7. 结尾我在实际操作中的一点体会说真的Slab/Slub/Slob这三兄弟很容易劝退新人因为它们命名相似、行为又有细微差别而内核代码里的宏定义还互相嵌套。我自己的经验是先不要纠结三个都要学透先把你当前内核的默认分配器通常就是SLUB的核心路径读通即kmem_cache_alloc - ___slab_alloc - new_slab这一条链。读通之后再去对比SLAB的区别你会发现SLUB的简化设计如此巧妙然后再去看SLOB会觉得原来极简主义也有它存在的理由。在多次排查内存问题之后我最深的感受是分配器本身很少出错大多数问题是使用分配器的人写错了对象大小、忘了释放、或者并发释放。但熟悉分配器的内部结构能让你在问题出现时快速判断出是哪类使用错误。这篇文章提到的所有参数和排查手段我都建议你在测试环境里亲手试一遍哪怕只是打开slub_debug跑一个普通测试程序看看输出格式。纸上得来终觉浅内核内存管理尤其需要动手做实验才能把“看过”变成“会了”。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →