夜雨聆风学习资料网

ARTICLE · 1087856

Linux 6.12 源码深度剖析: ___slab_alloc

Linux 6.12 源码深度剖析: ___slab_alloc

Linux 6.12 内核深度解析:___slab_alloc 核心机制与多核协同架构

1. 📌 技术点速览

        在 Linux 内核中,___slab_alloc 是 MemoryManagement (内存管理系统) 模块中 SLUB 分配器的慢速路径(Slow Path)核心控制引擎。

        当内核执行 kmalloc 或 kmem_cache_alloc 时,会首先尝试极其高效的“快速路径(Fast Path)”——直接通过无锁的 this_cpu_cmpxchg 从当前 CPU 的本地缓存 kmem_cache_cpu 的 freelist 中获取空闲对象。然而,一旦本地空闲链表枯竭、发生 NUMA 节点不匹配、或者发生 CPU 抢占重调度,分配流程就会退化并调用 ___slab_alloc。

  ___slab_alloc 的核心职责是:在保证多核并发安全与 NUMA 亲和性的前提下,通过多级级联回溯策略(本地 CPU 局部链表 -> CPU Partial 链表 -> Node Partial 链表 -> 伙伴系统页分配器),为当前 CPU 重新填充(Refill)高可用性的 Slab 缓存,并返回可用的内存对象。


2. 🗺️ 软件功能架构图

        下图展示了以 ___slab_alloc 为核心的内存分配多级回溯架构,清晰地呈现了 MemoryManagement (内存管理系统) 与 ProcessScheduler (进程调度器) 之间的跨模块协同关系。


3. 🔍 核心源码硬核解析 (基于 Linux 6.12)

        在 arm64/x86_64 架构下,SLUB 分配器的核心慢速路径实现位于 mm/slub.c。以下是 ___slab_alloc 函数的关键源码解析。

/*  * 源码路径: mm/slub.c * 核心职责: SLUB 分配器慢速路径分配,负责多级缓存填充与 NUMA 亲和性适配 */static void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,              unsigned long addr, struct kmem_cache_cpu *c, unsigned int orig_size){    void *freelist;    struct slab *slab;    unsigned long flags;    struct partial_context pc;    bool try_thisnode = true;    /* 增加慢速路径分配的统计计数 */    stat(s, ALLOC_SLOWPATH);reread_slab:    /*      * [步骤 1] 读取当前 CPU 绑定的 slab。     * 使用 READ_ONCE 防止编译器优化,因为当前线程可能被抢占并迁移到其他 CPU     */    slab = READ_ONCE(c->slab);    if (!slab) {        /* 如果指定的 NUMA 节点不在线,则忽略节点限制,设为 NUMA_NO_NODE */        if (unlikely(node != NUMA_NO_NODE &&                 !node_isset(node, slab_nodes)))            node = NUMA_NO_NODE;        goto new_slab;    }    /*      * [步骤 2] 检查当前 Slab 是否与请求的 NUMA 节点匹配。     * 如果不匹配,说明发生了跨节点访问,需要将当前 Slab 钝化(Deactivate)     */    if (unlikely(!node_match(slab, node))) {        if (!node_isset(node, slab_nodes)) {            node = NUMA_NO_NODE;        } else {            stat(s, ALLOC_NODE_MISMATCH);            goto deactivate_slab; /* 节点不匹配,剥离当前 Slab */        }    }    /* 校验 pfmemalloc 标志,确保紧急内存分配安全 */    if (unlikely(!pfmemalloc_match(slab, gfpflags)))        goto deactivate_slab;    /*      * [步骤 3] 获取本地 CPU 锁,防止中断和抢占干扰。     * 在 Linux 6.12 中,使用 local_lock_irqsave 代替了传统的 raw_local_irqsave,     * 这对 RT (Real-Time) 内核更加友好。     */    local_lock_irqsave(&s->cpu_slab->lock, flags);    /*      * 双重检查(Double Check):     * 在获取锁之前,当前线程可能被中断或抢占,导致 c->slab 发生变化。     * 如果变化了,必须释放锁并重新读取。     */    if (unlikely(slab != c->slab)) {        local_unlock_irqrestore(&s->cpu_slab->lock, flags);        goto reread_slab;    }    freelist = c->freelist;    if (freelist)        goto load_freelist;    /* 从当前已冻结的 slab 中获取空闲对象链表 */    freelist = get_freelist(s, slab);    if (!freelist) {        /* 如果当前 slab 确实没有空闲对象了,将其解绑 */        c->slab = NULL;        c->tid = next_tid(c->tid); /* 更新 Transaction ID,防止 ABA 问题 */        local_unlock_irqrestore(&s->cpu_slab->lock, flags);        stat(s, DEACTIVATE_BYPASS);        goto new_slab;    }    stat(s, ALLOC_REFILL);load_freelist:    /* 确保当前 CPU 已经持有了 cpu_slab 的本地锁 */    lockdep_assert_held(this_cpu_ptr(&s->cpu_slab->lock));    /*      * 核心断言:用于 CPU 分配的 Slab 必须处于 "Frozen" (冻结) 状态。     * 冻结状态意味着其他 CPU 不能向该 Slab 释放对象,所有释放的对象都会挂在临时链表上,     * 从而保证了当前 CPU 对该 Slab 的独占无锁写入。     */    VM_BUG_ON(!c->slab->frozen);    /* 更新当前 CPU 的 freelist 指针,指向下一个空闲对象 */    c->freelist = get_freepointer(s, freelist);    c->tid = next_tid(c->tid); /* 递增事务 ID */    local_unlock_irqrestore(&s->cpu_slab->lock, flags);    return freelist;deactivate_slab:    /*      * [步骤 4] 钝化 Slab 流程。     * 当 Slab 已满或 NUMA 节点不匹配时,需要解除当前 CPU 与该 Slab 的绑定,     * 并将其放回 Node 的 Partial 链表或完全释放。     */    local_lock_irqsave(&s->cpu_slab->lock, flags);    if (slab != c->slab) {        local_unlock_irqrestore(&s->cpu_slab->lock, flags);        goto reread_slab;    }    freelist = c->freelist;    c->slab = NULL;    c->freelist = NULL;    c->tid = next_tid(c->tid);    local_unlock_irqrestore(&s->cpu_slab->lock, flags);    deactivate_slab(s, slab, freelist); /* 实际执行钝化,可能涉及解冻(Unfreeze) */new_slab:#ifdef CONFIG_SLUB_CPU_PARTIAL    /*      * [步骤 5] 尝试从 CPU 本地 Partial 链表中获取 Slab。     * 这是为了避免获取 Node 级别的全局锁 `n->list_lock`,从而提升多核并发性能。     */    while (slub_percpu_partial(c)) {        local_lock_irqsave(&s->cpu_slab->lock, flags);        if (unlikely(c->slab)) {            local_unlock_irqrestore(&s->cpu_slab->lock, flags);            goto reread_slab;        }        if (unlikely(!slub_percpu_partial(c))) {            local_unlock_irqrestore(&s->cpu_slab->lock, flags);            goto new_objects;        }        /* 从 CPU Partial 链表中弹出一个 Slab */        slab = slub_percpu_partial(c);        slub_set_percpu_partial(c, slab); /* 更新 CPU Partial 链表表头 */        /* 检查节点匹配性 */        if (likely(node_match(slab, node) &&               pfmemalloc_match(slab, gfpflags))) {            c->slab = slab;            freelist = get_freelist(s, slab);            VM_BUG_ON(!freelist);            stat(s, CPU_PARTIAL_ALLOC);            goto load_freelist; /* 成功加载 */        }        local_unlock_irqrestore(&s->cpu_slab->lock, flags);        /* 如果节点不匹配,则将该 Slab 移出 CPU Partial,放回 Node Partial */        slab->next = NULL;        __put_partials(s, slab);    }#endifnew_objects:    pc.flags = gfpflags;    /*      * 优化策略:如果指定了特定 NUMA 节点,先尝试以 GFP_NOWAIT | __GFP_THISNODE      * 快速从该节点获取,避免跨节点锁竞争。     */    if (unlikely(node != NUMA_NO_NODE && !(gfpflags & __GFP_THISNODE)             && try_thisnode))        pc.flags = GFP_NOWAIT | __GFP_THISNODE;    pc.orig_size = orig_size;    /*      * [步骤 6] 尝试从 Node 级别的 Partial 链表中获取 Slab。     * 此过程会持有 `n->list_lock` 锁。     */    slab = get_partial(s, node, &pc);    if (slab) {        if (kmem_cache_debug(s)) {            /* 调试模式下的特殊处理 */            freelist = pc.object;            if (s->flags & SLAB_STORE_USER)                set_track(s, freelist, TRACK_ALLOC, addr,                      gfpflags & ~(__GFP_DIRECT_RECLAIM));            return freelist;        }        /*          * 核心操作:冻结(Freeze)获取到的 Slab。         * 冻结后,该 Slab 专属于当前 CPU,其他 CPU 释放对象时不能直接操作其 freelist。         */        freelist = freeze_slab(s, slab);        goto retry_load_slab;    }    /*      * [步骤 7] 终极 fallback:向伙伴系统申请全新的物理页框。     * 在调用 new_slab 期间,由于可能发生阻塞(取决于 gfpflags),     * 必须先释放当前 CPU 的 cpu_slab 指针,避免持有引用跨越阻塞点。     */    slub_put_cpu_ptr(s->cpu_slab);    slab = new_slab(s, pc.flags, node); /* 跨模块调用:伙伴系统分配器 */    c = slub_get_cpu_ptr(s->cpu_slab);    if (unlikely(!slab)) {        /* 如果带 __GFP_THISNODE 的本地分配失败,则放宽限制,允许跨节点分配 */        if (node != NUMA_NO_NODE && !(gfpflags & __GFP_THISNODE)            && try_thisnode) {            try_thisnode = false;            goto new_objects;        }        slab_out_of_memory(s, gfpflags, node); /* OOM 报错 */        return NULL;    }    stat(s, ALLOC_SLAB);    if (kmem_cache_debug(s)) {        freelist = alloc_single_from_new_slab(s, slab, orig_size);        if (unlikely(!freelist))            goto new_objects;        if (s->flags & SLAB_STORE_USER)            set_track(s, freelist, TRACK_ALLOC, addr,                      gfpflags & ~(__GFP_DIRECT_RECLAIM));        return freelist;    }    /*      * 初始化新分配的 Slab。     * 因为是全新分配的,没有其他 CPU 竞争,所以可以直接修改其属性,无需 cmpxchg。     */    freelist = slab->freelist;    slab->freelist = NULL;    slab->inuse = slab->objects;    slab->frozen = 1; /* 设为冻结状态 */    inc_slabs_node(s, slab_nid(slab), slab->objects);    if (unlikely(!pfmemalloc_match(slab, gfpflags))) {        deactivate_slab(s, slab, get_freepointer(s, freelist));        return freelist;    }retry_load_slab:    /*      * [步骤 8] 将新获取或新分配的 Slab 绑定到当前 CPU 的 c->slab。     * 绑定前,如果发现当前 CPU 又被绑定了新的 slab(可能在释放锁期间被中断处理程序填充),     * 则需要先将那个 slab 钝化。     */    local_lock_irqsave(&s->cpu_slab->lock, flags);    if (unlikely(c->slab)) {        void *flush_freelist = c->freelist;        struct slab *flush_slab = c->slab;        c->slab = NULL;        c->freelist = NULL;        c->tid = next_tid(c->tid);        local_unlock_irqrestore(&s->cpu_slab->lock, flags);        deactivate_slab(s, flush_slab, flush_freelist); /* 钝化冲突的 Slab */        stat(s, CPUSLAB_FLUSH);        goto retry_load_slab;    }    /* 成功绑定 */    c->slab = slab;    goto load_freelist;}

💡 核心设计哲学解析:何为 "Slab 冻结 (Freeze)"?

        在 ___slab_alloc 中,频繁出现 frozen 状态的操作。这是 SLUB 区别于传统 SLAB 的精妙之处:

  • 未冻结状态(Unfrozen):Slab 处于全局共享池中(如 Node 的 Partial 链表)。任何 CPU 都可以从中分配或释放对象,但必须通过 n->list_lock 自旋锁或复杂的 cmpxchg_double 来保证原子性。

  • 已冻结状态(Frozen):当一个 Slab 被 ___slab_alloc 选中并绑定到某个 CPU 的 c->slab 时,它会被标记为 frozen = 1。此时,只有该 CPU 拥有从该 Slab 分配对象的权力。

  • 跨核释放的处理:如果 CPU-A 占有了 Slab-X(处于 Frozen 状态),而 CPU-B 此时释放了一个属于 Slab-X 的对象,CPU-B 发现该 Slab 是 Frozen 的,它不会将对象放回 slab->freelist,而是将其挂在 slab->freelist 的一个临时并发链表上。当 CPU-A 最终用完该 Slab 并执行 deactivate_slab(解冻)时,才会统一合并这些跨核释放的对象。这种设计极大地减少了多核之间的 Cache Line 冲突和锁竞争。


4. ⚙️ 系统运行背景与上下文

___slab_alloc 绝非孤立运行,它处于 Linux 内核三大核心子系统(内存、调度、中断)的交汇处。

+-------------------------------------------------------------------+|                      ProcessScheduler (调度器)                     ||  - 维护进程上下文、抢占状态                                          ||  - 提供抢占/重调度感知 (TID 机制)                                    |+-----------------------------------+-------------------------------+                                    |                                    | 触发慢速路径 (Preempted / TID mismatch)                                    v+-----------------------------------+-------------------------------+|                 MemoryManagement (SLUB 分配器)                     ||  - ___slab_alloc 控制核心                                          ||  - 维护 CPU 本地缓存 (c->slab) 与 Node 局部链表                     |+-----------------+---------------------------------+---------------+                  |                                 |                  | 物理页框申请 (new_slab)          | 关中断保护 (local_lock)                  v                                 v+-----------------+-----------------+ +-------------+---------------+|   MemoryManagement (Buddy System) | |       Interrupts (中断子系统)||  - 伙伴系统页分配器                | |  - 避免中断嵌套重入导致死锁    |+-----------------------------------+ +-----------------------------+

1. 与 ProcessScheduler (进程调度器) 的协同

        在快速路径中,SLUB 使用 c->tid(事务 ID)来检测抢占。tid 是一个单调递增的数值。如果一个进程在执行 this_cpu_cmpxchg 前被调度器抢占,并迁移到了另一个 CPU,当它迁回并恢复执行时,this_cpu_cmpxchg 会因为 tid 不匹配而失败,从而安全地退入 ___slab_alloc。 在 ___slab_alloc 内部,一旦需要调用 new_slab 向伙伴系统申请新页,由于该操作可能会因为内存回收而发生阻塞(Sleep),代码会显式调用 slub_put_cpu_ptr 释放当前 CPU 引用,允许调度器自由调度该线程。

2. 与 Interrupts (中断子系统) 的协同

        内核申请内存可能发生在硬中断或软中断上下文中(例如网卡驱动收到数据包时调用 dev_alloc_skb)。 为了防止中断处理程序与被中断的进程并发修改同一个 CPU 的 c->slab 导致数据损坏,___slab_alloc 在修改 c->freelist 和 c->slab 时,必须调用 local_lock_irqsave。这在非 RT 内核下会直接翻译为关中断(Disable IRQ),确保了临界区绝对的排他性。

3. 与 NUMA 架构的协同

        在多路服务器上,跨 NUMA 节点访问内存会带来巨大的延迟惩罚(QPI/UPI 总线瓶颈)。___slab_alloc 具有极强的 NUMA 亲和性感知:

  1. 优先校验当前 CPU 绑定的 slab 是否属于当前运行的 NUMA 节点(node_match)。

  2. 如果不匹配,宁可放弃当前高效的本地 Slab(将其 deactivate),也要跨越到目标节点的 Partial 链表中寻找物理距离更近的内存。


5. 💡 10年老兵避坑指南/实战案例

        在超大规模高并发的生产环境中,SLUB 分配器的慢速路径往往是性能瓶颈和系统不稳定的高发区。以下整理了两个经典的实战案例及排查调优方案。

案例一:高并发下 Node Partial 锁严重自旋,导致 CPU 暴涨

1. 现象描述

        在一台 128 核的双路 AMD EPYC 服务器上,运行高并发的 Nginx 或 Redis 业务时,系统响应时间(Latency)偶尔出现剧烈抖动。通过 perf top 观察,发现一个热点函数高居榜首:

92.5%  [kernel]  [k] _raw_spin_lock   |   +-- _raw_spin_lock   |       +-- get_partial        +-- ___slab_alloc             +-- kmem_cache_alloc

同时,/proc/slabinfo 显示某些小对象的 shared 数量极高。

2. 原因剖析

        当本地 CPU 的 freelist 和 cpu_partial 链表全部枯竭时,多个 CPU 会同时涌入 ___slab_alloc 的 new_objects 阶段,尝试从 Node 级别的 Partial 链表中获取 Slab。 此时,它们必须争抢 Node 级别的全局自旋锁 n->list_lock。在 128 核的超高并发下,这个全局锁成为了严重的物理瓶颈,导致大量 CPU 核心在 _raw_spin_lock 上处于忙等自旋状态,造成 CPU 使用率虚高,系统吞吐量骤降。

3. 调优与排查方案

  • 排查工具:使用 ftrace 追踪 get_partial 的耗时。

    echo function_graph > /sys/kernel/debug/tracing/current_tracerecho get_partial > /sys/kernel/debug/tracing/set_ftrace_filtercat /sys/kernel/debug/tracing/trace | head -n 20
  • 调优手段 1:增大 CPU Partial 缓存容量 通过增大每个 CPU 缓存的未满 Slab 数量,减少其向 Node 级别全局锁索取 Slab 的频率。

    # 针对 kmalloc-512 提高其 cpu_partial 阈值(默认可能为 30)echo 80 > /sys/kernel/slab/kmalloc-512/cpu_partial
  • 调优手段 2:开启 NUMA 自动平衡或绑定 CPU 使用 numactl --localalloc 或将高并发线程绑定在单路 NUMA 节点内,避免跨节点的内存分配触发 ALLOC_NODE_MISMATCH。


案例二:频繁触发 "Deactivate Bypass" 导致内存碎片化与 OOM

1. 现象描述

        某分布式存储系统在长时间运行后,系统物理内存逐渐耗尽,最终触发 OOM Killer。然而,通过 free -m 观察,伙伴系统的 Free 内存确实很少,但通过 slabtop 观察,Slab 占用的总内存也并不算极大,存在大量“幽灵碎片”。

2. 原因剖析

        在 ___slab_alloc 中,有这样一段逻辑:

    freelist = get_freelist(s, slab);    if (!freelist) {        c->slab = NULL;        ...        stat(s, DEACTIVATE_BYPASS);        goto new_slab;    }

        如果一个 Slab 中的对象被频繁地“交替申请与释放”(例如:A 申请,B 释放,跨核操作),会导致该 Slab 频繁在 Frozen 和 Unfrozen 状态间切换。 当触发 DEACTIVATE_BYPASS 时,SLUB 会直接绕过当前 CPU 缓存,将该 Slab 钝化并放回 Node Partial。如果此时伴随着不合理的 gfpflags(如频繁使用 GFP_ATOMIC 且限制了内存回收),SLUB 就会不断向伙伴系统申请新页(new_slab),而老页中由于残留了极少数未释放的长生存周期对象,导致整页无法被伙伴系统回收,造成严重的内存碎片化。

3. 诊断与解决

  • 诊断工具:监控 /proc/slabinfo 中的 active_objs 与 num_objs 的比例。如果比例低于 30%,说明碎片化极其严重。

  • 使用 slabinfo 工具分析: 内核源码自带了 tools/mm/slabinfo 工具,编译后运行:

    ./slabinfo -v -D kmalloc-256

    重点关注 Deactivate Bypass 和 Slabs per Node 的比例。

  • 解决方案:

    1. 合并小对象:优化业务代码,避免频繁申请极小生命周期的内存对象,改用对象池(Object Pool)。

    2. 调整 slub_max_order:在内核启动参数中加入 slub_max_order=0 或 1,限制单个 Slab 的物理页阶数,防止大块连续物理内存被碎片化蚕食。

    3. 主动收缩 Slab 缓存:在系统空闲时,可以通过写入 sysfs 强制触发 SLUB 的收缩与合并:

      echo 1 > /sys/kernel/slab/kmalloc-256/shrink

6. 总结

  ___slab_alloc 是 Linux 内核内存管理中极具工业美感的设计。它通过无锁快速路径与多级锁退化慢速路径的完美结合,既保证了单核情况下的极致分配性能,又兼顾了多核、多路 NUMA 架构下的并发安全与拓扑亲和性。理解其内部的“冻结”、“钝化”以及“多级回溯”机制,是进行内核级性能调优与高并发系统架构设计的必修课。

相关学习资料