天天看點

三十七、PHP核心探索:變量指派與銷毀 ☞ 指派操作的同時已經進行聲明操作

在強類型的語言當中,當使用一個變量之前,我們需要先聲明這個變量。然而,對于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) {
        ...//省略
    }