一、 內存
在PHP中,填充一個字符串變量相當簡單,這只需要一個語句"<?php $str = 'hello world '; ?>"即可,並且該字符串能夠被自由地修改、拷貝和移動。而在C語言中,盡管你能夠編寫例如"char *str = "hello world ";"這樣的一個簡單的靜態字符串;但是,卻不能修改該字符串,因為它生存於程序空間內。為了創建一個可操縱的字符串,你必須分配一個內存塊,並且通過一個函數(例如strdup())來復制其內容。
{
char *str;
str = strdup("hello world");
if (!str) {
fprintf(stderr, "Unable to allocate memory!");
}
}
由於後面我們將分析的各種原因,傳統型內存管理函數(例如malloc(),free(),strdup(),realloc(),calloc(),等等)幾乎都不能直接為PHP源代碼所使用。
二、 釋放內存
在幾乎所有的平台上,內存管理都是通過一種請求和釋放模式實現的。首先,一個應用程序請求它下面的層(通常指"操作系統"):"我想使用一些內存空間"。如果存在可用的空間,操作系統就會把它提供給該程序並且打上一個標記以便不會再把這部分內存分配給其它程序。
當應用程序使用完這部分內存,它應該被返回到OS;這樣以來,它就能夠被繼續分配給其它程序。如果該程序不返回這部分內存,那麼OS無法知道是否這塊內存不再使用並進而再分配給另一個進程。如果一個內存塊沒有釋放,並且所有者應用程序丟失了它,那麼,我們就說此應用程序"存在漏洞",因為這部分內存無法再為其它程序可用。
在一個典型的客戶端應用程序中,較小的不太經常的內存洩漏有時能夠為OS所"容忍",因為在這個進程稍後結束時該洩漏內存會被隱式返回到OS。這並沒有什麼,因為OS知道它把該內存分配給了哪個程序,並且它能夠確信當該程序終止時不再需要該內存。
而對於長時間運行的服務器守護程序,包括象Apache這樣的web服務器和擴展php模塊來說,進程往往被設計為相當長時間一直運行。因為OS不能清理內存使用,所以,任何程序的洩漏-無論是多麼小-都將導致重復操作並最終耗盡所有的系統資源。
現在,我們不妨考慮用戶空間內的stristr()函數;為了使用大小寫不敏感的搜索來查找一個字符串,它實際上創建了兩個串的各自的一個小型副本,然後執行一個更傳統型的大小寫敏感的搜索來查找相對的偏移量。然而,在定位該字符串的偏移量之後,它不再使用這些小寫版本的字符串。如果它不釋放這些副本,那麼,每一個使用stristr()的腳本在每次調用它時都將洩漏一些內存。最後,web服務器進程將擁有所有的系統內存,但卻不能夠使用它。
你可以理直氣壯地說,理想的解決方案就是編寫良好、干淨的、一致的代碼。這當然不錯;但是,在一個象PHP解釋器這樣的環境中,這種觀點僅對了一半。
三、 錯誤處理
為了實現"跳出"對用戶空間腳本及其依賴的擴展函數的一個活動請求,需要使用一種方法來完全"跳出"一個活動請求。這是在Zend引擎內實現的:在一個請求的開始設置一個"跳出"地址,然後在任何die()或exit()調用或在遇到任何關鍵錯誤(E_ERROR)時執行一個longjmp()以跳轉到該"跳出"地址。
盡管這個"跳出"進程能夠簡化程序執行的流程,但是,在絕大多數情況下,這會意味著將會跳過資源清除代碼部分(例如free()調用)並最終導致出現內存漏洞。現在,讓我們來考慮下面這個簡化版本的處理函數調用的引擎代碼:
void call_function(const char *fname, int fname_len TSRMLS_DC){
zend_function *fe;
char *lcase_fname;
/* PHP函數名是大小寫不敏感的,
*為了簡化在函數表中對它們的定位,
*所有函數名都隱含地翻譯為小寫的
*/
lcase_fname = estrndup(fname, fname_len);
zend_str_tolower(lcase_fname, fname_len);
if (zend_hash_find(EG(function_table),lcase_fname, fname_len + 1, (void **)&fe) == FAILURE) {
zend_execute(fe->op_array TSRMLS_CC);
} else {
php_error_docref(NULL TSRMLS_CC, E_ERROR,"Call to undefined function: %s()", fname);
}
efree(lcase_fname);
}
當執行到php_error_docref()這一行時,內部錯誤處理器就會明白該錯誤級別是critical,並相應地調用longjmp()來中斷當前程序流程並離開call_function()函數,甚至根本不會執行到efree(lcase_fname)這一行。你可能想把efree()代碼行移動到zend_error()代碼行的上面;但是,調用這個call_function()例程的代碼行會怎麼樣呢?fname本身很可能就是一個分配的字符串,並且,在它被錯誤消息處理使用完之前,你根本不能釋放它。
注意,這個php_error_docref()函數是trigger_error()函數的一個內部等價實現。它的第一個參數是一個將被添加到docref的可選的文檔引用。第三個參數可以是任何我們熟悉的E_*家族常量,用於指示錯誤的嚴重程度。第四個參數(最後一個)遵循printf()風格的格式化和變量參數列表式樣。
四、 Zend內存管理器
在上面的"跳出"請求期間解決內存洩漏的方案之一是:使用Zend內存管理(ZendMM)層。引擎的這一部分非常類似於操作系統的內存管理行為-分配內存給調用程序。區別在於,它處於進程空間中非常低的位置而且是"請求感知"的;這樣以來,當一個請求結束時,它能夠執行與OS在一個進程終止時相同的行為。也就是說,它會隱式地釋放所有的為該請求所占用的內存。圖1展示了ZendMM與OS以及PHP進程之間的關系。
圖1.Zend內存管理器代替系統調用來實現針對每一種請求的內存分配。
六、 寫復制(Copy on Write)
通過refcounting來節約內存的確是不錯的主意,但是,當你僅想改變其中一個變量的值時情況會如何呢?為此,請考慮下面的代碼片斷:
<?php
$a = 1;
$b = $a;
$b += 5;
?>
通過上面的邏輯流程,你當然知道$a的值仍然等於1,而$b的值最後將是6。並且此時,你還知道,Zend在盡力節省內存-通過使$a和$b都引用相同的zval(見第二行代碼)。那麼,當執行到第三行並且必須改變$b變量的值時,會發生什麼情況呢?
回答是,Zend要查看refcount的值,並且確保在它的值大於1時對之進行分離。在Zend引擎中,分離是破壞一個引用對的過程,正好與你剛才看到的過程相反:
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) {
/* 變量根本並不存在-失敗而導致退出*/
return NULL;
}
if ((*varval)->refcount < 2) {
/* varname是唯一的實際引用,
*不需要進行分離
*/
return *varval;
}
/* 否則,再復制一份zval*的值*/
MAKE_STD_ZVAL(varcopy);
varcopy = *varval;
/* 復制任何在zval*內的已分配的結構*/
zval_copy_ctor(varcopy);
/*刪除舊版本的varname
*這將減少該過程中varval的refcount的值
*/
zend_hash_del(EG(active_symbol_table), varname, varname_len + 1);
/*初始化新創建的值的引用計數,並把它依附到
* varname變量
*/
varcopy->refcount = 1;
varcopy->is_ref = 0;
zend_hash_add(EG(active_symbol_table), varname, varname_len + 1,&varcopy, sizeof(zval*), NULL);
/*返回新的zval* */
return varcopy;
}
現在,既然引擎有一個僅為變量$b所擁有的zval*(引擎能知道這一點),所以它能夠把這個值轉換成一個long型值並根據腳本的請求給它增加5。
七、 寫改變(change-on-write)
引用計數概念的引入還導致了一個新的數據操作可能性,其形式從用戶空間腳本管理器看來與"引用"有一定關系。請考慮下列的用戶空間代碼片斷:
<?php
$a = 1;
$b = &$a;
$b += 5;
?>
在上面的PHP代碼中,你能看出$a的值現在為6,盡管它一開始為1並且從未(直接)發生變化。之所以會發生這種情況是因為當引擎開始把$b的值增加5時,它注意到$b是一個對$a的引用並且認為"我可以改變該值而不必分離它,因為我想使所有的引用變量都能看到這一改變"。
但是,引擎是如何知道的呢?很簡單,它只要查看一下zval結構的第四個和最後一個元素(is_ref)即可。這是一個簡單的開/關位,它定義了該值是否實際上是一個用戶空間風格引用集的一部分。在前面的代碼片斷中,當執行第一行時,為$a創建的值得到一個refcount為1,還有一個is_ref值為0,因為它僅為一個變量($a)所擁有並且沒有其它變量對它產生寫引用改變。在第二行,這個值的refcount元素被增加為2,除了這次is_ref元素被置為1之外(因為腳本中包含了一個"&"符號以指示是完全引用)。
最後,在第三行,引擎再一次取出與變量$b相關的值並且檢查是否有必要進行分離。這一次該值沒有被分離,因為前面沒有包括一個檢查。下面是get_var_and_separate()函數中與refcount檢查有關的部分代碼:
if ((*varval)->is_ref || (*varval)->refcount < 2) {
/* varname是唯一的實際引用,
* 或者它是對其它變量的一個完全引用
*任何一種方式:都沒有進行分離
*/
return *varval;
}
這一次,盡管refcount為2,卻沒有實現分離,因為這個值是一個完全引用。引擎能夠自由地修改它而不必關心其它變量值的變化。
八、 分離問題
盡管已經存在上面討論到的復制和引用技術,但是還存在一些不能通過is_ref和refcount操作來解決的問題。請考慮下面這個PHP代碼塊:
<?php
$a = 1;
$b = $a;
$c = &$a;
?>
在此,你有一個需要與三個不同的變量相關聯的值。其中,兩個變量是使用了"change-on-write"完全引用方式,而第三個變量處於一種可分離的"copy-on-write"(寫復制)上下文中。如果僅使用is_ref和refcount來描述這種關系,有哪些值能夠工作呢?
回答是:沒有一個能工作。在這種情況下,這個值必須被復制到兩個分離的zval*中,盡管兩者都包含完全相同的數據(見圖2)。
圖2.引用時強制分離
圖3.復制時強制分離