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中,不會被釋放。
相關文章
相關標籤/搜索