在FreeRTOS8.0.1这个版本中,一共有四个内存堆模型。这一次讲的就是第二个模型Heap_2.c。从一开始就可以看到注释中对Heap_2的模型解释:这是对pvPortMalloc()和vPortFree()的简单实现,除了可以分配内存之外,还可以对已分配的内存进行回收,但相邻空闲块不会进行合并,因此会造成一定的内存碎片。
(A sample implementation of pvPortMalloc() and vPortFree() that permits allocated blocks to be freed, but does not combine adjacent free blocks into a single larger block (and so will fragment memory).)
在Heap_2中,由于开始支持对内存进行回收,因此FreeRTOS以空闲块对内存堆进行管理,并且使用了最佳适配算法(best fit algorithm)去进行内存的分配。
首先,还是由内存中开辟一个静态数组ucHeap[ configTOTAL_HEAP_SIZE ]作为FreeRTOS的内存堆。同样也会因为对齐的原因FreeRTOS对内存堆的可用空间进行了调整,并定义了常量configADJUSTED_HEAP_SIZE。(具体已在上一篇《内存管理Heap_1.c》中介绍)
接下来可以留意Heap_2.c中最重要的结构struct A_BLOCK_LINK。由于FreeRTOS用空闲块对内存堆进行管理,于是用这一个结构来形成一条空闲块链表对空闲块进行组织和管理。
/* Define the linked list structure. This is used to link free blocks in order of their size. */
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;
这个结构有两个成员,第一个是节点Next指针pxNextFreeBlock,第二个是空闲块大小。一个空闲块就用这样的一个结构节点表示,所有节点通过Next指针形成一条空闲块链表。FreeRTOS还定义了这个链表的头xStart和尾xEnd。
/* Create a couple of list links to mark the start and end of the list. */
static BlockLink_t xStart, xEnd;
与Heap_1不同,在Heap_2中会有一个堆初始化的过程prvHeapInit()。这一个过程被pvPortMalloc()调用,但只被调用一次。主要还是对内存堆进行对齐还有对空闲块表进行初始化工作。下面是它的工作流程。
首先,初始化流程对整个内存堆进行地址对齐工作。对齐的原因的原理与Heap_1一样。
/* Ensure the heap starts on a correctly aligned boundary. */
pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) & ( ( portPOINTER_SIZE_TYPE ) ~portBYTE_ALIGNMENT_MASK ) );
在获取到地址对齐后的内存堆首地址之后,就要对空闲块链表进行初始化。留意,xStart是链表头,并不表示一个空闲块,xEnd是链表尾,也不表示一个空闲块,但记录着整个堆的大小。其代码如下:
/* xStart is used to hold a pointer to the first item in the list of free
blocks. The void cast is used to prevent compiler warnings. */
xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
xStart.xBlockSize = ( size_t ) 0;
/* xEnd is used to mark the end of the list of free blocks. */
xEnd.xBlockSize = configADJUSTED_HEAP_SIZE;
xEnd.pxNextFreeBlock = NULL;
/* To start with there is a single free block that is sized to take up the
entire heap space. */
pxFirstFreeBlock = ( void * ) pucAlignedHeap;
pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE;
pxFirstFreeBlock->pxNextFreeBlock = &xEnd;
经过上面的初始化流程后,整个链表如下图所示。注意,xStart和xEnd是存在于静态存储区中,并不在FreeRTOS申请的内存堆数组中,但初始时第一个节点却在内存堆数组中。我用的编译器是Keil MDK 5.11,并且将FreeRTOS移植到STM32上,因此一个A_BLOCK_LINK的大小为8个字节。
整个初始化的流程就完成了。接下来看看pvPortMalloc()分配内存的流程。如Heap_1一样,在真正开始分配内存时,用vTaskSuspendAll()挂起所有任务,防止分配内存的过程被中断,确保操作的原子性。紧接着,如果是第一次调用pvPortMalloc(),则调用prvHeapInit()对内存堆和空闲块链表进行初始化。由于在Heap_2中将内存堆用空闲块处理,因此用户每申请一次内存,FreeRTOS都会在申请的空间前加上空闲块头部BlockLink_t,用于记录分配出去的空间的大小。因此,真正要分配的内存空间大小就等于用户申请的内存空间大小加上空闲块头部的大小。加上头部之后,还要对整个大小进行对齐。因此,在真正分配空间之前,FreeRTOS都对用户申请的空间大小进行了调整。如下面的代码所示。
/* The wanted size is increased so it can contain a BlockLink_t
structure in addition to the requested amount of bytes. */
if( xWantedSize > 0 )
{
xWantedSize += heapSTRUCT_SIZE;
/* Ensure that blocks are always aligned to the required number of bytes. */
if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0 )
{
/* Byte alignment required. */
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
}
}
在这一段代码里,有一个地方要注意的就是这里用到的空闲块头部并不是直接sizeof(BlockLink_t),而是heapSTRUCT_SIZE,这个常量也定义在Heap_2.c中,这是对空闲块头部的大小再进行了一次大小对齐。
接下来做了一个分配判断。xWantedSize0,还看得我很迷糊的。本来xWantedSize就是unsigned int,是大于等于0的,一下子没有留意到等于0是什么情况。等于0,就是加上空闲块头之后的大小变为0了?这是神马情况?!看来这个判断条件很另人费解啊。过了分配判断之后,接下来就是best fit algorithm的实现了。在这里,空闲块的大小是按从小到大的顺序排列的。因此,遍历链表,找到第一块比申请空间大的空闲块即为最合适的空闲块。然后返回这个空闲块头后的首地址。注意,一定是空闲块头后的首地址哦,要是直接将空闲块的首地址返回的话,那用户就会将空闲块头修改了。另一个要注意的地方是,要是分配出去的空闲块的剩余空间要是比两倍的空闲块头还要大,则将分配出去的这个空闲块分割剩余的空间出来,重新放到空闲块链表中。例如,初始化后只有一个空闲块,这个空闲块大小为17KB。经过调整后的用户申请空间大小为1KB,则FreeRTOS就从这个空闲块靠近块首的地方分割出1KB出来分配出去,剩余的16KB则重新放回空闲块链表里。以便下一次继续分配。这一个切割空闲块的代码如下。
/* 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 ) );
}
在上面的一段代码里,有一个值得注意的宏prvInsertBlockIntoFreeList()。这个宏的作用是将空闲块重新添加到空闲块链表中。注意,并不能将分割出来的空闲块放到原空闲块的位置中,因为链表中的空闲块是按从小到大的顺序排列的,这个宏的作用就是将新空闲块插入到合适的链表位置中。这是一个简单的链表操作,非常简单,也不必详细说明了。这个宏的具体代码如下。
/*
* Insert a block into the list of free blocks - which is ordered by size of
* the block. Small blocks at the start of the list and large blocks at the end
* of the list.
*/
#define prvInsertBlockIntoFreeList( pxBlockToInsert ) \
{ \
BlockLink_t *pxIterator; \
size_t xBlockSize; \
\
xBlockSize = pxBlockToInsert->xBlockSize; \
\
/* Iterate through the list until a block is found that has a larger size */ \
/* than the block we are inserting. */ \
for( pxIterator = &xStart; pxIterator->pxNextFreeBlock->xBlockSize < xBlockSize; pxIterator = pxIterator->pxNextFreeBlock ) \
{ \
/* There is nothing to do here - just iterate to the correct position. */ \
} \
\
/* Update the list to include the block being inserted in the correct */ \
/* position. */ \
pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock; \
pxIterator->pxNextFreeBlock = pxBlockToInsert; \
}
完成以上操作后,修改剩余空闲块空闲大小xFreeBytesRemaining,整个分配内存的工作就差不多结束了。接下来FreeRTOS调用一个调试信息用的宏traceMALLOC(),恢复所有挂起的任务,再根据宏配置调用勾子函数vApplicationMallocFailedHook(),最后返回分配出来的空间地址pvReturn(成功时为空间地址,失败时为NULL)。这样整个pvPortMalloc()就结束了。
接下来要继续剖析的是Heap_1中所没有具体实现的vPortFree()。这一个函数非常地短,首先判断要释放的内存是否为空。要是不为空,则寻找这个内存空间的空闲块头,然后挂起所有任务,按照空闲块头的信息把它重新插入到空闲块链表中,最后调用调试信息宏,恢复挂起的任务就结束了。其代码如下。
void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;
if( pv != NULL )
{
/* The memory being freed will have an BlockLink_t structure immediately
before it. */
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();
}
}
不过在仔细想想这一段代码,我觉得这一段代码是有点问题,就是判断要释放内存的有效性。这里它只是很简单地判断传进来的指针是否为空而已,但是要是这个指针不为空,但却在FreeRTOS申请的内存堆数组之外呢?这样的话就会修改到内存的其它部分了,非常危险的操作。因此我觉得,如果要修改的话,应该还要加上判断传入进来的地址是否在有效的地址范围内,如下面代码所示。至于我这一个想法是否有问题,希望大家能讨论一下,这样我也可以从大家的讨论中学习学习。
if( pv!=NULL)
{
if( pv >= &ucHeap[ pucAlignedHeap ] && pv <= &ucHeap[ pucAlignedHeap + configADJUSTED_HEAP_SIZE ] )
{
/* the operation of adding the block to the list of free blocks */
}
}
到这里,Heap_2的重点部分就已经剖析完了。剩下的xPortGetFreeHeapSize()只是返回剩余堆大小而已,vPortInitialiseBlocks()实际上啥也没实现,根据注释它的作用只是防止链接器放警告而已。
总结:Heap_2比Heap_1的代码更长,要剖析的知识点更多。一开始看的时候还觉得有点怕怕,怕剖着剖着就不知道怎么剖了。在剖的过程中还有很多知识点要翻翻课本,例如在看到最佳适配算法(best fit algorithm)时,要翻翻Andrew S. Tanenbaum写的《Modern Operating Systems》。经过这样的实例剖析之后,我发觉对以前学的东西有了更深刻的理解。以前上课只是说说有这个算法而已,具体怎么实现却完全没有讲。直到现在,我才发觉原来这个算法的真实应用,真是另我大开眼界。以后学习的路还很长,剖析完FreeRTOS还有很长的一段距离。要继续加油加油!