Linux Slab 与 Slub 分配器机制:kmem_cache 源码走读与内存对象复用

发布时间:2026/9/20 23:20:42
Linux Slab 与 Slub 分配器机制:kmem_cache 源码走读与内存对象复用 Linux Slab 与 Slub 分配器机制kmem_cache 源码走读与内存对象复用在 Linux 操作系统中伙伴系统Buddy System负责管理整个物理内存的分配但其最小管理粒度是整整一个物理页通常为 4KB。然而在内核日常运行中充斥着海量极小数据结构的频繁创建与销毁一个struct inode约几百字节一个网络收发包的struct sk_buff约 200 字节一个进程的struct task_struct约几 KB。如果每一次分配小对象都向伙伴系统申请一个 4KB 页面不仅会造成高达 90% 以上的内部碎片Internal Fragmentation更会导致频繁的 TLB 刷新与内存初始化开销。为了解决小内存对象的高性能复用与碎片抑制问题Linux 内核引入了基于对象池机制的Slab / Slub 分配器。一、从 Slab 到 Slub内核设计哲学的演进最初由 Jeff Bonwick 提出的经典 Slab 分配器在多核高并发场景下逐渐暴露了瓶颈每个 Cache 需要维护庞大的描述符队列Full、Partial、Empty 三链表元数据内存开销巨大且伴随着复杂的自旋锁竞争。现代 Linux 内核默认采用Slub 分配器对架构进行了极致的精简与无锁化优化【struct kmem_cache 核心拓扑】 │ ├──► [Per-CPU 局部缓存: struct kmem_cache_cpu] (快速无锁路径) │ ├── freelist 指针 ──► [Object 1] ──► [Object 2] ──► NULL (空闲对象链) │ └── page 指针 ──► 当前正在分配的物理页 (struct page) │ └──► [Per-Node 节点缓存: struct kmem_cache_node] (慢速路径兜底) ├── partial 链表 ──► [Page A (部分使用)] ──► [Page B] └── list_lock 自旋锁Slub 分配器的两大核心创新零外部元数据开销Zero Metadata OverheadSlub 彻底移除了独立的 slab 描述符直接将空闲链表指针freelist保存在空闲对象本身的内存首地址上。当对象被分配出去时这块内存被业务数据覆盖当对象释放回池中时重新写回链表指针。Per-CPU 极致无锁快速路径每个 CPU 核心独占一个kmem_cache_cpu结构只要当前 CPU 的freelist中有可用对象单次分配仅需几条汇编指令全程无锁Lockless。二、kmem_cache核心数据结构源码走读在内核源码include/linux/slub_def.h与mm/slub.c中核心定义如下/* 每 CPU 局部缓存结构体 */ struct kmem_cache_cpu { void **freelist; /* 指向当前 CPU 本地下一个可用空闲对象的指针 */ unsigned long tid; /* 事务 ID用于实现无锁并发检测 (cmpxchg_double) */ struct page *page; /* 当前正在提供分配的 Slub 页面 */ }; /* 内核小对象高速缓存池描述符 */ struct kmem_cache { struct kmem_cache_cpu __percpu *cpu_slab; /* Per-CPU 局部缓存指针 */ /* 核心属性 */ slab_flags_t flags; unsigned long min_partial; unsigned int size; /* 对象实际占用大小 (包含对齐与填充) */ unsigned int object_size; /* 对象纯业务大小 */ struct kmem_cache_order_objects oo; /* 每次向伙伴系统申请页面时的 order 阶数与对象数 */ /* 构造函数初始化新对象时调用 */ void (*ctor)(void *); /* NUMA 节点缓存 */ struct kmem_cache_node *node[MAX_NUMNODES]; };三、kmem_cache_alloc源码执行路径剖析当内核调用kmem_cache_alloc(cachep, flags)时执行流程被严格区分为快速路径Fastpath与慢速路径Slowpathkmem_cache_alloc() │ ▼ [进入 slab_alloc_node 快速路径] │ ├── 1. 读取当前 CPU 的 c this_cpu_ptr(s-cpu_slab) ├── 2. 获取 object c-freelist │ ├────► [object ! NULL?] │ │ │ ├── 是 ──► [Fast Path 命中] │ │ - 将 c-freelist 更新为 object 的下一个节点: *(void **)object │ │ - 直接返回 object (耗时仅 ~10ns, 零锁竞争!) │ │ │ └── 否 ──► [Fast Path 缺失] │ │ ▼ ▼ [跌入 __slab_alloc 慢速路径] │ ├── 3. 当前 CPU 页面用尽从 node-partial 链表中尝试获取一个包含空闲对象的页面 ├── 4. 若 partial 链表亦为空调用伙伴系统 alloc_pages() 申请全新的物理页 ├── 5. 按照 s-size 将新页面切分成等长 Object串成 freelist └── 6. 挂载到当前 CPU 的 cpu_slab 并返回对象四、生产环境 Slab 观测与泄漏排查实战在高负载 Linux 服务器或端侧网关上Slab 缓存占用过多会导致可用内存枯竭。4.1 观测内核 Slab 占用# 1. 实时查看内核 Slab 对象占用排行榜 (按内存消耗排序) $ slabtop -s c # 2. 输出示例 Active / Total Objects (% used) : 1420500 / 1450000 (97.9%) Active / Total Size (% used) : 350.2M / 360.5M (97.1%) OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME 480000 478000 99% 0.58K 30000 16 281.2M radix_tree_node 250000 248000 99% 0.20K 12500 20 50.0M dentry 120000 119000 99% 0.10K 3000 40 12.0M buffer_head 45000 42000 93% 0.62K 3750 12 28.1M inode_cache4.2 典型生产问题治理Dentry / Inode Cache 膨胀当服务器频繁遍历或创建大量临时文件时目录项缓存dentry与索引节点缓存inode_cache会迅速吃满数十 GB 内存。虽然内核在内存紧缺时会自动回收Reclaim但在需要紧急释放缓存时可通过向/proc/sys/vm/drop_caches写入控制指令强制收回 Slab 缓存# 强制内核收回未使用的 Slab 缓存 (dentry, inode) sync echo 2 /proc/sys/vm/drop_caches # 调整 Slab 回收意愿积极程度 (默认 100增大至 200 会加速内核回收 VFS 缓存) echo 200 /proc/sys/vm/vfs_cache_pressure深入理解 Slub 分配器的 Per-CPU 局部缓存与无锁对象池模型不仅能帮助系统工程师精准分析内核内存的真实分布更能将这种极致的“对象复用与零拷贝”设计思想反哺到用户态高并发服务的数据结构设计中。