FreeRTOS进阶内存管理示例完全解析

内存管理对应用程序和操作系统来说都非常重要。现在很多的程序漏洞和运行崩溃都和内存分配使用错误有关。

FreeRTOS操作系统将内核与内存管理分开实现,操作系统内核仅规定了必要的内存管理函数原型,而不关心这些内存管理函数是如何实现的。这样做大有好处,可以增加系统的灵活性:不同的应用场合可以使用不同的内存分配实现,选择对自己更有利的内存管理策略。比如对于安全型的嵌入式系统,通常不允许动态内存分配,那么可以采用非常简单的内存管理策略,一经申请的内存,甚至不允许被释放。在满足设计要求的前提下,系统越简单越容易做的更安全。再比如一些复杂应用,要求动态的申请、释放内存操作,那么也可以设计出相对复杂的内存管理策略,允许动态分配和动态释放。

FreeRTOS内核规定的几个内存管理函数原型为:

void *pvPortMalloc( size_t xSize ) :内存申请函数void vPortFree( void *pv ) :内存释放函数void vPortInitialiseBlocks( void ) :初始化内存堆函数size_t xPortGetFreeHeapSize( void ) :获取当前未分配的内存堆大小size_t xPortGetMinimumEverFreeHeapSize( void ):获取未分配的内存堆历史最小值 FreeRTOS提供了5种内存管理实现,有简单也有复杂的,可以应用于绝大多数场合。它们位于下载包目录...\FreeRTOS\Source\portable\MemMang中,文件名分别为:heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c。我在《FreeRTOS系列第8篇---FreeRTOS内存管理》这篇文章中介绍了这5种内存管理的特性以及各自应用的场合,今天我们要分析它们的实现方法。
FreeRTOS提供的内存管理都是从内存堆中分配内存的。默认情况下,FreeRTOS内核创建任务、队列、信号量、事件组、软件定时器都是借助内存管理函数从内存堆中分配内存。最新的FreeRTOS版本(V9.0.0及其以上版本)可以完全使用静态内存分配方法,也就是不使用任何内存堆。
对于heap_1.c、heap_2.c和heap_4.c这三种内存管理策略,内存堆实际上是一个很大的数组,定义为:
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
其中宏configTOTAL_HEAP_SIZE用来定义内存堆的大小,这个宏在FreeRTOSConfig.h中设置。
对于heap_3.c,这种策略只是简单的包装了标准库中的malloc()和free()函数,包装后的malloc()和free()函数具备线程保护。因此,内存堆需要通过编译器或者启动文件设置堆空间。
heap_5.c比较有趣,它允许程序设置多个非连续内存堆,比如需要快速访问的内存堆设置在片内RAM,稍微慢速访问的内存堆设置在外部RAM。每个内存堆的起始地址和大小由应用程序设计者定义。
1. heap_1.c        这是5个内存管理策略中最简单的一个,我们称为第一个内存管理策略,它简单到只能申请内存。是的,跟你想的一样,一旦申请成功后,这块内存再也不能被释放。对于大多数嵌入式系统,特别是对安全要求高的嵌入式系统,这种内存管理策略很有用,因为对系统软件来说,逻辑越简单越容易兼顾安全。实际上,大多数的嵌入式系统并不需要动态删除任务、信号量、队列等,而是在初始化的时候一次性创建好,便一直使用,永远不用删除。所以这个内存管理策略实现简洁、安全可靠,使用的非常广泛。我对这个对内存管理策略也情有独钟。
        我们可以将第一种内存管理看作是切面包:初始化的内存就像一根完整的长棍面包,每次申请内存,就从一端切下适当长度的面包返还给申请者,直到面包被分配完毕,就这么简单。
这个内存管理策略使用两个局部静态变量来跟踪内存分配,变量定义为:

static size_t xNextFreeByte = ( size_t ) 0;static uint8_t *pucAlignedHeap = NULL;

其中,变量xNextFreeByte记录已经分配的内存大小,用来定位下一个空闲的内存堆位置。因为内存堆实际上是一个大数组,我们只需要知道已分配内存的大小,就可以用它作为偏移量找到未分配内存的起始地址。变量xNextFreeByte被初始化为0,然后每次申请内存成功后,都会增加申请内存的字节数目。
变量pucAlignedHeap指向对齐后的内存堆起始位置。为什么要对齐?这是因为大多数硬件访问内存对齐的数据速度会更快。为了提高性能,FreeRTOS会进行对齐操作,不同的硬件架构对齐操作也不尽相同,对于Cortex-M3架构,进行8字节对齐。
我们来看一下第一种内存管理策略对外提供的API函数。
1.1内存申请:pvPortMalloc() 函数源码为:

void *pvPortMalloc( size_t xWantedSize ){void *pvReturn = NULL;static uint8_t *pucAlignedHeap = NULL;    /* 确保申请的字节数是对齐字节数的倍数 */    #if( portBYTE_ALIGNMENT != 1 )    {        if( xWantedSize & portBYTE_ALIGNMENT_MASK )        {            xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );        }    }    #endif    vTaskSuspendAll();    {        if( pucAlignedHeap == NULL )        {            /* 第一次使用,确保内存堆起始位置正确对齐 */            pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );        }        /* 边界检查,变量xNextFreeByte是局部静态变量,初始值为0 */        if( ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) &&            ( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) )        {            /* 返回申请的内存起始地址并更新索引 */            pvReturn = pucAlignedHeap + xNextFreeByte;            xNextFreeByte += xWantedSize;        }    }    ( void ) xTaskResumeAll();    #if( configUSE_MALLOC_FAILED_HOOK == 1 )    {        if( pvReturn == NULL )        {            extern void vApplicationMallocFailedHook( void );            vApplicationMallocFailedHook();        }    }    #endif    return pvReturn;}

函数一开始会将申请的内存数量调整到对齐字节数的整数倍,所以实际分配的内存空间可能比申请内存大。比如对于8字节对齐的系统,申请11字节内存,经过对齐后,实际分配的内存是16字节(8的整数倍)。
接下来会挂起所有任务,因为内存申请是不可重入的(使用了静态变量)。 
如果是第一次执行这个函数,需要将变量pucAlignedHeap指向内存堆区域第一个地址对齐处。我们上面说内存堆其实是一个大数组,编译器为这个数组分配的起始地址是随机的,可能不符合我们的对齐需要,这时候要进行调整。比如内存堆数组ucHeap从RAM地址0x10002003处开始,系统按照8字节对齐,则对齐后的内存堆如图1-1所示:
  图1-1:内存堆大小与地址对齐示意图 之后进行边界检查,查看剩余的内存堆是否够分配,检查xNextFreeByte + xWantedSize是否溢出。如果检查通过,则为申请者返回有效的内存指针并更新已分配内存数量计数器xNextFreeByte(从指针pucAlignedHeap开始,偏移量为xNextFreeByte处的内存区域为未分配的内存堆起始位置)。比如我们首次调用内存分配函数pvPortMalloc(20),申请20字节内存。根据对齐原则,我们会实际申请到24字节内存,申请成功后,内存堆示意图如图1-2所示。
  图1-2:第一次分配内存后的内存堆空间示意图 内存分配完成后,不管有没有分配成功都恢复之前挂起的调度器。
如果内存分配不成功,这里最可能是内存堆空间不够用了,会调用一个钩子函数vApplicationMallocFailedHook()。这个钩子函数由应用程序提供,通常我们可以打印内存分配设备信息或者点亮也故障指示灯。
1.2获取当前未分配的内存堆大小:xPortGetFreeHeapSize() 函数用于返回未分配的内存堆大小。这个函数也很有用,通常用于检查我们设置的内存堆是否合理,通过这个函数我们可以估计出最坏情况下需要多大的内存堆,以便合理的节省RAM。
对于第一个内存管理策略,这个函数实现十分简单,源码如下:

size_t xPortGetFreeHeapSize( void ){    return ( configADJUSTED_HEAP_SIZE - xNextFreeByte );}

从图1-1和图1-2我们知道,宏configADJUSTED_HEAP_SIZE表示内存堆有效的大小,这个值减去已经分配出去的内存大小,正是我们需要的未分配的内存堆大小。
1.3其它函数 第一个内存管理策略中还有两个函数:vPortFree()和vPortInitialiseBlocks()。但实际上第一个函数什么也不做;第二个函数仅仅将静态局部变量xNextFreeByte设置为0。
2. heap_2.c        第二种内存管理策略要比第一种内存管理策略复杂,它使用一个最佳匹配算法,允许释放之前已分配的内存块,但是它不会把相邻的空闲块合成一个更大的块(换句话说,这会造成内存碎片)。
        这个内存管理策略用于重复的分配和删除具有相同堆栈空间的任务、队列、信号量、互斥量等等,并且不考虑内存碎片的应用程序,不适用于分配和释放随机字节堆栈空间的应用程序!
        与第一种内存管理策略一样,内存堆仍然是一个大数组,定义为:

static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

局部静态变量pucAlignedHeap指向对齐后的内存堆起始位置。地址对齐的原因在第一种内存管理策略中已经说明。假如内存堆数组ucHeap从RAM地址0x10002003处开始,系统按照8字节对齐,则对齐后的内存堆与第一个内存管理策略一样,如图2-1所示:
  图2-1:内存堆示大小与地址对齐示意图2.1内存申请:pvPortMalloc() 与第一种内存管理策略不同,第二种内存管理策略使用一个链表结构来跟踪记录空闲内存块,将空闲块组成一个链表。结构体定义为:

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

两个BlockLink_t类型的局部静态变量xStart和xEnd用来标识空闲内存块的起始和结束。刚开始时,整个内存堆有效空间就是一个空闲块,如图2-2所示。因为要包含的信息越来越多,我们必须舍弃一些信息,舍弃的信息可以在上一幅图中找到。
  图2-2:内存堆初始化示意图        图2-2中的pvReturn是我自己增加的,用于接下来分析内存申请操作,堆栈初始化并没有这个变量,也没有对其操作的代码。从图2-2中可以看出,整个有效空间组成唯一一个空闲块,在空闲块的起始位置放置了一个链表结构,用于存储这个空闲块的大小和下一个空闲块的地址。由于目前只有一个空闲块,所以空闲块的pxNextFreeBlock指向链表xEnd,而链表xStart结构的pxNextFreeBlock指向空闲块。这样,xStart、空闲块和xEnd组成一个单链表,xStart表示链表头,xEnd表示链表尾。随着内存申请和释放,空闲块可能会越来越多,但它们仍是以xStart链表开头以xEnd链表结尾,根据空闲块的大小排序,小的在前,大的在后,我们在内存释放一节中会给出示意图。
       当申请N字节内存时,实际上不仅需要分配N字节内存,还要分配一个BlockLink_t类型结构体空间,用于描述这个内存块,结构体空间位于空闲内存块的最开始处。当然,和第一种内存管理策略一样,申请的内存大小和BlockLink_t类型结构体大小都要向上扩大到对齐字节数的整数倍。
        我们看一下内存申请过程:首先计算实际要分配的内存大小,判断申请的内存是否合法。如果合法则从链表头xStart开始查找,如果某个空闲块的xBlockSize字段大小能容得下要申请的内存,则从这块内存取出合适的部分返回给申请者,剩下的内存块组成一个新的空闲块,按照空闲块的大小顺序插入到空闲块链表中,小块在前大块在后。注意,返回的内存中不包括链表结构,而是紧邻链表结构(经过对齐)后面的位置。举个例子,如图2-2所示的内存堆,当调用申请内存函数,如果内存堆空间足够大,就将pvReturn指向的地址返回给申请者,而不是静态变量pucAlignedHeap指向的内存堆起始位置!
        当多次调用内存申请函数后(没有调用内存释放函数),内存堆结构如图2-3所示。注意图中的pvReturn仍是我自己增加上去的,pvReturn指向的位置返回给申请者。后面我们讲内存释放时,就是根据这个地址完成内存释放工作的。
  图2-3:经过两次内存分配后的内存堆示意图 有了上面的这些基础知识,再看内存申请函数源码就比较简单了,我把需要注意的要点以注释的方式放在源码中,不再单独对这个函数做讲解,值得注意的是函数中使用的一个静态局部变量xFreeBytesRemaining,它用来记录未分配的内存堆大小。这个变量将提供给函数xPortGetFreeHeapSize()使用,以方便用户估算内存堆使用情况。

void *pvPortMalloc( size_t xWantedSize ){BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;static BaseType_t xHeapHasBeenInitialised = pdFALSE;void *pvReturn = NULL;    /* 挂起调度器 */    vTaskSuspendAll();    {        /* 如果是第一次调用内存分配函数,这里先初始化内存堆,如图2-2所示 */        if( xHeapHasBeenInitialised == pdFALSE )        {            prvHeapInit();            xHeapHasBeenInitialised = pdTRUE;        }        /* 调整要分配的内存值,需要增加上链表结构体空间,heapSTRUCT_SIZE表示经过对齐扩展后的结构体大小 */        if( xWantedSize > 0 )        {            xWantedSize += heapSTRUCT_SIZE;            /* 调整实际分配的内存大小,向上扩大到对齐字节数的整数倍 */            if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0 )            {                xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );            }        }                if( ( xWantedSize > 0 ) && ( xWantedSize < configADJUSTED_HEAP_SIZE ) )        {            /* 空闲内存块是按照块的大小排序的,从链表头xStart开始,小的在前大的在后,以链表尾xEnd结束 */            pxPreviousBlock = &xStart;            pxBlock = xStart.pxNextFreeBlock;            /* 搜索最合适的空闲块 */            while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )            {                pxPreviousBlock = pxBlock;                pxBlock = pxBlock->pxNextFreeBlock;            }            /* 如果搜索到链表尾xEnd,说明没有找到合适的空闲内存块,否则进行下一步处理 */            if( pxBlock != &xEnd )            {                /* 返回内存空间,注意是跳过了结构体BlockLink_t空间. */                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;                    /* 将剩余的空闲块插入到空闲块列表中,按照空闲块的大小顺序,小的在前大的在后 */                    prvInsertBlockIntoFreeList( ( pxNewBlockLink ) );                }                /* 计算未分配的内存堆大小,注意这里并不能包含内存碎片信息 */                xFreeBytesRemaining -= pxBlock->xBlockSize;            }        }        traceMALLOC( pvReturn, xWantedSize );    }    ( void ) xTaskResumeAll();    #if( configUSE_MALLOC_FAILED_HOOK == 1 )    {   /* 如果内存分配失败,调用钩子函数 */        if( pvReturn == NULL )        {            extern void vApplicationMallocFailedHook( void );            vApplicationMallocFailedHook();        }    }    #endif    return pvReturn;}

2.2内存释放:vPortFree() 因为不需要合并相邻的空闲块,第二种内存管理策略的内存释放也非常简单:根据传入的参数找到链表结构,然后将这个内存块插入到空闲块列表,更新未分配的内存堆计数器大小,结束。因为简单,我们直接看源码。

void vPortFree( void *pv ){uint8_t *puc = ( uint8_t * ) pv;BlockLink_t *pxLink;    if( pv != NULL )    {        /* 根据传入的参数找到链表结构 */        puc -= heapSTRUCT_SIZE;        /* 预防某些编译器警告 */        pxLink = ( void * ) puc;        vTaskSuspendAll();        {            /* 将这个块添加到空闲块列表 */            prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );            /* 更新未分配的内存堆大小 */            xFreeBytesRemaining += pxLink->xBlockSize;                        traceFREE( pv, pxLink->xBlockSize );        }        ( void ) xTaskResumeAll();    }}

我们举一个例子,将图2-3 pvReturn指向的内存块释放掉,假设(configADJUSTED_HEAP_SIZE-40)远大于要释放的内存块大小,释放后的内存堆如图2-4所示:
  图2-4:释放内存后,内存堆示意图 从图2-4我们可以看出第二种内存管理策略的两个特点:第一,空闲块是按照大小排序的;第二,相邻的空闲块不会组合成一个大块。
我们再接着引申讨论一下这种内存管理策略的优缺点。通过对内存申请和释放函数源码分析,我们可以看出它的一个优点是速度足够快,因为它的实现非常简单;第二个优点是可以动态释放内存。但是它的缺点也非常明显:由于在释放内存时不会将相邻的内存块合并,所以这可能造成内存碎片。这就对其应用的场合要求极其苛刻:第一,每次创建或释放的任务、信号量、队列等必须大小相同,如果分配或释放的内存是随机的,绝对不可以用这种内存管理策略;第二,如果申请和释放的顺序不可预料,也很危险。举个例子,对于一个已经初始化的10KB内存堆,先申请48字节内存,然后释放;再接着申请32字节内存,那么一个本来48字节的大块就会被分为32字节和16字节的小块,如果这种情况经常发生,就会导致每个空闲块都可能很小,最终在申请一个大块时就会因为没有合适的空闲块而申请失败(并不是因为总的空闲内存不足)!
2.3获取未分配的内存堆大小:xPortGetFreeHeapSize() 函数用于返回未分配的内存堆大小。这个函数也很有用,通常用于检查我们设置的内存堆是否合理,通过这个函数我们可以估计出最坏情况下需要多大的内存堆,以便进行合理的节省RAM。需要注意的是,这个函数返回值并不能函数源码为:

size_t xPortGetFreeHeapSize( void ){    return xFreeBytesRemaining;}

局部静态变量xFreeBytesRemaining在内存申请和内存释放函数中多次提到,它用来动态记录未分配的内存堆大小。
3.heap_3.c 第三种内存管理策略简单的封装了标准库中的malloc()和free()函数,采用的封装方式是操作内存前挂起调度器、完成后再恢复调度器。封装后的malloc()和free()函数具备线程保护。
第一种和第二种内存管理策略都是通过定义一个大数组作为内存堆,数组的大小由宏configTOTAL_HEAP_SIZE指定。第三种内存管理策略与前两种不同,它不再需要通过数组定义内存堆,而是需要使用编译器设置内存堆空间,一般在启动代码中设置。因此宏configTOTAL_HEAP_SIZE对这种内存管理策略是无效的。
3.1内存申请:pvPortMalloc()

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 )        {            extern void vApplicationMallocFailedHook( void );            vApplicationMallocFailedHook();        }    }    #endif    return pvReturn;}

3.2 内存释放:vPortFree()

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

4.heap_4.c 第四种内存分配方法与第二种比较相似,只不过增加了一个合并算法,将相邻的空闲内存块合并成一个大块。
与第一种和第二种内存管理策略一样,内存堆仍然是一个大数组,定义为:

static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

4.1 内存申请:pvPortMalloc() 和第二种内存管理策略一样,它也使用一个链表结构来跟踪记录空闲内存块。结构体定义为:

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

与第二种内存管理策略一样,空闲内存块也是以单链表的形式组织起来的,BlockLink_t类型的局部静态变量xStart表示链表头,但第四种内存管理策略的链表尾保存在内存堆空间最后位置,并使用BlockLink_t指针类型局部静态变量pxEnd指向这个区域(第二种内存管理策略使用静态变量xEnd表示链表尾),如图4-1所示。
第四种内存管理策略和第二种内存管理策略还有一个很大的不同是:第四种内存管理策略的空闲块链表不是以内存块大小为存储顺序,而是以内存块起始地址大小为存储顺序,地址小的在前,地址大的在后。这也是为了适应合并算法而作的改变。
  图4-1:内存堆初始化示意图        从图4-1中可以看出,整个有效空间组成唯一一个空闲块,在空闲块的起始位置放置了一个链表结构,用于存储这个空闲块的大小和下一个空闲块的地址。由于目前只有一个空闲块,所以空闲块的pxNextFreeBlock指向指针pxEnd指向的位置,而链表xStart结构的pxNextFreeBlock指向空闲块。xStart表示链表头,pxEnd指向位置表示链表尾。
        当申请x字节内存时,实际上不仅需要分配x字节内存,还要分配一个BlockLink_t类型结构体空间,用于描述这个内存块,结构体空间位于空闲内存块的最开始处。当然,和第一种、第二种内存管理策略一样,申请的内存大小和BlockLink_t类型结构体大小都要向上扩大到对齐字节数的整数倍。
        我们先说一下内存申请过程:首先计算实际要分配的内存大小,判断申请内存合法性,如果合法则从链表头xStart开始查找,如果某个空闲块的xBlockSize字段大小能容得下要申请的内存,则将这块内存取出合适的部分返回给申请者,剩下的内存块组成一个新的空闲块,按照空闲块起始地址大小顺序插入到空闲块链表中,地址小的在前,地址大的在后。在插入到空闲块链表的过程中,还会执行合并算法:判断这个块是不是可以和上一个空闲块合并成一个大块,如果可以则合并;然后再判断能不能和下一个空闲块合并成一个大块,如果可以则合并!合并算法是第四种内存管理策略和第二种内存管理策略最大的不同!经过几次内存申请和释放后,可能的内存堆如图4-2所示:
  图4-2:经过数次内存申请和释放后,某个内存堆示意图 有了上面的基础,我们再来看一下源码,我把需要注意的要点以注释的方式放在源码中,不再单独对这个函数做讲解。函数中会用到几个局部静态变量在这里简单说明一下:
xFreeBytesRemaining:表示当前未分配的内存堆大小xMinimumEverFreeBytesRemaining:表示未分配内存堆空间历史最小值。这个值跟xFreeBytesRemaining有很大区别,只有记录未分配内存堆的最小值,才能知道最坏情况下内存堆的使用情况。xBlockAllocatedBit:这个变量在第一次调用内存申请函数时被初始化,将它能表示的数值的最高位置1。比如对于32位系统,这个变量被初始化为0x80000000(最高位为1)。内存管理策略使用这个变量来标识一个内存块是否空闲。如果内存块被分配出去,则内存块链表结构成员xBlockSize按位或上这个变量(即xBlockSize最高位置1),在释放一个内存块时,会把xBlockSize的最高位清零。

void *pvPortMalloc( size_t xWantedSize ){BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;void *pvReturn = NULL;    vTaskSuspendAll();    {        /* 如果是第一次调用内存分配函数,则初始化内存堆,初始化后的内存堆如图4-1所示 */        if( pxEnd == NULL )        {            prvHeapInit();        }        /* 申请的内存大小合法性检查:是否过大.结构体BlockLink_t中有一个成员xBlockSize表示块的大小,这个成员的最高位被用来标识这个块是否空闲.因此要申请的块大小不能使用这个位.*/        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 );                }            }            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 );                    /* 将已经分配出去的内存块从空闲块链表中删除 */                    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 );                        pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;                        pxBlock->xBlockSize = xWantedSize;                        /* 将剩余的空闲块插入到空闲块列表中,按照空闲块的地址大小顺序,地址小的在前,地址大的在后 */                        prvInsertBlockIntoFreeList( pxNewBlockLink );                    }                                        /* 计算未分配的内存堆空间,注意这里并不能包含内存碎片信息 */                    xFreeBytesRemaining -= pxBlock->xBlockSize;                                        /* 保存未分配内存堆空间历史最小值 */                    if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )                    {                        xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;                    }                    /* 将已经分配的内存块标识为"已分配" */                    pxBlock->xBlockSize |= xBlockAllocatedBit;                    pxBlock->pxNextFreeBlock = NULL;                }            }        }        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( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 );    return pvReturn;}

4.2 内存释放:vPortFree() 第四种内存管理策略的内存释放也比较简单:根据传入的参数找到链表结构,然后将这个内存块插入到空闲块列表,需要注意的是在插入过程中会执行合并算法,这个我们已经在内存申请中讲过了。最后是将这个内存块标志为“空闲”、更新未分配的内存堆大小,结束。源代码如下:

void vPortFree( void *pv ){uint8_t *puc = ( uint8_t * ) pv;BlockLink_t *pxLink;    if( pv != NULL )    {        /* 根据参数地址找出内存块链表结构 */        puc -= xHeapStructSize;        pxLink = ( void * ) puc;        /* 检查这个内存块确实被分配出去 */        if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 )        {            if( pxLink->pxNextFreeBlock == NULL )            {                /* 将内存块标识为"空闲" */                pxLink->xBlockSize &= ~xBlockAllocatedBit;                vTaskSuspendAll();                {                    /* 更新未分配的内存堆大小 */                    xFreeBytesRemaining += pxLink->xBlockSize;                    traceFREE( pv, pxLink->xBlockSize );                    /* 将这个内存块插入到空闲块链表中,按照内存块地址大小顺序 */                    prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );                }                ( void ) xTaskResumeAll();            }        }    }}

如图4-2所示的内存堆示意图,如果我们将32字节的“已分配空间2”释放,由于这个内存块的上面和下面都是空闲块,所以在将它插入到空闲块链表的过程在中,会先和“剩余空闲块1”合并,合并后的块再和“剩余空闲块2”合并,这样组成一个大的空闲块,如图4-3所示:
  图4-3:内存释放后,会和相邻的空闲块合并4.3获取当前未分配的内存堆大小:xPortGetFreeHeapSize() 在内存申请和内存释放函数中以及多次提到过变量xFreeBytesRemaining。它就是一个计数器,不能说明内存堆碎片信息。

size_t xPortGetFreeHeapSize( void ){    return xFreeBytesRemaining;}

4.4获取未分配的内存堆历史最小值:xPortGetFreeHeapSize() 在内存申请中讲解过变量xMinimumEverFreeBytesRemaining,这个函数很有用,通过这个函数我们可以估计出最坏情况下需要多大的内存堆,从而辅助我们合理的设置内存堆大小。

size_t xPortGetMinimumEverFreeHeapSize( void ){    return xMinimumEverFreeBytesRemaining;}

5.heap_5.c 第五种内存管理策略允许内存堆跨越多个非连续的内存区,并且需要显示的初始化内存堆,除此之外其它操作都和第四种内存管理策略十分相似。
第一、第二和第四种内存管理策略都是利用一个大数组作为内存堆使用,并且只需要应用程序指定数组的大小(通过宏configTOTAL_HEAP_SIZE定义),数组定义由内存管理策略实现。第五种内存管理策略有些不同,首先它允许跨内存区定义多个内存堆,比如在片内RAM中定义一个内存堆,还可以在片外RAM再定义内存堆;其次,用户需要指定每个内存堆区域的起始地址和内存堆大小、将它们放在一个HeapRegion_t结构体类型数组中,并需要在使用任何内存分配和释放操作前调用vPortDefineHeapRegions()函数初始化这些内存堆。
让我们看一个例子:假设我们为内存堆分配两个内存块,第一个内存块大小为0x10000字节,起始地址为0x80000000;第二个内存块大小为0xa0000字节,起始地址为0x90000000。HeapRegion_t结构体类型数组可以定义如下:

HeapRegion_t xHeapRegions[] = {  { ( uint8_t * ) 0x80000000UL, 0x10000 },   { ( uint8_t * ) 0x90000000UL, 0xa0000 },   { NULL, 0 }                 };

两个内存块要按照地址顺序放入到数组中,地址小的在前,因此地址为0x80000000的内存块必须放数组的第一个位置。数组必须以使用一个NULL指针和0字节元素作为结束,以便让内存管理程序知道何时结束。
定义好内存堆数组后,需要应用程序调用vPortDefineHeapRegions()函数初始化这些内存堆:将它们组成一个链表,以xStart链表结构开头,以pxEnd指针指向的位置结束。我们看一下内存堆数组是如何初始化的,以上面的内存堆数组为例,初始化后的内存堆如图5-1所示(32为平台,sizeof(BlockLink_t)=8字节)。
  图5-1:多个非连续内存区用作内存堆初始化示意图 一旦内存堆初始化之后,内存申请和释放都和第四种内存管理策略相同,不再单独分析。

以上就是FreeRTOS进阶内存管理示例完全解析的详细内容,更多关于FreeRTOS内存管理分析的资料请关注我们其它相关文章!

(0)

相关推荐

  • FreeRTOS进阶任务通知示例分析

    目录 在FreeRTOS版本V8.2.0中推出了全新的功能:任务通知.在大多数情况下,任务通知可以替代二进制信号量.计数信号量.事件组,可以替代长度为1的队列(可以保存一个32位整数或指针值),并且任务通知速度更快.使用的RAM更少!我在< FreeRTOS系列第14篇---FreeRTOS任务通知>一文中介绍了任务通知如何使用以及局限性,今天我们将分析任务通知的实现源码,看一下任务通知是如何做到效率与RAM消耗双赢的.        在<FreeRTOS高级篇6---FreeRTOS信

  • FreeRTOS实时操作系统队列基础

    目录 本文介绍队列的基本知识,详细源码分析见<FreeRTOS高级篇5---FreeRTOS队列分析> 1.FreeRTOS队列 队列是主要的任务间通讯方式.可以在任务与任务间.中断和任务间传送信息.大多数情况下,队列用于具有线程保护的FIFO(先进先出)缓冲区:新数据放在队列的后面.当然,数据也可以放在队列的前面,在下一篇讲队列API函数时,会涉及到数据的存放位置. 图1-1:读写队列 图1-1所示的队列中,最多能保存5个项目,并且假设队列永远不会满.任务A使用API函数xQueueSend

  • FreeRTOS进阶之队列示例分析

    目录 FreeRTOS提供了多种任务间通讯方式,包括:任务通知(版本V8.2以及以上版本)队列二进制信号量计数信号量互斥量递归互斥量      其中,二进制信号量.计数信号量.互斥量和递归互斥量都是使用队列来实现的,因此掌握队列的运行机制,是很有必要的.      队列是FreeRTOS主要的任务间通讯方式.可以在任务与任务间.中断和任务间传送信息.发送到队列的消息是通过拷贝实现的,这意味着队列存储的数据是原数据,而不是原数据的引用.先看一下队列的数据结构: typedef struct Que

  • FreeRTOS实时操作系统信号量基础

    目录 前言 1.信号量简介 2.二进制信号量 3.计数信号量 4.互斥量 5.递归互斥量 前言 本文介绍信号量的基础知识,详细源码分析见<FreeRTOS进阶FreeRTOS信号量分析> 1.信号量简介 FreeRTOS的信号量包括二进制信号量.计数信号量.互斥信号量(以后简称互斥量)和递归互斥信号量(以后简称递归互斥量). 我们可以把互斥量和递归互斥量看成特殊的信号量.互斥量和信号量在用法上不同: 信号量用于同步,任务间或者任务和中断间同步:互斥量用于互锁,用于保护同时只能有一个任务访问的资

  • FreeRTOS进阶列表和列表项示例分析

    目录 前言 1.初始化列表 2.初始化列表项 4.将列表项插入到列表末端 前言 FreeRTOS内核调度大量使用了列表(list)和列表项(list item)数据结构.我们如果想一探FreeRTOS背后的运行机制,首先遇到的拦路虎就是列表和列表项.对于FreeRTOS内核来说,列表就是它最基础的部分.我们在这一章集中讲解列表和列表项的结构以及操作函数,在下一章讲解任务创建时,会用到本章的知识点. 列表被FreeRTOS调度器使用,用于跟踪任务,处于就绪.挂起.延时的任务,都会被挂接到各自的列表

  • FreeRTOS实时操作系统队列的API函数讲解

    目录 FreeRTOS为操作队列提供了非常丰富的API函数,包括队列的创建.删除,灵活的入队和出队方式.带中断保护的入队和出队等等.下面就来详细讲述这些API函数. 1.获取队列入队信息数目1.1函数描述 UBaseType_t uxQueueMessagesWaiting( QueueHandle_t xQueue ); 返回队列中存储的信息数目.具有中断保护的版本为uxQueueMessagesWaitingFromISR(),原型为:UBaseType_t uxQueueMessagesW

  • FreeRTOS信号量API函数基础教程

    目录 前言 1创建二进制信号量 1.1函数描述 2创建计数信号量 3创建互斥量 4创建递归互斥量 5删除信号量 6获取信号量 7获取信号量(带中断保护) 8获取递归互斥量 9释放信号量 10释放信号量(带中断保护) 11释放递归互斥量 12获取互斥量持有任务的句柄 前言 FreeRTOS的信号量包括二进制信号量.计数信号量.互斥信号量(以后简称互斥量)和递归互斥信号量(以后简称递归互斥量).我们可以把互斥量和递归互斥量看成特殊的信号量. 信号量API函数实际上都是宏,它使用现有的队列机制.这些宏

  • FreeRTOS进阶内存管理示例完全解析

    内存管理对应用程序和操作系统来说都非常重要.现在很多的程序漏洞和运行崩溃都和内存分配使用错误有关. FreeRTOS操作系统将内核与内存管理分开实现,操作系统内核仅规定了必要的内存管理函数原型,而不关心这些内存管理函数是如何实现的.这样做大有好处,可以增加系统的灵活性:不同的应用场合可以使用不同的内存分配实现,选择对自己更有利的内存管理策略.比如对于安全型的嵌入式系统,通常不允许动态内存分配,那么可以采用非常简单的内存管理策略,一经申请的内存,甚至不允许被释放.在满足设计要求的前提下,系统越简单

  • FreeRTOS进阶之队列示例完全解析

    目录 前言 1.队列创建函数 2.入队 2.1 xQueueGenericSend() 2.2 xQueueGenericSendFromISR () 3.出队 前言 FreeRTOS提供了多种任务间通讯方式,包括: 任务通知(版本V8.2以及以上版本) 队列 二进制信号量 计数信号量 互斥量 递归互斥量 其中,二进制信号量.计数信号量.互斥量和递归互斥量都是使用队列来实现的,因此掌握队列的运行机制,是很有必要的. 队列是FreeRTOS主要的任务间通讯方式.可以在任务与任务间.中断和任务间传送

  • FreeRTOS进阶之任务创建完全解析

    目录 在FreeRTOS基础系列<FreeRTOS系列第10篇---FreeRTOS任务创建和删除>中介绍了任务创建API函数xTaskCreate(),我们这里先回顾一下这个函数的声明: BaseType_t xTaskCreate( TaskFunction_tp vTaskCode, const char * constpcName, unsigned short usStackDepth, void *pvParameters, UBaseType_t uxPriority, Task

  • FreeRTOS进阶之系统延时完全解析

    目录 1. 相对延时函数vTaskDelay() 2. 绝对延时函数vTaskDelayUntil() 3.小结 FreeRTOS提供了两个系统延时函数:相对延时函数vTaskDelay()和绝对延时函数 vTaskDelayUntil().相对延时是指每次延时都是从任务执行函数vTaskDelay()开始,延时指定的时间结束: 绝对延时是指每隔指定的时间,执行一次调用vTaskDelayUntil()函数的任务.换句话说:任务以固定的频率执行. 在<FreeRTOS任务控制>一文中,已经介绍

  • linux 内存管理机制详细解析

    物理内存和虚拟内存我们知道,直接从物理内存读写数据要比从硬盘读写数据要快的多,因此,我们希望所有数据的读取和写入都在内存完成,而内存是有限的,这样就引出了物理内存与虚拟内存的概念. 物理内存就是系统硬件提供的内存大小,是真正的内存,相对于物理内存,在linux下还有一个虚拟内存的概念,虚拟内存就是为了满足物理内存的不足而提出的策略,它是利用磁盘空间虚拟出的一块逻辑内存,用作虚拟内存的磁盘空间被称为交换空间(Swap Space). 作为物理内存的扩展,linux会在物理内存不足时,使用交换分区的

  • 关于C/C++内存管理示例详解

    1.内存分配方式 在C++中,内存分成五个区,分别是堆.栈.自由存储区.静态存储区和常量存储区. 1) 栈 执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放.栈内存分配运算内置处理器指令集中,效率很高,但分配的内存容量有限. 2) 堆 由new分配的内存块,释放由程序员控制.如果程序员没有释放,那么就在程序结束的时候,被操作系统回收. 3) 自由存储区 由malloc等分配的内存块,用free结束自己的生命. 4) 静态存储区 全局变量和静态变量被分配到

  • FreeRTOS动态内存分配管理heap_2示例

    目录 heap_2.c 内存堆管理 分配 初始化内存堆 把新构造的结构体插入空闲链表 释放 还剩空闲字节数 适用范围.特点 heap_2.c 内存堆管理 heap_2和heap_1一样是开辟一个大数组作为堆空间供用户使用,但是采用单项不循环链表来管理内存的分配释放,主要思想是用链表把内存块串起来,数据结构如下 /* Define the linked list structure. This is used to link free blocks in order of their size.

  • FreeRTOS进阶之任务通知示例完全解析

    目录 前言 1.发送通知 1.1 xTaskGenericNotify() 1.2 vTaskNotifyGiveFromISR() 1.3 xTaskGenericNotifyFromISR() 2.等待通知 2.1 ulTaskNotifyTake() 2.2 xTaskNotifyWait() 前言 在FreeRTOS版本V8.2.0中推出了全新的功能:任务通知.在大多数情况下,任务通知可以替代二进制信号量.计数信号量.事件组,可以替代长度为1的队列(可以保存一个32位整数或指针值),并且

随机推荐