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()");elseBUG();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_DMAKMALLOC_DMA = KMALLOC_NORMAL,
#endif#ifndef CONFIG_MEMCG_KMEMKMALLOC_CGROUP = KMALLOC_NORMAL,
#endif#ifdef CONFIG_SLUB_TINYKMALLOC_RECLAIM = KMALLOC_NORMAL,
#elseKMALLOC_RECLAIM,
#endif#ifdef CONFIG_ZONE_DMA/* 对应着dma-kmalloc的内存 */KMALLOC_DMA,
#endif#ifdef CONFIG_MEMCG_KMEMKMALLOC_CGROUP,
#endifNR_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_DMAsize_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_SLABunsigned 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_LEAKatomic_t store_user_clean;
#endifint obj_offset; //对象偏移量
#endif #ifdef CONFIG_MEMCGstruct memcg_cache_params memcg_params; //用于内存控制组的参数
#endif#ifdef CONFIG_KASANstruct kasan_cache kasan_info; //KASan 相关信息
#endif#ifdef CONFIG_SLAB_FREELIST_RANDOMunsigned int *random_seq; //用于slab freelist随机化的随机序列
#endifunsigned int useroffset; //用户复制区域的偏移量unsigned int usersize; //用户复制区域的大小struct kmem_cache_node *node[MAX_NUMNODES]; //每个内存节点上的slab对象信息,每个node上包括部分空闲,全满以及全部空闲三个队列
};
keme_cache_cpu
struct kmem_cache_cpu{void **freelist; //指向下一个可用的objectunsigned long tid; //全局独一无二的事物IDstruct page *page; //slab内存的page指针#ifdef CONFIG_SLUB_CPU_PARTIALstruct 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
-
- 查找合适的Slab Cache:当系统需要一个特定类型的对象时,首先在对应的Slab Cache中查找。
-
- 选择Slab:在找到的Slab Cache中,系统会寻找状态为部分满(Partial)或空(Empty)的Slab。优先选择部分满的Slab,以提高内存利用率。
-
- 分配对象:从选定的Slab中分配一个空闲对象。如果选择的是空Slab,系统会先初始化该Slab,然后分配对象。
-
- 更新Slab状态:分配对象后,更新Slab的状态。如果所有对象都被分配,Slab状态变为满(Full)。
-
-
对象释放:若是full则移动到partial,若partial全空则还给buddy分配器
-
- 确定对象所属的Slab:释放对象时,系统首先确定该对象属于哪个Slab。
-
- 释放对象:将对象标记为未使用,返回到Slab的空闲对象池中。
-
- 更新Slab状态:如果释放对象前Slab是满的,则释放后状态变为部分满(Partial)。如果释放后Slab中所有对象都是空闲的,则状态变为空(Empty)。
-
- Slab的回收:如果一个Slab长时间处于空状态,系统可能会决定回收该Slab,释放内存给操作系统。
-
2.3.2 缓存色彩和内存对齐
- 缓存色彩:
- 缓存色彩是一种用于优化CPU缓存利用率的技术。由于CPU缓存行的存在,不同的内存地址可能会映射到同一个缓存行,这种现象称为缓存行冲突。缓存色彩通过在对象的内存地址上加上小的偏移量,使得连续分配的对象不会映射到相同的缓存行上,从而减少缓存行冲突,提高缓存的使用效率。
- 内存对齐:
- 内存对齐是指按照一定的边界来分配内存地址,使得数据的存取更加高效。在处理器架构中,对齐的内存访问通常比非对齐的内存访问速度要快。SLAB内存管理机制通过确保对象在内存中正确对齐,提高了数据访问的速度,减少了内存访问时间。
- 内存对齐的另一个好处是减少了系统的内存碎片。
3. SLAB、SLUB和SLOB
- SLAB分配器:适用于内存分配和释放频繁,且需要稳定内存使用的环境。它的设计注重减少内存碎片和提高内存利用率,非常适合长时间运行的服务或系统。
- SLUB分配器:适用于对性能要求高的场景,特别是在多核处理器上。它的设计简化了内存管理的数据结构,减少了锁的竞争,优化了CPU缓存的使用,提供了高效的内存分配。
- SLOB分配器:适用于内存资源非常有限的环境,如嵌入式系统或老旧的硬件。它的设计优先考虑内存的紧凑使用,尽可能减少内存的浪费。