SLAB缓存管理之缓存回收cache_reap

注:本文分析基于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--;
	}
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值