Linux内核学习轨迹第五部:内核内存分配器:SLUB/SLOB/SLAB全解析(第四小节)

张开发
2026/6/6 22:02:41 15 分钟阅读

分享文章

Linux内核学习轨迹第五部:内核内存分配器:SLUB/SLOB/SLAB全解析(第四小节)
4. 内核内存分配器SLUB/SLOB/SLAB全解析伙伴系统解决了外部碎片问题以页为单位分配内存但内核中大量的小对象比如task_struct、inode、file结构体大小只有几十到几百字节如果用整页分配会导致严重的内部碎片。SLAB/SLUB/SLOB分配器就是为了解决这个问题基于伙伴系统分配的整页拆分更小的对象实现小内存的高效分配减少内部碎片。4.1 三种分配器的对比与适用场景Linux内核提供了三种slab分配器可通过内核启动参数选择默认使用SLUB分配器分配器核心设计优势劣势适用场景SLUB无队列设计简化结构基于页管理对象性能高、扩展性好、调试简单、内存占用低解决了SLAB的队列锁竞争问题对小对象的内存利用率略低于SLABLinux默认分配器绝大多数场景包括服务器、桌面、嵌入式SLAB基于每CPU/每节点队列设计复杂的缓存结构小对象内存利用率高缓存命中率高代码复杂、多核锁竞争严重、扩展性差、调试困难、内存开销大老旧系统对小对象内存利用率要求极高的嵌入式场景SLOB简单的首次适配分配器代码极简代码量极小内存占用极低内存碎片化严重性能差多核扩展性极差内存极小的嵌入式设备比如单片机、物联网设备本章节重点讲解默认的SLUB分配器它是目前Linux内核的主流性能和扩展性最好代码结构清晰。4.2 SLUB分配器的核心设计思想SLUB分配器的核心设计是简化结构消除SLAB的复杂队列基于页管理对象核心思想Slab页SLUB从伙伴系统分配一个或多个连续的页Slab页把页拆分为固定大小的对象每个Slab页只管理一种大小的对象Kmem Cache每种大小的对象对应一个kmem_cache缓存比如task_struct对应一个kmem_cacheinode对应另一个kmem_cache每个缓存管理自己的Slab页无队列设计SLUB没有SLAB复杂的每CPU/每节点队列直接把空闲对象链表存储在Slab页本身减少锁竞争提升多核扩展性每CPU缓存为每个CPU维护一个活动Slab页单CPU的对象分配和释放直接从本地CPU的活动Slab中操作不需要加锁性能极高合并缓存SLUB会自动合并大小相近的kmem_cache减少Slab页的数量提升内存利用率。4.3 SLUB分配器的核心数据结构SLUB分配器的核心数据结构有两个struct kmem_cache对象缓存和struct slabSlab页管理结构定义在mm/slub.c中。4.3.1 kmem_cache结构体每个固定大小的对象对应一个kmem_cache实例管理该大小对象的所有Slab页核心字段拆解struct kmem_cache { // 每CPU的活动Slab缓存无锁快速分配 struct kmem_cache_cpu __percpu *cpu_slab; // 缓存的名称比如task_struct、kmalloc-128 const char *name; // 对象的大小包括对齐填充 unsigned int size; // 对象的实际大小不包括对齐填充 unsigned int object_size; // 对象的对齐要求 unsigned int align; // 每个Slab页包含的对象数量 unsigned int num; // 每个Slab页的阶数2^order个页 unsigned int order; // 对象的偏移量在Slab页中的起始位置 unsigned int offset; // Slab的标志位比如SLAB_POISON、SLAB_RED_ZONE等调试标志 slab_flags_t flags; // 构造函数对象分配时调用 void (*ctor)(void *obj); // 节点的Slab管理结构每个NUMA节点对应一个 struct kmem_cache_node *node[MAX_NUMNODES]; // 缓存合并相关字段 struct kmem_cache *next; // 调试相关字段 int refcount; int in_slab; } ____cacheline_aligned;核心字段解析cpu_slab每CPU的活动Slab缓存是SLUB快速分配的核心每个CPU有一个独立的活动Slab页分配和释放不需要加锁性能极高size/object_size对象的大小object_size是用户实际需要的大小size是对齐后的大小包括填充和元数据order每个Slab页的阶数决定了从伙伴系统分配的连续页数量对象越大order越大um每个Slab页包含的对象数量由Slab页的总大小和对象大小决定ode[MAX_NUMNODES]每个NUMA节点的Slab管理结构管理该节点内的所有Slab页包括满的、部分空闲的、完全空闲的Slab页。4.3.2 kmem_cache_cpu结构体每CPU的活动Slab缓存是SLUB无锁快速分配的核心定义在mm/slub.c中struct kmem_cache_cpu { // 空闲对象链表的第一个对象无锁分配直接从这里取 void **freelist; // 当前活动的Slab页对应的page结构体 struct page *page; // 下一个要释放的对象用于批量释放 void **next_freelist; // 事务编号用于调试 unsigned int tid; };核心设计每个CPU的kmem_cache_cpu有一个当前活动的Slab页freelist指向该Slab页中的空闲对象链表分配时直接从freelist取第一个对象释放时直接把对象放回freelist整个过程不需要加锁因为是每CPU的私有数据多核之间没有竞争性能极高。4.3.3 Slab页与page结构体的关联SLUB的Slab页就是从伙伴系统分配的连续物理页用struct page结构体管理page结构体中的联合体字段对应SLUB的管理信息// page结构体中SLUB相关的联合体字段 struct { struct slab *slab; // 指向Slab管理结构 void *s_mem; // Slab内的第一个对象地址 union { unsigned int active; // Slab内的活跃对象数 void *freelist; // 空闲对象链表 }; };每个Slab页有三种状态满FullSlab内的所有对象都被分配了没有空闲对象部分空闲PartialSlab内有部分对象被分配还有空闲对象完全空闲FreeSlab内的所有对象都被释放了没有活跃对象。4.4 SLUB分配器的核心流程源码解析SLUB分配器的核心流程分为对象分配、对象释放、Slab页的分配与释放我们基于Linux 6.6内核拆解核心逻辑。4.4.1 对象分配流程kmem_cache_alloc()对象分配的核心入口是kmem_cache_alloc()最终落到slab_alloc()函数核心分为快速路径和慢速路径。分配流程核心步骤kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags)↓slab_alloc(s, gfpflags, _RET_IP_)↓1. 快速路径分配├→ 关闭抢占获取当前CPU的kmem_cache_cpu├→ 从freelist中取出第一个空闲对象├→ 更新freelist指向下一个空闲对象├→ 开启抢占返回对象地址└→ 如果freelist为空进入慢速路径↓2. 慢速路径分配__slab_alloc()├→ 检查当前活动Slab页是否还有空闲对象有则刷新freelist回到快速路径├→ 如果没有从当前NUMA节点的Partial Slab链表中取出一个有空闲对象的Slab页设置为当前CPU的活动Slab回到快速路径├→ 如果Partial链表为空调用new_slab()从伙伴系统分配新的Slab页初始化对象和空闲链表设置为活动Slab回到快速路径└→ 如果伙伴系统分配失败返回NULL分配失败核心逻辑解析快速路径是无锁、无睡眠的性能极高99%的分配都会在快速路径完成只有当当前CPU的活动Slab页没有空闲对象时才会进入慢速路径需要加锁访问节点的Partial链表或者从伙伴系统分配新的Slab页ew_slab()函数会从伙伴系统分配对应order的连续页初始化Slab页把页拆分为固定大小的对象构建空闲对象链表设置为当前CPU的活动Slab。4.4.2 对象释放流程kmem_cache_free()对象释放的核心入口是kmem_cache_free()最终落到slab_free()函数核心流程和分配对应。释放流程核心步骤kmem_cache_free(struct kmem_cache *s, void *obj)↓slab_free(s, obj, _RET_IP_)↓1. 快速路径释放├→ 关闭抢占获取当前CPU的kmem_cache_cpu├→ 把释放的对象插入到freelist的头部├→ 开启抢占释放完成└→ 如果Slab页从满变为部分空闲进入慢速路径↓2. 慢速路径释放__slab_free()├→ 把Slab页加入到节点的Partial链表中├→ 检查Slab页是否所有对象都被释放如果是把Slab页释放回伙伴系统└→ 完成释放核心逻辑解析快速路径释放也是无锁的直接把对象放回当前CPU活动Slab的freelist中性能极高只有当Slab页从满变为部分空闲或者完全空闲时才会进入慢速路径需要加锁操作节点的链表或者把Slab页释放回伙伴系统SLUB会定期把完全空闲的Slab页释放回伙伴系统避免内存浪费这一点和SLAB不同SLAB会缓存大量的Slab页不会轻易释放。4.5 kmalloc通用内核内存分配器kmalloc是内核中最常用的通用内存分配接口基于SLUB分配器实现类似于用户态的malloc用于分配任意大小的内核小内存。kmalloc的实现原理内核预定义了一系列通用的kmem_cache对应不同的大小比如kmalloc-8、kmalloc-16、kmalloc-32、kmalloc-64、kmalloc-128、kmalloc-256、kmalloc-512、kmalloc-1024、kmalloc-2048、kmalloc-4096等每个大小对应一个kmem_cache缓存。当调用kmalloc(size)时内核会找到大于等于size的最小的kmalloc缓存从对应的缓存中分配对象比如分配100字节会从kmalloc-128缓存中分配。4.6 SLUB分配器的工程实践与调试1.SLUB调试功能的使用SLUB提供了强大的调试功能可以检测内存泄漏、越界访问、释放后使用、重复释放等常见的内核内存bug是驱动开发、内核调试的利器。a.内核启动参数开启SLUB调试slub_debugFUZP常用调试标志F开启sanity checks每次分配和释放都检查Slab的合法性U开启释放后使用检测释放的对象填充毒值访问会触发警告Z开启红区检测对象前后设置红区越界访问会触发警告P开启Poison检测分配的对象填充毒值未初始化访问会触发警告A开启所有调试功能。调试信息查看cat /sys/kernel/slab//可以查看每个缓存的分配统计、错误信息、活动对象等。2.Slab内存泄漏排查线上常见的问题是Slab内存占用持续增长导致系统内存不足OOM触发根源是内核驱动/模块的内存泄漏。排查流程查看/proc/meminfo确认Slab、SReclaimable可回收、SUnreclaim不可回收的占用如果SUnreclaim持续增长说明有不可回收的Slab内存泄漏查看/proc/slabinfo或者slabtop命令找到占用内存最多、对象数量持续增长的kmem_cache开启SLUB调试跟踪对象的分配和释放栈找到泄漏的位置常见根因内核驱动分配的对象没有释放、引用计数泄漏、缓存对象没有正确回收。临时解决方案如果是可回收的Slab内存可以手动回收echo 2 /proc/sys/vm/drop_caches释放可回收的slab对象。3.自定义kmem_cache的最佳实践内核驱动开发中如果需要频繁分配和释放固定大小的对象应该创建自定义的kmem_cache而不是用kmalloc性能更高内存利用率更好也更容易调试。自定义kmem_cache的标准实现// 定义自定义对象结构体struct my_obj { int id; char name[32]; struct list_head list; }; // 定义缓存指针 static struct kmem_cache *my_obj_cache; // 模块初始化时创建缓存 static int __init my_module_init(void) { // 创建自定义kmem_cache my_obj_cache kmem_cache_create( my_obj_cache, // 缓存名称 sizeof(struct my_obj), // 对象大小 __alignof__(struct my_obj), // 对齐要求 SLAB_HWCACHE_ALIGN | SLAB_POISON, // 标志位 NULL // 构造函数 ); if (!my_obj_cache) { return -ENOMEM; } return 0; } // 分配对象 struct my_obj *obj kmem_cache_alloc(my_obj_cache, GFP_KERNEL); // 释放对象 kmem_cache_free(my_obj_cache, obj); // 模块退出时销毁缓存 static void __exit my_module_exit(void) { // 销毁缓存前必须确保所有对象都被释放 kmem_cache_destroy(my_obj_cache); } module_init(my_module_init); module_exit(my_module_exit);避坑指南模块退出时必须确保所有从缓存中分配的对象都被释放否则调用kmem_cache_destroy()会失败导致内存泄漏自定义缓存的名称必须唯一不能和系统已有的缓存重名。4.SLUB与SLAB的选型绝大多数场景下使用默认的SLUB分配器即可它的性能、扩展性、调试性都远优于SLAB只有在内存极小的嵌入式设备对小对象的内存利用率要求极高时才考虑使用SLAB只有在内存极小的单片机、物联网设备代码量要求极简时才考虑使用SLOB。

更多文章