Linux 内核 SLUB 分配器深度工程实战:从 Per-CPU 缓存到 NUMA 感知的内存分配

引言

内核内存分配器是 Linux 性能基础设施的基石。SLUB(Unqueued Slab Allocator)自 2.6.23 起取代 SLAB 成为默认分配器,其核心设计哲学是"极简即高效"——去除复杂的队列管理,利用 Per-CPU 本地缓存和 cmpxchg 无锁路径实现极致的快速分配。

本文将从工程实战角度深入剖析 SLUB 分配器的完整工作机制:从 Per-CPU _partial 队列的二阶缓存策略,到 NUMA 节点本地内存池的组织方式;从 kmem_cache 的自定义创建与调试接口,到跨 CPU 缓存污染(cache bouncing)的排查与优化策略;再到内存碎片化检测、POISON/REDFZONE 安全机制以及 kmemleak 与 KASAN 的联动分析。

一、SLUB 的数据结构模型

1.1 核心结构体关系

SLUB 的内存管理建立在三个核心结构体之上:


struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;    // Per-CPU 快速路径
    struct kmem_cache_node *node[MAX_NUMNODES]; // NUMA 节点级管理
    unsigned long object_size;                    // 对象实际大小
    unsigned long size;                           // 含元数据的对象大小
    unsigned int offset;                          // 下一个空闲对象偏移
    struct kmem_cache_order_objects oo;          // 最佳 slab 阶数
    // ...
};

struct kmem_cache_cpu {
    void **freelist;           // 空闲对象链表头
    unsigned long tid;         // 全局事务 ID(防 ABA)
    struct page *page;         // 当前正在使用的 slab 页
    struct page *partial;      // Per-CPU partial 链表
};

struct kmem_cache_node {
    spinlock_t list_lock;
    unsigned long nr_partial;
    struct list_head partial;  // Node 级 partial slab 链表
};

1.2 对象生命周期与空闲链表

SLUB 使用内嵌式空闲链表(intrusive free list)管理 slab 内的空闲槽位。一个空闲对象的内存位置被复用为 void *next 指针:


static inline void *get_freep(struct kmem_cache *s, void *object)
{
    return *(void **)(object + s->offset);
}

static inline void set_freep(struct kmem_cache *s, void *object, void *fp)
{
    *(void **)(object + s->offset) = fp;
}

当 slab 全满时,该 slab 脱离 CPU 控制;当 slab 中部分对象被空闲时,根据「最近使用」策略挂载到 Per-CPU partial 或 Node partial 链表。

二、快速路径:kmem_cache_alloc 的执行流

2.1 热路径(无锁快速分配)

SLUB 最快路径仅需约 10 条汇编指令,完全不涉及任何锁:


static __always_inline void *slab_alloc_node(struct kmem_cache *s,
        gfp_t gfpflags, int idx, unsigned long addr)
{
    struct kmem_cache_cpu *c = raw_cpu_ptr(s->cpu_slab);
    struct page *page = c->page;
    void *object;

    if (unlikely(!c->freelist))
        return __slab_alloc(s, gfpflags, idx, addr, c);

    object = c->freelist;            // 取空闲链表头
    c->freelist = get_freep(s, object);  // 更新链表头
    c->tid = next_tid(c->tid);       // 事务 ID 递增

    return object;
}

这条路径的关键约束是无锁——每 CPU 独占 cpu_slab,除 slab 页面切换外不存在任何竞争。

2.2 慢路径:Per-CPU Page 耗尽

当 c->page 的 freelist 为空时,进入 __slab_alloc():


__slab_alloc:
  1. 将 c->page 从 Per-CPU 移除(可能移入 node->partial 或释放回 buddy)
  2. 尝试从 c->partial 获取新 page
  3. 若 partial 也为空,从 node->partial 借一个
  4. 若 node->partial 也为空,调用 new_slab() 从 buddy 系统申请
  5. 将新 page 设为 c->page,重试热路径

2.3 事务 ID 与 RCU 安全的关联

tid(Transaction ID)用于实现 lock-free 的 RCU 读取端保护。cmpxchg 操作结合 tid 的检测可以在不加锁的情况下检测期间是否有其他 CPU 修改了 per-cpu 状态:


void *ptr = c->freelist;
unsigned long tid = c->tid;

preempt_disable();  // 阻止被抢占导致 slab 被迁移
ptr = ____cmpxchg(&c->freelist, ptr, ...);
preempt_enable();

三、NUMA 感知与 Node 级管理

3.1 NUMA 本地分配策略

SLUB 在每个 NUMA 节点上维护独立的 partial 列表,以尽量减少远程内存访问:


struct kmem_cache_node {
    spinlock_t list_lock;         // node 级锁(跨 CPU 共享)
    unsigned long nr_partial;
    struct list_head partial;     // 跨 CPU 共享的 partial 链表
    
    #ifdef CONFIG_SLUB_DEBUG
    unsigned long nr_slabs;
    unsigned long total_objects;
    struct list_head full;        // 全满链表仅用于调试
    #endif
};

分配优先级(由 kmem_cache_alloc_node 决定):


1. CPU 本地 freelist
2. CPU 本地 partial
3. 同一 NUMA node 的 partial
4. 远程 NUMA node 的 partial(fallback)
5. 从 buddy system 申请新 slab

3.2 kmem_cache_alloc_node 的实现


static __always_inline void *slab_alloc_node(struct kmem_cache *s,
        gfp_t gfpflags, int node, unsigned long addr)
{
    // 若 node 为 NUMA_NO_NODE(-1),则使用当前 CPU 的 node
    if (node == NUMA_NO_NODE)
        node = numa_mem_id();
    
    // ... 快速路径失败后尝试 node partial
    if (node != NUMA_NO_NODE)
        n = get_node(s, node);
}

四、内存碎片化问题

4.1 内部碎片 vs 外部碎片

SLUB 主要面临两类碎片问题:

  • 内部碎片(Internal Fragmentation):一个 object 实际需要的空间比 kmem_cache->size 小(由于元数据和对齐造成的浪费)
  • 外部碎片(External Fragmentation):partial slab 中散布的空闲对象无法合并成完整 slab

4.2 /proc/slabinfo 解析


$ 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         2304   2304    128   32    1 : tunables  0    0    0 : slabdata     72     72    0
dentry             18540  18540    192   21    2 : tunables  0    0    0 : slabdata    883    883    0
kmalloc-96          4186   4256     96   42    1 : tunables  0    0    0 : slabdata    101    101    0

关键字段说明:

字段 含义
active_objs 正在使用的对象数
num_objs 总分配对象数
objsize 单个对象大小(含元数据)
objperslab 每 slab 对象数
active_slabs 活跃 slab 数
num_slabs 总 slab 数

4.3 碎片评估指标


# 碎片率 = 1 - (active_objs / num_objs)
# 高碎片率意味着大量内存被"半空 slab"占用

# 使用 slabtop 实时监控
watch -n 1 'sudo slabtop -o'

五、Slab 合并与抗碎片策略

5.1 Slab 合并(Slab Merging)

某些大小和生命周期的 kmem_cache 在初始化阶段会被自动合并以减少碎片:


// mm/slub.c
static struct kmem_cache *create_kmalloc_cache(const char *name,
        unsigned int size)
{
    struct kmem_cache *s = kmem_cache_create(name, size, 0,
                            SLAB_HWCACHE_ALIGN, NULL);
    // 若存在相近大小的现有 cache,可能合并
    return s;
}

SLUB 内部对相似大小的 cache 不做自动合并(区别于 SLAB 的部分合并),但通过精心设计的 kmalloc 层次(kmalloc-{8,16,32,64,96,128,192,256,512,1024,2048,4096,8192})来覆盖大部分分配需求。

5.2 CONFIG_SLUB_CPU_PARTIAL 优化

内核参数 slub_max_slabs(通过 /sys/kernel/slab//cpu_partial 控制)决定了 Per-CPU partial 链表的最大 slab 数:


# 查看和设置 CPU partial 限制
cat /sys/kernel/slab/dentry/cpu_partial
echo 20 > /sys/kernel/slab/dentry/cpu_partial

较高的值能减少跨 Node 分配但也增加内存占用;较低的值相反。

六、调试与安全机制

6.1 KASAN(Kernel Address Sanitizer)与 SLUB 的集成

KASAN 通过 shadow memory 检测 use-after-free 和越界访问。与 SLUB 配合时:

  • 每个 SLUB 对象周围放置 redzone 区域
  • 释放的对象进入 freelist 但 shadow memory 被标记为不可访问
  • 分配时清除 shadow 标记

// KASAN 释放流程
void kasan_kfree_large(struct kpage *page)
{
    // 标记整个页不可访问
    kasan_poison_shadow(page_address(page), page_size(page),
                       KASAN_KFREE);
}

6.2 SLUB_DEBUG 的运行时检测

SLUB_DEBUG 提供多层次的安全检查:

  • F(Free)检测:释放后填充 0x6b/0x5a 模式,读取时检查是否被篡改
  • P(Poison)检测:释放时填充 0x5a 模式,分配时清除
  • U(User Tracking)检测:跟踪每个对象的分配/释放调用栈
  • R(Redzone)检测:在对象边界添加哨兵区域
  • Z(Tracking Zero)检测:跟踪未初始化但已分配的对象
  • T(Trace)检测:打印每次分配/释放的调用栈

# 开启特定 cache 的调试
echo 1 | sudo tee /sys/kernel/slab/kmalloc-256/redzone
echo 1 | sudo tee /sys/kernel/slab/kmalloc-256/poison
echo tty_*_p | sudo tee /sys/kernel/slab/kmalloc-256/trace

6.3 通过 kmemleak 检测内存泄漏

kmemleak 通过扫描内存发现不可达但未被释放的内核对象:


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

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

# 清除当前扫描结果(重新基线)
echo clear > /sys/kernel/debug/kmemleak

# 典型泄漏报告
unreferenced object 0xffff888123456000 (size 256):
  comm "insmod", pid=1234, jiffies 5432100
  backtrace:
    [<00000000c0ffee12>] kmem_cache_alloc_trace+0x1a2/0x230
    [<00000000deadbeef>] my_module_init+0x45/0x100
    [<00000000cafebabe>] do_one_initcall+0x4a/0x1c0

七、性能调优实践

7.1 kmem_cache_create 的最佳实践


// 自定义对象缓存 - 替代 kmalloc 以获得确定性的性能
struct my_struct {
    spinlock_t lock;
    void *data;
    struct list_head node;
    // 频繁创建销毁的结构体适合专用缓存
};

static struct kmem_cache *my_cache;

// 模块初始化时创建
my_cache = kmem_cache_create("my_struct_cache",
                             sizeof(struct my_struct),
                             L1_CACHE_BYTES,      // 缓存行对齐
                             SLAB_HWCACHE_ALIGN | SLAB_ACCOUNT,
                             NULL);

// 分配(比 kmalloc 更快,无对齐惩罚)
struct my_struct *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);

// 批量预分配优化
for (i = 0; i < BATCH; i++)
    pool[i] = kmem_cache_alloc(my_cache, GFP_KERNEL);

7.2 percpu 计数器与 SLUB 的协同

高速场景中可以结合 percpu 计数器减少对 SLUB 的分配压力:


// 分配 percpu 数据,完全绕过 SLUB
struct my_stats __percpu *stats;
stats = alloc_percpu(struct my_stats);  // 每个 CPU 一份,NUMA-local

// 使用
struct my_stats *s = get_cpu_ptr(stats);
s->counter++;
put_cpu_ptr(s);

7.3 CPU Cache Bouncing 排查

当多个 CPU 频繁操作同一 slab_cache 中的不同对象时,可能引发缓存行乒乓(cache bouncing):


# 方法 1:使用 perf 检测 LLC miss
perf stat -e cache-misses -e cache-references -C 0-7 -a sleep 5

# 方法 2:统计每 slab cache 的分配频率
perf probe 'kmem_cache_alloc name=+0(%di):string'
perf record -e probe:kmem_cache_alloc -aR sleep 10

# 方法 3:使用 ftrace 跟踪
echo kmem_cache_alloc > /sys/kernel/debug/tracing/set_ftrace_filter
echo function_graph > /sys/kernel/debug/tracing/current_tracer
cat /sys/kernel/debug/tracing/trace_pipe

八、SLUB 与 SLAB 的工程权衡

8.1 何时 SLUB 不如 SLAB

虽然 SLUB 是默认选择,但在以下场景下可能并不适合:

场景 分析
极小对象(<64B) SLUB 的元数据开销占比过高
非常大的对象(>PAGE_SIZE/2) 都走 page allocator,无差异
高频 bulk 分配释放 SLAB 的批量回收更高效
NUMA 跨节点频繁访问 SLAB 的 NUMA-aware 着法更成熟(但 SLUB 已追平)

8.2 SLUB 与 SLOB:嵌入式场景

SLOB(Simple List Of Blocks)是面向极内存受限嵌入式设备的分配器:

  • 代码量极小(约 1500 行)
  • 使用首次适应(first-fit)简单算法
  • 无 per-cpu 缓存,因此开销极低但性能不稳定

选择优先级:
- 服务器/桌面/移动: SLUB(默认)
- 嵌入式且内存 < 64MB: SLOB
- 实时性要求极高: 自定义静态分配或 mempool

九、实战问题排查案例

9.1 案例1:系统内存被 slab "吃掉"

现象:free -m 显示 buff/cache 很高,且 Slab 字段持续上升。

排查过程:


# 1. 查看哪些 cache 占用最多
sudo slabtop -o --sort=a

# 2. 检查是否是 dentry/inode 缓存过多
cat /proc/sys/vm/vfs_cache_pressure  # 默认 100
# 值越高,内核越倾向于回收缓存

# 3. 手动触发回收
sync
echo 2 > /proc/sys/vm/drop_caches  # 清除 slab 可回收项

9.2 案例2:SLUB_DEBUG 揭示 use-after-free

现象:开启 SLUB_DEBUG 后,slab 错误日志:


=============================================================================
BUG kmalloc-256 (Tainted: P         C): Redzone overwritten
INFO: 0x00000000deadbeef-0x00000000cafebabe. First byte 0x6b vs 0x6a
INFO: Allocated in module_init+0x45/0x100 [my_mod] age=1234 cpu=3 pid=5678
INFO: Freed in my_work_handler+0x8a/0x120 [my_mod] age=1111 cpu=2 pid=1234

根因:工作队列处理线程在释放对象后仍在使用它。需要在 kfree 前确保无其他引用。

9.3 案例3:启动参数调试


# 启动时关闭 SLUB 合并/Slab debug 验证
slub_debug=-    # 全局禁用
slub_debug=FZPU # 强制开启所有调试特性

# 查看当前配置
cat /proc/slabinfo | grep -E "kmalloc|dentry|inode"

十、内核新进展

5.14+ 版本引入的特性

  • slub_kunit:KUnit 测试框架覆盖核心分配/释放路径
  • Batch freeing:一次性多个对象释放时降级为批量链表操作,减少 cmpxchg 次数
  • Per-object metadata reduction:减少非调试模式下的元数据开销

6.x 方向

  • Memory Tiering Support:SLUB 增加对 MEMORY_HOTPLUG 和异构内存(如 CXL)的 tiering 感知分配
  • Folio-based Allocation:将 SLUB 与大页(Folio)结合,提升大对象分配效率
  • MTE (Memory Tagging Extension):ARM64 的硬件内存标签支持与 SLUB 深度集成,提供硬件级 use-after-free 检测

总结

作为 Linux 内核的默认内存分配器,SLUB 在设计上坚持了"热路径极简"的工程哲学:无锁快速通道、Per-CPU 本地缓存、NUMA 感知 fallback 策略三者的组合使其在绝大多数场景下都能提供稳定且优异的性能。掌握其内部机制不仅有助于内存问题的精准调优,同时也是理解内核内存管理全貌的关键入口。

对于高性能驱动或内核模块开发者,建议:高频分配对象的场景使用 kmem_cache_create 创建专用缓存;调试阶段开启 SLUB_DEBUG 确保内存安全;生产环境结合 KASAN 和 kmemleak 持续监控内存健康状态。

点赞(0) 打赏

评论列表 共有 0 条评论

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

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
/* 跳过导航链接 (无障碍) */ position: absolute; top: -100px; left: 15px; z-index: 99999; padding: 8px 16px; background: #007bff; color: #fff; font-size: 14px; border-radius: 0 0 4px 4px; text-decoration: none; transition: top 0.2s; } top: 0; outline: 3px solid #0056b3; }