PHP內核分析-變量+引用+寫時複製+變量分離

變量

PHP5 變量的內部實現

      變量存儲結構:

typedef struct _zval_struct zval;
...
struct _zval_struct {
    /* Variable information */
    zvalue_value value;        /*變量的值的存放容器*/
    zend_uint refcount__gc;    /*引用計數器*/
    zend_uchar type;           /*變量類型*/
    zend_uchar is_ref__gc;     /*變量是否爲引用變量*/
};

      變量的值存儲結構:(union能節省內存空間,因爲所有的字段重疊在內存中的相同偏移處,且一個變量同時只能屬於一種類型)

 

typedef union _zvalue_value {
    long lval;                  /* integer */
    double dval;                /* float */
    struct {                    /* string */
        char *val;
        int len;
    } str;
    HashTable *ht;              /* hash table */
    zend_object_value obj;      /* obj */
} zvalue_value;
/*另外還有 null類型 和 resource類型*/

PHP7 變量的內部實現

      變量存儲結構:

 

typedef struct _zval_struct     zval;
struct _zval_struct {
    zend_value        value; //變量實際的value
    union {
        struct {
            ZEND_ENDIAN_LOHI_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; //一些輔助值
};

      變量的值存儲結構:

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;

 

二者的區別:

1、在PHP5中,變量引用計數器是存在變量容器zval中的,且用is_ref判斷變量是否爲引用類型,然而在PHP7中,變量計數器不再存放在zval中,且也不再通過is_ref判斷變量是否爲引用類型,而刪除了zval中的is_count和is_ref標識字段而在zend_value增加了一個zend_referrence類型 ,而將引用計數存新增到了每個類型字段中。

2、引用類型的差別:

      PHP5是通過is_ref字段來區分一個變量是否爲引用類型;

      而PHP7的引用原理如下:

            數據結構:

struct _zend_reference {
    zend_refcounted_h gc;
    zval              val;
};

 

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

<?php
//注意:引用只能通過&產生,無法通過賦值傳遞,比如:
$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的value賦值給$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語言中指針的指針的概念。

引用:(引用 != 指針)

php引入引用的目的:

    背景:

    【

        其主要目的是遵循『面對對象模式』:對象傳參給函數或者方法後,這個函數發送一個指令給對象(例如調用了一個方法)以此來改變對象的狀態(例如對象的屬性)。因此傳參進去的對象必須爲同一個。

    】

    實際:

    【

        在 PHP 4 中,對象被當成變量來對待,所以當對象作爲函數傳參時,他們是被複制的。 PHP 4 的面對對象用戶使用『引用傳參』來解決這個問題,不過很難做到完美。

        PHP 5 引進了獨立於變量容器的『對象存儲器』。當一個對象賦值給變量時,變量不再存儲整個對象(屬性表和其他的『類』信息),而是存儲這個對象所在 存儲器的引用 —— 當我們複製一個對象變量時,我們複製的是這個『存儲器的引用』。

    】

    *注:【這很容易被誤解爲『引用』,但是『存儲器的引用』與『引用』是完全不同的概念。

php引用的本質:

    宏觀來看:引用是變量的別名

    從php5和php7分別來看:

    【

        php5中通過zval結構的is_ref字段表示該變量是否爲引用變量,故再使用引用類型變量進行賦值操作時,會有不同的賦值操作(正常賦值或者變量分離等)

        php7新增了引用類型變量值容器`struct _zend_reference`,同樣對引用類型變量進行賦值操作時,會有不同的賦值操作(引用變量的值替換`作爲左值`或者變量分離`作爲右值`等)

    】

    對一個變量進行賦值時,會遞歸遍歷右值的所有值類型嗎(然後對是引用類型的變量發生`變量分離操作`)?

<?php
$a['hello'] = 'world';
$a['l'] = 1;
$b = &$a['hello'];
$c = $a;
$c['hello'] = 'kaka';
$c['l'] = 2;

/**
 * 引用可以將一個變量給污染了
 *      原因如下:
 *          $a['hello']由於`被引用`,因此$a['hello']成了一個引用類型的變量,然而$c = $a的賦值操作中,由於$a並不是引用類型的變量,所以$c和$a指向同一個`變量值容器`, 當執行 $c['hello'] = 'kaka' 這個賦值操作時,由於$c['hello']是一個引用類型,會直接在更改值內容,而不會發生`寫時複製`,而$c['l']變量類型不爲引用類型,且$c['l']的值不等於原值,所以會發生一次`寫時複製`操作(即產生一塊新的內存,將新值賦值給該內存,然後$c['1']重新指向這塊新內存)
 */
var_dump($a);
var_dump($c);

 

    答案是:不會!!

 

寫時複製

    如果通過賦值的方式賦值給變量時不一定會申請新內存來存放 新變量的值,而是簡單的通過一個`變量值容器`來共用內存,只有在其中的一個引用這個`容器`的變量的 值發生變化時才申請新空間來保存值內容以減少對內存的佔用。 在很多場景下PHP都COW進行內存的優化。比如:變量的多次賦值、函數參數傳遞,並在函數體內修改實參等。

變量分離

 

<?php
$a = 1;
$b = $a;
$a = 2;

    當$foo的值被賦給$bar時,PHP並沒有將內存複製一份交給$bar,而是把$foo和$bar指向同一個地址。 隨後,我們更改了$bar的值,這時如果直接需該$bar變量指向的內存,則$foo的值也會跟着改變。 這不是我們想要的結果。於是,PHP內核將內存複製出來一份,並將其值更新爲賦值的:2(這個操作也稱爲變量分離操作), 同時原$foo變量指向的內存只有$foo指向。所以變量分離是寫時複製時產生的一種現象(自己的解釋)。

 

 

參考:

    http://www.php-internals.com/book/?p=chapt06/06-06-copy-on-write 《寫時複製》

    https://zhuanlan.zhihu.com/p/35107602 《PHP 引用是個坑,請慎用》

    https://github.com/pangudashu/php7-internal 《php7-內核分析》

 

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