Linux kmalloc原理

1 概述

本文基于slab cache原理基础上,从内核源码角度探讨kmalloc的原理,包括kmalloc内存池初始化、kmalloc内存申请和释放过程,关于slab cache原理,参考笔者之前总结过的文章

nullhttps://blog.csdn.net/tang_vincent/article/details/155630942?spm=1001.2014.3001.5502

2 kmalloc介绍

kmalloc是基于slab缓存池创建的内存池,其在内核物理内存管理关系如下图。它是为解决内核通用小内存的频繁分配释放需求而专门创建的内存池,内核在启动阶段建立了不同大小的slab cache缓存池,根据用户申请大小不同,使用不同的内存池进行分配。

3 kmalloc在内核的初始化

3.1 kmalloc的组织形式

内核为kmalloc内存池声明了全局的内存组织信息kmalloc_info

复制代码
mm\slab_common.c

#define INIT_KMALLOC_INFO(__size, __short_size)			\
{								\
	.name[KMALLOC_NORMAL]  = "kmalloc-" #__short_size,	\
	.name[KMALLOC_RECLAIM] = "kmalloc-rcl-" #__short_size,	\
	KMALLOC_CGROUP_NAME(__short_size)			\
	KMALLOC_DMA_NAME(__short_size)				\
	.size = __size,						\
}

/*
 * kmalloc_info[] is to make slub_debug=,kmalloc-xx option work at boot time.
 * kmalloc_index() supports up to 2^25=32MB, so the final entry of the table is
 * kmalloc-32M.
 */
const struct kmalloc_info_struct kmalloc_info[] __initconst = {
	INIT_KMALLOC_INFO(0, 0),
	INIT_KMALLOC_INFO(96, 96),
	INIT_KMALLOC_INFO(192, 192),
	INIT_KMALLOC_INFO(8, 8),
	INIT_KMALLOC_INFO(16, 16),
	INIT_KMALLOC_INFO(32, 32),
	INIT_KMALLOC_INFO(64, 64),
	INIT_KMALLOC_INFO(128, 128),
	INIT_KMALLOC_INFO(256, 256),
	INIT_KMALLOC_INFO(512, 512),
	INIT_KMALLOC_INFO(1024, 1k),
	INIT_KMALLOC_INFO(2048, 2k),
	INIT_KMALLOC_INFO(4096, 4k),
	INIT_KMALLOC_INFO(8192, 8k),
	INIT_KMALLOC_INFO(16384, 16k),
	INIT_KMALLOC_INFO(32768, 32k),
	INIT_KMALLOC_INFO(65536, 64k),
	INIT_KMALLOC_INFO(131072, 128k),
	INIT_KMALLOC_INFO(262144, 256k),
	INIT_KMALLOC_INFO(524288, 512k),
	INIT_KMALLOC_INFO(1048576, 1M),
	INIT_KMALLOC_INFO(2097152, 2M),
	INIT_KMALLOC_INFO(4194304, 4M),
	INIT_KMALLOC_INFO(8388608, 8M),
	INIT_KMALLOC_INFO(16777216, 16M),
	INIT_KMALLOC_INFO(33554432, 32M)
};



mm\slab.h

extern const struct kmalloc_info_struct {
	const char *name[NR_KMALLOC_TYPES];
	unsigned int size;
} kmalloc_info[];

kmalloc_info从index=3开始,每一个 index 都表示其对应的 kmalloc_infoindex 指向的通用 slab cache 尺寸,即大小为2^index字节,例如kmalloc_info3 对应的通用 slab cache 中所管理的内存块尺寸为 8 字节。这里的 index = 1 和 index = 2 是个例外,内核单独支持了 kmalloc-96 和 kmalloc-192 这两个通用 slab cache。它们分别管理了 96 字节大小和 192 字节大小的通用内存块。

内核通过一个全局的size_index素组来根据当前申请的大小选取合适尺寸的内存块提供申请

复制代码
mm\slab_common.c

static u8 size_index[24] __ro_after_init = {
	3,	/* 8 */
	4,	/* 16 */
	5,	/* 24 */
	5,	/* 32 */
	6,	/* 40 */
	6,	/* 48 */
	6,	/* 56 */
	6,	/* 64 */
	1,	/* 72 */
	1,	/* 80 */
	1,	/* 88 */
	1,	/* 96 */
	7,	/* 104 */
	7,	/* 112 */
	7,	/* 120 */
	7,	/* 128 */
	2,	/* 136 */
	2,	/* 144 */
	2,	/* 152 */
	2,	/* 160 */
	2,	/* 168 */
	2,	/* 176 */
	2,	/* 184 */
	2	/* 192 */
};

例如,当用户申请内存小于8为7时,此时通过计算(大小-1/8,后续提到的size_index_elem即是根据大小计算偏移)得偏移是0,size_index0 存储的信息表示,如果内核申请的内存块低于 8 字节时,那么 kmalloc 将会到 kmalloc_info3 所指定的通用 slab cache ------ kmalloc-8 中分配一个 8 字节大小的内存块,其他大小同理。

上述所有的内存信息确定后,调用slab cache缓存池创建接口,所有的缓存对象被维护在一个全局变量kmalloc_caches中

复制代码
struct kmem_cache *
kmalloc_caches[NR_KMALLOC_TYPES][KMALLOC_SHIFT_HIGH + 1] __ro_after_init =
{ /* initialization for https://bugs.llvm.org/show_bug.cgi?id=42570 */ };
EXPORT_SYMBOL(kmalloc_caches);

3.2 kmalloc内存池初始化

kmalloc的初始化在内核启动过程调用关系如下

setup_kmalloc_cache_index_table主要初始化了size_index

复制代码
void __init setup_kmalloc_cache_index_table(void)
{
	unsigned int i;

	BUILD_BUG_ON(KMALLOC_MIN_SIZE > 256 ||
		(KMALLOC_MIN_SIZE & (KMALLOC_MIN_SIZE - 1)));

	for (i = 8; i < KMALLOC_MIN_SIZE; i += 8) {
		unsigned int elem = size_index_elem(i);

		if (elem >= ARRAY_SIZE(size_index))
			break;
		size_index[elem] = KMALLOC_SHIFT_LOW;
	}

	if (KMALLOC_MIN_SIZE >= 64) {
		/*
		 * The 96 byte size cache is not used if the alignment
		 * is 64 byte.
		 */
		for (i = 64 + 8; i <= 96; i += 8)
			size_index[size_index_elem(i)] = 7;

	}

	if (KMALLOC_MIN_SIZE >= 128) {
		/*
		 * The 192 byte sized cache is not used if the alignment
		 * is 128 byte. Redirect kmalloc to use the 256 byte cache
		 * instead.
		 */
		for (i = 128 + 8; i <= 192; i += 8)
			size_index[size_index_elem(i)] = 8;
	}
}

不同的CPU架构定义出来的size_index结果可能不一样,例如arm64中由于定了ARCH_DMA_MINALIGN,此时KMALLOC_MIN_SIZE 的值变成128,KMALLOC_SHIFT_LOW为7,即kmalloc提供最小的内存申请大小是128。

复制代码
#if defined(ARCH_DMA_MINALIGN) && ARCH_DMA_MINALIGN > 8
#define ARCH_KMALLOC_MINALIGN ARCH_DMA_MINALIGN
#define KMALLOC_MIN_SIZE ARCH_DMA_MINALIGN
#define KMALLOC_SHIFT_LOW ilog2(ARCH_DMA_MINALIGN)
#else
#define ARCH_KMALLOC_MINALIGN __alignof__(unsigned long long)
#endif

arch\arm64\include\asm\cache.h
#define ARCH_DMA_MINALIGN	(128)

kmalloc内存池内存可能来自不同区域例如DMA区、NORMAL或者可回收区域,根据区域不同malloc在内核定义了内存来源类型:

复制代码
enum kmalloc_cache_type {
	KMALLOC_NORMAL = 0,
#ifndef CONFIG_ZONE_DMA
	KMALLOC_DMA = KMALLOC_NORMAL,
#endif
#ifndef CONFIG_MEMCG_KMEM
	KMALLOC_CGROUP = KMALLOC_NORMAL,
#else
	KMALLOC_CGROUP,
#endif
	KMALLOC_RECLAIM,
#ifdef CONFIG_ZONE_DMA
	KMALLOC_DMA,
#endif
	NR_KMALLOC_TYPES
};

内核会为每个区域申请所有kmalloc_info定义的大小,总结如下关系图:

create_kmalloc_caches函数的主要任务就是创建和初始化用来管理所有kmalloc缓存池slab cache对象的kmalloc_caches这个二维数组

复制代码
void __init create_kmalloc_caches(slab_flags_t flags)
{
	int i;
	enum kmalloc_cache_type type;

	/*
	 * Including KMALLOC_CGROUP if CONFIG_MEMCG_KMEM defined
	 */
	 // 初始化二维数组 kmalloc_caches,为每一个 kmalloc_cache_type 类型创建内存块尺寸从 KMALLOC_SHIFT_LOW 到 KMALLOC_SHIFT_HIGH 大小的 kmalloc 内存池
	for (type = KMALLOC_NORMAL; type <= KMALLOC_RECLAIM; type++) {
		// 这里会从 128B(2^KMALLOC_SHIFT_LOW 7) 尺寸的内存池开始创建,一直到创建完 8K(2^KMALLOC_SHIFT_HIGH 13) 尺寸的内存池
		for (i = KMALLOC_SHIFT_LOW; i <= KMALLOC_SHIFT_HIGH; i++) {
			if (!kmalloc_caches[type][i])
				new_kmalloc_cache(i, type, flags);// 创建对应尺寸的 kmalloc 内存池,其中内存块大小为 2^i 字节

			/*
			 * Caches that are not of the two-to-the-power-of size.
			 * These have to be created immediately after the
			 * earlier power of two caches
			 */
			//arm64下KMALLOC_MIN_SIZE是128.这里在arm64下实际不会创建

			// 创建 kmalloc-96 内存池管理 96B 尺寸的内存块
            // 专门特意创建一个 96字节尺寸的内存池的目的是为了,应对 64B 到 128B 之间的内存分配需求,要不然超过 64B 就分配一个 128B 的内存块有点浪费
			if (KMALLOC_MIN_SIZE <= 32 && i == 6 &&
					!kmalloc_caches[type][1])
				new_kmalloc_cache(1, type, flags);
            // 创建 kmalloc-192 内存池管理 192B 尺寸的内存块
            // 这里专门创建一个 192字节尺寸的内存池.是为了分配 128B 到 192B 之间的内存分配需求
            // 要不然超过 128B 直接分配一个 256B 的内存块太浪费了
			if (KMALLOC_MIN_SIZE <= 64 && i == 7 &&
					!kmalloc_caches[type][2])
				new_kmalloc_cache(2, type, flags);
		}
	}
// 当 kmalloc 体系全部创建完毕之后,slab 体系的状态就变为 up 状态了
	/* Kmalloc array is now usable */
	slab_state = UP;

#ifdef CONFIG_ZONE_DMA
 // 如果配置了 DMA 内存区域,则需要为该区域也创建对应尺寸的内存池
	for (i = 0; i <= KMALLOC_SHIFT_HIGH; i++) {
		struct kmem_cache *s = kmalloc_caches[KMALLOC_NORMAL][i];

		if (s) {
			kmalloc_caches[KMALLOC_DMA][i] = create_kmalloc_cache(
				kmalloc_info[i].name[KMALLOC_DMA],
				kmalloc_info[i].size,
				SLAB_CACHE_DMA | flags, 0,
				kmalloc_info[i].size);
		}
	}
#endif
}
#endif /* !CONFIG_SLOB */

函数从KMALLOC_NORMAL到KMALLOC_RECLAIM循环每个区域,为每个区域从KMALLOC_SHIFT_LOW(前面得知为7)到KMALLOC_SHIFT_HIGH(13)创建对应大小区域的slab cache,函数最后还检查了CONFIG_ZONE_DMA,同样操作创建slab cache对象到kmalloc_caches中。

创建slab cache对象调用的是slab cache接口,最终会根据偏移与当前内存区域放置到kmalloc_caches中。

复制代码
static void __init
new_kmalloc_cache(int idx, enum kmalloc_cache_type type, slab_flags_t flags)
{
	//slab_flags_t flags 指定创建 slab cache 时的标志位,这里主要用来指定 slab cache 中的内存来源于哪个内存区域。
	if (type == KMALLOC_RECLAIM) {
		flags |= SLAB_RECLAIM_ACCOUNT;
	} else if (IS_ENABLED(CONFIG_MEMCG_KMEM) && (type == KMALLOC_CGROUP)) {
		if (cgroup_memory_nokmem) {
			kmalloc_caches[type][idx] = kmalloc_caches[KMALLOC_NORMAL][idx];
			return;
		}
		flags |= SLAB_ACCOUNT;
	}
	// // 底层调用 __kmem_cache_create 创建 kmalloc_info[idx].size 尺寸的 slab cache
	kmalloc_caches[type][idx] = create_kmalloc_cache(
					kmalloc_info[idx].name[type],
					kmalloc_info[idx].size, flags, 0,
					kmalloc_info[idx].size);

	/*
	 * If CONFIG_MEMCG_KMEM is enabled, disable cache merging for
	 * KMALLOC_NORMAL caches.
	 */
	if (IS_ENABLED(CONFIG_MEMCG_KMEM) && (type == KMALLOC_NORMAL))
		kmalloc_caches[type][idx]->refcount = -1;
}

3.3 内存申请kmalloc调用

kmalloc调用__kmalloc最重要的是根据申请的size,计算出对应的最合适的内存区域找到slab cache对象,根据slab cache对象申请内存。

复制代码
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
	...
	return __kmalloc(size, flags);
}

void *__kmalloc(size_t size, gfp_t flags)
{
	struct kmem_cache *s;
	void *ret;
   // KMALLOC_MAX_CACHE_SIZE 规定 kmalloc 内存池所能管理的内存块最大尺寸,在 slub 实现中是 2页 大小
    // 如果使用 kmalloc 申请超过 2页 大小的内存,则直接走伙伴系统
	if (unlikely(size > KMALLOC_MAX_CACHE_SIZE))
		return kmalloc_large(size, flags);// 底层调用 alloc_pages 向伙伴系统申请超过 2页 的内存块
// 根据申请内存块的尺寸 size,在 kmalloc_caches 缓存中选择合适尺寸的内存池
	s = kmalloc_slab(size, flags);

	if (unlikely(ZERO_OR_NULL_PTR(s)))
		return s;
// 向选取的 slab cache 申请内存块
	ret = slab_alloc(s, flags, _RET_IP_, size);

	trace_kmalloc(_RET_IP_, ret, size, s->size, flags);

	ret = kasan_kmalloc(s, ret, size, flags);

	return ret;
}
EXPORT_SYMBOL(__kmalloc);

需要注意的是,当申请的内存大于KMALLOC_MAX_CACHE_SIZE 2页即8k时,内核会直接从伙伴系统中申请,kmalloc_large最终会调用伙伴系统接口申请内存。否则则调用slab的接口slab_alloc进行内存分配。

关于计算合适的偏移得到合适内存池slab cache实现

复制代码
struct kmem_cache *kmalloc_slab(size_t size, gfp_t flags)
{
	unsigned int index;
// 如果申请的内存块 size 在 192 字节以下,则通过 size_index 数组定位 kmalloc_caches 缓存索引
    // 从而获取到最佳合适尺寸的内存池 slab cache
	if (size <= 192) {
		if (!size)
			return ZERO_SIZE_PTR;
// 根据申请的内存块 size,定义 size_index 数组索引,从而获取 kmalloc_caches 缓存的 index
		index = size_index[size_index_elem(size)];
	} else {
		if (WARN_ON_ONCE(size > KMALLOC_MAX_CACHE_SIZE))
			return NULL;
 // 如果申请的内存块 size 超过 192 字节,则通过 fls 定位 kmalloc_caches 缓存的 index
         // fls 可以获取参数的最高有效 bit 的位数,比如 fls(0)=0,fls(1)=1,fls(4) = 3
		index = fls(size - 1);
	}
// 根据 kmalloc_type 以及 index 获取最佳尺寸的内存池 slab cache
	return kmalloc_caches[kmalloc_type(flags)][index];
}

3.4 kfree释放内存

释放内存较为简单,先判断当前释放的内存是否在slab管理范围,如果不在,则调用伙伴系统直接释放,如果属于slab管理则调用slab_free slab的释放接口进行释放。

复制代码
void kfree(const void *x)
{
	struct page *page;
	// x 为要释放的内存块的虚拟内存地址
	void *object = (void *)x;

	trace_kfree(_RET_IP_, x);

	if (unlikely(ZERO_OR_NULL_PTR(x)))
		return;
// 通过虚拟内存地址找到内存块所在的 page
	page = virt_to_head_page(x);
	// 如果 page 不在 slab cache 的管理体系中,则直接释放回伙伴系统
	if (unlikely(!PageSlab(page))) {
		free_nonslab_page(page, object);
		return;
	}
	// 将内存块释放回其所在的 slub 中
	slab_free(page->slab_cache, page, object, NULL, 1, _RET_IP_);
}
EXPORT_SYMBOL(kfree);

4 附录

https://heapdump.cn/article/6091586

相关推荐
夜听莺儿鸣2 小时前
502-003_Linux 中断与异常(一):从Linux角度理解中断与异常
linux·中断与异常·linux中断
杨云龙UP3 小时前
TDengine 3.4.2.8 Community 三节点三副本生产集群部署实战(DNode/MNode/taosAdapter/Explorer)
大数据·linux·运维·数据库·tdengine·时序库
向成科技4 小时前
XC3576H工控主板|深度适配Ubuntu 26.04 LTS,释放边缘AI与工业开发新潜能
linux·人工智能·ubuntu·机器人·硬件·主板·边缘ai
Fcy6484 小时前
Linux下 进程间关系与守护进程
linux·运维·服务器·守护进程
码农小韩5 小时前
Linux驱动理论(二)——Linux字符设备驱动
linux·嵌入式软件开发·linux操作系统·linux应用开发·linux驱动理论
well06125 小时前
Linux粘滞位与Makefile机制深度解析
linux·运维·服务器
再写一行代码就下班5 小时前
linux sh脚本在windows修改导致无法使用解决方式
java·linux·centos
额额额对了5 小时前
SPI通信
linux·c语言·汇编·单片机·嵌入式硬件·arm
子木HAPPY阳VIP6 小时前
Ubuntu 关闭防火墙操作步骤
linux·运维·ubuntu
王振超wzc6 小时前
嵌入式开发环境搭建--WM软件安装,Ubuntu操作系统安装
linux·运维·ubuntu