【RTOS学习】FreeRTOS中的链表 | 堆的管理

🐱作者:一只大喵咪1201
🐱专栏:《RTOS学习》
🔥格言:你只管努力,剩下的交给时间!
图

目录

🥩FreeRTOS中的链表

链表是FreeRTOS的核心结构,它让系统的功能正常运行,本喵下面来解释一下FreeRTOS中的链表结构以及操作。

图
如上图所示是FreeRTOS源码中的链表的定义List_t,这是一个链表头,重要的成员变量有三个:

  • volatile UBaseType_t uxNumberOfItems:表示链表中包含的节点个数。
  • ListItem_t * configLIST_VOLATILE pxIndex:这是一个指针变量,表示当前正在操作的节点。
  • MiniListItem_t xListEnd:这是一个结构体变量,从这个变量可以找到链表中所有节点。

tu
如上图所示MinListItem_t结构体定义,它也有三个重要的成员变量:

  • configLIST_VOLATILE TickType_t xItemValue:这是一个值,需要排序的时候才有意义,后面会见识到。
  • struct xLIST_ITEM * configLIST_VOLATILE pxNext:这是一个指针变量,该变量指向链表中的第一个链表项。
  • struct xLIST_ITEM * configLIST_VOLATILE pxPrevious:这也是一个指针变量,该变量指向链表中的最后一个链表项。

tu
如上图所示是链表项ListItem_t的定义,它包含五个重要的成员变量:

  • configLIST_VOLATILE TickType_t xItemValue:该值作为链表项中的一个数值,在排序时才会起作用。
  • struct xLIST_ITEM * configLIST_VOLATILE pxNext:该指针变量指向下一个链表项。
  • struct xLIST_ITEM * configLIST_VOLATILE pxPrevious:该指针变量指向前一个链表项。
  • void * pvOwner:这也是一个指针变量,指向该链表项所属的容器。以后本喵会介绍。
  • struct xLIST * configLIST_VOLATILE pxContainer:这也是一个指针变量,指向该链表项所在的List_t类型链表头。
  • 从链表头以及链表项的定义中就可以看出,FreeRTOS中的链表是一个带头双向循环链表。

tu
如上图所示便是FreeRTOS中链表的链接关系,链表头结构体成员xListEnd中的pxNext指向第一个链表项的起始地址,链表项中的pxNext指向下一个链表项的起始地址,如此下去,最后一个链表项中的pxNext指向链表头中的xListEnd

  • 此时链表头和链表项中的所有pxNext构成一个环路。

链表头中的结构体成员xListEnd中的pxPrevious指向最后一个链表项的起始地址,该链表项中的pxPrevious指向前一个链表项,如此下去,第一个链表项中的pxPrevious指向链表头中的xListEnd

  • 此时链表头和链表项中的所有pxPrevious构成另一个环路。

tu
如上图所示,由于真正的指向关系不直观,所以本喵后面使用这样的示意图来表示这个链表,要记住无论是pxNext还是pxPrevious指向的都是链表项的起始地址。

🥞初始化

图
如上图所示链表初始化函数vListInitialise,在该函数中对传入的链表pxList主要进行了四步操作:

  1. 让链表头中当前操作节点指针pxIndex指向自己的xListEnd
  2. xListEnd中的xItemValue = portMAX_DELAY,用该值来区别这是链表尾而不是链表项。
  3. xListEnd中的pxNextpxPrevious都指向自己的xListEnd,初步形成两个环路。
  4. uxNumberOfItems = 0表示当前链表中没有链表项,是一个空链表。

此时一个初步的链表就形成了,操作的都是链表头自己。

图
如上图所示链表项初始化函数vListInitialiseItem,在该函数中,仅是将链表项中的pxContainer设置为NULL,因为此时并不知道该链表项属于哪个链表头。

🥞尾部插入

tu
如上图所示插入链表尾部的函数vListInsertEnd,在该函数中,总体进行了四步操作:

  1. 从链表头中获得当前正常操作的链表项pxIndex
  2. 将新的链表项插入到pxIndexpxIndex->pxPrevious之间。
  3. 让新链表项的pxContainer = pxList,让其找到所属的链表。
  4. 将链表头中记录链表项个数的unNumberOfItems加一。

图
如上图所示便是尾部插入新链表项的结果,但是它并不是插入到了尾部呀。

假设现在正在操作的是编号为2的链表项,所以pxIndex指向它,如果没有新链表项插入的话,这几个链表项的操作顺序是2->3->1

此时要插入新的链表项,但是并不能影响原本的操作顺序,否则就破坏了公平性,所以将新的链表项4插入到pxIndexpxIndex->pxPreviou之间,此时这几个链表项的操作顺序是2->3->1->4,并不会影响原来三个链表项的操作顺序。

  • 尾插时要保证公共性,插入于正在操作链表项pxIndex和它的前一个链表项pxIndex->pxPrevious之间。

上面所演示的是链表中已经存在链表项时尾插的结果,如果是一个空链表呢?

图
如上图所示第一次尾插的结果,在插入之前pxIndexpxIndex->pxPrevious都是指向的链表头中的xListEnd,当新节点插入时,pxIndex->pxPrevious指向新节点,pxIndex->pxNext也指向新节点,新节点的pxIndexpxPrevious都指向链表头中的xListEnd

所以说,尾插函数vListInsertEnd可以实现第一次插入和已经存在多个链表项时再插入新链表项两个场景。

🥞按顺序插入

图

如上图所示按照链表项中的xItemValue值插入函数vListInsert,该函数中主要进行了四步操作:

  1. 获取新插入链表项中的xItemValue值。
  2. 根据xItemValue值在链表中寻找合适位置:
    • xItemValue = portMAX_DELAY说明新的链表项应该插入到链表的最后面,直接让插入位置pxIterator等于链表的最后一项。
    • xItemValue不是最大,则迭代寻找合适位置,pxIterator从链表头开始,直到pxIterator->pxNext指向的链表项中的xItemValue大于新链表项的xItemValue,此时得到插入位置就是pxIterator后面。
  3. 插入新链表项,改变指向关系。
  4. 修改新链表项中的pxContainer,让其属于当前链表头,并且修改链表头中的链表项个数。

tu
如上图所示是插入后的结果,原本链表中的xItemValue从左到右依次是1->3->4,将xItemValue为2的新链表项插入以后,xItemValue成了1->2->3->4

🥞删除

tu
如上图所示删除指定链表项的函数uxListRemove,该函数中进行的主要操作也是四步:

  1. 从要删除的链表项中得到它所属的链表pxList
  2. 改变链表中的指向关系,让被删除链表项的前一个直接和被删除链表项的后一个建立互相的指向关系。
  3. 进行判断,如果删除的是正在操作的链表项,那么需要将pxIndex指向下一个链表项,更换正在操作的链表项。
  4. 让被删除的链表项失忆,也就是将其pxContainer = NULL,让它找不到原本所属的链表,再将链表头中链表项的个数减一,最后返回链表项的个数。

tu
如上图所示是删除后的示意图,所谓删除就是改变被删除链表项的前一个和后一个链表项的指向关系,让其从链表中脱离出来。

🥩堆的管理

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

  • 在汇编代码里指定一个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. 对申请的空间大小进行对齐。

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

tu
如上图所示,假设现在有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. 获取申请空间

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

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

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

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

🥞heap_2.c

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

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

  • pxNextFreeBlock:指向下一个空闲块
  • xBlockSize:当前空闲块的内存大小

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

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

初始化:

tu

如上图所示堆的初始化函数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之后。

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

释放内存:

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

  1. 由于传入的pv是申请者使用的地址,所以向前偏移8个字节得到该内存块的BlcokLink_t地址。
  2. 调用pxBlockToInsert将该内存块链入到管理链表中。

tu
如上图所示,假设释放的是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不一样。

分配内存:

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

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

释放内存:

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

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

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

tu
如上图所示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插入到管理链表中。

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

tu

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

  • 管理链表中的内存块合并以后变大了,再次申请比上一次释放掉的更大内存块时,合并后的内存可以继续被利用。
  • 克服了heap_2再次申请更大空间时会有内存碎片的问题。

🥞heap_5.c

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

初始化:

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

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

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

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

  1. 创建while循环来遍历所有不连续的堆区域。
  2. 求每个堆区域的对齐地址和对齐大小。
  3. 处理第一个堆区时,将其链入到xStart链表头。
  4. 在每个堆区域末尾构造BlockLink_t
  5. 将所有堆区域首尾相连在一起。

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

其他操作:

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

🥩总结

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

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

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
乘风的头像乘风管理团队
上一篇 2023年12月14日
下一篇 2023年12月14日

相关推荐