_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 本地缓存的 sl