FreeRTOS学习笔记(十一)内存管理


前言

  本章是Free RTOS系列的终章,我们来讲述贯穿全系列的一个核心元素——内存管理。


一、内存管理

1.1 内存管理的引入

  内存管理是一个系统基本组成部分,FreeRTOS 中大量使用到了内存管理,比如创建任务、信号量、队列等会自动从堆中申请内存。用户应用层代码也可以 FreeRTOS 提供的内存管理函数来申请和释放内存。FreeRTOS 创建任务、队列、信号量等的时候有两种方法,一种是动态的申请所需的 RAM;一种是由用户自行定义所需的 RAM,这种方法也叫静态方法,使用静态方法的函数一般以“Static”结尾,比如任务创建函数 xTaskCreateStatic(),使用此函数创建任务的时候需要由用户定义任务堆栈,本章我们不讨论这种静态方法。
  使用动态内存管理的时候 FreeRTOS 内核在创建任务、队列、信号量的时候会动态的申请RAM。标准 C 库中的 malloc()和 free()也可以实现动态内存管理,但是出于种种原因限制了其使用,因此一个内存分配算法可以作为系统的可选选项。FreeRTOS 将内存分配作为移植层的一部分,这样 FreeRTOS 使用者就可以使用自己的合适的内存分配方法。
  动态内存分配需要一个内存堆,FreeRTOS 中的内存堆为ucHeap[] ,大小为configTOTAL_HEAP_SIZE,这个前面讲 FreeRTOS 配置的时候就讲过了。不管是哪种内存分配方法,它们的内存堆都为 ucHeap[],而且大小都是 configTOTAL_HEAP_SIZE。

1.2 内存碎片

  在学习 FreeRTOS 的内存分配方法之前我们先来看一下什么叫做内存碎片,看名字就知道是小块的、碎片化的内存。内存碎片是伴随着内存申请和释放而来的,如下图所示:
在这里插入图片描述

  可以看到经过很多次的申请和释放以后,内存块被不断的分割、最终导致大量很小的内存块!也就是图中 80B 和 50B 这两个内存块之间的小内存块,这些内存块由于太小导致大多数应用无法使用,这些没法使用的内存块就沦为了内存碎片!
  内存碎片是内存管理算法重点解决的一个问题,否则的话会导致实际可用的内存越来越少,最终应用程序因为分配不到合适的内存而奔溃!FreeRTOS 的 heap_4.c 就给我们提供了一个解决内存碎片的方法,那就是将内存碎片进行合并组成一个新的可用的大内存块。

二、内存分配的方法

  在Free RTOS的移植一章中,我们提到了其提供了 5 种内存分配方法这 5 种方法是 5 个文件,分别为:heap_1.c、heap_2.c、heap_3.c、heap_4.c 和heap_5.c。这部分我们FreeRTOS 使用者可以其中的某一个方法,或者自定义一个合适的分配方法。
在这里插入图片描述

2.1 heap_1

  heap_1 实现起来就是当需要 RAM 的时候就从一个大数组(内存堆)中分一小块出来,大数组(内存堆)的容量为 configTOTAL_HEAP_SIZE,上面已经说了。使用函数xPortGetFreeHeapSize()可以获取内存堆中剩余内存大小。在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 中。

2.1.1 实现原理

  1. heap_1.c 中使用了一个简单的静态数组作为堆空间,并按需从该数组中分配内存
  2. 该实现非常简单,只允许内存分配,不支持内存释放,内存的释放只能在任务结束或系统重启时实现。
  3. 主要适用于小型嵌入式系统中,内存需求相对简单、可预测的场景。

2.1.2 源码解析

  heap_1 的内存申请函数 pvPortMalloc()源码如下:

// 该函数(简化)用于动态分配内存,返回指向分配内存的指针
void *pvPortMalloc( size_t xWantedSize )
{
    void *pvReturn = NULL;
    
    // 确保 xWantedSize 是 8 字节对齐(xWantedSize是用户请求的内存大小,按照默认要求堆分配会将该大小调整为 8 字节对齐)
    if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0 ) 
    {
        xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
    }

    // 检查是否有足够的空间,如果有返回堆中当前位置 xNextFreeByte 的地址,并将 xNextFreeByte 向前移动 xWantedSize 字节;不足返回NULL
    if( ( ( xNextFreeByte + xWantedSize ) < configTOTAL_HEAP_SIZE ) &&
        ( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) )
    {
    	// pucAlignedHeap表示起始地址(ucHeap的起始地址不一定是8字节对齐的,需要我们利用这个参数补齐)
    	// 如果内存够分配并且不会产生越界,那么就将申请到的内存首地址赋给 pvReturn
    	// 值得注意的是如果我们要申请 30 个字节的内存,字节对齐以后实际需要申请 32 字节
        pvReturn =  pucAlignedHeap + xNextFreeByte; 
        xNextFreeByte += xWantedSize;
    }
    
    return pvReturn;
}

  heap_1 的内存释放函数为 pvFree(),可以看出 vPortFree()并没有具体释放内存的过程,这说明使用一旦申请内存成功就不允许释放!

void vPortFree( void *pv )
{
( void ) pv;
configASSERT( pv == NULL );
}

  这说明了heap_1适用于在系统一开始就创建好任务、信号量或队列等,在程序运行的整个过程这些任务和内核对象都不会删除。

2.2 heap_2 内存分配方法

  heap_2提供了一个更好的分配算法,不像heap_1那样,heap_2提供了内存释放函数。heap_2不会把释放的内存块合并成一个大块,这样有一个缺点,随着你不断的申请内存,内存堆就被分为很多个大小不一的内存(块),也就是会导致内存碎片
  为了实现内存释放,heap_2 引入了内存块的概念,每分出去的一段内存就是一个内存块,剩下的空闲内存也是一个内存块,内存块大小不定,每个内存块前面都会有一个 BlockLink_t 类型的变量来描述此内存块。为了管理内存块又引入了一个链表结构,链表结构如下:

typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK *pxNextFreeBlock; // 指向链表中下一个空闲内存块
size_t xBlockSize; 					  // 当前空闲内存块大小
} BlockLink_t;

  值得注意的是,例如我们只申请了 16 个字节内存,但是还需要另外为BlockLink_t 类型的结构体变量申请8字节,xBlockSize 记录的是整个内存块的大小(24个字节)。

2.2.1 实现原理

  1. heap_2.c 使用链表管理空闲内存块,每个内存块的前面都有一个头部,存储该块的大小和状态(已分配或空闲)。
  2. 当请求内存时,遍历空闲内存链表,找到第一个足够大的块进行分配。
  3. 当释放内存时,将空闲块合并回链表中,以减少碎片化。

2.2.2 源码解析

  内存申请函数 vPortFree()的源码如下:

void *pvPortMalloc( size_t xWantedSize )
{
    BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
    void *pvReturn = NULL;
    
	// 进入临界区,暂停任务调度
    vTaskSuspendAll();    

    // 确保 xWantedSize 是 8 字节对齐
    if( xWantedSize & portBYTE_ALIGNMENT_MASK )
    {
        xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
    }

    // 增加头部大小:为了管理内存块,增加A_BLOCK_LINK的大小
    xWantedSize += xHeapStructSize;

    // 遍历空闲链表,找到合适的块
    pxPreviousBlock = &xStart;
    pxBlock = xStart.pxNextFreeBlock;
    while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
    {
        pxPreviousBlock = pxBlock;
        pxBlock = pxBlock->pxNextFreeBlock;
    }
	// 找到的可用内存块不能是链表尾 xEnd
    if( pxBlock != pxEnd )
    {	
    	// 找到内存块后就将可用内存首地址保存在 pvReturn 中,函数返回的时候返回此值
		// 这个内存首地址要跳过结构体 BlockLink_t
        pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );
        pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
    }

    xTaskResumeAll();     // 退出临界区,恢复任务调度
    return pvReturn;
}

  内存释放函数 vPortFree()还是很简单的,主要目的就是将需要释放的内存所在的内存块,其源码如下:

void vPortFree( void *pv )
{
	// puc 为要释放的内存首地址
	uint8_t *puc = ( uint8_t * ) pv;
	BlockLink_t *pxLink;
	if( pv != NULL )
	{
		// 指向申请函数中pvReturn 所指向的地址
		puc -= heapSTRUCT_SIZE; 
		pxLink = ( void * ) puc; 
		vTaskSuspendAll();
		{
		// 将内存块添加到空闲内存块链表中
		prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) ); 
		// 更新变量 xFreeBytesRemaining
		xFreeBytesRemaining += pxLink->xBlockSize; 
		traceFREE( pv, pxLink->xBlockSize );
		}
		( void ) xTaskResumeAll();
	}
}

2.3 heap_3 内存分配方法

  这个分配方法是对标准 C 中的函数 malloc()和 free()的简单封装,FreeRTOS 对这两个函数做了线程保护,这里就简单解释一下,不再赘述了。heap_3 它通过封装这些标准函数,保证 FreeRTOS 兼容多任务调度环境中的内存分配和释放需求。vTaskSuspendAll() 和 xTaskResumeAll() 确保了在分配和释放内存时,不会出现任务切换导致的竞态条件。
  pvPortMalloc( )调用标准的 malloc() 函数分配内存,并通过 vTaskSuspendAll() 暂停任务调度。

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

    vTaskSuspendAll();
    {
        pvReturn = malloc( xWantedSize );
    }
    xTaskResumeAll();

    return pvReturn;
}

  vPortFree()调用标准的 free() 函数释放内存,并通过 vTaskSuspendAll() 暂停任务调度。

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

2.4 heap_4 内存分配方法

  这种方法是我们学习的重中之重,heap_4 提供了一个最优的匹配算法,它会将内存碎片合并成一个大的可用内存块。这意味着它可以用在那些需要重复创建和删除任务、队列、信号量和互斥信号量等的应用中。虽然具有不确定性,但是远比 C 标准库中的 malloc()和 free()效率高。

2.4.1 实现原理

  1. heap_4.c 实现了类似 heap_2.c 的内存分配策略,即利用A_BLOCK_LINK 结构体来管理内存块,但在其基础上增加了空闲内存块的合并功能。
  2. 每当内存块被释放时,会尝试将它与相邻的空闲块合并,以减少内存碎片化的发生。
  3. 采用了“最佳适配”算法来分配内存,优先使用最适合的空闲块,进一步降低碎片化。

2.4.2 源码解析

  heap_4 的内存申请函数源码如下:

void *pvPortMalloc( size_t xWantedSize )
{
    BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
    void *pvReturn = NULL;
    // 临界区开始
    vTaskSuspendAll(); 

    // 最小块大小检查
    if( xWantedSize > 0 )
    {
        xWantedSize += heapSTRUCT_SIZE;

        // 8 字节对齐
        if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
        {
            xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
        }
    }

    // 查找合适的块
    pxPreviousBlock = &xStart;
    pxBlock = xStart.pxNextFreeBlock;
    
    // 从空闲内存链表头 xStart 开始,查找满足所需内存大小的内存块
	// pxPreviousBlock 的下一个内存块就是找到的可用内存块
    while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
    {
        pxPreviousBlock = pxBlock;
        pxBlock = pxBlock->pxNextFreeBlock;
    }

    // 分配内存
    if( pxBlock != pxEnd )
    {
        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;
            pxNewBlockLink->pxNextFreeBlock = pxPreviousBlock->pxNextFreeBlock;
            pxPreviousBlock->pxNextFreeBlock = pxNewBlockLink;
        }
		// 减少剩余的可用内存,并更新系统曾经最少剩余的内存量
        xFreeBytesRemaining -= pxBlock->xBlockSize;
        if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
        {
            xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
        }
    }

    xTaskResumeAll(); // 临界区结束
    return pvReturn;
}

  内存释放函数源码如下:

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

    if( pv != NULL )
    {
    	// 获取内存块的 BlockLink_t 类型结构体
        puc -= heapSTRUCT_SIZE;
        pxLink = ( void * ) puc;

        vTaskSuspendAll(); 
		// 更新增加的内存
        xFreeBytesRemaining += pxLink->xBlockSize;
        if( ( ( uint8_t * ) pxLink + pxLink->xBlockSize ) == ( uint8_t * ) pxLink->pxNextFreeBlock )
        {
            pxLink->xBlockSize += pxLink->pxNextFreeBlock->xBlockSize;
            pxLink->pxNextFreeBlock = pxLink->pxNextFreeBlock->pxNextFreeBlock;
        }

        // 合并前面的块
        pxLink->pxNextFreeBlock = xStart.pxNextFreeBlock;
        xStart.pxNextFreeBlock = pxLink;

        xTaskResumeAll(); 
    }
}

2.5 heap_5 内存分配方法

  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 ()来对内存堆做初始化处理,在其未执行完之前禁止调用任何可能会调用pvPortMalloc()的 API 函数。函数 vPortDefineHeapRegions()
只有一个参数,参数是一个 HeapRegion_t 类型的数组,HeapRegion 为一个结构体,此结构体在portable.h 中有定义,定义如下:

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

  使用 heap_5 的时候在一开始就应该先调用函数 vPortDefineHeapRegions()完成内存堆的初始化!然后才能创建任务、信号量这些东西。heap_5 的内存申请和释放函数和 heap_4 基本一样,这里就不详细讲解了,大家可以对照着前面 heap_4 的相关内容来自行分析。

免责声明:本文参考了网上公开资料,仅用于学习交流,若有错误或侵权请联系笔者。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值