1. 程式人生 > >Linux堆記憶體管理深入分析(下)

Linux堆記憶體管理深入分析(下)



Linux堆記憶體管理深入分析 (下半部)

作者@走位,阿里聚安全

前言回顧

在上一篇文章中,詳細介紹了堆記憶體管理中涉及到的基本概念以及相互關係,同時也著重介紹了堆中chunk分配和釋放策略中使用到的隱式連結串列技術。通過前面的介紹,我們知道使用隱式連結串列來管理記憶體chunk總會涉及到記憶體的遍歷,效率極低。對此glibc malloc引入了顯示連結串列技術來提高堆記憶體分配和釋放的效率。

所謂的顯示連結串列就是我們在資料結構中常用的連結串列,而連結串列本質上就是將一些屬性相同的結點串聯起來,方便管理。在glibc malloc中這些連結串列統稱為bin,連結串列中的

結點就是各個chunk,結點的共同屬性就是:1)均為free chunk2)同一個連結串列中各個chunk的大小相等(有一個特例,詳情見後文)

1 bin介紹

如前文所述,bin是一種記錄free chunk的連結串列資料結構。系統針對不同大小的free chunk,將bin分為了4類:1) Fast bin; 2) Unsorted bin; 3) Small bin; 4) Large bin

glibc中用於記錄bin的資料結構有兩種,分別如下所示:

fastbinsY這是一個數組,用於記錄所有的fast bins

bins這也是一個數組,用於記錄除

fast bins之外的所有bins。事實上,一共有126bins,分別是:

bin 1 unsorted bin;

bin 2 63small bin;

bin 64126large bin

其中具體資料結構定義如下:

struct malloc_state

{

  ……

  /* Fastbins */

mfastbinptr fastbinsY[NFASTBINS];

  ……

  /* Normal bins packed as described above */

mchunkptr bins[NBINS * 2 - 2];  // #define NBINS    128

  ……

};

這裡mfastbinptr的定義:typedef struct malloc_chunk *mfastbinptr;

mchunkptr的定義:typedef struct malloc_chunk* mchunkptr;

畫圖更直觀:

1-1 bins分類

那麼處於bins中個各個free chunk是如何連結在一起的呢?回顧malloc_chunk的資料結構:

struct malloc_chunk {

  /* #define INTERNAL_SIZE_T size_t */

  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */

  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */

struct malloc_chunk* fd;         /* 這兩個指標只在free chunk中存在*/

struct malloc_chunk* bk;

  /* Only used for large blocks: pointer to next larger size.  */

  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */

  struct malloc_chunk* bk_nextsize;

};

其中的fdbk指標就是指向當前chunk所屬的連結串列中forward或者backward chunk

2 Fast bin

既然有fast bin,那就肯定有fast chunk——chunk size1680位元組的chunk就叫做fast chunk。為了便於後文描述,這裡對chunk大小做如下約定:

1)      只要說到chunk size,那麼就表示該malloc_chunk的實際整體大小;

2)      而說到chunk unused size,就表示該malloc_chunk中刨除諸如prev_size, size, fdbk這類輔助成員之後的實際可用的大小。因此,對free chunk而言,其實際可用大小總是比實際整體大小16位元組

在記憶體分配和釋放過程中,fast bin是所有bin中操作速度最快的。下面詳細介紹fast bin的一些特性:

1) fast bin的個數——10

2)每個fast bin都是一個單鏈表(只使用fd指標)。為什麼使用單鏈表呢?因為在fast bin中無論是新增還是移除fast chunk,都是對連結串列尾進行操作,而不會對某個中間的fast chunk進行操作。更具體點就是LIFO(後入先出)演算法:新增操作(free記憶體)就是將新的fast chunk加入連結串列尾,刪除操作(malloc記憶體)就是將連結串列尾部的fast chunk刪除。需要注意的是,為了實現LIFO演算法,fastbinsY陣列中每個fastbin元素均指向了該連結串列的rear end(尾結點),而尾結點通過其fd指標指向前一個結點,依次類推,如圖2-1所示。

3) chunk size10fast bin中所包含的fast chunk size是按照步進8位元組排列的,即第一個fast bin中所有fast chunk size均為16位元組,第二個fast bin中為24位元組,依次類推。在進行malloc初始化的時候,最大的fast chunk size被設定為80位元組(chunk unused size64位元組),因此預設情況下大小為1680位元組的chunk被分類到fast chunk。詳情如圖2-1所示。

4) 不會對free chunk進行合併操作。鑑於設計fast bin的初衷就是進行快速的小記憶體分配和釋放,因此係統將屬於fast binchunkP(未使用標誌位)總是設定為1,這樣即使當fast bin中有某個chunk同一個free chunk相鄰的時候,系統也不會進行自動合併操作,而是保留兩者。雖然這樣做可能會造成額外的碎片化問題,但瑕不掩瑜。

5) malloc(fast chunk)操作:即使用者通過malloc請求的大小屬於fast chunk的大小範圍(注意:使用者請求size加上16位元組就是實際記憶體chunk size)。在初始化的時候fast bin支援的最大記憶體大小以及所有fast bin連結串列都是空的,所以當最開始使用malloc申請記憶體的時候,即使申請的記憶體大小屬於fast chunk的記憶體大小(1680位元組),它也不會交由fast bin來處理,而是向下傳遞交由small bin來處理,如果small bin也為空的話就交給unsorted bin處理:

/* Maximum size of memory handled in fastbins.  */

static INTERNAL_SIZE_T global_max_fast;

/* offset 2 to use otherwise unindexable first 2 bins */

/*這裡SIZE_SZ就是sizeof(size_t),在32位系統為464位為8fastbin_index就是根據要mallocsize來快速計算該size應該屬於哪一個fast bin,即該fast bin的索引。因為fast binchunk是從16位元組開始的,所有這裡以8位元組為單位(32位系統為例)有減2*8 = 16的操作!*/

#define fastbin_index(sz) \

  ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

/* The maximum fastbin request size we support */

#define MAX_FAST_SIZE     (80 * SIZE_SZ / 4)

#define NFASTBINS  (fastbin_index (request2size (MAX_FAST_SIZE)) + 1)

那麼fast bin 是在哪?怎麼進行初始化的呢?當我們第一次呼叫malloc(fast bin)的時候,系統執行_int_malloc函式,該函式首先會發現當前fast bin為空,就轉交給small bin處理,進而又發現small bin 也為空,就呼叫malloc_consolidate函式對malloc_state結構體進行初始化,malloc_consolidate函式主要完成以下幾個功能:

a. 首先判斷當前malloc_state結構體中的fast bin是否為空,如果為空就說明整個malloc_state都沒有完成初始化,需要對malloc_state進行初始化。

b. malloc_state的初始化操作由函式malloc_init_state(av)完成,該函式先初始化除fast bin之外的所有的bins(構建雙鏈表,詳情見後文small bins介紹),再初始化fast bins

然後當再次執行malloc(fast chunk)函式的時候,此時fast bin相關資料不為空了,就開始使用fast bin(見下面程式碼中的※1部分)

static void *

_int_malloc (mstate av, size_t bytes)

{

  ……

  /*

     If the size qualifies as a fastbin, first check corresponding bin.

     This code is safe to execute even if av is not yet initialized, so we

     can try it without checking, which saves some time on this fast path.

   */

//第一次執行malloc(fast chunk)時這裡判斷為false,因為此時get_max_fast ()0

  if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))

    {

1  idx = fastbin_index (nb);

      mfastbinptr *fb = &fastbin (av, idx);

      mchunkptr pp = *fb;

      do

        {

          victim = pp;

          if (victim == NULL)

            break;

        }

while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))!= victim);

      if (victim != 0)

        {

          if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))

            {

              errstr = "malloc(): memory corruption (fast)";

            errout:

              malloc_printerr (check_action, errstr, chunk2mem (victim));

              return NULL;

            }

          check_remalloced_chunk (av, victim, nb);

          void *p = chunk2mem (victim);

          alloc_perturb (p, bytes);

          return p;

        }

    }

得到第一個來自於fast binchunk之後,系統就將該chunk從對應的fast bin中移除,並將其地址返回給使用者,見上面程式碼※2處。

6) free(fast chunk)操作:這個操作很簡單,主要分為兩步:先通過chunksize函式根據傳入的地址指標獲取該指標對應的chunk的大小;然後根據這個chunk大小獲取該chunk所屬的fast bin,然後再將此chunk新增到該fast bin的鏈尾即可。整個操作都是在_int_free函式中完成。

main arenaFast bins(即陣列fastbinsY)的整體操作示意圖如下圖所示:

2-1 fast bin示意圖

3 Unsorted bin

當釋放較小或較大的chunk的時候,如果系統沒有將它們新增到對應的bins(為什麼,在什麼情況下會發生這種事情呢?詳情見後文),系統就將這些chunk新增到unsorted bin中。為什麼要這麼做呢?這主要是為了讓“glibc malloc機制能夠有第二次機會重新利用最近釋放的chunk(第一次機會就是fast bin機制)。利用unsorted bin,可以加快記憶體的分配和釋放操作,因為整個操作都不再需要花費額外的時間去查詢合適的bin了。

Unsorted bin的特性如下:

1) unsorted bin的個數: 1個。unsorted bin是一個由f