首頁  >  文章  >  後端開發  >  php之 Zend 記憶體管理器

php之 Zend 記憶體管理器

coldplay.xixi
coldplay.xixi轉載
2020-07-29 17:14:152393瀏覽

php之 Zend 記憶體管理器

Zend 記憶體管理器

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

注意上面句子中的「請求綁定」。

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

相關學習推薦: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” 字串將在請求之間重複使用,因為每個請求都會建立 $_SERVER PHP 陣列。所以 “_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()的工作原理基本上與libc malloc()一樣:它將在不同的池中尋找足夠大的區域,並為你提供最合適的空間。因此,你可能會得到一個指向垃圾的回收指針。

然後是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()

注意

關於持久分配的說明。持久分配在請求之間保持有效。你通常會使用常見的 libc malloc/ 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 記憶體管理器的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:learnku.com。如有侵權,請聯絡admin@php.cn刪除