15、内存池的初步设计:为什么需要内存池、简单的固定大小内存池实现
为什么我们需要内存池?
先说说我自己的经历。刚入行那会儿,我写一个网络服务器,频繁地 malloc/free 小内存块。程序跑起来倒是能工作,但一上压力测试,性能就惨不忍睹。更头疼的是,跑了几个小时之后,内存碎片化严重,明明还有几百兆空闲,却分配不出一个 64 字节的小块。
为什么会这样?说白了,标准库的 malloc 是个通用方案。它要应对各种大小的分配请求,还要兼顾多线程安全。这种「万能」的代价就是:每次分配都要查空闲链表、可能触发系统调用、还要处理内存对齐。对于频繁分配固定大小对象的场景,这就像用牛刀杀鸡。
内存池就是专门解决这个问题的。它的核心思想很简单:提前申请一大块内存,然后自己管理其中的小块分配。你想想看,如果每次分配的大小都一样,那管理起来就太容易了——根本不需要复杂的空闲链表,一个简单的链表就够用。
- 分配速度极快——通常只需要几条指令,比 malloc 快一个数量级
- 不会产生内存碎片——所有块大小一致,释放后直接复用
- 内存局部性好——连续分配的对象在物理上也是连续的,缓存友好
- 便于统计和调试——可以轻松追踪内存使用情况
固定大小内存池的设计思路
固定大小内存池,顾名思义,就是管理固定大小内存块的池子。它的设计其实很直观:
首先,我们预分配一大块连续内存,然后把它切成若干个大小相等的块。每个块要么是空闲的,要么已被分配。空闲块通过一个链表串起来,这个链表就叫做「空闲链表」。
分配时,从空闲链表头部取一个块返回。释放时,把块重新挂回空闲链表头部。就这么简单。
嗯,这里要注意一个细节:空闲链表怎么存?我们不需要额外的内存来存储链表指针。因为空闲块本身是未使用的,我们可以在空闲块内部存储下一个空闲块的地址。这就是所谓的「隐式空闲链表」——链表指针就藏在数据区里。
代码实现:一个简单的固定大小内存池
下面是我写的一个最小实现。它管理固定 64 字节的块,适合用来存储网络包、小结构体等。
#include <stdlib.h>
#include <stdint.h>
// 内存池结构
typedef struct mempool {
void *pool; // 预分配的内存块起始地址
void *free_list; // 空闲链表头指针
size_t block_size; // 每个块的大小(字节)
size_t block_count; // 块的总数
} mempool_t;
// 初始化内存池
// block_size: 每个块的大小
// block_count: 块的数量
mempool_t* mempool_create(size_t block_size, size_t block_count) {
mempool_t *mp = (mempool_t*)malloc(sizeof(mempool_t));
if (!mp) return NULL;
// 分配一整块内存
mp->pool = malloc(block_size * block_count);
if (!mp->pool) {
free(mp);
return NULL;
}
mp->block_size = block_size;
mp->block_count = block_count;
mp->free_list = mp->pool;
// 初始化空闲链表:每个空闲块的前8字节存放下一个空闲块的地址
// 注意:这里假设 block_size >= sizeof(void*)
char *ptr = (char*)mp->pool;
for (size_t i = 0; i < block_count - 1; i++) {
void **next = (void**)(ptr + i * block_size);
*next = (void*)(ptr + (i + 1) * block_size);
}
// 最后一个块指向 NULL
void **last = (void**)(ptr + (block_count - 1) * block_size);
*last = NULL;
return mp;
}
// 从内存池分配一个块
void* mempool_alloc(mempool_t *mp) {
if (!mp->free_list) return NULL; // 没有空闲块了
void *block = mp->free_list;
// 空闲链表头指向下一个空闲块
mp->free_list = *(void**)block;
return block;
}
// 释放一个块回内存池
void mempool_free(mempool_t *mp, void *block) {
// 把块插回空闲链表头部
*(void**)block = mp->free_list;
mp->free_list = block;
}
// 销毁内存池
void mempool_destroy(mempool_t *mp) {
if (mp) {
free(mp->pool);
free(mp);
}
}
核心逻辑流程图
下面这张图展示了内存池的完整工作流程,从初始化到分配和释放:
使用示例
来看看怎么用这个内存池:
// 创建一个管理 64 字节块、共 1024 个块的内存池
mempool_t *mp = mempool_create(64, 1024);
if (!mp) {
fprintf(stderr, "内存池创建失败\n");
return -1;
}
// 分配一个块
void *block = mempool_alloc(mp);
if (block) {
// 使用这个块...
memset(block, 0, 64);
// 用完后释放
mempool_free(mp, block);
}
// 销毁内存池
mempool_destroy(mp);
这个设计的局限性
当然,这个简单的内存池也有它的短板:
- 只能管理固定大小——如果你需要不同大小的块,就得另想办法
- 内存利用率不高——如果块大小设置得偏大,内部碎片就多
- 不支持动态扩容——块用完了就真的用完了,没法自动扩展
- 不是线程安全的——多线程环境下需要加锁
不过话说回来,这个设计虽然简单,但已经能解决 80% 的固定大小内存分配问题。我在嵌入式开发中经常用它来管理任务控制块、消息队列等固定结构体,效果非常好。
如果你需要更灵活的内存池——比如支持不同大小、支持动态扩容、支持线程安全——那就要在这个基础上做扩展了。但核心思想是一样的:预分配、自己管、避免碎片。
- 内存池的核心是「预分配 + 空闲链表」
- 固定大小内存池实现简单,性能极高
- 适合频繁分配/释放固定大小对象的场景
- 理解了这个基础,后面学习更复杂的内存池就容易多了
公众号:蓝海资料掘金营,微信 deep3321