FreeRTOS学习-内存管理

1. 动态内存分配与FreeRTOS

从v9.0.0后,FreeRTOS开始支持内核对象的静态分配方式,因此,内存管理库可以被裁剪。但在大多数嵌入式应用中,堆的使用还是非常常见的。因此,还是有必要研究一下FreeRTOS的内存管理。

在绝大多数情况下,嵌入式应用会使用动态内存分配的编程模型,所以,对于FreeRTOS内核而言,每当需要创建内核对象时,内核会申请内存用于存储内核对象的元信息;当删除内核对象时,则会释放相应的内存。而FreeRTOS所使用的堆是由其自身的内存管理模块来实现的。

2. 内存分配模式

在FreeRTOS的内存管理中,提供了5种内存管理实现方式,每种管理方式对外的接口一致。

在所有实现中,FreeRTOS的堆都只有一个,即由内核管理的系统堆,而非由任务管理,因此,所有任务都共享这个系统堆

2.1. Heap_1

Heap_1是最简单的实现方式。它不实现内存释放接口

适用场景:在调度器开启之前,只创建内核对象,事后也不进行释放

设计思路:

  • 系统堆大小:通过configTOTAL_HEAP_SIZE设置。静态申请一片内存作为FreeRTOS的堆;
  • 内存块大小:自动调整,使得块大小满足地址对齐需求;
  • 使用一个变量记录空闲空间位置;
  • pvPortMalloc():该函数被调用时,一个数组(heap)会被划分成小块分配出去。

Heap1

  • A:没有任务被创建时,堆的情况;
  • B:创建了一个任务;
  • C:创建了3个任务。

设定自定义位置的堆空间的方法:

  • 设置configAPPLICATION_ALLOCATED_HEAP = 1
  • 根据编译器的语法,设定地址,也即设定ucHeap数组的地址,以GCC为例:
uint8_t ucHeap[configTOTAL_HEAP_SIZE] __attribute__ ((section(".my_heap")));

2.2. Heap_2

Heap_2的实现开始支持内存的释放。

适用场景:重复的申请和释放内存,但操作的内存块大小一致。(为了兼容旧版本,新的应用不建议使用)

设计思路:

  • 系统堆大小:与Heap_1一样;
  • 内存块大小:增加元信息长度, 自动调整使满足对齐条件;
  • 将空闲块通过链表形式按照块大小排列,并将元信息嵌入内存块中。
  • pvPortMalloc():使用Best fit算法找到一个最接近的空闲块,分配出去;如果满足分裂条件,还会将空闲块拆分成两份再分配。
  • pvPortFree():释放时,并不会对相邻的块进行合并。

该实现在申请和释放内存时的耗时是不确定的,但比C标准库的malloc()free()实现要快一些。

2.3. Heap_3

设计思路:使用标准的malloc()free()函数实现,因此堆大小由链接器的配置决定。需要特别说明的是,Heap_3实现了线程安全,具体是通过挂起调度器实现。

2.4. Heap_4

Heap_4是在实际工程中使用最多的一种实现方式。

设计思路:与Heap_2类似,不同之处在于,释放内存时,会自动对相邻的空闲块进行合并。

  • 内存块大小: 因为使用特殊的分配位(即size_t的最高有效位), 所以内存块的最大长度为2sizeof(size_t) * 8 - 1字节, 例如在ARM CA9中,必须小于2GB
  • pvPortMalloc(): 使用First fit算法, 找到第一个满足需求的空闲块, 分配出去.

2.5. Heap_5

适用场景:系统的物理RAM不是连续的,需要将分散的多个地址范围联合

设计思路:与Heap_4类似。

  • 需要在申请内存前调用vPortDefineHeapRegions(const HeapRegion_t * const pxHeapRegions),申明所有被用作堆的内存区域。

3. 堆的使用

3.1. 申请内存

函数接口:

void *pvPortMalloc( size_t xSize ) PRIVILEGED_FUNCTION;

该函数会从系统堆中申请xSize个字节的内存,如果申请成功,则返回成功分配的内存地址。

下面则简要地说明5个堆如何实现该接口的。

3.1.1. Heap_1的实现

在介绍具体的实现之前,需要先了解几个比较重要的宏定义和全局静态变量。

  • 对齐后的系统堆大小:这是一个宏定义,用于记录地址对齐调整后的系统堆的大小。其定义如下:可以看到, 这个调整值会使得可用的系统堆偏小, 即堆数组的空间存在浪费. 最极端的例子就是堆的首地址本身就符合对齐要求, 这里却进行了调整, 浪费了portBYTE_ALIGNMENT字节的空间.

    #define configADJUSTED_HEAP_SIZE    ( configTOTAL_HEAP_SIZE - portBYTE_ALIGNMENT )
    
  • 系统堆数组:这个实际上就是系统堆的实际承载者,是一个连续的无符号char数组,大小为configTOTAL_HEAP_SIZE。它也可以在外部定义。

    #if ( configAPPLICATION_ALLOCATED_HEAP == 1 )
        extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
    #else
        static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
    #endif
    
  • 下一个空闲的字节(xNextFreeByte):记录了相对于堆首地址的空闲内存地址的偏移字节数。每当申请内存时更新。

要理解内存管理的实现,最关键的是需要理解空闲块的组织形式。Heap_1的空闲快组织形式如下图:
Heap_1的空闲快组织形式

3.1.2. Heap_2的实现

与Heap_1一样,Heap_2也定义了调整地址对齐后的堆大小configADJUSTED_HEAP_SIZE。也定义了系统堆的承载数组(ucHeap[])。

除此之外,由于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;

系统通过静态全局变量xStartxEnd指向空闲块链表的首部和尾部。首部是一个不存在的块。而尾部xEnd也只是一个结尾标识, 并不占用堆空间。

在前文提到,Heap_2的实现将内存块的元信息嵌入到申请的内存块中,因此需要调整用户所需的内存块的大小,并实现地址对齐,于是定义了元信息已对齐时占用的内存大小heapSTRUCT_SIZE

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

这个空闲块链表最初时(即堆初始化完成后),只有一个空闲块,这个块大小就是整个系统堆的大小。但随着用户不断的申请内存,这个块大小会逐渐被分割,每申请一次就有可能触发一次分割,而这个触发分割的条件便是目标内存块是否大于heapMINIMUN_BLOCK_SZIE,其定义如下:

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

从定义可以看出,只要目标块大于等于两个空闲块元信息的大小,那么表示它还有可能进行下一次分配,所以可以将其分隔。当然, 也不必过于纠结这个值, 只是一个经验值而已.

Heap_2使用变量xFreeBytesRemaining来记录当前剩余的堆空闲:

static size_t xFreeBytesRemaining = configADJUSTED_HEAP_SIZE;

需要注意的是,这个变量并不考虑碎片的情况。

Heap_2的空闲块链表的模型如下图所示:

Heap_2的空闲块链表的模型

3.1.3. Heap_3的实现

Heap_3的实现完全基于C的标准库,只是增加了调度器挂起的动作以确保线程安全。

具体实现如下:

{
    挂起调度器(`vTaskSuspendAll()`)。

    申请内存(`pvReturn = malloc( xWantedSize )`)。

    恢复调度器(`vTaskResumeAll()`)。

    (仅开启USE_MALLOC_FAILED_HOOK)如果申请内存失败(`pvReturn == NULL`),调用钩子函数(`vApplicationMallocFailedHook()`)。

    返回`pvReturn`。
}
3.1.4. Heap_4的实现

Heap_4的实现与Heap_2相似,以相同的形式定义了系统堆的承载数组ucHeap[],也定义了一样的内存块元信息结构体BlockLink_t以及结构体链表的首部(xStart)和尾部(pxEnd)指针。与Heap_2不同的是,空闲链表的尾部pxEnd是一个空闲块,但只占用了系统堆一个内存块的元信息大小的内存。另外, Heap_4的空闲链表不是按照块大小排序的.

为了将内存块元信息嵌入内存块,也定义了该结构体的长度变量(xHeapStructSize)。实现内存分割时,也以heapMINIMUM_BLOCK_SIZE作为分割条件。

为了记录系统堆的空闲内存,也使用了xFreeBytesRemaining进行记录,同样地,其不考虑内存碎片。该实现还增加了xMinimumEverFreeBytesRemaining,其记录的是到目前为止, 最小的空闲内存量,随着内存的释放,也不会增加这个值。通过这个值, 可以知道系统对内存的最大使用压力.

此外,该实现还定义了字节的位数heapBITS_PER_BYTE

#define heapBITS_PER_BYTE   ( ( size_t ) 8 )

与Heap_2不同的是,它还使用了分配位来表示某个块是否已经被分配:

static size_t xBlockAllocatedBit = 0;

这个分配位位于size_t的最高有效位,如果该位为1,表示已分配,0表示未分配。目前只在内存释放时, 用与检查目标内存块的有效性.

Heap_4的空闲块模型如下图所示:

Heap_4的空闲块模型
由于Heap_4使用最多,所以下面以UML活动图的方式给出了其对该函数的实现。

Heap_4的实现活动图

3.1.5. Heap_5的实现

Heap_5的实现与Heap_4非常相似,唯一的不同点在与系统堆的初始化:

void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions ) PRIVILEGED_FUNCTION;

在Heap_5中,该函数必须在申请内存接口被调用之前就调用。

pxHeapRegions是一个HeapRegion_t的数组,定义了Heap_5可用的内存区域。

3.2. 释放内存

函数接口:

void vPortFree( void *pv ) PRIVILEGED_FUNCTION;

该函数将pv指向的内存块归还给系统堆,不同的堆实现的具体行为不一致。

3.2.1. Heap_1的实现

因为不支持内存释放,所以未实现该接口。

3.2.2. Heap_2的实现

先来看看Heap_2是如何将空闲块插入空闲块队列中的(#define prvInsertBlockIntoFreeList( pxBlockToInsert ) ...):为了减少调用栈的深度,这里使用宏来实现这个函数。其主要是遍历链表,找到第一个不小于当前需要被放入的空闲块的大小的位置,将内存块插入其中。这便使得空闲块按照块大小顺序排列。

{
    获取被插入块的长度(`xBlockSize = pxBlockToInsert->xBlockSize`)。

    遍历空闲块链表,直到找到不小于被插入块大小的位置(`for ( pxInterator = &xStart; pxInterator->pxNextFreeBlock->xBlockSize < xBlockSize; pxInterator = pxIterator->pxNextFreeBlock )`)。

    // 此时`pxInterator`已经指向了不小于被插入块大小的内存块的前驱
    将被插入块的下一个空闲块指针指向合适的位置(`pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock`)。
    将空闲块链入空闲块链表(`pxIterator->pxNextFreeBlock = pxBlockToInsert`)。
}

接下来就可以给出Heap_2对释放函数的具体实现如下:

{
    初始化`puc = ( uint8_t * ) pv`。

    如果传入的地址不为空(`pv != NULL`):
    {
        获取该内存块的元信息的首地址(`puc -= heapSTRUCT_SIZE`)。

        获取该内存块的元信息(`pxLink = ( void * ) puc`)。

        挂起调度器(`vTaskSuspendAll()`)。

        将该内存块加入到空闲块链表(`prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) )`)。
        更新系统堆空闲空间(`xFreeBytesRemaining += pxLink->xBlockSize`)。

        恢复调度器(`vTaskResumeAll()`)。
    }
}

从实现中可以看到,Heap_2的内存块释放只是简单地将该内存块加入到空闲块链表,而不会对相邻的内存块进行合并。

3.2.3. Heap_3的实现

如前文所说,Heap_3只是对标准库的一个简单封装,具体实现如下:

{
    如果地址不为空(`pv != NULL`):
    {
        挂起调度器(`vTaskSuspendAll()`)。

        释放内存(`free( pv )`)。

        恢复调度器(`xTaskResumeAll()`)。
    }
}
3.2.4. Heap_4的实现

先看看Heap_4如何将空闲块插入到空闲块链表中(static void prvInsertBlockIntoFreeList( BlockLink_t * pxBlockToInsert )):与Heap_2的实现不同,Heap_4因为更加复杂,所以将其实现为了静态函数。它新增了一个非常重要的功能,就是自动联合相邻的空闲块。下面该函数实现的活动图如下:

Heap_4将空闲块插入到空闲块链表中
基于以上函数,Heap_4对内存释放的具体实现如下:

Heap_4对内存释放的具体实现

3.2.5. Heap_5的实现

与Heap_4一致,请参看Heap_4的实现。

3.3. 内存工具函数

用于实现MPU功能的堆初始化:

void vPortInitialiseBlocks( void ) PRIVILEGED_FUNCTION;

// TODO: 需要进一步研究

获取空闲的堆空间:

size_t xPortGetFreeHeapSize( void ) PRIVILEGED_FUNCTION;
size_t xPortGetMinimumEverFreeHeapSize( void ) PRIVILEGED_FUNCTION;
3.3.1. Heap_1的实现

Heap_1中只实现了vPortInitialiseBlocks()xPortGetFreeHeapSize()

vPortInitialiseBlocks()中,只是简单的将xNextFreeByte = 0,重置了当前的空闲偏移量。

而在xPortGetFreeHeapSize()中,空闲空间的计算也比较简单,即直接返回configADJUSTED_HEAP_SIZE - xNextFreeByte

3.3.2. Heap_2的实现

Heap_2中只实现了xPortGetFreeHeapSize(),只是简单的将xFreeBytesRemaining返回给调用者。

3.3.3. Heap_3的实现

不实现工具函数。

3.3.4. Heap_4的实现

Heap_4实现了xPortGetFreeHeapSize()xPortGetMinimumEverFreeHeapSize(),即直接返回xFreeBytesRemainingxMinimumEverFreeBytesRemaining

3.3.5. Heap_5的实现

请参看Heap_4的实现。

4. 内存管理的实现细节

4.1. 内存管理的接口

4.1.1. 内存管理的数据结构接口
4.1.1.1. 堆区域结构体(HeapRegion_t)

定义如下:

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

该结构体用于实现Heap_5(物理内存地址不连续的堆)。一个结构体则定义了一个可用的内存区域,最终的实现将会是通过一个HeapRegion_t数组,定义一组可用的内存区域作为Heap_5的堆空间。

该数组定义时有两点要求:

  • 以大小为0的区域终止;
  • 必须将这些内存区域以区域首地址升序排列。
4.1.2. 内存管理的接口宏
4.1.2.1. 内存管理的接口常量

定义内存地址对齐的常量:

#if portBYTE_ALIGNMENT == 32
    #define portBYTE_ALIGNMENT_MASK ( 0x001F )
#endif

#if portBYTE_ALIGNMENT == 16
    #define portBYTE_ALIGNMENT_MASK ( 0x000F )
#endif

#if portBYTE_ALIGNMENT == 8
    #define portBYTE_ALIGNMENT_MASK ( 0x0007 )
#endif

#if portBYTE_ALIGNMENT == 4
    #define portBYTE_ALIGNMENT_MASK ( 0x0003 )
#endif

#if portBYTE_ALIGNMENT == 2
    #define portBYTE_ALIGNMENT_MASK ( 0x0001 )
#endif

#if portBYTE_ALIGNMENT == 1
    #define portBYTE_ALIGNMENT_MASK ( 0x0000 )
#endif

#ifndef portBYTE_ALIGNMENT_MASK
    #error "Invalid portBYTE_ALIGNMENT definition"
#endif

这些掩码用于实现堆内存地址的对齐。

下面这个宏定义了Heap_5支持的内存区域个数:

#ifndef portNUM_CONFIGURABLE_REGIONS
    #define portNUM_CONFIGURABLE_REGIONS 1
#endif
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值