本博文只对SLUB做概念上的介绍,贴上少量的代码,大家有个初步认知即可。若想深入了解,可以走读内核代码,推荐学习该博客图解SLUB。

1.why need SLUB?

Linux的物理内存管理采用了以页为单位的buddy system(伙伴系统)。在编写用户空间程序,当需要申请内存/释放时,我们只需要调用malloc/free接口,内存分配器(ptmalloc2、jemalloc等)就会根据自己的策略向操作系统申请内存(也可能不需要OS,比如首次申请20Bytes,ptmalloc2会直接给你128KB,后续申请的40Bytes就不会涉及到OS)。在内核空间,也需要有类似的内存分配器,不然在需要申请小对象时就只能分配一整页了;同时,内核对对象的管理又有一定的特殊性,有些对象的访问非常频繁,需要采用缓冲机制。因此SLAB分配器就登场了。

Linux提供了3中分配器方案,2.6.23之后的版本,SLUB成了默认的分配器。在rgos系统,有使用SLAB和SLUB的,可以看内核配置宏,或者直接在设备看是否有/sys/kernel/slab目录,有就是SLUB没有就是SLAB。

2. what is SLUB?

Linux使用数据结构struct kmem_cache来描述缓存池,系统内所有的kmem_cache都挂到一个链表上,每个缓存池都包含若干个slab,每个slab由若干个连续的物理页帧组成,每个slab包含固定数目的对象。如下所示:

kmem_cache包含两个关键的数据结构: struct kmem_cache_cpu和struct kmem_cache_node,结构体都比较简单:


struct kmem_cache_cpu { 
    void **freelist;    /* 指向下一个可用的object */
    unsigned long tid;  /* 主要用来同步作用 */
    struct page *page;  /* slab内存的page指针 */
#ifdef CONFIG_SLUB_CPU_PARTIAL
    struct page *partial;   /* 本地slab partial链表。主要是一些部分使用object的slab */
#endif
}; 

struct kmem_cache_node {
    spinlock_t list_lock;     /* 自旋锁,保护数据 */
    unsigned long nr_partial; /* slab节点中的slab数量 */
    struct list_head partial; /* slab节点的slab partial链表,和struct kmem_cache_cpu的partial链表功能类似 */
};
/* SLUB接口 */
struct kmem_cache *kmem_cache_create(const char *name,
        size_t size,
        size_t align,
        unsigned long flags,
        void (*ctor)(void *)); /* 创建一个缓存池 */
void kmem_cache_destroy(struct kmem_cache *); /* 销毁一个缓冲池 */
void *kmem_cache_alloc(struct kmem_cache *cachep, int flags);  /* 从指定缓存池中分配一个对象 */
void kmem_cache_free(struct kmem_cache *cachep, void *objp);   /* 对象交还给缓存池 */

基于SLUB的完整视图如下所示:

在这里简要描述下申请对象的过程:fast-path、slow-path以及very-slow-path。

  1. fast-path:当kmem_cache_cpu(本地缓存池)的slab(数据结构中的page)有空闲对象时,也即freelist不为空,则直接将该空闲对象分配出去。
  2. slow-path:当freelist为空时,也即这个slab没有空闲对象,若此时本地slab partial链表不为空(表明首个slab有空闲的对象),则将该slab挂到page上,从中分配空闲的对象。
  3. very-slow-path:当本地缓存池没有空闲对象,此时发现per node partial链表中有可用slab用于分配,那么就会从per node partial链表中取下一个slab挂到page上,用于分配对象;如果连node链表也为空,这是最糟糕的情形,则需要向伙伴系统申请一个slab,挂到per cpu的page上,从其中分配对象。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

网上有个非常贴切的比喻:kmem_cache是“零售商”,包含两个部门,kmem_cache_cpu是“营业厅”,kmem_cache_node是“仓库”。“营业厅”又包含“展示厅”*page和“储物间”*partial,其中“展示厅”仅包含一个slab。当展示厅的物品(即对象)售完时直接从储物间拿货(slab),若储物间都售尽,则需要从总部仓库拿货(slab),若总部仓库都售尽,则需要“工厂”(即伙伴分配系统)生产新的货品(slab)。

3.kmalloc

kmalloc用于内核获取物理地址连续(虚拟地址也是连续的)的空间,其也是基于SLAB机制实现的。

/* /mm/slab_common.c */
struct kmem_cache *kmalloc_caches[KMALLOC_SHIFT_HIGH + 1];
EXPORT_SYMBOL(kmalloc_caches);
/*
 * kmalloc 缓存池
 * kmalloc_info[] is to make slub_debug=,kmalloc-xx option work at boot time.
 * kmalloc_index() supports up to 2^26=64MB, so the final entry of the table is
 * kmalloc-67108864.
 */
static struct {
    const char *name;
    unsigned long size;
} const kmalloc_info[] __initconst = {
    {NULL,                      0},     {"kmalloc-96",             96},
    {"kmalloc-192",           192},     {"kmalloc-8",               8},
    {"kmalloc-16",             16},     {"kmalloc-32",             32},
    {"kmalloc-64",             64},     {"kmalloc-128",           128},
    {"kmalloc-256",           256},     {"kmalloc-512",           512},
    {"kmalloc-1024",         1024},     {"kmalloc-2048",         2048},
    {"kmalloc-4096",         4096},     {"kmalloc-8192",         8192},
    {"kmalloc-16384",       16384},     {"kmalloc-32768",       32768},
    {"kmalloc-65536",       65536},     {"kmalloc-131072",     131072},
    {"kmalloc-262144",     262144},     {"kmalloc-524288",     524288},
    {"kmalloc-1048576",   1048576},     {"kmalloc-2097152",   2097152},
    {"kmalloc-4194304",   4194304},     {"kmalloc-8388608",   8388608},
    {"kmalloc-16777216", 16777216},     {"kmalloc-33554432", 33554432},
    {"kmalloc-67108864", 67108864}
};
/* 创建cache */
static void __init new_kmalloc_cache(int idx, unsigned long flags)
{
    kmalloc_caches[idx] = create_kmalloc_cache(kmalloc_info[idx].name,
                    kmalloc_info[idx].size, flags);
}

我们在调用kmalloc接口时,会根据入参size判断,选择一个最合适的kmalloc-cache。

4.SLUB 的debug机制

SLUB提供非常强大的内存检测功能(但KASAN才是最强的),包括:

  • 内存泄露(leak),alloc之后忘了free,导致内存占用不断增长
  • 越界(overrun),访问了alloc分配的区域之外的内存,覆盖了不属于自己的数据
  • 使用已经释放的内存(use after free)
  • 使用未经初始化的数据(use uninitialised bytes)