背景:
事情是这样来的,测压力测试,发现内存在缓慢上涨(一天涨5MB左右),坑D了,已经用mleak扫描过,内核模块不应该有泄露。
然后开始往细节上学习linux内存知识。
经过几天学习,找各种资料和书来看slab什么的。算是有所掌握了slab。怕时间一久自己给忘记了,故写下一些杂言,帮助将来快速恢复。
几个问题:
1.内存回收,dcache,slab 回收,它们是个啥关系,基本机制是啥?
slab本身
学习的过程,非常像盲人摸象的过程。有好几个方面,单独抽一个部分来学,并不好学,也不明白。但是有时候又不得不从部分学起,直到该有的都知道了,一个干净
纯洁的东西,出现了。简化时就是一个最基础模型,扩展开来就是完整的全部。哪个部分是为什么而来,有什么好处都清楚知道。哪个部件负责什么,边界又是什么也清楚。从问题答案角度来讲,学的时候有许多问题,学完应该都能回答。
Q1: slab最基础的结构模型是啥?
我们从最基础的slab一步步来搭出kernel现在的slab.
1.slab就是一个包含多个obj的内存页组,然后多个slab构成一个list.
2.由于存在3类slab状态,full.partial,free。所以整了3个list来加速。
struct kmem_cache_node {
struct list_head slabs_partial; /* partial list first, better asm code */
struct list_head slabs_full;
struct list_head slabs_free;
}
3.然后多个不同名字的slab构成slab_chain.
4.这个版本太简单了,得填充细节,优化性能。
由于buddy系统是page^order来分配page的,所以slab是以gfporder来申请释放内存页的。
struct kmem_cache {
unsigned int gfporder;
}
这样slab内部是连续的内存块,可以紧凑的放下多个obj。如何有效管理slab的obj了?考虑数据的warm热性,LIFO先进后出性能最好,因为它更可能在cache上,缩短load时间。用一个stack是合适的,最初的slab就是用的list链表,不过linux使用array数组来实现stack,因为真的不需要伸缩。一个数组加一个index就能构成stack。
struct slab {
kmem_bufctl_t free; 这个就是stack栈索引,SP。
它的运行机制有点小技巧,是用数字表示obj位置,存在index->obj addr的转换。还用负数表示BUFCTL_END ,BUFCTL_FREE
}
数组在哪了?它本身在初始化时动态分配大小。因此没有出现在slab,而是在slab之后。
static inline kmem_bufctl_t *slab_bufctl(struct slab *slabp)
{
return (kmem_bufctl_t *) (slabp + 1);
}
这样就高效的管理了free obj了。如何初始化stack,这个真不用太看。
5. slab着色
slab color听起来是不是高大上,有点玄乎?其实它从内容上看并不多,也不难。属于性能优化。
简单点讲,就是对象本来都是整整齐齐的分布在slab上。这不利于CPU cache line:cache line希望你们不要占同一个地址的坑。最好是分布在不同的cache line上。
着色就是:如果可以,就将slab上的对象组整体以cache line(通常64B)进行填充偏移。这个每个slab上的obj起始位置将均匀分布在不同的cache line上。
理论分析解释:
1.CPU现在有L1,L2,L3级缓存,每个数据是以cache line进行操作的,由于cache比主存小很多,就必然有替换算法。由于数据和指令的相邻效应,相应cache line地址肯定不会冲突。局部性是要优先保证的。造成cache冲突的是非局部性地址块(局部性大小=total cache size/N-way),主流的是N-way组相联,本质就是地址hash后,hash地址冲突的定长为N的链表。
2.)对全相联的确是没有作用。
3.)对组相联cache来说,作用还是有的,首先slab的obj数据里面也是有热点区域,如果着色,比如一个热点区数据,如果不着色,理想的cache冲突深度为N-way。如果着色有一个cache size的偏移,假设obj数量很多,概率是均匀分布(因为slab就是顺序来填的)。那么着色收益,将是cache冲突深度为2*N-way。相当于将CPU硬件的N-way,比如8路组相联变成16路组相联。提高了热点数据的容纳深度(也可以理解为宽度,因为它真的是热点数据移动到旁边的坑里,占了更的的坑)。
6. slab这一层就2个东西:着色,LIFO先进后管理空闲obj. 还有一个使用计数unsigned int inuse方便管理状态,用空就去full list,没用一个自然是free list。
7. per CPU多核加速的stack free obj
struct array_cache {
unsigned int limit; stack top
unsigned int avail; SP
void *entry[]; stack数组
}
多核已经是主流,为了降低并发访问的开销,无锁的per CPU方案自然是更好的选择(就是每个CPU使用自己专用的slab list,是不是听起来很爽)。不过了,由于linux支持
抢占,所以还有一个锁来防止抢占。slab为每个CPU分配一个array_cache。里面配置一个数组来缓存free的obj。为了用上cache-warm objects,数组还是LIFO机制的栈,SP是ac->avail。注意这个SP指向的是void *,比slab里的stack要省事,直接指向对象obj。
还要注意一点:obj放回slab list时,要obj addr->slab的转换。利用obj->page->slab的方法。
page结构体太难了,对于不同的page应用,page有多种身份。其实struct page是个union多态。如此紧凑的struct page内存使用,真的是C语言系才能胜任。
slabp = virt_to_slab(objp);
static inline struct slab *virt_to_slab(const void *obj)
{
struct page *page = virt_to_head_page(obj);
return page->slab_page; 我struct page里面还在了slab 相关的数据指针,666
}
然后unsigned int batchcount;来了。当stack满了时,一次批量将obj释放回slab的数量。这样是不是比一个一个释放要高效。
8 生长和回收
当slab obj没有了,就要从buddy中申请。grow倒没什么好说的。
回收reclaim是重点,也是难点。这有个性能平衡的考虑。过于激进的回收释放page,free内存是多了,但是重新申请时,又有时间上的开销。
1.struct kmem_cache_node { unsigned long free_objects;unsigned int free_limit; } 当一个slab是free的,同时 free_objects>free_limit,就释放这个slab回buddy.
2.shrink_slab()是一个重要的入口,dentry cache,inode cache之类可以shrink的会被调用各自的shrnker,释放自己的slab obj回到slab.然后slab内部释放page.
可以看到shrink只有通知建议权,如何释放归各使用都自己管。
好像完了。
Q2: slab内存回收是啥?slab可回收和不可回收有啥区别?
这个问题最坑爹了,不敢说能回答对,就目前而言:它们真没啥区别!唯一的区别就是显示时分类下,安慰一下user。可回收slab几乎全部和文件系统VFS有关:主要是dentry,inode。然而可回收也只能把没有用的dentry,inode给释放,如果在使用中,也是不可能回收滴。
在我没有学完前,一度以为SLAB_RECLAIM_ACCOUNT会有什么黑魔法,有啥激进的回收策略。现在看起来,真的只是用来标识分类。不用没有任何区别。不会少回收内存。
Q3,dentry cache,inode cache是如何回收?
dentry有4种状态,in used, no used,negative,free. 当不在inused时,会被放入LRU中,当系统要回收时,会通知dcache释放时,它选择要释放的dentry回到slab.