【内存池】动态内存分配机制
【内存池】动态内存分配机制
- 设计背景与目的
- 数据结构
- 1. 堆区结构体与函数指针
- 2. 创建实体对象,并确定具体函数
- 3. 内存对齐变量
- 4. 空闲内存块设计
- 5. 其他核心变量
- 函数实现
- 1. 内存初始化 my_heap_init
- 2. 内存分配 my_heap_malloc
- 3. 空闲块链表插入空闲块
- 4. 内存释放 my_heap_free
- 优势
- 问题与解答
设计背景与目的
设计一套基于内存池的自定义堆内存管理机制,主要用于嵌入式系统或实时系统场景,标准库的malloc/free存在明显缺陷:
- 依赖操作系统动态内存管理,分配 / 释放时间不确定(不符合实时系统 “确定性” 要求);
- 容易产生内存碎片(小空闲块无法合并,导致大内存请求失败);
- 可能因误操作导致堆溢出(无边界检查),且内存使用状态不可控。
数据结构
1. 堆区结构体与函数指针
typedef struct _MY_HEAP
{void * (*malloc)(uint32_t xWantedSize); // 分配堆区内存void (*free)(void *pv); // 释放堆区内存 uint32_t(*get_free_heapsize)(void); // 获取当前可用堆大小uint32_t(*get_min_ever_free_heapsize)(void); // 获取历史最小可用堆大小
}c_my_heap;
2. 创建实体对象,并确定具体函数
const c_my_heap my_heap = {.malloc = my_heap_malloc,.free = my_heap_free,.get_free_heapsize = my_heap_get_free_heapsize, .get_min_ever_free_heapsize = my_heap_get_min_ever_free_heapsize
};
// 获取单例实例的函数
const c_my_heap* get_my_heap_instance(void) {return &my_heap;
}
3. 内存对齐变量
// 对齐字节数
#define portBYTE_ALIGNMENT 8
// 对齐字节掩码 用于内存对齐
#define portBYTE_ALIGNMENT_MASK (portBYTE_ALIGNMENT - 1)
// 对一个字节或者内存进行对齐 ******核心操作******
uint32_t address = (address + portBYTE_ALIGNMENT_MASK) & ~portBYTE_ALIGNMENT_MASK
4. 空闲内存块设计
// 定义块链接结构体
typedef struct A_BLOCK_LINK {struct A_BLOCK_LINK *pxNextFreeBlock; // 指向下一个空闲块 size_t xBlockSize; // 空闲块的大小
} BlockLink_t;// 空闲块也必须做内存对齐
static const uint32_t xHeapStructSize = (sizeof(BlockLink_t) + portBYTE_ALIGNMENT_MASK) & (~portBYTE_ALIGNMENT_MASK);// 定义一个最小块,因为后期获取的空闲内存块可能可以分成两块,即可以分割的最小块大小
#define heapMINIMUM_BLOCK_SIZE (size_t)((xHeapStructSize << 1)) // 最小块大小
5. 其他核心变量
// 内存池区域 从数组的起点地址开始维护一段 4096 字节的内存
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
// 空闲内存块头尾指针,都是哨兵节点
static BlockLink_t xStart, *pxEnd = NULL;// 获取 uint32_t 类型的最高位。
// 当该位在 BlockLink_t 的 xBlockSize 成员中被设置时,块属于应用程序。
// 当该位未设置时,该块仍然是自由堆空间的一部分。
static uint32_t xBlockAllocatedBit = 0; // 初始化函数中初始化为0x80000000 (1 << 31)// 记录剩余可用字节数,但不能显示内存碎片
static uint32_t xFreeBytesRemaining = 0U;
static uint32_t xMinimumEverFreeBytesRemaining = 0U;
函数实现
1. 内存初始化 my_heap_init
- 起始内存对齐 -> 确定头尾哨兵节点(尾指针内存对齐) -> 初始化唯一空闲内存块 -> 统计内存剩余信息 -> 初始化内存分配标记位
static void my_heap_init( void ){BlockLink_t *pxFirstFreeBlock; // 指向第一个空闲内存块的指针uint8_t *pucAlignedHeap; // 对齐后的堆起始地址uint32_t uxAddress;uint32_t xTotalHeapSize = configTOTAL_HEAP_SIZE; // 调整后的实际可用堆大小// 1. 地址对齐开始处理uxAddress = (uint32_t)ucHeap;if ((uxAddress & portBYTE_ALIGNMENT_MASK) != 0){// (1) 先加上对齐边界减1的值, 确保跨越下一个对齐边界uxAddress += (portBYTE_ALIGNMENT - 1);// (2) 通过与操作清除低位,实现向下对齐uxAddress &= ~( (uint32_t) portBYTE_ALIGNMENT_MASK );// (3) 调整总堆大小xTotalHeapSize -= uxAddress - (uint32_t)ucHeap;}pucAlignedHeap = (uint8_t *)uxAddress;// 2. 初始化链表头尾节点:// xStart 用于持有空闲块链表的第一个元素的指针xStart.pxNextFreeBlock = (void *)pucAlignedHeap;xStart.xBlockSize = (uint32_t)0;// pxEnd 用于标记空闲块链表的结束,并插入在堆空间的末尾。uxAddress = ((uint32_t)pucAlignedHeap) + xTotalHeapSize;uxAddress -= sizeof(BlockLink_t); // 留出一块做内存对齐,并防止越界访问uxAddress &= ~( (uint32_t) portBYTE_ALIGNMENT_MASK );pxEnd = (void *)uxAddress; // 设置 pxEnd 为堆的末尾pxEnd->xBlockSize = 0; // 结束块的大小设置为0pxEnd->pxNextFreeBlock = NULL; // 下一个指针设为NULL// 3. 初始化唯一空闲块 开始时只有一个空闲块,该块的大小为整个堆空间pxFirstFreeBlock = (void *)pucAlignedHeap;pxFirstFreeBlock->xBlockSize = uxAddress - (uint32_t)pxFirstFreeBlock; // 计算空闲块大小pxFirstFreeBlock->pxNextFreeBlock = pxEnd; // 链接到结束块// 4. 初始化统计信息:仅有一个块,覆盖整个可用堆空间。xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize; // 初始化最小可用xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize; // 初始化可用字节// 5. 分配标志位初始化 uint32_t 变量中顶位的位置。xBlockAllocatedBit = ((uint32_t) 1) << ((sizeof(uint32_t) * heapBITS_PER_BYTE) - 1 );}
2. 内存分配 my_heap_malloc
初始化检查 -> 参数检查 -> 调整大小(+xHeapStructSize)并对齐 -> 空闲块检查, 遍历空闲链表寻找合适块 -> 标记返回指向的内存空间,跳过 BlockLink_t 的结构体 -> 块分割处理 -> 创建新块(更新两个块的大小) -> 把新块插入到空闲块链表
void *my_heap_malloc( uint32_t xWantedSize ){BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;void *pvReturn = NULL;// 1. 初始化检查if( pxEnd == NULL ) my_heap_init();// 2. 参数校验// 检查请求的块大小是否过大,最高位应设置为 0if( ( xWantedSize & xBlockAllocatedBit ) == 0 ) {if( xWantedSize > 0 ){// 3. 大小调整xWantedSize += xHeapStructSize; // (1) 增加块头大小// (2) 进行内存对齐处理if((xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0)xWantedSize = (xWantedSize + portBYTE_ALIGNMENT_MASK) & (~portBYTE_ALIGNMENT_MASK);}// 4. 空闲内存检查: 确认有足够剩余内存if((xWantedSize > 0 ) && (xWantedSize <= xFreeBytesRemaining)){// 5. 空闲块查找:遍历空闲链表寻找合适块 // 从低地址块开始遍历链表,直到找到合适大小的块。pxPreviousBlock = &xStart;pxBlock = xStart.pxNextFreeBlock;while((pxBlock->xBlockSize < xWantedSize ) && (pxBlock->pxNextFreeBlock != NULL)){pxPreviousBlock = pxBlock;pxBlock = pxBlock->pxNextFreeBlock;}// 如果到达了结束标志,则未找到合适大小的块。if( pxBlock != pxEnd ){// 6. 标记返回指向的内存空间,跳过 BlockLink_t 的结构体pvReturn = (void *)((( uint8_t *) pxPreviousBlock->pxNextFreeBlock) + xHeapStructSize);// 此块正在被返回使用,因此必须将其从空闲块列表中移除。pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;// 7. 块分割处理:如果找到的块过大,进行分割if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE ){// 该块要拆分为两个块。创建在请求的字节之后的新块。pxNewBlockLink = ( void * ) (((uint8_t *)pxBlock) + xWantedSize );// 更新后面一块与前面分配内存的一个块的大小pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;pxBlock->xBlockSize = xWantedSize;// 将新块插入空闲块列表中。 my_heap_insert_block_into_freelist( pxNewBlockLink );}// 更新剩余字节数xFreeBytesRemaining -= pxBlock->xBlockSize;if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining ){xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;}// 此块正在被返回 - 它已分配并归应用程序所有,并且没有“下一个”块。 */pxBlock->xBlockSize |= xBlockAllocatedBit;pxBlock->pxNextFreeBlock = NULL; // 设置下一个块指针为 NULL}}}return pvReturn;}
3. 空闲块链表插入空闲块
- 找合适的位置,从头开始,找到第一个pxIterator->next > Insert的地址,Interator是Insert前面一个块的地址,这里数字当然是为了便于理解,实际的是内存对齐的
-
找到合适的空闲块插入的前一个块起始地址Interator
// 遍历链表,找到适合插入的新块位置 BlockLink_t *pxIterator = &xStart; while(pxIterator->pxNextFreeBlock < pxBlockToInsert){pxIterator = pxIterator->pxNextFreeBlock } -
前序合并
// 可能与前一个块合并 puc = (uint8_t *)pxIterator; if ((puc + pxIterator->xBlockSize) == (uint8_t *)pxBlockToInsert) {pxIterator->xBlockSize += pxBlockToInsert->xBlockSize; // 合并块pxBlockToInsert = pxIterator; // 更新插入块 } // 这里可能先与前面的块合并后继续与后续的块合并,走下面的逻辑 -
后续合并
// 可能与后一个块合并 puc = (uint8_t *)pxBlockToInsert; if ((puc + pxBlockToInsert->xBlockSize) == (uint8_t *)pxIterator->pxNextFreeBlock) {if (pxIterator->pxNextFreeBlock != pxEnd) {/* 合并两个块 */pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock; // 更新链表} else {pxBlockToInsert->pxNextFreeBlock = pxEnd; // 更新结束块} } // 插入新块 insert->next else {pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock; } -
完整插入空闲块代码
static void my_heap_insert_block_into_freelist(BlockLink_t *pxBlockToInsert){BlockLink_t *pxIterator;uint8_t *puc;// 遍历链表,找到适合插入的新块位置pxIterator = &xStart;while(pxIterator->pxNextFreeBlock < pxBlockToInsert){pxIterator = pxIterator->pxNextFreeBlock;}// 可能与前一个块合并puc = (uint8_t *)pxIterator;if ((puc + pxIterator->xBlockSize) == (uint8_t *)pxBlockToInsert) {pxIterator->xBlockSize += pxBlockToInsert->xBlockSize; // 合并块pxBlockToInsert = pxIterator; // 更新插入块}/* 可能与后一个块合并 */puc = (uint8_t *)pxBlockToInsert;if ((puc + pxBlockToInsert->xBlockSize) == (uint8_t *)pxIterator->pxNextFreeBlock) {/* 合并两个块 */if (pxIterator->pxNextFreeBlock != pxEnd) {// 更新块大小pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;// 更新链表pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock; } else {pxBlockToInsert->pxNextFreeBlock = pxEnd; // 更新结束块}} // 插入新块 insert->nextelse {pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;}// 如果未发生合并,插入块 前面连接 insertif (pxIterator != pxBlockToInsert) {pxIterator->pxNextFreeBlock = pxBlockToInsert;}}
4. 内存释放 my_heap_free
void my_heap_free( void *pv ){uint8_t *puc = ( uint8_t * ) pv;BlockLink_t *pxLink;if( pv != NULL ){/* 被释放的内存会在它前面有一个 BlockLink_t 结构。 */puc -= xHeapStructSize;if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 ){/* 该块正在返回给堆 - 它不再属于已分配块。 */pxLink->xBlockSize &= ~xBlockAllocatedBit;/* 将该块添加到空闲块列表。 */xFreeBytesRemaining += pxLink->xBlockSize;my_heap_insert_block_into_freelist( ( ( BlockLink_t * ) pxLink ) );}}}
优势
结合代码细节,该实现的核心优势如下:
- 内存使用可控
- 堆大小由
configTOTAL_HEAP_SIZE固定(基于静态数组ucHeap),避免堆无限制增长导致的内存溢出; - 通过
xFreeBytesRemaining和xMinimumEverFreeBytesRemaining跟踪内存使用,便于调试和资源监控。
- 堆大小由
- 分配效率与确定性
- 基于链表管理空闲块,遍历和插入操作逻辑简单,避免标准库复杂的系统调用开销;
- 分配 / 释放时间可预测(无复杂算法),符合实时系统对 “操作耗时固定” 的要求。
- 减少内存碎片
- 释放内存时通过
my_heap_insert_block_into_freelist实现双向合并(与前 / 后空闲块合并),减少外部碎片; - 分配时若空闲块过大,会拆分出多余部分重新加入空闲链表(
pxNewBlockLink逻辑),提高内存利用率。
- 释放内存时通过
- 适配硬件要求
- 强制内存对齐(
portBYTE_ALIGNMENT处理),确保分配的内存地址符合硬件访问要求(如某些 CPU 需 4 字节 / 8 字节对齐); - 无动态内存依赖(基于静态数组),适合无操作系统或资源受限的嵌入式芯片(如 MCU)。
- 强制内存对齐(
- 可维护性与安全性
- 封装为单例模式(
c_my_heap结构体),避免多实例冲突,便于全局管理; - 通过
xBlockAllocatedBit标记块是否分配,防止重复释放或非法访问。
- 封装为单例模式(
问题与解答
1. 为什么不使用标准库的malloc/free,而要自定义堆管理?
- 标准库
malloc/free分配时间不确定(依赖系统内存管理策略),不适合实时系统; - 容易产生内存碎片,且无法控制堆大小(可能溢出到其他内存区域);
- 嵌入式系统资源有限(如小容量 RAM),自定义堆可基于静态数组固定大小,避免内存滥用。
2. 内存对齐是如何实现的?为什么需要内存对齐?
- 通过
(xWantedSize + portBYTE_ALIGNMENT_MASK) & (~portBYTE_ALIGNMENT_MASK)调整大小,通过uxAddress &= ~portBYTE_ALIGNMENT_MASK调整地址,确保满足portBYTE_ALIGNMENT对齐要求; - 原因:硬件限制(如 CPU 访问非对齐地址可能报错或性能下降),同时保证
BlockLink_t结构体成员(如指针)的正确访问。
3. 如何处理内存碎片?代码中具体做了哪些操作?
- 主要通过空闲块合并解决外部碎片:
- 释放内存时,
my_heap_insert_block_into_freelist先检查前序块(pxIterator)是否相邻,若相邻则合并; - 再检查后序块(
pxIterator->pxNextFreeBlock)是否相邻,若相邻则合并;
- 释放内存时,
- 分配时若空闲块过大(剩余空间 > 最小块大小
heapMINIMUM_BLOCK_SIZE),会拆分出多余部分重新加入空闲链表,避免大空闲块被浪费。
4. 代码中用链表管理空闲块,为什么选择链表而不是其他数据结构(如树)
- 嵌入式系统对内存和计算资源敏感,链表实现简单(仅需指针和大小字段),内存开销小;
- 代码中空闲块按地址有序排列(遍历插入时保证
pxIterator->pxNextFreeBlock < pxBlockToInsert),线性遍历即可满足需求,无需更复杂的树结构(如二叉树); - 实时系统更关注 “确定性” 而非极致性能,链表的固定操作耗时更符合要求。
5. 堆初始化(my_heap_init)主要做了哪些工作?
- 地址对齐:调整
ucHeap的起始地址,确保满足对齐要求; - 初始化链表:创建哨兵节点
xStart和pxEnd,将整个ucHeap初始化为一个大空闲块,加入链表; - 初始化统计信息:设置
xFreeBytesRemaining(初始为总大小)和xMinimumEverFreeBytesRemaining(跟踪最小剩余内存); - 初始化分配标志位:计算
xBlockAllocatedBit(uint32_t的最高位)。
6.xBlockAllocatedBit的作用是什么?如何避免重复释放?
- 作用:
xBlockAllocatedBit是xBlockSize的最高位,用于标记块是否被分配(置位表示已分配,清零表示空闲); - 避免重复释放:
my_heap_free中先检查(pxLink->xBlockSize & xBlockAllocatedBit) != 0,仅当块处于 “已分配” 状态时才执行释放,防止重复释放导致的链表错乱。
7. 代码是否支持多线程?若不支持,如何改进?
- 不支持。当前代码无互斥机制,多线程同时调用
my_heap_malloc或my_heap_free可能导致链表指针错乱(如同时修改pxNextFreeBlock); - 改进:添加互斥锁(如嵌入式中的
vTaskSuspendAll/xTaskResumeAll,或 POSIX 的pthread_mutex),确保内存操作的原子性。
