PHP-Zend引擎剖析之CV变量

来源:互联网 发布:百会软件 编辑:程序博客网 时间:2024/04/30 21:01

讨论完虚拟机的基本流程后,接下来就是把细节的地方揪干净,接下来几篇文章先揪一下PHP的变量。我们知道PHP的变量是弱类型的,但是我并不打算从这个开始分析,我先把最简单的赋值语句:<?php $var = 1;?>开始分析整个变量赋值的过程,同时解释一下PHP的CV(compiled variable)变量。

从上一篇《PHP-Zend引擎剖析之Hello World(二)》知道每条语法规则从编译到运行需要经历以下两个步骤:
  1. 词法分析,接着语法分析匹配中规则
  2. 虚拟机执行一条opcode涉及到几个东西:opcode的操作数op1,op2以及opcode的返回结果result(三个参数对应的类型);opcode对应的handler。

我们先看一下在zend_language_parser.y里边是如何书写赋值语句的语法规则(在没有任何说明的情况下,本文讨论的仅仅是:$var = 1;这样一条简单的赋值语法规则)。

赋值语句的语法规则

去除一些不想关的代码,赋值语句的语法规则如下:

expr_without_variable:| variable '=' expr { zend_check_writable_variable(&$1); zend_do_assign(&$$, &$1, &$3 TSRMLS_CC); }base_variable_with_function_calls:      base_variable  { $$ = $1; }base_variable:  reference_variable { $$ = $1; $$.EA = ZEND_PARSED_VARIABLE; };reference_variable:| compound_variable { zend_do_begin_variable_parse(TSRMLS_C); fetch_simple_variable(&$$, &$1, 1 TSRMLS_CC); };compound_variable:  T_VARIABLE               { $$ = $1; }

从以上的规则中我们可以分析到,”var”字符串(因此词法分析中解析到$var时会返回字符串”var”)匹配中了compound_variable,$var变量匹配中了reference_variable(留意这两句话的差别,同时我们先忽略expr匹配到常数1的情况(之后分析表达式的时候再回过头看expr)。
首先我们关注一下在compound_variable这条规则里边,为什么$1就是字符串”var”呢?
这里有点迷惑的是,在zend_language_parser.y里边定义了$$,$1这些参数是znode类型的:#define YYSTYPE znode
而词法扫描阶段解析出来的是一个zval*类型的变量:ZEND_API int lex_scan(zval *zendlval TSRMLS_DC);
这里当中做了什么事情?
在语法分析器调用词法分析器获得Token时,是调用一个叫做YYLEX的宏来做词法扫描的,其定义在zend_language_parser.c文件里边:
紧接着我们发现了yylex也是一个宏:#define yylex zendlex
在zend_compile.c里边,我们终于找到了从zval*类型(词法分析阶段解析的变量类型)到znode类型(语法阶段中的节点)的转换:
其实逻辑非常的简单,就是调lex_scan来扫描Token,获得词法分析阶段得到的变量,然后复制给znode节点的u.constant!再设置其类型为IS_CONST(我的理解就是:词法阶段识别出来的都是常量!因为不涉及运行时)
接着,我们再关注一下reference_variable这条规则做的事情,在这里我们只关心fetch_simple_variable做了什么事情。
在$PHPSRC/Zend/zend_compile.c里边:
result以及varname是什么呢?从语法规则来看,result是reference_variable的返回值,varname就是compound_variable的返回值。注意到刚刚说过的那句话:
“var”字符串匹配中了compound_variable,$var变量匹配中了reference_variable
于是我们知道了其实varname就是”var”这个字符串,而fetch_simple_variable这个函数就是要生成名字为”var”的变量!
我们可以看到fetch_simple_variable最后是调用了fetch_simple_variable_ex(定义在zend_compile.c的第653行)来生成变量的,这里剔除一些不相关的代码之后,fetch_simple_variable_ex的定义如下所示:
首先我们需要知道varname这个节点的哪个属性是字符串”var”,从以上代码可以看到,varname->u.constant.value.str就是字符串”var”!(这里涉及到PHP弱类型对应的数据结构,我们先忽略这里的细节专心看看如何Zend引擎是怎么生成变量的)。
接着我们看到了如果这个变量不是$this的话!生成的变量(也就是返回值result指针!)的类型就是IS_CV,同时会使用lookup_cv去寻找在当前作用域下这个变量有没有定义过(如果没有,就会在当前作用域下定义它,一会看一下lookup_cv的实现就知道了,:))。
一开始我也很奇怪IS_CV是什么类型变量,我们vld来看一下生成的opcde:

php -dvld.active=1 -dvld.verbosity=3 var.php

我们可以额看到对于赋值语句的第一个操作数op1它旁边有个IS_CV。从刚刚执行过程下来,最后使用到一个叫做lookup_cv的函数去搜索变量的,在$PHPSRC/Zend/zend_compile.c有其定义:
直接上两个图解释这里的数据结构以及流程:
为什么PHP不干脆点,直接把变量丢到Hash表去?这里也是我一开始迷惑的地方,不过一个非常简单的道理就是:数组的随机访问肯定比Hash表的访问要快!
但是使用数组有一个缺点,就是当数组不够用的时候,需要去内存在申请一段空间,然后把旧数组拷贝过去。
从源码里边我们可以看到,当CV列表不够用的时候,会扩充多16个节点,也即是说如果为了效率的话,我们使用的局部变量(大多都是CV变量)最好不要超过16个,否则就会消耗一次CV表的拷贝迁移数据。
可见PHP源码的开发人员可能做了一个假设(也或者已经调研过):一般的PHP程序员很少使用过多的局部变量。
于是,我们已经通过语法分析在对应的表中生成了名字为var的变量,此时还没赋值!也即是还没绑定其opcode以及opcode的handler处理。

CV变量的赋值

回过头我们看一下在赋值的这条语法规则里边做了啥:


expr_without_variable:
|     variable ‘=’ expr          { zend_check_writable_variable(&$1); zend_do_assign(&$$, &$1, &$3 TSRMLS_CC); }

首先使用zend_check_writable_variable检查一下variable是不是可写的(否则就报错),接着把variable以及expr丢给zend_do_assign去处理。
zend_do_assign定义在zend_compile.c的921行,去掉一些不想关的代码:
首先用get_next_op往当前的opcode列表生成一条opcode,接着看一下当前的变量是不是指向了$this变量,如果是就报错(显然我们不能这样做:$var = $this; $var = 1;)。
接着设置opcode为ZEND_ASSIGN,设置opcode的操作数op1,op2以及返回值result。
紧接着需要绑定这种opcode类型的handler,源码位于zend_vm_def.h里边的1717行,去除不相关的代码:
留意一下在最后调用了FREE_OP1_VAR_PTR()以及FREE_OP2_VAR_PTR(),这里就涉及到PHP的垃圾回收机制了,先忽略这块。

此时的variable_ptr_ptr还需要通过GET_OP1_ZVAL_PTR_PTR来获得CV变量的指针:

会先通过CV_DEF_OF宏先找一下在当前的CV列表里边有没有,没有的话就会去当前作用域的Hash表里边查询。

最后调用到zend_assign_const_to_variable来赋值:$var = 1;(定义在zend_execute.c的847行):

so,CV变量的赋值过程结


IS_TMP_VAR:一个临时变量,通常是一些像表达$ foo的+ $ bar的结果。临时变量不能共享,所以他们没有实现引用计数。它们通常非常短暂,因此一个指令创建和下一个已再次释放他们。临时变量通常被写入,所以〜N 〜0将是第一个临时变量,〜1第二,等
IS_CV:编译变量。为了节省哈希表查找PHP缓存简单的变量,如$ foo的一个数组(阵列)的位置。此外编译变量允许PHP优化哈希表,离完全。简历都表示使用!列印(列印这里编译变量数组偏移。)
IS_VAR:只有简单的变量可以转向到CVS。变量访问的所有其他种类,如$ foo的'酒吧']或$ foo的>酒吧返回IS_VAR变量。它基本上只是一个普通的zval(引用计数和一切)。瓦尔写成$ N。
IS_CONST:常量在代码中的文字。例如,如果你写的“富”或3.141在您的代码,这些类型IS_CONST。常量允许一些进一步的优化,如重复使用相同的值的zvals,或预先计算哈希。

0 0