看下面的内容之前先对zval这个结构体做个了解<span class="hljs-keyword"><br>
</span>
<ol class="dp-j"><li class="alt"><span><span>typedef struct _zval_struct { </span></span></li><li><span> zvalue_value value; </span></li><li class="alt"><span> zend_uint refcount; </span></li><li><span> zend_uchar type; </span></li><li class="alt"><span> zend_uchar is_ref; </span></li><li><span>} zval; </span></li></ol>
zval结构体中共有4个元素,value是一个联合体,用来真正的存储zval的值,refcount用来计数该zval被多少个变量使用,type表示zval所存储的数据类型,is_ref用来标志该zval是否被引用。
引用计数<span class="php"><span class="hljs-preprocessor"><br>
</span></span>
<ol class="dp-j"><li class="alt"><span><span><php </span></span></li><li><span> $a = <span class="string">'Hello World'</span><span>; </span></span></li><li class="alt"><span> $b = $a; </span></li><li><span> unset($a); </span></li><li class="alt"><span>> </span></li></ol>
我们一起来剖析下上面这段代码:
$a = 'Hello World';
首先这句代码被执行,内核创建一个变量,并分配12字节的内存去存储字符串'Hello World'和末尾的NULL。
$b = $a;
接着执行这句代码,执行这句的时候内核里面发生了什么呢?
对$a
所指向的zval中的refcount进行加1操作。
将变量$b
指向$a
所指向的zval。
在内核中大概是这样的,其中active_symbol_table
是当前的变量符号表<span class="hljs-collection"><br>
</span>
<ol class="dp-j"><li class="alt"><span><span>{ </span></span></li><li><span> zval *helloval; </span></li><li class="alt"><span> MAKE_STD_ZVAL(helloval); </span></li><li><span> ZVAL_STRING(helloval, <span class="string">"Hello World"</span><span>, </span><span class="number">1</span><span>); </span></span></li><li class="alt"><span> zend_hash_add(EG(active_symbol_table), <span class="string">"a"</span><span>, sizeof(</span><span class="string">"a"</span><span>), </span></span></li><li><span> &helloval, sizeof(zval*), NULL); </span></li><li class="alt"><span> ZVAL_ADDREF(helloval); </span></li><li><span> zend_hash_add(EG(active_symbol_table), <span class="string">"b"</span><span>, sizeof(</span><span class="string">"b"</span><span>), </span></span></li><li class="alt"><span> &helloval, sizeof(zval*), NULL); </span></li><li><span>} </span></li></ol>
unset($a);
这句代码执行后,内核会将
写时复制
<ol class="dp-c"><li class="alt"><span><span><?php </span></span></li><li><span> <span class="vars">$a</span><span> = 1; </span></span></li><li class="alt"><span> <span class="vars">$b</span><span> = </span><span class="vars">$a</span><span>; </span></span></li><li><span> <span class="vars">$b</span><span> += 5; </span></span></li><li class="alt"><span>?> </span></li></ol>
上面这段代码执行完之后,一般肯定希望$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进行操作,这就是写时复制。
下面看看内核中分离时的主要代码:<br type="_moz">
<ol class="dp-c"><li class="alt"><span><span>zval *get_var_and_separate(char *varname, int varname_len TSRMLS_DC) </span></span></li><li><span>{ </span></li><li class="alt"><span> zval **varval, *varcopy; </span></li><li><span> <span class="keyword">if</span><span> (zend_hash_find(EG(active_symbol_table), </span></span></li><li class="alt"><span> varname, varname_len + 1, (void**)&varval) == FAILURE) { </span></li><li><span> <span class="comment">/* Variable doesn't actually exist fail out */</span><span> </span></span></li><li class="alt"><span> <span class="keyword">return</span><span> NULL; </span></span></li><li><span>} </span></li><li class="alt"><span><span class="keyword">if</span><span> ((*varval)->is_ref || (*varval)->refcount < 2) { </span></span></li><li><span> <span class="comment">/* varname is the only actual reference,</span> </span></li><li class="alt"><span><span class="comment"> * or it's a full reference to other variables</span> </span></li><li><span><span class="comment"> * either way: no separating to be done</span> </span></li><li class="alt"><span><span class="comment"> */</span><span> </span></span></li><li><span> <span class="keyword">return</span><span> *varval; </span></span></li><li class="alt"><span>} </span></li><li><span><span class="comment">/* Otherwise, make a copy of the zval* value */</span><span> </span></span></li><li class="alt"><span>MAKE_STD_ZVAL(varcopy); </span></li><li><span>varcopy = *varval; </span></li><li class="alt"><span><span class="comment">/* Duplicate any allocated structures within the zval* */</span><span> </span></span></li><li><span>zval_copy_ctor(varcopy); </span></li><li class="alt"><span> </span></li><li><span><span class="comment">/* Remove the old version of varname</span> </span></li><li class="alt"><span><span class="comment">* This will decrease the refcount of varval in the process</span> </span></li><li><span><span class="comment">*/</span><span> </span></span></li><li class="alt"><span>zend_hash_del(EG(active_symbol_table), varname, varname_len + 1); </span></li><li><span> </span></li><li class="alt"><span><span class="comment">/* Initialize the reference count of the</span> </span></li><li><span><span class="comment">* newly created value and attach it to</span> </span></li><li class="alt"><span><span class="comment">* the varname variable</span> </span></li><li><span><span class="comment">*/</span><span> </span></span></li><li class="alt"><span>varcopy->refcount = 1; </span></li><li><span>varcopy->is_ref = 0; </span></li><li class="alt"><span>zend_hash_add(EG(active_symbol_table), varname, varname_len + 1, </span></li><li><span> &varcopy, sizeof(zval*), NULL); </span></li><li class="alt"><span><span class="comment">/* Return the new zval* */</span><span> </span></span></li><li><span><span class="keyword">return</span><span> varcopy; </span></span></li><li class="alt"><span>} </span></li></ol>
写时改变
<ol class="dp-c"><li class="alt"><span><span><?php </span></span></li><li><span> <span class="vars">$a</span><span> = 1; </span></span></li><li class="alt"><span> <span class="vars">$b</span><span> = &</span><span class="vars">$a</span><span>; </span></span></li><li><span> <span class="vars">$b</span><span> += 5; </span></span></li><li class="alt"><span>?> </span></li></ol>
上面这段代码执行完之后一般希望是:$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引用这个时候怎么办?<span class="php"><span class="hljs-preprocessor"><br type="_moz">
</span></span>
<ol class="dp-c"><li class="alt"><span><span><?php </span></span></li><li><span> <span class="vars">$a</span><span> = 1; </span></span></li><li class="alt"><span> <span class="vars">$b</span><span> = </span><span class="vars">$a</span><span>; </span></span></li><li><span> <span class="vars">$c</span><span> = &</span><span class="vars">$a</span><span>; </span></span></li><li class="alt"><span>?> </span></li></ol>
如果出现上面这种情况的时候,如果$a、$b、$c
指向同一个zval结构体,进行改变的时候Zend到底去听谁的?其实这个地方不会指向同一个zval了。
如果对一个is_ref = 0 && refcount >1
的zval进行写时改变这种赋值形式(就是引用赋值)的时候,Zend会将等号右边的变量分离出来一个新的zval,
对这个zval进行初始化,对之前的zval的refcount进行减1操作,让等号左边的变量指向这个新的zval,refcount进行加1操作,is_ref=1。看看下面这张图片
<span class="php"><span class="hljs-preprocessor"><br type="_moz">
</span></span>
<ol class="dp-c"><li class="alt"><span><span><?php </span></span></li><li><span> <span class="vars">$a</span><span> = 1; </span></span></li><li class="alt"><span> <span class="vars">$b</span><span> = &</span><span class="vars">$a</span><span>; </span></span></li><li><span> <span class="vars">$c</span><span> = </span><span class="vars">$a</span><span>; </span></span></li><li class="alt"><span>?> </span></li></ol>
上面这又是另外一种情况,在is_ref = 1
的情况下,试图单纯的进行refcount+1操作的时候会分离出来一个新的zval给等号左边的变量,并初始化他,看看下面这张图片
参考文献
1.《Extending and Embedding PHP》- Chaper 3 - Memory Management.