變量改變時(shí)PHP內(nèi)核做了些什么?
看下面的內(nèi)容之前先對zval這個結(jié)構(gòu)體做個了解
- typedef struct _zval_struct {
- zvalue_value value;
- zend_uint refcount;
- zend_uchar type;
- zend_uchar is_ref;
- } zval;
zval結(jié)構(gòu)體中共有4個元素,value是一個聯(lián)合體,用來真正的存儲zval的值,refcount用來計(jì)數(shù)該zval被多少個變量使用,type表示zval所存儲的數(shù)據(jù)類型,is_ref用來標(biāo)志該zval是否被引用。
引用計(jì)數(shù)
- <php
- $a = 'Hello World';
- $b = $a;
- unset($a);
- >
我們一起來剖析下上面這段代碼:
-
$a = 'Hello World';
首先這句代碼被執(zhí)行,內(nèi)核創(chuàng)建一個變量,并分配12字節(jié)的內(nèi)存去存儲字符串'Hello World'和末尾的NULL。 -
$b = $a;
接著執(zhí)行這句代碼,執(zhí)行這句的時(shí)候內(nèi)核里面發(fā)生了什么呢?-
對
$a
所指向的zval中的refcount進(jìn)行加1操作。 -
將變量
$b
指向$a
所指向的zval。
在內(nèi)核中大概是這樣的,其中active_symbol_table
是當(dāng)前的變量符號表
- {
- 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);
這句代碼執(zhí)行后,內(nèi)核會將a對應(yīng)的zval結(jié)構(gòu)體中的refcount計(jì)數(shù)減一, b還和原來一樣
寫時(shí)復(fù)制
- <?php
- $a = 1;
- $b = $a;
- $b += 5;
- ?>
上面這段代碼執(zhí)行完之后,一般肯定希望$a=1,$b=6
,但是如果像引用計(jì)數(shù)那樣,$a
和$b
指向相同的zval,修改$b
之后$a
不是也變了?
這個具體是怎么實(shí)現(xiàn)的呢,我們一起來看下:
-
$a = 1;
內(nèi)核創(chuàng)建一個zval,并分配4個字節(jié)存儲數(shù)字1。 -
$b = $a;
這一步和引用計(jì)數(shù)中的第二步一樣,將$b
指向和$a
相同的zval,并將zval中的引用計(jì)數(shù)值refcount加1。 -
$b += 5;
關(guān)鍵是這一步,這一步驟發(fā)生了什么呢,怎么確保修改之后不影響$a
。-
其實(shí)Zend內(nèi)核在改變zval之前都會去進(jìn)行
get_var_and_separete
操作,如果recfount>1,就需要分離就創(chuàng)建新的zval返回,否則直接返回變量所指向的zval,下面看看如何分離產(chǎn)生新的zval。 -
復(fù)制一個和
$b
所指向zval一樣的zval。 -
將
$b
所指向的zval中的refcount計(jì)數(shù)減1。 -
初始化生成的新zval,設(shè)置refcount=1,is_ref=0。
-
讓
$b
指向新生成的zval。 -
對新生成的zval進(jìn)行操作,這就是寫時(shí)復(fù)制。
下面看看內(nèi)核中分離時(shí)的主要代碼:
- 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;
- }
-
寫時(shí)改變
- <?php
- $a = 1;
- $b = &$a;
- $b += 5;
- ?>
上面這段代碼執(zhí)行完之后一般希望是:$a == $b == 1
。這個又是怎么實(shí)現(xiàn)的呢?
-
$a = 1;
這一步驟和寫時(shí)復(fù)制中的***步一樣。 -
$b = &$a;
這一步驟內(nèi)核會將$b
指向$a
所指向的zval,將zval中的refcount加1,并將zval中的is_ref置為1。 -
$b += 5;
這一步驟和寫時(shí)復(fù)制中的第三步驟一樣,但是內(nèi)核中發(fā)生的事情卻不一樣。-
內(nèi)核看到
$b
進(jìn)行變化的時(shí)候,也會執(zhí)行get_var_and_separate函數(shù),看是否需要分離。 -
如果
(*varval)->is_ref
的話也會直接返回$b
所指向的zval,不去分離產(chǎn)生新的zval,不管zval的refcount是否>1。 -
這時(shí)候再去修改
$b
值,$a
的值也就改變了,因?yàn)樗麄冎赶蛳嗤膠val。
-
分離的問題
說道現(xiàn)在聰明的你可能已經(jīng)看出點(diǎn)問題了,如果一個zval結(jié)構(gòu)體既有refcount計(jì)數(shù)又有is_ref引用這個時(shí)候怎么辦?
- <?php
- $a = 1;
- $b = $a;
- $c = &$a;
- ?>
如果出現(xiàn)上面這種情況的時(shí)候,如果$a、$b、$c
指向同一個zval結(jié)構(gòu)體,進(jìn)行改變的時(shí)候Zend到底去聽誰的?其實(shí)這個地方不會指向同一個zval了。
如果對一個is_ref = 0 && refcount >1
的zval進(jìn)行寫時(shí)改變這種賦值形式(就是引用賦值)的時(shí)候,Zend會將等號右邊的變量分離出來一個新的zval,
對這個zval進(jìn)行初始化,對之前的zval的refcount進(jìn)行減1操作,讓等號左邊的變量指向這個新的zval,refcount進(jìn)行加1操作,is_ref=1??纯聪旅孢@張圖片
- <?php
- $a = 1;
- $b = &$a;
- $c = $a;
- ?>
上面這又是另外一種情況,在is_ref = 1
的情況下,試圖單純的進(jìn)行refcount+1操作的時(shí)候會分離出來一個新的zval給等號左邊的變量,并初始化他,看看下面這張圖片
參考文獻(xiàn)
1.《Extending and Embedding PHP》- Chaper 3 - Memory Management.