转载

变量改变时PHP内核做了些什么?

引言

内容来自于《Extending and Embedding PHP》- Chaper 3 - Memory Management,加上自己的理解,对php中变量的引用计数、写时复制,写时改变,写时复制和改变做个”翻译“。

zval

看下面的内容之前先对zval这个结构体做个了解

typedef struct _zval_struct {     zvalue_value value;     zend_uint refcount;     zend_uchar type;     zend_uchar is_ref; } zval;

zval结构体中共有4个元素,value是一个联合体,用来真正的存储zval的值,refcount用来计数该zval被多少个变量使用,type表示zval所存储的数据类型,is_ref用来标志该zval是否被引用。

引用计数

<?php     $a = 'Hello World';     $b = $a;     unset($a); ?>

我们一起来剖析下上面这段代码:

  • $a = 'Hello World'; 首先这句代码被执行,内核创建一个变量,并分配12字节的内存去存储字符串'Hello World'和末尾的NULL。
  • $b = $a; 接着执行这句代码,执行这句的时候内核里面发生了什么呢?

    • $a 所指向的zval中的refcount进行加1操作。
    • 将变量 $b 指向 $a 所指向的zval。

      在内核中大概是这样的,其中 active_symbol_table 是当前的变量符号表

       {  zval *helloval;  MAKE_STD_ZVAL(helloval);  ZVAL_STRING(helloval, "Hello World", 1);  zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),           &helloval, sizeof(zval*), NULL);  ZVAL_ADDREF(helloval);  zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),           &helloval, sizeof(zval*), NULL); } 
  • unset($a); 这句代码执行后,内核会将$a对应的zval结构体中的refcount计数减一,$b还和原来一样

写时复制

<?php     $a = 1;     $b = $a;     $b += 5; ?>

上面这段代码执行完之后,一般肯定希望 $a=1,$b=6 ,但是如果像引用计数那样, $a$b 指向相同的zval,修改 $b 之后 $a 不是也变了?

这个具体是怎么实现的呢,我们一起来看下:

  • $a = 1; 内核创建一个zval,并分配4个字节存储数字1。
  • $b = $a; 这一步和引用计数中的第二步一样,将 $b 指向和 $a 相同的zval,并将zval中的引用计数值refcount加1。
  • $b += 5; 关键是这一步,这一步骤发生了什么呢,怎么确保修改之后不影响 $a

    • 其实Zend内核在改变zval之前都会去进行 get_var_and_separete 操作,如果recfount>1,就需要分离就创建新的zval返回,否则直接返回变量所指向的zval,下面看看如何分离产生新的zval。
    • 复制一个和 $b 所指向zval一样的zval。
    • $b 所指向的zval中的refcount计数减1。
    • 初始化生成的新zval,设置refcount=1,is_ref=0。
    • $b 指向新生成的zval。
    • 对新生成的zval进行操作,这就是写时复制。

      下面看看内核中分离时的主要代码:

       zval *get_var_and_separate(char *varname, int varname_len TSRMLS_DC) {  zval **varval, *varcopy;  if (zend_hash_find(EG(active_symbol_table),      varname, varname_len + 1, (void**)&varval) == FAILURE) {  /* Variable doesn't actually exist  fail out */  return NULL; } if ((*varval)->is_ref || (*varval)->refcount < 2) {  /* varname is the only actual reference,  * or it's a full reference to other variables  * either way: no separating to be done  */  return *varval; } /* Otherwise, make a copy of the zval* value */ MAKE_STD_ZVAL(varcopy); varcopy = *varval; /* Duplicate any allocated structures within the zval* */ zval_copy_ctor(varcopy); /* Remove the old version of varname * This will decrease the refcount of varval in the process */ zend_hash_del(EG(active_symbol_table), varname, varname_len + 1); /* Initialize the reference count of the * newly created value and attach it to * the varname variable */ varcopy->refcount = 1; varcopy->is_ref = 0; zend_hash_add(EG(active_symbol_table), varname, varname_len + 1,           &varcopy, sizeof(zval*), NULL); /* Return the new zval* */ return varcopy; } 

写时改变

<?php     $a = 1;     $b = &$a;     $b += 5; ?>

上面这段代码执行完之后一般希望是: $a == $b == 1 。这个又是怎么实现的呢?

  • $a = 1; 这一步骤和写时复制中的第一步一样。
  • $b = &$a; 这一步骤内核会将 $b 指向 $a 所指向的zval,将zval中的refcount加1,并将zval中的is_ref置为1。
  • $b += 5; 这一步骤和写时复制中的第三步骤一样,但是内核中发生的事情却不一样。

    • 内核看到 $b 进行变化的时候,也会执行get_var_and_separate函数,看是否需要分离。
    • 如果 (*varval)->is_ref 的话也会直接返回 $b 所指向的zval,不去分离产生新的zval,不管zval的refcount是否>1。
    • 这时候再去修改 $b 值, $a 的值也就改变了,因为他们指向相同的zval。

分离的问题

说道现在聪明的你可能已经看出点问题了,如果一个zval结构体既有refcount计数又有is_ref引用这个时候怎么办?

<?php     $a = 1;     $b = $a;     $c = &$a; ?>

如果出现上面这种情况的时候,如果 $a、$b、$c 指向同一个zval结构体,进行改变的时候Zend到底去听谁的?其实这个地方不会指向同一个zval了。

如果对一个 is_ref = 0 && refcount >1 的zval进行写时改变这种赋值形式(就是引用赋值)的时候,Zend会将等号右边的变量分离出来一个新的zval,

对这个zval进行初始化,对之前的zval的refcount进行减1操作,让等号左边的变量指向这个新的zval,refcount进行加1操作,is_ref=1。看看下面这张图片

变量改变时PHP内核做了些什么?

<?php     $a = 1;     $b = &$a;     $c = $a; ?>

上面这又是另外一种情况,在 is_ref = 1 的情况下,试图单纯的进行refcount+1操作的时候会分离出来一个新的zval给等号左边的变量,并初始化他,看看下面这张图片

变量改变时PHP内核做了些什么?

参考文献

1.《Extending and Embedding PHP》- Chaper 3 - Memory Management.

微信号: love_skills

越努力,越幸运!越幸运,越努力!

做上CEO不是梦

赢取白富美不是梦

屌丝逆袭不是梦

就是现在!!加油

变量改变时PHP内核做了些什么?

正文到此结束
Loading...