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结构图

下面这张图展示了固定大小内存池的核心结构:

固定大小内存池结构图 mem_pool_t pool_start ────→ block_size = 32 block_count = 8 free_list ────→ 预分配内存块(8个节点,每个32字节) 块0 块1 块2 块3 块4 块5 块6 块7 free_list 块0~6在空闲链表中,块7已被分配出去 分配流程 1. 从 free_list 取头节点 2. free_list 指向下一个节点 3. 返回取下的节点地址 4. 时间复杂度 O(1) 释放流程 1. 将节点插入 free_list 头部 2. 更新 free_list 指向该节点 3. 时间复杂度 O(1) 4. 无碎片产生

避坑指南

内存池用起来爽,但坑也不少。我踩过的几个,分享给你:

我曾经犯过的错:

  • 块大小没对齐:ARM Cortex-M系列要求4字节对齐,否则访问会触发异常。初始化时一定要做对齐处理。
  • 释放了不属于池的指针:有一次我把栈上的地址传给了mem_pool_free,结果链表指针乱飞,整个池子废了。加个地址范围检查能救命。
  • 多线程竞争:如果多个任务同时分配/释放,必须加锁。我一般用关中断或信号量保护。
  • 池大小估算不足:项目初期觉得64个节点够用,结果后期需求翻倍。建议留30%余量,或者做成可扩展的。

什么时候用内存池?

不是所有场景都适合内存池。我个人总结了几条经验:

场景 推荐方案 原因
固定大小的结构体频繁分配/释放 内存池 O(1)分配,无碎片
大小不固定的数据块 标准malloc或伙伴系统 内存池无法处理变长
实时性要求高的系统 内存池 分配时间可预测
内存极度紧张(几十KB) 静态分配+内存池 避免堆碎片浪费

我的习惯:只要项目中出现了频繁分配释放固定大小结构体的场景,我第一反应就是上内存池。比如通信协议栈的包缓冲区、传感器数据队列、任务控制块池。这玩意儿写一次,能省后面无数调试堆碎片的时间。

嗯,关于结构体和内存池的配合,今天就聊到这儿。记住一句话:固定大小、频繁分配、实时要求——这三个条件同时满足时,别犹豫,上内存池


公众号:蓝海资料掘金营,微信deep3321