50-写时复制COW机制

原创 2016年04月26日 12:30:07

50-写时复制COW机制

写时复制(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的作用,在一个数组变量tipitipi_copy时, 内存的使用并没有立刻增加一半,甚至在循环遍历数 tipicopytipi指向的同一块内存。

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

在这个例子中,如果tipicopytipi的值是不应该发生变化的, 那么,此时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

经过前文对变量的章节,我们可以理解当foofoo变量的引用计数为1。 当foobar时,PHP并没有将内存直接复制一份交给barfoo和barrefcount=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;
?>

理论上oonowamagic的引用,这两者应该指向同一块内存,其中一个被标识为回收, 另一个也应该被回收才是。但这是不可能的,因为内存本身并不知道都有哪些指针 指向了自已。在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 , 大家会非常惊讶于nowamagicfoo, 这完全是两个不同的变量么!至少我们希望是这样。

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

相关文章推荐

写时拷贝机制(copy on write)(转自搜狐达人空间的笔记)

Linux中内存的管理和分配 Linux的页目录表和页表是在程序head.s中设置的.head.s程序在物理地址0处存放了一个页目录表,紧随其后的是4个页表. page.s程序用于实现页异...

Copy-On-Write 写时复制原理

linux内核在使用fork创建进程时,基本上会使用Copy-On-Write(COW)技术。这里解释一下COW技术以及为什么在fork中使用。 WIKI上对COW的解释: Copy-on...

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

写时复制(Copy-on-Write,也缩写为COW),顾名思义,就是在写入时才真正复制一份内存进行修改。 COW最早应用在*nix系统中对线程与内存使用的优化,后面广泛的被使用在各种编程语言中,如C...

linux 写时复制 COW 过程梳理

最后一次谈到缺页,是在一年多以前,http://blog.csdn.net/chenyu105/article/details/7061845 那时结个了草率的尾,定格在了handle_pte_fa...

linux进程地址空间(2) 缺页异常详解(3)写时复制COW详解

接上一篇 现在分析写时复制COW,对于写时复制,首先把握一点就是只有写操作时才有可能触发写时复制,所以首先总要判断异常flag是否含有标志FAULT_FLAG_WRITE,然后判断二级页表条目值是否...

【C++】写时拷贝COW

#include using namespace std; class String { public: //构造函数 String(const char* str="") :_str(new...

fork,你拿什么证明你的写时拷贝(COW)

前段时间在学习内核的进程管理方面的东西,看了进程创建和进程调度的代码,想写个大而全的东西,即有内核代码分析,又有一些实验在效果上证明内核的代码。 但是这篇文章很难产,感觉自己还是驾驭不了这个宏大的主题...

Linux写时拷贝技术(copy-on-write)--COW

Linux写时拷贝技术(copy-on-write) 源于网上资料 COW技术初窥:       在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统...

fork,你拿什么证明你的写时拷贝(COW)

本文转载于:http://blog.chinaunix.net/uid-24774106-id-3361500.html  前段时间在学习内核的进程管理方面的东西,看了进程创建和进程调度的代码,想写...

写高质量OC代码52建议总结:50.构建缓存时选用NSCache而非NSDictionary

NSCache胜过NSDictionary之处在于,当系统资源耗尽时,他可以自动删减缓存,用字典就得自己写挂钩,系统低内存时手工删减,十分复杂。  NSCache不会“拷贝”键而是“保留”,这个点字...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:50-写时复制COW机制
举报原因:
原因补充:

(最多只允许输入30个字)