0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

基于Linux解决SLAB不足的SLUB分配器解决方案

454398 来源:蜗窝科技 作者:itrocker 2020-09-30 15:09 次阅读

Linux的物理内存管理采用了以页为单位的buddy system(伙伴系统),但是很多情况下,内核仅仅需要一个较小的对象空间,而且这些小块的空间对于不同对象又是变化的、不可预测的,所以需要一种类似用户空间堆内存的管理机制(malloc/free)。然而内核对对象的管理又有一定的特殊性,有些对象的访问非常频繁,需要采用缓冲机制;对象的组织需要考虑硬件cache的影响;需要考虑多处理器以及NUMA架构的影响。90年代初期,在Solaris 2.4操作系统中,采用了一种称为“slab”(原意是大块的混凝土)的缓冲区分配和管理方法,在相当程度上满足了内核的特殊需求。

多年以来,SLAB成为linux kernel对象缓冲区管理的主流算法,甚至长时间没有人愿意去修改,因为它实在是非常复杂,而且在大多数情况下,它的工作完成的相当不错。但是,随着大规模多处理器系统和 NUMA系统的广泛应用,SLAB 分配器逐渐暴露出自身的严重不足:

1)。 缓存队列管理复杂;

2)。 管理数据存储开销大;

3)。 对NUMA支持复杂;

4)。 调试调优困难;

5)。 摒弃了效果不太明显的slab着色机制;

针对这些SLAB不足,内核开发人员Christoph Lameter在Linux内核2.6.22版本中引入一种新的解决方案:SLUB分配器。SLUB分配器特点是简化设计理念,同时保留SLAB分配器的基本思想:每个缓冲区由多个小的slab 组成,每个 slab 包含固定数目的对象。SLUB分配器简化kmem_cache,slab等相关的管理数据结构,摒弃了SLAB 分配器中众多的队列概念,并针对多处理器、NUMA系统进行优化,从而提高了性能和可扩展性并降低了内存的浪费。为了保证内核其它模块能够无缝迁移到SLUB分配器,SLUB还保留了原有SLAB分配器所有的接口API函数。

(注:本文源码分析基于linux-4.1.x)

整体数据结构关系如下图所示:

1 SLUB分配器的初始化

SLUB初始化有两个重要的工作:第一,创建用于申请struct kmem_cache和struct kmem_cache_node的kmem_cache;第二,创建用于常规kmalloc的kmem_cache。

1.1 申请kmem_cache的kmem_cache

第一个工作涉及到一个“先有鸡还是先有蛋”的问题,因为创建kmem_cache需要从kmem_cache的缓冲区申请,而这时候还没有创建kmem_cache的缓冲区。kernel的解决办法是先用两个静态变量boot_kmem_cache和boot_kmem_cache_node来保存struct kmem_cach和struct kmem_cache_node缓冲区管理数据,以两个静态变量为基础申请大小为struct kmem_cache和struct kmem_cache_node对象大小的slub缓冲区,随后再从这些缓冲区中分别申请两个kmem_cache,然后把boot_kmem_cache和boot_kmem_cache_node中的内容拷贝到新申请的对象中,从而完成了struct kmem_cache和struct kmem_cache_node管理结构的bootstrap(自引导)。

void __init kmem_cache_init(void)

{

//声明静态变量,存储临时kmem_cache管理结构

static __initdata struct kmem_cache boot_kmem_cache,

boot_kmem_cache_node;

•••

kmem_cache_node = &boot_kmem_cache_node;

kmem_cache = &boot_kmem_cache;

//申请slub缓冲区,管理数据放在临时结构中

create_boot_cache(kmem_cache_node, “kmem_cache_node”,

sizeof(struct kmem_cache_node), SLAB_HWCACHE_ALIGN);

create_boot_cache(kmem_cache, “kmem_cache”,

offsetof(struct kmem_cache, node) +

nr_node_ids * sizeof(struct kmem_cache_node *),

SLAB_HWCACHE_ALIGN);

//从刚才挂在临时结构的缓冲区中申请kmem_cache的kmem_cache,并将管理数据拷贝到新申请的内存中

kmem_cache = bootstrap(&boot_kmem_cache);

//从刚才挂在临时结构的缓冲区中申请kmem_cache_node的kmem_cache,并将管理数据拷贝到新申请的内存中

kmem_cache_node = bootstrap(&boot_kmem_cache_node);

•••

}

1.2 创建kmalloc常规缓存

原则上系统会为每个2次幂大小的内存块申请一个缓存,但是内存块过小时,会产生很多碎片浪费,所以系统为96B和192B也各自创建了一个缓存。于是利用了一个size_index数组来帮助确定小于192B的内存块使用哪个缓存

void __init create_kmalloc_caches(unsigned long flags)

{

•••

/*使用SLUB时KMALLOC_SHIFT_LOW=3,KMALLOC_SHIFT_HIGH=13

也就是说使用kmalloc能够申请的最小内存是8B,最大内存是8KB

申请内存是向上对其2的n次幂,创建对应大小缓存保存在kmalloc_caches [n]*/

for (i = KMALLOC_SHIFT_LOW; i 《= KMALLOC_SHIFT_HIGH; i++) {

if (!kmalloc_caches[i]) {

kmalloc_caches[i] = create_kmalloc_cache(NULL,

1 《《 i, flags);

}

/*

* Caches that are not of the two-to-the-power-of size.

* These have to be created immediately after the

* earlier power of two caches

*/

/*有两个例外,大小为64~96B和128B~192B,单独创建了两个缓存

保存在kmalloc_caches [1]和kmalloc_caches [2]*/

if (KMALLOC_MIN_SIZE 《= 32 && !kmalloc_caches[1] && i == 6)

kmalloc_caches[1] = create_kmalloc_cache(NULL, 96, flags);

if (KMALLOC_MIN_SIZE 《= 64 && !kmalloc_caches[2] && i == 7)

kmalloc_caches[2] = create_kmalloc_cache(NULL, 192, flags);

}

•••

}

2 缓存的创建与销毁

2.1 缓存的创建

创建缓存通过接口kmem_cache_create进行,在创建新的缓存以前,尝试找到可以合并的缓存,合并条件包括对对象大小以及缓存属性的判断,如果可以合并则直接返回已存在的kmem_cache,并创建一个kobj链接指向同一个节点。

创建新的缓存主要是申请管理结构暂用的空间,并初始化,这些管理结构包括kmem_cache、kmem_cache_nodes、kmem_cache_cpu。同时在sysfs创建kobject节点。最后把kmem_cache加入到全局cahce链表slab_caches中。

2.2 缓存的销毁

销毁过程比创建过程简单的多,主要工作是释放partial队列所有page,释放kmem_cache_cpu,释放每个node的kmem_cache_node,最后释放kmem_cache本身。

3 申请对象

对象是SLUB分配器中可分配的内存单元,与SLAB相比,SLUB对象的组织非常简洁,申请过程更加高效。SLUB没有任何管理区结构来管理对象,而是将对象之间的关联嵌入在对象本身的内存中,因为申请者并不关心对象在分配之前内存的内容是什么。而且各个SLUB之间的关联,也利用page自身结构进行处理。

每个CPU都有一个slab作为本地高速缓存,只要slab所在的node与申请者要求的node匹配,同时该slab还有空闲对象,则直接在cpu_slab中取出空闲对象,否则就进入慢速路径。

每个对象内存的offset偏移位置都存放着下一个空闲对象,offset通常为0,也就是复用对象内存的第一个字来保存下一个空闲对象的指针,当满足条件(flags & (SLAB_DESTROY_BY_RCU | SLAB_POISON)) 或者有对象构造函数时,offset不为0,每个对象的结构如下图。

cpu_slab的freelist则保存着当前第一个空闲对象的地址。

如果本地CPU缓存没有空闲对象,则申请新的slab;如果有空闲对象,但是内存node不相符,则deactive当前cpu_slab,再申请新的slab。

deactivate_slab主要进行两步工作:

第一步,将cpu_slab的freelist全部释放回page-》freelist;

第二部,根据page(slab)的状态进行不同操作,如果该slab有部分空闲对象,则将page移到kmem_cache_node的partial队列;如果该slab全部空闲,则直接释放该slab;如果该slab全部占用,而且开启了CONFIG_SLUB_DEBUG编译选项,则将page移到full队列。

page的状态也从frozen改变为unfrozen。(frozen代表slab在cpu_slub,unfroze代表在partial队列或者full队列)。

申请新的slab有两种情况,如果cpu_slab的partial队列不为空,则取出队列中下一个page作为新的cpu_slab,同时再次检测内存node是否相符,还不相符则循环处理。

如果cpu_slab的partial队列为空,则查看本node的partial队列是否为空,如果不空,则取出page;如果为空,则看一定距离范围内其它node的partial队列,如果还为空,则需要创建新slab。

创建新slab其实就是申请对应order的内存页,用来放足够数量的对象。值得注意的是其中order以及对象数量的确定,这两者又是相互影响的。order和object数量同时存放在kmem_cache成员kmem_cache_order_objects中,低16位用于存放object数量,高位存放order。order与object数量的关系非常简单:((PAGE_SIZE 《《 order) - reserved) / size。

下面重点看calculate_order这个函数

static inline int calculate_order(int size, int reserved)

{

•••

//尝试找到order与object数量的最佳配合方案

//期望的效果就是剩余的碎片最小

min_objects = slub_min_objects;

if (!min_objects)

min_objects = 4 * (fls(nr_cpu_ids) + 1);

max_objects = order_objects(slub_max_order, size, reserved);

min_objects = min(min_objects, max_objects);

//fraction是碎片因子,需要满足的条件是碎片部分乘以fraction小于slab大小

// (slab_size - reserved) % size 《= slab_size / fraction

while (min_objects 》 1) {

fraction = 16;

while (fraction 》= 4) {

order = slab_order(size, min_objects,

slub_max_order, fraction, reserved);

if (order 《= slub_max_order)

return order;

//放宽条件,容忍的碎片大小增倍

fraction /= 2;

}

min_objects--;

}

//尝试一个slab只包含一个对象

order = slab_order(size, 1, slub_max_order, 1, reserved);

if (order 《= slub_max_order)

return order;

//使用MAX_ORDER且一个slab只含一个对象

order = slab_order(size, 1, MAX_ORDER, 1, reserved);

if (order 《 MAX_ORDER)

return order;

return -ENOSYS;

}

4 释放对象

从上面申请对象的流程也可以看出,释放的object有几个去处:

1)cpu本地缓存slab,也就是cpu_slab;

2)放回object所在的page(也就是slab)中;另外要处理所在的slab:

2.1)如果放回之后,slab完全为空,则直接销毁该slab;

2.2)如果放回之前,slab为满,则判断slab是否已被冻结;如果已冻结,则不需要做其他事;如果未冻结,则将其冻结,放入cpu_slab的partial队列;如果cpu_slab partial队列过多,则将队列中所有slab一次性解冻到各自node的partial队列中。

值得注意的是cpu partial队列的功能是个可选项,依赖于内核选项CONFIG_SLUB_CPU_PARTIAL,如果没有开启,则不使用cpu partial队列,直接使用各个node的partial队列。
编辑:hfy

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 处理器
    +关注

    关注

    68

    文章

    19342

    浏览量

    230217
  • cpu
    cpu
    +关注

    关注

    68

    文章

    10878

    浏览量

    212154
  • Linux
    +关注

    关注

    87

    文章

    11319

    浏览量

    209822
  • 内存管理
    +关注

    关注

    0

    文章

    168

    浏览量

    14158
收藏 人收藏

    评论

    相关推荐

    PS2-185/NF带状线2路电源分配器

    PS2-185/NF带状线2路电源分配器PS2-185/NF带状线2路电源分配器具备高可靠性,通过不同种类的结构(如带状线、微带和集总器件方式)来适合各种需求和应用。主要特性电气性能频率范围
    发表于 01-08 09:23

    CDCL1810A 1.8V、10 输出高性能时钟分配器数据表

    电子发烧友网站提供《CDCL1810A 1.8V、10 输出高性能时钟分配器数据表.pdf》资料免费下载
    发表于 08-23 10:08 0次下载
    CDCL1810A 1.8V、10 输出高性能时钟<b class='flag-5'>分配器</b>数据表

    CDCL1810 1.8V 10路输出高性能时钟分配器数据表

    电子发烧友网站提供《CDCL1810 1.8V 10路输出高性能时钟分配器数据表.pdf》资料免费下载
    发表于 08-22 11:14 0次下载
    CDCL1810 1.8V 10路输出高性能时钟<b class='flag-5'>分配器</b>数据表

    CDCE18005高性能时钟分配器数据表

    电子发烧友网站提供《CDCE18005高性能时钟分配器数据表.pdf》资料免费下载
    发表于 08-21 11:12 0次下载
    CDCE18005高性能时钟<b class='flag-5'>分配器</b>数据表

    CDCE62005高性能时钟发生器和分配器数据表

    电子发烧友网站提供《CDCE62005高性能时钟发生器和分配器数据表.pdf》资料免费下载
    发表于 08-21 11:12 0次下载
    CDCE62005高性能时钟发生器和<b class='flag-5'>分配器</b>数据表

    液压分配器起什么作用的

    液压分配器是一种用于控制液压系统中液体流量和压力的设备。它在许多工业和工程应用中发挥着重要作用,例如在液压升降机、液压挖掘机、液压起重机等设备中。以下是液压分配器的主要功能和原理: 流量控制 :液压分配器
    的头像 发表于 07-10 10:56 1012次阅读

    液压分配器工作原理是什么

    液压分配器,又称液压多路阀,是液压系统中的关键部件之一。它的作用是将液压泵输出的油液分配到各个执行机构,实现液压系统的控制和调节。 一、液压分配器的工作原理 液压分配器的基本组成 液压
    的头像 发表于 07-10 10:55 1924次阅读

    液压分配器压力调整方法有哪些

    液压分配器,又称液压分配器或液压分流器,是一种用于液压系统中的设备,主要用于将液压系统中的压力油分配到各个执行元件,以实现对液压系统的控制和调节。 一、液压分配器压力调整的重要性 液压
    的头像 发表于 07-10 10:53 2093次阅读

    单线分配器与双线分配器的区别是什么

    单线分配器与双线分配器是两种不同类型的电子设备,它们在通信、广播、电视等领域中有着广泛的应用。本文将介绍单线分配器与双线分配器的区别。 一、定义 单线
    的头像 发表于 07-10 10:44 947次阅读

    四路数据分配器的基本概念、工作原理、应用场景及设计方法

    四路数据分配器是一种数字电路元件,它的作用是将一个数据输入信号分配成多个数据输出信号。 1. 四路数据分配器的基本概念 四路数据分配器是一种多路复用器(Multiplexer),它将一
    的头像 发表于 07-10 10:42 1821次阅读

    八路数据分配器的基本概念及工作原理

    八路数据分配器是一种常见的电子设备,用于将一个输入信号分配到多个输出端。在本文中,我们将详细介绍八路数据分配器的基本概念、工作原理、应用场景以及设计方法。 一、八路数据分配器的基本概念
    的头像 发表于 07-10 10:40 2176次阅读

    DS90LV110AT 1至10 LVDS数据/时钟分配器数据表

    电子发烧友网站提供《DS90LV110AT 1至10 LVDS数据/时钟分配器数据表.pdf》资料免费下载
    发表于 07-05 11:34 0次下载
    DS90LV110AT 1至10 LVDS数据/时钟<b class='flag-5'>分配器</b>数据表

    Linux内核内存管理之slab分配器

    本文在行文的过程中,会多次提到cache或缓存的概念。如果没有特殊在前面添加硬件的限定词,就说明cache指的是slab分配器使用的软件缓存的意思。如果添加了硬件限定词,则指的是处理器的硬件缓存,比如L1-DCache、L1-ICache之类的。
    的头像 发表于 02-22 09:25 1276次阅读
    <b class='flag-5'>Linux</b>内核内存管理之<b class='flag-5'>slab</b><b class='flag-5'>分配器</b>

    Linux内核内存管理之ZONE内存分配器

    内核中使用ZONE分配器满足内存分配请求。该分配器必须具有足够的空闲页帧,以便满足各种内存大小请求。
    的头像 发表于 02-21 09:29 916次阅读

    请问为什么CAN不使用手动引脚分配器来更改引脚?

    了 Pin28 (P2.8) 使用手动引脚分配器,它起作用了, 然后想把 \" sync2 \" 从 Pin25 (P2.15) 改为 Pin1 (P0.1), 但是在手动引脚分配器
    发表于 01-30 07:24