堆内存管理
先决条件
FreeRTOS是作为一组C源文件提供的,因此成为一个合格的C程序员是使用FreeRTOS的先决条件。
动态内存分配及其与FreeRTOS的相关性
内核对象:如任务、队列、信号量和事件组。为了使FreeRTOS尽可能易于使用,这些内核对象不是在编译时静态分配的,而是在运行时动态分配的。
**FreeRTOS在每次创建内核对象时分配RAM,在每次删除内核对象时分配RAM。**该策略减少了设计和规划工作,简化了API,并将RAM占用最小化。
动态内存分配是一个C编程概念,它与FreeRTOS相关,因为内核对象是动态分配的,通用编译器提供的动态内存分配方案并不总是适合实时应用程序。
可以使用标准C库malloc和free()函数分配内存,但由于以下原因,它们并不合适:
- 它们并不总是在小型嵌入式系统上可用。
- 它们的实现可能相对较大,占用宝贵的代码空间。
- 它们很少是线程安全的。
- 它们不是决定性的,执行函数所花费的时间因调用而异。
- 它们会遭受分裂。(如果堆中空闲的RAM被分割成彼此分离的小块,则认为堆是碎片化的。如果堆是碎片化的,那么如果堆中没有一个空闲块大到足以容纳该块,即使堆中所有独立的空闲块的总大小比无法分配的块的大小大很多倍,分配块的尝试也是失败的。)
- 它们会使链接器配置复杂化。
- 如果允许堆空间增长为其它变量所使用的内存,则它们可能是难以调试错误的来源。
动态内存分配选项
FreeRTOS现在将内存分配作为可移植层的一部分,因为不同的嵌入式系统有不同的动态内存分配和定时需求,单一的动态内存分配算法只适用于应用程序的一个子集。从核心代码库中删除动态内存分配使应用程序编写者能够在适当的时候提供自己的特定实现。
当FreeRTOS需要RAM时,调用pvPortMalloc()。当RAM被释放时,内核调用vPortFree()。
pvPortMalloc()和vPortFree()是公共函数,因此可以从应用程序代码中调用。
FreeRTOS附带了pvPortMalloc()和vPortFree()的五个示例实现,所有这些都在本章中有文档记录。FreeRTOS应用程序可以使用其中一个示例实现,也可以提供自己的示例实现。
五个实例分别在heap_1.c…heap_5.c源文件中定义,所有这些源文件位于FreeRTOS/source/protable/MemMang目录中。
内存分配方案示例
Heap_1
对于小型专用嵌入式系统来说,通常只在调度程序启动之前创建任务和其它内核对象。在这种情况下,只有在应用程序开始执行任何实时功能之前,内核才会动态分配内存,并且在应用程序的生命周期内一直分配内存。这意味着所选择的分配方案不必考虑任何更复杂的内存分配问题,如确定性和碎片,可以只考虑代码大小和简单性等属性。
Heap_1.c实现了pvPortMalloc()的一个非常基本的版本,而没有实现vPortFree()。不删除任务或其它内核对象的应用程序可以使用heap_1。
一些商业关键和安全关键系统可能禁止使用动态内存分配,但它们也有可能使用heap_1。关键系统通常禁止动态内存分配,因为不确定性、内存碎片和失败的分配相关的不确定性,但是Heap_1总是确定的,并且不能使内存碎片化。
当调用pvPortMalloc()时,heap_1分配方案将一个简单数组细分为更小的块,该数组称为FreeRTOS堆。
数组的总大小(以字节为单位)由FreeRTOSConfig.h中的configTOTAL_HEAP_SIZE定义设置。用这种方式定义大型数组会使应用程序看起来消耗大量RAM,甚至在从数组分配任何内存之前。
每个创建的任务都需要从堆中分配一个任务控制块(TCB)和一个堆栈。图5展示了在创建任务时heap_1如何细分简单数组。
每次创建任务时,从heap_1数组分配RAM。
- A显示了创建任何任务之前的数组是空闲的。
- B展示了创建一个任务后的数组。
- C展示了创建三个任务后的数组。
Heap_2
为了向后兼容,在FreeRTOS发行版中保留了Heap_2,但不建议在新的设计中使用它。考虑使用heap_4而不是heap_2,因为heap_4提供了增强的功能。
Heap_2.c的工作原理还包括细分一个由configTOTA_HEAP_SIZE表示大小的数组。它使用最适合的算法来分配内存,与heap_1不同,它允许释放内存。同样数组是静态声明的,因此会使应用程序看起来消耗大量RAM,甚至在数组的任何内存被分配之前。
最佳拟合算法确保pvPortMalloc()使用在大小上最接近请求字节数的空闲内存块。
例如,考虑以下场景:“堆包含三个空闲内存块,分别为5字节、25字节和100字节。
调用pvPortMalloc()请求20字节的RAM。
能够容纳所请求的字节数的最小空闲RAM块是25字节块,因此pvPortMalloc()将25字节块分割为一个20字节的块和一个5字节的块,然后返回一个指向20字节块的指针。新的5字节块在以后对pvPortMalloc()的调用中仍然可用。
与heap_4不同,heap_2不会将相邻的内存块组合成单个较大的块,因此更容易发生碎片化。但是,如果分配的块和随后释放的块的总是相同的大小,碎片就不是问题。
因此,Heap_2适用于重复创建和删除任务的应用程序,只要分配给已创建任务的堆栈大小不变。
图6演示了在创建、删除和再次创建任务时,最佳匹配算法是如何工作的。参考图6:
- A显示创建三个任务后的数组。一个大的空闲块保留在数组的顶部。
- B显示一个任务被删除后的数组。数组顶部的大空闲块保持不变。现在还有两个更小的空闲块,它们以前分配给了被删除任务的TCB和堆栈。
- C显示创建另一个任务后的情况,创建任务调用pvPortMalloc()的两次调用,一个用于分配新的TCB,另一个用于分配任务堆栈。任务是使用xTaskCreate()API函数创建的。对pvPortMalloc()的调用发生在xTaskCreate()内部。
每个TCB的大小完全相同,因此最佳拟合算法确保了之前分配给删除任务的TCB的RAM块被重用来分配新任务的TCB。
分配给新创建的任务的堆栈大小与分配给之前删除的任务的堆栈大小相同,因此最佳拟合算法确保了之前分配给被删除任务的堆栈的RAM块被重用来分配新任务的堆栈。
Heap_2不是确定的,但是比大多数标准库实现的malloc()和free()快。
Heap_3
Heap_3.c使用标准库malloc()和free()函数,因此堆的大小由链接器配置定义,configTOTAL_HEAP_SIZE设置不受影响。
Heap_3通过临时挂起FreeRTOS调度器使malloc()和free()线程安全。
Heap_4
与heap_1和heap_2一样,heap_4的工作原理是将数组细分为更小的块。与前面一样,数组是静态声明的,并由configTOTAL_HEAP_SIZE进行尺寸划分,因此将使应用程序看起来消耗大量RAM,甚至在实际从数组分配任何内存之前。
Heap_4使用first fit算法来分配内存:将相邻的空闲内存块合并为更大的内存块,将内存碎片的风险降到最低。
First fit算法确保pvPortMalloc()使用第一个空闲内存块,该内存块足够大,可以容纳请求的字节数。
1.堆包含三个空闲内存块,按照它们在数组中出现的顺序,分别为5字节、200字节和100字节。
2.调用pvPortMalloc()请求20字节的RAM。
3.第一个空闲的RAM块中,所请求的字节数将装入200字节块,因此pvPortMalloc()将200字节块分割为一个20字节的块和一个180字节的块,然后返回一个指向20字节块的指针。新的180字节块仍然可用于将来对pvPortMalloc()的调用。
Heap_4将相邻的空闲块组合(合并)为单个较大的块,最大限度地降低了碎片的风险,并使其适合于重复分配和释放不同大小的RAM块的应用程序。
图7演示了在分配和释放内存时,heap_4首先适合内存合并算法是如何工作的。参考图7:
- A显示创建三个任务后的数组。一个大的空闲块保留在数组的顶部。
- B显示一个任务被删除后的数组。数组顶部的大空闲块保持不变。还有一个空闲块的TCB和堆栈的已删除的任务以前已分配。注意,与演示heap_2时不同的是,删除TCB时释放的内存和删除堆栈时释放的内存并不是两个独立的空闲块,而是组合在一起创建一个更大的单个空闲块。
- C显示了创建FreeRTOS队列后的情况。队列是使用xQueueCreate() API函数创建的,该函数在4.3节中介绍。**xQueueCreate()调用pvPortMalloc()来分配队列使用的RAM。**由于heap_4使用优先匹配算法,pvPortMalloc()将从第一个足够大的空闲RAM块分配RAM,该RAM块可以容纳队列,在图7中,这是删除任务时释放的RAM。但是,队列不会消耗空闲块中的所有RAM,因此块被分成两个,未使用的部分仍然可以用于将来对pvPortMalloc()的调用。
- D显示了直接从应用程序代码调用pvPortMalloc()之后的情况,而不是通过调用FreeRTOS API函数来间接调用。用户分配的块足够小,可以放入第一个空闲块,这是分配给队列的内存和分配给下面TCB的内存之间的块。删除任务时释放的内存现在被分成三个独立的块;第一个块保存队列,第二个块保存用户分配的内存,第三个块保持空闲。
- E显示队列被删除后的情况,它会自动释放已分配给被删除队列的内存。现在在用户分配的块的两边都有空闲内存。
- F显示用户分配的内存也被释放后的情况。已被用户分配的块使用的内存与任意一侧的空闲内存相结合,以创建更大的单个空闲块。
Heap_4是不确定的。
设置Heap_4使用的Array的起始地址
有时,应用程序编写者有必要将heap_4使用的数组放在特定的内存地址。例如,FreeRTOS任务使用的堆栈是从堆中分配的,因此可能需要确保堆位于快速的内部内存中,而不是慢速的外部内存中。
默认情况下,heap_4使用的数组是在heap_4.c源文件中声明的,它的起始地址由链接器自动设置。但是,如果在FreeRTOSConfig.h中configAPPLICATION_ALLOCATED_HEAP编译时配置常量被设置为1,那么数组必须由使用FreeRTOS的应用程序声明。如果数组被声明为应用程序的一部分,那么应用程序的编写者可以设置它的起始地址。
Heap_5
heap_5分配和释放内存的算法与heap_4使用的算法相同。与heap_4不同,heap_5不局限于从单个静态声明的数组分配内存;Heap_5可以从多个独立的内存空间分配内存。
当运行FreeRTOS的系统提供的RAM在系统的内存映射中**不作为单个连续(**没有空格)块出现时,Heap_5是有用的。
在编写本文时,heap_5是唯一提供的内存分配方案,在调用pvPortMalloc()之前必须显式初始化它。Heap_5使用vPortDefineHeapRegions() API函数初始化。当使用heap_5时,必须在创建任何内核对象(任务、队列、信号量等)之前调用vPortDefineHeapRegions()。
vPortDefineHeapRegions()API函数
vPortDefineHeapRegions()用于指定每个独立内存区域的起始地址和大小,它们共同构成了heap_5使用的总内存。
void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions );
每个独立的内存区域都由类型为HeapRegion_t的结构描述。所有可用内存区域的描述作为HeapRegion_t结构数组传递给vPortDefineHeapRegions()。
typedef struct HeapRegion
{
/* 内存块的起始地址(堆的一部分)*/
uint8_t *pucStartAddress;
/* 内存块大小 */
size_t xSizeInBytes;
} HeapRegion_t;
vPortDefineHeapRegions()的参数
- 唯一的参数pxHeapRegions:指向HeapRegion_t结构数组开头的指针。数组中的每个结构都描述了使用heap_5时将成为堆一部分的内存区域的起始地址和长度。
- 数组中的HeapRegion_t结构必须按起始地址排序;描述起始地址最低的内存区域的HeapRegion_t结构必须是数组中的第一个结构,描述起始地址最高的内存区域的HeapRegion_t结构必须是数组中的最后一个结构。
- 数组的结尾由一个HeapRegion_t结构标记,该结构的pucStartAddress成员设置为NULL。
list 6 显示了HeapRegion_t结构的数组,它们一起完整地描述了三个RAM块。
虽然list6正确地描述了RAM,但它没有演示一个可用的示例,因为它将所有RAM分配给堆,没有任何RAM可供其他变量使用。
当编译一个项目时,编译过程的链接阶段为每个变量分配一个RAM地址。可供链接器使用的RAM通常由链接器配置文件描述,如链接器叫别。在图8B中,假设链接器脚本包含ARM1上的信息,但不包含RAM2和RAM3上的信息。因此,链接器在RAM1中放置了变量,只留下RAM1地址0x0001nnnn上面的部分供heap_5使用。0x0001nnnn的实际值取决于被链接的应用程序中包含的所有变量的综合。链接器没有使用RAM2和RAM3,只留下整个RAM2和RAM3供heap_5使用。
堆相关实用函数
xPortGetFreeHeapSize() API函数:返回调用该函数时堆中的空闲字节数。它可用于优化堆大小。例如,如果xPortGetFreeHeapSize() 在创建所有内核对象后返回2000,configTOTAL_HEAP_SIZE的值可以减少2000。
当使用heap_3时,xPortGetFreeHeapSize()不可用。
size_t xPortGetFreeHeapSize( void );
xPortGetMinimumEverFreeHeapSize() API函数:返回自FreeRTOS应用程序开始执行以来堆中存在的未分配字节的最小数量。
xPortGetMinimumEverFreeHeapSize()返回的值指示了应用程序接近耗尽堆空间的程序。
如果xPortGetMinimumEverFreeHeapSize()返回200,那么在应用程序开始执行后的某个时刻,它距离堆空间耗尽不足200字节。
xPortGetMinimumEverFreeHeapSize()仅在使用heap_4或heap_5时可用。
size_t xPortGetMinimumEverFreeHeapSize( void );
Malloc钩子函数失败
**pvPortMalloc()可以直接从应用程序代码调用,每次创建内核对象时,也会在FreeRTOS源文件中调用它。**内核对象的例子包括任务、队列、信号量和事件组。
就像标准库的malloc()函数一样,如果pvPortMalloc()因为请求大小的块不存在而不能返回RAM块,那么它将返回NULL。如果由于应用程序编写器正在创建内核对象而执行pvPortMalloc(),并且对pvPortMalloc()的调用返回NULL,则不会创建内核对象。
如果对pvPortMalloc()的调用返回NULL,所有堆分配方案都可以分配回调函数(钩子函数)。
如果在FreeRTOSConfig.h中将configUSE_MALLOC_FAILED_HOOK设置为1,那么应用程序必须提供一个malloc失败钩子函数。
void vApplicationMallocFailedHook(void);