# 3.2.5 SLAB/SLUB/SLOB

上一节我们讨论了使用Buddy System进行连续内存页面的分配，但对于使用内存的程序而言，Buddy System 还存在如下问题：

* 粒度太大：Buddy System一次最少也要分配一页内存，通常情况下是4KB, 这对于程序而言还是太大了，我们需要一种更加细致的方式来对内存进行分配与释放。
* 缺乏语义：程序在使用内存时考虑的通常也不会是“物理内存页”这种底层概念，而是程序中定义的各种具备业务意义的数据结构与对象；不仅如此，对象的初始化与释放的逻辑有时比内存分配更加耗时。
* 效率偏低：Buddy System在分配与释放内存时会对Buddy进行拆分与合并，在频繁的内存申请与释放的场景下这将非常影响性能。

为了解决这些问题，内核基于Buddy System构建了一个“对象分配系统”，该系统叫着SLAB Allocator, SLAB以对象为基本单位进行分配与释放，并且为对象提供了缓存机制，从而一举解决了上述的各种问题。

> SLAB详细思路可以参考论文：[The Slab Allocator: An Object-Caching Kernel Memory Allotor](https://people.eecs.berkeley.edu/~kubitron/courses/cs194-24-S13/hand-outs/bonwick_slab.pdf)
>
> SLUB是SLAB的改进版本，本节后续内容我们将基于SLUB的代码来讨论具体实现，但依然使用SLAB来描述对应的算法和概念。
>
> SLOB是用于嵌入式等内存容量不大的场景下的对象分配算法，这里我们不做介绍。

## 3.2.5.1 对象（Object） <a href="#org9a182bb" id="org9a182bb"></a>

> 对象（Object）一词常用于面向对象编程语言之中，从技术上讲，一个对象是一组数据（属性）与行为（方法）的封装；从业务上讲，对象用来对业务概念进行建模，以便更好地通过模块化地方式构建系统。但这里我们所说的对象确不是这个概念，此处的对象指满足内核内存申请时的任意大小的一块连续内存。

SLAB通过两个维度来区分对象：

* 用途，例如表示对象是用于通用的目的（例如用于内核的各种数据结构），还是用于特定目的（例如用于DMA）；
* 大小，从内存的视角上来看，所谓对象就是固定大小的一块连续内存，SLAB将对象通过大小进行区分，并将相同大小的对象组织在一起进行管理；

从逻辑上讲，一个SLAB Allocator管理的就是一个 <用途，大小> 形成的对象集合，Linux中所有的SLAB信息记录在 `/proc/slabinfo` 文件中，我们可以看一下其中的内容：

```
❯ sudo cat /proc/slabinfo
slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
kmalloc-8k           204    208   8192    4    8 : tunables    0    0    0 : slabdata     52     52      0
kmalloc-4k           709    752   4096    8    8 : tunables    0    0    0 : slabdata     94     94      0
kmalloc-2k          1266   1312   2048   16    8 : tunables    0    0    0 : slabdata     82     82      0
kmalloc-1k          2827   3008   1024   32    8 : tunables    0    0    0 : slabdata     94     94      0
kmalloc-512        54469  57856    512   32    4 : tunables    0    0    0 : slabdata   1808   1808      0
kmalloc-256        21418  21536    256   32    2 : tunables    0    0    0 : slabdata    673    673      0
kmalloc-192        43876  48867    192   21    1 : tunables    0    0    0 : slabdata   2327   2327      0
kmalloc-128         2157   2240    128   32    1 : tunables    0    0    0 : slabdata     70     70      0
kmalloc-96          2745   2772     96   42    1 : tunables    0    0    0 : slabdata     66     66      0
kmalloc-64         14032  14720     64   64    1 : tunables    0    0    0 : slabdata    230    230      0
kmalloc-32         22733  23040     32  128    1 : tunables    0    0    0 : slabdata    180    180      0
kmalloc-16         14485  14848     16  256    1 : tunables    0    0    0 : slabdata     58     58      0
kmalloc-8          10691  10752      8  512    1 : tunables    0    0    0 : slabdata     21     21      0
kmem_cache_node      576    576     64   64    1 : tunables    0    0    0 : slabdata      9      9      0
kmem_cache           352    352    256   32    2 : tunables    0    0    0 : slabdata     11     11      0
```

这里展示了系统中部分的 slab 信息，其中 `kmalloc` 是内核运行时申请内存的通用slab, 内核可能申请任意大小的内存，为了满足各种应用场景，内核预备了各种大小的 kmalloc slab, 从最小的8Byte一直到最大的8KB, 大小呈几何级数分布，并且各个 slab 的大小都满足 2n。内核申请内存时需要指定内存大小，然后系统找到满足要求的最小的 `kmalloc slab` 来进行分配。

为何 kmalloc slab 的大小呈几何级数增长呢？原因是这样的设定能够尽可能地减少内存的内部碎片（Internal Fragmentation），因为不管 slab 使用了多少个物理内存页，都能够被 slab 的对象大小所整除；另外由于相邻 slab 的大小相差两倍，所以任何分配出去的对象的使用率都会超过50%。例如一个内核的结构体刚好是17字节，满足该大小的最小的 slab 是 kmalloc-32, 这样分配出去的对象最终使用了17 字节，浪费了15字节，浪费率低于50%.

## 3.2.5.2 Slab <a href="#orge722230" id="orge722230"></a>

一个Slab包含一个或多个连续的物理页面，然后将这些页面均分成固定的大小的各等份，每一等份就是一个对象，各对象通过链表串起来。有关slab 的信息封装在 `page` 中，对应的字段如下：

```
/* file: include/linux/mm_types.h */

struct page {
    union {
        struct { /* slab, slob and slub */
            union {
                /* slab 列表，slab 可能在 partial */
                struct list_head slab_list;
                struct { /* Partial pages */
                    struct page *next;
#ifdef CONFIG_64BIT
                    int pages; /* Nr of pages left */
                    int pobjects; /* Approximate count */
#else
                    short int pages;
                    short int pobjects;
#endif
                };
            };
            /* 使用该页作为 slab 的 kmem_cache, 通过文件 slub.c 中的函数 allocate_slab() 设置 */
            struct kmem_cache *slab_cache; /* not slob */
            /* 当页面用于 slab 缓存时，slab 的首页对应的 page 的该字段会指向整个 slab 的空闲对象列表。
             * 但当 slab 当前正在被 kmem_cache_cpu 使用时，page 的该字段会设置为 NULL, 而 kmem_cache_cpu 中的 freelist 字段会指向 slab 的空闲对象列表 */
            void *freelist; /* first free object */
            union {
                void *s_mem; /* slab: first object */
                /* 因为 counters 与下面包含 inuse, objects, frozen 字段的结构体是 union 关系，所以很多时候需要新建 page 然后对后面三个字段赋值时，直接将 counters 的值付过去就 OK 了 */
                unsigned long counters; /* SLUB */
                struct { /* SLUB */
                    /* 记录被使用的对象，但是初始值与 objects 相同 */
                    unsigned inuse : 16;
                    /* 记录 slab 中包含 object 的总数，即为 kmem_cache 中 kmem_cache_order_objects 中低 15 位表示的值。该值在 slub.c 中的函数 allocate_slab 中设置 */
                    unsigned objects : 15;
                    /* 标记该 slab 是否被某个 cpu “锁定”，如果处于 frozen 状态，那么只有对应的CPU 能够从该slab中分配对象，其他CPU 只能往该页面释放对象。初始值设置为 1 */
                    unsigned frozen : 1;
                };
            };
        };
} _struct_page_alignment;
```

如果页面被分配用于 slab, 那么这些字段就会被设置。其中 `kmem_cache` 与注释中提到的 `kmem_cache_cpu` 等结构会在下一节介绍。内核创建一个slab的逻辑也很直观：首先向Buddy System申请一定数量的连续页面，然后初始化对象并构建好对象列表。代码如下：

```
/* file: mm/slub.c */

static struct page *allocate_slab(struct kmem_cache *s, gfp_t flags, int node)
{
    struct page *page;
    /* 结构 kmem_cache_order_objects 中记录着一个 slab 应该申请的页面数量与总的对象数量，两个量封装在一个字段 unsigned int x 中，其中低16位表示对象总数，高位表示连续页面的阶，即需要分配2^(oo.x >> 16)个连续页面 */
    struct kmem_cache_order_objects oo = s->oo;
    /* 传给Buddy System的各类Flag, 这里我们删掉了对该参数的初始化逻辑 */
    gfp_t alloc_gfp;
    /* 初始化对象列表时使用的指针变量 */
    void *start, *p, *next;
    int idx;
    bool shuffle;

    /* 分配连续 2^(oo.x>>OO_SHIFT) 个连续页面，其中 OO_SHIFT 为 16  */
    page = alloc_slab_page(s, alloc_gfp, node, oo);
    if (unlikely(!page)) {
        /* 如果 buddy system 没有足够的连续物理页，则减少 oo 的数值再尝试一次 */
        oo = s->min;
        alloc_gfp = flags;
        page = alloc_slab_page(s, alloc_gfp, node, oo);
        /* s->min 是实例化一个 slab 所需要的最小的内存页数量，如果依旧无法满足的话就直接退出了 */
        if (unlikely(!page))
            goto out;
        stat(s, ORDER_FALLBACK);
    }

    /* oo.x 的低 15 位表示该 slab 中可以存放的 object 总数 */
    /* 函数oo_objects 就是取出 oo.x 的低16位的数字，即 oo.x&(1<<16 -1) 的值*/
    page->objects = oo_objects(oo);

    /* 将 kmem_cache 记录到第一个内存页中，kmem_cache 在下一节介绍 */
    page->slab_cache = s;
    /* 设置 page 的标记位，标记该页面用于 slab */
    __SetPageSlab(page);

    start = page_address(page);

    shuffle = shuffle_freelist(s, page);

    /* if block 中构建对象列表 */
    if (!shuffle) {
        /* 初始化第一个对象，因为 for 循环中处理的都是下一个对象 */
        start = fixup_red_left(s, start);
        /* 如果为 slab 配置了对象的初始化函数，则会在函数 setup_object 调用，对每个对象进行初始化  */
        start = setup_object(s, page, start);
        /* slab 中空闲对象列表的地址为第一个对象 */
        page->freelist = start;
        /* 初始化 slab 中的空闲对象列表 */
        for (idx = 0, p = start; idx < page->objects - 1; idx++) {
            /* s->size 表示每个对象的大小，这里计算出下一个对象的地址 */
            next = p + s->size;
            /* 初始化下一个对象 */
            next = setup_object(s, page, next);
            /* 建立空闲对象列表的单链表，其中 p 为当前对象，next 为下一个对象，将 next 的地址写入 p+s.offset 位置处 */
            set_freepointer(s, p, next);
            p = next;
        }
        /* 链表最后一个元素的next属性指向 NULL */
        set_freepointer(s, p, NULL);
    }

    page->inuse = page->objects;
    page->frozen = 1;

out:
    if (!page)
        return NULL;

    return page;
}
```

在前文中我们提到过slab中的对象就是一个固定大小的连续内存块，通过对象列表的构建逻辑我们可以对此有更深刻的理解，内核并没有单独定义一个结构体来封装对象信息，在构建对象列表时，指向下一个空闲对象的指针也是直接存放在该对象的内存地址内部，因为对象还没有被分配出去时内存是不会被使用的，而被分配之后也不需要该指针了，这是一个比较精巧的设计。一个初始化完毕的 slab 的示意图如下所示：

![](https://640510796-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Me-3_JXLYv-4hrEXyDb%2Fuploads%2FP9C9bCXL72n4DgzYFI1m%2Fslab.png?alt=media\&token=d636e662-4115-460e-9e6b-4ada49d025a9)

freelist 总是指向slab 中第一个空闲对象，也是 slab 分配与释放对象的入口点，这在后续章节会详细介绍。

## 3.2.5.3 缓存（Cache） <a href="#orgdec88b5" id="orgdec88b5"></a>

前面讨论了 slab 如何组织管理对象，但我们还面临如下问题：

* 性能问题：多核系统中，如果多个CPU都使用共享的slab, 那么在分配与释放对象时会面临强烈的竞争问题，从而极大影响系统性能；
* 如何管理多个slab: 上一节只讲到了内核如何初始化一个slab, slab 从Buddy System中分配的内存页面是固定的，当slab中的对象耗尽之后内核需要重新创建新的slab, 如何管理这些相同类型的 slab 也是一个问题；

内核引入了缓存的概念来解决这些问题。一个缓存使用一个 `kmem_cache` 来表示，该结构体的定义如下：

```
/* file: include/linux/slub_def.h */

struct kmem_cache {
    /* 为每个CPU单独维护的slab, 避免竞争带来的冲突，这也是分配对象时的快速通道 */
    struct kmem_cache_cpu __percpu *cpu_slab;
    /* 包含了 metadata 的对象大小 */
    unsigned int size; /* The size of an object including metadata */
    /* 不包含 metadata 的对象大小 */
    unsigned int object_size; /* The size of an object without metadata */
    /* 空闲对象中保存 next 指针的偏移 */
    unsigned int offset; /* Free pointer offset */
#ifdef CONFIG_SLUB_CPU_PARTIAL
    /* Number of per cpu partial objects to keep around */
    unsigned int cpu_partial;
#endif
    /* kmem_cache_order_objects 用来存放一个 slab 中的实际物理页数，以及对象的总数。*/
    struct kmem_cache_order_objects oo;
    struct kmem_cache_order_objects max;
    struct kmem_cache_order_objects min;
    /* 对象的构造函数，如果非空的话，slab 在初始化时就会通过该函数初始化每个对象 */
    void (*ctor)(void *);
    unsigned int inuse; /* Offset to metadata */
    unsigned int align; /* Alignment */
    const char *name; /* Name (only for display!) */
    struct list_head list; /* List of slab caches */

    /* 存放缓存中的备用 slab,  */
    struct kmem_cache_node *node[MAX_NUMNODES];
};
```

我们在前文讨论 slab 初始化时已经见过该结构体，并且已经使用过其中的某些字段，例如用来确定 slab 页面数量与对象数量的字段 `struct kmem_cache_order_objects oo;`

内核使用 `kmem_cache` 来管理同一类对象的所有 slab, 其中最重要的两个字段是 `struct kmem_cache_cpu __percpu * cpu_slab` 与 `struct kmem_cache_node * node[MAX_NUMNODES]`. 为了避免分配对象时的竞争问题， `kmem_cache` 为每个CPU都单独维护了一个slab 缓存，变量 `cpu_slab` 的修饰符 `__percpu` 就是告诉编译器，该字段是 per cpu 的。该结构体定义如下：

```
/* file: include/linux/slub_def.h  */

struct kmem_cache_cpu {
    /* 指向下一个空闲对象 */
    void **freelist; /* Pointer to next available object */
    /* 事务 ID, 用来做同步。kmem_cache_cpu 是分配对象的快速路径，因此性能是首要考虑因素，所以此处没有考虑使用加锁的方式来进行同步 */
    unsigned long tid; /* Globally unique transaction id */
    /*
     * 指向当前 slab 的首个物理页面
     */
    struct page *page; /* The slab from which we are allocating */
};
```

内核申请对象时，CPU都会首先尝试从自己的缓存对象 `kmem_cache_cpu` 中进行分配，这是效率最高的分配通道。

既然CPU使用的是自己独占的缓存对象了，那么为什么还需要字段 `tid` 来做同步呢？因为虽然CPU不用与其它CPU竞争资源，但在对象分配过程中调度器可能切入触发任务切换，当前任务在下次被调度时可能就跑到了其它CPU上去执行了；或者当前CPU可能发生中断，而中断处理程序可能会从同一个Slab缓存中申请对象，因此我们需要一种机制来保证对象分配发生在同一个CPU上，并且分配过程中不会被干扰，slab 通过 tid 来实现这一点。tid 是一个全局递增的数字，slab 在每次开始分配对象前会读取到当前的tid数值，完成分配后将 tid 递增，然后通过原子操作CAS(Compare And Set) 来同时更新 freelist 与 tid 两个值，这样如果中途有其它的分配操作乱入的话，CAS 操作就会失败，slab 就会重头开始重新进行分配，直到成功为止。该段逻辑在函数 `slab_alloc_node` 中，这里我们在不深入具体分配逻辑的情况下看一下同步策略：

```
/* file: mm/slub.c */

static __always_inline void *slab_alloc_node(struct kmem_cache *s,
                                            gfp_t gfpflags, int node,
                                            unsigned long addr,
                                            size_t orig_size)
{
    void *object;
    struct kmem_cache_cpu *c;
    struct page *page;
    unsigned long tid;

redo:
    /* 1. 分配逻辑开始时获取到当前 tid */
    do {
        tid = this_cpu_read(s->cpu_slab->tid);
        c = raw_cpu_ptr(s->cpu_slab);
    } while (IS_ENABLED(CONFIG_PREEMPTION) &&
            unlikely(tid != READ_ONCE(c->tid)));

    /* 从当前 kmem_cache_cpu 的 free list 中拿到第一个对象 */
    /* 2. 分配对象并计算下一个空闲对象的地址，即freelist 的新值*/
    object = c->freelist;
    void *next_object = get_freepointer_safe(s, object);

    /* 3. 通过 CMPXCHG 指令设置 freelist 与 tid 的新值, 如果此时的 tid 与 s->cpu_slab->tid 不同，则说明发生了干扰，代码跳转到 redo 重新开始分配逻辑 */
    if (unlikely(!this_cpu_cmpxchg_double(
                        s->cpu_slab->freelist, s->cpu_slab->tid, object,
                        tid, next_object, next_tid(tid)))) {
        goto redo;
    }

out:
    return object;
}
```

如果CPU的本地缓存没有空闲对象，那么就需要从其它地方拿一个可用的slab过来，这个地方就是 `kmem_cache_node`, 这相当于 `kmem_cache` 的一个中心化仓库，用来管理暂时没有被 `kmem_cache_cpu` 使用到的slab. 前文介绍NUMA架构时提到过内存会根据CPU的拓扑结果划分成不同的Node, 为了区分从不同Node 分配过来的 slab, kmemcache 也根据Node对 `kmem_cache_node` 进行了区分，该字段是一个与Node数量相等的数组。

`kmem_cache_node` 的定义如下：

```
/* file: mm/slab.h */

struct kmem_cache_node {
    spinlock_t list_lock;

#ifdef CONFIG_SLUB
    unsigned long nr_partial;
    /* 指向 partial slab 的链表，partial 的意思是 slab 中还有剩余的空闲对象可用 */
    struct list_head partial;
#ifdef CONFIG_SLUB_DEBUG
    atomic_long_t nr_slabs;
    atomic_long_t total_objects;
    /* 指向 full slab 的链表，full 的意思是 slab 中所有的对象都已经被分配出去了，没有可用的空闲对象 */
    struct list_head full;
#endif
#endif
};
```

这里我们只留下了与 `SLUB` 算法相关的部分，一个 `kmem_cache_node` 实际上就包含了两个列表，一个是partial slab列表，一个是full slab列表。示意图如下：&#x20;

![](https://640510796-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Me-3_JXLYv-4hrEXyDb%2Fuploads%2FatBbWdNPTBP3FTBlLBX8%2Fkmem_cache_node.png?alt=media\&token=9b6c95ce-07a3-40cc-bd78-3ec768de9b25)

综合起来看，一个缓存 `kmem_cache` 管理着一个特定类型的所有slab, `kmem_cache_cpu` 中包含着当前CPU正在使用的slab, 任何对象分配的请求都直接从该slab中进行分配；而 `kmem_cache_node` 用来充当 `kmem_cache_cpu` 与Buddy System之间的缓冲区，当 `kmem_cache_cpu` 中的空闲对象分配完了之后，会将 slab 放入 `kmem_cache_node` 的full 列表，并从 partial 列表获取新的 slab 来使用，而当 `kmem_cache_node` 也没有多余的 slab 时，便会从Buddy System 中分配新的 slab 进行补充。同时，如果系统对该类对象的使用量下降，导致 `kmem_cache_node` 有很多完全空闲的 slab 时，系统也会酌情返回一些 slab 给Buddy System, 以缓解系统的总体内存压力。

所有的 `kmem_cache` 保留在全局变量 `kmalloc_caches` 中，代码如下：

```
/* file: mm/slab_common.c */

struct kmem_cache *kmalloc_caches[NR_KMALLOC_TYPES][KMALLOC_SHIFT_HIGH +
                                                    1] __ro_after_init = {
/* initialization for https://bugs.llvm.org/show_bug.cgi?id=42570 */
};


/* file: include/linux/slab.h */
/* 通过内存用途对 kmem_cache 进行分类 */
enum kmalloc_cache_type {
KMALLOC_NORMAL = 0,
KMALLOC_RECLAIM,
#ifdef CONFIG_ZONE_DMA
KMALLOC_DMA,
#endif
NR_KMALLOC_TYPES
};
```

总结起来，系统所有 kmemcache 的示意图如下：&#x20;

![](https://640510796-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Me-3_JXLYv-4hrEXyDb%2Fuploads%2FZzyBgjPLBmnUPwzR1O2U%2Fkmem_caches.png?alt=media\&token=7eec39ef-b4ad-46ae-82ec-15f9b64b03c4)

## 3.2.5.4 分配（Allocation) 与释放（Free） <a href="#org8b92d2a" id="org8b92d2a"></a>

通过前文对几个核心概念的分析，我们可以总结出关于SLAB的一些关键设计思想：

* 懒加载（Lazy Loading）：缓存 `kmem_cache` 初始化时只会创建出 `kmem_cache_cpu` 与 `kmem_cache_node`, 但对slab的初始化会推迟到对象分配时才会发生，这可以降低系统总体的内存压力，因为除了SLAB 系统，整个内核在很多其它地方还需要使用内存；
* 本地化（Locality）：为了减少竞争，每个CPU都持有一个自己的 slab, 做到独立运作不冲突；

本节我们将继续深入，探讨SLAB系统分配对象的详细流程，以及如何与Buddy System进行交互的。内核申请内存的入口函数是 `kmalloc`:

```
/* file: include/linux/slab.h */

/* 函数有两个参数，size 表示连续的内存大小；flags用来指定内存种类（即内存分区）与内核分配内存时的行为（例如是否允许分配失败）。  */
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
    /* 删除部分无关代码 */

    return __kmalloc(size, flags);
}

/* file: mm/slab.c */
void *__kmalloc(size_t size, gfp_t flags)
{
    /* _RET_IP_ 是 GCC 的一个内置函数，参考 https://gcc.gnu.org/onlinedocs/gcc/Return-Address.html */
    return __do_kmalloc(size, flags, _RET_IP_);
}
```

函数简单地调用 `__kmalloc` 并最终调用到 `__do_kmalloc`, 该函数通过内存大小与 `flags` 中的内存种类找到对应的 `kmem_cache`, 然后从该缓存中分配对象：

```
/* file: mm/slab.c */

static __always_inline void *__do_kmalloc(size_t size, gfp_t flags,
                                          unsigned long caller)
{
    struct kmem_cache *cachep;
    void *ret;

    /* KMALLOC_MAX_CACHE_SIZE 为 page * 2, 超过该大小的内存分配请求需要使用 page allocator */
    if (unlikely(size > KMALLOC_MAX_CACHE_SIZE))
        return NULL;
    /* 根据 size 和内存类型找出对应的 kmem_cache, 然后从 cachep 中分配空闲对象。函数 kmalloc_slab 就是从全局变量 kmalloc_caches 找出对应的缓存 */
    cachep = kmalloc_slab(size, flags);
    if (unlikely(ZERO_OR_NULL_PTR(cachep)))
        return cachep;

    /* 从缓存中分配对象 */
    ret = slab_alloc(cachep, flags, size, caller);

    return ret;
}
```

从缓存中分配对象有两条路径：

* 快路径（fastpath）：直接从 `kmem_cache_cpu` 中的slab 中分配到对象
* 慢路径（slowpath）：无法直接从 `kmem_cache_cpu` 中分配到对象，需要先从 `kmem_cache_node` 的 partial 列表拿一个slab，甚至需要从Buddy System 中分配一个全新的 slab 来进行补充

快路径的逻辑在函数 `slab_alloc_node` 中，前文讨论 `kmem_cache_cpu` 的并发控制时已经探索过该函数，这里再着重看一下有关对象分配的逻辑：

```
/* file: mm/slub.c */

static __always_inline void *slab_alloc_node(struct kmem_cache *s,
                                             gfp_t gfpflags, int node,
                                             unsigned long addr,
                                             size_t orig_size)
{
    void *object;
    struct kmem_cache_cpu *c;
    struct page *page;
    unsigned long tid;

redo:
    do {
        tid = this_cpu_read(s->cpu_slab->tid);
        /* 拿到当前 CPU 的 kmem_cache_cpu  */
        c = raw_cpu_ptr(s->cpu_slab);
    } while (IS_ENABLED(CONFIG_PREEMPTION) &&
             unlikely(tid != READ_ONCE(c->tid)));

    /* 从 kmem_cache_cpu 的 freelist 中拿到第一个对象 */
    object = c->freelist;
    page = c->page;
    if (unlikely(!object || !page || !node_match(page, node))) {
        /* 进入慢路径进行分配。!object 表示 kmem_cache_cpu 的slab 中已经没有了空闲对象，!page 表示还没有为 kmem_cache_cpu 分配 slab, 不管哪种情况，都需要先搞定一个 slab 才能继续分配对象。  */
        object = __slab_alloc(s, gfpflags, node, addr, c);
    } else {
        /* 快路径分配成功，修改各个变量，同步原理在前文中已经讲过 */
        void *next_object = get_freepointer_safe(s, object);
        if (unlikely(!this_cpu_cmpxchg_double(
                         s->cpu_slab->freelist, s->cpu_slab->tid, object,
                         tid, next_object, next_tid(tid)))) {
            note_cmpxchg_failure("slab_alloc", s, tid);
            goto redo;
        }
    }

out:
    /* 对分配到的对象做一些边界检查等工作 */
    slab_post_alloc_hook(s, objcg, gfpflags, 1, &object);

    return object;
}
```

慢路径的逻辑在函数 `___slab_alloc` 中，其主要思想是先尝试着从 `kmem_cache_node` 的partial 列表获取一个slab, 不行的话就从Buddy System 申请一个全新的slab 来使用。主要代码如下：

```
/* file: mm/slub.c */

static void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
                           unsigned long addr, struct kmem_cache_cpu *c)
{
    void *freelist;
    struct page *page;

    page = c->page;
    if (!page) {
        /* 说明当前CPU的slab还为空，尝试分配一个新的 slab */
        if (unlikely(node != NUMA_NO_NODE &&
                     !node_isset(node, slab_nodes)))
            node = NUMA_NO_NODE;
        goto new_slab;
    }
/* 分配对象的代码块 */
redo:
    /* must check again c->freelist in case of cpu migration or IRQ */
    /* 再次检查 c->freelist，如果分配成功则跳转到 load_freelist 做后续处理 */
    freelist = c->freelist;
    if (freelist)
        goto load_freelist;

    /* 如果 kmem_cache_cpu 的 freelist 已经没有空闲对象，而 page 可能是重新分配的 slab, 该函数将 page 中的 freelist 转移出来。
     * 在 reload_freelist 部分会将该 freelist 设置到 kmem_cache_cpu 中。总体来说，该操作就是为了将新的slab设置到 kmem_cache_cpu 中
     */
    freelist = get_freelist(s, page);

    if (!freelist) {
        c->page = NULL;
        goto new_slab;
    }

load_freelist:
    /* 设置 slab 到 kmem_cache_cpu 中 */
    c->freelist = get_freepointer(s, freelist);
    c->tid = next_tid(c->tid);
    /* 分配成功，返回 */
    return freelist;

new_slab:
    /* 函数 new_slab_objects 会先检查 kmem_cache_node 的partial 列表，如果列表为空则从Buddy System 重新分配 slab */
    freelist = new_slab_objects(s, gfpflags, node, &c);

    if (unlikely(!freelist)) {
        slab_out_of_memory(s, gfpflags, node);
        return NULL;
    }

    page = c->page;
    if (likely(!kmem_cache_debug(s) && pfmemalloc_match(page, gfpflags)))
        goto load_freelist;

    /* 该函数会将新分配的 slab 放入 kmem_cache_node 的 partial 列表中 */
    deactivate_slab(s, page, get_freepointer(s, freelist), c);
    return freelist;
}
```

从Buddy System 分配与初始化slab 的函数在前面已经介绍过，这里不再讨论。

向缓存中释放对象时逻辑很简单，就是将该对象放入对应slab 的freelist 列表即可。但在如下几种情况下会调整slab 的位置：

* slab 在 kmemcachenode 的full 列表中时，需要将该slab 放入 partial 列表中
* slab 变成完全空闲状态时，即没有对象被使用，那么如果此时 kmemcachenode 有太多 slab 的话，需要将整个 slab 释放，将内存还给Buddy System
