5.1 实时内存分配策略


5.1 实时内存分配策略

本节摘要:实时系统的内存供给有四条来源路线:编译期静态、任务栈、固定尺寸静态池、通用堆。本节逐一分析各自的时间确定性与风险,讲清 FreeRTOS 五种堆管理器的行为差异,并给出「什么需求走哪条路」的决策规则。

别以为「实时系统禁用动态内存」意味着嵌入式开发者比桌面程序员多了条条框框少了一条路。实际情况相反:嵌入式把内存分配这件事拆成了多条专业路线,每条路线在自己的适用面上都比通用堆更确定、更省、更快。真正的损失是拿着通用堆这一把锤子去敲所有钉子。本节把这些路线一次摊清。

路线一:全静态,编译期见底

把所有任务栈、内核对象、缓冲区都声明成全局或编译期分配,运行期不存在任何分配动作。这是资源最紧张的项目的默认选择,好处有三:内存占用在链接输出的映射文件里一目了然;不存在分配失败的运行期故障模式;分配耗时严格为零。代价是灵活性——对象数量在编译期定死,规格变更要重新编译。实践中的折中是「静态为主,按规格上限预留」:缓冲区按最大并发量声明,宁多几 KB,不担一分险。

路线二:任务栈,启动时划清

每个任务的栈在创建时一次性划出,此后不再增长收缩。这部分在第 3.1 节已经详细处理过——预算三步法(估算、水印实测、溢出钩子)就是栈的全部管理要点。本节只需补一句立场:栈不属于「动态内存」,它是确定性分配的一种,预算做实的栈是实时系统里最安心的内存。

路线三:固定尺寸静态池

池是一组尺寸相同、编译期定数的内存块加一个取还接口。取与还都是常数时间——从链表头摘一块、还回链表头,几次指针操作,无搜索、无合并。它适合「同一类对象的反复取用」:报文缓冲、图像帧、协议上下文。池深按最大并发声明,取空的行为由使用者定义(等待、拒绝或降级),与队列满的处理一脉相承:

/* 简化实现:看懂结构即可,正式项目用内核自带组件 */ typedef struct Block { struct Block *next; } Block; static Block s_pool[POOL_DEPTH]; static Block *s_freeHead = s_pool; /* 初始化时串成空闲链 */ void *Pool_Take(void) /* 取块:常数时间 */ { Block *b = s_freeHead; if (b) s_freeHead = b->next; /* 摘头 */ return b; } void Pool_Give(void *p) /* 还块:常数时间 */ { ((Block *)p)->next = s_freeHead; s_freeHead = (Block *)p; /* 压回头部 */ }

第 4.2 节的「池发筹码、队列传指针」正是它的应用场景:中断里取块装数据、指针进队列、任务用完还块,全程无一次通用堆分配。

路线四:通用堆与五种管理器

确实需要变长分配时(解析配置、动态加载协议),才轮到通用堆,而且要选对管理器。FreeRTOS 内置五种堆实现,行为差异很大:第一种只分配不回收,适合「启动时分配一次、终身持有」的场景,最简单也最防碎片;第二种支持回收但永不合并相邻空闲块,会积累外部碎片,已少用;第三种是标准库接口的封装,线程安全性取决于所包装的库实现,实时性最差;第四种在回收时合并相邻空闲块,是通用场景的默认选择;第五种把多个不连续内存区域组成一个堆,适合内存物理上分散的芯片。选型口诀:能不回收就不回收(第一种),要回收就合并(第四种),内存分散才上(第五种)。

图:四种来源的确定性与适用面对比

图:四种来源的确定性与适用面对比

决策规则与一条红线

把四条路线合成决策流程:先问数量与尺寸是否编译期可知,可知则静态;同尺寸反复取用则池;确需变长才进堆,且进堆的对象要满足两条——分配发生在启动期或非实时任务里,生命周期由单一所有者管理。红线只有一条:中断服务程序与硬实时任务的截止期路径上,禁止通用堆分配。这一条与 4.2 节的红线(队列策略写进文档)并列为第五章最重要的两句话。

本节要点回顾

  • 四条路线按确定性排序:编译期静态、任务栈、静态池、通用堆;
  • 全静态的价值不是省内存,是把「分配失败」从运行期故障清单里删除;
  • 静态池取还均为常数时间,是中断与硬实时路径的默认缓冲来源;
  • 五种堆管理器按「回收与合并」能力区分,能不回收就不回收;
  • 通用堆仅限启动期或非实时任务,且对象须有单一所有者;
  • 截止期路径禁通用堆,是内存侧不可谈判的红线。

常见问题

问:静态池的块尺寸怎么定? 按家族对象的实际最大尺寸对齐到访问粒度,宁可略大于最大需求,也不要按平均值——块不够用是设计错误,块略大只是常数浪费。分档时每一档独立成池,档间不通用,语义才清晰。

问:堆一型(只分配不回收)会不会太极端? 它是「启动期分配、终身持有」模式的最优解:不回收就没有合并逻辑,分配是纯指针推进,快且天然抗碎片。运行期确需少量动态行为的对象,单独给它们一个小的回收型堆实例,隔离风险。

问:怎么知道项目里谁在偷偷用堆? 重定向堆入口为自定义版本,打上调用点统计——多数项目会发现三到五个隐蔽调用者(格式化、日志、协议解析)。发现后逐一改为静态或池供给,堆使用就收敛了。

问:栈与堆共享一块内存区的设计可行吗? 可行且常见(两者相向生长),但要对水印做双向监控,防止穿越相撞。资源紧张的芯片值得做,资源宽裕时分开更省心。

问:池深度怎么算? 与队列深度同法:生产峰值速率乘允许积压时长,再留五成余量。池空计数持续增长就是深度不足的直接证据,比任何理论推算都诚实。

问:多块内存的芯片怎么安排供给? 高速内存优先给中断与 DMA 缓冲、关键任务栈;大容量慢内存给日志、显示帧这类宽容对象。供给路线与物理位置是两个正交决策,先定路线再定位置。

一次供给改造的完整记录

把一个真实改造浓缩成步骤,作为本节的实操收尾。某项目的日志模块最初用堆动态分配每条日志缓冲,长时间运行后偶发分配失败。改造分四步:统计原行为,确认日志为等长小对象、生产速率为每秒多条、消费为专职任务;选定供给方案为深度六十四的静态池加队列传递指针;实现耗尽策略为「池空则丢弃并计数」,与产品确认日志可丢;上线后观察一周,丢弃计数恒为零,堆分配调用归零。改造全程没有调过一行「分配算法」——供给路线换了,问题就不存在了。这类改造也最能体现供给路线思维的复利:第一次花半天改的是日志,第二次遇到同类问题,团队会自己按同一套方法改掉协议解析与显示缓冲。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U