本节摘要:FreeRTOS 把堆实现做成五个可替换的源文件,从"只分不还"到"多段合并"覆盖全谱系需求。本节逐个拆解五种方案的内部结构与能力边界:方案一的极简确定性、方案二的历史地位、方案三的系统库包装、方案四的首次适应加相邻合并、方案五的多段注册。中间走读一次完整分配的路径,算清每块的管理开销,最后给出决策表与两类特殊场景的处置。
通用操作系统里,申请内存是"操作系统的事";在裸芯片上,这个责任落到固件自己头上。FreeRTOS 的选择很务实:不给唯一答案,给五个——你按系统的确定性要求、释放需求、内存拓扑来挑。挑的前提是看得懂,本节就来开盖。
方案一的能力清单短得反常:只有分配,没有释放。内部结构简单到可以口述——一个指向"下一个空闲位置"的指针,每次分配就把指针往前挪请求的字节数,挪之前检查是否越界。没有空闲链表、没有合并、没有碎片——因为从不释放,堆里永远只有已分配块,一个挨一个排到底。
听起来残废,实则是一大批真实系统的最优解。其一,任务与内核对象在启动阶段一次创建、终身运行(产品的主干任务结构很少在运行期增减),"释放"这个能力本来就用不上。其二,它的时间行为完全确定:分配耗时与请求大小无关、与历史无关,就是一次加法与比较——硬实时系统最爱的性质。其三,零碎片风险、零元数据开销,代码量最小,审查成本最低。给不需要释放的系统强加释放能力,是给确定性买保险单之外还引入了风险——方案一的存在提醒我们:能力要用需求换,不要用习惯换。
⚠️ 方案一最典型的翻车现场:系统里混着一个"用完就删"的动态任务(比如连接处理任务,来一个连接建一个)。分配只进不出,堆只降不升,连接风暴一来就见底。判断标准很硬:只要存在任何运行期的删除或释放调用,方案一就不合格。
方案二在方案一之上补了释放:空闲块用链表串起来,分配用"最佳适应"(在所有空闲块里找最小的够用者),释放把块还回链表。它的历史贡献是把"可释放"带进内核,但它不做相邻合并——相邻的两个空闲块还是两块,反复分配释放后堆被切得越来越碎。它已被方案四全面取代,官方明确建议新项目不再采用;认识它的意义在于理解"为什么合并如此重要"——5.2 节的碎片演化分析将从它的缺陷讲起。
方案三换了个思路:自己不管理堆,转而包装编译环境自带的系统分配器(标准库的分配与释放函数),包装层只做一件事——用挂起调度器保证线程安全。它的适用场景明确:系统库里已有一套久经考验的分配器(比如带调试版本、带统计钩子),或链接器的堆布局已由其他组件约定。代价是引入了系统库的体积与行为(大小不确定、实现随库而变),且内核配置里的堆总大小不再生效——堆归链接器管了。它是"借外部引擎"的方案,自由度换来的是不可控性,选它通常有具体的工程理由,而不是默认。
方案四是大多数项目的正确起点:可分配、可释放、相邻空闲块自动合并。内部由一条空闲块链表构成,每块带一个小头部(记录块大小与空闲标志)。分配采用首次适应:沿链表找第一个够用的空闲块,用它的前段服务请求,剩余部分仍作为空闲块留在链表(块分裂)。释放时把块标为空闲并回链,然后检查前后邻居——邻居也空闲就融合成一块(合并)。
首次适应对最佳适应(方案二的策略),孰优孰劣值得说透。最佳适应总想找"最小够用"的块,结果是留下大量"谁都用不上"的细小残块,碎片生长更快;首次适应倾向用掉低地址的大块,大请求反而更容易失败——但配合合并机制,它的总碎片表现更稳,且链表遍历通常更快(前面的块常被复用)。内核从方案二换到方案四,等于用工程数据承认了这一点。
走读一次完整分配,把开销算清楚。任务申请一百二十八字节栈:内核换算成字的个数,加上块头(典型两个指针/字的大小,即八字节),向上对齐到对齐边界;沿空闲链表找到首块够用者;若剩余空间不足以再组成"头部加最小块",整块交给请求(内部碎片,最多浪费一个最小块);写入头部、从链表摘除、返回块首地址。这次走读给出三个直接结论:每块有固定管理税(头部加对齐,小块请求的税率惊人——四个字节的对象也要付八字节头部);分裂是碎片的直接来源(大块被啃成小块);分配耗时与空闲块数量相关(碎片多时变慢)——方案四时间上"有界但不恒定",硬实时场景要评估最坏块数。
芯片的随机存储器经常不是一整块:片内静态存储器一块、外挂内存一块、某个低功耗保留区一块。方案三、四都假设堆是一段连续地址,方案五放开这个假设:在方案四的算法之上,增加"多段注册"接口——启动时把若干个(起始地址加长度)内存段按地址升序登记,分配器把它们当作逻辑上一体的空间管理,合并算法天然处理跨段边界的相邻关系(相邻指地址相接,两段地址相接即可融合)。
使用要点有三。其一,段必须按地址升序注册,乱序注册的地址计算会错乱。其二,每段仍付一次结构成本,太碎的段不值得注册。其三,它常与"分而治之"策略搭配:把最快的片内存储给实时关键对象,把大片慢速存储给缓冲区——两个方案五实例各管一摊,比一个混合大池更可控。

场景一:运行期确有增删,但增删模式固定。比如连接任务"来一个建一个"。直接方案四可行,但更稳的做法是对象池:按最大并发数预分配一批任务结构与栈(静态或启动时一次性分配),来连接就从池里领,断开就还池。池化把"任意大小的随机分配"规约成"固定大小的领还",碎片归零、时间恒定,等于在方案四之上再造了一个方案一。第 8 章的性能优化会把它列为内存优化的头号手段。
场景二:堆在多核构建下的归属。对称多核构建下,默认堆被所有核共享,分配需要跨核保护——开销上升且引入核间耦合。更优的布局是按核分堆:每个核一个独立堆(各自的池),跨核共享的对象用专用池加显式保护。这不是内核的强制要求,而是多核内存设计的常识:共享越少,锁越少。
💡 把五个方案想成五种交通工具:方案一是直达巴士(固定路线、绝不回头),方案三是租车(用别人的车,规矩别人定),方案四是私家车(自由但要自己保养防堵),方案五是拼车(路线不直但都能到)。选错交通工具,旅程的痛苦与车价无关。
方案选定只是开始。堆真正的敌人是时间:碎片会生长,峰值会逼近,分配会在最不该失败的时候失败。下一章把镜头对准运行期的堆——怎么观测、怎么设防、要不要干脆放弃动态分配。