注:本文讲述的SLAB相关代码是基于Linux内核v4.7,代码网址。
一,SLAB分配器的由来
在讲SLAB分配器之前先说两个概念: 内部碎片和外部碎片。
外部碎片指的是还没有被分配出去(不属于任何进程)但由于太小而无法分配给申请内存空间的新进程的内存空闲区域。外部碎片是除了任何已分配区域或页面外部的空闲存储块。这些存储块的总和可以满足当前申请的长度要求,但是由于它们的地址不连续或其他原因,使得系统无法满足当前申请。简单示例如下图:
如果某进程现在需要向操作系统申请地址连续的32K内存空间,注意是地址连续,实际上系统中当前共有10K+23K=33K空闲内存,但是这些空闲内存并不连续,所以不能满足进程的请求。这就是所谓的外部碎片,造成外部碎片的原因主要是进程或者系统频繁的申请和释放不同大小的一组连续页框。Linux操作系统中为了尽量避免外部碎片而采用了伙伴系统(Buddy System)算法。
内部碎片就是已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间;内部碎片是处于区域内部或页面内部的存储块,占有这些区域或页面的进程并不使用这个存储块,而在进程占有这块存储块时,系统无法利用它。直到进程释放它,或进程结束时,系统才有可能利用这个存储块。
简单示例如下图:
某进程向系统申请了3K内存空间,系统通过伙伴系统算法可能分配给进程4K(一个标准页面)内存空间,导致剩余1K内存空间无法被系统利用,造成了浪费。这是由于进程请求内存大小与系统分配给它的内存大小不匹配造成的。由于伙伴算法采用页框(Page Frame)作为基本内存区,适合于大块内存请求。在很多情况下,进程或者系统申请的内存都是4K(一个标准页面)的,依然采用伙伴算法必然会造成系统内存的极大浪费。为满足进程或者系统对小片内存的请求,对内存管理粒度更小的SLAB分配器就产生了。(注:Linux中的SLAB算法实际上是借鉴了Sun公司的Solaris操作系统中的SLAB模式)
【文章福利】小编推荐自己的Linux内核技术交流群:【】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100名进群领取,额外赠送一份价值699的内核资料包(含视频教程、电子书、实战项目及代码)
二,SLAB分配器简介
SLAB分配器实际上是建立在伙伴系统算法之上的,SLAB分配器使用的内存空间是通过伙伴算法进行分配的,只不过SLAB对这些内存空间实现了自己的算法进而对小块内存进行管理。
在讲解SLAB原理前,我们考虑下面场景:如果一个应用程序经常使用某一种类型的对象,或者说频繁的创建、回收某一种类型的对象,那我们是不是可以尝试将这类对象单独存放起来,当进程不在使用时,我们暂时先不回收,等应用程序再使用时,我们把之前应该回收的对象在拿出来,只不过重新构造一下对象,这样我们就减少了一次释放、申请内存的操作了。
注:下文说明的缓存指的并不是真正的缓存,真正的缓存指的是硬件缓存,也就是我们通常所说的L1 cache、L2 cache、L3 cache,硬件缓存是为了解决快速的CPU和速度较慢的内存之间速度不匹配的问题,CPU访问cache的速度要快于内存,如果将常用的数据放到硬件缓存中,使用时CPU直接访问cache而不用再访问内存,从而提升系统速度。下文中的缓存实际上是用软件在内存中预先开辟一块空间,使用时直接从这一块空间中去取,是SLAB分配器为了便于对小块内存的管理而建立的。
三,SLAB分配器原理
3.1 SLAB相关说明
- (1)SLAB与伙伴(Buddy)算法
伙伴系统的相关介绍可以参加其他博客。在伙伴系统中,根据用户请求,伙伴系统算法会为用户分配2^order个页框,order的大小从0到11。在上文中,提到SLAB分配器是建立在伙伴系统之上的。简单来说,就是用户进程或者系统进程向SLAB申请了专门存放某一类对象的内存空间,但此时SLAB中没有足够的空间来专门存放此类对象,于是SLAB就像伙伴系统申请2的幂次方个连续的物理页框,SLAB的申请得到伙伴系统满足之后,SLAB就对这一块内存进行管理,用以存放多个上文中提到的某一类对象。
- (2)SLAB与对象
对象实际上指的是某一种数据类型。一个SLAB只针对一种数据类型(对象)。为了提升对对象的访问效率,SLAB可能会对对象进行对齐。
(3)SLAB与per-CPU缓存
为了提升效率,SLAB分配器为每一个CPU都提供了每CPU数据结构struct array_cache,该结构指向被释放的对象。当CPU需要使用申请某一个对象的内存空间时,会先检查array_cache中是否有空闲的对象,如果有的话就直接使用。如果没有空闲对象,就像SLAB分配器进行申请。
3.2 SLAB相关数据结构
SLAB分配器把对象分组放进高速缓存。每个高速缓存都是同种类型对象的一种“储备”。包含高速缓存的主内存区被划分为多个SLAB,每个SLAB由一个或多个连续的页框组成,这些页框中既包含已分配的对象,页包含空闲的对象。
在Linux内核中,SLAB高速缓存用struct kmem_cache表示,源代码网址,其定义如下:
在struct kmem_cache结构体中,struct array_cache __percpu *cpu_cache和struct kmem_cache_node *node[MAX_NUMNODES]这两个属性比较重要。array_cache针对的是每一个CPU的SLAB高速缓存,kmem_cache_node是针对每一个NUMA节点的SLAB高速缓存。其中,struct kmem_cache_node结构体定义如下:源代码网址。
在struct kmem_cache_node结构体中,有三个比较重要的属性:slabs_partial、slabs_full、slabs_free,其中slabs_partial指向只有部分对象被使用的slab的描述符的链表,slabs_full指向所有对象都被使用的slab的描述符的链表,slabs_free指向所有对象都没有被使用的slab的描述符的链表。
有关slab的描述符,其定义是在struct page中,相关代码如下:
在struct page结构体中,与SLAB有关的比较重要的两个属性:s_mem和freelist,其中s_mem指向slab中的第一个对象的地址(或者已经被分配或者空闲)。freelist指向空闲对象链表。
在struct kmem_cache结构体中有一个属性cpu_cache,cpu_cache是一个指向数组的指针,每个数组项都对应系统中的一个CPU,每个数组项都包含了一个指向struct array_cache的指针,其定义如下:
说了这么多,你可能不太清楚上面的这些结构体到底都什么关系。为了更好的理解上面这些结构体之间的关系,请看下面的这幅图:
3.3 SLAB与分区页框分配器
先介绍一下分区页框分配器,分区页框分配器用于处理对连续页框组的内存分配请求。其中有一些函数和宏请求页框,一般情况下,它们都返回第一个所分配的页的线性地址,如果分配失败,则返回NULL。这些函数简介如下:
- alloc_pages(gfp_mask,order):请求2^order个连续的页框,他返回第一个所分配页框的描述符的地址。分配失败则返回NULL。
- __get_free_pages(gfp_mask,order):与函数alloc_pages(gfp_mask,orser)类似,但是此函数返回第一个所分配页的线性地址。
- __free_pages(page,order):该函数先检查page指向的页描述符,如果该页框未被保留(PG_reserved标志位为0),就把描述符的count字段值减1。如果count字段的值变为0,就假定从与page对应的页框开始的2^order个连续的页框不再被使用。在这种情况下该函数释放页框。
- free_pages(addr,order):类似于__free_pages(),但是它接收的参数为要释放的第一个页框的线性地址addr。
当SLAB分配器创建新的SLAB时,需要依靠分区页框分配器来获得一组连续的空闲页框。为了达到此目的,需要调用kmem_getpages()函数,函数定义如下:源代码网址
其中,参数cachep指向需要额外页框的高速缓存的高速缓存描述符(请求页框的个数存放在cache->gfporder字段中的order决定),flags说明如何请求页框,nodeid指明从哪个NUMA节点的内存中请求页框。与kmem_getpages()函数相对的是kmem_freepages(),kmem_freepages()函数可以释放分配给slab的页框。kmem_freepages()函数定义如下:
3.4 SLAB与高速缓存
创建新的slab缓存需要调用函数kmem_cache_create()。该函数定义如下:
kmem_cache_create()函数在调用成功时返回一个指向所构造的高速缓存的指针,失败则返回NULL。
与kmem_cache_create()函数相对应的是销毁高速缓存的函数kmem_cache_destroy(),该函数定义如下:
需要注意的是:kmem_cache_destroy()函数销毁的高速缓存中应该只包含未使用对象,如果一个高速缓存中含有正在使用的对象时调用kmem_cache_destroy()函数将会失败,从kmem_cache_destroy()函数的源代码中我们很容易看出。
下面看一个有关这两个函数使用的示例:
上面的代码很简单,就是建立和销毁rmap_item、mm_slot、stable_node三种数据类型的高速缓存。
3.5 SLAB与SLAB的对象
创建完成某一种数据类型或者某一种对象的高速缓存之后,我们可以从该对象的高速缓存中分配与释放对象。其中,kmem_cache_alloc()函数用于从特定的缓存中获取对象,该函数定义如下:
从函数的名称、参数、返回值、注释中,我们很容易知道kmem_cache_alloc()函数从给定的slab高速缓存中获取一个指向空闲对象的指针。实际上,进行获取空闲对象的时候,会先从per-CPU缓存中也就是array_cache中查找空闲对象,如果没有则会从kmem_cache_node中获取空闲对象,如果也没有则需要利用伙伴算法分配新的连续页框,然后从新的页框中获取空闲对象。从kmem_cache_alloc()到大致的调用链如下:
kmem_cache_alloc()——>slab_alloc()——>__do_cache_alloc()——>____cache_alloc()——>cpu_cache_get()(这里实际上是从array_cache中获取空闲对象)——>cache_alloc_refill()(这里会在array_cache中没有空闲对象时执行)——>cpu_cache_get()(经过cache_alloc_refill()的执行基本保证array_cache中有空闲对象)——>返回可用空闲对象
对cache_alloc_refill()函数执行步骤分解如下:
cache_alloc_refill()——>尝试从被一个NUMA节点所有CPU共享的缓冲中获取空闲对象(源代码注释中写道:See if we can refill from the shared array),如果有则返回可用对象,refill结束——>从kmem_cache_node中的slab中获取空闲对象,有则返回,没有就执行下一步——>kmem_getpages()而kmem_cache_free()函数用于从特定的缓存中释放对象,函数定义如下:
释放对象与分配对象的过程类似,不再赘述。
3.6 SLAB着色
同一硬件高速缓存行可以映射RAM中很多不同的内存块。相同大小的对象倾向于存放在硬件高速缓存内相同的偏移量处。在不同的SLAB内具有相同偏移量的度下行最终很有可能映射在同一硬件高速缓存行中。高速缓存的硬件可能因此而花费内存周期在同一高速缓存行与RAM内存单元之间来来往往传送这两个对象,而其他的硬件高速缓存行并未充分使用(以上语句出自《深入理解Linux内核》第三版第334页)。SLAB分配器为了降低硬件高速缓存的这种行为,采用了SLAB着色(slab coloring)的策略。所谓着色,简单来说就是给各个slab增加不同的偏移量,设置偏移量的过程就是着色的过程。通过着色尽量使得不同的对象对应到硬件不同的高速缓存行上,以最大限度的利用硬件高速缓存,提升系统效率。
3.7 普通与专用高速缓存
高速缓存(指的不是硬件高速缓存)被在SLAB中被分成两种类型:普通和专用。普通高速缓存只由SLAB分配器用于自己的目的,而专用高速缓存由内核的其余部分使用。专用的高速缓存是由kmem_cache_create()函数创建的,由kmem_cache_destroy()函数撤销,用于存放对象(或具体数据类型),普通高速缓存是系统初始化期间调用keme_cache_init()建立的。普通高速缓存中分配和释放空间使用kmalloc()和kfree()函数。
下面是kmem_cache_init()函数的源代码:
上面的代码结合下面的图,获取会更容易理解一些:
在Linux中使用 sudo cat /proc/slabinfo就可以得到上面的信息,每一列的含义如下:
下面我们重点介绍kmalloc()函数,其源代码如下:
函数参数中的size表示请求分配的字节数,需要注意的是kmalloc()函数分配的是连续的物理内存。
与kmalloc()函数相对的是kfree()函数,其定义如下:
3.8 SLAB分配器体现的改进思想:
(1)SLAB分配器把内存区看成对象
(2)SLAB分配器吧对象分组放进高速缓存
(3)每个SLAB都是同种类型的内存对象
如上文内容有不当之处,请批评指针,谢谢!