# 3.1 內存管理
在PHP里,我們可以定義字符串變量,比如<?php $str="hello";?>,$str這個字符串變量可以被自由的修改與復制等。這一切在C語言里看起來都是不可能的事情,我們用#char *p = "hello";#來定義一個字符串,但它是常量,是不能被修改的,如果你用p[1]='c';來修改這個字符串會引發段錯誤(Gcc,c99),為了修改C語言里的字符串常量,我們往往需要定義字符串數組。為了得到一個能夠讓我們自由修改的字符串,我們往往需要用strdup函數來復制一個字符串出來。
````c
{
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擴展開發這樣的底層環境中,這并不能解決全部的問題。比如,你需要自己保證在層層嵌套調用中對某塊內存的使用都是正確的,且會及時釋放的。<sup> [1](#sup1) </sup>
### 錯誤處理
為了實現從用戶端(PHP語言中)"跳出",需要使用一種方法來完全"跳出"一個活動請求。這個功能是在內核中實現的:在一個請求的開始設置一個"跳出"地址,然后在任何die()或exit()調用或在遇到任何關鍵錯誤(E_ERROR)時執行一個longjmp()以跳轉到該"跳出"地址。
````c
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) == SUCCESS)
{
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函數便永遠不會被執行了。
<div class="tip-common">
其實php_error_docref()函數就相當于php語言里的trigger_error()函數.它的第一個參數是一個將被添加到docref的可選的文檔引用第三個參數可以是任何我們熟悉的E_*家族常量,用于指示錯誤的嚴重程度。后面的兩個參數就像printf()風格的格式化和變量參數列表式樣。</div>
### Zend內存管理器
在上面的"跳出"請求期間解決內存泄漏的方案之一是:使用Zend內存管理(Zend Memory Manager,簡稱ZendMM、ZMM)層。內核的這一部分非常類似于操作系統的內存管理功能——分配內存給調用程序。區別在于,它處于進程空間中非常低的位置而且是"請求感知"的;這樣一來,當一個請求結束時,它能夠執行與OS在一個進程終止時相同的行為。也就是說,它會隱式地釋放所有的為該請求所占用的內存。圖1展示了ZendMM與OS以及PHP進程之間的關系。
<p style="text-align:center;"><img src="http://www.walu.cc/phpbook/image/03fig01.jpg" />
除了提供隱式的內存清除功能之外,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):
````c
//define in Zend/zend_alloc.h:
#define pemalloc(size, persistent) ((persistent)?malloc(size): emalloc(size))
````
所有這些在ZendMM中提供的內存管理函數都能夠從下表中找到其在C語言中的函數。
<table class="table-common">
<tr>
<td>C語言原生函數</td>
<td>PHP內核封裝后的函數</td>
</tr>
<tr>
<td>void *malloc(size_t count);</td>
<td>void *emalloc(size_t count);<br />void *pemalloc(size_t count, char persistent);</td>
</tr>
<tr>
<td>void *calloc(size_t count);</td>
<td>void *ecalloc(size_t count);<br />void *pecalloc(size_t count, char persistent);</td>
</tr>
<tr>
<td>void *realloc(void *ptr, size_t count);</td>
<td>void *erealloc(void *ptr, size_t count);<br />void *perealloc(void *ptr, size_t count, char persistent);</td>
</tr>
<tr>
<td>void *strdup(void *ptr);</td>
<td>void *estrdup(void *ptr);<br />void *pestrdup(void *ptr, char persistent);</td>
</tr>
<tr>
<td>void free(void *ptr);</td>
<td>void efree(void *ptr);<br />void pefree(void *ptr, char persistent);</td>
</tr>
</table>
你可能會注意到,即使是pefree()函數也要求使用永久性標志。這是因為在調用pefree()時,它實際上并不知道是否ptr是一種永久性分配。需要注意的是,如果針對一個ZendMM申請的非永久性內存直接調用free()能夠導致雙倍的空間釋放,而針對一種永久性分配調用efree()有可能會導致一個段錯誤,因為ZendMM需要去查找并不存在的管理信息。因此,你的代碼需要記住它申請的內存是否是永久性的,從而選擇不同的內存函數,free()或者efree()。
除了上述內存管理函數外,還存在其它一些非常方便的ZendMM函數,例如:
````c
void *estrndup(void *ptr,int len);
````
該函數能夠分配len+1個字節的內存并且從ptr處復制len個字節到最新分配的塊。這個estrndup()函數的行為可以大致描述如下:
````c
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()來復制非字符串數據時,最后一個字節實質上浪費了,但其中的利明顯大于弊。
````c
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);
````
這些函數分配的內存空間最終大小都是((size*count)+addtl)。
你可以會問:"為什么還要提供額外函數呢?為什么不使用一個emalloc/pemalloc呢?"。
原因很簡單:為了安全,以防萬一。盡管有時候可能性相當小,但是,正是這一"可能性相當小"的結果導致宿主平臺的內存溢出。
這可能會導致分配負數個數的字節空間,或更有甚者,會導致分配一個小于調用程序要求大小的字節空間。
而safe_emalloc()能夠避免這種類型的陷井-通過檢查整數溢出并且在發生這樣的溢出時顯式地預以結束。
> 注意,并不是所有的內存分配例程都有一個相應的p*對等實現。例如,不存在pestrndup(),并且在PHP 5.1版本前也不存在safe_pemalloc()。
<hr />
<dl>
<dt>貢獻者名單</dt>
<dd id="sup1">絆大象的螞蟻{eaglevean#163.com},注1</dd>
</dl>
## links
* 3 [內存管理](<3.md>)
* 3.2 [引用計數](<3.2.md>)
- about
- 開始閱讀
- 目錄
- 1 PHP的生命周期
- 1.讓我們從SAPI開始
- 2.PHP的啟動與終止
- 3.PHP的生命周期
- 4.線程安全
- 5.小結
- 2 PHP變量在內核中的實現
- 1. 變量的類型
- 2. 變量的值
- 3. 創建PHP變量
- 4. 變量的存儲方式
- 5. 變量的檢索
- 6. 類型轉換
- 7. 小結
- 3 內存管理
- 1. 內存管理
- 2. 引用計數
- 3. 總結
- 4 動手編譯PHP
- 1. 編譯前的準備
- 2. PHP編譯前的config配置
- 3. Unix/Linux平臺下的編譯
- 4. 在Win32平臺上編譯PHP
- 5. 小結
- 5 Your First Extension
- 1. 一個擴展的基本結構
- 2. 編譯我們的擴展
- 3. 靜態編譯
- 4. 編寫函數
- 5. 小結
- 6 函數返回值
- 1. 一個特殊的參數:return_value
- 2. 引用與函數的執行結果
- 3. 小結
- 7 函數的參數
- 1. zend_parse_parameters
- 2. Arg Info 與類型綁定
- 3. 小結
- 8 使用HashTable與{數組}
- 1. 數組(C中的)與鏈表
- 2. 操作HashTable的API
- 3. 在內核中操作PHP語言中數組
- 4. 小結
- 9 PHP中的資源類型
- 1. 復合類型的數據——{資源}
- 2. Persistent Resources
- 3. {資源}自有的引用計數
- 4. 小結
- 10 PHP中的面向對象(一)
- 1. zend_class_entry
- 2. 定義一個類
- 3. 定義一個接口
- 4. 類的繼承與接口的實現
- 5. 小結
- 11 PHP中的面向對象(二)
- 1. 生成對象的實例與調用方法
- 2. 讀寫對象的屬性
- 3. 小結
- 12 啟動與終止的那點事
- 2. 小結
- 1. 關于生命周期
- 2. MINFO與phpinfo
- 3. 常量
- 4. PHP擴展中的全局變量
- 5. PHP語言中的超級全局變量
- 6. 小結
- 13 INI設置
- 1. 聲明和訪問ini設置
- 2. 小結
- 2. 小結
- 14 流式訪問
- 1. 概覽
- 2. 打開流
- 3. 訪問流
- 4. 靜態資源操作
- 5. 小結
- 15 流的實現
- 1. php流的表象之下
- 2. 包裝器操作
- 3. 實現一個包裝器
- 4. 操縱
- 5. 檢查
- 6. 小結
- 16 有趣的流
- 1. 上下文
- 2. 過濾器
- 3. 小結
- 17 配置和鏈接
- 1. autoconf
- 2. 庫的查找
- 3. 強制模塊依賴
- 4. Windows方言
- 5. 小結
- 18 擴展生成
- 1. ext_skel
- 2. PECL_Gen
- 3. 小結
- 19 設置宿主環境
- 1. 嵌入式SAPI
- 2. 構建并編譯一個宿主應用
- 3. 通過嵌入包裝重新創建cli
- 4. 老技術新用
- 5. 小結
- 20 高級嵌入式
- 1. 回調到php中
- 2. 錯誤處理
- 3. 初始化php
- 4. 覆寫INI_SYSTEM和INI_PERDIR選項
- 5. 捕獲輸出
- 6. 同時擴展和嵌入
- 7. 小結
- 約定