首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >FreeRTOS 内存 Heap管理

FreeRTOS 内存 Heap管理

作者头像
orientlu
发布2018-09-13 10:51:31
1.1K0
发布2018-09-13 10:51:31
举报
文章被收录于专栏:orientluorientluorientlu

@(嵌入式)

[TOC]

Freertos

FreeRtos

FreeRtos 提供的几种 heap 管理在源码目录 Source/Portable/MemMang 下,选择哪种类型管理直接在编译时把原文件加入(比如在 makefile SRC中加入)即可, 堆大小是 FreeRTOSConfig.h 中的常量 configTOTAL_HEAP_SIZE,默认是17*1024,即17KB。

FreeRtos 内存管理提供的主要接口:

  • pvPortMalloc() 对应 malloc()
  • vPortFree() 对应 free()
  • xPortGetFreeHeapSize() 获取剩余可分配内存大小

为了适配不同平台、场合需求,对接口提供了不同的实现。

内存对齐

在 portmacro.h (Source/Portable/ + 对应编译器 + 平台 目录下) 的常量 portBYTE_ALIGNMENT 定义了字节对齐,对应的这个变量决定了 portable.h 中的一个常量 portBYTE_ALIGNMENT_MASK, 对应关系如下:

portBYTE_ALIGNMENT

portBYTE_ALIGNMENT_MASK

8(表示以8个字节对齐)

0x0007

4(表示以4个字节对齐)

0x0003

2(表示以2个字节对齐)

0x0001

1(表示以1个字节对齐)

0x0000

在堆管理中涉及了一些字节对齐,此处做准备。

Heap_1

这个版本的堆管理,如源码注释

The simplest possible implementation of pvPortMalloc(). Note that this implementation does NOT allow allocated memory to be freed again.

实现 pvPortMalloc() 用于内存分配,但是不支持回收,适用于一些比较小的嵌入式设备,在系统 boot 后申请内存运行任务,队列和信号量等,在程序生命期内一般没有释放的需求。对于一些安全型的系统,一般是不允许动态申请的,满足设计需求下,越简单越安全。

调用函数 pvPortMalloc( size_t xWantedSize ) 申请内存时,按顺序完成如下工作:

  • 字节对齐
  • 分配内存
  • 调用钩子函数
  • 返回分配内存地址

初始化

#define configADJUSTED_HEAP_SIZE    ( configTOTAL_HEAP_SIZE - portBYTE_ALIGNMENT )
/* Allocate the memory for the heap. */
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
static size_t xNextFreeByte = ( size_t ) 0;

configADJUSTED_HEAP_SIZE 定义了实际可用的堆大小,因为保证字节的对齐,所以减去一个对齐的长度。 ucHeap[ configTOTAL_HEAP_SIZE ] 和 xNextFreeByte 分别对应堆 的地址和已经分配的值,堆实际上就是一个静态分配的大数组。

以下代码,均是函数 pvPortMalloc() 的内容

字节对齐处理

/* Ensure that blocks are always aligned to the required number of bytes. */
#if portBYTE_ALIGNMENT != 1
    if( xWantedSize & portBYTE_ALIGNMENT_MASK )
    {
        /* Byte alignment required. */
        xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
    }
#endif

if 判断了申请的内存大小是否符合字节对齐,如果不符合,则进行对齐处理。举个例子,设置8字节对齐,你本来申请的 xWantedSize == 12 个byte,与 mask & 的结果是4(0100B), 不对齐,为了对齐,系统会 ”强迫症“ 多给你4个字节。实际上你不应该用到,因为你申请了12bye。

分配内存

vTaskSuspendAll();
{
    if( pucAlignedHeap == NULL )
    {
        /* Ensure the heap starts on a correctly aligned boundary. */
        // 字节对齐
        pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) 
        &ucHeap[ portBYTE_ALIGNMENT ] ) & ( ( portPOINTER_SIZE_TYPE ) ~portBYTE_ALIGNMENT_MASK ) );
    }

    /* Check there is enough room left for the allocation. */
    // 边界判断
    if( ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) &&
        ( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) )/* Check for overflow. */
    {
        /* Return the next free byte then increment the index past this
        block. */
        // 切块蛋糕一样把内存分配出来
        pvReturn = pucAlignedHeap + xNextFreeByte;
        xNextFreeByte += xWantedSize;
    }
    traceMALLOC( pvReturn, xWantedSize );
}   
xTaskResumeAll();
  1. 系统调用了 vTaskSuspendAll() 挂起所有任务,保证线程安全, 避免分配时被切任务导致出错。
  2. 对堆的首地址作对齐处理 可能有人疑问堆首地址不就是上面提到那个数组的首地址 &ucHeap[0], 为什么这里要使用 &ucHeap[ portBYTE_ALIGNMENT ] & portPOINTER_SIZE_TYPE? 其实这个地方处理还是考虑对齐问题,举个例子就明白了: 设置8byte对齐, 假如&ucHeap[0]地址是 0x00000006, 为了保证对齐,FreeRtos 直接在这个地址加上一个对齐的长度(保证&后不会越界访问),然后和 mark &一下,对应的,&ucHeap[ portBYTE_ALIGNMENT ]对应了0x000000014,& 上 mark 就是 0x00000008。由于做了这个调整后,实际的堆大小改变了,所以 configADJUSTED_HEAP_SIZE 表示实际可用的内存大小
  3. 分配内存 Heap_1 比较简单,按顺序分配,所以只需要判断剩下的内存够大,直接切出来,更新已分配大小的值,返回地址就可以了

钩子函数调用&返回地址

定义了configUSE_MALLOC_FAILED_HOOK == 1 后, 当申请失败的时候会调用钩子函数, 也可以自己添加其他处理代码。

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

Heap_1 的 vPortFree 函数就不提了

Heap_2

Heap_2 内存分配使用最佳匹配算法(best fit algorithm),比如我们申请25k的内存,而可申请内存中有三块对应大小30K, 50K 和 100 K,按照最小匹配,这时候会把30k进行分割并返回申请内存的地址,剩余部分插回链表留待下次申请。 Heap_2 支持内存回收,但是不会把碎片合并,对于每次申请内存大小都比较固定的,这个方式是没有问题的。

开始和 Heap_1 差不多, 在内存中开辟了一个静态数组作为堆的空间,定义大小,字节对齐处理等。

建立链表

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;

两个变量分别是指向下一块内存的地址指针 pxNextFreeBlock 以及自己的内存大小 xBlockSize。

在第一次申请内存的时候会调用初始化函数 prvHeapInit() 初始化列表。初始化包括链表头 xStart 和链表尾 xEnd (这两个节点不包含空闲内存),以及把整个堆作为一个完整的空闲节点。

每块内存(分配出的和未分配的)的结构如下 :

下一个空闲块地址

当前块大小

当前块可用内存

xx

xx

xx

每块的开头节点数据,提供了分配内存或者回收内存所需要的信息。

分配内存

当我们尝试申请内存的时候,除了和 Heap_1 一样进行对齐等处理外,系统会在我们申请内存大小 xWantedSize 的基础上增加一个 heapSTRUCT_SIZE (链表节点对齐后的大小)的链表节点,记录这块分配出去的内存的大小,供回收的时候使用。

从链表头开始遍历未分配内存链表,查找符合大小的内存块(链表按内存块大小排列,所以最先返回的的块最符合申请内存大小,所谓的最匹配算法就是这个意思来的)。返回该块 heapSTRUCT_SIZE 个字节后的地址给函数调用者, 前面预留的字节保留链表节点信息。

同时会判断当前这块内存是否有剩余(大于一个链表节点所需空间),如果有,就把剩余的内存再新建一个未分配内存块节点,插入到未分配链表中,供下次分配使用。其中 prvInsertBlockIntoFreeList() 这个宏函数是把节点按大小插入到链表中。

if( ( xWantedSize > 0 ) && ( xWantedSize < configADJUSTED_HEAP_SIZE ) )
{
    /* Blocks are stored in byte order - traverse the list from the start
    (smallest) block until one of adequate size is found. */
    pxPreviousBlock = &xStart;
    pxBlock = xStart.pxNextFreeBlock;
    // 寻找匹配的内存块节点
    while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
    {
        pxPreviousBlock = pxBlock;
        pxBlock = pxBlock->pxNextFreeBlock;
    }

    /* If we found the end marker then a block of adequate size was not found. */
    if( pxBlock != &xEnd )
    {
        /* Return the memory space - jumping over the BlockLink_t structure
        at its start. */
        //返回给我们用地址,前面 heapSTRUCT_SIZE 保存链表节点数据
        pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + heapSTRUCT_SIZE );

        /* This block is being returned for use so must be taken out of the
        list of free blocks. */
        pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;

        /* If the block is larger than required it can be split into two. */
        // 内存块比较大,拆分多余的内存,插回到链表
        if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
        {
            /* This block is to be split into two.  Create a new block
            following the number of bytes requested. The void cast is
            used to prevent byte alignment warnings from the compiler. */
            pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );

            /* Calculate the sizes of two blocks split from the single
            block. */
            pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
            pxBlock->xBlockSize = xWantedSize;

            /* Insert the new block into the list of free blocks. */
            // 按内存大小插入
            prvInsertBlockIntoFreeList( ( pxNewBlockLink ) );
        }

        xFreeBytesRemaining -= pxBlock->xBlockSize;
    }
}

回收内存

回收内存, 拿到分配时返回的地址,向前索引到对应链表节点,取出这块返回内存块的信息,调用链表插入函数,将这个节点归还。(线程安全)

puc -= heapSTRUCT_SIZE;
/* This unexpected casting is to keep some compilers from issuing
byte alignment warnings. */
pxLink = ( void * ) puc;
vTaskSuspendAll();
{
    /* Add this block to the list of free blocks. */
    prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
    xFreeBytesRemaining += pxLink->xBlockSize;
    traceFREE( pv, pxLink->xBlockSize );
}
xTaskResumeAll();

!!! 这里感觉,如果随便传个地址进来,会不会把系统弄傻.....

Heap_3

Heap_3 实现是直接对标准库的 malloc 进行封装, 保证线程安全。

void *pvPortMalloc( size_t xWantedSize ) 
{ 
void *pvReturn; 
    vTaskSuspendAll();  // 挂起任务
    { 
        pvReturn = malloc( xWantedSize ); 
    } 
    xTaskResumeAll(); 
    return pvReturn; 
} 
void vPortFree( void *pv ) 
{ 
    if( pv != NULL ) 
    { 
        vTaskSuspendAll(); 
        { 
            free( pv ); 
        } 
        xTaskResumeAll(); 
    } 
} 

!! 这种模式下,堆大小不再由 FreeRTOSConfig.h 中定义的常量 configTOTAL_HEAP_SIZE 决定,而是由连接器或者启动代码决定。

Heap_4

相比 Heap_2, Heap_4 能够把内存碎片合并成大块内存,为了实现这个合并算法,空闲内存链表是按内存地址大小进行存储的(Heap_2 是按照内存块大小进行存储)。

xEnd 的位置

不同 heap_2 中 用一个静态变量 xEnd 作为链表尾,heap_4 把链表尾放在了堆的最后位置,如源码:

// 堆地址最后往回推一个链表节点的空间
uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize;
uxAddress -= xHeapStructSize;
uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
pxEnd = ( void * ) uxAddress;
pxEnd->xBlockSize = 0;
pxEnd->pxNextFreeBlock = NULL;

xBlockAllocatedBit 判断

另外,为了安全,增加一个位(xBlockSize 的最高位)标记检测 Free 时传入地址的正确性,在初始化的时候设置 xBlockAllocatedBit 的值, 一个 size_t 大小的值最高位置1, 分配出去的内存块链表节点的 xBlockSize 或上,回收的时候判断,如果最高位不是1, 说明出错。

/* Work out the position of the top bit in a size_t variable. */
xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );

进行字节对齐和线程安全等的操作和前面两种方式差不多。

链表插入 (合并实现)

Heap_2 中的链表插入是通过宏实现的,按内存块大小进行插入,而 Heap_4 的插入操作是一个函数,该函数按内存块地址进行插入(低位前),这么做是为了实现内存块合并。 如下, 准备插入的内存块p, 系统查找到内存地址对应与其前面的内存块A, 判断 A 和 P 之间是否还有其他分配的块,如果没有,直接合并; 然后再判断和内存C 的位置关系,没有其他分配了的内存块的话,就直接合并。

内存块 ..

内存块 A

准备插入内存块 P

内存块 C

xx

xx

xx

xx

下面对应看看源码 :

static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert )
{
    BlockLink_t *pxIterator;
    uint8_t *puc;
    // for 的目的是知道 pxIterator 指向内存块A
    // pxIterator->pxNextFreeBlock 指向内存块C
    // 插入内存块P 刚好夹在中间
    for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
    {
        /* Nothing to do here, just iterate to the right position. */
    }
    // 判断内存块A 能否与插入内存块P 合并
    // 合并条件:头尾衔接刚好
    puc = ( uint8_t * ) pxIterator;
    if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
    {
        pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
        pxBlockToInsert = pxIterator;
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }
    // 判断内存块C 能否与插入块P 合并
    puc = ( uint8_t * ) pxBlockToInsert;
    if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock )
    {
        // 如果是内存块C 是最后一个节点 xEnd,不能合并
        if( pxIterator->pxNextFreeBlock != pxEnd )
        {
            pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;
            pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock;
        }
        else
        {
            pxBlockToInsert->pxNextFreeBlock = pxEnd;
        }
    }
    else
    {
        // 内存块C 不能合并的话,让上一块指向他
        pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
    }
    if( pxIterator != pxBlockToInsert )
    {   
        // 内存块A 不能合并,更新他指向刚插入的内存块P
        pxIterator->pxNextFreeBlock = pxBlockToInsert;
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }
}

分配内存

相比 Heap_2 差别不大,主要是在分配过程多了一个位标记防止出错,因为使用了 xBlockSize 的最高位做标记,所以实际传入的 xWantedSize 最高位不能为1,否则超出范围。 其他差别不大,此处不做赘述。

回收内存

相比 Heap_2, Heap_4 多了一些检查,更加安全。

// 判断位标记,判断指向下一个节点是否 设置为 Null
configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );
configASSERT( pxLink->pxNextFreeBlock == NULL );
if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 )
{
    if( pxLink->pxNextFreeBlock == NULL )
    {
    ...

最后清除 xBlockSize 的高位标记,调用插入函数归还内存。

获取历史堆剩余最小值

可用于最坏情况下,堆的使用情况, 在每次调用 pvPortMalloc() 中进行更新。

size_t xPortGetMinimumEverFreeHeapSize( void )

Heap_5

前面方式1、2和4 方式都是静态申请一个数组作为堆,Heap_5 允许使用多个不连续的区域组成堆,申请函数前,必须通过函数 vPortDefineHeapRegions() 进行设置,对于一些内存分布不连续的嵌入式设备还是很有价值的,之后其他操作,和 Heap_4 基本一致。

所以这里主要分析下 vPortDefineHeapRegions() 的实现。

设置作为堆的区域需要用到一下结构体

/* Used by heap_5.c. */
typedef struct HeapRegion
{
    uint8_t *pucStartAddress;
    size_t xSizeInBytes;
} HeapRegion_t;

举个例子, 有两个不连续区域首地址0x00200000, 长度0x10000 和首地址0x00300000, 长度0xA0000 只做为堆,则可以建立如下数组传递给 vPortDefineHeapRegions(), 进行设置。

HeapRegion_t xHeapRegions[] =  
{  
    { ( uint8_t * ) 0x00200000UL, 0x10000 },   
    { ( uint8_t * ) 0x00300000UL, 0xA0000 },   
    { NULL, 0 }         // 告知结束         
};  

相比前面其他模式,初始化时,把可以分配的内存作为一个节点,Heap_5 就把这几个不连续的内存区域分别作为节点链接起来。

每个连续区域作为一个节点,其结构如下

指向末端节点

本块内存区域大小

本块可用内存区域

指向下一块内存

0


参考

Using the FreeRTOS Real Time Kernel - A Practical Guide_opened FreeRTOS memory

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2016.09.30 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 内存对齐
  • Heap_1
    • 初始化
      • 字节对齐处理
        • 分配内存
          • 钩子函数调用&返回地址
          • Heap_2
            • 建立链表
              • 分配内存
                • 回收内存
                • Heap_3
                • Heap_4
                  • xEnd 的位置
                    • xBlockAllocatedBit 判断
                      • 链表插入 (合并实现)
                        • 分配内存
                          • 回收内存
                            • 获取历史堆剩余最小值
                            • Heap_5
                            • 参考
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档