收藏 分享(赏)

[share]深入探讨php中的内存管理问题.docx

上传人:kuailexingkong 文档编号:1226086 上传时间:2018-06-19 格式:DOCX 页数:13 大小:218.72KB
下载 相关 举报
[share]深入探讨php中的内存管理问题.docx_第1页
第1页 / 共13页
[share]深入探讨php中的内存管理问题.docx_第2页
第2页 / 共13页
[share]深入探讨php中的内存管理问题.docx_第3页
第3页 / 共13页
[share]深入探讨php中的内存管理问题.docx_第4页
第4页 / 共13页
[share]深入探讨php中的内存管理问题.docx_第5页
第5页 / 共13页
点击查看更多>>
资源描述

1、一、 内存在 PHP 中,填充一个字符串变量相当简单,这只需要一 个语句“?php $str = hello world ; ?“即可,并且该字符串能够被自由地修改、拷贝和移动。而在 C 语言中,尽管你能够编写例如“char *str = “hello world “;“这样的一个简单的静态字符串;但是,却不能修改该字符串,因为它生存于程序空间内。为了创建一个可操纵的字符串,你必须分配一个内存块,并且通过一个函数(例如 strdup())来复制其内容。char *str;str = strdup(“hello world“);if (!str) fprintf(stderr, “Unable

2、to allocate memory!“);由于后面我们将分析的各种原因,传统型内存管理函数(例如 malloc(),free(),strdup(),reall oc(),calloc() ,等等)几乎都不能直接为 PHP 源代码所使用。二、 释放内存在几乎所有的平台上,内存管理都是通过一种请求和释放模式实现的。首先,一个应用程序请求它下面的层(通常指“操作系统“) :“我想使用一些内存空间 “。如果存在可用的空间,操作系统就会把它提供给该程序并且打上一个标记以便不会再把这部分内存分配给其它程序。当应用程序使用完这部分内存,它应该被返回到 OS;这样以来,它就能够被 继续分配给其它程序。如果该

3、程序不返回这部分内存,那么 OS 无法知道是否 这块内存不再使用并进而再分配给另一个进程。如果一个内存块没有释放,并且所有者应用程序丢失了它,那么,我们就说此应用程序“存在漏洞“,因为这部分内存无法再为其它程序可用。在一个典型的客户端应用程序中,较小的不太经常的内存泄漏有时能够为 OS 所“容忍“,因为在这个进程稍后结束时该泄漏内存会被隐式返回到 OS。这并没有什么,因为 OS 知道它把该内存分配给了哪个程序,并且它能够确信当该程序终止时不再需要该内存。而对于长时间运行的服务器守护程序,包括象 Apache 这样的 web服务器和扩展 php 模块来说,进程往往被设计为相当长时间一直运行。因为

4、 OS 不能清理内存使用,所以,任何程序的泄漏-无论是多么小- 都将导致重复操作并最终耗尽所有的系统资源。现在,我们不妨考虑用户空间内的 stristr()函数;为了使用大小写不敏感的搜索来查找一个字符串,它实际上创建了两个串的各自的一个小型副本,然后执行一个更传统型的大小写敏感的搜索来查找相对的偏移量。然而,在定位该字符串的偏移量之后,它不再使用这些小写版本的字符串。如果它不释放这些副本,那么,每一个使用stristr()的脚本在每次调用它时都将泄漏一些内存。最后,web 服务器进程将拥有所有的系统内存,但却不能够使用它。你可以理直气壮地说,理想的解决方案就是编写良好、干净的、一致的代码。这

5、当然不错;但是,在一个象 PHP 解释器这样的环境中,这种观点仅 对了一半。三、 错误处理为了实现“跳出“对用户空间脚本及其依赖的扩展函数的一个活动请求,需要使用一种方法来完全“跳出“一个活动请求。这是在 Zend 引擎内实现的:在一个请求的开始设置一个“跳出“地址,然后在任何die() 或 exit()调用或在遇到任何关键错误(E_ERROR) 时执行一个longjmp()以跳转到该“跳出 “地址。尽管这个“跳出“进程能够简化程序执行的流程,但是,在绝大多数情况下,这会意味着将会跳过资源清除代码部分(例如 free()调用)并最终导致出现内存漏洞。现在,让我们来考虑下面这个简化版本的处理函数

6、调用的引擎代码:void call_function(const char *fname, int fname_len TSRMLS_DC)zend_function *fe;char *lcase_fname;/* PHP 函数名是大小写不敏感的,*为了简化在函数表中对它们的定位,*所有函数名都隐含地翻译为小写的*/lcase_fname = estrndup(fname, fname_len);zend_str_tolower(lcase_fname, fname_len);if (zend_hash_find(EG(function_table),lcase_fname, fname_l

7、en + 1, (void *) else php_error_docref(NULL TSRMLS_CC, E_ERROR,“Call to undefined function: %s()“, fname);efree(lcase_fname);当执行到 php_error_docref()这一行时,内部错误处理器就会明白该错误级别是critical,并相应地调 用 longjmp()来中断当前程序流程并离开 call_function()函数,甚至根本不会执行到 efree(lcase_fname)这一行。你可能想把efree()代码行移动到zend_error()代码行的上面;但是,调

8、用这个 call_function()例程的代码行会怎么样呢?fname本身很可能就是一个分配的字符串,并且,在它被错误消息处理使用完之前,你根本不能释放它。注意,这个 php_error_docref()函数是 trigger_error()函数的一个内部等价实现。它的第一个参数是一个将被添加到docref 的可选的文档引用。第三个参数可以是任何我们熟悉的E_* 家族常量,用于指示错误的严重程度。第四个参数(最后一个)遵循 printf()风格的格式化和变量参数列表式样。四、 Zend 内存管理器在上面的“跳出“请求期间解决内存泄漏的方案之一是:使用 Zend 内存管理(ZendM M)层。

9、引擎的这一部分非常类似于操作系统的内存管理行为-分配内存给调用程序。区别在于,它处于进程空间中非常低的位置而且是“请求感知“的;这样以来,当一个请求结束时,它能够执行与 OS 在一个进程终止时相同的行为。也就是说,它会隐式地 释放所有的为该请求所占用的内存。图 1 展示了ZendMM 与 OS 以及 PHP 进程之间的关系。图 1.Zend 内存管理器代 替系统调用来实现针对每一种请求的内存分配。除了提供隐式内存清除功能之外,ZendM M 还能够根据 php.ini 中 memory_limit 的设置控制每一种内存请求的用法。如果一个脚本试图请求比系统中可用内存更多的内存,或大于它每次应该

10、请求的最大量,那么,ZendM M 将自动地发出一个 E_ERROR 消息并且启动相应的“跳出“进程。这种方法的一个额外优点在于,大多数内存分配调用的返回值并不需要检查,因为如果失败的话将会导致立即跳转到引擎的退出部分。把 PHP 内部代码和 OS 的实际的 内存管理层“钩“在一起的原理并不复杂:所有内部分配的内存都要使用一组特定的可选函数实现。例如,PHP 代码 不是使用 malloc(16)来分配一个16 字节内 存块而是使用了 emalloc(16)。除了实现实 际的内存分配任务外,ZendM M还会使用相应的绑定请求类型来标志该内存块;这样以来,当一个请求“跳出“时,ZendMM 可以

11、隐式地释放它。经常情况下,内存一般都需要被分配比单个请求持续时间更长的一段时间。这种类型的分配(因其在一次请求结束之后仍然存在而被称为“永久性分配“),可以使用传统型内存分配器来实现,因为这些分配并不会添加 ZendMM 使用的那些额外的相应于每种请求的信息。然而有时,直到运行时刻才会确定是否一个特定的分配需要永久性分配,因此ZendMM 导出了一组帮助宏,其行为类似 于其它的内存分配函数,但是使用最后一个额外参数来指示是否为永久性分配。如果你确实想实现一个永久性分配,那么这个参数应该被设置为 1;在这种情况下,请求是通过传统型 malloc()分配器家族 进行传递的。然而,如果运行时刻逻辑认

12、为这个块不需要永久性分配;那么,这个参数可以被设置为零,并且调用将会被调整到针对每种请求的内存分配器函数。例如,pemalloc(buffer_len,1)将映射到 malloc(buffer_len),而pemalloc(buffer_len,0)将被使用下列语句映射到 emalloc(buffer_len):#define in Zend/zend_alloc.h:#define pemalloc(size, persistent) (persistent)?malloc(size): emalloc(size)所有这些在ZendMM 中提供的 分配器函数都能够从下表中找到其更传统的对应实

13、现。表格 1 展示了 ZendMM 支持下的每一个分配器函数以及它们的 e/pe 对应实现:表格 1.传统型相对 于 PHP 特 定的分配器。分配器函数 e/pe 对应实现void *malloc(size_t count); void *emalloc(size_t count);void *pemalloc(size_t count,char persistent);void *calloc(size_t count); void *ecalloc(size_t count);void *pecalloc(size_t count,char persistent);void *reallo

14、c(void *ptr, size_t count);void *erealloc(void *ptr, size_t count);void *perealloc(void *ptr,size_t count,char persistent);void *strdup(void *ptr); void *estrdup(void *ptr);void *pestrdup(void *ptr,char persistent);void free(void *ptr); void efree(void *ptr);void pefree(void *ptr,char persistent);你可

15、能会注意到,即使是 pefree()函数也要求使用永久性标志。这是因为在调用 pefree()时,它实际上并不知道是否ptr 是一种永久性分配。针对一个非永久性分配调用 free()能够导致双倍的空间释放,而针对一种永久性分配调用 efree()有可能会导致一个段错误,因为内存管理器会试图查找并不存在的管理信息。因此,你的代码需要记住它分配的数据结构是否是永久性的。 除了分配器函数核心部分外,还存在其它一些非常方便的 ZendMM 特定的函数,例如:void *estrndup(void *ptr,int len);该函数能够分配 len+1 个字节的 内存并且从ptr 处复制 len 个字节

16、到最新分配的块。这个estrndup()函数的行为可以大致描 述如下:void *estrndup(void *ptr, int len)char *dst = emalloc(len + 1);memcpy(dst, ptr, len);dstlen = 0;return dst;在此,被隐式放置在缓冲区最后的 NULL 字节可以确保任何使 用 estrndup()实现字符串复制操作的函数都不需要担心会把结果缓冲区传递给一个例如 printf()这样的希望以为NULL 为结束符 的函数。当使用 estrndup()来复制非字符串数据时,最后一个字节实质上都浪费了,但其中的利明显大于弊。voi

17、d *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 呢?“原因很简单:为了安全。尽管有时候可能性相当小,但是,正是这一“可能性相当小“的结果导致宿主平台的内存溢出。这可能会导致分配负数个数的字节空间,或更有甚者,会导致分配

18、一个小于调用程序要求大小的字节空间。而 safe_emalloc()能够避免这种类型的陷井-通过检查整数溢出并且在发生这样的溢出时显式地预以结束。注意,并不是所有的内存分配例程都有一个相应的 p*对等实现。例如,不存在pestrndup(),并且在 PHP 5.1 版本前也不存在 safe_pemalloc()。五、 引用计数慎重的内存分配与释放对于 PHP(它是一种多请求进程)的长期性能 有极其重大的影响;但是,这还仅是问题的一半。为了使一个每秒处理上千次点击的服务器高效地运行,每一次请求都需要使用尽可能少的内存并且要尽可能减少不必要的数据复制操作。请考虑下列PHP 代码片断:?php$a

19、= Hello World;$b = $a;unset($a);?在第一次调用之后,只有一个变量被创建,并且一个 12 字节的内存块指派给它以便存储字符串“Hello World“,还包括一个结尾处的 NULL 字符。现在,让我们来观察后面的两行:$b 被置为与 变量$a 相同的值,然后变量$a 被释放。如果 PHP因每次变量赋值都要复制变量内容的话,那么,对于上例中 要复制的字符串还需要复制额外的12 个字节,并且在数据复制期间还要进行另外的处理器加载。这一行为乍看起来有点荒谬,因为当第三行代码出现时,原始变量被释放,从而使得整个数据复制显得完全不必要。其实,我们不妨再远一层考虑,让我们设想

20、当一个 10MB 大小的文件的内容被装载到两个变量中时会发生什么。这将会占用20MB 的空间,此时,10 已经足够了。引擎会把那么多的时间和内存浪费在这样一种无用的努力上吗?你应该知道,PHP 的设计者早已深谙此理。记住,在引擎中,变量名和它们的值实际上是两个不同的概念。值本身是一个无名的zval*存储体(在本例中,是一个字符串值),它被通过 zend_hash_add()赋给变量$a。如果两个变量名都指向同一个值,会发生什么呢?zval *helloval;MAKE_STD_ZVAL(helloval);ZVAL_STRING(helloval, “Hello World“, 1);zend

21、_hash_add(EG(active_symbol_table), “a“, sizeof(“a“),zend_hash_add(EG(active_symbol_table), “b“, sizeof(“b“),此时,你可以实际地观察$a 或$b,并且会看到它们都包含字符串“Hello World“。遗憾的是,接下来,你继续执行第三行代码“unset ($a);“。此时,unset()并不知道$a 变量指向的数据还被另一个变量所使用,因此它只是盲目地释放掉该内存。任何随后的对变量$b 的存取都将被分析为已经释放的内存空间并因此导致引擎崩溃。这个问题可以借助于 zval(它有好几种形式)的第

22、四个成员 refcount 加以解决。当一个变量被首次创建并赋值时,它的 refcount被初始化为1 ,因为它被假定仅由最初创建它时相应的变量所使用。当你的代码片断开始把helloval 赋给$b 时,它需要把 refcount 的值增加为 2;这样以来,现在该值被两个变量所引用: zval *helloval;MAKE_STD_ZVAL(helloval);ZVAL_STRING(helloval, “Hello World“, 1);zend_hash_add(EG(active_symbol_table), “a“, sizeof(“a“),ZVAL_ADDREF(helloval);

23、zend_hash_add(EG(active_symbol_table), “b“, sizeof(“b“),现在,当 unset()删除原变量的$a 相应的副本时,它就能够从refcount 参数中看到,还有另外其他人对该数据感兴趣;因此,它应该只是减少 refcount的计数值,然后不再管它。六、 写复制(Copy on Write)通过 refcounting 来节约内存的确是不错的主意,但是,当你仅想改变其中一个变量的值时情况会如何呢?为此,请考虑下面的代码片断:?php$a = 1;$b = $a;$b += 5;?通过上面的逻辑流程,你当然知道$a 的值仍然等于 1,而$b 的值

24、最后将是 6。并且此时,你还知道,Zend 在尽力节省内 存-通过使$a 和$b 都引用相同的 zval(见第二行代码)。那么,当执行到第三行并且必须改变$b 变量的值时,会发生什么 情况呢?回答是,Zend 要查看 refcount的值,并且确保在它的值大于1 时对之进行分离。在Zend引擎中,分离是破坏一个引用对 的过程,正好与你刚才看到的过程相反:zval *get_var_and_separate(char *varname, int varname_len TSRMLS_DC)zval *varval, *varcopy;if (zend_hash_find(EG(active_sy

25、mbol_table),varname , varname_len + 1, (void*)if (*varval)-refcount 2) /* varname 是唯一的实际引用,*不需要进行分离*/return *varval;/* 否则,再复制一份zval*的值*/MAKE_STD_ZVAL(varcopy);varcopy = *varval;/* 复制任何在zval*内的已分配的结构 */zval_copy_ctor(varcopy);/*删除旧版本的 varname*这将减少该过程中 varval 的refcount 的值*/zend_hash_del(EG(active_symb

26、ol_table), varname, varname_len + 1);/*初始化新创建的值的引用计数,并把它依附 到* varname 变量*/varcopy-refcount = 1;varcopy-is_ref = 0;zend_hash_add(EG(active_symbol_table), varname, varname_len + 1,/*返回新的 zval* */return varcopy;现在,既然引擎有一个仅为变量$b 所拥有的zval*(引擎能知道这一点),所以它能够把这个值转换成一个 long 型值并根据脚本的请求给它增加 5。七、 写改变(change-on-w

27、rite)引用计数概念的引入还导致了一个新的数据操作可能性,其形式从用户空间脚本管理器看来与“引用“有一定关系。请考虑下列的用户空间代码片断:?php$a = 1;$b = $b += 5;?在上面的 PHP 代码中,你能看出$a 的值现在为 6,尽管它一开 始为 1 并且从未(直接) 发生变化。之所以会发生这种情况是因为当引擎开始把$b 的值增加5 时,它注意到$b 是一个对$a 的引用并且认为“我可以改变该值而不必分离它,因为我想使所有的引用变量都能看到这一改变“。但是,引擎是如何知道的呢?很简单,它只要查看一下 zval 结构的第四个和最后一个元素(is_ref )即可。这是一个简单的开

28、/关位,它定义了该值是否实际上是一个用户空间风格引用集的一部分。在前面的代码片断中,当执行第一行时,为$a 创建的值得到一个refcount 为 1,还有一个 is_ref值为 0,因为它仅为一个变量($a)所拥有并且没有其它变量对它产生写引用改变。在第二行,这个值的 refcount 元素被增加为 2,除了这次is_ref元素被置为1 之外(因为脚本中包含了一个“&“符号以指示是完全引用)。最后,在第三行,引擎再一次取出与变量$b 相关的值并且检查是 否有必要进行分离。这一次该值没有被分离,因为前面没有包括一个检查。下面是 get_var_and_separate()函数中与 refcount 检查有关的部分代 码:if (*varval)-is_ref | (*varval)-refcount 2) /* varname 是唯一的实际引用,* 或者它是对其它变量的一个完全引用*任何一种方式:都没有进行分离*/

展开阅读全文
相关资源
猜你喜欢
相关搜索
资源标签

当前位置:首页 > 学术论文 > 论文指导/设计

本站链接:文库   一言   我酷   合作


客服QQ:2549714901微博号:道客多多官方知乎号:道客多多

经营许可证编号: 粤ICP备2021046453号世界地图

道客多多©版权所有2020-2025营业执照举报