PHP變量的底層結構

PHP變量

變量是一個語言實現的基礎,變量有兩個組成部分:變量名、變量值,PHP中可以將其對應爲:zval、zend_value,這兩個概念一定要區分開,PHP中變量的內存是通過引用計數進行管理的,而且PHP7中引用計數是在zend_value而不是zval上,變量之間的傳遞、賦值通常也是針對zend_value。
PHP中可以通過關鍵詞定義一個變量:a;,在定義的同時可以進行初始化:$a = “hi~”;,注意這實際是兩步:定義、初始化,只定義一個變量也是可以的,可以不給它賦值,比如:

$a;
$b = 1;

這段代碼在執行時會分配兩個zval。

接下來我們具體看下變量的結構以及不同類型的實現。

PHP5的zval

在PHP5的時候, zval的定義如下:

struct _zval_struct {
    union {
        long lval;
        double dval;
        struct {
            char *val;
            int len;
        } str;
        HashTable *ht;
        zend_object_value obj;
        zend_ast *ast;
    } value;
    zend_uint refcount__gc;
    zend_uchar type;
    zend_uchar is_ref__gc;
};

對PHP5內核有了解的同學應該對這個結構比較熟悉, 因爲zval可以表示一切PHP中的數據類型, 所以它包含了一個type字段, 表示這個zval存儲的是什麼類型的值, 常見的可能選項是IS_NULL, IS_LONG, IS_STRING, IS_ARRAY, IS_OBJECT等等.

根據type字段的值不同, 我們就要用不同的方式解讀value的值, 這個value是個聯合體, 比如對於type是IS_STRING, 那麼我們應該用value.str來解讀zval.value字段, 而如果type是IS_LONG, 那麼我們就要用value.lval來解讀.

另外, 我們知道PHP是用引用計數來做基本的垃圾回收的, 所以zval中有一個refcount__gc字段, 表示這個zval的引用數目, 但這裏有一個要說明的, 在5.3以前, 這個字段的名字還叫做refcount, 5.3以後, 在引入新的垃圾回收算法來對付循環引用計數的時候, 作者加入了大量的宏來操作refcount, 爲了能讓錯誤更快的顯現, 所以改名爲refcount__gc, 迫使大家都使用宏來操作refcount.

類似的, 還有is_ref, 這個值表示了PHP中的一個類型是否是引用, 這裏我們可以看到是不是引用是一個標誌位.

這就是PHP5時代的zval, 在2013年我們做PHP5的opcache JIT的時候, 因爲JIT在實際項目中表現不佳, 我們轉而意識到這個結構體的很多問題. 而PHPNG項目就是從改寫這個結構體而開始的.

PHP7的zval

//zend_types.h
typedef struct _zval_struct     zval;

typedef union _zend_value {
    zend_long         lval;    //int整形
    double            dval;    //浮點型
    zend_refcounted  *counted;
    zend_string      *str;     //string字符串
    zend_array       *arr;     //array數組
    zend_object      *obj;     //object對象
    zend_resource    *res;     //resource資源類型
    zend_reference   *ref;     //引用類型,通過&$var_name定義的
    zend_ast_ref     *ast;     //下面幾個都是內核使用的value
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;

struct _zval_struct {
    zend_value        value; //變量實際的value
    union {
        struct {
            ZEND_ENDIAN_LOHI_4( //這個是爲了兼容大小字節序,小字節序就是下面的順序,大字節序則下面4個順序翻轉
                zend_uchar    type,         //變量類型
                zend_uchar    type_flags,  //類型掩碼,不同的類型會有不同的幾種屬性,內存管理會用到
                zend_uchar    const_flags,
                zend_uchar    reserved)     //call info,zend執行流程會用到
        } v;
        uint32_t type_info; //上面4個值的組合值,可以直接根據type_info取到4個對應位置的值
    } u1;
    union {
        uint32_t     var_flags;
        uint32_t     next;                 //哈希表中解決哈希衝突時用到
        uint32_t     cache_slot;           /* literal cache slot */
        uint32_t     lineno;               /* line number (for ast nodes) */
        uint32_t     num_args;             /* arguments number for EX(This) */
        uint32_t     fe_pos;               /* foreach position */
        uint32_t     fe_iter_idx;          /* foreach iterator index */
    } u2; //一些輔助值
};

zval結構比較簡單,內嵌一個union類型的zend_value保存具體變量類型的值或指針,zval中還有兩個unionu1u2:

  • u1: 它的意義比較直觀,變量的類型就通過u1.v.type區分,另外一個值type_flags爲類型掩碼,在變量的內存管理、gc機制中會用到,第三部分會詳細分析,至於後面兩個const_flagsreserved暫且不管
  • u2: 這個值純粹是個輔助值,假如zval只有:value、u1兩個值,整個zval的大小也會對齊到16byte,既然不管有沒有u2大小都是16byte,把多餘的4byte拿出來用於一些特殊用途還是很划算的,比如next在哈希表解決哈希衝突時會用到,還有fe_posforeach會用到…

zend_value可以看出,除longdouble類型直接存儲值外,其它類型都爲指針,指向各自的結構。

變量類型

zval.u1.type類型:

/* 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

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

最簡單的類型是true、false、long、double、null,其中true、false、null沒有value,直接根據type區分,而long、double的值則直接存在value中:zend_long、double,也就是標量類型不需要額外的value指針。

字符串

PHP中字符串通過zend_string表示:

struct _zend_string {
    zend_refcounted_h gc;
    zend_ulong        h;                /* hash value */
    size_t            len;
    char              val[1];
};
  • gc: 變量引用信息,比如當前value的引用數,所有用到引用計數的變量類型都會有這個結構,3.1節會詳細分析
  • h: 哈希值,數組中計算索引時會用到
  • len: 字符串長度,通過這個值保證二進制安全
  • val: 字符串內容,變長struct,分配時按len長度申請內存

事實上字符串又可具體分爲幾類:IS_STR_PERSISTENT(通過malloc分配的)、IS_STR_INTERNED(php代碼裏寫的一些字面量,比如函數名、變量值)、IS_STR_PERMANENT(永久值,生命週期大於request)、IS_STR_CONSTANT(常量)、IS_STR_CONSTANT_UNQUALIFIED,這個信息通過flag保存:zval.value->gc.u.flags,後面用到的時候再具體分析。

數組

array是PHP中非常強大的一個數據結構,它的底層實現就是普通的有序HashTable,這裏簡單看下它的結構,下一節會單獨分析數組的實現。

typedef struct _zend_array HashTable;

struct _zend_array {
    zend_refcounted_h gc; //引用計數信息,與字符串相同
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    flags,
                zend_uchar    nApplyCount,
                zend_uchar    nIteratorsCount,
                zend_uchar    reserve)
        } v;
        uint32_t flags;
    } u;
    uint32_t          nTableMask; //計算bucket索引時的掩碼
    Bucket           *arData; //bucket數組
    uint32_t          nNumUsed; //已用bucket數
    uint32_t          nNumOfElements; //已有元素數,nNumOfElements <= nNumUsed,因爲刪除的並不是直接從arData中移除
    uint32_t          nTableSize; //數組的大小,爲2^n
    uint32_t          nInternalPointer; //數值索引
    zend_long         nNextFreeElement;
    dtor_func_t       pDestructor;
};

對象/資源

struct _zend_object {
    zend_refcounted_h gc;
    uint32_t          handle;
    zend_class_entry *ce; //對象對應的class類
    const zend_object_handlers *handlers;
    HashTable        *properties; //對象屬性哈希表
    zval              properties_table[1];
};

struct _zend_resource {
    zend_refcounted_h gc;
    int               handle;
    int               type;
    void             *ptr;
};

對象比較常見,資源指的是tcp連接、文件句柄等等類型,這種類型比較靈活,可以隨意定義struct,通過ptr指向,後面會單獨分析這種類型,這裏不再多說。

引用

引用是PHP中比較特殊的一種類型,它實際是指向另外一個PHP變量,對它的修改會直接改動實際指向的zval,可以簡單的理解爲C中的指針,在PHP中通過&操作符產生一個引用變量,也就是說不管以前的類型是什麼,&首先會創建一個zend_reference結構,其內嵌了一個zval,這個zval的value指向原來zval的value(如果是布爾、整形、浮點則直接複製原來的值),然後將原zval的類型修改爲IS_REFERENCE,原zval的value指向新創建的zend_reference結構。

struct _zend_reference {
    zend_refcounted_h gc;
    zval              val;
};

結構非常簡單,除了公共部分zend_refcounted_h外只有一個val,舉個示例看下具體的結構關係:

$a = "time:" . time();      //$a    -> zend_string_1(refcount=1)
$b = &$a;                   //$a,$b -> zend_reference_1(refcount=2) -> zend_string_1(refcount=1)

最終的結果如圖:
在這裏插入圖片描述
注意:引用只能通過&產生,無法通過賦值傳遞,比如:

$a = "time:" . time();      //$a    -> zend_string_1(refcount=1)
$b = &$a;                   //$a,$b -> zend_reference_1(refcount=2) -> zend_string_1(refcount=1)
$c = $b;                    //$a,$b -> zend_reference_1(refcount=2) -> zend_string_1(refcount=2)
                            //$c    ->                                 ---

$b = &$a這時候$a$b的類型是引用,但是$c = $b並不會直接將$b賦值給$c,而是把$b實際指向的zval賦值給$c,如果想要$c也是一個引用則需要這麼操作:

$a = "time:" . time();      //$a       -> zend_string_1(refcount=1)
$b = &$a;                   //$a,$b    -> zend_reference_1(refcount=2) -> zend_string_1(refcount=1)
$c = &$b;/*或$c = &$a*/     //$a,$b,$c -> zend_reference_1(refcount=3) -> zend_string_1(refcount=1) 

這個也表示PHP中的 引用只可能有一層 ,不會出現一個引用指向另外一個引用的情況 ,也就是沒有C語言中指針的指針的概念

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章