slab分配器
所谓尺有所长,寸有所短。以页为最小单位分配内存对于内核管理系统物理内存来说的确比较方便,但内核自身最常使用的内存却往往是很小(远远小于一页)的内存块——比如存放文件描述符、进程描述符、虚拟内存区域描述符等行为所需的内存都不足一页。这些用来存放描述符的内存相比页面而言,就好比是面包屑与面包。一个整页中可以聚集多个这种这些小块内存;而且这些小块内存块也和面包屑一样频繁地生成/销毁。
为了满足内核对这种小内存块的需要,Linux系统采用了一种被称为slab分配器的技术。Slab分配器的实现 相当复杂,但原理不难,其核心思想就是“存储池”的运用。内存片段(小块内存)被看作对象,当被使用完后,并不直接释放而是被缓存到“存储池”里,留做下 次使用,这无疑避免了频繁创建与销毁对象所带来的额外负载。
Linux 所使用的slab 分配器的基础是 Jeff Bonwick 为SunOS 操作系统首次引入的一种算法。Jeff 的分配器是围绕对象缓存进行的。在内核中,会为有限的对象集(例如文件描述符和其他常见结构)分配大量内存。Jeff 发现对内核中普通对象进行初始化所需的时间超过了对其进行分配和释放所需的时间。因此他的结论是不应该将内存释放回一个全局的内存池,而是将内存保持为针对特定目而初始化的状态。例如,如果内存被分配给了一个互斥锁,那么只需在为互斥锁首次分配内存时执行一次互斥锁初始化函数(mutex_init
)即可。后续的内存分配不需要执行这个初始化函数,因为从上次释放和调用析构之后,它已经处于所需的状态中了。
Linux slab分配器使用了这种思想和其他一些思想来构建一个在空间和时间上都具有高效性的内存分配器。
1。slab的特点
下图给出了slab 结构的高层组织结构。在最高层是cache_chain,这是一个 slab 缓存的链接列表。这对于best-fit 算法非常有用,可以用来查找最适合所需要的分配大小的缓存(遍历列表)。cache_chain 的每个元素都是一个kmem_cache 结构的引用(称为一个cache)。它定义了一个要管理的给定大小的对象池。
每个缓存都包含了一个slabs 列表,这是一段连续的内存块(通常都是页面)。
存在3种类型的slab:
slabs_full:完全分配的slab
slabs_partial:部分分配的slab
slabs_empty:空slab,或者没有对象被分配
注意,slabs_empty列表中的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_empty 列表中。
与传统的内存管理模式相比,slab 缓存分配器提供了很多优点。
首先,内核通常依赖于对小对象的分配,它们会在系统生命周期内进行无数次分配。slab 缓存分配器通过对类似大小的对象进行缓存而提供这种功能,从而避免了常见的碎片问题。
slab分配器还支持通用对象的初始化,从而避免了为同一目的而对一个对象重复进行初始化。
最后,slab分配器还可以支持硬件缓存对齐和着色,这允许不同缓存中的对象占用相同的缓存行,从而提高缓存的利用率并获得更好的性能。
2。slab在Linux中的实现
下面是在Linux中创建新slab 缓存、向缓存中增加内存、销毁缓存的应用程序接口(API),以及slab中对对象进行分配和释放操作的函数实现介绍。
2.1slab缓存结构
struct struct kmem_cache *my_cachep;
kmem_cache 结构包含了每个中央处理器单元的数据、一组可调整的(可以通过 proc 文件系统访问)参数、统计信息和管理slab 缓存所必须的元素。
2.2kmem_cache_create
内核函数 kmem_cache_create 用来创建一个新缓存。这通常是在内核初始化时执行的,或者在首次加载内核模块时执行
struct kmem_cache * kmem_cache_create( const char *name, size_t size,size_t align, unsigned long flags,
void (*ctor)(void*, struct kmem_cache *, unsigned long),
void (*dtor)(void*, struct kmem_cache *, unsigned long));
name 参数定义了缓存名称,proc 文件系统(在 /proc/slabinfo 中)使用它标识这个缓存。 size 参数指定了为这个缓存创建的对象的大小, align 参数定义了每个对象必需的对齐。 flags 参数指定了为缓存启用的选项。
SLAB_RED_ZONE 在对象头、尾插入标志,用来支持对缓冲区溢出的检查。 SLAB_POISON 使用一种己知模式填充 slab,允许对缓存中的对象进行监视(对象属对象所有,不过可以在外部进行修改)。 SLAB_HWCACHE_ALIGN 指定缓存对象必须与硬件缓存行对齐。 ctor 和 dtor 参数定义了一个可选的对象构造器和析构器。构造器和析构器是用户提供的回调函数。当从缓存中分配新对象时,可以通过构造器进行初始化。
在创建缓存之后, kmem_cache_create 函数会返回对它的引用。注意这个函数并没有向缓存分配任何内存。相反,在试图从缓存(最初为空)分配对象时,refill 操作将内存分配给它。当所有对象都被使用掉时,也可以通过相同的操作向缓存添加内存。
2.3kmem_cache_destroy
内核函数 kmem_cache_destroy 用来销毁缓存。这个调用是由内核模块在被卸载时执行的。在调用这个函数时,缓存必须为空。
void kmem_cache_destroy( struct kmem_cache *cachep );
2.4kmem_cache_alloc
要从一个命名的缓存中分配一个对象,可以使用 kmem_cache_alloc 函数。调用者提供了从中分配对象的缓存以及一组标志:
void kmem_cache_alloc( struct kmem_cache *cachep, gfp_t flags );
这个函数从缓存中返回一个对象。注意如果缓存目前为空,那么这个函数就会调用 cache_alloc_refill 向缓存中增加内存。kmem_cache_alloc 的 flags 选项与kmalloc 的 flags 选项相同。表 2 给出了标志选项的部分列表。
GFP_USER 为用户分配内存(这个调用可能会睡眠)。 GFP_KERNEL 从内核 RAM 中分配内存(这个调用可能会睡眠)。 GFP_ATOMIC 使该调用强制处于非睡眠状态(对中断处理程序非常有用)。 GFP_HIGHUSER 从高端内存中分配内存。
2.5kmem_cache_zalloc
内核函数 kmem_cache_zalloc 与 kmem_cache_alloc 类似,只不过它对对象执行 memset 操作,用来在将对象返回调用者之前对其进行清除操作。
2.6kmem_cache_free
要将一个对象释放回 slab,可以使用 kmem_cache_free。调用者提供了缓存引用和要释放的对象。
void kmem_cache_free( struct kmem_cache *cachep, void *objp );