Linux Slub Slow Allocate path
在上文中,slub通过fast路径分配内存对象失败后会进入到慢速路径当中
慢速分配路径总览
___slab_alloc() 先尝试从 partial 链表获取对象,失败后申请新 slab。指定首选节点但未设置 __GFP_THISNODE 时,先限制在目标节点尝试;新 slab 分配失败后,恢复原始标志重试。
flowchart TD
A["进入 ___slab_alloc()
per-CPU sheaves 分配失败或不可用"] --> B["计算 allow_spin
try_thisnode = true
记录 ALLOC_SLOWPATH"]
B --> C["new_objects
trynode_flags = gfpflags"]
C --> D{"指定首选 node
且原始标志无 __GFP_THISNODE
且 try_thisnode?"}
D -->|是| E["收紧 trynode_flags
保留 GFP_NOWAIT / __GFP_NOMEMALLOC / __GFP_ACCOUNT 范围内的位
加入 __GFP_NOWARN 和 __GFP_THISNODE"]
D -->|否| F["保留原始 gfpflags"]
E --> G["get_from_partial(s, node, trynode_flags, ac)
先查目标节点;未指定时查本地内存节点
失败且允许时,再查其他节点的 partial"]
F --> G
G --> H{"取得 object?"}
H -->|是| S["success"]
H -->|否| I["new_slab(s, trynode_flags, ac->alloc_flags, node)
申请并初始化新 slab"]
I --> J{"取得 slab?"}
J -->|否| K{"指定首选 node
且原始标志无 __GFP_THISNODE
且 try_thisnode?"}
K -->|是| L["try_thisnode = false
下轮恢复原始 gfpflags
允许跨节点回退"]
L --> C
K -->|否| M["slab_out_of_memory()
返回 NULL"]
J -->|是| N["记录 ALLOC_SLAB"]
N --> O{"CONFIG_SLUB_TINY
或 kmem_cache_debug(s)?"}
O -->|是| P["alloc_single_from_new_slab(s, slab, ac)
分配单个对象并执行调试处理
维护 partial / full 链表"]
P --> Q{"取得 object?"}
Q -->|是| S
Q -->|否| U{"allow_spin?"}
O -->|否| R["alloc_from_new_slab(s, slab, &object, 1, allow_spin)
取出一个对象,构建剩余 freelist
有剩余对象时挂入 partial"]
R --> T{"返回分配数量非零?"}
T -->|是| V["直接返回 object"]
T -->|否| U
U -->|是| C
U -->|否| W["避免无休止重试
返回 NULL"]
S --> X{"启用 SLAB_STORE_USER 调试?"}
X -->|是| Y["set_track()
记录分配调用位置等信息"]
X -->|否| Z["返回 object"]
Y --> Z
try_thisnode 仅在目标节点的新 slab 分配失败并触发回退时变为 false;从新 slab 获取对象失败后的重试不会主动修改它。普通缓存从新 slab 成功取得对象后直接返回,partial 分配成功及 TINY / debug 分支成功则经过 success。
内存对象分配的三个步骤
/*
* 指定了首选节点,但未设置 __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 链表,之后才可能从其他节点分配新的页。
*/
第一步 从当前节点进行内存分配
在分配的第一步骤中
// ___slab_alloc 4533
if (unlikely(node != NUMA_NO_NODE && !(gfpflags & __GFP_THISNODE)
&& try_thisnode)) {
trynode_flags &= GFP_NOWAIT | __GFP_NOMEMALLOC | __GFP_ACCOUNT;
trynode_flags |= __GFP_NOWARN | __GFP_THISNODE;
}
首先会在当前node被设置但是没有设置__GFP_THISNODE时,增加THISNODE标志,将首次尝试限制在目标节点,并设置 __GFP_NOWARN 抑制分配失败警告。同时收紧分配标志,避免这次优先尝试触发较重的回收行为。
尝试从当前的节点进行内存对象分配,在设置当前的flags后,进入到get_from_partial中
// ___slab_alloc
object = get_from_partial(s, node, trynode_flags, ac);
/*
* 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);
}
在外部调用kmalloc函数最终进入到慢速分配路径时,node的设置即为NUAM_NO_NODE(CONFIG_NUMA未配置开启,否则在进入慢速路径前node可能会被NUMA策略分配对应的值)
所以在使用其他的NUMA进行分配前会使用(node != NUMA_NO_NODE && (flags & __GFP_THISNODE))来停止第一步的步骤,使下方的从其他的NUMA处分配成为第二步。
get_from_partial_node
/*
* Try to allocate object from a partial slab on a specific node.
*/
static void *get_from_partial_node(struct kmem_cache *s,
struct kmem_cache_node *n,
gfp_t gfp_flags,
const struct slab_alloc_context *ac)
{
struct slab *slab, *slab2;
unsigned long flags;
void *object = NULL;
/*
* Racy check. If we mistakenly see no partial slabs then we
* just allocate an empty slab. If we mistakenly try to get a
* partial slab and there is none available then get_from_partial()
* will return NULL.
*/
if (!n || !n->nr_partial)
return NULL;
if (alloc_flags_allow_spinning(ac->alloc_flags))
spin_lock_irqsave(&n->list_lock, flags);
else if (!spin_trylock_irqsave(&n->list_lock, flags))
return NULL;
list_for_each_entry_safe(slab, slab2, &n->partial, slab_list) {
struct freelist_counters old, new;
if (!pfmemalloc_match(slab, gfp_flags))
continue;
if (IS_ENABLED(CONFIG_SLUB_TINY) || kmem_cache_debug(s)) {
object = alloc_single_from_partial(s, n, slab,
ac->orig_size);
if (object)
break;
continue;
}
/*
* get a single object from the slab. This might race against
* __slab_free(), which however has to take the list_lock if
* it's about to make the slab fully free.
*/
do {
old.freelist = slab->freelist;
old.counters = slab->counters;
new.freelist = get_freepointer(s, old.freelist);
new.counters = old.counters;
new.inuse++;
} while (!__slab_update_freelist(s, slab, &old, &new, "get_from_partial_node"));
object = old.freelist;
if (!new.freelist)
remove_partial(n, slab);
break;
}
spin_unlock_irqrestore(&n->list_lock, flags);
return object;
}
进入到get_from_partial_node中
最开始会进行简单的检查,尽管有数据竞争的风险,但是是简单可控的,通过一个检查如果当前没有空闲slab则进行返回。
获取分配对象,需要修改链表的内容,需要尝试获取锁,允许自旋时,使用 spin_lock_irqsave() 获取锁;不允许自旋时,使用 spin_trylock_irqsave(),获取失败则返回 NULL。
/*
* Called only for kmem_cache_debug() caches instead of remove_partial(), with a
* slab from the n->partial list. Remove only a single object from the slab, do
* the alloc_debug_processing() checks and leave the slab on the list, or move
* it to full list if it was the last free object.
*/
static void *alloc_single_from_partial(struct kmem_cache *s,
struct kmem_cache_node *n, struct slab *slab, int orig_size)
{
void *object;
slab_lockdep_assert_held(&n->list_lock);
#ifdef CONFIG_SLUB_DEBUG
if (s->flags & SLAB_CONSISTENCY_CHECKS) {
if (!validate_slab_ptr(slab)) {
slab_err(s, slab, "Not a valid slab page");
return NULL;
}
}
#endif
object = slab->freelist;
slab->freelist = get_freepointer(s, object);
slab->inuse++;
if (!alloc_debug_processing(s, slab, object, orig_size)) {
remove_partial(n, slab);
return NULL;
}
if (slab->inuse == slab->objects) {
remove_partial(n, slab);
add_full(s, n, slab);
}
return object;
}
在开启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中需要保证传入的slab数据来自kmem_cache_node->partial中,只有这样才能保证slab中有数据(alloc_single_from_partial中并无对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
};
/*
* 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中取出对象,并更新链表。
完成从kmem_cache_node中partial链表中分配空闲对象的功能。
同样的,在这里,当更新后的链表为空,即slab中无可分配对象时,仅仅将链表移出了空闲列表,却没有挂载到full列表中,而在alloc_single_from_partial中,完全使用后却会挂载full列表,这也就是我们上面说的,full 链表的维护取决于调试配置。
get_from_any_partial
进入到get_from_any_partial意味着,在当前开启NUMA配置的情况下,第一步的尝试分配失败,进入第二部,尝试从其他的NUMA上分配对象,或者,NUMA未配置,node=NUMA_NO_NODE最终此函数返回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 链表获取对象。
第二步 尝试分配新slab
接下来就是第二步,尝试从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中,会先进行高阶的内存分配,用以分摊管理开销,较大的内存分配并不是必须的,所以在尝试高阶内存分配失败时,会退回到低阶内存分配中。
/**
* DOC: 回收行为修饰标志
*
* 回收行为修饰标志
*
* %__GFP_DIRECT_RECLAIM 表示调用者可以进入直接内存回收。
* 如果有备用方案,可以清除此标志,以避免不必要的延迟。
* %__GFP_RECLAIM 是同时允许或禁止直接回收与 kswapd 回收的简写。
*
* 分配器的默认行为取决于请求大小。内核将阶数
* order > %PAGE_ALLOC_COSTLY_ORDER 的分配称为高成本分配。
* 非高成本分配通常非常关键,不能轻易失败,因此默认会隐式采用
* 尽量保证成功的策略,但存在例外,例如 OOM 受害任务的分配可能失败,
* 所以调用者仍然必须检查分配结果。
* 高成本分配则尽量避免干扰系统,即使没有触发 OOM killer,
* 也可能放弃尝试。
*
* 以下三个修饰标志可以覆盖其中一些隐含规则。
* 注意,它们都必须与 %__GFP_DIRECT_RECLAIM 一起使用。
*
* %__GFP_NORETRY:
* 在内存压力下,虚拟内存子系统(VM)只会尝试非常轻量的
* 直接内存回收来获取内存,因此仍可能睡眠。
* 它会避免触发 OOM killer 等对系统影响较大的操作。
* 调用者必须处理分配失败;在严重内存压力下,失败很可能发生。
* 当失败可以以较低成本处理时,例如仅导致吞吐量下降,
* 适合使用此标志。
*
* %__GFP_NOFAIL:
* VM 必须无限重试,因为调用者无法处理分配失败。
* 分配可能无限期阻塞,但绝不会返回失败,因此检查失败没有意义。
*
* 此类分配必须允许阻塞,并与 __GFP_DIRECT_RECLAIM 一起使用。
* 绝不能在不允许睡眠的上下文中使用。
*
* 对新增使用场景应仔细评估,只有不存在合理的失败处理策略时
* 才应使用此标志。不过,使用该标志明确优于调用者自行编写
* 围绕分配器无限重试的循环。
*
* 不支持使用 __GFP_NOFAIL 从伙伴系统分配 order > 1 的页面。
* 请考虑改用 kvmalloc()。
*
* %__GFP_NOMEMALLOC 用于明确禁止使用紧急保留内存。
* 如果同时设置了 %__GFP_MEMALLOC,则本标志优先
*/
在alloc_gfp设置中,标志所代表的含义已经放到了上方。
if ((alloc_gfp & __GFP_DIRECT_RECLAIM) && oo_order(oo) > oo_order(s->min))
alloc_gfp = (alloc_gfp | __GFP_NOMEMALLOC) & ~__GFP_RECLAIM;
在这里标志的设置,是一种优化,当前在oo_order(oo) > oo_order(s->min)开始尝试高阶内存分配的时候,并不是必须一个大小(s->min才是最小分配),所以在非必须的情况下,要禁止使用紧急内存空间,并禁止回收提高效率。
allocate_slab通过alloc_slab_page函数进行slab的内存分配,在alloc_slab_page中会使用页分配器进行分配空间,在这里就不再讲解页分配器的内容。
在尝试较大内存分配失败时,会进行小内存的分配的重试,失败则返回null。
在分配完成的后续中,会进行一些额外的操作。
包括设置slab的debug操作,进行数据填充,初始化扩展信息并填充,最后登记当前的slab信息
重新执行上述步骤
回到___slab_alloc执行流程中
通常情况下,slab都会进行正常的分配,当内存紧张时,node被设置且没有固定当前的node时,会通过goto返回开头重新开始分配,在这个阶段中,恢复原始 gfpflags 并重新尝试。此时 get_from_partial() 仍先检查目标节点,失败后才尝试其他节点的 partial 链表。如果仍没有对象,则调用 new_slab();传入的 node 仍是首选节点,但未设置 __GFP_THISNODE 时,底层页分配允许回退到其他节点。
同时跳回操作只会执行一次,之后因为try_thisnode = false不再进行重试。
slab分配成功
在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中。
alloc_single_from_new_slab的分配类似,自行查阅。
分配成功后返回,失败,在允许自旋的情况下返回new_objects并继续尝试。