3. Slab 分配器详解:Slab、Slub、Slob 的区别与演进、Slab 缓存结构、对象复用机制

Slab 分配器是 Linux 内核中用于管理小块内存(通常小于一个页面)的核心机制。它专门针对内核中频繁分配和释放的“对象”(如 task_struct、inode、sk_buff 等)进行优化,旨在解决伙伴系统(Buddy System)在处理小对象时产生的内部碎片和分配效率低下的问题。本章将深入剖析 Slab 分配器的演进历程、三种实现(Slab、Slub、Slob)的差异、核心缓存结构以及其高效的对象复用机制。

3.1 演进历程:从 Slab 到 Slub 与 Slob

Linux 内核的内存分配器经历了从原始 Slab 到 Slub 和 Slob 的演进,以适应不同硬件规模和场景的需求。下表总结了三种分配器的核心区别与适用场景。

特性 Slab (原始) Slub (Unqueued Slab) Slob (Simple List Of Blocks)
设计目标 通用、功能完整、缓存着色 简化代码、减少元数据开销、提高可扩展性 极致精简、最小内存占用
元数据开销 较高(每个 slab 有描述符,管理队列复杂) 较低(slab 描述符可复用对象空间,队列简化) 极低(使用隐式链表,无专用描述符)
数据结构 复杂的三级链表(full、partial、free) 简化的 per-node 链表(partial 为主) 简单的全局/CPU 局部链表
缓存着色 支持(通过着色区域对齐不同对象起始地址) 不支持(认为现代 CPU cache 足够大,着色收益低) 不支持
可扩展性 较差(全局锁竞争激烈) 优秀(per-node 和 per-CPU 操作,锁粒度细) 一般(适用于单核或小内存系统)
代码复杂度 高(约 6000 行) 中等(约 2000 行) 低(约 600 行)
适用场景 早期内核、嵌入式(已逐步被 Slub 取代) 桌面、服务器、多核大内存系统(当前默认) 嵌入式、极低内存系统(如 MMU-less)
演进趋势: 从 Slab 到 Slub 的演进,核心是“去中心化”和“元数据精简”。Slub 移除了 Slab 中复杂的全局队列和着色机制,将管理压力分散到每个 slab 页框和 per-node 结构上,从而大幅提升了多核环境下的并发性能。Slob 则走向另一个极端,为最小内存占用而设计,几乎放弃了所有管理结构。

3.2 Slab 缓存结构:kmem_cache 与 slab 页框

Slab 分配器的核心数据结构是 kmem_cache(缓存描述符)和 slab(页框描述符)。每个 kmem_cache 管理一种特定大小的对象,而每个 slab 则对应一个或多个连续的物理页框,用于实际存放对象。

下图展示了 Slab 缓存的核心架构,包括 kmem_cache、per-node 结构、以及 slab 页框内部的对象布局。

Slab 缓存核心架构图 (kmem_cache & slab 页框) kmem_cache (缓存描述符) name: "skbuff_head_cache" object_size: 256B align: 8 flags: SLAB_HWCACHE_ALIGN kmem_cache_node (per-node) nr_partial: 3 partial: [slab_A] -> [slab_B] nr_slabs: 5 total_objects: 40 Slab 页框 (连续物理页) 对象 0 对象 1 对象 2 对象 3 对象 4 对象 5 对象 6 对象 7 绿色: 空闲对象 红色: 已分配对象 slab 描述符 (可位于页框内或外部) slab 描述符
关键数据结构解读:
  • kmem_cache: 全局唯一的缓存描述符,包含对象大小、对齐方式、构造函数、标志位等。每个缓存都有一个名称(如 skbuff_head_cache),便于在 /proc/slabinfo 中识别。
  • kmem_cache_node: 每个 NUMA 节点一个,管理该节点上的所有 slab 页框。核心是 partial 链表,链接部分空闲的 slab。
  • slab 页框: 由 1 个或多个连续物理页组成。内部被划分为多个等大的对象槽位。每个 slab 都有一个 slab 描述符,记录该 slab 的状态(空闲对象数量、管理指针等)。

3.3 对象复用机制:从分配、释放到缓存着色

Slab 分配器的高效性源于其精妙的对象复用机制。它通过维护不同状态的 slab 链表,以及利用 per-CPU 的本地对象缓存,实现了极低延迟的分配和释放操作。

3.3.1 对象分配流程

当内核请求分配一个对象时(例如通过 kmem_cache_alloc()),Slab 分配器遵循以下优先级路径:

  1. 检查 per-CPU 空闲对象缓存 (Slub 的快速路径): 如果当前 CPU 的本地缓存中有空闲对象,直接返回,无需任何锁操作。这是最快速的路径。
  2. 检查 slab 的 partial 链表: 如果本地缓存为空,则从所在 NUMA 节点的 partial 链表中获取一个 slab。从该 slab 中取出一个空闲对象,并更新 slab 的空闲计数。
  3. 检查 full 链表 (Slab 特有): 如果 partial 链表也为空,则从 full 链表中寻找是否有 slab 因为对象释放而变为 partial。
  4. 分配新的 slab 页框: 如果所有链表都为空,则调用伙伴系统分配一个新的物理页框,创建新的 slab,并将其加入 partial 链表,然后从中分配对象。
Slub 的优化: Slub 移除了 Slab 中的 full 链表。当一个 slab 的所有对象都被分配后,它不再被显式管理,而是“游离”在外。只有当 slab 上的对象被释放,它才会重新被加入 partial 链表。这简化了链表操作,减少了元数据维护成本。

3.3.2 对象释放流程

对象释放(通过 kmem_cache_free())同样遵循高效路径:

  1. 尝试放回 per-CPU 缓存: 如果当前 CPU 的本地缓存未满,直接将对象放回,操作完成。
  2. 将对象归还给所属 slab: 如果本地缓存已满,则将对象归还给其原本所在的 slab 页框。更新 slab 的空闲计数。
  3. 检查 slab 状态:
    • 如果 slab 变为 partial(从 full 变为部分空闲),将其加入 partial 链表。
    • 如果 slab 变为 free(所有对象都空闲),且系统内存压力较大,则可能将整个 slab 页框释放回伙伴系统。

3.3.3 缓存着色 (Cache Coloring)

缓存着色是原始 Slab 分配器的一个重要特性,旨在优化 CPU 缓存利用率。其原理是在同一个 kmem_cache 的不同 slab 页框中,将第一个对象的起始地址进行偏移(着色),使得不同 slab 中的对象不会映射到 CPU 缓存的同一组(cache set)中,从而减少缓存行冲突。

Slab 缓存着色 (Cache Coloring) 示意图 Slab 0 (着色偏移 = 0) 对象 0 对象 1 对象 2 ... Slab 1 (着色偏移 = 16 字节) 对象 0 对象 1 对象 2 ... Slab 2 (着色偏移 = 32 字节) 对象 0 对象 1 对象 2 ... 通过偏移对象起始地址,使不同 slab 的对象映射到不同的 CPU 缓存组,减少冲突未命中。
注意: 缓存着色在现代内核(尤其是使用 Slub 分配器时)中已不再使用。原因在于现代 CPU 的 L2/L3 缓存通常足够大,且采用更智能的替换策略(如伪 LRU),着色带来的收益微乎其微,反而增加了代码复杂度和内存碎片。Slub 分配器默认关闭了着色功能。

3.4 调试接口:/proc/slabinfo 与 slabtop

在排查内存泄漏时,/proc/slabinfo 和 slabtop 命令是查看 Slab 分配器状态的第一手工具。它们能提供每个 kmem_cache 的详细统计信息。

以下是一个典型的 /proc/slabinfo 输出片段(部分列):

slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> ...
skbuff_head_cache    1024    2048    256    16    1 : tunables    0    0    0 : slabdata    64    64    0
tcp_bind_bucket_cache    512     512     64    64    1 : tunables    0    0    0 : slabdata     8     8     0
dentry               8192   16384    192    21    1 : tunables    0    0    0 : slabdata   780   780    0
关键字段解读:
  • active_objs: 当前正在使用的对象数量。
  • num_objs: 该缓存中总共分配的对象数量(包括空闲的)。
  • objsize: 每个对象的大小(字节)。
  • objperslab: 每个 slab 页框中包含的对象数量。
  • pagesperslab: 每个 slab 占用的物理页框数量。
  • slabdata: 包含 active_slabs、num_slabs、shared 等信息。

通过观察 active_objs 与 num_objs 的比值,可以快速判断某个缓存是否存在对象泄漏。如果 active_objs 持续增长且不回落,而 num_objs 也随之增长,则很可能存在内存泄漏。

实战技巧: 使用 watch -n 1 'grep "skbuff_head_cache" /proc/slabinfo' 可以每秒刷新一次,实时监控特定缓存的对象数量变化,是定位网络栈内存泄漏的常用手段。