摘要 內存管理對於長期運行的程序,例如服務器守護程序,是相當重要的影響;因此,理解PHP是如何分配與釋放內存的對於創建這類程序極為重要。本文將重點探討PHP的內存管理問題。
一、 內存
在PHP中,填充一個字符串變量相當簡單,這只需要一個語句"<?php $str = 'hello world '; ?>"即可,並且該字符串能夠被自由地修改、拷貝和移動。而在C語言中,盡管你能夠編寫例如"char *str = "hello world ";"這樣的一個簡單的靜態字符串;但是,卻不能修改該字符串,因為它生存於程序空間內。為了創建一個可操縱的字符串,你必須分配一個內存塊,並且通過一 個函數(例如strdup())來復制其內容。
由於後面我們將分析的各種原因,傳統型內存管理函數(例如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()調用)並最終導致出現內存漏洞。現在,讓我們來考慮下面這個簡化版本的處理函數調用的引擎代碼:
當 執行到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內存管理器代替系統調用來實現針對每一種請求的內存分配。
除 了提供隱式內存清除功能之外,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中提供的分配器函數都能夠從下表中找到其更傳統的對應實現。
表格1展示了ZendMM支持下的每一個分配器函數以及它們的e/pe對應實現:
表格1.傳統型相對於PHP特定的分配器。