11-FreeRTOS之堆的管理

FreeRTOS中的链表 | 堆的管理

堆的管理

很多人把"堆栈"相提并论,其实"堆"、"栈"是完全没有联系。"栈"的作用我们前面已经讲了很多,"堆"是什么?

  • 在汇编代码里指定一个AREA:在汇编代码里,使用SPACE命令可以分配一段空间,这段空间就是堆:

  • 在C代码里,定义一个全局数组:该数组的大小是17KB,这段空间就是堆:

"堆"就是一块或者多块内存,我们可以从中申请一小块内存来使用,使用完毕后可以释放这一小块内存。

简单地说,一开始,"堆"是一些空闲内存,我们可以:

  • 使用malloc函数从中申请、获得一小块内存

  • 使用free函数释放这一小块内存

  • 这些malloc、free函数就是用来管理这些内存的

  • malloc、free函数可以有其他名称,比如FreeRTOS里是pvPortMalloc、vPortFree。

在FreeRTOS的源码中,默认提供了5个文件,对应内存管理的5种方法:

文件优点缺点
heap_1.c分配简单,时间确定只分配、不回收
heap_2.c动态分配、最佳匹配碎片、时间不定
heap_3.c调用标准库函数速度慢、时间不定
heap_4.c相邻空闲内存可合并可解决碎片问题、时间不定
heap_5.c在heap_4基础上支持分隔的内存块可解决碎片问题、时间不定

只能使用上诉五种方式的中的一种来管理堆,其中heap_3.c由于使用的是标准库函数中的mallocfree,速度比较慢,所以本喵这里也不做介绍,只介绍其他四种。

heap_1.c

如上图所示在堆区上申请空间的函数pvPortMalloc,其中xWantedSize是要申请的空间大小,申请过程主要分为四步:

  1. 创建返回地址pvReturn和对齐地址pucAlignedHeap,让这两个指针都为0。

  2. 对申请的空间大小进行对齐。


如上图所示宏定义#define portBYTE_ALIGNMENT 8规定了8字节对齐,什么是字节对齐呢?

如上图所示,假设现在有9个字节的空间在内存中连续排列,而我们的CPU是32位的,也就是说一次读取或者写入数据必须操作的是4字节大小的空间。

现在一共9字节,操作两次以后还剩下一个字节,再操作时仍然是4字节,所以要想操作第9个字节,就会额外多操作三个字节,此时就会导致效率低下。

  • 甚至有的CPU并不支持这种不对齐访问。


由于是8字节对齐,所以申请的空间大小必须是8的整数倍,如果不符合8字节对齐,就需要向上对齐,如申请100个字节,实际上申请的是对齐后的104字节。

  1. 找到堆区的对齐地址pucAlignedHeap

如上图所示,假设整个堆区ucHeap在内存上的起始地址是0x20000001,按照CPU每次操作4字节的规律,该地址无论如何也无法作为单次CPU的起始地址,所以操作起来不方便。

按照代码中所写,所以对齐地址求出来就是0x20000008,该地址一定是CPU操作时4字节中的起始地址。

此时对齐地址pucAlignedHeap就是指向这里,而且让xNextFreeByte + = xWantedSize,得到此次申请空间大小。

  1. 获取申请空间

如上图所示,如果是第一次申请空间,那么最后返回的就是对齐地址pucAlignedHeap所指向的地址,如果不是第一次申请,那么在pucAlignedHeap基础上增加上次空间大小的偏移量pxNextFreeByte得到的就是返回地址。

简单来说,heap_1管理堆的方式就是,通过pucAlignedHeappxNextFreeByte两个全局变量,每申请一次,就返回上一次申请后剩余空间的起始位置。


如上图释放动态空间函数vPortFree,并不支持释放空间。

  • heap_1.c里,只能用pvPortMalloc函数来申请空间,无法使用vPortFree函数来释放空间。

heap_2.c

heap_2.c里,使用链表来管理内存。链表结构体为:

这个结构体用来表示空闲块:

  • pxNextFreeBlock:指向下一个空闲块

  • xBlockSize:当前空闲块的内存大小

如上图所示,在这个结构体后面,紧跟着空闲内存。

  • 在堆空间static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]上,空闲空间往前偏移8个字节就得到BlockLink_t结构体的起始地址。

初始化:

如上图所示堆的初始化函数prvHeapInit,在该文件中定义了两个BlockLink_t类型的全局变量,一个是链表头xStart,另一个是链表尾xEnd,在该函数中主要进行四步操作:

  1. 从整个堆空间上计算对齐地址pucAlignedHeap

  2. 让链表头xStart中的pxNextFreeBlock指向对齐地址,并且将链表头中的xBlockSize = 0,因为链表头并不管理空闲内存。

  3. 让链表尾xEnd中的pxNextFreeBlock指向空,xBlockSize = configADJUSTED_HEAP_SIZE,这是堆的最大值,为了排序时方便才这样设计的。

  4. pxFirstFreeBlock指向对齐地址,并且构造BlockLink_t中的成员,让xBlockSize = configADJUSTED_HEAP_SIZEpxNextFreeBlock指向链表尾。

如上图所示初始化完成的结果,此时在整块堆空间的中,最前面的8字节就是BlockLink_t结构体,包含该空间的大小,并链入了管理空闲堆空间的链表中。

分配内存:

如上图所示分配空间函数pvPortMalloc的部分代码,主要进行了四步操作:

  1. 如果是第一次调用pvPortMalloc,则先对整个堆区进行初始化,让整个堆区作为空闲空间链入到空闲空间管理链表xStartxEnd之间。

  2. 对申请的空间大小进行对齐计算。

  3. 从空闲空间管理链表中寻找合适的空间块。

    • 假设此时链表中存在多个含有BlockLink_t的空闲块,所以根据要申请空间的大小迭代找到比申请空间大的空闲块。

  4. 通过链表找到的是空闲块的起始地址,包含BlockLink_t,所以向后偏移8个字节,得到该空闲块的对齐地址作为返回值供申请者使用。

如上图,返回的是该空闲块中红色箭头指向的地址。

如上图所示的紧接着前面pvPortMalloc函数剩下的代码,主要有四步操作,本喵这里接着前面从标号5开始讲解:

  1. 将寻找到的空闲块从空闲链表中移除,也就是修改该块前后的指向关系。

  2. 如果找到的这块空间大于所申请的字节数,将用不了的部分赋值给pxNewBlockLink

  3. 构造用不了的那部分块中的BlockLink_t结构体。

  4. 调用prvInsertBlockIntoFreeList将这个剩余的块作为空闲块插入到管理链表中。


如上图所示prvInsertBlockIntoFreeList宏函数,用来插入空闲内存块到管理链表中,主要操作分为三步:

  1. 获取插入内存块的大小xBlockSize

  2. 根据大小xBlcokSize迭代按照升序找到合适的插入位置pxIterator

  3. 将内存块pxBlockToInsert插入到pxIterator之后。

如上图所示是最终分配效果,Block1从管理链表中移除,供申请者使用,Blcok2是用不了剩下的部分,重新链入到管理链表中。

释放内存:

如上图所示释放内存函数vPortFree,主要操作有两步:

  1. 由于传入的pv是申请者使用的地址,所以向前偏移8个字节得到该内存块的BlcokLink_t地址。

  2. 调用pxBlockToInsert将该内存块链入到管理链表中。

如上图所示,假设释放的是Block1内存块,此时该内存块和原本的Block2一起在管理链表中。

  • heap_2管理的链表,支持内存的申请和释放。

  • 由于释放的内存块直接链入到管理链表,所以如果再次申请的内存比上次释放的大,那么释放的这块就无法再次利用,就会存在内存碎片。

  • 所以heap_2适合用在频繁申请和释放固定大小内存的场景。

heap_4.c

初始化:

如上图所示是使用prvHeapInit初始化完的示意图,heap_4整体上和heap_2的管理方式一样,也是通过链表和BlockLink_t来管理空闲内存块,但是不一样的是:

  • 链表尾xEnd位于整个堆空间的末尾,它并不是独立的一个变量,而是依附在空闲空间上。

  • 链表尾xEnd中的xBlockSize = 0,和heap_2中的configADJUSTED_HEAP_SIZE不一样。

分配内存:

如上图所示,分配内存时和heap_2一样,将Block1中管理链表中移除,将用不完的Block2部分再次链入到链表中,返回值也是Block1的对齐地址。

在将用不了的内存块重新链入到管理链表中时,也会调用prvInsertBlockIntoFreeList函数,heap_4heap_2的区别就在这个函数中。

释放内存:

释放内存时,和heap_2一样,也会调用到prvInsertBlockIntoFreeList函数来将释放的内存块链入到管理链表,来分析一下这个函数:

如上图所示prvInsertBlockIntoFreeList的部分代码,这里进行的操作就是寻找合适的插入位置。

  • 没有获取插入内存块的大小。

  • 迭代时按照内存块的地址寻找,当插入内存块pxBlockToInsert地址小于等于pxIterator->pxNextFreeBlock时,将内存块插入到pxIterator后面。

如上图所示prvInsertBlockIntoFreeList中紧接前面部分的后续代码,进行的操作主要分为3步:

  1. 在寻找到插入位置pxIterator以后,先判断插入内存块pxBlockToInsert和其前面的pxIterator是否紧挨着(pxIterator的地址 + 偏移量 == pxBlockToInsert的地址)。

    • 如果相等,说明pxBlockToInsertpxIterator紧挨着,可以进行合并,此时改变pxBlockToInsert的大小xBlockSizepxNextFreeBlock覆盖pxIterator

  2. 再判断判断插入内存块pxBlockToInsert和其后面的pxIterator->pxNextFreeBlock是否紧挨着(pxBlockToInsert的地址 + 偏移量 == pxIterator->pxNextFreeBlock的地址)。

    • 如果相等,说明 pxBlockToInsertpxIterator->pxNextFreeBlock是紧挨着,可以进行合并,同样仅修改pxBlockToInsertBlockLink_t覆盖pxIterator->pxNextFreeBlock即可。

  • 这里的偏移量就是块的大小pxBlockToInsert->xBlockSize

  1. 如果找到插入位置pxIterator后,发现pxBlockToInsert和它前面以及后面的内存块都不挨着,就无法实现合并,只能和heap_2一样将pxBlockToInsert插入到管理链表中。

如上图所示是在分配内存后将用不了的内存块插入管理链表和释放内存时将释放的内存块插入管理链表时,没有进行合并的结果示意图,此时和heap_2没有区别。

如上图所示是将插入的内存块与管理链表中的其他内存块合并后的结果示意图。

  • 管理链表中的内存块合并以后变大了,再次申请比上一次释放掉的更大内存块时,合并后的内存可以继续被利用。

  • 克服了heap_2再次申请更大空间时会有内存碎片的问题。

heap_5.c

heap_5heap_4机会一样,只是heap_5支持多个不连续区域作为堆。

初始化:

如上图所示结构体HeapRegion_t是用来描述用作堆的区域的,pucStartAddress表示该区域的起始地址,xSizeInbytes表示该区域的大小。

如上图所示,在使用heap_5之前,需要先定义一个全局的HeapRegion_t类型的数组array,该数组中存放的就是多个连续的堆区,然后调用vPortDefineHeapRegions来初始化所有的堆。

  • 数组中的最后一项是{NULL, 0},当发现某一个堆区的地址是NULL的时候,说明就遍历到了数组中的最后一项。

如上图所示vPortDefineHeapRegions函数,该函数内部主要进行了五步操作:

  1. 创建while循环来遍历所有不连续的堆区域。

  2. 求每个堆区域的对齐地址和对齐大小。

  3. 处理第一个堆区时,将其链入到xStart链表头。

  4. 在每个堆区域末尾构造BlockLink_t

  5. 将所有堆区域首尾相连在一起。

如上图所示是将两个不连续堆区初始化后首尾相连在一起的结果示意图。

其他操作:

在初始化以后,就可以将这些不连续的堆看成是一个堆,其他操作完全按照heap_4来就可以。

总结

对于FreeRTOS的链表操作,一定要理解透彻,因为后面的任务TCB等结构体完全依赖于链表操作,尤其注意尾部插入时不能破坏原本链表的公平性。

对于多种堆的管理方式,要知道它们适用的场合和大致的原理,根据适用场景适用合适的管理方式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值