一、Slab系统理论
slab技术是一种“对象分配器”,slab技术在这样一种背景下出现:例如在笔者阅读过的Linux1.2源码中,内核中如果需要通过动态方式分配一个对象,就调用kmalloc函数完成分配。kmalloc函数通过伙伴系统直接分配物理页面后,在页面内部进行对象划分,从而获得对象。但是这样的管理显然无法实现复杂结构体的管理或者对象划分的复杂特性(例如硬件cache特性的利用)。在这样的背景需求下,能够管理复杂结构体内存分配、能够支持复杂特性的slab内存分配器出现了。slab内存分配器用于在内核运行过程中动态分配、动态释放对象(包含通用对象和专用对象),并且根据硬件cache特性对动态分配、释放过程进行优化。
1.1 slab系统结构
首先从一个粗略的角度出发观察slab系统。一个处于运行状态的slab系统具备3层结构:cache、slab、object。object指动态分配的对象,是slab系统管理的基本单元,也是slab系统的操作对象。一个动态分配出的对象必定处于一个或一组连续的物理页面中,我们称用于包含一个或多个相同类型对象的一个或多个连续物理页面为一个slab结构。为了管理多个包含相同类型对象的slab结构,我们使用cache结构来完成这一工作。如果我们对一个处于运行中的slab系统拍照,其3层结构的关系大致如下图所示:
如上图所示:系统中所有的cache描述符均链接在一个双向链表中,并且这个链表头对象的名字是cache_chain。在一个cache中存在3个双向链表,分别为slab_partial,slab_full,slab_free,每个链上都链接了从属该cache的一类slab,slab_partial链接了一类“对象部分空闲”的slab,slab_full链接了一类“对象全部使用”的slab,slab_free链接了一类“对象全部空闲”的slab。每个slab的头部存放一个slab管理数据结构(也可能放在了外部),随后存放动态分配的对象。
在下一节中,我们将介绍slab的精细结构,展示slab内部精确的结构设计。
1.2 slab精细结构
如上文提到的那样,一个slab是一个或多个包含一个或多个相同类型对象的连续物理页面。
为了便于理解slab精细结构的设计思路,我们从原始的设计出发进行逐步改进:
在原始设计中,为了描述一个对象的属性信息,我们使用block_header引导访问一个对象,这个结构中保存了对象的长度信息和属性信息。为了能快速找到页面中空闲状态的对象,我们在原始设计中引入了一个free链表,这个链表能够将空闲状态对象的block_header链接到一起,用free做链表头。
当一个对象被分配时,必定使用free指向的这个对象,只需要将这个对象的block_header从链表上删除即可。
当一个对象被释放时,只需要将对象的block_header通过“头插法”插入到链表上即可。
然而,这样做的弊端也非常明显,“slab管理包含一个或多个相同类型对象的页面”与“每个对象对应一个block_header”二者发生了冲突。为此,我们首先考虑,能不能用一个数据结构对象存放slab中所有对象的属性信息,答案是肯定可以。所以我们有了改进版的原始设计:
通过将属性、长度信息存储在slab头部的slab_mgmt结构中,我们可以用类似数组的随机访问方式访问所有对象。但是这样的设计没能实现原始设计中的free链机制。为此,我们需要一种方式,能够兼具链表的插入删除性能和数组的低空间占用特性。我们很自然地想到要使用静态链表结构进行实现。
为了建立free链机制,我们在slab_mgmt的后面加入了一个数组,用这个数组实现静态free链表。我们使用一个包含3个对象的slab进行举例:
bufctl数组的含义是:数组长度是slab中对象的数量;每个数组元素对应slab中的一个对象;每个数组元素的值对应下一个空闲状态对象的bufctl数组元素下标。
在这个例子中,该slab刚刚初始化完毕,slab的对象全部处于空闲状态。slab_mgmt管理结构的free字段指向了bufctl数组第一个元素的下标0,含义是整个slab中第一个空闲状态对象的bufctl数组元素下标为0. 而bufctl[0]又指向第二个元素的下标1,含义是0号元素对应对象(第一个对象)的下一个空闲状态对象的bufctl数组元素下标为1. 依次递进,直到最后一个元素(bufctl[2])指向BUFCTL_END,含义是2号元素对应对象(第三个对象)的下一个空闲对象不存在。
这样,我们对原始的设计进行了改进,而最后的改进版本设计也正是slab使用的精细结构设计的基础版本,在下一节中,我们将会介绍slab的染色与对齐机制,到1.3节讲解完毕,我们将能够给出slab的完整精细结构描述。
1.3 slab染色与硬件cache对齐
为了与slab系统的cache相区分,在本文中,凡是“硬件cache”均指以CPU上L1-CACHE为代表的硬件缓存。
现在我们来考虑一种情况:
假设我们现在拥有一个cache,这个cache包含两个正在使用的slab。根据上面的叙述很显然这两个slab中存储的对象是完全一致的。
我们不妨暂时引用源码中的slab管理数据结构及其依赖的数据结构与类型定义:
typedef unsigned short kmem_bufctl_t;
struct list_head {
struct list_head *next, *prev;
};
struct slab {
struct list_head list;
unsigned long colouroff;
void *s_mem;
unsigned int inuse;
kmem_bufctl_t free;
};
假设我们处于64位系统下,该结构的指针长度均为8字节,unsigned long类型长度8字节,unsigned int类型长度4字节,kmem_bufctl_t类型长度2字节,结合结构体大小计算中的对齐原则和补齐原则,我们可以计算得出,一个slab管理数据结构的长度为40个字节
不妨继续假设我们存储的对象大小是119字节,硬件cache行大小为64字节,并设定slab长度为2个页面长度(8192字节)。
在以上条件下,当系统使用一个对象时,由于对象大小无法整除硬件cache行大小,必然存在对象加载进硬件cache时被硬件cache行割裂的情况,其后果就是硬件cache必须将对象后续部分再次加载进cache。这种情况是我们无论如何不想看到的。
因此我们宁可浪费几个字节的内存,也要让对象能够与硬件cache行对齐。在我们的案例中,我们将对象大小对齐为128字节,实际使用的119字节后的9个字节作为填充字节使用。在下文中我们将对象大小视为128字节处理(尽管我们知道实际大小并不是128字节,但9个填充字节将对使用者和我们是“透明”的)。
同样的,为了避免对象割裂,我们也会对slab中对象的起始位置进行设计,使起始位置也是硬件cache行对齐的。假设ALIGN()是一个对齐函数,能够将参数向上对齐到硬件cache行大小的倍数(64的倍数)。那么根据1.2节结构图,一个包含i个对象的slab有效部分长度为ALIGN(sizeof(slab_mgmt)+sizeof(bufctl_t)*i)+sizeof(obj)*i
根据这个公式和长度上限(slab页面长度),我们很容易找到一个最大值i,在我们的案例中,最大值i为62. 在我们的slab包含的两个页面上,含有62个对象、1个长度为62的bufctl数组和1个长度为40的slab_mgmt管理数据结构。并且实际使用的有效部分长度为8128字节。
我们很容易发现,2个页面的长度是8192字节,但在我们的slab中只使用了8128字节。相当于“浪费”了64字节。那么我们开始思考,能不能让这64字节变得有使用价值。在slab系统的设计中,slab系统使用这些浪费的字节对slab进行“染色”。
为了说明“染色”机制,我们首先来分析一种情况:
当所有的slab_mgmt数据结构均从slab的头部开始存放时,对于两个存放相同种类对象的slab,其内部的对象起始偏移量应当相同,当同时存在多个存放该对象的slab时,由于对象起始位置相同,可能发生这样一种情况:对不同slab内对象访问使用时,这些对象映射到了相同的硬件cache行中,这就导致硬件cache必须频繁读写某一个行,但没有利用其他硬件行,进而导致性能的下降。
为了缓解这个问题,我们使用“染色”技术,将相同结构的slab起始位置手动错开一个硬件cache行的长度,从而使具有相同偏移量的对象在加载到硬件cache时占据不同的行。在我们的案例中,由于浪费的空间恰好为64字节,也就是一个硬件cache行的长度,因此我们可以染两种颜色:0和1,对染色0的slab,其slab_mgmt前没有染色段,放置的偏移量为0;对染色1的slab,其slab_mgmt前有一个1*64字节的染色段,放置的偏移量为64.
在引入染色和对齐机制后,我们可以完整描述slab的精细结构了:
- 在slab的头部是一个染色段,长度为64*colour
- 在染色段后面紧跟slab_mgmt管理数据结构和bufctl数组,同时对两部分一起进行对齐,称为描述符段
- 描述符段后面开始放置经过对齐的对象
接下来,我们将引入slab系统的cache本地缓存机制,为阐述slab系统的对象操作彻底铺平道路
1.4 cache本地缓存机制
cache本地缓存机制用于隔离调用者和slab结构,使得调用者不能直接“看到”slab内部的细节。cache本地缓存内部存放了当前cache拥有的可用空闲对象物理地址。当需要分配、使用、释放一个对象时,均通过cache本地缓存进行,只有当cache本地缓存达到上下限时,才会对slab结构进行操作。
由于处理器往往存在各自的局部访问特性,因此与处理器分配、使用、释放单个对象紧密相关的cache本地缓存是per-CPU的,每个CPU都有一个属于自己的cache本地缓存。
在cache描述符中,有一个字段array,这个指针指向一个“指针数组”,数组的长度是CPU数量,每个数组元素是一个指针,指向对应cpu的array_cache,也就是cache本地缓存结构描述符。在每一个array_cache结构的后面,紧跟一段数组空间,这段空间是cache本地缓存的空间,上文提到cache本地缓存中存放了可用的空闲对象物理地址,其具体的存放位置就是这段数组空间。array_cache描述符用于记录这段数组内部内容的属性,例如数量和上下限等。
需要分配一个对象时,首先将访问cache本地缓存,从本地缓存中取出可用的空闲对象地址使用;当需要释放一个对象时,首先将访问cache本地缓存,将对象释放到缓存中。如果要分配一个对象,但本地缓存中没有可用对象,此时触发cache本地缓存的下限,考虑从底层的slab系统中创建一些可用对象。如果要释放一个对象,但本地缓存已经填满,此时触发cache本地缓存的上限,考虑批量将一组对象释放到slab中去,来清出一些本地缓存供使用。具体的本地缓存作用将在下文解析具体过程时具体体现。
而为了在CPU之间快速共享一些小对象,我们使用一个专门的cache本地缓存做共享,这个共享cache本地缓存位于cache描述符的lists.shared字段。当需要分配一个小对象时,我们将首先尝试从共享cache本地缓存中获取可用的对象,而当需要释放一个小对象时,我们也将首先尝试释放到共享cache本地缓存中。这样,在一些拥有小对象的cache中,一次分配的对象数量庞大(batchcount字段的作用,在下一节中解释),如果多个CPU都需要使用这些小对象,就可以使用共享cache本地缓存来充分利用这些对象。如果共享机制不存在,CPU0在初次使用对象时需要深入到slab增长过程进行分配,CPU1也需要申请一个slab进行使用。这样申请到的对象由于大小比较小,一次分配的数量庞大,就会造成时间和空间上的浪费,引入共享机制就可以有效解决这个问题。
1.5 slab系统数据结构字段注释
1.5.1 cache描述符
//cache描述符的数据结构
struct kmem_cache_s {
//1. array_cache本地缓存相关字段
struct array_cache *array[NR_CPUS]; //每个CPU独立的array_cache本地缓存
unsigned int batchcount; //array_cache全局的批量操作数
unsigned int limit; //array_cache全局的容量上限
//2. 相对静态的cache属性字段
struct kmem_list3 lists; //用于链接三种slab的链表结构
unsigned int objsize; //cache中存放的对象大小(字节)
unsigned int flags; //标志字段
unsigned int num; //一个slab中能够存放的对象数量
unsigned int free_limit; //链表中最大的空闲对象数量
spinlock_t spinlock; //spin锁
//3. cache在物理内存视角下的属性
unsigned int gfporder; //一个slab所占连续物理页数量(2^gfporder个)
unsigned int gfpflags; //分配连续物理页时的分配控制标志
//4. cache管理的slab属性字段
size_t colour; //颜色最大数值
unsigned int colour_off; //单位颜色偏移量(字节)
unsigned int colour_next; //要创建下一个slab时使用的颜色值
kmem_cache_t *slabp_cache; //如果slab的管理数据结构没有存放在slab内,
//这个指针就指向数据结构对象所在的cache描述符
unsigned int slab_size; //一个slab的大小(字节)
unsigned int dflags; //slab的动态标志
//5.构造函数(ctor)和析构函数(dtor)
void (*ctor)(void *, kmem_cache_t *, unsigned long);
void (*dtor)(void *, kmem_cache_t *, unsigned long);
//6. 与cache标识、链接相关的字段
const char *name; //cache名称
struct list_head next; //用于链接各个cache的双向链表项
};
1.5.2 array_cache缓存数据结构
//array_cache缓存的数据结构
struct arraycache_init {
struct array_cache cache; //缓存描述符
void * entries[BOOT_CPUCACHE_ENTRIES]; //存放对象地址的数组
};
//array_cache缓存描述符
struct array_cache {
unsigned int avail; //缓存中的对象数量
unsigned int limit; //缓存中最大的对象数量
unsigned int batchcount; //一次批量操作(申请对象/释放对象)操作的对象数量
unsigned int touched; //标识缓存最近是否被使用过(使用过为1)
};
1.5.3 kmem_list3三种slab的链表数据结构
//kmem_list3三种slab的链表数据结构
struct kmem_list3 {
struct list_head slabs_partial; //存放部分空闲slab的链表头
struct list_head slabs_full; //存放满slab的链表头
struct list_head slabs_free; //存放全空闲slab的链表头
unsigned long free_objects; //3种slab中空闲状态对象数量之和
int free_touched; //页回收算法使用的字段
unsigned long next_reap; //页回收算法使用的字段
struct array_cache *shared; //1.4节中指出的共享cache本地缓存指针
};
1.5.4 slab描述符
//slab描述符的数据结构
struct slab {
struct list_head list; //用于将slab链接的链表项
unsigned long colouroff; //slab的染色偏移量(字节)
void *s_mem; //slab中第一个对象的地址
unsigned int inuse; //slab中已使用对象的个数
kmem_bufctl_t free; //bufctl数组中第一个可用对象的下标值
};
1.5.5 通用对象分配器描述符
struct cache_sizes {
size_t cs_size; //通用对象大小
kmem_cache_t *cs_cachep; //通用对象在非DMA空间内的cache
kmem_cache_t *cs_dmacachep; //通用对象在DMA空间内的cache
};