当我们的分析来到了slab分配器时,我们先总结一下之前的内核启动早期的内存的流程:
第一个阶段:Fixmap 固定映射阶段
setup_arch() → early_fixmap_init()
建立 fixmap 区域
通过 fixmap 可以动态建立更多页表项
此时内存管理: memblock(物理内存位图管理)
第二阶段:线性映射阶段
paging_init() → map_memory()
将所有物理内存线性映射到内核虚拟地址空间
映射关系: virt_addr = phys_addr + PAGE_OFFSET
此后,内核就可以通过虚拟地址访问所有的物理内存
第三阶段:Buddy 分配器初始化
mem_init() → free_area_init()
遍历 memblock 获取所有空闲物理内存信息
建立 free_area[] 数据结构
将可用页面加入 buddy 空闲链表
输出: 页面级分配器 ready,可以 alloc_pages()/free_pages()
第四阶段:Slab 分配器初始化(自举)
kmem_cache_init
静态变量: boot_kmem_cache, boot_kmem_cache_node
用 buddy 分配页面给第一个 slab
创建核心缓存:建立
kmem_cache和kmem_cache_node的对象缓存构建缓存体系:创建完整的 kmalloc 缓存族
输出:对象级缓存分配器 ready,可以kmalloc
各层职责:
Slab:管理小对象缓存,快速分配/释放
Buddy:管理物理页面,按 2^order 分配连续页面
线性映射:提供物理到虚拟地址的直接转换
Fixmap:保留用于特殊场景的动态映射
而本章我们要探索的就是slab分配器的自举!为什么说是自举呢?
什么是"自举"?
"自举"原意是 "拽着自己的鞋带把自己提起来"(pull oneself up by one's bootstraps),在计算机科学中指:
一个系统需要利用自己来初始化自己
这是一个经典的 "鸡生蛋,蛋生鸡" 问题。
Slab 分配器的自举困境
矛盾的核心:
// 我们需要分配内存来创建分配器
struct kmem_cache *cache = 分配内存(sizeof(struct kmem_cache));
// 但是分配内存需要分配器!
// kmalloc() 需要 slab,slab 需要 kmem_cache...具体困境:
要分配
struct kmem_cache,需要内存分配器要分配
struct kmem_cache_node,也需要内存分配器但是 slab 分配器还没初始化,无法分配这些结构
没有这些结构,slab 分配器无法工作
slab分配器的自举逻辑
定义静态变量
void __init kmem_cache_init(void)
{
// 定义静态数据段变量
static __initdata struct kmem_cache boot_kmem_cache, boot_kmem_cache_node;
//...
}第一阶段:创建静态启动缓存
伪装“slab”缓存对象
void __init kmem_cache_init(void)
{
//...
create_boot_cache(kmem_cache_node, "kmem_cache_node",
sizeof(struct kmem_cache_node), SLAB_HWCACHE_ALIGN, 0, 0);
create_boot_cache(kmem_cache, "kmem_cache",
offsetof(struct kmem_cache, node) +
nr_node_ids * sizeof(struct kmem_cache_node *),
SLAB_HWCACHE_ALIGN, 0, 0);
//...
}create_boot_cache的实现
void __init create_boot_cache(struct kmem_cache *s, const char *name,
unsigned int size, slab_flags_t flags,
unsigned int useroffset, unsigned int usersize)
{
int err;
unsigned int align = ARCH_KMALLOC_MINALIGN; //默认对齐是 128字节
s->name = name;
s->size = s->object_size = size;
/*
* For power of two sizes, guarantee natural alignment for kmalloc
* caches, regardless of SL*B debugging options.
*/
// 计算 align,首先如果size 是2的整数幂,那么最小的align 为初始值128,
// 如果不是2的整数幂,则通过calculate_alignment函数计算
if (is_power_of_2(size))
align = max(align, size);
s->align = calculate_alignment(flags, align, size);
s->useroffset = useroffset;
s->usersize = usersize;
// 创建slab 描述符的核心函数
err = __kmem_cache_create(s, flags);
if (err)
panic("Creation of kmalloc slab %s size=%u failed. Reason %d\n",
name, size, err);
// 初始化kmeme cache 引用计数为-1
s->refcount = -1; /* Exempt from merging for now */
}调__kmem_cache_create
int __kmem_cache_create(struct kmem_cache *s, slab_flags_t flags)
{
int err;
//创建剩余成员,例如 flags、random、size、object_size、oo、min、
// max、min_partial、cpu_partial、random_seq、node、cpu_slab 等等;
err = kmem_cache_open(s, flags);
if (err)
return err;
/* Mutex is not taken during early boot */
if (slab_state <= UP)
return 0;
//将slab 信息添加到 /sys/kernel/slab/kmem_cache_node和kmem_cache
err = sysfs_slab_add(s);
if (err) {
__kmem_cache_release(s);
return err;
}
if (s->flags & SLAB_STORE_USER)
debugfs_slab_add(s);
return 0;
}创建kmem_cache_node缓存
static int kmem_cache_open(struct kmem_cache *s, slab_flags_t flags)
{
// 根据 slub_debug 确定最终的flags,如果CONFIG_SLUB_DEBUG没有使能,返回值就是flags
s->flags = kmem_cache_flags(s->size, flags, s->name);
#ifdef CONFIG_SLAB_FREELIST_HARDENED
s->random = get_random_long();
#endif
// 根据object 大小,计算出最佳order值和这个slab缓存的object 数目,
// 然后初始化kmem_cache 结构中的oo、min、max,计算异常返回0,正常为1
if (!calculate_sizes(s, -1))
goto error;
// 开启slub debug后,disable_higher_order_debug 为1,否则为0
if (disable_higher_order_debug) {
/*
* Disable debugging flags that store metadata if the min slab
* order increased.
*/
if (get_order(s->size) > get_order(s->object_size)) {
s->flags &= ~DEBUG_METADATA_FLAGS;
s->offset = 0;
if (!calculate_sizes(s, -1))
goto error;
}
}
// 使能快速模式,后面slab缓存对象分配的时候用到
#if defined(CONFIG_HAVE_CMPXCHG_DOUBLE) && \
defined(CONFIG_HAVE_ALIGNED_STRUCT_PAGE)
if (system_has_cmpxchg_double() && (s->flags & SLAB_NO_CMPXCHG) == 0)
/* Enable fast mode */
s->flags |= __CMPXCHG_DOUBLE;
#endif
// 根据object size设定s->min_partial
set_min_partial(s, ilog2(s->size) / 2);
//设定s->cpu_partial
set_cpu_partial(s);
#ifdef CONFIG_NUMA
s->remote_node_defrag_ratio = 1000;
#endif
/* Initialize the pre-computed randomized freelist if slab is up */
if (slab_state >= UP) {
if (init_cache_random_seq(s))
goto error;
}
//初始化s->node,如成功则返回1
if (!init_kmem_cache_nodes(s))
goto error;
//初始化s->cpu_slab,如成功则返回1,然后退出kmem_cache_open函数
if (alloc_kmem_cache_cpus(s))
return 0;
error:
//如果初始化期间出现error,则释放描述符
__kmem_cache_release(s);
return -EINVAL;
}继续调init_kmem_cache_nodes
static int init_kmem_cache_nodes(struct kmem_cache *s)
{
int node;
for_each_node_mask(node, slab_nodes) {
struct kmem_cache_node *n;
// 当kmem_cache_node还没有初始化完成,slab_state默认为DOWN,故第一次使用early_kmem_cache_node_alloc
if (slab_state == DOWN) {
early_kmem_cache_node_alloc(node);
continue;
}
// 待kmem_cache_node初始化完成后,slab_state被设置为PARTIAL,因此后面的kmem_cache初始化或者创建新的slub描述符时走这里
n = kmem_cache_alloc_node(kmem_cache_node,
GFP_KERNEL, node);
if (!n) {
free_kmem_cache_nodes(s);
return 0;
}
init_kmem_cache_node(n);
s->node[node] = n;
}
return 1;
}当kmem_cache_node还没有初始化完成,slab_state默认为DOWN,故第一次使用early_kmem_cache_node_alloc
static void early_kmem_cache_node_alloc(int node)
{
struct page *page;
struct kmem_cache_node *n;
BUG_ON(kmem_cache_node->size < sizeof(struct kmem_cache_node));
// 通过调用 allocate_slab()从buddy中申请order阶数的page,并将page按照objects的数目打乱object list顺序
// 除此,通过new_slab()申请的页块还初始化了objects、slab_cache、inuse、frozen
page = new_slab(kmem_cache_node, GFP_NOWAIT, node);
BUG_ON(!page);
if (page_to_nid(page) != node) {
pr_err("SLUB: Unable to allocate memory from node %d\n", node);
pr_err("SLUB: Allocating a useless per node structure in order to be able to continue\n");
}
// 通过调用 allocate_slab()从buddy中申请order阶数的page,并将page按照objects的数目打乱object list顺序
// 除此,通过new_slab()申请的页块还初始化了objects、slab_cache、inuse、frozen
n = page->freelist;
BUG_ON(!n);
#ifdef CONFIG_SLUB_DEBUG
init_object(kmem_cache_node, n, SLUB_RED_ACTIVE);
init_tracking(kmem_cache_node, n);
#endif
n = kasan_slab_alloc(kmem_cache_node, n, GFP_KERNEL, false);
// 上面n 已经去到对象了,page->freelist指向下一个free object
page->freelist = get_freepointer(kmem_cache_node, n);
// page->inuse在new_slab()中初始化为page->objects,这里恢复,第一次使用所以该值为1
// 后面如果在申请page->inuse会自加
page->inuse = 1;
// 在new_slab()中进行冻结,这里表示slab进入正常使用了,解冻
page->frozen = 0;
kmem_cache_node->node[node] = n;
// 初始化该node的nr_partial和partial
init_kmem_cache_node(n);
inc_slabs_node(kmem_cache_node, node, page->objects);
/*
* No locks need to be taken here as it has just been
* initialized and there is no concurrent access.
*/
__add_partial(n, page, DEACTIVATE_TO_HEAD);
}allocate_slab分配page
static struct page *new_slab(struct kmem_cache *s, gfp_t flags, int node)
{
if (unlikely(flags & GFP_SLAB_BUG_MASK))
flags = kmalloc_fix_flags(flags);
WARN_ON_ONCE(s->ctor && (flags & __GFP_ZERO));
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;
struct kmem_cache_order_objects oo = s->oo;
gfp_t alloc_gfp;
void *start, *p, *next;
int idx;
bool shuffle;
flags &= gfp_allowed_mask;
flags |= s->allocflags;
/*
* Let the initial higher-order allocation fail under memory pressure
* so we fall-back to the minimum order allocation.
*/
alloc_gfp = (flags | __GFP_NOWARN | __GFP_NORETRY) & ~__GFP_NOFAIL;
if ((alloc_gfp & __GFP_DIRECT_RECLAIM) && oo_order(oo) > oo_order(s->min))
alloc_gfp = (alloc_gfp | __GFP_NOMEMALLOC) & ~(__GFP_RECLAIM|__GFP_NOFAIL);
// 从buddy 分配器中分配页块
page = alloc_slab_page(s, alloc_gfp, node, oo);
if (unlikely(!page)) {
oo = s->min;
alloc_gfp = flags;
/*
* Allocation may have failed due to fragmentation.
* Try a lower order alloc if possible
*/
page = alloc_slab_page(s, alloc_gfp, node, oo);
if (unlikely(!page))
goto out;
stat(s, ORDER_FALLBACK);
}
// page申请成功,初始化page数据结构中slub相关的成员
// 这里更新page->objects,标记页块的objects数量
page->objects = oo_objects(oo);
account_slab_page(page, oo_order(oo), s, flags);
page->slab_cache = s;
__SetPageSlab(page); // 标记页块为slab专用
if (page_is_pfmemalloc(page))
SetPageSlabPfmemalloc(page); //标记页块为prmemalloc
kasan_poison_slab(page);
// 获取page 虚拟地址
start = page_address(page);
setup_page_debug(s, page, start);
//需要使能CONFIG_SLAB_FREELIST_RANDOM,主要是打乱slab中object,形成一个随机的object list,
//函数shuffle_freelist()除了上面这个功能,还会对页块中object进行setup
//因为slab空间是连续的,这样做降低可预测性,若objects >= 2,都会返回true
//如果没有使能config,则返回false
shuffle = shuffle_freelist(s, page);
if (!shuffle) {
start = fixup_red_left(s, start);
start = setup_object(s, page, start);
page->freelist = start;
for (idx = 0, p = start; idx < page->objects - 1; idx++) {
next = p + s->size;
next = setup_object(s, page, next);
set_freepointer(s, p, next);
p = next;
}
set_freepointer(s, p, NULL);
}
page->inuse = page->objects;
page->frozen = 1;
out:
if (!page)
return NULL;
inc_slabs_node(s, page_to_nid(page), page->objects);
return page;
}调用buddy接口分配page
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;
// 获取需要分配的order
unsigned int order = oo_order(oo);
if (node == NUMA_NO_NODE)
page = alloc_pages(flags, order);
else
page = __alloc_pages_node(node, flags, order); // 调用buddy分配器的接口
return page;
}此时:
有了 slab 页面(物理页面)
页面被划分为多个 slab 对象
freelist 指向第一个可用的 slab 对象
可以从这个 slab 分配对象了
第二阶段:引导创建动态缓存
在第一阶段结束后,会设置
slab_state = PARTIAL; kmem_cache = bootstrap(&boot_kmem_cache);
kmem_cache_node = bootstrap(&boot_kmem_cache_node);static struct kmem_cache * __init bootstrap(struct kmem_cache *static_cache)
{
int node;
// 通过kmem_cache_zalloc() 从之前创建好的kmem_cache 中申请slub 空间
struct kmem_cache *s = kmem_cache_zalloc(kmem_cache, GFP_NOWAIT);
struct kmem_cache_node *n;
// 将描述符信息通过memcpy() 拷贝到新申请的空间中
memcpy(s, static_cache, kmem_cache->object_size);
/*
* This runs very early, and only the boot processor is supposed to be
* up. Even if it weren't true, IRQs are not up so we couldn't fire
* IPIs around.
*/
// 刷新cpu的slab信息,主要更新c->page,c->freelist和c->partial
__flush_cpu_slab(s, smp_processor_id());
// 循环遍历s->node数组中的所有node,然后遍历每个node上面的partial链表,
// 修改上面的struct page的slab_cache指针,指向当前新申请的kmem_cache
for_each_kmem_cache_node(s, node, n) {
struct page *p;
list_for_each_entry(p, &n->partial, slab_list)
p->slab_cache = s;
#ifdef CONFIG_SLUB_DEBUG
list_for_each_entry(p, &n->full, slab_list)
p->slab_cache = s;
#endif
}
// 将kmem_cache添加到全局slab_caches链表中
list_add(&s->list, &slab_caches);
return s;
}分配新的 kmem_cache 结构
struct kmem_cache *s = kmem_cache_zalloc(kmem_cache, GFP_NOWAIT);
这里有三个容易混的东西:
static_cache:传进来的,是 静态的 boot_kmem_cache / boot_kmem_cache_node。全局变量
kmem_cache:此时仍然指向boot_kmem_cache,是“用于存放 struct kmem_cache 的 cache”。局部变量
s:从kmem_cache这个 cache 里新分配出的一个 struct kmem_cache 对象。
也就是说:
用 临时的 cache(
kmem_cache = &boot_kmem_cache)
从 它自己 管理的 slab 里,malloc 出一个新的 struct kmem_cache,叫s。
这一步的底层实际还是走 SLUB 的页分配逻辑(allocate_slab() → new_slab() → alloc_pages()),只是当前的 s 还没完全接入全局 list。
把静态描述符“深拷贝”到新对象里
memcpy(s, static_cache, kmem_cache->object_size);这里有几点细节:
kmem_cache->object_size就是 一个 struct kmem_cache 的大小(加上 debug 扩展时也可能略有差别,但总之能覆盖整个结构)。static_cache和kmem_cache当前其实都是指向boot_kmem_cache,所以可以理解为:“把 boot_kmem_cache 这个临时的 struct kmem_cache 原封不动复制 到新分配出来的
s里”。复制后,
s里已经包含:名字、flags、object_size、size、对齐等属性;
指向 per-node
kmem_cache_node数组的指针;指向 percpu
struct kmem_cache_cpu的指针s->cpu_slab;等等。
换句话说,s 是一个 完全克隆出来的“真正对象”版 kmem_cache 描述符。
刷新 cpu 的 slab 信息
__flush_cpu_slab(s, smp_processor_id());__flush_cpu_slab()做的事情(看实现):找到
s->cpu_slab里的struct kmem_cache_cpu *c;如果
c->page非空,调用flush_slab(),把 per-CPU freelist 上残留的对象都退回到对应的struct page;然后
unfreeze_partials()把 per-CPU partial slabs 挪回每个 node 的kmem_cache_node::partial链表;最后把
c->page、c->freelist、c->partial清空。
为什么要在 bootstrap 阶段做这个?
因为这时所有 per-CPU 的 slab 状态(
c->page、partial 链表等)里,页的page->slab_cache指针还是指向旧的static_cache。后面我们会去修改各个
struct page里的slab_cache指针,如果不先把 per-CPU 的东西刷回去,可能会有“挂在 per-CPU 上但指针还没改”的残留状态。
注意这里传的是新对象 s,但因为我们刚刚 memcpy(s, static_cache, ...),s->cpu_slab 和旧的相同,所以刷的是同一套 per-CPU 数据。
修正所有 slab 页的 slab_cache 指针
// 循环遍历s->node数组中的所有node,然后遍历每个node上面的partial链表,
// 修改上面的struct page的slab_cache指针,指向当前新申请的kmem_cache
for_each_kmem_cache_node(s, node, n) {
struct page *p;
list_for_each_entry(p, &n->partial, slab_list)
p->slab_cache = s;
#ifdef CONFIG_SLUB_DEBUG
list_for_each_entry(p, &n->full, slab_list)
p->slab_cache = s;
#endif
}
这里的逻辑是:
for_each_kmem_cache_node(s, node, n)遍历s对应的所有 NUMA node 上的kmem_cache_node。s->node[node]数组是create_boot_cache()时就准备好的,刚刚已经被 memcpy 到s。对每个 node:
先遍历
n->partial链表上的所有struct page *p;如果编译了
CONFIG_SLUB_DEBUG,再遍历n->full链表。
对每一个
page,把p->slab_cache从原来的static_cache改成新对象s。
为什么只改 partial/full,而不改 per-CPU 链表上那堆?
上一步
__flush_cpu_slab()已经把 per-CPU 的 slab 状态全部“刷新”回 node 层,per-CPU 层不再持有 slab 页了。所以现在所有 slab 页要么在
n->partial要么在n->full,逐个改它们的slab_cache就够了。
这一步完成之后:
所有属于这个 cache 的 slab 页(不论 partial 还是 full),
它们的page->slab_cache都统一指向了新的s,
再也没人指向那个静态的boot_kmem_cache了。
把新 cache 挂到全局 slab_caches 链表
// 将kmem_cache添加到全局slab_caches链表中
list_add(&s->list, &slab_caches);slab_caches 是 SLUB 维护的“所有 slab cache 的全局链表”,后面 debug、收缩 (kmem_cache_shrink())、统计等功能都会遍历这个链表。
这里直接
list_add()不加锁,是因为还在 早期启动阶段,只有 boot CPU、且slab_state <= PARTIAL,SLUB 的注释里也明确写了 “Mutex is not taken during early boot”。静态的
boot_kmem_cache并 不会 被挂到slab_caches上,它只是启动阶段的“踏脚石”。
之后,kmem_cache_init() 里会用返回值覆盖全局指针:
kmem_cache = bootstrap(&boot_kmem_cache);
kmem_cache_node = bootstrap(&boot_kmem_cache_node);从这一刻起:
kmem_cache 指向“真正的、动态分配的 struct kmem_cache 对象 s”;
kmem_cache_node 也同理;
所有之前已经分配出来的 slab 页、per-CPU 状态都已经指向 s;
SLUB 的“专用 cache of kmem_cache / kmem_cache_node”算是完全自举完成。
bootstrap总结
把这段函数和 ARM64 启动时机放一起看,可以这么理解:
mem_init()之后,buddy allocator 就绪;kmem_cache_init()用create_boot_cache()在 静态 struct kmem_cache 上“硬编码”了一套最小可用的 SLUB cache(只支持 kmem_cache / kmem_cache_node 两种对象);bootstrap()做四件事:用这套临时 cache self-hosting 地
kmem_cache_zalloc()出一个新的struct kmem_cache;把静态
boot_kmem_cache的内容 memcpy 进去;刷干净当前 CPU 的 slab 状态,并修正所有
page->slab_cache指针指向新的s;把
s加入全局slab_caches链表。
然后用返回的
s覆盖全局指针kmem_cache / kmem_cache_node,
接下来创建各种kmalloc-xxcache、普通模块自己的 cache,就都走正常的 SLUB 创建路径了。