标签 - PHP内核

PHP内核    2019-04-25 15:50:20    2    0    0

PHP是一种弱类型的脚本语言,弱类型不表示PHP的变量没有类型区分,PHP变量有8种原始类型:

四种标量类型:

  • boolean(布尔型)
  • integer(整型)
  • float(浮点型)
  • string(字符串)

两种复合类型:

  • array(数组)
  • object(对象)

两种特殊类型:

  • resource(资源)
  • NULL

一个变量能在运行期间从一种类型转换为另一种类型,那么PHP是如何实现这种变量的类型戏法的呢?

在引擎内部,变量都是用一个结构体来表示,这个结构体可以在{PHPSRC}/Zend/zend.h中找到:

1struct _zval_struct { 
2    /* Variable information */ 
3    zvalue_value value;     /* value */ 
4    zend_uint refcount__gc; 
5    zend_uchar type;    /* active type */ 
6    zend_uchar is_ref__gc; 
7};

这里我们暂时只关心 value和type两个成员,其中value是一个联合, 也就是变量的实际值,type是变量的动态类型,根据类型的不同,value使用不同的成员。

type的各种类型都被定义成了宏,同样在此文件中定义了这些宏:

01#define IS_NULL     0 
02#define IS_LONG     1 
03#define IS_DOUBLE   2 
04#define IS_BOOL     3 
05#define IS_ARRAY    4 
06#define IS_OBJECT   5 
07#define IS_STRING   6 
08#define IS_RESOURCE 7 
09#define IS_CONSTANT 8 
10#define IS_CONSTANT_ARRAY   9

value的类型zvalue_value同样定义在此文件中:

01typedef union _zvalue_value { 
02    long lval;                  /* long value */ 
03    double dval;                /* double value */ 
04    struct 
05        char
PHP内核    2019-04-25 15:49:56    3    0    0

在PHP语言中,变量都是保存在哈希表中,称为变量符号表,其中变量名为哈希表的键,变量名对应的容器zval的指针为哈希表中的值。所有全局变量放在一张主符号表中(也就是数组$GLOBALS对应的哈希表)。PHP语言有个特性,变量在命名时,$变量标识符后不能以数字开头。例如我们在以下代码:

1<?php
2$111"nowamagic";
3?>

会报如下错误:Parse error: syntax error, unexpected T_LNUMBER, expecting T_VARIABLE or '$' in...

从错误的描叙来看,这是一个语法错误,于是我们推论对变量名合法性的判断应该是在编译时的语法分析阶段。为了证明观点,我们可以试着在执行阶段定义一个数字字符开头的变量:

1<?php
2$a = 111;
3$$a "nowamagic"//以变量$a的值作为变量名
4echo $$a;
5var_dump($GLOBALS);
6?>

运行之后发现不报错,并且在全局符号表$GLOBALS中发现相关符号:

1["a"]=>
2int(111)
3["111"]=>
4string(2) "nowamagic"

这样我们就定义了一个全数字字符命名的变量,似乎违背了PHP的规则,但是确实做到了。(读者可以试着输出$GLOBALS["111"]的值,虽然有值,但是结果却为NULL,这个是PHP中一个类型转换的特性)

在这段代码中,$$a = "nowamagic"这条语句具有多态性,只有当实际执行到这条语句的时候,我们才能确定变量名,PHP在语法分析阶段无法知道这个变量名会是什么,所以就不会报错,在执行阶段,PHP语言不判断变量名的合法性,于是就产生了这样一个叛逆的变量。

知道了这个特性之后,马上会想到一个另外一个特殊的变量:$this,在类的方法中,$this关键字用来指向当前类的对象实例,如果对$this进行赋值操作,会发生什么事情?

01<?php 
02class Person 
03
04    protected $_name "phper"
05   
06    protected $_age  = 18; 
07   
08    public function getName() 
09    
10        $this = 123; 
11        return
PHP内核    2019-04-25 15:49:25    6    0    0

变量的作用域是变量的一个作用范围,在这个范围内变量为可见的,即可以访问该变量的代码区域, 相反,如果不在这个范围内,变量是不可见的,无法被调用。 (全局变量可以将作用范围看作为整个程序) 如下面的例子:(会输出什么样的结果呢?)

1<?php
2    $foo 'nowamagic';
3    function variable_scope(){
4        $foo 'foo';
5        print $foo ;
6        print $bar ;
7    }
8?>

由此可见,变量的作用域是一个很基础的概念,在变量的实现中比较重要。

全局变量与局部变量

变量按作用域类型分为:全局变量和局部变量。全局变量是在整个程序中任何地方随意调用的变量, 在PHP中,全局变量的“全局化”使用gloal语句来实现。 相对于全局变量,局部变量的作用域是程序中的部分代码(如函数中),而不是程序的全部。

变量的作用域与变量的生命周期有一定的联系, 如在一个函数中定义的变量, 这个变量的作用域从变量声明的时候开始到这个函数结束的时候。 这种变量我们称之为局部变量。它的生命周期开始于函数开始,结束于函数的调用完成之时。

变量的作用域决定其生命周期吗?程序运行到变量作用域范围之外,就会将变量进行销毁吗?

对于不同作用域的变量,如果存在冲突情况,就像上面的例子中,全局变量中有一个名为$bar的变量, 在局部变量中也存在一个名为$bar的变量, 此时如何区分呢?

对于全局变量,ZEND内核有一个_zend_executor_globals结构,该结构中的symbol_table就是全局符号表, 其中保存了在顶层作用域中的变量。同样,函数或者对象的方法在被调用时会创建active_symbol_table来保存局部变量。 当程序在顶层中使用某个变量时,ZE就会在symbol_table中进行遍历, 同理,如果程序运行于某个函数中,Zend内核会遍历查询与其对应的active_symbol_table, 而每个函数的active_symbol_table是相对独立的,由此而实现的作用域的独立。

展开来看,如果我们调用的一个函数中的变量,ZE使用_zend_execute_data来存储 某个单独的op_array(每个函数都会生成单独的op_array)执行过程中所需要的信息,它的结构如下:

PHP内核    2019-04-25 15:48:45    6    0    0

在强类型的语言当中,当使用一个变量之前,我们需要先声明这个变量。然而,对于PHP来说, 在使用一个变量时,我们不需要声明,也不需要初始化,直接对其赋值就可以使用,这是如何实现的?

在PHP中没有对常规变量的声明操作,如果要使用一个变量,直接进行赋值操作即可。在赋值操作的同时已经进行声明操作。 一个简单的赋值操作:

1$a = 10;

使用VLD扩展查看其生成的中间代码为 ASSIGN。 依此,我们找到其执行的函数为 ZEND_ASSIGN_SPEC_CV_CONST_HANDLER。 (找到这个函数的方法之一:$a为CV,10为CONST,操作为ASSIGN。) CV是PHP在5.1后增加的一个在编译期的缓存。如我们在使用VLD查看上面的PHP代码生成的中间代码时会看到:

1compiled vars:  !0 = $a

这个$a变量就是op_type为IS_CV的变量。IS_CV值的设置是在语法解析时进行的。可以参见Zend/zend_complie.c文件中的zend_do_end_variable_parse函数。

在这个函数中,获取这个赋值操作的左值和右值的代码为:

1zval *value = &opline->op2.u.constant;
2zval **variable_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1,
3                                    EX(Ts), BP_VAR_W TSRMLS_CC);

由于右值为一个数值,我们可以理解为一个常量,则直接取操作数存储的constant字段, 关于这个字段的说明将在后面的虚拟机章节说明。 左值是通过 _get_zval_ptr_ptr_cv函数获取zval值。这个函数最后的调用顺序为: [_get_zval_ptr_ptr_cv] --> [_get_zval_cv_lookup]

在_get_zval_cv_lookup函数中关键代码为:

1zend_hash_quick_find(EG(active_symbol_table), cv->name, cv->name_len+1,
2                                    cv->hash_value, (void **)ptr)

这是一个HashTable的查

PHP内核    2019-04-25 15:48:15    11    0    0

通过前面章节的描述,我们已经知道了PHP中变量的存储方式--所有的变量都保存在zval结构中。 下面介绍一下PHP内核如何实现变量的定义方式以及作用域。

在ZE进行词法和语法的分析之后,生成具体的opcode,这些opcode最终被execute函数(Zend/zend_vm_execute.h:46)解释执行。 在excute函数中,有以下代码:

01while (1) {
02    ...
03    if ((ret = EX(opline)->handler(execute_data TSRMLS_CC)) > 0) {
04        switch (ret) {
05            case 1:
06                EG(in_execution) = original_in_execution;
07                return;
08            case 2:
09                op_array = EG(active_op_array);
10                goto zend_vm_enter;
11            case 3:
12                execute_data = EG(current_execute_data);
13            default:
14                break;
15        }    
16    }    
17    ...
18}

这里的EX(opline)->handler(...)将op_array中的操作顺序执行, 其中变量赋值操作在ZEND_ASSIGN_SPEC_CV_CONST_HANDLER()函数中进行。 ZEND_ASSIGN_SPEC_CV_CONST_HANDLER中进行一些变量类型的判断并在内存中分配一个zval,然后将变量的值存储其中。 变量名和指向这个zval的指针,则会存储于符号表内。 ZEND_ASSIGN_SPEC_CV_CONST_HANDLER的最后会调用ZEND_VM_NEXT_OPCODE()将op_array的指针移到下一条opline, 这样就会形成循环执行的效果。

在ZE执行的过程中,有四个全局的变量,这些变量都是用于ZE运行时所需信息的存储:

1/
PHP内核    2019-04-25 15:47:38    6    0    0

PHP是弱类型语言,向方法传递参数时候也并不严格检查数据类型。 不过有时需要判断传递到方法中的参数,为此PHP中提供了一些函数,来判断数据的类型。 比如is_numeric(),判断是否是一个数值或者可转换为数值的字符串,比如用于判断对象的类型运算符:instanceof。 instanceof 用来测定一个给定的对象是否来自指定的对象类。instanceof 运算符是 PHP 5 引进的。

PHP内核    2019-04-25 15:46:53    24    0    0

通常意义上静态变量是静态分配的,他们的生命周期和程序的生命周期一样, 只有在程序退出时才结束期生命周期,这和局部变量相反,有的语言中全局变量也是静态分配的。 例如PHP和Javascript中的全局变量。

静态变量可以分为:

  • 静态全局变量,PHP中的全局变量也可以理解为静态全局变量,因为除非明确unset释放,在程序运行过程中始终存在。
  • 静态局部变量,也就是在函数内定义的静态变量,函数在执行时对变量的操作会保持到下一次函数被调用。
  • 静态成员变量,这是在类中定义的静态变量,和实例变量相对应,静态成员变量可以在所有实例中共享。

最常见的是静态局部变量及静态成员变量。局部变量只有在函数执行时才会存在。 通常,当一个函数执行完毕,它的局部变量的值就已经不存在,而且变量所占据的内存也被释放。 当下一次执行该过程时,它的所有局部变量将重新初始化。如果某个局部变量定义为静态的, 则它的值不会在函数调用结束后释放,而是继续保留变量的值。

在本小节将介绍静态局部变量,有关静态成员变量的内容将在类与对象章节进行介绍。

先看看如下局部变量的使用:

1function t() {
2    static $i = 0;
3    $i++;
4    echo $i' ';
5}
6  
7t();
8t();
9t();

上面的程序会输出1 2 3。从这个示例可以看出,$i变量的值在改变后函数继续执行还能访问到, $i变量就像是只有函数t()才能访问到的一个全局变量。 那PHP是怎么实现的呢?

static是PHP的关键字,我们需要从词法分析,语法分析,中间代码生成到执行中间代码这几个部分探讨整个实现过程。

词法分析

首先查看 Zend/zend_language_scanner.l文件,搜索 static关键字。我们可以找到如下代码:

1<ST_IN_SCRIPTING>"static" {
2    return T_STATIC;
3}

语法分析

在词法分析找到token后,通过这个token,在Zend/zend_language_parser.y文件中查找。找到相关代码如下:

1|   T_STATIC static_var_list ';'
2  
3static_var_list:
4        static_var_list ',' T_VARIABLE { zend_do_fetch_static_variable(&
PHP内核    2019-04-25 15:46:06    7    0    0

PHP是弱类型,动态的语言脚本。在申明一个变量的时候,并不需要指明它保存的数据类型。

1<?php 
2$var = 1; 
3$var "variable"
4$var = 1.00; 
5$var array(); 
6$var new Object(); 
7?>

动态变量,在运行期间是可以改变的,并且在使用前无需声明变量类型。

那么,问题一、Zend引擎是如何用C实现这种弱类型的呢?

实际上,在PHP中声明的变量,在ZE中都是用结构体zval来保存的。首先我们打开Zend/zend.h来看zval的定义:

01typedef struct _zval_struct zval; 
02   
03struct _zval_struct { 
04    /* Variable information */ 
05    zvalue_value value;     /* value */ 
06    zend_uint refcount__gc; 
07    zend_uchar type;    /* active type */ 
08    zend_uchar is_ref__gc; 
09}; 
10   
11typedef union _zvalue_value { 
12    long lval;  /* long value */ 
13    double dval;    /* double value */ 
14    struct 
15        char *val; 
16        int len; 
17    } str; 
18    HashTable *ht;  /* hash table value */ 
19    zend_object_value obj; 
20} zvalue_value;

Zend/zend_types.h:

1typedef unsigned char zend_bool; 
2typedef unsigned char zend_uchar; 
3typedef unsigned int zend_uint; 
4typedef unsigned long zend_ulong; 
5typedef unsigned short zend_ushort; 

从上述代码中,可以看到_zvalue_value是真正保存数据

PHP内核    2019-04-25 15:45:33    17    0    0

现在我们已经可以从符号表中获取用户在PHP语言里定义的变量了,是该做点其它事的时候了,举个比例,比如给它来个类型转换:-)。想想C语言中的类型转换细则,你的头是不是已经大了?但是变量的类型转换就是如此重要,如果没有,那我们的代码就会是下面这样了:

01void display_zval(zval *value)
02{
03    switch (Z_TYPE_P(value)) {
04        case IS_NULL:
05            /* 如果是NULL,则不输出任何东西 */
06            break;
07         
08        case IS_BOOL:
09            /* 如果是bool类型,并且true,则输出1,否则什么也不干 */
10            if (Z_BVAL_P(value)) {
11                php_printf("1");
12            }
13            break;
14        case IS_LONG:
15            /* 如果是long整型,则输出数字形式 */
16            php_printf("%ld", Z_LVAL_P(value));
17            break;
18        case IS_DOUBLE:
19            /* 如果是double型,则输出浮点数 */
20            php_printf("%f", Z_DVAL_P(value));
21            break;
22        case IS_STRING:
23            /* 如果是string型,则二进制安全的输出这个字符串 */
24            PHPWRITE(Z_STRVAL_P(value), Z_STRLEN_P(value));
25            break;
26        case IS_RESOURCE:
27            /* 如果是资源,则输出Resource #10 格式的东东 */
28            php_printf("Resource #%ld", Z_RESVAL_P(value)
PHP内核    2019-04-25 15:44:59    16    0    0

用户在PHP语言里定义的变量,我们能否在内核中获取到呢?答案当然是肯定的,下面我们就看如何通过zend_hash_find()函数来找到当前某个作用域下用户已经定义好的变量。zend_hash_find()函数是内核提供的操作HashTable的API之一,如果你没有接触过,可以先记住这么使用就可以了。

01{
02    zval **fooval;
03 
04    if (zend_hash_find(
05            EG(active_symbol_table), //这个参数是地址,如果我们操作全局作用域,则需要&EG(symbol_table)
06            "foo",
07            sizeof("foo"),
08            (void**)&fooval
09        ) == SUCCESS
10    )
11    {
12        php_printf("成功发现$foo!");
13    }
14    else
15    {
16        php_printf("当前作用域下无法发现$foo.");
17    }
18}

首先我们定义了一个指向指针的指针,然后通过zend_hash_find去EG(active_symbol_table)作用域下寻找名称为foo($foo)的变量,如果成功找到,此函数将返回SUCCESS。看完代码,你肯定有很多疑问。为什么还要进行sizeof("foo")运算,fooval明明是zval**型的,为什么转成void**的?而且为什么还要进行&fooval运算,fooval本身不就已经是指向指针的指针了吗?:-),该回答的问题确实很多,不要过于担心,让我们带着这些问题继续往下走。

首先要说明的是,内核定义HashTable这个结构,并不是单单用来储存PHP语言里的变量的,其它很多地方都在应用HashTable(这就是个神器)。一个HashTable有很多元素,在内核里叫做bucket。然而每个bucket的大小是固定的,所以如果我们想在bucket里存储任意数据时,最好的办法便是申请一块内存保存数据,然后在bucket里保存它的指针。以zval *foo为例,内核会先申请一块足够保存指针内存来保存foo,比如这块内存的地址是p,也就是p=&foo,并在bucket

6/9