先看一个slab和slub的缓存框架图,有疑问我们可以评论区或者加微信群一起讨论第一章:问题本质——页分配模型的结构性失配
Linux 内核底层依赖 Buddy allocator 管理物理内存,它以页(通常 4KB)为单位,通过 2^order 的拆分与合并,解决“如何快速获得连续内存”的问题。在 DMA、页表、内核栈等场景中,这种模型非常高效,因为这些场景天然以页为粒度。
但内核的主流负载不遵循这个模型。大量核心数据结构(如 dentry、inode、sk_buff)都是小对象 + 高频分配,尺寸往往在几十到几百字节之间。这导致一个根本矛盾:用页级分配器处理对象级需求,本质上是模型错位。
这种错位带来的问题不是简单的浪费,而是系统性失效。每次小对象分配都需要获取整页,每次释放都可能触发页合并,这使得内存结构在高频操作下不断被破坏。结果不是“效率低一点”,而是系统逐渐失去稳定性与可预测性。
第二章:碎片从空间到时间问题
碎片在内核中至少有三种表现形式,它们共同作用,最终破坏系统行为。
首先是内部碎片,小对象占用整页,利用率极低:
其次是外部碎片,即使总内存充足,也可能无法提供连续页:
free memory: 1GBorder-3 allocation: FAIL
但更关键的是时间碎片。频繁的页拆分与合并,会导致分配路径震荡:
alloc → split → free → merge → alloc → split ...
这种震荡直接带来锁竞争、cacheline 失效以及延迟抖动。最终系统表现为:性能不稳定,而不是单纯变慢。这也是很多线上问题“看起来资源正常,但系统卡顿”的根本原因。
第三章:Slab 的引入——从“分配”到“复用”
Slab allocator 的核心思想非常直接:既然对象类型是有限的,就不应该每次重新分配,而应该缓存并复用。
它引入 kmem_cache 作为核心抽象,每一种对象对应一个缓存:
struct kmem_cache { size_t size; struct list_head slabs_full; struct list_head slabs_partial; struct list_head slabs_free;};
页不再直接交付给对象,而是被切分为多个等大小槽位:
+------------------------+| slab (one or more pages)|------------------------| obj | obj | obj | obj || obj | obj | obj | obj |+------------------------+
分配路径变为:
kmem_cache_alloc → CPU cache → partial slab → new slab (buddy)
释放路径:
kmem_cache_free → return to slab → (延迟) → buddy
这种设计的关键变化在于:分配行为被转化为缓存命中,而不是底层内存操作。
第四章:碎片是如何被消灭的
Slab 并不是“优化一点点”,而是从结构上消除了碎片产生的条件。
首先,固定大小对象切分页,从根本上消除了内部碎片:
4KB → 32 × 128B objects 利用率接近 100%
其次,小对象不再直接访问 buddy 分配器,从而减少页结构扰动:
before: obj → buddyafter: obj → slab cache → (少量) buddy
Slab 引入延迟回收机制。对象释放后不会立即归还页,而是保留在缓存中等待复用:
free(obj) → cache → reuse
这避免了频繁的 alloc/free 震荡。
同时,由于同类对象连续排列:
带来了更好的 cacheline 命中率和 TLB 稳定性。可以说,Slab 同时解决了空间碎片 + 结构碎片 + 性能碎片。
第五章:SLUB——面向并发的极简重构
随着多核系统发展,传统 Slab 的链表结构和锁竞争逐渐成为瓶颈,因此演进出了 SLUB allocator。
SLUB 的核心思想是“去结构化”:删除复杂队列,让分配路径最短。
它将 freelist 直接嵌入对象:
struct obj { struct obj *next;};
并引入 per-CPU freelist:
CPU0 → freelist0CPU1 → freelist1
分配路径:alloc → this_cpu freelist → (empty?) → new slab → buddy释放路径:free → push to freelist
绝大多数情况下无需加锁,分配复杂度接近 O(1)。这在网络栈等高频路径中,直接决定系统是否能够线性扩展。
第六章:从内存分配到生命周期管理
从整体结构来看,Buddy allocator 与 Slab allocator / SLUB allocator 构成分层体系:
[ Buddy ] → 提供页[ Slab/SLUB ] → 提供对象[ 内核子系统 ] → 使用对象
Buddy 解决的是“能否分配连续内存”,而 Slab/SLUB 解决的是“如何高效使用内存”。两者并不是替代关系,而是职责分离。
更深层的变化在于,Slab/SLUB 将内核从“字节分配模型”推进到“对象生命周期模型”。内存不再只是分配和释放,而是围绕对象的创建、复用与回收进行管理。这种转变使系统在高负载下依然具备稳定性与可预测性。
如果移除 Slab/SLUB,系统不会只是变慢,而是会出现:
内存利用率崩塌
高阶分配失败
延迟抖动严重
系统在压力下失稳
所以Slab/SLUB 不是优化策略,而是 Linux 内核能够在真实负载下正常运行的基础设施。
我建了一个嵌入式Linux技术群,专门聊难题分析和求职面试,欢迎大家一起加入,共同解决工作中的疑难杂症问题