天天看點

四十四、PHP核心探索:Zend記憶體管理器 非常類似于作業系統的記憶體管理功能

在PHP裡,我們可以定義字元串變量,比如 <?php $str="nowamagic"; ?>,$str這個字元串變量可以被自由的修改與複制等。這一切在C語言裡看起來都是不可能的事情,我們用#char *p = "hello";#來定義一個字元串,但它是常量,是不能被修改的,如果你用p[1]='c';來修改這個字元串會引發段錯誤(Gcc,c99),為了修改C語言裡的字元串常量,我們往往需要定義字元串數組。為了得到一個能夠讓我們自由修改的字元串,我們往往需要用strdup函數來複制一個字元串出來。

{

char *p = "hello world";

// p[0] = 'a'; 如果這麼做,就等着運作時段錯誤吧。

char *str;

str = strdup(p);

str[0] = 'a'; //這時就能自由修改了。

}

在PHP核心中,大多數情況下都不應改直接使用C語言中自帶着malloc、free、strdup、realloc、calloc等操作記憶體的函數,而應使用核心提供的操作記憶體的函數,這樣可以由核心整體統一的來管理記憶體。

Free the Mallocs

每個平台操作記憶體的方式都是差不多的有兩個方面,一負責申請,二負責釋放。如果應用程式向系統申請記憶體,系統便會在記憶體中尋找還沒有被使用的地方,如果有合适的,便配置設定給這個程式,并标記下來,不再給其它的程式了。如果一個記憶體塊沒有釋放,而所有者應用程式也永遠不再使用它了。那麼,我們就稱其為"記憶體洩漏",那麼這部分記憶體就無法再為其它程式所用了。

在一個典型的用戶端應用程式中,偶爾的小量的記憶體洩漏是可以被作業系統容忍的,因為在程序結束後該洩漏記憶體會被傳回給OS。這并沒有什麼高科技含量,因為OS知道它把該記憶體配置設定給了哪個程式,并且它能夠在一個程式結束後把這些記憶體給回收回來。

但是,世界總是不缺乏特例!對于一些需要長時間運作的程式,比如像Apache這樣的web伺服器以及它的php子產品來說,都是伴随着作業系統長時間運作的,是以OS在很長一段時間内不能主動的回收記憶體,進而導緻這個程式的每一個記憶體洩漏都會促進量變到質變的進化,最終引起嚴重的記憶體洩漏錯誤,使系統的資源消耗殆盡。現在,我們來在C語言中故意錯誤的模拟一下PHP的stristr()函數為例,為了使用大小寫不敏感的方式來搜尋一個字元串,我們需要建立兩個輔助的字元串,它們分别是被查找字元串和待查找字元串的小寫化副本,然後由這兩個副本來幫助我們來完成這次搜尋。如果我們在執行這個函數後不釋放這些副本占用的資源,那麼每一次stristr函數都将是對記憶體的一次永遠的侵占,最終導緻這個函數占用了所有的系統記憶體,而沒有實際意義!

大多數人提出來的理想的解決方案是:書寫優秀,整潔并且風格一緻的代碼,這當然是毫無疑問的。但是在PHP擴充開發這樣的底層環境中,這并不能解決全部的問題。比如,你需要自己保證在層層嵌套調用中對某塊記憶體的使用都是正确的,且會及時釋放的。

錯誤處理

為了實作從使用者端(PHP語言中)"跳出",需要使用一種方法來完全"跳出"一個活動請求。這個功能是在核心中實作的:在一個請求的開始設定一個"跳出"位址,然後在任何die()或exit()調用或在遇到任何關鍵錯誤(E_ERROR)時執行一個longjmp()以跳轉到該"跳出"位址。

void call_function(const char *fname, int fname_len TSRMLS_DC)
{
    zend_function *fe;
    char *lcase_fname;
    /* php函數的名字是大小寫不敏感的
     * 我們可以在function tables裡找到他們
     * 儲存的所有函數名都是小寫的。
     */
    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這個函數被調用的時候,便會觸發核心中的錯誤處理機制,根據錯誤級别來決定是否調用longjmp來終止目前請求并退出call_function函數,進而efree函數便永遠不會被執行了。

其實php_error_docref()函數就相當與php語言裡的trigger_error()函數.它的第一個參數是一個将被添加到docref的可選的文檔引用第三個參數可以是任何我們熟悉的E_*家族常量,用于訓示錯誤的嚴重程度。後面的兩個參數就像printf()風格的格式化和變量參數清單式樣。

Zend記憶體管理器

在上面的"跳出"請求期間解決記憶體洩漏的方案之一是:使用Zend記憶體管理(Zend Memory Manager,簡稱ZendMM、ZMM)層。核心的這一部分非常類似于作業系統的記憶體管理功能——配置設定記憶體給調用程式。差別在于,它處于程序空間中非常低的位置而且是"請求感覺"的;這樣以來,當一個請求結束時,它能夠執行與OS在一個程序終止時相同的行為。也就是說,它會隐式地釋放所有的為該請求所占用的記憶體。下圖展示了ZendMM與OS以及PHP程序之間的關系。

​​

四十四、PHP核心探索:Zend記憶體管理器 非常類似于作業系統的記憶體管理功能

​​

除了提供隐式的記憶體清除功能之外,ZendMM還能夠根據php.ini中memory_limit設定來控制每一次記憶體請求行為,如果一個腳本試圖請求比系統中可用記憶體更多的記憶體,或大于它每次應該請求的最大量,那麼,ZendMM将自動地發出一個E_ERROR消息并且啟動相應的終止程序。這種方法的一個額外優點在于,大多數記憶體配置設定調用的傳回值并不需要檢查,因為如果失敗的話将會導緻立即跳轉到引擎的退出部分。

把PHP核心代碼和OS的實際的記憶體管理層"鈎"在一起的原理并不複雜:所有内部配置設定的記憶體都要使用一組特定的可選函數實作。例如,PHP核心代碼不是使用malloc(16)來配置設定一個16位元組記憶體塊而是使用了emalloc(16)。除了實作實際的記憶體配置設定任務外,ZendMM還會使用相應的綁定請求類型來标志該記憶體塊;這樣以來,當一個請求"跳出"時,ZendMM可以隐式地釋放它。

有些事後,某次申請的記憶體需要在一個請求結束後仍然存活一段時間,也就是持續性存在于各個請求之間。這種類型的配置設定(因其在一次請求結束之後仍然存在而被稱為"永久性配置設定"),可以使用傳統型記憶體配置設定器來實作,因為這些配置設定并不會添加ZendMM使用的那些額外的相應于每種請求的資訊。然而有時,我們必須在程式運作時根據某個資料的具體值或者狀态才能确定是否需要進行永久性配置設定,是以ZendMM定義了一組幫助宏,其行為類似于其它的記憶體配置設定函數,但是使用最後一個額外參數來訓示是否為永久性配置設定。

如果你确實想實作一個永久性配置設定,那麼這個參數應該被設定為1;在這種情況下,請求是通過傳統型malloc()配置設定器家族進行傳遞的。然而,如果運作時刻邏輯認為這個塊不需要永久性配置設定;那麼,這個參數可以被設定為零,并且調用将會被調整到針對每種請求的記憶體配置設定器函數。

例如,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中提供的記憶體管理函數都能夠從下表中找到其在C語言中的函數。

C語言原生函數 PHP核心封裝後的函數
void *malloc(size_t count);

void *emalloc(size_t count);

void *pemalloc(size_t count, char persistent);

void *calloc(size_t count);

void *ecalloc(size_t count);

void *pecalloc(size_t count, char persistent);

void *realloc(void *ptr, size_t count);

void *erealloc(void *ptr, size_t count);

void *perealloc(void *ptr, size_t count, char persistent);

void *strdup(void *ptr);

void *estrdup(void *ptr);

void *pestrdup(void *ptr, char persistent);

void free(void *ptr);

void efree(void *ptr);

void pefree(void *ptr, char persistent);

你可能會注意到,即使是pefree()函數也要求使用永久性标志。這是因為在調用pefree()時,它實際上并不知道是否ptr是一種永久性配置設定。需要注意的是,如果針對一個ZendMM申請的非永久性記憶體直接調用free()能夠導緻雙倍的空間釋放,而針對一種永久性配置設定調用efree()有可能會導緻一個段錯誤,因為ZendMM需要去查找并不存在的管理資訊。是以,你的代碼需要記住它申請的記憶體是否是永久性的,進而選擇不同的記憶體函數,free()或者efree()。

除了上述記憶體管理函數外,還存在其它一些非常友善的ZendMM函數,例如:

void *estrndup(void *ptr,int len);

該函數能夠配置設定len+1個位元組的記憶體并且從ptr處複制len個位元組到最新配置設定的塊。這個estrndup()函數的行為可以大緻描述如下:

ZEND_API char *_estrndup(const char *s, uint length ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)

{

char *p;

p = (char *) _emalloc(length+1 ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);

if (UNEXPECTED(p == NULL))

{

return p;

}

memcpy(p, s, length);

p[length] = 0;

return p;

}

在此,被隐式放置在緩沖區最後的0可以確定任何使用estrndup()實作字元串複制操作的函數都不需要擔心會把結果緩沖區傳遞給一個例如printf()這樣的希望以為NULL為結束符的函數。當使用estrndup()來複制非字元串資料時,最後一個位元組實質上浪費了,但其中的利明顯大于弊。

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);