一、基础
1、静态大数组用于分配内存
cpp
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];//编译期的静态数组,用于分配
2、块头链表
cpp
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK *pxNextFreeBlock; /*<<下一块空闲内容*/
size_t xBlockSize; /*<<当前空闲块大小,最高位表示是否分配*/
} BlockLink_t;
3、头尾哨兵
cpp
static BlockLink_t xStart; /* 链表头(地址最小,xBlockSize=0) */
static BlockLink_t *pxEnd; /* 链表尾(地址最大,标记"没有更多块了") */
头哨兵:作为查找起始
尾哨兵:作为查找结束
二、prvHeapInit
1、从堆内存起点进行八字节对齐
2、初始化头哨兵,指向对对齐后的起点,大小为0
cpp
xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap; /* 链表第一项 = 对齐后的起点 */
xStart.xBlockSize = ( size_t ) 0; /* 哨兵自身大小为 0 */
3、初始化尾哨兵,从堆内存尾部往前8个字节再向下对齐
4、初始化唯一空闲块也就是第一个空闲块,大小为可用堆-尾哨兵开销,指向尾哨兵
初始化后的ucHeap布局,头哨兵不在这块内存,但指向第一个空闲内存块
ucHeap 数组(3072B)内部:
┌─────────────┐ 0x20000100(数组首地址,可能未对齐)
│ 对齐损失 │ 0~7 字节(不可用,prvHeapInit 里扣掉了)
├─────────────┤ ← pucAlignedHeap(对齐后的堆起点)
│ 块头 BlockLink_t │ 8 字节
│ ├ pxNextFreeBlock → 指向 pxEnd ← 这就是"指向尾哨兵"的字段
│ └ xBlockSize → 整个块的大小(含块头)
├─────────────┤
│ 空闲内容区 │ ← 真正能 malloc 出去的字节,当前整块都是空闲的
│ (约 3056B) │
├─────────────┤ ← pxEnd 地址(= 起点 + 总大小 - 8,向下对齐)
│ pxEnd 哨兵 │ 8 字节(xBlockSize=0, pxNextFreeBlock=NULL)
├─────────────┤
│ 尾部余量 │ 0~7 字节(pxEnd 对齐后剩下的,不可用)
└─────────────┘ 0x20000100 + 3072(数组末尾)
三、void *pvPortMalloc( size_t xWantedSize )
首先暂停任务调度,第一次调用时若没有初始化则进行初始化,将申请大小加上8字节块头,若剩余总空间大于总需要空间,则从头哨兵开始找第一个符合大小的内存块,内存块大小包含8字节块头和申请的内存大小。然后记录块的返回地址,把这块地方从空闲链表里摘除,将后续剩余空间作为一个新的空闲块放入空闲块链表,更新统计值,恢复任务调度。返回地址。
四、void vPortFree( void *pv )
首先往前8个地址找到块头,校验是否可以释放,可以的话就清楚标志位(最高位),然后挂起调度器,把空闲块大小加进总空闲数,调用prvInsertBlockIntoFreeList将该块插回空闲链表,prvInsertBlockIntoFreeList里的操作是首先从头哨兵开始找到该空闲块,判断前后是否也为空闲块,若是则合并,合并就是改大小字段和跳指针,然后链接进空闲块链表。