ホームページ >バックエンド開発 >PHPチュートリアル >PHPコードの実行プロセスを深く理解する

PHPコードの実行プロセスを深く理解する

WBOY
WBOYオリジナル
2016-06-20 12:31:141005ブラウズ

1. はじめに

言語は、人々がコミュニケーションや伝達に使用する表現記号です。それぞれの言語には独自の記号、表現、ルールがあります。 プログラミング言語に関しても、特定の記号、特定の式、規則で構成されています。自然言語であってもプログラミング言語であっても、言語の機能はコミュニケーションです。それらの違いは、自然言語は人間間のコミュニケーションのツールであるのに対し、プログラミング言語は人間と機械の間のコミュニケーションチャネルであるということです。

PHP 言語に関する限り、それは特定のルールに準拠した合意された命令のセットでもあります。 プログラマーがアイデアを PHP 言語で実装した後、これらの PHP 命令は、PHP の仮想マシン (正確には、PHP の言語エンジン Zend である必要があります) 命令を通じて C 言語 (低レベルの命令セットとして理解できます) に変換されます。 、そしてC言語はアセンブリ言語に変換され、最後にアセンブリ言語はプロセッサの規則に従って実行されるマシンコードに変換されます。これは、より高いレベルの抽象化を継続的に具体化し、洗練させるプロセスです。

ある言語から別の言語への変換はコンパイルと呼ばれ、2 つの言語はそれぞれソース言語とターゲット言語と呼ばれます。 このコンパイル プロセスは、ターゲット言語がソース言語よりも低レベル (または低レベル) の場合に発生します。 言語変換のコンパイルプロセスは、通常、字句解析、構文解析、意味解析、中間コード生成、コード最適化、ターゲットコード生成などの一連のプロセスに分割されます。 前段階 (字句解析、構文解析、意味解析) の機能は、コンパイラのフロントエンドと呼ぶことができるソース プログラムを解析することです。 次の段階 (中間コード生成、コード最適化、ターゲット コード生成) の機能は、コンパイラーのバックエンドと呼ぶことができるターゲット プログラムを構築することです。 言語をコンパイル言語と呼ぶのは、プログラムを実行する前に翻訳プロセスが存在するためです。重要な点は、全く異なる形式の同等のプログラムが生成されることです。 PHP がインタープリタ型言語と呼ばれる理由は、そのようなプログラム生成がないためです。生成されるのは、PHP の内部データ構造である中間コード Opcode だけです。

2. PHP コードの実行処理

例えば、簡単なプログラムを書いてみましょう

<!--?php    echo"Hello World!";    $a =1+1;    echo $a;?-->

这个简单的程序他执行过程是怎样的呢?其实,执行过程也正如我们前面所说分为4个步骤。(这里只是指PHP语言引擎Zend执行过程,不包含Web服务器的执行过程。)

1.Scanning(Lexing) ,将PHP代码转换为语言片段(Tokens)2.Parsing, 将Tokens转换成简单而有意义的表达式3.Compilation, 将表达式编译成Opocdes4.Execution, 顺次执行Opcodes,每次一条,从而实现PHP脚本的功能。

注1:Opcode是一种PHP脚本编译后的中间语言,就像Java的ByteCode,或者.NET的MSL

注2:现在有的Cache比如APC,可以使得PHP缓存住Opcodes,这样,每次有请求来临的时候,就不需要重复执行前面3步,从而能大幅的提高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代码。

<!--?phpecho"<pre class="brush:java;"-->";$phpcode = <<<phpcode <?php=""echo="""hello="" world!";=""$a="1"+=""1;=""$a;=""?="">PHPCODE;// $tokens = token_get_all($phpcontent);// print_r($tokens);$tokens = token_get_all($phpcode);foreach ($tokens as $key => $token) {    $tokens[$key][0] = token_name($token[0]);}print_r($tokens);?>

注:为了便于理解和查看,我使用token_name函数将解析器代号修改成了符号名称说明。

如果有的童鞋想要看原始的,可以将上面代码中的第10,11行代码注释去掉。

解释器代号列表详见:http://www.php.net/manual/zh/tokens.php

得到的结果如下:

Array(    [0] => Array        (            [0] => T_OPEN_TAG            [1] => 1        )     [1] => Array        (            [0] => T_WHITESPACE            [1] =>              [2] =>2        )     [2] => Array        (            [0] => T_ECHO            [1] => echo            [2] =>2        )     [3] => Array        (            [0] => T_WHITESPACE            [1] =>             [2] =>2        )     [4] => Array        (            [0] => T_CONSTANT_ENCAPSED_STRING            [1] =>"Hello World!"            [2] =>2        )     [5] =>    [6] => Array        (            [0] => T_WHITESPACE            [1] =>                  [2] =>2        )     [7] =>    [8] => Array        (            [0] => T_WHITESPACE            [1] =>             [2] =>3        )     [9] => Array        (            [0] => T_LNUMBER            [1] =>1            [2] =>3        )     [10] => Array        (            [0] => T_WHITESPACE            [1] =>             [2] =>3        )     [11] =>    [12] => Array        (            [0] => T_WHITESPACE            [1] =>             [2] =>3        )     [13] => Array        (            [0] => T_LNUMBER            [1] =>1            [2] =>3        )     [14] =>    [15] => Array        (            [0] => T_WHITESPACE            [1] =>                 [2] =>3        )     [16] => Array        (            [0] => T_ECHO            [1] => echo            [2] =>4        )     [17] => Array        (            [0] => T_WHITESPACE            [1] =>             [2] =>4        )     [18] =>    [19] => Array        (            [0] => T_WHITESPACE            [1] =>             [2] =>4        )     [20] => Array        (            [0] => T_CLOSE_TAG            [1] => ?>            [2] =>5        ) )

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

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

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

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

2、源码中的原来的内容

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

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

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

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

?

1

2

3

4

1.echo a constant string

2.add two numbers together

3.store the result of the prior expression to a variable

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由如下的结构体表如下:

struct _zend_op {opcode_handler_t handler;// 执行该opcode时调用的处理函数znode result;znode op1;znode op2;ulong extended_value;uint lineno;zend_uchar opcode;// opcode代码};

CPU 命令と同様に、命令を示すオペコード フィールドと、このオペコードによって操作されるオペランドがあります。

PHP はアセンブリほど低レベルではありません。この種の情報は、スクリプトが実際に実行されるときに必要になる場合があります。

結果フィールドには、命令の実行後の結果が保存されます。

PHP スクリプトは opcode にコンパイルされ、op_array に保存されます。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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;

    unsignedcharreturn_reference;

    /* END of common elements */

    zend_bool done_pass_two;

    zend_uint *refcount;

    zend_op *opcodes;// opcode数组

    zend_uint last,size;

    zend_compiled_variable *vars;

    intlast_var,size_var;

    // ...

}

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
struct _zend_op_array { /* 共通要素 */ zend_uchar type; char*function_name;/ / はいの場合、ユーザー定義関数の場合、関数の名前がここに保存されます。 zend_uint fn_flags; zend_uint required_num_args; zend_arg_info *arg_info; zend_bool pass_rest_by_reference; unsignedcharreturn_reference; > /* 共通要素の終了 * / zend_bool doned_pa​​ss_two; > zend_uint *refcount; zend_op *opcodes;// オペコード配列 zend_uint last, size; zend_compiled_variable *vars; // ... }

前述のように、オペコードはここに保存され、実行中に次の実行関数によって実行されます:

1

2

3

4

ZEND_APIvoidexecute(zend_op_array *op_array TSRMLS_DC)

{

    // ... 循环执行op_array中的opcode或者执行其他op_array中的opcode

}

1 2

3

4

ZEND_APIvoidexecute(zend_op_array *op_array TSRMLS_DC)

{


// ... ループ内の op_array のオペコードを実行するか、他の op_array のオペコードを実行します

}

td>

1

2

3

4

5

* ZEND_ECHO    'Hello World%21'

* ZEND_ADD       ~011

* ZEND_ASSIGN  !0~0

* ZEND_ECHO     !0

* ZEND_RETURN 1

前述したように、各オペコードには、オペコードの実行に使用される opcode_handler_t の関数ポインタ フィールドがあります。

PHP には、CALL、SWITCH、GOTO という 3 つの方法でオペコードを処理できます。

PHP はデフォルトで CALL メソッドを使用します。これは関数呼び出しのメソッドです。オペコードの実行はすべての PHP プログラムで頻繁に必要な操作であるため、通常、

1

2

3

a)op_type : 为IS_CONST, IS_TMP_VAR, IS_VAR, IS_UNUSED, or IS_CV

  

b)u,一个联合体,根据op_type的不同,分别用不同的类型保存了这个操作数的值(const)或者左值(var)

は SWITCH または GOTO を使用して分散できます。 GOTO の効率は比較的高くなります。 ただし、効率が向上するかどうかは CPU によって異なります。 上記の例では、PHP コードは次のように解析されます: ?
1 2 3 4 5 * ZEND_ECHO 'こんにちはWorld%21' * ZEND_ADD ~011 * ZEND_ASSIGN !0~0 * ZEND_ECHO !0 * ZEND_RETURN 1
$a はどこに行ったのかと疑問に思うかもしれません。ここではオペランドの説明について説明します。各オペランドは次の 2 つの部分で構成されます。 width:714px;border:0px !重要;パディング:0px !重要;マージン:0px !重要;高さ:自動 !重要;フォントファミリー:Consolas、'Bitstream Vera Sans Mono'、'Courier New'、Courier 、monospace ! important;font-size:1em ! important;background:none ! important;"> 1 2 3 a)op_type: IS_CONST、IS_TMP_VAR、IS_VAR、IS_UNUSED、または IS_CV b)u、共用体。op_type に応じて異なる型でオペランドの値 (const) または左辺値 (var) を格納します。
var の場合、IS_TMP_VAR は名前が示すように、次の op_array で使用するために op_array の結果を保存する一時変数です。 (整数) 変数テーブルを指します。このようなオペランドは通常 ~ で始まります。 たとえば、~0 は変数テーブルの 0 番にある未知の一時変数 IS_VAR を表します。これは、$
で始まる変数です。IS_CV は、ZE2.1/PHP5 以降のコンパイラで使用される変数を表します。 1. キャッシュ メカニズム。変数が初めて参照されるとき、この変数は、アクティブなシンボル テーブルを再度参照する必要がありません。 、CV変数は!で始まります。始まりはそれを示しています。

$a は !0 に最適化されているようです。
たとえば、VLD を使用してオペコードを表示すると、表示は次のようになります: html
(注: ニアオ兄弟のブログ投稿は 2008 年であるため、この記事のデータはニアオ兄弟の PHP に多少似ていますが、は開発以来多くの変更が加えられているため、私のブログ投稿にあるプログラムの実行結果と関連手順が Brother Bird のものと異なっていても驚かないでください。私の実行結果は 5.4 です。 )
TIPI: http://www.php-internals.com/

レイアウトはいつも乱雑で、何度か変更されています - -。
声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。