首页 > 代码库 > Slab 算法

Slab 算法

        作为一种出名的设计及算法 Slab 已经很早就出现在各大系统中了,Linux 也实现了 Slab,那 Slab 一般作什么用途?解决了什么问题?如何实现?为什么要这样实现?这就是今天要讨论的内容了。
        我们知道 Buddy 系统解决了物理内存分配的外碎片问题,但由于粒度太大,都是以页为单位,显然用起来有些浪费,当如果要申请一些小的内存,并且会频繁的申请相同数据结构的内存来存储一些内核中的数据时,这时 Slab 便应运而生了。在内核中,经常会使用一些链表,链表中会申请许多相同结构的结构体,比如文件对象,进程对象等等,如果申请比较频繁,那么为它们建立一个内存池,内存池中都是相同结构的结构体,当想申请这种结构体时,直接从这种内存池中取一个结构体出来,想必是有用且速度极快的。一个物理页就可以作用这种内存池的载体,进而进行充分利用,减少了内碎片的产生。
        所以,首先,Slab 相当于内存池是思想,且是为了解决内碎片而产生的。

        我们看一下 Slab 算法的结构图。

技术分享

        即使是专有结构的内存池,所以一个高速缓存即 kmem_cache ,就代表一个结构体的内存池,它有一个每 CPU 数据 array, 进一步加快了申请速度,解决了多 CPU 加锁,且小数据缓存的目的,因为保留少量的频繁申请和释放得来的空间,等下次申请时直接从这里取得,由于结构简单,所以速度极快。所以每一个 CPU 都会对应一个 array_cache 结构体,在内存布局上,该结构体后面,紧接着有一个数组,数组项就是一些结构体内存的指针,vail  即可计算空闲项的下标,这样,当出现一个申请请求时,直接从这里取一个结构体块的指针,(这里的结构体我们称之为对象)然后返回,效率极高。释放时,原理相同,细节很简单,这里不讨论。
        那对象的具体内存是在哪里呢?
        这里称之为对象,你没看错,Slab 采用了面向对象的概念,每一个结构体我们看作是一个对象,它们有共用的构造和析构的方法,其实就是为一些结构体赋值和释放。它们存放的位置才是 slab 的关键所在,在每一种对象内存池中,即 kmem_cache 中,有三个链表,slabs_partial, slabs_full, slabs_free, 链表元素都是 slab 结构体,而 slab 结构体所描述的就是对象的内存空间,顾名思义,这三个链表分别代表,slab 里对象部分被装满,全满,全空三种链表,重点就在这 slab 结构里。
        每一个 Slab 描述了这种类型对象内存池中的一小部分内存,它会描述一段对象数组的使用情况,通过 slab 描述符可以得到一些未使用的对象的地址。从这句话里,可以知道,一个 slab 描述的对象的内存都相当于数组般连续排列的。这个数组的起始地址非常重要,因为考虑到了 Cache line,所以起始地址都会以 cache line 对齐,这样理想情况下,对象会被装入一整条 cache line 中,也容易再次命中。但如果两个 slab 中的对象对齐到同一 cache line ,事必会造成 cache line 不命中而重新读取 RAM,所以尽量使每个 slab 的对象的开始地址分配到不同 cache line 中,就有了每个 slab 都会有一个 colouroff 作为随机种子,来使对象起始地址分散到不同 cache line 中。但我认为这种效果意义不大,但有了这个随机种子,总是会起到一点效果的。
        对象所占用的空间也会进行取整,规则如下:
        1. 如果对象的大小大于 cache line 的一半,那么就以 L1_CACHE_BYTES 的倍数对齐对象;
        2.否则,对象大小就是 L1_CACHE_BYTES 的因子取整,这样一个小对象,就不会跨越两个 cache line。即 cache line 一直除以 2 ,找到一个刚好大于对象大小的值,作为对象的大小来处理。
        这们,当对象的起始地址,以及大小决定以后,然后再从物理页中安排这些对象数组就变得简单了。此时有个情况,因为当从伙伴系统中申请物理页来作为对象缓冲池时,既可以把 slab 的描述符与它描述的对象池放在一起,也可以分开放,放在一起,即 slab 描述符后面,就是对象池,反之,就是分开存储,两者皆可,取决于 slab 中存放的对象的个数。
        讲到这里,某一对象的缓冲池差不多建好了,那么一个 slab 描述符如何描述自己的对象池呢,如上图,每一个 slab 描述符后面,紧接着是它所描述对象的对象描述符,对象描述符是一个无符号整型数,只有在它所描述的对象空闲时才有意义。当某一对象空闲时,它的描述符包含下一个空闲对象在该 slab 中的下标,最后一个空闲对象的对象描述符的值为 BUFCTL_END. 这样便形成了一个空闲链表。
        总结一下,slab 算法中申请某一对象的情景,当为某一结构体创建一个高速缓存时,会调用,kmem_cache_create 来创建一个上图中的结构,此时就可以从该高速缓冲申请该结构体的内存了,申请是通过 kmem_cache_alloc, 来分配,它首先检查每 CPU 高速缓存中是否可以得到一个空闲对象,如果没有,那么它从 slab 链表中选取一个,得到空闲对象,去填充每 CPU 高速缓存,然后再从每 CPU 高速缓存中返回一个空闲对象的地址,释放过程与之相反。

Slab 算法