关于后端:从内核源码看-slab-内存池的创建初始化流程

42次阅读

共计 44755 个字符,预计需要花费 112 分钟才能阅读完成。

在上篇文章 [《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现
》](https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&mid=2247487…) 中,笔者从 slab cache 的总体架构演进角度以及 slab cache 的运行原理角度为大家勾画出了 slab cache 的总体架构视图,基于这个视图具体论述了 slab cache 的内存调配以及开释原理。

slab cache 机制的确比较复杂,波及到的场景又很多,大家读到这里,我想必定会好奇或者狐疑笔者在上篇文章中所阐述的那些原理的正确性,毕竟 talk is cheap,所以为了让大家看着安心,了解起来释怀,从本文开始,咱们将正式进入 show you the code 的阶段。笔者会基于内核 5.4 版本,具体为大家分析 slab cache 在内核中的源码实现。

在上篇文章《5. 从一个简略的内存页开始聊 slab》和《6. slab 的总体架构设计》大节中,笔者带大家从一个最简略的物理内存页开始,一步一步演进 slab cache 的架构,最终失去了一副 slab cache 残缺的架构图:

在本文的内容中,笔者会带大家到内核源码实现中,来看一下 slab cache 在内核中是如何被一步一步创立进去的,以及内核是如何安顿 slab 对象在内存中的布局的。

咱们先以内核创立 slab cache 的接口函数 kmem_cache_create 为终点,来一步一步揭秘 slab cache 的创立过程。

struct kmem_cache *
kmem_cache_create(const char *name, unsigned int size, unsigned int align,
        slab_flags_t flags, void (*ctor)(void *))
{
    return kmem_cache_create_usercopy(name, size, align, flags, 0, 0,
                      ctor);
}

kmem_cache_create 接口中的参数,是由用户指定的对于 slab cache 的一些外围属性,这些属性值与咱们在前文《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现》的《6.1 slab 的根底信息管理》大节中介绍 struct kmem_cache 构造的相应属性一一对应,在创立 slab cache 的过程中,内核会将 kmem_cache_create 接口中参数指定的值一一赋值到 struct kmem_cache 构造中。

struct kmem_cache {
    // slab cache 的名称,也就是在 slabinfo 命令中 name 那一列
    const char *name;  
    // 对应参数 size,指 slab 中对象的理论大小,不蕴含填充的字节数
    unsigned int object_size;/* The size of an object without metadata */
    // 对象依照指定的 align 进行对齐
    unsigned int align; 
    // slab cache 的治理标记位,用于设置 slab 的一些个性
    slab_flags_t flags;
    // 池化对象的构造函数,用于创立 slab 对象池中的对象
    void (*ctor)(void *);
}

slab cache 的整个创立过程其实是封装在 kmem_cache_create_usercopy 函数中,kmem_cache_create 间接调用了该函数,并将创立参数透传过来。

struct kmem_cache *
kmem_cache_create_usercopy(const char *name,
          unsigned int size, unsigned int align,
          slab_flags_t flags,
          unsigned int useroffset, unsigned int usersize,
          void (*ctor)(void *))

内核提供 kmem_cache_create_usercopy 函数的目标其实是为了避免 slab cache 中治理的内核外围对象被泄露,通过 useroffset 和 usersize 两个变量来指定内核对象内存布局区域中 useroffset 到 usersize 的这段内存区域能够被复制到用户空间中,其余区域则不能够。

在 Linux 内核初始化的过程中会提前为内核外围对象创立好对应的 slab cache,比方:在内核初始化函数 start_kernel 中调用 fork_init 函数为 struct task_struct 创立其所属的 slab cache —— task_struct_cachep。

在 fork_init 中就调用了 kmem_cache_create_usercopy 函数来创立 task_struct_cachep,同时指定 task_struct 对象中 useroffset 到 usersize 这段内存区域能够被复制到用户空间。例如:通过 ptrace 零碎调用拜访过程的 task_struct 构造时,只能拜访 task_struct 对象 useroffset 到 usersize 的这段区域。

void __init fork_init(void)
{
    ......... 省略 ..........
    unsigned long useroffset, usersize;

    /* create a slab on which task_structs can be allocated */
    task_struct_whitelist(&useroffset, &usersize);
    task_struct_cachep = kmem_cache_create_usercopy("task_struct",
            arch_task_struct_size, align,
            SLAB_PANIC|SLAB_ACCOUNT,
            useroffset, usersize, NULL);
            
    ......... 省略 ..........
}
struct kmem_cache *
kmem_cache_create_usercopy(const char *name,
          unsigned int size, unsigned int align,
          slab_flags_t flags,
          unsigned int useroffset, unsigned int usersize,
          void (*ctor)(void *))
{
    struct kmem_cache *s = NULL;
    const char *cache_name;
    int err;

    // 获取 cpu_hotplug_lock,避免 cpu 热插拔扭转 online cpu map
    get_online_cpus();
    // 获取 mem_hotplug_lock,避免拜访内存的时候进行内存热插拔
    get_online_mems();
    // memory cgroup 相干,获取 memcg_cache_ids_sem 读写信号量
    // 避免 memcg_nr_cache_ids(caches array 大小)被批改
    memcg_get_cache_ids();
    // 获取 slab cache 链表的全局互斥锁
    mutex_lock(&slab_mutex);

    // 入参查看,校验 name 和 size 的有效性,避免创立过程在中断上下文中进行
    err = kmem_cache_sanity_check(name, size);
    if (err) {goto out_unlock;}

    // 查看无效的 slab flags 标记位,如果传入的 flag 是有效的,则回绝本次创立申请
    if (flags & ~SLAB_FLAGS_PERMITTED) {
        err = -EINVAL;
        goto out_unlock;
    }

    // 设置创立 slab  cache 时用到的一些标记位
    flags &= CACHE_CREATE_MASK;

    // 校验 useroffset 和 usersize 的有效性
    if (WARN_ON(!usersize && useroffset) ||
        WARN_ON(size < usersize || size - usersize < useroffset))
        usersize = useroffset = 0;

    if (!usersize)
        // 在全局 slab cache 链表中查找与以后创立参数相匹配的 kmem_cache
        // 如果有,就不须要创立新的了,间接和已有的  slab cache  合并
        // 并且在 sys 文件系统中应用指定的 name 作为已有  slab cache  的别名
        s = __kmem_cache_alias(name, size, align, flags, ctor);
    if (s)
        goto out_unlock;
    // 在内核中为指定的 name 生成字符串常量并分配内存
    // 这里的 cache_name 就是将要创立的 slab cache 名称,用于在 /proc/slabinfo 中显示
    cache_name = kstrdup_const(name, GFP_KERNEL);
    if (!cache_name) {
        err = -ENOMEM;
        goto out_unlock;
    }
    // 依照咱们指定的参数,创立新的 slab cache
    s = create_cache(cache_name, size,
             calculate_alignment(flags, align, size),
             flags, useroffset, usersize, ctor, NULL, NULL);
    if (IS_ERR(s)) {err = PTR_ERR(s);
        kfree_const(cache_name);
    }

out_unlock:
    // 走到这里示意创立 slab cache 失败,开释相干的自旋锁和信号量
    mutex_unlock(&slab_mutex);
    memcg_put_cache_ids();
    put_online_mems();
    put_online_cpus();

    if (err) {if (flags & SLAB_PANIC)
            panic("kmem_cache_create: Failed to create slab'%s'. Error %d\n",
                name, err);
        else {pr_warn("kmem_cache_create(%s) failed with error %d\n",
                name, err);
            dump_stack();}
        return NULL;
    }
    return s;
}

在创立 slab cache 的开始,内核为了保障整个创立过程是并发平安的,所以须要先获取一系列的锁,比方:

  • 获取 cpu_hotplug_lock,mem_hotplug_lock 来避免在创立 slab cache 的过程中 cpu 或者内存进行热插拔。
  • 避免 memory group 相干的 caches array 被批改,cgroup 相干的不是本文重点,这里简略理解一下即可。
  • 内核中应用一个全局的双向链表来串联起零碎中所有的 slab cache,这里须要获取全局链表 list 的锁,避免并发对 list 进行批改。

在确保 slab cache 的整个创立过程并发平安之后,内核会首先校验 kmem_cache_create 接口函数传递进来的那些创立参数的非法有效性。

比方,kmem_cache_sanity_check 函数中会确保 slab cache 的创立过程不能在中断上下文中进行,如果过程所处的上下文为中断上下文,那么内核就会返回 -EINVAL谬误进行 slab cache 的创立。因为中断处理程序是不会被内核从新调度的,这就导致处于中断上下文的操作必须是原子的,不能睡眠,不能阻塞,更不能持有锁等同步资源。而 slab cache 的创立并不是原子的,内核须要确保整个创立过程不能在中断上下文中进行。

除此之外 kmem_cache_sanity_check 函数还须要校验用户传入的 name 和 对象大小 object size 的有效性,确保 object size 在无效范畴:8 字节到 4M 之间

#define MAX_ORDER       11
#define PAGE_SHIFT      12

// 定义在 /include/linux/slab.h 文件
#ifdef CONFIG_SLUB
#define KMALLOC_SHIFT_MAX   (MAX_ORDER + PAGE_SHIFT - 1)
/* Maximum allocatable size */
#define KMALLOC_MAX_SIZE    (1UL << KMALLOC_SHIFT_MAX)

static int kmem_cache_sanity_check(const char *name, unsigned int size)
{   
    // 1: 传入 slab cache 的名称不能为空
    // 2: 创立 slab cache 的过程不能处在中断上下文中
    // 3: 传入的对象大小 size 须要在 8 字节到 KMALLOC_MAX_SIZE = 4M 之间
    if (!name || in_interrupt() || size < sizeof(void *) ||
        size > KMALLOC_MAX_SIZE) {pr_err("kmem_cache_create(%s) integrity check failed\n", name);
        return -EINVAL;
    }

    WARN_ON(strchr(name, ' ')); /* It confuses parsers */
    return 0;
}

最初内核会校验传入的 slab cache 治理标记位 slab_flags_t 的合法性,确保 slab_flags_t 在内核规定的无效标记汇合中:

/* Common flags permitted for kmem_cache_create */
#define SLAB_FLAGS_PERMITTED (SLAB_CORE_FLAGS | \
                  SLAB_RED_ZONE | \
                  SLAB_POISON | \
                  SLAB_STORE_USER | \
                  SLAB_TRACE | \
                  SLAB_CONSISTENCY_CHECKS | \
                  SLAB_MEM_SPREAD | \
                  SLAB_NOLEAKTRACE | \
                  SLAB_RECLAIM_ACCOUNT | \
                  SLAB_TEMPORARY | \
                  SLAB_ACCOUNT)

随后 flags &= CACHE_CREATE_MASK 初始化 slab_flags_t 标记位:

/* Common flags available with current configuration */
#define CACHE_CREATE_MASK (SLAB_CORE_FLAGS | SLAB_DEBUG_FLAGS | SLAB_CACHE_FLAGS)

在校验完各项创立参数的有效性之后,依照常理来说就应该进入 slab cache 的创立流程了,然而当初还没到创立的时候,内核的理念是尽最大可能复用零碎中已有的 slab cache。

在 __kmem_cache_alias 函数中,内核会遍历零碎中 slab cache 的全局链表 list,试图在零碎现有 slab cache 中查找到一个各项外围参数与咱们指定的创立参数贴近的 slab cache。比方,零碎中存在一个 slab cache 它的各项外围参数,object size,align,slab_flags_t 和咱们指定的创立参数十分贴近。

这样一来内核就不须要反复创立新的 slab cache 了,间接复用原有的 slab cache 即可,将咱们指定的 name 作为原有 slab cache 的别名。

如果找不到这样一个能够被复用的 slab cache,那么内核就会调用 create_cache 开始创立 slab cache 流程。

以上是 slab cache 创立的总体框架流程,接下来,咱们来具体看下创立流程中波及到的几个外围函数。

1. __kmem_cache_alias

__kmem_cache_alias 函数的外围是在 find_mergeable 办法中,内核在 find_mergeable 办法里边会遍历 slab cache 的全局链表 list,查找与以后创立参数贴近能够被复用的 slab cache。

一个能够被复用的 slab cache 须要满足以下四个条件:

  1. 指定的 slab_flags_t 雷同。
  2. 指定对象的 object size 要小于等于已有 slab cache 中的对象 size(kmem_cache->size)。
  3. 如果指定对象的 object size 与已有 kmem_cache->size 不雷同,那么它们之间的差值须要再一个 word size 之内。
  4. 已有 slab cache 中的 slab 对象对齐 align(kmem_cache->align)要大于等于指定的 align 并且能够整除 align。
struct kmem_cache *
__kmem_cache_alias(const char *name, unsigned int size, unsigned int align,
           slab_flags_t flags, void (*ctor)(void *))
{
    struct kmem_cache *s, *c;
    // 在全局 slab cache 链表中查找与以后创立参数相匹配的 slab cache
    // 如果在全局查找到一个  slab cache,它的外围参数和咱们指定的创立参数很贴近
    // 那么就没必要再创立新的 slab cache 了,复用已有的 slab cache
    s = find_mergeable(size, align, flags, name, ctor);
    if (s) {
        // 如果存在可复用的 kmem_cache,则将它的援用计数 + 1
        s->refcount++;
        // 采纳较大的值,更新已有的 kmem_cache 相干的元数据
        s->object_size = max(s->object_size, size);
        s->inuse = max(s->inuse, ALIGN(size, sizeof(void *)));
        // 遍历 mem cgroup 中的 cache array,更新对应的元数据
        // cgroup 相干,这里简略理解也可间接疏忽
        for_each_memcg_cache(c, s) {
            c->object_size = s->object_size;
            c->inuse = max(c->inuse, ALIGN(size, sizeof(void *)));
        }
        // 因为这里咱们会复用已有的 kmem_cache 并不会创立新的,而且咱们指定的 kmem_cache 名称是 name。// 为了看起来像是创立了一个名称为 name 的新 kmem_cache,所以要给被复用的 kmem_cache 起一个别名,这个别名就是咱们指定的 name
        // 在 sys 文件系统中应用咱们指定的 name 为被复用 kmem_cache 创立别名
        // 这样一来就会在 sys 文件系统中呈现一个这样的目录 /sys/kernel/slab/name,该目录下的文件蕴含了对应 slab cache 运行时的详细信息
        if (sysfs_slab_alias(s, name)) {
            s->refcount--;
            s = NULL;
        }
    }

    return s;
}

如果通过 find_mergeable 在现有零碎中所有 slab cache 中找到了一个能够复用的 slab cache,那么就不须要在创立新的了,间接返回已有的 slab cache 就能够了。

然而在返回之前,须要更新一下已有 slab cache 构造 kmem_cache 中的相干信息:

struct kmem_cache {
    // slab cache 的援用计数,为 0 时就能够销毁并开释内存回搭档零碎重
    int refcount;   
    // slab 中对象的理论大小,不蕴含填充的字节数
    unsigned int object_size;/* The size of an object without metadata */
    // 对象的 object_size 依照 word 字长对齐之后的大小
    unsigned int inuse;  
}
  • 减少原有 slab cache 的援用计数 refcount++。
  • slab cache 中的 object size 更新为咱们在创立参数中指定的 object size 与原有 object size 之间的最大值。
  • slab cache 中的 inuse 也是更新为原有 kmem_cache->inuse 与咱们指定的对象 object size 与 word size 对齐之后的最大值。

最初调用 sysfs_slab_alias 在 sys 文件系统中创立一个这样的目录 /sys/kernel/slab/name,name 就是 kmem_cache_create 接口函数传递过去的参数,示意要创立的 slab cache 名称。

零碎中的所有 slab cache 都会在 sys 文件系统中有一个专门的目录:/sys/kernel/slab/<cachename>,该目录下的所有文件都是 read only 的,每一个文件代表 slab cache 的一项运行时信息,比方:

  • /sys/kernel/slab/<cachename>/align 文件标识该 slab cache 中的 slab 对象的对齐 align
  • /sys/kernel/slab/<cachename>/alloc_fastpath 文件记录该 slab cache 在疾速门路下调配的对象个数
  • /sys/kernel/slab/<cachename>/alloc_from_partial 文件记录该 slab cache 从本地 cpu 缓存 partial 链表中调配的对象次数
  • /sys/kernel/slab/<cachename>/alloc_slab 文件记录该 slab cache 从搭档零碎中申请新 slab 的次数
  • /sys/kernel/slab/<cachename>/cpu_slabs 文件记录该 slab cache 的本地 cpu 缓存中缓存的 slab 个数
  • /sys/kernel/slab/<cachename>/partial 文件记录该 slab cache 在每个 NUMA 节点缓存 partial 链表中的 slab 个数
  • /sys/kernel/slab/<cachename>/objs_per_slab 文件记录该 slab cache 中治理的 slab 能够包容多少个对象。

该目录下还有很多文件笔者就不一一列举了,然而咱们能够看到 /sys/kernel/slab/<cachename> 目录下的文件形容了对应 slab cache 十分具体的运行信息。前边咱们介绍的 cat /proc/slabinfo 命名输入的信息就来源于 /sys/kernel/slab/<cachename> 目录下的各个文件。

因为咱们以后并没有真正创立一个新的 slab cache,而是复用零碎中已有的 slab cache,然而内核须要让用户感觉上曾经依照咱们指定的创立参数创立了一个新的 slab cache,所以须要为咱们要创立的 slab cache 也独自在 sys 文件系统中创立一个 /sys/kernel/slab/name 目录,然而该目录下的文件须要 软链接 到原有 slab cache 在 sys 文件系统对应目录下的文件。

这就相当于给原有 slab cache 起了一个别名,这个别名就是咱们指定的 name,然而 /sys/kernel/slab/name 目录下的文件还是用的原有 slab cache 的。

咱们能够通过 /sys/kernel/slab/<cachename>/aliases 文件查看该 slab cache 的所有别名个数,也就是说有多少个 slab cache 复用了该 slab cache。

1.1 find_mergeable 查找可被复用的 slab cache

struct kmem_cache *find_mergeable(unsigned int size, unsigned int align,
        slab_flags_t flags, const char *name, void (*ctor)(void *))
{
    struct kmem_cache *s;
    // 与 word size 进行对齐
    size = ALIGN(size, sizeof(void *));
    // 依据咱们指定的对齐参数 align 并联合 CPU cache line 大小,计算出一个适合的对齐参数
    align = calculate_alignment(flags, align, size);
    // 对象 size 从新依照 align 进行对齐
    size = ALIGN(size, align);

    // 如果 flag 设置的是不容许合并,则进行
    if (flags & SLAB_NEVER_MERGE)
        return NULL;

    // 开始遍历内核中已有的 slab cache,寻找能够合并的 slab cache
    list_for_each_entry_reverse(s, &slab_root_caches, root_caches_node) {if (slab_unmergeable(s))
            continue;
        // 指定对象 size 不能超过已有 slab cache 中的对象 size
        if (size > s->size)
            continue;
        // 校验指定的 flag 是否与已有 slab cache 中的 flag 统一
        if ((flags & SLAB_MERGE_SAME) != (s->flags & SLAB_MERGE_SAME))
            continue;
        // 两者的 size 相差在一个 word size 之内 
        if (s->size - size >= sizeof(void *))
            continue;
        // 已有 slab cache 中对象的对齐 align 要大于等于指定的 align 并且能够整除 align。if (IS_ENABLED(CONFIG_SLAB) && align &&
            (align > s->align || s->align % align))
            continue;
        // 查找到能够合并的已有 slab cache,不须要再创立新的 slab cache 了
        return s;
    }
    return NULL;
}

一个能够被复用的 slab cache 须要满足以下四个条件:

  1. 指定的 slab_flags_t 雷同。
  2. 指定对象的 object size 要小于等于已有 slab cache 中的对象 size(kmem_cache->size)。
  3. 如果指定对象的 object size 与已有 kmem_cache->size 不雷同,那么它们之间的差值须要再一个 word size 之内。
  4. 已有 slab cache 中的 slab 对象对齐 align(kmem_cache->align)要大于等于指定的 align 并且能够整除 align。

1.2 calculate_alignment 综合计算出一个正当的对齐 align

事实上,内核并不会齐全依照咱们指定的 align 进行内存对齐,而是会综合思考 cpu 硬件 cache line 的大小,以及 word size 计算出一个正当的 align 值。

内核在对 slab 对象进行内存布局的时候,会依照这个最终的 align 进行内存对齐。

static unsigned int calculate_alignment(slab_flags_t flags,
        unsigned int align, unsigned int size)
{
    // SLAB_HWCACHE_ALIGN 示意须要依照硬件 cache line 对齐
    if (flags & SLAB_HWCACHE_ALIGN) {
        unsigned int ralign;
        // 获取 cache line 大小 通常为 64 字节
        ralign = cache_line_size();
        // 依据指定对齐参数 align,对象 object size 以及 cache line 大小
        // 综合计算出一个适合的对齐参数 ralign 进去
        while (size <= ralign / 2)
            ralign /= 2;
        align = max(align, ralign);
    }

    // ARCH_SLAB_MINALIGN 为 slab 设置的最小对齐参数,8 字节大小,align 不能小于该值
    if (align < ARCH_SLAB_MINALIGN)
        align = ARCH_SLAB_MINALIGN;
    // 与 word size 进行对齐
    return ALIGN(align, sizeof(void *));
}
// 定义在文件:/include/linux/slab.h
#define ARCH_SLAB_MINALIGN __alignof__(unsigned long long)

2. create_cache 开始正式创立 slab cache

在前文《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现》中的《6.2 slab 的组织架构》大节中,为大家介绍的 slab cache 的整体架构就是在 create_cache 函数中搭建实现的。

create_cache 函数的次要工作就是为 slab cache 创立它的内核数据结构 struct kmem_cache,并为其填充咱们在前文《6.1 slab 的根底信息管理》大节中介绍的对于 struct kmem_cache 相干的属性。

随后内核会为其创立 slab cache 的本地 cpu 构造 kmem_cache_cpu,每个 cpu 对应一个这样的缓存构造。

struct kmem_cache {
    // 每个 cpu 领有一个本地缓存,用于无锁化疾速调配开释对象
    struct kmem_cache_cpu __percpu *cpu_slab;
}

最初为 slab cache 创立 NUMA 节点缓存构造 kmem_cache_node,每个 NUMA 节点对应一个。

struct kmem_cache {
    // slab cache 中 numa node 中的缓存,每个 node 一个
    struct kmem_cache_node *node[MAX_NUMNODES];
}

当 slab cache 的整个骨架被创立进去之后,内核会为其在 sys 文件系统中创立 /sys/kernel/slab/name 目录节点,用于具体记录该 slab cache 的运行状态以及行为信息。

最初将新创建进去的 slab cache 增加到全局双向链表 list 的开端。上面咱们来一起看下这个创立过程的具体实现。

static struct kmem_cache *create_cache(const char *name,
        unsigned int object_size, unsigned int align,
        slab_flags_t flags, unsigned int useroffset,
        unsigned int usersize, void (*ctor)(void *),
        struct mem_cgroup *memcg, struct kmem_cache *root_cache)
{
    struct kmem_cache *s;
    // 为将要创立的 slab cache 调配 kmem_cache 构造
    // kmem_cache 也是内核的一个外围数据结构,同样也会被它对应的 slab cache 所治理
    // 这里就是从 kmem_cache 所属的 slab cache 中拿出一个 kmem_cache 对象进去
    s = kmem_cache_zalloc(kmem_cache, GFP_KERNEL);

    // 利用咱们指定的创立参数初始化 kmem_cache 构造
    s->name = name;
    s->size = s->object_size = object_size;
    s->align = align;
    s->ctor = ctor;
    s->useroffset = useroffset;
    s->usersize = usersize;
    // 创立 slab cache 的外围函数,这里会初始化 kmem_cache 构造中的其余重要属性
    // 包含创立初始化 kmem_cache_cpu 和 kmem_cache_node 构造
    err = __kmem_cache_create(s, flags);
    if (err)
        goto out_free_cache;
    // slab cache 初始状态下,援用计数为 1
    s->refcount = 1;
    // 将刚刚创立进去的 slab cache 退出到 slab cache 在内核中的全局链表治理
    list_add(&s->list, &slab_caches);

out:
    if (err)
        return ERR_PTR(err);
    return s;

out_free_cache:
    // 创立过程呈现谬误之后,开释 kmem_cache 对象
    kmem_cache_free(kmem_cache, s);
    goto out;
}

内核中的每个外围数据结构都会有其专属的 slab cache 来治理,比方,笔者在本文《3. slab 对象池在内核中的利用场景》大节介绍的 task_struct,mm_struct,page,file,socket 等等一系列的内核外围数据结构。

而这里的 slab cache 的数据结构 struct kmem_cache 同样也属于内核的外围数据结构,它也有其专属的 slab cache 来专门治理 kmem_cache 对象的调配与开释。

内核在启动阶段,会专门为 struct kmem_cache 创立其专属的 slab cache,保留在全局变量 kmem_cache 中。

// 全局变量,用于专门治理 kmem_cache 对象的 slab cache
// 定义在文件:/mm/slab_common.c
struct kmem_cache *kmem_cache;

同理,slab cache 的 NUMA 节点缓存 kmem_cache_node 构造也是如此,内核也会为其创立一个专属的 slab cache,保留在全局变量 kmem_cache_node 中。

// 全局变量,用于专门治理 kmem_cache_node 对象的 slab cache
// 定义在文件:/mm/slub.c
static struct kmem_cache *kmem_cache_node;

在 create_cache 函数的开始,内核会从 kmem_cache 专属的 slab cache 中申请一个 kmem_cache 对象。

   s = kmem_cache_zalloc(kmem_cache, GFP_KERNEL);

而后用咱们在 kmem_cache_create 接口函数中指定的参数初始化 kmem_cache 对象。

struct kmem_cache *
kmem_cache_create(const char *name, unsigned int size, unsigned int align,
        slab_flags_t flags, void (*ctor)(void *))

随后会在 __kmem_cache_create 函数中近一步初始化 kmem_cache 对象的其余重要属性。比方,初始化 slab 对象的内存布局相干信息,计算 slab 所须要的物理内存页个数以及所能包容的对象个数,创立初始化 cpu 本地缓存构造以及 NUMA 节点的缓存构造。

最初将刚刚创立进去的 slab cache 退出到 slab cache 在内核中的全局链表 list 中治理

 list_add(&s->list, &slab_caches);

3. __kmem_cache_create 初始化 kmem_cache 对象

__kmem_cache_create 函数的次要工作就是建设 slab cache 的根本骨架,包含初始化 kmem_cache 构造中的其余重要属性,创立初始化本地 cpu 缓存构造以及 NUMA 节点缓存构造,这一部分的重要工作封装在 kmem_cache_open 函数中实现。

随后会查看内核 slab allocator 整个体系的状态,只有 slab_state = FULL 的状态才示意整个 slab allocator 体系曾经在内核中建设并初始化实现了,能够失常运行了。

通过 slab allocator 的状态查看之后,就是 slab cache 整个创立过程的最初一步,利用 sysfs_slab_add 为其在 sys 文件系统中创立 /sys/kernel/slab/name 目录,该目录下的文件具体记录了 slab cache 运行时的各种信息。

int __kmem_cache_create(struct kmem_cache *s, slab_flags_t flags)
{
    int err;
    // 外围函数,在这里会初始化 kmem_cache 的其余重要属性
    err = kmem_cache_open(s, flags);
    if (err)
        return err;

    // 查看内核中 slab 分配器的整体体系是否曾经初始化结束,只有状态是 FULL 的时候才是初始化结束,其余的状态示意未初始化结束。// 在 slab  allocator 体系初始化的时候在 slab_sysfs_init 函数中将 slab_state 设置为 FULL
    if (slab_state <= UP)
        return 0;
    // 在 sys 文件系统中创立 /sys/kernel/slab/name 节点,该目录下的文件蕴含了对应 slab cache 运行时的详细信息
    err = sysfs_slab_add(s);
    if (err)
        // 呈现谬误则开释 kmem_cache 构造
        __kmem_cache_release(s);

    return err;
}

4. slab allocator 整个体系的状态变迁

__kmem_cache_create 函数的整个逻辑还是比拟好了解的,这里惟一不好了解的就是 slab allocator 整个体系的状态 slab_state。

只有 slab_state 为 FULL 状态的时候,才代表 slab allocator 体系可能失常运行,包含这里的创立 slab cache,以及后续从 slab cache 调配对象,开释对象等操作。

只有 slab_state 不是 FULL 状态,slab allocator 体系就是处于半初始化状态,上面笔者就为大家介绍一下 slab_state 的状态变迁流程,这里大家只做简略理解,因为随着后续源码的深刻,笔者还会在相干章节反复提起。

// slab allocator 整个体系的状态 slab_state。enum slab_state {
    DOWN,           /* No slab functionality yet */
    PARTIAL,        /* SLUB: kmem_cache_node available */
    UP,         /* Slab caches usable but not all extras yet */
    FULL            /* Everything is working */
};

在内核没有启动的时候,也就是 slab allocator 体系齐全没有建设的状况下,slab_state 的初始化状态就是 DOWN

当内核启动的过程中,会开始创立初始化 slab allocator 体系,第一步就是为 struct kmem_cache_node 构造创立其专属的 slab cache —— kmem_cache_node 。后续再创立新的 slab cache 的时候,其中的 NUMA 节点缓存构造就是从 kmem_cache_node 里调配。

当 kmem_cache_node 专属的 slab cache 创立结束之后,slab_state 的状态就变为了 PARTIAL

slab allocator 体系建设的最初一项工作,就是创立 kmalloc 内存池体系,kmalloc 体系胜利创立之后,slab_state 的状态就变为了 UP,其实当初 slab allocator 体系就能够失常运行了,然而还不是最终的现实状态。

当内核的初始化工作全副实现的时候,会在 arch_call_rest_init 函数中调用 do_initcalls(),开启内核的 initcall 阶段。

asmlinkage __visible void __init start_kernel(void)
{      
      ........ 省略 .........
      /* Do the rest non-__init'ed, we're now alive */ 
      arch_call_rest_init();}

在内核的 initcall 阶段,会调用内核中定义的所有 initcall,而建设 slab allocator 体系的最初一项工作就为其在 sys 文件系统中创立 /sys/kernel/slab 目录节点,这里会寄存零碎中所有 slab cache 的具体运行信息。

这一项工作就封装在 slab_sysfs_init 函数中,而 slab_sysfs_init 在内核中被定义成了一个 __initcall 函数。

__initcall(slab_sysfs_init);

static int __init slab_sysfs_init(void)
{
    struct kmem_cache *s;
    int err;

    mutex_lock(&slab_mutex);

    slab_kset = kset_create_and_add("slab", &slab_uevent_ops, kernel_kobj);
    if (!slab_kset) {mutex_unlock(&slab_mutex);
        pr_err("Cannot register slab subsystem.\n");
        return -ENOSYS;
    }

    slab_state = FULL;
    
    ....... 省略 ......

}

/sys/kernel/slab 目录节点被创立之后,在 slab_sysfs_init 函数中会将 slab_state 变为 FULL。至此内核中的 slab allocator 整个体系就全副建设起来了。

5. 初始化 slab cache 的外围函数 kmem_cache_open

kmem_cache_open 是初始化 slab cache 内核数据结构 kmem_cache 的外围函数,在这里会初始化 kmem_cache 构造中的一些重要外围参数,以及为 slab cache 创立初始化本地 cpu 缓存构造 kmem_cache_cpu 和 NUMA 节点缓存构造 kmem_cache_node。

经验过 kmem_cache_open 之后,如下图所示的 slab cache 的整个骨架就全副创立进去了。

static int kmem_cache_open(struct kmem_cache *s, slab_flags_t flags)
{
    // 计算 slab 中对象的整体内存布局所须要的 size
    // slab 所需最合适的内存页面大小 order,slab 中所能包容的对象个数
    // 初始化 slab cache 中的外围参数 oo ,min,max 的值
    if (!calculate_sizes(s, -1))
        goto error;

    // 设置 slab cache 在 node 缓存  kmem_cache_node 中的 partial 列表中 slab 的最小个数 min_partial
    set_min_partial(s, ilog2(s->size) / 2);
    // 设置 slab cache 在 cpu 本地缓存的 partial 列表中所能包容的最大闲暇对象个数
    set_cpu_partial(s);

    // 为 slab cache 创立并初始化 node cache 数组
    if (!init_kmem_cache_nodes(s))
        goto error;
    // 为 slab cache 创立并初始化 cpu 本地缓存列表
    if (alloc_kmem_cache_cpus(s))
        return 0;
}

calculate_sizes 函数中封装了 slab 对象内存布局的全副逻辑,笔者在上篇文章《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现》中的《5. 从一个简略的内存页开始聊 slab》大节中介绍的内容,背地的实现逻辑全副封装在此。

除了确定 slab 对象的内存布局之外,calculate_sizes 函数还会初始化 kmem_cache 的其余外围参数:

struct kmem_cache {
    // slab 中治理的对象大小,留神:这里蕴含对象为了对齐所填充的字节数
    unsigned int size;  /* The size of an object including metadata */
    // slab 对象池中的对象在没有被调配之前,咱们是不关怀对象里边存储的内容的。// 内核奇妙的利用对象占用的内存空间存储下一个闲暇对象的地址。// offset 示意用于存储下一个闲暇对象指针的地位间隔对象首地址的偏移
    unsigned int offset;    /* Free pointer offset */
    // 示意 cache 中的 slab 大小,包含 slab 所申请的页面个数,以及所蕴含的对象个数
    // 其中低 16 位示意一个 slab 中所蕴含的对象总数,高 16 位示意一个 slab 所占有的内存页个数。struct kmem_cache_order_objects oo;
    // slab 中所能蕴含对象以及内存页个数的最大值
    struct kmem_cache_order_objects max;
    // 当依照 oo 的尺寸为 slab 申请内存时,如果内存缓和,会采纳 min 的尺寸为 slab 申请内存,能够包容一个对象即可。struct kmem_cache_order_objects min;
}

在实现了对 kmem_cache 构造的外围属性初始化工作之后,内核紧接着会调用 set_min_partial 来设置 kmem_cache->min_partial,从而限度 slab cache 在 numa node 中缓存的 slab 个数下限。

struct kmem_cache {
    // slab cache 在 numa node 中缓存的 slab 个数下限,slab 个数超过该值,闲暇的 empty slab 则会被回收至搭档零碎
    unsigned long min_partial;
}

调用 set_cpu_partial 来设置 kmem_cache->cpu_partial,从而限度 slab cache 在 cpu 本地缓存 partial 链表中闲暇对象个数的下限。

struct kmem_cache {
    // 限定 slab cache 在每个 cpu 本地缓存 partial 链表中所有 slab 中闲暇对象的总数
    // cpu 本地缓存 partial 链表中闲暇对象的数量超过该值,则会将 cpu 本地缓存 partial 链表中的所有 slab 转移到 numa node 缓存中。unsigned int cpu_partial;
};

最初调用 init_kmem_cache_nodes 函数为 slab cache 在每个 NUMA 节点中创立其所属的缓存构造 kmem_cache_node。

调用 alloc_kmem_cache_cpus 函数为 slab cache 创立每个 cpu 的本地缓存构造 kmem_cache_cpu。

当初 slab cache 的整个骨架就被残缺的创立进去了,上面咱们来看一下这个过程中波及到的几个外围函数。

6. slab 对象的内存布局

在上篇文章《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现》的《5. 从一个简略的内存页开始聊 slab》大节的内容介绍中,笔者具体的为大家介绍了 slab 对象的内存布局,本大节,咱们将从内核源码实现角度再来谈一下 slab 对象的内存布局,看一下内核是如何具体布局 slab 对象的内存布局的。

再开始本大节的内容之前,笔者倡议大家先去回顾下前文第五大节的内容。

static int calculate_sizes(struct kmem_cache *s, int forced_order)
{
    slab_flags_t flags = s->flags;
    unsigned int size = s->object_size;
    unsigned int order;

    // 为了进步 cpu 拜访对象的速度,slab 对象的 object size 首先须要与 word size 进行对齐
    size = ALIGN(size, sizeof(void *));

#ifdef CONFIG_SLUB_DEBUG
    // SLAB_POISON:对象中毒标识,是 slab 中的一个术语,用于将对象所占内存填充某些特定的值,示意这块对象不同的应用状态,避免非法越界拜访。// 比方:在将对象调配进来之前,会将对象所占内存用 0x6b 填充,并用 0xa5 填充 object size 区域的最初一个字节。// SLAB_TYPESAFE_BY_RCU:启用 RCU 锁开释 slab
    if ((flags & SLAB_POISON) && !(flags & SLAB_TYPESAFE_BY_RCU) &&
            !s->ctor)
        s->flags |= __OBJECT_POISON;
    else
        s->flags &= ~__OBJECT_POISON;

    // SLAB_RED_ZONE:示意在闲暇对象前后插入 red zone 红色区域(填充特定字节 0xbb),避免对象溢出越界
    // size == s->object_size 示意对象 object size 与 word size 原本就是对齐的,并没有填充任何字节
    // 这时就须要在对象 object size 内存区域的前面插入一段 word size 大小的 red zone。// 如果对象 object size 与 word size 不是对齐的,填充了字节,那么这段填充的字节恰好能够作为右侧 red zone,而不须要额定调配 red zone 空间
    if ((flags & SLAB_RED_ZONE) && size == s->object_size)
        size += sizeof(void *);
#endif

    // inuse 示意 slab 中的对象理论应用的内存区域大小
    // 该值是通过与 word size 对齐之后的大小,如果设置了 SLAB_RED_ZONE,则也包含红色区域大小
    s->inuse = size;

    if (((flags & (SLAB_TYPESAFE_BY_RCU | SLAB_POISON)) ||
        s->ctor)) {
        // 如果咱们开启了 RCU 爱护或者设置了对象 poison 或者设置了对象的构造函数
        // 这些都会占用对象中的内存空间。这种状况下,咱们须要额定减少一个 word size 大小的空间来寄存 free pointer,否则 free pointer 存储在对象的起始地位
        // offset 为 free pointer 与对象起始地址的偏移
        s->offset = size;
        size += sizeof(void *);
    }

#ifdef CONFIG_SLUB_DEBUG
    if (flags & SLAB_STORE_USER)
        // SLAB_STORE_USER 示意须要跟踪对象的调配和开释信息
        // 须要再对象的开端减少两个 struct track 构造,存储调配和开释的信息
        size += 2 * sizeof(struct track);

#ifdef CONFIG_SLUB_DEBUG
    if (flags & SLAB_RED_ZONE) {
        // 在对象内存区域的左侧减少 red zone,大小为 red_left_pad
        // 避免对这块对象内存的写越界
        size += sizeof(void *);
        s->red_left_pad = sizeof(void *);
        s->red_left_pad = ALIGN(s->red_left_pad, s->align);
        size += s->red_left_pad;
    }
#endif

    // slab 从它所申请的内存页 offset 0 开始,一个接一个的存储对象
    // 调整对象的 size 保障对象之间依照指定的对齐形式 align 进行对齐
    size = ALIGN(size, s->align);
    s->size = size;
    // 这里 forced_order 传入的是 -1
    if (forced_order >= 0)
        order = forced_order;
    else
        // 计算 slab 所须要申请的内存页数(2 ^ order 个内存页)order = calculate_order(size);

    if ((int)order < 0)
        return 0;
    // 依据 slab 的 flag 设置,设置向搭档零碎申请内存时应用的 allocflags
    s->allocflags = 0;
    if (order)
        // slab 所须要的内存页多于 1 页时,则向搭档零碎申请复合页。s->allocflags |= __GFP_COMP;

    // 从 DMA 区域中获取实用于 DMA 的内存页
    if (s->flags & SLAB_CACHE_DMA)
        s->allocflags |= GFP_DMA;
    // 从 DMA32 区域中获取实用于 DMA 的内存页
    if (s->flags & SLAB_CACHE_DMA32)
        s->allocflags |= GFP_DMA32;
    // 申请可回收的内存页
    if (s->flags & SLAB_RECLAIM_ACCOUNT)
        s->allocflags |= __GFP_RECLAIMABLE;

    // 计算 slab cache 中的 oo,min,max 值
    // 一个 slab 到底须要多少个内存页,可能存储多少个对象
    // 低 16 为存储 slab 所能蕴含的对象总数,高 16 为存储 slab 所需的内存页个数
    s->oo = oo_make(order, size);
    // get_order 函数计算出的 order 为包容一个 size 大小的对象至多须要的内存页个数
    s->min = oo_make(get_order(size), size);
    if (oo_objects(s->oo) > oo_objects(s->max))
        // 初始时 max 和 oo 相等
        s->max = s->oo;
    // 返回 slab 中所能包容的对象个数
    return !!oo_objects(s->oo);
}

在内核查 slab 对象开始内存布局之前,为了进步 cpu 拜访对象的速度,首先须要将 slab 对象的 object size 与 word size 进行对齐。如果 object size 与 word size 原本就是对齐的,那么内核不会做任何事件。如果不是对齐的,那么就须要在对象前面填充一些字节,达到与 word size 对齐的目标。

 size = ALIGN(size, sizeof(void *));

如果咱们设置了 SLAB_RED_ZONE,示意须要再对象 object size 内存区域前后各插入一段 red zone 区域,目标是为了避免内存的读写越界。

如果对象 object size 与 word size 原本就是对齐的,并没有填充任何字节:size == s->object_size,那么此时就须要在对象 object size 内存区域的前面插入一段 word size 大小的 red zone。

如果对象 object size 与 word size 不是对齐的,那么内核就会在 object size 区域前面填充字节达到与 word size 对齐的目标,而这段填充的字节恰好能够作为对象右侧 red zone,而不须要额定为右侧 red zone 分配内存空间。

 if ((flags & SLAB_RED_ZONE) && size == s->object_size)
        size += sizeof(void *);

如果咱们设置了 SLAB_POISON 或者开启了 RCU 或者设置了对象的构造函数,它们都会占用对象的理论内存区域 object size。

比方咱们设置 SLAB_POISON 之后,slab 对象的 object size 内存区域会被内核用特殊字符 0x6b 填充,并用 0xa5 填充对象 object size 内存区域的最初一个字节示意填充结束。

这样一来,用于指向下一个闲暇对象的 freepointer 就没中央寄存了,所以须要在以后对象内存区域的根底上再额定开拓一段 word size 大小的内存区域专门寄存 freepointer。

    if (((flags & (SLAB_TYPESAFE_BY_RCU | SLAB_POISON)) ||
        s->ctor)) {
        // offset 为 free pointer 与对象起始地址的偏移
        s->offset = size;
        size += sizeof(void *);
    }

除此之外,对象的 freepointer 指针就会放在对象自身内存区域 object size 中,因为在对象被调配进来之前,用户基本不会关怀对象内存里到底寄存的是什么。

如果咱们设置了 SLAB_STORE_USER,示意咱们冀望跟踪 slab 对象的调配与开释相干的信息,而这些跟踪信息内核应用一个 struct track 构造来存储。

所以在这种状况下,内核须要在目前 slab 对象的内存区域前面额定减少两个 sizeof(struct track) 大小的区域进去,用来别离存储 slab 对象的调配和开释信息。

如果咱们设置了 SLAB_RED_ZONE,最初,还须要再 slab 对象内存区域的左侧填充一段 red_left_pad 大小的内存区域作为左侧 red zone。另外还须要再 slab 对象内存区域的开端再次填充一段 word size 大小的内存区域作为 padding 局部。

右侧 red zone,在本大节开始的中央曾经被填充了。

    if (flags & SLAB_RED_ZONE) {size += sizeof(void *);
        s->red_left_pad = sizeof(void *);
        s->red_left_pad = ALIGN(s->red_left_pad, s->align);
        size += s->red_left_pad;
    }

当初对于 slab 对象内存布局的全部内容,咱们就介绍完了,最终咱们失去了 slab 对象实在占用内存大小 size,内核会依据这个 size,在物理内存页中划分出一个一个的对象进去。

那么一个 slab 到底须要多少个物理内存页呢?内核会通过 calculate_order 函数依据肯定的算法计算出一个正当的 order 值。这个过程笔者前面会细讲,当初咱们次要关怀整体流程。

slab 所需的物理内存页个数计算出来之后,内核会依据 slab 对象占用内存的大小 size,计算出一个 slab 能够包容的对象个数。并将这个后果保留在 kmem_cache 构造中的 oo 属性中。

s->oo = oo_make(order, size);
struct kmem_cache {
    // 示意 cache 中的 slab 大小,包含 slab 所申请的页面个数,以及所蕴含的对象个数
    // 其中低 16 位示意一个 slab 中所蕴含的对象总数,高 16 位示意一个 slab 所占有的内存页个数。struct kmem_cache_order_objects oo;
}

内核会通过 struct kmem_cache_order_objects 这样一个构造来保留 slab 所须要的物理内存页个数以及 slab 所能包容的对象个数,其中 kmem_cache_order_objects 的高 16 位保留 slab 所须要的物理内存页个数,低 16 位保留 slab 所能包容的对象个数。

#define OO_SHIFT    16

struct kmem_cache_order_objects {
     // 高 16 为存储 slab 所需的内存页个数, 低 16 为存储 slab 所能蕴含的对象总数
    unsigned int x;
};

static inline struct kmem_cache_order_objects oo_make(unsigned int order,
        unsigned int size)
{
    struct kmem_cache_order_objects x = {
        // 高 16 为存储 slab 所需的内存页个数, 低 16 为存储 slab 所能蕴含的对象总数
        (order << OO_SHIFT) + order_objects(order, size)
    };

    return x;
}

static inline unsigned int order_objects(unsigned int order, unsigned int size)
{
    // 依据 slab 中蕴含的物理内存页个数以及对象的 size,计算 slab 可包容的对象个数
    return ((unsigned int)PAGE_SIZE << order) / size;
}

static inline unsigned int oo_order(struct kmem_cache_order_objects x)
{
    // 获取高 16 位,slab 中所须要的内存页 order
    return x.x >> OO_SHIFT;
}

// 十进制为:65535,二进制为:16 个 1,用于截取低 16 位
#define OO_MASK     ((1 << OO_SHIFT) - 1) 

static inline unsigned int oo_objects(struct kmem_cache_order_objects x)
{
    // 获取低 16 位,slab 中能包容的对象个数
    return x.x & OO_MASK;
}

随后内核会通过 get_order 函数来计算,包容 一个 size 大小的对象所须要的起码物理内存页个数。用这个值作为 kmem_cache 构造中的 min 属性。

s->min = oo_make(get_order(size), size);
struct kmem_cache {struct kmem_cache_order_objects min;}

内核在创立 slab 的时候,最开始会依照 oo 指定的尺寸来向搭档零碎申请内存页,如果内存缓和,申请内存失败。那么内核会降级采纳 min 的尺寸再次向搭档零碎申请内存。也就是说 slab 中至多会蕴含一个对象。

最初会设置 max 的值,从源码中咱们能够看到 max 的值与 oo 的值是相等的

  if (oo_objects(s->oo) > oo_objects(s->max))
        // 初始时 max 和 oo 相等
        s->max = s->oo;

到当初为止,笔者在本文《6.1 slab 的根底信息管理》大节中介绍的 kmem_cache 构造相干的重要属性就全副设置实现了。

7. 计算 slab 所须要的 page 个数

一个 slab 到底须要多少个物理内存页就是在这里计算出来的,这里内核会依据肯定的算法,尽量保障 slab 中的内存碎片最小化,综合计算出一个正当的 order 值。上面咱们来一起看下这个计算逻辑:

static unsigned int slub_min_order;
static unsigned int slub_max_order = PAGE_ALLOC_COSTLY_ORDER;// 3
static unsigned int slub_min_objects;

static inline int calculate_order(unsigned int size)
{
    unsigned int order;
    unsigned int min_objects;
    unsigned int max_objects;

    // 计算 slab 中能够包容的最小对象个数
    min_objects = slub_min_objects;
    if (!min_objects)
        // nr_cpu_ids 示意以后零碎中的 cpu 个数
        // fls 能够获取参数的最高无效 bit 的位数,比方 fls(0)=0,fls(1)=1,fls(4) = 3
        // 如果以后零碎中有 4 个 cpu,那么 min_object 的初始值为 4*(3+1) = 16 
        min_objects = 4 * (fls(nr_cpu_ids) + 1);
    // slab 最大内存页 order 初始为 3,计算 slab 最大可包容的对象个数
    max_objects = order_objects(slub_max_order, size);
    min_objects = min(min_objects, max_objects);

    while (min_objects > 1) {// slab 中的碎片管制系数,碎片大小不能超过 (slab 所占内存大小 / fraction)
        // fraction 值越大,slab 中所能容忍的碎片就越小
        unsigned int fraction;

        fraction = 16;
        while (fraction >= 4) {
            // 依据以后 fraction 计算 order,须要查找出可能使 slab 产生碎片最小化的 order 值进去
            order = slab_order(size, min_objects,
                    slub_max_order, fraction);
             // order 不能超过 max_order,否则须要升高 fraction,放宽对碎片的要求限度,从新循环计算
            if (order <= slub_max_order)
                return order;
            fraction /= 2;
        }
        // 进一步放宽对 min_object 的要求,slab 会尝试少放一些对象
        min_objects--;
    }

    // 通过前边 while 循环的计算,咱们无奈在这一个 slab 中搁置多个 size 大小的对象,因为 min_object = 1 的时候就退出循环了。// 那么上面就会尝试看能不能只放入一个对象
    order = slab_order(size, 1, slub_max_order, 1);
    if (order <= slub_max_order)
        return order;
    // 流程到这里示意,咱们要池化的对象 size 太大了,slub_max_order 都放不下
    // 当初只能放宽对 max_order 的限度到 MAX_ORDER = 11
    order = slab_order(size, 1, MAX_ORDER, 1);
    if (order < MAX_ORDER)
        return order;
    return -ENOSYS;
}

首先内核会计算出 slab 须要包容对象的最小个数 min_objects,计算公式: min_objects = 4 * (fls(nr_cpu_ids) + 1)

  • nr_cpu_ids 示意以后零碎中的 cpu 个数
  • fls 获取参数二进制模式的最高无效 bit 的位数,比方 fls(0)=0,fls(1)=1,fls(4) = 3

这里咱们看到 min_objects 是和以后零碎中的 cpu 个数有关系的。

内核规定 slab 所须要的物理内存页个数的最大值 slub_max_order 初始化为 3,也就是 slab 最多只能向搭档零碎中申请 8 个内存页。

依据这里的 slub_max_order 和 slab 对象的 size 通过 order_objects 函数计算出 slab 所能包容对象的最大值。

slab 所能包容的对象个数越多,那么所须要的物理内存页就越多,slab 所能包容的对象个数越少,那么所须要的物理内存页就越少。

内核通过刚刚计算出的 min_objects 能够计算出 slab 所须要的最小内存页个数,咱们临时称为 min_order。

随后内核会遍历 min_order 与 slub_max_order 之间的所有 order 值,直到找到满足内存碎片限度要求的一个 order。

那么内核对于内存碎片限度的要求具体如何定义呢?

内核会定义一个 fraction 变量作为 slab 内存碎片的管制系数,内核要求 slab 中内存碎片大小不能超过 (slab 所占内存大小 / fraction),fraction 的值越大,示意 slab 中所能容忍的内存碎片就越小。fraction 的初始值为 16。

在内核寻找最佳适合 order 的过程中,最高优先级是要将内存碎片管制在一个非常低的范畴内,在这个根底之上,遍历 min_order 与 slub_max_order 之间的所有 order 值,看他们产生碎片的大小是否低于 (slab 所占内存大小 / fraction) 的要求。如果满足,那么这个 order 就是最终的计算结果,后续 slab 会依据这个 order 值向搭档零碎申请物理内存页。这个逻辑封装在 slab_order 函数中。

如果内核遍历完一遍 min_order 与 slub_max_order 之间的所有 order 值均不合乎内存碎片限度的要求,那么内核只能尝试放宽对内存碎片的要求,将 fraction 调小一些——fraction /= 2,再次从新遍历所有 order。但 fraction 系数最低不能低于 4。

如果 fraction 系数低于 4 了,阐明内核曾经将碎片限度要求放到最宽了,在这么宽松的条件下仍然无奈找到一个满足限度要求的 order 值,那么内核会在近一步的降级,放宽对 min_objects 的要求——min_objects--,尝试在 slab 中少放一些对象。fraction 系数复原为 16,在从新遍历,尝试查找合乎内存碎片限度要求的 order 值。

最极其的状况就是,无论内核怎么放宽对内存碎片的限度,无论怎么放宽 slab 中包容对象的最小个数要求,内核始终无奈找到一个 order 值可能满足如此宽松的内存碎片限度条件。当 min_objects == 1 的时候就会退出 while (min_objects > 1) 循环进行寻找。

最终内核的托底计划是将 min_objects 调整为 1,fraction 调整为 1,再次调用 slab_order,这里的语义是:在这种极其的状况下,slab 中起码只能包容一个对象,那么内核就调配包容一个对象所须要的内存页。

如果 slab 对象太大了,有可能冲破了 slub_max_order = 3 的限度,内核会近一步放宽至 MAX_ORDER = 11,这里咱们能够看出内核的信心,无论如何必须保障 slab 中至多包容一个对象。

上面是 slab_order 函数的逻辑,它是整个计算过程的外围:

// 一个 page 最多容许寄存 32767 个对象
#define MAX_OBJS_PER_PAGE    32767

static inline unsigned int slab_order(unsigned int size,
        unsigned int min_objects, unsigned int max_order,
        unsigned int fract_leftover)
{
    unsigned int min_order = slub_min_order;
    unsigned int order;

    // 如果 2^min_order 个内存页能够寄存的对象个数超过 32767 限度
    // 那么返回 size * MAX_OBJS_PER_PAGE 所须要的 order 减 1
    if (order_objects(min_order, size) > MAX_OBJS_PER_PAGE)
        return get_order(size * MAX_OBJS_PER_PAGE) - 1;

    // 从 slab 所须要的最小 order 到最大 order 之间开始遍历,查找可能使 slab 碎片最小的 order 值
    for (order = max(min_order, (unsigned int)get_order(min_objects * size));
            order <= max_order; order++) {
        // slab 在以后 order 下,所占用的内存大小
        unsigned int slab_size = (unsigned int)PAGE_SIZE << order;
        unsigned int rem;
        // slab 的碎片大小:调配完 object 之后,所产生的碎片大小
        rem = slab_size % size;
        // 碎片大小 rem 不能超过 slab_size / fract_leftover 即符合要求
        if (rem <= slab_size / fract_leftover)
            break;
    }

    return order;
}

get_order(size) 函数的逻辑就比较简单了,它不会像 calculate_order 函数那样简单,不须要思考内存碎片的限度。它的逻辑只是简略的计算调配一个 size 大小的对象所须要的起码内存页个数,用于在 calculate_sizes 函数的最初计算 kmem_cache 构造的 min 值。

s->min = oo_make(get_order(size), size);

get_order 函数的计算逻辑如下:

  • 如果给定的 size 在 [0,PAGE_SIZE] 之间,那么 order = 0,须要一个内存页面即可。
  • size 在 [PAGE_SIZE + 1,2^1 * PAGE_SIZE] 之间,order = 1
  • size 在 [2^1 PAGE_SIZE + 1,2^2 PAGE_SIZE] 之间,order = 2
  • size 在 [2^2 PAGE_SIZE + 1,2^3 PAGE_SIZE] 之间,order = 3
  • size 在 [2^3 PAGE_SIZE + 1,2^4 PAGE_SIZE] 之间,order = 4
// 定义在文件 /include/asm-generic/getorder.h
// 该函数的次要作用就是依据给定的 size 计算出所需最小的 order
static inline __attribute_const__ int get_order(unsigned long size)
{if (__builtin_constant_p(size)) {if (!size)
            return BITS_PER_LONG - PAGE_SHIFT;

        if (size < (1UL << PAGE_SHIFT))
            return 0;

        return ilog2((size) - 1) - PAGE_SHIFT + 1;
    }

    size--;
    size >>= PAGE_SHIFT;
#if BITS_PER_LONG == 32
    return fls(size);
#else
    return fls64(size);
#endif
}

当初,一个 slab 所须要的内存页个数的计算过程,笔者就为大家交代结束了,上面咱们来看一下 kmem_cache 构造的其余属性的初始化过程。

8. set_min_partial

该函数的次要目标是为了计算 slab cache 在 NUMA 节点缓存 kmem_cache_node->partial 链表中的 slab 个数下限,超过该值,闲暇的 empty slab 则会被回收至搭档零碎中。

kmem_cache 构造中的 min_partial 初始值为 min = ilog2(s->size) / 2,须要保障 min_partial 的值在 [5,10] 的范畴之内。

#define MIN_PARTIAL 5
#define MAX_PARTIAL 10

// 计算 slab cache 在 node 中缓存的个数,kmem_cache_node 中 partial 列表中 slab 个数的下限 min_partial
// 超过该值,闲暇的 slab 就会被回收
// 初始 min = ilog2(s->size) / 2,必须保障 min_partial 的值 在 [MIN_PARTIAL,MAX_PARTIAL] 之间
static void set_min_partial(struct kmem_cache *s, unsigned long min)
{if (min < MIN_PARTIAL)
        min = MIN_PARTIAL;
    else if (min > MAX_PARTIAL)
        min = MAX_PARTIAL;
    s->min_partial = min;
}

9. set_cpu_partial

这里会设置 kmem_cache 构造的 cpu_partial 属性,该值限度了 slab cache 在 cpu 本地缓存的 partial 列表中所能包容的最大闲暇对象个数。

同时该值也决定了当 kmem_cache_cpu->partial 链表为空时,内核会从 kmem_cache_node->partial 链表填充 cpu_partial / 2 个 slab 到 kmem_cache_cpu->partial 链表中。相干具体内容可回顾上篇文章《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现》中的《7.3 从 NUMA 节点缓存中调配》大节。

set_cpu_partial 函数的逻辑也很简略,就是依据上篇文章《6 slab 对象的内存布局》大节中计算出的 slab 对象 size 大小来决定 cpu_partial 的值。

static void set_cpu_partial(struct kmem_cache *s)
{
// 当配置了 CONFIG_SLUB_CPU_PARTIAL,则 slab cache 的 cpu 本地缓存 kmem_cache_cpu 中蕴含 partial 列表
#ifdef CONFIG_SLUB_CPU_PARTIAL
    // 判断 kmem_cache_cpu 是否蕴含有 partial 列表
    if (!kmem_cache_has_cpu_partial(s))
        s->cpu_partial = 0;
    else if (s->size >= PAGE_SIZE)
        s->cpu_partial = 2;
    else if (s->size >= 1024)
        s->cpu_partial = 6;
    else if (s->size >= 256)
        s->cpu_partial = 13;
    else
        s->cpu_partial = 30;
#endif
}

10. init_kmem_cache_nodes

到当初为止,kmem_cache 构造中的所有重要属性就曾经初始化结束了,slab cache 的创立过程也进入了序幕,最初内核须要为 slab cache 创立本地 cpu 缓存构造以及 NUMA 节点缓存构造

本大节的次要内容就是内核如何为 slab cache 创立其在 NUMA 节点中的缓存构造:

struct kmem_cache {
    // slab cache 中 numa node 中的缓存,每个 node 一个
    struct kmem_cache_node *node[MAX_NUMNODES];
}

slab cache 在每个 NUMA 节点中都有本人的缓存构造 kmem_cache_node,init_kmem_cache_nodes 函数须要遍历所有的 NUMA 节点,并利用 struct kmem_cache_node 专属的 slab cache —— 全局变量 kmem_cache_node,调配一个 kmem_cache_node 对象,并调用 init_kmem_cache_node 对其进行初始化。

static int init_kmem_cache_nodes(struct kmem_cache *s)
{
    int node;
    // 遍历所有的 numa 节点,为 slab cache 创立 node cache
    for_each_node_state(node, N_NORMAL_MEMORY) {
        struct kmem_cache_node *n;

        if (slab_state == DOWN) {
            // 如果此时 slab allocator 体系还未建设,则调用该办法调配 kmem_cache_node 构造,并初始化。// slab cache 的失常创立流程不会走到这个分支,该分支用于在内核初始化的时候创立 kmem_cache_node 对象池应用
            early_kmem_cache_node_alloc(node);
            continue;
        }
        // 为 node cache 调配对应的 kmem_cache_node 对象
        // kmem_cache_node 对象也由它对应的 slab cache 治理
        n = kmem_cache_alloc_node(kmem_cache_node,
                        GFP_KERNEL, node);
        // 初始化 node cache
        init_kmem_cache_node(n);
        // 初始化 slab cache 构造 kmem_cache 中的 node 数组
        s->node[node] = n;
    }
    return 1;
}
static void
init_kmem_cache_node(struct kmem_cache_node *n)
{
    n->nr_partial = 0;
    spin_lock_init(&n->list_lock);
    INIT_LIST_HEAD(&n->partial);
#ifdef CONFIG_SLUB_DEBUG
    atomic_long_set(&n->nr_slabs, 0);
    atomic_long_set(&n->total_objects, 0);
    INIT_LIST_HEAD(&n->full);
#endif
}

11. alloc_kmem_cache_cpus

这里次要是为 slab cache 创立其 cpu 本地缓存构造 kmem_cache_cpu,每个 cpu 一个这样的构造,并调用 per_cpu_ptr 将创立好的 kmem_cache_cpu 构造与对应的 cpu 相关联初始化。

struct kmem_cache {
    // 每个 cpu 领有一个本地缓存,用于无锁化疾速调配开释对象
    struct kmem_cache_cpu __percpu *cpu_slab;
}
static inline int alloc_kmem_cache_cpus(struct kmem_cache *s)
{
    // 为 slab cache 调配 cpu 本地缓存构造 kmem_cache_cpu
    // __alloc_percpu 函数在内核中专门用于调配 percpu 类型的构造体(the percpu allocator)//  kmem_cache_cpu 构造也是 percpu 类型的,这里通过 __alloc_percpu 间接调配
    s->cpu_slab = __alloc_percpu(sizeof(struct kmem_cache_cpu),
                     2 * sizeof(void *));
    // 初始化 cpu 本地缓存构造 kmem_cache_cpu
    init_kmem_cache_cpus(s);
    return 1;
}
static void init_kmem_cache_cpus(struct kmem_cache *s)
{
    int cpu;
    // 遍历所有 CPU,通过 per_cpu_ptr 将后面调配的 kmem_cache_cpu 构造与对应的 CPU 关联对应起来
    // 同时初始化 kmem_cache_cpu 变量外面的 tid 为其所关联 cpu 的编号
    for_each_possible_cpu(cpu)
        per_cpu_ptr(s->cpu_slab, cpu)->tid = init_tid(cpu);
}

至此,slab cache 的整个骨架就全副被创立进去了,最终失去的 slab cache 残缺架构如下图所示:

最初,咱们能够联合下面的 slab cache 架构图与上面这副 slab cache 创立流程图加以比照,回顾总结。

12. 内核第一个 slab cache 是如何被创立进去的

在上大节介绍 slab cache 的创立过程中,笔者其实暗暗地埋下了一个伏笔,不晓得,大家有没有发现,在 slab cache 创立的过程中须要创立两个非凡的数据结构:

  • 一个是 slab cache 本身的治理构造 struct kmem_cache。
  • 另一个是 slab cache 在 NUMA 节点中的缓存构造 struct kmem_cache_node。

而 struct kmem_cache 和 struct kmem_cache_node 同样也都是内核的外围数据结构,他俩各自也有一个专属的 slab cache 来治理 kmem_cache 对象和 kmem_cache_node 对象的调配与开释。

// 全局变量,用于专门治理 kmem_cache 对象的 slab cache
// 定义在文件:/mm/slab_common.c
struct kmem_cache *kmem_cache;

// 全局变量,用于专门治理 kmem_cache_node 对象的 slab cache
// 定义在文件:/mm/slub.c
static struct kmem_cache *kmem_cache_node;

slab cache 的 cpu 本地缓存构造 struct kmem_cache_cpu 是一个 percpu 类型的变量,由 __alloc_percpu间接创立,并不需要一个专门的 slab cache 来治理。

在 slab cache 的创立过程中,内核首先须要向 struct kmem_cache 构造专属的 slab cache 申请一个 kmem_cache 对象。

static struct kmem_cache *create_cache(const char *name,
        unsigned int object_size, unsigned int align,
        slab_flags_t flags, unsigned int useroffset,
        unsigned int usersize, void (*ctor)(void *),
        struct mem_cgroup *memcg, struct kmem_cache *root_cache)
{
    struct kmem_cache *s;
    s = kmem_cache_zalloc(kmem_cache, GFP_KERNEL);

                  ......... 省略 .........
}

当 kmem_cache 对象初始化实现之后,内核须要向 struct kmem_cache_node 构造专属的 slab cache 申请一个 kmem_cache_node 对象,作为 slab cache 在 NUMA 节点中的缓存构造。

static int init_kmem_cache_nodes(struct kmem_cache *s)
{
    int node;
    // 遍历所有的 numa 节点,为 slab cache 创立 node cache
    for_each_node_state(node, N_NORMAL_MEMORY) {
        struct kmem_cache_node *n;

                ......... 省略 .........

        n = kmem_cache_alloc_node(kmem_cache_node,
                        GFP_KERNEL, node);
        init_kmem_cache_node(n);
        s->node[node] = n;
    }
    return 1;
}

那么问题来了,kmem_cachekmem_cache_node 这两个 slab cache 是怎么来的?

因为他俩实质上是一个 slab cache,而 slab cache 的创立又须要 kmem_cache(slab cache)和 kmem_cache_node(slab cache),当零碎中第一个 slab cache 被创立的时候,此时并没有 kmem_cache(slab cache),也没有 kmem_cache_node(slab cache),这就变成死锁了,是一个先有鸡还是先有蛋的问题。

那么内核是如何来解决这个先有鸡还是先有蛋的问题呢?让咱们先把思路拉回到内核启动的阶段~~~

12.1 slab allocator 体系的初始化

内核启动的外围初始化逻辑封装 /init/main.c 文件的 start_kernel 函数中,在这里会初始化内核的各个子系统,内存管理子系统的初始化工作就在这里,封装在 mm_init 函数里。

在 mm_init 函数中会初始化内核的 slab allocator 体系 —— kmem_cache_init()。

asmlinkage __visible void __init start_kernel(void)
{     
      ........ 省略 .........
      // 初始化内存管理子系统
      mm_init();
      
      ........ 省略 .........
}

/*
 * Set up kernel memory allocators
 */
static void __init mm_init(void)
{
      ........ 省略 .........
      // 创立并初始化 slab allocator 体系
      kmem_cache_init();

      ........ 省略 .........
}

而内核解决这个“先有鸡还是先有蛋”问题的机密就藏在 /mm/slub.c 文件的 kmem_cache_init 函数中。

内核首先会定义两个动态的 static __initdata struct kmem_cache 构造 boot_kmem_cache,boot_kmem_cache_node,用于在内核初始化内存管理子系统的时候 长期动态地 创立 kmem_cache(slab cache)和 kmem_cache_node(slab cache)所须要的 struct kmem_cache 和 struct kmem_cache_node 构造

这样一来,内核就通过这两个长期的动态 kmem_cache 构造:boot_kmem_cache,boot_kmem_cache_node 突破了死锁的循环期待条件。

当这两个长期的 boot_kmem_cache,boot_kmem_cache_node 被创立初始化之后,随后内核会通过 bootstrap 将这两个长期 slab cache 深拷贝到全局变量 kmem_cache(slab cache)和 kmem_cache_node(slab cache)中。

从此,内核就有了正式的 kmem_cache(slab cache)和 kmem_cache_node(slab cache),后续就能够依照失常流程 动静地 创立 slab cache 了,失常的创立流程就是笔者在本文前边几个大节中为大家介绍的内容。

上面咱们来一起看下 slab allocator 体系的初始化过程:

// 全局变量,用于专门治理 kmem_cache 对象的 slab cache
// 定义在文件:/mm/slab_common.c
struct kmem_cache *kmem_cache;

// 全局变量,用于专门治理 kmem_cache_node 对象的 slab cache
// 定义在文件:/mm/slub.c
static struct kmem_cache *kmem_cache_node;

void __init kmem_cache_init(void)
{
    // slab allocator 体系结构中最外围的就是 kmem_cache 构造和 kmem_cache_node 构造,而这两个构造同时又被各自的 slab cache 所治理
    // 而当初 slab allocator 体系还未创立,所以须要利用两个动态的构造来创立 kmem_cache,kmem_cache_node 对象
    // 这里就是定义两个 kmem_cache 类型的动态局部变量(动态构造):内核启动的时候被加载进 BSS 段中,随后会为其分配内存。// boot_kmem_cache 用于长期创立 kmem_cache 构造。// boot_kmem_cache_node 用于长期创立 kmem_cache_node 构造
    // boot_kmem_cache 和 boot_kmem_cache_node 当初只是两个空的构造,须要动态的进行初始化。static __initdata struct kmem_cache boot_kmem_cache,
        boot_kmem_cache_node;

    // 临时先将这两个动态构造赋值给对应的全局变量,前面会初始化这两个全局变量
    kmem_cache_node = &boot_kmem_cache_node;
    kmem_cache = &boot_kmem_cache;

    // 动态地初始化 boot_kmem_cache_node 
    // 从这里能够看出 slab 体系,建设的第一个 slab cache 就是 kmem_cache_node(slab cache)
    create_boot_cache(kmem_cache_node, "kmem_cache_node",
        sizeof(struct kmem_cache_node), SLAB_HWCACHE_ALIGN, 0, 0);

    // 当 kmem_cache_node(slab cache)被创立初始化之后,slab_state 变为 PARTIAL
    // 这个状态示意目前 kmem_cache_node cache 曾经创立结束,能够利用它动态分配 kmem_cache_node 对象了。slab_state = PARTIAL;

    // 动态地初始化 boot_kmem_cache
    // 从这里能够看出 slab 体系,建设的第二个 slab cache 就是 kmem_cache(slab cache)
    create_boot_cache(kmem_cache, "kmem_cache",
            offsetof(struct kmem_cache, node) +
                nr_node_ids * sizeof(struct kmem_cache_node *),
               SLAB_HWCACHE_ALIGN, 0, 0);

    // 流程到这里,两个动态的 kmem_cache 构造:boot_kmem_cache,boot_kmem_cache_node 就曾经初始化结束了。// 然而这两个动态构造只是长期的,目标是为了在 slab 体系初始化阶段动态的创立 kmem_cache 对象和 kmem_cache_node 对象。// 在 bootstrap 中会将 boot_kmem_cache,boot_kmem_cache_node 中的内容深拷贝到最终的 kmem_cache(slab cache),kmem_cache_node(slab cache)中。// 前面咱们就能够利用这两个最终的外围构造来动静的进行 slab 创立。kmem_cache = bootstrap(&boot_kmem_cache);
    kmem_cache_node = bootstrap(&boot_kmem_cache_node);

    ........ 省略 kmalloc 相干初始化过程 .........
}

初始化 slab allocator 体系的外围就是如何动态的创立和初始化这两个动态的 slab cache:boot_kmem_cache,boot_kmem_cache_node。

这个外围逻辑封装在 create_boot_cache 函数中,大家须要留神该函数第一个参数 struct kmem_cache *s,参数 s 指向的是下面两个长期的动态的 slab cache。当初是内核初始化阶段,以后零碎中并不存在一个正式残缺的 slab cache,这一点大家在浏览本大节的时候要时刻留神。

/* Create a cache during boot when no slab services are available yet */
void __init create_boot_cache(struct kmem_cache *s, const char *name,
        unsigned int size, slab_flags_t flags,
        unsigned int useroffset, unsigned int usersize)
{
    int err;
    unsigned int align = ARCH_KMALLOC_MINALIGN;

    // 上面就是动态初始化 kmem_cache 构造的逻辑
    // 挨个对 kmem_cache 构造的外围属性进行动态赋值
    s->name = name;
    s->size = s->object_size = size;

    if (is_power_of_2(size))
        align = max(align, size);
    // 依据指定的对齐参数 align 以及 CPU Cache line 的大小计算出一个适合的 align 进去
    s->align = calculate_alignment(flags, align, size);

    s->useroffset = useroffset;
    s->usersize = usersize;
    // 这里又来到了之前介绍的创立 slab cache 的创立流程
    // 该函数是创立 slab cache 的外围函数,这里会初始化 kmem_cache 构造中的其余重要属性
    // 以及创立初始化 slab cache 中的 cpu 本地缓存 和 node 节点缓存构造
    err = __kmem_cache_create(s, flags);
    // 临时不须要合并 merge,援用计数设置为 -1
    s->refcount = -1; 
}

这里在对动态 kmem_cache 构造进行简略初始化之后,内核又调用了 __kmem_cache_create 函数,这个函数咱们曾经十分相熟了,遗记的同学能够回看下本文的《3. __kmem_cache_create 初始化 kmem_cache 对象》大节。

__kmem_cache_create 函数的次要工作就是建设 slab cache 的根本骨架,包含初始化 kmem_cache 构造中的其余重要外围属性,创立初始化本地 cpu 缓存构造以及 NUMA 节点缓存构造。

这里咱们来重点看下 init_kmem_cache_nodes 函数,在内核初始化动态 boot_kmem_cache_node(动态 slab cache)的场景下,这里的流程逻辑与《10. init_kmem_cache_nodes》大节中介绍的会有所不同。

在 slab allocator 体系中,第一个被创立进去的 slab cache 就是这里的 boot_kmem_cache_node,以后 slab_state == DOWN。以后流程正在创立初始化 boot_kmem_cache_node,所以目前内核无奈利用 boot_kmem_cache_node 来动静的调配 kmem_cache_node 对象。

所以当创立初始化 boot_kmem_cache_node 的时候,流程会进入 if (slab_state == DOWN) 分支,通过 early_kmem_cache_node_alloc 函数来动态调配 kmem_cache_node 对象。

static int init_kmem_cache_nodes(struct kmem_cache *s)
{
    int node;
    // 遍历所有的 numa 节点,为 slub cache 创立初始化 node cache 数组
    for_each_node_state(node, N_NORMAL_MEMORY) {
        struct kmem_cache_node *n;
        // 当 slub 在系统启动阶段初始化时,创立 kmem_cache_node cache 的时候,此时 slab_state == DOWN
        // 因为此时 kmem_cache_node cache 正在创立,所以无奈利用 kmem_cache_node 所属的 slub cache 动静的调配 kmem_cache_node 对象
        // 这里会通过 early_kmem_cache_node_alloc 函数动态的调配 kmem_cache_node 对象,并初始化。if (slab_state == DOWN) {
             // 创立 boot_kmem_cache_node 时会走到这个分支
            early_kmem_cache_node_alloc(node);
            continue;
        }

        // 当 slab 体系在初始化 boot_kmem_cache 时,这时 slab_state 为 PARTIAL,流程就会走到这里。// 示意此时 boot_kmem_cache_node 曾经初始化,能够利用它动静的调配 kmem_cache_node 对象了
        // 这里的 kmem_cache_node 就是 boot_kmem_cache_node
        n = kmem_cache_alloc_node(kmem_cache_node,
                        GFP_KERNEL, node);
        // 初始化 kmem_cache_node 对象
        init_kmem_cache_node(n);
        // 初始化 slab cache 构造 kmem_cache 中的 node 数组
        s->node[node] = n;
    }
    return 1;
}

在 slab allocator 体系中,第二个被创立进去的 slab cache 就 boot_kmem_cache,在创立初始化 boot_kmem_cache 的时候,slab_state 就变为了 PARTIAL,示意 kmem_cache_node 构造的专属 slab cache 曾经创立进去了,能够利用它来动态分配 kmem_cache_node 对象了。

12.2 kmem_cache_node 构造的长期动态创立

正如后面大节中所介绍的,在 slab allocator 体系中第一个被内核创立进去的 slab cache 正是 boot_kmem_cache_node,而它自身就是一个 slab cache,专门用于调配 kmem_cache_node 对象。

而创立一个 slab cache 最外围的就是要为其调配 struct kmem_cache 构造(slab cache 在内核中的数据结构)还有就是 slab cache 在 NUMA 节点的缓存构造 kmem_cache_node。

而针对 struct kmem_cache 构造内核曾经通过定义动态构造 boot_kmem_cache_node 解决了。

static __initdata struct kmem_cache boot_kmem_cache_node;

而针对 kmem_cache_node 构造,内核中既没有定义这样一个静态数据构造,也没有一个 slab cache 专门治理,所以内核会通过这里的 early_kmem_cache_node_alloc 函数来创立 kmem_cache_node 对象。

留神:这里是为 boot_kmem_cache_node 这个动态的 slab cache 初始化它的 NUMA 节点缓存数组。

struct kmem_cache {
    // slab cache 中 numa node 中的缓存,每个 node 一个
    struct kmem_cache_node *node[MAX_NUMNODES];
}
static void early_kmem_cache_node_alloc(int node)
{
    // slab 的实质就是一个或者多个物理内存页 page,这里用于指向 slab 所属的 page。// 如果 slab 是由多个物理页 page 组成(复合页),这里指向复合页的首页
    struct page *page;
    // 这里次要为 boot_kmem_cache_node 初始化它的 node cache 数组
    // 这里会动态创立指定 node 节点对应的 kmem_cache_node 构造
    struct kmem_cache_node *n;

    // 此时 boot_kmem_cache_node 这个 kmem_cache 构造曾经初始化好了(参见第 9 大节的内容)。// 依据 kmem_cache 构造中的 kmem_cache_order_objects oo 属性向指定 node 节点所属的搭档零碎申请 2^order 个内存页 page
    // 这里返回复合页的首页,目标是为 kmem_cache_node 构造调配 slab,后续该 slab 会挂在 kmem_cache_node 构造中的 partial 列表中
    page = new_slab(kmem_cache_node, GFP_NOWAIT, node);

    // struct page 构造中的 freelist 指向 slab 中第一个闲暇的对象
    // 这里的对象就是  struct kmem_cache_node 构造
    n = page->freelist;
#ifdef CONFIG_SLUB_DEBUG
    // 依据 slab cache 中的 flag 初始化 kmem_cache_node 对象
    init_object(kmem_cache_node, n, SLUB_RED_ACTIVE);
#endif
    // 从新设置 slab 中的下一个闲暇对象。// 这里是获取对象 n 中的 free_pointer 指针, 指向 n 的下一个闲暇对象
    page->freelist = get_freepointer(kmem_cache_node, n);
    // 示意 slab 中曾经有一个对象被应用了
    page->inuse = 1;
    // 这里能够看出 boot_kmem_cache_node 的 NUMA 节点缓存在这里初始化的时候
    // 内核会为每个 NUMA 节点申请一个 slab,并缓存在它的 partial 链表中
    // 并不是缓存在 boot_kmem_cache_node 的本地 cpu 缓存中
    page->frozen = 0;
    // 这里的 kmem_cache_node 指的是 boot_kmem_cache_node
    // 初始化 boot_kmem_cache_node 中的 node cache 数组
    kmem_cache_node->node[node] = n;
    // 初始化 node 节点对应的 kmem_cache_node 构造
    init_kmem_cache_node(n);
    // kmem_cache_node 构造中的 nr_slabs 计数加 1,total_objects 加 page->objects
    inc_slabs_node(kmem_cache_node, node, page->objects);
    // 将新创建进去的 slab(page 示意),增加到对象 n(kmem_cache_node 构造)中的 partial 列表头部
    __add_partial(n, page, DEACTIVATE_TO_HEAD);
}

当 boot_kmem_cache_node 被初始化之后,它的整个构造如下图所示:

12.3 将长期动态的 kmem_cache 构造变为正式的 slab cache

流程到这里 boot_kmem_cache,boot_kmem_cache_node 这两个动态构造就曾经被初始化好了,当初内核就能够通过他们来动静的创立 kmem_cache 对象和 kmem_cache_node 对象了。

然而这里的 boot_kmem_cache 和 boot_kmem_cache_node 只是长期的 kmem_cache 构造,目标是在 slab allocator 体系初始化的时候用于动态创立 kmem_cache(slab cache),kmem_cache_node(slab cache)。

// 全局变量,用于专门治理 kmem_cache 对象的 slab cache
// 定义在文件:/mm/slab_common.c
struct kmem_cache *kmem_cache;

// 全局变量,用于专门治理 kmem_cache_node 对象的 slab cache
// 定义在文件:/mm/slub.c
static struct kmem_cache *kmem_cache_node;

既然是长期的构造,所以这里须要创立两个最终的全局 kmem_cache 构造,而后将这两个动态长期构造深拷贝到最终的全局 kmem_cache 构造中。

static struct kmem_cache * __init bootstrap(struct kmem_cache *static_cache)
{
    int node;
    // kmem_cache 指向专门治理 kmem_cache 对象的 slab cache
    // 该 slab cache 当初曾经全副初始化结束,能够利用它动静的调配最终的 kmem_cache 对象
    struct kmem_cache *s = kmem_cache_zalloc(kmem_cache, GFP_NOWAIT);
    struct kmem_cache_node *n;
    // 将动态的 kmem_cache 对象,比方:boot_kmem_cache,boot_kmem_cache_node
    // 深拷贝到最终的 kmem_cache 对象 s 中
    memcpy(s, static_cache, kmem_cache->object_size);

    // 开释本地 cpu 缓存的 slab
    __flush_cpu_slab(s, smp_processor_id());
    // 遍历 node cache 数组,修改 kmem_cache_node 构造中 partial 链表中蕴含的 slab(page 示意)对应 page 构造的 slab_cache 指针
    // 使其指向最终的 kmem_cache 构造,之前在 create_boot_cache 中指向的动态 kmem_cache 构造,这里须要修改
    for_each_kmem_cache_node(s, node, n) {
        struct page *p;

        list_for_each_entry(p, &n->partial, slab_list)
            p->slab_cache = s;
    }
    // 将最终的 kmem_cache 构造退出到全局 slab cache 链表中
    list_add(&s->list, &slab_caches);
    return s;
}

12.4 为什么要先创立 boot_kmem_cache_node 而不是 boot_kmem_cache

当初对于 slab alloactor 体系的初始化流程笔者就为大家全副介绍完了,最初咱们借用这个问题,再对这个流程做一个简略的总体回顾。

首先 slab cache 创立要依赖两个外围的数据机构,kmem_cache,kmem_cache_node:

其中 kmem_cache 构造是 slab cache 在内核中的数据结构,同样也须要被一个专门的 slab cache 所治理,然而在内核初始化阶段 slab 体系还未建设,所以内核通过定义两个部分动态变量来解决 kmem_cache 构造的创立问题。

  static __initdata struct kmem_cache boot_kmem_cache,
        boot_kmem_cache_node;

随后内核会在 calculate_size 函数中初始化 struct kmem_cache 构造中的外围属性。具体内容可回顾上篇文章的《6 slab 对象的内存布局》大节的内容。

当初 kmem_cache 构造的问题解决了,然而这两个 slab cache 中的 kmem_cache_node 构造的问题又来了。

所以内核决定首先创立 boot_kmem_cache_node,并通过 early_kmem_cache_node_alloc 函数为 boot_kmem_cache_node 创立 kmem_cache_node 构造。

当 boot_kmem_cache_node 被创立进去之后,内核就能够动静的调配 kmem_cache_node 对象了。

所以最初创立 boot_kmem_cache,在遇到 kmem_cache_node 构造创立的时候,间接应用 boot_kmem_cache_node 进行动态创建。

最初通过 bootstrap 将这两个长期动态的 slab cache : boot_kmem_cache,boot_kmem_cache_node 深拷贝到最终的全局 slab cache 中:

// 全局变量,用于专门治理 kmem_cache 对象的 slab cache
// 定义在文件:/mm/slab_common.c
struct kmem_cache *kmem_cache;

// 全局变量,用于专门治理 kmem_cache_node 对象的 slab cache
// 定义在文件:/mm/slub.c
static struct kmem_cache *kmem_cache_node;

从此以后,内核就能够动态创建 slab cache 了。

总结

本文笔者基于内核 5.4 版本,从源码角度具体探讨了 slab cache 的创立初始化过程,创立流程如下图所示:

通过该流程的创立之后,咱们失去了如下图所示的 slab cache 架构:

在这个过程中,笔者又近一步从源码角度介绍了内核具体是如何对 slab 对象进行内存布局的。

在这个内存布局的根底上,笔者又近一步开展了内核如何计算一个 slab 到底须要多少个物理内存页,以及一个 slab 到底可能包容多少内存块的相干逻辑。

最初咱们介绍了 slab cache 在内核中的数据结构 struct kmem_cache 里的 min_partial,cpu_partial 的计算逻辑。以及 slab cache 的 cpu 缓存构造 cpu_slab 以及 NUMA 节点缓存构造 node[MAX_NUMNODES] 的具体初始化过程。

/*
 * Slab cache management.
 */
struct kmem_cache {

    // slab cache 在 numa node 中缓存的 slab 个数下限,slab 个数超过该值,闲暇的 empty slab 则会被回收至搭档零碎
    unsigned long min_partial;

    // slab cache 中 numa node 中的缓存,每个 node 一个
    struct kmem_cache_node *node[MAX_NUMNODES];

#ifdef CONFIG_SLUB_CPU_PARTIAL
    // 限定 slab cache 在每个 cpu 本地缓存 partial 链表中所有 slab 中闲暇对象的总数
    // cpu 本地缓存 partial 链表中闲暇对象的数量超过该值,则会将 cpu 本地缓存 partial 链表中的所有 slab 转移到 numa node 缓存中。unsigned int cpu_partial;

    // 每个 cpu 领有一个本地缓存,用于无锁化疾速调配开释对象
    struct kmem_cache_cpu __percpu *cpu_slab;
#endif

};

在介绍完 slab cache 的整个创立流程之后,笔者在本文的最初一个大节里又具体的为大家介绍了整个 slab allocator 体系的初始化过程,并从源码实现上,看到了内核是如何解决这个先有鸡还是先有蛋的问题

好了,本文的内容就到这里了,在下篇文章中,笔者会带大家持续深刻到内核源码中,去看一下 slab cache 是如何进行内存调配的~~~

正文完
 0