您好,欢迎访问一九零五行业门户网

【问底】王帅:深入PHP内核(一)弱类型变量原理探究

php是一门简单而强大的语言,提供了很多web适用的语言特性,其中就包括了 变量 弱 类型 ,在弱 类型 机制下,你能够给一个 变量 赋任意 类型 的值。 php的执行是通过zend engine(下面简称ze),ze是使用c编写,在底层实现了一套弱 类型 机制。ze的内存管理
php是一门简单而强大的语言,提供了很多web适用的语言特性,其中就包括了变量弱类型,在弱类型机制下,你能够给一个变量赋任意类型的值。
php的执行是通过zend engine(下面简称ze),ze是使用c编写,在底层实现了一套弱类型机制。ze的内存管理使用写时拷贝、引用计数等优化策略,减少再变量赋值时候的内存拷贝。
下面不光带你探索php弱类型的原理,也会在写php扩展角度,介绍如何操作php的变量。
1. php的变量类型php的变量类型有8种:
标准类型:布尔boolen,整型integer,浮点float,字符string 复杂类型:数组array,对象object 特殊类型:资源resource  php不会严格检验变量类型,变量可以不显示的声明其类型,而在运行期间直接赋值。也可以将变量自由的转换类型。如下例,没有实现声明的情况下,$i可以赋任意类型的值。
php $i = 1; //int $i = 'show me the money'; //string $i = 0.02; // float $i = array(1, 2, 3); // array $i = new exception('test', 123); // object $i = fopen('/tmp/aaa.txt', 'a') // resource ?>
如果你对弱类型原理理解不深刻,在变量比较时候,会出现“超出预期”的惊喜。
php $str1 = null; $str2 = false; echo $str1==$str2 ? '相等' : '不相等'; $str3 = ''; $str4 = 0; echo $str3==$str4 ? '相等' : '不相等'; $str5 = 0; $str6 = '0'; echo $str5==$str6 ? '相等' : '不相等'; ?>
以上三个结果全部是相等,因为在变量比较的时候,php内部做了变量转换。如果希望值和类型同时判断,请使用三个=(如,$a===0)来判断。也许你会觉得司空见惯,也许你会觉得很神奇,那么请跟我一起深入php内核,探索php变量原理。
2. 变量的存储及标准类型介绍php的所有变量,都是以结构体zval来实现,在zend/zend.h中我们能看到zval的定义:
typedef union _zvalue_value { long lval; /* long value */ double dval; /* double value */ struct { char *val; int len; /* this will always be set for strings */ } str; /* string (always has length) */ hashtable *ht; /* an array */ zend_object_value obj; /* stores an object store handle, and handlers */ } zvalue_value;

属性名 含义 默认值
refcount__gc 表示引用计数 1
is_ref__gc 表示是否为引用 0
value 存储变量的值
type 变量具体的类型
其中refcount__gc和is_ref__gc表示变量是否是一个引用。type字段标识变量的类型,type的值可以是:is_null,is_bool,is_long,is_float,is_string,is_array,is_object,is_resource。php根据type的类型,来选择如何存储到zvalue_value。
zvalue_value能够实现变量弱类型的核心,定义如下:
typedef union _zvalue_value { long lval; /* long value */ double dval; /* double value */ struct { char *val; int len; /* this will always be set for strings */ } str; /* string (always has length) */ hashtable *ht; /* an array */ zend_object_value obj; /* stores an object store handle, and handlers */ } zvalue_value;

布尔型,zval.type=is_bool,会读取zval.value.lval字段,值为1/0。如果是字符串,zval.type=is_string,会读取zval.value.str,这是一个结构体,存储了字符串指针和长度。
c语言中,用\0作为字符串结束符。也就是说一个字符串hello\0world在c语言中,用printf来输出的话,只能输出hello,因为\0会认为字符已经结束。php中是通过结构体的_zval_value.str.len来控制字符串长度,相关函数不会遇到\0结束。所以php的字符串是二进制安全的。
如果是null,只需要zval.type=is_null,不需要读取值。
通过对zval的封装,php实现了弱类型,对于ze来说,通过zval可以存取任何类型。
3. 高级类型array和object数组array数组是php语言中非常强大的一个数据结构,分为索引数组和关联数组,zval.type=is_array。在关联数组中每个key可以存储任意类型的数据。php的数组是用hash table实现的,数组的值存在zval.value.ht中。 
后面会专门讲到php哈希表的实现。
对象类型的zval.type=is_object,值存在zval.value.obj中。
4. 特殊类型——资源类型(resource)介绍资源类型是个很特殊的类型,zval.type=is_resource,在php中有一些很难用常规类型描述的数据结构,比如文件句柄,对于c语言来说是一个指针,不过php中没有指针的概念,也不能用常规类型来约束,因此php通过资源类型概念,把c语言中类似文件指针的变量,用zval结构来封装。资源类型值是一个整数,ze会根据这个值去资源的哈希表中获取。  
资源类型的定义:
typedefstruct_zend_rsrc_list_entry { void *ptr; int type; int refcount; }zend_rsrc_list_entry;
其中,ptr是一个指向资源的最终实现的指针,例如一个文件句柄,或者一个数据库连接结构。type是一个类型标记,用于区分不同的资源类型。refcount用于资源的引用计数。
内核中,资源类型是通过函数zend_fetch_resource获取的。
zend_fetch_resource(con, type, zval *, default, resource_name, resource_type);
5. 变量类型的转换按照现在我们对php语言的了解,变量的类型依赖于zval.type字段指示,变量的内容按照zval.type存储到zval.value。当php中需要变量的时候,只需要两个步骤:把zval.value的值或指针改变,再改变zval.type的类型。不过对于php的一些高级变量array/object/resource,变量转换要进行更多操作。
变量转换原理分为3种:
5.1 标准类型相互转换比较简单,按照上述的步骤转化即可。
5.2 标准类型与资源类型转换资源类型可以理解为是int,比较方便转换标准类型。转换后资源会被close或回收。
php $var = fopen('/tmp/aaa.txt', 'a'); // 资源 #1 $var = (int) $var; var_dump($var); // 输出1 ?>
5.3 标准类型与复杂类型转换
array转换整型int/浮点型float会返回元素个数;转换bool返回array中是否有元素;转换成string返回'array',并抛出warning。
详细内容取决于经验,请阅读php手册: http://php.net/manual/en/language.types.type-juggling.php
5.4 复杂类型相互转换array和object可以互转。如果其它任何类型的值被转换成对象,将会创建一个内置类stdclass的实例。
在我们写php扩展的时候,php内核提供了一组函数用于类型转换:  
void convert_to_long(zval* pzval)
void convert_to_double(zval* pzval)
void convert_to_long_base(zval* pzval, int base)
void convert_to_null(zval* pzval)
void convert_to_boolean(zval* pzval)
void convert_to_array(zval* pzval)
void convert_to_object(zval* pzval)
void convert_object_to_type(zval* pzval, convert_func_t converter)
php内核提供的一组宏来方便的访问zval,用于更细粒度的获取zval的值:
内核访问zval容器的api
宏 访问变量
z_lval(zval) (zval).value.lval
z_dval(zval) (zval).value.dval
z_strval(zval) (zval).value.str.val
z_strlen(zval) (zval).value.str.len
z_arrval(zval) (zval). value.ht
z_type(zval) (zval).type
z_lval_p(zval) (*zval).value.lval
z_dval_p(zval) (*zval).value.dval
z_strval_p(zval_p) (*zval).value.str.val
z_strlen_p(zval_p) (*zval).value.str.len
z_arrval_p(zval_p) (*zval). value.ht
z_obj_ht_p(zval_p) (*zval).value.obj.handlers
z_lval_pp(zval_pp) (**zval).value.lval
z_dval_pp(zval_pp) (**zval).value.dval
z_strval_pp(zval_pp) (**zval).value.str.val
z_strlen_pp(zval_pp) (**zval).value.str.len
z_arrval_pp(zval_pp) (**zval). value.ht
6. 变量的符号表与作用域php的变量符号表与zval值的映射,是通过hashtable(哈希表,又叫做散列表,下面简称ht),hashtable在ze中广泛使用,包括常量、变量、函数等语言特性都是ht来组织,在php的数组类型也是通过hashtable来实现。
举个例子:
php $var = 'hello world'; ?>
$var的变量名会存储在变量符号表中,代表$var的类型和值的zval结构存储在哈希表中。内核通过变量符号表与zval地址的哈希映射,来实现php变量的存取。
为什么要提作用域呢?因为函数内部变量保护。按照作用域php的变量分为全局变量和局部变量,每种作用域php都会维护一个符号表的hashtable。当在php中创建一个函数或类的时候,ze会创建一个新的符号表,表明函数或类中的变量是局部变量,这样就实现了局部变量的保护--外部无法访问函数内部的变量。当创建一个php变量的时候,ze会分配一个zval,并设置相应type和初始值,把这个变量加入当前作用域的符号表,这样用户才能使用这个变量。
内核中使用zend_set_symbol来设置变量:
zend_set_symbol( eg(active_symbol_table), foo, foo);
查看_zend_executor_globals结构
zend/zend_globals.h struct _zend_executor_globals { //略 hashtable symbol_table;//全局变量的符号表 hashtable *active_symbol_table;//局部变量的符号表 //略 };
在写php扩展时候,可以通过eg宏来访问php的变量符号表。eg(symbol_table)访问全局作用域的变量符号表,eg(active_symbol_table)访问当前作用域的变量符号表,局部变量存储的是指针,在对hashtable进行操作的时候传递给相应函数。
为了更好的理解变量的哈希表与作用域,举个简单的例子:
php $temp = 'global'; function test() { $temp = 'active'; } test(); var_dump($temp); ?>
创建函数外的变量$temp,会把这个它加入全局符号表,同时在全局符号表的hashtable中,分配一个字符类型的zval,值为‘global‘。创建函数test内部变量$temp,会把它加入属于函数test的符号表,分配字符型zval,值为’active' 。
7. php扩展中变量操作创建php变量我们可以在扩展中调用函数make_std_zval(pzv)来创建一个php可调用的变量,make_std_zval应用到的宏有:
#define make_std_zval(zv) alloc_zval(zv);init_pzval(zv) #define alloc_zval(z) zend_fast_alloc(z, zval, zval_cache_list) #define zend_fast_alloc(p, type, fc_type) (p) = (type *) emalloc(sizeof(type)) #define init_pzval(z) (z)->refcount__gc = 1;(z)->is_ref__gc = 0;
make_std_zval(foo)展开后得到:
(foo) = (zval *) emalloc(sizeof(zval)); (foo)->refcount__gc = 1; (foo)->is_ref__gc = 0;
可以看出,make_std_zval做了三件事:分配内存、初始化zval结构中的refcount、is_ref。 
内核中提供一些宏来简化我们的操作,可以只用一步便设置好zval的类型和值。
api macros for accessing zval 
宏 实现方法
zval_null(pvz) z_type_p(pzv) = is_null
zval_bool(pvz) z_type_p(pzv) = is_bool;
z_bval_p(pzv) = b ? 1 : 0;
zval_true(pvz) zval_bool(pzv, 1);
zval_false(pvz) zval_bool(pzv, 0);
zval_long(pvz, l)(l 是值) z_type_p(pzv) = is_long;z_lval_p(pzv) = l;
zval_double(pvz, d) z_type_p(pzv) = is_double;z_lval_p(pzv) = d;
zval_stringl(pvz, str, len, dup) z_type_p(pzv) = is_string;z_strlen_p(pzv) = len;
if (dup) {
    {z_strval_p(pzv) =estrndup(str, len + 1);} 
}else {
    {z_strval_p(pzv) = str;}
}
zval_string(pvz, str, len) zval_stringl(pzv, str,strlen(str), dup);
zval_resource(pvz, res) z_type_p(pzv) = is_resource;z_resval_p(pzv) = res;
zval_stringl(pzv,str,len,dup)中的dup参数
先阐述一下zval_stringl(pzv,str,len,dup); str和len两个参数很好理解,因为我们知道内核中保存了字符串的地址和它的长度,后面的dup的意思其实很简单,它指明了该字符串是否需要被复制。值为 1 将先申请一块新内存并赋值该字符串,然后把新内存的地址复制给pzv,为 0 时则是直接把str的地址赋值给zval。
zval_stringl与zval_string的区别
如果你想在某一位置截取该字符串或已经知道了这个字符串的长度,那么可以使用宏 zval_stringl(zval, string, length, duplicate) ,它显式的指定字符串长度,而不是使用strlen()。这个宏该字符串长度作为参数。但它是二进制安全的,而且速度也比zval_string快,因为少了个strlen。
zval_resource约等于zval_long
在章节4中我们说过,php中的资源类型的值是一个整数,所以zval_resource和zval_long的工作差不多,只不过它会把zval的类型设置为 is_resource。
8. 总结php的弱类型是通过ze的zval容器转换完成,通过哈希表来存储变量名和zval数据,在运行效率方面有一定牺牲。另外因为变量类型的隐性转换,在开发过程中对变量类型检测力度不够,可能会导致问题出现。 
不过php的弱类型、数组、内存托管、扩展等语言特性,非常适合web开发场景,开发效率很高,能够加快产品迭代周期。在海量服务中,通常瓶颈存在于数据访问层,而不是语言本身。在实际使用php不仅担任逻辑层和展现层的任务,我们甚至用php开发的udpserver/tcpserver作为数据和cache的中间层。
关于作者:王帅,腾讯企业qq saas团队leader。
专访企业qq saas团队,谈企业级lnmp架构设计
更多《问底》内容
【问底】严澜:数据挖掘入门(一)——分词 【问底】yao yu谈twitter的百tb级redis缓存实践 【问底】王帅:深入php内核(一)——弱类型变量原理探究
《问底》是csdn云计算频道新建栏目,以实践为本,分享个人对于新时代软件架构与研发的深刻见解。在含有“【问底】”字样标题的文章中,你会看到某个国外it巨头的架构分享,会看到国内资深工程师对某个技术的实践总结,更会看到一系列关于某个新技术的探索。《问底》邀请对技术具有独特/深刻见解的你一起打造一片只属于技术的天空,详情可邮件至zhonghao@csdn.net。
免费订阅“csdn云计算(左)和csdn大数据(右)”微信公众号,实时掌握第一手云中消息,了解最新的大数据进展!
csdn发布虚拟化、docker、openstack、cloudstack、数据中心等相关云计算资讯,     分享hadoop、spark、nosql/newsql、hbase、impala、内存计算、流计算、机器学习和智能算法等相关大数据观点,提供云计算和大数据技术、平台、实践和产业信息等服务。
其它类似信息

推荐信息