Linux 内核 SLUB 分配器深度实战

Linux 内核 SLUB 分配器深度实战:从 kmem_cache 到生产级内存碎片治理

在 Linux 内核的内存管理体系中,SLUB(Unqueued Slab)分配器是当前默认的 slab 分配器实现,它直接服务于内核中高频、小对象的分配请求。本文将深入剖析 SLUB 的内部机制,并结合生产环境中的 OOM 排查、碎片治理和性能调优,给出可落地的工程实践方案。

一、为什么需要 Slab 分配器

Linux 物理内存管理以 页(Page,通常 4KB) 为基本单位。但内核中大量对象远小于一页:task_struct 约 1.8KB、inode 约 600B、dentry 约 192B。如果直接用 alloc_pages() 分配整页来存放这些小对象,内部碎片(Internal Fragmentation)将极其严重。

Slab 分配器的核心思想是:将一页(或多个页)预先划分为固定大小的对象槽(slab object),通过对象池复用,消除内部碎片,同时减少 Buddy System 的分配压力。

Linux 内核实现了三种 slab 分配器: - SLAB:最早的实现,引入 per-CPU 缓存和着色(coloring),但锁竞争和队列管理复杂。 - SLOB:面向嵌入式极简场景,使用首次适应(first-fit)算法,碎片严重。 - SLUB:当前默认分配器,简化了队列管理("Unqueued"即取消 partial 队列的全局链表),降低锁开销,更适合 SMP 系统。

二、SLUB 数据结构全貌

理解 SLUB 的核心数据结构,是进行调试和优化的前提。

2.1 四大核心结构

struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;  // per-CPU 快速路径
    struct kmem_cache_node *node[MAX_NUMNODES]; // 每个 NUMA node 的 partial/full 页链表
    unsigned int object_size;     // 对象原始大小
    unsigned int size;            // 含对齐和元数据的实际大小
    unsigned int offset;          // 下一个空闲对象的指针偏移
    struct kmem_cache_order_objects oo;  // 一页中包含的对象数
    ...
};

struct kmem_cache_cpu {
    void **freelist;    // 指向下一个空闲对象的指针
    unsigned long tid;   // 事务 ID,用于无锁同步
    struct page *page;   // 当前正在使用的 slab 页
};

struct kmem_cache_node {
    struct list_head partial;  // 部分空闲的 slab 页链表
    spinlock_t list_lock;      // 保护 partial 链表的锁
};

2.2 对象分配的三级路径

SLUB 的分配遵循"快路径优先"原则:

分配请求 → per-CPU freelist(原子操作,无锁)
              ↓ 无空闲对象
         per-CPU page 的 partial 链表
              ↓ 仍无空闲
         kmem_cache_node->partial(跨 CPU 窃取,需加锁)
              ↓ 仍无空闲
         alloc_pages() 从 Buddy 申请新页,初始化 slab

这个三级路径的设计确保了热路径(hot path)几乎不需要加锁,极大提升了多核并发的吞吐量。

三、SLUB 着色(Cache Coloring)

现代 CPU 的 L1 Cache 通常采用组相联映射,物理地址的 index bit 决定映射到哪个 cache line。当多个 slab 对象在内存中的偏移具有相同的 cache index 时,会发生 cache line 冲突(cache thrashing),导致性能急剧下降。

SLUB 通过着色(coloring)解决此问题:每次新分配 slab 时,随机偏移 slab 内部对象的起始位置(偏移量为 colour_off),使不同 slab 中同一逻辑偏移的对象映射到不同的 cache line。

slab 1: [colour=0x00] | obj0 | obj1 | obj2 | obj3 | ...
slab 2: [colour=0x40] | padding | obj0 | obj1 | obj2 | ...
slab 3: [colour=0x80] | padding | padding | obj0 | obj1 | ...

可用的颜色数量 = (cache_line_size - sizeof(kmem_cache_node_alignment)) / sizeof(void*)。在 x86-64 上,L1 cache line 为 64B,若对象大小为 192B,则每页大约可用 4096/192 ≈ 21 个对象,着色范围约数十个偏移档位。

四、SLUB Debug:对象 Poisoning 与 Red Zone

生产环境中最常见的 slab 相关 bug 是越界写(out-of-bounds write) 和释放后重用(use-after-free)。SLUB 提供了强大的调试机制来帮助捕获这些问题。

4.1 Poisoning

通过 slub_debug=UFP 启动参数启用 poison 调试:

  • U(Use-after-free):释放时填充 0x6b("free"),分配时填充 0x5a("used"),检测未初始化使用。
  • F(Free):释放时填充 poison 字节,检测重复释放(double-free)。
  • P(Poisoning):对象两侧填充特定 poison 字节,检测越界写。

当 poison 校验失败,内核会打印类似如下信息:

slub: Poison overwritten at ffff888123456780: size 192 offset 200
[<ffffffff81234567>] my_oob_write_function+0x45/0x80 [my_module]

4.2 Red Zone

在对象末尾(超出用户可见长度之外)额外分配一段 guard area,填充 0xcc。写入越界时立即被检测到。代价是每个对象增加一个 cache line 的内存开销。

# 查看特定 cache 的 debug 状态
cat /sys/kernel/slab/kmalloc-192/trace

4.3 常用的 Debug 接口

# 开启对特定 kmem_cache 的调试
echo 1 > /sys/kernel/slab/dentry/sanity_checks
echo 1 > /sys/kernel/slab/dentry/red_zone
echo 1 > /sys/kernel/slab/dentry/poison
echo 1 > /sys/kernel/slab/dentry/store_user  # 记录分配/释放调用栈

# 查看所有 slab 缓存的活跃对象数
cat /proc/slabinfo | head -20

/proc/slabinfo 输出详解:

dentry       254321 267890  192   21    1 : tunables  0   0   0 : slabdata  12757  12757    0
 活跃对象   全部对象  大小  每页   页数

注意到本例中 254321 / 267890 ≈ 95% 活跃率,这表明 dentry cache 内存回收压力不大。若活跃率极低但 slab 页数很大,说明存在严重的 slab 页碎片/泄漏。

五、生产环境 OOM 与 Slab 排查实战

5.1 场景一:dentry/inode cache 失控导致 OOM

某线上服务在批量文件操作后突然 OOM,dmesg 显示:

Out of memory: Killed process 12345 (file-writer) total-vm:8GB anon-rss:1GB file-rss:0 slab-risk:5GB

排查步骤:

# 1. 确认 slab 占用
cat /proc/meminfo | grep -i slab
Slab:             5242880 kB
SReclaimable:     4718592 kB    # dentry/inode cache 都属于可回收 slab
SUnreclaim:        524288 kB    # 不可回收部分,关注是否有内核模块泄漏

# 2. 指出具体 cache
awk '$3*$4 > 1048576 {print $1, $3*$4/1024 "KB"}' /proc/slabinfo
dentry       3145728 KB
inode_cache  1572864 KB
kmalloc-64    655360 KB        # 异常偏高,需排查所属模块

# 3. 触发 slab 回收
echo 2 > /proc/sys/vm/drop_caches  # 先清 pagecache,释放文件句柄引用
echo 3 > /proc/sys/vm/drop_caches  # 清 dentry/inode cache

根因:应用大量 open() 但不 close(),导致 dentry 关联的 inode 引用计数不为零,shrinker 无法回收。事后修改代码,确保文件操作使用 O_CLOSEEXEC 并在完成后显式关闭。

5.2 场景二:kmalloc 通用 cache 的内存泄漏

某自定义字符设备模块在高频中断下触发以下 slabinfo:

kmalloc-512   838860  838860  512  30    2 : ...
kmalloc-256  1677720 1677720  256  51    2 : ...
kmalloc-128  3355440 3355440  128  64    2 : ...

所有对象全部活跃,且重启后数量持续增长。

调试方法:

# 启用 slub debug 追踪分配栈
echo 1 > /sys/kernel/slab/kmalloc-512/store_user
# 等待泄漏复现后
cat /sys/kernel/slab/kmalloc-512/trace | head -50

输出会显示每次分配的调用栈:

0xffffc90000a3bde0: 0xffffc90000a3bde0 my_driver_ioctl+0x8a/0x120 [my_driver]
0xffffc90000a3bde0: 0xffffc90000a3bde0 my_driver_ioctl+0x8a/0x120 [my_driver]
...

根因:ioctl 分配了缓冲区,但在错误路径(error path)中忘记 kfree。修复后,建议使用 devm_kzalloc() 自动管理资源,从根本上避免泄漏。

六、高级议题:Folio 与 Slab 的融合演进

Linux 5.16 引入了 Folio——将复合页(compound page)抽象为单一类型,替代原始的 struct page。这对 SLUB 有深远影响:

  1. 大页 slab:SLUB 可以利用 Folio 实现 transparent huge slab——当 slab 对象较大(≥PAGE_SIZE/2),自动分配 2MB 大页,减少 TLB miss。
  2. type 安全:folio 与 page 在类型系统层面区分,防止将 folio 误当单页使用,提升代码健壮性。
  3. 降低初始化开销:Folio 通过 memcg 延迟记账减少 slab 分配路径的 memcg 开销。
// 早期的 alloc_slab_page(单页)
struct page *alloc_slab_page(gfp_t gfp, ..., unsigned int order);

// Folio 时代的演进:对 order > 0 的 slab 使用 folio
struct folio *alloc_slab_folio(gfp_t gfp, ..., unsigned int order);

七、性能调优参数与最佳实践

7.1 关键 sysctl 参数

参数 含义 典型场景
vm.vfs_cache_pressure dentry/inode 回收优先级(默认100) 大量文件操作时调高
slub_min_objects 最少对象数/每 slab 页 大对象 slab 需限制页数
slub_max_order 单个 slab 页最大 buddy 阶数(默认3,即8页) 高内存机器可调到 4
slab_nomerge 禁止相似 cache 合并(debug 用) 开启时精确追踪泄漏源

7.2 自定义 kmem_cache 的最佳实践

当需要为特定数据结构创建专属 cache 时:

static struct kmem_cache *my_ctx_cache;

static int __init my_init(void)
{
    my_ctx_cache = kmem_cache_create("my_ctx",
                                     sizeof(struct my_ctx),  // 对象大小
                                     0,                       // 对齐(0 = 自然对齐)
                                     SLAB_HWCACHE_ALIGN,     // 硬件 cache 对齐
                                     NULL);                  // 无构造函数
    if (!my_ctx_cache)
        return -ENOMEM;
    return 0;
}

void my_func(void)
{
    struct my_ctx *ctx = kmem_cache_alloc(my_ctx_cache, GFP_KERNEL);
    // ... 使用 ctx
    kmem_cache_free(my_ctx_cache, ctx);
}

关键原则: - 避免频繁 alloc/free:若分配频率极高,kmem_cache 不一定优于对象池(mempool);但在路径中对象生命周期短、大小固定时,kmem_cache 是最佳选择。 - 注意 GFP 标志:中断上下文必须用 GFP_ATOMIC,可能失败;进程上下文可用 GFP_KERNEL,会触发 direct reclaim。 - 关注 NUMA locality:kmem_cache_alloc 默认从当前 node 分配,跨 node 访问可能引发 40% 性能损失。使用 kmem_cache_alloc_node() 明确指定 node。

八、总结

SLUB 作为 Linux 内核的默认 slab 分配器,承载着几乎全部内核对象的快速分配职责。掌握其数据结构、debug 机制和性能调优方法,是系统级工程师的核心能力。

核心要点回顾:

  1. 三级分配架构:per-CPU freelist → kmem_cache_node partial → Buddy alloc,确保热路径无锁。
  2. 着色机制:通过随机偏移消除 cache line 冲突,提升多核扩展性。
  3. slub_debug:生产环境排查 OOM / 泄漏 / 越界的利器,poison、red_zone、store_user 三件套。
  4. 监控指标:/proc/slabinfo 的活跃率(active / total)是最直观的 slab 健康度指标。
  5. 演进趋势:Folio 的引入为大页 slab 和 type safety 铺平了道路,未来 SLUB 将在 HPC 和存储场景发挥更大价值。

对于运维和开发同学而言,养成阅读 /proc/slabinfo 的习惯,如同使用 top 监控 CPU、iostat 监控 I/O,是每一位 Linux 内核使用者必备的基本功。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部