內(nèi)存管理對于長期運行的程序,例如服務(wù)器守護程序,是相當(dāng)重要的影響;因此,理解PHP是如何分配與釋放內(nèi)存的對于創(chuàng)建這類程序極為重要。本文將重點探討PHP的內(nèi)存管理問題。
一、 內(nèi)存
在PHP中,填充一個字符串變量相當(dāng)簡單,這只需要一個語句"<?php $str = ‘hello world ‘; ?>"即可,并且該字符串能夠被自由地修改、拷貝和移動。而在C語言中,盡管你能夠編寫例如"char *str = "hello world ";"這樣的一個簡單的靜態(tài)字符串;但是,卻不能修改該字符串,因為它生存于程序空間內(nèi)。為了創(chuàng)建一個可操縱的字符串,你必須分配一個內(nèi)存塊,并且通過一 個函數(shù)(例如strdup())來復(fù)制其內(nèi)容。
{
char *str;
str = strdup("hello world");
if (!str) {
fprintf(stderr, "Unable to allocate memory!");
}
}
由于后面我們將分析的各種原因,傳統(tǒng)型內(nèi)存管理函數(shù)(例如malloc(),free(),strdup(),realloc(),calloc(),等等)幾乎都不能直接為PHP源代碼所使用。
二、 釋放內(nèi)存
在幾乎所有的平臺上,內(nèi)存管理都是通過一種請求和釋放模式實現(xiàn)的。首先,一個應(yīng)用程序請求它下面的層(通常指"操作系統(tǒng)"):"我想使用一些內(nèi)存空間"。如果存在可用的空間,操作系統(tǒng)就會把它提供給該程序并且打上一個標(biāo)記以便不會再把這部分內(nèi)存分配給其它程序。
當(dāng) 應(yīng)用程序使用完這部分內(nèi)存,它應(yīng)該被返回到OS;這樣以來,它就能夠被繼續(xù)分配給其它程序。如果該程序不返回這部分內(nèi)存,那么OS無法知道是否這塊內(nèi)存不 再使用并進而再分配給另一個進程。如果一個內(nèi)存塊沒有釋放,并且所有者應(yīng)用程序丟失了它,那么,我們就說此應(yīng)用程序"存在漏洞",因為這部分內(nèi)存無法再為 其它程序可用。
在一個典型的客戶端應(yīng)用程序中,較小的不太經(jīng)常的內(nèi)存泄漏有時能夠為OS所"容忍",因為在這個進程稍后結(jié)束時該泄漏內(nèi)存會被隱式返回到OS。這并沒有什么,因為OS知道它把該內(nèi)存分配給了哪個程序,并且它能夠確信當(dāng)該程序終止時不再需要該內(nèi)存。
而對于長時間運行的服務(wù)器守護程序,包括象Apache這樣的web服務(wù)器和擴展php模塊來說,進程往往被設(shè)計為相當(dāng)長時間一直運行。因為OS不能清理內(nèi)存使用,所以,任何程序的泄漏-無論是多么小-都將導(dǎo)致重復(fù)操作并最終耗盡所有的系統(tǒng)資源。
現(xiàn)在,我們不妨考慮用戶空間內(nèi)的stristr()函數(shù);為了使用大小寫不敏感的搜索來查找一個字符串,它實際上創(chuàng)建了兩個串的各自的一個小型 副本,然后執(zhí)行一個更傳統(tǒng)型的大小寫敏感的搜索來查找相對的偏移量。然而,在定位該字符串的偏移量之后,它不再使用這些小寫版本的字符串。如果它不釋放這 些副本,那么,每一個使用stristr()的腳本在每次調(diào)用它時都將泄漏一些內(nèi)存。最后,web服務(wù)器進程將擁有所有的系統(tǒng)內(nèi)存,但卻不能夠使用它。
你可以理直氣壯地說,理想的解決方案就是編寫良好、干凈的、一致的代碼。這當(dāng)然不錯;但是,在一個象PHP解釋器這樣的環(huán)境中,這種觀點僅對了一半。
三、 錯誤處理
為了實現(xiàn)"跳出"對用戶空間腳本及其依賴的擴展函數(shù)的一個活動請求,需要使用一種方法來完全"跳出"一個活動請求。這是在Zend引擎內(nèi)實現(xiàn)的:在一個請求的開始設(shè)置一個"跳出"地址,然后在任何die()或exit()調(diào)用或在遇到任何關(guān)鍵錯誤(E_ERROR)時執(zhí)行一個longjmp()以跳轉(zhuǎn)到該"跳出"地址。
盡管這個"跳出"進程能夠簡化程序執(zhí)行的流程,但是,在絕大多數(shù)情況下,這會意味著將會跳過資源清除代碼部分(例如free()調(diào)用)并最終導(dǎo)致出現(xiàn)內(nèi)存漏洞?,F(xiàn)在,讓我們來考慮下面這個簡化版本的處理函數(shù)調(diào)用的引擎代碼:
void call_function(const char *fname, int fname_len TSRMLS_DC){
zend_function *fe;
char *lcase_fname;
/* PHP函數(shù)名是大小寫不敏感的,
*為了簡化在函數(shù)表中對它們的定位,
*所有函數(shù)名都隱含地翻譯為小寫的
*/
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);
}
當(dāng)執(zhí)行到php_error_docref()這一行時,內(nèi)部錯誤處理器就會明白該錯誤級別是critical,并相應(yīng)地調(diào)用longjmp ()來中斷當(dāng)前程序流程并離開call_function()函數(shù),甚至根本不會執(zhí)行到efree(lcase_fname)這一行。你可能想把 efree()代碼行移動到zend_error()代碼行的上面;但是,調(diào)用這個call_function()例程的代碼行會怎么樣呢?fname本 身很可能就是一個分配的字符串,并且,在它被錯誤消息處理使用完之前,你根本不能釋放它。
注意,這個php_error_docref()函數(shù)是trigger_error()函數(shù)的一個內(nèi)部等價實現(xiàn)。它的第一個參數(shù)是一個將被添加 到docref的可選的文檔引用。第三個參數(shù)可以是任何我們熟悉的E_*家族常量,用于指示錯誤的嚴(yán)重程度。第四個參數(shù)(最后一個)遵循printf() 風(fēng)格的格式化和變量參數(shù)列表式樣。
四、 Zend內(nèi)存管理器
在上面的"跳出"請求期間解決內(nèi)存泄漏的方案之一是:使用Zend內(nèi)存管理(ZendMM)層。引擎的這一部分非常類似于操作系統(tǒng)的內(nèi)存管理行 為-分配內(nèi)存給調(diào)用程序。區(qū)別在于,它處于進程空間中非常低的位置而且是"請求感知"的;這樣以來,當(dāng)一個請求結(jié)束時,它能夠執(zhí)行與OS在一個進程終止時 相同的行為。也就是說,它會隱式地釋放所有的為該請求所占用的內(nèi)存。圖1展示了ZendMM與OS以及PHP進程之間的關(guān)系。
除了提供隱式內(nèi)存清除功能之外,ZendMM還能夠根據(jù)php.ini中memory_limit的設(shè)置控制每一種內(nèi)存請求的用法。如果一個腳本試圖請求 比系統(tǒng)中可用內(nèi)存更多的內(nèi)存,或大于它每次應(yīng)該請求的最大量,那么,ZendMM將自動地發(fā)出一個E_ERROR消息并且啟動相應(yīng)的"跳出"進程。這種方 法的一個額外優(yōu)點在于,大多數(shù)內(nèi)存分配調(diào)用的返回值并不需要檢查,因為如果失敗的話將會導(dǎo)致立即跳轉(zhuǎn)到引擎的退出部分。
把PHP內(nèi)部代碼和OS的實際的內(nèi)存管理層"鉤"在一起的原理并不復(fù)雜:所有內(nèi)部分配的內(nèi)存都要使用一組特定的可選函數(shù)實現(xiàn)。例如,PHP代碼 不是使用malloc(16)來分配一個16字節(jié)內(nèi)存塊而是使用了emalloc(16)。除了實現(xiàn)實際的內(nèi)存分配任務(wù)外,ZendMM還會使用相應(yīng)的綁 定請求類型來標(biāo)志該內(nèi)存塊;這樣以來,當(dāng)一個請求"跳出"時,ZendMM可以隱式地釋放它。
經(jīng)常情況下,內(nèi)存一般都需要被分配比單個請求持續(xù)時間更長的一段時間。這種類型的分配(因其在一次請求結(jié)束之后仍然存在而被稱為"永久性分配 "),可以使用傳統(tǒng)型內(nèi)存分配器來實現(xiàn),因為這些分配并不會添加ZendMM使用的那些額外的相應(yīng)于每種請求的信息。然而有時,直到運行時刻才會確定是否 一個特定的分配需要永久性分配,因此ZendMM導(dǎo)出了一組幫助宏,其行為類似于其它的內(nèi)存分配函數(shù),但是使用最后一個額外參數(shù)來指示是否為永久性分配。
如果你確實想實現(xiàn)一個永久性分配,那么這個參數(shù)應(yīng)該被設(shè)置為1;在這種情況下,請求是通過傳統(tǒng)型malloc()分配器家族進行傳遞的。然而, 如果運行時刻邏輯認(rèn)為這個塊不需要永久性分配;那么,這個參數(shù)可以被設(shè)置為零,并且調(diào)用將會被調(diào)整到針對每種請求的內(nèi)存分配器函數(shù)。
例如,pemalloc(buffer_len,1)將映射到malloc(buffer_len),而pemalloc(buffer_len,0)將被使用下列語句映射到emalloc(buffer_len):
#define in Zend/zend_alloc.h:
#define pemalloc(size, persistent) ((persistent)?malloc(size): emalloc(size))
所有這些在ZendMM中提供的分配器函數(shù)都能夠從下表中找到其更傳統(tǒng)的對應(yīng)實現(xiàn)。
表格(下)展示了ZendMM支持下的每一個分配器函數(shù)以及它們的e/pe對應(yīng)實現(xiàn):
你可能會注意到,即使是pefree()函數(shù)也要求使用永久性標(biāo)志。這是因為在調(diào)用pefree()時,它實際上并不知道是否ptr是 一種永久性分配。針對一個非永久性分配調(diào)用free()能夠?qū)е码p倍的空間釋放,而針對一種永久性分配調(diào)用efree()有可能會導(dǎo)致一個段錯誤,因為內(nèi) 存管理器會試圖查找并不存在的管理信息。因此,你的代碼需要記住它分配的數(shù)據(jù)結(jié)構(gòu)是否是永久性的。
除了分配器函數(shù)核心部分外,還存在其它一些非常方便的ZendMM特定的函數(shù),例如:
void *estrndup(void *ptr,int len);
該函數(shù)能夠分配len+1個字節(jié)的內(nèi)存并且從ptr處復(fù)制len個字節(jié)到最新分配的塊。這個estrndup()函數(shù)的行為可以大致描述如下:
void *estrndup(void *ptr, int len)
{
char *dst = emalloc(len + 1);
memcpy(dst, ptr, len);
dst[len] = 0;
return dst;
}
在此,被隱式放置在緩沖區(qū)最后的NULL字節(jié)可以確保任何使用estrndup()實現(xiàn)字符串復(fù)制操作的函數(shù)都不需要擔(dān)心會把結(jié)果緩沖區(qū)傳遞給 一個例如printf()這樣的希望以為NULL為結(jié)束符的函數(shù)。當(dāng)使用estrndup()來復(fù)制非字符串?dāng)?shù)據(jù)時,最后一個字節(jié)實質(zhì)上都浪費了,但其中 的利明顯大于弊。
void *safe_emalloc(size_t size, size_t count, size_t addtl);
void *safe_pemalloc(size_t size, size_t count,size_t addtl,char persistent);
這些函數(shù)分配的內(nèi)存空間最終大小是((size*count)+addtl)。你可以會問:"為什么還要提供額外函數(shù)呢?為什么不使用一個 emalloc/pemalloc呢?"原因很簡單:為了安全。盡管有時候可能性相當(dāng)小,但是,正是這一"可能性相當(dāng)小"的結(jié)果導(dǎo)致宿主平臺的內(nèi)存溢出。 這可能會導(dǎo)致分配負(fù)數(shù)個數(shù)的字節(jié)空間,或更有甚者,會導(dǎo)致分配一個小于調(diào)用程序要求大小的字節(jié)空間。而safe_emalloc()能夠避免這種類型的陷 井-通過檢查整數(shù)溢出并且在發(fā)生這樣的溢出時顯式地預(yù)以結(jié)束。
注意,并不是所有的內(nèi)存分配例程都有一個相應(yīng)的p*對等實現(xiàn)。例如,不存在pestrndup(),并且在PHP 5.1版本前也不存在safe_pemalloc()。
五、 引用計數(shù)
慎重的內(nèi)存分配與釋放對于PHP(它是一種多請求進程)的長期性能有極其重大的影響;但是,這還僅是問題的一半。為了使一個每秒處理上千次點擊的服務(wù)器高效地運行,每一次請求都需要使用盡可能少的內(nèi)存并且要盡可能減少不必要的數(shù)據(jù)復(fù)制操作。請考慮下列PHP代碼片斷:
<?php
$a = ‘Hello World‘;
$b = $a;
unset($a);
>
在第一次調(diào)用之后,只有一個變量被創(chuàng)建,并且一個12字節(jié)的內(nèi)存塊指派給它以便存儲字符串"Hello World",還包括一個結(jié)尾處的NULL字符?,F(xiàn)在,讓我們來觀察后面的兩行:$b被置為與變量$a相同的值,然后變量$a被釋放。
如果PHP因每次變量賦值都要復(fù)制變量內(nèi)容的話,那么,對于上例中要復(fù)制的字符串還需要復(fù)制額外的12個字節(jié),并且在數(shù)據(jù)復(fù)制期間還要進行另外 的處理器加載。這一行為乍看起來有點荒謬,因為當(dāng)?shù)谌写a出現(xiàn)時,原始變量被釋放,從而使得整個數(shù)據(jù)復(fù)制顯得完全不必要。其實,我們不妨再遠(yuǎn)一層考慮, 讓我們設(shè)想當(dāng)一個10MB大小的文件的內(nèi)容被裝載到兩個變量中時會發(fā)生什么。這將會占用20MB的空間,此時,10已經(jīng)足夠了。引擎會把那么多的時間和內(nèi) 存浪費在這樣一種無用的努力上嗎?
你應(yīng)該知道,PHP的設(shè)計者早已深諳此理。
記住,在引擎中,變量名和它們的值實際上是兩個不同的概念。值本身是一個無名的zval*存儲體(在本例中,是一個字符串值),它被通過zend_hash_add()賦給變量$a。如果兩個變量名都指向同一個值,會發(fā)生什么呢?
{
zval *helloval;
MAKE_STD_ZVAL(helloval);
ZVAL_STRING(helloval, "Hello World", 1);
zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),&helloval, sizeof(zval*), NULL);
zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),&helloval, sizeof(zval*), NULL);
}
此時,你可以實際地觀察$a或$b,并且會看到它們都包含字符串"Hello World"。遺憾的是,接下來,你繼續(xù)執(zhí)行第三行代碼"unset($a);"。此時,unset()并不知道$a變量指向的數(shù)據(jù)還被另一個變量所使 用,因此它只是盲目地釋放掉該內(nèi)存。任何隨后的對變量$b的存取都將被分析為已經(jīng)釋放的內(nèi)存空間并因此導(dǎo)致引擎崩潰。
這個問題可以借助于zval(它有好幾種形式)的第四個成員refcount加以解決。當(dāng)一個變量被首次創(chuàng)建并賦值時,它的refcount被 初始化為1,因為它被假定僅由最初創(chuàng)建它時相應(yīng)的變量所使用。當(dāng)你的代碼片斷開始把helloval賦給$b時,它需要把refcount的值增加為2; 這樣以來,現(xiàn)在該值被兩個變量所引用:
{
zval *helloval;
MAKE_STD_ZVAL(helloval);
ZVAL_STRING(helloval, "Hello World", 1);
zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),&helloval, sizeof(zval*), NULL);
ZVAL_ADDREF(helloval);
zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),&helloval,sizeof(zval*),NULL);
}
現(xiàn)在,當(dāng)unset()刪除原變量的$a相應(yīng)的副本時,它就能夠從refcount參數(shù)中看到,還有另外其他人對該數(shù)據(jù)感興趣;因此,它應(yīng)該只是減少refcount的計數(shù)值,然后不再管它。
六、 寫復(fù)制(Copy on Write)
通過refcounting來節(jié)約內(nèi)存的確是不錯的主意,但是,當(dāng)你僅想改變其中一個變量的值時情況會如何呢?為此,請考慮下面的代碼片斷:
<?php
$a = 1;
$b = $a;
$b += 5;
>
通過上面的邏輯流程,你當(dāng)然知道$a的值仍然等于1,而$b的值最后將是6。并且此時,你還知道,Zend在盡力節(jié)省內(nèi)存-通過使$a和$b都引用相同的zval(見第二行代碼)。那么,當(dāng)執(zhí)行到第三行并且必須改變$b變量的值時,會發(fā)生什么情況呢?
回答是,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) {
/* 變量根本并不存在-失敗而導(dǎo)致退出*/
return NULL;
}
if ((*varval)->refcount < 2) {
/* varname是唯一的實際引用,
*不需要進行分離
*/
return *varval;
}
/* 否則,再復(fù)制一份zval*的值*/
MAKE_STD_ZVAL(varcopy);
varcopy = *varval;
/* 復(fù)制任何在zval*內(nèi)的已分配的結(jié)構(gòu)*/
zval_copy_ctor(varcopy);
/*刪除舊版本的varname
*這將減少該過程中varval的refcount的值
*/
zend_hash_del(EG(active_symbol_table), varname, varname_len + 1);
/*初始化新創(chuàng)建的值的引用計數(shù),并把它依附到
* 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;
}
現(xiàn)在,既然引擎有一個僅為變量$b所擁有的zval*(引擎能知道這一點),所以它能夠把這個值轉(zhuǎn)換成一個long型值并根據(jù)腳本的請求給它增加5。
七、 寫改變(change-on-write)
引用計數(shù)概念的引入還導(dǎo)致了一個新的數(shù)據(jù)操作可能性,其形式從用戶空間腳本管理器看來與"引用"有一定關(guān)系。請考慮下列的用戶空間代碼片斷:
<?php
$a = 1;
$b = &$a;
$b += 5;
>
在上面的PHP代碼中,你能看出$a的值現(xiàn)在為6,盡管它一開始為1并且從未(直接)發(fā)生變化。之所以會發(fā)生這種情況是因為當(dāng)引擎開始把$b的 值增加5時,它注意到$b是一個對$a的引用并且認(rèn)為"我可以改變該值而不必分離它,因為我想使所有的引用變量都能看到這一改變"。
但是,引擎是如何知道的呢?很簡單,它只要查看一下zval結(jié)構(gòu)的第四個和最后一個元素(is_ref)即可。這是一個簡單的開/關(guān)位,它定義 了該值是否實際上是一個用戶空間風(fēng)格引用集的一部分。在前面的代碼片斷中,當(dāng)執(zhí)行第一行時,為$a創(chuàng)建的值得到一個refcount為1,還有一個 is_ref值為0,因為它僅為一個變量($a)所擁有并且沒有其它變量對它產(chǎn)生寫引用改變。在第二行,這個值的refcount元素被增加為2,除了這 次is_ref元素被置為1之外(因為腳本中包含了一個"&"符號以指示是完全引用)。
最后,在第三行,引擎再一次取出與變量$b相關(guān)的值并且檢查是否有必要進行分離。這一次該值沒有被分離,因為前面沒有包括一個檢查。下面是get_var_and_separate()函數(shù)中與refcount檢查有關(guān)的部分代碼:
if ((*varval)->is_ref || (*varval)->refcount < 2) {
/* varname是唯一的實際引用,
* 或者它是對其它變量的一個完全引用
*任何一種方式:都沒有進行分離
*/
return *varval;
}
這一次,盡管refcount為2,卻沒有實現(xiàn)分離,因為這個值是一個完全引用。引擎能夠自由地修改它而不必關(guān)心其它變量值的變化。
八、 分離問題
盡管已經(jīng)存在上面討論到的復(fù)制和引用技術(shù),但是還存在一些不能通過is_ref和refcount操作來解決的問題。請考慮下面這個PHP代碼塊:
<?php
$a = 1;
$b = $a;
$c = &$a;
>
在此,你有一個需要與三個不同的變量相關(guān)聯(lián)的值。其中,兩個變量是使用了"change-on-write"完全引用方式,而第三個變量處于一 種可分離的"copy-on-write"(寫復(fù)制)上下文中。如果僅使用is_ref和refcount來描述這種關(guān)系,有哪些值能夠工作呢?
回答是:沒有一個能工作。在這種情況下,這個值必須被復(fù)制到兩個分離的zval*中,盡管兩者都包含完全相同的數(shù)據(jù)(見圖3)。
同樣,下列代碼塊將引起相同的沖突并且強迫該值分離出一個副本(見圖4)。
<?php
$a = 1;
$b = &$a;
$c = $a;
>
注意,在這里的兩種情況下,$b都與原始的zval對象相關(guān)聯(lián),因為在分離發(fā)生時引擎無法知道介于到該操作當(dāng)中的第三個變量的名字。
九、 總結(jié)
PHP是一種托管語言。從普通用戶角度來看,這種仔細(xì)地控制資源和內(nèi)存的方式意味著更為容易地進行原型開發(fā)并導(dǎo)致出現(xiàn)更少的沖突。然而,當(dāng)我們深入"內(nèi)里"之后,一切的承諾似乎都不復(fù)存在,最終還要依賴于真正有責(zé)任心的開發(fā)者來維持整個運行時刻環(huán)境的一致性。
from: http://www.phpchina.com/?action_viewnews_itemid_2447.html