freeRTOS中heap_1/2/3/4/5.c的对比


参考链接: link.

什么是堆

c语言中有两个概念经常一起被提及,堆和栈。
栈一般是系统调用,用于函数调用中保存现场(局部变量等)。不难想象,函数调用中主函数往往是最先执行,调用其他函数,然后最后返回。而最深层次的函数(内部不调用其他任何函数的函数)往往是执行完就离开,所以使用栈管理函数调用最为合适。
堆则一般是由程序员调用并管理。最直观的,一般malloc函数和free函数都是操作的这个空间,不过freeRTOS中一般则是使用freeRTOS中定义的函数(pvPortMalloc和vPortFree代替malloc和free函数)。

heap_1/2/4.c共有部分

freeRTOS是怎么代替c语言中的malloc和free函数的呢,可以看以下代码,代码的作用是申请一个很大的数组。

#if( configAPPLICATION_ALLOCATED_HEAP == 1 )
	extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#else
	static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

这里我的理解是,freeRTOS层使用c语言申请内存的方式申请一个很大的数组(此处configTOTAL_HEAP_SIZE 的数值是15360),极端情况下,我们把可以申请到的所有空间全部申请下来。然后用户层调用的(freeRTOS的)pvPortMalloc和vPortFree函数则是在此数组中操作,操作的函数原型自然是freeRTOS函数层中定义的。

heap_1.c

/*
 * The simplest possible implementation of pvPortMalloc().  Note that this
 * implementation does NOT allow allocated memory to be freed again.
 */

以上是heap_1.c开头处的一段注释,此文件最大的特点就是只负责申请内存,不负责内存的释放。所以这里我们只看pvPortMalloc函数。
在实际分配内存之前,函数要先进行一些操作,其中...代表省略的代码(下同)。

void *pvPortMalloc( size_t xWantedSize )
{
   
   
	...
	#if( portBYTE_ALIGNMENT != 1 )
	{
   
   
		if( xWantedSize & portBYTE_ALIGNMENT_MASK )
		{
   
   
			/* Byte alignment required. */
			xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
		}
	}
	#endif
	...

以上代码的作用是将要申请的内存区间扩充到比xWantedSize 大的最小的portBYTE_ALIGNMENT的倍数。此处xWantedSize & portBYTE_ALIGNMENT_MASK执行的操作应该等同于xWantedSize % portBYTE_ALIGNMENT,至于为什么要这么写我猜测是因为位运算要更快一些。
之后还要对申请到的数组进行同样的操作:

pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) & \
							  ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );

此处将原先申请到的数组的首地址调整到portBYTE_ALIGNMENT的倍数,不然之前的操作就没有意义了。试想,如果首地址不是portBYTE_ALIGNMENT的整数倍,之后即使按照portBYTE_ALIGNMENT的整数倍申请地址,申请到地址也不是portBYTE_ALIGNMENT的整数倍。
之后才是实际的内存分配的内容:

		//确保1,申请的地址没有超过内存中数组的最大值
		//2,地址没有溢出
		if( ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) &&
			( ( xNextFreeByte + xWantedSize ) > xNextFreeByte )	)
		{
   
   
			//返回当前申请到的地址
			pvReturn = pucAlignedHeap + xNextFreeByte;
			xNextFreeByte += xWantedSize;
		}

如果定义了宏configUSE_MALLOC_FAILED_HOOK,内存分配失败时会调用vApplicationMallocFailedHook函数

	#if( configUSE_MALLOC_FAILED_HOOK == 1 )
	{
   
   
		if( pvReturn == NULL )
		{
   
   
			extern void vApplicationMallocFailedHook( void );
			vApplicationMallocFailedHook();
		}
	}
	#endif

至此,heap_1.c中pvPortMalloc函数基本结束。

heap_2.c

/*
 * A sample implementation of pvPortMalloc() and vPortFree() that permits
 * allocated blocks to be freed, but does not combine adjacent free blocks
 * into a single larger block (and so will fragment memory).  See heap_4.c for
 * an equivalent that does combine adjacent blocks into single larger blocks.
 */

相比于heap_1.c中“管杀不管埋”的内存分配,heap_2.c不仅可以申请内存,还可以释放内存。美中不足的是,它不会合并相邻的空闲内存块。不过注释中也说明这一点会在heap_4.c中改进。
heap_2.c使用链表表示内存中的占用(空闲)情况,链表节点通过指针和一个size_t类型的数据确定了整个堆区的空闲内存的分配情况,链表项里的指针指向的是下一个链表项结点。那么如何确定空闲内存的地址呢:用户申请内存后,内存中实际分配的地址有两部分,第一部分是这个链表项,紧挨着链表项的才是用户申请的地址,所以链表项地址加上链表项结点的大小就是用户申请分配的地址。

typedef struct A_BLOCK_LINK
{
   
   
	struct A_BLOCK_LINK *pxNextFreeBlock;
	size_t xBlockSize;
} BlockLink_t;

prvHeapInit函数

第一次调用pvPortMalloc函数时,会先执行prvHeapInit函数,目的是初始化上面的链表。

static void prvHeapInit( void )
{
   
   
BlockLink_t *pxFirstFreeBlock;
uint8_t *pucAlignedHeap;

	//同heap_1.c中一样,将数组的首位对齐到portPOINTER_SIZE_TYPE 的整数倍
	pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );

	//用于指向链表的首地址(对齐过后的)
	xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
	xStart.xBlockSize = ( size_t ) 0;

	//标记链表的结尾
	xEnd.xBlockSize = configADJUSTED_HEAP_SIZE;
	xEnd.pxNextFreeBlock = NULL;

	//刚开始,只有一个空闲的内存块,其大小是整个内存空间
	pxFirstFreeBlock = ( void * ) pucAlignedHeap;
	pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE;
	pxFirstFreeBlock->pxNextFreeBlock = &xEnd;
}

pvPortMalloc函数

同heap_1.c,每次申请内存都要将要申请的内存大小拓展到portBYTE_ALIGNMENT的倍数,不同的是,这里的申请的内存除了用户申请使用的之外,还要额外添加A_BLOCK_LINK需要的内存。毕竟每次申请内存都要创建一个链表结点做记录,C语言层面能被申请的内存都被申请完了(ucHeap[ configTOTAL_HEAP_SIZE ]),自然要负责将这一部分的内存也申请下来。举个例子,假设A_BLOCK_LINK需要的内存大小是12,用户申请50个内存,那么至少要分配50+12=62个内存,这还不算内存对齐操作之后的部分,代码如下:

		if( xWantedSize > 0 )
		{
   
   
			xWantedSize += heapSTRUCT_SIZE;

			/* Ensure that blocks are always aligned to the required number of bytes. */
			if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0 )
			{
   
   
				/* Byte alignment required. */
				xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
			}
		}

以上都是对xWantedSize进行操作,操作完成后,从链表的第一项开始查找空闲内存比xWantedSize大的项:

pxPreviousBlock = &xStart;
pxBlock = xStart.pxNextFreeBlock;
while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
{
   
   
	pxPreviousBlock = pxBlock;
	pxBlock = pxBlock->pxNextFreeBlock;
}

如果pxBlock不是xEnd,代表找到了满足要求的链表项:

//返回地址,由于申请时连带申请了A_BLOCK_LINK链表项,所以地址要加heapSTRUCT_SIZE ,返回的是用户实际申请的地址
pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + heapSTRUCT_SIZE );

//由于当前列表项的内存空间被占用,不是free状态,所以将此链表项删除
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;

//如果原先空闲内存大于要分配的内存,分配完内存后原内存分为两个部分
//第一部分是分配出去的,第二的部分是依旧空闲的内存
if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
{
   
   
	//为剩余的空闲内存创建链表项,原先的链表项现在作为已分配的内存的链表项
	pxNewBlockLink 
<think> 我们正在讨论FreeRTOS的堆管理方案,从heap_1heap_5。用户希望从三个维度(是什么、什么时候用、怎么用)进行解释,并辅以基础示例和形象比喻。 根据引用[1],FreeRTOS在创建任务、队列、信号量等对象时需要动态分配内存,它提供了多种内存分配方案(位于Source/portable/MemMang目录下)。这些方案就是heap_1heap_5。 接下来,我将逐一解释每种堆管理方案: 1. heap_1: - 是什么:这是最简单的内存分配方案。它只允许分配内存,不允许释放内存。在系统启动时,先分配一个大数组作为堆,然后从这个数组中分配内存块。分配时,从堆的起始位置开始,依次分配,没有碎片问题,但无法回收内存。 - 什么时候用:适用于那些一旦分配就不再释放内存的嵌入式系统,或者系统运行期间不需要删除任务、队列等对象的情况。由于简单,它非常可靠。 - 怎么用:在FreeRTOSConfig.h中配置configTOTAL_HEAP_SIZE为堆的大小,然后将heap_1.c添加到工程中。 - 示例:比如在系统启动时创建了所有任务和内核对象,之后不再删除它们,那么就可以使用heap_1。 - 形象比喻:就像一个一次性的便签本,你只能一页一页地写,不能擦掉重写,写满了就不能再写了。 2. heap_2: - 是什么:支持分配和释放内存,但使用最佳匹配算法(best fit algorithm)。它不会合并相邻的空闲块,因此会产生碎片。适用于重复分配和释放相同大小内存块的场景。 - 什么时候用:适用于分配和释放的内存块大小总是相同的场景(例如创建和删除相同任务)。但由于碎片问题,不推荐用于分配释放大小多变的情况。注意:heap_2FreeRTOS v9.0.0后被标记为过时,由heap_4替代。 - 怎么用:同样配置configTOTAL_HEAP_SIZE,将heap_2.c加入工程。 - 示例:创建和删除相同优先级任务(任务栈大小相同)的场景。 - 形象比喻:就像一排固定大小的格子,你申请一个格子就占用,释放就空出来。但如果申请不同大小的格子,就会留下一些无法利用的小空隙(碎片)。 3. heap_3: - 是什么:它是对标准库malloc和free的简单封装,使用链接器设置的堆空间。它通过暂时挂起调度器来保证线程安全。 - 什么时候用:当系统中已经使用了标准库的malloc/free,并且想要保持一致性时。但要注意标准库的malloc/free可能不是实时安全的,且可能产生较大碎片。 - 怎么用:将heap_3.c加入工程,同时需要确保链接器配置了堆(例如在启动文件中设置的堆大小)。 - 示例:在需要与第三方库(使用malloc/free)交互时,可以使用heap_3。 - 形象比喻:就像使用一个公共仓库(标准库的堆),FreeRTOS在取用和归还物品时,会先挂起其他工人的工作(挂起调度器),避免多人同时操作仓库造成混乱。 4. heap_4: - 是什么:支持分配和释放内存,使用首次适应算法(first fit algorithm),并且会合并相邻的空闲块,减少碎片。它是最常用的方案。 - 什么时候用:适用于需要重复分配和释放不同大小内存块的场景。由于合并相邻空闲块,碎片问题得到缓解。 - 怎么用:配置configTOTAL_HEAP_SIZE,将heap_4.c加入工程。 - 示例:动态创建和删除不同大小任务或队列的场景。 - 形象比喻:像整理衣柜,把衣服(内存块)拿出来后,再把相邻的空位合并,这样就能放更大件的衣服。虽然可能还有小空隙,但通过合并,大块连续空间更多。 5. heap_5: - 是什么:在heap_4的基础上,支持堆空间分布在多个不连续的内存区域。比如,可以将内部RAM和外部SDRAM组合成一个堆。 - 什么时候用:当系统有多个非连续内存区域(如内部SRAM和外部SDRAM)需要作为一个堆使用时。 - 怎么用:除了配置configTOTAL_HEAP_SIZE外,还需要在程序开始时调用vPortDefineHeapRegions()函数,指定每个内存区域的起始地址和大小。将heap_5.c加入工程。 - 示例:在具有多个内存区域的复杂系统中,例如STM32H7系列,同时使用内部RAM和外部SDRAM。 - 形象比喻:就像把多个分散的仓库(不同的内存区域)统一管理成一个大的虚拟仓库,你从任意仓库取货,但管理上是一个整体。 总结: - heap_1: 只分配不释放,简单可靠,适用于静态系统。 - heap_2: 可分配释放,但过时,有碎片问题,不推荐使用(用heap_4替代)。 - heap_3: 使用标准库的malloc/free,适用于需要兼容标准库的场景。 - heap_4: 可分配释放,合并空闲块,减少碎片,最常用。 - heap_5: 在heap_4基础上支持非连续内存区域,适用于复杂内存布局。 注意:在FreeRTOS中,选择哪种堆管理方案,只需要将对应的heap_x.c文件包含到工程中即可,无需其他配置(除了heap_5需要初始化内存区域)。 以上内容参考了FreeRTOS官方文档和实际应用经验[^1][^3]。 </think>### FreeRTOS堆管理方案详解:heap_1heap_5 #### 1. **heap_1:静态内存分配器** - **是什么**: 最简单的内存管理方案,仅支持内存分配,不支持内存释放。启动时一次性分配整个堆空间,所有内存块连续分配。 - **何时使用**: 适用于**确定性系统**(如安全认证场景)或**永不删除对象**的应用(如启动后创建所有任务/队列并永久运行)。 - **如何使用**: ```c // FreeRTOSConfig.h #define configTOTAL_HEAP_SIZE ((size_t)10240) // 定义堆大小 // 工程中引入heap_1.c ``` - **基础示例**: 系统启动时创建3个固定任务: ```c void main() { xTaskCreate(task1, ...); // 分配堆内存 xTaskCreate(task2, ...); // 继续向后分配 xTaskCreate(task3, ...); // 无释放操作 vTaskStartScheduler(); } ``` - **形象比喻**: 像**一次性便签本**,只能顺序写满每一页,无法擦除重用。 --- #### 2. **heap_2:基础动态分配器** - **是什么**: 支持分配/释放,使用**最佳匹配算法**(寻找最接近需求大小的空闲块)。但**不合并相邻空闲块**,可能产生内存碎片。 - **何时使用**: 适用于**分配释放块大小固定**的场景(如重复创建/删除相同栈大小的任务),**已过时**(由heap_4替代)。 - **如何使用**: ```c // 选择heap_2.c并实现内存释放 vTaskDelete(xTaskHandle); // 释放任务内存 ``` - **碎片问题示例**: 分配10B+20B+10B后释放中间20B: ``` [已用10][空闲20][已用10] // 后续无法分配30B连续空间 ``` - **形象比喻**: 像**固定格子的储物柜**,取出物品后留下空位,但相邻空柜不打通,大物件放不进。 --- #### 3. **heap_3:标准库封装器** - **是什么**: 封装标准库`malloc()`/`free()`,通过**挂起调度器**保证线程安全。 - **何时使用**: 需**兼容现有malloc的代码**或**使用特殊内存控制器**(如硬件内存管理单元)。 - **如何使用**: ```c // FreeRTOSConfig.h #define configUSE_MALLOC_FAILED_HOOK 1 // 启用分配失败钩子 // 链接时需提供标准库堆空间 ``` - **注意事项**: 标准库分配器可能**非实时友好**,且**碎片问题更严重**[^3]。 --- #### 4. **heap_4:碎片优化分配器** - **是什么**: 支持分配/释放,使用**首次适应算法**+**相邻空闲块合并**,显著减少碎片。 - **何时使用**: **最通用方案**,适用于**频繁创建删除不同大小对象**的场景(如动态任务/队列)。 - **如何使用**: ```c // heap_4.c 已包含合并算法 void* pvBuffer = pvPortMalloc(50); // 分配 vPortFree(pvBuffer); // 释放并自动合并相邻块 ``` - **合并机制示例**: 释放中间块后自动合并: ``` [已用][空闲][空闲] → [已用][大空闲块] ``` - **形象比喻**: 像**智能整理的书架**,取出书后自动推拢相邻空位,腾出完整空间。 --- #### 5. **heap_5:非连续内存管理器** - **是什么**: 在heap_4基础上支持**多个不连续内存区域**(如SRAM+外部SDRAM)。 - **何时使用**: **复杂内存架构**系统(如MCU内部RAM不足,需扩展外部内存)。 - **如何使用**: ```c // 启动时注册内存区域 HeapRegion_t regions[] = { { (uint8_t*)0x20000000, 0x10000 }, // 内部SRAM 64KB { (uint8_t*)0xC0000000, 0x80000 } // 外部SDRAM 512KB }; vPortDefineHeapRegions(regions); // 关键初始化! ``` - **优势**: 可组合**高速/大容量存储区**,例如: - 任务栈放在**内部SRAM**(快速访问) - 数据缓冲区放在**外部SDRAM**(大容量) --- ### 总结对比表 | 方案 | 分配释放 | 碎片处理 | 适用场景 | 内存来源 | |----------|----------|----------|------------------------------|----------------| | **heap_1** | 仅分配 | 无碎片 | 静态系统 | 单一连续数组 | | **heap_2** | 支持 | 不合并 | 固定块操作(已过时) | 单一连续数组 | | **heap_3** | 支持 | 依赖库 | 兼容标准库 | 链接器定义堆 | | **heap_4** | 支持 | 自动合并 | 通用动态系统(推荐) | 单一连续数组 | | **heap_5** | 支持 | 自动合并 | 多存储器复杂系统 | 多个非连续区域 | > 提示:现代项目**优先选择heap_4**(平衡性能与碎片),内存受限时用heap_1,扩展硬件选heap_5[^1][^3]。 --- ### 相关问题 1. FreeRTOS中如何诊断堆内存不足问题? 2. heap_4的首次适应算法相比最佳适应有何优缺点? 3. 在RTOS中为什么需要避免内存碎片? 4. 如何为heap_5配置跨存储区的内存池? 5. FreeRTOS堆管理与裸机malloc/free有何本质区别
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值