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 内存碎片化测试
碎片化是什么?你想想看,系统运行久了,内存被分配又释放,留下很多小空隙。这时候你想申请一块连续的大内存,明明总空闲内存够,但就是申请不到。
为什么会这样?因为空闲内存被切成了一堆小碎片。
我建议用以下方法测试碎片化程度:
- 随机分配释放测试:模拟真实场景,随机分配不同大小的内存块,随机释放
- 最大块查询测试:每次分配后,查询当前能分配的最大连续块大小
- 长期运行监控:跑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 综合压力测试方案
把上面三个测试整合起来,才是完整的内存压力测试。我个人建议这样安排:
- 第一阶段(2小时):跑内存泄漏检测,反复分配释放固定大小内存
- 第二阶段(4小时):跑碎片化测试,随机大小分配释放
- 第三阶段(24小时):全功能压力测试,所有功能同时跑,监控内存状态
- 第四阶段(72小时):长期稳定性测试,模拟真实使用场景
关键指标:
- 内存泄漏:0容忍,发现一处必须修复
- 碎片化:最大连续块不应低于总堆空间的30%
- 栈使用率:峰值不超过80%,建议控制在60%以内
说实话,内存测试是最耗时的,但也是最值得投入的。我见过太多产品因为内存问题在客户现场翻车,那种代价比开发阶段多花一周测试大得多。
记住一句话:内存问题不会自己消失,只会越积越多,直到系统崩溃。早发现,早解决,这是嵌入式开发的铁律。