前言
在上篇文章中,我们提到了动态内存分配的四大金刚:malloc、calloc、realloc、free。然而,频繁调用这些函数会带来两个问题:
1. 性能开销:系统调用涉及用户态/内核态切换,频繁分配小块内存时效率低下
2. 内存碎片:反复分配释放会产生大量外部碎片,导致大块连续内存分配失败
内存池 是一种常见的解决方案:预先申请一大块内存,然后由我们自己管理其内部的小块分配与释放。今天,我们就手写一个简单实用的内存池,彻底理解其背后的原理。
一、内存池的核心设计思想
一个经典的内存池需要解决三个核心问题:
· 如何快速分配:维护空闲块链表,分配时直接取用
· 如何高效合并:释放时尝试合并相邻空闲块,减少碎片
· 如何管理元数据:每个内存块需要记录大小、是否空闲等信息
我们采用 边界标记法(Boundary Tag) 实现一个简单但完整的内存池。
二、数据结构设计
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
// 内存块头结构(紧挨着用户数据之前)
typedef struct block_header {
size_t size; // 当前块大小(包括头部)
int is_free; // 是否空闲
struct block_header* next; // 指向下一个空闲块(仅空闲块使用)
struct block_header* prev; // 指向上一个空闲块
} block_header_t;
// 内存池结构
typedef struct {
void* pool_start; // 池起始地址
size_t pool_size; // 池总大小
block_header_t* free_list; // 空闲链表头
} memory_pool_t;
```
关键设计点:
· 每个内存块前都有一个 block_header_t,存储元数据
· 空闲块通过 next/prev 连接成双向链表
· 用户得到的地址是 header + 1(跳过头部)
三、核心函数实现
1. 内存池创建
```c
memory_pool_t* mem_pool_create(size_t size) {
if (size < sizeof(block_header_t) + 8) {
size = sizeof(block_header_t) + 8; // 最小池大小
}
memory_pool_t* pool = (memory_pool_t*)malloc(sizeof(memory_pool_t));
if (!pool) return NULL;
pool->pool_start = malloc(size);
if (!pool->pool_start) {
free(pool);
return NULL;
}
pool->pool_size = size;
// 初始化唯一的空闲块
block_header_t* first_block = (block_header_t*)pool->pool_start;
first_block->size = size;
first_block->is_free = 1;
first_block->next = NULL;
first_block->prev = NULL;
pool->free_list = first_block;
return pool;
}
```
2. 分配函数(首次适应算法)
```c
void* mem_pool_alloc(memory_pool_t* pool, size_t req_size) {
if (!pool || req_size == 0) return NULL;
// 对齐到8字节边界(常见CPU要求)
size_t aligned_size = (req_size + 7) & ~7;
size_t total_size = aligned_size + sizeof(block_header_t);
block_header_t* current = pool->free_list;
while (current) {
if (current->is_free && current->size >= total_size) {
// 找到合适的空闲块
if (current->size >= total_size + sizeof(block_header_t) + 8) {
// 拆分:剩余空间足够成为一个新的空闲块
block_header_t* new_block = (block_header_t*)((char*)current + total_size);
new_block->size = current->size - total_size;
new_block->is_free = 1;
new_block->next = current->next;
new_block->prev = current;
if (current->next) {
current->next->prev = new_block;
}
current->size = total_size;
current->next = new_block;
}
current->is_free = 0;
// 从空闲链表中移除
if (current->prev) {
current->prev->next = current->next;
} else {
pool->free_list = current->next;
}
if (current->next) {
current->next->prev = current->prev;
}
// 返回用户数据区地址
return (void*)(current + 1);
}
current = current->next;
}
return NULL; // 内存不足
}
```
3. 释放与合并
```c
void mem_pool_free(memory_pool_t* pool, void* ptr) {
if (!pool || !ptr) return;
// 获取块头地址
block_header_t* block = (block_header_t*)ptr - 1;
block->is_free = 1;
// 尝试与前一个块合并(如果前一个块存在且空闲)
// 注意:这里简化了,完整实现需要遍历或维护相邻块指针
// 实际工程中会维护每个块的前后块指针(不仅仅是空闲块)
// 插入回空闲链表(按地址排序)
block_header_t* current = pool->free_list;
block_header_t* prev = NULL;
while (current && current < block) {
prev = current;
current = current->next;
}
block->next = current;
block->prev = prev;
if (prev) {
prev->next = block;
} else {
pool->free_list = block;
}
if (current) {
current->prev = block;
}
// 尝试与后一个空闲块合并
if (current && (char*)block + block->size == (char*)current) {
block->size += current->size;
block->next = current->next;
if (current->next) {
current->next->prev = block;
}
}
// 尝试与前一个空闲块合并
if (prev && (char*)prev + prev->size == (char*)block) {
prev->size += block->size;
prev->next = block->next;
if (block->next) {
block->next->prev = prev;
}
}
}
```
4. 销毁内存池
```c
void mem_pool_destroy(memory_pool_t* pool) {
if (!pool) return;
if (pool->pool_start) {
free(pool->pool_start);
}
free(pool);
}
```
四、测试代码
```c
int main() {
// 创建 4KB 的内存池
memory_pool_t* pool = mem_pool_create(4096);
printf("内存池创建成功,总大小: %zu 字节\n", pool->pool_size);
// 分配三次
int* a = (int*)mem_pool_alloc(pool, sizeof(int));
char* b = (char*)mem_pool_alloc(pool, 64);
double* c = (double*)mem_pool_alloc(pool, sizeof(double));
*a = 42;
*c = 3.14159;
printf("a=%d, c=%f\n", *a, *c);
// 释放中间块
mem_pool_free(pool, b);
printf("释放 b 后,空闲链表重新连接\n");
// 再次分配,应复用刚刚释放的内存
char* d = (char*)mem_pool_alloc(pool, 32);
printf("重新分配得到: %p\n", d);
// 清理
mem_pool_destroy(pool);
return 0;
}
```
五、优化与扩展方向
以上实现是一个教学版内存池,实际工程中可以从以下方面优化:
1. 多个固定大小池:预先创建不同尺寸的池(如 8B、16B、32B...),分配时取最接近的,彻底消除内部碎片
2. 线程安全:加互斥锁或使用线程局部存储(TLS)
3. 调试支持:在头部增加魔数、分配调用栈记录,便于检测内存越界和泄漏
4. 缓存对齐:将块大小对齐到 CPU 缓存行,避免伪共享
六、常见陷阱
· 越界访问:用户数据超出申请大小,会破坏下一个块的头部元数据
· 重复释放:释放同一块内存两次,链表会被破坏
· 释放非池内地址:需要增加校验机制
结语
手写内存池的过程,本质上是在重新思考操作系统的内存管理算法。通过这个练习,你不仅掌握了一个实用工具的实现,更能深刻理解内存布局、链表操作和碎片管理这些核心概念。
当然,生产环境建议使用成熟的内存池库(如 tcmalloc、jemalloc),但亲手写一遍的价值,无可替代。
下一篇预告:《深入理解C语言指针:从一级指针到函数指针》