26. 结构体与内存池:固定大小内存池、结构体的高效分配与释放
嵌入式开发里,内存分配是个老生常谈的话题。你想想看,一个跑着RTOS的小系统,总共就那么几百KB的RAM,要是频繁调用malloc/free,用不了多久堆就碎成渣了。我早年做一款数据采集设备时就吃过这个亏——程序跑着跑着就卡死了,查了半天,堆碎片导致的分配失败。
那怎么办?说白了,就是用内存池。今天我们就聊聊怎么用固定大小的内存池来高效管理结构体。
为什么需要内存池?
标准库的malloc有个问题:它分配的内存块大小不一,释放后堆里会留下很多小空洞。下次再分配时,即使总空闲空间够,也可能因为找不到连续的大块而失败。这就是碎片化。
嵌入式系统里,很多场景下我们分配的结构体大小是固定的。比如一个网络协议栈里的数据包结构体、一个传感器数据队列里的节点。既然大小固定,那为什么不提前划好一块区域,按固定大小切分管理呢?
这就是内存池的核心思想:预分配一大块连续内存,切成固定大小的块,用链表串起来。分配时取一个块,释放时还回去。没有碎片,速度极快。
核心优势:
- 分配/释放时间复杂度 O(1)
- 无内存碎片
- 分配时间可预测(适合实时系统)
固定大小内存池的实现
我习惯用空闲链表来实现。先定义内存池的结构:
// 内存池块头结构
typedef struct mem_pool_node {
struct mem_pool_node *next;
} mem_pool_node_t;
// 内存池控制结构
typedef struct {
void *pool_start; // 池起始地址
size_t block_size; // 每个块的大小(字节)
size_t block_count; // 块总数
mem_pool_node_t *free_list; // 空闲链表头
} mem_pool_t;
初始化时,把一整块内存切成等大小的块,串成链表:
int mem_pool_init(mem_pool_t *pool, void *buffer,
size_t block_size, size_t block_count) {
if (!pool || !buffer || block_size == 0 || block_count == 0)
return -1;
// 对齐到指针大小(4或8字节)
block_size = (block_size + sizeof(void*) - 1) & ~(sizeof(void*) - 1);
pool->pool_start = buffer;
pool->block_size = block_size;
pool->block_count = block_count;
// 构建空闲链表
pool->free_list = (mem_pool_node_t *)buffer;
mem_pool_node_t *node = pool->free_list;
for (size_t i = 0; i < block_count - 1; i++) {
node->next = (mem_pool_node_t *)((uint8_t*)node + block_size);
node = node->next;
}
node->next = NULL; // 最后一个节点指向空
return 0;
}
分配和释放就非常简单了:
void *mem_pool_alloc(mem_pool_t *pool) {
if (!pool || !pool->free_list)
return NULL;
mem_pool_node_t *node = pool->free_list;
pool->free_list = node->next;
return (void *)node;
}
void mem_pool_free(mem_pool_t *pool, void *ptr) {
if (!pool || !ptr)
return;
// 检查指针是否属于本池(可选)
mem_pool_node_t *node = (mem_pool_node_t *)ptr;
node->next = pool->free_list;
pool->free_list = node;
}
小技巧:我一般会在mem_pool_free里加一个地址范围检查,确保释放的指针确实属于这个池。防止野指针把链表搞乱。
结构体的高效分配与释放
有了内存池,结构体的分配就变成了两步:从池里拿一块内存,然后构造对象。我习惯写一个分配+初始化的函数:
// 假设我们有一个传感器数据结构体
typedef struct {
uint32_t sensor_id;
float temperature;
float humidity;
uint64_t timestamp;
} sensor_data_t;
// 内存池对象(全局或静态)
static mem_pool_t sensor_pool;
static uint8_t sensor_pool_buffer[64 * sizeof(sensor_data_t)]; // 64个节点
// 初始化传感器内存池
void sensor_pool_init(void) {
mem_pool_init(&sensor_pool, sensor_pool_buffer,
sizeof(sensor_data_t), 64);
}
// 从池中分配一个传感器数据节点
sensor_data_t *sensor_data_alloc(uint32_t id, float temp, float hum) {
sensor_data_t *data = (sensor_data_t *)mem_pool_alloc(&sensor_pool);
if (!data)
return NULL;
data->sensor_id = id;
data->temperature = temp;
data->humidity = hum;
data->timestamp = get_system_tick();
return data;
}
// 释放传感器数据节点
void sensor_data_free(sensor_data_t *data) {
if (data) {
// 可以在这里做清理工作
mem_pool_free(&sensor_pool, data);
}
}
你看,这样分配一个结构体,只需要从链表头取一个节点,时间复杂度是O(1)。释放也是O(1)。相比malloc,没有锁竞争(单线程下),没有碎片,速度还快一个数量级。
内存池的SVG结构图
下面这张图展示了固定大小内存池的核心结构:
避坑指南
内存池用起来爽,但坑也不少。我踩过的几个,分享给你:
我曾经犯过的错:
- 块大小没对齐:ARM Cortex-M系列要求4字节对齐,否则访问会触发异常。初始化时一定要做对齐处理。
- 释放了不属于池的指针:有一次我把栈上的地址传给了mem_pool_free,结果链表指针乱飞,整个池子废了。加个地址范围检查能救命。
- 多线程竞争:如果多个任务同时分配/释放,必须加锁。我一般用关中断或信号量保护。
- 池大小估算不足:项目初期觉得64个节点够用,结果后期需求翻倍。建议留30%余量,或者做成可扩展的。
什么时候用内存池?
不是所有场景都适合内存池。我个人总结了几条经验:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 固定大小的结构体频繁分配/释放 | 内存池 | O(1)分配,无碎片 |
| 大小不固定的数据块 | 标准malloc或伙伴系统 | 内存池无法处理变长 |
| 实时性要求高的系统 | 内存池 | 分配时间可预测 |
| 内存极度紧张(几十KB) | 静态分配+内存池 | 避免堆碎片浪费 |
我的习惯:只要项目中出现了频繁分配释放固定大小结构体的场景,我第一反应就是上内存池。比如通信协议栈的包缓冲区、传感器数据队列、任务控制块池。这玩意儿写一次,能省后面无数调试堆碎片的时间。
嗯,关于结构体和内存池的配合,今天就聊到这儿。记住一句话:固定大小、频繁分配、实时要求——这三个条件同时满足时,别犹豫,上内存池。
公众号:蓝海资料掘金营,微信deep3321