【PHP7源碼學習】2019-03-18 複習前面的內容

baiyanphp

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

原視頻地址:http://replay.xesv5.com/ll/24...算法

本筆記中部分圖片截自視頻中的片斷,圖片版權歸視頻原做者全部。express

malloc函數深刻

  • PHP內存管理1筆記中提到,malloc()函數會在分配的內存空間前面額外分配32位,用來存儲分配的大小和幾個標誌位,如圖:

  • 那麼到底是否是這樣的呢?咱們寫一段測試代碼驗證一下:
#include <stdlib.h>
int main() {
    void *ptr = malloc(8);
    return 1;
}
  • 利用gdb調試這段代碼:

  • 首先打印ptr的地址,爲0x602010,利用x命令日後看20個內存單元(1個內存單元 = 4個字節),故一共展現了80個字節,後面的x是以16進制打印內容。
  • 咱們發現緊鄰0x602010地址的上面32位均是0,沒有任何內容,不符合咱們的預期。
  • 上圖只是一個最簡單的思路,但絕大多數操做系統是按照以下的方式實現的:
操做系統中有一個記錄空閒內存地址的鏈表。當操做系統收到程序的申請時,就會遍歷該鏈表,而後就尋找第一個空間大於所申請空間的堆結點,而後就將該結點從空閒結點鏈表中刪除,並將該結點的空間分配給程序。malloc函數的實質體如今,它有一個將可用的內存塊鏈接爲一個長長的列表的所謂空閒鏈表(Free List)。調用malloc函數時,它沿鏈接表尋找一個大到足以知足用戶請求所須要的內存塊(根據不一樣的算法而定(將最早找到的不小於申請的大小內存塊分配給請求者,將最合適申請大小的空閒內存分配給請求者,或者是分配最大的空閒塊內存塊)。而後,將該內存塊一分爲二(一塊的大小與用戶請求的大小相等,另外一塊的大小就是剩下的字節)。接下來,將分配給用戶的那塊內存傳給用戶,並將剩下的那塊(若是有的話)返回到鏈接表上。調用free函數時,它將用戶釋放的內存塊鏈接到空閒鏈上。到最後,空閒鏈會被切成不少的小內存片斷,若是這時用戶申請一個大的內存片斷,那麼空閒鏈上可能沒有能夠知足用戶要求的片斷了。因而,malloc函數請求延時,並開始在空閒鏈上翻箱倒櫃地檢查各內存片斷,對它們進行整理,將相鄰的小空閒塊合併成較大的內存塊。若是沒法得到符合要求的內存塊,malloc函數會返回NULL指針,所以在調用malloc動態申請內存塊時,必定要進行返回值的判斷。

結構體與聯合體

結構體

  • 在b是char類型的時候,a和b的內存地址是緊鄰的;若是b是int類型的話,就會出現如圖所示的狀況。咱們能夠這樣記憶:不看b以後的字段,a和b以前也是按照它們的最小公倍數對齊的(若是b是int類型,a和b的最小公倍數是4,按4對齊;若是b是char類型,最小公倍數爲1,按1對齊,就會出現a和b緊鄰的狀況)
  • 若是不想對齊,有以下解決方案:segmentfault

    • 編譯的時候不加優化參數
    • 代碼層面:在struct後加關鍵字,例如redis中的sds簡單動態字符串的實現:數組

      struct __attribute__ ((packed)) sdshdr16 {
              uint16_t len;
              uint16_t alloc;
              unsigned char flags;
              char buf[];
          }

聯合體

  • 全部字段共用一段內存,用於PHP中變量值的存儲(由於變量只有一種類型),也能夠用來判斷機器的大小端問題。

宏定義

  • 宏就是替換。
  • 關於下面這段代碼的複雜宏替換問題,在PHP內存管理3筆記中已經有詳細解釋,此處再也不贅述。
#define _BIN_DATA_SIZE(num, size, elements, pages, x, y) size,
static const uint32_t bin_data_size[] = {
  ZEND_MM_BINS_INFO(_BIN_DATA_SIZE, x, y)
};

PHP7中的基本變量

  • 在PHP7中,全部變量都以zval結構體來表示。一個zval是16字節;在PHP5中,一個zval是48字節。
struct _zval_struct {
    zend_value value;
    union u1;
    union u2;
};
  • 存儲變量須要考慮兩個要素:值與類型。

變量值的存放

  • 在PHP7中,變量的值存在zend_value 這個聯合體中。只有整型和浮點型是直接存在zend_value中,其他類型都只存放了一個指向專門存放該類型的結構體指針。這個聯合體共佔用8字節。
typedef union _zend_value {
    zend_long         lval;    //整型
    double            dval;    //浮點
    zend_refcounted  *counted; //引用計數
    zend_string      *str; //字符串
    zend_array       *arr; //數組
    zend_object      *obj; //對象
    zend_resource    *res; //資源
    zend_reference   *ref; //引用
    zend_ast_ref     *ast; //抽象語法樹
    zval             *zv;  //內部使用
    void             *ptr; //不肯定類型,取出來以後強轉
    zend_class_entry *ce;  //類
    zend_function    *func;//函數
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww; //這個union一共8B,這個結構體每一個字段都是4B,由於全部聯合體字段共用一塊內存,故至關於取了一半的union
} zend_value;

變量類型的存放

  • 在PHP7中,其變量的類型存放在zval中的u1聯合體中:
...
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    type,     /* 在這裏用unsigned char存放PHP變量值的類型 */
                zend_uchar    type_flags,
                zend_uchar    const_flags,
                zend_uchar    reserved)        /* call info for EX(This) */
        } v;
        uint32_t type_info;
    } u1;
...
  • PHP7中全部的變量類型:
/* regular data types */
#define IS_UNDEF                    0
#define IS_NULL                        1
#define IS_FALSE                    2
#define IS_TRUE                        3
#define IS_LONG                        4
#define IS_DOUBLE                    5
#define IS_STRING                    6
#define IS_ARRAY                    7
#define IS_OBJECT                    8
#define IS_RESOURCE                    9
#define IS_REFERENCE                10

/* constant expressions */
#define IS_CONSTANT                    11
#define IS_CONSTANT_AST                12

/* fake types */
#define _IS_BOOL                    13
#define IS_CALLABLE                    14
#define IS_ITERABLE                    19
#define IS_VOID                        18

/* internal types */
#define IS_INDIRECT                 15
#define IS_PTR                        17
#define _IS_ERROR                    20

PHP7中的字符串

字符串基本結構

  • 設計字符串存儲的數據結構兩大要素:字符串值和長度。
  • PHP7字符串存儲結構的設計:
struct _zend_string {
    zend_refcounted_h gc;         /*引用計數,與垃圾回收相關,暫不展開*/
    zend_ulong        h;          /* 冗餘的hash值,計算數組key的哈希值時避免重複計算*/
    size_t            len;        /* 長度 */
    char              val[1];     /* 柔性數組,真正存放字符串值 */
};
  • 由爲何存長度引伸出二進制安全的問題。二進制安全:寫入的數據和讀出來的數據徹底相同,就是二進制安全的,詳情見PHP字符串筆記

字符串寫時複製

  • 看下面一段PHP代碼:
<?php
$a = "string" . time("Y-m-d");
echo $a;
$b = $a;
echo $a;
echo $b;
$b = "new string";
echo $a;
echo $b;
  • 利用gdb調試這段代碼,觀察其引用計數狀況。
  • 在第一個echo語句處打斷點,並查看$a中zend_stinrg中的引用計數gc.refcount = 1(下簡稱refcount)。由於如今只有一個$a引用zend_string。

  • 利用gdb的c命令繼續運行下一行PHP代碼$b = $a,而後觀察$a的zend_sting,咱們發現$a引用的zend_string的refcount變爲2:

  • 查看此時的$b,發現引用的zend_string的refcount也是2,且地址均是0x7ffff5e6b0f0,說明$a與$b所引用的是同一個zend_string。

  • 此時的內存結構如圖所示:

  • 這樣作的優勢就是僅僅須要1個zend_string就能夠存儲兩個PHP變量的值,而不是2個zend_string,節省了1個zend_string的內存空間。
  • 那麼咱們看接下來$b = "new string",這樣的話,$a和$b因爲存儲的內容不一樣,故不能夠繼續引用同一個zend_string,這時就會發生寫時複製。咱們繼續gdb調試,看一下是否符合預期:
  • 給$b賦值後,觀察$a的存儲狀況:

  • 咱們看到,此時$a所指向的zend_string的refcount變爲了1,接下來再看一下$b的存儲狀況:

  • 注意此時$b所指向的zend_string的refcount變爲了0(注意這裏爲何是0而不是1呢?下面會講),並且b指向的zend_string的地址爲0x7ffff5e6a5c8,與$a所指向的zend_string的地址0x7ffff5e6b0f0不一樣,說明發生了寫時複製,即因爲字符串值的改變,被迫生成了一個新的zend_string結構體,用來專門存儲$b的值;而$a指向的zend_string只是refcount減小了1,其他並未發生變化。
  • 那麼爲何$b所指向的zend_string的refcount是0呢,咱們先給PHP中的字符串分個類:安全

    • 常量字符串:在PHP代碼中硬編碼的字符串,在編譯階段初始化,存儲在全局變量表中,refcount一直爲0,其在請求結束以後才被銷燬(方便重複利用)。
    • 臨時字符串:計算出來的臨時字符串,是執行階段通過zend虛擬機執行opcode計算出來的字符串,存儲在臨時變量區。
  • 咱們舉一個例子:
<?php
$a = "hello" . time("Y-m-d"); //臨時字符串,由於time()會隨時間變化而變化
$b = "hello world";           //常量字符串
  • 這裏$a因爲調用了time()函數,因此最終的值是不肯定的,是臨時字符串。
  • $b也能夠叫作字面量,是被硬編碼在PHP代碼中的,是常量字符串。
  • 咱們畫一下最終$a與$b的內存結構圖:

  • 由此咱們能夠清晰地看到,$a與$b不在引用同一個zend_string。那麼咱們給寫時複製下一個定義:給$b從新賦值而致使不能與$a共用一個zend_string的現象,叫作寫時複製。

PHP7中的數組

  • PHP7中的數組是一個hashtable,key-value對存儲於bucket中。

  • PHP7數組基本結構:
struct _zend_array {
    zend_refcounted_h gc;
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    flags,
                zend_uchar    nApplyCount,
                zend_uchar    nIteratorsCount,
                zend_uchar    consistency)
        } v;
        uint32_t flags;
    } u;
    uint32_t          nTableMask;       //數組大小減一,用來作或運算,packed array初始值是-2,hash array初始值是-8
    Bucket            *arData;          //指針,指向實際存儲數組元素的bucket
    uint32_t          nNumUsed;         //使用了多少bucket,可是unset的時候這個值不減小
    uint32_t          nNumOfElements;   //真正有多少元素,unset的時候會減小
    uint32_t          nTableSize;       //bucket的個數
    uint32_t          nInternalPointer;
    zend_long         nNextFreeElement; //支持$arr[] = 1;語法,沒插入1個元素就會遞增1
    dtor_func_t       pDestructor;
};

typedef struct _zend_array HashTable;
  • 此結構在內存中的結構圖以下:

  • 思考:爲何要存儲gc字段?由於gc字段冗餘存儲了變量的類型,給任意一個變量,把它強轉成zend_refcounted_h類型,均可以拿到它的類型,zend_refcounted_h類型結構以下:
typedef struct _zend_refcounted_h {
    uint32_t         refcount;            /* 引用計數 */
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar    type,
                zend_uchar    flags,    /* used for strings & objects */
                uint16_t      gc_info)  /* keeps GC root number (or 0) and color */
        } v;
        uint32_t type_info;
    } u;
} zend_refcounted_h;
  • 進行強制類型轉換以後,經過取該變量的u.type字段,就能夠拿到當前變量的類型了。
  • 咱們接着看一下bucket的結構:
typedef struct _Bucket {
    zval              val;   //元素的值,注意這裏直接存了zval而不是一個zval指針
    zend_ulong        h;     //冗餘的哈希值,避免重複計算哈希值
    zend_string      *key;   //元素的key值,指向一個zend_string結構體
} Bucket;
  • 思考若是利用$arr[] = 1;語法進行數組賦值,key字段的值是多少?答案是0x0,就是一個空指針。
  • hashtable的問題:哈希衝突,解決衝突的方法有開放定製法和鏈地址法,經常使用的是鏈地址法。
  • PHP7中並無採用真正的鏈表結構,而是利用數組模擬鏈表。這個時候須要在Bucket數組以前額外開闢一段內存空間(叫作索引數組,每一個索引數組的單元叫一個slot),來存儲同一hash值的第一個bucket的索引下標。
  • 看一個簡單的數組查找過程:數據結構

    • 通過time33哈希算法算出哈希值h
    • 計算出索引數組的nIndex = h | nTableMask = -7(假設),這個nIndex也別稱作slot
    • 訪問索引數組,取出索引爲-7位置上的元素值爲3
    • 訪問bucket數組,取出索引爲3位置上的key,爲x,發現並不等於s,那麼繼續查找,訪問val.u2.next指針,爲2
    • 取出索引爲2位置上的key,爲s,發現正好是咱們要找的那個key
    • 取出對應的val值3
  • 注意若是bucket的存儲空間滿了,須要從新計算和nIndex(即slot)的值並將值放到正確的bucket位置上,這個過程也叫作rehash。
  • 具體的插入過程詳見PHP基本變量筆記的文章末尾。
  • PHP7中的數組分爲兩種:packed array與hash array。函數

    • packed array:測試

      • key是數字,且順序遞增
      • 位置固定,如訪問key是0的元素,即$arr1[0],就直接訪問bucket數組的第0個位置便可(即arData[0]),這樣就不須要前面的索引數組。
    • 若是不知足上述條件,就是hash array
相關文章
相關標籤/搜索