【PHP7源碼學習】2019-03-07 PHP內存管理1筆記

baiyanphp

所有視頻:https://segmentfault.com/a/11...segmentfault

源視頻地址:http://replay.xesv5.com/ll/24...數組

malloc和free函數思考

  • 字符串的非二進制安全:若是存的字符串內容中帶有\0,那麼它會被當作字符串結束標誌,提早斷定字符串結束,而並不是到字符串末尾正常結束。
  • malloc一塊內存以後,它的size是存在哪裏的?
  • free一塊內存,如何知道free多大的內存?
  • 解決方案:在分配的內存地址空間附近額外分配一塊內存記錄size。

  • malloc()系統調用是很是慢的,至關於你去麪包店買一個麪包,須要和麪、發酵...等等一系列繁瑣的工做,在操做系統層面會引發用戶態和內核態的切換,耗時很長,用戶是不能接受的,因此PHP實現了本身的一套內存管理機制,避免頻繁的進行系統調用。

操做系統內存基本概念

  • chunk:2MB大小內存
  • page:4KB大小內存
  • 兩者關係:一個chunk由512個page組成(2MB/4KB = 512)(512 = 2^9,4KB = 2^12,2MB = 2^21)。
  • mmap():系統調用,將一個文件或者其它對象映射進內存,分配chunk。
  • 內存不夠用怎麼辦:選擇一個佔用內存大的進程並掛起,並將其使用的內存置換到硬盤上,分配給新進程使用。當掛起進程被喚醒的時候,喚醒進程會到內存中找對應的頁,可是沒法找到,故產生缺頁中斷,操做系統會將硬盤中的頁置換回內存,供喚醒進程繼續運行。

PHP內存分配

PHP內存分類:small/large/huge

  • small:size <= 3KB(有30種規格)
  • large:3KB < size <= 2MB - 4KB
  • huge:size >= 2MB - 4KB
  • 思考:爲何large規格內存是 <= 2MB - 4KB?

答:在small和large內存分配規格中,首先會分配一個chunk,chunk的第一個page中存有一個結構體,用來保存chunk中每個page的相關存儲信息(即zend_alloc.h中的mm_heap結構體)。可是huge規格不須要,直接拿來用便可(由於huge內存已經大於2MB(即一個chunk)),無需記錄內部存儲細節信息)。思考一個問題:一個page有4KB,至於用這麼大的空間去存嗎?安全

struct _zend_mm_heap {
#if ZEND_MM_CUSTOM
    int                use_custom_heap;
#endif
#if ZEND_MM_STORAGE
    zend_mm_storage   *storage;
#endif
#if ZEND_MM_STAT
    size_t             size;                  /* 當前使用內存(PHP庫函數memory_get_usage()方法就是取此字段的值)*/
    size_t             peak;                /*  內存使用峯值 */
#endif
    zend_mm_free_slot *free_slot[ZEND_MM_BINS];  /*它是一個大小爲ZEND_MM_BINS = 30的數組,每一個存儲單元是一個單向鏈表,用來掛載30種不一樣規格的small內存(下面會講)*/
#if ZEND_MM_STAT || ZEND_MM_LIMIT
    size_t             real_size;               /* current size of allocated pages */
#endif
#if ZEND_MM_STAT
    size_t             real_peak;               /* peak size of allocated pages */
#endif
#if ZEND_MM_LIMIT
    size_t             limit;                   /* memory limit */
    int                overflow;                /* memory overflow flag */
#endif

    zend_mm_huge_list *huge_list;               /* list of huge allocated blocks */

    zend_mm_chunk     *main_chunk;
    zend_mm_chunk     *cached_chunks;            /* list of unused chunks */
    int                chunks_count;            /* number of alocated chunks */
    int                peak_chunks_count;        /* peak number of allocated chunks for current request */
    int                cached_chunks_count;        /* number of cached chunks */
    double             avg_chunks_count;        /* average number of chunks allocated per request */
    int                last_chunks_delete_boundary; /* numer of chunks after last deletion */
    int                last_chunks_delete_count;    /* number of deletion over the last boundary */
#if ZEND_MM_CUSTOM
    union {
        struct {
            void      *(*_malloc)(size_t);
            void       (*_free)(void*);
            void      *(*_realloc)(void*, size_t);
        } std;
        struct {
            void      *(*_malloc)(size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
            void       (*_free)(void*  ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
            void      *(*_realloc)(void*, size_t  ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
        } debug;
    } custom_heap;
#endif
};
  • 重點關注中文註釋的部分,咱們看到zend_mm_heap結構體並非很大,那麼繼續關注其中重點的zend_mm_chunk結構體:
struct _zend_mm_chunk {
    zend_mm_heap      *heap;
    zend_mm_chunk     *next;
    zend_mm_chunk     *prev;
    uint32_t           free_pages;                /* number of free pages */
    uint32_t           free_tail;               /* number of free pages at the end of chunk */
    uint32_t           num;
    char               reserve[64 - (sizeof(void*) * 3 + sizeof(uint32_t) * 3)];
    zend_mm_heap       heap_slot;               /* used only in main chunk */
    zend_mm_page_map   free_map;                 /* 512 bits or 64 bytes */
    zend_mm_page_info  map[ZEND_MM_PAGES];      /* 2 KB = 512 * 4 */
};
  • 1個chunk有512個page。
  • 第一個heap字段是zend_mm_heap類型的結構體指針,方便在512個page中直接找到第一個存儲額外信息的page。下面兩個next和prev字段構成一個雙向鏈表,用於chunk與chunk之間的鏈接。
  • 重點關注倒數第二個字段zend_mm_page_map類型的字段,有512bit(即64B),每個bit用來存儲當前這個chunk中的每個頁(由於一共有512個頁)是否使用。
  • 除了看它是否使用,還要看每一個page的具體使用的大小和其餘相關狀況並記錄相關信息。
  • 最後一個zend_mm_page_info類型的字段解決了上述問題。ZEND_MM_PAGES = (ZEND_MM_CHUNK_SIZE / ZEND_MM_PAGE_SIZE) = 2MB/4KB = 512個存儲單元,每一個存儲單元是typedef uint32_t zend_mm_page_info類型,即4B大小用來存儲額外的使用狀況信息,這個字段一共是2KB。用來存儲使用的大小和額外信息,因此這裏2KB加上剩餘字段,差很少會使用4KB的空間,因此chunk的第一個page須要留出來記錄這些信息。
  • 那麼這個page_info(uint32_t),這裏32位是如何利用的呢,先看如何標記內存規格
#define ZEND_MM_IS_FRUN                  0x00000000
#define ZEND_MM_IS_LRUN                  0x40000000
#define ZEND_MM_IS_SRUN                  0x80000000
  • 第32位爲1表明small內存,即0x8。而後低5位(2^5 > 30種規格)存儲bit_num(即ZEND_MM_BINS_INFO宏的索引,以便快速找到所需頁的數量等信息(下面會講))
  • 第31位爲1表明large內存,即0x4。而後低10位表示分配的頁數(>= 512便可)爲何不用9位呢?2^9 = 512足夠了???
  • 第3一、32位都是1,即0xC,低5位表示bit_num,這裏3KB規格就是29,16~25bit表示偏移量。以申請3KB爲例,必定是3個4KB的page,分爲4個3KB(見下文)。第一個3KB就是0xC做爲連續分配4個3KB的起始標記(約定),偏移量是0,bit_num是29;第二個3KB就是0x8,偏移量是1,bit_num一樣是29;第三、4個3KB內存同理,只是偏移量不一樣。
  • _emalloc() -> zend_mm_alloc_heap() ->分配三種內存規格的其中一種:
static zend_always_inline void *zend_mm_alloc_heap(zend_mm_heap *heap, size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
    void *ptr;
    if (size <= ZEND_MM_MAX_SMALL_SIZE) {
        ptr = zend_mm_alloc_small(heap, size, ZEND_MM_SMALL_SIZE_TO_BIN(size) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
        return ptr;
    } else if (size <= ZEND_MM_MAX_LARGE_SIZE) {
        ptr = zend_mm_alloc_large(heap, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
        return ptr;
    } else {
        return zend_mm_alloc_huge(heap, size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
    }
}

small內存

  • 注意一個page能夠包含多個small內存塊,首先知足一次性分配的page總量必須可以除盡分配的規格大小。換句話說,儘可能不要有內存碎片。例如:分配3KB的small內存,不能只分配1個page,由於會剩下1KB大小的內存碎片,不可以再次利用並分配。因此須要求得一個規格和4KB的最小公倍數,做爲總的分配空間。可是若是最小公倍數過大,致使所須要的page太多,那麼退而求其次,即便留有不多的內存碎片,也算是能夠知足需求的。下面舉幾個例子:
  • 例1:假如分配8B規格的small內存:咱們先找最小的能夠除盡8B的page大小。由於4KB能夠除盡8B,因此咱們取1個page就夠了,故一共正好能夠分紅512個8B的內存。咱們取1個8B內存返回給用戶,而後剩餘511個8B內存會掛在zend_mm_heap結構體的zend_mm_free_slot *free_slot[ZEND_MM_BINS]字段中。這裏ZEND_MM_BINS宏 = 30,剛好這30個數組單元對應30種small內存規格,共有30個單向鏈表,每個單向鏈表都是掛載的多分配的內存。如上例中的8B內存,就會掛載剩下多分配的511個8B內存。當下次再次有分配請求的時候,直接從鏈表上拿便可,不用再去從新分配。同時也避免了內存碎片的產生。當釋放內存的時候,直接利用頭插法(相比尾插法複雜度O(1))插入到鏈表頭部便可。
  • small內存最小的分配規格就是8B,由於指針在64位系統下是8B,可以知足free_slot字段單向鏈表的需求。

  • 例2:假如分配3KB規格的small內存:咱們一樣先找能夠整除3KB的page大小。由於4KB不能夠除盡3KB,因此1個page雖然能夠知足3KB的分配需求,可是仍是會留有1KB的內存碎片,因此咱們不能直接拿1個page來用。咱們取最小的能夠整除3KB的4KB倍數,求得該值爲3(4KB * 3 / 3KB = 4)個3KB大小的內存塊。那麼一樣咱們取1個3KB內存返回給用戶,剩餘3個3KB內存掛在上面的zend_mm_heap結構體中free_slot字段的鏈表上,等待後續的分配請求。

  • 例3:假如分配40B規格的small內存,爲何下面代碼也僅僅用了1個page,那是由於4KB和40B的最小公倍數過大,致使一次性申請的page太多,因此不能一味的找兩者的最小公倍數,因此退而求其次,只分配1個page。雖然4096/40 = 102.5,代碼中直接取了102做爲最終分配的數量。第1個返回給用戶,剩下101個掛在free_slot鏈表上等待後續的分配請求。函數

    • 下面是全部的small內存分配規格:
/* bin_num, size, count, pages */
/*數組下標,分配規格大小,共能分配多少個這樣規格大小的內存塊,須要的頁的數量*/
#define  ZEND_MM_BINS_INFO(_, x, y) \
    _( 0,    8,  512, 1, x, y) \
    _( 1,   16,  256, 1, x, y) \
    _( 2,   24,  170, 1, x, y) \
    _( 3,   32,  128, 1, x, y) \
    _( 4,   40,  102, 1, x, y) \
    _( 5,   48,   85, 1, x, y) \
    _( 6,   56,   73, 1, x, y) \
    _( 7,   64,   64, 1, x, y) \
    _( 8,   80,   51, 1, x, y) \
    _( 9,   96,   42, 1, x, y) \
    _(10,  112,   36, 1, x, y) \
    _(11,  128,   32, 1, x, y) \
    _(12,  160,   25, 1, x, y) \
    _(13,  192,   21, 1, x, y) \
    _(14,  224,   18, 1, x, y) \
    _(15,  256,   16, 1, x, y) \
    _(16,  320,   64, 5, x, y) \
    _(17,  384,   32, 3, x, y) \
    _(18,  448,    9, 1, x, y) \
    _(19,  512,    8, 1, x, y) \
    _(20,  640,   32, 5, x, y) \
    _(21,  768,   16, 3, x, y) \
    _(22,  896,    9, 2, x, y) \
    _(23, 1024,    8, 2, x, y) \
    _(24, 1280,   16, 5, x, y) \
    _(25, 1536,    8, 3, x, y) \
    _(26, 1792,   16, 7, x, y) \
    _(27, 2048,    8, 4, x, y) \
    _(28, 2560,    8, 5, x, y) \
    _(29, 3072,    4, 3, x, y)
  • 思考:給一個size,如何肯定它的bin_num(也就是索引),從而快速找到count和pages?
static zend_always_inline int zend_mm_small_size_to_bin(size_t size)
{
...
    unsigned int t1, t2;

    if (size <= 64) {
        /* we need to support size == 0 ... */
        return (size - !!size) >> 3;
    } else {
        t1 = size - 1;
        t2 = zend_mm_small_size_to_bit(t1) - 3;
        t1 = t1 >> t2;
        t2 = t2 - 3;
        t2 = t2 << 2;
        return (int)(t1 + t2);
    }
#endif
}
  • 當size <= 64的時候,size會遞增8,那麼直接>>3位(除以8)就能夠獲得索引的偏移量,很好理解。這裏的!!size是爲了兼容size = 0的狀況,這樣在右移3位以前就不會出現負值;減去!!size(即減1)是爲了兼容size爲8B規格等邊界狀況。
  • 當size > 64的時候,詳情參考:small內存規格的計算 核心思想:能夠將size當作等差數列,按照公差分組。首先找出當前size屬於哪一個組,在組內的偏移量是多少,最終把兩者相加。
  • small內存分配流程:
static zend_always_inline void *zend_mm_alloc_small(zend_mm_heap *heap, size_t size, int bin_num ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
...
    if (EXPECTED(heap->free_slot[bin_num] != NULL)) {
        zend_mm_free_slot *p = heap->free_slot[bin_num];
        heap->free_slot[bin_num] = p->next_free_slot;
        return (void*)p;
    } else {
        return zend_mm_alloc_small_slow(heap, bin_num ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
    }
}
  • 若是free_slot上掛載的多餘分配的內存不是空,那麼直接從free_slot鏈表的第一個位置上拿就能夠了,而後鏈指針向後移,等待下次內存分配。不然調用zend_mm_alloc_small_slow函數,注意這個slow就表明從新申請chunk、分配page等等,這裏暫不展開。

內存對齊

  • 內存對齊:所申請的內存返回的起始地址都是邏輯地址(下面會講),且該地址必定是2MB的整數倍,這就是內存對齊。在PHP中,能夠斷定必定是申請的huge規格的內存。由於small和large內存的第一個page存放了zend_mm_heap結構體,確定不是2MB地址的整數倍。這樣能夠快速計算出當前地址屬於第幾個chunk。好比地址是4096,就屬於第3個chunk。
  • 因此內存對齊的優勢就是若是對齊了,就能夠直接判斷是huge規格,不須要額外存儲用於之後判斷的字段。
  • x64體系結構的計算機的地址共有64位,任何一個內存地址均可以分爲首地址 + 偏移量,如1019地址(10進制) = 1000(首地址) + 19 (偏移量);從二進制角度來看,2MB = 10 0000 0000 0000 0000 0000,低21位所有爲0(偏移量爲0),高43位爲首地址(第22位爲1,高64-22 = 42位所有爲0)就構成了64位的邏輯地址。這樣能夠方便地進行邏輯地址與物理地址的轉換。
  • 向操做系統申請內存都是以頁爲單位,因此申請的內存都是4KB的整數倍,不會出現4K+1這種邏輯地址。
  • 思考:PHP向操做系統申請內存是以chunk爲單位,可是操做系統只返回4KB整數倍的內存起始地址,並非2MB內存的起始地址,一個chunk中,分配到2MB整數倍起始地址是1/512機率,如何讓操做系統申請的地址直接是2MB的整數倍呢?
  • 答案:多申請一部分存儲空間,掐頭去尾,保證起始地址2MB對齊。

gdb調試相關

  • gdb須要指定一個可執行的二進制程序來進行調試,相關命令用法google便可,經常使用r/b/n/s/p
  • 當利用gdb b main的時候,爲何php沒有main()入口函數卻仍能夠正常執行上述gdb命令?由於虛擬機會默認幫助你加一個main()和return
  • 編譯器優化:將inline函數直接注入到函數裏面去,把宏進行替換等等,因此編譯的時候爲方便調試須要禁用編譯器優化
相關文章
相關標籤/搜索