从零到负一

09. 【slab】1 - slab简介以及相关结构体

2023/02/18

最近开始学习slab,感觉内容比较杂乱,花了不少时间稍微明白了一些,这里通过几篇笔记总结下学习的slab知识。

为什么需要slab

关于这点,书上和网上都说了很多,我这里简单地说3点:

  1. 可以分配比PAGE_SIZE更小的内存空间,大大地降低了内部碎片;
  2. 效率更高。第一,slab的分配和释放比伙伴系统更轻量级,速度更快,使用资源更少;第二,slab可以更快地分配和释放经常使用的对象(释放后并不还给伙伴系统,slab可以之后再使用);
  3. 对物理缓存更友好,效率更高。slab分配内存空间时会尽量减少物理缓存的冲突。

相关数据结构

slab是多个结构体共同组成的系统,因此在理解上会有些困难,下图展示了一个简化版的slab系统。我们可以看到,slab系统至少需要4个结构体 - 高速缓存,slab,对象和page(外加一个array_cache)。page在这里就不介绍了,下面主要介绍其它几种结构体。

一定要注意,这里所指的高速缓存,是软件意义上的,和硬件缓存没有任何关系!同时,对象是没有结构体的,对象就是大小一定的连续内存块。

图一:简化版的slab结构体间的关系

struct kmem_cache_s

kmem_cache_s中,有两个结构体需要说明,分别是array_cachekmem_list3array_cache是CPU本地高速缓存的描述符,kmem_list3则包含了该高速缓存连接slab的三种链表。

struct array_cache

1
2
3
4
5
6
struct array_cache {
unsigned int avail; // 指向本地高速缓存中可使用对象的指针的个数,也是第一个空槽的下标
unsigned int limit; // 指向本地高速缓存中可使用对象的指针的最大个数
unsigned int batchcount; // 本地高速缓存填充/腾空时使用的对象块的数量
unsigned int touched;
};

struct kmem_list3

1
2
3
4
5
6
7
8
9
struct kmem_list3 {
struct list_head slabs_partial; // 包含空闲和非空闲slab
struct list_head slabs_full; // 不包含空闲slab
struct list_head slabs_free; // 只包含空闲slab
unsigned long free_objects; // 链表中所有空闲对象的个数
int free_touched;
unsigned long next_reap;
struct array_cache *shared; // 指向所有CPU共享的一个本地高速缓存的指针
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
struct kmem_cache_s {
/* 1) per-cpu data, touched during every alloc/free */
struct array_cache *array[NR_CPUS];
unsigned int batchcount;
unsigned int limit; // 本地高速缓存中空闲对象的上限,这个和array_cache中的limit相同

/* 2) touched by every alloc & free from the backend */
struct kmem_list3 lists;
/* NUMA: kmem_3list_t *nodelists[MAX_NUMNODES] */
// kmem_cache中的每个对象大小都是一样的,每个slab的大小也是一样的
unsigned int objsize;
unsigned int flags; /* constant flags */
unsigned int num; /* # of objs per slab */
unsigned int free_limit; // 高速缓存中所有空闲对象的上限
spinlock_t spinlock;

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

/* force GFP flags, e.g. GFP_DMA */
unsigned int gfpflags;

size_t colour;
unsigned int colour_off; // slab中最小的对齐偏移单位
unsigned int colour_next; // 下一个被分配的slab的颜色
kmem_cache_t *slabp_cache;
unsigned int slab_size; // 单个slab的大小,这里只包括slab和对象的描述符
unsigned int dflags; /* 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_head next;

/* 5) statistics */
#if STATS
unsigned long num_active;
unsigned long num_allocations;
unsigned long high_mark;
unsigned long grown;
unsigned long reaped;
unsigned long errors;
unsigned long max_freeable;
unsigned long node_allocs;
atomic_t allochit;
atomic_t allocmiss;
atomic_t freehit;
atomic_t freemiss;
#endif
#if DEBUG
int dbghead;
int reallen;
#endif
};

struct slab

该结构体是slab描述符,它通过kmem_list3kmem_cache_s联系起来。在后面的笔记中会介绍slab描述符和对象描述符如何一起工作的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* struct slab
*
* Manages the objs in a slab. Placed either at the beginning of mem allocated
* for a slab, or allocated from an general cache.
* slabs are chained into three list: fully used, partial, fully free slabs.
*/
struct slab {
struct list_head list;
unsigned long colouroff; // slab中的偏移,该偏移包含slab和对象的描述符(如果它们在slab内部)
void *s_mem; // 第一个对象的地址
unsigned int inuse; // 当前正在使用的非空闲slab中的对象个数
kmem_bufctl_t free; // 下一个空闲对象的下标,它的类型是unsigned short
};

slab的基本组成

slab各种操作的具体分析我会在之后的笔记中详细记录(和进程、内存回收相关的暂不记录在slab系列),这里只是在大的方向上介绍下slab是如何组成的。

初始化阶段和之后的使用是有一些区别的(初始化也需要分配内存空间,并且存在“鸡生蛋,蛋生鸡”的问题),要搞懂初始化,通过阅读源码,我总结出3点:

  1. 我们需要明确一点,在伙伴系统完成后,系统可以正常地按页进行分配内存空间。因此,只要是需要分配页的地方,即使slab没有初始化完成也是可以用的;
  2. 对于结构体的内存空间申请(就像我们使用malloc()给结构体申请内存空间),在slab初始化完成前,内核是不能直接完成这项任务的。内核是通过使用全局变量(静态)的方法解决slab初始化完成前需要给结构体申请内存空间的难题;
  3. slab分为两种,一种是专用高速缓存,一种是普通高速缓存。第一种是针对某一特定作用设置的高速缓存,第二种是普通的,任何结构体、任何设备都可以通过kmalloc()来使用的高速缓存。初始化时就先建立了普通高速缓存池,我们可以使用kmalloc()来给结构体等分配动态的内存空间。

有了这些基础,我们就可以看看下图了:

图二:高速缓存、slab和对象

内核通过cache_chainkmem_cache链接起来,第一个kmem_cachecache_cache,这是静态分配的缓存(这里只分配了描述符,并没有分配对应的slab、对象以及CPU本地缓存描述符等)。之后的高速缓存也都是在此基础上连入cache_chain的。每个kmem_cache又有CPU本地slab对象的地址以及三条slab的链表。注意,上图和2.6.11的内核有些区别,比如2.6.11中没有nodelists,但其它部分都比较类似。

[补充内容] cache_cache的分析

我这里对cache_cache这个高速缓存进行简单的分析,这对后面理解slab的初始化很有帮助。下面是和cache_cache相关的初始化代码,我们来看看cache_cache这些结构体变量有什么作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//===============================================================================
// 这部分是cache_cache本地高速缓存的静态分配情况
//===============================================================================

/* bootstrap: The caches do not work without cpuarrays anymore,
* but the cpuarrays are allocated from the generic caches...
*/
#define BOOT_CPUCACHE_ENTRIES 1
struct arraycache_init {
struct array_cache cache;
void * entries[BOOT_CPUCACHE_ENTRIES];
};

static struct arraycache_init initarray_cache __initdata =
{ { 0, BOOT_CPUCACHE_ENTRIES, 1, 0} };
static struct arraycache_init initarray_generic =
{ { 0, BOOT_CPUCACHE_ENTRIES, 1, 0} };

//===============================================================================
// 这部分是cache_cache初始化的值
// 它的本地高速缓存和shared的缓存在kmem_cache_init()的最后阶段会重新进行设置
//===============================================================================
struct array_cache {
unsigned int avail = 0; // 指向本地高速缓存中可使用对象的指针的个数,也是第一个空槽的下标
unsigned int limit = BOOT_CPUCACHE_ENTRIES; // 指向本地高速缓存中可使用对象的指针的最大个数
unsigned int batchcount = 1; // 本地高速缓存填充/腾空时使用的对象块的数量
unsigned int touched = 0;
};

/* internal cache of cache description objs */
static kmem_cache_t cache_cache = {
.lists = LIST3_INIT(cache_cache.lists), // free/partial/full初始化了,其它都为0(shared == NULL)
.batchcount = 1, // 每次转移1个对象
.limit = BOOT_CPUCACHE_ENTRIES, // 它的本地高速缓存最多只有一个对象,这个在初始化最后会进行调整
.objsize = sizeof(kmem_cache_t), // cache_cache就是专门用于分配kmem_cache_t的专用高速缓存
.flags = SLAB_NO_REAP, // 它的slab描述符都在slab内部
.spinlock = SPIN_LOCK_UNLOCKED,
.name = "kmem_cache", // 它的名字就是kmem_cache
#if DEBUG
.reallen = sizeof(kmem_cache_t),
#endif
};

// 其它还有些重要的,比如 gfporder = 0, objsize, colour, slab_size等需要下面的一些计算
// 有了这些我们就能大概知道cache_cache中本地高速缓存和slab的基本形态了
cache_cache.colour_off = cache_line_size();
cache_cache.array[smp_processor_id()] = &initarray_cache.cache;

cache_cache.objsize = ALIGN(cache_cache.objsize, cache_line_size());

cache_estimate(0, cache_cache.objsize, cache_line_size(), 0,
&left_over, &cache_cache.num);
if (!cache_cache.num)
BUG();

cache_cache.colour = left_over/cache_cache.colour_off; // ULK, p335
cache_cache.colour_next = 0;
cache_cache.slab_size = ALIGN(cache_cache.num*sizeof(kmem_bufctl_t) +
sizeof(struct slab), cache_line_size());


CATALOG
  1. 1. 为什么需要slab
  2. 2. 相关数据结构
    1. 2.1. struct kmem_cache_s
      1. 2.1.1. struct array_cache
      2. 2.1.2. struct kmem_list3
    2. 2.2. struct slab
  3. 3. slab的基本组成
    1. 3.1. [补充内容] cache_cache的分析