本文目的在于分析Linux内存管理机制的slab分配器。内核版本为2.6.31。
1. SLAB分配器
内核需要经常分配内存,我们在内核中最常用的分配内存的方式就是kmalloc了。前面讲过的伙伴系统只支持按页分配内存,但这个单位太大了,有时候我们并不需要这么大的内存,比如我想申请128字节的空间,如果直接使用伙伴系统则需分配4KB的一整页,这显然是浪费。
slab分配器将页拆分为更小的单位来管理,来满足小于一页的内存需求。它将连续的几个页划分出更小的部分拿来分配相同类型的内存对象,对象的位置尽量做到某种对齐要求(如处理器的L1高速缓存行对齐)以便不对CPU高速缓存和TLB带来显著影响。slab把一类对象统一管理,例如,划出两个页的内存,将其分成n小份用来分配一类对象的实例,同时slab也维护一些通用的对象,用来供kmalloc使用。
提供小块内存并不是slab分配器的唯一任务,由于结构上的特点,它也用作一个缓存,主要针对经常分配内存并释放的对象。slab分配器将释放的内存保存在一个内部列表中,并不马上返还给伙伴系统。在请求为该类对象分配一个实例时,会使用最近释放的内存块,这样就不必触及伙伴系统以缩短分配消耗的时间,另外由于该内存块是“新”的,其驻留在CPU高速缓存的概率也会较高。
在下面的代码分析中,你会看到slab的这些“手段”是如何实现的。
先说一下“slab着色(slab coloring)”机制,是用来防止高速缓存冲突的:相同类型的slab、对象很有可能被保存到相同的CPU cache的缓存行中,经常使用的对象被放到CPU cache中,这当然使我们想要的,但如果两个不同的对象每次都被放到相同的缓存行中,那交替的读取这两个对象,会导致缓存行的内容不断的被更新,也就无法体现缓存的好处了。不过我的内核版本是2.6.31,已经没有slab着色机制了,所以大家看到coloring什么的就不要再纠结了。
内核中还提供了slob和slub两种替代品,因为slab的结构很复杂,并且需要使用太多额外的空间去管理一类对象。关于这两个替代品我就不多说了,slub在性能和缓存占用方面都要优于slab,并且在一些嵌入式设备上会看到内核使用slub。
2. SLAB分配器的实现
2.1 SLAB分配器初始化
系统启动时slab分配器初始化的函数为kmem_cache_init()和kmem_cache_init_late()。函数名中的“cache”是指slab分配器,我们也称作slab缓存,注意,它与CPU中的高速缓存没有关系,但上面讲到slab利用了高速缓存的特性。下面的分析中,我会使用“slab缓存”这种叫法。
kmem_cache_init()函数为分配slab对象准备最基本的环境。在分析这个函数之前,我们先看一个内核中创建slab缓存的例子:
static struct kmem_cache *nf_conntrack_cachep __read_mostly;
nf_conntrack_cachep= kmem_cache_create("nf_conntrack",
sizeof(struct nf_conn),
0, SLAB_DESTROY_BY_RCU, NULL);
上面的代码在内核协议栈的链接跟踪模块中创建struct nf_conn的slab缓存,这个slab缓存用于分配struct nf_conn对象。
当想申请一个struct nf_conn结构的对象时,使用kmem_cache_alloc()函数进行分配。
struct nf_conn *ct;
ct =kmem_cache_alloc(nf_conntrack_cachep, gfp);
可以看到,创建和使用slab缓存是非常方便的。在/proc/slabinfo文件中可以看到内核中所创建的slab缓存。
kmem_cache_create()用于创建一个slab缓存,在哪里创建呢,kmem_cache_init()函数的工作就是初始化用于创建slab缓存的slab缓存。也就是说,一个slab缓存也应该是通过函数kmem_cache_create()来创建的,但是很容易想到,内核中的第一个slab缓存肯定不能通过这个函数来创建,在内核中使用一个编译时生成的静态变量作为第一个slab缓存。
slab缓存用一个struct kmem_cache结构来描述。内核中的第一个slab缓存定义如下:
static struct kmem_cache cache_cache = {
.batchcount = 1,
.limit = BOOT_CPUCACHE_ENTRIES,
.shared = 1,
.buffer_size = sizeof(struct kmem_cache),
.name = "kmem_cache",
};
系统中所有的slab缓存都被放入一个全局链表中:
staticstruct list_head cache_chain;
接下来我们来分析kmem_cache_init()函数,它的实现分为下面几个步骤:
1. 创建cache_cache,它将用于分配系统中除了它自身以外的所有slab缓存的kmem_cache对象。
2. 创建可以分配struct arraycache_init和struct kmem_list3的slab cache。先创建这两个通用cache的原因后面会讲到,他们也供kmalloc使用。这两个cache是通过kmem_cache_create()创建的,因为cache_cache已经可用了。这一步之后,将slab_early_init置为0。
3. 使用kmem_cache_create()创建剩余的通用cache,“剩余”是相对第2步中的两个cache,他们都是可以供kmalloc使用的。这些通用cache的名字和对象大小见下面表格。
4. 为cache_cache.array[]和malloc_sizes[INDEX_AC].cs_cachep->array[]重新分配空间。
5. 为cache_cache.nodelists[]、malloc_sizes[INDEX_AC].cs_cachep-> nodelists[]和malloc_sizes[INDEX_L3].cs_cachep-> nodelists[]重新分配空间。
通用cache的名字和对象大小用两个数组来记录:malloc_sizes[]和cache_names[]。
cache_names[] |
malloc_sizes[] |
size-32 |
32 |
size-64 |
64 |
size-96 |
96 |
size-128 |
128 |
…… |
…… |
NULL |