Markdown to HTML

AuroBreeze Blog

A tiny, fast Markdown blog for GitHub Pages.

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并继续尝试。