一、为什么需要 Slab 分配器?伙伴系统的局限性

Linux 内核内存管理采用伙伴系统(Buddy System)+ Slab 分配器的两层架构。伙伴系统是底层物理页框分配器,以 2^n 页为单位管理内存。但对于内核中大量的小对象分配(task_struct、inode、dentry 等,通常几十到几百字节),伙伴系统直接分配 4KB 页面会造成严重的内部碎片。

Slab 分配器由 Jeff Bonwick 于 1994 年为 Solaris 设计,Linux 2.0 引入。其核心思想是对象缓存:为高频使用的内核对象建立专属缓存池,预先分配页面并切片为固定大小的对象,分配时直接返回空闲对象,无需初始化;释放时标记为空闲,保留数据结构供下次复用。

Slab 解决的核心问题:

  • 减少内部碎片:伙伴系统按页分配,Slab 在页内按对象大小精确切割
  • 加速对象分配:已初始化的对象直接从空闲链表取出,避免重复构造/析构
  • 降低锁竞争:每 CPU 本地缓存避免全局锁争用
  • 提升 Cache 命中率:Slab 着色技术减少 Cache Line 冲突

二、Slab 分配器核心三层架构

2.1 架构总览

Slab 分配器采用三级结构:

┌─────────────────────────────────────────┐
│           layer 3: kmem_cache           │  每个对象类型一个 cache
│  (管理同类型对象的多个 slab 链表)          │
├─────────────────────────────────────────┤
│            layer 2: slab                │  一个或多个连续物理页
│  (物理内存容器,被切成固定大小格子)         │
├─────────────────────────────────────────┤
│            layer 1: 伙伴系统             │  物理页框分配器
│  (alloc_pages / free_pages)             │
└─────────────────────────────────────────┘

kmem_cache是管理同一种对象的缓存,如task_struct_cachep、dentry_cache、inode_cache。每个 cache 维护三个 slab 链表:slabs_partial(部分空闲)、slabs_full(全部占用)、slabs_free(全部空闲)。

slab是从伙伴系统分配的一个或多个连续物理页,被切割为固定数量的对象格子,包含空闲对象链表。

2.2 数据结构关系

struct kmem_cache {
    struct kmem_cache_cpu *cpu_slab;   // 每 CPU 本地缓存
    struct kmem_cache_node *node[MAX_NUMNODES]; // 每 NUMA 节点 slab 链表
    unsigned int size;                 // 对象实际大小(含对齐)
    unsigned int object_size;          // 用户请求的对象大小
    unsigned int flags;                // SLAB flags
    unsigned int num;                  // 每个 slab 中的对象数量
    unsigned int gfporder;             // 每个 slab 占用的页数(2^n)
    colour_t colour;                   // 着色偏移量
    colour_t colour_off;               // 着色增量
    const char *name;                  // cache 名称
    // ...
};

struct kmem_cache_cpu {
    void **freelist;     // 空闲对象链表头
    unsigned long tid;   // 全局事务 ID
    struct page *page;   // 当前使用的 slab 页
    struct page *partial; // 本地 partial 链表
} ____cacheline_aligned_in_smp;

三、kmem_cache 缓存创建与管理

3.1 创建专用缓存

对于自定义的内核数据结构(如驱动中的私有结构体),应创建专属 kmem_cache:

#include <linux/slab.h>

static struct kmem_cache *my_cache;

// 模块初始化时创建缓存
static int __init my_module_init(void) {
    my_cache = kmem_cache_create(
        "my_object",          // 缓存名称(/proc/slabinfo 中可见)
        sizeof(struct my_obj), // 对象大小
        0,                    // 对齐偏移(0 = 自然对齐)
        SLAB_HWCACHE_ALIGN,  // 标志:对齐到硬件 Cache Line
        NULL                  // 构造函数(可为 NULL)
    );
    if (!my_cache) {
        pr_err("Failed to create my_cache\n");
        return -ENOMEM;
    }
    return 0;
}

// 模块退出时销毁缓存
static void __exit my_module_exit(void) {
    kmem_cache_destroy(my_cache);
}

3.2 从缓存分配和释放对象

// 分配对象(类似 malloc)
struct my_obj *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
if (!obj)
    return -ENOMEM;

// 使用对象...
obj->field = value;

// 释放对象
kmem_cache_free(my_cache, obj);

3.3 通用分配接口 kmalloc

当对象大小不固定或无需专属缓存时,使用 kmalloc(实际上也是 Slab 分配器):

// kmalloc 根据大小选择最近的 kmem_cache
void *buf = kmalloc(128, GFP_KERNEL);
void *big_buf = kmalloc(65536, GFP __GFP_NOWARN);

// kzalloc 分配并清零
void *zero_buf = kzalloc(256, GFP_KERNEL);

// 释放
kfree(buf);

kmalloc 大小上限:默认 4MB(KMALLOC_MAX_SIZE),超过需使用 vmalloc()。可分配的最大 slab 缓存大小为 8KB * 8 = 64KB(KMALLOC_MAX_CACHE_SIZE),更大的走专用路径。

四、每 CPU 本地缓存(cpu_slab)

4.1 设计原理

多处理器系统中,全局 slab 缓存的链表操作需要加锁,成为性能瓶颈。SLUB 分配器为每个 CPU 分配了per-cpu freelist:每个 CPU 有自己的空闲对象链表,分配/释放时先操作本地链表,仅当本地为空时才从 Node 的 partial 链表补充。

4.2 分配路径详解

/**
 * SLUB 分配路径(简化版):
 * 1. 检查 cpu_slab->freelist 是否非空 -> 直接 pop 对象返回
 * 2. freelist 为空 -> 检查 cpu_slab->partial 是否有页 -> 切换到 partial 页
 * 3. 仍无 -> 从 kmem_cache_node->partial 链表获取 slab
 * 4. 仍无 -> 从伙伴系统 alloc_pages 分配新 slab
 */
static __always_inline void *slab_alloc(struct kmem_cache *s,
        gfp_t gfpflags, unsigned long addr)
{
redo:
    freelist = cpu_slab->freelist;    // 读取本地链表头
    if (freelist)
        goto load_freelist;           // 快速路径:直接分配
    
    // 慢速路径:本地为空,从 partial 或伙伴系统补充
    freelist = get_partial(s, ...);
    if (freelist)
        goto load_freelist;
    
    // 必须从伙伴系统申请新 slab
    page = new_slab(s, gfpflags);
    if (page) {
        cpu_slab_set(page);
        goto redo;
    }
    
    return NULL;  // 分配失败
}

4.3 释放路径

/**
 * SLUB 释放路径:
 * 1. 将对象追加到 cpu_slab->freelist 头部(无锁,原子操作)
 * 2. 若本地缓存超额(超过 s->cpu_partial),归还部分到 Node partial 链表
 */
static __always_inline void slab_free(struct kmem_cache *s, void *object)
{
    // 直接头插法入链
    set_freelist(s, object, freelist);
    // 检查是否需要批量归还
    if (unlikely(slab_want_init_on_free(...)))
        // 安全清零逻辑...
    
    // 检查本地缓存是否超额
    if (unlikely(node_partial_unused(s) > s->cpu_partial_slabs))
        flush_cpu_slab(s);
}

优势总结:热路径分配/释放无锁、无原子操作、无内存屏障,性能接近 O(1)。

五、Slab 着色(Cache Coloring)原理

5.1 为什么需要着色?

CPU Cache 采用set-associative映射(如 64 组 8 路),物理地址的 bit[6:11] 决定落在哪个 Cache Set。如果多个 slab 的相同偏移位置的对象被同时访问,会导致 Cache Set 冲突(Cache Thrashing),降低命中率。

5.2 着色实现机制

Slab 的着色(Colouring)通过在每个 slab 起始位置添加不同大小的偏移量(称为 colour offset),使得不同 slab 中相同位置对象的 Cache Set 分布均匀。

/**
 * 着色参数:
 *   colour     = 可用偏移量数量(基于剩余空间计算)
 *   colour_off = 每次偏移增量(对齐到 cache line)
 */
struct kmem_cache {
    colour_t colour;       // 着色范围内可用颜色数
    colour_t colour_off;   // 增量 = cache_line_size()
};

// 创建 slab 时计算偏移
static inlinecolour_t get_colour(struct kmem_cache *s)
{
    return s->colour_next++ & (s->colour - 1);
}

struct page *new_slab(struct kmem_cache *s, gfp_t flags, int node)
{
    colour = get_colour(s);
    slab_size = colour_off * colour;  // 起始偏移
    // ... 分配页面
}

5.3 着色效果分析

假设:L1 D-Cache 32KB、64 字节 Cache Line、4-way set associative。可确认 Cache Set 数 = 32KB / (64B × 4) = 128 组。着色使得偏移差异在 [0, colour_off × colour) 范围内,确保相同对象在不同 slab 中以不同 Cache Set 访问,减少冲突失效。实测着色可提升密集场景下 Cache 命中率 3-8%,具体效果取决于对象大小和访问模式。

六、SLUB —— Linux 现代 Slab 分配器

6.1 SLUB 设计目标

SLUB(Unqueued Slab Allocator)自 Linux 2.6.23 起成为默认分配器,替代传统的 SLAB。其设计原则:

  • 简化元数据:放弃独立的 slab 管理结构,利用 struct page 空闲区域内嵌 freelist 指针
  • NUMA 感知:每个 NUMA 节点维护独立 slab 列表,减少跨节点访问
  • 减少内存开销:相比 SLAB 的 slab 描述符数组,SLUB 内存 overhead 更低
  • 更好的可扩展性:per-cpu freelist 设计天然适配多核

6.2 SLUB 核心结构

/**
 * SLUB 使用 struct page 内嵌的 union 存储 metadata
 * 当页被分配为 slab 时:
 *   page->freelist   -> slab 中的空闲对象列表
 *   page->inuse      -> 已使用对象数
 *   page->objects    -> slab 中总对象数
 *   page->frozen     -> 是否冻结(per-cpu 专属)
 *   page->slab_cache -> 指向 kmem_cache
 */
struct page {
    union {
        struct {    /* SLUB */
            union {
                struct {
                    void *freelist;       // 空闲对象头
                    union {
                        unsigned long counters;
                        struct {
                            unsigned inuse:16;
                            unsigned objects:15;
                            unsigned frozen:1;
                        };
                    };
                };
                struct {    /* 每页 slab 描述符 */
                    struct { };
                    void *freelist;
                    void *next;
                } kmem_cache_node;
            };
        };
    };
};

6.3 SLAB vs SLUB vs SLOB

特性SLABSLUBSLOB
引入时间1994 (Solaris)2007 (Linux 2.6.23)2006 (嵌入式)
元数据结构独立 slab_t 描述符struct page 内联对象内链表
默认分配器否是(Linux 主流)否
多核扩展性较差(全局锁)优秀(per-cpu)差
内存开销较高最低最低
调试支持完善完善完善(Red Zone/Poison)有限
适用场景服务器(已废弃)通用、高性能内存受限嵌入式

七、调试与监控

7.1 /proc/slabinfo 解读

# 查看所有 slab 缓存
cat /proc/slabinfo

# 输出格式:
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>

# 示例:
cat /proc/slabinfo | head -20
slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
kmalloc-128       24514  24576     128   32    1 : tunables  0    0    0 : slabdata    768    768      0
dentry            18992  21315      192   21    2 : tunables  0    0    0 : slabdata   1015   1015      0
inode_cache       15403  15820    608   13    2 : tunables  0    0    0 : slabdata   1217   1217      0
task_struct        369    420   5928    5    8 : tunables  0    0    0 : slabdata     84     84      0

关键指标含义:

  • active_objs:当前已使用的对象数
  • num_objs:总对象数(含空闲)
  • objsize:对象实际大小(含对齐)
  • objperslab:每个 slab 切多少个对象
  • active_slabs / num_slabs:活跃 slab 比例(高比例=内存浪费少)

内存占用量估算:num_objs × objsize / 1024 KB。若 active_objs 远小于 num_objs,有大量空闲对象可回收。

7.2 slabtop 实时监控

# 类似 top,实时查看 slab 缓存使用情况
watch -n 1 "cat /proc/slabinfo | sort -k3 -rn | head -10"

# 或使用 slabtop
echo 1 /proc/sys/vm/drop_caches  # 仅清除 dentries/inodes(不影响 slab 分配器)

7.3 启用 SLUB 调试

# 启动参数启用完整 SLUB 调试
slab_debug=FZP  # F=Fault-injection Z=Red-Zone P=Poison

# F(Fault-injection):故意注入分配失败,测试错误处理路径
# Z(Red-Zone):在对象边界插入哨兵值,检测越界写
# P(Poison):释放后填充 0xdead007,检测 use-after-free

# 对单个 cache 启用(内核调试):
echo 1 > /sys/kernel/slab/dentry/validate

八、内存泄漏检测实战

8.1 kmemleak:内核内存泄漏检测器

kmemleak 通过追踪所有分配的内存块,定期扫描未被任何指针引用的内存块,标记为泄漏。

# 编译内核时启用
CONFIG_DEBUG_KMEMLEAK=y

# 查看泄漏报告
cat /sys/kernel/debug/kmemleak

# 输出示例:
unreferenced object 0xffff888123456780 (size 192):
  comm "test_module", pid 1234, jiffies 4294967295
  hex dump (first 32 bytes):
    00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff  .."3DUfw..........
  backtrace:
    [<ffffffff81234567>] kmem_cache_alloc+0x3c/0xe0 [<task_struct>]
    [<ffffffff81abcdef>] do_init_module+0x56/0x1a0
    [<ffffffff81123456>] load_module+0x1a3/0x5b0

# 手动触发扫描
echo scan > /sys/kernel/debug/kmemleak

# 清除旧报告
echo clear > /sys/kernel/debug/kmemleak

8.2 /proc/meminfo 中的 Slab 信息

$ cat /proc/meminfo | grep -i slab
Slab:             3846124 kB   # 总共分配的 slab 内存
SReclaimable:     3124520 kB   # 可回收的 slab(如 dentry/inode 缓存)
SUnreclaim:        721604 kB   # 不可回收的 slab(活跃对象持有的 page cache)

8.3 使用 drgn 做内存分析

#!/usr/bin/env python3
import drgn
from drgn import container_of
from drgn.helpers.linux.slab import for_each_slab_cache, slab_cache_size, slab_cache_total_used

prog = drgn.program()  # 连接到崩溃转储或 live kernel

# 遍历所有 slab cache
for cache in for_each_slab_cache(prog):
    name = cache.name.string_().decode()
    active_objs = cache.active_slabs.counter
    objs = cache.total_slabs.counter
    obj_size = cache.size.value_()
    
    waste = (objs - active_objs) * obj_size
    if waste > 1024*1024:  # 超过 1MB 浪费
        print(f"{name}: {active_objs}/{objs} objs, "
              f"objsize={obj_size}B, waste={waste//1024}KB")

九、生产环境调优与最佳实践

9.1 驱动模块中的 Slab 使用建议

/*
 * 最佳实践 1: 为高频分配的对象创建专属 cache
 * 避免使用 kmalloc/kfree(存在 cache 查找和 CPU 迁移开销)
 */
static struct kmem_cache *driver_cache;

static int driver_probe(struct pci_dev *pdev, ...)
{
    driver_cache = kmem_cache_create("driver_desc",
        sizeof(struct driver_desc),
        SMP_CACHE_BYTES,  // 对齐到 L1 Cache Line
        SLAB_HWCACHE_ALIGN,
        NULL);
    // 分配
    desc = kmem_cache_alloc(driver_cache, GFP_KERNEL);
}

/*
 * 最佳实践 2: GFP 标志选择
 *   GFP_KERNEL  : 可睡眠分配(进程上下文)
 *   GFP_ATOMIC  : 不可睡眠(中断、持有自旋锁)
 *   GFP_NOIO    : 不触发 I/O(块层内部使用,避免死锁)
 *   GFP_NOFS    : 不调用文件系统(文件系统内部使用,避免死锁)
 */

/*
 * 最佳实践 3: 模块退出时必须销毁 cache,否则内存泄漏
 */
static void driver_remove(struct pci_dev *pdev)
{
    kmem_cache_free(driver_cache, desc);
    kmem_cache_destroy(driver_cache);
}

9.2 sysctl 调优

# /etc/sysctl.conf 推荐配置

# 1. slab 回收积极性(默认 100,增大更积极回收 vm.slab_reclaim_factor = 200

# 2. 限制 slab 内存占总内存比例(默认无显式限制,通过 min_free_kbytes 间接控制)
vm.min_free_kbytes = 262144  # 1GB 内存建议设为 256MB 级别

# 3. 开启 slab 合并(减少碎片)
# 自 Linux 3.15 起 SLUB 支持同大小 cache 合并,默认开启
slab_nomerge=0

# 4. 调优 slab 缓存大小分配策略(按需调整)
# /sys/kernel/slab/$cache_name/ 中的参数可以运行时调整
echo 32 > /sys/kernel/slab/kmalloc-128/cpu_partial  # 增加每 CPU 缓存量

9.3 NUMA 感知分配

# 查看 NUMA 拓扑(影响 slab node 分布)
numactl --hardware

# 强制在特定 NUMA 节点分配
numactl --membind=0 ./my_workload

# 在驱动中指定节点分配
desc = kmem_cache_alloc_node(driver_cache, GFP_KERNEL, node_id);

十、高级话题:Slab 分配器的未来演进

10.1 Folio 与 Slab

Linux 5.16 引入 Folio(大页封装结构),为 2 的幂次大小连续页框提供服务。Folio 与分配器的协作被认为是处理各种连续大小内存请求的方向,长期目标是实现更灵活的 slab 基础结构。

10.2 cgroup 与 Slab 内存控制

Linux 6.0 逐步引入系统化内存控制,cgroup 逐步获得对回收、分配行为的更多控制。未来 cgroup v2 将影响 slab 分配器,因为内存压力指标直接影响 slab 回收。

10.3 BPF 与 Slab 分配器

eBPF 子系统大量使用BPF 本地存储(per-cpu/hash/array map),依赖 SLUB 分配器管理 map 元数据。高频 BPF map 操作对 SLUB 分配器提出了低延迟要求。Linux 6.8+ 对 BPF map 缓存进行了优化。

10.4 自动合并大小策略

Linux 3.15+ SLUB 支持合并相同或相似大小的 cache(slab 合并)。通过减少碎片,提高 cache 利用率,同时对内核编译时即创建的缓存效果尤为显著。

十一、总结与学习路径

Slab 分配器是 Linux 内核内存管理的基石。从原理到实践:

学习阶段一 - 原理理解:阅读 mm/slab.c 早期实现,理解 kmem_cache、slab 和伙伴系统的协作关系。

学习阶段二 - SLUB 深入:阅读 mm/slub.c 核心代码,理解 per-cpu freelist、partial 列表管理、着色算法。

学习阶段三 - 调试与调优:使用 /proc/slabinfo、slabtop、kmemleak 分析系统内存使用,通过 sysctl 和 pfn 参数优化。

学习阶段四 - 模块实战:编写内核模块,使用 kmem_cache_create 管理私有对象,结合 drgn 做深度分析。

推荐资源:

  • 内核源码 mm/slub.c:真正的权威文档
  • Documentation/vm/slub.rst:内核官方文档
  • Understanding the Linux Kernel 第 8 章:经典教材
  • kernel 邮件列表 (mm/tree):最新的 Slub 演进

理解 Slab 分配器不仅是理解 Linux 内核内存管理的必经之路,也是开发高性能内核模块和驱动的基础。在内存敏感的场景(网络、存储、容器)中,合理的 Slab 策略可以显著减少内存浪费和 Cache 提升系统整体吞吐。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部