深入理解 slab cache 内存分配全链路实现
本文源码部分基于内核 5.4 版本讨论
在经过上篇文章 《从内核源码看 slab 内存池的创建初始化流程》 的介绍之后,我们最终得到下面这幅 slab cache 的完整架构图:
本文笔者将带大家继续从内核源码的角度继续拆解 slab cache 的实现细节,接下来笔者会基于上面这幅 slab cache 完整架构图,详细介绍一下 slab cache 是如何进行内存分配的。
1. slab cache 如何分配内存
当我们使用 fork() 系统调用创建进程的时候,内核需要为进程创建 task_struct 结构,struct task_struct 是内核中的核心数据结构,当然也会有专属的 slab cache 来进行管理,task_struct 专属的 slab cache 为 task_struct_cachep。
下面笔者就以内核从 task_struct_cachep 中申请 task_struct 对象为例,为大家剖析 slab cache 分配内存的整个源码实现。
内核通过定义在文件 /kernel/fork.c
中的 dup_task_struct 函数来为进程申请
task_struct 结构并初始化。
static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
{
...........
struct task_struct *tsk;
// 从 task_struct 对象专属的 slab cache 中申请 task_struct 对象
tsk = alloc_task_struct_node(node);
...........
}
// task_struct 对象专属的 slab cache
static struct kmem_cache *task_struct_cachep;
static inline struct task_struct *alloc_task_struct_node(int node)
{
// 利用 task_struct_cachep 动态分配 task_struct 对象
return kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node);
}
内核中通过 kmem_cache_alloc_node 函数要求 slab cache 从指定的 NUMA 节点中分配对象。
// 定义在文件:/mm/slub.c
void *kmem_cache_alloc_node(struct kmem_cache *s, gfp_t gfpflags, int node)
{
void *ret = slab_alloc_node(s, gfpflags, node, _RET_IP_);
return ret;
}
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;
}
2. slab cache 的快速分配路径
正如笔者在前边文章 《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现》 中的 “ 7. slab 内存分配原理 ” 小节里介绍的原理,slab cache 在最开始会进入 fastpath 分配对象,也就是说首先会从 cpu 本地缓存 kmem_cache_cpu->freelist 中获取对象。
在获取 kmem_cache_cpu 结构的时候需要保证这个 cpu 本地缓存是属于当前执行进程的 cpu。
在开启了 CONFIG_PREEMPT 的情况下,内核是允许优先级更高的进程抢占当前 cpu 的,当发生 cpu 抢占之后,进程会被内核重新调度到其他 cpu 上执行,这样一来,进程在被抢占之前获取到的 kmem_cache_cpu 就与当前执行进程 cpu 的 kmem_cache_cpu 不一致了。
内核在 slab_alloc_node 函数开始的地方通过在 do..while
循环中不断判断两者的 tid 是否一致来保证这一点。
随后内核会通过 kmem_cache_cpu->freelist 来获取 cpu 缓存 slab 中的第一个空闲对象。
如果当前 cpu 缓存 slab 是空的(没有空闲对象可供分配)或者该 slab 所在的 NUMA 节点并不是我们指定的。那么就会通过 __slab_alloc 进入到慢速分配路径 slowpath 中。
如果当前 cpu 缓存 slab 有空闲的对象并且 slab 所在的 NUMA 节点正是我们指定的,那么将当前 kmem_cache_cpu->freelist 指向的第一个空闲对象从 slab 中拿出,并分配出去。
随后通过 get_freepointer_safe 获取当前分配对象的 freepointer 指针(指向其下一个空闲对象),然后将 kmem_cache_cpu->freelist 更新为 freepointer (指向的下一个空闲对象)。
// slub 中的空闲对象中均保存了下一个空闲对象的指针 free_pointer
// free_pointor 在 object 中的位置由 kmem_cache 结构的 offset 指定
static inline void *get_freepointer_safe(struct kmem_cache *s, void *object)
{
// freepointer 在 object 内存区域的起始地址
unsigned long freepointer_addr;
// 指向下一个空闲对象的 free_pontier
void *p;
// free_pointer 位于 object 起始地址的 offset 偏移处
freepointer_addr = (unsigned long)object + s->offset;
// 获取 free_pointer 指向的地址(下一个空闲对象)
probe_kernel_read(&p, (void **)freepointer_addr, sizeof(p));
// 返回下一个空闲对象地址
return freelist_ptr(s, p, freepointer_addr);
}
3. slab cache 的慢速分配路径
static void *__slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
unsigned long addr, struct kmem_cache_cpu *c)
{
void *p;
unsigned long flags;
// 关闭 cpu 中断,防止并发访问
local_irq_save(flags);
#ifdef CONFIG_PREEMPT
// 当开启了 CONFIG_PREEMPT,表示允许其他进程抢占当前 cpu
// 运行进程的当前 cpu 可能会被其他优先级更高的进程抢占,当前进程可能会被调度到其他 cpu 上
// 所以这里需要重新获取 slab cache 的 cpu 本地缓存
c = this_cpu_ptr(s->cpu_slab);
#endif
// 进入 slab cache 的慢速分配路径
p = ___slab_alloc(s, gfpflags, node, addr, c);
// 恢复 cpu 中断
local_irq_restore(flags);
return p;
}
内核为了防止 slab cache 在慢速路径下的并发安全问题,在进入 slowpath 之前会把中断关闭掉,并重新获取 cpu 本地缓存。这样做的目的是为了防止再关闭中断之前,进程被抢占,调度到其他 cpu 上。
static void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
unsigned long addr, struct kmem_cache_cpu *c)
{
// 指向 slub 中可供分配的第一个空闲对象
void *freelist;
// 空闲对象所在的 slub (用 page 表示)
struct page *page;
// 从 slab cache 的本地 cpu 缓存中获取缓存的 slub
page = c->page;
if (!page)
// 如果缓存的 slub 中的对象已经被全部分配出去,没有空闲对象了
// 那么就会跳转到 new_slab 分支进行降级处理走慢速分配路径
goto new_slab;
redo:
// 这里需要再次检查 slab cache 本地 cpu 缓存中的 freelist 是否有空闲对象
// 因为当前进程可能被中断,当重新调度之后,其他进程可能已经释放了一些对象到缓存 slab 中
// freelist 可能此时就不为空了,所以需要再次尝试一下
freelist = c->freelist;
if (freelist)
// 从 cpu 本地缓存中的 slub 中直接分配对象
goto load_freelist;
// 本地 cpu 缓存的 slub 用 page 结构来表示,这里是检查 page 结构的 freelist 是否还有空闲对象
// c->freelist 表示的是本地 cpu 缓存的空闲对象列表,刚我们已经检查过了
// 现在我们检查的 page->freelist ,它表示由其他 cpu 所释放的空闲对象列表
// 因为此时有可能其他 cpu 又释放了一些对象到 slub 中这时 slub 对应的 page->freelist 不为空,可以直接分配
freelist = get_freelist(s, page);
// 注意这里的 freelist 已经变为 page->freelist ,并不是 c->freelist;
if (!freelist) {
// 此时 cpu 本地缓存的 slub 里的空闲对象已经全部耗尽
// slub 从 cpu 本地缓存中脱离,进入 new_slab 分支走慢速分配路径
c->page = NULL;
stat(s, DEACTIVATE_BYPASS);
goto new_slab;
}
stat(s, ALLOC_REFILL);
load_freelist:
// 被 slab cache 的 cpu 本地缓存的 slub 所属的 page 必须是 frozen 冻结状态,只允许本地 cpu 从中分配对象
VM_BUG_ON(!c->page->frozen);
// kmem_cache_cpu 中的 freelist 指向被 cpu 缓存 slub 中第一个空闲对象
// 由于第一个空闲对象马上要被分配出去,所以这里需要获取下一个空闲对象更新 freelist
c->freelist = get_freepointer(s, freelist);
// 更新 slab cache 的 cpu 本地缓存分配对象时的全局 transaction id
// 每当分配完一次对象,kmem_cache_cpu 中的 tid 都需要改变
c->tid = next_tid(c->tid);
// 返回第一个空闲对象
return freelist;
new_slab:
......... 进入 slowpath 分配对象 ..........
}
在 slab cache 进入慢速路径之前,内核还需要再次检查本地 cpu 缓存的 slab 的存储容量,确保其真的没有空闲对象了。
如果本地 cpu 缓存的 slab 为空( kmem_cache_cpu->page == null ),直接跳转到 new_slab 分支进入 slow path。
如果本地 cpu 缓存的 slab 不为空,那么需要再次检查 slab 中是否有空闲对象,这么做的目的是因为当前进程可能被中断,当重新调度之后,其他进程可能已经释放了一些对象到缓存 slab 中了,所以在进入 slowpath 之前还是有必要再次检查一下 kmem_cache_cpu->freelist。
如果碰巧,其他进程在当前进程被中断之后,已经释放了一些对象回缓存 slab 中了,那么就直接跳转至 load_freelist 分支,走 fastpath 路径,直接从缓存 slab (kmem_cache_cpu->freelist) 中分配对象,避免进入 slowpath。
load_freelist:
// 更新 freelist,指向下一个空闲对象
c->freelist = get_freepointer(s, freelist);
// 更新 tid
c->tid = next_tid(c->tid);
// 返回第一个空闲对象
return freelist;
如果 kmem_cache_cpu->freelist 还是为空,则需要再次检查 slab 本身的 freelist 是否空,注意这里指的是 struct page 结构中的 freelist。
struct page {
// 指向内存页中第一个空闲对象
void *freelist; /* first free object */
// 该 slab 是否在对应 slab cache 的本地 CPU 缓存中
// frozen = 1 表示缓存再本地 cpu 缓存中
unsigned frozen:1;
}
大家读到这里一定会感觉非常懵,kmem_cache_cpu 结构中有一个 freelist,page 结构也有一个 freelist,懵逼的是这两个 freelist 均是指向 slab 中第一个空闲对象,它俩之间有什么差别吗?
事实上,这一块的确比较复杂,逻辑比较绕,所以笔者有必要详细的为大家说明一下,以解决大家心中的困惑。
首先,在 slab cache 的整个架构体系中的确存在两个 freelist:
-
一个是 page->freelist,因为 slab 在内核中是使用 struct page 结构来表示的,所以 page->freelist 只是单纯的站在 slab 的视角来表示 slab 中的空闲对象列表,这里不考虑 slab 在 slab cache 架构中的位置。
-
另一个是 kmem_cache_cpu->freelist,特指 slab 被 slab cache 的本地 cpu 缓存之后,slab 中的空闲对象链表。这里可以理解为 slab 中被 cpu 缓存的空闲对象。当 slab 被提升为 cpu 缓存之后,page->freeelist 直接赋值给 kmem_cache_cpu->freelist,然后 page->freeelist 置空。slab->frozen 设置为 1,表示 slab 被冻结在当前 cpu 的本地缓存中。
而 slab 一旦被当前 cpu 缓存,它的状态就变为了冻结状态(slab->frozen = 1),处于冻结状态下的 slab,当前 cpu 可以从该 slab 中分配或者释放对象,但是其他 cpu 只能释放对象到该 slab 中,不能从该 slab 中分配对象。
-
如果一个 slab 被一个 cpu 缓存之后,那么这个 cpu 在该 slab 看来就是本地 cpu,当本地 cpu 释放对象回这个 slab 的时候会释放回 kmem_cache_cpu->freelist 链表中
-
如果其他 cpu 想要释放对象回该 slab 时,其他 cpu 只能将对象释放回该 slab 的 page->freelist 中。
什么意思呢?笔者来举一个具体的例子为大家详细说明。
如下图所示,cpu1 在本地缓存了 slab1,cpu2 在本地缓存了 slab2,进程先从 slab1 中获取了一个对象,正常情况下如果进程一直在 cpu1 上运行的话,当进程释放该对象回 slab1 中时,会直接释放回 kmem_cache_cpu1->freelist 链表中。
但如果进程在 slab1 中获取完对象之后,被调度到了 cpu2 上运行,这时进程想要释放对象回 slab1 中时,就不能走快速路径了,因为 cpu2 本地缓存的是 slab2,所以 cpu2 只能将对象释放至 slab1->freelist 中。
这种情况下,在 slab1 的内部视角里,就有了两个 freelist 链表,它们的共同之处都是用于组织 slab1 中的空闲对象,但是 kmem_cache_cpu1->freelist 链表中组织的是缓存再 cpu1 本地的空闲对象,slab1->freelist 链表组织的是由其他 cpu 释放的空闲对象。
明白了这些,让我们再次回到 ___slab_alloc 函数的开始处,首先内核会在 slab cache 的本地 cpu 缓存 kmem_cache_cpu->freelist 中查找是否有空闲对象,如果这里没有,内核会继续到 page->freelist 中查看是否有其他 cpu 释放的空闲对象
如果两个 freelist 链表都没有空闲对象了,那就证明 slab cache 在当前 cpu 本地缓存中的 slab 已经为空了,将该 slab 从当前 cpu 本地缓存中脱离解冻,程序跳转到 new_slab 分支进入慢速分配路径。
// 查看 page->freelist 中是否有其他 cpu 释放的空闲对象
static inline void *get_freelist(struct kmem_cache *s, struct page *page)
{
// 用于存放要更新的 page 属性值
struct page new;
unsigned long counters;
void *freelist;
do {
// 获取 page 结构的 freelist,当其他 cpu 向 page 释放对象时 freelist 指向被释放的空闲对象
// 当 page 被 slab cache 的 cpu 本地缓存时,freelist 置为 null
freelist = page->freelist;
counters = page->counters;
new.counters = counters;
VM_BUG_ON(!new.frozen);
// 更新 inuse 字段,表示 page 中的对象 objects 全部被分配出去了
new.inuse = page->objects;
// 如果 freelist != null,表示其他 cpu 又释放了一些对象到 page 中 (slub)。
// 则 page->frozen = 1 , slub 依然冻结在 cpu 本地缓存中
// 如果 freelist == null,则 page->frozen = 0, slub 从 cpu 本地缓存中脱离解冻
new.frozen = freelist != NULL;
// 最后 cas 原子更新 page 结构中的相应属性
// 这里需要注意的是,当 page 被 slab cache 本地 cpu 缓存时,page -> freelist 需要置空。
// 因为在本地 cpu 缓存场景下 page -> freelist 指向其他 cpu 释放的空闲对象列表
// kmem_cache_cpu->freelist 指向的是被本地 cpu 缓存的空闲对象列表
// 这两个列表中的空闲对象共同组成了 slub 中的空闲对象
} while (!__cmpxchg_double_slab(s, page,
freelist, counters,
NULL, new.counters,
"get_freelist"));
return freelist;
}
3.1 从本地 cpu 缓存 partial 列表中分配
内核经过在 redo
分支的检查,现在已经确认了 slab cache 在当前 cpu 本地缓存的 slab 已经没有任何可供分配的空闲对象了。
下面内核正式进入到 slowpath 开始分配对象,首先内核会到本地 cpu 缓存的 partial 列表中去查看是否有一个 slab 可以分配对象。这里内核会从 partial 列表中的头结点开始遍历直到找到一个可以满足分配的 slab 出来。
随后内核会将该 slab 从 partial 列表中摘下,直接提升为新的本地 cpu 缓存,这样一来 slab cache 的本地 cpu 缓存就被更新了,内核通过 kmem_cache_cpu->freelist 指针将缓存 slab 中的第一个空闲对象分配出去,随后更新 kmem_cache_cpu->freelist 指向 slab 中的下一个空闲对象。
static void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
unsigned long addr, struct kmem_cache_cpu *c)
{
............ 检查本地 cpu 缓存是否为空 ...........
redo:
............ 再次确认 kmem_cache_cpu->freelist 中是否有空闲对象 ...........
............ 再次确认 page->freelist 中是否有空闲对象 ...........
load_freelist:
............ 回到 fastpath 直接从 freelist 中分配对象 ...........
new_slab:
// 查看 kmem_cache_cpu->partial 链表中是否有 slab 可供分配对象
if (slub_percpu_partial(c)) {
// 获取 cpu 本地缓存 kmem_cache_cpu 的 partial 列表中的第一个 slub (用 page 表示)
// 并将这个 slub 提升为 cpu 本地缓存中的 slub,赋值给 c->page
page = c->page = slub_percpu_partial(c);
// 将 partial 列表中第一个 slub (c->page)从 partial 列表中摘下
// 并将列表中的下一个 slub 更新为 partial 列表的头结点
slub_set_percpu_partial(c, page);
// 更新状态信息,记录本次分配是从 kmem_cache_cpu 的 partial 列表中分配
stat(s, CPU_PARTIAL_ALLOC);
// 重新回到 redo 分支,这下就可以从 page->freelist 中获取对象了
// 并且在 load_freelist 分支中将 page->freelist 更新到 c->freelist 中,page->freelist 设置为 null
// 此时 slab cache 中的 cpu 本地缓存 kmem_cache_cpu 的 freelist 以及 page 就变为了 partial 列表中的 slub
goto redo;
}
// 流程走到这里表示 slab cache 中的 cpu 本地缓存 partial 列表中也没有 slub 了
// 需要近一步降级到 numa node cache —— kmem_cache_node 中的 partial 列表去查找
// 如果还是没有,就只能去伙伴系统中申请新的 slub,然后分配对象
// 该函数为 slab cache 在慢速路径下分配对象的核心逻辑
freelist = new_slab_objects(s, gfpflags, node, &c);
if (unlikely(!freelist)) {
// 如果伙伴系统中无法分配 slub 所需的 page,那么就提示内存不足,分配失败,返回 null
slab_out_of_memory(s, gfpflags, node);
return NULL;
}
page = c->page;
if (likely(!kmem_cache_debug(s) && pfmemalloc_match(page, gfpflags)))
// 此时从 kmem_cache_node->partial 列表中获取的 slub
// 或者从伙伴系统中重新申请的 slub 已经被提升为本地 cpu 缓存了 kmem_cache_cpu->page
// 这里需要跳转到 load_freelist 分支,从本地 cpu 缓存 slub 中获取第一个对象返回
goto load_freelist;
}
内核对 kmem_cache_cpu->partial 链表的相关操作:
// 定义在文件 /include/linux/slub_def.h 中
#ifdef CONFIG_SLUB_CPU_PARTIAL
// 获取 slab cache 本地 cpu 缓存的 partial 列表
#define slub_percpu_partial(c) ((c)->partial)
// 将 partial 列表中第一个 slub 摘下,提升为 cpu 本地缓存,用于后续快速分配对象
#define slub_set_percpu_partial(c, p) \
({ \
slub_percpu_partial(c) = (p)->next; \
})
如果 slab cache 本地 cpu 缓存 kmem_cache_cpu->partial 链表也是空的,接下来内核就只能到对应 NUMA 节点缓存中去分配对象了。
3.2 从 NUMA 节点缓存中分配
// slab cache 慢速路径下分配对象核心逻辑
static inline void *new_slab_objects(struct kmem_cache *s, gfp_t flags,
int node, struct kmem_cache_cpu **pc)
{
// 从 numa node cache 中获取到的空闲对象列表
void *freelist;
// slab cache 本地 cpu 缓存
struct kmem_cache_cpu *c = *pc;
// 分配对象所在的内存页
struct page *page;
// 尝试从指定的 node 节点缓存 kmem_cache_node 中的 partial 列表获取可以分配空闲对象的 slub
// 如果指定 numa 节点的内存不足,则会根据 cpu 访问距离的远近,进行跨 numa 节点分配
freelist = get_partial(s, flags, node, c);
if (freelist)
// 返回 numa cache 中缓存的空闲对象列表
return freelist;
// 流程走到这里说明 numa cache 里缓存的 slub 也用尽了,无法找到可以分配对象的 slub 了
// 只能向底层伙伴系统重新申请内存页(slub),然后从新的 slub 中分配对象
page = new_slab(s, flags, node);
// 将新申请的内存页 page (slub),缓存到 slab cache 的本地 cpu 缓存中
if (page) {
// 获取 slab cache 的本地 cpu 缓存
c = raw_cpu_ptr(s->cpu_slab);
// 刷新本地 cpu 缓存,将旧的 slub 缓存与 cpu 本地缓存解绑
if (c->page)
flush_slab(s, c);
// 将新申请的 slub 与 cpu 本地缓存绑定,page->freelist 赋值给 kmem_cache_cpu->freelist
freelist = page->freelist;
// 绑定之后 page->freelist 置空
// 现在新的 slub 中的空闲对象就已经缓存再了 slab cache 的本地 cpu 缓存中,后续就直接从这里分配了
page->freelist = NULL;
stat(s, ALLOC_SLAB);
// 将新申请的 slub 对应的 page 赋值给 kmem_cache_cpu->page
c->page = page;
*pc = c;
}
// 返回空闲对象列表
return freelist;
}
内核首先会在 get_partial 函数中找到我们指定的 NUMA 节点缓存结构 kmem_cache_node ,然后开始遍历 kmem_cache_node->partial 链表直到找到一个可供分配对象的 slab。然后将这个 slab 提升为 slab cache 的本地 cpu 缓存,并从 kmem_cache_node->partial 链表中依次填充 slab 到 kmem_cache_cpu->partial。
如果我们指定的 NUMA 节点 kmem_cache_node->partial 链表也是空的,随后内核就会跨 NUMA 节点进行查找,按照访问距离由近到远,开始查找其他 NUMA 节点 kmem_cache_node->partial 链表。
如果还是不行,最后就只能通过 new_slab 函数到伙伴系统中重新申请一个 slab,并将这个 slab 提升为本地 cpu 缓存。
3.2.1 从 NUMA 节点缓存 partial 链表中查找
static void *get_partial(struct kmem_cache *s, gfp_t flags, int node,
struct kmem_cache_cpu *c)
{
// 从指定 node 的 kmem_cache_node 缓存中的 partial 列表中获取到的对象
void *object;
// 即将要所搜索的 kmem_cache_node 缓存对应 numa node
int searchnode = node;
// 如果我们指定的 numa node 已经没有空闲内存了,则选取访问距离最近的 numa node 进行跨节点内存分配
if (node == NUMA_NO_NODE)
searchnode = numa_mem_id();
else if (!node_present_pages(node))
searchnode = node_to_mem_node(node);
// 从 searchnode 的 kmem_cache_node 缓存中的 partial 列表中获取对象
object = get_partial_node(s, get_node(s, searchnode), c, flags);
if (object || node != NUMA_NO_NODE)
return object;
// 如果 searchnode 对象的 kmem_cache_node 缓存中的 partial 列表是空的,没有任何可供分配的 slub
// 那么继续按照访问距离,遍历 searchnode 之后的 numa node,进行跨节点内存分配
return get_any_partial(s, flags, c);
}
get_partial 函数的主要内容是选取合适的 NUMA 节点缓存,优先使用我们指定的 NUMA 节点,如果指定的 NUMA 节点中没有足够的内存,内核就会跨 NUMA 节点按照访问距离的远近,选取一个合适的 NUMA 节点。
然后通过 get_partial_node 在选取的 NUMA 节点缓存 kmem_cache_node->partial 链表中查找 slab。
/*
* Try to allocate a partial slab from a specific node.
*/
static void *get_partial_node(struct kmem_cache *s, struct kmem_cache_node *n,
struct kmem_cache_cpu *c, gfp_t flags)
{
// 接下来就会挨个遍历 kmem_cache_node 的 partial 列表中的 slub
// 这两个变量用于临时存储遍历的 slub
struct page *page, *page2;
// 用于指向从 partial 列表 slub 中申请到的对象
void *object = NULL;
// 用于记录 slab cache 本地 cpu 缓存 kmem_cache_cpu 中所缓存的空闲对象总数(包括 partial 列表)
// 后续会向 kmem_cache_cpu 中填充 slub
unsigned int available = 0;
// 临时记录遍历到的 slub 中包含的剩余空闲对象个数
int objects;
spin_lock(&n->list_lock);
// 开始挨个遍历 kmem_cache_node 的 partial 列表,获取 slub 用于分配对象以及填充 kmem_cache_cpu
list_for_each_entry_safe(page, page2, &n->partial, slab_list) {
void *t;
// page 表示当前遍历到的 slub,这里会从该 slub 中获取空闲对象赋值给 t
// 并将 slub 从 kmem_cache_node 的 partial 列表上摘下
t = acquire_slab(s, n, page, object == NULL, &objects);
// 如果 t 是空的,说明 partial 列表上已经没有可供分配对象的 slub 了
// slub 都满了,退出循环,进入伙伴系统重新申请 slub
if (!t)
break;
// objects 表示当前 slub 中包含的剩余空闲对象个数
// available 用于统计目前遍历的 slub 中所有空闲对象个数
// 后面会根据 available 的值来判断是否继续填充 kmem_cache_cpu
available += objects;
if (!object) {
// 第一次循环会走到这里,第一次循环主要是满足当前对象分配的需求
// 将 partila 列表中第一个 slub 缓存进 kmem_cache_cpu 中
c->page = page;
stat(s, ALLOC_FROM_PARTIAL);
object = t;
} else {
// 第二次以及后面的循环就会走到这里,目的是从 kmem_cache_node 的 partial 列表中
// 摘下 slub,然后填充进 kmem_cache_cpu 的 partial 列表里
put_cpu_partial(s, page, 0);
stat(s, CPU_PARTIAL_NODE);
}
// 这里是用于判断是否继续填充 kmem_cache_cpu 中的 partial 列表
// kmem_cache_has_cpu_partial 用于判断 slab cache 是否配置了 cpu 缓存的 partial 列表
// 配置了 CONFIG_SLUB_CPU_PARTIAL 选项意味着开启 kmem_cache_cpu 中的 partial 列表,没有配置的话, cpu 缓存中就不会有 partial 列表
// kmem_cache_cpu 中缓存被填充之后的空闲对象个数(包括 partial 列表)不能超过 ( kmem_cache 结构中 cpu_partial 指定的个数 / 2 )
if (!kmem_cache_has_cpu_partial(s)
|| available > slub_cpu_partial(s) / 2)
// kmem_cache_cpu 已经填充满了,就退出循环,停止填充
break;
}
spin_unlock(&n->list_lock);
return object;
}
get_partial_node 函数通过遍历 NUMA 节点缓存结构 kmem_cache_node->partial 链表主要做两件事情:
-
将第一个遍历到的 slab 从 partial 链表中摘下,提升为本地 cpu 缓存 kmem_cache_cpu->page。
-
继续遍历 partial 链表,后面遍历到的 slab 会填充进本地 cpu 缓存 kmem_cache_cpu->partial 链表中,直到当前 cpu 缓存的所有空闲对象数目 available (既包括 kmem_cache_cpu->page 中的空闲对象也包括 kmem_cache_cpu->partial 链表中的空闲对象)超过了
kmem_cache->cpu_partial / 2
的限制。
现在 slab cache 的本地 cpu 缓存已经被填充好了,随后内核会从 kmem_cache_cpu->freelist 中分配一个空闲对象出来给进程使用。
3.2.2 从 NUMA 节点缓存 partial 链表中将 slab 摘下
// 从 kmem_cache_node 的 partial 列表中摘下一个 slub 分配对象
// 随后将摘下的 slub 放入 cpu 本地缓存 kmem_cache_cpu 中缓存,后续分配对象直接就会 cpu 缓存中分配
static inline void *acquire_slab(struct kmem_cache *s,
struct kmem_cache_node *n, struct page *page,
int mode, int *objects)
{
void *freelist;
unsigned long counters;
struct page new;
lockdep_assert_held(&n->list_lock);
// page 表示即将从 kmem_cache_node 的 partial 列表摘下的 slub
// 获取 slub 中的空闲对象列表 freelist
freelist = page->freelist;
counters = page->counters;
new.counters = counters;
// objects 存放该 slub 中还剩多少空闲对象
*objects = new.objects - new.inuse;
// mode = true 表示将 slub 摘下之后填充到 kmem_cache_cpu 缓存中
// mode = false 表示将 slub 摘下之后填充到 kmem_cache_cpu 缓存的 partial 列表中
if (mode) {
new.inuse = page->objects;
new.freelist = NULL;
} else {
new.freelist = freelist;
}
// slub 放入 kmem_cache_cpu 之后需要冻结,其他 cpu 不能从这里分配对象,只能释放对象
new.frozen = 1;
// 更新 slub (page表示)中的 freelist 和 counters
if (!__cmpxchg_double_slab(s, page,
freelist, counters,
new.freelist, new.counters,
"acquire_slab"))
return NULL;
// 将 slub (page表示)从 kmem_cache_node 的 partial 列表上摘下
remove_partial(n, page);
// 返回 slub 中的空闲对象列表
return freelist;
}
3.3 从伙伴系统中重新申请 slab
假设 slab cache 当前的架构如上图所示,本地 cpu 缓存 kmem_cache_cpu->page 为空,kmem_cache_cpu->partial 为空,kmem_cache_node->partial 链表也为空,比如 slab cache 在刚刚被创建出来的时候就是这个架构。
在这种情况下,内核就需要通过 new_slab 函数到伙伴系统中申请一个新的 slab,填充到 slab cache 的本地 cpu 缓存 kmem_cache_cpu->page 中。
static struct page *new_slab(struct kmem_cache *s, gfp_t flags, int node)
{
return allocate_slab(s,
flags & (GFP_RECLAIM_MASK | GFP_CONSTRAINT_MASK), node);
}
static struct page *allocate_slab(struct kmem_cache *s, gfp_t flags, int node)
{
// 用于指向从伙伴系统中申请到的内存页
struct page *page;
// kmem_cache 结构的中的 kmem_cache_order_objects oo,表示该 slub 需要多少个内存页,以及能够容纳多少个对象
// kmem_cache_order_objects 的高 16 位表示需要的内存页个数,低 16 位表示能够容纳的对象个数
struct kmem_cache_order_objects oo = s->oo;
// 控制向伙伴系统申请内存的行为规范掩码
gfp_t alloc_gfp;
void *start, *p, *next;
int idx;
bool shuffle;
// 向伙伴系统申请 oo 中规定的内存页
page = alloc_slab_page(s, alloc_gfp, node, oo);
if (unlikely(!page)) {
// 如果伙伴系统无法满足正常情况下 oo 指定的内存页个数
// 那么这里再次尝试用 min 中指定的内存页个数向伙伴系统申请内存页
// min 表示当内存不足或者内存碎片的原因无法满足内存分配时,至少要保证容纳一个对象所使用内存页个数
oo = s->min;
alloc_gfp = flags;
// 再次向伙伴系统申请容纳一个对象所需要的内存页(降级)
page = alloc_slab_page(s, alloc_gfp, node, oo);
if (unlikely(!page))
// 如果内存还是不足,则走到 out 分支直接返回 null
goto out;
stat(s, ORDER_FALLBACK);
}
// 初始化 slub 对应的 struct page 结构中的属性
// 获取 slub 可以容纳的对象个数
page->objects = oo_objects(oo);
// 将 slub cache 与 page 结构关联
page->slab_cache = s;
// 将 PG_slab 标识设置到 struct page 的 flag 属性中
// 表示该内存页 page 被 slub 所管理
__SetPageSlab(page);
// 用 0xFC 填充 slub 中的内存,用于内核对内存访问越界检查
kasan_poison_slab(page);
// 获取内存页对应的虚拟内存地址
start = page_address(page);
// 在配置了 CONFIG_SLAB_FREELIST_RANDOM 选项的情况下
// 会在 slub 的空闲对象中以随机的顺序初始化 freelist 列表
// 返回值 shuffle = true 表示随机初始化 freelist,shuffle = false 表示按照正常的顺序初始化 freelist
shuffle = shuffle_freelist(s, page);
// shuffle = false 则按照正常的顺序来初始化 freelist
if (!shuffle) {
// 获取 slub 第一个空闲对象的真正起始地址
// slub 可能配置了 SLAB_RED_ZONE,这样会在 slub 对象内存空间两侧填充 red zone,防止内存访问越界
// 这里需要跳过 red zone 获取真正存放对象的内存地址
start = fixup_red_left(s, start);
// 填充对象的内存区域以及初始化空闲对象
start = setup_object(s, page, start);
// 用 slub 中的第一个空闲对象作为 freelist 的头结点,而不是随机的一个空闲对象
page->freelist = start;
// 从 slub 中的第一个空闲对象开始,按照正常的顺序通过对象的 freepointer 串联起 freelist
for (idx = 0, p = start; idx < page->objects - 1; idx++) {
// 获取下一个对象的内存地址
next = p + s->size;
// 填充下一个对象的内存区域以及初始化
next = setup_object(s, page, next);
// 通过 p 的 freepointer 指针指向 next,设置 p 的下一个空闲对象为 next
set_freepointer(s, p, next);
// 通过循环遍历,就把 slub 中的空闲对象按照正常顺序串联在 freelist 中了
p = next;
}
// freelist 中的尾结点的 freepointer 设置为 null
set_freepointer(s, p, NULL);
}
// slub 的初始状态 inuse 的值为所有空闲对象个数
page->inuse = page->objects;
// slub 被创建出来之后,需要放入 cpu 本地缓存 kmem_cache_cpu 中
page->frozen = 1;
out:
if (!page)
return NULL;
// 更新 page 所在 numa 节点在 slab cache 中的缓存 kmem_cache_node 结构中的相关计数
// kmem_cache_node 中包含的 slub 个数加 1,包含的总对象个数加 page->objects
inc_slabs_node(s, page_to_nid(page), page->objects);
return page;
}
内核在向伙伴系统申请 slab 之前,需要知道一个 slab 具体需要多少个物理内存页,而这些信息定义在 struct kmem_cache 结构中的 oo 属性中:
struct kmem_cache {
// 其中低 16 位表示一个 slab 中所包含的对象总数,高 16 位表示一个 slab 所占有的内存页个数。
struct kmem_cache_order_objects oo;
}
通过 oo 的高 16 位获取 slab 需要的物理内存页数,然后调用 alloc_pages 或者 __alloc_pages_node 向伙伴系统申请。
static inline struct page *alloc_slab_page(struct kmem_cache *s,
gfp_t flags, int node, struct kmem_cache_order_objects oo)
{
struct page *page;
unsigned int order = oo_order(oo);
if (node == NUMA_NO_NODE)
page = alloc_pages(flags, order);
else
page = __alloc_pages_node(node, flags, order);
return page;
}
关于 alloc_pages 函数分配物理内存页的详细过程,感兴趣的读者可以回看下 《深入理解 Linux 物理内存分配全链路实现》
如果当前 NUMA 节点中的空闲内存不足,或者由于内存碎片的原因导致伙伴系统无法满足 slab 所需要的内存页个数,导致分配失败。
那么内核会降级采用 kmem_cache->min 指定的尺寸,向伙伴系统申请只容纳一个对象所需要的最小内存页个数。
struct kmem_cache {
// 当按照 oo 的尺寸为 slab 申请内存时,如果内存紧张,会采用 min 的尺寸为 slab 申请内存,可以容纳一个对象即可。
struct kmem_cache_order_objects min;
}
如果伙伴系统仍然无法满足,那么就只能跨 NUMA 节点分配了。如果成功地向伙伴系统申请到了 slab 所需要的内存页 page。紧接着就会初始化 page 结构中与 slab 相关的属性。
通过 kasan_poison_slab 函数将 slab 中的内存用 0xFC 填充,用于 kasan 对于内存越界相关的检查。
// 定义在文件:/mm/kasan/kasan.h
#define KASAN_KMALLOC_REDZONE 0xFC /* redzone inside slub object */
// 定义在文件:/mm/kasan/common.c
void kasan_poison_slab(struct page *page)
{
unsigned long i;
// slub 可能包含多个内存页 page,挨个遍历这些 page
// 清除这些 page->flag 中的内存越界检查标记
// 表示当访问到这些内存页的时候临时禁止内存越界检查
for (i = 0; i < compound_nr(page); i++)
page_kasan_tag_reset(page + i);
// 用 0xFC 填充这些内存页的内存,用于内存访问越界检查
kasan_poison_shadow(page_address(page), page_size(page),
KASAN_KMALLOC_REDZONE);
}
最后会初始化 slab 中的 freelist 链表,将内存页中的空闲内存块通过 page->freelist 链表组织起来。
如果内核开启了 CONFIG_SLAB_FREELIST_RANDOM
选项,那么就会通过
shuffle_freelist 函数将内存页中空闲的内存块按照随机的顺序串联在 page->freelist 中。
如果没有开启,则会在 if (!shuffle)
分支中,按照正常的顺序初始化 page->freelist。
最后通过 inc_slabs_node 更新 NUMA 节点缓存 kmem_cache_node 结构中的相关计数。
struct kmem_cache_node {
// slab 的个数
atomic_long_t nr_slabs;
// 该 node 节点中缓存的所有 slab 中包含的对象总和
atomic_long_t total_objects;
};
static inline void inc_slabs_node(struct kmem_cache *s, int node, int objects)
{
// 获取 page 所在 numa node 再 slab cache 中的缓存
struct kmem_cache_node *n = get_node(s, node);
if (likely(n)) {
// kmem_cache_node 中的 slab 计数加1
atomic_long_inc(&n->nr_slabs);
// kmem_cache_node 中包含的总对象计数加 objects
atomic_long_add(objects, &n->total_objects);
}
}
4. 初始化 slab freelist 链表
内核在对 slab 中的 freelist 链表初始化的时候,会有两种方式,一种是按照内存地址的顺序,一个一个的通过对象 freepointer 指针顺序串联所有空闲对象。
另外一种则是通过随机的方式,随机获取空闲对象,然后通过对象的 freepointer 指针将 slab 中的空闲对象按照随机的顺序串联起来。
考虑到顺序初始化 freelist 比较直观,为了方便大家的理解,笔者先为大家介绍顺序初始化的方式。
static struct page *allocate_slab(struct kmem_cache *s, gfp_t flags, int node)
{
// 获取 slab 的起始内存地址
start = page_address(page);
// shuffle_freelist 随机初始化 freelist 链表,返回 false 表示需要顺序初始化 freelist
shuffle = shuffle_freelist(s, page);
// shuffle = false 则按照正常的顺序来初始化 freelist
if (!shuffle) {
// 获取 slub 第一个空闲对象的真正起始地址
// slub 可能配置了 SLAB_RED_ZONE,这样会在 slub 对象内存空间两侧填充 red zone,防止内存访问越界
// 这里需要跳过 red zone 获取真正存放对象的内存地址
start = fixup_red_left(s, start);
// 填充对象的内存区域以及初始化空闲对象
start = setup_object(s, page, start);
// 用 slub 中的第一个空闲对象作为 freelist 的头结点,而不是随机的一个空闲对象
page->freelist = start;
// 从 slub 中的第一个空闲对象开始,按照正常的顺序通过对象的 freepointer 串联起 freelist
for (idx = 0, p = start; idx < page->objects - 1; idx++) {
// 获取下一个对象的内存地址
next = p + s->size;
// 填充下一个对象的内存区域以及初始化
next = setup_object(s, page, next);
// 通过 p 的 freepointer 指针指向 next,设置 p 的下一个空闲对象为 next
set_freepointer(s, p, next);
// 通过循环遍历,就把 slub 中的空闲对象按照正常顺序串联在 freelist 中了
p = next;
}
// freelist 中的尾结点的 freepointer 设置为 null
set_freepointer(s, p, NULL);
}
}
内核在顺序初始化 slab 中的 freelist 之前,首先需要知道 slab 的起始内存地址 start,但是考虑到 slab 如果配置了 SLAB_RED_ZONE 的情况,那么在 slab 对象左右两侧,内核均会插入两段 red zone,为了防止内存访问越界。
所以在这种情况下,我们通过 page_address
获取到的只是 slab 的起始内存地址,正是 slab 中第一个空闲对象的左侧 red zone 的起始位置。
所以我们需要通过 fixup_red_left 方法来修正 start 位置,使其越过 slab 对象左侧的 red zone,指向对象内存真正的起始位置,如上图中所示。
void *fixup_red_left(struct kmem_cache *s, void *p)
{
// 如果 slub 配置了 SLAB_RED_ZONE,则意味着需要再 slub 对象内存空间两侧填充 red zone,防止内存访问越界
// 这里需要跳过填充的 red zone 获取真正的空闲对象起始地址
if (kmem_cache_debug(s) && s->flags & SLAB_RED_ZONE)
p += s->red_left_pad;
// 如果没有配置 red zone,则直接返回对象的起始地址
return p;
}
当我们确定了对象的起始位置之后,对象所在的内存块也就确定了,随后调用 setup_object 函数来初始化内存块,这里会按照 slab 对象的内存布局进行填充相应的区域。
slab 对象详细的内存布局介绍,可以回看下笔者之前的文章 《细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现》 中的 “ 5. 从一个简单的内存页开始聊 slab ” 小节。
当初始化完对象的内存区域之后,slab 中的 freelist 指针就会指向这第一个已经被初始化好的空闲对象。
page->freelist = start;
随后通过 start + kmem_cache->size
顺序获取下一个空闲对象的起始地址,重复上述初始化对象过程。直到 slab 中的空闲对象全部串联在 freelist 中,freelist 中的最后一个空闲对象 freepointer 指向 null。
一般来说,都会使用顺序的初始化方式来初始化 freelist, 但出于安全因素的考虑,防止被攻击,会配置 CONFIG_SLAB_FREELIST_RANDOM
选项,这样就会使 slab 中的空闲对象以随机的方式串联在 freelist 中,无法预测。
在我们明白了 slab freelist 的顺序初始化方式之后,随机的初始化方式其实就很好理解了。
随机初始化和顺序初始化唯一不同的点在于,获取空闲对象起始地址的方式不同:
-
顺序初始化的方式是直接获取 slab 中第一个空闲对象的地址,然后通过
start + kmem_cache->size
按照顺序一个一个地获取后面对象地址。 -
随机初始化的方式则是通过随机的方式获取 slab 中空闲对象,也就是说 freelist 中的头结点可能是 slab 中的第一个对象,也可能是第三个对象。后续也是通过这种随机的方式来获取下一个随机的空闲对象。
// 返回值为 true 表示随机的初始化 freelist,false 表示采用第一个空闲对象初始化 freelist
static bool shuffle_freelist(struct kmem_cache *s, struct page *page)
{
// 指向第一个空闲对象
void *start;
void *cur;
void *next;
unsigned long idx, pos, page_limit, freelist_count;
// 如果没有配置 CONFIG_SLAB_FREELIST_RANDOM 选项或者 slub 容纳的对象个数小于 2
// 则无需对 freelist 进行随机初始化
if (page->objects < 2 || !s->random_seq)
return false;
// 获取 slub 中可以容纳的对象个数
freelist_count = oo_objects(s->oo);
// 获取用于随机初始化 freelist 的随机位置
pos = get_random_int() % freelist_count;
page_limit = page->objects * s->size;
// 获取 slub 第一个空闲对象的真正起始地址
// slub 可能配置了 SLAB_RED_ZONE,这样会在 slub 中对象内存空间两侧填充 red zone,防止内存访问越界
// 这里需要跳过 red zone 获取真正存放对象的内存地址
start = fixup_red_left(s, page_address(page));
// 根据随机位置 pos 获取第一个随机对象的距离 start 的偏移 idx
// 返回第一个随机对象的内存地址 cur = start + idx
cur = next_freelist_entry(s, page, &pos, start, page_limit,
freelist_count);
// 填充对象的内存区域以及初始化空闲对象
cur = setup_object(s, page, cur);
// 第一个随机对象作为 freelist 的头结点
page->freelist = cur;
// 以 cur 为头结点随机初始化 freelist(每一个空闲对象都是随机的)
for (idx = 1; idx < page->objects; idx++) {
// 随机获取下一个空闲对象
next = next_freelist_entry(s, page, &pos, start, page_limit,
freelist_count);
// 填充对象的内存区域以及初始化空闲对象
next = setup_object(s, page, next);
// 设置 cur 的下一个空闲对象为 next
// next 对象的指针就是 freepointer,存放于 cur 对象的 s->offset 偏移处
set_freepointer(s, cur, next);
// 通过循环遍历,就把 slub 中的空闲对象随机的串联在 freelist 中了
cur = next;
}
// freelist 中的尾结点的 freepointer 设置为 null
set_freepointer(s, cur, NULL);
// 表示随机初始化 freelist
return true;
}
5. slab 对象的初始化
内核按照 kmem_cache->size 指定的尺寸,将物理内存页中的内存划分成一个一个的小内存块,每一个小内存块即是 slab 对象占用的内存区域。setup_object 函数用于初始化这些内存区域,并对 slab 对象进行内存布局。
static void *setup_object(struct kmem_cache *s, struct page *page,
void *object)
{
// 初始化对象的内存区域,填充相关的字节,比如填充 red zone,以及 poison 对象
setup_object_debug(s, page, object);
object = kasan_init_slab_obj(s, object);
// 如果 kmem_cache 中设置了对象的构造函数 ctor,则用构造函数初始化对象
if (unlikely(s->ctor)) {
kasan_unpoison_object_data(s, object);
// 使用用户指定的构造函数初始化对象
s->ctor(object);
// 在对象内存区域的开头用 0xFC 填充一段 KASAN_SHADOW_SCALE_SIZE 大小的区域
// 用于对内存访问越界的检查
kasan_poison_object_data(s, object);
}
return object;
}
// 定义在文件:/mm/kasan/kasan.h
#define KASAN_KMALLOC_REDZONE 0xFC /* redzone inside slub object */
#define KASAN_SHADOW_SCALE_SIZE (1UL << KASAN_SHADOW_SCALE_SHIFT)
// 定义在文件:/arch/x86/include/asm/kasan.h
#define KASAN_SHADOW_SCALE_SHIFT 3
void kasan_poison_object_data(struct kmem_cache *cache, void *object)
{
// 在对象内存区域的开头用 0xFC 填充一段 KASAN_SHADOW_SCALE_SIZE 大小的区域
// 用于对内存访问越界的检查
kasan_poison_shadow(object,
round_up(cache->object_size, KASAN_SHADOW_SCALE_SIZE),
KASAN_KMALLOC_REDZONE);
}
关于 slab 对象内存布局的核心逻辑封装在 setup_object_debug 函数中:
// 定义在文件:/include/linux/poison.h
#define SLUB_RED_INACTIVE 0xbb
static void setup_object_debug(struct kmem_cache *s, struct page *page,
void *object)
{
// SLAB_STORE_USER:存储最近访问该对象的 owner 信息,方便 bug 追踪
// SLAB_RED_ZONE:在 slub 中对象内存区域的前后填充分别填充一段 red zone 区域,防止内存访问越界
// __OBJECT_POISON:在对象内存区域中填充一些特定的字符,表示对象特定的状态。比如:未被分配状态
if (!(s->flags & (SLAB_STORE_USER|SLAB_RED_ZONE|__OBJECT_POISON)))
return;
// 初始化对象内存,比如填充 red zone,以及 poison
init_object(s, object, SLUB_RED_INACTIVE);
// 设置 SLAB_STORE_USER 起作用,初始化访问对象的所有者相关信息
init_tracking(s, object);
}
init_object 函数主要针对 slab 对象的内存区域进行布局,这里包括对 red zone 的填充,以及 POISON 对象的 object size 区域。
// 定义在文件:/include/linux/poison.h
#define SLUB_RED_INACTIVE 0xbb
// 定义在文件:/include/linux/poison.h
#define POISON_FREE 0x6b /* for use-after-free poisoning */
#define POISON_END 0xa5 /* end-byte of poisoning */
static void init_object(struct kmem_cache *s, void *object, u8 val)
{
// p 为真正存储对象的内存区域起始地址(不包含填充的 red zone)
u8 *p = object;
// red zone 位于真正存储对象内存区域 object size 的左右两侧,分别有一段 red zone
if (s->flags & SLAB_RED_ZONE)
// 首先使用 0xbb 填充对象左侧的 red zone
// 左侧 red zone 区域为对象的起始地址到 s->red_left_pad 的长度
memset(p - s->red_left_pad, val, s->red_left_pad);
if (s->flags & __OBJECT_POISON) {
// 将对象的内容用 0x6b 填充,表示该对象在 slub 中还未被使用
memset(p, POISON_FREE, s->object_size - 1);
// 对象的最后一个字节用 0xa5 填充,表示 POISON 的末尾
p[s->object_size - 1] = POISON_END;
}
// 在对象内存区域 object size 的右侧继续用 0xbb 填充右侧 red zone
// 右侧 red zone 的位置为:对象真实内存区域的末尾开始一个字长的区域
// s->object_size 表示对象本身的内存占用,s->inuse 表示对象在 slub 管理体系下的真实内存占用(包含填充字节数)
// 通常会在对象内存区域末尾处填充一个字长大小的 red zone 区域
// 对象右侧 red zone 区域后面紧跟着的就是 freepointer
if (s->flags & SLAB_RED_ZONE)
memset(p + s->object_size, val, s->inuse - s->object_size);
}
内核首先会用 0xbb 来填充对象左侧 red zone,长度为 kmem_cache-> red_left_pad。
随后内核会用 0x6b 填充 object size 内存区域,并用 0xa5 填充该区域的最后一个字节。object size 内存区域正是真正存储对象的区域。
最后用 0xbb 来填充对象右侧 red zone,右侧 red zone 的起始地址为:p + s->object_size,长度为:s->inuse - s->object_size。如下图所示:
总结
本文我们基于 slab cache 的完整的架构,近一步深入到内核源码中详细介绍了 slab cache 关于内存分配的完整流程:
我们可以看到 slab cache 内存分配的整个流程分为 fastpath 快速路径和 slowpath 慢速路径。
其中在 fastpath 路径下,内核会直接从 slab cache 的本地 cpu 缓存中获取内存块,这是最快的一种方式。
在本地 cpu 缓存没有足够的内存块可供分配的时候,内核就进入到了 slowpath 路径,而 slowpath 下又分为多种情况:
- 从本地 cpu 缓存 partial 列表中分配
- 从 NUMA 节点缓存中分配,其中涉及到了对本地 cpu 缓存的填充。
- 从伙伴系统中重新申请 slab
最后我们介绍了 slab 所在内存页的详细初始化流程,其中包括了对 slab freelist 链表的初始化,以及 slab 对象的初始化。
好了本文的内容到这里就结束了,感谢大家的收看,我们下篇文章见~~~