Linux 专用内存缓存池 (SLAB/SLUB) 解析Linux 内核中的专用内存缓存池是一种高效的内存管理机制用于为频繁分配和释放的、大小相同的对象提供专门的内存管理。它通过预分配和重用对象避免了频繁调用底层内存分配器带来的开销和碎片问题。 一、为什么需要专用内存缓存池问题场景考虑一个网络驱动每秒需要处理数万个数据包每个数据包对应一个struct sk_buff结构。如果使用通用分配器频繁的系统调用每次都调用底层分配器内存碎片不同大小的对象混在一起导致碎片化缓存局部性差相关对象分散在内存中CPU 缓存命中率低解决方案专用内存缓存池通过以下方式解决预分配对象池一次性申请大块内存切成固定大小的对象快速分配/释放只需从空闲链表中取出/放回对象O(1) 操作改善局部性相同类型的对象集中存放提高缓存效率减少碎片固定大小的对象不会产生外部碎片️ 二、核心数据结构1. SLAB 体系结构┌─────────────────────────┐ │ kmem_cache (缓存描述符) │ │ - 对象大小: 64 bytes │ │ - 空闲对象链表 │ │ - 每个CPU的本地缓存 │ └───────────┬─────────────┘ │ ┌───────────────────────┼───────────────────────┐ │ │ │ ┌────▼────┐ ┌─────▼─────┐ ┌─────▼─────┐ │ SLAB 1 │ │ SLAB 2 │ │ SLAB 3 │ │ (页面1) │ │ (页面2) │ │ (页面3) │ ├─────────┤ ├───────────┤ ├───────────┤ │ 对象1 │ │ 对象1 │ │ 对象1 │ │ 对象2 │ │ 对象2 │ │ 对象2 │ │ 对象3 │ │ 对象3 │ │ 对象3 │ │ ... │ │ ... │ │ ... │ │ 空闲列表 │ │ 空闲列表 │ │ 空闲列表 │ └─────────┘ └───────────┘ └───────────┘2. 关键数据结构 (以 SLUB 为例)structkmem_cache{constchar*name;// 缓存名称unsignedintsize;// 对象大小含对齐unsignedintobject_size;// 原始对象大小unsignedintoffset;// 空闲指针偏移量unsignedintalign;// 对齐要求unsignedlongflags;// 标志位如 SLAB_HWCACHE_ALIGNstructkmem_cache_node*node[MAX_NUMNODES];// 每个NUMA节点的缓存structkmem_cache_cpu*cpu_slab;// 每个CPU的本地缓存}; 三、核心 API 详解1.kmem_cache_create- 创建缓存池#includelinux/slab.hstructkmem_cache*kmem_cache_create(constchar*name,// 缓存名称会出现在 /proc/slabinfounsignedintsize,// 每个对象的大小unsignedintalign,// 对齐要求0 表示自然对齐slab_flags_tflags,// 标志位void(*ctor)(void*obj)// 构造函数可选);常用标志位标志作用SLAB_HWCACHE_ALIGN按硬件缓存行对齐提高缓存效率SLAB_POISON在分配/释放时填充垃圾数据便于调试SLAB_RED_ZONE在对象周围添加红区检测溢出SLAB_PANIC创建失败时 panicSLAB_ACCOUNT计入 cgroup 内存统计示例structmy_device{intid;charname[32];structlist_headlist;};staticstructkmem_cache*my_device_cache;// 在模块初始化时staticint__initmy_init(void){my_device_cachekmem_cache_create(my_device,// 名称sizeof(structmy_device),// 对象大小0,// 默认对齐SLAB_HWCACHE_ALIGN|SLAB_PANIC,// 标志NULL// 无构造函数);if(!my_device_cache)return-ENOMEM;return0;}2.kmem_cache_alloc- 分配对象void*kmem_cache_alloc(structkmem_cache*cache,gfp_tflags);GFP 标志GFP_KERNEL正常分配可能睡眠GFP_ATOMIC原子分配用于中断上下文GFP_NOWAIT不等待立即返回structmy_device*dev;devkmem_cache_alloc(my_device_cache,GFP_KERNEL);if(!dev)return-ENOMEM;// 初始化对象如果没有构造函数dev-id1;strcpy(dev-name,my device);INIT_LIST_HEAD(dev-list);3.kmem_cache_free- 释放对象voidkmem_cache_free(structkmem_cache*cache,void*obj);// 使用完毕后释放list_del(dev-list);kmem_cache_free(my_device_cache,dev);4.kmem_cache_destroy- 销毁缓存池voidkmem_cache_destroy(structkmem_cache*cache);重要销毁前必须确保所有对象都已释放否则会导致内存泄漏。staticvoid__exitmy_exit(void){// 确保所有对象都已释放kmem_cache_destroy(my_device_cache);} 四、完整使用示例场景字符设备驱动管理设备实例#includelinux/module.h#includelinux/slab.h#includelinux/fs.h#includelinux/cdev.h#defineMAX_DEVICES32structmy_device{intid;charname[64];structcdevcdev;structlist_headlist;};staticstructkmem_cache*dev_cache;staticLIST_HEAD(dev_list);staticDEFINE_MUTEX(dev_mutex);// 构造函数可选staticvoidmy_device_ctor(void*obj){structmy_device*devobj;// 初始化固定部分每次分配对象时都会执行mutex_init(dev-lock);// 假设添加了锁INIT_LIST_HEAD(dev-list);dev-id-1;}// 创建设备structmy_device*my_device_create(intid,constchar*name){structmy_device*dev;devkmem_cache_alloc(dev_cache,GFP_KERNEL);if(!dev)returnERR_PTR(-ENOMEM);// 初始化可变部分dev-idid;strncpy(dev-name,name,sizeof(dev-name)-1);// 添加到全局列表mutex_lock(dev_mutex);list_add_tail(dev-list,dev_list);mutex_unlock(dev_mutex);returndev;}// 销毁设备voidmy_device_destroy(structmy_device*dev){mutex_lock(dev_mutex);list_del(dev-list);mutex_unlock(dev_mutex);kmem_cache_free(dev_cache,dev);}// 模块初始化staticint__initmy_module_init(void){// 创建专用缓存池dev_cachekmem_cache_create(my_device_cache,sizeof(structmy_device),0,SLAB_HWCACHE_ALIGN|SLAB_POISON,// 调试用生产环境可去掉 SLAB_POISONmy_device_ctor);if(!dev_cache)return-ENOMEM;// 创建一些设备for(inti0;i10;i){charname[32];snprintf(name,sizeof(name),device%d,i);my_device_create(i,name);}return0;}// 模块退出staticvoid__exitmy_module_exit(void){structmy_device*dev,*tmp;// 释放所有设备mutex_lock(dev_mutex);list_for_each_entry_safe(dev,tmp,dev_list,list){list_del(dev-list);kmem_cache_free(dev_cache,dev);}mutex_unlock(dev_mutex);// 销毁缓存池kmem_cache_destroy(dev_cache);}module_init(my_module_init);module_exit(my_module_exit);MODULE_LICENSE(GPL); 五、调试和监控1. 查看/proc/slabinfo$cat/proc/slabinfo|grepmy_device my_device_cache1010128301:tunables000:slabdata110字段含义10活跃对象数10总对象数128每个对象大小30每个 SLAB 中的对象数2. 使用slabtop实时监控$ slabtop-sc Active / Total Objects(% used):123456/150000(82.3%)Active / Total Slabs(% used):5000/5200(96.2%)OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME1010100%0.12K1104K my_device_cache3. 内核调试选项# 配置内核时启用调试选项CONFIG_SLUB_DEBUGyCONFIG_SLUB_DEBUG_ONy# 总是启用调试CONFIG_DEBUG_KMEMLEAKy# 检测内存泄漏调试特性对象毒化检测使用未初始化内存红区检查检测缓冲区溢出内存泄漏检测通过/sys/kernel/debug/kmemleak⚡ 六、高级使用技巧1. 每CPU缓存优化SLUB 自动为每个 CPU 维护一个本地对象缓存减少锁竞争// 分配时会优先从本地CPU缓存获取void*objkmem_cache_alloc(cache,GFP_KERNEL);// 本地CPU缓存访问不需要锁性能极高2. NUMA 感知分配// 从指定节点分配对象void*objkmem_cache_alloc_node(cache,GFP_KERNEL,node_id);3. 批量分配/释放// 批量分配SLUB 内部优化structmy_device*devs[32];for(inti0;i32;i){devs[i]kmem_cache_alloc(cache,GFP_KERNEL);}4. 使用kfree_rcu配合 RCUstructmy_data{intvalue;structrcu_headrcu;};voidupdate_data(structmy_data__rcu*ptr,intnew_value){structmy_data*newkmem_cache_alloc(cache,GFP_KERNEL);new-valuenew_value;structmy_data*oldrcu_replace_pointer(ptr,new,1);synchronize_rcu();// 等待读者完成kmem_cache_free(cache,old);}⚠️ 七、常见陷阱与最佳实践陷阱 1忘记销毁缓存// 错误模块卸载时未调用 kmem_cache_destroystaticvoid__exitbad_exit(void){// 缺失: kmem_cache_destroy(dev_cache);}后果内存泄漏内核报错 “cache still has objects”陷阱 2在中断中使用 GFP_KERNEL// 错误中断上下文不能睡眠irq_handler_thandler(intirq,void*data){devkmem_cache_alloc(cache,GFP_KERNEL);// 错误}正确做法使用GFP_ATOMIC陷阱 3构造函数使用不当// 构造函数只会在创建新 SLAB 时调用不会在每次分配时调用staticvoidbad_ctor(void*obj){int*refcountobj;*refcount1;// 错误构造函数不会在每次分配时执行}正确在kmem_cache_alloc后手动初始化最佳实践清单✅ 选择合适的对象大小考虑对齐和缓存行✅ 在模块__exit函数中销毁所有缓存✅ 使用SLAB_HWCACHE_ALIGN提高性能✅ 调试时启用SLAB_POISON和CONFIG_SLUB_DEBUG✅ 对于大对象 页大小考虑使用vmalloc或alloc_pages✅ 避免在缓存中混用不同生命周期的对象 八、性能对比分配方式操作开销缓存局部性碎片情况kmalloc(通用)中等一般有外部碎片专用缓存池低好无外部碎片get_free_pages高差严重实测数据100万次分配/释放kmalloc~150 毫秒专用缓存池~50 毫秒性能提升3倍专用内存缓存池是 Linux 内核性能优化的基石理解并正确使用它可以让你的内核模块和驱动获得接近硬件极限的性能表现。