学习FreeRTOS(第四天)

FreeRTOS的内存管理

在讲述FreeRTOS的内存管理之前,先来看看这张图片

在Memory management scheme中,有五个不同的堆区,每一个堆区都有不同的特点

1 、动态创建和静态创建

动态:自动地从FreeRTOS管理的内存堆中申请创建对象所需的内存,并且在对象删除后,可将这块内存释放回FreeRTOS管理的内存堆

静态:由用户预先分配内存(通常是全局变量或静态变量),并且使用静态方式占用的内存空间一般固定下来了,即使任务、队列等被删除后,这些被占用的内存空间也没有释放

感兴趣的了解可以了解一下静态创建和动态创建对象的区别-CSDN博客

2 、函数的使用

为什么不使用malloc()和free()?主要原因与 其内存管理特性不适应嵌入式实时系统的需求有关

cpp 复制代码
1、内存分配时间不确定(非确定性)
标准malloc()和free()的实现(如基于链表的内存块管理)中,分配/释放内存的耗时与当前堆内存的碎片化
程度、已分配块的数量相关,无法提前预测执行时间。
对于实时系统(如工业控制、汽车电子),任务必须在严格的时间窗口内响应(如微秒级),
malloc() / free()的耗时波动可能导致任务超时,破坏系统实时性。

2、内存碎片问题
频繁调用malloc()和free()会导致堆内存碎片化:即堆中存在大量无法被有效利用的小内存块(碎片),最终
可能出现 "总内存充足但无法分配连续大内存块" 的情况,导致malloc()失败。
嵌入式系统通常内存资源有限(如RAM仅几十KB),碎片化问题更易爆发,且难以通过外部手段(如重启)修
复,可能导致系统长期运行后崩溃。

3、线程安全问题
标准C库的malloc()/free()并非天生线程安全(不同编译器实现不同),在多任务系统中,多个任务同时调用
时可能导致堆数据结构(如链表指针)被破坏,引发内存泄漏、程序崩溃等难以调试的问题。
虽然部分库通过加锁实现线程安全,但锁的引入会进一步增加耗时不确定性,且可能导致任务优先级反转

4、内存使用不可控
malloc()分配的内存大小由运行时参数决定,若代码中存在错误(如分配过大内存),可能瞬间耗尽堆资源,
导致其他关键任务(如系统任务)因内存不足而失败。
嵌入式系统通常需要在编译期确定内存使用上限(如静态内存分配),而malloc()的动态性使其难以在开发阶
段预估和验证内存需求。

在这里

FreeRTOS 针对嵌入式场景提供了更优的内存管理方案,避免了malloc()/free()的缺陷:

动态内存:使用内核自带的 pvPortMalloc() 和 vPortFree() ,其实现(如 heap_4.c 、 heap_5.c )采用内存块合并策略,减少碎片,且支持线程安全(通过临界区保护)。

静态内存:通过 xTaskCreateStatic() 、 xQueueCreateStatic() 等API,在编译期分配内存,完全避免动态分配,确保确定性和无碎片。

二 、 FreeRTOS内存管理算法

1、 heap_1内存管理算法

heap_1只实现了pvPortMalloc,没有实现vPortFree。也就是说,它只能申请内存,无法释放内存 。

如果你的工程,创建好的任务、队列、信号量等都不需要被删除,那么可以使用heap_1内存管理算法, heap_1的实现最为简单,管理的内存堆是一个数组,在申请内存的时候,heap_1内存管理算法只是简单地从数组中分出合适大小的内存 ,内存堆数组的定义如下所示

/* 定义一个大数组作为FreeRTOS管理的内存堆 */

static uint8_t ucHeap[configTOTAL_HEAP_SIZE];

pvPortMalloc()函数原理:就是把数组切分不同小块使用,这个数组大小为configTOTAL_HEAP_SIZE,通常创建任务的内存分配为两块:TCB和任务堆栈(之前提到过感兴趣的可以看看之前的)

cpp 复制代码
void * pvPortMalloc( size_t xWantedSize )
{
    void * pvReturn;
    vTaskSuspendAll();
    {
    pvReturn = malloc( xWantedSize );
    traceMALLOC( pvReturn, xWantedSize );
    }
    ( void ) xTaskResumeAll();
    #if ( configUSE_MALLOC_FAILED_HOOK == 1 )
    {
    if( pvReturn == NULL )
    {
    vApplicationMallocFailedHook();
    }
    }
    #endif
    return pvReturn;
}
cpp 复制代码
void vPortFree( void * pv )
{
    if( pv != NULL )
    {
    vTaskSuspendAll();
    {
    free( pv );
    traceFREE( pv, 0 );
    }
    ( void ) xTaskResumeAll();
    }
}

图A表明创建task前,整个数组是空的

图B表明创建一个task后,数组使用情况

图C表明创建3个Task后,数组使用情况的变化

2 、 heap_2内存管理算法

Heap_2也是通过configTOTAL_HEAP_SIZE来定义数组,使用了best-fit算法[最适应算法]分配内存,实现了VPortFree()函数。best-fit算法可以确保pvPortMalloc申请的空闲内存大小必须非常接近函数请求大小。因为Heap_2不能对相邻空闲块进行合并,因此缺点是会产生内存碎片,当然,如果每次申请空间都是相等,此缺点也可以忽略

最适应算法:假设heap有3块空闲内存(按内存块大小由小到大排序)︰5字节、25字节、50字节,现在新创建一个任务需要申请20字节的内存:找出最小的、能满足pvPortMalloc的内存:25字节,把它划分为20字节和5字节,返回这20字节的地址,剩下的5字节仍然是空闲状态,留给后续的pvPortMalloc使用

无法合并的会导致资源的浪费,所以最适用于创建任务堆栈都相同的场景

3、Heap_3内存管理算法

Heap_3方案只是简单的封装了标准C库中的malloc()和free()函数,并且能满足常用的编译器。 重新封装后的malloc()和free()函数具有保护功能,采用的封装方式是操作内存前挂起调度器、完成后再恢复调度器。

在使用这种模式时,FreeRTOSConfig.h文件中的configTOTAL_HEAP_SIZE宏定义不起作用。使用的是启动文件中设置的堆的大小,单位为字节

4 、 Heap_4内存管理算法

heap_4内存管理算法使用了 首次适应算法 , 也支持内存的申请与释放,并且能够将空闲且相邻的内存进行合并,从而减少内存碎片的现象。

首次适应算法:假设heap有3块空闲内存(按内存块地址由低到高排序)︰5字节、50字节、25字节,现在新创建一个任务需要申请20字节的内存,找出第一个能满足pvPortMalloc的内存:50字节,把它划分为20字节和30字节;返回这20字节的地址,剩下30字节仍然是空闲状态,留给后续的pvPortMalloc使用

相比于最适应算法,最大的不同是释放的内存可以合并

5 、 Heap_5内存管理算法

Heap_5使用与Heap_4相同的算法进行分配内存, 但是heap_5内存管理算法在heap_4内存管理算法的基础上实现了管理多个非连续内存区域的能力 ,heap_5内存管理算法默认并没有定义内存堆,需要用户手动指定内存区域的信息,并对其进行初始化。

怎么指定一块内存?------使用如下结构体:

cpp 复制代码
/* Used by heap_5.c to define the start address and size of each memory region that
together comprise the total FreeRTOS heap space. */
typedef struct HeapRegion
{
    uint8_t * pucStartAddress; // 内存区域的起始地址。
    size_t xSizeInBytes; // 内存区域的大小
} HeapRegion_t;

HeapRegion_t结构体:用户需要指定每个内存堆区域的起始地址和内存堆大小、将它们放在一个
HeapRegion_t结构体类型数组中,这个数组必须用一个 NULL指针和0作为结尾,起始地址必须从小到大排
列

怎么指定多块不连续内存?这个是嵌入式实时操作系统 FreeRTOS 中的一个特定数据结构,用于 配置和管理 "多区域堆内存"(Multiple Heap Regions)

cpp 复制代码
*
在内存中为内存堆分配两个内存块。
第一个内存块大小为0x10000字节,起始地址为0x80000000,
第二个内存块大小为0xa0000字节,起始地址为0x90000000。
起始地址为0x80000000的内存块的起始地址更低,因此放到了数组的第一个位置。
*/
const HeapRegion_t xHeapRegions[] = {
    { ( uint8_t * ) 0x80000000UL, 0x10000 },
    { ( uint8_t * ) 0x90000000UL, 0xa0000 },
    { NULL, 0 } /* 数组结尾 */
}

三 、 FreeRTOS内存管理相关API函数介绍

1. 申请内存 (pvPortMalloc)

pvPortMalloc 是 FreeRTOS 提供的动态内存分配函数,类似于标准 C 库的 malloc

cpp 复制代码
void *pvPortMalloc( size_t xSize );

示例:

cpp 复制代码
#include "FreeRTOS.h"
#include "task.h"

void vMyTask( void *pvParameters )
{
    int *piData;

    // 申请 4 个字节的内存(一个 int)
    piData = (int *)pvPortMalloc( sizeof(int) );

    if( piData != NULL )
    {
        *piData = 123; // 写入数据
        vTaskDelay( pdMS_TO_TICKS( 1000 ) ); // 延时
        vPortFree( piData ); // 释放内存
    }

    vTaskDelete( NULL ); // 删除任务
}

2. 释放内存 (vPortFree)

vPortFree 用于释放之前由 pvPortMalloc 分配的内存

cpp 复制代码
void vPortFree( void *pv );

3. 获取当前空闲内存大小

cpp 复制代码
size_t xPortGetFreeHeapSize( void );
size_t xPortGetMinimumEverFreeHeapSize( void );
cpp 复制代码
void vCheckHeap( void )
{
    size_t xFreeBytes;
    size_t xMinFreeBytes;

    xFreeBytes = xPortGetFreeHeapSize();
    xMinFreeBytes = xPortGetMinimumEverFreeHeapSize();

    printf("当前空闲内存: %u 字节\n", xFreeBytes);
    printf("历史最小空闲内存: %u 字节\n", xMinFreeBytes);
}
相关推荐
d111111111d3 小时前
STM32外设学习-I2C(细节)--学习笔记
笔记·stm32·单片机·嵌入式硬件·学习
chuwengeileyan13 小时前
stm32 光敏电阻 光控灯
stm32·单片机·嵌入式硬件
ElfBoard4 小时前
ElfBoard技术贴|如何在【RK3588】ELF 2开发板上进行UART引脚复用配置
人工智能·单片机·嵌入式硬件·物联网
就是蠢啊5 小时前
51单片机——数码管
单片机·嵌入式硬件·51单片机
别掩5 小时前
三极管恒流电路
单片机·嵌入式硬件
花落已飘5 小时前
STM32 SDIO接口介绍
stm32·单片机·嵌入式硬件
DIY机器人工房7 小时前
嵌入式面试题:了解软件SPI和软件I2C吗?说一说。
stm32·单片机·嵌入式硬件
大聪明-PLUS7 小时前
编程语言保证是安全软件开发的基础
linux·嵌入式·arm·smarc
小尧嵌入式8 小时前
基于HAL库实现F407的基本外设GPIO输入输出USART收发RTC时钟I2CEEPROM和SPIW25Q128读写及CAN通信
arm开发·单片机·嵌入式硬件