变量是一个语言实现的基础,变量有两个组成部分:变量名、变量值,PHP中可以将其对应为:zval、zend_value,这两个概念一定要区分开,PHP中变量的内存是通过引用计数进行管理的,而且PHP7中引用计数是在zend_value而不是zval上,变量之间的传递、赋值通常也是针对zend_value。
PHP中可以通过$
关键词定义一个变量:$a;
,在定义的同时可以进行初始化:$a = "hi~";
,注意这实际是两步:定义、初始化,只定义一个变量也是可以的,可以不给它赋值,比如:
$a; $b = 1;
这段代码在执行时会分配两个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
中还有两个union:u1
、u2
:
u1:它的意义比较直观,变量的类型就通过u1.v.type
区分,另外一个值type_flags
为类型掩码,在变量的内存管理、gc机制中会用到,第三部分会详细分析,至于后面两个const_flags
、reserved
暂且不管
u2:这个值纯粹是个辅助值,假如zval
只有:value
、u1
两个值,整个zval的大小也会对齐到16byte,既然不管有没有u2大小都是16byte,把多余的4byte拿出来用于一些特殊用途还是很划算的,比如next在哈希表解决哈希冲突时会用到,还有fe_pos在foreach会用到......
从zend_value
可以看出,除long
、double
类型直接存储值外,其它类型都为指针,指向各自的结构。
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)