伙伴系统用于分配内存时以page为单位的,在实际中有很多内存需求是以Byte为单位的,那么如果我们需要分配以Byte为单位的小内存块时,该如何分配呢?slab分配器就是用来解决小内存块分配问题的,也是内存分配中非常重要的角色之一。slab分配器最终还是由伙伴系统来分配出实际的物理页面,只不过slab分配器在这些连续的物理页面上实现了自己的算法,以此来对小内存块进行管理。关于slab分配器,我们需要思考如下几个问题。
-
slab分配器是如何分配和释放小内存块的?
答:
-
slab分配器中有一个着色的概念(cache color),着色有什么作用?
答:通过cache color可以更好的利用缓存,slab分配器能够均匀地分布对象,以实现均匀地缓存利用,着色这个术语是隐喻性的。它与颜色无关,只是表示slab中的对象需要移动的特定偏移量,以便使对象放置到不同的缓存行。
-
slab分配器中的slab对象有没有根据Per-CPU做一些优化?
答:
-
slab增长并导致大量不用的空闲对象,该如何解决?
答:
slab分配器提供如下接口来创建、释放slab描述符合分配缓存对象。
//创建slab描述符
struct kmem_cache *
kmem_cache_create(const char *name, size_t size, size_t align,
unsigned long flags, void (*ctor)(void *))
//释放slab描述符
void kmem_cache_destroy(struct kmem_cache *s)
//分配缓存对象
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
//释放缓存对象
void kmem_cache_free(struct kmem_cache *cachep, void *objp)
kmem_cache_create()函数中有如下参数:
-
name: slab描述符的名称。
-
size: 缓存对象的大小。
-
align:缓存对象需要对齐的字节数。
-
flags: 分配掩码。
-
ctor: 对象的构造函数。
例如, 在Intel显卡驱动中就大量使用kmem_cache_create()来创建自己的slab描述符。
[drivers/gpu/drm/i915/i915_gem.c]
//创建名为"i915_gem_object" slab描述符
void i915_gem_load(struct drm_device *dev)
{
......
dev_priv->slab =
kmem_cache_create("i915_gem_object",
sizeof(struct drm_i915_gem_object), 0,
SLAB_HWCACHE_ALIGN,
NULL);
......
}
void *i915_gem_object_alloc(struct drm_device *dev)
{
//分配缓存对象
return kmem_cache_zalloc(dev_priv->slab, GFP_KERNEL);
}
另外一个大量使用slab机制的是kmalloc()函数接口。kmem_cache_create()函数用于创建自己的缓存描述符,类似于创建通用的缓存,类似于用户空间中C标准库malloc()函数。
slab分配器有两个好处
-
调用伙伴系统的操作对系统的数据和指令高速缓存有相当的影响。内核越浪费这些资源,这些资源对用户空间进程就越不可用。更轻量级的slab分配器在可能的情况下减少了对伙伴系统的调用,有助于防止不受欢迎的缓存"污染".
-
如果数据存储在伙伴系统直接提供的页中,那么其地址总是出现在2的幂次的整数倍附近(许多将页划分为更小块的其他分配方法,也有同样的特征)。这对CPU高速缓存的利用有负面影响,由于这种地址分布,使得某些缓存行为过度使用,而其他的则几乎为空。多处理器系统可能会加剧这种不利情况,因为不同的内存地址可能在不同的总线上传输,上述情况会导致某些总线拥塞,而其他总线则几乎没有使用。
通过slab着色(slab coloring),slab分配器能够均匀地分布对象,以实现均匀地缓存利用,如下所示。
经常使用的内核对象保存在CPU高速缓存中, 这是我们想要的效果。从slab分配器的角度进行衡量,伙伴系统的高速缓存和TLB占用较大,这是一个负面效应。因为这会导致不重要的数据驻留在CPU高速缓存中,而重要的数据则被置换到内存,显然应该防止这种情况出现。
着色这个术语是隐喻性的。它与颜色无关,只是表示slab中的对象需要移动的特定偏移量,以便使对象放置到不同的缓存行。
slab分配器由何得名?各个缓存管理的对象,会合并为较大的组,覆盖一个或多个连续页,这中组称作slab,每个缓存由几个这中slab组成。简单说:slab就是一组缓存对象
slab分配的原理:
slab分配器由一个紧密地交织的数据结构和内存结构的网络组成,初看起来不容易理解其运作方式。因此在考察其实现前,重要的是获得各个结构之间关系的概观。
基本上,slab缓存由下图的两部分组成:保存管理性数据的缓存对象和保存被管理对象的各个slab。
每个缓存只负责一种对象类型(例如:struct unix_sock实例),或提供一般性的缓冲区。各个缓存中slab的数目各有不同,这与已经使用的页的数目、对象长度、和被管理对象的数目有关。
另外,系统中所有的缓存都保存在一个双向链表中。这使得内核有机会依次遍历所有的缓存。这是有必要的,例如在即将发生内存不足时,内核可能需要缩减分配给缓存的内存数量。
-
缓存的精细结构
如果我们更仔细地研究缓存的结构,就可以注意到一些更重要的细节。如下图
除了管理性数据(如已用和空闲对象或标志寄存器数目),缓存结构包括两个特别重要的成员。
-
指向一个数组的指针,其中保存了各个CPU最后释放的对象。
-
每个内存结点都对应3个表头,用于组织slab的链表。第1个链表包含完全用尽的slab,第2个是部分空闲的slab,第3个是空闲的slab。
缓存结构指向一个数组,其中包含了与系统CPU数目相同的数组项。每个元素都是一个指针,指向一个进一步的结构称为数组缓存(array cache),其中包含了对应于特定系统CPU的管理数据(就总体来看,不是用于缓存)。管理性数据之后的内存区包含了一个指针数组,各个数组项指向slab中未使用的对象。
为了最好地利用CPU高速缓存,这些per-CPU指针是很重要的。在分配和释放对象时,采用后进先出原理(LIFO,last in first out)。内核刚释放的对象仍然处于CPU高速缓存中,会尽快再次分配它(响应下一个分配请求)。仅当per-CPU缓存为空时,才会用slab中的空闲对象重新填充它们。
这样,对象分配的体系就形成了一个三级的层次结构,分配成本和操作对CPU高速缓存和TLB的负面影响逐级升高。
-
仍然处于CPU高速缓存中的per-CPU对象。
-
现存slab中未使用的对象。
-
刚使用伙伴系统分配的新slab中未使用的对象。
slab的精细结构
对象在slab中并非连续排列,而是按照一个相当复杂的方案分布。如下图:
用于每个对象的长度并不反应其确切地大小。相反,长度已经进行了舍入,以满足某些对齐方式的要求。有两种可用的备选对齐方案。
-
slab创建时使用表示SLAB_HWCACHE_ALIGN, slab用户可以要求对象按硬件缓存行对齐。那么会按照cache_line_size的返回值进行对齐,该函数返回特定于处理器的L1缓存大小。如果对象小于缓存行长度的一半,那么将多个对象放入一个缓存行。
-
如果不要求按硬件缓存行对齐,那么内核保证对象按BYTES_PER_WORD对齐,该值是表示void 指针所需字节的数目。
在32位处理器上,void指针需要4个字节。因此,对于6字节的对象,则需要8 = 2*4 个字节,15个字节的对象需要16 = 4*4 个字节。多余的字节称为填充字节。
填充字节可以加速对slab中对象的访问。如果使用对齐的地址,那么在几乎所有的体系结构上,内存访问都会更快。这弥补了使用填充字节必然导致需要更多内存的不利情况。
管理结构位于每个slab的起始处,保存了所有的管理数据(和用于连接缓存链表的元素)。其后面是一个数组,每个(整数)数组项对应于slab中的一个对象的索引。由于最低编号的空闲对象的标号还保存在slab起始处的管理结构中,内存无需使用链表或其他复杂的关联机制,即可轻松找到当前可用的所有对象。数组的最后一项总是一个结束标记,值为BUFCTL_END。slab中空闲兑现的管理图:
大多数情况下,slab内存区的长度(减去头部管理数据)是不能被(可能填补过的)对象长度整除的。因此内存就有了一些多余的内存,可以用来以偏移量的形式给slab"着色",如上图所述。缓存的各个slab成员会指定不同的偏移量,以便将数据定位到不同的缓存行,因而slab开始和结束处的空闲内存是不同的。在计算偏移量时,内存必须考虑其他的对齐因素。例如,L1高速缓存中数据的对齐。
管理数据可以放置在slab自身,也可以放置到使用kmalloc分配的不同内存区中。内核如何选择,取决于slab的长度和已用对象的数量。管理数据和slab内存之间的关联很容易建立,因为slab头包含了一个指针,指向slab数据区的起始处(无论管理数据是否在slab上)。
slab首部位于slab外部的情形:
最后,内核需要一种方法,通过对象自身即可识别slab(以及对象驻留的缓存)。根据对象的物理内存地址,可以找到相关的页,因此可以在全局mem_map数组中找到对应的page实例。
我们已经知道,page结构包含一个链表元素,用于管理各种链表中的页。对于slab缓存中的页而言,该指针是不必要的,可用于其他用途。
-
page->lru.next 指向页驻留的缓存管理结构。
-
page->lru.prev 指向保存该页的slab管理结构。
从下面三幅图可以观察出slab的基本数据结构逻辑关系。