Linux内核Slab分配器(Slab Allocator)物理内存分配深度工程实战
引言
在上一篇文章中,我们深入剖析了Buddy分配器(Buddy System)如何以页为单位管理物理内存。然而,内核中大量的小对象分配需求(如task_struct、inode、dentry等)无法直接使用按页分配的Buddy系统,否则会产生严重的内部碎片。Slab分配器正是为解决这个问题而生——它在Buddy系统之上构建了一个高效的对象缓存层,实现了小内存单元的零碎片分配。
1. Slab分配器的核心思想
Slab分配器的设计由Sun公司的Jeff Bonwick在Solaris中首次提出,后被引入Linux内核。其核心思想可以概括为:
"一次性分配一批对象,循环使用零碎内存。"
具体来说,Slab分配器将一页或多页连续内存划分为多个大小相同的对象(object)槽位,通过缓存管理实现对象的快速分配和释放。当对象被释放时不立即归还内存,而是标记为空闲状态供下次分配使用,避免了频繁的创建/销毁开销。
2. SLAB / SLUB / SLOB 三代分配器
Linux内核历史上存在过三种Slab分配器实现:
2.1 SLAB(经典实现)
原始实现,特点包括:每个缓存对应多个slab(每个slab占据一页或多页),slab中包含固定数量的对象;通过三个链表(full/partial/partial_empty)管理slab;使用对象描述符(kmem_bufctl_t)跟踪空闲对象。
2.2 SLUB(现代默认实现)
SLUB(Unqueued Slab Allocator)是Linux 2.6.23之后的默认分配器。相比SLAB,SLUB大幅简化了设计:取消了每CPU的freelist链表合并逻辑;将freelist指针直接嵌入到对象内存中(空闲时利用对象内存存储指针);支持调试功能如red zoning、poisoning。SLUB目前是绝大多数Linux发行版的选择。
2.3 SLOB(精简实现)
面向嵌入式系统等内存极小的环境,基于简单的链表管理,适合内存小于64MB的系统。在嵌入式Linux中仍有使用。
3. Slab分配器核心数据结构
3.1 kmem_cache(缓存描述符)
每个对象类型对应一个kmem_cache结构体(mm/slab.h),关键字段:
struct kmem_cache {
struct kmem_cache_cpu *cpu_slab; // 每CPU热路径缓存
unsigned long flags; // 标志位(如 SLAB_POISON)
unsigned int size; // 对象实际大小
unsigned int object_size; // 用户请求的大小
unsigned int align; // 对齐要求
const char *name; // 缓存名称
void (*ctor)(void *); // 构造函数
struct kmem_cache_node *node[MAX_NUMNODES]; // NUMA节点管理
};
3.2 kmem_cache_cpu(每CPU缓存)
这是SLUB的核心优化——每个CPU维护一个私有缓存,避免了多核竞争:
struct kmem_cache_cpu {
void **freelist; // 指向下一个空闲对象的指针
unsigned long tid; // 全局事务ID(用于无锁同步)
struct page *page; // 当前正在使用的页
struct page *partial; // 部分空闲的slab链表(CPU私有)
};
3.3 kmem_cache_node(NUMA节点缓存)
针对NUMA架构,每个节点维护一个kmem_cache_node,包含full和partial两个链表,用于节点级别的对象再平衡。
4. Slab分配器的对象分配流程
以SLUB分配器为例,kmem_cache_alloc()的分配路径如下:
kmem_cache_alloc()
└── slab_alloc()
├── 1. 检查CPU freelist(热路径,无锁)
│ └── freelist非空 → 直接返回对象(O(1))
│
├── 2. 检查CPU partial列表
│ └── 转到page,更新freelist
│
├── 3. 检查node->partial列表
│ └── 取一个partial slab补充CPU缓存
│
├── 4. 向Buddy系统申请新页
│ └── alloc_pages() → 初始化新slab
│
└── 5. 返回NULL(内存不足)
可以看到,最快速路径只需要一次指针解引用,这便是Slab分配器高性能的关键。
5. 伙伴系统与Slab的协作关系
Slab分配器并不是独立存在的,它构建在Buddy系统之上:
用户调用 kmalloc(size)
│
├── size <= 特定阈值(通常8KB~8MB)
│ └── 通过 kmem_cache 查找最佳匹配的缓存
│ └── kmem_cache_alloc() → SLUB热路径
│ └── 不足时调用 alloc_pages()(Buddy系统)
│
└── size > 阈值
└── 直接调用 alloc_pages()(Buddy系统)
内核中典型的kmalloc缓存大小包括:32, 64, 128, 192, 256, 512, 1024, 2048, 4096, 8192 等(include/linux/kmalloc_sizes.h)。
6. Slab中的着色机制(Cache Coloring)
高性能缓存中,不同slab中相同偏移的对象可能映射到相同的cache line,导致缓存冲突(cache line bouncing)。Slab分配器通过着色(coloring)机制缓解这一问题:
着色偏移 = rand() % colour_off
实际对象地址 = slab_base (slot_index × (cache_line_size colour))
其中colour_off决定颜色范围,每次分配新slab时随机选择一个颜色偏移,打散对象在CPU cache中的映射位置。
7. Slab的创建与销毁
7.1 创建缓存
// 创建专用缓存
struct kmem_cache *my_cache = kmem_cache_create(
"my_task_struct", // 缓存名称
sizeof(my_struct), // 对象大小
0, // 对齐
SLAB_HWCACHE_ALIGN, // 标志
my_ctor // 构造函数(可选)
);
7.2 销毁缓存
kmem_cache_destroy(my_cache);
销毁前必须确保所有对象已释放,否则会触发WARN_ON。
8. Slab分配器的性能优化技巧
8.1 SLAB_HWCACHE_ALIGN
该标志要求对象按CPU缓存行对齐,避免false sharing。对于频繁访问的小型对象(如自旋锁、计数器),使用此标志可显著提升多核性能。
8.2 SLAB_POISON
释放后的对象内存被填充为0x5a5a5a5a(或类似模式),能有效检测use-after-free类bug。
8.3 SLAB_RED_ZONE
在对象两端添加red zone区域(填充特殊值),用于检测buffer overflow。
8.4 通用缓存 vs 专用缓存
kmalloc()使用预创建的通用缓存(按大小分级),适合一次性分配;频繁创建/销毁同类型对象时应使用kmem_cache_create()创建专用缓存,避免通用缓存的锁竞争和着色浪费。
9. 内核调试与性能观测
9.1 /proc/slabinfo
$ cat /proc/slabinfo
kmalloc-128 2400 2400 128 32 1 : ...
kmalloc-256 1632 3200 256 32 1 : ...
task_struct 512 640 2880 4 4 : ...
dentry 16384 16384 192 21 1 : ...
可以看到每个缓存活跃的对象数、总对象数、对象大小等关键指标。
9.2 slabtop(实时监控)
$ slabtop -o # 按对象数排序显示
9.3 kmemleak(内存泄漏检测)
CONFIG_DEBUG_KMEMLEAK可以检测未引用的内存对象,帮助发现kmalloc分配后遗漏释放的bug。
10. Slab分配器的设计哲学与现代替代
Slab分配器体现了操作系统设计中几个经典思想:
① 对象池思想:预分配 复用,消除频繁创建销毁的开销,与现代内存池(memory pool)理念一脉相承。
② 层级化设计:小内存走Slab,大内存走Buddy,各层专注解决特定场景的问题。
③ 局部性优化:每CPU缓存 着色机制,是一种早期但极为有效的缓存局部性实践。
在现代内核中,SLUB经历了进一步演进:
queue-RCU slub:利用实现了无锁的延迟free,减少了回调RCU导致的延迟。
CPU Partial v.s. Node Partial平衡:内核5.x引入更智能的partial slab回收策略,减少内存浪费。
kmalloc()透明分页:大块分配(

发表评论 取消回复