解密Slab分配器:内存管理的高效武器

系统 Linux
Slab 分配器是 Linux 内核中一种极为重要的内存管理机制,主要用于高效管理小块内存的分配。它针对频繁分配和释放的小对象进行了专门优化,能够有效减少内存碎片,显著提高系统性能。

在 Linux 操作系统的神秘世界里,内存管理如同一场精密的棋局,每一步都关乎着系统的性能和稳定性。而在这场棋局中,Slab 分配器宛如一颗璀璨的明珠,是内存管理的高效武器。它以其独特的设计和强大的功能,为 Linux 系统的内存分配与释放提供了卓越的解决方案。现在,让我们一同揭开 Slab 分配器的神秘面纱,探索它在内存管理中的神奇之处。

Linux内核版本:5.0
架构:ARM64
Linux 5.0内核源码注释仓库地址:
zhangzihengya/LinuxSourceCode_v5.0_study (http://github.com)

一、Slab分配器概述

Slab 分配器是 Linux 内核中一种极为重要的内存管理机制,主要用于高效管理小块内存的分配。它针对频繁分配和释放的小对象进行了专门优化,能够有效减少内存碎片,显著提高系统性能。在 Linux 内核的众多子系统中,Slab 分配器得到了广泛应用,比如网络缓冲区、文件系统缓存以及进程控制块等领域。

Slab 分配器的核心概念包括 Cache(缓存)、Slab 和 Object。其中,Cache 是为每种类型的对象创建的缓存,每个缓存存储相同大小的对象集合。Slab 则是一块连续的内存区域,用于存储一组特定大小的对象。而 Object 是在 Slab 中实际存储的数据单元。

Slab 分配器具有诸多优势。首先,它通过对象复用,避免了频繁的分配与释放操作,极大地提高了内存分配的效率。其次,它能够保证内存分配尺寸和对齐一致,从而有效减少内存碎片问题。最后,Slab 分配器适用于不同大小的对象,能够显著提升内核的整体性能。

通过查看/proc/slabinfo文件,可以了解 Slab 分配器的内存使用情况,帮助调优系统性能。该文件输出的头部包含多个字段,其中name字段表示 slab 缓存的名称,每个 slab 缓存存储相同类型和大小的对象;active_objs字段表示当前在使用的(已分配的)对象数量;num_objs字段表示缓存中分配的对象总数,包括已经分配和空闲的对象;objsize字段表示每个对象的大小(以字节为单位);objperslab字段表示每个 slab 中包含的对象数量;pagesperslab字段表示每个 slab 使用的页数;tunables字段中的值控制 slab 缓存的行为,如limit表示 slab 缓存中每个 CPU 可以缓存的最大对象数量,batchcount表示每次从全局缓存到 CPU 本地缓存中批量获取的对象数,sharedfactor控制多个 CPU 是否共享 slab 缓存;slabdata部分包含有关 slab 使用的统计数据,如active_slabs表示当前正在使用的 slab 数量,num_slabs表示系统中分配的总 slab 数量,sharedavail表示 CPU 本地缓存中可用对象的数量。

二、核心技术

2.1 slab机制

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

图片图片

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

把分配的内存块当作对象(object)来看待。对象可以自定义构造函数(constructor) 和析构函数(destructor)来初始化对象的内容并释放对象的内容。

slab对象被释放之后不会马上丢弃而是继续保留在内存中,可能稍后会被用到,这样不需要重新向伙伴系统申请内存。

slab机制可以根据特定大小的内存块来创建slab描述符,如内存中常见的数据结构、打开文件对象等,这样可以有效地避免内存碎片的产生,也可以快速获得频繁访问的数据结构。另外,slab机制也支持按2的n次方字节大小分配内存块。

slab机制创建了多层的缓冲池,充分利用了空间换时间的思想,未雨绸谬,有效地解决了效率问题。

每个CPU有本地对象缓冲池,避免了多核之间的锁争用问题。

每个内存节点有共享对象缓冲池。

2.2 slab框架

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

图片图片

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[]数组用于存放多个对象,如下图所示:

图片图片

⑴Cache(缓存)

Slab 分配器中的缓存(Cache)扮演着关键的角色。它是为每种类型的对象专门创建的,每个缓存都存储着相同大小的对象集合。比如,对于特定大小的内核数据结构,会有对应的缓存来管理这些对象的分配和释放。这种设计使得在需要分配相同类型的对象时,可以快速从缓存中获取,提高了内存分配的效率。

⑵Slab

Slab 是一块连续的内存区域,用于存储一组特定大小的对象。每个 Slab 都经过精心划分,以适应特定对象的存储需求。Slab 的大小通常由所存储对象的大小和数量决定。当系统需要为特定类型的对象分配内存时,Slab 分配器会从合适的 Slab 中分配空间。如果没有合适的 Slab 可用,分配器可能会创建新的 Slab。

⑶Object(对象)

Object 是在 Slab 中实际存储的数据单元。每个对象代表着特定类型的数据结构或资源。例如,在 Linux 内核中,进程控制块(PCB)可以作为一个对象存储在 Slab 中。对象可以处于不同的状态,如已分配、空闲或部分空闲。空闲对象通过空闲链表进行跟踪,以便在需要分配新对象时能够快速找到可用的空间。

三、优势解析

3.1 对象复用

Slab 分配器通过缓存对象避免了频繁的分配与释放操作。在内核中,会为有限的对象集分配大量内存,例如文件描述符和其他常见结构。而 Slab 分配器围绕对象缓存进行,将内存保持为针对特定目的而初始化的状态。例如,如果内存被分配给了一个互斥锁,那么只需在为互斥锁首次分配内存时执行一次互斥锁初始化函数即可。后续的内存分配不需要执行这个初始化函数,因为从上次释放和调用析构之后,它已经处于所需的状态中了。这样,当系统再次需要相同类型的对象时,可以直接从缓存中获取,极大地提高了内存分配的效率。

3.2 减少内存碎片

Slab 分配器能够保证内存分配尺寸和对齐一致,从而有效减少内存碎片问题。每个缓存结构都包括了两个重要的成员:nodelists 和 array。nodelists 中的 kmem_list3 结构将 slab 分为完全用尽的 slab 链表、部分用尽的 slab 链表和空闲的 slab 链表。部分空闲的 slab 在最开始,当一个 slab 中的所有对象都被使用完时,就从 slabs_partial 列表中移动到 slabs_full 列表中。当一个 slab 完全被分配并且有对象被释放后,就从 slabs_full 列表中移动到 slabs_partial 列表中。当所有对象都被释放之后,就从 slabs_partial 列表移动到 slabs_empty 列表中。这种管理方式使得内存的分配和释放更加有序,减少了内存碎片的产生。

此外,对象在 slab 中不是连续排列的,为了满足对齐要求,会在 slab 对象中添加填充字节以满足对齐要求,使用对齐的地址可以加速内存访问。如果创建 slab 时指定了 SLAB_HWCACHE_ALIGN 标志,则会按照 cache_line_size 的返回值对齐,即对齐的硬件缓存行上。如果对象小于硬件缓存行的一半,则将多个对象放入一个缓存行。如果没有指定对齐标记,则对齐到 BYTES_PER_WORD,即对齐到 void 指针所需字节数目。

3.2 高效的内存管理

Slab 分配器适用于不同大小的对象,能够显著提升内核的整体性能。Slab 分配器把对象分组放进高速缓存,每个高速缓存都是同种类型对象的一种 “储备”。一个 cache 管理一组大小固定的内存块,每个内存块都可用作一种数据结构。cache 中的内存块来自一到多个 slab,一个 slab 来自物理内存管理器的一到多个物理页,该 slab 被分成一组固定大小的块,被称为 slab 对象。

与传统的内存管理模式相比,Slab 缓存分配器提供了很多优点。首先,内核通常依赖于对小对象的分配,它们会在系统生命周期内进行无数次分配。Slab 缓存分配器通过对类似大小的对象进行缓存而提供这种功能,从而避免了常见的碎片问题。Slab 分配器还支持通用对象的初始化,从而避免了为同一目的而对一个对象重复进行初始化。最后,Slab 分配器还可以支持硬件缓存对齐和着色,这允许不同缓存中的对象占用相同的缓存行,从而提高缓存的利用率并获得更好的性能。

四、关键结构

4.1 kmem_cache

kmem_cache定义了要管理的给定大小的对象池,是 Linux 内存管理中 Slab 分配器的核心结构之一。它包含多个重要参数和引用,对内存分配起着关键作用。

kmem_cache结构中的struct array_cache __percpu *cpu_cache是一个重要的成员,它是每个 CPU 的对象缓存池,相当于快表。当系统进行内存分配时,会优先从这个本地缓存中获取对象,提高分配速度。

此外,batchcount、limit、shared等参数分别控制着从共享缓存或其他列表获取对象的数量、本地缓存中空闲对象的最大数量以及多核系统中的共享设置。size参数表示要管理的对象的长度,这个长度需要加上对齐字节。flags是对象的分配掩码,num表示一个 slab 中最多可以有多少个对象。

gfporder参数决定了一个 slab 中占用的连续页框数的对数,而allocflags则是与伙伴系统交互时提供的分配标识。colour和colour_off参数用于控制 slab 的颜色设置,实现缓存着色以提高缓存命中率。freelist_cache和freelist_size在 off-slab 时使用,将 freelist 放在 slab 物理页面外部。

ctor是构造函数指针,用于在创建对象时进行初始化操作。name是 slab 描述符的名称,list用于将该结构链接到全局链表中,refcount是引用次数,在释放 slab 描述符时会判断,只有引用次数为 0 时才真正释放。object_size是对象的实际大小,align是对齐的长度。

4.2 array_cache

array_cache是每个 CPU 的对象缓存池,在 Slab 分配器中起着实现快速分配和减少操作的关键作用。

array_cache结构中的avail表示对象缓存池中可用的对象数目。limit和batchcount与kmem_cache中的语义一致,分别控制着缓存的上限和从共享缓存或其他列表获取对象的数量。touched参数在从缓存池移除一个对象时置 1,而收缩缓存时置 0。entry数组保存着对象的实体,采用 LIFO(后进先出)方式进行分配,即将该数组中的最后一个索引对应的对象分配出去,以保证该对象还驻留在高速缓存中的可能性。

4.3 kmem_cache_node

kmem_cache_node管理从伙伴系统分配的物理页面,是 Slab 分配器在 NUMA 架构下的重要组成部分。它包含多个 slab 链表,对内存的分配和回收进行精细管理。

kmem_cache_node结构中的spinlock_t list_lock用于保护链表操作的互斥。在支持 CONFIG_SLAB 的情况下,它包含slabs_partial、slabs_full和slabs_free三个链表,分别对应部分用尽的 slab 链表、完全用尽的 slab 链表和空闲的 slab 链表。total_slabs表示三个链表中所有 slab 的总数,free_slabs表示空闲 slab 的数量,free_objects表示三个链表中所有空闲对象数目,free_limit表示 slab 中可以容许的空闲对象数目最大阈值。colour_next用于控制每个节点的缓存着色。

shared是多核 CPU 中的共享缓存区 slab 对象的指针。当一个 slab 中的所有对象都被使用完时,就从slabs_partial列表中移动到slabs_full列表中。当一个 slab 完全被分配并且有对象被释放后,就从slabs_full列表中移动到slabs_partial列表中。当所有对象都被释放之后,就从slabs_partial列表移动到slabs_empty列表中。这种管理方式使得内存的分配和释放更加有序,减少了内存碎片的产生。

五、操作流程

5.1 创建 slab 缓存

使用kmem_cache_create函数创建一个描述特定对象类型内存池的结构。kmem_cache_create函数需要多个参数,包括可读的名称、被管理对象以字节计的长度、对齐数据时使用的偏移量、一组标志以及构造函数等。首先,对象长度会向上舍入到处理器字长的倍数。如果设置了SLAB_HWCACHE_ALIGN标志,内核会按照特定于体系结构的函数cache_line_size给出的值来对齐数据,并尝试将尽可能多的对象填充到一个缓存行中。如果对象长度大于页帧的 1/8,则将头部管理数据存储在 slab 之外,否则存储在 slab 上。最后,通过迭代过程找到理想的 slab 长度,并对 slab 进行着色。

图片图片

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

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;
}

5.2 分配内存

通过kmem_cache_alloc从已创建的 slab 缓存中分配内存。首先会从每个 CPU 的本地对象缓存池(array_cache)中获取对象,如果本地缓存为空,则从共享缓存或其他列表中获取。如果所有列表中都没有空闲对象,则会调用cache_grow函数创建新的 slab。kmem_cache_alloc() 函数的流程图如下所示:

图片图片

为了使读者有更真切的理解,下文将根据流程图围绕源代码进行讲解这个过程: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];
}

5.3 释放内存

使用kmem_cache_free释放内存。如果 per-CPU 缓存中的对象数目低于允许的限制,则在其中存储一个指向缓存中对象的指针。否则,将一些对象从缓存移回 slab,并将剩余的对象向数组起始处移动。如果在删除之后,slab 中的所有对象都是未使用的,且缓存中空闲对象的数目超过预定义的限制,则使用slab_destroy将整个 slab 返回给伙伴系统。如果 slab 同时包含使用和未使用对象,则插入到slabs_partial链表。如果要销毁只包含未使用对象的一个缓存,则必须调用kmem_cache_destroy函数。该流程如下所示:

图片图片

为了使读者有更真切的理解,下文将根据流程图围绕源代码进行讲解这个过程: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;
}

六、重要字段解析

name(缓存名称)

表示 slab 缓存的名称,存储相同类型和大小的对象。例如 /proc/slabinfo 文件中的kmem_cache字段表示缓存的对象名,如task_struct。每个 slab 缓存存储相同类型和大小的对象,例如kmalloc-32是用于分配 32 字节的内存块。

active_objs(活动对象数)

当前在使用的对象数量。该字段表示当前在使用的(已分配的)对象数量,即系统中实际分配给内核使用的对象数量。例如,在系统中实际分配给内核使用的进程控制块等对象的数量就是活动对象数。

num_objs(总对象数)

缓存中分配的对象总数。这个字段表示缓存中分配的对象总数,包括已经分配和空闲的对象。这个值通常大于或等于active_objs。比如一个特定大小的 slab 缓存中,所有已经分配出去和尚未分配但已准备好的对象总数就是num_objs。

objsize(对象大小)

每个对象的大小。该字段表示每个对象的大小(以字节为单位),即 slab 缓存中每个对象占用的内存空间大小。例如对于特定的内核数据结构,其对象大小可以通过这个字段确定。

objperslab(每个 slab 包含的对象数)

每个 slab 中包含的对象数量。每个 slab 是一个较大的内存块,其中包含多个对象,这个字段表示每个 slab 中具体包含的对象数量。例如对于一个特定大小的 slab,根据对象大小和 slab 总大小,可以计算出objperslab的值。

pagesperslab(每个 slab 使用的页数)

每个 slab 使用的页数,通常为 4KB 大小。该字段表示每个 slab 使用的页数。Linux 内核使用分页机制来管理内存,页面通常为 4KB 大小。一个 slab 由一定数量的连续物理页组成,这个字段反映了每个 slab 占用的物理页数量。

tunables(可调参数)

控制 slab 缓存的行为,如limit、batchcount、sharedfactor等。这个字段中的值控制 slab 缓存的行为:

  • limit:slab 缓存中每个 CPU 可以缓存的最大对象数量。
  • batchcount:每次从全局缓存到 CPU 本地缓存中批量获取的对象数。
  • sharedfactor:控制多个 CPU 是否共享 slab 缓存。

slabdata(slab 统计信息)

包含有关 slab 使用的统计数据,如active_slabs、num_slabs、sharedavail等。这部分包含有关 slab 使用的统计数据:

  • active_slabs:当前正在使用的 slab 数量。
  • num_slabs:系统中分配的总 slab 数量。
  • sharedavail:CPU 本地缓存中可用对象的数量。通过这些统计数据,管理员可以了解系统中 slab 分配器的内存使用情况,帮助调优系统性能。

七、高速缓存分类

7.1 普通高速缓存

普通高速缓存为 kmem_cache结构本身和内核提供通用高速缓存。它通过通用高速缓存实现小块连续内存的分配,为内核提供了一种高效的内存管理方式。

普通高速缓存首先会为 kmem_cache结构本身提供高速缓存,这类缓存保存在 cache_cache变量中。cache_cache变量代表着 cache_chain 链表中的第一个元素,它保存着对高速缓存描述符的高速缓存。

通用高速缓存所提供的对象具有几何分布的大小,范围为 32 到 131072 字节。内核中提供了 kmalloc()和 kfree()两个接口分别进行内存的申请和释放。

7.2 专用高速缓存

专用高速缓存是根据内核所需,通过指定具体的对象而创建。它提供一套完整的接口用于高速缓存的申请、释放以及 slab 的申请和释放。

内核为专用高速缓存的申请和释放提供了接口,kmem_cache_create()用于对一个指定的对象创建高速缓存。它从 cache_cache普通高速缓存中为新的专有缓存分配一个高速缓存描述符,并把这个描述符插入到高速缓存描述符形成的 cache_chain链表中。kmem_cache_destory()用于撤销一个高速缓存,并将它从 cache_chain链表上删除。

对于 slab 的申请和释放,kmem_cache_alloc()在其参数所指定的高速缓存中分配一个 slab。相反,kmem_cache_free()在其参数所指定的高速缓存中释放一个 slab。

八、应用案例分析

8.1 定义和使用特定大小的对象

以下以专用 slab 缓存为例,展示如何定义和使用特定大小的对象。

首先,我们需要为特定的结构体创建一个专用的 slab 缓存。假设我们有一个名为sample_struct的结构体:

struct sample_struct {
    int id;
    char name[20];
    char address[50];
};

我们可以使用kmem_cache_create函数来创建一个用于存储sample_struct结构体的专用 slab 缓存:

static struct kmem_cache *sample_struct_cachep;
static void init_sample_struct_cache( void ){
    sample_struct_cachep = kmem_cache_create(
        "sample_struct_cachep",  /* Name */
        sizeof(struct sample_struct), /* Object Size */
        0,     /* Alignment */
        SLAB_HWCACHE_ALIGN,  /* Flags */
        NULL);  /* Constructor */
    return;
}

这里创建的特定缓存包含sample_struct大小的对象,并且是硬件缓存对齐的(由标志参数SLAB_HWCACHE_ALIGN定义)。

使用所分配的 slab 缓存对象可以通过以下方式进行:

int slab_test( void ){
    struct sample_struct *object;
    printk( "Cache name is %s\n", kmem_cache_name( sample_struct_cachep ) );
    printk( "Cache object size is %d\n", kmem_cache_size( sample_struct_cachep ) );
    object = kmem_cache_alloc(sample_struct_cachep, GFP_KERNEL);
    if (object) {
        // 使用 object...
        kmem_cache_free(sample_struct_cachep, object);
    }
    return 0;
}

8.2 销毁缓存

调用者必须确保在执行销毁操作过程中,不要从缓存中分配对象。销毁缓存的主要步骤如下:

  • 将缓存从cache_chain链表中删除。
  • 将本地高速缓存、alien 高速缓存和共享本地高速缓存中的对象都释放回 slab,并释放所有的 free 链表,然后判断 full 链表以及 partial 链表是否都为空,如果有一个不为空说明存在非空闲 slab,也就是说有对象还未释放,此时无法销毁缓存,重新将缓存添加到cache_chain链表中。
  • 确定所有的 slab 都为空闲状态后,将缓存涉及到的所有描述符都释放(这些描述符都是保存在普通高速缓存中的)。

负责销毁缓存的函数为kmem_cache_destroy,其代码实现如下:

void kmem_cache_destroy(struct kmem_cache *cachep){
    BUG_ON(!cachep || in_interrupt());
    /* Find the cache in the chain of caches. */
    get_online_cpus();
    mutex_lock(&cache_chain_mutex);
    /** the chain is never empty, cache_cache is never destroyed*//*将 cache 从 cache_chain 中删除*/
    list_del(&cachep->next);
    /*释放完 free 链表,如果 FULL 链表或 partial 链表中还有 slab,说明还有对象处于分配状态因此不能销毁该缓存!*/
    if (__cache_shrink(cachep)) {
        slab_error(cachep, "Can't free all objects");
        /*重新将缓存添加到 cache_chain 链表中*/
        list_add(&cachep->next, &cache_chain);
        mutex_unlock(&cache_chain_mutex);
        put_online_cpus();
        return;
    }
    if (unlikely(cachep->flags & SLAB_DESTROY_BY_RCU))
        rcu_barrier();
    /*释放 cache 所涉及到的各个描述符的存储对象*/
    __kmem_cache_destroy(cachep);
    mutex_unlock(&cache_chain_mutex);
    put_online_cpus();
}

static int __cache_shrink(struct kmem_cache *cachep){
    int ret = 0, i = 0;
    struct kmem_list3 *l3;
    /*将本地高速缓存,share 本地高速缓存以及 alien 高速缓存的空闲对象释放 slab*/
    drain_cpu_caches(cachep);
    check_irq_on();
    for_each_online_node(i) {
        l3 = cachep->nodelists[i];
        if (!l3) continue;
        /*销毁空闲链表中的 slab*/
        drain_freelist(cachep, l3, l3->free_objects);
        /*判断 full 和 partial 是否为空,有一个不为空则 ret 就为 1*/
        ret +=!list_empty(&l3->slabs_full) ||!list_empty(&l3->slabs_partial);
    }
    return (ret? 1 : 0);
}

drain_cpu_caches()的最终落脚在 free_block()函数上,该函数在前面已做过分析,在此不再列出。

static int drain_freelist(struct kmem_cache *cache, struct kmem_list3 *l3, int tofree){
    struct list_head *p;
    int nr_freed;
    struct slab *slabp;
    nr_freed = 0;
    /*slab 中的对象还未释放完并且 free 链表不为空*/
    while (nr_freed < tofree &&!list_empty(&l3->slabs_free)) {
        spin_lock_irq(&l3->list_lock);
        p = l3->slabs_free.prev;
        if (p == &l3->slabs_free) {/*链表中已无元素*/
            spin_unlock_irq(&l3->list_lock);
            goto out;
        }
        /*从 free 链表中取出一个 slab*/
        slabp = list_entry(p, struct slab, list);
#if DEBUG
        BUG_ON(slabp->inuse);
#endif
        /*从链表中删除*/
        list_del(&slabp->list);
        /** Safe to drop the lock. The slab is no longer linked* to the cache.*//*空闲对象数量总数减去 num*/
        l3->free_objects -= cache->num;
        spin_unlock_irq(&l3->list_lock);
        /*销毁 slab*/
        slab_destroy(cache, slabp);
        nr_freed++;
    }
out:
    return nr_freed;
}

slab_destroy()函数已在前文中分析。

static void __kmem_cache_destroy(struct kmem_cache *cachep){
    int i;
    struct kmem_list3 *l3;
    /*释放存储本地高速缓存描述符的对象*/
    for_each_online_cpu(i)
        kfree(cachep->array[i]);
    /* NUMA: free the list3 structures */
    for_each_online_node(i) {
        l3 = cachep->nodelists[i];
        if (l3) {
            /*释放存储共享本地高速缓存描述符的对象*/
            kfree(l3->shared);
            /*释放存储 alien 本地高速缓存描述符的对象*/
            free_alien_cache(l3->alien);
            /*释放存储 kmem_list3 描述符的对象*/
            kfree(l3);
        }
        /*释放存储缓存描述符的对象*/
        kmem_cache_free(&cache_cache, cachep);
    }}


责任编辑:武晓燕 来源: 深度Linux
相关推荐

2021-08-03 09:02:58

LinuxSlab算法

2009-12-25 15:34:54

slab分配器

2013-10-12 11:15:09

Linux运维内存管理

2017-01-17 16:17:48

C++固定分配器

2017-02-08 08:40:21

C++固定内存块

2017-01-20 14:21:35

内存分配器存储

2024-10-11 10:00:20

2023-04-03 08:25:02

Linux内存slub

2020-12-15 08:54:06

Linux内存碎片化

2020-03-11 13:44:20

编程语言PythonJava

2013-10-14 10:41:41

分配器buddy syste

2023-12-22 07:55:38

Go语言分配策略

2014-09-01 10:09:44

Linux

2013-10-11 17:24:47

Linux运维内存管理

2023-04-13 14:42:26

PoE供电器PoE交换机

2022-02-23 16:49:19

Linux内存数据结构

2018-12-06 10:22:54

Linux内核内存

2024-10-28 11:25:21

丰巢快递jemalloc

2020-07-07 07:57:39

Linux内存碎片化

2021-08-10 16:50:37

内核内存管理
点赞
收藏

51CTO技术栈公众号