Linux深入理解内存管理26

Linux深入理解内存管理26(基于Linux6.6)---slub分配器介绍

一、概述

SLUB(Simple List-based Unqueued Buffer Allocator)是 Linux 内核中的一种内存分配器,专门用于优化内存分配和释放的效率。SLUB 是为了解决内核中多种内存分配需求而设计的,尤其是对于内核对象(如结构体、缓存等)的内存分配。

SLUB 是继 SLOB(Simple List of Blocks)和 SLAB 分配器之后的一个内存分配器。与其他内存分配器相比,SLUB 更加简洁且高效,它试图通过减少锁的使用和避免多余的内存操作来提高性能。

1.1、SLUB 分配器的特点

  1. 简洁的设计

    • SLUB 分配器设计上采用了 slab(内存块)缓存管理的思想,但相比旧版的 SLAB 分配器,SLUB 在设计上简化了许多不必要的机制,减少了内存管理的复杂性。
    • 不再使用复杂的“空闲链表”(free list)来管理空闲块,而是直接使用一个简单的单链表来管理每个 CPU 上的空闲内存块,避免了 SLAB 中的复杂管理。
  2. 每 CPU 本地化

    • SLUB 的重要特性之一是 每个 CPU 都有自己的本地缓存(local cache),这种设计可以极大地减少 CPU 之间的锁竞争,提高多核系统的并行性能。每个 CPU 都拥有独立的内存分配区域,只有当某个 CPU 的本地缓存无法满足分配请求时,才会去访问共享缓存。
  3. 内存对齐

    • SLUB 分配器对内存进行良好的对齐,确保内存块按照最适合的方式进行分配,减少因内存不对齐导致的性能问题。
  4. 少量的锁

    • SLUB 使用锁的次数非常少,通常只在必要的地方才会使用锁。这是为了提高并发性能,尤其是在多核机器中,尽量避免频繁的全局锁竞争。
  5. 延迟回收

    • SLUB 分配器采用了一种延迟回收策略,当内存块被释放后,它不会立刻返回到全局缓存中,而是会在一定的条件下再回收。这有助于减少内存分配和释放时的开销。
  6. 优化内存碎片化

    • SLUB 会通过对内存块进行组织和优化,尽量减少碎片化的发生。每个 slab 缓存管理的内存块数量相对固定,使得内存块的分配和释放效率较高。

1.2、SLUB 的工作原理

SLUB 的工作原理可以简单地总结为:

  1. 内存池管理

    • SLUB 使用多个“slab”来管理内存块,每个 slab 是一段连续的内存区域,这些 slab 会被分配给不同的内存缓存(cache)。每个 cache 存储了特定类型的内存块(例如特定结构体的内存)。
  2. 每 CPU 本地缓存

    • 每个 CPU 会有自己的本地缓存来存储已经分配出来但还未被释放的内存块。当 CPU 需要分配内存时,首先会从本地缓存中查找空闲的内存块。如果本地缓存没有足够的内存块,则会向共享缓存申请。
  3. 空闲块回收

    • 当某个内存块被释放时,它首先会被加入到本地缓存中。如果本地缓存中已满,或者无法继续存储更多内存块,则该块会被返回到共享缓存中。通过延迟回收和合理的分配策略,SLUB 能够最大限度地减少内存碎片。
  4. 对象的分配与释放

    • 每个分配的内存块被视为一个内核对象,每个对象都具有一定的生命周期。内存分配时,SLUB 会检查是否有空闲的对象,如果没有,SLUB 会向操作系统申请更多内存块。

1.3、SLUB 分配器的性能特点

  1. 适用于大规模系统

    • SLUB 的设计对于高并发的多核系统进行了优化,能有效减少 CPU 之间的竞争,从而提高内存分配的效率。
  2. 低内存碎片化

    • 通过在不同大小的 slab 中分配内存块,SLUB 能够减少内存碎片化的问题。由于内存块是固定大小的,这有助于内存的管理和回收。
  3. 减少锁的使用

    • SLUB 分配器极大地减少了对全局锁的依赖,它通过在每个 CPU 上本地缓存空闲块的方式,减少了多核系统中共享资源的竞争,进一步提高了并发性能。
  4. 延迟回收

    • 延迟回收的机制有助于减少内存回收时的开销,从而提高了内存分配的性能。

1.4、动态分配

在linux的内核运行需要动态分配内存的时候,其中有两种分配方案:

  • 第一种是以页为单位分配内存,即一次分配内存的大小必须是页的整数倍
  • 第二种是按需分配,一次分配的内存大小是随机的

​ 对于第一种方案是通过伙伴系统实现,以页为单位管理和分配内存,但是这个单位确实也太大了。对于第二种方案,在现实的需求中,如果我们要为一个10个字符的字符串分配空间,如果按照伙伴系统采用分配一个4KB或者更多空间的完整页面,不仅浪费而且完全不可接受。显然的解决方案是需要将页拆分为更小的单位,可以容纳大量的小对象。同时新的解决方案月不能给内核带来更大的开销,不能对系统性能产生影响,同时也必须保证内存的利用率和效率。基于此,slab的分配器就应运而生,该机制是并没有脱离伙伴系统,是基于伙伴系统分配的大内存进一步细化分成小内存分配,SLAB 就是为了解决这个小粒度内存分配的问题的。

slab分配器对许多可能的工作负荷都能很好工作,但是有一些场景,也无法提供最优化的性能。如果某些计算机处于当前硬件尺度的边界上,slab分配器就会出现一些问题。同时对于嵌入式系统来说slab分配器代码量和复杂度都太高,所以内核增加了两个替代品,所以目前有三种实现算法,分别是slab、slub、slob,并且,依据它们各自的分配算法,在适用性方面会有一定的侧重。

  • Slab是最基础的,最早基于Bonwick的开创性论文并且可用 自Linux内核版本2.2起。
  • slob是被改进的slab,针对嵌入式系统进行了特别优化,以便减小代码量。围绕一个简单的内存块链表展开,在分配内存时,使用同样简单的最新适配算法。slob分配器只有大约600行代码,总的代码量很小。从速度来说,它不是最高效的分配器,页肯定不是为大型系统设计的。
  • slub是在slab上进行的改进简化,在大型机上表现出色,并且能更好的使用NUMA系统,slub相对于slab有5%-10%的性能提升和减小50%的内存占用。

二、Slub数据结构

 要想理解slub分配器,首先需要了解slub分配器的核心结构体,kmem_cache的结构体定义如下:

include/linux/slab_def.h

/*
 * Definitions unique to the original Linux SLAB allocator.
 */

struct kmem_cache {
	struct array_cache __percpu *cpu_cache;

/* 1) Cache tunables. Protected by slab_mutex */
	unsigned int batchcount;
	unsigned int limit;
	unsigned int shared;

	unsigned int size;
	struct reciprocal_value reciprocal_buffer_size;
/* 2) touched by every alloc & free from the backend */

	slab_flags_t flags;		/* constant flags */
	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;

	size_t colour;			/* cache colouring range */
	unsigned int colour_off;	/* colour offset */
	unsigned int freelist_size;

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

/* 4) cache creation/removal */
	const char *name;
	struct list_head list;
	int refcount;
	int object_size;
	int align;

/* 5) statistics */
#ifdef CONFIG_DEBUG_SLAB
	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;
	unsigned long node_frees;
	unsigned long node_overflow;
	atomic_t allochit;
	atomic_t allocmiss;
	atomic_t freehit;
	atomic_t freemiss;

	/*
	 * If debugging is enabled, then the allocator can add additional
	 * fields and/or padding to every object. 'size' contains the total
	 * object size including these internal fields, while 'obj_offset'
	 * and 'object_size' contain the offset to the user object and its
	 * size.
	 */
	int obj_offset;
#endif /* CONFIG_DEBUG_SLAB */

#ifdef CONFIG_KASAN
	struct kasan_cache kasan_info;
#endif

#ifdef CONFIG_SLAB_FREELIST_RANDOM
	unsigned int *random_seq;
#endif

	unsigned int useroffset;	/* Usercopy region offset */
	unsigned int usersize;		/* Usercopy region size */

	struct kmem_cache_node *node[MAX_NUMNODES];
};
字段名称类型含义说明
cpu_slabstruct kmem_cache_cpu __percpu *每个 CPU 的本地 slab 缓存指针,指向每个 CPU 专用的 slab 分配器。
flagsunsigned long用于存储 cache 的一些标志位,控制缓存的行为。
min_partialunsigned long最小的部分 slab 对象数目,控制部分 slabs 的最小数量。
sizeint单个对象的大小(包括元数据)。
object_sizeint单个对象的大小(不包括元数据)。
offsetint指定每个对象的空闲指针的偏移量,用于对象的内存管理。
cpu_partialint每个 CPU 上保持的部分对象的数量,控制 CPU 本地缓存中的部分对象数目。
oostruct kmem_cache_order_objects定义分配对象时的顺序和数量,具体涉及对象分配的顺序规则。
maxstruct kmem_cache_order_objects最大对象分配数量限制,控制每个缓存可分配的最大对象数。
minstruct kmem_cache_order_objects最小对象分配数量限制,控制每个缓存可分配的最小对象数。
allocflagsgfp_t内存分配时使用的 GFP 标志,控制分配内存的行为(如阻塞或非阻塞)。
refcountintslab cache 的引用计数,用于管理缓存的生命周期(销毁缓存时减少引用)。
ctorvoid (*)(void *)构造函数,用于初始化分配的内存对象(例如清零或初始化数据)。
inuseint内存使用中的偏移量,指示有多少内存正在被占用。
alignint对象的对齐方式,确保每个对象在内存中按照指定的对齐要求分配。
reservedintslab 区域末尾保留的字节数,用于内存对齐或其他目的。
nameconst char *slab cache 的名称(仅用于显示),便于调试和查看。
liststruct list_head连接所有 slab 缓存的链表头,表示缓存之间的关系。
red_left_padint左侧的红区填充大小,用于防止内存覆盖问题(通常用于保护堆栈溢出)。
node[MAX_NUMNODES]struct kmem_cache_node *指向每个 NUMA 节点的指针,表示不同节点上的 slab 分配器。

 在该结构体中,有一个变量struct list_head list,可以想象下,对于操作系统来讲,要创建和管理的缓存不至于task_struct,对于mm_struct,fs_struct都需要这个结构体,所有的缓存最后都会放到这个链表中,也就是LIST_HEAD(slab_caches)。对于缓存中哪些对象被分配,哪些是空着,什么情况下整个大内存块都被分配完了,需要向伙伴系统申请几个页形成新的大内存块?这些信息该由谁来维护呢??就引出了两个成员变量kmem_cache_cpu和kmem_cache_node。

在分配缓存的时候,需要分两种路径,快速通道(kmem_cache_cpu)和普通通道(kmem_cache_node),每次分配的时候,要先从kmem_cache_cpu分配;如果kmem_cache_cpu里面没有空闲块,那就从kmem_cache_node中进行分配;如果还是没有空闲块,最后从伙伴系统中分配新的页。

cpu_cache对于每个CPU来说,相当于一个本地内存缓冲池,当分配内存的时候,优先从本地CPU分配内存以及保证cache的命中率,struct kmem_cache_cpu用于管理slub缓存:

include/linux/slub_def.h

/*
 * When changing the layout, make sure freelist and tid are still compatible
 * with this_cpu_cmpxchg_double() alignment requirements.
 */
struct kmem_cache_cpu {
	void **freelist;	/* Pointer to next available object */
	unsigned long tid;	/* Globally unique transaction id */
	struct slab *slab;	/* The slab from which we are allocating */
#ifdef CONFIG_SLUB_CPU_PARTIAL
	struct slab *partial;	/* Partially allocated frozen slabs */
#endif
	local_lock_t lock;	/* Protects the fields above */
#ifdef CONFIG_SLUB_STATS
	unsigned stat[NR_SLUB_STAT_ITEMS];
#endif
};
字段名称类型含义说明
freelistvoid **指向下一个可用对象的指针,表示当前 CPU 上的空闲对象链表。
tidunsigned long全局唯一的事务 ID,用于标识当前的内存分配事务。
pagestruct page *指向正在为其分配内存的 slab 页,表示当前的分配页。
partialstruct page *指向部分已分配的“冻结” slab 页,表示未完全分配的 slab 页。

struct kmem_cache_node:用于管理每个Node的slub页面,由于每个Node的访问速度不一致,slub页面由Node来管理 。

mm/slab.h

/*
 * The slab lists for all objects.
 */
struct kmem_cache_node {
	spinlock_t list_lock;

#ifdef CONFIG_SLAB
	struct list_head slabs_partial;	/* partial list first, better asm code */
	struct list_head slabs_full;
	struct list_head slabs_free;
	unsigned long total_slabs;	/* length of all slab lists */
	unsigned long free_slabs;	/* length of free slab list only */
	unsigned long free_objects;
	unsigned int free_limit;
	unsigned int colour_next;	/* Per-node cache coloring */
	struct array_cache *shared;	/* shared per node */
	struct alien_cache **alien;	/* on other nodes */
	unsigned long next_reap;	/* updated without locking */
	int free_touched;		/* updated without locking */
#endif

#ifdef CONFIG_SLUB
	unsigned long nr_partial;
	struct list_head partial;
#ifdef CONFIG_SLUB_DEBUG
	atomic_long_t nr_slabs;
	atomic_long_t total_objects;
	struct list_head full;
#endif
#endif

};
字段名称类型含义说明
list_lockspinlock_t该锁用于保护 partial 链表,防止多个处理器同时修改该链表。
nr_partialunsigned long当前 NUMA 节点中 partial slab 链表中 slab 的数量。
partialstruct list_head部分已分配的 slab 链表头,包含所有分配了一部分对象但未完全分配的 slab。

 其结构图如下图所示 :

三、Slub初始化

为了初始化slub的数据结构,内核需要若干远小于一整页的内存块,这些最适合使用kmalloc来分配。但是此时只有slub系统已经完成初始化后,才能使用kmalloc。换而言之,kmalloc只能在kmalloc已经初始化之后初始化,这个是不可能,所以内核使用kmem_cache_init函数用于初始化slub分配器。
分配器的初始化工作主要是初始化用于kmalloc的gerneral cache,slub分配器的gerneral cache定义如下:

extern struct kmem_cache *kmalloc_caches[KMALLOC_SHIFT_HIGH + 1];
#define KMALLOC_SHIFT_HIGH	(PAGE_SHIFT + 1)
#define PAGE_SHIFT  12 

那么KMALLOC_SHIFT_HIGH=PAGE_SHIFT + 1 = 12 + 1 = 13,KMALLOC_SHIFT_HIGH+1=13+ 1= 14说明kmalloc_caches数组中有14个元素,每个元素是kmem_cache这个结构体。

它在内核初始化阶段(start_kernel)、伙伴系统启用之后调用,它首先执行缓存初始化过程,如下图所示:

  • 从缓存中分配kmem_cache对象,并复制并使用临时kmem_cache
  • 从缓存中分配kmem_cache_node对象,然后复制并使用临时使用的kmem_cache_node
  • kmalloc boot cache

起初并没有boot cache,因此定义了两个静态变量(boot_kmem_cache,boot_kmem_cache_node)用于临时使用。这里的核心是boot cache创建函数:create_boot_cache()

当调用create_boot_cache创建完kmem_cache和kmem_cache_node两个Cache后,就需要调用bootstrap从Cache中为kmem_cache和kmem_cache_node分配内存空间然后将静态变量boot_kmem_cache和boot_kmem_cache_node中的内容复制到分配的内存空间中,这相当于完成了一次对自身的重建:

mm/slub.c

static struct kmem_cache * __init bootstrap(struct kmem_cache *static_cache)
{
	int node;
	struct kmem_cache *s = kmem_cache_zalloc(kmem_cache, GFP_NOWAIT);---解析1
	struct kmem_cache_node *n;

	memcpy(s, static_cache, kmem_cache->object_size);

	/*
	 * This runs very early, and only the boot processor is supposed to be
	 * up.  Even if it weren't true, IRQs are not up so we couldn't fire
	 * IPIs around.
	 */
	__flush_cpu_slab(s, smp_processor_id());
	for_each_kmem_cache_node(s, node, n) {---解析2
		struct slab *p;

		list_for_each_entry(p, &n->partial, slab_list)
			p->slab_cache = s;

#ifdef CONFIG_SLUB_DEBUG
		list_for_each_entry(p, &n->full, slab_list)
			p->slab_cache = s;
#endif
	}
	list_add(&s->list, &slab_caches);
	return s;
}
  1. 首先将会通过kmem_cache_zalloc()申请kmem_cache空间,值得注意的是该函数申请调用kmem_cache_zalloc()->kmem_cache_alloc()->slab_alloc(),其最终将会通过前面create_boot_cache()初始化创建的kmem_cache来申请slub空间来使用。临时使用的kmem_cache结构形式接收的参数static_cache的内容复制到新分配的缓存中,其大小与object_size相同。早期引导过程中,因此无法对其他CPU进行IPI调用,因此仅刷新本地CPU。
  2. 通过for_each_node_state()遍历各个内存管理节点node,在通过get_node()获取对应节点的slab,如果slab不为空这回遍历部分满slab链,修正每个slab指向kmem_cache的指针,如果开启CONFIG_SLUB_DEBUG,则会遍历满slab链,设置每个slab指向kmem_cache的指针;最后将kmem_cache添加到全局slab_caches链表中。

接下来是创建kmalloc boot cache - create_kmalloc_caches(),来初始化kmalloc_caches表,其最终创建的kmalloc_caches是以{0,96,192,8,16,32,64,128,256,512,1024,2046,4096,8196}为大小的slab表;创建完之后,将设置slab_state为UP,然后将kmem_cache的name成员进行初始化;最后如果配置了CONFIG_ZONE_DMA,将会初始化创建kmalloc_dma_caches表。可以得到size_index与kmalloc_caches的对应关系:

以通常情况下KMALLOC_MIN_SIZE等于8为例进行说明。size_index[0-23]数组根据对象大小映射到不同的kmalloc_caches[0-13]保存的cache。观察kmalloc_caches[0-13]数组,可见索引即该cache slab块的order。由于最小对象为8(23)字节,kmalloc_caches[0-2]这三个数组元素没有用到,slub使用kmalloc_caches[1]保存96字节大小的对象,kmalloc_caches[2] 保存192字节大小的对象,相当于细分了kmalloc的粒度,有利于减少空间的浪费。kmalloc_caches[0]未使用。

四、总结

1、核心思想

slab分配器中用到了对象这个概念,就是内核中的数据结构以及对该数据结构进行创建和撤销的操作。其核心思想如下:

  • 将内核中经常使用的对象放到高速缓存中,并且由系统保持为初始的可利用状态,比如进程描述符,内核中会频繁对此数据进行申请和释放。
  • 当一个新进场创建时,内核就会直接从slab分配器的高速缓存中获取一个已经初始化的对象。
  • 当进程结束时,该结构所占的页框并不被释放,而是重新返回slab分配器中,如果没有基于对象的slab分配器,内核将花费更多的时间去分配、初始化、已经释放对象。

上图显示了slab、cache及object 三者之间的关系。该图显示了2个大小为3KB 的内核对象和3个大小为7KB的对象,它们位于各自的cache中。slab分配算法采用 cache来存储内核对象。在创建 cache 时,若干起初标记为free的对象被分配到 cache。cache内的对象数量取决于相关slab的大小。例如,12KB slab(由3个连续的4KB页面组成)可以存储6个2KB对象。最初,cache内的所有对象都标记为空闲。当需要内核数据结构的新对象时,分配器可以从cache上分配任何空闲对象以便满足请求。从cache上分配的对象标记为used(使用)。

考虑一个场景,这里内核为表示进程描述符的对象从slab分配器请求内存。在 Linux 系统中,进程描述符属于 struct task_struct 类型,它需要大约1.7KB的内存。当Linux内核创建一个新任务时,它从cache中请求 struct task_struct对象的必要内存。cache 利用已经在slab中分配的并且标记为 free (空闲)的 struct task_struct对象来满足请求。
在Linux中,slab可以处于三种可能状态之一:

  • 满的:slab的所有对象标记为使用。
  • 空的:slab上的所有对象标记为空闲。
  • 部分:slab上的对象有的标记为使用,有的标记为空闲。

slab分配器首先尝试在部分为空的slab中用空闲对象来满足请求。如果不存在,则从空的slab 中分配空闲对象。如果没有空的slab可用,则从连续物理页面分配新的slab,并将其分配给cache;从这个slab上,再分配对象内存。slab分配器提供两个主要优点:

  • 减小伙伴算法在分配小块连续内存时所产生的内部碎片问题,因为每个内核数据结构都有关联的cache,每个 cache都由一个或多个slab组成,而slab按所表示对象的大小来分块。因此,当内核请求对象内存时,slab 分配器可以返回刚好表示对象的所需内存。
  • 将频繁使用的对象缓存起来,减小分配、初始化和释放的时间开销 ,当对象频繁地被分配和释放时,如来自内核请求的情况,slab 分配方案在管理内存时特别有效。分配和释放内存的动作可能是一个耗时过程。然而,由于对象已预先创建,因此可以从cache 中快速分配。再者,当内核用完对象并释放它时,它被标记为空闲并返回到cache,从而立即可用于后续的内核请求。

对于伙伴系统和slab分配器,就好比“批发商”和“零售商”,“批发商”,是指按页面管理并分配内存的机制;而“零售商”,则是从“批发商”那里批发获取资源,并以字节为单位,管理和分配内存的机制。作为零售商的slab,那么就需要解决两个问题:

  • 该如何从批发商buddy system批发内存。
  • 如何管理批发的内存并把这些内存“散卖“出去,如何使这些散内存由更高的使用效率。

2、SLUB 分配器的优缺点

优点:

  • 简单且高效:相比于 SLAB,SLUB 的设计更加简洁,且对多核系统的支持更为高效。
  • 低锁竞争:SLUB 在并发环境下减少了锁竞争,通常具有较好的性能。
  • 延迟回收:延迟回收和合并策略能降低不必要的资源开销。

缺点:

  • 内存碎片:SLUB 的设计可能会导致较高的内存碎片,特别是在频繁分配和回收大小不均的对象时。
  • 内存占用:对于一些非常小或非常大的对象,SLUB 可能会导致更多的内存浪费。

3、SLUB 与其他分配器的比较

  • SLAB:SLAB 分配器相较于 SLUB 在设计上更复杂,提供了更细粒度的缓存管理,适合需要精细化内存管理的场景。SLUB 在多核环境下的性能更优,但 SLAB 在精度和内存回收方面可能会更具优势。
  • SLOB:SLOB 是一个针对嵌入式系统的简单内存分配器,适用于内存资源较少的环境。SLUB 提供了更好的性能和可扩展性。

4、总结

SLUB 分配器在 Linux 内核中被广泛使用,特别适用于高效的内存分配和回收。在多核系统中,它的低锁竞争和高效的缓存机制使其成为首选的内存分配器之一。SLUB 的设计目标是提供简单而高效的内存管理,减少了锁争用,并对多核系统进行了优化。通过延迟回收、紧凑的内存分配和部分已分配 slab 的使用,SLUB 分配器能够在性能和内存使用之间保持较好的平衡。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值