Linux Kernel Slab Allocator 深度实战:从 kmem_cache 到内存分配器实现原理
引言
在 Linux 内核开发中,内存管理是一个核心课题。与用户空间使用 malloc/free 不同,内核需要处理更复杂的场景:物理内存碎片、DMA 约束、NUMA 拓扑、实时性要求。Slab Allocator(slab 分配器)作为内核内存管理子系统的重要组成部分,专门解决频繁创建/销毁同类型对象时的性能问题。
本文将深入剖析 slab 分配器的架构设计、核心数据结构、分配与释放流程、三种变体(SLAB/SLUB/SLOB)、性能调优技巧以及驱动开发中的最佳实践。
一、为什么需要 Slab Allocator
1.1 直接操作伙伴系统的问题
Linux 内核的 Buddy System(伙伴系统)以页(通常 4KB)为单位管理物理内存,最小分配单位为 1 页。如果内核频繁分配一个小结构体(如 128 字节的 file 结构),直接通过 __get_free_pages() 会产生严重的内存浪费:
- 内部碎片:128 字节对象占用整页 4096 字节,利用率仅 3.1%
- 初始化开销:每次分配都需要重新初始化对象的各个字段
- 缓存局部性差:对象分散在不同物理页,CPU Cache 命中率低
1.2 Slab 的核心思想
Slab 分配器借鉴了 SunOS 中 Jeff Bonwick 提出的方案,核心思路是对象缓存(Object Cache):
- 预分配一组连续物理页(slab),在其中切分出固定大小的槽位
- 相同类型的对象共享同一个缓存(kmem_cache),复用已初始化状态
- 利用着色(Coloring)优化 Cache Line 利用率
二、核心数据结构
2.1 kmem_cache 结构体
每个对象类型对应一个 kmem_cache 实例,它是 slab 分配器的核心管理结构:
struct kmem_cache {
struct kmem_cache_cpu *cpu_slab; // Per-CPU 快速分配缓存
unsigned int size; // 对象实际大小(含对齐)
unsigned int object_size; // 用户请求的裸对象大小
unsigned long flags; // 标志位(如 SLAB_ACCOUNT)
unsigned int offset; // 下一个空闲对象的 freelist 偏移
const char *name; // 缓存名称(用于 /proc/slabinfo)
struct list_head list; // 全局缓存链表
struct kmem_cache_node **node; // NUMA 节点数组
struct array_cache *array[NR_CPUS]; // Per-CPU 辅助缓存(SLAB 变体)
unsigned int limit; // array_cache 中最大空闲对象数
unsigned int batchcount; // 一次转移的对象数量
unsigned int gfporder; // 每个 slab 包含的页数 (2^n)
unsigned int colour; // 可用着色区域数量(偏移种类)
unsigned int colour_off; // 每次着色的偏移增量(Cache Line 大小)
struct kmem_cache *slabp_cache; // 管理 slab 描述符的专用缓存
slab_t *freelist; // 全局空闲对象链表
// ... 统计与调试字段
};
2.2 Slab 描述符
每个物理 slab 页组都有一个 slab 描述符,记录该 slab 的使用状态:
struct slab {
struct list_head list; // 挂载到 node 的 full/partial/free 链表
unsigned long s_mem; // slab 页中第一个对象的虚拟地址
unsigned int inuse; // 已被分配的对象数量
unsigned int free; // 第一个空闲对象的索引
};
根据 slab 所在 NUMA 节点的链表位置,分为三类:
- free:所有槽位未使用
- partial:部分已使用、部分空闲
- full:全部槽位已被分配
2.3 Per-CPU 快速缓存:kmem_cache_cpu
SLUB 变体中使用,每个 CPU 维护一个 freelist 缓存,避免加锁:
struct kmem_cache_cpu {
void **freelist; // 下一个空闲对象的指针
unsigned long tid; // 事务 ID(防止竞争)
struct page *page; // 当前使用的 slab 页
struct page *partial; // Per-CPU partial slab 链表
};
2.4 NUMA 感知:kmem_cache_node
在 NUMA 系统中,每个 NUMA 节点都有一套独立的部分满/空闲 slab 列表:
struct kmem_cache_node {
spinlock_t list_lock; // 节点级保护锁
unsigned long nr_partial; // partial slab 数量
struct list_head partial; // partial slab 链表
unsigned long nr_slabs; // 节点上的总 slab 数
unsigned long total_objects; // 节点上的总对象数
struct list_head full; // full slab 链表
};
三、分配与释放流程详解
3.1 分配流程(kmem_cache_alloc)
以现代 SLUB 分配器为例,分配路径分为三级快速缓存:
kmem_cache_alloc(cache, flags)
│
▼
kmem_cache_alloc_trace() → 添加 kasan/kfence 插装
│
▼
slab_alloc_node()
│ ┌─ 第一级: kmem_cache_cpu->freelist(无锁,per-CPU)
│ │ └─ 命中: 直接返回 freelist 顶部对象
│ │
│ ├─ 第二级: Per-CPU partial 链表
│ │ └─ 有可用 page: 切换到该 page,更新 freelist
│ │
│ ├─ 第三级: kmem_cache_node->partial(加节点锁)
│ │ └─ 取出一个 partial slab,移至 CPU 页
│ │
│ └─ 慢路径: 通过 Buddy System 分配新页
│ └─ new_slab() → 返回一个 free slab
▼
返回对象指针
关键优化点:热路径上(第一级命中),仅需几条汇编指令即可完成分配,无需任何原子操作或锁。
3.2 释放流程(kmem_cache_free)
释放是分配的逆过程,同样优先走 Per-CPU fast path:
kmem_cache_free(cache, objp)
│
▼
slab_free()
│ ┌─ 判断 obj 是否属于当前 CPU 的 kmem_cache_cpu->page
│ │ ├─ 是: 直接归还到 per-CPU freelist(无锁)
│ │ └─ 否: 进入慢路径
│ │
│ ├─ 第三级: 若 slab 从未满变为 empty → 归还给 node
│ │ 若 slab 从 full 变为 partial → 挂载到 node->partial
│ │
│ └─ 慢路径: 若 slab 变回 free → 可归还给 Buddy System
│ (通常保留在 partial 链表中以加快下次分配)
▼
释放完成
3.3 Freelist 指针存储技巧
SLUB 使用 union 技巧存储空闲指针:未使用的槽位复用为 freelist 指针,无需额外链表结构。
// 空闲槽位的内存布局(Union 复用):
// [freelist 指针] 指向下一个空闲索引/地址
// 分配的槽位:存储实际对象数据,完全覆盖 union 区域
// 获取下一个空闲对象
void *next = freelist[offset];
// 更新 freelist
freelist = next;
四、三种 slab 变体对比
| 特性 | SLAB(传统) | SLUB(默认) | SLOB(精简) |
|---|---|---|---|
| 设计目标 | 高性能、全功能 | 简化、可维护 | 极低内存开销(嵌入式) |
| 内核引入 | 2.1 早期 | 2.6.23 起默认 | 2.6 早期 |
| 元数据占用 | 较高(每 slab 额外数组) | 内嵌在 struct page | 极低(仅 next/prev 指针) |
| 调试支持 | Red Zone, Poisoning | KASAN 集成, Fuzzer | 基本无 |
| NUMA 支持 | 完善 | 优化良好 | 有限 |
| 典型场景 | 服务器系统 | 通用默认 | 内存 < 64MB 的嵌入式设备 |
| 当前状态 | 已废弃(5.x 后移除) | 活跃开发中 | 保留但较少使用 |
4.1 SLUB 的核心优化
- Freelist 内嵌:SLAB 中 slab 描述符的 freelist 是单独数组,SLUB 将其嵌入到 struct page 的 mapping/freelist 字段中,减少了内存开销
- CPU 亲和:per-CPU page 缓存减少跨节点访问和锁竞争
- 合并小缓存:对于小对象(< 1/8 页大小),kmem_cache_create 时尝试合并到已有相近大小缓存中,减少缓存数量
五、SLAB 着色(Cache Coloring)
5.1 问题背景
现代 CPU 的 L1 Cache 通常采用直接映射或组相联映射。如果多个 slab 中相同偏移的对象恰好映射到同一个 Cache Line,就会产生 Cache Line 冲突(Cache Thrashing),导致性能急剧下降。
5.2 着色原理
着色通过在每个 slab 的起始添加不同大小的偏移(colour_off,通常是 L1 Cache Line 大小),打散相同偏移对象的 Cache 映射:
slab 布局(无着色):
[obj0][obj1][obj2][obj3]...
偏移: 0, 128, 256, 384 → 全部映射到同一 Cache Line
slab 布局(有着色):
[跳过colour_off_0][obj0][obj1][obj2] // offset = 0
[跳过colour_off_1][obj0][obj1][obj2] // offset = 64
[跳过colour_off_2][obj0][obj1][obj2] // offset = 128
→ 不同 slab 的 obj0 映射到不同 Cache Lines
5.3 相关参数
cache_line_size():返回当前 CPU 的 L1 Cache Line 字节数colour_off = cache_line_size()- 最大可用偏移种类数
colour = (end - addr) / colour_off
六、kmem_cache API 实战
6.1 创建和使用自定义缓存
// include/linux/slab.h
// 1. 定义私有结构体
struct my_device_desc {
struct list_head list;
char name[32];
void *reg_base; // 映射的寄存器基地址
dma_addr_t dma_handle; // DMA 地址
spinlock_t lock;
wait_queue_head_t wq;
};
// 2. 全局缓存指针
static struct kmem_cache *my_desc_cache;
// 3. 模块初始化时创建缓存
static int __init my_driver_init(void)
{
my_desc_cache = kmem_cache_create("my_device_desc",
sizeof(struct my_device_desc),
0, // 对齐(0 表示自然对齐)
SLAB_HWCACHE_ALIGN, // 硬件 Cache 对齐
NULL); // 构造函数(可为 NULL)
if (!my_desc_cache)
return -ENOMEM;
pr_info("My driver: kmem_cache created\n");
return 0;
}
// 4. 分配对象
struct my_device_desc *desc = kmem_cache_alloc(my_desc_cache, GFP_KERNEL);
if (!desc)
return -ENOMEM;
// 正常使用 desc...
desc->reg_base = ioremap(REG_BASE, REG_SIZE);
// 5. 释放对象(返回给缓存,不会立即归还给 Buddy System)
kmem_cache_free(my_desc_cache, desc);
// 6. 模块退出时销毁缓存
static void __exit my_driver_exit(void)
{
kmem_cache_destroy(my_desc_cache);
}
6.2 kmem_cache 家族的变体函数
// GFP 标志语义:
GFP_KERNEL // 标准分配,可能睡眠(可回收内存)
GFP_ATOMIC // 原子上下文,不可睡眠(紧急内存池)
GFP_DMA // 分配 ISA DMA 可用内存(< 16MB)
GFP_DMA32 // 分配 32-bit DMA 可用内存(< 4GB)
GFP_USER // 分配给用户空间的内存(需可映射)
GFP_NOFS // 分配时不触发文件系统操作
GFP_NOIO // 分配时不触发 I/O 操作
GFP_NOWAIT // 绝不睡眠(同 GFP_ATOMIC)
6.3 查看 slab 缓存
所有通过 kmem_cache_create() 创建的缓存都会显示在 /proc/slabinfo 中:
$ cat /proc/slabinfo | grep my_device
my_device_desc 256 384 512 16 2 : tunables ...
// active total size perslab 页序
七、通用内存分配:kmalloc 系列
7.1 kmalloc 与 slab 的关系
kmalloc() 本质上是一组预创建的 kmem_cache 集合(kmalloc_caches[]),按 2 的幂次大小分类:
// mm/slab.c 中的内置缓存表(简化)
static struct kmem_cache *kmalloc_caches[NR_KMALLOC_TYPES][KMALLOC_SHIFT_HIGH + 1];
// 典型大小: 32, 64, 96, 128, 192, 256, 384, 512, 768, 1024,
// 1536, 2048, 4096, 8192, 16384, 32768, 65536, 131072
调用 kmalloc(200, GFP_KERNEL) 时:
- 向上取整到匹配的内置 slab 大小(256 字节)
- 调用
kmem_cache_alloc(kmalloc_caches[KMALLOC_NORMAL][9], GFP_KERNEL) - 走标准 slab 分配路径
7.2 选择合适的分配函数
| 场景 | 推荐函数 | 说明 |
|---|---|---|
| 频繁创建同类型对象 | kmem_cache_create + alloc/free | 最佳性能,避免反复初始化 |
| 大小 < 页、不规律分配/释放 | kmalloc/kfree | fallback 到 slab,简单方便 |
| 大小 > 4KB(大对象) | alloc_pages + kmem_cache | 大对象用连续物理页更高效 |
| 需要虚拟地址 + DMA 映射 | dma_alloc_coherent | 确保 DMA Cache 一致性 |
八、调试与问题排查
8.1 SLAB 调试选项
内核配置启用调试选项后,slab 分配器会增加额外的检测机制:
CONFIG_DEBUG_SLAB // 基础调试(启用后性能下降 50%+)
CONFIG_DEBUG_SLAB_LEAK // 内存泄漏追踪
CONFIG_DEBUG_OBJECTS // 对象生命周期追踪
CONFIG_KMEMLEAK // 自动检测未释放内存
CONFIG_KASAN // Kernel Address Sanitizer(检测越界/UAF)
CONFIG_KFENCE // 采样型越界/Use-After-Free 检测
8.2 Red Zone 检测
启用调试时,slab 分配器会在每个对象的前后添加哨兵区(Red Zone),填充固定魔术字。释放时检查哨兵是否被覆盖,可检测越界写操作。
8.3 Poison 检测
释放后的对象用特定字节填充:
0x5a:已释放(POISON_FREE)0x6b:已分配(POISON_ALLOC)
分配前检查槽位是否全为 0x6b,否则触发 Bug。
8.4 使用 KASAN 和 MEMLEAK
# 查看 KMALLOC 分配统计
$ cat /proc/slabinfo | sort -k3 -n | tail -20
# 使用 kmemleak 检测泄漏
$ echo scan > /sys/kernel/debug/kmemleak
$ cat /sys/kernel/debug/kmemleak
# 使用 SLUB_DEBUG 追踪特定缓存
$ echo 1 > /sys/kernel/slab/my_device_desc/trace
九、性能调优实战
9.1 关键可调参数(/sys/kernel/slab/)
/sys/kernel/slab/<cache_name>/
├── limit # array_cache 中最大空闲对象数
├── batchcount # 一次从 slab 转移的对象数
├── object_size # 对象设计大小
├── objects_per_slab # 每个 slab 中的对象数
├── order # slab 大小(2^n 页)
├── partial # partial slab 数量
├── cpu_partial # per-CPU partial 阈值
└── validate # 强制验证 slab 一致性(调试)
9.2 减少内存碎片策略
- 批量操作:将 batchcount 适当调大,减少 slab 与 Buddy System 的交互频率
- 对象对齐:使用 SLAB_HWCACHE_ALIGN 标志保证对象 Cache Line 对齐
- 避免频繁创建/销毁缓存:缓存创建有开销,尽量在模块初始化时一次性创建
- NUMA 亲和:在大规模 NUMA 系统中,缓存分配到正确节点
9.3 监控 slab 使用状况
# 查看单个 slab 详细状态
$ slabtop -o
# 查看前 20 个 slab 缓存(按活跃对象排序)
$ cat /proc/slabinfo | head -5
$ cat /proc/slabinfo | sort -k2 -n | tail -20
# 查看内核内存使用(快速估算 slab 占用)
$ cat /proc/meminfo | grep -i slab
Slab: 3456000 kB
SReclaimable: 2340000 kB // 可回收(dentry/cache)
SUnreclaim: 1116000 kB // 不可回收(自定义缓存)
十、内核驱动开发最佳实践
10.1 对象生命周期管理
驱动中的字符设备和块设备常需要管理数百个实例。为每个实例使用独立的 kmem_cache 不仅提高性能,还便于调试:
// 好的做法:一个设备类型一个缓存
static struct kmem_cache *nic_priv_cache;
struct nic_priv {
struct net_device *ndev;
struct sk_buff *rx_skb[RING_SIZE];
struct sk_buff *tx_skb[RING_SIZE];
dma_addr_t rx_dma[RING_SIZE];
dma_addr_t tx_dma[RING_SIZE];
spinlock_t rx_lock, tx_lock;
unsigned int rx_next, tx_next;
};
// 分配 netdev_priv 区域
struct nic_priv *priv = netdev_priv(ndev);
priv = kmem_cache_zalloc(nic_priv_cache, GFP_KERNEL);
// 好习惯:使用 zalloc 变体避免信息泄露
// kmem_cache_zalloc() 分配后 memset 为 0
10.2 DMA 场景下的特殊处理
当驱动需要与 DMA 配合使用时,应结合 dma_alloc_coherent() 分配 DMA 安全缓冲区,配合 kmem_cache_alloc() 分配描述符:
struct dma_desc {
u32 src_addr;
u32 dst_addr;
u32 ctrl;
struct list_head list;
};
struct dma_priv {
struct kmem_cache *desc_cache; // 描述符缓存
dma_addr_t desc_ring_dma; // DMA 环形描述符总线地址
struct dma_desc *desc_ring; // 虚拟地址
};
static int setup_dma_ring(struct dma_priv *priv, int count)
{
// 分配 DMA 安全的环形缓冲区
priv->desc_ring = dma_alloc_coherent(dev,
count * sizeof(struct dma_desc),
&priv->desc_ring_dma, GFP_KERNEL);
if (!priv->desc_ring)
return -ENOMEM;
// 分配单个描述符使用 slab 缓存
priv->desc_cache = kmem_cache_create("dma_desc_cache",
sizeof(struct dma_desc), 0,
SLAB_HWCACHE_ALIGN, NULL);
INIT_LIST_HEAD(&priv->free_list);
for (int i = 0; i < count; i++) {
struct dma_desc *d = kmem_cache_alloc(priv->desc_cache, GFP_KERNEL);
list_add(&d->list, &priv->free_list);
}
return 0;
}
10.3 RCU 延迟释放
对于需要 RCU 保护的 slab 对象,使用 kfree_rcu() 延迟释放,避免 Read Side 竞态:
struct rcu_protected_obj {
struct rcu_head rcu;
// 数据字段...
};
struct rcu_protected_obj *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
// ... 读操作 (rcu_read_lock)
// 释放时延迟到宽限期结束
kfree_rcu(obj, rcu);
10.4 SLUB 与大页(Huge Page)优化
Linux 5.x 中,SLUB 开始支持 Transparent Huge Pages(THP)优化,将 slab 透明地映射到 2MB 大页:
// 创建时可请求大页(需硬件支持)
kmem_cache_create("my_large_cache", 64 * 1024, 0,
SLAB_HWCACHE_ALIGN | SLAB_PANIC, NULL);
// 监控
$ cat /sys/kernel/slab/my_large_cache/flags
$ cat /sys/kernel/slab/my_large_cache/oo // 阶数详情
十一、总结
Slab Allocator 是 Linux 内核内存管理的关键组件,理解其工作原理对驱动开发者和内核开发者至关重要:
- 架构:kmem_cache 管理 Per-CPU 快速路径 + NUMA 节点级二级缓存 + Buddy System 慢路径
- 优化:着色优化 Cache Line 利用、per-CPU 无锁快速分配、freelist 内嵌
- 变体:SLUB 是默认首选(简单高效),SLOB 仅用于极端嵌入式环境
- API:高频同类型对象用 kmem_cache_create/alloc/free,通用分配用 kmalloc/kfree
- 调试:KASAN + KFENCE + SLUB_DEBUG + /proc/slabinfo + slabtop
- 监控:重点关注 SUnreclaim 增长趋势和特定缓存的 active/total 比率
通过合理使用 slab 分配器,可以在驱动开发中实现接近 O(1) 的分配速度、有效控制内存碎片,并利用 NUMA 亲和性优化多核系统的内存访问性能。

发表评论 取消回复