23. 实时系统优化:确定性、最坏情况执行时间(WCET)、避免动态内存

做实时系统优化,说白了就是跟「不确定性」死磕。

我早年做车载控制器时,有个血泪教训:系统跑着跑着,某个任务突然多花了 3 毫秒。3 毫秒啊,在普通应用里根本不算事,但在刹车控制里,这 3 毫秒可能就是一场事故。从那以后,我养成了一个习惯——写代码前先问自己:这段代码的最坏情况执行时间(WCET)是多少?

23.1 确定性:实时系统的命根子

什么是确定性?简单说就是:同样的输入,同样的执行路径,每次跑出来的时间几乎一模一样。

你想想看,如果今天跑 1 毫秒,明天跑 10 毫秒,你敢用它做安全关键系统吗?不敢。所以实时系统优化的第一原则就是:消除一切不可预测的因素

核心观点: 确定性比性能更重要。一个慢但确定的系统,比一个快但不稳定的系统更可靠。

我在项目中遇到过最典型的「不确定性杀手」有三个:

  • 动态内存分配——malloc/free 的执行时间不固定
  • 缓存未命中——Cache Miss 带来的延迟抖动
  • 任务抢占——高优先级任务打断低优先级任务

嗯,这里面最要命的,就是动态内存。咱们重点聊聊它。

23.2 最坏情况执行时间(WCET)分析

WCET 分析不是算平均时间,而是找上限。说白了就是:这段代码在最倒霉的情况下,到底要跑多久?

我建议你养成一个习惯:给每个关键函数标注 WCET 注释。比如:

/*
 * 函数: process_sensor_data
 * WCET: 125 us (基于 100MHz CPU, 最坏路径: 分支A + 循环满)
 * 分析工具: 静态WCET分析 + 实测验证
 */
void process_sensor_data(void) {
    for (int i = 0; i < MAX_SENSORS; i++) {
        // 每个传感器处理固定 12.5 us
        read_sensor(i);
        filter_data(i);
        update_output(i);
    }
}

为什么要写这个?因为半年后你回头看代码,根本记不住当初的时序假设。我吃过这个亏——有一次重构代码,把循环次数从固定值改成了变量,结果 WCET 直接翻倍,系统开始丢包。查了两天才发现是这里的问题。

23.2.1 WCET 分析的两种方法

方法 原理 优点 缺点
静态分析 分析代码路径 + 指令周期 不依赖硬件运行,可提前发现 对复杂流水线/缓存建模困难
实测法 跑大量测试用例,取最大值 真实可靠 可能漏掉极端情况

我个人习惯是两者结合:先用静态分析算出理论上限,再用实测法验证。如果实测值远小于静态值,说明你的分析太保守了,可以适当优化。

小技巧: 在关键路径上插入时间戳测量点,用 GPIO 翻转配合示波器看实际执行时间。这个方法比 printf 打印时间戳更精确,而且不影响实时性。

23.3 避免动态内存:从根源消除不确定性

为什么动态内存是实时系统的大敌?原因有三:

  1. 执行时间不确定——malloc 可能触发内存整理,耗时从几微秒到几毫秒不等
  2. 内存碎片——频繁分配释放会导致碎片,后续分配越来越慢
  3. 失败风险——内存不足时返回 NULL,处理不当直接崩溃

我曾经在一个无人机飞控项目里,看到同事用 malloc 分配传感器数据缓冲区。平时跑得好好的,但有一次连续分配了 100 次后,内存碎片导致分配失败,无人机直接失控坠毁。嗯,从那以后,我对动态内存的态度就是:能不用,坚决不用

23.3.1 替代方案:静态分配 + 内存池

静态分配是最简单的替代方案。把所有缓冲区都声明为全局数组:

#define MAX_PACKET_SIZE 1024
#define NUM_PACKET_BUFFERS 8

static uint8_t packet_buffers[NUM_PACKET_BUFFERS][MAX_PACKET_SIZE];
static uint8_t buffer_used[NUM_PACKET_BUFFERS] = {0};

uint8_t* get_packet_buffer(void) {
    for (int i = 0; i < NUM_PACKET_BUFFERS; i++) {
        if (!buffer_used[i]) {
            buffer_used[i] = 1;
            return packet_buffers[i];
        }
    }
    return NULL;  // 所有缓冲区都在使用
}

void release_packet_buffer(uint8_t* buf) {
    for (int i = 0; i < NUM_PACKET_BUFFERS; i++) {
        if (packet_buffers[i] == buf) {
            buffer_used[i] = 0;
            return;
        }
    }
}

你看,这个实现没有 malloc/free,执行时间完全可预测。get_packet_buffer 的最坏情况就是遍历 8 个元素,耗时固定。

注意: 静态分配需要提前估算最大使用量。如果估算不足,系统会拒绝服务。所以设计时要留有余量,我一般按峰值需求的 1.5 倍来分配。

23.3.2 内存池的高级用法

如果固定大小的缓冲区不够灵活,可以用内存池。内存池预分配一大块内存,然后按固定大小切块管理:

typedef struct {
    uint8_t* pool;          // 内存池起始地址
    size_t block_size;      // 每个块的大小
    size_t num_blocks;      // 块数量
    uint8_t* free_list;     // 空闲块链表
} MemoryPool;

// 初始化内存池
void pool_init(MemoryPool* mp, uint8_t* buffer, 
               size_t block_size, size_t num_blocks) {
    mp->pool = buffer;
    mp->block_size = block_size;
    mp->num_blocks = num_blocks;
    
    // 构建空闲链表
    for (size_t i = 0; i < num_blocks - 1; i++) {
        *(uint8_t**)(buffer + i * block_size) = buffer + (i+1) * block_size;
    }
    *(uint8_t**)(buffer + (num_blocks-1) * block_size) = NULL;
    mp->free_list = buffer;
}

// 分配一个块(O(1) 时间)
void* pool_alloc(MemoryPool* mp) {
    if (!mp->free_list) return NULL;
    void* block = mp->free_list;
    mp->free_list = *(uint8_t**)block;
    return block;
}

// 释放一个块(O(1) 时间)
void pool_free(MemoryPool* mp, void* block) {
    *(uint8_t**)block = mp->free_list;
    mp->free_list = (uint8_t*)block;
}

这个内存池的分配和释放都是 O(1) 时间,没有碎片问题。我在多个实时项目里都用这个模式,效果非常好。

23.4 知识体系总览

下面这张图总结了实时系统优化的核心逻辑:

实时系统优化核心逻辑 实时系统优化目标 确定性 WCET 分析 避免动态内存 固定执行路径 无锁/无等待 可预测调度 静态分析 实测验证 注释标注 静态分配 内存池 栈上分配 核心原则:确定性 > 平均性能 用可预测的慢,换不可预测的快

23.5 实战避坑指南

最后,分享几个我踩过的坑:

  • 别信「几乎不会发生」——我曾经觉得某个分支一年也走不到一次,结果上线第三天就触发了。实时系统里,所有可能路径都要算进 WCET。
  • 小心隐式动态内存——有些库函数内部会调用 malloc,比如某些 printf 实现、正则表达式库。用之前一定要查文档确认。
  • 中断服务程序里绝对不要用动态内存——中断上下文里 malloc 可能导致死锁或系统崩溃。我在一个网络驱动里见过这种写法,后果很惨。
  • 静态数组大小要留余量——我一般按峰值需求的 1.5 倍来分配,并且加编译时断言检查:static_assert(MAX_BUFFERS <= POOL_SIZE, "内存池不足");

推荐工具: 如果你用 GCC,可以试试 -fstack-usage 选项,它会输出每个函数的栈使用量。配合 -Wstack-usage=256 可以在栈使用超过 256 字节时报警。这个对实时系统非常有用。

好了,关于实时系统优化的确定性、WCET 和动态内存问题,就聊到这里。记住一句话:在实时系统里,可预测的慢,好过不可预测的快。下次写代码前,先想想这段代码的最坏情况执行时间是多少——这个习惯,能帮你省下无数个调试的夜晚。


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