这几天在看Glibc 内存管理模块的内容,感觉收获颇多,在此做个简单的总结,以便知识点回顾。
-
先介绍一下相关的背景。有个项目组在研发一个类似数据库的NoSql 系统时,遇到了Glibc 内存暴增问题。据此,在经过一系列排查过后,他们提出了几个问题,分别是:
-
1.Glibc 在什么情况下不会将内存归还给操作系统系统?
- 2.Glibc 的内存管理方式有哪些约束?适合什么样的内存分配场景?
-
3.我们系统中的内存管理方式是与Glibc的内存管理的约束相悖的吗?
-
4.Glibc 是如何管理内存的?
带着这些问题,我去看了Glibc的ptmalloc2源码,总结出了一些知识点。
首先,我们来看一个x86_64下Linux进程的默认内存布局形式的示意图:
如上图所示,对于AMD64 系统,内存布局采用经典布局:
- 首先被载入的是.text 段 ,然后是.data段,最后是.bss段。
- 最上面的128TB是内核使用的,应用程序不可以直接访问。
- 应用程序的堆栈从高地址处开始向下生长。
-
.bss段与堆栈之间的空间是空闲的,空闲空间被分成两部分,一部分为heap,一部分为mmap映射区域。mmap区域与栈区域相对增长。
-
在不同的Linux内核和机器上,mmap区域的开始位置一般是不同的。并且在当前内核默认配置下,进程的
栈
和mmap映射区域
并不是从一个固定地址开始,而且每次启动时的值都不一样。当然,可以通过以下命令:sudo sysctl -w kernel.randomize_va_space
改变全局变量randomize_va_space
的值,使之为0,让进程的栈和mmap映射区域从一个固定位置开始。
简单了解了一下x86_64下Linux进程的默认内存布局后,让我们来看一些 操作系统内存分配的相关函数
:
heap和mmap:
heap和mmap映射区域都是可以供用户程序自由使用的虚拟内存空间,但是它们在刚开始的时候并没有映射到内存空间内,是不可访问的。在内核请求分配该空间之前,对这个空间的访问会导致segmentation fault。用户可以直接使用系统调用来管理heap 和mmap映射区域,但更多的时候程序都是使用c语言提供的malloc 函数和free函数来动态分配和释放内存。Heap操作相关函数:
系统调用的brk()和C库函数sbrk()。Glibc的malloc函数族就是调用sbrk(),sbrk()函数在内核的管理下将虚拟地址空间映射到内存,供malloc函数使用。
C语言的动态内存分配基本函数是malloc(),在Linux上的实现(系统调用)是通过调用简单的brk()。Mmap映射区域操作相关函数:
Unix进程可以使用mmap函数来创建新的虚拟存储器区域,并将文件或对象映射到这些区域中。文件被映射到多个页上,如果文件的大小不是所有页之和,最后一个页不被使用的空间
将会清零。munmap执行相反的操作,删除特定地址区域的对象映射。函数的定义如下:
<code class="hljs glsl has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#include<sys/mman.h></span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> *mmap(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> *addr, size_t <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">length</span>, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">int</span> port, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">int</span> flags, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">int</span> fd, off_t offset); <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">int</span> munmap(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> *addr, size_t <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">length</span>);</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul>
内存的延迟分配:
注意,只有在真正访问一个地址的时候才建立这个地址的物理映射。Linux内核在用户申请内存的时候,只是给它分配了一个线性区(也就是虚拟内存),并没有分配实际物理内存;只有当用户使用这块内存的时候,内核才会分配具体的物理页面给用户,这时候才占用宝贵的物理内存。释放时,释放线性区,找到对应的物理页面,将其全部释放。
说明:当不知道程序的每个部分将需要多少内存时,系统内存空间有限,而内存需求又是变化的,这时就需要内存管理程序来负责分配和回收内存。程序的动态性越强,内存管理就越重要,内存分配程序的选择也就越重要。
内存管理的方法:
内存管理的方法有很多,它们各有优缺点,都有各自的最适用情形,在这里我只列出一些方法的主要优缺点及其比较适用的场景。
1.c风格的内存管理程序:
c风格的内存管理程序主要实现malloc和free函数。内存管理程序主要通过调用 brk()或者mmap()给进程添加额外的虚拟内存。
-
优点:
a.生存期局限于当前函数的内存时 非常容易 管理 b.对程序结构的变化要求不高。
-
缺点:不适合管理内存生存期长的程序。
-
常用情形:Doug Lea Malloc 、ptmalloc、BSD malloc、Hoard、TCMalloc .
2.池式内存管理:
内存池是一种半内存管理方法。内存池帮助某些程序进行自动内存管理,这些程序经历一些特定的阶段,每个阶段都有分配给进程的特定阶段自己的内存池。在结束每个阶段时,会一次释放所有内存。
-
优点:
a.显著优点是:内存分配和回收更快。因为每次都是在一个池中完成的,分配可以在O(1)时间内完成,释放内存池所需时间也差不多。 b.可以预先分配错误处理池,以便程序在常规内存被耗尽时,仍可以恢复。 c.应用程序可以简单地管理内存。有非常易于使用的标准实现。
-
缺点:缺点很多,个人觉得池式内存管理一般只适用特定的、有明显内存池特征的程序。
- 常用情形:很多网络服务进程,例如,Apache。
3.引用计数器
4.垃圾收集器:
垃圾收集器是一种动态存储分配器,它自动释放程序不再需要的已分配的块。在c程序的上下文中,应用调用malloc,但从不调用free,垃圾收集器定期识别垃圾块,并相应地调用free,将这些块放回空闲链表中。很多类型的垃圾收集器都需要知道数据结构内部指针的规划,为了正确运行,它们必须是语言本身的一部分。
-
优点:
a.永远不必担心内存的双重释放或者对象的生命周期。 b. 使用某些收集器,可以使用与常规分配相同的API。
-
缺点:
a.无法干涉何时释放内存。 b.慢 c.垃圾收集错误引发的缺陷难以调试。 d.不把 不再使用的指针 设置为 NULL,会有内存泄漏(很容易忘的,对吧!)
- 常用情形:诸如Java、ML、Perl等现代语言系统的一个重要部分。
好啦,第一部分的简单介绍就以 对ptmalloc 内存管理的概述结尾吧:
1.
ptmalloc实现了malloc(),free()以及一组其他的函数,以提供动态内存管理的支持。
2.
分配器处在用户程序和内核之间,它响应用户的分配请求,向操作系统申请内存,然后将其返回给用户程序。
3.为了保持高效的分配,分配器一般都会预先分配一块大于用户请求的内存,并通过某种算法来管理这块内存。
4.
用户释放掉内存并不立即返回给操作系统,分配器会管理这些被释放掉的空闲空间。当响应用户分配要求时,分配器会首先在空闲空间找一块合适的内存给用户,当在空闲空间找不到的情况下才分配一块新的内存。 5.
为了设计一个高效的分配器,所考虑的因素应该有很多,例如分配器本身所占内存等。