PHP内核探索:写时复制COW机制

在写入时才真正复制一份内存进行修改
服务器君一共花费了779.209 ms进行了6次数据库查询,努力地为您提供了这个页面。
试试阅读模式?希望听取您的建议

写时复制(Copy-on-Write,也缩写为COW),顾名思义,就是在写入时才真正复制一份内存进行修改。 COW最早应用在*nix系统中对线程与内存使用的优化,后面广泛的被使用在各种编程语言中,如C++的STL等。 在PHP内核中,COW也是主要的内存优化手段。 在前面关于变量和内存的讨论中,引用计数对变量的销毁与回收中起着至关重要的标识作用。 引用计数存在的意义,就是为了使得COW可以正常运作,从而实现对内存的优化使用。

写时复制的作用

经过上面的描述,大家可能会COW有了个主观的印象,下面让我们看一个小例子, 非常容易看到COW在内存使用优化方面的明显作用:

<?php
$j = 1;
var_dump(memory_get_usage());
 
$tipi = array_fill(0, 100000, 'php-internal');
var_dump(memory_get_usage());
 
$tipi_copy = $tipi;
var_dump(memory_get_usage());
 
foreach($tipi_copy as $i){
    $j += count($i); 
}
var_dump(memory_get_usage());
 
?>
//-----执行结果-----
$ php t.php 
int(630904)
int(10479840)
int(10479944)
int(10480040)

上面的代码比较典型的突出了COW的作用,在一个数组变量$tipi被赋值给$tipi_copy时, 内存的使用并没有立刻增加一半,甚至在循环遍历数 $tipi_copy时, 实际上遍历的,仍是$tipi指向的同一块内存。

也就是说,即使我们不使用引用,一个变量被赋值后,只要我们不改变变量的值 ,也与使用引用一样。 进一步讲,就算变量的值立刻被改变,新值的内存分配也会洽如其分。 据此我们很容易就可以想到一些COW可以非常有效的控制内存使用的场景, 如函数参数的传递,大数组的复制等等。

在这个例子中,如果$tipi_copy的值发生了变化,$tipi的值是不应该发生变化的, 那么,此时PHP内核又会如何去做呢?我们引入下面的示例:

<?php
//$tipi = array_fill(0, 3, 'php-internal');  
//这里不再使用array_fill来填充 ,为什么?
$tipi[0] = 'php-internal';
$tipi[1] = 'php-internal';
$tipi[2] = 'php-internal';
var_dump(memory_get_usage());
 
$copy = $tipi;
xdebug_debug_zval('tipi', 'copy');
var_dump(memory_get_usage());
 
$copy[0] = 'php-internal';
xdebug_debug_zval('tipi', 'copy');
var_dump(memory_get_usage());
 
?>
//-----执行结果-----
$ php t.php 
int(629384)
tipi: (refcount=2, is_ref=0)=array (0 => (refcount=1, is_ref=0)='php-internal', 1 => (refcount=1, is_ref=0)='php-internal', 2 => (refcount=1, is_ref=0)='php-internal')
copy: (refcount=2, is_ref=0)=array (0 => (refcount=1, is_ref=0)='php-internal', 1 => (refcount=1, is_ref=0)='php-internal', 2 => (refcount=1, is_ref=0)='php-internal')
int(629512)
tipi: (refcount=1, is_ref=0)=array (0 => (refcount=1, is_ref=0)='php-internal', 1 => (refcount=2, is_ref=0)='php-internal', 2 => (refcount=2, is_ref=0)='php-internal')
copy: (refcount=1, is_ref=0)=array (0 => (refcount=1, is_ref=0)='php-internal', 1 => (refcount=2, is_ref=0)='php-internal', 2 => (refcount=2, is_ref=0)='php-internal')
int(630088)

从上面例子我们可以看出,当一个数组整个被赋给一个变量时,只是将内存将内存地址赋值给变量。 当数组的值被改变时,Zend内核重新申请了一块内存,然后赋之以新值,但不影响其他值的内存状态。 写时复制的最小粒度,就是zval结构体, 而对于zval结构体组成的集合(如数组和对象等),在需要复制内存时,将复杂对象分解为最小粒度来处理。 这样做就使内存中复杂对象中某一部分做修改时,不必将该对象的所有元素全部“分离”出一份内存拷贝, 从而节省了内存的使用。

写时复制的实现

由于内存块没有办法标识自己被几个指针同时使用, 仅仅通过内存本身并没有办法知道什么时候应该进行复制工作, 这样就需要一个变量来标识这块内存是“被多少个变量名指针同时指向的”, 这个变量,就是前面关于变量的章节提到的:引用计数。

这里有一个比较典型的例子:

<?php
    $foo = 1;
    xdebug_debug_zval('foo');
    $bar = $foo;
    xdebug_debug_zval('foo');
    $bar = 2;
    xdebug_debug_zval('foo');   
?>
//-----执行结果-----
foo: (refcount=1, is_ref=0)=1
foo: (refcount=2, is_ref=0)=1
foo: (refcount=1, is_ref=0)=1

经过前文对变量的章节,我们可以理解当$foo被赋值时,$foo变量的引用计数为1。 当$foo的值被赋给$bar时,PHP并没有将内存直接复制一份交给$bar, 而是直接把$foo和$bar指向同一个地址。这时,我们可以看到refcount=2; 最后,我们更改了$bar的值,这时如果两个变量再指向同一个内存地址的话, 其值就会同时改变,于是,PHP内核这时将内存复制出来一份,并将其值写为2 ,(这个操作也称为分离操作), 同时维护原$foo变量的引用计数:refcount=1。

上面小例子中的xdebug_debug_zval()是xdebug扩展中的一个函数,用于输出变量在zend内部的引用信息。 如果你没有安装xdebug扩展,也可以使用debug_zval_dump()来代替。 参考:http://www.php.net/manual/zh/function.debug-zval-dump.php

写时复制应用的场景很多,最常见是赋值和函数传参。 在上面的例子中,就使用了zend_assign_to_variable()函数(Zend/zend_execute.c) 对变量的赋值进行了各种判断和处理。 其中最终处理代码如下:

if (PZVAL_IS_REF(value) && Z_REFCOUNT_P(value) > 0) {
    ALLOC_ZVAL(variable_ptr);
    *variable_ptr_ptr = variable_ptr;
    *variable_ptr = *value;
    Z_SET_REFCOUNT_P(variable_ptr, 1);
    zval_copy_ctor(variable_ptr);
} else {
    *variable_ptr_ptr = value;
    Z_ADDREF_P(value);
}

从这段代码可以看出,如果要进行操作的值已经是引用类型(如已经被&操作符操作过), 则直接重新分配内存,否则只是将value的地址赋与变量,同时将值的zval_value.refcount进行加1操作。

如果大家看过前面的章节, 应该对变量存储的结构体zval(Zend/zend.h)还有印象:

typedef struct _zval_struct zval;
...
struct _zval_struct {
    /* Variable information */
    zvalue_value value;     /* value */
    zend_uint refcount__gc;
    zend_uchar type;    /* active type */
    zend_uchar is_ref__gc;
};

PHP对值的写时复制的操作,主要依赖于两个参数:refcount__gc与is_ref__gc。 如果是引用类型,则直接进行“分离”操作,即时分配内存, 否则会写时复制,也就是在修改其值的时候才进行内存的重新分配。

写时复制的规则比较繁琐,什么情况会导致写时复制及分离,是有非常多种情况的。 在这里只是举一个简单的例子帮助大家理解,后续会在附录中列举PHP中所有写时复制的相关规则。

写时复制的矛盾,PHP中不推荐使用&操作符的部分解释

上面是一个比较典型的例子,但现实中的PHP实现经过各种权衡, 甚至有时对一个特性的支持与否,是互相矛盾且难以取舍的。 比如,unset()看上去是用来把变量释放,然后把内存标记于空闲的。 可是,在下面的例子中,unset并没有使内存池的内存增加:

<?php
$nowamagic = 10;
$o_o  = &$nowamagic;
unset($o_o);
echo $nowamagic;
?>

理论上$o_o是$nowamagic的引用,这两者应该指向同一块内存,其中一个被标识为回收, 另一个也应该被回收才是。但这是不可能的,因为内存本身并不知道都有哪些指针 指向了自已。在C中,o_o这时的值应该是无法预料的, 但PHP不想把这种维护变量引用的工作交给用户,于是, 使用了折中的方法,unset()此时只会把nowamagic变量名从hashtable中去掉, 而内存值的引用计数减1。实际的内存使用完全没有变化。

试想,如果$nowamagic是一个非常大的数组,或者是一个资源型的变量。 这种情形绝对是我们不想看到的。

上面这个例子我们还可以理解,如果每个这种类似操作都要用户来关心。 那PHP就是变换了语法的C了。而下面的这个例子,与其说是语言特性, 倒不如说是更像BUG多一些。(事实上对此在PHP官方的邮件组里有也争论)

<?php
$foo ['love'] = 1;
$bar  = &$foo['love'];
$tipi = $foo;
$tipi['love'] = '2';
echo $foo['love'];
?>

这个例子最后会输出 2 , 大家会非常惊讶于$nowamagic怎么会影响到$foo, 这完全是两个不同的变量么!至少我们希望是这样。

最后,不推荐大家使用 & ,让PHP自己决定什么时候该使用引用好了, 除非你知道自己在做什么。

延伸阅读

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

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

不打个分吗?

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

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

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

大家都在看

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

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

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

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

《重构:改善既有代码的设计》 福勒(Martin Fowler) (作者), 熊节 (译者)

《重构:改善既有代码的设计》清晰地揭示了重构的过程,解释了重构的原理和最佳实践方式,并给出了何时以及何地应该开始挖掘代码以求改善。书中给出了70多个可行的重构,每个重构都介绍了一种经过验证的代码变换手法的动机和技术。《重构:改善既有代码的设计》提出的重构准则将帮助你一次一小步地修改你的代码,从而减少了开发过程中的风险。

更多计算机宝库...