优雅的slab内存分配器(一)——slab内存分配器基础知识

slab内存分配器是linux内核中比较经典的内存分配器,目前已经被slub内存分配器取代了。不过为了致敬经典,更是为理解slub分配器做铺垫,我还是会从slab分配器讲起。

为什么需要slab内存分配器?


这个问题其实很好回答,之所以提出slab分配器,是因为buddy system只能按page对齐来分配内存。然而大多数情况下,需要的内存size都不是按page对齐的,如果直接通过buddy system分配内存,就会出现很大的内存碎片,内存碎片即分配了却没有使用,也无法再被分配的内存。

正是由于buddy system的这种限制,slab分配器应运而生。slab分配器的底层依托于buddy system,上层却对用户提供了更加灵活的内存分配服务。

slab分配器工作机制主要包括两点:

  1. 从buddy system分配pages,放入slab分配器内存池,也可以称为cache。
  2. 用户调用slab分配器提供的内存分配接口如kmalloc,从slab分配器内存池中分配内存,内存的size没有按page size对齐的要求。

当然,以上两点只是slab分配器最本质的工作机制,我们后面会介绍细节。

slab分配器基础概念


什么是slab?

slab是slab内存分配器从buddy system申请页面的基本单位。然而slab的大小不是固定的,slab从属于某个kmem cache实例,不同的kmem cache实例,其slab的大小是不同的。slab的大小必须是2^order个pages,order不能超过buddy system所支持的最大的order。

slab内存分配器从buddy system分配了slab之后,会将其挂在对应的kmem cache实例的node节点。

什么是object?

object是slab内存分配器对外提供的申请内存的基本单位。slab内存分配器从buddy system申请了buddy之后,会将其拆分成一个个object,并缓存在kmem cache实例的cpu_cache中,用户申请内存时,其实获取的就是一个个object。

一旦object缓存耗尽,就会重新从buddy system申请slab,并将其拆分成object,放入内存池。

什么是cache?

slab内存分配器中的cache跟硬件cache无关,是一个纯软件的概念。slab内存分配器有两种cache,一个是slab的cache,一个是object的cache。slab内存分配器从buddy system获取页面后,会将其加入kmem cache的node节点,这个就是slab的cache;将slab拆分成多个object,并将object加入kmem cache的cpu_cache内存池,这个就是object的cache;可以看到这两种cache实际是对共同的物理页面的两种缓存形式。

有哪些kmem cache实例?

  1. 一个静态分配的kmem cache实例 “kmem_cache”:kmem_cache_boot
    该kmem cache实例是为创建其他kmem cache实例分配空间的。
  2. 一个预分配的kmem cache实例“kmalloc-node”:kmalloc_caches[INDEX_NODE]
    该kmem cache实例是为创建struct kmem_cache_node实例分配空间的。
  3. 预分配多个面向用户的kmem cache实例
    这些kmem cache实例预分配,其初始信息定义在kmalloc_info中。这些kmem cache实例的object size是预定义好的。普通用户一般使用这些预分配的kmem cache实例即可,当然这些kmem cache实例对用户来说也是不可见的,用户通过调用kmalloc函数,就会根据所要分配的memory size找到最合适的kmem cache实例。

    用户直接用使用预分配的kmem cache有一个很明显的缺陷,就是会产生内存碎片。因为用户申请的memory size并不总是等于kmem cache所维护的object size,slab分配器会选择object size不小于memory size并且两者差值最小的kmem cache,这就会产生内存碎片。

  4. 用户动态分配的kmem cache实例
    对于一些有想法的用户,可以调用kmem_cache_create动态分配kmem cache实例,通过该kmem cache申请内存不能再使用通用的kmalloc,而需要使用kmem_cache_alloc函数来分配。

    好处也是显而易见,需要多大的内存,就创建多个的object,不存在内存碎片。那么为什么内存不抛弃预分配的kmem cache实例,完全采用该方案呢?其实稍微思考一下就明白了,用户需要分配的memory size五花八门,每一种memory size所需要分配的次数也是不确定的,完全采用这种方案会造成更大的浪费。

slab size和object size如何确定?

slab内存分配器维护了多个kmem cache实例,调用kmem_cache_create创建kmem cache实例的时候,会设置object size。而slab size是根据object动态选择的,选择的步骤如下:

  1. 从order为0开始选择
  2. 计算该order对应pages分配给object后,剩余多少空间无法使用。
  3. 如果剩余的空间不超过该order对应的空间的八分之一,则认为可以接受,该order作为slab的order,选择成功。
  4. 如果剩余空间超过该order对应的空间的八分之一,则继续判断下一个order。

slab内存分配器基本数据结构


理解一个模块,必须要先理解其数据结构,以及各个数据结构之间的关系,一旦将这些搞清楚,代码逻辑顺理成章就看明白了。当然,要理解数据结构,必须要辅以阅读代码逻辑,二者也算是相辅相成。

另外,我并不赞成一开始就将数据结构中所有的成员全部搞清楚,学习是一个迭代的过程,一开始只需要将主要的成员及成员关系搞清楚就可以了。后面随着对模块理解的深入,之前一些难以理解的成员,可能自然而然就理解了。

slab分配器的数据结构不多,也很简单,数据结构之间的关系也不复杂。所以slab分配器并不复杂,我用一个词来给slab分配器贴上一个标签的话,我会选择优雅这个词。

struct kmem_cache

结构体定义如下(删除了一些无关紧要的成员):

struct kmem_cache {
    struct array_cache __percpu *cpu_cache;

/* 1) Cache tunables. Protected by slab_mutex */
    unsigned int batchcount;
    unsigned int limit;
    unsigned int shared;

    unsigned int size;
    struct reciprocal_value reciprocal_buffer_size;
/* 2) touched by every alloc & free from the backend */

    unsigned int flags;     /* constant flags */
    unsigned int num;       /* # of objs per slab */

/* 3) cache_grow/shrink */
    /* order of pgs per slab (2^n) */
    unsigned int gfporder;

    /* force GFP flags, e.g. GFP_DMA */
    gfp_t allocflags;

    size_t colour;          /* cache colouring range */
    unsigned int colour_off;    /* colour offset */
    struct kmem_cache *freelist_cache;
    unsigned int freelist_size;

    /* constructor func */
    void (*ctor)(void *obj);

/* 4) cache creation/removal */
    const char *name;
    struct list_head list;
    int refcount;
    int object_size;
    int align;


    struct kmem_cache_node *node[MAX_NUMNODES];
};

该结构体是slab内存分配器最顶层的结构体,其他结构体基本上都从属于该结构体。

成员 解释
cpu_cache 该kmem cache实例中object的缓存
batchcount
limit
shared
size
reciprocal_buffer_size
flags 描述内存属性的标识
num 一个slab中object的数量
gfporder 一个slab的order,即2^order个连续物理页面
allocflags 内存分配的标识
colour
colour_off
freelist_cache 为freelist分配空间的kmem cache实例
freelist_size freelist的空间的大小,freelist是一段连续的内存空间,这段内存空间存放object 的索引,所以freelist size跟object的数量有关。freelist区域可以存放在slab内部,也可以存放在slab外面,此时需要专门为freelist分配空间,就会用到freelist_cache.
ctor
name kmem cache实例的名字
list
refcount
object_size object的size
align
node 用于管理该kmem cache实例中所有的slab

struct kmem_cache_node

结构体定义如下(删除了一些无关紧要的成员):

struct kmem_cache_node {
    spinlock_t list_lock;

#ifdef CONFIG_SLAB
    struct list_head slabs_partial; /* partial list first, better asm code */
    struct list_head slabs_full;
    struct list_head slabs_free;
    unsigned long num_slabs;
    unsigned long free_objects;
    unsigned int free_limit;
    unsigned int colour_next;   /* Per-node cache coloring */
    struct array_cache *shared; /* shared per node */
    struct alien_cache **alien; /* on other nodes */
    unsigned long next_reap;    /* updated without locking */
    int free_touched;       /* updated without locking */
#endif
};

该结构体用于管理所有的slab。

成员 解释
slabs_partial 包含空闲和非空闲object的slab放在该链表
slabs_full 不包含空闲object的slab放在该链表
slabs_free 只包含空闲object的slab放在该链表
num_slabs slab的数量
free_objects 空闲object的数量
free_limit
colour_next
shared
alien
next_reap
free_touched

struct array_cache

结构体定义如下(删除了一些无关紧要的成员):

struct array_cache {
    unsigned int avail;
    unsigned int limit;
    unsigned int batchcount;
    unsigned int touched;
    void *entry[];  /*
             * Must have this definition in here for the proper
             * alignment of array_cache. Also simplifies accessing
             * the entries.
             */
};

该结构体用于管理所有的object。

成员 解释
avail 可用的object的数量
limit
batchcount
touched
entry 指向object的指针,每一个成员指向一个object

你可能感兴趣的:(slab内存管理方案学习记录)