malloc、free 内存泄露怎么办 栈溢出怎么查 怎么处理堆碎片化 为什么要内存对齐 RTOS里任务栈大小怎么算 数组、指针、结构体、联合体内存布局 芯片的Memory Map、SRAM、Flash怎么分布 RTOS任务栈消息队列、内存池这些机制的内存消耗和分配策略、怎么调API
内存就是一片带地址编号的字节数组;程序的一切问题,最后都落到四个问题:在哪?多大?活多久?谁释放?
数学上:N 位地址总线 → 可寻址 2^N 字节。
物理上:像一栋大楼,每个房间有门牌号,房间大小 1 字节。指针就是门牌号。
0. 总纲:先建立内存地图
0.1 程序典型内存分区
text
高地址 +------------------+ | 栈 Stack | SP 通常向下生长 | ↓ | | | | ↑ | | 堆 Heap | 向上生长 +------------------+ | .bss | 未初始化全局/静态,启动清零 +------------------+ | .data | 已初始化全局/静态,启动从 Flash 拷贝 +------------------+ | .rodata | 常量、字符串,通常在 Flash +------------------+ | .text | 代码,在 Flash +------------------+ 低地址
Flash:掉电不丢,放代码、常量、初始值。
SRAM:掉电丢,放栈、堆、全局变量、RTOS 对象。
外设寄存器:也挂在地址空间里,读写寄存器就是读写某个地址。
0.2 四问法
| 问题 | 例子 |
|---|---|
| 在哪? | Flash / SRAM / 栈 / 堆 / 外设 |
| 多大? | 1 字节、4 字节、结构体 12 字节 |
| 活多久? | 函数结束就没、程序结束才没、手动释放 |
| 谁释放? | 栈自动、全局自动、堆必须 free |
1. malloc / free
1.1 第一性原理
堆是一大块 SRAM,malloc 负责从里面切一块给你,free 负责还回去。
它像一个“仓库管理员”,维护空闲块链表。
数学条件:请求 R 字节,需要找到空闲块 B,满足:
B.size≥R+头部开销
如果块很大,就分裂成“给你用的块”和“剩余空闲块”。
关键技术点:
-
malloc 返回的地址必须满足最大对齐要求。
-
free 必须知道这块多大,所以分配块前面通常有隐藏头部。
-
free 不能释放栈、全局、非 malloc 地址。
-
中断里不要 malloc/free,因为可能加锁、耗时、不可重入。
-
嵌入式里长期运行,慎用标准 malloc。
1.2 简易模型代码
(AI生成)
c
typedef struct Block {
size_t size; // 数据区大小
int free; // 1 空闲
struct Block *next; // 下一个块
} Block;
static Block *free_list;
void *my_malloc(size_t n) {
// 1. 对齐 n
// 2. 遍历 free_list 找 size >= n 的块
// 3. 若块很大,分裂
// 4. 返回 Block 后面的数据地址
// 5. 标记 free=0
}
void my_free(void *p) {
// 1. p 往前偏移得到 Block 头
// 2. 标记 free=1
// 3. 合并相邻空闲块,减少碎片
}
真实 malloc 更复杂:分离空闲链表、伙伴系统、TLSF、线程安全等。
1.3 内存泄漏怎么办?
定义:分配了,不再使用,但没 free。
数学:分配速率 λa,释放速率 λf。若长期 λa>λf,未释放量:

最终 SRAM 耗尽,系统死机。
排查思路:
-
监控堆使用量:每次 malloc/free 更新计数。
-
记录分配点:
__FILE__,__LINE__。 -
复现操作,看哪类操作后堆只增不减。
-
代码审查:所有 malloc 是否有对应 free?错误分支是否漏 free?
-
工具:Valgrind、AddressSanitizer、RTOS 堆统计、静态分析。
示例:简单计数钩子
(AI生成)
c
static size_t g_used = 0;
void *dbg_malloc(size_t n) {
void *p = malloc(n);
if (p) g_used += n;
return p;
}
void dbg_free(void *p, size_t n) {
free(p);
g_used -= n;
}
实际中可用宏记录大小和位置。
1.4 堆碎片化
-
内部碎片:给你 100 字节,你只用 90,浪费 10。
-
外部碎片:总空闲 1000 字节,但最大连续只有 50,请求 80 失败。

解决:
-
固定块内存池:同尺寸对象,O(1) 分配,无外部碎片。
-
slab / buddy / TLSF。
-
避免频繁小分配。
-
生命周期分组:一起分配,一起释放。
-
预分配静态数组。
固定块内存池示例:
(AI生成)
c
typedef struct Block {
struct Block *next;
} Block;
typedef struct {
Block *free;
uint8_t *pool;
size_t block_size;
size_t count;
} Pool;
void pool_init(Pool *mp, uint8_t *buf, size_t block_size, size_t count) {
mp->free = NULL;
mp->pool = buf;
mp->block_size = block_size;
mp->count = count;
for (size_t i = 0; i < count; i++) {
Block *b = (Block *)(buf + i * block_size);
b->next = mp->free;
mp->free = b;
}
}
void *pool_alloc(Pool *mp) {
if (!mp->free) return NULL;
Block *b = mp->free;
mp->free = b->next;
return b;
}
void pool_free(Pool *mp, void *p) {
Block *b = (Block *)p;
b->next = mp->free;
mp->free = b;
}
注意:多任务下要加临界区保护。
2. 栈溢出怎么查
2.1 第一性原理
栈是后进先出,函数调用时压栈帧:
text
高地址 +------------------+ | 参数 | | 返回地址 | | 保存的寄存器 | | 局部变量 | | 对齐填充 | +------------------+ 低地址 ← SP
函数返回时 SP 恢复。
如果局部数组太大、递归太深、中断嵌套太多,SP 越过栈边界,就会踩到别的内存。
2.2 查法
-
填充魔数:启动时把栈填
0xCD,运行后看尾部还有多少没被改写。 -
高水位:FreeRTOS 用
uxTaskGetStackHighWaterMark()。 -
MPU 保护页:栈边界设不可访问区,溢出立刻异常。
-
Canary:栈尾放固定值,检查是否被改。
-
静态分析:看最大调用深度、大局部数组。
-
压测:最坏分支、中断全开、通信最忙时测。
(AI生成)
FreeRTOS 示例:
c
void vTask(void *pv) {
UBaseType_t hw = uxTaskGetStackHighWaterMark(NULL);
// hw 是历史最小剩余栈,单位通常是“字”
if (hw < 32) {
// 栈快满了
}
for (;;) {
// ...
}
}
注意:uxTaskGetStackHighWaterMark 返回的是历史最低剩余量,越小越危险。
预防:
-
增大任务栈。
-
大数组改静态或内存池。
-
限制递归。
-
中断用独立栈,或给中断留足嵌套余量。
-
栈边界加保护。
3. 内存对齐
3.1 第一性原理
CPU 不是按字节随便读,它按总线宽度读。
地址 A 对齐到 N:
A mod N=0
例如 4 字节对齐:地址是 0,4,8,12...
不对齐可能导致:
-
多次访问,变慢。
-
总线异常,HardFault。
-
原子操作失败。
-
DMA、外设要求失败。
3.2 结构体布局
(AI生成)
c
struct A {
char c; // 偏移 0
int i; // 偏移 4,中间填充 3 字节
short s; // 偏移 8
}; // 总大小 12,因为最大对齐 4
c
struct B {
char c; // 0
short s; // 2
int i; // 4
}; // 总大小 8
同样成员,顺序不同,大小不同。
规则:
-
每个成员偏移必须是自身对齐的整数倍。
-
结构体总大小必须是最大成员对齐的整数倍。
-
可用
#pragma pack压紧,但可能降低性能或导致异常。
查看偏移:
(AI生成)
c
#include <stddef.h>
printf("%zu\n", offsetof(struct A, i));
协议解析建议:不要直接把网络包强转成结构体。用 memcpy 到本地变量,再转大小端。
4. 数组、指针、结构体、联合体
4.1 数组
数组是连续内存:
(AI生成)
c
int a[4]; // 4 个 int 连续
a[i] 本质:
∗(a+i)∗(a+i)
步长是 sizeof(int)。
4.2 指针
指针存地址,类型决定解引用宽度和步长。
(AI生成)
c
int *p; // p+1 地址加 4 char *q; // q+1 地址加 1
多级指针:
c
int x = 10; int *p = &x; int **pp = &p;
4.3 结构体
成员按声明顺序放,但会插入填充。
第一性原理:为了对齐,空间换时间。
4.4 联合体
所有成员共享同一块内存:
(AI生成)
c
union U {
int i;
char c[10];
double d;
};
大小 = 最大成员大小,再按最大对齐补齐。
上例最大是 char[10] 10 字节,但 double 对齐 8,所以大小通常 16。
联合体可做类型双关,但 C 中更安全用 memcpy。
5. 芯片 Memory Map、SRAM、Flash
5.1 典型分布
(AI生成)
以 Cortex-M 为例:
text
0x0000_0000 启动区 / BootROM / 别名 0x0800_0000 Flash 代码 .text、常量 .rodata、.data 初值 0x2000_0000 SRAM 栈、堆、.data、.bss、RTOS 对象 0x4000_0000 外设寄存器 0xE000_0000 内核外设 NVIC、SysTick
Flash:非易失,读快写慢,擦除按扇区。
SRAM:易失,读写快,容量小。
启动过程:
-
从 Flash 取复位向量。
-
把
.data从 Flash 拷贝到 SRAM。 -
把
.bss清零。 -
设置栈指针 SP。
-
跳 main。
链接脚本片段:
(AI生成)
ld
MEMORY {
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
SRAM (rwx): ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS {
.text : { *(.text*) *(.rodata*) } > FLASH
.data : { *(.data*) } > SRAM AT> FLASH
.bss : { *(.bss*) } > SRAM
}
看 .map 文件能知道每个函数、变量占了多少 Flash/SRAM。
6. RTOS 任务栈大小怎么算
6.1 第一性原理
任务栈要装:
-
任务上下文:寄存器保存区。
-
函数调用链:返回地址、保存寄存器、局部变量。
-
中断嵌套:ISR 栈帧。
-
库函数开销。
-
对齐填充。
-
安全余量。
公式

最坏路径:不是平均路径,是调用最深、局部最大、中断最多的路径。
6.2 实操方法
-
先给经验值:小任务 512~1024 字,复杂任务 2048~4096 字。
-
运行高水位:
uxTaskGetStackHighWaterMark。 -
压测最坏情况。
-
剩余小于 20%~30% 就加栈。
-
注意单位:FreeRTOS
xTaskCreate的栈深度很多端口是“字”,不是字节。32 位机 1 字 = 4 字节。
(AI生成)
示例:
c
xTaskCreate(vTask, "task", 512, NULL, 3, NULL); // 512 可能是 512 字 = 2048 字节
运行检查:
c
UBaseType_t hw = uxTaskGetStackHighWaterMark(NULL); configASSERT(hw > 64);
7. RTOS 消息队列、内存池
7.1 消息队列
队列 = 环形缓冲区 + 头尾指针 + 互斥/临界区。
内存消耗:
-
队列控制块。
-
存储区:
N * item_size。 -
等待链表、事件控制块。
FreeRTOS 队列默认是值拷贝:发送时拷贝 item_size 字节。
如果传指针,指针本身拷贝,但指向的数据生命周期要自己管。
(AI生成)
API 示例:
c
typedef struct { int id; float value; } Msg;
QueueHandle_t q = xQueueCreate(10, sizeof(Msg));
// 任务中发送
Msg m = {1, 3.14f};
xQueueSend(q, &m, portMAX_DELAY);
// 任务中接收
Msg r;
xQueueReceive(q, &r, portMAX_DELAY);
// 中断中发送
BaseType_t woken = pdFALSE;
xQueueSendFromISR(q, &m, &woken);
portYIELD_FROM_ISR(woken);
关键点:
-
中断用
FromISR。 -
超时别乱用
portMAX_DELAY在中断。 -
队列满要处理,否则丢数据。
-
大消息传指针 + 内存池,避免拷贝。
7.2 内存池
RTOS 内存池常用固定块:
(AI生成)
c
Pool mp; uint8_t buf[32 * 16]; pool_init(&mp, buf, 32, 16); void *p = pool_alloc(&mp); pool_free(&mp, p);
策略:
-
固定块:快,无外部碎片,有内部碎片。
-
TLSF:适合变长、实时。
-
Buddy:2 的幂,合并方便。
-
FreeRTOS heap_4:带合并,适合一般动态分配。
-
heap_5:多块内存。
原则:
-
中断不要动态分配。
-
实时任务优先内存池。
-
生命周期一致的对象放一起。
-
监控池使用率。
8. 排查流程图
内存泄漏
text
堆使用持续上升 ↓ 记录 malloc/free 点 ↓ 复现操作,看哪步后只增不减 ↓ 检查错误分支、提前 return、异常路径 ↓ 修复并回归压测
栈溢出
text
HardFault / 数据被改 / 随机死机 ↓ 查任务高水位 ↓ 栈填充魔数,看边界 ↓ MPU 保护页 / Canary ↓ 减少大局部变量、递归、中断嵌套 ↓ 增大栈并压测
9. 实际应用建议
-
嵌入式优先静态分配:全局数组、内存池、队列存储区预分配。
-
动态分配只用于启动阶段:运行中少 malloc/free。
-
中断里绝不用 malloc/free,用
FromISRAPI。 -
每个任务栈留 20%~50% 余量,用高水位持续监控。
-
结构体注意对齐和大小,协议解析用
memcpy。 -
DMA 缓冲区要对齐,注意 Cache 一致性。
-
看
.map文件,知道 Flash/SRAM 谁占了多少。 -
建内存预算表:Flash、SRAM、栈、堆、队列、池各多少。
-
加监控:堆使用峰值、栈高水位、池失败次数。
-
代码规范:谁分配谁释放;错误分支也要释放;队列传指针要明确所有权。
最后一句话总结:
栈是自动的,堆是手动的;全局是永久的,池是可控的。写嵌入式内存代码,先问“在哪、多大、活多久、谁释放”,再动手。
报道山中去,归时每日斜。——皎然《寻陆鸿渐不遇》
329



被折叠的 条评论
为什么被折叠?



