本文仍然以slab cache kmalloc_caches 为例,结合 kfree 函数的实现,说明slab对象的回收过程。

1. kfree

通过 kfree 函数释放 kmalloc 申请的内存时,对应的函数定义在 mm/slub.c 中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void kfree(const void *x)
{
    struct page *page;
    void *object = (void *)x;

    trace_kfree(_RET_IP_, x);
    /* 地址为空直接返回 */
    if (unlikely(ZERO_OR_NULL_PTR(x)))
        return;
    /* 通过内核虚拟地址获取对应的struct page * */
    page = virt_to_head_page(x);
    /* slab分配器没有管理这个内存页 */
    if (unlikely(!PageSlab(page))) {
        BUG_ON(!PageCompound(page));
        kfree_hook(x);
        /* 直接通过伙伴系统释放内存页 */
        __free_kmem_pages(page, compound_order(page));
        return;
    }
    /* struct page中包含内存页的slab信息,包括该内存页
       所属的slab cache,*/
    slab_free(page->slab_cache, page, object, _RET_IP_);
}
EXPORT_SYMBOL(kfree);

2. slab_free( mm/slub.c )

如果 kfree 的参数地址所在的页面属于slab分配器,通过 slab_free 函数释放。 slab_free 函数也有两个分支,快路径和慢路径。

如果 kfree 的对象属于当前的cpu slab,执行快路径;否则执行慢路径。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
static __always_inline void slab_free(struct kmem_cache *s,
            struct page *page, void *x, unsigned long addr)
{
    void **object = (void *)x;
    struct kmem_cache_cpu *c;
    unsigned long tid;

    slab_free_hook(s, x);

redo:
    /*
     * Determine the currently cpus per cpu slab.
     * The cpu may change afterward. However that does not matter since
     * data is retrieved via this pointer. If we are on the same cpu
     * during the cmpxchg then the free will succedd.
     */
    preempt_disable();
    c = this_cpu_ptr(s->cpu_slab);

    tid = c->tid;
    preempt_enable();
    /*
     释放的对象所在的内存页刚好是当前CPU正在使用的
     slab,执行快路径,只需要添加到freelist中 */
    if (likely(page == c->page)) {
        /*
         设置要释放的对象指向当前的空闲对象,即把释放的对象
         添加到CPU的freelist中 */
        set_freepointer(s, object, c->freelist);
        /*
         更新slab cache的cpu_slab变量,指向
         最新的freelist,并且更新tid */
        if (unlikely(!this_cpu_cmpxchg_double(
                s->cpu_slab->freelist, s->cpu_slab->tid,
                c->freelist, tid,
                object, next_tid(tid)))) {

            note_cmpxchg_failure("slab_free", s, tid);
            goto redo;
        }
        stat(s, FREE_FASTPATH);
    } else
        /*
         page不是当前CPU正在分配对象的slab,执行慢路径。
         可能是以下情况:
         1. 释放的对象属于当前CPU的partial表
         2. 释放的对象属于其他CPU的slab  */
        __slab_free(s, page, x, addr);

}

3. __slab_free( mm/slub.c )

慢路径释放slab时,涉及到多个名称相近的变量,先对这些变量进行说明。

  • struct kmem_cache —— 描述一个slab cache:

    • unsigned long min_partial
      node结点中部分空slab缓冲区数量不能小于这个值,如果小于这个值,空闲slab缓冲区则不能够进行释放,而是将空闲slab加入到node结点的部分空slab链表中。
    • int cpu_partial
      同min_partial类似,只是这个值表示的是对象的数量,而不是部分空slab数量,即CPU的partial对象数量不能小于这个值,小于的情况下要去对应node结点的部分空链表中获取若干个部分空slab;否则在 put_cpu_partial 函数中进行解冻操作。
  • struct kmem_cache_cpu —— 描述一个CPU的slab cache:

    • void **freelist
      指向下一个空闲的对象
    • struct page *partial
      CPU的部分空slab链表,放到CPU的部分空slab链表中的slab会被冻结,而放入node中的部分空slab链表则解冻,冻结标志在slab缓冲区描述符( struct page )中
    • struct page *page
      当前分配对象的slab
  • struct kmem_cache_node —— 描述一个节点的slab cache:

    • unsigned long nr_partial
      节点中部分空slab的数量
    • struct list_head partial
      保存部分空slab的链表
  • struct page —— 描述一个slab:

    • void *freelist
      第一个空闲的对象
    • unsigned inuse:16
      slab中已经分配的对象的数量
    • unsigned frozen:1
      被冻结的slab不属于任何链表,不会进行列表的管理操作。
      只有冻结slab的CPU可以执行列表操作——其他的CPU可以往freelist中添加对象,但是只有冻结slab的CPU可以从freelist中获取对象。因此, slab_free 函数会跳过列表的相关操作。
      被冻结的slab主要用于特定目的,比如响应特定CPU的分配请求。
      frozen 标志用来判断某一个slab(即内存页)是否已经被某个CPU用作slab cache,即属于某个 kmem_cache_cpu

和快路径 slab_free 不同,执行慢路径的原因是 kfree 释放的对象所属的内存页不是当前CPU正在分配对象的slab。
因此,函数释放对象的时候修改的是 struct page 内的 freelist 指针,而不是CPU的freelist。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
static void __slab_free(struct kmem_cache *s, struct page *page,
            void *x, unsigned long addr)
{
    void *prior;
    void **object = (void *)x;
    int was_frozen;
    struct page new;
    unsigned long counters;
    struct kmem_cache_node *n = NULL;
    unsigned long uninitialized_var(flags);

    ...
    /* do-while循环保证更改成功 */
    do {
        if (unlikely(n)) {
            spin_unlock_irqrestore(&n->list_lock, flags);
            n = NULL;
        }
        prior = page->freelist;
        counters = page->counters;
        set_freepointer(s, object, prior);
        /*
         counters的赋值操作会将和其位于相同union内的
         inuse,objects,frozen字段赋值 */
        new.counters = counters;
        was_frozen = new.frozen;
        new.inuse--;
        /*
         !new.inuse = true,slab中没有分配出去的对象,目前我
         只想到了一种情况,即当前释放的对象是slab中的最后一个
         对象,释放之后slab为空。
         !prior = true,slab中没有可用的对象,即slab为满。
         !was_frozen = true,slab没有被冻结,即不属于某一个CPU
         的slab cache  */
        if ((!new.inuse || !prior) && !was_frozen) {
            /*
             slab为满,释放当前对象后会变成partial,而且
             不属于某一个CPU的slab cache,将其冻结,使其
             属于当前CPU的slab cache */
            if (kmem_cache_has_cpu_partial(s) && !prior) {
                new.frozen = 1;
            } else {
                /*
                 释放当前的对象后slab为空,先获取节点信息,
                 并且获取修改slab list所需的锁,以便之后
                 释放空slab */
                n = get_node(s, page_to_nid(page));
                /*
                 只有当前slab为空时才需要获取锁,并且在执行
                 释放操作后释放锁 */
                spin_lock_irqsave(&n->list_lock, flags);
            }
        }
    } while (!cmpxchg_double_slab(s, page,
        prior, counters,
        object, new.counters,
        "__slab_free"));
    /*
     n为空的可能性较大,即当前释放的对象是slab中的最后一个对象
     的可能性较小。其他的可能情况为:
     1. slab已满,并且slab不属于某个CPU
     2. slab已经属于某个CPU
     3. 无论slab是否属于某个CPU,slab的freelist不为空,且inuse
     字段不为0 */
    if (likely(!n)) {
        /*
         刚刚执行的冻结操作,将内存页添加到当前CPU的slab
         cache的partial表。
         这里put_cpu_partial函数将新的slab添加到partial
         表时,如果当前CPU中已有的partial对象大于slab
         cache的cpu_partial值,就会将当前CPU中的所有的
         partial slab解冻,并且在节点中的partial slab
         的数量不小于slab cache的cpu_partial值的情况下,
         释放解冻的slab;否则将解冻的slab添加到节点的
         partial表中。 */
        if (new.frozen && !was_frozen) {
            put_cpu_partial(s, page, 1);
            stat(s, CPU_PARTIAL_FREE);
        }
        /*
         slab已经属于其他CPU的slab cache,当前的CPU不是冻结
         slab的CPU,无法执行其他的操作;这种情况下也没有获取
         锁的操作,因此也不需要释放  */
        if (was_frozen)
            stat(s, FREE_FROZEN);
        return;
    }
    /*
     释放当前对象后slab为空,并且节点的partial slab数量仍然
     大于slab cache中的最小阈值,可以直接将slab释放 */
    if (unlikely(!new.inuse && n->nr_partial > s->min_partial))
        goto slab_empty;

    /* slab从full变为partial */
    if (!kmem_cache_has_cpu_partial(s) && unlikely(!prior)) {
        if (kmem_cache_debug(s))
            remove_full(s, n, page);
        add_partial(n, page, DEACTIVATE_TO_TAIL);
        stat(s, FREE_ADD_PARTIAL);
    }
    spin_unlock_irqrestore(&n->list_lock, flags);
    return;

slab_empty:
    /* 当前释放的对象是slab中的最后一个对象 */
    if (prior) {
        /* 当前slab有多个对象 */
        remove_partial(n, page);
        stat(s, FREE_REMOVE_PARTIAL);
    } else {
        /* 当前slab只有一个对象,导致位于full表 */
        remove_full(s, n, page);
    }
    /* 释放获取的锁 */
    spin_unlock_irqrestore(&n->list_lock, flags);
    stat(s, FREE_SLAB);
    /* 释放slab */
    discard_slab(s, page);
}

4. slab的操作函数

__slab_free 函数调用了多个操作slab的函数,包括 put_cpu_partialadd_partialremove_partialremove_fulldiscard_slab

4.1. put_cpu_partial

__slab_free 函数执行时,如果如果slab不属于任何CPU的slab cache,就会在释放对象后把slab添加到当前CPU的slab cache中,作为partial slab。

struct kmem_cache 结构体的 struct kmem_cache_cpu __percpu *cpu_slab 成员,其 struct page *partial 包含CPU的所有partial slab。
这些slab通过 struct pagestruct page *next 成员链接;每个slab(page)包含partial链表中剩余partial slabs所有对象的数量,即 pobjects ,还包含剩余的partial slabs的数量,即 pages
也就是说,每个CPU的slab cache包含的partial slab都保存在一个链表中,这个链表通过 *next 指针链接;链表中的第一个slab包含链表中对象的总数和slab的总数,第二个slab包含除了第一个slab外对象的总数和slab的总数。

put_cpu_partial 函数的原型如下:
static void put_cpu_partial(struct kmem_cache *s, struct page *page, int drain);
s 是当前正在操作的slab cache, page 是要添加到partial list的slab, drain 是一个标志符,设置后如果当前CPU的slab cache已经超出了 s->cpu_partial ,就会执行 unfreeze_partials 函数,将当前CPU的slab cache中已有的partial slab全部解冻,然后将新增的slab中包含的partial对象的数量添加到统计信息。
slab中包含的partial对象的数量计算方法为 pobjects = page->objects - page->inuse

4.1.1. unfreeze_partials

put_cpu_partial 调用 unfreeze_partial 函数时,传入的参数为操作的slab cache和当前CPU的slab cache对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
static void unfreeze_partials(struct kmem_cache *s,
        struct kmem_cache_cpu *c)
{
#ifdef CONFIG_SLUB_CPU_PARTIAL  /* x86默认开启 */
    struct kmem_cache_node *n = NULL, *n2 = NULL;
    /* discard_page中保存要释放的slab */
    struct page *page, *discard_page = NULL;
    while ((page = c->partial)) {
        struct page new;
        struct page old;

        /*
         配合while语句遍历当前CPU的slab cache中
         所有的partial slab */
        c->partial = page->next;
        /* 获取当前slab所属的kmem_cache_node */
        n2 = get_node(s, page_to_nid(page));
        if (n != n2) {
            if (n)
                spin_unlock(&n->list_lock);

            n = n2;
            spin_lock(&n->list_lock);
        }
        /* 将frozen从1设置为0 */
        do {
            old.freelist = page->freelist;
            old.counters = page->counters;
            VM_BUG_ON(!old.frozen);

            new.counters = old.counters;
            new.freelist = old.freelist;

            new.frozen = 0;
        } while (!__cmpxchg_double_slab(s, page,
                old.freelist, old.counters,
                new.freelist, new.counters,
                "unfreezing slab"));
        /*
         !new.inuse为真表示slab为空
         如果slab空,并且节点中部分空slab的数量nr_partial不小于
         slab cache中指明的最小值min_partial,可以将当前的slab
         释放,先添加到discad_page中 */
        if (unlikely(!new.inuse && n->nr_partial >= s->min_partial)) {
            page->next = discard_page;
            discard_page = page;
        } else {
            /* 添加到节点的partial list中 */
            add_partial(n, page, DEACTIVATE_TO_TAIL);
            stat(s, FREE_ADD_PARTIAL);
        }
    }

    if (n)
        spin_unlock(&n->list_lock);

    /* 将所有可以释放的slab释放 */
    while (discard_page) {
        page = discard_page;
        discard_page = discard_page->next;
        stat(s, DEACTIVATE_EMPTY);
        discard_slab(s, page);
        stat(s, FREE_SLAB);
    }
#endif
}

4.2. add_partial / remove_partial / remove_full

三个函数都是操作 struct pagestruct list_head lru 成员,即从双向链表中直接删除slab项,并且设置 lru->prev = LIST_POISON2lru->next = LIST_POISON1

需要说明的是,在 struct page 中, lrunextpagespobjects 等成员位于一个union中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct page {
    ...
    union {
        struct list_head lru;
        struct {
            struct page *next;
            int pages;
            int pobjects;
        };
        ...
    };
    ...
}

也就是说,在执行删除操作时,赋值 lru->nextlru->prev 会使 struct page *next 指向 LIST_POISON1 ,其他地方比如 unfreeze_partials 函数通过 next 遍历partial表,不会访问已经删除的slab。

4.3. discard_slab

remove_partialremove_full 只是将slab从表中删除,而 discard_slab 则将slab从slab cache中删除。

discard_slab 首先调用 dec_slabs_node 将要删除的slab包含对象的数量从slab cache中删除,并且减少节点的 nr_slabs ,然后调用 free_slab 函数。

free_slab 根据slab cache的标志执行不同的分支,这里介绍 __free_slab 分支。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static void __free_slab(struct kmem_cache *s, struct page *page)
{
    /* 获取页面的order */
    int order = compound_order(page);
    int pages = 1 << order;

    ...

    kmemcheck_free_shadow(page, compound_order(page));
    /* 修改zone->vm_stat和全局统计变量vm_stat */
    mod_zone_page_state(page_zone(page),
        (s->flags & SLAB_RECLAIM_ACCOUNT) ?
        NR_SLAB_RECLAIMABLE : NR_SLAB_UNRECLAIMABLE,
        -pages);
    /* 清除page的标志位 */
    __ClearPageSlabPfmemalloc(page);
    __ClearPageSlab(page);
    /* 设置page->_mapcount = -1 */
    page_mapcount_reset(page);
    if (current->reclaim_state)
        current->reclaim_state->reclaimed_slab += pages;
    /* 通过伙伴系统释放页面 */
    __free_pages(page, order);
    /* 从memcg系统删除 */
    memcg_uncharge_slab(s, order);
}

其中,清除标志位的函数都通过 include/linux/page-flags.h 中的宏定义,包括以双下划线开头的和不以双下划线开头的,后者是原子的,前者不是原子的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#define SETPAGEFLAG(uname, lname)                   \
static inline void SetPage##uname(struct page *page)            \
            { set_bit(PG_##lname, &page->flags); }

#define CLEARPAGEFLAG(uname, lname)                 \
static inline void ClearPage##uname(struct page *page)          \
            { clear_bit(PG_##lname, &page->flags); }

#define __SETPAGEFLAG(uname, lname)                 \
static inline void __SetPage##uname(struct page *page)          \
            { __set_bit(PG_##lname, &page->flags); }

#define __CLEARPAGEFLAG(uname, lname)                   \
static inline void __ClearPage##uname(struct page *page)        \
            { __clear_bit(PG_##lname, &page->flags); }

5. deactivate_slab

虽然释放slab对象的过程中没有调用 deactivate_slab 函数,但是在slab分配对象的慢路径 __slab_alloc 执行的过程中,如果CPU正在使用的slab不属于当前的节点,或者slab设置了pfmemalloc标志,但是请求对象时没有设置ALLOC_NO_WATERMARK,就会将slab移除,放到节点的partial list中

deactivate_slab 函数只有两个函数调用,一个是分配slab对象的慢路径,即 __slab_alloc ;另一个是 flush_slab 函数。
而且,从代码来看,前一种情况的可能性较小,更多的情况是通过后一种路径调用函数,因此默认将移除的slab放到表头。

此处说明 deactivate_slab 时,假定通过 __slab_alloc 函数调用,即 deactivate_slab(s, page, c->freelist) ,则 page 为当前CPU正在使用的slab, freelist 不为空, page 可能位于当前的节点。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
static void deactivate_slab(struct kmem_cache *s, struct page *page,
                void *freelist)
{
    enum slab_modes { M_NONE, M_PARTIAL, M_FULL, M_FREE };
    /* n指向要移除的slab所属的内存节点 */
    struct kmem_cache_node *n = get_node(s, page_to_nid(page));
    int lock = 0;
    enum slab_modes l = M_NONE, m = M_NONE;
    void *nextfree;
    /*
     默认情况下,移除的slab放到表头,因为是通过flush_slab
     调用,放到表头之后可在短时间内再次使用 */
    int tail = DEACTIVATE_TO_HEAD;
    struct page new;
    struct page old;

    /*
     如果page->freelist不等于空,说明其他CPU曾经释放对象
     到当前要移除的slab中,将其放到表尾 */
    if (page->freelist) {
        stat(s, DEACTIVATE_REMOTE_FREES);
        tail = DEACTIVATE_TO_TAIL;
    }
    /*
     如果freelist不为空,并且要移除的slab中有其他CPU释放的
     可用对象,则移除这些对象,将freelist指向slab最后一个
     可用的对象 */
    while (freelist && (nextfree = get_freepointer(s, freelist))) {
        void *prior;
        unsigned long counters;

        do {
            prior = page->freelist;
            counters = page->counters;
            /* 将freelist指向page->freelist */
            set_freepointer(s, freelist, prior);
            new.counters = counters;
            new.inuse--;
            VM_BUG_ON(!new.frozen);

        } while (!__cmpxchg_double_slab(s, page,
            prior, counters,
            freelist, new.counters,
            "drain percpu freelist"));

        freelist = nextfree;
    }
redo:

    old.freelist = page->freelist;
    old.counters = page->counters;
    VM_BUG_ON(!old.frozen);

    /* Determine target state of the slab */
    new.counters = old.counters;
    /* 此时freelist指向移除的slab的最后一个可用对象 */
    if (freelist) {
        new.inuse--;
        /*
         将freelist指向page->freelist,即slab中
         最后一个空闲对象 */
        set_freepointer(s, freelist, old.freelist);
        new.freelist = freelist;
    } else
        /* new.freelist = old.freelist = page->freelist */
        new.freelist = old.freelist;
    /* 解冻 */
    new.frozen = 0;

    /*
     slab为空,并且节点的partial slab数量大于最小值,
     可以释放要移除的slab,设置为M_FREE状态 */
    if (!new.inuse && n->nr_partial >= s->min_partial)
        m = M_FREE;
    /* slab的freelist有可用对象,应该放到partial表 */
    else if (new.freelist) {
        m = M_PARTIAL;
        if (!lock) {
            lock = 1;
            spin_lock(&n->list_lock);
        }
    /* 否则是M_FULL */
    } else {
        m = M_FULL;
        if (kmem_cache_debug(s) && !lock) {
            lock = 1;
            spin_lock(&n->list_lock);
        }
    }

    if (l != m) {
        if (l == M_PARTIAL)

            remove_partial(n, page);

        else if (l == M_FULL)

            remove_full(s, n, page);

        if (m == M_PARTIAL) {

            add_partial(n, page, tail);
            stat(s, tail);

        } else if (m == M_FULL) {

            stat(s, DEACTIVATE_FULL);
            add_full(s, n, page);

        }
    }

    l = m;
    /*
     如果修改page的freelist失败,回到redo重新执行。
     这时需要根据l的状态先把之前添加到节点的slab移除,
     然后再插入,以保证一致性 */
    if (!__cmpxchg_double_slab(s, page,
                old.freelist, old.counters,
                new.freelist, new.counters,
                "unfreezing slab"))
        goto redo;

    if (lock)
        spin_unlock(&n->list_lock);

    if (m == M_FREE) {
        stat(s, DEACTIVATE_EMPTY);
        discard_slab(s, page);
        stat(s, FREE_SLAB);
    }
}

6. 总结

每个 struct kmem_cache 包含所有内存节点的partial slab信息,每个节点都有一个 struct list_head partial ,保存当前节点的所有partial slab;还有 unsigned long nr_partials 保存本节点partial slab的总数。

struct kmem_cache 包含所有CPU的slab信息,每个CPU只有一个正在分配对象的slab,即 struct page *page 成员,以及指向当前可用对象的指针 void **freelist
同时还有属于本CPU的partial slab列表 strut page *partial ,这些partial slab来自CPU的partial list,CPU会冻结属于自己的partial slab——别的CPU只能释放对象,不能执行其他操作。

创建一个新的slab时, struct page 的成员 freelist 会被置空,只通过CPU的 freelist 分配空闲对象。释放对象时也只操作CPU的 freelist
如果其他CPU释放不属于本CPU的slab的对象,直接操作 struct pagefreelist ,这样在移除slab时,可以根据这个信息判断是否有异地释放对象的操作。