FreeRTOS内存管理
1. FreeRTOS 内存管理简介
静态方法创建任务、队列、信号量等对象的 API 函数一般是以“Static”结尾的,例如静态创建任务的 API 函数xTaskCreateStatic()。使用静态方式创建各种对象时,需要用户提供各种内存空间,例如任务的栈空间、任务控制块所用内存空间等等,并且使用静态方式占用的内存空间一般固定下来了,
即使任务、队列等被删除后,这些被占用的内存空间也没有其他用途。
在使用动态方式管理内存的时候,FreeRTOS 就能够在创建任务、队列、信号量等对象的时候,自动地从 FreeRTOS 管理的内存堆中申请所创建对象所需的内存,在
对象被删除后,又可以将这块内存释放会 FreeRTOS 管理的内存堆
,这样看来,动态方式管理内存相比与静态方式,显得灵活许多。除了 FreeRTOS 提供的动态内存管理方法,标准的 C 库也提供了函数 malloc()和函数 free() 来实现动态地申请和释放内存,但是标准 C 库的动态内存管理方法有如下几个缺点:
- 并不适用于所有嵌入式系统。
- 占用大量的代码空间。
- 没有线程安全的相关机制。
- 具有不确定性,体现在每次执行的时间不同。
- ……
FreeRTOS 一共提供了 5 种动态内存管理算法,这 5 种动态内存管理算法本别对应了 5 个C 源文件,分别为:heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c
文件 原理 特点 heap_1.c
仅支持单向分配,无释放功能。内存一经分配即永久占用,适用于确定性要求高且无需动态删除对象的系统(如安全关键型嵌入式系统) 实现简单、无碎片风险,但灵活性极低 heap_2.c
允许申请和释放内存,但不能合并相邻的空闲内存块。适用于重复创建和删除相同大小任务的场景 长期运行后碎片化严重,已逐渐被heap_4.c取代 heap_3.c
封装标准库的malloc和free,通过挂起调度器(vTaskSuspendAll())保证线程安全。堆大小由链接器决定,不受configTOTAL_HEAP_SIZE限制 分配速度较慢且不确定性高,适用于需要兼容现有代码的场景 heap_4.c
允许申请和释放内存,并且能够合并相邻的空闲内存块,减少内存碎片的产生。支持动态分配不同大小的内存块,是通用型应用的首选方案 分配效率高于标准库,且能通过xPortGetMinimumEverFreeHeapSize()监控内存使用情况 heap_5.c
能够管理多个非连续内存区域的 heap_4(如同时使用内部RAM和外部SRAM)。需通过vPortDefineHeapRegions()显式初始化多个内存区域 复杂硬件环境下的内存扩展需求
2. heap_1 内存管理算法
/* 此宏用于定义 FreeRTOS 内存堆的定义方式 */ #if ( configAPPLICATION_ALLOCATED_HEAP == 1 ) extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; /* 用户自定义一个大数组作为 FreeRTOS 管理的内存堆 */ #else static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; /* 定义一个大数组作为 FreeRTOS 管理的内存堆 */ #endif void * pvPortMalloc( size_t xWantedSize ) { void * pvReturn = NULL; static uint8_t * pucAlignedHeap = NULL; /* 确保申请的内存大小按照 portBYTE_ALIGNMENT 字节对齐 * 如果审定的内存大小没有按照 portBYTE_ALIGNMENT 字节对齐, * 则会加大申请的内存大小,是指按 portBYTE_ALIGNMENT 字节对齐 */ #if ( portBYTE_ALIGNMENT != 1 ) { if( xWantedSize & portBYTE_ALIGNMENT_MASK ) { if ((xWantedSize +(portBYTE_ALIGNMENT - (xWantedSize & portBYTE_ALIGNMENT_MASK))) > xWantedSize ) { xWantedSize +=(portBYTE_ALIGNMENT - (xWantedSize & portBYTE_ALIGNMENT_MASK)); } else { xWantedSize = 0; } } } #endif /* 挂起任务调度器 */ vTaskSuspendAll(); { if( pucAlignedHeap == NULL ) { /* 确保内存堆的起始地址按照 portBYTE_ALIGNMENT 字节对齐 */ pucAlignedHeap = ( uint8_t * )(((portPOINTER_SIZE_TYPE)&ucHeap[portBYTE_ALIGNMENT-1]) & (~((portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK))); } /* 申请的内存大小需大于 0 * 检查内存堆中是否有足够的空间 */ if( ( xWantedSize > 0 ) && ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) && ( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) ) { /* 计算申请到内存的起始地址 * 内存堆的对齐地址+内存堆已分配的大小 */ pvReturn = pucAlignedHeap + xNextFreeByte; /* 更新内存堆已分配的大小 */ xNextFreeByte += xWantedSize; } /* 用于调式,不用理会 */ traceMALLOC( pvReturn, xWantedSize ); } ( void ) xTaskResumeAll(); /* 此宏用于开启动态内存申请失败钩子函数 */ #if ( configUSE_MALLOC_FAILED_HOOK == 1 ) { /* 动态内存申请失败 */ if( pvReturn == NULL ) { extern void vApplicationMallocFailedHook( void ); /* 调用动态内存申请失败钩子函数 */ vApplicationMallocFailedHook(); } } #endif /* 返回申请到内存的首地址 */ return pvReturn; } void vPortFree( void * pv ) { /* 没有实现释放内存的功能 */ ( void ) pv; configASSERT( pv == NULL ); }
heap_1 内存管理算法管理的内存堆是一个数组,在申请内存的时候,heap_1 内存管理算法只是简单地从数组中分出合适大小的内存。
heap_1 内存管理算法的内存释放函数并没有实现,因此使用heap_1 内存管理算法申请的内存,是无法释放的。
heap_1 内存管理算法的申请内存函数的实现非常简单,就是从内存堆的低地址开始往高地址分配内存,内存堆利用率是非常高的,除了内存堆起始地址的位置可能会因地址对齐产生一小块无用内存外,内存堆中其余的内存空间都可以用来分配,并且也不会产生内存碎片。内存堆的结构示意图如下:
heap_1 内存管理算法具有如下特性:
- 适用于一旦创建好任务、队列、信号量等就不会删除的应用,实际上大多数的 FreeRTOS 应用都是这样的。
- 具有确定性,体现在每次执行的时间都是一样的,而且不会产生内存碎片。
- 实现的方式非常简单,分配的内存都是从一个静态分配的数组中分配的,因此也意味着并不适用于那些真正需要动态申请和释放内存的应用。
3. heap_2 内存管理算法
相比于 heap_1 内存管理算法,heap_2 内存管理算法使用了
最适应算法
,以支持释放先前申请的内存,但是 heap_2 内存管理算法并不能将相邻的空闲内存块合并成一个大的空闲内存块,因此 heap_2 内存管理算法不可避免地会产生内存碎片。内存碎片是由于多次申请和释放内存,但释放的内存无法与相邻的空闲内存合并而产生的,具体的产生过程,如下图所示:
- heap_2 内存管理算法的内存堆与 heap_1 内存管理算法的内存堆一样,都是一个数组。
- 可以在 FreeRTOSConfig.h 文件中配置 configTOTAL_HEAP_SIZE 配置项,以配置内存堆的字节大小,同样地,也可以用过 configAPPLICATION_ALLOCATED_HEAP 配置项将内存堆定义在指定的内存地址中。
- 用户可以通过函数 xPortGetFreeHeapSize()获取内存堆中未分配的内存总量,并根据系统运行时内存堆中剩余内存的大小,针对性地对 configTOTAL_HEAP_SIZE 配置项进行优化配置。
heap_2 内存管理算法申请内存的过程,大致如下:
- 因为空闲块链表中的空闲内存块是按照内存块的大小从小到大排序的,因此从头开始遍历空闲块链表,找到第一个大小适合的空闲内存块。
- 找到大小适合的空闲内存块后,由于找到的空闲内存块可能比需要申请的内存大,因此需要将整个内存块分为两个小的内存块,其中一个内存块的大小就是需要申请内存的大小,另一个小内存块作为空闲内存块重新插入空闲块链表。
3.heap_2 内存管理算法的释放函数很简单,就是将带释放的内存块插入到空闲块链表中。
4. heap_3 内存管理算法
heap_3 内存管理算法本质使用的是调用标准 C 库提供的内存管理函数,标准 C 库的内存管理需要链接器设置好一个堆,这个堆将作为内存管理的内存堆使用,在启动文件中可以配置这个堆的大小,如下所示:
heap_3 内存管理算法具有如下特性:
- 需要链接器提供一个堆,还需要编译器的库提供用于申请内存的函数 malloc()和用于释放内存的函数 free()。
- 具有不确定性。
- 有可能会大大地增减编译后的代码量。
5. heap_4 内存管理算法
heap_4 内存管理算法使用了首次
适应算法
,与 heap_2 内存管理算法一样,heap_4 内存管理算法也支持内存的申请与释放,并且 heap_4 内存管理算法还能够将空闲且相邻的内存进行合并,从而减少内存碎片的现象。
- heap_4 内存管理算法的内存堆与 heap_1、heap_2 内存管理算法的内存堆一样,都是一个数组
- heap_4 内存管理算法中定义的内存堆与 heap_1、heap_2 内存管理算法一样,可以在FreeRTOSConfig.h 文件中配置 configTOTAL_HEAP_SIZE 配置项,以配置内存堆的字节大小,同样地,也可以用过 configAPPLICATION_ALLOCATED_HEAP 配置项将内存堆定义在指定的内存地址中。
- 用户可以通过函数 xPortGetFreeHeapSize()获取内存堆中未分配的内存总量,根据系统运行时内存堆中剩余的内存空间大小,就可以针对性地对 configTOTAL_HEAP_SIZE 配置项进行优化配置。
- 与 heap_2 内存管理算法不同的是,heap_4内存管理算法中空闲块链表中的内存块并不是按照内存块大小的顺序从小到大排序,而是按照空闲块链表中内存块的起始地址大小从小到大排序,这也是为了后续往空闲块链表中插入内存块时,能够将相邻的内存块合并。
- heap_4 内存管理算法整体与 heap_2 内存管理算法很相似,但是 heap_4 内存管理算法相较于 heap_2 内存管理算法能够将物理内存空间上相邻的两个空闲内存块合并成一个大的空闲内存块,而这正是在将空闲内存块插入空闲块链表的时候实现的。
- 与 heap_2 内存管理算法将空闲块链表中的空闲内存块按照内存块的内存大小从小到大排序的方式不同,heap_4 内存管理算法是将空闲内存块链表中的空闲内存块按照内存块在物理内存上的起始地址从低到高进行排序的,也正是因此,才能够更加方便地找出物理内存地址相邻的空闲内存块,并将其进行合并。
- 将空闲内存块插入空闲块链表之前,会先从头开始遍历空闲块链表,按照内存块在物理内存上起始地址从低到高的排序规则,找到空闲块要插入的位置。接着判断待插入空闲内存块的起始地址或结束地址是否分别与该位置前面内存块的结束地址或该位置后面内存块的起始地址相同,如果相同侧表示待插入的空闲内存块在物理地址上与该位置前面的内存块或该位置后面的内存块相邻,那么就将响铃的两个空闲内存块合并成一个大的内存块,再将这个大的内存块插入到空闲块链表中,这个操作的示意图如下所示(以待插入空闲内存块与找到位置的上一个内存块相邻为例)
heap_4 内存管理算法具有如下特性:
- 适用于在程序中多次创建和删除任务、队列、信号量等的应用。
- 与 heap_2 内存管理算法相比,即使多次分配和释放随机大小的内存,产生内存碎片的几率也要小得多。
- 具有不确定性,但是执行的效率比标准 C 库的内存管理高得多。
6. heap_5 内存管理算法
- heap_5 内存管理算法是在 heap_4 内存管理算法的基础上实现的,因为 heap_5 内存管理算法使用与 heap_4 内存管理算法相同的内存分配、释放和合并算法,但是 heap_5 内存管理算法在 heap_4 内存管理算法的基础上实现了管理多个非连续内存区域的能力。
- heap_5 内 存 管 理 算 法 默 认 并 没 有 定 义 内 存 堆 , 需 要 用 户 手 动 调 用 函 数vPortDefindHeapRegions(),并传入作为内存堆的内存区域的信息,对其进行初始化。初始化后的内存堆将被作为空闲内存块链接到空闲块链表中,再接下来的内存申请与释放就和 heap_4 内存管理算法一致了。
- 要注意的是,因为 heap_5 内存管理算法并不会自动创建好内存堆,因此需要用户手动为 heap_5 初始化好作为内存堆的内存区域后,才能够动态创建任务、队列、信号量等对象。