资讯详情

深入SLUB分配器:从快速路径到慢速路径的Object分配全解析

📅 2026/10/22 4:05:27 | 华诺云谱 👁 阅读
深入SLUB分配器:从快速路径到慢速路径的Object分配全解析
我先给一个直接的判断SLUB分配器的Object分配逻辑是整个SLUB设计里最能体现性能与复杂度折中的地方。前两篇分别讲了SLUB整体架构和Object布局的初始化这一篇我们把最核心的路径彻底拆开从快速路径的cmpxchg_double到慢速路径的partial补充再到最后从伙伴系统new_slab的兜底一条分配请求到底经历了什么每个环节为什么长这样代码和设计意图逐个掰清楚。这篇文章适合读过前两篇、对kmem_cache和freelist初始化有基本概念的同学也适合正在做内存性能分析、想理解slub_debug输出、或者想改SLUB参数调优的开发者。1. 从调用链看Object分配的整体设计1.1 一条分配请求的完整旅程内核里几乎所有小对象动态内存分配最终都会走到kmem_cache_alloc()这个接口上。kmalloc本质上是使用kmalloc_caches中按对象大小分好级的kmem_cache来分配而直接调用kmem_cache_alloc(s, gfp)时实际执行路径是kmem_cache_alloc() - slab_alloc() - slab_alloc_node() - __slab_alloc() // 快速路径条件不满足时进入 - ___slab_alloc() // 真正的慢速路径主体slab_alloc_node()被设计成一个__always_inline的快速入口函数核心职责非常纯粹先从当前CPU的kmem_cache_cpu结构中读取freelist如果有空闲对象就直接摘下链表头部的object返回只有这第一步失败才降级到__slab_alloc()再进入___slab_alloc()走完整慢速路径。你可能会问为什么kmem_cache_alloc()不干脆直接调用___slab_alloc()反而要先套一层快速路径的检查答案就一个字锁。快速路径全程使用this_cpu_*操作访问per-CPU变量不碰任何spinlock一次成功分配大约只需要几十个CPU周期。而一旦进入慢速路径可能要拿node结构里的list_lock去遍历partial链表更糟糕的情况下要调用alloc_pages()从伙伴系统拿新页面这个开销比快速路径高出两到三个数量级。所以SLUB的Object分配逻辑本质上就是围绕一个原则设计的把绝大多数分配请求挡在快速路径里让它们以最快的速度完成只有当当前CPU的缓存确实耗尽时才付出更大的代价去补充新的slab。这种快慢路径分离的思想不只SLUB在用内核里面向per-CPU的高频路径基本都采用了类似的套路。1.2 快慢路径的分岔条件为什么判断放在函数内层slab_alloc_node()里的分岔条件并不复杂核心判断就那么几行tid READ_ONCE(c-tid); object c-freelist; slab c-slab; if (unlikely(!object || !slab || !node_match(slab, node))) { object __slab_alloc(s, gfpflags, node, addr, c); } else { // 快速路径 void *next_object get_freepointer_safe(s, object); if (unlikely(!this_cpu_cmpxchg_double( s-cpu_slab, c-freelist, c-tid, object, tid, next_object, tid 1))) { // cmpxchg失败说明被抢占或并发修改重走读取过程 ... } }这里有个非常关键的设计点node_match(slab, node)的检查。SLUB允许在非本NUMA节点上分配对象如果一个slab虽然在本CPU的freelist里有对象但这个slab本身属于其他NUMA节点那么此时还走快速路径就有可能导致跨节点的内存访问这在NUMA架构下是很伤的。所以只要发现对象所在slab与目标节点不匹配就宁可慢一点也要把它归置到正确的节点上。另外注意this_cpu_cmpxchg_double()这个操作。它一次性比较并交换两个wordc-freelist和c-tid。为什么需要同时交换两个值因为freelist的更新必须和tid的递增绑定在一起否则就可能出现这样的竞态中断处理程序或抢占路径中又发生了一次分配/释放把freelist改了而你这边检测不到依然沿着旧的freelist指针往下走那拿出来的object就可能已经被别人用了问题就很严重了。tid在这里就像一个版本号任何一次操作都会让它递增快速路径通过cmpxchg_double同时验证版本号和freelist才能保证操作是在一个干净的状态下进行的。1.3 两个容易误解的细节interrupt和preempt快速路径在进入后会先执行local_irq_save(flags)关掉本地中断直到完成cmpxchg后才恢复。这是为了防止分配过程中被中断打断而中断处理程序里也可能调用kmem_cache_alloc()从而复用同一个kmem_cache_cpu结构。关于抢占内核里的this_cpu_*操作在CONFIG_PREEMPT开启时并不保证线程不被切换。所以SLUB的做法是配合tid的检查如果cmpxchg_double失败就说明在我们读取freelist和tid之后、执行cmpxchg之前发生了并发操作通常是中断或内核抢占此时不能再次尝试快速路径而是回到s reread的标签处重新读取一份最新的freelist和tid再做一次快速分配尝试。如果反复失败才会进入慢速路径。这种“失败就从头读读不到再降速”的方式是内核高并发路径里常见的乐观并发策略。2. 分配依赖的核心数据结构freelist、kmem_cache_cpu和node2.1 kmem_cache_cpu每个CPU的家底kmem_cache_cpu是SLUB分配给每个CPU的per-CPU结构它揭示了Object分配重点依赖的全部本地状态struct kmem_cache_cpu { void **freelist; // 指向当前CPU可用object链表的头 unsigned long tid; // 事务ID用于并发安全检测 struct slab *slab; // 当前CPU正在使用的slab struct slab *partial; // 当前CPU的partial slab链表 unsigned int cpu; // 所属CPU编号 unsigned int random; // 随机种子用于freelist硬化的混淆 unsigned int fp_offset; // freepointer在object内的偏移 };freelist指向的是空闲object链表的头部但这个链表并非独立的指针数组而是把指针存储在空闲object自身内部的某个偏移位置这个偏移量由fp_offset记录。分配时从freelist头部摘下一个object然后把freelist更新为object内部的next_object指针。这里有一个在阅读代码时容易困惑的点c-freelist和c-slab-freelist并不总是一回事。c-freelist可以理解为“当前CPU已经拿在手里、随时可以分配出去的object链表”而c-slab-freelist是“当前激活slab页面上的剩余空闲object链表”。在大多数时候c-freelist就是从slab-freelist上摘下来的一小批对象二者的关系需要结合c-slab的赋值时机来理解。tid字段在每次成功分配或释放时都会递增它配合快速路径的cmpxchg_double使用防止并发错乱。c-partial则维护着一个本CPU专用的partial slab链表——这是避免慢速路径频繁锁node链表的重要缓冲层后续会专门讲。2.2 kmem_cache_node跨CPU共享的partial池全局而言每个NUMA节点都有一个kmem_cache_node结构来管理这个节点上的slab资源struct kmem_cache_node { spinlock_t list_lock; // 保护partial链表的锁 unsigned long nr_partial; // partial链表中的slab总数 struct list_head partial; // partial slab链表 };list_lock就是慢速路径必须面对的锁。node的partial链表是多个CPU共享的任何一个CPU在往链表里添加或摘取slab时都必须持有这把锁。所以SLUB会在CPU本地维护一个partial链表目的就是宁愿让这些slab在CPU本地暂存也不让每次分配/释放都去抢list_lock。从性能角度理解这个层次kmem_cache_cpu的优先级最高分配时先碰CPU local的freelist和partialkmem_cache_node的优先级次之是跨CPU共享的后备池伙伴系统优先级最低是最后的兜底来源。这个三层存储结构是理解整个Object分配逻辑的骨架。2.3 freelist链接关系freepointer藏在哪里freepointer是SLUBfreelist实现里一个很精巧的设计。传统SLAB分配器用bitmap记录空闲对象而SLUB把“下一个空闲对象”的指针直接存储在空闲对象自身内部的固定偏移处。为什么要这么设计最主要的原因是为了节省空间。如果每个slab用一个bitmap假设一个slab里有256个object光bitmap就得占32字节而且还要额外维护对象状态数组。SLUB把freepointer放在object内部空闲状态下这个object不需要保存用户数据所以这些字节是空闲的不会产生额外开销。代价就是如果你在CONFIG_SLAB_FREELIST_HARDENED开启时去直接读取这个指针会发现它是被混淆过的。CONFIG_SLAB_FREELIST_HARDENED是内核为了加固内存破坏漏洞引入的配置。开启后freepointer会被写入一个经过混淆的值读取时需要解码static inline void *freelist_ptr(const struct kmem_cache *s, void *ptr, unsigned long ptr_addr) { #ifdef CONFIG_SLAB_FREELIST_HARDENED return (void *)(s-random ^ ptr ^ swab(ptr_addr)); #else return ptr; #endif }这里ptr_addr是freepointer所在的地址即object地址加偏移s-random是cache创建时生成的随机值。这样即使攻击者改写了某个object里的freepointer解码后极大概率指向非法地址分配时就会被检查逻辑拦下。这个机制在分配快速路径里表现为get_freepointer_safe()和get_freepointer()两种变体前者还会多做一次sanity check。2.4 新版本内核的形态差异slab与folio近年来内核版本中SLUB代码大量使用struct slab而不是struct page来表示一个slab页面。struct slab是一个与struct page共享存储空间的联合体类型专门用于slub管理。如果你翻看不同版本的源代码看到c-slab、page-slab_cache、slab-freelist等字段都是同一个概念的不同命名。阅读代码时要注意版本但设计思想完全一致。这篇博客从逻辑层面讲不对具体版本做严格限制只要你理解了freelist、partial、node三层结构迁移到任何代码版本都不难。3. 快速路径解析一次最爽的分配怎么完成3.1 快速路径的完整步骤分解快速路径是SLUB Object分配的“理想世界”整个过程可以拆成6个步骤关闭本地中断防止分配过程中被中断路径打扰读取当前CPU的c-freelist、c-slab、c-tid检查!object || !slab || !node_match(slab, node)任一条件为真就进入慢速路径预先读取下一个空闲对象指针next_object get_freepointer_safe(s, object)执行this_cpu_cmpxchg_double(s-cpu_slab, c-freelist, c-tid, object, tid, next_object, tid1)成功后返回object恢复中断这6步里最需要琢磨的是第4步和第5步之间的配合。注意第4步在cmpxchg之前就预读了next_object然后第5步的cmpxchg会把freelist从object更新为next_object同时将tid从tid更新为tid1。那为什么要多此一举地预读呢从设计意图来看预读是在为cmpxchg成功之后的操作做准备——如果cmpxchg成功我们不需要再读一次object内部指针如果cmpxchg失败预读的值作废重新走流程。这种先取值、后提交的模式在无锁数据结构里很常见关键是要配合版本号保证取到的值没有过期。这里再展开一下tid的作用。快速路径要求tid在读取后到cmpxchg执行前没有被改变。如果这个期间发生了中断、抢占或远程操作tid就会变化cmpxchg就会失败。失败的代价并不高只是跳转到重新读取的标签处再走一遍但如果连续失败就会进入慢速路径。内核里还有一个note_cmpxchg_failure()函数专门统计这种失败事件当你发现这个计数在增长时往往说明某个缓存经常发生并发竞争。3.2 prefetch与硬件预取为了下一个对象更快快速路径最后还有个小彩蛋prefetch_freepointer()。当我们成功分配出object后会尝试对get_freepointer(s, object)指向的下一个空闲对象做一次prefetchnta指令预取。为什么要有这一步因为下一次分配几乎肯定拿到的是next_object而这个对象所在的内存行很可能尚未加载到CPU cache中。提前把它预取到cache就能让下一次分配的访存延迟降低。当然并非所有情形都适合预取static void prefetch_freepointer(const struct kmem_cache *s, void *object) { if (object NULL) return; if (s-offset) { prefetch(object s-offset); } else { prefetch(object); } }在slub_min_order较大、object分布相对稀疏时预取会更有效。而在object非常紧凑、同一个cacheline内包含多个object的情况下预取的收益相对有限。实测中这个优化对高吞吐分配场景比如网络收发包、io调度器频繁分配请求块能有可感知的提升。3.3 快速路径的性能边界快速路径也有它的边界。一个很典型的情况是c-freelist里明明还有对象但对象所在的slab不是目标NUMA节点。此时SLUB宁可放弃快速路径也要保证NUMA亲和性。另一个边界是当CONFIG_SLUB_DEBUG开启时调试模式下会有额外的校验逻辑快速路径很难完整走通性能会明显下降。这也是为什么生产环境强烈建议不要开启slub_debug的原因。一般实测下来在纯快速路径下一次kmem_cache_alloc()的开销大约在20~40个CPU周期之间具体取决于cmpxchg_double的代价和cache命中的情况。这个数字已经非常接近理论极限毕竟连查一次链表都要十几个周期。4. 慢速路径全流程拆解___slab_alloc内部的分支修罗场4.1 慢速路径的触发条件与初始处理凡是快速路径没能搞定的分配请求都会进入__slab_alloc()最终落到___slab_alloc()。这个函数的分支非常多但它解决的无非三类问题当前CPU的c-slab存在但slab-freelist已经为空当前slab耗尽当前CPU的c-slab为NULL需要从partial或伙伴系统获取一个可用的slab上面两种情况里遇到NUMA不匹配或pfmemalloc限制需要特殊处理___slab_alloc()首先会对gfpflags做一次过滤去掉gfp_allowed_mask之外的标志。随后统计ALLOC_SLOWPATH事件方便后续通过/sys/kernel/slab/cache/alloc_slowpath观察慢速路径的频率。紧接着是处理pfmemalloc的情况。如果当前请求没有携带__GFP_MEMALLOC而c-slab是一个内存紧缺时用__GFP_MEMALLOC从保留内存里拿到的slab那么这个slab不能用于普通请求必须先从c-slab里释放出来。这个细节平时不太容易遇到但在系统内存极端紧张时它会决定你能否成功分配到一个关键对象。4.2 当c-slab存在但freelist耗尽deactivate_slab逻辑如果c-slab存在但是它的freelist已经为空说明这个slab的所有object都被分配出去了。此时的处理逻辑由deactivate_slab()完成它要决定这个“空壳slab”的去向如果这个slab的frozen状态为false说明它不在任何CPU手里冻结应该归还到node partial链表如果它的pobjects或者pages达到某个阈值也可以先挂到CPU partial链表如果整个partial链表已经足够长或者这个slab已经不满足partial条件就直接释放回伙伴系统。这里有个细节要特别注意deactivate_slab()不是简单地把slab丢到node partial就完事。当CPU partial链表的总量超过slub_cpu_partial限定时还会触发unfreeze_partials()把CPU partial里面所有slab一次性丢到node partial或直接释放。这个机制防止CPU partial无限膨胀造成内存占用失衡。为什么会有frozen这个概念因为一个slab在某个CPU上被“冻结”使用中时其他CPU不能将其从node partial链表中摘走。SLUB用slab-frozen标记来区分“这个slab正在被某个CPU使用”和“这个slab在共享的partial池里”。如果忘了维护这个标志就可能出现两个CPU同时认为某个slab是自己的导致freelist被并发修改。4.3 当c-slab为NULL从partial补货在c-slab被清空或置NULL后___slab_alloc()开始寻找新的可用slab。路径分三步优先级从高到低第一优先是get_partial_cpu()。这一步只访问c-partial链表不需要拿任何锁只需要将它维护的slab链表中取出一个slab重新激活。因为不需要跨CPU同步所以这个操作很快。如果CPU partial里有货慢速路径也就比快速路径多几个周期的开销。第二优先是get_partial_node()。这一步要从对应当前NUMA节点的kmem_cache_node的partial链表里取slab。此时必须获取n-list_lock然后在链表中遍历通过acquire_slab()把某个符合条件的slab摘下来。有一个值得注意的点get_partial_node()在取出一个slab后并不会立即把整个链表都搬空而是一次一个地取控制锁的持有时间避免长时间锁住共享链表造成其他CPU饥饿。第三优先是get_any_partial()。这一步会在所有NUMA节点上尝试寻找partial slab。触发条件一般是node参数是NUMA_NO_NODE或者本节点确实找不到合适的slab。跨节点分配对NUMA架构来说是一种性能退化的选择所以SLUB对它做了比较保守的处理一般建议尽量避免走到这一步。4.4 partial补货的具体实现acquire_slab与计数器更新acquire_slab()是把slab从node partial链表摘下来并交给当前CPU的核心函数。它做的事情包括static bool acquire_slab(struct kmem_cache *s, struct kmem_cache_node *n, struct slab *slab, int nr_objects) { if (!pfmemalloc_match(slab, gfpflags)) return false; if (node ! NUMA_NO_NODE slab_nid(slab) ! node) return false; list_del(slab-slab_list); n-nr_partial - nr_objects; slab-frozen 1; return true; }这里最重要的是frozen 1的赋值。一旦这个slab被当前CPU接管它的frozen就被置为1表示它已经不在共享partial池中其他CPU不再可能把它取走。同时n-nr_partial要减去这个slab上实际的object数量而不是简单地减1这是很多人在看代码时容易忽略的地方——nr_partial记录的是partial链表中所有slab上剩余的object总数而不只是slab数量。在get_partial_node()里还有一个微妙的细节它可能一次从node partial中取出一个slab后发现该slab也被掏空了freelist为NULL于是把它放回去再取下一个。这种情况在并发环境下并不罕见处理不好就会造成活锁。SLUB通过限制单次取slab的数量上限来规避这个问题。4.5 当partial也空new_slab兜底如果CPU partial和node partial都找不到可用的slab那就只能走最后一条路new_slab()即从伙伴系统中分配新的页把它变成一个slab并初始化freelist。这一步是整个分配路径里最重的操作因为它涉及与页面分配器的交互还可能触发直接回收、内存规整等更昂贵机制。new_slab()的基本流程是检查gfpflags合法性尤其是GFP_SLAB_BUG_MASK标志组合根据slub_min_order、slub_max_order、slub_min_objects等参数计算出本次需要分配几页order调用alloc_pages()从伙伴系统拿页面对拿到的页面做init_slab_page()初始化通过allocate_slab()计算对象数量并初始化freelist。这一步还涉及shuffle_freelist()和init_freelist()的选择我们下一节专门展开。5. 新slab的创建与freelist初始化5.1 order与objects如何被确定在kmem_cache_create()创建缓存时系统会根据对象大小、对齐要求、以及slub_min_objects、slub_max_order等参数通过calculate_order()算出一个合适的order。分配新slab时new_slab()会直接使用这个已经算好的order如果失败才会考虑降低order。这个order的选择逻辑非常讲究。一个slab要能装下尽量多的object才能减少与伙伴系统的交互频率但order越大单个slab占用的连续内存越多伙伴系统分配大页面的成功率就越低。所以SLUB的调参思路是在slub_min_objects足够的情况下尽量把订单控制在slub_max_order以下如果对象太小order可以大一些以容纳更多object如果对象很大order就必须小否则一个slab就占太多内存。通常不同size的kmem_cache会有完全不同的order比如kmalloc-8可能order3一个slab里装着上千个object而task_struct这种大对象缓存可能是order0一个slab就一页装不了几个对象。这也是为什么你在/sys/kernel/slab/下看每个cache的order和objects都不一样的原因。5.2 init_slab_page和allocate_slab的详细流程当new_slab()从伙伴系统拿到页面后第一件事是init_slab_page()static inline void init_slab_page(struct slab *slab, struct kmem_cache *s) { slab-slab_cache s; slab-freelist NULL; slab-inuse 0; slab-objects calculate_objects(s); slab-frozen 0; slab-pobjects 0; slab-pages 0; }这里slab_cache指向所属的kmem_cache而freelist暂时为空因为还没有建立起空闲对象链表。真正的初始化在allocate_slab()里完成它根据object在slab中的排列方式决定如何把所有object串成一个freelist。allocate_slab()中有一个值得展开的细节s-offset。当object大小大于一个word且能够容纳freepointer时freepointer就放在object内部但如果object大小非常小比如kmalloc-8整个object连一个指针都放不下此时freepointer就不能存到object内部而是使用slab页面的slab-freelist区域存储。这种情况下offset可能为0表示freepointer存放在object所在页面上的专用位置而fp_offset记录了它在object内的实际偏移。具体对应到代码里就是set_freepointer()和get_freepointer()在offset0和offset!0时的不同分支。5.3 freelist初始化init_freelist与shuffle_freelistfreelist的初始化有两种方式init_freelist()和shuffle_freelist()。init_freelist()是简单版本它按object地址顺序把链表串起来static void init_freelist(struct kmem_cache *s, struct slab *slab) { int object_size s-object_size; int idx; void *start slab_address(slab); void *cur; void *next; void *freelist NULL; for (idx 0; idx slab-objects; idx s-align) { cur start idx * object_size; next ...; set_freepointer(s, cur, freelist); freelist cur; } slab-freelist freelist; }注意它每次跳过align个slot这是因为SLUB为了对齐要求或硬件预取可能在slab内部填充一些空洞这些空洞不能算作可分配的object。shuffle_freelist()则是在CONFIG_SLAB_FREELIST_RANDOM开启时使用的版本。它使用伪随机数生成器打乱freelist的顺序目的有两个一是让相邻的几个object在物理上不连续防止顺序分配的object集中在同一块缓存区域二是配合SLAB_FREELIST_HARDENED对抗堆喷射攻击。代价是初始化时多了一些随机数运算而且因为freelist不再按地址顺序排列prefetch的命中率会有轻微下降。5.4 把新slab交给CPU从new_slab到c-freelist当new_slab()成功返回一个已经初始化好freelist的slab后___slab_alloc()会做最后一步整合if (likely(slab)) { freelist slab-freelist; slab-freelist get_freepointer(s, freelist); stat(s, ALLOC_REFILL); c-freelist get_freepointer(s, freelist); c-slab slab; ... }注意这里的问题slab-freelist取出了第一个object之后c-freelist也被设置为get_freepointer(s, freelist)即第二个object的地址。如此设置的目的是为了让后续的快速路径可以直接从c-freelist继续分配而不必再回到slab上去读。而c-slab则记录了这个slab方便释放操作时快速定位。与此同时如果当前CPU之前还有其他slab被冻结___slab_alloc()会检查是否需要将旧slab的残余状态清理掉。这一点在代码里表现为c-slab的重新赋值和deactivate_slab()的成对调用。5.5 SLAB_TYPESAFE_BY_RCU与分配的特殊性使用SLAB_TYPESAFE_BY_RCU标志的cache在分配路径里有个独特的约定分配器不会在分配时清空object内容也不会在free时真正清除数据它允许RCU读侧在对象释放后仍能安全访问旧数据。这意味着这类对象上的用户数据不能依赖SLUB的初始化保证而且重用时内核会在freelist里设置一个特殊的“毒化”标记防止重用后的数据被误读。对于写驱动或者内核模块的朋友如果你使用了kmem_cache_create的SLAB_TYPESAFE_BY_RCU标志请务必在分配给用户空间或后续使用者之前自己完成数据的完整初始化。因为SLUB分配逻辑只保证给你一个“可用的内存地址”并不保证内容被清零这是系列文章中我反复强调过的最容易踩的坑。6. 常见问题与排查技巧实录6.1 如何判断一个cache的慢速路径占比在实际定位性能问题时一个很重要的手段是观察/sys/kernel/slab/cache-name/目录下的统计文件。SLUB在CONFIG_SLUB_STATS编译选项下会记录每个cache的分配统计ls /sys/kernel/slab/kmalloc-128/里面有alloc_fastpath、alloc_slowpath、cpu_partial_alloc、cpu_partial_free等文件。如果你发现某个cache的alloc_slowpath数值在总分配量里占比很高说明这个cache的per-CPU freelist经常为空需要频繁去node partial或者伙伴系统补充slab。针对这种情况比较直接的手段是调大slub_min_objects、slub_max_order或者调大cpu_partial的上限值让单个slab承载更多object也更容易在CPU本地保留下充足的partial资源。当然这些参数对整个系统是全局的修改前要做基准测试。6.2 排查内存异常slub_debug追踪object生命周期当怀疑某个对象被use-after-free或double-free时可以借助slub_debug来排查。在启动参数中加入slub_debugP,slabcache-name其中P表示在free时对object填充特定字节模式slab限定只追踪指定cache避免日志量过大。分配时SLUB会把object用0x6b填充free时用0xab填充。如果代码在free之后又去读这个对象里面全是0xab非常容易辨认而double-free时分配器会检测到freelist指针已经在对象内部被改写触发红名单告警。我实测遇到过一种情况某个驱动释放request结构后并没有把该结构从硬件队列中摘除导致中断处理中又访问了这块内存。开启slub_debugP后从崩溃日志里看到了0xab填充的数据很快就定位到了问题比盲猜代码段节省了大量时间。6.3 关于frozen状态的现场观测调试SLUB逻辑时很难直接从崩溃现场看到frozen的值因为struct slab和struct page是联合体很多字段在不同状态下含义不同。我的经验是借助crash工具或gdb直接读取kmem_cache_cpu和kmem_cache_node中的链表头再反向检查每个slab的frozen标志。如果发现一个slab的frozen1但它同时出现在某个node的partial链表中这就说明状态不一致通常是并发路径上deactivate_slab()的竞态问题。当然绝大多数情况下你不需要自己调试SLUB内部slub_debug已经覆盖了大部分使用层面的问题。只有当你修改了SLUB参数或者写了一个新的per-CPU缓存分配路径时才需要关注frozen和partial链表的一致性。6.4 CONFIG_SLAB_FREELIST_HARDENED对性能的影响在开启CONFIG_SLAB_FREELIST_HARDENED的系统上每次从freelist摘取或挂入一个object都要做一次异或解码/编码。虽然这个操作只有几个CPU周期但在极高频分配场景下还是会造成百分之几的性能损失。因此做性能敏感的内核开发时要清楚发行版默认开启了哪些加固选项不要想当然地用开卷代码上的周期数去估算实际性能。另一方面加固选项带来的安全性收益是实打实的。它配合CONFIG_SLAB_FREELIST_RANDOM使堆布局随机化也能有效检测一部分内存破坏。对于需要在内核里处理不可信输入的模块这个开关值得开启。6.5 一个调优实践以网络收包缓存为例最后分享一个我实际做过的调优案例。某网络设备上网络栈使用的kmem_cache比如与skb head相关的cache在高PPS场景下alloc_slowpath比例接近15%导致分配热点集中在___slab_alloc()里单核吞吐上不去。当时的调整思路是先通过/sys/kernel/slab/cache/objects确认单个slab的容量适度调大slub_min_objects让单个slab容纳更多对象减少慢速路径触发次数增大cpu_partial的上限让更多空闲slab留在CPU本地partial重新跑基准slowpath占比降到3%以下吞吐提升约8%。注意这组参数是全局的调大后内核整体更倾向于为各类cache保留更多partial slab所以物理内存占用会略有上升。内存与性能之间的权衡只能针对具体的部署环境来定。从这些实操经验回头再看SLUB的Object分配逻辑你会发现它像一个精心维护的三级缓存系统CPU freelist是L1CPU partial是L2node partial是L3伙伴系统是最后的内存源。每一级都通过不同的同步机制来保证性能与正确性的平衡快速路径几乎是“零同步”慢速路径则逐步升级到锁和全局操作。在我个人调试内核内存问题的经历里理解这三级缓存之间的关系比记住任何单个函数的代码都更重要因为绝大多数定位问题的场景最后都要归结为一个问题这个对象到底是从哪一级缓存来又是通过哪条路径落到当前CPU上的。搞清这个问题SLUB的分配逻辑就算真正掌握了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑