伙伴分配器(buddy allocator)和slab

伙伴分配器(buddy allocator)和slab

当系统内核初始化完毕后,使用页分配器管理物理页,当使用的页分配器是伙伴分配器,伙伴分配器的特点是算法简单且高效。连续的物理页称为页块(page block)。阶(order)是伙伴分配器的一个专业术语,是页的数量单位, 2 n 2^n 2n个连续页称为n阶页块。物理内存被分成多个order:0 ~ 10,每个order中连续page的个数是2order,如果一个order中可用的memory size小于期望分配的size,那么更大order的内存块会被对半切分,切分之后的两个小块互为buddies。其中一个子块用于分配,另一个空闲的。这些块在必要时会连续减半,直到达到所需大小的memory 块为止,当一个block被释放之后,会检查它的buddies是否也是空闲的,如果是,那么这对buddies将会被合并。

满足以下条件 的两个n阶页块称为伙伴(buddy):
1、两个页块是相邻的,即物理地址是连续的;
2、页块的第一页的物理页号必须是2n的整数倍;
3、如果合并成(n+1)阶页块,第一页的物理页号必须是 2 n + 1 2^{n+1} 2n+1的整数倍。

详情请看视频: https://ke.qq.com/course/417774?flowToken=1041370

a.原理

伙伴分配器分配和释放物理页的数量单位为阶。分配n阶页块的过程如下:
1、查看是否有空闲的n阶页块,如果有直接分配;否则,继续执行下一步;
2、查看是否存在空闲的(n+1)阶页块,如果有,把(n+1)阶页块分裂为两个n阶页块,一个插入空闲n阶页块链表,另一个分配出去;否则继续执行下一步。
3、查看是否存在空闲的(n+2)阶页块,如果有把(n+2)阶页块分裂为两个(n+1)阶页块,一个插入空闲(n+1)阶页块链表,另一个分裂为两个n阶页块,一个插入空间(n阶页块链表,另一个分配出去;如果没有,继续查看更高阶是否存在空闲页块。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mx6C7RJy-1639925093037)(C:\Users\wangzhen\AppData\Roaming\Typora\typora-user-images\image-20211219152953787.png)]

上图的buddy分配和回收可以简单叙述成:

  • 需要分配内存时如果没有合适的内存块,会对半切分内存块直到分离出合适大小的内存块为止,最后再将其返回。

  • 需要释放内存时,会寻找相邻的块,如果其已经释放了,就将这俩合并,再递归这个过程,直到无法再合并为止

b. 数据结构

分区的伙伴分配器专注于某个内存节点的某个区域。内存区域的结构体成员free_area用来维护空闲页块,数组下标对应页块的阶数。 系统内存中的每个物理内存页(页帧),都对应于一个struct page实例, 每个内存域都关联了一个struct zone的实例,其中保存了用于管理伙伴数据的主要数数组,内核源码结构

/include/linux/mmzone.h
struct zone
{
     /* free areas of different sizes */
    struct free_area        free_area[MAX_ORDER];
};
......
/* Free memory management - zoned buddy allocator.  */
#ifndef CONFIG_FORCE_MAX_ZONEORDER
#define MAX_ORDER 11
#else
#define MAX_ORDER CONFIG_FORCE_MAX_ZONEORDER
#endif
#define MAX_ORDER_NR_PAGES (1 << (MAX_ORDER - 1))

struct free_area是一个伙伴系统的辅助数据结构

struct free_area {
    struct list_head        free_list[MIGRATE_TYPES];//是用于连接空闲页的链表. 页链表包含大小相同的连续内存区
    unsigned long           nr_free;//指定了当前内存区中空闲页块的数目(对0阶内存区逐页计算,对1阶内存区计算2页的数目,对2阶内存区计算4页集合的数目,依次类推
};

伙伴系统的分配器维护空闲页面所组成的块, 这里每一块都是2的方幂个页面, 方幂的指数称为阶.内存块的长度是 2 0 2^0 20,order , 其中order的范围从0到 M A X O R D E R MAX_ORDER MAXORDER

zone->free_area[MAX_ORDER]数组中阶作为各个元素的索引, 用于指定对应链表中的连续内存区包含多少个页帧.

  • 数组中第0个元素的阶为0, 它的free_list链表域指向具有包含区为单页 ( 2 0 = 1 ) (2^0 = 1) (20=1)的内存页面链表
  • 数组中第1个元素的free_list域管理的内存区为两页 ( 2 1 = 2 ) (2^1 = 2) (21=2)
  • 第3个管理的内存区为4页, 依次类推.
  • 直到 2 M A X O R D E R − 1 2^{MAXORDER-1} 2MAXORDER1个页面大小的块

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A1eXP544-1639925093039)(C:\Users\wangzhen\AppData\Roaming\Typora\typora-user-images\image-20211219154425067.png)]

伙伴系统的明显缺点是:由于圆整到下一个 2 的幂,很可能造成分配段内的碎片。例如,33KB 的内存请求只能使用 64KB 段来满足。事实上,我们不能保证因内部碎片而浪费的单元一定少于 50%。

slab 分配器

在Linux中,伙伴分配器(buddy allocator)是以页为单位管理和分配内存。但在内核中的需求却以字节为单位(在内核中面临频繁的结构体内存分配问题)。假如我们需要动态申请一个内核结构体(占 20 字节),若仍然分配一页内存,这将严重浪费内存。那么该如何分配呢?slab 分配器专为小内存分配而生,slab分配器分配内存以字节为单位,基于伙伴分配器的大内存进一步细分成小内存分配。换句话说,slab 分配器仍然从 Buddy 分配器中申请内存,之后自己对申请来的内存细分管理。除了提供小内存外,slab 分配器的第二个任务是维护常用对象的缓存。对于内核中使用的许多结构,初始化对象所需的时间可等于或超过为其分配空间的成本。当创建一个新的slab 时,许多对象将被打包到其中并使用构造函数(如果有)进行初始化。释放对象后,它会保持其初始化状态,这样可以快速分配对象。

每个 slab 由一个或多个物理连续的页面组成,每个 cache 由一个或多个 slab 组成,每个内核数据结构都有一个 cache。例如,用于表示进程描述符、文件对象、信号量等的数据结构都有各自单独的 cache。每个 cache 含有内核数据结构的对象实例(称为 object)。例如,信号量 cache 有信号量对象,进程描述符 cache 有进程描述符对象,等等。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GvYeebuf-1639925093041)(C:\Users\wangzhen\AppData\Roaming\Typora\typora-user-images\image-20211219211351086.png)]

图显示了 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 可以处于三种可能状态之一:

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

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

slab 分配器提供两个主要优点:

  1. 没有因碎片而引起内存浪费。碎片不是问题,因为每个内核数据结构都有关联的 cache,每个 cache 都由一个或多个 slab 组成,而 slab 按所表示对象的大小来分块。因此,当内核请求对象内存时,slab 分配器可以返回刚好表示对象的所需内存。
  2. 可以快速满足内存请求。因此,当对象频繁地被分配和释放时,如来自内核请求的情况,slab 分配方案在管理内存时特别有效。分配和释放内存的动作可能是一个耗时过程。然而,由于对象已预先创建,因此可以从 cache 中快速分配。再者,当内核用完对象并释放它时,它被标记为空闲并返回到 cache,从而立即可用于后续的内核请求。

通过命令sudo cat /proc/slabinfo可查看系统当前 slab 使用情况。

slab 分配器的实现源码:

  • include/linux/slab_def.h
  • include/linux/slab.h
  • mm/slab.c

用户API

与 libc 提供的内存申请 API (malloc 和 free )类似,Slab 分配器提供的 API 为 kmalloc()kfree()kmalloc()函数定义在include/linux/slab.h文件中,接收两个参数,并调用__kmalloc()函数。最终分配内存的实现是__do_kmalloc()函数。

分配内存:void * kmalloc (size_t size,gfp_t flags);
重新分配内存:void *krealloc(const void *p, size_t new_size, gfp_t flags)
释放内存:void kfree ( const void * objp);

  • size:申请的内存大小
  • flags:参数使用 GFP_XXX来指定分配内存的具体内存域,例如 GFP_DMA 指定分配适合于DMA的内存区.
//include/linux/slab.h
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
......
	return __kmalloc(size, flags);
}
// in mm/slab.c
void *__kmalloc(size_t size, gfp_t flags)
{
    return __do_kmalloc(size, flags, _RET_IP_);
}
static __always_inline void *__do_kmalloc(size_t size, gfp_t flags,
                      unsigned long caller)
{
    ...
}
/*
 * Common kmalloc functions provided by all allocators
 */
void * __must_check __krealloc(const void *, size_t, gfp_t);
void * __must_check krealloc(const void *, size_t, gfp_t);
void kfree(const void *);
void kzfree(const void *);
size_t ksize(const void *);

创建内存缓存:struct kmem_cache *kmem_cache_create(const char , size_t,
size_t,unsigned long, void (
)(void *));
指定的内存缓存分配对象:void *kmem_cache_alloc(struct kmem_cache *, gfp_t);
释放对象:void kmem_cache_free(struct kmem_cache *, void *);
销毁内存缓存:void kmem_cache_destroy(struct kmem_cache *);

struct kmem_cache *kmem_cache_create(const char *name, size_t size, size_t align,
		  unsigned long flags, void (*ctor)(void *))
{
	struct kmem_cache *s = NULL;
	const char *cache_name;
	int err;

数据结构

• 每个内存缓存对应一个kmem_cache实例。
• 每个内存节点对应一个kmem_cache_node实例。
• kmem_cache实例的成员cpu_slab指向array_cache实例,每个处理器对应一个array_cache实例,称为数组缓存,用来缓存刚刚释放的对象,分配时首先从当前处理器的数据缓存分配,避免每次都要从slab分配,减少链表操作的锁操作,提高分配的速度。

由于slab的缓存特性,slab 分配器从 buddy 分配器中获取的物理内存称为 内存缓存使用结构体struct kmem_cache(定义在include/linux/slab_def.h文件中)描述。 一个高速缓存中可以含有多个kmem_cache对应的高速缓存,就拿L1高速缓存来举例,一个L1高速缓存对应一个kmem_cache链表,这个链表中的任何一个kmem_cache类型的结构
体均描述一个高速缓存,而这些高速缓存在L1 cache中各自占用着不同的区域。

SLAB分配器由可变数量的缓存组成,这些缓存由称为“缓存链”的双向循环链表链接在一起(如下图中的 kmem_cache 链表)。 在slab分配器的上下文中,缓存是特定类型的多个对象的管理器,例如使用cat /proc/slabinfo命令输出的mm_structfs_cache缓存,其名字保存在kmem_cache->name中(Linux 支持单个最大的 slab 缓存大小为32MB )。kmem_cache 中所有对象的大小是相同的(object_size),并且此 kmem_cache 中所有SLAB的大小也是相同的(gfpordernum)。

每个缓存节点在内存中维护称为slab的连续页块,这些页面被切成小块,用于缓存数据结构和对象。 kmem_cachekmem_cache_node 成员记录了该kmem_cache 下的所有 slabs 列表。形成的结构如下图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3wb1bPZt-1639925093043)(C:\Users\wangzhen\AppData\Roaming\Typora\typora-user-images\image-20211219222747959.png)]

struct kmem_cache {
    //为了提高效率,每个CPU都有一个slab空闲对象缓存
	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 */

	unsigned int flags;		/* constant flags */
	unsigned int num;	//每个slab的obj对象个数

/* 3) cache_grow/shrink */
	/* order of pgs per slab (2^n) */
	unsigned int gfporder; //每个slab中连续页框的数量

	/* force GFP flags, e.g. GFP_DMA */
	gfp_t allocflags;

	size_t colour;			/* cache colouring range */
	unsigned int colour_off;	/* colour offset */
	struct kmem_cache *freelist_cache;
	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, the following two
	 * variables contain the offset to the user object and its size.
	 */
	int obj_offset;
#endif /* CONFIG_DEBUG_SLAB */
#ifdef CONFIG_MEMCG_KMEM
	struct memcg_cache_params memcg_params;
#endif

	struct kmem_cache_node *node[MAX_NUMNODES]; //内存节点实例个数
};
struct array_cache {
	unsigned int avail; //本地高速缓存中可用对象的个数,也是空闲数组位置的索引
	unsigned int limit; //本地高速缓存大小
	unsigned int batchcount; //本地高速缓存天长或者清空时是用到的对象个数
	unsigned int touched;//如果本地高速缓存最近被使用过,设置成1
	void *entry[];	/* 对象的地址
			 * Must have this definition in here for the proper
			 * alignment of array_cache. Also simplifies accessing
			 * the entries.
			 *
			 * Entries should not be directly dereferenced as
			 * entries belonging to slabs marked pfmemalloc will
			 * have the lower bits set SLAB_OBJ_PFMEMALLOC
			 */
};
/*
 * The slab lists for all objects.
 */
struct kmem_cache_node {
	spinlock_t list_lock;

#ifdef CONFIG_SLAB
	struct list_head slabs_partial;	//部分分配的slab
	struct list_head slabs_full; //已经完全分配的 slab
	struct list_head slabs_free; //空slab,或者没有对象被分配
	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

};

以上3个链表保存的是slab 描述符,Linux kernel 使用 struct page 来描述一个slab。单个slab可以在slab链表之间移动,例如如果一个半满slab被分配了对象后变满了,就要从 slabs_partial 中被删除,同时插入到 slabs_full 中去。

page 结构体关于 slab 的部分。struct page定义在include/linux/mm_types.h文件中,与slab相关的结构体成员如下所示:

struct page {
	/* First double word block */
	unsigned long flags;		/* Atomic flags, some possibly
					 * updated asynchronously */
	union {
		struct address_space *mapping;	/* If low bit clear, points to
						 * inode address_space, or NULL.
						 * If page mapped as anonymous
						 * memory, low bit is set, and
						 * it points to anon_vma object:
						 * see PAGE_MAPPING_ANON below.
						 */
		void *s_mem;			/* slab first object */
	};

	/* Second double word */
	struct {
		union {
			pgoff_t index;		/* Our offset within mapping. */
			void *freelist;		/* sl[aou]b first free object */
		};

void *s_mem: 指向该页框中第一个object 的地址 。

void *freelist: 用于指向页框中空闲对象链表。空闲对象链表包含页框中每个空闲对象的索引。

内存缓存为每个处理器创建一个数组缓存(结构体array_cache)。释放对象时,把对象存放到当前处理器对应的数组缓存中;分配对象的时候,先从当前处理器的数组缓存分配对象,采用后进先出(LIFO)原则,可以提高性能。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-79yrXxVw-1639925093044)(C:\Users\wangzhen\AppData\Roaming\Typora\typora-user-images\image-20211219224337169.png)]

对于所有对象空间的slab,没有立即释放,而是放在空闲slab链表中。只有内存节点上空闲对象的数量超过限制,才开始回收空闲slab,直到空闲对象的数量小于或等于限制。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5QMOMxtM-1639925093046)(C:\Users\wangzhen\AppData\Roaming\Typora\typora-user-images\image-20211219224406298.png)]

参考:
Linux内核内存管理算法Buddy和Slab
Linux 内核 | 内存管理——slab 分配器
slab分配器–Linux内存管理(二十二)
linux内存源码分析 - SLAB分配器概述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值