FreeRTOS 内存 Heap管理

@(嵌入式)

[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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容