第三章 内存池与静态内存分配:避免动态内存碎片
说到HUD的内存管理,我第一个想聊的就是动态内存碎片问题。你想想看,HUD系统里图像数据、帧缓冲、字符纹理,哪个不是吃内存的大户?如果放任动态分配满天飞,系统跑个几小时就开始卡顿,甚至直接崩掉——我在项目里真遇到过这种事。
说白了,嵌入式系统最怕的就是内存碎片。动态分配就像在沙滩上挖坑,挖了填、填了挖,最后沙滩上全是小坑,大块数据就放不下了。HUD这种实时性要求极高的系统,绝对不能容忍这种情况。
3.1 固定大小内存池实现
我个人习惯的做法是:提前规划好所有内存需求。HUD里哪些数据是固定大小的?帧缓冲、字符位图、告警图标,这些都可以用固定大小内存池来管理。
来看一个我常用的实现:
/* 固定大小内存池结构 */
typedef struct {
void *pool_start; /* 内存池起始地址 */
size_t block_size; /* 每个块的大小 */
uint32_t total_blocks; /* 总块数 */
uint32_t free_list; /* 空闲链表头索引 */
uint8_t *bitmap; /* 分配位图 */
} fix_pool_t;
/* 初始化内存池 */
int fix_pool_init(fix_pool_t *pool, void *mem,
size_t blk_size, uint32_t blk_num) {
pool->pool_start = mem;
pool->block_size = blk_size;
pool->total_blocks = blk_num;
pool->free_list = 0;
pool->bitmap = (uint8_t *)mem;
/* 构建空闲链表 */
uint8_t *ptr = (uint8_t *)mem + blk_num; /* 位图之后 */
for (uint32_t i = 0; i < blk_num - 1; i++) {
*(uint32_t *)(ptr + i * blk_size) = i + 1;
}
*(uint32_t *)(ptr + (blk_num - 1) * blk_size) = 0xFFFFFFFF;
return 0;
}
/* 分配一个块 */
void *fix_pool_alloc(fix_pool_t *pool) {
if (pool->free_list == 0xFFFFFFFF) return NULL;
uint32_t idx = pool->free_list;
uint8_t *base = (uint8_t *)pool->pool_start + pool->total_blocks;
void *block = base + idx * pool->block_size;
pool->free_list = *(uint32_t *)block;
pool->bitmap[idx / 8] |= (1 << (idx % 8));
return block;
}
/* 释放一个块 */
void fix_pool_free(fix_pool_t *pool, void *block) {
uint8_t *base = (uint8_t *)pool->pool_start + pool->total_blocks;
uint32_t idx = ((uint8_t *)block - base) / pool->block_size;
*(uint32_t *)block = pool->free_list;
pool->free_list = idx;
pool->bitmap[idx / 8] &= ~(1 << (idx % 8));
}
3.2 伙伴算法在HUD中的应用
固定大小池有个局限——它只能处理单一尺寸。HUD里有些数据大小是变化的,比如动态生成的导航箭头、变长的文本字符串。这时候伙伴算法就派上用场了。
伙伴算法的核心思想:把内存按2的幂次分割。分配时找最小能满足需求的块,释放时检查相邻块是否空闲,是的话就合并。这样既能灵活分配,又能控制碎片。
我在HUD项目中是这样用的:
/* 伙伴算法内存管理器 */
#define MIN_BLOCK_SIZE 32 /* 最小块32字节 */
#define MAX_ORDER 10 /* 最大阶数:32 * 2^10 = 32KB */
typedef struct {
void *base_addr;
uint32_t total_size;
list_t free_lists[MAX_ORDER + 1]; /* 每个阶的空闲链表 */
uint32_t alloc_count;
} buddy_allocator_t;
/* 分配内存 */
void *buddy_alloc(buddy_allocator_t *alloc, size_t size) {
/* 计算需要的阶数 */
uint32_t order = 0;
size_t req_size = MIN_BLOCK_SIZE;
while (req_size < size && order < MAX_ORDER) {
req_size <<= 1;
order++;
}
/* 查找可用块 */
uint32_t current = order;
while (current <= MAX_ORDER) {
if (!list_empty(&alloc->free_lists[current])) {
break;
}
current++;
}
if (current > MAX_ORDER) return NULL;
/* 从current阶取出块,如果大于需求则分割 */
block_t *block = list_pop(&alloc->free_lists[current]);
while (current > order) {
current--;
/* 分割:将块分成两个伙伴 */
block_t *buddy = (block_t *)((uint8_t *)block +
(MIN_BLOCK_SIZE << current));
buddy->order = current;
list_add(&alloc->free_lists[current], buddy);
}
block->order = order;
block->used = 1;
alloc->alloc_count++;
return (void *)((uint8_t *)block + sizeof(block_t));
}
关键点:伙伴算法在HUD里特别适合处理图像层数据。比如一个告警图标需要2KB,一个导航箭头需要512B,一个文本层需要4KB。这些大小都是2的幂次,伙伴算法分配起来几乎零碎片。
3.3 栈空间预分配策略
嗯,这里要重点说说栈。HUD系统里任务多、中断多,栈溢出是常见问题。我见过一个案例:某个HUD在高温环境下运行,栈空间因为函数调用深度变化而溢出,导致图像撕裂——这问题排查了整整两周。
我的策略很简单:静态分析 + 动态监测 + 冗余预留。
先看静态分析:
/* 栈使用分析工具(伪代码) */
typedef struct {
const char *task_name;
uint32_t stack_size; /* 配置的栈大小 */
uint32_t peak_usage; /* 峰值使用量 */
uint32_t reserved; /* 预留空间 */
uint8_t overflow_flag; /* 溢出标志 */
} stack_info_t;
/* 栈空间预分配表 */
static stack_info_t hud_stack_table[] = {
{"display_task", 4096, 0, 1024, 0}, /* 显示任务 */
{"graphics_task", 8192, 0, 2048, 0}, /* 图形渲染 */
{"input_task", 2048, 0, 512, 0}, /* 输入处理 */
{"sensor_task", 1024, 0, 256, 0}, /* 传感器采集 */
{"comm_task", 2048, 0, 512, 0}, /* 通信任务 */
};
动态监测怎么做?我习惯在栈底填充一个特殊模式(比如0xDEADBEEF),然后定时检查这个模式是否被覆盖:
/* 栈溢出检测 */
void stack_watermark_check(stack_info_t *info) {
uint32_t *stack_bottom = (uint32_t *)get_stack_base(info->task_name);
uint32_t pattern = 0xDEADBEEF;
uint32_t count = 0;
/* 从栈底向上扫描,统计未被覆盖的字节数 */
while (*stack_bottom == pattern) {
count += 4;
stack_bottom++;
if (count > info->reserved) break;
}
uint32_t used = info->stack_size - count;
if (used > info->peak_usage) {
info->peak_usage = used;
}
/* 如果预留空间被侵蚀,触发告警 */
if (count < info->reserved) {
info->overflow_flag = 1;
/* 触发系统告警或记录日志 */
}
}
3.4 三种策略的对比与选择
这三种方法各有适用场景。我整理了一个对比表,方便你选型:
| 策略 | 适用场景 | 碎片控制 | 分配速度 | 灵活性 |
|---|---|---|---|---|
| 固定大小内存池 | 帧缓冲、字符位图、图标 | 零碎片 | O(1) | 低 |
| 伙伴算法 | 图像层、动态文本 | 低碎片 | O(log N) | 中 |
| 栈预分配 | 任务栈、中断栈 | 无碎片 | 编译时确定 | 低 |
实际项目中,我通常三种混用:
- 固定大小池:管理所有固定尺寸的数据,比如16x16图标、32x32字符
- 伙伴算法:处理变长数据,比如动态生成的导航路径
- 栈预分配:每个任务独立栈空间,加上水印检测
这样组合下来,HUD系统的内存碎片率基本能控制在1%以下。我上一个项目跑了连续72小时压力测试,内存分配/释放循环了上百万次,没有出现一次分配失败。
总结一句话:HUD的内存管理,核心就是「提前规划、静态为主、动态为辅」。别等到运行时再去malloc/free,那是在给自己挖坑。我见过太多项目因为动态内存问题返工,真的不值得。
下一章我会讲HUD的实时调度与任务优先级设计,到时候聊聊怎么让图像渲染和传感器采集和谐共处。嗯,那个话题更有意思。