深入理解變量改變時PHP內核發生的一些變化
《Extending and Embedding PHP》PHP擴展開發和內核應用最好的書,沒有之一。對php中變量的引用計數、寫時復制,寫時改變,寫時復制和改變做個”翻譯“。
ZVAL
看下面的內容之前先對zval這個結構體做個了解
zval結構體中共有4個元素,value是一個聯合體,用來真正的存儲zval的值,refcount用來計數該zval被多少個變量使用,type表示zval所存儲的數據類型,is_ref用來標誌該zval是否被引用。
引用計數
我們一起來剖析下上面這段代碼:
$a = ‘Hello World‘;
首先這句代碼被執行,內核創建一個變量,並分配12字節的內存去存儲字符串‘Hello World‘和末尾的NULL。-
$b = $a;
接著執行這句代碼,執行這句的時候內核裏面發生了什麽呢?- 對
$a
所指向的zval中的refcount進行加1操作。 -
將變量
$b
指向$a
所指向的zval。
在內核中大概是這樣的,其中active_symbol_table
是當前的變量符號表
- 對
unset($a);
這句代碼執行後,內核會將a對應的zval結構體中的refcount計數減一,
寫時復制
上面這段代碼執行完之後,一般肯定希望$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進行操作,這就是寫時復制。
下面看看內核中分離時的主要代碼: -
寫時改變
上面這段代碼執行完之後一般希望是:
$a == $b == 6
。這個又是怎麽實現的呢?$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引用這個時候怎麽辦?
- 如果出現上面這種情況的時候,如果
$a、$b、$c
指向同一個zval結構體,進行改變的時候Zend到底去聽誰的?其實這個地方不會指向同一個zval了。
如果對一個is_ref = 0 && refcount >1
的zval進行寫時改變這種賦值形式(就是引用賦值)的時候,Zend會將等號右邊的變量分離出來一個新的zval,
對這個zval進行初始化,對之前的zval的refcount進行減1操作,讓等號左邊的變量指向這個新的zval,refcount進行加1操作,is_ref=1。看看下面這張圖片 -
-
上面這又是另外一種情況,在
is_ref = 1
的情況下,試圖單純的進行refcount+1操作的時候會分離出來一個新的zval給等號左邊的變量,並初始化他,看看下面這張圖片 -
參考文獻:
- 1.《Extending and Embedding PHP》- Chaper 3 - Memory Management.
- 內核看到
- 其實Zend內核在改變zval之前都會去進行
深入理解變量改變時PHP內核發生的一些變化