FreeRTOS 内存管理我们知道在单片机程序中一般很少直接使用 C 标准库提供的malloc和free函数主要原因是它们容易产生内存碎片。但在 Linux 系统中却可以放心使用malloc这是为什么呢Linux 上实现的 glibc 标准库较为智能而专为单片机设计的 newlib‑nano 则比较简单。此外Linux 下使用的是虚拟地址堆空间可以非常大单片机则直接操作物理地址内存申请容易与栈等其他内存区域发生冲突。静态创建和动态创建的区别FreeRTOS 提供了自己的内存管理机制。细心的读者会发现FreeRTOS 中很多动态创建和静态创建的函数其核心区别就在于内存的来源动态创建任务控制块TCB和栈由 FreeRTOS 内部通过内存管理函数如pvPortMalloc自动分配。静态创建TCB 和栈由用户以全局变量或静态变量的形式预先提供。值得注意的一个细节是静态创建函数的原型TaskHandle_txTaskCreateStatic(TaskFunction_t pxTaskCode,constchar*constpcName,constconfigSTACK_DEPTH_TYPE uxStackDepth,void*constpvParameters,UBaseType_t uxPriority,StackType_t*constpuxStackBuffer,StaticTask_t*constpxTaskBuffer)PRIVILEGED_FUNCTION;这个函数没有直接出现我们熟知的 TCB 指针而是多了一个新的类型StaticTask_t。其定义如下略去部分条件编译选项typedefstructxSTATIC_TCB{void*pxDummy1;#if(portUSING_MPU_WRAPPERS1)xMPU_SETTINGS xDummy2;#endif#if(configUSE_CORE_AFFINITY1)(configNUMBER_OF_CORES1)UBaseType_t uxDummy26;#endifStaticListItem_t xDummy3[2];UBaseType_t uxDummy5;void*pxDummy6;#if(configNUMBER_OF_CORES1)BaseType_t xDummy23;UBaseType_t uxDummy24;#endifuint8_tucDummy7[configMAX_TASK_NAME_LEN];/* ... 更多占位成员 ... */}StaticTask_t;看起来全是“占位符”这难免让人困惑。之所以这样设计是因为静态/全局变量在编译时必须知道其大小而真正的 TCB 结构体在头文件中只以不透明指针的形式暴露隐藏内部细节。动态创建时FreeRTOS 自动申请内存并返回 TCB 指针用户无需关心内部布局但静态创建需要用户预先提供存放 TCB 的内存空间。StaticTask_t这个结构体的大小、成员偏移量都与真正的 TCB 完全一致后续使用时直接进行指针强制转换就可以像普通 TCB 一样操作。下面是一个典型的使用示例/* 定义栈大小 */#defineSTACK_SIZE128/* 1. 静态定义任务控制块 (TCB) – 这就是上面看到的“占位符”结构体 */StaticTask_t xTaskBuffer;/* 2. 静态定义任务栈 – 本质就是一个数组 */StackType_t xStack[STACK_SIZE];/* 3. 静态创建任务 */xHandlexTaskCreateStatic(vTaskCode,/* 任务函数 */StaticTask,/* 任务名 */STACK_SIZE,/* 栈深度单位字 */NULL,/* 参数 */1,/* 优先级 */xStack,/* 栈起始地址 – 用户提供的内存 */xTaskBuffer/* TCB 地址 – 用户提供的内存 */);总结为了封装任务控制块的内部细节FreeRTOS 没有直接暴露 TCB 结构体而是对外提供指针及操作函数。为了实现静态创建定义了一个与 TCB 完全等价的StaticTask_t结构体成员仅作占位不推荐直接访问使用时强制转换为 TCB 指针从而达到统一管理的目的。内存管理下面以目前最常用的heap_4管理方案为例进行分析其他 heap 方案的原理大同小异。内存初始化在heap_4.c中定义了一个静态数组uint8_tucHeap[configTOTAL_HEAP_SIZE];FreeRTOS 可管理的内存就来自这个数组。内存管理基于一个精简的链表每个节点代表一个空闲内存块typedefstructA_BLOCK_LINK{structA_BLOCK_LINK*pxNextFreeBlock;// 指向下一个空闲块 (4 字节)size_txBlockSize;// 本块的大小 (4 字节)}BlockLink_t;// 总共 8 字节staticBlockLink_t xStart;// 链表头全局变量staticBlockLink_t*pxEndNULL;// 链表尾指针初始化函数prvHeapInit()的简化流程如下staticvoidprvHeapInit(void){BlockLink_t*pxFirstFreeBlock;portPOINTER_SIZE_TYPE uxStartAddress,uxEndAddress;size_txTotalHeapSizeconfigTOTAL_HEAP_SIZE;uxStartAddress(portPOINTER_SIZE_TYPE)ucHeap;/* 按 portBYTE_ALIGNMENT通常为 8 字节对齐起始地址 */if((uxStartAddressportBYTE_ALIGNMENT_MASK)!0){uxStartAddress(portBYTE_ALIGNMENT-1);uxStartAddress~((portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK);xTotalHeapSize-(size_t)(uxStartAddress-(portPOINTER_SIZE_TYPE)ucHeap);}/* 初始化链表头 xStart */xStart.pxNextFreeBlock(void*)heapPROTECT_BLOCK_POINTER(uxStartAddress);xStart.xBlockSize0;/* 在堆数组末尾预留空间存放 pxEnd 节点 */uxEndAddressuxStartAddress(portPOINTER_SIZE_TYPE)xTotalHeapSize;uxEndAddress-(portPOINTER_SIZE_TYPE)xHeapStructSize;// 减去一个 BlockLink_t 的大小uxEndAddress~((portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK);pxEnd(BlockLink_t*)uxEndAddress;pxEnd-xBlockSize0;pxEnd-pxNextFreeBlockheapPROTECT_BLOCK_POINTER(NULL);/* 第一个空闲块占据整个堆数组除去末尾的 pxEnd */pxFirstFreeBlock(BlockLink_t*)uxStartAddress;pxFirstFreeBlock-xBlockSize(size_t)(uxEndAddress-(portPOINTER_SIZE_TYPE)pxFirstFreeBlock);pxFirstFreeBlock-pxNextFreeBlockheapPROTECT_BLOCK_POINTER(pxEnd);/* 更新全局统计变量 */xMinimumEverFreeBytesRemainingpxFirstFreeBlock-xBlockSize;xFreeBytesRemainingpxFirstFreeBlock-xBlockSize;}经过初始化后空闲链表的结构如下[xStart(全局链表头)] - [pxFirstFreeBlock(整个可用堆)] - [pxEnd(链表尾)]其中xStart不是堆数组的一部分而pxEnd保存在堆数组的最后 8 个字节中。内存申请pvPortMallocpvPortMalloc的实现比较复杂下面逐步分析。void*pvPortMalloc(size_txWantedSize){BlockLink_t*pxBlock;BlockLink_t*pxPreviousBlock;BlockLink_t*pxNewBlockLink;void*pvReturnNULL;size_txAdditionalRequiredSize;if(xWantedSize0){/* 检查是否会溢出然后加上 BlockLink_t 结构体的大小 */if(heapADD_WILL_OVERFLOW(xWantedSize,xHeapStructSize)0){xWantedSizexHeapStructSize;/* 按 portBYTE_ALIGNMENT 对齐 */if((xWantedSizeportBYTE_ALIGNMENT_MASK)!0x00){xAdditionalRequiredSizeportBYTE_ALIGNMENT-(xWantedSizeportBYTE_ALIGNMENT_MASK);if(heapADD_WILL_OVERFLOW(xWantedSize,xAdditionalRequiredSize)0){xWantedSizexAdditionalRequiredSize;}else{xWantedSize0;}}}else{xWantedSize0;}}/* 挂起调度器保护临界区 */vTaskSuspendAll();{if(pxEndNULL){prvHeapInit();// 首次使用时初始化堆}if(heapBLOCK_SIZE_IS_VALID(xWantedSize)!0){if((xWantedSize0)(xWantedSizexFreeBytesRemaining)){pxPreviousBlockxStart;pxBlockheapPROTECT_BLOCK_POINTER(xStart.pxNextFreeBlock);heapVALIDATE_BLOCK_POINTER(pxBlock);/* 找到第一个能满足需求大小的空闲块 */while((pxBlock-xBlockSizexWantedSize)(pxBlock-pxNextFreeBlock!heapPROTECT_BLOCK_POINTER(NULL))){pxPreviousBlockpxBlock;pxBlockheapPROTECT_BLOCK_POINTER(pxBlock-pxNextFreeBlock);heapVALIDATE_BLOCK_POINTER(pxBlock);}if(pxBlock!pxEnd){/* 返回给用户的地址应跳过 BlockLink_t 结构体 */pvReturn(void*)(((uint8_t*)heapPROTECT_BLOCK_POINTER(pxPreviousBlock-pxNextFreeBlock))xHeapStructSize);heapVALIDATE_BLOCK_POINTER(pvReturn);/* 将当前块从空闲链表中移除 */pxPreviousBlock-pxNextFreeBlockpxBlock-pxNextFreeBlock;/* 如果剩余空间足够大大于 heapMINIMUM_BLOCK_SIZE则拆分出一个新的空闲块 */if((pxBlock-xBlockSize-xWantedSize)heapMINIMUM_BLOCK_SIZE){pxNewBlockLink(void*)(((uint8_t*)pxBlock)xWantedSize);configASSERT((((size_t)pxNewBlockLink)portBYTE_ALIGNMENT_MASK)0);pxNewBlockLink-xBlockSizepxBlock-xBlockSize-xWantedSize;pxBlock-xBlockSizexWantedSize;/* 将新的空闲块插入链表 */pxNewBlockLink-pxNextFreeBlockpxPreviousBlock-pxNextFreeBlock;pxPreviousBlock-pxNextFreeBlockheapPROTECT_BLOCK_POINTER(pxNewBlockLink);}else{/* 剩余空间太小不再拆分整个块都分配给用户 */mtCOVERAGE_TEST_MARKER();}/* 更新统计信息 */xFreeBytesRemaining-pxBlock-xBlockSize;if(xFreeBytesRemainingxMinimumEverFreeBytesRemaining){xMinimumEverFreeBytesRemainingxFreeBytesRemaining;}heapALLOCATE_BLOCK(pxBlock);pxBlock-pxNextFreeBlockNULL;xNumberOfSuccessfulAllocations;}}}traceMALLOC(pvReturn,xWantedSize);}(void)xTaskResumeAll();#if(configUSE_MALLOC_FAILED_HOOK1)if(pvReturnNULL){vApplicationMallocFailedHook();}#endifconfigASSERT((((size_t)pvReturn)(size_t)portBYTE_ALIGNMENT_MASK)0);returnpvReturn;}分配过程示意图BL表示BlockLink_t结构体初始状态 [xStart] - [ BL | 空闲数据区 ] - [ BL (pxEnd) | NULL ] 分配后若拆分了空闲块 [xStart] - [ BL | 已分配数据区 ] - [ BL | 新的小块空闲区 ] - [ BL (pxEnd) | NULL ]注意BL结构体直接保存在堆内存中紧跟在它后面的是用户可用的数据区。内存释放vPortFree释放时将用户传入的指针回退一个BlockLink_t的大小得到管理块的头指针然后将其重新插入空闲链表并尝试合并相邻的空闲块。voidvPortFree(void*pv){uint8_t*puc(uint8_t*)pv;BlockLink_t*pxLink;if(pv!NULL){puc-xHeapStructSize;// 回退到管理块起始pxLink(void*)puc;heapVALIDATE_BLOCK_POINTER(pxLink);configASSERT(heapBLOCK_IS_ALLOCATED(pxLink)!0);configASSERT(pxLink-pxNextFreeBlockNULL);if(heapBLOCK_IS_ALLOCATED(pxLink)!0){if(pxLink-pxNextFreeBlockNULL){heapFREE_BLOCK(pxLink);#if(configHEAP_CLEAR_MEMORY_ON_FREE1)if(heapSUBTRACT_WILL_UNDERFLOW(pxLink-xBlockSize,xHeapStructSize)0){(void)memset(pucxHeapStructSize,0,pxLink-xBlockSize-xHeapStructSize);}#endifvTaskSuspendAll();{xFreeBytesRemainingpxLink-xBlockSize;traceFREE(pv,pxLink-xBlockSize);prvInsertBlockIntoFreeList(((BlockLink_t*)pxLink));xNumberOfSuccessfulFrees;}(void)xTaskResumeAll();}}}}核心的合并函数prvInsertBlockIntoFreeList负责将释放的块插入合适位置并与前后空闲块合并staticvoidprvInsertBlockIntoFreeList(BlockLink_t*pxBlockToInsert){BlockLink_t*pxIterator;uint8_t*puc;/* 寻找插入位置使链表按地址递增有序 */for(pxIteratorxStart;heapPROTECT_BLOCK_POINTER(pxIterator-pxNextFreeBlock)pxBlockToInsert;pxIteratorheapPROTECT_BLOCK_POINTER(pxIterator-pxNextFreeBlock)){}/* 与前面的空闲块合并a 和 b 合并 */puc(uint8_t*)pxIterator;if((pucpxIterator-xBlockSize)(uint8_t*)pxBlockToInsert){pxIterator-xBlockSizepxBlockToInsert-xBlockSize;pxBlockToInsertpxIterator;}/* 与后面的空闲块合并ab 和 c 合并 */puc(uint8_t*)pxBlockToInsert;if((pucpxBlockToInsert-xBlockSize)(uint8_t*)heapPROTECT_BLOCK_POINTER(pxIterator-pxNextFreeBlock)){if(heapPROTECT_BLOCK_POINTER(pxIterator-pxNextFreeBlock)!pxEnd){pxBlockToInsert-xBlockSizeheapPROTECT_BLOCK_POINTER(pxIterator-pxNextFreeBlock)-xBlockSize;pxBlockToInsert-pxNextFreeBlockheapPROTECT_BLOCK_POINTER(pxIterator-pxNextFreeBlock)-pxNextFreeBlock;}else{pxBlockToInsert-pxNextFreeBlockheapPROTECT_BLOCK_POINTER(pxEnd);}}else{pxBlockToInsert-pxNextFreeBlockpxIterator-pxNextFreeBlock;}/* 更新前驱节点的 next 指针 */if(pxIterator!pxBlockToInsert){pxIterator-pxNextFreeBlockheapPROTECT_BLOCK_POINTER(pxBlockToInsert);}}合并逻辑示意a、b、c分别代表前一个空闲块、要释放的块、后一个空闲块先判断a的结束地址是否等于b的起始地址若是则合并a和b。再判断合并后或原本的b的结束地址是否等于c的起始地址若是则继续合并c。最终将合并后的大块正确链接到空闲链表中。关键数据结构回顾staticBlockLink_t xStart;// 链表头静态变量staticBlockLink_t*pxEndNULL;// 链表尾指针指向堆内最后一个 BlockLink_tuint8_tucHeap[configTOTAL_HEAP_SIZE];// 堆数组typedefstructA_BLOCK_LINK{structA_BLOCK_LINK*pxNextFreeBlock;// 下一个空闲块size_txBlockSize;// 本块大小包含管理块自身}BlockLink_t;xStart.pxNextFreeBlock永远指向第一个空闲块可能位于堆数组起始位置。xStart.xBlockSize始终为 0。pxEnd指向堆数组末尾预留的 8 字节其pxNextFreeBlock为NULLxBlockSize为 0。链表永远只是连接空闲块被申请使用块通过地址没有直接在链表中但是通过地址加减进行管理。空闲链表的节点都是直接保存在堆中的只有xStart是静态全局变量没有保存在堆中。申请内存时从xStart开始遍历找到第一个足够大的空闲块从中分割出所需部分若剩余空间足够小则不再分割。被分配出去的块会从空闲链表中移除且其pxNextFreeBlock被设为NULL以标记为已分配。释放内存时根据地址回算得到管理块将其重新插入空闲链表并自动进行前后合并。以上就是 FreeRTOS heap_4 内存管理的核心原理。理解这些细节有助于在资源受限的嵌入式系统中更高效地使用动态内存。
FreeRTOS源码解读系列文章之六
FreeRTOS 内存管理我们知道在单片机程序中一般很少直接使用 C 标准库提供的malloc和free函数主要原因是它们容易产生内存碎片。但在 Linux 系统中却可以放心使用malloc这是为什么呢Linux 上实现的 glibc 标准库较为智能而专为单片机设计的 newlib‑nano 则比较简单。此外Linux 下使用的是虚拟地址堆空间可以非常大单片机则直接操作物理地址内存申请容易与栈等其他内存区域发生冲突。静态创建和动态创建的区别FreeRTOS 提供了自己的内存管理机制。细心的读者会发现FreeRTOS 中很多动态创建和静态创建的函数其核心区别就在于内存的来源动态创建任务控制块TCB和栈由 FreeRTOS 内部通过内存管理函数如pvPortMalloc自动分配。静态创建TCB 和栈由用户以全局变量或静态变量的形式预先提供。值得注意的一个细节是静态创建函数的原型TaskHandle_txTaskCreateStatic(TaskFunction_t pxTaskCode,constchar*constpcName,constconfigSTACK_DEPTH_TYPE uxStackDepth,void*constpvParameters,UBaseType_t uxPriority,StackType_t*constpuxStackBuffer,StaticTask_t*constpxTaskBuffer)PRIVILEGED_FUNCTION;这个函数没有直接出现我们熟知的 TCB 指针而是多了一个新的类型StaticTask_t。其定义如下略去部分条件编译选项typedefstructxSTATIC_TCB{void*pxDummy1;#if(portUSING_MPU_WRAPPERS1)xMPU_SETTINGS xDummy2;#endif#if(configUSE_CORE_AFFINITY1)(configNUMBER_OF_CORES1)UBaseType_t uxDummy26;#endifStaticListItem_t xDummy3[2];UBaseType_t uxDummy5;void*pxDummy6;#if(configNUMBER_OF_CORES1)BaseType_t xDummy23;UBaseType_t uxDummy24;#endifuint8_tucDummy7[configMAX_TASK_NAME_LEN];/* ... 更多占位成员 ... */}StaticTask_t;看起来全是“占位符”这难免让人困惑。之所以这样设计是因为静态/全局变量在编译时必须知道其大小而真正的 TCB 结构体在头文件中只以不透明指针的形式暴露隐藏内部细节。动态创建时FreeRTOS 自动申请内存并返回 TCB 指针用户无需关心内部布局但静态创建需要用户预先提供存放 TCB 的内存空间。StaticTask_t这个结构体的大小、成员偏移量都与真正的 TCB 完全一致后续使用时直接进行指针强制转换就可以像普通 TCB 一样操作。下面是一个典型的使用示例/* 定义栈大小 */#defineSTACK_SIZE128/* 1. 静态定义任务控制块 (TCB) – 这就是上面看到的“占位符”结构体 */StaticTask_t xTaskBuffer;/* 2. 静态定义任务栈 – 本质就是一个数组 */StackType_t xStack[STACK_SIZE];/* 3. 静态创建任务 */xHandlexTaskCreateStatic(vTaskCode,/* 任务函数 */StaticTask,/* 任务名 */STACK_SIZE,/* 栈深度单位字 */NULL,/* 参数 */1,/* 优先级 */xStack,/* 栈起始地址 – 用户提供的内存 */xTaskBuffer/* TCB 地址 – 用户提供的内存 */);总结为了封装任务控制块的内部细节FreeRTOS 没有直接暴露 TCB 结构体而是对外提供指针及操作函数。为了实现静态创建定义了一个与 TCB 完全等价的StaticTask_t结构体成员仅作占位不推荐直接访问使用时强制转换为 TCB 指针从而达到统一管理的目的。内存管理下面以目前最常用的heap_4管理方案为例进行分析其他 heap 方案的原理大同小异。内存初始化在heap_4.c中定义了一个静态数组uint8_tucHeap[configTOTAL_HEAP_SIZE];FreeRTOS 可管理的内存就来自这个数组。内存管理基于一个精简的链表每个节点代表一个空闲内存块typedefstructA_BLOCK_LINK{structA_BLOCK_LINK*pxNextFreeBlock;// 指向下一个空闲块 (4 字节)size_txBlockSize;// 本块的大小 (4 字节)}BlockLink_t;// 总共 8 字节staticBlockLink_t xStart;// 链表头全局变量staticBlockLink_t*pxEndNULL;// 链表尾指针初始化函数prvHeapInit()的简化流程如下staticvoidprvHeapInit(void){BlockLink_t*pxFirstFreeBlock;portPOINTER_SIZE_TYPE uxStartAddress,uxEndAddress;size_txTotalHeapSizeconfigTOTAL_HEAP_SIZE;uxStartAddress(portPOINTER_SIZE_TYPE)ucHeap;/* 按 portBYTE_ALIGNMENT通常为 8 字节对齐起始地址 */if((uxStartAddressportBYTE_ALIGNMENT_MASK)!0){uxStartAddress(portBYTE_ALIGNMENT-1);uxStartAddress~((portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK);xTotalHeapSize-(size_t)(uxStartAddress-(portPOINTER_SIZE_TYPE)ucHeap);}/* 初始化链表头 xStart */xStart.pxNextFreeBlock(void*)heapPROTECT_BLOCK_POINTER(uxStartAddress);xStart.xBlockSize0;/* 在堆数组末尾预留空间存放 pxEnd 节点 */uxEndAddressuxStartAddress(portPOINTER_SIZE_TYPE)xTotalHeapSize;uxEndAddress-(portPOINTER_SIZE_TYPE)xHeapStructSize;// 减去一个 BlockLink_t 的大小uxEndAddress~((portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK);pxEnd(BlockLink_t*)uxEndAddress;pxEnd-xBlockSize0;pxEnd-pxNextFreeBlockheapPROTECT_BLOCK_POINTER(NULL);/* 第一个空闲块占据整个堆数组除去末尾的 pxEnd */pxFirstFreeBlock(BlockLink_t*)uxStartAddress;pxFirstFreeBlock-xBlockSize(size_t)(uxEndAddress-(portPOINTER_SIZE_TYPE)pxFirstFreeBlock);pxFirstFreeBlock-pxNextFreeBlockheapPROTECT_BLOCK_POINTER(pxEnd);/* 更新全局统计变量 */xMinimumEverFreeBytesRemainingpxFirstFreeBlock-xBlockSize;xFreeBytesRemainingpxFirstFreeBlock-xBlockSize;}经过初始化后空闲链表的结构如下[xStart(全局链表头)] - [pxFirstFreeBlock(整个可用堆)] - [pxEnd(链表尾)]其中xStart不是堆数组的一部分而pxEnd保存在堆数组的最后 8 个字节中。内存申请pvPortMallocpvPortMalloc的实现比较复杂下面逐步分析。void*pvPortMalloc(size_txWantedSize){BlockLink_t*pxBlock;BlockLink_t*pxPreviousBlock;BlockLink_t*pxNewBlockLink;void*pvReturnNULL;size_txAdditionalRequiredSize;if(xWantedSize0){/* 检查是否会溢出然后加上 BlockLink_t 结构体的大小 */if(heapADD_WILL_OVERFLOW(xWantedSize,xHeapStructSize)0){xWantedSizexHeapStructSize;/* 按 portBYTE_ALIGNMENT 对齐 */if((xWantedSizeportBYTE_ALIGNMENT_MASK)!0x00){xAdditionalRequiredSizeportBYTE_ALIGNMENT-(xWantedSizeportBYTE_ALIGNMENT_MASK);if(heapADD_WILL_OVERFLOW(xWantedSize,xAdditionalRequiredSize)0){xWantedSizexAdditionalRequiredSize;}else{xWantedSize0;}}}else{xWantedSize0;}}/* 挂起调度器保护临界区 */vTaskSuspendAll();{if(pxEndNULL){prvHeapInit();// 首次使用时初始化堆}if(heapBLOCK_SIZE_IS_VALID(xWantedSize)!0){if((xWantedSize0)(xWantedSizexFreeBytesRemaining)){pxPreviousBlockxStart;pxBlockheapPROTECT_BLOCK_POINTER(xStart.pxNextFreeBlock);heapVALIDATE_BLOCK_POINTER(pxBlock);/* 找到第一个能满足需求大小的空闲块 */while((pxBlock-xBlockSizexWantedSize)(pxBlock-pxNextFreeBlock!heapPROTECT_BLOCK_POINTER(NULL))){pxPreviousBlockpxBlock;pxBlockheapPROTECT_BLOCK_POINTER(pxBlock-pxNextFreeBlock);heapVALIDATE_BLOCK_POINTER(pxBlock);}if(pxBlock!pxEnd){/* 返回给用户的地址应跳过 BlockLink_t 结构体 */pvReturn(void*)(((uint8_t*)heapPROTECT_BLOCK_POINTER(pxPreviousBlock-pxNextFreeBlock))xHeapStructSize);heapVALIDATE_BLOCK_POINTER(pvReturn);/* 将当前块从空闲链表中移除 */pxPreviousBlock-pxNextFreeBlockpxBlock-pxNextFreeBlock;/* 如果剩余空间足够大大于 heapMINIMUM_BLOCK_SIZE则拆分出一个新的空闲块 */if((pxBlock-xBlockSize-xWantedSize)heapMINIMUM_BLOCK_SIZE){pxNewBlockLink(void*)(((uint8_t*)pxBlock)xWantedSize);configASSERT((((size_t)pxNewBlockLink)portBYTE_ALIGNMENT_MASK)0);pxNewBlockLink-xBlockSizepxBlock-xBlockSize-xWantedSize;pxBlock-xBlockSizexWantedSize;/* 将新的空闲块插入链表 */pxNewBlockLink-pxNextFreeBlockpxPreviousBlock-pxNextFreeBlock;pxPreviousBlock-pxNextFreeBlockheapPROTECT_BLOCK_POINTER(pxNewBlockLink);}else{/* 剩余空间太小不再拆分整个块都分配给用户 */mtCOVERAGE_TEST_MARKER();}/* 更新统计信息 */xFreeBytesRemaining-pxBlock-xBlockSize;if(xFreeBytesRemainingxMinimumEverFreeBytesRemaining){xMinimumEverFreeBytesRemainingxFreeBytesRemaining;}heapALLOCATE_BLOCK(pxBlock);pxBlock-pxNextFreeBlockNULL;xNumberOfSuccessfulAllocations;}}}traceMALLOC(pvReturn,xWantedSize);}(void)xTaskResumeAll();#if(configUSE_MALLOC_FAILED_HOOK1)if(pvReturnNULL){vApplicationMallocFailedHook();}#endifconfigASSERT((((size_t)pvReturn)(size_t)portBYTE_ALIGNMENT_MASK)0);returnpvReturn;}分配过程示意图BL表示BlockLink_t结构体初始状态 [xStart] - [ BL | 空闲数据区 ] - [ BL (pxEnd) | NULL ] 分配后若拆分了空闲块 [xStart] - [ BL | 已分配数据区 ] - [ BL | 新的小块空闲区 ] - [ BL (pxEnd) | NULL ]注意BL结构体直接保存在堆内存中紧跟在它后面的是用户可用的数据区。内存释放vPortFree释放时将用户传入的指针回退一个BlockLink_t的大小得到管理块的头指针然后将其重新插入空闲链表并尝试合并相邻的空闲块。voidvPortFree(void*pv){uint8_t*puc(uint8_t*)pv;BlockLink_t*pxLink;if(pv!NULL){puc-xHeapStructSize;// 回退到管理块起始pxLink(void*)puc;heapVALIDATE_BLOCK_POINTER(pxLink);configASSERT(heapBLOCK_IS_ALLOCATED(pxLink)!0);configASSERT(pxLink-pxNextFreeBlockNULL);if(heapBLOCK_IS_ALLOCATED(pxLink)!0){if(pxLink-pxNextFreeBlockNULL){heapFREE_BLOCK(pxLink);#if(configHEAP_CLEAR_MEMORY_ON_FREE1)if(heapSUBTRACT_WILL_UNDERFLOW(pxLink-xBlockSize,xHeapStructSize)0){(void)memset(pucxHeapStructSize,0,pxLink-xBlockSize-xHeapStructSize);}#endifvTaskSuspendAll();{xFreeBytesRemainingpxLink-xBlockSize;traceFREE(pv,pxLink-xBlockSize);prvInsertBlockIntoFreeList(((BlockLink_t*)pxLink));xNumberOfSuccessfulFrees;}(void)xTaskResumeAll();}}}}核心的合并函数prvInsertBlockIntoFreeList负责将释放的块插入合适位置并与前后空闲块合并staticvoidprvInsertBlockIntoFreeList(BlockLink_t*pxBlockToInsert){BlockLink_t*pxIterator;uint8_t*puc;/* 寻找插入位置使链表按地址递增有序 */for(pxIteratorxStart;heapPROTECT_BLOCK_POINTER(pxIterator-pxNextFreeBlock)pxBlockToInsert;pxIteratorheapPROTECT_BLOCK_POINTER(pxIterator-pxNextFreeBlock)){}/* 与前面的空闲块合并a 和 b 合并 */puc(uint8_t*)pxIterator;if((pucpxIterator-xBlockSize)(uint8_t*)pxBlockToInsert){pxIterator-xBlockSizepxBlockToInsert-xBlockSize;pxBlockToInsertpxIterator;}/* 与后面的空闲块合并ab 和 c 合并 */puc(uint8_t*)pxBlockToInsert;if((pucpxBlockToInsert-xBlockSize)(uint8_t*)heapPROTECT_BLOCK_POINTER(pxIterator-pxNextFreeBlock)){if(heapPROTECT_BLOCK_POINTER(pxIterator-pxNextFreeBlock)!pxEnd){pxBlockToInsert-xBlockSizeheapPROTECT_BLOCK_POINTER(pxIterator-pxNextFreeBlock)-xBlockSize;pxBlockToInsert-pxNextFreeBlockheapPROTECT_BLOCK_POINTER(pxIterator-pxNextFreeBlock)-pxNextFreeBlock;}else{pxBlockToInsert-pxNextFreeBlockheapPROTECT_BLOCK_POINTER(pxEnd);}}else{pxBlockToInsert-pxNextFreeBlockpxIterator-pxNextFreeBlock;}/* 更新前驱节点的 next 指针 */if(pxIterator!pxBlockToInsert){pxIterator-pxNextFreeBlockheapPROTECT_BLOCK_POINTER(pxBlockToInsert);}}合并逻辑示意a、b、c分别代表前一个空闲块、要释放的块、后一个空闲块先判断a的结束地址是否等于b的起始地址若是则合并a和b。再判断合并后或原本的b的结束地址是否等于c的起始地址若是则继续合并c。最终将合并后的大块正确链接到空闲链表中。关键数据结构回顾staticBlockLink_t xStart;// 链表头静态变量staticBlockLink_t*pxEndNULL;// 链表尾指针指向堆内最后一个 BlockLink_tuint8_tucHeap[configTOTAL_HEAP_SIZE];// 堆数组typedefstructA_BLOCK_LINK{structA_BLOCK_LINK*pxNextFreeBlock;// 下一个空闲块size_txBlockSize;// 本块大小包含管理块自身}BlockLink_t;xStart.pxNextFreeBlock永远指向第一个空闲块可能位于堆数组起始位置。xStart.xBlockSize始终为 0。pxEnd指向堆数组末尾预留的 8 字节其pxNextFreeBlock为NULLxBlockSize为 0。链表永远只是连接空闲块被申请使用块通过地址没有直接在链表中但是通过地址加减进行管理。空闲链表的节点都是直接保存在堆中的只有xStart是静态全局变量没有保存在堆中。申请内存时从xStart开始遍历找到第一个足够大的空闲块从中分割出所需部分若剩余空间足够小则不再分割。被分配出去的块会从空闲链表中移除且其pxNextFreeBlock被设为NULL以标记为已分配。释放内存时根据地址回算得到管理块将其重新插入空闲链表并自动进行前后合并。以上就是 FreeRTOS heap_4 内存管理的核心原理。理解这些细节有助于在资源受限的嵌入式系统中更高效地使用动态内存。