mm-slab对象的回收

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

1. kfree

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

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,执行快路径;否则执行慢路径。

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。

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对象。

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中:

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 分支。

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 中的宏定义,包括以双下划线开头的和不以双下划线开头的,后者是原子的,前者不是原子的。

#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 可能位于当前的节点。

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时,可以根据这个信息判断是否有异地释放对象的操作。

你可能感兴趣的:(mm-slab对象的回收)