Glibc内存管理--ptmalloc2源代码分析

原文地址:http://mqzhuang.iteye.com/blog/1005909

1。问题

项目组正在研发的一个类似数据库的NoSql系统,遇到了Glibc的内存暴增问题。现象如下:在我们的NoSql系统中实现了一个简单的内存管理模块,在高压力高并发环境下长时间运行,当内存管理模块的内存释放给C运行时库以后,C运行时库并没有立即把内存归还给操作系统,比如内存管理模块占用的内存为10GB,释放内存以后,通过TOP命令或者/proc/pid/status查看进程占用的内存有时仍然为10G,有时为5G,有时为3G,etc,内存释放的行为不确定。

我们的NoSql系统中的内存管理方式比较简单,使用全局的定长内存池,内存管理模块每次分配/释放2MB内存,然后分成64KB为单位的一个个小内存块用hash加链表的方式进行管理。如果申请的内存小于等于64KB时,直接从内存池的空闲链表中获取一个内存块,内存释放时归还空闲链表;如果申请的内存大于64KB,直接通过C运行时库的malloc和free获取。某些数据结构涉及到很多小对象的管理,比如Hash表,B-Tree,这些数据结构从全局内存池获取内存后再根据数据结构的特点进行组织。为了提高内存申请/释放的效率,减少锁冲突,为每一个线程单独保留8MB的内存块,每个线程优先从线程专属的8MB内存块获取内存,专属内存不足时才从全局的内存池获取。

系统中使用的网络库有独立的内存管理方式,并不从全局内存池中分配内存,该网络库在处理网络请求时也是按2M内存为单位向C运行时库申请内存,一次请求完成以后,释放分配的内存到C运行时库。

在弄清楚了系统的内存分配位置以后,对整个系统进行了内存泄露的排查,在解决了数个内存泄露的潜在问题以后,发现系统在高压力高并发环境下长时间运行仍然会发生内存暴增的现象,最终进程因OOM被操作系统杀掉。

为了便于跟踪分析问题,在全局的内存池中加入对每个子模块的内存统计功能:每个子模块申请内存时都将子模块编号传给全局的内存池,全局的内存池进行统计。复现问题后发现全局的内存池的统计结果符合预期,同样对网络模块也做了类似的内存使用统计,仍然符合预期。由于内存管理不外乎三个层面,用户管理层,C运行时库层,操作系统层,在操作系统层发现进程的内存暴增,同时又确认了用户管理层没有内存泄露,因此怀疑是C运行时库的问题,也就是Glibc的内存管理方式导致了进程的内存暴增。

问题范围缩小了,但有如下的问题还没有搞清楚,搞不清楚这些问题,我们系统的中的问题就无法根本性解决。

  1.  Glibc在什么情况下不会将内存归还给操作系统?
  2.  Glibc的内存管理方式有哪些约束?适合什么样的内存分配场景?
  3. 我们的系统中的内存管理方式是与Glibc的内存管理的约束相悖的?
  4.  Glibc是如何管理内存的?

带着这些问题,决定对Glibc的ptmalloc2源代码进行一番研究,希望能找到这些问题的答案,并解决我们系统中遇到的问题。我研究的对象是当前最新版的glibc-2.12.1中的内存管理的相关代码。

2.2 操作系统内存分配的相关函数

上节提到heap和mmap映射区域是可以提供给用户程序使用的虚拟内存空间,如何获得该区域的内存呢?操作系统提供了相关的系统调用来完成相关工作。对heap的操作,操作系统提供了brk()函数,C运行时库提供了sbrk()函数;对mmap映射区域的操作,操作系统提供了mmap()和munmap()函数。sbrk(),brk() 或者 mmap() 都可以用来向我们的进程添加额外的虚拟内存。Glibc同样是使用这些函数向操作系统申请虚拟内存。

这里要提到一个很重要的概念,内存的延迟分配,只有在真正访问一个地址的时候才建立这个地址的物理映射,这是Linux内存管理的基本思想之一。Linux内核在用户申请内存的时候,只是给它分配了一个线性区(也就是虚拟内存),并没有分配实际物理内存;只有当用户使用这块内存的时候,内核才会分配具体的物理页面给用户,这时候才占用宝贵的物理内存。内核释放物理页面是通过释放线性区,找到其所对应的物理页面,将其全部释放的过程。

2.2.1 Heap操作相关函数

Heap操作函数主要有两个,brk()为系统调用,sbrk()为C库函数。系统调用通常提供一种最小功能,而库函数通常提供比较复杂的功能。Glibc的malloc函数族(realloc,calloc等)就调用sbrk()函数将数据段的下界移动,sbrk()函数在内核的管理下将虚拟地址空间映射到内存,供malloc()函数使用。

内核数据结构mm_struct中的成员变量start_code和end_code是进程代码段的起始和终止地址,start_data和 end_data是进程数据段的起始和终止地址,start_stack是进程堆栈段起始地址,start_brk是进程动态内存分配起始地址(堆的起始地址),还有一个 brk(堆的当前最后地址),就是动态内存分配当前的终止地址。C语言的动态内存分配基本函数是malloc(),在Linux上的实现是通过内核的brk系统调用。brk()是一个非常简单的系统调用,只是简单地改变mm_struct结构的成员变量brk的值。

这两个函数的定义如下:

       #include <unistd.h>

       int brk(void *addr);

       void *sbrk(intptr_t increment);

         需要说明的是,但sbrk()的参数increment为0时,sbrk()返回的是进程的当前brk值,increment为正数时扩展brk值,当increment为负值时收缩brk值。

    2.2.2 Mmap映射区域操作相关函数

mmap()函数将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。munmap执行相反的操作,删除特定地址区域的对象映射。函数的定义如下:

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

int munmap(void *addr, size_t length);

在这里不准备对这两个函数做详细介绍,只是对ptmalloc中用到的功能做一下介绍,其他的用法请参看相关资料。

参数:
  start:映射区的开始地址。
  length:映射区的长度。
  prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起。Ptmalloc中主要使用了如下的几个标志:
  PROT_EXEC //页内容可以被执行,ptmalloc中没有使用
  PROT_READ //页内容可以被读取,ptmalloc直接用mmap分配内存并立即返回给用户时设置该标志
  PROT_WRITE //页可以被写入,ptmalloc直接用mmap分配内存并立即返回给用户时设置该标志
  PROT_NONE //页不可访问,ptmalloc用mmap向系统“批发”一块内存进行管理时设置该标志


  flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体


  MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。Ptmalloc在回收从系统中“批发”的内存时设置该标志。
  MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。Ptmalloc每次调用mmap都设置该标志。
  MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。Ptmalloc向系统“批发”内存块时设置该标志。
  MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。Ptmalloc每次调用mmap都设置该标志。


  fd:有效的文件描述词。如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1。
  offset:被映射对象内容的起点。

没转完..........................................


  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值