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执行过程中的数据》上有1条评论

  1. skylar

    请问博主,EX(fbc)和EX(function_state).function有什么区别,我看do_fcall里面是把查得函数放到functio_stage里面啊。还有就是希望博主讲一讲EX(CVs)的事情,这块我实在是看不懂。。。

    回复

skylar进行回复 取消回复

电子邮件地址不会被公开。 必填项已用*标注


*

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>