内存管理是一个系统基本组成部分,FreeRTOS 中大量使用到了内存管理,比如创建任务、信号量、队列等会自动从堆中申请内存。用户应用层代码也可以 FreeRTOS 提供的内存管理函数来申请和释放内存。
FreeRTOS 创建任务、队列、信号量等的时候有两种方法,一种是动态的申请所需的 RAM。一种是由用户自行定义所需的 RAM,这种方法也叫静态方法,使用静态方法的函数一般以“Static”结尾,比如任务创建函数xTaskCreateStatic(),使用此函数创建任务的时候需要由用户定义任务堆栈,下面不讨论这种静态方法。关于静态创建任务可以参考该链接:静态创建任务和删除
使用动态内存管理的时候 FreeRTOS 内核在创建任务、队列、信号量的时候会动态的申请RAM。
关于动态创建任务可以参考该链接:动态创建任务和删除
标准 C 库中的 malloc()和 free()也可以实现动态内存管理,但是如下原因限制了其使用:
● 在小型的嵌入式系统中效率不高。
● 会占用很多的代码空间。
● 它们不是线程安全的。
● 具有不确定性,每次执行的时间不同。
● 会导致内存碎片。
● 使链接器的配置变得复杂。
不同的嵌入式系统对于内存分配和时间要求不同,因此一个内存分配算法可以作为系统的可选选项。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 源码中,路径:FreeRTOS->Source->portable->MemMang,后面会详细讲解这 5 种方法有何区别。
内存碎片
在看 FreeRTOS 的内存分配方法之前我们先来看一下什么叫做内存碎片,看名字就知道是小块的、碎片化的内存。那么内存碎片是怎么来的呢?内存碎片是伴随着内存申请和释放而来的,如图所示。
(1)、此时内存堆还没有经过任何操作,为全新的。
(2)、此时经过第一次内存分配,一共分出去了 4 块内存块,大小分别为 80B、80B、10B 和100B。
(3)、有些应用使用完内存,进行了释放,从左往右第一个 80B 和后面的 10B 这两个内存块就是释放的内存。如果此时有个应用需要 50B 的内存,那么它可以从两个地方来获取到,一个是最前面的还没被分配过的剩余内存块,另一个就是刚刚释放出来的 80B 的内存块。但是很明显,刚刚释放出来的这个 10B 的内存块就没法用了,除非此时有另外一个应用所需要的内存小于10B。
(4)、经过很多次的申请和释放以后,内存块被不断的分割、最终导致大量很小的内存块!也就是图中 80B 和 50B 这两个内存块之间的小内存块,这些内存块由于太小导致大多数应用无法使用,这些没法使用的内存块就沦为了内存碎片!
内存碎片是内存管理算法重点解决的一个问题,否则的话会导致实际可用的内存越来越少,最终应用程序因为分配不到合适的内存而奔溃!FreeRTOS的 heap_4.c 就给我们提供了一个解决内存碎片的方法,那就是将内存碎片进行合并组成一个新的可用的大内存块。
分配方法简介
动 态 内 存 分 配 需 要 一 个 内 存 堆 , FreeRTOS 中 的 内 存 堆 为 ucHeap[ ] , 大 小 为configTOTAL_HEAP_SIZE。不管是哪种内存分配方法,它们的内存堆都为 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 ]; //需要用户自行定义内存堆
#else
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; //编译器决定
#endif
当宏 configAPPLICATION_ALLOCATED_HEAP 为 1 的时候需要用户自行定义内存堆,否则的话由编译器来决定,默认都是由编译器来决定的。如果自己定义的话就可以将内存堆定义到外部 SRAM 或者 SDRAM 中。
heap_1 实现起来就是当需要 RAM 的时候就从一个大数组(内存堆)中分一小块出来,大数组(内存堆)的容量为configTOTAL_HEAP_SIZE,上面已经说了。使用函数xPortGetFreeHeapSize()可以获取内存堆中剩余内存大小。
heap_1 特性如下:
1、适用于那些一旦创建好任务、信号量和队列就再也不会删除的应用,实际上大多数的FreeRTOS 应用都是这样的。
2、具有可确定性(执行所花费的时间大多数都是一样的),而且不会导致内存碎片。
3、代码实现和内存分配过程都非常简单,内存是从一个静态数组中分配到的,也就是适合于那些不需要动态内存分配的应用。
内存申请函数详解
heap_1 的内存申请函数 pvPortMalloc()源码如下:
void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn = NULL;
static uint8_t *pucAlignedHeap = NULL;
//确保字节对齐
#if( portBYTE_ALIGNMENT != 1 ) (1)
{
if( xWantedSize & portBYTE_ALIGNMENT_MASK ) (2)
{
//需要进行字节对齐
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize &\ (3)
portBYTE_ALIGNMENT_MASK ) );
}
}
#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)
}
(1)、是否需要进行字节对齐,宏 portBYTE_ALIGNMENT 是需要对齐的字节数,默认为 8,需要进行 8 字节对齐,也就是说参数 xWantedSize 要为 8 的倍数,如果不是的话就需要调整为8 的倍数。
(2)、参数xWantedSize与宏portBYTE_ALIGNMENT_MASK进行与运算来判断xWantedSize是否为 8 字节对齐,如果结果等于 0 就说明 xWantedSize 是 8 字节对齐的,否则的话不为 8 字节对齐,portBYTE_ALIGNMENT_MASK为0x0007。假如xWantedSize为13,那么13&0x0007=5,5大于0,所以13不为8的倍数,需要做字节对齐处理。当xWantedSize为16的时候,16&0x0007=0,所以 16 是 8 的倍数,无需字节对齐。
(3)、当 xWantedSize 不是 8 字节对齐的时候就需要调整为 8 字节对齐,调整方法就是找出大于它并且离它最近的那个 8 字节对齐的数,对于 13 来说就是 16。体现在代码中就是本行的这个公式,同样以 xWantedSize 为 13 为例,计算公式就是:
xWantedSize=13+(8-(13&0x0007))=13+(8-5)=16;
(4)、调用函数 vTaskSuspendAll()挂起任务调度器,因为申请内存过程中要做保护,不能被其他任务打断。
(5)、确保内存堆的可用起始地址也是 8 字节对齐的,内存堆 ucHeap 的起始地址是由编译器分配的,ucHeap 的起始地址不一定是 8 字节对齐的。但是我们在使用的时候肯定要使用一个8 字节对齐的起始地址,这个地址用 pucAlignedHeap 表示,同样需要用公式计算一下,公式就是本行代码,ucHeap 和 pucAlignedHeap 如图所示:
图中内存堆 ucHeap 实际起始地址为 0x200006C4,这个地址不是 8 字节对齐的,所以不能拿来使用,经过字节对齐以后可以使用的开始地址是 0x200006C8,所以pucAlignedHeap 就为 0x200006C8。
(6)、检查一下可用内存是否够分配,分配完成以后是否会产生越界(超出内存堆范围),xNextFreeByte 是个全局变量,用来保存 pucAlignedHeap 到内存堆剩余内存首地址之间的偏移值,如图所示:
(7)、如果内存够分配并且不会产生越界,那么就将申请到的内存首地址赋给 pvReturn,比如我们要申请 30 个字节(字节对齐以后实际需要申请 32 字节)的内存,申请过程如图所示:
(8)、内存申请完成以后更新一下变量 xNextFreeByte。
(9)、调用函数 xTaskResumeAll()恢复任务调度器。
(10)、宏 configUSE_MALLOC_FAILED_HOOK 为 1 的话就说明使能了内存申请失败钩子函数,因此会调用钩子函数 vApplicationMallocFailedHook(),此函数需要用户自行编写实现。
(11)、返回 pvRerurn 值,如果内存申请成功的话就是申请到的内存首地址,内存申请失败的话就返回 NULL。
内存释放函数详解
heap_1 的内存释放函数为 pvFree(),可以看一下 pvFree()的源码,如下:
void vPortFree( void *pv )
{
( void ) pv;
configASSERT( pv == NULL );
}
可以看出 vPortFree()并没有具体释放内存的过程。因此如果使用 heap_1,一旦申请内存成功就不允许释放!但是 heap_1 的内存分配过程简单,如此看来 heap_1 似乎毫无任何使用价值啊。千万不能这么想,有很多小型的应用在系统一开始就创建好任务、信号量或队列等,在程序运行的整个过程这些任务和内核对象都不会删除,那么这个时候使用 heap_1 就很合适的。
分配方法简介
heap_2提供了一个更好的分配算法,不像heap_1那样,heap_2提供了内存释放函数。 heap_2不会把释放的内存块合并成一个大块,这样有一个缺点,随着你不断的申请内存,内存堆就会被分为很多个大小不一的内存(块),也就是会导致内存碎片!heap_4 提供了空闲内存块合并的功能。
heap_2 的特性如下:
1、可以使用在那些可能会重复的删除任务、队列、信号量等的应用中,要注意有内存碎片产生!
2、如果分配和释放的内存 n 大小是随机的,那么就要慎重使用了,比如下面的示例:
● 如果一个应用动态的创建和删除任务,而且任务需要分配的堆栈大小都是一样的,那么 heap_2 就非常合适。如果任务所需的堆栈大小每次都是不同,那么 heap_2 就不适合了,因为这样会导致内存碎片产生,最终导致任务分配不到合适的堆栈!不过 heap_4 就很适合这种场景了。
● 如果一个应用中所使用的队列存储区域每次都不同,那么 heap_2 就不适合了,和上面一样,此时可以使用 heap_4。
● 应用需要调用 pvPortMalloc()和 vPortFree()来申请和释放内存,而不是通过其他FreeRTOS 的其他 API 函数来间接的调用,这种情况下 heap_2 不适合。
3、如果应用中的任务、队列、信号量和互斥信号量具有不可预料性(如所需的内存大小不能确定,每次所需的内存都不相同,或者说大多数情况下所需的内存都是不同的)的话可能会导致内存碎片。虽然这是小概率事件,但是还是要引起我们的注意!
4、具有不可确定性,但是也远比标准 C 中的 mallo()和 free()效率高!
heap_2 基本上可以适用于大多数的需要动态分配内存的工程中,而 heap_4 更是具有将内存碎片合并成一个大的空闲内存块(就是内存碎片回收)的功能。
20.4.2 内存块详解同 heap_1 一样,大小为 configTOTAL_HEAP_SIZE。可以通过函数 xPortGetFreeHeapSize()来获取剩余的内存大小。为了实现内存释放,heap_2 引入了内存块的概念,每分出去的一段内存就是一个内存块,剩下的空闲内存也是一个内存块,内存块大小不定。为了管理内存块又引入了一个链表结构,链表结构如下:
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK *pxNextFreeBlock; //指向链表中下一个空闲内存块
size_t xBlockSize; //当前空闲内存块大小
} BlockLink_t;
每个内存块前面都会有一个 BlockLink_t 类型的变量来描述此内存块,比如我们现在申请了一个 16 个字节的内存块,那么此内存块结构就如图所示:
图中内存块的总大小是 24 字节,虽然我们只申请了 16 个字节,但是还需要另外8 字节来保存 BlockLink_t 类型的结构体变量,xBlockSize 记录的是整个内存块的大小。为了方便管理,可用的内存块会被全部组织在一个链表内,局部静态变量 xStart, xEnd 用来记录这个链表的头和尾,这两个变量定义如下:
static BlockLink_t xStart, xEnd;
内存堆初始化函数详解
static void prvHeapInit( void )
{
BlockLink_t *pxFirstFreeBlock;
uint8_t *pucAlignedHeap;
//确保内存堆的开始地址是字节对齐的
pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE )\ (1)
&ucHeap[ portBYTE_ALIGNMENT ] ) & \
( ~( ( portPOINTER_SIZE_TYPE )\
portBYTE_ALIGNMENT_MASK ) ) );
//xStart 指向空闲内存块链表首。
xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap; (2)
xStart.xBlockSize = ( size_t ) 0;
//xEnd 指向空闲内存块链表尾。
xEnd.xBlockSize = configADJUSTED_HEAP_SIZE; (3)
xEnd.pxNextFreeBlock = NULL;
//刚开始只有一个空闲内存块,空闲内存块的总大小就是可用的内存堆大小。
pxFirstFreeBlock = ( void * ) pucAlignedHeap; (4)
pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE;
pxFirstFreeBlock->pxNextFreeBlock = &xEnd;
}
(1)、同 heap_1 一样,确保内存堆的可用起始地址为 8 字节对齐。
(2)、初始化 xStart 变量。
(3)、初始化 xEnd 变量。
(4)、每个内存块前面都会保存一个 BlockLink_t 类型的结构体变量,这个结构体变量用来描述此内存块的大小和下一个空闲内存块的地址。初始化以后的内存堆如图所示:
内存块插入函数详解
heap_2 允 许 内 存 释 放 , 释 放 的 内 存 肯 定 是 要 添 加 到 空 闲 内 存 链 表 中 的 , 宏prvInsertBlockIntoFreeList()用来完成内存块的插入操作,宏定义如下:
#define prvInsertBlockIntoFreeList( pxBlockToInsert )
{
BlockLink_t *pxIterator;
size_t xBlockSize;
xBlockSize = pxBlockToInsert->xBlockSize;
//遍历链表,查找插入点
for( pxIterator = &xStart; pxIterator->pxNextFreeBlock->xBlockSize < xBlockSize; (1)
pxIterator = pxIterator->pxNextFreeBlock )
{
//不做任何事情
}
//将内存块插入到插入点
pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock; (2)
pxIterator->pxNextFreeBlock = pxBlockToInsert;
}
(1)、寻找内存块的插入点,内存块是按照内存大小从小到大连接起来的,因为只是用来寻找插入点的,所以 for 循环体内没有任何代码。
(2)、找到内存插入点以后就将内存块插入到链表中。假如我们现在需要将大小为 80 字节的内存块插入到链表中,过程如图所示:
内存申请函数详解
heap_2 的内存申请函数源码如下:
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
static BaseType_t xHeapHasBeenInitialised = pdFALSE;
void *pvReturn = NULL;
vTaskSuspendAll();
{
//如果是第一次申请内存的话需要初始化内存堆
if( xHeapHasBeenInitialised == pdFALSE ) (1)
{
prvHeapInit();
xHeapHasBeenInitialised = pdTRUE;
}
//内存大小字节对齐,实际申请的内存大小还要加上结构体BlockLink_t 的大小
if( xWantedSize > 0 ) (2)
{
xWantedSize += heapSTRUCT_SIZE; (3)
//xWantedSize 做字节对齐处理
if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0 )
{
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize &
portBYTE_ALIGNMENT_MASK ) );
}
}
//所申请的内存大小合理,进行内存分配。
if( ( xWantedSize > 0 ) && ( xWantedSize < configADJUSTED_HEAP_SIZE ) )
{
//从 xStart(最小内存块)开始,查找大小满足所需要内存的内存块。
pxPreviousBlock = &xStart;
pxBlock = xStart.pxNextFreeBlock;
while( ( pxBlock->xBlockSize < xWantedSize ) &&\ (4)
( pxBlock->pxNextFreeBlock != NULL ) )
{
pxPreviousBlock = pxBlock;
pxBlock = pxBlock->pxNextFreeBlock;
}
if( pxBlock != &xEnd ) (5)
{
//返回申请到的内存首地址
pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) +\(6)
heapSTRUCT_SIZE );
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock; (7)
if( ( pxBlock->xBlockSize - xWantedSize ) >\ (8)
heapMINIMUM_BLOCK_SIZE )
{
pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
pxBlock->xBlockSize = xWantedSize;
prvInsertBlockIntoFreeList( ( pxNewBlockLink ) ); (9)
}
xFreeBytesRemaining -= pxBlock->xBlockSize; (10)
}
}
traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll();
#if( configUSE_MALLOC_FAILED_HOOK == 1 ) (11)
{
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
}
#endif
return pvReturn;
}
(1)、如果是第一次调用函数 pvPortMalloc()申请内存的话就需要先初始化一次内存堆。
(2)、所申请的内存大小进行字节对齐。
(3)、实际申请的内存大小需要再加上结构体 BlockLink_t 的大小,因为每个内存块都会保存一个 BlockLink_t 类型变量,BlockLink_t 结构体的大小为 heapSTRUCT_SIZE。
(4)、从空闲内存链表头 xStart 开始,查找满足所需内存大小的内存块,pxPreviousBlock 所指向的下一个内存块就是找到的可用内存块。
(5)、找到的可用内存块不能是链表尾 xEnd!
(6)、找到内存块以后就将可用内存首地址保存在 pvReturn 中,函数返回的时候返回此值,这个内存首地址要跳过结构体 BlockLink_t,如图所示:
(7)、内存块已经被申请了,所以需要将这个内存块从空闲内存块链表中移除。
(8)、存在这样一种情况(不考虑结构体 BlockLink_t 的大小),我需要申请 100 个字节的内存,但是经过上面几步我得到了一个 1K 字节的内存块,实际使用中我只需要 100 个字节,剩下的 900 个字节就浪费掉了。这个明显是不合理的,所以需要判断,如果申请到的实际内存减去所需的内存大小(xBlockSize-xWantedSize)大于某个阈值的时候就把多余出来的内存重新组合成一个新的可用空闲内存块。这个阈值由宏heapMINIMUM_BLOCK_SIZE 来设置,这个阈值要大于 heapSTRUCT_SIZE。
(9)、将新的空闲内存块插入到空闲内存块链表中。
(10)、更新全局变量 xFreeBytesRemaining,此变量用来保存内存堆剩余内存大小。
(11)、如果使能了钩子函数的话就调用钩子函数vApplicationMallocFailedHook()。
内存释放函数详解
内存释放函数 vPortFree()的源码如下:
void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;
if( pv != NULL )
{
puc -= heapSTRUCT_SIZE; (1)
pxLink = ( void * ) puc; (2)
vTaskSuspendAll();
{
//将内存块添加到空闲内存块链表中
prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) ); (3)
xFreeBytesRemaining += pxLink->xBlockSize; (4)
traceFREE( pv, pxLink->xBlockSize );
}
( void ) xTaskResumeAll();
}
}
(1)、puc 为要释放的内存首地址,这个地址就是图 20.3.2.4 中 pvReturn 所指向的地址。所以必须减去 heapSTRUCT_SIZE 才是要释放的内存段所在内存块的首地址。
(2)、防止编译器报错。
(3)、将内存块添加到空闲内存块列表中。
(4)、更新变量 xFreeBytesRemaining。内存释放函数 vPortFree()还是很简单的,主要目的就是将需要释放的内存所在的内存块添加到空闲内存块链表中。
这个分配方法是对标准 C 中的函数 malloc()和 free()的简单封装,FreeRTOS 对这两个函数做了线程保护,两个函数的源码如下:
void *pvPortMalloc( size_t xWantedSize )
{
void *pvReturn;
vTaskSuspendAll(); (1)
{
pvReturn = malloc( xWantedSize ); (2)
traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll(); (3)
#if( configUSE_MALLOC_FAILED_HOOK == 1 )
{
if( pvReturn == NULL )
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
}
#endif
return pvReturn;
}
void vPortFree( void *pv )
{
if( pv )
{
vTaskSuspendAll(); (4)
{
free( pv ); (5)
traceFREE( pv, 0 );
}
( void ) xTaskResumeAll(); (6)
}
}
(1)和(4)、挂起任务调度器,为 malloc()和 free()提供线程保护
(2)、调用函数 malloc()来申请内存。
(3)和(6)、恢复任务调度器。
(5)、调用函数 free()释放内存。
heap_3 的特性如下:
1、需要编译器提供一个内存堆,编译器库要提供 malloc()和 free()函数。比如使用 STM32的话可以通过修改启动文件中的 Heap_Size 来修改内存堆的大小,如图所示:
2、具有不确定性
3、可能会增加代码量。
注意,在 heap_3 中 configTOTAL_HEAP_SIZE 是没用的!
分配方法简介
heap_4 提供了一个最优的匹配算法,不像 heap_2,heap_4 会将内存碎片合并成一个大的可用内存块,它提供了内存块合并算法。内存堆为ucHeap[],大小同样为configTOTAL_HEAP_SIZE。可以通过函数 xPortGetFreeHeapSize()来获取剩余的内存大小。
heap_4 特性如下:
1、可以用在那些需要重复创建和删除任务、队列、信号量和互斥信号量等的应用中。
2、不会像 heap_2 那样产生严重的内存碎片,即使分配的内存大小是随机的。
3、具有不确定性,但是远比 C 标准库中的 malloc()和 free()效率高。
heap_4 非常适合于那些需要直接调用函数 pvPortMalloc()和 vPortFree()来申请和释放内存的应用,注意,我们移植 FreeRTOS 的时候就选择的 heap_4!heap_4 也使用链表结构来管理空闲内存块,链表结构体与 heap_2 一样。heap_4 也定义了两个局部静态变量 xStart 和 pxEnd 来表示链表头和尾,其中 pxEnd 是指向 BlockLink_t 的指针。
内存堆初始化函数详解
内存初始化函数 prvHeapInit()源码如下:
static void prvHeapInit( void )
{
BlockLink_t *pxFirstFreeBlock;
uint8_t *pucAlignedHeap;
size_t uxAddress;
size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;
//起始地址做字节对齐处理
uxAddress = ( size_t ) ucHeap;
if( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 ) (1)
{
uxAddress += ( portBYTE_ALIGNMENT - 1 );
uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
xTotalHeapSize -= uxAddress - ( size_t ) ucHeap; (2)
}
pucAlignedHeap = ( uint8_t * ) uxAddress; (3)
//xStart 为空闲链表头。
xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap; (4)
xStart.xBlockSize = ( size_t ) 0;
//pxEnd 为空闲内存块列表尾,并且将其放到到内存堆的末尾
uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize; (5)
uxAddress -= xHeapStructSize;
uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
pxEnd = ( void * ) uxAddress;
pxEnd->xBlockSize = 0;
pxEnd->pxNextFreeBlock = NULL;
//开始的时候将内存堆整个可用空间看成一个空闲内存块。
pxFirstFreeBlock = ( void * ) pucAlignedHeap; (6)
pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;
pxFirstFreeBlock->pxNextFreeBlock = pxEnd;
//只有一个内存块,而且这个内存块拥有内存堆的整个可用空间
xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize; (7)
xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );(8)
}
(1)、可用内存堆起始地址做字节对齐处理。
(2)、可用起始地址做字节对齐处理以后难免会有几个字节被抛弃掉,被抛弃的这几个字节
不能使用,因此内存堆总的可用大小需要重新计算一下。
(3)、pucAlignedHeap 为内存堆字节对齐以后的可用起始地址。
(4)、初始化 xStart,xStart 为可用内存块链表头。
(5)、初始化 pxEnd,pxEnd 为可用内存块链表尾,pxEnd 放到了内存堆末尾。
(6)、同 heap_2 一样,内存块前面会有一个 BlockLink_t 类型的变量来描述内存块,这里是
完成这个变量初始化的。
(7) 、 xMinimumEverFreeBytesRemaining 记 录 最 小 的 那 个 空 闲 内 存 块 大 小 ,
xFreeBytesRemaining 表示内存堆剩余大小。
(8)、初始化静态变量 xBlockAllocatedBit,初始化完成以后此变量值为 0X80000000,此变量
是size_t类型的,其实就是将size_t类型变量的最高位置1,对于32位MCU来说就是0X80000000。
此变量会用来标记某个内存块是被使用,BlockLink_t 中的成员变量 xBlockSize 是用来描述内存
块大小的,在 heap_4 中其最高位表示此内存块是否被使用,如果为 1 的话就表示被使用了,所
以在 heap_4 中一个内存块最大只能为 0x7FFFFFFF。
假设内存堆 ucHeap 的大小为 46KB,即 configTOTAL_HEAP_SIZE =46*1024,ucHeap 的起
始地址为 0X200006D4,经过函数 prvHeapInit()初始化以后的内存堆如图所示:
内存块插入函数详解
内存块插入函数 prvInsertBlockIntoFreeList()用来将某个内存块插入到空闲内存块链表中,函数源码如下:
static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert )
{
BlockLink_t *pxIterator;
uint8_t *puc;
//遍历空闲内存块链表,找出内存块插入点,内存块按照地址从低到高连接在一起
for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert;\ (1)
pxIterator = pxIterator->pxNextFreeBlock )
{
//不做任何处理
}
//插入内存块,如果要插入的内存块可以和前一个内存块合并的话就
//合并两个内存块
puc = ( uint8_t * ) pxIterator;
if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert ) (2)
{
pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
pxBlockToInsert = pxIterator;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//检查是否可以和后面的内存块合并,可以的话就合并
puc = ( uint8_t * ) pxBlockToInsert;.
if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock ) (3)
{
if( pxIterator->pxNextFreeBlock != pxEnd )
{
//将两个内存块组合成一个大的内存块
pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;
pxBlockToInsert->pxNextFreeBlock =\
pxIterator->pxNextFreeBlock->pxNextFreeBlock;
}
else
{
pxBlockToInsert->pxNextFreeBlock = pxEnd;
}
}
else
{
pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock; (4)
}
if( pxIterator != pxBlockToInsert )
{
pxIterator->pxNextFreeBlock = pxBlockToInsert;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
(1)、遍历空闲内存块链表,找出当前内存块插入点,内存块是按照地址从低到高的顺序链接在一起的。
(2)、找到插入点以后判断是否可以和要插入的内存块合并,如果可以的话就合并在一起。如图所示:
在上图中,右边椭圆圈起来的就是要插入的内存块,其起始地址为 0x20009040,该地址刚好和内存块 Block2 的末地址一样,所以这两个内存块可以合并在一起。合并以后 Block2的大小 xBlockSize 要更新为最新的内存块大小,即 64+80=144。
(3)、再接着检查(2)中合并的新内存块是否可以和下一个内存块合并,也就是 Block3,如果可以的话就再次合并,合并完成以后如图所示:
在图中可以看出,最终新插入的内存块和 Blokc2、Block3 合并成一个大小为64+80+80=224 字节的大内存块,这个就是 heap_4 解决内存碎片的方法!
(4)、如果不能和 Block3 合并的话就将这两个内存块链接起来。
(5)、pxIterator 不等于 pxBlockToInsert 就意味着在内存块插入的过程中没有进行过一次内存合并,这样的话就使用最普通的处理方法。pxIterator 所指向的内存块在前,pxBlockToInsert所指向的内存块在后,将两个内存块链接起来。
内存申请函数详解
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;
vTaskSuspendAll();
{
//第一次调用,初始化内存堆
if( pxEnd == NULL ) (1)
{
prvHeapInit();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//需要申请的内存块大小的最高位不能为 1,因为最高位用来表示内存块有没有被使用
if( ( xWantedSize & xBlockAllocatedBit ) == 0 ) (2)
{
if( xWantedSize > 0 ) (3)
{
xWantedSize += xHeapStructSize;
if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
{
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize &\
portBYTE_ALIGNMENT_MASK ) );
configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
{
//从 xStart(内存块最小)开始,查找大小满足所需要内存的内存块。
pxPreviousBlock = &xStart;
pxBlock = xStart.pxNextFreeBlock;
while( ( pxBlock->xBlockSize < xWantedSize ) &&\ (4)
( pxBlock->pxNextFreeBlock != NULL ) )
{
pxPreviousBlock = pxBlock;
pxBlock = pxBlock->pxNextFreeBlock;
}
//如果找到的内存块是 pxEnd 的话就表示没有内存可以分配
if( pxBlock != pxEnd ) (5)
{
pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->\ (6)
pxNextFreeBlock ) + xHeapStructSize );
//将申请到的内存块从空闲内存链表中移除
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock; (7)
//如果申请到的内存块大于所需的内存,就将其分成两块
if( ( pxBlock->xBlockSize - xWantedSize ) >\ (8)
heapMINIMUM_BLOCK_SIZE )
{
pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
pxBlock->xBlockSize = xWantedSize;
prvInsertBlockIntoFreeList( pxNewBlockLink ); (9)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
xFreeBytesRemaining -= pxBlock->xBlockSize; (10)
if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
{
xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//内存块申请成功,标记此内存块已经被时候
pxBlock->xBlockSize |= xBlockAllocatedBit; (11)
pxBlock->pxNextFreeBlock = NULL;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll();
#if( configUSE_MALLOC_FAILED_HOOK == 1 )
{
if( pvReturn == NULL )
{
//调用钩子函数
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
configASSERT( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 );
return pvReturn;
}
(1)、pxEnd 为 NULL,说明内存堆还没初始化,所以需要调用函数 prvHeapInit()初始化内存堆。
(2)、BlockLink_t 中的变量 xBlockSize 是来描述内存块大小的,其最高位用来记录内存块有没有被使用,所以申请的内存块大小最高位不能为 1。
(3)、实际所需申请的内存数要加上结构体 BlockLink_t 的大小,因为内存块前面需要保存一个 BlockLink_t 类型的变量。最后还需要对最终的大小做字节对齐处理。这里有个疑问,假如xWantedSize 为 0x7FFFFFFF,那么 xWantedSize 加上结构体 BlockLink_t 的大小就是0x7FFFFFFF+8=0x80000007,在做一次 8 字节对齐 xWantedSize 就是 0x80000008,其最高位为1。前面已经说了,BlockLink_t 中的变量 xBlockSize 的最高位是用来标记内存块是否被使用的,这里明显冲突了,但是 FreeRTOS 对此并没有做处理。
(4)、从空闲内存链表头 xStart 开始,查找满足所需内存大小的内存块,pxPreviousBlock 的下一个内存块就是找到的可用内存块。
(5)、找到的可用内存块不能是链表尾 pxEnd!
(6)、找到内存块以后就将内存首地址保存在 pvReturn 中,函数返回的时候返回此值。
(7)、内存块已经被申请了,所以需要将这个内存块从空闲内存块链表中移除。
(8)、申请到的内存块大于所需的大小,因此要把多余出来的内存重新组合成一个新的可用空闲内存块。
(9)、将新的空闲内存块插入到空闲内存块链表中。
(10)、更新全局变量 xFreeBytesRemaining 和 xMinimumEverFreeBytesRemaining。
(11)、xBlockSize 与 xBlockAllocatedBit 进行或运算,也就是将 xBlockSize 的最高位置 1,表示此内存块被使用。
内存释放函数详解
内存释放函数源码如下:
void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;
if( pv != NULL )
{
puc -= xHeapStructSize; (1)
pxLink = ( void * ) puc; //防止编译器报错
configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );
configASSERT( pxLink->pxNextFreeBlock == NULL );
if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 ) (2)
{
if( pxLink->pxNextFreeBlock == NULL )
{
pxLink->xBlockSize &= ~xBlockAllocatedBit; (3)
vTaskSuspendAll();
{
xFreeBytesRemaining += pxLink->xBlockSize;
traceFREE( pv, pxLink->xBlockSize );
prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) ); (4)
}
( void ) xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
(1)、获取内存块的 BlockLink_t 类型结构体。
(2)、要释放的内存块肯定是被使用了的,没有被使用的空闲内存块肯定没有释放这一说。
这里通过判断 xBlockSize 的最高位是否等于 0 来得知要释放的内存块是否被应用使用。前面已经说了 BlockLink_t 中成员变量 xBlockSize 的最高位用来表示此内存块有没有被使用。
(3)、xBlockSize 的最高位清零,重新标记此内存块没有使用。xBlockSize 也表示内存块大小,就跟在 pvPortMalloc()函数里面分析的一样,如果最终申请的内存大小是 0x80000008,那么在经过这一行代码处理之后这个大小就变成了 0x00000008。所以一定要保证在使用函数pvPortMalloc()申请内存的时候经过字节对齐等处理以后,最后申请大小不能超过 0x7FFFFFFF。
(4)、将内存块插到空闲内存链表中。
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 ()来对内存堆做初始化处理,在 vPortDefineHeapRegions()未执行完之前禁止调用任何可能会调用pvPortMalloc()的 API 函数!比如创建任务、信号量、队列等函数。函数 vPortDefineHeapRegions()只有一个参数,参数是一个 HeapRegion_t 类型的数组,HeapRegion 为一个结构体,此结构体在portable.h 中有定义,定义如下:
typedef struct HeapRegion
{
uint8_t *pucStartAddress; //内存块的起始地址
size_t xSizeInBytes; //内存段大小
} HeapRegion_t;
上面说了,heap_5 允许内存堆跨越多个不连续的内存段,这些不连续的内存段就是由结构体 HeapRegion_t 来定义的。比如以 STM32F103 开发板为例,现在有连个内存段:内部 SRAM、
外部 SRAM,起始分别为:0X20000000、0x68000000,大小分别为:64KB、1MB,那么数组就
如下:
HeapRegion_t xHeapRegions[] =
{
{ ( uint8_t * ) 0X20000000UL, 0x10000 },//内部 SRAM 内存,起始地址 0X20000000,
//大小为 64KB
{ ( uint8_t * ) 0X68000000UL, 0x100000},//外部 SRAM 内存,起始地址 0x68000000,
//大小为 1MB
{ NULL, 0 } //数组结尾
};
注意,数组中成员顺序按照地址从低到高的顺序排列,而且最后一个成员必须使用 NULL。heap_5 允许内存堆不连续,说白了就是允许有多个内存堆。在 heap_2 和 heap_4 中只有一个内存堆,初始化的时候只也只需要处理一个内存堆。 heap_5 有多个内存堆,这些内存堆会被连接在一起,和空闲内存块链表类似,这个处理过程由函数 vPortDefineHeapRegions()完成。
使用 heap_5 的时候在一开始就应该先调用函数 vPortDefineHeapRegions()完成内存堆的初始化!然后才能创建任务、信号量这些东西,如下示例代码:
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //设置系统中断优先级分组 4
delay_init(); //延时函数初始化
uart_init(115200); //初始化串口
LED_Init(); //初始化 LED
KEY_Init(); //初始化按键
BEEP_Init(); //初始化蜂鸣器
LCD_Init(); //初始化 LCD
my_mem_init(SRAMIN); //初始化内部内存池
//使用 heap_5 的时候在开启任务调度器、创建任务、创建信号量之前一定要先
//调用函数 vPortDefineHeapRegions()初始化内存堆!
vPortDefineHeapRegions((const HeapRegion_t *)xHeapRegions);
//创建开始任务
xTaskCreate((TaskFunction_t )start_task, //任务函数
(const char* )"start_task", //任务名称
(uint16_t )START_STK_SIZE, //任务堆栈大小
(void* )NULL, //传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, //任务优先级
(TaskHandle_t* )&StartTask_Handler); //任务句柄
vTaskStartScheduler(); //开启任务调度
}
heap_5 的内存申请和释放函数和 heap_4 基本一样,这里就不详细讲解了,大家可以对照着前面 heap_4 的相关内容来自行分析。至此,FreeRTOS 官方提供的 5 种内存分配方法已经讲完了,heap_1 最简单,但是只能申请内存,不能释放。heap_2 提供了内存释放函数,用户代码也可以直接调用函数 pvPortMalloc()和vPortFree()来申请和释放内存,但是 heap_2 会导致内存碎片的产生!heap_3 是对标准 C 库中的函数 malloc()和 free()的简单封装,并且提供了线程保护。heap_4 相对与 heap_2 提供了内存合并功能,可以降低内存碎片的产生,我们移植 FreeRTOS 的时候就选择了 heap_4。heap_5 基本上和 heap_4 一样,只是 heap_5 支持内存堆使用不连续的内存块。
后面会补充C语言的内存知识,感兴趣的可以关注一下!