概述
在扩展
tcmalloc进行内存诊断追踪时,记录内存的全局链表节点,本身也需要分配内存,如果采用
tcmalloc默认的内存分配器,那么这些链表节点内存就会散落在
heap中,和用户申请的小内存掺杂在一起,如果用户内存发生了写越界,那么就可能影响到链表节点的正确性。更好的方案是从一段远离
heap的内存中集中分配链表节点内存。
本方案参考
linux kernel中
slab的思想,设计了
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倍,其中
X是
2的
m次方。
在创建
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都已被释放。
free和
reclaimed的区别是,
free状态下的
span,曾经有
object被分配使用过,也暗示着该
span极有可能被分配了物理内存(一种极端情况除外:用户申请了内存却从没使用过,然后直接释放)。
- Active
活跃状态,这是一种介于
Full和
Free之间的状态,表示该
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的值为
0,
full状态下,
touched_num增长到最大值。
inuse_num表示当前已分配给用户的
obj数目。
free_objs是一个双向链表,存储的是曾被分配但已释放的
objects。
申请内存的流程
申请内存的过程,主要分成
4大步骤:从
slab中获取
span;从
span中获取
object;更新
span状态;紧急内存分配。
从
slab
中获取
span
我们已经知道,
slab中有
4个
span链表,除了
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_objs中
pop一个对象,如果
free_objs为空则再尝试利用增长
touched_num的方式从
span中分配一个对象。
一个示例图如下,
图
3 span对
objects的管理
更新
span
状态
当完成从某
span分配内存之后,
span的状态可能发生变化,这时应及时更新
span状态并将其放入
slab的正确链表中。
紧急内存分配
当用户申请的
object数目超过该
slab的最大
object数时,不得不进行紧急内存分配,也就是利用系统内存分配接口(如
malloc函数)直接从进程堆里分配小内存对象。
释放内存的流程
释放内存的逻辑相对简单一些:
通过用户传入的内存地址,很容易计算得到该内存所在的
span,而后将其插入该
span的
free_objs中即可,最后更新该
span状态。
如果这个内存是通过紧急分配接口分配的,那么直接调用系统内存释放接口释放掉即可。
内存回收(
page reclaiming
)
内存回收的单位是
span,哪些
span需要回收内存呢?当然是那些曾经被用户使用过但已经不再使用的内存,也就是处于
free状态的
spans。
由于这些
span已然被
kernel分配了物理匿名页,除非我们将整个
slab释放掉,否则
kernel是无法主动将这些匿名页回收的。因此需要我们自己设计触发回收的机制。
目前设计的策略是,在释放内存函数的尾部,判断当前
free spans的占比是否达到一个阈值(创建
slab时可设定该阈值),当达到该阈值后,新创建一个线程,在该线程内执行一次内存回收,执行完毕后线程退出。
内存回收流程:
循环从
slab的
free_list中
pop一个
span,调用
madvise(span->base, span->size, MADV_DONTNEED)来将其回收,并将该
span push到
reclaimed_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操作。