一、为啥越来越多使用slub
slab分配器的弊端,我们知道slab分配器中每个node结点(用slab结构体kmem_cache中的kmem_cache_node结构体表示node)有三个链表,分别是空闲slab链表(slabs_free),部分空slab链表(slabs_partial),已满slab链表(slabs_full),这三个链表中维护着对应的slab缓冲区。我们也知道slab缓冲区的内存是从伙伴系统中申请过来的,假设,如果没有内存回收机制的情况下,只要申请的slab缓冲区就会存入这三个链表中,并不会返回到伙伴系统里,如果这个类型的SLAB迎来了一个分配高峰期,将会从伙伴系统中获取很多页面去生成许多slab缓冲区,之后这些slab缓冲区并不会自动返回到伙伴系统中,而是会添加到node结点的这三个slab链表中去,这样就会有很多slab缓冲区是很少用到的。而slub分配器把node结点的这三个链表精简为了一个链表,只保留了部分空slab链表(partial),而SLUB中对于每个CPU来说已经不使用空闲对象链表,而是直接使用单个slab,并且每个CPU都维护有自己的一个部分空链表。在slub分配器中,对于每个node结点,也没有了所有CPU共享的空闲对象链表。
发明SLUB分配器的主要目的就是减少slab缓冲区的个数,让更多的空闲内存得到使用。首先,SLUB和SLAB一样,都分为多种,同时也分为专用SLUB和普通SLUB。如TCP,UDP,dquot这些,它们都是专用SLAB,专属于它们自己的模块。而后面这张图,如kmalloc-8,kmalloc-16...还有dma-kmalloc-96,dma-kmalloc-192...在这方面与SLAB是一样的,同样地,也是使用一个struct kmem_cache结构来描述一个SLUB(与SLAB一样)。并且这个struct kmem_cache与SLAB的struct kmem_cache几乎是同一个,通过编译选项来具体区分,而且对于SLAB和SLUB,向外提供的接口是统一的(函数名、参数以及返回值一模一样),这样也就让驱动和其他模块在编写代码时无需操心系统使用的是SLAB还是SLUB。这是为了同一个内核可以通过编译选项使用SLAB或者SLUB。
下图是一个来自别人的一个slub分配器结构图:(参见https://blog.csdn.net/chenying126/article/details/78451344)
再来个简图(参考:https://www.ibm.com/developerworks/cn/linux/l-linux-slab-allocator/index.html),需要注意的是对于slub来说,cpu缓存中只有slabs_partial部分空一个链表了,默认没有full(Debug有)和empty;
此结构是slab分配器结构,每个SLAB缓存都有它自己的名字,例如kmalloc-8,kmalloc-16等。总的来说,kmem_cache结构用于描述一种SLAB,并且管理着这种SLAB中所有的对象。所有的kmem_cache结构会保存在以slab_caches作为头的链表中。在c可以通过kmem_cache_create自行创建一个kmem_cache用于管理属于自己模块的SLAB。
二、几个重要的结构体及其关系
下面来个图,这个图是真心的很棒,对于理解slub分配原理,各数据结构元素的意义的理解和代码流程的理解都非常重要。
这些数据结构之间的关系具体简单的例子,很形象。什么是slab缓存池呢?我的解释是使用struct kmem_cache结构描述的一段内存就称作一个slab缓存池。一个slab缓存池就像是一箱牛奶,一箱牛奶中有很多瓶牛奶,每瓶牛奶就是一个object。分配内存的时候,就相当于从牛奶箱中拿一瓶。总有拿完的一天。当箱子空的时候,你就需要去超市再买一箱回来。超市就相当于partial链表,超市存储着很多箱牛奶。如果超市也卖完了,自然就要从厂家进货,然后出售给你。厂家就相当于伙伴系统。
图来自:http://www-x-wowotech-x-net.img.abc188.com/content/uploadfile/201803/4a471520078976.png
slub中主要的结构体有kmem_cache,kmem_cache_cpu,kmem_cache_node,还有个与页描述符复用的slab描述符page结构体,仔细想想,我们把kmem_cache叫做slub缓存(slub分配器),那么它里面存放的是什么呢?当然是slab描述符喽,由于slab是由伙伴系统分配的一段连续的物理页,所以用page描述符复用slab描述符,也解释的通。所以slub缓存的一系列操作其实都是基于slab描述符,也就是page描述符,因此每cpu缓存kmem_cache_cpu(当前slab描述符和使用了部分对象的slab描述符链表)和kmem_cache_node(其中三个链表保存的是这组页框的首页框的SLAB描述符)节点缓存的维护,都是维护的slab描述符(基于page描述符),这里就有利于理解上图。记下来细看各个数据结构。
1)kmem_cache结构
struct kmem_cache {struct kmem_cache_cpu __percpu *cpu_slab;/* 标志 */unsigned long flags;/* 每个node结点中部分空slab缓冲区数量不能低于这个值 */unsigned long min_partial;/* 分配给对象的内存大小(大于对象的实际大小,大小包括对象后边的下个空闲对象指针) */int size; /* 对象的实际大小 */int object_size; /* 存放空闲对象指针的偏移量 */int offset; /* cpu的可用objects数量范围最大值 */int cpu_partial; /* 保存slab缓冲区需要的页框数量的order值和objects数量的值,通过这个值可以计算出需要多少页框,这个是默认值,初始化时会根据经验计算这个值 */struct kmem_cache_order_objects oo;/* 保存slab缓冲区需要的页框数量的order值和objects数量的值,这个是最大值 */struct kmem_cache_order_objects max;/* 保存slab缓冲区需要的页框数量的order值和objects数量的值,这个是最小值,当默认值oo分配失败时,会尝试用最小值去分配连续页框 */struct kmem_cache_order_objects min;/* 每一次分配时所使用的标志 */gfp_t allocflags; /* 重用计数器,当用户请求创建新的SLUB种类时,SLUB 分配器重用已创建的相似大小的SLUB,从而减少SLUB种类的个数。 */int refcount; /* 创建slab时的构造函数 */void (*ctor)(void *);/* 元数据的偏移量 */int inuse; /* 对齐 */int align; int reserved; /* 高速缓存名字 */const char *name; /* 所有的 kmem_cache 结构都会链入这个链表,链表头是 slab_caches */struct list_head list; #ifdef CONFIG_SYSFS/* 用于sysfs文件系统,在/sys中会有个slub的专用目录 */struct kobject kobj; #endif#ifdef CONFIG_MEMCG_KMEM/* 这两个主要用于memory cgroup的 */struct memcg_cache_params *memcg_params;int max_attr_size; #ifdef CONFIG_SYSFSstruct kset *memcg_kset;#endif#endif#ifdef CONFIG_NUMA/* 用于NUMA架构,该值越小,越倾向于在本结点分配对象 */int remote_node_defrag_ratio;#endif/*在这个结构中,最重要的可能就属struct kmem_cache_node * node[MAX_NUMNODES]这个指针数组了,指向的struct kmem_cache_node中保存着slab链表,在NUMA架构中每个node对应数组中的一个元素,因为每个SLAB高速缓存都有可能在不同结点维护有自己的SLAB用于这个结点的分配*/struct kmem_cache_node *node[MAX_NUMNODES];};
扫一下整个kmem_cache结构,知识点最重要的有4个:每CPU对应的cpu_slab结构,每个node结点对应的kmem_cache_node结构,slub重用以及struct kmem_cache_order_objects结构对应的oo,max,min这三个值。
除去以上4个知识点,先简单说说kmem_cache中的一些成员变量:
1) cpu_slab:一个per cpu变量,对于每个cpu来说,相当于一个本地内存缓存池。当分配内存的时候优先从本地cpu分配内存以保证cache的命中率。
2) flags:object分配掩码,例如经常使用的SLAB_HWCACHE_ALIGN标志位,代表创建的kmem_cache管理的object按照硬件cache 对齐,一切都是为了速度。
3) min_partial:限制struct kmem_cache_node中的partial链表slab的数量。虽说是mini_partial,但是代码的本意告诉我这个变量是kmem_cache_node中partial链表最大slab数量,如果大于这个mini_partial的值,那么多余的slab就会被释放。
4) size:分配的object size
5) object_size:实际的object size,就是创建kmem_cache时候传递进来的参数。和size的关系就是,size是各种地址对齐之后的大小。因此,size要大于等于object_size。
6) offset:slub分配在管理object的时候采用的方法是:既然每个object在没有分配之前不在乎每个object中存储的内容,那么完全可以在每个object中存储下一个object内存首地址,就形成了一个单链表。很巧妙的设计。那么这个地址数据存储在object什么位置呢?offset就是存储下个object地址数据相对于这个object首地址的偏移。
7) cpu_partial:per cpu partial中所有slab的free object的数量的最大值,超过这个值就会将所有的slab转移到kmem_cache_node的partial链表。
8) oo&#xff1a;低16位代表一个slab中所有object的数量&#xff08;oo & ((1 <<16) - 1)&#xff09;&#xff0c;高16位代表一个slab管理的page数量&#xff08;(2^(oo 16)) pages&#xff09;。
9) max&#xff1a;看了代码好像就是等于oo。
10) min&#xff1a;当按照oo大小分配内存的时候出现内存不足就会考虑min大小方式分配。min只需要可以容纳一个object即可。
11) allocflags&#xff1a;从伙伴系统分配内存掩码。
12) inuse&#xff1a;object_size按照word对齐之后的大小。
13) align&#xff1a;字节对齐大小。
14) name&#xff1a;sysfs文件系统显示使用。
15) list&#xff1a;系统有一个slab_caches链表&#xff0c;所有的slab都会挂入此链表。
16) node&#xff1a;slab节点。在NUMA系统中&#xff0c;每个node都有一个struct kmem_cache_node数据结构。
2&#xff09;kmem_cache_cpu结构体
再来看看struct kmem_cache_cpu __percpu *cpu_slab&#xff0c;对于同一种kmem_cache来说&#xff0c;每个CPU对应有自己的struct kmem_cache_cpu结构&#xff0c;这个结构如下&#xff1a;
struct kmem_cache_cpu {/* 指向下一个空闲对象&#xff0c;用于快速找到对象 */void **freelist;/* 用于保证cmpxchg_double计算发生在正确的CPU上&#xff0c;并且可作为一个锁保证不会同时申请这个kmem_cache_cpu的对象 */unsigned long tid; /* CPU当前所使用的slab缓冲区描述符&#xff0c;freelist会指向此slab的下一个空闲对象 */struct page *page; /* CPU的部分空slab链表&#xff0c;放到CPU的部分空slab链表中的slab会被冻结&#xff0c;而放入node中的部分空slab链表则解冻&#xff0c;冻结标志在slab缓冲区描述符中 */struct page *partial;#ifdef CONFIG_SLUB_STATSunsigned stat[NR_SLUB_STAT_ITEMS];#endif};
在此结构中主要注意有个partial部分空slab链表以及page指针&#xff0c;page指针指向当前使用的slab缓冲区描述符&#xff0c;内核中slab缓冲区描述符与页描述符共用一个struct page结构。SLUB分配器与SLAB分配器有一部分不同就在此&#xff0c;SLAB分配器的每CPU结构中保存的是空闲对象链表&#xff0c;而SLUB分配器的每CPU结构中保存的是一个slab缓冲区。而对于tid&#xff0c;它主要用于检查是否有并发&#xff0c;对于一些操作&#xff0c;操作前读取其值&#xff0c;操作结束后再检查其值是否与之前读取的一致&#xff0c;非一致则要进行一些相应的处理&#xff0c;这个tid一般是递增状态&#xff0c;每分配一次对象加1。这个结构说明了一个问题&#xff0c;就是每个CPU有自己当前使用的slab缓冲区&#xff0c;CPU0不能够使用CPU1所在使用的slab缓存&#xff0c;CPU1也不能够使用CPU0正在使用的slab缓存。而CPU从node获取slab缓冲区时&#xff0c;一般倾向于从该CPU所在的node结点上分配&#xff0c;如果该node结点没有空闲的内存&#xff0c;则根据memcg以及node结点的zonelist从其他node获取slab缓冲区。
3&#xff09;kmem_cache_node结构&#xff1a;
再看看kmem_cache_node结构&#xff1a;
struct kmem_cache_node {/* 锁 */spinlock_t list_lock;/* SLAB使用 */#ifdef CONFIG_SLAB/* 只使用了部分对象的SLAB描述符的双向循环链表 */struct list_head slabs_partial; /* partial list first, better asm code *//* 不包含空闲对象的SLAB描述符的双向循环链表 */struct list_head slabs_full;/* 只包含空闲对象的SLAB描述符的双向循环链表 */struct list_head slabs_free;/* 高速缓存中空闲对象个数(包括slabs_partial链表中和slabs_free链表中所有的空闲对象) */unsigned long free_objects;/* 高速缓存中空闲对象的上限 */unsigned int free_limit;/* 下一个被分配的SLAB使用的颜色 */unsigned int colour_next; /* Per-node cache coloring *//* 指向这个结点上所有CPU共享的一个本地高速缓存 */struct array_cache *shared; /* shared per node */struct alien_cache **alien; /* on other nodes *//* 两次缓存收缩时的间隔&#xff0c;降低次数&#xff0c;提高性能 */unsigned long next_reap; /* 0:收缩 1:获取一个对象 */int free_touched; /* updated without locking */#endif/* SLUB使用 */#ifdef CONFIG_SLUBunsigned long nr_partial; struct list_head partial; /* 只使用了部分对象的SLAB描述符的双向循环链表 */#ifdef CONFIG_SLUB_DEBUG/* 该node中此kmem_cache的所有slab的数量 */atomic_long_t nr_slabs;/* 该node中此kmem_cache中所有对象的数量 */atomic_long_t total_objects;struct list_head full;#endif#endif};
这个结构中对于slub&#xff0c;只需要看#ifdef CONFIG_SLUB部分&#xff0c;这个结构里正常情况下只有一个node结点部分空slab链表partial&#xff0c;如果在编译内核时选择了CONFIG_SLUB_DEBUG选项&#xff0c;则会有个node结点满slab链表full。对于SLAB分配器&#xff0c;SLUB分配器在这个结构也做出了相应的变化&#xff0c;去除了满slab缓冲区链表和空闲slab缓冲区链表&#xff0c;只使用了一个部分空slab缓冲区链表。对于所有的CPU来说&#xff0c;它们可以使用这个node结点里面部分空链表中保存的那些slab缓冲区&#xff0c;当它们需要使用时&#xff0c;要先将缓冲区拿到CPU对应自己的链表或者当前使用中&#xff0c;也就是说node结点上部分空slab缓冲区同一个时间只能让一个CPU使用。
而关于slub重用&#xff0c;这里只做一个简单的解释&#xff0c;其作用是为了减少slub的种类&#xff0c;比如我有个kmalloc-8类型的slub&#xff0c;里面每个对象大小是8&#xff0c;而我某个驱动想申请自己所属的slub&#xff0c;其对象大小是6&#xff0c;这时候系统会给驱动一个假象&#xff0c;让驱动申请了自己专属的slub&#xff0c;但系统实际把kmalloc-8这个类型的slub返回给了驱动&#xff0c;之后驱动中分配对象时实际上就是从kmalloc-8中分配对象&#xff0c;这就是slub重用&#xff0c;将相近大小的slub共用一个slub类型&#xff0c;虽然会造成一些内碎片&#xff0c;但是大大减少了slub种类过多以及减少使用了跟多的内存。