文章目录
- 前言
- 一、内存管理
- 1.1 内存管理的引入
- 1.2 内存碎片
- 二、内存分配的方法
- 2.1 heap_1
- 2.1.1 实现原理
- 2.1.2 源码解析
- 2.2 heap_2 内存分配方法
- 2.2.1 实现原理
- 2.2.2 源码解析
- 2.3 heap_3 内存分配方法
- 2.4 heap_4 内存分配方法
- 2.4.1 实现原理
- 2.4.2 源码解析
- 2.5 heap_5 内存分配方法
前言
本章是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 )
{
( void ) pv;
configASSERT( pv == NULL );
}
这说明了heap_1适用于在系统一开始就创建好任务、信号量或队列等,在程序运行的整个过程这些任务和内核对象都不会删除。
2.2 heap_2 内存分配方法
heap_2提供了一个更好的分配算法,不像heap_1那样,heap_2提供了内存释放函数。heap_2不会把释放的内存块合并成一个大块,这样有一个缺点,随着你不断的申请内存,内存堆就被分为很多个大小不一的内存(块),也就是会导致内存碎片!
为了实现内存释放,heap_2 引入了内存块的概念,每分出去的一段内存就是一个内存块,剩下的空闲内存也是一个内存块,内存块大小不定,每个内存块前面都会有一个 BlockLink_t 类型的变量来描述此内存块。为了管理内存块又引入了一个链表结构,链表结构如下:
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK *pxNextFreeBlock; // 指向链表中下一个空闲内存块
size_t xBlockSize; // 当前空闲内存块大小
} BlockLink_t;
值得注意的是,例如我们只申请了 16 个字节内存,但是还需要另外为BlockLink_t 类型的结构体变量申请8字节,xBlockSize 记录的是整个内存块的大小(24个字节)。
2.2.1 实现原理
- heap_2.c 使用链表管理空闲内存块,每个内存块的前面都有一个头部,存储该块的大小和状态(已分配或空闲)。
- 当请求内存时,遍历空闲内存链表,找到第一个足够大的块进行分配。
- 当释放内存时,将空闲块合并回链表中,以减少碎片化。
2.2.2 源码解析
内存申请函数 vPortFree()的源码如下:
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;
// 进入临界区,暂停任务调度
vTaskSuspendAll();
// 确保 xWantedSize 是 8 字节对齐
if( xWantedSize & portBYTE_ALIGNMENT_MASK )
{
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
}
// 增加头部大小:为了管理内存块,增加A_BLOCK_LINK的大小
xWantedSize += xHeapStructSize;
// 遍历空闲链表,找到合适的块
pxPreviousBlock = &xStart;
pxBlock = xStart.pxNextFreeBlock;
while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
{
pxPreviousBlock = pxBlock;
pxBlock = pxBlock->pxNextFreeBlock;
}
// 找到的可用内存块不能是链表尾 xEnd
if( pxBlock != pxEnd )
{
// 找到内存块后就将可用内存首地址保存在 pvReturn 中,函数返回的时候返回此值
// 这个内存首地址要跳过结构体 BlockLink_t
pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
}
xTaskResumeAll(); // 退出临界区,恢复任务调度
return pvReturn;
}
内存释放函数 vPortFree()还是很简单的,主要目的就是将需要释放的内存所在的内存块,其源码如下:
void vPortFree( void *pv )
{
// puc 为要释放的内存首地址
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;
if( pv != NULL )
{
// 指向申请函数中pvReturn 所指向的地址
puc -= heapSTRUCT_SIZE;
pxLink = ( void * ) puc;
vTaskSuspendAll();
{
// 将内存块添加到空闲内存块链表中
prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
// 更新变量 xFreeBytesRemaining
xFreeBytesRemaining += pxLink->xBlockSize;
traceFREE( pv, pxLink->xBlockSize );
}
( void ) xTaskResumeAll();
}
}
2.3 heap_3 内存分配方法
这个分配方法是对标准 C 中的函数 malloc()和 free()的简单封装,FreeRTOS 对这两个函数做了线程保护,这里就简单解释一下,不再赘述了。heap_3 它通过封装这些标准函数,保证 FreeRTOS 兼容多任务调度环境中的内存分配和释放需求。vTaskSuspendAll() 和 xTaskResumeAll() 确保了在分配和释放内存时,不会出现任务切换导致的竞态条件。
pvPortMalloc( )调用标准的 malloc() 函数分配内存,并通过 vTaskSuspendAll() 暂停任务调度。
void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn;
vTaskSuspendAll();
{
pvReturn = malloc( xWantedSize );
}
xTaskResumeAll();
return pvReturn;
}
vPortFree()调用标准的 free() 函数释放内存,并通过 vTaskSuspendAll() 暂停任务调度。
void vPortFree( void *pv )
{
if( pv != NULL )
{
vTaskSuspendAll();
{
free( pv );
}
xTaskResumeAll();
}
}
2.4 heap_4 内存分配方法
这种方法是我们学习的重中之重,heap_4 提供了一个最优的匹配算法,它会将内存碎片合并成一个大的可用内存块。这意味着它可以用在那些需要重复创建和删除任务、队列、信号量和互斥信号量等的应用中。虽然具有不确定性,但是远比 C 标准库中的 malloc()和 free()效率高。
2.4.1 实现原理
- heap_4.c 实现了类似 heap_2.c 的内存分配策略,即利用A_BLOCK_LINK 结构体来管理内存块,但在其基础上增加了空闲内存块的合并功能。
- 每当内存块被释放时,会尝试将它与相邻的空闲块合并,以减少内存碎片化的发生。
- 采用了“最佳适配”算法来分配内存,优先使用最适合的空闲块,进一步降低碎片化。
2.4.2 源码解析
heap_4 的内存申请函数源码如下:
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;
// 临界区开始
vTaskSuspendAll();
// 最小块大小检查
if( xWantedSize > 0 )
{
xWantedSize += heapSTRUCT_SIZE;
// 8 字节对齐
if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
{
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
}
}
// 查找合适的块
pxPreviousBlock = &xStart;
pxBlock = xStart.pxNextFreeBlock;
// 从空闲内存链表头 xStart 开始,查找满足所需内存大小的内存块
// pxPreviousBlock 的下一个内存块就是找到的可用内存块
while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
{
pxPreviousBlock = pxBlock;
pxBlock = pxBlock->pxNextFreeBlock;
}
// 分配内存
if( pxBlock != pxEnd )
{
pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + heapSTRUCT_SIZE );
// 将申请到的内存块从空闲内存链表中移除
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
// 如果申请到的内存块大于所需的内存,要把多余的内存重新组合成一个新的可用内存块
if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
{
// 创建新的块
pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
pxBlock->xBlockSize = xWantedSize;
pxNewBlockLink->pxNextFreeBlock = pxPreviousBlock->pxNextFreeBlock;
pxPreviousBlock->pxNextFreeBlock = pxNewBlockLink;
}
// 减少剩余的可用内存,并更新系统曾经最少剩余的内存量
xFreeBytesRemaining -= pxBlock->xBlockSize;
if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
{
xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
}
}
xTaskResumeAll(); // 临界区结束
return pvReturn;
}
内存释放函数源码如下:
void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;
if( pv != NULL )
{
// 获取内存块的 BlockLink_t 类型结构体
puc -= heapSTRUCT_SIZE;
pxLink = ( void * ) puc;
vTaskSuspendAll();
// 更新增加的内存
xFreeBytesRemaining += pxLink->xBlockSize;
if( ( ( uint8_t * ) pxLink + pxLink->xBlockSize ) == ( uint8_t * ) pxLink->pxNextFreeBlock )
{
pxLink->xBlockSize += pxLink->pxNextFreeBlock->xBlockSize;
pxLink->pxNextFreeBlock = pxLink->pxNextFreeBlock->pxNextFreeBlock;
}
// 合并前面的块
pxLink->pxNextFreeBlock = xStart.pxNextFreeBlock;
xStart.pxNextFreeBlock = pxLink;
xTaskResumeAll();
}
}
2.5 heap_5 内存分配方法
heap_5 使用了和 heap_4 相同的合并算法,内存管理实现起来基本相同,但是 heap_5 允许内存堆跨越多个不连续的内存段。比如 STM32 的内部 RAM 可以作为内存堆,但是 STM32 内部 RAM 比较小,遇到那些需要大容量 RAM 的应用就不行了,如音视频处理。不过 STM32 可以外接 SRAM 甚至大容量的 SDRAM,如果使用 heap_4 的话你就只能在内部 RAM 和外部SRAM 或 SDRAM 之间二选一了,使用 heap_5 的话就不存在这个问题,两个都可以一起作为内存堆来用。
如果使用 heap_5 的话,在调用 API 函数之前需要先调用函数vPortDefineHeapRegions ()来对内存堆做初始化处理,在其未执行完之前禁止调用任何可能会调用pvPortMalloc()的 API 函数。函数 vPortDefineHeapRegions()
只有一个参数,参数是一个 HeapRegion_t 类型的数组,HeapRegion 为一个结构体,此结构体在portable.h 中有定义,定义如下:
typedef struct HeapRegion
{
uint8_t *pucStartAddress; //内存块的起始地址
size_t xSizeInBytes; //内存段大小
} HeapRegion_t;
使用 heap_5 的时候在一开始就应该先调用函数 vPortDefineHeapRegions()完成内存堆的初始化!然后才能创建任务、信号量这些东西。heap_5 的内存申请和释放函数和 heap_4 基本一样,这里就不详细讲解了,大家可以对照着前面 heap_4 的相关内容来自行分析。
免责声明:本文参考了网上公开资料,仅用于学习交流,若有错误或侵权请联系笔者。