芯片中最为稀缺珍贵的往往是存储资源,为了更好的利用它们,开发者不得不变得吝啬分配、斤斤计较到每一个字节。
FreeRtos V8.0.1针对动态内存分配提供了四种方案,分别放在heap1、heap2、heap3、heap4文件中。
heap1分配方法最为简单,代码量也相对较小,heap1只能申请不能回收,适合任务、队列等不需要执行删除操作的工程。
ucHeap占据的整块内存既为可申请的堆空间,空间大小由configTOTAL_HEAP_SIZE定义。
分配之前首先对堆的起始地址做偶对齐,这有可能会使一些起始的字节作为“占位”而丢弃掉,最终得到堆有效空间使用configADJUSTED_HEAP_SIZE代替。
(堆初始化)
Heap1分配时使用xNextFreeByte始终记录堆中可分配的首地址,起始值为0。
若初始化后需要申请20字节:
(申请20字节)
第二次申请30字节:
(申请30字节)
Heap2中每个内存块都有对应结构体的记录链表和块大小,链表将空闲块按照块大小升序相连。Start和end结构体作为索引独立于堆区,end作为结束标识xBlockSize总是等于堆区总大小。
Heap2初始化堆区后图:
(heap2初始化后)
如果程序需要申请A、B、C三块内存,需要的空间大小为B>A>C,使用heap2的申请分配图如下:
(申请A空间)
(申请B、C空间)
Heap2回收时将内存块按照升序方式插入链表。假设剩余的D空间大小等于A,程序分别释放B、A、C内存,释放后堆区如下:
(释放B内存后)
(释放A内存后)
(释放C内存后)
内存链上的内存按照块大小递增,记录块信息的结构体随着分配逐渐增加,永远得不到释放。一旦程序频繁申请小块内存,即使释放后也无法申请大内存块。
Heap3使用了库中的malloc和free,此时堆区已经脱离freertos控制,configTOTAL_HEAP_SIZE成为无效参数。没有源码,,,,先不做分析~
Heap4中每个内存块都有对应结构体的记录链表和块大小,链表将空闲块按照地址升序相连。Start结构体作为索引独立于堆区,end属于堆区一部分,所以去除一个结构体的size才是堆实际分配范围。
(heap4初始化后的堆区)
每当申请出空闲块后都会对应产生一个新的结构体,这些结构体会占据一部分堆空间。申请内存块A后堆区图:
(申请A内存后)
再次申请B、C内存块:
(申请B内存后)
(申请C内存后)
heap4在回收内存时会尝试将零散的内存块进行合并,减少碎片。
假设在申请A、B、C之后,B内存使用完毕需要释放掉,B块的上下都没有相邻的空闲内存,所以只插入空闲链表就可以完成释放。如图:
(释放B后)
释放A时,存在相邻的B区,两块内存会进行拼接,B块的结构体也被合并,如图:绿色区是被回收的内存
(释放A后)
回收C块时,上下都存在空闲区,回收工作分为两个步骤进行:
(释放C)
相同点
除heap3外,都使用大数组作为堆区进行分配,这看起来比较浪费,即使程序没有申请任何空间,堆区占据的内存在程序编译前就已经确定。
Heap2与heap4都需要为记录内存块信息而消耗一定堆空间。
调用相同,统一使用pvPortMalloc和vPortFree,方便heap文件更换。
进行堆操作时都没有屏蔽中断,只是将调度器挂起。所以FreeRtos堆操作不能在中断里使用,如果一定要实现的话,线程中每一个堆操作的地方如创建任务、信号量、队列等~都要先屏蔽中断,这会使FreeRtos调度器实时性下降。
区别
四种方式主要在分配方法上存在差异:
Heap1分配最为简单迅速,它适合只申请不释放的工程。
Heap2进行频繁申请释放后会造成较多的碎片,适合只申请固定大小内存的工程。
Heap4像是Heap2的升级版,分配方式几乎相同,在回收时添加了内存块拼接以尝试消除碎片。