标签归档:PHP内核

PHP执行过程中的数据

PHP脚本在内核中一般会经过词法解析,语法解析、编译生成中间代码,执行中间代码这样四个大的步骤。其中,第四个步骤,执行中间代码PHP内核默认情况下是通过zend/zend_vm_execute.h文件中的execute函数调用执行完成,对于所有的中间代码,默认实现是以按顺序执行,当遇到函数等情况时跳出去执行,执行完后再回到跳出的位置继续执行。

与过程相比,过程中的数据会更加重要,那么在执行过程中的核心数据结构有哪些呢? 在Zend/zend_vm_execute.h文件中的execute函数实现中,zend_execute_data类型的execute_data变量贯穿整个中间代码的执行过程, 其在调用时并没有直接使用execute_data,而是使用EX宏代替,其定义在Zend/zend_compile.h文件中,如下:

#define EX(element) execute_data.element

因此我们在execute函数或在opcode的实现函数中会看到EX(fbc),EX(object)等宏调用, 它们是调用函数局部变量execute_data的元素:execute_data.fbc和execute_data.object。 execute_data不仅仅只有fbc、object等元素,它包含了执行过程中的中间代码,上一次执行的函数,函数执行的当前作用域,类等信息。 其结构如下:

typedef struct _zend_execute_data zend_execute_data;
 
struct _zend_execute_data {
    struct _zend_op *opline;
    zend_function_state function_state;
    zend_function *fbc; /* Function Being Called */
    zend_class_entry *called_scope; 
    zend_op_array *op_array;  /* 当前执行的中间代码 */
    zval *object;
    union _temp_variable *Ts;
    zval ***CVs;
    HashTable *symbol_table; /* 符号表 */
    struct _zend_execute_data *prev_execute_data;   /* 前一条中间代码执行的环境*/
    zval *old_error_reporting;
    zend_bool nested;
    zval **original_return_value; /* */
    zend_class_entry *current_scope;
    zend_class_entry *current_called_scope;
    zval *current_this;
    zval *current_object;
    struct _zend_op *call_opline;
};

在前面的中间代码执行过程中有介绍:中间代码的执行最终是通过EX(opline)->handler(execute_data TSRMLS_CC)来调用最终的中间代码程序。 在这里会将主管中间代码执行的execute函数中初始化好的execture_data传递给执行程序。

zend_execute_data结构体部分字段说明如下:

  • opline字段:struct _zend_op类型,当前执行的中间代码
  • op_array字段: zend_op_array类型,当前执行的中间代码队列
  • fbc字段:zend_function类型,已调用的函数
    called_scope字段:zend_class_entry类型,当前调用对象作用域,常用操作是EX(called_scope) = Z_OBJCE_P(EX(object)), 即将刚刚调用的对象赋值给它。
  • symbol_table字段: 符号表,存放局部变量,这在前面的<< 第六节 变量的生命周期 » 变量的作用域 >>有过说明。 在execute_data初始时,EX(symbol_table) = EG(active_symbol_table);
  • prev_execute_data字段:前一条中间代码执行的中间数据,用于函数调用等操作的运行环境恢复。
    在execute函数中初始化时,会调用zend_vm_stack_alloc函数分配内存。 这是一个栈的分配操作,对于一段PHP代码的上下文环境,它存在于这样一个分配的空间作放置中间数据用,并作为栈顶元素。 当有其它上下文环境的切换(如函数调用),此时会有一个新的元素生成,上一个上下文环境会被新的元素压下去, 新的上下文环境所在的元素作为栈顶元素存在。

在zend_vm_stack_alloc函数中我们可以看到一些PHP内核中的优化。 比如在分配时,这里会存在一个最小分配单元,在zend_vm_stack_extend函数中, 分配的最小单位是ZEND_VM_STACK_PAGE_SIZE((64 * 1024) – 64),这样可以在一定范围内控制内存碎片的大小。 又比如判断栈元素是否为空,在PHP5.3.1之前版本(如5.3.0)是通过第四个元素elelments与top的位置比较来实现, 而从PHP5.3.1版本开始,struct _zend_vm_stack结构就没有第四个元素,直接通过在当前地址上增加整个结构体的长度与top的地址比较实现。 两个版本结构代码及比较代码如下:

// PHP5.3.0
struct _zend_vm_stack {
    void **top;
    void **end;
    zend_vm_stack prev;
    void *elements[1];
};
 
if (UNEXPECTED(EG(argument_stack)->top == EG(argument_stack)->elements)) {
}
 
//  PHP5.3.1
struct _zend_vm_stack {
    void **top;
    void **end;
    zend_vm_stack prev;
};
 
if (UNEXPECTED(EG(argument_stack)->top == ZEND_VM_STACK_ELEMETS(EG(argument_stack)))) {
}
 
#define ZEND_VM_STACK_ELEMETS(stack) \
((void**)(((char*)(stack)) + ZEND_MM_ALIGNED_SIZE(sizeof(struct _zend_vm_stack))))

当一个上下文环境结束其生命周期后,如果回收这段内存呢? 还是以函数为例,我们在前面的函数章节中<< 函数的返回 >>中我们知道每个函数都会有一个函数返回, 即使没有在函数的实现中定义,也会默认返回一个NULL。以ZEND_RETURN_SPEC_CONST_HANDLER实现为例, 在函数的返回最后都会调用一个函数zend_leave_helper_SPEC。

在zend_leave_helper_SPEC函数中,对于执行过程中的函数处理有几个关键点:

  • 上下文环境的切换:这里的关键代码是:EG(current_execute_data) = EX(prev_execute_data);。 EX(prev_execute_data)用于保留当前函数调用前的上下文环境,从而达到恢复和切换的目的。
  • 当前上下文环境所占用内存空间的释放:这里的关键代码是:zend_vm_stack_free(execute_data TSRMLS_CC);。 zend_vm_stack_free函数的实现存在于Zend/zend_execute.h文件,它的作用就是释放栈元素所占用的内存。
  • 返回到之前的中间代码执行路径中:这里的关键代码是:ZEND_VM_LEAVE();。 我们从zend_vm_execute.h文件的开始部分就知道ZEND_VM_LEAVE宏的效果是返回3。 在执行中间代码的while循环当中,当ret=3时,这个执行过程就会恢复之前上下文环境,继续执行。

更多内容请请移步TIPI项目

PHP内核中用户函数、内部函数和中间代码的转换

昨天和一朋友在邮件中讨论这样一个问题:zend_internal_function,zend_function,zend_op_array这三种结构是可以相互转化的,这三者的转化是如何进行的呢? 以此文,总结。

在函数调用的执行代码中我们会看到这样一些强制转换:

EX(function_state).function = (zend_function *) op_array;

或者:

EG(active_op_array) = (zend_op_array *) EX(function_state).function;

这种不同结构间的强制转换是如何进行的呢?

首先我们来看zend_function的结构,在Zend/zend_compile.h文件中,其定义如下:

typedef union _zend_function {
    zend_uchar type;    /* MUST be the first element of this struct! */

    struct {
        zend_uchar type;  /* never used */
        char *function_name;
        zend_class_entry *scope;
        zend_uint fn_flags;
        union _zend_function *prototype;
        zend_uint num_args;
        zend_uint required_num_args;
        zend_arg_info *arg_info;
        zend_bool pass_rest_by_reference;
        unsigned char return_reference;
    } common;

    zend_op_array op_array;
    zend_internal_function internal_function;
} zend_function;

这是一个联合体,我们来温习一下联合体的一些特性。 联合体的所有成员变量共享内存中的一块内存,在某个时刻只能有一个成员使用这块内存, 并且当使用某一个成员时,其仅能按照它的类型和内存大小修改对应的内存空间。 我们来看看一个例子:

#include <stdio.h>
#include <stdlib.h>

int main() {
    typedef  union _utype
    {
        int i;
        char ch[2];
    } utype; 

    utype a;

    a.i = 10;
    a.ch[0] = '1';
    a.ch[1] = '1';

    printf("a.i= %d a.ch=%s",a.i, a.ch);
    getchar();

    return (EXIT_SUCCESS);
}

程序输出:a.i= 12593 a.ch=11 当修改ch的值时,它会依据自己的规则覆盖i字段对应的内存空间。 ’1′对应的ASCII码值是49,二进制为00110001,当ch字段的两个元素都为’1′时,此时内存中存储的二进制为 00110001 00110001 转成十进制,其值为12593。

回过头来看zend_function的结构,它也是一个联合体,第一个字段为type, 在common中第一个字段也为type,并且其后面注释为/* Never used*/,此处的type字段的作用就是为第一个字段的type留下内存空间。并且不让其它字段干扰了第一个字段。 我们再看zend_op_array的结构:

struct _zend_op_array {
    /* Common elements */
    zend_uchar type;
    char *function_name;
    zend_class_entry *scope;
    zend_uint fn_flags;
    union _zend_function *prototype;
    zend_uint num_args;
    zend_uint required_num_args;
    zend_arg_info *arg_info;
    zend_bool pass_rest_by_reference;
    unsigned char return_reference;
    /* END of common elements */

    zend_bool done_pass_two;
    ....//  其它字段
}

这里的字段集和common的一样,于是在将zend_function转化成zend_op_array时并不会产生影响,这种转变是双向的。

再看zend_internal_function的结构:

typedef struct _zend_internal_function {
    /* Common elements */
    zend_uchar type;
    char * function_name;
    zend_class_entry *scope;
    zend_uint fn_flags;
    union _zend_function *prototype;
    zend_uint num_args;
    zend_uint required_num_args;
    zend_arg_info *arg_info;
    zend_bool pass_rest_by_reference;
    unsigned char return_reference;
    /* END of common elements */

    void (*handler)(INTERNAL_FUNCTION_PARAMETERS);
    struct _zend_module_entry *module;
} zend_internal_function;

同样存在公共元素,和common结构体一样,我们可以将zend_function结构强制转化成zend_internal_function结构,并且这种转变是双向的。

总的来说zend_internal_function,zend_function,zend_op_array这三种结构在一定程序上存在公共的元素, 于是这些元素以联合体的形式共享内存,并且在执行过程中对于一个函数,这三种结构对应的字段在值上都是一样的, 于是可以在一些结构间发生完美的强制类型转换。 可以转换的列表如下:

  • zend_function可以与zend_op_array互换
  • zend_function可以与zend_internal_function互换

但是一个zend_op_array结构转换成zend_function是不能再次转变成zend_internal_function结构的,反之亦然。

其实zend_function就是一个混合的数据结构,这种结构在一定程序上节省了内存空间。

PHP内核中的对象管理机制

在PHP中,变量存储在一个名叫ZVAL的容器中。它也是PHP实现弱语言的关键因素之一。 这个容器是一个标记类型和记录所有PHP实现的类型的集合体。而对象作为其存储类型的一种, 以type=IS_OBJECT为标记,以zend_object_value结构体为值。但是zend_object_value的结构体仅有两个字段handle和handlers。 而这两个字段就是今天我们所要说的对象管理机制的关键点。

handle字段是zend_object_handle类型,而zend_object_handle仅仅是unsigned int的一个别名。 这个字段是干嘛用的呢?它是一个索引,是一个对象存储列表的索引。这个对象存储列表是PHP内核中对象的存储地, 或者我们可以称其为“对象池”。

在PHP的请求初始化阶段,PHP会初始化这个对象池,预先分配 1024 个存储对象的空间。 当我们使用 new 关键字创建一个对象时,PHP会将这个对象放入到对象池中,handle字段作为其索引返回。 如果总的对象个数小于1024(或现有列表长度的最大值),则handle会返回最后的一个值, 如果总的对象个数大于1024(或现有列表长度的最大值),则将列表的长度左移一位,将之前的top值作为handle字段返回。 当我们将对象的引用计数减小时,PHP最终会调用对象操作API中的引用计数减少操作函数,当对象的引用计数小于1时会执行垃圾回收机制。

PHP的这个对象存储机制,有点类似于在表设计时将一对多关系中的多的一边独立成一个表存储, 这样的设计不仅仅是低耦合的,而且在对象利用,节省内存等方面有一定的优化。

handlers字段是zend_object_handlers类型,这个类型是一个结构体,包括对象的所有处理函数。默认情况下,对象创建时使用标准处理函数。 对象的变量调用,引用计数处理,克隆,函数调用等等操作都包括在这个结构体中,这相当于是对象处理的统一接口。 对于不同的需求,也可以定制这些处理函数的实现,这又相当于是一个默认值,你可以选择非默认的自定义的处理函数。 如果从面向对象的角度思考这个设计,这应该是一个类似于门面模式 ,或者说是面向接口的编程原则。

对象池的相关操作的实现在 Zend/zend_object_API.c文件,对象的操作的标准实现基本上都在 Zend/zend_object_handlers.c文件。

关于对象的前前后后在即将发布的 TIPI 第五章类和面向对象中有详细说明。