内存管理机制SLAB

1. 为什么需要内存分配管理?为什么需要SLAB?

  • 在学习c语言时,我们常常会使用到malloc()去申请一块内存空间,用于存放我们的数据,这是代码层面的语言

  • 如果我们想要关心malloc这个命令向系统发出后,系统会做什么呢?系统会给这个变量分配一个内存空间。那么系统是如何分配的呢,这就需要了解系统的内存分配管理方法了。

  • 在linux中,最先推出用于分配内存的管理单元和算法是伙伴分配器(buddy allocator),它是以页为单位管理和分配内存,最小分配一页,也就是4KB 大小。而可能内核的需求只是以字节为单位。

    • 假如我们需要动态申请一个内核结构体(占 20 字节),若仍然分配一页内存,这将严重浪费内存。这也将会导致内部碎片问题
  • 这时候就出现了slab分配器,它专门用于分配小内存,分配内存以字节为单位,基于伙伴分配器的大内存进一步细分成小内存分配。

    • 概括来讲:slab 分配器仍然从buddy分配器中申请内存,之后自己对申请来的内存细分管理。从而达到减少内存碎片化的目的。
    • 形象概括:buddy分配器理解成一个仓库,slab分配器理解为一个商店,仓库给商店进行批发的货物,商店从仓库进货以后,再零售给消费者(使用kmalloc的用户)
      在这里插入图片描述
  • SLAB分配器内存管理机制的核心思想:

    • 提供小内存,减少内存碎片
    • 维护常用对象的缓存
    • 提高CPU硬件缓存的利用率
  • 随着时间的推移,SLAB分配器演变成SLUB和SLOB分配器

2. SLAB底层机制

2.1 基本概念

2.1.1 Slab

  • 是SLAB机制中的基本组成单元,它是预先分配的一块连续的内存区域
  • 每个Slab由一个或多个大小相同的对象组成,这些对象属于同一个Cache。
  • Slab可以处于三种状态之一:满(full),部分满(partial)和空(empty)。
  • 系统优先从部分满的Slab中分配对象,以提高内存利用率。

2.1.2 Slab Cache

  • Slab Cache专门用于管理一种特定大小和类型的对象。
  • 每个Slab Cache都由多个Slab组成,它们共同组成了该类型对象的存储池。
  • 通过Slab Cache,系统可以快速地分配和释放对象,避免了每次分配时都进行昂贵的内存搜索和设置操作。
  • 主要负责三个事情:
    • 对象缓存:缓存常用对象,加快分配速度。
    • 内存管理:根据需要增加或释放Slab,优化内存使用。
    • 碎片最小化:通过维护大小相同的对象集合,减少内存碎片。

2.1.3 缓冲色彩(Cache Coloring)

  • 缓存色彩是一种用于优化CPU缓存利用率的技术。通过对内存分配的微小调整,它能减少不同Slab中对象的缓存行冲突。

2.1.4 构造器和析构器

  • 为了进一步提高效率,SLAB机制允许为每种类型的对象定义构造器(Constructor)和析构器(Destructor)。构造器在对象第一次被创建时被调用,用于初始化对象。析构器在对象最终被释放回系统前被调用,用于执行必要的清理工作。通过这种方式,SLAB机制确保了资源的有效利用和稳定的性能表现。

2.2 结构定义

2.2.1 slab分配的内存大小

  • 问题:Linux中采用4KB大小的页框作为标准的内存分配单元,在实际应用中,经常需要分配一组连续的页框,而频繁的申请和释放不同大小的连续页框,必然导致在已分配页框的内存块中分散了许多小块的空闲页框,这样,即使这些页框是空闲的,其他需要分配连续页框的应用也很难得到满足。
  • Linux内核引入了伙伴系统算法来避免这种情况。其把所有的空闲页框分组为11个块链表,每个块链表分别包含大小为1、2、4、8、16、32、64、128、256、512和1024个连续页框的页框块。最大可以申请1024个连续页框,也即4MB大小的连续空间。
  • 而slab则基于伙伴系统,进一步将页框划分成各个小的内存块,而他的实现则是通过在kmem_cache_init过程中,通过kmalloc_caches[KMALLOC_NORMAL][INDEX_NODE]来建立caches数组
void __init kmem_cache_init(void){
	kmalloc_caches[KMALLOC_NORMAL][INDEX_NODE] = create_kmalloc_cache(
        kmalloc_info[INDEX_NODE].name[KMALLOC_NORMAL],
        kmalloc_info[INDEX_NODE].size,
        ARCH_KMALLOC_FLAGS, 0,
        kmalloc_info[INDEX_NODE].size);
        ...
        create_kmalloc_caches(ARCH_KMALLOC_FLAGS);
}
  • kmalloc_caches[KMALLOC_NORMAL][INDEX_NODE]中的INDEX_NODE即为kmalloc_index,INDEX_NODE取值范围为0-21,分别存放着不同大小的内存caches
static __always_inline unsigned int __kmalloc_index(size_t size, bool size_is_constant){
    /* 0 = zero alloc */
    if (!size)
        return 0;
        
    if (size <= KMALLOC_MIN_SIZE)
        return KMALLOC_SHIFT_LOW;
 
    /* 1 =  65 .. 96 bytes分配65-96bytes的内存大小的块*/
    if (KMALLOC_MIN_SIZE <= 32 && size > 64 && size <= 96)
        return 1;
        
    /* 2 = 129 .. 192 bytes分配65-96bytes的内存大小的块*/
    if (KMALLOC_MIN_SIZE <= 64 && size > 128 && size <= 192)
        return 2;
        
    /* n = 2^(n-1)+1 .. 2^n 分配2^(n-1)+1 .. 2^n大小内存的块内存*/
    if (size <= 8) return 3;
    if (size <= 16) return 4;
    if (size <= 32) return 5;
    if (size <= 64) return 6;
    if (size <= 128) return 7;
    if (size <= 256) return 8;
    if (size <= 512) return 9;
    if (size <= 1024) return 10;
    if (size <= 2 * 1024) return 11;
    if (size <= 4 * 1024) return 12;
    if (size <= 8 * 1024) return 13;
    if (size <= 16 * 1024) return 14;
    if (size <= 32 * 1024) return 15;
    if (size <= 64 * 1024) return 16;
    if (size <= 128 * 1024) return 17;
    if (size <= 256 * 1024) return 18;
    if (size <= 512 * 1024) return 19;
    if (size <= 1024 * 1024) return 20;
    if (size <= 2 * 1024 * 1024) return 21;
 
    if (!IS_ENABLED(CONFIG_PROFILE_ALL_BRANCHES) && size_is_constant)
        BUILD_BUG_ON_MSG(1, "unexpected size in kmalloc_index()");
    else
        BUG();
    return -1;
}
  • 通过create_kmalloc_cache创建出对应的内存区域,且它们对应着0,96byte,192byte,8byte,16byte,32byte,64byte . . . 2M的连续内存空间。

2.2.2 slab分配的内存类型

  • 内存类型则由kmalloc_caches[KMALLOC_NORMAL][INDEX_NODE]中的KMALLOC_NORMAL决定,可选诸如KMALLOC_NORMAL、KMALLOC_DMA、KMALLOC_CGROUP、NR_KMALLOC_TYPES、KMALLOC_RECLAIM等,也可以由用户自己定义自己的专用内存类型,诸如kvm_vcpu、dquot、signal_cache等等都是其他模块自行定义的内存类型。
enum kmalloc_cache_type {
    /* 对应着kmalloc的内存 */
    KMALLOC_NORMAL = 0,
#ifndef CONFIG_ZONE_DMA
    KMALLOC_DMA = KMALLOC_NORMAL,
#endif

#ifndef CONFIG_MEMCG_KMEM
    KMALLOC_CGROUP = KMALLOC_NORMAL,
#endif

#ifdef CONFIG_SLUB_TINY
    KMALLOC_RECLAIM = KMALLOC_NORMAL,
#else
    KMALLOC_RECLAIM,
#endif

#ifdef CONFIG_ZONE_DMA
    /* 对应着dma-kmalloc的内存 */
    KMALLOC_DMA,
#endif

#ifdef CONFIG_MEMCG_KMEM
    KMALLOC_CGROUP,
#endif

    NR_KMALLOC_TYPES
};

2.2.3 基本结构

在这里插入图片描述

  • kmem_cache数据结构代表一个slab 缓存
  • kmem_cache_cpu表示了每个 CPU 对象的缓存信息。它用于存储每个 CPU 上的缓存数组(array_cache)以及一些与 CPU 相关的缓存统计信息
  • array_cache用于表示该缓存在各个CPU中的slab对象
  • kmem_cache_node用于管理各个内存节点上slab对象的分配
    在这里插入图片描述
kmem_cache
struct kmem_cache {
    struct array_cache __percpu *cpu_cache;  //表示每个cpu中的slab对象
 
    unsigned int batchcount; 
    //当cpu_cache为空时,从缓存slab中获取的对象数目,它还表示缓存增长时分配的对象数目。
    //初始时为1,后续会调整
    
    unsigned int limit; 
    //cpu_cache中的对象数目上限
    //当slab free达到limit时,需要将array_caches中的部分obj返回到kmem_cache_node的页帧中
    
    unsigned int shared; //表示该缓存是否是共享的
 
    unsigned int size; //表示slab中的每个对象大小
    
    struct reciprocal_value reciprocal_buffer_size; //用于存储一个缓存的倒数大小的数据结构
 
    slab_flags_t flags; //用于存储常量标志的位掩码
    unsigned int num;   //每个slab中的对象数目
 
    unsigned int gfporder; //slab关联页数
 
    gfp_t allocflags;  //强制使用的 GFP 标志,例如 GFP_DMA
 
    size_t colour;   //缓存颜色范围
    unsigned int colour_off;  //颜色偏移量
    
    struct kmem_cache *freelist_cache; // 空闲对象管理
    unsigned int freelist_size; // 空闲对象数量
 
    //构造函数指针
    void (*ctor)(void *obj); //这个在2.6之后已经废弃了
 
    const char *name;      //缓存名称
    struct list_head list; //用于将缓存连接到全局缓存列表的链表节点
    int refcount;          //引用计数
    int object_size;       //对象的大小
    int align;             //对齐方式

#ifdef CONFIG_DEBUG_SLAB
    unsigned long num_active;       //活动对象的数量
    unsigned long num_allocations;  //分配的对象数量
    unsigned long high_mark;        //高水位标记
    unsigned long grown;            //已增长的对象数量
    unsigned long reaped;           //已收割的对象数量
    unsigned long errors;           //错误数量
    unsigned long max_freeable;     //最大可释放的空闲数量
    unsigned long node_allocs;      //节点分配数量
    unsigned long node_frees;       //节点释放数量
    unsigned long node_overflow;    //节点溢出数量
    atomic_t allochit;              //分配命中计数 
    atomic_t allocmiss;             //分配未命中计数
    atomic_t freehit;               //释放命中计数
    atomic_t freemiss;              //释放未命中计数
    
#ifdef CONFIG_DEBUG_SLAB_LEAK
    atomic_t store_user_clean;
#endif

    int obj_offset; //对象偏移量
#endif 
 
#ifdef CONFIG_MEMCG
    struct memcg_cache_params memcg_params; //用于内存控制组的参数
#endif

#ifdef CONFIG_KASAN
    struct kasan_cache kasan_info; //KASan 相关信息
#endif
 
#ifdef CONFIG_SLAB_FREELIST_RANDOM
    unsigned int *random_seq;  //用于slab freelist随机化的随机序列
#endif
 
    unsigned int useroffset;   //用户复制区域的偏移量
    unsigned int usersize;     //用户复制区域的大小
 
    struct kmem_cache_node *node[MAX_NUMNODES];  
    //每个内存节点上的slab对象信息,每个node上包括部分空闲,全满以及全部空闲三个队列
};
keme_cache_cpu
struct kmem_cache_cpu{
	void **freelist;        //指向下一个可用的object
	unsigned long tid;      //全局独一无二的事物ID
	struct page *page;      //slab内存的page指针

#ifdef CONFIG_SLUB_CPU_PARTIAL
	struct page *partial;   //本地slab partial链表,主要是一些部分使用object的slab
#endif
};
array_cache
  • array_cache是一个per_cpu数组,访问不需要加锁,是与cpu cache打交道的直接数据结构,每次获取空闲slab对象时都是通过entry[avail--]去获取,当avail==0时,又从kmem_cache_node中获取batchcount个空闲对象到array_cache中。
struct array_cache {
    unsigned int avail; //保存了当前array中的可用数目
    unsigned int limit; //同上
    unsigned int batchcount; //同上
    unsigned int touched; 
    //缓存收缩时置0,缓存移除对象时置1,使得内核能确认在上一次收缩之后是否被访问过
    void *entry[]; 
    //用于存储缓存的条目的数组,大小在运行时动态确定
};     
kmem_cache_node
  • kmem_cache_node用于管理slab(实际对象存储伙伴页帧),其会管理三个slab列表:
    • 部分空闲partial
    • 全部空闲empty
    • 全部占用full
  • array_cache获取batchcount空闲对象时,先尝试从partial分配,如果不够则再从empty分配剩余对象,如果都不够,则需要grow分配新的slab页帧。
struct kmem_cache_node {
    spinlock_t list_lock;       //自旋锁,用于保护缓存节点的链表操作
    unsigned long nr_partial;   //slab节点中slab的数量
    struct list_head partial;   //slab节点的slab partial链表
};
struct page
  • 用于描述slab页面,一个slab页面由一个或多个page组成,page页帧是物理存储地址

2.3 工作原理

2.3.1 对象分配和释放过程

  • 三个指针:

    • current指针,仅指向一个slab
    • partial指针,指向未满slab链表
    • full指针,指向全满slab链表
      在这里插入图片描述
  • 对象分配:使用current slab,若满,从partial指向的slab中取空闲区域,把current指向的slab移到full

      1. 查找合适的Slab Cache:当系统需要一个特定类型的对象时,首先在对应的Slab Cache中查找。
      1. 选择Slab:在找到的Slab Cache中,系统会寻找状态为部分满(Partial)或空(Empty)的Slab。优先选择部分满的Slab,以提高内存利用率。
      1. 分配对象:从选定的Slab中分配一个空闲对象。如果选择的是空Slab,系统会先初始化该Slab,然后分配对象。
      1. 更新Slab状态:分配对象后,更新Slab的状态。如果所有对象都被分配,Slab状态变为满(Full)。
  • 对象释放:若是full则移动到partial,若partial全空则还给buddy分配器

      1. 确定对象所属的Slab:释放对象时,系统首先确定该对象属于哪个Slab。
      1. 释放对象:将对象标记为未使用,返回到Slab的空闲对象池中。
      1. 更新Slab状态:如果释放对象前Slab是满的,则释放后状态变为部分满(Partial)。如果释放后Slab中所有对象都是空闲的,则状态变为空(Empty)。
      1. Slab的回收:如果一个Slab长时间处于空状态,系统可能会决定回收该Slab,释放内存给操作系统。

2.3.2 缓存色彩和内存对齐

  • 缓存色彩:
    • 缓存色彩是一种用于优化CPU缓存利用率的技术。由于CPU缓存行的存在,不同的内存地址可能会映射到同一个缓存行,这种现象称为缓存行冲突。缓存色彩通过在对象的内存地址上加上小的偏移量,使得连续分配的对象不会映射到相同的缓存行上,从而减少缓存行冲突,提高缓存的使用效率。
  • 内存对齐:
    • 内存对齐是指按照一定的边界来分配内存地址,使得数据的存取更加高效。在处理器架构中,对齐的内存访问通常比非对齐的内存访问速度要快。SLAB内存管理机制通过确保对象在内存中正确对齐,提高了数据访问的速度,减少了内存访问时间。
    • 内存对齐的另一个好处是减少了系统的内存碎片。

3. SLAB、SLUB和SLOB

  • SLAB分配器:适用于内存分配和释放频繁,且需要稳定内存使用的环境。它的设计注重减少内存碎片和提高内存利用率,非常适合长时间运行的服务或系统。
  • SLUB分配器:适用于对性能要求高的场景,特别是在多核处理器上。它的设计简化了内存管理的数据结构,减少了锁的竞争,优化了CPU缓存的使用,提供了高效的内存分配。
  • SLOB分配器:适用于内存资源非常有限的环境,如嵌入式系统或老旧的硬件。它的设计优先考虑内存的紧凑使用,尽可能减少内存的浪费。
  • 30
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值