属虎的属相婚配表
在PHP中,填充一个字符串变量相当简单,这只需要一个语句<?php$str=helloworld;?>即可,并且该字符串能够被地修改、拷贝和移动。而在C语言中,尽管你能够编写例如char*str=helloworld;这样的一个简单的静态字符串;但是,却不能修改该字符串,因为它于程序空间内。为了创建一个可的字符串,你必须分配一个内存块,并且通过一个函数(例如strdup())来复制其内容。
由于后面我们将分析的各种原因,传统型内存管理函数(例如malloc(),free(),strdup(),realloc(),calloc(),等等)几乎都不能直接为PHP源代码所使用。
在几乎所有的平台上,内存管理都是通过一种请求和模式实现的。首先,一个应用程序请求它下面的层(通常指操作系统):我想使用一些内存空间。如果存在可用的空间,操作系统就会把它提供给该程序并且打上一个标记以便不会再把这部分内存分配给其它程序。
当应用程序使用完这部分内存,它应该被返回到OS;这样以来,它就能够被继续分配给其它程序。如果该程序不返回这部分内存,那么OS无法知道是否这块内存不再使用并进而再分配给另一个进程。如果一个内存块没有,并且所有者应用程序丢失了它,那么,我们就说此应用程序存在漏洞,因为这部分内存无法再为其它程序可用。
在一个典型的客户端应用程序中,较小的不太经常的内存泄漏有时能够为OS所,因为在这个进程稍后结束时该泄漏内存会被隐式返回到OS。这并没有什么,因为OS知道它把该内存分配给了哪个程序,并且它能够确信当该程序终止时不再需要该内存。
而对于长时间运行的服务器守护程序,包括象Apache这样的web服务器和扩展php模块来说,进程往往被设计为相当长时间一直运行。因为OS不能清理内存使用,所以,任何程序的泄漏-无论是多么小-都将导致重复操作并最终耗尽所有的系统资源。
现在,我们不妨考虑用户空间内的stristr()函数;为了使用大小写不的搜索来查找一个字符串,它实际上创建了两个串的各自的一个小型副本,然后执行一个更传统型的大小写的搜索来查找相对的偏移量。然而,在定位该字符串的偏移量之后,它不再使用这些小写版本的字符串。如果它不这些副本,那么,每一个使用stristr()的脚本在每次调用它时都将泄漏一些内存。最后,web服务器进程将拥有所有的系统内存,但却不能够使用它。
你可以理直气壮地说,理想的解决方案就是编写良好、干净的、一致的代码。这当然不错;但是,在一个象PHP解释器这样的中,这种观点仅对了一半。
为了实现跳出对用户空间脚本及其依赖的扩展函数的一个活动请求,需要使用一种方法来完全跳出一个活动请求。这是在Zend引擎内实现的:在一个请求的开始设置一个跳出地址,然后在任何die()或exit()调用或在遇到任何关键错误(E_ERROR)时执行一个longjmp()以跳转到该跳出地址。
尽管这个跳出进程能够简化程序执行的流程,但是,在绝大多数情况下,这会意味着将会跳过资源清除代码部分(例如free()调用)并最终导致出现内存漏洞。现在,让我们来考虑下面这个简化版本的处理函数调用的引擎代码:
当执行到php_error_docref()这一行时,内部错误处理器就会明白该错误级别是critical,并相应地调用longjmp()来中断当前程序流程并离开call_function()函数,甚至根本不会执行到efree(lcase_fname)这一行。你可能想把efree()代码行移动到zend_error()代码行的;但是,调用这个call_function()例程的代码行会怎么样呢?fname本身很可能就是一个分配的字符串,并且,在它被错误消息处理使用完之前,你根本不能它。
注意,这个php_error_docref()函数是trigger_error()函数的一个内部等价实现。它的第一个参数是一个将被添加到docref的可选的文档引用。第三个参数可以是任何我们熟悉的E_*家族常量,用于错误的严重程度。第四个参数(最后一个)遵循printf()风格的格式化和变量参数列表式样。
在的跳出请求期间解决内存泄漏的方案之一是:使用Zend内存管理(ZendMM)层。引擎的这一部分非常类似于操作系统的内存管理行为-分配内存给调用程序。区别在于,它处于进程空间中非常低的而且是请求的;这样以来,当一个请求结束时,它能够执行与OS在一个进程终止时相同的行为。也就是说,它会隐式地所有的为该请求所占用的内存。图1展示了ZendMM与OS以及PHP进程之间的关系。
通过的逻辑流程,你当然知道$a的值仍然等于1,而$b的值最后将是6。并且此时,你还知道,Zend在尽力节省内存-通过使$a和$b都引用相同的zval(见第二行代码)。那么,当执行到第三行并且必须改变$b变量的值时,会发生什么情况呢?
回答是,Zend要查看refcount的值,并且确保在它的值大于1时对之进行分离。在Zend引擎中,分离是一个引用对的过程,正好与你刚才看到的过程相反:
现在,既然引擎有一个仅为变量$b所拥有的zval*(引擎能知道这一点),所以它能够把这个值转换成一个long型值并根据脚本的请求给它增加5。
引用计数概念的引入还导致了一个新的数据操作可能性,其形式从用户空间脚本管理器看来与引用有一定关系。请考虑下列的用户空间代码片断:
在的PHP代码中,你能看出$a的值现在为6,尽管它一开始为1并且从未(直接)发生变化。之所以会发生这种情况是因为当引擎开始把$b的值增加5时,它注意到$b是一个对$a的引用并且认为我可以改变该值而不必分离它,因为我想使所有的引用变量都能看到这一改变。
但是,引擎是如何知道的呢?很简单,它只要查看一下zval结构的第四个和最后一个元素(is_ref)即可。这是一个简单的开/关位,它定义了该值是否实际上是一个用户空间风格引用集的一部分。在前面的代码片断中,当执行第一行时,为$a创建的值得到一个refcount为1,还有一个is_ref值为0,因为它仅为一个变量($a)所拥有并且没有其它变量对它产生写引用改变。在第二行,这个值的refcount元素被增加为2,除了这次is_ref元素被置为1之外(因为脚本中包含了一个&符号以是完全引用)。
最后,在第三行,引擎再一次取出与变量$b相关的值并且检查是否有必要进行分离。这一次该值没有被分离,因为前面没有包括一个检查。下面是get_var_and_separate()函数中与refcount检查有关的部分代码:
这一次,尽管refcount为2,却没有实现分离,因为这个值是一个完全引用。引擎能够地修改它而不必关心其它变量值的变化。
尽管已经存在讨论到的复制和引用技术,但是还存在一些不能通过is_ref和refcount操作来解决的问题。请考虑下面这个PHP代码块:
在此,你有一个需要与三个不同的变量相关联的值。其中,两个变量是使用了change-on-write完全引用方式,而第三个变量处于一种可分离的copy-on-write(写复制)上下文中。如果仅使用is_ref和refcount来描述这种关系,有哪些值能够工作呢?
回答是:没有一个能工作。在这种情况下,这个值必须被复制到两个分离的zval*中,尽管两者都包含完全相同的数据(见图2)。
网友评论 ()条 查看