文章目录
为什么需要内存池
简而言之,反复地进行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 <= 0 || pool == NULL) return NULL;
if(size <= pool->block_size) // block足矣,不需要使用 bucket
{
return malloc_a_piece(pool, size);
}
else // 需要使用 bucket
{
return malloc_a_bucket(pool, size);
}
}
1)申请一个piece
,如果没有可分配的block
就新增一个挂到block
链表尾部:
static ADDR malloc_a_piece(MP_POOL* pool, size_t size)
{
// 先尝试找到一个剩余空间足够的 block
MP_BLOCK* block = pool->current_block;
size_t real_piece_size = size + sizeof(MP_PIECE);
while(block)
{
if(rest_block_space(block) >= real_piece_size)
{ // 找到了
MP_PIECE* piece = (MP_PIECE*)block->start_of_rest;
block->start_of_rest += real_piece_size;
block->ref_counter++;
piece->block = block;
return (ADDR)piece->data;
}
// 对于申请失败的 block,增加其失败次数
block->failed_time++;
if(block->failed_time >= MP_MAX_BLOCK_FAIL_TIME)
pool->current_block = block->next; // 如果当前这个 block的失败次数太多了,就放弃这个 block,下一次从其后面开始遍历
block = block->next;
}
// 没找到,那就新建一个 block
block = malloc_a_block(pool);
if(block)
{
MP_PIECE* piece = (MP_PIECE*)block->start_of_rest;
block->start_of_rest += real_piece_size;
block->ref_counter++;
piece->block = block;
return (ADDR)piece->data;
}
else
return NULL;
}
2)申请一个新的block
:
static MP_BLOCK* malloc_a_block(MP_POOL* pool)
{
MP_BLOCK* newblock;
int ret = posix_memalign((void**)&newblock, MP_MEM_ALIGN, pool->block_size + sizeof(MP_BLOCK));
if(ret)
{
log("[%d]posix_memalign error[%d].\n", __LINE__, errno);
return NULL;
}
init_a_new_block(pool, newblock);
MP_BLOCK* block = pool->current_block; // 从 current_block 开始找链表尾部就行
while(block->next) block = block->next;
block->next = newblock; // 插入到链表末尾
return newblock;
}
3)申请一个bucket
,先找一下是否有之前已经释放的bucket
留下的描述符可以利用,有的话就直接使用这个bucket
描述符,没有的话就从block
中申请一个piece
来存描述符。
static ADDR malloc_a_bucket(MP_POOL* pool, size_t size)
{
// 在 block中先给 bucket 描述符找一块可用空间
MP_BUCKET* bucket = pool->first_bucket;
MP_BUCKET* prev_bucket = bucket;
while(bucket)
{
prev_bucket = bucket;
if(bucket->start_of_bucket == NULL)
break; // bucket 描述符是可以复用的,因此找到一个已经被释放掉的 bucket就可以直接用它的描述符
bucket = bucket->next;
}
if(bucket == NULL)
{ // 如果所有 bucket 描述符当前都在使用,那就再从 block中申请一个新的 bucket 描述符
bucket = (MP_BUCKET*)malloc_a_piece(pool, sizeof(MP_BUCKET));
if(bucket == NULL)
return NULL;
// 该 bucket所在的 block引用由 malloc_a_piece增加,此处不需要处理
bucket->still_in_use = 0;
if(prev_bucket) // 将新 bucket插到链表末尾
prev_bucket->next = bucket;
else // prev_bucket 为 NULL说明这是链表的第一个节点
pool->first_bucket = bucket;
}
else
{ // 如果是复用之前的 bucket 描述符,则将其所在的block 引用增加
MP_PIECE *piece = (MP_PIECE *)((ADDR)bucket - sizeof(MP_PIECE));
piece->block->ref_counter++;
}
int ret = posix_memalign((void**)&bucket->start_of_bucket, MP_MEM_ALIGN, size);
if(ret)
{
MP_PIECE *piece = (MP_PIECE *)((ADDR)bucket - sizeof(MP_PIECE));
piece->block->ref_counter--; // 出错则不能增加 block引用
log("[%d]posix_memalign error[%d].\n", __LINE__, errno);
return NULL;
}
bucket->still_in_use = 1;
return bucket->start_of_bucket;
}
释放内存
void mp_free(MP_POOL* pool, void* addr)
{
if(pool == NULL || addr == NULL)
return ;
MP_BLOCK* block = NULL;
// 先看一下这个地址是不是一个 bucket
MP_BUCKET* bucket = find_bucket_with_addr(pool, addr);
if(bucket) // 这个地址是 bucket
{
#ifdef MP_ALLWAYS_FREE_BUCKET // 每次清理 bucket时都将其独立内存空间释放,这里可能可以做优化
free(bucket->start_of_bucket);
bucket->start_of_bucket = NULL;
#endif
bucket->still_in_use = 0;
MP_PIECE *piece = (MP_PIECE *)((ADDR)bucket - sizeof(MP_PIECE));
block = piece->block;
}
else // 如果这个地址不是 bucket, 则什么都不需要处理, 找出它所在的 block就行了
{
MP_PIECE *piece = (MP_PIECE *)(addr - sizeof(MP_PIECE));
block = piece->block;
}
if(block)
block->ref_counter--; // 减少 block 引用计数
else
return ; // 这个地址没有对应的内存块
// 如果启用了自动清理内存池,则在适当的条件下执行清理操作
if(pool->auto_clear)
clear_block(pool, block);
}
此处我留了一个宏MP_ALLWAYS_FREE_BUCKET
来控制每次清理bucket
时是否将其独立内存空间释放,目前代码中该宏是开启的。这里之后似乎可以做一些优化,即:不将bucket
的独立内存空间释放掉,尝试用于后面新来的bucket
。
用户主动清理内存池
用户在确保内存池中的数据都可以丢弃时,可以发起主动清理内存池。
/* 用户主动发起清理操作 */
void mp_reset_pool(MP_POOL* pool)
{
if(pool == NULL)
return;
// 先确保释放掉所有 bucket 的空间
MP_BUCKET* bucket = pool->first_bucket;
while(bucket)
{
if(bucket->start_of_bucket)
{
free(bucket->start_of_bucket);
bucket->start_of_bucket = NULL;
}
bucket = bucket->next;
}
pool->first_bucket = NULL;
MP_BLOCK* block = pool->first_block;
while(block)
{
block->ref_counter = 0;
block->failed_time = 0;
block->start_of_rest = (ADDR)block + sizeof(MP_BLOCK);
block = block->next;
}
pool->current_block = pool->first_block;
}
销毁内存池
void mp_destroy_pool(MP_POOL* pool)
{
mp_reset_pool(pool);
MP_BLOCK* block = pool->first_block->next; // 从第二个 block 开始释放内存! 第一个比较特殊
MP_BLOCK* prev_block = block;
while (block)
{
prev_block = block;
block = block->next;
free(prev_block);
}
free(pool); // 释放池的描述符空间以及第一个初始 block
}
block的自动清理
内存池自动清理block
的前提是当前block
的引用计数降为0。除了将block
自身的状态复位,在这之前还应该找出属于这个block
的所有bucket
确保其独立内存释放掉了。其实清理的逻辑很简单,这段代码看起来长只是因为增加了对bucket
链表的调整操作。
static void clear_block(MP_POOL* pool, MP_BLOCK* block)
{
if(block->ref_counter > 0)
return;
// 清空 block前先确保其中的 bucket内存都释放了
MP_BUCKET* bucket = pool->first_bucket;
MP_BUCKET* prev_bucket = bucket;
while(bucket)
{
MP_PIECE *piece = (MP_PIECE *)((ADDR)bucket - sizeof(MP_PIECE));
if(piece->block == block) // 找出属于这个 block的 bucket确保其独立内存释放掉
{
if(bucket->start_of_bucket)
{
free(bucket->start_of_bucket);
bucket->start_of_bucket = NULL;
}
// 删除 bucket 要注意相应的调整 bucket 链表
if(bucket == pool->first_bucket)
{
pool->first_bucket = bucket->next;
prev_bucket = pool->first_bucket;
}
else
{
prev_bucket->next = bucket->next;
}
}
else
{
prev_bucket = bucket;
}
bucket = bucket->next;
}
// 恢复 block 参数
block->ref_counter = 0;
block->failed_time = 0;
block->start_of_rest = (ADDR)block + sizeof(MP_BLOCK);
pool->current_block = pool->first_block; // 一定要将 current_block 也复位,否则可能导致后面一直不会用到这个 block
}
内存池使用情况统计
/* 输出内存池统计信息 */
void mp_pool_statistic(MP_POOL* pool)
{
if(pool == NULL) return;
int bnum = 0;
int currnum = 0;
MP_BLOCK* block = pool->first_block;
while(block)
{
bnum++;
if(block == pool->current_block)
currnum = bnum;
block = block->next;
}
printf("# block size: %lu\n", pool->block_size);
printf("# block(s) num: %d\n", bnum);
printf("# block current: %d\n", currnum);
block = pool->first_block;
bnum = 0;
while(block)
{
bnum++;
printf("\n#### block %d", bnum);
if(block == pool->current_block)
printf(" *\n");
else
printf("\n");
printf("space used: %ld\n", block->start_of_rest - ((ADDR)block + sizeof(MP_BLOCK)));
printf("space free: %lu\n", rest_block_space(block));
int bucket_in_this_block = 0;
int bucket_in_this_block_in_use = 0;
MP_BUCKET* bucket = pool->first_bucket;
while(bucket)
{
MP_PIECE *piece = (MP_PIECE *)((ADDR)bucket - sizeof(MP_PIECE));
if(piece->block == block)
{
bucket_in_this_block++;
if(bucket->still_in_use)
bucket_in_this_block_in_use++;
}
bucket = bucket->next;
}
printf("buckets in the block: %d\n", bucket_in_this_block);
printf("buckets in use: %d\n", bucket_in_this_block_in_use);
printf("reference remain: %d\n", block->ref_counter);
printf("failed time: %d\n", block->failed_time);
block = block->next;
}
}
测试程序
#include "MemPool.h"
#include <stdio.h>
int main()
{
MP_POOL* pool = mp_create_pool(MP_PAGE_SIZE, 1);
if(pool == NULL)
{
printf("mp_create_pool failed.\n");
return -1;
}
int i;
/* 测试申请和释放 piece */
printf("------------ piece malloc test --------------\n");
char* mem[10];
for(i = 0; i < 10; i++)
mem[i] = (char*)mp_malloc(pool, 512);
mp_pool_statistic(pool);
printf("------------ piece free test 1 --------------\n");
for(i = 0; i < 5; i++)
mp_free(pool, mem[i]);
mp_pool_statistic(pool);
printf("------------ piece free test 2 --------------\n");
for(; i < 10; i++)
mp_free(pool, mem[i]);
mp_pool_statistic(pool);
/* 测试申请和释放 bucket */
printf("------------ bucket malloc test --------------\n");
for(i = 0; i < 4; i++)
mem[i] = (char*)mp_malloc(pool, 8192);
mp_pool_statistic(pool);
printf("------------ bucket free test 1 --------------\n");
for(i = 0; i < 2; i++)
mp_free(pool, mem[i]);
mp_pool_statistic(pool);
printf("------------ bucket free test 2 --------------\n");
for(; i < 4; i++)
mp_free(pool, mem[i]);
mp_pool_statistic(pool);
mp_destroy_pool(pool);
return 0;
}
输出打印:
------------ piece malloc test --------------
# block size: 4096
# block(s) num: 2 # 当前两个 block
# block current: 1
#### block 1 *
space used: 3640
space free: 456
buckets in the block: 0
buckets in use: 0
reference remain: 7 # <-----申请了10个 piece,7个在block 1, 3个在block 2
failed time: 3
#### block 2
space used: 1560
space free: 2536
buckets in the block: 0
buckets in use: 0
reference remain: 3
failed time: 0
------------ piece free test 1 --------------
# block size: 4096
# block(s) num: 2
# block current: 1
#### block 1 *
space used: 3640
space free: 456
buckets in the block: 0
buckets in use: 0
reference remain: 2 # <-----释放前5个,block 1 的引用还有2个
failed time: 3
#### block 2
space used: 1560
space free: 2536
buckets in the block: 0
buckets in use: 0
reference remain: 3
failed time: 0
------------ piece free test 2 --------------
# block size: 4096
# block(s) num: 2
# block current: 1
#### block 1 * # 全部内存都归还给内存池了
space used: 0
space free: 4096
buckets in the block: 0
buckets in use: 0
reference remain: 0
failed time: 0
#### block 2
space used: 0
space free: 4096
buckets in the block: 0
buckets in use: 0
reference remain: 0
failed time: 0
------------ bucket malloc test --------------
# block size: 4096
# block(s) num: 2
# block current: 1
#### block 1 *
space used: 128 # <-----bucket 描述符占用 block
space free: 3968
buckets in the block: 4
buckets in use: 4 # <-----当前 block 中4个 bucket
reference remain: 4
failed time: 0
#### block 2
space used: 0
space free: 4096
buckets in the block: 0
buckets in use: 0
reference remain: 0
failed time: 0
------------ bucket free test 1 --------------
# block size: 4096
# block(s) num: 2
# block current: 1
#### block 1 *
space used: 128
space free: 3968
buckets in the block: 4 # <-----bucket 描述符并没有真正释放,可以下次复用
buckets in use: 2
reference remain: 2 # <-----通过引用计数可知实际还有两个 bucket的空间在使用
failed time: 0
#### block 2
space used: 0
space free: 4096
buckets in the block: 0
buckets in use: 0
reference remain: 0
failed time: 0
------------ bucket free test 2 --------------
# block size: 4096
# block(s) num: 2
# block current: 1
#### block 1 * # 全部内存都归还给内存池了
space used: 0
space free: 4096
buckets in the block: 0
buckets in use: 0
reference remain: 0
failed time: 0
#### block 2
space used: 0
space free: 4096
buckets in the block: 0
buckets in use: 0
reference remain: 0
failed time: 0
完整代码已上传github仓库。
补充: 内存泄漏的排查
- 先判断内存池是否有内存泄漏,一般通过打印的形式可以查出
- 如果没有使用内存池,则引入tcmalloc/jemalloc进行检查
- 如果不是内存池的内存泄漏则看是否是第三方库产生的内存泄漏