简而言之,反复地进行malloc和free不利于内存管理,同时容易产生内存碎片。复杂的代码中还容易出现内存泄漏问题。内存池则提前分配好大块内存作为备用,然后根据用户需求提供现成的小内存块给程序使用。相对来说使用内存池的效率更高一些,也不那么容易产生内存碎片,同时因为对内存块的集中管理,也可以很好地避免内存泄漏问题,即使出现了泄漏也很容易排查。
内存池以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;