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()透明分页:大块分配(

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部