引言
Linux glibc 内存站岗问题及解决方法
作者简介
刘冬云
2017年硕士毕业于杭州电子科技大学。现就职于杭州一家网络设备通信公司,2020年开始任职Linux内核工程师,主要负责操作系统的维护工作,偏重于内存管理方面。本文基于glibc2.17版本进行分析,围绕glibc内存分配原理、内存站岗问题形成原因展开讨论,并对glibc缓存大量内存(高达几十个 G甚至上百 G)且不释放的问题给出一种解决方案。
笔者遇到的问题是基于glibc进行内存管理的64 位Linux系统。具体现象如下:设备32G物理内存,在大规格打流情况下,某用户进程占用的物理内存暴涨至20G左右。
在停止打流后,观察到业务模块已经释放了绝大部分内存,但是进程占用的物理内存依然达到16G左右,此后内存状况一直维持该状态,导致系统内存紧张,若叠加上其他业务则出现了OOM的现象,已排除该进程内存泄露的可能性。
Glibc内存分配基本原理
Glibc使用了ptmalloc的内存管理方式,本文在描述时均使用glibc来称呼。Glibc申请内存时是从分配区申请的,分为主分配区和非主分配区,分配区都有锁,在分配内存前需要先获取锁,然后再去申请内存。
一般进程都是多线程的,当多个线程同时需要申请内存时,如果只有一个分配区,那么效率太低。
glibc为了支持多线程的内存申请释放,会在多个线程同时需要申请内存时根据cpu核数分配一定数量的分配区,将分配区分配给线程。如果线程数量较多,则会出现多个线程争用一个分配区的的情况,这里不展开。
内存申请基本原理:当用户调用malloc申请内存时,glibc会查看是否已经缓存了内存,如果有缓存则会优先使用缓存内存,返回一块符合用户请求大小的内存块。
如果没有缓存或者缓存不足则会去向操作系统申请内存(可通过brk、mmap申请内存),然后切一块内存给用户,如图1所示。
内存释放基本原理:当业务模块使用完毕后调用free释放内存时,glibc会检查该内存块虚拟地址上下内存块的使用状态(fast bin除外)。若其上一块内存空闲,则与上一块内存进行合并。若下一块内存空闲,则与下一块内存进行合并。如图2所示。
若下一块内存时top chunk(top chunk一直是空闲的),则看top chunk的大小是否超过一个阈值,如果超过一个阈值则将其释放给OS,如图3所示。
Glibc内存站岗及其原因
内存站岗概念:
内存站岗指的是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所示。
Glibc内存站岗解决方法及patch
在内存释放时,对于主分配区和非主分配其走的流程是不一样的,我们64位系统的进程内存模型为经典模式,栈是从高地址向低地址生长的。
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. static int
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内存管理存在的一个共性问题,并给出可行的解决方案。
扫码识别二维码,关注“Linux阅码场”