十年網(wǎng)站開發(fā)經(jīng)驗 + 多家企業(yè)客戶 + 靠譜的建站團(tuán)隊
量身定制 + 運營維護(hù)+專業(yè)推廣+無憂售后,網(wǎng)站問題一站解決
Zend 內(nèi)存管理器,經(jīng)??s寫為 ZendMM或 ZMM,是一個 C 層,旨在提供分配和釋放動態(tài)請求綁定內(nèi)存的能力。
注意上面句子中的“請求綁定”。
ZendMM 不僅僅是 libc 的動態(tài)內(nèi)存分配器上的一個經(jīng)典層,主要由兩個 API 調(diào)用 malloc()/free()
表示。ZendMM 是關(guān)于 PHP 在處理請求時必須分配的請求綁定內(nèi)存。
PHP 是一個無共享架構(gòu)。 Well, not at 100%. Let us explain.
注意
在繼續(xù)之前,你可能需要閱讀 PHP 生命周期章節(jié),你將獲得有關(guān) PHP 生命周期中的不同步驟和周期的更多信息。
PHP可以在同一個進(jìn)程中處理數(shù)百或數(shù)千個請求。默認(rèn)情況下,PHP 會在完成當(dāng)前請求后,忘記對當(dāng)前請求的任何信息。
“忘記” 信息解釋為釋放處理請求時分配的任何動態(tài)緩沖區(qū)。這意味著在處理一個請求的過程中,不能使用傳統(tǒng)的 libc 調(diào)用來分配動態(tài)內(nèi)存。這樣做是完全有效的,但是您給忘記釋放緩沖區(qū)了機會。
ZendMM 附帶了一個 API,通過復(fù)制其 API 來替代 libc 的動態(tài)分配器。在處理請求的過程中,程序員必須使用該 API 而不是 libc 的分配器。
例如,當(dāng) PHP 處理請求時,它將解析 PHP 文件。例如,那些將導(dǎo)致函數(shù)和類的聲明。當(dāng)編譯器開始編譯 PHP 文件時,它將分配一些動態(tài)內(nèi)存來存儲它發(fā)現(xiàn)的類和函數(shù)。但是,在請求結(jié)束時,PHP 會釋放這些。默認(rèn)情況下,PHP 會忘記從一個請求到另一個請求的大量信息。
然而,存在一些非常罕見的信息,你需要持久地跨越多個請求。但這并不常見。
什么可以通過請求保持不變?我們所說的持久對象。再次說明:那是不常見的情況。例如,當(dāng)前的 PHP 可執(zhí)行路徑不會在請求之間更改。其信息是永久分配的,這意味著它調(diào)用了 傳統(tǒng) libc 的 malloc ()
來分配。
還有什么? 一些字符串。例如,“_SERVER”字符串將在請求之間重用,因為每個請求都將創(chuàng)建 $_SERVER
PHP 數(shù)組。所以 “_SERVER”字符串本身可以永久分配,因為它只會被分配一次。
你必須記住:
在編寫 PHP 核心或擴(kuò)展時,存在兩種動態(tài)內(nèi)存分配方式:
請求綁定的動態(tài)分配。永久動態(tài)分配。請求綁定動態(tài)內(nèi)存分配
僅在PHP處理請求時才執(zhí)行(不在此之前或之后)。應(yīng)該只使用 ZendMM 動態(tài)內(nèi)存分配 API 執(zhí)行。在擴(kuò)展設(shè)計中非常常見,基本上95%的動態(tài)分配都是請求綁定的。由 ZendMM 追蹤,并會通知你有關(guān)泄漏的信息。永久動態(tài)內(nèi)存分配
不應(yīng)該在PHP處理請求時執(zhí)行(這不是禁止的,但是是一個壞主意)。不會被 ZendMM 追蹤,你也不會被告知泄漏。在擴(kuò)展中應(yīng)該很少見。另外,請記住,所有 PHP 源代碼都基于這種內(nèi)存級別。因此,許多內(nèi)部結(jié)構(gòu)使用 Zend 內(nèi)存管理器進(jìn)行分配。大多數(shù)都調(diào)用了一個“持久的” API,當(dāng)調(diào)用這個時,將導(dǎo)致傳統(tǒng)的 libc 分配。
這是一個請求綁定的分配 zend_string:
zend_string *foo = zend_string_init("foo", strlen("foo"), 0);
這是持久分配的:
zend_string *foo = zend_string_init("foo", strlen("foo"), 1);
同樣的 HashTable。
請求綁定分配:
zend_array ar; zend_hash_init(&ar, 8, NULL, NULL, 0);
持久分配:
zend_array ar; zend_hash_init(&ar, 8, NULL, NULL, 1);
在所有不同的 Zend API中,它始終是相同的。通常是作為最后一個參數(shù)傳遞的,“0”表示“我希望使用 ZendMM 分配此結(jié)構(gòu),因此請求綁定”,或“1”表示“我希望通過 ZendMM 調(diào)用傳統(tǒng)的 libc 的malloc()
分配此結(jié)構(gòu)”。
顯然,這些結(jié)構(gòu)提供了一個 API,該 API 會記住它如何分配結(jié)構(gòu),以便在銷毀時使用正確的釋放函數(shù)。因此,在這樣的代碼中:
zend_string_release(foo); zend_hash_destroy(&ar);
API 知道這些結(jié)構(gòu)是使用請求綁定分配還是永久分配的,第一種情況將使用efree()
釋放它,第二種情況是libc的free()
。
該 API 位于 Zend/zend_alloc.h
API 主要是 C 宏,而不是函數(shù),因此,如果你調(diào)試它們并想了解它們的工作原理,請做好準(zhǔn)備。這些 API 復(fù)制了 libc 的函數(shù),通常在函數(shù)名稱中添加“e”;因此,你不應(yīng)該認(rèn)錯,關(guān)于該API的細(xì)節(jié)不多。
基本上,你最常使用的是 emalloc(size_t)
和efree(void *)
。
還提供了ecalloc(size_t nmemb,size_t size)
,它分配單個大小size
的nmemb
,并將區(qū)域歸零。如果你是一位經(jīng)驗豐富的 C 程序員,那么你應(yīng)該知道,只要有可能,最好在emalloc()
上使用ecalloc()
,因為ecalloc()
會將內(nèi)存區(qū)域清零,這在指針錯誤檢測中可能會有很大幫助。請記住,emalloc()
的工作原理基本上與libc malloc()
一樣:它將在不同的池中尋找足夠大的區(qū)域,并為你提供最合適的空間。因此,你可能會得到一個指向垃圾的回收指針。
然后是 safe_emalloc(size_t nmemb,size_t size,size_t offset)
,這是emalloc(size * nmemb + offset)
,但它會為你檢查溢出情況。如果必須提供的數(shù)字來自不受信任的來源(例如用戶區(qū)),則應(yīng)使用此API調(diào)用。
關(guān)于字符串,estrdup(char *)
和 estrndup(char *, size_t len)
允許復(fù)制字符串或二進(jìn)制字符串。
無論發(fā)生什么,ZendMM 返回的指針必須調(diào)用 ZendMM 的efree()
釋放,而不是 libc 的 free()。
Zend 內(nèi)存管理器調(diào)試盾注意
關(guān)于持久分配的說明。持久分配在請求之間保持有效。你通常使用常見的 libc
malloc/ free
來執(zhí)行此操作,但是 ZendMM 有一些 libc 分配器的快捷方式:“持久” API。該 API以“p” 字母開頭,讓你在 ZendMM 分配或持久分配之間進(jìn)行選擇。因此pemalloc(size_t, 1)
不過是malloc()
,pefree(void *, 1)
是free()
,pestrdup(void *, 1)
是strdup()
。只是說。
ZendMM 提供以下功能:
內(nèi)存消耗管理。內(nèi)存泄漏跟蹤和自動釋放。通過預(yù)分配已知大小的緩沖區(qū)并保持空閑狀態(tài)下的熱緩存來加快分配速度內(nèi)存消耗管理ZendMM 是 PHP 用戶區(qū)“memory_limit”功能的底層。使用 ZendMM 層分配的每單個字節(jié)都會被計數(shù)并相加。當(dāng)達(dá)到 INI 的 memory_limit后,你知道會發(fā)生什么。這也意味著通過 ZendMM 執(zhí)行的任何分配都反映在 PHP 用戶區(qū)的memory_get_usage()
中。
作為擴(kuò)展開發(fā)人員,這是一件好事,因為它有助于掌握 PHP 進(jìn)程的堆大小。
如果啟動了內(nèi)存限制錯誤,則引擎將從當(dāng)前代碼位置釋放到捕獲塊,然后平穩(wěn)終止。但是它不可能回到超出限制的代碼位置。你必須為此做好準(zhǔn)備。
從理論上講,這意味著 ZendMM 無法向你返回 NULL 指針。如果從操作系統(tǒng)分配失敗,或者分配產(chǎn)生內(nèi)存限制錯誤,則代碼將運行到 catch 塊中,并且不會返回到你的分配調(diào)用。
如果出于任何原因需要繞過該保護(hù),則必須使用傳統(tǒng)的 libc 調(diào)用,例如malloc()
。無論如何請小心,并且知道你在做什么。如果使用 ZendMM,可能需要分配大量內(nèi)存并可能超出 PHP 的 memory_limit。因此,請使用另一個分配器(如libc),但要注意:你的擴(kuò)展將增加當(dāng)前進(jìn)程堆的大小。在 PHP 中不能看到 memory_get_usage()
,但是可以通過使用 OS 設(shè)施分析當(dāng)前堆(如/proc/{pid}/maps)
內(nèi)存泄漏追蹤注意
如果需要完全禁用 ZendMM,則可以使用
USE_ZEND_ALLOC = 0
環(huán)境變量啟動PHP。這樣,每次對 ZendMM API的調(diào)用(例如emalloc())都將定向到 libc 調(diào)用,并且 ZendMM 將被禁用。這在調(diào)試內(nèi)存的情況下尤其有用。
請記住 ZendMM 的主要規(guī)則:它在請求啟動時啟動,然后在你處理請求時需要動態(tài)內(nèi)存時期望你調(diào)用其API。當(dāng)前請求結(jié)束時,ZendMM 關(guān)閉。
通過關(guān)閉,它將瀏覽其所有活動指針,如果使用 PHP 的調(diào)試構(gòu)建,它將警告你有關(guān)內(nèi)存泄漏的信息。
讓我們解釋得更清楚一些:如果在當(dāng)前請求結(jié)束時,ZendMM 找到了一些活動的內(nèi)存塊,則意味著這些內(nèi)存塊正在泄漏。請求結(jié)束時,ZendMM 堆上不應(yīng)存在任何活動內(nèi)存塊,因為分配了某些內(nèi)存的任何人都應(yīng)該釋放了它們。
如果您忘記釋放塊,它們將全部顯示在 stderr上。此內(nèi)存泄漏報告進(jìn)程僅在以下情況下有效:
你正在使用 PHP 的調(diào)試構(gòu)建在 php.ini 中具有 report_memleaks = On(默認(rèn))這是一個簡單泄漏到擴(kuò)展中的示例:
PHP_RINIT_FUNCTION(example) { void *foo = emalloc(128); }
在啟動該擴(kuò)展的情況下啟動 PHP,在調(diào)試版本上會在 stderr 上生成:
[Fri Jun 9 16:04:59 2017] Script: '/tmp/foobar.php' /path/to/extension/file.c(123) : Freeing 0x00007fffeee65000 (128 bytes), script=/tmp/foobar.php === Total 1 memory leaks detected ===
當(dāng) Zend 內(nèi)存管理器關(guān)閉時,在每個已處理請求的末尾,將生成這些行。
但是要當(dāng)心:
顯然,ZendMM 對持久分配或以不同于使用持久分配的方式執(zhí)行的分配一無所知。因此,ZendMM 只能警告你有關(guān)它知道的分配信息,在這里不會報告每個傳統(tǒng)的 libc 分配信息。如果 PHP 以錯誤的方式關(guān)閉(我們稱之為不正常關(guān)閉),ZendMM 將報告大量泄漏。這是因為引擎在錯誤關(guān)閉時會使用longjmp()調(diào)用 catch 塊,防止清理所有內(nèi)存的代碼運行。因此,許多泄漏得到報告。尤其是在調(diào)用 PHP 的 exit()/ die()之后,或者在 PHP 的某些關(guān)鍵部分觸發(fā)了致命錯誤時,就會發(fā)生這種情況。如果你使用非調(diào)試版本的 PHP,則 stderr上不會顯示任何內(nèi)容,ZendMM 是愚蠢的,但仍會清除程序員尚未明確釋放的所有分配的請求綁定緩沖區(qū)你必須記住的是 ZendMM 泄漏跟蹤是一個不錯的獎勵工具,但它不能代替真正的 C 內(nèi)存調(diào)試器。
ZendMM 內(nèi)部設(shè)計常見錯誤和錯誤這是使用 ZendMM 時最常見的錯誤,以及你應(yīng)該怎么做。
不處理請求時使用 ZendMM。獲取有關(guān) PHP 生命周期的信息,以了解在擴(kuò)展中何時處理請求,何時不處理。如果在請求范圍之外使用 ZendMM(例如在MINIT()中),在處理第一個請求之前,ZendMM 會靜默清除分配,并且可能會使用after-after-free:根本沒有。
緩沖區(qū)上溢和下溢。使用內(nèi)存調(diào)試器。如果你在 ZendMM 返回的內(nèi)存區(qū)域以下或過去寫入內(nèi)容,則將覆蓋關(guān)鍵的 ZendMM 結(jié)構(gòu)并觸發(fā)崩潰。如果 ZendMM 能夠為你檢測到混亂,則可能會顯示“zend_mm_heap損壞”的消息。堆棧追蹤將顯示從某些代碼到某些 ZendMM 代碼的崩潰。ZendMM 代碼不會自行崩潰。如果你在 ZendMM 代碼中間崩潰,那很可能意味著你在某個地方弄亂了指針。插入你喜歡的內(nèi)存調(diào)試器,查找有罪的部分并進(jìn)行修復(fù)。
混合 API 調(diào)用如果分配一個 ZendMM 指針(即emalloc()
)并使用 libc 釋放它(free()
),或相反的情況:你將崩潰。要嚴(yán)謹(jǐn)對待。另外,如果你將其不知道的任何指針傳遞給 ZendMM 的efree()
:將會崩潰。
網(wǎng)站標(biāo)題:php之Zend內(nèi)存管理器
網(wǎng)頁鏈接:http://m.jiaotiyi.com/article/cheegj.html