php内核探索之zend_execute的具体执行过程

简介:

解释器引擎最终执行op的函数是zend_execute,实际上zend_execute是一个函数指针,在引擎初始化的时候zend_execute默认指向了execute,这个execute定义在{PHPSRC}/Zend/zend_vm_execute.h:


ZEND_API void execute(zend_op_array *op_array TSRMLS_DC)  
{  
    zend_execute_data *execute_data;  
    zend_bool nested = 0;  
    zend_bool original_in_execution = EG(in_execution);  
   
   
    if (EG(exception)) {  
        return;  
    }  
   
    EG(in_execution) = 1;  
   
zend_vm_enter:  
    /* Initialize execute_data */ 
    execute_data = (zend_execute_data *)zend_vm_stack_alloc(  
        ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)) +  
        ZEND_MM_ALIGNED_SIZE(sizeof(zval**) * op_array->last_var * (EG(active_symbol_table) ? 1 : 2)) +  
        ZEND_MM_ALIGNED_SIZE(sizeof(temp_variable)) * op_array->T TSRMLS_CC);  
   
    EX(CVs) = (zval***)((char*)execute_data + ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)));  
    memset(EX(CVs), 0, sizeof(zval**) * op_array->last_var);  
    EX(Ts) = (temp_variable *)(((char*)EX(CVs)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval**) * op_array->last_var * (EG(active_symbol_table) ? 1 : 2)));  
    EX(fbc) = NULL;  
    EX(called_scope) = NULL;  
    EX(object) = NULL;  
    EX(old_error_reporting) = NULL;  
    EX(op_array) = op_array;  
    EX(symbol_table) = EG(active_symbol_table);  
    EX(prev_execute_data) = EG(current_execute_data);  
    EG(current_execute_data) = execute_data;  
    EX(nested) = nested;  
    nested = 1;  
   
    if (op_array->start_op) {  
        ZEND_VM_SET_OPCODE(op_array->start_op);  
    } else {  
        ZEND_VM_SET_OPCODE(op_array->opcodes);  
    }  
   
    if (op_array->this_var != -1 && EG(This)) {  
        Z_ADDREF_P(EG(This)); /* For $this pointer */ 
        if (!EG(active_symbol_table)) {  
            EX(CVs)[op_array->this_var] = (zval**)EX(CVs) + (op_array->last_var + op_array->this_var);  
            *EX(CVs)[op_array->this_var] = EG(This);  
        } else {  
            if (zend_hash_add(EG(active_symbol_table), "this", sizeof("this"), &EG(This), sizeof(zval *), (void**)&EX(CVs)[op_array->this_var])==FAILURE) {  
                Z_DELREF_P(EG(This));  
            }  
        }  
    }  
   
    EG(opline_ptr) = &EX(opline);  
   
    EX(function_state).function = (zend_function *) op_array;  
    EX(function_state).arguments = NULL;  
       
    while (1) {  
        int ret;  
#ifdef ZEND_WIN32  
        if (EG(timed_out)) {  
            zend_timeout(0);  
        }  
#endif  
   
        if ((ret = EX(opline)->handler(execute_data TSRMLS_CC)) > 0) {  
            switch (ret) {  
                case 1:  
                    EG(in_execution) = original_in_execution;  
                    return;  
                case 2:  
                    op_array = EG(active_op_array);  
                    goto zend_vm_enter;  
                case 3:  
                    execute_data = EG(current_execute_data);  
                default:  
                    break;  
            }  
        }  
   
    }  
    zend_error_noreturn(E_ERROR, "Arrived at end of main loop which shouldn't happen");  
}


zend_op_array简介
此类型的定义在{PHPSRC}/Zend/zend_compile.h:


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;  
   
    zend_uint *refcount;  
   
    zend_op *opcodes;  
    zend_uint last, size;  
   
    zend_compiled_variable *vars;  
    int last_var, size_var;  
   
    zend_uint T;  
   
    zend_brk_cont_element *brk_cont_array;  
    int last_brk_cont;  
    int current_brk_cont;  
   
    zend_try_catch_element *try_catch_array;  
    int last_try_catch;  
   
    /* static variables support */ 
    HashTable *static_variables;  
   
    zend_op *start_op;  
    int backpatch_count;  
   
    zend_uint this_var;  
   
    char *filename;  
    zend_uint line_start;  
    zend_uint line_end;  
    char *doc_comment;  
    zend_uint doc_comment_len;  
    zend_uint early_binding; /* the linked list of delayed declarations */ 
   
    void *reserved[ZEND_MAX_RESERVED_RESOURCES];  
};  
   
typedef struct _zend_op_array zend_op_array;  

此结构比较复杂,我们目前只介绍最基本的几个字段。
1.type:
op_array的类型,首先需要说明的是,一段PHP代码被编译之后,虽然返回的是一个zend_op_array指针,但是实际上生成的zend_op_array结构可能不止一个,通过这个结构中的一些字段,例如function_name ,num_args等你也许会发现这个zend_op_array结构似乎能和函数产生一定的联系,确实如此,用户自定义的函数,以及用户定义的类的方法,都是一个zend_op_array结构,这些zend_op_array结构在编译过程中被保存在某些地方,例如用户自定义的函数被保存进了GLOBAL_FUNCTION_TABLE,这个是全局函数符号表,通过函数名可以在此表中检索到函数体。那么编译后返回的那个zend_op_array指针是什么呢,其实编译后返回的zend_op_array是执行的一个入口,也可以认为它是最外层,即不在任何函数体内的全局代码组成的op_array。然而全局代码,用户自定义函数,用户自定义的方法都拥有相同的type值:2 。type可取值的宏定义为:


#define ZEND_INTERNAL_FUNCTION              1  
#define ZEND_USER_FUNCTION                  2  
#define ZEND_OVERLOADED_FUNCTION            3  
#define ZEND_EVAL_CODE                      4  
#define ZEND_OVERLOADED_FUNCTION_TEMPORARY  5  

可以看到全局代码,用户函数,用户方法都对应的是ZEND_USER_FUNCTION,这个也是最常见的type了,其中ZEND_EVAL_CODE对应的是eval函数中的PHP代码,所以我们可以想到,eval函数参数中的PHP代码也会被编译成单独的zend_op_array。
2.function_name
如果op_array是由用户定义的函数或则方法编译而生成,那么此字段对应函数的名字,如果是全局代码或则是eval部分的代码,那么此字段为控制。
3.opcodes
这个字段类型为zend_op *,因此这是一个zend_op的数组,这个数组保存的就是此编译过程中生成的op,如果不了解zend_op,可以看看之前的文章 OPcode简介 , 这个字段是最重要的部分了,zend_execute最终就是执行这里保存的op。
现在基本对参数op_array有了一定的了解,那么我们就开始进入到execute中。
执行过程详解
execute函数开始的时候是一些基础变量的申明,其中zend_execute_data *execute_data;是执行期的数据结构,此变量在进行一定的初始化之后将会被传递给每个op的handler函数作为参数,op在执行过程中随时有可能改变execute_data中的内容。

第14行zend_vm_enter 这个跳转标签是作为虚拟机执行的入口,当op中涉及到函数调用的时候,就有可能会跳转到这里来执行函数体。
第16行到第19行为execute_data分配空间
第21行到第32行主要是对execute_data进行一些初始化,以及保存现场工作,要保存现场是因为在进入函数调用的时候,需要保存当前一些运行期间的数据,在函数调用结束之后再进行还原,可以想象为操作系统中进程调度,当进程在调出的时候需要保存寄存器等上下文环境,而当进程被调入的时候再取出来继续执行。
第41行到第51行主要是在当前动态符号表中加入$this变量,这个是在调用对象的方法时才有必要进行。
第58行开始的while无限循环就是开始执行op_array中的opcodes了,在第66行中调用当前执行的op的handler:


EX(opline)->handler(execute_data TSRMLS_CC))

然后如果handler的返回值小于0则循环继续,如果大于0则进入一个switch结构:

当返回值为1时:execute函数将返回,执行也就结束了。
当返回值为2时:op_array被重新设置,并跳转到zend_vm_enter ,这个一般是函数调用或则执行eval函数中的代码,将在新的上下文执行相关函数的op_array
当返回值为3时:循环体继续继续执行,当然再继续执行之前,EX(opline)已经往后移了一位(可能多位),也就是已经指向了后面一个新的opline,于是继续执行新的opline
当返回其他值时:结束循环,报错,结束应该用return,也就是返回1
在op的handler中返回特定的值都被定义成了宏,例如{PHPSRC}/Zend/zend_execute.c中定义的:

然后如果handler的返回值小于0则循环继续,如果大于0则进入一个switch结构:

当返回值为1时:execute函数将返回,执行也就结束了。
当返回值为2时:op_array被重新设置,并跳转到zend_vm_enter ,这个一般是函数调用或则执行eval函数中的代码,将在新的上下文执行相关函数的op_array
当返回值为3时:循环体继续继续执行,当然再继续执行之前,EX(opline)已经往后移了一位(可能多位),也就是已经指向了后面一个新的opline,于是继续执行新的opline
当返回其他值时:结束循环,报错,结束应该用return,也就是返回1
在op的handler中返回特定的值都被定义成了宏,例如{PHPSRC}/Zend/zend_execute.c中定义的:


#define ZEND_VM_NEXT_OPCODE() /  
    CHECK_SYMBOL_TABLES() /  
    EX(opline)++; /  
    ZEND_VM_CONTINUE()  
   
#define ZEND_VM_SET_OPCODE(new_op) /  
    CHECK_SYMBOL_TABLES() /  
    EX(opline) = new_op  
   
#define ZEND_VM_JMP(new_op) /  
    CHECK_SYMBOL_TABLES() /  
    if (EXPECTED(!EG(exception))) { /  
        EX(opline) = new_op; /  
    } /  
    ZEND_VM_CONTINUE()  
   
#define ZEND_VM_INC_OPCODE() /  
    EX(opline)++

以及在{PHPSRC}/Zend/zend_vm_execute.c中定义的:


#define ZEND_VM_CONTINUE()   return 0  
#define ZEND_VM_RETURN()     return 1  
#define ZEND_VM_ENTER()      return 2  
#define ZEND_VM_LEAVE()      return 3  
#define ZEND_VM_DISPATCH(opcode, opline) return zend_vm_get_opcode_handler(opcode, opline

简单介绍功能
ZEND_VM_NEXT_OPCODE():移动到下一条op,返回0,不进入switch,循环继续(这个是最常用到的)
ZEND_VM_SET_OPCODE(new_op):当前opline设置成new_op
ZEND_VM_JMP(new_op) :当前opline设置成new_op,返回0,不进入switch,循环继续
ZEND_VM_INC_OPCODE():仅仅移动到下一条op
执行环境的切换
在前面的内容已经提到,用户自定义函数,类方法,eval的代码都会编译成单独的op_array,那么当进行函数调用等操作时,必然涉及到调用前的op_array执行环境和新的函数的op_array执行环境的切换,这一段我们将以调用用户自定义函数来介绍整个切换过程如何进行。
介绍此过程前必须了解执行环境的相关数据结构,涉及到执行环境的数据结构主要有两个:
1. 执行期全局变量结构
相关的定义在{PHPSRC}/Zend/zend_globals_macros.h:


/* Executor */ 
#ifdef ZTS  
# define EG(v) TSRMG(executor_globals_id, zend_executor_globals *, v)  
#else  
# define EG(v) (executor_globals.v)  
extern ZEND_API zend_executor_globals executor_globals;  
#endif 

这里是一个条件编译,ZTS表示线程安全启用,为了简化,我们这里以非线程安全模式的情况下来介绍,那么执行期的全局变量就是executor_globals,其类型为zend_executor_globals, zend_executor_globals的定义在{PHPSRC}/Zend/zend_globals.h,结构比较庞大,这里包含了整个执行期需要用到的各种变量,无论是哪个op_array在执行,都共用这一个全局变量,在执行过程中,此结构中的一些成员可能会改变,比如当前执行的op_array字段active_op_array,动态符号表字段active_symbol_table可能会根据不同的op_array而改变,This指针会根据在不同的对象环境而改变。
另外还定义了一个EG宏来取此变量中的字段值,此宏是针对线程安全和非线程安全模式的一个封装。
2.每个op_array自身的执行数据
针对每一个op_array,都会有自己执行期的一些数据,在函数execute开始的时候我们能看到zend_vm_enter跳转标签下面就会初始一个局部变量execute_data,所以我们每次切换到新的op_array的时候,都会为新的op_array建立一个execute_data变量,此变量的类型为zend_execute_data的指针,相关定义在{PHPSRC}/Zend/zend_compile.h:


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宏来取其中的值:#define EX(element) execute_data->element
这里只简单介绍其中两个字段:
opline: 当前正在执行的op。
prev_execute_data: op_array环境切换的时候,这个字段用来保存切换前的op_array,此字段非常重要,他能将每个op_array的execute_data按照调用的先后顺序连接成一个单链表,每当一个op_array执行结束要还原到调用前op_array的时候,就通过当前的execute_data中的prev_execute_data字段来得到调用前的执行器数据。
在executor_globals中的字段current_execute_data就是指向当前正在执行的op_array的execute_data。
再正式介绍之前还需要简单的介绍一下用户自定义函数的调用过程,详细的过程以后再函数章节中专门介绍,这里简单的说明一下:
在调用函数的时候,比如test()函数,会先在全局函数符号表中根据test来搜索相关的函数体,如果搜索不到则会报错函数没有定义,找到test的函数体之后,取得test函数的op_array,然后跳转到execute中的goto标签:zend_vm_enter,于是就进入到了test函数的执行环境。
下面我们将以一段简单的代码来介绍执行环境切换过程,例子代码:


<?php  
$a = 123;  
   
test();  
   
function test()  
{  
    return 1;  
}  
?>  

这段代码非常简单,这样方便我们介绍原理,复杂的代码读者可以举一反三。此代码编译之后会生成两个op_array,一个是全局代码的op_array,另外一个是test函数的op_array,其中全局代码中会通过函数调用进入到test函数的执行环境,执行结束之后,会返回到全局代码,然后代码结束。
下面我们分几个阶段来介绍这段代码的过程,然后从中可以知道执行环境切换的方法。
1. 进入execute函数,开始执行op_array ,这个op_array就是全局代码的op_array,我们暂时称其为op_array1
首先在execute中为op_array1建立了一个execute_data数据,我们暂时命名为execute_data1,然后进行相关的初始化操作,其中比较重要的是:


EX(op_array) = op_array; // 设置op_array字段为当前执行的op_array,也就是全局代码的op_array1
EX(prev_execute_data) = EG(current_execute_data);//将全局执行数据中保存的当前op_array执行数

2. 在op_array1执行到test函数调用的的时候,首先从全局函数符号表中找到test的函数体,将函数体保存在execute_data1的function_state字段,然后从函数体中取到test的op_array,我们这里用op_array2来表示,并将op_array2赋值给EG(active_op_array):


EG(active_op_array) = &EX(function_state).function->op_array;

于是执行期全局变量的动态op_array字段指向了函数test的op_array,然后用调用ZEND_VM_ENTER();这个时候会先回到execute函数中的switch结构,并且满足以下case


case 2:
     op_array = EG(active_op_array);
     goto zend_vm_enter;

EG(active_op_array)之前已经被我们设置为test函数的op_array2,于是在函数execute中,op_array变量就指向了test的op_array2,然后跳转到zend_vm_enter。
3. 跳转到zend_vm_enter之后其实又回到了类似1中的步骤,此时为test的op_array2建立了它的执行数据execute_data,我们这里用execute_data2来表示。跟1中有些不同的是EX(prev_execute_data) = EG(current_execute_data);这个时候current_execute_data = execute_data1,也就是全局代码的执行执行期数据,然后EG(current_execute_data) = execute_data;这样current_execute_data就等于test的执行期数据execute_data2了,同时全局代码的execute_data1被保存在execute_data2的prev_execute_data字段。这个时候进行环境的切换已经完成,于是开始执行test函数。
4. test函数执行完之后就要返回到调用前的执行环境了,也就是全局代码执行环境,此阶段最重要的一个操作就是EG(current_execute_data) = EX(prev_execute_data); 在3中EX(prev_execute_data)已经设置成了全局代码的execute_data1,所以这样当前执行数据就变成了全局代码的执行数据,这样就成功的从函数test执行环境返回到了全局代码执行环境
这样,执行环境的切换过程就完成了,对于深层次的函数调用,原理一样,执行数据execute_data组成的单链表会更长。 


目录
相关文章
|
6月前
|
存储 缓存 自然语言处理
深入PHP内核:理解Opcode缓存对性能的影响
【4月更文挑战第25天】 在提升PHP应用性能的众多策略中,Opcode缓存技术因其显著的效果和较低的复杂度而备受开发者青睐。本文将深入探讨Opcode缓存机制,解析其对PHP执行效率的提升原理,并通过实验数据展示启用Opcode缓存前后的性能差异。我们还将讨论几种流行的Opcode缓存工具,如APC、OpCache与APCu,并评估它们的优劣及适用场景,帮助开发者根据不同的项目需求做出合适的选择。通过本文,读者不仅能够了解Opcode缓存的工作原理,还能学会如何在实际项目中应用这一技术以优化PHP应用程序的性能。
|
6月前
|
缓存 自然语言处理 PHP
深入PHP内核:理解OPcode和执行生命周期
在PHP的执行过程中,源代码经过词法分析、语法分析等一系列复杂的步骤后,最终会编译成一种叫做“操作码”(OPcode)的中间代码。本文将深入探讨PHP的操作码(OPcode)是什么,它是如何生成的,以及它在整个PHP脚本执行周期中扮演的角色。我们还将了解不同的OPcode优化技术,以及它们如何影响PHP应用程序的性能。通过本文,读者将对PHP的内部工作原理有更深刻的理解,为进一步的性能调优打下坚实的基础。
|
6月前
|
数据采集 前端开发 PHP
深入PHP内核:探索高性能异步编程
【2月更文挑战第24天】在现代Web开发中,提供快速响应和高并发处理能力是至关重要的。传统的同步阻塞模型已无法满足现代互联网服务的需求。本文将深入探讨PHP语言中的异步编程概念,介绍其原理、实现方式以及如何通过异步编程提升应用性能。我们将剖析PHP的多线程扩展,如pthreads,以及事件驱动编程模型,重点关注它们在处理大量并发连接时的优势和潜在问题。此外,文中还将展示如何使用Swoole等高级框架来构建高效的异步应用。最后,我们将讨论异步编程在实际应用中的挑战与未来发展趋势。
|
6月前
|
存储 缓存 自然语言处理
深入PHP内核:理解Opcode缓存与性能优化
【5月更文挑战第14天】 在动态语言的世界里,PHP一直因其高性能的执行效率和广泛的社区支持而备受青睐。随着Web应用的复杂性增加,对性能的要求也越来越高。本文将探讨PHP的Opcode缓存机制,解析其对性能提升的贡献,并展示如何通过配置和使用不同的Opcode缓存方案来进一步优化PHP应用的性能。我们将深入到PHP的核心,了解Opcode是如何生成的,以及它如何影响最终的执行效率。
|
6月前
|
存储 缓存 PHP
深入PHP内核:理解Zend Engine与Opcode缓存
【5月更文挑战第30天】 在PHP的开发世界中,性能优化是一个永恒的话题。随着现代Web应用的复杂性日益增加,仅仅依靠代码层面的优化已经远远不够。本文将深入探讨PHP的执行心脏——Zend Engine,以及如何通过Opcode缓存机制提升PHP应用的执行效率。我们将透过对Zend Engine工作原理的分析,了解Opcode缓存的实现原理,并通过实例来展示其对性能提升的显著影响。
|
6月前
|
存储 缓存 自然语言处理
深入PHP内核:理解OPcache的工作原理与优化实践
【5月更文挑战第6天】 在现代Web开发中,提升性能和响应速度是持续追求的目标。PHP作为一种广泛使用的服务端脚本语言,其执行效率至关重要。本文将深入探索PHP的OPcache(优化器缓存)组件,解析其如何改善PHP的性能表现。通过剖析OPcache的工作机制,我们将讨论有效的配置策略以及实践中的最佳优化方法,旨在帮助开发者充分理解并利用OPcache来提升应用性能。
|
6月前
|
数据库连接 PHP
深入PHP内核:理解Zend Engine和PHP生命周期
【5月更文挑战第4天】 在本文中,我们将探讨PHP的核心——Zend Engine。我们会详细解释Zend Engine的工作原理,以及它如何驱动PHP代码的执行。此外,我们还将深入讨论PHP生命周期的各个阶段,包括从请求开始到输出结果的整个过程。这篇文章将为你提供一个深入的理解,关于PHP如何处理你的代码,以及它在背后是如何运作的。
153 3
|
6月前
|
存储 缓存 自然语言处理
深入PHP内核:探索Opcode缓存机制
【5月更文挑战第1天】 在动态语言如PHP的执行过程中,每次脚本被请求时都需要经过一系列复杂的解析和编译步骤。为了优化这一过程并提高性能,PHP引入了Opcode缓存机制。本文将详细探讨Opcode的概念、作用以及它如何显著提升PHP应用的执行效率。我们将从缓存原理出发,分析几种常见的Opcode缓存工具,并通过实例说明如何在实际项目中实现和优化缓存策略。
|
6月前
|
存储 缓存 自然语言处理
深入PHP内核:探索Opcode缓存对性能的影响
【4月更文挑战第30天】 在动态内容生成的Web开发领域,PHP一直是广受欢迎的脚本语言之一。然而,由于其每次请求都需要经过解释执行的特性,性能上往往受到质疑。随着现代PHP引擎如Zend和Facebook的HipHop Virtual Bytecode (HPHPc)的出现,引入了Opcode(操作码)缓存机制来提升PHP的性能。本文将深入探讨Opcode缓存技术是如何工作的,以及它对PHP应用程序性能的具体影响。我们将通过实验数据与分析,揭示Opcode缓存在不同场景下的优势和局限性,并提出优化策略。
|
6月前
|
存储 缓存 自然语言处理
深入PHP内核:Opcode缓存与性能优化
【5月更文挑战第31天】 在提升PHP应用性能的众多策略中,Opcode缓存技术以其显著的性能提升效果而广受关注。本文旨在深入探讨PHP的Opcode缓存机制,解析其对性能提升的影响,并讨论如何通过配置和优化实践来充分利用Opcode缓存。文章将首先介绍Opcode的概念及其在PHP执行过程中的作用,然后分析几种流行的Opcode缓存解决方案,最后提供针对性的优化建议,帮助开发者实现高效的PHP应用。