前言:目前有很多讲slab的文章,要么是纯讲原理画一堆图结合源码不深导致理解困难,要么是纯代码注释导致理解更困难,我在猛攻了一周时间后,细致总结一下slab,争取从原理到源码都能细致的理解到并立刻达到清楚的使用。
2.3、slab分配机制
不论kmalloc还是kmem_cache_alloc,最终都是调用函数__cache_alloc,这是给调用者分配slab的总接口:
【文章福利】小编推荐自己的Linux内核技术交流群:【】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100名进群领取,额外赠送一份价值699的内核资料包(含视频教程、电子书、实战项目及代码)
学习直通车:
内核资料直通车:
接下来的if判断,判断array的avail成员是否为0即判断array里边是否有空闲的slab,可以回忆前面一节的cache创建,在函数setup_cpu_cache的中,不论是slab初始化完毕前还是初始化完毕后,avail成员都是置为0即当前没有空闲slab,结合这里一起验证了第一次通过kmalloc/kmem_cache_alloc获取物理内存时,会触发物理内存的实际分配,即函数____cache_alloc的else分支;
如果avail成员非0说明目前array中有空闲的slab,那么就直接把它的第一个slab返回给kmalloc/kmem_cache_alloc调用者,并更新array的avail值(运算符—即减一),同时置位array的touched;
如果avail成员为0说明目前array中没有空闲的slab,这就需要按照前面加深的描述的道理设法分配slab,调用函数cache_alloc_refill:
首先注意一下“batchcount = ac->batchcount;”,前面曾说过这是slab批量移入/移出的个数单位;然后通过“l3 = cachep->nodelists[node];”获取cache的slab三链,接下来的shared的操作个人认为可暂不关注,先考虑单CPU的情况(事实上理解slab原理后悔发现shared这个机制对于多CPU还是很有效率的);然后是while循环,依次检测slab三链的半空链表、全空链表是否有空闲的slab,如果有就可以直接从里边取出转给array,这里先看没有空闲slab的情况,这就跳到标号must_grow:
l3->free_objects -= ac->avail;
这里avail成员为0,相当于没减,关键是后面的alloc_done:
正常情况下,因为avail值为0所以会进入if (unlikely(!ac->avail))分支,因为这时slab三链也没有空闲的slab,所以需要从伙伴系统获取物理内存,调用函数cache_grow,这也就是很多文章包括ULK描述的创建新的slab的两个条件:1、array没有空闲slab;2、slab三链也没有空闲slab:
首先是获取slab三链指针l3,处理一下着色问题,关于着色问题后面专门描述,不影响理解slab分配先忽略;
然后是从伙伴系统获取物理页,调用函数kmem_getpages:
暂时直接看这个函数主要干了什么,主要就是调用函数alloc_pages_exact_node从伙伴系统申请物理页,需要注意申请的页数由cache的gfporder成员决定,它是在创建这个长度的“规则”的cache时计算出来的(最多2页),然后设置物理页的一些状态,注意下可回收标志这将在释放的时候有关系,最终返回的是该物理页在内存页表中映射的虚拟地址;现在变量objp保存了从伙伴系统这申请的物理页对应的虚拟地址;
然后是获取一个slab描述符,通过函数alloc_slabmgmt,这也是重点:
这验证了前面描述过的,对于外置slab,它的管理对象即slab描述符和每个对象的对象描述符(其实就是编号),slab结构体变量需要从该结构体长度的cache申请,对象描述符不需要申请,而内置的情况slab管理对象是和slab申请的内存在一起的,确切的说它在slab申请的内存位置处,所以它的slabp就在slab申请的内存的着色处之后,而外置的slabp是另外申请的位置处,这就是为什么对于内置情况,着色偏移还要加上cachep->slab_size的原因(注意slab_size值为slab结构体长度+每个对象描述符之和),而外置情况不用;
这里容易理不清,细致地描述下:
对于外置,cachep->slabp_cache在创建本cache时即调用函数kmem_cache_create时已经初始化了它的slabp_cache成员,值为slab_size所处的长度分档的cache,对于外置还是内置都是结构体slab长度和每个对象描述符之和,外置下在本函数alloc_slabmgmt的if判断中将从该cache(slab_size所处的长度分档的cache)申请一段内存用于存储所申请cache的slab管理对象,长度是结构体slab长度和每个对象描述符之和,这也就体现了外置的特点即slab管理对象在外部另外申请,如下图:
- Slab结构体 + 每个对象描述符
- Slab管理对象
- Slab内存
- 着色偏移 + 每个对象
而内置都在一起,如下图:
着色偏移 + slab结构体 + 每个对象描述符 +每个对象
Slab内存
注意外置和内置的slabp指针都是指向slab管理对象,但管理对象的位置不同,slab描述符的colouroff成员赋值为colour_off,但对于外置和内置,该值是不同的,内置还要多slab结构体及所有对象描述符的长度,这也就使外置和内置情况的slab的s_mem成员即第一个对象的虚拟地址的值不一样,objp都是指向slab内存,但colour_off的不同使外置只需向后偏移着色偏移即可,而内置还需多偏移slab_size个长度;
最后,slab的inuse成员标识当前正在使用的对象个数,初始值为0;free成员标识第一个空闲对象的编号,初始值为0;nodeid成员标识内存节点;
接下来是调用函数slab_map_pages,把slab描述符slabp赋给物理页的prev字段,把高速缓存描述符cachep赋给物理页的lru字段,本质是建立slab和cache到物理页的映射,用于快速根据物理页定位slab描述符和cache描述符,可先不太关注;
接下来是调用cache_init_objs,初始化cache描述符和slab对象描述符:
根据对象个数,利用循环定位每一个对象objp,并调用本cache的构造函数ctor初始化每个对象,但事实上大多数cache在创建时,ctor都是NULL即无需初始化每个对象;比较重要的是“slab_bufctl(slabp)[i] = i + 1”,它初始化每个对象的描述符为1、2、3、……、BUFCTL_END,这是对每个对象描述符的初始化;
到这里,这个slab已初始化好,内存也分配了,各个属性也初始化了,现在把它链在本cache的slab三链上,即 “list_add_tail(&slabp->list, &(l3->slabs_free));”,并更新slab三链的空闲对象个数成员free_objects;最终返回1意为slab创建成功,cache_grow函数调用成功。
回到函数cache_alloc_refill,这时需要重新获取本cache的array,因为上面的操作使能了中断,此期间local cache指针可能发生了变化,然后最终做判断if (!ac->avail),如果有其他模块的操作填充了本cache的array那么直接return,多数情况下array的avail还是0,返回标号retry,重新进行一次,这时因为slab三链肯定是有了空闲slab了,所以肯定可以在while循环中执行“ac->entry[ac->avail++] = slab_get_obj(cachep, slabp, node);”,即把slab转给array:
这个函数本身很明显,不断取出slab三链中的这个slab里的对象并同时更新该slab的空闲对象编号和可用空闲对象个数,要注意是在while循环中调用该函数,它是需要批量移出的即转给array共计batchcount个对象(在不超过该cache的对象个数前提下,正常情况下不会超出);
最终,这个slab的对象由全空闲减少了batchcount个对象,根据其是否还剩下对象的情况,把它从slab三链的全空闲链表中摘下放入半空闲链表或全满链表。