PHP内存管理的深入探讨
1。记忆在PHP中,填充字符串变量非常简单。它只需要一个语句< php $str =你好世界在C语言中,尽管你可以写一个简单的静态字符串,如char * str =你好世界为了创造一个可操作的字符串,你必须分配一个内存块,并复制其内容通过一个函数(例如strdup())。
{
字符;
STR = strdup(Hello World);
如果(!STR){
Fprintf(stderr,无法分配内存!;
}
}
由于分析背后的种种原因,传统的内存管理函数(如malloc(),自由的(),(),strdup realloc(),calloc等不能直接用于PHP源代码。
两。释放内存
在几乎所有平台上,内存管理是通过申请和释放模式。首先,应用程序请求它下面的层(通常指的是操作系统):我想用一些内存空间。如果有可用的空间,操作系统将提供它的程序和它的标签,没有内存将被分配给其他程序的标志。
当应用程序使用完这部分的记忆,它应该返回到操作系统;这样,它可以继续被分配给其他程序。如果程序不返回这部分内存,那么操作系统不知道这记忆是没有用的,然后重新分配给另一个进程。如果一个内存块没有被释放和业主申请失败了,那么我们说的应用有漏洞
在一个典型的客户机应用程序中,操作系统有时可以容忍较小而频繁的内存泄漏,因为泄漏内存将在进程结束时隐式地返回到OS中,这没什么,因为OS知道分配内存的程序,并且可以确保程序终止时不需要内存。
对于长时间运行的服务器程序,如Apache Web服务器和PHP扩展模块,工艺设计运行相当长的一段时间。因为OS不能清理内存使用任何程序漏洞,无论多么小,都会造成重复操作,最终耗尽所有的系统资源。
现在,我们考虑用户空间(stristr)功能;为了使用不区分大小写的搜索字符串,它实际上造成了两个每一个小副本,然后执行一个更传统的类型的情况下,敏感的搜索找到的相对偏移量。然而,定位字符串的偏移后,它没有再利用这些小写的字符串版本。如果不释放这些副本,每个脚本使用stristr()会泄漏一些记忆每次调用它。最后,Web服务器进程将所有的系统内存,但它不能够使用它。
您可以合理地说,理想的解决方案是编写好的、干净的、一致的代码,这当然是好的,但在PHP解释器这样的环境中,这种视图只占了一半。
三,错误处理
为了实现跳到用户空间的脚本和相关的扩展功能的活动要求,我们需要用一个方法完全跳出来活动的要求。这是Zend引擎实现的:建立一个跳出了一个请求的地址,开始然后调用任何模具()或退出(),或执行longjmp()当遇到任何关键错误(e_error),这样我们就可以跳到跳出地址。
虽然跳出过程简化了程序的执行过程,但在大多数情况下,这意味着我们将跳过资源清理部分(例如,免费())并最终导致内存泄漏。现在,让我们考虑一下以下处理函数调用的简化版本的引擎代码:
无效call_function(const char *名,int fname_len tsrmls_dc){
zend_function *铁;
char * lcase_fname;
* PHP函数名不区分大小写,
*为了简化它们在函数表中的位置,
*所有函数名都隐式地翻译成小写。
* /
lcase_fname = estrndup(名、fname_len);
zend_str_tolower(lcase_fname,fname_len);
如果(zend_hash_find(如(function_table),lcase_fname,fname_len + 1(void)Fe)= =失败){
zend_execute(铁> op_array tsrmls_cc);
其他{ }
php_error_docref(空tsrmls_cc,e_error,调用未定义的函数:%s()
}
依芙利特(lcase_fname);
}
当实现的php_error_docref(一),内部错误处理器会理解错误水平是至关重要的,而相应的调用longjmp()中断当前进程和离开call_function()函数,甚至进行饱和(lcase_fname)这行。你可能想移动饱和线()代码的zend_error顶()代码行;但什么叫的call_function代码行()例程名本身可能是一个指定的字符串,你不能释放它,直到它完成了错误消息处理。
请注意,这php_error_docref()函数的trigger_error内部等效()函数,它的第一个参数是一个可选的参考文件将被添加到docref。第三个参数可以是任何e_ *家庭等大家都很熟悉,这是用来指示错误的严重程度。第四参数(最后一个)遵循printf格式()式和可变参数列表。
四、Zend内存管理器
对一个内存泄漏的解决方案在上述跳出来的要求是使用Zend内存管理(zendmm)层。这部分的引擎是对操作系统的内存管理的行为非常类似于调用程序内存分配。不同的是,它是在进程空间非常低的位置,要求意识。此后,当一个请求端,它可以执行同样的行为与操作系统进程终止时,就是说,它将隐式释放所有的记忆,是被占领的要求。图1显示了ZendMM和OS和PHP的过程之间的关系。
图1中的内存管理器,而不是系统的Zend是用来为每个请求的内存分配的实现。
除了提供隐式内存清理,zendmm也可以控制基于对php.ini.if脚本试图要求比系统中的可用内存更大的内存memory_limit设置每个存储器请求使用,或大于最大金额应该请求一次,然后zendmm会自动发出e_error消息并启动相应的跳出来的过程。这种方法的另一个优点是大多数内存分配调用的值不需要检查,因为失败将导致发动机的出口部分立即跳。
挂钩的PHP内部代码与操作系统的内存管理层的工作原理并不复杂:所有内部分配的内存是由一组特定的可选功能实现。例如,PHP代码不使用malloc(16)分配一个16字节的内存块,而不是使用emalloc(16)。除了实现实际内存分配的任务,zendmm也将使用相应的绑定请求类型标记的内存块。这样,当请求跳出
通常,内存通常需要分配到一个单一的请求更长。这种类型的赋值,这是所谓的永久性分配建成后的请求后,可以通过传统的内存分配器来实现的,因为这些分布不添加相应的每一个请求zendmm附加信息。然而,有时候,直到运行时间将决定一个特定的永久性分配,所以ZendMM推导出一套宏的帮助,其行为类似于其他的内存分配函数,最后用一个额外的参数,指示是否永久性分配。
如果你真的想要实现永久赋值,这个参数应该设置为1。在这种情况下,要求通过传统的malloc()分配的家庭。然而,如果运行时逻辑不需要永久性的分布,这个参数可以被设置为零,并称将调整内存分配函数为每个请求。
例如,pemalloc(buffer_len,1)将被映射到malloc(buffer_len),而pemalloc(buffer_len,0)将被映射到emalloc(buffer_len)使用下面的语句。
#定义Zend / zend_alloc。H:
#定义pemalloc(大小、持续性)(持续)malloc(大小):emalloc(大小))
All of these allocator functions provided in the ZendMM can find their more traditional corresponding implementations from the lower table.
表1显示了每个分配器功能的zendmm和E / PE相应的实施支持:
表1。相对于PHP特定分配器的传统类型。
内存分配函数
e/PE相应的实现
void * malloc(size_t计数);
void * emalloc(size_t计数);void * pemalloc(size_t计数,煤焦持续);
void * calloc(size_t计数);
void * ecalloc(size_t计数);void * pecalloc(size_t计数,煤焦持续);
void* realloc(void *ptr,size_t计数);
void * erealloc(void *ptr,size_t计数);
void * perealloc(void *ptr,size_t计数,煤焦持续);
void * strdup(void *ptr);
void * estrdup(void *ptr);void * pestrdup(void *ptr,煤焦持续);
无空隙(void *ptr);
虚空(void *ptr)饱和;
虚空(void *ptr,pefree煤焦持续);
你可能会注意到,即使是pefree()函数需要一个永久性的标志使用。这是因为,当调用pefree(),它实际上并不知道是否PTR是一个永久性的分布,为非永久性分配,呼叫免费()可以引起双空间释放。一个永久的任务,叫EFREE()会导致段错误,因为内存管理器会尝试找出管理信息不存在。因此,您的代码需要知道数据结构将是永久性的。
除了对分配器功能的核心部分,还有其他很方便zendmm特定功能,如:
void * estrndup(void *ptr,len);
这个功能可以分配内存和复制len个字节从PTR最新分配的块的字节长度+ 1。这estrndup行为()函数可以大致描述如下:
void * estrndup(void *ptr,len)
{
char * DST = emalloc(len + 1);
Memcpy(DST,PTR,Len);
DST = } = 0;
回归测试;
}
在这里,最后一个空字节放中隐含的缓冲区,以确保任何使用estrndup()函数实现字符串的拷贝操作时不需要担心它会把结果缓冲区函数如printf(),这让零结束。当estrndup()是用来复制非字符串数据,最后一个字节是大大浪费了,但优势明显大于劣势。
void * safe_emalloc(size_t大小,size_t计数,size_t addtl);
void * safe_pemalloc(size_t大小,size_t计数,size_t addtl,煤焦持续);
内存空间的最终大小分配的这些功能是((size*count)+ addtl)。你可以问,你为什么要提供额外的功能吗为什么不使用emalloc / pemalloc原因很简单:安全。虽然它有时是相当小的,正是这种可能性很小的结果,导致宿主平台的内存溢出。这可能导致消极的数字,数量的字节空间,甚至更多,分配一个字节的空间较小比需要调用program.safe_emalloc尺寸()通过检查整数溢出,溢出避免这种陷阱出现在年底前明确。
注意不是所有的内存分配例程有一个相应的P *对等的实现。例如,没有pestrndup(),并没有safe_pemalloc()在PHP 5.1。
五。引用计数
细心的内存分配和释放对PHP的长期性能影响很大,这是一个多请求的过程,但这只是问题的一半。为了有效地运行的处理每千次点击服务器,每个请求需要使用尽可能小的记忆,尽可能减少不必要的数据复制。请考虑下面的代码片断:
< PHP
美元=你好世界;
美元=美元;
unset(美元);
>
在第一个调用之后,只创建一个变量,并给它分配一个12字节的内存块来存储字符串Hello World,现在让我们看看下面的两行:$ B被设置为与变量a相同的值,并且变量$ A被释放。
如果每个变量赋值PHP将被复制在变量的内容的话,那么,比如说你要复制的字符串也需要12字节的额外副本,并在数据复制其他处理器加载过程。这种行为有点可笑,乍一看,因为当第三行代码出现,原始变量被释放,使整个数据复制是完全不必要的。事实上,我们想远一点,让我们想象一下会发生什么当一个10MB大小的文件的内容加载到两个变量。这会占用20MB的空间,在这个时候,10就够了。将发动机浪费这么多时间在这种无用的努力记忆
你应该知道,PHP的设计者已经深深地意识到了这一点。
请记住,在发动机、变量名和他们的价值观其实是两个不同的概念,价值本身是一个无名的zval *银行(在这种情况下,是一个字符串值),它是通过zend_hash_add(美元)分配给变量。如果两变量名指向相同的值会发生什么
{
* helloval zval;
make_std_zval(helloval);
zval_string(helloval,你好世界
zend_hash_add(如(active_symbol_table),
zend_hash_add(如(active_symbol_table)、B
}
在这一点上,你可以看$或$,你可以看到,他们都包含字符串Hello World。不幸的是,接下来,你继续执行第三行代码的撤消(合一);。在这一点上,unset()不知道,$变量指向的另一个变量的数据,所以它就释放内存的盲目。任何后续访问的变量$ B将分析存储空间已被释放,从而导致发动机崩溃。
这个问题可以在zval第四成员的帮助下解决了(它有几种形式),引用计数。当一个变量是创造和分配,其引用计数初始化为1,因为它是假定仅仅通过相应的变量,当它最初创造的。当你的代码段开始分配helloval为B,需要增加引用计数的值为2;这样的价值现在已经是由两个变量引用:
{
* helloval zval;
make_std_zval(helloval);
zval_string(helloval,你好世界
zend_hash_add(如(active_symbol_table),
zval_addref(helloval);
zend_hash_add(如(active_symbol_table)、B
}
现在,当unset()删除了一元的原始变量对应的拷贝,它可以从引用计数参数,和其他感兴趣的数据。因此,它只能减少引用计数计数的值,然后停止。
六。写和复制(写的拷贝)
它通过引用计数保存记忆真的是个好主意,但是当你想改变一个变量的值为此,请考虑以下代码片段:
< PHP
$ = 1;
美元=美元;
$ = 5;
>
上述的逻辑流程,您一定知道美元的价值仍然等于1,美元和B的值将是6。在这一点上,你知道,Zend是试图通过美元美元和B参考相同的变量保存记忆(见代码的第二行)。所以,当第三线执行的$变量的值必须被改变吗
答案是,Zend想看看引用计数的值,确保它是分离时,其值大于1。在Zend引擎,分离是一个过程,破坏了一个参考对,正好相反的是,你刚才看到的。
* get_var_and_separate(char*变量varname,int varname_len tsrmls_dc)
{
varval zval *,* varcopy;
如果(zend_hash_find(如(active_symbol_table),Varname,varname_len + 1(void *)varval)= =失败){
变量不存在-不退出。
返回null;
}
如果((* varval)->引用计数<2){
* Varname是唯一可行的参考,
*不需要分离
* /
varval回报*;
}
*否则,重复的变量值。
make_std_zval(varcopy);
varcopy = * varval;
/ * copy any within the zval* assigned structure * /
zval_copy_ctor(varcopy);
/ *删除varname旧版本
这将减少的varval的引用计数在这一过程中的价值
* /
zend_hash_del(如(active_symbol_table),Varname,varname_len + 1);
初始化新创建的值引用计数,并附加到
* Varname变
* /
varcopy ->引用计数= 1;
varcopy -> is_ref = 0;
zend_hash_add(如(active_symbol_table),Varname,varname_len + 1,varcopy,sizeof(zval *),null);
返回一个新的机制* / * * /
返回varcopy;
}
现在,因为发动机有一个zval *这是只有一个变量$,发动机可以知道它,所以它可以变换值转换为长值增加到5根据脚本的要求。
七。写更改(写更改)
引用计数概念的引入也带来了一种新的数据操作的可能性,它涉及到用户空间脚本管理器的引用:
< PHP
$ = 1;
美元=美元;
$ = 5;
>
在上面的PHP代码,你可以看到,美元的价值是6,虽然是1,从未改变。这是因为当发动机启动时增加了5美元的B值,它注意到B是参考美元美元和认为,我可以改变价值没有分开,因为我想让所有的参考变量,看到这种变化。
但是引擎是怎么知道的呢这很简单。看看第四个和最后一个元素(is_ref)的zval结构。这是一个简单的开/关点,它定义了是否值实际上是一个用户空间的风格参考集的一部分。在前面的代码片段,执行第一行时,值得创造一美元引用计数,和is_ref值是0,因为它只有一个变量(合一),没有其他变量写和引用它的变化。排在第二,这个值的引用计数单元增加到2个,除了那个is_ref元素设置为1(因为脚本包含一个符号来表明它是完全引用)。
最后,在第三行中,发动机再次用变量$ B和检查有关是否有必要单独的价值。这时间的价值是不会因为一个检查是不包括在前面的分离。以下是代码的一部分,是在get_var_and_separate引用计数检查相关()函数:
如果((* varval)- is_ref(* varval)- | | > >引用计数<2){
* Varname是唯一可行的参考,
或者它是对其他变量的完全引用。
*任何方式:没有分离
* /
varval回报*;
}
这一次,虽然引用计数是2,没有分离,因为价值是一个完整的参考。发动机可以修改它的自由而不必在乎其他变量的值的变化。
八,分离问题
虽然已经有一个讨论的复制和参考技术的讨论中,还存在一些不可is_ref和引用计数的操作问题,考虑下面的PHP代码块:
< PHP
$ = 1;
美元=美元;
美元=美元;
>
在这里,你是有价值的,需要有三个不同的变量有关,其中,两变量是改变使用上写全参考模式,而第三个变量是在一个可分离的写时复制(写复制)背景。如果is_ref和引用计数是用来描述这种关系,什么价值可以工作吗
答案是:没有人能工作。在这种情况下,该值必须复制到两个独立的变量,虽然都含有相同的数据(见图2)。
图2。参考强迫分离
类似地,下面的代码块将引起相同的冲突并强制值分隔一个副本(参见图3)。
图3。复制时强制分离
< PHP
$ = 1;
美元=美元;
美元=美元;
>
注意,这里的两例,$与原始变量对象相关,因为当发生分离,发动机不知道位于操作的第三个变量的名称。
九。总结
PHP是一种托管语言。从普通用户的角度来看,这样的谨慎控制资源和内存意味着更容易成型和更少的冲突。但是,当我们深入到Neri