glibc下的内存管理

几周前我曾提到,我被项目组分配去作了一些探究linux下内存管理机制的活儿。由于咱们的产品遇到了一些与之相关的“诡异”问题。这些问题以及相关状况能够归纳以下: html

  • 先介绍一下相关的背景。因为咱们是3D软件,因此用户常常会有“导入/导出”各类geometry的需求。而一个存储这些数据的文件,可能含有不止一个geometry,并且每一个geometry中也可能存在着成千上万个面片/多边形等各类基本元素。这些元素自己都不大,但数量不少。
  • 第一次导入geometry时,会占据大量内存(好比说吧,有1.5G)以上;在不关闭软件而进行各类“清理”操做后,内存却基本不释放;接着再次导入相同的geometry时,内存也没有明显增长;然而若是再进行一次导入操做的话,内存又会被大量占用(约1G以上)。
  • 将以上试验,换成先导入geometry1, 而后清理场景, 再导入geometry2,此时geometry2的内存占用量,要比单独首次导入geometry2时所占用的内存量要小。
  • valgrind是一款在linux下常用检查各类内存管理问题的工具集合。咱们用valgrind的memcheck组件进行过专门的内存泄露测试,并未发现明显的泄露状况。
  • 咱们的产品在mac平台上也有相应的版本。拿到mac os x上作实验,发现一样的代码,表现并不相同。其中每次清理场景后,都会有可观的内存(约600-800MB)被退回给操做系统(OS), 不过并不彻底等于导入geometry前的内存量。
  • 能够肯定linux上的malloc函数用的是glibc的ptmalloc的实现。而mac上没有用到glibc,是它本身的实现。(具体信息待查)
  • 咱们的产品在为这些需求分配内存的时候,虽然通过“包装”,但主要是为了检查内存是否用尽从而及时提出警告。归根到底使用的仍是标准的glibc的分配器(__libc_malloc(size_t)

以上的描述都是基于客观事实。而我探索的主要手段,就是根据这些事实搜索互联网(google/百度)。几天下来收获颇丰。下面总结一些收获。 linux

  • 相似的案例 :

    • GLIBC内存分配机制引起的“内存泄露”

      咱们正在开发的类数据库系统有一个内存模块,出现了一个疑似”内存泄露”问题,现象以下:内存模块的内存释放之后没有归还操做系统,好比内存模块占用的内存为10GB,释放内存之后,经过TOP命令或者/proc/pid/status查看占用的内存有时仍然为10G,有时为5G,有时为3G, etc,内存释放的行为不肯定。 git

    • 有大量的相关提问在stackoverflow上能够被搜到。可自行搜索。好比这个 : Linux Allocator Does Not Release Small Chunks of Memory
  • malloc()/free(), mmap(), brk(), 还有,用户程序-->glibc -->linux kernel之间的关系

    • malloc()/free()是C语言下负责内存分配/释放的两个很是基础的函数。然而,做为C标准,ANSI C并无指定它们具体应该如何实现。所以在各个系统级平台上(windows, mac, linux等等),调用这两个函数时,底层的内存操纵方式并不同。
    • 在linux下,malloc()/free()的实现是由glibc库负责的。这是一个至关底层的库,它会根据必定的策略,与系统底层通讯(调用系统API)。由于glibc的这层关系,在涉及到内存管理方面,用户程序并不会直接和linux kernel进行交互,而是交由glibc托管,因此能够认为glibc提供了一个默认版本的内存管理器。它们的关系就像这样:用户程序---->glibc---->linux kernel。
    • glibc使用了ptmalloc做为其内存管理器的实现。关于ptmalloc到底是如何管理内存的,我看了不少教程,其中这篇 我认为讲得最通透,想了解真相的同窗推荐去那里看。下面是给本身作的潦草总结,不适合做为学习读物(截图都是link过来的)。 github

      screenshot


      bins, fastbins

      • brk分配的内chunk list,只能从top开始线性向下释放。释放掉中间的chunk,没法归还给OS,而是并链入到了bins/fast bins的容器中。
      • mmap分配的内存,等因而直接从物理内存中映射了一块过来。释放这块内存时,能够直接归还给OS。
      • 对于reqest的一块内存,究竟是由brk分配,仍是由mmap分配,这是由glibc策略机制决定的。
      • 有个threshold,能够调节这种策略。默认下,小于128kb由brk分配,大于等于则由mmap分配。
      • 但现代的glibc实现中(还没调查从哪一个版本开始),支持了动态调节threshold技术。默认下,在64位系统上,brk能够动态调整到从128kb到32mb。调整策略基本能够归纳为:发现对顶能够release的可用内存超过256kb的话,就将threshold调整到256kb。依次类推直到32mb.
      • 这个threshold也是能够人为控制的。具体见下面的连接。
      • 以上几点我写了一个小程序进行过验证,发现的确如此。测试的内容大概为,用一个双向链表(std::deque)装载设计过的chuck,根据指令,要么为尾端压入一个chunk, 要么从尾端弹出一个chunk,要么从首端弹出一个chunk,观察内存用量。发现,对于小size的chunk,从尾端弹出元素后,内存均可以释放,但从首端弹出的chunk,内存并无释放;若是chunk足够大,不管从尾端仍是首端,内存均可以释放。

      glibc使用如此的两种机制管理用户程序的内存,是有意设计使然。毕竟,与系统底层通讯的代价是昂贵的,若是动辄就直接操纵大量小块内存,就至关于频繁地与系统调用进行通讯,这样显然会下降程序的运行效率。将小块内存放入brk维护的一个堆中,就至关于实现了一块缓存(cache),用完了能够先攒起来,到时候能够一块儿归还给系统。公正地讲,这种设计挺smart的。 sql

      但是,它尚未smart得足够好。首先,因为它的实现相对来讲仍是比较简单,只维护了堆顶的一个指针。所以想要归还给系统的话,必须从顶向下,依次归还。想象一下这种状况,假如堆顶有块内存一直被占用着,而下面的全部内存都已经没用了。那下面的这些内存,能够归还给系统吗?很遗憾,这种设计决定了答案是不能够。这就出现了“洞(Hole)”的问题。 数据库

      另外,这种设计对一些因为业务需求,频繁申请/释放小块内存的用户程序而言,也不够友好。像咱们的这种3D软件,正是典型的一种状况:一个巨大的几何体,其实是由成千上万的小面片组成的,每个都不大,就是数量多。因此咱们的软件就会面临“已经释放了内存,但却没有归还给系统”的诡异问题。对付这种问题,最佳的策略,应该是早期就精心设计并使用一种适合咱们软件的“专用内存池”技术,申请连续的大块内存空间,手动”切割“开给众多小面片使用。到时候根据状况再分批归还给系统。总之,专门设计本身的内存管理方案总归是灵活多变的,能够视项目的需求状况而打造。 小程序

      话说回来, 虽然glibc制定了这种有些“强硬”的内存管理方案,但也提供了一些方法容许调节相关阈值(threshold),咱们虽然不能干涉怎么管理内存,但好歹能够经过这些方法,决定“多大算大,多小算小”以及“攒到多少就归还”等这类问题。 windows

  • mallopt() 与 malloc_trim(0)

    • mallopt是一个专门调节相关阈值的函数,具体细节就不讲了,man手册上说得就挺明白的。下面贴的一段仍是留给本身的。想了解详情的同窗请点这里数组

      #include < malloc.h > 缓存

      int mallopt(int param, int value);


      M_MMAP_THRESHOLD

      For allocations greater than or equal to the limit specified (in bytes) by M_MMAP_THRESHOLD that can't be satisfied from the free list, the memory-allocation functions employ mmap(2) instead of increasing the program break using sbrk(2).

      Allocating memory using mmap(2) has the significant advantage that the allocated memory blocks can always be independently released back to the system. (By contrast, the heap can be trimmed only if memory is freed at the top end.) On the other hand, there are some disadvantages to the use of mmap(2): deallocated space is not placed on the free list for reuse by later allocations; memory may be wasted because mmap(2) allocations must be page-aligned; and the kernel must perform the expensive task of zeroing out memory allocated via mmap(2). Balancing these factors leads to a default setting of 128*1024 for the M_MMAP_THRESHOLD parameter.

      The lower limit for this parameter is 0. The upper limit is DEFAULT_MMAP_THRESHOLD_MAX: 5121024 on 32-bit systems or 410241024sizeof(long) on 64-bit systems.

      Note: Nowadays, glibc uses a dynamic mmap threshold by default. The initial value of the threshold is 128*1024, but when blocks larger than the current threshold and less than or equal to DEFAULT_MMAP_THRESHOLD_MAX are freed, the threshold is adjusted upwards to the size of the freed block. When dynamic mmap thresholding is in effect, the threshold for trimming the heap is also dynamically adjusted to be twice the dynamic mmap threshold. Dynamic adjustment of the mmap threshold is disabled if any of the M_TRIM_THRESHOLD, M_TOP_PAD, M_MMAP_THRESHOLD, or M_MMAP_MAX parameters is set.

    • malloc_trim()是一个颇有意思的函数。“有意思”在我到如今还不是很明白它究竟是怎么工做的。这里也是我很想向各位请教的地方(若有看法,请不吝赐教)。根据man手册的解释,它应该是负责告诉glibc在brk维护的堆队列中,堆顶留下多少的空余空间(free space),其余往上的空余空间所有归还给系统。并且手册明确说明,它不能归还除堆顶以外的内存。下面贴一段man手册的官方描述:

      The malloc_trim() function attempts to release free memory at the top of the heap (by calling sbrk(2) with a suitable argument).

      The pad argument specifies the amount of free space to leave untrimmed at the top of the heap. If this argument is 0, only the minimum amount of memory is maintained at the top of the heap (i.e., one page or less). A nonzero argument can be used to maintain some trailing space at the top of the heap in order to allow future allocations to be made without having to extend the heap with sbrk(2).

      按照描述所说,malloc_trim(0)应该只是归还堆顶上所有的空余内存给系统,按道理,它不该该会有能力归还堆顶下面的那些空余内存(那些“洞”)。不过,我本身作的小程序实验中,却推翻了这个论断。当我调用了malloc_trim(0)之后,我发现堆中所有的空余内存所有被归还给系统了,包括那些洞。不过,free list bing/fast bin中依然维护着这些内存地址,当再次须要申请小内存块时,老是前面的洞被再次从系统中“要”回来,而后分给调用者。这一点显得malloc_trim(0)很高级,我固然也很欢迎它具备这样出色的表现。但由于这样的行为与官方的手册描述有出入,让我理解起这个模型来至关困惑,真是百思不得姐...

      我作实验的平台是Linux RH5。代码也贴了出来(写得很烂)。考虑到贴在这里会显得很臃肿,我把它分享在这里。注意这个版本中已经把双向链表替换成了静态数组,纯粹是为了作实验,效果是同样的。

  • 由此想到的一些经验之谈

    • 注意之后写geometry相关的功能时,使用std::vector操做的时候尽量当心。尽可能成批reserve一块内存使用。减小在容器已满的状况下仍然push_back单个元素的操做,这样很是容易产生碎片。
    • 另外即使是在栈上分配一个std::vector(意味着出栈即被回收),也要注意它维护的队列倒是分配在heap上的。也就是说一个这样的临时对象所操做过的内存,依然可能产生碎片。若是这样的函数被频繁调用,碎片就会很是多。
    • 还有即使咱们作过shrink_to_fit的工做(std::vector<t*>(v).swap(v)),若是里面是碎片,那也会被驻留在brk维护的free_list中,不会被释放。
相关文章
相关标签/搜索