一、FreeRTOS内存管理简介
在使用 FreeRTOS 创建任务、队列、信号量等对象时,通常都有动态创建和静态创建的方式。动态方式提供了更灵活的内存管理,而静态方式则更注重内存的静态分配和控制。
如果是动态创建的,那么标准 C 库 malloc() 和 free() 函数有时可用于此目的,但是有以下缺点:
- 它们在嵌入式系统上并不总是可用。
- 它们占用了宝贵的代码空间。
- 它们不是线程安全的。
- 它们不是确定性的 (执行函数所需时间将因调用而异)。
- ...
所以更多的时候需要的不是一个替代的内存分配实现。一个嵌入式/实时系统的 RAM 和定时要求可能与另一个非常不同,所以单一的 RAM 分配算法将永远只适用于一个应用程序子集。为了避免此问题,FreeRTOS 将内存分配 API 保留在其可移植层,提供了五种内存管理算法:
- heap_1:最简单,不允许释放内存。
- heap_2:允许释放内存,但不会合并相邻的空闲块。
- heap_3:简单包装了标准 malloc() 和 free(),以保证线程安全。
- heap_4:合并相邻的空闲块以避免碎片化。包含绝对地址放置选项。
- heap_5:如同 heap_4,能够跨越多个不相邻内存区域的堆。
二、FreeRTOS内存管理算法
heap_1算法
-
heap_1 是最简单的实现方式。内存一经分配,它不允许内存再被释放。尽管如此,heap_1.c 还是适用于大量嵌入式应用程序。这是因为许多小型和深度嵌入的应用程序在系统启动时创建了所需的所有任务、队列、信号量等,并在程序的生命周期内使用所有这些对象(直到应用程序再次关闭或重新启动)。任何内容都不会被删除。
heap_2算法
heap_2 使用最佳适应算法,并且与方案 1 不同,它允许释放先前分配的块,它不将相邻的空闲块组合成一个大块。
heap_2.c 适用于许多必须动态创建对象的小型实时系统 。
- 如果动态地创建和删除任务,且分配给正在创建任务的堆栈大小总是相同的,那么 heap2.c 可以在大多数情况下使用。
- 但是,如果分配给正在创建任务的堆栈的大小不是总相同,那么可用的空闲内存可能会被碎片化成许多小块,最终导致分配失败。
heap_2 使用最佳适应算法,该算法在空闲内存中选择与请求的内存大小最接近的块来分配内存。下面是一个简单的例子来说明最佳适应算法:
假设有一个空闲内存,其中包含以下块:
- 大小为 20 字节的空闲块。
- 大小为 15 字节的空闲块。
- 大小为 25 字节的空闲块。
现在有一个任务请求分配 18 字节的内存。最佳适应算法将选择大小为 20 字节的块,因为它与请求的大小最接近。在选择这个块后,分配器可能会将该块分割为两部分,一部分大小为 18 字节,用于任务的内存,另一部分大小为 2 字节,留作未分配的块。
heap_3算法
heap_3使用 C 库的 malloc 和 free 函数来进行内存分配和释放。它通过分配固定大小的块来管理内存,这些块的大小在配置 FreeRTOS 时进行定义,不会动态改变。
假设我们使用 Heap_3 管理内存,其中块的大小固定为 32 字节。初始时,整个内存被分割成大小为 32 字节的块:
- 块 1(32 字节)。
- 块 2(32 字节)。
- 块 3(32 字节)。
现在,有一个任务请求分配 20 字节的内存。Heap_3 算法将选择块 1,并将其分割成两部分:
- 分配给任务的内存块(20 字节)。
- 剩余未分配的块(12 字节)。
再假设另一个任务请求分配 40 字节的内存。由于没有足够大的块可供分配,heap_3 将返回分配失败的状态。
heap_3 的特点是块大小固定,这样可以简化内存管理。然而,也因为块大小不可变,可能导致内存碎片问题,即一些块可能无法完全被利用,从而浪费了一些内存。
heap_4算法
heap_4使用第一适应算法,并且会将相邻的空闲内存块合并成大内存块,减少内存碎片。
第一适应算法会在可用内存块中选择第一个足够大的内存块进行分配。
假设有一个内存块链表,其中包含以下顺序的内存块:
- 大小为 40 字节的块。
- 大小为 30 字节的块。
- 大小为 15 字节的块。
- 大小为 20 字节的块。
如果一个任务需要申请 25 字节的内存,第一适应算法将选择大小为 40 字节的块,因为它是第一个足够大以容纳任务需求的内存块。(如果是heap_2的最佳适应算法,会选择30字节的块)
heap_5算法
heap_5使用与 heap_4 相同的第一适应和内存合并算法,允许堆跨越多个不相邻(非连续)内存区域。适用于内存地址不连续的复杂场景。
三、FreeRTOS内存管理相关API函数介绍
函数 | 描述 |
void * pvPortMalloc( size_t xWantedSize ); | 申请内存 |
void vPortFree( void * pv ); | 释放内存 |
size_t xPortGetFreeHeapSize( void ); | 获取当前空闲内存的大小 |
四、FreeRTOS内存管理实验
任务名 | 任务功能描述 |
start_task | 用于创建其他任务 |
task1 | 扫描按键,并作相应的按键解释 |
代码
#include "freertos_demo.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
/*FreeRTOS*********************************************************************************************/
#include "FreeRTOS.h"
#include "task.h"
/******************************************************************************************************/
/*FreeRTOS配置*/
/* START_TASK 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
#define START_TASK_PRIO 1 /* 任务优先级 */
#define START_STK_SIZE 128 /* 任务堆栈大小 */
TaskHandle_t StartTask_Handler; /* 任务句柄 */
void start_task(void *pvParameters); /* 任务函数 */
/* TASK1 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
#define TASK1_PRIO 2 /* 任务优先级 */
#define TASK1_STK_SIZE 128 /* 任务堆栈大小 */
TaskHandle_t Task1Task_Handler; /* 任务句柄 */
void task1(void *pvParameters); /* 任务函数 */
/******************************************************************************************************/
/**
* @brief FreeRTOS例程入口函数
* @param 无
* @retval 无
*/
void freertos_demo(void)
{
lcd_show_string(10, 10, 220, 32, 32, "STM32", RED);
lcd_show_string(10, 47, 220, 24, 24, "Mem Manage", RED);
lcd_show_string(10, 76, 220, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 118, 200, 16, 16, "Total Mem: Bytes", RED);
lcd_show_string(30, 139, 200, 16, 16, "Free Mem: Bytes", RED);
lcd_show_string(30, 160, 200, 16, 16, "Malloc Addr:", RED);
xTaskCreate((TaskFunction_t )start_task, /* 任务函数 */
(const char* )"start_task", /* 任务名称 */
(uint16_t )START_STK_SIZE, /* 任务堆栈大小 */
(void* )NULL, /* 传入给任务函数的参数 */
(UBaseType_t )START_TASK_PRIO, /* 任务优先级 */
(TaskHandle_t* )&StartTask_Handler); /* 任务句柄 */
vTaskStartScheduler();
}
/**
* @brief start_task
* @param pvParameters : 传入参数(未用到)
* @retval 无
*/
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); /* 进入临界区 */
/* 创建任务1 */
xTaskCreate((TaskFunction_t )task1,
(const char* )"task1",
(uint16_t )TASK1_STK_SIZE,
(void* )NULL,
(UBaseType_t )TASK1_PRIO,
(TaskHandle_t* )&Task1Task_Handler);
vTaskDelete(StartTask_Handler); /* 删除开始任务 */
taskEXIT_CRITICAL(); /* 退出临界区 */
}
/**
* @brief task1
* @param pvParameters : 传入参数(未用到)
* @retval 无
*/
void task1(void *pvParameters)
{
uint8_t key = 0;
uint8_t *buf = NULL;
size_t free_size = 0;
while (1)
{
key = key_scan(0);
switch (key)
{
case KEY0_PRES: /* 申请内存和使用内存 */
{
buf = pvPortMalloc(30);
sprintf((char *)buf, "0x%p", buf);
lcd_show_string(130, 160, 200, 16, 16, (char *)buf, BLUE);
break;
}
case KEY1_PRES: /* 释放内存 */
{
if (NULL != buf)
{
vPortFree(buf);
buf = NULL;
}
break;
}
default:
{
break;
}
}
lcd_show_xnum(114, 118, configTOTAL_HEAP_SIZE, 5, 16, 0, BLUE); /* 显示总内存大小 */
free_size = xPortGetFreeHeapSize(); /* 获取内存剩余大小 */
lcd_show_xnum(114, 139, free_size, 5, 16, 0, BLUE); /* 显示剩余内存大小 */
vTaskDelay(10);
}
}
效果
可以看到,LCD 上显示了用于动态内存内存管理的总内存大小为 1KBytes,由于内存对齐、内存块结构体占用、系统启动等因素,已经使用了一部分内存,此时还剩余 7664Bytes 的可分配内存空间。
接着按下按键 0,动态地从内存堆中申请 30Bytes 内存,LCD 显示的内容如下所示:
首先可以看到,LCD 上显示了申请到内存的首地址,这说明申请到内存的读取和写入都没有问题,因此成功地申请到了内存。
接着按下按键 1,释放刚刚申请的内存,LCD 显示的内容如下所示:
释放完内存后,可以看到内存堆中剩余的可分配内存又变回了 7664Bytes,说明之前申请到的内存被成功释放。
内存管理机制为用户提供了灵活的管理内存方法,用户可以在程序运行过程中,根据需求申请和释放内存,但是这也就要求用户对申请的内存进行管理。对于程序中动态申请的内存,在程序执行完毕后需要进行内存释放,将不用的内存释放回内存堆中。如果没有释放不用且动态申请的内存,将导致内存泄漏,这也是使用内存内存管理的问题之一。因此在一般情况下,临时申请内存时,申请和释放内存的函数都是成对出现的,除非保证申请到的内存,需要一直使用,这样才能尽可能地避免内存泄露问题的发生。