0简介对于嵌入式设备来说,用户态内存管理是一项基本功能。目前主流的用户态内存管理库有glibc、uclibc、tcmalloc、jemalloc等,本文在分析glibc2.17版本的基础上,讨论glibc内存分配的原理和内存站岗问题的原因,并给出解决glibc缓存大量内存(高达几十G甚至上百G)不释放的问题。计划。我遇到的问题是基于glibc进行内存管理的64位Linux系统。具体现象如下:设备物理内存为32G,在大规模流的情况下,一个用户进程占用的物理内存飙升至20G左右。停止推流后,观察到业务模块已经释放了大部分内存,但是进程占用的物理内存仍然达到了16G左右。此后内存状态一直保持这种状态,导致系统内存吃紧。如果叠加在其他业务上,就会出现OOM现象,已经排除了这个过程内存泄露的可能。1Glibc内存分配的基本原理Glibc使用ptmalloc的内存管理方式,本文描述中称之为glibc。Glibc申请内存时,从分配区开始申请,分配区分为主分配区和非主分配区。分配区有锁。在分配内存之前,需要先获取锁,然后再申请内存。通常,进程是多线程的。当多个线程需要同时申请内存时,如果只有一个分配区,效率太低。glibc为了支持多线程内存申请释放,当多个线程需要同时申请内存时,glibc会根据CPU核数分配一定数量的分配区,并将分配区分配给线程。如果线程数较多,会出现多个线程争夺一个分配区的情况,这里不展开。内存申请的基本原理:当用户调用malloc申请内存时,glibc会检查内存是否已经被缓存。如果有缓存,则优先使用缓存内存,返回满足用户请求大小的内存块。如果没有缓存或者缓存不足,它会向操作系统申请内存(可以通过brk和mmap申请内存),然后切一块内存给用户,如图1所示。图1内存释放的基本原理:当业务模块调用free释放使用后的内存时,glibc会检查内存块虚拟地址上下内存块的使用情况(fastbin除外)。如果前一块内存是空闲的,它将与前一块内存合并。如果下一块内存空闲,则与下一块内存合并。如图2所示。如果下一块内存是topchunk(topchunk一直空闲),检查topchunk的大小是否超过一个阈值,如果超过一个阈值就释放给OS,如图3.图2图32Glibc内存保护及其原因内存保护概念:内存保护是指glibc向OS申请内存后,为业务模块分配内存,业务模块使用后释放内存,但glibc并不不释放空闲内存给操作系统。即大量空闲内存被缓存,无法归还给系统。memorystandingguard的原因:glibc在设计上就确定了它的内存被使用的生命周期很短,所以在设计中向OS释放内存的时机是当topchunk的大小超过一个阈值时,它就会将topchunk的部分内存释放给操作系统。当topchunk不超过阈值时,内存不会释放给OS。那么问题来了,如果与topchunk相邻的内存块一直在使用,那么topchunk永远不会超过阈值,即使业务模块大量释放内存,达到几十G或者几百G,glibc也不可能把内存还给OS。对于glibc来说,它有主分配区和非主分配区的概念。主分配使用sbrk增加分配区的内存大小,而非主分配区使用一个或多个内存块从mmap中用链表连接起来模拟主分配区。为了更清楚的解释内存保护,下面以主分配区的内存保护为例进行说明,如图4所示。如图4所示,有(a)(c)(e)(g)正在使用的内存块,所以freememory(b)(d)(f)不能和topchunk连接起来形成更大的freememoryblock,glibc的阈值(64位系统默认是128K),虽然目前有将近130M的空闲内存,但是无法归还给OS。接下来,看看非主分配区的内存守卫。如图5所示,实际的非主分配区可能有很多堆。这里,假设只有4个堆。图5在定位过程中,笔者多次与同事讨论如何解决站岗问题。在一次讨论中,邓宏杰提出减小堆的大小(类似于tcmalloc的做法)。虽然经过实测发现完全无效,但对日后解决问题起到了启发作用。后来看代码的时候发现这是glibc的native机制。同时,我在查看内存布局时,观察到非主分配区有大量堆处于空闲状态。原来的机制是先释放heap3。如果heap3有内存,即使释放heap0、heap1、heap2的内存,也无法释放给系统。glibc有多个分配区,如果每个分配区都有几百M的空闲内存,那么整个进程占用几十G也就不足为奇了。3Glibcmemoryguard解决方案及内存释放时的patch,主要分配的进程区和非主分配是不同的。我们64位系统的进程内存模型是经典模式,栈是从高地址向低地址增长的。的。我没有在主分配区遇到内存守卫。如果主分配区内存被守护,一种方法是尝试madvise释放主分配区pagesize-alignedfreememory,但效果可能不明显。另一种是创建一个线程,然后把主线程的业务移到一个新的线程中,这样主分布区就不会引起守卫,守卫转移到非主分布区,也就是我们的.接下来要优化的主战场。针对非主分配区做了两处优化:a)heap0,heap1,heap2是空闲的,那么我们就可以释放heap1和heap2;b)heap默认为64M,减少每个heap的大小(笔者测试时设置的)为512K)。图6这里需要说明一下为什么heap0和最后一个heap3没有释放。heap0的组成如图7所示,图中左侧是第一个堆,即heap0,图右侧是最后一个堆,即heap3。从图中可以清楚的看出,如果释放heap0,就会释放structmalloc_state结构体,从而导致进程崩溃。右边的不能释放,因为有内存在使用。当然,如果heap3的内存全部释放了,会由glibcnative代码处理,patch就不再处理了。图7修改glibc源码后,优化其发布机制,实际播放流式测试。stream到峰值后,进程使用了??20G内存,停止stream后几秒内又回到stream前的内存水平,进程占用的内存基本归还给系统。至此glibc内存站岗问题解决。上面我们已经介绍了如何解决内存保护的原理。纸上谈兵不容易看懂。下面我们来看补丁源码实现。目前,作者已将优化后的补丁提交开源社区审核。提交给社区的补丁没有修改堆的大小。这是因为我想更加谨慎。毕竟开源代码的使用场景很多。如果需要,您可以自行决定堆的大小。补丁基于glibc2.17代码1.Index:arena.c2.=====================================================================3.---arena.c(revision2)4.+++arena.c(workingcopy)5.@@-652,7+652,7@@6.7.staticint8.internal_function9.-heap_trim(heap_info*heap,size_tpad)10.+heap_trim(heap_info*heap,heap_info*free_heap,size_tpad)11.{12.mstatear_ptr=heap->ar_ptr;13.unsignedlongpagesz=GLRO(dl_pagesize);14.@@-659,7+659,29@@15.mchunkptrtop_chunk=top(ar_ptr),p,bck,fwd;16.heap_info*prev_heap;17.longnew_size,top_size,extra,prev_size,misalign;18.+heap_info*last_heap;19.20.+/*Releaseheapifpossible*/21.+last_heap=heap_for_ptr(top_chunk);22.+if((NULL!=free_heap->prev)&&(last_heap!=free_heap)){23.+p=chunk_at_offset(free_heap,sizeof(*free_heap));24.+if(!inuse(p)){25.+if(chunksize(p)+sizeof(*free_heap)+MINSIZE==free_heap->size){26.+while(last_heap){27.+if(last_heap->prev==free_heap){28.+last_heap->prev==free_heap->prev;29.+break;30.+}31.+last_heap=last_heap->prev;32.+}33.+ar_ptr->system_mem-=free_heap->size;34.+arena_mem-=free_heap->size;35.+unlink(p,bck,fwd);36.+delete_heap(free_heap);37.+return1;38.+}39.+}40.+}41./*这个堆能完全消失吗?*/42.while(top_chunk==chunk_at_offset(heap,sizeof(*heap))){43.prev_heap=heap->上一页;44.索引:malloc.c45.=============================================================================46.---malloc.c(revision2)47.+++malloc.c(工作副本)48.@@-915,7+915,7@@49.#if__WORDSIZE==3250.#defineDEFAULT_MMAP_THRESHOLD_MAX(512*1024)51.#else52.-#defineDEFAULT_MMAP_THRESHOLD_MAX(4*1024*1024*sizeof(long))53。+#defineDEFAULT_MMAP_THRESHOLD_MAX(256*1024)54.#endif55.#endif56.57.@@-3984,7+3984,7@@58.heap_info*heap=heap_for_ptr(top(av));59.60.assert(heap->ar_ptr==av);61.-heap_trim(heap,mp_.top_pad);62.+heap_trim(heap,heap_for_ptr(p),mp_.top_pad);63.}64.}结论不同的内存管理方式各有千秋及缺点,由于工作需要,笔者有幸研究了glibc、tcmalloc、uclibc内存管理,本文讨论glibc内存管理中的一个常见问题,并给出了可行的解决方案。自己缓存一些长时间不释放的内存。另一种是简单地将glibc替换为tcmalloc。因为tcmalloc的span比较小,所以guarding发生的概率极低,即使发生了也是一个span的大小。如果由于某些原因不能使用tcmalloc代替glibc,可以尝试上述解决方案。这个问题困扰了我们很久,花了很长时间,费了很大力气才定位出来。在glibc2.28版本,glibc有tcache的特性,对于业务进程使用大量小内存的场景,更容易出现内存站岗的问题。写这篇文章的时候查看了glibc2.33版本,开源社??区没有修改问题(可能开源社区高手认为这不是glibc的问题,而是用户没有释放内存)。
