- 前言
- 8.1 C标准库的内存管理
- 8.2 freertos内存管理接口
- 8.3 freertos五种内存管理
- 8.4 heap5内存管理实现细节
前言
本章主要讲解内部存储空间(RAM)的管理。
详细分析heap5方案。
参考:
8.1 C标准库的内存管理
C标准库的内存管理用到的API是malloc()
和free()
,但是不建议在RTOS中直接调用,因为:
- C标准库的内存管理实现可能比较大,不适合小型嵌入式RAM不足的设备。
- 可能会产生内存碎片,对于安全性要求高的嵌入式设备不适合。
- 这两个函数会使得链接器配置得复杂。
- 待补充。
8.2 freertos内存管理接口
freertos的内存管理和内核实现是相互独立的,内核规定内存管理接口,而接口内容却是可由外部自由实现。
但是freertos官方也提供了几种内存分配算法:heap1、heap2、heap3、heap4、heap5。
所以,需要内存管理的有合适的算法可以单独使用freertos提供内存分配算法到自己的设备或系统中。
内存堆大小由宏configTOTAL_HEAP_SIZE
决定。(heap3方案除外)
/* * Map to the memory management routines required for the port. */ void * pvPortMalloc( size_t xSize ) PRIVILEGED_FUNCTION; //内存申请函数 void vPortFree( void * pv ) PRIVILEGED_FUNCTION; //内存释放函数 void vPortInitialiseBlocks( void ) PRIVILEGED_FUNCTION; //初始化内存堆函数 size_t xPortGetFreeHeapSize( void ) PRIVILEGED_FUNCTION; //获取当前未分配的内存堆大小 size_t xPortGetMinimumEverFreeHeapSize( void ) PRIVILEGED_FUNCTION; //获取未分配的内存堆历史最小值
8.3 freertos五种内存管理
简单介绍。
8.3.1 heap1
特点:
- 只能申请不能释放。
- 不会产生内存碎片。
- 函数的执行时间是确定的。因为可直接查到空闲空间地址和大小。
应用:这种方案一般用在安全性要求较高的系统中。用于从不删除任务、队列、信号量、互斥量等的应用程序。
实现:
使用xNextFreeByte
来定位下一个空闲的内存堆位置。
因为freertos系统堆是一个大数组,所以,内存空间是连续的。
所以xNextFreeByte
值在heap1方案中实际保存的是已经被分配的内存大小,下次申请时跳过这些已申请的,剩下就是空闲空间。
pucAlignedHeap
是一个指向对齐后的内存堆起始地址。
用户提供的系统堆内存的起始地址不一定是对齐的内存地址,需要纠正,纠正后的系统堆内存起始地址保存在pucAlignedHeap
。
static size_t xNextFreeByte = ( size_t ) 0; static uint8_t *pucAlignedHeap = NULL;
API注意:
vPortInitialiseBlocks()
仅仅将静态局部变量xNextFreeByte
设置为0,表示内存没有被申请。xPortGetFreeHeapSize()
并不是释放内存,因为heap1方案不支持释放,所以该API是获取当前未分配的内存堆大小。
8.3.2 heap2
特点:
- 支持动态申请和释放。
- 链表管理,但是不支持拼接相邻空闲块。所以容易产生内存碎片。
- 申请时间具有不确定性,因为检索空闲块需要检索链表,空闲块多、内存碎片多时,检索会久点。但是效率比标准C库中的malloc函数高得多。
应用:不建议用于内存分配和释放是随机大小的应用程序。
实现:
heap2方案的内存管理链表:
typedef struct A_BLOCK_LINK { struct A_BLOCK_LINK *pxNextFreeBlock; size_t xBlockSize; } BlockLink_t;
pxNextFreeBlock
:是指向下一个空闲内存块的指针。
xBlockSize
:记录当前内存块大小。(内存管理函链表结构体)
8.3.3 heap3
特点:
- 需要链接器设置一个堆,malloc()和free()函数由编译器提供。
- 具有不确定性。
- 很可能增大RTOS内核的代码大小。
configTOTAL_HEAP_SIZE
不起作用,因为堆空间由编译器决定提供的。一般在启动文件里设置。
实现:
heap3方案只是简单的封装了标准C库中的malloc()和free()函数。
重新封装后的malloc()和free()函数具有保护功能,采用的封装方式是操作内存前挂起调度器、完成后再恢复调度器。
8.3.4 heap4
和heap2方案类似,且支持相邻空闲块拼接,降低内存块碎片化几率。
特点:
- 支持动态申请和释放。
- 按地址升序,优先返回第一个满足size需求的空闲块。
- 链表管理,支持相邻空闲块拼接。
- 申请时间具有不确定性,因为检索空闲块需要检索链表,但是效率比标准C库中的malloc函数高得多。
应用:
- 可用于重复删除任务、队列、信号量、互斥量等的应用程序。
- 可用于分配和释放随机字节内存的应用程序。
8.3.5 heap5
heap_5.c方案在实现动态内存分配时与heap4.c方案一样,采用最佳匹配算法和合并算法。
并且允许内存堆跨越多个非连续的内存区。
如可以在片内RAM中定义一个内存堆,还可以在外部SDRAM再定义一个或多个内存堆,这些内存都归系统管理。
heap1、heap2、heap4的堆空间都是有个大数组,拓展下,支持非连续的内存堆,可以使用多个大数组啊。
特点:
- 支持动态申请和释放。
- 按地址升序,优先返回第一个满足size需求的空闲块。
- 链表管理,支持相邻空闲块拼接。
- 支持内存堆跨越多个非连续的内存区。
- 申请时间具有不确定性,因为检索空闲块需要检索链表,但是效率比标准C库中的malloc函数高得多。
实现:
各块内存堆管理结构体:
typedef struct HeapRegion { uint8_t *pucStartAddress; // 内存堆起始地址 size_t xSizeInBytes; // 内存块大小 } HeapRegion_t;
初始化后的内存如下:
8.4 heap5内存管理实现细节
实现的内存管理特点如下:
- 支持动态申请和动态释放。
- 按地址升序,优先返回第一个满足size需求的空闲块。
- 支持相邻空闲块拼接。
- 支持多个非连续堆空间。
- 注意:在创建内核组件前,先调用
vPortDefineHeapRegions()
完成系统堆初始化。
8.4.1 相关接口
/* Map to the memory management routines required for the port. */ /* 先初始化多个非连续堆空间 */ void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions ) PRIVILEGED_FUNCTION; /* 内存申请函数 */ void * pvPortMalloc( size_t xSize ) PRIVILEGED_FUNCTION; /* 内存释放函数 */ void vPortFree( void * pv ) PRIVILEGED_FUNCTION; void vPortInitialiseBlocks( void ) PRIVILEGED_FUNCTION; // 初始化内存堆函数 size_t xPortGetFreeHeapSize( void ) PRIVILEGED_FUNCTION; // 获取当前未分配的内存堆大小 size_t xPortGetMinimumEverFreeHeapSize( void ) PRIVILEGED_FUNCTION; // 获取未分配的内存堆历史最小值 void vPortGetHeapStats( HeapStats_t *xHeapStats ); // 提供堆状态信息
8.4.2 相关参数
xFreeBytesRemaining
:表示当前系统中未分配的内存堆大小。
xMinimumEverFreeBytesRemaining
:表示未分配内存堆空间历史最小的内存值。为了了解最坏情况下内存堆的使用情况。
xBlockAllocatedBit
:系统最高位比特位1。如在32位系统下,该变量值位0x80000000。
- 通常用法:当当前内存块被占用时,当前内存块size记录值会与该变量按位或,标志当前内存块被占用。
xStart
:内存管理块链表头节点,用于指向第一个内存块。
pxEnd
:内存管理块链表尾节点指针,用于表示后面没有合法内存空间。
xNumberOfSuccessfulAllocations
:申请成功的次数。
xHeapStructSize
:某个内存块的管理结构的size,该size也是(向上)字节对齐的。
heapMINIMUM_BLOCK_SIZE
:内存块size下限。(内存块管理区+数据区),在heap5方案中是两倍的xHeapStructSize
,即是数据区最小也要有一个xHeapStructSize
大小。
8.4.3 数据结构
8.4.3.1 各非连续堆块管理数据结构
各大块堆空间管理数据结构类型HeapRegion_t
:
pucStartAddress
:该块堆空间内存起始地址。xSizeInBytes
:该块堆空间大小。
typedef struct HeapRegion { uint8_t * pucStartAddress; size_t xSizeInBytes; } HeapRegion_t;
例如,如果定义两个非连续堆空间:
- 第一个内存块大小为0x10000字节,起始地址为0x80000000;
- 第二个内存块大小为0xa0000字节,起始地址为0x90000000。
- 按照地址顺序放入到数组中,地址小的在前。
- 该数据结构供给堆空间初始化API
vPortDefineHeapRegions()
使用。
const HeapRegion_t xHeapRegions[] = { { ( uint8_t * ) 0x80000000UL, 0x10000 }, { ( uint8_t * ) 0x90000000UL, 0xa0000 }, { NULL, 0 } /* 数组结尾 */ };
8.4.3.2 内存块管理数据结构
内存块管理数据结构类型BlockLink_t
:
-
pxNextFreeBlock
:是指向下一个空闲内存块的指针。 -
xBlockSize
:记录当前内存块大小。(内存管理函链表结构体)- 需要特别注意的是,这个内存块大小不仅仅代表当前内存块可用空间,还表示当前内存块是否空闲:该变量最高位为0时,表示空闲,为1时,表示被占用。(参考:
heapBITS_PER_BYTE
)
- 需要特别注意的是,这个内存块大小不仅仅代表当前内存块可用空间,还表示当前内存块是否空闲:该变量最高位为0时,表示空闲,为1时,表示被占用。(参考:
-
就是单向链表。
typedef struct A_BLOCK_LINK { struct A_BLOCK_LINK * pxNextFreeBlock; /* 下一个空闲内存块的指针 */ size_t xBlockSize; /* 当前内存块大小 */ } BlockLink_t
8.4.3.3 主要数据
除了数据结构类型外,管理数据结构还需要一些变量来实现。
链表头xStart
:
- 内存块链表头。
static BlockLink_t xStart;
链表尾指针pxEnd
:
- 内存块链表尾。
static BlockLink_t *pxEnd = NULL;
8.4.4 初始化堆空间:vPortDefineHeapRegions()
函数原型:
void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions );
-
pxHeapRegions
:传入保存非连续堆空间地址的数据结构地址。HeapRegion_t
:各非连续块堆空间管理数据结构类型。
8.4.4.1 数据校验
初始时内存块链表尾指针为空,表示没有被初始化过,没有被使用过才能重置这些堆块。
configASSERT( pxEnd == NULL );
如果有堆块被初始化过了,内存块链表尾指针不应该为空。
同时,新的堆块地址在旧块后。
/* 初始化非首个堆块 */ if(xDefinedRegions != 0) { /* 如果初始化过堆块,肯定有尾节点的 */ configASSERT( pxEnd != NULL ); /* 新的堆块起始地址要在前面堆块尾地址后 */ configASSERT( xAddress > ( size_t ) pxEnd ); }
8.4.4.2 地址对齐
用户传入的堆块空间始末地址不一定符合地址对齐的,在初始化时需要裁剪,使堆块地址向内对齐。
首地址向上对齐:
if( ( xAddress & portBYTE_ALIGNMENT_MASK ) != 0 ) /* 如果还没对齐,需要实现向上对齐 */ { /* 先向上漂移到下一个对齐空间 */ xAddress += ( portBYTE_ALIGNMENT - 1 ); /* 去除余数,即是往当前对齐空间下对齐 */ xAddress &= ~portBYTE_ALIGNMENT_MASK; /* 更新对齐后,可用的实际空间。即是减去对齐丢弃的空间 */ xTotalRegionSize -= xAddress - ( size_t ) pxHeapRegion->pucStartAddress; } /* 对齐后的堆块起始地址 */ xAlignedHeap = xAddress;
同时,在堆块尾部需要预留出尾部链表节点的空间,该空间起始地址也需要符合地址对齐。
8.4.4.3 头节点:xStart
内存管理块链表的头节点未xStart
,该节点只是一个哨兵,不含数据,只指向第一个堆块。
该节点的实际空间并不在堆块内,而内存管理块节点和xpEnd
指向的尾节点的空间都是存在堆块内部。
8.4.4.4 内存管理块
内存管理块数据结构内容参考前面。
刚刚初始化的堆块内存,一整块都是空闲空间,但是这些空间需要内存管理块数据结构来管理。
heap5方案与其它方案不一样,需要支持不连续地址,,其实现是在堆块尾部需要预留一个内存管理块节点空间出来,用于对接下一个堆块。
所以初始化完堆块后,其内部结构如下图所示:
8.4.4.5 完整代码实现
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; /* 没有被初始化过才能往下执行 */ 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为哨兵节点,不带数据,只带指向首个堆块 */ xStart.pxNextFreeBlock = ( BlockLink_t * ) xAlignedHeap; xStart.xBlockSize = ( size_t ) 0; } else /* 初始化非首个堆块 */ { /* 如果初始化过堆块,肯定有尾节点的 */ configASSERT( pxEnd != NULL ); /* 新的堆块起始地址要在前面堆块尾地址后 */ configASSERT( xAddress > ( size_t ) pxEnd ); } /* 备份系统堆块尾部节点 */ pxPreviousFreeBlock = pxEnd; /* 在新堆块末留出尾节点空间,用于表示系统堆块空间结束点 */ xAddress = xAlignedHeap + xTotalRegionSize; xAddress -= xHeapStructSize; xAddress &= ~portBYTE_ALIGNMENT_MASK; pxEnd = ( BlockLink_t * ) xAddress; pxEnd->xBlockSize = 0; pxEnd->pxNextFreeBlock = NULL; /* 在当前堆块内前部空间作为当前堆块管理数据结构 */ 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; /* 系统堆必须有空间可用才能在后面被访问 */ configASSERT( xTotalHeapSize ); /* 系统最高位标记为1。 如32为系统时,该值为0x80000000。 用于标记内存块是否空闲。被占用时,将内存块节点内的内存块大小xBlockSize和该值按位或。 */ xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 ); }
8.4.5 内存块插入空闲链表:prvInsertBlockIntoFreeList()
这是一个内部接口函数,用户不会接触到,但是后面的malloc和free的实现会使用到,所以在这里先分析了。
原型
static void prvInsertBlockIntoFreeList( BlockLink_t * pxBlockToInsert );
8.4.5.1 检索内存块相邻的方法
插入空闲块是按空闲块地址顺序插入的。
需要找到当前空闲块起始地址比新插入的空闲块起始地址小,且当前空闲块链表指向的下一个空闲块的起始地址比新插入的空闲块起始地址大,新的空闲块就是需要插入到这两中间。
/* 先找到和pxBlockToInsert相邻的前一个空闲块 */ for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock ) { /* Nothing to do here, just iterate to the right position. */ }
8.4.5.2 合并内存块
检查内存块地址连续的方法就是:本块内存起始地址+本块内存大小==下一块内存起始地址。
/* 如果前一个空闲块和pxBlockToInsert在地址上是连续的,就和前一块合并 */ if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert ) { /* ... */ } /* 如果pxBlockToInsert和其下一个空闲块地址连续,就和下一个空闲块合并 */ if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock ) { /* ... */ }
8.4.5.3 完整代码实现
static void prvInsertBlockIntoFreeList( BlockLink_t * pxBlockToInsert ) { BlockLink_t * pxIterator; uint8_t * puc; /* 先找到和pxBlockToInsert相邻的前一个空闲块 */ for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock ) { /* Nothing to do here, just iterate to the right position. */ } /* 获取pxBlockToInsert相邻的前一个空闲块的起始地址 */ puc = ( uint8_t * ) pxIterator; /* 如果前一个空闲块和pxBlockToInsert在地址上是连续的,就和前一块合并 */ if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert ) { /* size先合并 */ pxIterator->xBlockSize += pxBlockToInsert->xBlockSize; /* 内存管理块也统一下 */ pxBlockToInsert = pxIterator; } else { mtCOVERAGE_TEST_MARKER(); } /* 获取当前需要插入的空闲块。(可能是合并后的,也可能是没有合并的,都是下一个空闲块的前一个空闲块) */ puc = ( uint8_t * ) pxBlockToInsert; /* 如果pxBlockToInsert和其下一个空闲块地址连续,就和下一个空闲块合并 */ if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock ) { if( pxIterator->pxNextFreeBlock != pxEnd ) /* 如果pxBlockToInsert的下一个空闲块的下一个空闲节点不是尾节点,便需要合并 */ { /* 合并size */ pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize; /* 更新指向。合并时,pxBlockToInsert指向原有下一个空闲块的下一个空闲块 */ pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock; } else /* 如果pxBlockToInsert的下一个空闲块的空闲节点是尾节点 */ { /* 合并时,不需要扩充size,只需要更新指向即可 */ pxBlockToInsert->pxNextFreeBlock = pxEnd; } } else { /* 如果不能合并,就直接插入 */ pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock; } /* 如果新插入的空闲块没有和前一个空闲块合并,插入新的空闲块后需要更新链表节点指向 */ if( pxIterator != pxBlockToInsert ) { pxIterator->pxNextFreeBlock = pxBlockToInsert; } else { mtCOVERAGE_TEST_MARKER(); } }
8.4.6 内存申请:pvPortMalloc()
8.4.6.1 原型
void * pvPortMalloc( size_t xWantedSize )
xWantedSize
:输入用户需要申请的空间大小。
返回:
- 申请成功后返回用户可用空间的起始地址。
- 申请失败返回NULL。
8.4.6.2 参数校验
用pxEnd
不为空证明该接口对应的堆空间已经被初始化过。
/* 堆块必须被初始化过 */ configASSERT( pxEnd );
xWantedSize
该值不能大到顶部标志都被覆盖掉。
/* 需要申请的内存不能大到顶部标志都被覆盖掉。 */ if( ( xWantedSize & xBlockAllocatedBit ) == 0 ) { /* ... */ }
8.4.6.3 参数纠正
xWantedSize
变量先是用户传入的期望申请的大小,而内部空闲块管理除了给用户使用的数据区外还需要内存块管理区,所以需要添加一个xHeapStructSize
。
/* 一个内存块除了数据区外,还需要管理区BlockLink_t */ xWantedSize += xHeapStructSize;
还需要字节向上对齐:
- 向上对齐是size往更大方向对齐。
- 防止溢出是防止
xWantedSize
扩大后不能大到覆盖掉顶部标志位。
/* 确保申请的内存大小也符合字节对齐 */ if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 ) { /* 要往上对齐,且要防止溢出 */ if( ( xWantedSize + ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) ) ) > xWantedSize ) { xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) ); } else { xWantedSize = 0; } }
8.4.6.4 拆分内存块
如果检索到size
符合要求的第一个空闲块,malloc会检查该块除了给用户使用的空间和内存管理块的空间后,剩余的空间是否满足组建新的空闲块,如果满足就进行拆分。
/* 如果当前空闲块满足用户需求后,还剩足够的空间组件另一个空闲块,那就需要拆分 */ if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE ) { /* 组建新的空闲块,其起始地址为上一个空闲块地址+xWantedSize偏移就是了 */ pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize ); /* 新的空闲块节点size处理。 */ pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize; pxBlock->xBlockSize = xWantedSize; /* 新的空闲块插入到空闲链表中 */ prvInsertBlockIntoFreeList( ( pxNewBlockLink ) ); }
8.4.6.5 完整代码实现
void * pvPortMalloc( size_t xWantedSize ) { BlockLink_t * pxBlock, * pxPreviousBlock, * pxNewBlockLink; void * pvReturn = NULL; /* 堆块必须被初始化过 */ configASSERT( pxEnd ); vTaskSuspendAll(); { /* 需要申请的内存不能大到顶部标志都被覆盖掉。 */ if( ( xWantedSize & xBlockAllocatedBit ) == 0 ) { /* 申请的size大于0,且加上内存管理块后不能溢出 */ if( ( xWantedSize > 0 ) && ( ( xWantedSize + xHeapStructSize ) > xWantedSize ) ) { /* 一个内存块除了数据区外,还需要管理区BlockLink_t */ xWantedSize += xHeapStructSize; /* 确保申请的内存大小也符合字节对齐 */ if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 ) { /* 要往上对齐,且要防止溢出 */ if( ( xWantedSize + ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) ) ) > xWantedSize ) { xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) ); } else { xWantedSize = 0; } } else { mtCOVERAGE_TEST_MARKER(); } } else { xWantedSize = 0; } /* 申请的内存占用的size要小于现有空闲size才会进行检索 */ if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) ) { /* 从低地址的空闲块开始检索 */ pxPreviousBlock = &xStart; pxBlock = xStart.pxNextFreeBlock; /* 找出能满足xWantedSize大小的空闲块 */ while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) ) { pxPreviousBlock = pxBlock; pxBlock = pxBlock->pxNextFreeBlock; } /* 确保当前块不是尾节点 */ if( pxBlock != pxEnd ) { /* 返回数据区起始地址给用户 */ pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize ); /* 把当前块从空闲链表中移除 */ pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock; /* 如果当前空闲块满足用户需求后,还剩足够的空间组件另一个空闲块,那就需要拆分 */ if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE ) { /* 组建新的空闲块,其起始地址为上一个空闲块地址+xWantedSize偏移就是了 */ pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize ); /* 新的空闲块节点size处理。 */ pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize; pxBlock->xBlockSize = xWantedSize; /* 新的空闲块插入到空闲链表中 */ prvInsertBlockIntoFreeList( ( pxNewBlockLink ) ); } else { mtCOVERAGE_TEST_MARKER(); } /* 更新系统堆剩余空闲空间 */ xFreeBytesRemaining -= pxBlock->xBlockSize; /* 更新申请内存的历史最低值 */ if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining ) { xMinimumEverFreeBytesRemaining = xFreeBytesRemaining; } else { mtCOVERAGE_TEST_MARKER(); } /* 标记当前块已被使用 */ pxBlock->xBlockSize |= xBlockAllocatedBit; /* 重置节点指向 */ pxBlock->pxNextFreeBlock = NULL; /* 更新全局malloc次数 */ xNumberOfSuccessfulAllocations++; } 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 /* if ( configUSE_MALLOC_FAILED_HOOK == 1 ) */ return pvReturn; }
8.4.7 内存释放
8.4.7.1 原型
void vPortFree( void * pv );
pv
:需要释放的内存空间的起始地址。
8.4.7.2 简要分析
通过传入的地址,偏移后找到该内存块的数据管理结构。
检查传入的地址是否合法,内存块是否符合特征。
在heap5代码中检查内存块是否合法的做法还可以添加一个条件,传入的地址在堆块范围内
检查合法后,把当前内存块回收到空闲链表:
- 清除被占用位。
- 把当前空闲块插回空闲链表。
8.4.7.3 完整代码实现
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 ) { /* 先消除被占用标志位,这样该变量才是表示当前内存块的size */ pxLink->xBlockSize &= ~xBlockAllocatedBit; vTaskSuspendAll(); { /* 把当前块回收到内核,更新空闲空间size */ xFreeBytesRemaining += pxLink->xBlockSize; traceFREE( pv, pxLink->xBlockSize ); /* 把当前块插回空闲链表 */ prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) ); /* 更新记录调用free次数 */ xNumberOfSuccessfulFrees++; } ( void ) xTaskResumeAll(); } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } }
8.4.8 获取总堆空闲size:xPortGetFreeHeapSize()
size_t xPortGetFreeHeapSize( void ) { return xFreeBytesRemaining; }
8.4.9 获取历史申请最小空间值:xPortGetMinimumEverFreeHeapSize()
size_t xPortGetMinimumEverFreeHeapSize( void ) { return xMinimumEverFreeBytesRemaining; }
8.4.10 获取堆状态信息
状态信息结构体:
/* 用于从vPortGetHeapStats()传递关于堆的信息. */ typedef struct xHeapStats { size_t xAvailableHeapSpaceInBytes; /* The total heap size currently available - this is the sum of all the free blocks, not the largest block that can be allocated. */ size_t xSizeOfLargestFreeBlockInBytes; /* The maximum size, in bytes, of all the free blocks within the heap at the time vPortGetHeapStats() is called. */ size_t xSizeOfSmallestFreeBlockInBytes; /* The minimum size, in bytes, of all the free blocks within the heap at the time vPortGetHeapStats() is called. */ size_t xNumberOfFreeBlocks; /* The number of free memory blocks within the heap at the time vPortGetHeapStats() is called. */ size_t xMinimumEverFreeBytesRemaining; /* The minimum amount of total free memory (sum of all free blocks) there has been in the heap since the system booted. */ size_t xNumberOfSuccessfulAllocations; /* The number of calls to pvPortMalloc() that have returned a valid memory block. */ size_t xNumberOfSuccessfulFrees; /* The number of calls to vPortFree() that has successfully freed a block of memory. */ } HeapStats_t;
接口实现:
void vPortGetHeapStats( HeapStats_t * pxHeapStats ) { BlockLink_t * pxBlock; size_t xBlocks = 0, xMaxSize = 0, xMinSize = portMAX_DELAY; /* portMAX_DELAY used as a portable way of getting the maximum value. */ vTaskSuspendAll(); { pxBlock = xStart.pxNextFreeBlock; /* 堆块被初始化过才能进去 */ if( pxBlock != NULL ) { /* 遍历每一个空闲内存块 */ do { /* 记录当前有多少个空闲内存块(也包括非连续堆块末节点的内存块,虽然数据区size为0,但是也记录在内) */ xBlocks++; /* 更新最大块的size */ if( pxBlock->xBlockSize > xMaxSize ) { xMaxSize = pxBlock->xBlockSize; } /* 需要注意的是heap5支持多个非连续的堆块,所以非连续堆块末节点值用于指向下一个内存块,其内数据区size为0 */ if( pxBlock->xBlockSize != 0 ) { /* 更新最小块size */ if( pxBlock->xBlockSize < xMinSize ) { xMinSize = pxBlock->xBlockSize; } } /* 遍历下一个内存块 */ pxBlock = pxBlock->pxNextFreeBlock; } while( pxBlock != pxEnd ); } } ( void ) xTaskResumeAll(); /* 收集部分堆状态信息 */ pxHeapStats->xSizeOfLargestFreeBlockInBytes = xMaxSize; pxHeapStats->xSizeOfSmallestFreeBlockInBytes = xMinSize; pxHeapStats->xNumberOfFreeBlocks = xBlocks; taskENTER_CRITICAL(); /* 进入临界。维护堆块管理的全局变量的原子性 */ { /* 收集剩余堆状态信息 */ pxHeapStats->xAvailableHeapSpaceInBytes = xFreeBytesRemaining; pxHeapStats->xNumberOfSuccessfulAllocations = xNumberOfSuccessfulAllocations; pxHeapStats->xNumberOfSuccessfulFrees = xNumberOfSuccessfulFrees; pxHeapStats->xMinimumEverFreeBytesRemaining = xMinimumEverFreeBytesRemaining; } taskEXIT_CRITICAL(); /* 退出临界 */ }