转载自:
http://blog.chinaunix.net/uid-23795818-id-2385151.html
http://www.ibm.com/developerworks/cn/linux/l-linux-slab-allocator/#resources
http://blog.chinaunix.net/uid-26948992-id-3227906.html
以上只是总体概述了一下,
详细的请参考:
http://blog.youkuaiyun.com/chenxiancool/article/details/7638804
http://blog.youkuaiyun.com/vanbreaker/article/category/1132690
++++++++++++++++++++++++++++++++++
久很久以前:一个叫做Mark Hemment的哥儿们写了Slab。在接下来的一些年里,其他人对Slab进行了完善。一年半以前,SLOB问世了。SLOB的目标是针对嵌入式系统的,主要是适用于那些内存非常有限的系统,比如32MB以下的内存,它不太注重large smp系统,虽然最近在这方面有一些小的改进。几个月之前,SLUB闪亮登场。它基本上属于对Slab的重设计(redesign),但是代码更少,并且能更好的适应large NUMA系统。
SLUB被很认为是Slab和Slob的取代者,内核开发人员称其为:more SMP-friendly SLUB allocator。显然,在桌面平台上的多核心处理器也能从中受益。
在研究SLUB之前,先说说SLAB。众所周知,操作系统进行内存分配的时候,是以页为单位进行的,也可以称为内存块或者堆。但是内核对象远小于页的大小,而这些对象在操作系统的生命周期中会被频繁的申请和释放,并且实验发现,这些对象初始化的时间超过了分配内存和释放内存的总时间,所以需要一种更细粒度的针对内核对象的分配算法,于是SLAB诞生了:
SLAB缓存已经释放的内核对象,以便下次申请时不需要再次初始化和分配空间,类似对象池的概念。并且没有修改通用的内存分配算法,以保证不影响大内存块分配时的性能。
由于SLAB按照对象的大小进行了分组,在分配的时候不会产生堆分配方式的碎片,也不会产生Buddy分配算法中的空间浪费,并且支持硬件缓存对齐来提高TLB的性能,堪称完美。
但是这个世界上没有完美的算法,一个算法要么占用更多的空间以减少运算时间,要么使用更多的运算时间减少空间的占用。优秀的算法就是根据实际应用情况在这两者之间找一个平衡点。SLAB虽然能更快的分配内核对象,但是metadata,诸如缓存队列等复杂层次结构占用了大量的内存。
SLUB因此而诞生:
SLUB 不包含SLAB这么复杂的结构。SLAB不但有队列,而且每个SLAB开头保存了该SLAB对象的metadata。SLUB只将相近大小的对象对齐填入页面,并且保存了未分配的SLAB对象的链表,访问的时候容易快速定位,省去了队列遍历和头部metadata的偏移计算。该链表虽然和SLAB一样是每 CPU节点单独维护,但使用了一个独立的线程来维护全局的SLAB对象,一个CPU不使用的对象会被放到全局的partial队列,供其他CPU使用,平衡了个节点的SLAB对象。回收页面时,SLUB的SLAB对象是全局失效的,不会引起对象共享问题。另外,SLUB采用了合并相似SLAB对象的方法,进一步减少内存的占用。
据内核开发人员称,SLUB相对于SLAB有5%-10%的性能提升和减少50%的内存占用(是内核对象缓存占用的,不是全局哦,否则Linux Kernel可以修改主版本号了)。所以SLUB是一个时间和空间上均有改善的算法,而且SLUB完全兼容SLAB的接口,所以内核其他模块不需要修改即可从SLUB的高性能中受益。SLUB在2.6.22内核中理所当然的替代了SLAB。
+++++++++++++++++++++++++++++++++++++++
Linux 所使用的 slab 分配器的基础是 Jeff Bonwick 为 SunOS 操作系统首次引入的一种算法。Jeff 的分配器是围绕对象缓存进行的。在内核中,会为有限的对象集(例如文件描述符和其他常见结构)分配大量内存。Jeff 发现对内核中普通对象进行初始化所需的时间超过了对其进行分配和释放所需的时间。因此他的结论是不应该将内存释放回一个全局的内存池,而是将内存保持为针对特定目而初始化的状态。例如,如果内存被分配给了一个互斥锁,那么只需在为互斥锁首次分配内存时执行一次互斥锁初始化函数(mutex_init)即可。后续的内存分配不需要执行这个初始化函数,因为从上次释放和调用析构之后,它已经处于所需的状态中了。
Linux slab 分配器使用了这种思想和其他一些思想来构建一个在空间和时间上都具有高效性的内存分配器。
图 1 给出了 slab 结构的高层组织结构。在最高层是 cache_chain,这是一个 slab 缓存的链接列表。这对于 best-fit 算法非常有用,可以用来查找最适合所需要的分配大小的缓存(遍历列表)。cache_chain 的每个元素都是一个 kmem_cache 结构的引用(称为一个 cache)。它定义了一个要管理的给定大小的对象池。
图 1. slab 分配器的主要结构
图 1. slab 分配器的主要结构
每个缓存都包含了一个 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 分配器还支持通用对象的初始化,从而避免了为同一目而对一个对象重复进行初始化。最后,slab 分配器还可以支持硬件缓存对齐和着色,这允许不同缓存中的对象占用相同的缓存行,从而提高缓存的利用率并获得更好的性能。
回页首
API 函数
现在来看一下能够创建新 slab 缓存、向缓存中增加内存、销毁缓存的应用程序接口(API)以及 slab 中对对象进行分配和释放操作的函数。
第一个步骤是创建 slab 缓存结构,您可以将其静态创建为:
struct struct kmem_cache *my_cachep;
然后其他 slab 缓存函数将使用该引用进行创建、删除、分配等操作。kmem_cache 结构包含了每个中央处理器单元(CPU)的数据、一组可调整的(可以通过 proc 文件系统访问)参数、统计信息和管理 slab 缓存所必须的元素。
kmem_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 参数指定了为缓存启用的选项。这些标志如表 1 所示。
表 1. kmem_cache_create 的部分选项(在 flags 参数中指定)
选项 说明
SLAB_RED_ZONE 在对象头、尾插入标志,用来支持对缓冲区溢出的检查。
SLAB_POISON 使用一种己知模式填充 slab,允许对缓存中的对象进行监视(对象属对象所有,不过可以在外部进行修改)。
SLAB_HWCACHE_ALIGN 指定缓存对象必须与硬件缓存行对齐。
ctor 和 dtor 参数定义了一个可选的对象构造器和析构器。构造器和析构器是用户提供的回调函数。当从缓存中分配新对象时,可以通过构造器进行初始化。
在创建缓存之后, kmem_cache_create 函数会返回对它的引用。注意这个函数并没有向缓存分配任何内存。相反,在试图从缓存(最初为空)分配对象时,refill 操作将内存分配给它。当所有对象都被使用掉时,也可以通过相同的操作向缓存添加内存。
kmem_cache_destroy
内核函数 kmem_cache_destroy 用来销毁缓存。这个调用是由内核模块在被卸载时执行的。在调用这个函数时,缓存必须为空。
void kmem_cache_destroy( struct kmem_cache *cachep );
kmem_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 给出了标志选项的部分列表。
表 2. kmem_cache_alloc 和 kmalloc 内核函数的标志选项
标志 说明
GFP_USER 为用户分配内存(这个调用可能会睡眠)。
GFP_KERNEL 从内核 RAM 中分配内存(这个调用可能会睡眠)。
GFP_ATOMIC 使该调用强制处于非睡眠状态(对中断处理程序非常有用)。
GFP_HIGHUSER 从高端内存中分配内存。
kmem_cache_zalloc
内核函数 kmem_cache_zalloc 与 kmem_cache_alloc 类似,只不过它对对象执行 memset 操作,用来在将对象返回调用者之前对其进行清除操作。
kmem_cache_free
要将一个对象释放回 slab,可以使用 kmem_cache_free。调用者提供了缓存引用和要释放的对象。
void kmem_cache_free( struct kmem_cache *cachep, void *objp );
kmalloc 和 kfree
内核中最常用的内存管理函数是 kmalloc 和 kfree 函数。这两个函数的原型如下:
void *kmalloc( size_t size, int flags );
void kfree( const void *objp );
注意在 kmalloc 中,惟一两个参数是要分配的对象的大小和一组标志(请参看 表 2 中的部分列表)。但是 kmalloc 和 kfree 使用了类似于前面定义的函数的 slab 缓存。kmalloc 没有为要从中分配对象的某个 slab 缓存命名,而是循环遍历可用缓存来查找可以满足大小限制的缓存。找到之后,就(使用 __kmem_cache_alloc)分配一个对象。要使用 kfree 释放对象,从中分配对象的缓存可以通过调用 virt_to_cache 确定。这个函数会返回一个缓存引用,然后在 __cache_free 调用中使用该引用释放对象。
其他函数
slab 缓存 API 还提供了其他一些非常有用的函数。 kmem_cache_size 函数会返回这个缓存所管理的对象的大小。您也可以通过调用 kmem_cache_name 来检索给定缓存的名称(在创建缓存时定义)。缓存可以通过释放其中的空闲 slab 进行收缩。这可以通过调用 kmem_cache_shrink 实现。注意这个操作(称为回收)是由内核定期自动执行的(通过 kswapd)。
unsigned int kmem_cache_size( struct kmem_cache *cachep );
const char *kmem_cache_name( struct kmem_cache *cachep );
int kmem_cache_shrink( struct kmem_cache *cachep );
回页首
slab 缓存的示例用法
下面的代码片断展示了创建新 slab 缓存、从缓存中分配和释放对象然后销毁缓存的过程。首先,必须要定义一个 kmem_cache 对象,然后对其进行初始化(请参看清单 1)。这个特定的缓存包含 32 字节的对象,并且是硬件缓存对齐的(由标志参数 SLAB_HWCACHE_ALIGN 定义)。
清单 1. 创建新 slab 缓存
static struct kmem_cache *my_cachep;
static void init_my_cache( void )
{
my_cachep = kmem_cache_create(
"my_cache", /* Name */
32, /* Object Size */
0, /* Alignment */
SLAB_HWCACHE_ALIGN, /* Flags */
NULL, NULL ); /* Constructor/Deconstructor */
return;
}
使用所分配的 slab 缓存,您现在可以从中分配一个对象了。清单 2 给出了一个从缓存中分配和释放对象的例子。它还展示了两个其他函数的用法。
清单 2. 分配和释放对象
int slab_test( void )
{
void *object;
printk( "Cache name is %s\n", kmem_cache_name( my_cachep ) );
printk( "Cache object size is %d\n", kmem_cache_size( my_cachep ) );
object = kmem_cache_alloc( my_cachep, GFP_KERNEL );
if (object) {
kmem_cache_free( my_cachep, object );
}
return 0;
}
最后,清单 3 演示了 slab 缓存的销毁。调用者必须确保在执行销毁操作过程中,不要从缓存中分配对象。
清单 3. 销毁 slab 缓存
static void remove_my_cache( void )
{
if (my_cachep) kmem_cache_destroy( my_cachep );
return;
}
回页首
slab 的 proc 接口
proc 文件系统提供了一种简单的方法来监视系统中所有活动的 slab 缓存。这个文件称为 /proc/slabinfo,它除了提供一些可以从用户空间访问的可调整参数之外,还提供了有关所有 slab 缓存的详细信息。当前版本的 slabinfo 提供了一个标题,这样输出结果就更具可读性。对于系统中的每个 slab 缓存来说,这个文件提供了对象数量、活动对象数量以及对象大小的信息(除了每个 slab 的对象和页面之外)。另外还提供了一组可调整的参数和 slab 数据。
要调优特定的 slab 缓存,可以简单地向 /proc/slabinfo 文件中以字符串的形式回转 slab 缓存名称和 3 个可调整的参数。下面的例子展示了如何增加 limit 和 batchcount 的值,而保留 shared factor 不变(格式为 “cache name limit batchcount shared factor”):
# echo "my_cache 128 64 8" > /proc/slabinfo
limit 字段表示每个 CPU 可以缓存的对象的最大数量。 batchcount 字段是当缓存为空时转换到每个 CPU 缓存中全局缓存对象的最大数量。 shared 参数说明了对称多处理器(Symmetric MultiProcessing,SMP)系统的共享行为。
注意您必须具有超级用户的特权才能在 proc 文件系统中为 slab 缓存调优参数。
+++++++++++++++++++++++++
slob分配器
slob是一个相对简单一些的分配器,主要使用在小型的嵌入式系统。在选择了CONFIG_EMBEDDED后,就可以选用CONFIG_SLOB选项,使用SLOB 分配器中。
slob是一个经典的K&R/UNIX堆分配器,其具有一个slab模拟层,和被slab替代的linux原来的kmalloc分配器比较相似,比slab更有空间效率,尺寸更小,但是依然存在碎片和难于扩展(对所有操作都简单地上锁)的问题,只适用于小系统。
slob获得的是已经对齐的对象。slob在x86上的粒度是8字节,甚至可以减少到4字节。slob堆是一个单向列表,连接了从get_free_page获得的页面,从堆上按照first-fit的原则依照需求增长和分配。
从kmalloc获得的块都是8字节对齐并包含一个8字节的头。如果kmalloc被要求一个PAGE_SIZE 或更大的对象,其将会调用直接get_free_pages以获得一个页面对齐的块,同时维护一个这些页面和级数的链接列表。这些对象会在kfree中被 检查页面对齐。
在slob最上层通过简单调用slab的构造和析构效仿slab。返回8字节对齐的对象,除非设置了SLAB_HWCACHE_ALIGN 标记,在这种情况下,低级的分配器将块分片,以创建正确的对齐方式。
五。[[Anchor(NBE5)]]slub分配器
2.6.22中的SLAB内存管理代码将被SLUB代替。SLAB是经典的管理内核的内存的代码,Glib开发者 受此启发,在Glib2.10中增加了GSlice内存管理方式,带来了性能上的飞跃。但是slab维护了大量的对象队列,这些队列虽然可以很快地被分 配,但是过于复杂,而且维护所占用的空间会随着系统节点的增加而急剧增长。
slub就是作为slab的可替代选项出现的。slub是一种不使用队列的分配器。slub取消了大量的队列和相 关维护费用,获得了极大的性能和伸缩性提高,并在总体上简化了slab结构,使用了基于每CPU的缓存,同时保留了slab的用户接口,而且slub还提 供了强大的诊断和调试能力。
在slub分配器中,slab只是一组页面,页面中整齐地填充了给定尺寸的对象 。slab本身不包含元数据metadata,比较特殊的是空余的对象被组织成简单相连的list.。当分配请求出现时,最开头的空余对象先被定位,并从list中移出,返回给请求者。
由于缺少每个slab中原先包含的元数据,寻找第一个空余对象就是首先需要考虑的问题。答案就是slub分配器将 相关信息保存到系统内存图表中,也就是在页面结构中标记出了产生slab的页面。增加页面结构尺寸不是个好注意,事实上slub分配器在页面结构中增加了 三个union成员:
short unsigned int inuse;slab中分配对象的数目
short unsigned int offset;下一个空余对象的偏移
void *freelist;指向slab中第一个空余对象
当slab刚被分配器创建时是没有立刻分配对象的。当对象被分配以后,就以"partial" slab形式被保存在kmem_cache结构的list中。对于每个NUMA节点都包含一个"partial"的list。
在多CPU环境中,每个CPU都包含一组活跃的slab,以避免cache line的污染。系统提供了一个特殊的线程(通过workqueue)来监视每个CPU上slab的利用。如果这个per-CPU的slab没有被使用, 就将其放回partial的list并提供给其它处理器。
如果slab中所有的对象都被分出去了,分配器可以简单地将slab忘记。当这个slab中有一个对象被释放,分 配器通过内存映射表重新定位相关的slab,并将其放回适当的"partial" list。如果给定slab中所有对象都被释放了(通过跟踪使用计数器),整个slab都将被放回页面分配器以被重新利用。
slub的一个主要好处就是可以合并具有相似尺寸和参数的对象的slab。这样在系统中只需要存在更少的slab缓存(具测试可以减少50%左右),而且slab分配的位置会更合理,slab内存中的碎片会更少。
下面是slub的一些特点:
a。对象队列的管理
在slab中一个必须关注的重点就是大量对象队列的复杂管理。在slub中没有这些队列。取而代之的是我们为每个对应的CPU分配选择slab,并且直接从slab中使用对象,而不是从队列中出队。
b。对象队列的存储费用
slab对象队列是每CPU每节点存在的。外部的缓存队列也需要一组队列,包含了每个节点上每个处理器的队列。这 在非常大的系统中,队列和对象的数目将造成这样的队列象指数一样增长。例如在我们的系统中,有1k的节点/处理器,这样就需要上G字节用于联系存储对象和 队列的关联,这还不包括在那些队列中本身包含的对象。这样子下去恐怕整个系统的内存都要被这些队列消耗光了。
c。slab中meta数据的费用
slab机制使用了每个slab开头作为meta数据。这意味着数据无法自然地从slab块的开头进行对齐。而 slub机制保存所有meta数据在相应的页面结构中。这样slab中的对象可以自然对齐。例如一个128字节的对象可以按照128字节边界对齐,并可以 很合适地放入4K的页面,这在slab是无法做到的。
d。slab使用了一个超复杂的缓存回收器
在单处理器系统slub不需要缓存回收器。在SMP系统中对应CPU的slab必须被放回partial列表,这样的操作是很简捷的并且不需要对象列表的遍历。而slab是每CPU失效的,在缓存回收期间,共享的和相异的对象队列会造成严重的分裂。
e。slab需要复杂的NUMA策略层支持
slub将NUMA策略放到页面分配器中处理。这意味着分配本身是简洁的(slub插入在页面层)。slab应用 策略将在slab机制中分配的slab对象区分开,这样一系列对象一个节点接一个节点到来,并频繁引用内存策略是可能导致性能问题的。slub只是从一个 节点拿到一个充满对象的slab,然后切换到另一个。
f。缩减partial列表的尺寸
在slab中每个节点都有partial列表。这意味着随着时间推移,大量的partial slab将聚集在这些列表上的。而分配器只有在特定的节点上才可以复用。slub使用了一个全局的partial slab池子,通过这个池子来消费slab,以减少碎片。
g。可微调的
slab机制对每个slab缓存的微调是很诡异的。可以详细地操作队列尺寸,填充队列仍然需要使用spin lock来检查slab。slub提供了一个全局参数min_slab_order来微调。增加这个值可以减少锁的费用。这个值越大,在per CPU和partial列表之间产生的页面移动就越小,slub的伸缩性就越好。
g。slab合并
我们经常遇到相似参数的slab缓存,slub可以在启动时检测并将其合并成相应的统一caches。这样可以提 高内存的利用率。通过slab的合并可以减少使用50%的缓存。这同样也可以减少slab碎片,因为partial的slab可以再次被使用。slab合 并可以在启动时使用slub_nomerge开关关闭。
h。诊断
目前的slab诊断方法是比较复杂并且需要内核的支持。slub本身包含了调试代码,slub诊断功能可以通过传递slab_debug参数打开。这些参数还可以指定诊断单个或一组slab caches。这意味着系统可以平时正常的性能运行,可以使运行条件复现。
j。修复
如果基本的完整检查是打开的,slub具备检测通用错误并修复的能力,尽一切可能让系统可以继续运行。
j。跟踪
跟踪功能可以通过在启动时传递slab_debug=T,<slabcache>参数打开。slub将处理定义的slabcache上的所有动作,并在释放是dump对象内容。
k。DMA缓存的按需创建
一般来说DMA缓存并不是必须的。只有在kmalloc时使用了GFP_DMA才创建这个单一的slabcache。对于系统来说,如果没有ZONE_DMA,就不需要支持了。
l。性能提高
一些测试显示slub可以有5-10%的性能提升。slub的锁耗费取决于分配尺寸。如果我们能够可靠地分配大量连续页面,就可以提高slub的性能。