从FreeRTOS V9.0.0起,可以完全静态分配FreeRTOS应用程序,而无需包括堆内存管理器
FreeRTOS是作为一组C源文件提供的,因此,成为一名合格的C程序员是使用FreeRTOS的先决条件,因此,本章假定读者熟悉以下概念:
从FreeRTOS V9.0.0起,可以在编译时静态分配内核对象,或者在运行时动态分配内核对象:
本书的后续章节将介绍内核对象,例如任务,队列,信号量和事件组。 为了使FreeRTOS尽可能易于使用,这些内核对象不是在编译时静态分配的,而是在运行时动态分配的。 FreeRTOS每次创建内核对象时都会分配RAM,并且每次删除内核对象时都会释放RAM。 该策略减少了设计和规划工作,简化了API,并最大程度地减少了RAM占用空间。
本章讨论动态内存分配。 动态内存分配是C编程概念,而不是FreeRTOS或多任务专用的概念。 它与FreeRTOS有关,因为内核对象是动态分配的,并且通用编译器提供的动态内存分配方案并不总是适合于实时应用程序。
可以使用标准C库malloc()和free()函数来分配内存,但是由于以下一种或多种原因,它们可能不合适,也可能不合适:
从FreeRTOS V9.0.0起,可以在编译时静态分配内核对象,或者在运行时动态分配内核对象:
FreeRTOS的早期版本使用内存池分配方案,即在编译时预先分配不同大小的内存块池,然后由内存分配函数返回。 尽管这是在实时系统中使用的常见方案,但事实证明,它是许多支持请求的源头,主要是因为它无法充分有效地使用RAM,无法在非常小的嵌入式系统中使用,因此该方案被放弃了。
FreeRTOS现在将内存分配视为可移植层的一部分(与核心代码库的一部分相对)。 认识到不同的嵌入式系统具有变化的动态内存分配和时序要求这一事实,因此,单个动态内存分配算法仅适用于部分应用程序。 此外,从核心代码库中删除动态内存分配,使应用程序编写者可以在适当时提供自己的特定实现。
当FreeRTOS需要RAM时,它不调用malloc(),而是调用pvPortMalloc()。 释放RAM时,内核将调用vPortFree()而不是调用free()。 pvPortMalloc()与标准C库malloc()函数具有相同的原型,而vPortFree()与标准C库free()函数具有相同的原型。
pvPortMalloc()和vPortFree()是公共函数,因此也可以从应用程序代码中调用。
从FreeRTOS V9.0.0起,可以在编译时静态分配内核对象,或者在运行时动态分配内核对象:
FreeRTOS带有pvPortMalloc()和vPortFree()的五个示例实现,所有这些都在本章中介绍。 FreeRTOS应用程序可以使用示例实现之一,也可以提供自己的示例实现。
这五个示例分别在heap_1.c,heap_2.c,heap_3.c,heap_4.c和heap_5.c源文件中定义,所有这些文件均位于FreeRTOS / Source / portable / MemMang目录中。
本章旨在使读者更好地理解:
从FreeRTOS V9.0.0起,可以完全静态分配FreeRTOS应用程序,而无需包括堆内存管理器
小型专用嵌入式系统通常在启动调度程序之前仅创建任务和其他内核对象。 在这种情况下,内存仅在应用程序开始执行任何实时功能之前由内核动态分配,并且在应用程序的生命周期内仍会分配内存。 这意味着选择的分配方案不必考虑任何更复杂的内存分配问题,例如确定性和分段性,而只需考虑诸如代码大小和简单性之类的属性。
Heap_1.c实现了pvPortMalloc()的非常基本的版本,而不实现vPortFree()。 永不删除任务或其他内核对象的应用程序有可能使用heap_1。
否则将禁止使用动态内存分配的某些商业关键和安全性至关重要的系统也有可能使用heap_1。 由于与不确定性,内存碎片和分配失败有关的不确定性,关键系统通常禁止动态内存分配,但是Heap_1始终是确定性的,无法碎片化内存。
随着对pvPortMalloc()的调用,heap_1分配方案将一个简单的数组细分为较小的块。 该阵列称为FreeRTOS堆。
数组的总大小(以字节为单位)由FreeRTOSConfig.h中的configTOTAL_HEAP_SIZE定义设置。 以这种方式定义大型阵列可能会使应用程序看起来消耗大量RAM,甚至在未从阵列分配任何内存之前也是如此。
每个创建的任务都需要一个任务控制块(TCB)和一个要从堆中分配的堆栈。 图5演示了堆_1如何在创建任务时细分简单数组。
参考图5:
图5.每次创建任务时从heap_1数组分配的RAM
为了向后兼容,Heap_2保留在FreeRTOS发行版中,但是不建议在新设计中使用它。 考虑使用heap_4而不是heap_2,因为heap_4提供了增强的功能。
Heap_2.c也可以通过细分由configTOTAL_HEAP_SIZE定义尺寸的数组来工作。 它使用最佳适合算法来分配内存,并且与heap_1不同,它确实允许释放内存。 同样,该数组是静态声明的,因此即使在分配了该数组中的任何内存之前,应用程序也似乎要消耗大量RAM。
最佳适合算法可确保pvPortMalloc()使用大小与请求的字节数最接近的空闲内存块。 例如,请考虑以下情形:
所请求的字节数可容纳的最小RAM空闲块是25个字节的块,因此pvPortMalloc()将25个字节的块分为一个20字节的块和一个5字节的块2,然后再返回20字节的块的指针。 新的5字节块仍可用于以后对pvPortMalloc()的调用。
与heap_4不同,Heap_2不会将相邻的空闲块合并为一个更大的块,因此它更易于碎片化。 但是,如果分配的块和随后释放的块始终具有相同的大小,则碎片不会成为问题。 Heap_2适用于重复创建和删除任务的应用程序,前提是分配给创建的任务的堆栈大小不变。
图6.在创建和删除任务时正在分配RAM并将其从heap_2数组中释放
图6演示了在创建,删除任务然后再次创建任务时最合适的算法如何工作。 参考图6:
每个TCB的大小完全相同,因此最佳适合算法可确保先前分配给已删除任务的TCB的RAM块被重新使用,以分配新任务的TCB。
分配给新创建的任务的堆栈大小与分配给先前删除的任务的堆栈大小相同,因此,最佳适合算法可确保将先前分配给已删除任务堆栈的RAM块用于分配新任务堆栈。
阵列顶部较大的未分配块保持不变。
Heap_2不是确定性的,但比大多数malloc()和free()的标准库实现要快。
Heap_3.c使用标准库malloc()和free()函数,因此堆的大小由链接器配置定义,并且configTOTAL_HEAP_SIZE设置没有影响。
Heap_3通过临时挂起FreeRTOS调度程序来使malloc()和free()线程安全。 线程安全和调度程序挂起都是第7章“资源管理”中涉及的主题。
像heap_1和heap_2一样,heap_4通过将数组细分为较小的块来工作。 与以前一样,该数组是静态声明的,并由configTOTAL_HEAP_SIZE确定其大小,因此即使在实际上未从该数组分配任何内存之前,也会使应用程序看起来要消耗大量RAM。
Heap_4使用第一个适合算法来分配内存。 与heap_2不同,heap_4将相邻的空闲内存块合并(合并)为单个较大的块,从而最大程度地减少了内存碎片的风险。
第一种适合算法可确保pvPortMalloc()使用足够大的第一个空闲内存块来容纳请求的字节数。 例如,请考虑以下情形:
所请求的字节数将适合的第一个空闲RAM块是200字节的块,因此pvPortMalloc()在返回20字节的块指针之前将200字节的块拆分为20字节的块和180字节的块3。新的180字节块仍可用于以后对pvPortMalloc()的调用。
Heap_4将相邻的空闲块合并为一个较大的块,从而最大程度地减少了分段的风险,使其适合重复分配和释放不同大小的RAM块的应用程序。
图7. RAM从heap_4数组中分配和释放
图7演示了在分配和释放内存时具有内存合并的heap_4首次适合算法。 参考图7:
Heap_4不是确定性的,但比malloc()和free()的大多数标准库实现要快。
本节包含高级信息。 无需阅读或理解本节即可使用Heap_4。
有时,应用程序编写者有必要将heap_4使用的数组放在特定的内存地址。 例如,FreeRTOS任务使用的堆栈是从堆中分配的,因此可能有必要确保堆位于快速内部存储器中,而不是在慢速外部存储器中。
默认情况下,heap_4使用的数组是在heap_4.c源文件中声明的,并且其起始地址由链接器自动设置。 但是,如果在FreeRTOSConfig.h中将configAPPLICATION_ALLOCATED_HEAP编译时配置常量设置为1,则必须由使用FreeRTOS的应用程序声明该数组。 如果将数组声明为应用程序的一部分,则应用程序的编写者可以设置其起始地址。
如果在FreeRTOSConfig.h中将configAPPLICATION_ALLOCATED_HEAP设置为1,则必须在应用程序的一个源文件中声明一个名为ucHeap的uint8_t数组,并以configTOTAL_HEAP_SIZE设置对其进行尺寸设置。
将变量放置到特定内存地址所需的语法取决于所使用的编译器,因此请参阅编译器的文档。 以下是两个编译器的示例:
heap_5用来分配和释放内存的算法与heap_4所使用的算法相同。 与heap_4不同,heap_5不限于从单个静态声明的数组中分配内存; heap_5可以从多个单独的内存空间分配内存。 当运行FreeRTOS的系统提供的RAM在系统的内存映射中没有显示为单个连续(无空间)的块时,Heap_5很有用。
在撰写本文时,heap_5是唯一提供的必须在调用pvPortMalloc()之前显式初始化的内存分配方案。 使用vPortDefineHeapRegions()API函数初始化Heap_5。 使用heap_5时,必须先调用vPortDefineHeapRegions(),然后才能创建任何内核对象(任务,队列,信号量等)。
vPortDefineHeapRegions()用于指定每个单独的内存区域的起始地址和大小,这些区域共同构成了heap_5使用的总内存。
清单4. vPortDefineHeapRegions()API函数原型
每个单独的存储区均由HeapRegion_t类型的结构描述。 所有可用内存区域的描述作为HeapRegion_t结构的数组传递到vPortDefineHeapRegions()中。
表5. vPortDefineHeapRegions()参数
参数名称/返回值 | 描述 |
---|---|
pxHeapRegions | 指向HeapRegion_t结构数组起点的指针。 数组中的每个结构都描述了当使用heap_5时将成为堆一部分的内存区域的起始地址和长度。 数组中的HeapRegion_t结构必须按起始地址排序; 描述数组起始地址最低的HeapRegion_t结构必须是数组中的第一个结构,描述缓冲区起始地址最高的HeapRegion_t结构必须是数组中的最后一个结构。数组的末尾由HeapRegion_t结构标记,该结构的pucStartAddress成员设置为NULL。 |
举例来说,考虑图8A所示的假设内存映射,其中包含三个独立的RAM块:RAM1,RAM2和RAM3。 假定可执行代码被放置在未示出的只读存储器中。
清单6显示了一个HeapRegion_t结构数组,这些结构一起完整描述了三个RAM块。
清单6.一组HeapRegion_t结构,它们共同描述了RAM的3个区域
尽管清单6正确地描述了RAM,但是它没有展示一个可用的示例,因为它将所有RAM分配给堆,而没有RAM可供其他变量使用。
在构建项目时,构建过程的链接阶段会为每个变量分配一个RAM地址。 链接器可使用的RAM通常由链接器配置文件(例如链接器脚本)描述。 在图8B中,假定链接描述文件包括有关RAM1的信息,但不包括有关RAM2或RAM3的信息。 因此,链接器已将变量放置在RAM1中,仅RAM0的地址0x0001nnnn上方的部分可用于heap_5。 实际值0x0001nnnn将取决于所链接的应用程序中包含的所有变量的总大小。 链接器将所有RAM2和所有RAM3保留为未使用状态,而剩下的整个RAM2和整个RAM3可供heap_5使用。
如果使用清单6中所示的代码,则分配给地址0x0001nnnn下的heap_5的RAM将与用于保存变量的RAM重叠。 为避免这种情况,xHeapRegions []数组中的第一个HeapRegion_t结构可以使用0x0001nnnn的起始地址,而不是0x00010000的起始地址。 但是,这不是推荐的解决方案,因为:
清单7展示了一个更方便和可维护的示例。 它声明了一个称为ucHeap的数组。 ucHeap是一个普通变量,因此它成为链接器分配给RAM1的数据的一部分。 xHeapRegions数组中的第一个HeapRegion_t结构描述了ucHeap的起始地址和大小,因此ucHeap成为了heap_5管理的内存的一部分。 可以增加ucHeap的大小,直到链接器使用的RAM耗尽了所有RAM1,如图8 C所示。
清单7.一个HeapRegion_t结构的数组,这些结构描述所有RAM2,所有RAM3,但仅一部分RAM1
清单7中展示的技术的优点包括:
xPortGetFreeHeapSize()API函数返回调用该函数时堆中的可用字节数。 它可用于优化堆大小。 例如,如果在创建所有内核对象之后xPortGetFreeHeapSize()返回2000,则configTOTAL_HEAP_SIZE的值可以减少2000。
使用heap_3时,xPortGetFreeHeapSize()不可用。
清单8. xPortGetFreeHeapSize()API函数原型
表6. xPortGetFreeHeapSize()返回值
参数名称/返回值 | 描述 |
---|---|
返回值 | 调用xPortGetFreeHeapSize()时堆中未分配的字节数。 |
xPortGetMinimumEverFreeHeapSize()API函数返回自FreeRTOS应用程序开始执行以来堆中已存在的未分配字节的最小数量。
xPortGetMinimumEverFreeHeapSize()返回的值指示应用程序距离堆空间耗尽有多接近。 例如,如果xPortGetMinimumEverFreeHeapSize()返回200,则自从应用程序开始执行以来的某个时间,它在不足堆空间的200个字节之内。
xPortGetMinimumEverFreeHeapSize()仅在使用heap_4或heap_5时可用。
清单9. xPortGetMinimumEverFreeHeapSize()API函数原型
表7. xPortGetMinimumEverFreeHeapSize()返回值
参数名称/返回值 | 描述 |
---|---|
返回值 | 自FreeRTOS应用程序开始执行以来,堆中已存在的最小未分配字节数。 |
可以直接从应用程序代码中调用pvPortMalloc()。 每次创建内核对象时,它在FreeRTOS源文件中也被调用。 内核对象的示例包括任务,队列,信号量和事件组,所有这些都将在本书的后续章节中进行介绍。
就像标准库malloc()函数一样,如果pvPortMalloc()由于请求的大小的块不存在而无法返回RAM块,则它将返回NULL。 如果由于应用程序编写者正在创建内核对象而执行了pvPortMalloc(),并且对pvPortMalloc()的调用返回NULL,则将不会创建内核对象。
如果对pvPortMalloc()的调用返回NULL,则可以将所有示例堆分配方案配置为调用钩子(或回调)函数。
**如果在FreeRTOSConfig.h中将configUSE_MALLOC_FAILED_HOOK设置为1,则应用程序必须提供一个malloc失败的钩子函数,**其名称和原型如清单10所示。该函数可以通过适合该应用程序的任何方式来实现。
如果将堆中的可用RAM分成彼此分开的小块,则认为该堆是碎片化的。 如果堆是零散的,那么即使堆中没有单个空闲块足够大以容纳该块,即使堆中所有单独的空闲块的总大小比其大很多倍,分配块的尝试也会失败。 无法分配的块的大小。 ↩︎
这是一个过分的简化,因为heap_2将有关块大小的信息存储在堆区域内,因此两个拆分块的总和实际上将小于25。 ↩︎
这是一个过分的简化,因为heap_4在堆区域内存储有关块大小的信息,因此两个拆分块的总和实际上将小于200个字节。 ↩︎