|
[导读] 前文描述了栈的基本概念,本文来聊聊堆是怎么会事儿。RT-Thread 在社区广受欢迎,阅读了其内核代码,实现了堆的管理,代码设计很清晰,可读性很好。故一方面了解RT-Thread内核实现,一方面可以弄清楚其堆的内部实现。将学习体会记录分享,希望对于堆的理解及实现有一个更深入的认知。 注,文中代码分析基于rt-thread-v4.0.2 版本。 什么是堆?
以C语言为例,将上面的描述,翻译成一个图:
要动态管理一片内存,且需要动态分配释放,这样一个需求。很显然C语言需要将动态内存区抽象描述起来并实现动态管理。事实上,C语言中堆管理器其本质是利用数据结构将堆区抽象描述,所需要描述的方面:
再利用相应算法对于这类数据结构对象进行动态管理而实现的堆管理器。 **经常看到各种算法书很多只讲算法原理,而不讲应用实例,往往体会不深。私以为可以做些改善。学而不能致用,何必费力去学。所以不是晦涩难懂的算法无用,而是没有去真正结合应用。可以再进一步想,如果算法没有应用场景,也一定会在技术发展的历程中逐渐被世人遗忘。所以建议学习阅读算法书籍时,找些实例来看看,一定会加深对算法的理解领悟。**这是比较重要的题外话,送给大家以共勉。 所以从本质上讲,堆管理器就是数据结构+算法实现的动态内存管理器,管理内存的动态分配以及释放。 为什么要堆? C编程语言对内存管理方式有静态,自动或动态三种方式。静态内存分配的变量通常与程序的可执行代码一起分配在主存储器中,并在程序的整个生命周期内有效。自动分配内存的变量在栈上分配,并随着函数的调用和返回而申请或释放。对于静态分配内存和自动分配内存的生命周期,分配的大小必须是编译时常量(可变长度自动数组[5]除外)。如果所需的内存大小直到运行时才知道(例如,如果要从用户或磁盘文件中读取任意大小的数据),则使用固定大小的数据对象则满足不了要求了。试想,即便假定都知道要多大内存,如在windows/Linux下有那么多应用程序,每个应用程序加载时都将运行中所需的内存采样静态分配策略,则如多个程序运行内存将很快耗尽。 分配的内存的生命周期也可能引起关注。静态或自动分配都不能满足所有情况。自动分配内存不能在多个函数调用之间保留,而静态数据在程序的整个生命周期中必然保留,无论是否真正需要(所以都采用这样的策略必然造成浪费)。在许多情况下,程序员在管理分配的内存的生命周期具有更多的灵活性。 通过使用动态内存分配则避免了这些限制/缺点,在动态内存分配中,更明确(但更灵活)地管理内存,通常是通过从免费存储区(非正式地称为“堆”)中分配内存(为此目的而构造的内存区域)进行分配的。在C语言中,库函数malloc用于在堆上分配一个内存块。程序通过malloc返回的指针访问该内存块。当不再需要内存时,会将指针传递给free,从而释放内存,以便可以将其用于其他目的。 谁实现堆如果一问道这个问题,马上会说C编译器。不错C编译器实现了堆管理器,而事实上并非编译器在编译的过程中实现动态内存管理器,而是C编译器所实现的C库实现了堆管理器,比如ANSI C,VC, IAR C编译器,GNU C等其实都需要一些C库的支持,那么这些库的内部就隐藏了这么一个堆管理器。眼见为实吧,还是以IAR ARM 8.40.1 为例,其堆管理器就实现在: .\IAR Systems\Embedded Workbench 8.3\arm\src\lib\dlib\heap
一看有这么多的源码,那么对于应用开发而言,有哪些选项需要进行配置呢?
支持四个选项:
但是如果认为仅仅标准C库负责实现堆管理器,则这种理解并不全面。回到事物的本质,堆管理器是利用数据结构及算法动态管理一片内存的分配与释放。那么有这样需求的地方,都可能需要实现一个堆管理器。 堆管理器的实现很大程度取决于操作系统以及硬件体系架构。大体上需要实现堆内存管理器的有两大类:
对于RT-Thread的内核而言,也实现了一个内核堆管理器,这里就来梳理一下RT-Thread内核版本的小堆管理器的实现,同时来了解一下链表数据结构及算法操作的实例应用。 其堆管理器实现位于.\rt-thread-v4.0.2\rt-thread\src下mem.c,memheap.c以及mempool.c。 关键数据结构其堆管理器主要的数据结构为heap_mem。
堆管理器的初始化入口在mem.c,函数为: void rt_system_heap_init(void *begin_addr, void *end_addr){ struct heap_mem *mem; /*按4字节对齐转换地址*/ /*如0x2000 0001~0x2000 0003,转后为0x2000 0004*/ rt_ubase_t begin_align = RT_ALIGN((rt_ubase_t)begin_addr, RT_ALIGN_SIZE); /*如0x3000 0001~0x3000 0003,转后为0x3000 0000*/ rt_ubase_t end_align = RT_ALIGN_DOWN((rt_ubase_t)end_addr, RT_ALIGN_SIZE); /*调试信息,函数不可用于中断内部*/ RT_DEBUG_NOT_IN_INTERRUPT; /* 分配地址范围至少能存储两个heap_mem */ if ((end_align > (2 * SIZEOF_STRUCT_MEM)) && ((end_align - 2 * SIZEOF_STRUCT_MEM) >= begin_align)) { /* 计算可用堆区,4字节对齐 */ mem_size_aligned = end_align - begin_align - 2 * SIZEOF_STRUCT_MEM; } else { rt_kprintf("mem init, error begin address 0x%x, and end address 0x%x\n", (rt_ubase_t)begin_addr, (rt_ubase_t)end_addr); return; } /* heap_ptr指向堆区起始地址 */ heap_ptr = (rt_uint8_t *)begin_align; RT_DEBUG_LOG(RT_DEBUG_MEM, ("mem init, heap begin address 0x%x, size %d\n", (rt_ubase_t)heap_ptr, mem_size_aligned)); /* 初始化堆起始描述符 */ mem = (struct heap_mem *)heap_ptr; mem->magic = HEAP_MAGIC; mem->next = mem_size_aligned + SIZEOF_STRUCT_MEM; mem->prev = 0; mem->used = 0; #ifdef RT_USING_MEMTRACE rt_mem_setname(mem, "INIT"); #endif /* 初始化堆结束描述符 */ heap_end = (struct heap_mem *)&heap_ptr[mem->next]; heap_end->magic = HEAP_MAGIC; heap_end->used = 1; heap_end->next = mem_size_aligned + SIZEOF_STRUCT_MEM; heap_end->prev = mem_size_aligned + SIZEOF_STRUCT_MEM; #ifdef RT_USING_MEMTRACE rt_mem_setname(heap_end, "INIT"); #endif rt_sem_init(&heap_sem, "heap", 1, RT_IPC_FLAG_FIFO); /* 初始化释放指针指向堆的开始 */ lfree = (struct heap_mem *)heap_ptr; } 传入链接堆区的内存起始地址,以及结束地址。以STM32为例,传入0x20000000--0x20018000,96k字节
上述rt_system_heap_init( 0x20000000,0x20018000),主要做了下图这么一件事情。
将堆管理头尾描述符进行了初始化,并指向对应的内存地址。用图翻译一下:
技巧点:
heap_end = (struct heap_mem *)&heap_ptr[mem->next];
用户调用rt_malloc 用于申请分配动态内存。 void *rt_malloc(rt_size_t size){ rt_size_t ptr, ptr2; struct heap_mem *mem, *mem2; if (size == 0) return RT_NULL; RT_DEBUG_NOT_IN_INTERRUPT; /*按四字节对齐申请,如申请5字节,则实际按8字节申请*/ if (size != RT_ALIGN(size, RT_ALIGN_SIZE)) RT_DEBUG_LOG(RT_DEBUG_MEM, ("malloc size %d, but align to %d\n", size, RT_ALIGN(size, RT_ALIGN_SIZE))); else RT_DEBUG_LOG(RT_DEBUG_MEM, ("malloc size %d\n", size)); /* 按四字节对齐申请,如申请5字节,则实际按8字节申请 */ size = RT_ALIGN(size, RT_ALIGN_SIZE); if (size > mem_size_aligned) { RT_DEBUG_LOG(RT_DEBUG_MEM, ("no memory\n")); return RT_NULL; } /* 每块的长度必须至少为MIN_SIZE_ALIGNED=12 STM32*/ if (size < MIN_SIZE_ALIGNED) size = MIN_SIZE_ALIGNED; /* 获取堆保护信号量 */ rt_sem_take(&heap_sem, RT_WAITING_FOREVER); for (ptr = (rt_uint8_t *)lfree - heap_ptr; ptr < mem_size_aligned - size; ptr = ((struct heap_mem *)&heap_ptr[ptr])->next) { mem = (struct heap_mem *)&heap_ptr[ptr]; /*如果该块未使用,且满足大小要求*/ if ((!mem->used) && (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size) { /* mem没有被使用,至少完美的配合是可能的: * mem->next - (ptr + SIZEOF_STRUCT_MEM) 计算出mem的“用户数据大小” */ if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED)) { /* (除了上面的,我们测试另一个结构heap_mem (SIZEOF_STRUCT_MEM) * 是否包含至少MIN_SIZE_ALIGNED的数据也适合'mem'的'用户数据空间') * -> 分割大的块,创建空的余数, * 余数必须足够大,以包含MIN_SIZE_ALIGNED大小数据: * 如果mem->next - (ptr + (2*SIZEOF_STRUCT_MEM)) == size, * struct heap_mem 会适合,在mem2及mem2->next没有使用 */ ptr2 = ptr + SIZEOF_STRUCT_MEM + size; /* create mem2 struct */ mem2 = (struct heap_mem *)&heap_ptr[ptr2]; mem2->magic = HEAP_MAGIC; mem2->used = 0; mem2->next = mem->next; mem2->prev = ptr; #ifdef RT_USING_MEMTRACE rt_mem_setname(mem2, " "); #endif /*将ptr2插入mem及mem->next之间 */ mem->next = ptr2; mem->used = 1; if (mem2->next != mem_size_aligned + SIZEOF_STRUCT_MEM) { ((struct heap_mem *)&heap_ptr[mem2->next])->prev = ptr2; } #ifdef RT_MEM_STATS used_mem += (size + SIZEOF_STRUCT_MEM); if (max_mem < used_mem) max_mem = used_mem; #endif } else { mem->used = 1; #ifdef RT_MEM_STATS used_mem += mem->next - ((rt_uint8_t *)mem - heap_ptr); if (max_mem < used_mem) max_mem = used_mem; #endif } /* 设置块幻数 */ mem->magic = HEAP_MAGIC; #ifdef RT_USING_MEMTRACE if (rt_thread_self()) rt_mem_setname(mem, rt_thread_self()->name); else rt_mem_setname(mem, "NONE"); #endif if (mem == lfree) { /* 寻找下一个空闲块并更新lfree指针*/ while (lfree->used && lfree != heap_end) lfree = (struct heap_mem *)&heap_ptr[lfree->next]; RT_ASSERT(((lfree == heap_end) || (!lfree->used))); } rt_sem_release(&heap_sem); RT_ASSERT((rt_ubase_t)mem + SIZEOF_STRUCT_MEM + size <= (rt_ubase_t)heap_end); RT_ASSERT((rt_ubase_t)((rt_uint8_t *)mem + SIZEOF_STRUCT_MEM) % RT_ALIGN_SIZE == 0); RT_ASSERT((((rt_ubase_t)mem) & (RT_ALIGN_SIZE - 1)) == 0); RT_DEBUG_LOG(RT_DEBUG_MEM, ("allocate memory at 0x%x, size: %d\n", (rt_ubase_t)((rt_uint8_t *)mem + SIZEOF_STRUCT_MEM), (rt_ubase_t)(mem->next - ((rt_uint8_t *)mem - heap_ptr)))); RT_OBJECT_HOOK_CALL(rt_malloc_hook, (((void *)((rt_uint8_t *)mem + SIZEOF_STRUCT_MEM)), size)); /* 返回除mem结构之外的内存地址 */ return (rt_uint8_t *)mem + SIZEOF_STRUCT_MEM; } } /* 释放堆保护信号量 */ rt_sem_release(&heap_sem); return RT_NULL; } 其基本思路,从空闲块链表开始检索内存块,如检索到某块空闲且满足申请大小且其剩余空间至少能存储描述符,则满足了申请要求,则将后续内存头部生成描述,更新前后指针,标记幻数以及块已被使用标记,将该块插入链表。返回申请成功的内存地址。如果检索不到,则返回空指针,表示申请失败,堆目前没有满足要求的内存可供使用。实际上,上述代码在运行时将堆内存区按照下述示意图进行动态维护。
概括一下:
释放内存由rt_free实现: void rt_free(void *rmem){ struct heap_mem *mem; if (rmem == RT_NULL) return; RT_DEBUG_NOT_IN_INTERRUPT; RT_ASSERT((((rt_ubase_t)rmem) & (RT_ALIGN_SIZE - 1)) == 0); RT_ASSERT((rt_uint8_t *)rmem >= (rt_uint8_t *)heap_ptr && (rt_uint8_t *)rmem < (rt_uint8_t *)heap_end); RT_OBJECT_HOOK_CALL(rt_free_hook, (rmem)); /* 申请释放地址不在堆区 */ if ((rt_uint8_t *)rmem < (rt_uint8_t *)heap_ptr || (rt_uint8_t *)rmem >= (rt_uint8_t *)heap_end) { RT_DEBUG_LOG(RT_DEBUG_MEM, ("illegal memory\n")); return; } /* 获取块描述符 */ mem = (struct heap_mem *)((rt_uint8_t *)rmem - SIZEOF_STRUCT_MEM); RT_DEBUG_LOG(RT_DEBUG_MEM, ("release memory 0x%x, size: %d\n", (rt_ubase_t)rmem, (rt_ubase_t)(mem->next - ((rt_uint8_t *)mem - heap_ptr)))); /* 获取堆保护信号量 */ rt_sem_take(&heap_sem, RT_WAITING_FOREVER); /* 待释放的内存,其块描述符需是使用状态 */ if (!mem->used || mem->magic != HEAP_MAGIC) { rt_kprintf("to free a bad data block:\n"); rt_kprintf("mem: 0x%08x, used flag: %d, magic code: 0x%04x\n", mem, mem->used, mem->magic); } RT_ASSERT(mem->used); RT_ASSERT(mem->magic == HEAP_MAGIC); /* 清除使用标志 */ mem->used = 0; mem->magic = HEAP_MAGIC; #ifdef RT_USING_MEMTRACE rt_mem_setname(mem, " "); #endif if (mem < lfree) { /* 更新空闲块lfree指针 */ lfree = mem; } #ifdef RT_MEM_STATS used_mem -= (mem->next - ((rt_uint8_t *)mem - heap_ptr)); #endif /* 如临近块也处于空闲态,则合并整理成一个更大的块 */ plug_holes(mem); rt_sem_release(&heap_sem); } RTM_EXPORT(rt_free); 合并空闲块plug_holes static void plug_holes(struct heap_mem *mem){ struct heap_mem *nmem; struct heap_mem *pmem; RT_ASSERT((rt_uint8_t *)mem >= heap_ptr); RT_ASSERT((rt_uint8_t *)mem < (rt_uint8_t *)heap_end); RT_ASSERT(mem->used == 0); /* 前向整理 */ nmem = (struct heap_mem *)&heap_ptr[mem->next]; if (mem != nmem && nmem->used == 0 && (rt_uint8_t *)nmem != (rt_uint8_t *)heap_end) { /*如果mem->next是空闲,且非尾节点,则合并*/ if (lfree == nmem) { lfree = mem; } mem->next = nmem->next; ((struct heap_mem *)&heap_ptr[nmem->next])->prev = (rt_uint8_t *)mem - heap_ptr; } /* 后向整理 */ pmem = (struct heap_mem *)&heap_ptr[mem->prev]; if (pmem != mem && pmem->used == 0) { /* 如mem->prev空闲,将mem与mem->prev合并 */ if (lfree == mem) { lfree = pmem; } pmem->next = mem->next; ((struct heap_mem *)&heap_ptr[mem->next])->prev = (rt_uint8_t *)pmem - heap_ptr; } } 动态内存的释放相对比较简单,其思路主要是判断传入地址是否在堆区,如是堆内存,则判断其块信息是否合法。如果合法,则将使用标志清除。同时如果临近块如果是空闲态,则利用plug_holes将空闲块进行合并,合并成一个大的空闲块。 内存泄漏使用free释放内存失败会导致不可重用内存的累积,程序不再使用这些内存。这将浪费内存资源,并可能在耗尽这些资源时导致分配失败。 怎么使用堆堆区的配置对于STM32而言,位于board.h / * 配置堆区大小,可根据实际使用进行修改 */#define HEAP_BEGIN STM32_SRAM1_START #define HEAP_END STM32_SRAM1_END /* 用于板级初始化堆区 */ void rt_system_heap_init(void *begin_addr, void *end_addr) 堆的使用接口用于动态申请内存 void *rt_malloc(rt_size_t size) /*追加申请内存,此函数将更改先前分配的内存块。*/ void *rt_realloc(void *rmem, rt_size_t newsize) /* 申请的内存被初始化为0 */ void *rt_calloc(rt_size_t count, rt_size_t size) 内存分配不能保证成功,而是可能返回一个空指针。使用返回的值,而不检查分配是否成功,将调用未定义的行为。这通常会导致崩溃,但不能保证会发生崩溃,因此依赖于它也会导致问题。 对于申请的内存,使用前必须进行返回值判断,否则申请失败,且任继续使用。将会出现意想不到的错误!! 总结一下通过对RT-Thread的小堆管理器实现的梳理,层层递进更深入理解以下一些要点:
|
| 谢谢分享 |
微信公众号
手机版