Linux内存管理:(二)slab分配器

文章说明:

  • Linux内核版本:5.0

  • 架构:ARM64

  • 参考资料:《奔跑吧Linux内核》

  • Linux 5.0内核源码注释及学习笔记仓库地址:

    zhangzihengya/LinuxSourceCode_v5.0_study (github.com)

1. slab分配器产生的背景

伙伴系统在分配内存时是以物理页面为单位的,在实际中有很多内存需求是以字节为单位的,那么如果我们需要分配以字节为单位的小内存块,该如何分配呢?slab分配器就是用来解决小内存块分配问题的,也是内存分配中非常重要的角色之一。

slab分配器最终还使用伙伴系统来分配实际的物理页面,只不过slab分配器在这些连续的物理页面上实现了自己的机制,以此来对小内存块进行管理。

slab机制如下图所示:

Linux内存管理:(二)slab分配器_第1张图片

其中每个slab描述符都会建立共享对象缓冲池和本地对象缓冲池。slab机制有如下特性:

  • 把分配的内存块当作对象(object)来看待。对象可以自定义构造函数(constructor) 和析构函数(destructor)来初始化对象的内容并释放对象的内容。
  • slab对象被释放之后不会马上丢弃而是继续保留在内存中,可能稍后会被用到,这样不需要重新向伙伴系统申请内存。
  • slab机制可以根据特定大小的内存块来创建slab描述符,如内存中常见的数据结构、打开文件对象等,这样可以有效地避免内存碎片的产生,也可以快速获得频繁访问的数据结构。另外,slab机制也支持按2的n次方字节大小分配内存块。
  • slab机制创建了多层的缓冲池,充分利用了空间换时间的思想,未雨绸谬,有效地解决了效率问题。
    • 每个CPU有本地对象缓冲池,避免了多核之间的锁争用问题。
    • 每个内存节点有共享对象缓冲池。

2. 宏观了解

为了更好地理解slab分配器的细节,我们先从宏观上大致了解下slab系统的架构,如下图所示:

Linux内存管理:(二)slab分配器_第2张图片

slab系统由slab描述符、slab节点、本地对象缓冲池、共享对象缓冲池、3个slab链表、n个slab分配器,以及众多slab缓存对象组成,相关数据结构的注解如下。

slab描述符:

// kmem_cache数据结构是 slab 分配器中的核心成员,每个 slab 描述符都用一个 kmem_cache 数据结构来抽象描述
struct kmem_cache {
	// Per-cpu 变量的 array_cache 数据结构,每个CPU一个,表示本地 CPU 的对象缓冲池
	struct array_cache __percpu *cpu_cache;

/* 1) Cache tunables. Protected by slab_mutex */
	// 表示在当前 CPU 的本地对象缓冲池 array_cache 为空时,从共享对象缓冲池或 slabs_partial/slabs_free 列表中获取的对象的数目
	unsigned int batchcount;
	// 当本地对象缓冲池中的空闲对象的数目大于 limit 时,会主动释放 batchcount 个对象,便于内核回收和销毁 slab
	unsigned int limit;
	// 用于多核系统
	unsigned int shared;

	// 对象的长度,这个长度要加上 align 对齐字节
	unsigned int size;
	struct reciprocal_value reciprocal_buffer_size;
/* 2) touched by every alloc & free from the backend */

	// 对象的分配掩码
	slab_flags_t flags;		/* constant flags */
	// 一个 slab 中最多有多少个对象
	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;

	// 一个 slab 中可以有多少个不同的缓存行
	size_t colour;			/* cache colouring range */
	// 着色区的长度,和 L1 缓存行大小相同
	unsigned int colour_off;	/* colour offset */
	struct kmem_cache *freelist_cache;
	// 每个对象要占用 1 字节来存放 freelist
	unsigned int freelist_size;

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

/* 4) cache creation/removal */
	// slab 描述符的名称
	const char *name;
	struct list_head list;
	int refcount;
	// 对象的实际大小
	int object_size;
	// 对齐的长度
	int align;

...

	// slab 节点
	// 在 NUMA 系统中,每个节点有一个 kmem_cache_node 数据结构
	// 在 ARM Vexpress 平台上,只有一个节点
	struct kmem_cache_node *node[MAX_NUMNODES];
};

slab节点:

struct kmem_cache_node {
	// 用于保护 slab 节点中的 slab 链表
	spinlock_t list_lock;

#ifdef CONFIG_SLAB
	// slab 链表,表示 slab 节点中有部分空闲对象
	struct list_head slabs_partial;	/* partial list first, better asm code */
	// slab 链表,表示 slab 节点中没有空闲对象
	struct list_head slabs_full;
	// slab 链表,表示 slab 节点中全部都是空闲对象
	struct list_head slabs_free;
	// 表示 slab 节点中有多少个 slab 对象
	unsigned long total_slabs;	/* length of all slab lists */
	// 表示 slab 节点中有多少个全是空闲对象的 slab 对象
	unsigned long free_slabs;	/* length of free slab list only */
	// 空闲对象的数目
	unsigned long free_objects;
	// 表示 slab 节点中所有空闲对象的最大阈值,即 slab 节点中可容许的空闲对象数目最大阈值
	unsigned int free_limit;
	// 记录当前着色区的编号。所有 slab 节点都按照着色编号来计算着色区的大小,达到最大值后又从 0 开始计算
	unsigned int colour_next;	/* Per-node cache coloring */
	// 共享对象缓冲区。在多核 CPU 中,除了本地 CPU 外,slab 节点中还有一个所有 CPU 都共享的对象缓冲区
	struct array_cache *shared;	/* shared per node */
	// 用于 NUMA 系统
	struct alien_cache **alien;	/* on other nodes */
	// 下一次收割 slab 节点的时间
	unsigned long next_reap;	/* updated without locking */
	// 表示访问了 slabs_free 的 slab 节点
	int free_touched;		/* updated without locking */
#endif

...

};

对象缓冲池:

// slab 描述符会给每个 CPU 提供一个对象缓冲池(array_cache)
// array_cache 可以描述本地对象缓冲池,也可以描述共享对象缓冲池
struct array_cache {
	// 对象缓冲池中可用对象的数目
	unsigned int avail;
	// 对象缓冲池中可用对象数目的最大阈值
	unsigned int limit;
	// 迁移对象的数目,如从共享对象缓冲池或者其他 slab 中迁移空闲对象到该对象缓冲池的数量
	unsigned int batchcount;
	// 从缓冲池中移除一个对象时,将 touched 置为 1 ;
	// 当收缩缓冲池时,将 touched 置为 0;
	unsigned int touched;
	// 保存对象的实体
	// 指向存储对象的变长数组,每一个成员存放一个对象的指针。这个数组最初最多有 limit 个成员
	void *entry[];
};

对象缓冲池的数据结构中采用了GCC编译器的零长数组,entry[]数组用于存放多个对象,如下图所示:

Linux内存管理:(二)slab分配器_第3张图片

通过对slab系统架构图有了宏观的认识之后,我们下一步进行细节上的学习。

3. 细节描述

3.1 slab分配器的内存布局

slab分配器的内存布局通常由如下3部分组成:

  • 着色区
  • n个slab对象
  • 管理区。管理区可以看作一个freelist数组,数组的每个成员占用1字节,每个成员代表一个slab对象

Linux 5.0内核支持如下3种slab分配器布局模式:

  • OBJFREELIST_SLAB模式。这是Linux 4.6内核新增的—个优化,其目的是高效利用slab分配器中的内存。使用slab分配器中最后一个slab对象的空间作为管理区,如下图所示

    Linux内存管理:(二)slab分配器_第4张图片

  • OFF_SLAB模式。slab分配器的管理数据不在sIab分配器中,额外分配的内存用于管理,如下图所示

    Linux内存管理:(二)slab分配器_第5张图片

  • 正常模式。传统的布局模式,如下图所示

    Linux内存管理:(二)slab分配器_第6张图片

3.2 创建slab描述符

在内核中,创建slab描述符使用kmem_cache_create()函数,同样先上图为敬,kmem_cache_create()函数的流程如下图所示:

Linux内存管理:(二)slab分配器_第7张图片

为了使读者有更真切的理解,下文将根据流程图围绕源代码进行讲解这个过程:

kmem_cache_create

// 创建 slab 描述符
// kmem_cache_create() 函数用于创建自己的缓存描述符;kmalloc() 函数用于创建通用的缓存
// name:slab 描述符的名称
// size:缓冲对象的大小
// align:缓冲对象需要对齐的字节数
// flags:分配掩码
// ctor:对象的构造函数
struct kmem_cache *
kmem_cache_create(const char *name, unsigned int size, unsigned int align,
		slab_flags_t flags, void (*ctor)(void *))

kmem_cache_create->...->__kmem_cache_create

// 创建 slab 缓存描述符
int __kmem_cache_create(struct kmem_cache *cachep, slab_flags_t flags)
{
    ...
	// 让 slab 描述符的大小和系统的 word 长度对齐(BYTES_PER_WORD)
	// 当创建的 slab 描述符的 size 小于 word 长度时,slab 分配器会最终按 word 长度来创建
	size = ALIGN(size, BYTES_PER_WORD);

	// SLAB_RED_ZONE 检查是否溢出,实现调试功能
	if (flags & SLAB_RED_ZONE) {
		ralign = REDZONE_ALIGN;
		size = ALIGN(size, REDZONE_ALIGN);
	}

	/* 3) caller mandated alignment */
	// 调用方强制对齐
	if (ralign < cachep->align) {
		ralign = cachep->align;
	}
	...
	 * 4) Store it.
	 */
	cachep->align = ralign;
	// colour_off 表示一个着色区的长度,它和 L1 高速缓存行大小相同
	cachep->colour_off = cache_line_size();
	/* Offset must be a multiple of the alignment. */
	if (cachep->colour_off < cachep->align)
		cachep->colour_off = cachep->align;

	// 枚举类型 slab_state 用来表示 slab 系统中的状态,如 DOWN、PARTIAL、PARTIAL_NODE、UP 和 FULL 等。当 slab 机制完全初始化完成后状态变成 FULL
	// slab_is_available() 表示当 slab 分配器处于 UP 或者 FULL 状态时,分配掩码可以使用 GFP_KERNEL;否则,只能使用 GFP_NOWAIT
	if (slab_is_available())
		gfp = GFP_KERNEL;
	else
		gfp = GFP_NOWAIT;

...

	// slab 对象的大小按照 cachep->align 大小来对齐
	size = ALIGN(size, cachep->align);
	
    ...

	// 若数组 freelist 小于一个 slab 对象的大小并且没有指定构造函数,那么 slab 分配器就可以采用 OBJFREELIST_SLAB 模式
	if (set_objfreelist_slab_cache(cachep, size, flags)) {
		flags |= CFLGS_OBJFREELIST_SLAB;
		goto done;
	}

	// 若一个 slab 分配器的剩余空间小于 freelist 数组的大小,那么使用 OFF_SLAB 模式
	if (set_off_slab_cache(cachep, size, flags)) {
		flags |= CFLGS_OFF_SLAB;
		goto done;
	}

	// 若一个 slab 分配器的剩余空间大于 slab 管理数组大小,那么使用正常模式
	if (set_on_slab_cache(cachep, size, flags))
		goto done;

	return -E2BIG;

done:
	// freelist_size 表示一个 slab 分配器中管理区————freelist 大小
	cachep->freelist_size = cachep->num * sizeof(freelist_idx_t);
	cachep->flags = flags;
	cachep->allocflags = __GFP_COMP;
	if (flags & SLAB_CACHE_DMA)
		cachep->allocflags |= GFP_DMA;
	if (flags & SLAB_RECLAIM_ACCOUNT)
		cachep->allocflags |= __GFP_RECLAIMABLE;
	// size 表示一个 slab 对象的大小
	cachep->size = size;
	cachep->reciprocal_buffer_size = reciprocal_value(size);

...

	// 继续配置 slab 描述符
	err = setup_cpu_cache(cachep, gfp);
	if (err) {
		__kmem_cache_release(cachep);
		return err;
	}

	return 0;
}

3.3 分配slab对象

kmem_cache_alloc()是分配slab缓存对象的核心函数,它内部调用slab_alloc()函数。在slab 对象分配过程中是全程关闭本地中断的。kmem_cache_alloc() 函数的流程图如下所示:

Linux内存管理:(二)slab分配器_第8张图片

为了使读者有更真切的理解,下文将根据流程图围绕源代码进行讲解这个过程:

kmem_cache_alloc->slab_alloc

// slab_alloc() 函数在 slab 对象分配过程中是全程关闭本地中断的
static __always_inline void *
slab_alloc(struct kmem_cache *cachep, gfp_t flags, unsigned long caller)
{
	...
    local_irq_save(save_flags);
	// 获取 slab 对象
	objp = __do_cache_alloc(cachep, flags);
	local_irq_restore(save_flags);
	...

	// 如果分配时设置了 __GFP_ZERO 标志位,那么使用 memset() 把 slab 对象的内容清零
	if (unlikely(flags & __GFP_ZERO) && objp)
		memset(objp, 0, cachep->object_size);

	slab_post_alloc_hook(cachep, flags, 1, &objp);
	return objp;
}

kmem_cache_alloc->slab_alloc->...->____cache_alloc

// 获取 slab 对象
static inline void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
	void *objp;
	struct array_cache *ac;

	check_irq_off();

	// 获取 slab 描述符 cachep 中的本地对象缓冲池 ac
	ac = cpu_cache_get(cachep);
	// 判断本地对象缓冲池中有没有空闲的对象
	if (likely(ac->avail)) {
		ac->touched = 1;
		// 获取 slab 对象
		objp = ac->entry[--ac->avail];

		STATS_INC_ALLOCHIT(cachep);
		goto out;
	}

	STATS_INC_ALLOCMISS(cachep);
	// 第一次分配缓存对象时 ac->avail 值为 0,所以它应该在 cache_alloc_refill() 函数中
	objp = cache_alloc_refill(cachep, flags);
	...
    
	return objp;
}

kmem_cache_alloc->slab_alloc->__do_cache_alloc->____cache_alloc->cache_alloc_refill

static void *cache_alloc_refill(struct kmem_cache *cachep, gfp_t flags)
{
	...
	
	// 获取本地对象缓冲池 ac
	ac = cpu_cache_get(cachep);
	...
	// 获取 slab 节点
	n = get_node(cachep, node);

	BUG_ON(ac->avail > 0 || !n);
	// shared 表示共享对象缓冲池
	shared = READ_ONCE(n->shared);
	// 若 slab 节点没有空闲对象并且共享对象缓冲池 shared 为空或者共享对象缓冲池里也没有空闲对象,那么直接跳转到 direct_grow 标签处
	if (!n->free_objects && (!shared || !shared->avail))
		goto direct_grow;

	...
	
	// 若共享对象缓冲池里有空闲对象,那么尝试迁移 batchcount 个空闲对象到本地对象缓冲池 ac 中
	// transfer_objects() 函数用于从共享对象缓冲池迁移空闲对象到本地对象缓冲池
	if (shared && transfer_objects(ac, shared, batchcount)) {
		shared->touched = 1;
		goto alloc_done;
	}

	while (batchcount > 0) {
		/* Get slab alloc is to come from. */
		// 如果共享对象缓冲池中没有空闲对象,那么 get_first_slab() 函数会查看 slab 节点中的 slabs_partial 链表和 slabs_free 链表
		page = get_first_slab(n, false);
		if (!page)
			goto must_grow;

		check_spinlock_acquired(cachep);

		// 从 slab 分配器中迁移 batchcount 个空闲对象到本地对象缓冲池中
		batchcount = alloc_block(cachep, ac, page, batchcount);
		fixup_slab_list(cachep, n, page, &list);
	}

must_grow:
	// 更新 slab 节点中的 free_objects 计数值
	n->free_objects -= ac->avail;
alloc_done:
	spin_unlock(&n->list_lock);
	fixup_objfreelist_debug(cachep, &list);

// 表示 slab 节点没有空闲对象并且共享对象缓冲池中也没有空闲对象,这说明整个内存节点里没有 slab 空闲对象
// 这种情况下只能重新分配 slab 分配器,这就是一开始初始化和配置 slab 描述符的情景
direct_grow:
	if (unlikely(!ac->avail)) {
		/* Check if we can use obj in pfmemalloc slab */
		if (sk_memalloc_socks()) {
			void *obj = cache_alloc_pfmemalloc(cachep, n, flags);

			if (obj)
				return obj;
		}

		// 分配一个 slab 分配器
		page = cache_grow_begin(cachep, gfp_exact_node(flags), node);

		/*
		 * cache_grow_begin() can reenable interrupts,
		 * then ac could change.
		 */
		ac = cpu_cache_get(cachep);
		if (!ac->avail && page)
			// 从刚分配的 slab 分配器的空闲对象中迁移 batchcount 个空闲对象到本地对象缓冲池中
			alloc_block(cachep, ac, page, batchcount);
		// 把刚分配的 slab 分配器添加到合适的队列中,这个场景下应该添加到 slabs_partial 链表中
		cache_grow_end(cachep, page);

		if (!ac->avail)
			return NULL;
	}
	// 设置本地对象缓冲池的 touched 为 1,表示刚刚使用过本地对象缓冲池
	ac->touched = 1;

	// 返回一个空闲对象
	return ac->entry[--ac->avail];
}

3.4 释放slab缓存对象

释放slab缓存对象的接口函数是kmem_cache_free(),该流程如下所示:

Linux内存管理:(二)slab分配器_第9张图片

为了使读者有更真切的理解,下文将根据流程图围绕源代码进行讲解这个过程:

kmem_cache_free->__cache_free->___cache_free

void ___cache_free(struct kmem_cache *cachep, void *objp,
		unsigned long caller)
{
	struct array_cache *ac = cpu_cache_get(cachep);
	...
	// 当本地对象缓冲池的空闲对象数量 ac->avail 大于或等于 ac->limit 阈值时,就会调用 cache_flusharray() 做刷新动作,尝试回收空闲对象
	if (ac->avail < ac->limit) {
		STATS_INC_FREEHIT(cachep);
	} else {
		STATS_INC_FREEMISS(cachep);
		// 主要用于回收 slab 分配器
		cache_flusharray(cachep, ac);
	}
	...
	// 把对象释放到本地对象缓冲池 ac 中
	ac->entry[ac->avail++] = objp;
}

你可能感兴趣的:(Linux内存管理篇,linux)