[导读] 前文描述了栈的基本概念,本文来聊聊堆是怎么会事儿。RT-Thread 在社区广受欢迎,阅读了其内核代码,实现了堆的管理,代码设计很清晰,可读性很好。故一方面了解RT-Thread内核实现,一方面能够弄清楚其堆的内部实现。将学习体会记录分享,但愿对于堆的理解及实现有一个更深刻的认知。程序员
注,文中代码分析基于rt-thread-v4.0.2 版本。算法
C语言堆是由malloc(),calloc(),realloc()等函数动态获取内存的一种机制。使用完成后,由程序员调用free()等函数进行释放。使用时,须要包含stdlib.h头文件。编程
C++预言的堆管理则是使用new操做符向堆管理器申请动态内存分配,使用delete操做符将使用完毕内存的释放给堆管理器。windows
注:本文只描述C的堆管理器实现相关内容。数组
以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),主要作了下图这么一件事情。
将堆管理头尾描述符进行了初始化,并指向对应的内存地址。用图翻译一下:
技巧点:
mem = (struct heap_mem *)heap_ptr; 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的小堆管理器实现的梳理,层层递进更深刻理解如下一些要点:
文章出自微信公众号:嵌入式客栈,更多内容,请关注本人公众号,严禁商业使用,违法必究