2. 静态内存分配:全局变量与静态数组
好,咱们正式开始聊内存管理。第一个要讲的,就是最原始、也最靠谱的方式——静态内存分配。
说白了,就是你在写代码的时候,就把内存安排得明明白白的。编译器在编译阶段就把这些变量安排到了固定的地址,程序跑起来之后,这些地址雷打不动。
2.1 什么是静态内存分配?
静态内存分配,指的是在程序编译链接阶段就确定好的内存分配方式。这些变量要么是全局变量,要么是用 static 关键字修饰的局部变量。它们存放在数据段(.data)或 BSS 段(.bss)中。
我举个例子你就明白了:
// 全局变量,放在 .data 段
uint32_t system_tick = 0;
// 静态全局变量,作用域仅限于本文件
static uint8_t task_priority_table[32];
// 静态局部变量,函数退出后值不丢失
void timer_handler(void) {
static uint32_t call_count = 0;
call_count++;
}
你看,这些变量在程序启动前就已经分配好了。系统 tick 变量、任务优先级表、定时器调用次数——它们的地址在编译时就固定了。
2.2 静态数组:RTOS 里的"硬核"用法
在 RTOS 内核里,静态数组用得特别多。我个人习惯用静态数组来预分配任务栈、消息队列缓冲区、内存池等核心数据结构。
比如,一个简单的任务控制块(TCB)池:
#define MAX_TASKS 16
typedef struct {
uint32_t *stack_ptr;
uint32_t stack_size;
uint8_t priority;
void (*entry)(void *arg);
// ... 其他字段
} tcb_t;
// 静态分配 TCB 数组
static tcb_t task_pool[MAX_TASKS];
// 静态分配每个任务的栈空间
static uint32_t task_stacks[MAX_TASKS][1024];
嗯,这里要注意:task_pool 和 task_stacks 都是在编译时就分配好的。程序一启动,这 16 个 TCB 和 16 个 4KB 的栈空间就已经占好位置了。
2.3 优缺点分析
咱们不搞虚的,直接上干货。静态内存分配到底好在哪、差在哪?
| 优点 | 缺点 |
|---|---|
|
|
我在项目中遇到过这样一个坑:有个同事用静态数组给每个任务分配了 2KB 的栈空间,结果有个任务在极端情况下需要 2.5KB。程序跑着跑着就栈溢出了,查了两天才找到原因。这就是静态分配的典型问题——你得提前预估好所有情况。
我曾经在一个工业控制项目里,因为静态数组开得太大,导致编译后的 .bss 段超过了芯片的 RAM 大小。程序下载进去直接跑不起来。后来我养成了一个习惯:每次改完静态数组大小,都会看一眼 map 文件,确认内存没超。
2.4 适用场景
静态内存分配不是万能的,但在某些场景下,它是最优解。我总结了几种典型场景:
- 系统核心数据结构:比如 TCB 池、就绪队列、定时器列表。这些结构在系统启动时就确定数量,不会动态变化。
- 中断服务程序:ISR 里不能调用 malloc,用静态变量最安全。
- 实时性要求极高的场景:比如电机控制、音频处理,不能容忍动态分配带来的时间不确定性。
- 资源极度受限的 MCU:只有几 KB RAM 的芯片,用动态分配反而增加复杂度。
- 安全关键系统:汽车电子、医疗设备,不允许出现内存泄漏或碎片问题。
你想想看,如果你的 RTOS 跑在一个只有 8KB RAM 的 Cortex-M0 芯片上,你敢用 malloc 吗?反正我不敢。老老实实用静态数组,心里踏实。
在写 RTOS 内核时,我通常把核心数据结构(TCB、队列、信号量)用静态数组预分配好。而对于用户任务创建的栈空间,我会提供一个配置宏,让用户在编译前调整大小。这样既保证了核心的确定性,又给了用户一定的灵活性。
2.5 代码示例:一个简单的静态内存池
最后,我给大家看一个实际 RTOS 内核里常用的静态内存池实现。这个代码我在好几个项目里都用过:
#define MEM_POOL_SIZE 8
#define MEM_BLOCK_SIZE 64
// 静态分配内存池
static uint8_t mem_pool[MEM_POOL_SIZE][MEM_BLOCK_SIZE];
static uint8_t mem_used[MEM_POOL_SIZE] = {0};
// 分配一个块
void* mem_alloc(void) {
for (int i = 0; i < MEM_POOL_SIZE; i++) {
if (!mem_used[i]) {
mem_used[i] = 1;
return (void*)&mem_pool[i][0];
}
}
return NULL; // 没有可用块
}
// 释放一个块
void mem_free(void *ptr) {
for (int i = 0; i < MEM_POOL_SIZE; i++) {
if (ptr == (void*)&mem_pool[i][0]) {
mem_used[i] = 0;
return;
}
}
}
这个实现很简单,但很实用。它没有动态分配的开销,也不会产生碎片。每个块大小固定,分配和释放都是 O(1) 的时间复杂度。我在一个传感器数据采集项目里就用这个方案,跑了三年没出过问题。
好了,静态内存分配就聊到这儿。说白了,它就像你家里的固定电话——虽然不够灵活,但关键时刻绝对靠谱。下一节咱们聊聊动态内存分配,那又是另一番天地了。