Linux内核-内存-slab分配器

时间:2022-08-24 23:39:44
         前面分析到Linux使用分区页框分配器满足对连续页框的请求,这适合对大块内存的请求,当处理对小内存的请求时,比如几十或几百个字节,很显然,如果此时为其分配一整个页框将会是一种浪费,所以需要引入一种新的数据结构来描述在同一页框中如何分配小内存区,Linux使用slab分配器来解决这个问题。
        slab分配器把内存区看做对象,这些对象由一组数据结构和几个叫做构造或析构的函数组成。根据请求的频率不同,可以创建一组具有适当大小的专用对象来高效处理频繁请求的内存区,例如进程描述符,打开的文件对象等;对于很少遇到的内存区大小,可以基于一系列几何分布大小(如2的幂次方)的对象的分配模式来处理。

         slab分配器把对象分组放进高速缓存,每个高速缓存都是同种类型对象的一种“储备”,例如,当一个文件被打开时,存放相应“打开文件”对象所需的内存区是从一个叫做filp的slab分配器的高速缓存中得到的。高速缓存包含有多个slab,每个slab由一个或多个连续的页框组成,这些页框中包含已分配和空闲的对象,它们的关系如下图所示:

Linux内核-内存-slab分配器

如上图,各个高速缓存是通过双向链表连接,高速缓存中的每种slab也是通过双向链表连接,每个高速缓存都有一个空闲slab对象的本地高速缓存,请求内存区时就是通过这个本地高速缓存来分配对象,本地高速缓存从slab中的对象获取,一旦分配给本地高速缓存,对于slab来说,这个对象就是非空闲的(有可能还未被使用),每个slab中的对象关系如下图:

Linux内核-内存-slab分配器

紧接着slab描述符之后是一个对象描述符的数组,数组中的第一个对象描述符描述slab中的第一个对象,依次类推。对象描述符只不过是一个无符号整数,只有在对象空闲时才有意义,它包含的是下一个空闲对象在slab中的下标,因此实现了slab内部空闲对象的一个简单链表,空闲对象链表的最后一个元素的对象描述符用BUFCTL_END(0xffff)标记。

1. 数据结构

1.1 高速缓存描述符kmem_cache_t:

struct kmem_cache_s {
/* 1) per-cpu data, touched during every alloc/free */
/**
* 每CPU指针数组,指向包含空闲对象的本地高速缓存。
*/
struct array_cache*array[NR_CPUS];
/**
* 要转移进本地高速缓存或从本地高速缓存中转移出的大批对象的数量。
*/
unsigned intbatchcount;
/**
* 本地高速缓存中空闲对象的最大数目。这个参数可调。
*/
unsigned intlimit;
/* 2) touched by every alloc & free from the backend */
/**
* 包含三个链表,为什么要单独放到一个描述符中呢?
*/
struct kmem_list3lists;
/* NUMA: kmem_3list_t*nodelists[MAX_NUMNODES] */
/**
* 高速缓存中包含的对象的大小。
*/
unsigned intobjsize;
/**
* 描述高速缓存永久属性的一组标志。
*/
unsigned int flags;/* constant flags */
/**
* 在一个单独slab中的对象的个数。高速缓存中的所有slab具有相同的大小。
*/
unsigned intnum;/* # of objs per slab */
/**
* 整个slab高速缓存中空闲对象的上限。
*/
unsigned intfree_limit; /* upper limit of objects in the lists */
/**
* 高速缓存自旋锁。
*/
spinlock_tspinlock;

/* 3) cache_grow/shrink */
/* order of pgs per slab (2^n) */
/**
* 一个单独slab中包含的连续页框数目的对数。
*/
unsigned intgfporder;

/* force GFP flags, e.g. GFP_DMA */
/**
* 分配页框时传递给伙伴系统函数的一组标志。
*/
unsigned intgfpflags;

/**
* slab使用的颜色个数。用于slab着色。
*/
size_tcolour;/* cache colouring range */
/**
* slab中的基本对齐偏移。
*/
unsigned intcolour_off;/* colour offset */
/**
* 下一个被分配的slab使用的颜色。就是对齐因子。
*/
unsigned intcolour_next;/* cache colouring */
/**
* 指向包含slab描述符的普通slab高速缓存。如果使用了内部slab描述符,则这个字段为NULL。
*/
kmem_cache_t*slabp_cache;
/**
* 单个slab的大小。
*/
unsigned intslab_size;
/**
* 高速缓存动态属性标志。
*/
unsigned intdflags;/* dynamic flags */

/* constructor func */
/**
* 高速缓存相关的构造方法的指针。
*/
void (*ctor)(void *, kmem_cache_t *, unsigned long);

/* de-constructor func */
/**
* 高速缓存相关的析构方法的指针。
*/
void (*dtor)(void *, kmem_cache_t *, unsigned long);

/* 4) cache creation/removal */
/**
* 高速缓存名称。
*/
const char*name;
/**
* 高速缓存链表。
*/
struct list_headnext;

/* 5) statistics */
#if STATS
/**
* 统计信息
*/
unsigned longnum_active;
unsigned longnum_allocations;
unsigned longhigh_mark;
unsigned longgrown;
unsigned longreaped;
unsigned long errors;
unsigned longmax_freeable;
unsigned longnode_allocs;
atomic_tallochit;
atomic_tallocmiss;
atomic_tfreehit;
atomic_tfreemiss;
#endif
#if DEBUG
/**
* 调试信息
*/
intdbghead;
intreallen;
#endif
};

其中kmem_list3结构如下:

struct kmem_list3 {
/**
* 空闲和非空闲对象的slab描述符双向循环链表。
*/
struct list_headslabs_partial;/* partial list first, better asm code */
/**
* 不包含空闲对象的slab描述符双向循环链表。
*/
struct list_headslabs_full;
/**
* 只包含空闲对象的slab描述符双向循环链表。
*/
struct list_headslabs_free;
unsigned longfree_objects;
/**
* slab分配器的页回收算法使用。
*/
intfree_touched;
/**
* slab分配器的页回收算法使用。
*/
unsigned longnext_reap;
/**
* 所有CPU共享的一个本地高速缓存的指针。它使得将空闲对象从一个本地高速缓存移动到另外一个高速缓存的任务更容易。
* 它的初始大小是batchcount字段的8倍。
*/
struct array_cache*shared;
};

1.2 slab描述符:

struct slab {
/**
* slab高速缓存描述符的三个双向循环链表中的一个。
*/
struct list_headlist;
/**
* slab中第一个对象的偏移。
* 同一个高速缓存的不同slab有不同的coloroff值。这样可以避免硬件缓存行的不利影响。
*/
unsigned longcolouroff;
/**
* slab中第一个对象的地址。
*/
void*s_mem;/* including colour offset */
/**
* 当前正在使用的slab中的对象个数。
*/
unsigned intinuse;/* num of objs active in slab */
/**
* slab中下一个空闲对象的下标。如果没有剩下空闲对象则为BUFCT_END
*/
kmem_bufctl_tfree;
};

1.3 对象描述符:

/**
* 高速缓存对象描述符。将slab中的空闲对象链接起来。
*/
typedef unsigned short kmem_bufctl_t;

2. 源码分析

2.1 创建新的slab

/*
* slab分配器调用此接口从页框分配器中获得一组连续的空闲页框。
* cachep-需要额外页框的高速缓存的高速缓存描述符。请求页框的个数由cachep->gfporder中的order决定。
* flags-说明如何请求页框。这组标志与存放在高速缓存描述符的gfpflags字段中专用高速缓存分配标志相结合。
*/
static void *kmem_getpages(kmem_cache_t *cachep, int flags, int nodeid)
{
struct page *page;
void *addr;
int i;

flags |= cachep->gfpflags;
/*
* 从分区页框分配器得到页框
*/
if (likely(nodeid == -1)) {
page = alloc_pages(flags, cachep->gfporder);
} else {
page = alloc_pages_node(nodeid, flags, cachep->gfporder);
}
if (!page)
return NULL;
addr = page_address(page);

i = (1 << cachep->gfporder);
if (cachep->flags & SLAB_RECLAIM_ACCOUNT)
atomic_add(i, &slab_reclaim_pages);
add_page_state(nr_slab, i);
/*
* 设置页框字段表明其为slab所用
while (i--) {
SetPageSlab(page);
page++;
}
return addr;
}

2.2 释放slab

/**
* 释放分配给slab的页框。
* addr-从该地址开始释放页框。
* cachep-slab是由cachep标识的高速缓存中的slab.
*/
static void kmem_freepages(kmem_cache_t *cachep, void *addr)
{
unsigned long i = (1<<cachep->gfporder);
struct page *page = virt_to_page(addr);
const unsigned long nr_freed = i;

while (i--) {
if (!TestClearPageSlab(page))
BUG();
page++;
}
sub_page_state(nr_slab, nr_freed);
/**
* 如果当前进程正在执行回存回收。就适当增加reclaimed_slab字段。
* 于是刚被释放的页就能通过回收算法被记录下来。
*/
if (current->reclaim_state)
current->reclaim_state->reclaimed_slab += nr_freed;
free_pages((unsigned long)addr, cachep->gfporder);
/**
* 如果SLAB_RECLAIM_ACCOUNT被置位,slab_reclaim_pages则被适当的减少。
*/
if (cachep->flags & SLAB_RECLAIM_ACCOUNT)
atomic_sub(1<<cachep->gfporder, &slab_reclaim_pages);
}


2.3 分配slab对象

/**
* 获得新的slab对象
* cachep-指向高速缓存描述符。新空闲对象必须从该高速缓存描述符获得。
* flag-表示传递给分区页框分配器函数的标志。
*/
void * kmem_cache_alloc (kmem_cache_t *cachep, int flags)
{
return __cache_alloc(cachep, flags);
}

static inline void * __cache_alloc (kmem_cache_t *cachep, int flags)
{
unsigned long save_flags;
void* objp;
struct array_cache *ac;

cache_alloc_debugcheck_before(cachep, flags);

local_irq_save(save_flags);
/**
* 首先试图从本地高速缓存获得一个空闲对象。
*/
ac = ac_data(cachep);
/**
* 如果本地高速缓存有空闲对象,那么avail字段就包含最后被释放的对象的项在本地高速缓存中的下标。
*/
if (likely(ac->avail)) {
STATS_INC_ALLOCHIT(cachep);
ac->touched = 1;
/**
* 因为本地高速缓存数组正好存放在ac描述符的后面。
* 所以(void**)(ac+1)[--ac->avail]获得空闲对象的地址,并递减ac->avail的值。
*/
objp = ac_entry(ac)[--ac->avail];
} else {/* 本地高速缓存中没有空闲对象。 */
STATS_INC_ALLOCMISS(cachep);
/**
* cache_alloc_refill重新填充本地高速缓存并获得一个空闲对象。
*/
objp = cache_alloc_refill(cachep, flags);
}
local_irq_restore(save_flags);
objp = cache_alloc_debugcheck_after(cachep, flags, objp, __builtin_return_address(0));
return objp;
}

cache_alloc_refill函数如下:

/**
* 重新填充本地高速缓存并获得一个空闲对象
*/
static void* cache_alloc_refill(kmem_cache_t* cachep, int flags)
{
int batchcount;
struct kmem_list3 *l3;
struct array_cache *ac;

check_irq_off();
/**
* ac = cachep->array[smp_processor_id()];
* 将本地高速缓存描述符的地址存放在ac局部变量中。
*/
ac = ac_data(cachep);
retry:
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;
}
l3 = list3_data(cachep);

BUG_ON(ac->avail > 0);
/**
* 获取spinlock
*/
spin_lock(&cachep->spinlock);
/**
* 如果slab高速缓存包含共享本地高速缓存
*/
if (l3->shared) {
struct array_cache *shared_array = l3->shared;
/**
* 并且该共享本地高速缓存包含一些空闲对象
*/
if (shared_array->avail) {
if (batchcount > shared_array->avail)
batchcount = shared_array->avail;
/**
* 从共享本地高速缓存中上移batchcount个指针来重新填充CPU的本地高速缓存
*/
shared_array->avail -= batchcount;
ac->avail = batchcount;
memcpy(ac_entry(ac), &ac_entry(shared_array)[shared_array->avail],
sizeof(void*)*batchcount);
shared_array->touched = 1;
goto alloc_done;
}
}
/**
* 函数试图填充本地高速缓存,填充值为高速缓存的slab中包含的多达ac->batchcount个空闲对象的指针
*/
while (batchcount > 0) {
struct list_head *entry;
struct slab *slabp;
/* Get slab alloc is to come from. */
entry = l3->slabs_partial.next;
/**
* 查看高速缓存描述符的slabs_partial和slabs_free链表
*/
if (entry == &l3->slabs_partial) {
l3->free_touched = 1;
entry = l3->slabs_free.next;
if (entry == &l3->slabs_free)
goto must_grow;
}

/**
* 获得slab描述符的地址slabp,该slab描述符的相应slab或者部分被填充或者为空。
*/
slabp = list_entry(entry, struct slab, list);
check_slabp(cachep, slabp);
check_spinlock_acquired(cachep);
/**
* 对于slab中的每个空闲对象
*/
while (slabp->inuse < cachep->num && batchcount--) {
kmem_bufctl_t next;
STATS_INC_ALLOCED(cachep);
STATS_INC_ACTIVE(cachep);
STATS_SET_HIGH(cachep);

/* get obj pointer */
/**
* 将对象地址插入本地高速缓存。
*/
ac_entry(ac)[ac->avail++] = slabp->s_mem + slabp->free*cachep->objsize;

/**
* 函数增加slab描述符的inuse字段
*/
slabp->inuse++;
next = slab_bufctl(slabp)[slabp->free];
#if DEBUG
slab_bufctl(slabp)[slabp->free] = BUFCTL_FREE;
#endif
/**
* 更新free字段,使得它指向slab中下一个空闲对象的下标。
*/
slabp->free = next;
}
check_slabp(cachep, slabp);

/* move slabp to correct slabp list: */
/**
* 如果有必要,将清空的slab插入到适当的链表上,可以是slabs_full链表,也可以是slabs_partial链表
*/
list_del(&slabp->list);
if (slabp->free == BUFCTL_END)
list_add(&slabp->list, &l3->slabs_full);
else
list_add(&slabp->list, &l3->slabs_partial);
}

must_grow:
/**
* 被加到本地高速缓存的指针个数被存放在ac->avail字段。
* 函数递减同样数量的free_objects来说明这些对象不再空闲
*/
l3->free_objects -= ac->avail;
alloc_done:
/**
* 释放spinlock
*/
spin_unlock(&cachep->spinlock);

/**
* 没有发生任何高速缓存再填充的情况
*/
if (unlikely(!ac->avail)) {
int x;
/**
* 调用cache_grow获得一个新的slab,从而获得了新的空闲对象
*/
x = cache_grow(cachep, flags, -1);

// cache_grow can reenable interrupts, then ac could change.
ac = ac_data(cachep);
/**
* cache_grow失败了,返回NULL
*/
if (!x && ac->avail == 0)// no objects in sight? abort
return NULL;

/**
* cache_grow成功了,返回再试。
*/
if (!ac->avail)// objects refilled by interrupt?
goto retry;
}
/**
* 如果ac->avail>0(一些高速缓存再填充的情况发生了)
*/
ac->touched = 1;
/**
* 返回最后插入到本地高速缓存的空闲对象指针
*/
return ac_entry(ac)[--ac->avail];
}

3.4 释放slab对象

**
* 释放一个曾经由slab分配器分配给某个内核函数的对象
* cachep-高速缓存描述符的地址。
* objp-要释放的对象的地址。
*/
void kmem_cache_free (kmem_cache_t *cachep, void *objp)
{
unsigned long flags;

local_irq_save(flags);
__cache_free(cachep, objp);
local_irq_restore(flags);
}

static inline void __cache_free (kmem_cache_t *cachep, void* objp)
{
struct array_cache *ac = ac_data(cachep);

check_irq_off();
objp = cache_free_debugcheck(cachep, objp, __builtin_return_address(0));

/**
* 首先检查本地高速缓存是否有空间给指向一个空闲对象的额外指针。
*/
if (likely(ac->avail < ac->limit)) {
STATS_INC_FREEHIT(cachep);
/**
* 本地高速缓存有空闲指针,则该指针被加到本地高速缓存后返回。
*/
ac_entry(ac)[ac->avail++] = objp;
return;
} else {
STATS_INC_FREEMISS(cachep);
/**
* 调用cache_flusharray,清空本地高速缓存
*/
cache_flusharray(cachep, ac);
/**
* 然后将指针加到本地高速缓存。
*/
ac_entry(ac)[ac->avail++] = objp;
}
}

cache_flusharray如下:
static void cache_flusharray (kmem_cache_t* cachep, struct array_cache *ac)
{
int batchcount;

batchcount = ac->batchcount;
#if DEBUG
BUG_ON(!batchcount || batchcount > ac->avail);
#endif
check_irq_off();
/**
* 获得自旋锁
*/
spin_lock(&cachep->spinlock);
/**
* 如果包含一个共享本地高速缓存
*/
if (cachep->lists.shared) {
struct array_cache *shared_array = cachep->lists.shared;
int max = shared_array->limit-shared_array->avail;
/**
* 该共享缓存还没有满
*/
if (max) {
if (batchcount > max)
batchcount = max;
/**
* 将batchcount个指针放到共享高速缓存中。
*/
memcpy(&ac_entry(shared_array)[shared_array->avail],
&ac_entry(ac)[0],
sizeof(void*)*batchcount);
/**
* 上移batchcount个指针来重新填充共享本地高速缓存
*/
shared_array->avail += batchcount;
goto free_done;
}
}

/**
* 将当前包含在本地高速缓存中的ac->batchcount个对象归还给slab分配器。
*/
free_block(cachep, &ac_entry(ac)[0], batchcount);
free_done:
#if STATS
{
int i = 0;
struct list_head *p;

p = list3_data(cachep)->slabs_free.next;
while (p != &(list3_data(cachep)->slabs_free)) {
struct slab *slabp;

slabp = list_entry(p, struct slab, list);
BUG_ON(slabp->inuse);

i++;
p = p->next;
}
STATS_SET_FREEABLE(cachep, i);
}
#endif
/**
* 释放锁
*/
spin_unlock(&cachep->spinlock);
/**
* 通过减去被移到共享本地高速缓存或被释放到slab分配器的对象的个数来更新本地高速缓存描述的avail字段
*/
ac->avail -= batchcount;
/**
* 移动本地高速缓存数组起始处的那个本地高速缓存中的所有指针。
* 因为已经把第一个对象指针从本地高速缓存上删除,因此剩余的指针必须上移。
*/
memmove(&ac_entry(ac)[0], &ac_entry(ac)[batchcount],
sizeof(void*)*ac->avail);
}

上面为专用对象的分配和释放函数,对于通用对象的管理函数如下:
/**
* 获得普通高速缓存中的对象。
*/
static inline void *kmalloc(size_t size, int flags)
{
if (__builtin_constant_p(size)) {
int i = 0;
/**
* 找size对应的几何分布大小值。
*/
#define CACHE(x) \
if (size <= x) \
goto found; \
else \
i++;
#include "kmalloc_sizes.h"
/**
* 运行到此,说明要分配的对象太大,不能分配这么大的对象。
*/
#undef CACHE
{
extern void __you_cannot_kmalloc_that_much(void);
__you_cannot_kmalloc_that_much();
}
/**
* 请求分配的size对应的高速缓存描述符索引号为i
* 根据GFP_DMA,在不同的高速缓存描述符中分配对象。
*/
found:
return kmem_cache_alloc((flags & GFP_DMA) ?
malloc_sizes[i].cs_dmacachep :
malloc_sizes[i].cs_cachep, flags);
}
return __kmalloc(size, flags);
}

void * __kmalloc (size_t size, int flags)
{
struct cache_sizes *csizep = malloc_sizes;

for (; csizep->cs_size; csizep++) {
if (size > csizep->cs_size)
continue;
#if DEBUG
/* This happens if someone tries to call
* kmem_cache_create(), or kmalloc(), before
* the generic caches are initialized.
*/
BUG_ON(csizep->cs_cachep == NULL);
#endif
return __cache_alloc(flags & GFP_DMA ?
csizep->cs_dmacachep : csizep->cs_cachep, flags);
}
return NULL;
}

释放函数如下:
/**
* 释放由kmalloc接口分配的内存
*/
void kfree (const void *objp)
{
kmem_cache_t *c;
unsigned long flags;

if (!objp)
return;
local_irq_save(flags);
kfree_debugcheck(objp);
/**
* 通过((kmem_cache_t *)(pg)->lru.next),可确定合适的高速缓存描述符。
*/
c = GET_PAGE_CACHE(virt_to_page(objp));
/**
* __cache_free释放高速缓存中的对象。
*/
__cache_free(c, (void*)objp);
local_irq_restore(flags);
}