天天看點

四十五、PHP核心探索:PHP的記憶體管理 ☞ 在ZEND核心中以宏的形式作為接口提供

記憶體管理一般會包括以下内容:

  • 是否有足夠的記憶體供我們的程式使用;
  • 如何從足夠可用的記憶體中擷取部分記憶體;
  • 對于使用後的記憶體,是否可以将其銷毀并将其重新配置設定給其它程式使用。

與此對應,PHP的内容管理也包含這樣的内容,隻是這些内容在ZEND核心中是以宏的形式作為接口提供給外部使用。 後面兩個操作分别對應emalloc宏,efree宏,而第一個操作可以根據emalloc宏傳回結果檢測。

PHP的記憶體管理可以被看作是分層(hierarchical)的。 它分為三層:存儲層(storage)、堆層(heap)和接口層(emalloc/efree)。 存儲層通過 malloc()、mmap() 等函數向系統真正的申請記憶體,并通過 free() 函數釋放所申請的記憶體。 存儲層通常申請的記憶體塊都比較大,這裡申請的記憶體大并不是指storage層結構所需要的記憶體大, 隻是堆層通過調用存儲層的配置設定方法時,其以大塊大塊的方式申請的記憶體,存儲層的作用是将記憶體配置設定的方式對堆層透明化。 如下圖所示,PHP記憶體管理器。PHP在存儲層共有4種記憶體配置設定方案: malloc,win32,mmap_anon,mmap_zero, 預設使用malloc配置設定記憶體,如果設定了ZEND_WIN32宏,則為windows版本,調用HeapAlloc配置設定記憶體, 剩下兩種記憶體方案為匿名記憶體映射,并且PHP的記憶體方案可以通過設定環境變量來修改。

​​

四十五、PHP核心探索:PHP的記憶體管理 ☞ 在ZEND核心中以宏的形式作為接口提供

​​

PHP記憶體管理器

首先我們看下接口層的實作,接口層是一些宏定義,如下:

/* Standard wrapper macros */
#define emalloc(size)                       _emalloc((size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC)
#define safe_emalloc(nmemb, size, offset)   _safe_emalloc((nmemb), (size), (offset) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC)
#define efree(ptr)                          _efree((ptr) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC)
#define ecalloc(nmemb, size)                _ecalloc((nmemb), (size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC)
#define erealloc(ptr, size)                 _erealloc((ptr), (size), 0 ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC)
#define safe_erealloc(ptr, nmemb, size, offset) _safe_erealloc((ptr), (nmemb), (size), (offset) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC)
#define erealloc_recoverable(ptr, size)     _erealloc((ptr), (size), 1 ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC)
#define estrdup(s)                          _estrdup((s) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC)
#define estrndup(s, length)                 _estrndup((s), (length) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC)
#define zend_mem_block_size(ptr)            _zend_mem_block_size((ptr) TSRMLS_CC ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC)      

這裡為什麼沒有直接調用函數?因為這些宏相當于一個接口層或中間層,定義了一個高層次的接口,使得調用更加容易它隔離了外部調用和PHP記憶體管理的内部實作,實作了一種松耦合關系。雖然PHP不限制這些函數的使用, 但是官方文檔還是建議使用這些宏。這裡的接口層有點門面模式(facade模式)的味道。

在接口層下面是PHP記憶體管理的核心實作,我們稱之為heap層。 這個層控制整個PHP記憶體管理的過程,首先我們看這個層的結構:

/* mm block type */
typedef struct _zend_mm_block_info {
    size_t _size;   /* block的大小*/
    size_t _prev;   /* 計算前一個塊有用到*/
} zend_mm_block_info;
 
 
typedef struct _zend_mm_block {
    zend_mm_block_info info;
} zend_mm_block;
 
typedef struct _zend_mm_small_free_block {  /* 雙向連結清單 */
    zend_mm_block_info info;
    struct _zend_mm_free_block *prev_free_block;    /* 前一個塊 */
    struct _zend_mm_free_block *next_free_block;    /* 後一個塊 */
} zend_mm_small_free_block; /* 小的空閑塊*/
 
typedef struct _zend_mm_free_block {    /* 雙向連結清單 + 樹結構 */
    zend_mm_block_info info;
    struct _zend_mm_free_block *prev_free_block;    /* 前一個塊 */
    struct _zend_mm_free_block *next_free_block;    /* 後一個塊 */
 
    struct _zend_mm_free_block **parent;    /* 父結點 */
    struct _zend_mm_free_block *child[2];   /* 兩個子結點*/
} zend_mm_free_block;
 
 
 
struct _zend_mm_heap {
    int                 use_zend_alloc; /* 是否使用zend記憶體管理器 */
    void               *(*_malloc)(size_t); /* 記憶體配置設定函數*/
    void                (*_free)(void*);    /* 記憶體釋放函數*/
    void               *(*_realloc)(void*, size_t);
    size_t              free_bitmap;    /* 小塊空閑記憶體辨別 */
    size_t              large_free_bitmap;  /* 大塊空閑記憶體辨別*/
    size_t              block_size;     /* 一次記憶體配置設定的段大小,即ZEND_MM_SEG_SIZE指定的大小,預設為ZEND_MM_SEG_SIZE   (256 * 1024)*/
    size_t              compact_size;   /* 壓縮操作邊界值,為ZEND_MM_COMPACT指定大小,預設為 2 * 1024 * 1024*/
    zend_mm_segment    *segments_list;  /* 段指針清單 */
    zend_mm_storage    *storage;    /* 所調用的存儲層 */
    size_t              real_size;  /* 堆的真實大小 */
    size_t              real_peak;  /* 堆真實大小的峰值 */
    size_t              limit;  /* 堆的記憶體邊界 */
    size_t              size;   /* 堆大小 */
    size_t              peak;   /* 堆大小的峰值*/
    size_t              reserve_size;   /* 備用堆大小*/
    void               *reserve;    /* 備用堆 */
    int                 overflow;   /* 記憶體溢出數*/
    int                 internal;
#if ZEND_MM_CACHE
    unsigned int        cached; /* 已緩存大小 */
    zend_mm_free_block *cache[ZEND_MM_NUM_BUCKETS]; /* 緩存數組/
#endif
    zend_mm_free_block *free_buckets[ZEND_MM_NUM_BUCKETS*2];    /* 小塊記憶體數組,相當索引的角色 */
    zend_mm_free_block *large_free_buckets[ZEND_MM_NUM_BUCKETS];    /* 大塊記憶體數組,相當索引的角色 */
    zend_mm_free_block *rest_buckets[2];    /* 剩餘記憶體數組*/
 
};      

當初始化記憶體管理時,調用函數是zend_mm_startup。它會初始化storage層的配置設定方案, 初始化段大小,壓縮邊界值,并調用zend_mm_startup_ex()初始化堆層。 這裡的配置設定方案就是圖6.1所示的四種方案,它對應的環境變量名為:ZEND_MM_MEM_TYPE。 這裡的初始化的段大小可以通過ZEND_MM_SEG_SIZE設定,如果沒設定這個環境變量,程式中預設為256 * 1024。 這個值存儲在_zend_mm_heap結構的block_size字段中,将來在維護的三個清單中都沒有可用的記憶體中,會參考這個值的大小來申請記憶體的大小。

PHP中的記憶體管理主要工作就是維護三個清單:小塊記憶體清單(free_buckets)、 大塊記憶體清單(large_free_buckets)和剩餘記憶體清單(rest_buckets)。 看到bucket這個單詞是不是很熟悉?在前面我們介紹HashTable時,這就是一個重要的角色,它作為HashTable中的一個單元角色。 在這裡,每個bucket也對應一定大小的記憶體塊清單,這樣的清單都包含雙向連結清單的實作。

我們可以把維護的前面兩個表看作是兩個HashTable,那麼,每個HashTable都會有自己的hash函數。 首先我們來看free_buckets清單,這個清單用來存儲小塊的記憶體配置設定,其hash函數為:

#define ZEND_MM_BUCKET_INDEX(true_size) ((true_size>>ZEND_MM_ALIGNMENT_LOG2)-(ZEND_MM_ALIGNED_MIN_HEADER_SIZE>>ZEND_MM_ALIGNMENT_LOG2))

假設ZEND_MM_ALIGNMENT為8(如果沒有特殊說明,本章的ZEND_MM_ALIGNMENT的值都為8),則ZEND_MM_ALIGNED_MIN_HEADER_SIZE=16, 若此時true_size=256,則((256>>3)-(16>>3))= 30。 當ZEND_MM_BUCKET_INDEX宏出現時,ZEND_MM_SMALL_SIZE宏一般也會同時出現, ZEND_MM_SMALL_SIZE宏的作用是判斷所申請的記憶體大小是否為小塊的記憶體, 在上面的示例中,小于272Byte的記憶體為小塊記憶體,則index最多隻能為31, 這樣就保證了free_buckets不會出現數組溢出的情況。

在記憶體管理初始化時,PHP核心對初始化free_buckets清單。 從heap的定義我們可知free_buckets是一個數組指針,其存儲的本質是指向zend_mm_free_block結構體的指針。 開始時這些指針都沒有指向具體的元素,隻是一個簡單的指針空間。 free_buckets清單在實際使用過程中隻存儲指針,這些指針以兩個為一對(即數組從0開始,兩個為一對),分别存儲一個個雙向連結清單的頭尾指針。 其結構如下圖所示。

​​

四十五、PHP核心探索:PHP的記憶體管理 ☞ 在ZEND核心中以宏的形式作為接口提供

​​

對于free_buckets清單位置的擷取,關鍵在于ZEND_MM_SMALL_FREE_BUCKET宏,宏代碼如下:

#define ZEND_MM_SMALL_FREE_BUCKET(heap, index) \

(zend_mm_free_block*) ((char*)&heap->free_buckets[index * 2] + \

sizeof(zend_mm_free_block*) * 2 - \

sizeof(zend_mm_small_free_block))

仔細看這個宏實作,發現在它的計算過程是取free_buckets清單的偶數位的記憶體位址加上 兩個指針的記憶體大小并減去zend_mm_small_free_block結構所占空間的大小。 而zend_mm_free_block結構和zend_mm_small_free_block結構的差距在于兩個指針。 據此計算過程可知,ZEND_MM_SMALL_FREE_BUCKET宏會擷取free_buckets清單 index對應雙向連結清單的第一個zend_mm_free_block的prev_free_block指向的位置。 free_buckets的計算僅僅與prev_free_block指針和next_free_block指針相關, 是以free_buckets清單也僅僅需要存儲這兩個指針。

那麼,這個數組在最開始是怎樣的呢? 在初始化函數zend_mm_init中free_buckets與large_free_buckts清單一起被初始化。 如下代碼:

p = ZEND_MM_SMALL_FREE_BUCKET(heap, 0);

for (i = 0; i < ZEND_MM_NUM_BUCKETS; i++) {

p->next_free_block = p;

p->prev_free_block = p;

p = (zend_mm_free_block*)((char*)p + sizeof(zend_mm_free_block*) * 2);

heap->large_free_buckets[i] = NULL;

}

對于free_buckets清單來說,在循環中,偶數位的元素(索引從0開始)将其next_free_block和prev_free_block都指向自己, 以i=0為例,free_buckets的第一個元素(free_buckets[0])存儲的是第二個元素(free_buckets[1])的位址, 第二個元素存儲的是第一個元素的位址。 此時将可能會想一個問題,在整個free_buckets清單沒有内容時,ZEND_MM_SMALL_FREE_BUCKET在擷取第一個zend_mm_free_block時, 此zend_mm_free_block的next_free_block元素和prev_free_block元素卻分别指向free_buckets[0]和free_buckets[1]。

在整個循環初始化過程中都沒有free_buckets數組的下标操作,它的移動是通過位址操作,以加兩個sizeof(zend_mm_free_block*)實作, 這裡的sizeof(zend_mm_free_block*)是擷取指針的大小。比如現在是在下标為0的元素的位置, 加上兩個指針的值後,指針會指向下标為2的位址空間,進而實作數組元素的向後移動, 也就是zend_mm_free_block->next_free_block和zend_mm_free_block->prev_free_block位置的後移。 這種不存儲zend_mm_free_block數組,僅存儲其指針的方式不可不說精妙。雖然在了解上有一些困難,但是節省了記憶體。

free_buckets清單使用free_bitmap标記是否該雙向連結清單已經使用過時有用。 當有新的元素需要插入到清單時,需要先根據塊的大小查找index, 查找到index後,在此index對應的雙向連結清單的頭部插入新的元素。

free_buckets清單的作用是存儲小塊記憶體,而與之對應的large_free_buckets清單的作用是存儲大塊的記憶體, 雖然large_free_buckets清單也類似于一個hash表,但是這個與前面的free_buckets清單一些差別。 它是一個內建了數組,樹型結構和雙向連結清單三種資料結構的混合體。 我們先看其數組結構,數組是一個hash映射,其hash函數為:

#define ZEND_MM_LARGE_BUCKET_INDEX(S) zend_mm_high_bit(S)
 
 
static inline unsigned int zend_mm_high_bit(size_t _size)
{
 
..//省略若幹不同環境的實作
    unsigned int n = 0;
    while (_size != 0) {
        _size = _size >> 1;
        n++;
    }
    return n-1;
}      

這個hash函數用來計算size的位數,傳回值為size二進碼中1的個數-1。 假設此時size為512Byte,則這段記憶體會放在large_free_buckets清單, 512的二進制碼為1000000000,其中僅包含一個1,則其對應的清單index為0。 關于右移操作,這裡有一點說明:

一般來說,右移分為邏輯右移和算術右移。邏輯位移在在左端補K個0,算術右移在左端補K個最高有效位的值。 C語言标準沒有明确定義應該使用哪種方式。對于無符号資料,右移必須是邏輯的。對于有符号的資料,則二者都可以。 但是,現實中都會預設為算術右移。

我們通過一次清單的元素插入操作來了解清單的結果。 首先确定目前需要記憶體所在的數組元素位置,然後查找此記憶體大小所在的位置。 這個查找行為是發生在樹型結構中,而樹型結構的位置與記憶體的大小有關。 其查找過程如下:

  • 第一步 通過索引擷取樹型結構第一個結點并作為目前結點,如果第一個結點為空,則将記憶體放到第一個元素的結點位置,傳回,否則轉第二步
  • 第二步 從目前結點出發,查找下一個結點,并将其作為目前結點
  • 第三步 判斷目前結點記憶體的大小與需要配置設定的記憶體大小是否一樣 如果大小一樣則以雙向連結清單的結構将新的元素添加到結點元素的後面第一個元素的位置。否則轉四步
  • 第四步 判斷目前結點是否為空,如果為空,則占據結點位置,結束查找,否則第二步。

從以上的過程我們可以畫出large_free_buckets清單的結構如下圖所示:

​​

四十五、PHP核心探索:PHP的記憶體管理 ☞ 在ZEND核心中以宏的形式作為接口提供

​​

large_free_buckets清單結構

從記憶體配置設定的過程中可以看出,記憶體塊查找判斷順序依次是小塊記憶體清單,大塊記憶體清單,剩餘記憶體清單。 在heap結構中,剩餘記憶體清單對應rest_buckets字段,這是一個包含兩個元素的數組, 并且也是一個雙向連結清單隊列,其中rest_buckets[0]為隊列的頭,rest_buckets[1]為隊列的尾。 而我們常用的插入和查找操作是針對第一個元素,即heap->rest_buckets[0], 當然,這是一個雙向連結清單隊列,隊列的頭和尾并沒有很明顯的差別。它們僅僅是作為一種認知上的區分。 在添加記憶體時,如果所需要的記憶體塊的大小大于初始化時設定的ZEND_MM_SEG_SIZE的值(在heap結構中為block_size字段) 與ZEND_MM_ALIGNED_SEGMENT_SIZE(等于8)和ZEND_MM_ALIGNED_HEADER_SIZE(等于8)的和的差,則會将新生成的塊插入 rest_buckts所在的雙向連結清單中,這個操作和前面的雙向連結清單操作一樣,都是從”隊列頭“插入新的元素。 此清單的結構和free_bucket類似,隻是這個清單所在的數組沒有那麼多元素,也沒有相應的hash函數。

在heap層下面是存儲層,存儲層的作用是将記憶體配置設定的方式對堆層透明化,實作存儲層和heap層的分離。 在PHP的源碼中有注釋顯示相關代碼為"Storage Manager"。 存儲層的主要結構代碼如下:

/* Heaps with user defined storage */
typedef struct _zend_mm_storage zend_mm_storage;
 
typedef struct _zend_mm_segment {
    size_t    size;
    struct _zend_mm_segment *next_segment;
} zend_mm_segment;
 
typedef struct _zend_mm_mem_handlers {
    const char *name;
    zend_mm_storage* (*init)(void *params);    //    初始化函數
    void (*dtor)(zend_mm_storage *storage);    //    析構函數
    void (*compact)(zend_mm_storage *storage);
    zend_mm_segment* (*_alloc)(zend_mm_storage *storage, size_t size);    //    記憶體配置設定函數
    zend_mm_segment* (*_realloc)(zend_mm_storage *storage, zend_mm_segment *ptr, size_t size);    //    重新配置設定記憶體函數
    void (*_free)(zend_mm_storage *storage, zend_mm_segment *ptr);    //    釋放記憶體函數
} zend_mm_mem_handlers;
 
struct _zend_mm_storage {
    const zend_mm_mem_handlers *handlers;    //    處理函數集
    void *data;
};      

以上代碼的關鍵在于存儲層處理函數的結構體,對于不同的記憶體配置設定方案,所不同的就是記憶體配置設定的處理函數。 其中以name字段辨別不同的配置設定方案。在圖6.1中,我們可以看到PHP在存儲層共有4種記憶體配置設定方案: malloc,win32,mmap_anon,mmap_zero預設使用malloc配置設定記憶體, 如果設定了ZEND_WIN32宏,則為windows版本,調用HeapAlloc配置設定記憶體,剩下兩種記憶體方案為匿名記憶體映射, 并且PHP的記憶體方案可以通過設定變量來修改。其官方說明如下:

The Zend MM can be tweaked using ZEND_MM_MEM_TYPE and ZEND_MM_SEG_SIZE environment

variables. Default values are “malloc” and “256K”. Dependent on target system you

can also use “mmap_anon”, “mmap_zero” and “win32″ storage managers.

在代碼中,對于這4種記憶體配置設定方案,分别對應實作了zend_mm_mem_handlers中的各個處理函數。 配合代碼的簡單說明如下:

/* 使用mmap記憶體映射函數配置設定記憶體 寫入時拷貝的私有映射,并且匿名映射,映射區不與任何檔案關聯。*/
# define ZEND_MM_MEM_MMAP_ANON_DSC {"mmap_anon", zend_mm_mem_dummy_init, zend_mm_mem_dummy_dtor, zend_mm_mem_dummy_compact, zend_mm_mem_mmap_anon_alloc, zend_mm_mem_mmap_realloc, zend_mm_mem_mmap_free}
 
/* 使用mmap記憶體映射函數配置設定記憶體 寫入時拷貝的私有映射,并且映射到/dev/zero。*/
# define ZEND_MM_MEM_MMAP_ZERO_DSC {"mmap_zero", zend_mm_mem_mmap_zero_init, zend_mm_mem_mmap_zero_dtor, zend_mm_mem_dummy_compact, zend_mm_mem_mmap_zero_alloc, zend_mm_mem_mmap_realloc, zend_mm_mem_mmap_free}
 
/* 使用HeapAlloc配置設定記憶體 windows版本 關于這點,注釋中寫的是VirtualAlloc() to allocate memory,實際在程式中使用的是HeapAlloc*/
# define ZEND_MM_MEM_WIN32_DSC {"win32", zend_mm_mem_win32_init, zend_mm_mem_win32_dtor, zend_mm_mem_win32_compact, zend_mm_mem_win32_alloc, zend_mm_mem_win32_realloc, zend_mm_mem_win32_free}
 
/* 使用malloc配置設定記憶體 預設為此種配置設定 如果有加ZEND_WIN32宏,則使用win32的配置設定方案*/
# define ZEND_MM_MEM_MALLOC_DSC {"malloc", zend_mm_mem_dummy_init, zend_mm_mem_dummy_dtor, zend_mm_mem_dummy_compact, zend_mm_mem_malloc_alloc, zend_mm_mem_malloc_realloc, zend_mm_mem_malloc_free}
 
static const zend_mm_mem_handlers mem_handlers[] = {
#ifdef HAVE_MEM_WIN32
    ZEND_MM_MEM_WIN32_DSC,
#endif
#ifdef HAVE_MEM_MALLOC
    ZEND_MM_MEM_MALLOC_DSC,
#endif
#ifdef HAVE_MEM_MMAP_ANON
    ZEND_MM_MEM_MMAP_ANON_DSC,
#endif
#ifdef HAVE_MEM_MMAP_ZERO
    ZEND_MM_MEM_MMAP_ZERO_DSC,
#endif
    {NULL, NULL, NULL, NULL, NULL, NULL}
};