如果RTOS对象是动态创建的,那么标准C库malloc()和free()函数有时可以用于完成这项任务,除了:
- 在嵌入式系统中,它们并不总是可以使用的;
- 它们会占用更多宝贵的代码空间;
- 它们没有线程保护;
- 它们不具有确定性(每次调用执行的时间可能会不同);
因此,通常需要一个替代的内存分配实现方案。
嵌入式/实时系统具有千差万别的RAM和时序要求,因此单个RAM内存分配算法永远仅属于一个应用的子集。
为了解决这个问题,FreeRTOS在移植层保留内存分配API函数。移植层在RTOS核心代码源文件之外(不属于核心源代码),这使得不同的应用程序可以提供适合自己的应用实现。当RTOS内核需要RAM时,调用pvPortMallo()函数来代替malloc()函数。当RAM要被释放时,调用vPortFree()函数来代替free()函数。
FreeRTOS下载包中提供5种简单的内存分配实现,本文稍后会进行描述。用户可以适当的选择其中的一个,也可以自己设计内存分配策略。
FreeRTOS提供了五种内存分配方案,用户可以根据自己的实际情况选择需要的方案。本篇将对这五种进行介绍。
FreeRTOS提供的内存分配方案分别位于不同的源文件(heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c)之中,源文件位于下载包\FreeRTOS\Source\portable\MemMang文件夹中。其它实现方法可以根据需要增加。如果要使用FreeRTOS提供的内存堆分配方案,选中的源文件必须被正确的包含到工程文件中。
如下所示:
- heap_1 - 最简单的一个。一旦分配内存之后,它甚至不允许释放分配的内存。
- heap_2 - 允许释放分配的内存块。但不会把相邻的空闲块合成一个更大的块(换句话说,这会造成内存碎片)。
- heap_3 - 简单的包装了标准库中的malloc()和free()函数,包装后的malloc()和free()函数具备线程保护。
- heap_4 - 相邻的空闲内存块合并成一个更大的块(包含一个合并算法),避免碎片化。
- heap_5 - 实现了heap_4.c中的合并算法,并且允许堆栈跨越多个非连续的内存区。
官方推荐的首选是heap_4。
1-heap_1.c
Heap_1是所有实现中最简单的。一旦分配了内存,就不允许释放内存。尽管如此,heap_1.c适用于大量嵌入式应用程序。这是因为许多小型和深度嵌入式应用程序在系统引导时创建所需的所有任务、队列、信号量等,然后在程序的生命周期内使用的所有这些对象(直到应用程序再次关闭或重新启动)。任何东西都不会被删除。
当需要分配RAM时,这个内存分配方案只是简单的将一个大数组细分出一个子集来。大数组的容量大小通过FreeRTOSConfig.h文件中的configTOTAL_HEAP_SIZE宏来设置。configAPPLICATION_ALLOCATED_HEAP FreeRTOSConfig.h配置常量允许将堆放置在内存中的特定地址。
xPortGetFreeHeapSize() API函数返回未分配的堆空间总量,允许对configTOTAL_HEAP_SIZE设置进行优化。
heap_1实现:
- 如果你的应用程序从来没有删除任务、队列、信号量、互斥量等,可以使用它(这实际上涵盖了使用FreeRTOS的大多数应用程序)。
- 始终是确定的(始终需要相同的时间来执行),并且不会导致内存碎片化。
- 实现和分配过程非常简单,需要的内存是从一个静态数组中分配的,意味着这种内存分配通常只是适用于那些不进行动态内存分配的应用。
2- heap_2.c
方案使用一个最佳匹配算法,与heap_1不同的是heap_2允许释放之前分配的内存块。它不会把相邻的空闲块合成一个更大的块(换句话说,这会造成内存碎片)。
有效的堆栈空间大小由位于FreeRTOSConfig.h文件中的configTOTAL_HEAP_SIZE宏来设置。
API函数xPortGetFreeHeapSize() 返回未分配的堆空间总量(允许configTOTAL_HEAP_SIZE设置进行优化),但不提供关于未分配内存如何被分割成更小块的碎片信息。
pvPortCalloc()函数具有与标准库calloc函数相同的功能。它为对象数组分配内存,并将分配的存储中的所有字节初始化为零。如果分配成功,它将返回一个指向已分配内存块中最低字节的指针。如果失败,它返回一个空指针。
2.1 heap_2功能实现
- 即使在应用程序重复删除任务、队列、信号量、互斥锁等情况下也可以使用,但需要注意内存碎片。
- 如果分配和释放的内存大小是随机的,则不应使用。例如:
- 如果一个应用程序动态的创建和删除任务,并且分配给任务的堆栈空间总是同样大小,那么大多数情况下heap_2.c是可以使用的。但是,如果分配给任务的堆栈不总是相等,那么释放的有效内存可能碎片化,形成很多小的内存块。最后会因为没有足够大的连续堆栈空间而造成内存分配失败。在这种情况下,heap_4.c是一个很好的选择。
- 如果一个应用程序动态的创建和删除任务,并且分配给任务的堆栈空间总是同样大小,那么大多数情况下heap_2.c是可以使用的。但是,如果分配给任务的堆栈不总是相等,那么释放的有效内存可能碎片化,形成很多小的内存块。最后会因为没有足够大的连续堆栈空间而造成内存分配失败。在这种情况下,heap_4.c是一个很好的选择。
- 应用程序直接调用pvPortMalloc() 和 vPortFree()函数,而不仅是通过FreeRTOS API间接调用。
- 如果应用程序队列、任务、信号量、互斥等的顺序不可预知,可能会导致内存碎片问题。这种情况发生的几率是非常低的,但应该牢记。
- 不具有确定性,但比大多数标准C库malloc的实现更有效。
Heap_2.c适用于需要动态创建对象的多数小型实时系统。
3-heap_3.c
简单的包装了标准库中的malloc()和free()函数,包装后的malloc()和free()函数具备线程保护作用。
3.1 功能实现
- 需要链接器设置一个堆栈,并且编译器库提供malloc()和free()函数。
- 不具有确定性
- 可能明显的增大RTOS内核的代码大小
注意,FreeRTOSConfig.h中的configTOTAL_HEAP_SIZE设置在使用heap_3时无效。
4-heap_4.c
可用堆空间的总量由configTOTAL_HEAP_SIZE设置(configTOTAL_HEAP_SIZE在FreeRTOSConfig.h中定义。)configAPPLICATION_ALLOCATED_HEAP FreeRTOSConfig.h配置常量允许将堆放置在内存中的特定地址。
xPortGetFreeHeapSize() API函数返回调用该函数时未分配的堆空间总量,xPortGetMinimumEverFreeHeapSize() API函数返回FreeRTOS应用程序启动时存在的最低空闲堆空间总量。这两个函数都没有提供关于如何将未分配的内存分割成更小的块的信息。
vPortGetHeapStats() API函数提供了附加信息。它填充heap_t结构的成员,如下所示。
1/* vPortGetHeapStats()函数的原型 */ 2void vPortGetHeapStats( HeapStats_t *xHeapStats ); 3 4/* Heap_stats_t结构的定义。*/ 5typedef struct xHeapStats 6{ 7size_t xAvailableHeapSpaceInBytes; /* 当前可用的堆总大小——这是所有空闲块的总和,而不是可以分配的最大块*/ 8size_t xSizeOfLargestFreeBlockInBytes; /* 调用vPortGetHeapStats()时堆中所有空闲块的最大大小(以字节为单位)。*/ 9size_t xSizeOfSmallestFreeBlockInBytes; /* 调用vPortGetHeapStats()时堆中所有空闲块的最小大小(以字节为单位)。*/ 10size_t xNumberOfFreeBlocks; /* 在调用vPortGetHeapStats()时,堆中空闲内存块的数量。 */ 11size_t xMinimumEverFreeBytesRemaining; /* 自系统启动以来,堆中的最小总空闲内存(所有空闲块的总和)。 */ 12size_t xNumberOfSuccessfulAllocations; /* 返回有效内存块的对pvPortMalloc()的调用次数。 */ 13size_t xNumberOfSuccessfulFrees; /* 成功释放内存块的对vPortFree()的调用次数。*/ 14} HeapStats_t;
pvPortCalloc()函数具有与标准库calloc函数相同的功能。它为对象数组分配内存,并将分配的存储中的所有字节初始化为零。如果分配成功,它将返回一个指向已分配内存块中最低字节的指针。失败时,它返回一个空指针。
4.1 实现:
- 可用于重复分配、删除任务、队列、信号量、互斥量等等的应用程序。
- 可以用于分配和释放随机字节内存的情况,并不像heap_2.c那样产生严重碎片。
- 不具有确定性,但是它比标准库中的malloc函数具有高得多的效率。
heap_4.c对于希望在应用程序代码中直接使用可移植层内存分配方案的应用程序特别有用(而不是通过调用本身调用pvPortMalloc()和vPortFree()的API函数间接使用)。
5-heap_5.c
Heap_5是通过调用vPortDefineHeapRegions()来初始化的,并且在执行vPortDefineHeapRegions()之前不能使用。创建一个RTOS对象(任务、队列、信号量等)将隐式调用pvPortMalloc(),因此在使用heap_5时,必须在创建任何此类对象之前调用vPortDefineHeapRegions()。
vPortDefineHeapRegions()接受单个参数。参数是HeapRegion_t结构的数组。HeapRegion_t在portable.h中定义为:
1typedef struct HeapRegion 2{ 3/* Start address of a block of memory that will be part of the heap.*/ 4uint8_t *pucStartAddress; 5 6/* Size of the block of memory. */ 7size_t xSizeInBytes; 8} HeapRegion_t;
The HeapRegion_t type definition
这个数组必须使用一个NULL指针和0字节元素作为结束,起始地址必须从小到大排列。下面的代码段提供一个例子。MSVCWin32模拟器演示例程使用了heap_5,因此可以当做一个参考例程。
/* 在内存中为内存堆分配两个内存块.第一个内存块0x10000字节,起始地址为0x80000000, 第二个内存块0xa0000字节,起始地址为0x90000000.起始地址为0x80000000的内存块的
起始地址更低,因此放到了数组的第一个位置.*/
1const HeapRegion_t xHeapRegions[] = 2{ 3{ ( uint8_t * ) 0x80000000UL, 0x10000 }, 4{ ( uint8_t * ) 0x90000000UL, 0xa0000 }, 5{ NULL, 0 } /* 数组结尾. */ 6}; 7/* 向函数vPortDefineHeapRegions()传递数组参数. */ 8vPortDefineHeapRegions( xHeapRegions );
xPortGetFreeHeapSize() API函数返回调用该函数时未分配的堆空间总量,xPortGetMinimumEverFreeHeapSize() API函数返回FreeRTOS应用程序启动时存在的最低空闲堆空间总量。这两个函数都没有提供关于如何将未分配的内存分割成更小的块的信息。
pvPortCalloc()函数具有与标准库calloc函数相同的功能。它为对象数组分配内存,并将分配的存储中的所有字节初始化为零。如果分配成功,它将返回一个指向已分配内存块中最低字节的指针。失败时,它返回一个空指针。