PHP内核原理(二)内存管理

2/22/2017来源:ASP.NET技巧人气:1271

转载请注明出处http://blog.csdn.net/fanhengguang_php

PHP内核原理 Zvals内存管理

zval结构有两个功能:第一,用于存储一个变量的值以及变量类型。第二,有效的管理内存中的zval变量的值,本章将会介绍这个功能。

接下来我们看一下引用计数和copy-on-write 这两个概念,以及在扩展中如何应用。

值和引用

在php中所有的变量都是值传递,除非你显示的指明引用传递。即任何时刻你传递一个变量给个函数或者给另外一个变量赋值,你得到的两个变量都会拥有一份独立的值的拷贝。看以下例子

<?php $a = 1; $b = $a; $a++; // Only $a was incremented, $b stays as is: var_dump($a, $b); // int(2), int(1) function inc($n) { $n++; } $c = 1; inc($c); // The $c value outside the function and the $n inside the function are distinct var_dump($c); // int(1)

虽然上面例子有些简单,但需要意识到这是php种一个基本的规则, 特别的这个规则页适用于对象。

<?php $obj = (object) ['value' => 1]; function fnByVal($val) { $val = 100; } function fnByRef(&$ref) { $ref = 100; } // The by-value function does not modify $obj, the by-reference function does: fnByVal($obj); var_dump($obj); // stdClass(value => 1) fnByRef($obj); var_dump($obj); // int(100)

人们经常说自从php5对象对象是自动的按引用传递的, 但是通过以上例子看出这是错的:按值传递的函数不能修改传递给他的变量,而按引用传递的函数可以。

看一下例子:

<?php class myclass { public $PRop; } function myfun($obj) { $obj->prop = 'world'; } $obj = new myclass(); $obj->prop = 'hello'; var_dump($obj); myfun($obj); var_dump($obj); 打印出: object(myclass)#1 (1) { ["prop"]=> string(5) "hello" } object(myclass)#1 (1) { ["prop"]=> string(5) "world" }

对象确实表现出引用传递的行为:虽然你不能将其赋值为一个完全不同的值, 但是你可以在函数中修改对象的成员。这是由于对象的值仅仅是一个用来查找实际内容的ID, 引用传递可以阻止你将其ID改为一个不同的对象或者不同的类型,但是并不能阻止你修改对象的实际的值。

以上也适用于resource类型。因为它也是同样仅仅存储了用于查找实际值的ID,所以同样的按引用传递可以阻止你修改其resource ID或者不同的类型,但是并不能阻止你resource的内容(如修改文件的指针位置)。

引用计数和写时复制

稍加思考你会得到这样的结论:php一定是做了可怕的大量拷贝。每次给函数传递变量,其值都会被拷贝一次,对于整形int或者double类型这可能没啥问题,但是想像一下给函数传递一个拥有百万元素的数组,每次调用都拷贝百万的元素将是多么低效。

为了避免拷贝,php使用了写时复制的方法:一个zval可以被多个变量、函数等共享,只要他们对变量是只读的不会修改她。如果一个变量想要做修改,在修改之前需要将zval拷贝一份。

如果一个zval可以被共享,那么php需要一个方法判断何时这个zval不被使用了,不再使用的zval将会被释放掉。PHP通过简单的跟踪一个zval被引用次数来解决这个问题, 注意这里引用指的是一个zval被变量、函数等使用,而不是变量引用(如&方式引用变量)。引用个数存储在zval结构的refcount__gc成员变量中。

为了理解引用计数原理,考虑下面的例子:

<?php $a = 1; // $a = zval_1(value=1, refcount=1) $b = $a; // $a = $b = zval_1(value=1, refcount=2) $c = $b; // $a = $b = $c = zval_1(value=1, refcount=3) $a++; // $b = $c = zval_1(value=1, refcount=2) // $a = zval_2(value=2, refcount=1) unset($b); // $c = zval_1(value=1, refcount=1) // $a = zval_2(value=2, refcount=1) unset($c); // zval_1 is destroyed, because refcount=0 // $a = zval_2(value=2, refcount=1)

当一个增加引用时refcount加1, 如果一个引用被删除refcount减1, 如果refcount变为0, 这个zval将会被销毁。

当出现循环引用时,这个方法就不适用了:

<?php $a = []; // $a = zval_1(value=[], refcount=1) $b = []; // $b = zval_2(value=[], refcount=1) $a[0] = $b; // $a = zval_1(value=[0 => zval_2], refcount=1) // $b = zval_2(value=[], refcount=2) // The refcount of zval_2 is incremented because it // is used in the array of zval_1 $b[0] = $a; // $a = zval_1(value=[0 => zval_2], refcount=2) // $b = zval_2(value=[0 => zval_1], refcount=2) // The refcount of zval_1 is incremented because it // is used in the array of zval_2 unset($a); // zval_1(value=[0 => zval_2], refcount=1) // $b = zval_2(value=[0 => zval_1], refcount=2) // The refcount of zval_1 is decremented, but the zval has // to stay alive because it's still referenced by zval_2 unset($b); // zval_1(value=[0 => zval_2], refcount=1) // zval_2(value=[0 => zval_1], refcount=1) // The refcount of zval_2 is decremented, but the zval has // to stay alive because it's still referenced by zval_1

上面的代码执行完后,出现这样一种情况两个zvals没有被任何变量引用,但是却将一直存在不被销毁,因为refcount不为0。 这是一个引用计数无效的典型的例子

为了解决这个问题php有第二种垃圾回收算法:循环垃圾回收器。我们现在可以暂时忽略他,因为循环回收对与扩展开发者来说是透明的,如果你想要了解这部分内容,可参考php手册上的介绍:http://php.net/manual/en/features.gc.collecting-cycles.php

另一种需要考虑的情况是实际的php引用(例如&$val, 而不是上面提到的引用计数的引用). php使用一个is_ref标记来表示php引用。这个标记存储在zval结构体中的is_ref__gc中。

is_ref=1标记表示这是一个引用,当变量修改时应该直接修改zval的值, 不要进行拷贝。

<?php $a = 1; // $a = zval_1(value=1, refcount=1, is_ref=0) $b =& $a; // $a = $b = zval_1(value=1, refcount=2, is_ref=1) $b++; // $a = $b = zval_1(value=2, refcount=2, is_ref=1) // Due to the is_ref=1 PHP directly changes the zval // rather than making a copy

上面的例子中在对其进行引用之前$a对应的zval的refcount为1, 现在考虑一个非常类似的例子,然而其refcount大于1

<?php $a = 1; // $a = zval_1(value=1, refcount=1, is_ref=0) $b = $a; // $a = $b = zval_1(value=1, refcount=2, is_ref=0) $c = $b // $a = $b = $c = zval_1(value=1, refcount=3, is_ref=0) $d =& $c; // $a = $b = zval_1(value=1, refcount=2, is_ref=0) // $c = $d = zval_2(value=1, refcount=2, is_ref=1) // $d is a reference of $c, but *not* of $a and $b, so // the zval needs to be copied here. Now we have the // same zval once with is_ref=0 and once with is_ref=1. $d++; // $a = $b = zval_1(value=1, refcount=2, is_ref=0) // $c = $d = zval_2(value=2, refcount=2, is_ref=1) // Because there are two separate zvals $d++ does // not modify $a and $b (as expected).

如你所见,当引用一个is_ref=0 and refcount>1的变量时需要先进行zval拷贝。 同样的当使用一个is_ref=1 and refcount>1的zval进行值传递的时候需要进行拷贝。 所以使用引用有时会使程序变慢:几乎所有的php函数都使用值传递方式,所以当传递一个is_ref=1zval给函数时会导致值拷贝。

zval创建和初始化

Now that you are familiar with the general concepts underlying zval memory management, we can move on to their practical implementation. Lets start with zval allocation:

现在你应该熟悉了zval内存管理的基本概念,接下来我们它们的实际应用。我们从zval的创建开始

zval *zv_ptr; ALLOC_ZVAL(zv_ptr);

这段代码创建了一个zval,但是没有初始化成员变量。有一个类似的宏辩题用来分配一个持久化的zval, 持久化zval在请求结束后不会被销毁。

zval *zv_ptr; ALLOC_PERMANENT_ZVAL(zv_ptr);

The difference between the two macros is that the former makes use of emalloc() whereas the latter uses malloc(). It’s important to know though that trying to directly allocate zvals will not work:

上面这两个宏的不同点是,前者使用emalloc() 二后者使用malloc()来分配内存,重要提示直接分配zvals是不行的:

/* This code is WRONG */ zval *zv_ptr = emalloc(sizeof(zval));

The reason is that the cycle collector needs to store some additional information in the zval, so the structure that needs to be allocated is actually not a zval but a zval_gc_info:

原因是循环垃圾回收需要在zval中存储一些额外的信息, 所以被创建的结构实际上是zval_gc_info二不是zval:

typedef struct _zval_gc_info { zval z; union { gc_root_buffer *buffered; struct _zval_gc_info *next; } u; } zval_gc_info;

Alloc*宏会创建一个zval_gc_info并初始化其额外的成员,但是在这之后这个值可以被透明的用作zval(因为这个结构中zval是其第一个元素).

在zval创建之后,有两个宏可以对其进行初始化,第一个INIT_PZVAL, 他会设置refcount=1 and is_ref=0但是zval的值不会被初始化。

zval *zv_ptr; ALLOC_ZVAL(zv_ptr); INIT_PZVAL(zv_ptr); /* zv_ptr has garbage type+value here */

第二个宏INIT_ZVAL也会设置refcount=1 and is_ref=0, 但是除此之外会将zval的类型初始化为IS_NULL

zval *zv_ptr; ALLOC_ZVAL(zv_ptr); INIT_ZVAL(*zv_ptr); /* zv_ptr has type=IS_NULL here */

INIT_PZVAL()接受一个zval*参数,而INIT_ZVAL()接受一个zval参数,当传递zval*给第二个宏时需要先对其解引用。

由于常常需要创建并初始化一个zval, 有两个宏可以完成创建并初始化合的工作。

zval *zv_ptr; MAKE_STD_ZVAL(zv_ptr); /* zv_ptr has garbage type+value here */ zval *zv_ptr; ALLOC_INIT_ZVAL(zv_ptr); /* zv_ptr has type=IS_NULL here */

refcount管理和zval销毁

一旦创建并初始化了一个zval, 你就可以使用之前介绍过的引用计数方法。php提供了几个宏来管理refcount:

Z_REFCOUNT_P(zv_ptr) /* Get refcount */ Z_ADDREF_P(zv_ptr) /* Increment refcount */ Z_DELREF_P(zv_ptr) /* Decrement refcount */ Z_SET_REFCOUNT(zv_ptr, 1) /* Set refcount to some particular value (here 1) */

想其他Z_类的宏一样, 没有后缀,一个P_P后缀,两个P_PP的宏分别可以接受zval, zval*, zval** 的参数

最常用的宏是Z_ADDREF_P()。 一个例子:

zval *zv_ptr; MAKE_STD_ZVAL(zv_ptr); ZVAL_LONG(zv_ptr, 42); add_index_zval(some_array, 0, zv_ptr); add_assoc_zval(some_array, "num", zv_ptr); Z_ADDREF_P(zv_ptr);

以上代码将42先插入数组到0号位置,然后用num这个key又插入了一次。所以这个zval被用在两个位置上,在创建并初始化zval后, 他的refcount为1, 想要在两个位置使用同一个zval需要将refcout设置为2, 所以必须用Z_ADDREF_P()对其加1.

The complement macro Z_DELREF_P() on the other hand is used rather rarely: Usually just decrementing the refcount is not enough, because you have to check for the refcount==0 case where the zval needs to be destroyed and freed:

另外一个宏Z_DELREF_P()相对来说用的很少,通常仅仅将refcount 减1 是不够的,因为你需要检查refcount是否等0,等0时需要销毁释放zval:

Z_DELREF_P(zv_ptr); if (Z_REFCOUNT_P(zv_ptr) == 0) { zval_dtor(zv_ptr); efree(zv_ptr); }

zval_dtor()宏接收一个zval*参数,它会将zval的value值释放掉: 如果值是字符串,则释放字符串,如果是一个数组那么对应的hashtable会被释放掉,如果是一个对象或者资源resource类型, 对应的实际的值的refcount会被减1(如果refcount减少至0将会导致他们被销毁和释放).

上面的代码中你必须自己检查refcount的值, 你可以用另外一个宏zval_ptr_dtor():来替代上面的方式:

zval_ptr_dtor(&zv_ptr);

这个宏接收一个zval**(由于历史原因,也可以传递一个zval*)。 这个宏会将refcount减1并且检查这个zval是否需要被销毁和释放。不用于我们上面手动的方式,它还支持垃圾回收,下面是相关的实现:

static zend_always_inline void i_zval_ptr_dtor(zval *zval_ptr ZEND_FILE_LINE_DC TSRMLS_DC) { if (!Z_DELREF_P(zval_ptr)) { ZEND_ASSERT(zval_ptr != &EG(uninitialized_zval)); GC_REMOVE_ZVAL_FROM_BUFFER(zval_ptr); zval_dtor(zval_ptr); efree_rel(zval_ptr); } else { if (Z_REFCOUNT_P(zval_ptr) == 1) { Z_UNSET_ISREF_P(zval_ptr); } GC_ZVAL_CHECK_POSSIBLE_ROOT(zval_ptr); } }

Z_DELREF_P()先将refcount减1然后将新的refcount返回, 所以!Z_DELREF_P(zval_ptr)和先执行Z_DELREF_P(zval_ptr)然后在检查 Z_REFCOUNT_P(zval_ptr) == 0是一样的。

上面的函数中除了执行zval_dtor()efree()操作之外,还调用了GC_*宏断言&EG(uninitialized_zval)不会被释放(这是一个被引擎使用的神奇zval)。

另外zval只被一个变量引用的话,代码中会将is_ref置为0。 因为&引用只有当两个以上变量引用zval时才有意义,所以保留is_ref=1是没有任何意义的。

提示:不要使用Z_DELREF_P()(除非你可以保证这个zval无需销毁),当你想减少refcount时,你应当使用zval_ptr_dtor()代替。zval_dtor()宏可用于临时的栈上创建的zvals:

zval zv; INIT_ZVAL(zv); /* Do something with zv here */ zval_dtor(&zv);

一个临时的栈上创建的zval是不能被共享的,当代码块退出后zval将会被销毁和释放。

拷贝zvals

虽然写时复制避免了很多zval拷贝,但是有时拷贝无可避免,例如你想要修改zval的value 或者将zval存储在其他地方。

针对不同使用场景,PHP提供了大量的宏用于拷贝。 最简单的宏是ZVAL_COPY_VALUE(),他仅仅拷贝zval的value和type成员。

zval *zv_src; MAKE_STD_ZVAL(zv_src); ZVAL_STRING(zv_src, "test", 1); zval *zv_dest; ALLOC_ZVAL(zv_dest); ZVAL_COPY_VALUE(zv_dest, zv_src);

此刻zv_destzv_src拥有同样的value和type。 注意同样的value意味着这两个zval使用同一个字符串指针(char*),这就是说如果zv_src被释放掉,那么其字符串value也会被释放掉,那么zv_dest的值将是一个悬空指针。 为了避免这中情况,应该使用zval_copy_ctor()进行拷贝

zval *zv_dest; ALLOC_ZVAL(zv_dest); ZVAL_COPY_VALUE(zv_dest, zv_src); zval_copy_ctor(zv_dest);

zval_copy_ctor会创建一个完全的拷贝。 即:如果是一个字符串那么对应char*会被拷贝,如果是一个数组,HashTable*会被拷贝, 如果是对象或者资源类型,zval内部的refcount将会+1

现在还剩下refcountis_ref的初始化没有介绍, 你可以使用INIT_PZVAL宏,或者用MAKE_STD_ZVAL替代ALLOC_ZVAL来进行初始化操作,另一种方案是使用INIT_PZVAL_COPY()替代ZVAL_COPY_VALUE()函数,它会执行拷贝并对refcountis_ref进行初始化:

zval *zv_dest; ALLOC_ZVAL(zv_dest); INIT_PZVAL_COPY(zv_dest, zv_src); zval_copy_ctor(zv_dest);

由于INIT_PZVAL_COPY()zval_copy_ctor()的联合使用非常广泛, 这二者的功能被整合到了MAKE_COPY_ZVAL()宏中:

zval *zv_dest; ALLOC_ZVAL(zv_dest); MAKE_COPY_ZVAL(&zv_src, zv_dest);

这个宏有一点欺骗性,函数参数的顺序与其他宏不同(目的指针参数不在第一位,而是第二位), 并且其接受zval**参数。

除了基本的zval拷贝宏之外还有一些比较复杂的宏,其中最重要的是ZVAL_ZVAL, 常用于从一个函数中返回zvals, 原型如下:

ZVAL_ZVAL(zv_dest, zv_src, copy, dtor)

copy参数指的是是否在目标zval上执行zval_copy_ctor(), dtor参数表示是否在源zval上执行zval_ptr_dtor()。 我们看下两个参数所有4中不同的组合的行为, 举一个最简单的例子copy和dtor参数都是0:

ZVAL_ZVAL(zv_dest, zv_src, 0, 0); /* equivalent to: */ ZVAL_COPY_VALUE(zv_dest, zv_src)

在这个例子中ZVAL_ZVAL()ZVAL_COPY_VALUE()效果一致, 想这样参数0,0没有实际意义。一个有用的变体为copy=1, dtor=0:

ZVAL_ZVAL(zv_dest, zv_src, 1, 0); /* equivalent to: */ ZVAL_COPY_VALUE(zv_dest, zv_src); zval_copy_ctor(&zv_src);

这是一个常规的zval拷贝与MAKE_COPY_ZVAL()效果类似, 只是缺少了INIT_PZVAL()这一步。当拷贝一个已经初始化的zval很有用(如return_value). 另外设置dtor=1只是增加了zval_ptr_dtor()调用。

ZVAL_ZVAL(zv_dest, zv_src, 1, 1); /* equivalent to: */ ZVAL_COPY_VALUE(zv_dest, zv_src); zval_copy_ctor(zv_dest); zval_ptr_dtor(&zv_src);

最有趣的情况是copy=0, dtor=1:

ZVAL_ZVAL(zv_dest, zv_src, 0, 1); /* equivalent to: */ ZVAL_COPY_VALUE(zv_dest, zv_src); ZVAL_NULL(zv_src); zval_ptr_dtor(&zv_src);

这构成了一个zval的move操作, zv_src中的值value被移动到了zv_dest中。 当zv_srcrefcount=1时,zval将会被zval_ptr_dtor()销毁。如果refcount大于1,zval将会以NULL值存在。

zval分离

之前介绍的一些宏主要用于拷贝zvals, 典型应用是拷贝一个zval的值到return_value中, 另外一些宏用于zval分离,这些宏用于写时复制, 我们通过示例代码来介绍他们的作用。

#define SEPARATE_ZVAL(ppzv) \ do { \ if (Z_REFCOUNT_PP((ppzv)) > 1) { \ zval *new_zv; \ Z_DELREF_PP(ppzv); \ ALLOC_ZVAL(new_zv); \ INIT_PZVAL_COPY(new_zv, *(ppzv)); \ *(ppzv) = new_zv; \ zval_copy_ctor(new_zv); \ } \ } while (0)

如果refcount是1 那么上面函数什么都不会执行, 如果refcount > 1 , 则先将refcount减1, 然后将value值拷贝到一个新的zval中。 这个函数接受一个zval**的参数,函数并不会修改这个指针指向的zval*

这在实际中有什么用处呢,假设你想要修改一个数组中的元素如$array[42], 你需要先拿到一个指向数组元素的zval*的一个指针zval**. 由于引用计数的关系, 你不能直接修改它(因为这个zval的值可能同时被其他变量引用), 所以你需要先进行zval分离, 如果refcount = 1 那么zval分离会保留原有zval不动, 如果大于1会进行zval拷贝, 后一种情况下新的zval会被赋值给一个zval**指针。 在上面这个例子的情况下, 也就是指数组中42这个元素。

在这里简单通过MAKE_COPY_ZVAL()进行拷贝时不够的, 因为拷贝后的zval并不是数组中存储的那个zval。

当修改zval前直接通过SEPARATE_ZVAL()函数进行分离并不适用于所有情况。 当zval is_ref=1时zval分离不会发生。 为解决这个问题,我们看一下php提供的is_ref操作宏:

Z_ISREF_P(zv_ptr) /* Get if zval is reference */ Z_SET_ISREF_P(zv_ptr) /* Set is_ref=1 */ Z_UNSET_ISREF_P(zv_ptr) /* Set is_ref=0 */ Z_SET_ISREF_TO_P(zv_ptr, 1) /* Same as Z_SET_ISREF_P(zv_ptr) */ Z_SET_ISREF_TO_P(zv_ptr, 0) /* Same as Z_UNSET_ISREF_P(zv_ptr) */

同样的上面这些宏也有诸如_P_PP之类的变体用于处理zval, zval*, zval**。另外还存在一个老的宏PZVAL_IS_REF()它等同于Z_ISREF_P()宏。

PHP提供了两个SEPARATE_ZVAL()宏的变体:

#define SEPARATE_ZVAL_IF_NOT_REF(ppzv) \ if (!PZVAL_IS_REF(*ppzv)) { \ SEPARATE_ZVAL(ppzv); \ } #define SEPARATE_ZVAL_TO_MAKE_IS_REF(ppzv) \ if (!PZVAL_IS_REF(*ppzv)) { \ SEPARATE_ZVAL(ppzv); \ Z_SET_ISREF_PP((ppzv)); \ }

SEPARATE_ZVAL_IF_NOT_REF在修改zval中经常用到, SEPARATE_ZVAL_TO_MAKE_IS_REF宏主要由php引擎调用,扩展开发中很少用到

有另外一个用于zval分离的宏, 与上面介绍的有些不同:

#define SEPARATE_ARG_IF_REF(varptr) \ if (PZVAL_IS_REF(varptr)) { \ zval *original_var = varptr; \ ALLOC_ZVAL(varptr); \ INIT_PZVAL_COPY(varptr, original_var); \ zval_copy_ctor(varptr); \ } else { \ Z_ADDREF_P(varptr); \ }

首先它接收一个zval*而不是zval**指针。 其次这个宏会增加refcount, 而其他宏不会。

除此之外这个宏是SEPARATE_ZVAL_IF_NO_REF宏的补充:当一个zval是一个引用时进行zval分离。 它主要用于确保一个参数是一个值而不是引用。