注:本文分析基于linux-4.18.0-193.14.2.el8_2内核版本,即CentOS 8.2
slab缓存使用需要从buddy system中获取页框并初始化为slab对象提供给进程使用,进程使用结束后,这些对象就会堆积在每个CPU和Node节点上,虽然没有进程在使用,但是其他进程也无法使用,因此需要有进程去回收这些不再使用的slab缓存。
回收可分为被动回收和主动回收,被动回收指系统内存不足时,触发当前需要使用内存的进程阻塞的去释放空闲内存;而主动回收是有进程每个一段时间就去回收,今天我们就来看下主动回收的机制。
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共享链表上。
主要逻辑:
如果之前使用过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); }主要逻辑:
调用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); }主要逻辑:
从前往后释放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--; } }