51

荐书读书会丨PHP7语言的执行原理

 5 years ago
source link: http://www.10tiao.com/html/320/201807/2651005022/2.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

点击上方“程序人生”,选择“置顶公众号”

第一时间关注程序猿(媛)身边的故事


小贴士


参与文末活动,有机会线上免费听取作者分享,并获得赠书~


我们常用的高级语言有很多种,比较出名的有C\C++、Python、 PHP、Go、Pascal等。而这些语言根据运行的方式不同,大体分为两种:编译型语言解释型语言


其中,编译型语言包括C\C++、Pascal、Go等。这里说的编译是指在应用源程序执行之前,就将程序源代码“翻译”成汇编语言,然后进一步根据软硬件环境编译成目标文件。一般我们称完成编译工作的工具叫编译器。而解释型语言,在程序运行时才被“翻译”为机器语言。但是执行一次“翻译”一次,所以执行效率较低。解释器的工作就是解释性语言中,负责“翻译”源代码的程序。


下面我们更详细地讨论一下编译型语言和解释性语言的运行方式。


一、编译型语言与解释型语言

我们知道,对于一段C语言代码,需要经过预编译、编译、汇编和链接,才能成为可执行的二进制文件。以hello.c为例:

#include<stdio.h>
int main(){
   printf("hello world");
   return 1;
}


对于这段C代码,main是程序入口函数,实现的功能是打印字符串“hello world” 到屏幕上。编译和执行过程如图1所示。


图1  编译型语言的执行示意图

1:C语言代码预处理(比如依赖处理、宏替换等)。如以上代码示例,#inlcude<stdio.h>就会在预处理阶段被替换。


2:编译。编译器会把C语言翻译成汇编语言程序,一条C语言通常便以为多条汇编代码。同时编译器会对程序进行优化,生成目标汇编程序。


3:编译得到的汇编语言通过汇编器再汇编成目标程序hello.o。


4:链接。程序中往往包含一些共享目标文件,如示例程序中的printf()函数,位于静态库,需要经过链接器(如Uinx连接器ld)进行链接。


以C语言为代表的编译型语言,代码发生更新都要经过以上步骤:

我们区别编译型语言与解释型语言,主要立足于源代码被编译成目标平台CPU指令的时机。对于编译型语言,编译结果已经是针对当前CPU体系的指令;而解释型语言,需要先编译成中间代码,再经由该解释型语言的特定虚拟机,翻译成特定CPU体系的指令被执行。解释型语言是在运行过程中,翻译为目标平台的指令。常说解释型语言“慢”,主要也是慢在这里。


在PHP7中,源代码首先将进行词法分析,将源代码切割为多个字符串单元,分割后的字符串称之为Token。而一个个独立的Token无法表达完整语义,需经过语法分析阶段,将Token转换为抽象语法树(简称AST)。之后,抽象语法树被转换为机器指令执行。在PHP中,这些指令称为opcode(后文会对opcode做更详细的解释,此处读者可以看待为CPU指令)。


到AST的生成这一步,编译型语言与解释型语言所需经历的过程相似。从抽象语法树之后开始产生差异。


图2是PHP(如无特殊说明,本章提到的PHP均为PHP7版本)代码被执行的简化步骤,其中最后一步的左侧分支,是编译型语言的过程。




图2  以PHP为例解释型语言的执行示意图

1:源码通过词法分析得到Token;


2:基于语法分析器生成抽象语法树(AST);


3:抽象语法树转换为Opcodes(opcode指令集合),PHP解释执行Opcodes。


接下来我们在基本步骤的基础上,细化PHP语言的执行原理,试图更清晰地建立认知。


二、PHP7的执行原理概述

首先我们补充说明下前文提到的PHP7程序执行过程,请参见图3。

图3  PHP7语言编写的程序的执行过程图

1:词法分析将PHP代码转换为有意义的标识Token。该步骤的词法分析器使用Re2c实现的。


2:语法分析将Token和符合文法规则的代码生成抽象语法树。语法分析器基于Bison实现。语法分析使用了巴科斯范式(BNF)来表达文法规则,Bison借助状态机、状态转移表和压栈、出栈等一系列操作,生成抽象语法树。


3:上步的抽象语法树生成对应的opcode,被虚拟机执行。opcode是PHP7定义的一组指令标识,指令对应着相应的handler(处理函数)。当虚拟机调用opcode,会找到opcode背后的处理函数,执行真正的处理。以我们常见的echo语句为例,其对应的opcode便是ZEND_ECHO。


注意:这里为了便于理解词法分析和语法分析过程,将两者分开描述。但实际情况,出于效率考虑,两个过程并非完全独立。


下面,我们通过一段示例代码,来建立PHP7运转的初步理解。

示例代码如下:

<?php
echo "hello world";


从图3可知,这段代码首先会被切割为Token。


1. Token


Token是PHP代码被切割成的有意义的标识。本书介绍的PHP7版本中有137 种Token,在zend_language_parser.h文件中做了定义:

/* Tokens.  */
#define END 0
#define T_INCLUDE 258
#define T_INCLUDE_ONCE 259

#define T_ERROR 392


更多Token的含义,感兴趣的读者可以参考《PHP 7底层设计与源码实现》附录。


PHP提供了token_get_all()函数来获取PHP代码被切割后的Token,可以在深入源码学习前,粗略查看PHP代码被切割后的Token。如下代码片段:

/home/vagrant/php7/bin/php –r 'print_r(Token_get_all("<?php echo \"hello world\";"));'


输出结果为:

Array
(
   [0] => Array
       (
           [0] => 379
           [1] => <?php
           [2] => 1
       )
   [1] => Array
       (
           [0] => 328
           [1] => echo
           [2] => 1
       )
   [2] => Array
       (
           [0] => 382
           [1] =>
           [2] => 1
       )
   [3] => Array
       (
           [0] => 323
           [1] => "hello world"
           [2] => 1
       )
   [4] => ;
)


上文输出中,二维数组的每个成员数组第一个值为Token对应的枚举值;第二个值为Token对应的原始字符串内容;第三个值为代码对应的行号。可以看出,词法解析器将 <?php echo "hello world"; 这段文本内容切分成了4部分。


1)文本“<?php”,切割后对应的Token值为379,参考PHP7中的源码:  

#define T_OPEN_TAG 379


不难理解,它是PHP代码的起始tag,也就是<?php标识;


2)echo对应的Token是T_ECHO:

#define T_ECHO 328


3)源码中的空格,对应的Token叫T_WHITESPACE,值为382:

#define T_WHITESPACE 382


4)字符串“hello world”对应的Token值为323:

#define T_CONSTANT_ENCAPSED_STRING 323


可见,Token就是一个个的“词块”,但是单独存在的词块不能表达完整的语义,还需要借助规则进行组织串联。语法分析器就是这个组织者。它会检查语法、匹配Token,对Token进行关联。


PHP7中,组织串联的产物就是抽象语法树(Abstract Syntax Tree,AST)。


2. AST

AST是PHP7版本新特性。在这之前的版本,PHP代码的执行过程中没有生成AST这一步。PHP7对抽象语法树的支持,实现了PHP编译器和解释器解耦,有效提升了可维护性。


顾名思义,抽象语法树具有树状结构。AST的节点分为多种类型,对应着不同的PHP语法。在当前章节,我们可以认为节点类型是对语法规则的抽象,例如赋值语句,生成的抽象语法树节点为ZEND_AST_ASSIGN。而赋值语句的左右操作数,又将作为ZEND_AST_ASSIGN类型节点的孩子。通过这样的节点关系,构建出抽象语法树。


如果读者希望一睹为快,可以直接跳到本书第13章函数的实现,其中图片描绘了一段简单的PHP代码生成的抽象语法树。


在这里,我们推荐读者了解下PhpParser工具,可以用它来查看PHP代码生成的AST。


注意:PHP-Parser是PHP7内核作者之一nikic编写的将PHP源码生成AST的工具。源码见https://github.com/nikic/PHP-Parser


3. Opcodes

AST扮演了源码到中间代码的临时存储介质的角色,还需要将其转换为opcode,才能被引擎直接执行。Opcode只是单条指令,Opcodes是opcode的集合形式,是PHP执行过程中的中间代码,类似Java中的字节码。生成之后由虚拟机执行。


我们知道,PHP工程优化措施中有个比较常见的“开启Opcache”,指的就是这里的Opcodes的缓存(Opcodes Cache)。通过省去从源码到opcode的阶段,引擎可以直接执行缓存的opcode,以此提升性能。


借助vld插件,可以直观地看到一段PHP代码生成的opcode:

php -dvld.active=1 hello.php
经过过滤整理,对应的opcode为:
line     op              
 1      ECHO            
 2      RETURN


其实在源码实现中,上述代码生成的opcode及handler为:

ZEND_ECHO  // handler: ZEND_ECHO_SPEC_CONST_HANDLER
ZEND_RETURN  // handler: ZEND_RETURN_SPEC_CONST_HANDLER


可见,ZEND_ECHO对应的handler是ZEND_ECHO_SPEC_CONST_HANDLER。此handler的实现的功能便是预期的“hello world”语句的输出。


本书的PHP版本中,内核在zend_vm_opcodes.h中定义了186种Opcodes,也可以参考《PHP 7底层设计与源码实现》附录部分。


在平时的业务开发中,了解一些 PHP的底层实现,尤其是语法机制的实现,对性能提升、故障排除非常有好处。希望这篇浅文,可以帮助读者在使用PHP7的同时,了解到编写的PHP代码如何被编译和执行。

(扫上方二维码7.9折购买

推荐理由:

  • 滴滴出行专家联合撰写,PHP领域大咖夏绪宏、韩天峰、王晶、谢华亮(黑夜路人)、伍星联袂推荐

  • 全面吃透PHP内核架构、核心实现与内存管理、词法与句法解析、Zend 虚拟机、函数及关键扩展等设计细节与源码实现


本期获书规则

扫码进群:

1 听嘉宾浅谈内存管理,Zval和HashTable,有机会和嘉宾互动交流;

2 嘉宾分享结束后,从群内活跃互动者中抽取5名幸运用户,免费得此书。


进群方式:

1、前100名扫码进群。

2、无法扫码进群者,可扫下图二维码添加小助手微信ID:Balance_Do,回复「PHP7」。然后拉你进群。


分享时间:7月6日12点(分享开始前一小时起停止拉人入群。想听作者关于PHP7源码内容分享的用户请留意在此时间前进群)


作者简介:陈雷,《PHP7底层设计与源码实现》第一作者及组织者,滴滴顺风车运营营销研发团队技术负责人,在互联网领域多年,经验丰富。


开奖时间:7月6日当天作者分享结束后,群内抽奖


更多精彩内容


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK