php代码的执行过程
CPU的基本结构都是逻辑电路,所以指令的执行(取值,运算等)都是逻辑电路实现的。CPU只认得二进制的机器码,只能执行二进制的机器码的指令。但是全二进制的机器码不容易记忆也容易出错,所以厂家就把机器码翻译成对应的汇编指令,如上假设加法指令010就被翻译成ADD,就成了指令。所有这个CPU能够执行的指令放在一起就是指令集,称为汇编语言。汇编语言作为机器语言与程序设计者之间的一个层,在汇编语言之上还有一个层-C语言,C语言更贴近人类熟悉的“自然语言”,程序设计者可以通过C语言编译器将C源代码文件编译成目标文件(二进制文件,中间会先翻译成汇编语言,然后由汇编语言生成机器码),然后将各个目标文件连接在一起就组成了一个可执行文件。正如有人说过的一句名言“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”(“Any problem in computer science can be solved by another layer of indirection.”) PHP语言就是在C语言之上的一个层,PHP引擎是由C语言来实现的,因此PHP语言这一个在C之上抽象出来的层使用起来比C更简单方便。
在php发展史中,PHP4.0之前都是被PHP解释器运行的。2000年5月新版PHP4.0发布,PHP使用zend引擎来编译运行。zend引擎是使用C语言编写的。2004年7月,PHP5正式版本的发布,它的核心是第二代Zend引擎。zend引擎先把PHP代码转换成opcode中间代码,然后再执行opcode。
举例详细解说
文件名称 help.php
<?php echo "Hello World"; $a = 1 + 1; echo $a; ?>
使用工具查看opcode码:
[root@localhost html]# /usr/local/php/bin/php -dvld.active=1 hello.php
Branch analysis from position: 0
Return found
filename: /var/www/html/hello.php
function name: (null)
number of ops: 6
compiled vars: !0 = $a
line # op fetch ext return operands
-------------------------------------------------------------------------------
2 0 ECHO 'Hello+world'
3 1 ADD ~0 1, 1
2 ASSIGN !0, ~0
4 3 ECHO !0
6 4 RETURN 1
5* ZEND_HANDLE_EXCEPTION
Hello world2引擎Zend执行help.php文件中的这段代码会经过如下4个步骤:
1. Scanning (Lexing):将PHP代码转换为语言片段(Tokens)
2. Parsing:将Tokens转换成简单而有意义的表达式
3. Compilation:将表达式编译成Opocdes
4. Execution:顺次执行Opcodes,每次一条,从而实现PHP脚本的功能。也可以说,引擎Zend执行help.php文件中的这段代码会经过如下3个步骤:
- 首先,Zend Engine(Zend引擎),调用词法分析器(Lex生成的,源文件在 Zend/zend_language_sanner.l), 将我们要执行的PHP源文件,去掉空格 ,注释,分割成一个一个的token。
- 然后,Zend引擎会将得到的token forward给语法分析器(yacc生成, 源文件在 Zend/zend_language_parser.y),生成一个一个的op code,opcode一般会以op array的形式存在,它是PHP执行的中间语言。
- 最后,Zend引擎调用zend_executor来执行op array,输出结果。
生成tokens
zend引擎使用Zend/zend_language_scanner.c扫描php文件中的代码,根据Zend/zend_language_scanner.l(Lex表文件),把来PHP代码进行词法分析,从而得到一个一个的“词”Tokens。(从PHP4.2开始提供了一个函数叫token_get_all,这个函数就可以讲一段PHP代码转化成Tokens)
token_get_all函数<?php $tokens = token_get_all('<?php echo "Hello World"; $a = 1 + 1; echo $a; ?>'); print_r($tokens); ?>php代码执行结果:
Array ( [0] => Array ( [0] => 367 [1] => 1 ) [1] => Array ( [0] => 370 [1] => [2] => 2 ) [2] => Array ( [0] => 316 [1] => echo [2] => 2 ) [3] => Array ( [0] => 370 [1] => [2] => 2 ) [4] => Array ( [0] => 315 [1] => "Hello World" [2] => 2 ) [5] => ; [6] => Array ( [0] => 370 [1] => [2] => 2 ) [7] => Array ( [0] => 309 [1] => $a [2] => 3 ) [8] => Array ( [0] => 370 [1] => [2] => 3 ) [9] => = [10] => Array ( [0] => 370 [1] => [2] => 3 ) [11] => Array ( [0] => 305 [1] => 1 [2] => 3 ) [12] => Array ( [0] => 370 [1] => [2] => 3 ) [13] => + [14] => Array ( [0] => 370 [1] => [2] => 3 ) [15] => Array ( [0] => 305 [1] => 1 [2] => 3 ) [16] => ; [17] => Array ( [0] => 370 [1] => [2] => 3 ) [18] => Array ( [0] => 316 [1] => echo [2] => 4 ) [19] => Array ( [0] => 370 [1] => [2] => 4 ) [20] => Array ( [0] => 309 [1] => $a [2] => 4 ) [21] => ; [22] => Array ( [0] => 370 [1] => [2] => 4 ) [23] => Array ( [0] => 369 [1] => ?> [2] => 5 ) )
返回的结果, 源码中的字符串,字符,空格,都会原样返回。每个源代码中的字符,都会出现在相应的顺序处。而,其他的比如标签、操作符、语句,都会被转换成一个包含俩部分的Array: Token ID (也就是在Zend内部的改Token的对应码,如,T_ECHO,T_STRING),和源码中的原来的内容。
Parsing阶段(从语法上描述或分析)
首先会丢弃Tokens Array中的多于的空格,然后将剩余的Tokens转换成一个一个的简单的表达式。
- echo a constant string
- add two numbers together
- store the result of the prior expression to a variable
- echo a variable
Compilation阶段(编译)
把Tokens编译成一个个op_array, 每个op_arrayd包含如下5个部分:
- Opcode数字的标识,指明了每个op_array的操作类型,比如add , echo
- 结果 存放Opcode结果
- 操作数1 给Opcode的操作数
- 操作数2
- 扩展值 1个整形用来区别被重载的操作符
PHP代码会被Parsing成:
* ZEND_ECHO 'Hello World' * ZEND_ADD ~0 1 1 * ZEND_ASSIGN !0 ~0 * ZEND_ECHO !0
每个操作数都是由以下俩个部分组成:
- op_type : 为IS_CONST, IS_TMP_VAR, IS_VAR, IS_UNUSED, or IS_CV
- 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了。