PHP内核探索:变量赋值与销毁

赋值操作的同时已经进行声明操作
服务器君一共花费了346.155 ms进行了5次数据库查询,努力地为您提供了这个页面。
试试阅读模式?希望听取您的建议

在强类型的语言当中,当使用一个变量之前,我们需要先声明这个变量。然而,对于PHP来说, 在使用一个变量时,我们不需要声明,也不需要初始化,直接对其赋值就可以使用,这是如何实现的?

在PHP中没有对常规变量的声明操作,如果要使用一个变量,直接进行赋值操作即可。在赋值操作的同时已经进行声明操作。 一个简单的赋值操作:

$a = 10;

使用VLD扩展查看其生成的中间代码为 ASSIGN。 依此,我们找到其执行的函数为 ZEND_ASSIGN_SPEC_CV_CONST_HANDLER。 (找到这个函数的方法之一:$a为CV,10为CONST,操作为ASSIGN。) 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值。这个函数最后的调用顺序为: [_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 = $a
line     # *  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,由于我们unse的是一个变量, 在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,然后将我们需要unset掉的变量从这个HashTable中删除。

延伸阅读

此文章所在专题列表如下:

  1. PHP内核探索:从SAPI接口开始
  2. PHP内核探索:一次请求的开始与结束
  3. PHP内核探索:一次请求生命周期
  4. PHP内核探索:单进程SAPI生命周期
  5. PHP内核探索:多进程/线程的SAPI生命周期
  6. PHP内核探索:Zend引擎
  7. PHP内核探索:再次探讨SAPI
  8. PHP内核探索:Apache模块介绍
  9. PHP内核探索:通过mod_php5支持PHP
  10. PHP内核探索:Apache运行与钩子函数
  11. PHP内核探索:嵌入式PHP
  12. PHP内核探索:PHP的FastCGI
  13. PHP内核探索:如何执行PHP脚本
  14. PHP内核探索:PHP脚本的执行细节
  15. PHP内核探索:操作码OpCode
  16. PHP内核探索:PHP里的opcode
  17. PHP内核探索:解释器的执行过程
  18. PHP内核探索:变量概述
  19. PHP内核探索:变量存储与类型
  20. PHP内核探索:PHP中的哈希表
  21. PHP内核探索:理解Zend里的哈希表
  22. PHP内核探索:PHP哈希算法设计
  23. PHP内核探索:翻译一篇HashTables文章
  24. PHP内核探索:哈希碰撞攻击是什么?
  25. PHP内核探索:常量的实现
  26. PHP内核探索:变量的存储
  27. PHP内核探索:变量的类型
  28. PHP内核探索:变量的值操作
  29. PHP内核探索:变量的创建
  30. PHP内核探索:预定义变量
  31. PHP内核探索:变量的检索
  32. PHP内核探索:变量的类型转换
  33. PHP内核探索:弱类型变量的实现
  34. PHP内核探索:静态变量的实现
  35. PHP内核探索:变量类型提示
  36. PHP内核探索:变量的生命周期
  37. PHP内核探索:变量赋值与销毁
  38. PHP内核探索:变量作用域
  39. PHP内核探索:诡异的变量名
  40. PHP内核探索:变量的value和type存储
  41. PHP内核探索:全局变量Global
  42. PHP内核探索:变量类型的转换
  43. PHP内核探索:内存管理开篇
  44. PHP内核探索:Zend内存管理器
  45. PHP内核探索:PHP的内存管理
  46. PHP内核探索:内存的申请与销毁
  47. PHP内核探索:引用计数与写时复制
  48. PHP内核探索:PHP5.3的垃圾回收机制
  49. PHP内核探索:内存管理中的cache
  50. PHP内核探索:写时复制COW机制
  51. PHP内核探索:数组与链表
  52. PHP内核探索:使用哈希表API
  53. PHP内核探索:数组操作
  54. PHP内核探索:数组源码分析
  55. PHP内核探索:函数的分类
  56. PHP内核探索:函数的内部结构
  57. PHP内核探索:函数结构转换
  58. PHP内核探索:定义函数的过程
  59. PHP内核探索:函数的参数
  60. PHP内核探索:zend_parse_parameters函数
  61. PHP内核探索:函数返回值
  62. PHP内核探索:形参return value
  63. PHP内核探索:函数调用与执行
  64. PHP内核探索:引用与函数执行
  65. PHP内核探索:匿名函数及闭包
  66. PHP内核探索:面向对象开篇
  67. PHP内核探索:类的结构和实现
  68. PHP内核探索:类的成员变量
  69. PHP内核探索:类的成员方法
  70. PHP内核探索:类的原型zend_class_entry
  71. PHP内核探索:类的定义
  72. PHP内核探索:访问控制
  73. PHP内核探索:继承,多态与抽象类
  74. PHP内核探索:魔术函数与延迟绑定
  75. PHP内核探索:保留类与特殊类
  76. PHP内核探索:对象
  77. PHP内核探索:创建对象实例
  78. PHP内核探索:对象属性读写
  79. PHP内核探索:命名空间
  80. PHP内核探索:定义接口
  81. PHP内核探索:继承与实现接口
  82. PHP内核探索:资源resource类型
  83. PHP内核探索:Zend虚拟机
  84. PHP内核探索:虚拟机的词法解析
  85. PHP内核探索:虚拟机的语法分析
  86. PHP内核探索:中间代码opcode的执行
  87. PHP内核探索:代码的加密与解密
  88. PHP内核探索:zend_execute的具体执行过程
  89. PHP内核探索:变量的引用与计数规则
  90. PHP内核探索:新垃圾回收机制说明

本文地址:http://www.nowamagic.net/librarys/veda/detail/1415,欢迎访问原出处。

不打个分吗?

转载随意,但请带上本文地址:

http://www.nowamagic.net/librarys/veda/detail/1415

如果你认为这篇文章值得更多人阅读,欢迎使用下面的分享功能。
小提示:您可以按快捷键 Ctrl + D,或点此 加入收藏

大家都在看

阅读一百本计算机著作吧,少年

很多人觉得自己技术进步很慢,学习效率低,我觉得一个重要原因是看的书少了。多少是多呢?起码得看3、4、5、6米吧。给个具体的数量,那就100本书吧。很多人知识结构不好而且不系统,因为在特定领域有一个足够量的知识量+足够良好的知识结构,系统化以后就足以应对大量未曾遇到过的问题。

奉劝自学者:构建特定领域的知识结构体系的路径中再也没有比学习该专业的专业课程更好的了。如果我的知识结构体系足以囊括面试官的大部分甚至吞并他的知识结构体系的话,读到他言语中的一个词我们就已经知道他要表达什么,我们可以让他坐“上位”毕竟他是面试官,但是在知识结构体系以及心理上我们就居高临下。

所以,阅读一百本计算机著作吧,少年!

《C程序设计语言(第2版新版)》 克尼汉 (作者), 等 (作者, 译者), 徐宝文 (译者)

《C程序设计语言》(第2版新版)是由C语言的设计者Brian W.Kernighan和Dennis M.Ritchie编写的一部介绍标准C语言及其程序设计方法的权威性经典著作。全面、系统地讲述了C语言的各个特性及程序设计的基本方法,包括基本概念,类型和表达式、控制流、函数与程序结构、指针与数组、结构、输入与输出、UNIX系统接口、标准库等内容。

更多计算机宝库...