轉載請注明出處http://blog.csdn.net/fanhengguang_php
zval結構有兩個功能:第一,用于存儲一個變量的值以及變量類型。第二,有效的管理內存中的zval變量的值,本章將會介紹這個功能。
接下來我們看一下引用計數和copy-on-write 這兩個概念,以及在擴展中如何應用。
在php中所有的變量都是值傳遞,除非你顯示的指明引用傳遞。即任何時刻你傳遞一個變量給個函數或者給另外一個變量賦值,你得到的兩個變量都會擁有一份獨立的值的拷貝??匆韵吕?/p><?php$a = 1;$b = $a;$a++;// Only $a was incremented, $b stays as is:var_dump($a, $b); // int(2), int(1)function inc($n) { $n++;}$c = 1;inc($c);// The $c value outside the function and the $n inside the function are distinctvar_dump($c); // int(1)
雖然上面例子有些簡單,但需要意識到這是php種一個基本的規則, 特別的這個規則頁適用于對象。
<?php$obj = (object) ['value' => 1];function fnByVal($val) { $val = 100;}function fnByRef(&$ref) { $ref = 100;}// The by-value function does not modify $obj, the by-reference function does:fnByVal($obj);var_dump($obj); // stdClass(value => 1)fnByRef($obj);var_dump($obj); // int(100)人們經常說自從php5對象對象是自動的按引用傳遞的, 但是通過以上例子看出這是錯的:按值傳遞的函數不能修改傳遞給他的變量,而按引用傳遞的函數可以。
看一下例子:
<?phpclass myclass { public $PRop;}function myfun($obj) { $obj->prop = 'world';}$obj = new myclass();$obj->prop = 'hello';var_dump($obj);myfun($obj);var_dump($obj);打印出:object(myclass)#1 (1) { ["prop"]=> string(5) "hello"}object(myclass)#1 (1) { ["prop"]=> string(5) "world"}對象確實表現出引用傳遞的行為:雖然你不能將其賦值為一個完全不同的值, 但是你可以在函數中修改對象的成員。這是由于對象的值僅僅是一個用來查找實際內容的ID, 引用傳遞可以阻止你將其ID改為一個不同的對象或者不同的類型,但是并不能阻止你修改對象的實際的值。
以上也適用于resource類型。因為它也是同樣僅僅存儲了用于查找實際值的ID,所以同樣的按引用傳遞可以阻止你修改其resource ID或者不同的類型,但是并不能阻止你resource的內容(如修改文件的指針位置)。
稍加思考你會得到這樣的結論:php一定是做了可怕的大量拷貝。每次給函數傳遞變量,其值都會被拷貝一次,對于整形int或者double類型這可能沒啥問題,但是想像一下給函數傳遞一個擁有百萬元素的數組,每次調用都拷貝百萬的元素將是多么低效。
為了避免拷貝,php使用了寫時復制的方法:一個zval可以被多個變量、函數等共享,只要他們對變量是只讀的不會修改她。如果一個變量想要做修改,在修改之前需要將zval拷貝一份。
如果一個zval可以被共享,那么php需要一個方法判斷何時這個zval不被使用了,不再使用的zval將會被釋放掉。PHP通過簡單的跟蹤一個zval被引用次數來解決這個問題, 注意這里引用指的是一個zval被變量、函數等使用,而不是變量引用(如&方式引用變量)。引用個數存儲在zval結構的refcount__gc
成員變量中。
為了理解引用計數原理,考慮下面的例子:
<?php$a = 1; // $a = zval_1(value=1, refcount=1)$b = $a; // $a = $b = zval_1(value=1, refcount=2)$c = $b; // $a = $b = $c = zval_1(value=1, refcount=3)$a++; // $b = $c = zval_1(value=1, refcount=2) // $a = zval_2(value=2, refcount=1)unset($b); // $c = zval_1(value=1, refcount=1) // $a = zval_2(value=2, refcount=1)unset($c); // zval_1 is destroyed, because refcount=0 // $a = zval_2(value=2, refcount=1)當一個增加引用時refcount加1, 如果一個引用被刪除refcount減1, 如果refcount變為0, 這個zval將會被銷毀。
當出現循環引用時,這個方法就不適用了:
<?php$a = []; // $a = zval_1(value=[], refcount=1)$b = []; // $b = zval_2(value=[], refcount=1)$a[0] = $b; // $a = zval_1(value=[0 => zval_2], refcount=1) // $b = zval_2(value=[], refcount=2) // The refcount of zval_2 is incremented because it // is used in the array of zval_1$b[0] = $a; // $a = zval_1(value=[0 => zval_2], refcount=2) // $b = zval_2(value=[0 => zval_1], refcount=2) // The refcount of zval_1 is incremented because it // is used in the array of zval_2unset($a); // zval_1(value=[0 => zval_2], refcount=1) // $b = zval_2(value=[0 => zval_1], refcount=2) // The refcount of zval_1 is decremented, but the zval has // to stay alive because it's still referenced by zval_2unset($b); // zval_1(value=[0 => zval_2], refcount=1) // zval_2(value=[0 => zval_1], refcount=1) // The refcount of zval_2 is decremented, but the zval has // to stay alive because it's still referenced by zval_1上面的代碼執行完后,出現這樣一種情況兩個zvals沒有被任何變量引用,但是卻將一直存在不被銷毀,因為refcount不為0。 這是一個引用計數無效的典型的例子
為了解決這個問題php有第二種垃圾回收算法:循環垃圾回收器。我們現在可以暫時忽略他,因為循環回收對與擴展開發者來說是透明的,如果你想要了解這部分內容,可參考php手冊上的介紹:http://php.net/manual/en/features.gc.collecting-cycles.php
另一種需要考慮的情況是實際的php引用(例如&$val, 而不是上面提到的引用計數的引用). php使用一個is_ref
標記來表示php引用。這個標記存儲在zval結構體中的is_ref__gc
中。
is_ref=1
標記表示這是一個引用,當變量修改時應該直接修改zval的值, 不要進行拷貝。
上面的例子中在對其進行引用之前$a對應的zval的refcount為1, 現在考慮一個非常類似的例子,然而其refcount大于1
<?php$a = 1; // $a = zval_1(value=1, refcount=1, is_ref=0)$b = $a; // $a = $b = zval_1(value=1, refcount=2, is_ref=0)$c = $b // $a = $b = $c = zval_1(value=1, refcount=3, is_ref=0)$d =& $c; // $a = $b = zval_1(value=1, refcount=2, is_ref=0) // $c = $d = zval_2(value=1, refcount=2, is_ref=1) // $d is a reference of $c, but *not* of $a and $b, so // the zval needs to be copied here. Now we have the // same zval once with is_ref=0 and once with is_ref=1.$d++; // $a = $b = zval_1(value=1, refcount=2, is_ref=0) // $c = $d = zval_2(value=2, refcount=2, is_ref=1) // Because there are two separate zvals $d++ does // not modify $a and $b (as expected).如你所見,當引用一個is_ref=0 and refcount>1
的變量時需要先進行zval拷貝。 同樣的當使用一個is_ref=1 and refcount>1
的zval進行值傳遞的時候需要進行拷貝。 所以使用引用有時會使程序變慢:幾乎所有的php函數都使用值傳遞方式,所以當傳遞一個is_ref=1
zval給函數時會導致值拷貝。
Now that you are familiar with the general concepts underlying zval memory management, we can move on to their practical implementation. Lets start with zval allocation:
現在你應該熟悉了zval內存管理的基本概念,接下來我們它們的實際應用。我們從zval的創建開始
zval *zv_ptr;ALLOC_ZVAL(zv_ptr);這段代碼創建了一個zval,但是沒有初始化成員變量。有一個類似的宏辯題用來分配一個持久化的zval, 持久化zval在請求結束后不會被銷毀。
zval *zv_ptr;ALLOC_PERMANENT_ZVAL(zv_ptr);The difference between the two macros is that the former makes use of emalloc() whereas the latter uses malloc(). It’s important to know though that trying to directly allocate zvals will not work:
上面這兩個宏的不同點是,前者使用emalloc() 二后者使用malloc()來分配內存,重要提示直接分配zvals是不行的:
/* This code is WRONG */zval *zv_ptr = emalloc(sizeof(zval));The reason is that the cycle collector needs to store some additional information in the zval, so the structure that needs to be allocated is actually not a zval but a zval_gc_info:
原因是循環垃圾回收需要在zval中存儲一些額外的信息, 所以被創建的結構實際上是zval_gc_info
二不是zval:
Alloc*
宏會創建一個zval_gc_info
并初始化其額外的成員,但是在這之后這個值可以被透明的用作zval(因為這個結構中zval是其第一個元素).
在zval創建之后,有兩個宏可以對其進行初始化,第一個INIT_PZVAL
, 他會設置refcount=1 and is_ref=0
但是zval的值不會被初始化。
第二個宏INIT_ZVAL
也會設置refcount=1 and is_ref=0
, 但是除此之外會將zval的類型初始化為IS_NULL
INIT_PZVAL()
接受一個zval*
參數,而INIT_ZVAL()
接受一個zval參數,當傳遞zval*
給第二個宏時需要先對其解引用。
由于常常需要創建并初始化一個zval, 有兩個宏可以完成創建并初始化合的工作。
zval *zv_ptr;MAKE_STD_ZVAL(zv_ptr);/* zv_ptr has garbage type+value here */zval *zv_ptr;ALLOC_INIT_ZVAL(zv_ptr);/* zv_ptr has type=IS_NULL here */一旦創建并初始化了一個zval, 你就可以使用之前介紹過的引用計數方法。php提供了幾個宏來管理refcount:
Z_REFCOUNT_P(zv_ptr) /* Get refcount */Z_ADDREF_P(zv_ptr) /* Increment refcount */Z_DELREF_P(zv_ptr) /* Decrement refcount */Z_SET_REFCOUNT(zv_ptr, 1) /* Set refcount to some particular value (here 1) */想其他Z_
類的宏一樣, 沒有后綴,一個P_P
后綴,兩個P_PP
的宏分別可以接受zval, zval*
, zval**
的參數
最常用的宏是Z_ADDREF_P()
。 一個例子:
以上代碼將42先插入數組到0號位置,然后用num這個key又插入了一次。所以這個zval被用在兩個位置上,在創建并初始化zval后, 他的refcount為1, 想要在兩個位置使用同一個zval需要將refcout設置為2, 所以必須用Z_ADDREF_P()
對其加1.
The complement macro Z_DELREF_P() on the other hand is used rather rarely: Usually just decrementing the refcount is not enough, because you have to check for the refcount==0 case where the zval needs to be destroyed and freed:
另外一個宏Z_DELREF_P()
相對來說用的很少,通常僅僅將refcount 減1 是不夠的,因為你需要檢查refcount是否等0,等0時需要銷毀釋放zval:
zval_dtor()
宏接收一個zval*
參數,它會將zval的value值釋放掉: 如果值是字符串,則釋放字符串,如果是一個數組那么對應的hashtable會被釋放掉,如果是一個對象或者資源resource類型, 對應的實際的值的refcount會被減1(如果refcount減少至0將會導致他們被銷毀和釋放).
上面的代碼中你必須自己檢查refcount的值, 你可以用另外一個宏zval_ptr_dtor():
來替代上面的方式:
zval_ptr_dtor(&zv_ptr);
這個宏接收一個zval**
(由于歷史原因,也可以傳遞一個zval*
)。 這個宏會將refcount減1并且檢查這個zval是否需要被銷毀和釋放。不用于我們上面手動的方式,它還支持垃圾回收,下面是相關的實現:
Z_DELREF_P()
先將refcount減1然后將新的refcount返回, 所以!Z_DELREF_P(zval_ptr)
和先執行Z_DELREF_P(zval_ptr)
然后在檢查 Z_REFCOUNT_P(zval_ptr) == 0
是一樣的。
上面的函數中除了執行zval_dtor()
和 efree()
操作之外,還調用了GC_*
宏斷言&EG(uninitialized_zval)不會被釋放(這是一個被引擎使用的神奇zval)。
另外zval只被一個變量引用的話,代碼中會將is_ref
置為0。 因為&引用只有當兩個以上變量引用zval時才有意義,所以保留is_ref=1
是沒有任何意義的。
提示:不要使用Z_DELREF_P()
(除非你可以保證這個zval無需銷毀),當你想減少refcount時,你應當使用zval_ptr_dtor()
代替。zval_dtor()
宏可用于臨時的棧上創建的zvals:
一個臨時的棧上創建的zval是不能被共享的,當代碼塊退出后zval將會被銷毀和釋放。
雖然寫時復制避免了很多zval拷貝,但是有時拷貝無可避免,例如你想要修改zval的value 或者將zval存儲在其他地方。
針對不同使用場景,PHP提供了大量的宏用于拷貝。 最簡單的宏是ZVAL_COPY_VALUE()
,他僅僅拷貝zval的value和type成員。
此刻zv_dest
和zv_src
擁有同樣的value和type。 注意同樣的value意味著這兩個zval使用同一個字符串指針(char*
),這就是說如果zv_src
被釋放掉,那么其字符串value也會被釋放掉,那么zv_dest
的值將是一個懸空指針。 為了避免這中情況,應該使用zval_copy_ctor()
進行拷貝
zval_copy_ctor
會創建一個完全的拷貝。 即:如果是一個字符串那么對應char*
會被拷貝,如果是一個數組,HashTable*
會被拷貝, 如果是對象或者資源類型,zval內部的refcount將會+1
現在還剩下refcount
和is_ref
的初始化沒有介紹, 你可以使用INIT_PZVAL
宏,或者用MAKE_STD_ZVAL
替代ALLOC_ZVAL
來進行初始化操作,另一種方案是使用INIT_PZVAL_COPY()
替代ZVAL_COPY_VALUE()
函數,它會執行拷貝并對refcount
和 is_ref
進行初始化:
由于INIT_PZVAL_COPY()
和 zval_copy_ctor()
的聯合使用非常廣泛, 這二者的功能被整合到了MAKE_COPY_ZVAL()
宏中:
這個宏有一點欺騙性,函數參數的順序與其他宏不同(目的指針參數不在第一位,而是第二位), 并且其接受zval**
參數。
除了基本的zval拷貝宏之外還有一些比較復雜的宏,其中最重要的是ZVAL_ZVAL
, 常用于從一個函數中返回zvals, 原型如下:
copy參數指的是是否在目標zval上執行zval_copy_ctor()
, dtor參數表示是否在源zval上執行zval_ptr_dtor()
。 我們看下兩個參數所有4中不同的組合的行為, 舉一個最簡單的例子copy和dtor參數都是0:
在這個例子中ZVAL_ZVAL()
與ZVAL_COPY_VALUE()
效果一致, 想這樣參數0,0沒有實際意義。一個有用的變體為copy=1, dtor=0:
這是一個常規的zval拷貝與MAKE_COPY_ZVAL()
效果類似, 只是缺少了INIT_PZVAL()
這一步。當拷貝一個已經初始化的zval很有用(如return_value). 另外設置dtor=1只是增加了zval_ptr_dtor()
調用。
最有趣的情況是copy=0, dtor=1:
ZVAL_ZVAL(zv_dest, zv_src, 0, 1);/* equivalent to: */ZVAL_COPY_VALUE(zv_dest, zv_src);ZVAL_NULL(zv_src);zval_ptr_dtor(&zv_src);這構成了一個zval的move操作, zv_src
中的值value被移動到了zv_dest
中。 當zv_src
refcount=1時,zval將會被zval_ptr_dtor()
銷毀。如果refcount大于1,zval將會以NULL值存在。
之前介紹的一些宏主要用于拷貝zvals, 典型應用是拷貝一個zval的值到return_value
中, 另外一些宏用于zval分離,這些宏用于寫時復制, 我們通過示例代碼來介紹他們的作用。
如果refcount是1 那么上面函數什么都不會執行, 如果refcount > 1 , 則先將refcount減1, 然后將value值拷貝到一個新的zval中。 這個函數接受一個zval**
的參數,函數并不會修改這個指針指向的zval*
。
這在實際中有什么用處呢,假設你想要修改一個數組中的元素如$array[42]
, 你需要先拿到一個指向數組元素的zval*
的一個指針zval**
. 由于引用計數的關系, 你不能直接修改它(因為這個zval的值可能同時被其他變量引用), 所以你需要先進行zval分離, 如果refcount = 1 那么zval分離會保留原有zval不動, 如果大于1會進行zval拷貝, 后一種情況下新的zval會被賦值給一個zval**
指針。 在上面這個例子的情況下, 也就是指數組中42這個元素。
在這里簡單通過MAKE_COPY_ZVAL()
進行拷貝時不夠的, 因為拷貝后的zval并不是數組中存儲的那個zval。
當修改zval前直接通過SEPARATE_ZVAL()
函數進行分離并不適用于所有情況。 當zval is_ref=1
時zval分離不會發生。 為解決這個問題,我們看一下php提供的is_ref
操作宏:
同樣的上面這些宏也有諸如_P
和 _PP
之類的變體用于處理zval
, zval*
, zval**
。另外還存在一個老的宏PZVAL_IS_REF()
它等同于Z_ISREF_P()
宏。
PHP提供了兩個SEPARATE_ZVAL()
宏的變體:
SEPARATE_ZVAL_IF_NOT_REF
在修改zval中經常用到, SEPARATE_ZVAL_TO_MAKE_IS_REF
宏主要由php引擎調用,擴展開發中很少用到
有另外一個用于zval分離的宏, 與上面介紹的有些不同:
#define SEPARATE_ARG_IF_REF(varptr) / if (PZVAL_IS_REF(varptr)) { / zval *original_var = varptr; / ALLOC_ZVAL(varptr); / INIT_PZVAL_COPY(varptr, original_var); / zval_copy_ctor(varptr); / } else { / Z_ADDREF_P(varptr); / }首先它接收一個zval*
而不是zval**
指針。 其次這個宏會增加refcount, 而其他宏不會。
除此之外這個宏是SEPARATE_ZVAL_IF_NO_REF
宏的補充:當一個zval是一個引用時進行zval分離。 它主要用于確保一個參數是一個值而不是引用。
新聞熱點
疑難解答
圖片精選