在上篇文章 《深刻了解 slab cache 内存调配全链路实现》 中,笔者具体地为大家介绍了 slab cache 进行内存调配的整个链路实现,本文咱们就来到了 slab cache 最初的一部分内容了,当申请的内存应用结束之后,上面就该开释内存了。

在接下来的内容中,笔者为大家介绍一下内核是如何将内存块开释回 slab cache 的。咱们还是先从 slab cache 开释内存的内核 API 开始聊起~~~

内核提供了 kmem_cache_free 函数,用于将对象开释回其所属的 slab cache 中,参数 x 示意咱们要开释的内存块(对象)的虚拟内存地址,参数 s 指向内存块所属的 slab cache。

void kmem_cache_free(struct kmem_cache *s, void *x){    // 确保指定的是 slab cache : s 为对象真正所属的 slab cache    s = cache_from_obj(s, x);    if (!s)        return;    // 将对象开释会 slab cache 中    slab_free(s, virt_to_head_page(x), x, NULL, 1, _RET_IP_);}

1. 内存开释之前的校验工作

在开始开释内存块 x 之前,内核须要首先通过 cache_from_obj 函数确认内存块 x 是否真正属于咱们指定的 slab cache。不能将内存块开释到其余的 slab cache 中。

随后在 virt_to_head_page 函数中通过内存块的虚拟内存地址 x 找到其所在的物理内存页 page。而后调用 slab_free 将内存块开释回 slab cache 中。

通过虚拟内存地址寻找物理内存页 page 的过程波及到的背景常识比较复杂,这个笔者前面会独自拎进去介绍,这里大家只须要简略理解 virt_to_head_page 函数的作用即可。
static inline struct kmem_cache *cache_from_obj(struct kmem_cache *s, void *x){    struct kmem_cache *cachep;    // 通过对象的虚拟内存地址 x 找到对象所属的 slab cache    cachep = virt_to_cache(x);    // 校验指定的 slab cache : s 是否是对象真正所属的 slab cache : cachep    WARN_ONCE(cachep && !slab_equal_or_root(cachep, s),          "%s: Wrong slab cache. %s but object is from %s\n",          __func__, s->name, cachep->name);    return cachep;}

virt_to_cache 函数首先会通过开释对象的虚拟内存地址找到其所在的物理内存页 page,而后通过 struct page 构造中的 slab_cache 指针找到 page 所属的 slab cache。

static inline struct kmem_cache *virt_to_cache(const void *obj){    struct page *page;    // 依据对象的虚拟内存地址 *obj 找到其所在的内存页 page    // 如果 slub 背地是多个内存页(复合页),则返回复合页的首页 head page    page = virt_to_head_page(obj);    if (WARN_ONCE(!PageSlab(page), "%s: Object is not a Slab page!\n",                    __func__))        return NULL;    // 通过 page 构造中的 slab_cache 属性找到其所属的 slub    return page->slab_cache;}

2. slab cache 在疾速门路下回收内存

static __always_inline void slab_free(struct kmem_cache *s, struct page *page,                      void *head, void *tail, int cnt,                      unsigned long addr){    if (slab_free_freelist_hook(s, &head, &tail))        do_slab_free(s, page, head, tail, cnt, addr);}

slab cache 回收内存相干的逻辑封装在 do_slab_free 函数中:

static __always_inline void do_slab_free(struct kmem_cache *s,                struct page *page, void *head, void *tail,                int cnt, unsigned long addr)
  • 参数 kmem_cache *s 示意开释对象所在的 slab cache,指定咱们要将对象开释到哪里。
  • 参数 page 示意开释对象所在的 slab,slab 在内核中应用 struct page 构造来示意。
  • 参数 head 指向开释对象的虚拟内存地址(起始内存地址)。
  • 该函数反对向 slab cache 批量的开释多个对象,参数 tail 指向批量开释对象中最初一个对象的虚拟内存地址。
  • 参数 cnt 示意开释对象的个数,也是用于批量开释对象
  • 参数 addr 用于 slab 调试,这里咱们不须要关怀。

slab cache 针对内存的回收流程其实和咱们在上篇文章 《深刻了解 slab cache 内存调配全链路实现》 中介绍的 slab cache 内存调配流程是类似的。

内存回收总体也是分为疾速门路 fastpath 和慢速门路 slow path,在 do_slab_free 函数中内核会首先尝试 fastpath 的回收流程。

如果开释对象所在的 slab 刚好是 slab cache 在本地 cpu 缓存 kmem_cache_cpu->page 缓存的 slab,那么内核就会间接将对象开释回缓存 slab 中。

static __always_inline void do_slab_free(struct kmem_cache *s,                struct page *page, void *head, void *tail,                int cnt, unsigned long addr){    void *tail_obj = tail ? : head;    struct kmem_cache_cpu *c;    // slub 中对象调配与开释流程的全局事务 id    // 既能够用来标识同一个调配或者开释的事务流程,也能够用来标识辨别所属 cpu 本地缓存    unsigned long tid;redo:    // 接下来咱们须要获取 slab cache 的 cpu 本地缓存    // 这里的 do..while 循环是要保障获取到的 cpu 本地缓存 c 是属于执行过程的以后 cpu    // 因为过程可能因为抢占或者中断的起因被调度到其余 cpu 上执行,所需须要确保两者的 tid 是否统一    do {        // 获取执行以后过程的 cpu 中的 tid 字段        tid = this_cpu_read(s->cpu_slab->tid);        // 获取 cpu 本地缓存 cpu_slab        c = raw_cpu_ptr(s->cpu_slab);        // 如果两者的 tid 字段不统一,阐明过程曾经被调度到其余 cpu 上了        // 须要再次获取正确的 cpu 本地缓存    } while (IS_ENABLED(CONFIG_PREEMPT) &&         unlikely(tid != READ_ONCE(c->tid)));    // 如果开释对象所属的 slub (page 示意)正好是 cpu 本地缓存的 slub    // 那么间接将对象开释到 cpu 缓存的 slub 中即可,这里就是疾速开释门路 fastpath    if (likely(page == c->page)) {        // 将对象开释至 cpu 本地缓存 freelist 中的头结点处        // 开释对象中的 freepointer 指向原来的 c->freelist        set_freepointer(s, tail_obj, c->freelist);        // cas 更新 cpu 本地缓存 s->cpu_slab 中的 freelist,以及 tid        if (unlikely(!this_cpu_cmpxchg_double(                s->cpu_slab->freelist, s->cpu_slab->tid,                c->freelist, tid,                head, next_tid(tid)))) {            note_cmpxchg_failure("slab_free", s, tid);            goto redo;        }        stat(s, FREE_FASTPATH);    } else        // 如果以后开释对象并不在 cpu 本地缓存中,那么就进入慢速开释门路 slowpath        __slab_free(s, page, head, tail_obj, cnt, addr);}

既然是疾速门路开释,那么在 do_slab_free 函数的开始首先就获取 slab cache 的本地 cpu 缓存构造 kmem_cache_cpu,为了保障咱们获取到的 cpu 本地缓存构造与运行以后过程所在的 cpu 是相符的,所以这里还是须要在 do .... while 循环内判断两者的 tid。这一点,笔者曾经在本文之前的内容里屡次强调过了,这里不在赘述。

内核在确保曾经获取了正确的 kmem_cache_cpu 构造之后,就会马上判断该开释对象所在的 slab 是否正是 slab cache 本地 cpu 缓存了的 slab —— page == c->page

如果是的话,间接将对象开释回缓存 slab 中,调整 kmem_cache_cpu->freelist 指向刚刚开释的对象,调整开释对象的 freepointer 指针指向原来的 kmem_cache_cpu->freelist 。

如果以后开释对象并不在 slab cache 的本地 cpu 缓存中,那么就会进入慢速门路 slowpath 开释内存。

3. slab cache 在慢速门路下回收内存

slab cache 在慢速门路下回收内存的逻辑比较复杂,因为这里波及到很多的场景,须要扭转开释对象所属 slab 在 slab cache 架构中的地位。

上面笔者会带大家一一梳理这些场景,咱们一起来看一下内核在这些不同场景中到底是如何解决的?

在开始浏览本大节的内容之前,倡议大家先回顾下 《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现》 一文中的 ”8. slab 内存开释原理“ 大节。

在将对象开释回对应的 slab 中之前,内核须要首先清理一下对象所占的内存,从新填充对象的内存布局复原到初始未应用状态。因为对象所占的内存此时蕴含了很多曾经被应用过的无用信息。这项工作内核在 free_debug_processing 函数中实现。

在将对象所在内存复原到初始状态之后,内核首先会将对象间接开释回其所属的 slab 中,并调整 slab 构造 page 的相干属性。

接下来就到简单的解决局部了,内核会在这里解决多种场景,并扭转 slab 在 slab cache 架构中的地位。

  1. 如果 slab 原本就在 slab cache 本地 cpu 缓存 kmem_cache_cpu->partial 链表中,那么对象在开释之后,slab 的地位不做任何扭转。
  2. 如果 slab 不在 kmem_cache_cpu->partial 链表中,并且该 slab 因为对象的开释刚好由一个 full slab 变为了一个 partial slab,为了利用局部性的劣势,内核须要将该 slab 插入到 kmem_cache_cpu->partial 链表中。

  1. 如果 slab 不在 kmem_cache_cpu->partial 链表中,并且该 slab 因为对象的开释刚好由一个 partial slab 变为了一个 empty slab,阐明该 slab 并不是很沉闷,内核会将该 slab 放入对应 NUMA 节点缓存 kmem_cache_node->partial 链表中,刀枪入库,马放南山。

  1. 如果不合乎第 2, 3 种场景,然而 slab 原本就在对应的 NUMA 节点缓存 kmem_cache_node->partial 链表中,那么对象在开释之后,slab 的地位不做任何扭转。

上面咱们就到内核的源码实现中,来一一验证这四种慢速开释场景。

static void __slab_free(struct kmem_cache *s, struct page *page,            void *head, void *tail, int cnt,            unsigned long addr){    // 用于指向对象开释回 slub 之前,slub 的 freelist    void *prior;    // 对象所属的 slub 之前是否在本地 cpu 缓存 partial 链表中    int was_frozen;    // 后续会对 slub 对应的 page 构造相干属性进行批改    // 批改后的属性会长期保留在 new 中,前面通过 cas 替换    struct page new;    unsigned long counters;    struct kmem_cache_node *n = NULL;    stat(s, FREE_SLOWPATH);    // free_debug_processing 中会调用 init_object,清理对象内存无用信息,从新复原对象内存布局到初始状态    if (kmem_cache_debug(s) &&     !free_debug_processing(s, page, head, tail, cnt, addr))        return;    do {        // 获取 slub 中的闲暇对象列表,prior = null 示意此时 slub 是一个 full slub,意思就是该 slub 中的对象曾经全副被调配进来了        prior = page->freelist;        counters = page->counters;        // 将开释的对象插入到 freelist 的头部,将对象开释回 slub        // 将 tail 对象的 freepointer 设置为 prior        set_freepointer(s, tail, prior);        // 将原有 slab 的相应属性赋值给 new page        new.counters = counters;        // 获取原来 slub 中的 frozen 状态,是否在 cpu 缓存 partial 链表中        was_frozen = new.frozen;        // inuse 示意 slub 曾经调配进来的对象个数,这里是开释 cnt 个对象,所以 inuse 要减去 cnt        new.inuse -= cnt;        // !new.inuse 示意此时 slub 变为了一个 empty slub,意思就是该 slub 中的对象还没有调配进来,全副在 slub 中        // !prior 示意因为本次对象的开释,slub 刚刚从一个 full slub 变成了一个 partial slub (意思就是该 slub 中的对象局部调配进来了,局部没有调配进来)        // !was_frozen 示意该 slub 不在 cpu 本地缓存中        if ((!new.inuse || !prior) && !was_frozen) {            // 留神:进入该分支的 slub 之前都不在 cpu 本地缓存中            // 如果配置了 CONFIG_SLUB_CPU_PARTIAL 选项,那么示意 cpu 本地缓存 kmem_cache_cpu 构造中蕴含 partial 列表,用于 cpu 缓存局部调配的 slub            if (kmem_cache_has_cpu_partial(s) && !prior) {                // 如果 kmem_cache_cpu 蕴含 partial 列表并且该 slub 刚刚由 full slub 变为 partial slub                // 解冻该 slub,后续会将该 slub 插入到 kmem_cache_cpu 的 partial 列表中                new.frozen = 1;            } else {                 // 如果 kmem_cache_cpu 中没有配置 partial 列表,那么间接开释至 kmem_cache_node 中                // 或者该 slub 由一个 partial slub 变为了 empty slub,调整 slub 的地位到 kmem_cache_node->partial 链表中                n = get_node(s, page_to_nid(page));                // 后续会操作 kmem_cache_node 中的 partial 列表,所以这里须要获取 list_lock                spin_lock_irqsave(&n->list_lock, flags);            }        }        // cas 更新 slub 中的 freelist 以及 counters    } while (!cmpxchg_double_slab(s, page,        prior, counters,        head, new.counters,        "__slab_free"));    // 该分支要解决的场景是:    // 1: 该 slub 原来不在 cpu 本地缓存的 partial 列表中(!was_frozen),然而该 slub 刚刚从 full slub 变为了 partial slub,须要放入 cpu-> partial 列表中    // 2: 该 slub 原来就在 cpu 本地缓存的 partial 列表中,间接将对象开释回 slub 即可    if (likely(!n)) {        // 解决场景 1        if (new.frozen && !was_frozen) {            // 将 slub 插入到 kmem_cache_cpu 中的 partial 列表中            put_cpu_partial(s, page, 1);            stat(s, CPU_PARTIAL_FREE);        }                // 解决场景2,因为之前曾经通过 set_freepointer 将对象开释回 slub 了,这里只须要记录 slub 状态即可        if (was_frozen)            stat(s, FREE_FROZEN);        return;    }        // 后续的逻辑就是解决须要将 slub 放入 kmem_cache_node 中的 partial 列表的情景    // 在将 slub 放入 node 缓存之前,须要判断 node 缓存的 nr_partial 是否超过了指定阈值 min_partial(位于 kmem_cache 构造)    // nr_partial 示意 kmem_cache_node 中 partial 列表中缓存的 slub 个数    // min_partial 示意 slab cache 规定 kmem_cache_node 中 partial 列表能够包容的 slub 最大个数    // 如果 nr_partial 超过了最大阈值 min_partial,则不能放入 kmem_cache_node 里    if (unlikely(!new.inuse && n->nr_partial >= s->min_partial))        // 如果 slub 变为了一个 empty slub 并且 nr_partial 超过了最大阈值 min_partial        // 跳转到 slab_empty 分支,将 slub 开释回搭档零碎中        goto slab_empty;    // 如果 cpu 本地缓存中没有配置 partial 列表并且 slub 刚刚从 full slub 变为 partial slub    // 则将 slub 插入到 kmem_cache_node 中    if (!kmem_cache_has_cpu_partial(s) && unlikely(!prior)) {        remove_full(s, n, page);        add_partial(n, page, DEACTIVATE_TO_TAIL);        stat(s, FREE_ADD_PARTIAL);    }    spin_unlock_irqrestore(&n->list_lock, flags);    // 剩下的状况均属于 slub 原来就在 kmem_cache_node 中的 partial 列表中    // 间接将对象开释回 slub 即可,无需扭转 slub 的地位,间接返回    return;slab_empty:    // 该分支解决的场景是: slub 太多了,将 empty slub 开释会搭档零碎    // 首先将 slub 从对应的治理链表上删除    if (prior) {        /*         * Slab on the partial list.         */        remove_partial(n, page);        stat(s, FREE_REMOVE_PARTIAL);    } else {        /* Slab must be on the full list */        remove_full(s, n, page);    }    spin_unlock_irqrestore(&n->list_lock, flags);    stat(s, FREE_SLAB);    // 开释 slub 回搭档零碎,底层调用 __free_pages 将 slub 所治理的所有 page 开释回搭档零碎    discard_slab(s, page);}

3.1 间接开释对象回 slab,调整 slab 相干属性

static void __slab_free(struct kmem_cache *s, struct page *page,            void *head, void *tail, int cnt,            unsigned long addr){    // 后续会对 slub 对应的 page 构造相干属性进行批改    // 批改后的属性会长期保留在 new 中,前面通过 cas 替换    struct page new;              ....... 省略 ..........    do {        prior = page->freelist;        counters = page->counters;        // 将对象间接开释回 slab 中,调整 slab 的 freelist 指针,以及对象的 freepointer 指针        set_freepointer(s, tail, prior);        new.counters = counters;        // 获取原来 slub 中的 frozen 状态,是否在 cpu 缓存 partial 中        was_frozen = new.frozen;        // inuse 示意 slub 曾经调配进来的对象个数,这里是开释 cnt 个对象,所以 inuse 要减去 cnt        new.inuse -= cnt;              ....... 省略 ..........        // cas 更新 slub 中的 freelist     } while (!cmpxchg_double_slab(s, page,        prior, counters,        head, new.counters,        "__slab_free")); .            ...... 省略 ..........}

这一部分的逻辑比较简单,在 __slab_free 内存开释流程的开始,内核不管三七二十一,首先会将对象间接开释回其所在的 slab 中。

当对象被开释回 slab 中之后,slab 构造中的相应属于就须要做出相应的调整,比方:

  • 调整 page 构造中的 freelist,它须要指向刚刚被开释的对象。
  • 调整 page 构造中的 inuse,inuse 示意 slab 中曾经被调配进来的对象个数,此时对象曾经开释回 slab 中,须要调整 inuse 字段。
  • 后续内核会依据不同状况,调整 page 构造的 frozen 属性。

内核会定义一个新的 page 构造 new,将原有 slab 的 page 构造须要更新的上述属性的新值,先一一复制给 new 的对应属性,最初通过 cmpxchg_double_slab 原子更新 slab 对应的属性。

struct page {        struct {    /*  slub 相干字段 */             ........ 省略 .........            // 指向 page 所属的 slab cache            struct kmem_cache *slab_cache;            // 指向 slab 中第一个闲暇对象            void *freelist;     /* first free object */            union {                unsigned long counters;                struct {            /* SLUB */                                 // slab 中曾经调配进来的对象                    unsigned inuse:16;                    // slab 中蕴含的对象总数                    unsigned objects:15;                    // 该 slab 是否在对应 slab cache 的本地 CPU 缓存中                    // frozen = 1 示意缓存再本地 cpu 缓存中                    unsigned frozen:1;                };            };        };}

依照失常的更新套路来说,咱们在更新原有 slab 构造中的 freelist,inuse,frozen 这三个属性之前,首先须要将原有 slab 的这三个旧的属性值一一赋值到长期构造 new page 中,而后在 slab 构造旧值的根底上调整着三个属性的新值,最初通过 cmpxchg_double_slab 将这三个属性的新值原子地更新回 slab 中。

然而咱们查看 __slab_free 的代码发现,内核并不是这样操作的,内核只是将原有 slab 的 counter 属性赋值给 new page,而原有 slab 中的 frozen,inuse 属性并没有赋值过来。

此时 new page 构造中的 frozen,inuse 属性仍然是上述 struct page 构造中展现的初始值。

而内核后续的操作就更加奇怪了,间接应用 new.frozen 来判断原有 slab 是否在 slab cache 本地 cpu 的 partial 链表中,间接把 new.inuse 属性当做原有 slab 中曾经调配进来对象的个数。

而 new.frozen, new.inuse 是 page 构造初始状态的值,并不是原有 slab 构造中的值,这样做必定不对啊,难道是内核的一个 bug ?

其实并不是,这是内核十分骚的一个操作,这一点对于 Java 程序员来说很难了解。咱们在认真看一下 struct page 构造,就会发现 counter 属性和 inuse,frozen 属性被定义在一个 union 构造体中。

union 构造体中定义的字段全副共享一片内存,union 构造体的内存占用由其中最大的属性决定。而 struct 构造体中的每个字段都是独占一片内存的。

因为 union 构造体中各个字段都是共享一块内存,所以一个字段的扭转就会影响其余字段的值,从另一方面来看,通过一个字段就能够将整个 union 构造占用的内存块拿进去。明确这些,咱们在回头来看内核的操作。

struct page {            union {                unsigned long counters;                struct {            /* SLUB */                                 // slab 中曾经调配进来的对象                    unsigned inuse:16;                    // slab 中蕴含的对象总数                    unsigned objects:15;                    // 该 slab 是否在对应 slab cache 的本地 CPU 缓存中                    // frozen = 1 示意缓存再本地 cpu 缓存中                    unsigned frozen:1;                };            };}

page 构造中的 counters 是和 inuse,frozen 共用同一块内存的,内核在 __slab_free 中将原有 slab 的 counters 属性赋值给 new.counters 的一瞬间,counters 所在的内存块也就赋值到 new page 的 union 构造中了。

而 inuse,frozen 属性的值也在这个内存块中,所以原有 slab 中的 inuse,frozen 属性也就跟着一起赋值到 new page 的对应属性中了。这样一来,后续的逻辑解决也就通顺了。

        counters = page->counters;        new.counters = counters;        // 获取原来 slub 中的 frozen 状态,是否在 cpu 缓存 partial 中        was_frozen = new.frozen;        // inuse 示意 slub 曾经调配进来的对象个数,这里是开释 cnt 个对象,所以 inuse 要减去 cnt        new.inuse -= cnt;

同样的情理,咱们再来看内核 cmpxchg_double_slab 中的更新操作:

内核明明在 do .... while 循环中更新了 freelist,inuse,frozen 这三个属性,而 counters 属性只是读取并没有更新操作,那么为什么在 cmpxchg_double_slab 只是更新 page 构造的 freelist 和 counters 呢?inuse,frozen 这两个属性又在哪里更新的呢?

   do {             ....... 省略 ..........        // cas 更新 slub 中的 freelist     } while (!cmpxchg_double_slab(s, page,        prior, counters,        head, new.counters,        "__slab_free"));

我想大家当初肯定可能解释这个问题了,因为 counters,inuse,frozen 共用一块内存,当 inuse,frozen 的值发生变化之后,尽管 counters 的值没有发生变化,然而咱们能够通过更新 counters 来将原有 slab 中的这块内存一起更新掉,这样 inuse,frozen 的值也跟着被更新了。

因为 page 的 freelist 指针在 union 构造体之外,所以须要在cmpxchg_double_slab 中独自更新。

笔者已经为了想给大家解释分明 page->counters 这个属性的作用,而翻遍了 slab 的所有源码,发现内核源码中对于 page->counters 的应用都是只做简略的读取,并不做扭转,而后间接在更新,这个问题也困扰了笔者很久。

直到为大家写这篇文章的时候,才顿悟。原来 page->counters 的作用只是为了指向 inuse,frozen 所在的内存,不便在 cmpxchg_double_slab 中同时原子地更新这两个属性。

接下来的内容就到了 slab cache 回收内存最为简单的环节了,大家须要多一些急躁,持续跟着笔者的思路走上来,咱们一起来看下内核如何解决三种内存慢速开释的场景。

3.2 开释对象所属 slab 原本就在 cpu 缓存 partial 链表中

was_frozen 指向开释对象所属 slab 构造中的 frozen 属性,用来示意 slab 是否在 slab cache 的本地 cpu 缓存 partial 链表中。

 was_frozen = new.frozen;

如果 was_frozen == true 示意开释对象所属 slab 原本就在 kmem_cache_cpu->partial 链表中,内核将对象间接开释回 slab 中,slab 的原有地位不做扭转。

上面咱们看下 was_frozen == fasle 也就是 slab 不在 kmem_cache_cpu->partial 链表中 的时候,内核又是如何解决的 ?

3.3 开释对象所属 slab 从 full slab 变为了 partial slab

如果开释对象所属 slab 原来是一个 full slab,恰好阐明该 slab 领有比拟好的局部性,过程常常从该 slab 中调配对象,slab 非常沉闷,才导致它变为了一个 full slab

 prior = page->freelist = null

随着对象的开释,该 slab 从一个 full slab 变为了 partial slab,内核为了更好的利用该 slab 的局部性,所以须要将该 slab 插入到 slab cache 的本地 cpu 缓存 kmem_cache_cpu->partial 链表中。

        if (kmem_cache_has_cpu_partial(s) && !prior) {                new.frozen = 1;        }         if (new.frozen && !was_frozen) {            // 将 slub 插入到 kmem_cache_cpu 中的 partial 列表中            put_cpu_partial(s, page, 1);            stat(s, CPU_PARTIAL_FREE);        }        

将 slab 插入到 kmem_cache_cpu->partial 链表的逻辑封装在 put_cpu_partial 中,put_cpu_partial 函数最重要的一个考量逻辑是须要确保 kmem_cache_cpu->partial 链表中所有 slab 中蕴含的闲暇对象总数不能超过 kmem_cache->cpu_partial 的限度。

struct kmem_cache {    // 限定 slab cache 在每个 cpu 本地缓存 partial 链表中所有 slab 中闲暇对象的总数    unsigned int cpu_partial;};

在开释对象所在的 slab 插入到 kmem_cache_cpu->partial 链表之前,put_cpu_partial 函数须要判断以后 kmem_cache_cpu->partial 链表中蕴含的闲暇对象总数 pobjects 是否超过了 kmem_cache->cpu_partial 的限度。

如果超过了,则须要先将以后 kmem_cache_cpu->partial 链表中所有的 slab 转移到其对应的 NUMA 节点缓存 kmem_cache_node->partial 链表中。转移实现之后,在将开释对象所属的 slab 插入到 kmem_cache_cpu->partial 链表中。

static void put_cpu_partial(struct kmem_cache *s, struct page *page, int drain){// 只有配置了 CONFIG_SLUB_CPU_PARTIAL 选项,kmem_cache_cpu 中才有会 partial 列表#ifdef CONFIG_SLUB_CPU_PARTIAL    // 指向原有 kmem_cache_cpu 中的 partial 列表    struct page *oldpage;    // slub 所在治理列表中的 slub 个数,这里的列表是指 partial 列表    int pages;    // slub 所在治理列表中的蕴含的闲暇对象总数,这里的列表是指 partial 列表    // 内核会将列表总体的信息寄存在列表首页 page 的相干字段中    int pobjects;    // 禁止抢占    preempt_disable();    do {        pages = 0;        pobjects = 0;        // 获取 slab cache 中原有的 cpu 本地缓存 partial 列表首页        oldpage = this_cpu_read(s->cpu_slab->partial);        // 如果 partial 列表不为空,则须要判断 partial 列表中所有 slub 蕴含的闲暇对象总数是否超过了 s->cpu_partial 规定的阈值        // 超过 s->cpu_partial 则须要将 kmem_cache_cpu->partial 列表中原有的所有 slub 转移到 kmem_cache_node-> partial 列表中        // 转移之后,再把以后 slub 插入到 kmem_cache_cpu->partial 列表中        // 如果没有超过 s->cpu_partial ,则无需转移直接插入        if (oldpage) {            // 从 partial 列表首页中获取列表中蕴含的闲暇对象总数            pobjects = oldpage->pobjects;            // 从 partial 列表首页中获取列表中蕴含的 slub 总数            pages = oldpage->pages;            if (drain && pobjects > s->cpu_partial) {                unsigned long flags;                // 敞开中断,避免并发拜访                local_irq_save(flags);                // partial 列表中所蕴含的闲暇对象总数 pobjects 超过了 s->cpu_partial 规定的阈值                // 则须要将现有 partial 列表中的所有 slub 转移到相应的 kmem_cache_node->partial 列表中                unfreeze_partials(s, this_cpu_ptr(s->cpu_slab));                // 复原中断                local_irq_restore(flags);                // 重置 partial 列表                oldpage = NULL;                pobjects = 0;                pages = 0;                stat(s, CPU_PARTIAL_DRAIN);            }        }        // 无论 kmem_cache_cpu-> partial 列表中的 slub 是否须要转移        // 开释对象所在的 slub 都须要填加到  kmem_cache_cpu-> partial 列表中        pages++;        pobjects += page->objects - page->inuse;        page->pages = pages;        page->pobjects = pobjects;        page->next = oldpage;        // 通过 cas 将 slub 插入到 partial 列表的头部    } while (this_cpu_cmpxchg(s->cpu_slab->partial, oldpage, page)                                != oldpage);    // s->cpu_partial = 0 示意 kmem_cache_cpu->partial 列表不能寄存 slub    // 将开释对象所在的 slub 转移到  kmem_cache_node-> partial 列表中    if (unlikely(!s->cpu_partial)) {        unsigned long flags;        local_irq_save(flags);        unfreeze_partials(s, this_cpu_ptr(s->cpu_slab));        local_irq_restore(flags);    }    preempt_enable();#endif  /* CONFIG_SLUB_CPU_PARTIAL */}

那么咱们如何晓得 kmem_cache_cpu->partial 链表所蕴含的闲暇对象总数到底是多少呢?

这就用到了 struct page 构造中的两个重要属性:

struct page {      // slab 所在链表中的蕴含的 slab 总数      int pages;        // slab 所在链表中蕴含的对象总数      int pobjects; }

咱们都晓得 slab 在内核中的数据结构用 struct page 中的相干构造体示意,slab 在 slab cache 架构中个别是由 kmem_cache_cpu->partial 链表和 kmem_cache_node->partial 链表来组织治理。

那么咱们如何晓得 partial 链表中蕴含多少个 slab ?蕴含多少个闲暇对象呢?

答案是内核会将 parital 链表中的这些总体统计信息存储在链表首个 slab 构造中。也就是说存储在首个 page 构造中的 pages 属性和 pobjects 属性中。

在 put_cpu_partial 函数的开始,内核间接获取 parital 链表的首个 slab —— oldpage,并通过 oldpage->pobjectss->cpu_partial 比拟,来判断以后 kmem_cache_cpu->partial 链表中蕴含的闲暇对象总数是否超过了 kmem_cache 构造中规定的 cpu_partial 阈值。

如果超过了,则通过 unfreeze_partials 转移 kmem_cache_cpu->partial 链表中的所有 slab 到对应的 kmem_cache_node->partial 链表中。

既然 kmem_cache_cpu->partial 链表有容量的限度,那么同样 kmem_cache_node->partial 链表中的容量也会有限度。

kmem_cache_node->partial 链表中所蕴含 slab 个数的下限由 kmem_cache 构造中的 min_partial 属性决定。

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

如果以后要转移的 slab 是一个 empty slab,并且此时 kmem_cache_node->partial 链表所蕴含的 slab 个数 kmem_cache_node->nr_partial 曾经超过了 kmem_cache-> min_partial 的限度,那么内核就会间接将这个 empty slab 开释回搭档零碎中。

// 将 kmem_cache_cpu->partial 列表中蕴含的 slub unfreeze// 并转移到对应的 kmem_cache_node->partial 列表中static void unfreeze_partials(struct kmem_cache *s,        struct kmem_cache_cpu *c){#ifdef CONFIG_SLUB_CPU_PARTIAL    struct kmem_cache_node *n = NULL, *n2 = NULL;    struct page *page, *discard_page = NULL;    // 挨个遍历 kmem_cache_cpu->partial 列表,将列表中的 slub 转移到对应 kmem_cache_node->partial 列表中    while ((page = c->partial)) {        struct page new;        struct page old;        // 将以后遍历到的 slub 从 kmem_cache_cpu->partial 列表摘下        c->partial = page->next;        // 获取以后 slub 所在的 numa 节点对应的 kmem_cache_node 缓存        n2 = get_node(s, page_to_nid(page));        // 如果和上一个转移的 slub 所在的 numa 节点不一样        // 则须要开释上一个 numa 节点的 list_lock,并对以后 numa 节点的 list_lock 加锁        if (n != n2) {            if (n)                spin_unlock(&n->list_lock);            n = n2;            spin_lock(&n->list_lock);        }        do {            old.freelist = page->freelist;            old.counters = page->counters;            VM_BUG_ON(!old.frozen);            new.counters = old.counters;            new.freelist = old.freelist;            // unfrozen 以后 slub,因为行将被转移到对应的 kmem_cache_node->partial 列表            new.frozen = 0;            // cas 更新以后 slub 的 freelist,frozen 属性        } while (!__cmpxchg_double_slab(s, page,                old.freelist, old.counters,                new.freelist, new.counters,                "unfreezing slab"));        // 因为 kmem_cache_node->partial 列表中所蕴含的 slub 个数是受 s->min_partial 阈值限度的        // 所以这里还须要查看 nr_partial 是否超过了 min_partial        // 如果以后被转移的 slub 是一个 empty slub 并且 nr_partial 超过了 min_partial 的限度,则须要将 slub 开释回搭档零碎中        if (unlikely(!new.inuse && n->nr_partial >= s->min_partial)) {            // discard_page 用于将须要开释回搭档零碎的 slub 串联起来            // 后续对立将 discard_page 链表中的 slub 开释回搭档零碎            page->next = discard_page;            discard_page = page;        } else {            // 其余状况,只有 slub 不为 empty ,不论 nr_partial 是否超过了 min_partial            // 都须要将 slub 转移到对应 kmem_cache_node->partial 列表的开端            add_partial(n, page, DEACTIVATE_TO_TAIL);            stat(s, FREE_ADD_PARTIAL);        }    }    if (n)        spin_unlock(&n->list_lock);    // 将 discard_page 链表中的 slub 对立开释回搭档零碎    while (discard_page) {        page = discard_page;        discard_page = discard_page->next;        stat(s, DEACTIVATE_EMPTY);        // 底层调用 __free_pages 将 slub 所治理的所有 page 开释回搭档零碎        discard_slab(s, page);        stat(s, FREE_SLAB);    }#endif  /* CONFIG_SLUB_CPU_PARTIAL */}

3.4 开释对象所属 slab 从 partial slab 变为了 empty slab

如果开释对象所在的 slab 原来是一个 partial slab ,因为对象的开释刚好变成了一个 empty slab,恰好阐明该 slab 并不是一个沉闷的 slab,它的局部性不好,内核曾经良久没有从该 slab 中调配对象了,所以内核抉择刀枪入库,马放南山。将它开释回 kmem_cache_node->partial 链表中作为本地 cpu 缓存的后备选项。

在将这个 empty slab 插入到 kmem_cache_node->partial 链表之前,同样须要查看以后 partial 链表中的容量 kmem_cache_node->nr_partial 不能超过 kmem_cache-> min_partial 的限度。如果超过限度了,间接将这个 empty slab 开释回搭档零碎中。

        if ((!new.inuse || !prior) && !was_frozen) {            if (kmem_cache_has_cpu_partial(s) && !prior) {                new.frozen = 1;            } else {                 // !new.inuse 示意以后 slab 刚刚从一个 partial slab 变为了 empty slab                n = get_node(s, page_to_nid(page));                spin_lock_irqsave(&n->list_lock, flags);            }        }      if (unlikely(!new.inuse && n->nr_partial >= s->min_partial))        // 如果 slub 变为了一个 empty slub 并且 nr_partial 超过了最大阈值 min_partial        // 跳转到 slab_empty 分支,将 slub 开释回搭档零碎中        goto slab_empty;

开释对象所属的 slab 原本就在 kmem_cache_node->partial 链表中,这种状况下就是间接开释对象回 slab 中,无需扭转 slab 的地位。

4. slab cache 的销毁

终于到了本文最初一个大节了, slab cache 最为简单的内容咱们曾经踏过来了,本大节的内容将会十分的轻松愉悦,这一次笔者来为大家介绍一下 slab cache 的销毁过程。

slab cache 的销毁过程刚刚好和 slab cache 的创立过程相同,笔者在 《从内核源码看 slab 内存池的创立初始化流程》的内容中,通过一步一步的源码演示,最终勾画出 slab cache 的残缺架构:

slab cache 销毁的外围步骤如下:

  1. 首先须要开释 slab cache 在所有 cpu 中的缓存 kmem_cache_cpu 中占用的资源,包含被 cpu 缓存的 slab (kmem_cache_cpu->page),以及 kmem_cache_cpu->partial 链表中缓存的所有 slab,将它们通通偿还到搭档零碎中。
  2. 开释 slab cache 在所有 NUMA 节点中的缓存 kmem_cache_node 占用的资源,也就是将 kmem_cache_node->partial 链表中缓存的所有 slab ,通通开释回搭档零碎中。
  3. 在 sys 文件系统中移除 /sys/kernel/slab/<cacchename> 节点相干信息。
  4. 从 slab cache 的全局列表中删除该 slab cache。
  5. 开释 kmem_cache_cpu 构造,kmem_cache_node 构造,kmem_cache 构造。开释对象的过程就是 《1. slab cache 如何回收内存》大节中介绍的内容。

上面咱们一起到内核源码中看一下具体的销毁过程:

void kmem_cache_destroy(struct kmem_cache *s){    int err;    if (unlikely(!s))        return;    // 获取 cpu_hotplug_lock,避免 cpu 热插拔扭转 online cpu map    get_online_cpus();    // 获取 mem_hotplug_lock,避免拜访内存的时候进行内存热插拔    get_online_mems();    // 获取 slab cache 链表的全局互斥锁    mutex_lock(&slab_mutex);    // 将 slab cache 的援用技术减 1    s->refcount--;    // 判断 slab cache 是否还存在其余中央的援用    if (s->refcount)        // 如果该 slab cache 还存在援用,则不能销毁,跳转到 out_unlock 分支        goto out_unlock;    // 销毁 memory cgroup 相干的 cache ,这里不是本文重点    err = shutdown_memcg_caches(s);    if (!err)        // slab cache 销毁的外围函数,销毁逻辑就封装在这里        err = shutdown_cache(s);    if (err) {        pr_err("kmem_cache_destroy %s: Slab cache still has objects\n",               s->name);        dump_stack();    }out_unlock:    // 开释相干的自旋锁和信号量    mutex_unlock(&slab_mutex);    put_online_mems();    put_online_cpus();}

在开始正式销毁 slab cache 之前,首先须要将 slab cache 的援用计数 refcount 减 1。并须要判断 slab cache 是否还存在其余中央的援用。

slab cache 这里在其余中央存在援用的可能性,相干细节笔者在《从内核源码看 slab 内存池的创立初始化流程》 一文中的 ”1. __kmem_cache_alias“ 大节的内容中曾经具体介绍过了。

当咱们利用 kmem_cache_create 创立 slab cache 的时候,内核会查看以后零碎中是否存在一个各项参数和咱们要创立 slab cache 参数差不多的一个 slab cache,如果存在,那么内核就不会再持续创立新的 slab cache,而是复用已有的 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 。 。

随后会在 sys 文件系统中为复用 slab cache 起一个别名 alias 并创立一个 /sys/kernel/slab/aliasname 目录,然而该目录下的文件须要软链接到原有 slab cache 在 sys 文件系统对应目录下的文件。这里的 aliasname 就是咱们通过 kmem_cache_create 指定的 slab cache 名称。

在这种状况,零碎中的 slab cache 就可能在多个中央产生援用,所以在销毁的时候须要判断这一点。

如果存在其余中央的援用,则须要进行销毁流程,如果没有其余中央的援用,则调用 shutdown_cache 开始正式的销毁流程。

static int shutdown_cache(struct kmem_cache *s){    // 这里会开释 slab cache 占用的所有资源    if (__kmem_cache_shutdown(s) != 0)        return -EBUSY;    // 从 slab cache 的全局列表中删除该 slab cache    list_del(&s->list);    // 开释 sys 文件系统中移除 /sys/kernel/slab/name 节点的相干资源    sysfs_slab_unlink(s);    sysfs_slab_release(s);    // 开释 kmem_cache_cpu 构造    // 开释 kmem_cache_node 构造    // 开释 kmem_cache 构造    slab_kmem_cache_release(s);    }    return 0;}

4.1 开释 slab cache 占用的所有资源

  1. 首先须要开释 slab cache 在所有 cpu 中的缓存 kmem_cache_cpu 中占用的资源,包含被 cpu 缓存的 slab (kmem_cache_cpu->page),以及 kmem_cache_cpu->partial 链表中缓存的所有 slab,将它们通通偿还到搭档零碎中。
  2. 开释 slab cache 在所有 NUMA 节点中的缓存 kmem_cache_node 占用的资源,也就是将 kmem_cache_node->partial 链表中缓存的所有 slab ,通通开释回搭档零碎中。
  3. 在 sys 文件系统中移除 /sys/kernel/slab/<cacchename> 节点相干信息。
/* * Release all resources used by a slab cache. */int __kmem_cache_shutdown(struct kmem_cache *s){    int node;    struct kmem_cache_node *n;    // 开释 slab cache 本地 cpu 缓存 kmem_cache_cpu 中缓存的 slub 以及 partial 列表中的 slub,通通归还给搭档零碎    flush_all(s);    // 开释 slab cache 中 numa 节点缓存 kmem_cache_node 中 partial 列表上的所有 slub    for_each_kmem_cache_node(s, node, n) {        free_partial(s, n);        if (n->nr_partial || slabs_node(s, node))            return 1;    }    // 在 sys 文件系统中移除 /sys/kernel/slab/name 节点相干信息    sysfs_slab_remove(s);    return 0;}

4.2 开释 slab cache 在各个 cpu 中的缓存资源

内核通过 on_each_cpu_cond 挨个遍历所有 cpu,在遍历的过程中通过 has_cpu_slab 判断 slab cache 是否在该 cpu 中还占有缓存资源,如果是则调用 flush_cpu_slab 将缓存资源开释回搭档零碎中。

// 开释 kmem_cache_cpu 中占用的所有内存资源static void flush_all(struct kmem_cache *s){    // 遍历每个 cpu,通过 has_cpu_slab 函数查看 cpu 上是否还有 slab cache 的相干缓存资源    // 如果有,则调用 flush_cpu_slab 进行资源的开释    on_each_cpu_cond(has_cpu_slab, flush_cpu_slab, s, 1, GFP_ATOMIC);}static bool has_cpu_slab(int cpu, void *info){    struct kmem_cache *s = info;    // 获取 cpu 在 slab cache 上的本地缓存    struct kmem_cache_cpu *c = per_cpu_ptr(s->cpu_slab, cpu);    // 判断 cpu 本地缓存中是否还有缓存的 slub    return c->page || slub_percpu_partial(c);}static void flush_cpu_slab(void *d){    struct kmem_cache *s = d;    // 开释 slab cache 在 cpu 上的本地缓存资源    __flush_cpu_slab(s, smp_processor_id());}static inline void __flush_cpu_slab(struct kmem_cache *s, int cpu){    struct kmem_cache_cpu *c = per_cpu_ptr(s->cpu_slab, cpu);    if (c->page)        // 开释 cpu 本地缓存的 slub 到搭档零碎        flush_slab(s, c);    // 将 cpu 本地缓存中的 partial 列表里的 slub 全副开释回搭档零碎    unfreeze_partials(s, c);}

4.3 开释 slab cache 的外围数据结构

这里的开释流程正是笔者在本文 《1. slab cache 如何回收内存》大节中介绍的内容。

void slab_kmem_cache_release(struct kmem_cache *s){    // 开释 slab cache 中的 kmem_cache_cpu 构造以及 kmem_cache_node 构造    __kmem_cache_release(s);    // 最初开释 slab cache 的外围数据结构 kmem_cache    kmem_cache_free(kmem_cache, s);}

总结

整个 slab cache 系列篇幅十分宏大,波及到的细节十分丰盛,为了不便大家回顾,笔者这里将 slab cache 系列波及到的重点内容再次梳理总结一下。

  • 《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现》
  • 《从内核源码看 slab 内存池的创立初始化流程》
  • 《深刻了解 slab cache 内存调配全链路实现》

在本文正式进入 slab 相干内容之后,笔者首先为大家具体介绍了 slab 内存池中对象的内存布局状况,如下图所示:

在此基础之上,咱们持续采纳一步一图的形式,一步一步推演出 slab 内存池的整体架构,如下图所示:

随后基于此架构,笔者介绍了在不同场景下 slab 内存池分配内存以及回收内存的外围原理。在交代完外围原理之后,咱们进一步深刻到内核源码实现中来一一验证。

在内核源码章节的开始,笔者首先为大家介绍了 slab 内存池的创立流程,流程图如下:

在 slab 内存池创立进去之后,随后笔者又深刻介绍了 slab 内存池如何分配内存块的相干源码实现,其中具体介绍了在多种不同场景下,内核如何解决内存块的调配。

在咱们革除了 slab 内存池如何分配内存块的源码实现之后,紧接着笔者又介绍了 slab 内存池如何进行内存块的回收,回收过程要比调配过程简单很多,同样也波及到多种简单场景的解决:

最初笔者介绍了 slab 内存池的销毁过程:

好了,整个 slab cache 相干的内容到此就完结了,感激大家的收看,咱们下篇文章见~~~