1. 为什么要内存池?
1.1 一个很常见的场景
想象一下,你的程序运行了很长时间 (比如服务器进程),过程中不断 malloc 和 free:
malloc(32) → free()
malloc(64) → free()
malloc(128) → free()
malloc(32) → free()
malloc(16) → free()
...
每次分配和释放的大小不一,释放后的空间又可能被下次不同大小的请求填上,大量不连续的空闲小块 就产生了------这就是内存碎片。
1.2 内存碎片会导致什么?
- 随机 coredump:进程明明还有大量空闲内存,但分配请求却失败了(找不到连续空间)
- 内存利用率下降:明明还有 200 bytes 的空闲空间,但请求 100 bytes 的连续空间却失败
- 性能下降:碎片化越严重,内存分配器越难找到合适的块
1.3 业界通用方案 vs 定制方案
| 方案 | 代表 | 特点 |
|---|---|---|
| 通用内存分配器 | jemalloc、tcmalloc | 通用、成熟,但不一定贴合你的业务 |
| 自研内存池 | 业务定制 | 针对业务场景深度优化,性能更高 |
通用方案不是万能的,举个例子:
- 游戏服务器按 16/32/64 字节分块,通用分配器可能做不到这么精细
- 高频交易系统要求极低延迟,自研内存池可以省掉锁和查找开销
2. 虚拟内存布局 --- 理解堆管理
在聊内存池之前,先搞清楚程序运行时的内存布局:
+------------------+ 0xFFFFFFFF (4GB)
| 内核空间 | ← 操作系统内核用,进程不能直接访问
+------------------+
| 栈 | ← 向低地址增长,函数局部变量放这里
| ↓ |
| ... |
| ↑ |
| 堆 | ← 向高地址增长,malloc/free 在这里管理
| BSS 段 | ← 未初始化的全局变量
| Data 段 | ← 已初始化的全局变量
| Code 段 | ← 程序代码
+------------------+ 0x00000000
堆 是 malloc / free 管理的区域。每次调用 malloc,向内核申请内存;调用 free,把内存归还堆。
Linux 中,每个进程有一个 task_struct(进程描述符),其中的 mm_struct 管理内存:
c
struct mm_struct {
unsigned long start_code, end_code; // 代码段
unsigned long start_data, end_data; // 数据段
unsigned long start_brk, brk; // 堆
unsigned long start_stack; // 栈
...
};
3. 内存池的核心思想
3.1 什么是内存池?
预先申请一大块内存,后续分配都从这一大块里切,不用每次都找内核要。
不用内存池:
内核 ← malloc(32) ← malloc(64) ← malloc(128) (每次都找内核)
内核 ← free() ← free() ← free() (碎片散落)
用内存池:
内核 ← 一次申请 4096 字节的大块
内存池内部 ← 切成 32 字节的小块,按需分配
(碎片被"池"兜住了)
3.2 定长 vs 不定长
| 类型 | 说明 | 适用场景 |
|---|---|---|
| 定长内存池 | 每块大小固定(如 32 字节),分配简单 | 对象池、连接池 |
| 不定长内存池 | 块大小不固定,更灵活但实现复杂 | 通用场景 |
本文聚焦定长内存池,因为实现更清晰、也更常见。
4. 内存池结构设计
4.1 整体架构
mp_pool_s(内存池)
├── max :每块的大小上限(如 4096)
├── head :mp_node_s 链表头(管理所有块)
│ ├── last :当前可用位置指针
│ ├── end :块末尾指针
│ └── next :指向下一个节点
└── large :mp_large_s 链表头(管理超大块 > max)
├── alloc :实际分配的内存指针
└── next :指向下一个大块
4.2 为什么要分"小块"和"大块"?
max是阈值:请求 ≤max的走小块池;请求 >max的走大块直接malloc- 原因:太大的请求不适合切成小块放池子里,浪费空间
4.3 最小内存块设计
我们把内存池的基本单元设计成这样:
c
struct mempool_s {
size_t blocksize; // 每一块的大小(如 32)
int freecount; // 还有多少块可以用
unsigned char *free_ptr; // 下次分配的起始位置
void *mem; // 整块内存
};
关键问题:释放完的块如何再次分配?
两种策略:
- 空闲链表:每个块头部存一个指针,指向下一个空闲块 → O(1) 分配
- 位置标记:从头开始扫描,找到第一个够大的空闲块 → 简单但 O(n)
我们这里用的是从头分配 (free_ptr 不断后移),简单直接。
5. 代码实现详解
5.1 数据结构
c
// 小块节点(链表中每个节点是一整块内存)
typedef struct mp_node_s {
unsigned char *last; // 当前可用位置(下一个分配从这里开始)
unsigned char *end; // 块的结束位置
struct mp_node_s *next;
} mp_node_t;
// 大块节点(超过 max 的请求走这里)
typedef struct mp_large_s {
struct mp_large_s *next;
void *alloc; // 实际分配的内存
} mp_large_t;
// 内存池
typedef struct mp_pool_s {
size_t max; // 阈值:超过这个大小走大块
struct mp_node_s *head; // 小块链表头
struct mp_large_s *large; // 大块链表头
} mp_pool_t;
图示:
mp_pool_s
├── max = 4096
├── head ──→ [mp_node] ──→ [mp_node] ──→ NULL
│ last↑ last↑
│ end↑ end↑
│ next──→ next──→
└── large ──→ [mp_large] ──→ NULL
alloc↑ alloc↑
5.2 创建内存池 mp_create
c
int mp_create(mp_pool_t *pool, size_t size) {
if (pool == NULL || size <= sizeof(mp_node_t)) return -1;
void *mem = malloc(size);
if (!mem) return -2;
// 第一个节点就是这块内存本身
mp_node_t *node = (mp_node_t *)mem;
node->last = (unsigned char *)mem + sizeof(mp_node_t);
node->end = (unsigned char *)mem + size;
node->next = NULL;
pool->head = node;
pool->max = size;
pool->large = NULL;
return 0;
}
逻辑: 第一次 malloc(size) 得到一整块内存,把前 sizeof(mp_node_t) 字节当作节点头,剩下的给 last 开始往后分配。
[ mp_node_t (头) | 可分配区域 last ─────────────── end ]
↑ sizeof(node) ↑ node->last
5.3 分配内存 mp_alloc
c
void *mp_alloc(mp_pool_t *pool, size_t size) {
if (!pool || size == 0) return NULL;
// 超过阈值,走大块
if (size > pool->max) {
return mp_alloc_large(pool, size);
}
// 在已有节点中找一块够大的
mp_node_t *node = pool->head;
do {
if (node->end - node->last >= size) {
void *ptr = node->last;
node->last += size;
return ptr;
}
node = node->next;
} while (node);
// 找不到,申请新节点
return mp_alloc_block(pool, size);
}
分配策略:
- 先在现有节点里找空间(从头扫描,找第一个够大的)
- 找不到 → 分配一个新节点
5.4 分配新块 mp_alloc_block
c
static void *mp_alloc_block(mp_pool_t *pool, size_t size) {
// 分配一个完整的新节点(大小为 pool->max)
void *mem = malloc(pool->max);
if (!mem) return NULL;
mp_node_t *node = (mp_node_t *)mem;
node->last = (unsigned char *)mem + sizeof(mp_node_t);
node->end = (unsigned char *)mem + pool->max;
node->next = NULL;
// 切一块给调用者
void *ptr = node->last;
node->last += size;
// 尾插到链表
mp_node_t *iter = pool->head;
if (iter) {
while (iter->next) iter = iter->next;
iter->next = node;
} else {
pool->head = node;
}
return ptr;
}
5.5 分配大块 mp_alloc_large
c
static void *mp_alloc_large(mp_pool_t *pool, size_t size) {
if (!pool) return NULL;
void *ptr = malloc(size); // 直接找内核要
if (!ptr) return NULL;
// 优先复用已释放的大块(alloc == NULL 的节点)
mp_large_t *l;
for (l = pool->large; l; l = l->next) {
if (l->alloc == NULL) {
l->alloc = ptr;
return ptr;
}
}
// 没有可复用的,分配新的 mp_large_t 头节点
// ⚠️ 注意:这里直接 malloc(sizeof(mp_large_t)),而不是 mp_alloc()
// 因为 mp_alloc() 在 size > max 时会递归调 mp_alloc_large(),会导致死循环
l = malloc(sizeof(mp_large_t));
if (!l) {
free(ptr);
return NULL;
}
l->alloc = ptr;
l->next = pool->large;
pool->large = l;
return ptr;
}
⚠️ 关键注意点:
mp_large_t头结构体本身用malloc而不是mp_alloc分配- 否则会触发
mp_alloc→mp_alloc_large→mp_alloc递归死循环
5.6 释放内存 mp_free
c
void mp_free(mp_pool_t *pool, void *ptr) {
if (!pool || !ptr) return;
// 在大块链表里找
mp_large_t *l = pool->large;
mp_large_t *prev = NULL;
while (l) {
if (l->alloc == ptr) {
free(l->alloc); // 释放实际内存
l->alloc = NULL; // 标记为可复用
// 从链表移除并释放 mp_large_t 头
if (prev) prev->next = l->next;
else pool->large = l->next;
free(l);
return;
}
prev = l;
l = l->next;
}
}
5.7 销毁内存池 mp_destroy
c
void mp_destroy(mp_pool_t *pool) {
if (!pool) return;
// 先释放所有大块
mp_large_t *l;
for (l = pool->large; l; l = l->next) {
if (l->alloc) free(l->alloc);
}
pool->large = NULL;
// 再释放所有小块节点
mp_node_t *node = pool->head;
while (node) {
mp_node_t *tmp = node->next;
free(node);
node = tmp;
}
pool->head = NULL;
}
6. 进阶:用面向对象的思想封装内存池
6.1 为什么需要面向对象?
想象你要管理多个不同类型的内存池(比如有的按 32 字节分块,有的按 64 字节分块),如果把属性 和方法分离,代码会更清晰:
c
// 属性
struct body {
void *_;
int high;
int weight;
};
// 方法(函数指针)
struct method {
void *(*eat)(struct body *);
void *(*play)(struct body *);
};
// 创建实例
struct body *b = malloc(sizeof(struct body));
*(struct method **)b = method_ptr; // 方法存在 body 头部
这样设计的好处:属性和数据隔离,方法通过函数指针调用,内存池可以更灵活扩展。
6.2 内存池的 OOP 封装思路
c
typedef struct mp_pool_s {
void *(*alloc)(struct mp_pool_s *, size_t);
void (*free)(struct mp_pool_s *, void *);
void (*destroy)(struct mp_pool_s *);
// ... 其他方法
size_t max;
mp_node_t *head;
mp_large_t *large;
} mp_pool_t;
7. 常见 Bug 总结
| # | Bug | 后果 |
|---|---|---|
| 1 | while(!node) 写成 while(node) |
节点无法释放,内存泄漏 |
| 2 | mp_alloc_large 里调 mp_alloc 分配 header |
递归死循环 |
| 3 | mp_free 只 free(alloc) 没 free(l) |
mp_large_t 结构体泄漏 |
| 4 | mp_alloc_block 里用未声明的 mem |
编译错误/段错误 |
| 5 | mp_create 返回 int* 而非 int |
类型不匹配 |
8. 总结
内存池核心思想:
→ 一次向内核申请一大块
→ 按需切成小片分配出去
→ 避免碎片化 + 减少系统调用
两个核心场景:
→ 小块(≤ max):从节点链表分配
→ 大块(> max):直接 malloc/free
设计要点:
→ max 阈值决定走哪条路径
→ 大块 header 不能递归调 mp_alloc
→ free 时要同时释放 alloc 和 header
完整代码
c
#include <stdlib.h>
#include <stddef.h>
#include <stdio.h>
typedef struct mp_node_s {
unsigned char *last;
unsigned char *end;
struct mp_node_s *next;
} mp_node_t;
typedef struct mp_large_s {
struct mp_large_s *next;
void *alloc;
} mp_large_t;
typedef struct mp_pool_s {
size_t max;
struct mp_node_s *head;
struct mp_large_s *large;
} mp_pool_t;
void *mp_alloc(mp_pool_t *pool, size_t size);
static void *mp_alloc_block(mp_pool_t *pool, size_t size);
static void *mp_alloc_large(mp_pool_t *pool, size_t size);
int mp_create(mp_pool_t *pool, size_t size) {
if (pool == NULL || size <= sizeof(mp_node_t)) return -1;
void *mem = malloc(size);
if (!mem) return -2;
mp_node_t *node = (mp_node_t *)mem;
node->last = (unsigned char *)mem + sizeof(mp_node_t);
node->end = (unsigned char *)mem + size;
node->next = NULL;
pool->head = node;
pool->max = size;
pool->large = NULL;
return 0;
}
void mp_destroy(mp_pool_t *pool) {
if (!pool) return;
mp_large_t *l;
for (l = pool->large; l; l = l->next) {
if (l->alloc) free(l->alloc);
}
pool->large = NULL;
mp_node_t *node = pool->head;
while (node) {
mp_node_t *tmp = node->next;
free(node);
node = tmp;
}
pool->head = NULL;
}
static void *mp_alloc_block(mp_pool_t *pool, size_t size) {
void *mem = malloc(pool->max);
if (!mem) return NULL;
mp_node_t *node = (mp_node_t *)mem;
node->last = (unsigned char *)mem + sizeof(mp_node_t);
node->end = (unsigned char *)mem + pool->max;
node->next = NULL;
void *ptr = node->last;
node->last += size;
mp_node_t *iter = pool->head;
if (iter) {
while (iter->next) iter = iter->next;
iter->next = node;
} else {
pool->head = node;
}
return ptr;
}
static void *mp_alloc_large(mp_pool_t *pool, size_t size) {
if (!pool) return NULL;
void *ptr = malloc(size);
if (!ptr) return NULL;
mp_large_t *l;
for (l = pool->large; l; l = l->next) {
if (l->alloc == NULL) {
l->alloc = ptr;
return ptr;
}
}
l = malloc(sizeof(mp_large_t));
if (!l) {
free(ptr);
return NULL;
}
l->alloc = ptr;
l->next = pool->large;
pool->large = l;
return ptr;
}
void *mp_alloc(mp_pool_t *pool, size_t size) {
if (!pool || size == 0) return NULL;
if (size > pool->max) {
return mp_alloc_large(pool, size);
}
mp_node_t *node = pool->head;
do {
if (node->end - node->last >= size) {
void *ptr = node->last;
node->last += size;
return ptr;
}
node = node->next;
} while (node);
return mp_alloc_block(pool, size);
}
void mp_free(mp_pool_t *pool, void *ptr) {
if (!pool || !ptr) return;
mp_large_t *l = pool->large;
mp_large_t *prev = NULL;
while (l) {
if (l->alloc == ptr) {
free(l->alloc);
l->alloc = NULL;
if (prev) prev->next = l->next;
else pool->large = l->next;
free(l);
return;
}
prev = l;
l = l->next;
}
}
int main() {
mp_pool_t pool;
if (mp_create(&pool, 4096) != 0) return -1;
int *a = mp_alloc(&pool, sizeof(int));
*a = 100;
int *b = mp_alloc(&pool, sizeof(int));
*b = 200;
char *str = mp_alloc(&pool, 32);
// 分配大块(超过 max)
char *big = mp_alloc(&pool, 5000);
if (big) big[0] = 'B';
mp_free(&pool, big);
char *big2 = mp_alloc(&pool, 6000);
if (big2) big2[0] = 'C';
mp_destroy(&pool);
return 0;
}
)