Linux内核slab分配器
一、slab思想以及编程接口
1、slab 核心思想
为每种对象类型创建一个内存缓存,每个内存缓存由多个大块组成,一个大块是一个或多个连续的物理页,每个大块包含多个对象。slab 采用面向对象的思想,基于对象类型管理内存,每种对象被划分为一个类,比如进程描述符(task_struct)是一个类,每个进程描述符实现是一个对象。内存缓存组成结构如下:
2.编程接口
【通用分配接口】
分配内存: 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);
【slab专用接口】
创建内存缓存: struct kmem_cache *kmem_cache_create (const char , size_t, size_t, unsigned long, void ()(void *));
指定内存缓存分配对象: void *kmem_cache_alloc (struct kmem_cache *, gfp_t);
释放对象: void kmem_cache_free (struct kmem_cache *, void *);
销毁内存缓存: void kmem_cache_destroy (struct kmem_cache *);
【接口细节】
1、创建内存缓存
struct kmem_cache *kmem_cache_create(const char *name, size_t size, size_t align,unsigned long flags,void (*strc)(void *));
name:名称
size:对象的长度
align:对象需要对齐的数值
flags:slab 标志位
strc:对象的构造函数
2、指定内存缓存分配对象
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
cachep:从指定的内存缓存分配
flags:传给页分配器的分配标志位,当内存缓存没有空闲对象,向页分配器请求分配页的时候使用这个分配标志位。
3、释放对象
void kmem_cache_free(struct kmem_cache *cachep, void *objp);
cachep:对象所属的内存缓存
objp:对象的地址
4、销毁内存缓存
void kmem_cache_destroy(struct kmem_cache *s);
s:内存缓存
【kmalloc与kmem_cache_alloc核心区别】
kmalloc
和kmem_cache_alloc
虽然都基于 Slab 分配器,但它们在用途、灵活性和性能优化方面存在显著差异。以下是两者的主要区别及适用场景:
1. 核心区别
特性 kmalloc
kmem_cache_alloc
缓存类型 使用预定义的通用缓存(如 kmalloc-32
、kmalloc-64
)使用用户自定义的专用缓存(需通过 kmem_cache_create
创建)内存对齐 默认对齐(通常按 CPU 缓存行对齐) 可自定义对齐方式(如按对象大小对齐) 构造函数/析构函数 不支持 支持(可在分配/释放时自动调用初始化或清理函数) 适用场景 通用的小块内存分配 高频分配/释放固定大小的对象(如结构体) 性能优化 适用于非频繁分配的通用场景 针对特定对象优化,减少碎片和查找开销
2. 使用示例
场景 1:通用内存分配(
kmalloc
)
需求:分配 128 字节的临时缓冲区。
实现:
void *buffer = kmalloc(128, GFP_KERNEL); if (!buffer) { // 错误处理 } // 使用 buffer... kfree(buffer);
特点:无需预定义缓存,内核自动选择
kmalloc-128
缓存。场景 2:专用对象分配(
kmem_cache_alloc
)
需求:频繁分配/释放固定大小的结构体(如
struct my_struct
,大小为 256 字节)。实现:
// 创建专用缓存 注意:新版内核只使用五个参数,析构函数参数被舍弃 struct kmem_cache *my_cache = kmem_cache_create( "my_struct_cache", // 缓存名称 sizeof(struct my_struct),// 对象大小 0, // 对齐偏移(默认按对象大小对齐) SLAB_HWCACHE_ALIGN, // 标志(按 CPU 缓存行对齐) NULL, NULL // 构造函数/析构函数(可选) ); // 分配对象 struct my_struct *obj = kmem_cache_alloc(my_cache, GFP_KERNEL); if (!obj) { // 错误处理 } // 使用 obj... kmem_cache_free(my_cache, obj); // 销毁缓存(模块卸载时) kmem_cache_destroy(my_cache);
特点:通过预定义缓存减少分配开销,支持自定义初始化和清理逻辑。
3. 性能对比
操作 kmalloc
kmem_cache_alloc
分配速度 较慢(需匹配通用缓存) 更快(直接命中专用缓存) 内存碎片 可能产生更多碎片 碎片较少(专用缓存隔离对象大小) 适用高频操作 不推荐 推荐
4. 总结
使用
kmalloc
的场景:
适用于临时、非频繁的小块内存分配,无需关心缓存管理(如临时缓冲区、字符串操作)。使用
kmem_cache_alloc
的场景:
适用于高频分配/释放固定大小的对象(如网络数据包、文件描述符),需通过kmem_cache_create
预定义缓存以优化性能。本质区别:
kmalloc
是对通用 Slab 缓存的封装,而kmem_cache_alloc
是对用户自定义缓存的精细化控制。前者简化了接口,后者提供了更高的灵活性和性能优化空间。
二、slab数据结构
- 每个内存缓存对应一个 kmem_cache 实例,所有cpu共享同一个全局对象。
- 每个内存节点对应一个 kmem_cache_node 实例。
- kmem_cache 实例的成员 cpu_slab 指向 array_cache 实例,每个处理器对应一个 array_cache 实例,称为数组缓存,用来缓存刚刚释放的对象,分配时首先从当前处理器的数据缓存分配,避免每次都要从 slab 分配,减少链表操作的锁操作,提高分配的速度。slab 分配器数据结构源码分析如下:
高速缓存描述符数据结构:struct kmem_cache
一个高速缓存中可以含有多个 kmem_cache 对应的高速缓存,就拿 L1 高速缓存来举例,一个 L1 高速缓存对应一个 kmem_cache 链表,这个链表中的任何一个 kmem_cache 类型的结构体均描述一个高速缓存,而这些高速缓存在 L1 cache 中各自占用着不同的区域。
具体源码分析如下:
三、每处理器数组缓存array_cache
内存缓存为每个处理器创建一个数组缓存(结构体 array_cache)。释放对象时,把对象存放到当前处理器对应的数组缓存中;分配对象的时候,先从当前处理器的数组缓存分配对象,采用后进先出(LIFO)原则,可以提高性能。
array_cache 是什么?
array_cache
是一个本地缓存结构,通常每个 CPU 或 NUMA 节点有独立实例,用于存储已分配但未使用的对象,提升内存分配效率,减少全局缓存访问、锁争用及内存访问延迟,主要针对小对象缓存。共享的含义
- 指不同 NUMA 节点共享
array_cache
中缓存的对象,shared
字段指向跨 NUMA 节点共享的缓存。- 每个 NUMA 节点有本地
array_cache
,本地缓存用尽时,可从其他 NUMA 节点缓存获取对象,提高内存重用率,减少跨节点内存访问延迟。共享的具体实现
- NUMA 系统中,节点有本地
array_cache
,为优化性能,多个节点可共享缓存对象。如某节点本地缓存满或无空闲对象,可从其他节点缓存(存储在shared
指向的array_cache
中)获取。shared
目的是提升内存分配效率,避免单一节点缓存资源过度消耗与访问延迟。共享的对象是什么?
- 共享
array_cache
中存储的小对象。节点本地缓存对象耗尽时,内核可访问其他 NUMA 节点缓存,快速获取对象,无需等待全局缓存或其他内存池分配。谁去共享?
- 每个 NUMA 节点的
array_cache
均可共享缓存。如 NUMA 系统中,节点 A 的array_cache
对象可与节点 B 的共享。CPU 核心请求对象时,可从本地或其他节点共享缓存获取。- 内核内存分配器执行共享机制,依当前 NUMA 节点缓存状况(如空闲对象数)决策。若节点 A 本地缓存不足,可从节点 B 缓存获取对象,减少对全局
kmem_cache
的访问,提升性能。
1. 设计优化
-
引入 per-CPU array_cache:
传统分配需操作全局 slab 缓存,多核 CPU 易引发锁争用。per-CPU 的array_cache
作为私有缓存,仅在缓存耗尽 / 满载时操作全局 slab,减少锁争用:- 分配:优先从本地
array_cache
分配,无需全局锁;缓存空时批量从全局获取。 - 释放:优先将对象存入本地
array_cache
;缓存满时批量释放回全局 slab。
- 分配:优先从本地
-
减少全局锁争用:
- 独立 per-CPU 缓存:每个 CPU 有自己的
array_cache
,分配释放优先用本地缓存。 - 批量操作:
array_cache
耗尽 / 满时批量操作全局 slab,降低锁操作频率。 - 减少链表操作:用数组存储指针,避免频繁操作全局 slab 链表。
- 提高缓存命中率:最近释放对象优先重新分配(LIFO),提高 CPU 缓存命中率。
- 独立 per-CPU 缓存:每个 CPU 有自己的
2. 分配释放规则
- 分配:先从当前处理器
array_cache
分配,若空则批量填充(批量值为batchcount
)。 - 释放:若
array_cache
满,先批量归还对象到 slab,再将当前释放对象存入缓存。
3. 全局锁存在必要性
- 共享对象管理:跨 NUMA 节点请求内存时,需全局锁确保分配公平性和一致性。
- 跨 NUMA 节点请求:防止不同节点同时修改全局状态,避免竞态条件。
- 内存池管理回收:内存池创建、销毁、调整等全局操作需同步,确保原子性。
为什么
array_cache
还需要全局锁?
共享对象的管理:
array_cache
是每个 NUMA 节点的本地缓存,存储预分配的小对象。当本地缓存对象被用完时,需向全局的kmem_cache
请求更多内存。若各 NUMA 节点的缓存完全独立,缺乏协调内存需求的机制,会导致跨节点资源分配不均衡。为确保内存分配的公平性和全局一致性,内核需要全局锁来同步不同节点之间的内存请求。跨 NUMA 节点的对象请求:
即使array_cache
是每个节点的本地缓存,当某个 NUMA 节点的缓存用尽时,可能需要从其他节点的array_cache
或全局的kmem_cache
中请求对象。此时,全局锁可确保跨节点的内存请求不发生竞态条件,防止不同节点同时修改全局状态,保障操作的安全性。内存池的管理和回收:
内核通过kmem_cache
管理内存池。当节点本地缓存中的对象过多时,会将多余对象回收到全局缓存或共享缓存中。这一过程需要全局锁来确保回收操作的同步。即便每个节点有独立的array_cache
,内存的最终回收和分配仍需全局协调,以维持整体内存管理的秩序。避免内存碎片:
在多核系统中,若没有全局同步机制,多个节点同时修改kmem_cache
可能导致内存碎片增加。通过全局锁管理内存池,能在一定程度上避免因并行操作引发的内存碎片化问题,提升内存使用效率。内存池的创建和销毁:
例如,kmem_cache
本身的创建、销毁和大小调整等操作具有全局性,涉及跨节点的内存管理。此时需要全局锁来保证这些操作的原子性和一致性,确保kmem_cache
生命周期管理与状态更新的正确性。解决方案:
全局锁并非保护所有操作。实际上,
array_cache
在处理常规对象分配时,通过局部缓存避免全局锁争用,使每个 CPU 或 NUMA 节点能独立工作,减少竞争和延迟。但当涉及跨节点内存请求(如某节点array_cache
资源短缺),或触发全局内存池操作(如kmem_cache
的创建、清理)时,仍需要全局锁来避免数据不一致或资源冲突,在性能优化与全局一致性间取得平衡。
【图解】
kmem_cache_node
与array_cache
区别:
特性 kmem_cache_node array_cache 作用 管理 NUMA 节点上 kmem_cache
的 slab 页,优化分配回收加速小对象分配,减少全局分配锁争用 粒度 以 slab 页为单位管理对象分配回收 以单个对象为单位缓存小对象 分配目标 管理空闲、部分满、完全满的 slab 页 缓存小对象以便快速分配 NUMA 相关性 每个 NUMA 节点一个实例,管理该节点 slab 页 每个 NUMA 节点一个或多个实例,加速分配 数据结构关系:
kmem_cache
:管理对象类型缓存池,包含 CPU 私有缓存指针、对象大小、分配标志等。kmem_cache_node
:管理特定 NUMA 节点缓存状态,包含 slab 页链表(slab_partial
、slab_full
、slab_free
)。array_cache
:每个 CPU/NUMA 节点的本地缓存,存储预分配小对象,优化分配效率。slabinfo
:监控kmem_cache
状态的结构体,通过/proc/slabinfo
展示缓存名称、活跃对象数等信息。
四、内存回收
对于所有对象空间的slab,没有立即释放,而是放在空闲的slab链表中。只有内存节点上的空闲对象的数量超过限制,才开始回收空闲slab,知道空闲对象的数量小于或等于限制。
1. Slab 页与内存管理
- slab 页概念:Slab 分配器的内存管理 “容器”,承载多个内存块。从系统页(4KB 或 8KB)分配,包含多个固定大小内存块,每个块存储一个对象,提升内存管理效率,避免频繁页分配释放和内存碎片化。
- 管理方式:将多个内存块打包到 slab 页,按需分配给应用程序 / 内核代码。
2. 回收机制
- 空闲对象数量限制:
节点 n 的空闲对象数量限制 =(1 + 节点处理器数量)*kmem_cache.batchcount
+keme_cache.num
。 - 定期回收:
slab 分配器通过每个处理器向全局工作队列添加延迟工作项(处理函数cache_reap
)定期回收:- 每 2 秒执行:回收节点 n 对应远程节点数组缓存对象;若当前处理器数组缓存 2 秒无分配,回收其缓存对象。
- 每 4 秒执行:若共享数组缓存 4 秒无分配,回收其对象;若空闲 slab 4 秒无分配,回收空闲 slab。
https://github.com/0voice