前言
讨论完虚拟机的基本流程后,接下来就是把细节的地方揪干净,接下来几篇文章先揪一下PHP的变量。我们知道PHP的变量是弱类型的,但是我并不打算从这个开始分析,我先把最简单的赋值语句:<?php $var = 1;?>开始分析整个变量赋值的过程,同时解释一下PHP的CV(compiled variable)变量。
从上一篇《PHP-Zend引擎剖析之Hello World(二)》知道每条语法规则从编译到运行需要经历以下两个步骤:
- 词法分析,接着语法分析匹配中规则
- 虚拟机执行一条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); }
| 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变量的赋值过程结束,下几篇文章应该会剖析一下:
- $var[0],$var->abc这样的属性赋值
- array类型变量
- PHP的弱类型机制
本文链接:PHP-Zend引擎剖析之CV变量(三)
转载声明:本博客文章若无特别说明,皆为原创,转载请注明来源:拉风的博客,谢谢!
赞一个,期待楼主更新