相信大家能够看到这篇文章,应该已经对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:
kmalloc的slab内存池是如何创建的?
使用kmalloc申请内存时是如何选择合适的内存池的?
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);
#endifslab_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);
}