news 2026/10/2 11:56:45

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
内核内存管理:Slab/Slub/Slob分配器原理与调优实战

刚接触内核内存管理的时候,我第一个绕不开的坑就是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、或者说不希望分配器自身占用太多内存:选Slob

1.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_slab(slob_page)等结构来组织页面内对象。它没有对象大小一致的假设,所以可以像普通堆一样从一个块里切出任意大小对象,这也是它实现简单但碎片化的原因。

特性SlabSlubSlob
引入时间早期核心2.6.23后默认早期嵌入式
元数据结构复杂,slab描述符+链表复用page字段,精简极小,块头+链表
每CPU缓存cpu_cache数组cpu_slab无锁队列无,简单全局锁
适用环境普通/研究、调试NUMA、多核、默认极小内存、无MMU
碎片控制较好较好较差
代码复杂度高中低

2. 分配流程逐层拆解:从kmalloc到cache

2.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_debug=P:开启Slub的调试功能。常用的组合还有slub_debug=U(跟踪未初始化内存)、slub_debug=F(跟踪释放后使用的内存)、slub_debug=Z(把对象用特定模式填充以检测修改)、slub_debug=A(打开全部检查)。开启后每个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_nomerge:slub_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_debug=U(检测未初始化)和slub_debug=F(释放后使用),重启内核,通过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_debug=FZPU组合,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.slabinfo(cgroup v1有类似基于kmem的机制)。通过限制某个cgroup的slab内存总量,可以避免一个容器疯狂创建线程导致整个宿主机的slab内存耗尽。不过cgroup v2下slab内存计入memory.current,你可以通过memory.high或memory.max来约束。但这里要小心:限制过严会导致容器内内核内存分配频繁回收,甚至触发不可预期的问题。

另一个虚拟化场景是VM热迁移时,由于不同宿主机的内核版本和分配器配置不同,源端缓存的部分slab状态不会迁移,目标端需要重新分配,这可能导致短暂的性能抖动。所以生产上最好保持宿主机内核版本和slub参数一致,别一台机器开了slub_debug,另一台没开,否则迁移后两个环境的内存分配行为差异很大。

5. 编译内核时如何选择分配器(结合银河麒麟/通用Linux)

5.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_alloc=1或init_on_free=1,虽然这两个有安全价值,但会显著影响分配和释放性能,除非你的业务痛点是未初始化内存泄漏,否则不推荐默认开启。

另外,如果你的系统是银河麒麟等基于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.slabinfo(v1)接近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_alloc=1,从安全角度能接受,但如果你的业务非常吃分配性能,可以在内核命令行里显式设init_on_alloc=0。

7. 结尾:我在实际操作中的一点体会

说真的,Slab/Slub/Slob这三兄弟很容易劝退新人,因为它们命名相似、行为又有细微差别,而内核代码里的宏定义还互相嵌套。我自己的经验是:先不要纠结三个都要学透,先把你当前内核的默认分配器(通常就是SLUB)的核心路径读通,即kmem_cache_alloc -> ___slab_alloc -> new_slab这一条链。读通之后再去对比SLAB的区别,你会发现SLUB的简化设计如此巧妙;然后再去看SLOB,会觉得原来极简主义也有它存在的理由。

在多次排查内存问题之后,我最深的感受是:分配器本身很少出错,大多数问题是使用分配器的人写错了对象大小、忘了释放、或者并发释放。但熟悉分配器的内部结构,能让你在问题出现时快速判断出是哪类使用错误。这篇文章提到的所有参数和排查手段,我都建议你在测试环境里亲手试一遍,哪怕只是打开slub_debug跑一个普通测试程序看看输出格式。纸上得来终觉浅,内核内存管理尤其需要动手做实验,才能把“看过”变成“会了”。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 11:55:53

老设备无线改造:ZigBee与Wi-Fi串口转换器选型实战指南

1. 老设备无线改造&#xff0c;不是“换个模块”就完事——先搞清你手里的设备到底在说什么老设备无线改造&#xff0c;这个需求我每年至少接到三十多个咨询&#xff0c;来自工厂老师傅、智能家居发烧友、还有做楼宇自控的集成商。他们掏出的设备五花八门&#xff1a;上世纪90年…

作者头像 李华
网站建设 2026/10/2 11:53:18

cch 架构是什么,Nginx 又是什么,针对 Claude Code AI 的请求链路拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 11:53:03

Redis MCP Server 实战:让 Claude Code 直接操作 Redis 缓存

1. Redis 接入 AI 这件事&#xff0c;到底在说什么Redis 这个名字做后端开发的人都不陌生&#xff0c;缓存、分布式锁、消息队列、排行榜&#xff0c;几乎每个项目里都能看到它的身影。但最近圈子里讨论的“Redis 已正式接入 AI”&#xff0c;说的并不是 Redis 数据库本身突然长…

作者头像 李华
网站建设 2026/10/2 11:52:09

AI神器之微软的编码助手Copilot:把Codex auth.json改到TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华