PHP内核探索:Zend内存管理器

非常类似于操作系统的内存管理功能
服务器君一共花费了284.061 ms进行了6次数据库查询,努力地为您提供了这个页面。
试试阅读模式?希望听取您的建议

在PHP里,我们可以定义字符串变量,比如 <?php $str="nowamagic"; ?>,$str这个字符串变量可以被自由的修改与复制等。这一切在C语言里看起来都是不可能的事情,我们用#char *p = "hello";#来定义一个字符串,但它是常量,是不能被修改的,如果你用p[1]='c';来修改这个字符串会引发段错误(Gcc,c99),为了修改C语言里的字符串常量,我们往往需要定义字符串数组。为了得到一个能够让我们自由修改的字符串,我们往往需要用strdup函数来复制一个字符串出来。

{
	char *p = "hello world";
	// p[0] = 'a'; 如果这么做,就等着运行时段错误吧。
	char *str;
	str = strdup(p);
	str[0] = 'a'; //这时就能自由修改了。
}

在PHP内核中,大多数情况下都不应改直接使用C语言中自带着malloc、free、strdup、realloc、calloc等操作内存的函数,而应使用内核提供的操作内存的函数,这样可以由内核整体统一的来管理内存。

Free the Mallocs

每个平台操作内存的方式都是差不多的有两个方面,一负责申请,二负责释放。如果应用程序向系统申请内存,系统便会在内存中寻找还没有被使用的地方,如果有合适的,便分配给这个程序,并标记下来,不再给其它的程序了。如果一个内存块没有释放,而所有者应用程序也永远不再使用它了。那么,我们就称其为"内存泄漏",那么这部分内存就无法再为其它程序所用了。

在一个典型的客户端应用程序中,偶尔的小量的内存泄漏是可以被操作系统容忍的,因为在进程结束后该泄漏内存会被返回给OS。这并没有什么高科技含量,因为OS知道它把该内存分配给了哪个程序,并且它能够在一个程序结束后把这些内存给回收回来。

但是,世界总是不缺乏特例!对于一些需要长时间运行的程序,比如像Apache这样的web服务器以及它的php模块来说,都是伴随着操作系统长时间运行的,所以OS在很长一段时间内不能主动的回收内存,从而导致这个程序的每一个内存泄漏都会促进量变到质变的进化,最终引起严重的内存泄漏错误,使系统的资源消耗殆尽。现在,我们来在C语言中故意错误的模拟一下PHP的stristr()函数为例,为了使用大小写不敏感的方式来搜索一个字符串,我们需要创建两个辅助的字符串,它们分别是被查找字符串和待查找字符串的小写化副本,然后由这两个副本来帮助我们来完成这次搜索。如果我们在执行这个函数后不释放这些副本占用的资源,那么每一次stristr函数都将是对内存的一次永远的侵占,最终导致这个函数占用了所有的系统内存,而没有实际意义!

大多数人提出来的理想的解决方案是:书写优秀,整洁并且风格一致的代码,这当然是毫无疑问的。但是在PHP扩展开发这样的底层环境中,这并不能解决全部的问题。比如,你需要自己保证在层层嵌套调用中对某块内存的使用都是正确的,且会及时释放的。

错误处理

为了实现从用户端(PHP语言中)"跳出",需要使用一种方法来完全"跳出"一个活动请求。这个功能是在内核中实现的:在一个请求的开始设置一个"跳出"地址,然后在任何die()或exit()调用或在遇到任何关键错误(E_ERROR)时执行一个longjmp()以跳转到该"跳出"地址。

void call_function(const char *fname, int fname_len TSRMLS_DC)
{
    zend_function *fe;
    char *lcase_fname;
    /* php函数的名字是大小写不敏感的
     * 我们可以在function tables里找到他们
     * 保存的所有函数名都是小写的。
     */
    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);
}

当php_error_docref这个函数被调用的时候,便会触发内核中的错误处理机制,根据错误级别来决定是否调用longjmp来终止当前请求并退出call_function函数,从而efree函数便永远不会被执行了。

其实php_error_docref()函数就相当与php语言里的trigger_error()函数.它的第一个参数是一个将被添加到docref的可选的文档引用第三个参数可以是任何我们熟悉的E_*家族常量,用于指示错误的严重程度。后面的两个参数就像printf()风格的格式化和变量参数列表式样。

Zend内存管理器

在上面的"跳出"请求期间解决内存泄漏的方案之一是:使用Zend内存管理(Zend Memory Manager,简称ZendMM、ZMM)层。内核的这一部分非常类似于操作系统的内存管理功能——分配内存给调用程序。区别在于,它处于进程空间中非常低的位置而且是"请求感知"的;这样以来,当一个请求结束时,它能够执行与OS在一个进程终止时相同的行为。也就是说,它会隐式地释放所有的为该请求所占用的内存。下图展示了ZendMM与OS以及PHP进程之间的关系。

除了提供隐式的内存清除功能之外,ZendMM还能够根据php.ini中memory_limit设置来控制每一次内存请求行为,如果一个脚本试图请求比系统中可用内存更多的内存,或大于它每次应该请求的最大量,那么,ZendMM将自动地发出一个E_ERROR消息并且启动相应的终止进程。这种方法的一个额外优点在于,大多数内存分配调用的返回值并不需要检查,因为如果失败的话将会导致立即跳转到引擎的退出部分。

把PHP内核代码和OS的实际的内存管理层"钩"在一起的原理并不复杂:所有内部分配的内存都要使用一组特定的可选函数实现。例如,PHP内核代码不是使用malloc(16)来分配一个16字节内存块而是使用了emalloc(16)。除了实现实际的内存分配任务外,ZendMM还会使用相应的绑定请求类型来标志该内存块;这样以来,当一个请求"跳出"时,ZendMM可以隐式地释放它。

有些事后,某次申请的内存需要在一个请求结束后仍然存活一段时间,也就是持续性存在于各个请求之间。这种类型的分配(因其在一次请求结束之后仍然存在而被称为"永久性分配"),可以使用传统型内存分配器来实现,因为这些分配并不会添加ZendMM使用的那些额外的相应于每种请求的信息。然而有时,我们必须在程序运行时根据某个数据的具体值或者状态才能确定是否需要进行永久性分配,因此ZendMM定义了一组帮助宏,其行为类似于其它的内存分配函数,但是使用最后一个额外参数来指示是否为永久性分配。

如果你确实想实现一个永久性分配,那么这个参数应该被设置为1;在这种情况下,请求是通过传统型malloc()分配器家族进行传递的。然而,如果运行时刻逻辑认为这个块不需要永久性分配;那么,这个参数可以被设置为零,并且调用将会被调整到针对每种请求的内存分配器函数。

例如,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中提供的内存管理函数都能够从下表中找到其在C语言中的函数。

C语言原生函数PHP内核封装后的函数
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是一种永久性分配。需要注意的是,如果针对一个ZendMM申请的非永久性内存直接调用free()能够导致双倍的空间释放,而针对一种永久性分配调用efree()有可能会导致一个段错误,因为ZendMM需要去查找并不存在的管理信息。因此,你的代码需要记住它申请的内存是否是永久性的,从而选择不同的内存函数,free()或者efree()。

除了上述内存管理函数外,还存在其它一些非常方便的ZendMM函数,例如:

void *estrndup(void *ptr,int len);

该函数能够分配len+1个字节的内存并且从ptr处复制len个字节到最新分配的块。这个estrndup()函数的行为可以大致描述如下:

ZEND_API char *_estrndup(const char *s, uint length ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
{
	char *p;

	p = (char *) _emalloc(length+1 ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
	if (UNEXPECTED(p == NULL))
	{
		return p;
	}
	memcpy(p, s, length);
	p[length] = 0;
	return p;
}

在此,被隐式放置在缓冲区最后的0可以确保任何使用estrndup()实现字符串复制操作的函数都不需要担心会把结果缓冲区传递给一个例如printf()这样的希望以为NULL为结束符的函数。当使用estrndup()来复制非字符串数据时,最后一个字节实质上浪费了,但其中的利明显大于弊。

void *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呢?"原因很简单:为了安全,以防万一。尽管有时候可能性相当小,但是,正是这一"可能性相当小"的结果导致宿主平台的内存溢出。这可能会导致分配负数个数的字节空间,或更有甚者,会导致分配一个小于调用程序要求大小的字节空间。而safe_emalloc()能够避免这种类型的陷井-通过检查整数溢出并且在发生这样的溢出时显式地预以结束。

注意,并不是所有的内存分配例程都有一个相应的p*对等实现。例如,不存在pestrndup(),并且在PHP 5.1版本前也不存在safe_pemalloc()。

延伸阅读

此文章所在专题列表如下:

  1. PHP内核探索:从SAPI接口开始
  2. PHP内核探索:一次请求的开始与结束
  3. PHP内核探索:一次请求生命周期
  4. PHP内核探索:单进程SAPI生命周期
  5. PHP内核探索:多进程/线程的SAPI生命周期
  6. PHP内核探索:Zend引擎
  7. PHP内核探索:再次探讨SAPI
  8. PHP内核探索:Apache模块介绍
  9. PHP内核探索:通过mod_php5支持PHP
  10. PHP内核探索:Apache运行与钩子函数
  11. PHP内核探索:嵌入式PHP
  12. PHP内核探索:PHP的FastCGI
  13. PHP内核探索:如何执行PHP脚本
  14. PHP内核探索:PHP脚本的执行细节
  15. PHP内核探索:操作码OpCode
  16. PHP内核探索:PHP里的opcode
  17. PHP内核探索:解释器的执行过程
  18. PHP内核探索:变量概述
  19. PHP内核探索:变量存储与类型
  20. PHP内核探索:PHP中的哈希表
  21. PHP内核探索:理解Zend里的哈希表
  22. PHP内核探索:PHP哈希算法设计
  23. PHP内核探索:翻译一篇HashTables文章
  24. PHP内核探索:哈希碰撞攻击是什么?
  25. PHP内核探索:常量的实现
  26. PHP内核探索:变量的存储
  27. PHP内核探索:变量的类型
  28. PHP内核探索:变量的值操作
  29. PHP内核探索:变量的创建
  30. PHP内核探索:预定义变量
  31. PHP内核探索:变量的检索
  32. PHP内核探索:变量的类型转换
  33. PHP内核探索:弱类型变量的实现
  34. PHP内核探索:静态变量的实现
  35. PHP内核探索:变量类型提示
  36. PHP内核探索:变量的生命周期
  37. PHP内核探索:变量赋值与销毁
  38. PHP内核探索:变量作用域
  39. PHP内核探索:诡异的变量名
  40. PHP内核探索:变量的value和type存储
  41. PHP内核探索:全局变量Global
  42. PHP内核探索:变量类型的转换
  43. PHP内核探索:内存管理开篇
  44. PHP内核探索:Zend内存管理器
  45. PHP内核探索:PHP的内存管理
  46. PHP内核探索:内存的申请与销毁
  47. PHP内核探索:引用计数与写时复制
  48. PHP内核探索:PHP5.3的垃圾回收机制
  49. PHP内核探索:内存管理中的cache
  50. PHP内核探索:写时复制COW机制
  51. PHP内核探索:数组与链表
  52. PHP内核探索:使用哈希表API
  53. PHP内核探索:数组操作
  54. PHP内核探索:数组源码分析
  55. PHP内核探索:函数的分类
  56. PHP内核探索:函数的内部结构
  57. PHP内核探索:函数结构转换
  58. PHP内核探索:定义函数的过程
  59. PHP内核探索:函数的参数
  60. PHP内核探索:zend_parse_parameters函数
  61. PHP内核探索:函数返回值
  62. PHP内核探索:形参return value
  63. PHP内核探索:函数调用与执行
  64. PHP内核探索:引用与函数执行
  65. PHP内核探索:匿名函数及闭包
  66. PHP内核探索:面向对象开篇
  67. PHP内核探索:类的结构和实现
  68. PHP内核探索:类的成员变量
  69. PHP内核探索:类的成员方法
  70. PHP内核探索:类的原型zend_class_entry
  71. PHP内核探索:类的定义
  72. PHP内核探索:访问控制
  73. PHP内核探索:继承,多态与抽象类
  74. PHP内核探索:魔术函数与延迟绑定
  75. PHP内核探索:保留类与特殊类
  76. PHP内核探索:对象
  77. PHP内核探索:创建对象实例
  78. PHP内核探索:对象属性读写
  79. PHP内核探索:命名空间
  80. PHP内核探索:定义接口
  81. PHP内核探索:继承与实现接口
  82. PHP内核探索:资源resource类型
  83. PHP内核探索:Zend虚拟机
  84. PHP内核探索:虚拟机的词法解析
  85. PHP内核探索:虚拟机的语法分析
  86. PHP内核探索:中间代码opcode的执行
  87. PHP内核探索:代码的加密与解密
  88. PHP内核探索:zend_execute的具体执行过程
  89. PHP内核探索:变量的引用与计数规则
  90. PHP内核探索:新垃圾回收机制说明

本文地址:http://www.nowamagic.net/librarys/veda/detail/1439,欢迎访问原出处。

不打个分吗?

转载随意,但请带上本文地址:

http://www.nowamagic.net/librarys/veda/detail/1439

如果你认为这篇文章值得更多人阅读,欢迎使用下面的分享功能。
小提示:您可以按快捷键 Ctrl + D,或点此 加入收藏

大家都在看

阅读一百本计算机著作吧,少年

很多人觉得自己技术进步很慢,学习效率低,我觉得一个重要原因是看的书少了。多少是多呢?起码得看3、4、5、6米吧。给个具体的数量,那就100本书吧。很多人知识结构不好而且不系统,因为在特定领域有一个足够量的知识量+足够良好的知识结构,系统化以后就足以应对大量未曾遇到过的问题。

奉劝自学者:构建特定领域的知识结构体系的路径中再也没有比学习该专业的专业课程更好的了。如果我的知识结构体系足以囊括面试官的大部分甚至吞并他的知识结构体系的话,读到他言语中的一个词我们就已经知道他要表达什么,我们可以让他坐“上位”毕竟他是面试官,但是在知识结构体系以及心理上我们就居高临下。

所以,阅读一百本计算机著作吧,少年!

《程序员修炼之道:从小工到专家》 亨特(Andrew Hunt) (作者), 托马斯(David Thomas) (作者), 马维达 (译者)

《程序员修炼之道:从小工到专家》内容简介:《程序员修炼之道》由一系列独立的部分组成,涵盖的主题从个人责任、职业发展,知道用于使代码保持灵活、并且易于改编和复用的各种架构技术,利用许多富有娱乐性的奇闻轶事、有思想性的例子及有趣的类比,全面阐释了软件开发的许多不同方面的最佳实践和重大陷阱。无论你是初学者,是有经验的程序员,还是软件项目经理,《程序员修炼之道:从小工到专家》都适合你阅读。

更多计算机宝库...