[置顶] PHP内核探索之变量(2)-理解引用
本文主要内容:
1、引论很久之前写了1篇关于援用的文章,当时写的寥寥草草,很多原理都没有说清楚。最近在翻阅Derick Rethans(home: http://derickrethans.nl/ Github: https://github.com/derickr)大牛之前做的报告时,发现了1篇讲授PHP援用机制的文章,也就是这个PDF.文中从zval和符号表的角度讲授了援用计数、援用传参、援用返回、全局参数等的原理,洋洋洒洒,图文并茂,甚是精彩,建议童鞋们有时间都读读原版,相信会有很多的收获。 空话不多说,接着说今天的正题。 我们知道,很多语言都提供了援用的机制,援用可让我们使用不同的名字(或符号)访问一样的内容。PHP手册中对援用的定义是:"在PHP中援用意味着用不同的名字访问同1个变量内容。这其实不像C的指针,替换的是,援用是符号表别名。",换句话说,援用实现了某种情势的"绑定"。例如我们常常碰到的这类面试题,便是援用的典范: $a = array(1,2,3,4);
foreach($a as &$v){
$v *= $v;
}
foreach($a as $v){
echo $v;
} 抛开本题的输出不谈,我们今天就跟随Derick Rethans先辈的脚步,1步1步去揭开援用的神秘面纱。 2、 符号表和zval在开始援用的原理之前,我们有必要对文中反复出现的术语做个简单的说明,其中最主要也最重要的便是: 1.符号表 2.zval. 1. 符号表 计算机语言是人与机器交换的工具,但不幸的是,我们赖以生存和引以为傲的高级语言却没法直接在计算机上履行,由于计算机只能理解某种情势的机器语言。这意味着,高级语言必须要经过编译(或解释)进程才能被计算机理解和履行。在这其间,要经过词法分析、语法分析、语义分析、中间代码生成和优化等很多复杂的进程,而这些进程中,编译程序可能要反复用到源程序中出现的标识符等信息(例如变量的类型检查、语义分析阶段的语义检查),这些信息便是保存在不同的符号表中的。符号表保存了源程序中标识符的名字和属性信息,这些信息可能包括:类型、存储类型、作用域、存储分配信息和其他1些额外信息等。为了高效的插入和查询符号表项,很多编译器的符号表都使用Hashtable来实现。我们可以简单的理解为:符号表就是1个保存了符号名和该符号的各类属性的hashtable或map。例如,对程序: $str = 'this is a test';
function foo( $a,$b ){
$tmp = 12;
return $tmp + $a + $b;
}
function to(){
} 1个可能的符号表(并不是实际的符号表)是类似这样的结构: 我们其实不去关注符号表的具体结构,只需要知道:每一个函数、类、命名空间等都有自己的独立的符号表(与全局的符号表分开)。说到这里,突然想起来1件事情,最开始使用PHP编程的时候,在读extract()函数的手册时,对"从数组中将变量导入到当前的符号表"这句话的含义百思不得其解,更是对先辈们所说的"不建议使用extract($_POST)和extract($_GET)提取变量"的建议万分苦恼。实际上,extract的滥用不但会有严重的安全性问题,而且会污染当前的符号表( active symbol table)。 那末active symbol table又是甚么东西呢? 我们知道,PHP代码的履行进程中,几近都是从全局作用域开始,顺次扫描,顺序履行。如果遇到函数调用,则进入该函数的内部履行,该函数履行终了以后会返回到调用程序继续履行。这意味着,必须要有某种机制用于辨别不同阶段所要使用的符号表,否则就会造成编译和履行的错乱。Active symbol table便是用于标志当前活动的符号表(这时候应当最少存在着全局的global symbol table和活动的active symbol table,通常情况下,active symbol table就是指global symbol table)。符号表其实不是1开始就建立好的,而是随着编译程序的扫描不断添加和更新的。在进入函数调用时,zend(PHP的语言解释引擎)会创建该函数的符号表,并将active symbol table指向该符号表。也就是说,在任意时刻使用的的符号表都应当是当前的active symbol table。 以上就是符号表的全部内容了,我们简单抽离1下其中的关键内容:
更多的资料可以查看: 1. http://www.scs.stanford.edu/11wi-cs140/pintos/specs/sysv-abi-update.html/ch4.symtab.html 2. http://arantxa.ii.uam.es/~modonnel/Compilers/04_SymbolTablesI.pdf 2. Zval 在上1篇博客(PHP内核探索之变量(1)Zval)中,我们已对zval的结构和基本原理有了1些了解。对zval不了解的童鞋可以先看看。为了方便浏览,我们再次贴出zval的结构: struct _zval_struct { zvalue_value value; /* value */ zend_uint refcount__gc; /* variable ref count */ zend_uchar type; /* active type */ zend_uchar is_ref__gc; /* if it is a ref variable */ }; typedef struct _zval_struct zval; 3、援用1. 援用计数 正如上节所言,zval是PHP变量底层的真正容器,为了节省空间,其实不是每一个变量都有自己独立的zval容器,例如对赋值(assign-by-value)操作:$a = $b(假定$b,$a都不是援用型变量),Zend其实不会为$b变量开辟新的空间,而是将符号表中a符号和b符号指向同1个zval。只有在其中1个变量产生变化时,才会履行zval分离的操作。这被称为COW(Copy-on-write)的机制,可以在1定程度上节省内存和提高效力。 为了实现上述机制,需要对zval的援用状态做标记,zval的结构中,refcount__gc便是用于计数的,这个值记录了有多少个变量指向该zval,在上述赋值操作中,$a=$b,会增加原始的$b的zval的refcount值。关于这1点,上次(PHP内核探索之变量(1)Zval)已做了详细的解释,这里不再赘述。 2. 函数传参 在脚本履行的进程中,全局的符号表几近是1直存在的,但除这个全局的global symbol table,实际上还会生成其他的symbol table:例如函数调用的进程中,Zend会创建该函数的内部symbol table,用于寄存函数内部变量的信息,而在函数调用结束后,会删除该symbol table。我们接下来以1个简单的函数调用为例,介绍1下在传参的进程中,变量和zval的状态变化,我们使用的测试脚本是: function do_zval_test($s){
$s = "change ";
return $s;
}
$a = "before";
$b = do_zval_test($a); 我们来逐渐分析: (1). $a = "before"; 这会为$a变量开辟1个新的zval(refcount=1,is_ref=0),以下所示: (2). 函数调用do_zval_test($a) 由于函数的调用,Zend会为do_zval_test这个函数创建单独的符号表(其中包括该函数内部的符号s),同时,由于$s实际上是函数的形参,因此其实不会为$s创建新的zval,而是指向$a的zval。这时候,$a指向的zval的refcount应当为3(分别是$a,$s和函数调用堆栈): a: (refcount=3,is_ref=0)='before func' 以下图所示: (3).函数内部履行$s = "change " 由于$s的值产生了改变,因此会履行zval分离,为s专门copy生成1个新的zval: (4).函数返回 return $s ; $b = do_zval_test($a). $b与$s同享zval(暂时),准备烧毁函数中的符号表: (5). 烧毁函数中的符号表,回到Global环境中: 这里我们顺便说1句,在你使用debug_zval_dump()等函数查看zval的refcount时,会令zval本身的refcount值加1,所以实际的refcount的值应当是打印出的refcount减1,以下所示: $src = "string";
debug_zval_dump($src); 结果是: string(6) "string" refcount(2)
3. 援用初探 同上,我们还是直接上代码,然后1步步分析(这个例子比较简单,为了完全性,我们还是略微分析1下): $a = "simple test";
$b = &a;
$c = &a;
$b = 42;
unset($c);
unset($b); 则变量与zval的对应关系以下图所示:(因而可知,unset的作用仅仅是将变量从符号表中删除,并减少对应zval的refcount值) 上图中值得注意的最后1步,在unset($b)以后,zval的is_ref值又变成了0。 那如果是混合了援用(assign-by-reference)和普通赋值(assign-by-value)的脚本,又是甚么情况呢? 我们的测试脚本: (1). 先普通赋值后援用赋值 $a = "src";
$b = $a;
$c = &$b; 具体的进程见下图: (2). 先援用赋值后普通赋值 $a = "src";
$b = &$a;
$c = $a; 具体进程见下图: 4. 传递援用 一样,向函数传递的参数也能够以援用的情势传递,这样可以在函数内部修改变量的值。作为实例,我们仍使用2(函数传参)中的脚本,只是参数改成援用的情势: function do_zval_test(&$s){
$s = "after";
return $s;
}
$a = "before";
$b = do_zval_test($a); 这与上述函数传参进程基本1致,不同的是,援用的传递使得$a的值产生了变化。而且,在函数调用结束以后 $a的is_ref恢复成0: 可以看出,与普通的值传递相比,援用传递的不同在于: (1) 第3步 $s = "change";时,并没有为$s新建1个zval,而是与$a指向同1个zval,这个zval的is_ref=1。 (2) 还是第3步。$s = "change";履行后,由于zval的is_ref=1,因此,间接的改变了$a的值 5. 援用返回 PHP支持的另外一个特性是援用返回。我们知道,在C/C++中,函数返回值时,实际上会生成1个值的副本,而在援用返回时,其实不会生成副本,这类援用返回的方式可以在1定程度上节省内存和提高效力。而在PHP中,情况其实不完全是这样。那末,究竟甚么是援用返回呢?PHP手册上是这么说的:"援用返回用在当想用函数找到援用应当被绑定在哪个变量上面时",是否是1头雾水,完全不知所云?其实,英文手册上是这样描写的"Returning by reference is useful when you want to use a function to find to which variable a reference should be bound"。提取文中的主干和关键点,我们可以得到这样的信息: (1). 援用返回是将援用绑定在1个变量上。 (2). 这个变量不是肯定的,而是通过函数得到的(否者我们就能够使用普通的援用了)。 这其实也说明了援用返回的局限性:函数必须返回1个变量,而不能是1个表达式,否者就会出现类似下面的问题: PHP Notice: Only variable references should be returned by reference in xxx(参看PHP手册中的Note). 那末,援用返回时如何工作的呢?例如,对以下的例子: function &find_node($key,&$tree){
$item = &$tree[$key];
return $item;
}
$tree = array(1=>'one',2=>'two',3=>'three');
$node =& find_node(3,$tree);
$node ='new'; Zend都做了哪些工作呢?我们1步步来看。 (1). $tree = array(1=>'one',2=>'two',3=>'three') 同之前1样,这会在Global symbol table中添加tree这个symbol,并生成该变量的zval。同时,为数组$tree的每一个元素都生成相应的zval: tree: (refcount=1,is_ref=0)=array ( 1 => (refcount=1,is_ref=0)='one',2 => (refcount=1,is_ref=0)='two',3 => (refcount=1,is_ref=0)='three' ) 以下图所示: (2). find_node(3,&$tree) 由于函数调用,Zend会进入函数的内部,创建该函数的内部symbol table,同时,由于传递的参数是援用参数,因此zval的is_ref被标志为1,而refcount的值增加为3(分别是全局tree,内部tree和函数堆栈): (3)$item = &$tree[$key]; 由于item是$tree[$key]的援用(在本例的调用中,$key是3),因此更新$tree[$key]指向zval的is_ref和refcount值: (4)return $item,并履行援用绑定:
(5)函数返回,烧毁局部符号表。 tree对应的zval的is_ref恢复了0,refcount=1,$tree[3]被绑定在了$node变量上,对该变量的任何改变都会间接更改$tree[3]:
(6) 更改$node的值,会反射到$tree的节点上,$node ='new': Note:为了使用援用返回,必须在函数定义和函数调用的地方都显式的使用&符号。 6. Global关键字 PHP中允许我们在函数内部使用Global关键字援用全局变量(不加global关键字时援用的是函数的局部变量),例如: $var = "outside";
function inside()
{
$var = "inside";
echo $var;
global $var;
echo $var;
}
inside();
输出为insideoutside 我们只知道global关键字建立了1个局部变量和全局变量的绑定,那末具体机制是甚么呢? 使用以下的脚本测试: $var = "one";
function update_var($value){
global $var;
unset($var);
global $var;
$var = $value;
}
update_var('four');
echo $var;
具体的分析进程为: (1).$var = 'one'; 同之前1样,这会在全局的symbol table中添加var符号,并创建相应的zval: (2).update_var('four') 由于直接传递的是string而不是变量,因此会创建1个zval,该zval的is_ref=0,ref_count=2(分别是形参$value和函数的堆栈),以下所示: (3)global $var global $var这句话,实际上会履行两件事情: (1).在函数内部的符号表中插入局部的var符号 (2).建立局部$var与全局变量$var之间的援用. (4)unset($var); 这里要注意的是,unset只是删除函数内部符号表中var符号,而不是删除全局的。同时,更新原zval的refcount值和is_ref援用标志(援用解绑): (5).global $var 同3,再次建立局部$var与全局的$var的援用: (6)$var = $value; 更改$var对应的zval的值,由于援用的存在,全局的$var的值也随之改变: (7)函数返回,烧毁局部符号表(又回到最初的出发点,但,1切已大不1样了): 据此,我们可以总结出global关键字的进程和特性:
4、回到最初的问题现在,我们对援用已有了1个基本的认识。让我们回到最初的问题: $a = array(1,3);
foreach($a as &$v){
$v *= $v;
}
foreach($a as $v){
echo $v;
} 这当中,究竟产生了甚么事情呢? (1).$a = array(1,3); 这会在全局的symbol table中生成$a的zval并且为每一个元素也生成相应的zval: (2). foreach($a as &$v) {$v *= $v;} 这里由因而援用绑定,所以相当于对数组中的元素履行: $v = &$a[0]; $v = &$a[1]; $v = &$a[2]; 履行进程以下: 我们发现,在这次的foreach履行终了以后,$v = &$a[2]. (3)第2次foreach循环 foreach($a as $v){
echo $v;
} 这次由于是普通的assign-by-value的赋值情势,因此,类似与履行: $v = $a[0]; $v = $a[1]; $v = $a[2]; 别忘了$v现在是$a[2]的援用,因此,赋值的进程会间接更改$a[2]的值。 进程以下: 因此,输出结果应当为144. 附:本文中的zval的调试方法。 如果要查看某1进程中zval的变化,最好的办法是在该进程的前后均加上调试代码。例如 $a = 123;
xdebug_debug_zval('a');
$b=&$a;
xdebug_debug_zval('a'); 配合画图,可以得到1个直观的zval更新进程。 参考文献:
由于写作匆忙,文中难免会有毛病的地方,欢迎指出探讨。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |