4. 协议栈基础架构
内存管理,说白了就是RTOS的命根子。你想想看,一个嵌入式设备总共就那么点RAM,协议栈要跑,任务要调度,缓冲区要来回倒腾——谁吃得多、谁吃得快、谁吃完了不吐骨头,全看内存管理这关过不过得去。
我早年做一款工业网关,协议栈跑着跑着就挂了。查了三天,最后发现是动态内存分配把堆给撕碎了。从那以后,我对内存管理就特别较真。今天咱们就把这块掰开揉碎讲清楚。
4.1 动态内存分配:灵活但危险
RTOS里的动态内存分配,核心就是 malloc/free 或者RTOS自带的 pvPortMalloc/vPortFree。好处是灵活——需要多少申请多少,用完就释放。但坑也在这里。
我个人习惯的做法是:只在初始化阶段用动态分配。系统启动时把需要的缓冲区、任务栈、消息队列一口气申请完,之后就不再调malloc了。这样既享受了动态的灵活性,又避开了运行时的风险。
// 我个人推荐的初始化分配模式
void system_init(void) {
// 启动时一次性分配
task_stack = pvPortMalloc(TASK_STACK_SIZE * TASK_COUNT);
msg_pool = pvPortMalloc(MSG_POOL_SIZE);
buf_pool = pvPortMalloc(BUF_POOL_SIZE);
// 分配失败直接断言,不要等到运行时才崩溃
configASSERT(task_stack != NULL);
configASSERT(msg_pool != NULL);
configASSERT(buf_pool != NULL);
// 后续运行时不再调用pvPortMalloc
}
4.2 静态内存池:RTOS的定海神针
静态内存池,说白了就是提前把内存切成固定大小的块,谁用谁领,用完归还。没有碎片,分配时间恒定——这才是RTOS该有的样子。
我在项目中遇到过最典型的场景:协议栈的接收缓冲区。网络包大小虽然不定,但MTU是固定的,所以接收缓冲区用固定大小的内存池最合适。
| 特性 | 动态分配 | 静态内存池 |
|---|---|---|
| 分配时间 | 不确定,依赖空闲链表长度 | O(1),恒定 |
| 内存碎片 | 严重 | 无 |
| 适用场景 | 初始化阶段、非实时路径 | 中断、实时任务、高频分配 |
| 内存利用率 | 高(按需分配) | 中等(存在内部碎片) |
实现一个内存池其实不复杂。核心就是维护一个空闲链表,分配时从链表头取一个,释放时插回去。嗯,这里要注意:释放操作一定要关中断,否则被任务切换打断就乱套了。
// 简单的内存池实现示意
typedef struct {
void *pool_start; // 池起始地址
uint32_t block_size; // 每个块的大小
uint32_t block_count; // 块数量
void *free_list; // 空闲链表头
} mem_pool_t;
void* pool_alloc(mem_pool_t *pool) {
void *block = NULL;
taskENTER_CRITICAL();
if (pool->free_list != NULL) {
block = pool->free_list;
pool->free_list = *(void**)block; // 链表头指向下一个空闲块
}
taskEXIT_CRITICAL();
return block;
}
void pool_free(mem_pool_t *pool, void *block) {
taskENTER_CRITICAL();
*(void**)block = pool->free_list;
pool->free_list = block;
taskEXIT_CRITICAL();
}
4.3 内存碎片问题:看不见的杀手
碎片分两种:外部碎片和内部碎片。外部碎片是动态分配搞出来的——你申请了又释放,释放了又申请,堆上就出现了一堆小洞,每个洞都不够大,但加起来空间不少。内部碎片是内存池的锅——块大小固定,你只需要50字节,但块大小是64字节,那14字节就浪费了。
为什么会这样?因为不同大小的分配和释放顺序是随机的。协议栈里,TCP段的长度、应用层消息的大小、DNS查询的响应,全都不一样。这些大小不一的内存块在堆上反复分配释放,就像往一个瓶子里倒不同大小的石子——最后全是缝隙。
4.4 对策:组合拳才是王道
光说问题不给解法,那是耍流氓。我这些年总结了一套组合拳,分享给你。
第一拳:分层管理。 把内存需求按大小分档。比如:64字节、256字节、1KB、4KB,各建一个内存池。申请时根据大小就近分配。这样既减少了内部碎片,又避免了外部碎片。
第二拳:伙伴算法。 如果确实需要动态大小,用伙伴算法代替标准malloc。伙伴算法把内存按2的幂次分割,释放时合并相邻空闲块。碎片率比标准malloc低得多。
第三拳:内存回收监控。 加一个看门狗任务,定期检查内存使用率。如果碎片率超过阈值,触发一次全局整理——把关键数据搬到新位置,释放旧块。
第四拳:协议栈专用内存。 把协议栈的内存和应用程序的内存彻底隔离。协议栈内部用静态内存池,应用层用动态分配。这样协议栈的实时性不受影响,应用层的灵活性也不丢。
4.5 实战中的选择建议
说了这么多,到底怎么选?我一般按这个原则来:
- 中断服务程序:只用静态内存池,分配时间必须O(1)
- 高优先级实时任务:优先用静态内存池,必要时用伙伴算法
- 低优先级后台任务:可以用动态分配,但要加超时保护
- 初始化代码:随便用,反正只跑一次