一、目标
本文希望你理解Slab分配器为什么存在,它如何解决伙伴系统的内部碎片问题,以及在真实项目中如何利用Slab来高效管理内核对象的生命周期。
具体聚焦于实战上:所有例子量化都基于真实内核版本(Linux 5.15/LTS和6.6/LTS);proc文件系统和sysfs显示的联系仅限于Linux内核专有文件。
为了后续阅读有知识连接,定义一些基础概念:
- 地址空间(virtual address space):进程看到的虚拟地址范围。
- 页表(page table):虚拟地址到物理地址的映射机制。
- 物理页框(physical page frame):实际的内存单元,由伙伴系统管理。
- Slab:一个或多个连续的物理页面,被划分成多个相同大小的对象,是伙伴系统的上层分配器。
二、问题背景:为什么需要Slab分配器
2.1 伙伴系统的必然制约:页级粒度分配的局限
伙伴系统(Buddy System)是内核物理内存管理的基石,但它存在一个必然的局限:分配粒度只能是2的幂次页面大小(4KB、8KB、16KB……)。
伙伴系统无法高效处理小于一页的内存需求。例如索引节点结构体只有48字节,若仍按分配一页4KB来分配,那么内存利用率仅为1.17%。内核中大量高频分配的小对象(如task_struct、dentry、inode等)都面临这种浪费问题。
2.2 内部碎片问题
100字节的对象请求4KB的页,利用率仅为2.44%。因此需要设计一种针对小于一页的内存需求的高效分配器,在伙伴系统分配的页面基础上,在页面内部分配更小块。
因此Slab分配器应运而生,它将一个或多个连续的物理页面作为一个Slab(也可以称为内存页槽),然后将这块内存切割成多个相同大小的对象(实体),本质上是一种面向特定类型的对象型内存分配器。
三、Slab分配器架构演进
3.1 三代实现对比
Slab分配器经历了三代实现:
- SLAB:经典实现(Jeff Bonwick, 1994),最初为Solaris设计,Linux移植后长期使用,但复杂度高,管理开销大,Linux 5.15后已废弃。
- SLUB:简化实现(Christoph Lameter, 2007),从Linux 2.6.23起引入,现在是默认分配器,代码更简洁,管理开销低。
- SLOB:极简实现,面向嵌入式小内存系统,使用首次适应算法,管理开销最小但性能较差。
| 特性 | SLAB | SLUB(默认) | SLOB |
|---|---|---|---|
| 复杂度 | 复杂(已废弃) | 简洁 | 最简单 |
| 用途 | 通用 | 通用(默认) | 小内存/嵌入式 |
| 管理开销 | 大 | 小 | 最小 |
| 复杂对象处理 | 支持 | 最佳 | 差 |
3.2 当前Linux内核中的默认选择
Linux 5.15及以后版本的默认分配器为SLUB。它解决了SLAB的核心问题:
- 去除了复杂的每Slab本地缓存管理
- 简化了Slab的管理逻辑
- 在NUMA系统上表现更优
- 内存碎片问题通过"合并"策略缓解
四、核心数据结构
4.1 三层结构关系
Slab分配器采用三层数据结构:kmem_cache、slab和object。
最上层是kmem_cache,它对应于一种对象类型的元数据管理,封装了对象的大小、对齐规则、构造/释放函数等。每个kmem_cache被串联在一个双向链表上,通过cache_chain连接。
中间层是slab对象:一个slab管理结构对应一块连续的物理内存区域(通常由一到几个物理页面组成),维护该slab上对象的分配状态。
最底层是object:实际可分配的内存单元,也是用户最终使用的内存块。
4.2 kmem_cache结构体关键字段(Linux 6.6)
struct kmem_cache {
unsigned int object_size; // 对象实际大小
unsigned int size; // 对象对齐后大小
unsigned int align; // 对象对齐参数
slab_flags_t flags; // 标志位
const char *name; // 缓存名称
ctor_t ctor; // 构造函数
struct list_head slabs_full; // 已全部分配的Slab链表
struct list_head slabs_partial; // 部分分配的Slab链表
struct list_head slabs_free; // 完全空闲的Slab链表
unsigned int num; // 每个Slab包含的对象数
unsigned int gfporder; // 创建Slab时请求的页数(2的n次方)
struct array_cache __percpu *cpu_cache; // 每CPU对象暂存
};
4.3 Slab管理结构(SLUB实现中无显式slab结构体)
在SLUB实现中,slab信息直接嵌入在页面结构的union中,每个slab(即一组页面)通过一个"slab page"来管理:
// SLUB: slab信息嵌入struct page
struct page {
// ... 标志位包含 Slab 相关的状态信息
struct {
struct { struct list_head *next, *prev; } lru;
struct kmem_cache *slab_cache;
void *freelist; // 空闲对象链表头
union {
struct { unsigned inuse:16, objects:16; };
struct { void *freelist; unsigned long counters; };
};
};
};
五、分配流程
当内核请求分配一个对象时(例如调用kmem_cache_alloc),流程如下:
- 查找缓存:首先在cache_chain中查找对应的kmem_cache(或由kmalloc根据大小查表找到)。
- 查找每CPU缓存:检查当前CPU的array_cache中有无空闲对象,若有则直接返回。
- 查找每节点缓存:在NUMA系统上,若CPU本地缓存为空,则尝试从同节点其他CPU缓存中获取。
- 查找Slab链表:从slabs_partial链表中取出一个Slab,从其空闲对象列表中取出一个对象。
- 申请新Slab:如果slabs_partial和空闲链表都没有可用对象,则通过伙伴系统(alloc_pages)申请新的页面,分割成对象后加入slabs_partial。
这个流程是内核高频分配的核心路径,通过维护对象空闲链表,可以快速定位可用对象,避免遍历开销。
六、释放流程
当对象不再需要时(例如kmem_cache_free),它不会立即归还给伙伴系统,而是被放回当前Slab的空闲对象链表中,以便下次分配时直接重用。
这就是Slab分配器的核心优势:快速重用——无需重新初始化,仅做链表头插入操作。
当某个Slab上所有对象都被释放后,它才会从kmem_cache的Slab列表中移出,归还给伙伴系统。释放过程中可能触发"Slab缩减"(shrink)逻辑,当系统内存压力大时会回收空闲Slab。Slab的状态转移规则为:全部分配 → slabs_full;部分分配 → slabs_partial;全部释放 → slabs_free → 归还伙伴系统。
七、着色(Colouring)机制
着色机制的引入是为了利用CPU缓存行(Cache Line)的特点。CPU缓存行通常64字节,当多个Slab中相同索引的对象被映射到同一缓存行时,会产生缓存行伪共享(False Sharing),导致不同CPU核心访问同一缓存行时产生缓存一致性流量。
着色的做法是在Slab起始处加入一定字节的偏移(colour offset),使得不同Slab中的相同索引对象映射到不同的缓存行位置,从而减少冲突。最大偏移量由 cache_line_size / align 决定。
结构体中 colouroff 字段记录了当前Slab的着色偏移,每次新建一个Slab时,偏移量递增(colour += cache_line_size),到达 colour_off 上限后回绕到零。
八、CPU本地缓存(array_cache)
在多核系统中,如果所有CPU都访问同一个Slab的数据结构来分配/释放对象,会产生严重的锁竞争。Slab分配器通过array_cache机制为每CPU维护一个本地空闲对象缓存来消除锁争用。
struct array_cache {
unsigned int avail; // 空闲对象数量(可分配)
unsigned int limit; // 最大对象数
unsigned int touched; // 最近是否被使用
void *entry[0]; // 柔性数组:空闲对象指针数组
};
分配路径:首先从array_cache中弹出(pop)一个空闲对象。如果array_cache为空(avail == 0),则从Slab中一次性填充batchcount个对象到array_cache。
释放路径:首先将对象推入(push)array_cache。如果array_cache已满(avail >= limit),则从array_cache中一次性返还batchcount个对象给Slab。
这种设计使得在SMP系统上大多数分配/释放操作无需任何锁即可完成,极大地提升了多核并发性能。batchcount的取值经过调优,通常为1或cache_line能容纳的对象数量。
九、kmalloc系列API
kmalloc是内核中最常用的通用内存分配函数,它是基于Slab分配器的封装,用于分配从几十字节到数MB的内存块。
9.1 核心API
void *kmalloc(size_t size, gfp_t flags); // 分配,内存不一定清零
void *kzalloc(size_t size, gfp_t flags); // 分配并清零
void *kmalloc_array(size_t n, size_t size, gfp_t flags); // 分配数组
void *kcalloc(size_t n, size_t size, gfp_t flags); // 分配并清零数组
void *krealloc(const void *p, size_t new_size, gfp_t flags); // 重新分配
void kfree(const void *objp); // 释放
9.2 内部实现原理
kmalloc内部通过预定义的一组"size cache"来快速找到匹配的kmem_cache:
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
if (size > KMALLOC_MAX_SIZE)
return NULL;
if (size <= KMALLOC_MAX_CACHE_SIZE)
return __kmalloc_cache(size, flags); // 从预定义缓存数组查找
return kmalloc_large(size, flags); // 大内存走伙伴系统
}
预定义缓存的size范围通常从32字节到8KB(具体取决于配置和CPU架构),以2的幂次为主。当请求大小超过8KB时,kmalloc直接调用伙伴系统分配整页内存。
kfree释放时,通过对象地址找到其所属的slab page(通过virt_to_slab或page_address),然后根据情况归还给array_cache或直接归还给Slab。
十、专用缓存API(kmem_cache)
除了kmalloc,内核中很多子系统会为自己的专用对象类型创建独立的缓存,这在需要高频分配同一类型对象的场景下效率极高。
// 创建专用缓存
struct kmem_cache *kmem_cache_create(const char *name, unsigned int size,
unsigned int align, slab_flags_t flags,
void (*ctor)(void *));
// 从缓存分配对象
void *kmem_cache_alloc(struct kmem_cache *cache, gfp_t flags);
// 释放对象回缓存
void kmem_cache_free(struct kmem_cache *cache, void *obj);
// 销毁缓存
void kmem_cache_destroy(struct kmem_cache *cache);
典型使用场景:
- task_struct:task_struct_cachep,每次fork/exec时分配
- dentry:dentry_cache,文件路径解析时高频分配
- inode:inode_cache,文件系统inode缓存
- sk_buff:skbuff_head_cache,网络数据包管理
- signal_struct、files_struct等进程相关结构
- 驱动中自定义的数据结构
十一、Slab调试与监控
11.1 /proc/slabinfo
通过读取/proc/slabinfo可以获取当前系统所有Slab缓存的运行时统计:
cat /proc/slabinfo
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : ...
dentry 28512 31071 192 21 1 : ...
inode_cache 18652 19053 656 6 2 : ...
task_struct 234 320 7360 1 4 : ...
各列含义:缓存名、已分配对象数、总对象数、单对象大小、每Slab对象数、每Slab页面数。
11.2 Slabtop工具
slabtop命令提供类似top的实时Slab内存使用视图,可按不同字段排序观察。
11.3 KASAN(内核地址消毒剂)
KASAN在编译时插入检查代码,在每个Slab对象周围设置"红区"(Red Zone),用来检测缓冲区越界和使用已释放内存(Use-After-Free)等严重错误。通过CONFIG_KASAN开启,主要用于内核开发和测试阶段。
11.4 Slab合并调试(SLUB debug)
通过slub_debug内核参数开启Slub调试功能:
slub_debug=FZP # F=启用对象分配追踪, Z=红区检测, P=启用毒化
常见应用场景:追踪某个对象类型的内存泄漏、检测重复释放。通过echo <pid> > /sys/kernel/slab/<cache>/alloc_from等接口结合ftrace可进一步分析。
十二、性能优化实践
12.1 选择合适的分配器参数
- 对于频繁分配的小对象(<1KB),使用专用kmem_cache而非kmalloc,避免碎片化
- 合理设置ctor/dtor避免重复初始化开销
- 分配标志GFP_KERNEL用于可睡眠上下文,GFP_ATOMIC用于中断/不可睡眠上下文
12.2 避免常见陷阱
- 不要在中断上下文中使用GFP_KERNEL分配(可能引发睡眠)
- 避免重复分配/释放相同大小的对象(应复用或预分配)
- 不要将kmalloc分配的内存通过free_pages归还(必须用kfree)
- SLUB的freelist指针存储在对象本身内存中,释放后不要立即访问
十三、总结
Slab分配器作为Linux内核内存管理的核心组件,通过对象复用、每CPU缓存、着色技术等机制,在伙伴系统之上构建了一层高效的小对象分配层。理解Slab分配器的工作机制,对于内核开发、驱动开发以及性能优化都有着重要意义。
本文覆盖了Slab分配器的架构设计、数据结构、分配/释放流程、缓存优化、kmalloc接口、调试工具和性能优化的全链路知识,希望能帮助读者建立对内核内存管理的完整认知。

发表评论 取消回复