小知识:Linux Glibc 内存站岗问题及解决方法

%小知识:Linux Glibc 内存站岗问题及解决方法-猿站网-插图

0引言

对于嵌入式设备来说,用户态内存管理是一项基础功能,目前主流的用户态内存管理库有glibc、uclibc、tcmalloc、jemalloc等。

本文基于glibc2.17版本进行分析,围绕glibc内存分配原理、内存站岗问题形成原因展开讨论,并对glibc缓存大量内存(高达几十个 G甚至上百 G)且不释放的问题给出一种解决方案。

笔者遇到的问题是基于glibc进行内存管理的64 位Linux系统。具体现象如下:设备32G物理内存,在大规格打流情况下,某用户进程占用的物理内存暴涨至20G左右。

在停止打流后,观察到业务模块已经释放了绝大部分内存,但是进程占用的物理内存依然达到16G左右,此后内存状况一直维持该状态,导致系统内存紧张,若叠加上其他业务则出现了OOM的现象,已排除该进程内存泄露的可能性。

1Glibc内存分配基本原理

Glibc使用了ptmalloc的内存管理方式,本文在描述时均使用glibc来称呼。Glibc申请内存时是从分配区申请的,分为主分配区和非主分配区,分配区都有锁,在分配内存前需要先获取锁,然后再去申请内存。

一般进程都是多线程的,当多个线程同时需要申请内存时,如果只有一个分配区,那么效率太低。

glibc为了支持多线程的内存申请释放,会在多个线程同时需要申请内存时根据cpu核数分配一定数量的分配区,将分配区分配给线程。如果线程数量较多,则会出现多个线程争用一个分配区的的情况,这里不展开。

内存申请基本原理:当用户调用malloc申请内存时,glibc会查看是否已经缓存了内存,如果有缓存则会优先使用缓存内存,返回一块符合用户请求大小的内存块。

如果没有缓存或者缓存不足则会去向操作系统申请内存(可通过brk、mmap申请内存),然后切一块内存给用户,如图1所示。

%小知识:Linux Glibc 内存站岗问题及解决方法-1猿站网-插图

图 1

内存释放基本原理:当业务模块使用完毕后调用free释放内存时,glibc会检查该内存块虚拟地址上下内存块的使用状态(fast bin除外)。若其上一块内存空闲,则与上一块内存进行合并。若下一块内存空闲,则与下一块内存进行合并。如图2所示。

若下一块内存时top chunk(top chunk一直是空闲的),则看top chunk的大小是否超过一个阈值,如果超过一个阈值则将其释放给OS,如图3所示。

%小知识:Linux Glibc 内存站岗问题及解决方法-2猿站网-插图

图2

%小知识:Linux Glibc 内存站岗问题及解决方法-3猿站网-插图

图3

2Glibc内存站岗及其原因

内存站岗概念:

内存站岗指的是glibc从OS申请到内存后分配给业务模块,业务模块使用完毕后释放了内存,但是glibc没有将这些空闲内存释放给OS,也就是缓存了很多空闲内存无法归还给系统的现象。

内存站岗原因:

glibc设计时就确定其内存是用于短生命周期的,因此在设计上内存释放给OS的时机是当top chunk的大小超过一个阈值时会释放top chunk的一部分内存给OS。当top chunk不超过阈值就不会释放内存给OS。

那么问题来了,若与top chunk相邻的内存块一直在使用中,那么top chunk就永远也不会超过阈值,即便业务模块释放了大量内存,达到几十个G 或者上百个G,glibc也是无法将内存还给OS的。

对于glibc来说,其有主分配和非主分配区的概念。主分配通过sbrk来增加分配区的内存大小,而非主分配区则是通过一个或多个mmap出来的内存块用链表链接起来模拟主分配区的。为了更清晰的解释内存站岗,下面举个例子来说明主分配区的内存站岗,如图4所示。

%小知识:Linux Glibc 内存站岗问题及解决方法-4猿站网-插图

图4

如上有(a) (c) (e) (g)内存块正在使用,故而导致了空闲内存(b) (d) (f)无法和top chunk连成一块更大的空闲内存块,glibc的阈值(64位系统默认是128K),尽管目前空闲内存有将近130M,也无法还给OS。

接下来看非主分配区的内存站岗,如图 5 所示,实际的非主分配区可能有很多个heap,这里假设只有4个heap。

%小知识:Linux Glibc 内存站岗问题及解决方法-5猿站网-插图

图5

在定位过程中,笔者与同事讨论过多次如何解决站岗。在一次讨论过程中由邓竑杰提出降低heap的size(类似于tcmalloc的做法),虽然实测后发现完全没有效果,但是为后续解决问题起到了启示作用。

后面笔者在走读代码时发现这是glibc原生机制,同时笔者在查看内存布局时观察到非主分配区大量heap均为free状态。原有机制是先释放heap3,如果heap3有内存在使用,尽管heap0、heap1、heap2的内存都释放了,那也是无法释放给系统。

glibc有多个分配区,每个分配区都几百 M 空闲内存的话,则整个进程占用达到几十个G也就不奇怪了。

3Glibc内存站岗解决方法及patch

在内存释放时,对于主分配区和非主分配其走的流程是不一样的,我们64位系统的进程内存模型为经典模式,栈是从高地址向低地址生长的。

对于主分配区的内存站岗我还没有遇到过,若主分配区内存站岗,一种方法是可以尝试madvise将主分配区的pagesize对齐的空闲内存进行释放,但是这样效果可能不太明显。

另外一种是通过创建线程,然后将主线程的业务移到新线程即可,这样主分配区就不会造成站岗了,而将站岗转移到了非主配区,而非主分配区则是我们接下来要进行优化的主战场。

针对非主分配区进行两处优化:

a) heap0,heap1,heap2是空闲的,那么我们就可以将heap1,heap2释放掉;

b) heap默认是64M,降低每个heap的size(笔者测试时设置为512K)。

%小知识:Linux Glibc 内存站岗问题及解决方法-6猿站网-插图

图 6

这里需要特别解释一下为什么不释放heap0和最后一个heap3,heap0的组成如图7所示。图左边是第一个heap即heap0,图右边是最后一个heap即heap3。

从图中可以清晰的看到如若释放掉heap0那么会将struct malloc_state结构体释放,会造成进程崩溃。右边这个由于有在用的内存,也不能释放掉。当然如果heap3的内存全部被释放了,则由glibc原生代码进行了处理,patch不再处理。

%小知识:Linux Glibc 内存站岗问题及解决方法-7猿站网-插图

图 7

经过修改glibc源码,优化其释放机制,实际打流测试。

在打流到峰值后,进程使用了20G的内存,在停止打流后数秒内便恢复到了打流前的内存水平,进程所占用的内存基本还给系统了。至此,glibc内存站岗问题得到解决。

以上我们介绍了如何解决内存站岗的原理,纸上得来终觉浅,现在我们看patch源码实现。

目前笔者已经将该优化的patch提交到开源社区审核,提交到社区的patch未对heap的size进行修改,这是因为想要谨慎一些,毕竟开源的代码使用场景较多,如有需要可自行决定heap的size。

Patch基于glibc2.17代码

1. Index: arena.c  2. ===================================================================  3. — arena.c (revision 2) 4. +++ arena.c (working copy)  5. @@ -652,7 +652,7 @@  6.  7. staticint 8. internal_function  9. -heap_trim(heap_info *heap, size_t pad)  10. +heap_trim(heap_info *heap, heap_info* free_heap, size_t pad)  11. {  12. mstate ar_ptr = heap->ar_ptr;  13. unsigned long pagesz = GLRO(dl_pagesize);  14. @@ -659,7 +659,29 @@  15. mchunkptr top_chunk = top(ar_ptr), p, bck, fwd;  16. heap_info *prev_heap;  17. long new_size, top_size, extra, prev_size, misalign;  18. + heap_info *last_heap;  19.  20. + /*Release heap if possible*/  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. + return 1;  38. + }  39. + }  40. + }  41. /* Can this heap go away completely? */  42. while(top_chunk == chunk_at_offset(heap, sizeof(*heap))) {  43. prev_heap = heap->prev;  44. Index: malloc.c  45. ===================================================================  46. — malloc.c (revision 2) 47. +++ malloc.c (working copy)  48. @@ -915,7 +915,7 @@  49. # if __WORDSIZE == 32  50. # define DEFAULT_MMAP_THRESHOLD_MAX (512 * 1024)  51. # else 52. –  # define DEFAULT_MMAP_THRESHOLD_MAX (4 * 1024 * 1024 * sizeof(long))  53. +# define DEFAULT_MMAP_THRESHOLD_MAX (256 * 1024)  54. # endif  55. #endif  56.  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比较小,所以站岗发生的概率极低,即便发生也就站岗一个span的大小。若由于某些原因不能用tcmalloc代替glibc的场景,如上的解决思路可以尝试一下,该问题也困扰我们多时了,花费了较长时间和较多精力去定位。

在glibc2.28的版本中,glibc有了tcache的特性,对于业务进程使用大量小内存的场景则更加容易出现内存站岗问题。在撰写本文时查看了glibc2.33版本,开源社区还未对该问题进行修改(或许是开源社区大神认为这不是glibc的问题,而是用户不释放内存)。

原文链接:https://mp.weixin.qq.com/s/CcLabnMzy8g3Ijq_Z4p_dg

声明: 猿站网有关资源均来自网络搜集与网友提供,任何涉及商业盈利目的的均不得使用,否则产生的一切后果将由您自己承担! 本平台资源仅供个人学习交流、测试使用 所有内容请在下载后24小时内删除,制止非法恶意传播,不对任何下载或转载者造成的危害负任何法律责任!也请大家支持、购置正版! 。本站一律禁止以任何方式发布或转载任何违法的相关信息访客发现请向站长举报,会员发帖仅代表会员个人观点,并不代表本站赞同其观点和对其真实性负责。本网站的资源部分来源于网络,如有侵权烦请发送邮件至:2697268773@qq.com进行处理。
建站知识

小知识:Linux用户宝典:值得一用的5款远程桌面应用程序

2023-4-29 5:17:07

建站知识

小知识:《Linux 内核编码风格》官方手册最新中译本

2023-4-29 5:34:19

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索