内存管理是一个系统基本组成部分,FreeRTOS 中大量使用到了内存管理,比如创建任务、信号量、队列等会自动从堆中申请内存,用户应用层代码也可以 FreeRTOS 提供的内存管理函数来申请和释放内存
FreeRTOS 内存管理简介
FreeRTOS 创建任务、队列、信号量等的时候有两种方法,一种是动态的申请所需的 RAM。一种是由用户自行定义所需的 RAM,这种方法也叫静态方法
不同的嵌入式系统对于内存分配和时间要求不同,因此一个内存分配算法可以作为系统的可选选项。FreeRTOS 将内存分配作为移植层的一部分,这样 FreeRTOS 使用者就可以使用自己的合适的内存分配方法。
当内核需要 RAM 的时候可以使用 pvPortMalloc()来替代 malloc()申请内存,不使用内存的时候可以使用 vPortFree()函数来替代 free()函数释放内存。函数 pvPortMalloc()、vPortFree()与函数 malloc()、free()的函数原型类似
FreeRTOS 提供了 5 种内存分配方法,FreeRTOS 使用者可以其中的某一个方法,或者自己的内存分配方法。这 5 种方法是 5 个文件,分为:heap_1.c、heap_2.c、heap_3.c、heap_4.c 和heap_5.c。这 5 个文件在FreeRTOS 源码中
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
学习内存分配方法之前我们先来看一下什么叫做内存碎片
有些应用使用完内存,进行了释放,从左往右第一个 80B 和后面的 10B 这两个内存块就是释放的内存。如果此时有个应用需要 50B 的内存,那么它可以从两个地方来获取到,一个是最前面的还没被分配过的剩余内存块,另一个就是刚刚释放出来的 80B 的内存块。但是很明显,刚刚释放出来的这个 10B 的内存块就没法用了,除非此时有另外一个应用所需要的内存小于 10B;
经过很多次的申请和释放以后,内存块被不断的分割、最终导致大量很小的内存块!也就是图中 80B 和 50B 这两个内存块之间的小内存块,这些内存块由于太小导致大多数应用无法使用,这些没法使用的内存块就沦为了内存碎片;
内存碎片是内存管理算法重点解决的一个问题,否则的话会导致实际可用的内存越来越少,最终应用程序因为分配不到合适的内存而奔溃!FreeRTOS 的 heap_4.c 就给我们提供了一个解决内存碎片的方法,那就是将内存碎片进行合并组成一个新的可用的大内存块
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
内存管理方法 | 简介 | 优点 | 缺点 |
---|---|---|---|
heap_1.c | 简单静态分配方式,提供单一的内存堆,分配后内存块不会被释放,内存块大小在编译时确定。 | 实现简单,占用资源少,无内存碎片问题,对于资源分配固定的简单系统可靠性高。 | 缺乏灵活性,不能动态释放内存,不适用于需要频繁分配和释放内存的复杂场景。 |
heap_2.c | 基于单向链表管理固定大小的内存块,可动态分配和释放内存。 | 对于固定大小内存块的分配和释放操作相对简单高效,适用于内存块大小固定的频繁分配和释放场景,如相同大小任务栈的管理。 | 只能处理固定大小的内存块,存在内存碎片风险,当内存块大小需求不一致时,内存利用率可能较低。 |
heap_3.c | 对标准C库的malloc() 和free() 函数进行简单包装。 | 利用标准C库的功能,易于理解和移植,在熟悉标准C库内存管理的情况下可以快速上手。 | 依赖标准C库的性能和特性,可能存在标准C库本身的内存碎片问题,对一些资源受限的嵌入式系统可能不太适用。 |
heap_4.c | 采用双向链表管理可变大小的内存块,能合并相邻空闲内存块来提高利用率,可动态分配和释放。 | 能灵活处理不同大小的内存块分配,通过合并空闲内存块提高了内存利用率,适用于复杂多变的内存分配需求。 | 实现相对复杂,占用一定的系统资源用于管理内存链表,内存分配和释放操作可能比简单的方法耗时。 |
heap_5.c | 基于heap_4.c的算法扩展到多个不连续的内存区域,可在这些区域间分配和释放内存。 | 能够有效利用分散的内存资源,适用于内存分布不连续的系统,提高了整个系统的内存可用性。 | 管理多个区域的内存增加了复杂性,对内存管理的开销进一步增大,实现和调试难度较高。 |
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
字节对齐的目的是什么?
xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
1、heap_1 内存分配方法
动 态 内 存 分 配 需 要 一 个 内 存 堆 , FreeRTOS 中 的 内 存 堆 ucHeap[] , 大 小 为configTOTAL_HEAP_SIZE,这个前面讲 FreeRTOS 配置的时候就讲过了。不管是哪种内存分配方法,它们的内存堆都为 ucHeap[] , 而且大小都是 configTOTAL_HEAP_SIZE。内存堆在文件heap_x.c (x 为 1~5) 中定义的,比如 heap_1.c 文件:
#if( configAPPLICATION_ALLOCATED_HEAP == 1 ) extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; //需要用户自行定义内存堆 //当宏 configAPPLICATION_ALLOCATED_HEAP 为 1 的时候需要//用户自行定义内存堆,否则的话由编译器来决定,默认都是由编译器//来决定的。如果自己定义的话就可以将内存堆定义//到外部 SRAM 或者 SDRAM 中
#else static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; //编译器决定
#endif
heap_1 实现起来就是当需要 RAM 的时候就从一个大数组(内存堆)中分一小块出来,大数组(内存堆)的容量为 configTOTAL_HEAP_SIZE,上面已经说了。使用函数 xPortGetFreeHeapSize()可以获取内存堆中剩余内存大小。
适用于那些一旦创建好任务、信号量和队列就再也不会删除的应用,实际上大多数的FreeRTOS 应用都是这样的,代码实现和内存分配过程都非常简单,内存是从一个静态数组中分配到的,也就是适合于那些不需要动态内存分配的应用
void *pvPortMalloc( size_t xWantedSize )
{ void *pvReturn = NULL; static uint8_t *pucAlignedHeap = NULL; //确保字节对齐 #if( portBYTE_ALIGNMENT != 1 ) //(1) 这是一个条件编译指令。只有当 portBYTE_ALIGNMENT //不等于 1 时,才会编译 #if 和 #endif 之间的代码{ if( xWantedSize & portBYTE_ALIGNMENT_MASK ) //(2) 这里使用按位与操作符 & 来检查 xWantedSize //与 portBYTE_ALIGNMENT_MASK 按位与的结果是否不为零。//如果结果不为零,说明 xWantedSize 不是 portBYTE_ALIGNMENT 的整数倍,//需要进行字节对齐{ //需要进行字节对齐 xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) ); //(3) 这一步是进行字节对齐的核心操作。xWantedSize & portBYTE_ALIGNMENT_MASK //得到 xWantedSize 对 portBYTE_ALIGNMENT 取模的结果,即当前 xWantedSize //距离下一个 portBYTE_ALIGNMENT 整数倍的差值。然后用 portBYTE_ALIGNMENT //减去这个差值,得到需要增加的字节数,最后将这个增加的字节数加到 xWantedSize 上,//从而实现字节对齐} } #endif vTaskSuspendAll(); //(4) { if( pucAlignedHeap == NULL ) { //确保内存堆的开始地址是字节对齐的 pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE )\ //(5) &ucHeap[ portBYTE_ALIGNMENT ] ) &\ ( ~( ( portPOINTER_SIZE_TYPE )\ portBYTE_ALIGNMENT_MASK ) ) ); } //检查是否有足够的内存供分配,有的话就分配内存 if( ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) && //(6) ( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) ) { pvReturn = pucAlignedHeap + xNextFreeByte; //(7) xNextFreeByte += xWantedSize; //(8) } traceMALLOC( pvReturn, xWantedSize ); } ( void ) xTaskResumeAll(); //(9) #if( configUSE_MALLOC_FAILED_HOOK == 1 ) //(10) { if( pvReturn == NULL ) { extern void vApplicationMallocFailedHook( void ); vApplicationMallocFailedHook(); } } #endif return pvReturn; //(11)
}
补充:
portBYTE_ALIGNMENT
是一个在特定编程环境(尤其是嵌入式系统或与硬件交互紧密的代码中)经常使用的宏定义或常量。它用于指定字节对齐的规则,即数据存储时按照多少字节的边界进行对齐;
指定对齐单位:它的值表示数据在内存中存储时需要对齐的字节数。例如,如果 portBYTE_ALIGNMENT 被定义为 4,意味着数据存储时会按照 4 字节的边界进行对齐。这通常是为了满足特定硬件架构对数据访问的要求,不同的硬件架构可能对数据的对齐方式有不同的规定,以提高内存访问的效率和稳定性
条件编译与对齐处理:在代码中,常通过 portBYTE_ALIGNMENT 来进行条件编译和字节对齐的逻辑判断。就像你提供的代码中,通过检查 portBYTE_ALIGNMENT 是否不等于 1 来决定是否执行后续的字节对齐操作。如果 portBYTE_ALIGNMENT 等于 1,说明不需要进行特殊的字节对齐处理,因为所有数据默认已经按 1 字节对齐;而当 portBYTE_ALIGNMENT 大于 1 时,需要对数据大小进行调整,使其满足指定的对齐要求。
portBYTE_ALIGNMENT_MASK 的作用:
portBYTE_ALIGNMENT_MASK
是一个与 portBYTE_ALIGNMENT
相关的掩码值。通常,portBYTE_ALIGNMENT
是 2 的幂次方,例如 2、4、8 等。当 portBYTE_ALIGNMENT
是 2 的幂次方时,portBYTE_ALIGNMENT_MASK
的值为 portBYTE_ALIGNMENT - 1
。
例如:
如果 portBYTE_ALIGNMENT
为 4(二进制 100),那么 portBYTE_ALIGNMENT_MASK
为 3(二进制 011)。
如果 portBYTE_ALIGNMENT
为 8(二进制 1000),那么 portBYTE_ALIGNMENT_MASK
为 7(二进制 0111)。
2、heap_2 内存分配方法
heap_2提供了一个更好的分配算法,不像heap_1那样,heap_2提供了内存释放函数。 heap_2不会把释放的内存块合并成一个大块,这样有一个缺点,随着你不断的申请内存,内存堆就会被分为很多个大小不一的内存(块),也就是会导致内存碎片
3、heap_3 内存分配方法
这个分配方法是对标准 C 中的函数 malloc()和 free()的简单封装,FreeRTOS 对这两个函数做了线程保护
4、heap_4 内存分配方法
heap_4 提供了一个最优的匹配算法,不像 heap_2,heap_4会将内存碎片合并成一个大的可用内存块,它提供了内存块合并算法。内存堆为ucHeap[ ],大小同样为configTOTAL_HEAP_SIZE。
可以通过函数xPortGetFreeHeapSize() 来获取剩余的内存大小
它采用双向链表结构来管理内存,并能够合并相邻的空闲内存块。这使得内存的利用率得到提高。在资源有限的嵌入式系统中,高效的内存利用至关重要。比如,当一个任务结束并释放其占用的内存块后,heap_4.c 可以将该内存块与相邻的空闲内存块合并为一个更大的空闲内存块,以便后续分配给需要较大内存空间的其他任务或资源。
5、heap_5内存分配算法
heap5 内存管理算法是在 heap4 内存管理算法的基础上实现的,但是 heap5 内存管理算法在 heap4 内存管理算法的基础上实现了管理多个非连续内存区域的能力。
heap_5 内存管理算法默认并没有定义内存堆,需要用户手动指定内存区域的信息,对其进行初始化。