MySlab--一种特殊的小内存分配器设计


概述
在扩展 tcmalloc进行内存诊断追踪时,记录内存的全局链表节点,本身也需要分配内存,如果采用 tcmalloc默认的内存分配器,那么这些链表节点内存就会散落在 heap中,和用户申请的小内存掺杂在一起,如果用户内存发生了写越界,那么就可能影响到链表节点的正确性。更好的方案是从一段远离 heap的内存中集中分配链表节点内存。
本方案参考 linux kernelslab的思想,设计了 MySlab(注意,仅参考了 slab的思想,没参考 slab算法)。

MySlab特点:
1,避免内存碎片。
先申请一大段连续内存,然后对其拆分成小内存对象。
2,支持页框回收。
通过一系列对齐优化与算法优化,保证在必要时刻能够及时将物理内存回收给系统。
3,数据安全性。
由于采取了集中管理的方式,相对于散落在 heap中的内存来说,不容易被其它内存越界破坏。也可以利用 mprotect函数在这段内存的前后添加保护页,防止写越界的发生。

注: tcmalloc的扩展方案中,对于 mmap分配的大内存已然添加了保护页,因此可以保护 myslab不被破坏。
设计方案
内存布局
1 myslab的内存布局
从上图看出,整个 Slab位于一大段连续内存,这段内存被分成了 3部分:

  • slab头
  • span数组
  • span区

slab头中存储的是全局信息,例如每个 span包含几个页,详见下文。
span数组存储的是每个 span的信息,例如该 span内还剩多少可用的 objects
span区就是实际的分配 object内存的区域。
名词解释
slab
一个小内存(固定大小)分配器,可以创建多个 slab来分配不同类型的数据。 slab适用于分配小结构体数据。
span
span是内存回收的基本单位,大小是 page_size(4096)X倍,其中 X2m次方。
在创建 slab时,会根据传入的参数进行计算得到每个 span的大小及 span个数。
span的首地址必须保证页对齐。

object
内存对象,这是用户 allocate/free的基本单位。 object的大小并不一定恰好等于用户申请的内存大小,因为考虑到内部管理,会对用户内存进行少量字节的扩展,用来当作链表节点。


span 的状态与转移
按照 span的实际使用情况,将其分为 4种状态:
  • Reclaimed
已回收状态, slab初始化时,所有 span默认都是 reclaimed状态,表示该 span的物理内存已经被系统回收了。为此我们不应在初始化时通过 memset等操作强制为每个 span分配物理内存。
  • Full
满载状态,表示该 span内所有 objects都已被占用,再无空闲 object可用。
  • Free
空闲状态,表示该 span内的所有 objects都已被释放。 freereclaimed的区别是, free状态下的 span,曾经有 object被分配使用过,也暗示着该 span极有可能被分配了物理内存(一种极端情况除外:用户申请了内存却从没使用过,然后直接释放)。
  • Active
活跃状态,这是一种介于 FullFree之间的状态,表示该 span内尚有 object可以被分配。

在申请释放内存过程中, span的状态可能发生变化,状态转移图如下:
2 span的状态转移图

slab span 的管理
slab中采用一个数组 (span_array)来记录每个 span的信息(如当前状态, objects信息等),
并且采用多个双向链表来记录出于不同状态的 span
reclaimed_list, full_list, free_list, active_list
注意,链表的节点指向的是 span_array中的某元素的地址,而不是 span的实际首地址。
span的状态发生变化时,应将该 span移动至对应的状态链表中。
slab初始化时,将所有 span设为 reclaimed状态并加入 reclaimed_list


span object 的管理
span信息结构体中,用 3个变量来管理 object的分配:
int touched_num;
int inuse_num;
List* free_objs;
touched_num表示曾经分配过的 obj数目,这类似于一个 brk指针, reclaimed状态下, touched_num的值为 0full状态下, touched_num增长到最大值。
inuse_num表示当前已分配给用户的 obj数目。
free_objs是一个双向链表,存储的是曾被分配但已释放的 objects

申请内存的流程
申请内存的过程,主要分成 4大步骤:从 slab中获取 span;从 span中获取 object;更新 span状态;紧急内存分配。
slab 中获取 span
我们已经知道, slab中有 4span链表,除了 full-list之外的其它 3个链表中的 span都可以使用,那么这几个链表的优先级如何排序?
PRIO(active_list) > PRIO(free_list) > PRIO(reclaimed_list)
也就是说,我们优先尝试从 active_list中获取 span,最后尝试从 reclaimed_list中获取 span
只有这样的优先级设计,才能尽量减少 span内部的碎片,减少物理内存的占用,并且利于内存回收。

span 中获取 object
span内部有两种方法来分配一个 object:从 free_objs中分配;通过增加 touched_num来分配。优先级:
PRIO(free_objs) > PRIO(touched_num)
也就是说,当从某 span中请求一个 object,优先尝试从 free_objspop一个对象,如果 free_objs为空则再尝试利用增长 touched_num的方式从 span中分配一个对象。
一个示例图如下,
3 spanobjects的管理

更新 span 状态
当完成从某 span分配内存之后, span的状态可能发生变化,这时应及时更新 span状态并将其放入 slab的正确链表中。
紧急内存分配
当用户申请的 object数目超过该 slab的最大 object数时,不得不进行紧急内存分配,也就是利用系统内存分配接口(如 malloc函数)直接从进程堆里分配小内存对象。

释放内存的流程
释放内存的逻辑相对简单一些:
通过用户传入的内存地址,很容易计算得到该内存所在的 span,而后将其插入该 spanfree_objs中即可,最后更新该 span状态。
如果这个内存是通过紧急分配接口分配的,那么直接调用系统内存释放接口释放掉即可。
内存回收( page reclaiming
内存回收的单位是 span,哪些 span需要回收内存呢?当然是那些曾经被用户使用过但已经不再使用的内存,也就是处于 free状态的 spans
由于这些 span已然被 kernel分配了物理匿名页,除非我们将整个 slab释放掉,否则 kernel是无法主动将这些匿名页回收的。因此需要我们自己设计触发回收的机制。
目前设计的策略是,在释放内存函数的尾部,判断当前 free spans的占比是否达到一个阈值(创建 slab时可设定该阈值),当达到该阈值后,新创建一个线程,在该线程内执行一次内存回收,执行完毕后线程退出。

内存回收流程:
循环从 slabfree_listpop一个 span,调用 madvise(span->base, span->size, MADV_DONTNEED)来将其回收,并将该 span pushreclaimed_list中。

用户 API
相关结构体
typedef struct
{
MSObjType type;
int obj_size;
int span_size;
int span_num;
int obj_num_per_span;
int span_size_exponent;
int inuse_objs;
int inuse_objs_highwatermark;
int free_span_num;
int reclaim_threshold; //if the free_span_num reaches the threshold, we do auto reclaim.
int reclaiming;
int reclaim_count;
pthread_mutex_t lock;//use recursive lock
/* first span's base address */
void* span_base;
MSSpan_t* span_array;
MSSpan_t* reclaimed_list;
MSSpan_t* free_list;
MSSpan_t* full_list;
MSSpan_t* active_list;
}MSSlabInfo_t;

typedef struct MSSpan
{
void* base;
int id;
int state;
int touched_num;
int inuse_num;
HackList* free_objs;
struct MSSpan* next;
struct MSSpan* prev;
}MSSpan_t;

typedef struct _HackList{
struct _HackList *next;
struct _HackList *prev;
void *data;
unsigned int flags;
}HackList;

typedef enum
{
ms_span_state_reclaimed,//default state
ms_span_state_active,
ms_span_state_free,
ms_span_state_full,
}MSSpanStatus;

typedef struct
{
MSObjType type;
int obj_size;
int obj_num;
int pages_per_span;
int auto_reclaim_ratio;// ratio = (free_spans/total_spans) * 10
}MSInitSettings_t;

对外接口
MSHandle MySlab_Create(MSInitSettings_t* settings);
void* MySlab_AllocateObj(MSHandle slab);
void MySlab_FreeObj(MSHandle slab, void*obj);
void MySlab_ReclaimMem(MSHandle slab);
void MySlab_Destroy(MSHandle slab);
待优化
1,关于紧急内存分配。
slab内存不够时,需进行紧急内存分配,从系统中申请内存。目前的做法是直接采用系统的 malloc接口进行小内存分配,这种小内存不经过 slab管理。
还有一种可选方案是,允许slab追加span。这样带来的结果是,slab cache不再是地址连续,因此不能用以前的偏移地址的方法来从obj地址计算得到span地址,而是改用如下方案:
1,slab创建时,在header区预留足够的内存以便直接扩展span_array。
2,obj的链表节点区,保存一个指向span的指针,在allocate obj时将该指针赋值,free obj时能立马通过该指针来找到对应的span,从而进行span操作。







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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值