注:本文分析基于linux-4.18.0-193.14.2.el8_2内核版本,即CentOS 8.2
1、背景
slab缓存使用需要从buddy system中获取页框并初始化为slab对象提供给进程使用,进程使用结束后,这些对象就会堆积在每个CPU和Node节点上,虽然没有进程在使用,但是其他进程也无法使用,因此需要有进程去回收这些不再使用的slab缓存。
回收可分为被动回收和主动回收,被动回收指系统内存不足时,触发当前需要使用内存的进程阻塞的去释放空闲内存;而主动回收是有进程每个一段时间就去回收,今天我们就来看下主动回收的机制。
2、cache_reap
slab的主动回收也可称之为周期回收,因为它是通过每个CPU上的工作队列,定时执行。
cpucache_init ->
cpuhp_setup_state ->
slab_online_cpu -
start_cpu_timer ->
INIT_DEFERRABLE_WORK(reap_work, cache_reap);
在初始化CPU cache时会在每个CPU上注册一个reap_work工作队列,其回调函数是cache_reap,用于周期回收空闲slab缓存。
主要逻辑:
- 回收alien链表,也就是和其他node共享的slab缓存链表
- 回收本地CPU高速缓存对象
- 回收当前node上共享的slab缓存
- 回收部分slabs_free链表上的对象
- 重置定时器,设置每隔2s进行一次slab空闲对象回收
static void cache_reap(struct work_struct *w)
{
struct kmem_cache *searchp;
struct kmem_cache_node *n;
int node = numa_mem_id();
struct delayed_work *work = to_delayed_work(w);
if (!mutex_trylock(&slab_mutex))
/* Give up. Setup the next iteration. */
goto out;
list_for_each_entry(searchp, &slab_caches, list) {
check_irq_on();
n = get_node(searchp, node);
//先回收alien链表,也就是和其他node共享的slab对象链表
reap_alien(searchp, n);
//再回收本地CPU高速缓存对象
drain_array(searchp, n, cpu_cache_get(searchp), node);
if (time_after(n->next_reap, jiffies))
goto next;
//设置此次回收的超时时间,REAPTIMEOUT_NODE,即4s
//因为回收的间隔是2s,所以尽量在下次开始前确保此次结束
n->next_reap = jiffies + REAPTIMEOUT_NODE;
//回收当前node上共享的slab缓存
drain_array(searchp, n, n->shared, node);
if (n->free_touched)
//free_touched不为0,说明此前使用过,回收后置0
n->free_touched = 0;
else {
int freed;
//如果free_touched为0,说明此前没使用过node上的缓存对象
//回收部分slabs_free链表上的对象,回收数量为节点空闲对象上限/5倍每个slab对象数
//且向上取整,比如free_limit=102,num=5,则此时要回收5个空闲对象
freed = drain_freelist(searchp, n, (n->free_limit +
5 * searchp->num - 1) / (5 * searchp->num));
STATS_ADD_REAPED(searchp, freed);
}
next:
cond_resched();
}
check_irq_on();
mutex_unlock(&slab_mutex);
//到下个node去回收slab缓存
next_reap_node();
out:
//设置下次回收缓存时间,REAPTIMEOUT_AC,即2s后
schedule_delayed_work_on(smp_processor_id(), work,
round_jiffies_relative(REAPTIMEOUT_AC));
}
cache_reap通过drain_array去回收kmem_cache_node对应的缓存,包括本地CPU上和当前node共享链表上。
3、drain_array
主要逻辑:
- 如果之前使用过CPU本地高速缓存,则不回收
- 如果之前一段时间都没使用过CPU本地高速缓存,将余下空闲对象都回收,对应的freelist数组也一并回收
- 释放超限的slab对象,并返回到buddy system中
static void drain_array(struct kmem_cache *cachep, struct kmem_cache_node *n,
struct array_cache *ac, int node)
{
LIST_HEAD(list);
/* ac from n->shared can be freed if we don't hold the slab_mutex. */
check_mutex_acquired();
if (!ac || !ac->avail)
return;
//如果之前使用过CPU本地高速缓存,则不回收,因为大概率之后还会使用
if (ac->touched) {
ac->touched = 0;
return;
}
spin_lock_irq(&n->list_lock);
//走到这,说明之前一段时间都没使用过CPU本地高速缓存,收缩下缓存数量
//将余下空闲对象都回收
drain_array_locked(cachep, ac, node, false, &list);
spin_unlock_irq(&n->list_lock);
//释放掉超限的page页面,返回到上级buddy system中
//对应的freelist数组也一起释放
slabs_destroy(cachep, &list);
}
4、drain_array_locked
主要逻辑:
- 调用free_block释放本地CPU剩余对象,并更新对应page状态
- 跟新entry链表
static void drain_array_locked(struct kmem_cache *cachep, struct array_cache *ac,
int node, bool free_all, struct list_head *list)
{
int tofree;
if (!ac || !ac->avail)
return;
//默认回收CPU本地高速缓存的剩余空闲对象
tofree = free_all ? ac->avail : (ac->limit + 4) / 5;
if (tofree > ac->avail)
tofree = (ac->avail + 1) / 2;
//释放本地CPU剩余对象,并将对应的page更新到对应slab链表
free_block(cachep, ac->entry, tofree, node, list);
ac->avail -= tofree;
//因为是从前往后释放,因此将前面几个对象覆盖
memmove(ac->entry, &(ac->entry[tofree]), sizeof(void *) * ac->avail);
}
5、free_block
主要逻辑:
- 从前往后释放entry链表上的空闲对象,主要是操作freelist索引数组
- 根据释放后page的状态将其挂到slabs_free或者slabs_partial链表
- 如果释放后该节点的空闲对象数量超限,将超限的slab挂到list链表,交由上层函数统一释放
static void free_block(struct kmem_cache *cachep, void **objpp,
int nr_objects, int node, struct list_head *list)
{
int i;
struct kmem_cache_node *n = get_node(cachep, node);
struct page *page;
n->free_objects += nr_objects;
//释放本地CPU上剩余空闲对象,从前往后释放
for (i = 0; i < nr_objects; i++) {
void *objp;
struct page *page;
objp = objpp[i];
//通过对象获取对应slab/page地址
page = virt_to_head_page(objp);
list_del(&page->slab_list);
check_spinlock_acquired_node(cachep, node);
//释放空闲对象,其实操作的就是freelist索引数组
slab_put_obj(cachep, page, objp);
STATS_DEC_ACTIVE(cachep);
//根据释放后page的状态将其挂到slabs_free或者slabs_partial链表
if (page->active == 0) {
list_add(&page->slab_list, &n->slabs_free);
n->free_slabs++;
} else {
/* Unconditionally move a slab to the end of the
* partial list on free - maximum time for the
* other objects to be freed, too.
*/
list_add_tail(&page->slab_list, &n->slabs_partial);
}
}
//如果释放后该节点的空闲对象数量超限,且空闲slab链表不为空
//需要将超限的slab释放,让其返回上级buddy system中
while (n->free_objects > n->free_limit && !list_empty(&n->slabs_free)) {
n->free_objects -= cachep->num;
page = list_last_entry(&n->slabs_free, struct page, slab_list);
//将超限的页面挂到临时链表上,上层函数会统一释放
list_move(&page->slab_list, list);
n->free_slabs--;
n->total_slabs--;
}
}