告别内存碎片!嵌入式系统内存池完美解决方案

立芯嵌入式 2025-08-07 12:30

大家好,欢迎来到立芯嵌入式。

最近在给一个工控项目做优化,又一次遇到了老生常谈的问题:动态内存分配。

先说说动态内存分配这个东西。在PC端写代码的时候,new一个对象,delete掉,这事儿再正常不过了。内存不够?加内存条呗。但在嵌入式系统里,尤其是那些只有几十K RAM的MCU上,情况就完全不同了。

动态内存分配确实有它的好处。比如你在写协议栈的时候,不同的数据包大小不一样,如果都按最大的来分配静态内存,那简直是暴殄天物。再比如你想实现一些高级的数据结构,链表啊、队列啊,没有动态分配还真不好搞。

但问题也很明显。首先是时间不确定性,malloc这货在分配内存时需要遍历空闲链表,在实时系统里这可是大忌。其次是内存碎片问题,用着用着你会发现,明明还有不少空闲内存,但就是分配不出一块连续的空间。最要命的是,很多小型MCU的工具链压根就不支持malloc,或者支持得很差,一用就让代码体积暴涨。

资讯配图

曾经维护过一个老项目,前任工程师在里面大量使用了malloc和free,结果系统运行个把小时就会莫名其妙地死机。后来花了整整一周时间排查,发现是内存碎片导致的分配失败,而错误处理又写得不完善,直接访问了空指针。那个项目最后全部改成静态分配才算稳定下来。

引入Pool模式

既然malloc有这么多问题,那有没有什么好的替代方案呢?这就要说到今天的主角了:内存池模式,也就是Pool Pattern。

资讯配图

内存池的思路其实很简单,就像食堂的餐盘一样。食堂准备了固定数量的餐盘,每个餐盘大小都一样。你来吃饭的时候拿一个,吃完了洗干净还回去,下一个人接着用。这比每次都去买新餐盘或者用完就扔要实际得多。

在嵌入式系统里,我们可以预先分配一块固定大小的内存区域,把它分成若干个相同大小的块。需要内存的时候从池子里拿一块,用完了还回去。这样做的好处显而易见:分配时间是确定的O(1),不会产生内存碎片,内存使用量在编译时就能确定,调试起来也方便。

动手实现一个内存池

资讯配图
MemPool 结构体组成

说了这么多理论,咱们来点实际的,展示一个通用的C语言内存池实现:

// 内存池结构体定义
typedefstruct {
    void* memory_pool;      // 内存池首地址
    void* free_list;        // 空闲块链表头
    uint8_t* bitmap;        // 位图管理数组
    size_t block_size;      // 每个块的大小
    size_t block_count;     // 总块数
    size_t free_count;      // 剩余空闲块数
} MemPool;

// 内存池接口函数
void mempool_init(MemPool* pool, void* buffer, size_t block_size, size_t block_count);
voidmempool_alloc(MemPool* pool);
void mempool_free(MemPool* pool, void* block);
size_t mempool_available(MemPool* pool);

实际使用的时候,你需要先准备一块静态内存区域:

// 定义一个可以存放32个CAN报文的内存池
#define CAN_MSG_SIZE    64
#define CAN_MSG_COUNT   32

staticuint8_t can_buffer[CAN_MSG_SIZE * CAN_MSG_COUNT];
staticuint8_t can_bitmap[(CAN_MSG_COUNT + 7) / 8];
static MemPool can_msg_pool;

// 初始化
mempool_init(&can_msg_pool, can_buffer, CAN_MSG_SIZE, CAN_MSG_COUNT);

// 使用
void* msg = mempool_alloc(&can_msg_pool);
if (msg != NULL) {
    // 使用消息缓冲区
    process_can_message(msg);
    // 用完归还
    mempool_free(&can_msg_pool, msg);
}

如果你不想每次都传递这么多参数,可以用宏来简化定义:

#define DEFINE_POOL(name, type, count) \
    static type name##_buffer[count]; \
    static uint8_t name##_bitmap[(count + 7) / 8]; \
    static MemPool name = { \
        .memory_pool = name##_buffer, \
        .bitmap = name##_bitmap, \
        .block_size = sizeof(type), \
        .block_count = count, \
        .free_count = count \
    }


// 使用宏定义一个内存池
DEFINE_POOL(uart_pool, UartFrame, 16);

// 分配和释放
UartFrame* frame = (UartFrame*)mempool_alloc(&uart_pool);
mempool_free(&uart_pool, frame);

巧用位图管理内存状态

资讯配图

实现内存池最关键的问题是如何高效地跟踪每个内存块的状态,是空闲还是已分配?最直观的方法是用一个bool数组,每个元素对应一个内存块。但在资源受限的MCU上,这种方法太奢侈了。一个bool通常占用一个字节,64个内存块就要64字节的管理开销。

资讯配图
位图存储效率对比

这里有个小技巧:用位图来管理。一个uint8_t有8个位,可以管理8个内存块的状态。64个内存块只需要8个字节就够了。

资讯配图
位图操作实例

计算需要多少个字节很简单:

#define BITS_PER_BYTE  8
#define BITMAP_SIZE(n) (((n) + BITS_PER_BYTE - 1) / BITS_PER_BYTE)

// 例如:64个块需要的位图大小
uint8_t bitmap[BITMAP_SIZE(64)];  // 实际分配8个字节

这个公式里的 ((n) + BITS_PER_BYTE - 1) 是个常见的向上取整技巧,确保即使块数不是8的倍数也能分配足够的空间。

操作位图需要一些位运算技巧,这里是具体实现:

// 设置某一位为1(标记为空闲)
static inline void bitmap_set(uint8_t* bitmap, size_t index)
{
    size_t byte_idx = index / 8;
    size_t bit_idx = index % 8;
    bitmap[byte_idx] |= (1u << bit_idx);
}

// 清除某一位为0(标记为已分配)
static inline void bitmap_clear(uint8_t* bitmap, size_t index)
{
    size_t byte_idx = index / 8;
    size_t bit_idx = index % 8;
    bitmap[byte_idx] &= ~(1u << bit_idx);
}

// 测试某一位的状态
static inline int bitmap_test(const uint8_t* bitmap, size_t index)
{
    size_t byte_idx = index / 8;
    size_t bit_idx = index % 8;
    return (bitmap[byte_idx] & (1u << bit_idx)) != 0;
}

// 查找第一个空闲位
static int bitmap_find_first_set(const uint8_t* bitmap, size_t total_bits)
{
    size_t bytes = BITMAP_SIZE(total_bits);
    for (size_t i = 0; i < bytes; i++) {
        if (bitmap[i] != 0) {
            // 使用__builtin_ctz查找最低位的1(GCC内建函数)
            // 如果编译器不支持,可以用循环代替
            uint8_t byte = bitmap[i];
            for (int j = 0; j < 8; j++) {
                if (byte & (1u << j)) {
                    size_t index = i * 8 + j;
                    if (index < total_bits) {
                        return (int)index;
                    }
                }
            }
        }
    }
    return-1;  // 没有找到空闲块
}

完整的内存池实现核心代码如下:

void mempool_init(MemPool* pool, void* buffer, size_t block_size, size_t block_count)
{
    pool->memory_pool = buffer;
    pool->block_size = block_size;
    pool->block_count = block_count;
    pool->free_count = block_count;
    
    // 初始化位图,全部标记为空闲
    size_t bitmap_bytes = BITMAP_SIZE(block_count);
    memset(pool->bitmap, 0xFF, bitmap_bytes);
}

voidmempool_alloc(MemPool* pool)
{
    if (pool->free_count == 0) {
        returnNULL;
    }
    
    // 查找第一个空闲块
    int index = bitmap_find_first_set(pool->bitmap, pool->block_count);
    if (index < 0) {
        returnNULL;
    }
    
    // 标记为已分配
    bitmap_clear(pool->bitmap, index);
    pool->free_count--;
    
    // 计算并返回块地址
    return (uint8_t*)pool->memory_pool + (index * pool->block_size);
}

void mempool_free(MemPool* pool, void* block)
{
    if (block == NULL) {
        return;
    }
    
    // 计算块的索引
    size_t offset = (uint8_t*)block - (uint8_t*)pool->memory_pool;
    size_t index = offset / pool->block_size;
    
    // 边界检查
    if (index >= pool->block_count) {
        return;  // 非法地址
    }
    
    // 标记为空闲
    bitmap_set(pool->bitmap, index);
    pool->free_count++;
}

总结

在实际项目中使用内存池,我总结了几点经验:

首先是池子大小的确定。这个需要根据实际应用场景来评估。我通常的做法是先用一个较大的值,然后在测试阶段监控实际使用情况,逐步调整到合适的大小。记得留出20%左右的余量,应对突发情况。

其次是错误处理。palloc可能返回NULL,一定要检查!我见过太多因为忽略这个检查导致的系统崩溃了。可以考虑在调试版本里加入断言,在分配失败时立即暴露问题。

还有一个细节是内存对齐。如果你的T类型有对齐要求,比如在某些ARM平台上double需要8字节对齐,要确保elements数组也满足这个要求。现代编译器通常会自动处理,但在一些特殊场景下可能需要手动指定对齐属性。

最后说说使用场景。内存池特别适合这些情况:对象大小固定且生命周期相对较短的场景,比如通信协议的数据包缓冲区、事件系统的消息对象、状态机的状态节点等。我们在一个Modbus RTU的实现里,用内存池管理请求和响应帧,效果很好。

如果你的系统需要多种大小的动态内存,可以创建多个不同规格的内存池。比如一个32字节的小对象池,一个256字节的中对象池,一个1K的大对象池。根据请求的大小选择合适的池子,这样可以减少内部碎片。

另一个优化方向是支持可变大小的分配。可以把多个连续的块合并分配给一个请求,但这会增加管理复杂度,需要权衡利弊。

对于只在初始化阶段分配、运行期间不释放的场景,可以实现一个更简单的单向内存池,只支持分配不支持释放,进一步降低开销。

声明:内容取材于网络,仅代表作者观点,如有内容违规问题,请联系处理。 
内存 嵌入式系统
more
潮讯:SK海力士超三星成全球最大内存制造商;华强北兴起“小孩哥”代送外卖潮;荣耀超级工作台支持其他Windows电脑
2.5w!英伟达推出机器人“最强大脑”:AI算力飙升750%配128GB大内存,宇树已经用上了
自研芯片、猛攻企业级,国内存储“量级跃迁”
SK海力士超越三星电子,首夺全球内存市场第一
首款英特尔 Panther Lake 工业主板曝光:三 2.5G 网口,支持 DDR5-7200 内存
攀登HBM之巅:AI加速器的内存墙突围战(四)分层存储战略与推理范式变革
2G内存就能跑!谷歌Gemma 3n实测:端侧大模型新王登基!
Transformer危!谷歌MoR架构发布:内存减半推理速度还翻倍
DDR5 内存超频新世界纪录诞生:海盗船内存飙至 12886MT/s
聊一台「64GB大内存」的主流价位笔电
Copyright © 2025 成都区角科技有限公司
蜀ICP备2025143415号-1
  
川公网安备51015602001305号