slab的“对象重用”
到目前为止,SUN于1991年发明的Slab Allocator是各种OS内核Memory Allocator中被认为整体性能最好的。它有几个措施来促进内存分配性能的提高,其中之一就是"对象重用"。
原理
OS可以使用Slab提供通用内存块的申请与释放;所谓通用内存块指的是可以被用于非特定目的的内存块,它们被申请出来之后,可以用做构建一个对象,也可以用做缓冲区。这种情况下,每一个slab cache都是一个提供固定大小的通用内存块的pool。在linux 2.4中,这样的slab cache有13个,分别用于提供大小为32,64,128,256,......,131072的通用内存块。
除了这样的slab cache,OS可以用slab提供特定对象的Allocator。在一个提供特定对象的slab cache中,只有一种类型的对象可以分配,不同的特定对象分别由自己特定的slab cache。比如inode,vm_area等等比较复杂而内核又非常频繁的使用的对象,都拥有自己专用的slab cache。
对于特定类型的slab cache,就可以使用"对象重用"技术。
其实,Slab的"对象重用"对性能的改进,并非纯粹意义上的内存分配与释放操作性能上的提高。我们使用C++来从内存分配器来构建对象时使用如下方式:
type_pointer* __p_object = new type_pointer(__init_para1, _init_para2);
这条语句包含两个过程:
1)从memory allocator中分配用于构建对象的内存;
2)调用class type_pointer的constructor函数,对对象进行构造。
C++的对象销毁方式如下:
delete __p_object;
这条语句也包含两个过程:
1)调用class type_pointer的destructor函数;
2)将对象占用的内存释放回memory allocator中。
一般的Memory Allocator仅仅负责内存的释放与申请,即上述对象构建过程中的过程1和对象销毁过程的过程2。但slab却可以在对象构建过程中的过程2和对象销毁过程的过程1中做了文章。
每一个对象,在传统的方式下,其生命周期总是包含如下如下五个过程:
1)从memory allocator中分配用于构建对象的内存;
2)调用class的constructor函数;
3)使用它;
4)调用class的destructor函数;
5)将对象占用的内存释放回memory allocator中。
由于Slab在内存分配与释放方面的良好设计,绝大多数情况下,内存的分配都非常迅速,在所有情况下,内存的释放操作都非常快捷。相比较之下,对象的constructor和destructor过程占去了对象的构建与销毁的大多数时间,对于复杂的对象更是如此。
仔细观察一下上面列出的五个过程,再稍微让你的脑细胞做点运动,你就会发现一个重要事实——既然在一个特定对象的slab cache中存放的都是同一对象,如果对象的使用者能够保证它所获取的对象在释放之前将对象恢复到对象刚刚调用constructor函数之后的状态,那么这个对象就没有必要被调用destructor函数;取而代之,我们只需要把这个对象放回slab cache,下一次,都这块内存重新被分配的时候,由于其状态和调用了constructor之后的状态是一样的,所以,也就不再需要重新调用constructor。直到最后,这个slab cache block被返还给更底层的内存管理器时,也就是说,这个对象所属的大块内存不再属于当前slab cache的时候,再调用其destructor就行了。
所以,对于一个特定对象的slab cache中,所有的对象内存块,在多次的分配与释放中,只需要被调用constructor一次,也只需要被调用destructor一次,这些很耗时的冗余过程就清除掉之后,性能自然能够得到显著的提高。
在我们为这一重大改进欢欣雀跃的时候,必须牢记这一改进所包含的前提,再说一遍——"调用者必须保证在使用后,把对象恢复到使用前的状态,即对对象刚刚调用了constructor之后的状态"。
一个例子
早期,Linux并没有使用Slab。但Slab的声誉蒸蒸日上,让Linux的开发者心猿意马,终于按耐不住,换了门庭。
Linux 2.4的slab cache的创建接口为:
kmem_cache_t* kmem_cache_create(const char* __name, size_t __size,
size_t __offset, unsigned long __flags,
void (*__ctor)(void*, kmem_cache_t*, unsigned long),
void (*__dtor)(void*, kmem_cache_t*, unsigned long));
这个函数的最后两个参数,__ctor和__dtor就是当前slab cache对象的constructor和destructor函数。
下面,我们看一个例子——
假设,我们有一个对象,其类型定义为:
struct object{
mutex_t lock;
char* buffer;
int count;
};
其constructor为:
void object_ctor(void* __mem, kmem_cache_t*, unsigned long)
{
struct object *__obj = (struct object*) __mem;
mutex_init(&__obj->lock);
__obj->buffer = NULL;
__obj->count = 0;
}
其destructor为:
void object_dtor(void* __mem, kmem_cache_t*, unsigned long)
{
struct object *__obj = (struct object*) __mem;
assert(__obj->buffer == NULL);
assert(__obj->count == 0);
assert(!mutex_test_lock(&__obj->lock));
mutex_destory(&__obj->lock);
}
在这个例子中,一个struct object类型的对象在被调用object_ctor后,其状态为:
1)lock被创建并初始化,处于unlock状态;
2)buffer指针为NULL;
3)count的值为0。
然后,它就可以使用这个对象,比如下面的代码:
void foo_change(object* __obj, size_t __count)
{
mutex_lock(&__obj->lock);
__obj->buffer = (char*) malloc(__count+1);
__obj->count = __count;
}
这段代码改变了__obj的状态,因为其buffer域和count域都改变了。所以,在将__obj返还给slab之前,用户必须将它们的状态还原。
void foo_recovery(object* __obj)
{
if(__obj->buffer)
free(__obj->buffer);
__obj->count = 0;
if(mutex_test_lock(&__obj->lock))
mutex_unlock(&__obj->lock);
}
这个例子展示了我们如何使用slab的"对象重用"机制,应该够了。但我还不想让自己的大脑这么快停止对这个问题的思考,我们接着往下看——
上面的例子是用C语言写的,我们不妨将其改为C++的实现。
class object
{
mutex_t lock;
char* buffer;
int count;
public:
object(void)
{
mutex_init(&lock);
buffer = NULL;
count = 0;
}
~object()
{
assert(buffer == NULL);
assert(count == 0);
assert(!mutex_test_lock(&lock));
mutex_destory(&lock);
}
void foo_change(size_t __count)
{
mutex_lock(&lock);
buffer = (char*) malloc(__count+1);
count = __count;
}
void foo_recovery(void)
{
if(buffer)
free(buffer);
count = 0;
if(mutex_test_lock(&lock))
mutex_unlock(&lock);
}
};
对于C++语言,由于自身存在着ctor/dtor机制,我们使用new从slab allocator中分配一个对象的时候,class的construct会被自动调用;同样,当我们使用delete将对象销毁的时候,destructor也会自动被调用,这是语言本身的机制,我们无法回避它。这样,每次对象被构造/销毁的时候,class的constructor函数和destructor函数都要被调用,这种情况下,slab提供的"对象重用"根本没有用武之地。
难道一种语言就会影响一个这么重要功能的使用?更何况C++还是一种通用编程语言!!我想你已经开始不安,然后愤怒,最后陷入绝望。
在你绝望之前,做几下深呼吸,然后仔细观察形势,认真考虑对策,很可能"柳岸花明"。
在你情绪平稳之后,我们再来看看上面的C语言的例子。
这个例子中,object的对象包含两部分资源:
1)由object_ctor构造的资源lock;
2)由调用者构造的资源buffer;count也应该算。
上帝是存在的,它在圣经里教导我们:"尘归尘,土归土"。所以我们应该让slab去做它该做的——构造和销毁lock;让class的ctor/dtor构造和销毁buffer。所以,上面的C++的改写是错误的,正确的实现应该是:
class object
{
public:
mutex_t lock;
char* buffer;
int count;
public:
void object(size_t __count)
{
mutex_lock(&lock);
buffer = (char*) malloc(__count+1);
count = __count;
mutex_unlock(&lock);
}
void ~object()
{
if(buffer)
free(buffer);
count = 0;
if(mutex_test_lock(&lock))
mutex_unlock(&lock);
}
public:
static void ctor(void* __obj,kmem_cache_t*, unsigned long)
{
object* __object = static_cast(__obj);
mutex_init(&__object->lock);
__object->buffer = NULL;
object->count = 0;
}
static void dtor(void* __obj, kmem_cache_t*, unsigned long)
{
object* __object = static_cast(__obj);
assert(__object->buffer == NULL);
assert(__object->count == 0);
assert(!mutex_test_lock(&__object->lock));
mutex_destory(&__object->lock);
}
};
函数ctor和dtor就是交给slab去调用的构造和析构函数,非常重要的一点是它们必须是static的,或者干脆把它们实现为外部过程。
然后,我们就可以这样来创建其slab cache:
typedef void (*SLAB_CDTOR)(void*, kmem_cache_t*, unsigned long);
kmem_cache_t* object_cache;
object_cache = kmem_cache_create("object cache", __size,
__offset, __flags,
(SLAB_CDTOR)object::ctor,
(SLAB_CDTOR)bject::dtor);