概述
Linux初始化通过bootmem
/memblock
引导内存分配进行内存管理,支持buddy system
完成相关初始化后,将物理内存分配的功能转交给buddy system
,buddy system
是以page
为单位分配方式。
对于内核要经常创建的对象, 如task_struct
,fs_struct
, mm_struct
通常会放到高速缓存中,保留基本结构,从而可以重复使用他们, 这里需要用到slab
分配器。
slab
分配器
- 通过
buddy system
申请空闲页 - 将申请到页处理为更小的分配单元,为其他子系统提供缓冲区存放内核对象
- 缓存经常使用的对象,释放后保存器初始状态,再次分配对象速度会很快
- 充分利用硬件高速缓存
历史发展
- 1991 Initial K&R allocator
- 1996 SLAB allocator
- 2003 SLOB allocator
- 2004 NUMA SLAB
- 2007 SLUB allocator
- 2008 SLOB mulitlist
- 2011 SLUB fastpath rework
- 2013 Common slab code
- 2014 SLUBification of SLAB
…
常用的分配器
- SLOB: K&R allocator (1991-1999)
- SLAB: Solaris type allocator (1999-2008)
- SLUB: Unqueued allocator (2008-today
SLOB
- 简单的空闲对象列表管理
- 遍历列表查找合适的空间,没有的话申请向伙伴系统申请page增加堆栈大小.
- 碎片化严重
- 优化: 按照不同大小多个链表,减少碎片。
原理图
可以看到通过page
中的freelist
将一个pageframe
中的空闲块一个一个链接起来.
具体实现
涉及文件
slob初期代码可以查看 slob: introduce the SLOB allocator
/mm/slab.h
/include/linux/slab.h
/mm/slob.c
/mm/slab_common.c 与分配策略无关
数据结构
标准的kmem_cache
文件结构
struct kmem_cache {
...
}
SLOB
原有结构在/mm/slob.c
struct slob_block {
slobidx_t units;
};
typedef struct slob_block slob_t;
#define SLOB_BREAK1 256
#define SLOB_BREAK2 1024
static LIST_HEAD(free_slob_small);
static LIST_HEAD(free_slob_medium);
static LIST_HEAD(free_slob_large);
可以看在原有一条freelist
的基础上增加为了三条free
slob*
, 根据slab
大小, 通过大小256
,1024
分为三段。
相关代码
- 核心流程首先从
free_slob_small
,free_slob_medium
,free_slob_large
,通过slob_page_alloc
查找空闲块, 没有的话通过slob_new_pages
申请新页
static void *slob_alloc(size_t size, gfp_t gfp, int align, int node,
int align_offset)
{
...
if (size < SLOB_BREAK1)
slob_list = &free_slob_small;
else if (size < SLOB_BREAK2)
slob_list = &free_slob_medium;
else
slob_list = &free_slob_large;
list_for_each_entry(sp, slob_list, slab_list) {
...
b = slob_page_alloc(sp, size, align, align_offset,\
&page_removed_from_list);
...
}
if (!b) {
b = slob_new_pages(gfp & ~__GFP_ZERO, 0, node);
...
}
}
- 支持NUMA,新版本可以看到相关节点参数
#ifdef CONFIG_NUMA
void *__kmalloc_node(size_t size, gfp_t gfp, int node)
{
return __do_kmalloc_node(size, gfp, node, _RET_IP_);
}
EXPORT_SYMBOL(__kmalloc_node);
void *kmem_cache_alloc_node(struct kmem_cache *cachep, gfp_t gfp, int node)
{
return slob_alloc_node(cachep, gfp, node);
}
EXPORT_SYMBOL(kmem_cache_alloc_node);
#endif
- 根据
PAGE_SIZE
判断使用slob
还是buddy system
static void *slob_alloc_node(struct kmem_cache *c, gfp_t flags, int node)
{
...
if (c->size < PAGE_SIZE) {
b = slob_alloc(c->size, flags, c->align, node, 0);
...
} else {
b = slob_new_pages(flags, get_order(c->size), node);
...
}
...
}
SLAB
- 本地队列存储热点数据
- 每个cpu和每个节点分配队列。
- 远程节点的队列。
- 复杂的数据结构。
- 基于对象的内存策略。
- 指数增长节点*nr_cpus, 大量的内存被缓存。
- 每个处理器都必须每2HZ扫描其每个slab缓存的队列。
原理图
- 针对一个页面中的free
object
的管理, 使用freelist
做为索引数组 page->active
保存了索引数组的最大长度。- 通过设置
Coloring
更好支持硬件缓存, 将对象放置在slab
中的不同起始偏移处,使得这些对象落在CPU
高速缓存中不同的行,减少碰撞(collide
)。
- 每个
cpu
分配一个array_cache
缓存结构,被称为cpu_cache
, (cpu_cache 是per-CPU
变量, 缓存可以不使用自旋锁,从而增加处理效率) - 针对
NUMA
结构,缓存结构针对每个节点为了一个节点缓存kmem_cache_node
, 通过三个queue
(parital_list
,full_list
,empty_list
)维护slab
信息。 CPU
Cache有每个CPU
单独使用的L1 cache
,L2 cache
,共享使用的L3 cache
, 结构中维护了本地节点per-CPU
的array_cache, 节点共享的缓冲kmem_cache_node
的shared
,及远程节点缓冲的alien
。RedZone
调试标识, 每一个对象末尾, 增加这个标识, 如果这个标识被修改, 判断是否发生缓冲区溢出last caller
调试标识,位于RedZone
之后,2.6
版本引入, 用于存放析构对象函数.Poinsing
调试标识,通过创建和释放slab
,将要感染的对象填充0x5A
, 分配时检查, 如果被改变, 就知道之前是否使用过Payload
就是缓存的object
在分析shared
, alien
过程中发现一个特殊数字0xbaadf00d
,0xBAADF00D
–bad food
在微软的LocalAlloc
–LMEM_FIXED
函数中使用,用以在已启用调试堆的情况下,标识未初始化的分配堆内存。
来自 wiki – Hexspeak,看来贡献者应该有对windows
开发比较了解。
具体实现
涉及文件
现有版本中涉及文件(v5.8-rc3)
/mm/slab.h
/include/linux/slab_def.h
/mm/slab.c
/mm/slab_common.c 与分配策略无关
数据结构
可以看到缓冲主要分为两部分, 一部分是per-CPU
变量, 一部分保存的是节点缓存
struct kmem_cache {
struct array_cache __percpu *cpu_cache;
...
struct kmem_cache_node *node[MAX_NUMNODES];
};
节点缓存区使用了slabs_partial
, slabs_full
, slabs_free
管理slabs
struct kmem_cache_node {
spinlock_t list_lock;
...
struct list_head slabs_partial;
struct list_head slabs_full;
struct list_head slabs_free;
...
};
下面这个是CPU
缓存结构
struct array_cache {
unsigned int avail;
unsigned int limit;
unsigned int batchcount;
unsigned int touched;
void *entry[];
};
可以看到默认情况下, 其他CPU的缓存,alien
缓存, 特别的多出一个自旋锁lock
struct alien_cache {
spinlock_t lock;
struct array_cache ac;
};
static int use_alien_caches __read_mostly = 1;
static int __init noaliencache_setup(char *s)
{
use_alien_caches = 0;
return 1;
}
相关代码
freelist
管理相关
下面是初始化时的代码片段在文件/mm/slab.c
中, 可以看到对page->freelist
和page->s_mem
的设置,
static void cache_init_objs(struct kmem_cache *cachep,
struct page *page)
{
...
if (!shuffled && OBJFREELIST_SLAB(cachep)) {
page->freelist = index_to_obj(cachep, page, cachep->num - 1) +
obj_offset(cachep);
}
...
for (i = 0; i < cachep->num; i++) {
objp = index_to_obj(cachep, page, i);
...
if (!shuffled)
set_free_obj(page, i, i);
}
...
}
static inline void set_free_obj(struct page *page,
unsigned int idx, freelist_idx_t val)
{
((freelist_idx_t *)(page->freelist))[idx] = val;
}
static inline void *index_to_obj(struct kmem_cache *cache,
struct page *page, unsigned int idx)
{
return page->s_mem + cache->size * idx;
}
通过page->active
设置索引数组的大小
static void *slab_get_obj(struct kmem_cache *cachep, struct page *page)
{
void *objp;
objp = index_to_obj(cachep, page, get_free_obj(page, page->active));
page->active++;
return objp;
}
static void slab_put_obj(struct kmem_cache *cachep,
struct page *page, void *objp)
{
unsigned int objnr = obj_to_index(cachep, page, objp);
...
page->active--;
if (!page->freelist)
page->freelist = objp + obj_offset(cachep);
set_free_obj(page, page->active, objnr);
}
- 缓存分配
可以看到分配顺序是先取cpu
缓存, 通过cache_alloc_refill
后还有一个回填到CPU
缓存的动作。
static inline void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
...
ac = cpu_cache_get(cachep);
...
objp = cache_alloc_refill(cachep, flags);
ac = cpu_cache_get(cachep);
...
return objp;
}
CPU
缓冲处理很简单高效
static inline struct array_cache *cpu_cache_get(struct kmem_cache *cachep)
{
return this_cpu_ptr(cachep->cpu_cache);
}
如果不使用cpu
缓存, 就判断节点share
出来的缓存通过transfer_objects
直接拷贝了, 否则话就会通过上面提到的slab_paritial
或者slab_free
将进行batchcount
分配, 如果还是没有就直接调用cache_grow_begin
申请物理页面了。
static void *cache_alloc_refill(struct kmem_cache *cachep, gfp_t flags)
{
...
shared = READ_ONCE(n->shared);
if (!n->free_objects && (!shared || !shared->avail))
goto direct_grow;
shared = READ_ONCE(n->shared);
if (shared && transfer_objects(ac, shared, batchcount)) {
shared->touched = 1;
goto alloc_done;
}
while (batchcount > 0) {
page = get_first_slab(n, false);
if (!page)
goto must_grow;
...
batchcount = alloc_block(cachep, ac, page, batchcount);
fixup_slab_list(cachep, n, page, &list);
}
must_grow:
n->free_objects -= ac->avail;
alloc_done:
spin_unlock(&n->list_lock);
fixup_objfreelist_debug(cachep, &list);
direct_grow:
...
page = cache_grow_begin(cachep, gfp_exact_node(flags), node);
ac = cpu_cache_get(cachep);
if (!ac->avail && page)
alloc_block(cachep, ac, page, batchcount);
cache_grow_end(cachep, page);
...
}
...
return ac->entry[--ac->avail];
}
static int transfer_objects(struct array_cache *to,
struct array_cache *from, unsigned int max)
{
...
memcpy(to->entry + to->avail, from->entry + from->avail -nr,
sizeof(void *) *nr);
...
}
针对跨节点情况, 如果是设置了SLAB_MEM_SPREAD
, 分散到各个cpu
缓存进行分配, 可以使用cpuset_slab_spread_node
, mempolicy_slab_node
两个方法得到节点编号, 通过 __cache_alloc_node
进行分配。
static void *alternate_node_alloc(struct kmem_cache *cachep,
gfp_t flags)
{
...
nid_alloc = nid_here = numa_mem_id();
if (cpuset_do_slab_mem_spread() && (cachep->flags & SLAB_MEM_SPREAD))
nid_alloc = cpuset_slab_spread_node();
else if (current->mempolicy)
nid_alloc = mempolicy_slab_node();
...
if (nid_alloc != nid_here)
return ____cache_alloc_node(cachep, flags, nid_alloc);
return NULL;
}
- 缓冲扫描 ,
2HZ
扫描一次
#define REAPTIMEOUT_AC (2*HZ)
#define REAPTIMEOUT_NODE (4*HZ)
static void cache_reap(struct work_struct *w)
{
...
schedule_delayed_work_on(smp_processor_id(), work,
round_jiffies_relative(REAPTIMEOUT_AC));
}
我的机器 1s = 100HZ, 所以 1HZ 约等于 10
ms, 也就是20ms
进行以此缓冲扫描回收空闲对象
[root@centosgpt ~]# cat /proc/interrupts | grep timer && \
sleep 1 && cat /proc/interrupts | grep timer
0: 4 0 IO-APIC 2-edge timer
LOC: 8475 8291 Local timer interrupts
HVS: 0 0 Hyper-V stimer0 interrupts
0: 4 0 IO-APIC 2-edge timer
LOC: 8577 8390 Local timer interrupts
HVS: 0 0 Hyper-V stimer0 interrupts
[root@centosgpt ~]#
- shared 与 alien
释放alient
远程缓冲时会先放到shared
缓冲中
static void __drain_alien_cache(struct kmem_cache *cachep,
struct array_cache *ac, int node,
struct list_head *list)
{
...
if (n->shared)
transfer_objects(n->shared, ac, ac->limit);
...
}
SLUB
Unqueued
无队列的设计 (针对SLAB
)- 队列就是一个单页面中的
slab
队列。 页面与CPU关联 - 使用
per-CPU
partials链表 - 在一个
cpu
完成实现快速通道 - 基于页面的策略。
- 多个级别的碎片整理功能。
- 当前的默认
slab
分配器。
原理说明
- 相比
slab
,slub
结构进行了精简,per-cpu
缓存结构体简化 cpu node
结构简化, 使用partial_list
FrozenPagelock
:page->frozen
设置为1仅能存放在指定cpu
的partial list
中Payload
就是缓存的object
RedZone
调试标识, 增加这个标识, 如果这个标识被修改, 判断是否发生缓冲区溢出, 如果启用填写0xcc
,不启用仅作填充(Padding
)0xbb
Poinsing
调试标识,通过创建和释放slab
,将要感染的对象填充0x5A
, 分配时检查, 如果被改变, 就知道之前是否使用过Tracking/Debugging
, 存放SLAB_STORE_USER
相关数据存储最后的拥有者信息Free Pointer
,一个大小为sizeof(void*)指针, 在设置Poison
时,填充POISON_FREE
(0x6b
)Padding
填充数据0x5a
具体实现
涉及文件
/mm/slab.h
/include/linux/slub_def.h
/mm/slub.c
/mm/slab_common.c 与分配策略无关
数据结构
可以看到和 slab
使用同样结构kmem_cache
, 缓冲主要分为两部分, 一部分是per-CPU
变量, 一部分保存的是节点缓存,不过cpu_slab
类型略有调整
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab;
...
struct kmem_cache_node *node[MAX_NUMNODES];
};
也就是这个结构直接挂在了页面page
struct kmem_cache_cpu {
void **freelist;
unsigned long tid;
struct page *page;
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct page *partial;
/* Partially allocated frozen slabs */
#endif
...
};
节点保存一个partial
指针做为Cpu
缓存的备份
struct kmem_cache_node {
spinlock_t list_lock;
...
unsigned long nr_partial;
struct list_head partial;
...
};
相关代码
- 缓冲申请
先去per-CPU
的cpu_slab, 取不到通过 __slab_alloc
再申请
static __always_inline void *slab_alloc_node(struct kmem_cache *s,
gfp_t gfpflags, int node, unsigned long addr)
{
...
do {
...
c = raw_cpu_ptr(s->cpu_slab);
} while (...);
...
if (...) {
object = __slab_alloc(s, gfpflags, node, addr, c);
stat(s, ALLOC_SLOWPATH);
} else {
...
}
...
}
配置了CONFIG_SLUB_CPU_PARTIAL
, 从per-CPU
的partial
申请
static void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
unsigned long addr, struct kmem_cache_cpu *c)
{
...
if (slub_percpu_partial(c)) {
page = c->page = slub_percpu_partial(c);
slub_set_percpu_partial(c, page);
stat(s, CPU_PARTIAL_ALLOC);
...
}
...
freelist = new_slab_objects(s, gfpflags, node, &c);
...
}
否则需要考虑从备用节点parital
申请或重新申请页
static inline void *new_slab_objects(struct kmem_cache *s, gfp_t flags,
int node, struct kmem_cache_cpu **pc)
{
...
freelist = get_partial(s, flags, node, c);
...
page = new_slab(s, flags, node);
...
}
从当前节点寻找partial
,找不到如果存在其他节点寻找其他节点上partial
static void *get_partial(struct kmem_cache *s, gfp_t flags, int node,
struct kmem_cache_cpu *c)
{
...
object = get_partial_node(s, get_node(s, searchnode), c, flags);
...
return get_any_partial(s, flags, c);
}
备用节点没有可用直接申请物理页面
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)
{
...
page = alloc_slab_page(s, alloc_gfp, node, oo);
...
}
参考及引用
Christoph Lameter Slab allocators in the Linux Kernel: SLAB, SLOB, SLUB
linux内存源码分析 – SLAB分配器概述
Linux slab allocator and cache performance
cache coloring on slab memory management in Linux kernel
(v5.8-rc3)
Be First to Comment