Markdown to HTML

AuroBreeze Blog

A tiny, fast Markdown blog for GitHub Pages.

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;
}

这里的分配路径和上文很像,就不再多说了。

成功后,就完成了慢速路径的分配。