
在 Linux 物理内存管理体系中伙伴系统Buddy System以页4KB 或其 2 的幂次方为最小粒度划分与回收物理内存。然而内核运行时的关键控制对象——如struct task_struct、struct mm_struct、struct sk_buff或struct inode——生命周期往往短至数微秒尺寸从数十字节到几千字节不等。如果每一次创建网络报文或发起系统调用都向伙伴系统申请整页物理内存内部碎片将迅速吞噬内存且伙伴系统的全局自旋锁zone-lock会立刻引发跨核心的总线颠簸与计算锁死。早期的 SLAB 分配器通过管理复杂的对象队列实现了小块内存缓存但其冗长的队列控制元数据与庞大的每 CPU 缓存开销在大规模多核服务器上显得极其臃肿。现代 Linux 内核已全面淘汰 SLOB 并以 SLUB 作为绝对默认的对象分配器。SLUB 摒弃了独立的对象控制块直接利用空闲对象自身的首部空间存储下一个空闲对象的指针freelist并将管理核心高度收敛为两大支柱属于每个 CPU 独立独占的struct kmem_cache_cpu以及面向 NUMA 节点的共享池struct kmem_cache_node。架构双核每 CPU 本地私有与 NUMA 共享池每个内核对象缓存池都由一个总控结构struct kmem_cache代表。在kmem_cache之下内存分配与回收的物理路径被清晰划分为两级存储体系本地高速私有池struct kmem_cache_cpu该结构通过内核的__percpu机制为每个逻辑 CPU 分配一份独立实例。其核心字段包括freelist指向当前正在分配的 slab 页面内第一个空闲对象的虚拟地址。page新内核演进为struct slab *slab指向当前绑定的物理 slab。tidTransaction ID递增的事务标识符用于结合cmpxchg_double实现无锁并发校验确保分配操作在抢占或中断发生时具备确定性的原子回退保护。NUMA 节点共享池struct kmem_cache_node在 NUMA 体系下系统为每一个物理内存节点分配一个kmem_cache_node结构负责兜底与跨核心调度list_lock自旋锁用于保护当前节点下的共享链表。partial双向链表串联起当前 NUMA 节点内所有“部分空闲既有已被使用对象也有未被使用对象”的 slab 页面。nr_partial当前 partial 链表中的 slab 总数。min_partial阈值保护线。当 partial 链表中的 slab 数量低于该值时即便某个 slab 的所有对象都被释放内核也不会将其归还给伙伴系统而是保留在池中以防后续分配抖动。协同机制对象获取与释放的状态机流转kmem_cache_cpu与kmem_cache_node之间的状态机流转决定了 Linux 内核对象分配的极限吞吐[CPU 分配请求] │ ▼ [kmem_cache_cpu] ──(freelist 有空闲?)──► [Yes] ──► cmpxchg_double 快速获取 (无锁) │ No ▼ [慢速路径 __slab_alloc] │ ├─► 检查本地 active slab 是否有残余空闲对象 │ ├─► 本地 slab 耗尽解除冻结 (Unfreeze) │ ├─► 加锁访问 [kmem_cache_node-partial] 链表 │ │ │ ├─► 获取 Partial Slab ──► 设为 Frozen ──► 挂载到 kmem_cache_cpu │ └─► Partial 链表为空 ──► 向伙伴系统申请物理页 (alloc_pages_node) ──► 格式化新 Slab快速路径Fastpath纯本地无锁弹栈当业务进程在某个 CPU 上触发内存申请如kmem_cache_alloc时分配器首先直接读取当前核心的kmem_cache_cpu。若freelist非空且当前核心未发生抢占导致tid变化分配器直接通过this_cpu_cmpxchg_double将freelist更新为下一个空闲对象并将当前对象返回。此过程无需加自旋锁没有跨核心的内存总线同步耗时仅需 5 到 10 个 CPU 周期。慢速路径SlowpathNUMA 提货与 Slab 冻结若本地freelist为空说明当前 active slab 已经用尽或尚未加载分配逻辑立刻坠入慢速路径__slab_alloc()检查当前绑定的 slab 中是否还有其他未激活的对象例如并发归还到当前 slab 的对象。若有重新装填freelist。若当前 slab 已经彻底占满内核将其从kmem_cache_cpu中解绑并清除其frozen标志使之变为一个完全在用的独立 slab。获取当前 CPU 所处 NUMA 节点的kmem_cache_node-list_lock从partial链表头部摘取一个可用 slab。将该 slab 标记为frozen冻结状态装入当前 CPU 的kmem_cache_cpu。所谓“冻结”意味着该 slab 已经专属于该 CPU其他核心如果要向该 slab 归还对象不能直接将其重新放回partial链表避免多核并发扰乱链表。若当前 NUMA 节点的partial链表完全枯竭SLUB 退出 SLUB 管理层直接调用伙伴系统接口alloc_pages_node()申请一组连续物理页将其切分为多个目标对象尺寸的槽位形成一个全新的 slab。C23 抽象模拟SLUB 分配与节点协同模型以下采用 C23 标准构建简化的 SLUB 状态机核心模型展示kmem_cache_cpu与kmem_cache_node在多核并发下的协同逻辑#define _GNU_SOURCE #include stdio.h #include stdlib.h #include stdint.h #include stdbool.h #include stdatomic.h #include pthread.h #include unistd.h constexpr size_t OBJECT_SIZE 64; constexpr size_t SLAB_OBJ_COUNT 8; constexpr size_t MAX_NUMA_NODES 2; typedef struct FreeObject { struct FreeObject *next; } FreeObject; typedef struct SlabPage { FreeObject *freelist; atomic_int inuse; atomic_bool frozen; struct SlabPage *next; } SlabPage; typedef struct { FreeObject *freelist; SlabPage *slab; uint64_t tid; } kmem_cache_cpu; typedef struct { pthread_mutex_t list_lock; SlabPage *partial; size_t nr_partial; size_t min_partial; } kmem_cache_node; typedef struct { kmem_cache_cpu cpu_slabs[4]; // 模拟 4 个 CPU 核心 kmem_cache_node nodes[MAX_NUMA_NODES]; size_t obj_size; } kmem_cache; static SlabPage *allocate_new_slab(size_t obj_size) { SlabPage *slab (SlabPage *)malloc(sizeof(SlabPage)); if (!slab) return nullptr; char *memory_block (char *)aligned_alloc(64, obj_size * SLAB_OBJ_COUNT); slab-freelist nullptr; slab-inuse 0; slab-frozen false; slab-next nullptr; // 格式化 slab 对象并构筑空闲单向链表 for (size_t i 0; i SLAB_OBJ_COUNT; i) { FreeObject *obj (FreeObject *)(memory_block i * obj_size); obj-next slab-freelist; slab-freelist obj; } return slab; } void *kmem_cache_alloc_mock(kmem_cache *cache, int cpu_id, int node_id) { kmem_cache_cpu *c cache-cpu_slabs[cpu_id]; // 1. 快速路径本地 CPU freelist 弹栈 if (c-freelist ! nullptr) { FreeObject *obj c-freelist; c-freelist obj-next; c-slab-inuse; c-tid; return (void *)obj; } // 2. 慢速路径本地 freelist 枯竭向 NUMA Node 获取 partial slab kmem_cache_node *n cache-nodes[node_id]; pthread_mutex_lock(n-list_lock); if (c-slab) { // 解冻当前旧 slab c-slab-frozen false; } if (n-partial ! nullptr) { // 从 partial 链表摘除一个 slab SlabPage *page n-partial; n-partial page-next; n-nr_partial--; page-frozen true; c-slab page; c-freelist page-freelist; page-freelist nullptr; } else { // 慢速路径底线从系统分配全新 slab SlabPage *new_page allocate_new_slab(cache-obj_size); new_page-frozen true; c-slab new_page; c-freelist new_page-freelist; new_page-freelist nullptr; } pthread_mutex_unlock(n-list_lock); if (c-freelist ! nullptr) { FreeObject *obj c-freelist; c-freelist obj-next; c-slab-inuse; c-tid; return (void *)obj; } return nullptr; }生产环境跨 NUMA 释放陷阱与调优指标SLUB 的精巧设计使本地单核分配性能达到极致但在分布式高并发后端架构中这种分层设计会放大一种典型的工程病症“远程释放”Remote Free。当线程 A 在 Node 0 上的 CPU 0 分配了海量网卡报文缓存sk_buff随后通过消息队列传递给在 Node 1 上的 CPU 4 工作线程处理。工作线程消费完毕后执行kfree释放行为发生在 Node 1。此时SLUB 分配器必须判定对象所属的物理 slab 究竟挂载在哪个节点的管理结构下。因为无法将该对象直接注入 CPU 4 的本地freelist释放流程被迫强行穿透至 Node 0 的kmem_cache_node并争抢其全局list_lock。当吞吐量达到百万 QPS 级别时Node 级别的自旋锁争抢会导致 CPU 产生惊人的spin_lock消耗在perf top中清晰展现为_raw_spin_lock占用突破 25%。工程应对手段包括网络中断与工作线程亲和性强绑定通过smp_affinity将网卡队列中断与处理该队列业务的用户态线程绑定到同一个 NUMA 节点使对象生命周期的申请与释放闭环在同一个物理节点内部。观察内核 slabinfo 统计持续监控/proc/slabinfo中的active_objs与num_objs比率当发现特定缓存池的nr_partial极其庞大且存在大量跨核心释放时应通过调整内核参数或应用层对象重用池避免让内核底层分配器承受高频跨节点洗牌。