slab算法

slab算法提出原因:

Buddy 系统解决了物理内存分配的外部碎片问题,但由于粒度太大(内存块的单位较大),以页为单位,采用伙伴算法分配内存时,每次至少分配一个页面(4K),显然用起来有些浪费,当如果要申请一些小的内存(请求分配的内存大小为几十个字节或几百个字节时,对于小块内存的分配和回收),并且会频繁的申请相同数据结构的内存来存储一些内核中的数据时,这时 Slab 便应运而生了。

在内核中,经常会使用一些链表,链表中会申请许多相同结构的结构体,比如文件对象,进程对象等等,如果申请比较频繁,那么为它们建立一个内存池,内存池中都是相同结构的结构体,当想申请这种结构体时,直接从这种内存池中取一个结构体出来,是有用且速度极快的。一个物理页就可以作用这种内存池的载体,进而进行充分利用,减少了内部碎片的产生。

所以,Slab 相当于内存池思想,且是为了解决内碎片而产生的,slab的核心思想是以对象的观点管理内存。

实际上内核中slab分配器对不同长度内存是分档的,这是slab分配器的一个基本原则,按申请的内存的大小分配相应长度的内存。同时也说明一个事实,内核中一定应该有这样的按不同长度slab内存单元,也就是说已经创建过这样的内存块,否则申请时怎能根据大小识别应该分配给怎样大小的内存。(这可以先参考kmalloc的实现,kmalloc申请的物理内存长度为参数size,它需要先根据这个长度找到相应的长度的缓存)。slab分配器并非一开始就能智能的根据内存分档值分配相应长度的内存。每种cache对应一种长度的slab分配slab分配接口,一个是函数kmalloc一个是函数kmem_cache_allockmalloc的参数比较轻松,直接输入自己想要的内存长度即可,由slab分配器去找应该是属于哪个长度分档的,然后由那个分档的kmem_cache结构指针去分配相应长度内存,而kmem_cache_alloc就显得比较“专业”,它不是输入我要多少长度内存,而是直接以kmem_cache结构指针作为参数,直接指定我要这样长度分档的内存。内核slab分配器能够默认的提供32-4194304共20种内存长度分档,肯定是需要创建这样20个“规则”的,这是在初始化时创建的。

比如需要一个100字节的连续物理内存,那么内核slab分配器会给我提供一个相应大小的连续物理内存单元(2的次幂),为128字节大小(不会是整好100字节,而是这个档的一个对齐值,如100字节对应128字节,30字节对应32字节,60字节对应64字节),这个物理内存实际上是从伙伴系统获取的物理页;当不再需要这个内存时应该释放它,释放它并非把它归还给伙伴系统,而是归还给slab分配器,这样等再需要获取时无需再从伙伴系统申请,这也就是为什么slab分配器往往会把最近释放的内存(即所谓“热”)分配给申请者,这样效率是比较高的。

对内核中普通对象进行初始化所需的时间超过了对其进行分配和释放所需的时间。因此不应该将内存释放回一个全局的内存池,而是将内存保持为针对特定目的而初始化的状态。

slab分配器是基于对象进行管理的,所谓的对象就是存放一组数据结构的内存区,为便于理解可把对象看作内核中的数据结构(例如:task_struct,file_struct 等),其方法就是构造或析构函数,构造函数用于初始化数据结构所在的内存区,而析构函数收回相应的内存区。相同类型的对象归为一类,每当要申请这样一个对象时,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免内部碎片。slab分配器并不丢弃已经分配的对象,而是释放并把它们保存在内存中。slab分配对象时,会使用最近释放的对象的内存块,因此其驻留在cpu高速缓存中的概率会大大提高。为了避免重复初始化对象,Slab分配模式并不丢弃已分配的对象,而是释放但把它们依然保留在内存中。当以后又要请求分配同一对象时,就可以从内存获取而不用进行初始化,这是在Solaris 中引入Slab的基本思想。实际上,Linux中对Slab分配模式有所改进,它对内存区的处理并不需要进行初始化或回收。出于效率的考虑,Linux并不调用对象的构造或析构函数,而是把指向这两个函数的指针都置为空。Linux中引入Slab的主要目的是为了减少对伙伴算法的调用次数。

实际上,内核经常反复使用某一内存区。例如,只要内核创建一个新的进程,就要为该进程相关的数据结构(task_struct、打开文件对象等)分配内存区。当进程结束时,收回这些内存区。因为进程的创建和撤销非常频繁,因此,Linux的早期版本把大量的时间花费在反复分配或回收这些内存区上。从Linux2.2开始,把那些频繁使用的页面保存在高速缓存中并重新使用。

Slab分配模式把对象分组放进缓冲区(尽管英文中使用了Cache这个词,但实际上指的是内存中的区域,而不是指硬件高速缓存)。因为缓冲区的组织和管理与硬件高速缓存的命中率密切相关,因此,Slab缓冲区并非由各个对象直接构成,而是由一连串的“大块(Slab)”构成,而每个slab中则包含了若干个同种类型的对象,这些对象或已被分配,或空闲。一般而言,对象分两种,一种是大对象,一种是小对象。所谓小对象,是指在一个页面中可以容纳下好几个对象的那种。例如,一个inode结构大约占300多个字节,因此,一个页面中可以容纳8个以上的inode结构,因此,inode结构就为小对象。Linux内核中把小于512字节的对象叫做小对象。

实际上,缓冲区就是主存中的一片区域,把这片区域划分为多个块,每块就是一个Slab,每个Slab由一个或多个页面组成,每个Slab中存放的就是对象。

 下图是slab 结构的高层组织结构。在最高层是 cache_chain,这是一个 slab 缓存的链接列表。这对于 best-fit 算法非常有用,可以用来查找最适合所需要的分配大小的缓存(遍历列表)。cache_chain 的每个元素都是一个 kmem_cache 结构的引用(称为一个 cache)。它定义了一个要管理的给定大小的对象池。

slab算法_第1张图片

每个缓存都包含了一个 slabs 列表,这是一段连续的内存块(通常都是页面)。存在 3 种 slab:
slabs_full:完全分配的 slab
slabs_partial:部分分配的 slab
slabs_free:空 slab,或者没有对象被分配
注意 slabs_free 列表中的 slab 是进行回收(reaping)的主要备选对象。正是通过此过程,slab 所使用的内存被返回给操作系统供其他用户使用。
slab 列表中的每个 slab 都是一个连续的内存块(一个或多个连续页,通常为一页),它们被划分成一个个对象。这些对象是从特定缓存中进行分配和释放的基本元素。注意 slab 是 slab 分配器进行操作的最小分配单位,因此如果需要对 slab 进行扩展,这也就是所扩展的最小值。通常来说,每个 slab 被分配为多个对象。

由于对象是从 slab 中进行分配和释放的,因此单个 slab 可以在 slab 列表之间进行移动。例如,当一个 slab 中的所有对象都被使用完时,就从 slabs_partial 列表中移动到 slabs_full 列表中。当一个 slab 完全被分配并且有对象被释放后,就从 slabs_full 列表中移动到 slabs_partial 列表中。当所有对象都被释放之后,就从 slabs_partial 列表移动到 slabs_free 列表中。

slab 分配器首先从部分空闲的slab 进行分配。如没有,则从空的slab 进行分配。如没有,则从物理连续页上分配新的slab,并把它赋给一个cache ,然后再重新slab 分配空间。

举例说明:如果有一个名叫inode_cachep的struct kmem_cache节点,它存放了一些inode对象。当内核请求分配一个新的inode对象时,slab分配器就开始工作了:

  • 首先要查看inode_cachep的slabs_partial链表,如果slabs_partial非空,就从中选中一个slab,返回一个指向已分配但未使用的inode结构的指针。完事之后,如果这个slab满了,就把它从slabs_partial中删除,插入到slabs_full中去,结束;
  • 如果slabs_partial为空,也就是没有半满的slab,就会到slabs_empty中寻找。如果slabs_empty非空,就选中一个slab,返回一个指向已分配但未使用的inode结构的指针,然后将这个slab从slabs_empty中删除,插入到slabs_partial(或者slab_full)中去,结束;
  • 如果slabs_empty也为空,那么没办法,cache内存已经不足,只能新创建一个slab了。

slab算法_第2张图片

其实slab机制的简介表示如下图所示:


slab内的结构如下图所示:

 每个Slab的首部都有一个小小的区域是不用的,称为“着色区(coloring area)”。着色区的大小使Slab中的每个对象的起始地址都按高速缓存中的缓存行(cache line)”大小进行对齐(80386的一级高速缓存行大小为16字节,Pentium为32字节)。因为Slab是由1个页面或多个页面(最多为32)组成,因此,每个Slab都是从一个页面边界开始的,它自然按高速缓存的缓冲行对齐。但是,Slab中的对象大小不确定设置着色区的目的就是将Slab中第一个对象的起始地址往后推到与缓冲行对齐的位置。因为一个缓冲区中有多个Slab,因此,应该把每个缓冲区中的各个Slab着色区的大小尽量安排成不同的大小,这样可以使得在不同的Slab中,处于同一相对位置的对象,让它们在高速缓存中的起始地址相互错开,这样就可以改善高速缓存的存取效率

 每个Slab上最后一个对象以后也有个小小的废料区是不用的,这是对着色区大小的补偿,其大小取决于着色区的大小,以及Slab与其每个对象的相对大小。但该区域与着色区的总和对于同一种对象的各个Slab是个常数

 每个对象的大小基本上是所需数据结构的大小。只有当数据结构的大小不与高速缓存中的缓冲行对齐时,才增加若干字节使其对齐。所以,一个Slab上的所有对象的起始地址都必然是按高速缓存中的缓冲行对齐的。

 Slab 算法的结构图:

slab算法_第3张图片

一个高速缓存即 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的着色问题

着色与硬件cache有关,这就牵扯到cache和主存的工作结构:

  1. 全相连 
    • 任一主存块能映射到任意缓存行(主存块的大小等于缓存行的大小)。
    • 优点:灵活,不易产生冲突
    • 缺点:比较难于实现,且效率低,速度慢
  2. 直接映射 
    • 某一主存块只能映射到特定的缓存行
    • 优点:硬件简单,成本低
    • 缺点:容易产生冲突,易产生缓存“颠簸”,不能有效利用cache空间
  3. 组相联 
    • 组间直接映射,组内全相联映射
    • 优点:结合上面两种的优点 
      • 因为组内行数较少,比较器容易实现
      • 组内又有灵活性,冲突大大减小

由上可知,slab着色对于直接映射和组相联映射的工作结构效率帮助较大,而全项联结构本身冲突就比较小,那么着色的帮助是很小的。在全相连工作结构中,使用着色无疑是一个巨大的内存浪费。 

slab算法的缺点

随着大规模多处理器系统和NUMA系统的广泛应用,slab分配器逐渐暴露出自身严重的不足:

  1. 较多复杂的队列管理。在slab分配器中存在众多的队列,例如针对处理器的本地缓存队列,slab中空闲队列,每个slab处于一个特定状态的队列之中。所以,管理太费劲了。
  2. slab管理数据和队列的存储开销比较大。每个slab需要一个struct slab数据结构和一个管理者kmem_bufctl_t型的数组。当对象体积较小时,该数组将造成较大的开销(比如对象大小为32字节时,将浪费1/8空间)。为了使得对象在硬件告诉缓存中对齐和使用着色策略,还必须浪费额外的内存。同时,缓冲区针对节点和处理器的队列也会浪费不少内存。测试表明在一个1000节点/处理器的大规模NUMA系统中,数GB内存被用来维护队列和对象引用。
  3. 缓冲区回收比较复杂。
  4. 对NUMA的支持非常复杂。slab对NUMA的支持基于物理页框分配器,无法细粒度的使用对象,因此不能保证处理器级的缓存来自同一节点(这个我暂时不太懂)。
  5. 冗余的partial队列。slab分配器针对每个节点都有一个partial队列,随着时间流逝,将有大量的partial slab产生,不利于内存的合理使用。
  6. 性能调优比较困难。针对每个slab可以调整的参数比较复杂,而且分配处理器本地缓存时,不得不使用自旋锁。
  7. 调试功能比较难于使用。

为了解决以上slab分配器的不足,引入新的解决方案,slub分配器。slub分配器的特点是简化设计理念,同时保留slab分配器的基本思想:每个缓冲区有多个slab组成,每个slab包含固定数目的对象。slub分配器简化了kmem_cache,slab等相关的管理结构,摈弃了slab分配器中的众多队列概念,并针对多处理器、NUMA系统进行优化,从而提高了性能和可扩展性并降低了内存的浪费。并且,为了保证内核其他模块能无缝迁移到slub分配器,API接口函数与slab保持一致。

缺点简单说就是:

  1. 缓存队列管理复杂;
  2. 管理数据存储开销大;
  3. 对NUMA支持复杂;
  4. 调试调优困难;
  5. 摒弃了效果不太明显的slab着色机制;

优点

编辑
与传统的内存管理模式相比, slab 缓存分配器提供了很多优点。
1、内核通常依赖于对小对象的分配,它们会在系统生命周期内进行无数次分配。
2、slab 缓存分配器通过对类似大小的对象进行缓存而提供这种功能,从而避免了常见的碎片问题。
3、slab 分配器还支持通用对象的初始化,从而避免了为同一目的而对一个对象重复进行初始化。
4、slab 分配器还可以支持硬件缓存对齐和着色,这允许不同缓存中的对象占用相同的缓存行,从而提高缓存的利用率并获得更好的性能。
与传统的内存管理模式相比, slab 缓存分配器提供了很多优点。 首先,内核通常依赖于对小对象的分配,它们会在系统生命周期内进行无数次分配。slab 缓存分配器通过对类似大小的对象进行缓存而提供这种功能,从而避免了常见的碎片问题。slab 分配器还支持通用对象的初始化,从而避免了为同一目的而对一个对象重复进行初始化。最后,slab 分配器还可以支持硬件缓存对齐和着色,这允许不同缓存中的对象占用相同的缓存行,从而提高缓存的利用率并获得更好的性能。


你可能感兴趣的:(操作系统)