深度理解PHP执行流程

时间:2024-04-02 17:38:48

一、前言

语言是人们进行沟通和交流的表达符号,每种语言都有专属于自己的符号,表达方式和规则。 就编程语言来说,它也是由特定的符号,特定的表达方式和规则组成。语言的作用是沟通,不管是自然语言,还是编程语言,它们的区别在于自然语言是人与人之间沟通的工具, 而编程语言是人与机器之间的沟通渠道。

   就PHP语言来说,它也是一组符合一定规则的约定的指令。 在编程人员将自己的想法以php语言实现后,通过PHP的虚拟机(确切的来说应该是PHP的语言引擎Zend)将这些PHP指令转变成C语言 (可以理解为更底层的一种指令集)指令,而c语言又会转变成汇编语言, 最后汇编语言将根据处理器的规则转变成机器码执行。这是一个更高层次抽象的不断具体化,不断细化的过程。

    从一种语言到另一种语言的转化称之为编译,这两种语言分别可以称之为源语言和目标语言。 这种编译过程通过发生在目标语言比源语言更低级(或者说更底层)。 语言转化的编译过程是由编译器来完成, 编码器通常被分为一系列的过程:词法分析、语法分析、语义分析、中间代码生成、代码优化、目标代码生成等。 前面几个阶段(词法分析、语法分析和语义分析)的作用是分析源程序,我们可以称之为编译器的前端。 后面的几个阶段(中间代码生成、代码优化和目标代码生成)的作用是构造目标程序,我们可以称之为编译器的后端。 一种语言被称为编译类语言,一般是由于在程序执行之前有一个翻译的过程, 其中关键点是有一个形式上完全不同的等价程序生成。 而PHP之所以被称为解释类语言,就是因为并没有这样的一个程序生成, 它生成的是中间代码Opcode,这只是PHP的一种内部数据结构

二、PHP的执行流程&opcode

            我们先来看看PHP代码的执行所经过的流程。

深度理解PHP执行流程


1. Scanning(Lexing),将PHP代码转换为语言片段(Tokens)

那什么是Lexing? 学过编译原理的同学都应该对编译原理中的词法分析步骤有所了解,Lex就是一个词法分析的依据表。 

对于PHP在开始使用的是Flex,之后改为re2c, MySQL的词法分析使用的Flex,除此之外还有作为UNIX系统标准词法分析器的Lex等。 这些工具都会读进一个代表词法分析器规则的输入字符串流,然后输出以C语言实做的词法分析器源代码。 这里我们只介绍PHP的现版词法分析器,re2c。 在源码目录下的Zend/zend_language_scanner.l 文件是re2c的规则文件, 如果需要修改该规则文件需要安装re2c才能重新编译,生成新的规则文件。Zend/zend_language_scanner.c会根据Zend/zend_language_scanner.l,来输入的 PHP代码进行词法分析,从而得到一个一个的“词”。

从PHP4.2开始提供了一个函数叫token_get_all,这个函数就可以将一段PHP代码 Scanning成Tokens;

我们用下面的代码使用token_get_all函数处理我们开头提到的PHP代码。

[php] view plain copy 深度理解PHP执行流程深度理解PHP执行流程
  1. <?php  
  2. echo "<pre>";  
  3. $phpcode = <<<PHPCODE  
  4. <?php  
  5.     echo "Hello World!";  
  6.     $a = 1 + 1;  
  7.     echo $a;  
  8. ?>  
  9. PHPCODE;  
  10. // $tokens = token_get_all($phpcontent);  
  11. // print_r($tokens);  
  12. $tokens = token_get_all($phpcode);   
  13. foreach ($tokens as $key => $token) {  
  14.     $tokens[$key][0] = token_name($token[0]);  
  15. }  
  16. print_r($tokens);  
  17. ?>  

    结果如图所示:

        

  1. Array  
  2. (  
  3.     [0] => Array  
  4.         (  
  5.             [0] => T_OPEN_TAG  
  6.             [1] =>  1  
  7.         )  
  8.   
  9.     [1] => Array  
  10.         (  
  11.             [0] => T_WHITESPACE  
  12.             [1] =>     
  13.             [2] => 2  
  14.         )  
  15.   
  16.     [2] => Array  
  17.         (  
  18.             [0] => T_ECHO  
  19.             [1] => echo  
  20.             [2] => 2  
  21.         )  
  22.   
  23.     [3] => Array  
  24.         (  
  25.             [0] => T_WHITESPACE  
  26.             [1] =>    
  27.             [2] => 2  
  28.         )  
  29.   
  30.     [4] => Array  
  31.         (  
  32.             [0] => T_CONSTANT_ENCAPSED_STRING  
  33.             [1] => "Hello World!"  
  34.             [2] => 2  
  35.         )  
  36.   
  37.     [5] =>   
  38.     [6] => Array  
  39.         (  
  40.             [0] => T_WHITESPACE  
  41.             [1] =>   
  42.        
  43.             [2] => 2  
  44.         )  
  45.   
  46.     [7] =>   
  47.     [8] => Array  
  48.         (  
  49.             [0] => T_WHITESPACE  
  50.             [1] =>    
  51.             [2] => 3  
  52.         )  
  53.   
  54.     [9] => Array  
  55.         (  
  56.             [0] => T_LNUMBER  
  57.             [1] => 1  
  58.             [2] => 3  
  59.         )  
  60.   
  61.     [10] => Array  
  62.         (  
  63.             [0] => T_WHITESPACE  
  64.             [1] =>    
  65.             [2] => 3  
  66.         )  
  67.   
  68.     [11] =>   
  69.     [12] => Array  
  70.         (  
  71.             [0] => T_WHITESPACE  
  72.             [1] =>    
  73.             [2] => 3  
  74.         )  
  75.   
  76.     [13] => Array  
  77.         (  
  78.             [0] => T_LNUMBER  
  79.             [1] => 1  
  80.             [2] => 3  
  81.         )  
  82.   
  83.     [14] =>   
  84.     [15] => Array  
  85.         (  
  86.             [0] => T_WHITESPACE  
  87.             [1] =>   
  88.       
  89.             [2] => 3  
  90.         )  
  91.   
  92.     [16] => Array  
  93.         (  
  94.             [0] => T_ECHO  
  95.             [1] => echo  
  96.             [2] => 4  
  97.         )  
  98.   
  99.     [17] => Array  
  100.         (  
  101.             [0] => T_WHITESPACE  
  102.             [1] =>    
  103.             [2] => 4  
  104.         )  
  105.   
  106.     [18] =>   
  107.     [19] => Array  
  108.         (  
  109.             [0] => T_WHITESPACE  
  110.             [1] =>   
  111.   
  112.             [2] => 4  
  113.         )  
  114.   
  115.     [20] => Array  
  116.         (  
  117.             [0] => T_CLOSE_TAG  
  118.             [1] => ?>  
  119.             [2] => 5  
  120.         )  
  121.   


原生:

     <?php
     echo <<<EOT
     My name is "$name". I am printing some $foo->foo.
     Now, I am printing some
{$foo->bar[1]}.
     This should print a capital 'A': \x41
     EOT;
    ?>

 This last example is tokenized as:
 T_ECHO
 echo
 T_WHITESPACE
 %20 (a space character)
 T_START_HEREDOC
  <<
 T_ENCAPSED_AND_WHITESPACE
  My name is "
T_VARIABLE
  $name
T_ENCAPSED_AND_WHITESPACE   
  ". I am printing some
T_VARIABLE   
  $foo
T_OBJECT_OPERATOR   
  ->
T_STRING   
  foo
T_ENCAPSED_AND_WHITESPACE   
  . Now, I am printing some
T_CURLY_OPEN   
  {
T_VARIABLE   
  $foo
T_OBJECT_OPERATOR   
  ->
T_STRING   
  bar
(terminal)
  [
T_LNUMBER   
  1
(terminal)
  ]
(terminal)
  }
T_ENCAPSED_AND_WHITESPACE   
  . This should print a capital 'A': \x41
T_END_HEREDOC
  EOT
(terminal)
  ;

析这个返回结果我们可以发现,源码中的字符串,字符,空格都会原样返回。

每个源代码中的字符,都会出现在相应的顺序处。

而其他的,比如标签,操作符,语句,都会被转换成一个包含部分的

1、Token ID解释器代号 (也就是在Zend内部的改Token的对应码,比如,T_ECHO,T_STRING)

2、源码中的原来的内容

3、该词在源码中是第几行


2. Parsing, 将Tokens转换成简单而有意义的表达式

接下来,就是Parsing阶段了,Parsing首先会丢弃Tokens Array中的多于的空格,

然后将剩余的Tokens转换成一个一个的简单的表达式

[php] view plain copy
 深度理解PHP执行流程深度理解PHP执行流程
  1. 1.echo a constant string  
  2. 2.add two numbers together  
  3. 3.store the result of the prior expression to a variable  
  4. 4.echo a variable  

Bison是一种通用目的的分析器生成器。它将LALR(1)上下文无关文法的描述转化成分析该文法的C程序。 使用它可以生成解释器,编译器,协议实现等多种程序。 Bison向上兼容Yacc,所有书写正确的Yacc语法都应该可以不加修改地在Bison下工作。 它不但与Yacc兼容还具有许多Yacc不具备的特性。

Bison分析器文件是定义了名为yyparse并且实现了某个语法的函数的C代码。 这个函数并不是一个可以完成所有的语法分析任务的C程序。 除此这外我们还必须提供额外的一些函数: 如词法分析器、分析器报告错误时调用的错误报告函数等等。 我们知道一个完整的C程序必须以名为main的函数开头,如果我们要生成一个可执行文件,并且要运行语法解析器, 那么我们就需要有main函数,并且在某个地方直接或间接调用yyparse,否则语法分析器永远都不会运行。

在PHP源码中,词法分析器的最终是调用re2c规则定义的lex_scan函数,而提供给Bison的函数则为zendlex。 而yyparse被zendparse代替。


3. Compilation, 将表达式编译成Opocdes


       之后就是Compilation阶段了,它会把Tokens编译成一个个op_array, 每个op_arrayd包含如下5个部分

在PHP实现内部,opcode由如下的结构体表如下:

[php] view plain copy
 深度理解PHP执行流程深度理解PHP执行流程
  1. struct _zend_op {  
  2. opcode_handler_t handler; // 执行该opcode时调用的处理函数  
  3. znode result;  
  4. znode op1;  
  5. znode op2;  
  6. ulong extended_value;  
  7. uint lineno;  
  8. zend_uchar opcode; // opcode代码  
  9. };  

和CPU的指令类似,有一个标示指令的opcode字段,以及这个opcode所操作的操作数。

PHP不像汇编那么底层, 在脚本实际执行的时候可能还需要其他更多的信息,extended_value字段就保存了这类信息。

 其中的result域则是保存该指令执行完成后的结果。

PHP脚本编译为opcode保存在op_array中,其内部存储的结构如下:

    

  1. struct _zend_op_array {  
  2.     /* Common elements */  
  3.     zend_uchar type;  
  4.     char *function_name; // 如果是用户定义的函数则,这里将保存函数的名字  
  5.     zend_class_entry *scope;  
  6.     zend_uint fn_flags;  
  7.     union _zend_function *prototype;  
  8.     zend_uint num_args;  
  9.     zend_uint required_num_args;  
  10.     zend_arg_info *arg_info;  
  11.     zend_bool pass_rest_by_reference;  
  12.     unsigned char return_reference;  
  13.     /* END of common elements */  
  14.     zend_bool done_pass_two;  
  15.     zend_uint *refcount;  
  16.     zend_op *opcodes; // opcode数组  
  17.     zend_uint last,size;  
  18.     zend_compiled_variable *vars;  
  19.     int last_var,size_var;  
  20.     // ...  

   
如上面的注释,opcodes保存在这里,在执行的时候由下面的execute函数执行:
  1. ZEND_API void execute(zend_op_array *op_array TSRMLS_DC)  
  2. {  
  3.     // ... 循环执行op_array中的opcode或者执行其他op_array中的opcode  

前面提到每条opcode都有一个opcode_handler_t的函数指针字段,用于执行该opcode。

PHP有三种方式来进行opcode的处理:CALL,SWITCH和GOTO。

PHP默认使用CALL的方式,也就是函数调用的方式, 由于opcode执行是每个PHP程序频繁需要进行的操作,

可以使用SWITCH或者GOTO的方式来分发, 通常GOTO的效率相对会高一些,

不过效率是否提高依赖于不同的CPU。

在我们上面的例子中,我们的PHP代码会被Parsing成:

[php] view plain copy
 深度理解PHP执行流程深度理解PHP执行流程
  1. * ZEND_ECHO     'Hello World%21'  
  2. * ZEND_ADD       ~0 1 1  
  3. * ZEND_ASSIGN  !0 ~0  
  4. * ZEND_ECHO     !0  
  5. * ZEND_RETURN  1  
 
你可能会问了,我们的$a去那里了?这个要介绍操作数了,每个操作数都是由以下俩个部分组成:
[php] view plain copy
 深度理解PHP执行流程深度理解PHP执行流程
  1. a)op_type : 为IS_CONST, IS_TMP_VAR, IS_VAR, IS_UNUSED, or IS_CV  
  2.    
  3. b)u,一个联合体,根据op_type的不同,分别用不同的类型保存了这个操作数的值(const)或者左值(var)  

而对于var来说,每个var也不一样
IS_TMP_VAR, 顾名思义,这个是一个临时变量,保存一些op_array的结果,以便接下来的op_array使用,
这种的操作数的u保存着一个指向变量表的一个句柄(整数),这种操作数一般用~开头。
比如~0,表示变量表的0号未知的临时变量
IS_VAR 这种就是我们一般意义上的变量了,他们以$开头表示
IS_CV 表示ZE2.1/PHP5.1以后的编译器使用的一种cache机制,
这种变量保存着被它引用的变量的地址,当一个变量第一次被引用的时候,就会被CV起来,
以后对这个变量的引用就不需要再次去查找active符号表了,CV变量以!开头表示。

这么看来,我们的$a被优化成!0了。

比如我们使用VLD来查看opcodes显示如下:
深度理解PHP执行流程

从上面这个我们看到,是不是和我们之前分析的一样呢。
如上为VLD输出的PHP代码生成的中间代码的信息,说明如下:
Branch analysis from position 这条信息多在分析数组时使用。
Return found 是否返回,这个基本上有都有。
filename 分析的文件名
function name 函数名,针对每个函数VLD都会生成一段如上的独立的信息,这里显示当前函数的名称
number of ops 生成的操作数
compiled vars 编译期间的变量,这些变量是在PHP5后添加的,它是一个缓存优化。
这样的变量在PHP源码中以IS_CV标记。
op list 生成的中间代码的变量列表

是不是和前面所说的一样呢。 

4. Execution,Zend引擎顺次执行Opcodes

最后一步,也就是Execution,Zend引擎 顺次执行Opcodes,每次一条,从而实现PHP脚本的功能,和机器指令运行相似。