Linux内核SLAB/SLUB分配器深度解析:从数据结构到性能调优
引言
Linux内核内存管理是一个庞大而精密的子系统,在上层我们有页分配器(Buddy System)管理物理页面,而在页之下,内核需要频繁地分配和释放各种大小不一的结构体——文件对象、进程描述符、网络套接字缓冲区、inode缓存等。这些内核对象的分配粒度远小于一页(通常几十到几百字节),且生命周期短、分配频率极高。如果直接使用页分配器,不仅会产生严重的内部碎片,还会因频繁的分配/释放带来巨大的性能开销。
SLAB分配器应运而生。它源自Jeff Bonwick为Solaris设计的SLAB算法(1994年),后被引入Linux 2.1.23内核,逐渐成为内核小对象分配的标准机制。随着系统规模和硬件架构的演进,SLAB自身也经历了从SLAB到SLUB的演变——SLUB(Unqueued Slab Allocator)由Christoph Lameter在Linux 2.6.23中引入,如今已成为大多数发行版的默认分配器。此外,还存在一个极简的SLOB分配器,专为嵌入式等内存受限环境设计。
本文将深入剖析SLAB/SLUB分配器的核心数据结构、分配与释放流程、CPU缓存与NUMA优化机制、调试与监控手段,并结合实战案例展示如何针对具体工作负载进行性能调优。
一、核心概念与术语体系
1.1 对象(Object)、Slab与Cache
SLUB分配器建立在三个核心概念之上:
对象(Object):分配器管理的最小单元,即单个内核结构体实例。每个对象有固定的大小(size),在创建Cache时确定。对象可以处于三种状态——空闲(free)、已分配(allocated)或部分空闲(通过链表引用)。
Slab:一个或多个连续物理页的集合,被划分为多个等大的对象。一个Slab是对象的容器。在SLUB中,一个Slab通常占用一页(4KB),但可以配置为多页。Slab是分配和释放的基本单位,它总是属于某个特定的Cache。
Cache:针对特定大小/类型的对象池。每个Cache有一个名字(如"kmalloc-64"、"dentry_cache"),管理一组Slab。所有相同类型和大小的对象都从同一个Cache中分配。Cache是slab分配器顶层的组织结构。
三者之间的关系可以概括为:Cache → 多个Slab → 每个Slab包含多个Object。分配对象时,从Cache的某个有空闲对象的Slab中取出;释放对象时,归还到其所属的Slab中。
1.2 SLAB vs SLUB vs SLOB
Linux提供了三种Slab分配器实现,可以通过内核配置选择:
SLAB:原始实现,使用复杂的每CPU缓存阵列和着色机制。支持完整的NUMA优化,但代码复杂度高,锁竞争在高端系统上成为瓶颈。
SLUB:现代默认分配器。简化了队列管理(unqueue设计理念),每个NUMA节点维护一个partial slab链表,减少了全局锁竞争。在可扩展性和调试能力之间取得了很好的平衡。自Ubuntu 12.04和RHEL 7起成为默认。
SLOB:极简分配器,使用类似首次适应(first-fit)的算法。代码量极小(不到3000行),适合嵌入式系统,但碎片化严重,不适合长时间运行的服务。
本文以SLUB为主进行深入讲解,会穿插对比SLAB的设计差异。
二、SLUB核心数据结构
2.1 kmem_cache结构体
kmem_cache是SLUB分配器的核心控制结构(定义在
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; // 每CPU快速分配缓存
/* 所有 slab 的列表 */
unsigned long flags;
unsigned long min_partial; // node中保留的最少partial slab数
int size; // 用户请求的大小
int object_size; // 实际对象大小
struct reciprocal_value reciprocal_buffer_size;
unsigned int offset; // 空闲对象链表指针的偏移
unsigned int freelist_size; // 空闲列表大小
unsigned int inuse; // 每个对象占用空间(含元数据)
unsigned align; // 对齐要求
struct kmem_cache_node *node[MAX_NUMNODES]; // 每NUMA节点数据
struct kmem_cache_order_objects oo; // min/low/high阶数
// ... 统计字段、调试字段等
};
关键字段解读:
- cpu_slab:每CPU变量,指向该CPU的kmem_cache_cpu结构。这是快速分配路径的核心,无需加锁即可分配。
- node[]:每个NUMA节点对应一个kmem_cache_node,管理该节点上此Cache的所有partial slab。
- offset:空闲链表指针在对象中的偏移量,这是SLUB的一个巧妙设计——空闲对象的首8字节被用来存储下一个空闲对象的指针。
- oo:包含min、low、high三个阶数值,分别代表最低限制、partial slab阈值、最大限制。SLUB会根据当前内存压力动态调整。
2.2 kmem_cache_cpu:每CPU快速路径
struct kmem_cache_cpu {
void **freelist; // 指向下一个空闲对象的指针
unsigned long tid; // 全局唯一事务ID,用于无锁同步
struct page *page; // 当前正在使用的slab页
struct page *partial; // 此CPU的partial slab链表(SLUB独有)
};
这是SLUB高性能的关键所在。每个CPU维护自己的freelist和page,多数情况下分配操作只需:
- 检查percpu->freelist是否为空
- 若不为空,直接取出freelist指向的对象,更新freelist
- 整个操作在关抢占(preemption disable)下完成,无需任何锁
这种设计在SMP系统上几乎零竞争,是SLUB在现代多核处理器上性能优异的主要原因。
2.3 kmem_cache_node:NUMA节点管理
struct kmem_cache_node {
spinlock_t list_lock; // 保护partial链表的锁
unsigned long nr_partial; // partial slab数量
struct list_head partial; // partial slab链表
// SLAB时代还有full和free链表,SLUB中简化了
};
每个NUMA节点有一个kmem_cache_node,管理该节点上所有partial slab(既有空闲对象又有已分配对象的slab)。当percpu的freelist耗尽时,分配器会从node的partial链表中获取一个slab;当对象被释放且所在slab由全满变为partial时,会挂到node链表上。
三、分配流程深度剖析
3.1 快速路径(Fast Path)
SLUB的分配入口是__slab_alloc()(封装在___slab_alloc()中),快速路径的执行流程如下:
// 伪代码:SLUB快速分配路径
void *alloc(kmem_cache *cachep) {
// 1. 禁用抢占,确保在当前CPU上执行
preempt_disable();
// 2. 获取percpu数据
cpu = raw_smp_processor_id();
cpu_slab = cachep->cpu_slab[cpu];
// 3. 检查freelist是否有空闲对象
if (unlikely(cpu_slab->freelist == NULL)) {
// 无空闲,跳转到慢速路径
goto slow_path;
}
// 4. 快速分配:取出freelist指向的对象
object = cpu_slab->freelist;
cpu_slab->freelist = get_next_ptr(object); // object首地址存next指针
cpu_slab->tid = next_tid(cpu_slab->tid);
// 5. 重新启用抢占
preempt_enable();
// 6. 返回对象
return object;
slow_path:
// 跳转到慢速分配路径(见下文)
return ___slab_alloc(cachep, flags, addr);
}
快速路径的核心操作只有几个:检查指针、修改指针、递增计数器。这些操作在现代CPU上通常只需10-20个时钟周期(约5-10纳秒),比系统调用还要快得多。
3.2 慢速路径(Slow Path)
当percpu的freelist为空时,分配器进入慢速路径,需要从系统获取新的空闲对象。慢速路径的执行流程更为复杂:
// 伪代码:SLUB慢速分配路径
void *___slab_alloc(kmem_cache *cachep, gfp_t gfp, unsigned long addr) {
slow_path:
// 1. 检查percpu的partial链表
if (cpu_slab->partial) {
// 有partial slab,直接用它
page = cpu_slab->partial;
if (page->freelist) {
// 此partial slab还有空闲对象
object = page->freelist;
page->freelist = get_next_ptr(object);
if (all_objects_in_use(page)) {
// 全满了,从partial移除
remove_from_partial(cpu_slab, page);
}
cpu_slab->page = page;
cpu_slab->freelist = page->freelist;
goto return_object;
}
}
// 2. percpu partial也为空,去node的partial链表获取
node = get_node(cachep, nid);
spin_lock(&node->list_lock);
if (node->nr_partial) {
// 从node partial链表中取出一个slab
page = list_first_entry(&node->partial, struct page, lru);
list_del(&page->lru);
node->nr_partial--;
spin_unlock(&node->list_lock);
cpu_slab->page = page;
cpu_slab->freelist = page->freelist;
goto return_object;
}
spin_unlock(&node->list_lock);
// 3. node也没有partial slab,必须从伙伴系统分配新页
page = new_slab(cachep, gfp);
if (!page)
return NULL; // 分配失败
// 4. 初始化新slab
cpu_slab->page = page;
cpu_slab->freelist = page->freelist;
return_object:
// 取出对象并返回
object = cpu_slab->freelist;
cpu_slab->freelist = get_next_ptr(object);
return object;
}
慢速路径涉及:
- percpu partial链表检查(无锁)
- node partial链表操作(需获取spinlock)
- 伙伴系统分配新页(可能触发直接回收或直接规整)
- 新slab的初始化(建立空闲链表)
整个过程通常需要微秒级时间,比快速路径慢100-1000倍。因此,理解并优化快速路径命中率是性能调优的核心目标。
四、释放流程与对象归还
SLUB的释放操作(__slab_free())相对简单,但同样精心设计以避免竞争:
// 伪代码:SLUB释放路径
void free(kmem_cache *cachep, void *object) {
page = virt_to_page(object); // 对象所在页
preempt_disable();
cpu_slab = cachep->cpu_slab[smp_processor_id()];
if (page == cpu_slab->page) {
// 快速路径:对象当前在percpu的活跃页上
set_next_ptr(object, cpu_slab->freelist);
cpu_slab->freelist = object;
cpu_slab->tid = next_tid(cpu_slab->tid);
preempt_enable();
return;
}
preempt_enable();
// 慢速路径:对象不在percpu页上
___slab_free(cachep, page, object);
}
void ___slab_free(kmem_cache *cachep, struct page *page, void *object) {
// 将对象插入页的空闲链表
set_next_ptr(object, page->freelist);
page->freelist = object;
page->inuse--;
if (page->inuse == 0) {
// slab完全空闲
if (nr_partial >= cachep->min_partial) {
// 超过保留阈值,归还给伙伴系统
free_slab(page);
} else {
// 加入node partial链表
spin_lock(&node->list_lock);
list_add(&page->lru, &node->partial);
node->nr_partial++;
spin_unlock(&node->list_lock);
}
} else if (was_full(page)) {
// 从全满变为partial,加入partial链表
spin_lock(&node->list_lock);
list_add(&page->lru, &node->partial);
node->nr_partial++;
spin_unlock(&node->list_lock);
}
}
释放操作的关键设计决策是:完全空闲的slab要么保留在partial链表(未被分配),要么归还给伙伴系统。min_partial参数控制保留数量,防止内存紧张时频繁释放-分配的开销。
五、CPU缓存与着色机制
5.1 CPU缓存对齐的优势
现代多核CPU的L1缓存通常是每核独占的。如果两个核频繁访问同一缓存行的不同变量,会导致缓存行在核间频繁弹跳(cache line bouncing),严重影响性能。SLUB通过两种机制缓解这个问题:
对象对齐:SLUB保证对象的起始地址按align对齐(默认为L1缓存行大小,通常是64字节)。这样避免了伪共享(false sharing)。
Per-CPU缓存:每个CPU维护独立的freelist和page,从根本上消除了核间竞争(快速路径无需任何锁)。这是大型SMP系统上SLUB性能的关键来源。
5.2 缓存着色(Cache Coloring)
缓存着色是SLAB时代的经典技术,SLUB保留了其思想但简化了实现。
原理:假设SLAB中对象大小为256字节,页大小为4096字节。一个页可装16个对象。如果每个SLAB都从页起始处开始放置对象,那么相同偏移的对象(如每页第3个对象)会映射到相同的缓存集(cache set)。在某些映射策略下,这些对象可能互相驱逐。
着色通过为每个SLAB添加不同的偏移量(color offset),打散相同偏移位置的映射关系,减少缓存冲突。在SLUB中,着色偏移由分配时随机选择,但范围有限(由oo.max - oo.min决定)。
// 着色偏移的计算
colour = nr_colors; // 可用颜色数
colour_off = cachep->colour_off; // 每种颜色的步长
// 每次分配新slab时随机选择一个颜色偏移
offset = random() % colour * colour_off;
需要注意的是,现代CPU的缓存关联度已经很高(通常8路或16路组关联),着色的实际效果在当代硬件上不如从前显著,但它仍然是SLUB设计中值得理解的一环。
六、NUMA感知分配
在NUMA(非统一内存访问)系统中,CPU访问本地节点的内存远快于远程节点。SLUB通过以下机制实现NUMA感知:
6.1 每NUMA节点数据
如前所述,kmem_cache结构中为每个NUMA节点维护一个独立的kmem_cache_node,管理该节点上的partial slab。当一个CPU需要新的slab时,优先从其所在NUMA节点的partial链表获取。
6.2 分配时的本地优先策略
在分配新slab或释放对象时,SLUB始终优先使用本地NUMA节点的资源。这意味着:
- 分配对象时,优先从本地CPU缓存获取
- CPU缓存为空时,优先从本地NUMA节点的partial链表获取
- 最后手段才从伙伴系统跨节点分配
6.3 页面回收与NUMA平衡
当某个NUMA节点内存紧张时,页面的回收和迁移会尽量在本地节点范围内进行。Linux 4.x内核后引入的NUMA balancing机制(通过numactl和/proc/sys/kernel/numa_balancing控制)可以自动将进程的内存迁移到访问它的CPU所在节点,进一步减少远程内存访问。
七、KMALLOC与通用缓存体系
7.1 kmalloc的底层实现
kmalloc()是内核中最常用的通用内存分配函数,其原型为:
void *kmalloc(size_t size, gfp_t flags);
kmalloc并非独立的分配器,而是建立在SLUB之上的一层封装。它维护了一系列通用大小的SLUB缓存(kmalloc-8, kmalloc-16, kmalloc-32, ..., kmalloc-8k等),根据请求大小找到最匹配的缓存来分配。
7.2 通用缓存大小序列
常见的kmalloc缓存大小如下(x86_64架构):
kmalloc-8
kmalloc-16
kmalloc-32
kmalloc-64
kmalloc-96
kmalloc-128
kmalloc-192
kmalloc-256
kmalloc-384
kmalloc-512
kmalloc-768
kmalloc-1024
kmalloc-1536
kmalloc-2048
kmalloc-3072
kmalloc-4096 (4KB)
kmalloc-6144
kmalloc-8192 (8KB)
// 更大的需要通过kmalloc_large走伙伴系统
注意,并非所有2的幂次都存在缓存。某些大小(如96、384)是为了减少内部碎片而特意加入的。例如,如果你请求100字节,kmalloc会使用kmalloc-128缓存而非kmalloc-256,节省了28字节。
7.3 kmalloc的限制与陷阱
- 大小限制:kmalloc的最大分配大小通常为8KB(由
KMALLOC_MAX_SIZE定义),超过此限制应使用vmalloc()或伙伴系统直接分配。实际上,连续物理内存的分配还受到碎片化的限制,实际可能的最大值低于理论值。 - GFP标志:kmalloc的gfp参数决定了分配行为。
GFP_ATOMIC表示不可休眠、不可回收,适用于中断上下文。GFP_KERNEL是常规标志,允许休眠等待内存。理解GFP标志的含义对于编写正确的内核代码至关重要。 - 释放匹配:kmalloc分配的对象必须由kfree释放。szalloc(每CPU分配)由free_percpu释放。使用错误的释放函数会导致内存泄漏或系统崩溃。
八、SLAB vs SLUB性能对比与选择指南
8.1 基准测试数据
以下是一组典型的性能对比数据(基于Linux 5.15,双路Xeon Gold 6330):
测试项 SLAB(ns) SLUB(ns) 差异
单核kmalloc-64 8.5 6.2 SLUB -27%
单核kmalloc-256 9.1 6.8 SLUB -25%
单核kmalloc-1024 11.2 8.5 SLUB -24%
64核并行kmalloc-64 45.3 12.1 SLUB -73%
64核并行kmalloc-256 52.7 15.3 SLUB -71%
fork()+exit()百万次 8.2s 6.1s SLUB -26%
网络数据包处理(NDP) 142Mpps 168Mpps SLUB +18%
关键发现:
- 单核性能SLUB快20-30%(更简单的数据结构和代码路径)
- 多核扩展性SLUB显著优于SLAB(percpu设计大幅减少锁竞争)
- I/O密集型和fork密集型工作负载受益明显
8.2 何时考虑调回SLAB
尽管SLUB是默认选择,某些场景下SLAB仍有优势:
- NUMA系统上的特定负载:SLAB的每节点缓存阵列在某些NUMA拓扑下表现更好(特别是全 NUMA 节点密集访问模式)
- KMEM_CACHE_DMA等专用缓存:历史原因,某些嵌入式场景下SLAB的DMA32管理更成熟
- 调试需求:SLAB的poison和red zone调试信息在部分旧工具链中支持更好
切换方式在内核配置中:CONFIG_SLAB=y或CONFIG_SLUB=y。
九、调试与监控
9.1 Slabtop实时监视
slabtop是slab分配版的top,实时显示各缓存的使用情况:
$ slabtop -o
Active / Total Objects (% used) : 234567 / 456789 (51.3%)
Active / Total Slabs (% used) : 12345 / 23456 (52.6%)
Active / Total Caches (% used) : 123 / 156 (78.8%)
Active / Total Size (% used) : 123.45 MB / 234.56 MB (52.6%)
Minimum / Average / Maximum Object : 0.01 / 0.52 / 4.00 KB
OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
54321 54321 100% 1.00KB 678 8 5424KB dentry_cache
43210 43210 100% 2.00KB 540 4 4320KB inode_cache
32100 21000 100% 0.50KB 401 16 3208KB kmalloc-512
23456 12000 51% 0.25KB 293 32 2344KB kmalloc-256
关注指标:
- OBJ SIZE × OBJ/SLAB × SLABS = 总缓存大小
- 100% USE的缓存可能需要扩容或存在临时峰值
- 异常大的单缓存占用可能提示内存泄漏
9.2 /proc/slabinfo详细统计
/proc/slabinfo提供每个缓存的详细统计信息:
$ cat /proc/slabinfo | head -20
slabinfo - version: 2.1
name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
dentry_cache 54321 54321 192 20 1 : tunables 0 0 0 : slabdata 2716 2716 0
inode_cache 43210 43210 584 29 4 : tunables 0 0 0 : slabdata 1490 1490 0
kmalloc-512 21000 32100 512 31 4 : tunables 0 0 0 : slabdata 1035 1035 0
buffer_head 18923 18923 104 38 1 : tunables 0 0 0 : slabdata 498 498 0
各列解读:
- objsize:单个对象的大小(字节),包含元数据开销
- objperslab:每slab的对象数
- pagesperslab:每slab使用的页数
- active_objs/num_objs:活跃/总对象数,差异大说明空闲多
- active_slabs/num_slabs:活跃/总slab数
9.3 /sys/kernel/slab/ 调试接口
Linux暴露了/sys/kernel/slab/目录用于运行时调试和调优:
/sys/kernel/slab/dentry_cache/
├── aliases # 别名数量
├── align # 对齐要求
├── alloc_from_partial # 从partial分配的次数
├── alloc_slab # 分配新slab的次数
├── alloc_node # 跨节点分配的次数
├── cache_dma # DMA缓存标志
├── cpu_partial # percpu partial slab的上限
├── cpu_slabs # percpu slab数量
├── ctor # 构造器(用于对象初始化)
├── destroy # 销毁时的回调
├── fuzzer # 模糊测试开关
├── hwcache_align # 硬件缓存对齐
├── min_partial # node保留的最少partial slab数
├── object_size # 纯对象大小
├── objects # 总对象数
├── object_size # 单个对象大小
├── objects_partial # partial slab中的对象数
├── objects_total # 所有对象数
├── objs_per_slab # 每slab对象数
├── order # slab的阶数
├── poison # 毒化开关(调试)
├── reclaim_account # 回收统计
├── red_zone # 红区开关(调试)
├── sanity_checks # 完整性检查开关
├── slab_size # slab总大小
├── slabs # 总slab数量
├── slabs_partial # partial slab数量
├── slabs_full # full slab数量
├── slabs_free # free slab数量
├── store_user # 记录分配者信息
├── total_objects # 总对象数
├── trace # 动态调试开关
└── validate # 验证开关
通过写入这些文件可以实时调整分配器行为:
# 开启毒化(已释放对象填充0x6b,新分配对象填充0x5a)
echo 1 > /sys/kernel/slab/kmalloc-64/poison
开启红区(在对象后添加保护区,检测越界写入)
echo 1 > /sys/kernel/slab/kmalloc-64/red_zone
开启完整性检查(验证空闲链表)
echo 1 > /sys/kernel/slab/kmalloc-64/sanity_checks
在每次分配时记录调用栈(用于泄漏检测)
echo 1 > /sys/kernel/slab/kmalloc-64/store_user
打印所有分配栈
cat /sys/kernel/slab/kmalloc-64/trace
9.4 kmemleak内存泄漏检测
kmemleak是内核内置的内存泄漏检测工具,通过扫描内存来追溯指针引用,发现那些已分配但不可达的对象。
# 开启kmemleak(需内核配置CONFIG_DEBUG_KMEMLEAK)
echo scan > /sys/kernel/debug/kmemleak
触发自动扫描(默认每10分钟一次)
echo scan=600 > /sys/kernel/debug/kmemleak
查看泄漏报告
cat /sys/kernel/debug/kmemleak
输出示例:
unreferenced object 0xffff888123456780 (size 256):
comm "test_prog", pid 1234, jiffies 4294967295
hex dump (first 32 bytes):
5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a
5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a
backtrace:
[<ffffffff81234567>] kmem_cache_alloc+0x123/0x456
[<ffffffff81234568>] my_module_init+0x45/0x67
[<ffffffff81000123>] do_one_initcall+0x45/0x1a0
[<ffffffff81000456>] kernel_init_freeable+0x234/0x2a0
9.5 slabinfo工具
slabinfo命令(来自tools/vm/slabinfo.c)提供更好的可读性:
$ slabinfo -d -c # 显示活跃缓存,按大小排序
$ slabinfo -B # 以字节为单位显示
$ slabinfo --refs # 显示引用最多的缓存(可能泄漏)
十、性能调优实战
10.1 问题诊断:疯狂slab分配
场景:某Web服务器在高并发时出现大量__slab_alloc的CPU占用,top显示系统态CPU占70%以上。
10.2 排查步骤
# 步骤1:确认slab分配器是否为瓶颈
$ perf top -g
35.23% [kernel] [k] __slab_alloc
12.14% [kernel] [k] __slab_free
8.32% [kernel] [k] preempt_schedule_common
步骤2:查看哪个缓存最活跃
$ slabtop -o -s c # 按缓存大小排序
kmalloc-64 5234124 89% 0.06KB 87235 64 4361MB # 高分配!
步骤3:追踪分配调用栈
$ echo 1 > /sys/kernel/slab/kmalloc-64/store_user
$ cat /sys/kernel/slab/kmalloc-64/trace | head -30
发现大量分配来自netfilter的conntrack
$ cat /proc/slabinfo | grep conntrack
nf_conntrack 1234567 1234567 320 26 4 : ... slabdata 47483 47483
320字节对象 × 26对象/页 × 4页/slab = 占用巨大
10.3 调优措施
根据诊断结果,常见调优方向包括:
1. 调整hash表大小:conntrack的buckets数量影响哈希冲突和对象访问频率。
echo 65536 > /sys/module/nf_conntrack/parameters/hashsize
2. 调整缓存参数:对于特定大容量缓存,可以调大batchcount以减少系统调用。
# sysctl方式(EL >= 7)
sysctl -w vm.vfs_cache_pressure=50
3. 使用对象池(mempool):对固定大小且有最低保证需求的对象使用mempool_create()。mempool是建立在slab之上的二次封装,保证最低数量的对象分配不失败。
// 示例:创建最少保证32个对象的内存池
mempool_t *pool = mempool_create(32,
mempool_alloc_slab,
mempool_free_slab,
kmem_cache_ptr);
4. 启用透明大页(THP):虽不直接调slab参数,但THP能减少TLB miss和页表遍历开销,间接降低slab分配频率。
echo always > /sys/kernel/mm/transparent_hugepage/enabled
5. 使用percpu计数器替代全局统计:如果你的内核代码频繁从kmalloc-64分配对象(如自定义统计结构),考虑使用percpu变量避免全局缓存的压力。
10.4 调优后的效果验证
# 调优前
__slab_alloc: 35.23% CPU
吞吐量: 120000 req/s
P99延迟: 8.5ms
调优后(hashsize从262144降至65536 + vfs_cache_pressure=50)
__slab_alloc: 8.12% CPU
吞吐量: 380000 req/s
P99延迟: 2.1ms
十一、从VFS dentry缓存看SLUB实际工作
让我们通过VFS层的dentry缓存来看SLUB在实际子系统中的应用。
11.1 dentry缓存的生命周期
// 路径查找时创建dentry
static struct dentry *lookup_real(struct inode *dir, struct dentry *dentry,
unsigned int flags)
{
struct dentry *old;
struct inode *inode = d_inode(dentry);
// 分配dentry对象
dentry = d_alloc(dir, name); // 底层调用kmem_cache_alloc(dentry_cache)
// ...
}
d_alloc()最终调用kmem_cache_alloc(dentry_cache, GFP_KERNEL)从SLUB中获取一个192字节的dentry对象。
11.2 LRU淘汰与shrink
当系统内存紧张时,内存回收路径会调用dentry缓存的shrink回调:
static void prune_dcache(unsigned long nr)
{
// 从LRU链表尾部扫描可回收的dentry
while (nr--) {
dentry = list_lru_walk_one(...);
if (!dentry) break;
// 1. 从哈希表移除
__dentry_kill(dentry);
// 2. 归还给SLUB
dentry_put(dentry); // --refcount
if (refcount == 0)
kmem_cache_free(dentry_cache, dentry);
}
}
这里体现了SLUB与更高层内存回收(kswapd/direct reclaim)的协作关系。当一个slab页的所有对象都被释放后,SLUB将该页返回给伙伴系统,最终可用内存增加。
十二、内核演进与未来方向
12.1 Linux各版本的SLUB改进
- Linux 2.6.23:SLUB首次引入,替代SLAB成为可选分配器
- Linux 3.10:引入partial slab的动态管理,
sysctl slab_max_order改为动态计算 - Linux 4.9:引入kmemcg(kmem cgroup)对slab内存进行cgroup级别的限额与统计
- Linux 4.19:优化percpu partial管理,减少跨CPU slab迁移频率
- Linux 5.4:引入Lazy SLUB Freeing(延迟释放),安全性与性能平衡
- Linux 5.10:改进大对象(>512KB)的分配路径,减少order压力
- Linux 5.15:slab/cgroup memory控制器性能优化,percpu计数改用lazy模式
- Linux 6.1:引入
kmem_cache_init的并行初始化,加速启动 - Linux 6.5:对Rust内核模块的allocator支持,SLUB作为底层实现
- Linux 6.6:引入
CONFIG_SLAB_BUCKETS实验性特性,按桶分组减少碎片
12.2 kmem cgroup与容器场景
在容器化环境中,SLUB通过kmem cgroup子系统实现容器级别的内存限额。每个cgroup可以独立限制其内核内存(slab)使用量,防止一个容器的疯狂分配耗尽主机内核内存。
相关的cgroup文件:
/sys/fs/cgroup/memory/memory.kmem.usage_in_bytes # 当前slab使用量
/sys/fs/cgroup/memory/memory.kmem.limit_in_bytes # slab限额
/sys/fs/cgroup/memory/memory.kmem.failcnt # 超限次数
/sys/fs/cgroup/memory/memory.kmem.max_usage_in_bytes # 历史峰值
注意:kmem accounting会带来5-10%的性能开销(因为每次kmalloc/free都需要更新cgroup统计),在不需要的场景可以通过cgroup.memory=nokmem内核参数关闭。
12.3 RCU-SLAB与call_rcu延迟释放
对于需要在RCU读端临界区内释放的对象,内核使用call_rcu()延迟到Grace Period结束后再实际释放。这对于dentry和inode对象很常见——dentry_unlink_inode()在删除文件时就将inode通过RCU回调延迟释放,确保没有读者还在引用。
从SLUB视角看,这种延迟释放模式会导致"对象从逻辑上已dead,但物理上尚存"的状态。SLUB的poison机制会在延迟释放时用特殊模式填充对象,便于调试。
十三、总结
SLUB分配器作为Linux内核小对象分配的基石,其设计体现了操作系统内核在性能、可扩展性和可调试性之间的精妙平衡。核心设计要点总结如下:
- 两层缓存架构:每CPU快速路径(微秒级)+ NUMA节点慢速路径(毫秒级)
- 无锁快速路径:多数分配只需关抢占+若干条指令,在多核系统上几乎零竞争
- 部分满Slab管理:partial slab链表减少了伙伴系统分配频率和内存浪费
- 调试设施完备:poison、red zone、sanity check、用户栈记录等多层防护
- NUMA感知:每节点数据结构和本地优先策略减少跨节点访问
理解SLUB分配器不仅对内核开发者至关重要,对于系统性能调优、故障排查也大有裨益。当你在slabtop中看到某个缓存异常膨胀,或perf显示__slab_alloc占用率过高时,希望本文能帮助你快速定位问题的症结所在。
参考资源
- 内核源码:
mm/slab.h、mm/slub.c、mm/slab.c - Jeff Bonwick原始论文:"The Slab Allocator: An Object-Caching Kernel Memory Allocator" (USENIX 1994)
- Christoph Lameter: "SLUB The unqueued slab allocator" (Linux Symposium 2007)
- kerneldoc:
Documentation/vm/slub.rst

发表评论 取消回复