### 3.3.2 执行流程
Zend的executor与linux二进制程序执行的过程是非常类似的,在C程序执行时有两个寄存器ebp、esp分别指向当前作用栈的栈顶、栈底,局部变量全部分配在当前栈,函数调用、返回通过`call`、`ret`指令完成,调用时`call`将当前执行位置压入栈中,返回时`ret`将之前执行位置出栈,跳回旧的位置继续执行,在Zend VM中`zend_execute_data`就扮演了这两个角色,`zend_execute_data.prev_execute_data`保存的是调用方的信息,实现了`call/ret`,`zend_execute_data`后面会分配额外的内存空间用于局部变量的存储,实现了`ebp/esp`的作用。
注意:在执行前分配内存时并不仅仅是分配了`zend_execute_data`大小的空间,除了`sizeof(zend_execute_data)`外还会额外申请一块空间,用于分配局部变量、临时(中间)变量等,具体的分配过程下面会讲到。
__Zend执行opcode的简略过程:__
* __step1:__ 为当前作用域分配一块内存,充当运行栈,zend_execute_data结构、所有局部变量、中间变量等等都在此内存上分配
* __step2:__ 初始化全局变量符号表,然后将全局执行位置指针EG(current_execute_data)指向step1新分配的zend_execute_data,然后将zend_execute_data.opline指向op_array的起始位置
* __step3:__ 从EX(opline)开始调用各opcode的C处理handler(即_zend_op.handler),每执行完一条opcode将`EX(opline)++`继续执行下一条,直到执行完全部opcode,函数/类成员方法调用、if的执行过程:
* __step3.1:__ if语句将根据条件的成立与否决定`EX(opline) + offset`所加的偏移量,实现跳转
* __step3.2:__ 如果是函数调用,则首先从EG(function_table)中根据function_name取出此function对应的编译完成的zend_op_array,然后像step1一样新分配一个zend_execute_data结构,将EG(current_execute_data)赋值给新结构的`prev_execute_data`,再将EG(current_execute_data)指向新的zend_execute_data,最后从新的`zend_execute_data.opline`开始执行,切换到函数内部,函数执行完以后将EG(current_execute_data)重新指向EX(prev_execute_data),释放分配的运行栈,销毁局部变量,继续从原来函数调用的位置执行
* __step3.3:__ 类方法的调用与函数基本相同,后面分析对象实现的时候再详细分析
* __step4:__ 全部opcode执行完成后将step1分配的内存释放,这个过程会将所有的局部变量"销毁",执行阶段结束
![](https://box.kancloud.cn/36e6668ac66e32b8e723c606ce0f65de_994x823.png)
接下来详细看下整个流程。
Zend执行入口为位于`zend_vm_execute.h`文件中的__zend_execute()__:
```c
ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value)
{
zend_execute_data *execute_data;
if (EG(exception) != NULL) {
return;
}
//分配zend_execute_data
execute_data = zend_vm_stack_push_call_frame(ZEND_CALL_TOP_CODE,
(zend_function*)op_array, 0, zend_get_called_scope(EG(current_execute_data)), zend_get_this_object(EG(current_execute_data)));
if (EG(current_execute_data)) {
execute_data->symbol_table = zend_rebuild_symbol_table();
} else {
execute_data->symbol_table = &EG(symbol_table);
}
EX(prev_execute_data) = EG(current_execute_data); //=> execute_data->prev_execute_data = EG(current_execute_data);
i_init_execute_data(execute_data, op_array, return_value); //初始化execute_data
zend_execute_ex(execute_data); //执行opcode
zend_vm_stack_free_call_frame(execute_data); //释放execute_data:销毁所有的PHP变量
}
```
上面的过程分为四步:
#### (1)分配stack
由`zend_vm_stack_push_call_frame`函数分配一块用于当前作用域的内存空间,返回结果是`zend_execute_data`的起始位置。
```c
//zend_execute.h
static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame(uint32_t call_info, zend_function *func, uint32_t num_args, ...)
{
uint32_t used_stack = zend_vm_calc_used_stack(num_args, func);
return zend_vm_stack_push_call_frame_ex(used_stack, call_info,
func, num_args, called_scope, object);
}
```
首先根据`zend_execute_data`、当前`zend_op_array`中局部/临时变量数计算需要的内存空间:
```c
//zend_execute.h
static zend_always_inline uint32_t zend_vm_calc_used_stack(uint32_t num_args, zend_function *func)
{
uint32_t used_stack = ZEND_CALL_FRAME_SLOT + num_args; //内部函数只用这么多,临时变量是编译过程中根据PHP的代码优化出的值,比如:`"hi~".time()`,而在内部函数中则没有这种情况
if (EXPECTED(ZEND_USER_CODE(func->type))) { //在php脚本中写的function
used_stack += func->op_array.last_var + func->op_array.T - MIN(func->op_array.num_args, num_args);
}
return used_stack * sizeof(zval);
}
//zend_compile.h
#define ZEND_CALL_FRAME_SLOT \
((int)((ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval)) - 1) / ZEND_MM_ALIGNED_SIZE(sizeof(zval))))
```
回想下前面编译阶段zend_op_array的结果,在编译过程中已经确定当前作用域下有多少个局部变量(func->op_array.last_var)、临时/中间/无用变量(func->op_array.T),从而在执行之初就将他们全部分配完成:
* __last_var__:PHP代码中定义的变量数,zend_op.op{1|2}_type = IS_CV 或 result_type & IS_CV的全部数量
* __T__:表示用到的临时变量、无用变量等,zend_op.op{1|2}_type = IS_TMP_VAR|IS_VAR 或resulte_type & (IS_TMP_VAR|IS_VAR)的全部数量
比如赋值操作:`$a = 1234;`,编译后`last_var = 1,T = 1`,`last_var`有`$a`,这里为什么会有`T`?因为赋值语句有一个结果返回值,只是这个值没有用到,假如这么用结果就会用到了`if(($a = 1234) == true){...}`,这时候`$a = 1234;`的返回结果类型是`IS_VAR`,记在`T`上。
`num_args`为函数调用时的实际传入参数数量,`func->op_array.num_args`为全部参数数量,所以`MIN(func->op_array.num_args, num_args)`等于`num_args`,在自定义函数中`used_stack=ZEND_CALL_FRAME_SLOT + func->op_array.last_var + func->op_array.T`,而在调用内部函数时则只需要分配实际传入参数的空间即可,内部函数不会有临时变量的概念。
最终分配的内存空间如下图:
![](https://box.kancloud.cn/cd3eff89c3b09a389d91e77a12cf688e_614x258.png)
这里实际分配内存时并不是直接`malloc`的,还记得上面EG结构中有个`vm_stack`吗?实际内存是从这里获取的,每次从`EG(vm_stack_top)`处开始分配,分配完再将此指针指向`EG(vm_stack_top) + used_stack`,这里不再对vm_stack作更多分析,更下层实际就是Zend的内存池(zend_alloc.c),后面也会单独分析。
```c
static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame_ex(uint32_t used_stack, ...)
{
zend_execute_data *call = (zend_execute_data*)EG(vm_stack_top);
...
//当前vm_stack是否够用
if (UNEXPECTED(used_stack > (size_t)(((char*)EG(vm_stack_end)) - (char*)call))) {
call = (zend_execute_data*)zend_vm_stack_extend(used_stack); //新开辟一块vm_stack
...
}else{ //空间够用,直接分配
EG(vm_stack_top) = (zval*)((char*)call + used_stack);
...
}
call->func = func;
...
return call;
}
```
#### (2)初始化zend_execute_data
注意,这里的初始化是整个php脚本最初的那个,并不是指函数调用时的,这一步的操作主要是设置几个指针:`opline`、`call`、`return_value`,同时将PHP的全局变量添加到`EG(symbol_table)`中去:
```c
//zend_execute.c
static zend_always_inline void i_init_execute_data(zend_execute_data *execute_data, zend_op_array *op_array, zval *return_value)
{
EX(opline) = op_array->opcodes;
EX(call) = NULL;
EX(return_value) = return_value;
if (UNEXPECTED(EX(symbol_table) != NULL)) {
...
zend_attach_symbol_table(execute_data);//将全局变量添加到EG(symbol_table)中一份,因为此处的execute_data是PHP脚本最初的那个,不是function的,所以所有的变量都是全局的
}else{ //这个分支的情况还未深入分析,后面碰到再补充
...
}
}
```
`zend_attach_symbol_table()`的作用是把当前作用域下的变量添加到EG(symbol_table)哈希表中,也就是全局变量,函数中通过global关键词获取的全局变量正是在此时添加的,EG(symbol_table)中的值间接的指向`zend_execute_data`中的局部变量,两者的结构如下图所示:
![](https://box.kancloud.cn/f987ea23edefcf51d2333d3deeee3fe2_739x206.png)
#### (3)执行opcode
这一步开始具体执行opcode指令,这里调用的是`zend_execute_ex`,这是一个函数指针,如果此指针没有被任何扩展重新定义那么将由默认的`execute_ex`处理:
```c
# define ZEND_OPCODE_HANDLER_ARGS_PASSTHRU execute_data
ZEND_API void execute_ex(zend_execute_data *ex)
{
zend_execute_data *execute_data = ex;
while(1) {
int ret;
if (UNEXPECTED((ret = ((opcode_handler_t)EX(opline)->handler)(execute_data /*ZEND_OPCODE_HANDLER_ARGS_PASSTHRU*/)) != 0)) {
if (EXPECTED(ret > 0)) { //调到新的位置执行:函数调用时的情况
execute_data = EG(current_execute_data);
}else{
return;
}
}
}
}
```
大概的执行过程上面已经介绍过了,这里只分析下整体执行流程,至于PHP各语法具体的handler处理后面会单独列一章详细分析。
#### (4)释放stack
这一步就比较简单了,只是将申请的`zend_execute_data`内存释放给内存池(注意这里并不是变量的销毁),具体的操作只需要修改几个指针即可:
```c
static zend_always_inline void zend_vm_stack_free_call_frame_ex(uint32_t call_info, zend_execute_data *call)
{
ZEND_ASSERT_VM_STACK_GLOBAL;
if (UNEXPECTED(call_info & ZEND_CALL_ALLOCATED)) {
zend_vm_stack p = EG(vm_stack);
zend_vm_stack prev = p->prev;
EG(vm_stack_top) = prev->top;
EG(vm_stack_end) = prev->end;
EG(vm_stack) = prev;
efree(p);
} else {
EG(vm_stack_top) = (zval*)call;
}
ZEND_ASSERT_VM_STACK_GLOBAL;
}
static zend_always_inline void zend_vm_stack_free_call_frame(zend_execute_data *call)
{
zend_vm_stack_free_call_frame_ex(ZEND_CALL_INFO(call), call);
}
```
- 前言
- 第1章 PHP基本架构
- 1.1 PHP简介
- 1.2 PHP7的改进
- 1.3 FPM
- 1.3.1 概述
- 1.3.2 基本实现
- 1.3.3 FPM的初始化
- 1.3.4 请求处理
- 1.3.5 进程管理
- 1.4 PHP执行的几个阶段
- 第2章 变量
- 2.1 变量的内部实现
- 2.2 数组
- 2.3 静态变量
- 2.4 全局变量
- 2.5 常量
- 第3章 Zend虚拟机
- 3.1 PHP代码的编译
- 3.1.1 词法解析、语法解析
- 3.1.2 抽象语法树编译流程
- 3.2 函数实现
- 3.2.1 内部函数
- 3.2.2 用户函数的实现
- 3.3 Zend引擎执行流程
- 3.3.1 基本结构
- 3.3.2 执行流程
- 3.3.3 函数的执行流程
- 3.3.4 全局execute_data和opline
- 3.4 面向对象实现
- 3.4.1 类
- 3.4.2 对象
- 3.4.3 继承
- 3.4.4 动态属性
- 3.4.5 魔术方法
- 3.4.6 类的自动加载
- 3.5 运行时缓存
- 3.6 Opcache
- 3.6.1 opcode缓存
- 3.6.2 opcode优化
- 3.6.3 JIT
- 第4章 PHP基础语法实现
- 4.1 类型转换
- 4.2 选择结构
- 4.3 循环结构
- 4.4 中断及跳转
- 4.5 include/require
- 4.6 异常处理
- 第5章 内存管理
- 5.1 Zend内存池
- 5.2 垃圾回收
- 第6章 线程安全
- 6.1 什么是线程安全
- 6.2 线程安全资源管理器
- 第7章 扩展开发
- 7.1 概述
- 7.2 扩展的实现原理
- 7.3 扩展的构成及编译
- 7.3.1 扩展的构成
- 7.3.2 编译工具
- 7.3.3 编写扩展的基本步骤
- 7.3.4 config.m4
- 7.4 钩子函数
- 7.5 运行时配置
- 7.5.1 全局变量
- 7.5.2 ini配置
- 7.6 函数
- 7.6.1 内部函数注册
- 7.6.2 函数参数解析
- 7.6.3 引用传参
- 7.6.4 函数返回值
- 7.6.5 函数调用
- 7.7 zval的操作
- 7.7.1 新生成各类型zval
- 7.7.2 获取zval的值及类型
- 7.7.3 类型转换
- 7.7.4 引用计数
- 7.7.5 字符串操作
- 7.7.6 数组操作
- 7.8 常量
- 7.9 面向对象
- 7.9.1 内部类注册
- 7.9.2 定义成员属性
- 7.9.3 定义成员方法
- 7.9.4 定义常量
- 7.9.5 类的实例化
- 7.10 资源类型
- 7.11 经典扩展解析
- 7.8.1 Yaf
- 7.8.2 Redis
- 第8章 命名空间
- 8.1 概述
- 8.2 命名空间的定义
- 8.2.1 定义语法
- 8.2.2 内部实现
- 8.3 命名空间的使用
- 8.3.1 基本用法
- 8.3.2 use导入
- 8.3.3 动态用法
- 附录
- break/continue按标签中断语法实现
- defer推迟函数调用语法的实现
- 一起线上事故引发的对PHP超时控制的思考