PHP100 2019-03-27
摘要 内存管理对于长期运行的程序,例如服务器守护程序,是相当重要的影响;因此,理解PHP是如何分配与释放内存的对于创建这类程序极为重要。本文将重点探讨PHP的内存管理问题。
一、 内存
在PHP中,填充一个字符串变量相当简单,这只需要一个语句"<?php $str = 'hello world '; ?>"即可,并且该字符串能够被自由地修改、拷贝和移动。而在C语言中,尽管你能够编写例如"char *str = "hello world ";"这样的一个简单的静态字符串;但是,却不能修改该字符串,因为它生存于程序空间内。为了创建一个可操纵的字符串,你必须分配一个内存块,并且通过一个函数(例如strdup())来复制其内容。
代码如下:
{ char *str; str = strdup("hello world"); if (!str) { fprintf(stderr, "Unable to allocate memory!"); } }
二、 释放内存
在几乎所有的平台上,内存管理都是通过一种请求和释放模式实现的。首先,一个应用程序请求它下面的层(通常指"操作系统"):"我想使用一些内存空间"。如果存在可用的空间,操作系统就会把它提供给该程序并且打上一个标记以便不会再把这部分内存分配给其它程序。
当应用程序使用完这部分内存,它应该被返回到OS;这样以来,它就能够被继续分配给其它程序。如果该程序不返回这部分内存,那么OS无法知道是否这块内存不再使用并进而再分配给另一个进程。如果一个内存块没有释放,并且所有者应用程序丢失了它,那么,我们就说此应用程序"存在漏洞",因为这部分内存无法再为其它程序可用。
在一个典型的客户端应用程序中,较小的不太经常的内存泄漏有时能够为OS所"容忍",因为在这个进程稍后结束时该泄漏内存会被隐式返回到OS。这并没有什么,因为OS知道它把该内存分配给了哪个程序,并且它能够确信当该程序终止时不再需要该内存。
而对于长时间运行的服务器守护程序,包括象Apache这样的web服务器和扩展php模块来说,进程往往被设计为相当长时间一直运行。因为OS不能清理内存使用,所以,任何程序的泄漏-无论是多么小-都将导致重复操作并最终耗尽所有的系统资源。
现在,我们不妨考虑用户空间内的stristr()函数;为了使用大小写不敏感的搜索来查找一个字符串,它实际上创建了两个串的各自的一个小型副本,然后执行一个更传统型的大小写敏感的搜索来查找相对的偏移量。然而,在定位该字符串的偏移量之后,它不再使用这些小写版本的字符串。如果它不释放这些副本,那么,每一个使用stristr()的脚本在每次调用它时都将泄漏一些内存。最后,web服务器进程将拥有所有的系统内存,但却不能够使用它。
你可以理直气壮地说,理想的解决方案就是编写良好、干净的、一致的代码。这当然不错;但是,在一个象PHP解释器这样的环境中,这种观点仅对了一半。
三、 错误处理
为了实现"跳出"对用户空间脚本及其依赖的扩展函数的一个活动请求,需要使用一种方法来完全"跳出"一个活动请求。这是在Zend引擎内实现的:在一个请求的开始设置一个"跳出"地址,然后在任何die()或exit()调用或在遇到任何关键错误(E_ERROR)时执行一个longjmp()以跳转到该"跳出"地址。
尽管这个"跳出"进程能够简化程序执行的流程,但是,在绝大多数情况下,这会意味着将会跳过资源清除代码部分(例如free()调用)并最终导致出现内存漏洞。现在,让我们来考虑下面这个简化版本的处理函数调用的引擎代码:
代码如下:
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_len + 1, (void **)&fe) == FAILURE) { zend_execute(fe->op_array TSRMLS_CC); } else { php_error_docref(NULL TSRMLS_CC, E_ERROR,"Call to undefined function: %s()", fname); } efree(lcase_fname); }
分配器函数 | 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 *realloc(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); |
你可能会注意到,即使是pefree()函数也要求使用永久性标志。这是因为在调用pefree()时,它实际上并不知道是否ptr是一种永久性分配。针对一个非永久性分配调用free()能够导致双倍的空间释放,而针对一种永久性分配调用efree()有可能会导致一个段错误,因为内存管理器会试图查找并不存在的管理信息。因此,你的代码需要记住它分配的数据结构是否是永久性的。
除了分配器函数核心部分外,还存在其它一些非常方便的ZendMM特定的函数,例如:
void *estrndup(void *ptr,int len);
该函数能够分配len+1个字节的内存并且从ptr处复制len个字节到最新分配的块。这个estrndup()函数的行为可以大致描述如下:
代码如下:
void *estrndup(void *ptr, int len) { char *dst = emalloc(len + 1); memcpy(dst, ptr, len); dst[len] = 0; return dst; }
五、 引用计数
慎重的内存分配与释放对于PHP(它是一种多请求进程)的长期性能有极其重大的影响;但是,这还仅是问题的一半。为了使一个每秒处理上千次点击的服务器高效地运行,每一次请求都需要使用尽可能少的内存并且要尽可能减少不必要的数据复制操作。请考虑下列PHP代码片断:
代码如下:
<?php $a = 'Hello World'; $b = $a; unset($a); ?>
代码如下:
{ zval *helloval; MAKE_STD_ZVAL(helloval); ZVAL_STRING(helloval, "Hello World", 1); zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),&helloval, sizeof(zval*), NULL); zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),&helloval, sizeof(zval*), NULL); }
代码如下:
{ zval *helloval; MAKE_STD_ZVAL(helloval); ZVAL_STRING(helloval, "Hello World", 1); zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),&helloval, sizeof(zval*), NULL); ZVAL_ADDREF(helloval); zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),&helloval,sizeof(zval*),NULL); }
六、 写复制(Copy on Write)
通过refcounting来节约内存的确是不错的主意,但是,当你仅想改变其中一个变量的值时情况会如何呢?为此,请考虑下面的代码片断:
代码如下:
<?php $a = 1; $b = $a; $b += 5; ?>
代码如下:
zval *get_var_and_separate(char *varname, int varname_len TSRMLS_DC) { zval **varval, *varcopy; if (zend_hash_find(EG(active_symbol_table),varname, varname_len + 1, (void**)&varval) == FAILURE) { /* 变量根本并不存在-失败而导致退出*/ return NULL; } 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_symbol_table), varname, varname_len + 1); /*初始化新创建的值的引用计数,并把它依附到 * varname变量 */ varcopy->refcount = 1; varcopy->is_ref = 0; zend_hash_add(EG(active_symbol_table), varname, varname_len + 1,&varcopy, sizeof(zval*), NULL); /*返回新的zval* */ return varcopy; }
七、 写改变(change-on-write)
引用计数概念的引入还导致了一个新的数据操作可能性,其形式从用户空间脚本管理器看来与"引用"有一定关系。请考虑下列的用户空间代码片断:
代码如下:
<?php $a = 1; $b = &$a; $b += 5; ?>
代码如下:
if ((*varval)->is_ref || (*varval)->refcount < 2) { /* varname是唯一的实际引用, * 或者它是对其它变量的一个完全引用 *任何一种方式:都没有进行分离 */ return *varval; }
八、 分离问题
尽管已经存在上面讨论到的复制和引用技术,但是还存在一些不能通过is_ref和refcount操作来解决的问题。请考虑下面这个PHP代码块:
代码如下:
<?php $a = 1; $b = $a; $c = &$a; ?>
图2.引用时强制分离
同样,下列代码块将引起相同的冲突并且强迫该值分离出一个副本(见图3)。
图3.复制时强制分离
代码如下:
<?php $a = 1; $b = &$a; $c = $a; ?>
九、 总结
PHP是一种托管语言。从普通用户角度来看,这种仔细地控制资源和内存的方式意味着更为容易地进行原型开发并导致出现更少的冲突。然而,当我们深入"内里"之后,一切的承诺似乎都不复存在,最终还要依赖于真正有责任心的开发者来维持整个运行时刻环境的一致性。