文章目录
为什么需要内存池
简而言之,反复地进行malloc和free不利于内存管理,同时容易产生内存碎片。复杂的代码中还容易出现内存泄漏问题。内存池则提前分配好大块内存作为备用,然后根据用户需求提供现成的小内存块给程序使用。相对来说使用内存池的效率更高一些,也不那么容易产生内存碎片,同时因为对内存块的集中管理,也可以很好地避免内存泄漏问题,即使出现了泄漏也很容易排查。
内存池的设计策略对比
- 由一个大的整块分散成多个小块,回收时再整合成大块(如伙伴系统)。一般以页为单位分配,回收时内存块地址必须连续才能整合起来。
- 提前划分好多个小块,随时回收(如slab分配器)。
- 多个小块不随时回收,在需要回收且满足回收条件时一起全部回收。相对来说更简单实用一些,适用于特定的业务场景。
内存池分配方法设计
内存池结构
内存池以block
为单位向系统申请内存,我们不妨把用户向内存池申请的小块内存叫作piece
。这里以block
大小为4k的内存池为例进行说明。
多个block
如何组织起来呢?通过链表来将各个block
串在一起不失为一种简单的好办法。同时,当一个block
不够分配时,就新建一个block
插入链表来使用。为了管理每个block
,我们给每个block
分配一个描述符。
那么描述符保存到哪里呢,单独申请内存来存吗?显然没必要。block
的描述符就位于每个block
内存区域的开头。因此,内存池向系统申请的实际内存大小要考虑这些描述符占用的空间,比如一个4k的block
实际大小是4k+sizeof(MP_BLOCK
)。
实际上内存池分配给用户的仅仅只是一个指针,指向所分配piece
区域的首地址,当用户需要释放内存时也只是传入这个指针。为了快速知道这个piece
属于哪个block
以方便对block
的使用情况进行统计,可以给每个piece
增加一个简单的描述符,保存一个指向所属block
的指针。这个描述符不宜存储其他内容,否则对于比较小的piece
其描述符可能比它自身的空间还大,就显得不划算了。
当用户申请内存时,我们只管从内存池的哪个位置划出下一个piece
的空间给用户使用,而不管之前申请的piece
各自的状态。因为当我们回收内存时,会直接将整个block
回收,所以特定piece
是否还在使用就无所谓了,只需要看整个block
的状态。
那么对于大于4k的大内存块如何处理呢?对于这种大块我们不妨称其为bucket
,我们对其做单独处理,根据其大小要求向系统申请独立的内存块给它,并且将bucket
描述符也保存到block
。同时将所有的bucket
用一个单独的链表串接起来方便管理,也方便与普通piece
进行区分。
于是最终内存池的结构如下图所示:
空洞的利用
每个block
的末尾难免会出现没有被使用的空洞,因此每次用户申请内存块时,内存池可以先遍历一下每个block
节点,查看一下block
剩下的空洞是否能够满足用户的要求,如果可以则这些空洞能够被使用。但是当block
的链表很长时,这样的每次遍历不够高效。因此可以统计在某个block
节点中申请内存失败的次数,如果失败次数达到某个值,则下一次就不再查看该block
了,直接从其后面的block
节点开始查看。
内存释放
当用户指定某个piece
的首地址进行释放时,其实我们暂时什么也不做,这个piece
依然在那里,等到特定时机再由内存池统一清理其所在block
。当然,如果用户指定释放的地址是一个bucket
,那么自然要把bucket
的空间释放掉归还给系统。这使得我们的内存池在管理上非常简单而高效。
只有当整个block
都处于释放状态时,才进行该block
的清理工作,也就是将整个block
空间清零(不必归还给系统)。所谓清零,其实就是改变某些标志使block
的状态复原,并不需要真的把内存区域全写为0,这也使得管理工作非常的高效。
那么,怎么判断所有内存块都处于释放状态呢?可以由用户主动调用特定接口来使整个内存池复位,也可以通过给每个block
增加一个引用计数来实现。一个block
中每被申请一个piece
引用计数就加1(bucket
的描述符也算一个piece
);相反的每次释放时引用计数就减1。当一个block
的引用计数减到0时,就清理这个block
,使之可以用于下一次内存申请。
数据结构设计
定义block
的描述符如下:
typedef unsigned char* ADDR;
struct _MP_BLOCK {
struct _MP_BLOCK* next;
ADDR start_of_rest; // 当前 block剩余空间的起始地址
ADDR end_of_block; // 当前 block的最后一个地址加1
int failed_time; // 这个 block被申请内存时出现失败的次数
int ref_counter; // 引用计数
};
typedef struct _MP_BLOCK MP_BLOCK;
piece
描述符如下:
struct _PIECE {
MP_BLOCK* block; // 所处的 block
unsigned char data[0]; // 仅作为内存起始地址,不占用空间
};
typedef struct _PIECE PIECE;
给bucket
也单独分配一个描述符,并且在申请bucket
时将其描述符保存在一个piece
中。数据结构的定义如下:
struct _MP_BUCKET {
// 超过 BLOCK_SIZE 内存用 MP_BUCKET 描述
struct _MP_BUCKET* next;
int still_in_use; // 这个 bucket当前有没有被释放掉,如果被释放了可以尝试复用
ADDR start_of_bucket;
};
typedef struct _MP_BUCKET MP_BUCKET;
内存池操作接口实现
初始化内存池
#define MP_PAGE_SIZE (4 * 1024) // 页大小4k,不要让用户设置的 block大小超过这个值
#define MP_MIN_BLK_SZIE 128 // 一个块也不能太小了
#define MP_MEM_ALIGN 32
#define MP_MAX_BLOCK_FAIL_TIME 4
static inline void init_a_new_block(MP_POOL* pool, MP_BLOCK* newblock)
{
newblock->next = NULL;
newblock->ref_counter = 0;
newblock->failed_time = 0;
newblock->start_of_rest = (ADDR)newblock + sizeof(MP_BLOCK);
newblock->end_of_block = newblock->start_of_rest + pool->block_size;
}
MP_POOL* mp_create_pool(size_t size, int auto_clear)
{
if(size < MP_MIN_BLK_SZIE) return NULL;
MP_POOL* pool;
size_t block_size = size < MP_PAGE_SIZE ? size : MP_PAGE_SIZE;
size_t real_size = sizeof(MP_POOL) + sizeof(MP_BLOCK) + block_size;
int ret = posix_memalign((void**)&pool, MP_MEM_ALIGN, real_size);
if(ret)
{
log("[%d]posix_memalign error[%d].\n", __LINE__, errno);
return NULL;
}
pool->block_size = block_size;
pool->auto_clear = auto_clear;
pool->first_bucket = NULL;
pool->current_block = pool->first_block; // first_block是柔性数组,不需要赋值,实际已经指向正确的位置
init_a_new_block(pool, pool->first_block);
return pool;
}
申请内存
void* mp_malloc(MP_POOL* pool, size_t size)
{
if(size <