PHP内核探索:引用计数与写时复制

引用计数是节省内存的一个超棒的模式
服务器君一共花费了295.912 ms进行了6次数据库查询,努力地为您提供了这个页面。
试试阅读模式?希望听取您的建议

对于PHP这种需要同时处理多个请求的程序来说,申请和释放内存的时候应该慎之又慎,一不小心便会酿成大错。另一方面,除了要安全的申请和释放内存外,还应该做到内存的最小化使用,因为它可能要处理每秒钟数以千计的请求,为了提高系统整体的性能,每一次操作都应该只使用最少的内存,对于不必要的相同数据的复制则应该能免则免。我们来看下面这段PHP代码:

<?php
$a = 'Hello NowaMagic!';
$b = $a;
unset($a);	
?>

第一条语句执行后,PHP创建了$a这个变量,并为它申请了12B的内存来存放"hello world"这个字符串(最后加个NULL字符,你懂的)。紧接着把$a赋给了$b,并释放掉$a;

对于PHP来说,如果每一次变量赋值都执行一次内存复制的话,那需要额外申请12B的内存来存放这个重复的数据,当然为了复制内存,还需要cpu执行某些计算,这当然会加重cpu的负载。当第三句执行后,$a被释放了,我们刚才的设想突然变的这么滑稽,这次赋值显得好多余哦。如果早就知道$a不用了,那我们直接让$b用$a的内存不就行了,还赋值干嘛?如果你觉得12B没什么,那设想下如果$a是个10M的文件内容,或者20M,是不是我们的计算机资源消耗的有点冤枉呢?

别担心,PHP很聪明!

前面说过,PHP变量的名称和值在内核中是保存在两个不同的地方的,值是通过一个与名字毫无关系的zval结构来保存,而这个变量的名字a则保存在符号表里,两者之间通过指针联系着。在我们上面的例子里,$a是一个字符串,我们通过zend_hash_add把它添加到符号表里,然后又把它赋值给$b,两者拥有相同的内容!如果两者指向完全相同的内容,我们有什么优化措施吗?

	zval *helloval;
	MAKE_STD_ZVAL(helloval);
	ZVAL_STRING(helloval, "Hello NowaMagic!", 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);
	//通过这个例子我们看出了,我们可以把$a和$b都指向helloval~!	

现在我们检查$a和$b两个变量,他们的值指向了"Hello NowaMagic!"这个字符串在内存中的位置。但是在第三行:unset($a);这条语句释放了$a。在这种情况下,unset函数并不知道$a的值同时被$b用着,所以如果它直接释放内存,则会导致$b的值也被清空了,从而导致逻辑错误,甚至可能会导致系统崩溃。

呵呵,其实你心里明白,PHP不会让上述问题发生的!回顾一下zval的四个成员value、type、is_ref__gc、refcount__gc,我们对value和type已经很熟了,现在则是后两个成员发挥威力的时候了,这里我们主要讲解refcount__gc这个成员。当一个变量被第一次创建的时候,它对应的zval结构体的refcount__gc成员的值会被初始化为1,理由很简单,因为只有这个变量自己在用它。但是当你把这个变量赋值给别的变量时,refcount__gc属性便会加1变成2,因为现在有两个变量在用这个zval结构了!

以上描述转为内核中的代码大体如下:

	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); //这句很特殊,我们显示的增加了helloval结构体的refcount
	zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),&helloval, sizeof(zval*), NULL);	

这个时候当我们再用unset删除$a的时候,它删除符号表里的$a的信息,然后清理它的值部分,这时它发现$a的值对应的zval结构的refcount值是2,也就是有另外一个变量在一起用着这个zval,所以unset只需把这个zval的refcount减去1就行了!

写时复制机制

引用计数绝对是节省内存的一个超棒的模式!但是当我们修改$b的值,而且还需要继续使用$a时,该怎么办呢?

	$a = 1;
	$b = $a;
	$b += 5;

从代码逻辑来看,我们希望语句执行后$a仍然是1,而$b则需要变成6。我们知道在第二句完成后内核通过让$a和$b共享一个zval结构来达到节省内存的目的,但是现在第三句来了,这时$b的改变应该怎样在内核中实现呢?

答案非常简单,内核首先查看refcount__gc属性,如果它大于1则为这个变化的变量从原zval结构中复制出一份新的专属与$b的zval来,并改变其值。

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 */
		return NULL;
	}

	if ((*varval)->refcount < 2)
	{	
		//如果这个变量的zval部分的refcount小于2,代表没有别的变量在用,return
		return *varval;
	}
	
	/* 否则,复制一份zval*的值 */
	MAKE_STD_ZVAL(varcopy);
	varcopy = *varval;
	
	/* Duplicate any allocated structures within the zval* */
	zval_copy_ctor(varcopy);

	/* 从符号表中删除原来的变量
	 * This will decrease the refcount of varval in the process
	 */
	zend_hash_del(EG(active_symbol_table), varname, varname_len + 1);

	/* 初始化新的zval的refcount,并在符号表中重新添加此变量信息,并将其值与我们的新zval相关联。*/
	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;
} 

现在$b变量拥有了自己的zval,并且可以自由的修改它的值了。

Change on Write 写时复制

如果用户在PHP脚本中显式的让一个变量引用另一个变量时,我们的内核是如何处理的呢?

	$a = 1;
	$b = &$a;
	$b += 5;  

作为一个标准的PHP程序猿,我们都知道$a的值也变成6了。当我们更改$b的值时,内核发现$b是$a的一个用户端引用,也就是所它可以直接改变$b对应的zval的值,而无需再为它生成一个新的不同与$a的zval。因为他知道$a和$b都想得到这次变化!

但是内核是怎么知道这一切的呢?简单的讲,它是通过zval的is_ref__gc成员来获取这些信息的。这个成员只有两个值,就像开关的开与关一样。它的这两个状态代表着它是否是一个用户在PHP语言中定义的引用。在第一条语句($a = 1;)执行完毕后,$a对应的zval的refcount__gc等于1,is_ref__gc等于0;。 当第二条语句执行后($b = &$a;),refcount__gc属性向往常一样增长为2,而且is_ref__gc属性也同时变为了1!

最后,在执行第三条语句的时候,内核再次检查$b的zval以确定是否需要复制出一份新的zval结构来,这次不需要复制,因为我们刚才上面的get_var_and_separate函数其实是个简化版,并且少写了一个条件:

/* 如果这个zval在php语言中是通过引用的形式存在的,或者它的refcount小于2,则不许要复制。*/
if ((*varval)->is_ref || (*varval)->refcount < 2) {
	return *varval;
} 

这一次,尽管它的refcount等于2,但是因为它的is_ref等于1,所以也不会被复制。内核会直接的修改这个zval的值。

Separation Anxiety

我们已经了解了php语言中变量的复制和引用的一些事,但是如果复制和引用这两个事件被组合起来使用了该怎么办呢?看下面这段代码:

	$a = 1;
	$b = $a;
	$c = &$a;  

这里我们可以看到,$a,$b,$c这三个变量现在共用一个zval结构,有两个属于change-on-write组合($a,$c),有两个属于copy-on-write组合($a,$b),我们的is_ref__gc和refcount__gc该怎样工作,才能正确的处理好这段复杂的关系呢?

The answer is: 不可能!在这种情况下,变量的值必须分离成两份完全独立的存在!$a与$c共用一个zval,$b自己用一个zval,尽管他们拥有同样的值,但是必须至少通过两个zval来实现。见下图【在引用时强制复制!】

同样,下面的这段代码同样会在内核中产生歧义,所以需要强制复制!

	//上图对应的代码
	$a = 1;
	$b = &$a;
	$c = $a; 

需要注意的是,在这两种情况下,$b都与原初的zval相关联,因为当复制发生时,内核还不知道第三个变量的名字。

延伸阅读

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

  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/1442,欢迎访问原出处。

不打个分吗?

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

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

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

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

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

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

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

《编译原理(第2版)》 Alfred V. Aho (作者), Monica S.Lam (作者), 赵建华 (译者), 郑滔 (译者), 戴新宇 (译者)

《编译原理(第2版)》全面、深入地探讨了编译器设计方面的重要主题,包括词法分析、语法分析、语法制导定义和语法制导翻译、运行时刻环境、目标代码生成、代码优化技术、并行性检测以及过程间分析技术,并在相关章节中给出大量的实例。与上一版相比,《编译原理(第2版)》进行了全面的修订,涵盖了编译器开发方面的最新进展。每章中都提供了大量的系统及参考文献。《编译原理(第2版)》是编译原理课程方面的经典教材,内容丰富,适合作为高等院校计算机及相关专业本科生及研究生的编译原理课程的教材,也是广大技术人员的极佳参考读物。

更多计算机宝库...