从v9.0.0后,FreeRTOS开始支持内核对象的静态分配方式,因此,内存管理库可以被裁剪。但在大多数嵌入式应用中,堆的使用还是非常常见的。因此,还是有必要研究一下FreeRTOS的内存管理。
在绝大多数情况下,嵌入式应用会使用动态内存分配的编程模型,所以,对于FreeRTOS内核而言,每当需要创建内核对象时,内核会申请内存用于存储内核对象的元信息;当删除内核对象时,则会释放相应的内存。而FreeRTOS所使用的堆是由其自身的内存管理模块来实现的。
在FreeRTOS的内存管理中,提供了5种内存管理实现方式,每种管理方式对外的接口一致。
在所有实现中,FreeRTOS的堆都只有一个,即由内核管理的系统堆,而非由任务管理,因此,所有任务都共享这个系统堆。
Heap_1是最简单的实现方式。它不实现内存释放接口。
适用场景:在调度器开启之前,只创建内核对象,事后也不进行释放。
设计思路:
configTOTAL_HEAP_SIZE
设置。静态申请一片内存作为FreeRTOS的堆;pvPortMalloc()
:该函数被调用时,一个数组(heap)会被划分成小块分配出去。设定自定义位置的堆空间的方法:
configAPPLICATION_ALLOCATED_HEAP = 1
;uint8_t ucHeap[configTOTAL_HEAP_SIZE] __attribute__ ((section(".my_heap")));
Heap_2的实现开始支持内存的释放。
适用场景:重复的申请和释放内存,但操作的内存块大小一致。(为了兼容旧版本,新的应用不建议使用)
设计思路:
pvPortMalloc()
:使用Best fit算法找到一个最接近的空闲块,分配出去;如果满足分裂条件,还会将空闲块拆分成两份再分配。pvPortFree()
:释放时,并不会对相邻的块进行合并。该实现在申请和释放内存时的耗时是不确定的,但比C标准库的malloc()
和free()
实现要快一些。
设计思路:使用标准的malloc()
和free()
函数实现,因此堆大小由链接器的配置决定。需要特别说明的是,Heap_3实现了线程安全,具体是通过挂起调度器实现。
Heap_4是在实际工程中使用最多的一种实现方式。
设计思路:与Heap_2类似,不同之处在于,释放内存时,会自动对相邻的空闲块进行合并。
size_t
的最高有效位), 所以内存块的最大长度为2sizeof(size_t) * 8 - 1
字节, 例如在ARM CA9中,必须小于2GBpvPortMalloc()
: 使用First fit算法, 找到第一个满足需求的空闲块, 分配出去.适用场景:系统的物理RAM不是连续的,需要将分散的多个地址范围联合
设计思路:与Heap_4类似。
vPortDefineHeapRegions(const HeapRegion_t * const pxHeapRegions)
,申明所有被用作堆的内存区域。函数接口:
void *pvPortMalloc( size_t xSize ) PRIVILEGED_FUNCTION;
该函数会从系统堆中申请xSize
个字节的内存,如果申请成功,则返回成功分配的内存地址。
下面则简要地说明5个堆如何实现该接口的。
在介绍具体的实现之前,需要先了解几个比较重要的宏定义和全局静态变量。
对齐后的系统堆大小:这是一个宏定义,用于记录地址对齐调整后的系统堆的大小。其定义如下:可以看到, 这个调整值会使得可用的系统堆偏小, 即堆数组的空间存在浪费. 最极端的例子就是堆的首地址本身就符合对齐要求, 这里却进行了调整, 浪费了portBYTE_ALIGNMENT
字节的空间.
#define configADJUSTED_HEAP_SIZE ( configTOTAL_HEAP_SIZE - portBYTE_ALIGNMENT )
系统堆数组:这个实际上就是系统堆的实际承载者,是一个连续的无符号char
数组,大小为configTOTAL_HEAP_SIZE
。它也可以在外部定义。
#if ( configAPPLICATION_ALLOCATED_HEAP == 1 )
extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#else
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#endif
下一个空闲的字节(xNextFreeByte
):记录了相对于堆首地址的空闲内存地址的偏移字节数。每当申请内存时更新。
要理解内存管理的实现,最关键的是需要理解空闲块的组织形式。Heap_1的空闲快组织形式如下图:
与Heap_1一样,Heap_2也定义了调整地址对齐后的堆大小configADJUSTED_HEAP_SIZE
。也定义了系统堆的承载数组(ucHeap[]
)。
除此之外,由于Heap_2的实现是将空闲块组织成链表的形式,所以还定义了空闲块的链表结构:
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;
系统通过静态全局变量xStart
和xEnd
指向空闲块链表的首部和尾部。首部是一个不存在的块。而尾部xEnd
也只是一个结尾标识, 并不占用堆空间。
在前文提到,Heap_2的实现将内存块的元信息嵌入到申请的内存块中,因此需要调整用户所需的内存块的大小,并实现地址对齐,于是定义了元信息已对齐时占用的内存大小heapSTRUCT_SIZE
:
static const uint16_t heapSTRUCT_SIZE = ( ( sizeof( BlockLink_t ) + ( portBYTE_ALIGNMENT - 1 ) ) & ~portBYTE_ALIGNMENT_MASK );
这个空闲块链表最初时(即堆初始化完成后),只有一个空闲块,这个块大小就是整个系统堆的大小。但随着用户不断的申请内存,这个块大小会逐渐被分割,每申请一次就有可能触发一次分割,而这个触发分割的条件便是目标内存块是否大于heapMINIMUN_BLOCK_SZIE
,其定义如下:
#define heapMINIMUM_BLOCK_SIZE ( ( size_t ) ( heapSTRUCT_SIZE * 2 ) )
从定义可以看出,只要目标块大于等于两个空闲块元信息的大小,那么表示它还有可能进行下一次分配,所以可以将其分隔。当然, 也不必过于纠结这个值, 只是一个经验值而已.
Heap_2使用变量xFreeBytesRemaining
来记录当前剩余的堆空闲:
static size_t xFreeBytesRemaining = configADJUSTED_HEAP_SIZE;
需要注意的是,这个变量并不考虑碎片的情况。
Heap_2的空闲块链表的模型如下图所示:
Heap_3的实现完全基于C的标准库,只是增加了调度器挂起的动作以确保线程安全。
具体实现如下:
{
挂起调度器(`vTaskSuspendAll()`)。
申请内存(`pvReturn = malloc( xWantedSize )`)。
恢复调度器(`vTaskResumeAll()`)。
(仅开启USE_MALLOC_FAILED_HOOK)如果申请内存失败(`pvReturn == NULL`),调用钩子函数(`vApplicationMallocFailedHook()`)。
返回`pvReturn`。
}
Heap_4的实现与Heap_2相似,以相同的形式定义了系统堆的承载数组ucHeap[]
,也定义了一样的内存块元信息结构体BlockLink_t
以及结构体链表的首部(xStart
)和尾部(pxEnd
)指针。与Heap_2不同的是,空闲链表的尾部pxEnd
也是一个空闲块,但只占用了系统堆一个内存块的元信息大小的内存。另外, Heap_4的空闲链表不是按照块大小排序的.
为了将内存块元信息嵌入内存块,也定义了该结构体的长度变量(xHeapStructSize
)。实现内存分割时,也以heapMINIMUM_BLOCK_SIZE
作为分割条件。
为了记录系统堆的空闲内存,也使用了xFreeBytesRemaining
进行记录,同样地,其不考虑内存碎片。该实现还增加了xMinimumEverFreeBytesRemaining
,其记录的是到目前为止, 最小的空闲内存量,随着内存的释放,也不会增加这个值。通过这个值, 可以知道系统对内存的最大使用压力.
此外,该实现还定义了字节的位数heapBITS_PER_BYTE
:
#define heapBITS_PER_BYTE ( ( size_t ) 8 )
与Heap_2不同的是,它还使用了分配位来表示某个块是否已经被分配:
static size_t xBlockAllocatedBit = 0;
这个分配位位于size_t
的最高有效位,如果该位为1,表示已分配,0表示未分配。目前只在内存释放时, 用与检查目标内存块的有效性.
Heap_4的空闲块模型如下图所示:
由于Heap_4使用最多,所以下面以UML活动图的方式给出了其对该函数的实现。
Heap_5的实现与Heap_4非常相似,唯一的不同点在与系统堆的初始化:
void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions ) PRIVILEGED_FUNCTION;
在Heap_5中,该函数必须在申请内存接口被调用之前就调用。
pxHeapRegions
是一个HeapRegion_t
的数组,定义了Heap_5可用的内存区域。
函数接口:
void vPortFree( void *pv ) PRIVILEGED_FUNCTION;
该函数将pv
指向的内存块归还给系统堆,不同的堆实现的具体行为不一致。
因为不支持内存释放,所以未实现该接口。
先来看看Heap_2是如何将空闲块插入空闲块队列中的(#define prvInsertBlockIntoFreeList( pxBlockToInsert ) ...
):为了减少调用栈的深度,这里使用宏来实现这个函数。其主要是遍历链表,找到第一个不小于当前需要被放入的空闲块的大小的位置,将内存块插入其中。这便使得空闲块按照块大小顺序排列。
{
获取被插入块的长度(`xBlockSize = pxBlockToInsert->xBlockSize`)。
遍历空闲块链表,直到找到不小于被插入块大小的位置(`for ( pxInterator = &xStart; pxInterator->pxNextFreeBlock->xBlockSize < xBlockSize; pxInterator = pxIterator->pxNextFreeBlock )`)。
// 此时`pxInterator`已经指向了不小于被插入块大小的内存块的前驱
将被插入块的下一个空闲块指针指向合适的位置(`pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock`)。
将空闲块链入空闲块链表(`pxIterator->pxNextFreeBlock = pxBlockToInsert`)。
}
接下来就可以给出Heap_2对释放函数的具体实现如下:
{
初始化`puc = ( uint8_t * ) pv`。
如果传入的地址不为空(`pv != NULL`):
{
获取该内存块的元信息的首地址(`puc -= heapSTRUCT_SIZE`)。
获取该内存块的元信息(`pxLink = ( void * ) puc`)。
挂起调度器(`vTaskSuspendAll()`)。
将该内存块加入到空闲块链表(`prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) )`)。
更新系统堆空闲空间(`xFreeBytesRemaining += pxLink->xBlockSize`)。
恢复调度器(`vTaskResumeAll()`)。
}
}
从实现中可以看到,Heap_2的内存块释放只是简单地将该内存块加入到空闲块链表,而不会对相邻的内存块进行合并。
如前文所说,Heap_3只是对标准库的一个简单封装,具体实现如下:
{
如果地址不为空(`pv != NULL`):
{
挂起调度器(`vTaskSuspendAll()`)。
释放内存(`free( pv )`)。
恢复调度器(`xTaskResumeAll()`)。
}
}
先看看Heap_4如何将空闲块插入到空闲块链表中(static void prvInsertBlockIntoFreeList( BlockLink_t * pxBlockToInsert )
):与Heap_2的实现不同,Heap_4因为更加复杂,所以将其实现为了静态函数。它新增了一个非常重要的功能,就是自动联合相邻的空闲块。下面该函数实现的活动图如下:
与Heap_4一致,请参看Heap_4的实现。
用于实现MPU功能的堆初始化:
void vPortInitialiseBlocks( void ) PRIVILEGED_FUNCTION;
// TODO: 需要进一步研究
获取空闲的堆空间:
size_t xPortGetFreeHeapSize( void ) PRIVILEGED_FUNCTION;
size_t xPortGetMinimumEverFreeHeapSize( void ) PRIVILEGED_FUNCTION;
Heap_1中只实现了vPortInitialiseBlocks()
和xPortGetFreeHeapSize()
。
在vPortInitialiseBlocks()
中,只是简单的将xNextFreeByte = 0
,重置了当前的空闲偏移量。
而在xPortGetFreeHeapSize()
中,空闲空间的计算也比较简单,即直接返回configADJUSTED_HEAP_SIZE - xNextFreeByte
。
Heap_2中只实现了xPortGetFreeHeapSize()
,只是简单的将xFreeBytesRemaining
返回给调用者。
不实现工具函数。
Heap_4实现了xPortGetFreeHeapSize()
和xPortGetMinimumEverFreeHeapSize()
,即直接返回xFreeBytesRemaining
和xMinimumEverFreeBytesRemaining
。
请参看Heap_4的实现。
定义如下:
typedef struct HeapRegion
{
uint8_t *pucStartAddress;
size_t xSizeInBytes;
} HeapRegion_t
该结构体用于实现Heap_5(物理内存地址不连续的堆)。一个结构体则定义了一个可用的内存区域,最终的实现将会是通过一个HeapRegion_t
数组,定义一组可用的内存区域作为Heap_5的堆空间。
该数组定义时有两点要求:
定义内存地址对齐的常量:
#if portBYTE_ALIGNMENT == 32
#define portBYTE_ALIGNMENT_MASK ( 0x001F )
#endif
#if portBYTE_ALIGNMENT == 16
#define portBYTE_ALIGNMENT_MASK ( 0x000F )
#endif
#if portBYTE_ALIGNMENT == 8
#define portBYTE_ALIGNMENT_MASK ( 0x0007 )
#endif
#if portBYTE_ALIGNMENT == 4
#define portBYTE_ALIGNMENT_MASK ( 0x0003 )
#endif
#if portBYTE_ALIGNMENT == 2
#define portBYTE_ALIGNMENT_MASK ( 0x0001 )
#endif
#if portBYTE_ALIGNMENT == 1
#define portBYTE_ALIGNMENT_MASK ( 0x0000 )
#endif
#ifndef portBYTE_ALIGNMENT_MASK
#error "Invalid portBYTE_ALIGNMENT definition"
#endif
这些掩码用于实现堆内存地址的对齐。
下面这个宏定义了Heap_5支持的内存区域个数:
#ifndef portNUM_CONFIGURABLE_REGIONS
#define portNUM_CONFIGURABLE_REGIONS 1
#endif