如果对代码细节不感兴趣,可以直接跳转底部内存分配逻辑总结部分。
关于slab几个结构体的关系和初始化请见:
[linux kernel]slub内存管理分析(0) 导读
[linux kernel]slub内存管理分析(1) 结构体
[linux kernel]slub内存管理分析(2) 初始化
[linux kernel]slub内存管理分析(2.5) slab重用
PS:为了方便描述,这里我们将一个用来切割分配内存的page 称为一个slab page,而struct kmem_cache
我们这里称为slab管理结构,它管理的真个slab 体系成为slab cache,struct kmem_cache_node
这里就叫node。单个堆块称为object或者堆块或内存对象。
kmalloc
是slub内存分配的核心函数,还是挺复杂的,需要仔细分析。挺不爽的地方在于,kmalloc
很多函数都是什么inline 要么就是allways_inline啥的,调试的时候很麻烦。必要时可以把这些inline 去掉再编译。
除此之外,kmalloc
之中有很多设计cpu抢占、NUMA架构node相关的逻辑,这里不详细分析了,因为我们分析的目的是为了搞内核安全,而写漏洞利用的时候可以通过将程序绑定在一个cpu 来避免涉及这些逻辑的代码发生。
还有一些和Kasan、slub_debug 等检测内存越界等的操作,也不过多分析。
kmalloc
kmalloc_large
大内存
kmalloc_order_trace
kmalloc_order
alloc_page
直接申请页用来分配kmalloc_index
获取下标
kmalloc_type
根据flag
获取slab
kmem_cache_alloc_trace
slab_alloc
=> slab_alloc_node
(node
为NUMA_NO_NODE
)
slab_pre_alloc_hook
分配前hook,里面有内存计数cache 相关操作,可能切换slab
kfence_alloc
类似检验内存破坏,每隔一段时间才会调用成功
this_cpu_read
; raw_cpu_ptr
获取cpu_slab
cpu freelist
为空page
为空 或者page
和node
匹配(slab_alloc
调用node
为NUMA_NO_NODE
) 时使用 __slab_alloc
(封装___slab_alloc
) 分配
从当前cpu_slab
的freelist
分配
从cpu->partical
中分配
如果当前cpu_slab->page
和cpu_slab->partical
都空,则新申请slab
new_slab_objects
get_partial
node有partial
slab则使用node的new_slab
=>allocate_slab
alloc_slab_page
alloc_pages
shuffle_freelist
是否freelist
随机cpu_slab->freelist
里分配,然后更新freelist
和tid
maybe_wipe_obj_freeptr
& slab_want_init_on_alloc
初始化slab_post_alloc_hook
分配结尾的动作kmalloc
的入口肯定是kmalloc
啊,直接看函数:
linux\include\linux\slab.h : kmalloc
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
if (__builtin_constant_p(size)) {
#ifndef CONFIG_SLOB//这里分析SLUB,不考虑SLOB相关(不共存)
unsigned int index;
#endif
if (size > KMALLOC_MAX_CACHE_SIZE)//[1]大于slab 支持的最大分配大小直接调用页分配
return kmalloc_large(size, flags);
#ifndef CONFIG_SLOB
index = kmalloc_index(size);//[2]根据需要分配的大小获取slab 下标
if (!index)
return ZERO_SIZE_PTR;
return kmem_cache_alloc_trace(//[3]进入分配流程
kmalloc_caches[kmalloc_type(flags)][index],
flags, size);
#endif
}
return __kmalloc(size, flags);
}
[1] slab支持分配的大小有限,最大是两页也就是8k,再大的内存会调用kmalloc_large
分配,里面是直接分配页。后面分析。
[2] 调用kmalloc_index
根据申请的内存大小获取对应的slab管理结构下标。
static __always_inline unsigned int kmalloc_index(size_t size)
{
if (!size)
return 0;
if (size <= KMALLOC_MIN_SIZE)
return KMALLOC_SHIFT_LOW;
if (KMALLOC_MIN_SIZE <= 32 && size > 64 && size <= 96)
return 1;
if (KMALLOC_MIN_SIZE <= 64 && size > 128 && size <= 192)
return 2;
if (size <= 8) return 3;
if (size <= 16) return 4;
··· ···
if (size <= 4 * 1024) return 12;
if (size <= 8 * 1024) return 13;
··· ···
/* Will never be reached. Needed because the compiler may complain */
return -1;
}
[3] 然后调用kmalloc_type
根据申请flag
获取对应的slab 类型,和刚获取的slab 下标,得到最终的合适的slab 管理结构,传入
kmem_cache_alloc_trace
来进行分配。
static __always_inline enum kmalloc_cache_type kmalloc_type(gfp_t flags)
{
#ifdef CONFIG_ZONE_DMA
/*
* The most common case is KMALLOC_NORMAL, so test for it
* with a single branch for both flags.
*/
if (likely((flags & (__GFP_DMA | __GFP_RECLAIMABLE)) == 0))
return KMALLOC_NORMAL;
/*
* At least one of the flags has to be set. If both are, __GFP_DMA
* is more important.
*/
return flags & __GFP_DMA ? KMALLOC_DMA : KMALLOC_RECLAIM;
#else
return flags & __GFP_RECLAIMABLE ? KMALLOC_RECLAIM : KMALLOC_NORMAL;
#endif
}
linux\include\linux\slub.c : kmem_cache_alloc_trace
#ifdef CONFIG_TRACING //[1]开启CONFIG_TRACING时执行如下函数
void *kmem_cache_alloc_trace(struct kmem_cache *s, gfp_t gfpflags, size_t size)
{
void *ret = slab_alloc(s, gfpflags, _RET_IP_, size);//[2]直接调用slab_alloc
trace_kmalloc(_RET_IP_, ret, size, s->size, gfpflags);
ret = kasan_kmalloc(s, ret, size, gfpflags);
return ret;
}
[1] 开启CONFIG_TRACING
的时候执行slub.c 中的kmem_cache_alloc_trace
,默认开启。
[2] 这里直接调用slab_alloc
。
linux\include\linux\slub.c : slab_alloc
static __always_inline void *slab_alloc(struct kmem_cache *s,
gfp_t gfpflags, unsigned long addr, size_t orig_size)
{
return slab_alloc_node(s, gfpflags, NUMA_NO_NODE, addr, orig_size);//[1]NUMA_NO_NODE
}
slab_alloc
就是封装了一层slab_alloc_node
。
[1] node 设置为了NUMA_NO_NODE
,代表从当前正在运行的cpu的node节点分配内存。
linux\include\linux\slub.c : slab_alloc_node
static __always_inline void *slab_alloc_node(struct kmem_cache *s,
gfp_t gfpflags, int node, unsigned long addr, size_t orig_size)
{
void *object;
struct kmem_cache_cpu *c;
struct page *page;
unsigned long tid;
struct obj_cgroup *objcg = NULL;
bool init = false;
s = slab_pre_alloc_hook(s, &objcg, 1, gfpflags);//[1]对内存分配进行预处理,不同版本实现不同
if (!s)
return NULL;
object = kfence_alloc(s, orig_size, gfpflags); //[2]内存抽样,每隔一段时间从kfence里分配一次
if (unlikely(object))
goto out;
redo:
do {//[3]与CPU抢占模式有关,读取当前可用cpu_slab和tid,CONFIG_PREEMPTION默认不开启。
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();//内存屏障,刷新cpu 缓存
object = c->freelist;//[4]获取cpu_slab当前的freelist的第一个对象
page = c->page;//以及slab所在page
if (unlikely(!object || !page || !node_match(page, node))) {//如果object和page任意为空则无法立刻申请
object = __slab_alloc(s, gfpflags, node, addr, c);
} else {//[5]不为空则说明可以直接用当前freelist 分配
void *next_object = get_freepointer_safe(s, object);//[5.1]获取freelist的下一个空闲堆块
if (unlikely(!this_cpu_cmpxchg_double(//[5.2]this_cpu_cmpxchg_double()原子指令操作取得该空闲对象
s->cpu_slab->freelist, s->cpu_slab->tid,
object, tid,
next_object, next_tid(tid)))) {
//操作等价于
// object = s->cpu_slab->freelist;
// s->cpu_slab->freelist=next_object;
// tid=s->cpu_slab->tid;s->cpu_slab->tid=next_tid(tid)
//[5.3]如果失败则经note_cmpxchg_failure()记录日志后重回redo标签再次尝试分配。
note_cmpxchg_failure("slab_alloc", s, tid);
goto redo;
}
prefetch_freepointer(s, next_object);//[5.4]预取操作,优化相关
stat(s, ALLOC_FASTPATH);//设置状态,CONFIG_SLUB_STATS 默认不开启则没用
}
maybe_wipe_obj_freeptr(s, object);//这俩函数初始化相关
init = slab_want_init_on_alloc(gfpflags, s);
out:
slab_post_alloc_hook(s, objcg, gfpflags, 1, &object, init);//[6]分配后的一些乱七八糟的处理
return object;
}
[1] 对slab分配的一些预处理hook,这里有CONFIG_MEMCG
相关的操作,也就是内存分配的时候是否带有__GFP_ACCOUNT
flag(内存申请计数)。为什么忽然提这个呢,因为在不同的linux 内核版本,这一块实现是不同的,在有些版本中(<5.9),MEMCG的实现方式是需要计数的申请统一从一个单独的slab 管理结构(struct kmem_cache
)分配,这就导致会和不带__GFP_ACCOUNT
flag 的内存分配不到一起去,对漏洞利用会造成影响。后续会独立分析。
[2] kfence
相关,其实就是每隔一段时间会从一个叫做kfence
的内存池中分配一块内存,主要是抽样检测内存越界等操作。没啥用不详细分析了。参考https://blog.csdn.net/pwl999/article/details/124494958
[3] 这里读取slab管理结构中的cpu_slab
(当前cpu)和它对应的tid
,之所以代码写成这样是涉及到cpu 的抢占模式,由于我们只分析内存分配逻辑,所以不考虑cpu抢占场景,默认就一直使用同一个cpu。值得一提的是CONFIG_PREEMPTION
默认不开启。
[4] 获取当前cpu_slab
的freelist
的第一个内存堆块和freelist
所在slab page。并且进行判断这两个对象是否存在(指针是否为空),如果为空则说明当前cpu_slab
还没有可以立刻分配的slab ,要么cpu_slab
分配完了(freelist
为空page
不为空),要么cpu_slab
还没有slab(page
为空)。则需要调用__slab_alloc
申请新的slab。后面详细分析。
[5] 如果不为空则说明可以直接在这个freelist
进行分配(freelist
的第一个对象object
拿出来分配就行)。但操作复杂一些,涉及到抢占之类的。
[5.1] 先获取freelist
的第二个空闲堆块,也就是object
的下一个对象。get_freepointer_safe
操作在下一章单独分析,这里值分析大流程。
[5.2] 使用一个原子操作(单cpu时间)来完成以下操作,即this_cpu_cmpxchg_double
函数里面有一些关于判断抢占的逻辑,我们不考虑,实际有意义的操作等价于如下四步:
object = s->cpu_slab->freelist;
s->cpu_slab->freelist=next_object;
tid=s->cpu_slab->tid;
s->cpu_slab->tid=next_tid(tid)
[5.3] 如果上面原子操作this_cpu_cmpxchg_double
没执行成功,则note_cmpxchg_failure
记录日志,然后redo重来一遍
[5.4] 把next_object
的下一个对象放到快速缓存吧,反正优化相关,不重要;后面的stat
是设置状态,跟CONFIG_SLUB_STATS
有关,这编译选项默认不开启,则不用关心。
[6] 分配完内存之后,可能对内存进行一些kasan
和memcg
相关操作,比如擦除内存里的别的内容之类的。或者打标记什么乱七八糟的,但总之进行到这里内存已经分配完毕了,不太重要了。
走到这里说明当前cpu_slab
还没有可以立刻分配的slab ,要么cpu_slab
分配完了(freelist
为空),要么cpu_slab
还没有slab(page
为空),需要调用__slab_alloc
分配新的slab。
linux\mm\slub.c : __slab_alloc
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;
local_irq_save(flags);
#ifdef CONFIG_PREEMPTION//默认不开启,开启抢占则需要重新获取一下当前cpu_slab
c = this_cpu_ptr(s->cpu_slab);
#endif
p = ___slab_alloc(s, gfpflags, node, addr, c);//直接调用___slab_alloc
local_irq_restore(flags);
return p;
}
这里可以看出__slab_alloc
就是封装了一下直接调用___slab_alloc
:
linux\mm\slub.c : ___slab_alloc
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;//[1]获取当前cpu 的page
if (!page) {//page 为空则去new_slab 搞一个新slab
if (unlikely(node != NUMA_NO_NODE &&//[1.1]如果传入不是NUMA_NO_NODE,说明指定了node
!node_isset(node, slab_nodes)))//并且该node还没分配kmem_cache_node的话
node = NUMA_NO_NODE;//就设置为NUMA_NO_NODE任意node都可以
goto new_slab;//然后去申请一个新的slab page
}
redo:
if (unlikely(!node_match(page, node))) {//[2] 一般不会走到这个分支,cpu_slab中的page不属于指定的node
if (!node_isset(node, slab_nodes)) {//[2.1] 指定的node没分配kmem_cache_node
node = NUMA_NO_NODE;//将node设定成任意node然后重来即可与过这里的判断了
goto redo;
} else {//[2.2] 虽然page和node不匹配,但可以从指定node分配,则将指定node换上来,page换下去
stat(s, ALLOC_NODE_MISMATCH);
deactivate_slab(s, page, c->freelist, c);//[2.3]强制下架,将指定node 的slab换到cpu_slab中
goto new_slab;
}
}
if (unlikely(!pfmemalloc_match(page, gfpflags))) {//[3]一般不会进入这个分支,检验page和gfpflags
deactivate_slab(s, page, c->freelist, c);//下架
goto new_slab;
}
//走到这里说明cpu_slab至少有page
//(要么原本就有page,但分配满了所以走到这,要么是从node刚换上来的,大概率有可以分配的堆块),
/* must check again c->freelist in case of cpu migration or IRQ */
freelist = c->freelist;//[4]获取cpu_slab的空闲链表,有page不一定代表有free_list
if (freelist)//[4.1]不为空则去load_freelist,直接从freelist 分配
goto load_freelist;
freelist = get_freelist(s, page);//[4.2]如果freelist为空则从page重新获取,可能有刚被其他CPU释放的堆块可用
//或者是是新拿到的page,则设置page状态
if (!freelist) {//[4.3]如果还是为获取不到,说明page分配满了,那就new_slab
c->page = NULL;//会将page设置为null,这里也说明在cpu_slab中分配满的page会直接将其"扔掉"
stat(s, DEACTIVATE_BYPASS);
goto new_slab;
}
stat(s, ALLOC_REFILL);
load_freelist://[5]直接从freelist分配
VM_BUG_ON(!c->page->frozen);
c->freelist = get_freepointer(s, freelist);//freelist 向后移动
c->tid = next_tid(c->tid);//更新tid
return freelist;//返回freelist第一个对象就是要分配的
new_slab://[6]申请新的slab
if (slub_percpu_partial(c)) {//[6.1]如果cpu_slab partical 有page先从partical 取
page = c->page = slub_percpu_partial(c);//[6.2]从partial中取出第一个page更新cpu_slab->page
slub_set_percpu_partial(c, page);//partical指向下一个
stat(s, CPU_PARTIAL_ALLOC);
goto redo;//在上面重来,这样就可以从新partical page 的freelist里申请了
}
//[6.3]都没有就要申请一个全新slab page了然后分配
freelist = new_slab_objects(s, gfpflags, node, &c);
if (unlikely(!freelist)) {//这还失败就返回空,申请失败
slab_out_of_memory(s, gfpflags, node);
return NULL;
}
page = c->page;//[6.4]如果未开启调试且页面属性匹配pfmemalloc
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;
}
[1] 获取当前cpu_slab
正在使用的page
,如果为空则说明当前cpu_slab->page
为空即没有slab page可以尝试立刻分配,则会去new_slab
分支申请新slab。
[1.1] 这里进行了一个node 的判断,___slab_alloc
函数的node参数是指定从特定node分配的意思,但这里kmalloc
路径下来这里传入的参数是NUMA_NO_NODE
,代表"从当前cpu 的node执行"的意思。如果当前指定node不是NUMA_NO_NODE
,说明传入路径明确指定"从某个特定node分配"并且如果指定的node还没分配kmem_cache_node
结构的话,则会将node设置为NUMA_NO_NODE
,也就是放弃指定node,从当前cpu 使用node中分配。
[2] 当前cpu_slab
有page
,这种情况下大概率是当前cpu_slab->page分配满了,需要先判断node是否和当前cpu 使用的slab page所在同一个node,如果传入是NUMA_NO_NODE
,则相当于同一个,则不会进入该分支。
[2.1] 如果指定了node并且当前cpu使用内存node不是指定的node,并且该node还没分配kmem_cache_node
结构的话,则将node重置为NUMA_NO_NODE
继续(跟上面一样)。
[2.2] 否则说明虽然指定的node和当前cpu_slab->page
所属node不一样,但指定的node可以正常分配,则调用deactivate_slab
函数强制下架cpu_slab
正在使用的slab page(也就是cpu_slab->page
),后续会换上指定node的slab page。
[2.3] 所谓强制下架,就是将cpu_slab
的freelist
和所在page
根据当前状态(部分空闲partical
或满full
)放入对应node 的kmem_cache_node
结构体的partical
或full
链表中。然后改变一些状态,目的就是*我不打算在你当前的cpu_slab
中分配了,先把你cpu_slab
中的page
放到属于它的kmem_cache_node
中,细节在下一章节进行分析。
[3] 通常不会走到这个分支之中,如果page
开启了PF_MEMALLOC
并且传入kmalloc
的分配flag
也有特定设置的话,也会进行slab切换(强制下架然后new_slab
)。这里不仔细分析了,反正unlikely。
[4] 走到这里说明不管怎样,这个cpu_slab
至少是有slab page的(cpu_slab->page
不为空),首先获取cpu_slab
的freelist
,也就是立马就能分配的内存。
[4.1] 如果freelist
不为空,那可以直接分配,走到load_freelist
分支直接进行分配。
[4.2] **cpu_slab
的freelist
为空则尝试一下从page
获取freelist
。**虽然走到这个函数的大部分情况说明cpu_slab->freelist
是空的,但却并不代表cpu_slab->page 无法完成分配,因为会出现"A CPU 释放了B CPU的cpu_slab->page
中的堆块到B CPU的cpu_slab->page->freelist
"的情况,这样虽然B CPU的cpu_slab->freelist
为空,但B CPU的cpu_slab->page->freelist
可能存在刚被A CPU释放的堆块可用,所以这里通过get_freelist
重新获取一下。除此之外,后面代码的逻辑会给cpu_slab
一个新的page
(从node换上来或申请),然后redo
重来,再到这里也会通过该函数获取freelist
,同时还会根据page
的状态进行不同的操作:
static inline void *get_freelist(struct kmem_cache *s, struct page *page)
{//传入的page可能为新申请的page或以前无法分配的page,新page则更新状态,老page解冻准备移除cpu_slab
struct page new;
unsigned long counters;
void *freelist;
do {
freelist = page->freelist; //获取page 的freelist
counters = page->counters; //获取counters(联合体,里面包含frozen等状态信息)
new.counters = counters;
VM_BUG_ON(!new.frozen);
new.inuse = page->objects;//更新page counters
new.frozen = freelist != NULL;
//如果freelist 为空,则说明page不是新page,是已经分配满了的page,这里解冻,后续会从cpu_slab中移除
} while (!__cmpxchg_double_slab(s, page,
freelist, counters,
NULL, new.counters,
"get_freelist"));
/* 原子操作
* page->freelist = null; page->counters = new.counters
* 只要被cpu_slab控制的page,freelist 都要设置为null,因为以cpu_slab->freelist为准
*/
return freelist;
}
如果传入get_freelist
的page
是在cpu_slab
中分配满了的page
,则会将其解冻(frozen
字段设为0),后续会从cpu_slab->page
中删除。
[4.3] 如果page->freelist
也为空的话,则说明当前cpu_slab
没可以直接分配内存的slab,那就new_slab
分支申请新slab。这里之所以分别判断,是因为后面重新分配了cpu_slab->page
后重新回到这里判断一遍。
这里会将cpu_slab->page直接置空,而不会将之前的page 先拿下来放到其他表中(如node的full列表中),这是因为如果cpu_slab->page如果还有,那也一定是分配满了才会走到这里,并且它已经刚从get_freelist函数中"解冻",现在将其置空会进入"不在任何列表中"的一种游离状态,它虽然不在任何列表中,但你释放这个slab page中的堆块的时候,会重新将其加入到某个列表之中。
[5] 直接把freelist
第一个内存块分配出去就行,然后freelist
往后移动一格,更新tid
啥的。
[6] 这里是申请一个全新slab。走到这里的话cpu_slab->page一定是空的,要么之前的page已经被强制下架,要么就是分配满了被舍弃进入"游离状态"。
[6.1] 首先判断cpu_slab
的partial
链表是否为空,如果不为空,则从partial
链表中取出一个slab page,该slab一定还有空闲内存块可以分配,然后partical
链表指向下一个page
,然后重头开始重新判断一下,这样走到[4] 的时候就可以从新page
的freelist
重新分配了。
#define slub_percpu_partial(c) ((c)->partial)
#define slub_set_percpu_partial(c, p) \
({ \
slub_percpu_partial(c) = (p)->next; \
})
[6.2] 从cpu_slab->partial
获取到新的page
之后赋值给cpu_slab->page
。
[6.3] 如果partial
也没有slab page了,那就要完全重新分配一个了。调用new_slab_objects
完全分配一个全新slab page。
[6.4] 申请到新的slab了之后,如果没有什么其他乱七八糟的配置,那就回到load_freelist
,从新slab page的freelist
分配。后面的就是一些乱七八糟的情况,比如新slab flag不匹配啥的。我们不详细分析了。
new_slab_objects
函数尝试从node中拿一个可用slab或负责分配一个新的slab,全新的page
:
linux\mm\slub.c : new_slab_objects
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);//[1]获取node 的partical slab
if (freelist)
return freelist;
page = new_slab(s, flags, node);//[2]调用new_slab分配新page
if (page) {
c = raw_cpu_ptr(s->cpu_slab);//[2.1]获取当前cpu
if (c->page)//[2.2]如果当前cpu有page则flush_slab刷新吗,这里会将page强制下架
flush_slab(s, c);
freelist = page->freelist;//[2.3]更新freelist、page
page->freelist = NULL;//被cpu_slab控制的page 的freelist 都要设置为NULL
stat(s, ALLOC_SLAB);
c->page = page;//该page被cpu_slab控制
*pc = c;
}
return freelist;//[3]返回freelist
}
[1] 在上一个函数___slab_alloc
我们只确认了cpu_slab
没有可用patial slab了,但不代表对应的node 没有可用partial slab了,这里调用get_partial
尝试从node 中获取一个可用slab返回。后续分析get_partial
函数。
[2] 如果node也没有了,那就new_slab
重新申请一个全新(这次真是全新了)slab,也就是一个新page
。
[2.1] 重新获取一下正在使用的cpu,不考虑抢占可以忽略。
[2.2] 这里如果当前cpu是有cpu_slab->page
的,则会调用flush_slab
将page
强制下架,让cpu_slab
使用刚申请到的新page
(否则新page
没地方可放,总不能刚申请完就放到node中)。
static inline void flush_slab(struct kmem_cache *s, struct kmem_cache_cpu *c)
{
stat(s, CPUSLAB_FLUSH);
deactivate_slab(s, c->page, c->freelist, c);//强制下架page
c->tid = next_tid(c->tid);
}
[2.3] 然后更新一下cpu_slab->page
、更新page->freelist
等,注意所有被cpu_slab
控制的page
的freelist
都要设置为NULL。
[3] 返回新slab 的freelist
,可以回到上面函数立即参与分配。
linux\mm\slub.c : get_partial
static void *get_partial(struct kmem_cache *s, gfp_t flags, int node,
struct kmem_cache_cpu *c)
{
void *object;
int searchnode = node;
if (node == NUMA_NO_NODE)//[1]NUMA_NO_NODE获取当前node
searchnode = numa_mem_id();
object = get_partial_node(s, get_node(s, searchnode), c, flags);//[2]从这个node获取partial 链表
if (object || node != NUMA_NO_NODE)
return object;
return get_any_partial(s, flags, c);//[3]随便来一个
}
[1] 如果node是NUMA_NO_NODE则获取当前node就行了。
[2] 从指定node 获取partial
slab 的freelist
,返回的是freelist
,除此之外,还会把freelist所属的page给cpu_slab->page
赋值并且从node->partial
中删除,除此之外还会从node->partial
中获取若干page
补充到cpu_slab->partial
中,详细操作在下一章节中分析,因为对kmalloc
整体流程影响不大。
[3] 实在没有就调用get_any_partial
随便来一个,get_any_partial
的逻辑就是遍历所有node,找到一个有partial
的node直接对齐调用上面说过的get_partial_node
,获取这个node里的page
资源。
申请全新slab:
linux\mm\slub.c : new_slab
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);
return allocate_slab(s,
flags & (GFP_RECLAIM_MASK | GFP_CONSTRAINT_MASK), node);
}
可以看到直接调用了allocate_slab
,补充了一些flag
:
linux\mm\slub.c : allocate_slab
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;
··· ···
page = alloc_slab_page(s, alloc_gfp, node, oo);//[1]分配一个新slab页
if (unlikely(!page)) {//[1.1]如果分配失败,则尝试缩小order重新分配
oo = s->min;
alloc_gfp = flags;
page = alloc_slab_page(s, alloc_gfp, node, oo);
if (unlikely(!page))
goto out;
stat(s, ORDER_FALLBACK);
}
page->objects = oo_objects(oo);//[2]设置page中的内存块数
account_slab_page(page, oo_order(oo), s, flags);//统计页数相关
page->slab_cache = s;//[3]page指向自己的slab
__SetPageSlab(page);//[3]page相关宏,修改page属性为slab
if (page_is_pfmemalloc(page))
SetPageSlabPfmemalloc(page);
kasan_poison_slab(page);
start = page_address(page);//[3]获取page 对应的虚拟地址
setup_page_debug(s, page, start);
shuffle = shuffle_freelist(s, page);//[4]CONFIG_SLAB_FREELIST_RANDOM相关
if (!shuffle) {//[5]不随机,一般都开启随机
start = fixup_red_left(s, start);//开启左边越界检测相关
start = setup_object(s, page, start);
page->freelist = start;//[5.1]设置freelist
for (idx = 0, p = start; idx < page->objects - 1; idx++) {//[5.1]依次给page 分块,每块之间链表链接
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;//[6]frozen代表在cpu_slab 中,unfroze代表在partial队列或者full队列
out://退出环节
if (gfpflags_allow_blocking(flags))
local_irq_disable();
if (!page)
return NULL;
inc_slabs_node(s, page_to_nid(page), page->objects);//增长node的slab和object计数
return page;
}
[1] 调用alloc_slab_page
给slab分配一个新页,这里kmem_cache->oo
高2字节代表该slab 每次申请新页的阶数。后面分析alloc_slab_page
函数。
[1.1] 如果分配失败,则尝试缩小oo
为kmem_cache->min
再次尝试分配。还不行就退出。
[2] 然后设定page
的object
,也就是管理的内存块数量,即oo
的低两字节
#define OO_SHIFT 16
#define OO_MASK ((1 << OO_SHIFT) - 1) //16
static inline unsigned int oo_objects(struct kmem_cache_order_objects x)
{
return x.x & OO_MASK;
}
[3] page
要指向自己的slab,并通过改变page
属性的宏将page
页面属性改为slab,这就代表这是一个slab 页。然后通过page_address
获取page
结构体对应的实际页虚拟地址
[4] CONFIG_SLAB_FREELIST_RANDOM
相关,开启了之后要把这个page
按照slab 规定大小分割成几块,这几块顺序随机化。后续分析。
[5] 如果没有开启freelist
随机化,那么就直接按顺序排列freelist
[5.1] 将这个页(不一定是一页,但肯定是连续的),按照slab 大小分割之后,依次按顺序组合成freelist
,指针首尾相接。在没开启freelist
随机化的情况下,set_freepointer
的操作如下,直接获取地址偏移s->offset
的地方就是指向下一个内存块的指针:
static inline void set_freepointer(struct kmem_cache *s, void *object, void *fp)
{
unsigned long freeptr_addr = (unsigned long)object + s->offset;
#ifdef CONFIG_SLAB_FREELIST_HARDENED
BUG_ON(object == fp); /* naive detection of double free or corruption */
#endif
freeptr_addr = (unsigned long)kasan_reset_tag((void *)freeptr_addr);
*(void **)freeptr_addr = freelist_ptr(s, fp, freeptr_addr);
}
[6] 将page->frozen
设置为frozen
,再回顾一下:frozen
代表该page
在cpu_slab
中,unfroze
代表该page
在partial
队列或者full
队列或者"游离状态"
linux\mm\slub.c : alloc_slab_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;
unsigned int order = oo_order(oo);//[1]获取申请页面的阶数
if (node == NUMA_NO_NODE)
page = alloc_pages(flags, order);//[2]申请页面
else
page = __alloc_pages_node(node, flags, order);//[2]在特定node中申请页面
return page;
}
[1] 通过oo
获取要申请的页面阶数,即oo
的高两字节,阶数就是申请多少页,如阶数2那就申请22=4页,阶数1就申请21=2页
#define OO_SHIFT 16
#define OO_MASK ((1 << OO_SHIFT) - 1) //16
static inline unsigned int oo_order(struct kmem_cache_order_objects x)
{
return x.x >> OO_SHIFT;
}
[2] 如果node为NUMA_NO_NODE
则在当前node调用alloc_pages
从伙伴系统申请页面。否则在指定node 申请页面。
对于大于slab可以最大可以分配的内存,这里调用kmalloc_large
分配,其实就是分配页面:
linux\include\linux\slab.h : kmalloc_large
static __always_inline void *kmalloc_large(size_t size, gfp_t flags)
{
unsigned int order = get_order(size);//计算该大小需要的页的阶数
return kmalloc_order_trace(size, flags, order);
}
先根据size
计算出需要的页数,也就是页的阶数,然后调用kmalloc_order_trace
:
linux\mm\slab_common.c : kmalloc_order_trace
#ifdef CONFIG_TRACING
void *kmalloc_order_trace(size_t size, gfp_t flags, unsigned int order)
{
void *ret = kmalloc_order(size, flags, order);
trace_kmalloc(_RET_IP_, ret, size, PAGE_SIZE << order, flags);
return ret;
}
直接调用kmalloc_order
:
linux\mm\slab_common.c : kmalloc_order
void *kmalloc_order(size_t size, gfp_t flags, unsigned int order)
{
void *ret = NULL;
struct page *page;
if (unlikely(flags & GFP_SLAB_BUG_MASK))
flags = kmalloc_fix_flags(flags);
flags |= __GFP_COMP;
page = alloc_pages(flags, order);//[1]调用alloc_page 分配页面
if (likely(page)) {
ret = page_address(page);//[2]返回page结构体对应的地址
mod_lruvec_page_state(page, NR_SLAB_UNRECLAIMABLE_B,
PAGE_SIZE << order);
}
ret = kasan_kmalloc_large(ret, size, flags);
/* As ret might get tagged, call kmemleak hook after KASAN. */
kmemleak_alloc(ret, size, 1, flags);
return ret;
}
[1] 直接调用alloc_pages
申请页面。
[2] 根据页结构体获得页面实际虚拟地址,返回给上面使用。
其实slab 就是几级缓存机制,从cpu_slab
分配最快,能从cpu_slab
当前freelist
分配就从cpu_slab
当前freelist
分配,不能则从partial
列表中把一个slab page拿出来放到freelist
,如果还没有就从node 里找slab page,都没有再新申请slab page。
kmalloc
分配内存的大小,大于slab可以分配的上限(通常8k)则调用kmalloc_large
进行分配。
kmalloc_large
则直接根据大小调用伙伴系统分配适当的页数。flag
和申请大小计算出kmalloc_caches
的类型和index
,获取对应的slab管理结构体。
flag
存在ACCOUNT
相关可能要切换到计数后slab,即kmalloc-cg
相关slab(slab_pre_alloc_hook
)cpu_slab
,如果当前cpu_slab
的freelist
不为空,则直接将freelist
的第一个分配返回,然后freelist
向后移动,更新tid
等。cpu_slab
的freelist
为空,则看cpu_slab
的partial
链表是否为空,不为空则将partial
链表第一个slab page 切换给cpu_slab
freelist
,然后分配,partial
指向下一个slab page。cpu_slab
无法完成分配,需要新slab,则会找到合适当前运行cpu 的内存node所属kmem_cache_node
结构,查询partial
链表是否有可用slab page有的话将这个slab page给cpu_slab
,然后也从这个node取出一些slab page补充到cpu_slab->partial
,然后从freelist
分配一个,跟上面一样。new_slab
申请一个全新slab page,从新slab page的freelist
分配。