FreeRTOS篇章之 heap堆內存分配分析

寫在前面:
本文章旨在總結備份、方便以後查詢,由於是個人總結,如有不對,歡迎指正;另外,內容大部分來自網絡、書籍、和各類手冊,如若侵權請告知,馬上刪帖致歉。

 

目錄

一、FreeRTOS內存分配選擇

二、靜態與動態內存分配

三、內存管理

四、heap_1.c

五、heap_2.c

六、heap_3.c

七、heap_4.c

八、heap_5.c

九、其他


 

一、FreeRTOS內存分配選擇

在 FreeRTOS中,可以用靜態(不使用 FreeRTOS堆)或動態來分配 RTOS的對象; 因此 FreeRTOS中提供了 5種堆管理方案,這些方案的複雜性和功能使得它的使用範圍廣泛,當然用戶也可以自己實現堆管理;在官方的例程中,動靜態的使用選擇可以通過使能宏 configSUPPORT_STATIC_ALLOCATION和 宏 configSUPPORT_DYNAMIC_ALLOCATION來進行決定(官方默認是使用動態內存進行分配的),當然,前提是使用 FreeRTOS提供的 heap堆分配的其中一個文件,而不是用自己實現的堆管理;甚至你還可以兩種方法都在同一 RTOS應用程序中使用

 

二、靜態與動態內存分配

V9.0.0之前的 FreeRTOS版本是從特殊的 FreeRTOS堆中(也就是這幾個 heap文件)分配下面列出的 RTOS對象使用的內存。而在 FreeRTOS V9.0.0及更高版本中,人們可以自己實現堆管理提供內存,從而可以選擇創建以下對象,而無需動態分配任何內存:

  • 任務
  • 軟件計時器
  • 隊列
  • 事件
  • 二值信號量
  • 計數信號量
  • 遞歸信號量
  • 互斥量

1、使用動態分配的 RAM創建 RTOS對象

動態創建 RTOS對象的好處是可以更加簡單、並有可能盡最大程度地減少應用程序的最大 RAM使用量

如果 configSUPPORT_DYNAMIC_ALLOCATION設置爲 1或未定義,以下 API函數將使用動態分配的 RAM創建 RTOS對象:

2、使用靜態分配的 RAM創建 RTOS對象

使用靜態分配的 RAM創建 RTOS對象的好處是爲應用程序編寫者提供了更多控制權

下列 API函數(如果 configSUPPORT_STATIC_ALLOCATION設置爲 1時可用)允許使用應用程序編寫者實現的堆管理創建 RTOS對象。爲了提供內存,應用程序編寫者只需要聲明一個適當對象類型的變量,然後將變量的地址傳遞給RTOS API函數即可。官方也提供了 StaticAllocation.c 標準的演示/測試任務來演示如何使用這些功能:

 

三、內存管理

如果 RTOS對象是動態創建的,則有時可以使用標準C庫malloc()和free()函數來實現此目的,但是

  1. 它們並不總是在嵌入式系統上可用,
  2. 他們佔用了寶貴的代碼空間,
  3. 它們不是線程安全的
  4. 它們不是確定性的(執行函數所花費的時間因調用而異)

因此,通常需要另一種內存分配實現。

一個嵌入式 /實時系統可能具有與另一個系統不同的 RAM和時序要求;因此,單個 RAM分配算法僅適用於部分應用程序。

爲了解決這個問題,FreeRTOS將內存分配 API保留在其可移植層中;當 RTOS內核需要 RAM時,它不調用 malloc(),而是調用 pvPortMalloc();當釋放 RAM時,RTOS內核將調用 vPortFree()而不是調用 free()。

在 FreeRTOSv9.0.0\FreeRTOS\Source\portable\MemMang文檔中,FreeRTOS提供了五個內存分配實現:

  • heap_1 – 最簡單,不允許釋放內存
  • heap_2 – 允許釋放內存,但不合並相鄰的空閒塊。
  • heap_3 – 簡單包裝標準 malloc()和 free()以確保線程安全
  • heap_4 – 合併相鄰的空閒塊以避免碎片;包括絕對地址放置選項
  • heap_5 – 參照 heap_4,能夠跨多個不相鄰的內存區域擴展堆

 

四、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這個文件的功能只能進行分配,不支持釋放內存

它的分配函數實現:

void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn = NULL;
static uint8_t *pucAlignedHeap = NULL;

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

	vTaskSuspendAll();
	{
		if( pucAlignedHeap == NULL )
		{
			/* Ensure the heap starts on a correctly aligned boundary. */
			pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
		}

		/* Check there is enough room left for the allocation. */
		if( ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) &&
			( ( xNextFreeByte + xWantedSize ) > xNextFreeByte )	)/* Check for overflow. */
		{
			/* Return the next free byte then increment the index past this
			block. */
			pvReturn = pucAlignedHeap + xNextFreeByte;
			xNextFreeByte += xWantedSize;
		}

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

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

	return pvReturn;
}

既然這麼簡單,就稍微分析一下吧!

1、首先是對內存對齊的判斷,這裏,如果內存對齊有效,則執行;如果要分配的內存不是對齊字節(通常是 8,根據 portBYTE_ALIGNMENT 來確定對齊字節)的整數倍,則補齊,所以 xWantedSize要加上要補齊的長度

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

2、接着掛起所有的調度器,防止在分配內存的過程中被打斷

vTaskSuspendAll();

3、判斷是否爲第一次分配,若是,則檢查初分配的內存是否有對齊,這裏 uheap是一個靜態數組(是 FreeRTOS爲我們申請的一個內存堆,大小由宏 configTOTAL_HEAP_SIZE決定),首先要確保它是否對齊,因爲 uheap的首地址可能不是 8的倍數,若是沒有對齊,那麼進行對齊處理,以確保實際開始分配的地址是對齊的,值得留意的是,這個對齊處理只做一次

if( pucAlignedHeap == NULL )
{
	/* Ensure the heap starts on a correctly aligned boundary. */
	pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
}

4、在正式分配前進行溢出檢查,如果空間夠用,則記錄新分配空間的首地址到 pvReturn,並重新記錄新的空閒空間的首地址到NextFreeByte

/* Check there is enough room left for the allocation. */
if( ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) &&
	( ( xNextFreeByte + xWantedSize ) > xNextFreeByte )	)/* Check for overflow. */
{
	/* Return the next free byte then increment the index past this	block. */
	pvReturn = pucAlignedHeap + xNextFreeByte;
	xNextFreeByte += xWantedSize;
}

5、traceMALLOC( pvReturn, xWantedSize )是一個宏,用於輸出內存分配的調試信息,這個宏定義在 FreeRTOS.h中,默認爲空,如果需要將這些調試信息輸出到串口或其它東西,就可以修改這個宏將信息輸出到所需要的地方。

traceMALLOC( pvReturn, xWantedSize );

6、最後,恢復之前掛起的調度器

( void ) xTaskResumeAll();

7、這個取決於宏 configUSE_MALLOC_FAILED_HOOK是否使能,如果開啓了分配失敗 hook函數,就調用該hook

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

 

適用環境以及功能簡介:

  • 如果您的應用程序從不刪除任務,隊列,信號量,互斥鎖等,則可以使用它(實際上涵蓋了使用FreeRTOS的大多數應用程序)。
  • 始終是確定性的(總是花費相同的時間來執行),並且不會導致內存碎片。
  • 它是非常簡單的,可以從靜態分配的數組中分配內存,這意味着它通常適用於不允許真正的動態內存分配的應用程序。

 

五、heap_2.c

此方案使用了最佳適應算法(best fit algorithm),並且與方案 1不同,它允許釋放以前分配的塊;它不會將相鄰的空閒塊合併成一個大塊。

同樣的,文件裏的註釋: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).  也就是說現在支持分配和釋放內存,但對內存碎片不做處理

1、在實現堆分配之前,有一個堆初始化的過程 prvHeapInit(),並且只調用一次(就是第一次進入該函數的時候)

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

	/* Ensure the heap starts on a correctly aligned boundary. */
	pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );

	/* xStart is used to hold a pointer to the first item in the list of free
	blocks.  The void cast is used to prevent compiler warnings. */
	xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
	xStart.xBlockSize = ( size_t ) 0;

	/* xEnd is used to mark the end of the list of free blocks. */
	xEnd.xBlockSize = configADJUSTED_HEAP_SIZE;
	xEnd.pxNextFreeBlock = NULL;

	/* To start with there is a single free block that is sized to take up the
	entire heap space. */
	pxFirstFreeBlock = ( void * ) pucAlignedHeap;
	pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE;
	pxFirstFreeBlock->pxNextFreeBlock = &xEnd;
}

這裏用到了 BlockLink_t結構體,這個結構有兩個成員,第一個是節點 Next指針 pxNextFreeBlock,第二個是空閒塊大小。結構體成員如下:

/* Define the linked list structure.  This is used to link free blocks in order
of their size. */
typedef struct A_BLOCK_LINK
{
	struct A_BLOCK_LINK *pxNextFreeBlock;	/*<< The next free block in the list. */
	size_t xBlockSize;			/*<< The size of the free block. */
} BlockLink_t;

首先,對開闢的堆進行地址對齊工作,對齊的原因的原理與 heap_1一樣,在獲取堆的首地址後,對 xStart鏈表頭和 xEnd鏈表尾進行初始化賦值;在 heap_2中,與 heap_1不同的是,由於 FreeRTOS用空閒塊對內存堆進行管理,於是用 BlockLink_t這一個結構來形成一條空閒塊鏈表對空閒塊進行組織和管理

2、好了,現在回到 pvPortMalloc函數中

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

	vTaskSuspendAll();
	{
		/* If this is the first call to malloc then the heap will require
		initialisation to setup the list of free blocks. */
		if( xHeapHasBeenInitialised == pdFALSE )
		{
			prvHeapInit();
			xHeapHasBeenInitialised = pdTRUE;
		}

		/* The wanted size is increased so it can contain a BlockLink_t
		structure in addition to the requested amount of bytes. */
		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 ) );
			}
		}

		if( ( xWantedSize > 0 ) && ( xWantedSize < configADJUSTED_HEAP_SIZE ) )
		{
			/* Blocks are stored in byte order - traverse the list from the start
			(smallest) block until one of adequate size is found. */
			pxPreviousBlock = &xStart;
			pxBlock = xStart.pxNextFreeBlock;
			while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
			{
				pxPreviousBlock = pxBlock;
				pxBlock = pxBlock->pxNextFreeBlock;
			}

			/* If we found the end marker then a block of adequate size was not found. */
			if( pxBlock != &xEnd )
			{
				/* Return the memory space - jumping over the BlockLink_t structure
				at its start. */
				pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + heapSTRUCT_SIZE );

				/* This block is being returned for use so must be taken out of the
				list of free blocks. */
				pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;

				/* If the block is larger than required it can be split into two. */
				if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
				{
					/* This block is to be split into two.  Create a new block
					following the number of bytes requested. The void cast is
					used to prevent byte alignment warnings from the compiler. */
					pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );

					/* Calculate the sizes of two blocks split from the single
					block. */
					pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
					pxBlock->xBlockSize = xWantedSize;

					/* Insert the new block into the list of free blocks. */
					prvInsertBlockIntoFreeList( ( pxNewBlockLink ) );
				}

				xFreeBytesRemaining -= pxBlock->xBlockSize;
			}
		}

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

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

	return pvReturn;
}

同樣的,在每次分配空間之前,先掛起調度器,這是沒得說的,然後如果是第一次調用 pvPortMalloc(),則調用 prvHeapInit()對內存堆和空閒塊鏈表進行初始化;

而下面這段 code:

/* The wanted size is increased so it can contain a BlockLink_t
structure in addition to the requested amount of bytes. */
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 ) );
	}
}

在用戶每申請一次內存,FreeRTOS都會在申請的空間前加上空閒塊頭部 BlockLink_t,用於記錄分配出去的空間的大小,因此,實際分配的內存空間大小就等於用戶申請的內存空間大小加上空閒塊頭部的大小;最後再加上頭部之後,還要對整個大小進行對齊

下一個 if判斷

if( ( xWantedSize > 0 ) && ( xWantedSize < configADJUSTED_HEAP_SIZE ) )
{
	/* Blocks are stored in byte order - traverse the list from the start
	(smallest) block until one of adequate size is found. */
	pxPreviousBlock = &xStart;
	pxBlock = xStart.pxNextFreeBlock;
	while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
	{
		pxPreviousBlock = pxBlock;
		pxBlock = pxBlock->pxNextFreeBlock;
	}

	/* If we found the end marker then a block of adequate size was not found. */
	if( pxBlock != &xEnd )
	{
		/* Return the memory space - jumping over the BlockLink_t structure
		at its start. */
		pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + heapSTRUCT_SIZE );

		/* This block is being returned for use so must be taken out of the
		list of free blocks. */
		pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;

		/* If the block is larger than required it can be split into two. */
		if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
		{
			/* This block is to be split into two.  Create a new block
			following the number of bytes requested. The void cast is
			used to prevent byte alignment warnings from the compiler. */
			pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );

			/* Calculate the sizes of two blocks split from the single
			block. */
			pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
			pxBlock->xBlockSize = xWantedSize;

			/* Insert the new block into the list of free blocks. */
			prvInsertBlockIntoFreeList( ( pxNewBlockLink ) );
		}

		xFreeBytesRemaining -= pxBlock->xBlockSize;
	}
}

先是判斷用戶申請的空間大小是否存在(不等於零)以及是否已經超過堆空間大小,若是不符合那就直接 pass掉了;在這裏,塊的大小是按從小到大的順序排列的,因此從一開始遍歷列表(最小的)塊,直到找到一個合適的大小,最後把位置記錄下來

/* If we found the end marker then a block of adequate size was not found. */
if( pxBlock != &xEnd )
{
	/* Return the memory space - jumping over the BlockLink_t structure
	at its start. */
	pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + heapSTRUCT_SIZE );

	/* This block is being returned for use so must be taken out of the
	list of free blocks. */
	pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;

	/* If the block is larger than required it can be split into two. */
	if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
	{
		/* This block is to be split into two.  Create a new block
		following the number of bytes requested. The void cast is
		used to prevent byte alignment warnings from the compiler. */
		pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );

		/* Calculate the sizes of two blocks split from the single
		block. */
		pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
		pxBlock->xBlockSize = xWantedSize;

		/* Insert the new block into the list of free blocks. */
		prvInsertBlockIntoFreeList( ( pxNewBlockLink ) );
	}

	xFreeBytesRemaining -= pxBlock->xBlockSize;
}

緊接着,在上面的一段代碼裏,先是把返回的空間地址值修改(跳過 BlockLink_t結構),然後把上一個的下一個的節點指向當前下一個的空閒塊;後面,若果分配出去的空閒塊的剩餘空間是比兩倍的空閒塊頭部結構體大小還要大,則將分配出去的這個空閒塊分割剩餘的空間出來,重新放到空閒塊鏈表中

值得注意的是 prvInsertBlockIntoFreeList這個是個宏,具體實現如下:

/*
 * Insert a block into the list of free blocks - which is ordered by size of
 * the block.  Small blocks at the start of the list and large blocks at the end
 * of the list.
 */
#define prvInsertBlockIntoFreeList( pxBlockToInsert )								\
{																					\
BlockLink_t *pxIterator;															\
size_t xBlockSize;																	\
																					\
	xBlockSize = pxBlockToInsert->xBlockSize;										\
																					\
	/* Iterate through the list until a block is found that has a larger size */	\
	/* than the block we are inserting. */											\
	for( pxIterator = &xStart; pxIterator->pxNextFreeBlock->xBlockSize < xBlockSize; pxIterator = pxIterator->pxNextFreeBlock )	\
	{																				\
		/* There is nothing to do here - just iterate to the correct position. */	\
	}																				\
																					\
	/* Update the list to include the block being inserted in the correct */		\
	/* position. */																	\
	pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;					\
	pxIterator->pxNextFreeBlock = pxBlockToInsert;									\
}

這個宏的作用就是將新空閒塊插入到合適的鏈表位置中

後面的代碼執行就跟 heap_1一樣了,修改剩餘空閒塊空閒大小 xFreeBytesRemaining、恢復所有掛起的任務等等

3、最後剩下的就是 heap_1中不支持的 vPortFree釋放函數了

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

	if( pv != NULL )
	{
		/* The memory being freed will have an BlockLink_t structure immediately
		before it. */
		puc -= heapSTRUCT_SIZE;

		/* This unexpected casting is to keep some compilers from issuing
		byte alignment warnings. */
		pxLink = ( void * ) puc;

		vTaskSuspendAll();
		{
			/* Add this block to the list of free blocks. */
			prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
			xFreeBytesRemaining += pxLink->xBlockSize;
			traceFREE( pv, pxLink->xBlockSize );
		}
		( void ) xTaskResumeAll();
	}
}

這一個函數炒雞簡單,而且非常短,首先判斷要釋放的內存是否爲空;若是不爲空,則獲取這個內存空間真正的的空閒塊(剔除頭部的 BlockLink_t結構),然後掛起所有任務,把釋放的內存重新插入到空閒塊鏈表中,最後調用調試信息宏,恢復掛起的任務就結束了

 

特點:

即使應用程序反覆刪除任務,隊列,信號量,互斥量等對象,也可以使用,有關內存碎片的警告如下:

  • 如果不是,如果被分配和釋放內存使用是隨機的大小。例如:
    • 如果應用程序以動態創建和刪除任務,並且分配給正在創建的任務的堆棧大小始終相同,則在大多數情況下可以使用heap_2.c;但是,如果分配給正在創建的任務的堆棧大小並不總是相同,則可用的空閒內存可能會分成許多小塊,最終導致分配失敗;在這種情況下,heap_4.c會更好
    • 如果應用程序以動態創建和刪除隊列,並且每種情況下隊列存儲區域都相同(隊列存儲區域指隊列項數目乘以每個隊列長度),則在大多數情況下都可以使用 heap_2.c;但是,如果隊列存儲區域在每種情況下都不相同,則可用的空閒內存可能會分成許多小塊,最終導致分配失敗;在這種情況下,heap_4.c會更好
    • 該應用程序直接調用 pvPortMalloc()和 vPortFree(),而不僅僅是通過其他 FreeRTOS API函數間接調用。

       

  • 如果您的應用程序的隊列,任務,信號量,互斥量等處於以不可預測的順序,則可能會導致內存碎片問題;雖然這是小概率事件,但應牢記
  • 不確定型,但比大多數標準 C庫 malloc實現要有效得多

 

六、heap_3.c

heap_3.c就更簡單了,只是簡單的包裝一下標準函數 malloc()和 free()以確保線程安全,在大多數情況下,這些包裝器將需要你的編譯器提供,它們依賴於編譯器自己的 malloc()和 free()實現;文件說明如下: Implementation of pvPortMalloc() and vPortFree() that relies on the compilers own malloc() and free() implementations.This file can only be used if the linker is configured to to generate a heap memory area.

1、封裝 malloc()

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;
}

和 heap_1、heap_2一樣,用 vTaskSuspendAll()掛起所有的任務,以確保分配內存的過程是線程安全的;隨後才使用 malloc()進行內存分配,記錄內存地址;然後就是調用調試信息宏 traceMALLOC(),最後調用 xTaskResumeAll()恢復所有被掛起的任務;如果在 FreeRTOS.h中使能了勾子函數宏 configUSE_MALLOC_FAILED_HOOK ,則在調用勾子函數  vApplicationMallocFailedHook()之後再向用戶返回分配內存的首地址

2、封裝 free()

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

一樣的,檢查指針的有效性、掛起全部任務、調用 free()接口將內存回收、調用調試信息宏、恢復掛起的任務,然後就沒了

 

此實現方法及特點:

  • 需要鏈接器設置堆,並且需要編譯器庫提供 malloc()和 free()實現
  • 具有不確定性
  • 可能會大大增加 RTOS內核代碼的大小

注意,當使用 heap_3時,FreeRTOSConfig.h中的宏 configTOTAL_HEAP_SIZE設置無效。

 

七、heap_4.c

跟方案 2不一樣的是,這個方案使用了首次適應算法(first fit algorithm);它會將相鄰的空閒內存塊合併成一個更大的塊(包含一個合併算法)

具體是它們會在釋放時合併相鄰的內存塊,從而限制內存碎片,文件中的註釋也有說: 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的代碼基本跟 heap_2的代碼實現差不多,只不過增加了些功能(合併算法),而且你別看他代碼太長,其實有很多東西都沒用到的,就像這個函數 mtCOVERAGE_TEST_MARKER(),其實他是個宏,但是他並沒有實現什麼,根據他的命名可以認定爲只是作者預留的一個調試輸出窗口而已;還有 configASSERT()這個也是一個宏,也只是用來調試輸出而已,具體配置修改看下一篇

1、沒錯,還是我們首要分析的初始化函數

static void prvHeapInit( void )
{
BlockLink_t *pxFirstFreeBlock;
uint8_t *pucAlignedHeap;
size_t uxAddress;
size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;

	/* Ensure the heap starts on a correctly aligned boundary. */
	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 is used to hold a pointer to the first item in the list of free
	blocks.  The void cast is used to prevent compiler warnings. */
	xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
	xStart.xBlockSize = ( size_t ) 0;

	/* pxEnd is used to mark the end of the list of free blocks and is inserted
	at the end of the heap space. */
	uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize;
	uxAddress -= xHeapStructSize;
	uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
	pxEnd = ( void * ) uxAddress;
	pxEnd->xBlockSize = 0;
	pxEnd->pxNextFreeBlock = NULL;

	/* To start with there is a single free block that is sized to take up the
	entire heap space, minus the space taken by pxEnd. */
	pxFirstFreeBlock = ( void * ) pucAlignedHeap;
	pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;
	pxFirstFreeBlock->pxNextFreeBlock = pxEnd;

	/* Only one block exists - and it covers the entire usable heap space. */
	xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
	xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;

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

根據上圖現在來對比一下 head_2的,這裏對起始地址做字節對齊處理是跟 head_2有點不同,他不止對內存堆進行首地址對齊保存 pucAlignedHeap,並且還算出可用空間大小爲 xTotalHeapSize;

然後,xStart的初始化是一樣的,沒什麼好說的,注意 xStart是BlockLink_t的一個實體變量,存儲在靜態存儲區;

接着是 pxEnd,他是一個 BlockLink_t的指針,存儲在靜態存儲區中,卻指向了內存堆的最後一個 BlockLink_t大小的位置上。也就是說,內存堆最後的空間是存儲着一個 BlockLink_t,用來指示空閒塊鏈表的最後位置,這是和 heap_2所不同的地方;

因爲 pxEnd佔用了一個 BlockLink_t大小的空間,所以在整個堆空間,減去 pxEnd佔用的空間;

另外,爲了安全,heap_4增加一個位(BlockLink_t中 xBlockSize的最高位)標記某個內存塊是否處於空閒狀態,因此在初始化的時候設置 xBlockAllocatedBit的值

2、在對比分析 pvPortMalloc()內存分佈的實現之前,我們先分析一下 prvInsertBlockIntoFreeList()函數

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

	/* Iterate through the list until a block is found that has a higher address
	than the block being inserted. */
	for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
	{
		/* Nothing to do here, just iterate to the right position. */
	}

	/* Do the block being inserted, and the block it is being inserted after
	make a contiguous block of memory? */
	puc = ( uint8_t * ) pxIterator;
	if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
	{
		pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
		pxBlockToInsert = pxIterator;
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}

	/* Do the block being inserted, and the block it is being inserted before
	make a contiguous block of memory? */
	puc = ( uint8_t * ) pxBlockToInsert;
	if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock )
	{
		if( pxIterator->pxNextFreeBlock != pxEnd )
		{
			/* Form one big block from the two blocks. */
			pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;
			pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock;
		}
		else
		{
			pxBlockToInsert->pxNextFreeBlock = pxEnd;
		}
	}
	else
	{
		pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
	}

	/* If the block being inserted plugged a gab, so was merged with the block
	before and the block after, then it's pxNextFreeBlock pointer will have
	already been set, and should not be set here as that would make it point
	to itself. */
	if( pxIterator != pxBlockToInsert )
	{
		pxIterator->pxNextFreeBlock = pxBlockToInsert;
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}
}

這個函數就是合併實現的主要環節了;在 heap_2中的鏈表插入是通過宏實現的,並且是按內存塊大小進行插入;而 heap_4 的插入操作是寫成了一個函數,該函數按內存塊地址進行插入(低位前),實際上是將這個空閒塊鏈表裏的所有空閒塊按地址順序排列,這麼做是爲了實現內存塊合併;前面也說了,相對於 heap_2,heap_4會將相鄰的空閒內存塊合併成一個更大的塊,若果不這麼排列,那麼就不能將相鄰的空閒塊進行合併了

首先,通過一次鏈表的遍歷,找出內存塊插入點(把 pxIterator找出來);然後判斷,正在插入的塊(pxBlockToInsert)和內存塊插入點(pxIterator)是否構成一個連續的內存塊;其標準爲 pxIterator的首地址加上 pxIterator的塊大小之後等於  pxBlockToInsert的首地址,相等就說明兩個塊是相鄰的,把這個待插入的空閒塊插到 pxIterator的後面,否則就什麼事都不做

/* Iterate through the list until a block is found that has a higher address
than the block being inserted. */
for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
{
	/* Nothing to do here, just iterate to the right position. */
}

/* Do the block being inserted, and the block it is being inserted after
make a contiguous block of memory? */
puc = ( uint8_t * ) pxIterator;
if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
{
	pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
	pxBlockToInsert = pxIterator;
}
else
{
	mtCOVERAGE_TEST_MARKER();
}

然後,再試着將 pxBlockToInsert和 pxIterator指向的下一個空閒塊進行合併;這次用 pxBlockToInsert的首地址加上  pxBlockToInsert的塊大小與 pxIterator指向的下一個塊地址比較,不符合,則要修改 pxBlockToInsert的 Next指針,指向pxIterator的下一個空閒塊;如果符合條件,則再判斷 pxIterator指向的下一個塊地址是否已經到了空閒塊鏈表的最後位置了,如果已經到了

空閒塊鏈表的最後位置,那麼就把 pxIterator指向的下一個塊地址更改爲空閒塊鏈表的最後位置的地址,否則就合併內存並且更新鏈表的數據

/* Do the block being inserted, and the block it is being inserted before
make a contiguous block of memory? */
puc = ( uint8_t * ) pxBlockToInsert;
if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock )
{
	if( pxIterator->pxNextFreeBlock != pxEnd )
	{
		/* Form one big block from the two blocks. */
		pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;
		pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock;
	}
	else
	{
		pxBlockToInsert->pxNextFreeBlock = pxEnd;
	}
}
else
{
	pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
}

最後,要是 pxBlockToInsert沒有和 pxIterator合併,則還要修改 pxIterator的 Next指針

/* If the block being inserted plugged a gab, so was merged with the block
before and the block after, then it's pxNextFreeBlock pointer will have
already been set, and should not be set here as that would make it point
to itself. */
if( pxIterator != pxBlockToInsert )
{
	pxIterator->pxNextFreeBlock = pxBlockToInsert;
}
else
{
	mtCOVERAGE_TEST_MARKER();
}

3、講了這麼多,現在終於到分析 pvPortMalloc()了

先看總代碼:

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

	vTaskSuspendAll();
	{
		/* If this is the first call to malloc then the heap will require
		initialisation to setup the list of free blocks. */
		if( pxEnd == NULL )
		{
			prvHeapInit();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}

		/* Check the requested block size is not so large that the top bit is
		set.  The top bit of the block size member of the BlockLink_t structure
		is used to determine who owns the block - the application or the
		kernel, so it must be free. */
		if( ( xWantedSize & xBlockAllocatedBit ) == 0 )
		{
			/* The wanted size is increased so it can contain a BlockLink_t
			structure in addition to the requested amount of bytes. */
			if( xWantedSize > 0 )
			{
				xWantedSize += xHeapStructSize;

				/* Ensure that blocks are always aligned to the required number
				of bytes. */
				if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
				{
					/* Byte alignment required. */
					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 ) )
			{
				/* Traverse the list from the start	(lowest address) block until
				one	of adequate size is found. */
				pxPreviousBlock = &xStart;
				pxBlock = xStart.pxNextFreeBlock;
				while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
				{
					pxPreviousBlock = pxBlock;
					pxBlock = pxBlock->pxNextFreeBlock;
				}

				/* If the end marker was reached then a block of adequate size
				was	not found. */
				if( pxBlock != pxEnd )
				{
					/* Return the memory space pointed to - jumping over the
					BlockLink_t structure at its start. */
					pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );

					/* This block is being returned for use so must be taken out
					of the list of free blocks. */
					pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;

					/* If the block is larger than required it can be split into
					two. */
					if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
					{
						/* This block is to be split into two.  Create a new
						block following the number of bytes requested. The void
						cast is used to prevent byte alignment warnings from the
						compiler. */
						pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
						configASSERT( ( ( ( size_t ) pxNewBlockLink ) & portBYTE_ALIGNMENT_MASK ) == 0 );

						/* Calculate the sizes of two blocks split from the
						single block. */
						pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
						pxBlock->xBlockSize = xWantedSize;

						/* Insert the new block into the list of free blocks. */
						prvInsertBlockIntoFreeList( pxNewBlockLink );
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}

					xFreeBytesRemaining -= pxBlock->xBlockSize;

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

					/* The block is being returned - it is allocated and owned
					by the application and has no "next" block. */
					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;
}

其實相對於 heap_2的變動不大,可以看一下它們的對比圖

同樣的,只在第一次調用該函數的時候才初始化堆管理,看上圖

接着在這裏只是相對於 heap_2多了個標誌位 xBlockAllocatedBit的判斷來是否執行以下的操作,若是符合,則像 heap_2一樣進行塊字節對齊,看上圖

而剩下的,主要的不同也就是因爲多了個 xBlockAllocatedBit操作,所以使用了 xBlockSize 的最高位做標記,因此就添加了相應的操作,如下圖


至於其他差別不大,此處不做贅述

4、vPortFree()實現

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

	if( pv != NULL )
	{
		/* The memory being freed will have an BlockLink_t structure immediately
		before it. */
		puc -= xHeapStructSize;

		/* This casting is to keep the compiler from issuing warnings. */
		pxLink = ( void * ) puc;

		/* Check the block is actually allocated. */
		configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );
		configASSERT( pxLink->pxNextFreeBlock == NULL );

		if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 )
		{
			if( pxLink->pxNextFreeBlock == NULL )
			{
				/* The block is being returned to the heap - it is no longer
				allocated. */
				pxLink->xBlockSize &= ~xBlockAllocatedBit;

				vTaskSuspendAll();
				{
					/* Add this block to the list of free blocks. */
					xFreeBytesRemaining += pxLink->xBlockSize;
					traceFREE( pv, pxLink->xBlockSize );
					prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
				}
				( void ) xTaskResumeAll();
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
}

相比 heap_2,heap_4只是多了一些檢查,使之更加安全,看下圖比較

 

應用場景及特點:

  • 即使應用程序反覆刪除任務,隊列,信號量,互斥量等也可以使用
  • 與 heap_2實現相比,即使將要分配和釋放的內存具有隨機大小,也很難將堆空間嚴重分割成多個小塊
  • 不確定性,但比大多數標準 C庫 malloc實現要有效得多

對於希望直接在應用程序代碼中使用便攜式層內存分配方案的應用程序,heap_4.c尤其有用(而不是僅通過調用本身調用  pvPortMalloc()和 vPortFree()的 API函數來間接使用)。

 

八、heap_5.c

該方案使用與 heap_4相同,使用首次適應算法和內存合併算法,並允許堆跨越多個不相鄰(不連續)的內存區域。

這個就不說了,跟 heap_4大同小異,有興趣可以看看,就只發一下它的文件註釋說明吧

/*
 * 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.
 *
 * See heap_1.c, heap_2.c, heap_3.c and heap_4.c for alternative
 * implementations, and the memory management pages of http://www.FreeRTOS.org
 * for more information.
 *
 * 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通過調用 vPortDefineHeapRegions()函數實現初始化,只有在 vPortDefineHeapRegions()執行後才允許使用內存分配和釋放。創建RTOS對象(任務、隊列、信號量等等)會隱含的調用 pvPortMalloc(),因此必須注意:使用 heap_5創建任何對象前,要先執行 vPortDefineHeapRegions()函數

 

九、其他

必須吐槽一下,終於結束了

官方的這個地址有詳細說 heap堆內存管理

然後想要了解 heap所用到的算法可以直點 ->實例分析首次適應算法、最佳適應算法、最差適應算法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章