手写一个内存池-页内分配
内存管理,管理的是虚拟内存(又包括堆和共享内存)。
在使用堆内存的时候,会发生频繁的、大小不一的空间释放和申请动作,而这些大小不一的空间如果直接在堆上释放申请,就会产生很多小的空闲块,这些小的块难以被利用,使得堆明明还有空间,但却无法再分配到内存。
以上问题的一种解决方式即是内存池,在先在堆上申请一个较大的内存块(把它称为页page,这个页就是一个内存池),然后再对内存块内进行固定的大小分配(不足这个固定大小的,也按这个固定大小分配,分配出来的内存称块,这个固定大小叫block_size)。对不同大小的对象,在不同的内存块上申请内存。避免小的块的产生。
示例:
/*** memory_pool_simple.hpp ***/
#pragma once
class mempool_s {
public:int block_size;// 指块大小 如在一个4k的内存池上,固定每次分配一个32byte的块int free_count;// 指当前空闲块数目void* mem;// 指向内存池的初始位置void* ptr;// 内存池的空闲块组成空闲链表,该指针指向空闲链表第一个空闲块,每次分配会将该指针所指空闲块取下用于分配
}mempool_t;void test_memory_pool();
#include"memory_pool_simple.hpp"
#include<iostream>
#include<cstdlib>
#include<string>void* _malloc(mempool_t *mp, size_t size) {// 若线程池为空,线程池无空闲块,或申请内存大于一个空闲块的大小,则返回NULLif (!mp || mp->free_count <=0 || size > mp->block_size) return NULL;void* ptr = mp->ptr;// 空闲块的第一个字节记录着下一个空闲块的位置,因此将该字节提取出来作为空闲链表的队首mp->ptr = *(char**)ptr;mp->free_count--;std::cout << "malloc\n";return ptr;
}void _free(mempool_t *mp, void* ptr) {if (!mp) return;// 将空闲链表的第一个块(ptr所指的块)的地址存入当前释放块的第一个字节。并使用头插法将当前释放的块放到空闲链表的首位*(char**)ptr = static_cast<char*>(mp->ptr);mp->ptr = ptr;mp->free_count++;std::cout << "free\n";std::cout << "mp->ptr:" << mp->ptr << "\n";}// mempool_s* mp = nullptr;// 兼容
// 初始化内存池
int memInit(mempool_t* mp, size_t block_size) {// 如果mp页为空,返回-1if (!mp) return -1;// 将mp所指内存清空置零,分配的块由于频繁调用,每次分配出来的并不一定是崭新的块,所以对于堆上分配的内存一定要记得清空。// memset(mp, 0, sizeof(mempool_t));mp->block_size = block_size;mp->free_count = MEM_PAGE_SIZE / block_size;mp->mem = malloc(MEM_PAGE_SIZE);if (!mp->mem) return -1;mp->ptr = mp->mem;char* q = static_cast<char*>(mp->ptr);for (int i = 0; i < mp->free_count; i++) {// q转化为char指针的指针,并对这个指针的指针解引用,结果是*q为char的指针,该指针指向下一个块的指针域。即q中存的是指向下一个指针域的指针*(char**)q = q + block_size;// 将q后移,继续对之后的指针域执行,使它们指向下一个指针域q += block_size;}*(char**)q = nullptr;// 最后一个空闲块的指针域指空
}// 在预编译阶段,将程序中的malloc函数语句用自定义的 _malloc 函数替换,free函数语句用自定义的 _free 函数替换。
#define malloc(mp, size) _malloc(mp, size)
#define free(mp, ptr) _free(mp, ptr)void test_memory_pool() {mempool_t mempool;mempool_t* mp = &mempool;memInit(mp, 32);void* p1 = malloc(mp, 5);std::cout << "p1:" << p1 << "\n";void* p2 = malloc(mp, 10);std::cout << "p2:" << p2 << "\n";void* p3 = malloc(mp, 15);std::cout << "p3:" << p3 << "\n";void* p4 = malloc(mp, 20);std::cout << "p4:" << p4 << "\n";free(mp, p2);// p2释放,并将p2所占内存块回收到内存池的空闲链表表头free(mp, p4);// p4释放,并将p2所占内存块回收到内存池的空闲链表表头void* p5 = malloc(mp, 20);// 将空闲链表第一个空闲块分配给p5,即之前p4释放的块分配给p5std::cout << "p5:" << p5 << "\n";void* p6 = malloc(mp, 20);std::cout << "p6:" << p6 << "\n";// 将空闲链表第一个空闲块分配给p5,即之前p2释放的块分配给p5free(mp, p1);free(mp, p3);free(mp, p5);free(mp, p6);}
执行结果:
无论是申请5个字节内存大小的空间,还是申请20个字节,指针间隔都是32个字节,说明都分配一个块大小32byte。而释放后再申请内存,会从最后一个释放的块开始分配内存,这说明程序按照我们所希望的那样将释放的块插入到空闲队列队首。