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 = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );

	//计算链表项的大小
	pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
	pxBlock->xBlockSize = xWantedSize;

	//将新创建的链表项插入链表中
	prvInsertBlockIntoFreeList( ( pxNewBlockLink ) );
}

xFreeBytesRemaining -= pxBlock->xBlockSize;

此处的prvInsertBlockIntoFreeList是一个宏定义,不难看出,整个链表是以空闲区域的大小由小到大链接的,所以上文中申请区域时申请的是能够满足条件的最小区域,避免了内存分割:

#define prvInsertBlockIntoFreeList( pxBlockToInsert )								\
{																					\
BlockLink_t *pxIterator;															\
size_t xBlockSize;																	\
																					\
	xBlockSize = pxBlockToInsert->xBlockSize;										\
																					\
	/* 找到这个链表项应该插入的位置 */	\
	for( pxIterator = &xStart; pxIterator->pxNextFreeBlock->xBlockSize < xBlockSize; pxIterator = pxIterator->pxNextFreeBlock )	\
	{																				\
		/* There is nothing to do here - just iterate to the correct position. */	\
	}																				\
																					\
	/* 插入链表项 */																\
	pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;					\
	pxIterator->pxNextFreeBlock = pxBlockToInsert;									\
}

至此,heap_2.c的pvPortMalloc函数分析完毕。

vPortFree函数

void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;

	if( pv != NULL )
	{
		//用户传入的是链表项之后的地址,要将链表项也释放掉
		puc -= heapSTRUCT_SIZE;

		//防止编译器发出字节对齐警告
		pxLink = ( void * ) puc;

		vTaskSuspendAll();
		{
			//将此链表项添加到free链表中
			prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
			xFreeBytesRemaining += pxLink->xBlockSize;
			traceFREE( pv, pxLink->xBlockSize );
		}
		( void ) xTaskResumeAll();
	}
}

为什么heap_2.c文件不能做到合并相邻的空闲内存块?我们可以看这段代码,当用户想要释放内存时,vPortFree函数并没有释放内存,而是将这个内存块添加进空闲内存块链表中。这样以来,实际上相邻的内存块之间不是完全空闲的,有一个链表项将这段空闲内存块“一分为二”了。而且,随着pvPortMalloc函数调用次数的增加,链表项的项目也会不断增加,链表项占用的内存只是比较小的一方面。相反的,这种对空闲内存块的分割才是影响内存块分配的比较大的方面。

heap_4.c

由于heap_2.c和heap_4.c之间有内存合并的改进,我们先看heap_4.c文件(事实上后面我们可以看到,heap_3.c几乎没有什么需要详解的必要)

/*
 * A sample implementation of pvPortMalloc() and vPortFree() that combines
 * (coalescences) adjacent memory blocks as they are freed, and in so doing
 * limits memory fragmentation.
 */

以上注释的意思是,heap_4.c相较于heap_2.c多了合并内存的功能。
同样的链表项定义,但是这里xBlockSize含义是有一点不同的!xBlockSize的最高位代表当前内存块是否空闲! heap_2.c中的是用一个链表记录当前空闲的内存块(宏定义prvInsertBlockIntoFreeList)。

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;
size_t uxAddress;
size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;

	/* 内存对齐,把数组的首地址对齐到portBYTE_ALIGNMENT的整数倍。 */
	uxAddress = ( size_t ) ucHeap;

	if( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 )
	{
		uxAddress += ( portBYTE_ALIGNMENT - 1 );
		uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
		xTotalHeapSize -= uxAddress - ( size_t ) ucHeap;
	}

	pucAlignedHeap = ( uint8_t * ) uxAddress;

	/* xStart 指向内存起始 */
	xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
	xStart.xBlockSize = ( size_t ) 0;

	/* pxEnd标记链表的结尾 */
	uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize;
	uxAddress -= xHeapStructSize;
	uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
	pxEnd = ( void * ) uxAddress;
	pxEnd->xBlockSize = 0;
	pxEnd->pxNextFreeBlock = NULL;

	/* 创建一个空闲内存块,大小是整个内存 */
	pxFirstFreeBlock = ( void * ) pucAlignedHeap;
	pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;
	pxFirstFreeBlock->pxNextFreeBlock = pxEnd;

	/* 只有一个空闲内存块,大小是整个内存 */
	xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
	xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;

	/* 由于A_BLOCK_LINK.xBlockSize的首位已经赋予的新的含义,所以这一位不能被占用 */
	/* 换言之,要申请的大小的首位一定要是0 */
	xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );
}

prvHeapInit函数处理后的内存空间模型prvHeapInit函数处理后的内存空间模型

pvPortMalloc函数

该方法和heap_2.c中基本没有太多差别,除了增加一个xMinimumEverFreeBytesRemaining 变量记录历史最小空闲内存之外。

void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;

	vTaskSuspendAll();
	{
		/* 第一次进入函数,调用prvHeapInit函数初始化内存空间 */
		if( pxEnd == NULL )
		{
			prvHeapInit();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}

		/* 由于A_BLOCK_LINK.xBlockSize的首位已经赋予的新的含义,所以这一位不能被占用 */
		if( ( xWantedSize & xBlockAllocatedBit ) == 0 )
		{
			/* 实际分配的内存大小需要加入链表项的大小 */
			if( xWantedSize > 0 )
			{
				xWantedSize += xHeapStructSize;

				/* 内存对齐 */
				if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
				{
					xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
					configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}

			if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
			{
				/* 遍历链表,找到足够大的内存空间 */
				pxPreviousBlock = &xStart;
				pxBlock = xStart.pxNextFreeBlock;
				while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
				{
					pxPreviousBlock = pxBlock;
					pxBlock = pxBlock->pxNextFreeBlock;
				}

				/* pxBlock等于pxEnd,代表没有找到符合要求的内存块。 */
				if( pxBlock != pxEnd )
				{
					/* 返回申请到的内存空间 */
					pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );

					/* 将当前链表项删除出链表 */
					pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;

					/* 分割内存,产生新的空闲内存块 */
					if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
					{
						/* 创建新的空闲链表项 */
						pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
						configASSERT( ( ( ( size_t ) pxNewBlockLink ) & portBYTE_ALIGNMENT_MASK ) == 0 );

						/* 计算内存块大小 */
						pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
						pxBlock->xBlockSize = xWantedSize;

						/* 添加到链表中,这里的prvInsertBlockIntoFreeList不是宏定义而是函数 */
						prvInsertBlockIntoFreeList( pxNewBlockLink );
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}

					xFreeBytesRemaining -= pxBlock->xBlockSize;

					if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
					{
						xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}

					/* 由于该内存已被占用,所以将xBlockSize的首位置1 */
					pxBlock->xBlockSize |= xBlockAllocatedBit;
					pxBlock->pxNextFreeBlock = NULL;
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}

		traceMALLOC( pvReturn, xWantedSize );
	}
	( void ) xTaskResumeAll();

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

	configASSERT( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 );
	return pvReturn;
}

vPortFree函数

该函数基本也和heap_2.c一致。

void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;

	if( pv != NULL )
	{
		/* 将指针指向此内存的链表项 */
		puc -= xHeapStructSize;
		pxLink = ( void * ) puc;

		/* 检查是该内存块是否的确被分配了 */
		configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );
		configASSERT( pxLink->pxNextFreeBlock == NULL );

		if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 )
		{
			if( pxLink->pxNextFreeBlock == NULL )
			{
				/* 将xBlockSize首位置0 */
				pxLink->xBlockSize &= ~xBlockAllocatedBit;

				vTaskSuspendAll();
				{
					/* 将此链表项添加到空闲内存链表中 */
					xFreeBytesRemaining += pxLink->xBlockSize;
					traceFREE( pv, pxLink->xBlockSize );
					prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
				}
				( void ) xTaskResumeAll();
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
}

prvInsertBlockIntoFreeList函数

既然pvPortMallocvPortFree函数都和heap_2.c中没有太大区别,那么heap_4.c是如何实现合并相邻内存块的?这里主要是靠prvInsertBlockIntoFreeList函数,在heap_2.c中这是一个宏定义,功能也比较简单:将链表按照内存大小递增链接,并将内存块添加到相应的位置。
但是heap_4.c的链表有一些不同:它不是按照内存大小排列的,而是按照内存地址递增的方式排列。估计这样也是出于方便合并相邻内存块的考虑。

static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert )
{
BlockLink_t *pxIterator;
uint8_t *puc;

	/* 按照内存地址递增找到要添加链表项的位置 */
	for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
	{
	}

	/* 如果要插入的内存块正好在空闲内存块之后 */
	/* 代表整个内存中出现连续空闲内存块,进行合并 */
	puc = ( uint8_t * ) pxIterator;
	if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
	{
		pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
		pxBlockToInsert = pxIterator;
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}
	
	/* 如果要插入的内存块正好在空闲内存块之前 */
	/* 代表整个内存中出现连续空闲内存块,进行合并 */
	puc = ( uint8_t * ) pxBlockToInsert;
	if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock )
	{
		if( pxIterator->pxNextFreeBlock != pxEnd )
		{
			/* 合并 */
			pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;
			pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock;
		}
		else
		{
			pxBlockToInsert->pxNextFreeBlock = pxEnd;
		}
	}
	else
	{
		pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
	}

	/* 如果要插入的内存块已经和其他内存块合并了,就不应该操作它的指针 */
	if( pxIterator != pxBlockToInsert )
	{
		pxIterator->pxNextFreeBlock = pxBlockToInsert;
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}
}

以上才是heap_4.c区别于heap_2.c能够实现合并相邻内存块的地方。

heap_3.c

这一个文件我感觉没有多少需要详解的东西,因为它只是对malloc和free的简单封装:

pvPortMalloc函数

void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn;

	vTaskSuspendAll();
	{
		pvReturn = malloc( xWantedSize );
		traceMALLOC( pvReturn, xWantedSize );
	}
	( void ) xTaskResumeAll();

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

	return pvReturn;
}

pvPortFree函数

void vPortFree( void *pv )
{
	if( pv )
	{
		vTaskSuspendAll();
		{
			free( pv );
			traceFREE( pv, 0 );
		}
		( void ) xTaskResumeAll();
	}
}

heap_5.c

/*
 * A sample implementation of pvPortMalloc() that allows the heap to be defined
 * across multiple non-contigous blocks and combines (coalescences) adjacent
 * memory blocks as they are freed.
 * 
 *  * Usage notes:
 *  * vPortDefineHeapRegions() ***must*** be called before pvPortMalloc().
 * pvPortMalloc() will be called if any task objects (tasks, queues, event
 * groups, etc.) are created, therefore vPortDefineHeapRegions() ***must*** be
 * called before any other objects are defined.
 *  * vPortDefineHeapRegions() takes a single parameter.  The parameter is an array
 * of HeapRegion_t structures.  HeapRegion_t is defined in portable.h as
 *  * typedef struct HeapRegion
 * {
 * uint8_t *pucStartAddress; << Start address of a block of memory that will be part of the heap.
 * size_t xSizeInBytes;	  << Size of the block of memory.
 * } HeapRegion_t;
 *  * The array is terminated using a NULL zero sized region definition, and the
 * memory regions defined in the array ***must*** appear in address order from
 * low address to high address.  So the following is a valid example of how
 * to use the function.
 *  * HeapRegion_t xHeapRegions[] =
 * {
 * 	{ ( uint8_t * ) 0x80000000UL, 0x10000 }, << Defines a block of 0x10000 bytes starting at address 0x80000000
 * 	{ ( uint8_t * ) 0x90000000UL, 0xa0000 }, << Defines a block of 0xa0000 bytes starting at address of 0x90000000
 * 	{ NULL, 0 }                << Terminates the array.
 * };
 *  * vPortDefineHeapRegions( xHeapRegions ); << Pass the array into vPortDefineHeapRegions().
 *  * Note 0x80000000 is the lower address so appears in the array first.
 *  */

heap_5.c的开头注释要长不少,总结有以下几点:

  • heap_5.c允许跨区域分配内存,我们看到之前的四个文件都是先申请一个巨大的数组(ucHeap[ configTOTAL_HEAP_SIZE ]),之后的操作都是在这个数组中进行的。换言之,整个可操作的内存空间是连续的。而heap_5.c允许在不连续的空间中分配内存。之前heap_1/2/4.c都有以下这段代码(heap_3.c是直接封装了malloc和free就不需要了):

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

    然而heap_5.c却没有这段代码,可见它不是事先申请一段连续空间操作的。

  • 在创建任务对象时(包括任务,队列,事件),都会调用pvPortMalloc()函数,而在调用这个函数之前必须要先调用vPortDefineHeapRegions()函数。

  • vPortDefineHeapRegions()的形参是一个HeapRegion_t数组类型的指针。HeapRegion_t类型的定义如下:

    typedef struct HeapRegion
    {
    	uint8_t *pucStartAddress;	//内存块启始地址
    	size_t xSizeInBytes;		//内存块大小
    } HeapRegion_t;
    

    并且这个数组应该满足:1,数组应该是按照pucStartAddress从小到大排列。2,数组的最后一位应该是{ NULL, 0 }

vPortDefineHeapRegions函数

heap_5.c中没有prvHeapInit函数,取而代之的是vPortDefineHeapRegions函数,这个函数除了实现了prvHeapInit中的功能之外还新增了一些其他的功能:

void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions )
{
BlockLink_t *pxFirstFreeBlockInRegion = NULL, *pxPreviousFreeBlock;
size_t xAlignedHeap;
size_t xTotalRegionSize, xTotalHeapSize = 0;
BaseType_t xDefinedRegions = 0;
size_t xAddress;
const HeapRegion_t *pxHeapRegion;

	/* Can only call once! */
	configASSERT( pxEnd == NULL );

	pxHeapRegion = &( pxHeapRegions[ xDefinedRegions ] );

	while( pxHeapRegion->xSizeInBytes > 0 )
	{
		xTotalRegionSize = pxHeapRegion->xSizeInBytes;

		/* 内存对齐操作 */
		xAddress = ( size_t ) pxHeapRegion->pucStartAddress;
		if( ( xAddress & portBYTE_ALIGNMENT_MASK ) != 0 )
		{
			xAddress += ( portBYTE_ALIGNMENT - 1 );
			xAddress &= ~portBYTE_ALIGNMENT_MASK;

			/* 内存块总大小减去因内存对齐失去的部分 */
			xTotalRegionSize -= xAddress - ( size_t ) pxHeapRegion->pucStartAddress;
		}

		xAlignedHeap = xAddress;

		if( xDefinedRegions == 0 )
		{
			/* 此处xStart功能同heap_4.c */
			xStart.pxNextFreeBlock = ( BlockLink_t * ) xAlignedHeap;
			xStart.xBlockSize = ( size_t ) 0;
		}
		else
		{
			configASSERT( pxEnd != NULL );

			configASSERT( xAddress > ( size_t ) pxEnd );
		}
		
		pxPreviousFreeBlock = pxEnd;

		/* 这里的代码同heap_4.c */
		xAddress = xAlignedHeap + xTotalRegionSize;
		xAddress -= xHeapStructSize;
		xAddress &= ~portBYTE_ALIGNMENT_MASK;
		pxEnd = ( BlockLink_t * ) xAddress;
		pxEnd->xBlockSize = 0;
		pxEnd->pxNextFreeBlock = NULL;

		/* 这里的代码同heap_4.c */
		pxFirstFreeBlockInRegion = ( BlockLink_t * ) xAlignedHeap;
		pxFirstFreeBlockInRegion->xBlockSize = xAddress - ( size_t ) pxFirstFreeBlockInRegion;
		pxFirstFreeBlockInRegion->pxNextFreeBlock = pxEnd;

		if( pxPreviousFreeBlock != NULL )
		{
			pxPreviousFreeBlock->pxNextFreeBlock = pxFirstFreeBlockInRegion;
		}

		xTotalHeapSize += pxFirstFreeBlockInRegion->xBlockSize;

		/* 移动到下一个内存块区域 */
		xDefinedRegions++;
		pxHeapRegion = &( pxHeapRegions[ xDefinedRegions ] );
	}

	xMinimumEverFreeBytesRemaining = xTotalHeapSize;
	xFreeBytesRemaining = xTotalHeapSize;

	/* Check something was actually defined before it is accessed. */
	configASSERT( xTotalHeapSize );

	/* Work out the position of the top bit in a size_t variable. */
	xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );
}

pvPortMalloc和vPortFree函数

这两个函数同heap_4.c,故这里略过。
可见,heap_5.c中很大部分使用的任然是heap_4.c中的代码逻辑,只不过在整体逻辑上使用一个HeapRegion_t类型的数组标记了分散的内存块,取代了原本连续的大内存块的方法。这种方法更灵活,同时,可分配的内存上限也更大。

总结

算法原理

heap_1.c

这种分配方式比较简单,实现申请一个比较大的内存空间,之后我们的所有操作(malloc和free)都只在这个内存空间内进行。实际文件中申请了一个大数组作为这个操作空间。由于这个文件只能申请空间而不能释放空间,所以我们不必知道每次申请的内存空间的确切位置,只需要知道当前总共申请了多少位置即可。
在这里插入图片描述上图中红色的部分代表已经分配出去的内存,黑色代表还未分配的内存。每一次执行pvPortMalloc函数都会在这个数组中按照地址申请空间。

heap_2.c

如果要实现pvPortFree函数,那么除了知道当前总共分配了多少空间,还要知道每一次都分配了多少空间。也就是每一次分配的空间在总的空间中的启始位置和结束位置,这样才能释放原先分配的那些空间。为了贴合c语言的free函数,freeRTOS的vPortFree函数传入的参数只有一个地址,要实现这个函数,数组中除了用户申请的用于储存数据的空间,还有一部分空间配分配出来用于管理这些数据空间,在文件中这些以BlockLink_t结构体的方式组织。
下图是这种组织方式的示意图,第一个部分是一个指针,指向下一个空闲的结构块,第二个部分保存当前结构块的大小,第三部分才是实际保存数据的内存空间。
在这里插入图片描述
这样一来,整个内存空间如下图所示。红色部分代表已被占用的内存空间。
在这里插入图片描述为了记录内存空间中哪些内存块是空闲的,哪些内存块是被占用的,文件中采用了链表的方式:空闲内存块中BlockLink_t结构体的指针指向下一个空闲的内存块。另外附加了一个头节点和一个尾节点,如下图所示:
在这里插入图片描述需要注意的是,1,这个链表中只有空闲的内存块,不空闲的内存块不在这个链表之中。2,按照图中的方式来看这种链表好像是以地址递增的方式组织的,但是实际上,链表是按照空闲结构体空间大小递增的方式组织的,这样再分配内存时可以优先分配空间比较小的内存块。
按照上图所示方式组织整个内存空间之后,调用vPortFree函数时只需传入一个地址,就知道当前地址所在的内存块大小是多少,即应该“free”掉哪些空间。vPortFree并不会想直觉那样清空要求的区域,它只是将这个内存块加入空闲内存块链表之中。在下一次调用pvPortMalloc函数时会从这个空闲内存块链表中寻找内存块并覆盖。

heap_4.c

以上heap_2.c实现了malloc和free的功能。但是会有一点问题:
在这里插入图片描述如上图的一个内存空间,现在我们要对红色部分进行free操作,理想情况下free操作后的效果应该如下:
在这里插入图片描述第二个内存块整个应该“并入”第一个内存块中,但是实际的效果却是如下的:
在这里插入图片描述虽然第二个内存块也被释放了,不过由于heap_2.c不允许跨内存块操作,所以实际上造成了内存分割。heap_4.c就解决了这种相邻空闲内存块合并的问题。
heap_4.c也使用了同heap_2.c一样的内存块结构,但是heap_4.c的空闲链表是按照地址递增的方式排列的,就是大家如图中直观看到的模式。
heap_4.c解决这种问题的方式比较简单,在将空闲内存块加入链表中时做一下判断,如果是相邻有空闲内存块,就直接将其中一个内存块大小扩大,覆盖相邻的内存块,解决内存分割的问题。另外,如果要加入的空闲内存块左右两边也都是空闲内存块,即有三个相邻的空闲内存块,这种方法也可以解决。具体做法是先合并前两个内存块为一个内存块,再合并剩下两个内存块。

heap_3.c

heap_3.c是最简单的一种分配方式,pvPortMallocvPortFree函数均是对进行了简单的封装而已,没有特别的算法。

heap_5.c

heap_5.c文件允许跨内存区域操作内存块,具体方法是:实现定义一个HeapRegion_t数组,数组中的成员定义了每个内存块开始的地址和结束的地址,举个例子:我们共有两个不相邻的内存区域,起始地址分别是0x80000000UL和0x90000000UL,地址分别是0x10000和0xa0000。有定义的数组:

 HeapRegion_t xHeapRegions[] =
 {
  	{ ( uint8_t * ) 0x80000000UL, 0x10000 },
 	{ ( uint8_t * ) 0x90000000UL, 0xa0000 },
 	{ NULL, 0 }                << Terminates the array.
 };

这样一来,分配好的地址就如下所示:
在这里插入图片描述设置好之后,就可以像之前4个文件一样管理内存分配。

各个内存分配方式的异同

heap_3.c文件是直接对mallocfree的简单封装,这里不放在一起比较,剩下的四个文件按照功能从简单到复杂依次排序:
在这里插入图片描述

何种情况下应该使用何种内存分配方式

heap_1.c

适合一些不会删除任务,队列以及信号量的任务,特别是一些比较简单的系统和一些对安全性要求比较高的系统。事实上,很多系统中只要创建了任务之后就会一直执行,不会删除任务。所以这个文件适用环境还是比较多的。

heap_2.c

能分配,能回收,但是回收时不考虑内存碎片的场所。适合一些申请和释放操作不是特别频繁,而且不会申请大数组的场所。(申请和释放会造成内存碎片,过多操作内存中可能没有大的连续区域)

heap_3.c

这种方法实际使用的还是c语言的mallocfree函数。与平台没有太大关系。

heap_4.c

内存分配和释放操作比较频繁的系统。例如要重复创建删除任务,队列,信号量等。

heap_5.c

系统需要管理不连续的内存空间,例如接入外部ram的场所。

  • 13
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值