[C/C++后端开发学习]15 简单内存池实现

本文介绍了内存池的概念和必要性,对比了不同的内存池设计策略,并详细讲解了一个简单的4k内存池实现,包括内存池结构、空洞利用、内存分配与释放,以及内存池的管理接口和内存泄漏排查。通过内存池可以提高内存管理效率,减少碎片,便于内存泄漏排查。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为什么需要内存池

简而言之,反复地进行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进行检查
  • 如果不是内存池的内存泄漏则看是否是第三方库产生的内存泄漏
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值