一、FreeRTOS的5种heap处理方法
- heap_1.c,最简单的处理,在静态区中定义一个超大数组作为heap,每次系统申请heap空间时,通过大heap数组的指针去控制申请或释放
a. 申请内存:pvPortMalloc (申请长度)
i. 当系统配置为不是byte对齐的时候,需要检查申请长度对齐和堆地址对齐
ii. 挂起所有任务
iii. 从静态全局变量数组中,当前heap首地址,作为申请空间的地址
iv. 将heap首地址向后移动申请长度
v. 恢复所有任务
vi. 返回申请空间的首地址
b. 释放内存:pvPortFree (内存指针)
i. 将指针置空
c. 获取剩余内存:xPortGetFreeHeapSize,获取当前heap大数组剩余长度
d. 缺点:这种方法会产生很多的内存碎片和内存泄漏,因为内存释放释放了个寂寞,只是把指针释放掉了,大heap数组中被申请的指针无法重复使用;
e. 优点:简单可控,一些对内存控制严格的、不使用动态内存的方案会使用
f. 申请heap时挂起所有任务的目的,因为heap是动态申请的,如果其他任务也有申请,则会出现内存申请冲突。 - heap_2.c,增加了内存块的链表结构体,将所有的内存以链表的形式组织起来,内存块首尾相连。使用的是最佳匹配原则,适用于申请和释放的内存空间大小一致,也能避免内存泄露
a. 链表管理,定义一个链表头和链表尾,申请一个空闲链项,表头的next指向空闲,空闲的next指向表尾,形成链式
b. 内存申请:pvPortMalloc (申请长度)
i. 挂起所有任务
ii. heap如需初始化则初始化
iii. 计算链表附加长度,主要是链表结构体长度,最后总长度为 附加长度加上申请长度
iv. 创建临时项,项指针挂到表头的next上,项的next指向freeblock空闲项
v. 恢复所有任务
vi. 返回申请空间地址
c. 释放内存:pvPortFree (内存指针),支持释放内存,减少内存泄露 - heap_3.c,调用C库的malloc和free申请和释放内存。这种方式的执行存在不确定性,代码量比较大,也存在线程安全的问题,不太适合RTOS。所说的线程安全主要是对内存的操作没有做到线程互斥,可能会出现内存访问冲突,在heap1heap2heap4中申请内存前都会将调度器挂起,确保只有一个线程操作内存。
- heap_4.c,是heap2的增强版,同样是通过链表管理内存块,匹配算法编程了首次匹配原则,同时支持内存的合并,减少内存的碎片化。
a. 链表管理,定义一个链表头和链表尾,申请一个空闲链项,表头的next指向空闲,空闲的next指向表尾,形成链式
b. 内存申请
c. 内存释放 - heap_5.c,在heap4的基础上支持了内存分区。
二、heap4代码分析
typedef struct {
BlockLink_t *pxNextFreeBlock;
size_t xBlockSize;
}BlockLink_t;
- 全局变量,
a. 用于单向链表控制链表头BlockLink_t xStart,链表尾BlockLink_t *xEnd = NULL,注意链表头是链表项实体,链表尾是链表项指针,说明表尾只做一个结束标记,申请时判断新表项指针是否等于表尾。
b. 缓冲池剩余Byte数:size_t xFreeBytesRemaining = 0;
c. xMinimumEverFreeByteRemaining = 0;
d. 已分配字节数:xNumberOfSuccessfulAllocations = 0;
e. 未分配字节数:xNumberOfSuccessfulFrees = 0; - heap初始化
heap的原理是申请一片大的内存池,将内存池分割成free内存块,每个free内存块组织为连续地址的【block结构体 + 内存空间】,
将所有freeblock通过链表的方式连接起来,当要申请空间时取一个满足大小的freeblock,将内存空间返回应用,将剩余的内存
重新创建一个小的新freeblock,插入到链表中去。释放也是一样,将拿到的空间转换成freeblock格式并做一些数据清0,插入到
freeblock链表中去。那么初始化heap,需要设置几个全局变量的值,剩余字节数、已分配字节数、未分配字节数以及
【表头 + freeblock + 表尾】,这时候的freeblock占用内存池出去表头表尾的全部空间。
i. 表头的nextfreeblock指向内存池,此时freeblock只有一个,整个内存池,每次申请是删一个插一个,按时每次释放则是插入新的,
所以随着后面释放空间而插入的freeblock逐渐增多。
ii. 表头的大小为0,表示表头没有使用空间
iii. 设置剩余长度 - 内存申请
a. 计算附加长度:附加长度 = Block结构体长度 + 请求长度后的非对齐剩余长度。比如请求9个字节,4Byte对齐的,那么附加长度 = sizeof(Block) + ( 4 – (9& 0x11))
b. 计算附加长度加上申请长度后是否会导致内存池溢出,不会则 申请长度+= 附加长度;
c. 申请长度的最高bit被系统占用,用于判断当前内存属于应用还是内核,实际是判断长度是否超出范围,如果是0则没超,1就是超了。Free RTOS牺牲了最大长度的一个bit,比如最大能申请4G的,变成最大2G,也换来了更大好处,后续分析。
d. 判断申请长度大于0且小于剩余长度,是则继续如下申请
e. first fit原则,从表头开始循环查找每一个free内存block,找到大于申请长度的第一个block,就是目标block,一起获取到的还有上一个block项。
f. 上一个block项的成员nextfreeblock的地址加上项头长度就能定位到实际能使用的内存空间:vreturn = 上一个表的pxNextFreeBlock + 当前表头结构体长度sizeof(Block)
g. 整个过程是,从freeblock链表中找到第一个长度满足的freeblock,将其从freeblock链表中删掉,从这个block中分配一个申请的长度的block,然后用剩余的空间创建一个新的freeblock,然后插入到freeblock链表中。 - 释放内存
a. 释放内存与申请内存的操作相反,拿到一片内存的地址后,往前移动block头结构体的长度,可以定位到该内存空间的block管理结构体
b. 将内存空间清0
c. 全局freeremain增加整个block的长度
d. 将首地址转换成一个freeblock块
e. 插入到freeblock链表中即完成内存的free。