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-内核分析》

 

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