引言:为什么内存管理是内核性能的核心
内核内存分配器是 Linux 性能栈中最容易被忽视却又最关键的子系统之一。用户态的 malloc() 如果慢了,程序顶多用时长一点;但内核态的 kmalloc() 如果慢了,影响的是文件系统缓存、网络包处理、中断上下文——几乎涵盖内核所有热路径。
从早期的 Buddy System,到 SLAB 再到今天的 SLUB(Unqueued Slab Allocator),Linux 内核走过了一条持续优化的道路。本文将从源代码和机制层面,深入剖析 SLUB 分配器的设计哲学、Per-CPU 页框缓存(PCP)的加速原理,以及在真实场景中如何调优内核内存子系统。
一、从 Buddy System 到 SLUB:分配器演进史
1.1 Buddy System:页级分配的基础
Buddy System 是 Linux 内核物理页分配的基础框架。它维护 11 个空闲链表(order 0~10),分别管理 2^n 个连续页框。分配时向上取阶,释放时与"伙伴"(地址相邻的相同大小的页块)合并。
Buddy System 的局限也非常明显:
- 内部碎片:申请 64 字节,实际分配一整页(4KB),浪费 98%
- 无对象缓存:反复分配释放相同结构体(如
task_struct、inode),每次都要经过完整初始化开销 - 无 CPU 亲和:多核竞争同一个全局链表时锁争用严重
1.2 SLAB:对象缓存的开创
1994 年,Jeff Bonwick 为 SunOS 发明了 SLAB 分配器,Linux 2.1.23 引入。核心思想是:在内核页上预分配并初始化同类型对象,分配时直接返回空闲对象,释放时不归还页面,仅标记清空。
SLAB 的核心数据结构:
struct kmem_cache {
struct list_head slabs_full; // 已满 slab 链表
struct list_head slabs_partial; // 部分空闲 slab 链表
struct list_head slabs_free; // 完全空闲 slab 链表
unsigned int objsize; // 单个对象大小
unsigned int size; // 含元数据的实际大小
gfp_t gfpflags; // 分配页框时使用的 GFP 标志
// ... 构造函数、析构函数、着色偏移等
};
每个 slab(物理页)内部分为三种区域:对象存储区、着色区(color offset,减少 cache line 冲突)、管理信息区。
1.3 SLQB 与 SLUB:解耦与简化
SLAB 在多核扩展时遇到瓶颈:全局锁保护三个链表,高并发场景下 cache 行弹跳严重。Linux 2.6.23(2008年)引入 SLUB 作为默认分配器,其设计理念是:
- 每个 CPU 维护独立的 slab 列表和 partial slab 列表
- 节点(NUMA node)级联的 partial 链表
- 无队列(Unqueued):kmem_cache 结构本身不持有 slab 链表
- 完全无锁 fast-path:Per-CPU 缓存命中时零锁分配
二、SLUB 核心数据结构深度解析
2.1 全局视角:kmem_cache 与 kmem_cache_node
struct kmem_cache {
struct kmem_cache_cpu *cpu_slab; // Per-CPU 缓存指针(每CPU一个)
struct kmem_cache_node *node[MAX_NUMNODES]; // NUMA node 级 partial 链表
unsigned long min_partial; // node 预链条表最小保留数
unsigned int offset; // 空闲对象链表指针偏移
unsigned int size; // 对象实际大小
unsigned int object_size; // 对象原始大小
struct kmem_cache_order_objects oo; // 最佳 slab 阶数
struct kmem_cache_order_objects max;
const char *name;
// ...
};
struct kmem_cache_cpu {
void **freelist; // 空闲对象链表头指针
unsigned long tid; // 全局唯一事务ID,用于无锁同步
struct page *page; // 当前正在使用的 slab 页
struct page *partial;// CPU local partial slab 链表头
#ifdef CONFIG_SLUB_CPU_PARTIAL
unsigned stat[NR_SLUB_STAT_ITEMS]; // 统计计数
#endif
};
struct kmem_cache_node {
spinlock_t list_lock; // 节点级互斥锁
unsigned long nr_partial; // partial slab 数量
struct list_head partial; // partial slab 链表
// NR slab 仅在 NUMA CONFIG 启用时存在
} ____cacheline_internodealigned_in_sminode;
2.2 对象生命周期:分配与释放路径
Fast-path 分配(零锁):
void *slab_alloc(struct kmem_cache *s, gfp_t gfpflags) {
// 1. 直接操作 CPU 本地 freelist
void *object = s->cpu_slab->freelist;
void *next_object = get_freepointer_safe(s, object);
if (unlikely(!object))
return __slab_alloc(s, gfpflags); // slow-path
s->cpu_slab->freelist = next_object;
return object;
}
当 cpu_slab->freelist 非空时,仅仅是一次原子指针交换,速度极快(约 5-10ns)。只有 fast-path 失败时才进入 slow-path,尝试从 CPU local partial、node partial,最后到 Buddy System 申请新页。
释放(deallocate)路径:
- 对象归还给当前 CPU 的 freelist(fast-path,仅当对象属于当前 CPU 的 slab)
- 对象归还给 CPU local partial(当对象属于此 CPU 的另一页 slab)
- 对象归还给 node partial(跨 CPU 释放或无 CPU local 时)
2.3 SLUB 着色机制(Slab Coloring)
不同 slab 的相同偏移对象映射到同一 cache line,会造成严重的 cache 伪共享。SLUB 通过在每个 slab 页起始位置添加动态偏移(color offset)来错开对象位置。
着色偏移的计算:
void *addr = page_address(page) +Colour_offset(s, cpu) * cache_line_size();
但注意:从 Linux 5.15 开始,随着硬件预取器的智能度和缓存容量的增长,color 机制在多数场景下的性价比下降,部分新内核版本中 color 逻辑已被简化。
三、Per-CPU 页框缓存(PCP):物理页的 CPU 亲和
3.1 PCP 的必要性
多核系统中,如果所有 CPU 都从 Buddy System 全局链表分配/释放页框,除了锁争用外,还会导致严重的 cache 失效——一个页框被 CPU A 分配使用、CPU B B 释放时,原本属于 CPU A 的 cache line 需要多次迁移。
PCP(Per-CPU Pagelist)的核心思想:让每个 CPU 维护一个本地页框缓存列表,分配时先从本地取,释放时先归还本地。
3.2 PCP 的实现内核数据结构
struct zone {
struct per_cpu_pageset __percpu *pageset; // 每 CPU 的 PCP 热缓存
struct per_cpu_pageset *pageset_high;
// ...
};
struct per_cpu_pageset {
struct per_cpu_pages pcp; // 热页缓存
s8 stat_threshold; // 压力阈值调整
s8 vm_stat_threshold;
// ...
};
struct per_cpu_pages {
int count; // 当前缓存页框数
int high; // 高水位,超过即归还给 Buddy System
int batch; // 批量迁移的页框数
struct list_head lists[NR_PCP_LISTS][MIGRATE_PCPTYPES]; // LRU 页框列表
} ____cacheline_aligned_in_sminode;
3.3 PCP 的工作流程
分配路径(rmqueue_bulk/rmqueue_pcplist):
- 检查当前 CPU 的 PCP 列表是否非空
- 非空:从本地链表移除一个页框,返回给调用者(无锁,单指令操作)
- 空:调用
rmqueue_bulk()从 Buddy System 批量迁移batch个页框到 PCP
释放路径(free_unref_page_commit/free_pcppages_bulk):
- 检查 PCP 是否已满(count >= high)
- 满:批量将
batch个页框释放回 Buddy System - 空:归还到本地 PCP 链表
- 页框热度热判断:若该页最近被访问(
PageActive),放入热页链表头部
3.4 PCP 的水位控制
PCP 的关键参数 batch 和 high 决定了缓存效率与全局压力的平衡:
- batch:PCP 空时一次从 Buddy System 迁入的页框数,太大会消耗全局页框资源,太小则频繁触发全局分配。默认值
pcp->batch = 1(单页迁移)对应CONFIG_HIGHMEM=y时为 63 - high:PCP 高水位,超过该值释放回全局。过大会造成全局回收压力下本地缓存过多;过小则批量迁移频繁
这些值通过 zone_batchsize() 函数动态计算:
static unsigned long zone_batchsize(struct zone *zone) {
unsigned long batch = zone->managed_pages / 1024;
batch *= max(1UL, nr_pcp_batch_pages);
batch /= 1024;
if (!batch)
batch = max_val(1, zone->managed_pages / 128);
// ...
return batch;
}
四、SLUB 与 PCP 的协同:全链路页对象分配
当内核调用 kmem_cache_alloc()(如分配 task_struct)时,实际经过的完整流程是:
- SLUB fast-path:检查
cpu_slab->freelist,命中则直接返回(5-10ns) - SLUB slow-path:检查 CPU local partial → node partial
- 从 Buddy System 分配:
new_slab()调用alloc_slab_page() - PCP 路径:最终到达
rmqueue_pcplist(),先从本地 PCP 取页 - Buddy fallback:PCP 空,退回全局 Buddy System 分配
- SLUB 初始化:新页返回后,SLUB 建立对象空闲链表,分配给请求
在这个完整链路中,SLUB 负责对象级缓存,PCP 负责物理页级缓存,Buddy System 负责全局阶分配。三层各自工作、互不干扰,构成了 Linux 内核高效的页对象分配体系。
五、CPU 本地与 SLUB 的 NUMA 感知
5.1 SLUB 的 NUMA 分配策略
在 NUMA 系统上,SLUB 的 node partial 机制确保对象分配尽可能来自本地 node:
- 默认 node 分配协议:
kmem_cache_alloc()尝试从当前 CPU 所属的 node partial 链表分配 - fail-soft 策略:本地 node partial 为空时,回退到远程 node 分配(fault-around 机制)
- 从 Linux 4.2 开始引入
CONFIG_SLUB_MEMCG,支持 cgroup 内存限制下的 SLUB 统计和限制
5.2 跨 NUMA 节点的缓存失效代价
x86 平台上,本地 NUMA 节点内存访问延迟约为 70-90ns,跨节点访问则需要 130-180ns。SLUB 通过 node partial 链表维护 NUMA 亲和的对象池,使频繁类型分配(如 task_struct、dentry)保持节点本地,避免昂贵的跨节点访问。
六、SLUB 调试与故障排查
6.1 Red Zone 与 Poison
SLUB 在每个对象末尾保留一个 Red Zone 区域(哨兵字节),填充固定值(如 0xbb)。每次分配和释放时检查哨兵值,若被覆盖则触发内核 panic 或警告:
dmesg | grep -i "slub"
# 典型输出:
# =============================================================================
# BUG: KASAN: slab-out-of-bounds in function_name+0xOFF/0xLEN [module]
# Read of size N at addr ADDR by task COMM:PID
SLUB Debug 选项:
- CONFIG_SLUB_DEBUG:启用 Red Zone、Poison、Tracking
- CONFIG_SLUB_DEBUG_ON:启动时即激活 debug 特性
- slub_debug= 内核参数:控制哪些 cache 启用 debug(如
slub_debug=FZP)
6.2 /proc/slabinfo 深度解读
$ cat /proc/slabinfo | head -25
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <batchcount> <limit> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
kmalloc-512 88 88 512 32 4 : tunables 0 0 0 : slabdata 3 3 0
task_struct 91 92 5952 5 8 : tunables 0 0 0 : slabdata 19 19 0
dentry 2456 2456 192 21 2 : tunables 0 0 0 : slabdata 117 117 0
inode_cache 826 896 632 6 2 : tunables 0 0 0 : slabdata 149 150 0
signal_cache 13 13 1152 6 8 : tunables 0 0 0 : slabdata 2 2 0
各列含义:
- active_objs / num_objs:活跃对象数 / 总对象数
- objsize:含元数据的对象实际大小
- objperslab:每个 slab 的对象数
- tunables 中的 batchcount / limit:批量分配限制
- slabdata 中的 num_slabs:该 cache 的 slab 总数,持续升高可能泄露
6.3 slubtop 实时监控
BPF 工具可以实时监控各 SLUB cache 的分配/释放速率:
$ sudo slubtop -d 1 # 每秒刷新
slab cache OBJS ACTIVE TOTAL OBJSIZE OBJ/Slab % USE CACHE MAX
vm_area_struct 98267 98267 98267 208 19 10.9 4.0M -
kmalloc-64 956342 956342 956342 64 64 38.0 6.0M -
dentry 674227 674227 674227 192 21 32.3 13.4M -
filp 418018 418018 418018 256 52 13.6 11.6M -
kmalloc-256 417445 474450 474450 256 16 99.8 8.7M -
七、性能调优实战
7.1 SLUB 调优参数
/proc/sys/kernel/slab_max_order:SLUB 最大分配阶,限制单次分配的连续性页数。
slub_min_objects:每个 slab 最少对象数,影响内存利用率与 cache 效率的平衡。
/sys/kernel/slab/<cache_name>/tunables/:每 cache 可调参数:
order:slab 阶数min_partial:node 级最少保留 partial slab 数cpu_partial:CPU local partial 对象上限
7.2 PCP 调优
/proc/sys/vm/zone_reclaim_mode:控制 NUMA 本地回收与远程分配策略。
在内存压力大的系统中,可通过 sysctl vm.min_free_kbytes 间接影响 PCP high 水位。
7.3 内核启动参数调优
# 完全启用 SLUB debug(仅调试,严重性能影响)
slub_debug=FZP
# 对特定 cache 启用 debug
slub_debug=U,kmalloc-256
# 禁用 SLUB merge 统计片段合并(更精确的泄露检测)
slub_nomerge
# slab allocator 选择(启动时命令行)
slab_nomerge slub_nomerge
7.4 cgroup 与 SLUB:memcg 控制
Linux 4.2+ 支持 memcg 对 SLUB cache 的内存限制。当一个 cgroup 的内存用量接近限制时,SLUB 会开始回收该 cgroup 不再使用的 partial slab。
相关参数:
- memory.limit_in_bytes / memory.max:总内存限制
- memory.kmem.limit_in_bytes:内核内存子限制(v1)
- memory.slabinfo:查看该 cgroup 的 SLUB cache 用量
八、从 SLUB 看内核架构演进趋势
SLUB 和 PCP 的设计体现了 Linux 内核在"数据结构的 CPU 节点化"上的持续努力:
- Per-CPU 缓存 → PCP → CPU 本地 freelist,内核分配器的每一层都在减少全局状态访问
- NUMA 亲和 → NUMA balancing,从硬件拓扑感知到自动迁移,内存分配越来越智能
- eBPF 可观测:BPF 程序可以 attach 到 SLUB 的每次 alloc/free 事件,实现运行时性能监控
- Carbon-Aware Scheduling:未来 SLUB 可能会感知 CPU 功耗状态,将非紧急分配延迟到低功耗时段批量执行
理解 SLUB 和 PCP,不仅是掌握一个子系统,更是理解 Linux 内核适应现代硬件演进的思维方式——减少共享、增加本地、让数据结构跟随 CPU 拓扑演进。
总结
Linux 内核的 SLUB 分配器通过 Per-CPU 缓存、NUMA 感知、Red Zone 保护等机制,将对象分配从"全局竞争"变为"本地命中",实现了纳秒级 fast-path。PCP 则在物理页级别做了同样的本地缓存策略,两者协同构成了内核内存分配的高速通道。
掌握这些机制,不仅有助于排查内存泄漏、内核 slab 飙升等故障,更能在系统调优时有的放矢:合理使用 slub_debug 定位问题、通过 /proc/slabinfo 监控 cache 利用率、为 NUMA 应用设计合适的内存策略。
内核内存分配器的设计哲学——减少锁争用、增加 CPU 亲和、让热数据近在咫尺——也是所有高性能系统软件的通用法则。

发表评论 取消回复