首发于术道经纬
内存分配[五] - Linux中的Slab(2)

内存分配[五] - Linux中的Slab(2)

上文介绍了slab cache的组成,接下来看下如何通过slab cache来分配和释放内存。

【分配object】

内核编程中如果需要申请在物理上连续的内存,最常用的函数就是kmalloc()了,而它的底层实现依靠的正是slab cache。

static __always_inline void *__do_kmalloc(size_t size, gfp_t flags, ...)
{
    if (unlikely(size > KMALLOC_MAX_CACHE_SIZE))
        return NULL;
    struct kmem_cache *cachep = kmalloc_slab(size, flags);
    void * ret = slab_alloc(cachep, flags, ...);

    return ret;
}

如果传入的参数是"sizeof(struct inode)"这样的,可能刚好有object大小完全匹配的cache。如果参数是一个普通的整数(比如100),那就需要遍历cache,并进行一定的计算,以寻找最合适的。

第一级分配(fast path)

每个"kmem_cache"除了包含多个slabs外,还包含一组"cpu_cache",这是一种per-CPU的cache,是slab的cache。这么说可能有点绕口,其实就是一个软件层面的二级cache。拿硬件cache来类比,"cpu_cache"就是L1 cache,slab就是L2 cache。

struct array_cache __percpu *cpu_cache;

struct array_cache {
    unsigned int avail;
    unsigned int limit;
    void *entry[]; 
};

"entry[]"表示了一个遵循LIFO顺序的数组(Last In, First Out),"avail"和"limit"分别指定了当前可用objects的数目和允许容纳的最大数目。

每当object试图从slab中分配内存时,都先从所在CPU对应的"entry[]"数组中获取。per-CPU的设计可以减少SMP系统对全局slab的锁的竞争,一些CPU bound的线程尤其适合使用CPU bound的内存分配。

void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
    void *objp;

    struct array_cache *ac = cpu_cache_get(cachep);
    if (likely(ac->avail)) {
        objp = ac->entry[--ac->avail];
        return objp;
    }

    objp = cache_alloc_refill(cachep, flags);
    ...
}
第二级分配(slow path)

如果在申请时数组为空,那么就需要从全局的slab中refill,那选择哪个slab呢?为了减少内存碎片,一个cache的slabs在每个内存node中(per-node),根据使用状态被分成了三类:全部分配完,没有空闲objects的full slabs。

  • 分配了一部分,尚有部分空闲的partial slabs。
  • 完全空闲可用的free slabs。
struct kmem_cache_node *node[MAX_NUMNODES];

struct kmem_cache_node {
    struct list_head slabs_partial; 
    struct list_head slabs_full;
    struct list_head slabs_free;
    ...
}

应首先从partial slabs中选择,等partial slabs都满了,成为了full slab,这时再从free slabs中选择。

又是per CPU,又是per node,在大型系统中,这是一笔不小的开销。

第三级分配(very slow path)

如果一个cache连free slab都没有了,那就需要新增一个slab来“扩容”了。新增slab的方法在上文已经介绍过了,最终是向buddy系统申请,回到这篇文章描述的page level分配器的代码路径。

【释放object】

释放时也归还到所在CPU的"cpu_cache"数组,如果释放导致数组溢出,则数组中的一部分entries将被返还到全局的slab中。

void ___cache_free(struct kmem_cache *cachep, void *objp, ..)
{
    struct array_cache *ac = cpu_cache_get(cachep);

    if (ac->avail >= ac->limit) {
        // 归还到对应的slab
        cache_flusharray(cachep, ac);
    }

    ac->entry[ac->avail++] = objp;
}

我们平时常调用的接口是kfree(),只有一个表示地址的参数,那如何知道应该归还到哪个slab中?寻找的方法是首先根据object的虚拟地址找到object所在的page frame,进而找到这个page frame所在compound page的中的head page,而head page中的"slab_cache"指针就指向了这个object所属的slab。

void kfree(const void *objp)
{
    struct kmem_cache *c = virt_to_cache(objp);
    __cache_free(c, (void *)objp, _RET_IP_);
    ...
}

struct kmem_cache *virt_to_cache(const void *obj)
{
    struct page *page = virt_to_head_page(obj);
    return page->slab_cache;
}

struct page *virt_to_head_page(const void *x)
{
    // object所在的page frame
    struct page *page = virt_to_page(x);
    // 所在compound page的head page
    return compound_head(page);
}

借助这条路径,进而还可以知道一个object的大小。

size_t __ksize(const void *objp)
{
    struct kmem_cache *c= virt_to_cache(objp);
    size_t size = c ? c->object_size : 0;
}
 

【小结】

至此,slab分配器的原理和在Linux中的实现就粗略的介绍完了,可以借助下面这张图来一览它的构成,包括kernel object, page frame和slab cache的关系,物理内存的组织和分配等。

slab分配器对内存的利用率是比较高的,因为充分借助了各种缓存机制,分配和释放的速度也比较理想。存在的缺点就是要为内核中众多的objects维护独立的cache,这会带来相当的管理上的开销。


参考:

tjtech.me/fix-an-issue-


原创文章,转载请注明出处。

发布于 05-14

文章被以下专栏收录