1. FreeRTOS内存管理简介
在使用 FreeRTOS 创建任务、队列、信号量等对象的时,一般都提供了两种方法:
- 动态方法创建:自动地从 FreeRTOS 管理的内存堆中申请创建对象所需的内存,并且在对象删除后,FreeRTOS 可自动地将这块内存释放回 FreeRTOS 管理的内存堆
- 静态方法创建:由用户手动地提供各种内存空间,并且使用静态方式占用的内存空间一般固定下来了,即使任务、队列等被删除后,这些被占用的内存空间一般没有其他用途 ,我们一般是不会再去使用了。
总结:动态方式管理内存相比与静态方式,更加灵活。
除了 FreeRTOS 提供的动态内存管理方法,标准的 C 库也提供了函数 malloc() 和函数 free() 来实现动态地申请和释放内存 。
疑问:为啥不用标准的 C 库自带的内存管理算法?
因为标准 C 库的动态内存管理方法有如下几个缺点:
- 占用大量的代码空间 不适合用在资源紧缺的嵌入式系统中。
而 FreeRTOS 号称是轻量级的,它的一个内存管理算法就几百行代码,所以相较之下 FreeRTOS 的代码量更小。
- 没有线程安全的相关机制。
很明显 C 库又不是专门为你 os 而设计的,像 os,它任务与任务之间是会抢占的,那这个呢就涉及到线程安全了,而 FreeRTOS 提供的内存管理算法,那很显然它就是为它自己而设计的,它里面就考虑到这个线程安全了。在我们要申请的时候,它就会挂起任务调度器,那任务切换不了,就打断不了了。
- 运行有不确定性,每次调用这些函数时花费的时间可能都不相同。
- 内存碎片化。
… …
多次申请释放之后,可用的内存空间就越来越小,这个就是 C 库自带的这个内存管理算法会存在的一些问题。
FreeRTOS 就是因为 C 库它这个内存管理算法有这么多缺点,因此,它提供了多种动态内存管理的算法,可针对(适配)不同的嵌入式系统!
那大家就可以根据自己的一个嵌入式系统来选择适合自己的一种算法。所以还是比较人性化的。
1.1 FreeRTOS内存管理算法
FreeRTOS提供了5种动态内存管理算法,分别为: heap_1、heap_2、heap_3、heap_4、heap_5。
其实都是一个 .c 文件,heap_1.c、heap_2.c、… … 这样下去,提供了 5 个 .c 文件。那这 .c 文件就是它们内存管理算法的实现。
如下所示:
算法 | 优点 | 缺点 |
---|---|---|
heap_1 | 分配简单,时间确定 | 只允许申请内存,不允许释放内存 |
heap_2 | 允许申请和释放内存 | 不能合并相邻的空闲内存块会产生碎片、时间不定 |
heap_3 | 直接调用C库函数malloc()和 free() ,在 C 库基础上增加了线程安全,简单 | 速度慢、时间不定 |
heap_4 | 相邻空闲内存可合并,减少内存碎片的产生 | 时间不定 |
heap_5 | 能够管理多个非连续内存区域的 heap_4 | 时间不定 |
在我们 FreeRTOS 例程中,使用的均为 heap_4 内存管理算法,因为它能减少内存碎片的产生,这是非常好的一个优点。
1.1.1 heap_1内存管理算法
heap_1只实现了pvPortMalloc,没有实现vPortFree;也就是说,它只能申请内存,无法释放内存!
所以 heap_1 的算法实现是最简单的,它直接就定义一个大数组来作为内存堆,然后我们要申请内存的时候, heap_1 内存管理算法直接在这个数组中分配出去合适大小的内存就可以了。内存堆数组的定义如下所示 :
/* 定义一个大数组作为 FreeRTOS 管理的内存堆 */
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; /* configTOTAL_HEAP_SIZE:内存堆的大小,默认定义为 10k */
但这种越简单的算法,其实它越安全。
适合场景:如果你的工程,创建好的任务、队列、信号量等都不需要被删除,那么可以使用heap_1内存管理算法,又简单又安全。
heap_1 内存管理算法的分配过程如下图所示:
注意: heap_1内存管理算法,只能申请无法释放!
1.1.2 heap_2内存管理算法
- 相比于 heap_1 内存管理算法, heap_2 内存管理算法使用最适应算法,并且支持释放内存;
- heap_2 内存管理算法并不能将相邻的空闲内存块合并成一个大的空闲内存块,因此 heap_2 内存管理算法不可避免地会产生内存碎片。
最适应算法:
- 假设 heap 有 3 块空闲内存块(注意:heap2 的空闲内存块是按内存块大小由小到大排序):5字节、25字节、50字节。现在新创建一个任务需要申请20字节的内存,那这时候怎么申请呢,找哪一个块呢?
- 找出最小的、能满足 pvPortMalloc 的内存:25字节。
- 把它划分为20字节、5字节;返回这20字节的地址,剩下的5字节仍然是空闲状态,留给后续的pvPortMalloc使用
这个就是最适应算法的概念。
内存碎片:
- 由于多次申请和释放内存,但释放的内存无法与相邻的空闲内存合并而产生的。
- 示意图:
适用场景:频繁的创建和删除任务,那后面再创建的时候,跟前面创建的那个内存堆就一模一样的大小,也就是说保证每次创建的任务堆栈都相同,要申请的内存大小都相同,这类场景下就可以用 heap_2,这时候 heap_2 没有产生碎片化的问题。那不然每一次释放或者说申请的大小都不一样,那肯定会产生越来越多的内存碎片导致最后没得用。
1.1.3 heap_3内存管理算法
heap_3 的话我们直接跳过了,因为它直接用的就是 C 库。
1.1.4 heap_4内存管理算法
heap_4 内存管理算法使用了首次适应算法,也支持内存的申请与释放,并且能够将空闲且相邻的内存进行合并,从而减少内存碎片的现象。
首次适应算法:
- 假设 heap 有 3 块空闲内存(注意:heap4 的空闲内存块是按内存块地址由低到高排序):5字节、50字节、25字节。现在新创建一个任务需要申请20字节的内存,那选择哪一个呢?
- 由低地址往高地址找,找出第一个能满足 pvPortMalloc 的内存:50 字节。
- 把它划分为20字节、30字节;返回这20字节的地址,剩下30字节仍然是空闲状态,留给后续的 pvPortMalloc 使用
heap_4内存管理算法会把相邻的空闲内存合并为一个更大的空闲内存,这有助于减少内存的碎片问题。
适用于这种场景:频繁地分配、释放不同大小的内存。
1.1.5 heap_5内存管理算法
heap_5 内存管理算法是在 heap_4 内存管理算法的基础上实现的,它同样也支持 heap_4 的所有功能,然后它多一个功能:heap_5 内存管理算法在 heap_4 内存管理算法的基础上实现了管理多个非连续内存区域的能力。
能够管理多个非连续内存区域的 heap_4,也就是说每一个区域其实都用了 heap_4 内存管理算法,只不过它多了一个功能,就管理多个内存区域,因为有些嵌入式系统的内存不是连续的,那我可以分段管理,那这时候就可以用 heap_5 了。
heap_5 内存管理算法默认并没有定义内存堆 , 需要用户手动指定内存区域的信息,对其进行初始化。
问题:怎么指定一块内存?
使用如下结构体:
typedef struct HeapRegion
{
uint8_t * pucStartAddress; /* 内存区域的起始地址 */
size_t xSizeInBytes; /* 内存区域的大小,单位:字节 */
} HeapRegion_t;
怎么指定多块且不连续的内存?
Const HeapRegion_t xHeapRegions[] =
{
{ (uint8_t *)0x80000000, 0x10000 }, /* 内存区域 1 */
{ (uint8_t *)0x90000000, 0xA0000 }, /* 内存区域 2 */
{ NULL, 0 } /* 数组终止标志 */
};
vPortDefineHeapRegions(xHeapRegions);
适用场景:在嵌入式系统中,那些内存的地址并不连续的场景。heap5 可以充分的把内存给利用起来。
那大家也可以根据自己的需求去选择不同的一个内存管理算法。
2. FreeRTOS内存管理相关API函数介绍
函数 | 描述 |
---|---|
void * pvPortMalloc( size_t xWantedSize ); | 申请内存 |
void vPortFree( void * pv ); | 释放内存 |
size_t xPortGetFreeHeapSize( void ); | 获取当前空闲内存的大小 |
2.1 申请内存
函数原型:
void * pvPortMalloc( size_t xWantedSize );
xWantedSize:申请的内存大小,以字节为单位;
返回值:返回一个指针 ,指向已分配大小的内存。如果申请内存失败,则返回 NULL;如果申请内存成功,则返回分配的内存块的首地址。
2.2 释放内存
函数原型:
void vPortFree( void * pv );
*pv:指针指向一个要释放内存的内存块;
2.3 获取当前空闲内存的大小
函数原型:
size_t xPortGetFreeHeapSize( void );
返回值:返回当前剩余的空闲内存大小
3. FreeRTOS内存管理实验
实验目的:学习使用 FreeRTOS 内存管理,并观察内存在申请和释放过程中内存大小的变化情况。
实验设计:将设计两个任务:start_task、task1
两个任务的功能如下:
start_task:用来创建task1任务
task1:用于按键扫描,当KEY0按下则申请内存,当KEY1按下则释放内存,并打印剩余内存信息
3.1 任务函数实现
/* 任务一,申请内存以及释放内存,并显示空闲内存大小 */
void task1( void * pvParameters )
{
uint8_t key = 0, t = 0;
uint8_t * buf = NULL;
while(1)
{
key = key_scan(0);
if(key == KEY0_PRES)
{
buf = pvPortMalloc(30); /* 申请内存 */
if(buf != NULL)
{
printf("申请内存成功!\r\n");
}else printf("申请内存失败\r\n");
}else if(key == KEY1_PRES)
{
if(buf != NULL)
{
vPortFree(buf); /* 释放内存 */
printf("释放内存!!\r\n");
}
}
if(t++ > 50)
{
t = 0;
printf("剩余的空闲内存大小为:%d\r\n",xPortGetFreeHeapSize());
}
vTaskDelay(10);
}
}
- FreeRTOS 所管理的一个内存堆总大小是 10k,就是 10240 字节,现在只剩下 7672 字节,那被使用了两千多字节,为什么?那肯定是前面我们创建了开始任务,又创建了任务 1,然后在开始调度器里面又创建了软件定时器,还创建了空闲任务等等等等这些,那这些也是要用到内存的嘛,所以现在已经剩下 7672 字节。
那大家可以根据自己的一个硬件去定义FreeRTOS 所管理的一个内存堆总大小。
- 申请内存大小 + 内存块结构体大小(8字节) + 八字节对齐而舍弃的内存大小 = 实际申请内存大小。
- 内存泄漏:我们每一次申请这个 buf,都是通过
pvPortMalloc(30);
申请之后返回它的首地址回来,每按一次 key0,那么它就会更新新申请的一个首地址给 buf,那这时候它就申请了多次;然后我释放只带了一个 buf,那这个 buf 肯定是最新的一个 buf,然后我释放 buf 第一次肯定是可以成功的,那后面再释放,这个 buf 已经被我释放了,那还是同样的这个地址,那我再释放也没用。而我前面申请的那些 buf 已经被最后一次申请的地址覆盖,我释放不到,没用。所以这里就会导致它只能释放一次是成功的,那后面再释放是不行了,它就会报错,这种情况就会导致内存泄漏。
3.2 内存堆分配流程
-
初始化后的内存堆,如下:
-
插入新的空闲内存块,如下: