前言:目前有很多讲slab的文章,要么是纯讲原理画一堆图结合源码不深导致理解困难,要么是纯代码注释导致理解更困难,我在猛攻了一周时间后,细致总结一下slab,争取从原理到源码都能细致的理解到并立刻达到清楚的使用。
好文推荐:
一、slab分配器概述
有了伙伴系统buddy,我们可以以页为单位获取连续的物理内存了,即4K为单位的获取,但如果需要频繁的获取/释放并不大的连续物理内存怎么办,如几十字节几百字节的获取/释放,这样的话用buddy就不太合适了,这就引出了slab。
比如我需要一个100字节的连续物理内存,那么内核slab分配器会给我提供一个相应大小的连续物理内存单元,为128字节大小(不会是整好100字节,而是这个档的一个对齐值,如100字节对应128字节,30字节对应32字节,60字节对应64字节),这个物理内存实际上还是从伙伴系统获取的物理页;当我不再需要这个内存时应该释放它,释放它并非把它归还给伙伴系统,而是归还给slab分配器,这样等再需要获取时无需再从伙伴系统申请,这也就是为什么slab分配器往往会把最近释放的内存(即所谓“热”)分配给申请者,这样效率是比较高的。
二、创建一个slab
2.1、什么叫创建slab
上面举了申请100字节连续物理内存的例子,还提到了实际分配的是128字节内存,也就是实际上内核中slab分配器对不同长度内存是分档的,其实这就是slab分配器的一个基本原则,按申请的内存的大小分配相应长度的内存。
同时也说明一个事实,内核中一定应该有这样的按不同长度slab内存单元,也就是说已经创建过这样的内存块,否则申请时怎能根据大小识别应该分配给怎样大小的内存,这可以先参加kmalloc的实现,kmalloc->__do_kmalloc,__do_kmalloc函数中的如下:
加深的部分就是说,kmalloc申请的物理内存长度为参数size,它需要先根据这个长度找到相应的长度的缓存,这个缓存的概念是什么马上就要引出先别着急,先看函数__find_general_cachep:
如上面加深的部分所示,这个函数唯一有用的部分就是这里,csizep初始化成全局变量malloc_sizes,根据全局变量malloc_sizes的cs_size成员和size的大小比较,不断后移malloc_sizes,现在就要看看malloc_sizes是怎么回事:
【文章福利】小编推荐自己的Linux内核技术交流群:【】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100名进群领取,额外赠送一份价值699的内核资料包(含视频教程、电子书、实战项目及代码)
学习直通车:
内核资料直通车:
观察文件linux/kmalloc_sizes.h的情况,篇幅太大这个文件内容就不列了,里面都是一堆的CACHE(X)的宏声明,根据里边的定制宏情况(L1_CACHE_BYTES值为32,KMALLOC_MAX_SIZE值为4194304),一共声明了CACHE(32)、CACHE(64)、CACHE(96)、CACHE(128)、CACHE(192)、CACHE(256)、CACHE(512)、CACHE(1024)、CACHE(2048)、CACHE(4096)、CACHE(8192)、CACHE(16384)、CACHE(32768)、CACHE(65536)、CACHE(131072)、CACHE(262144)、CACHE(524288)、CACHE(1048576)、CACHE(2097152)、CACHE(4194304)和最后的CACHE(0xffffffff)共计21个CACHE(X)的宏声明,结合结构类型struct cache_sizes,对于arm它实际上有两个成员:
除X86以外基本都没有DMA必须在物理内存前16MB的限制,所以包括arm的很多体系结构都没有CONFIG_ZONE_DMA,所以本结构实际上是两个成员cs_size和cs_cachep,那么这里就比较清晰了,全局变量malloc_sizes共有21个成员,每个成员都定义了cs_size值,从32到4194304加上0xffffffff,cs_cachep都为NULL;其实这些值就是slab分配器的一个个按长度的分档;
回到函数__find_general_cachep,已经很清晰了,全局变量malloc_sizes的第0个成员开始,当申请的内存长度比该成员的档次值cs_size大,就换下一个成员,直到比它小为止,仍然如申请100字节的例子,在96字节的分档时还比申请长度小,在128字节的分档时就可以满足了,这就是为什么说申请100字节实际获取到的是128字节的内存单元的原因。
回到函数__do_kmalloc,接下来调用的是__cache_alloc,将按照前面确定的内存分档值给申请者分配一个相应值的内存,这说明,内核有能力给分配这样的内存单元;
内核为什么有能力创建这样的内存单元?slab分配器并非一开始就能智能地根据内存分档值分配相应长度的内存的,它需要先创建一个这样的“规则”式的东西,之后才可以根据这个“规则”分配相应长度的内存,看看前面的结构struct cache_sizes的定义,里边的成员cs_cachep,它的结构类型是struct kmem_cache *,这个结构也是同样是刚才提到的缓存的概念,每种长度的slab分配都得通过它对应的cache分配,换句话说就是每种cache对应一种长度的slab分配,这里顺便能看看slab分配接口,一个是函数kmalloc一个是函数kmem_cache_alloc,kmalloc的参数比较轻松,直接输入自己想要的内存长度即可,由slab分配器去找应该是属于哪个长度分档的,然后由那个分档的kmem_cache结构指针去分配相应长度内存,而kmem_cache_alloc就显得比较“专业”,它不是输入我要多少长度内存,而是直接以kmem_cache结构指针作为参数,直接指定我要这样长度分档的内存,稍微看看这两个函数的调用情况就可以发现它们很快都是调用函数__cache_alloc,只是前面的这些不同而已。
比如现在有一个内核模块想要申请一种它自创的结构,这个结构是111字节,并且它不想获取128字节内存就想获取111字节长度内存,那么它需要在slab分配器中创建一个这样的“规则”,这个规则规定slab分配器当按这种“规则”分配时要给我111字节的内存,这个“规则”的创建方法就是调用函数kmem_cache_create;
同样,内核slab分配器之所以能够默认地提供32-4194304共20种内存长度分档,肯定也是需要创建这样20个“规则”的,这是在初始化时创建的,由函数kmem_cache_init,先不要纠结kmem_cache_init,它里边有一些道理需要在理解slab分配器原理后才能更好的理解,先看kmem_cache_create
2.2、创建slab的过程
现在去看结构kmem_cache的各个成员定义是很模糊的,直接看函数源码:
直到函数中的“if (slab_is_available()) gfp = GFP_KERNEL;”这里,前面的都可以不用关注,分别是运行环境和参数的检查(需要注意本函数会可能睡眠,所以绝不能在中断中调用本函数)、一堆对齐机制的东西,看看这一段:
到这里首先根据当前slab是否初始化完成确定变量gfp的值,gfp并不陌生,它规定了从伙伴系统寻找内存的地点和方式,这里的在slab初始化完成时gfp值为GFP_KERNEL说明了为什么可能会睡眠,而slab初始化完成之前gfp值为GFP_NOWAIT说明不会睡眠;
接下来是获取一个kmem_cache结构,调用kmem_cache_zalloc,它和kmem_cache_zalloc唯一区别就是会对所分配区域进行清零操作,即在kmem_cache_alloc函数的gfp参数中加入标志__GFP_ZERO,其他没有区别;
由前面2.1节的分析已知,如果想要通过slab分配器获取某长度的内存,必须创建这样的“规则”,那么现在需要一个kmem_cache结构体长度的内存,同样也是需要一个该长度的“规则”,没错该长度的“规则”也是在初始化函数kmem_cache_init中创建,而我们创建这个“规则”的结果就是全局变量cache_cache,所以现在需要申请一个kmem_cache结构体长度的内存时就通过全局变量cache_cache这样一个已创建好的kmem_cache结构变量。
不过全局变量cache_cache并不是一个理解slab创建的好例子,原因在后面就会明白,理解slab还是继续观察函数kmem_cache_create,接下来是确定slab管理对象的存储方式:
这里引出了slab管理对象的存储方式,分为内置和外置,简单地说,内置就是说slab管理部分的内容和实际供使用的内存都在申请到的内存区域中,外置slab管理部分的内容自己再单独申请一个内存区域,和实际申请的内存区域分开,所谓slab管理部分,包括slab结构体、对象描述符,后面会细致描述,这里的if的意思是,当slab初始化完成后,如果创建的“规则”的内存长度大于(PAGE_SIZE >> 3)即512字节时,就使用外置方式,否则使用内置方式,初始化完成之前均使用内置方式。
接下来是left_over = calculate_slab_order(cachep, size, align, flags);这是在计算,所创建的“规则”的内存长度size,最终创建的slab将应该有多少个物理页面、有多少个这样size的对象、有多少碎片(碎片就是说申请的内存长度除了对象以外剩下的不能用的内存的长度):
calculate_slab_order通过for循环调用函数cache_estimate就是最终得出了所要创建的“规则”的内存长度size,也就是创建这样的slab,每个slab有多少物理页,每个slab有多少个这样的对象,每个slab的碎片是多大;每个slab其实最多2个物理页,所能容纳的size大小的对象个数与外置还是内置相关,外置情况下slab管理对象不占用所申请的空间,内置则占用,slab管理对象包括slab结构长度和“对象个数”个对象描述符;
小结:在调用完calculate_slab_order后,能算出这样的slab应该从伙伴系统申请多少物理页(最多2页)(cache->gfporder),里边有多少个期望长度(size)的对象(cache->num),每个slab的碎片是多大(变量left_over);
接下来是一部分根据碎片大小情况,可能得把外置slab改造成内置slab的情况,不用特别关注,这往往出现在由于申请长度size和对齐单位align的值的原因,实际改为内置的话可省下很多空间即碎片可减小很多的情况;最终得出内置/外置的管理对象大小slab_size和碎片大小left_over(源码不贴了就);
接下来是对该“规则”的slab的一些属性设置:
前面几个是关于着色的,着色将在后面描述一下,但个人认为对于着色不用特别关注,要知道它的原理和作用方式,但着色在事实上有它的缺点,并且导致slab的管理非常复杂,在linux后续版本更多是通过slub来替代slab的着色机制;
接下来是管理对象大小slab_size、slab的分配flag、对象大小buffer_size(及其倒数)、伙伴系统接口gfpflag、构造函数ctor、slab名称name的设置;
需要注意一下对于外置slab的slab管理对象的位置,已经知道外置slab的slab管理对象不在所申请的空间内而是另外再申请一段空间,源码就是对外置slab的slabp_cache专门再申请管理对象slab_size大小的一段空间用于存储外置slab的管理对象,对于内置slab无需关注该成员默认为NULL;
接下来是一个重点内容,为该slab创建其本地缓存(local cache)和slab三链,函数setup_cpu_cache
现在可以先看看结构体kmem_cache,如下:
到目前为止还未设置的成员有:
batchcount和limit与实际分配内存相关,shared只在多CPU情况下有意义,dflags暂无需关注,重点关注array和nodelists,它们涉及了所申请内存的分配机制:
在实际开始分配内存时,每个CPU都从kmem_cache结构体中的array中获取需要的内存,如果这里没有内存(用光或第一次用,第一次都是没有内存的需从buddy获取),需要从buddy获取,从buddy获取的方式是通过slab三链的成员nodelists(slab三链这个名字是发现某个文章中这么叫的,所谓slab三链,是指全空、半空、全满三种slab链表)从buddy获取到物理页,然后把相关的物理页地址再传给array,可以看到在kmem_cache结构中array是每个CPU都有一个的(NR_CPUS代表CPU个数),之所以有这种机制是因为如果都是通过slab三链获取物理页,那么在多CPU的情况下就会出现多个CPU抢占slab自旋锁的情况,这样会导致效率比较低,发没发现,这个array的机制和伙伴系统buddy的冷热页框机制很像,关于array和slab三链是如何分配内存的细节后面详细讨论;现在只要知道它们的大概道理即可,继续观察函数setup_cpu_cache:
首先分析最开始的“if (g_cpucache_up == FULL)”,这里涉及了slab初始化进度的内容,静态全局变量g_cpucache_up就定义在slab.c文件中,它记录着slab初始化的情况流程如下:
- NONE:最开始;
- PARTIAL_AC:创建sizeof(struct arraycache_init)大小的cache之后;
- PARTIAL_L3:创建sizeof(struct kmem_list3)大小的cache之后;
- EARLY:kmem_cache_init函数末尾;
- FULL:start_kernel调用kmem_cache_init_late时;
先说在达到FULL之后,调用的是enable_cpucache函数,这个函数根据我们需要申请的对象的大小确定上限limit,然后先后调用do_tune_cpucache和alloc_kmemlist函数创建该cache的array和slab三链,这两个函数我也分析了,但个人认为可以不作为分析重点,因为它们这时都是已经可以轻松的通过kmalloc申请sizeof(struct arraycache_init)大小的空间和sizeof(struct kmem_list3)大小的空间分别存储自己的array和slab三链,这里应该重点看下在slab初始化完成之前的情况:
我们已经知道g_cpucache_up还为NONE说明内核还没有创建sizeof(struct arraycache_init)大小的cache,所以第一个if (g_cpucache_up == NONE)里边就是创建sizeof(struct arraycache_init)大小的cache的过程,如下:
cachep->array[smp_processor_id()] = &initarray_generic.cache;
发现没有,cache的array是被赋值一个全局变量,为什么?这是因为在这时候内核还没有创建过sizeof(struct arraycache_init)大小的cache,所以第一个array没法通过kmalloc创建,只能借助一个全局变量模拟一下,看看这个全局变量initarray_generic:
static struct arraycache_init initarray_generic = { {0, BOOT_CPUCACHE_ENTRIES, 1, 0} };
其结构类型为:
这里再次借用include/linux/kmalloc_sizes.h文件的CACHE(X)宏声明,只是重新定义了宏定义,如果展开就是21个if else判断,它实际上判断的是sizeof(struct arraycache_init)和sizeof(struct kmem_list3)即这两个结构体大小在20个长度分档中属于哪个分档,事实上在初始化中即函数kmem_cache_init中是会特意创建这两个长度的“规则”的cache,回到函数setup_cpu_cache,这里是比较这两个结构体在20个长度分档中的分档编号是否一样,应该说肯定不一样,所以g_cpucache_up在这时肯定赋值为PARTIAL_AC;
可见,执行到这里内核已经有了struct arraycache_init结构体长度的“规则”的cache,以后创建下一个新的长度的cache时,当申请其array成员时不需要借助什么全局变量了,直接可以kmalloc;事实上在初始化时,马上就会创建struct kmem_list3结构体长度的“规则”的cache,将会执行本函数的else,当申请其array成员时,就直接kmalloc即可;
并且此时,当g_cpucache_up如果为PARTIAL_AC,说明处于正在创建struct arraycache_init)结构体长度的“规则”的cache,这时内核还没有sizeof(struct kmem_list3)结构体长度的“规则”的cache,还得借助全局变量即调用函数set_up_list3s申请nodelists成员,然后g_cpucache_up初始化进度更新为PARTIAL_L3;当然如果g_cpucache_up值为EARLY的话说明已经kmem_cache_init函数已调用结束即sizeof(struct kmem_list3)结构体长度的“规则”的cache已经创建,则直接通过调用kmalloc申请nodelists成员;
最后注意一下底下的一些初始化操作,注意这些只是在slab没有完全初始化完毕即g_cpucache_up还不为FULL时调用,nodelists的next_reap成员个人暂时可以不关注;array的avail成员表示目前可用的slab初始化为0,这说明我们虽然创建了这样“规则”的长度的cache,但并没有实际从伙伴系统申请物理页;limit是指slab的个数上限为1;batchcount是指批量移入/移出的个数,slab的申请/释放的单位是batchcount值,这在后面会显而易见的发现;touched指slab是否被动过,个人认为暂无需关注;最后是cache的limit和batchcount;
至此,应该能对内核slab分配器工作原理有个初步的认识了:它需要根据所需长度创建相应长度的“规则”的cache,这样今后在申请这样长度的内存,就可以直接用kmalloc/kmem_cache_alloc即可使用slab的服务了;仅仅创建这样长度的“规则”的cache并没有真正分配内存创建相应的slab,这将在调用kmalloc/kmem_cache_alloc函数时去真正分配内存创建slab;不同长度的cache会有不同长度的物理页、slab个数、碎片大小,slab分为内置和外置方式存储,主要体现在slab管理对象存储位置的不同(内置下和slab实际内存在一起,外置则另外申请内存存储),slab管理对象包括slab结构体和每个slab的对象描述符;