AI智能摘要
深入解析Linux内核中kmalloc的内存池体系,详解其在高效分配小至中等连续物理内存块中的核心作用。文章重点说明kmalloc通过预先创建多种尺寸的slab内存池,有效应对频繁的通用及专属内存分配需求,并结合源码分析创建流程、类型划分及对应场景,帮助开发者理解内核内存分配机制的高效性与灵活性。
此摘要由AI分析文章内容生成,仅供参考。

相信大家能够看到这篇文章,应该已经对kmalloc有一定的了解。作为一名Linux驱动开发工程师,kmalloc的使用随处可见。kmalloc 是 Linux 内核中最常用的连续物理内存分配器,适用于分配小到中等大小的内存块。

内核已经有了buddy分配器来分配页,而我们在实际开发中申请的较多的基本都是小内存,如果都使用buddy来进行分配,那么每次申请都会申请一页,这就会造成很大的浪费!所以 slab 内存池是专门为了应对内核中关于小内存分配需求而应运而生的,内核会为每一个核心数据结构创建一个专属的 slab 内存池,专门用于内核核心对象频繁分配和释放的场景。比如,内核中的 task_struct 结构,mm_struct 结构,struct page 结构,struct file 结构,socket 结构等等,在内核中都有一个属于自己的专属 slab 内存池。

内核中除了上述这些专有内存的分配需求之外,其实更多的是通用小内存的分配需求,比如说,内核会申请一些 8 字节,16 字节,32 字节等特定尺寸的通用内存块,内核并不会限制这些通用内存块的用途,可以拿它们来存储任何信息

内核为了应对这些通用小内存的频繁分配释放需求,于是本文的主题 —— kmalloc 内存池体系就应用而生了,在内核启动初始化的时候,通过 kmem_cache_create 接口函数预先创建多个特定尺寸的 slab cache 出来,用以应对不同尺寸的通用内存块的申请。

本文将从以下几点来全面详解kmalloc:

  1. kmalloc的slab内存池是如何创建的?

  2. 使用kmalloc申请内存时是如何选择合适的内存池的?

  3. kmalloc内存池如何进行分配与回收?

kmalloc的内存池的创建

笔者现在手上有一台手机,通cat /proc/slabinfo |grep kmalloc- 可以找到所有的kmalloc内存池

我们可以看到该手机的kmalloc内存池预分配了诸kmalloc-64 kmalloc-128 等等,那这些是如何创建的呢?在上一篇文章 [linux内存管理] 第031篇 内核启动早期的slab分配器的自举 中我们提到了在内核启动前期会调kmem_cache_init 创建了系统第一个slab cache,而同样在这个函数中,还会调用一个函create_kmalloc_caches

	/* Now we can use the kmem_cache to allocate kmalloc slabs */
	//根据KMALLOC_MIN_SIZE,更新kmalloc时要用到的size_index table
	setup_kmalloc_cache_index_table();
	
    //初始化kmem_caches,里面包含各种kmalloc-x
	create_kmalloc_caches(0);

PS: setup_kmalloc_cache_index_table函数后面在聊

create_kmalloc_caches

void __init create_kmalloc_caches(slab_flags_t flags)
{
	int i;
	enum kmalloc_cache_type type;

	//初始化kmalloc_cache_type为KMALLOC_NORMAL和KMALLOC_RECLAIM的kmalloc cache
	//注意,第二层for循环,从下标7~13,即每个kmalloc_cache_type各创建7个kmalloc cache,且存放在数组后7个元素中
	for (type = KMALLOC_NORMAL; type <= KMALLOC_RECLAIM; type++) {
		for (i = KMALLOC_SHIFT_LOW; i <= KMALLOC_SHIFT_HIGH; i++) {
			if (!kmalloc_caches[type][i])
				new_kmalloc_cache(i, type, flags);  // 申请新的kmem_cache

			// 对于更小的kmalloc size,单独创建两个kmem_cache保存在每个type的[1]和[2]处
			if (KMALLOC_MIN_SIZE <= 32 && i == 6 &&
					!kmalloc_caches[type][1])
				new_kmalloc_cache(1, type, flags);
			if (KMALLOC_MIN_SIZE <= 64 && i == 7 &&
					!kmalloc_caches[type][2])
				new_kmalloc_cache(2, type, flags);
		}
	}

	//初始化完kamlloc_caches,此时slab_state从PARTIAL变为UP,slab缓存基本功能算完成
	slab_state = UP;

#ifdef CONFIG_ZONE_DMA
    // 如果定义了CONFIG_ZONE_DMA,则会初始化kmalloc_cache_type为KMALLOC_DMA的kmalloc cache
    // DMA 取名为dma-kmalloc-x,x为size大小组成
	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
}

kmalloc_cache_type

create_kmalloc_caches 的第一个循环中,会根据kmalloc_cache_type来new_kmalloc_cache不同的kmem_cache。

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

我司这个手机没有定义CONFIG_ZONE_DMA,定义了CONFIG_MEMCG_KMEM,所以这个type就是如下的情况

enum kmalloc_cache_type {
	KMALLOC_NORMAL = 0,
	KMALLOC_DMA = KMALLOC_NORMAL = 0,
	KMALLOC_CGROUP,
	KMALLOC_RECLAIM,
	NR_KMALLOC_TYPES
};

所以一维只有KMALLOC_NORMAL、KMALLOC_CGROUP、KMALLOC_RECLAIM

KMALLOC_SHIFT_LOW和KMALLOC_SHIFT_HIGH

#ifdef CONFIG_SLUB
/*
 * SLUB directly allocates requests fitting in to an order-1 page
 * (PAGE_SIZE*2).  Larger requests are passed to the page allocator.
 */
#define KMALLOC_SHIFT_HIGH	(PAGE_SHIFT + 1)
#define KMALLOC_SHIFT_MAX	(MAX_ORDER + PAGE_SHIFT - 1)
#ifndef KMALLOC_SHIFT_LOW
#define KMALLOC_SHIFT_LOW	3
#endif
#endif

#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

#define ARCH_DMA_MINALIGN	(64)    # 本文演示的代码中的定义

KMALLOC_SHIFT_HIGH : 13

KMALLOC_SHIFT_MAX : 22

KMALLOC_SHIFT_LOW : 6

所以这个内层循环,是从6开始,到13为止,创建这8个kmalloc cache

PS: 请注意创建kmalloc caches是根据各自项目中的源码,并不是一定与本文演示的案例一样!

PS: 请注意创建kmalloc caches是根据各自项目中的源码,并不是一定与本文演示的案例一样!

PS: 请注意创建kmalloc caches是根据各自项目中的源码,并不是一定与本文演示的案例一样!

new_kmalloc_cache

static void __init
new_kmalloc_cache(int idx, enum kmalloc_cache_type type, slab_flags_t flags)
{
	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;
	}

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

笔者这台手机的cmdline中设置了 cgroup.memory=nokmem ,此时cgroup_memory_nokmem为true,所以当type为KMALLOC_CGROUP时创建的kmalloc_caches[KMALLOC_CGROUP]=kmalloc_caches[KMALLOC_NORMAL]

所以kmalloc_caches[0]和kmalloc_caches[1]是一样的内容都是KMALLOC_NORMAL类型的,而kmalloc_caches[2]是KMALLOC_RECLAIM

通过trace32查询这个变量也是和我们的推导是一致的!

所以首先每个类型都从idx=6到idx=13的创建,传入的是kmalloc_info[idx]的参数

#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^21=2MB, so the final entry of the table is
 * kmalloc-2M.
 */
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)
};

kmalloc_info里就定义了kmalloc缓存池的名字以及size,idx=7到idx=13就是以下

	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),

而这和我们在trace32中看到的也是一致的

kmalloc_info[index] 指向的通用 slab cache 尺寸,也就是说 kmalloc 内存池体系中的每个通用 slab cache 中内存块的尺寸由其所在的 kmalloc_info[] 数组 index 决定,对应内存块大小为:2^index字节

但是这里的 index = 1 和 index = 2 是个例外,内核单独支持了 kmalloc-96 和 kmalloc-192 这两个通用 slab cache。它们分别管理了 96 字节大小和 192 字节大小的通用内存块。这些内存块的大小都不是 2 的次幂。

那么内核为什么会单独支持这两个尺寸而不是其他尺寸的通用 slab cache 呢?

因为在内核中,对于内存块的申请需求大部分情况下都在 96 字节或者 192 字节附近,如果内核不单独支持这两个尺寸的通用 slab cache。那么当内核申请一个尺寸在 64 字节到 96 字节之间的内存块时,内核会直接从 kmalloc-128 中分配一个 128 字节大小的内存块,这样就导致了内存块内部碎片比较大,浪费宝贵的内存资源。

同理,当内核申请一个尺寸在 128 字节到 192 字节之间的内存块时,内核会直接从 kmalloc-256 中分配一个 256 字节大小的内存块。

当内核申请超过 256 字节的内存块时,一般都是会按照 2 的次幂来申请的,所以这里只需要单独支持 kmalloc-96 和 kmalloc-192 即可。

kmalloc函数申请内存

static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
	if (__builtin_constant_p(size)) {
#ifndef CONFIG_SLOB
		unsigned int index;
#endif
        //如果size 超过kmalloc caches最大值,则通过kmalloc_large()从buddy系统分配
		if (size > KMALLOC_MAX_CACHE_SIZE)
			return kmalloc_large(size, flags);
#ifndef CONFIG_SLOB
		index = kmalloc_index(size);  ///查找使用的哪个slab缓冲区

		if (!index)
			return ZERO_SIZE_PTR;
        //该函数与kmem_cache_alloc()几乎一样,无非就是多了一个kasan的tag设置
		return kmem_cache_alloc_trace(    ///从slab分配内存
				kmalloc_caches[kmalloc_type(flags)][index],
				flags, size);
#endif
	}
	return __kmalloc(size, flags);
}
  • static __always_inline:声明为静态内联函数,编译器会尝试内联展开

  • __alloc_size(1):GCC属性,表示第一个参数(size)是分配大小,用于编译时的边界检查

  • __builtin_constant_p(size) 是 GCC 内置函数,用于判断 size 是否在编译时是常量

// 编译时已知大小的示例
ptr = kmalloc(256, GFP_KERNEL);  // size=256 是编译时常量
ptr = kmalloc(sizeof(struct foo), GFP_KERNEL);  // sizeof 是编译时常量
ptr = kmalloc(1024, GFP_ATOMIC);  // 1024 是编译时常量

// 编译时未知大小的示例
ptr = kmalloc(user_input, GFP_KERNEL);  // user_input 是变量
ptr = kmalloc(calculate_size(), GFP_KERNEL);  // 函数返回值

逻辑比较简单,对于 slub分配器,有两种途径分配内存:

  • 当申请的size 为常数时:

    • 当申请size 超过 kmalloc caches 里最大值时,通过 kmalloc_large() 申请,走 buddy 系统

    • 申请size 不超过kmalloc caches最大值时,通过 kmem_cache_alloc_trace() 申请,走 kmalloc caches

  • 当申请的size为变量时,调用 __kmalloc() 申请

kmalloc_large

static __always_inline void *kmalloc_large(size_t size, gfp_t flags)
{
	// 获取size 对应的order
	unsigned int order = get_order(size);
	return kmalloc_order_trace(size, flags, order);
}

kmalloc_order_trace

// include/linux/slab.h

static __always_inline void *
kmalloc_order_trace(size_t size, gfp_t flags, unsigned int order)
{
	return kmalloc_order(size, flags, order);
}

其实最终都是通过 kmalloc_order() 进行分配,如果使能 CONFIG_TRACING ,会多一个 trace_kmalloc() 跟踪

// mm/slab_common.c

void *kmalloc_order_trace(size_t size, gfp_t flags, unsigned int order)
{
	void *ret = kmalloc_order(size, flags, order);
	trace_kmalloc(_RET_IP_, ret, size, PAGE_SIZE << order, flags);
	return ret;
}

kmalloc_order


mm/slab_common.c
 
void *kmalloc_order(size_t size, gfp_t flags, unsigned int order)
{
	void *ret = NULL;
	struct page *page;
 
	flags |= __GFP_COMP;
	page = alloc_pages(flags, order);
	if (likely(page)) {
		ret = page_address(page);
		mod_node_page_state(page_pgdat(page), NR_SLAB_UNRECLAIMABLE,
				    1 << order);
	}
	ret = kasan_kmalloc_large(ret, size, flags);
	/* As ret might get tagged, call kmemleak hook after KASAN. */
	kmemleak_alloc(ret, size, 1, flags);
	return ret;
}
EXPORT_SYMBOL(kmalloc_order);

注意:

因为是通过 kmalloc() 申请了超过 8K 大小的内存,只能选择从 buddy 系统直接申请,所以,会将这部分特殊的 slab 信息存入到 NR_SLAB_UNRECLAIMABLE 这一项中。可以通过节点 /proc/meminfo 、节点 /proc/zoneinfo 、节点 /proc/vmstat 直观地看到申请了多少这样的内存。当然,在 kfree() 的时候会将这些内存释放,到时这一项的数据也会相应地减少

kmalloc_index

#define kmalloc_index(s) __kmalloc_index(s, true)

static __always_inline unsigned int __kmalloc_index(size_t size,
						    bool size_is_constant)
{
	if (!size)
		return 0;

	if (size <= KMALLOC_MIN_SIZE)
		return KMALLOC_SHIFT_LOW;

	if (KMALLOC_MIN_SIZE <= 32 && size > 64 && size <= 96)
		return 1;
	if (KMALLOC_MIN_SIZE <= 64 && size > 128 && size <= 192)
		return 2;
	if (size <=          8) return 3;
	if (size <=         16) return 4;
	if (size <=         32) return 5;
	if (size <=         64) return 6;
	if (size <=        128) return 7;
	if (size <=        256) return 8;
	if (size <=        512) return 9;
	if (size <=       1024) return 10;
	if (size <=   2 * 1024) return 11;
	if (size <=   4 * 1024) return 12;
	if (size <=   8 * 1024) return 13;
	if (size <=  16 * 1024) return 14;
	if (size <=  32 * 1024) return 15;
	if (size <=  64 * 1024) return 16;
	if (size <= 128 * 1024) return 17;
	if (size <= 256 * 1024) return 18;
	if (size <= 512 * 1024) return 19;
	if (size <= 1024 * 1024) return 20;
	if (size <=  2 * 1024 * 1024) return 21;
	if (size <=  4 * 1024 * 1024) return 22;
	if (size <=  8 * 1024 * 1024) return 23;
	if (size <=  16 * 1024 * 1024) return 24;
	if (size <=  32 * 1024 * 1024) return 25;

	if ((IS_ENABLED(CONFIG_CC_IS_GCC) || CONFIG_CLANG_VERSION >= 110000)
	    && !IS_ENABLED(CONFIG_PROFILE_ALL_BRANCHES) && size_is_constant)
		BUILD_BUG_ON_MSG(1, "unexpected size in kmalloc_index()");
	else
		BUG();

	/* Will never be reached. Needed because the compiler may complain */
	return -1;
}

这个函数很好理解,就是根据size,返回kmalloc_caches[type][index]的index值

kmem_cache_alloc_trace


include/linux/slab.h
 
#ifdef CONFIG_TRACING
extern void *kmem_cache_alloc_trace(struct kmem_cache *, gfp_t, size_t) __assume_slab_alignment __malloc;
#else
static __always_inline void *kmem_cache_alloc_trace(struct kmem_cache *s,
		gfp_t flags, size_t size)
{
	void *ret = kmem_cache_alloc(s, flags);
 
	ret = kasan_kmalloc(s, ret, size, flags);
	return ret;
}
#endif

最终都是通过 slab_alloc() 进行分配,如果使能 CONFIG_TRACING ,会多一个 trace_kmalloc() 跟踪


mm/slub.c
 
#ifdef CONFIG_TRACING
void *kmem_cache_alloc_trace(struct kmem_cache *s, gfp_t gfpflags, size_t size)
{
	void *ret = slab_alloc(s, gfpflags, _RET_IP_, size);
	trace_kmalloc(_RET_IP_, ret, size, s->size, gfpflags);
	ret = kasan_kmalloc(s, ret, size, gfpflags);
	return ret;
}
EXPORT_SYMBOL(kmem_cache_alloc_trace);
#endif

slab_alloc调用的就slab_alloc_node

static __always_inline void *slab_alloc(struct kmem_cache *s, struct list_lru *lru,
		gfp_t gfpflags, unsigned long addr, size_t orig_size)
{
	return slab_alloc_node(s, lru, gfpflags, NUMA_NO_NODE, addr, orig_size);
}
static __always_inline void *slab_alloc_node(struct kmem_cache *s,
        gfp_t gfpflags, int node, unsigned long addr)
{
    // 用于指向分配成功的对象
    void *object;
    // slab cache 在当前 cpu 下的本地 cpu 缓存
    struct kmem_cache_cpu *c;
    // object 所在的内存页
    struct page *page;
    // 当前 cpu 编号
    unsigned long tid;

redo:
    // slab cache 首先尝试从当前 cpu 本地缓存 kmem_cache_cpu 中获取空闲对象
    // 这里的 do..while 循环是要保证获取到的 cpu 本地缓存 c 是属于执行进程的当前 cpu
    // 因为进程可能由于抢占或者中断的原因被调度到其他 cpu 上执行,所需需要确保两者的 tid 是否一致
    do {
        // 获取执行当前进程的 cpu 中的 tid 字段
        tid = this_cpu_read(s->cpu_slab->tid);
        // 获取 cpu 本地缓存 cpu_slab
        c = raw_cpu_ptr(s->cpu_slab);
        // 如果开启了 CONFIG_PREEMPT 表示允许优先级更高的进程抢占当前 cpu
        // 如果发生抢占,当前进程可能被重新调度到其他 cpu 上运行,所以需要检查此时运行当前进程的 cpu tid 是否与刚才获取的 cpu 本地缓存一致
        // 如果两者的 tid 字段不一致,说明进程已经被调度到其他 cpu 上了, 需要再次获取正确的 cpu 本地缓存
    } while (IS_ENABLED(CONFIG_PREEMPT) &&
         unlikely(tid != READ_ONCE(c->tid)));

    // 从 slab cache 的 cpu 本地缓存 kmem_cache_cpu 中获取缓存的 slub 空闲对象列表
    // 这里的 freelist 指向本地 cpu 缓存的 slub 中第一个空闲对象
    object = c->freelist;
    // 获取本地 cpu 缓存的 slub,这里用 page 表示,如果是复合页,这里指向复合页的首页 head page
    page = c->page;
    if (unlikely(!object || !node_match(page, node))) {
        // 如果 slab cache 的 cpu 本地缓存中已经没有空闲对象了
        // 或者 cpu 本地缓存中的 slub 并不属于我们指定的 NUMA 节点
        // 那么我们就需要进入慢速路径中分配对象:
        // 1. 检查 kmem_cache_cpu 的 partial 列表中是否有空闲的 slub
        // 2. 检查 kmem_cache_node 的 partial 列表中是否有空闲的 slub
        // 3. 如果都没有,则只能重新到伙伴系统中去申请内存页
        object = __slab_alloc(s, gfpflags, node, addr, c);
        // 统计 slab cache 的状态信息,记录本次分配走的是慢速路径 slow path
        stat(s, ALLOC_SLOWPATH);
    } else {
        // 走到该分支表示,slab cache 的 cpu 本地缓存中还有空闲对象,直接分配
        // 快速路径 fast path 下分配成功,从当前空闲对象中获取下一个空闲对象指针 next_object        
        void *next_object = get_freepointer_safe(s, object);
        // 更新 kmem_cache_cpu 结构中的 freelist 指向 next_object
        if (unlikely(!this_cpu_cmpxchg_double(
                s->cpu_slab->freelist, s->cpu_slab->tid,
                object, tid,
                next_object, next_tid(tid)))) {

            note_cmpxchg_failure("slab_alloc", s, tid);
            goto redo;
        }
        // cpu 预取 next_object 的 freepointer 到 cpu 高速缓存,加快下一次分配对象的速度
        prefetch_freepointer(s, next_object);
        stat(s, ALLOC_FASTPATH);
    }

    // 如果 gfpflags 掩码中设置了  __GFP_ZERO,则需要将对象所占的内存初始化为零值
    if (unlikely(slab_want_init_on_alloc(gfpflags, s)) && object)
        memset(object, 0, s->object_size);
    // 返回分配好的对象
    return object;
}

这部分请详看 [linux内存管理] 第030篇 深入理解 slab cache 内存分配全链路实现

__kmalloc

void *__kmalloc(size_t size, gfp_t flags)
{
	struct kmem_cache *s;
	void *ret;

	if (unlikely(size > KMALLOC_MAX_CACHE_SIZE))
		return kmalloc_large(size, flags);

	s = kmalloc_slab(size, flags);

	if (unlikely(ZERO_OR_NULL_PTR(s)))
		return s;

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

此函数是针对 size 为变量时的处理过程:

  • 如果 size 超过kmalloc caches最大值,使用 kmalloc_large() 从buddy 系统申请;

  • 通过kmalloc_slab() 获取size 对应的kmalloc cache;

  • 通过 slab_alloc() 从kmalloc cache申请内存;

  • trace_kmalloc() 进行ftrace 追踪;

kmalloc_slab

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 {         
    // 如果申请的内存块 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];
}

kmalloc 内存池分配内存块的核心就是需要在 kmalloc_caches 二维数组中查找到最佳合适尺寸的 slab cache

index 确认有两种方式:

  • 当size <= 192 时,通过 size_index_elem() 从size_index 数组中选择;

  • 当size > 192时,通过 fls() 获取size 最高bit 位数,例如fls(0x80000000)=32,fls(1)=1;

如果我们能通过申请内存块的大小 size,定位到 size_index 数组本身的索引 sizeindex,那么我们就可以通过 size_index[sizeindex] 找到 kmalloc_caches 中的最佳 slab cache 了。

kmalloc 内存池回收内存

内核提供了 kfree 函数来释放由 kmalloc 内存池分配的内存块,参数 x 表示释放内存块的虚拟内存地址。

void kfree(const void *x)
{
	struct page *page;
	void *object = (void *)x;

    //ftrace追踪kfree轨迹
	trace_kfree(_RET_IP_, x);

	if (unlikely(ZERO_OR_NULL_PTR(x)))
		return;
    //通过object 的虚拟地址得到对应的page
	page = virt_to_head_page(x);
    //确认page 是否来自slab,当通过slab从buddy系统申请页块时,都会将page打上slab标记
    //如果没有打上slab标记,通过__free_pages() 释放页块
	if (unlikely(!PageSlab(page))) {
		free_nonslab_page(page, object);
		return;
	}
    //kfree() 的核心处理函数
	slab_free(page->slab_cache, page, object, NULL, 1, _RET_IP_);
}
EXPORT_SYMBOL(kfree);

注意:

  • 如果通过kmalloc cache 申请的对象,在通过 new_slab() 向buddy 申请的页块之后都会调用 SetPageSlab(page); 打上slab标记。但是不是通过kmalloc cache 申请的对象,例如通过 kmalloc_large() 申请的,是不会打上 slab 标记;

  • 代码中会判断是否打上slab 标记PG_slab 标识,来确认是否从 kamlloc cache 中申请的对象,说明该物理内存页没有被 slab cache 管理,说明当初调用 kmalloc 分配的时候直接走的是伙伴系统,是通过 kmalloc_large() 申请的内存,需要将 NR_SLAB_UNRECLAIMABLE 这项数据相应地减去,并通过 __free_pages() 归还给 buddy 系统;

  • 当通过 kmalloc cache 申请内存时,需要通过函数 slab_free() 释放内存

slab_free

static __always_inline void slab_free(struct kmem_cache *s, struct page *page,
				      void *head, void *tail, int cnt,
				      unsigned long addr)
{
	/*
	 * With KASAN enabled slab_free_freelist_hook modifies the freelist
	 * to remove objects, whose reuse must be delayed.
	 */
	if (slab_free_freelist_hook(s, &head, &tail, &cnt))
		do_slab_free(s, page, head, tail, cnt, addr);

	trace_android_vh_slab_free(addr, s);
}

slab_free_freelist_hook

bool slab_free_freelist_hook(struct kmem_cache *s,
                           void **head, void **tail,
                           int *cnt)
{
    void *object;
    void *next = *head;
    void *old_tail = *tail ? *tail : *head;

    // 1. 检查是否是 kfence 内存(一种内存错误检测机制)
    if (is_kfence_address(next)) {
        slab_free_hook(s, next, false);
        return true;
    }

    // 2. 初始化新链表
    *head = NULL;
    *tail = NULL;

    // 3. 遍历原始空闲链表
    do {
        object = next;
        next = get_freepointer(s, object);  // 获取下一个空闲对象

        // 4. 调用 slab_free_hook 检查是否可以重用
        if (!slab_free_hook(s, object, slab_want_init_on_free(s))) {
            // 可以重用:添加到新链表
            set_freepointer(s, object, *head);
            *head = object;
            if (!*tail)
                *tail = object;
        } else {
            // 不能重用:减少计数
            --(*cnt);
        }
    } while (object != old_tail);

    // 5. 特殊处理:如果只有一个对象,tail设为NULL
    if (*head == *tail)
        *tail = NULL;

    return *head != NULL;  // 返回是否有可用对象
}
  • 处理安全相关的内存释放钩子(如 KASAN、KMSAN 等)

  • 过滤掉不能立即重用的对象

  • 重建可用的空闲链表

slab_free_hook

// 这个函数处理各种安全检查和内存标记
static bool slab_free_hook(struct kmem_cache *s, void *object, bool init)
{
    // 1. 如果开启了 CONFIG_SLAB_FREELIST_HARDENED,会检查内存损坏
    if (IS_ENABLED(CONFIG_SLAB_FREELIST_HARDENED) &&
        check_unpoison_object(s, object))
        return true;  // 对象被污染,延迟重用
    
    // 2. KASAN(内核地址消毒剂)处理
    kasan_slab_free(s, object, init);
    
    // 3. KMSAN(内核内存消毒剂)处理
    kmsan_slab_free(s, object);
    
    // 4. 如果需要在释放时清零内存(安全特性)
    if (init)
        memset(object, 0, s->object_size);
    
    // 5. 检查是否启用内存隔离(防止 use-after-free)
    if (slab_want_init_on_free(s) && !init) {
        // 可能需要延迟重用
        quarantine_object(s, object);
        return true;
    }
    
    return false;  // 可以立即重用
}

do_slab_free

快速路径(无锁优化)

// 非 RT 内核的快速路径
if (likely(page == c->page)) {
    void **freelist = READ_ONCE(c->freelist);
    
    // 将新对象链入 per-CPU 缓存
    set_freepointer(s, tail_obj, freelist);
    
    // 使用双字 CAS 原子操作更新 freelist 和 tid
    if (unlikely(!this_cpu_cmpxchg_double(
            s->cpu_slab->freelist, s->cpu_slab->tid,
            freelist, tid,
            head, next_tid(tid)))) {
        // CAS 失败,重试
        note_cmpxchg_failure("slab_free", s, tid);
        goto redo;
    }
    stat(s, FREE_FASTPATH);
}
this_cpu_cmpxchg_double
// this_cpu_cmpxchg_double 伪代码
bool this_cpu_cmpxchg_double(void **ptr1, void **ptr2,
                            void *old1, void *old2,
                            void *new1, void *new2)
{
    // 原子比较并交换两个指针
    if (*ptr1 == old1 && *ptr2 == old2) {
        *ptr1 = new1;
        *ptr2 = new2;
        return true;
    }
    return false;
}
tid 的作用
// tid 用于检测并发修改
typedef struct { unsigned int tid; } kmem_cache_cpu_tid_t;

// 每次修改 freelist 时递增 tid
static inline unsigned int next_tid(unsigned int tid)
{
    return tid + 1;
}

// 读-修改-写操作中使用 tid 确保一致性
// 1. 读取当前 tid 和 freelist
// 2. 准备新的 freelist
// 3. 使用 CAS 更新,如果 tid 变化说明有并发修改
PREEMPT_RT 实时内核的特殊处理
#ifdef CONFIG_PREEMPT_RT
// RT 内核使用锁而不是无锁操作
local_lock(&s->cpu_slab->lock);
c = this_cpu_ptr(s->cpu_slab);
if (unlikely(page != c->page)) {
    local_unlock(&s->cpu_slab->lock);
    goto redo;
}
tid = c->tid;
freelist = c->freelist;

// 直接更新,不需要 CAS
set_freepointer(s, tail_obj, freelist);
c->freelist = head;
c->tid = next_tid(tid);

local_unlock(&s->cpu_slab->lock);
#endif

慢速路径

static void __slab_free(struct kmem_cache *s, struct page *page,
                       void *head, void *tail, int cnt,
                       unsigned long addr)
{
    void *prior;
    int was_frozen;
    struct page new;
    unsigned long counters;
    struct kmem_cache_node *n = NULL;
    unsigned long flags;
    
    // 1. 获取节点锁
    local_irq_save(flags);
    
    // 2. 如果是部分空的 slab,添加到 per-node 部分空链表
    if (kmem_cache_debug(s) && !free_debug_processing(s, page, head, addr))
        goto out_unlock;
    
    // 3. 更新 slab 的计数器
    do {
        prior = page->freelist;
        counters = page->counters;
        set_freepointer(s, tail, prior);
        new.counters = counters;
        was_frozen = new.frozen;
        new.inuse -= cnt;
        
        // 4. 如果是最后一个对象被释放,slab 变为完全空闲
        if ((new.inuse == 0) && !was_frozen) {
            // 从部分空链表移除
            if (kmem_cache_has_cpu_partial(s) && !prior) {
                // 保持部分空状态
            } else {
                // 完全空闲,可以释放给伙伴系统
                discard_slab(s, page);
                stat(s, FREE_SLAB);
            }
        }
        
    } while (!cmpxchg_double_slab(s, page,
            prior, counters,
            head, new.counters,
            "__slab_free"));
    
    // 5. 如果是部分空的 slab,添加到节点部分空链表
    if (was_frozen && !new.frozen && n) {
        spin_lock(&n->list_lock);
        add_partial(n, page, DEACTIVATE_TO_TAIL);
        stat(s, FREE_ADD_PARTIAL);
    }
    
out_unlock:
    local_irq_restore(flags);
}