内存池的原理
内存池是一种 预先分配和管理内存 的技术,主要用于 提高内存分配效率,减少内存碎片,并优化程序性能。
预先分配一大块内存(称为“池”),避免频繁调用 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
有什么还不明白的可以看这个大佬的文章,感谢大佬,希望跳转的朋友可以给参考大佬点点关注点点赞