FreeRTOS内存管理

在计算系统中,变量、中间数据一般存放在系统存储空间中,只有在实际使用时才将它们从存储空间调入到中央处理器内部进行运算。

通常存储空间可以分为两种:内部存储空间和外部存储空间。

  • 内部存储空间访问速度比较快,能够按照变量地址随机地访问,也就是我们通常所说的 RAM(随机存储器)
  • 外部存储空间内所保存的内容相对来说比较固定,即使掉电后数据也不会丢失。

内存管理主要面向的是RAM的管理。

FreeRTOS 操作系统将内核与内存管理分开实现,操作系统内核仅规定了必要的内存管理函数原型,而不关心这些内存管理函数是如何实现的,所以在 FreeRTOS 中提供了多种内存分配算法(分配策略),但是上层接口(API)却是统一的。

在嵌入式程序设计中内存分配应该是根据所设计系统的特点来决定选择使用动态内存分配还是静态内存分配算法,一些可靠性要求非常高的系统应选择使用静态的,而普通的业务系统可以使用动态来提高内存使用效率。静态可以保证设备的可靠性但是需要考虑内存上限,内存使用效率低,而动态则是相反。RTOS中的内存管理模块管理用于系统中的内存资源,主要报错内存的初始化、分配以及释放

为什么不使用malloc()和free()来进行分配以及释放内存?

在嵌入式实时操作系统中,调用malloc()和free()其实是比较危险的,其主要原因有如下:

  • 这些函数在小型嵌入式系统中并不总是可用的,小型嵌入式设备中的 RAM 不足
  • 其实现所需要的依赖太多,占用大部分的代码空间
  • 几乎都不安全
  • 可能产生碎片
  • 会使得链接器配置变得复杂
  • 如果允许堆空间的生长方向覆盖其他变量占据的内存,它们会成为 debug 的灾难

在一般的实时嵌入式系统中,由于实时性的要求,很少使用虚拟内存机制。所有的内存都需要用户参与分配,直接操作物理内存,所分配的内存不能超过系统的物理内存,所有的系统堆栈的管理,都由用户自己管理。在嵌入式实时操作系统中,对内存分配时间要求更为严格,即分配内存的时间必须确定。一般内存管理算法是根据需要存储的数据的长度在内存中去寻找一个与这段数据相适应的空闲内存块,然后将数据存储在里面。而寻找这样一个空闲内存块所耗费的时间是不确定的。实时系统必须要保证内存块的分配过程在可预测的确定时间内完成,否则实时任务对外部事件的响应也将变得不可确定。

在嵌入式系统中,内存是有限的,在分配过程中会不断被分配以及不断被释放,整个系统内存区域会产生越来越多的碎片,导致地址不连续,不能够作为一整个大的内存分配出去,容易导致系统瘫痪,所以需要一个良好的内存分配算法来避免这种情况发生。其实这都是因为malloc和free的设计上考虑,它们是基于列表分配算法的,该算法将内存池组织到单个链表中的连续位置,分配器管理该链表,每次分配实际上就是寻找空闲位置。

不同的嵌入式系统具有不同的内存配置和时间要求。所以单一的内存分配算法只可能适合部分应用程序。FreeRTOS 将内存分配作为可移植层面,有针对性的提供了不同的内存分配管理算法。RTOS提供了物种内存管理算法,分别是heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c,其源文件位于FreeRTOS\Source\portable\MemMang 路径下,选择一个添加即可。

内存管理应用场景

内存管理的主要工作是动态划分并管理用户分配好的内存区间,主要是在用户需要使用大小不等的内存块的场景中使用,当用户需要分配内存时,可以通过操作系统的内存申请函数索取指定大小内存块,一旦使用完毕,通过动态内存释放函数归还所占用内存,使之可以重复使用(heap_1.c 的内存管理除外)。

  • 静态分配内存:定义数组,分配固定大小的内存。这种在大多数情况下会浪费大量的内存空间,在少数情况下会发生内存不够大,导致数组越界的情况。
  • 动态内存分配:所谓动态内存分配就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。其分配由系统根据的需要即使分配,且分配的大小就是程序要求的大小。

RTOS内存管理方案

RTOS所规定的内存管理函数接口:

void *pvPortMalloc( size_t xSize ); 				//内存申请函数
void vPortFree( void *pv ); 						//内存释放函数
void vPortInitialiseBlocks( void ); 				//初始化内存堆函数
size_t xPortGetFreeHeapSize( void ); 				//获取当前未分配的内存堆大小
size_t xPortGetMinimumEverFreeHeapSize( void ); 	//获取未分配的内存堆历史最小值

FreeRTOS 提供的内存管理都是从内存堆中分配内存的。对于 heap_1.c、heap_2.c 和 heap_4.c 这三种内存管理方案,内存堆实际上是一个很大的数组 , 定 义为 static uint8_t ucHeap[ configTOTAL_HEAP_SIZE] , 而宏定义configTOTAL_HEAP_SIZE 则表示系统管理内存大小,单位为字,在 FreeRTOSConfig.h 中由用户设定。

对于 heap_3.c 这种内存管理方案,它封装了 C 标准库中的 malloc()free()函数,封装后的 malloc()和 free()函数具备保护,可以安全在嵌入式系统中执行。因此,用户需要通过编译器或者启动文件设置堆空间。

heap_5.c 方案允许用户使用多个非连续内存堆空间,每个内存堆的起始地址和大小由用户定义。这种应用其实还是很大的,比如做图形显示、GUI 等,可能芯片内部的 RAM是不够用户使用的,需要外部 SDRAM,那这种内存管理方案则比较合适。

heap_1.c

heap_1.c 管理方案是 FreeRTOS 提供所有内存管理方案中最简单的一个,它只能申请内存而不能进行内存释放,并且申请内存的时间是一个常量,其系统稳定性较高,且不会产生内存碎片,但是内存利用率低,只能用于内存申请的地方,内存也只能用一次,无法回收利用。实际上,大多数的嵌入式系统并不会经常动态申请与释放内存,一般都是在系统完成的时候,就一直使用下去,所以这个方案使用非常广泛。

其特点如下:

  • 用于从不删除任务、队列、信号量、互斥量等的应用程序(实际上大多数使用FreeRTOS 的应用程序都符合这个条件)。
  • 函数的执行时间是确定的并且不会产生内存碎片。

数据结构

heap_1.c 管理方案使用两个静态变量对系统管理的内存进行跟踪内存分配:

static size_t 		xNextFreeByte = ( size_t ) 0;
static uint8_t * 	pucAlignedHeap = NULL
  • 变量 xNextFreeByte 用来定位下一个空闲的内存堆位置。真正的运作过程是记录已经被分配的内存大小,在每次申请内存成功后,都会增加申请内存的字节数目。内存堆其实是一个大数组,只需要知道已分配内存的大小,就可以用它作为偏移量找到未分配内存的起始地址。
  • 静态变量 pucAlignedHeap 是一个指向对齐后的内存堆起始地址,我们使用一个数组作为堆内存,但是数组的起始地址并不一定是对齐的内存地址,所以我们需要得到FreeRTOS 管理的内存空间对齐后的起始地址,并且保存在静态变量 pucAlignedHeap 中。对齐会使得为大多数硬件访问内存对齐的数据速度会更快,为了提高性能,不同的硬件架构的内存对齐操作可能不一样,对于 Cortex-M3架构,进行 8 字节对齐。

内存申请 pvPortMalloc()

内存申请函数就是用于申请一块用户指定大小的内存空间,当系统管理的内存空间满足用户需要的大小的时候,就能申请成功,并且返回内存空间的起始地址

void * pvPortMalloc( size_t xWantedSize )
{
    void * pvReturn = NULL;
    static uint8_t * pucAlignedHeap = NULL;

    /* 如果内存对齐字节!=1,即申请内存不是 1 字节对齐,那么就把要申请的内存大小(xWantedSize)按照要求对齐 */
    #if ( portBYTE_ALIGNMENT != 1 )
    {
        if( xWantedSize & portBYTE_ALIGNMENT_MASK )
        {
            /* Byte alignment required. Check for overflow. */
            if( ( xWantedSize + ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) ) ) > xWantedSize )
            {
                xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
            }
            else
            {
                xWantedSize = 0;
            }
        }
    }
    #endif /* if ( portBYTE_ALIGNMENT != 1 ) */
	// 挂起调度器
    vTaskSuspendAll();
    {
        if( pucAlignedHeap == NULL )
        {
            /* 第一次使用,确保内存堆起始位置正确对齐,
            系统需要保证 pucAlignedHeap 也是在按照指定内存要求对齐的,
            通过这里可以知道,初始化 pucAlignedHeap 时并不是一定等于&ucHeap[0]的,
            而是会根据字节对齐的要求,在&ucHeap[0]和&ucHeap[portBYTE_ALIGNMENT]之间*/
            pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) & ucHeap[ portBYTE_ALIGNMENT - 1 ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
        }

        /*边界检测,如果已经使用的内存空间 + 新申请的内存大小 < 系统能够提供的内存大小,那么就从数组中取一块*/
        if( ( xWantedSize > 0 ) &&                                /* valid size */
            ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) &&
            ( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) ) /* Check for overflow. */
        {
            /*  获取申请的内存空间起始地址并且保存在返回值中*/
            pvReturn = pucAlignedHeap + xNextFreeByte;
            // 更新索引
            xNextFreeByte += xWantedSize;
        }
		// 恢复调度器运行
        traceMALLOC( pvReturn, xWantedSize );
    }
    ( void ) xTaskResumeAll();
	//如果内存分配不成功,这里最可能是内存堆空间不够用了。如果
	//用户使能了内存申请失败钩子函数这个宏定义,那么在内存申请失败的时候会调用
	//vApplicationMallocFailedHook()钩子函数,这个钩子函数由用户实现,通常可以输出内存申
	//请失败的相关提示。
    #if ( configUSE_MALLOC_FAILED_HOOK == 1 )
    {
        if( pvReturn == NULL )
        {
            vApplicationMallocFailedHook();
        }
    }
    #endif
	//返回申请成功的内存起始地址
    return pvReturn;
}

如果系统要求内存对齐的字节不是按 1 字节对齐,那么就把要申请的内存大小 xWantedSize 按照要求对齐。举个例子,如果系统设置按 8 字节对齐,我们本来想要申请的内存大小 xWantedSize 是 30 个字节,与 portBYTE_ALIGNMENT_MASK相与的结果是 2,这代表着我们申请的内存与系统设定对齐不一致,为了内存统一对齐,系统会再多给我们分配 2 个字节,也就是 32个字节。

在 使 用 内 存 申 请 函 数 之 前 , 需 要 将 管 理 的 内 存 进 行 初 始 化 , 需 要 将 变 量pucAlignedHeap 指向内存域第一个地址对齐处,因为系统管理的内存其实是一个大数组,而编译器为这个数组分配的起始地址是随机的,不一定符合系统的对齐要求,这时候要进行内存地址对齐操作。

在这里插入图片描述

其他函数

void vPortFree( void * pv )
{
    /*heap_1.c 采用的内存管理算法中不支持释放内存,啥都不干 */
    ( void ) pv;

    /* Force an assert as it is invalid to call this function. */
    configASSERT( pv == NULL );
}
/*-----------------------------------------------------------*/

void vPortInitialiseBlocks( void )
{
    /* 将静态局部变量 xNextFreeByte 设置为 0,表示内存没有被申请 */
    xNextFreeByte = ( size_t ) 0;
}
/*-----------------------------------------------------------*/

size_t xPortGetFreeHeapSize( void )
{
    // 是获取当前未分配的内存堆大小
    return( configADJUSTED_HEAP_SIZE - xNextFreeByte );
}

heap_2.c

该内存管理使用的是最佳匹配算法(best fit algorithm),比如我们申请 100 字节的内存,而可申请内存中有三块对应大小 200 字节, 500 字节和 1000 字节大小的内存块,按照算法的最佳匹配,这时候系统会把 200 字节大小的内存块进行分割并返回申请内存的起始地址,剩余的内存则插回链表留待下次申请。Heap_2.c 方案支持释放申请的内存,但是它不能把相邻的两个小的内存块合成一个大的内存块对于每次申请内存大小都比较固定的,这个方式是没有问题的,而对于每次申请并不是固定内存大小的则会造成内存碎片,heap_4.c 方案采用的内存管理算法能解决内存碎片的问题,可以把这些释放的相邻的小的内存块合并成一个大的内存块

同样的 ,内存分配时需 要的总的内存堆 空间由文件 FreeRTOSConfig.h 中的宏configTOTAL_HEAP_SIZE 配置,单位为字。通过调用函数 xPortGetFreeHeapSize() 我们可以知道还剩下多少内存没有使用,但是并不包括内存碎片,这样一来我们可以实时的调整和优化 configTOTAL_HEAP_SIZE 的大小。

其特点如下:

  • 可以用在那些反复的删除任务、队列、信号量、等内核对象且不担心内存碎片的应用程序。
  • 如果我们的应用程序中的队列、任务、信号量、等工作在一个不可预料的顺序,这样子也有可能会导致内存碎片。
  • 具有不确定性,但是效率比标准 C 库中的 malloc 函数高得多
  • 不能用于那些内存分配和释放是随机大小的应用程序。

数据结构

heap_2.c 方案与 heap_1 方案在内存堆初始化的时候操作都是一样的,在内存中开辟了一个静态数组作为堆的空间,大小由用户定义,然后进行字节对齐处理。

heap_2.c 方案采用链表的数据结构记录空闲内存块,将所有的空闲内存块组成一个空闲内存块链表,FreeRTOS 采用 2 个 BlockLink_t 类型的局部静态变量 xStart、xEnd 来标识空闲内存块链表的起始位置与结束位置。

typedef struct A_BLOCK_LINK
{
    struct A_BLOCK_LINK * pxNextFreeBlock; /*指向下一个空闲内存块的指针 */
    size_t xBlockSize;                     /*用于记录申请的内存块的大小,包括链表结构体大小 */
} BlockLink_t;

PRIVILEGED_DATA static BlockLink_t xStart, xEnd;
PRIVILEGED_DATA static size_t xFreeBytesRemaining = configADJUSTED_HEAP_SIZE;		//未分配内存大小

内存申请函数 pvPortMalloc()

heap_2.c 内存管理方案采用最佳匹配算法管理内存,系统会先从内存块空闲链表头开始进行遍历,查找符合用户申请大小的内存块(内存块空闲链表按内存块大小升序排列,所以最先返回的的块一定是最符合申请内存大小)。当找到内存块的时候,返回该内存块偏移 heapSTRUCT_SIZE 个字节后的地址,因为在每块内存块前面预留的节点是用于记录内存块的信息,用户不需要也不允许操作这部分内存。

在申请内存成功的同时系统还会判断当前这块内存是否有剩余(大于一个链表节点所需内存空间),这样子就表示剩下的内存块还是能存放东西的,也要将其利用起来。如果有剩余的内存空间,系统会将内存块进行分割,在剩余的内存块头部添加一个内存节点,并且完善该空闲内存块的信息,然后将其按内存块大小插入内存块空闲链表中,供下次分配使用,其中prvInsertBlockIntoFreeList()这个函数就是把节点按大小插入到链表中。

void * pvPortMalloc( size_t xWantedSize )
{
    BlockLink_t * pxBlock;
    BlockLink_t * pxPreviousBlock;
    BlockLink_t * pxNewBlockLink;
    PRIVILEGED_DATA static BaseType_t xHeapHasBeenInitialised = pdFALSE;
    void * pvReturn = NULL;
    size_t xAdditionalRequiredSize;
    
	// 挂起调度器
    vTaskSuspendAll();
    {
        /* 如果是第一次调用内存分配函数,先初始化内存堆 */
        if( xHeapHasBeenInitialised == pdFALSE )
        {
            prvHeapInit();
            xHeapHasBeenInitialised = pdTRUE;
        }

        if( xWantedSize > 0 )
        {
            /* 调整要分配的内存值,需要增加上链表结构体所占的内存空间
             heapSTRUCT_SIZE 表示链表结构体节点经过内存对齐后的内存大小
            因为空余内存的头部要放一个 BlockLink_t 类型的节点来管理,
            因此这里需要人为的扩充下申请的内存大小*/
            // // 需要申请的内存大小与系统要求对齐的字节数不匹配,需要进行内存对齐
            xAdditionalRequiredSize = heapSTRUCT_SIZE + portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK );
			
 			// 判断是否溢出
            if( heapADD_WILL_OVERFLOW( xWantedSize, xAdditionalRequiredSize ) == 0 )
            {
                xWantedSize += xAdditionalRequiredSize;
            }
            else
            {
                xWantedSize = 0;
            }
        }

        if( heapBLOCK_SIZE_IS_VALID( xWantedSize ) != 0 )
        {
            //如果当前的空闲内存足够满足用户申请的内存大小,就进行内存申请操作
            if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
            {
                /* 从空余内存链表的头部开始找,如果该空余内存的大小>xWantedSize,
                 就从这块内存中抠出一部分内存返回,剩余的内存生成新的 BlockLink_t 插入链表中 */
                pxPreviousBlock = &xStart;
                pxBlock = xStart.pxNextFreeBlock;
				//从链表头部开始查找大小符合条件的空余内存
                while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
                {
                    pxPreviousBlock = pxBlock;
                    pxBlock = pxBlock->pxNextFreeBlock;
                }
                
                /*如果搜索到链表尾 xEnd,说明没有找到合适的空闲内存块,否则进行下一步处理*/
                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 );

                        /* 通过计算得到剩余的内存大小,并且赋值给剩余内存块链表节点中的 xBlockSize 成员变量,方便下一次的内存查找*/
                        pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
                        pxBlock->xBlockSize = xWantedSize;

                        /* 将被切割而产生的新空闲内存块添加到空闲链表中 */
                        prvInsertBlockIntoFreeList( ( pxNewBlockLink ) );
                    }

                    xFreeBytesRemaining -= pxBlock->xBlockSize;

                    /* The block is being returned - it is allocated and owned
                     * by the application and has no "next" block. */
                    heapALLOCATE_BLOCK( pxBlock );
                    pxBlock->pxNextFreeBlock = NULL;
                }
            }
        }

        traceMALLOC( pvReturn, xWantedSize );
    }
    ( void ) xTaskResumeAll();
	//如果内存分配不成功,这里最可能是内存堆空间不够用了。如果
	//用户使能了内存申请失败钩子函数这个宏定义,那么在内存申请失败的时候会调用
	//vApplicationMallocFailedHook()钩子函数,这个钩子函数由用户实现,通常可以输出内存申
	//请失败的相关提示。
    #if ( configUSE_MALLOC_FAILED_HOOK == 1 )
    {
        if( pvReturn == NULL )
        {
            vApplicationMallocFailedHook();
        }
    }
    #endif

    return pvReturn;
}

内存初始化函数prvHeapInit()

static void prvHeapInit( void ) /* PRIVILEGED_FUNCTION */
{
    BlockLink_t * pxFirstFreeBlock;
    uint8_t * pucAlignedHeap;

    /* 保证 pucAlignedHeap 也是按照指定内存要求对齐的 */
    pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) & ucHeap[ portBYTE_ALIGNMENT - 1 ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );

    /*空闲内存链表头部初始化 */
    xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
    xStart.xBlockSize = ( size_t ) 0;

    /*  空闲内存链表尾部初始化 */
    xEnd.xBlockSize = configADJUSTED_HEAP_SIZE;
    xEnd.pxNextFreeBlock = NULL;

    /*将 pxFirstFreeBlock 放入空闲链表中,因为空闲内存块链表除了要有头部与尾部,
     还需要有真正可用的内存,而第一块可用的内存就是 pxFirstFreeBlock,
     pxFirstFreeBlock 的大小是系统管理的内存大小 configADJUSTED_HEAP_SIZE*/
    pxFirstFreeBlock = ( BlockLink_t * ) pucAlignedHeap;
    pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE;
    pxFirstFreeBlock->pxNextFreeBlock = &xEnd;
}

在这里插入图片描述

内存释放函数vPortFree()

只需要向内存释放函数中传入要释放的内存地址,那么系统会自动向前索引到对应链表节点,并且取出这块内存块的信息,将这个节点插入到空闲内存块链表中,将这个内存块归还给系统。

void vPortFree( void * pv )
{
    uint8_t * puc = ( uint8_t * ) pv;
    BlockLink_t * pxLink;

    if( pv != NULL )
    {
        /* 根据要释放的内存块找到对应的链表节点 */
        puc -= heapSTRUCT_SIZE;
        pxLink = ( void * ) puc;

        configASSERT( heapBLOCK_IS_ALLOCATED( pxLink ) != 0 );
        configASSERT( pxLink->pxNextFreeBlock == NULL );

        if( heapBLOCK_IS_ALLOCATED( pxLink ) != 0 )
        {
            if( pxLink->pxNextFreeBlock == NULL )
            {
                /* The block is being returned to the heap - it is no longer
                 * allocated. */
                heapFREE_BLOCK( pxLink );
                #if ( configHEAP_CLEAR_MEMORY_ON_FREE == 1 )
                {
                    ( void ) memset( puc + heapSTRUCT_SIZE, 0, pxLink->xBlockSize - heapSTRUCT_SIZE );
                }
                #endif

                vTaskSuspendAll();
                {
                    /* 将要释放的内存块添加到空闲链表*/
                    prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
                    // 更新一下当前的未分配的内存大小
                    xFreeBytesRemaining += pxLink->xBlockSize;
                    traceFREE( pv, pxLink->xBlockSize );
                }
                ( void ) xTaskResumeAll();
            }
        }
    }
}

从内存的申请与释放看来,heap_2.c 方案采用的内存管理算法虽然是高效但还是有缺陷的,由于在释放内存时不会将相邻的内存块合并,所以这可能造成内存碎片,要求用户每次创建或释放的任务、队列等必须大小相同如果分配或释放的内存是随机的,绝对不可以用这种内存管理策略。

heap_3.c

heap_3.c 方案只是简单的封装了标准 C 库中的 malloc()和 free()函数,并且能满足常用的编译器。重新封装后的 malloc()和 free()函数具有保护功能,采用的封装方式是操作内存前挂起调度器、完成后再恢复调度器。

特点:

  • 需要链接器设置一个堆,malloc()和 free()函数由编译器提供。
  • 具有不确定性。
  • 很可能增大 RTOS 内核的代码大小。

在使用这种模式时,FreeRTOSConfig.h 文件中的configTOTAL_HEAP_SIZE 宏定义不起作用。使用的是启动文件中设置的堆的大小,单位为字节。

void * pvPortMalloc( size_t xWantedSize )
{
    void * pvReturn;

    vTaskSuspendAll();
    {
        pvReturn = malloc( xWantedSize );
        traceMALLOC( pvReturn, xWantedSize );
    }
    ( void ) xTaskResumeAll();

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

    return pvReturn;
}

void vPortFree( void * pv )
{
    if( pv != NULL )
    {
        vTaskSuspendAll();
        {
            free( pv );
            traceFREE( pv, 0 );
        }
        ( void ) xTaskResumeAll();
    }
}

heap_4.c

heap_4.c 方案与 heap_2.c 方案一样都采用最佳匹配算法来实现动态的内存分配,但是不一样的是 heap_4.c 方案还包含了一种合并算法,能把相邻的空闲的内存块合并成一个更大的块,这样可以减少内存碎片。内存分配时需要的总的堆空间由文件 FreeRTOSConfig.h中的宏configTOTAL_HEAP_SIZE 配置,单位为字。通过调用函数 xPortGetFreeHeapSize() 我们可以知道还剩下多少内存没有使用,但是并不包括内存碎片

heap_4.c 方案的空闲内存块也是以单链表的形式连接起来的,BlockLink_t 类型的局部静态变量 xStart 表示链表头,但 heap_4.c 内存管理方案的链表尾部则保存在内存堆空间最后位置,并使用 BlockLink_t 指针类型局部静态变量 pxEnd 指向这个区域。

heap_4.c 内存管理方案的空闲块链表不是以内存块大小进行排序的,而是以内存块起始地址大小排序,内存地址小的在前,地址大的在后,因为 heap_4.c 方案还有一个内存合并算法,在释放内存的时候,假如相邻的两个空闲内存块在地址上是连续的,那么就可以合并为一个内存块。

其特点有:

  • 可用于重复删除任务、队列、信号量、互斥量等的应用程序。
  • 可用于分配和释放随机字节内存的应用程序,但并不像 heap2.c 那样产生严重的内存碎片。
  • 具有不确定性,但是效率比标准 C 库中的 malloc 函数高得多。

数据结构

PRIVILEGED_DATA static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
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;

PRIVILEGED_DATA static BlockLink_t xStart;
PRIVILEGED_DATA static BlockLink_t * pxEnd = NULL;

内存申请函数pvPortMalloc()

heap_4.c 方案的内存申请函数与 heap_2.c 方案的内存申请函数大同小异,同样是从链表头 xStart 开始遍历查找合适的内存块,如果某个空闲内存块的大小能容得下用户要申请的内存,则将这块内存取出用户需要内存空间大小的部分返回给用户,剩下的内存块组成一个新的空闲块,按照空闲内存块起始地址大小顺序插入到空闲块链表中,内存地址小的在前,内存地址大的在后。(对于空闲块都不浪费)

在插入到空闲内存块链表的过程中,系统还会执行合并算法将地址相邻的内存块进行合并:判断这个空闲内存块是相邻的空闲内存块合并成一个大内存块,如果可以则合并,合并算法是 heap_4.c 内存管理方案和 heap_2.c 内存管理方案最大的不同之处,这样一来,会导致的内存碎片就会大大减少,内存管理方案适用性就很强,能一样随机申请和释放内存的应用中,灵活性得到大大的提高。

此算法可以较好的解决了上述内存碎片的问题,不放过一点内存。

void * pvPortMalloc( size_t xWantedSize )
{
    BlockLink_t * pxBlock;
    BlockLink_t * pxPreviousBlock;
    BlockLink_t * pxNewBlockLink;
    void * pvReturn = NULL;
    size_t xAdditionalRequiredSize;

    vTaskSuspendAll();
    {
        /* 如果是第一次调用内存分配函数,先初始化内存堆 */
        if( pxEnd == NULL )
        {
            prvHeapInit();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }

        if( xWantedSize > 0 )
        {
            /* 内存对齐 + 管理块的大小 */
            xAdditionalRequiredSize = xHeapStructSize + portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK );

            if( heapADD_WILL_OVERFLOW( xWantedSize, xAdditionalRequiredSize ) == 0 )
            {
                xWantedSize += xAdditionalRequiredSize;
            }
            else
            {
                xWantedSize = 0;
            }
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }

        /* 检测当前内存大小是否符合要求,需要大于规定的最小值 */
        if( heapBLOCK_SIZE_IS_VALID( xWantedSize ) != 0 )
        {
            // 如果当前的空闲内存足够满足用户申请的内存大小,就进行内存申请操作
            if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
            {
                /* 从空余内存链表的头部开始找,如果该空余内存的大小>xWantedSize,
                 就从这块内存中抠出一部分内存返回,剩余的内存生成新的 BlockLink_t 插入链表中 */
                pxPreviousBlock = &xStart;
                pxBlock = xStart.pxNextFreeBlock;
				// 从链表头部开始查找大小符合条件的空余内存
                while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
                {
                    pxPreviousBlock = pxBlock;
                    pxBlock = pxBlock->pxNextFreeBlock;
                }

                /* 如果搜索到链表尾 xEnd,说明没有找到合适的空闲内存块,否则进行下一步处 */
                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 );
                        configASSERT( ( ( ( size_t ) pxNewBlockLink ) & portBYTE_ALIGNMENT_MASK ) == 0 );

                        /* 通过计算得到剩余的内存大小,并且赋值给剩余内存块链表节点中
                         的 xBlockSize 成员变量,方便下一次的内存查找*/
                        pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
                        pxBlock->xBlockSize = xWantedSize;

                        /* 将被切割而产生的新空闲内存块添加到空闲链表中 */
                        prvInsertBlockIntoFreeList( pxNewBlockLink );
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
					// 更新剩余内存总大小
                    xFreeBytesRemaining -= pxBlock->xBlockSize;
					// 如果当前内存大小小于历史最小记录,更新历史最小内存记录
                    if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
                    {
                        xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }

                    /* 注意这里的 xBlockSize 的最高位被设置为 1,标记内存已经被申请使用 */
                    heapALLOCATE_BLOCK( pxBlock );
                    pxBlock->pxNextFreeBlock = NULL;
                    xNumberOfSuccessfulAllocations++;
                }
                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 )
        {
            vApplicationMallocFailedHook();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    #endif /* if ( configUSE_MALLOC_FAILED_HOOK == 1 ) */

    configASSERT( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 );
    return pvReturn;
}
#define heapALLOCATE_BLOCK( pxBlock )            ( ( pxBlock->xBlockSize ) |= heapBLOCK_ALLOCATED_BITMASK )
  • xBlockAllocatedBit:这个变量在内存堆初始化的时候被初始化,初始化将它能表示的数值的最高位置 1。比如对于 32 位系统,这个变量被初始化为 0x80000000(最高位为 1)。heap_4.c 内存管理方案使用 xBlockAllocatedBit 来标识一个内存块是否已经被分配使用了(是否为空闲内存块),如果内存块已经被分配出去,则该内存块上的链表节点的成员变量 xBlockSize 会按位或上这个变量(即xBlockSize 最高位置 1),而在释放一个内存块时,则会把 xBlockSize 的最高位清零,表示内存块是空闲的。

内存初始化函数 prvHeapInit()

static void prvHeapInit( void )
{
    BlockLink_t * pxFirstFreeBlock;
    uint8_t * pucAlignedHeap;
    portPOINTER_SIZE_TYPE uxAddress;
    size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;

    /* 进行内存对齐操作 */
    uxAddress = ( portPOINTER_SIZE_TYPE ) ucHeap;

    if( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 )
    {
        uxAddress += ( portBYTE_ALIGNMENT - 1 );
        uxAddress &= ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK );
        // xTotalHeapSize 表示系统管理的总内存大小
        xTotalHeapSize -= uxAddress - ( portPOINTER_SIZE_TYPE ) ucHeap;
    }

    pucAlignedHeap = ( uint8_t * ) uxAddress;

    //初始化链表头部
    xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
    xStart.xBlockSize = ( size_t ) 0;

    /* 初始化 pxEnd,计算 pxEnd 的位置,它的值为内存尾部向前偏移一个
     BlockLink_t 结构体大小,偏移出来的这个 BlockLink_t 就是 pxEnd */
    uxAddress = ( ( portPOINTER_SIZE_TYPE ) pucAlignedHeap ) + xTotalHeapSize;
    uxAddress -= xHeapStructSize;
    uxAddress &= ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK );
    pxEnd = ( BlockLink_t * ) uxAddress;
    pxEnd->xBlockSize = 0;
    pxEnd->pxNextFreeBlock = NULL;

    /* 和 heap_2.c 中的初始化类似,将当前所有内存插入空闲内存块链表中。
    不同的是链表的尾部不是静态的,而是放在了内存的最后。 */
    pxFirstFreeBlock = ( BlockLink_t * ) pucAlignedHeap;
    pxFirstFreeBlock->xBlockSize = ( size_t ) ( uxAddress - ( portPOINTER_SIZE_TYPE ) pxFirstFreeBlock );
    pxFirstFreeBlock->pxNextFreeBlock = pxEnd;

    /* 更新统计变量 */
    xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
    xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
}

在这里插入图片描述

内存合并函数prvInsertBlockIntoFreeList()

static void prvInsertBlockIntoFreeList( BlockLink_t * pxBlockToInsert ) /* PRIVILEGED_FUNCTION */
{
    BlockLink_t * pxIterator;
    uint8_t * puc;

    /* 首先找到和 pxBlockToInsert 相邻的前一个空闲内存 */
    for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
    {
        /* Nothing to do here, just iterate to the right position. */
    }

    /* Do the block being inserted, and the block it is being inserted after
     * make a contiguous block of memory? */
    puc = ( uint8_t * ) pxIterator;
    
	//如果前一个内存的尾部恰好是 pxBlockToInsert 的头部,那代表这两个内存是连续的,可以合并
    if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
    {
        // 将 pxBlockToInsert 合并入 pxIterator 中
        pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
        pxBlockToInsert = pxIterator;
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    /* 判断 pxBlockToInsert 是否和后面的空闲内存相邻 */
    puc = ( uint8_t * ) pxBlockToInsert;

    if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock )
    {
        // 与之相邻的下一个内存块不是链表尾节点
        if( pxIterator->pxNextFreeBlock != pxEnd )
        {
            /* 将后面的内存合入 pxBlockToInsert,并用 pxBlockToInsert 代替该内存在链表中的位置 */
            pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;
            pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock;
        }
        else
        {
            pxBlockToInsert->pxNextFreeBlock = pxEnd;
        }
    }
    else
    {
        // 后面不相邻,那么只能插入链表了
        pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
    }

    /*  判断下前面是否已经合并了,如果合并了,就不用再更新链表了 */
    if( pxIterator != pxBlockToInsert )
    {
        pxIterator->pxNextFreeBlock = pxBlockToInsert;
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }
}

其实,这个合并的算法常用于释放内存的合并,申请内存的时候能合并的早已合并,因为申请内存是从一个空闲内存块前面分割,分割后产生的内存块都是一整块的,基本不会进行合并

在这里插入图片描述

内存释放函数vPortFree()

根据传入要释放的内存块地址,偏移之后找到链表节点,然后将这个内存块插入到空闲内存块链表中,在内存块插入过程中会执行合并算法。最后是将这个内存块标志为“空闲”(内存块节点的 xBlockSize 成员变量最高位清 0)、再更新未分配的内存堆大小即可

void vPortFree( void * pv )
{
    uint8_t * puc = ( uint8_t * ) pv;
    BlockLink_t * pxLink;

    if( pv != NULL )
    {
        /* 偏移得到节点地址 */
        puc -= xHeapStructSize;

        /* This casting is to keep the compiler from issuing warnings. */
        pxLink = ( void * ) puc;

        configASSERT( heapBLOCK_IS_ALLOCATED( pxLink ) != 0 );
        configASSERT( pxLink->pxNextFreeBlock == NULL );
		
        // 判断一下内存块是否已经是被分配使用的,如果是就释放该内存块
        if( heapBLOCK_IS_ALLOCATED( pxLink ) != 0 )
        {
            if( pxLink->pxNextFreeBlock == NULL )
            {
                /*将内存块标识为空闲 */
                // #define heapFREE_BLOCK( pxBlock )                ( ( pxBlock->xBlockSize ) &= ~heapBLOCK_ALLOCATED_BITMASK )
                heapFREE_BLOCK( pxLink );
                #if ( configHEAP_CLEAR_MEMORY_ON_FREE == 1 )
                {
                    ( void ) memset( puc + xHeapStructSize, 0, pxLink->xBlockSize - xHeapStructSize );
                }
                #endif

                vTaskSuspendAll();
                {
                    /* 更新系统当前空闲内存的大小,添加到内存块空闲链表中 */
                    xFreeBytesRemaining += pxLink->xBlockSize;
                    traceFREE( pv, pxLink->xBlockSize );
                    // 调用 prvInsertBlockIntoFreeList()函数将释放的内存块添加到空闲
					// 内存块链表中,在这过程中,如果内存块可以合并就会进行内存块合并,否则就单纯插入
					// 空闲内存块链表(按内存地址排序)		
                    prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
                    xNumberOfSuccessfulFrees++;
                }
                ( void ) xTaskResumeAll();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
}

按照内存释放的过程,当我们释放一个内存时,如果与它相邻的内存块都不是空闲的,那么该内存块并不会合并,只会被添加到空闲内存块链表中。而如果某个时间段释放了另一个内存块,发现该内存块前面有一个空闲内存块与它在地址上是连续的,那么这两个内存块会合并成一个大的内存块,并插入空闲内存块链表中。他的合并取决于前一个是否具有空闲内存块

heap_5.c

heap_5.c 方案在实现动态内存分配时与 heap4.c 方案一样,采用最佳匹配算法和合并算法,并且允许内存堆跨越多个非连续的内存区,也就是允许在不连续的内存堆中实现内存分配,比如用户在片内 RAM 中定义一个内存堆,还可以在外部 SDRAM 再定义一个或多个内存堆,这些内存都归系统管理。

heap_5.c 方案通过调用 vPortDefineHeapRegions()函数来实现系统管理的内存初始化,在内存初始化未完成前不允许使用内存分配和释放函数

数据结构

typedef struct HeapRegion
{
      uint8_t *pucStartAddress; // 用于内存堆的内存块起始地址
      size_t xSizeInBytes;      //  内存块大小
} HeapRegion_t;

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;

static BlockLink_t xStart;
static BlockLink_t * pxEnd = NULL;
  • HeapRegion_t结构体:用户需要指定每个内存堆区域的起始地址和内存堆大小 、将它们放 在一 个HeapRegion_t 结构体类型数组中,这个数组必须用一个 NULL 指针和 0 作为结尾,起始地址必须从小到大排列。

再使用内存前,需要调用void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions )来进行初始化,传入一个内存堆区域的形参,初始化后才可以对这块内存区域进行使用。

/* 在内存中为内存堆分配两个内存块。
第一个内存块大小为 0x10000 字节,起始地址为 0x80000000,
第二个内存块大小为 0xa0000 字节,起始地址为 0x90000000。
起始地址为 0x80000000 的内存块的起始地址更低,因此放到了数组的第一个位置。*/
const HeapRegion_t xHeapRegions[] = {
        { ( uint8_t * ) 0x80000000UL, 0x10000 },
        { ( uint8_t * ) 0x90000000UL, 0xa0000 },
        { NULL, 0 } /* 数组结尾 */
};

/* 向函数 vPortDefineHeapRegions()传递形参 */
vPortDefineHeapRegions( xHeapRegions );

用户在自定义好内存堆数组后,需要调用 vPortDefineHeapRegions()函数初始化这些内存堆,系统会已一个空闲内存块链表的数据结构记录这些空闲内存,链表以 xStart 节点构开头,以 pxEnd 指针指向的位置结束。

在这里插入图片描述

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

郑烯烃快去学习

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值