1. 程式人生 > 其它 >php之 Zend 記憶體管理器

php之 Zend 記憶體管理器

Zend 記憶體管理器

Zend 記憶體管理器,經常縮寫為 ZendMM 或 ZMM,是一個 C 層,旨在提供分配和釋放動態請求繫結記憶體的能力。

注意上面句子中的“請求繫結”。

ZendMM 不僅僅是 libc 的動態記憶體分配器上的一個經典層,主要由兩個 API 呼叫malloc()/free()表示。ZendMM 是關於php在處理請求時必須分配的請求繫結記憶體。

PHP 中兩種主要的動態記憶體池

PHP 是一個無共享架構。 Well, not at 100%. Let us explain.

注意

在繼續之前,你可能需要閱讀 PHP 生命週期章節,你將獲得有關 PHP 生命週期中的不同步驟和週期的更多資訊。

PHP可以在同一個程序中處理數百或數千個請求。預設情況下,PHP 會在完成當前請求後,忘記對當前請求的任何資訊。

“忘記” 資訊解釋為釋放處理請求時分配的任何動態緩衝區。這意味著在處理一個請求的過程中,不能使用傳統的 libc 呼叫來分配動態記憶體。這樣做是完全有效的,但是您給忘記釋放緩衝區了機會。

ZendMM 附帶了一個 API,通過複製其 API 來替代 libc 的動態分配器。在處理請求的過程中,程式設計師必須使用該 API 而不是 libc 的分配器。

例如,當 PHP 處理請求時,它將解析 PHP 檔案。例如,那些將導致函式和類的宣告。當編譯器開始編譯 PHP 檔案時,它將分配一些動態記憶體來儲存它發現的類和函式。但是,在請求結束時,PHP 會釋放這些。預設情況下,PHP 會忘記從一個請求到另一個請求的大量資訊。

然而,存在一些非常罕見的資訊,你需要持久地跨越多個請求。但這並不常見。

什麼可以通過請求保持不變?我們所說的持久物件。再次說明:那是不常見的情況。例如,當前的 PHP 可執行路徑不會在請求之間更改。其資訊是永久分配的,這意味著它呼叫了 傳統 libc 的malloc ()來分配。

還有什麼? 一些字串。例如,“_SERVER” 字串將在請求之間重用,因為每個請求都將建立$_SERVERPHP 陣列。所以 “_SERVER” 字串本身可以永久分配,因為它只會被分配一次。

你必須記住:

  • 在編寫 PHP 核心或擴充套件時,存在兩種動態記憶體分配方式:

    • 請求繫結的動態分配。
    • 永久動態分配。
  • 請求繫結動態記憶體分配

    • 僅在PHP處理請求時才執行(不在此之前或之後)。
    • 應該只使用 ZendMM 動態記憶體分配 API 執行。
    • 在擴充套件設計中非常常見,基本上95%的動態分配都是請求繫結的。
    • 由 ZendMM 追蹤,並會通知你有關洩漏的資訊。
  • 永久動態記憶體分配

    • 不應該在PHP處理請求時執行(這不是禁止的,但是是一個壞主意)。
    • 不會被 ZendMM 追蹤,你也不會被告知洩漏。
    • 在擴充套件中應該很少見。

另外,請記住,所有 PHP 原始碼都基於這種記憶體級別。因此,許多內部結構使用 Zend 記憶體管理器進行分配。大多數都呼叫了一個“持久的” API,當呼叫這個時,將導致傳統的 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中,它始終是相同的。通常是作為最後一個引數傳遞的,“0”表示“我希望使用 ZendMM 分配此結構,因此請求繫結”,或“1”表示“我希望通過 ZendMM 呼叫傳統的 libc 的malloc()分配此結構”。

顯然,這些結構提供了一個 API,該 API 會記住它如何分配結構,以便在銷燬時使用正確的釋放函式。因此,在這樣的程式碼中:

zend_string_release(foo);
zend_hash_destroy(&ar);

API 知道這些結構是使用請求繫結分配還是永久分配的,第一種情況將使用efree()釋放它,第二種情況是libc的free()

Zend 記憶體管理器 API

該 API 位於 Zend/zend_alloc.h

API 主要是 C 巨集,而不是函式,因此,如果你除錯它們並想了解它們的工作原理,請做好準備。這些 API 複製了 libc 的函式,通常在函式名稱中新增“e”;因此,你不應該認錯,關於該API的細節不多。

基本上,你最常使用的是emalloc(size_t)efree(void *)

還提供了ecalloc(size_t nmemb,size_t size),它分配單個大小sizenmemb,並將區域歸零。如果你是一位經驗豐富的 C 程式設計師,那麼你應該知道,只要有可能,最好在emalloc()上使用ecalloc(),因為ecalloc()會將記憶體區域清零,這在指標錯誤檢測中可能會有很大幫助。請記住,emalloc()的工作原理基本上與libcmalloc()一樣:它將在不同的池中尋找足夠大的區域,併為你提供最合適的空間。因此,你可能會得到一個指向垃圾的回收指標。

然後是safe_emalloc(size_t nmemb,size_t size,size_t offset),這是emalloc(size * nmemb + offset),但它會為你檢查溢位情況。如果必須提供的數字來自不受信任的來源(例如使用者區),則應使用此API呼叫。

關於字串,estrdup(char *)estrndup(char *, size_t len)允許複製字串或二進位制字串。

無論發生什麼,ZendMM 返回的指標必須呼叫 ZendMM 的efree()釋放,而不是 libc 的 free()。

注意

關於持久分配的說明。持久分配在請求之間保持有效。你通常使用常見的 libcmalloc/ free來執行此操作,但是 ZendMM 有一些 libc 分配器的快捷方式:“持久” API。該 API以“p” 字母開頭,讓你在 ZendMM 分配或持久分配之間進行選擇。因此pemalloc(size_t, 1)不過是malloc()pefree(void *, 1)free()pestrdup(void *, 1)strdup()。只是說。

Zend 記憶體管理器除錯盾

ZendMM 提供以下功能:

  • 記憶體消耗管理。
  • 記憶體洩漏跟蹤和自動釋放。
  • 通過預分配已知大小的緩衝區並保持空閒狀態下的熱快取來加快分配速度

記憶體消耗管理

ZendMM 是 PHP 使用者區“memory_limit”功能的底層。使用 ZendMM 層分配的每單個位元組都會被計數並相加。當達到 INI 的 memory_limit 後,你知道會發生什麼。這也意味著通過 ZendMM 執行的任何分配都反映在 PHP 使用者區的memory_get_usage()中。

作為擴充套件開發人員,這是一件好事,因為它有助於掌握 PHP 程序的堆大小。

如果啟動了記憶體限制錯誤,則引擎將從當前程式碼位置釋放到捕獲塊,然後平穩終止。但是它不可能回到超出限制的程式碼位置。你必須為此做好準備。

從理論上講,這意味著 ZendMM 無法向你返回 NULL 指標。如果從作業系統分配失敗,或者分配產生記憶體限制錯誤,則程式碼將執行到 catch 塊中,並且不會返回到你的分配呼叫。

如果出於任何原因需要繞過該保護,則必須使用傳統的 libc 呼叫,例如malloc()。無論如何請小心,並且知道你在做什麼。如果使用 ZendMM,可能需要分配大量記憶體並可能超出 PHP 的 memory_limit。因此,請使用另一個分配器(如libc),但要注意:你的擴充套件將增加當前程序堆的大小。在 PHP 中不能看到memory_get_usage(),但是可以通過使用 OS 設施分析當前堆(如/proc/{pid}/maps)

注意

如果需要完全禁用 ZendMM,則可以使用USE_ZEND_ALLOC = 0環境變數啟動PHP。這樣,每次對 ZendMM API的呼叫(例如emalloc())都將定向到 libc 呼叫,並且 ZendMM 將被禁用。這在除錯記憶體的情況下尤其有用。

記憶體洩漏追蹤

請記住 ZendMM 的主要規則:它在請求啟動時啟動,然後在你處理請求時需要動態記憶體時期望你呼叫其API。當前請求結束時,ZendMM 關閉。

通過關閉,它將瀏覽其所有活動指標,如果使用 PHP 的除錯構建,它將警告你有關記憶體洩漏的資訊。

讓我們解釋得更清楚一些:如果在當前請求結束時,ZendMM 找到了一些活動的記憶體塊,則意味著這些記憶體塊正在洩漏。請求結束時,ZendMM 堆上不應存在任何活動記憶體塊,因為分配了某些記憶體的任何人都應該釋放了它們。

如果您忘記釋放塊,它們將全部顯示在 stderr上。此記憶體洩漏報告程序僅在以下情況下有效:

  • 你正在使用 PHP 的除錯構建
  • 在 php.ini 中具有 report_memleaks = On(預設)

這是一個簡單洩漏到擴充套件中的示例:

PHP_RINIT_FUNCTION(example)
{
    void *foo = emalloc(128);
}

在啟動該擴充套件的情況下啟動 PHP,在除錯版本上會在 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 ===

當 Zend 記憶體管理器關閉時,在每個已處理請求的末尾,將生成這些行。

但是要當心:

  • 顯然,ZendMM 對持久分配或以不同於使用持久分配的方式執行的分配一無所知。因此,ZendMM 只能警告你有關它知道的分配資訊,在這裡不會報告每個傳統的 libc 分配資訊。
  • 如果 PHP 以錯誤的方式關閉(我們稱之為不正常關閉),ZendMM 將報告大量洩漏。這是因為引擎在錯誤關閉時會使用longjmp()呼叫 catch 塊,防止清理所有記憶體的程式碼執行。因此,許多洩漏得到報告。尤其是在呼叫 PHP 的 exit()/ die()之後,或者在 PHP 的某些關鍵部分觸發了致命錯誤時,就會發生這種情況。
  • 如果你使用非除錯版本的 PHP,則 stderr 上不會顯示任何內容,ZendMM 是愚蠢的,但仍會清除程式設計師尚未明確釋放的所有分配的請求繫結緩衝區

你必須記住的是 ZendMM 洩漏跟蹤是一個不錯的獎勵工具,但它不能代替真正的 C 記憶體偵錯程式。

ZendMM 內部設計常見錯誤和錯誤

這是使用 ZendMM 時最常見的錯誤,以及你應該怎麼做。

  1. 不處理請求時使用 ZendMM。

獲取有關 PHP 生命週期的資訊,以瞭解在擴充套件中何時處理請求,何時不處理。如果在請求範圍之外使用 ZendMM(例如在MINIT()中),在處理第一個請求之前,ZendMM 會靜默清除分配,並且可能會使用after-after-free:根本沒有。

  1. 緩衝區上溢和下溢。

使用記憶體偵錯程式。如果你在 ZendMM 返回的記憶體區域以下或過去寫入內容,則將覆蓋關鍵的 ZendMM 結構並觸發崩潰。如果 ZendMM 能夠為你檢測到混亂,則可能會顯示“zend_mm_heap損壞”的訊息。堆疊追蹤將顯示從某些程式碼到某些 ZendMM 程式碼的崩潰。ZendMM 程式碼不會自行崩潰。如果你在 ZendMM 程式碼中間崩潰,那很可能意味著你在某個地方弄亂了指標。插入你喜歡的記憶體偵錯程式,查詢有罪的部分並進行修復。

  1. 混合 API 呼叫

如果分配一個 ZendMM 指標(即emalloc())並使用 libc 釋放它(free()),或相反的情況:你將崩潰。要嚴謹對待。另外,如果你將其不知道的任何指標傳遞給 ZendMM 的efree():將會崩潰。

以上就是php之 Zend 記憶體管理器的詳細內容,更多請關注考高分網其它相關文章!

文章轉自:http://www.kaotop.com/it/262787.html

本文來自部落格園,作者:考高分網,轉載請註明原文連結:http://www.kaotop.com/