文章目录
前言
本章是Free RTOS系列的终章,我们来讲述贯穿全系列的一个核心元素——内存管理。
一、内存管理
1.1 内存管理的引入
内存管理是一个系统基本组成部分,FreeRTOS 中大量使用到了内存管理,比如创建任务、信号量、队列等会自动从堆中申请内存。用户应用层代码也可以 FreeRTOS 提供的内存管理函数来申请和释放内存。FreeRTOS 创建任务、队列、信号量等的时候有两种方法,一种是动态的申请所需的 RAM;一种是由用户自行定义所需的 RAM,这种方法也叫静态方法,使用静态方法的函数一般以“Static”结尾,比如任务创建函数 xTaskCreateStatic(),使用此函数创建任务的时候需要由用户定义任务堆栈,本章我们不讨论这种静态方法。
使用动态内存管理的时候 FreeRTOS 内核在创建任务、队列、信号量的时候会动态的申请RAM。标准 C 库中的 malloc()和 free()也可以实现动态内存管理,但是出于种种原因限制了其使用,因此一个内存分配算法可以作为系统的可选选项。FreeRTOS 将内存分配作为移植层的一部分,这样 FreeRTOS 使用者就可以使用自己的合适的内存分配方法。
动态内存分配需要一个内存堆,FreeRTOS 中的内存堆为ucHeap[] ,大小为configTOTAL_HEAP_SIZE,这个前面讲 FreeRTOS 配置的时候就讲过了。不管是哪种内存分配方法,它们的内存堆都为 ucHeap[],而且大小都是 configTOTAL_HEAP_SIZE。
1.2 内存碎片
在学习 FreeRTOS 的内存分配方法之前我们先来看一下什么叫做内存碎片,看名字就知道是小块的、碎片化的内存。内存碎片是伴随着内存申请和释放而来的,如下图所示:
可以看到经过很多次的申请和释放以后,内存块被不断的分割、最终导致大量很小的内存块!也就是图中 80B 和 50B 这两个内存块之间的小内存块,这些内存块由于太小导致大多数应用无法使用,这些没法使用的内存块就沦为了内存碎片!
内存碎片是内存管理算法重点解决的一个问题,否则的话会导致实际可用的内存越来越少,最终应用程序因为分配不到合适的内存而奔溃!FreeRTOS 的 heap_4.c 就给我们提供了一个解决内存碎片的方法,那就是将内存碎片进行合并组成一个新的可用的大内存块。
二、内存分配的方法
在Free RTOS的移植一章中,我们提到了其提供了 5 种内存分配方法这 5 种方法是 5 个文件,分别为:heap_1.c、heap_2.c、heap_3.c、heap_4.c 和heap_5.c。这部分我们FreeRTOS 使用者可以其中的某一个方法,或者自定义一个合适的分配方法。
2.1 heap_1
heap_1 实现起来就是当需要 RAM 的时候就从一个大数组(内存堆)中分一小块出来,大数组(内存堆)的容量为 configTOTAL_HEAP_SIZE,上面已经说了。使用函数xPortGetFreeHeapSize()可以获取内存堆中剩余内存大小。在heap_1.c 文件就有如下定义:
#if( configAPPLICATION_ALLOCATED_HEAP == 1 )
extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; //需要用户自行定义内存堆
#else
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; //编译器决定
#endif
当宏 configAPPLICATION_ALLOCATED_HEAP 为 1 的时候需要用户自行定义内存堆,否则的话由编译器来决定,默认都是由编译器来决定的。如果自己定义的话就可以将内存堆定义到外部 SRAM 或者 SDRAM 中。
2.1.1 实现原理
- heap_1.c 中使用了一个简单的静态数组作为堆空间,并按需从该数组中分配内存。
- 该实现非常简单,只允许内存分配,不支持内存释放,内存的释放只能在任务结束或系统重启时实现。
- 主要适用于小型嵌入式系统中,内存需求相对简单、可预测的场景。
2.1.2 源码解析
heap_1 的内存申请函数 pvPortMalloc()源码如下:
// 该函数(简化)用于动态分配内存,返回指向分配内存的指针
void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn = NULL;
// 确保 xWantedSize 是 8 字节对齐(xWantedSize是用户请求的内存大小,按照默认要求堆分配会将该大小调整为 8 字节对齐)
if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0 )
{
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
}
// 检查是否有足够的空间,如果有返回堆中当前位置 xNextFreeByte 的地址,并将 xNextFreeByte 向前移动 xWantedSize 字节;不足返回NULL
if( ( ( xNextFreeByte + xWantedSize ) < configTOTAL_HEAP_SIZE ) &&
( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) )
{
// pucAlignedHeap表示起始地址(ucHeap的起始地址不一定是8字节对齐的,需要我们利用这个参数补齐)
// 如果内存够分配并且不会产生越界,那么就将申请到的内存首地址赋给 pvReturn
// 值得注意的是如果我们要申请 30 个字节的内存,字节对齐以后实际需要申请 32 字节
pvReturn = pucAlignedHeap + xNextFreeByte;
xNextFreeByte += xWantedSize;
}
return pvReturn;
}
heap_1 的内存释放函数为 pvFree(),可以看出 vPortFree()并没有具体释放内存的过程,这说明使用一旦申请内存成功就不允许释放!
void vPortFree( void *pv )