3. Slab核心数据结构:kmem_cache、slab、object布局、着色与硬件缓存行对齐
Slab分配器是Linux内核中用于管理固定大小对象(如task_struct、inode)的高效内存分配机制。其核心设计围绕三个层次的数据结构展开:kmem_cache(缓存描述符)、slab(物理页帧容器)和object(实际分配对象)。本章深入剖析这三者的布局关系,并重点解释着色(coloring)技术如何与硬件缓存行对齐协同工作,以最大化CPU缓存命中率。
3.1 三层数据结构总览
Slab分配器采用“缓存→ slab → 对象”的三级架构。每个kmem_cache管理一组相同大小的对象,内部维护多个slab,每个slab占用一个或多个连续物理页帧,并在其上划分出等长的对象槽位。
3.2 kmem_cache:缓存描述符
kmem_cache是slab分配器的顶层管理结构,每个缓存对应一种对象类型。内核通过kmem_cache_create()创建,通过kmem_cache_destroy()销毁。
| 字段 | 类型 | 说明 |
|---|---|---|
name |
const char * |
缓存名称,如"task_struct"、"mm_struct" |
object_size |
unsigned int |
每个对象的大小(字节),已对齐 |
align |
unsigned int |
对象对齐要求,通常为硬件缓存行大小(64B) |
size |
unsigned int |
实际分配大小(含元数据、填充) |
num |
unsigned int |
每个slab可容纳的对象数量 |
gfporder |
unsigned int |
每个slab占用的页阶(2^gfporder页) |
colour |
unsigned int |
着色范围(以缓存行为单位) |
colour_off |
unsigned int |
着色偏移步长(通常为缓存行大小) |
slabp_cache |
struct kmem_cache * |
用于分配slab描述符的缓存(大slab时独立分配) |
partial |
struct list_head |
部分空闲slab链表 |
full |
struct list_head |
完全分配slab链表 |
colour和colour_off是着色机制的核心参数。内核在创建缓存时根据对象大小和页帧大小计算着色范围,确保不同slab中相同索引的对象映射到不同的CPU缓存行。
3.3 slab描述符与对象布局
每个slab对应一个或多个连续物理页帧。slab描述符(struct slab)可以存储在页帧内部(小slab)或独立分配(大slab)。对象在slab内连续排列,对象之间可能因对齐要求存在填充字节。
/* 内核源码:mm/slab.h 中的 slab 结构(简化) */
struct slab {
struct list_head slab_list; /* 挂入 partial/full 链表 */
unsigned long colouroff; /* 当前slab的着色偏移(字节) */
void *s_mem; /* 第一个对象的起始地址 */
unsigned int inuse; /* 已分配对象数 */
unsigned int free; /* 空闲对象数 */
unsigned int active; /* 活跃对象数(调试用) */
struct kmem_cache *slab_cache; /* 所属缓存 */
/* ... 其他字段 */
};
对象布局遵循以下规则:
- 连续排列:对象在slab内从
s_mem开始连续存放,间距为size(已对齐)。 - 空闲链表:空闲对象通过自身前几个字节构成单向链表,指向下一个空闲对象索引。
- 着色偏移:每个slab的
colouroff不同,使得相同索引的对象在不同slab中起始地址不同。
3.4 着色(Coloring)技术原理
着色是slab分配器为减少CPU缓存行冲突而引入的优化技术。其核心思想是:让不同slab中相同索引的对象起始地址错开一定偏移量,避免它们映射到相同的CPU缓存行。
3.4.1 为什么需要着色?
假设两个slab的物理页帧起始地址相差恰好是缓存行大小的整数倍(如64B、128B),那么两个slab中相同偏移的对象会竞争同一缓存行。频繁访问这两个对象会导致缓存行颠簸(cache line bouncing),性能急剧下降。
3.4.2 着色计算过程
- 确定着色范围:
colour = (slab_size - object_size * num) / colour_off,即slab内剩余空间能容纳多少个着色步长。 - 分配着色偏移:每个新创建的slab依次使用
colouroff = (slab_index % colour) * colour_off。 - 调整对象起始地址:
s_mem = slab_start + colouroff,对象从偏移后的位置开始排列。
| 参数 | 示例值 | 说明 |
|---|---|---|
| slab_size | 4096 B(4KB页) | 单个slab占用的物理页帧总大小 |
| object_size | 256 B | 每个对象大小(已对齐) |
| num | 15 | 每个slab可容纳对象数(4096/256=16,预留1个对象空间给着色) |
| colour_off | 64 B | 着色步长,等于L1缓存行大小 |
| colour | 16 | 着色范围:(4096 - 256*15) / 64 = 16 |
| colouroff | 0, 64, 128, ..., 960 B | 16种可能的着色偏移值 |
colour参数严格限制偏移范围。
3.5 硬件缓存行对齐
现代CPU的L1/L2缓存行大小通常为64字节。slab分配器在对象对齐和着色偏移上都以缓存行为单位,确保:
- 对象起始地址:按
align(至少64B)对齐,避免对象跨缓存行。 - 着色偏移:是
colour_off(64B)的整数倍,保证偏移后的对象仍对齐缓存行。 - 对象大小:向上取整到缓存行大小的倍数,防止对象尾部与下一个对象头部共享缓存行。
/* 内核源码:mm/slab.c 中的对齐与着色计算(简化) */
static int calculate_slab_order(struct kmem_cache *cachep,
size_t size, size_t align,
unsigned long flags)
{
unsigned long offslab_limit;
unsigned int gfporder;
unsigned int num;
size_t left_over;
for (gfporder = 0; gfporder <= MAX_GFP_ORDER; gfporder++) {
unsigned int num;
size_t slab_size = PAGE_SIZE << gfporder;
/* 计算每个slab可容纳的对象数 */
num = (slab_size - sizeof(struct slab)) / (size + align);
num = min(num, (slab_size - sizeof(struct slab)) / size);
/* 计算剩余空间,用于着色 */
left_over = slab_size - num * size;
/* 设置着色参数 */
cachep->colour = left_over / cachep->colour_off;
cachep->colour_off = cache_line_size(); /* 64B */
cachep->num = num;
cachep->gfporder = gfporder;
if (left_over * 2 <= slab_size)
break; /* 剩余空间不超过一半,接受此阶 */
}
return 0;
}
3.6 着色与缓存行对齐的协同效果
着色与缓存行对齐共同作用,带来以下性能收益:
| 场景 | 无着色/无对齐 | 有着色+对齐 | 性能提升 |
|---|---|---|---|
| 多核访问相同对象索引 | 缓存行颠簸,大量cache miss | 对象分散在不同缓存行,cache hit率高 | 3x ~ 10x |
| 顺序遍历slab对象 | 跨缓存行访问,预取失效 | 对象对齐缓存行,硬件预取有效 | 1.5x ~ 2x |
| 对象频繁分配释放 | 伪共享导致缓存一致性协议开销 | 不同slab对象隔离,伪共享消除 | 2x ~ 5x |