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

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

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

  1. typedef struct _zval_struct { 
  2.     zvalue_value value; 
  3.     zend_uint refcount; 
  4.     zend_uchar type; 
  5.     zend_uchar is_ref; 
  6. } zval; 

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

引用计数

  1. <php 
  2.     $a = 'Hello World'
  3.     $b = $a; 
  4.     unset($a); 

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

  • $a = 'Hello World';首先这句代码被执行,内核创建一个变量,并分配12字节的内存去存储字符串'Hello World'和末尾的NULL。

  • $b = $a;接着执行这句代码,执行这句的时候内核里面发生了什么呢?

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

    • 将变量$b指向$a所指向的zval。
      在内核中大概是这样的,其中active_symbol_table是当前的变量符号表

      1.     zval *helloval; 
      2.     MAKE_STD_ZVAL(helloval); 
      3.     ZVAL_STRING(helloval, "Hello World"1); 
      4.     zend_hash_add(EG(active_symbol_table), "a", sizeof("a"), 
      5.                                         &helloval, sizeof(zval*), NULL); 
      6.     ZVAL_ADDREF(helloval); 
      7.     zend_hash_add(EG(active_symbol_table), "b", sizeof("b"), 
      8.                                         &helloval, sizeof(zval*), NULL); 
  • unset($a);这句代码执行后,内核会将azvalrefcountb还和原来一样

写时复制

 

  1. <?php 
  2.     $a = 1; 
  3.     $b = $a
  4.     $b += 5; 
  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进行操作,这就是写时复制。
      下面看看内核中分离时的主要代码:

      1. zval *get_var_and_separate(char *varname, int varname_len TSRMLS_DC) 
      2.     zval **varval, *varcopy; 
      3.     if (zend_hash_find(EG(active_symbol_table), 
      4.                     varname, varname_len + 1, (void**)&varval) == FAILURE) { 
      5.     /* Variable doesn't actually exist  fail out */ 
      6.     return NULL; 
      7. if ((*varval)->is_ref || (*varval)->refcount < 2) { 
      8.     /* varname is the only actual reference, 
      9.     * or it's a full reference to other variables 
      10.     * either way: no separating to be done 
      11.     */ 
      12.     return *varval; 
      13. /* Otherwise, make a copy of the zval* value */ 
      14. MAKE_STD_ZVAL(varcopy); 
      15. varcopy = *varval; 
      16. /* Duplicate any allocated structures within the zval* */ 
      17. zval_copy_ctor(varcopy); 
      18.  
      19. /* Remove the old version of varname 
      20. * This will decrease the refcount of varval in the process 
      21. */ 
      22. zend_hash_del(EG(active_symbol_table), varname, varname_len + 1); 
      23.  
      24. /* Initialize the reference count of the 
      25. * newly created value and attach it to 
      26. * the varname variable 
      27. */ 
      28. varcopy->refcount = 1; 
      29. varcopy->is_ref = 0; 
      30. zend_hash_add(EG(active_symbol_table), varname, varname_len + 1, 
      31.                                         &varcopy, sizeof(zval*), NULL); 
      32. /* Return the new zval* */ 
      33. return varcopy; 

写时改变

 

  1. <?php 
  2.     $a = 1; 
  3.     $b = &$a
  4.     $b += 5; 
  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引用这个时候怎么办?

  1. <?php 
  2.     $a = 1; 
  3.     $b = $a
  4.     $c = &$a
  5. ?> 

如果出现上面这种情况的时候,如果$a、$b、$c指向同一个zval结构体,进行改变的时候Zend到底去听谁的?其实这个地方不会指向同一个zval了。
如果对一个is_ref = 0 && refcount >1的zval进行写时改变这种赋值形式(就是引用赋值)的时候,Zend会将等号右边的变量分离出来一个新的zval,
对这个zval进行初始化,对之前的zval的refcount进行减1操作,让等号左边的变量指向这个新的zval,refcount进行加1操作,is_ref=1。看看下面这张图片


  1. <?php 
  2.     $a = 1; 
  3.     $b = &$a
  4.     $c = $a
  5. ?> 

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

参考文献

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

责任编辑:王雪燕 来源: 博客园
相关推荐

2011-07-01 08:41:12

PHP

2021-01-03 10:37:50

前端开发技术

2011-07-07 14:28:23

PHP

2017-10-21 10:12:05

戴尔

2019-05-14 09:18:18

程序员PythonJava

2009-12-09 13:03:12

2017-11-14 09:03:36

Spring Clou架构演进

2017-11-13 15:48:36

架构Spring Clou演进

2019-12-10 11:01:06

云计算/预判性科研/自

2017-07-19 16:58:53

PHPFastCGI 内核探索

2019-12-09 09:50:18

程序员技能开发者

2024-03-28 14:16:43

容灾云计算

2009-12-04 13:31:21

PHP全局变量不能生效

2017-07-27 14:21:40

phpPHP源码分析hashtable

2016-08-12 10:11:22

2019-05-16 09:07:42

华为方舟编译器

2016-12-21 10:35:55

PHP内核PHP哈希表

2021-03-15 08:40:42

Vue组件函数

2011-07-06 10:37:46

开发技术周刊

2011-04-19 10:04:25

NeopPIshell网站后门
点赞
收藏

51CTO技术栈公众号