块分配器SLAB的内核实现

主要参考了《深入linux内核》和《Linux内核深度解析》,另外简单浅析了一下相关内容

块分配器

SLAB分配器在某些情况下表现不太好,所以Linux内核提供了两个改进的块分配器。

在配备了大量物理内存的大型计算机上,SLAB分配器的管理数据结构的内存开销比较大,所以设计了SLUB分配器。

在小内存的嵌入式设备上,SLAB分配器的代码太多、太复杂,所以设计了一个精简的SLOB分配器。SLOB是“Simple List Of Blocks”的缩写,

目前SLUB分配器已成为默认的块分配器。

image-20220531001031422

块分配器核心思想

SLAB分配器的作用不仅仅是分配小块内存,更重要的作用是针对经常分配和释放的对象充当缓存。

为每种对象类型创建一个内存缓存,每个内存缓存由多个大块组成,一个大块是一个或多个连续的物理页,每个大块包含多个对象。
slab采用面向对象的思想,基于对象类型管理内存,每种对象被划分为一个类,比如进程描述符(task_struct)是一个类,每个进程描述符实现是一个对象。

内存缓存组成结构如下:

image-20220531001205713

编程接口

使用通用的内存缓存

include\linux\slab.h

3种块分配器提供了统一的编程接口

  • 分配内存:void * kmalloc (size_t size,gfp_t flags);
  • 重新分配内存:void *krealloc(const void *p, size_t new_size, gfp_t flags)
  • 释放内存:void kfree ( const void * objp);

创建专用的内存缓存

使用通用的内存缓存的缺点是:块分配器需要找到一个对象的长度刚好大于或等于请求的内存长度的通用内存缓存,如果请求的内存长度和内存缓存的对象长度相差很远,浪费比较大,例如申请36字节,实际分配的内存长度是64字节,浪费了28字节。所以有时候使用者需要创建专用的内存缓存,编程
接口如下。

/*
	name:名称
    size:对象的长度
    align:对象需要对齐的数值
    flags: slab标志位
    strc:对象的构造函数
    如果创建成功,返回内存缓存的地址,否则返回空指针。
*/
// 创建内存缓存
struct kmem_cache *kmem_cache_create(const char *name, size_t size, size_talign,unsigned long flags,void (*strc)(void *));
/*
    cachep:从指定的内存缓存分配
    flags:传给页分配器的分配标志位,当内存缓存没有空闲对象,向页分配器请求分配页的时候使用这个分配标志位。
    如果分配成功,返回对象的地址,否则返回空指针。
*/
// 指定的内存缓存分配对象
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
/*
    cachep:对象所属的内存缓存
    objp:对象的地址
*/
// 释放对象
void kmem_cache_free(struct kmem_cache *cachep, void *objp);
// s 内存缓存
// 销毁内存缓存
void kmem_cache_destroy(struct kmem_cache *s);

SLAB分配器

内存缓存数据结构

image-20220804164521321

kmem_cache

一个高速缓存中可以含有多个kmem_cache对应的高速缓存,就拿L1高速缓存来举例,一个L1高速缓存对应一个kmem_cache链表,这个链表中的任何一个kmem_cache类型的结构体均描述一个高速缓存,而这些高速缓存在L1 cache中各自占用着不同的区域。

每个内存缓存对应一个kmem_cache实例。

成员gfporder是slab的阶数
成员num是每个slab包含的对象数量
成员object_size是对象原始长度
成员size是包括填充的对象长度。

kmem_cache实例的成员cpu_slab指向array_cache实例,每个处理器对应一个array_cache实例,称为数组缓存,用来缓存刚刚释放的对象,分配时首先从当前处理器的数据缓存分配,避免每次都要从slab分配,减少链表操作的锁操作,提高分配的速度。

array_cache

  • 成员limit是数组大小
  • 成员avail是数组entry存放的对象数量
  • 数组entry存放对象的地址。
kmem_cache_node

每个内存节点对应一个kmem_cache_node实例。

kmem_cache_node实例包含3条slab链表:

  • 链表slabs_partial把部分对象空闲的slab链接起来
  • 链表slabs_full把没有空闲对象的slab链接起来
  • 链表slabs_free把所有对象空闲的slab链接起来。

成员total_slabs是slab数量。

page的slab相关成员

每个slab由一个或多个连续的物理页组成,页的阶数是kmem_cache.gfporder,如果阶数大于0,组成一个复合页。

slab被划分为多个对象,大多数情况下slab长度不是对象长度的整数倍,slab有剩余部分,可以用来给slab着色:“把slab的第一个对象从slab的起始位置偏移一个数值,偏移值是处理器的一级缓存行长度的整数倍,不同slab的偏移值不同,使不同slab的对象映射到处理器不同的缓存行”,所以我们看到在slab的前面有一个着色部分。

slab嵌入在page结构体里

page结构体的相关成员如下。

1)成员flags设置标志位PG_slab,表示页属于SLAB分配器。
2)成员s_mem存放slab第一个对象的地址。
3)成员active表示已分配对象的数量。
4)成员lru作为链表节点加入其中一条slab链表。
5)成员slab_cache指向kmem_cache实例。
6)成员freelist指向空闲对象链表。

kfree函数怎么知道对象属于哪个通用的内存缓存?分为5步。

  • 根据对象的虚拟地址得到物理地址,因为块分配器使用的虚拟地址属于直接映射的内核虚拟地址空间,虚拟地址=物理地址+常量,把虚拟地址转换成物理地址很方便。
  • 根据物理地址得到物理页号。
  • 根据物理页号得到page实例。
  • 如果是复合页,需要得到首页的page实例。
  • 根据page实例的成员slab_cache得到kmem_cache实例。

每个对象的内存布局

image-20220804170358447

(1)红色区域1:长度是8字节,写入一个魔幻数,如果值被修改,说明对象被改写。
(2)真实对象:长度是kmem_cache.obj_size,偏移是kmem_cache.obj_offset。
(3)填充:用来对齐的填充字节。
(4)红色区域2:长度是8字节,写入一个魔幻数,如果值被修改,说明对象被改写。
(5)最后一个使用者:在64位系统上长度是8字节,存放最后一个调用者的地址,用来确定对象被谁改写。

对象的长度是kmem_cache.size。红色区域1、红色区域2和最后一个使用者是可选的,当想要发现内存分配和使用的错误,打开调试配置CONFIG_DEBUG_SLAB的时候,对象才包含这3个成员。

kmem_cache.obj_size是调用者指定的对象长度,kmem_cache.size是对象实际占用的内存长度,通常比前者大,原因是为了提高访问对象的速度,需要把对象的地址和长度都对齐到某个值,对齐值的计算步骤如下。

(1)如果创建内存缓存时指定了标志位SLAB_HWCACHE_ALIGN,要求和处理器的一级缓存行的长度对齐,计算对齐值的方法如下。
● 如果对象的长度大于一级缓存行的长度的一半,对齐值取一级缓存行的长度。
● 如果对象的长度小于或等于一级缓存行的长度的一半,对齐值取(一级缓存行的长度/2n),把2n个对象放在一个一级缓存行里面,需要为n找到一个合适值。
● 如果对齐值小于指定的对齐值,取指定的对齐值。

举例说明:假设指定的对齐值是4字节,一级缓存行的长度是32字节,对象的长度是12字节,那么对齐值是16字节,对象占用的内存长度是16字节,把两个对象放在一个一级缓存行里面。

(2)如果对齐值小于ARCH_SLAB_MINALIGN,那么取ARCH_SLAB_MINALIGN。ARCH_SLAB_MINALIGN是各种处理器架构定义的最小对齐值,默认值是8。
(3)把对齐值向上调整为指针长度的整数倍。

空闲对象链表

每个slab需要一个空闲对象链表,从而把所有空闲对象链接起来,空闲对象链表是用数组实现的,数组的元素个数是slab的对象数量,数组存放空闲对象的索引。假设一个slab包含4个对象,空闲对象链表的初始状态如图3.24所示。

image-20220804172058577

page->freelist指向空闲对象链表,数组中第n个元素存放的对象索引是n,如果打开了SLAB空闲链表随机化的配置宏CONFIG_SLAB_FREELIST_RANDOM,数组中第n个元素存放的对象索引是随机的。

page->active为0,有两重意思。
(1)存放空闲对象索引的第一个数组元素的索引是0。
(2)已分配对象的数量是0。

第一次分配对象,从0号数组元素取出空闲对象索引0,page->active增加到1,空闲对象链表如图3.25所示。

image-20220804172416082

当所有对象分配完毕后,page->active增加到4,等于slab的对象数量,空闲对象链表如图3.26所示。

image-20220804172509209

当释放索引为0的对象以后,page->active减1变成3,3号数组元素存放空闲对象索引0,空闲对象链表如图3.27所示。

image-20220804172601590

空闲对象链表的位置有3种选择
(1)使用一个对象存放空闲对象链表,此时kmem_cache.flags设置了标志位CFLGS_OBJFREELIST_SLAB。
(2)把空闲对象链表放在slab外面,此时kmem_cache.flags设置了标志位CFLGS_OFF_SLAB。
(3)把空闲对象链表放在slab尾部。如果kmem_cache.flags没有设置上面两个标志位,就表示把空闲对象链表放在slab尾部。
如果使用一个对象存放空闲对象链表,默认使用最后一个对象。如果打开了SLAB空闲链表随机化的配置宏CONFIG_SLAB_FREELIST_RANDOM,这个对象是随机选择的。

假设一个slab包含4个对象,使用1号对象存放空闲对象链表,初始状态如图3.28所示。

image-20220804172927153

这种方案会不会导致可以分配的对象减少一个呢?答案是不会,存放空闲对象链表的对象可以被分配。这种方案采用了巧妙的方法。

(1)必须把存放空闲对象链表的对象索引放在空闲对象数组的最后面,保证这个对象是最后一个被分配出去的。
(2)分配最后一个空闲对象,page->active增加到4,page->freelist变成空指针,所有对象被分配出去,已经不需要空闲对象链表,如图3.29所示。

image-20220804173542151

(3)在所有对象分配完毕后,假设现在释放2号对象,slab使用2号对象存放空闲对象链表,page->freelist指向2号对象,把对象索引2存放在空闲对
象数组的最后面,如图3.30所示。

image-20220804173705415

如果把空闲对象链表放在slab外面,需要为空闲对象链表创建一个内存缓存,kmem_cache.freelist_cache指向空闲对象链表的内存缓存,如图3.31所示。

image-20220804173754704

如果 slab 尾部的剩余部分足够大,可以把空闲对象链表放在 slab 尾部,如图3.32所示。

image-20220804173959315

创建内存缓存的时候,确定空闲对象链表的位置的方法如下。

(1)首先尝试使用一个对象存放空闲对象链表。

  1. 如果指定了对象的构造函数,那么这种方案不适合。

  2. 如果指定了标志位SLAB_TYPESAFE_BY_RCU,表示使用RCU技术延迟释放slab,那么这种方案不适合。

  3. 计算出slab长度和slab的对象数量,空闲对象链表的长度等于(slab的对象数量 * 对象索引长度)。如果空闲对象链表的长度大于对象长度,那么这种方案不适合。

    计算slab长度

    函数calculate_slab_order负责计算slab长度,从0阶到kmalloc()函数支持的最大阶数(KMALLOC_MAX_ORDER),尝试如下。

    (1)计算对象数量和剩余长度。
    (2)如果对象数量是0,那么不合适。
    (3)如果对象数量大于允许的最大slab对象数量,那么不合适。允许的最大slab对象数量是SLAB_OBJ_MAX_NUM,等于(2 ^ sizeof(freelist_idx_t) × 8 − 1),freelist_idx_t是对象索引的数据类型。
    (4)对于空闲对象链表在slab外面的情况,如果空闲对象链表的长度大于对象长度的一半,那么不合适。
    (5)如果slab是可回收的(设置了标志位SLAB_RECLAIM_ACCOUNT),那么选择这个阶数。
    (6)如果阶数大于或等于允许的最大slab阶数(slab_max_order),那么选择这个阶数。尽量选择低的阶数,因为申请高阶页块成功的概率低。
    (7)如果剩余长度小于或等于slab长度的1/8,那么选择这个阶数。

    slab_max_order:允许的最大slab阶数。如果内存容量大于32MB,那么默认值是1,否则默认值是0。可以通过内核参数“slab_max_order”指定。

(2)接着尝试把空闲对象链表放在slab外部,计算出slab长度和slab的对象数量。如果slab的剩余长度大于或等于空闲对象链表的长度,应该把空闲对象链表放在slab尾部,不应该使用这种方案。

(3)最后尝试把空闲对象链表放在slab尾部。

着色

slab是一个或多个连续的物理页,起始地址总是页长度的整数倍,不同slab中相同偏移的位置在处理器的一级缓存中的索引相同。

  • 如果slab的剩余部分的长度超过一级缓存行的长度,剩余部分对应的一级缓存行没有被利用;
  • 如果对象的填充字节的长度超过一级缓存行的长度,填充字节对应的一级缓存行没有被利用。

这两种情况导致处理器的某些缓存行被过度使用,另一些缓存行很少使用。

在slab的剩余部分的长度超过一级缓存行长度的情况下,为了均匀利用处理器的所有一级缓存行,slab着色(slab coloring)利用slab的剩余部分,使不同slab的第一个对象的偏移不同。着色是一个比喻,和颜色无关,只是表示slab中的第一个对象需要移动一个偏移值,使对象放到不同的一级缓存行里。
内存缓存中着色相关的成员如下。

(1)kmem_cache.colour_off是颜色偏移,等于处理器的一级缓存行的长度,如果小于对齐值,那么取对齐值。
(2)kmem_cache.colour是着色范围,等于(slab的剩余长度/颜色偏移)。
(3)kmem_cache.node[n]->colour_next是下一种颜色,初始值是0。在内存节点n上创建新的slab,计算slab的颜色偏移的方法如下。

  1. 把kmem_cache.node[n]->colour_next加1,如果大于或等于着色范围,那么把值设置为0。
  2. slab的颜色偏移 = kmem_cache.node[n]->colour_next * kmem_cache.colour_off。slab对应的page结构体的成员s_mem存放第一个对象的地址,等于(slab的起始地址 + slab的颜色偏移)。

每处理器数组缓存

内存缓存为每个处理器创建一个数组缓存(结构体array_cache)。释放对象时,把对象存放到当前处理器对应的数组缓存中;分配对象的时候,先从当前处理器的数组缓存分配对象,采用后进先出(Last In First Out,LIFO)原则,可以提高性能。

image-20220804185809392

  • 刚释放的对象很可能还在处理器的缓存中,可以更好地利用处理器的缓存;
  • 减少对链表操作;
  • 避免处理器之间互斥,减少自旋锁操作;

结构体array_cache如下。

(1)成员entry是存放对象地址的数组。
(2)成员avail是数组存放的对象的数量。
(3)成员limit是数组的大小,和结构体kmem_cache的成员limit的值相同,是根据对象长度猜测的一个值。
(4)成员batchcount是批量值,和结构体kmem_cache的成员batchcount的值相同,批量值是数组大小的一半。

1、分配对象的时候,先从当前处理器的数组缓存分配对象,如果数组缓存是空的,那么批量分配对象以重新填充数组缓存,批量值就是数组缓存的成员batchcount.

2、释放对象的时候,如果数组缓存是满的,那么先把数组缓存中的对象地址批量归还给slab,批量值是数组缓存的成员batchcount,然后把正在释放的对象存放到数组缓存中。

对NUMA的支持

我们看看SLAB分配器怎么支持NUMA系统。如图3.34所示,内存缓存针对每个内存节点创建一个kmem_cache_node实例。

image-20220804232431440

kmem_cache_node实例的成员shared指向共享数组缓存,成员alien指向远程节点数组缓存,每个节点一个远程节点数组缓存。

这两个成员有什么用处呢?

  • 用来分阶段释放从其他节点借用的对象,先释放到远程节点数组缓存,然后转移到共享数组缓存,最后释放到远程节点的slab。

假设处理器0属于内存节点0,处理器1属于内存节点1。

  • 处理器0申请分配对象的时候,首先从节点0分配对象,如果分配失败,从节点1借用对象。

  • 处理器0释放从节点1借用的对象时,需要把对象放到节点0的kmem_cache_node实例中与节点1对应的远程节点缓存数组中,先看是不是满了,
    如果是满的,那么必须先清空:把对象转移到节点1的共享数组缓存中,如果节点1的共享数组缓存满了,那么把剩下的对象直接释放到slab。

分配和释放本地内存节点的对象时,也会使用共享数组缓存。

(1)申请分配对象时,如果当前处理器的数组缓存是空的,共享数组缓存里面的对象可以用来重填。

(2)释放对象时,如果当前处理器的数组缓存是满的,并且共享数组缓存有空闲空间,那么可以转移一部分对象到共享数组缓存,不需要把对象批量归还给slab,然后把正在释放的对象添加到当前处理器的数组缓存中。

全局变量use_alien_caches用来控制是否使用远程节点数组缓存分阶段释放从其他节点分配的对象,默认值是1,可以在引导内核时使用内核数“noaliencache”指定。

当包括填充的对象长度不超过页长度的时候,使用共享数组缓存,数组大小是(kmem_cache.shared * kmem_cache.batchcount),kmem_cache.batchcount是批量值,kmem_cache.shared用来控制共享数组缓存的大小,当前代码实现指定的值是8。

内存缓存合并

**为了减少内存开销和增加对象的缓存热度,块分配器会合并相似的内存缓存。在创建内存缓存的时候,从已经存在的内存缓存中找到一个相似的内存缓存,和原始的创建者共享这个内存缓存。**3种块分配器都支持内存缓存合并。

假设正在创建的内存缓存是t。

如果合并控制变量slab_nomerge的值是1,那么不能合并。默认值是0,如果想要禁止合并,可以在引导内核时使用内核参数“slab_nomerge”指定。

如果t指定了对象构造函数,不能合并。
如果t设置了阻止合并的标志位,那么不能合并。阻止合并的标志位是调试和使用RCU技术延迟释放slab,其代码如下:

#define SLAB_NEVER_MERGE (SLAB_RED_ZONE | SLAB_POISON | SLAB_STORE_USER|\
         SLAB_TRACE | SLAB_TYPESAFE_BY_RCU | SLAB_NOLEAKTRACE | \
         SLAB_FAILSLAB | SLAB_KASAN)

遍历每个内存缓存s,判断t是否可以和s合并。
(1)如果s设置了阻止合并的标志位,那么t不能和s合并。
(2)如果s指定了对象构造函数,那么t不能和s合并。
(3)如果t的对象长度大于s的对象长度,那么t不能和s合并。
(4)如果t的下面4个标志位和s不相同,那么t不能和s合并。

#define SLAB_MERGE_SAME (SLAB_RECLAIM_ACCOUNT | SLAB_CACHE_DMA | \
             SLAB_NOTRACK | SLAB_ACCOUNT)

(5)如果对齐值不兼容,即s的对象长度不是t的对齐值的整数倍,那么t不能和s合并。
(6)如果s的对象长度和t的对象长度的差值大于或等于指针长度,那么t不能和s合并。

(7)SLAB分配器特有的检查项:如果t的对齐值不是0,并且t的对齐值大于s的对齐值,或者s的对齐值不是t的对齐值的整数倍,那么t不能和s合并。
(8)顺利通过前面7项检查,说明t和s可以合并。

找到可以合并的内存缓存以后,把引用计数加1,对象的原始长度取两者的最大值,然后把内存缓存的地址返回给调用者。

回收内存

对于所有对象空间的slab,没有立即释放,而是放在空闲slab链表中。只有内存节点上空闲对象的数量超过限制,才开始回收空闲slab,直到空闲对象的数量小于或等于限制。

如图3.35所示,结构体kmem_cache_node的成员slabs_free是空闲slab链表的头节点,成员free_objects是空闲对象的数量,成员free_limit是空闲对象的数量限制。

image-20220804190106762

节点n的空闲对象数量限制= (1+节点的处理器数量) * kmem_cache.batchcount(批量值) + keme_cache.num(每个slab包含对象数量),
slab分配器定期回收对象和空闲slab,实现方法是在每个处理器上向全局工作队列添加1个延迟工作项,工作项的处理函数cache_reap。

struct kmem_cache_node {
    ...
    struct array_cache *shared;	/* shared per node */
	struct alien_cache **alien;	/* on other nodes */
    ...
};

struct alien_cache {
	spinlock_t lock;
	struct array_cache ac;
};

1、每个处理器每隔2秒针对每个内存缓存(kemem_cash)执行

回收节点n(假设当前处理器属于节点n)对应的远程节点数组缓存中的对象;
如果过去2秒没有从当前处理器的数组缓存分配对象,那么回收数组缓存中的对象。

2、每个处理器隔4秒针对每个内存缓存执行

如果过去4秒没有从共享数组缓存分配对象,那么回收共享数组缓存中的对象;
如果过去4秒没有从空闲slab分配对象,那么回收空闲slab。

SLUB分配器(略)

SLUB分配器继承了SLAB分配器的核心思想,在某些地方做了改进。
(1)SLAB分配器的管理数据结构开销大,早期每个slab有一个描述符和跟在后面的空闲对象数组。SLUB分配器把slab的管理信息保存在page结构体中,使用联合体重用page结构体的成员,没有使page结构体的大小增加。

现在SLAB分配器反过来向SLUB分配器学习,抛弃了slab描述符,把slab的管理信息保存在page结构体中。

(2)SLAB分配器的链表多,分为空闲slab链表、部分空闲slab链表和满slab链表,管理复杂。SLUB分配器只保留部分空闲slab链表。

(3)SLAB分配器对NUMA系统的支持复杂,每个内存节点有共享数组缓存和远程节点数组缓存,对象在这些数组缓存之间转移,实现复杂。SLUB分配器做了简化。

(4)SLUB分配器抛弃了效果不明显的slab着色。

image-20220804234541875

SLOB分配器

SLOB分配器最大的特点就是简洁,代码只有600多行,特别适合小内存的嵌入式设备。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值