由于是根据自己的理解写的东西,如有错误请指出
## 它的优势
首先对与用过FreeRTOS人的来说,系统对于任务创建所用需要到的任务堆栈和任务控制块都是通过动态内存的方式来实现的,其算法比较好的都在 heap_4.c这个文件里面,当然heapc_5.c 与 heap_4.c基本一样的
- void *pvPortMalloc( size_t xWantedSize ) 申请内存
- void vPortFree( void *pv ) 释放内存
从这两个函数的源码分析可以出它最主要的优势 就是可以将一些内存碎片整合重新变成一块大的内存
当然这里的内存碎片是指 当它需要内存的时候是从一块大的内存里面割一块出来分配给申请的对象
当申请的对象不需要这块内存的时候即释放这块申请的内存,此时这块内存不会放回之前的大内存块,而是作为一块新的小内存,最终这块大的内存被一分为2,而这一个新的小内存块就是内存碎片,
那么他的危害就是随着不断的申请大小不同的内存块会使大的内存块不断割裂出小内存块,此时如果我要申请一个大的内存块,则无法申请,程序此时可能会蹦
所以接下来分析一下上述两个函数的源码,从中借鉴它做出自己的内存管理函数,这才是目的
内存链表初始化
static void prvHeapInit( void )
{
BlockLink_t *pxFirstFreeBlock;
uint8_t *pucAlignedHeap;
size_t uxAddress;
size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;
/* uxAddress是内存数组的首地址 */
uxAddress = ( size_t ) ucHeap;
if( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 ) /* 首地址做8字节对齐,就是可以整除8的地址*/
{
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;
uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize;
uxAddress -= xHeapStructSize;
uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
/* 上面三行是计算尾结点指针地址,并做8字节对齐*/
/* 对尾结点初始化*/
pxEnd = ( void * ) uxAddress;
pxEnd->xBlockSize = 0;
pxEnd->pxNextFreeBlock = NULL;
/* . 初始化pxFirstFreeBlock xStart要挂载它 */
pxFirstFreeBlock = ( void * ) pucAlignedHeap;
pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;
pxFirstFreeBlock->pxNextFreeBlock = pxEnd;
/* 更新 xFreeBytesRemaining 即内存剩余大小*/
xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
/* 对未使用过的内存块做标记 */
xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );
}
最终结果看图最好,来自原子
内存申请
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;
vTaskSuspendAll(); /* 任务调度器挂起,是防止被任务切换 */
{
/* 第一次开辟内存,尾结点指针是空的,所以要初始化内存的链表(它是驱动内存的) */
if( pxEnd == NULL )
{
prvHeapInit(); //初始化内存链表
}
if( ( xWantedSize & xBlockAllocatedBit ) == 0 ) /* 内存块是否被使用,一定被使用它的最高位是会置一的,原则是这样的,但是这里没有必要,因为这个参数最高位肯定是0,这里写的多余了 */
{
if( xWantedSize > 0 ) /* 申请的内存块要大于0 */
{
xWantedSize += xHeapStructSize; /* 申请的内存大小= 自身大小+结构体内存(因为节点是要内 存,也要申请)注意这里内存管理是通过链表来完成的,所以每次申请一块内存都要为链表节点(即结构体)申请内存。这两块同时作用才是要申请的大小 */
/* 内存大小做8字节对齐处理,即如果这块内存大小不是8的倍数,就要变成可以整除8的数,最后会做小小的扩张, 当然这样是为了可以出来 float型类似于这种8字节的数据 */
if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
{
/* Byte alignment required. */
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
}
}
if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) ) /* 内存同时要小可以内存的最大值*/
{
pxPreviousBlock = &xStart;
pxBlock = xStart.pxNextFreeBlock;
/* 找出一个内存块,它的大小比自己申请的要大 */
while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
{
pxPreviousBlock = pxBlock;
pxBlock = pxBlock->pxNextFreeBlock;
}
//如果找到这块内存的话 那么pxBlock=pxPreviousBlock->pxNextFreeBlock 是这块内存的首地址
/* If the end marker was reached then a block of adequate size
was not found. pxPreviousBlock 是前一块 pxBlock 是下一块就是比申请的内存要大的这一块 */
if( pxBlock != pxEnd ) /* 这块内存地址不是结尾节点内存地址,如果是说明没有内存可以申请 */
{
/* pxPreviousBlock->pxNextFreeBlock=pxBlock
pxBlock要偏移一个结构体内存的大小=当前要申请内存的首地址pvReturn */
pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );
/* This block is being returned for use so must be taken out of the list of free blocks. 从链表删除这块要申请的内存(为了下一次更加快速的申请到内存,因为链表的都是未申请的内存,申请了的都删掉了) */
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
/* 如果申请的内存太多从一个大的内存块里面即小于这个heapMINIMUM_BLOCK_SIZE阈值,那么就不需要将这块内存一分为2,整块内存将分割出去,反之就是这次申请内存太小在找到的这个内存块里,就要将这块内存一分为2,然后重新插入到链表里面 */
if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
{
/* 从内存块一分为2 剩余的插会链表 所以更新 pxNewBlockLink 地址= */
pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
/* 更新将要插入链表内存块pxNewBlockLink 里面xBlockSize这个成员的大小*/
pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
// pxBlock->xBlockSize = xWantedSize; 这句我放到下面解释,因为移动它的位置没有影响的
/* Insert the new block into the list of free blocks. pxNewBlockLink插入链表,下面介绍 */
prvInsertBlockIntoFreeList( pxNewBlockLink );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
xFreeBytesRemaining -= pxBlock->xBlockSize;/* 更新内存剩余大小 xFreeBytesRemaining 是全局变量 */
if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
{
xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
}
/* The block is being returned - it is allocated and owned
by the application and has no "next" block. 上面pvReturn就是我们申请到内存的起始地址,它偏移了一个节点的大小,那么这个节点的东西即这个结构体要做赋值,这在内存释放的时候会访问这个节点的地址从而可以内存释放 */
pxBlock->xBlockSize = xWantedSize; //申请了多少内存
pxBlock->xBlockSize |= xBlockAllocatedBit;/* 内存标记使用*/
pxBlock->pxNextFreeBlock = NULL; //情空它的指向
}
}
}
}
( void ) xTaskResumeAll(); //任务调度器解挂
if( pvReturn == NULL ) //内存申请失败 然后调用的钩子函数,这个不用管了,可以改成串口打印
{
extern void vApplicationMallocFailedHook( void );
vApplicationMallocFailedHook();
}
return pvReturn;
总结内存申请
- 尾结点是空,就初始化内存管理的一些参数
- 计算申请内存大小=申请字节数+自身节点的字节数 如果没有满足8个字节对齐,还要做8字节对齐处理最后可能会再有小小的偏移
- 从链表找出一块内存,比当前申请内存的要大,就将这个内存分配给申请者,同时从链表删除掉这块内存
- 计算出申请到内存的首地址,就是上面链表找出的那个内存的首地址在偏移一个链表节点的大小
- 如果这块申请的内存占链表找出来那块内存的空间太小,就将链表那个内存一分为2,将剩余的内存重新插入会链表 ,同时做内存碎片的处理 prvInsertBlockIntoFreeList( )
- 对自身节点的成员做赋值,同时标记这块内存被使用
- 更新内存剩余大小
比较重要
链表节点插入函数,这个函数就是解决内存碎片的核心函数,就是 通过历遍链表通过节点现在的地址,找出的前驱节点和后继节点 ,前驱节点地址+自身内存的大小=当前pxBlockToInsert 的地址,表示现在这块内存与上一块内存相连,则更新前驱节点的数据, 同理 后继节点=pxBlockToInsert 的地址+自身内存的大小 则表示现在这块内存与下一块内存相连 就跟新pxBlockToInsert 这个节点的数据当然如果在此前他已经与前驱节点相连,则在此更新前驱节点的数据 ,最后就是链表节点的插入操作了
截原子的图片
static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert )
{
BlockLink_t *pxIterator;
uint8_t *puc;
/* 遍历列表,直到找到一个比插入的块地址更高的块就是pxIterator->pxNextFreeBlock 即从链表找出比当前释放内存首地址还要高的节点, 现在 pxIterator是 pxBlockToInsert 前一块内存的节点,pxIterator->pxNextFreeBlock 是pxBlockToInsert 下一块的节点 */
for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
{
/* Nothing to do here, just iterate to the right position. */
}
/* puc=前一块内存的地址 */
puc = ( uint8_t * ) pxIterator;
/* puc+自身内存= 当前要释放的内存地首地址 相等表示要释放的这一块内存与上一块内存连续 */
if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
{
pxIterator->xBlockSize += pxBlockToInsert->xBlockSize; /* 两块内存大小做累加 */
pxBlockToInsert = pxIterator; /* pxBlockToInsert 指向上一块内存首地址 */
}
else /* 不相等表示要释放的这一块内存与上一块内存不连续,则啥也不干 */
{
mtCOVERAGE_TEST_MARKER();
}
/* 下面是判断 当前要释放的内存与他下一块内存连不连续,同时做链表插入工作 */
/* puc 指向当前释放内存首地址 (主要如果与上一块连续,则指向上一块内存的首地址) */
puc = ( uint8_t * ) pxBlockToInsert;
/* puc+自身内存= 当前要下一块内存地首地址 相等表示要释放的这一块内存与下一块内存连续*/
if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock )
{
if( pxIterator->pxNextFreeBlock != pxEnd ) /* 下一块内存首地址是不是链表尾结点 */
{
/* 不是*/
pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize; /* 当前要释放的内存大小与下一块内存大小做累加 */
pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock; /* pxBlockToInsert 的 pxNextFreeBlock 指向下一块的下一块内存首地址 */
}
else /* 是尾结点 */
{
pxBlockToInsert->pxNextFreeBlock = pxEnd; /* pxBlockToInsert 的 pxNextFreeBlock 指向尾结点 */
}
}
else /* 不连续 */
{
pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock; /* pxBlockToInsert 的 pxNextFreeBlock 指向下一块内存首地址 */
}
/* 如果与前一块内存不相连,则当前释放的内存还有与前一块内存的结点相连 */
if( pxIterator != pxBlockToInsert )
{
pxIterator->pxNextFreeBlock = pxBlockToInsert;
}
}
内存释放
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 ) /* ?? */
{
/* The block is being returned to the heap - it is no longer
allocated.清除标记为 */
pxLink->xBlockSize &= ~xBlockAllocatedBit;
vTaskSuspendAll();
{
/*xFreeBytesRemaining 更新内存剩下的大小 */
xFreeBytesRemaining += pxLink->xBlockSize;
/* 释放内存处理,里面为内存碎片减少做了处理:
主要是对其上一片内存和它的下一片内存与这个将要释放的这一片内存 是否是连续的(通过地址),连续一块就连一块,两块就两块不连接就直接插入链表 */
prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
}
( void ) xTaskResumeAll();
}
}
}
}
内存释放总结
- 释放内存非空
- 找到自身链表节点
- 判断该内存块是否是申请的,就是申请的标志位,是申请过的就先去掉申请标志位,然后重新插回链表,同时最内存碎片处理
- 更新内存剩余大小
最后在说一下其实内存管理就是处理一个大的数组