FreeRTOS内存管理之heap_4.c源码解析

heap_1 —— 最简单,,具有确定性,从静态数组中分配内存,不允许释放内存,不会导致内存碎片化,一锤子买卖,不算真正的动态内存分配;
heap_2—— 非确定性,允许释放内存,但不会合并相邻的空闲块,也就是说没有内存碎片优化措施;
heap_3 —— 简单包装了标准 malloc() 和 free(),以保证线程安全,借壳上市,需要连接器设置堆空间分布,且需要编译器库提供malloc和free函数的实现,可能回增加RTOS内核大小;
heap_4 —— 非确定性,使用第一适应算法(first fit,FF),支持合并相邻的空闲块以避免碎片化,允许将堆放置在内存中的特定地址,官方称比大多数标准 C 库 malloc 的实现要快;
heap_5—— 如同 heap_4,但能够跨越多个不相邻内存区域的堆,且使用动态内存分配前,必须调用 vPortDefineHeapRegions() 进行初始化。
vPortFree:判断指针合法性的时候多了两个条件,一个是检查回收的块大小最高位是否为1,为1才是合法的,毕竟是分配出去了嘛。第二个是Next指针是否为空,为空了说明那是pxEnd,那就不能回收了

在实时操作系统(RTOS)的内存管理中, Heap4高效的原因:

1. **内存碎片管理**:
   - Heap4 使用了一种先适配算法,当内存块被释放时,它会尝试将相邻的空闲内存块合并成一个更大的块,从而减少内存碎片。这种策略有助于在内存使用过程中保持较大的连续空闲区域,使得在需要分配大内存块时更容易找到足够的空间。heap3不会对碎片合并优化。
2. **线程安全**:
   - Heap4 通过暂时挂起 FreeRTOS 调度程序来确保 malloc() 和 free() 是线程安全的,这避免了在多任务环境中由于内存分配和释放操作导致的竞态条件。
   - Heap3 虽然也通过挂起调度程序来保证线程安全,但它依赖于标准库的 malloc() 和 free(),这些函数可能没有针对实时系统进行优化。

3. **确定性**:
   - Heap4 提供了更好的内存分配确定性,因为它的算法设计考虑了实时系统对确定性的要求。在需要快速响应的系统中,这种确定性是非常重要的。

4. **内存分配策略**:
   - Heap4 的内存分配策略是先适配,这意味着它会寻找足够大的内存块来满足请求,这有助于减少内存的浪费。
   - Heap3 可能没有特定的内存分配策略。

5. **内存利用率**:
   - 由于 Heap4 的内存合并策略,它可以更有效地利用内存,减少因碎片导致的内存浪费。

6. **适用场景**:
   - Heap4 适用于需要频繁动态分配和释放内存的复杂场景,而 Heap3 可能更适合内存分配模式较为固定的应用。

综上所述,Heap4 通过减少内存碎片、提供线程安全和确定性,以及优化内存分配策略,通常在实时操作系统中比 Heap3 更高效。

每当创建任务、队列、互斥量、软件定时器、信号量或事件组时,RTOS内核会为它们分配RAM。标准函数库中的malloc()和free()函数有些时候能够用于完成这个任务,但是:

在嵌入式系统中,它们并不总是可以使用的;
它们会占用更多宝贵的代码空间;
它们没有线程保护;
它们不具有确定性(每次调用执行的时间可能会不同);
当RTOS内核需要RAM时,调用**pvPortMalloc()函数来代替malloc()函数。当RAM要被释放时,调用vPortFree()**函数来代替free()函数。

xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
xWantedSize 是要申请的字节数
portBYTE_ALIGNMENT是字节对齐数
portBYTE_ALIGNMENT_MASK 是字节对齐掩码
一、heap_4.c
第四种内存分配方法与第二种比较相似,只不过增加了一个合并算法,将相邻的空闲内存块合并成一个大块。与第一种和第二种内存管理策略一样,内存堆仍然是一个大数组,定义为:

static uint8_t ucHeap[ configTOTAL_HEAP_SIZE];
1
1.1 内存申请:pvPortMalloc()
使用一个链表结构来跟踪记录空闲内存块。结构体定义为:

typedef struct A_BLOCK_LINK
{
    struct A_BLOCK_LINK *pxNextFreeBlock;   /*指向列表中下一个空闲块*/
    size_t xBlockSize;                      /*当前空闲块的大小,包括链表结构大小*/
} BlockLink_t;

第四种内存管理策略和第二种内存管理策略还有一个很大的不同是:第四种内存管理策略的空闲块链表不是以内存块大小为存储顺序,而是以内存块起始地址大小为存储顺序,地址小的在前,地址大的在后。这也是为了适应合并算法而作的改变。
函数中会用到几个局部静态变量在这里简单说明一下:

xFreeBytesRemaining:表示当前未分配的内存堆大小
xMinimumEverFreeBytesRemaining:表示未分配内存堆空间历史最小值。这个值跟xFreeBytesRemaining有很大区别,只有记录未分配内存堆的最小值,才能知道最坏情况下内存堆的使用情况。
xBlockAllocatedBit:这个变量在第一次调用内存申请函数时被初始化,将它能表示的数值的最高位置1。比如对于32位系统,这个变量被初始化为0x80000000(最高位为1)。内存管理策略使用这个变量来标识一个内存块是否空闲。如果内存块被分配出去,则内存块链表结构成员xBlockSize按位或上这个变量(即xBlockSize最高位置1),在释放一个内存块时,会把xBlockSize的最高位清零。

内存申请过程:

首先计算实际要分配的内存大小,判断申请内存合法性,如果合法则从链表头xStart开始查找,如果某个空闲块的xBlockSize字段大小能容得下要申请的内存,则将这块内存取出合适的部分返回给申请者,剩下的内存块组成一个新的空闲块,按照空闲块起始地址大小顺序插入到空闲块链表中,地址小的在前,地址大的在后。
在插入到空闲块链表的过程中,还会执行合并算法:判断这个块是不是可以和上一个空闲块合并成一个大块,如果可以则合并;然后再判断能不能和下一个空闲块合并成一个大块,如果可以则合并!合并算法是第四种内存管理策略和第二种内存管理策略最大的不同!经过几次内存申请和释放后。
源代码解析:
/* 定义一个结构体 */
typedef struct A_BLOCK_LINK
{
#ifdef MTK_SUPPORT_HEAP_DEBUG
    uint32_t magic_header;
#endif
    struct A_BLOCK_LINK *pxNextFreeBlock;    /*<< The next free block in the list. */
    size_t xBlockSize;                        /*<< The size of the free block. */
#ifdef MTK_SUPPORT_HEAP_DEBUG
    uint32_t xLinkRegAddr;
#endif /* MTK_SUPPORT_HEAP_DEBUG */
} BlockLink_t;

/* 创建两个结构体变量,分别指向单链表的头和尾部 */
static BlockLink_t xStart, *pxEnd = NULL;

void *pvPortMalloc( size_t xWantedSize )
{
    BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
    void *pvReturn = NULL;
    vTaskSuspendAll();      //将所有的任务挂起
    {
        if( pxEnd == NULL )
        {
            prvHeapInit(); //进入堆初始化,根据系统的需要,分配合法合理的堆空间
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }

        /* 检查申请分配的内存是否具有合法性和检查申请的内存是否过大 */
        if( ( xWantedSize & xBlockAllocatedBit ) == 0 )
        {
            /* 计算实际要分配的内存大小,包含链接结构体BlockLink_t在内,并且要向上字节对齐 */
            if( xWantedSize > 0 )
            {
                xWantedSize += xHeapStructSize;

                /* 确保块始终与所需的字节数对齐。 */
                if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
                {
                    /* 字节对齐 */
                    xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
                    configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }

            /*  首先遍历链表,找到第1块能比申请空间大小大的空闲块,修改空闲块的信息,记录用户可用的内存首地址。 */
            if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
            {
                /* 从xStart起始(最低地址)块遍历列表,直到找到一个足够大的空闲块。 */
                pxPreviousBlock = &xStart;
                pxBlock = xStart.pxNextFreeBlock;
                while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
                {
                    pxPreviousBlock = pxBlock;
                    pxBlock = pxBlock->pxNextFreeBlock;
                }

                /* 如果找到结束标记,则没有找到足够大小的块;否则则进行内存分配工作 */
                if( pxBlock != pxEnd )
                {
                    /* 返回分配的内存指针,要跳过内存开始处的BlockLink_t结构体 */
                    pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );
                    #ifdef MTK_SUPPORT_HEAP_DEBUG
                    
                    pxPreviousBlock->pxNextFreeBlock->xLinkRegAddr = xLinkRegAddr;
                    configASSERT(pxPreviousBlock->pxNextFreeBlock->magic_header == MAGIC_HEAP_OVERHEADER);
                    #endif /* MTK_SUPPORT_HEAP_DEBUG */

                    /* 将已经分配出去的内存块从空闲块链表中删除 */
                    pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;

                    /* 如果剩下的内存足够大,则组成一个新的空闲块 */
                    /* 如果分配出去的空闲块比申请的空间大很多,则将该空闲块进行分割,把剩余的部分重新添加到链表中。*/
                    if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
                    {
                        /* 在剩余内存块的起始位置放置一个链表结构并初始化链表成员*/
                        pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
                        configASSERT( ( ( ( uint32_t ) pxNewBlockLink ) & portBYTE_ALIGNMENT_MASK ) == 0 );

                
                        pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
                        pxBlock->xBlockSize = xWantedSize;
                        #ifdef MTK_SUPPORT_HEAP_DEBUG
                        pxNewBlockLink->magic_header = MAGIC_HEAP_OVERHEADER;
                        #endif
                        /* 将剩余的空闲块插入到空闲块列表中,按照空闲块的地址大小顺序,地址小的在前,地址大的在后 */
                        prvInsertBlockIntoFreeList( pxNewBlockLink );
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }

                    xFreeBytesRemaining -= pxBlock->xBlockSize;
                    /* 保存未分配内存堆空间历史最小值 */
                    if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
                    {
                        xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }

                    /* 将已经分配的内存块标识为"已分配" */
                    pxBlock->xBlockSize |= xBlockAllocatedBit;
                    pxBlock->pxNextFreeBlock = NULL;
                    xNumberOfSuccessfulAllocations++;
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
        /*  traceMALLOC( pvReturn, xWantedSize)是一个宏,用于输出内存分配的调试信息,这个宏定义在FreeRTOS.h中,默认为空,如果需要将这些调试信息输出到串口...*/
        traceMALLOC( pvReturn, xWantedSize );
    }
    ( void ) xTaskResumeAll();
    /* 如果内存分配失败,调用钩子函数 */
    #if( configUSE_MALLOC_FAILED_HOOK == 1 )
    {
        if( pvReturn == NULL )
        {
            extern void vApplicationMallocFailedHook( void );
            vApplicationMallocFailedHook();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    #endif

    configASSERT( ( ( ( uint32_t ) pvReturn ) & portBYTE_ALIGNMENT_MASK ) == 0 );
    return pvReturn;
}
下面是链表的初始化
heap_2.c中链表的尾部数据并未保存在链表内,是以变量的形式存在的。heap_4.c中的链表尾部数据结构保存在链表空间尾部。

//关于堆栈的初始化
//第一步:起始地址做字节对齐,保存pucAlignedHeap 可用空间大小为xTotalHeapSize 
//第二步:计算首尾 ,这里需要注意的是链表的尾部指针是保存到该地址尾部的
//第三部:完成链表的初始化,记录内存块信息
 
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 += ( portBYTE_ALIGNMENT - 1 );
        uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
        xTotalHeapSize -= uxAddress - ( size_t ) ucHeap;  //减去对齐舍弃的字节
    }
 
    pucAlignedHeap = ( uint8_t * ) uxAddress;         //对齐后可以用的起始地址

    //xStart链表的头 
    xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap; 
    xStart.xBlockSize = ( size_t ) 0;

    //pxEnd链表的尾
    uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize;
    uxAddress -= xHeapStructSize;
    uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
    pxEnd = ( void * ) uxAddress;                       
    pxEnd->xBlockSize = 0;
    pxEnd->pxNextFreeBlock = NULL;
 
    //开始时候将内存堆整个看作为一个空闲内存块
    pxFirstFreeBlock = ( void * ) pucAlignedHeap;
    pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;
    pxFirstFreeBlock->pxNextFreeBlock = pxEnd;
 

    xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize; //记录最小的空闲内存块大小
    xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;            //剩余内存堆大小
 
    /* Work out the position of the top bit in a size_t variable. */
    xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );//用来标记内存块是否被使用
}
空闲块链表的插入:会判断前后的空闲块能否合并,解决内存碎片化的问题。

static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert )
{
    BlockLink_t *pxIterator;
    uint8_t *puc;
 
    //遍历空闲内存块链表,找出内存块插入点。内存块是按照地址从低到高连接在一起的
    for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
    {
        /* Nothing to do here, just iterate to the right position. */
    }

    //插入内存块,检查和前面的内存是否可以合并,如果内存可以合并则合并
    puc = ( uint8_t * ) pxIterator;
    if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
    {
        pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
        pxBlockToInsert = pxIterator;
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }
 
    //检查是否可以与后面的内存合并
    puc = ( uint8_t * ) pxBlockToInsert;
    if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock )
    {
        if( pxIterator->pxNextFreeBlock != pxEnd )
        {
            //合并
            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();
    }
}
在pvPortMalloc()中,若分配出去的空闲块比申请的内存大太多,则需要将内存进行分割,并把分割出的部分重新添加至链表中。
在heap_4.c中的重点为:
将分割出来的空闲块重新添加到链表中的过程,即使用prvInsertBlockIntoFreeList()将其空闲块添加至原本的链表中,则不会产生内存碎片。

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

    /*  遍历找到分割出来的内存块的下一个内存块,将pxIterator指向它  */
    for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
    {
        /* Nothing to do here, just iterate to the right position. */
    }

    puc = ( uint8_t * ) pxIterator;
    /* 可以合并的标准为pxIterator的首地址加上pxIterator的块大小之后等于pxBlockToInsert的首地址。相等就说明两个块是相邻的。 */
    if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
    {
        pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
        pxBlockToInsert = pxIterator;
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    puc = ( uint8_t * ) pxBlockToInsert;
    /* FreeRTOS再试着将pxBlockToInsert和pxIterator指向的下一个空闲块进行合并。可合并的标准和刚刚说的一样,只是这次用pxBlockToInsert的首地址加上pxBlockToInsert的块大小与pxIterator指向的下一个块地址比较。*/
    if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock )
    {
        if( pxIterator->pxNextFreeBlock != pxEnd )
        {
            pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;
            pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock;
        }
        else
        {
            pxBlockToInsert->pxNextFreeBlock = pxEnd;//若是没有合并,则需要修改链表的next指针
        }
    }
    else
    {
        pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
    }

    if( pxIterator != pxBlockToInsert )
    {
        pxIterator->pxNextFreeBlock = pxBlockToInsert;
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }
}
1.2 内存释放vPortFree()
判断指针合法性的时候多了两个条件,一个是检查回收的块大小最高位是否为1,为1才是合法的,毕竟是分配出去了嘛。第二个是Next指针是否为空,为空了说明那是pxEnd,那就不能回收了。在这两个判断之前也有这两个条件的断言configASSERT(),定义在FreeRTOS.h里,同样也是定义为空,可能是留给用户另外用的吧。
源代码解析:

/* 放置在每个分配的内存块开头的结构的大小必须正确对齐字节。*/
static const size_t xHeapStructSize    = ( ( sizeof( BlockLink_t ) + ( portBYTE_ALIGNMENT - 1 ) ) & ~portBYTE_ALIGNMENT_MASK );

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

    if( pv != NULL )
    {
        /* 被释放的内存在它之前会有一个BlockLink_t结构,向前偏移,重新找回BlockLink_t */
        /* 根据参数地址找出内存块链表结构 */
        puc -= xHeapStructSize;

        /* 这种类型转换是为了防止编译器发出警告 */
        pxLink = ( void * ) puc;

        /* 检查块是否被实际分配。 */
        configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );
        configASSERT( pxLink->pxNextFreeBlock == NULL );
#ifdef MTK_SUPPORT_HEAP_DEBUG
        configASSERT(pxLink->magic_header == MAGIC_HEAP_OVERHEADER);
#endif
        /*    。*/
        if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 )
        {
            if( pxLink->pxNextFreeBlock == NULL )
            {
                /* The block is being returned to the heap - it is no longer allocated. */
                pxLink->xBlockSize &= ~xBlockAllocatedBit;
#ifdef MTK_SUPPORT_HEAP_DEBUG
                configASSERT(((BlockLink_t *)((uint8_t *)puc + pxLink->xBlockSize))->magic_header == MAGIC_HEAP_OVERHEADER);
#endif

                vTaskSuspendAll();//通过挂起调度器来创建临界区,挂起调度器有些时候也被称为锁定调度器
                {
                    /* Add this block to the list of free blocks. */
                    xFreeBytesRemaining += pxLink->xBlockSize;
                    traceFREE( pv, pxLink->xBlockSize );
                    prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
                    xNumberOfSuccessfulFrees++;
                }
                ( void ) xTaskResumeAll();//挂起,直到调度器被唤醒后才会得到执行
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
}

 

 

你可能感兴趣的:(网络,嵌入式,c语言,驱动开发,硬件工程)