PHP扩展接口的声明

我们经常使用PHP_FUNCTION(count)之类,来声明对外提供的count接口,经过一系列的宏转换之后,会变成:

void zif_count(int ht, zval *return_value, zval **return_value_ptr, zval *this_ptr, int return_value_used TSRMLS_DC)

php脚本的解析运行过程

  1. 使用lex进行词法解析,根据src/main/Zend/zend_language_scanner.l 将php代码解析为token
  2. 使用bison进行语法解析,根据src/main/Zend/zend_language_parser.y将token码,通过y文件里对应的zend compiler函数,填充其对应的struct opcode
  3. zend引擎 解析执行这些opcode

那么PHP语言层面的一些概念,如何实现呢?比如变量的作用域、弱类型?

PHP变量与内存使用

PHP虽然是弱类型语言,但底层基于C,故其底层的真正存储仍然需要区分类型。目前支持的类型有bool、long、double、string、array、object、resouce和NULL。存储对应的数据结构是zval,使用type标明变量的真实类型,使用union _zvalue_value存储变量的真实数据。

从zvalue的定义可以看到:

typedef union _zvalue_value {

        long lval;                                      /* long value */

        double dval;                            /* double value */

        struct {

                char *val;

                int len;

        } str;

        HashTable *ht;                          /* hash table value */

        zend_object_value obj;

} zvalue_value;

bool、long都是使用long类型来存储的,也包括看起来像是short、int、unsigned的数据,也就是说,你以为只需要一个byte的时候,光底层真实数据的存储,就会消耗掉8个byte!类似的情况也出现在float类型上。这也是PHP消耗内存过多的一个原因吧。

NULL型变量比较节省资源,zvalue_value是空指针,仅使用type标识即可。所以,在关注内存的情况下,对于不确定类型的初始化,使用NULL会比使用空字符串、0等要更节省空间。

与array、hash相关的实现是HashTable,其采用链表法解决hash冲突。这里存在的安全漏洞是用户可以通过构造特定的hash key,增大冲突率,从而是hash的O(1)操作退化为链表的O(n)操作,从而造成系统服务能力的退化。PHP目前仅是通过限制用户提交的数据个数的方式,来提高门槛,所以风险依然存在!

max_input_vars integer

How many input variables may be accepted (limit is applied to $_GET, $_POST and $_COOKIE superglobal separately). Use of this directive mitigates the possibility of denial of service attacks which use hash collisions. If there are more input variables than specified by this directive, an E_WARNING is issued, and further input variables are truncated from the request.

另外,在一个hash表里,数据量太大时,可能也会存在resize的情况,这也会在瞬间造成一些数据的copy。需要关注的是rehash函数zend_hash_rehash,在hash中存储的元素数 > hash bucket size的时候会被调用,因为元素过多,会导致冲突率增大。rehash以2x的速率进行,在此过程中确实会分配新的内存以增大buckets 大小,并调整两组双向指针,但不会涉及用户data的copy,所以一定程度上而言还是考虑了性能问题的。

变量赋值与引用计数

总体来说,PHP使用了基于引用计数的GC机制,和在内存使用方面使用写时复制。

在struct zval 里,有ref_count,和is_ref两个字段。前者记录该变量指向的内存,被引用的次数。后者是bool值,标记该内存是否是被引用的。

在不考虑其他变量的情况下,如果$b = $a,虽然这时候是copy by value,但由于此时b和a的值确实是一样的,所以基于写时复制的思想,a和b的refcount都是2,且两者的is_ref都是false。随后,如果修改b,例如$b=20,就需要将b和a指向的内存分裂开了,这时a和b的refcount都会变成1,is_ref维持为false。(修改a的道理是一样的)

而如果初始时是by reference的,即$b = &$a,则两者的refcount都是2,且is_ref为true。无论怎样修改a和b的,两个字段都不发生变化。

那么数组与对象的赋值是怎样的呢?

从表面看,跟标量是一样的,assign-by-value时,共享同样的zval array/object内存区。但需要注意的是,它们还含有子zval结构!

先来看array,数组变量和其内部字段,都遵循写时复制!所以,当$b = $a,这种赋值方式时,a和b的表现如同两个独立的变量,虽然PHP会尽量复用内存空间。

再来看object,可以将object的assign-by-value赋值视为指针,但其is_ref=false。PHP中对象是按引用传递的,即对象进行赋值和操作的时候是按引用(reference)传递的,而不是整 个对象的拷贝。

混合使用assign-by-value和assign-by-ref

ref_count和is_ref的设计,都是为了尽可能节省内存,并且正确处理变量的赋值和改变(基本跟C语言的传值、传引用差不多)。那么考虑下,当把assign-by-value和assign-by-ref混合使用会怎样呢?答案是,没法省内存了。

$a = ‘sample';

$b = $a;

$c = &$a;

上面这段代码,line2的时候,还遵循写时复制,a和b共享一块内存。到line3时,假设还共享内存,那么其对应的ref_count和is_ref怎样设置呢?如果ref_count=3,且is_ref=false,那修改c的时候,将导致开辟新内存给c,而a和b共享旧内存,而a的值这时是应该随之改变的;如果is_ref=true,那么a、b、c还共享这块内存,而b的值应该是不变的!所以,PHP的做法是,在line3时,就分配了新内存,a与c共享内存,ref_count=2,is_ref=true;b的ref_count=1,is_ref=false。颠倒line2和line3的情况差不多。

所以,最好切记,除非清楚知道自己在做什么,否则别把assign-by-value和assign-by-ref混合使用

另外,如果进行字符串连接,例如 $a = ‘a'; $b = ‘b'; $c = $a . $b; 那么c没有复用a和b的存储空间,所以它们的refcount都=0。

is_ref的用途

从上面可以推测出,is_ref如果为true,当值发生改变的时候,不发生写时复制导致的分裂,所有指向该内存的“指针”对应的值都自然随之改变。

另外,还需要注意的是,仅当refcount>1时,is_ref才可以为true。

When the reference count of a variable container reaches 1 and the is_ref value is set to 1, the is_ref value is reset to 0. The reason for this is that a variable container can only be marked as a referenced variable container when there is more than one variable pointing to the variable container.

用户函数调用与引用计数

用户自定义的php函数调用时,会初始化两个符号表(symbol tables),一个是存储函数参数的function stack,另一个是存储函数生命周期的变量的function symbol table。假设有如下方法:function foo($a),并使用foo($init)那么$a的ref_count=3,且is_ref=false。3次引用分别是init、function stack的arg0、function symbol table的变量a。之所以init和a这时会共享同样的内存,还是由于没有发生写时复制。

但需要注意的是,如果init被显示声明为global变量,那么init与arg0和a不会共享同样的内存。因为global会在建立一个指向local变量的引用!

global $intval;

$intval = 10;

xdebug_debug_zval(‘intval’); // refcount = 1, is_ref = 1

function test_pass_int($i)

{

xdebug_debug_zval(‘i’);  // refcount = 2, is_ref = 0  被function stack和function symbol table中的变量使用,没有与intval共享zval内存区,因为intval是is_ref=true的!

 

$i = 20;

xdebug_debug_zval(‘i’); // refcount = 1, is_ref = 1 发生了写时复制,function stack和function symbol table中的变量分裂了

}

test_pass_int($intval);

 

如果做一个小小的改变,改为pass-by-ref,function test_pass_int(&$i),则function stack、function symbol和intval会共享zval了, test_pass_init里print的结果都是refcount = 3, is_ref = 1。

可以通过传引用调用(Passing References to Functions)或返回引用(Returning by Reference)来模拟C里的指针,从而使函数体内可以修改传入的值。在需要交互大数据array或对象时,也可以提高效率。

function &find_node($key, &$tree)

{

$item = &$tree[$key];

return $item;
}

$tree = array(…);

$node = &find_node(3, $tree);

$node = ‘new value';

 

类静态方法与成员方法

有时会看到 采用静态调用的形式,调用成员方法,例如:

class A

{

function foo() {}

public $var = 1;  // should change to : public static $var = 1;

}

A::foo();

var_dump(A::$var); // error

而这时居然可以调用foo成功 ,且PHP不会报error或warn(只在E_STRICT时有提示)。除了语言设计的疏漏和兼容性之外,也是因为静态方法和成员方法都是存储在struct _zend_class_entry 的HashTable function_table字段里的。但想以类似的方式访问成员变量var就不行了,因为静态变量存储在HashTable default_static_members 里,而成员变量存储在default_properties里。

内存管理

参考资料

http://nikic.github.io/2012/03/28/Understanding-PHPs-internal-array-implementation.html

http://derickrethans.nl/talks/phparch-php-variables-article.pdf

http://php.net/language.references

http://php.net/manual/en/internals2.php

 

 

Leave a Reply