• Linux下的物理内存管理2-slab缓存的管理


    2017-03-02

    在Linux下的物理内存管理中,对SLAB机制大致做了介绍,对SLAB管理结构对象也做了介绍,但是对于小内存块的分配没有介绍,本节重点介绍下slab对小内存块的管理。


     内核中使用全局的kmem_cache数组kmalloc_caches组织不同大小的缓存块,每个缓存块由一个kmem_cache结构描述,缓存块大小一般是按8字节递增,分配时不足8字节按照8字节算,依次向上舍入。内核有两种方式根据size获取对应的阶

    1、使用一个size_index数组保存对应内存块的阶,当size小于192时,使用此数组。

    static s8 size_index[24] = {
        3,    /* 8 */
        4,    /* 16 */
        5,    /* 24 */
        5,    /* 32 */
        6,    /* 40 */
        6,    /* 48 */
        6,    /* 56 */
        6,    /* 64 */
        1,    /* 72 */
        1,    /* 80 */
        1,    /* 88 */
        1,    /* 96 */
        7,    /* 104 */
        7,    /* 112 */
        7,    /* 120 */
        7,    /* 128 */
        2,    /* 136 */
        2,    /* 144 */
        2,    /* 152 */
        2,    /* 160 */
        2,    /* 168 */
        2,    /* 176 */
        2,    /* 184 */
        2    /* 192 */
    };

    2,、当size大于192时,使用fls函数。

    小内存块的总体分配架构如下图所示:

    根据请求分配的内存的size,通过size_index_elem(size)获取该size对应的index,上篇文章介绍过,块的大小按照2的阶计算。kmalloc_caches数组中的下标其实也是表示了块的大小即2^n字节。

    下面结合内核kmalloc函数执行流程,分析下具体的小内存块的分配。正常情况下会调用到__kmalloc函数,该函数主体可分为两部分:根据size得到对应的缓存块结构kmem_cache;从缓存块中取对象;

    前者使用kmalloc_slab函数,而后者调用slab_alloc函数。从这里我们仔细深入分析:

    struct kmem_cache *kmalloc_slab(size_t size, gfp_t flags)
    {
        int index;
        /*如果size大于最大值,则返回NULL*/
    
        if (size > KMALLOC_MAX_SIZE) {
            WARN_ON_ONCE(!(flags & __GFP_NOWARN));
            return NULL;
        }
    
        if (size <= 192) {
            if (!size)
                return ZERO_SIZE_PTR;
            
            index = size_index[size_index_elem(size)];
        } else
            index = fls(size - 1);
    
    #ifdef CONFIG_ZONE_DMA
        if (unlikely((flags & GFP_DMA)))
            return kmalloc_dma_caches[index];
    
    #endif
        return kmalloc_caches[index];
    }

    kmalloc_slab代码本身比较简单,先对size做了判断,看是否符合要求,如果size小于等于192,则从size_index数组中获取对应的阶;否则调用fls函数获取对应的阶。最后根据此阶作为下标,获取对应的kmem_cache对象。而内存分配的重点在slab_alloc函数,该函数中直接调用了slab_alloc_node函数

    static __always_inline void *
    slab_alloc_node(struct kmem_cache *cachep, gfp_t flags, int nodeid,
               unsigned long caller)
    {
        unsigned long save_flags;
        void *ptr;
        int slab_node = numa_mem_id();
    
        flags &= gfp_allowed_mask;
    
        lockdep_trace_alloc(flags);
    
        if (slab_should_failslab(cachep, flags))
            return NULL;
    
        cachep = memcg_kmem_get_cache(cachep, flags);
    
        cache_alloc_debugcheck_before(cachep, flags);
        local_irq_save(save_flags);
    
        if (nodeid == NUMA_NO_NODE)
            nodeid = slab_node;
    
        if (unlikely(!cachep->node[nodeid])) {
            /* Node not bootstrapped yet */
            ptr = fallback_alloc(cachep, flags);
            goto out;
        }
    
        if (nodeid == slab_node) {
            /*
             * Use the locally cached objects if possible.
             * However ____cache_alloc does not allow fallback
             * to other nodes. It may fail while we still have
             * objects on other nodes available.
             */
            ptr = ____cache_alloc(cachep, flags);
            if (ptr)
                goto out;
        }
        /* ___cache_alloc_node can fall back to other nodes */
        ptr = ____cache_alloc_node(cachep, flags, nodeid);
      out:
        local_irq_restore(save_flags);
        ptr = cache_alloc_debugcheck_after(cachep, flags, ptr, caller);
        kmemleak_alloc_recursive(ptr, cachep->object_size, 1, cachep->flags,
                     flags);
    
        if (likely(ptr))
            kmemcheck_slab_alloc(cachep, flags, ptr, cachep->object_size);
    
        if (unlikely((flags & __GFP_ZERO) && ptr))
            memset(ptr, 0, cachep->object_size);
    
        return ptr;
    }

    函数u首先获取了节点ID即slab_node,如果参数中未指定具体的node,默认从当前节点开始分配。中间是复杂的检查机制,而实质性的工作在____cache_alloc函数中

    static inline void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags)
    {
        void *objp;
        struct array_cache *ac;
        bool force_refill = false;
    
        check_irq_off();
        /*先从CPU 缓存中取*/
        ac = cpu_cache_get(cachep);
        /**/
        if (likely(ac->avail)) {
            ac->touched = 1;
            objp = ac_get_obj(cachep, ac, flags, false);
    
            /*
             * Allow for the possibility all avail objects are not allowed
             * by the current flags
             */
            if (objp) {
                STATS_INC_ALLOCHIT(cachep);
                goto out;
            }
            force_refill = true;
        }
    
        STATS_INC_ALLOCMISS(cachep);
        objp = cache_alloc_refill(cachep, flags, force_refill);
        /*
         * the 'ac' may be updated by cache_alloc_refill(),
         * and kmemleak_erase() requires its correct value.
         */
        ac = cpu_cache_get(cachep);
    
    out:
        /*
         * To avoid a false negative, if an object that is in one of the
         * per-CPU caches is leaked, we need to make sure kmemleak doesn't
         * treat the array pointers as a reference to the object.
         */
        if (objp)
            kmemleak_erase(&ac->entry[ac->avail]);
        return objp;
    }

    可以看到,该函数中首先根据参数中的kmem_cache获取当前CPU对应的array_cache,array_cache结构如下:

    struct array_cache {
        unsigned int avail;//可用对象的数目
        unsigned int limit;//可拥有的最大对象的数目
        unsigned int batchcount;//
        unsigned int touched;
        spinlock_t lock;
        void *entry[];    /*主要是为了访问后面的对象
                 * Must have this definition in here for the proper
                 * alignment of array_cache. Also simplifies accessing
                 * the entries.
                 *
                 * Entries should not be directly dereferenced as
                 * entries belonging to slabs marked pfmemalloc will
                 * have the lower bits set SLAB_OBJ_PFMEMALLOC
                 */
    };

    这个结构per_CPU 缓存,每个CPU都有对应的缓存,其中avail对应当前缓存中可用对象的数目,limit表示可拥有的最大对象的数目,batchcount表示在缓存为空时,需要填充的对象的数量,touched是一个活动位,表示当前缓存的活跃程度,便于后续的收缩操作。entry指向该缓存的对象数组,数组中保存的是对象的地址,这里就表示缓存块的地址。有一点就是从这里分配缓存对象不是从数组起始位置分配,而是从数组末尾分配,avail就表示分配的对象在entry数组中的下标,这里发现设计的还真是巧妙。

    回到____cache_alloc函数中,如果当前CPU缓存中有可用对象,则设置首先设置活跃位,然后调用ac_get_obj函数从缓存中获取一个对象。如果没有可用的对象,则调用cache_alloc_refill函数填充缓存,之后再次调用cpu_cache_get获取对象。获取对象之后需要调用kmemleak_erase函数设置entry数组中的对应指针为NULL。到这里发现主要有两个操作,获取对象,填充缓存。

    先看获取对象ac_get_obj

    static inline void *ac_get_obj(struct kmem_cache *cachep,
                struct array_cache *ac, gfp_t flags, bool force_refill)
    {
        void *objp;
    
        if (unlikely(sk_memalloc_socks()))
            objp = __ac_get_obj(cachep, ac, flags, force_refill);
        else
            objp = ac->entry[--ac->avail];
    
        return objp;
    }

    sk_memalloc_socks意思暂不清楚,从unlikely可以看到这里大部分都可以直接通过entry得到对象,所以获取对象的方式还是挺简单的,直接从根据avail从entry数组中获的一个对象地址即可。并且这里avail应该指向首个为NULL的entry。

    如果缓存中没有呢,就需要填充per_CPU 缓存了,这里看cache_alloc_refill函数

    static void *cache_alloc_refill(struct kmem_cache *cachep, gfp_t flags,
                                bool force_refill)
    {
        int batchcount;
        struct kmem_cache_node *n;
        struct array_cache *ac;
        int node;
    
        check_irq_off();
        /*获取当前节点kmem_cache_node*/
        node = numa_mem_id();
        if (unlikely(force_refill))
            goto force_grow;
    retry:
        ac = cpu_cache_get(cachep);
        /*要填充对象的数目*/
        batchcount = ac->batchcount;
        /*如果当前缓存访问并不频繁,且要填充的数目较多,减少分配的数目*/
        if (!ac->touched && batchcount > BATCHREFILL_LIMIT) {
            /*
             * If there was little recent activity on this cache, then
             * perform only a partial refill.  Otherwise we could generate
             * refill bouncing.
             */
            batchcount = BATCHREFILL_LIMIT;
        }
        /**/
        n = cachep->node[node];
    
        BUG_ON(ac->avail > 0 || !n);
        /*枷锁*/
        spin_lock(&n->list_lock);
    
        /* See if we can refill from the shared array */
        if (n->shared && transfer_objects(ac, n->shared, batchcount)) {
            n->shared->touched = 1;
            goto alloc_done;
        }
    
        while (batchcount > 0) {
            struct list_head *entry;
            struct slab *slabp;
            /* Get slab alloc is to come from. */
            /*首先从slabs_partial链表中分配*/
            entry = n->slabs_partial.next;
            /*如果半分配的slab链表为空*/
            if (entry == &n->slabs_partial) {
                n->free_touched = 1;
                /*从free链表分配*/
                entry = n->slabs_free.next;
                /*如果free链表满,则增长*/
                if (entry == &n->slabs_free)
                    goto must_grow;
            }
            /*由entry定位slab的地址*/
            slabp = list_entry(entry, struct slab, list);
            check_slabp(cachep, slabp);
            check_spinlock_acquired(cachep);
    
            /*
             * The slab was either on partial or free list so
             * there must be at least one object available for
             * allocation.
             */
            BUG_ON(slabp->inuse >= cachep->num);
            /*循环从slab获取对象*/
            while (slabp->inuse < cachep->num && batchcount--) {
                STATS_INC_ALLOCED(cachep);
                STATS_INC_ACTIVE(cachep);
                STATS_SET_HIGH(cachep);
                /*填充缓存*/
                ac_put_obj(cachep, ac, slab_get_obj(cachep, slabp,
                                        node));
            }
            check_slabp(cachep, slabp);
    
            /* move slabp to correct slabp list: */
            /*把slab移除链表*/
            list_del(&slabp->list);
            /*根据情况添加到对应的链表*/
            if (slabp->free == BUFCTL_END)
                list_add(&slabp->list, &n->slabs_full);
            else
                list_add(&slabp->list, &n->slabs_partial);
        }
    
    must_grow:
        n->free_objects -= ac->avail;
    alloc_done:
        spin_unlock(&n->list_lock);
    
        if (unlikely(!ac->avail)) {
            int x;
    force_grow:
            x = cache_grow(cachep, flags | GFP_THISNODE, node, NULL);
    
            /* cache_grow can reenable interrupts, then ac could change. */
            ac = cpu_cache_get(cachep);
            node = numa_mem_id();
    
            /* no objects in sight? abort */
            if (!x && (ac->avail == 0 || force_refill))
                return NULL;
    
            if (!ac->avail)        /* objects refilled by interrupt? */
                goto retry;
        }
        ac->touched = 1;
    
        return ac_get_obj(cachep, ac, flags, force_refill);
    }

    代码部分并不难理解,首先获取当前NUMA节点ID,从而在后面获取对应的kmem_cache_node结构,该结构中保存了与其关联的slab,然后获取当前CPU的缓存对象array_cache。根据其中的touched字段判断是否该缓存是否活跃,如果不活跃且要求充填的对象数还比较多,就把填充对象数设置成BATCHREFILL_LIMIT。然后即根据NUMA ID 从kmem_cache结构的node数组中获取kmem_cache_node结构。首先尝试从共享数组中分配,这相当于又添加了一层缓存。如果共享数组为NULL或者从共享数组填充失败,则走正规途径即从slab链表中分配。进入while循环

    前面提到过NUMA架构下,每个NODE关联三条SLAB链表,分别表示full,partial,free的slab.这里首先尝试的是从未使用完的链表中分配,如果该链表为NULL,则从free链表中分配,如果free链表也为NULL,则必须must_grow的途径,从伙伴系统申请对象。到这里不管从哪里,已经获取一个slab了,接下来从该slab分配对象,再次进入一个while循环。这里重要的是首先通过slab_get_obj函数从slab中获取一个对象,然后通过ac_put_obj函数把对象填充到CPU关联的缓存数组中。看下两个函数

    static void *slab_get_obj(struct kmem_cache *cachep, struct slab *slabp,
                    int nodeid)
    {
        void *objp = index_to_obj(cachep, slabp, slabp->free);
        kmem_bufctl_t next;
    
        slabp->inuse++;
        next = slab_bufctl(slabp)[slabp->free];
    #if DEBUG
        slab_bufctl(slabp)[slabp->free] = BUFCTL_FREE;
        WARN_ON(slabp->nodeid != nodeid);
    #endif
        slabp->free = next;
    
        return objp;
    }

     首先从SLAB中取一个对象,更新计数器,然后获取下一个可用对象的索引,设置到slab的free字段,接着就返回了。

    static inline void ac_put_obj(struct kmem_cache *cachep, struct array_cache *ac,
                                    void *objp)
    {
        if (unlikely(sk_memalloc_socks()))
            objp = __ac_put_obj(cachep, ac, objp);
    
        ac->entry[ac->avail++] = objp;
    }

    而put函数就更显简单,直接在对象地址和entry数组建立关联即可。从这里看avail同样是指首个为NULL的索引。这样while循环结束,填充per_CPU 缓存的工作节完成了,接来下要做的就是把SLAB从对应的链表摘下来。如果里面的对象已经用完,则假如到full链表,否则加入到slabs_partial链表。

    最后会通过ac_get_obj从当前CPU的缓存entry数组中获取一个对象,返回。而在最终,需要设置entry中分配出去的对象位置为NULL。至此,小缓存块的分配工作就完成了!

    总结:

      其实小缓存块的管理和普通对象的管理方式基本类似,都是作为对象存储,不同的是一般对象的大小不确定,而小缓存块的大小由系统初始指定;一般对象的缓存可以自己创建,而小缓存块由系统维护。其余的分配流程、保存方式等都是使用相同的API。

  • 相关阅读:
    Java中RuntimeException和Exception
    RuntimeException和Exception的区别
    Spring事务异常回滚
    iOS 卖票中多线程分析;
    凝视转换(部分)
    HDU 5386 Cover(模拟)
    iOS开发之软键盘使用小技巧
    【每日算法】高速幂
    CKEditor高级编辑器
    iOS开发 剖析网易新闻标签栏视图切换(addChildViewController属性介绍)
  • 原文地址:https://www.cnblogs.com/ck1020/p/6492067.html
Copyright © 2020-2023  润新知