《Linux内核深度解析》选载之块分配器

原创 Linux阅码场 2022-07-27 08:00

Ftrace训练营火热报名中:Ftrace训练营:站在设计者的角度来理解ftrace(限50人)。训练营第一期报名已圆满成功,好评如潮。第二期课程正在进行中,第三期报名正在火爆进行中(咨询小月微信:linuxer2016)。


ARM安全架构训练营2期火热报名中:阅码场训练营:ARM安全架构之Trustzone-TEE实战。报名咨询客服(小月微信:linuxer2016)。


ARM架构与调优调试训练营火热报名中:阅码场训练营:ARM架构与调试调优。报名咨询客服(小月微信:linuxer2016)。



阅码场用户程磊对《Linux内核深度解析》推荐如下:

1.语言浅显易懂,内容深入浅出。

2.逻辑清晰,条理分明,逐步深入,层层递进。

3.基于较新的4.12内核版本,很多经典内核书籍虽然写的都非常好,但是都是基于2.6内核,很多在2.6之后引入的新技术并没有讲到,而本书对这些新技术都有非常详细的讲解。




作者简介:

余华兵,2005年毕业于华中科技大学计算机学院,取得硕士学位。毕业后的十余年一直在网络通信行业从事软件设计和开发工作,研究方向包括IPv4协议栈、IPv6协议栈和Linux内核。



目录

3.8 块分配器

3.8.1 编程接口 
3.8.2 SLAB分配器 
3.8.3 SLUB分配器 
3.8.4 SLOB分配器 


3.8 块分配器


为了解决小块内存的分配问题,Linux 内核提供了块分配器,最早实现的块分配器是SLAB 分配器。

SLAB 分配器的作用不仅仅是分配小块内存,更重要的作用是针对经常分配和释放的对象充当缓存。SLAB 分配器的核心思想是:为每种对象类型创建一个内存缓存,每个内存缓存由多个大块(slab,原意是大块的混凝土)组成,一个大块是一个或多个连续的物理页,每个大块包含多个对象。SLAB 采用了面向对象的思想,基于对象类型管理内存,每种对象被划分为一类,例如进程描述符(task_struct)是一个类,每个进程描述符实例是一个对象。

内存缓存的组成如图 3.21 所示。



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

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

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

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


3.8.1 编程接口


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

为了方便使用,块分配器在初始化的时候创建了一些通用的内存缓存,对象的长度大多数是 字节,从普通区域分配页的内存缓存的名称是“kmalloc-”(size 是对象的长度),从 DMA 区域分配页的内存缓存的名称是“dma-kmalloc-”,执行命令cat /proc/slabinfo”可以看到这些通用的内存缓存。

通用的内存缓存的编程接口如下。

1)分配内存。


2)重新分配内存。


3)释放内存。


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


1)创建内存缓存。


2)从指定的内存缓存分配对象。


3)释放对象。


4)销毁内存缓存。

3.8.2 SLAB 分配器


1.数据结构


内存缓存的数据结构如图 3.22 所示。

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

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

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

kmem_cache_node 实例包含 3 slab 链表:链表 slabs_partial 把部分对象空闲的 slab链接起来,链表 slabs_full 把没有空闲对象的 slab 链接起来,链表 slabs_free 把所有对象空闲的 slab 链接起来。成员 total_slabs slab 数量。



每个 slab 由一个或多个连续的物理页组成,页的阶数是 kmem_cache.gfporder,如果阶数大于 0,组成一个复合页。slab 被划分为多个对象,大多数情况下 slab 长度不是对象长度的整数倍,slab 有剩余部分,可以用来给 slab 着色:“把 slab 的第一个对象从 slab 的起始位置偏移一个数值,偏移值是处理器的一级缓存行长度的整数倍,不同 slab 的偏移值不同,使不slab 的对象映射到处理器不同的缓存行”,所以我们看到在 slab 的前面有一个着色部分。

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 实例。

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

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


每个对象的内存布局如图 3.23 所示。


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,要求和处理器的一级缓存行的长度对齐,计算对齐值的方法如下。


  • 如果对象的长度大于一级缓存行的长度的一半,对齐值取一级缓存行的长度。

  • 如果对象的长度小于或等于一级缓存行的长度的一半,对齐值取(一级缓存行的长度/),把 个对象放在一个一级缓存行里面,需要为 n 找到一个合适的值。

  • 如果对齐值小于指定的对齐值,取指定的对齐值。

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


(2)如果对齐值小于 ARCH_SLAB_MINALIGN,那么取 ARCH_SLAB_MINALIGNARCH_SLAB_MINALIGN 是各种处理器架构定义的最小对齐值,默认值是 8

3)把对齐值向上调整为指针长度的整数倍。


2.空闲对象链表


每个 slab 需要一个空闲对象链表,从而把所有空闲对象链接起来,空闲对象链表是用数组实现的,数组的元素个数是 slab 的对象数量,数组存放空闲对象的索引。假设一个 slab

包含 4 个对象,空闲对象链表的初始状态如图 3.24 所示。

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


page->active 0,有两重意思。


1)存放空闲对象索引的第一个数组元素的索引是 0

2)已分配对象的数量是 0


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


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

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



空闲对象链表的位置有 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 所示。


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

1)必须把存放空闲对象链表的对象索引放在空闲对象数组的最后面,保证这个对象是最后一个被分配出去的。

2)分配最后一个空闲对象,page->active增加到 4page->freelist 变成空指针,所有对象被分配出去,已经不需要空闲对象链表,如图 3.29 所示。


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


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


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


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


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


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

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


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


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


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


3.计算 slab 长度


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

1)计算对象数量和剩余长度。


2)如果对象数量是 0,那么不合适。


3)如果对象数量大于允许的最大 slab 对象数量,那么不合适。允许的最大 slab 对象数量SLAB_OBJ_MAX_NUM,等于(× 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”指定。

4.着色


slab 是一个或多个连续的物理页,起始地址总是页长度的整数倍,不同 slab 中相同偏移的位置在处理器的一级缓存中的索引相同。如果 slab 的剩余部分的长度超过一级缓存行的长度,剩余部分对应的一级缓存行没有被利用;如果对象的填充字节的长度超过一级缓存行的长度,填充字节对应的一级缓存行没有被利用。这两种情况导致处理器的某些缓存行被过度使用,另一些缓存行很少使用。


slab 的剩余部分的长度超过一级缓存行长度的情况下,为了均匀利用处理器的所有一级缓存行,slab 着色(slab coloring)利用 slab 的剩余部分,使不同 slab 的第一个对象的偏移不同。

着色是一个比喻,和颜色无关,只是表示 slab 中的第一个对象需要移动一个偏移值,使对象放到不同的一级缓存行里。


内存缓存中着色相关的成员如下。


1kmem_cache.colour_off 是颜色偏移,等于处理器的一级缓存行的长度,如果小于对齐值,那么取对齐值。

2kmem_cache.colour 是着色范围,等于(slab 的剩余长度/颜色偏移)。

192 3.8 块分配器

3kmem_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 的颜色偏移)。

5.每处理器数组缓存


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


1)刚释放的对象很可能还在处理器的缓存中,可以更好地利用处理器的缓存。

2)减少链表操作。

3)避免处理器之间的互斥,减少自旋锁操作。


结构体 array_cache 如下。


1)成员 entry 是存放对象地址的数组。

2)成员 avail 是数组存放的对象的数量。

3)成员 limit 是数组的大小,和结构体 kmem_cache 的成员 limit 的值相同,是根据对象长度猜测的一个值。

4)成员 batchcount 是批量值,和结构体 kmem_cache 的成员 batchcount 的值相同,批量值是数组大小的一半。


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

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

6.对 NUMA 的支持


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


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

7.内存缓存合并


为了减少内存开销和增加对象的缓存热度,块分配器会合并相似的内存缓存。在创建内存缓存的时候,从已经存在的内存缓存中找到一个相似的内存缓存,和原始的创建者共享这个内存缓存。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 合并。


7SLAB 分配器特有的检查项:如果 t 的对齐值不是 0,并且 t 的对齐值大于 s 的对齐值,或者 s 的对齐值不是 t 的对齐值的整数倍,那么 t 不能和 s 合并。

8)顺利通过前面 7 项检查,说明 t s 可以合并。


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


8.回收内存


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

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

节点 n 的空闲对象的数量限制 = 1 + 节点的处理器数量)* kmem_cache.batchcount +kmem_cache.num

SLAB 分配器定期回收对象和空闲 slab,实现方法是在每个处理器上向全局工作队列添加 1 个延迟工作项,工作项的处理函数是 cache_reap

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


1)回收节点 n(假设当前处理器属于节点 n)对应的远程节点数组缓存中的对象。

2)如果过去 2 秒没有从当前处理器的数组缓存分配对象,那么回收数组缓存中的对象。


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

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

2)如果过去 4 秒没有从空闲 slab 分配对象,那么回收空闲 slab


9.调试


出现内存改写时,我们需要定位出是谁改写。SLAB 分配器提供了调试功能,我们可以打开调试配置宏 CONFIG_DEBUG_SLAB,此时对象增加 3 个字段:红色区域 1、红色区域 2 和最后一个使用者,如图 3.36 所示。

分配对象时,把对象毒化:把最后 1 字节以外的每个字节设置为 0x5a,把最后一个字节设置为 0xa5;把对象前后的红色区域设置为宏 RED_ACTIVE 表示的魔幻数;字段“最后一个使用者”保存调用函数的地址。


释放对象时,检查对象:如果对象前后的红色区域都是宏 RED_ACTIVE 表示的魔幻数,说明正常;如果对象前后的红色区域都是宏 RED_INACTIVE 表示的魔幻数,说明重复释放;其他情况,说明写越界。


释放对象时,把对象毒化:把最后 1 字节以外的每个字节设置为 0x6b,把最后 1 字节设置为 0xa5;把对象前后的红色区域都设置为 RED_INACTIVE,字段“最后一个使用者”保存调用函数的地址。


再次分配对象时,检查对象:如果对象不符合“最后 1 字节以外的每个字节是 0x6b最后 1 字节是 0xa5”,说明对象被改写;如果对象前后的红色区域不是宏 RED_INACTIVE表示的魔幻数,说明重复释放或者写越界。


3.8.3 SLUB 分配器


SLUB 分配器继承了 SLAB 分配器的核心思想,在某些地方做了改进。


1SLAB 分配器的管理数据结构开销大,早期每个 slab 有一个描述符和跟在后面的空闲对象数组。SLUB 分配器把 slab 的管理信息保存在 page 结构体中,使用联合体重用 page

结构体的成员,没有使 page 结构体的大小增加。现在 SLAB 分配器反过来向 SLUB 分配器学习,抛弃了 slab 描述符,把 slab 的管理信息保存在 page 结构体中。

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

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

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


1.数据结构


SLUB 分配器内存缓存的数据结构如图 3.37 所示。

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

成员 size 是包括元数据的对象长度,成员 object_size 是对象原始长度。

成员 oo 存放最优 slab 的阶数和对象数,低 16 位是对象数,高 16 位是 slab 的阶数,oo 等于((slab 的阶数 << 16| 对象数)。最优 slab 是剩余部分最小的 slab

成员 min 存放最小 slab 的阶数和对象数,格式和 oo 相同。最小 slab 只需要足够存放一个对象。当设备长时间运行以后,内存碎片化,分配连续物理页很难成功,如果分配最slab 失败,就分配最小 slab

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

链表 partial 把部分空闲的 slab 链接起来,成员 nr_partial 是部分空闲 slab 的数量。


3)每个 slab 由一个或多个连续的物理页组成,页的阶数是最优 slab 或最小 slab 的阶3 章 内存管理数,如果阶数大于 0,组成一个复合页。

slab 被划分为多个对象,如果 slab 长度不是对象长度的整数倍,尾部有剩余部分。尾部也可能有保留部分,kmem_cache 实例的成员 reserved 存放保留长度。



在创建内存缓存的时候,如果指定标志位 SLAB_TYPESAFE_BY_RCU,要求使用 RCU延迟释放 slab,在调用函数 call_rcu 把释放 slab 的函数加入 RCU 回调函数队列的时候,需要提供一个 rcu_head 实例,slab 提供的 rcu_head 实例的位置分两种情况。

1)如果 page 结构体的成员 lru 的长度大于或等于 rcu_head 结构体的长度,那么重用成员 lru

2)如果 page 结构体的成员 lru 的长度小于 rcu_head 结构体的长度,那么必须在 slab尾部为 rcu_head 结构体保留空间,保留长度是 rcu_head 结构体的长度。

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

1)成员 flags 设置标志位 PG_slab,表示页属于 SLUB 分配器。

2)成员 freelist 指向第一个空闲对象。

3)成员 inuse 表示已分配对象的数量。

4)成员 objects 是对象数量。

5)成员 frozen 表示 slab 是否被冻结在每处理器 slab 缓存中。如果 slab 在每处理器 slab缓存中,它处于冻结状态;如果 slab 在内存节点的部分空闲 slab 链表中,它处于解冻状态。

6)成员 lru 作为链表节点加入部分空闲 slab 链表。

7)成员 slab_cache 指向 kmem_cache 实例。


4kmem_cache 实例的成员 cpu_slab 指向 kmem_cache_cpu 实例,每个处理器对应一kmem_cache_cpu 实例,称为每处理器 slab 缓存。

SLAB 分配器的每处理器数组缓存以对象为单位,而 SLUB 分配器的每处理器 slab 存以 slab 为单位。

成员 freelist 指向当前使用的 slab 的空闲对象链表,成员 page 指向当前使用的 slab 应的 page 实例,成员 partial 指向每处理器部分空闲 slab 链表。

对象有两种内存布局,区别是空闲指针的位置不同。

第一种内存布局如图 3.38 所示,空闲指针在红色区域 2 的后面。



第二种内存布局如图 3.39 所示,空闲指针重用真实对象的第一个字。



kmem_cache.offset 是空闲指针的偏移,空闲指针的地址等于(真实对象的地址 + 空闲指针偏移)。


红色区域 1 的长度 = kmem_cache.red_left_pad = 字长对齐到指定的对齐值

红色区域 2 的长度 = 字长 (对象长度 % 字长)


当开启 SLUB 分配器的调试配置宏 CONFIG_SLUB_DEBUG 的时候,对象才包含红色区域 1、红色区域 2、分配用户跟踪和释放用户跟踪这 4 个成员。

以下 3 种情况下选择第一种内存布局。

1)指定构造函数。

2)指定标志位 SLAB_TYPESAFE_BY_RCU,要求使用 RCU 延迟释放 slab

3)指定标志位 SLAB_POISON,要求毒化对象。

其他情况下使用第二种内存布局。


2.空闲对象链表

以对象使用第一种内存布局为例说明,一个 slab 的空闲对象链表的初始状态如图 3.40所示,page->freelist 指向第一个空闲对象中的真实对象,前一个空闲对象中的空闲指针指向后一个空闲对象中的真实对象,最后一个空闲对象中的空闲指针是空指针。如果打开了SLAB 空闲链表随机化的配置宏 CONFIG_SLAB_FREELIST_RANDOM,每个对象在空闲对象链表中的位置是随机的。



分配一个对象以后,page->freelist 指向下一个空闲对象中的真实对象,空闲对象链表如图 3.41 所示。


3.计算 slab 长度


SLUB 分配器在创建内存缓存的时候计算了两种 slab 长度:最优 slab 和最小 slab。最slab 是剩余部分比例最小的 slab,最小 slab 只需要足够存放一个对象。当设备长时间运行以后,内存碎片化,分配连续物理页很难成功,如果分配最优 slab 失败,就分配最小 slab

计算最优 slab 的长度时,有 3 个重要的控制参数。

1slub_min_objectsslab 的最小对象数量,默认值是 0,可以在引导内核时使用内核参数“slub_min_objects”设置。

2slub_min_orderslab 的最小阶数,默认值是 0,可以在引导内核时使用内核参数slub_min_order”设置。

3slub_max_orderslab 的最大阶数,默认值是页分配器认为昂贵的阶数 3,可以在引导内核时使用内核参数“slub_max_order”设置。

函数 calculate_order 负责计算最优 slab 的长度,其算法如下:



函数 slab_order 负责计算阶数,输入参数是(对象长度 size,最小对象数量 min_objects最大阶数 max_order,剩余部分比例 fraction,保留长度 reserved),其算法如下:

4.每处理器 slab 缓存


SLAB分配器的每处理器缓存以对象为单位,而SLUB分配器的每处理器缓存以slab为单位。

如图 3.42 所示,内存缓存为每个处理器创建了一个 slab 缓存。



1)使用结构体 kmem_cache_cpu 描述 slab 缓存,成员 page 指向当前使用的 slab 对应page 实例,成员 freelist 指向空闲对象链表,成员 partial 指向部分空闲 slab 链表。

2)当前使用的 slab 对应的 page 实例:成员 frozen 的值为 1,表示当前 slab 被冻结在每处理器 slab 缓存中;成员 freelist 被设置为空指针。

3)部分空闲 slab 链表:只有打开配置宏 CONFIG_SLUB_CPU_PARTIAL,才会使用部分空闲 slab 链表(如果打开了调试配置宏 CONFIG_SLUB_DEBUG,还要求没有设置 slab调试标志位),目前默认打开了这个配置宏。为了和内存节点的空闲 slab 链表区分,我们把每处理器 slab 缓存中的空闲 slab 链表称为每处理器空闲 slab 链表。

链表中每个 slab 对应的 page 实例的成员 frozen 的值为 1,表示 slab 被冻结在每处理器slab 缓存中;成员 next 指向下一个 slab 对应的 page 实例。

链表中第一个 slab 对应的 page 实例的成员 pages 存放链表中 slab 的数量,成员 pobjects存放链表中空闲对象的数量;后面的 slab 没有使用这两个成员。

kmem_cache 实例的成员 cpu_partial 决定了链表中空闲对象的最大数量,是根据对象长度估算的值。


分配对象时,首先从当前处理器的 slab 缓存分配,如果当前有一个 slab 正在使用并且有空闲对象,那么分配一个对象;如果 slab 缓存中的部分空闲 slab 链表不是空的,那么取

第一个 slab 作为当前使用的 slab;其他情况下,需要重填当前处理器的 slab 缓存。


1)如果内存节点的部分空闲 slab 链表不是空的,那么取第一个 slab 作为当前使用的slab,并且重填 slab 缓存中的部分空闲 slab 链表,直到取出的所有 slab 的空闲对象总数超过限制 kmem_cache.cpu_partial 的一半为止。

2)否则,创建一个新的 slab,作为当前使用的 slab


什么情况下会把 slab 放到每处理器部分空闲 slab 链表中?

释放对象的时候,如果对象所属的 slab 以前没有空闲对象,并且没有冻结在每处理器slab 缓存中,那么把 slab 放到当前处理器的部分空闲 slab 链表中。如果发现当前处理器的部分空闲 slab 链表中空闲对象的总数超过限制 kmem_cache.cpu_partial,先把链表中的所有slab 归还到内存节点的部分空闲 slab 链表中。

这种做法的好处是:把空闲对象非常少的 slab 放在每处理器空闲 slab 链表中,优先从空闲对象非常少的 slab 分配对象,减少内存浪费。

5.对 NUMA 的支持


我们看看 SLUB 分配器怎么支持 NUMA 系统。


1)内存缓存针对每个内存节点创建一个 kmem_cache_node 实例。

2)分配对象时,如果当前处理器的 slab 缓存是空的,需要重填当前处理器的 slab 存。首先从本地内存节点的部分空闲 slab 链表中取 slab,如果本地内存节点的部分空闲 slab链表是空的,那么从其他内存节点的部分空闲 slab 链表借用 slab


kmem_cache 实例的成员 remote_node_defrag_ratio 称为远程节点反碎片比例,用来控制从远程节点借用部分空闲 slab 和从本地节点取部分空闲 slab 的比例,值越小,从本地节点取部分空闲 slab 的倾向越大。默认值是 1000,可以通过文件“/sys/kernel/slab/<内存缓存名>/remote_node_defrag_ratio”设置某个内存缓存的远程节点反碎片比例,用户设置的范围[0, 100],内存缓存保存的比例值是乘以 10 以后的值。

函数 get_any_partial 负责从其他内存节点借用部分空闲 slab,算法如下:


6.回收内存


对于所有对象空闲的 slab,如果内存节点的部分空闲 slab 的数量大于或等于最小部分空闲 slab 数量,那么直接释放,否则放在部分空闲 slab 链表的尾部。

最小部分空闲 slab 数量 kmem_cache.min_partial 的计算方法是:(log2 对象长度)/2,并且把限制在范围[5,10]

7.调试


如果我们需要使用 SLUB 分配器的调试功能,首先需要打开调试配置宏 CONFIG_DEBUG_SLUB,然后有如下两种选择。

1)打开配置宏 CONFIG_SLUB_DEBUG_ON,为所有内存缓存打开所有调试选项。

2)在引导内核时使用内核参数“slub_debug”。

slub_debug=<调试选项> 为所有内存缓存打开调试选项slub_debug=<调试选项>,<内存缓存名称> 只为指定的内存缓存打开调试选项

调试选项如下所示。


1F:在分配和释放时执行昂贵的一致性检查(对应标志位 SLAB_CONSISTENCY_CHECKS

2Z:红色区域(对应标志位 SLAB_RED_ZONE

3P:毒化对象(对应标志位 SLAB_POISON

4U:分配/释放用户跟踪(对应标志位 SLAB_STORE_USER

5T:跟踪分配和释放(对应标志位 SLAB_TRACE),只在一个内存缓存上使用。

6A:注入分配对象失败的错误(对应标志位 SLAB_FAILSLAB,需要打开配置宏CONFIG_FAILSLAB

7O:为可能导致更高的最小 slab 阶数的内存缓存关闭调试。

8-:关闭所有调试选项,在内核配置了 CONFIG_SLUB_DEBUG_ON 时有用处。

如果没有指定调试选项(即“slub_debug=”),表示打开所有调试选项。


3.8.4 SLOB 分配器


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


1.数据结构

SLOB 分配器内存缓存的数据结构如图 3.43 所示。


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

成员 object_size 是对象原始长度,成员 size 是包括填充的对象长度,align 是对齐值。

2)所有内存缓存共享 slab,所有对象长度小于 256 字节的内存缓存共享小对象 slab链表中的 slab,所有对象长度小于 1024 字节的内存缓存共享中等对象 slab 链表中的 slab所有对象长度小于 1 页的内存缓存共享大对象 slab 链表中的 slab。对象长度大于或等于 1页的内存缓存,直接从页分配器分配页,不需要经过 SLOB 分配器。

每个 slab 的长度是一页,page 结构体的相关成员如下。

1)成员 flags 设置标志位 PG_slab,表示页属于 SLOB 分配器;设置标志位 PG_slob_free表示 slab slab 链表中。

2)成员 freelist 指向第一个空闲对象。

3)成员 units 表示空闲单元的数量。

4)成员 lru 作为链表节点加入 slab 链表。

SLOB 分配器的分配粒度是单元,也就是说,分配长度必须是单元的整数倍,单元是数据类型 slobidx_t 的长度,通常是 2 字节。数据类型 slobidx_t 的定义如下:


mm/slob.c#if PAGE_SIZE <= (32767 * 2)typedef s16 slobidx_t;#elsetypedef s32 slobidx_t;#endif

2.空闲对象链表


我们看看 SLOB 分配器怎么组织空闲对象。在 SLOB 分配器中,对象更准确的说法是块(block),因为多个对象长度不同的内存缓存可能从同一个 slab 分配对象,一个 slab 能出现大小不同的块。


空闲块的内存布局分为如下两种情况。

1)对于长度大于一个单元的块,第一个单元存放块长度,第二个单元存放下一个空闲块的偏移。

2)对于只有一个单元的块,该单元存放下一个空闲块的偏移的相反数,也就是说,是一个负数。


长度和偏移都是单元数量,偏移的基准是页的起始地址。


已经分配出去的块:如果是使用 kmalloc()从通用内存缓存分配的块,使用块前面的 4字节存放申请的字节数,因为使用 kfree()释放时需要知道块的长度。如果是从专用内存缓存分配的块,从 kmem_cache 结构体的成员 size 可以知道块的长度。

假设页长度是 4KB,单元是 2 字节,一个 slab 的空闲对象链表的初始状态如图 3.44 所示。

slab 只有一个空闲块,第一个单元存放长度 2048,第二个单元存放下一个空闲块的偏移 2048

slab 对应的 page 结构体的成员 freelist 指向第一个空闲块,成员 units 存放空闲单元数量 2048



假设一个对象长度是 32 字节的内存缓存从这个 slab 分配了一个对象,空闲对象链表如3.45 所示,slab 的前面 32 字节被分配,空闲块从第 32 字节开始,第一个单元存放长度2032,第二个单元存放下一个空闲块的偏移 2048slab 对应的 page 结构体的成员 freelist指向这个空闲块,成员 units 存放空闲单元数量 2032



3.分配对象


分配对象时,根据对象长度选择不同的策略。


1)如果对象长度小于 256 字节,那么从小对象 slab 链表中查找 slab 分配。

2)如果对象长度小于 1024 字节,那么从中等对象 slab 链表中查找 slab 分配。

3)如果对象长度小于 1 页,那么从大对象 slab 链表中查找 slab 分配。

4)如果对象长度大于或等于 1 页,那么直接从页分配器分配页,不需要经过 SLOB分配器。

对于前面 3 种情况,遍历 slab 链表,对于空闲单元数量(page.units)大于或等于对象长度的 slab,遍历空闲对象链表,当找到一个合适的空闲块时,处理方法是:如果空闲块的长度等于对象长度,那么把这个空闲块从空闲对象链表中删除;如果空闲块的长度大于对象长度,那么把这个空闲块分裂为两部分,一部分分配出去,剩下部分放在空闲对象链表中。


如果分配对象以后,slab 的空闲单元数量变成零,那么从 slab 链表中删除,并且清除标志位 PG_slob_free

为了减少平均查找时间,从某个 slab 分配对象以后,把 slab 链表的头节点移到这个 slab的前面,下一次分配对象的时候从这个 slab 开始查找。

如果遍历完 slab 链表,没有找到合适的空闲块,那么创建新的 slab

感兴趣想进群者可以添加小月微信(linuxer2016)邀请进群一起交流学习



连载已发布文章:

《Linux内核深度解析》选载之伙伴分配器

《Linux内核深度解析》选载之引导内存分配器

《Linux内核深度解析》选载之物理内存组织

《Linux内核深度解析》选载之内存映射

《Linux内核深度解析》选载之内存地址空间


精华文章:【精华】Linux阅码场原创精华文章汇总


Linux阅码场 专业的Linux技术社区和Linux操作系统学习平台,内容涉及Linux内核,Linux内存管理,Linux进程管理,Linux文件系统和IO,Linux性能调优,Linux设备驱动以及Linux虚拟化和云计算等各方各面.
评论
  • 戴上XR眼镜去“追龙”是种什么体验?2024年11月30日,由上海自然博物馆(上海科技馆分馆)与三湘印象联合出品、三湘印象旗下观印象艺术发展有限公司(下简称“观印象”)承制的《又见恐龙》XR嘉年华在上海自然博物馆重磅开幕。该体验项目将于12月1日正式对公众开放,持续至2025年3月30日。双向奔赴,恐龙IP撞上元宇宙不久前,上海市经济和信息化委员会等部门联合印发了《上海市超高清视听产业发展行动方案》,特别提到“支持博物馆、主题乐园等场所推动超高清视听技术应用,丰富线下文旅消费体验”。作为上海自然
    电子与消费 2024-11-30 22:03 93浏览
  •         温度传感器的精度受哪些因素影响,要先看所用的温度传感器输出哪种信号,不同信号输出的温度传感器影响精度的因素也不同。        现在常用的温度传感器输出信号有以下几种:电阻信号、电流信号、电压信号、数字信号等。以输出电阻信号的温度传感器为例,还细分为正温度系数温度传感器和负温度系数温度传感器,常用的铂电阻PT100/1000温度传感器就是正温度系数,就是说随着温度的升高,输出的电阻值会增大。对于输出
    锦正茂科技 2024-12-03 11:50 97浏览
  • 作为优秀工程师的你,已身经百战、阅板无数!请先醒醒,新的项目来了,这是一个既要、又要、还要的产品需求,ARM核心板中一个处理器怎么能实现这么丰富的外围接口?踌躇之际,你偶阅此文。于是,“潘多拉”的魔盒打开了!没错,USB资源就是你打开新世界得钥匙,它能做哪些扩展呢?1.1  USB扩网口通用ARM处理器大多带两路网口,如果项目中有多路网路接口的需求,一般会选择在主板外部加交换机/路由器。当然,出于成本考虑,也可以将Switch芯片集成到ARM核心板或底板上,如KSZ9897、
    万象奥科 2024-12-03 10:24 53浏览
  • RDDI-DAP错误通常与调试接口相关,特别是在使用CMSIS-DAP协议进行嵌入式系统开发时。以下是一些可能的原因和解决方法: 1. 硬件连接问题:     检查调试器(如ST-Link)与目标板之间的连接是否牢固。     确保所有必要的引脚都已正确连接,没有松动或短路。 2. 电源问题:     确保目标板和调试器都有足够的电源供应。     检查电源电压是否符合目标板的规格要求。 3. 固件问题: &n
    丙丁先生 2024-12-01 17:37 91浏览
  • 11-29学习笔记11-29学习笔记习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记
    youyeye 2024-12-02 23:58 59浏览
  • 当前,智能汽车产业迎来重大变局,随着人工智能、5G、大数据等新一代信息技术的迅猛发展,智能网联汽车正呈现强劲发展势头。11月26日,在2024紫光展锐全球合作伙伴大会汽车电子生态论坛上,紫光展锐与上汽海外出行联合发布搭载紫光展锐A7870的上汽海外MG量产车型,并发布A7710系列UWB数字钥匙解决方案平台,可应用于数字钥匙、活体检测、脚踢雷达、自动泊车等多种智能汽车场景。 联合发布量产车型,推动汽车智能化出海紫光展锐与上汽海外出行达成战略合作,联合发布搭载紫光展锐A7870的量产车型
    紫光展锐 2024-12-03 11:38 88浏览
  • 最近几年,新能源汽车愈发受到消费者的青睐,其销量也是一路走高。据中汽协公布的数据显示,2024年10月,新能源汽车产销分别完成146.3万辆和143万辆,同比分别增长48%和49.6%。而结合各家新能源车企所公布的销量数据来看,比亚迪再度夺得了销冠宝座,其10月新能源汽车销量达到了502657辆,同比增长66.53%。众所周知,比亚迪是新能源汽车领域的重要参与者,其一举一动向来为外界所关注。日前,比亚迪汽车旗下品牌方程豹汽车推出了新车方程豹豹8,该款车型一上市就迅速吸引了消费者的目光,成为SUV
    刘旷 2024-12-02 09:32 107浏览
  • 遇到部分串口工具不支持1500000波特率,这时候就需要进行修改,本文以触觉智能RK3562开发板修改系统波特率为115200为例,介绍瑞芯微方案主板Linux修改系统串口波特率教程。温馨提示:瑞芯微方案主板/开发板串口波特率只支持115200或1500000。修改Loader打印波特率查看对应芯片的MINIALL.ini确定要修改的bin文件#查看对应芯片的MINIALL.ini cat rkbin/RKBOOT/RK3562MINIALL.ini修改uart baudrate参数修改以下目
    Industio_触觉智能 2024-12-03 11:28 66浏览
  • 《高速PCB设计经验规则应用实践》+PCB绘制学习与验证读书首先看目录,我感兴趣的是这一节;作者在书中列举了一条经典规则,然后进行详细分析,通过公式推导图表列举说明了传统的这一规则是受到电容加工特点影响的,在使用了MLCC陶瓷电容后这一条规则已经不再实用了。图书还列举了高速PCB设计需要的专业工具和仿真软件,当然由于篇幅所限,只是介绍了一点点设计步骤;我最感兴趣的部分还是元件布局的经验规则,在这里列举如下:在这里,演示一下,我根据书本知识进行电机驱动的布局:这也算知行合一吧。对于布局书中有一句:
    wuyu2009 2024-11-30 20:30 116浏览
  • 概述 说明(三)探讨的是比较器一般带有滞回(Hysteresis)功能,为了解决输入信号转换速率不够的问题。前文还提到,即便使能滞回(Hysteresis)功能,还是无法解决SiPM读出测试系统需要解决的问题。本文在说明(三)的基础上,继续探讨为SiPM读出测试系统寻求合适的模拟脉冲检出方案。前四代SiPM使用的高速比较器指标缺陷 由于前端模拟信号属于典型的指数脉冲,所以下降沿转换速率(Slew Rate)过慢,导致比较器检出出现不必要的问题。尽管比较器可以使能滞回(Hysteresis)模块功
    coyoo 2024-12-03 12:20 86浏览
  • 光伏逆变器是一种高效的能量转换设备,它能够将光伏太阳能板(PV)产生的不稳定的直流电压转换成与市电频率同步的交流电。这种转换后的电能不仅可以回馈至商用输电网络,还能供独立电网系统使用。光伏逆变器在商业光伏储能电站和家庭独立储能系统等应用领域中得到了广泛的应用。光耦合器,以其高速信号传输、出色的共模抑制比以及单向信号传输和光电隔离的特性,在光伏逆变器中扮演着至关重要的角色。它确保了系统的安全隔离、干扰的有效隔离以及通信信号的精准传输。光耦合器的使用不仅提高了系统的稳定性和安全性,而且由于其低功耗的
    晶台光耦 2024-12-02 10:40 118浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦