### 3.3.3 函数的执行流程
(这里的函数指用户自定义的PHP函数,不含内部函数)
上一节我们介绍了zend执行引擎的几个关键步骤,也简单的介绍了函数的调用过程,这里再单独总结下:
* __【初始化阶段】__ 这个阶段首先查找到函数的zend_function,普通function就是到EG(function_table)中查找,成员方法则先从EG(class_table)中找到zend_class_entry,然后再进一步在其function_table找到zend_function,接着就是根据zend_op_array新分配 __zend_execute_data__ 结构并设置上下文切换的指针
* __【参数传递阶段】__ 如果函数没有参数则跳过此步骤,有的话则会将函数所需参数传递到 __初始化阶段__ 新分配的 __zend_execute_data动态变量区__
* __【函数调用阶段】__ 这个步骤主要是做上下文切换,将执行器切换到调用的函数上,可以理解会在这个阶段__递归调用zend_execute_ex__函数实现call的过程(实际并一定是递归,默认是在while(1){...}中切换执行空间的,但如果我们在扩展中重定义了zend_execute_ex用来介入执行流程则就是递归调用)
* __【函数执行阶段】__ 被调用函数内部的执行过程,首先是接收参数,然后开始执行opcode
* __【函数返回阶段】__ 被调用函数执行完毕返回过程,将返回值传递给调用方的zend_execute_data变量区,然后释放zend_execute_data以及分配的局部变量,将上下文切换到调用前,回到调用的位置继续执行,这个实际是函数执行中的一部分,不算是独立的一个过程
接下来我们一个具体的例子详细分析下各个阶段的处理过程:
```php
function my_function($a, $b = false, $c = "hi"){
return $c;
}
$a = array();
$b = true;
my_function($a, $b);
```
主脚本、my_function的opcode为:
![](https://box.kancloud.cn/57b7a89d107c648e1bef5ab113a70e45_664x312.png)
#### 3.3.3.1 初始化阶段
此阶段的主要工作有两个:查找函数zend_function、分配zend_execute_data。
上面的例子此过程执行的opcode为`ZEND_INIT_FCALL`,根据op_type计算可得handler为`ZEND_INIT_FCALL_SPEC_CONST_HANDLER`:
```c
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INIT_FCALL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
zval *fname = EX_CONSTANT(opline->op2); //调用的函数名称通过操作数2记录
zval *func;
zend_function *fbc;
zend_execute_data *call;
//这里牵扯到zend的一种缓存机制:运行时缓存,后面我们会单独分析,这里忽略即可
...
//首先根据函数名去EG(function_table)索引zend_function
func = zend_hash_find(EG(function_table), Z_STR_P(fname));
if (UNEXPECTED(func == NULL)) {
SAVE_OPLINE();
zend_throw_error(NULL, "Call to undefined function %s()", Z_STRVAL_P(fname));
HANDLE_EXCEPTION();
}
fbc = Z_FUNC_P(func); //(*func).value.func
...
//分配zend_execute_data
call = zend_vm_stack_push_call_frame_ex(
opline->op1.num, ZEND_CALL_NESTED_FUNCTION,
fbc, opline->extended_value, NULL, NULL);
call->prev_execute_data = EX(call);
EX(call) = call; //将当前正在运行的zend_execute_data.call指向新分配的zend_execute_data
ZEND_VM_NEXT_OPCODE();
}
```
当前zend_execute_data及新生成的zend_execute_data关系:
![](https://box.kancloud.cn/18a83aef75e7263a5d055bf07148732f_746x394.png)
注意 __This__ 这个值,它并不仅仅指的是面向对象中那个this,此外它还记录着其它两个信息:
* __call_info:__ 调用信息,通过 __This.u1.reserved__ 记录,因为我们的主脚本、用户自定义函数调用、内核函数调用、include/require/eval等都会生成一个zend_execute_data,这个值就是用来区分这些不同类型的,对应的具体值为:ZEND_CALL_TOP_CODE、ZEND_CALL_NESTED_FUNCTION、ZEND_CALL_TOP_FUNCTION、ZEND_CALL_NESTED_CODE,这个信息是在分配zend_execute_data时显式声明的
* __num_args:__ 函数调用实际传入的参数数量,通过 __This.u2.num_args__ 记录,比如示例中我们定义的函数有3个参数,其中1个是必传的,而我们调用时传入了2个,所以这个例子中的num_args就是2,这个值在编译时知道的,保存在 __zend_op->extended_value__ 中
#### 3.3.3.2 参数传递阶段
这个过程就是将当前作用空间下的变量值"复制"到新的zend_execute_data动态变量区中,那么调用方怎么知道要把值传递到新zend_execute_data哪个位置呢?实际这个地方是有固定规则的,zend_execute_data的动态变量区最前面是参数变量,按照参数的顺序依次分配,接着才是普通的局部变量、临时变量等,所以调用方就可以根据传的是第几个参数来确定其具体的存储位置。
另外这里的"复制"并不是硬拷贝,而是传递的value指针(当然bool/int/double类型不需要),通过引用计数管理,当在被调函数内部改写参数的值时将重新拷贝一份,与普通的变量用法相同。
![](https://box.kancloud.cn/0bb7cdac91057457510d5186757af7c5_653x344.png)
图中画的只是上面示例那种情况,比如`my_function(array());`直接传值则会是 __literals区->新zend_execute_data动态变量区__ 的传递。
#### 3.3.3.3 函数调用阶段
这个过程主要是进行一些上下文切换,将执行器切换到调用的函数上。
上面例子对应的opcode为`ZEND_DO_UCALL`,handler为`ZEND_DO_UCALL_SPEC_HANDLER`:
```c
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_UCALL_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
zend_execute_data *call = EX(call);
zend_function *fbc = call->func;
zval *ret;
SAVE_OPLINE();
EX(call) = call->prev_execute_data;
EG(scope) = NULL;
ret = NULL;
call->symbol_table = NULL;
if (RETURN_VALUE_USED(opline)) {
ret = EX_VAR(opline->result.var); //函数返回值的存储位置
ZVAL_NULL(ret);
Z_VAR_FLAGS_P(ret) = 0;
}
call->prev_execute_data = execute_data; //将新zend_execute_data->prev_execute_data指向当前data
i_init_func_execute_data(call, &fbc->op_array, ret, 0);
ZEND_VM_ENTER();
}
//zend_execute.c
static zend_always_inline void i_init_func_execute_data(zend_execute_data *execute_data, zend_op_array *op_array, zval *return_value, int check_this)
{
uint32_t first_extra_arg, num_args;
ZEND_ASSERT(EX(func) == (zend_function*)op_array);
EX(opline) = op_array->opcodes;
EX(call) = NULL;
EX(return_value) = return_value;
first_extra_arg = op_array->num_args; //函数的总参数数量,示例中为3
num_args = EX_NUM_ARGS(); //实际传入参数数量,示例中为2
if (UNEXPECTED(num_args > first_extra_arg)) {
...
} else if (EXPECTED((op_array->fn_flags & ZEND_ACC_HAS_TYPE_HINTS) == 0)) {
//跳过前面几个已经传参的参数接收的指令,因为已经显式的传递参数了,无需再接收默认值
EX(opline) += num_args;
}
//初始化动态变量区,将所有变量(除已经传入的外)设置为IS_UNDEF
if (EXPECTED((int)num_args < op_array->last_var)) {
zval *var = EX_VAR_NUM(num_args);
zval *end = EX_VAR_NUM(op_array->last_var);
do {
ZVAL_UNDEF(var);
var++;
} while (var != end);
}
...
//分配运行时缓存,此机制后面再单独说明
if (UNEXPECTED(!op_array->run_time_cache)) {
op_array->run_time_cache = zend_arena_alloc(&CG(arena), op_array->cache_size);
memset(op_array->run_time_cache, 0, op_array->cache_size);
}
EX_LOAD_RUN_TIME_CACHE(op_array); //execute_data.run_time_cache = op_array.run_time_cache
EX_LOAD_LITERALS(op_array); //execute_data.literals = op_array.literals
//EG(current_execute_data)为执行器当前执行空间,将执行器切到函数内
EG(current_execute_data) = execute_data;
}
```
![](https://box.kancloud.cn/4171ebae6bccd9858e336f6ac422ca46_508x393.png)
#### 3.3.3.4 函数执行阶段
这个过程就是函数内部opcode的执行流程,没什么特别的,唯一的不同就是前面会接收未传的参数,如下图所示。
![](https://box.kancloud.cn/af2c564e5b27965ce15616baa04bcb30_363x232.png)
#### 3.3.3.5 函数返回阶段
实际此过程可以认为是3.3.3.4的一部分,这个阶段就是函数调用结束,返回调用处的过程,这个过程中有三个关键工作:拷贝返回值、执行器切回调用位置、释放清理局部变量。
上面例子此过程opcode为`ZEND_RETURN`,对应的handler为`ZEND_RETURN_SPEC_CV_HANDLER`:
```c
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_RETURN_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
zval *retval_ptr;
zend_free_op free_op1;
//获取返回值
retval_ptr = _get_zval_ptr_cv_undef(execute_data, opline->op1.var);
if (IS_CV == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(retval_ptr) == IS_UNDEF)) {
//返回值未定义,返回NULL
retval_ptr = GET_OP1_UNDEF_CV(retval_ptr, BP_VAR_R);
if (EX(return_value)) {
ZVAL_NULL(EX(return_value));
}
} else if(!EX(return_value)){
//无返回值
...
}else{ //返回值正常
...
ZVAL_DEREF(retval_ptr); //如果retval_ptr是引用则将找到其具体引用的zval
ZVAL_COPY(EX(return_value), retval_ptr); //将返回值复制给调用方接收值:EX(return_value)
...
}
ZEND_VM_TAIL_CALL(zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));
}
```
继续看下`zend_leave_helper_SPEC`,执行器切换、局部变量清理就是在这个函数中完成的。
```c
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS)
{
zend_execute_data *old_execute_data;
uint32_t call_info = EX_CALL_INFO();
if (EXPECTED(ZEND_CALL_KIND_EX(call_info) == ZEND_CALL_NESTED_FUNCTION)) {
//普通的函数调用将走到这个分支
i_free_compiled_variables(execute_data);
...
}
//include、eval及整个脚本的结束(main函数)走到下面
//...
//将执行器切回调用的位置
EG(current_execute_data) = EX(prev_execute_data);
}
//zend_execute.c
//清理局部变量的过程
static zend_always_inline void i_free_compiled_variables(zend_execute_data *execute_data)
{
zval *cv = EX_VAR_NUM(0);
zval *end = cv + EX(func)->op_array.last_var;
while (EXPECTED(cv != end)) {
if (Z_REFCOUNTED_P(cv)) {
if (!Z_DELREF_P(cv)) { //引用计数减一后为0
zend_refcounted *r = Z_COUNTED_P(cv);
ZVAL_NULL(cv);
zval_dtor_func_for_ptr(r); //释放变量值
} else {
GC_ZVAL_CHECK_POSSIBLE_ROOT(cv); //引用计数减一后>0,启动垃圾检查机制,清理循环引用导致无法回收的垃圾
}
}
cv++;
}
}
```
除了函数调用完成时有return操作,其它还有两种情况也会有此过程:
* __1.PHP主脚本执行结束时:__ 也就是PHP脚本开始执行的入口脚本(PHP没有显式的main函数,这种就可以认为是main函数),但是这种情况并不会在return时清理,因为在main函数中定义的变量并非纯碎的局面变量,它们都是全局变量,与$__GET、$__POST是一类,这些全局变量的清理是在request_shutdown阶段处理
* __2.include、eval:__ 以include为例,如果include的文件中定义了全局变量,那么这些变量实际与上面1的情况一样,它们的存储位置是在一起的
所以实际上面说的这两种情况属于一类,它们并不是局部变量的清理,而是 __全局变量的清理__ ,另外局部变量的清理也并非只有return一个时机,另外还有一个更重要的时机就是变量分离时,这种情况我们在《PHP语法实现》一节再具体说明。
- 前言
- 第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超时控制的思考