Linux内存管理:(四)物理页面分配之slab机制分配小内存 及 Linux6.5源码分析

《Linux6.5源码分析:内存管理系列文章》

本系列文章将对内存管理相关知识进行梳理与源码分析,重点放在linux源码分析上,并结合eBPF程序对内核中内存管理机制进行数据实时拿取与分析。

在进行正式介绍之前,有必要对文章引用进行提前说明。本系列文章参考了大量的博客、文章以及书籍:

Linux内存管理:(四)物理页面分配之slab机制分配小内存 及 Linux6.5源码分析

前面的文章中,我们介绍了Linux中分配物理页面的流程,梳理了如何从伙伴系统中申请物理页面,如何释放物理页面到伙伴系统中,具体内容见如下文章:
Linux内存管理:(一)物理页面分配流程 及 Linux6.5源码分析(上)-优快云博客
Linux内存管理:(二)物理页面分配流程 及 Linux6.5源码分析(中)-优快云博客
Linux内存管理:(三)物理页面分配流程 及 Linux6.5源码分析(下)-优快云博客
通过伙伴系统分配的物理内存是以物理页为单位的,最小的一个物理页面也有4KB大小,很多时候所需要的内存比较小,达不到1页,这种情况下便可以通过slab机制来满足内存需求。

slab机制可以为系统提供特定大小的内存块,也可以提供按2的整数次幂字节的内存块,以满足不同进程对小内存的需求;slab实际上是内核在伙伴系统的基础上构建的资源池,他批量申请内存,完成映射,用户需要使用内存时直接由他返回,不必经过伙伴系统;

借鉴《图解Linux内核》一书中的一句话:slab机制就像是豆腐零售商,从豆腐工厂(伙伴系统)批量进货(物理页面),并进行二次加工将豆腐切成固定大小的豆腐块供用户购买(将物理页面化分为不同大小的slab分配器),普通用户没必要直接到工厂买一整板豆腐,直接按需求从不同零售商手中购买一小块豆腐即可;

接下来将深入slab机制,对slab机制进行探索学习;

1. slab机制介绍

前面已经对slab机制有了简单的介绍,用一句话概括一下: slab机制就是用于满足系统中对小内存的需求,在伙伴系统上设计出的一套机制, 为系统提供连续的小块固定大小的物理内存。slab机制将内存划分为多个独立缓存(每个缓存专用于特定类型对象,如进程描述符),每个缓存(kmem_cache)由多个slab组成,而每个slab进一步分割为相同大小的对象单元。

前面提到的缓存/独立缓存指的是slab机制中描述不同类型大小的slab描述符,内核中用struct kmem_cache描述,下文中会详细介绍该结构体;每个缓存代表一种固定的对象缓存仓库(比如专门为task_struct ,dentry, inode等内核数据结构提供的内存slab缓存); 下图展示了系统中slab机制的分布 ,系统中所有类型的slab缓存均在slab缓存链表上,每个slab缓存由一个struct kmem_cache描述, 一个slab缓存会为每个cpu申请一个专属该slab缓存的本地对象缓存池(存放着若干个空闲对象)、为每个NUMA节点申请一个slab节点,该slab节点中有一个slab共享对象缓存池(存放着若干个空闲对象)、三个链表(slabs_free、slabs_partial、slabs_full),这三个链表上链接着若干个slab ,每个slab均是一段连续的物理内存区域(由若干个对象obj组成),通过slab分配物理内存块是以对象为单位,这就意味着有的slab中所有的obj对象可能是空闲的,有的slab中所有obj对象可能均被使用,而有的slab中部分obj空闲部分被使用,这三种slab分别会被维护在三种slab链表中;具体布局见下图;
在这里插入图片描述

slab机制有如下特性:

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

1.1. slab系统框架描述

前文中对slab机制有了一定的介绍,我们也可以通过上图知道slab机制由slab缓存来描述不同类型大小的slab,为每个cpu及NUMA节点创建了对象缓存池以及slab链表。为了让读者对NUMA框架下 slab机制的分布情况以及slab和伙伴系统的关系有一个直观的认识,本文章将结合下图对slab机制进行逐层介绍:

在这里插入图片描述

  • 首先slab分配器是基于伙伴系统提供的物理内存块分配功能,他从伙伴系统上申请连续的物理页面,并最终将物理页面释放到伙伴系统中;
  • 其次每个类型的slab缓存,在NUMA的各个节点上均有一个slab节点,该slab节点包括三个链表及一个array_cache共享对象缓存池;在每个CPU上包含一个本地对象缓存池,其中包含若干个空闲对象;
  • slab节点中的三个slab链表上,维护着若干个slab区域(连续的物理页面),slab区域是有多干个空闲/非空闲对象obj组成的;

以上是一个slab缓存在单个NUMA节点中的分布情况,对于一个slab缓存在不同NUMA节点,以及在不同cpu上的布局情况可参考下图:

在这里插入图片描述

不同的slab缓存均在每个NUMA节点有着自己的一套slab节点及本地对象缓存池;

1.2. 结构体介绍

对slab分配器在NUMA系统中的布局有了一定的了解后,需要深入内核源码,对前文中涉及到的相关结构体进行了解,主要包括:slab描述符、slab节点、本地对象缓冲池、共享对象缓冲池、3个slab链表、n个slab分配器,以及众多slab缓存对象;

1.2.1. slab描述符(kmem_cache)

该结构体描述一个slab缓存,用于对每个slab缓存进行管理;

/*每个slab缓存都由 kmem_cache 数据结构表示*/
struct kmem_cache {
	/*1.每个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) */
	/*一个slab分配器从伙伴系统申请多少个物理页面2^gfporder个*/
	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 */
	/*每个对象要占用 1 字节来存放 freelist*/
	unsigned int freelist_size;

	/* constructor func */
	/*构造函数*/
	void (*ctor)(void *obj);

/* 4) cache creation/removal */
	/*slab 描述符的名称*/
	const char *name;

	/*在slab_caches 链表中,该slab缓存用于找到前一个和后一个slab缓存*/
	struct list_head list;

	/*当前slab缓存被引用的次数,
	 *当创建其他slab描述符并引用该slab描述符时,
	 *会增加引用次数
	 */
	int refcount;

	/*对象的实际大小*/
	int object_size;

	/*对齐的长度*/
	int align;

/* 5) statistics */
#ifdef CONFIG_DEBUG_SLAB
	......
#endif
	/*NUMA系统中,每个节点下对应一个slab节点*/
	struct kmem_cache_node *node[MAX_NUMNODES];
};

1.2.2. slab节点 (kmem_cache_node)

每个NUMA节点下都有一个slab节点, slab节点中维护三个slab链表和一个共享对象缓存池,当前NUMA节点下的所有cpu均可访问共享对象缓存池;

在这里插入图片描述

/*
 * NUMA当前节点下的slab节点
 */
struct kmem_cache_node {
#ifdef CONFIG_SLAB
	/*1. 用于保护 slab 节点中的 slab 链表*/
	raw_spinlock_t list_lock;

	/*2.  slab节点的三个链表*/
	/*2.1 slab 链表,表示 slab 节点中有部分空闲对象*/
	struct list_head slabs_partial;	/* partial list first, better asm code */
	/*2.2 slab 链表,表示 slab 节点中没有空闲对象*/
	struct list_head slabs_full;
	/*2.3 slab 链表,表示 slab 节点中全部都是空闲对象*/
	struct list_head slabs_free;

	/*3. 表示 slab 节点中有多少个 slab */
	unsigned long total_slabs;	/* length of all slab lists */

	/*4. 表示 slab 节点中有多少全是空闲对象的空闲 slab*/
	unsigned long free_slabs;	/* length of free slab list only */

	/*5. 表示 slab 节点中有多少个空闲对象*/
	unsigned long free_objects;

	/*6. 表示 slab 节点中所有空闲对象的最大阈值,
	 *   即 slab 节点中可容许的空闲对象数目最大阈值
	 */
	unsigned int free_limit;

	/*7. 记录当前着色区的编号。
	 *   所有 slab 节点都按照着色编号来计算着色区的大小,
	 *   达到最大值后又从 0 开始计算
	 */
	unsigned int colour_next;	/* Per-node cache coloring */

	/*8. 当前节点中的 共享对象缓存池
	 *   在多核 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

...

};

1.2.3. 对象缓冲池结构体 (array_cache):

对象缓存池通过struct array_cache结构体描述,该结构体可以描述本地对象缓冲池,也可以描述共享对象缓存池;

// 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[];
};

1.2.4. slab分配器

slab分配器由struct slab结构体描述, 会根据slab机制是slab, slub, slob进行字段选择;

/* Reuses the bits in struct page */
struct slab {
	unsigned long __page_flags;

#if defined(CONFIG_SLAB)
	/*1. 指向该slab分配器所属的slab缓存类型*/
	struct kmem_cache *slab_cache;
	union {
		struct {
			/*2. 维护在slab节点下的三个slab链表*/
			struct list_head slab_list;
			/*3. 指向当前slab分配器的管理区*/
			void *freelist;	/* array of free object indexes */
			/*4. 当前slab分配器下的第一个对象*/
			void *s_mem;	/* first object */
		};
		struct rcu_head rcu_head;
	};
	/*5. 当前slab分配器中 活跃的对象*/
	unsigned int active;

#elif defined(CONFIG_SLUB)
	...
#endif
	...
}

1.3. slab分配器的布局

slab节点中的三个slab链表(slabs_full, slabs_free,slabs_partial)上链接着若干个slab分配器,slab分配器的内存布局由下面三个部分组成:

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

slab分配器的布局模式不是唯一的,内核提供了三种布局模式:

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

    在这里插入图片描述

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

    在这里插入图片描述

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

    在这里插入图片描述

1.4. 着色区

着色区(colouring)是 slab 分配器用于优化高速缓存访问效率的一种策略。它通过在内存分配时引入偏移量,使对象在缓存行中的分布更加均匀,减少多个对象共享同一缓存行带来的缓存冲突(cache contention)。如果两个对象的起始地址总是对齐到相同的缓存行(如起始地址都为 0x100),同时访问这些对象会反复覆盖缓存行的内容,导致性能下降。

具体来说,着色区的大小由 colour_next × colour_off 计算得出,其中 colour_next 表示可用的偏移量数量,colour_off 代表 L1 缓存行大小。这样,每个 slab 中的对象在分配时会被适当错开,从而避免 CPU 访问时频繁发生缓存行覆盖,提高数据局部性和访问效率。

通过着色区的优化,同一 slab 分配器管理的对象不再集中在相同的缓存行,而是分散在不同的缓存行中,这样可以有效减少缓存争用,提高 CPU 访问内存的性能。这一机制对于多核系统尤为重要,因为它有助于提高数据在不同核心间的独立性,降低不必要的缓存同步开销。

2. 创建一个slab缓存(kmem_cache)

内核中创建一个slab缓存(slab描述符)的大致步骤如下:

  • 在创建slab缓存之前, 会首先查看是否存在一个满足所要创建slab描述符要求的 已存在的slab描述符,如果存在这样的slab描述符, 那么将其复用 (__kmem_cache_alias() -> find_mergeable());不满足复用条件, 则尝试创建一个新的slab描述符;
    • 可复用条件: 已存在slab描述符大小 大于 所要申请的大小;
    • 可复用条件: 二者对齐方式相同;
    • 可复用条件: 标志位slag匹配;
  • 为slab描述符 重新申请一个缓存用于存放slab描述符名字;
  • 开始创建一个新的slab描述符:
    • a. 创建slab描述符所用到的结构体 struct kmem_cache
    • b. 创建slab描述符实体: 着色区的计算, 分配掩码确认, slab分配器的布局设置, 配置slab描述符
    • c. 将新创建好的slab描述符加入系统slab_caches链表中

具体的函数调用图如下:

在这里插入图片描述

借鉴《奔跑吧Linux内核》一书中的插图,slab描述符的创建过程如下:

在这里插入图片描述

kmem_cache_create()是slab分配器提供的接口函数,用于创建一个slab描述符,该函数会调用kmem_cache_create_usercopy函数:

struct kmem_cache *
kmem_cache_create(const char *name, unsigned int size, unsigned int align,
		slab_flags_t flags, void (*ctor)(void *))
{
	/*1.调用kmem_cache_create_usercopy实现*/
	return kmem_cache_create_usercopy(name, size, align, flags, 0, 0,
					  ctor);
}

kmem_cache_create() -> kmem_cache_create_usercopy()函数会检查是否存在可复用的slab描述符, 如果不存在,才会进一步为slab描述符进行名称和实体的创建; 其中通过__kmem_cache_alias检查系统中是否存在可复用的slab描述符, 通过kstrdup_const()为新slab描述符所用名字申请缓存, 通过create_cache()申请新的slab描述符实体;

struct kmem_cache *
kmem_cache_create_usercopy(const char *name,
		  unsigned int size, unsigned int align,
		  slab_flags_t flags,
		  unsigned int useroffset, unsigned int usersize,
		  void (*ctor)(void *))
{
	struct kmem_cache *s = NULL;
	const char *cache_name;
	int err;

#ifdef CONFIG_SLUB_DEBUG
	/*0. 如果启用了SLUB调试选项,则初始化相关调试功能*/
	if (flags & SLAB_DEBUG_FLAGS)
		static_branch_enable(&slub_debug_enabled);
	if (flags & SLAB_STORE_USER)
		stack_depot_init();
#endif

	/*1. 申请slab_mutex互斥锁进行上锁保护
	 */
	mutex_lock(&slab_mutex);

	/*2.做必要的检查
	 *  检查名称、大小是否合法,
	 *  以及是否在中断上下文中
	 *  验证传入的flag标志是否在允许范围内
	 */
	err = kmem_cache_sanity_check(name, size);
	if (err) {
		goto out_unlock;
	}

	/* Refuse requests with allocator specific flags */
	if (flags & ~SLAB_FLAGS_PERMITTED) {
		err = -EINVAL;
		goto out_unlock;
	}

	flags &= CACHE_CREATE_MASK;

	/* Fail closed on bad usersize of useroffset values. */
	if (!IS_ENABLED(CONFIG_HARDENED_USERCOPY) ||
	    WARN_ON(!usersize && useroffset) ||
	    WARN_ON(size < usersize || size - usersize < useroffset))
		usersize = useroffset = 0;
		
	/*3. 检查是否有现成的slab描述符可复用
	 *   调用__kmem_cache_alias 判断是否可复用
	 */
	if (!usersize)
		s = __kmem_cache_alias(name, size, align, flags, ctor);
	if (s)
		goto out_unlock;

	/*4. 没有可复用的slab描述符,重新分配一个缓冲区存放slab描述符的名称*/
	cache_name = kstrdup_const(name, GFP_KERNEL);
	if (!cache_name) {
		err = -ENOMEM;
		goto out_unlock;
	}

	/*5. 创建一个新的slab描述符*/
	s = create_cache(cache_name, size,
			 calculate_alignment(flags, align, size),
			 flags, useroffset, usersize, ctor, NULL);
	if (IS_ERR(s)) {
		err = PTR_ERR(s);
		kfree_const(cache_name);
	}

out_unlock:
	/*6. 释放互斥锁*/
	mutex_unlock(&slab_mutex);
	...
	return s;
}
EXPORT_SYMBOL(kmem_cache_create_usercopy);

2.1. create_cache 创建slab描述符实体

该函数会为slab缓存申请slab描述符的结构体kmeme_cache并填充, 调用 __kmem_cache_create()函数创建slab描述符, 并最终将新创建好的slab描述符加入系统slab_caches链表;

在这里插入图片描述

static struct kmem_cache *create_cache(const char *name,
		unsigned int object_size, unsigned int align,
		slab_flags_t flags, unsigned int useroffset,
		unsigned int usersize, void (*ctor)(void *),
		struct kmem_cache *root_cache)
{
	struct kmem_cache *s;
	int err;

	if (WARN_ON(useroffset + usersize > object_size))
		useroffset = usersize = 0;

	err = -ENOMEM;

	/*1. 创建一个新的kmem_cache数据结构,并填充*/
	s = kmem_cache_zalloc(kmem_cache, GFP_KERNEL);
	if (!s)
		goto out;

	s->name = name;
	s->size = s->object_size = object_size;
	s->align = align;
	s->ctor = ctor;
#ifdef CONFIG_HARDENED_USERCOPY
	s->useroffset = useroffset;
	s->usersize = usersize;
#endif

	/*2. 创建该slab描述符*/
	err = __kmem_cache_create(s, flags);
	if (err)
		goto out_free_cache;

	s->refcount = 1;
	
	/*3. 将新建的slab 描述加入slab_cache 链表中*/
	list_add(&s->list, &slab_caches);
	return s;

out_free_cache:
	kmem_cache_free(kmem_cache, s);
out:
	return ERR_PTR(err);
}

2.2. __kmem_cache_create

该函数的重点步骤如下: 首先会计算着色区大小和便偏移, 避免同一 slab 内的对象占用相同的缓存行,减少 CPU 缓存冲突; 其次会确定分配掩码, 并根据slab分配器的内存布局模式进行内存布局的设置, 其实无论是什么布局模式,最终都会调用calculate_slab_order();最后会调用set_cpu_cache()函数配置slab缓存, 其实就是在每个cpu建立该slab缓存的本地对象缓存池,在每个NUMA节点建立该slab缓存的slab节点;

/**
 * @brief 根据传入的缓存描述符 (struct kmem_cache *cachep) 和标志位 (slab_flags_t flags),
 *        为特定的内存分配需求创建一个 slab 缓存
 */
int __kmem_cache_create(struct kmem_cache *cachep, slab_flags_t flags)
{
	size_t ralign = BYTES_PER_WORD;
	gfp_t gfp;
	int err;
	unsigned int size = cachep->size;

	...
	/*1.  对齐方式的确认与调整;
	 */
	/*1.1 让slab 描述符的大小size 与 系统的word长度对齐
	 *    确保对象大小是字对齐的,
	 *    避免在某些架构上因未对齐访问引发错误;
	 */
	size = ALIGN(size, BYTES_PER_WORD);

	if (flags & SLAB_RED_ZONE) {/*SLAB_RED_ZONE,检查是否溢出,实现调试功能*/
		ralign = REDZONE_ALIGN;
		/* If redzoning, ensure that the second redzone is suitably
		 * aligned, by adjusting the object size accordingly. */
		size = ALIGN(size, REDZONE_ALIGN);
	}

	/*1.2 强制对齐;
	 *    如果指定的对齐方式 比当前对齐方式ralign更严格,
	 *    则调整为更严格的对齐方式;*/
	if (ralign < cachep->align) {
		ralign = cachep->align;
	}
	/* disable debug if necessary */
	if (ralign > __alignof__(unsigned long long))
		flags &= ~(SLAB_RED_ZONE | SLAB_STORE_USER);
	/*
	 * 4) Store it.
	 */
	cachep->align = ralign;

	/*2. 着色区的处理:
	 *   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;

	/*3. 根据slab初试化程度,确认分配掩码;
	 *   枚举类型 slab_state 表示 slab 系统初始化的状态;
	 *   如 DOWN、PARTIAL、PARTIAL_NODE、UP 和 FULL 等;
	 *   FULL 和 UP 表示slab子系统已经初始化完成;
	 *   当 slab 机制完全初始化完成后状态变成 FULL;
	 * 
	 *   slab_is_available()表示当 slab 分配器处于 UP 或者 FULL 状态时,
	 *   分配掩码可以使用 GFP_KERNEL;否则,只能使用 GFP_NOWAIT;
	 */
	if (slab_is_available())
		gfp = GFP_KERNEL;
	else
		gfp = GFP_NOWAIT;

	kasan_cache_create(cachep, &size, &flags);
	/*slab 对象的大小按照 cachep->align 大小来对齐*/
	size = ALIGN(size, cachep->align);
	
	... 
	
	/*4. slab分配器的内存布局设置;
	 *   
	 *   会根据三种不同的slab分配器布局模式,进行设置,主要解决以下问题:
	 *   a. 一个slab分配器 需要多少个连续物理页面?
	 *   b. 一个slab分配器 可包含多少个slab对象?
	 *   c. 管理slab对象的大小是多少?
	 *   d. 一个slab分配器包含多少个着色区?
	 */

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

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

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

	return -E2BIG;

done:
	/*5. 配置slab描述符*/

	/*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_CACHE_DMA32)
		cachep->allocflags |= GFP_DMA32;
	if (flags & SLAB_RECLAIM_ACCOUNT)
		cachep->allocflags |= __GFP_RECLAIMABLE;
	/*size 表示一个 slab 对象的大小*/
	cachep->size = size;
	cachep->reciprocal_buffer_size = reciprocal_value(size);

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

	return 0;
}

3. 分配slab对象

kmem_cache_alloc()是slab机制中用于分配slab缓存对象的核心函数, 该函数会从指定的slab缓存分配一个空闲的slab对象, 在分配过程中处于关中断状态; __do_cache_alloc()->____cache_alloc()实现分配一个空闲对象, ____cache_alloc()函数会通过快速分配路径和慢速分配路径两种方法 从本地对象缓存池中分配一个空闲slab对对象;

快速分配路径: 在分配对象过程中, 会优先从本地对象缓存池中分配, 如果本地对象缓存池有空闲对象, 则直接弹出缓存池中最后一个空闲对象, 并返回该对象即可;

慢速分配路径: 当本地对象缓存池中 没有一个空闲对象, 那么就需要依次通过以下方法,迁移batchcount个空闲对象到本地对象缓存池中:

  • 1.当前NUMA节点中的共享对象缓存池,如果共享对象缓存池中存在空闲对象,则迁移到跟本地对象缓存池ac中;
  • 2.slab节点下的slab分配器中迁移,如果当前slab节点中的slabs_freeslabs_partial链表中存在足够的空闲对象,则迁移batchcount个空闲对象到本地对象缓存池;
  • 3.扩展slab分配器,如果当前slab节点下的共享对象缓存池 以及 三大链表中的slab分配器均没有一个空闲的slab对象,则需要从伙伴系统中申请一段连续的物理页面,即重新分配一个全新的slab分配器,并从中迁移batchcount个空闲对象到本地对象缓存池;

在迁移结束,依然要从本地对象缓存池中申请一个空闲对象,并将该对象返回;

以上便是申请一个slab对象的全流程,接下来上一张图:

在这里插入图片描述

kmem_cache_alloc()函数最终调用slab_alloc_node()函数 从指定NUMA节点分配slab空闲对象;其实现源码如下:

/*从指定NUMA节点 分配物理对象*/
static __always_inline void *
slab_alloc_node(struct kmem_cache *cachep, struct list_lru *lru, gfp_t flags,
		int nodeid, size_t orig_size, unsigned long caller)
{
	unsigned long save_flags;
	void *objp;
	struct obj_cgroup *objcg = NULL;
	bool init = false;

	/*1. 预分配检查,涉及对象 cgroup 资源限制*/
	flags &= gfp_allowed_mask;
	cachep = slab_pre_alloc_hook(cachep, lru, &objcg, 1, flags);
	if (unlikely(!cachep))
		return NULL;

	/*2. 使用 KFENCE 机制分配对象;
	 *   如果 kfence_alloc() 能够满足分配需求(可能是为了检测越界访问),
	 *   直接返回 objp,无需再进入普通分配流程
	 */
	objp = kfence_alloc(cachep, orig_size, flags);
	if (unlikely(objp))
		goto out;

	/*3.  正常slab对象分配流程
	 *3.1 首先关中断,因为要从本地CPU对象缓存池中分配空闲对象,
	 *    防止cpu资源竞争,保证slab分配的原子性;
	 *3.2 其次调用__do_cache_alloc -> ____cache_alloc 进行快速或慢速路径
	 *    从本地对象缓存池分配slab对象;
	 *3.3 最后 开中断;
	 */
	/*3.1 关中断*/
	local_irq_save(save_flags);
	/*3.2 分配slab对象*/
	objp = __do_cache_alloc(cachep, flags, nodeid);
	/*3.3 开中断*/
	local_irq_restore(save_flags);

	/*4. 分配后调试检查*/
	objp = cache_alloc_debugcheck_after(cachep, flags, objp, caller);
	prefetchw(objp);
	/*5. 初始化分配的对象*/
	init = slab_want_init_on_alloc(flags, cachep);

out:
	/*6. 后处理钩子*/
	slab_post_alloc_hook(cachep, objcg, flags, 1, &objp, init,
				cachep->object_size);
	return objp;
}

执行真正分配操作的是__do_cache_alloc()函数,该函数会调用____cache_alloc()函数通过快速及慢速方式分配空闲slab对象;

/*分配slab缓存对象*/
static __always_inline void *
__do_cache_alloc(struct kmem_cache *cachep, gfp_t flags, int nodeid __maybe_unused)
{
	return ____cache_alloc(cachep, flags);
}

我们看一下____cache_alloc()函数通过快速或慢速路径 从本地对象缓存池分配slab对象的逻辑:

  • 快速路径: 如果本地对象缓存池中有空闲的对象,则直接分配;
  • **慢速路径: **从当前节点对应的共享对象缓存池、slab分配器、或扩展slab分配器 迁移空闲对象 至 本地对象缓存池, 并从本地对象缓存池中分配一个空闲对象;
/**
 * @brief 通过快速或慢速路径 从本地对象缓存池分配slab对象;
 *        a.如果本地对象缓存池中有空闲的对象,则直接分配;
 *        b.如果没有,则尝试慢速路径分配,
 *        即:从当前节点对应的共享对象缓存池、slab分配器、或扩展slab 
 *        来将对应的空闲对象迁移至 本地对象缓存池,并分配;
 */
static inline void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
	void *objp;
	struct array_cache *ac;
	/*0. 检查是否处于关中断状态*/
	check_irq_off();

	/*1. 获取slab描述符中cache的本地CPU对象缓冲池ac*/
	ac = cpu_cache_get(cachep);

	/*2. 快速路径: 从当前CPU 本地缓存池中快速获取对象;
	 *
	 *   判断本地对象缓冲池中有没有空闲的对象;
	 *   ac->avail 表示本地对象缓冲池中有空闲对象;
	 *   这里直接通过 ac->entry[--ac->avai] 弹出最后一个 slab对象。
	 *   获取到slab对象后, 去out处返回该objp slab对象
	 */
	if (likely(ac->avail)) {
		ac->touched = 1;
		objp = ac->entry[--ac->avail];

		STATS_INC_ALLOCHIT(cachep);
		goto out;
	}

	/*3. 慢速路径: 从共享缓存池、slab分配器、或扩展slab分配器 来分配;
	 *   本地CPU缓存池中没有足够的对象,尝试从共享slab中 重新填充本地CPU缓存池;
	 */
	STATS_INC_ALLOCMISS(cachep);
	objp = cache_alloc_refill(cachep, flags);

	/*4. 再次获取本地CPU 缓存池*/
	ac = cpu_cache_get(cachep);

out:
	if (objp)
		kmemleak_erase(&ac->entry[ac->avail]);
	return objp;
}

3.1. 慢速路径分配空闲对象

慢速路径主要是先将空闲对象迁移至本地对象缓存池, 再从本地对象缓存池中弹出最后一个空闲对象;从哪里迁移空闲对象? 从当前slab节点中迁移, 优先从当前slab节点中的shared共享对象缓存池迁移, 不够则从slab链表中的slab分配器中迁移空闲对象, 如果slab链表中没有一个slab分配器含有空闲对象, 说明需要从伙伴系统中再申请一段连续的物理页面用于slab分配器,然后迁移空闲对象到本地对象缓存池中; 详细分配流程可见下图:

在这里插入图片描述

/* 慢速路径分配对象
 * 本地CPU对象缓存池为空,即没有空闲slab对象,
 * 则从共享对象缓存池、共享数组、通过扩展slab 向本地CPU对象缓存池ac 迁移
 * 并弹出本地对象缓存池中最后一个 slab对象;
 */
static void *cache_alloc_refill(struct kmem_cache *cachep, gfp_t flags)
{
	int batchcount;
	struct kmem_cache_node *n;
	struct array_cache *ac, *shared;
	int node;
	void *list = NULL;
	struct slab *slab;
	/*0. 确保本地关中断
	 *   获取当前 CPU 的 NUMA 节点 ID
	 *   获取CPU本地对象缓冲池ac;
	 *   获取slab节点;
	 *   获取共享对象缓存池 shared;
	 */
	check_irq_off();
	node = numa_mem_id();

	ac = cpu_cache_get(cachep);//获取CPU本地对象缓冲池ac
	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;
	}
	n = get_node(cachep, node);//获取当前NUMA节点的 slab节点

	BUG_ON(ac->avail > 0 || !n);
	shared = READ_ONCE(n->shared);//获取当前节点的共享对象缓存池 shared

	/*1. 若共享缓存池为空、当前slab节点没有可用对象,则直接去扩展slab*/
	if (!n->free_objects && (!shared || !shared->avail))
		goto direct_grow;
	/*2. 加锁保护 NUMA 节点的共享和 slab 数据结构*/
	raw_spin_lock(&n->list_lock);
	shared = READ_ONCE(n->shared);

	/* See if we can refill from the shared array */
	/*3. 从共享对象缓冲池shared 迁移 到本地缓存池
	 *   如果共享对象缓冲池shared 中有空闲对象,
	 *   则从共享对象缓冲池shared 向 本地对象缓冲池ac 迁移 batchcount个 空闲对象;
	 *   执行迁移操作(transfer_objects);
	 */
	if (shared && transfer_objects(ac, shared, batchcount)) {
		shared->touched = 1;// 标记共享缓存最近被访问
		goto alloc_done;	// 如果成功,从共享缓存补充完成,跳转至完成流程
	}

	/*4. 从 slab节点 中两个链表中 迁移空闲对象 到 本地对象缓存池ac
	 *   如果共享对象缓存池中没有空闲对象;
	 *   尝试从当前NUMA节点的 slab节点中 (slabs_partial , slabs_free 链表) 
	 *   迁移 batchcount 个空闲对象到本地缓冲池;
	*/
	while (batchcount > 0) {
		/* Get slab alloc is to come from. */
		/*4.1 获取slab节点中第一个slab成员;
		 *    查看 slabs_partial , slabs_free 链表
		 *    返回该链表中第一个slab成员;
		 *    若无slab可用,则跳转到 扩展slab 流程中;
		 */
		slab = get_first_slab(n, false);
		if (!slab)
			goto must_grow;

		check_spinlock_acquired(cachep);
		/*4.2 从slab中迁移batchcount个空闲对象 到 本地缓存池中*/
		batchcount = alloc_block(cachep, ac, slab, batchcount);

		/*4.3 根据 slab 使用情况修正其链表位置*/
		fixup_slab_list(cachep, n, slab, &list);
	}

must_grow:
	/*4.4. 更新 NUMA 节点的 可用对象free_objects 计数*/
	n->free_objects -= ac->avail;
alloc_done:
	/*5. 分配成功,释放锁 并 修正调试数据(用于对象泄漏检测)*/
	raw_spin_unlock(&n->list_lock);
	fixup_objfreelist_debug(cachep, &list);

direct_grow:
	/*6. 本地缓存仍为空,扩展slab,即重新从伙伴系统申请物理内存分配到slab分配器
	 *   a.共享对象缓存池没有空闲对象 及 b.slab节点没有空闲对象;
	 *   说明当前NUMA节点没有slab空闲对象;
	 *   只能重新分配该类型的slab,这就是一开始初始化和配置slab描述符的情景;
	 */
	if (unlikely(!ac->avail)) {
		/* Check if we can use obj in pfmemalloc slab */
		/*6.1 检查是否可以使用 pfmemalloc 的特殊 slab(用于内存压力情况下)*/
		if (sk_memalloc_socks()) {
			void *obj = cache_alloc_pfmemalloc(cachep, n, flags);

			if (obj)
				return obj;
		}

		/*6.2 扩展slab(分配一个cachep类型的slab),
		 *    然后返回该slab中第一个物理页面的page结构体指针
		 */
		slab = cache_grow_begin(cachep, gfp_exact_node(flags), node);

		/*
		 * cache_grow_begin() can reenable interrupts,
		 * then ac could change.
		 */
		ac = cpu_cache_get(cachep);

		/*6.3 从新扩展的slab分配器中 迁移 batchcount 个空闲对象 到 本地对象缓存池ac 中*/
		if (!ac->avail && slab) 
			alloc_block(cachep, ac, slab, batchcount);

		/*6.4 将刚分配的slab分配器添加到合适的队列中,
		 *    这个场景下应该添加到slabs partial 链表中
		 */
		cache_grow_end(cachep, slab);

		/*6.5 扩展slab失败,
		 *    本地缓存池中依然没有空闲对象,返回失败
		 */
		if (!ac->avail)
			return NULL;
	}

	/*7. 设置本地对象缓冲池的touched 为1,表示刚刚使用过本地对象缓冲池。*/
	ac->touched = 1;

	/*8. 从本地对象缓存区 弹出一个空闲对象*/
	return ac->entry[--ac->avail];
}

3.2. 扩展slab分配器

扩展slab分配器其实就是根据前面创建slab描述符时设定的slab布局, 为slab分配器申请连续物理页面,并为slab分配器中的着色区、freelist管理区进行初始化;在看源码时,我们可以知道,slab机制是通过kmem_getpages()函数从伙伴系统中申请固定大小的连续物理页面;

在这里插入图片描述

/*
 * Grow (by 1) the number of slabs within a cache.  This is called by
 * kmem_cache_alloc() when there are no active objs left in a cache.
 * 扩展slab,当共享对象缓存池,slab节点没有空闲对象时,需要扩展slab;
 */
static struct slab *cache_grow_begin(struct kmem_cache *cachep,
				gfp_t flags, int nodeid)
{
	void *freelist;
	size_t offset;
	gfp_t local_flags;
	int slab_node;
	struct kmem_cache_node *n;
	struct slab *slab;

	/*0. 标志检查,条件检查,中断环境检查*/
	if (unlikely(flags & GFP_SLAB_BUG_MASK))
		flags = kmalloc_fix_flags(flags);

	WARN_ON_ONCE(cachep->ctor && (flags & __GFP_ZERO));
	local_flags = flags & (GFP_CONSTRAINT_MASK|GFP_RECLAIM_MASK);

	check_irq_off();//确认中断已经关闭;
	if (gfpflags_allow_blocking(local_flags))
		local_irq_enable();

	/*1. 给slab 分配物理页面,从伙伴系统中分配;
	 *   分配一个 slab分配器所需要的物理页面,
	 *   这里会从指定的节点(nodeid)分配 2^cachep->gfporder个页面;
	 *   调用的是__alloc_pages_node来从伙伴系统分配物理页面;
	 */
	slab = kmem_getpages(cachep, local_flags, nodeid);
	if (!slab)
		goto failed;

	slab_node = slab_nid(slab);
	n = get_node(cachep, slab_node);

	/* Get colour for the slab, and cal the next value. */
	/*2. 计算slab分配器的着色区大小和实际偏移,便于后面为slab管理区分配freelist
	 *   offset在分配对象时用于调整起始地址,
	 *   使得不同slab的对象在缓存行中的位置不同,提高缓存利用率;
	 */
	n->colour_next++;
	if (n->colour_next >= cachep->colour)
		n->colour_next = 0;

	offset = n->colour_next;
	if (offset >= cachep->colour)
		offset = 0;

	offset *= cachep->colour_off;


	/* Get slab management. */
	/*3. 计算slab管理区freelist的起始地址
	 *   并为slab分配管理结构 freelist
	 *   这里会根据slab分配器的不同模式 对管理区进行分配;
	 */
	/*3.1 找到管理区首地址,并分配freelist*/
	freelist = alloc_slabmgmt(cachep, slab, offset,
			local_flags & ~GFP_CONSTRAINT_MASK, slab_node);
	if (OFF_SLAB(cachep) && !freelist)//如果是OFF_SLAB模式,并且freelist没申请成功,跳转到opps1去清理
		goto opps1;
	/*3.2 建立页面与缓存的映射关系,关联freelist*/
	slab->slab_cache = cachep;
	slab->freelist = freelist;

	/*4. 初始化slab中的对象(调用构造函数,设置freelist等)
	 *   OBJFREELIST_SLAB 模式:使用最后一个slab对象作为管理区;
	 */
	cache_init_objs(cachep, slab);

	if (gfpflags_allow_blocking(local_flags))
		local_irq_disable();

	return slab;

opps1:
	kmem_freepages(cachep, slab);
failed:
	if (gfpflags_allow_blocking(local_flags))
		local_irq_disable();
	return NULL;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值