Markdown to HTML

AuroBreeze Blog

A tiny, fast Markdown blog for GitHub Pages.

Linux Slub Fast Allocate path

在 Linux 中,以字节为单位申请内存时,通常会使用 kmalloc()。对于适合对象缓存的分配请求,kmalloc() 会使用底层的 SLUB 对象分配器。

SLUB 的分配流程包含快速路径和慢速路径。本文主要分析所列代码中基于 per-CPU sheaves 的快速路径。

kmalloc 调用链

kmalloc() 会根据请求大小是否为编译期常量,选择不同的入口:

flowchart TD
    A["kmalloc()"] --> B["kmalloc_noprof()"]
    B --> C["_kmalloc_noprof()"]

    C -->|"非零编译期常量,且大小适合缓存"| D["提前确定缓存
__kmalloc_cache_noprof()"] C -->|"大小无法编译期确定,或为零"| E["__kmalloc_noprof()"] C -->|"编译期确定为大块分配"| H["大块分配路径
页分配器"] E --> F["__do_kmalloc_node()"] F -->|"普通大小"| G["kmalloc_slab()
选择缓存"] F -->|"超过缓存最大尺寸"| H F -->|"大小为零"| I["ZERO_SIZE_PTR"] D --> J["slab_alloc_node()"] G --> J

将路径展开后

#define kmalloc(size, flags) alloc_hooks(kmalloc_noprof(size, flags)) // linux/slab.h 1056 line
// alloc_tag.h
#define DEFINE_ALLOC_TAG(_alloc_tag)

#define alloc_hooks_tag(_tag, _do_alloc)                \
({                                  \
    typeof(_do_alloc) _res;                     \
    if (mem_alloc_profiling_enabled()) {                \
        struct alloc_tag * __maybe_unused _old;         \
        _old = alloc_tag_save(_tag);                \
        _res = _do_alloc;                   \
        alloc_tag_restore(_tag, _old);              \
    } else                              \
        _res = _do_alloc;                   \
    _res;                               \
})

#define alloc_hooks(_do_alloc)                      \
({                                  \
    DEFINE_ALLOC_TAG(_alloc_tag);                   \
    alloc_hooks_tag(&_alloc_tag, _do_alloc);            \
})

在这里,alloc_hooks中,只有在启用CONFIG_MEM_ALLOC_PROFILING(分配数据统计)配置开关时,才会使用DEFINE_ALLOC_TAG。

而在alloc_hooks_tag中,因为当前并没有开启对应的配置开关,mem_alloc_profiling_enabled()会直接返回为false,所以整个alloc_hooks仅执行_res = _do_alloc,即执行传入的分配表达式。

而传入的_do_alloc即为

// linux/slab.h 997 line
#define kmalloc_noprof(...)         _kmalloc_noprof(__VA_ARGS__, __kmalloc_token(__VA_ARGS__))

#define __kmalloc_token(...) ((kmalloc_token_t){}) /* no-op */ // 533

// 982
static __always_inline __alloc_size(1) void *_kmalloc_noprof(size_t size, gfp_t flags, kmalloc_token_t token)
{
    if (__builtin_constant_p(size) && size) {
        unsigned int index;

        if (size > KMALLOC_MAX_CACHE_SIZE)
            return __kmalloc_large_noprof(size, flags);

        index = kmalloc_index(size);
        return __kmalloc_cache_noprof(
                kmalloc_caches[kmalloc_type(flags, token)][index],
                flags, size);
    }
    return __kmalloc_noprof(PASS_TOKEN_PARAMS(size, token), flags);
}

在本文所讨论的配置下,kmalloc_token_t 未发挥实际作用。

_kmalloc_noprof() 首先区分请求大小能否在编译期确定。

对于非零且不超过 KMALLOC_MAX_CACHE_SIZE 的编译期常量,直接选定缓存并调用 __kmalloc_cache_noprof();运行时才能确定大小的请求则进入 __kmalloc_noprof()。两条对象缓存分配路径最终汇合到 slab_alloc_node(),区别主要在于选择 kmem_cache 的时机。大块分配和零大小请求单独处理。


编译期常量的缓存选择

先通过 kmalloc_type() 选择缓存类型。

kmalloc_type() 根据分配标志选择缓存类型。未启用 CONFIG_KMALLOC_PARTITION_RANDOM 或 CONFIG_KMALLOC_PARTITION_TYPED 时,这里的 token 参数不参与选择。

/*
 * Whenever changing this, take care of that kmalloc_type() and
 * create_kmalloc_caches() still work as intended.
 *
 * KMALLOC_NORMAL can contain only unaccounted objects whereas KMALLOC_CGROUP
 * is for accounted but unreclaimable and non-dma objects. All the other
 * kmem caches can have both accounted and unaccounted objects.
 */
enum kmalloc_cache_type {
    KMALLOC_NORMAL = 0,
#ifndef CONFIG_ZONE_DMA
    KMALLOC_DMA = KMALLOC_NORMAL,
#endif
#ifndef CONFIG_MEMCG
    KMALLOC_CGROUP = KMALLOC_NORMAL,
#endif
#ifndef CONFIG_SLAB_OBJ_EXT
    KMALLOC_NO_OBJ_EXT = KMALLOC_NORMAL,
#endif
    KMALLOC_PARTITION_START = KMALLOC_NORMAL,
    KMALLOC_PARTITION_END = KMALLOC_PARTITION_START + KMALLOC_PARTITION_CACHES_NR,
#ifdef CONFIG_SLUB_TINY
    KMALLOC_RECLAIM = KMALLOC_NORMAL,
#else
    KMALLOC_RECLAIM,
#endif
#ifdef CONFIG_ZONE_DMA
    KMALLOC_DMA,
#endif
#ifdef CONFIG_MEMCG
    KMALLOC_CGROUP,
#endif
#ifdef CONFIG_SLAB_OBJ_EXT
    KMALLOC_NO_OBJ_EXT,
#endif
    NR_KMALLOC_TYPES
};

static __always_inline enum kmalloc_cache_type kmalloc_type(gfp_t flags, kmalloc_token_t token)
{
    /*
     * The most common case is KMALLOC_NORMAL, so test for it
     * with a single branch for all the relevant flags.
     */
    if (likely((flags & KMALLOC_NOT_NORMAL_BITS) == 0))
#ifdef CONFIG_KMALLOC_PARTITION_RANDOM
        /* KMALLOC_PARTITION_CACHES_NR (=15) copies + the KMALLOC_NORMAL */
        return KMALLOC_PARTITION_START + hash_64(token.v ^ random_kmalloc_seed,
                             ilog2(KMALLOC_PARTITION_CACHES_NR + 1));
#elif defined(CONFIG_KMALLOC_PARTITION_TYPED)
        return KMALLOC_PARTITION_START + token.v;
#else
        return KMALLOC_NORMAL;
#endif

    /*
     * At least one of the flags has to be set. Their priorities in
     * decreasing order are:
     *  1) __GFP_DMA
     *  2) __GFP_RECLAIMABLE
     *  3) __GFP_ACCOUNT
     */
    if (IS_ENABLED(CONFIG_ZONE_DMA) && (flags & __GFP_DMA))
        return KMALLOC_DMA;
    if (!IS_ENABLED(CONFIG_MEMCG) || (flags & __GFP_RECLAIMABLE))
        return KMALLOC_RECLAIM;
    else
        return KMALLOC_CGROUP;
}

需要注意的是,在不同的配置下,各个类型的数值可能不同。

typedef struct kmem_cache * kmem_buckets[KMALLOC_SHIFT_HIGH + 1];

kmem_buckets kmalloc_caches[NR_KMALLOC_TYPES] __ro_after_init =
{ /* initialization for https://llvm.org/pr42570 */ };

NR_KMALLOC_TYPES 位于枚举末尾,表示缓存类型的数量。kmem_buckets 是一组缓存指针,因此 kmalloc_caches 相当于一个二维数组:kmalloc_caches[NR_KMALLOC_TYPES][KMALLOC_SHIFT_HIGH + 1],第一维选择类型,第二维选择尺寸档位。

然后使用kmalloc_index进行第二层的选择。

#define kmalloc_index(s) __kmalloc_index(s, true)
/*
 * Figure out which kmalloc slab an allocation of a certain size
 * belongs to.
 * 0 = zero alloc
 * 1 =  65 .. 96 bytes
 * 2 = 129 .. 192 bytes
 * n = 2^(n-1)+1 .. 2^n
 *
 * Note: __kmalloc_index() is compile-time optimized, and not runtime optimized;
 * typical usage is via kmalloc_index() and therefore evaluated at compile-time.
 * Callers where !size_is_constant should only be test modules, where runtime
 * overheads of __kmalloc_index() can be tolerated.  Also see kmalloc_slab().
 */
static __always_inline unsigned int __kmalloc_index(size_t size,
                            bool size_is_constant)
{
    if (!size)
        return 0;

    if (size <= KMALLOC_MIN_SIZE)
        return KMALLOC_SHIFT_LOW;

    if (KMALLOC_MIN_SIZE <= 32 && size > 64 && size <= 96)
        return 1;
    if (KMALLOC_MIN_SIZE <= 64 && size > 128 && size <= 192)
        return 2;
    if (size <=          8) return 3;
    if (size <=         16) return 4;
    if (size <=         32) return 5;
    if (size <=         64) return 6;
    if (size <=        128) return 7;
    if (size <=        256) return 8;
    if (size <=        512) return 9;
    if (size <=       1024) return 10;
    if (size <=   2 * 1024) return 11;
    if (size <=   4 * 1024) return 12;
    if (size <=   8 * 1024) return 13;
    if (size <=  16 * 1024) return 14;
    if (size <=  32 * 1024) return 15;
    if (size <=  64 * 1024) return 16;
    if (size <= 128 * 1024) return 17;
    if (size <= 256 * 1024) return 18;
    if (size <= 512 * 1024) return 19;
    if (size <= 1024 * 1024) return 20;
    if (size <=  2 * 1024 * 1024) return 21;

    if (!IS_ENABLED(CONFIG_PROFILE_ALL_BRANCHES) && size_is_constant)
        BUILD_BUG_ON_MSG(1, "unexpected size in kmalloc_index()");
    else
        BUG();

    /* Will never be reached. Needed because the compiler may complain */
    return -1;
}

最终得到该编译期常量请求对应的 kmem_cache。


两条入口的汇合

void *__kmalloc_cache_noprof(struct kmem_cache *s, gfp_t gfpflags, size_t size)
{
    void *ret;
    const struct slab_alloc_context ac = {
        .caller_addr = _RET_IP_,
        .orig_size = size,
        .alloc_flags = SLAB_ALLOC_DEFAULT,
    };

    ret = slab_alloc_node(s, gfpflags, NUMA_NO_NODE, &ac);

    trace_kmalloc(_RET_IP_, ret, size, s->size, gfpflags, NUMA_NO_NODE);

    ret = kasan_kmalloc(s, ret, size, gfpflags);
    return ret;
}
void *__kmalloc_noprof(DECL_TOKEN_PARAMS(size, token), gfp_t flags)
{
    const struct slab_alloc_context ac = {
        .caller_addr = _RET_IP_,
        .orig_size = size,
        .alloc_flags = SLAB_ALLOC_DEFAULT,
    };

    return __do_kmalloc_node(NULL, flags,  NUMA_NO_NODE,
                 PASS_TOKEN_PARAM(token), &ac);
}

static __always_inline
void *__do_kmalloc_node(kmem_buckets *b, gfp_t flags, int node,
            kmalloc_token_t token, const struct slab_alloc_context *ac)
{
    const size_t size = ac->orig_size;
    struct kmem_cache *s;
    void *ret;

    if (unlikely(size > KMALLOC_MAX_CACHE_SIZE)) {
        ret = __kmalloc_large_node_noprof(size, flags, node);
        trace_kmalloc(ac->caller_addr, ret, size,
                  PAGE_SIZE << get_order(size), flags, node);
        return ret;
    }

    if (unlikely(!size))
        return ZERO_SIZE_PTR;

    s = kmalloc_slab(size, b, flags, token, ac->alloc_flags);

    ret = slab_alloc_node(s, flags, node, ac);
    ret = kasan_kmalloc(s, ret, size, flags);
    trace_kmalloc(ac->caller_addr, ret, size, s->size, flags, node);
    return ret;
}

__kmalloc_cache_noprof直接使用了slab_alloc_node进行了内存分配,而__kmalloc_noprof则需要进行选择kmem_cache,进行slab_alloc_node。

不过对于超过当前对象分配池大小的,都会通过__kmalloc_large_node_noprof,从而进入到页分配器进行内存分配。

在这段代码中还使用了__alloc_size的宏,在展开后则为gcc的__attribute__((alloc_size(n)))扩展。

在这个扩展中,会在编译阶段,帮助编译器分析内存分配,在只有一个参数的时候,默认最后返回的空间大小为第n个参数传入的大小,单位为字节。

而其中的__builtin_constant_p用来进行判断是否是编译时常量,常量为1,否则为0。


进入 SLUB 快速分配路径

在使用slab_alloc_node进行对象分配前,会先使用kmalloc_slab进行kmem_cache的选择

kmem_cache 选择

kmalloc_slab() 根据缓存类型和请求大小选择缓存:

flowchart TD
A["kmalloc_slab()"] --> B["kmalloc_type(flags, token)"]
    B --> C{"设置 SLAB_ALLOC_NO_OBJ_EXT?"}
    C -->|是| D["type = KMALLOC_NO_OBJ_EXT"]
    C -->|否| E{"b 为 NULL?"}
    D --> E
    E -->|是| F["b = &kmalloc_caches[type]"]
    E -->|否| G{"size 不超过 192?"}
    F --> G
    G -->|是| H["通过 kmalloc_size_index 查表"]
    G -->|否| I["index = fls(size - 1)"]
    H --> J["返回对应缓存指针"]
    I --> J
/*
 * Find the kmem_cache structure that serves a given size of
 * allocation
 *
 * This assumes size is larger than zero and not larger than
 * KMALLOC_MAX_CACHE_SIZE and the caller must check that.
 */
static inline struct kmem_cache *
kmalloc_slab(size_t size, kmem_buckets *b, gfp_t flags, kmalloc_token_t token,
         unsigned int alloc_flags)
{
    unsigned int index;
    enum kmalloc_cache_type type = kmalloc_type(flags, token);

    if (alloc_flags & SLAB_ALLOC_NO_OBJ_EXT)
        type = KMALLOC_NO_OBJ_EXT;

    if (!b)
        b = &kmalloc_caches[type];
    if (size <= 192)
        index = kmalloc_size_index[size_index_elem(size)];
    else
        index = fls(size - 1);

    return (*b)[index];
}

这里值得注意的是,缓存尺寸并不全是 2 的幂,还包含 96、192 字节的档位。

static inline unsigned int size_index_elem(unsigned int bytes)
{
    return (bytes - 1) / 8;
}

/*
 * Conversion table for small slabs sizes / 8 to the index in the
 * kmalloc array. This is necessary for slabs < 192 since we have non power
 * of two cache sizes there. The size of larger slabs can be determined using
 * fls.
 */
u8 kmalloc_size_index[24] __ro_after_init = {
    3,  /* 8 */
    4,  /* 16 */
    5,  /* 24 */
    5,  /* 32 */
    6,  /* 40 */
    6,  /* 48 */
    6,  /* 56 */
    6,  /* 64 */
    1,  /* 72 */
    1,  /* 80 */
    1,  /* 88 */
    1,  /* 96 */
    7,  /* 104 */
    7,  /* 112 */
    7,  /* 120 */
    7,  /* 128 */
    2,  /* 136 */
    2,  /* 144 */
    2,  /* 152 */
    2,  /* 160 */
    2,  /* 168 */
    2,  /* 176 */
    2,  /* 184 */
    2   /* 192 */
};

从这张表可以看出,小尺寸请求按每 8 字节一个区间进行查表,但多个区间可能映射到同一个缓存索引。例如,17~32 字节的请求都映射到索引 5,对应 32 字节缓存;65~96 字节的请求都映射到索引 1,对应 96 字节缓存。因此,这里并不是每隔 8 字节设置一个缓存,而是通过映射表选择能够容纳请求大小的缓存档位。

结合前面的 kmalloc_index() 可以发现,两条路径都是把请求大小转换为缓存索引:编译期已知大小的路径,可以由编译器折叠索引计算;这里的 kmalloc_slab() 则在运行时通过查表或 fls(size - 1) 计算索引。对于不超过 192 字节的请求,查表还能处理 96、192 字节这两个非 2 的幂的特殊档位。

有一个问题是,既然这里的查表方式和上方的__kmalloc_index一致,为什么不选择直接使用__kmalloc_index?

上方使用__kmalloc_index是在编译期使用进行的判断,在编译期间可以通过确定的数值,直接进行代码的优化,折叠成索引数值,从而不进行判断操作。

而在此处使用,是为了让运行时选索引的开销更低、更稳定。

在kmalloc的调用路径中,传入的kmem_buckets *b为null即为空,所以b会被设置为kmalloc_caches中的其中一个对象分配池,最终返回对象分配池中对应索引的kmem_cache。

slab_alloc_node 获取分配空间

slab_alloc_node() 的整体流程如下,快速分配失败后汇入慢速路径;KFENCE 成功则直接进入分配后处理。

flowchart TD
A["slab_alloc_node()"] --> B["slab_pre_alloc_hook()"]
    B --> C{"缓存 s 非 NULL?"}
    C -->|否| N["返回 NULL"]
    C -->|是| D["kfence_alloc()"]
    D --> E{"取得 KFENCE 对象?"}
    E -->|是| P["slab_post_alloc_hook()"]
    E -->|否| F["apply_strict_numa_policy()"]
    F --> G["alloc_from_pcs()"]
    G --> H{"取得对象?"}
    H -->|否| I["___slab_alloc()
慢速分配"] H -->|是| J["maybe_wipe_obj_freeptr()"] I --> J J --> P P --> R["返回 object
后处理仍可能使其为 NULL"]
// __do_kmalloc_node()
ret = slab_alloc_node(s, flags, node, ac);
/*
 * Inlined fastpath so that allocation functions (kmalloc, kmem_cache_alloc)
 * have the fastpath folded into their functions. So no function call
 * overhead for requests that can be satisfied on the fastpath.
 *
 * The fastpath works by first checking if the lockless freelist can be used.
 * If not then __slab_alloc is called for slow processing.
 *
 * Otherwise we can simply pick the next object from the lockless free list.
 */
static __fastpath_inline void *slab_alloc_node(struct kmem_cache *s,
        gfp_t gfpflags, int node, const struct slab_alloc_context *ac)
{
    void *object;

    s = slab_pre_alloc_hook(s, gfpflags);
    if (unlikely(!s))
        return NULL;

    object = kfence_alloc(s, ac->orig_size, gfpflags);
    if (unlikely(object))
        goto out;

    node = apply_strict_numa_policy(node);

    object = alloc_from_pcs(s, gfpflags, ac->alloc_flags, node);

    if (unlikely(!object))
        object = ___slab_alloc(s, gfpflags, node, ac);

    maybe_wipe_obj_freeptr(s, object);

out:
    /*
     * In case this fails due to memcg_slab_post_alloc_hook(),
     * object is set to NULL
     */
    slab_post_alloc_hook(s, gfpflags, 1, &object, ac);

    return object;
}

当 alloc_from_pcs() 返回 NULL 时,才会进入下面的慢速路径:

    if (unlikely(!object))
        object = ___slab_alloc(s, gfpflags, node, ac);

slab_pre_alloc_hook 分配预检

预检只检查分配条件并返回缓存指针:

flowchart TD
A["slab_pre_alloc_hook()"] --> B["flags &= gfp_allowed_mask"]
    B --> C["might_alloc(flags)"]
    C --> D{"should_failslab() 触发失败?"}
    D -->|是| E["返回 NULL"]
    D -->|否| F["返回 s"]
/**
 * might_alloc - Mark possible allocation sites
 * @gfp_mask: gfp_t flags that would be used to allocate
 *
 * Similar to might_sleep() and other annotations, this can be used in functions
 * that might allocate, but often don't. Compiles to nothing without
 * CONFIG_LOCKDEP. Includes a conditional might_sleep() if @gfp allows blocking.
 */
static inline void might_alloc(gfp_t gfp_mask)
{
    fs_reclaim_acquire(gfp_mask);
    fs_reclaim_release(gfp_mask);

    if (current->flags & PF_MEMALLOC)
        return;

    might_sleep_if(gfpflags_allow_blocking(gfp_mask));
}

static __fastpath_inline
struct kmem_cache *slab_pre_alloc_hook(struct kmem_cache *s, gfp_t flags)
{
    flags &= gfp_allowed_mask;

    might_alloc(flags);

    if (unlikely(should_failslab(s, flags)))
        return NULL;

    return s;
}

在其中使用gfp_allowed_mask,会将当前不允许使用的分配标志去除。

might_alloc

might_alloc() 的检查顺序如下;相关调试功能未启用时,对应检查为空实现。

flowchart TD
A["might_alloc()"] --> B["fs_reclaim_acquire()
fs_reclaim_release()
标记回收锁依赖"] B --> C{"设置 PF_MEMALLOC?"} C -->|是| R["返回"] C -->|否| D{"GFP 允许阻塞?"} D -->|是| E["might_sleep_if()
检查当前上下文是否允许睡眠"] D -->|否| R E --> R

might_alloc() 用于检测在不允许睡眠的上下文中进行的不合法阻塞式内存分配

// gfp_allowed_mask注释翻译
在系统启动早期,gfp_allowed_mask 会被设置为 GFP_BOOT_MASK,以限制此时可以使用的 GFP 标志,因为中断尚未启用。
中断启用后,系统正常运行期间,它会被设置为 __GFP_BITS_MASK,允许使用完整的 GFP 标志集合。
在系统休眠期间,电源管理(PM)子系统也会使用该掩码:当设备已经被挂起时,它通过限制内存分配所使用的 GFP 标志,避免分配过程触发 I/O 操作。

might_alloc() 中先向 lockdep 标记一次回收锁的获取和释放:

    fs_reclaim_acquire(gfp_mask);
    fs_reclaim_release(gfp_mask);

只有当CONFIG_LOCKDEP锁调试配置开启时才会有内容。

在此处的获取和释放,是为了模拟后续的申请锁的过程是否会造成死锁,并进行检查。

而只有当CONFIG_DEBUG_ATOMIC_SLEEP配置开关开启后,might_sleep_if才会真正的起作用,否则为空实现。

而下面则会进行允许阻塞检查。

static inline bool gfpflags_allow_blocking(const gfp_t gfp_flags)
{
    return !!(gfp_flags & __GFP_DIRECT_RECLAIM);
}

阻塞检查通过检查__GFP_DIRECT_RECLAIM标志进行。

为什么会选择使用__GFP_DIRECT_RECLAIM标志进行阻塞标志检查,而不是单独的BLOCK标志?

在接口约定中,DIRECT_RECLAIM承担了内存回收,路径阻塞的约定,但是并没有专门的阻塞标志,在其他的文档当中,也会使用~DIRECT_RECLAIM来进行标明不进行内存回收,不进行阻塞。这里的使用DIRECT_RECLAIM更多的是接口约定。

而

    if (current->flags & PF_MEMALLOC)
        return;

跳过下面的睡眠检查。

current->flags 存放当前任务的标志,其中 PF_MEMALLOC 表示特殊的内存分配作用域。

按照 memalloc_noreclaim_save() 的注释,这个作用域会阻止分配过程进入内存回收,并允许使用内存保留区。这里跳过的是 might_alloc() 末尾的睡眠检查。

所以,如果不进行跳过,might_sleep_if的警告可能造成误导。

这是归档补丁的邮件:https://lkml.indiana.edu/hypermail/linux/kernel/2510.0/04687.html。


在预检中,还有可以注入的失败

if (unlikely(should_failslab(s, flags)))
    return NULL;

注入失败由CONFIG_FAILSLAB配置开关控制,这是进行测试时才会使用检测失败处理的函数。

这里也是唯一会影响传入的kmem_cache的地方,当失败时,返回NULL,让外部的kmem_cache赋值为NULL。

KFENCE

// 内核文档翻译
Kernel Electric-Fence(KFENCE)是一种基于采样技术的内存安全错误检测器,其运行成本较低。KFENCE 能够检测到堆内存越界访问、释放后使用错误以及无效释放等内存安全漏洞。

KFENCE 旨在能够在生产环境中被启用,其性能开销几乎为零。与 KASAN 相比,KFENCE 在性能与精度之间做出了权衡。设计 KFENCE 的主要目的是为了在总运行时间足够长的情况下,能够检测到那些非生产测试负载无法发现的代码中的错误。当该工具被部署在大量机器上时,就能快速实现足够长的总运行时间。

kfence分配由CONFIG_KFENCE开关进行控制,未配置时为空实现。

kfence会根据配置概率随机进行对象的分配,并进行保护,进行检查错误。

  • 越界读写;
  • 释放后使用(use-after-free);
  • 重复释放或非法释放;
  • 某些内存破坏问题。

在这里(开关开启时)

/**
 * kfence_alloc() - 以较低概率分配一个 KFENCE 对象
 * @s:     指定对象要求的 struct kmem_cache
 * @size:  要分配对象的确切大小(可以小于 @s->size,
 *         例如使用 kmalloc 缓存时)
 * @flags: GFP 标志
 *
 * 返回值:
 * * NULL     - 必须继续按常规方式分配内存,
 * * 非 NULL  - 指向 KFENCE 对象的指针。
 *
 * 应将 kfence_alloc() 插入堆内存分配的快速路径中,
 * 通过静态分支,以较低概率透明地返回由 KFENCE 分配的对象。
 * 该概率由启动参数 kfence.sample_interval 控制。
 */

获取 NUMA 节点编号

apply_strict_numa_policy() 只在配置和入口条件满足时应用策略:

flowchart TD
A["apply_strict_numa_policy(node)"] --> B{"启用 CONFIG_NUMA 和 strict_numa
且 node 为 NUMA_NO_NODE?"} B -->|否| R["返回原 node"] B -->|是| C{"当前任务存在 mempolicy?"} C -->|否| R C -->|是| D{"MPOL_BIND 且本地节点
在允许集合内?"} D -->|是| R D -->|否| E["node = mempolicy_slab_node()"] E --> F["返回选定 node"]
node = apply_strict_numa_policy(node);
非统一内存访问架构 Non-Uniform Memory Access(NUMA) 是一种为多处理器的电脑设计的内存架构,内存访问时间取决于内存相对于处理器的位置。在NUMA下,处理器访问它自己的本地内存的速度比非本地内存(内存位于另一个处理器,或者是处理器之间共享的内存)快一些。
非统一内存访问架构的特点是:被共享的内存物理上是分布式的,所有这些内存的集合就是全局地址空间。所以处理器访问这些内存的时间是不一样的,显然访问本地内存的速度要比访问全局共享内存或远程访问外地内存要快些。另外,NUMA中内存可能是分层的:本地内存,群内共享内存,全局共享内存。
—— 维基百科

因为NUMA为多处理器电脑设计,在正常个人使用笔记本或台式机,通常只有一个处理器插槽,所以内存一般作为一个NUMA节点呈现。

static __always_inline int apply_strict_numa_policy(int node)
{
#ifdef CONFIG_NUMA
    if (static_branch_unlikely(&strict_numa) &&
            node == NUMA_NO_NODE) {

        struct mempolicy *mpol = current->mempolicy;

        if (mpol) {
            /*
             * Special BIND rule support. If the local node
             * is in permitted set then do not redirect
             * to a particular node.
             * Otherwise we apply the memory policy to get
             * the node we need to allocate on.
             */
            if (mpol->mode != MPOL_BIND ||
                    !node_isset(numa_mem_id(), mpol->nodes))
                node = mempolicy_slab_node();
        }
    }
#endif
    return node;
}

启用 CONFIG_NUMA 后,还需要开启 strict_numa,且传入的 node 为 NUMA_NO_NODE,才会尝试应用当前任务的内存策略:

// mempolicy注释翻译
/*
 * 描述内存策略。
 *
 * mempolicy 可以关联到进程,也可以关联到 VMA(虚拟内存区域)。
 * 对于与 VMA 相关的内存分配,优先使用 VMA 的策略;
 * 其他情况下则使用进程的策略。中断处理会忽略当前进程的内存策略。
 */
struct mempolicy {
    atomic_t refcnt;
    unsigned short mode;    /* See MPOL_* above */
    unsigned short flags;   /* See set_mempolicy() MPOL_F_* above */
    nodemask_t nodes;   /* interleave/bind/preferred/etc */
    int home_node;      /* Home node to use for MPOL_BIND and MPOL_PREFERRED_MANY */

    union {
        nodemask_t cpuset_mems_allowed; /* relative to these nodes */
        nodemask_t user_nodemask;   /* nodemask passed by user */
    } w;
    struct rcu_head rcu;
};
mpol->mode       // 策略类型
mpol->nodes      // 策略指定的节点集合
numa_mem_id()    // 当前 CPU 对应的本地内存节点

if (策略是 MPOL_BIND && 本地内存节点在允许集合中)
    不修改 node;
else
    node = mempolicy_slab_node();

在上述条件下,如果策略为 MPOL_BIND 且本地节点已在允许集合内,就保留原来的 node;否则调用 mempolicy_slab_node() 选择节点。

/*
 * Depending on the memory policy provide a node from which to allocate the
 * next slab entry.
 */
unsigned int mempolicy_slab_node(void)
{
    struct mempolicy *policy;
    int node = numa_mem_id();

    if (!in_task())
        return node;

    policy = current->mempolicy;
    if (!policy)
        return node;

    switch (policy->mode) {
    case MPOL_PREFERRED:
        return first_node(policy->nodes);

    case MPOL_INTERLEAVE:
        return interleave_nodes(policy);

    case MPOL_WEIGHTED_INTERLEAVE:
        return weighted_interleave_nodes(policy);

    case MPOL_BIND:
    case MPOL_PREFERRED_MANY:
    {
        struct zoneref *z;

        /*
         * Follow bind policy behavior and start allocation at the
         * first node.
         */
        struct zonelist *zonelist;
        enum zone_type highest_zoneidx = gfp_zone(GFP_KERNEL);
        zonelist = &NODE_DATA(node)->node_zonelists[ZONELIST_FALLBACK];
        z = first_zones_zonelist(zonelist, highest_zoneidx,
                            &policy->nodes);
        return zonelist_zone(z) ? zonelist_node_idx(z) : node;
    }
    case MPOL_LOCAL:
        return node;

    default:
        BUG();
    }
}

在mempolicy_slab_node中

mempolicy_slab_node() 根据任务上下文和策略类型选择节点:

flowchart TD
A["mempolicy_slab_node()"] --> B{"处于任务上下文?"}
    B -->|否| R["返回本地节点"]
    B -->|是| C{"存在当前任务的内存策略?"}
    C -->|否| R
    C -->|是| D{"policy->mode"}
    D -->|MPOL_PREFERRED| E["返回首选节点"]
    D -->|MPOL_INTERLEAVE| F["返回轮转节点"]
    D -->|MPOL_WEIGHTED_INTERLEAVE| G["返回按权重轮转的节点"]
    D -->|MPOL_BIND / MPOL_PREFERRED_MANY| H["按本地回退顺序查找策略集合内的 zone
返回其节点,未找到则返回本地节点"] D -->|MPOL_LOCAL| R D -->|其他| I["BUG()"]

in_task() 判断当前是否处于任务上下文。若正在处理中断等非任务上下文,就直接返回本地节点,不使用当前任务的内存策略。

否则会根据当前的分配策略mpol->mode进行numa的节点分配,包括MPOL_PREFERRED:选择单个首选节点,MPOL_INTERLEAVE:轮流选择节点,MPOL_WEIGHTED_INTERLEAVE:按照权重分配选择次数,MPOL_BIND 和 MPOL_PREFERRED_MANY:按本地回退顺序,在策略集合里找节点,MPOL_LOCAL:直接选本地节点

其中 MPOL_BIND 和 MPOL_PREFERRED_MANY 共用一段处理逻辑。

首先在处理中,使用了zone

zone 是管理内存区域的结构体,而page是管理页的结构体,每个numa下都有不同的多个类型的zone。 为什么需要zone?所有的物理地址并不是对所有设备,内核用途都可用的,比如某些老设备只能在低地址的物理内存进行DMA。

每一个实际的 zone 都由一个 struct zone 描述

一个 zone 管理一段范围内的物理页:

struct zone
    ├── struct page
    ├── struct page
    ├── struct page
    ├── ...
    └── struct page

zone 通常记录它管理的 PFN 范围。PFN 是 Page Frame Number,即物理页帧号。

在numa的系统中,每个内存节点由pg_data_t(也叫 struct pglist_data)描述,

它包含这个节点的各类 zone,概念上类似:

struct pglist_data {
    struct zone node_zones[MAX_NR_ZONES];
    ...
};

层级关系为:

pg_data_t(NUMA 节点)
    └── node_zones[ZONE_DMA]
    └── node_zones[ZONE_DMA32]
    └── node_zones[ZONE_NORMAL]
    └── node_zones[ZONE_MOVABLE]

zone_idx 表示 zone 的类型编号。

由于有不同的配置,在不同的配置下各个类型的数值可能不同

假设有:

Node 3 的 ZONE_NORMAL

那么:

NUMA node id = 3
zone_idx     = ZONE_NORMAL

二者含义完全不同:

  • node id 表示内存属于哪个 NUMA 节点;
  • zone_idx 表示这是什么类型的内存区域。

zone_idx(zone) 通常根据 zone 在所属节点的 node_zones[] 数组中的位置,算出它的类型编号。

而zonelist会将numa上各种类型的zoneref放置在一起,如果首选 zone 没有可用页,内核可能尝试其他 zone,甚至其他 NUMA 节点。

struct zonelist {
    struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};

而zoneref是存储的某一类型的zone的指针,及它的类型,

/*
 * This struct contains information about a zone in a zonelist. It is stored
 * here to avoid dereferences into large structures and lookups of tables
 */
struct zoneref {
    struct zone *zone;  /* Pointer to actual zone */
    int zone_idx;       /* zone_idx(zoneref->zone) */
};

注释写的也很明确:

/*
 * 这个结构体保存 zonelist 中某个 zone 的信息。
 * 将这些信息直接保存在这里,是为了避免对大型结构体进行
 * 多次指针解引用,以及避免额外的查表操作。
 */

zoneref是zonelist中的一个轻量级的条目,结构体中一个指向真实的zone,另一个表明当前zone的类型。

最终mempolicy_slab_node()返回的是,zone所属的numa编号。

从 per-CPU sheaves 分配对象

alloc_from_pcs() 是对象分配的核心入口,下面标出了退出快速路径的分支:

flowchart TD
A["alloc_from_pcs()"] --> B{"指定节点与本地节点不匹配?"}
    B -->|是| N["返回 NULL"]
    B -->|否| C{"local_trylock() 成功?"}
    C -->|否| N
    C -->|是| D["pcs = this_cpu_ptr(cpu_sheaves)"]
    D --> E{"main->size 为 0?"}
    E -->|是| F["__pcs_replace_empty_main()"]
    F --> G{"返回 pcs?"}
    G -->|否,锁已释放| N
    G -->|是,仍持锁| H["取 main 数组末尾的对象"]
    E -->|否| H
    H --> I{"指定节点且对象节点不匹配?"}
    I -->|是| J["释放本地锁
记录 ALLOC_NODE_MISMATCH"] J --> N I -->|否| K["main->size--
释放本地锁
记录 ALLOC_FASTPATH"] K --> R["返回对象"]
object = alloc_from_pcs(s, gfpflags, ac->alloc_flags, node);
static __fastpath_inline
void *alloc_from_pcs(struct kmem_cache *s, gfp_t gfp, unsigned int alloc_flags, int node)
{
    struct slub_percpu_sheaves *pcs;
    bool node_requested;
    void *object;

    node_requested = IS_ENABLED(CONFIG_NUMA) && node != NUMA_NO_NODE;

    /*
     * We assume the percpu sheaves contain only local objects although it's
     * not completely guaranteed, so we verify later.
     */
    if (unlikely(node_requested && node != numa_mem_id())) {
        stat(s, ALLOC_NODE_MISMATCH);
        return NULL;
    }

    if (!local_trylock(&s->cpu_sheaves->lock))
        return NULL;

    pcs = this_cpu_ptr(s->cpu_sheaves);

    if (unlikely(pcs->main->size == 0)) {
        pcs = __pcs_replace_empty_main(s, pcs, gfp, alloc_flags);
        if (unlikely(!pcs))
            return NULL;
    }

    object = pcs->main->objects[pcs->main->size - 1];

    if (unlikely(node_requested)) {
        /*
         * Verify that the object was from the node we want. This could
         * be false because of cpu migration during an unlocked part of
         * the current allocation or previous freeing process.
         */
        if (page_to_nid(virt_to_page(object)) != node) {
            local_unlock(&s->cpu_sheaves->lock);
            stat(s, ALLOC_NODE_MISMATCH);
            return NULL;
        }
    }

    pcs->main->size--;

    local_unlock(&s->cpu_sheaves->lock);

    stat(s, ALLOC_FASTPATH);

    return object;
}
node_requested = IS_ENABLED(CONFIG_NUMA) && node != NUMA_NO_NODE;

if (unlikely(node_requested && node != numa_mem_id())) {
        stat(s, ALLOC_NODE_MISMATCH);
        return NULL;
}

启用 CONFIG_NUMA 且明确指定 node 时,如果请求节点与当前 CPU 的本地内存节点不同,快速路径返回 NULL,交给慢速路径处理。未指定节点时,不会因这一条件退出。

if (!local_trylock(&s->cpu_sheaves->lock))
        return NULL;

这里尝试获取当前 CPU 的本地锁,失败则返回 NULL。为什么不等待锁释放?因为这把锁保护的是当前 CPU 的 sheaves,可能遇到同一 CPU 上的重入。

普通的跨 CPU 锁竞争是:

CPU 0                         CPU 1
持有锁                        等待锁
修改数据                      自旋……
释放锁                        获得锁

而当前的本地CPU锁

CPU 0 → CPU 0 的 sheaves 和锁
CPU 1 → CPU 1 的 sheaves 和锁

考虑这样的执行顺序

CPU 0 上的普通代码
    │
    ├─ 获得当前 CPU 的 sheaves 锁
    │
    ├─ 正在访问 pcs
    │
    └─ 此时发生中断
           │
           └─ 中断处理程序也申请同一个 kmem_cache 的对象
                  │
                  └─ 进入 alloc_from_pcs()
                         │
                         └─ 尝试获取同一把本地锁

在上述重入场景中,如果等待同一把锁释放,就可能造成死锁。

即

中断处理程序:等原来的代码释放锁
原来的代码:等中断处理结束,才能继续执行并释放锁

同时这里获取锁,也是为了保护下面的所需要读取的pcs等数据。


获取到当前的s->cpu_sheaves后,判断当前cpu快速路径下的sheaves中main是否还有剩余的可分配对象可供使用,没有则进行替换main所指向的slab_sheaf的指针

pcs的定义struct slub_percpu_sheaves如下

struct slub_percpu_sheaves {
    local_trylock_t lock;
    struct slab_sheaf *main; /* never NULL when unlocked */
    struct slab_sheaf *spare; /* empty or full, may be NULL */
    struct slab_sheaf *rcu_free; /* for batching kfree_rcu() */
};

struct node_barn {
    spinlock_t lock;
    struct list_head sheaves_full;
    struct list_head sheaves_empty;
    unsigned int nr_full;
    unsigned int nr_empty;
};

struct slab_sheaf {
    union {
        struct rcu_head rcu_head;
        struct list_head barn_list;
        /* only used to defer call_rcu() in unknown context */
        struct llist_node llnode;
        /* only used for prefilled sheafs */
        struct {
            unsigned int capacity;
            bool pfmemalloc;
        };
    };
    struct kmem_cache *cache;
    unsigned int size;
    int node; /* only used for rcu_sheaf */
    void *objects[];
};

快速路径涉及 slub_percpu_sheaves、slab_sheaf 和 node_barn 三个结构体。可以把 sheaf 理解为一捆对象指针,把 barn 理解为存放这些捆的谷仓。

  • slub_percpu_sheaves(即 pcs)中的 main、spare 指向 slab_sheaf,分别用于当前分配和备用。
  • slab_sheaf 的 objects[] 存放对象指针,size 表示当前可用对象数。
  • node_barn 使用链表保存满 sheaf 和空 sheaf,供各 CPU 交换使用。
main 为空时的替换

__pcs_replace_empty_main() 的主线是本地交换、仓库交换和重填。重填后的安置分支见本节后面的独立流程图。

flowchart TD
A["入口:已持有本地锁"] --> B{"cache_has_sheaves()?"}
    B -->|否| U["释放本地锁,返回 NULL"]
    B -->|是| C{"spare 存在且有对象?"}
    C -->|是| D["交换 main / spare
持锁返回 pcs"] C -->|否| E{"get_barn() 成功?"} E -->|否| U E -->|是| F["计算 allow_spin
barn_replace_empty_sheaf()"] F --> G{"取得 full?"} G -->|是| H["main = full
持锁返回 pcs"] G -->|否| I{"allow_spin?"} I -->|否| U I -->|是| J["从 spare 或 barn 取得空 sheaf
释放本地锁,pcs = NULL"] J --> K{"已有空 sheaf?"} K -->|否| L["alloc_empty_sheaf()"] L --> M{"分配成功?"} M -->|否| N["返回 NULL,锁已释放"] M -->|是| O["refill_sheaf()"] K -->|是| O O --> P{"填充成功?"} P -->|否| Q["释放已填对象和容器"] Q --> N P -->|是| R["full = empty
重新尝试获取本地锁并安置 full"]
// slub.c 4714
/*
 * Replace the empty main sheaf with a (at least partially) full sheaf.
 *
 * Must be called with the cpu_sheaves local lock locked. If successful, returns
 * the pcs pointer and the local lock locked (possibly on a different cpu than
 * initially called). If not successful, returns NULL and the local lock
 * unlocked.
 */
static struct slub_percpu_sheaves *
__pcs_replace_empty_main(struct kmem_cache *s, struct slub_percpu_sheaves *pcs,
             gfp_t gfp, unsigned int alloc_flags)
{
    struct slab_sheaf *empty = NULL;
    struct slab_sheaf *full;
    struct node_barn *barn;
    bool allow_spin;

    slab_lockdep_assert_held(this_cpu_ptr(&s->cpu_sheaves->lock));

    /* Bootstrap or debug cache, back off */
    if (unlikely(!cache_has_sheaves(s))) {
        local_unlock(&s->cpu_sheaves->lock);
        return NULL;
    }

    if (pcs->spare && pcs->spare->size > 0) {
        swap(pcs->main, pcs->spare);
        return pcs;
    }

    barn = get_barn(s);
    if (!barn) {
        local_unlock(&s->cpu_sheaves->lock);
        return NULL;
    }

    allow_spin = alloc_flags_allow_spinning(alloc_flags);

    full = barn_replace_empty_sheaf(barn, pcs->main, allow_spin);

    if (full) {
        stat(s, BARN_GET);
        pcs->main = full;
        return pcs;
    }

    stat(s, BARN_GET_FAIL);

    if (allow_spin) {
        if (pcs->spare) {
            empty = pcs->spare;
            pcs->spare = NULL;
        } else {
            empty = barn_get_empty_sheaf(barn, true);
        }
    }

    local_unlock(&s->cpu_sheaves->lock);
    pcs = NULL;

    if (!allow_spin)
        return NULL;

    if (!empty) {
        empty = alloc_empty_sheaf(s, gfp, alloc_flags);
        if (!empty)
            return NULL;
    }

    if (refill_sheaf(s, empty, gfp | __GFP_NOMEMALLOC | __GFP_NOWARN)) {
        /*
         * we must be very low on memory so don't bother
         * with the barn
         */
        sheaf_flush_unused(s, empty);
        free_empty_sheaf(s, empty);

        return NULL;
    }

    full = empty;
    empty = NULL;

    if (!local_trylock(&s->cpu_sheaves->lock))
        goto barn_put;
    pcs = this_cpu_ptr(s->cpu_sheaves);

    /*
     * If we put any empty or full sheaf to the barn below, it's due to
     * racing or being migrated to a different cpu. Breaching the barn's
     * sheaf limits should be thus rare enough so just ignore them to
     * simplify the recovery.
     */

    if (pcs->main->size == 0) {
        if (!pcs->spare)
            pcs->spare = pcs->main;
        else
            barn_put_empty_sheaf(barn, pcs->main);
        pcs->main = full;
        return pcs;
    }

    if (!pcs->spare) {
        pcs->spare = full;
        return pcs;
    }

    if (pcs->spare->size == 0) {
        barn_put_empty_sheaf(barn, pcs->spare);
        pcs->spare = full;
        return pcs;
    }

barn_put:
    barn_put_full_sheaf(barn, full);
    stat(s, BARN_PUT);

    return pcs;
}

快速路径力求不经过完整的分配算法实现快速的分配。

当 pcs->main->size > 0 时,可以直接从 main->objects[] 取出对象。

当 main->size == 0 时,进入 __pcs_replace_empty_main() 替换当前 sheaf。这里的“空”指没有可用对象,而非 main 指针为 NULL。

首先检查 spare 是否存在且仍有可用对象(spare->size > 0);如果有,直接交换 main 和 spare。

否则,就是当前的pcs中,已经没有对象进行分配,也就是无sheaf可用,此时就会从barn即谷仓中,获取新的可用的sheaf。

barn_replace_empty_sheaf() 尝试将当前空的 main 放入 barn,并取出一个满 sheaf。交换不会改变 sheaf 总数;成功时返回满 sheaf,由调用者设置为新的 pcs->main。

barn_replace_empty_sheaf() 在持有 barn 锁后完成交换:

flowchart TD
A["barn_replace_empty_sheaf()"] --> B{"无锁读取 nr_full 非零?"}
    B -->|否| N["返回 NULL"]
    B -->|是| C{"allow_spin?"}
    C -->|是| D["spin_lock_irqsave()"]
    C -->|否| E{"spin_trylock_irqsave() 成功?"}
    E -->|否| N
    E -->|是| F{"持锁后 nr_full 非零?"}
    D --> F
    F -->|是| G["取出满 sheaf,加入空 sheaf
nr_full--,nr_empty++"] F -->|否| H["full 保持 NULL"] G --> I["释放 barn 锁"] H --> I I --> R["返回 full"]
// slub.c 3229
/*
 * If a full sheaf is available, return it and put the supplied empty one to
 * barn. We ignore the limit on empty sheaves as the number of sheaves doesn't
 * change.
 */
static struct slab_sheaf *
barn_replace_empty_sheaf(struct node_barn *barn, struct slab_sheaf *empty,
             bool allow_spin)
{
    struct slab_sheaf *full = NULL;
    unsigned long flags;

    if (!data_race(barn->nr_full))
        return NULL;

    if (likely(allow_spin))
        spin_lock_irqsave(&barn->lock, flags);
    else if (!spin_trylock_irqsave(&barn->lock, flags))
        return NULL;

    if (likely(barn->nr_full)) {
        full = list_first_entry(&barn->sheaves_full, struct slab_sheaf,
                    barn_list);
        list_del(&full->barn_list);
        list_add(&empty->barn_list, &barn->sheaves_empty);
        barn->nr_full--;
        barn->nr_empty++;
    }

    spin_unlock_irqrestore(&barn->lock, flags);

    return full;
}

首先通过 data_race(barn->nr_full) 无锁读取满 sheaf 的计数,做一次提前检查。nr_full 是计数而非链表;data_race() 标记这次允许竞争的读取,真正的链表操作仍在持锁后进行。

随后根据 allow_spin 选择等待获取锁或尝试获取锁;尝试获取失败时返回 NULL。

持锁后再次确认存在满 sheaf,将其从 sheaves_full 取出,并把传入的空 sheaf(原来的 pcs->main)加入 sheaves_empty,同时更新两个计数,最后返回取出的满 sheaf。

分配空 sheaf 并补充对象

alloc_empty_sheaf() 与 __alloc_empty_sheaf() 负责申请容器,对象由后续重填流程提供:

flowchart TD
A["alloc_empty_sheaf()"] --> B{"设置 SLAB_ALLOC_NO_RECURSE?"}
    B -->|是| N["返回 NULL"]
    B -->|否| C["清除 OBJCGS_CLEAR_MASK
进入 __alloc_empty_sheaf()"] C --> D{"缓存设置 SLAB_KMALLOC?"} D -->|是| E["追加 SLAB_ALLOC_NO_RECURSE"] D -->|否| F["计算容器大小
kmalloc_flags() 清零分配"] E --> F F --> G{"取得容器?"} G -->|否| N G -->|是| H["设置 cache,记录 SHEAF_ALLOC
返回空 sheaf"]

如果获得 full,设置 pcs->main 后直接返回;否则继续尝试补充对象。交换失败也可能由尝试获取 barn 锁失败引起。

当 allow_spin 为真时,先从本地 spare 或 barn 获取空 sheaf,再释放本地锁;若仍未取得空 sheaf,则调用 alloc_empty_sheaf() 分配容器,并通过 refill_sheaf() 填充对象。这里的“允许自旋”由 alloc_flags 决定,与 GFP 标志是否允许睡眠是不同的判断。

static struct slab_sheaf *__alloc_empty_sheaf(struct kmem_cache *s, gfp_t gfp,
                unsigned int alloc_flags, unsigned int capacity)
{
    struct slab_sheaf *sheaf;
    size_t sheaf_size;

    /*
     * Prevent recursion to the same cache, or a deep stack of kmallocs of
     * varying sizes (sheaf capacity might differ for each kmalloc size
     * bucket)
     */
    if (s->flags & SLAB_KMALLOC)
        alloc_flags |= SLAB_ALLOC_NO_RECURSE;

    sheaf_size = struct_size(sheaf, objects, capacity);
    sheaf = kmalloc_flags(sheaf_size, gfp | __GFP_ZERO, alloc_flags, NUMA_NO_NODE);

    if (unlikely(!sheaf))
        return NULL;

    sheaf->cache = s;

    stat(s, SHEAF_ALLOC);

    return sheaf;
}

static inline struct slab_sheaf *alloc_empty_sheaf(struct kmem_cache *s,
                gfp_t gfp, unsigned int alloc_flags)
{
    if (alloc_flags & SLAB_ALLOC_NO_RECURSE)
        return NULL;

    gfp &= ~OBJCGS_CLEAR_MASK;

    return __alloc_empty_sheaf(s, gfp, alloc_flags, s->sheaf_capacity);
}

/*
 * The only version of kmalloc_node() that takes alloc_flags and thus can
 * determine on its own whether to handle the allocation via kmalloc_nolock() or
 * normally
 */
void *__kmalloc_flags_noprof(DECL_TOKEN_PARAMS(size, token), gfp_t flags,
                 unsigned int alloc_flags, int node)
{
    const struct slab_alloc_context ac = {
        .caller_addr = _RET_IP_,
        .orig_size = size,
        .alloc_flags = alloc_flags,
    };

    if (alloc_flags_allow_spinning(alloc_flags)) {
        return __do_kmalloc_node(NULL, flags, node,
                PASS_TOKEN_PARAM(token), &ac);
    } else {
        return __kmalloc_nolock_noprof(PASS_TOKEN_PARAMS(size, token),
                           flags, node, &ac);
    }
}

如果释放锁前未取得 empty,则调用 alloc_empty_sheaf(),最终通过 __kmalloc_flags_noprof() 分配空 sheaf 容器。

在这里需要注意的是,alloc_empty_sheaf会重入slub分配器,这样的话就会导致深度的递归分配的操作,比如申请sheaf时又申请sheaf。

为了处理这个问题,在alloc_empty_sheaf中通过

// alloc_empty_sheaf
if (alloc_flags & SLAB_ALLOC_NO_RECURSE)
    return NULL;
// __alloc_empty_sheaf
/*
 * Prevent recursion to the same cache, or a deep stack of kmallocs of
 * varying sizes (sheaf capacity might differ for each kmalloc size
 * bucket)
 */
if (s->flags & SLAB_KMALLOC)
    alloc_flags |= SLAB_ALLOC_NO_RECURSE;

通过 SLAB_ALLOC_NO_RECURSE 限制递归申请 sheaf。

refill_sheaf() 负责更新容器中的对象数,底层 refill_objects() 负责取得对象:

flowchart TD
A["refill_sheaf()"] --> B["to_fill = capacity - size"]
    B --> C{"to_fill 为 0?"}
    C -->|是| R["返回 0"]
    C -->|否| D["refill_objects()
min = max = to_fill"] D --> E["增加 sheaf->size
记录 SHEAF_REFILL"] E --> F{"filled 小于 to_fill?"} F -->|是| N["返回 -ENOMEM"] F -->|否| R
// slub.c 7396
static unsigned int
refill_objects(struct kmem_cache *s, void **p, gfp_t gfp, unsigned int min,
           unsigned int max)
{
    int local_node = numa_mem_id();
    unsigned int refilled;
    struct slab *slab;

    refilled = __refill_objects_node(s, p, gfp, min, max,
                     get_node(s, local_node),
                     /* allow_spin = */ true);
    if (refilled >= min)
        return refilled;

    refilled += __refill_objects_any(s, p + refilled, gfp, min - refilled,
                     max - refilled);
    if (refilled >= min)
        return refilled;

new_slab:

    slab = new_slab(s, gfp, SLAB_ALLOC_DEFAULT, local_node);
    if (!slab)
        goto out;

    stat(s, ALLOC_SLAB);

    refilled += alloc_from_new_slab(s, slab, p + refilled, max - refilled,
                    /* allow_spin = */ true);

    if (refilled < min)
        goto new_slab;

out:
    return refilled;
}

// slub.c 2876
static int refill_sheaf(struct kmem_cache *s, struct slab_sheaf *sheaf,
             gfp_t gfp)
{
    int to_fill = s->sheaf_capacity - sheaf->size;
    int filled;

    if (!to_fill)
        return 0;

    filled = refill_objects(s, &sheaf->objects[sheaf->size], gfp, to_fill,
                to_fill);

    sheaf->size += filled;

    stat_add(s, SHEAF_REFILL, filled);

    if (filled < to_fill)
        return -ENOMEM;

    return 0;
}

refill_objects() 先使用已有 slab,不足时循环申请新 slab:

flowchart TD
A["refill_objects()"] --> B["__refill_objects_node()
从本地节点取对象"] B --> C{"已达到 min?"} C -->|是| R["返回已取得对象数"] C -->|否| D["__refill_objects_any()
继续从其他可用 slab 取对象"] D --> E{"已达到 min?"} E -->|是| R E -->|否| F["new_slab()"] F --> G{"取得新 slab?"} G -->|否| R G -->|是| H["alloc_from_new_slab()
增加已取得对象数"] H --> I{"已达到 min?"} I -->|是| R I -->|否| F

refill_sheaf() 填充成功时返回 0,未填满时返回 -ENOMEM。失败后由调用者释放已填入的对象和 sheaf 容器,再返回 NULL。

重填后的 sheaf 安置

这是 __pcs_replace_empty_main() 重填成功后的后半段,注意返回时本地锁的状态:

flowchart TD
A["重填成功,full 已就绪"] --> B{"重新获取本地锁成功?"}
    B -->|否| P["full 放回 barn
返回 NULL,未持锁"] B -->|是| C["重新取得当前 CPU 的 pcs"] C --> D{"main->size 为 0?"} D -->|是| E{"spare 存在?"} E -->|否| F["spare = 原 main"] E -->|是| G["原 main 放回 barn"] F --> H["main = full"] G --> H H --> R["返回 pcs,保持本地锁"] D -->|否| I{"spare 存在?"} I -->|否| J["spare = full"] I -->|是| K{"spare->size 为 0?"} K -->|是| L["空 spare 放回 barn
spare = full"] K -->|否| M["full 放回 barn"] J --> R L --> R M --> R

重填成功后,重新尝试获取当前 CPU 的本地锁,并重新取得 pcs。根据此时的状态安置 full:

  • main->size == 0:将原来的空 main 留作 spare,或放回 barn,再将 full 设为新的 main。
  • main 已有可用对象,且 spare 不存在:将 full 设为 spare。
  • main 已有可用对象,且 spare->size == 0:将空 spare 放回 barn,再将 full 设为 spare。
  • main 和 spare 都有可用对象,或重新获取本地锁失败:将 full 放回 barn。

这里可能有疑问。分配的原因就是因为main和spare没有可分配的对象才进行的,为什么到这里还要再去判断他们到底有没有。

因为补充对象之前,代码执行了:

local_unlock(&s->cpu_sheaves->lock);
pcs = NULL;

释放本地锁后,可能发生抢占或 CPU 迁移,也可能有其他执行上下文改变 sheaves 的状态。因此重新获取锁后,要再次通过 this_cpu_ptr() 获取 pcs 并检查 main、spare。

替换成功后,回到 alloc_from_pcs(),从 main->objects[main->size - 1] 取出对象。若指定了 NUMA 节点,还会验证对象实际所属的节点;验证通过后递减 size,释放本地锁,记录 ALLOC_FASTPATH 并返回对象。

如果 alloc_from_pcs() 返回 NULL,则由 slab_alloc_node() 调用 ___slab_alloc() 进入慢速路径。随后执行 maybe_wipe_obj_freeptr() 和 slab_post_alloc_hook(),完成分配后的处理。