博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
php源码之路第三章第六节( 变量的生命周期之变量的赋值和销毁)
阅读量:2353 次
发布时间:2019-05-10

本文共 8071 字,大约阅读时间需要 26 分钟。

我们知道了PHP中变量的存储方式——所有的变量都保存在zval结构中。下面我们将介绍一下PHP内核如何实现变量的定义方式以及作用域。变量的生命周期在ZE进行词法和语法的分析之后,生成具体的opcode,这些opcode最终被execute函数(Zend/zend_vm_execute.h:46)解释执行。在excute函数中,有以下代码:
while (1) {     ...     if ((ret = EX(opline)->handler(execute_data TSRMLS_CC)) > 0) {        switch (ret) {            case 1:                EG(in_execution) = original_in_execution;                return;            case 2:                op_array = EG(active_op_array);                goto zend_vm_enter;            case 3:                execute_data = EG(current_execute_data);            default:                break;        }         }         ...}
这里的EX(opline)->handler(...)将op_array中的操作顺序执行,其中变量赋值操作在ZEND_ASSIGN_SPEC_CV_CONST_HANDLER()函数中进行。 ZEND_ASSIGN_SPEC_CV_CONST_HANDLER中进行一些变量类型的判断并在内存中分配一个zval,然后将变量的值存储其中。变量名和指向这个zval的指针,则会存储于符号表内。 ZEND_ASSIGN_SPEC_CV_CONST_HANDLER的最后会调用ZEND_VM_NEXT_OPCODE()将op_array的指针移到下一条opline,这样就会形成循环执行的效果。在ZE执行的过程中,有四个全局的变量,这些变量都是用于ZE运行时所需信息的存储:
//_zend_compiler_globals 编译时信息,包括函数表等zend_compiler_globals    *compiler_globals;  //_zend_executor_globals 执行时信息zend_executor_globals    *executor_globals; //_php_core_globals 主要存储php.ini内的信息php_core_globals         *core_globals; //_sapi_globals_struct SAPI的信息sapi_globals_struct      *sapi_globals;
在执行的过程中,变量名及指针主要存储于_zend_executor_globals的符号表中,_zend_executor_globals的结构这样的:
struct _zend_executor_globals {.../* symbol table cache */HashTable *symtable_cache[SYMTABLE_CACHE_SIZE];HashTable **symtable_cache_limit;HashTable **symtable_cache_ptr;zend_op **opline_ptr;HashTable *active_symbol_table;  /* active symbol table */HashTable symbol_table;     /* main symbol table */HashTable included_files;   /* files already included */...}
在执行的过程中,active_symbol_table会根据执行的具体语句不断发生变化(详请见本节下半部分),针对线程安全的EG宏就是用来取此变量中的值。 ZE将op_array执行完毕以后,HashTable会被FREE_HASHTABLE()释放掉。     如果程序使用了unset语句来主动消毁变量,则会调用ZEND_UNSET_VAR_SPEC_CV_HANDLER来将变量销毁,回收内存。变量的赋值和销毁在强类型的语言当中,当使用一个变量之前,我们需要先声明这个变量。然而,对于PHP来说,在使用一个变量时,我们不需要声明,也不需要初始化,直接对其赋值就可以使用,这是如何实现的?变量的声明和赋值在PHP中没有对常规变量的声明操作,如果要使用一个变量,直接进行赋值操作即可。在赋值操作的同时已经进行声明操作。一个简单的赋值操作:
$a = 10;
使用VLD扩展查看其生成的中间代码为 ASSIGN。依此,我们找到其执行的函数为 ZEND_ASSIGN_SPEC_CV_CONST_HANDLER。(找到这个函数的方法之一:$a为CV,10为CONST,操作为ASSIGN。其他方法可以参见opcode处理函数查找) CV是PHP在5.1后增加的一个在编译期的缓存。如我们在使用VLD查看上面的PHP代码生成的中间代码时会看到:
compiled vars:  !0 = $a
这个$a变量就是op_type为IS_CV的变量。IS_CV值的设置是在语法解析时进行的。参见Zend/zend_complie.c文件中的zend_do_end_variable_parse函数。在这个函数中,获取这个赋值操作的左值和右值的代码为:
zval *value = &opline->op2.u.constant;zval **variable_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts), BP_VAR_W TSRMLS_CC);
由于右值为一个数值,我们可以理解为一个常量,则直接取操作数存储的constant字段,关于这个字段的说明将在后面的虚拟机章节说明。左值是通过 _get_zval_ptr_ptr_cv函数获取zval值。
static zend_always_inline zval **_get_zval_ptr_ptr_cv(const znode *node, const temp_variable *Ts, int type TSRMLS_DC){    zval ***ptr = &CV_OF(node->u.var);    if (UNEXPECTED(*ptr == NULL)) {        return _get_zval_cv_lookup(ptr, node->u.var, type TSRMLS_CC);    }    return *ptr;}// 函数中的CV_OF宏定义#define CV_OF(i)     (EG(current_execute_data)->CVs[i])
_get_zval_ptr_ptr_cv函数程序会先判断变量是否存在于EX(CVs),如果存在则直接返回,否则调用_get_zval_cv_lookup,通过HastTable操作在EG(active_symbol_table)表中查找变量。虽然HashTable的查找操作已经比较快了,但是与原始的数组操作相比还是不在一个数量级。这就是CV类型变量的性能优化点所在。 CV以数组的方式缓存变量所在HashTable的值,以取得对变量更快的访问速度。如果变量不在EX(CVs)中,程序会调用_get_zval_cv_lookup。从而最后的调用顺序为: [_get_zval_ptr_ptr_cv] --> [_get_zval_cv_lookup] 在_get_zval_cv_lookup函数中关键代码为:
zend_hash_quick_find(EG(active_symbol_table), cv->name, cv->name_len+1, cv->hash_value, (void **)ptr)
这是一个HashTable的查找函数,它的作用是从EG(active_symbol_table)中查找名称为cv->name的变量,并将这个值赋值给ptr。最后,这个在符号表中找到的值将传递给ZEND_ASSIGN_SPEC_CV_CONST_HANDLER函数的variable_ptr_ptr变量。以上是获取左值和右值的过程,在这步操作后将执行赋值操作的核心操作--赋值。赋值操作是通过调用zend_assign_to_variable函数实现。在zend_assign_to_variable函数中,赋值操作分为好几种情况来处理,在程序中就是以几层的if语句体现。情况一:赋值的左值存在引用(即zval变量中is_ref__gc字段不为0),并且左值不等于右值这种形容很抽象,看下面例子:
$a = 10;$b = &$a;xdebug_debug_zval('a');$a = 20;xdebug_debug_zval('a');
试想,如果我们来做这个$b = &$a;的底层实现,我们可能会这样做:判断左值是不是已经被引用过了; 左值已经被引用,则不改变左值的引用计数,将右值赋与左值; 事实上,ZE也是用同样的方法来实现,其代码如下:
if (PZVAL_IS_REF(variable_ptr)) {    if (variable_ptr!=value) {        zend_uint refcount = Z_REFCOUNT_P(variable_ptr);        garbage = *variable_ptr;        *variable_ptr = *value;        Z_SET_REFCOUNT_P(variable_ptr, refcount);        Z_SET_ISREF_P(variable_ptr);        if (!is_tmp_var) {            zendi_zval_copy_ctor(*variable_ptr);        }        zendi_zval_dtor(garbage);        return variable_ptr;    }}
PZVAL_IS_REF(variable_ptr)判断is_ref__gc字段是否为0。在左值不等于右值的情况下执行操作。所有指向这个zval容器的变量的值都变成了*value。并且引用计数的值不变。下面是这种情况的一个示例:上面的例子的输出结果:
a:    (refcount=2, is_ref=1),int 10    a:    (refcount=2, is_ref=1),int 20
情况二:赋值的左值不存在引用,左值的引用计数为1,左值等于右值在这种情况下,应该是什么都不会发生吗?看一个示例:
$a = 10;        $a = $a;
看上去真的像是什么都没有发生,左值的引用计数还是1,值仍是10 。然而在这个赋值过程中,$a的引用计数经历了一次加一和一次减一的操作。如以下代码:
if (Z_DELREF_P(variable_ptr)==0) {  //  引用计数减一操作        if (!is_tmp_var)         {            if (variable_ptr==value) {             Z_ADDREF_P(variable_ptr);                //  引用计数加一操作            }                ...//省略
情况三:赋值的左值不存在引用,左值的引用计数为1,右值存在引用用一个PHP的示例来描述一下这种情况:
$a = 10;        $b = &$a;        $c = $a;
这里的$c = $a;的操作就是我们所示的第三种情况。对于这种情况,ZEND内核直接创建一个新的zval容器,左值的值为右值,并且左值的引用计数为1。也就是说,这种情形$c不会与$a指向同一个zval。其内核实现代码如下:
garbage = *variable_ptr;    *variable_ptr = *value;    INIT_PZVAL(variable_ptr);   //  初始化一个新的zval变量容器    zval_copy_ctor(variable_ptr);       zendi_zval_dtor(garbage);    return variable_ptr;
在这个例子中,若将 $c = $a; 换成 $c = &$a;,$a,$b和$c三个变量的引用计数会发生什么变化?
将 $b = &$a; 换成 $b = $a; 呢?
情况四:赋值的左值不存在引用,左值的引用计数为1,右值不存在引用这种情形如下面的例子:
$a = 10;    $c = $a;
这时,右值的引用计数加上,一般情况下,会对左值进行垃圾收集操作,将其移入垃圾缓冲池。垃圾缓冲池的功能是在PHP5.3后才有的。在PHP内核中的代码体现为:
Z_ADDREF_P(value);  //  引用计数加1*variable_ptr_ptr = value;if (variable_ptr != &EG(uninitialized_zval)) {    GC_REMOVE_ZVAL_FROM_BUFFER(variable_ptr);   //  调用垃圾收集机制    zval_dtor(variable_ptr);    efree(variable_ptr);    //  释放变量内存空间}return value;
情况五:赋值的左值不存在引用,左值的引用计数为大于0,右值存在引用,并且引用计数大于0    一个演示这种情况的PHP示例:
$a = 10;$b = $a;$va = 20;$vb = &$va;$a = $va;
最后一个操作就是我们的情况五。使用xdebug看引用计数发现,最终$a变量的引用计数为1,$va变量的引用计数为2,并且$va存在引用。从源码层分析这个原因:
ALLOC_ZVAL(variable_ptr);   //  分配新的zval容器*variable_ptr_ptr = variable_ptr;*variable_ptr = *value;zval_copy_ctor(variable_ptr);Z_SET_REFCOUNT_P(variable_ptr, 1);  //  设置引用计数为1
从代码可以看出是新分配了一个zval容器,并设置了引用计数为1,印证了我们之前的例子$a变量的结果。    除上述五种情况之外,zend_assign_to_variable函数还对全部的临时变量做了处理。变量赋值的各种操作全部由此函数完成。变量的销毁在PHP中销毁变量最常用的方法是使用unset函数。 unset函数并不是一个真正意义上的函数,它是一种语言结构。在使用此函数时,它会根据变量的不同触发不同的操作。一个简洁的例子:
$a = 10;unset($a);
使用VLD扩展查看其生成的中间代码:
compiled vars:  !0 = $aline     # *  op      fetch  ext  return  operands---------------------------------------------------------------------------------  2     0  >   EXT_STMT         1     ASSIGN                      !0, 10  3      2      EXT_STMT         3      UNSET_VAR                   !0                              4    > RETURN                       1
去掉关于赋值的中间代码,得到unset函数生成的中间代码为 UNSET_VAR,由于我们unset的是一个变量,在Zend/zend_vm_execute.h文件中查找到其最终调用的执行中间代码的函数为: ZEND_UNSET_VAR_SPEC_CV_HANDLER 关键代码代码如下:
target_symbol_table = zend_get_target_symbol_table(opline, EX(Ts),        BP_VAR_IS, varname TSRMLS_CC);    if (zend_hash_quick_del(target_symbol_table, varname->value.str.val,            varname->value.str.len+1, hash_value) == SUCCESS) { //  删除HashTable元素        zend_execute_data *ex = execute_data;        do {            int i;            if (ex->op_array) {                for (i = 0; i < ex->op_array->last_var; i++) {                    if (ex->op_array->vars[i].hash_value == hash_value &&                        ex->op_array->vars[i].name_len == varname->value.str.len &&                        !memcmp(ex->op_array->vars[i].name, varname->value.str.val, varname->value.str.len)) {                        ex->CVs[i] = NULL; // 置空EX(CVs)                        break;                    }                }            }            ex = ex->prev_execute_data;        } while (ex && ex->symbol_table == target_symbol_table);    }
程序会先获取目标符号表,这个符号表是一个HashTable,然后将我们需要unset掉的变量从这个HashTable中删除。如果对HashTable的元素删除操作成功,程序还会对EX(CVs)内存储的值进行清空操作。以缓存机制来解释,在删除原始数据后,程序也会删除相对应的缓存内容,以免用户获取到脏数据。

转载地址:http://nmrvb.baihongyu.com/

你可能感兴趣的文章
java获取数字和汉字
查看>>
excel Option Explicit webadi
查看>>
ICX错误
查看>>
windows Xp NTLDR is missing
查看>>
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)
查看>>
Centos 6.x 安装配置MySQL
查看>>
-source 1.5 中不支持 diamond 运算 请使用 -source 7 或更高版本以启用
查看>>
jar包读取资源文件报错:找不到资源文件(No such file or directory)
查看>>
超简单:Linux安装rar/unrar工具与解压到目录示例
查看>>
Eclipse创建Maven Java8 Web项目,并直接部署Tomcat
查看>>
RedHad 7.x服务器操作记录
查看>>
BindException: Cannot assign requested address (Bind failed)解决办法
查看>>
Centos7:Docker安装Gitlab
查看>>
Kafka日志配置
查看>>
logstash 6.x 收集syslog日志
查看>>
Apache Kylin 2.3 构建Cube失败
查看>>
Apache Kylin 2.3 样例分析
查看>>
Apache Kylin 2.3 JDBC Java API 示例
查看>>
An internal error occurred during: "Initializing Java Tooling". java.lang.NullPointerException
查看>>
ClassNotFoundException: org.springframework.web.context.ContextLoaderListener
查看>>