FreeRTOS笔记:堆内存管理

来源:互联网 发布:vscode安装插件 编辑:程序博客网 时间:2024/05/22 14:21

堆内存管理

FreeRTOS的内核对象全都采用运行时的动态内存分配。但不同设备内存的容量大小、地址映射、访问速度以及应用对实时性的要求都是不同的,因此FreeRTOS 把堆内存管理放在可移植层,且提供了5套不同的管理方案,位于MemMang/heap_n.c 其中n为1-5。用户也可按照接口自己实现堆内存管理。

接口

堆内存管理接口是一组函数声明,位于portable.h

void *pvPortMalloc( size_t xSize ) PRIVILEGED_FUNCTION;void vPortFree( void *pv ) PRIVILEGED_FUNCTION;

其中 PRIVILEGED_FUNCTION 宏提示这些是特权级函数,
pvPortMalloc 类似于C库中的malloc; vPortFree 类似于C库中的free。

实现

堆所使用的内存空间定义为一个 uint8_t 数组,configAPPLICATION_ALLOCATED_HEAP宏配置ucHeap所在位置,configTOTAL_HEAP_SIZE配置堆大小。如果configAPPLICATION_ALLOCATED_HEAP为1 ,则应用的某个c文件中必须定义ucHeap数组。

#if( configAPPLICATION_ALLOCATED_HEAP == 1 )    extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];#else    static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];#endif /* configAPPLICATION_ALLOCATED_HEAP */

通过在应用中定义ucHeap,用户可以自定义堆的具体存放位置。如果使用的是gcc,可以指定一个ucHeap存放于一个段:

uint8_t ucHeap[ configTOTAL_HEAP_SIZE ] __attribute__ ( ( section( ".my_heap" ) ) );

其中,.my_heap可以在link srcipt中指定地址。对于一些具备高速内存和低速内存的设备,可以用这种方式指定堆位于高速内存来提高访问速度。如果configAPPLICATION_ALLOCATED_HEAP宏不为1,则堆的位置由链接器分配。

heap_1.c

heap_1是最基本的分配方案,只能分配而不能回收,pvPortMalloc的实现:
首先要计算分配内存大小的对齐大小,例如4 byte对齐,则实际分配大小为大于xWantedSize的最小的4的倍数。

if( xWantedSize & portBYTE_ALIGNMENT_MASK ){  /* Byte alignment required. */  xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );}

然后需要挂起所有任务,放置分配过程被打断

vTaskSuspendAll();

空间使用用变量xNextFreeByte表示,xNextFreeByte是空闲区域相对于堆起始处的偏移量,每次分配时只需把偏移量增加分配的大小即可。
如果需要分配失败时,应用获取提示,可以定义configUSE_MALLOC_FAILED_HOOK宏为1,应用中还需定义一个vApplicationMallocFailedHook();函数用于回调。

#if( configUSE_MALLOC_FAILED_HOOK == 1 ){if( pvReturn == NULL )   {    extern void vApplicationMallocFailedHook( void );    vApplicationMallocFailedHook();   }}#endif

heap_2.c

heap_2 具备分配和回收功能,分配策略使用最优适配算法(best fit)。分配内存时,遍历所有空闲内存块,从中选择可分配空间不小于要分配大小的最小块,然后把该空闲块分为已分配块和一个新的空闲块。
需要注意的是heap_2不会合并相邻的空闲块,所以该方案会导致总的内存块数量随时间单调递增,多次分配后可能导致大量的内存碎片,大量内存空闲却无法分配到。

该方案使用链表来管理空闲块:

typedef struct A_BLOCK_LINK {  struct A_BLOCK_LINK *pxNextFreeBlock; /*<< The next free block in the list. */  size_t xBlockSize;                    /*<< The size of the free block. */} BlockLink_t;

其中xBlockSize表示当前空闲块大小,包括BlockLink_t结构所占用空间(考虑对齐,可能大于sizeof(BlockLink_t))。
BlockLink_t类型的全局变量 xStart作为链表的第一项,xEnd作为链表的最后一项。
第一次调用pvPortMalloc 会先调用prvHeapInit进行初始化,首先确定堆起始位置,也就是ucHeap 后面的第一个对齐地址pucAlignedHeap,设置xStart.pxNextFreeBlock为该地址。pucAlignedHeap开始的地方要存放一个BlockLink_t结构,存放第一个空闲块的链表项pxFirstFreeBlock,pxFirstFreeBlock的下一项为xEnd。

  pxFirstFreeBlock = (void *)pucAlignedHeap;  pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE;  pxFirstFreeBlock->pxNextFreeBlock = &xEnd;

初始化后,堆中只包含一个空闲块,其大小为 configADJUSTED_HEAP_SIZE:

#define configADJUSTED_HEAP_SIZE (configTOTAL_HEAP_SIZE - portBYTE_ALIGNMENT)

由于开始位置进行了对齐,因为对齐而浪费的字节数必定小于portBYTE_ALIGNMENT,所以真正的可用空间不小于 configADJUSTED_HEAP_SIZE。
下面考虑一个已初始化的堆的分配:
每分配出一块内存,都需要保存一个BlockLink_t结构,需要额外的内存:

      xWantedSize += heapSTRUCT_SIZE;

其中heapSTRUCT_SIZE是考虑对之后的保存BlockLink_t的所需内存:

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

还需考虑分配内存本身的对齐:

       xWantedSize += (portBYTE_ALIGNMENT - (xWantedSize & portBYTE_ALIGNMENT_MASK));

计算好实际所需内存之后,则从前往后查找空闲链表,直到找到一个可用空闲块或到达链表尾:

      pxPreviousBlock = &xStart;      pxBlock = xStart.pxNextFreeBlock;      while ((pxBlock->xBlockSize < xWantedSize) &&             (pxBlock->pxNextFreeBlock != NULL)) {        pxPreviousBlock = pxBlock;        pxBlock = pxBlock->pxNextFreeBlock;      }

如果查找到满足条件的空闲块,则判断多出的内存是否可以划分出新的空闲块。若可以将新的空闲块插入空闲链表。
前面已经提到heap_2采用最佳适配的分配策略,而实现此策略的关键就是空闲块插入方法prvInsertBlockIntoFreeList,每次插入后都保证块大小按从小到大的次序排列,这就保证第一个满足条件的空闲块是所有满足条件的空闲块中最小的一个。

#define prvInsertBlockIntoFreeList( pxBlockToInsert )                                                                         \{                                                                                                                             \BlockLink_t *pxIterator;                                                                                                      \size_t xBlockSize;                                                                                                            \xBlockSize = pxBlockToInsert->xBlockSize;                                                                                     \for( pxIterator = &xStart; pxIterator->pxNextFreeBlock->xBlockSize < xBlockSize; pxIterator = pxIterator->pxNextFreeBlock )   \{                                                                                                                             \}                                                                                                                             \pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;                                                               \pxIterator->pxNextFreeBlock = pxBlockToInsert;                                                                                \}

第一次插入之前,链表中只有唯一个空闲块,显然是排序好的。
假设某次插入前,链表已经是按空间大小从小到大排序好的。则要插入的空闲块会插入到第一个不小于其大小的空闲块之前,所以插入后链表依旧是从小到大排序的。
所以空闲链表经过任意次插入后,都是按从小到大排序好的。
分配完成后,恢复所有任务,然后返回当前块偏移heapSTRUCT_SIZE的地址。

内存释放函数void vPortFree( void *pv )的实现:
把指针向前移动heapSTRUCT_SIZE,也就是当前块的BlockLink_t结构的首地址,挂起所有线程,然后把当前块插入到空闲块,然后恢复所有任务。

heap_3.c

heap_3.c 只是简单封装了C库中的malloc和free,为保证线程安全在调用malloc或free的前后挂起和恢复所有任务。而C库使用的堆需要在link script中配置。

heap_4.c

heap_4 采用最先适配算法(first fit,也有人翻译为首次适配算法),而且heap_4会合并相邻的空闲块,所以大大降低了内存碎片化的风险。首次适配算法使用第一个找到的足够大的空闲块作为请求的内存空间。heap_4中pvPortMalloc和vPortFree的实现基本与heap_2类似,分配策略的区别主要在prvInsertBlockIntoFreeList中实现。

for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock ){}

与heap_2比较,这里的迭代器pxIterator的终止条件是比较pxNextFreeBlock和pxBlockToInsert,是地址的比较;而heap2_c中是对空间大小的比较。所以插入之后是按照其所在物理地址的顺序从小到大排序的,使用归纳法可以证明任意次插入前后链表都是按照地址的顺序排序的。也就是说如果存在2个物理地址上相邻的空闲块,则它们在链表次序上也是相邻的。要保证地址上相邻的空闲块都被合并,只需要保证任意链表项结构的pxNextFreeBlock 大于 该结构起始地址 + xBlockSize
而初始情况不存在地址相邻的空闲块,所以只要每次插入时检查相邻空闲块,并进行合并即可。

puc = ( uint8_t * ) pxIterator;if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert ){pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;pxBlockToInsert = pxIterator;}

还需要合并之后的空闲块:

puc = ( uint8_t * ) pxBlockToInsert;if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock ){    if( pxIterator->pxNextFreeBlock != pxEnd )    {      /* Form one big block from the two blocks. */      pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;      pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock;    }    else    {      pxBlockToInsert->pxNextFreeBlock = pxEnd;    }}else{    pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;}

此外,heap_4还使用xBlockSize的一个位来标识当前块是属于应用还是空闲快,这个位由xBlockAllocatedBit指定。而初始化中xBlockAllocatedBit被设置为size_t类型的最高位。所以调用pvPortMalloc要检查是否最高位被使用。

heap_5.c

heap_5 使用的分配算法与heap_4相同,也是最先适配算法(first fit)。但heap_5不限定堆位于连续的内存空间,也可以使用多个不同区域的内存作为堆。

这种设计是考虑到有些设备存储器映射的内存地址并非连续,而是分布在地址空间的不同区域。例如:

在这种情况下,堆使用的内存需要有具体用户来指定,所以FreeRTOS提供了vPortDefineHeapRegions方法来定义堆。在使用pvPortMalloc之前必须先调用void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions )来进行初始化。
参数是一个HeapRegion_t结构的数组。每个分开的内存区域都用个HeapRegion_t类型的元素代表,并按照地址从小到大的顺序排列。还要一个哨兵元素作为数组最后一项,其pucStartAddress设为NULL;

typedef struct HeapRegion{  uint8_t *pucStartAddress;  size_t xSizeInBytes;} HeapRegion_t;

循环中,分别对每一个内存区域进行初始化,其过程类似于前面方案中的prvHeapInit方法。
完成后得到初始的空闲链表,其中包含有一定数量的空闲块,每个空闲块对应一个堆中的一个连续内存区域。

heap_5 的pvPortMallocvPortFree,prvInsertBlockIntoFreeList的实现与heap_4基本相同。由于要先使用vPortDefineHeapRegions 所以第一次使用pvPortMalloc无需再进行初始化。而不同的内存区域地址是不连续的,不会被合并,所以prvInsertBlockIntoFreeList沿用heap_4的实现即可。

0 0
原创粉丝点击