前言
前一篇文章对 linux5.10 的 slab 分配器底层实现进行了探究与学习。进一步地,本篇文章将对 linux5.10 的 slub 分配器进行探究,对比看看两者的实现有何不同,做了哪些"必要的"改进。
slub 分配器
关于 slub 分配器的基本原理与 slab 分配器类似,只是相较于 slab 更快更直接以及更简单,主要对涉及的结构体进行学习,再以 kmalloc 为入口开始探究,并对比一下初始化过程的不同
主要数据结构
struct kmem_cache {struct kmem_cache_cpu __percpu *cpu_slab;/* Used for retrieving partial slabs, etc. */slab_flags_t flags;unsigned long min_partial;unsigned int size; /* The size of an object including metadata */unsigned int object_size;/* The size of an object without metadata */struct reciprocal_value reciprocal_size;unsigned int offset; /* Free pointer offset */struct kmem_cache_order_objects oo;/* Allocation and freeing of slabs */struct kmem_cache_order_objects max;struct kmem_cache_order_objects min;gfp_t allocflags; /* gfp flags to use on each alloc */int refcount; /* Refcount for slab cache destroy */void (*ctor)(void *);unsigned int inuse; /* Offset to metadata */unsigned int align; /* Alignment */unsigned int red_left_pad; /* Left redzone padding size */const char *name; /* Name (only for display!) */struct list_head list; /* List of slab caches */
};struct kmem_cache_cpu {void **freelist; /* Pointer to next available object */unsigned long tid; /* Globally unique transaction id */struct page *page; /* The slab from which we are allocating */
#ifdef CONFIG_SLUB_CPU_PARTIALstruct page *partial; /* Partially allocated frozen slabs */
#endif
#ifdef CONFIG_SLUB_STATSunsigned stat[NR_SLUB_STAT_ITEMS];
#endif
};
struct kmem_cache_node {spinlock_t list_lock;unsigned long nr_partial;struct list_head partial;
}
struct page {unsigned long flags; /* Atomic flags, some possiblystruct list_head slab_list;struct kmem_cache *slab_cache; /* not slob *//* Double-word boundary */void *freelist; /* first free object */struct { /* SLUB */unsigned inuse:16;unsigned objects:15;unsigned frozen:1;};atomic_t _refcount;
};
其中,page 用了很多共同体,笔者将其简化只留下与 slub 相关的。对比 slab 分配器数据结构,主要不同在于:
- slub 本地缓存 kmem_cache_cpu 相较于 slab 的 array_cache ,由存储 obj 指针的数组变为直接存储一整个 slub ,并添加了 partial 串联小部分可用 slub。该改变意味着本地缓存可分配容量变大,当需要分配内存时,基本上都能够通过本地缓存进行分配,缩短了分配路径提升了分配效率
- kmem_cache_node 结构被简化,由多链表变成单链表,且没有共享缓存,进一步缩短了分配路径
- 丢弃了 color 着色
对此,绘制关系图如下

根据上述结构关系图,简单地描述一下 slub 分配器的分配过程,由于 slub 不在用 obj 指针数组来管理本地缓存,因此分配内存的流程会有所变化
- 首先根据 size 从 kmem_caches 中获取对应的 kmem_cache
- 从 kmem_cache 中获取本地缓存 kmem_cache_cpu ,根据其 freelist 可得到空闲 obj 的地址,该 obj 对应页由 page 指针记录
- 如果 freelist 为空说明当前 page 已经被分配完,则搜索 partial 关联的 slub 链表,看是否有空闲 obj ,有则将 freelist 指向,并修改 page 指针指向该 sub 所在页
- 如果 partial 依然没有空闲 slub ,则进入 kmem_cache_node 的 partial 链表进行进一步地搜索。如果搜索到空闲的 obj ,则只需要直接返回该 obj 地址,同时修改 kmem_cache_cpu 中 freelist 和 page 指针指向。
- 同时,判断 kmem_cache_cpu 中的 partial 串联的 slub 数量是否满足 slub_cpu_partial ,如果不满足则申请 page 并初始化为 slub 并添加到 kmem_cache_cpu 的 partial 中
从上述描述中,可以明显地感觉到, slub 并不需要对本地缓存进行过多的维护,对于 slab 而言,却需要总是更新 obj 指针数组的下标。
并且,可以看出本地缓存 kmem_cache_cpu 的 partial 一旦被初始化到指定数量后,就不会进行变更。变的只有 freelist 和 page 指针。意味着,本地缓存当前所使用的 slub 可能来自 kmem_cache_node 的 partial,也可能来自 kmem_cache_node 的 partial。且如果当前 slub 被分配完,搜寻下一个空闲 obj 的顺序仍然是先搜寻本地缓存的 partial ,再到 kmem_cache_node 的 partial
kmalloc
与 slub 的探究思路类似,从 kmalloc 入口进入,看看 slub 分配器如何进行内存分配的。
kmalloc => __kmalloc => __do_kmalloc\--- kmalloc_slab : 根据 size 从 kmem_caches 中获取相应 kmem_cache--- slab_alloc => slab_alloc_node--- raw_cpu_ptr : 获取本地缓存--- __slab_alloc => ___slab_alloc : 进一步获取obj--- kasan_kamlloc
kmalloc_slab 根据传入参数 size ,从 kmalloc_caches 获取相应的 kmem_cache,比较简单
struct kmem_cache *kmalloc_slab(size_t size, gfp_t flags)
{unsigned int index;if (size <= 192) {if (!size)return ZERO_SIZE_PTR;index = size_index[size_index_elem(size)];} else {if (WARN_ON_ONCE(size > KMALLOC_MAX_CACHE_SIZE))return NULL;index = fls(size - 1);}return kmalloc_caches[kmalloc_type(flags)][index];
}
slab_alloc_node 相较于 slab 分配器的 ____cache_alloc 就简单了许多。
- 先是通过 raw_cpu_ptr 获取当前 cpu 对应的本地缓存 kmem_cache_cpu ,并从中获取可用的 obj(即 freelist 指向的地址)
- 如果没有则调用 __slab_alloc 进入 本地缓存链表 partial 进行获取,还是没有则从 kmem_cache_node 的 partial 获取,再没有只能申请新的 page 并初始化 slub
static __always_inline void *slab_alloc_node(struct kmem_cache *s,gfp_t gfpflags, int node, unsigned long addr)
{void *object;struct kmem_cache_cpu *c;struct page *page;unsigned long tid;struct obj_cgroup *objcg = NULL;s = slab_pre_alloc_hook(s, &objcg, 1, gfpflags);if (!s)return NULL;
redo:do {tid = this_cpu_read(s->cpu_slab->tid);c = raw_cpu_ptr(s->cpu_slab);} while (IS_ENABLED(CONFIG_PREEMPTION) &&unlikely(tid != READ_ONCE(c->tid)));barrier();object = c->freelist;page = c->page;if (unlikely(!object || !page || !node_match(page, node))) {object = __slab_alloc(s, gfpflags, node, addr, c);} // 省略部分代码return object;
}
假设本地缓存的 freelist 中已经获取不到空闲 obj,则需要通过 __slab_alloc 进行进一步地获取,该函数的调用阶段主要如下所示
__slab_alloc--- redo:--- get_freelist--- load_freelist:--- new_slab:--- slub_percpu_partial--- new_slab_objects--- get_partial--- get_partial_node
- 调用 slub_percpu_partial 先从本地缓存的 partial 链表上进行空闲 obj 的获取
- 如果获取不到,则调用 new_slab_objects–>get_partial_node 进入 kmem_cache_node 的 partial 链表进行获取
- 获取到的 obj 地址要同步到 kmem_cache_cpu 的 freelist 上,并返回
static void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,unsigned long addr, struct kmem_cache_cpu *c)
{void *freelist;struct page *page;stat(s, ALLOC_SLOWPATH);page = c->page;if (!page) {/** if the node is not online or has no normal memory, just* ignore the node constraint*/if (unlikely(node != NUMA_NO_NODE &&!node_state(node, N_NORMAL_MEMORY)))node = NUMA_NO_NODE;goto new_slab;}
redo:if (unlikely(!node_match(page, node))) {/** same as above but node_match() being false already* implies node != NUMA_NO_NODE*/if (!node_state(node, N_NORMAL_MEMORY)) {node = NUMA_NO_NODE;goto redo;} else {stat(s, ALLOC_NODE_MISMATCH);deactivate_slab(s, page, c->freelist, c);goto new_slab;}}/** By rights, we should be searching for a slab page that was* PFMEMALLOC but right now, we are losing the pfmemalloc* information when the page leaves the per-cpu allocator*/if (unlikely(!pfmemalloc_match(page, gfpflags))) {deactivate_slab(s, page, c->freelist, c);goto new_slab;}/* must check again c->freelist in case of cpu migration or IRQ */freelist = c->freelist;if (freelist)goto load_freelist;freelist = get_freelist(s, page);if (!freelist) {c->page = NULL;stat(s, DEACTIVATE_BYPASS);goto new_slab;}stat(s, ALLOC_REFILL);load_freelist:/** freelist is pointing to the list of objects to be used.* page is pointing to the page from which the objects are obtained.* That page must be frozen for per cpu allocations to work.*/VM_BUG_ON(!c->page->frozen);c->freelist = get_freepointer(s, freelist);c->tid = next_tid(c->tid);return freelist;new_slab:if (slub_percpu_partial(c)) {page = c->page = slub_percpu_partial(c);slub_set_percpu_partial(c, page);stat(s, CPU_PARTIAL_ALLOC);goto redo;}freelist = new_slab_objects(s, gfpflags, node, &c);if (unlikely(!freelist)) {slab_out_of_memory(s, gfpflags, node);return NULL;}page = c->page;if (likely(!kmem_cache_debug(s) && pfmemalloc_match(page, gfpflags)))goto load_freelist;/* Only entered in the debug case */if (kmem_cache_debug(s) &&!alloc_debug_processing(s, page, freelist, addr))goto new_slab; /* Slab failed checks. Next slab needed */deactivate_slab(s, page, get_freepointer(s, freelist), c);return freelist;
}
具体地,slub_percpu_partial 如果能获取到可用的 slub ,则直接修改 kmem_cache_cpu 的 page 指针指向该 slub ,然后进入 redo 阶段,将该 page 的 freelist 拿出来,并作为 kmem_cache_cpu 新的 freelist ,进行返回
如果 slub_percpu_partial 获取不到 slub ,则调用 new_slab_objects 进行获取。其调用链如下
new_slab_objects--- get_partial--- get_partial_node--- get_any_partial--- new_slab
首先,调用 get_partial 进行 kmem_cache_node 关联的 partial 搜索。其中先搜索当前 cpu 对应的 partial ,调用 get_partial_node,如果搜索不到,则调用 get_any_partial 搜索其他 node 上的 partial,还搜不到则通过 new_slab 申请新的 slub
要注意的是,在获取到 obj 地址后,需要将对应 page 的 freelist 置空,并修改 kmem_cache_cpu 的 page 指针,后续也会将 kmem_cache_cpu 的 freelist 指向获取到的 obj 地址
static inline void *new_slab_objects(struct kmem_cache *s, gfp_t flags,int node, struct kmem_cache_cpu **pc)
{void *freelist;struct kmem_cache_cpu *c = *pc;struct page *page;WARN_ON_ONCE(s->ctor && (flags & __GFP_ZERO));freelist = get_partial(s, flags, node, c);if (freelist)return freelist;page = new_slab(s, flags, node);if (page) {c = raw_cpu_ptr(s->cpu_slab);if (c->page)flush_slab(s, c);/** No other reference to the page yet so we can* muck around with it freely without cmpxchg*/freelist = page->freelist;page->freelist = NULL;stat(s, ALLOC_SLAB);c->page = page;*pc = c;}return freelist;
}
更细节的是,在遍历 partial 的过程中,会进行本地缓存 kmem_cache_cpu 的 partial 填充,直到其满足 available > slub_cpu_partial(s) / 2。要注意的是,尽管填充了 kmem_cache_cpu 的 partial ,但分配的空闲 obj 是 kmem_cache_node 的
static void *get_partial_node(struct kmem_cache *s, struct kmem_cache_node *n,struct kmem_cache_cpu *c, gfp_t flags)
{struct page *page, *page2;void *object = NULL;unsigned int available = 0;int objects;if (!n || !n->nr_partial)return NULL;spin_lock(&n->list_lock);list_for_each_entry_safe(page, page2, &n->partial, slab_list) {void *t;if (!pfmemalloc_match(page, flags))continue;t = acquire_slab(s, n, page, object == NULL, &objects);if (!t)break;available += objects;if (!object) {c->page = page;stat(s, ALLOC_FROM_PARTIAL);object = t;} else {put_cpu_partial(s, page, 0);stat(s, CPU_PARTIAL_NODE);}if (!kmem_cache_has_cpu_partial(s)|| available > slub_cpu_partial(s) / 2)break;}spin_unlock(&n->list_lock);return object;
}
static int transfer_objects(struct array_cache *to,struct array_cache *from, unsigned int max)
{/* Figure out how many entries to transfer */int nr = min3(from->avail, max, to->limit - to->avail);if (!nr)return 0;memcpy(to->entry + to->avail, from->entry + from->avail -nr,sizeof(void *) *nr);from->avail -= nr;to->avail += nr;return nr;
}
对于 new_slab ,主要做的事是申请一页page,并根据 slub 的要求进行初始化,在此就不展开。需要留意的是申请好了 slub 之后,似乎并没有将其关联到前述两类 partial 中,而是将地址直接返回,并更新 kmem_cache_cpu 的 page 指针。
至此,slub 的分配与扩容梳理完成
















