slub devlog 10-6
time start: 2026年10月6日 time end: 2026年10月8日
slub 慢速分配路径
4564 进入___slab_alloc函数后,就是慢速分配路径
/*
* The slab lists for all objects.
*/
struct kmem_cache_node {
spinlock_t list_lock;
unsigned long nr_partial;
struct list_head partial;
#ifdef CONFIG_SLUB_DEBUG
atomic_long_t nr_slabs;
atomic_long_t total_objects;
struct list_head full;
#endif
};
/* Reuses the bits in struct page */
struct slab {
memdesc_flags_t flags;
struct kmem_cache *slab_cache;
union {
struct {
struct list_head slab_list;
/* Double-word boundary */
struct freelist_counters;
};
struct rcu_head rcu_head;
};
unsigned int __page_type;
atomic_t __page_refcount;
#ifdef CONFIG_SLAB_OBJ_EXT
unsigned long obj_exts;
#endif
};
在关系上,kmem_cache_node中的partial中会将slab挂载上,即partial下有多个slab挂载,而slab的freelist下又挂载多个空闲分配对象。
在注释中,说明慢速路径获取对象,会进行三步
// 4547
/*
* 指定了首选节点,但未设置 __GFP_THISNODE 时:
*
* 1) 在传给 get_from_partial() 的 trynode_flags 中加入
* __GFP_THISNODE,只尝试从目标节点获取部分空闲的 slab。
* 2) 如果步骤 1) 失败,则以最多允许
* GFP_NOWAIT | __GFP_THISNODE 的方式,尽力尝试
* 从目标节点分配一个新的 slab。
* 3) 如果步骤 2) 失败,则使用原始 gfpflags 重试。
* 这样,get_from_partial() 会先尝试其他节点的部分空闲
* slab 链表,之后才可能从其他节点分配新的页。
*/
首先会在当前node被设置但是没有设置__GFP_THISNODE时,增加THISNODE标志,将首次尝试限制在目标节点,并设置 __GFP_NOWARN 抑制分配失败警告。同时收紧分配标志,避免这次优先尝试触发较重的回收行为。
/*
* Get an object from a partial slab
*/
static void *get_from_partial(struct kmem_cache *s, int node, gfp_t flags,
const struct slab_alloc_context *ac)
{
int searchnode = node;
void *object;
if (node == NUMA_NO_NODE)
searchnode = numa_mem_id();
object = get_from_partial_node(s, get_node(s, searchnode), flags, ac);
if (object || (node != NUMA_NO_NODE && (flags & __GFP_THISNODE)))
return object;
return get_from_any_partial(s, flags, ac);
}
进入到get_from_partial_node中
最开始会进行简单的检查,尽管有数据竞争的风险,但是是可控简单的。
获取分配对象,需要修改链表的内容,需要尝试获取锁,允许自旋时,使用 spin_lock_irqsave() 获取锁;不允许自旋时,使用 spin_trylock_irqsave(),获取失败则返回 NULL。
在开启CONFIG_SLUB_TINY 或启用debug后,进入到的alloc_single_from_partial,则是从slab的freelist中获取分配对象,并将keme_cache_node中的slab调整位置,从 slab 的 freelist 取出一个对象,并更新 freelist 和 inuse。
只有取走最后一个空闲对象时,才将 slab 从 partial 链表移除;full 链表的维护取决于调试配置,并非所有满 slab 都会被挂入 full。
在这里可以看出,在slab的freelist链表中挂载者多个空闲对象
alloc_single_from_partial就是简单的从kmem_cache_node中获取slab中的一个分配对象,但是我似乎没有看到校验当前slab是否数据,不知道这个是不是个问题。
/* Reuses the bits in struct page */
struct slab {
memdesc_flags_t flags;
struct kmem_cache *slab_cache;
union {
struct {
struct list_head slab_list;
/* Double-word boundary */
struct freelist_counters;
};
struct rcu_head rcu_head;
};
unsigned int __page_type;
atomic_t __page_refcount;
#ifdef CONFIG_SLAB_OBJ_EXT
unsigned long obj_exts;
#endif
};
/*
* Slab cache management.
*/
struct kmem_cache {
struct slub_percpu_sheaves __percpu *cpu_sheaves;
/* Used for retrieving partial slabs, etc. */
slab_flags_t flags;
unsigned long min_partial;
unsigned int size; /* Object size including metadata */
unsigned int object_size; /* Object size without metadata */
struct reciprocal_value reciprocal_size;
unsigned int offset; /* Free pointer offset */
unsigned int sheaf_capacity;
struct kmem_cache_order_objects oo;
/* Allocation and freeing of slabs */
struct kmem_cache_order_objects min;
gfp_t allocflags; /* gfp flags to use on each alloc */
int refcount; /* Refcount for slab cache destroy */
void (*ctor)(void *object); /* Object constructor */
unsigned int inuse; /* Offset to metadata */
unsigned int align; /* Alignment */
unsigned int red_left_pad; /* Left redzone padding size */
const char *name; /* Name (only for display!) */
struct list_head list; /* List of slab caches */
struct kmem_cache_per_node_ptrs per_node[MAX_NUMNODES];
};
/*
* The slab lists for all objects.
*/
struct kmem_cache_node {
spinlock_t list_lock;
unsigned long nr_partial;
struct list_head partial;
#ifdef CONFIG_SLUB_DEBUG
atomic_long_t nr_slabs;
atomic_long_t total_objects;
struct list_head full;
#endif
};
通过在取出slab中freelist所指向的空闲对象,并更新链表
完成从kmem_cache_node中partial链表中分配空闲对象的功能。
当分配没有完成,同时node未被设置或flag没有设置THISNODE,则进入到get_from_any_partial中
在上文中,当node被设置,但是THISNODE未被设置时,会给trynode_flags增加THISNODE标签。
这里进入get_from_any_partial判断,是在当前无法从partial中分配时,从可用的空间进行分配。
当CONFIG_NUMA未被设置时,返回NULL。
被设置时
zonelist = node_zonelist(mempolicy_slab_node(), gfp_flags);
for_each_zone_zonelist(zone, z, zonelist, highest_zoneidx) {
struct kmem_cache_node *n;
n = get_node(s, zone_to_nid(zone));
if (n && cpuset_zone_allowed(zone, gfp_flags) &&
n->nr_partial > s->min_partial) {
void *object = get_from_partial_node(s, n,
gfp_flags, ac);
if (object) {
/*
* Don't check read_mems_allowed_retry()
* here - if mems_allowed was updated in
* parallel, that was a harmless race
* between allocation and the cpuset
* update
*/
return object;
}
}
}
按内存策略得到 zonelist,并遍历候选 zone。cpuset_zone_allowed() 检查当前任务是否允许从该 zone 所属节点分配内存;只有节点存在,且 nr_partial > min_partial 等条件成立时,才尝试从该节点的 partial 链表获取对象。
至此介绍完 get_from_partial()。在前述三步策略的首次尝试中,由于设置了 __GFP_THISNODE,目标节点查找失败后会直接返回,不会进入 get_from_any_partial()。
接下来就是第二步,尝试从THISNODE分配新的slab进行分配。
// 4568
slab = new_slab(s, trynode_flags, ac->alloc_flags, node);
// 3440
static struct slab *new_slab(struct kmem_cache *s, gfp_t flags,
unsigned int alloc_flags, int node)
{
if (unlikely(flags & GFP_SLAB_BUG_MASK))
flags = kmalloc_fix_flags(flags);
WARN_ON_ONCE(s->ctor && (flags & __GFP_ZERO));
flags &= GFP_RECLAIM_MASK | GFP_CONSTRAINT_MASK;
return allocate_slab(s, flags, alloc_flags, node);
}
在这里会先修复flags中slub分配器不允许使用的分配标志(__GFP_DMA32|__GFP_HIGHMEM|~__GFP_BITS_MASK)
之后通过allocate_slab进行分配新的slab
/* Allocate and initialize a slab without building its freelist. */
static struct slab *allocate_slab(struct kmem_cache *s, gfp_t flags,
unsigned int alloc_flags, int node)
{
bool allow_spin = alloc_flags_allow_spinning(alloc_flags);
struct slab *slab;
struct kmem_cache_order_objects oo = s->oo;
gfp_t alloc_gfp;
void *start;
flags &= gfp_allowed_mask;
flags |= s->allocflags;
/*
* Let the initial higher-order allocation fail under memory pressure
* so we fall-back to the minimum order allocation.
*/
alloc_gfp = (flags | __GFP_NOWARN | __GFP_NORETRY) & ~__GFP_NOFAIL;
if ((alloc_gfp & __GFP_DIRECT_RECLAIM) && oo_order(oo) > oo_order(s->min))
alloc_gfp = (alloc_gfp | __GFP_NOMEMALLOC) & ~__GFP_RECLAIM;
slab = alloc_slab_page(alloc_gfp, node, oo, allow_spin);
if (unlikely(!slab)) {
oo = s->min;
alloc_gfp = flags;
/*
* Allocation may have failed due to fragmentation.
* Try a lower order alloc if possible
*/
slab = alloc_slab_page(alloc_gfp, node, oo, allow_spin);
if (unlikely(!slab))
return NULL;
stat(s, ORDER_FALLBACK);
}
/* Initializes frozen, inuse, and any extra 64bit-only flags */
slab->counters = 0;
slab->objects = oo_objects(oo);
#ifdef CONFIG_64BIT
if (cache_needs_objcg(s))
slab->obj_exts_needs_objcg = 1;
#endif
slab->slab_cache = s;
kasan_poison_slab(slab);
start = slab_address(slab);
setup_slab_debug(s, slab, start);
init_slab_obj_exts(slab);
/*
* Poison the slab before initializing the slabobj_ext array
* to prevent the array from being overwritten.
*/
alloc_slab_obj_exts_early(s, slab);
account_slab(slab, oo_order(oo), s, flags, alloc_flags);
return slab;
}
在allocate_slab中会先进行尝试较多的内存分配,分摊管理消费,因为较大的内存分配并不是必须的,首次使用缓存首选的 order 分配页面,以分摊管理开销,并设置 __GFP_NOWARN、__GFP_NORETRY,清除 __GFP_NOFAIL。只有请求允许直接回收,且首选 order 大于最小 order 时,才进一步清除 __GFP_RECLAIM。失败后使用 s->min 和原来的 flags 再次尝试。
allocate_slab通过alloc_slab_page函数进行slab的内存分配,在alloc_slab_page中会使用页分配器进行分配空间,在这里就不再讲解页分配器的内容。
在分配完成的后续中,会进行一些额外的操作。
包括设置slab的debug操作,进行数据填充,初始化扩展信息并填充,最后登记当前的slab信息
回到___slab_alloc执行流程中
通常情况下,slab都会进行正常的分配,当内存紧张时,node被设置且没有固定当前的node时,会通过goto返回开头重新开始分配,在这个阶段中,首次在目标节点分配新 slab 失败后,恢复原始 gfpflags 并重新尝试。此时 get_from_partial() 仍先检查目标节点,失败后才尝试其他节点的 partial 链表。如果仍没有对象,则调用 new_slab();传入的 node 仍是首选节点,但未设置 __GFP_THISNODE 时,底层页分配允许回退到其他节点。
在slab中,会继续进行分配对象
/* Structure used to iterate over objects within a slab */
struct slab_obj_iter {
unsigned long pos;
void *start;
};
static unsigned int alloc_from_new_slab(struct kmem_cache *s, struct slab *slab,
void **p, unsigned int count, bool allow_spin)
{
unsigned int allocated = 0;
struct slab_obj_iter iter;
bool needs_add_partial = true;
unsigned long flags;
/*
* Are we going to put the slab on the partial list?
* Note slab->inuse is 0 on a new slab.
*/
if (count >= slab->objects) {
needs_add_partial = false;
count = slab->objects;
}
init_slab_obj_iter(s, slab, &iter, allow_spin);
while (allocated < count) {
p[allocated] = next_slab_obj(s, &iter);
allocated++;
}
slab->inuse = count;
build_slab_freelist(s, slab, &iter);
if (needs_add_partial) {
struct kmem_cache_node *n = get_node(s, slab_nid(slab));
if (allow_spin) {
spin_lock_irqsave(&n->list_lock, flags);
} else if (!spin_trylock_irqsave(&n->list_lock, flags)) {
/*
* Unlucky, discard newly allocated slab.
* The slab is not fully free, but it's fine as
* objects are not allocated to users.
*/
free_new_slab_nolock(s, slab);
return 0;
}
add_partial(n, slab, ADD_TO_HEAD);
spin_unlock_irqrestore(&n->list_lock, flags);
}
inc_slabs_node(s, slab_nid(slab), slab->objects);
return allocated;
}
在alloc_from_new_slab中,当前当 count < slab->objects 时,分配后还有空闲对象,将 slab 挂入 partial 链表;当 count >= slab->objects 时,将 count 限制为 slab 的对象总数,取出全部对象,不挂入 partial。
为什么全部分配完成后,不进行挂载?不进行挂载怎么将slab找回?
释放对象时,可以根据对象地址找到所属页,再取得对应的 slab 元数据。因此,即使满 slab 不在 partial 链表中,也仍然能够找到它。
当然在看到p[allocated] = next_slab_obj(s, &iter);可能会有疑问,我们在调用的时候,传入的p的数据是void *object,这个数据根本就没有分配空间,他是怎么通过p[allocated]进行分配的?
调用时传入的是 &object,且 count=1。局部变量 void *object 本身具有存储空间,p[0] 对应这个变量,函数通过它写回分配得到的对象地址,因此不需要另外申请指针数组。其他count>1的情况下就需要使用数组等方式了。
取出分配对象后,重建freelist链表,并在有空闲分配对象时,挂载入slab所属node下的kmem_cache_node中。
/*
* Called only for kmem_cache_debug() caches to allocate from a freshly
* allocated slab. Allocate a single object instead of whole freelist
* and put the slab to the partial (or full) list.
*/
static void *alloc_single_from_new_slab(struct kmem_cache *s, struct slab *slab,
const struct slab_alloc_context *ac)
{
bool allow_spin = alloc_flags_allow_spinning(ac->alloc_flags);
struct kmem_cache_node *n;
struct slab_obj_iter iter;
bool needs_add_partial;
unsigned long flags;
void *object;
init_slab_obj_iter(s, slab, &iter, allow_spin);
object = next_slab_obj(s, &iter);
slab->inuse = 1;
needs_add_partial = (slab->objects > 1);
build_slab_freelist(s, slab, &iter);
/* alloc_debug_processing() always expects a valid freepointer */
set_freepointer(s, object, slab->freelist);
if (!alloc_debug_processing(s, slab, object, ac->orig_size)) {
/*
* It's not really expected that this would fail on a
* freshly allocated slab, but a concurrent memory
* corruption in theory could cause that.
* Leak memory of allocated slab.
*/
return NULL;
}
n = get_node(s, slab_nid(slab));
if (allow_spin) {
spin_lock_irqsave(&n->list_lock, flags);
} else if (!spin_trylock_irqsave(&n->list_lock, flags)) {
/*
* Unlucky, discard newly allocated slab.
* The slab is not fully free, but it's fine as
* objects are not allocated to users.
*/
free_new_slab_nolock(s, slab);
return NULL;
}
if (needs_add_partial)
add_partial(n, slab, ADD_TO_HEAD);
else
add_full(s, n, slab);
/*
* Debug caches require nr_slabs updates under n->list_lock so validation
* cannot race with slab (de)allocations and observe inconsistent state.
*/
inc_slabs_node(s, slab_nid(slab), slab->objects);
spin_unlock_irqrestore(&n->list_lock, flags);
return object;
}
这里的分配路径和上文很像,就不再多说了。
成功后,就完成了慢速路径的分配。