RTOS内核需要使用 RAM 来为每次创建任务、队列、互斥量、信号量、事件组、软件定时器分配内存,内存分配可以由 freeRTOS 的API动态自动从堆上创建,也可以由开发者自己分配。
如果RTOS对象是动态创建的,标准C库中的malloc()
和free()
函数有时可以达到目的,但是:
- 在某些嵌入式系统中可能不可用
- 标准库函数占用代码空间
- 不是线程安全的
- 不是精确的,会导致总体执行时间降低
以上问题导致,使用标准C库中的malloc()
和free()
不太合适。
嵌入式/实时系统存在多种多样的RAM和时序,因此,单一的RAM 分配算法不可能满足所有的应用。
为了解决这个问题,freeRTOS将内存分配API放在了适配层,这意味着,可以做到跟具体处理器及其架构相关,开发者可以根据实际情况实现适合自己系统的内存分配算法。当RTOS内核需要RAM时,调用适配的pvPortMalloc()
来获取分配的内存,使用pvPortFree()
释放内存。
freeRTOS提供集中堆内存管理机制,用以管理堆内存,当然,开发者也可以实现自己的堆内存管理机制,甚至是同时使用两种堆内存管理机制,例如,使用两种内存管理机制,将任务等RTOS对象放在速度较快的内部RAM中,如51单片机的idate
。而将用户的应用程序数据放在速度较慢的外部RAM,如51的xdate
。
freeRTOS源代码中的内存分配机制实现
在freeRTOS的源代码里,提供了5个示例内存分配机制,开发者可以根据这5个分配机制的不同特点选择最适合自己应用的。
这5个内存分配实现源代码位于路径Source/Portable/MemMang
下的五个文件中(heap_x.c , x = 1 , 2 , 3 , 4 , 5
),这5个文件在应用工程中只能包含一个,RTOS的内核将使用这5个文件提供的分配机制管理内存。这5个文件特点如下:
heap_1.c
最简单的,不允许释放已分配的内存heap_2.c
允许释放已分配内存,但是不允许合并相邻的空块heap_3.c
malloc()
和free()
的线程安全版本heap_4.c
合并相邻的空快,避免内存碎片,包括绝对地址定位选项heap_5.c
在heap_4.c
的基础上,允许堆内存跨越多个不同的RAM段
heap_1.c
这是最简单的一种实现机制,不允许释放已经分配的内存。鉴于这种特性,heap_1.c
适合具有很大内存的嵌入式系统,因为所有的内存分配在整个系统的生命周期内都是保持不变的,永远不会被释放。
实现方式很简单,通过分割一个数组给所需要的变量,将原来数组占用的RAM分配给变量。这个数组的大小通过confifTOTAL_HEAP_SIZE
设置,使用configAPPLICATION_ALLOCATED_HEAP
设置这个数组在RAM中的地址。
在开发过程中,可以使用xPortGetFreeHeapSize()
API函数返回空闲的数组内存大小,开发者可以根据这个返回值确定系统实际所需堆内存大小,从而优化configTOTAL_HEAP_SIZE
的大小。
heap_1.c
特点:
- 如果你的应用不会删除任务、队列等RTOS对象,则使用这个机制是合适的
- 总是精确的(总是花费相同的时间去执行),并且不会导致内存碎片
- 从一个已经静态分配好的数组中分配内存是很简单的,这意味着,这很适合那些不允许真正的动态内存分配的系统
heap_2.c
相比heap_1.c
,允许释放以及分配的内存,但是不会合并相邻的空块,这意味着会造成内存碎片。
堆内存的大小通过confifTOTAL_HEAP_SIZE
设置,使用configAPPLICATION_ALLOCATED_HEAP
设置这个堆在RAM中的地址。
在开发过程中,可以使用xPortGetFreeHeapSize()
API函数返回空闲的数组内存大小,开发者可以根据这个返回值确定系统实际所需堆内存大小,从而优化configTOTAL_HEAP_SIZE
的大小。但是不会提供堆内存中的碎片信息。
特点:
- 可以使用在那些需要删除任务、队列等的系统,但是,会产生内存碎片
- 不应该使用在需要随机分配和释放内存的应用中。经常随机分配和释放内存会导致内存中的碎片逐渐变多,即使空闲内存足够多,但是因为内存碎片,可能仍然会导致内存分配失败
- 会导致内存碎片,而且碎片分布无法预测
- 不精确,但是比标准C库中的库函数高效
因此,heap_2.c适合用在那些每次分配和释放内存大小都是固定的应用中。
heap_3.c
malloc()
和free()
的线程安全版本,大多数情况下,需要编译器支持。
特点:
- 需要链接器设置堆,以及由编译器库文件提供
malloc()
和free()
的实现 - 不精确
- 会增加代码空间占用
注意:configTOTAL_HEAP_SIZE
设置在使用heap_3.c
时无效。
heap_4.c
相比heap_2.c
,实现了碎片整理的功能,会合并相邻的空块。
堆内存的大小通过confifTOTAL_HEAP_SIZE
设置,使用configAPPLICATION_ALLOCATED_HEAP
设置这个堆在RAM中的地址。
在开发过程中,可以使用xPortGetFreeHeapSize()
API函数返回空闲的数组内存大小,开发者可以根据这个返回值确定系统实际所需堆内存大小,从而优化configTOTAL_HEAP_SIZE
的大小。
特点:
- 可以使用在经常删除任务、队列等的应用中
- 即使分配和释放的内存大小都是随机的,也不会造成很多内存碎片
- 不精确,但是比标准C库函数高效
heap_4.c
在那些需要在应用中直接使用适配层的内存分配机制,而不是调用API函数的应用中尤其有用。
heap_5.c
在heap_4.c
的基础上,增加对堆内存跨越多个RAM段的支持。
heap_5 使用vPortDefineHeapRegions()
完成初始化,在初始化完成之前,堆内存不可用。在创建RTOS对象(队列、信号量等)时,会隐式的调用pvPortMalloc()
。因此,初始化对于使用heap_5是很重要的。
vPortDefineHeapRegions
接收一个参数,这个参数是个heapRegion_t
类型的结构体数组,heapRegion_t
定义在portable.h
文件中:
typedef struct HeapRegion
{
/* 作为堆内存的一部分的RAM的开始地址.*/
uint8_t *pucStartAddress;
/* RAM大小. */
size_t xSizeInBytes;
} HeapRegion_t;
堆内存使用一个零大小的空区域作为结束,并且内存范围的定义必须是按照从低地址到高地址的方式安排。下面的例子展示了这个机制:
/* 堆内存由两块RAM组成,第三部分为结束标志 */
const HeapRegion_t xHeapRegions[] =
{
{ ( uint8_t * ) 0x80000000UL, 0x10000 },
{ ( uint8_t * ) 0x90000000UL, 0xa0000 },
{ NULL, 0 } /* 数组结束. */
};
/* 传递结构体数组到初始化函数. */
vPortDefineHeapRegions( xHeapRegions );
同样,可以使用xPortGetFreeHeapSize()
获取可用堆内存大小,并据此优化堆内存大小。