深入解析kmem_cache:从创建到销毁的SLUB分配器实现

张开发
2026/4/10 20:00:33 15 分钟阅读

分享文章

深入解析kmem_cache:从创建到销毁的SLUB分配器实现
1. SLUB分配器与kmem_cache基础认知第一次看到kmem_cache这个名词时我也是一头雾水。这其实是Linux内核中一个非常精妙的设计专门用来高效管理内核对象的内存分配。想象一下你经营着一家汽车租赁公司如果每次客户来租车都临时去工厂定制新车效率肯定低下。而kmem_cache就像是你的停车场里面停放着预先准备好的各种车型内核对象随时可以快速交付使用。SLUBUnqueued Slab Allocator是当前Linux内核默认采用的分配器它的核心数据结构就是kmem_cache。与传统的SLAB分配器相比SLUB简化了管理结构减少了内存开销。我们可以通过一个简单的命令查看系统中的kmem_cache实例cat /proc/slabinfo输出结果中你会看到类似kmalloc-128、inode_cache这样的条目它们都是内核预创建的kmem_cache。每个kmem_cache管理着固定大小的对象比如inode_cache就专门用于分配inode结构体。这种设计带来了三大优势内存局部性同类型对象集中存放提高CPU缓存命中率减少碎片固定大小的对象分配避免了外部碎片快速分配通过预分配和对象复用机制加速分配过程2. kmem_cache的创建过程详解2.1 创建入口与参数校验让我们从一个实际例子入手——inode_cache的创建。在内核源码中可以看到这样的调用inode_cachep kmem_cache_create(inode_cache, sizeof(struct inode), 0, SLAB_RECLAIM_ACCOUNT|SLAB_PANIC, init_once);这个创建过程实际上经过了多层封装。kmem_cache_create是开发者最常用的接口但它内部调用了kmem_cache_create_usercopy。我曾在驱动开发中踩过一个坑在中断上下文中调用这个函数会导致内核oops因为它会获取互斥锁mutex_lock。参数校验是创建过程的第一道关卡。内核会检查名称不能为空指针对象大小必须在8字节到KMALLOC_MAX_SIZE之间标志位必须属于允许的范围SLAB_FLAGS_PERMITTED用户空间拷贝相关参数必须合法usersize/useroffset2.2 缓存合并优化策略内核有个很聪明的设计——缓存合并。当新建的kmem_cache与现有缓存特征匹配时会直接复用现有缓存。这个优化由__kmem_cache_alias函数实现其核心逻辑是检查slab_nomerge参数是否禁用合并对齐对象大小按void*大小对齐遍历现有缓存链表寻找匹配项匹配标准包括标志位相同SLAB_MERGE_SAME对象大小相差不超过一个指针大小对齐要求兼容我曾通过实验验证过这个机制的效果创建两个对象大小相差8字节的缓存当它们满足合并条件时在/proc/slabinfo中会显示为同一个缓存只是多了一个别名。2.3 核心数据结构初始化当没有找到可合并的缓存时内核会调用create_cache创建新缓存。这个过程就像搭建一个新仓库分配管理结构从kmem_cache缓存中分配一个kmem_cache结构体有点绕但这就是自举过程设置基本属性名称、对象大小、对齐方式等内存控制组初始化设置memcg相关参数调用__kmem_cache_create完成深度初始化__kmem_cache_create是真正的核心它通过kmem_cache_open完成以下关键操作// 设置缓存标志位 s-flags kmem_cache_flags(s-size, flags, s-name, s-ctor); // 计算各种尺寸参数 calculate_sizes(s, -1); // 设置partial链表参数 set_min_partial(s, ilog2(s-size)/2); set_cpu_partial(s); // 初始化节点管理结构 init_kmem_cache_nodes(s); // 分配per-CPU缓存 alloc_kmem_cache_cpus(s);3. kmem_cache的内存布局管理3.1 对象布局与内存对齐SLUB分配器对内存布局的处理非常精细。通过calculate_sizes函数内核会计算以下关键值object_size用户请求的对象大小size实际分配的大小包含元数据offset空闲指针的偏移量inuse对象实际使用的大小考虑对齐内存对齐的处理特别值得关注。在我的测试中发现当对象大小为30字节时在64位系统上实际会分配32字节考虑void*对齐。这种处理虽然会浪费少量内存但能显著提升访问性能。3.2 三级缓存结构设计SLUB采用了经典的三级缓存结构CPU本地缓存kmem_cache_cpu最快速分配路径包含一个活跃的slab页使用无锁操作通过cmpxchg_double实现节点缓存kmem_cache_node每个NUMA节点一个管理partial链表部分空闲的slab需要获取自旋锁访问全局页分配器当上述缓存不足时从伙伴系统分配新页面性能开销最大这种设计使得90%以上的分配请求都能在CPU本地缓存中完成我通过内核tracepoint实测的命中率确实能达到这个水平。3.3 调试支持与安全特性SLUB内置了强大的调试功能通过以下标志位启用SLAB_RED_ZONE在对象周围插入红色区域检测越界SLAB_POISON用特定模式填充对象0xa5a5a5a5SLAB_FREELIST_HARDENED强化空闲链表安全性在开发过程中我曾利用这些特性发现了好几个隐蔽的内存损坏问题。比如RED_ZONE可以帮助检测缓冲区溢出而POISON模式能快速发现使用已释放内存的情况。4. kmem_cache的销毁机制4.1 引用计数与销毁条件kmem_cache_destroy是创建过程的逆操作但它的触发条件更为严格。内核通过引用计数refcount来管理缓存生命周期每次创建新引用时refcount销毁时首先refcount--只有当refcount降为0时才真正销毁这个机制确保了安全性——避免还有对象在使用时缓存就被销毁。我在编写内核模块时就遇到过因为忘记释放对象导致缓存无法销毁的情况最后通过/proc/slabinfo中的引用计数找到了问题。4.2 销毁流程分步解析真正的销毁工作在shutdown_cache中完成主要步骤包括关闭kasan隔离区释放被kasan隔离的污染对象调用__kmem_cache_shutdown释放所有per-CPU缓存释放节点缓存回收所有slab页面解除memcg关联从全局链表移除处理sysfs相关清理特别需要注意的是RCURead-Copy-Update情况。对于标记了SLAB_TYPESAFE_BY_RCU的缓存销毁过程会被延迟到所有读者退出临界区之后。4.3 实际应用中的注意事项在真实项目中使用kmem_cache时有几个经验教训值得分享避免频繁创建/销毁缓存的重建成本很高应该尽量复用合理设置对象大小过大的对象会浪费内存过小会导致频繁分配监控/proc/slabinfo关注active_objs和num_objs的比例谨慎选择标志位错误的标志组合可能导致性能下降或内存浪费有一次我们的性能测试显示某个场景下内存分配成为瓶颈通过分析发现是因为没有设置SLAB_HWCACHE_ALIGN导致缓存行利用率低下。调整后性能提升了约15%。

更多文章