在嵌入式系統中,內存資源通常非常有限,內存泄漏可能導致系統性能下降甚至崩潰。內存泄漏是指程序分配的內存未被正確釋放,逐漸耗盡可用內存。

FreeRTOS作為一種輕量級實時操作系統(RTOS),廣泛應用于資源受限的嵌入式設備,其內存管理機制為開發者提供了檢測和預防內存泄漏的工具和方法。
我們先聊一聊FreeRTOS內存管理機制。
FreeRTOS通過不同的堆實現管理動態內存分配,位于源代碼的portable/MemMang目錄下。
以下是五種堆實現及其特點:

由于heap_1.c不支持釋放,內存泄漏在傳統意義上不存在。因此,本文重點關注支持釋放的堆實現(heap_2.c、heap_3.c、heap_4.c和heap_5.c),因為這些實現中若分配的內存未被釋放,可能導致泄漏。
FreeRTOS提供兩個關鍵函數用于監控堆使用情況,幫助開發者檢測潛在的內存泄漏:
以下是一個監控任務,定期記錄堆使用情況:
void?vMonitorHeapTask(void?*pvParameters)?{? ??size_t?xFreeHeapSize, xMinFreeHeapSize;? ??for(;;) {? ? ? ? xFreeHeapSize =?xPortGetFreeHeapSize();? ? ? ? xMinFreeHeapSize =?xPortGetMinimumEverFreeHeapSize();? ? ? ??printf("當前剩余堆: %u 字節, 歷史最小剩余堆: %u 字節\n",?? ? ? ? ? ? ? ?xFreeHeapSize, xMinFreeHeapSize);? ? ? ??vTaskDelay(pdMS_TO_TICKS(1000));?// 每秒記錄一次? ? }}
FreeRTOS提供跟蹤宏(trace macros),允許開發者自定義記錄內核和應用程序事件的行為。
針對內存管理,traceMALLOC和traceFREE宏可用于跟蹤pvPortMalloc和vPortFree調用,幫助識別未釋放的內存塊。
在FreeRTOSConfig.h或應用代碼中,可以定義以下宏:
#define traceMALLOC(pvReturn, xSize) do { \? ? TaskHandle_t xCurrentTask = xTaskGetCurrentTaskHandle(); \? ? char *pcTaskName = pcTaskGetName(xCurrentTask); \? ??printf("任務 %s 分配 %u 字節于地址 %p\n", pcTaskName, xSize, pvReturn); \}?while(0)#define traceFREE(pv, xSize) do { \? ??printf("釋放地址 %p\n", pv); \}?while(0)
上述代碼使用printf僅為示例。在實際嵌入式系統中,可能需要將日志寫入緩沖區或通過串口輸出,具體取決于硬件支持。
通過檢查日志,開發者可以對比分配和釋放記錄,尋找未釋放的內存塊。例如,若某個地址在traceMALLOC中出現但未在traceFREE中出現,則可能是泄漏點。
通過修改heap_4.c的BlockLink_t結構,添加字段記錄分配任務的句柄或名稱。例如,Chris Hockuba的文章建議維護一個分配列表(如BlockLink_t* allocList[256]),記錄每個分配的內存塊及其所屬任務。
以下是簡化實現:
void?vPortAddToList(BlockLink_t *pxBlock)?{? ??for?(int?i =?0; i 256; i++) {? ? ? ??if?(allocList[i] ==?NULL) {? ? ? ? ? ? allocList[i] = pxBlock;? ? ? ? ? ??break;? ? ? ? }? ? }}void?vPortRmFromList(BlockLink_t *pxBlock)?{? ??for?(int?i =?0; i 256; i++) {? ? ? ??if?(allocList[i] == pxBlock) {? ? ? ? ? ? allocList[i] =?NULL;? ? ? ? ? ??break;? ? ? ? }? ? }}
此方法需要深入理解FreeRTOS源碼,適合高級開發者。
內存泄漏有時與緩沖區溢出相關。可以在分配的內存塊首尾添加canary值(固定模式),定期檢查是否被覆蓋。例如,在pvPortMalloc中額外分配4字節用于尾部canary值,并在釋放時驗證。
若不希望修改堆實現,可以在應用層包裝pvPortMalloc和vPortFree,記錄分配信息:
typedef?struct?{? ??void?*pvAddress;? ??size_t?xSize;? ??const?char?*pcTaskName;} AllocationRecord;AllocationRecord xAllocations[MAX_ALLOCATIONS];int?xAllocationCount =?0;void?*myMalloc(size_t?xSize)?{? ??void?*pvReturn =?pvPortMalloc(xSize);? ??if?(pvReturn !=?NULL?&& xAllocationCount < MAX_ALLOCATIONS) {? ? ? ? TaskHandle_t xCurrentTask =?xTaskGetCurrentTaskHandle();? ? ? ??char?*pcTaskName =?pcTaskGetName(xCurrentTask);? ? ? ? xAllocations[xAllocationCount].pvAddress = pvReturn;? ? ? ? xAllocations[xAllocationCount].xSize = xSize;? ? ? ? xAllocations[xAllocationCount].pcTaskName = pcTaskName;? ? ? ? xAllocationCount++;? ? }? ??return?pvReturn;}void?myFree(void?*pv)?{? ??for?(int?i =?0; i < xAllocationCount; i++) {? ? ? ??if?(xAllocations[i].pvAddress == pv) {? ? ? ? ? ? xAllocations[i] = xAllocations[xAllocationCount -?1];? ? ? ? ? ? xAllocationCount--;? ? ? ? ? ??break;? ? ? ? }? ? }? ??vPortFree(pv);}void?vPrintAllocations(void)?{? ??for?(int?i =?0; i < xAllocationCount; i++) {? ? ? ??printf("任務 %s 分配 %u 字節于 %p\n",?? ? ? ? ? ? ? ?xAllocations[i].pcTaskName, xAllocations[i].xSize, xAllocations[i].pvAddress);? ? }}
此跟蹤器記錄每個分配的地址、大小和任務名稱,可通過vPrintAllocations檢查當前分配狀態。
最后,總結一下,為預防和檢測內存泄漏,建議遵循以下實踐:
