暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

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

术道经纬 2020-05-18
655
上文介绍了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,并进行一定的计算,以寻找最合适的。
第一级分配
每个"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);
...
}
第二级分配
如果在申请时数组为空,那么就需要从全局的slab中refill,那选择哪个slab呢?

为了减少内存碎片,一个cache的slabs根据使用状态被分成了三类:
  • 全部分配完,没有空闲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中选择。

第三级分配
如果一个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,这会带来相当的管理上的开销。

文章转载自术道经纬,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论