1. 程式人生 > >【php7核心】靜態變數,全域性變數,常量的實現

【php7核心】靜態變數,全域性變數,常量的實現

最近在讀php7核心,本文是由《PHP7核心剖析》整理而來。

靜態變數

PHP中區域性變數分配在zend_execute_data結構上,每次執行zend_op_array都會生成一個新的zend_execute_data,區域性變數在執行之初分配,然後在執行結束時釋放,這是區域性變數的生命週期,而區域性變數中有一種特殊的型別:靜態變數,它們不會在函式執行完後釋放,當程式執行離開函式域時靜態變數的值被保留下來,下次執行時仍然可以使用之前的值。


PHP中的靜態變數通過static關鍵詞建立:

function my_func(){
    static $count = 4;
    $count++;
    echo $count,"\n";
}
my_func();
my_func();
===========================
5
6

靜態變數的儲存

靜態變數既然不會隨執行的結束而釋放,那麼很容易想到它的儲存位置:zend_op_array->static_variables,這是一個雜湊表,所以PHP中的靜態變數與普通區域性變數不同,它們沒有分配在執行空間zend_execute_data上,而是以雜湊表的形式儲存在zend_op_array中

靜態變數只會初始化一次,注意:它的初始化發生在編譯階段而不是執行階段,上面這個例子中:static $count = 4;是在編譯階段發現定義了一個靜態變數,然後插進了zend_op_array->static_variables中,並不是執行的時候把static_variables中的值修改為4,所以上面執行的時候會輸出5、6,再次執行並沒有重置靜態變數的值。

這個特性也意味著靜態變數初始的值不能是變數,比如:static $count = $xxx;這樣定義將會報錯。

靜態變數的訪問

區域性變數通過編譯時確定的編號進行讀寫操作,而靜態變數通過雜湊表儲存,這就使得其不能像普通變數那樣有一個固定的編號,有一種可能是通過變數名索引的,那麼究竟是否如此呢?我們分析下其編譯過程。

靜態變數編譯的語法規則:靜態變數編譯的語法規則:

statement:
    ...
    |   T_STATIC static_var_list ';'    { $$ = $2; }
    ...
;
static_var_list:
        static_var_list ',' static_var { $$ = zend_ast_list_add($1, $3); }
    |   static_var { $$ = zend_ast_create_list(1, ZEND_AST_STMT_LIST, $1); }
;
static_var:
        T_VARIABLE          { $$ = zend_ast_create(ZEND_AST_STATIC, $1, NULL); }
    |   T_VARIABLE '=' expr { $$ = zend_ast_create(ZEND_AST_STATIC, $1, $3); }
;
語法解析後生成了一個ZEND_AST_STATIC語法樹節點,接著再看下這個節點編譯為opcode的過程:zend_compile_static_var。
void zend_compile_static_var(zend_ast *ast)
{
    zend_ast *var_ast = ast->child[0];
    zend_ast *value_ast = ast->child[1];
    zval value_zv;
    if (value_ast) {
        //定義了初始值
        zend_const_expr_to_zval(&value_zv, value_ast);
    } else {
        //無初始值
        ZVAL_NULL(&value_zv);
    }
    zend_compile_static_var_common(var_ast, &value_zv, 1);
}

這裡首先對初始化值進行編譯,最終得到一個固定值,然後呼叫:zend_compile_static_var_common()處理,首先判斷當前編譯的zend_op_array->static_variables是否已建立,未建立則分配一個HashTable,接著將定義的靜態變數插入:

/zend_compile_static_var_common():
if (!CG(active_op_array)->static_variables) {
    ALLOC_HASHTABLE(CG(active_op_array)->static_variables);
    zend_hash_init(CG(active_op_array)->static_variables, 8, NULL, ZVAL_PTR_DTOR, 0);
}
//插入靜態變數
zend_hash_update(CG(active_op_array)->static_variables, Z_STR(var_node.u.constant), value);
插入靜態變數雜湊表後並沒有完成,接下來還有一個重要操作:
//生成一條ZEND_FETCH_W的opcode
opline = zend_emit_op(&result, by_ref ? ZEND_FETCH_W : ZEND_FETCH_R, &var_node, NULL);
opline->extended_value = ZEND_FETCH_STATIC;
if (by_ref) {
    zend_ast *fetch_ast = zend_ast_create(ZEND_AST_VAR, var_ast);
    //生成一條ZEND_ASSIGN_REF的opcode
    zend_emit_assign_ref_znode(fetch_ast, &result);
}

後面生成了兩條opcode:

ZEND_FETCH_W: 這條opcode對應的操作是建立一個IS_INDIRECT型別的zval,指向static_variables中對應靜態變數的zval

ZEND_ASSIGN_REF: 它的操作是引用賦值,即將一個引用賦值給CV變數

通過上面兩條opcode可以確定靜態變數的讀寫過程:首先根據變數名在static_variables中取出對應的zval,然後將它修改為引用型別並賦值給區域性變數,也就是說static $count = 4;包含了兩個操作,嚴格的將$count並不是真正的靜態變數,它只是一個指向靜態變數的區域性變數,執行時實際操作是:$count = & static_variables["count"];。上面例子$count與static_variables["count"]間的關係如圖所示。

全域性變數

PHP中在函式、類之外直接定義的變數可以在函式、類成員方法中通過global關鍵詞引入使用,這些變數稱為:全域性變數。

這些直接在PHP中定義的變數(包括include、require檔案中的)相對於函式、類方法而言它們是全域性變數,但是對自身執行域zend_execute_data而言它們是普通的區域性變數,自身執行時它們與普通變數的讀寫方式完全相同。

全域性變數初始化

全域性變數在整個請求執行期間始終存在,它們儲存在EG(symbol_table)中,也就是全域性變數符號表,與靜態變數的儲存一樣,這也是一個雜湊表,主指令碼(或include、require)在zend_execute_ex執行開始之前會把當前作用域下的所有區域性變數新增到EG(symbol_table)中,這一步操作後面介紹zend執行過程時還會講到,這裡先簡單提下:

ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value)
{
    ...
    i_init_execute_data(execute_data, op_array, return_value);
    zend_execute_ex(execute_data);
    ...
}
i_init_execute_data()這個函式中會把區域性變數插入到EG(symbol_table):
ZEND_API void zend_attach_symbol_table(zend_execute_data *execute_data)
{
    zend_op_array *op_array = &execute_data->func->op_array;
    HashTable *ht = execute_data->symbol_table;
    if (!EXPECTED(op_array->last_var)) { 
        return;
    }
    zend_string **str = op_array->vars;
    zend_string **end = str + op_array->last_var;
    //區域性變數陣列起始位置
    zval *var = EX_VAR_NUM(0);
    do{
        zval *zv = zend_hash_find(ht, *str);
        //插入全域性變數符號表
        zv = zend_hash_add_new(ht, *str, var);
        //雜湊表中value指向區域性變數的zval
        ZVAL_INDIRECT(zv, var);
        ...
    }while(str != end);
}

從上面的過程可以很直觀的看到,在執行前遍歷區域性變數,然後插入EG(symbol_table),EG(symbol_table)中的value直接指向區域性變數的zval,示例經過這一步的處理之後(此時區域性變數只是分配了zval,但還未初始化,所以是IS_UNDEF):

與靜態變數的訪問一樣,全域性變數也是將原來的值轉換為引用,然後在global匯入的作用域內建立一個區域性變數指向該引用

global $id; // 相當於:$id = & EG(symbol_table)["id"];

具體的操作過程不再細講,與靜態變數的處理過程一致,這時示例中區域性變數與全域性變數的引用情況如下圖。

超全域性變數

全部變數除了通過global引入外還有一類特殊的型別,它們不需要使用global引入而可以直接使用,這些全域性變數稱為:超全域性變數。

超全域性變數實際是PHP核心定義的一些全域性變數:$GLOBALS、$_SERVER、$_REQUEST、$_POST、$_GET、$_FILES、$_ENV、$_COOKIE、$_SESSION、argv、argc。

銷燬

區域性變數如果沒有手動銷燬,那麼在函式執行結束時會將它們銷燬,而全域性變數則是在整個請求結束時才會銷燬,即使是我們直接在PHP指令碼中定義在函式外的那些變數

常量

常量是一個簡單值的識別符號(名字)。如同其名稱所暗示的,在指令碼執行期間該值不能改變。常量預設為大小寫敏感。通常常量識別符號總是大寫的。

常量名和其它任何 PHP 標籤遵循同樣的命名規則。合法的常量名以字母或下劃線開始,後面跟著任何字母,數字或下劃線。

PHP中的常量通過define()函式定義:

define('CONST_VAR_1', 1234);

常量的儲存

在核心中常量儲存在EG(zend_constant)雜湊表中,訪問時也是根據常量名直接到雜湊表中查詢,其實現比較簡單。

常量的資料結構:

typedef struct _zend_constant {
    zval value;   //常量值
    zend_string *name; //常量名
    int flags;  //常量標識位
    int module_number; //所屬擴充套件、模組
} zend_constant;

常量的幾個屬性都比較直觀,這裡只介紹下flags,它的值可以是以下三個中任意組合:

#define CONST_CS                (1<<0)  //大小寫敏感
#define CONST_PERSISTENT        (1<<1)  //持久化的
#define CONST_CT_SUBST          (1<<2)  //允許編譯時替換

介紹下三種flag代表的含義:

CONST_CS: 大小寫敏感,預設是開啟的,使用者通過define()定義的始終是區分大小寫的,通過擴充套件定義的可以自由選擇

CONST_PERSISTENT: 持久化的,只有通過擴充套件、核心定義的才支援,這種常量不會在request結束時清理掉

CONST_CT_SUBST: 允許編譯時替換,編譯時如果發現有地方在讀取常量的值,那麼編譯器會嘗試直接替換為常量值,而不是在執行時再去讀取,目前這個flag只有TRUE、FALSE、NULL三個常量在使用

常量的銷燬

非持久化常量在request請求結束時銷燬,具體銷燬操作在:

php_request_shutdown()->zend_deactivate()->shutdown_executor()->clean_non_persistent_constants()。

void clean_non_persistent_constants(void)
{
    if (EG(full_tables_cleanup)) {
        zend_hash_apply(EG(zend_constants), clean_non_persistent_constant_full);
    } else {
        zend_hash_reverse_apply(EG(zend_constants), clean_non_persistent_constant);
    }
}

然後從雜湊表末尾開始向前遍歷EG(zend_constants),將非持久化常量刪除,直到碰到第一個持久化常量時,停止遍歷,正常情況下所有通過擴充套件定義的常量一定是在PHP中通過define定義之前,當然也並非絕對,這裡只是說在所有常量均是在MINT階段定義的情況。

持久化常量是在php_module_shutdown()階段銷燬的,具體過程與上面類似。