目录

1. 概述

1.1 RTOS中内存分配特点

1.2 内存堆(heap space)来源

1.2.1 ucHeap数组

1.2.2 链接器设置的堆

1.2.3 多个非连续内存堆

1.3 关于字节对齐

1.3.1 字节对齐的目的

1.3.2 字节对齐的内容

2. heap_1内存管理实现

2.1 方案特点

2.2 内存堆定义

2.3 函数实现

2.3.1 vPortInitialiseBlocks函数

2.3.2 pvPortMalloc函数

2.3.3 vPortFree函数

2.3.4 xPortGetFreeHeapSize函数

3. heap_2内存管理实现

3.1 方案特点

3.2 空闲内存管理机制

3.3 函数实现

3.3.1 prvHeapInit函数

3.3.2 pvPortMalloc函数

3.3.3 pvPortFree函数

3.3.4 xPortGetFreeHeapSize函数

4. heap_3内存管理实现

4.1 方案特点

4.2 函数实现

5. heap_4内存管理实现

5.1 方案特点

5.2 函数实现

5.2.1 prvHeapInit函数

5.2.2 prvInsertBlockIntoFreeList函数

5.2.3 pvPortMalloc函数

5.2.4 vPortFree函数

6. heap_5内存管理实现

6.1 方案特点

6.2 函数实现

6.2.1 内存堆描述结构

6.2.2 vPortDefineHeapRegions函数


1. 概述

1.1 RTOS中内存分配特点

① 在一般的RTOS中,由于实时性的要求,很少使用虚拟内存机制,直接操作物理内存

② 所分配的内存不能超过系统的物理内存,所有系统栈都由用户自己管理

③ 在RTOS中,对内存的分配时间要求苛刻,必须是确定的。一般内存管理算法是根据需要存储的数据长度在内存中寻找一个长度匹配的空闲内存块,而寻找这样一个空闲内存块的耗时是不确定的

说明1:FreeRTOS提供5种内存管理方案

出于RTOS中内存分配的特点与要求,FreeRTOS将内存管理在可移植部分实现(FreeRTOS/Source/portable/MemMang),并且有针对性地提供了5种内存分配管理算法,不同场景的设备可以选择合适的内存算法

说明2:静态分配与动态分配

① 对于一些可靠性要求非常高的系统,应选择使用静态分配(e.g. FreeRTOS中带Static后缀的函数);而普通的业务系统可以使用动态分配来提高内存使用效率

② 静态分配可以保证设备的可靠性,但是内存使用效率低;而动态分配则相反

1.2 内存堆(heap space)来源

根据不同的内存管理算法,FreeRTOS中的内存堆共有3种来源,

1.2.1 ucHeap数组

对应内存管理算法:heap_1 / heap_2 / heap_4

实现说明:

① 内存堆大小由configTOTAL_HEAP_SIZE控制

② ucHeap有2种指定方式,

a. 由编译系统自己决定链接地址

b. 由用户指定链接地址

说明:如何指定内存堆的地址

指定内存堆的地址,需要用到编译器的attribute属性,示例如下,

我们在任务函数中访问该数组,

经过上机调试,结果与预期一致

在指定变量内存地址时,需要注意以下点,

① 不要与其他指定的内存区域重叠

例如在0x20000000处已经设置了STACK区域,如果将specified_heap也指定到该区域,则会报错

② 不能超过内存大小总量

目前模拟设备的iRAM范围为[0x20000000, 0x20020000],那么设置的specified_heap数组也不能超出该范围

参考资料:

https://blog.csdn.net/weixin_43387091/article/details/106489919

1.2.2 链接器设置的堆

对应内存管理算法:heap_3

实现说明:

heap_3算法是对编译器提供的malloc & free函数的简单封装,所以需要链接器设置一个堆,而堆的大小在启动文件中设置

1.2.3 多个非连续内存堆

对应内存管理算法:heap_5

实现说明:

heap_5算法允许用户使用多个非连续内存堆,每个内存堆的起始地址和大小由用户定义

1.3 关于字节对齐

1.3.1 字节对齐的目的

① 大多数硬件访问内存对齐的数据速度会更快,为了提高性能,FreeRTOS会进行对齐操作

② 不同体系结构的内存对齐操作可能不一样,因此FreeRTOS中通过portBYTE_ALIGNMENT宏进行设置

③ 对于Cortex-M3架构,进行8字节对齐,可见FreeRTOS/Source/portable/RVDS/ARM_CM3/portmacro.h文件

1.3.2 字节对齐的内容

字节对齐所要对齐的是要访问的内存地址,在内存分配中体现为2个方面,

① 起始地址字节对齐

② 分配内存大小字节对齐

说明:当起始地址字节对齐之后,因为分配内存大小也字节对齐,所以运算后的内存地址肯定也是字节对齐的

2. heap_1内存管理实现

2.1 方案特点

① 只能申请而不能释放内存

② 分配内存时间确定

适用场景:

① 从不删除任务、队列、信号量等内核对象的应用程序

② 要求安全的嵌入式设备

2.2 内存堆定义

如上文所述,heap_1算法使用的内存堆为ucHeap数组(后续同样使用ucHeap数组的heap_2 & heap_4算法不再赘述)

说明:configADJUSTED_HEAP_SIZE宏

由于FreeRTOS中要求地址对齐,而分配的ucHeap数组的起始地址不一定符合字节对齐要求,所以在实际使用中会计算符合字节对齐要求的内存堆起始地址

因此可能会有部分内存字节被丢弃不用,而丢弃的字节不会超过portBYTE_ALIGNMENT,也就是当前体系结构的字节对齐要求

对于configADJUSTED_HEAP_SIZE宏,就是统一丢弃portBYTE_ALIGNMENT个字节

假设ucHeap的起始地址为0x20000123,长度为256B,字节对齐要求为8B,则布局如下,

① 内存堆起始位置丢弃的5B是为了实现地址对齐

② 内存堆结束位置丢弃的3B是因为统一丢弃portBYTE_ALIGNMENT个字节造成的

2.3 函数实现

2.3.1 vPortInitialiseBlocks函数

static size_t xNextFreeByte = ( size_t ) 0;void vPortInitialiseBlocks( void )
{/* Only required when static memory is not cleared. */xNextFreeByte = ( size_t ) 0;
}

说明1:该函数一般无需被调用,因为xNextFreeByte被定义为全局变量且初始值为0,只有当bss段不被清零时才需要调用

说明2:xNextFreeByte变量记录的是当前可用的第1个空闲字节的偏移量,该偏移量相对于地址对齐的内存堆起始地址

在数值上,就相当于已经被分配的内存大小

2.3.2 pvPortMalloc函数

void *pvPortMalloc( size_t xWantedSize )
{void *pvReturn = NULL;// 静态局部变量static uint8_t *pucAlignedHeap = NULL;#if( portBYTE_ALIGNMENT != 1 ){// 对于要分配的内存大小进行字节对齐if( xWantedSize & portBYTE_ALIGNMENT_MASK ){/* Byte alignment required. */xWantedSize += ( portBYTE_ALIGNMENT -( xWantedSize & portBYTE_ALIGNMENT_MASK ) );}}#endif// 关闭调度器vTaskSuspendAll();{// 计算字节对齐的内存堆起始地址if( pucAlignedHeap == NULL ){pucAlignedHeap = ( uint8_t * )( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] )&( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );}// 检查是否有足够的内存可供分配if( ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE )&& ( ( xNextFreeByte + xWantedSize ) > xNextFreeByte )){pvReturn = pucAlignedHeap + xNextFreeByte;xNextFreeByte += xWantedSize;}}( void ) xTaskResumeAll();// 内存分配失败回调函数#if( configUSE_MALLOC_FAILED_HOOK == 1 ){if( pvReturn == NULL ){extern void vApplicationMallocFailedHook( void );vApplicationMallocFailedHook();}}#endifreturn pvReturn;
}

说明1:关闭调度器的原因

由于内存分配过程中使用了全局变量xNextFreeByte,因此是不可重入的,通过关闭调度器实现了任务间的互斥

从实现中可知,内存分配并没有实现任务与ISR之间的互斥。但是在ISR中,只能调用以FromISR结尾的函数,也不应该调用pvPortMalloc函数

说明2:判断是否有内存可供分配

判断条件中的如下条件,确保了传入的xWantedSize不会是一个负数

更正:这个条件是为了防止出现绕回,因为size_t为无符号类型,并不会出现负数

( xNextFreeByte + xWantedSize ) > xNextFreeByte )

2.3.3 vPortFree函数

// heap_1算法不支持释放内存
void vPortFree( void *pv )
{( void ) pv;/* Force an assert as it is invalid to call this function. */configASSERT( pv == NULL );
}

2.3.4 xPortGetFreeHeapSize函数

size_t xPortGetFreeHeapSize( void )
{// 返回(实际可用的内存堆数量 - 已经使用的内存堆数量)return ( configADJUSTED_HEAP_SIZE - xNextFreeByte );
}

3. heap_2内存管理实现

3.1 方案特点

① 支持释放已申请的内存

② 不能将相邻的两个小内存块合并成一个大的内存块,因此容易造成内存碎片

适用场景:

① 需要反复申请 & 删除任务、队列、信号量等内核对象且不担心内存碎片的应用程序

② 每次申请的内存大小都比较固定(可以减少内存碎片)

3.2 空闲内存管理机制

heap_2引入了空闲内存块链表进行内存管理,链表结构如下,

typedef struct A_BLOCK_LINK
{// 指向下一个空闲内存块struct A_BLOCK_LINK *pxNextFreeBlock;// 在空闲内存块被分配前,表示当前空闲内存块的大小// 在空闲内存块被分配后,表示被分配的内存块的大小// 这2种情况下的内存块大小均包含链表结构size_t xBlockSize;
} BlockLink_t;

说明:链表结构布局

① 链表结构的大小由全局变量heapSTRUCT_SIZE标识,定义如下,

static const uint16_t heapSTRUCT_SIZE = ( ( sizeof ( BlockLink_t ) +
( portBYTE_ALIGNMENT - 1 ) ) & ~portBYTE_AL

可见链表结构实际占用的内存也考虑到了字节对齐

② 链表结构被部署在内存块的起始位置,以8B对齐为例,一个16B的内存块布局如下,

3.3 函数实现

3.3.1 prvHeapInit函数

prvHeapInit函数用于初始化内存堆,在第1次调用pvPortMalloc函数时会被间接调用

// 空闲内存块链表的首尾节点
static BlockLink_t xStart, xEnd;static void prvHeapInit( void )
{BlockLink_t *pxFirstFreeBlock;uint8_t *pucAlignedHeap;// 计算地址对齐的内存堆(heap space)起始地址pucAlignedHeap = ( uint8_t * )( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) &( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );// 初始化内存堆时,将整个内存堆作为一个空闲内存块// 空闲内存块链表首节点属性,// 1. pxNextFreeBlock指向地址对齐的内存堆起始地址,也就是第1个空闲内存块//    的地址// 2. xBlockSize为0,因为链表首节点不持有可供分配的内存xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;xStart.xBlockSize = ( size_t ) 0;// 空闲内存块链表尾节点属性,// 1. pxNextFreeBlock为NULL,表示没有后继节点// 2. xBlockSize为实际管理的内存堆大小// 空闲内存块链表尾节点其实也不持有可供分配的内存,此处的内存块大小设置是为了// 实现空闲内存块的升序排列,确保尾节点始终是尾节点xEnd.xBlockSize = configADJUSTED_HEAP_SIZE;xEnd.pxNextFreeBlock = NULL;// 设置第1个空闲内存块属性,该链表结构部署在内存堆地址对齐的起始地址处// 1. xBlockSize为实际管理的内存堆大小,也就是整个内存堆// 2. pxNextFreeBlock指向尾节点pxFirstFreeBlock = ( void * ) pucAlignedHeap;pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE;pxFirstFreeBlock->pxNextFreeBlock = &xEnd;
}

初始化后的内存堆布局如下图所示,

3.3.2 pvPortMalloc函数

// 标识剩余的内存堆大小
// 特别注意:该大小不反映内存碎片,只是简单统计剩余内存
static size_t xFreeBytesRemaining = configADJUSTED_HEAP_SIZE;void *pvPortMalloc( size_t xWantedSize )
{BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;// 静态局部变量,标识内存堆是否被初始化过static BaseType_t xHeapHasBeenInitialised = pdFALSE;void *pvReturn = NULL;// 关闭调度器vTaskSuspendAll();{// 如果内存堆尚未初始化过,则初始化之if( xHeapHasBeenInitialised == pdFALSE ){prvHeapInit();xHeapHasBeenInitialised = pdTRUE;}if( xWantedSize > 0 ){// 在申请的内存数量之上增加链表结构的大小xWantedSize += heapSTRUCT_SIZE;// 对申请的内存数量进行字节对齐if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0 ){xWantedSize += ( portBYTE_ALIGNMENT -( xWantedSize & portBYTE_ALIGNMENT_MASK ) );}}if( ( xWantedSize > 0 ) &&( xWantedSize < configADJUSTED_HEAP_SIZE ) ){// 如果申请的内存数量基本合法,则进入空闲内存块查找流程// 空闲内存块根据大小升序组织在链表中// 保存了前驱指针,便于链表维护pxPreviousBlock = &xStart;pxBlock = xStart.pxNextFreeBlock;while( ( pxBlock->xBlockSize < xWantedSize ) &&( pxBlock->pxNextFreeBlock != NULL ) ){pxPreviousBlock = pxBlock;pxBlock = pxBlock->pxNextFreeBlock;}if( pxBlock != &xEnd ){// 执行到此处,说明已经找到合适的空闲内存块// 返回的内存地址要越过链表结构大小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;// 将剩余空闲内存块加入链表prvInsertBlockIntoFreeList( ( pxNewBlockLink ) );}// 维护剩余内存堆大小xFreeBytesRemaining -= pxBlock->xBlockSize;}}}( void ) xTaskResumeAll();#if( configUSE_MALLOC_FAILED_HOOK == 1 ){// 内存分配失败回调函数#endifreturn pvReturn;
}

说明1:需要将空闲内存块拆分的阈值由heapMINIMUM_BLOCK_SIZE控制,该值为字节对齐后的链表结构大小的2倍

#define heapMINIMUM_BLOCK_SIZE ( ( size_t ) ( heapSTRUCT_SIZE * 2 ) )

说明2:prvInsertBlockIntoFreeList函数

// heap_2算法的prvInsertBlock实现为带参数宏(我们此处忽略续行符)
// 传入的参数为调用pvPortMalloc函数返回的内存地址
#define prvInsertBlockIntoFreeList( pxBlockToInsert )
{BlockLink_t *pxIterator;size_t xBlockSize;// 要插入的空闲内存块大小xBlockSize = pxBlockToInsert->xBlockSize;for( pxIterator = &xStart;pxIterator->pxNextFreeBlock->xBlockSize < xBlockSize;pxIterator = pxIterator->pxNextFreeBlock ){// 仅为遍历空闲内存块链表,按大小升序插入}// 退出循环时,pxIterator->pxNextFreeBlock->xBlockSize >= xBlockSize// 所以需要将pxBlockToInsert指向的节点插入pxIterator指向的节点之后// 由于是单链表,后插比较方便pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;pxIterator->pxNextFreeBlock = pxBlockToInsert;
}

3.3.3 pvPortFree函数

void vPortFree( void *pv )
{uint8_t *puc = ( uint8_t * ) pv;BlockLink_t *pxLink;if( pv != NULL ){// 指向内存块的链表结构puc -= heapSTRUCT_SIZE;pxLink = ( void * ) puc;vTaskSuspendAll();{// 将内存块加入空闲内存块链表prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );xFreeBytesRemaining += pxLink->xBlockSize;}( void ) xTaskResumeAll();}
}

说明:与heap_1算法相比,heap_2算法存在不确定性,因为有一个查找的过程,但是效率也远比标准C的malloc & free高

3.3.4 xPortGetFreeHeapSize函数

size_t xPortGetFreeHeapSize( void )
{return xFreeBytesRemaining;
}

4. heap_3内存管理实现

4.1 方案特点

① 只是简单封装标准C库的malloc & free函数,也就是使用编译器提供的内存分配函数

② 需要链接器设置内存堆(上文已有说明)

③ 具有不确定性

④ 很可能增大RTOS内核的代码大小

注意:这种用法需要使用的编译器提供了malloc & free函数

4.2 函数实现

void *pvPortMalloc( size_t xWantedSize )
{void *pvReturn;vTaskSuspendAll();{pvReturn = malloc( xWantedSize );}( void ) xTaskResumeAll();return pvReturn;
}void vPortFree( void *pv )
{if( pv ){vTaskSuspendAll();{free( pv );}( void ) xTaskResumeAll();}
}

可见heap_3算法只是对编译器提供的malloc & free函数进行了任务间的互斥保护

5. heap_4内存管理实现

5.1 方案特点

① 支持释放已申请的内存

② 提供内存块合并算法,会将地址相邻的内存块合并,以此减少内存碎片

适用场景:

① 需要反复申请 & 删除任务、队列、信号量等内核对象的应用程序

② 每次申请的内存大小随机

注意:heap_4算法只是减轻了内存碎片问题,并非不会产生内存碎片,这在理论上是无法避免的

5.2 函数实现

5.2.1 prvHeapInit函数

// 在heap_4算法中,空闲内存块链表尾节点存储在内存堆中,而非独立的变量,
// 其中pxEnd指针指向该地址
static BlockLink_t xStart, *pxEnd = NULL;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 ){// 计算地址对齐的内存堆起始地址,// 保存在uxAddress中uxAddress += ( portBYTE_ALIGNMENT - 1 );uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );// 从内存堆总量中减去因地址对齐而舍弃的字节数xTotalHeapSize -= uxAddress - ( size_t ) ucHeap;}// 指向字节对齐的内存堆起始地址pucAlignedHeap = ( uint8_t * ) uxAddress;// 空闲内存块链表首节点属性// 1. pxNextFreeBlock指向地址对齐的内存堆起始地址,也就是第1个空闲内存块//    的地址// 2. xBlockSize为0,因为链表首节点不持有可供分配的内存xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;xStart.xBlockSize = ( size_t ) 0;// 计算空闲内存块链表尾节点地址// 与heap_2算法不同,heap_4算法的尾节点就存储在内存堆中(内存堆结尾处)uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize;uxAddress -= xHeapStructSize;// 向低地址端对齐uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );pxEnd = ( void * ) uxAddress;// 空闲内存块链表尾节点属性// 1. pxNexFreeBlock为NULL,表示没有后继节点// 2. xBlockSize为0,因为链表尾节点不持有可供分配的内存pxEnd->xBlockSize = 0;pxEnd->pxNextFreeBlock = NULL;// 设置第1个空闲内存块属性,该链表结构部署在内存堆地址对齐的起始地址处// 1. xBlockSize为实际管理的可用的内存堆大小// 2. pxNextFreeBlock指向尾节点// 注意:uxAddress此时存储的是在内存堆中的链表尾节点的地址pxFirstFreeBlock = ( void * ) pucAlignedHeap;pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;pxFirstFreeBlock->pxNextFreeBlock = pxEnd;// 初始化用于统计的2个全局变量,// xMinimumEverFreeBytesRemaining:表示未分配内存堆的历史最小值// xFreeBytesRemaining:当前系统中未分配的内存堆大小xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;// xBlockAllocatedBit表示将系统能表示的数值的最高位置为1,// 对于32位系统,该值为0x80000000// 在heap_4算法中,申请内存大小的最高位被用于表示一个内存块是否已经被分配// 因此可分配的内存大小上限为0x7FFFFFFF(包含链表结构大小)xBlockAllocatedBit = ( ( size_t ) 1 ) <<( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );
}

heap_4算法初始化后的内存堆如下图所示,

5.2.2 prvInsertBlockIntoFreeList函数

内存块插入函数是heap_4算法的核心,需要注意如下2点,

① 内存块合并算法在该函数中实现

② heap_4算法中的空闲内存块是按地址升序排序的(heap_2算法是按内存块大小升序排序)

static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert )
{BlockLink_t *pxIterator;uint8_t *puc;// step1:查找插入点// 由于是单链表,适合后插操作// 所以退出循环的条件是pxIterator->pxNextFreeBlock >= pxBlockToInsert// 那么插入的位置就是pxIterator之后for( pxIterator = &xStart;pxIterator->pxNextFreeBlock < pxBlockToInsert;pxIterator = pxIterator->pxNextFreeBlock ){// 仅为遍历空闲内存块链表,按地址升序插入}// step2:检查要插入的内存块能否和前一个内存块合并// 此时pxIterator指向前一个内存块puc = ( uint8_t * ) pxIterator;if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert ){// 合并操作pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;// 修改pxBlockToInsert非常重要pxBlockToInsert = pxIterator;// 与前一个内存块合并,没有调整链表指针指向的工作}// step3:检查要插入的内存块能否和后一个内存块合并// 此时pxIterator->pxNextFreeBlock指向后一个内存块// 注意:此时可能已经进行过一次和前一个内存块的合并操作puc = ( uint8_t * ) pxBlockToInsert;if( ( puc + pxBlockToInsert->xBlockSize ) ==( uint8_t * ) pxIterator->pxNextFreeBlock ){// 可以与后一个内存块合并// 其中一种情况是将内存块插入pxEnd之前(else语句处理),实际并非合并if( pxIterator->pxNextFreeBlock != pxEnd ){pxBlockToInsert->xBlockSize +=pxIterator->pxNextFreeBlock->xBlockSize;pxBlockToInsert->pxNextFreeBlock =pxIterator->pxNextFreeBlock->pxNextFreeBlock;}else{pxBlockToInsert->pxNextFreeBlock = pxEnd;}}else{// 无法与后一个内存块合并// 将要插入的内存块插入pxIterator之后pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;}// pxIterator与pxBlockToInsert不相等,// 说明没有进行过和前一个内存块的合并操作,if( pxIterator != pxBlockToInsert ){pxIterator->pxNextFreeBlock = pxBlockToInsert;}
}

说明1:查询插入点的操作一定可以正确退出

因为当空闲内存块链表为空时,xStart.pxNextFreeBlock指向pxEnd,该地址一定大于所有要插入的内存块地址,此时pxIterator就是指向xStart节点,插入的位置就是xStart节点之后

说明2:内存块插入操作图示

在进行内存块插入时,共有4种情况,

① 与前后均无法合并

② 仅向前合并

③ 仅向后合并

④ 同时与前后合并

下面分别图示说明,并整理代码实现思路

① 与前后均无法合并

对于前后均无法合并的情况,需要进行简单的后插操作

pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
pxIterator->pxNextFreeBlock = pxBlockToInsert;

② 仅向前合并

向前合并后,待插入内存块与前内存块合二为一,其实可以不调整指针指向,只是为了统一操作,进行了如下调整

pxBlockToInsert = pxIterator;

这样无论是否有向后合并的操作,均可以使用pxBlockToInsert变量

③ 仅向后合并

向后合并需要注意处理一种情况,就是后内存块为pxEnd,此时实际上是没有内存块合并操作的

④ 同时与前后合并

5.2.3 pvPortMalloc函数

void *pvPortMalloc( size_t xWantedSize )
{BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;void *pvReturn = NULL;vTaskSuspendAll();{// 首次调用pvPortMalloc时,初始化内存堆if( pxEnd == NULL ){prvHeapInit();}// 检查要申请的内存大小是否超限// 由于heap_4使用内存大小的最高位标识内存是否被分配,所以对于32位系统,// 可申请的最大内存为0x7FFFFFFF(包含链表结构大小)if( ( xWantedSize & xBlockAllocatedBit ) == 0 ){if( xWantedSize > 0 ){// 增加申请链表结构大小xWantedSize += xHeapStructSize;// 对内存申请大小进行字节对齐if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 ){xWantedSize += ( portBYTE_ALIGNMENT -( xWantedSize & portBYTE_ALIGNMENT_MASK ) );}}if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) ){// 查找满足条件的空闲内存块pxPreviousBlock = &xStart;pxBlock = xStart.pxNextFreeBlock;while( ( pxBlock->xBlockSize < xWantedSize ) &&( pxBlock->pxNextFreeBlock != NULL ) ){pxPreviousBlock = pxBlock;pxBlock = pxBlock->pxNextFreeBlock;}if( pxBlock != pxEnd ){// 返回地址需要越过链表结构大小pvReturn = ( void * )( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) +xHeapStructSize );// 将分配的内存块从空闲内存块链表删除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;prvInsertBlockIntoFreeList( pxNewBlockLink );}// 维护统计值xFreeBytesRemaining -= pxBlock->xBlockSize;if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining ){xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;}// 标识该内存块已被分配pxBlock->xBlockSize |= xBlockAllocatedBit;pxBlock->pxNextFreeBlock = NULL;}}}}( void ) xTaskResumeAll();return pvReturn;
}

说明:heap_4算法内存分配bug

如上文所述,由于heap_4使用链表结构中xBlockSize的最高位标识该内存块是否已被分配,所以分配内存的大小上限为0x7FFFFFFF(包含链表节点)

但是pvPortMalloc代码中并未检查要包含链表节点,不过这不会导致实际的故障,因为MCU上一般没有这么多内存可供分配

假设xWantedSize为0x7FFFFFFF,在检查内存分配大小时可以通过,但是一旦加上8B的链表结构大小,则xWantedSize变为0x80000007,该长度在heap_4算法中并不合法

5.2.4 vPortFree函数

void vPortFree( void *pv )
{uint8_t *puc = ( uint8_t * ) pv;BlockLink_t *pxLink;if( pv != NULL ){// 寻址到内存块的链表结构puc -= xHeapStructSize;pxLink = ( void * ) puc;configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );configASSERT( pxLink->pxNextFreeBlock == NULL );// 该内存块必须确实被分配过if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 ){if( pxLink->pxNextFreeBlock == NULL ){pxLink->xBlockSize &= ~xBlockAllocatedBit;vTaskSuspendAll();{// 将内存块重新加入空闲内存块链表prvInsertBlockIntoFreeList((( BlockLink_t * ) pxLink));}( void ) xTaskResumeAll();}}}
}

6. heap_5内存管理实现

6.1 方案特点

① heap_5使用了与heap_4相同的合并算法,内存的申请与释放也与heap_4基本相同

② heap_5允许内存跨越多个不连续的内存段

适用场景:

需要在不同存储介质上使用内存堆的场景(e.g. 内存堆同时使用了片内SRAM与片外DDR)

6.2 函数实现

6.2.1 内存堆描述结构

由于heap_5算法可以定义多个内存堆,FreeRTOS引入了HeapRegion_t类型描述每一个内存堆

// portable.h
typedef struct HeapRegion
{// 内存堆起始地址uint8_t *pucStartAddress;// 内存堆大小size_t xSizeInBytes;
} HeapRegion_t;

在使用时,可定义HeapRegion_t类型数组,描述每一个内存堆

HeapRegion_t xHeapRegions[] =
{// Defines a block of 0x10000 bytes starting at address 0x80000000{ ( uint8_t * ) 0x80000000UL, 0x10000 },// Defines a block of 0xa0000 bytes starting at address of 0x90000000{ ( uint8_t * ) 0x90000000UL, 0xa0000 },// 必须以NULL结尾{ NULL, 0 }
};

对xHeapRegions数组的要求,

① 数组中成员顺序按照地址从低到高排列

② 数组最后一个成员必须使用NULL

6.2.2 vPortDefineHeapRegions函数

heap_5算法在调用内存管理相关API之前,需要先调用vPortDefineHeapRegions函数初始化内存堆。在vPortDefineHeapRegions函数执行之前,禁止调用任何可能调用pvPortMalloc的API函数

而vPortDefineHeapRegions函数的主要工作,就是将不连续的内存堆组织为链表,本质上与使用单个内存堆的场景一致

void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions )
{BlockLink_t *pxFirstFreeBlockInRegion = NULL, *pxPreviousFreeBlock;size_t xAlignedHeap;size_t xTotalRegionSize, xTotalHeapSize = 0;BaseType_t xDefinedRegions = 0;size_t xAddress;const HeapRegion_t *pxHeapRegion;// 该函数只能被调用一次configASSERT( pxEnd == NULL );// 指向描述内存堆描述数组pxHeapRegion = &( pxHeapRegions[ xDefinedRegions ] );// 遍历内存堆描述数组while( pxHeapRegion->xSizeInBytes > 0 ){xTotalRegionSize = pxHeapRegion->xSizeInBytes;// 计算内存堆字节对齐的起始地址,保存在xAddress中// 计算内存堆实际可用大小,保存在xTotalRegionSize中xAddress = ( size_t ) pxHeapRegion->pucStartAddress;if( ( xAddress & portBYTE_ALIGNMENT_MASK ) != 0 ){xAddress += ( portBYTE_ALIGNMENT - 1 );xAddress &= ~portBYTE_ALIGNMENT_MASK;xTotalRegionSize -=xAddress - ( size_t ) pxHeapRegion->pucStartAddress;}// 指向内存堆对齐的起始地址xAlignedHeap = xAddress;// 处理第1个内存堆时,设置xStart链表首节点属性if( xDefinedRegions == 0 ){xStart.pxNextFreeBlock = ( BlockLink_t * ) xAlignedHeap;xStart.xBlockSize = ( size_t ) 0;}else{configASSERT( pxEnd != NULL );configASSERT( xAddress > ( size_t ) pxEnd );}// 第1次调用时,pxEnd为NULL// 所以第1次调用时,pxPreviousFreeBlock也为NULLpxPreviousFreeBlock = pxEnd;// 在当前内存堆计算链表尾节点的地址xAddress = xAlignedHeap + xTotalRegionSize;xAddress -= xHeapStructSize;xAddress &= ~portBYTE_ALIGNMENT_MASK;pxEnd = ( BlockLink_t * ) xAddress;// 设置链表尾节点属性pxEnd->xBlockSize = 0;pxEnd->pxNextFreeBlock = NULL;// 在当前内存堆,设置第1个空闲内存块属性pxFirstFreeBlockInRegion = ( BlockLink_t * ) xAlignedHeap;pxFirstFreeBlockInRegion->xBlockSize =xAddress - ( size_t ) pxFirstFreeBlockInRegion;pxFirstFreeBlockInRegion->pxNextFreeBlock = pxEnd;if( pxPreviousFreeBlock != NULL ){// 跨内存堆构建空闲内存块链表pxPreviousFreeBlock->pxNextFreeBlock =pxFirstFreeBlockInRegion;}xTotalHeapSize += pxFirstFreeBlockInRegion->xBlockSize;// 寻址下一个内存堆描述结构xDefinedRegions++;pxHeapRegion = &( pxHeapRegions[ xDefinedRegions ] );}// 初始化统计信息xMinimumEverFreeBytesRemaining = xTotalHeapSize;xFreeBytesRemaining = xTotalHeapSize;// 内存块已被分配标识xBlockAllocatedBit =( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );
}

说明1:根据函数实现,多个内存堆的描述确实需要按升序排列,因为初始化时不会处理地址顺序

从代码实现上分析,最后一个内存堆描述信息只需要大小为0,起始地址不一定要为NULL(当然指定为NULL是最合理的)

说明2:跨内存堆链表构成图示

可见是用原先每个内存堆中的尾节点指向下一个内存堆的第1个空闲内存块,同时该节点的大小仍然为0,并不实际持有内存

附件:heap_5跨内存堆链表

说明3:在调用pvPortMalloc函数分配内存时,由于是遍历空闲内存块链表查找大小符合条件的内存块,所以实际分配在哪个内存堆是不确定的

FreeRTOS源码分析与应用开发10:内存管理相关推荐

  1. FreeRTOS源码分析与应用开发02:任务管理

    目录 1. 任务概述 1.1 任务表示 1.2 任务状态 1.2.1 运行态 1.2.2 就绪态 1.2.3 阻塞态 1.2.4 挂起态 1.3 任务优先级 1.3.1 FreeRTOS优先级配置 1 ...

  2. FreeRTOS源码分析与应用开发01:中断配置与临界段

    目录 1. 异常与中断的基本概念 1.1 异常分类 1.2 中断概述 1.2.1 中断处理宜短暂 1.2.2 临界段影响中断实时性 1.3 中断硬件基础 1.3.1 外设 1.3.2 中断控制器 1. ...

  3. FreeRTOS源码分析与应用开发07:事件标志组

    目录 1. 概述 2. 事件标志组类型 3. 创建事件标志组 4. 删除事件标志组 5. 设置事件标志位 5.1 任务级设置 5.2 中断级设置 6. 清除事件标志位 6.1 任务级清除 6.2 中断 ...

  4. FreeRTOS源码分析与应用开发04:消息队列

    目录 1. 队列结构 2. 创建队列 2.1 动态创建队列 2.1.1 xQueueCreate函数 2.1.2 xQueueGenericCreate函数 2.1.3 xQueueGenericRe ...

  5. FreeRTOS源码分析与应用开发09:低功耗Tickless模式

    目录 1. STM32F4低功耗模式简介 2. Tickless模式详解 2.1 如何降低功耗 2.2 关闭SysTick的问题与解决方案 2.2.1 关闭SysTick导致系统节拍计数器停止 2.2 ...

  6. FreeRTOS源码分析与应用开发11(完):编译、链接与部署

    目录 1. 存储设备布局 2. 链接器脚本 2.1 链接器脚本生成 2.2 链接器脚本分析 2.2.1 分散加载文件 2.2.2 加载区 & 运行区 2.2.3 ER_IROM1运行区分析 2 ...

  7. FreeRTOS源码分析与应用开发08:任务通知

    目录 1. 概述 1.1 任务通知概念 1.2 任务通知控制结构 2. 发送任务通知 2.1 任务级发送 2.2 中断级发送 2.2.1 xTaskNotifyFromISR函数 2.2.2 vTas ...

  8. FreeRTOS源码分析与应用开发06:软件定时器

    目录 1. 概述 1.1 软件定时器 & 硬件定时器 1.2 软件定时器精度 1.3 单次模式 & 周期模式 2. 软件定时器组件 2.1 定时器任务 2.2 定时器列表 2.3 定时 ...

  9. FreeRTOS源码分析与应用开发05:信号量

    目录 1. 信号量概述 1.1 信号量概念 1.2 4种信号量 1.2.1 二值信号量 1.2.2 计数信号量 1.2.3 互斥信号量 1.2.4 递归互斥信号量 1.3 信号量相关控制结构 1.3. ...

最新文章

  1. 特斯拉AI团队招兵买马:“英雄不问出处”
  2. hadoop day 3
  3. 在备份流程中使用date
  4. ALV列(Column)换到行(Row) 之 列上限不固定篇
  5. php跳转404_php伪静态.htaccess实现403,404跳转
  6. oracle日志文件大小规则,修改oracle日志文件大小
  7. xp系统怎样启动打印机服务器,WinXP系统如何开启Printspooler服务?
  8. html中加一个框与底部平齐,div+CSS实现单选复选框与文本对齐
  9. 每日算法系列【LeetCode 233】数字 1 的个数
  10. GitLab版本管理(转)
  11. 计算机学不学p图的,五分钟学会P图!只需要电脑自带的看图软件!无需专业的PS软件!...
  12. 02网络爬虫-使用 Beautiful Soup 解析网页
  13. marked is not a function问题解决
  14. NEAT(基于NEAT-Python模块)实现监督学习和强化学习
  15. 测试人的Java之编程那点事
  16. C语言按行读文件与读文件中每一个字符
  17. php 点赞 代码,WordPress模板如何使用纯代码实现点赞功能?
  18. 中英文排版规范化 API 接口
  19. L1 L2正则化和优化器的weight_decay参数
  20. 第1讲 移动互联网概述

热门文章

  1. 天津市七下计算机课程,七年级下册信息技术课程教案.doc
  2. MySQL中的locate函数
  3. Layui--代码修饰器layui.code
  4. java 与sas交互_SAS与MACRO的交互使用
  5. render注册一个链接组件_vue: 单文件组件 render函数
  6. 【电脑帮助】解决Wind10系统没有本地用户和组的问题
  7. excel清空sheet内容 poi_java – 从POI中的WorkBook中删除工作表
  8. magic winmail邮件服务器,使用magic winmail server轻松架设邮件服务器(三)_邮件服务器...
  9. java中jsp怎么传递参数_急!Java问题,Java如何获得jsp传递的参数??
  10. python简单超级马里奥游戏下载大全_Python实现超级玛丽游戏系列教程02玛丽走跑...