Slab分配器:一种对象缓存内核内存分配器(1-2节)

                              Slab分配器:一种对象缓存内核内存分配器

                                                    Jeff bonwick

Sun

译者:陈盛荣(wengermail@gmail.com)

摘要:

本文提出了SunOS 5.4内核内存分配器的设计概要。这个分配器是基于一套对象缓存primitive机制。这种机制通过保留复杂内存对象的状态来减少分配他们的时间。对于无状态的内存(例如:数据页面和临时缓冲区),这些相同的primitives也被证明是有效的,因为这些内存的空间效率很高、(分配)速度很快。分配器的缓存对象根据全局的内存压力动态响应,实施对象着色方案改进系统整体的缓存利用率和总线平衡。分配器同时也提供一些统计和调试的函数,能够检测系统大范围的问题。

1.       介绍

分配和释放对象是内核操作中最常见的。因此一种快速的内核内存分配器显得很重要。然而很多情况下,初始化和销毁对象的时间大于分配和释放内存对象的时间。因此,改进分配器很有用,甚至通过缓存经常用的内存对象(用的过程中得以保存基本结构)可以获得更多的好处。

本文首先讨论对象缓存,因为所需要的接口将对剩下的分配器整形(??)。接下来描述实现细节。第4部分描述缓冲区(buffer)地址分布在系统整体缓存(cache)利用和总线平衡上的作用,以及表明一种简单的着色模式能改进这两种情况。第5部分跟几个著名的内核内存分配器做性能比较,发现本分配器在空间和时间上普遍占优势。最后,第6部分描述了分配器的调试特性,该特性能在检查出整个系统各种各样的问题。

 

2 对象缓存

对象缓存是一种处理经常被分配和释放的对象的技术。这种思想是:在对象被利用过程中,保存对象的初始化状态(刚创建时的状态)即对象的不变部分,从而不必要销毁对象以至于每次用到对象的时候,都重新创建一次。例如:一个包含互斥变量的对象,只在对象被分配的时候执行一次mutex_init()。对象能够被释放和多次重新创建的时候,避免每次都执行mutex_destroy ()mutex_init()的开销。通常适合作为创建状态的有:对象的嵌套锁,条件变量、引用计数、只读对象和数据的列表等。

由于创建对象的开销可能远远大于内存分配的开销,因此缓存很重要。例如,在SPARCstation-2上跑SunOS5.4内核,分配器在分配和释放流头部中能减少5.733微秒的时间。大部分的减少时间都是由于对象缓存,如下表所示:

缓存对于多线程的环境特别有利,由于许多最常见的被分配对象包含一个或者多个嵌套锁、条件变量和其他可创建(contructible)变量。

设计对象缓存的直接做法:

分配一个对象:

if(缓存中有该对象)

直接利用(不必再创建了)

else

{

  分配内存;

  创建对象;

}

释放一个对象:

返回到缓存中(没有调用析构函数)

向缓存要内存(译注:内存不够用的情况):

从缓存中取出一些对象;(译注: 可以用FIFOLRU等)

销毁对象

释放一些额外的内存

当对象第一次进入缓存的时候,必须保证对象的创建状态只初始化一次。一旦缓存(索引)是稠密的,分配和释放对象将是快速的操作。

2.1 一个例子

考虑下面的数据结构

struct  foo { 
kmutex_t foo_lock; 
kcondvar_t foo_cv; 
struct  bar  * foo_barlist; 
int  foo_refcnt; 
};

 

假设foo结构必须在没有引用计数(foot_refcnt)和没有悬挂bar 事件(不管是什么)即(foo_barlist == NULL)时,才能释放foo。那么动态分配foo结构的生命周期将是这样:

 

foo  =  kmem_alloc( sizeof  ( struct  foo), 

KM_SLEEP); 

mutex_init(
& foo -> foo_lock, ...); 

cv_init(
& foo -> foo_cv, ...); 

foo
-> foo_refcnt  =   0

foo
-> foo_barlist  =  NULL; 

use foo; 


ASSERT(foo
-> foo_barlist  ==  NULL); 

ASSERT(foo
-> foo_refcnt  ==   0 ); 

cv_destroy(
& foo -> foo_cv); 

mutex_destroy(
& foo -> foo_lock); 

kmem_free(foo);

 

注意到每次用到foo结构的时候,我们执行了一系列没有建树但是时间开销昂贵的操作。所有这些额外的工作(在这个例子中:除了use foo之外)都能通过对象缓存砍掉(eliminated)

 

2.2 中央分配器的对象缓存案例

当然,任何子系统都可以根据上面的描述自己实现对象缓存的算法,而不用借助中央分配器。然而,这种方法有几个不好的地方:

1) 在对象缓存(object cache)和其他系统之间有一种微妙(natural tension)的关系。对象缓存需要长久持有内存,而其他系统需要这些内存。私自管理缓存不能很好处理这种关系。他们有限的看到系统整体的内存需求和看不到其它的内存需求。类似的,其它系统不知道对象缓存的存在因此没有办法从他们那“pull”内存。

2) 由于私有缓存实现可能绕过中央分配器,也绕过分配器要处理的统计机制和调试特性。这使得操作系统更难监测和调试(内存问题)。

3) 对一个普通问题有很多一种解决方案的不同实例,将增加内核代码的大小和维护开销。

分配器与客户端之间的协作,对象缓存相对于标准的kmem_alloc( 9F )/kmem_free( 9F )接口允许的还要高。接下来的部分提供一个支持在中央分配器中缓存创建对象的接口。

 

2.3 缓存对象接口

接口必须遵循下面两点:

A)对象的描述(名字,大小,对齐, 构造函数以及析构函数)属于客户端,而不在中央分配器。分配器不应该知道sizeof (struct inode)是一个有效的池大小。例如:这种假想是不可靠的,因为不可预测第三方驱动设备、流模块以及文件系统的需求。

 

(B) 内存管理策略属于中央分配器,而不是客户端。客户端只需要快速的分配和释放对象。他们不必关心怎样有效的管理潜在的内存。

 

 按照规则A,对象缓存创建必须是客户端驱动的,同时包括对象的详细描述。

 (1)

  struct  kmem_cache  * kmem_cache_create( char   * name, size_t size,  int  align,  void  ( * constructor)( void   * , size_t),  void  ( * destructor)( void   * , size_t));

 

创建一系列对象的高速缓存,包括每个规格大小和所对齐的对齐边界。对齐边界根据最小的允许值四舍五入,所以对于没有特定的对齐要求,aligin可以等于0 名字唯一区别缓存中统计和调试信息。构造函数构造对象(对象初始化的时候执行一次)并放入缓存;如果适用,析构函数不一定执行。构造函数和析构函数带有参数size_t,以支持相似高速缓存族,例如:流消息。kmem_cache_create返回一个访问缓存的指针(不透明的描述符)。

 

接着,按照规则B,客户端只需要分配和释放对象两个函数

2该函数从缓存中获取一个对象。但对象必须处于已构造(初始)状态。参数flags可以是KM_SLEEP或者KM_NOSLEEP,表明在当前没有可用缓存对象的情况下,是否等待。

void   * kmem_cache_alloc( struct  kmem_cache  * cp,  int  flags);

 

(3)

void  kmem_cache_free( struct  kmem_cache  * cp,  void   * buf);

 

该函数返回一个对象进入缓存。对象必须仍处于已构造(初始)状态。

最后,如果不再需要缓存,客户端可以销毁对象。

(4)

void  kmem_cache_destroy( struct  kmem_cache  * cp);

 

销毁缓存和回收相关的资源。所有已分配的对象必须已经在缓存中。

 

这个接口允许我们实现最适合客户端需求的灵活分配器。从这个意义上来说,是个定制的分配器。然而,这并不一定都像大部分定制分配器做的那样,建立编译期知识[Bozman 84A , Grunwald 93A , Margolin71]或者猜测自适应接口方法[Bozman84B,

Leverett82, Oldehoeft85]。而是对象缓存接口允许客户端根据自己的需要快速指定分配服务。

2.4 一个例子

下面的例子示范了2.1所介绍的foo对象在对象缓存中的应用。构造函数和析构函数如下:

 

 

void  foo_constructor( void *  buf,  int  size)

{

  
struct  foo  * foo  =  buf;
  mutex_init(
& fo cv_init( & foo -> foo_cv, ...); 
  foo
-> foo_refcnt  =   0
  foo
-> foo_barlist  =  NULL; o -> foo_lock, …); 
}

void  
foo_destructor(
void   * buf,  int  size) 

struct  foo  * foo  =  buf; 
ASSERT(foo
-> foo_barlist  ==  NULL); 
ASSERT(foo
-> foo_refcnt  ==   0 ); 
cv_destroy(
& foo -> foo_cv); 
mutex_destroy(
& foo -> foo_lock); 
}

创建foo对象的缓存

foo_cache  =  kmem_cache_create(“foo_cache”,  sizeof ( struct  foo), 0 , foo_constructor, foo_destructor);

 

分配、利用和释放foo对象

 

foo对象的速度很快,因为分配器经常只需要从缓存中取出已构造的对象。foo_contructor foo_destructor将被分别调用从缓存中取对象和存放对象。

 

foo  =  kmem_cache_alloc(foo_cache, KM_SLEEP);

use foo;

kmem_cache_free(foo_cache, foo);

这使得分配foo对象的速度很快,因为分配器经常只需要从缓存中取出已构造的对象。foo_contructor foo_destructor将被分别调用从缓存中取对象和存放对象。

 

上面的例子示范了对象缓存的一个有用的副作用:通过从热路径??(经常执行的指令)中移除极少执行的构造函数和析构函数,来利用缓存对象,达到减少代码的指令缓存路径。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值