C++实现简易内存池(详细版)

内存池的原理

内存池是一种 预先分配和管理内存 的技术,主要用于 提高内存分配效率,减少内存碎片,并优化程序性能。

预先分配一大块内存(称为“池”),避免频繁调用 malloc/new 和 free/delete。

自主管理内存分配和释放,减少系统调用的开销。

减少内存碎片,提高内存利用率。

本贴主要介绍可变长内存池的设计及实现

小内存分配

当用户申请的内存是小内存时(这里的小内存指的是小于small_buffer_capacity),检查small_block组成的链表,图示的是一开始初始化memory_pool的样子,如果余下内存空间足够放得下用户申请的内存(可以理解为图中绿色部分的内存量大于申请内存量)那么就将small_block的可用指针移动。(这里不理解数据结构中各个变量的作用,先不着急,接下来会详细阐述,这一部分请尽量体会内存池的运行逻辑,加油)。

大内存分配

当用户的内存过大(大于small_buffer_capacity时)就需要使用big_block来管理这些内存块,在内存池看来,各个数据结构是需要占据小内存的,需要当作申请小内存来处理,而申请的大内存就直接开辟一个大内存即可,形成一个大内存链来管理。

扩容

其实聪明的你应该会有疑问,当我申请小内存时,我原先预先开辟的内存块不够用了怎么办?那么,我们就需要动态扩容,同样再开辟一个内存块,将新的small_block来管理这个内存块即可。

好啦,你已经了解了内存池的运行模式啦,那就动手设计吧

内存池各个数据结构设计

由于nignix中内存池是纯C语言书写的,这里我们用C++进行改写,那么我们就需要预料到,内存池类是不是在运行过程中只保留一份,不必存在多个对象,那么其内部的成员函数怎么办?该如何设计呢?是不是很熟悉,对!就是单例模式!我们只需要把成员函数static一下,那么它们就独立于内存池类了,并且只会保留一份,不必占用过多内存,什么?不知道设计模式,害,等我更新。

内存池类结构
class memory_pool {
public:
    size_t small_buffer_capacity;//小块大小,作为初始数据
    small_block* cur_usable_small_block;//当前可用小块
    big_block* big_block_start;//大块头
    small_block small_block_start[0];//小块头
    static memory_pool* createPool(size_t capacity);//创建内存池
    static void destroyPool(memory_pool* pool);//销毁内存池
    static char* createNewSmallBlock(memory_pool* pool, size_t size);//创建小块
    static char* mallocBigBlock(memory_pool* pool, size_t size);//创建大块
    static void* poolMalloc(memory_pool* pool, size_t size);//申请块api
    static void freeBigBlock(memory_pool* pool, char* buffer_ptr);//free函数
};
管理小内存的结构
class small_block {
public:
    char* cur_usable_buffer;//当前可以使用的小块地址
    char* buffer_end;//地址范围的末尾
    small_block* next_block;//下一个可以使用的块
    int no_enough_times;//寻找失败的次数
};
管理大内存的结构
class big_block {
public:
    char* big_buffer;//大块地址
    big_block* next_block;
};

接下来就需要解释一下接口函数的作用及工作过程

函数解释

createPool
memory_pool* memory_pool::createPool(size_t capacity) {
    //根据上面的图示可以看到初始化内存池的时候,需要预先申请一片内存
    //预先申请的内存其中包含内存池类+small_block类然后才是可以使用的大小
    size_t total_size = sizeof(memory_pool) + sizeof(small_block) + capacity;
    void* temp = malloc(total_size);
    memset(temp, 0, total_size);
    //初始化pool
    memory_pool* pool = (memory_pool*)temp;
    fprintf(stdout, "pool address:%p\n", pool);
    //-此时temp是pool的指针,先来初始化pool对象
    pool->small_buffer_capacity = capacity;
    pool->big_block_start = nullptr;
    pool->cur_usable_small_block = (small_block*)(pool->small_block_start);

    //-pool+1的1是整个memory_pool的步长,别弄错了。此时sbp是small_block的指针
    small_block* sbp = (small_block*)(pool + 1);
    fprintf(stdout, "first small block address:%p\n", sbp);
    //-初始化small_block对象
    sbp->cur_usable_buffer = (char*)(sbp + 1);
    fprintf(stdout, "first small block buffer address:%p\n", sbp->cur_usable_buffer);
    sbp->buffer_end = sbp->cur_usable_buffer + capacity;//-第一个可用的buffer就是开头,所以end=开头+capacity
    sbp->next_block = nullptr;
    sbp->no_enough_times = 0;

    return pool;
};

这个函数显而易见是初始化内存池的函数,对内存池的初始化主要包括开辟内存空间,初始化memory_pool和small_block两个类,然后就是返回。

destroyPool
//-销毁内存池
void memory_pool::destroyPool(memory_pool* pool) {
    //-销毁大内存
    //我们知道大内存的数据结构就是一个链表,因此需要while遍历该链表一个一个进行free
    big_block* bbp = pool->big_block_start;
    while (bbp) {
        if (bbp->big_buffer) {
            free(bbp->big_buffer);
            bbp->big_buffer = nullptr;
        }
        bbp = bbp->next_block;
    }
    //-为什么不删除big_block节点?因为big_block在小内存池中,等会就和小内存池一起销毁了

    //-销毁小内存
    //销毁小内存的过程也是因此它本质上是一个链表
    small_block* temp = pool->small_block_start->next_block;
    while (temp) {
        small_block* next = temp->next_block;
        free(temp);
        temp = next;
    }
    free(pool);
}

该函数负责进行销毁内存池,内存池由内存池类和小内存块链表和大内存块链表组成,因此就需要分成两部分进行释放内存。

createNewSmallBlock
char* memory_pool::createNewSmallBlock(memory_pool* pool, size_t size) {
    //-先创建新的small block,注意还有buffer
    size_t malloc_size = sizeof(small_block) + pool->small_buffer_capacity;
    void* temp = malloc(malloc_size);
    memset(temp, 0, malloc_size);

    //-初始化新的small block
    small_block* sbp = (small_block*)temp;
    fprintf(stdout, "new small block address:%p\n", sbp);
    sbp->cur_usable_buffer = (char*)(sbp + 1);//-跨越一个small_block的步长
    fprintf(stdout, "new small block buffer address:%p\n", sbp->cur_usable_buffer);
    sbp->buffer_end = (char*)temp + malloc_size;
    sbp->next_block = nullptr;
    sbp->no_enough_times = 0;
    //-预留size空间给新分配的内存
    char* res = sbp->cur_usable_buffer;//-存个副本作为返回值
    sbp->cur_usable_buffer = res + size;

    //-因为目前的所有small_block都没有足够的空间了。
    //-意味着可能需要更新内存池的cur_usable_small_block,也就是寻找的起点
    small_block* p = pool->cur_usable_small_block;
    while (p->next_block) {
        if (p->no_enough_times > 4) {
            pool->cur_usable_small_block = p->next_block;
        }
        ++(p->no_enough_times);
        p = p->next_block;
    }

    //-此时p正好指向当前pool中最后一个small_block,将新节点接上去。
    p->next_block = sbp;

    //-因为最后一个block有可能no_enough_times>4导致cur_usable_small_block更新成nullptr
    //-所以还要判断一下
    if (pool->cur_usable_small_block == nullptr) {
        pool->cur_usable_small_block = sbp;
    }
    return res;//-返回新分配内存的首地址
}

该函数是当扩容的时候需要调用的。忘记了扩容的工作流程的上滑即可。因为扩容现需要开辟新的内存块和新的管理内存块的类。所以,这个函数就是负责这两个模块。先开辟新的内存块,然后初始化新的small_block,注意,这里的内存块就不是包含内存池类+small_block类然后才是可以使用的大小。这里不包含内存池类了,仅包含small_block类然后才是可以使用的大小。当然是不是注意到no_enough_times还没用到,这里就用到了!(超级大声),由于需要扩容了,说明之前的内存块不够用了,或者说失败了,那么之前各个small_block中的no_enough_times就得++,如果大于4(这是为了效率考虑,避免老是扫描失败的内存块)就更新pool中的cur_usable_small_block。最后,别忘了把新的内存块插入内存池中。

mallocBigBlock
char* memory_pool::mallocBigBlock(memory_pool* pool, size_t size) {
    //-先分配size大小的空间
    void* temp = malloc(size);
    memset(temp, 0, size);

    //-从big_block_start开始寻找,注意big block是一个栈式链,插入新元素是插入到头结点的位置。
    big_block* bbp = pool->big_block_start;
    int i = 0;
    while (bbp) {
        if (bbp->big_buffer == nullptr) {
            bbp->big_buffer = (char*)temp;
            return bbp->big_buffer;
        }
        if (i > 3) {
            break;//-为了保证效率,如果找三轮还没找到有空buffer的big_block,就直接建立新的big_block
        }
        bbp = bbp->next_block;
        ++i;
    }

    //-创建新的big_block,这里比较难懂的点,就是Nginx觉得big_block的buffer虽然是一个随机地址的大内存
    //-但是big_block本身算一个小内存,那就不应该还是用随机地址,应该保存在内存池内部的空间。
    //-所以这里有个套娃的内存池malloc操作
    big_block* new_bbp = (big_block*)memory_pool::poolMalloc(pool, sizeof(big_block));
    //-初始化
    new_bbp->big_buffer = (char*)temp;
    new_bbp->next_block = pool->big_block_start;
    pool->big_block_start = new_bbp;

    //-返回分配内存的首地址
    return new_bbp->big_buffer;
}

该函数用来开辟大内存,那么就粗暴地先malloc需要的大小,然后就分先看看有没有空的big_block。有的话就更新big_block即可,没有就得新开辟新的big_block。注意,这里可能有同志不太理解为什么我要开辟big_block引用poolMalloc,因为在内存池中各个管理结构就是小内存,想要开辟小内存,就是和申请小内存同理。

poolMalloc
void* memory_pool::poolMalloc(memory_pool* pool, size_t size) {
    //-先判断要malloc的是大内存还是小内存
    if (size < pool->small_buffer_capacity) {//-如果是小内存
        //-从cur small block开始寻找
        small_block* temp = pool->cur_usable_small_block;
        do {
            //-判断当前small block的buffer够不够分配
            //-如果够分配,直接返回
            if (temp->buffer_end - temp->cur_usable_buffer > size) {
                char* res = temp->cur_usable_buffer;
                temp->cur_usable_buffer = temp->cur_usable_buffer + size;
                return res;
            }
            temp = temp->next_block;
        } while (temp);
        //-如果最后一个small block都不够分配,则创建新的small block;
        //-该small block在创建后,直接预先分配size大小的空间,所以返回即可.
        return createNewSmallBlock(pool, size);
    }
    //-分配大内存
    return mallocBigBlock(pool, size);
}

这是暴露给用户的api,因为其中的mallocBigBlock和createNewSmallBlock是我们秃头程序员需要考虑的,而这个仅来调用前两个就好,那么什么情况需要区分呢?就是我们申请小内存时,分为内存够用和扩容,我们申请大内存时,开辟大内存这三种情况。

freeBigBlock
void memory_pool::freeBigBlock(memory_pool* pool, char* buffer_ptr) {
    big_block* bbp = pool->big_block_start;
    while (bbp) {
        if (bbp->big_buffer == buffer_ptr) {
            free(bbp->big_buffer);
            bbp->big_buffer = nullptr;
            return;
        }
        bbp = bbp->next_block;
    }
}

C语言中free函数的 实现

void mp_free(mp_pool_s* pool, void* p) {
    mp_large_s* large = NULL;
    for (large = pool->large_head; large; large = large->next) {
        if (p == large) {
            free(large->addr_large);
            large->size = 0;
            large->addr_large = NULL;
            return;
        }
    }
    //mp_node_s释放node
    mp_node_s* node = pool->head;
    for (; node; node->next) {
        if ((unsigned char*)p >= (unsigned char*)node && (unsigned char*)p <= (unsigned char*)node->end) {
            node->quote--;
            if (node->quote == 0) {
                if (node == pool->head) {
                    node->last = (unsigned char*)pool + sizeof(mp_pool_s) + sizeof(mp_node_s);
                }
                else {
                    node->last = (unsigned char*)node + sizeof(mp_node_s);
                }
                node->failed = 0;
                pool->cur = pool->head;
            }
            return;
        }
    }
}

可以对比看到,我这仅考虑了大内存的释放,小内存的释放我也没整明白。

到这里就基本结束了,打字这么久累死了,不过你们和我一起回顾了这个内存池应该也是收获满满。加油,很棒了。

测试一下吧
int main() {
    memory_pool* pool = memory_pool::createPool(1024);
    //-分配小内存
    char* p1 = (char*)memory_pool::poolMalloc(pool, 2);
    fprintf(stdout, "little malloc1:%p\n", p1);
    char* p2 = (char*)memory_pool::poolMalloc(pool, 4);
    fprintf(stdout, "little malloc2:%p\n", p2);
    char* p3 = (char*)memory_pool::poolMalloc(pool, 8);
    fprintf(stdout, "little malloc3:%p\n", p3);
    char* p4 = (char*)memory_pool::poolMalloc(pool, 256);
    fprintf(stdout, "little malloc4:%p\n", p4);
    char* p5 = (char*)memory_pool::poolMalloc(pool, 512);
    fprintf(stdout, "little malloc5:%p\n", p5);

    //-测试分配不足开辟新的small block
    char* p6 = (char*)memory_pool::poolMalloc(pool, 512);
    fprintf(stdout, "little malloc6:%p\n", p6);

    //-测试分配大内存
    char* p7 = (char*)memory_pool::poolMalloc(pool, 2048);
    fprintf(stdout, "big malloc1:%p\n", p7);

    char* p8 = (char*)memory_pool::poolMalloc(pool, 4096);
    fprintf(stdout, "big malloc2:%p\n", p8);

    //-测试free大内存
    memory_pool::freeBigBlock(pool, p8);

    //-测试再次分配大内存(我这里测试结果和p8一样)
    char* p9 = (char*)memory_pool::poolMalloc(pool, 2048);
    fprintf(stdout, "big malloc3:%p\n", p9);

    //-销毁内存池
    memory_pool::destroyPool(pool);

    exit(EXIT_SUCCESS);
}

完整代码

骗你的!上面的代码一个一个复制会不会!问你呢!?我已经燃尽了,你就动动小手复制吧。

参考大佬

https://zhuanlan.zhihu.com/p/631952956

有什么还不明白的可以看这个大佬的文章,感谢大佬,希望跳转的朋友可以给参考大佬点点关注点点赞

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值