4、内存压力测试:内存泄漏检测、内存碎片化测试、堆栈溢出测试

内存问题,是嵌入式系统里最让人头疼的「隐形杀手」。

我见过太多项目,功能跑起来好好的,一跑几天就莫名其妙死机。查来查去,最后发现是内存泄漏。说实话,这种问题比逻辑bug难抓多了。今天咱们就把内存压力测试的三个核心方向掰开揉碎讲清楚。

4.1 内存泄漏检测

内存泄漏说白了就是「借了不还」。你malloc了一块内存,用完了没free,这块内存就永远丢了。一次两次没事,跑上几天几夜,系统内存被慢慢吃光,最终崩溃。

我个人习惯,在项目中会做两件事来防泄漏:

  • 静态代码审查:每次malloc必须配对free,这是铁律
  • 动态检测工具:用工具跑压力测试,让泄漏现形

我常用的检测手段是这样的:

方法一:内存钩子(Hook)

重写malloc/free,记录每次分配和释放的地址、大小、调用位置。跑完测试后,检查哪些地址没被释放。

// 简易内存泄漏检测示例
#include <stdio.h>
#include <stdlib.h>

#define MAX_RECORDS 1000

typedef struct {
    void *addr;
    size_t size;
    const char *file;
    int line;
} MemRecord;

MemRecord records[MAX_RECORDS];
int record_count = 0;

void *my_malloc(size_t size, const char *file, int line) {
    void *ptr = malloc(size);
    if (ptr && record_count < MAX_RECORDS) {
        records[record_count].addr = ptr;
        records[record_count].size = size;
        records[record_count].file = file;
        records[record_count].line = line;
        record_count++;
    }
    return ptr;
}

void my_free(void *ptr) {
    int found = 0;
    for (int i = 0; i < record_count; i++) {
        if (records[i].addr == ptr) {
            // 标记为已释放
            records[i].addr = NULL;
            found = 1;
            break;
        }
    }
    if (!found) {
        printf("警告:释放未记录的内存 %p\n", ptr);
    }
    free(ptr);
}

void check_leaks() {
    printf("\n===== 内存泄漏检测报告 =====\n");
    int leak_count = 0;
    for (int i = 0; i < record_count; i++) {
        if (records[i].addr != NULL) {
            printf("泄漏: %p, 大小 %zu 字节, 分配于 %s:%d\n",
                   records[i].addr, records[i].size,
                   records[i].file, records[i].line);
            leak_count++;
        }
    }
    if (leak_count == 0) {
        printf("恭喜!未检测到内存泄漏。\n");
    } else {
        printf("共检测到 %d 处内存泄漏。\n", leak_count);
    }
}

// 使用宏替换标准函数
#define malloc(s) my_malloc(s, __FILE__, __LINE__)
#define free(p) my_free(p)

我的经验:在项目早期就植入这个钩子,比后期排查省事十倍。我曾经在一个通信协议栈里,靠这个工具抓到了7处泄漏,其中一处只泄漏了16字节,但每秒钟泄漏一次,跑一天就吃掉1.3MB内存。

4.2 内存碎片化测试

碎片化是什么?你想想看,系统运行久了,内存被分配又释放,留下很多小空隙。这时候你想申请一块连续的大内存,明明总空闲内存够,但就是申请不到。

为什么会这样?因为空闲内存被切成了一堆小碎片。

我建议用以下方法测试碎片化程度:

  1. 随机分配释放测试:模拟真实场景,随机分配不同大小的内存块,随机释放
  2. 最大块查询测试:每次分配后,查询当前能分配的最大连续块大小
  3. 长期运行监控:跑72小时以上,观察最大块的变化趋势
// 内存碎片化测试示例
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define TEST_ITERATIONS 10000
#define MAX_BLOCK_SIZE 1024

void fragmentation_test() {
    void *blocks[100] = {NULL};
    int block_count = 0;
    int max_continuous = 0;
    
    srand(time(NULL));
    
    for (int i = 0; i < TEST_ITERATIONS; i++) {
        int action = rand() % 3;  // 0:分配 1:释放 2:查询
        
        if (action == 0 && block_count < 100) {
            // 随机分配 16-1024 字节
            int size = (rand() % MAX_BLOCK_SIZE) + 16;
            blocks[block_count] = malloc(size);
            if (blocks[block_count]) {
                block_count++;
            }
        } else if (action == 1 && block_count > 0) {
            // 随机释放一个块
            int idx = rand() % block_count;
            if (blocks[idx]) {
                free(blocks[idx]);
                blocks[idx] = NULL;
            }
        } else if (action == 2) {
            // 查询最大连续块
            // 这里用二分法试探
            int low = 0, high = 1024 * 1024; // 1MB
            while (low < high) {
                int mid = (low + high + 1) / 2;
                void *test = malloc(mid);
                if (test) {
                    free(test);
                    low = mid;
                } else {
                    high = mid - 1;
                }
            }
            max_continuous = low;
            printf("迭代 %d: 最大连续块 = %d 字节\n", i, max_continuous);
        }
    }
    
    // 清理
    for (int i = 0; i < block_count; i++) {
        if (blocks[i]) free(blocks[i]);
    }
}

注意:碎片化测试一定要在目标硬件上跑。我在模拟器上测得好好的,移植到真实芯片上,内存布局不同,碎片化程度天差地别。

4.3 堆栈溢出测试

堆栈溢出,嗯,这个我太熟了。刚入行时写了一个递归函数,忘记加终止条件,结果栈空间被吃光,系统直接跑飞。从那以后,我对栈的使用就特别敏感。

堆栈溢出测试主要做三件事:

测试项目 方法 检测指标
栈深度测试 填充特殊模式,运行后检查覆盖情况 最大栈使用量
递归深度测试 故意调用深层递归 触发HardFault的深度值
中断嵌套测试 高频中断嵌套调用 中断栈是否溢出

我常用的栈溢出检测手法:

// 栈溢出检测 - 填充法
#include <stdio.h>
#include <string.h>

// 栈空间定义(假设4KB)
#define STACK_SIZE 4096
uint8_t stack[STACK_SIZE];
uint32_t *stack_top;

// 栈填充模式
#define STACK_PATTERN 0xDEADBEEF

void stack_init() {
    // 将整个栈填充为特定模式
    for (int i = 0; i < STACK_SIZE / 4; i++) {
        ((uint32_t *)stack)[i] = STACK_PATTERN;
    }
    stack_top = (uint32_t *)(stack + STACK_SIZE);
}

uint32_t check_stack_usage() {
    uint32_t used = 0;
    // 从栈底开始找第一个未被覆盖的模式
    for (int i = 0; i < STACK_SIZE / 4; i++) {
        if (((uint32_t *)stack)[i] != STACK_PATTERN) {
            used = (STACK_SIZE / 4 - i) * 4;
            break;
        }
    }
    return used;
}

// 在任务切换或空闲时调用
void stack_monitor_task() {
    uint32_t usage = check_stack_usage();
    uint32_t percent = (usage * 100) / STACK_SIZE;
    
    printf("栈使用: %d 字节 (%d%%)\n", usage, percent);
    
    if (percent > 80) {
        printf("警告:栈使用率超过80%!\n");
    }
    if (percent > 95) {
        printf("危险:栈即将溢出!\n");
    }
}

避坑指南:我曾经在一个RTOS项目里,把每个任务的栈都设成512字节,觉得够用了。结果跑压力测试时,一个任务里调用了三层函数加一个中断,栈直接爆了。后来我养成了习惯:每个任务至少留30%的栈余量,并且在开发阶段打开栈溢出检测。

4.4 综合压力测试方案

把上面三个测试整合起来,才是完整的内存压力测试。我个人建议这样安排:

  1. 第一阶段(2小时):跑内存泄漏检测,反复分配释放固定大小内存
  2. 第二阶段(4小时):跑碎片化测试,随机大小分配释放
  3. 第三阶段(24小时):全功能压力测试,所有功能同时跑,监控内存状态
  4. 第四阶段(72小时):长期稳定性测试,模拟真实使用场景

关键指标

  • 内存泄漏:0容忍,发现一处必须修复
  • 碎片化:最大连续块不应低于总堆空间的30%
  • 栈使用率:峰值不超过80%,建议控制在60%以内

说实话,内存测试是最耗时的,但也是最值得投入的。我见过太多产品因为内存问题在客户现场翻车,那种代价比开发阶段多花一周测试大得多。

记住一句话:内存问题不会自己消失,只会越积越多,直到系统崩溃。早发现,早解决,这是嵌入式开发的铁律。