我们根据上面的流程图,依次看下slub是如何分配的
首先从kmem_cache_cpu中分配,如果没有则从kmem_cache_cpu的partial链表分配,如果还没有则从kmem_cache_node中分配,如果kmem_cache_node中也没有,则需要向伙伴系统申请内存。
第一步先看看kmem_cache_cpu是如何实现
/*
* When changing the layout, make sure freelist and tid are still compatible
* with this_cpu_cmpxchg_double() alignment requirements.
*/
struct kmem_cache_cpu {
union {
struct {
void **freelist; /* Pointer to next available object */
unsigned long tid; /* Globally unique transaction id */
};
freelist_aba_t freelist_tid; /* 将 freelist 和 tid 封装为原子操作单元 */
};
struct slab *slab; /* 当前用于分配对象的Slab页 */
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct slab *partial; /* 部分分配的冻结Slab链表(仅启用CPU Partial时存在) */
#endif
local_lock_t lock; /* 保护上述字段的本地CPU锁 */
#ifdef CONFIG_SLUB_STATS
unsigned stat[NR_SLUB_STAT_ITEMS]; /* Slab分配统计信息 */
#endif
};
接下来,我们可以详细拆解一下这段代码:
联合体(Union)中的 freelist 和 tid
union {
struct {
void **freelist; // 指向当前Slab中下一个可用对象的指针
unsigned long tid; // 全局唯一的事务ID(Transaction ID)
};
freelist_aba_t freelist_tid; // 将两者封装为一个原子操作单元
};
目的:
无锁快速路径:在SLUB分配器中,对象的分配和释放通常通过无锁操作(如this_cpu_cmpxchg_double())实现,以规避传统锁的性能开销。
ABA问题防御:tid(事务ID)用于防止ABA问题。每次修改freelist时,tid会递增,确保即使freelist的值在并发操作中“看似未变”(如A→B→A),其tid也已变化,使得原子操作能检测到状态不一致。
联合体的作用:
freelist_tid(类型通常为u64或类似)将freelist和tid在内存中紧密打包,确保它们占据连续且对齐的内存空间,满足双字原子操作(如cmpxchg_double)的硬件对齐要求。
例如,在64位系统中,freelist(8字节)和tid(8字节)组合为一个16字节的单元,对齐到16字节边界,从而允许通过单条指令原子地比较和交换这两个字段。## 对齐要求:
this_cpu_cmpxchg_double()需要操作的两个字段必须满足:
a. 在内存中连续。
b. 对齐到双字(例如,16字节对齐)。
联合体强制freelist和tid共享同一内存区域,确保它们的布局符合上述条件。
slab指针
struct slab *slab; // 当前活跃的Slab页,用于快速分配对象
作用:指向当前CPU正在使用的Slab页,其中包含可分配的对象。
性能优化:通过本地化访问减少NUMA或缓存一致性开销。
partial 指针(条件编译)
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct slab *partial; // 部分空闲的冻结Slab链表
#endif
功能:
当启用CONFIG_SLUB_CPU_PARTIAL时,每个CPU会缓存部分空闲的Slab(称为“冻结”状态),避免频繁向全局链表归还/申请Slab。
在内存压力或特定条件下(如flush_slab),这些Partial Slab会被转移到全局链表(NUMA节点的partial链表)。
local_lock_t lock
local_lock_t lock; // 本地CPU锁,保护kmem_cache_cpu结构中的字段
作用:
在慢速路径(如Slab切换、统计更新)中,防止同一CPU上的不同上下文(如进程与中断)竞争访问kmem_cache_cpu结构。
注意:快速路径(无锁分配/释放)不依赖此锁,仅在慢速路径中使用。
stat 统计数组(条件编译)
#ifdef CONFIG_SLUB_STATS
unsigned stat[NR_SLUB_STAT_ITEMS]; // 统计计数器(如分配次数、缓存命中率等)
#endif
功能:在内核启用CONFIG_SLUB_STATS时,记录Slab分配器的运行时性能指标,用于调优和监控。
现在我们来看看kmem_cache_cpu在慢速分配时候是如何工作(___slab_alloc)
/*
* Slow path. The lockless freelist is empty or we need to perform
* debugging duties.
*
* Processing is still very fast if new objects have been freed to the
* regular freelist. In that case we simply take over the regular freelist
* as the lockless freelist and zap the regular freelist.
*
* If that is not working then we fall back to the partial lists. We take the
* first element of the freelist as the object to allocate now and move the
* rest of the freelist to the lockless freelist.
*
* And if we were unable to get a new slab from the partial slab lists then
* we need to allocate a new slab. This is the slowest path since it involves
* a call to the page allocator and the setup of a new slab.
*
* Version of __slab_alloc to use when we know that preemption is
* already disabled (which is the case for bulk allocation).
*/
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;
stat(s, ALLOC_SLOWPATH);
reread_slab:
slab = READ_ONCE(c->slab);
if (!slab) {
/*
* if the node is not online or has no normal memory, just
* ignore the node constraint
*/
if (unlikely(node != NUMA_NO_NODE &&
!node_isset(node, slab_nodes)))
node = NUMA_NO_NODE;
goto new_slab;
}
redo:
if (unlikely(!node_match(slab, node))) {
/*
* same as above but node_match() being false already
* implies node != NUMA_NO_NODE
*/
if (!node_isset(node, slab_nodes)) {
node = NUMA_NO_NODE;
} else {
stat(s, ALLOC_NODE_MISMATCH);
goto deactivate_slab;
}
}
/*
* By rights, we should be searching for a slab page that was
* PFMEMALLOC but right now, we are losing the pfmemalloc
* information when the page leaves the per-cpu allocator
*/
if (unlikely(!pfmemalloc_match(slab, gfpflags)))
goto deactivate_slab;
/* must check again c->slab in case we got preempted and it changed */
local_lock_irqsave(&s->cpu_slab->lock, flags);
if (unlikely(slab != c->slab)) {
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
goto reread_slab;
}
freelist = c->freelist;
if (freelist)
goto load_freelist;
freelist = get_freelist(s, slab);
if (!freelist) {
c->slab = NULL;
c->tid = next_tid(c->tid);
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
stat(s, DEACTIVATE_BYPASS);
goto new_slab;
}
stat(s, ALLOC_REFILL);
load_freelist:
lockdep_assert_held(this_cpu_ptr(&s->cpu_slab->lock));
/*
* freelist is pointing to the list of objects to be used.
* slab is pointing to the slab from which the objects are obtained.
* That slab must be frozen for per cpu allocations to work.
*/
VM_BUG_ON(!c->slab->frozen);
c->freelist = get_freepointer(s, freelist);
c->tid = next_tid(c->tid);
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
return freelist;
deactivate_slab:
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);
new_slab:
if (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);
/* we were preempted and partial list got empty */
goto new_objects;
}
slab = c->slab = slub_percpu_partial(c);
slub_set_percpu_partial(c, slab);
local_unlock_irqrestore(&s->cpu_slab->lock, flags);
stat(s, CPU_PARTIAL_ALLOC);
goto redo;
}
new_objects:
pc.flags = gfpflags;
pc.slab = &slab;
pc.orig_size = orig_size;
freelist = get_partial(s, node, &pc);
if (freelist)
goto check_new_slab;
slub_put_cpu_ptr(s->cpu_slab);
slab = new_slab(s, gfpflags, node);
c = slub_get_cpu_ptr(s->cpu_slab);
if (unlikely(!slab)) {
slab_out_of_memory(s, gfpflags, node);
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);
return freelist;
}
/*
* No other reference to the slab yet so we can
* muck around with it freely without cmpxchg
*/
freelist = slab->freelist;
slab->freelist = NULL;
slab->inuse = slab->objects;
slab->frozen = 1;
inc_slabs_node(s, slab_nid(slab), slab->objects);
check_new_slab:
if (kmem_cache_debug(s)) {
/*
* For debug caches here we had to go through
* alloc_single_from_partial() so just store the tracking info
* and return the object
*/
if (s->flags & SLAB_STORE_USER)
set_track(s, freelist, TRACK_ALLOC, addr);
return freelist;
}
if (unlikely(!pfmemalloc_match(slab, gfpflags))) {
/*
* For !pfmemalloc_match() case we don't load freelist so that
* we don't make further mismatched allocations easier.
*/
deactivate_slab(s, slab, get_freepointer(s, freelist));
return freelist;
}
retry_load_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);
stat(s, CPUSLAB_FLUSH);
goto retry_load_slab;
}
c->slab = slab;
goto load_freelist;
}
`___slab_alloc 是 SLUB 分配器的慢速路径函数,当快速路径(无锁分配)失败时,负责处理复杂场景
___slab_alloc
是 Linux 内核 SLUB 分配器中处理慢速路径的核心函数,主要用于以下场景和逻辑:
核心作用
- 处理快速路径失败
当 CPU 本地缓存(kmem_cache_cpu->freelist
)无可用对象时,通过慢速路径获取新对象。 - NUMA 优化
确保内存分配符合请求的 NUMA 节点,减少跨节点访问延迟。 - 调试与隔离
支持调试功能(如内存跟踪、毒化)和隔离 PFMEMALLOC 内存(用于内存回收的专用页)。 - 并发同步
通过锁和事务 ID(tid
)确保多核环境下的数据一致性,避免 ABA 问题。
核心逻辑流程
-
初始化检查
- 读取当前 CPU 的活跃 Slab(
c->slab
),若为空则跳转至新 Slab 分配(new_slab
)。 - 检查 NUMA 节点是否有效,若无效则忽略节点约束。
- 读取当前 CPU 的活跃 Slab(
-
NUMA 与 PFMEMALLOC 匹配
- 若 Slab 的 NUMA 节点与请求不匹配,停用当前 Slab(
deactivate_slab
)。 - 检查 Slab 的 PFMEMALLOC 标志是否与分配标志(
gfpflags
)匹配,不匹配则停用。
- 若 Slab 的 NUMA 节点与请求不匹配,停用当前 Slab(
-
加锁与状态重验
- 获取本地锁(
local_lock_irqsave
),防止同一 CPU 上的进程与中断竞争。 - 二次验证 Slab 是否被其他上下文修改,若已修改则重新读取(
reread_slab
)。
- 获取本地锁(
-
获取空闲链表(Freelist)
- 若本地
freelist
存在可用对象,直接分配。 - 若本地
freelist
为空,尝试从 Slab 页获取新freelist
(get_freelist
)。 - 若获取失败,标记 Slab 失效(
c->slab = NULL
),触发新 Slab 分配。
- 若本地
-
分配新对象
- 从 CPU 的 Partial 链表获取:若启用
CONFIG_SLUB_CPU_PARTIAL
,优先重用部分空闲 Slab。 - 从节点的 Partial 链表获取:通过
get_partial
批量获取部分空闲对象。 - 分配全新 Slab:调用伙伴系统(
new_slab
)分配新页,初始化并冻结 Slab。
- 从 CPU 的 Partial 链表获取:若启用
-
更新状态
- 递增事务 ID(
c->tid
),确保后续快速路径能检测到状态变化。 - 若启用调试,记录内存分配跟踪信息(
set_track
)。
- 递增事务 ID(
-
异常处理
- 若内存不足(
slab_out_of_memory
),触发 OOM 处理。 - 若并发冲突(如锁内发现 Slab 被修改),回滚并重试。
- 若内存不足(
关键设计
-
锁与无锁混合
- 快速路径无锁:通过原子操作(
this_cpu_cmpxchg_double
)实现高效分配。 - 慢速路径加锁:使用本地锁保护
kmem_cache_cpu
结构,避免并发修改。
- 快速路径无锁:通过原子操作(
-
事务 ID(
tid
)防 ABA- 每次修改
freelist
后递增tid
,确保并发操作能检测到状态变化。
- 每次修改
-
Partial 链表优化
- 缓存部分空闲 Slab,减少全局锁争用和内存碎片化。
-
NUMA 感知
- 优先从请求的 NUMA 节点分配,降低跨节点访问开销。
性能影响
- 快速恢复:通过重用 Partial 链表,减少全新 Slab 分配频率。
- 最小化锁范围:仅对关键操作加锁,缩短锁持有时间。
- 统计与调试:通过
stat()
记录性能事件,支持调优和问题排查。
总结
___slab_alloc
是 SLUB 分配器在复杂场景下(如本地缓存耗尽、NUMA 约束、调试需求)实现内存分配的核心逻辑。其通过精细的状态管理、锁优化和资源重用,平衡了性能与可靠性,确保多核系统中内存分配的高效性和正确性。
(参考链接:https://zhuanlan.zhihu.com/p/382056680#/)