Slab分配器是Linux内核中用于高效管理内核对象内存分配的核心机制。我第一次接触Slab是在调试一个内核模块的内存泄漏问题时,发现常规的kmalloc()/kfree()无法满足频繁创建销毁相同类型对象的需求。Slab通过预分配和缓存策略,显著减少了内存碎片和分配开销。
Slab的核心思想其实很像现实生活中的"物件回收站":当我们频繁使用某种固定大小的对象时,与其每次都新建再销毁,不如把用完的对象放在一个专门区域,下次需要时直接取用。这种思路在内核中尤为重要,因为像task_struct、inode等关键数据结构会被频繁创建和释放。
Slab的初始化发生在内核启动的早期阶段,具体在start_kernel() -> mm_init() -> kmem_cache_init()中。这个时机选择很有讲究 - 必须在内存管理系统基本就绪后,但在其他子系统开始大量创建对象之前完成初始化。
c复制// mm/slab.c
void __init kmem_cache_init(void)
{
// 初始化阶段划分
enum {
DOWN, // 准备阶段
PARTIAL, // 部分初始化
PARTIAL_NODE,
UP, // 运行阶段
FULL // 完全初始化
} stage = DOWN;
// 分阶段初始化
while (stage != FULL) {
switch (stage) {
case DOWN:
create_boot_cache(kmem_cache, "kmem_cache",
sizeof(struct kmem_cache), 0);
stage = PARTIAL;
break;
// 其他阶段处理...
}
}
}
Slab的核心是kmem_cache结构体,它相当于特定类型对象的"模具"。初始化时会先创建kmem_cache的cache(是的,这是个自举问题),然后逐步建立其他关键cache:
注意:这个自举过程非常精妙 - 必须先有kmem_cache才能分配kmem_cache,所以最初的几个cache是通过特殊方式手动构建的。
Slab初始化时有个容易被忽视但很重要的细节 - 缓存着色(cache coloring)。这是为了解决硬件缓存冲突问题而设计的:
c复制// mm/slab.c
static size_t calculate_slab_order(struct kmem_cache *cachep,
size_t size, size_t align, unsigned long flags)
{
// 计算最佳slab大小时考虑缓存着色
unsigned long offslab_limit;
size_t left_over = 0;
int gfporder;
for (gfporder = 0; gfporder <= KMALLOC_MAX_ORDER; gfporder++) {
// ...计算过程...
cachep->colour = left_over / cachep->colour_off;
// ...
}
}
缓存着色的本质是通过在对象间插入不同大小的填充(colour_off),使得不同slab中相同偏移的对象不会在硬件缓存中相互冲突。这就像停车场里错开车位,避免多辆车同时进出造成的拥堵。
当内核代码调用kmem_cache_alloc()时,理想情况下会走快速路径:
c复制// mm/slab.c
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
void *ret = ____cache_alloc(cachep, flags);
// 调试相关代码省略...
return ret;
}
static inline void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
void *objp;
struct array_cache *ac;
// 获取当前CPU的缓存
ac = cpu_cache_get(cachep);
if (likely(ac->avail)) {
ac->touched = 1;
objp = ac->entry[--ac->avail];
return objp;
}
// 慢路径处理...
}
当per-CPU缓存耗尽时,会进入慢路径:
这个过程中最耗时的部分是可能需要新建slab,这涉及到从伙伴系统申请连续页框:
c复制// mm/slab.c
static void *cache_alloc_refill(struct kmem_cache *cachep, gfp_t flags)
{
// 尝试从shared_array补充
batchcount = ac->batchcount;
if (shared_array) {
// ...从共享缓存获取...
}
// 从partial slab补充
while (batchcount > 0) {
struct page *page;
// 获取partial slab
page = get_first_slab(cachep, node, false);
if (!page)
goto must_grow;
// 从page中获取对象
// ...
batchcount--;
}
must_grow:
// 必须增长slab
if (!grow_slab(cachep, flags))
return NULL;
// 重试分配
return ____cache_alloc(cachep, flags);
}
Slab设计中最精妙的部分就是锁的运用:
这种分层锁设计大幅减少了竞争。我在实际性能调优中发现,当系统有大量内存分配时,合理调整batchcount(每次补充的对象数)可以显著减少锁争用。
kmem_cache_free()的执行路径与alloc相反:
c复制// mm/slab.c
void kmem_cache_free(struct kmem_cache *cachep, void *objp)
{
// 调试检查省略...
____cache_free(cachep, objp, _RET_IP_);
}
static inline void ____cache_free(struct kmem_cache *cachep, void *objp,
unsigned long caller)
{
struct array_cache *ac = cpu_cache_get(cachep);
// 如果per-CPU缓存有空位
if (likely(ac->avail < ac->limit)) {
ac->entry[ac->avail++] = objp;
return;
}
// 否则批量释放
cache_flusharray(cachep, ac);
ac->entry[ac->avail++] = objp;
}
Slab的内存回收主要通过以下机制协同工作:
一个实际案例:我们在生产环境发现,某些很少使用的内核对象缓存占据了大量内存。通过注册shrinker回调,可以在内存紧张时主动释放这些缓存:
c复制// 示例shrinker实现
static unsigned long my_shrink(struct shrinker *shrink,
struct shrink_control *sc)
{
struct kmem_cache *cachep = container_of(shrink, struct kmem_cache, shrinker);
unsigned long freed = 0;
if (sc->nr_to_scan) {
// 尝试释放slab
freed = __kmem_cache_shrink(cachep);
}
return freed;
}
// 注册shrinker
cachep->shrinker.shrink = my_shrink;
cachep->shrinker.seeks = DEFAULT_SEEKS;
register_shrinker(&cachep->shrinker);
假设我们需要频繁分配一种自定义结构体:
c复制struct my_object {
atomic_t refcnt;
unsigned long data[16];
struct list_head list;
};
// 创建专用缓存
static struct kmem_cache *my_cachep;
void init_my_cache(void)
{
my_cachep = kmem_cache_create("my_object_cache",
sizeof(struct my_object),
0, // 对齐要求
SLAB_HWCACHE_ALIGN | SLAB_PANIC, // 标志
NULL); // 构造函数
if (!my_cachep)
panic("Failed to create my_object cache\n");
}
// 使用示例
struct my_object *obj = kmem_cache_alloc(my_cachep, GFP_KERNEL);
if (!obj)
return -ENOMEM;
// 初始化对象...
Slab提供了丰富的调试选项,可以通过slub_debug内核参数启用:
code复制slub_debug=FPUZ // 启用全部调试功能
常用调试技巧包括:
我在调试一个内存损坏问题时,通过以下方式找到了问题根源:
sh复制echo 1 > /sys/kernel/slab/kmalloc-128/trace
dmesg -w
这会在每次分配/释放时打印调用栈,最终发现是一个驱动在释放后仍在使用内存。
通过/proc/slabinfo可以查看所有缓存的统计信息,重点关注:
优化案例:我们发现某个缓存的对象利用率很低(objperslab=8但平均active_objs=2),通过调整构造函数和对齐参数,将objperslab提升到16,内存使用减少了40%。
在NUMA系统中,Slab默认会为每个节点创建缓存。我们可以通过以下方式优化跨节点访问:
c复制// 在分配时指定节点
obj = kmem_cache_alloc_node(my_cachep, GFP_KERNEL, node_id);
// 或者在创建缓存时设置标志
my_cachep = kmem_cache_create(..., SLAB_NUMA);
实际测试显示,在4节点NUMA系统上,正确使用节点感知分配可以将性能提升25%以上。
Slab内存泄漏的典型表现是某个缓存的active_objs持续增长。诊断步骤:
sh复制# 跟踪kmem_cache_alloc/free调用
echo 'kmem_cache_alloc' > /sys/kernel/debug/tracing/set_ftrace_filter
echo 'kmem_cache_free' >> /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
cat /sys/kernel/debug/tracing/trace_pipe
当遇到内核崩溃且指向slab错误时,可能的原因包括:
一个实用的技巧是在对象中嵌入魔术字:
c复制struct my_object {
#define MY_OBJ_MAGIC 0x4D794F62
unsigned int magic;
// 其他字段...
};
// 在构造函数中设置
static void my_ctor(void *obj)
{
struct my_object *o = obj;
o->magic = MY_OBJ_MAGIC;
}
// 在使用前验证
static inline bool is_valid_obj(struct my_object *o)
{
return o && o->magic == MY_OBJ_MAGIC;
}
现代内核主要使用SLUB(非统一内存块)作为默认分配器,它相比传统SLAB有诸多改进:
而SLOB(简单列表块)则用于内存受限的系统(如嵌入式设备)。选择策略:
在最近的一个嵌入式项目中,我们将默认分配器从SLOB切换到SLUB后,虽然内存开销增加了约5%,但性能提升了近3倍。