10、状态机与内存管理:静态内存分配、动态内存分配、内存池在状态机中的应用

内存管理这个话题,说实话,在嵌入式圈子里一直是个「老生常谈」但又绕不开的坎儿。我见过不少新手写状态机,功能跑通了就万事大吉,结果一上电跑个几天,系统莫名其妙就挂了——查到最后,往往是内存泄漏或者碎片化惹的祸。

今天咱们就聊聊,状态机里到底该怎么管内存。三种方式:静态分配、动态分配、内存池。各有各的脾气,也各有各的适用场景。

10.1 静态内存分配:最稳,但也最死板

静态分配,说白了就是「编译时就定死了」。你在代码里写一个全局数组,或者用 static 关键字修饰的变量,内存地址和大小在链接阶段就固定了。

核心特点:

  • 分配时间:编译期
  • 释放方式:无需手动释放
  • 碎片风险:无
  • 适用场景:状态数量固定、状态数据大小已知

我在项目中遇到过一个小型传感器采集系统,状态机只有 4 个状态:初始化、等待、采集、上报。每个状态需要的数据结构都很简单,一个 uint8_t 的缓冲区就够了。这种场景,静态分配就是最优解。

// 静态分配的状态上下文
typedef struct {
    uint8_t state_id;
    uint32_t timestamp;
    uint8_t buffer[64];
} StateContext;

static StateContext g_ctx;  // 全局静态分配

void state_machine_run(void) {
    switch (g_ctx.state_id) {
        case STATE_INIT:
            // 直接使用 g_ctx.buffer
            break;
        // ...
    }
}

嗯,这里要注意:静态分配虽然稳,但灵活性差。如果你不确定状态机未来会不会增加状态,或者状态数据大小会不会变,那静态分配可能就有点「捉襟见肘」了。

10.2 动态内存分配:灵活,但容易翻车

动态分配,就是运行时用 mallocfree 来申请和释放内存。听起来很美好,对吧?状态机需要多少内存,运行时动态决定,多灵活。

但说实话,我在嵌入式项目里很少用标准库的 malloc。为什么?你想想看,一个状态机可能在 1 秒内切换几十次状态,每次切换都 malloc/free 一次,用不了多久堆空间就碎成渣了。

我曾经踩过的坑:

有一次做网络协议栈的状态机,每个连接需要动态分配一个 256 字节的缓冲区。跑了大概 3 个小时,系统突然报内存分配失败。查了一整天,发现是频繁的 malloc/free 导致堆碎片化,虽然总空闲内存还有 40%,但找不到一块连续的 256 字节空间。

// 动态分配——不推荐在状态机中频繁使用
void state_transition(StateMachine *sm, int new_state) {
    if (sm->context) {
        free(sm->context);  // 释放旧上下文
        sm->context = NULL;
    }
    sm->context = malloc(sizeof(StateContext));  // 申请新上下文
    if (!sm->context) {
        // 分配失败!状态机怎么办?
        error_handler();
    }
}

说白了,动态分配不是不能用,但你要想清楚:你的状态机切换频率有多高?每次分配的内存块大小是否一致?系统有没有内存碎片回收机制?

10.3 内存池:兼顾灵活与稳定

内存池,是我个人在状态机项目中最喜欢的方式。它本质上就是「预分配 + 复用」。你在初始化时一次性申请一大块内存,然后切成固定大小的块,状态机需要时从池子里取,用完还回去。

内存池的优势:

  • 分配/释放速度快(O(1) 复杂度)
  • 无碎片问题(所有块大小一致)
  • 内存使用量可预测

我建议,如果你的状态机状态数量在 10 个以内,每个状态的数据结构大小差不多,那就直接用内存池。下面是一个简单的实现示例:

#define POOL_SIZE 10
#define BLOCK_SIZE sizeof(StateContext)

static uint8_t pool_memory[POOL_SIZE][BLOCK_SIZE];
static uint8_t pool_used[POOL_SIZE];  // 0: 空闲, 1: 已用

void* pool_alloc(void) {
    for (int i = 0; i < POOL_SIZE; i++) {
        if (!pool_used[i]) {
            pool_used[i] = 1;
            return &pool_memory[i];
        }
    }
    return NULL;  // 池子满了
}

void pool_free(void *ptr) {
    for (int i = 0; i < POOL_SIZE; i++) {
        if (&pool_memory[i] == ptr) {
            pool_used[i] = 0;
            memset(&pool_memory[i], 0, BLOCK_SIZE);
            return;
        }
    }
}

你看,这个实现很简单,但非常实用。状态机切换时,从池子里拿一块内存,用完还回去,永远不会产生碎片。

小技巧:

如果状态机中某些状态需要的内存特别大,可以设计「多级内存池」。比如一个池子管理 64 字节块,另一个管理 256 字节块。根据状态需求,从不同池子分配。我在一个多协议通信项目中就这么干过,效果很好。

10.4 三种方式对比

为了让你看得更清楚,我整理了一个对比表:

特性 静态分配 动态分配 内存池
分配时机 编译期 运行时 初始化时预分配
分配速度 无开销 较慢(可能涉及系统调用) 极快(O(1))
碎片风险
灵活性
适用状态机规模 小型(状态固定) 中型(状态动态变化) 中小型(状态数量可预估)

10.5 知识体系总览

下面这张图,把三种内存管理方式在状态机中的定位和关系梳理了一下。你可以看到,选择哪种方式,取决于你的状态机规模、切换频率和实时性要求。

状态机内存管理方式对比 静态内存分配 动态内存分配 内存池 编译期分配 · 无碎片 运行时分配 · 碎片风险高 预分配 · O(1)分配 · 低碎片 适用场景 状态固定 数据大小已知 状态动态变化 内存需求不确定 状态数量可预估 实时性要求高 推荐:中小型状态机优先使用内存池

10.6 我的选择建议

说了这么多,到底该怎么选?我个人的经验是:

  • 原型验证阶段:用动态分配,快速迭代,别在内存管理上浪费时间。
  • 产品化阶段:如果状态数量不超过 20 个,直接上内存池。如果状态数量极少且固定,静态分配就够了。
  • 安全关键系统:强制使用静态分配或内存池。动态分配?想都别想。

嗯,最后再啰嗦一句:不管你选哪种方式,一定要在状态机初始化时就把内存分配好,不要在状态切换的「热路径」上做分配操作。这个习惯,能帮你省掉很多半夜被叫起来修 bug 的麻烦。


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