一、概念
内存分为堆和栈两部分:
栈(Stack)是一种后进先出(LIFO)的数据结构,用于存储函数的调用栈、内存的分配操作、表达式求值的临时变量以及与程序中的控制流相关的数据。每当程序执行函数调用、变量声明或其他类型的操作时,都会在栈中添加一个栈帧(Stack Frame),用于存储函数的执行环境。
栈内存:主要存放函数地址、函数参数、局部变量等,空间较小,远小于堆内存,所以常有栈溢出错误。它由系统自动申请和回收,只由单线程使用,访问速度快,是连续内存,集中在内存块附近。
堆(Heap)则是用于分配程序中动态数据结构的内存空间,它的生命周期不由程序的函数调用栈管理,因此堆空间通常会被程序员直接管理。堆内存通常是一个可以被看做是一棵树的数组对象,它满足堆的性质:堆中某个节点的值总是不大于或不小于其父节点的值。堆空间为程序提供了极为灵活的空间分配和管理手段,既可以手动管理,也可以交由垃圾回收机制自动管理。
堆内存:主要存放new出来的对象和malloc申请的空间,空间大。它由程序分配,使用new或malloc申请,使用free或delete释放。堆内存中的实体数据地址都存储在栈变量中(即引用),以便能够高速访问。
总的来说,堆和栈在程序中都扮演着重要的角色。栈是一种高效的内存结构,用于存放基础数据类型和引用类型的变量,大大简化内存的管理,提高了程序的执行效率。堆空间则为程序提供了极为灵活的空间分配和管理手段。
被管理的内存称为堆,未被管理的内存称为栈:
- 堆, heap,就是一块空闲的内存,需要提供管理函数
- malloc:从堆里划出一块空间给程序使用
- free:用完后,再把它标记为"空闲"的,可以再次使用
- 栈, stack,函数调用时局部变量保存在栈中,当前程序的环境也是保存在栈中
- 可以从堆中分配一块空间用作栈
二、自定义malloc函数
代码:
char heap_buf[1024]; //自定义1024字节内存的数组,模拟堆
int pos = 0; //指向堆数组可用空间的首地址
void *my_malloc(int size) //自定义malloc函数
{
int old_pos = pos; //记录开辟空间的首地址
pos += size; //malloc的空间大小
return &heap_buf[old_pos]; //返回开辟空间的首地址
}
void my_free(void *buf) //可用自定义malloc函数,但是无法自定义free函数,后面分析原因
{
/* err */
}
int main(void)
{
char ch = 65; // char ch = 'A';
int i;
char *buf = my_malloc(100); //使用自定义的malloc函数在自定义堆数组中开辟100字节空间
for (i = 0; i < 26; i++)
buf[i] = 'A' + i; //在新开辟的空间中依次填入ABC……XYZ
return 0;
}
三、Debug运行
1、查看heap_buf的首地址
2、查看malloc的buf首地址与heap_buf的首地址相同
3、多运行几次,ABCDE字母填入buf空间里,在heap_buf中也可以看到ABCDE
四、heap_4简单分析
4.1 heap管理链表结构体
对堆的管理意味着需要有一个链表结构体对空闲的堆空间和已使用的堆空间进行管理。
参考Heap_4的堆管理链表结构体:
typedef unsigned long size_t;
/* Define the linked list structure. This is used to link free blocks in order
* of their memory address. */
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK * pxNextFreeBlock; /*<< The next free block in the list. */
size_t xBlockSize; /*<< The size of the free block. */
} BlockLink_t;
链表结构体包含2部分:
- 下一个空闲块
- 当前块的size
4.2 堆初始化
当第一次使用malloc时,会对Heap进行初始化:
static void prvHeapInit( void ) /* PRIVILEGED_FUNCTION */
{
BlockLink_t * pxFirstFreeBlock;
uint8_t * pucAlignedHeap;
portPOINTER_SIZE_TYPE uxAddress;
size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;
/* 确保堆从正确对齐的边界开始。 */
uxAddress = ( portPOINTER_SIZE_TYPE ) ucHeap;
if( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 )
{
uxAddress += ( portBYTE_ALIGNMENT - 1 );
uxAddress &= ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK );
xTotalHeapSize -= uxAddress - ( portPOINTER_SIZE_TYPE ) ucHeap;
}
pucAlignedHeap = ( uint8_t * ) uxAddress;
/* xStart用于保存指向空闲块列表中第一项的指针。void强制转换用于防止编译器警告。 */
xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
xStart.xBlockSize = ( size_t ) 0;
/* pxEnd用于标记空闲块列表的末尾,并插入到堆空间的末尾。 */
uxAddress = ( ( portPOINTER_SIZE_TYPE ) pucAlignedHeap ) + xTotalHeapSize;
uxAddress -= xHeapStructSize;
uxAddress &= ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK );
pxEnd = ( BlockLink_t * ) uxAddress;
pxEnd->xBlockSize = 0;
pxEnd->pxNextFreeBlock = NULL;
/* 首先,有一个空闲块,它的大小是占用整个堆空间,减去pxEnd占用的空间。 */
pxFirstFreeBlock = ( BlockLink_t * ) pucAlignedHeap;
pxFirstFreeBlock->xBlockSize = ( size_t ) ( uxAddress - ( portPOINTER_SIZE_TYPE ) pxFirstFreeBlock );
pxFirstFreeBlock->pxNextFreeBlock = pxEnd;
/* 只有一个块存在——它覆盖了整个可用的堆空间。 */
xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
}
通过代码可以看到,对Heap初始化时会创建一个xStart和*pxEnd :
//heap_4中定义:
/* 创建两个列表链接来标记列表的开始和结束。 */
PRIVILEGED_DATA static BlockLink_t xStart;
PRIVILEGED_DATA static BlockLink_t * pxEnd = NULL;
//heap_2中定义:
/* 创建两个列表链接来标记列表的开始和结束。 */
PRIVILEGED_DATA static BlockLink_t xStart, xEnd;
heap_2中的xStart, xEnd,用2个结构体代表头尾,不占用堆空间;
heap_4中是xStart, *pxEnd = NULL,pxEnd与heap_2中xEnd不一样,pxEnd占用了堆空间。这样做能够适配heap_5,heap_5支持多块不连续的内存合并,使用pxEnd链表,可以直接将pxEnd指向下一个非连续堆。
4.3 malloc使用
假设malloc开辟了3块空间,第一块空间从heap头开始100字节,第二块空间接着第一块空间100字节,第三块空间接着第二块50字节,最后释放了第二块,则示意图如下:
对照代码分析:
void * pvPortMalloc( size_t xWantedSize )
{
BlockLink_t * pxBlock;
BlockLink_t * pxPreviousBlock;
BlockLink_t * pxNewBlockLink;
void * pvReturn = NULL;
size_t xAdditionalRequiredSize;
vTaskSuspendAll();
{
/* 如果这是第一次调用malloc,那么堆将需要初始化来设置空闲块列表。 */
if( pxEnd == NULL )
{
prvHeapInit();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if( xWantedSize > 0 )
{
/* 所需的大小必须增加,这样除了请求的字节数之外,它还可以包含BlockLink_t结构。为了对齐,可能还需要一些额外的增量。 */
xAdditionalRequiredSize = xHeapStructSize + portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK );
if( heapADD_WILL_OVERFLOW( xWantedSize, xAdditionalRequiredSize ) == 0 )
{
xWantedSize += xAdditionalRequiredSize;
}
else
{
xWantedSize = 0;
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 检查我们尝试分配的块大小是否太大,以至于设置了顶部位。BlockLink_t结构的块大小成员的顶部位用于确定谁拥有该块-应用程序或内核,因此它必须是空闲的。 */
if( heapBLOCK_SIZE_IS_VALID( xWantedSize ) != 0 )
{
if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
{
/* 从起始(最低地址)块开始遍历列表,直到找到一个足够大的块。 */
pxPreviousBlock = &xStart;
pxBlock = xStart.pxNextFreeBlock;
while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
{
pxPreviousBlock = pxBlock;
pxBlock = pxBlock->pxNextFreeBlock;
}
/* 如果到达终点标记,则没有找到足够大小的块。 */
if( pxBlock != pxEnd )
{
/* 返回指向的内存空间-在开始时跳过BlockLink_t结构。 */
pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );
/* 该块正在返回使用,因此必须从空闲块列表中删除。 */
pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
/* 如果块大于要求,则可以将其分成两个。 */
if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
{
/* 这个块将被分成两部分。根据请求的字节数创建一个新的块。void强制转换用于防止编译器发出字节对齐警告。 */
pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
configASSERT( ( ( ( size_t ) pxNewBlockLink ) & portBYTE_ALIGNMENT_MASK ) == 0 );
/* 计算从单个块分割出的两个块的大小。 */
pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
pxBlock->xBlockSize = xWantedSize;
/* 将新块插入空闲块列表中。 */
prvInsertBlockIntoFreeList( pxNewBlockLink );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
xFreeBytesRemaining -= pxBlock->xBlockSize;
if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
{
xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 块正在被返回——它被应用程序分配和拥有,没有“下一个”块。 */
heapALLOCATE_BLOCK( pxBlock );
pxBlock->pxNextFreeBlock = NULL;
xNumberOfSuccessfulAllocations++;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
traceMALLOC( pvReturn, xWantedSize );
}
( void ) xTaskResumeAll();
#if ( configUSE_MALLOC_FAILED_HOOK == 1 )
{
if( pvReturn == NULL )
{
vApplicationMallocFailedHook();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* if ( configUSE_MALLOC_FAILED_HOOK == 1 ) */
configASSERT( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 );
return pvReturn;
}
4.4 free使用
假设使用free释放内存时,输入的参数pv是Heap中实际使用地址的首地址,但是malloc的空间除了实际使用的部分外还有一个头部的链表结构体,因此需要将pv地址向前移动xHeapStructSize,把malloc开辟空间的头部链表结构体与实际使用部分一同释放并插入到空闲Heap中:
void vPortFree( void * pv )
{
uint8_t * puc = ( uint8_t * ) pv;
BlockLink_t * pxLink;
if( pv != NULL )
{
/* 被释放的内存在其前面有一个BlockLink_t结构。 */
puc -= xHeapStructSize;
/* 这种类型转换是为了防止编译器发出警告。 */
pxLink = ( void * ) puc;
configASSERT( heapBLOCK_IS_ALLOCATED( pxLink ) != 0 );
configASSERT( pxLink->pxNextFreeBlock == NULL );
if( heapBLOCK_IS_ALLOCATED( pxLink ) != 0 )
{
if( pxLink->pxNextFreeBlock == NULL )
{
/* 该块被返回到堆中——它不再被分配。 */
heapFREE_BLOCK( pxLink );
#if ( configHEAP_CLEAR_MEMORY_ON_FREE == 1 )
{
( void ) memset( puc + xHeapStructSize, 0, pxLink->xBlockSize - xHeapStructSize );
}
#endif
vTaskSuspendAll();
{
/* 将此块添加到空闲块列表中。 */
xFreeBytesRemaining += pxLink->xBlockSize;
traceFREE( pv, pxLink->xBlockSize );
prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
xNumberOfSuccessfulFrees++;
}
( void ) xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
其中heap4中把相邻的空闲内存合并为一个更大的空闲内存是在prvInsertBlockIntoFreeList函数中实现的:
static void prvInsertBlockIntoFreeList( BlockLink_t * pxBlockToInsert ) /* PRIVILEGED_FUNCTION */
{
BlockLink_t * pxIterator;
uint8_t * puc;
/* 遍历列表,直到发现一个空闲块的下一个空闲块指针地址比插入的块的地址高,即得到比插入块地址小的相邻空闲块。
问题:假设释放heap中地址最高的一段时,所有的空闲块地址都比要插入的块的地址低,此时该如何执行?*/
for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
{
/* 这里什么都不用做,只需要迭代到正确的位置。 */
}
/* 插入的块和比其地址低的相邻块是否构成连续的内存块? 体现相邻空闲空间插入思想*/
puc = ( uint8_t * ) pxIterator;
if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
{
pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
pxBlockToInsert = pxIterator;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 插入的块和比其地址高的相邻块是否构成一个连续的内存块? 体现相邻空闲空间插入思想*/
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;
}
}
else
{
pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
}
/* 如果被插入的块和比其地址低的相邻块不连续,而和比其地址高的相邻块连续,因此和比其地址低的相邻块的pxNextFreeBlock指针应该由原本指向和比其地址高的相邻块变为指向被插入的块 */
if( pxIterator != pxBlockToInsert )
{
pxIterator->pxNextFreeBlock = pxBlockToInsert;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}