写点什么

PHP7 内核实现原理 - 变量的基本结构

作者:菜皮日记
  • 2023-09-09
    北京
  • 本文字数:6829 字

    阅读完需:约 22 分钟

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.htypedef 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

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

如代码:

$a = "time:" . time();$b = &$a;
复制代码

图示的最终结果:

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

再看一个例子:

$a = 'hello';$b = $a;$c = &$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;
复制代码

在 后,拷贝出一份,供 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

写时复制机制可以提高性能减少内存占用,简单的变量回收可以通过引用计数实现,复杂的变量引用关系导致的垃圾需要使用垃圾回收来处理。

用户头像

菜皮日记

关注

全干程序员 2018-08-08 加入

还未添加个人简介

评论

发布
暂无评论
PHP7内核实现原理-变量的基本结构_php_菜皮日记_InfoQ写作社区