本文仍然以slab cache kmalloc_caches 为例,结合 kfree
函数的实现,说明slab对象的回收过程。
1. kfree
通过 kfree
函数释放 kmalloc
申请的内存时,对应的函数定义在 mm/slub.c 中。
void kfree(const void *x)
{
struct page *page;
void *object = (void *)x;
trace_kfree(_RET_IP_, x);
/* 地址为空直接返回 */
if (unlikely(ZERO_OR_NULL_PTR(x)))
return;
/* 通过内核虚拟地址获取对应的struct page * */
page = virt_to_head_page(x);
/* slab分配器没有管理这个内存页 */
if (unlikely(!PageSlab(page))) {
BUG_ON(!PageCompound(page));
kfree_hook(x);
/* 直接通过伙伴系统释放内存页 */
__free_kmem_pages(page, compound_order(page));
return;
}
/* struct page中包含内存页的slab信息,包括该内存页
所属的slab cache,*/
slab_free(page->slab_cache, page, object, _RET_IP_);
}
EXPORT_SYMBOL(kfree);
2. slab_free( mm/slub.c )
如果 kfree
的参数地址所在的页面属于slab分配器,通过 slab_free
函数释放。 slab_free
函数也有两个分支,快路径和慢路径。
如果 kfree
的对象属于当前的cpu slab,执行快路径;否则执行慢路径。
static __always_inline void slab_free(struct kmem_cache *s,
struct page *page, void *x, unsigned long addr)
{
void **object = (void *)x;
struct kmem_cache_cpu *c;
unsigned long tid;
slab_free_hook(s, x);
redo:
/*
* Determine the currently cpus per cpu slab.
* The cpu may change afterward. However that does not matter since
* data is retrieved via this pointer. If we are on the same cpu
* during the cmpxchg then the free will succedd.
*/
preempt_disable();
c = this_cpu_ptr(s->cpu_slab);
tid = c->tid;
preempt_enable();
/*
释放的对象所在的内存页刚好是当前CPU正在使用的
slab,执行快路径,只需要添加到freelist中 */
if (likely(page == c->page)) {
/*
设置要释放的对象指向当前的空闲对象,即把释放的对象
添加到CPU的freelist中 */
set_freepointer(s, object, c->freelist);
/*
更新slab cache的cpu_slab变量,指向
最新的freelist,并且更新tid */
if (unlikely(!this_cpu_cmpxchg_double(
s->cpu_slab->freelist, s->cpu_slab->tid,
c->freelist, tid,
object, next_tid(tid)))) {
note_cmpxchg_failure("slab_free", s, tid);
goto redo;
}
stat(s, FREE_FASTPATH);
} else
/*
page不是当前CPU正在分配对象的slab,执行慢路径。
可能是以下情况:
1. 释放的对象属于当前CPU的partial表
2. 释放的对象属于其他CPU的slab */
__slab_free(s, page, x, addr);
}
3. __slab_free( mm/slub.c )
慢路径释放slab时,涉及到多个名称相近的变量,先对这些变量进行说明。
- struct kmem_cache —— 描述一个slab cache:
unsigned long min_partial
node结点中部分空slab缓冲区数量不能小于这个值,如果小于这个值,空闲slab缓冲区则不能够进行释放,而是将空闲slab加入到node结点的部分空slab链表中。
int cpu_partial
同min_partial类似,只是这个值表示的是对象的数量,而不是部分空slab数量,即CPU的partial对象数量不能小于这个值,小于的情况下要去对应node结点的部分空链表中获取若干个部分空slab;否则在put_cpu_partial
函数中进行解冻操作。
- struct kmem_cache_cpu —— 描述一个CPU的slab cache:
void **freelist
指向下一个空闲的对象struct page *partial
CPU的部分空slab链表,放到CPU的部分空slab链表中的slab会被冻结,而放入node中的部分空slab链表则解冻,冻结标志在slab缓冲区描述符( struct page )中
struct page *page
当前分配对象的slab
- struct kmem_cache_node —— 描述一个节点的slab cache:
unsigned long nr_partial
节点中部分空slab的数量struct list_head partial
保存部分空slab的链表
- struct page —— 描述一个slab:
void *freelist
第一个空闲的对象unsigned inuse:16
slab中已经分配的对象的数量unsigned frozen:1
被冻结的slab不属于任何链表,不会进行列表的管理操作。
只有冻结slab的CPU可以执行列表操作——其他的CPU可以往freelist中添加对象,但是只有冻结slab的CPU可以从freelist中获取对象。因此,slab_free
函数会跳过列表的相关操作。
被冻结的slab主要用于特定目的,比如响应特定CPU的分配请求。
frozen 标志用来判断某一个slab(即内存页)是否已经被某个CPU用作slab cache,即属于某个 kmem_cache_cpu。
和快路径 slab_free
不同,执行慢路径的原因是 kfree
释放的对象所属的内存页不是当前CPU正在分配对象的slab。
因此,函数释放对象的时候修改的是 struct page 内的 freelist 指针,而不是CPU的freelist。
static void __slab_free(struct kmem_cache *s, struct page *page,
void *x, unsigned long addr)
{
void *prior;
void **object = (void *)x;
int was_frozen;
struct page new;
unsigned long counters;
struct kmem_cache_node *n = NULL;
unsigned long uninitialized_var(flags);
...
/* do-while循环保证更改成功 */
do {
if (unlikely(n)) {
spin_unlock_irqrestore(&n->list_lock, flags);
n = NULL;
}
prior = page->freelist;
counters = page->counters;
set_freepointer(s, object, prior);
/*
counters的赋值操作会将和其位于相同union内的
inuse,objects,frozen字段赋值 */
new.counters = counters;
was_frozen = new.frozen;
new.inuse--;
/*
!new.inuse = true,slab中没有分配出去的对象,目前我
只想到了一种情况,即当前释放的对象是slab中的最后一个
对象,释放之后slab为空。
!prior = true,slab中没有可用的对象,即slab为满。
!was_frozen = true,slab没有被冻结,即不属于某一个CPU
的slab cache */
if ((!new.inuse || !prior) && !was_frozen) {
/*
slab为满,释放当前对象后会变成partial,而且
不属于某一个CPU的slab cache,将其冻结,使其
属于当前CPU的slab cache */
if (kmem_cache_has_cpu_partial(s) && !prior) {
new.frozen = 1;
} else {
/*
释放当前的对象后slab为空,先获取节点信息,
并且获取修改slab list所需的锁,以便之后
释放空slab */
n = get_node(s, page_to_nid(page));
/*
只有当前slab为空时才需要获取锁,并且在执行
释放操作后释放锁 */
spin_lock_irqsave(&n->list_lock, flags);
}
}
} while (!cmpxchg_double_slab(s, page,
prior, counters,
object, new.counters,
"__slab_free"));
/*
n为空的可能性较大,即当前释放的对象是slab中的最后一个对象
的可能性较小。其他的可能情况为:
1. slab已满,并且slab不属于某个CPU
2. slab已经属于某个CPU
3. 无论slab是否属于某个CPU,slab的freelist不为空,且inuse
字段不为0 */
if (likely(!n)) {
/*
刚刚执行的冻结操作,将内存页添加到当前CPU的slab
cache的partial表。
这里put_cpu_partial函数将新的slab添加到partial
表时,如果当前CPU中已有的partial对象大于slab
cache的cpu_partial值,就会将当前CPU中的所有的
partial slab解冻,并且在节点中的partial slab
的数量不小于slab cache的cpu_partial值的情况下,
释放解冻的slab;否则将解冻的slab添加到节点的
partial表中。 */
if (new.frozen && !was_frozen) {
put_cpu_partial(s, page, 1);
stat(s, CPU_PARTIAL_FREE);
}
/*
slab已经属于其他CPU的slab cache,当前的CPU不是冻结
slab的CPU,无法执行其他的操作;这种情况下也没有获取
锁的操作,因此也不需要释放 */
if (was_frozen)
stat(s, FREE_FROZEN);
return;
}
/*
释放当前对象后slab为空,并且节点的partial slab数量仍然
大于slab cache中的最小阈值,可以直接将slab释放 */
if (unlikely(!new.inuse && n->nr_partial > s->min_partial))
goto slab_empty;
/* slab从full变为partial */
if (!kmem_cache_has_cpu_partial(s) && unlikely(!prior)) {
if (kmem_cache_debug(s))
remove_full(s, n, page);
add_partial(n, page, DEACTIVATE_TO_TAIL);
stat(s, FREE_ADD_PARTIAL);
}
spin_unlock_irqrestore(&n->list_lock, flags);
return;
slab_empty:
/* 当前释放的对象是slab中的最后一个对象 */
if (prior) {
/* 当前slab有多个对象 */
remove_partial(n, page);
stat(s, FREE_REMOVE_PARTIAL);
} else {
/* 当前slab只有一个对象,导致位于full表 */
remove_full(s, n, page);
}
/* 释放获取的锁 */
spin_unlock_irqrestore(&n->list_lock, flags);
stat(s, FREE_SLAB);
/* 释放slab */
discard_slab(s, page);
}
4. slab的操作函数
__slab_free
函数调用了多个操作slab的函数,包括 put_cpu_partial
, add_partial
, remove_partial
, remove_full
, discard_slab
。
4.1. put_cpu_partial
__slab_free
函数执行时,如果如果slab不属于任何CPU的slab cache,就会在释放对象后把slab添加到当前CPU的slab cache中,作为partial slab。
struct kmem_cache 结构体的 **struct kmem_cache_cpu __percpu *cpu_slab** 成员,其 struct page *partial 包含CPU的所有partial slab。
这些slab通过 struct page 的 struct page *next 成员链接;每个slab(page)包含partial链表中剩余partial slabs所有对象的数量,即 pobjects ,还包含剩余的partial slabs的数量,即 pages 。
也就是说,每个CPU的slab cache包含的partial slab都保存在一个链表中,这个链表通过 *next
指针链接;链表中的第一个slab包含链表中对象的总数和slab的总数,第二个slab包含除了第一个slab外对象的总数和slab的总数。
put_cpu_partial
函数的原型如下:
static void put_cpu_partial(struct kmem_cache *s, struct page *page, int drain);
。
s 是当前正在操作的slab cache, page 是要添加到partial list的slab, drain 是一个标志符,设置后如果当前CPU的slab cache已经超出了 s->cpu_partial ,就会执行 unfreeze_partials
函数,将当前CPU的slab cache中已有的partial slab全部解冻,然后将新增的slab中包含的partial对象的数量添加到统计信息。
slab中包含的partial对象的数量计算方法为 pobjects = page->objects - page->inuse
。
4.1.1. unfreeze_partials
put_cpu_partial
调用 unfreeze_partial
函数时,传入的参数为操作的slab cache和当前CPU的slab cache对象。
static void unfreeze_partials(struct kmem_cache *s,
struct kmem_cache_cpu *c)
{
#ifdef CONFIG_SLUB_CPU_PARTIAL /* x86默认开启 */
struct kmem_cache_node *n = NULL, *n2 = NULL;
/* discard_page中保存要释放的slab */
struct page *page, *discard_page = NULL;
while ((page = c->partial)) {
struct page new;
struct page old;
/*
配合while语句遍历当前CPU的slab cache中
所有的partial slab */
c->partial = page->next;
/* 获取当前slab所属的kmem_cache_node */
n2 = get_node(s, page_to_nid(page));
if (n != n2) {
if (n)
spin_unlock(&n->list_lock);
n = n2;
spin_lock(&n->list_lock);
}
/* 将frozen从1设置为0 */
do {
old.freelist = page->freelist;
old.counters = page->counters;
VM_BUG_ON(!old.frozen);
new.counters = old.counters;
new.freelist = old.freelist;
new.frozen = 0;
} while (!__cmpxchg_double_slab(s, page,
old.freelist, old.counters,
new.freelist, new.counters,
"unfreezing slab"));
/*
!new.inuse为真表示slab为空
如果slab空,并且节点中部分空slab的数量nr_partial不小于
slab cache中指明的最小值min_partial,可以将当前的slab
释放,先添加到discad_page中 */
if (unlikely(!new.inuse && n->nr_partial >= s->min_partial)) {
page->next = discard_page;
discard_page = page;
} else {
/* 添加到节点的partial list中 */
add_partial(n, page, DEACTIVATE_TO_TAIL);
stat(s, FREE_ADD_PARTIAL);
}
}
if (n)
spin_unlock(&n->list_lock);
/* 将所有可以释放的slab释放 */
while (discard_page) {
page = discard_page;
discard_page = discard_page->next;
stat(s, DEACTIVATE_EMPTY);
discard_slab(s, page);
stat(s, FREE_SLAB);
}
#endif
}
4.2. add_partial
/ remove_partial
/ remove_full
三个函数都是操作 struct page 的 struct list_head lru 成员,即从双向链表中直接删除slab项,并且设置 lru->prev = LIST_POISON2
, lru->next = LIST_POISON1
。
需要说明的是,在 struct page 中, lru 和 next , pages , pobjects 等成员位于一个union中:
struct page {
...
union {
struct list_head lru;
struct {
struct page *next;
int pages;
int pobjects;
};
...
};
...
}
也就是说,在执行删除操作时,赋值 lru->next
和 lru->prev
会使 struct page *next 指向 LIST_POISON1 ,其他地方比如 unfreeze_partials
函数通过 next 遍历partial表,不会访问已经删除的slab。
4.3. discard_slab
remove_partial
, remove_full
只是将slab从表中删除,而 discard_slab
则将slab从slab cache中删除。
discard_slab
首先调用 dec_slabs_node
将要删除的slab包含对象的数量从slab cache中删除,并且减少节点的 nr_slabs ,然后调用 free_slab
函数。
free_slab
根据slab cache的标志执行不同的分支,这里介绍 __free_slab
分支。
static void __free_slab(struct kmem_cache *s, struct page *page)
{
/* 获取页面的order */
int order = compound_order(page);
int pages = 1 << order;
...
kmemcheck_free_shadow(page, compound_order(page));
/* 修改zone->vm_stat和全局统计变量vm_stat */
mod_zone_page_state(page_zone(page),
(s->flags & SLAB_RECLAIM_ACCOUNT) ?
NR_SLAB_RECLAIMABLE : NR_SLAB_UNRECLAIMABLE,
-pages);
/* 清除page的标志位 */
__ClearPageSlabPfmemalloc(page);
__ClearPageSlab(page);
/* 设置page->_mapcount = -1 */
page_mapcount_reset(page);
if (current->reclaim_state)
current->reclaim_state->reclaimed_slab += pages;
/* 通过伙伴系统释放页面 */
__free_pages(page, order);
/* 从memcg系统删除 */
memcg_uncharge_slab(s, order);
}
其中,清除标志位的函数都通过 include/linux/page-flags.h 中的宏定义,包括以双下划线开头的和不以双下划线开头的,后者是原子的,前者不是原子的。
#define SETPAGEFLAG(uname, lname) \
static inline void SetPage##uname(struct page *page) \
{ set_bit(PG_##lname, &page->flags); }
#define CLEARPAGEFLAG(uname, lname) \
static inline void ClearPage##uname(struct page *page) \
{ clear_bit(PG_##lname, &page->flags); }
#define __SETPAGEFLAG(uname, lname) \
static inline void __SetPage##uname(struct page *page) \
{ __set_bit(PG_##lname, &page->flags); }
#define __CLEARPAGEFLAG(uname, lname) \
static inline void __ClearPage##uname(struct page *page) \
{ __clear_bit(PG_##lname, &page->flags); }
5. deactivate_slab
虽然释放slab对象的过程中没有调用 deactivate_slab
函数,但是在slab分配对象的慢路径 __slab_alloc
执行的过程中,如果CPU正在使用的slab不属于当前的节点,或者slab设置了pfmemalloc标志,但是请求对象时没有设置ALLOC_NO_WATERMARK,就会将slab移除,放到节点的partial list中
deactivate_slab
函数只有两个函数调用,一个是分配slab对象的慢路径,即 __slab_alloc
;另一个是 flush_slab
函数。
而且,从代码来看,前一种情况的可能性较小,更多的情况是通过后一种路径调用函数,因此默认将移除的slab放到表头。
此处说明 deactivate_slab
时,假定通过 __slab_alloc
函数调用,即 deactivate_slab(s, page, c->freelist)
,则 page 为当前CPU正在使用的slab, freelist 不为空, page 可能位于当前的节点。
static void deactivate_slab(struct kmem_cache *s, struct page *page,
void *freelist)
{
enum slab_modes { M_NONE, M_PARTIAL, M_FULL, M_FREE };
/* n指向要移除的slab所属的内存节点 */
struct kmem_cache_node *n = get_node(s, page_to_nid(page));
int lock = 0;
enum slab_modes l = M_NONE, m = M_NONE;
void *nextfree;
/*
默认情况下,移除的slab放到表头,因为是通过flush_slab
调用,放到表头之后可在短时间内再次使用 */
int tail = DEACTIVATE_TO_HEAD;
struct page new;
struct page old;
/*
如果page->freelist不等于空,说明其他CPU曾经释放对象
到当前要移除的slab中,将其放到表尾 */
if (page->freelist) {
stat(s, DEACTIVATE_REMOTE_FREES);
tail = DEACTIVATE_TO_TAIL;
}
/*
如果freelist不为空,并且要移除的slab中有其他CPU释放的
可用对象,则移除这些对象,将freelist指向slab最后一个
可用的对象 */
while (freelist && (nextfree = get_freepointer(s, freelist))) {
void *prior;
unsigned long counters;
do {
prior = page->freelist;
counters = page->counters;
/* 将freelist指向page->freelist */
set_freepointer(s, freelist, prior);
new.counters = counters;
new.inuse--;
VM_BUG_ON(!new.frozen);
} while (!__cmpxchg_double_slab(s, page,
prior, counters,
freelist, new.counters,
"drain percpu freelist"));
freelist = nextfree;
}
redo:
old.freelist = page->freelist;
old.counters = page->counters;
VM_BUG_ON(!old.frozen);
/* Determine target state of the slab */
new.counters = old.counters;
/* 此时freelist指向移除的slab的最后一个可用对象 */
if (freelist) {
new.inuse--;
/*
将freelist指向page->freelist,即slab中
最后一个空闲对象 */
set_freepointer(s, freelist, old.freelist);
new.freelist = freelist;
} else
/* new.freelist = old.freelist = page->freelist */
new.freelist = old.freelist;
/* 解冻 */
new.frozen = 0;
/*
slab为空,并且节点的partial slab数量大于最小值,
可以释放要移除的slab,设置为M_FREE状态 */
if (!new.inuse && n->nr_partial >= s->min_partial)
m = M_FREE;
/* slab的freelist有可用对象,应该放到partial表 */
else if (new.freelist) {
m = M_PARTIAL;
if (!lock) {
lock = 1;
spin_lock(&n->list_lock);
}
/* 否则是M_FULL */
} else {
m = M_FULL;
if (kmem_cache_debug(s) && !lock) {
lock = 1;
spin_lock(&n->list_lock);
}
}
if (l != m) {
if (l == M_PARTIAL)
remove_partial(n, page);
else if (l == M_FULL)
remove_full(s, n, page);
if (m == M_PARTIAL) {
add_partial(n, page, tail);
stat(s, tail);
} else if (m == M_FULL) {
stat(s, DEACTIVATE_FULL);
add_full(s, n, page);
}
}
l = m;
/*
如果修改page的freelist失败,回到redo重新执行。
这时需要根据l的状态先把之前添加到节点的slab移除,
然后再插入,以保证一致性 */
if (!__cmpxchg_double_slab(s, page,
old.freelist, old.counters,
new.freelist, new.counters,
"unfreezing slab"))
goto redo;
if (lock)
spin_unlock(&n->list_lock);
if (m == M_FREE) {
stat(s, DEACTIVATE_EMPTY);
discard_slab(s, page);
stat(s, FREE_SLAB);
}
}
6. 总结
每个 struct kmem_cache 包含所有内存节点的partial slab信息,每个节点都有一个 struct list_head partial ,保存当前节点的所有partial slab;还有 unsigned long nr_partials 保存本节点partial slab的总数。
struct kmem_cache 包含所有CPU的slab信息,每个CPU只有一个正在分配对象的slab,即 struct page *page 成员,以及指向当前可用对象的指针 void **freelist 。
同时还有属于本CPU的partial slab列表 strut page *partial ,这些partial slab来自CPU的partial list,CPU会冻结属于自己的partial slab——别的CPU只能释放对象,不能执行其他操作。
创建一个新的slab时, struct page 的成员 freelist 会被置空,只通过CPU的 freelist 分配空闲对象。释放对象时也只操作CPU的 freelist。
如果其他CPU释放不属于本CPU的slab的对象,直接操作 struct page 的 freelist ,这样在移除slab时,可以根据这个信息判断是否有异地释放对象的操作。