背景知识
从FreeRTOS V9.0.0内核版本开始,内核对象占用的内存可以在编译期间静态分配,也可以在运行期间动态分配。内核对象如:tasks(任务),queues(队列),semaphores(信号量)和event groups(事件组)。而为了让FreeRTOS更易于使用,新版内核对象都不是在编译期间静态分配的,而是在运行时动态分配。每次一个内核对象创建时,FreeRTOS就会在RAM分上分配内存,当对内核象销毁时,内核又会释放对象所占的RAM内存。这种原则使得FreeRTOS的设计更加简明,内核API更易于使用。
动态内存分配是C语言编程中的一个概念,而不是FreeRTOS或者多任务里面的特有的概念。FreeRTOS需要涉及到动态内存分配是因为内核对象是动态构造的。在通用的C语言编程环境里,我们可以使用标准库中的malloc()和free()来进行动态内存分配的操作,但这些函数对于实时应用(real-time application)是不合适的,原因有如下几点:
- 在嵌入式系统中他们不总是可用的,有的环境不支持或者没有实现
- 他们的底层实现可能需要较多的资源,调用时开销大
- 他们很少情况下是线程安全的
- 他们的调用耗时是不确定的(not deterministic),每次调用耗时都可能不一样(标准只要求他们的执行尽可能的快速完成并返回)
- 可能会造成内存碎片问题
- 他们会使连接器的配置变的复杂
- 会使得代码很难调试
注:内存碎片化就是:当进行频繁的小块动态内存分配和释放以后,RAM中的堆内存破碎为一个个小的,不连续的内存块。当碎片化太严重后,假设某一次内存分配,他所需要的内存大于所有的分裂的碎片内存块的大小,那么这个分配申请就会失败,即便所有可用的碎片内存大小总和远大于所申请的内存大小。
早期版本的FreeRTOS使用内存池分配方案,不同大小的内存池区块在编译期间提前分配好,并由分配函数返回给调用者。后来这种方案由于效率不好而被抛弃了。
现在的FreeRTOS将内存分配放在了portable层,而并不是放在内核核心代码层。这是因为FreeRTOS认识到:不同的嵌入式系统有不同的动态内存分配方式和时间要求,所以一个单一的内存分配算法只能作为应用层的一部分,而不适合作为内核的一部分。同时把动态内存分配从内核核心代码层转移到portable层也使得应用程序的开发者可以实现他自己想要的内存分配方式,更加灵活。
内存申请释放函数
在FreeRTOS中,当FreeRTOS内核需要申请RAM空间时,它调用pvPortMalloc()而不是malloc(),同样当释放RAM空间时调用vPortFree()而不是free()。
void *pvPortMalloc( size_t xWantedSize )
void vPortFree( void *pv )
pvPortMalloc()和vPortFree()函数和C的原型与maloc()和free()一致。他们都是FreeRTOS的公开接口函数,因此可以在应用程序中直接调用。
FreeRTOS提供了5种不同方案的pvPortMalloc和vPortFree的实现,他们都会在本章详细解释。应用程序的开发者可以根据需求使用五者之一,或者自己实现。这5种实现别定义在heap_1.c,heap_2.c,heap_3.c,heap_4.c和heap_5.c。这些文件位于FreeRTOS/Source/portable/MemMang目录下。
五种内置的堆内存管理方案
- heap_1.c:heap_1是一个非常基础pvPortMalloc()函数实现,并且这本版本没有实现vPortFree()函数。因为heap_1.c的实现方案假设应用程序绝不会删除任何一个任务,或者销毁任何一个内核对象。
- heap_2.c:提供heap_2是为了保持FreeRTOS向后兼容考虑的,现今不建议使用heap_2方案。请使用heap_4替代之。
- heap_3.c:heap_3就是对传统的C标准库中的malloc()和free()的简单封装,因此堆的总大小是在连接器中配置的,configTOTAL_HEAP_SIZE配置无效。heap_3是线程安全(多任务安全)的,因为pvPortMalloc()和vPortFree()在调用时会临时暂停FreeRTOS的任务调度器。
- heap_4.c:本文将介绍。
- heap_5.c:相比heap_4,heap_5支持多个堆内存池,也就是可以使用多个不连续的堆内存空间。使用起来相对复杂。不是很常用。
heap_4方案简介
heap_4方案要定义一个全局数组ucHeap,用于当做堆内存池,其大小在FreeRTOSConfig.h中使用configTOTAL_HEAP_SIZE 来定义。heap_4方案在绝大多数情况下都优于标准库中的malloc()和free()函数。
/* Allocate the memory for the heap. */
#if( configAPPLICATION_ALLOCATED_HEAP == 1 )
/* The application writer has already defined the array used for the RTOS
heap - probably so it can be placed in a special segment or address. */
extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#else
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#endif /* configAPPLICATION_ALLOCATED_HEAP */
heap_4方案有关键特性
1、使用链表来记录所有的空闲碎片内存块。将整个堆分割成多个未使用的碎片块,使用一个链表结构来记录他们。
/* Define the linked list structure. This is used to link free blocks in order
of their memory address. */
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;
2、使用最先适合最先使用算法来分配堆内存:例如堆内存池被分为3块大小分别是5字节、200字节和100字节的小块,现在使用pvPortMalloc()来请求20字节空间,则遍历空闲块链表,找到第一个大于等于20字节的空闲块,即200字节的块,将其分割20字节出来返回其地址,并将剩余下的空间留给下次申请使用。
3、自动将堆内存池中相邻的小块堆内存块组合成一个大的堆内存块,来解决内存碎片问题。
4、可以在程序运行过程中动态创建和删除内核对象。
5、具有不确定性,但是比标准库的malloc()和free()效率高。
下面演示heap_4方案的操作流程
场景A:创建了三个任务,用不同颜色表示。每个任务的栈空间和任务控制块都在堆中
场景B:绿色代表的任务被删除了,其占用的内存也被自动回收
场景C:创建了一个队列,由于heap_4使用了最先适合最先使用算法来分配堆内存,所以队列占用的空间位于蓝色任务占用空间的后面而不是其他地方
场景D:用户使用pvPortMalloc申请了一块空间,紧跟着队列内存的后面
场景E:队列被销毁,其占用内存被自动回收
场景F:用户使用vPortFree释放了申请的内存,这块内存被回收,同时heap_4方案自动将这里的3块连续的内存碎片组合成一个大块内存
用户自己定义ucHeap内存池
默认情况下,ucHeap堆内存池是由内核帮我们定义的。我们也可以自己定义这个ucHeap数组。
任务的栈空间也是在ucHeap内存池中申请的,而heap_4.c中由内核定义的内存池数组ucHeap的存放位置是在编译期间,由链接器(linker)决定的,如果ucHeap被放到了访问速度较慢的外部SRAM,则任务的执行速度将受到不利影响。这个时候可以由开发者自己定义ucHeap内存池数组:具体做法是在FreeRTOSConfig.h中将configAPPLICATION_ALLOCATED_HEAP定义为1,然后在自己的源文件中定义ucHeap内存池数组,使用编译器扩展指令强制指定内存池数组的位置。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)