关于后端:PHP7内核实现原理变量的基本结构

50次阅读

共计 6814 个字符,预计需要花费 18 分钟才能阅读完成。

PHP 5 的变量构造

PHP 5 中一个变量的内存占用比拟节约,比方 long 和 double 类型的变量是不须要援用计数的

PHP 7 的变量变动:变量名 zval、变量值 zend_value

PHP 7 应用名为 zval 的构造存储变量名,应用名为 zend_value 的构造体存储变量值。zval 中有一个 zend_value 类型的属性,将 zval 和 zend_value 关联起来。

在 PHP 中所有类型的变量都是用 zval 来存储,zval 中蕴含一个 type 属性,示意本变量的类型。这就是 PHP 弱类型的外围

//zend_types.h
typedef struct _zval_struct     zval;

/*** 变量值构造体 zend_value ***/
typedef union _zend_value {
    /***
    这里就是寄存具体值的中央的了,除了 zend_log 整型和 double 浮点型是间接存储具体值,其余类型都是应用指针指向额定的构造体地址
    ***/
    zend_long         lval;    // 如果是整型 则其值寄存在这里
    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;

/*** 变量名构造体 zval ***/
struct _zval_struct {
    zend_value        value; /*** 变量理论的 value,指向 zend_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 中 u1 和 u2 都是联合体,联合体特点是外部给字段复用存储空间,故 v 和 type_info 是共享一块空间的,v 和 type_info 的关系:

图中 type 值为 6,即等于宏定义 IS_STRING,示意以后变量类型是字符串,且字符串的 type_flag 掩码为 24,这两项设置完后,再从 type_info 侧读取,type_info 值就为 6150 了(2^1 + 2^2 + 2^11 + 2^12)。

u2 中记录了一些辅助字段,如 next 用来解决哈希表中的哈希抵触。

zval 的整体内存构造如图:

变量类型

变量的类型存储在 zval.u1.v.type 中,可选值有:

其中 IS_TRUE(布尔类型 true)、IS_FALSE(布尔类型 false)、IS_NULL(空类型 null) 这几个类型没有具体的变量值,间接依据其类型来辨别。

而 IS_LONG(整型 long)、IS_DOUBLE(浮点型 double) 的变量值则别离存在 zend_value 中的 zend_long、double 两个属性下。也就是说,标量类型不须要额定的 value 指针,,其余类型都是通过指针,指向额定的数据结构构造体地址。

变量类型属性

zval.u1.v.type_flags 的宏定义如下:

/* zval.u1.v.type_flags */
#define IS_TYPE_CONSTANT             (1<<0)
#define IS_TYPE_IMMUTABLE            (1<<1)
#define IS_TYPE_REFCOUNTED           (1<<2)
#define IS_TYPE_COLLECTABLE          (1<<3)
#define IS_TYPE_COPYABLE             (1<<4)

通过 bitmap 设置以后变量的内存属性,如是否反对写时复制,是否反对垃圾回收等

字符串构造体 zend_string

字符串构造体应用 _zend_string 示意,其中 zend_refcounted_h 示意援用计数,以及前面所有反对援用计数的数据结构都应用这个构造体来实现。

struct _zend_string {
    zend_refcounted_h gc; /*** 援用计数 ***/
    zend_ulong        h;
    size_t            len;
    char              val[1];
};

数组构造体 zend_array

数组底层实现就是一般的有序 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;
};

对象 zend_object / 资源构造体 zend_resource

资源是指 tcp 连贯、文件句柄等等。

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;
};

援用构造体 zend_reference

援用是一种非凡的类型:

struct _zend_reference {
    zend_refcounted_h gc;
    zval              val;
};

在 PHP 中通过 & 操作符产生一个援用变量。具体过程:不论以前的类型是什么,& 首先会创立一个 zend_reference 构造体,其内嵌的 zval 的 value 指向原来 zval 的 value (如果是布尔、整形、浮点则间接复制原来的值),而后将原 zval 的类型批改为 IS_REFERENCE,行将其变成援用类型的 zval,最初将原 zval 的 value 指向新创建的 zend_reference 构造。

一开始的状况:zval(类型 xxx).value → zend_value。

取 & 后的状况:zval(类型 IS_REFERENCE).value → zend_reference.zval.value → zend_value

这个过程有点像往链表中插入节点的过程。留神,此时会将 $a $b 这两个变量都变成 ref 类型。

如代码:

$a = "time:" . time();
$b = &$a;

图示的最终后果:

另外援用只能通过 & 产生,赋值操作是不会产生援用的,也就是说援用只会有一层,不会呈现援用指向援用的状况。

再看一个例子:

$a = 'hello';
$b = $a;
$c = &$b;

首先 $b = $a 的赋值后的状况:

之后 $c = &$b 的状况:

但如果此时 unset 掉 b 的话,b 的 type 会变成 null,但 c 仍旧是 reference,而并不是第一印象中的 b 和 c 都变成 null 了,因为 unset b 只是将 b 的 type 改成了 null,不影响 c 的 type。

同样的,如果将 b 或 c 扭转了值,此时会产生 COW,b 和 c 指向新的 string,a 还指向最后的 string,如下代码:

$a = 'hello' . time();
echo $a;

$b = $a;
echo $a;
echo $b;
// 此时 z.value.str 指向的 zend_value 的 refcount=2

$c = &$b;
echo $a;
echo $b;
echo $c;
// 此时老的 zend_value 的 refcount 仍旧是 2,因为有 a 和新的 bc 的 reference 两个指向。// 新的 zend_reference 的 refcount 也是 2,由 b 和 c 指向

$c = 'xxx' . time();

echo $a;
echo $b;
echo $c;
// a 和 bc 的指向拆散,bc 指向新的字符串。老的字符串 refcount 变成 1 

内存治理

绝大部分对变量的操作都是读操作,如果赋值一个变量就齐全 alloc 一份数据进去实践上是可行的,也是最简略的,但对性能耗费太大效率太低,所以个别的计划都是 援用计数 + 写时复制 ,附加 垃圾回收 来保护内存。

援用计数

援用计数是 zend_value 中的 value 构造体(比方字符串 zend_string)中的一个属性(个别记为 zend_refcounted_h),记录指向以后 value 的数量。

  • 变量复制、函数传参时 此计数 +1
  • 变量销毁时计数 -1
  • 直到计数为 0 时 将销毁。
$a = "time:" . time();   //$a       ->  zend_string_1(refcount=1)
$b = $a;                 //$a,$b    ->  zend_string_1(refcount=2)
$c = $b;                 //$a,$b,$c ->  zend_string_1(refcount=3)

unset($b);               //$b = IS_UNDEF  $a,$c ->  zend_string_1(refcount=2)

援用计数 zend_refcounted_h 构造体:

typedef struct _zend_refcounted_h {
    uint32_t         refcount;          /* reference counter 32-bit */
    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;

不是所有类型都有援用计数,是否反对援用计数是通过 zend_value.u1.v.type_flags 类型掩码管制的

反对援用计数的类型,掩码蕴含 IS_TYPE_REFCOUNTED

#define IS_TYPE_REFCOUNTED          (1<<2)

|     type       | refcounted |
+----------------+------------+
|simple types    |            |
|string          |      Y     |
|interned string |            |
|array           |      Y     |
|immutable array |            |
|object          |      Y     |
|resource        |      Y     |
|reference       |      Y     |

简略数据类型没有不用说,比方 long、double 都是没有的,另外两个阐明一下:

  • interned string:外部字符串,像这种 $a = "hi~",可了解为在申请期间不会扭转且申请完结后被销毁的值,这样就不须要通过援用计数治理了。
  • immutable array:还不太分明

写时复制

写时复制即:当多个变量指向同一个 zend_value 的状况下,当某一个变量产生更改,会从新拷贝一份 value 进去供批改,同时断开旧的指向。

$a = array(1,2);
$b = &$a;
$c = $a;

// 尝试批改,$a $b 
$b[] = 3;

在 $b[]=3 后,value 拷贝出一份,供 $a $b 指向。

跟援用计数一样,不是所有类型都反对写时复制。是否反对写时复制也是通过 zend_value.u1.v.type_flags 类型掩码管制的,下图中不反对 copyable 的就是不反对写时复制。

反对写时复制的类型,掩码中蕴含 IS_TYPE_COPYABLE。#define IS_TYPE_COPYABLE         (1<<4)

|     type       |  copyable  |
+----------------+------------+
|simple types    |            |
|string          |      Y     |
|interned string |            |
|array           |      Y     |
|immutable array |            |
|object          |            |
|resource        |            |
|reference       |            |

变量回收

  • 被动销毁回收:应用 unset
  • 主动销毁回收:函数 return、产生写时复制等,可能导致援用计数为 0

垃圾回收

垃圾回收相当于是高阶的变量回收,一般的垃圾回收通过援用计数为 0 即可实现,简单的状况如下:

$a = [1];
$a[] = &$a;

unset($a);

unset 之前:

unset 之后:

此时该数值内部曾经没有指向的变量,导致数组无法访问到。这种状况只可能产生在数组或对象中,所以 PHP 会针对这两种类型做非凡查看:如果当销毁一个变量后,发现援用计数还大于 0,并且是 IS_ARRAY、IS_OBJECT 时,则此 value 则会被放入 gc 的垃圾链表中,期待链表达到肯定数量后会启动查看回收掉。

垃圾回收步骤:

  • 对 roots 环(即新退出的有可能是垃圾的变量)中每个元素进行深度优先遍历,将每个元素中 gc_info 为紫色的标记元素为灰色,且援用计数减 1。
  • 扫描 roots 环中 gc_info 为灰色的元素,如果发现其援用计数仍旧大于 0,阐明这个元素还在其余中央应用,那么将其色彩从新标记回彩色,并将其援用计数加 1(在第一步有减 1 操作)。如果发现其援用计数为 0,则将其标记为红色。该过程同样为深度优先遍历。
  • 扫描 roots 环,将 gc_info 色彩为彩色的元素从 roots 移除。而后对 roots 中色彩为红色的元素进行深度优先遍历,将其援用计数加 1(在第一步有减 1 操作),而后将 roots 链表挪动到待开释的列表中(to_free)。
  • 开释 to_free 列表的元素。

是否反对垃圾回收也是 zend_value.u1.v.type_flags 掩码定义的

#define IS_TYPE_COLLECTABLE

|     type       | collectable |
+----------------+-------------+
|simple types    |             |
|string          |             |
|interned string |             |
|array           |      Y      |
|immutable array |             |
|object          |      Y      |
|resource        |             |
|reference       |             |

总结一下:一个类型是否反对援用计数、写时复制、垃圾回收,是依据 zval.u1.v.type_flags 掩码来决定的,上面三个值别离示意是否反对的对应的性能。

  • IS_TYPE_REFCOUNTED
  • IS_TYPE_COPYABLE
  • IS_TYPE_COLLECTABLE

写时复制机制能够进步性能缩小内存占用,简略的变量回收能够通过援用计数实现,简单的变量援用关系导致的垃圾须要应用垃圾回收来解决。

本文由 mdnice 多平台公布

正文完
 0