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) |
3.2 Slab 缓存结构:kmem_cache 与 slab 页框
Slab 分配器的核心数据结构是 kmem_cache(缓存描述符)和 slab(页框描述符)。每个 kmem_cache 管理一种特定大小的对象,而每个 slab 则对应一个或多个连续的物理页框,用于实际存放对象。
下图展示了 Slab 缓存的核心架构,包括 kmem_cache、per-node 结构、以及 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 分配器遵循以下优先级路径:
- 检查 per-CPU 空闲对象缓存 (Slub 的快速路径): 如果当前 CPU 的本地缓存中有空闲对象,直接返回,无需任何锁操作。这是最快速的路径。
- 检查 slab 的
partial链表: 如果本地缓存为空,则从所在 NUMA 节点的partial链表中获取一个 slab。从该 slab 中取出一个空闲对象,并更新 slab 的空闲计数。 - 检查
full链表 (Slab 特有): 如果partial链表也为空,则从full链表中寻找是否有 slab 因为对象释放而变为partial。 - 分配新的 slab 页框: 如果所有链表都为空,则调用伙伴系统分配一个新的物理页框,创建新的 slab,并将其加入
partial链表,然后从中分配对象。
full 链表。当一个 slab 的所有对象都被分配后,它不再被显式管理,而是“游离”在外。只有当 slab 上的对象被释放,它才会重新被加入 partial 链表。这简化了链表操作,减少了元数据维护成本。
3.3.2 对象释放流程
对象释放(通过 kmem_cache_free())同样遵循高效路径:
- 尝试放回 per-CPU 缓存: 如果当前 CPU 的本地缓存未满,直接将对象放回,操作完成。
- 将对象归还给所属 slab: 如果本地缓存已满,则将对象归还给其原本所在的 slab 页框。更新 slab 的空闲计数。
- 检查 slab 状态:
- 如果 slab 变为
partial(从full变为部分空闲),将其加入partial链表。 - 如果 slab 变为
free(所有对象都空闲),且系统内存压力较大,则可能将整个 slab 页框释放回伙伴系统。
- 如果 slab 变为
3.3.3 缓存着色 (Cache Coloring)
缓存着色是原始 Slab 分配器的一个重要特性,旨在优化 CPU 缓存利用率。其原理是在同一个 kmem_cache 的不同 slab 页框中,将第一个对象的起始地址进行偏移(着色),使得不同 slab 中的对象不会映射到 CPU 缓存的同一组(cache set)中,从而减少缓存行冲突。
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' 可以每秒刷新一次,实时监控特定缓存的对象数量变化,是定位网络栈内存泄漏的常用手段。