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)
,它分配单个大小size
的nmemb
,并将区域归零。如果你是一位经验丰富的 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 时最常见的错误,以及你应该怎么做。
不处理请求时使用 ZendMM。
获取有关 PHP 生命周期的信息,以了解在扩展中何时处理请求,何时不处理。如果在请求范围之外使用 ZendMM(例如在MINIT()中),在处理第一个请求之前,ZendMM 会静默清除分配,并且可能会使用after-after-free:根本没有。
缓冲区上溢和下溢。
使用内存调试器。如果你在 ZendMM 返回的内存区域以下或过去写入内容,则将覆盖关键的 ZendMM 结构并触发崩溃。如果 ZendMM 能够为你检测到混乱,则可能会显示“zend_mm_heap损坏”的消息。堆栈追踪将显示从某些代码到某些 ZendMM 代码的崩溃。ZendMM 代码不会自行崩溃。如果你在 ZendMM 代码中间崩溃,那很可能意味着你在某个地方弄乱了指针。插入你喜欢的内存调试器,查找有罪的部分并进行修复。
混合 API 调用
如果分配一个 ZendMM 指针(即emalloc()
)并使用 libc 释放它(free()
),或相反的情况:你将崩溃。要严谨对待。另外,如果你将其不知道的任何指针传递给 ZendMM 的efree()
:将会崩溃。