diff --git a/1/fpm.md b/1/fpm.md index 49f961f..cea236c 100644 --- a/1/fpm.md +++ b/1/fpm.md @@ -236,8 +236,8 @@ worker处理到各个阶段时将会把当前阶段更新到`fpm_scoreboard_proc ```c void fpm_event_loop(int err) { - //创建一个io read的监听事件,这里监听的就是在fpm_init()阶段中通过socketpair()创建管道sp[1] - //当sp[1]可读时将回调fpm_got_signal() + //创建一个io read的监听事件,这里监听的就是在fpm_init()阶段中通过socketpair()创建管道sp[0] + //当sp[0]可读时将回调fpm_got_signal() fpm_event_set(&signal_fd_event, fpm_signals_get_fd(), FPM_EV_READ, &fpm_got_signal, NULL); fpm_event_add(&signal_fd_event, 0); diff --git a/2/global_var.md b/2/global_var.md index a6e90fc..e9515d4 100644 --- a/2/global_var.md +++ b/2/global_var.md @@ -1,7 +1,5 @@ ## 2.4 全局变量 -PHP中在函数、类之外直接定义的变量可以在函数、类成员方法中通过global关键词引入使用,这些变量称为:全局变量。 - -这些直接在PHP中定义的变量(包括include、require文件中的)相对于函数、类方法而言它们是全局变量,但是对自身执行域zend_execute_data而言它们是普通的局部变量,自身执行时它们与普通变量的读写方式完全相同。 +PHP中把定义在函数、类之外的变量称之为全局变量,也就是定义在主脚本中的变量,这些变量可以在函数、成员方法中通过global关键字引入使用。 ```php function test() { @@ -64,7 +62,7 @@ global $id; // 相当于:$id = & EG(symbol_table)["id"]; ![](../img/zend_global_ref.png) ### 2.4.3 超全局变量 -全部变量除了通过global引入外还有一类特殊的类型,它们不需要使用global引入而可以直接使用,这些全局变量称为:超全局变量。 +全局变量除了通过global引入外还有一类特殊的类型,它们不需要使用global引入而可以直接使用,这些全局变量称为:超全局变量。 超全局变量实际是PHP内核定义的一些全局变量:$GLOBALS、$_SERVER、$_REQUEST、$_POST、$_GET、$_FILES、$_ENV、$_COOKIE、$_SESSION、argv、argc。 diff --git a/2/static_var.md b/2/static_var.md index d382cb4..c134cf2 100644 --- a/2/static_var.md +++ b/2/static_var.md @@ -87,7 +87,7 @@ if (by_ref) { * __ZEND_FETCH_W:__ 这条opcode对应的操作是创建一个IS_INDIRECT类型的zval,指向static_variables中对应静态变量的zval * __ZEND_ASSIGN_REF:__ 它的操作是引用赋值,即将一个引用赋值给CV变量 -通过上面两条opcode可以确定静态变量的读写过程:首先根据变量名在static_variables中取出对应的zval,然后将它修改为引用类型并赋值给局部变量,也就是说`static $count = 4;`包含了两个操作,严格的将`$count`并不是真正的静态变量,它只是一个指向静态变量的局部变量,执行时实际操作是:`$count = & static_variables["count"];`。上面例子$count与static_variables["count"]间的关系如图所示。 +通过上面两条opcode可以确定静态变量的读写过程:首先根据变量名在static_variables中取出对应的zval,然后将它修改为引用类型并赋值给局部变量,也就是说`static $count = 4;`包含了两个操作,严格的说`$count`并不是真正的静态变量,它只是一个指向静态变量的局部变量,执行时实际操作是:`$count = & static_variables["count"];`。上面例子$count与static_variables["count"]间的关系如图所示。 ![](../img/zend_static_ref.png) diff --git a/2/zend_constant.md b/2/zend_constant.md index fad305c..eca3f65 100644 --- a/2/zend_constant.md +++ b/2/zend_constant.md @@ -8,7 +8,7 @@ PHP中的常量通过`define()`函数定义: define('CONST_VAR_1', 1234); ``` ### 2.5.1 常量的存储 -在内核中常量存储在`EG(zend_constant)`哈希表中,访问时也是根据常量名直接到哈希表中查找,其实现比较简单。 +在内核中常量存储在`EG(zend_constants)`哈希表中,访问时也是根据常量名直接到哈希表中查找,其实现比较简单。 常量的数据结构: ```c diff --git a/2/zend_ht.md b/2/zend_ht.md index 95d8177..56c818c 100644 --- a/2/zend_ht.md +++ b/2/zend_ht.md @@ -61,6 +61,8 @@ unset($arr["c"]); ![](../img/zend_hash_1.png) +> 图中Bucket的zval.u2.next默认值应该为-1,不是0 + ### 2.2.2 映射函数 映射函数(即:散列函数)是散列表的关键部分,它将key与value建立映射关系,一般映射函数可以根据key的哈希值与Bucket数组大小取模得到,即`key->h % ht->nTableSize`,但是PHP却不是这么做的: ```c diff --git a/2/zval.md b/2/zval.md index 6f96b7e..dd82bc0 100644 --- a/2/zval.md +++ b/2/zval.md @@ -278,7 +278,7 @@ $b[] = 3; ![zval_sep](../img/zval_sep.png) -不是所有类型都可以copy的,比如对象、资源,实时上只有string、array两种支持,与引用计数相同,也是通过`zval.u1.type_flag`标识value是否可复制的: +不是所有类型都可以copy的,比如对象、资源,事实上只有string、array两种支持,与引用计数相同,也是通过`zval.u1.type_flag`标识value是否可复制的: ```c #define IS_TYPE_COPYABLE (1<<4) ``` diff --git a/3/function_implement.md b/3/function_implement.md index 5373d41..ef4bcb5 100644 --- a/3/function_implement.md +++ b/3/function_implement.md @@ -8,7 +8,7 @@ function my_func(){ ... } ``` -汇编中函数对应的是一组独立的汇编指令,然后通过call指令实现函数的调用,前面已经说过PHP编译的结果是opcode数组,与汇编指令对应,PHP用户自定义函数的实现就是将函数编译为独立的opcode数组,调用时分配独立的执行栈依次执行opcode,所以自定义函数对于zend而言并没有什么特别之处,只是将opcode进行了打包封装,实际PHP脚本中函数之外的指令整个可以认为是一个函数(或者理解为main函数更直观)。 +汇编中函数对应的是一组独立的汇编指令,然后通过call指令实现函数的调用。前面已经说过PHP编译的结果是opcode数组,与汇编指令对应。PHP用户自定义函数的实现就是将函数编译为独立的opcode数组,调用时分配独立的执行栈依次执行opcode,所以自定义函数对于zend而言并没有什么特别之处,只是将opcode进行了打包封装。PHP脚本中函数之外的指令,整个可以认为是一个函数(或者理解为main函数更直观)。 ```php /* function main(){ */ @@ -287,7 +287,7 @@ $greet = function($name) $greet('World'); $greet('PHP'); ``` -这里提函数函数只是想说明编译函数时那个use的用法: +这里提匿名函数只是想说明编译函数时那个use的用法: __匿名函数可以从父作用域中继承变量。 任何此类变量都应该用 use 语言结构传递进去。__ diff --git a/3/zend_class.md b/3/zend_class.md index e0bc8f5..62eea77 100644 --- a/3/zend_class.md +++ b/3/zend_class.md @@ -1,7 +1,7 @@ ### 3.4.1 类 类是现实世界或思维世界中的实体在计算机中的反映,它将某些具有关联关系的数据以及这些数据上的操作封装在一起。在面向对象中类是对象的抽象,对象是类的具体实例。 -在PHP中类编译阶段的产物,而对象是运行时产生的,它们归属于不同阶段。 +在PHP中类是编译阶段的产物,而对象是运行时产生的,它们归属于不同阶段。 PHP中我们这样定义一个类: ```php @@ -416,7 +416,7 @@ void zend_compile_class_const_decl(zend_ast *ast) zend_class_entry *ce = CG(active_class_entry); uint32_t i; - for (i = 0; i < list->children; ++i) { //不清楚这个地方为什么要用list,试了几个例子这个节点都只有一个child,即for只循环一次 + for (i = 0; i < list->children; ++i) { //const声明了多个常量,遍历编译每个子节点 zend_ast *const_ast = list->child[i]; zend_ast *name_ast = const_ast->child[0]; //常量名节点 zend_ast *value_ast = const_ast->child[1];//常量值节点 @@ -446,7 +446,6 @@ void zend_compile_prop_decl(zend_ast *ast) zend_class_entry *ce = CG(active_class_entry); uint32_t i, children = list->children; - //也不清楚这里为啥用循环,测试的情况child只有一个 for (i = 0; i < children; ++i) { zend_ast *prop_ast = list->child[i]; //这个节点类型为:ZEND_AST_PROP_ELEM zend_ast *name_ast = prop_ast->child[0]; //属性名节点 diff --git a/3/zend_compile_opcode.md b/3/zend_compile_opcode.md index a8fde50..6969c1a 100644 --- a/3/zend_compile_opcode.md +++ b/3/zend_compile_opcode.md @@ -516,7 +516,7 @@ void zend_compile_expr(znode *result, zend_ast *ast) ``` >> __第3步:__ 上面两步已经分别生成了变量赋值的op1、op2,下面就是根据这俩值生成opcode的过程。 ```c -tatic zend_op *zend_emit_op(znode *result, zend_uchar opcode, znode *op1, znode *op2) +static zend_op *zend_emit_op(znode *result, zend_uchar opcode, znode *op1, znode *op2) { zend_op *opline = get_next_op(CG(active_op_array)); //当前zend_op_array下生成一条新的指令 opline->opcode = opcode; diff --git a/3/zend_executor.md b/3/zend_executor.md index d589492..b1ed95d 100644 --- a/3/zend_executor.md +++ b/3/zend_executor.md @@ -62,7 +62,7 @@ ZEND_API zend_executor_globals executor_globals; struct _zend_execute_data { const zend_op *opline; //指向当前执行的opcode,初始时指向zend_op_array起始位置 zend_execute_data *call; /* current call */ - zval *return_value; //返回值指针 */ + zval *return_value; //返回值指针 zend_function *func; //当前执行的函数(非函数调用时为空) zval This; //这个值并不仅仅是面向对象的this,还有另外两个值也通过这个记录:call_info + num_args,分别存在zval.u1.reserved、zval.u2.num_args zend_class_entry *called_scope; //当前call的类 diff --git a/3/zend_global_register.md b/3/zend_global_register.md new file mode 100644 index 0000000..9c9462e --- /dev/null +++ b/3/zend_global_register.md @@ -0,0 +1,173 @@ +### 3.3.4 全局execute_data和opline +Zend执行器在opcode的执行过程中,会频繁的用到execute_data和opline两个变量,execute_data为zend_execute_data结构,opline为当前执行的指令。普通的处理方式在执行每条opcode指令的handler时,会把execute_data地址作为参数传给handler使用,使用时先从当前栈上获取execute_data地址,然后再从堆上获取变量的数据,这种方式下Zend执行器展开后是下面这样: +```c +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)execute_data->opline->handler)(execute_data)) != 0)) { + if (EXPECTED(ret > 0)) { + execute_data = EG(current_execute_data); + } else { + return; + } + } + } +} +``` +执行器实际是一个大循环,从第一条opcode开始执行,execute_data->opline指向当前执行的指令,执行完以后指向下一条指令,opline类似eip(或rip)寄存器的作用。通过这个循环,ZendVM完成opcode指令的执行。opcode执行完后以后指向下一条指令的操作是在当前handler中完成,也就是说每条执行执行完以后会主动更新opline,这里会有下面几个不同的动作: +```c +#define ZEND_VM_CONTINUE() return 0 +#define ZEND_VM_ENTER() return 1 +#define ZEND_VM_LEAVE() return 2 +#define ZEND_VM_RETURN() return -1 +``` +ZEND_VM_CONTINUE()表示继续执行下一条opcode;ZEND_VM_ENTER()/ZEND_VM_LEAVE()是调用函数时的动作,普通模式下ZEND_VM_ENTER()实际就是return 1,然后execute_ex()中会将execute_data切换到被调函数的结构上,对应的,在函数调用完成后ZEND_VM_LEAVE()会return 2,再将execute_data切换至原来的结构;ZEND_VM_RETURN()表示执行完成,返回-1给execute_ex(),比如exit,这时候execute_ex()将退出执行。下面看一个具体的例子: +```php +$a = "hi~"; +echo $a; +``` +执行过程如下图所示: + +![](../img/executor.png) + +以ZEND_ASSIGN这条赋值指令为例,其handler展开前如下: +```c +static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ASSIGN_SPEC_CV_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + ... + ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); +} +``` +所有opcode的handler定义格式都是相同的,其参数列表通过ZEND_OPCODE_HANDLER_ARGS宏定义,展开后实际只有一个execute_data,展开后: +```c +static int ZEND_ASSIGN_SPEC_CV_CONST_HANDLER(zend_execute_data *execute_data) +{ + //USE_OPLINE + const zend_op *opline = execute_data->opline; + ... + + //ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION() + execute_data->opline = execute_data->opline + 1; + return 0; +} +``` +从这个例子可以很清楚的看到,执行完以后会将execute_data->opline加1,也就是指向下一条opcode,然后返回0给execute_ex(),接着执行器在下一次循环时执行下一条opcode,依次类推,直至所有的opcode执行完成。这个处理过程比较简单,并没有不好理解的地方,而且整个过程看起来也都那么顺理成章。PHP7针对execute_data、opline两个变量的存储位置进行了优化,那就是使用全局寄存器保存这两个变量的地址,以实现更高效率的读取。这种方式下execute_data、opline直接从寄存器读取地址,在性能上大概有5%的提升(官方说法)。在分析PHP7的优化之前,我们先简单介绍下什么是寄存器变量。 + +寄存器变量存放在CPU的寄存器中,使用时,不需要访问内存直接从寄存器中读写,与存储在内存中的变量相比,寄存器变量具有更快的访问速度,在计算机的存储层次中,寄存器的速度最快,其次是内存,最慢的是硬盘。C语言中使用关键字register来声明局部变量为寄存器变量,需要注意的是,只有局部自动变量和形式参数才能够被定义为寄存器变量,全局变量和局部静态变量都不能被定义为寄存器变量。而且,一个计算机中寄存器数量是有限的,一般为2到3个,因此寄存器变量的数量不能太多。对于在一个函数中说明的多于2到3个的寄存器变量,C编译程序会自动地将寄存器变量变为自动变量。 受硬件寄存器长度的限制,寄存器变量只能是char、int或指针型,而不能使其他复杂数据类型。由于register变量使用的是硬件CPU中的寄存器,寄存器变量无地址,所以不能使用取地址运算符"&"求寄存器变量的地址。 + +GCC从4.8.0版本开始支持了另外一项特性:全局寄存器变量(Global Register Variables,[详细介绍](https://gcc.gnu.org/onlinedocs/gcc-6.1.0/gcc/Global-Register-Variables.html)),也就是可以把全局变量定义为寄存器变量,从而可以实现函数间共享数据。可以通过下面的语法告诉编译器使用寄存器来保存数据: +```c +register int *foo asm ("r12"); //r12、%r12 +``` +或者: +```c +register int *foo __asm__ ("r12"); //r12、%r12 +``` +这里r12就是指定使用的寄存器,它必须是运行平台上有效的寄存器,这样就可以像使用普通的变量一样使用foo,但是foo同样没有地址,也就是无法通过&获取它的地址,在gdb调试时也无法使用foo符号,只能使用对应的寄存器获取数据。举个例子来看: +```c +//main.c +#include + +typedef struct _execute_data { + int ip; +}zend_execute_data; + + +register zend_execute_data* execute_data __asm__ ("%r14"); + +int main(void) +{ + execute_data = (zend_execute_data *)malloc(sizeof(zend_execute_data)); + execute_data->ip = 9999; + + return 0; +} +``` +编译:`$ gcc -o main -g main.c`,然后通过gdb看下: +```sh +$ gdb main +(gdb) break main +(gdb) r +Starting program: /home/qinpeng/c/php/main + +Breakpoint 1, main () at main.c:12 +12 execute_data = (zend_execute_data *)malloc(sizeof(zend_execute_data)); +(gdb) n +13 execute_data->ip = 9999; +(gdb) n +15 return 0; +``` +这时我们就无法再像普通变量那样直接使用execute_data访问数据,只能通过r14寄存器读取: +```sh +(gdb) p execute_data +Missing ELF symbol "execute_data". +(gdb) info register r14 +r14 0x601010 6295568 +(gdb) p ((zend_execute_data *)$r14)->ip +$3 = 9999 +``` +了解完全局寄存器变量,接下来我们再回头看下PHP7中的用法,处理也比较简单,就是在execute_ex()执行各opcode指令的过程中,不再将execute_data作为参数传给handler,而是通过寄存器保存execute_data及opline的地址,handler使用时直接从全局变量(寄存器)读取,执行完再把下一条指令更新到全局变量。 + +该功能需要GCC 4.8+支持,默认开启,可以通过 --disable-gcc-global-regs 编译参数关闭。以x86_64为例,execute_data使用r14寄存器,opline使用r15寄存器: +```c +//file: zend_execute.c line: 2631 +# define ZEND_VM_FP_GLOBAL_REG "%r14" +# define ZEND_VM_IP_GLOBAL_REG "%r15" + +//file: zend_vm_execute.h line: 315 +register zend_execute_data* volatile execute_data __asm__(ZEND_VM_FP_GLOBAL_REG); +register const zend_op* volatile opline __asm__(ZEND_VM_IP_GLOBAL_REG); +``` +execute_data、opline定义为全局变量,下面看下execute_ex()的变化,展开后: +```c +ZEND_API void execute_ex(zend_execute_data *ex) +{ + const zend_op *orig_opline = opline; + zend_execute_data *orig_execute_data = execute_data; + + //将当前execute_data、opline保存到全局变量 + execute_data = ex; + opline = execute_data->opline + + while (1) { + ((opcode_handler_t)opline->handler)(); + + if (UNEXPECTED(!opline)) { + execute_data = orig_execute_data; + opline = orig_opline; + + return; + } + } +} +``` +这个时候调用各opcode指令的handler时就不再传入execute_data的参数了,handler使用时直接从全局变量读取,仍以上面的赋值ZEND_ASSIGN指令为例,handler展开后: +```c +static int ZEND_ASSIGN_SPEC_CV_CONST_HANDLER(void) +{ + ... + + //ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION() + opline = execute_data->opline + 1; + return; +} +``` +当调用函数时,会把execute_data、opline更新为被调函数的,然后回到execute_ex()开始执行被调函数的指令: +```c +# define ZEND_VM_ENTER() execute_data = EG(current_execute_data); LOAD_OPLINE(); ZEND_VM_CONTINUE() +``` +展开后: +```c +//ZEND_VM_ENTER() +execute_data = execute_data->current_execute_data; +opline = execute_data->opline; +return; +``` +这两种处理方式并没有本质上的差异,只是通过全局寄存器变量提升了一些性能。 + +> __Note:__ automake编译时的命令是cc,而不是gcc,如果更新gcc后发现PHP仍然没有支持这个特性,请检查下cc是否指向了新的gcc diff --git a/3/zend_object.md b/3/zend_object.md index cf44ef7..756d2fa 100644 --- a/3/zend_object.md +++ b/3/zend_object.md @@ -437,7 +437,7 @@ static int zend_std_compare_objects(zval *o1, zval *o2) "==="的比较通过函数`zend_is_identical()`处理,比较简单,这里不再展开。 #### 3.4.2.6 对象的销毁 -object与string、array等类型不同,它是个符合类型,所以它的销毁过程更加复杂,赋值、函数调用结束或主动unset等操作中如果发现object引用计数为0则将触发销毁动作。 +object与string、array等类型不同,它是个复合类型,所以它的销毁过程更加复杂,赋值、函数调用结束或主动unset等操作中如果发现object引用计数为0则将触发销毁动作。 ```php //情况1 $obj1 = new my_function(); diff --git a/4/break.md b/4/break.md index 5bfc256..47ec5b8 100644 --- a/4/break.md +++ b/4/break.md @@ -19,7 +19,15 @@ typedef struct _zend_brk_cont_element { ``` cont记录的是当前循环判断条件opcode起始位置,brk记录的是当前循环结束的位置,parent记录的是父层循环`zend_brk_cont_element`结构的存储位置,也就是说多层嵌套循环会生成一个`zend_brk_cont_element`的链表,每层循环编译结束时更新自己的`zend_brk_cont_element`结构,所以break、continue的处理过程实际就是根据跳出的层级索引到那一层的`zend_brk_cont_element`结构,然后得到它的cont、brk进行相应的opcode跳转。 -各循环的`zend_brk_cont_element`结构保存在`zend_op_array->brk_cont_array`数组中,实际这个数组在编译前就已经分配好了,编译各循环时依次申请一个`zend_brk_cont_element`,`zend_op_array->last_brk_cont`记录此数组第一个可用位置,每申请一个元素last_brk_cont就相应的增加1,parent记录的就是父层循环在`zend_op_array->brk_cont_array`中的位置。 +各循环的`zend_brk_cont_element`结构保存在`zend_op_array->brk_cont_array`数组中,编译各循环时依次申请一个`zend_brk_cont_element`,`zend_op_array->last_brk_cont`记录此数组第一个可用位置,每申请一个元素last_brk_cont就相应的增加1,然后将数组扩容,parent记录的就是父层循环结构在该数组中的存储位置。 +```c +zend_brk_cont_element *get_next_brk_cont_element(zend_op_array *op_array) +{ + op_array->last_brk_cont++; + op_array->brk_cont_array = erealloc(op_array->brk_cont_array, sizeof(zend_brk_cont_element)*op_array->last_brk_cont); + return &op_array->brk_cont_array[op_array->last_brk_cont-1]; +} +``` 示例: ```php diff --git a/4/loop.md b/4/loop.md index 19ce394..f698d0e 100644 --- a/4/loop.md +++ b/4/loop.md @@ -241,7 +241,7 @@ foreach($arr as $k=>$v){ 了解了foreach的实现、运行机制我们再回头看下其编译过程: -* __(1)__ 编译"拷贝"数组/对象操作的opcode:`ZEND_FE_RESET_R`,如果value是引用则是`ZEND_FE_RESET_RW`,执行时如果发现数组或对象属性为空则直接跳出遍历,所以这条opcode还需要知道跳出的位置,这个位置需要编译完foreach以后才能确定; +* __(1)__ 编译拷贝数组、对象操作的指令:ZEND_FE_RESET_R,如果value是引用则是ZEND_FE_RESET_RW。执行时如果发现遍历的变量不是数组、对象,则抛出一个warning,然后跳出循环,所以这条指令还需要知道跳出的位置,这个位置需要编译完foreach以后才能确定; * __(2)__ 编译fetch数组/对象当前单元key、value的opcode:`ZEND_FE_FETCH_R`,如果是引用则是`ZEND_FE_FETCH_RW`,此opcode还需要知道当遍历已经到达数组末尾时跳出遍历的位置,与步骤(1)的opcode相同,另外还有一个关键操作,前面已经说过遍历的key、value实际就是普通的局部变量,它们的内存存储位置正是在这一步分配确定的,分配过程与普通局部变量的过程完全相同,如果value不是一个CV变量(比如:foreach($arr as $v["xx"]){...})则还会编译其它操作的opcode; * __(3)__ 如果foreach定义了key则编译一条赋值opcode,此操作是对key进行赋值; * __(4)__ 编译循环体statement; diff --git a/5/zend_alloc.md b/5/zend_alloc.md index 5fe35aa..df3fa05 100644 --- a/5/zend_alloc.md +++ b/5/zend_alloc.md @@ -151,7 +151,56 @@ static void *zend_mm_alloc_huge(zend_mm_heap *heap, size_t size ZEND_FILE_LINE_D return ptr; } ``` -huge的分配过程还是比较简单的。 +huge的分配实际就是分配多个chunk,chunk的分配也是large、small内存分配的基础,它是ZendMM向系统申请内存的唯一粒度。在申请chunk内存时有一个关键操作,那就是将内存地址对齐到ZEND_MM_CHUNK_SIZE,也就是说申请的chunk地址都是ZEND_MM_CHUNK_SIZE的整数倍,注意:这里说的内存对齐值并不是系统的字节对齐值,所以需要在申请后自己调整下。ZendMM的处理方法是:先按实际要申请的内存大小申请一次,如果系统分配的地址恰好是ZEND_MM_CHUNK_SIZE的整数倍那么就不需要调整了,直接返回使用;如果不是ZEND_MM_CHUNK_SIZE的整数倍,ZendMM会把这块内存释放掉,然后按照"实际要申请的内存大小+ZEND_MM_CHUNK_SIZE"的大小重新申请一块内存,多申请的ZEND_MM_CHUNK_SIZE大小的内存是用来调整的,ZendMM会从系统分配的地址向后偏移到ZEND_MM_CHUNK_SIZE的整数倍位置,调整完以后会把多余的内存再释放掉,如下图所示,虚线部分为alignment大小的内容,灰色部分为申请的内容大小,系统返回的地址为ptr1,而实际使用的内存是从ptr2开始的。 + +![](../img/chunk_alloc.png) + +下面看下chunk的具体分配过程: +```c +//size为申请内存的大小,alignment为内存对齐值,一般为ZEND_MM_CHUNK_SIZE +static void *zend_mm_chunk_alloc_int(size_t size, size_t alignment) +{ + //向系统申请size大小的内存 + void *ptr = zend_mm_mmap(size); + if (ptr == NULL) { + return NULL; + } else if (ZEND_MM_ALIGNED_OFFSET(ptr, alignment) == 0) {//判断申请的内存是否为alignment的整数倍 + //是的话直接返回 + return ptr; + }else{ + //申请的内存不是按照alignment对齐的,注意这里的alignment并不是系统的字节对齐值 + size_t offset; + + //将申请的内存释放掉重新申请 + zend_mm_munmap(ptr, size); + //重新申请一块内存,这里会多申请一块内存,用于截取到alignment的整数倍,可以忽略REAL_PAGE_SIZE + ptr = zend_mm_mmap(size + alignment - REAL_PAGE_SIZE); + //offset为ptr距离上一个alignment对齐内存位置的大小,注意不能往前移,因为前面的内存都是分配了的 + offset = ZEND_MM_ALIGNED_OFFSET(ptr, alignment); + if (offset != 0) { + offset = alignment - offset; + zend_mm_munmap(ptr, offset); + //偏移ptr,对齐到alignment + ptr = (char*)ptr + offset; + alignment -= offset; + } + if (alignment > REAL_PAGE_SIZE) { + zend_mm_munmap((char*)ptr + size, alignment - REAL_PAGE_SIZE); + } + return ptr; + } +} +``` +这个过程中用到了一个宏: +```c +#define ZEND_MM_ALIGNED_OFFSET(size, alignment) \ + (((size_t)(size)) & ((alignment) - 1)) +``` +这个宏的作用是计算按alignment对齐的内存地址距离上一个alignment整数倍内存地址的大小,alignment必须为2的n次方,比如一段n*alignment大小的内存,ptr为其中一个位置,那么就可以通过位运算计算得到ptr所属内存块的offset: + +![](../img/align.png) + +这个位运算是因为alignment为2^n,所以可以通过alignment取到最低位的位置,也就是相对上一个整数倍alignment的offset,实际如果不用运算的话可以通过:`offset = (ptr/alignment取整)*alignment - ptr`得到,这个更容易理解些。 #### 5.1.3.2 Large分配 大于3/4的page_size(4KB)且小于等于511个page_size的内存申请,也就是一个chunk的大小够用(之所以是511个page而不是512个是因为第一个page始终被chunk结构占用),__如果申请多个page的话 分配的时候这些page都是连续的__ 。 @@ -352,7 +401,9 @@ ZEND_API void ZEND_FASTCALL _efree(void *ptr ZEND_FILE_LINE_DC ZEND_FILE_LINE_OR static zend_always_inline void zend_mm_free_heap(zend_mm_heap *heap, void *ptr ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC) { - size_t page_offset = ZEND_MM_ALIGNED_OFFSET(ptr, ZEND_MM_CHUNK_SIZE); //根据内存地址及对齐值判断内存地址偏移量是否为0,是的话只有huge情况符合,page、slot分配出的内存地址偏移量一定是>=ZEND_MM_CHUNK_SIZE的,因为第一页始终被chunk自身结构占用,不可能分配出去 + //根据内存地址及对齐值判断内存地址偏移量是否为0,是的话只有huge情况符合,page、slot分配出的内存地>址偏移量一定是>=ZEND_MM_CHUNK_SIZE的,因为第一页始终被chunk自身结构占用,不可能分配出去 + //offset就是ptr距离当前chunk起始位置的偏移量 + size_t page_offset = ZEND_MM_ALIGNED_OFFSET(ptr, ZEND_MM_CHUNK_SIZE); if (UNEXPECTED(page_offset == 0)) { if (ptr != NULL) { @@ -360,6 +411,7 @@ static zend_always_inline void zend_mm_free_heap(zend_mm_heap *heap, void *ptr Z zend_mm_free_huge(heap, ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC); } } else { //page或slot,根据chunk->map[]值判断当前page的分配类型 + //根据ptr获取chunk的起始位置 zend_mm_chunk *chunk = (zend_mm_chunk*)ZEND_MM_ALIGNED_BASE(ptr, ZEND_MM_CHUNK_SIZE); int page_num = (int)(page_offset / ZEND_MM_PAGE_SIZE); zend_mm_page_info info = chunk->map[page_num]; @@ -377,6 +429,8 @@ static zend_always_inline void zend_mm_free_heap(zend_mm_heap *heap, void *ptr Z } } ``` +释放的内存地址可能是chunk中间的任意位置,因为chunk分配时是按照ZEND_MM_CHUNK_SIZE对齐的,也就是chunk的起始内存地址一定是ZEND_MM_CHUNK_SIZE的整数倍,所以可以根据chunk上的任意位置知道chunk的起始位置。 + 释放page的过程有一个地方值得注意,如果释放后发现当前chunk所有page都已经被释放则可能会释放所在chunk,还记得heap->cached_chunks吗?内存池会维持一定的chunk数,每次释放并不会直接销毁而是加入到cached_chunks中,这样下次申请chunk时直接就用了,同时为了防止占用过多内存,cached_chunks会根据每次request请求计算的chunk使用均值保证其维持在一定范围内。 每次request请求结束会对内存池进行一次清理,检查cache的chunk数是否超过均值,超过的话就进行清理,具体的操作:`zend_mm_shutdown`,这里不再展开。 diff --git a/7/extension_intro.md b/7/extension_intro.md index f6aafc8..020a179 100644 --- a/7/extension_intro.md +++ b/7/extension_intro.md @@ -270,7 +270,7 @@ __(5)PHP_ADD_INCLUDE(path):__ 添加include路径,即:`gcc -Iinclude_dir`, __(6)PHP_CHECK_LIBRARY(library, function [, action-found [, action-not-found [, extra-libs]]]):__ 检查依赖的库中是否存在需要的function,action-found为存在时执行的动作,action-not-found为不存在时执行的动作,比如扩展里使用到线程pthread,检查pthread_create(),如果没找到则终止./configure执行: ```sh -PHP_ADD_INCLUDE(pthread, pthread_create, [], [ +PHP_CHECK_LIBRARY(pthread, pthread_create, [], [ AC_MSG_ERROR([not find pthread_create() in lib pthread]) ]) ``` diff --git a/7/func.md b/7/func.md index 0807f69..4c4a47c 100644 --- a/7/func.md +++ b/7/func.md @@ -67,7 +67,7 @@ const zend_function_entry mytest_functions[] = { #define ZEND_FENTRY(zend_name, name, arg_info, flags) { #zend_name, name, arg_info, (uint32_t) (sizeof(arg_info)/sizeof(struct _zend_internal_arg_info)-1), flags }, #define ZEND_FN(name) zif_##name ``` -最后将`zend_module_entry->functions`设置为`timeout_functions`即可: +最后将`zend_module_entry->functions`设置为`mytest_functions`即可: ```c zend_module_entry mytest_module_entry = { STANDARD_MODULE_HEADER, @@ -123,7 +123,7 @@ PHP_FUNCTION(my_func_1) ![](../img/internal_func_param.png) -注意:解析时除了整形、浮点型、布尔型是直接硬拷贝value外,其它解析到的变量只能是指针,arr为zend_execute_data上param_1的地址,即:`zval *arr = ¶m_1`,所以图中arr、param_1之间用的不是箭头指向,也就是说参数始终存储在zend_execute_data上,解析获取的是这些参数的地址。`zend_parse_parameters()`调用了`zend_parse_va_args()`进行处理,简单看下解析过程: +注意:解析时除了整形、浮点型、布尔型是直接硬拷贝value外,其它解析到的变量只能是指针,arr为zend_execute_data上param_1的地址,即:`zval *arr = ¶m_1`,也就是说参数始终存储在zend_execute_data上,解析获取的是这些参数的地址。`zend_parse_parameters()`调用了`zend_parse_va_args()`进行处理,简单看下解析过程: ```c //va就是定义的要解析到的各个变量的地址 static int zend_parse_va_args(int num_args, const char *type_spec, va_list *va, int flags) @@ -229,7 +229,7 @@ static zend_always_inline int zend_parse_arg_long(zval *arg, zend_long *dest, ze return 1; } ``` -> __Note:__ "l"与"L"的区别在于,当传参不是整形且转为整形后超过了整形的大小范围时,"L"将值调整为整形的最大或最小值,而"l"将报错,比如传的参数是字符串"9223372036854775808",转整形后超过了unsigned int64的最大值:0xFFFFFFFFFFFFFFFF,"L"将解析为0xFFFFFFFFFFFFFFFF。 +> __Note:__ "l"与"L"的区别在于,当传参不是整形且转为整形后超过了整形的大小范围时,"L"将值调整为整形的最大或最小值,而"l"将报错,比如传的参数是字符串"9223372036854775808"(0x7FFFFFFFFFFFFFFF + 1),转整形后超过了有符号int64的最大值:0x7FFFFFFFFFFFFFFF,所以如果是"L"将解析为0x7FFFFFFFFFFFFFFF。 #### 7.6.2.2 布尔型:b 通过"b"标识符表示将传入的参数解析为布尔型,解析到的变量必须是zend_bool: @@ -355,7 +355,7 @@ callable指函数或成员方法,如果参数是函数名称字符串、array( zend_fcall_info callable; //注意,这两个结构不能是指针 zend_fcall_info_cache call_cache; -if(zend_parse_parameters( +if(zend_parse_parameters(ZEND_NUM_ARGS(), "f", &callable, &call_cache) == FAILURE){ RETURN_FALSE; } ``` @@ -376,8 +376,25 @@ my_func_1(array($object, 'method')); #### 7.6.2.11 其它标识符 除了上面介绍的这些解析符号以外,还有几个有特殊用法的标识符:"|"、"+"、"*",它们并不是用来表示某种数据类型的。 -* __|:__ 表示此后的参数为可选参数,可以不传,比如解析规则为:"al|b",则可以传2个或3个参数,如果是:"alb",则必须传3个,否则将报错; -* __+/*:__ 用于可变参数,注意这里与PHP函数...的用法不太一样,PHP中可以把函数最后一个参数前加...,表示调用时可以传多个参数,这些参数都会插入...参数的数组中,"*/+"也表示这个参数是可变的,但内核中只能接收一个值,即使传了多个后面那些也解析不到,"*"、"+"的区别在于"*"表示可以不传可变参数,而"+"表示可变参数至少有一个。 +* __|:__ 表示此后的参数为可选参数,可以不传,比如解析规则为:"al|b",则可以传2个或3个参数,如果是:"alb",则必须传3个,否则将报错 +* __+、* :__ 用于可变参数,`+、*`的区别在于 * 表示可以不传可变参数,而 + 表示可变参数至少有一个。可变参数将被解析到zval数组,可以通过一个整形参数,用于获取具体的数量,例如: +```c +PHP_FUNCTION(my_func_1) +{ + zval *args; + int argc; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "+", &args, &argc) == FAILURE) { + return; + } + //... +} +``` +argc获取的就是可变参数的数量,args为参数数组,指向第一个参数,可以通过args[i]获取其它参数,比如这样传参: +```php +my_func_1(array(), 1, false, "ddd"); +``` +那么传入的4个参数就可以在解析后通过args[0]、args[1]、args[2]、args[3]获取。 ### 7.6.3 引用传参 上一节介绍了如何在内部函数中解析参数,这里还有一种情况没有讲到,那就是引用传参: @@ -497,7 +514,7 @@ echo $a; ### 7.6.4 函数返回值 调用内部函数时其返回值指针作为参数传入,这个参数为`zval *return_value`,如果函数有返回值直接设置此指针即可,需要特别注意的是设置返回值时需要增加其引用计数,举个例子来看: - ```c +```c PHP_FUNCTION(my_func_1) { zval *arr; @@ -510,8 +527,6 @@ PHP_FUNCTION(my_func_1) Z_ADDREF_P(arr); //设置返回值为数组: - //return_value->u1.type = IS_ARRAY; - //return_value->value->arr = arr->value->arr; ZVAL_ARR(return_value, Z_ARR_P(arr)); } ``` diff --git a/7/implement.md b/7/implement.md index bebf3d4..7c84d1c 100644 --- a/7/implement.md +++ b/7/implement.md @@ -1,5 +1,5 @@ ## 7.2 扩展的实现原理 -PHP中扩展通过`zend_module_entry`这个结构来表示,此结构定义了扩展的全部信息:扩展名、扩展版本、扩展提供的函数列表以及PHP四个执行阶段的hook函数等,每一个扩展都需要定义一个此结构的变量,而且这个变量的名称格式必须是:`{mudule_name}_module_entry`,内核正是通过这个结构获取到扩展提供的功能的。 +PHP中扩展通过`zend_module_entry`这个结构来表示,此结构定义了扩展的全部信息:扩展名、扩展版本、扩展提供的函数列表以及PHP四个执行阶段的hook函数等,每一个扩展都需要定义一个此结构的变量,而且这个变量的名称格式必须是:`{module_name}_module_entry`,内核正是通过这个结构获取到扩展提供的功能的。 扩展可以在编译PHP时一起编译(静态编译),也可以单独编译为动态库,动态库需要加入到php.ini配置中去,然后在`php_module_startup()`阶段把这些动态库加载到PHP中: ```c diff --git a/8/namespace.md b/8/namespace.md index b9b7c6d..58ac9fe 100644 --- a/8/namespace.md +++ b/8/namespace.md @@ -259,9 +259,9 @@ typedef struct _zend_file_context { ``` 简单总结下use的几种不同用法: * __a.导入命名空间:__ 导入的名称保存在FC(imports)中,编译使用的语句时搜索此符号表进行补全 -* __b.导入类:__ 导入的名称保存在FC(imports)中,与a不同的时如果不会根据"\"切割后的最后一节检索,而是直接使用类名查找 +* __b.导入类:__ 导入的名称保存在FC(imports)中,与a不同的是不会根据"\"切割后的最后一节检索,而是直接使用类名查找 * __c.导入函数:__ 通过`use function`导入到FC(imports_function),补全时先查找FC(imports_function),如果没有找到则继续按照a的情况处理 -* __d.导入常量:__ 通过`use const`导入到FC(imports_const),不全是先查找FC(imports_const),如果没有找到则继续按照a的情况处理 +* __d.导入常量:__ 通过`use const`导入到FC(imports_const),补全时先查找FC(imports_const),如果没有找到则继续按照a的情况处理 ```php use aa\bb; //导入namespace @@ -427,7 +427,7 @@ zend_string *zend_resolve_non_class_name( return zend_prefix_with_ns(name); } ``` -可以看到,函数与常量的的补全逻辑只是优先用原始名称去FC(imports_function)或FC(imports_const)查找,如果没有找到再去FC(imports)中匹配。如果我们这样导入了一个函数:`use aa\bb\my_func;`,编译`my_func()`会在FC(imports_function)中根据"my_func"找到"aa\bb\my_func",从而使用完整的这个名称。 +可以看到,函数与常量的的补全逻辑只是优先用原始名称去FC(imports_function)或FC(imports_const)查找,如果没有找到再去FC(imports)中匹配。如果我们这样导入了一个函数:`use function aa\bb\my_func;`,编译`my_func()`会在FC(imports_function)中根据"my_func"找到"aa\bb\my_func",从而使用完整的这个名称。 ### 8.3.3 动态用法 前面介绍的这些命名空间的使用都是名称为CONST类型的情况,所有的处理都是在编译环节完成的,PHP是动态语言,能否动态使用命名空间呢?举个例子: diff --git a/README.md b/README.md index 0fcbe4d..566fac0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,13 @@ ## 反馈 [交流&吐槽](https://github.com/pangudashu/php7-internal/issues/3) [错误反馈](https://github.com/pangudashu/php7-internal/issues/2) -![](img/my_wx2.png) +## 纸质版 +
+ +
+ +[京东](https://item.jd.com/12267210.html) +[当当](http://product.dangdang.com/25185400.html) ## 目录: * 第1章 PHP基本架构 @@ -37,6 +43,7 @@ * 3.3.1 基本结构 * 3.3.2 执行流程 * 3.3.3 函数的执行流程 + * [3.3.4 全局execute_data和opline](3/zend_global_register.md) * 3.4 面向对象实现 * [3.4.1 类](3/zend_class.md) * [3.4.2 对象](3/zend_object.md) @@ -45,6 +52,10 @@ * [3.4.5 魔术方法](3/zend_magic_method.md) * [3.4.6 类的自动加载](3/zend_autoload.md) * [3.5 运行时缓存](3/zend_runtime_cache.md) + * 3.6 Opcache + * 3.6.1 opcode缓存 + * 3.6.2 opcode优化 + * 3.6.3 JIT * 第4章 PHP基础语法实现 * [4.1 类型转换](4/type.md) * [4.2 选择结构](4/if.md) @@ -85,11 +96,11 @@ * [7.7.6 数组操作](7/var.md) * [7.8 常量](7/constant.md) * 7.9 面向对象 - 7.9.1 内部类注册 - 7.9.2 定义成员属性 - 7.9.3 定义成员方法 - 7.9.4 定义常量 - 7.9.5 类的实例化 + * 7.9.1 内部类注册 + * 7.9.2 定义成员属性 + * 7.9.3 定义成员方法 + * 7.9.4 定义常量 + * 7.9.5 类的实例化 * 7.10 资源类型 * 7.11 经典扩展解析 * 7.8.1 Yaf @@ -104,6 +115,10 @@ * [8.3.2 use导入](8/namespace.md) * [8.3.3 动态用法](8/namespace.md) -## 附录 - * [附录1:break/continue按标签中断语法实现](try/break.md) +## 实现PHP新特性 + * [1、break/continue按标签中断语法实现](try/break.md) + * 2、defer语法 + * 3、协程 + * 3.1 协程的原理 + * 3.2 上下文切换 diff --git a/SUMMARY.md b/SUMMARY.md new file mode 100644 index 0000000..d26efd0 --- /dev/null +++ b/SUMMARY.md @@ -0,0 +1,108 @@ +# PHP7-internal + +## 目录 + +* 第1章 PHP基本架构 + * 1.1 PHP简介 + * 1.2 PHP7的改进 + * [1.3 FPM](1/fpm.md) + * [1.3.1 概述](1/fpm.md) + * [1.3.2 基本实现](1/fpm.md) + * [1.3.3 FPM的初始化](1/fpm.md) + * [1.3.4 请求处理](1/fpm.md) + * [1.3.5 进程管理](1/fpm.md) + * [1.4 PHP执行的几个阶段](1/base_process.md) +* 第2章 变量 + * [2.1 变量的内部实现](2/zval.md) + * [2.2 数组](2/zend_ht.md) + * [2.3 静态变量](2/static_var.md) + * [2.4 全局变量](2/global_var.md) + * [2.5 常量](2/zend_constant.md) +* 第3章 Zend虚拟机 + * [3.1 PHP代码的编译](3/zend_compile.md) + * [3.1.1 词法解析、语法解析](3/zend_compile_parse.md) + * [3.1.2 抽象语法树编译流程](3/zend_compile_opcode.md) + * [3.2 函数实现](3/function_implement.md) + * [3.2.1 内部函数](3/function_implement.md) + * 3.2.2 用户函数的实现 + * [3.3 Zend引擎执行流程](3/zend_executor.md) + * 3.3.1 基本结构 + * 3.3.2 执行流程 + * 3.3.3 函数的执行流程 + * [3.3.4 全局execute_data和opline](3/zend_global_register.md) + * 3.4 面向对象实现 + * [3.4.1 类](3/zend_class.md) + * [3.4.2 对象](3/zend_object.md) + * [3.4.3 继承](3/zend_extends.md) + * [3.4.4 动态属性](3/zend_prop.md) + * [3.4.5 魔术方法](3/zend_magic_method.md) + * [3.4.6 类的自动加载](3/zend_autoload.md) + * [3.5 运行时缓存](3/zend_runtime_cache.md) + * 3.6 Opcache + * 3.6.1 opcode缓存 + * 3.6.2 opcode优化 + * 3.6.3 JIT +* 第4章 PHP基础语法实现 + * [4.1 类型转换](4/type.md) + * [4.2 选择结构](4/if.md) + * [4.3 循环结构](4/loop.md) + * [4.4 中断及跳转](4/break.md) + * [4.5 include/require](4/include.md) + * [4.6 异常处理](4/exception.md) +* 第5章 内存管理 + * [5.1 Zend内存池](5/zend_alloc.md) + * [5.2 垃圾回收](5/gc.md) +* 第6章 线程安全 + * [6.1 什么是线程安全](6/ts.md) + * [6.2 线程安全资源管理器](6/ts.md) +* 第7章 扩展开发 + * [7.1 概述](7/intro.md) + * [7.2 扩展的实现原理](7/implement.md) + * [7.3 扩展的构成及编译](7/extension_intro.md) + * [7.3.1 扩展的构成](7/extension_intro.md) + * [7.3.2 编译工具](7/extension_intro.md) + * [7.3.3 编写扩展的基本步骤](7/extension_intro.md) + * [7.3.4 config.m4](7/extension_intro.md) + * [7.4 钩子函数](7/hook.md) + * [7.5 运行时配置](7/conf.md) + * [7.5.1 全局变量](7/conf.md) + * [7.5.2 ini配置](7/conf.md) + * [7.6 函数](7/func.md) + * 7.6.1 内部函数注册 + * 7.6.2 函数参数解析 + * 7.6.3 引用传参 + * 7.6.4 函数返回值 + * 7.6.5 函数调用 + * [7.7 zval的操作](7/var.md) + * [7.7.1 新生成各类型zval](7/var.md) + * [7.7.2 获取zval的值及类型](7/var.md) + * [7.7.3 类型转换](7/var.md) + * [7.7.4 引用计数](7/var.md) + * [7.7.5 字符串操作](7/var.md) + * [7.7.6 数组操作](7/var.md) + * [7.8 常量](7/constant.md) + * 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/namespace.md) + * [8.2 命名空间的定义](8/namespace.md) + * [8.2.1 定义语法](8/namespace.md) + * [8.2.2 内部实现](8/namespace.md) + * [8.3 命名空间的使用](8/namespace.md) + * [8.3.1 基本用法](8/namespace.md) + * [8.3.2 use导入](8/namespace.md) + * [8.3.3 动态用法](8/namespace.md) + +---- + +## 附录 + * [附录1:break/continue按标签中断语法实现](try/break.md) + * 附录2:defer推迟函数调用语法的实现 \ No newline at end of file diff --git a/book.json b/book.json new file mode 100644 index 0000000..26f051e --- /dev/null +++ b/book.json @@ -0,0 +1,7 @@ +{ + "title" : "PHP7内核剖析", + "author" : "pangudashu", + "description" : "PHP7内核剖析,基于PHP版本:php-7.0.12", + "language" : "zh-hans", + "gitbook" : ">=3.0.0" +} \ No newline at end of file diff --git a/img/EG.png b/img/EG.png index 64efe82..0dcd16c 100644 Binary files a/img/EG.png and b/img/EG.png differ diff --git a/img/align.png b/img/align.png new file mode 100644 index 0000000..63d6e12 Binary files /dev/null and b/img/align.png differ diff --git a/img/book.jpg b/img/book.jpg new file mode 100644 index 0000000..89fc991 Binary files /dev/null and b/img/book.jpg differ diff --git a/img/chunk_alloc.png b/img/chunk_alloc.png new file mode 100644 index 0000000..049b76d Binary files /dev/null and b/img/chunk_alloc.png differ diff --git a/img/defer.png b/img/defer.png new file mode 100644 index 0000000..08c48e5 Binary files /dev/null and b/img/defer.png differ diff --git a/img/defer_ast.png b/img/defer_ast.png new file mode 100644 index 0000000..db1da70 Binary files /dev/null and b/img/defer_ast.png differ diff --git a/img/defer_call.png b/img/defer_call.png new file mode 100644 index 0000000..37c7632 Binary files /dev/null and b/img/defer_call.png differ diff --git a/img/executor.png b/img/executor.png new file mode 100644 index 0000000..dfbb461 Binary files /dev/null and b/img/executor.png differ diff --git a/try/break.md b/try/break.md index 4e27d2b..adafa30 100644 --- a/try/break.md +++ b/try/break.md @@ -79,7 +79,7 @@ for(...){ __(1) 编译循环语句__ ```c -void zend_compile_for(zend_ast *ast) /* {{{ */ +void zend_compile_for(zend_ast *ast) { zend_ast *init_ast = ast->child[0]; zend_ast *cond_ast = ast->child[1]; diff --git a/try/defer.md b/try/defer.md new file mode 100644 index 0000000..761d149 --- /dev/null +++ b/try/defer.md @@ -0,0 +1,355 @@ +# 附录2:defer推迟函数调用语法的实现 + +使用过Go语言的应该都知道defer这个语法,它用来推迟一个函数的执行,在函数执行返回前首先检查当前函数内是否有推迟执行的函数,如果有则执行,然后再返回。defer是一个非常有用的语法,这个功能可以很方便的在函数结束前执行一些清理工作,比如关闭打开的文件、关闭连接、释放资源、解锁等等。这样延迟一个函数有以下两个好处: + +* (1) 靠近使用位置,避免漏掉清理工作,同时比放在函数结尾要清晰 +* (2) 如果有多处返回的地方可以避免代码重复,比如函数中有很多处return + +在一个函数中可以使用多个defer,其执行顺序与栈类似:后进先出,先定义的defer后执行。另外,在返回之后定义的defer将不会被执行,只有返回前定义的才会执行,通过exit退出程序的情况也不会执行任何defer。 + +在PHP中并没有实现类似的语法,本节我们将尝试在PHP中实现类似Go语言中defer的功能。此功能的实现需要对PHP的语法解析、抽象语法树/opcode的编译、opcode指令的执行等环节进行改造,涉及的地方比较多,但是改动点比较简单,可以很好的帮助大家完整的理解PHP编译、执行两个核心阶段的实现。总体实现思路: + +* __(1)语法解析:__ defer本质上还是函数调用,只是将调用时机移到了函数的最后,所以编译时可以复用调用函数的规则,但是需要与普通的调用区分开,所以我们新增一个AST节点类型,其子节点为为正常函数调用编译的AST,语法我们定义为:`defer function_name()`; +* __(2)opcode编译:__ 编译opcode时也复用调用函数的编译逻辑,不同的地方在于把defer放在最后编译,另外需要在编译return前新增一条opcode,用于执行return前跳转到defer开始的位置,在defer的最后也需要新增一条opcode,用于执行完defer后跳回return的位置; +* __(3)执行阶段:__ 执行时如果发现是return前新增的opcode则跳转到defer开始的位置,同时把return的位置记录下来,执行完defer后再跳回return。 + +编译后的opcode指令如下图所示: + +![](../img/defer.png) + +接下来我们详细介绍下各个环节的改动,一步步实现defer功能。 + +__(1)语法解析__ + +想让PHP支持`defer function_name()`的语法首先需要修改的是词法解析规则,将"defer"关键词解析为token:T_DEFER,这样词法扫描器在匹配token时遇到"defer"将告诉语法解析器这是一个T_DEFER。这一步改动比较简单,PHP的词法解析规则定义在zend_language_scanner.l中,加入以下代码即可: +```c +"defer" { + RETURN_TOKEN(T_DEFER); +} +``` +完成词法解析规则的修改后接着需要定义语法解析规则,这是非常关键的一步,语法解析器会根据配置的语法规则将PHP代码解析为抽象语法树(AST)。普通函数调用会被解析为ZEND_AST_CALL类型的AST节点,我们新增一种节点类型:ZEND_AST_DEFER_CALL,抽象语法树的节点类型为enum,定义在zend_ast.h中,同时此节点只需要一个子节点,这个子节点用于保存ZEND_AST_CALL节点,因此zend_ast.h的修改如下: +```c +enum _zend_ast_kind { + ... + /* 1 child node */ + ... + ZEND_AST_DEFER_CALL + .... +} +``` +定义完AST节点后就可以在配置语法解析规则了,把defer语法解析为ZEND_AST_DEFER_CALL节点,我们把这条语法规则定义在"statement:"节点下,if、echo、for等语法都定义在此节点下,语法解析规则文件为zend_language_parser.y: +```c +statement: + '{' inner_statement_list '}' { $$ = $2; } + ... + | T_DEFER function_call ';' { $$ = zend_ast_create(ZEND_AST_DEFER_CALL, $2); } +; +``` +修改完这两个文件后需要分别调用re2c、yacc生成对应的C文件,具体的生成命令可以在Makefile.frag中看到: +```sh +$ re2c --no-generation-date --case-inverted -cbdFt Zend/zend_language_scanner_defs.h -oZend/zend_language_scanner.c Zend/zend_language_scanner.l +$ yacc -p zend -v -d Zend/zend_language_parser.y -oZend/zend_language_parser.c +``` +执行完以后将在Zend目录下重新生成zend_language_scanner.c、zend_language_parser.c两个文件。到这一步已经完成生成抽象语法树的工作了,重新编译PHP后已经能够解析defer语法了,将会生成以下节点: + +![](../img/defer_ast.png) + +__(2)编译ZEND_AST_DEFER_CALL__ + +生成抽象语法树后接下来就是编译生成opcodes的操作,即从AST->Opcodes。编译ZEND_AST_DEFER_CALL节点时不能立即进行编译,需要等到当前脚本或函数全部编译完以后再进行编译,所以在编译过程需要把ZEND_AST_DEFER_CALL节点先缓存下来,参考循环结构编译时生成的zend_brk_cont_element的存储位置,我们也把ZEND_AST_DEFER_CALL节点保存在zend_op_array中,通过数组进行存储,将ZEND_AST_DEFER_CALL节点依次存入该数组,zend_op_array中加入以下几个成员: + +* __last_defer:__ 整形,记录当前编译的defer数 +* __defer_start_op:__ 整形,用于记录defer编译生成opcode指令的起始位置 +* __defer_call_array:__ 保存ZEND_AST_DEFER_CALL节点的数组,用于保存ast节点的地址 + +```c +struct _zend_op_array { + ... + int last_defer; + uint32_t defer_start_op; + zend_ast **defer_call_array; +} +``` +修改完数据结构后接着对应修改zend_op_array初始化的过程: +```c +//zend_opcode.c +void init_op_array(zend_op_array *op_array, zend_uchar type, int initial_ops_size) +{ + ... + op_array->last_defer = 0; + op_array->defer_start_op = 0; + op_array->defer_call_array = NULL; + ... +} +``` +完成依赖的这些数据结构的改造后接下来开始编写具体的编译逻辑,也就是编译ZEND_AST_DEFER_CALL的处理。抽象语法树的编译入口函数为zend_compile_top_stmt(),然后根据不同节点的类型进行相应的编译,我们在zend_compile_stmt()函数中对ZEND_AST_DEFER_CALL节点进行编译: +```c +void zend_compile_stmt(zend_ast *ast) +{ + ... + switch (ast->kind) { + ... + case ZEND_AST_DEFER_CALL: + zend_compile_defer_call(ast); + break + ... + } +} +``` +编译过程只是将ZEND_AST_DEFER_CALL的子节点(即:ZEND_AST_CALL)保存到zend_op_array->defer_call_array数组中,注意这里defer_call_array数组还没有分配内存,参考循环结构的实现,这里我们定义了一个函数用于数组的分配: +```c +//zend_compile.c +void zend_compile_defer_call(zend_ast *ast) +{ + if(!ast){ + return; + } + + zend_ast **call_ast = NULL; + //将普通函数调用的ast节点保存到defer_call_array数组中 + call_ast = get_next_defer_call(CG(active_op_array)); + *call_ast = ast->child[0]; +} + +//zend_opcode.c +zend_ast **get_next_defer_call(zend_op_array *op_array) +{ + op_array->last_defer++; + op_array->defer_call_array = erealloc(op_array->defer_call_array, sizeof(zend_ast*)*op_array->last_defer); + return &op_array->defer_call_array[op_array->last_defer-1]; +} +``` +既然分配了defer_call_array数组的内存就需要在zend_op_array销毁时释放: +```c +//zend_opcode.c +ZEND_API void destroy_op_array(zend_op_array *op_array) +{ + ... + if (op_array->defer_call_array) { + efree(op_array->defer_call_array); + } + ... +} +``` +编译完整个脚本或函数后,最后还会编译一条ZEND_RETURN,也就是返回指令,相当于ret指令,注意:这条opcode并不是我们在脚本中定义的return语句的,而是PHP内核为我们加的一条指令,这就是为什么有些函数我们没有写return也能返回的原因,任何函数或脚本都会生成这样一条指令。我们缓存在zend_op_array->defer_call_array数组中defer就是要在这时进行编译,也就是把defer的指令编译在最后。内核最后编译返回的这条指令由zend_emit_final_return()方法完成,我们把defer的编译放在此方法的末尾: +```c +//zend_compile.c +void zend_emit_final_return(zval *zv) +{ + ... + ret = zend_emit_op(NULL, returns_reference ? ZEND_RETURN_BY_REF : ZEND_RETURN, &zn, NULL); + ret->extended_value = -1; + + //编译推迟执行的函数调用 + zend_emit_defer_call(); +} +``` +前面已经说过,defer本质上就是函数调用,所以编译的过程直接复用普通函数调用的即可。另外,在编译时把起始位置记录到zend_op_array->defer_start_op中,因为在执行return前需要知道跳转到什么位置,这个值就是在那时使用的,具体的用法稍后再作说明。编译时按照倒序的顺序进行编译: +```c +//zend_compile.c +void zend_emit_defer_call() +{ + if (!CG(active_op_array)->defer_call_array) { + return; + } + + zend_ast *call_ast; + zend_op *nop; + znode result; + uint32_t opnum = get_next_op_number(CG(active_op_array)); + int defer_num = CG(active_op_array)->last_defer; + + //记录推迟的函数调用指令开始位置 + CG(active_op_array)->defer_start_op = opnum; + + while(--defer_num >= 0){ + call_ast = CG(active_op_array)->defer_call_array[defer_num]; + if (call_ast == NULL) { + continue; + } + nop = zend_emit_op(NULL, ZEND_NOP, NULL, NULL); + nop->op1.var = -2; + //编译函数调用 + zend_compile_call(&result, call_ast, BP_VAR_R); + } + //compile ZEND_DEFER_CALL_END + zend_emit_op(NULL, ZEND_DEFER_CALL_END, NULL, NULL); +} +``` +编译完推迟的函数调用之后,编译一条ZEND_DEFER_CALL_END指令,该指令用于执行完推迟的函数后跳回return的位置进行返回,opcode定义在zend_vm_opcodes.h中: +```c +//zend_vm_opcodes.h +#define ZEND_DEFER_CALL_END 174 +``` +还有一个地方你可能已经注意到,在逐个编译defer的函数调用前都生成了一条ZEND_NOP的指令,这个的目的是什么呢?开始的时候已经介绍过defer语法的特点,函数中定义的defer并不是全部执行,在return之后定义的defer是不会执行的,比如: +```go +func main(){ + defer fmt.Println("A") + + if 1 == 1{ + return + } + + defer fmt.Println("B") +} +``` +这种情况下第2个defer就不会生效,因此在return前跳转的位置就不一定是zend_op_array->defer_start_op,有可能会跳过几个函数的调用,所以这里我们通过ZEND_NOP这条空指令对多个defer call进行隔离,同时为避免与其它ZEND_NOP指令混淆,增加一个判断条件:op1.var=-2。这样在return前跳转时就根据此前定义的defer数跳过部分函数的调用,如下图所示。 + +![](../img/defer_call.png) + +到这一步我们已经完成defer函数调用的编译,此时重新编译PHP后可以看到通过defer推迟的函数调用已经被编译在最后了,只不过这个时候它们不能被执行。 + +__(3)编译return__ + +编译return时需要插入一条指令用于跳转到推迟执行的函数调用指令处,因此这里需要再定义一条opcode:ZEND_DEFER_CALL,在编译过程中defer call还未编译,因此此时还无法知道具体的跳转值。 +```c +//zend_vm_opcodes.h +#define ZEND_DEFER_CALL 173 +#define ZEND_DEFER_CALL_END 174 +``` +PHP脚本中声明的return语句由zend_compile_return()方法完成编译,在编译生成ZEND_DEFER_CALL指令时还需要将当前已定义的defer数(即在return前声明的defer)记录下来,用于计算具体的跳转值。 +```c +void zend_compile_return(zend_ast *ast) +{ + ... + //在return前编译ZEND_DEFER_CALL:用于在执行retur前跳转到defer call + if (CG(active_op_array)->defer_call_array) { + defer_zn.op_type = IS_UNUSED; + defer_zn.u.op.num = CG(active_op_array)->last_defer; + zend_emit_op(NULL, ZEND_DEFER_CALL, NULL, &defer_zn); + } + + //编译正常返回的指令 + opline = zend_emit_op(NULL, by_ref ? ZEND_RETURN_BY_REF : ZEND_RETURN, + &expr_node, NULL); + ... +} +``` +除了这种return外还有一种我们上面已经提过的return,即PHP内核编译的return指令,当PHP脚本中没有声明return语句时将执行内核添加的那条指令,因此也需要在zend_emit_final_return()加上上面的逻辑。 +```c +void zend_emit_final_return(zval *zv) +{ + ... + //在return前编译ZEND_DEFER_CALL:用于在执行retur前跳转到defer call + if (CG(active_op_array)->defer_call_array) { + //当前return之前定义的defer数 + defer_zn.op_type = IS_UNUSED; + defer_zn.u.op.num = CG(active_op_array)->last_defer; + zend_emit_op(NULL, ZEND_DEFER_CALL, NULL, &defer_zn); + } + + //编译返回指令 + ret = zend_emit_op(NULL, returns_reference ? ZEND_RETURN_BY_REF : ZEND_RETURN, &zn, NULL); + ret->extended_value = -1; + + //编译推迟执行的函数调用 + zend_emit_defer_call(); +} +``` +__(4)计算ZEND_DEFER_CALL指令的跳转位置__ + +前面我们已经完成了推迟调用函数以及return编译过程的改造,在编译完成后ZEND_DEFER_CALL指令已经能够知道具体的跳转位置了,因为推迟调用的函数已经编译完成了,所以下一步就是为全部的ZEND_DEFER_CALL指令计算跳转值。前面曾介绍过,在编译完成有一个pass_two()的环节,我们就在这里完成具体跳转位置的计算,并把跳转位置保存到ZEND_DEFER_CALL指令的操作数中,在执行阶段直接跳转到对应位置。 + +```c +ZEND_API int pass_two(zend_op_array *op_array) +{ + zend_op *opline, *end; + ... + //遍历opcode + opline = op_array->opcodes; + end = opline + op_array->last; + while (opline < end) { + switch (opline->opcode) { + ... + case ZEND_DEFER_CALL: //设置jmp + { + uint32_t defer_start = op_array->defer_start_op; + //skip_defer为当前return之后声明的defer数,也就是不需要执行的defer + uint32_t skip_defer = op_array->last_defer - opline->op2.num; + //defer_opline为推迟的函数调用起始位置 + zend_op *defer_opline = op_array->opcodes + defer_start; + uint32_t n = 0; + + while(n <= skip_defer){ + if (defer_opline->opcode == ZEND_NOP && defer_opline->op1.var == -2) { + n++; + } + defer_opline++; + defer_start++; + } + + //defer_start为opcode在op_array->opcodes数组中的位置 + opline->op1.opline_num = defer_start; + //将跳转位置保存到操作数op1中 + ZEND_PASS_TWO_UPDATE_JMP_TARGET(op_array, opline, opline->op1); + } + break; + } + ... + } + ... +} +``` +这里我们并没有直接编译为ZEND_JMP跳转指令,虽然ZEND_JMP可以跳转到后面的指令位置,但是最后的那条跳回return位置的指令(即:ZEND_DEFER_CALL_END)由于可能存在多个return的原因无法在编译期间确定具体的跳转值,只能在运行期间执行ZEND_DEFER_CALL时才能确定,所以需要在ZEND_DEFER_CALL指令的handler中将return的位置记录下来,执行ZEND_DEFER_CALL_END时根据这个值跳回。 + +__(5)定义ZEND_DEFER_CALL、ZEND_DEFER_CALL_END指令的handler__ + +ZEND_DEFER_CALL指令执行时需要将return的位置保存下来,我们把这个值保存到zend_execute_data结构中: +```c +//zend_compile.h +struct _zend_execute_data { + ... + const zend_op *return_opline; + ... +} +``` +opcode的handler定义在zend_vm_def.h文件中,定义完成后需要执行`php zend_vm_gen.php`脚本生成具体的handler函数。 +```c +ZEND_VM_HANDLER(173, ZEND_DEFER_CALL, ANY, ANY) +{ + USE_OPLINE + + //1) 将return指令的位置保存到EX(return_opline) + EX(return_opline) = opline + 1; + + //2) 跳转 + ZEND_VM_SET_OPCODE(OP_JMP_ADDR(opline, opline->op1)); + ZEND_VM_CONTINUE(); +} + +ZEND_VM_HANDLER(174, ZEND_DEFER_CALL_END, ANY, ANY) +{ + USE_OPLINE + + ZEND_VM_SET_OPCODE(EX(return_opline)); + ZEND_VM_CONTINUE(); +} +``` +到目前为止我们已经完成了全部的修改,重新编译PHP后就可以使用defer语法了: +```php +function shutdown($a){ + echo $a."\n"; +} +function test(){ + $a = 1234; + defer shutdown($a); + + $a = 8888; + + if(1){ + return "mid end\n"; + } + defer shutdown("9999"); + return "last end\n"; +} + +echo test(); +``` +执行后将显示: +```sh +8888 +mid end +``` +这里我们只实现了普通函数调用的方式,关于成员方法、静态方法、匿名函数等调用方式并未实现,留给有兴趣的读者自己去实现。 + +完整代码:[https://github.com/pangudashu/php-7.0.12](https://github.com/pangudashu/php-7.0.12)