关于php7内部实现的几个问题

  1. php-fpm的实现
  2. 一个php脚本的整体的执行流程
  3. PHP代码的编译
  4. 词法解析、语法解析
  5. AST(abstract sytax tree)抽象语法树编译流程
    1. handler
    2. CALL模式
    3. GOTO模式
    4. SWITCH模式
  6. PHP5、7的编译执行差异
  7. 参考

针对本文内容,主要是基于下面几个问题去探寻记录整理的:

php-fpm的实现

概括来说,fpm的实现就是创建一个master进程,在master进程中创建并监听socket,然后fork出多个子进程,这些子进程各自accept请求,子进程的处理非常简单,它在启动后阻塞在accept上,有请求到达后开始读取请求数据,读取完成后开始处理然后再返回,在这期间是不会接收其它请求的,也就是说fpm的子进程同时只能响应一个请求,只有把这个请求处理完成后才会accept下一个请求,这一点与nginx的事件驱动有很大的区别,nginx的子进程通过epoll管理套接字,如果一个请求数据还未发送完成则会处理下一个请求,即一个进程会同时连接多个请求,它是非阻塞的模型,只处理活跃的套接字。

fpm的master进程与worker进程之间不会直接进行通信,master通过共享内存获取worker进程的信息,比如worker进程当前状态、已处理请求数等,当master进程要杀掉一个worker进程时则通过发送信号的方式通知worker进程。

fpm可以同时监听多个端口,每个端口对应一个worker pool,而每个pool下对应多个worker进程,类似nginx中server概念。

请求处理
fpm_run()执行后将fork出worker进程,worker进程返回main()中继续向下执行,后面的流程就是worker进程不断accept请求,然后执行PHP脚本并返回。整体流程如下:

但是,该工作模式的隐患:

一个php脚本的整体的执行流程

PHP执行的几个阶段

其中已经语法分析、词法分析等包含其中。

PHP代码的编译

从PHP代码到opcode是怎么实现的?PHP编译过程包括词法分析、语法分析,使用re2c、bison完成,旧的PHP版本直接生成了opcode,PHP7新增了抽象语法树(AST),在语法分析阶段生成AST,然后再生成opcode数组。

PHP编译阶段的基本过程如下图:

词法解析、语法解析

该PHP的解析阶段,即 PHP代码->抽象语法树(AST) 的过程。

PHP使用re2c、bison完成这个阶段的工作:

AST(abstract sytax tree)抽象语法树编译流程

完成了从 PHP代码解析为抽象语法树 的过程,就要开始 抽象语法树->Opcodes 的过程。

语法解析过程的产物保存于CG(AST),接着zend引擎会把AST进一步编译为 zend_op_array ,它是编译阶段最终的产物,也是执行阶段的输入,后面我们介绍的东西基本都是围绕zend_op_array展开的,AST解析过程确定了当前脚本定义了哪些变量,并为这些变量 顺序编号,这些值在使用时都是按照这个编号获取的,另外也将变量的初始化值、调用的函数/类/常量名称等值(称之为字面量)保存到zend_op_array.literals中,这些字面量也有一个唯一的编号,所以执行的过程实际就是根据各指令调用不同的C函数,然后根据变量、字面量、临时变量的编号对这些值进行处理加工。

我们首先看下zend_op_array的结构,明确几个关键信息,然后再看下ast编译为zend_op_array的过程。

struct _zend_op_array {
//common是普通函数或类成员方法对应的opcodes快速访问时使用的字段,后面分析PHP函数实现的时候会详细讲
...
uint32_t *refcount;
uint32_t this_var;
uint32_t last;
//opcode指令数组
zend_op *opcodes;
//PHP代码里定义的变量数:op_type为IS_CV的变量,不含IS_TMP_VAR、IS_VAR的
//编译前此值为0,然后发现一个新变量这个值就加1
int last_var;
//临时变量数:op_type为IS_TMP_VAR、IS_VAR的变量
uint32_t T;
//PHP变量名数组
zend_string **vars; //这个数组在ast编译期间配合last_var用来确定各个变量的编号,非常重要的一步操作
...
//静态变量符号表:通过static声明的
HashTable *static_variables;
...
//字面量数量
int last_literal;
//字面量(常量)数组,这些都是在PHP代码定义的一些值
zval *literals;
//运行时缓存数组大小
int cache_size;
//运行时缓存,主要用于缓存一些znode_op以便于快速获取数据,后面单独介绍这个机制
void **run_time_cache;
void *reserved[ZEND_MAX_RESERVED_RESOURCES];
};

zend_op_array.opcodes指向指令列表,具体每条指令的结构如下:

struct _zend_op {
const void *handler; //指令执行handler
znode_op op1; //操作数1
znode_op op2; //操作数2
znode_op result; //返回值
uint32_t extended_value;
uint32_t lineno;
zend_uchar opcode; //opcode指令
zend_uchar op1_type; //操作数1类型
zend_uchar op2_type; //操作数2类型
zend_uchar result_type; //返回值类型
};
//操作数结构
typedef union _znode_op {
uint32_t constant;
uint32_t var;
uint32_t num;
uint32_t opline_num; /* Needs to be signed */
uint32_t jmp_offset;
} znode_op;

opcode各字段含义下面展开说明。

handler

handler为每条opcode对应的C语言编写的处理过程,所有opcode对应的处理过程定义在zend_vm_def.h中,值得注意的是这个文件并不是编译时用到的,因为opcode的处理过程 有三种不同的提供形式:CALLSWITCHGOTO,默认方式为CALL,这个是什么意思呢?

每个opcode都代表了一些特定的处理操作,这个东西怎么提供呢?一种是把每种opcode负责的工作封装成一个function,然后执行器循环执行即可,这就是CALL模式的工作方式;
另外一种是把所有opcode的处理方式通过C语言里面的label标签区分开,然后执行器执行的时候goto到相应的位置处理,这就是GOTO模式的工作方式;
最后还有一种方式是把所有的处理方式写到一个switch下,然后通过case不同的opcode执行具体的操作,这就是SWITCH模式的工作方式。

假设opcode数组是这个样子:

int op_array[] = {
opcode_1,
opcode_2,
opcode_3,
...
};

各模式下的工作过程类似这样:

CALL模式

//CALL模式
void opcode_1_handler() {...}
void opcode_2_handler() {...}
...
void execute(int []op_array)
{
void *opcode_handler_list[] = {&opcode_1_handler, &opcode_2_handler, ...};
while(1){
void handler = opcode_handler_list[op_array[i]];
handler(); //call handler
i++;
}
}

GOTO模式

//GOTO模式
void execute(int []op_array)
{
while(1){
goto opcode_xx_handler_label;
}
opcode_1_handler_label:
...
opcode_2_handler_label:
...
...
}

SWITCH模式

//SWITCH模式
void execute(int []op_array)
{
while(1){
switch(op_array[i]){
case opcode_1:
...
case opcode_2:
...
...
}
i++;
}
}

三种模式效率是不同的,GOTO最快。
怎么选择其它模式呢?下载PHP源码后不要直接编译,Zend目录下有个文件:zend_vm_gen.php,在编译PHP前执行:php zend_vm_gen.php –with-vm-kind=CALL|SWITCH|GOTO,这个脚本将重新生成:zend_vm_opcodes.h、zend_vm_opcodes.c、zend_vm_execute.h三个文件覆盖原来的,然后再编译PHP即可。

PHP5、7的编译执行差异

其实从以上的编译部分,还有zval的数据结构的变化,也都是导致5、7编译差异之处的根本原因。

参考

script>