# 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 的示意图如下所示：

![](/files/MVOIxW1ulYn9uZ7e6mGt)

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;

![](/files/nAUkB9DtIQHdJ0ubOl2A)

综合起来看，一个缓存 `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;

![](/files/tp0Mj3futUuQVUAqLtwK)

## 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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://s3.shizhz.me/linux-mm/3.2-wu-li-nei-cun/3.2.5-slab-slub-slob.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
