样章-第04讲-告别malloc-静态分配与内存池

本文针对资源受限的联网固件(如STM32),深入剖析malloc在嵌入式环境中的四大致命缺陷:内存碎片、时间不确定性、分配失败难处理及无MMU导致的越界风险。提出以静态分配为基础的定长内存池方案,实现O(1)分配释放、零碎片、确定性时延,并支持跨任务零拷贝、双缓冲等关键机制,适用于物联网协议帧、MQTT包等典型场景。

第 4 讲 | 告别 malloc:静态分配 + 内存池的固件架构

【本讲要点】在 16KB RAM 的联网固件里,malloc 不是"方便",而是"定时炸弹"。这一讲给你一套可直接移植的定长内存池。


一、为什么 malloc 是灾难

先问一个问题:你的固件要连续运行多久?

如果是消费类产品,答案是"用户不拔电就一直跑"——可能是几个月、几年。

在这个时间尺度下,malloc 有三个致命问题:

问题 1:内存碎片(最致命)

假设你有 4KB 可用堆。程序运行中做这样的循环:

void *a = malloc(64);
void *b = malloc(128);
free(a);
void *c = malloc(96);   /* 能分配成功吗? */

看起来"总空闲 = 64 + 剩余",应该够。但如果 a 和 b 相邻,free(a) 释放的 64 字节是个"洞",而 c 需要 96 字节连续空间——分配失败。

碎片是渐进的、不可逆的。 系统跑得越久,可用连续内存越少,最终某次分配失败,设备死机。这就是"跑几天就挂"的典型成因。

问题 2:时间不确定性

malloc 的耗时依赖于当前堆的状态——可能是几十个周期,也可能是几万个周期(需要遍历空闲链表、合并碎片)。

在实时系统里,一段代码的执行时间不可预测,就是设计缺陷。

问题 3:分配失败的善后

malloc 失败返回 NULL。你的代码检查了吗?

uint8_t *buf = malloc(len);
memcpy(buf, src, len);   /* 如果 buf 是 NULL,直接崩 */

在小内存系统里,分配失败不是"异常情况",而是"迟早会发生的情况"。 靠一套庞大的错误处理去兜住它,成本极高。

问题 4:没有 MMU,踩了就真踩了

PC 上有 MMU,越界访问会被操作系统拦住。MCU 上没有 MMU,malloc 越界一个字节,踩掉的就是别的变量——而且可能几小时后才表现出症状。

结论:在资源受限的联网固件里,所有内存分配都应该是"启动期确定、运行期零分配"。


二、替代方案总览

方案适用场景确定性
静态数组数量固定、生命周期贯穿全程最高
定长内存池数量不定但结构同构(协议帧、包)高,O(1)
FreeRTOS heap_1只在启动期分配高
malloc / heap_4不可避免时低

本讲重点讲"定长内存池"——它覆盖了 90% 的业务缓冲需求(协议帧、MQTT 包、日志条目),而且实现简单、性能确定。


三、定长内存池:完整实现

核心思想:所有块大小相同,空闲块串成一个链表。分配 = 摘链表头,释放 = 挂回链表头。O(1),无碎片。

/* ==================== mem_pool.h ==================== */
#ifndef MEM_POOL_H
#define MEM_POOL_H

#include <stdint.h>
#include <stdbool.h>

typedef struct mem_pool mem_pool_t;

/* 创建内存池:blk_size 单块字节数,blk_cnt 块数
 * 返回 NULL 表示内存不足 */
mem_pool_t *mem_pool_create(void *storage, uint32_t blk_size, uint32_t blk_cnt);

void *mem_pool_alloc(mem_pool_t *pool);       /* 分配一块,失败返回 NULL */
void  mem_pool_free(mem_pool_t *pool, void *blk);  /* 归还一块 */
uint32_t mem_pool_free_count(const mem_pool_t *pool);
uint32_t mem_pool_used_count(const mem_pool_t *pool);

#endif
/* ==================== mem_pool.c ==================== */
#include "mem_pool.h"

/* 空闲块用前 4 字节存下一块地址,所以单块最小要能放下一个指针 */
#define POOL_MIN_BLK_SIZE   (sizeof(void *))

struct mem_pool {
    void    *free_list;    /* 空闲块链表头 */
    uint32_t blk_size;     /* 每块字节数(已对齐) */
    uint32_t blk_cnt;      /* 总块数 */
    uint32_t free_cnt;     /* 当前空闲块数 */
};

/* 把 size 向上对齐到 4 字节边界 */
static uint32_t align4(uint32_t size)
{
    return (size + 3u) & ~3u;
}

mem_pool_t *mem_pool_create(void *storage, uint32_t blk_size, uint32_t blk_cnt)
{
    if (storage == NULL || blk_cnt == 0) {
        return NULL;
    }

    blk_size = align4(blk_size);
    if (blk_size < POOL_MIN_BLK_SIZE) {
        blk_size = POOL_MIN_BLK_SIZE;
    }

    /* 池头放在 storage 开头,后面跟数据块区域 */
    mem_pool_t *pool = (mem_pool_t *)storage;
    uint8_t *base = (uint8_t *)storage + align4(sizeof(mem_pool_t));

    pool->blk_size = blk_size;
    pool->blk_cnt  = blk_cnt;
    pool->free_cnt = blk_cnt;
    pool->free_list = NULL;

    /* 从后往前串链表,保证返回的地址是升序,便于调试 */
    for (int32_t i = (int32_t)blk_cnt - 1; i >= 0; i--) {
        void *blk = base + (uint32_t)i * blk_size;
        *(void **)blk = pool->free_list;
        pool->free_list = blk;
    }

    return pool;
}

void *mem_pool_alloc(mem_pool_t *pool)
{
    if (pool == NULL || pool->free_list == NULL) {
        return NULL;                 /* 池空,绝不阻塞、绝不崩溃 */
    }

    /* 临界区:多任务/中断环境下必须保护 */
    void *blk = pool->free_list;
    pool->free_list = *(void **)blk;
    pool->free_cnt--;

    return blk;
}

void mem_pool_free(mem_pool_t *pool, void *blk)
{
    if (pool == NULL || blk == NULL) {
        return;
    }

    *(void **)blk = pool->free_list;
    pool->free_list = blk;
    pool->free_cnt++;
}

uint32_t mem_pool_free_count(const mem_pool_t *pool)
{
    return pool ? pool->free_cnt : 0;
}

uint32_t mem_pool_used_count(const mem_pool_t *pool)
{
    return pool ? (pool->blk_cnt - pool->free_cnt) : 0;
}

怎么用

所有存储都是静态的,启动时一次性分配好,运行期零开销:

/* 16KB RAM 上的实际配置 */
#define FRAME_BLK_SIZE   64
#define FRAME_BLK_CNT    8

/* 静态存储区,编译期确定,不占用堆 */
static uint8_t s_frame_pool_storage[
    sizeof(mem_pool_t) + 4 + FRAME_BLK_SIZE * FRAME_BLK_CNT
] __attribute__((aligned(4)));

static mem_pool_t *s_frame_pool;

void comm_init(void)
{
    s_frame_pool = mem_pool_create(s_frame_pool_storage,
                                   FRAME_BLK_SIZE, FRAME_BLK_CNT);
    /* 上线前自检:池必须创建成功 */
    configASSERT(s_frame_pool != NULL);
}

收益对比:

方案 A:固定 8 个静态数组方案 B:内存池
RAM 占用8 × 64 = 512B8 × 64 = 512B(相同)
使用数组索引需手工管理哪块空闲自动
支持动态数量否是(最多 8 块)
碎片无无
分配耗时0O(1),恒定

内存池用同样的 RAM,换来了"按需分配"的灵活性,且没有 malloc 的碎片风险。这就是它值得用的原因。


四、跨任务零拷贝:队列传指针

有了内存池,跨任务传递数据就变得很干净:分配一块 → 填数据 → 把指针塞进队列 → 消费者用完归还。

/* 队列里存的是"指针",不是数据本身 —— 这就是零拷贝的含义 */
static QueueHandle_t s_frame_queue;

typedef struct {
    uint8_t *data;
    uint16_t len;
} frame_msg_t;

/* 生产者任务:采集 */
void producer_task(void *arg)
{
    for (;;) {
        uint8_t *blk = mem_pool_alloc(s_frame_pool);
        if (blk == NULL) {
            /* 池满:说明消费者跟不上,丢弃本帧并计数 */
            stat_inc(STAT_FRAME_DROP);
            vTaskDelay(pdMS_TO_TICKS(100));
            continue;
        }

        uint16_t len = sensor_pack(blk, FRAME_BLK_SIZE);

        frame_msg_t msg = { .data = blk, .len = len };
        if (xQueueSend(s_frame_queue, &msg, 0) != pdPASS) {
            mem_pool_free(s_frame_pool, blk);   /* 队满,归还,不留残块 */
            stat_inc(STAT_QUEUE_DROP);
        }

        vTaskDelay(pdMS_TO_TICKS(1200));
    }
}

/* 消费者任务:上报 */
void consumer_task(void *arg)
{
    frame_msg_t msg;
    for (;;) {
        if (xQueueReceive(s_frame_queue, &msg, portMAX_DELAY) == pdPASS) {
            iot_publish(msg.data, msg.len);
            mem_pool_free(s_frame_pool, msg.data);   /* 用完必须归还 */
        }
    }
}

三个关键点:

  1. 零拷贝:队列只传 8 字节指针,数据本身不搬;
  2. 绝不泄露:xQueueSend 失败也必须归还块;
  3. 绝不阻塞在分配上:池空就丢帧记账,保证实时性。

五、双缓冲:解决"发送中被覆盖"

第 7 讲留过一个尾巴:一帧正在发送时,新数据来了会覆盖它,导致数据混帧。

用内存池,这个问题天然就解决了——发送中的块还在被引用,新数据会去申请新块:

时间线:
  T1  数据帧 A 申请 blk1,投入队列
  T2  消费者取出 blk1,开始发送(blk1 被占用)
  T3  新数据帧 B 到达 → 申请 blk2(而不是覆盖 blk1)✓
  T4  blk1 发送完成 → 归还给池
  T5  消费者取出 blk2 → 发送

"发送中"和"待发送"自然分离,这就是零拷贝架构的额外红利。


六、内存池的四个注意事项

1. 临界区保护。
mem_pool_alloc / free 里的链表操作不是原子的。如果可能被中断或多个任务调用,必须加保护:

void *mem_pool_alloc(mem_pool_t *pool)
{
    taskENTER_CRITICAL();       /* 或 configASSERT 单任务独占 */
    void *blk = pool->free_list;
    if (blk) {
        pool->free_list = *(void **)blk;
        pool->free_cnt--;
    }
    taskEXIT_CRITICAL();
    return blk;
}

在中断里调用要用 taskENTER_CRITICAL_FROM_ISR。

2. 块大小要"按最大需求+对齐"定。
如果一帧最大 48 字节,别把块定成 32,会溢出。留 20% 余量。

3. 块数要算清楚最坏情况。
"生产者比消费者快"时,队列会积压。块数 ≥ 队列深度 + 在途帧数 + 1。 否则池会频繁空。

4. 一定要做统计。
free_cnt 的长期最小值,就是你内存压力的真实指标。加上"丢弃计数",才能在量产设备上远程定位问题。

/* 串口周期性输出,用于现场诊断 */
log_info("pool: used=%u free=%u, drop=%u/%u",
         mem_pool_used_count(s_frame_pool),
         mem_pool_free_count(s_frame_pool),
         stat_get(STAT_FRAME_DROP), stat_get(STAT_QUEUE_DROP));

七、小结

三句话记住:

1. 联网固件里,运行期绝不能有 malloc——碎片和时间不确定性是致命的。
2. 定长内存池 = 摘/挂链表头,O(1)、无碎片、可实现、可统计。
3. 队列传指针 = 零拷贝;顺带解决"发送中被覆盖",自然实现双缓冲。

下一讲(第 5 讲),我们进入通信链路的第一站:如何用 UART + DMA + 空闲中断 + 环形缓冲,搭出一套"绝不掉包"的收帧架构。它正是第 7 讲那个上报问题的数据源头。

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值