嵌入式面试真题第 12 题:资源受限长期运行系统的确定性内存管理与内存池架构设计

问题
在资源受限或对实时性、可靠性有严格要求的系统中,运行期通常同时存在多种内存需求:任务和协议对象的创建销毁、通信报文收发、音视频或传感器数据流、日志与诊断记录、文件系统缓存、DMA 缓冲、临时算法工作区,以及不同模块之间的零拷贝传递。
如果所有需求都直接依赖通用 malloc/free,系统可能在短期测试中表现正常,但在长时间运行、突发流量、异常重连、并发超时、模块反复启停或错误恢复后,出现外部碎片、内存泄漏、分配延迟抖动、优先级反转、关键路径资源被非关键模块耗尽等问题。即使"剩余总内存"看起来仍然充足,也可能因为缺少满足尺寸和对齐要求的连续块而分配失败。
请设计一套适用于裸机、RTOS、嵌入式 Linux 用户态组件以及其他受限运行环境的通用内存管理架构。要求说明:
- 如何从系统需求和对象生命周期出发选择静态分配、固定块池、分级尺寸池、专用对象池、环形缓冲、Arena/Region、Buddy、TLSF 或普通堆。
- 如何从架构上避免外部碎片,而不是只在崩溃后扩大堆空间。
- 如何处理并发、ISR、DMA、Cache 一致性、内存区域隔离、零拷贝所有权和引用计数。
- 如何量化每个池的块大小和块数量,定义池耗尽策略,并保证关键业务不会被日志、遥测或后台任务拖垮。
- 如何建立统计、越界检测、泄漏检测、故障注入和长稳测试体系。
- 有哪些成熟的开源实现或标准接口可以直接使用或参考,它们分别解决了什么问题。
回答
结论:不要把"换一个更好的 malloc"当作完整答案。通用且可靠的方案应是分层的确定性内存架构:
- 生命周期固定的对象采用链接期或启动期静态分配。
- 数量有上界、尺寸固定的对象采用专用对象池或固定块内存池。
- 尺寸有限但有若干离散档位的请求采用分级尺寸池。
- 连续数据流采用环形缓冲或预分配分片链,不为每帧反复申请和释放。
- 生命周期一致的一组临时对象采用 Arena/Region,一次申请、整体回收。
- 只有无法提前归类的少量非关键请求才进入受控通用堆;若实时性要求较高,可考虑 TLSF 等有界时间分配器。
- 关键路径、ISR、DMA 和错误恢复路径使用独立池或保留配额,不能与日志、UI、后台同步等非关键模块共享最后一块内存。
- 所有池必须具备容量预算、峰值统计、失败计数、持有时长和所有权诊断;池耗尽是可设计的运行状态,而不是不可预期的系统崩溃。
真正需要消除的不是"所有形式的动态申请",而是无边界、无分类、无所有权、无失败策略、无观测能力的动态申请。
先澄清:内存碎片不是一个单一问题
讨论内存池之前,必须区分几类经常混在一起的问题。
外部碎片
外部碎片指空闲内存被分散成多个不连续区间。空闲总量可能足够,但不存在满足请求尺寸的连续块。
text
已用 16B | 空闲 12B | 已用 32B | 空闲 20B | 已用 8B | 空闲 24B
空闲总量 = 56B
申请 32B 仍然失败,因为最大连续空闲块只有 24B
固定块池不会产生池内外部碎片,因为所有块尺寸相同,任意释放块都可以满足同一池的下一次申请。
内部碎片
内部碎片指实际占用块大于业务请求。例如请求 33B,但从 64B 池取块,浪费 31B。分级尺寸池消除了外部碎片,却会引入可量化的内部碎片。
text
内部浪费率 = (实际块大小 - 请求大小) / 实际块大小
内部碎片不是一定要"归零",而是要通过尺寸档位设计和真实分配直方图控制在预算内。
泄漏与长期持有
如果对象没有归还、引用计数无法归零、异常分支遗漏释放,池同样会耗尽。固定池只能消除碎片,不能自动消除泄漏。
峰值并发超过容量
即使每个对象最终都会释放,只要同一时间的活动对象数量超过池容量,申请仍会失败。这不是碎片,而是容量模型错误、生产消费失衡或缺少背压。
分配时间不可预测
通用堆可能需要遍历空闲链表、拆分块、合并相邻块或进入锁竞争。系统即使从不耗尽,也可能在实时路径出现不可接受的长尾延迟。
因此,设计目标应同时覆盖空间确定性、时间确定性、所有权正确性、并发安全和失败可控性。
设计目标
一套可长期运行的内存管理体系至少要满足以下目标。
| 目标 | 设计含义 |
|---|---|
| 空间上界明确 | 每个模块、对象类型和数据通道有明确预算,不能无限侵占全局内存。 |
| 分配时间可界定 | 关键路径优先使用 O(1) 或可严格估计的申请与释放操作。 |
| 外部碎片可避免 | 高频、长期、交错生命周期对象不进入普通可变块堆。 |
| 故障隔离 | 非关键池耗尽不能直接耗尽控制、通信恢复或安全停机所需内存。 |
| 并发模型清晰 | 明确单生产者/单消费者、多线程、ISR 与任务上下文的访问规则。 |
| 所有权可追踪 | 每个块只有一个明确所有者,或采用受控引用计数,不允许模糊共享。 |
| 失败可处理 | 每种池耗尽时都有返回错误、等待、丢弃、降级、复位通道等策略。 |
| 可观测 | 能看到当前占用、历史峰值、失败次数、最长持有时间和异常调用点。 |
| 可验证 | 能通过压力测试、随机序列、故障注入和长稳测试证明设计成立。 |
总体架构
推荐把内存管理拆成"静态内存布局、分配策略层、对象所有权层、监控与故障策略层"四个部分,而不是只提供一个 mem_alloc(size)。
#mermaid-svg-izCSbY5aYnkI3qbb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-izCSbY5aYnkI3qbb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-izCSbY5aYnkI3qbb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-izCSbY5aYnkI3qbb .error-icon{fill:#552222;}#mermaid-svg-izCSbY5aYnkI3qbb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-izCSbY5aYnkI3qbb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-izCSbY5aYnkI3qbb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-izCSbY5aYnkI3qbb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-izCSbY5aYnkI3qbb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-izCSbY5aYnkI3qbb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-izCSbY5aYnkI3qbb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-izCSbY5aYnkI3qbb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-izCSbY5aYnkI3qbb .marker.cross{stroke:#333333;}#mermaid-svg-izCSbY5aYnkI3qbb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-izCSbY5aYnkI3qbb p{margin:0;}#mermaid-svg-izCSbY5aYnkI3qbb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-izCSbY5aYnkI3qbb .cluster-label text{fill:#333;}#mermaid-svg-izCSbY5aYnkI3qbb .cluster-label span{color:#333;}#mermaid-svg-izCSbY5aYnkI3qbb .cluster-label span p{background-color:transparent;}#mermaid-svg-izCSbY5aYnkI3qbb .label text,#mermaid-svg-izCSbY5aYnkI3qbb span{fill:#333;color:#333;}#mermaid-svg-izCSbY5aYnkI3qbb .node rect,#mermaid-svg-izCSbY5aYnkI3qbb .node circle,#mermaid-svg-izCSbY5aYnkI3qbb .node ellipse,#mermaid-svg-izCSbY5aYnkI3qbb .node polygon,#mermaid-svg-izCSbY5aYnkI3qbb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-izCSbY5aYnkI3qbb .rough-node .label text,#mermaid-svg-izCSbY5aYnkI3qbb .node .label text,#mermaid-svg-izCSbY5aYnkI3qbb .image-shape .label,#mermaid-svg-izCSbY5aYnkI3qbb .icon-shape .label{text-anchor:middle;}#mermaid-svg-izCSbY5aYnkI3qbb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-izCSbY5aYnkI3qbb .rough-node .label,#mermaid-svg-izCSbY5aYnkI3qbb .node .label,#mermaid-svg-izCSbY5aYnkI3qbb .image-shape .label,#mermaid-svg-izCSbY5aYnkI3qbb .icon-shape .label{text-align:center;}#mermaid-svg-izCSbY5aYnkI3qbb .node.clickable{cursor:pointer;}#mermaid-svg-izCSbY5aYnkI3qbb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-izCSbY5aYnkI3qbb .arrowheadPath{fill:#333333;}#mermaid-svg-izCSbY5aYnkI3qbb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-izCSbY5aYnkI3qbb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-izCSbY5aYnkI3qbb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-izCSbY5aYnkI3qbb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-izCSbY5aYnkI3qbb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-izCSbY5aYnkI3qbb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-izCSbY5aYnkI3qbb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-izCSbY5aYnkI3qbb .cluster text{fill:#333;}#mermaid-svg-izCSbY5aYnkI3qbb .cluster span{color:#333;}#mermaid-svg-izCSbY5aYnkI3qbb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-izCSbY5aYnkI3qbb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-izCSbY5aYnkI3qbb rect.text{fill:none;stroke-width:0;}#mermaid-svg-izCSbY5aYnkI3qbb .icon-shape,#mermaid-svg-izCSbY5aYnkI3qbb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-izCSbY5aYnkI3qbb .icon-shape p,#mermaid-svg-izCSbY5aYnkI3qbb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-izCSbY5aYnkI3qbb .icon-shape .label rect,#mermaid-svg-izCSbY5aYnkI3qbb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-izCSbY5aYnkI3qbb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-izCSbY5aYnkI3qbb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-izCSbY5aYnkI3qbb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 业务模块 / 驱动 / 协议栈 / 算法
统一内存服务 API
按用途、尺寸、生命周期、上下文路由
静态对象区
任务栈/控制块/永久表
专用对象池
协议对象/消息节点/描述符
分级尺寸池
16/32/64/128/256B 等
环形缓冲或分片链
连续流/变长报文
Arena / Region
阶段性临时对象
受控通用堆
低频非关键请求
紧急保留池
恢复/告警/关键控制
静态链接布局与 MPU 区域
固定块空闲链表或位图
读写索引/块链/引用计数
单调指针/整体 reset
TLSF / heap_4 / Buddy / 其他分配器
仅关键调用方可访问
统计、Canary、Poison、调用点
遥测、告警、故障注入、长稳分析
统一内存服务的作用不是隐藏所有差异,而是把分配意图显式化。例如:
c
void *mem_alloc(mem_domain_t domain, size_t size, size_t alignment, uint32_t flags);
void mem_free(mem_domain_t domain, void *ptr);
void *obj_pool_acquire(obj_pool_t *pool, uint32_t timeout_ms);
void obj_pool_release(obj_pool_t *pool, void *object);
int buffer_acquire(buffer_pool_t *pool, size_t min_len, buffer_handle_t *handle);
void buffer_retain(buffer_handle_t *handle);
void buffer_release(buffer_handle_t *handle);
void *arena_alloc(arena_t *arena, size_t size, size_t alignment);
void arena_reset(arena_t *arena);
domain、flags 或独立 API 应至少表达以下信息:
- 申请发生在任务还是 ISR。
- 是否允许阻塞。
- 是否要求 DMA 可访问。
- 是否要求特定对齐或无 Cache 区域。
- 是否为关键路径。
- 是否允许使用备用池。
- 对象是否需要清零。
- 失败时是否允许降级到更大尺寸池或通用堆。
不要让一个没有上下文信息的 malloc(size)承担所有决策。
机制与开源实现的对应关系
下表列出可以直接使用或重点参考的成熟方案。它们不是互斥选项,实际系统通常会组合使用。
| 机制 | 开源组件或标准接口 | 核心原理 | 适合场景 | 主要限制 |
|---|---|---|---|---|
| 启动期单调分配 | FreeRTOS heap_1、Arena/Region |
指针单向增长,不支持单对象释放 | 系统对象只在启动阶段创建 | 无法回收单个对象 |
| 固定块池 | Zephyr k_mem_slab、CMSIS-RTOS2 Memory Pool、ThreadX Block Pool、Contiki-NG MEMB |
等尺寸块通过空闲链表或位图管理 | 协议对象、消息节点、描述符 | 块尺寸必须预先确定 |
| 专用协议对象池 | lwIP memp |
每种协议对象独立静态池 | PCB、超时节点、协议控制结构 | 容量配置错误会导致对应功能耗尽 |
| 分片报文池 | lwIP pbuf/PBUF_POOL |
固定尺寸缓冲可串成链,支持引用和外部数据 | 网络报文、变长帧、零拷贝 | 链遍历和引用管理更复杂 |
| 分级尺寸池 | Slab/SLUB 思路、多个 Zephyr slab、多个 CMSIS Pool | 请求向上匹配到最近尺寸类 | 小对象尺寸离散、频繁申请释放 | 有内部碎片和跨池失衡 |
| 合并型普通堆 | FreeRTOS heap_4 |
空闲块链表,释放时合并相邻块 | 非实时、请求尺寸变化较大 | 仍有延迟和碎片风险 |
| 多区域普通堆 | FreeRTOS heap_5 |
在多个不连续内存区上使用类似 heap_4 的管理 |
多 SRAM、外部 RAM、不同地址区 | 不能自动表达 DMA/安全域语义 |
| 确定性可变块堆 | TLSF | 两级尺寸分类、位图定位近似合适块 | 必须支持可变尺寸且要求有界时间 | 元数据和最小池开销不适合极小系统 |
| Buddy | Linux 页分配器及多种嵌入式实现 | 按 2 的幂次拆分和伙伴合并 | 大块、页、DMA 区域 | 小对象内部碎片明显 |
| Slab 对象缓存 | Linux kmem_cache |
从页或大块中切出同类型对象缓存 | 大量同类型内核对象 | 完整 Linux 机制对小 MCU 过重 |
| 保留内存池 | Linux mempool 思路 |
常规分配失败时使用预留元素 | I/O、恢复、必须前进的关键路径 | 预留容量需严格限制调用方 |
| 静态流缓冲 | FreeRTOS Stream/Message Buffer | 环形数组承载字节流或变长消息 | 单生产者单消费者数据通道 | 默认并发模型不是多生产者多消费者 |
| 静态容器 | Rust heapless 等 |
容量为编译期常量的 Vec/Queue/Pool | Rust 裸机与 RTOS 应用 | 容量不足时必须显式处理错误 |
FreeRTOS 的参考价值
FreeRTOS 官方提供多种堆实现,适合用来说明"分配器必须按系统模型选择"。
heap_1只分配不释放,操作简单、确定,不产生碎片,适合启动阶段创建全部对象。heap_2允许释放但不合并相邻空闲块,官方已将其视为旧方案。heap_3包装 C 库malloc/free并增加线程安全,不解决底层分配器自身的碎片和实时性问题。heap_4释放时合并相邻空闲块,能显著缓解外部碎片,但不能把任意分配序列变成"永不失败"。heap_5在多个不连续区域上提供类似heap_4的能力,适合片上 SRAM、CCM、外部 SRAM 等并存的系统。
FreeRTOS 还提供静态创建任务、队列、信号量、Stream Buffer 和 Message Buffer 的接口。对于长期存在的 RTOS 对象,应优先使用静态创建接口,而不是仅因为 API 默认提供动态创建就使用堆。
Zephyr k_mem_slab
Zephyr Memory Slab 由固定块大小、块数量和用户提供的缓冲区组成。空闲块通常以链表组织,申请和释放不需要搜索不同尺寸块。Zephyr 还提供当前使用数、空闲数、历史最大使用数和运行统计接口,这一点非常值得移植到自研池中:池实现不仅要能分配,还要原生提供峰值观测能力。
CMSIS-RTOS2 Memory Pool
CMSIS-RTOS2 将 Memory Pool 定义为线程安全的固定尺寸块集合,支持查询容量、块大小、已用数量和剩余数量。控制块和数据区都可以由用户提供静态内存,因此既能统一 RTOS API,又能避免内核内部再从堆申请。
它还很适合实现零拷贝邮箱:
- 生产者从 Memory Pool 取得对象。
- 填充对象后,只把指针放入消息队列。
- 消费者处理完成后归还池。
- 大块数据不在队列中重复复制。
ThreadX Block Pool 与 Byte Pool
ThreadX 将固定块池和可变尺寸字节池分开。Block Pool 适合固定大小对象,Byte Pool 面向可变尺寸请求。这个分离本身就是一个重要架构原则:固定对象和不可预测尺寸请求不应默认共享同一分配策略。
lwIP memp 与 pbuf
lwIP 的 memp 为不同协议对象建立独立池,典型对象包括连接控制块、超时节点和协议消息。其价值在于:
- 每类资源都有独立上限。
- 一个资源类型耗尽时,不会直接把所有堆内存吃光。
- 配置项可以直接表达最大并发连接、重组队列和消息数量。
pbuf 则解决变长网络报文问题。固定大小 PBUF_POOL 块可以形成链,报文不必依赖一块同等长度的连续内存。PBUF_REF 和 PBUF_ROM 还能引用外部或只读数据,体现了零拷贝和所有权分离思想。
Contiki-NG MEMB
Contiki-NG MEMB(name, structure, num) 通过静态数组和使用标记管理固定数量的同类型对象。实现很小,适合 RAM 以 KB 计的系统。它说明固定池并不一定需要复杂链表;块数量较少时,位图或使用数组同样合理。
TLSF
TLSF 使用两级尺寸分类和位图快速定位合适空闲块,目标是让申请和释放具有 O(1) 渐进复杂度,并保持较低碎片。它适合以下情况:
- 业务确实存在较多可变尺寸对象。
- 无法通过少量固定池覆盖全部请求。
- 实时任务要求分配耗时有明确上界。
- 系统有足够 RAM 承担 TLSF 控制结构和块头开销。
TLSF 不是"极小 RAM 系统的默认答案"。当可用堆只有几 KB,而对象尺寸和数量本来就可枚举时,专用池通常更简单、更省空间、更容易验证。
从对象生命周期出发,而不是从尺寸出发
很多内存池设计只统计"申请了多少字节",却忽略生命周期。真正导致普通堆碎片的核心因素之一,是不同生命周期对象交错分配和释放。
可以把对象分成以下几类。
| 生命周期类型 | 示例 | 推荐策略 |
|---|---|---|
| 永久对象 | 任务栈、驱动实例、设备表、协议全局上下文 | 静态分配或启动期单调分配 |
| 会话对象 | 连接、一次升级任务、一次文件传输 | 会话专用 Arena 或对象池 |
| 报文对象 | CAN、UART、TCP、BLE、IPC 消息 | 固定块池、分片池、环形缓冲 |
| 帧对象 | 音频帧、图像块、传感器批次 | 固定帧池、DMA 池、环形缓冲 |
| 短期临时对象 | 解析树、中间计算数组、编码临时区 | 栈、Arena、可复用工作区 |
| 低频不规则对象 | 调试命令、非关键扩展数据 | 受控通用堆 |
| 错误恢复对象 | 告警帧、重连上下文、崩溃转储描述符 | 独立保留池 |
如果一组对象总是在某个阶段一起创建、一起失效,就不应逐个 free。使用 Arena 后,释放成本是一次 reset,也不会形成外部碎片。
Parser/Protocol Session Arena Session Manager Parser/Protocol Session Arena Session Manager #mermaid-svg-S2HfHpjdttbkaVLb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-S2HfHpjdttbkaVLb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-S2HfHpjdttbkaVLb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-S2HfHpjdttbkaVLb .error-icon{fill:#552222;}#mermaid-svg-S2HfHpjdttbkaVLb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-S2HfHpjdttbkaVLb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-S2HfHpjdttbkaVLb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-S2HfHpjdttbkaVLb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-S2HfHpjdttbkaVLb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-S2HfHpjdttbkaVLb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-S2HfHpjdttbkaVLb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-S2HfHpjdttbkaVLb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-S2HfHpjdttbkaVLb .marker.cross{stroke:#333333;}#mermaid-svg-S2HfHpjdttbkaVLb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-S2HfHpjdttbkaVLb p{margin:0;}#mermaid-svg-S2HfHpjdttbkaVLb .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-S2HfHpjdttbkaVLb text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-S2HfHpjdttbkaVLb .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-S2HfHpjdttbkaVLb .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-S2HfHpjdttbkaVLb .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-S2HfHpjdttbkaVLb .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-S2HfHpjdttbkaVLb #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-S2HfHpjdttbkaVLb .sequenceNumber{fill:white;}#mermaid-svg-S2HfHpjdttbkaVLb #sequencenumber{fill:#333;}#mermaid-svg-S2HfHpjdttbkaVLb #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-S2HfHpjdttbkaVLb .messageText{fill:#333;stroke:none;}#mermaid-svg-S2HfHpjdttbkaVLb .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-S2HfHpjdttbkaVLb .labelText,#mermaid-svg-S2HfHpjdttbkaVLb .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-S2HfHpjdttbkaVLb .loopText,#mermaid-svg-S2HfHpjdttbkaVLb .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-S2HfHpjdttbkaVLb .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-S2HfHpjdttbkaVLb .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-S2HfHpjdttbkaVLb .noteText,#mermaid-svg-S2HfHpjdttbkaVLb .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-S2HfHpjdttbkaVLb .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-S2HfHpjdttbkaVLb .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-S2HfHpjdttbkaVLb .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-S2HfHpjdttbkaVLb .actorPopupMenu{position:absolute;}#mermaid-svg-S2HfHpjdttbkaVLb .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-S2HfHpjdttbkaVLb .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-S2HfHpjdttbkaVLb .actor-man circle,#mermaid-svg-S2HfHpjdttbkaVLb line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-S2HfHpjdttbkaVLb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 仅移动 offset,不维护逐块空闲链表 整个会话内存一次回收 arena_init(buffer) 分配对象 A 分配对象 B 分配临时数组 arena_reset()
第一层:永久对象与启动期内存布局
系统启动后长期存在的对象,应在链接期或初始化阶段完成分配,包括:
- RTOS 任务栈和任务控制块。
- 队列、信号量、事件组、定时器等内核对象。
- 设备驱动实例和总线描述符。
- 永久协议上下文。
- 固定日志缓冲。
- 各内存池的数据区和控制块。
- DMA 描述符和长期 DMA 缓冲。
- 错误恢复保留区。
推荐在链接脚本中按用途划分区域:
text
.fast_ram 控制环、ISR 数据、低延迟对象
.dma_ram DMA 可访问、满足对齐和 Cache 策略的缓冲
.noinit 软复位后保留的诊断信息
.retention 低功耗保持区
.pool_ctrl 内存池控制块和统计
.pool_data 固定池数据区
.general_heap 仅供受控非关键动态请求
这样可以避免以下问题:
- DMA 引擎无法访问某些 TCM/CCM 区域。
- Cacheable 缓冲未正确 clean/invalidate。
- 关键控制对象和大数据缓冲争夺同一 SRAM bank。
- 软复位后需要保留的崩溃信息被启动清零。
- 外部 RAM 失效影响关键控制路径。
启动期可以使用 heap_1 或简单 Bump Allocator,但初始化完成后应冻结该分配器,防止运行期继续增长。
text
boot_alloc() 只允许在 SYSTEM_INIT 状态调用
进入 SYSTEM_RUNNING 后:
boot_alloc() -> 返回错误或触发开发期断言
第二层:专用对象池
专用对象池按业务类型建立,例如:
text
connection_pool
can_rx_message_pool
ble_event_pool
audio_frame_pool
log_record_pool
filesystem_request_pool
dma_descriptor_pool
相比"所有 64B 对象共用一个 64B 池",专用池的优势是故障隔离和容量语义清晰。例如网络连接对象耗尽,不应占用电机控制命令对象的内存。
固定池的基本结构
最常见结构是把空闲块本身的前几个字节作为空闲链表节点。
#mermaid-svg-tmHJmlJb1YwtSlTl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tmHJmlJb1YwtSlTl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tmHJmlJb1YwtSlTl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tmHJmlJb1YwtSlTl .error-icon{fill:#552222;}#mermaid-svg-tmHJmlJb1YwtSlTl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tmHJmlJb1YwtSlTl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tmHJmlJb1YwtSlTl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tmHJmlJb1YwtSlTl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tmHJmlJb1YwtSlTl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tmHJmlJb1YwtSlTl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tmHJmlJb1YwtSlTl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tmHJmlJb1YwtSlTl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tmHJmlJb1YwtSlTl .marker.cross{stroke:#333333;}#mermaid-svg-tmHJmlJb1YwtSlTl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tmHJmlJb1YwtSlTl p{margin:0;}#mermaid-svg-tmHJmlJb1YwtSlTl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tmHJmlJb1YwtSlTl .cluster-label text{fill:#333;}#mermaid-svg-tmHJmlJb1YwtSlTl .cluster-label span{color:#333;}#mermaid-svg-tmHJmlJb1YwtSlTl .cluster-label span p{background-color:transparent;}#mermaid-svg-tmHJmlJb1YwtSlTl .label text,#mermaid-svg-tmHJmlJb1YwtSlTl span{fill:#333;color:#333;}#mermaid-svg-tmHJmlJb1YwtSlTl .node rect,#mermaid-svg-tmHJmlJb1YwtSlTl .node circle,#mermaid-svg-tmHJmlJb1YwtSlTl .node ellipse,#mermaid-svg-tmHJmlJb1YwtSlTl .node polygon,#mermaid-svg-tmHJmlJb1YwtSlTl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tmHJmlJb1YwtSlTl .rough-node .label text,#mermaid-svg-tmHJmlJb1YwtSlTl .node .label text,#mermaid-svg-tmHJmlJb1YwtSlTl .image-shape .label,#mermaid-svg-tmHJmlJb1YwtSlTl .icon-shape .label{text-anchor:middle;}#mermaid-svg-tmHJmlJb1YwtSlTl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tmHJmlJb1YwtSlTl .rough-node .label,#mermaid-svg-tmHJmlJb1YwtSlTl .node .label,#mermaid-svg-tmHJmlJb1YwtSlTl .image-shape .label,#mermaid-svg-tmHJmlJb1YwtSlTl .icon-shape .label{text-align:center;}#mermaid-svg-tmHJmlJb1YwtSlTl .node.clickable{cursor:pointer;}#mermaid-svg-tmHJmlJb1YwtSlTl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tmHJmlJb1YwtSlTl .arrowheadPath{fill:#333333;}#mermaid-svg-tmHJmlJb1YwtSlTl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tmHJmlJb1YwtSlTl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tmHJmlJb1YwtSlTl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tmHJmlJb1YwtSlTl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tmHJmlJb1YwtSlTl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tmHJmlJb1YwtSlTl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tmHJmlJb1YwtSlTl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tmHJmlJb1YwtSlTl .cluster text{fill:#333;}#mermaid-svg-tmHJmlJb1YwtSlTl .cluster span{color:#333;}#mermaid-svg-tmHJmlJb1YwtSlTl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tmHJmlJb1YwtSlTl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tmHJmlJb1YwtSlTl rect.text{fill:none;stroke-width:0;}#mermaid-svg-tmHJmlJb1YwtSlTl .icon-shape,#mermaid-svg-tmHJmlJb1YwtSlTl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tmHJmlJb1YwtSlTl .icon-shape p,#mermaid-svg-tmHJmlJb1YwtSlTl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tmHJmlJb1YwtSlTl .icon-shape .label rect,#mermaid-svg-tmHJmlJb1YwtSlTl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tmHJmlJb1YwtSlTl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tmHJmlJb1YwtSlTl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tmHJmlJb1YwtSlTl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} free_head
空闲块 3
next
空闲块 1
next
空闲块 7
next
NULL
已分配块 0
已分配块 2
已分配块 4
申请时从表头弹出一个块,释放时压回表头,算法复杂度为 O(1)。
c
/**
* @brief Fixed-size memory pool control block.
*/
typedef struct mem_fixed_pool {
uint8_t *storage;
void *free_head;
uint32_t block_size;
uint32_t block_count;
uint32_t used_count;
uint32_t peak_used;
uint32_t alloc_failures;
} mem_fixed_pool_t;
初始化时需要满足:
text
effective_block_size =
align_up(max(user_object_size, sizeof(void *)), required_alignment)
pool_bytes =
effective_block_size * block_count
空闲链表方案不需要为每块单独增加常驻链表节点,因为空闲块本身可以存储 next 指针。但如果需要在块已分配时保留状态、调用点、Canary 或引用计数,就要额外设计块头或旁路元数据。
空闲链表还是位图
| 方式 | 申请复杂度 | 每块开销 | 优点 | 缺点 |
|---|---|---|---|---|
| 块内空闲链表 | O(1) | 空闲时占用一个指针 | 实现简单、速度稳定 | 空闲块内容会被覆盖 |
| 外部索引链表 | O(1) | 每块索引或指针 | 用户数据不被写入 | 元数据占用较大 |
| 位图 | O(位图字数量),可用位扫描优化 | 1 bit/块 | 元数据很小,容易检查重复释放 | 需要定位第一个空闲位 |
| 固定索引栈 | O(1) | 每块一个索引 | 指针宽度可压缩到 8/16 bit | 需要独立索引数组 |
在 64KB 以下系统中,如果一个池只有几十个块,位图往往非常合适。若块数不超过 32 或 64,可以用一个机器字表示全部状态,并通过 ctz/ffs 指令快速找空闲块。
块状态机
关键对象池不应只有"空闲/占用"两个隐式状态。建议在调试版本中维护状态机:
#mermaid-svg-v1s6XId4HIypfvPy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-v1s6XId4HIypfvPy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-v1s6XId4HIypfvPy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-v1s6XId4HIypfvPy .error-icon{fill:#552222;}#mermaid-svg-v1s6XId4HIypfvPy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-v1s6XId4HIypfvPy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-v1s6XId4HIypfvPy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-v1s6XId4HIypfvPy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-v1s6XId4HIypfvPy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-v1s6XId4HIypfvPy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-v1s6XId4HIypfvPy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-v1s6XId4HIypfvPy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-v1s6XId4HIypfvPy .marker.cross{stroke:#333333;}#mermaid-svg-v1s6XId4HIypfvPy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-v1s6XId4HIypfvPy p{margin:0;}#mermaid-svg-v1s6XId4HIypfvPy defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-v1s6XId4HIypfvPy g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-v1s6XId4HIypfvPy g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-v1s6XId4HIypfvPy g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-v1s6XId4HIypfvPy g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-v1s6XId4HIypfvPy g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-v1s6XId4HIypfvPy .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-v1s6XId4HIypfvPy .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-v1s6XId4HIypfvPy .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-v1s6XId4HIypfvPy .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-v1s6XId4HIypfvPy .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-v1s6XId4HIypfvPy .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-v1s6XId4HIypfvPy .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-v1s6XId4HIypfvPy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-v1s6XId4HIypfvPy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-v1s6XId4HIypfvPy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-v1s6XId4HIypfvPy .edgeLabel .label text{fill:#333;}#mermaid-svg-v1s6XId4HIypfvPy .label div .edgeLabel{color:#333;}#mermaid-svg-v1s6XId4HIypfvPy .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-v1s6XId4HIypfvPy .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-v1s6XId4HIypfvPy .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-v1s6XId4HIypfvPy .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-v1s6XId4HIypfvPy .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-v1s6XId4HIypfvPy .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-v1s6XId4HIypfvPy .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-v1s6XId4HIypfvPy #statediagram-barbEnd{fill:#333333;}#mermaid-svg-v1s6XId4HIypfvPy .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-v1s6XId4HIypfvPy .cluster-label,#mermaid-svg-v1s6XId4HIypfvPy .nodeLabel{color:#131300;}#mermaid-svg-v1s6XId4HIypfvPy .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-v1s6XId4HIypfvPy .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-v1s6XId4HIypfvPy .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-v1s6XId4HIypfvPy .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-v1s6XId4HIypfvPy .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-v1s6XId4HIypfvPy .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-v1s6XId4HIypfvPy .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-v1s6XId4HIypfvPy .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-v1s6XId4HIypfvPy .note-edge{stroke-dasharray:5;}#mermaid-svg-v1s6XId4HIypfvPy .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-v1s6XId4HIypfvPy .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-v1s6XId4HIypfvPy .statediagram-note text{fill:black;}#mermaid-svg-v1s6XId4HIypfvPy .statediagram-note .nodeLabel{color:black;}#mermaid-svg-v1s6XId4HIypfvPy .statediagram .edgeLabel{color:red;}#mermaid-svg-v1s6XId4HIypfvPy #dependencyStart,#mermaid-svg-v1s6XId4HIypfvPy #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-v1s6XId4HIypfvPy .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-v1s6XId4HIypfvPy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} acquire
publish
consume
retain/reuse
release
cancel
double free
越界或非法所有者
FREE
ALLOCATED
QUEUED
IN_USE
ERROR
状态机可以定位:
- 同一块被重复释放。
- 生产者发布后仍继续修改。
- 消费者未释放。
- 对象从错误的池归还。
- ISR 释放了只允许任务上下文操作的对象。
第三层:分级尺寸池
当请求尺寸不是单一值,但集中在若干范围内,可以使用 Size Class Pool。
text
Class 0: 16B
Class 1: 32B
Class 2: 48B
Class 3: 64B
Class 4: 96B
Class 5: 128B
Class 6: 256B
不要机械地只使用 2 的幂。尺寸档位应依据真实请求直方图、对齐要求和对象结构体大小设计。
假设统计得到:
| 请求范围 | 占比 | 推荐档位 |
|---|---|---|
| 1~20B | 28% | 24B 或 32B |
| 21~36B | 35% | 40B 或 48B |
| 37~60B | 20% | 64B |
| 61~100B | 12% | 104B 或 112B |
| 100B 以上 | 5% | 专用池或受控堆 |
如果全部向 32/64/128B 对齐,内部碎片可能明显大于按实际结构体尺寸设计的档位。
档位选择公式
对请求尺寸 S,选择最小满足条件的池:
text
class(S) = min { i | block_size[i] >= align_up(S, alignment[i]) }
内部碎片总量估算为:
text
W_internal =
Σ request_count[i] * (selected_block_size[i] - request_size[i])
内部碎片率:
text
F_internal =
W_internal / Σ request_count[i] * selected_block_size[i]
应使用采集到的分配轨迹计算,而不是凭经验拍脑袋。
池间回退策略
分级池耗尽时有三种常见策略。
- 严格池策略:只允许从对应池申请,耗尽立即失败。
- 向上借用策略:允许 64B 请求使用 128B 块。
- 通用堆回退策略:池耗尽后尝试通用堆。
关键路径通常应采用严格池策略,因为回退会让最坏空间和最坏时间重新变得不可预测。非关键路径可允许向上借用,但必须统计借用次数和浪费字节。
text
严禁形成无界回退链:
32B 池 -> 64B 池 -> 128B 池 -> 256B 池 -> 通用堆 -> 紧急池
这会让低优先级小对象逐步吃掉全部大块和紧急资源。更安全的做法是为每个调用域设置可访问池集合和配额。
第四层:环形缓冲与固定分片链
连续字节流和高频帧数据不应为每次收发执行 malloc/free。
环形缓冲
适用于:
- UART、SPI、I2S、ADC、CAN 接收数据。
- 音频采集和播放。
- 日志字节流。
- 单生产者单消费者的数据管线。
- DMA 双缓冲或多缓冲。
#mermaid-svg-2qy8u7ho17gHUXBz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2qy8u7ho17gHUXBz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2qy8u7ho17gHUXBz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2qy8u7ho17gHUXBz .error-icon{fill:#552222;}#mermaid-svg-2qy8u7ho17gHUXBz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2qy8u7ho17gHUXBz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2qy8u7ho17gHUXBz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2qy8u7ho17gHUXBz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2qy8u7ho17gHUXBz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2qy8u7ho17gHUXBz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2qy8u7ho17gHUXBz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2qy8u7ho17gHUXBz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2qy8u7ho17gHUXBz .marker.cross{stroke:#333333;}#mermaid-svg-2qy8u7ho17gHUXBz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2qy8u7ho17gHUXBz p{margin:0;}#mermaid-svg-2qy8u7ho17gHUXBz .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2qy8u7ho17gHUXBz .cluster-label text{fill:#333;}#mermaid-svg-2qy8u7ho17gHUXBz .cluster-label span{color:#333;}#mermaid-svg-2qy8u7ho17gHUXBz .cluster-label span p{background-color:transparent;}#mermaid-svg-2qy8u7ho17gHUXBz .label text,#mermaid-svg-2qy8u7ho17gHUXBz span{fill:#333;color:#333;}#mermaid-svg-2qy8u7ho17gHUXBz .node rect,#mermaid-svg-2qy8u7ho17gHUXBz .node circle,#mermaid-svg-2qy8u7ho17gHUXBz .node ellipse,#mermaid-svg-2qy8u7ho17gHUXBz .node polygon,#mermaid-svg-2qy8u7ho17gHUXBz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2qy8u7ho17gHUXBz .rough-node .label text,#mermaid-svg-2qy8u7ho17gHUXBz .node .label text,#mermaid-svg-2qy8u7ho17gHUXBz .image-shape .label,#mermaid-svg-2qy8u7ho17gHUXBz .icon-shape .label{text-anchor:middle;}#mermaid-svg-2qy8u7ho17gHUXBz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2qy8u7ho17gHUXBz .rough-node .label,#mermaid-svg-2qy8u7ho17gHUXBz .node .label,#mermaid-svg-2qy8u7ho17gHUXBz .image-shape .label,#mermaid-svg-2qy8u7ho17gHUXBz .icon-shape .label{text-align:center;}#mermaid-svg-2qy8u7ho17gHUXBz .node.clickable{cursor:pointer;}#mermaid-svg-2qy8u7ho17gHUXBz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2qy8u7ho17gHUXBz .arrowheadPath{fill:#333333;}#mermaid-svg-2qy8u7ho17gHUXBz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2qy8u7ho17gHUXBz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2qy8u7ho17gHUXBz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2qy8u7ho17gHUXBz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2qy8u7ho17gHUXBz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2qy8u7ho17gHUXBz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2qy8u7ho17gHUXBz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2qy8u7ho17gHUXBz .cluster text{fill:#333;}#mermaid-svg-2qy8u7ho17gHUXBz .cluster span{color:#333;}#mermaid-svg-2qy8u7ho17gHUXBz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2qy8u7ho17gHUXBz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2qy8u7ho17gHUXBz rect.text{fill:none;stroke-width:0;}#mermaid-svg-2qy8u7ho17gHUXBz .icon-shape,#mermaid-svg-2qy8u7ho17gHUXBz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2qy8u7ho17gHUXBz .icon-shape p,#mermaid-svg-2qy8u7ho17gHUXBz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2qy8u7ho17gHUXBz .icon-shape .label rect,#mermaid-svg-2qy8u7ho17gHUXBz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2qy8u7ho17gHUXBz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2qy8u7ho17gHUXBz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2qy8u7ho17gHUXBz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} write
read
Producer / DMA / ISR
Ring Buffer
Consumer Task
read_index / write_index / watermark
环形缓冲需要明确:
- 是否允许覆盖旧数据。
- 满时生产者丢弃、阻塞还是触发背压。
- 空时消费者等待、补零还是降级。
- 是否支持 DMA 直接写入连续可用区。
- 回绕时是否需要两段 claim。
- 生产者和消费者是否严格各只有一个。
FreeRTOS Stream Buffer 的默认并发模型是单写者、单读者;多写者或多读者需要应用层串行化。这个限制并不是缺陷,而是通过缩小并发模型换取更低开销。
固定分片链
变长报文如果必须连续存储,会迫使系统准备等于最大报文的块,利用率很差。可以借鉴 lwIP pbuf:
#mermaid-svg-6Bhtm2ze7FymLBUO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6Bhtm2ze7FymLBUO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6Bhtm2ze7FymLBUO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6Bhtm2ze7FymLBUO .error-icon{fill:#552222;}#mermaid-svg-6Bhtm2ze7FymLBUO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6Bhtm2ze7FymLBUO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6Bhtm2ze7FymLBUO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6Bhtm2ze7FymLBUO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6Bhtm2ze7FymLBUO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6Bhtm2ze7FymLBUO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6Bhtm2ze7FymLBUO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6Bhtm2ze7FymLBUO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6Bhtm2ze7FymLBUO .marker.cross{stroke:#333333;}#mermaid-svg-6Bhtm2ze7FymLBUO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6Bhtm2ze7FymLBUO p{margin:0;}#mermaid-svg-6Bhtm2ze7FymLBUO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6Bhtm2ze7FymLBUO .cluster-label text{fill:#333;}#mermaid-svg-6Bhtm2ze7FymLBUO .cluster-label span{color:#333;}#mermaid-svg-6Bhtm2ze7FymLBUO .cluster-label span p{background-color:transparent;}#mermaid-svg-6Bhtm2ze7FymLBUO .label text,#mermaid-svg-6Bhtm2ze7FymLBUO span{fill:#333;color:#333;}#mermaid-svg-6Bhtm2ze7FymLBUO .node rect,#mermaid-svg-6Bhtm2ze7FymLBUO .node circle,#mermaid-svg-6Bhtm2ze7FymLBUO .node ellipse,#mermaid-svg-6Bhtm2ze7FymLBUO .node polygon,#mermaid-svg-6Bhtm2ze7FymLBUO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6Bhtm2ze7FymLBUO .rough-node .label text,#mermaid-svg-6Bhtm2ze7FymLBUO .node .label text,#mermaid-svg-6Bhtm2ze7FymLBUO .image-shape .label,#mermaid-svg-6Bhtm2ze7FymLBUO .icon-shape .label{text-anchor:middle;}#mermaid-svg-6Bhtm2ze7FymLBUO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6Bhtm2ze7FymLBUO .rough-node .label,#mermaid-svg-6Bhtm2ze7FymLBUO .node .label,#mermaid-svg-6Bhtm2ze7FymLBUO .image-shape .label,#mermaid-svg-6Bhtm2ze7FymLBUO .icon-shape .label{text-align:center;}#mermaid-svg-6Bhtm2ze7FymLBUO .node.clickable{cursor:pointer;}#mermaid-svg-6Bhtm2ze7FymLBUO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6Bhtm2ze7FymLBUO .arrowheadPath{fill:#333333;}#mermaid-svg-6Bhtm2ze7FymLBUO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6Bhtm2ze7FymLBUO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6Bhtm2ze7FymLBUO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6Bhtm2ze7FymLBUO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6Bhtm2ze7FymLBUO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6Bhtm2ze7FymLBUO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6Bhtm2ze7FymLBUO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6Bhtm2ze7FymLBUO .cluster text{fill:#333;}#mermaid-svg-6Bhtm2ze7FymLBUO .cluster span{color:#333;}#mermaid-svg-6Bhtm2ze7FymLBUO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6Bhtm2ze7FymLBUO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6Bhtm2ze7FymLBUO rect.text{fill:none;stroke-width:0;}#mermaid-svg-6Bhtm2ze7FymLBUO .icon-shape,#mermaid-svg-6Bhtm2ze7FymLBUO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6Bhtm2ze7FymLBUO .icon-shape p,#mermaid-svg-6Bhtm2ze7FymLBUO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6Bhtm2ze7FymLBUO .icon-shape .label rect,#mermaid-svg-6Bhtm2ze7FymLBUO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6Bhtm2ze7FymLBUO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6Bhtm2ze7FymLBUO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6Bhtm2ze7FymLBUO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Packet Header
len=620
256B block
256B block
108B valid in 256B block
NULL
优点:
- 不要求获得一块 620B 连续内存。
- 固定块池没有外部碎片。
- 可在协议层使用 Scatter/Gather 或链式解析。
- DMA 支持描述符链时可以减少复制。
缺点:
- 每个块有链指针和有效长度开销。
- 随机访问需要遍历。
- 发送接口若不支持 Scatter/Gather,最终仍可能需要合并。
- 引用计数和链释放必须严格正确。
描述符与载荷分离
可以将小型描述符和大块载荷放在不同池:
text
message_desc_pool: 32 个 × 32B
payload_256_pool: 16 个 × 256B
payload_1024_pool: 4 个 × 1024B
描述符包含:
c
typedef struct buffer_desc {
void *payload;
uint16_t capacity;
uint16_t length;
uint16_t offset;
uint8_t pool_id;
uint8_t ref_count;
uint16_t flags;
} buffer_desc_t;
这样消息队列只传递描述符指针,大载荷无需复制。
第五层:Arena/Region 分配器
Arena 是一个预分配区域和一个当前偏移量。申请时只进行对齐和指针推进,释放时不支持单个对象释放,而是在生命周期结束时整体重置。
c
/**
* @brief Allocate memory from an arena.
*
* @param arena Arena control block.
* @param size Requested size in bytes.
* @param alignment Required power-of-two alignment.
*
* @return Pointer to allocated memory, or NULL when the arena is exhausted.
*/
void *arena_alloc(arena_t *arena, size_t size, size_t alignment);
核心逻辑:
text
aligned_offset = align_up(current_offset, alignment)
new_offset = aligned_offset + size
if new_offset > capacity:
return NULL
ptr = base + aligned_offset
current_offset = new_offset
return ptr
Arena 的申请时间近似常数,完全没有外部碎片。适合:
- 一次协议会话中的解析对象。
- 一帧图像或一次推理的临时工作区。
- 一次命令处理过程。
- 启动阶段设备和服务注册。
- 可整体取消的事务。
不适合:
- 对象生命周期互相独立。
- 必须提前释放大对象以回收空间。
- 长期会话中对象持续增长且没有阶段边界。
可以采用双 Arena 或 Ping-Pong Arena:
text
Frame N 使用 Arena A
Frame N+1 使用 Arena B
Frame N 完成后 reset Arena A
这在图形、音频、传感器批处理和模型推理中很常见。
第六层:受控通用堆
不是所有系统都能完全消除可变尺寸申请。插件系统、脚本解释器、复杂文件格式、第三方库和少量低频对象可能仍需要通用堆。
此时应遵守以下约束:
- 通用堆不能承载硬实时路径。
- 通用堆不能承载 ISR 申请。
- 通用堆应有独立区域和硬配额。
- 第三方库的分配接口应重定向到该受控堆。
- 必须记录最小剩余量、最大连续空闲块和失败次数。
- 不允许关键恢复路径依赖通用堆成功。
- 对象生命周期应尽量聚类,减少长短生命周期交错。
- 可在开发版本定期执行一致性检查,但不要把重型检查放入实时路径。
heap_4 适用边界
heap_4 释放时合并相邻空闲块,适合一般嵌入式应用的低频动态对象。但它不能保证:
- 任意分配序列下永不碎片。
- 申请时间严格恒定。
- 大量线程竞争时无长尾。
- 分配失败不会影响关键模块。
因此它更适合作为"剩余需求的通用堆",而不是整个产品的唯一内存策略。
TLSF 适用边界
TLSF 通过一级和二级尺寸分类组织空闲块,并使用位图快速找到可用类别。简化理解如下:
#mermaid-svg-95dqAektb7P1F49z{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-95dqAektb7P1F49z .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-95dqAektb7P1F49z .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-95dqAektb7P1F49z .error-icon{fill:#552222;}#mermaid-svg-95dqAektb7P1F49z .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-95dqAektb7P1F49z .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-95dqAektb7P1F49z .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-95dqAektb7P1F49z .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-95dqAektb7P1F49z .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-95dqAektb7P1F49z .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-95dqAektb7P1F49z .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-95dqAektb7P1F49z .marker{fill:#333333;stroke:#333333;}#mermaid-svg-95dqAektb7P1F49z .marker.cross{stroke:#333333;}#mermaid-svg-95dqAektb7P1F49z svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-95dqAektb7P1F49z p{margin:0;}#mermaid-svg-95dqAektb7P1F49z .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-95dqAektb7P1F49z .cluster-label text{fill:#333;}#mermaid-svg-95dqAektb7P1F49z .cluster-label span{color:#333;}#mermaid-svg-95dqAektb7P1F49z .cluster-label span p{background-color:transparent;}#mermaid-svg-95dqAektb7P1F49z .label text,#mermaid-svg-95dqAektb7P1F49z span{fill:#333;color:#333;}#mermaid-svg-95dqAektb7P1F49z .node rect,#mermaid-svg-95dqAektb7P1F49z .node circle,#mermaid-svg-95dqAektb7P1F49z .node ellipse,#mermaid-svg-95dqAektb7P1F49z .node polygon,#mermaid-svg-95dqAektb7P1F49z .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-95dqAektb7P1F49z .rough-node .label text,#mermaid-svg-95dqAektb7P1F49z .node .label text,#mermaid-svg-95dqAektb7P1F49z .image-shape .label,#mermaid-svg-95dqAektb7P1F49z .icon-shape .label{text-anchor:middle;}#mermaid-svg-95dqAektb7P1F49z .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-95dqAektb7P1F49z .rough-node .label,#mermaid-svg-95dqAektb7P1F49z .node .label,#mermaid-svg-95dqAektb7P1F49z .image-shape .label,#mermaid-svg-95dqAektb7P1F49z .icon-shape .label{text-align:center;}#mermaid-svg-95dqAektb7P1F49z .node.clickable{cursor:pointer;}#mermaid-svg-95dqAektb7P1F49z .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-95dqAektb7P1F49z .arrowheadPath{fill:#333333;}#mermaid-svg-95dqAektb7P1F49z .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-95dqAektb7P1F49z .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-95dqAektb7P1F49z .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-95dqAektb7P1F49z .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-95dqAektb7P1F49z .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-95dqAektb7P1F49z .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-95dqAektb7P1F49z .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-95dqAektb7P1F49z .cluster text{fill:#333;}#mermaid-svg-95dqAektb7P1F49z .cluster span{color:#333;}#mermaid-svg-95dqAektb7P1F49z div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-95dqAektb7P1F49z .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-95dqAektb7P1F49z rect.text{fill:none;stroke-width:0;}#mermaid-svg-95dqAektb7P1F49z .icon-shape,#mermaid-svg-95dqAektb7P1F49z .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-95dqAektb7P1F49z .icon-shape p,#mermaid-svg-95dqAektb7P1F49z .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-95dqAektb7P1F49z .icon-shape .label rect,#mermaid-svg-95dqAektb7P1F49z .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-95dqAektb7P1F49z .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-95dqAektb7P1F49z .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-95dqAektb7P1F49z :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求 size
映射到一级 FL 和二级 SL
查询 FL/SL 位图
定位非空空闲链表
取块并按需要拆分
返回用户块
free
与相邻空闲块合并
重新插入对应 FL/SL
TLSF 的优势是分配和释放路径有界、查找不需要线性扫描所有空闲块。代价包括:
- 每个块需要头部元数据。
- 控制结构本身需要一定内存。
- 线程安全通常需要外部锁。
- 小池中元数据比例可能过高。
- "O(1)"不等于在所有硬件和锁竞争情况下延迟完全相同。
推荐把 TLSF 放在明确的非 ISR 动态域,而不是替代全部专用池。
Buddy 适用边界
Buddy 将内存划分为 2 的幂大小的块。申请大块时,从更高阶块逐级拆分;释放时如果伙伴块空闲则合并。
#mermaid-svg-oQgBf6vuSiIIf5Mj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oQgBf6vuSiIIf5Mj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oQgBf6vuSiIIf5Mj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oQgBf6vuSiIIf5Mj .error-icon{fill:#552222;}#mermaid-svg-oQgBf6vuSiIIf5Mj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oQgBf6vuSiIIf5Mj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oQgBf6vuSiIIf5Mj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oQgBf6vuSiIIf5Mj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oQgBf6vuSiIIf5Mj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oQgBf6vuSiIIf5Mj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oQgBf6vuSiIIf5Mj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oQgBf6vuSiIIf5Mj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oQgBf6vuSiIIf5Mj .marker.cross{stroke:#333333;}#mermaid-svg-oQgBf6vuSiIIf5Mj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oQgBf6vuSiIIf5Mj p{margin:0;}#mermaid-svg-oQgBf6vuSiIIf5Mj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oQgBf6vuSiIIf5Mj .cluster-label text{fill:#333;}#mermaid-svg-oQgBf6vuSiIIf5Mj .cluster-label span{color:#333;}#mermaid-svg-oQgBf6vuSiIIf5Mj .cluster-label span p{background-color:transparent;}#mermaid-svg-oQgBf6vuSiIIf5Mj .label text,#mermaid-svg-oQgBf6vuSiIIf5Mj span{fill:#333;color:#333;}#mermaid-svg-oQgBf6vuSiIIf5Mj .node rect,#mermaid-svg-oQgBf6vuSiIIf5Mj .node circle,#mermaid-svg-oQgBf6vuSiIIf5Mj .node ellipse,#mermaid-svg-oQgBf6vuSiIIf5Mj .node polygon,#mermaid-svg-oQgBf6vuSiIIf5Mj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oQgBf6vuSiIIf5Mj .rough-node .label text,#mermaid-svg-oQgBf6vuSiIIf5Mj .node .label text,#mermaid-svg-oQgBf6vuSiIIf5Mj .image-shape .label,#mermaid-svg-oQgBf6vuSiIIf5Mj .icon-shape .label{text-anchor:middle;}#mermaid-svg-oQgBf6vuSiIIf5Mj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oQgBf6vuSiIIf5Mj .rough-node .label,#mermaid-svg-oQgBf6vuSiIIf5Mj .node .label,#mermaid-svg-oQgBf6vuSiIIf5Mj .image-shape .label,#mermaid-svg-oQgBf6vuSiIIf5Mj .icon-shape .label{text-align:center;}#mermaid-svg-oQgBf6vuSiIIf5Mj .node.clickable{cursor:pointer;}#mermaid-svg-oQgBf6vuSiIIf5Mj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oQgBf6vuSiIIf5Mj .arrowheadPath{fill:#333333;}#mermaid-svg-oQgBf6vuSiIIf5Mj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oQgBf6vuSiIIf5Mj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oQgBf6vuSiIIf5Mj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oQgBf6vuSiIIf5Mj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oQgBf6vuSiIIf5Mj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oQgBf6vuSiIIf5Mj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oQgBf6vuSiIIf5Mj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oQgBf6vuSiIIf5Mj .cluster text{fill:#333;}#mermaid-svg-oQgBf6vuSiIIf5Mj .cluster span{color:#333;}#mermaid-svg-oQgBf6vuSiIIf5Mj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-oQgBf6vuSiIIf5Mj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oQgBf6vuSiIIf5Mj rect.text{fill:none;stroke-width:0;}#mermaid-svg-oQgBf6vuSiIIf5Mj .icon-shape,#mermaid-svg-oQgBf6vuSiIIf5Mj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oQgBf6vuSiIIf5Mj .icon-shape p,#mermaid-svg-oQgBf6vuSiIIf5Mj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oQgBf6vuSiIIf5Mj .icon-shape .label rect,#mermaid-svg-oQgBf6vuSiIIf5Mj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oQgBf6vuSiIIf5Mj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oQgBf6vuSiIIf5Mj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oQgBf6vuSiIIf5Mj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 1024B
512B
512B
256B
256B
128B
128B
它适合页、大块缓冲、DMA 区域和需要快速合并的场景。对 33B、70B 这类小对象可能造成明显内部碎片,因此通常与 Slab/对象池组合:Buddy 管大块,Slab 从大块中切固定对象。
关键路径使用保留池
一个常见故障是:系统已经内存紧张时,错误处理代码还需要申请一条告警消息、重连上下文或复位命令,但所有内存已经被普通业务耗尽,导致系统无法恢复。
应建立仅允许关键模块访问的保留池:
text
emergency_message_pool
recovery_context_pool
watchdog_report_pool
critical_dma_desc_pool
借鉴 Linux mempool 的思路,正常情况下可以从普通来源申请;普通来源失败时,关键路径可以使用预留元素。预留池必须有严格规则:
- 只允许少数已审计调用点。
- 不得用于提高普通业务成功率。
- 归还优先级高于普通清理。
- 使用一次必须产生诊断事件。
- 容量应按"最小恢复闭环"计算,而不是按平均流量计算。
#mermaid-svg-LPM6PLbty0GmOSAg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LPM6PLbty0GmOSAg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LPM6PLbty0GmOSAg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LPM6PLbty0GmOSAg .error-icon{fill:#552222;}#mermaid-svg-LPM6PLbty0GmOSAg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LPM6PLbty0GmOSAg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LPM6PLbty0GmOSAg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LPM6PLbty0GmOSAg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LPM6PLbty0GmOSAg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LPM6PLbty0GmOSAg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LPM6PLbty0GmOSAg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LPM6PLbty0GmOSAg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LPM6PLbty0GmOSAg .marker.cross{stroke:#333333;}#mermaid-svg-LPM6PLbty0GmOSAg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LPM6PLbty0GmOSAg p{margin:0;}#mermaid-svg-LPM6PLbty0GmOSAg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LPM6PLbty0GmOSAg .cluster-label text{fill:#333;}#mermaid-svg-LPM6PLbty0GmOSAg .cluster-label span{color:#333;}#mermaid-svg-LPM6PLbty0GmOSAg .cluster-label span p{background-color:transparent;}#mermaid-svg-LPM6PLbty0GmOSAg .label text,#mermaid-svg-LPM6PLbty0GmOSAg span{fill:#333;color:#333;}#mermaid-svg-LPM6PLbty0GmOSAg .node rect,#mermaid-svg-LPM6PLbty0GmOSAg .node circle,#mermaid-svg-LPM6PLbty0GmOSAg .node ellipse,#mermaid-svg-LPM6PLbty0GmOSAg .node polygon,#mermaid-svg-LPM6PLbty0GmOSAg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LPM6PLbty0GmOSAg .rough-node .label text,#mermaid-svg-LPM6PLbty0GmOSAg .node .label text,#mermaid-svg-LPM6PLbty0GmOSAg .image-shape .label,#mermaid-svg-LPM6PLbty0GmOSAg .icon-shape .label{text-anchor:middle;}#mermaid-svg-LPM6PLbty0GmOSAg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LPM6PLbty0GmOSAg .rough-node .label,#mermaid-svg-LPM6PLbty0GmOSAg .node .label,#mermaid-svg-LPM6PLbty0GmOSAg .image-shape .label,#mermaid-svg-LPM6PLbty0GmOSAg .icon-shape .label{text-align:center;}#mermaid-svg-LPM6PLbty0GmOSAg .node.clickable{cursor:pointer;}#mermaid-svg-LPM6PLbty0GmOSAg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LPM6PLbty0GmOSAg .arrowheadPath{fill:#333333;}#mermaid-svg-LPM6PLbty0GmOSAg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LPM6PLbty0GmOSAg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LPM6PLbty0GmOSAg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LPM6PLbty0GmOSAg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LPM6PLbty0GmOSAg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LPM6PLbty0GmOSAg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LPM6PLbty0GmOSAg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LPM6PLbty0GmOSAg .cluster text{fill:#333;}#mermaid-svg-LPM6PLbty0GmOSAg .cluster span{color:#333;}#mermaid-svg-LPM6PLbty0GmOSAg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LPM6PLbty0GmOSAg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LPM6PLbty0GmOSAg rect.text{fill:none;stroke-width:0;}#mermaid-svg-LPM6PLbty0GmOSAg .icon-shape,#mermaid-svg-LPM6PLbty0GmOSAg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LPM6PLbty0GmOSAg .icon-shape p,#mermaid-svg-LPM6PLbty0GmOSAg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LPM6PLbty0GmOSAg .icon-shape .label rect,#mermaid-svg-LPM6PLbty0GmOSAg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LPM6PLbty0GmOSAg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LPM6PLbty0GmOSAg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LPM6PLbty0GmOSAg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
关键申请
普通池可用?
普通池
保留池可用?
保留元素 + 记录告警
进入最小安全状态或受控复位
并发、ISR 与锁设计
内存池是否"线程安全"不是一个布尔标签,需要说明具体并发模型。
单生产者单消费者
环形缓冲最适合 SPSC。只要读写索引更新满足平台原子性和内存屏障要求,可以不使用互斥锁。典型场景是 ISR 写、任务读。
多生产者或多消费者
可选方案:
- 在池操作外使用短临界区。
- 使用自旋锁或互斥锁,取决于是否允许阻塞。
- 为每个生产者建立局部池,再由消费者归并。
- 使用无锁 MPSC/MPMC 队列,但必须处理 ABA、内存序和回收问题。
- 把所有释放操作投递给单一 Pool Manager 线程。
小型 MCU 上,短临界区往往比复杂无锁算法更可靠。固定池申请和释放只有几个指针操作,关中断时间可以严格测量。
ISR 分配规则
推荐规则:
- ISR 只能访问专门标记为 ISR-safe 的池。
- ISR 申请必须非阻塞。
- ISR 不得调用可能遍历、合并或获取可睡眠锁的通用堆。
- ISR 池耗尽时必须立即执行丢弃、覆盖、置位或唤醒高优先级任务等确定动作。
- ISR 释放是否允许,要看池的同步实现和优先级嵌套模型。
- 如果中断频率高,可采用"ISR 只取得描述符,任务负责复杂载荷处理"。
每核池与每上下文缓存
多核系统可使用 Per-CPU/Per-Core 小池,降低全局锁竞争。释放到其他核时可以进入远程释放队列,再由所属核批量回收。
在单核 MCU 上,也可以按上下文建立局部缓存:
text
ISR reserve cache
high-priority task cache
normal shared pool
但局部缓存会造成容量被切碎,必须设置归还或再平衡机制。
所有权与零拷贝
零拷贝不是"把指针到处传",而是把数据所有权协议化。
单所有者模型
任一时刻只有一个模块拥有块:
text
FREE -> PRODUCER -> QUEUE -> CONSUMER -> FREE
传入队列后,生产者不再访问对象。队列成功接收意味着所有权转移;发送失败则所有权仍属于生产者。
引用计数模型
同一载荷需要被多个消费者读取时,可以使用引用计数。
text
acquire: ref = 1
retain: ref++
release: ref--
ref == 0: return to pool
注意事项:
- 引用计数增减必须原子或受锁保护。
- 必须防止溢出和重复释放。
- 引用计数不能自动解决循环引用。
- ISR 与任务共享时要明确内存屏障。
- 调试版本应记录最后几个 retain/release 调用点。
对于极小系统,优先采用单所有者模型,因为它更容易验证。
句柄优于裸指针
可以使用包含池 ID、块索引和代数的句柄:
text
handle = { pool_id, block_index, generation }
代数每次重新分配时递增,可以检测旧句柄访问已经复用的块,降低 Use-After-Free 难以定位的问题。
DMA、Cache 与对齐
内存池设计必须了解硬件存储体系。
DMA 可访问性
某些 MCU 的 TCM、CCM 或紧耦合 SRAM 不能被特定 DMA 控制器访问。DMA 池应放在链接脚本指定区域,并在初始化时断言地址范围。
对齐
常见对齐要求:
- CPU 字宽对齐。
- Cache line 对齐。
- DMA burst 对齐。
- USB、Ethernet 描述符对齐。
- MPU 区域边界和大小限制。
- SIMD/DSP 指令对齐。
有效块大小必须是对齐后的尺寸:
text
block_stride = align_up(header_size + payload_size + tail_guard, alignment)
Cache 一致性
若 DMA 缓冲位于 Cacheable 区域,所有权转移时要执行正确的 clean/invalidate:
text
CPU -> DMA:
填充数据
clean cache
memory barrier
启动 DMA
DMA -> CPU:
等待 DMA 完成
memory barrier
invalidate cache
CPU 读取
不能只在分配器中"统一清 Cache",因为 Cache 操作取决于数据方向和所有权时机。池可以记录属性,但调用点必须遵守协议。
不同内存域分池
建议至少区分:
text
FAST_POOL 低延迟片上 SRAM
DMA_POOL DMA 可访问区
RETENTION_POOL 低功耗保持区
EXTERNAL_POOL 外部 SDRAM/PSRAM
SECURE_POOL 安全域或受 MPU 保护区
不要通过"申请后再判断地址是否可用"来处理硬件约束,而应在路由阶段选择正确池。
池容量如何量化
池容量不能只靠"估计差不多"。应从最大并发对象数、持有时间、突发量、在途操作和恢复余量计算。
固定对象池
text
N_pool >= N_active_max
+ N_inflight_max
+ N_queued_max
+ N_recovery_reserved
+ N_margin
其中:
N_active_max:业务正在处理的最大对象数。N_inflight_max:DMA、网络栈或异步设备持有的对象数。N_queued_max:队列中等待消费的最大对象数。N_recovery_reserved:错误恢复闭环所需保留对象。N_margin:测量误差和短暂抖动余量。
根据速率和持有时间估算
对于稳定流,可以使用类似 Little 定律的关系:
text
N_average ≈ λ * T_hold_average
但容量必须按峰值和长尾设计:
text
N_required >= λ_peak * T_hold_p99
+ burst_max
+ inflight_fixed
+ safety_margin
例如某消息生产峰值为 500 条/s,P99 持有时间为 40ms,最大瞬时突发为 8 条,设备内部固定在途 4 条,安全余量 25%:
text
λ_peak * T_hold_p99 = 500 * 0.040 = 20
基础需求 = 20 + 8 + 4 = 32
加入 25% 余量后,建议至少 40 个块
总内存预算
text
B_total =
Σ align_up(payload_i + header_i + guard_i, alignment_i) * N_i
+ metadata_bytes
+ queue_storage
+ arena_storage
+ ring_storage
+ emergency_reserve
必须同时计入:
- 池控制块。
- 每块头尾保护区。
- 位图或索引数组。
- RTOS 队列内部存储。
- 对齐填充。
- Cache line 浪费。
- DMA 描述符。
- 统计和调试字段。
- 链接器段间填充。
以 64KB RAM 系统为例
下面只是量化方法示例,不是固定模板。假设 64KB RAM 中有 8KB 用于中断栈、全局变量和硬件保留,剩余 56KB 可规划。
| 区域 | 预算 | 说明 |
|---|---|---|
| 任务栈与 RTOS 控制块 | 18KB | 通过栈高水位持续校准 |
| 128B 通信消息池 × 48 | 6KB | 含块头和对齐后约 128B |
| 32B 事件对象池 × 64 | 2KB | 控制事件和小消息 |
| 256B 数据分片池 × 32 | 8KB | 变长载荷可链式组合 |
| 4KB RX Ring | 4KB | UART/SPI/网络接收 |
| 4KB TX Ring | 4KB | 发送和日志输出 |
| 8KB 算法 Arena | 8KB | 单次处理完成后整体 reset |
| 紧急保留池 | 2KB | 告警、恢复和安全停机 |
| 受控通用堆 | 2KB | 仅低频非关键请求 |
| 监控、Canary 与余量 | 2KB | 统计及未预见开销 |
该例的关键不是具体数字,而是所有内存都能解释用途、上限和失败行为,而不是把 38KB 全部交给一个公共堆。
池耗尽策略
"池满了怎么办"必须在设计阶段回答。
| 业务类型 | 推荐策略 |
|---|---|
| 控制命令 | 保留配额;短等待;仍失败则进入安全状态 |
| 网络 RX | 丢弃新包或按协议施加背压,记录丢包 |
| 音频播放 | 补静音、降低质量或报告 underrun |
| 传感器采样 | 覆盖最旧数据、降低采样率或丢弃低优先级通道 |
| 日志 | 丢弃低级别日志,保留错误和关键事件 |
| UI/遥测 | 降频、合并更新、放弃本次刷新 |
| 文件系统后台任务 | 延迟重试,不阻塞实时 I/O |
| 固件升级 | 暂停进入新阶段,保持可恢复检查点 |
| 错误报告 | 使用独立保留池,不能依赖普通池 |
不要在关键路径盲目 assert
开发阶段可以断言池容量模型被违反,但量产运行中关键路径不能因为一次可恢复的资源耗尽直接崩溃。应区分:
- 编程错误:非法指针、重复释放、跨池释放,应快速暴露。
- 运行压力:池暂时耗尽,应执行已定义的降级或背压。
- 不可恢复状态:关键保留池也耗尽,进入安全状态或受控复位。
配额与资源隔离
即使使用固定池,多个模块共用同一池时仍可能发生资源饥饿。
可采用:
- 独立池:隔离最强,空间利用率可能降低。
- 共享池 + 模块配额:每模块有软/硬上限。
- 共享公共区 + 私有保底区:先使用公共区,关键模块有私有块。
- 优先级借用:高优先级可以借用低优先级额度,但反向不允许。
- 按流量整形:生产者必须取得 credit 才能创建新对象。
Credit 模型示例:
Consumer Queue Credit Manager Producer Consumer Queue Credit Manager Producer #mermaid-svg-hxLZeaQTjtMYSHd1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hxLZeaQTjtMYSHd1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hxLZeaQTjtMYSHd1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hxLZeaQTjtMYSHd1 .error-icon{fill:#552222;}#mermaid-svg-hxLZeaQTjtMYSHd1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hxLZeaQTjtMYSHd1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hxLZeaQTjtMYSHd1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hxLZeaQTjtMYSHd1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hxLZeaQTjtMYSHd1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hxLZeaQTjtMYSHd1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hxLZeaQTjtMYSHd1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hxLZeaQTjtMYSHd1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hxLZeaQTjtMYSHd1 .marker.cross{stroke:#333333;}#mermaid-svg-hxLZeaQTjtMYSHd1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hxLZeaQTjtMYSHd1 p{margin:0;}#mermaid-svg-hxLZeaQTjtMYSHd1 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hxLZeaQTjtMYSHd1 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-hxLZeaQTjtMYSHd1 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-hxLZeaQTjtMYSHd1 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-hxLZeaQTjtMYSHd1 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-hxLZeaQTjtMYSHd1 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-hxLZeaQTjtMYSHd1 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-hxLZeaQTjtMYSHd1 .sequenceNumber{fill:white;}#mermaid-svg-hxLZeaQTjtMYSHd1 #sequencenumber{fill:#333;}#mermaid-svg-hxLZeaQTjtMYSHd1 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-hxLZeaQTjtMYSHd1 .messageText{fill:#333;stroke:none;}#mermaid-svg-hxLZeaQTjtMYSHd1 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hxLZeaQTjtMYSHd1 .labelText,#mermaid-svg-hxLZeaQTjtMYSHd1 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-hxLZeaQTjtMYSHd1 .loopText,#mermaid-svg-hxLZeaQTjtMYSHd1 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-hxLZeaQTjtMYSHd1 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-hxLZeaQTjtMYSHd1 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-hxLZeaQTjtMYSHd1 .noteText,#mermaid-svg-hxLZeaQTjtMYSHd1 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-hxLZeaQTjtMYSHd1 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hxLZeaQTjtMYSHd1 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hxLZeaQTjtMYSHd1 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hxLZeaQTjtMYSHd1 .actorPopupMenu{position:absolute;}#mermaid-svg-hxLZeaQTjtMYSHd1 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-hxLZeaQTjtMYSHd1 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hxLZeaQTjtMYSHd1 .actor-man circle,#mermaid-svg-hxLZeaQTjtMYSHd1 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-hxLZeaQTjtMYSHd1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} request credit grant enqueue object deliver return credit
Credit 把"队列容量、对象池容量和生产速率"统一起来,避免队列还能入但对象池已被耗尽,或者对象已创建却无队列位置。
内存池 API 设计
推荐 API 显式返回错误,而不是隐藏等待或回退。
c
typedef enum mem_result {
MEM_RESULT_OK = 0,
MEM_RESULT_EMPTY,
MEM_RESULT_TIMEOUT,
MEM_RESULT_INVALID_ARGUMENT,
MEM_RESULT_INVALID_POINTER,
MEM_RESULT_DOUBLE_FREE,
MEM_RESULT_WRONG_POOL,
MEM_RESULT_CORRUPTED
} mem_result_t;
/**
* @brief Acquire one block from a fixed-size pool.
*
* @param pool Pool instance.
* @param timeout_ticks Maximum wait time. Use zero for non-blocking operation.
* @param[out] block Receives the allocated block on success.
*
* @return MEM_RESULT_OK on success, otherwise an error code.
*/
mem_result_t mem_fixed_acquire(mem_fixed_pool_t *pool,
uint32_t timeout_ticks,
void **block);
/**
* @brief Release a block to its owning pool.
*
* @param pool Pool instance.
* @param block Block previously returned by mem_fixed_acquire().
*
* @return MEM_RESULT_OK on success, otherwise an error code.
*/
mem_result_t mem_fixed_release(mem_fixed_pool_t *pool, void *block);
重要规则:
acquire成功后由调用者拥有。release后指针立即失效。- 超时语义必须统一。
- ISR 版本必须独立命名或通过 flags 严格限制。
- 不能在内部静默切换到另一个语义不同的池。
- 失败必须可统计。
- 调试版本应校验地址范围、块边界、池 ID 和状态。
指针合法性检查
给定池起始地址 base、块步长 stride 和块数 count:
text
offset = ptr - base
合法条件:
ptr >= base
ptr < base + stride * count
offset % stride == 0
再结合位图检查当前块是否处于已分配状态,可以检测跨池释放和重复释放。
调试保护与运行观测
固定池可靠性很高,但内存越界仍可能破坏空闲链表。建议在开发和测试版本启用以下机制。
前后 Canary
text
[header][front_guard][payload][tail_guard]
释放时检查 guard 是否保持预设值。若被破坏,记录池、块索引、申请调用点和最后所有者。
Poison
- 申请时填充
0xCD或指定模式。 - 释放时填充
0xDD。 - 未初始化池填充
0xA5。 - 关键结构体初始化后显式赋值,避免依赖填充值。
Poison 可帮助识别未初始化读取和 Use-After-Free,但会增加耗时,通常仅在调试版本启用。
统计字段
每个池至少记录:
text
capacity
block_size
current_used
peak_used
minimum_free
allocation_count
free_count
allocation_failures
timeout_count
fallback_count
corruption_count
longest_hold_time
可选记录:
text
per-caller allocation count
per-module quota usage
block owner
allocation timestamp
last allocation PC
last release PC
generation
持有时间监控
池容量不足有时不是块数太少,而是某个模块持有时间异常。可以在释放时计算:
text
hold_time = release_tick - allocation_tick
当持有时间超过阈值时记录告警。对引用计数对象,还应监控长期 ref_count > 0 的块。
栈与池统一观测
许多"堆不够"实际上是任务栈过度保守,占用了大量 RAM。应同时采集:
- 每个任务栈高水位。
- 每个池峰值。
- 环形缓冲高低水位。
- Arena 峰值 offset。
- 通用堆最小剩余量和最大连续块。
- DMA 描述符峰值。
只看总空闲堆无法完成全局调优。
故障注入
需要主动验证池耗尽路径,而不是等待现场发生。
建议支持:
text
fail_next(pool_id)
fail_after(pool_id, N)
limit_capacity(pool_id, temporary_count)
delay_release(pool_id, ticks)
corrupt_guard(pool_id, block_index)
force_queue_full(queue_id)
force_consumer_stall(channel_id)
测试目标包括:
- 普通池耗尽时关键控制仍能工作。
- 日志池耗尽只丢日志,不阻塞控制线程。
- 网络 RX 池耗尽能按预期丢包并恢复。
- 保留池被使用时产生诊断事件。
- 分配失败后所有权没有泄漏。
- 超时取消和异步完成竞争时不会重复释放。
- 软复位后池状态能正确重建。
长稳与压力测试
"运行三天没有崩溃"不是充分验证。测试应覆盖构造出的最坏序列。
分配序列测试
对通用堆和尺寸池执行:
- 小块与大块交错申请。
- 长生命周期小块夹在短生命周期大块之间。
- 随机尺寸、随机释放顺序。
- 周期性模块启停。
- 重复连接、断开、超时和重连。
- 请求取消与完成回调同时发生。
- 所有池接近满载时触发错误恢复。
池不变量
每次操作后可在测试版本检查:
text
free_count + used_count == capacity
空闲链表无环
空闲链表节点都位于池内并按块边界对齐
位图与链表状态一致
同一块不会同时出现在空闲表和已分配表
peak_used >= current_used
长稳指标
持续记录:
- 峰值是否随时间单调接近容量。
- 是否存在一直不归还的块。
- 失败率是否与负载相关。
- 最长持有时间是否增长。
- 通用堆最大连续空闲块是否持续缩小。
- 模块重启后资源是否回到基线。
- 经过异常恢复后池计数是否一致。
测试结束判定
不能只看"未 Crash"。至少应满足:
- 所有池当前占用回到预期基线。
- 分配和释放计数差符合永久对象数量。
- 没有 Canary 损坏。
- 没有非法释放和重复释放。
- 峰值低于容量并保留设计余量。
- 关键路径分配延迟满足上界。
- 非关键池耗尽不会扩大为全系统故障。
从现有 malloc/free 迁移
直接全局替换通常风险很大。推荐按以下流程迁移。
第一步:建立分配清单
通过封装、链接器 wrapper 或第三方库钩子,记录:
text
size
alignment
caller
module
allocation time
free time
thread/ISR context
peak concurrent count
failure path
形成分配尺寸直方图和生命周期图。
第二步:分类
对每个调用点回答:
- 对象尺寸是否固定。
- 最大并发数量是否可计算。
- 生命周期是否与会话或阶段一致。
- 是否在 ISR 或实时路径。
- 是否需要 DMA。
- 是否可以用环形缓冲。
- 是否允许失败。
- 失败时的业务策略是什么。
第三步:优先替换高频和关键路径
替换顺序通常是:
- ISR 和硬实时路径。
- 高频报文和帧对象。
- 反复创建销毁的协议对象。
- 大块连续数据流。
- 会话临时对象。
- 剩余低频不规则对象。
第四步:冻结运行期入口
当大部分调用已迁移后:
- 禁止 ISR 调用
malloc/free。 - 禁止关键线程调用通用堆。
- 初始化完成后禁止创建永久 RTOS 对象。
- 为第三方库设置独立堆和上限。
- 在 CI 中扫描新增直接
malloc/free调用。 - 对允许使用通用堆的调用点建立白名单。
第五步:缩小并监控通用堆
通用堆不必一开始就删除,可以逐步缩小。若缩小后仍能通过压力和长稳测试,说明分类和容量预算逐渐可靠。
常见错误
只把 malloc 换成一个大固定块池
如果所有对象都从 256B 块池申请,虽然没有外部碎片,但小对象会造成严重内部碎片,大对象仍无法满足。必须按对象或尺寸分层。
池数量越多越好
池太多会把空闲容量分散在不同池中,出现某池耗尽而其他池大量空闲。应通过真实分配统计确定池边界,并设计有限的借用或再平衡策略。
允许所有池自动回退到通用堆
这会掩盖容量错误,使测试阶段看起来稳定,现场高负载时才暴露。关键池应严格失败并产生诊断。
忽略队列容量和池容量的关系
如果对象池有 32 个块,但下游队列可积压 64 个指针,生产者可能先拿完全部对象却无法入队。对象池、队列和在途 DMA 必须统一预算。
认为固定池天然线程安全
固定块结构简单,但并发修改空闲链表仍需要原子操作、临界区或锁。ISR 嵌套、多核和优先级抢占都必须分析。
只看总空闲字节
总空闲量不能说明最大连续块、某一专用池余量、关键保留池状态或对象持有时间。
释放时不校验归属
把 A 池的块释放到 B 池会立即或延迟破坏链表。调试版本必须检查地址范围和块边界。
引用计数没有所有权规则
引用计数只能记录共享数量,不能替代"谁负责最后释放"的协议。异常取消、超时和回调竞争是最常见的重复释放来源。
为极小系统直接引入重型分配器
TLSF、Buddy 或完整 Slab 都有控制结构、代码体积和元数据成本。若系统只有十几种固定对象,简单专用池通常更合适。
方案选择矩阵
| 需求特征 | 首选方案 | 次选方案 |
|---|---|---|
| 启动后不再释放 | 静态分配、Bump/Arena | FreeRTOS heap_1 |
| 固定结构体、高频申请释放 | 专用对象池 | Zephyr slab、CMSIS Pool、ThreadX Block Pool |
| 多种小对象、尺寸集中 | 分级尺寸池 | Slab 类设计 |
| 连续字节流 | Ring Buffer | Stream Buffer |
| 变长报文、可分片 | 固定分片链 | lwIP pbuf 思路 |
| 同一阶段批量失效 | Arena/Region | 双 Arena |
| 可变尺寸且要求有界分配时间 | TLSF | 定制分级池 |
| 大块 2 的幂尺寸、页或 DMA 区 | Buddy | 专用大块池 |
| 必须在内存压力下继续前进 | 保留池 | Linux mempool 思路 |
| 低频、非关键、不规则请求 | 受控 heap_4/heap_5 |
TLSF |
| 多内存区域、属性不同 | 按区域分池 | heap_5 加外部路由层 |
一个推荐的落地组合
对多数 MCU/RTOS 产品,可以从以下组合起步:
text
1. 所有任务栈和 RTOS 内核对象静态创建。
2. 每种长期协议对象建立专用池。
3. 建立 32B、64B、128B、256B 少量尺寸池,覆盖低频通用小对象。
4. UART/SPI/I2S/ADC 使用静态 Ring Buffer。
5. 网络或变长报文使用 256B/512B 固定分片链。
6. 算法处理使用一个或两个 Arena。
7. DMA 缓冲和描述符使用独立对齐池。
8. 保留少量恢复对象和关键消息块。
9. 保留一个容量受限的通用堆,只允许白名单模块使用。
10. 每个池记录 current/peak/fail/hold-time,并提供故障注入。
对于极小系统,可进一步简化:
text
静态对象 + 两三个专用池 + 两个 Ring Buffer + 一个 Arena
对于更复杂系统,可增加:
text
多区域路由 + TLSF 动态域 + Per-Core 缓存 + MPU 隔离 + 统一遥测
面试回答组织方式
回答这类题时,可以按以下顺序展开。
- 先指出问题不只是总内存不足,而是外部碎片、生命周期交错、分配时延和资源隔离问题。
- 给出总体原则:静态优先、固定池为主、连续流用 Ring、阶段对象用 Arena、通用堆受控保留。
- 按对象尺寸和生命周期说明专用池、分级池、分片链和 Arena 的选择。
- 说明关键路径和 ISR 不能依赖普通堆,并设置独立保留池。
- 给出容量公式,强调最大并发、在途、队列、突发和恢复余量。
- 说明所有权、零拷贝、引用计数、DMA 对齐和 Cache 一致性。
- 说明池耗尽策略和模块配额,避免低优先级业务拖垮关键业务。
- 最后列出统计、Canary、Poison、故障注入和长稳测试方法。
- 补充开源参考:FreeRTOS 各 heap、Zephyr slab、CMSIS Memory Pool、ThreadX Pool、lwIP
memp/pbuf、ContikiMEMB、TLSF、Linux Slab/Buddy/mempool。
一句话概括:可靠的嵌入式内存管理不是寻找一个"永不碎片的万能 malloc",而是把不同尺寸、生命周期、实时等级和硬件属性的内存需求分流到可证明的专用机制中,并让容量、所有权、失败和监控都成为架构的一部分。
参考链接
RTOS 与嵌入式组件
- FreeRTOS Heap Memory Management
- FreeRTOS Static Message Buffer
- FreeRTOS Stream Buffer Receive
- Zephyr Memory Slabs
- Zephyr Memory Slab API
- CMSIS-RTOS2 Memory Pool
- CMSIS-RTOS2 Memory Management
- Eclipse ThreadX
- Contiki-NG Memory Management
- Contiki-NG MEMB API
网络缓冲与对象池
- lwIP Memory Pool API
- lwIP Internal Memory Pools
- lwIP Packet Buffers
- lwIP Heap and Memory Pool Options