├── 1 ├── base_introduction.md ├── base_process.md └── fpm.md ├── 2 ├── global_var.md ├── static_var.md ├── zend_constant.md ├── zend_ht.md └── zval.md ├── 3 ├── function_implement.md ├── zend_autoload.md ├── zend_class.md ├── zend_compile.md ├── zend_compile_opcode.md ├── zend_compile_parse.md ├── zend_executor.md ├── zend_extends.md ├── zend_global_register.md ├── zend_magic_method.md ├── zend_object.md ├── zend_prop.md └── zend_runtime_cache.md ├── 4 ├── break.md ├── exception.md ├── if.md ├── include.md ├── loop.md └── type.md ├── 5 ├── gc.md └── zend_alloc.md ├── 6 └── ts.md ├── 7 ├── class.md ├── conf.md ├── constant.md ├── extension_intro.md ├── func.md ├── hook.md ├── implement.md ├── intro.md └── var.md ├── 8 └── namespace.md ├── README.md ├── SUMMARY.md ├── book.json ├── execute_timeout.md ├── img ├── EG.png ├── align.png ├── alloc_all.png ├── ast_break_div.png ├── ast_class.png ├── ast_fetch_class.png ├── ast_for.png ├── ast_foreach.png ├── ast_function.png ├── ast_function_op.png ├── ast_if.png ├── ast_namespace.png ├── ast_switch.png ├── ast_while.png ├── autoconf.png ├── book.jpg ├── break_run.png ├── chunk_alloc.png ├── chunk_init.png ├── defer.png ├── defer_ast.png ├── defer_call.png ├── do_run.png ├── exception_ast.png ├── exception_run.png ├── exception_run_2.png ├── executor.png ├── fastcgi.png ├── for_run.png ├── foreach_ref_struct.png ├── foreach_run.png ├── foreach_struct.png ├── free_map.png ├── free_map_1.png ├── free_slot.png ├── func_exe_call.png ├── func_exe_eg1.png ├── func_exe_init.png ├── func_exe_send_var.png ├── func_exe_start.png ├── function_dec.png ├── gc_1.png ├── gc_2.png ├── if.png ├── if_run.png ├── include.png ├── include_2.png ├── include_3.png ├── include_4.png ├── include_5.png ├── internal_func_param.png ├── loop_op.png ├── magic_function.png ├── master_event_1.png ├── my_wx2.png ├── namespace_com.png ├── object_class_prop.png ├── object_new_op.png ├── oparray-1.png ├── php.png ├── php_function.jpg ├── php_vs_c.png ├── runtime_cache_1.png ├── switch_run.png ├── symbol_cv.png ├── talk.png ├── throw.png ├── tsrm_tls_a.png ├── tsrm_tls_table.png ├── var_T.png ├── while_run.png ├── worker_pool.png ├── worker_pool_struct.png ├── zend_ast.png ├── zend_ast_class.png ├── zend_ast_echo.png ├── zend_ast_echo_p.png ├── zend_class.png ├── zend_class_function.png ├── zend_class_init.png ├── zend_class_property.png ├── zend_compile.png ├── zend_compile2.png ├── zend_compile_process.png ├── zend_dy_prop.png ├── zend_eg_class.png ├── zend_ex_op.png ├── zend_execute_data.png ├── zend_extends.png ├── zend_extends_merge_prop.png ├── zend_gc_1.png ├── zend_gc_2.png ├── zend_global_ref.png ├── zend_global_var.png ├── zend_hash_1.png ├── zend_heap.png ├── zend_lookup_cv.png ├── zend_op_array.png ├── zend_op_array_2.png ├── zend_op_array_3.png ├── zend_parse_1.png ├── zend_parse_2.png ├── zend_property_info.png ├── zend_ref.png ├── zend_static_ref.png ├── zend_vm.png └── zval_sep.png └── try ├── break.md └── defer.md /1/base_introduction.md: -------------------------------------------------------------------------------- 1 | ## 1.1 PHP概述 2 | 3 | ### 1.1.1 PHP的历史发展 4 | PHP是一种非常流行的高级脚本语言,尤其适合Web开发,快速、灵活和实用是PHP最重要的特点。PHP自1995年由Lerdorf创建以来,在全球得到了非常广泛的应用。 5 | 6 | 在1995年早期以Personal Home Page Tools (PHP Tools) 开始对外发表第一个版本,Lerdorf写了一些介绍此程序的文档,并且发布了PHP1.0。在这早期的版本中,提供了访客留言本、访客计数器等简单的功能,之后越来越多的网站开始使用PHP,并且强烈要求增加一些特性,在新的成员加入开发行列之后,Rasmus Lerdorf 在1995年6月8日将 PHP/FI 公开发布,希望可以通过社群来加速程序开发与寻找错误。这个发布的版本命名为 PHP 2,已经有今日 PHP 的一些雏型,像是类似 Perl 的变量命名方式、表单处理功能、以及嵌入到 HTML 中执行的能力。程序语法上也类似 Perl,有较多的限制,不过更简单、更有弹性。PHP/FI加入了对MySQL的支持,从此建立了PHP在动态网页开发上的地位。到了1996年底,有15000个网站使用 PHP/FI。 7 | 8 | 在1997年,任职于 Technion IIT 公司的两个以色列程序设计师:Zeev Suraski 和 Andi Gutmans,重写了PHP的解析器,成为PHP3的基础,而 PHP 也在这个时候改称为PHP:Hypertext Preprocessor,1998年6月正式发布 PHP 3。Zeev Suraski 和 Andi Gutmans 在 PHP 3 发布后开始改写 PHP 的核心,这个在1999年发布的解析器称为 Zend Engine,他们也在以色列的 Ramat Gan 成立了 Zend Technologies 来管理 PHP 的开发。 9 | 10 | 在2000年5月22日,以Zend Engine 1.0为基础的PHP 4正式发布,2004年7月13日则发布了PHP 5,PHP 5则使用了第二代的Zend Engine。PHP包含了许多新特色:完全实现面向对象、引入PDO、以及许多性能方面的改进。目前PHP5.X仍然是应用非常广泛的一个版本。 11 | 12 | ### 1.1.2 特性 13 | PHP 独特的语法混合了 C、Java、Perl 以及 PHP 自创新的语法,丰富的语法支持、同时支持面向对象、面向过程,相比C、Java等语言具有语法简洁、使用灵活、开发效率高、容易学习等特点。 14 | 15 | * 开源免费:PHP社群有大量活跃的开发者贡献代码 16 | * 快捷:程序开发快,运行快,技术本身学习快,实用性强 17 | * 效率高:PHP消耗相当少的系统资源,自动gc机制 18 | * 类库资源:有大量可用类库供开发者使用 19 | * 扩展性:允许用户使用C/C++扩展PHP 20 | * 跨平台:可以在unix、windows、max os等系统上面使用PHP 21 | 22 | ### 1.1.3 PHP的相关组成 23 | 24 | #### 1.1.3.1 SAPI 25 | PHP本身可以理解为是一个库函数,提供语言的编译与执行服务,它有标准的输入、输出,而SAPI是PHP的接入层,它接收用户的请求,然后调用PHP内核提供的一些接口完成PHP脚本的执行,所以严格意义上讲SAPI并不算PHP内核的一部分。 26 | 27 | PHP的角色就好比是leveldb,它实现了基本存储功能,但是没有网络处理模块,而我们基于leveldb实现的完整存储服务就好比是SAPI。 28 | 29 | PHP中常用的SAPI有cli、php-fpm,cli是命令行下执行PHP脚本的实现:`bin/php script.php`,它是单进程的,处理模型比较简单,而php-fpm相对比较复杂,它实现了网络处理模块,用于与web服务器交互。 30 | 31 | #### 1.1.3.2 Zend引擎 32 | Zend是PHP语言实现的最为重要的部分,是PHP最基础、最核心的部分,它的源码在/Zend目录下,PHP代码从编译到执行都是由Zend完成的,后面章节绝大部分的源码分析都是针对Zend的。Zend整体由两个部分组成: 33 | 34 | * __编译器:__ 负责将PHP代码编译为抽象语法树,然后进一步编译为可执行的opcodes,这个过程相当于GCC的工作,编译器是一个语言实现的基础 35 | * __执行器:__ 负责执行编译器输出的opcodes,也就是执行PHP脚本中编写的代码逻辑 36 | 37 | #### 1.1.3.3 扩展 38 | 39 | -------------------------------------------------------------------------------- /1/base_process.md: -------------------------------------------------------------------------------- 1 | ## 1.2 执行流程 2 | PHP的生命周期: 3 | 4 | ![php_process](../img/php.png) 5 | 6 | ### 1.2.1 模块初始化阶段 7 | 8 | ### 1.2.2 请求初始化阶段 9 | 10 | ### 1.2.3 执行PHP脚本阶段 11 | 12 | ### 1.2.4 请求结束阶段 13 | 14 | ### 1.2.5 模块关闭阶段 15 | 16 | 17 | -------------------------------------------------------------------------------- /2/global_var.md: -------------------------------------------------------------------------------- 1 | ## 2.4 全局变量 2 | PHP中把定义在函数、类之外的变量称之为全局变量,也就是定义在主脚本中的变量,这些变量可以在函数、成员方法中通过global关键字引入使用。 3 | 4 | ```php 5 | function test() { 6 | global $id; 7 | $id++; 8 | } 9 | 10 | $id = 1; 11 | test(); 12 | echo $id; 13 | ``` 14 | ### 2.4.1 全局变量初始化 15 | 全局变量在整个请求执行期间始终存在,它们保存在`EG(symbol_table)`中,也就是全局变量符号表,与静态变量的存储一样,这也是一个哈希表,主脚本(或include、require)在`zend_execute_ex`执行开始之前会把当前作用域下的所有局部变量添加到`EG(symbol_table)`中,这一步操作后面介绍zend执行过程时还会讲到,这里先简单提下: 16 | ```c 17 | ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value) 18 | { 19 | ... 20 | i_init_execute_data(execute_data, op_array, return_value); 21 | zend_execute_ex(execute_data); 22 | ... 23 | } 24 | ``` 25 | `i_init_execute_data()`这个函数中会把局部变量插入到EG(symbol_table): 26 | ```c 27 | ZEND_API void zend_attach_symbol_table(zend_execute_data *execute_data) 28 | { 29 | zend_op_array *op_array = &execute_data->func->op_array; 30 | HashTable *ht = execute_data->symbol_table; 31 | 32 | if (!EXPECTED(op_array->last_var)) { 33 | return; 34 | } 35 | 36 | zend_string **str = op_array->vars; 37 | zend_string **end = str + op_array->last_var; 38 | //局部变量数组起始位置 39 | zval *var = EX_VAR_NUM(0); 40 | 41 | do{ 42 | zval *zv = zend_hash_find(ht, *str); 43 | //插入全局变量符号表 44 | zv = zend_hash_add_new(ht, *str, var); 45 | //哈希表中value指向局部变量的zval 46 | ZVAL_INDIRECT(zv, var); 47 | ... 48 | }while(str != end); 49 | } 50 | ``` 51 | 从上面的过程可以很直观的看到,在执行前遍历局部变量,然后插入EG(symbol_table),EG(symbol_table)中的value直接指向局部变量的zval,示例经过这一步的处理之后(此时局部变量只是分配了zval,但还未初始化,所以是IS_UNDEF): 52 | 53 | ![](../img/zend_global_var.png) 54 | 55 | ### 2.4.2 全局变量的访问 56 | 与静态变量的访问一样,全局变量也是将原来的值转换为引用,然后在global导入的作用域内创建一个局部变量指向该引用: 57 | ```php 58 | global $id; // 相当于:$id = & EG(symbol_table)["id"]; 59 | ``` 60 | 具体的操作过程不再细讲,与静态变量的处理过程一致,这时示例中局部变量与全局变量的引用情况如下图。 61 | 62 | ![](../img/zend_global_ref.png) 63 | 64 | ### 2.4.3 超全局变量 65 | 全局变量除了通过global引入外还有一类特殊的类型,它们不需要使用global引入而可以直接使用,这些全局变量称为:超全局变量。 66 | 67 | 超全局变量实际是PHP内核定义的一些全局变量:$GLOBALS、$_SERVER、$_REQUEST、$_POST、$_GET、$_FILES、$_ENV、$_COOKIE、$_SESSION、argv、argc。 68 | 69 | ### 2.4.4 销毁 70 | 局部变量如果没有手动销毁,那么在函数执行结束时会将它们销毁,而全局变量则是在整个请求结束时才会销毁,即使是我们直接在PHP脚本中定义在函数外的那些变量。 71 | ```c 72 | void shutdown_destructors(void) 73 | { 74 | if (CG(unclean_shutdown)) { 75 | EG(symbol_table).pDestructor = zend_unclean_zval_ptr_dtor; 76 | } 77 | zend_try { 78 | uint32_t symbols; 79 | do { 80 | symbols = zend_hash_num_elements(&EG(symbol_table)); 81 | //销毁 82 | zend_hash_reverse_apply(&EG(symbol_table), (apply_func_t) zval_call_destructor); 83 | } while (symbols != zend_hash_num_elements(&EG(symbol_table))); 84 | } 85 | ... 86 | } 87 | ``` 88 | -------------------------------------------------------------------------------- /2/static_var.md: -------------------------------------------------------------------------------- 1 | ## 2.3 静态变量 2 | PHP中局部变量分配在zend_execute_data结构上,每次执行zend_op_array都会生成一个新的zend_execute_data,局部变量在执行之初分配,然后在执行结束时释放,这是局部变量的生命周期,而局部变量中有一种特殊的类型:静态变量,它们不会在函数执行完后释放,当程序执行离开函数域时静态变量的值被保留下来,下次执行时仍然可以使用之前的值。 3 | 4 | PHP中的静态变量通过`static`关键词创建: 5 | ```php 6 | function my_func(){ 7 | static $count = 4; 8 | $count++; 9 | echo $count,"\n"; 10 | } 11 | my_func(); 12 | my_func(); 13 | =========================== 14 | 5 15 | 6 16 | ``` 17 | ### 2.3.1 静态变量的存储 18 | 静态变量既然不会随执行的结束而释放,那么很容易想到它的保存位置:`zend_op_array->static_variables`,这是一个哈希表,所以PHP中的静态变量与普通局部变量不同,它们没有分配在执行空间zend_execute_data上,而是以哈希表的形式保存在zend_op_array中。 19 | 20 | > 静态变量只会初始化一次,注意:它的初始化发生在编译阶段而不是执行阶段,上面这个例子中:`static $count = 4;`是在编译阶段发现定义了一个静态变量,然后插进了zend_op_array->static_variables中,并不是执行的时候把static_variables中的值修改为4,所以上面执行的时候会输出5、6,再次执行并没有重置静态变量的值。 21 | > 22 | > 这个特性也意味着静态变量初始的值不能是变量,比如:`static $count = $xxx;`这样定义将会报错。 23 | 24 | ### 2.3.2 静态变量的访问 25 | 局部变量通过编译时确定的编号进行读写操作,而静态变量通过哈希表保存,这就使得其不能像普通变量那样有一个固定的编号,有一种可能是通过变量名索引的,那么究竟是否如此呢?我们分析下其编译过程。 26 | 27 | 静态变量编译的语法规则: 28 | ```c 29 | statement: 30 | ... 31 | | T_STATIC static_var_list ';' { $$ = $2; } 32 | ... 33 | ; 34 | 35 | static_var_list: 36 | static_var_list ',' static_var { $$ = zend_ast_list_add($1, $3); } 37 | | static_var { $$ = zend_ast_create_list(1, ZEND_AST_STMT_LIST, $1); } 38 | ; 39 | 40 | static_var: 41 | T_VARIABLE { $$ = zend_ast_create(ZEND_AST_STATIC, $1, NULL); } 42 | | T_VARIABLE '=' expr { $$ = zend_ast_create(ZEND_AST_STATIC, $1, $3); } 43 | ; 44 | ``` 45 | 语法解析后生成了一个`ZEND_AST_STATIC`语法树节点,接着再看下这个节点编译为opcode的过程:zend_compile_static_var。 46 | ```c 47 | void zend_compile_static_var(zend_ast *ast) 48 | { 49 | zend_ast *var_ast = ast->child[0]; 50 | zend_ast *value_ast = ast->child[1]; 51 | zval value_zv; 52 | 53 | if (value_ast) { 54 | //定义了初始值 55 | zend_const_expr_to_zval(&value_zv, value_ast); 56 | } else { 57 | //无初始值 58 | ZVAL_NULL(&value_zv); 59 | } 60 | 61 | zend_compile_static_var_common(var_ast, &value_zv, 1); 62 | } 63 | ``` 64 | 这里首先对初始化值进行编译,最终得到一个固定值,然后调用:`zend_compile_static_var_common()`处理,首先判断当前编译的`zend_op_array->static_variables`是否已创建,未创建则分配一个HashTable,接着将定义的静态变量插入: 65 | ```c 66 | //zend_compile_static_var_common(): 67 | if (!CG(active_op_array)->static_variables) { 68 | ALLOC_HASHTABLE(CG(active_op_array)->static_variables); 69 | zend_hash_init(CG(active_op_array)->static_variables, 8, NULL, ZVAL_PTR_DTOR, 0); 70 | } 71 | //插入静态变量 72 | zend_hash_update(CG(active_op_array)->static_variables, Z_STR(var_node.u.constant), value); 73 | ``` 74 | 插入静态变量哈希表后并没有完成,接下来还有一个重要操作: 75 | ```c 76 | //生成一条ZEND_FETCH_W的opcode 77 | opline = zend_emit_op(&result, by_ref ? ZEND_FETCH_W : ZEND_FETCH_R, &var_node, NULL); 78 | opline->extended_value = ZEND_FETCH_STATIC; 79 | 80 | if (by_ref) { 81 | zend_ast *fetch_ast = zend_ast_create(ZEND_AST_VAR, var_ast); 82 | //生成一条ZEND_ASSIGN_REF的opcode 83 | zend_emit_assign_ref_znode(fetch_ast, &result); 84 | } 85 | ``` 86 | 后面生成了两条opcode: 87 | * __ZEND_FETCH_W:__ 这条opcode对应的操作是创建一个IS_INDIRECT类型的zval,指向static_variables中对应静态变量的zval 88 | * __ZEND_ASSIGN_REF:__ 它的操作是引用赋值,即将一个引用赋值给CV变量 89 | 90 | 通过上面两条opcode可以确定静态变量的读写过程:首先根据变量名在static_variables中取出对应的zval,然后将它修改为引用类型并赋值给局部变量,也就是说`static $count = 4;`包含了两个操作,严格的说`$count`并不是真正的静态变量,它只是一个指向静态变量的局部变量,执行时实际操作是:`$count = & static_variables["count"];`。上面例子$count与static_variables["count"]间的关系如图所示。 91 | 92 | ![](../img/zend_static_ref.png) 93 | 94 | -------------------------------------------------------------------------------- /2/zend_constant.md: -------------------------------------------------------------------------------- 1 | ## 2.5 常量 2 | 常量是一个简单值的标识符(名字)。如同其名称所暗示的,在脚本执行期间该值不能改变。常量默认为大小写敏感。通常常量标识符总是大写的。 3 | 4 | 常量名和其它任何 PHP 标签遵循同样的命名规则。合法的常量名以字母或下划线开始,后面跟着任何字母,数字或下划线。 5 | 6 | PHP中的常量通过`define()`函数定义: 7 | ```php 8 | define('CONST_VAR_1', 1234); 9 | ``` 10 | ### 2.5.1 常量的存储 11 | 在内核中常量存储在`EG(zend_constants)`哈希表中,访问时也是根据常量名直接到哈希表中查找,其实现比较简单。 12 | 13 | 常量的数据结构: 14 | ```c 15 | typedef struct _zend_constant { 16 | zval value; //常量值 17 | zend_string *name; //常量名 18 | int flags; //常量标识位 19 | int module_number; //所属扩展、模块 20 | } zend_constant; 21 | ``` 22 | 常量的几个属性都比较直观,这里只介绍下flags,它的值可以是以下三个中任意组合: 23 | ```c 24 | #define CONST_CS (1<<0) //大小写敏感 25 | #define CONST_PERSISTENT (1<<1) //持久化的 26 | #define CONST_CT_SUBST (1<<2) //允许编译时替换 27 | ``` 28 | 介绍下三种flag代表的含义: 29 | * __CONST_CS:__ 大小写敏感,默认是开启的,用户通过define()定义的始终是区分大小写的,通过扩展定义的可以自由选择 30 | * __CONST_PERSISTENT:__ 持久化的,只有通过扩展、内核定义的才支持,这种常量不会在request结束时清理掉 31 | * __CONST_CT_SUBST:__ 允许编译时替换,编译时如果发现有地方在读取常量的值,那么编译器会尝试直接替换为常量值,而不是在执行时再去读取,目前这个flag只有TRUE、FALSE、NULL三个常量在使用 32 | 33 | ### 2.5.2 常量的销毁 34 | 非持久化常量在request请求结束时销毁,具体销毁操作在:`php_request_shutdown()->zend_deactivate()->shutdown_executor()->clean_non_persistent_constants()`。 35 | ```c 36 | void clean_non_persistent_constants(void) 37 | { 38 | if (EG(full_tables_cleanup)) { 39 | zend_hash_apply(EG(zend_constants), clean_non_persistent_constant_full); 40 | } else { 41 | zend_hash_reverse_apply(EG(zend_constants), clean_non_persistent_constant); 42 | } 43 | } 44 | ``` 45 | 然后从哈希表末尾开始向前遍历EG(zend_constants),将非持久化常量删除,直到碰到第一个持久化常量时,停止遍历,正常情况下所有通过扩展定义的常量一定是在PHP中通过define定义之前,当然也并非绝对,这里只是说在所有常量均是在MINT阶段定义的情况。 46 | 47 | 持久化常量是在`php_module_shutdown()`阶段销毁的,具体过程与上面类似。 48 | -------------------------------------------------------------------------------- /2/zend_ht.md: -------------------------------------------------------------------------------- 1 | ## 2.2 数组 2 | 数组是PHP中非常强大、灵活的一种数据类型,它的底层实现为散列表(HashTable,也称作:哈希表),除了我们熟悉的PHP用户空间的Array类型之外,内核中也随处用到散列表,比如函数、类、常量、已include文件的索引表、全局符号表等都用的HashTable存储。 3 | 4 | 散列表是根据关键码值(Key value)而直接进行访问的数据结构,它的key - value之间存在一个映射函数,可以根据key通过映射函数直接索引到对应的value值,它不以关键字的比较为基本操作,采用直接寻址技术(就是说,它是直接通过key映射到内存地址上去的),从而加快查找速度,在理想情况下,无须任何比较就可以找到待查关键字,查找的期望时间为O(1)。 5 | 6 | ### 2.2.1 数组结构 7 | 存放记录的数组称做散列表,这个数组用来存储value,而value具体在数组中的存储位置由映射函数根据key计算确定,映射函数可以采用取模的方式,key可以通过一些譬如“times 33”的算法得到一个整形值,然后与数组总大小取模得到在散列表中的存储位置。这是一个普通散列表的实现,PHP散列表的实现整体也是这个思路,只是有几个特殊的地方,下面就是PHP中HashTable的数据结构: 8 | 9 | ```c 10 | //Bucket:散列表中存储的元素 11 | typedef struct _Bucket { 12 | zval val; //存储的具体value,这里嵌入了一个zval,而不是一个指针 13 | zend_ulong h; //key根据times 33计算得到的哈希值,或者是数值索引编号 14 | zend_string *key; //存储元素的key 15 | } Bucket; 16 | 17 | //HashTable结构 18 | typedef struct _zend_array HashTable; 19 | struct _zend_array { 20 | zend_refcounted_h gc; 21 | union { 22 | struct { 23 | ZEND_ENDIAN_LOHI_4( 24 | zend_uchar flags, 25 | zend_uchar nApplyCount, 26 | zend_uchar nIteratorsCount, 27 | zend_uchar reserve) 28 | } v; 29 | uint32_t flags; 30 | } u; 31 | uint32_t nTableMask; //哈希值计算掩码,等于nTableSize的负值(nTableMask = -nTableSize) 32 | Bucket *arData; //存储元素数组,指向第一个Bucket 33 | uint32_t nNumUsed; //已用Bucket数 34 | uint32_t nNumOfElements; //哈希表有效元素数 35 | uint32_t nTableSize; //哈希表总大小,为2的n次方 36 | uint32_t nInternalPointer; 37 | zend_long nNextFreeElement; //下一个可用的数值索引,如:arr[] = 1;arr["a"] = 2;arr[] = 3; 则nNextFreeElement = 2; 38 | dtor_func_t pDestructor; 39 | }; 40 | ``` 41 | HashTable中有两个非常相近的值:`nNumUsed`、`nNumOfElements`,`nNumOfElements`表示哈希表已有元素数,那这个值不跟`nNumUsed`一样吗?为什么要定义两个呢?实际上它们有不同的含义,当将一个元素从哈希表删除时并不会将对应的Bucket移除,而是将Bucket存储的zval修改为`IS_UNDEF`,只有扩容时发现nNumOfElements与nNumUsed相差达到一定数量(这个数量是:`ht->nNumUsed - ht->nNumOfElements > (ht->nNumOfElements >> 5)`)时才会将已删除的元素全部移除,重新构建哈希表。所以`nNumUsed`>=`nNumOfElements`。 42 | 43 | HashTable中另外一个非常重要的值`arData`,这个值指向存储元素数组的第一个Bucket,插入元素时按顺序 __依次插入__ 数组,比如第一个元素在arData[0]、第二个在arData[1]...arData[nNumUsed]。PHP数组的有序性正是通过`arData`保证的,这是第一个与普通散列表实现不同的地方。 44 | 45 | 既然arData并不是按key映射的散列表,那么映射函数是如何将key与arData中的value建立映射关系的呢? 46 | 47 | 实际上这个散列表也在`arData`中,比较特别的是散列表在ht->arData内存之前,分配内存时这个散列表与Bucket数组一起分配,arData向后移动到了Bucket数组的起始位置,并不是申请内存的起始位置,这样散列表可以由arData指针向前移动访问到,即arData[-1]、arData[-2]、arData[-3]......散列表的结构是`uint32_t`,它保存的是value在Bucket数组中的位置。 48 | 49 | 所以,整体来看HashTable主要依赖arData实现元素的存储、索引。插入一个元素时先将元素按先后顺序插入Bucket数组,位置是idx,再根据key的哈希值映射到散列表中的某个位置nIndex,将idx存入这个位置;查找时先在散列表中映射到nIndex,得到value在Bucket数组的位置idx,再从Bucket数组中取出元素。 50 | 51 | 比如: 52 | ```php 53 | $arr["a"] = 1; 54 | $arr["b"] = 2; 55 | $arr["c"] = 3; 56 | $arr["d"] = 4; 57 | 58 | unset($arr["c"]); 59 | ``` 60 | 对应的HashTable如下图所示。 61 | 62 | ![](../img/zend_hash_1.png) 63 | 64 | > 图中Bucket的zval.u2.next默认值应该为-1,不是0 65 | 66 | ### 2.2.2 映射函数 67 | 映射函数(即:散列函数)是散列表的关键部分,它将key与value建立映射关系,一般映射函数可以根据key的哈希值与Bucket数组大小取模得到,即`key->h % ht->nTableSize`,但是PHP却不是这么做的: 68 | ```c 69 | nIndex = key->h | ht->nTableMask; 70 | ``` 71 | 显然位运算要比取模更快。 72 | 73 | `nTableMask`为`nTableSize`的负数,即:`nTableMask = -nTableSize`,因为`nTableSize`等于2^n,所以`nTableMask`二进制位右侧全部为0,也就保证了nIndex落在数组索引的范围之内(`|nIndex| <= nTableSize`): 74 | ```c 75 | 11111111 11111111 11111111 11111000 -8 76 | 11111111 11111111 11111111 11110000 -16 77 | 11111111 11111111 11111111 11100000 -32 78 | 11111111 11111111 11111111 11000000 -64 79 | 11111111 11111111 11111111 10000000 -128 80 | ``` 81 | ### 2.2.3 哈希碰撞 82 | 哈希碰撞是指不同的key可能计算得到相同的哈希值(数值索引的哈希值直接就是数值本身),但是这些值又需要插入同一个散列表。一般解决方法是将Bucket串成链表,查找时遍历链表比较key。 83 | 84 | PHP的实现也是如此,只是将链表的指针指向转化为了数值指向,即:指向冲突元素的指针并没有直接存在Bucket中,而是保存到了value的`zval`中: 85 | ```c 86 | struct _zval_struct { 87 | zend_value value; /* value */ 88 | ... 89 | union { 90 | uint32_t var_flags; 91 | uint32_t next; /* hash collision chain */ 92 | uint32_t cache_slot; /* literal cache slot */ 93 | uint32_t lineno; /* line number (for ast nodes) */ 94 | uint32_t num_args; /* arguments number for EX(This) */ 95 | uint32_t fe_pos; /* foreach position */ 96 | uint32_t fe_iter_idx; /* foreach iterator index */ 97 | } u2; 98 | }; 99 | ``` 100 | 当出现冲突时将原value的位置保存到新value的`zval.u2.next`中,然后将新插入的value的位置更新到散列表,也就是后面冲突的value始终插入header。所以查找过程类似: 101 | ```c 102 | zend_ulong h = zend_string_hash_val(key); 103 | uint32_t idx = ht->arHash[h & ht->nTableMask]; 104 | while (idx != INVALID_IDX) { 105 | Bucket *b = &ht->arData[idx]; 106 | if (b->h == h && zend_string_equals(b->key, key)) { 107 | return b; 108 | } 109 | idx = Z_NEXT(b->val); //移到下一个冲突的value 110 | } 111 | return NULL; 112 | ``` 113 | ### 2.2.4 插入、查找、删除 114 | 这几个基本操作比较简单,不再赘述,定位到元素所在Bucket位置后的操作类似单链表的插入、删除、查找。 115 | 116 | ### 2.2.5 扩容 117 | 散列表可存储的value数是固定的,当空间不够用时就要进行扩容了。 118 | 119 | PHP散列表的大小为2^n,插入时如果容量不够则首先检查已删除元素所占比例,如果达到阈值(ht->nNumUsed - ht->nNumOfElements > (ht->nNumOfElements >> 5),则将已删除元素移除,重建索引,如果未到阈值则进行扩容操作,扩大为当前大小的2倍,将当前Bucket数组复制到新的空间,然后重建索引。 120 | 121 | ```c 122 | //zend_hash.c 123 | static void ZEND_FASTCALL zend_hash_do_resize(HashTable *ht) 124 | { 125 | 126 | if (ht->nNumUsed > ht->nNumOfElements + (ht->nNumOfElements >> 5)) { 127 | //只有到一定阈值才进行rehash操作 128 | zend_hash_rehash(ht); //重建索引数组 129 | } else if (ht->nTableSize < HT_MAX_SIZE) { 130 | //扩容 131 | void *new_data, *old_data = HT_GET_DATA_ADDR(ht); 132 | //扩大为2倍,加法要比乘法快,小的优化点无处不在... 133 | uint32_t nSize = ht->nTableSize + ht->nTableSize; 134 | Bucket *old_buckets = ht->arData; 135 | 136 | //新分配arData空间,大小为:(sizeof(Bucket) + sizeof(uint32_t)) * nSize 137 | new_data = pemalloc(HT_SIZE_EX(nSize, -nSize), ...); 138 | ht->nTableSize = nSize; 139 | ht->nTableMask = -ht->nTableSize; 140 | //将arData指针偏移到Bucket数组起始位置 141 | HT_SET_DATA_ADDR(ht, new_data); 142 | //将旧的Bucket数组拷到新空间 143 | memcpy(ht->arData, old_buckets, sizeof(Bucket) * ht->nNumUsed); 144 | //释放旧空间 145 | pefree(old_data, ht->u.flags & HASH_FLAG_PERSISTENT); 146 | 147 | //重建索引数组:散列表 148 | zend_hash_rehash(ht); 149 | ... 150 | } 151 | ... 152 | } 153 | 154 | #define HT_SET_DATA_ADDR(ht, ptr) do { \ 155 | (ht)->arData = (Bucket*)(((char*)(ptr)) + HT_HASH_SIZE((ht)->nTableMask)); \ 156 | } while (0) 157 | ``` 158 | 159 | ### 2.2.6 重建散列表 160 | 当删除元素达到一定数量或扩容后都需要重建散列表,因为value在Bucket位置移动了或哈希数组nTableSize变化了导致key与value的映射关系改变,重建过程实际就是遍历Bucket数组中的value,然后重新计算映射值更新到散列表,除了更新散列表之外,这里还有一个重要的处理:移除已删除的value,开始的时候我们说过,删除value时只是将value的type设置为IS_UNDEF,并没有实际从Bucket数组中删除,如果这些value一直存在那么将浪费很多空间,所以这里会把它们移除,操作的方式也比较简单:将后面未删除的value依次前移,具体过程如下: 161 | ```c 162 | //zend_hash.c 163 | ZEND_API int ZEND_FASTCALL zend_hash_rehash(HashTable *ht) 164 | { 165 | Bucket *p; 166 | uint32_t nIndex, i; 167 | ... 168 | i = 0; 169 | p = ht->arData; 170 | if (ht->nNumUsed == ht->nNumOfElements) { //没有已删除的直接遍历Bucket数组重新插入索引数组即可 171 | do { 172 | nIndex = p->h | ht->nTableMask; 173 | Z_NEXT(p->val) = HT_HASH(ht, nIndex); 174 | HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(i); 175 | p++; 176 | } while (++i < ht->nNumUsed); 177 | } else { 178 | do { 179 | if (UNEXPECTED(Z_TYPE(p->val) == IS_UNDEF)) { 180 | //有已删除元素则将后面的value依次前移,压实Bucket数组 181 | ...... 182 | while (++i < ht->nNumUsed) { 183 | p++; 184 | if (EXPECTED(Z_TYPE_INFO(p->val) != IS_UNDEF)) { 185 | ZVAL_COPY_VALUE(&q->val, &p->val); 186 | q->h = p->h; 187 | nIndex = q->h | ht->nTableMask; 188 | q->key = p->key; 189 | Z_NEXT(q->val) = HT_HASH(ht, nIndex); 190 | HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(j); 191 | if (UNEXPECTED(ht->nInternalPointer == i)) { 192 | ht->nInternalPointer = j; 193 | } 194 | q++; 195 | j++; 196 | } 197 | } 198 | ...... 199 | ht->nNumUsed = j; 200 | break; 201 | } 202 | 203 | nIndex = p->h | ht->nTableMask; 204 | Z_NEXT(p->val) = HT_HASH(ht, nIndex); 205 | HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(i); 206 | p++; 207 | }while(++i < ht->nNumUsed); 208 | } 209 | } 210 | ``` 211 | 除了上面这些操作,PHP中关于HashTable的还有很多,这里不再介绍。 212 | -------------------------------------------------------------------------------- /2/zval.md: -------------------------------------------------------------------------------- 1 | ## 2.1 变量的内部实现 2 | 3 | 变量是一个语言实现的基础,变量有两个组成部分:变量名、变量值,PHP中可以将其对应为:zval、zend_value,这两个概念一定要区分开,PHP中变量的内存是通过引用计数进行管理的,而且PHP7中引用计数是在zend_value而不是zval上,变量之间的传递、赋值通常也是针对zend_value。 4 | 5 | PHP中可以通过`$`关键词定义一个变量:`$a;`,在定义的同时可以进行初始化:`$a = "hi~";`,注意这实际是两步:定义、初始化,只定义一个变量也是可以的,可以不给它赋值,比如: 6 | ```php 7 | $a; 8 | $b = 1; 9 | ``` 10 | 这段代码在执行时会分配两个zval。 11 | 12 | 接下来我们具体看下变量的结构以及不同类型的实现。 13 | 14 | ### 2.1.1 变量的基础结构 15 | ```c 16 | //zend_types.h 17 | typedef struct _zval_struct zval; 18 | 19 | typedef union _zend_value { 20 | zend_long lval; //int整形 21 | double dval; //浮点型 22 | zend_refcounted *counted; 23 | zend_string *str; //string字符串 24 | zend_array *arr; //array数组 25 | zend_object *obj; //object对象 26 | zend_resource *res; //resource资源类型 27 | zend_reference *ref; //引用类型,通过&$var_name定义的 28 | zend_ast_ref *ast; //下面几个都是内核使用的value 29 | zval *zv; 30 | void *ptr; 31 | zend_class_entry *ce; 32 | zend_function *func; 33 | struct { 34 | uint32_t w1; 35 | uint32_t w2; 36 | } ww; 37 | } zend_value; 38 | 39 | struct _zval_struct { 40 | zend_value value; //变量实际的value 41 | union { 42 | struct { 43 | ZEND_ENDIAN_LOHI_4( //这个是为了兼容大小字节序,小字节序就是下面的顺序,大字节序则下面4个顺序翻转 44 | zend_uchar type, //变量类型 45 | zend_uchar type_flags, //类型掩码,不同的类型会有不同的几种属性,内存管理会用到 46 | zend_uchar const_flags, 47 | zend_uchar reserved) //call info,zend执行流程会用到 48 | } v; 49 | uint32_t type_info; //上面4个值的组合值,可以直接根据type_info取到4个对应位置的值 50 | } u1; 51 | union { 52 | uint32_t var_flags; 53 | uint32_t next; //哈希表中解决哈希冲突时用到 54 | uint32_t cache_slot; /* literal cache slot */ 55 | uint32_t lineno; /* line number (for ast nodes) */ 56 | uint32_t num_args; /* arguments number for EX(This) */ 57 | uint32_t fe_pos; /* foreach position */ 58 | uint32_t fe_iter_idx; /* foreach iterator index */ 59 | } u2; //一些辅助值 60 | }; 61 | ``` 62 | `zval`结构比较简单,内嵌一个union类型的`zend_value`保存具体变量类型的值或指针,`zval`中还有两个union:`u1`、`u2`: 63 | * __u1:__ 它的意义比较直观,变量的类型就通过`u1.v.type`区分,另外一个值`type_flags`为类型掩码,在变量的内存管理、gc机制中会用到,第三部分会详细分析,至于后面两个`const_flags`、`reserved`暂且不管 64 | * __u2:__ 这个值纯粹是个辅助值,假如`zval`只有:`value`、`u1`两个值,整个zval的大小也会对齐到16byte,既然不管有没有u2大小都是16byte,把多余的4byte拿出来用于一些特殊用途还是很划算的,比如next在哈希表解决哈希冲突时会用到,还有fe_pos在foreach会用到...... 65 | 66 | 从`zend_value`可以看出,除`long`、`double`类型直接存储值外,其它类型都为指针,指向各自的结构。 67 | 68 | ### 2.1.2 类型 69 | `zval.u1.type`类型: 70 | ```c 71 | /* regular data types */ 72 | #define IS_UNDEF 0 73 | #define IS_NULL 1 74 | #define IS_FALSE 2 75 | #define IS_TRUE 3 76 | #define IS_LONG 4 77 | #define IS_DOUBLE 5 78 | #define IS_STRING 6 79 | #define IS_ARRAY 7 80 | #define IS_OBJECT 8 81 | #define IS_RESOURCE 9 82 | #define IS_REFERENCE 10 83 | 84 | /* constant expressions */ 85 | #define IS_CONSTANT 11 86 | #define IS_CONSTANT_AST 12 87 | 88 | /* fake types */ 89 | #define _IS_BOOL 13 90 | #define IS_CALLABLE 14 91 | 92 | /* internal types */ 93 | #define IS_INDIRECT 15 94 | #define IS_PTR 17 95 | ``` 96 | 97 | #### 2.1.2.1 标量类型 98 | 最简单的类型是true、false、long、double、null,其中true、false、null没有value,直接根据type区分,而long、double的值则直接存在value中:zend_long、double,也就是标量类型不需要额外的value指针。 99 | 100 | #### 2.1.2.2 字符串 101 | PHP中字符串通过`zend_string`表示: 102 | ```c 103 | struct _zend_string { 104 | zend_refcounted_h gc; 105 | zend_ulong h; /* hash value */ 106 | size_t len; 107 | char val[1]; 108 | }; 109 | ``` 110 | * __gc:__ 变量引用信息,比如当前value的引用数,所有用到引用计数的变量类型都会有这个结构,3.1节会详细分析 111 | * __h:__ 哈希值,数组中计算索引时会用到 112 | * __len:__ 字符串长度,通过这个值保证二进制安全 113 | * __val:__ 字符串内容,变长struct,分配时按len长度申请内存 114 | 115 | 事实上字符串又可具体分为几类:IS_STR_PERSISTENT(通过malloc分配的)、IS_STR_INTERNED(php代码里写的一些字面量,比如函数名、变量值)、IS_STR_PERMANENT(永久值,生命周期大于request)、IS_STR_CONSTANT(常量)、IS_STR_CONSTANT_UNQUALIFIED,这个信息通过flag保存:zval.value->gc.u.flags,后面用到的时候再具体分析。 116 | 117 | #### 2.1.2.3 数组 118 | array是PHP中非常强大的一个数据结构,它的底层实现就是普通的有序HashTable,这里简单看下它的结构,下一节会单独分析数组的实现。 119 | 120 | ```c 121 | typedef struct _zend_array HashTable; 122 | 123 | struct _zend_array { 124 | zend_refcounted_h gc; //引用计数信息,与字符串相同 125 | union { 126 | struct { 127 | ZEND_ENDIAN_LOHI_4( 128 | zend_uchar flags, 129 | zend_uchar nApplyCount, 130 | zend_uchar nIteratorsCount, 131 | zend_uchar reserve) 132 | } v; 133 | uint32_t flags; 134 | } u; 135 | uint32_t nTableMask; //计算bucket索引时的掩码 136 | Bucket *arData; //bucket数组 137 | uint32_t nNumUsed; //已用bucket数 138 | uint32_t nNumOfElements; //已有元素数,nNumOfElements <= nNumUsed,因为删除的并不是直接从arData中移除 139 | uint32_t nTableSize; //数组的大小,为2^n 140 | uint32_t nInternalPointer; //数值索引 141 | zend_long nNextFreeElement; 142 | dtor_func_t pDestructor; 143 | }; 144 | ``` 145 | #### 2.1.2.4 对象/资源 146 | ```c 147 | struct _zend_object { 148 | zend_refcounted_h gc; 149 | uint32_t handle; 150 | zend_class_entry *ce; //对象对应的class类 151 | const zend_object_handlers *handlers; 152 | HashTable *properties; //对象属性哈希表 153 | zval properties_table[1]; 154 | }; 155 | 156 | struct _zend_resource { 157 | zend_refcounted_h gc; 158 | int handle; 159 | int type; 160 | void *ptr; 161 | }; 162 | ``` 163 | 对象比较常见,资源指的是tcp连接、文件句柄等等类型,这种类型比较灵活,可以随意定义struct,通过ptr指向,后面会单独分析这种类型,这里不再多说。 164 | 165 | #### 2.1.2.5 引用 166 | 引用是PHP中比较特殊的一种类型,它实际是指向另外一个PHP变量,对它的修改会直接改动实际指向的zval,可以简单的理解为C中的指针,在PHP中通过`&`操作符产生一个引用变量,也就是说不管以前的类型是什么,`&`首先会创建一个`zend_reference`结构,其内嵌了一个zval,这个zval的value指向原来zval的value(如果是布尔、整形、浮点则直接复制原来的值),然后将原zval的类型修改为IS_REFERENCE,原zval的value指向新创建的`zend_reference`结构。 167 | ```c 168 | struct _zend_reference { 169 | zend_refcounted_h gc; 170 | zval val; 171 | }; 172 | ``` 173 | 结构非常简单,除了公共部分`zend_refcounted_h`外只有一个`val`,举个示例看下具体的结构关系: 174 | ```php 175 | $a = "time:" . time(); //$a -> zend_string_1(refcount=1) 176 | $b = &$a; //$a,$b -> zend_reference_1(refcount=2) -> zend_string_1(refcount=1) 177 | ``` 178 | 最终的结果如图: 179 | 180 | ![ref](../img/zend_ref.png) 181 | 182 | 注意:引用只能通过`&`产生,无法通过赋值传递,比如: 183 | ```php 184 | $a = "time:" . time(); //$a -> zend_string_1(refcount=1) 185 | $b = &$a; //$a,$b -> zend_reference_1(refcount=2) -> zend_string_1(refcount=1) 186 | $c = $b; //$a,$b -> zend_reference_1(refcount=2) -> zend_string_1(refcount=2) 187 | //$c -> --- 188 | ``` 189 | `$b = &$a`这时候`$a`、`$b`的类型是引用,但是`$c = $b`并不会直接将`$b`赋值给`$c`,而是把`$b`实际指向的zval赋值给`$c`,如果想要`$c`也是一个引用则需要这么操作: 190 | ```php 191 | $a = "time:" . time(); //$a -> zend_string_1(refcount=1) 192 | $b = &$a; //$a,$b -> zend_reference_1(refcount=2) -> zend_string_1(refcount=1) 193 | $c = &$b;/*或$c = &$a*/ //$a,$b,$c -> zend_reference_1(refcount=3) -> zend_string_1(refcount=1) 194 | ``` 195 | 这个也表示PHP中的 __引用只可能有一层__ ,__不会出现一个引用指向另外一个引用的情况__ ,也就是没有C语言中`指针的指针`的概念。 196 | 197 | ### 2.1.3 内存管理 198 | 接下来分析下变量的分配、销毁。 199 | 200 | 在分析变量内存管理之前我们先自己想一下可能的实现方案,最简单的处理方式:定义变量时alloc一个zval及对应的value结构(ref/arr/str/res...),赋值、函数传参时硬拷贝一个副本,这样各变量最终的值完全都是独立的,不会出现多个变量同时共用一个value的情况,在执行完以后直接将各变量及value结构free掉。 201 | 202 | 这种方式是可行的,而且内存管理也很简单,但是,硬拷贝带来的一个问题是效率低,比如我们定义了一个变量然后赋值给另外一个变量,可能后面都只是只读操作,假如硬拷贝的话就会有多余的一份数据,这个问题的解决方案是: __引用计数+写时复制__ 。PHP变量的管理正是基于这两点实现的。 203 | 204 | #### 2.1.3.1 引用计数 205 | 引用计数是指在value中增加一个字段`refcount`记录指向当前value的数量,变量复制、函数传参时并不直接硬拷贝一份value数据,而是将`refcount++`,变量销毁时将`refcount--`,等到`refcount`减为0时表示已经没有变量引用这个value,将它销毁即可。 206 | ```php 207 | $a = "time:" . time(); //$a -> zend_string_1(refcount=1) 208 | $b = $a; //$a,$b -> zend_string_1(refcount=2) 209 | $c = $b; //$a,$b,$c -> zend_string_1(refcount=3) 210 | 211 | unset($b); //$b = IS_UNDEF $a,$c -> zend_string_1(refcount=2) 212 | ``` 213 | 引用计数的信息位于给具体value结构的gc中: 214 | ```c 215 | typedef struct _zend_refcounted_h { 216 | uint32_t refcount; /* reference counter 32-bit */ 217 | union { 218 | struct { 219 | ZEND_ENDIAN_LOHI_3( 220 | zend_uchar type, 221 | zend_uchar flags, /* used for strings & objects */ 222 | uint16_t gc_info) /* keeps GC root number (or 0) and color */ 223 | } v; 224 | uint32_t type_info; 225 | } u; 226 | } zend_refcounted_h; 227 | ``` 228 | 从上面的zend_value结构可以看出并不是所有的数据类型都会用到引用计数,`long`、`double`直接都是硬拷贝,只有value是指针的那几种类型才__可能__会用到引用计数。 229 | 230 | 下面再看一个例子: 231 | ```php 232 | $a = "hi~"; 233 | $b = $a; 234 | ``` 235 | 猜测一下变量`$a/$b`的引用情况。 236 | 237 | 这个不跟上面的例子一样吗?字符串`"hi~"`有`$a/$b`两个引用,所以`zend_string1(refcount=2)`。但是这是错的,gdb调试发现上面例子zend_string的引用计数为0。这是为什么呢? 238 | ```c 239 | $a,$b -> zend_string_1(refcount=0,val="hi~") 240 | ``` 241 | 242 | 事实上并不是所有的PHP变量都会用到引用计数,标量:true/false/double/long/null是硬拷贝自然不需要这种机制,但是除了这几个还有两个特殊的类型也不会用到:interned string(内部字符串,就是上面提到的字符串flag:IS_STR_INTERNED)、immutable array,它们的type是`IS_STRING`、`IS_ARRAY`,与普通string、array类型相同,那怎么区分一个value是否支持引用计数呢?还记得`zval.u1`中那个类型掩码`type_flag`吗?正是通过这个字段标识的,这个字段除了标识value是否支持引用计数外还有其它几个标识位,按位分割,注意:`type_flag`与`zval.value->gc.u.flag`不是一个值。 243 | 244 | 支持引用计数的value类型其`zval.u1.type_flag` __包含__ (注意是&,不是等于)`IS_TYPE_REFCOUNTED`: 245 | ```c 246 | #define IS_TYPE_REFCOUNTED (1<<2) 247 | ``` 248 | 下面具体列下哪些类型会有这个标识: 249 | ```c 250 | | type | refcounted | 251 | +----------------+------------+ 252 | |simple types | | 253 | |string | Y | 254 | |interned string | | 255 | |array | Y | 256 | |immutable array | | 257 | |object | Y | 258 | |resource | Y | 259 | |reference | Y | 260 | ``` 261 | simple types很显然用不到,不再解释,string、array、object、resource、reference有引用计数机制也很容易理解,下面具体解释下另外两个特殊的类型: 262 | * __interned string:__ 内部字符串,这是种什么类型?我们在PHP中写的所有字符都可以认为是这种类型,比如function name、class name、variable name、静态字符串等等,我们这样定义:`$a = "hi~";`后面的字符串内容是唯一不变的,这些字符串等同于C语言中定义在静态变量区的字符串:`char *a = "hi~";`,这些字符串的生命周期为request期间,request完成后会统一销毁释放,自然也就无需在运行期间通过引用计数管理内存。 263 | 264 | * __immutable array:__ 只有在用opcache的时候才会用到这种类型,不清楚具体实现,暂时忽略。 265 | 266 | #### 2.1.3.2 写时复制 267 | 上一小节介绍了引用计数,多个变量可能指向同一个value,然后通过refcount统计引用数,这时候如果其中一个变量试图更改value的内容则会重新拷贝一份value修改,同时断开旧的指向,写时复制的机制在计算机系统中有非常广的应用,它只有在必要的时候(写)才会发生硬拷贝,可以很好的提高效率,下面从示例看下: 268 | 269 | ```php 270 | $a = array(1,2); 271 | $b = &$a; 272 | $c = $a; 273 | 274 | //发生分离 275 | $b[] = 3; 276 | ``` 277 | 最终的结果: 278 | 279 | ![zval_sep](../img/zval_sep.png) 280 | 281 | 不是所有类型都可以copy的,比如对象、资源,事实上只有string、array两种支持,与引用计数相同,也是通过`zval.u1.type_flag`标识value是否可复制的: 282 | ```c 283 | #define IS_TYPE_COPYABLE (1<<4) 284 | ``` 285 | ```c 286 | | type | copyable | 287 | +----------------+------------+ 288 | |simple types | | 289 | |string | Y | 290 | |interned string | | 291 | |array | Y | 292 | |immutable array | | 293 | |object | | 294 | |resource | | 295 | |reference | | 296 | ``` 297 | __copyable__ 的意思是当value发生duplication时是否需要或者能够copy,这个具体有两种情形下会发生: 298 | * a.从 __literal变量区__ 复制到 __局部变量区__ ,比如:`$a = [];`实际会有两个数组,而`$a = "hi~";//interned string`则只有一个string 299 | * b.局部变量区分离时(写时复制):如改变变量内容时引用计数大于1则需要分离,`$a = [];$b = $a; $b[] = 1;`这里会分离,类型是array所以可以复制,如果是对象:`$a = new user;$b = $a;$a->name = "dd";`这种情况是不会复制object的,$a、$b指向的对象还是同一个 300 | 301 | 具体literal、局部变量区变量的初始化、赋值后面编译、执行两篇文章会具体分析,这里知道变量有个`copyable`的属性就行了。 302 | 303 | #### 2.1.3.3 变量回收 304 | PHP变量的回收主要有两种:主动销毁、自动销毁。主动销毁指的就是 __unset__ ,而自动销毁就是PHP的自动管理机制,在return时减掉局部变量的refcount,即使没有显式的return,PHP也会自动给加上这个操作,另外一个就是写时复制时会断开原来value的指向,这时候也会检查断开后旧value的refcount。 305 | 306 | #### 2.1.3.4 垃圾回收 307 | PHP变量的回收是根据refcount实现的,当unset、return时会将变量的引用计数减掉,如果refcount减到0则直接释放value,这是变量的简单gc过程,但是实际过程中出现gc无法回收导致内存泄漏的bug,先看下一个例子: 308 | 309 | ```php 310 | $a = [1]; 311 | $a[] = &$a; 312 | 313 | unset($a); 314 | ``` 315 | `unset($a)`之前引用关系: 316 | 317 | ![gc_1](../img/gc_1.png) 318 | 319 | `unset($a)`之后: 320 | 321 | ![gc_2](../img/gc_2.png) 322 | 323 | 可以看到,`unset($a)`之后由于数组中有子元素指向`$a`,所以`refcount > 0`,无法通过简单的gc机制回收,这种变量就是垃圾,垃圾回收器要处理的就是这种情况,目前垃圾只会出现在array、object两种类型中,所以只会针对这两种情况作特殊处理:当销毁一个变量时,如果发现减掉refcount后仍然大于0,且类型是IS_ARRAY、IS_OBJECT则将此value放入gc可能垃圾双向链表中,等这个链表达到一定数量后启动检查程序将所有变量检查一遍,如果确定是垃圾则销毁释放。 324 | 325 | 标识变量是否需要回收也是通过`u1.type_flag`区分的: 326 | ```c 327 | #define IS_TYPE_COLLECTABLE 328 | ``` 329 | ```c 330 | | type | collectable | 331 | +----------------+-------------+ 332 | |simple types | | 333 | |string | | 334 | |interned string | | 335 | |array | Y | 336 | |immutable array | | 337 | |object | Y | 338 | |resource | | 339 | |reference | | 340 | ``` 341 | 具体的垃圾回收过程这里不再介绍,后面会单独分析。 342 | 343 | -------------------------------------------------------------------------------- /3/zend_autoload.md: -------------------------------------------------------------------------------- 1 | ### 3.4.6 类的自动加载 2 | 在实际使用中,通常会把一个类定义在一个文件中,然后使用时include加载进来,这样就带来一个问题:在每个文件的头部都需要包含一个长长的include列表,而且当文件名称修改时也需要把每个引用的地方都改一遍,另外前面我们也介绍过,原则上父类需要在子类定义之前定义,当存在大量类时很难得到保证,因此PHP提供了一种类的自动加载机制,当使用未被定义的类时自动调用类加载器将类加载进来,方便类的同一管理。 3 | 4 | 在内核实现上类的自动加载实际就是定义了一个钩子函数,实例化类时如果在EG(class_table)中没有找到对应的类则会调用这个钩子函数,调用完以后再重新查找一次。这个钩子函数保存在EG(autoload_func)中。 5 | 6 | PHP中提供了两种方式实现自动加载:`__autoload()`、`spl_autoload_register()`。 7 | 8 | ***(1)__autoload():*** 9 | 10 | 这种方式比较简单,用户自定义一个`__autoload()`函数即可,参数是类名,当实例化一个类是如果没有找到这个类则会查找用户是否定义了`__autoload()`函数,如果定义了则调用此函数,比如: 11 | ```php 12 | //文件1:my_class.php 13 | ", $class_name, "\n"; 41 | } 42 | 43 | function autoload_two($class_name){ 44 | echo "autoload_two->", $class_name, "\n"; 45 | } 46 | 47 | spl_autoload_register("autoload_one"); 48 | spl_autoload_register("autoload_two"); 49 | 50 | $obj = new my_class(); 51 | var_dump($obj); 52 | ``` 53 | 这个例子执行时就会将autoload_one()、autoload_two()都调一遍,假如第一个函数就成功注册了my_class类则不会再调后面的加载器。 54 | 55 | 内核查找类通过`zend_lookup_class_ex()`完成,我们简单看下其处理过程。 56 | ```c 57 | //file: zend_execute_API.c 58 | ZEND_API zend_class_entry *zend_lookup_class_ex(zend_string *name, const zval *key, int use_autoload) 59 | { 60 | ... 61 | //从EG(class_table)符号表找类的zend_class_entry,如果找到说明类已经编译,直接返回 62 | ce = zend_hash_find_ptr(EG(class_table), lc_name); 63 | if (ce) { 64 | if (!key) { 65 | zend_string_release(lc_name); 66 | } 67 | return ce; 68 | } 69 | ... 70 | //如果没有通过spl注册则看下是否定义了__autoload() 71 | if (!EG(autoload_func)) { 72 | zend_function *func = zend_hash_str_find_ptr(EG(function_table), "__autoload", sizeof("__autoload") - 1); 73 | if (func) { 74 | EG(autoload_func) = func; 75 | } else { 76 | return NULL; 77 | } 78 | } 79 | ... 80 | fcall_cache.function_handler = EG(autoload_func); 81 | ... 82 | //调用EG(autoload_func)函数,然后再查一次EG(class_table) 83 | if ((zend_call_function(&fcall_info, &fcall_cache) == SUCCESS) && !EG(exception)) { 84 | ce = zend_hash_find_ptr(EG(class_table), lc_name); 85 | } 86 | ... 87 | } 88 | ``` 89 | SPL的具体实现比较简单,这里不再介绍。 90 | -------------------------------------------------------------------------------- /3/zend_compile.md: -------------------------------------------------------------------------------- 1 | ## 3.1 PHP代码的编译 2 | 3 | PHP是解析型高级语言,事实上从Zend内核的角度来看PHP就是一个普通的C程序,它有main函数,我们写的PHP代码是这个程序的输入,然后经过内核的处理输出结果,内核将PHP代码"翻译"为C程序可识别的过程就是PHP的编译。 4 | 5 | 那么这个"翻译"过程具体都有哪些操作呢? 6 | 7 | C程序在编译时将一行行代码编译为机器码,每一个操作都认为是一条机器指令,这些指令写入到编译后的二进制程序中,执行的时候将二进制程序load进相应的内存区域(常量区、数据区、代码区)、分配运行栈,然后从代码区起始位置开始执行,这是C程序编译、执行的简单过程。 8 | 9 | 同样,PHP的编译与普通的C程序类似,只是PHP代码没有编译成机器码,而是解析成了若干条opcode数组,每条opcode就是C里面普通的struct,含义对应C程序的机器指令,执行的过程就是引擎依次执行opcode,比如我们在PHP里定义一个变量:`$a = 123;`,最终到内核里执行就是malloc一块内存,然后把值写进去。 10 | 11 | 所以PHP的解析过程任务就是将PHP代码转化为opcode数组,代码里的所有信息都保存在opcode中,然后将opcode数组交给zend引擎执行,opcode就是内核具体执行的命令,比如赋值、加减操作、函数调用等,每一条opcode都对应一个处理handle,这些handler是提前定义好的C函数。 12 | 13 | 从PHP代码到opcode是怎么实现的?最容易想到的方式就是正则匹配,当然过程没有这么简单。PHP编译过程包括词法分析、语法分析,使用re2c、bison完成,旧的PHP版本直接生成了opcode,PHP7新增了抽象语法树(AST),在语法分析阶段生成AST,然后再生成opcode数组。 14 | 15 | ![zend_compile2](../img/zend_compile2.png) 16 | 17 | PHP编译阶段的基本过程如下图: 18 | 19 | ![zend_compile_process](../img/zend_compile_process.png) 20 | 21 | 后面两个小节将看下 __PHP代码->AST->Opcodes__ 的具体编译过程。 22 | -------------------------------------------------------------------------------- /3/zend_compile_parse.md: -------------------------------------------------------------------------------- 1 | ### 3.1.1 词法解析、语法解析 2 | 这一节我们分析下PHP的解析阶段,即 __PHP代码->抽象语法树(AST)__ 的过程。 3 | 4 | PHP使用re2c、bison完成这个阶段的工作: 5 | * __re2c:__ 词法分析器,将输入分割为一个个有意义的词块,称为token 6 | * __bison:__ 语法分析器,确定词法分析器分割出的token是如何彼此关联的 7 | 8 | 例如: 9 | ```php 10 | $a = 2 + 3; 11 | ``` 12 | 词法分析器将上面的语句分解为这些token:$a、=、2、+、3,接着语法分析器确定了`2+3`是一个表达式,而这个表达式被赋值给了`a`,我们可以这样定义词法解析规则: 13 | ```c 14 | /*!re2c 15 | LABEL [a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]* 16 | LNUM [0-9]+ 17 | 18 | //规则 19 | "$"{LABEL} {return T_VAR;} 20 | {LNUM} {return T_NUM;} 21 | */ 22 | ``` 23 | 然后定义语法解析规则: 24 | ```c 25 | //token定义 26 | %token T_VAR 27 | %token T_NUM 28 | 29 | //语法规则 30 | statement: 31 | T_VAR '=' T_NUM '+' T_NUM {ret = str2int($3) + str2int($5);printf("%d",ret);} 32 | ; 33 | ``` 34 | 上面的语法规则只能识别两个数值相加,假如我们希望支持更复杂的运算,比如: 35 | ```php 36 | $a = 3 + 4 - 6; 37 | ``` 38 | 则可以配置递归规则: 39 | ```c 40 | //语法规则 41 | statement: 42 | T_VAR '=' expr {} 43 | ; 44 | expr: 45 | T_NUM {...} 46 | |expr '?' T_NUM {} 47 | ; 48 | ``` 49 | 这样将支持若干表达式,用语法分析树表示: 50 | 51 | ![](../img/zend_parse_1.png) 52 | 53 | 接下来我们看下PHP具体的解析过程,PHP编译阶段流程: 54 | 55 | ![zend_compile_process](../img/zend_compile_process.png) 56 | 57 | 其中 __zendparse()__ 就是词法、语法解析过程,这个函数实际就是bison中提供的语法解析函数 __yyparse()__ : 58 | ```c 59 | #define yyparse zendparse 60 | ``` 61 | __yyparse()__ 不断调用 __yylex()__ 得到token,然后根据token匹配语法规则: 62 | 63 | ![](../img/zend_parse_2.png) 64 | 65 | ```c 66 | #define yylex zendlex 67 | 68 | //zend_compile.c 69 | int zendlex(zend_parser_stack_elem *elem) 70 | { 71 | zval zv; 72 | int retval; 73 | ... 74 | 75 | again: 76 | ZVAL_UNDEF(&zv); 77 | retval = lex_scan(&zv); 78 | if (EG(exception)) { 79 | //语法错误 80 | return T_ERROR; 81 | } 82 | ... 83 | 84 | if (Z_TYPE(zv) != IS_UNDEF) { 85 | //如果在分割token中有zval生成则将其值复制到zend_ast_zval结构中 86 | elem->ast = zend_ast_create_zval(&zv); 87 | } 88 | 89 | return retval; 90 | } 91 | ``` 92 | 这里两个关键点需要注意: 93 | 94 | __(1) token值__:词法解析器解析到的token值内容就是token值,这些值统一通过 __zval__ 存储,上面的过程中可以看到调用lex_scan参数是是个zval*,在具体的命中规则总会将解析到的token保存到这个值,从而传递给语法解析器使用,比如PHP中的解析变量的规则:`$a;`,其词法解析规则为: 95 | ```c 96 | "$"{LABEL} { 97 | //将匹配到的token值保存在zval中 98 | zend_copy_value(zendlval, (yytext+1), (yyleng-1)); //只保存{LABEL}内容,不包括$,所以是yytext+1 99 | RETURN_TOKEN(T_VARIABLE); 100 | } 101 | ``` 102 | zendlval就是我们传入的zval*,yytext指向命中的token值起始位置,yyleng为token值的长度。 103 | 104 | __(2) 语义值类型__:bison调用re2c分割token有两个含义,第一个是token类型,另一个是token值,token类型一般以yylex的返回值告诉bison,而token值就是语义值,这个值一般定义为固定的类型,这个类型就是语义值类型,默认为int,可以通过 __YYSTYPE__ 定义,而PHP中这个类型是 __zend_parser_stack_elem__ ,这就是为什么zendlex的参数为`zend_parser_stack_elem`的原因。 105 | ```c 106 | #define YYSTYPE zend_parser_stack_elem 107 | 108 | typedef union _zend_parser_stack_elem { 109 | zend_ast *ast; //抽象语法树主要结构 110 | zend_string *str; 111 | zend_ulong num; 112 | } zend_parser_stack_elem; 113 | ``` 114 | 实际这是个union,ast类型用的比较多(其它两种类型暂时没发现有地方在用),这样可以通过%token、%type将对应的值修改为elem.ast,所以在zend_language_parser.y中使用的$$、$1、$2......多数都是 __zend_parser_stack_elem.ast__ : 115 | ```c 116 | %token T_LNUMBER "integer number (T_LNUMBER)" 117 | %token T_DNUMBER "floating-point number (T_DNUMBER)" 118 | %token T_STRING "identifier (T_STRING)" 119 | %token T_VARIABLE "variable (T_VARIABLE)" 120 | 121 | %type top_statement namespace_name name statement function_declaration_statement 122 | %type class_declaration_statement trait_declaration_statement 123 | %type interface_declaration_statement interface_extends_list 124 | ``` 125 | 126 | 语法解析器从start开始调用,然后层层匹配各个规则,语法解析器根据命中的语法规则创建AST节点,最后将生成的AST根节点赋到 __CG(ast)__ : 127 | ```c 128 | %% /* Rules */ 129 | 130 | start: 131 | top_statement_list { CG(ast) = $1; } 132 | ; 133 | 134 | top_statement_list: 135 | top_statement_list top_statement { $$ = zend_ast_list_add($1, $2); } 136 | | /* empty */ { $$ = zend_ast_create_list(0, ZEND_AST_STMT_LIST); } 137 | ; 138 | ``` 139 | 首先会创建一个根节点list,然后将后面不断命中top_statement生成的ast加到这个list中,zend_ast具体结构: 140 | 141 | ```c 142 | enum _zend_ast_kind { 143 | ZEND_AST_ZVAL = 1 << ZEND_AST_SPECIAL_SHIFT, 144 | ZEND_AST_ZNODE, 145 | 146 | /* list nodes */ 147 | ZEND_AST_ARG_LIST = 1 << ZEND_AST_IS_LIST_SHIFT, 148 | ... 149 | }; 150 | 151 | struct _zend_ast { 152 | zend_ast_kind kind; /* Type of the node (ZEND_AST_* enum constant) */ 153 | zend_ast_attr attr; /* Additional attribute, use depending on node type */ 154 | uint32_t lineno; /* Line number */ 155 | zend_ast *child[1]; /* Array of children (using struct hack) */ 156 | }; 157 | 158 | typedef struct _zend_ast_list { 159 | zend_ast_kind kind; 160 | zend_ast_attr attr; 161 | uint32_t lineno; 162 | uint32_t children; 163 | zend_ast *child[1]; 164 | } zend_ast_list; 165 | ``` 166 | 根节点实际为zend_ast_list,每条语句对应的ast保存在child中,使用中zend_ast_list、zend_ast可以相互转化,kind标识的是ast节点类型,后面会根据这个值生成具体的opcode,另外函数、类还会用到另外一种ast节点结构: 167 | ```c 168 | typedef struct _zend_ast_decl { 169 | zend_ast_kind kind; 170 | zend_ast_attr attr; /* Unused - for structure compatibility */ 171 | uint32_t start_lineno; //开始行号 172 | uint32_t end_lineno; //结束行号 173 | uint32_t flags; 174 | unsigned char *lex_pos; 175 | zend_string *doc_comment; 176 | zend_string *name; 177 | zend_ast *child[4]; //类中会将继承的父类、实现的接口以及类中的语句解析保存在child中 178 | } zend_ast_decl; 179 | ``` 180 | 这么看比较难理解,接下来我们从一个简单的例子看下最终生成的语法树。 181 | 182 | ```php 183 | $a = 123; 184 | $b = "hi~"; 185 | 186 | echo $a,$b; 187 | ``` 188 | 具体解析过程这里不再解释,有兴趣的可以翻下zend_language_parse.y中,这个过程不太容易理解,需要多领悟几遍,最后生成的ast如下图: 189 | 190 | ![zend_ast](../img/zend_ast.png) 191 | 192 | __总结:__ 193 | 194 | 这一节我们主要介绍了PHP词法、语法解析生成抽象语法树(AST)的过程,此过程是PHP语法实现的基础,也是zend引擎非常关键的一部分,后续介绍的内容都是基于此过程的产出结果展开的。这部分内容关键在于对re2c、bison的应用上,如果是初次接触它们可能不太容易理解,这里不再对re2c、bison作更多解释,想要了解更多的推荐看下 __《flex与bison》__ 这本书。 195 | -------------------------------------------------------------------------------- /3/zend_global_register.md: -------------------------------------------------------------------------------- 1 | ### 3.3.4 全局execute_data和opline 2 | Zend执行器在opcode的执行过程中,会频繁的用到execute_data和opline两个变量,execute_data为zend_execute_data结构,opline为当前执行的指令。普通的处理方式在执行每条opcode指令的handler时,会把execute_data地址作为参数传给handler使用,使用时先从当前栈上获取execute_data地址,然后再从堆上获取变量的数据,这种方式下Zend执行器展开后是下面这样: 3 | ```c 4 | ZEND_API void execute_ex(zend_execute_data *ex) 5 | { 6 | zend_execute_data *execute_data = ex; 7 | 8 | while (1) { 9 | int ret; 10 | 11 | if (UNEXPECTED((ret = ((opcode_handler_t)execute_data->opline->handler)(execute_data)) != 0)) { 12 | if (EXPECTED(ret > 0)) { 13 | execute_data = EG(current_execute_data); 14 | } else { 15 | return; 16 | } 17 | } 18 | } 19 | } 20 | ``` 21 | 执行器实际是一个大循环,从第一条opcode开始执行,execute_data->opline指向当前执行的指令,执行完以后指向下一条指令,opline类似eip(或rip)寄存器的作用。通过这个循环,ZendVM完成opcode指令的执行。opcode执行完后以后指向下一条指令的操作是在当前handler中完成,也就是说每条执行执行完以后会主动更新opline,这里会有下面几个不同的动作: 22 | ```c 23 | #define ZEND_VM_CONTINUE() return 0 24 | #define ZEND_VM_ENTER() return 1 25 | #define ZEND_VM_LEAVE() return 2 26 | #define ZEND_VM_RETURN() return -1 27 | ``` 28 | 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()将退出执行。下面看一个具体的例子: 29 | ```php 30 | $a = "hi~"; 31 | echo $a; 32 | ``` 33 | 执行过程如下图所示: 34 | 35 | ![](../img/executor.png) 36 | 37 | 以ZEND_ASSIGN这条赋值指令为例,其handler展开前如下: 38 | ```c 39 | static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ASSIGN_SPEC_CV_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS) 40 | { 41 | USE_OPLINE 42 | ... 43 | ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); 44 | } 45 | ``` 46 | 所有opcode的handler定义格式都是相同的,其参数列表通过ZEND_OPCODE_HANDLER_ARGS宏定义,展开后实际只有一个execute_data,展开后: 47 | ```c 48 | static int ZEND_ASSIGN_SPEC_CV_CONST_HANDLER(zend_execute_data *execute_data) 49 | { 50 | //USE_OPLINE 51 | const zend_op *opline = execute_data->opline; 52 | ... 53 | 54 | //ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION() 55 | execute_data->opline = execute_data->opline + 1; 56 | return 0; 57 | } 58 | ``` 59 | 从这个例子可以很清楚的看到,执行完以后会将execute_data->opline加1,也就是指向下一条opcode,然后返回0给execute_ex(),接着执行器在下一次循环时执行下一条opcode,依次类推,直至所有的opcode执行完成。这个处理过程比较简单,并没有不好理解的地方,而且整个过程看起来也都那么顺理成章。PHP7针对execute_data、opline两个变量的存储位置进行了优化,那就是使用全局寄存器保存这两个变量的地址,以实现更高效率的读取。这种方式下execute_data、opline直接从寄存器读取地址,在性能上大概有5%的提升(官方说法)。在分析PHP7的优化之前,我们先简单介绍下什么是寄存器变量。 60 | 61 | 寄存器变量存放在CPU的寄存器中,使用时,不需要访问内存直接从寄存器中读写,与存储在内存中的变量相比,寄存器变量具有更快的访问速度,在计算机的存储层次中,寄存器的速度最快,其次是内存,最慢的是硬盘。C语言中使用关键字register来声明局部变量为寄存器变量,需要注意的是,只有局部自动变量和形式参数才能够被定义为寄存器变量,全局变量和局部静态变量都不能被定义为寄存器变量。而且,一个计算机中寄存器数量是有限的,一般为2到3个,因此寄存器变量的数量不能太多。对于在一个函数中说明的多于2到3个的寄存器变量,C编译程序会自动地将寄存器变量变为自动变量。 受硬件寄存器长度的限制,寄存器变量只能是char、int或指针型,而不能使其他复杂数据类型。由于register变量使用的是硬件CPU中的寄存器,寄存器变量无地址,所以不能使用取地址运算符"&"求寄存器变量的地址。 62 | 63 | GCC从4.8.0版本开始支持了另外一项特性:全局寄存器变量(Global Register Variables,[详细介绍](https://gcc.gnu.org/onlinedocs/gcc-6.1.0/gcc/Global-Register-Variables.html)),也就是可以把全局变量定义为寄存器变量,从而可以实现函数间共享数据。可以通过下面的语法告诉编译器使用寄存器来保存数据: 64 | ```c 65 | register int *foo asm ("r12"); //r12、%r12 66 | ``` 67 | 或者: 68 | ```c 69 | register int *foo __asm__ ("r12"); //r12、%r12 70 | ``` 71 | 这里r12就是指定使用的寄存器,它必须是运行平台上有效的寄存器,这样就可以像使用普通的变量一样使用foo,但是foo同样没有地址,也就是无法通过&获取它的地址,在gdb调试时也无法使用foo符号,只能使用对应的寄存器获取数据。举个例子来看: 72 | ```c 73 | //main.c 74 | #include 75 | 76 | typedef struct _execute_data { 77 | int ip; 78 | }zend_execute_data; 79 | 80 | 81 | register zend_execute_data* execute_data __asm__ ("%r14"); 82 | 83 | int main(void) 84 | { 85 | execute_data = (zend_execute_data *)malloc(sizeof(zend_execute_data)); 86 | execute_data->ip = 9999; 87 | 88 | return 0; 89 | } 90 | ``` 91 | 编译:`$ gcc -o main -g main.c`,然后通过gdb看下: 92 | ```sh 93 | $ gdb main 94 | (gdb) break main 95 | (gdb) r 96 | Starting program: /home/qinpeng/c/php/main 97 | 98 | Breakpoint 1, main () at main.c:12 99 | 12 execute_data = (zend_execute_data *)malloc(sizeof(zend_execute_data)); 100 | (gdb) n 101 | 13 execute_data->ip = 9999; 102 | (gdb) n 103 | 15 return 0; 104 | ``` 105 | 这时我们就无法再像普通变量那样直接使用execute_data访问数据,只能通过r14寄存器读取: 106 | ```sh 107 | (gdb) p execute_data 108 | Missing ELF symbol "execute_data". 109 | (gdb) info register r14 110 | r14 0x601010 6295568 111 | (gdb) p ((zend_execute_data *)$r14)->ip 112 | $3 = 9999 113 | ``` 114 | 了解完全局寄存器变量,接下来我们再回头看下PHP7中的用法,处理也比较简单,就是在execute_ex()执行各opcode指令的过程中,不再将execute_data作为参数传给handler,而是通过寄存器保存execute_data及opline的地址,handler使用时直接从全局变量(寄存器)读取,执行完再把下一条指令更新到全局变量。 115 | 116 | 该功能需要GCC 4.8+支持,默认开启,可以通过 --disable-gcc-global-regs 编译参数关闭。以x86_64为例,execute_data使用r14寄存器,opline使用r15寄存器: 117 | ```c 118 | //file: zend_execute.c line: 2631 119 | # define ZEND_VM_FP_GLOBAL_REG "%r14" 120 | # define ZEND_VM_IP_GLOBAL_REG "%r15" 121 | 122 | //file: zend_vm_execute.h line: 315 123 | register zend_execute_data* volatile execute_data __asm__(ZEND_VM_FP_GLOBAL_REG); 124 | register const zend_op* volatile opline __asm__(ZEND_VM_IP_GLOBAL_REG); 125 | ``` 126 | execute_data、opline定义为全局变量,下面看下execute_ex()的变化,展开后: 127 | ```c 128 | ZEND_API void execute_ex(zend_execute_data *ex) 129 | { 130 | const zend_op *orig_opline = opline; 131 | zend_execute_data *orig_execute_data = execute_data; 132 | 133 | //将当前execute_data、opline保存到全局变量 134 | execute_data = ex; 135 | opline = execute_data->opline 136 | 137 | while (1) { 138 | ((opcode_handler_t)opline->handler)(); 139 | 140 | if (UNEXPECTED(!opline)) { 141 | execute_data = orig_execute_data; 142 | opline = orig_opline; 143 | 144 | return; 145 | } 146 | } 147 | } 148 | ``` 149 | 这个时候调用各opcode指令的handler时就不再传入execute_data的参数了,handler使用时直接从全局变量读取,仍以上面的赋值ZEND_ASSIGN指令为例,handler展开后: 150 | ```c 151 | static int ZEND_ASSIGN_SPEC_CV_CONST_HANDLER(void) 152 | { 153 | ... 154 | 155 | //ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION() 156 | opline = execute_data->opline + 1; 157 | return; 158 | } 159 | ``` 160 | 当调用函数时,会把execute_data、opline更新为被调函数的,然后回到execute_ex()开始执行被调函数的指令: 161 | ```c 162 | # define ZEND_VM_ENTER() execute_data = EG(current_execute_data); LOAD_OPLINE(); ZEND_VM_CONTINUE() 163 | ``` 164 | 展开后: 165 | ```c 166 | //ZEND_VM_ENTER() 167 | execute_data = execute_data->current_execute_data; 168 | opline = execute_data->opline; 169 | return; 170 | ``` 171 | 这两种处理方式并没有本质上的差异,只是通过全局寄存器变量提升了一些性能。 172 | 173 | > __Note:__ automake编译时的命令是cc,而不是gcc,如果更新gcc后发现PHP仍然没有支持这个特性,请检查下cc是否指向了新的gcc 174 | -------------------------------------------------------------------------------- /3/zend_magic_method.md: -------------------------------------------------------------------------------- 1 | ### 3.4.5 魔术方法 2 | PHP在类的成员方法中预留了一些特殊的方法,它们会在一些特殊的时机被调用(比如创建对象之初、访问成员属性时...),这类方法称为:魔术方法,包括:__construct()、__destruct()、__call()、__callStatic()、__get()、__set()、__isset()、__unset()、__sleep()、__wakeup()、__toString()、__invoke()、 __set_state()、 __clone() 和 __debugInfo(),关于这些方法的用法这里不作说明,不清楚的可以翻下官方文档。 3 | 4 | 魔术方法实际是PHP提供的一些特殊操作时的钩子函数,与普通成员方法无异,它们只是与一些操作的口头约定,并没有什么字段标识它们,比如我们定义了一个函数:my_function(),我们希望在这个函数处理对象时首先调用其成员方法my_magic(),那么my_magic()也可以认为是一个魔术方法。 5 | 6 | 魔术方法与普通成员方法一样保存在`zend_class_entry.function_table`中,另外针对一些内核常用到的成员方法在zend_class_entry中还有一些单独的指针指向具体的成员方法: 7 | ```c 8 | struct _zend_class_entry { 9 | ... 10 | union _zend_function *constructor; 11 | union _zend_function *destructor; 12 | union _zend_function *clone; 13 | union _zend_function *__get; 14 | union _zend_function *__set; 15 | union _zend_function *__unset; 16 | union _zend_function *__isset; 17 | union _zend_function *__call; 18 | union _zend_function *__callstatic; 19 | union _zend_function *__tostring; 20 | union _zend_function *__debugInfo; 21 | ... 22 | } 23 | ``` 24 | 在编译成员方法时如果发现与这些魔术方法名称一致,则除了插入`zend_class_entry.function_table`哈希表以外,还会设置zend_class_entry中对应的指针。 25 | 26 | ![](../img/magic_function.png) 27 | 28 | 具体在编译成员方法时设置:zend_begin_method_decl()。 29 | ```c 30 | void zend_begin_method_decl(zend_op_array *op_array, zend_string *name, zend_bool has_body) 31 | { 32 | ... 33 | //插入类的function_table中 34 | if (zend_hash_add_ptr(&ce->function_table, lcname, op_array) == NULL) { 35 | zend_error_noreturn(..); 36 | } 37 | 38 | if (!in_trait && zend_string_equals_ci(lcname, ce->name)) { 39 | if (!ce->constructor) { 40 | ce->constructor = (zend_function *) op_array; 41 | } 42 | } else if (zend_string_equals_literal(lcname, ZEND_CONSTRUCTOR_FUNC_NAME)) { 43 | ce->constructor = (zend_function *) op_array; 44 | } else if (zend_string_equals_literal(lcname, ZEND_DESTRUCTOR_FUNC_NAME)) { 45 | ce->destructor = (zend_function *) op_array; 46 | } else if (zend_string_equals_literal(lcname, ZEND_CLONE_FUNC_NAME)) { 47 | ce->clone = (zend_function *) op_array; 48 | } else if (zend_string_equals_literal(lcname, ZEND_CALL_FUNC_NAME)) { 49 | ce->__call = (zend_function *) op_array; 50 | } else if (zend_string_equals_literal(lcname, ZEND_CALLSTATIC_FUNC_NAME)) { 51 | ce->__callstatic = (zend_function *) op_array; 52 | } else if (...){ 53 | ... 54 | } 55 | ... 56 | } 57 | ``` 58 | 除了这几个其它魔术方法都没有单独的指针指向,比如:__sleep()、__wakeup(),这两个主要是serialize()、unserialize()序列化、反序列化时调用的,它们是在这俩函数中写死的,我们简单看下serialize()的实现,这个函数是通过扩展提供的: 59 | ```c 60 | //file: ext/standard/var.c 61 | PHP_FUNCTION(serialize) 62 | { 63 | zval *struc; 64 | php_serialize_data_t var_hash; 65 | smart_str buf = {0}; 66 | 67 | if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &struc) == FAILURE) { 68 | return; 69 | } 70 | 71 | php_var_serialize(&buf, struc, &var_hash); 72 | ... 73 | } 74 | ``` 75 | 最终由`php_var_serialize_intern()`处理,这个函数会根据不同的类型选择不同的处理方式: 76 | ```c 77 | static void php_var_serialize_intern(smart_str *buf, zval *struc, php_serialize_data_t var_hash) 78 | { 79 | ... 80 | switch (Z_TYPE_P(struc)) { 81 | case IS_FALSE: 82 | ... 83 | case IS_TRUE: 84 | ... 85 | case IS_NULL: 86 | ... 87 | case IS_LONG: 88 | ... 89 | } 90 | } 91 | ``` 92 | 其中类型是对象时将先检查`zend_class_function.function_table`中是否定义了`__sleep()`,如果有的话则调用: 93 | ```c 94 | //case IS_OBJEST: 95 | ... 96 | if (ce != PHP_IC_ENTRY && zend_hash_str_exists(&ce->function_table, "__sleep", sizeof("__sleep")-1)) { 97 | ZVAL_STRINGL(&fname, "__sleep", sizeof("__sleep") - 1); 98 | //调用用户自定义的__sleep()方法 99 | res = call_user_function_ex(CG(function_table), struc, &fname, &retval, 0, 0, 1, NULL); 100 | 101 | if (res == SUCCESS) { 102 | if (Z_TYPE(retval) != IS_UNDEF) { 103 | if (HASH_OF(&retval)) { 104 | php_var_serialize_class(buf, struc, &retval, var_hash); 105 | } else { 106 | smart_str_appendl(buf,"N;", 2); 107 | } 108 | zval_ptr_dtor(&retval); 109 | } 110 | return; 111 | } 112 | } 113 | //后面会走到IS_ARRAY分支继续序列化处理 114 | ... 115 | ``` 116 | 其它魔术方法与__sleep()类似,都是在一些特殊操作中固定调用的。 117 | -------------------------------------------------------------------------------- /3/zend_prop.md: -------------------------------------------------------------------------------- 1 | ### 3.4.4 动态属性 2 | 前面介绍的成员属性都是在类中明确的定义过的,这些属性在实例化时会被拷贝到对象空间中去,PHP中除了显示的在类中定义成员属性外,还可以动态的创建非静态成员属性,这种属性不需要在类中明确定义,可以直接通过:`$obj->property_name=xxx`、`$this->property_name = xxx`为对象设置一个属性,这种属性称之为动态属性,举个例子: 3 | ```php 4 | class my_class { 5 | public $id = 123; 6 | 7 | public function test($name, $value){ 8 | $this->$name = $value; 9 | } 10 | } 11 | 12 | $obj = new my_class; 13 | $obj->test("prop_1", array(1,2,3)); 14 | //或者直接: 15 | //$obj->prop_1 = array(1,2,3); 16 | 17 | print_r($obj); 18 | ``` 19 | 在`test()`方法中直接操作了没有定义的成员属性,上面的例子将输出: 20 | ``` 21 | my_class Object 22 | ( 23 | [id] => 123 24 | [prop_1] => Array 25 | ( 26 | [0] => 1 27 | [1] => 2 28 | [2] => 3 29 | ) 30 | ) 31 | ``` 32 | 前面类、对象两节曾介绍,非静态成员属性值在实例化时保存到了对象中,属性的操作按照编译时按顺序编好的序号操作,各对象对其非静态成员属性的操作互不干扰,那么动态属性是在运行时创建的,它是如何存储的呢? 33 | 34 | 与普通非静态属性不同,动态创建的属性保存在`zend_object->properties`哈希表中,查找的时候首先按照普通属性在`zend_class_entry.properties_info`找,没有找到再去`zend_object->properties`继续查找。动态属性的创建过程(即:修改属性的操作): 35 | ```c 36 | //zend_object->handlers->write_property: 37 | ZEND_API void zend_std_write_property(zval *object, zval *member, zval *value, void **cache_slot) 38 | { 39 | ... 40 | zobj = Z_OBJ_P(object); 41 | //先在zend_class_entry.properties_info查找此属性 42 | property_offset = zend_get_property_offset(zobj->ce, Z_STR_P(member), (zobj->ce->__set != NULL), cache_slot); 43 | 44 | if (EXPECTED(property_offset != ZEND_WRONG_PROPERTY_OFFSET)) { 45 | if (EXPECTED(property_offset != ZEND_DYNAMIC_PROPERTY_OFFSET)) { 46 | //普通属性,直接根据根据属性ofsset取出属性值 47 | } else if (EXPECTED(zobj->properties != NULL)) { //有动态属性 48 | ... 49 | //从动态属性中查找 50 | if ((variable_ptr = zend_hash_find(zobj->properties, Z_STR_P(member))) != NULL) { 51 | found: 52 | zend_assign_to_variable(variable_ptr, value, IS_CV); 53 | goto exit; 54 | } 55 | } 56 | } 57 | 58 | if (zobj->ce->__set) { 59 | //定义了__set()魔法函数 60 | }else if (EXPECTED(property_offset != ZEND_WRONG_PROPERTY_OFFSET)){ 61 | if (EXPECTED(property_offset != ZEND_DYNAMIC_PROPERTY_OFFSET)) { 62 | ... 63 | } else { 64 | //首次创建动态属性将在这里完成 65 | if (!zobj->properties) { 66 | rebuild_object_properties(zobj); 67 | } 68 | //将动态属性插入properties 69 | zend_hash_add_new(zobj->properties, Z_STR_P(member), value); 70 | } 71 | } 72 | } 73 | ``` 74 | 上面就是成员属性的修改过程,普通属性根据其offset再从对象中取出属性值进行修改,而首次创建动态属性将通过`rebuild_object_properties()`初始化`zend_object->properties`哈希表,后面再创建动态属性直接插入此哈希表,`rebuild_object_properties()`过程并不仅仅是创建一个HashTable,还会将普通成员属性值插入到这个数组中,与动态属性不同,这里的插入并不是增加原zend_value的refcount,而是创建了一个IS_INDIRECT类型的zval,指向原属性值zval,具体结构如下图。 75 | 76 | ![](../img/zend_dy_prop.png) 77 | 78 | > __Note:__ 这里不清楚将原有属性也插入properties的用意,已知用到的一个地方是在GC垃圾回收获取对象所有属性时(zend_std_get_gc()),如果有动态属性则直接返回properties给GC遍历,假如不把普通的显式定义的属性"拷贝"进来则需要返回、遍历两个数组。 79 | > 80 | > 另外一个地方需要注意,把原属性"转移"到properties并不仅仅是创建动态属性时触发的,调用对象的get_properties(即:zend_std_get_properties())也会这么处理,比如将一个object转为array时就会触发这个动作: $arr = (array)$object,通过foreach遍历一个对象时也会调用get_properties获取属性数组进行遍历。 81 | 82 | 成员属性的读取通过`zend_object->handlers->read_property`(默认zend_std_read_property())函数完成,动态属性的查找过程实际与`write_property`中相同: 83 | ```c 84 | zval *zend_std_read_property(zval *object, zval *member, int type, void **cache_slot, zval *rv) 85 | { 86 | ... 87 | zobj = Z_OBJ_P(object); 88 | 89 | //首先查找zend_class_entry.properties_info,普通属性可以在这里找到 90 | property_offset = zend_get_property_offset(zobj->ce, Z_STR_P(member), (type == BP_VAR_IS) || (zobj->ce->__get != NULL), cache_slot); 91 | 92 | if (EXPECTED(property_offset != ZEND_WRONG_PROPERTY_OFFSET)) { 93 | if (EXPECTED(property_offset != ZEND_DYNAMIC_PROPERTY_OFFSET)) { 94 | //普通属性 95 | retval = OBJ_PROP(zobj, property_offset); 96 | } else if (EXPECTED(zobj->properties != NULL)) { 97 | //动态属性从zend_object->properties中查找 98 | retval = zend_hash_find(zobj->properties, Z_STR_P(member)); 99 | if (EXPECTED(retval)) goto exit; 100 | } 101 | } 102 | ... 103 | } 104 | ``` 105 | -------------------------------------------------------------------------------- /3/zend_runtime_cache.md: -------------------------------------------------------------------------------- 1 | ## 3.5 运行时缓存 2 | 在本节开始之前我们先分析一个例子: 3 | ```php 4 | class my_class { 5 | public $id = 123; 6 | 7 | public function test() { 8 | echo $this->id; 9 | } 10 | } 11 | 12 | $obj = new my_class; 13 | $obj->test(); 14 | $obj->test(); 15 | ... 16 | ``` 17 | 这个例子定义了一个类,然后多次调用同一个成员方法,这个成员方法功能很简单:输出一个成员属性,根据前面对成员属性的介绍可以知道其查找过程为:"首先根据对象找到所属zend_class_entry,然后再根据属性名查找`zend_class_entry.properties_info`哈希表,得到`zend_property_info`,最后根据属性结构的offset定位到属性值的存储位置",概括一下这个过程就是:zend_object->zend_class_entry->properties_info->属性值,那么问题来了:每次执行`my_class::test()`时难道上面的过程都要完整走一遍吗? 18 | 19 | 我们再仔细看下这个过程,字面量"id"在"$this->id"此条语句中就是用来索引属性的,不管执行多少次它的任务始终是这个,那么有没有一种办法将"id"与查找到的zend_class_entry、zend_property_info.offset建立一种关联关系保存下来,这样再次执行时直接根据"id"拿到前面关联的这两个数据,从而避免多次重复相同的工作呢?这就是本节将要介绍的内容:运行时缓存。 20 | 21 | 在执行期间,PHP经常需要根据名称去不同的哈希表中查找常量、函数、类、成员方法、成员属性等,因此PHP提供了一种缓存机制用于缓存根据名称查找到的结果,以便再次执行同一opcode时直接复用上次缓存的值,无需重复查找,从而提高执行效率。 22 | 23 | 开始提到的那个例子中会缓存两个东西:zend_class_entry、zend_property_info.offset,此缓存可以认为是opcode操作的缓存,它只属于"$this->id"此语句的opcode:这样再次执行这条opcode时就直接取出上次缓存的两个值。 24 | 25 | 所以运行时缓存机制是在同一opcode执行多次的情况下才会生效,特别注意这里的同一opcode指的并不是opcode值相同,而是指内存里的同一份数据,比如:`echo $a; echo $a;`这种就不算,因为这是两条opcode。 26 | 27 | 那么缓存是如何保存和索引的呢?执行opcode时如何知道缓存的位置? 28 | 29 | 实际上运行时缓存是基于所属opcode中CONST操作数存储的,也就是说只有包含IS_CONST类型的操作数才有可能用到此机制,其它类型都不会用到,这是因为只有CONST操作数是固定不变的,其它CV、VAR等类型值都不是固定的,既然其值是不固定的那么缓存的值也就不是固定的,所以不会针对CONST以外类型的opcode操作进行缓存,还是以开始那个例子为例,比如:`echo $this->$var;`这种,操作数类型是CV,其正常查找时的zend_property_info是随$var值而变的,所以给他们建立一种不可变的关联关系,而:`echo $this->id;`中"id"是固定写死的,它索引到zend_property_info始终是不变的。 30 | 31 | 缓存的存储格式是一个数组,用于保存缓存的数据指针,而指针在数组中的起始存储位置则保存在CONST操作数对应的`zval.u2.cache_slot`中(前面讲过,CONST操作数对应值的zval保存在zend_op_array->literals数组中)。上面那个例子对应的缓存结构: 32 | 33 | ![](../img/runtime_cache_1.png) 34 | 35 | * __(1)__ 第一次执行`echo $this->id;`时首先根据$this取出zend_class_entry,然后根据“id”查找zend_class_entry.properties_info找到属性zend_property_info,取出此结构的offset,第一次执行后将zend_class_entry及offset保存到了test()函数的zend_op_array->run_time_cache中,占用16字节,起始位置为0,这个值记录在“id”的zval.u2.cache_slot中; 36 | * __(2)__ 之后再次执行`echo $this->id;`时直接根据opline从zend_op_literals中取出“id”的zval,得到缓存数据保存位置:0,然后去zend_op_array->run_time_cache取出缓存的zend_class_entry、offset。 37 | 38 | 这个例子缓存数据占用了16字节(2个sizeof(void*))大小的空间,而有的只需要8字节,取决于操作类型: 39 | 40 | * 8字节:常量、函数、类 41 | * 16字节:成员属性、成员方法、类常量 42 | 43 | 另外一个问题是这些操作数的缓存位置(zval.u2.cache_slot)是在什么阶段确定的呢?实际上这个值是在编译阶段确定的,通过zend_op_array.cache_size记录缓存可用起始位置,编译过程中如果发现当前操作适用缓存机制,则根据缓存数据的大小从cache_size开始分配8或16字节给那个操作数,cache_size向后移动对应大小,然后将起始位置保存于CONST操作数的zval.u2.cache_slot中,执行时直接根据这个值确定缓存位置。 44 | 45 | 具体缓存的读写通过以下几个宏完成: 46 | ```c 47 | //设置缓存 48 | CACHE_PTR(Z_CACHE_SLOT_P(EX_CONSTANT(opline->op1/2)), ptr); //ptr: 缓存的数据指针 49 | 50 | //读取缓存 51 | CACHED_PTR(Z_CACHE_SLOT_P(EX_CONSTANT(opline->op1/2))); 52 | 53 | //EX_CONSTANT(opline->op1/2)是取当前IS_CONST操作数对应数据的zval 54 | ``` 55 | 展开后: 56 | ```c 57 | ((void**)((char*)execute_data->run_time_cache + (num)))[0] 58 | ``` 59 | `execute_data->run_time_cache`缓存的`zend_op_array->run_time_cache`。 60 | -------------------------------------------------------------------------------- /4/break.md: -------------------------------------------------------------------------------- 1 | ## 4.4 中断及跳转 2 | PHP中的中断及跳转语句主要有break、continue、goto,这几种语句的实现基础都是跳转。 3 | 4 | ### 4.4.1 break与continue 5 | break用于结束当前for、foreach、while、do-while 或者 switch 结构的执行;continue用于跳过本次循环中剩余代码,进行下一轮循环。break、continue是非常相像的,它们都可以接受一个可选数字参数来决定跳过的循环层数,两者的不同点在于break是跳到循环结束的位置,而continue是跳到循环判断条件的位置,本质在于跳转位置的不同。 6 | 7 | break、continue的实现稍微有些复杂,下面具体介绍下其编译过程。 8 | 9 | 上一节我们已经介绍过循环语句的编译,其中在各种循环编译过程中有两个特殊操作:zend_begin_loop()、zend_end_loop(),分别在循环编译前以及编译后调用,这两步操作就是为break、continue服务的。 10 | 11 | 在每层循环编译时都会创建一个`zend_brk_cont_element`的结构: 12 | ```c 13 | typedef struct _zend_brk_cont_element { 14 | int start; 15 | int cont; 16 | int brk; 17 | int parent; 18 | } zend_brk_cont_element; 19 | ``` 20 | 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跳转。 21 | 22 | 各循环的`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记录的就是父层循环结构在该数组中的存储位置。 23 | ```c 24 | zend_brk_cont_element *get_next_brk_cont_element(zend_op_array *op_array) 25 | { 26 | op_array->last_brk_cont++; 27 | op_array->brk_cont_array = erealloc(op_array->brk_cont_array, sizeof(zend_brk_cont_element)*op_array->last_brk_cont); 28 | return &op_array->brk_cont_array[op_array->last_brk_cont-1]; 29 | } 30 | ``` 31 | 32 | 示例: 33 | ```php 34 | $i = 0; 35 | while(1){ 36 | while(1){ 37 | if($i > 10){ 38 | break 2; 39 | } 40 | ++$i 41 | } 42 | } 43 | ``` 44 | 循环编译完以后对应的内存结构: 45 | 46 | ![](../img/loop_op.png) 47 | 48 | 介绍完编译循环结构时为break、continue做的准备,接下来我们具体分析下break、continue的编译。 49 | 50 | 有了前面的准备,break、continue的编译过程就比较简单了,主要就是各生成一条临时opcode:ZEND_BRK、ZEND_CONT,这条opcode记录着两个重要信息: 51 | * __op1:__ 记录着当前循环`zend_brk_cont_element`结构的存储位置(在循环编译过程中CG(context).current_brk_cont记录着当前循环zend_brk_cont_element的位置) 52 | * __op2:__ 记录着要跳出循环的层级,如果break/continue没有加数字,则默认为1 53 | 54 | ```c 55 | void zend_compile_break_continue(zend_ast *ast) 56 | { 57 | zend_ast *depth_ast = ast->child[0]; 58 | 59 | zend_op *opline; 60 | int depth; 61 | 62 | if (depth_ast) { 63 | zval *depth_zv; 64 | ... 65 | depth = Z_LVAL_P(depth_zv); 66 | } else { 67 | depth = 1; 68 | } 69 | ... 70 | 71 | //生成opcode 72 | opline = zend_emit_op(NULL, ast->kind == ZEND_AST_BREAK ? ZEND_BRK : ZEND_CONT, NULL, NULL); 73 | opline->op1.num = CG(context).current_brk_cont; //break、continue所在循环层 74 | opline->op2.num = depth; //要跳出的层数 75 | } 76 | ``` 77 | `zend_compile_break_continue()`到这一步完成整个break、continue的编译还没有完成,因为`CG(active_op_array)->brk_cont_array`这个数组只是编译期间使用的一个临时结构,break、continue编译生成的opcode:ZEND_BRK、ZEND_CONT并不是运行时直接执行的,这条opcode在整个脚本编译完成后、执行前被优化为 __ZEND_JMP__ ,这个操作在`pass_two()`中完成,关于这个过程在《3.1.2.2 AST->zend_op_array》一节曾经介绍过。 78 | 79 | ```c 80 | ZEND_API zend_op_array *compile_file(zend_file_handle *file_handle, int type) 81 | { 82 | //语法解析 83 | zendparse(); 84 | 85 | //AST->opcodes 86 | zend_compile_top_stmt(CG(ast)); 87 | 88 | pass_two(op_array); 89 | ... 90 | } 91 | ``` 92 | ```c 93 | ZEND_API int pass_two(zend_op_array *op_array) 94 | { 95 | ... 96 | 97 | opline = op_array->opcodes; 98 | end = opline + op_array->last; 99 | while (opline < end) { 100 | switch (opline->opcode) { 101 | ... 102 | case ZEND_BRK: 103 | case ZEND_CONT: 104 | { 105 | //计算跳转位置 106 | uint32_t jmp_target = zend_get_brk_cont_target(op_array, opline); 107 | ... 108 | //将opcode修改为ZEND_JMP 109 | opline->opcode = ZEND_JMP; 110 | opline->op1.opline_num = jmp_target; 111 | opline->op2.num = 0; 112 | 113 | //将绝对跳转opcode位置修改为相对当前opcode的位置 114 | ZEND_PASS_TWO_UPDATE_JMP_TARGET(op_array, opline, opline->op1); 115 | } 116 | break; 117 | ... 118 | } 119 | } 120 | 121 | op_array->fn_flags |= ZEND_ACC_DONE_PASS_TWO; 122 | return 0; 123 | } 124 | ``` 125 | 从上面的过程可以看出,如果opcode为:ZEND_BRK或ZEND_CONT则统一设置opcode为`ZEND_JMP`,新opcode的op1记录的是break、continue跳到opcode的位置,这个值根据编译期间的`zend_brk_cont_element`计算得到,首先从op1、op2取出break、continue所在循环的zend_brk_cont_element结构以及要跳过的层级,然后根据`zend_brk_cont_element.parent`及层级数找到具体要跳出层的`zend_brk_cont_element`结构,从这个结构中获得那层循环判断条件及循环结束的opcode的位置。 126 | ```c 127 | static uint32_t zend_get_brk_cont_target(const zend_op_array *op_array, const zend_op *opline) { 128 | int nest_levels = opline->op2.num; //跳出的层级:break n; 129 | int array_offset = opline->op1.num;//break、continue所属循环zend_brk_cont_element的存储下标 130 | zend_brk_cont_element *jmp_to; 131 | do { 132 | //从break/continue所在循环层开始 133 | jmp_to = &op_array->brk_cont_array[array_offset]; 134 | if (nest_levels > 1) { 135 | //如果还没到要跳出的层数则接着跳到上层 136 | array_offset = jmp_to->parent; 137 | } 138 | } while (--nest_levels > 0); 139 | 140 | return opline->opcode == ZEND_BRK ? jmp_to->brk : jmp_to->cont; 141 | } 142 | ``` 143 | 上面那个例子最终执行前的opcode如下图: 144 | 145 | ![](../img/break_run.png) 146 | 147 | 执行时直接跳到对应的opcode位置即可。 148 | 149 | > __Note:__ 150 | > 151 | > 在多层循环中break、continue直接根据层级数字跳转很不方便,这点PHP可以借鉴Golang的语法:break/continue + LABEL,支持按标签break、continue,根据上一节及本节介绍的内容这一个实现起来并不复杂,有兴趣的可以思考下如何实现。 152 | 153 | ### 4.4.2 goto 154 | goto 操作符可以用来跳转到程序中的另一位置。该目标位置可以用目标名称加上冒号来标记,而跳转指令是 goto 之后接上目标位置的标记。PHP 中的 goto 有一定限制,目标位置只能位于同一个文件和作用域,也就是说无法跳出一个函数或类方法,也无法跳入到另一个函数,可以跳出循环但无法跳入循环(可以在同一层循环中跳转),多层循环中通常会用goto代替多层break。 155 | 156 | goto语法: 157 | ```php 158 | goto LABEL; 159 | 160 | LABEL: 161 | statement; 162 | ``` 163 | goto与label需要组合使用,其实现与break、continue类似,最终也是被优化为`ZEND_JMP`,首先看下定义一个label时都有哪些操作: 164 | ```c 165 | statement: 166 | ... 167 | 168 | | T_STRING ':' { $$ = zend_ast_create(ZEND_AST_LABEL, $1); } 169 | ; 170 | ``` 171 | label的编译过程非常简单,与循环结构的编译类似,编译时会把label插入`CG(context).labels`哈希表中,key就是label名称,value是一个`zend_label`结构: 172 | ```c 173 | typedef struct _zend_label { 174 | int brk_cont; //当前label所在循环 175 | uint32_t opline_num; //下一条opcode位置 176 | } zend_label; 177 | ``` 178 | brk_cont用于记录当前label所在的循环,这个值就是上面介绍的每个循环在`zend_op_array->brk_cont_array`数组中的位置;opline_num比较容易理解,就是label下面第一条opcode的位置。到这里你应该能猜得到goto的工作过程了,首先根据label名称在`CG(context).labels`查找到跳转label的`zend_label`结构,然后jmp到`zend_label.opline_num`的位置,brk_cont的作用是用来判断是不是goto到了另一层循环中去。label具体的编译过程: 179 | ```c 180 | void zend_compile_label(zend_ast *ast) 181 | { 182 | zend_string *label = zend_ast_get_str(ast->child[0]); 183 | zend_label dest; 184 | 185 | //编译时会将label插入CG(context).labels哈希表 186 | if (!CG(context).labels) { 187 | ALLOC_HASHTABLE(CG(context).labels); 188 | zend_hash_init(CG(context).labels, 8, NULL, label_ptr_dtor, 0); 189 | } 190 | 191 | //设置label信息:当前所在循环、下一条opcode编号 192 | dest.brk_cont = CG(context).current_brk_cont; 193 | dest.opline_num = get_next_op_number(CG(active_op_array)); 194 | 195 | if (!zend_hash_add_mem(CG(context).labels, label, &dest, sizeof(zend_label))) { 196 | zend_error_noreturn(E_COMPILE_ERROR, "Label '%s' already defined", ZSTR_VAL(label)); 197 | } 198 | } 199 | ``` 200 | goto的编译过程: 201 | ```c 202 | void zend_compile_goto(zend_ast *ast) 203 | { 204 | zend_ast *label_ast = ast->child[0]; 205 | znode label_node; 206 | zend_op *opline; 207 | uint32_t opnum_start = get_next_op_number(CG(active_op_array)); 208 | 209 | zend_compile_expr(&label_node, label_ast); 210 | 211 | //如果当前在一个循环内则有的情况下是不能简单跳出循环的 212 | zend_handle_loops_and_finally(); 213 | //编译一条临时opcode:ZEND_GOTO 214 | opline = zend_emit_op(NULL, ZEND_GOTO, NULL, &label_node); 215 | opline->op1.num = get_next_op_number(CG(active_op_array)) - opnum_start - 1; 216 | opline->extended_value = CG(context).current_brk_cont; 217 | } 218 | ``` 219 | goto初步被编译为`ZEND_GOTO`,其中label名称保存在op2,extended_value记录的是goto所在循环,如果没有在循环中这个值就等于-1,op1比较特殊,从上面编译的过程分析,它的值等于goto之间的opcode数,goto只编译了一条`ZEND_GOTO`哪来的其他opcode呢?这种情况就是goto在一个循环中,上一节介绍的循环结构中有一个比较特殊:foreach,它在遍历前会新生成一个zval用于遍历,这个zval是在循环结束时才被释放,假如foreach循环体中执行了goto,直接像普通跳转一样跳到了别的位置,那么这个zval就无法释放了,所以这种情况下在goto跳转前需要先执行这些收尾的opcode,这些opcode就是上面`zend_handle_loops_and_finally()`编译的,具体的细节这里不再展开,有兴趣的可以仔细研究下foreach编译时`zend_begin_loop()`的特殊处理。 220 | 221 | 后面的处理就与break、continue一样了,在`pass_two()`中`ZEND_GOTO`被重置为`ZEND_JMP`,具体的处理过程在`zend_resolve_goto_label()`,比较简单,不再赘述。 222 | 223 | 224 | 225 | -------------------------------------------------------------------------------- /4/exception.md: -------------------------------------------------------------------------------- 1 | ## 4.6 异常处理 2 | PHP的异常处理与其它语言的类似,在程序中可以抛出、捕获一个异常,异常抛出必须只有定义在try{...}块中才可以被捕获,捕获以后将跳到catch块中进行处理,不再执行try中抛出异常之后的代码。 3 | 4 | 异常可以在任意位置抛出,然后将由最近的一个try所捕获,如果在当前执行空间没有进行捕获,那么将调用栈一直往上抛,比如在一个函数内部抛出一个异常,但是函数内没有进行try,而在函数调用的位置try了,那么就由调用处的catch捕获。 5 | 6 | 接下来我们从两个方面介绍下PHP异常处理的实现。 7 | 8 | ### 4.6.1 异常处理的编译 9 | 异常捕获及处理的语法: 10 | ```php 11 | try{ 12 | try statement; 13 | }catch(exception_class_1 $e){ 14 | catch statement 1; 15 | }catch(exception_class_2 $e){ 16 | catch statement 2; 17 | }finally{ 18 | finally statement; 19 | } 20 | ``` 21 | try表示要捕获try statement中可能抛出的异常;catch是捕获到异常后的处理,可以定义多个,当try中抛出异常时会依次检查各个catch的异常类是否与抛出的匹配,如果匹配则有命中的那个catch块处理;finally为最后执行的代码,不管是否有异常抛出都会执行。 22 | 23 | 语法规则: 24 | ```c 25 | statement: 26 | ... 27 | | T_TRY '{' inner_statement_list '}' catch_list finally_statement 28 | { $$ = zend_ast_create(ZEND_AST_TRY, $3, $5, $6); } 29 | ... 30 | ; 31 | catch_list: 32 | /* empty */ 33 | { $$ = zend_ast_create_list(0, ZEND_AST_CATCH_LIST); } 34 | | catch_list T_CATCH '(' name T_VARIABLE ')' '{' inner_statement_list '}' 35 | { $$ = zend_ast_list_add($1, zend_ast_create(ZEND_AST_CATCH, $4, $5, $8)); } 36 | ; 37 | finally_statement: 38 | /* empty */ { $$ = NULL; } 39 | | T_FINALLY '{' inner_statement_list '}' { $$ = $3; } 40 | ; 41 | ``` 42 | 从语法规则可以看出,try-catch-finally最终编译为一个`ZEND_AST_TRY`节点,包含三个子节点,分别是:try statement、catch list、finally statement,try statement、finally statement就是普通的`ZEND_AST_STMT_LIST`节点,catch list包含多个`ZEND_AST_CATCH`节点,每个节点有三个子节点:exception class、exception object及catch statement,最终生成的AST: 43 | 44 | ![](../img/exception_ast.png) 45 | 46 | 具体的编译过程如下: 47 | 48 | * __(1)__ 向所属zend_op_array注册一个zend_try_catch_element结构,所有try都会注册一个这样的结构,与循环结构注册的zend_brk_cont_element类似,当前zend_op_array所有定义的异常保存在zend_op_array->try_catch_array数组中,这个结构用来记录try、catch以及finally开始的位置,具体结构: 49 | ```c 50 | typedef struct _zend_try_catch_element { 51 | uint32_t try_op; //try开始的opcode位置 52 | uint32_t catch_op; //第1个catch块的opcode位置 53 | uint32_t finally_op; //finally开始的opcode位置 54 | uint32_t finally_end;//finally结束的opcode位置 55 | } zend_try_catch_element; 56 | ``` 57 | * __(2)__ 编译try statement,编译完以后如果定义了catch块则编译一条`ZEND_JMP`,此opcode的作用时当无异常抛出时跳过所有catch跳到finally或整个异常之外的,因为catch块是在try statement之后编译的,所以具体的跳转值目前还无法确定; 58 | 59 | * __(3)__ 依次编译各个catch块,如果没有定义则跳过此步骤,每个catch编译时首先编译一条`ZEND_CATCH`,此opcode保存着此catch的exception class、exception object以及下一个catch块开始的位置,编译第1个catch时将此opcode的位置记录在zend_try_catch_element.catch_op上,接着编译catch statement,最后编译一条`ZEND_JMP`(最后一个catch不需要),此opcode的作用与步骤(2)的相同; 60 | 61 | * __(4)__ 将步骤(2)、步骤(3)中`ZEND_JMP`跳转值设置为finally第1条opcode或异常定义之外的代码,如果没有定义finally则结束编译,否则编译finally块,首先编译一条`ZEND_FAST_CALL`及`ZEND_JMP`,接着编译finally statement,最后编译一条`ZEND_FAST_RET`。 62 | 63 | 编译完以后的结构: 64 | 65 | ![](../img/exception_run.png) 66 | 67 | 异常的抛出通过throw一个异常对象来实现,这个对象必须继承>自Exception类,抛出异常的语法: 68 | ```php 69 | throw exception_object; 70 | ``` 71 | throw的编译比较简单,最终只编译为一条opcode:`ZEND_THROW`。 72 | 73 | ### 4.6.2 异常的抛出与捕获 74 | 上一小节我们介绍了exception结构在编译阶段的处理,接下来我们再介绍下运行时exception的处理过程,这个过程相对比较复杂,整体的讲其处理流程整体如下: 75 | 76 | * __(1)__ 检查抛出的是否是object,否则将导致error错误; 77 | * __(2)__ 将EG(exception)设置为抛出的异常对象,同时将当前stack(即:zend_execute_data)接下来要执行的opcode设置为`ZEND_HANDLE_EXCEPTION`; 78 | * __(3)__ 执行`ZEND_HANDLE_EXCEPTION`,查找匹配的catch: 79 | * __(3.1)__ 首先遍历当前zend_op_array下定义的所有异常捕获,即`zend_op_array->try_catch_array`数组,然后根据throw的位置、try开始的位置、catch开始的位置、finally开始的位置判断判断异常是否在try范围内,如果同时命中了多个try(即嵌套try的情况)则选择最后那个(也就是最里层的),遍历完以后如果命中了则进入步骤(3.2)处理,如果没有命中当前stack下任何try则进入步骤(4); 80 | * __(3.2)__ 到这一步表示抛出的异常在当前zend_op_array下有try拦截(注意这里只是表示异常在try中抛出的,但是抛出的异常并一定能被catch),然后根据当前try块的`zend_try_catch_element`结构取出第一个catch的位置,将opcode设置为zend_try_catch_element.catch_op,跳到第一个catch块开始的位置执行,即:执行`ZEND_CATCH`; 81 | * __(3.3)__ 执行`ZEND_CATCH`,检查抛出的异常对象是否与当前catch的类型匹配,检查的过程为判断两个类是否存在父子关系,如果匹配则表示异常被成功捕获,将EG(exception)清空,如果没有则跳到下一个catch的位置重复步骤(3.3),如果到最后一个catch仍然没有命中则在这个catch的位置抛出一个异常(实际还是原来按个异常,只是将抛出的位置转移了当前catch的位置),然后回到步骤(3); 82 | * __(4)__ 当前zend_op_array没能成功捕获异常,需要继续往上抛:回到调用位置,将接下来要执行的opcode设置为`ZEND_HANDLE_EXCEPTION`,比如函数中抛出了一个异常没有在函数中捕获,则跳到调用的位置继续捕获,回到步骤(3);如果到最终主脚本也没有被捕获则将结束执行并导致error错误。 83 | 84 | ![](../img/throw.png) 85 | 86 | 这个过程最复杂的地方在于异常匹配、传递的过程,主要为`ZEND_HANDLE_EXCEPTION`、`ZEND_CATCH`两条opcode之间的调用,当抛出一个异常时会终止后面opcode的执行,转向执行`ZEND_HANDLE_EXCEPTION`,根据异常抛出的位置定位到最近的一个try的catch位置,如果这个catch没有匹配则跳到下一个catch块,然后再次执行`ZEND_HANDLE_EXCEPTION`,如果到最后一个catch仍没有匹配则将异常抛出前位置EG(opline_before_exception)更新为最后一个catch的位置,再次执行`ZEND_HANDLE_EXCEPTION`,由于异常抛出的位置已经更新了所以不会再匹配上次检查过的那个catch,这个过程实际就是不断递归执行`ZEND_HANDLE_EXCEPTION`、`ZEND_CATCH`;如果当前zend_op_array都无法捕获则将异常抛向上一个调用栈继续捕获,下面根据一个例子具体说明下: 87 | ```php 88 | function my_func(){ 89 | //... 90 | throw new Exception("This is a exception from my_func()"); 91 | } 92 | 93 | try{ 94 | my_func(); 95 | }catch(ErrorException $e){ 96 | echo "ErrorException"; 97 | }catch(Exception $e){ 98 | echo "Exception"; 99 | } 100 | ``` 101 | my_func()中抛出了一个异常,首先在my_func()中抛出一个异常,然后在my_func()的zend_op_array中检查是不是能够捕获,发现没有,则回到调用的位置,再次检查,第1次匹配到`catch(ErrorException $e)`,检查后发现并不匹配,然后跳到下一个catch块继续匹配,第2次匹配到`catch(Exception $e)`,检查后发现命中,捕获成功。 102 | 103 | ![](../img/exception_run_2.png) 104 | 105 | 上面的过程并没有提到finally的执行时机,首先要明确finally在哪些情况下会执行,命中catch的情况比较简单,即在catch statement执行完以后跳到finally执行,另外一种情况是如果一个异常在try中但没有命中任何catch那么其finally也是会被执行的,这种情况的finally实际是在步骤(3)中执行的,最后一个catch检查完以后会更新异常抛出位置:EG(opline_before_exception),然后会再次执行`ZEND_HANDLE_EXCEPTION`,再次检查时就会发现没有命中任何catch但命中finally了(因为异常位置更新了),这时候就会将异常对象保存在finally块中,然后执行finally,执行完再将异常对象还原继续捕获,下面看下步骤(3)的具体处理过程: 106 | 107 | ```c 108 | static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_HANDLE_EXCEPTION_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS) 109 | { 110 | //op_num为异常抛出的位置,根据异常抛出前最后一条opcode与第一条opcode计算得出 111 | uint32_t op_num = EG(opline_before_exception) - EX(func)->op_array.opcodes; 112 | 113 | uint32_t catch_op_num = 0, finally_op_num = 0, finally_op_end = 0; 114 | 115 | //查找异常是不是被try了:找最近的一层try 116 | for (i = 0; i < EX(func)->op_array.last_try_catch; i++) { 117 | if (EX(func)->op_array.try_catch_array[i].try_op > op_num) { 118 | //try在抛出之后 119 | break; 120 | } 121 | in_finally = 0; 122 | //异常抛出位置在try后且比第一个catch位置小,表明这个try有可能捕获异常 123 | if (op_num < EX(func)->op_array.try_catch_array[i].catch_op) { 124 | //第一个catch的位置 125 | catch_op_num = EX(func)->op_array.try_catch_array[i].catch_op; 126 | } 127 | //当前try有finally 128 | if (op_num < EX(func)->op_array.try_catch_array[i].finally_op) { 129 | finally_op_num = EX(func)->op_array.try_catch_array[i].finally_op; 130 | finally_op_end = EX(func)->op_array.try_catch_array[i].finally_end; 131 | } 132 | if (op_num >= EX(func)->op_array.try_catch_array[i].finally_op && 133 | op_num < EX(func)->op_array.try_catch_array[i].finally_end) { 134 | finally_op_end = EX(func)->op_array.try_catch_array[i].finally_end; 135 | in_finally = 1; 136 | } 137 | } 138 | 139 | cleanup_unfinished_calls(execute_data, op_num); 140 | 141 | //异常命中了try但没有命中任何catch且那个try定义了finally:需要执行finally 142 | //catch_op_num >= finally_op_num是嵌套try的情况,因为finally是检查完所有catch、更新异常抛出位置之后再执行的 143 | //所以检查完内层try再检查外层循环时会出现这种情况 144 | if (finally_op_num && (!catch_op_num || catch_op_num >= finally_op_num)) { 145 | zval *fast_call = EX_VAR(EX(func)->op_array.opcodes[finally_op_end].op1.var); 146 | 147 | cleanup_live_vars(execute_data, op_num, finally_op_num); 148 | if (in_finally && Z_OBJ_P(fast_call)) { 149 | zend_exception_set_previous(EG(exception), Z_OBJ_P(fast_call)); 150 | } 151 | //临时将EG(exception)转移到finally下,执行完finally再抛出 152 | Z_OBJ_P(fast_call) = EG(exception); 153 | EG(exception) = NULL; 154 | fast_call->u2.lineno = (uint32_t)-1; 155 | ZEND_VM_SET_OPCODE(&EX(func)->op_array.opcodes[finally_op_num]); 156 | ZEND_VM_CONTINUE(); 157 | }else{ 158 | //这个是善后处理,因为异常抛出后后面的opcode将不再执行,但有些情况下还需要把一些资源释放掉 159 | //比如前面我们介绍goto时提到的foreach中是不能直接跳出的,throw也是类似 160 | cleanup_live_vars(execute_data, op_num, catch_op_num); 161 | ... 162 | if (catch_op_num) { 163 | //匹配到catch(但不一定命中),跳到catch处执行ZEND_CATCH进行判断 164 | ZEND_VM_SET_OPCODE(&EX(func)->op_array.opcodes[catch_op_num]); 165 | ZEND_VM_CONTINUE(); 166 | } else if (UNEXPECTED((EX(func)->op_array.fn_flags & ZEND_ACC_GENERATOR) != 0)) { 167 | ... 168 | } else { 169 | //当前zend_op_array下已经没有匹配到的try了,如果异常仍没有被捕获则将在zend_leave_helper_SPEC()将异常抛给prev_execute_data继续捕获 170 | ZEND_VM_TAIL_CALL(zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); 171 | } 172 | } 173 | } 174 | ``` 175 | 176 | 具体的实现过程还有很多额外的处理,这里不再展开,感兴趣的可以详细研究下`ZEND_HANDLE_EXCEPTION`、`ZEND_CATCH`两条opcode以及zend_exception.c中具体逻辑。 177 | 178 | ### 4.6.3 内核的异常处理 179 | 前面介绍的异常处理是PHP语言层面的实现,在内核中也有一套供内核使用的异常处理模型,也就是C语言异常处理的实现,如: 180 | ```c 181 | static int php_start_sapi(void) 182 | { 183 | ... 184 | 185 | zend_try { 186 | ... 187 | } zend_catch { 188 | ... 189 | } zend_end_try(); 190 | ... 191 | } 192 | ``` 193 | C语言并没有在语言层面提供try-catch机制,那么PHP中的是如何实现的呢?这个主要利用sigsetjmp()、siglongjmp()两个函数实现堆栈的保存、还原,在try的位置通过sigsetjmp()将当前位置的堆栈保存在一个变量中,异常抛出通过siglongjmp()跳回原位置,具体看下这几个宏的定义: 194 | ```c 195 | #define zend_try \ 196 | { \ 197 | JMP_BUF *__orig_bailout = EG(bailout); \ 198 | JMP_BUF __bailout; \ 199 | \ 200 | EG(bailout) = &__bailout; \ 201 | if (SETJMP(__bailout)==0) { 202 | #define zend_catch \ 203 | } else { \ 204 | EG(bailout) = __orig_bailout; 205 | #define zend_end_try() \ 206 | } \ 207 | EG(bailout) = __orig_bailout; \ 208 | } 209 | 210 | 211 | # define JMP_BUF sigjmp_buf 212 | # define SETJMP(a) sigsetjmp(a, 0) 213 | # define LONGJMP(a,b) siglongjmp(a, b) 214 | # define JMP_BUF sigjmp_buf 215 | ``` 216 | 展开后: 217 | ```c 218 | { 219 | //保存上一个zend_try记录的JMP_BUF,目的是实现多层嵌套try 220 | JMP_BUF *__orig_bailout = EG(bailout); 221 | JMP_BUF __bailout; 222 | 223 | //将当前堆栈保存在__bailout 224 | EG(bailout) = &__bailout; 225 | if (SETJMP(__bailout)==0) { 226 | //try中的代码 227 | //抛出异常调用:LONGJMP() 228 | }else { //异常抛出后到这个分支 229 | EG(bailout) = __orig_bailout; 230 | } 231 | EG(bailout) = __orig_bailout; 232 | } 233 | ``` 234 | 235 | -------------------------------------------------------------------------------- /4/if.md: -------------------------------------------------------------------------------- 1 | ## 4.2 选择结构 2 | 程序并不都是顺序执行的,选择结构用于判断给定的条件,根据判断的结果来控制程序的流程。PHP中通过if、elseif、else和switch语句实现条件控制。这一节我们就分析下PHP中两种条件语句的具体实现。 3 | 4 | ### 4.2.1 if语句 5 | If语句用法: 6 | ```php 7 | if(Condition1){ 8 | Statement1; 9 | }elseif(Condition2){ 10 | Statement2; 11 | }else{ 12 | Statement3; 13 | } 14 | ``` 15 | IF语句有两部分组成:condition(条件)、statement(声明),每个条件分支对应一组这样的组合,其中最后的else比较特殊,它没有条件,编译时也是按照这个逻辑编译为一组组的condition和statement,其具体的语法规则如下: 16 | ```c 17 | if_stmt: 18 | if_stmt_without_else %prec T_NOELSE { $$ = $1; } 19 | | if_stmt_without_else T_ELSE statement 20 | { $$ = zend_ast_list_add($1, zend_ast_create(ZEND_AST_IF_ELEM, NULL, $3)); } 21 | ; 22 | 23 | if_stmt_without_else: 24 | T_IF '(' expr ')' statement { $$ = zend_ast_create_list(1, ZEND_AST_IF, 25 | zend_ast_create(ZEND_AST_IF_ELEM, $3, $5)); } 26 | | if_stmt_without_else T_ELSEIF '(' expr ')' statement 27 | { $$ = zend_ast_list_add($1, zend_ast_create(ZEND_AST_IF_ELEM, $4, $6)); } 28 | ; 29 | ``` 30 | 从上面的语法规则可以看出,编译if语句时首先会创建一个`ZEND_AST_IF`的节点,这个节点是一个list,用于保存各个分支的condition、statement,编译每个分支时将创建一个`ZEND_AST_IF_ELEM`的节点,它有两个子节点,分别用来记录:condition、statement,然后把这个节点插入到`ZEND_AST_IF`下,最终生成的AST: 31 | 32 | ![](../img/ast_if.png) 33 | 34 | 编译opcode时顺序编译每个分支的condition、statement即可,编译过程大致如下: 35 | 36 | * __(1)__ 编译当前分支的condition语句,这里可能会有多个条件,但最终会归并为一个true/false的结果; 37 | * __(2)__ 编译完condition后编译一条ZEND_JMPZ的opcode,这条opcode用来判断当前condition最终为true还是false,如果当前condition成立直接继续执行本组statement即可,无需进行跳转,但是如果不成立就需要跳过本组的statement,所以这条opcode还需要知道该往下跳过多少条opcode,而跳过的这些opcode就是本组的statement,因此这个值需要在编译完本组statement后才能确定,现在还无法确定; 38 | * __(3)__ 编译当前分支的statement列表,其节点类型ZEND_AST_STMT_LIST,就是普通语句的编译; 39 | * __(4)__ 编译完statement后编译一条ZEND_JMP的opcode,这条opcode是当condition成立执行完本组statement时跳出if的,因为当前分支既然条件成立就不需要再跳到其他分支,执行完当前分支的statement后将直接跳出if,所以ZEND_JMP需要知道该往下跳过多少opcode,而跳过的这些opcode是后面所有分支的opcode数,只有编译完全部分支后才能确定; 40 | * __(5)__ 编译完statement后再设置步骤(2)中条件不成立时ZEND_JMPZ应该跳过的opcode数; 41 | * __(6)__ 重复上面的过程依次编译后面的condition、statement,编译完全部分支后再设置各分支在步骤(4)中ZEND_JMP跳出if的opcode位置。 42 | 43 | 具体的编译过程在`zend_compile_if()`中,过程比较清晰: 44 | ```c 45 | void zend_compile_if(zend_ast *ast) 46 | { 47 | zend_ast_list *list = zend_ast_get_list(ast); 48 | uint32_t i; 49 | uint32_t *jmp_opnums = NULL; 50 | 51 | //用来保存每个分支在步骤(4)中的ZEND_JMP opcode 52 | if (list->children > 1) { 53 | jmp_opnums = safe_emalloc(sizeof(uint32_t), list->children - 1, 0); 54 | } 55 | //依次编译各个分支 56 | for (i = 0; i < list->children; ++i) { 57 | zend_ast *elem_ast = list->child[i]; 58 | zend_ast *cond_ast = elem_ast->child[0]; //条件 59 | zend_ast *stmt_ast = elem_ast->child[1]; //声明 60 | 61 | znode cond_node; 62 | uint32_t opnum_jmpz; 63 | if (cond_ast) { 64 | //编译condition 65 | zend_compile_expr(&cond_node, cond_ast); 66 | //编译condition跳转opcode:ZEND_JMPZ 67 | opnum_jmpz = zend_emit_cond_jump(ZEND_JMPZ, &cond_node, 0); 68 | } 69 | //编译statement 70 | zend_compile_stmt(stmt_ast); 71 | //编译statement执行完后跳出if的opcode:ZEND_JMP(最后一个分支无需这条opcode) 72 | if (i != list->children - 1) { 73 | jmp_opnums[i] = zend_emit_jump(0); 74 | } 75 | if (cond_ast) { 76 | //设置ZEND_JMPZ跳过opcode数 77 | zend_update_jump_target_to_next(opnum_jmpz); 78 | } 79 | } 80 | 81 | if (list->children > 1) { 82 | //设置前面各分支statement执行完后应跳转的位置 83 | for (i = 0; i < list->children - 1; ++i) { 84 | zend_update_jump_target_to_next(jmp_opnums[i]); //设置每组stmt最后一条jmp跳转为if外 85 | } 86 | efree(jmp_opnums); 87 | } 88 | } 89 | ``` 90 | 最终if语句编译后基本是这样的结构: 91 | 92 | ![](../img/if_run.png) 93 | 94 | 执行时依次判断各分支条件是否成立,成立则执行当前分支statement,执行完后跳到if外语句;不成立则调到下一分支继续判断是否成立,以此类推。不管各分支条件有几个,其最终都会归并为一个结果,也就是每个分支只需要判断最终的条件值是否为true即可,而多个条件计算得到最终值的过程就是普通的逻辑运算。 95 | 96 | > __Note:__ 注意elseif与else if,上面介绍的是elseif的编译,而else if则实际相当于嵌套了一个if,也就是说一个if的分支中包含了另外一个if,在编译、执行的过程中这两个是有差别的。 97 | 98 | ### 4.2.2 switch语句 99 | switch语句与if类似,都是条件语句,很多时候需要将一个变量或者表达式与不同的值进行比较,根据不同的值执行不同的代码,这种场景下用if、switch都可以实现,但switch相对更加直观。 100 | 101 | switch语法: 102 | ```php 103 | switch(expression){ 104 | case value1: 105 | statement1; 106 | case value2: 107 | statement2; 108 | ... 109 | default: 110 | statementn; 111 | } 112 | ``` 113 | 这里并没有将break加入到switch的语法中,因为严格意义上break并不是switch的一部分,break属于另外一类单独的语法:中断语法,PHP中如果没有在switch中加break则执行时会从命中的那个case开始一直执行到结束,这与很多其它的语言不同(比如:golang)。 114 | 115 | 从switch的语法可以看出,switch主要包含两部分:expression、case list,case list包含多个case,每个case包含value、statement两部分。expression是一个表达式,但它将在case对比前执行,所以switch最终执行时就是拿expression的值逐个与case的value比较,如果相等则从命中case的statement开始向下执行。 116 | 117 | 下面看下switch的语法规则: 118 | ```c 119 | statement: 120 | ... 121 | | T_SWITCH '(' expr ')' switch_case_list { $$ = zend_ast_create(ZEND_AST_SWITCH, $3, $5); } 122 | ... 123 | ; 124 | 125 | switch_case_list: 126 | '{' case_list '}' { $$ = $2; } 127 | | '{' ';' case_list '}' { $$ = $3; } 128 | | ':' case_list T_ENDSWITCH ';' { $$ = $2; } 129 | | ':' ';' case_list T_ENDSWITCH ';' { $$ = $3; } 130 | ; 131 | 132 | case_list: 133 | /* empty */ { $$ = zend_ast_create_list(0, ZEND_AST_SWITCH_LIST); } 134 | | case_list T_CASE expr case_separator inner_statement_list 135 | { $$ = zend_ast_list_add($1, zend_ast_create(ZEND_AST_SWITCH_CASE, $3, $5)); } 136 | | case_list T_DEFAULT case_separator inner_statement_list 137 | { $$ = zend_ast_list_add($1, zend_ast_create(ZEND_AST_SWITCH_CASE, NULL, $4)); } 138 | ; 139 | 140 | case_separator: 141 | ':' 142 | | ';' 143 | ; 144 | ``` 145 | 从语法解析规则可以看出,switch最终被解析为一个`ZEND_AST_SWITCH`节点,这个节点主要包含两个子节点:expression、case list,其中expression节点比较简单,case list节点对应一个`ZEND_AST_SWITCH_LIST`节点,这个节点是一个list,有多个case子节点,每个case节点对应一个`ZEND_AST_SWITCH_CASE`节点,包括value(或expr)、statement两个子节点,生成的AST如下: 146 | 147 | ![](../img/ast_switch.png) 148 | 149 | 与if不同,switch不会像if那样依次把每个分支编译为一组组的condition、statement,而是会先编译全部case的value表达式,再编译全部case的statement,编译过程大致如下: 150 | 151 | * (1)首先编译expression,其最终将得到一个固定的value; 152 | * (2)依次编译每个case的value,如果value是一个表达式则编译expression,与(1)相同,执行时其最终也是一个固定的value,每个case编译一条ZEND_CASE的opcode,除了这条opcode还会编译出一条ZEND_JMPNZ的opcode,这条opcode用来跳到当前case的statement的开始位置,但是statement在这时还未编译,所以ZEND_JMPNZ的跳转值暂不确定; 153 | * (3)编译完全部case的value后接着从头开始编译每个case的statement,编译前首先设置步骤(2)中ZEND_JMPNZ的跳转值为当前statement起始位置。 154 | 155 | 具体编译过程在`zend_compile_switch()`中,这里不再展开,编译后的基本结构如下: 156 | 157 | ![](../img/switch_run.png) 158 | 159 | 执行时首先如果switch的是一个表达式则会首先执行表达式的语句,然后再拿最终的结果逐个与case的值比较,如果case也是一个表达式则也先执行表达式,执行完再与switch的值比较,比较结果如果为true则跳到当前case的statement位置开始顺序执行,如果结果为false则继续向下执行,与下一个case比较,以此类推。 160 | 161 | > __Note:__ 162 | > 163 | > __(1)__ case不管是表达式还是固定的值其最终比较时是一样的,如果是表达式则将其执行完以后再作比较,也就是说switch并不支持case多个值的用法,比如:case value1 || value2 : statement,这么写首先是会执行(value1 || value2),然后把结果与switch的值比较,并不是指switch的值等于value1或value2,这个地方一定要注意,如果想命中多个value只能写到不同case下 164 | > 165 | > __(2)__ switch的value与case的value比较用的是"==",而不是"===" 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /4/include.md: -------------------------------------------------------------------------------- 1 | ## 4.5 include/require 2 | 在实际应用中,我们不可能把所有的代码写到一个文件中,而是会按照一定的标准进行文件划分,include与require的功能就是将其他文件包含进来并且执行,比如在面向对象中通常会把一个类定义在单独文件中,使用时再include进来,类似其他语言中包的概念。 3 | 4 | include与require没有本质上的区别,唯一的不同在于错误级别,当文件无法被正常加载时include会抛出warning警告,而require则会抛出error错误,本节下面的内容将以include说明。 5 | 6 | 在分析include的实现过程之前,首先要明确include的基本用法及特点: 7 | 8 | * 被包含的文件将继承include所在行具有的全部变量范围,比如调用文件前面定义了一些变量,那么这些变量就能够在被包含的文件中使用,反之,被包含文件中定义的变量也将从include调用处开始可以被被调用文件所使用。 9 | * 被包含文件中定义的函数、类在include执行之后将可以被随处使用,即具有全局作用域。 10 | * include是在运行时加载文件并执行的,而不是在编译时。 11 | 12 | 这几个特性可以理解为include就是把其它文件的内容拷贝到了调用文件中,类似C语言中的宏(当然执行的时候并不是这样),举个例子来说明: 13 | ```php 14 | //a.php 15 | $var_1 = "hi"; 16 | $var_2 = array(1,2,3); 17 | 18 | include 'b.php'; 19 | 20 | var_dump($var_2); 21 | var_dump($var_3); 22 | 23 | //b.php 24 | $var_2 = array(); 25 | $var_3 = 9; 26 | ``` 27 | 执行`php a.php`结果显示$var_2值被修改为array()了,而include文件中新定义的$var_3也可以在调用文件中使用。 28 | 29 | 接下来我们就以这个例子详细介绍下include具体是如何实现的。 30 | 31 | ![zend_compile_process](../img/zend_compile_process.png) 32 | 33 | 前面我们曾介绍过Zend引擎的编译、执行两个阶段(见上图),整个过程的输入是一个文件,然后经过`PHP代码->AST->Opcodes->execute`一系列过程完成整个处理,编译过程的输入是一个文件,输出是zend_op_array,输出接着成为执行过程的输入,而include的处理实际就是这个过程,执行include时把被包含的文件像主脚本一样编译然后执行,接着在回到调用处继续执行。 34 | 35 | ![](../img/include.png) 36 | 37 | include的编译过程非常简单,只编译为一条opcode:`ZEND_INCLUDE_OR_EVAL`,下面看下其具体处理过程: 38 | ```c 39 | static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INCLUDE_OR_EVAL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS) 40 | { 41 | //include文件编译的zend_op_array 42 | zend_op_array *new_op_array=NULL; 43 | 44 | zval *inc_filename; 45 | zval tmp_inc_filename; 46 | zend_bool failure_retval=0; 47 | 48 | SAVE_OPLINE(); 49 | inc_filename = EX_CONSTANT(opline->op1); 50 | ... 51 | 52 | switch (opline->extended_value) { 53 | ... 54 | case ZEND_INCLUDE: 55 | case ZEND_REQUIRE: 56 | //编译include的文件 57 | new_op_array = compile_filename(opline->extended_value, inc_filename); 58 | break; 59 | ... 60 | } 61 | ... 62 | 63 | zend_execute_data *call; 64 | 65 | //分配运行时的zend_execute_data 66 | call = zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_CODE, 67 | (zend_function*)new_op_array, 0, EX(called_scope), Z_OBJ(EX(This))); 68 | 69 | //继承调用文件的全局变量符号表 70 | if (EX(symbol_table)) { 71 | call->symbol_table = EX(symbol_table); 72 | } else { 73 | call->symbol_table = zend_rebuild_symbol_table(); 74 | } 75 | //保存当前zend_execute_data,include执行完再还原 76 | call->prev_execute_data = execute_data; 77 | //执行前初始化 78 | i_init_code_execute_data(call, new_op_array, return_value); 79 | //zend_execute_ex执行器入口,如果没有自定义这个函数则默认为execute_ex() 80 | if (EXPECTED(zend_execute_ex == execute_ex)) { 81 | //将执行器切到新的zend_execute_data,回忆下execute_ex()中的切换过程 82 | ZEND_VM_ENTER(); 83 | } 84 | ... 85 | } 86 | ``` 87 | 整个过程比较容易理解,编译的过程不再重复,与之前介绍的没有差别;执行的过程实际非常像函数的调用过程,首先也是重新分配了一个zend_execute_data,然后将执行器切到新的zend_execute_data,执行完以后再切回调用处,如果include文件中只定义了函数、类,没有定义全局变量则执行过程实际直接执行return,只是在编译阶段将函数、类注册到EG(function_table)、EG(class_table)中了,这种情况比较简单,但是如果有全局变量定义处理就比较复杂了,比如上面那个例子,两个文件中都定义了全局变量,这些变量是如何被继承、合并的呢? 88 | 89 | 上面的过程中还有一个关键操作:`i_init_code_execute_data()`,关于这个函数在前面介绍`zend_execute()`时曾提过,这里面除了一些上下文的设置还会把当前zend_op_array下的变量移到EG(symbol_table)全局变量符号表中去,这些变量相对自己的作用域是局部变量,但它们定义在函数之外,实际也是全局变量,可以在函数中通过global访问,在执行前会把所有在php中定义的变量(zend_op_array->vars数组)插入EG(symbol_table),value指向zend_execute_data局部变量的zval,如下图: 90 | 91 | ![](../img/symbol_cv.png) 92 | 93 | 而include时也会执行这个步骤,如果发现var已经在EG(symbol_table)存在了,则会把value重新指向新的zval,也就是被包含文件的zend_execute_data的局部变量,同时会把原zval的value"拷贝"给新zval的value,概括一下就是被包含文件中的变量会继承、覆盖调用文件中的变量,这就是为什么被包含文件中可以直接使用调用文件中定义的变量的原因。被包含文件在`zend_attach_symbol_table()`完成以后EG(symbole_table)与zend_execute_data的关系: 94 | 95 | ![](../img/include_2.png) 96 | 97 | > 注意:这里include文件中定义的var_2实际是替换了原文件中的变量,也就是只有一个var_2,所以此处zend_array的引用是1而不是2 98 | 99 | 接下来就是被包含文件的执行,执行到`$var_2 = array()`时,将原array(1,2,3)引用减1变为0,这时候将其释放,然后将新的value:array()赋给$var_2,这个过程就是普通变量的赋值过程,注意此时调用文件中的$var_2仍然指向被释放掉的value,此时的内存关系: 100 | 101 | ![](../img/include_3.png) 102 | 103 | 看到这里你可能会有一个疑问:$var_2既然被重新修改为新的一个值了,那么为什么调用文件中的$var_2仍然指向释放掉的value呢?include执行完成回到原来的调用文件中后为何可以读取到新的$var_2值以及新定义的var_3呢?答案在被包含文件执行完毕return的过程中。 104 | 105 | 被包含文件执行完以后最后执行return返回调用文件include的位置,return时会把***被包含文件中的***全局变量从zend_execute_data中移到EG(symbol_table)中,这里的移动是把value值更新到EG(symbol_table),而不是像原来那样间接的指向value,这个操作在`zend_detach_symbol_table()`中完成,具体的return处理: 106 | ```c 107 | static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS) 108 | { 109 | ... 110 | if (EXPECTED((ZEND_CALL_KIND_EX(call_info) & ZEND_CALL_TOP) == 0)) { 111 | //将include文件中定义的变量移到EG(symbol_table) 112 | zend_detach_symbol_table(execute_data); 113 | //释放zend_op_array 114 | destroy_op_array(&EX(func)->op_array); 115 | 116 | old_execute_data = execute_data; 117 | //切回调用文件的zend_execute_data 118 | execute_data = EG(current_execute_data) = EX(prev_execute_data); 119 | //释放include文件的zend_execute_data 120 | zend_vm_stack_free_call_frame_ex(call_info, old_execute_data); 121 | 122 | //重新attach 123 | zend_attach_symbol_table(execute_data); 124 | 125 | LOAD_NEXT_OPLINE(); 126 | ZEND_VM_LEAVE(); 127 | }else{ 128 | //函数、主脚本返回的情况 129 | } 130 | } 131 | ``` 132 | `zend_detach_symbol_table()`操作: 133 | ```c 134 | ZEND_API void zend_detach_symbol_table(zend_execute_data *execute_data) 135 | { 136 | zend_op_array *op_array = &execute_data->func->op_array; 137 | HashTable *ht = execute_data->symbol_table; 138 | 139 | /* copy real values from CV slots into symbol table */ 140 | if (EXPECTED(op_array->last_var)) { 141 | zend_string **str = op_array->vars; 142 | zend_string **end = str + op_array->last_var; 143 | zval *var = EX_VAR_NUM(0); 144 | 145 | do { 146 | if (Z_TYPE_P(var) == IS_UNDEF) { 147 | zend_hash_del(ht, *str); 148 | } else { 149 | zend_hash_update(ht, *str, var); 150 | ZVAL_UNDEF(var); 151 | } 152 | str++; 153 | var++; 154 | } while (str != end); 155 | } 156 | } 157 | ``` 158 | 完成以后EG(symbol_table): 159 | 160 | ![](../img/include_4.png) 161 | 162 | 接着是还原调用文件的zend_execute_data,切回调用文件的include位置,在将执行器切回之前再次执行了`zend_attach_symbol_table()`,这时就会将原调用文件的变量重新插入全局变量符号表,插入$var_2、$var_3时发现已经存在了,则将局部变量区的$var_2、$var_3的value修改为这个值,这就是$var_2被include文件更新后覆盖原value的过程,同时$var_3也因为在调用文件中出现了所以值被修改为include中设定的值,此时的内存关系: 163 | 164 | ![](../img/include_5.png) 165 | 166 | 这就是include的实现原理,整个过程并不复杂,比较难理解的一点在于两个文件之间变量的继承、覆盖,可以仔细研究下上面不同阶段时的内存关系图。 167 | 168 | 最后简单介绍下include_once、require_once,这两个与include、require的区别是在一次请求中同一文件只会被加载一次,第一次执行时会把这个文件保存在EG(included_files)哈希表中,再次加载时检查这个哈希表,如果发现已经加载过则直接跳过。 169 | -------------------------------------------------------------------------------- /4/loop.md: -------------------------------------------------------------------------------- 1 | ## 4.3 循环结构 2 | 实际应用中有许多具有规律性的重复操作,因此在程序中就需要重复执行某些语句。循环结构是在一定条件下反复执行某段程序的流程结构,被反复执行的程序被称为循环体。循环语句是由循环体及循环的终止条件两部分组成的。 3 | 4 | PHP中的循环结构有4种:while、for、foreach、do while,接下来我们分析下这几个结构的具体的实现。 5 | 6 | ### 4.3.1 while循环 7 | while循环的语法: 8 | ```php 9 | while(expression) 10 | { 11 | statement;//循环体 12 | } 13 | ``` 14 | while的结构比较简单,由两部分组成:expression、statement,其中expression为循环判断条件,当expression为true时重复执行statement,具体的语法规则: 15 | ```c 16 | statement: 17 | ... 18 | | T_WHILE '(' expr ')' while_statement { $$ = zend_ast_create(ZEND_AST_WHILE, $3, $5); } 19 | ... 20 | ; 21 | 22 | while_statement: 23 | statement { $$ = $1; } 24 | | ':' inner_statement_list T_ENDWHILE ';' { $$ = $2; } 25 | ; 26 | ``` 27 | 从while语法规则可以看出,在解析时会创建一个`ZEND_AST_WHILE`节点,expression、statement分别保存在两个子节点中,其AST如下: 28 | 29 | ![](../img/ast_while.png) 30 | 31 | while编译的过程也比较简单,比较特别的是while首先编译的是循环体,然后才是循环判断条件,更像是do while,编译过程大致如下: 32 | * __(1)__ 首先编译一条ZEND_JMP的opcode,这条opcode用来跳到循环判断条件expression的位置,由于while是先编译循环体再编译循环条件,所以此时还无法确定具体的跳转值; 33 | * __(2)__ 编译循环体statement;编译完成后更新步骤(1)中ZEND_JMP的跳转值; 34 | * __(3)__ 编译循环判断条件expression; 35 | * __(4)__ 编译一条ZEND_JMPNZ的opcode,这条opcode用于循环判断条件执行完以后跳到循环体的,如果循环条件成立则通过此opcode跳到循环体开始的位置,否则继续往下执行(即:跳出循环)。 36 | 37 | 具体的编译过程: 38 | ```c 39 | void zend_compile_while(zend_ast *ast) 40 | { 41 | zend_ast *cond_ast = ast->child[0]; 42 | zend_ast *stmt_ast = ast->child[1]; 43 | znode cond_node; 44 | uint32_t opnum_start, opnum_jmp, opnum_cond; 45 | 46 | //(1)编译ZEND_JMP 47 | opnum_jmp = zend_emit_jump(0); 48 | 49 | zend_begin_loop(ZEND_NOP, NULL); 50 | 51 | //(2)编译循环体statement,opnum_start为循环体起始位置 52 | opnum_start = get_next_op_number(CG(active_op_array)); 53 | zend_compile_stmt(stmt_ast); 54 | 55 | //设置ZEND_JMP opcode的跳转值 56 | opnum_cond = get_next_op_number(CG(active_op_array)); 57 | zend_update_jump_target(opnum_jmp, opnum_cond); 58 | 59 | //(3)编译循环条件expression 60 | zend_compile_expr(&cond_node, cond_ast); 61 | 62 | //(4)编译ZEND_JMPNZ,用于循环条件成立时跳回循环体开始位置:opnum_start 63 | zend_emit_cond_jump(ZEND_JMPNZ, &cond_node, opnum_start); 64 | 65 | zend_end_loop(opnum_cond); 66 | } 67 | ``` 68 | 编译后opcode整体如下: 69 | 70 | ![](../img/while_run.png) 71 | 72 | 运行时首先执行`ZEND_JMP`,跳到while条件expression处开始执行,然后由`ZEND_JMPNZ`对条件的执行结果进行判断,如果条件成立则跳到循环体statement起始位置开始执行,如果条件不成立则继续向下执行,跳出while,第一次循环执行以后将不再执行`ZEND_JMP`,后续循环只有靠`ZEND_JMPNZ`控制跳转,循环体执行完成后接着执行循环判断条件,进行下一轮循环的判断。 73 | 74 | > __Note:__ 实际执行时可能会省略`ZEND_JMPNZ`这一步,这是因为很多while条件expression执行完以后会对下一条opcode进行判断,如果是`ZEND_JMPNZ`则直接根据条件成立与否进行快速跳转,不需要再由`ZEND_JMPNZ`判断,比如: 75 | > 76 | > $a = 123; 77 | > while($a > 100){ 78 | > echo "yes"; 79 | > } 80 | > `$a > 100`对应的opcode:ZEND_IS_SMALLER,执行时发现$a与100类型可以直接比较(都是long),则直接就能知道循环条件的判断结果,这种情况下将会判断下一条opcode是否为ZEND_JMPNZ,是的话直接设置下一条要执行的opcode,这样就不需要再单独执行依次ZEND_JMPNZ了。 81 | > 82 | > 上面的例子如果`$a = '123';`就不会快速进行处理了,而是按照正常的逻辑调用ZEND_JMPNZ。 83 | 84 | ### 4.3.2 do while循环 85 | do while与while非常相似,唯一的区别在于do while第一次执行时不需要判断循环条件。 86 | 87 | do while循环的语法: 88 | ```php 89 | do{ 90 | statement;//循环体 91 | }while(expression) 92 | ``` 93 | do while编译过程与while的基本一致,不同的地方在于do while没有`ZEND_JMP`这条opcode: 94 | ```c 95 | void zend_compile_do_while(zend_ast *ast) 96 | { 97 | zend_ast *stmt_ast = ast->child[0]; 98 | zend_ast *cond_ast = ast->child[1]; 99 | 100 | znode cond_node; 101 | uint32_t opnum_start, opnum_cond; 102 | 103 | //(1)编译循环体statement,opnum_start为循环体起始位置 104 | opnum_start = get_next_op_number(CG(active_op_array)); 105 | zend_compile_stmt(stmt_ast); 106 | 107 | //(2)编译循环判断条件expression 108 | opnum_cond = get_next_op_number(CG(active_op_array)); 109 | zend_compile_expr(&cond_node, cond_ast); 110 | 111 | //(3)编译ZEND_JMPNZ 112 | zend_emit_cond_jump(ZEND_JMPNZ, &cond_node, opnum_start); 113 | } 114 | ``` 115 | 编译后的结果: 116 | 117 | ![](../img/do_run.png) 118 | 119 | 运行时首先执行循环体statement,然后执行循环判断条件,如果条件成立跳到循环体起始位置,否则结束循环。 120 | 121 | ### 4.3.3 for循环 122 | for循环语法: 123 | ```php 124 | for (init expr; condition expr; loop expr){ 125 | statement 126 | } 127 | ``` 128 | init expr在循环开始前无条件执行一次,后面循环不再执行;condition expr在每次循环开始前运算,是循环的判断条件,如果值为true,则继续循环,执行循环体,如果值为false,则终止循环;loop expr在每次循环体执行完以后被执行。 129 | 130 | for的语法规则: 131 | ```c 132 | statement: 133 | ... 134 | | T_FOR '(' for_exprs ';' for_exprs ';' for_exprs ')' for_statement 135 | { $$ = zend_ast_create(ZEND_AST_FOR, $3, $5, $7, $9); } 136 | ... 137 | ; 138 | ``` 139 | 从语法规则可以看出,for被编译为`ZEND_AST_FOR`节点,包含4个子节点,分别为:expr1、expr2、expr3、statement。 140 | 141 | ![](../img/ast_for.png) 142 | 143 | for的编译与while类似,只是多了init expr、loop expr两部分,编译过程大致如下: 144 | * __(1)__ 首先编译初始化表达式:init expr; 145 | * __(2)__ 编译一条`ZEND_JMP`的opcode,此opcode用于跳到条件expression位置,具体跳转值需要后面才能确定; 146 | * __(3)__ 编译循环体statement; 147 | * __(4)__ 编译loop expr;然后设置步骤(2)中`ZEND_JMP`的跳转值; 148 | * __(5)__ 编译循环条件:condition expr; 149 | * __(6)__ 编译一条`ZEND_JMPNZ`,此opcode用于循环条件成立时跳到循环体起始位置。 150 | 151 | 具体编译过程: 152 | ```c 153 | void zend_compile_for(zend_ast *ast) 154 | { 155 | zend_ast *init_ast = ast->child[0]; 156 | zend_ast *cond_ast = ast->child[1]; 157 | zend_ast *loop_ast = ast->child[2]; 158 | zend_ast *stmt_ast = ast->child[3]; 159 | 160 | znode result; 161 | uint32_t opnum_start, opnum_jmp, opnum_loop; 162 | 163 | //(1)编译init expression 164 | zend_compile_expr_list(&result, init_ast); 165 | zend_do_free(&result); 166 | 167 | //(2)编译ZEND_JMP 168 | opnum_jmp = zend_emit_jump(0); 169 | 170 | //opnum_start是循环体起始位置 171 | opnum_start = get_next_op_number(CG(active_op_array)); 172 | 173 | //(3)编译循环体 174 | zend_compile_stmt(stmt_ast); 175 | 176 | //(4)编译loop expression 177 | opnum_loop = get_next_op_number(CG(active_op_array)); 178 | zend_compile_expr_list(&result, loop_ast); 179 | zend_do_free(&result); 180 | 181 | //设置ZEND_JMP跳转值 182 | zend_update_jump_target_to_next(opnum_jmp); 183 | 184 | //(5)编译循环条件expression 185 | zend_compile_expr_list(&result, cond_ast); 186 | zend_do_extended_info(); 187 | 188 | //(6)编译ZEND_JMPNZ 189 | zend_emit_cond_jump(ZEND_JMPNZ, &result, opnum_start); 190 | } 191 | ``` 192 | 最终编译结果: 193 | 194 | ![](../img/for_run.png) 195 | 196 | 运行时首先执行初始化表达式:init expression,然后执行`ZEND_JMP`跳到循环条件expression处,如果条件成立则执行`ZEND_JMPNZ`跳到循环体起始位置依次执行循环体、loop expression,如果条件不成立则终止循环,第一次循环之后就是:`循环条件->ZEND_JMPNZ->循环体->loop expression`之间循环了。 197 | 198 | ### 4.3.4 foreach循环 199 | foreach是PHP针对数组、对象提供的一种遍历方式,foreach语法: 200 | ```php 201 | foreach (array_expression as $key => $value){ 202 | statement 203 | } 204 | ``` 205 | 遍历arraiy_expression时每次循环会把当前单元的值赋给$value,当前单元的键值赋给$key,其中$key可以省略,$value前也可以加"&"表示引用单元的值。 206 | 207 | foreach的语法规则: 208 | ```c 209 | statement: 210 | ... 211 | //省略key的规则: foreach($array as $v){ ... } 212 | | T_FOREACH '(' expr T_AS foreach_variable ')' foreach_statement 213 | { $$ = zend_ast_create(ZEND_AST_FOREACH, $3, $5, NULL, $7); } 214 | //有key的规则: foreach($array as $k=>$v){ ... } 215 | | T_FOREACH '(' expr T_AS foreach_variable T_DOUBLE_ARROW foreach_variable ')' foreach_statement 216 | { $$ = zend_ast_create(ZEND_AST_FOREACH, $3, $7, $5, $9); } 217 | ... 218 | ; 219 | ``` 220 | foreach在编译阶段解析为`ZEND_AST_FOREACH`节点,包含4个子节点,分别表示:遍历的数组或对象、遍历的value、遍历的key以及循环体,生成的AST类似这样: 221 | 222 | ![](../img/ast_foreach.png) 223 | 224 | 如果value是指向数组或对象成员的引用,则value对应的节点类型为`ZEND_AST_REF`。 225 | 226 | 相对上面几种常规的循环结构,foreach的实现略显复杂:$key、$value实际就是两个普通的局部变量,遍历的过程就是对两个局部变量不断赋值、更新的过程,以数组为例,首先将数组拷贝一份用于遍历(只拷贝zval,value还是指向同一份),从arData第一个元素开始,把Bucket.zval.value值赋值给$value,把Bucket.key(或Bucket.h)赋值给$key,然后更新迭代位置:将下一个元素的位置记录在`zval.u2.fe_iter_idx`中,这样下一轮遍历时直接从这个位置开始,这也是遍历前为什么要拷贝一份zval用于遍历的原因,如果发现`zval.u2.fe_iter_idx`已经到达arData末尾了则结束遍历,销毁一开始拷贝的zval。举个例子来看: 227 | 228 | ```php 229 | $arr = array(1,2,3); 230 | foreach($arr as $k=>$v){ 231 | echo $v; 232 | } 233 | ``` 234 | 局部变量对应的内存结构: 235 | 236 | ![](../img/foreach_struct.png) 237 | 238 | 如果value是引用则在循环前首先将原数组或对象重置为引用类型,然后新分配一个zval指向这个引用,后面的过程就与上面的一致了,仍以上面的例子为例,如果是:`foreach($arr as $k=>&$v){ ... }`则: 239 | 240 | ![](../img/foreach_ref_struct.png) 241 | 242 | 了解了foreach的实现、运行机制我们再回头看下其编译过程: 243 | 244 | * __(1)__ 编译拷贝数组、对象操作的指令:ZEND_FE_RESET_R,如果value是引用则是ZEND_FE_RESET_RW。执行时如果发现遍历的变量不是数组、对象,则抛出一个warning,然后跳出循环,所以这条指令还需要知道跳出的位置,这个位置需要编译完foreach以后才能确定; 245 | * __(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; 246 | * __(3)__ 如果foreach定义了key则编译一条赋值opcode,此操作是对key进行赋值; 247 | * __(4)__ 编译循环体statement; 248 | * __(5)__ 编译跳回遍历开始位置的opcode:`ZEND_JMP`,一次遍历结束时会跳回步骤(2)编译的opcode处进行下次遍历; 249 | * __(6)__ 设置步骤(1)、(2)两条opcode跳过的opcode数; 250 | * __(7)__ 编译`ZEND_FE_FREE`,此操作用于释放步骤(1)"拷贝"的数组。 251 | 252 | 最终编译后的结构: 253 | 254 | ![](../img/foreach_run.png) 255 | 256 | 运行时的步骤: 257 | * __(1)__ 执行`ZEND_FE_RESET_R`,过程上面已经介绍了; 258 | * __(2)__ 执行`ZEND_FE_FETCH_R`,此opcode的操作主要有三个:检查遍历位置是否到达末尾、将数组元素的value赋值给$value、将数组元素的key赋值给一个临时变量(注意与value不同); 259 | * __(3)__ 如果定义了key则执行`ZEND_ASSIGN`,将key的值从临时变量赋值给$key,否则跳到步骤(4); 260 | * __(4)__ 执行循环体的statement; 261 | * __(5)__ 执行`ZEND_JMPNZ`跳回步骤(2); 262 | * __(6)__ 遍历结束后执行`ZEND_FE_FREE`释放数组。 263 | 264 | PHP中还有几个与遍历相关的函数: 265 | 266 | * current() - 返回数组中的当前单元 267 | * each() - 返回数组中当前的键/值对并将数组指针向前移动一步 268 | * end() - 将数组的内部指针指向最后一个单元 269 | * next() - 将数组中的内部指针向前移动一位 270 | * prev() - 将数组的内部指针倒回一位 271 | 272 | 273 | -------------------------------------------------------------------------------- /4/type.md: -------------------------------------------------------------------------------- 1 | ## 4.1 类型转换 2 | PHP是弱类型语言,不需要明确的定义变量的类型,变量的类型根据使用时的上下文所决定,也就是变量会根据不同表达式所需要的类型自动转换,比如求和,PHP会将两个相加的值转为long、double再进行加和。每种类型转为另外一种类型都有固定的规则,当某个操作发现类型不符时就会按照这个规则进行转换,这个规则正是弱类型实现的基础。 3 | 4 | 除了自动类型转换,PHP还提供了一种强制的转换方式: 5 | * (int)/(integer):转换为整形 integer 6 | * (bool)/(boolean):转换为布尔类型 boolean 7 | * (float)/(double)/(real):转换为浮点型 float 8 | * (string):转换为字符串 string 9 | * (array):转换为数组 array 10 | * (object):转换为对象 object 11 | * (unset):转换为 NULL 12 | 13 | 无论是自动类型转换还是强制类型转换,不是每种类型都可以转为任意其他类型。 14 | 15 | ### 4.1.1 转换为NULL 16 | 这种转换比较简单,任意类型都可以转为NULL,转换时直接将新的zval类型设置为`IS_NULL`即可。 17 | 18 | ### 4.1.2 转换为布尔型 19 | 当转换为 boolean 时,根据原值的TRUE、FALSE决定转换后的结果,以下值被认为是 FALSE: 20 | * 布尔值 FALSE 本身 21 | * 整型值 0 22 | * 浮点型值 0.0 23 | * 空字符串,以及字符串 "0" 24 | * 空数组 25 | * NULL 26 | 27 | 所有其它值都被认为是 TRUE,比如资源、对象(这里指默认情况下,因为可以通过扩展改变这个规则)。 28 | 29 | 判断一个值是否为true的操作: 30 | ```c 31 | static zend_always_inline int i_zend_is_true(zval *op) 32 | { 33 | int result = 0; 34 | 35 | again: 36 | switch (Z_TYPE_P(op)) { 37 | case IS_TRUE: 38 | result = 1; 39 | break; 40 | case IS_LONG: 41 | //非0即真 42 | if (Z_LVAL_P(op)) { 43 | result = 1; 44 | } 45 | break; 46 | case IS_DOUBLE: 47 | if (Z_DVAL_P(op)) { 48 | result = 1; 49 | } 50 | break; 51 | case IS_STRING: 52 | //非空字符串及"0"外都为true 53 | if (Z_STRLEN_P(op) > 1 || (Z_STRLEN_P(op) && Z_STRVAL_P(op)[0] != '0')) { 54 | result = 1; 55 | } 56 | break; 57 | case IS_ARRAY: 58 | //非空数组为true 59 | if (zend_hash_num_elements(Z_ARRVAL_P(op))) { 60 | result = 1; 61 | } 62 | break; 63 | case IS_OBJECT: 64 | //默认情况下始终返回true 65 | result = zend_object_is_true(op); 66 | break; 67 | case IS_RESOURCE: 68 | //合法资源就是true 69 | if (EXPECTED(Z_RES_HANDLE_P(op))) { 70 | result = 1; 71 | } 72 | case IS_REFERENCE: 73 | op = Z_REFVAL_P(op); 74 | goto again; 75 | break; 76 | default: 77 | break; 78 | } 79 | return result; 80 | } 81 | ``` 82 | 在扩展中可以通过`convert_to_boolean()`这个函数直接将原zval转为bool型,转换时的判断逻辑与`i_zend_is_true()`一致。 83 | 84 | ### 4.1.3 转换为整型 85 | 其它类型转为整形的转换规则: 86 | * NULL:转为0 87 | * 布尔型:false转为0,true转为1 88 | * 浮点型:向下取整,比如:`(int)2.8 => 2` 89 | * 字符串:就是C语言strtoll()的规则,如果字符串以合法的数值开始,则使用该数值,否则其值为 0(零),合法数值由可选的正负号,后面跟着一个或多个数字(可能有小数点),再跟着可选的指数部分 90 | * 数组:很多操作不支持将一个数组自动整形处理,比如:`array() + 2`,将报error错误,但可以强制把数组转为整形,非空数组转为1,空数组转为0,没有其他值 91 | * 对象:与数组类似,很多操作也不支持将对象自动转为整形,但有些操作只会抛一个warning警告,还是会把对象转为1操作的,这个需要看不同操作的处理情况 92 | * 资源:转为分配给这个资源的唯一编号 93 | 94 | 具体处理: 95 | ```c 96 | ZEND_API zend_long ZEND_FASTCALL _zval_get_long_func(zval *op) 97 | { 98 | try_again: 99 | switch (Z_TYPE_P(op)) { 100 | case IS_NULL: 101 | case IS_FALSE: 102 | return 0; 103 | case IS_TRUE: 104 | return 1; 105 | case IS_RESOURCE: 106 | //资源将转为zend_resource->handler 107 | return Z_RES_HANDLE_P(op); 108 | case IS_LONG: 109 | return Z_LVAL_P(op); 110 | case IS_DOUBLE: 111 | return zend_dval_to_lval(Z_DVAL_P(op)); 112 | case IS_STRING: 113 | //字符串的转换调用C语言的strtoll()处理 114 | return ZEND_STRTOL(Z_STRVAL_P(op), NULL, 10); 115 | case IS_ARRAY: 116 | //根据数组是否为空转为0,1 117 | return zend_hash_num_elements(Z_ARRVAL_P(op)) ? 1 : 0; 118 | case IS_OBJECT: 119 | { 120 | zval dst; 121 | convert_object_to_type(op, &dst, IS_LONG, convert_to_long); 122 | if (Z_TYPE(dst) == IS_LONG) { 123 | return Z_LVAL(dst); 124 | } else { 125 | //默认情况就是1 126 | return 1; 127 | } 128 | } 129 | case IS_REFERENCE: 130 | op = Z_REFVAL_P(op); 131 | goto try_again; 132 | EMPTY_SWITCH_DEFAULT_CASE() 133 | } 134 | return 0; 135 | } 136 | ``` 137 | ### 4.1.4 转换为浮点型 138 | 除字符串类型外,其它类型转换规则与整形基本一致,就是整形转换结果加了一位小数,字符串转为浮点数由`zend_strtod()`完成,这个函数非常长,定义在`zend_strtod.c`中,这里不作说明。 139 | 140 | ### 4.1.5 转换为字符串 141 | 一个值可以通过在其前面加上 (string) 或用 strval() 函数来转变成字符串。在一个需要字符串的表达式中,会自动转换为 string,比如在使用函数 echo 或 print 时,或在一个变量和一个 string 进行比较时,就会发生这种转换。 142 | 143 | ```c 144 | ZEND_API zend_string* ZEND_FASTCALL _zval_get_string_func(zval *op) 145 | { 146 | try_again: 147 | switch (Z_TYPE_P(op)) { 148 | case IS_UNDEF: 149 | case IS_NULL: 150 | case IS_FALSE: 151 | //转为空字符串"" 152 | return ZSTR_EMPTY_ALLOC(); 153 | case IS_TRUE: 154 | //转为"1" 155 | ... 156 | return zend_string_init("1", 1, 0); 157 | case IS_RESOURCE: { 158 | //转为"Resource id #xxx" 159 | ... 160 | len = snprintf(buf, sizeof(buf), "Resource id #" ZEND_LONG_FMT, (zend_long)Z_RES_HANDLE_P(op)); 161 | return zend_string_init(buf, len, 0); 162 | } 163 | case IS_LONG: { 164 | return zend_long_to_str(Z_LVAL_P(op)); 165 | } 166 | case IS_DOUBLE: { 167 | return zend_strpprintf(0, "%.*G", (int) EG(precision), Z_DVAL_P(op)); 168 | } 169 | case IS_ARRAY: 170 | //转为"Array",但是报Notice 171 | zend_error(E_NOTICE, "Array to string conversion"); 172 | return zend_string_init("Array", sizeof("Array")-1, 0); 173 | case IS_OBJECT: { 174 | //报Error错误 175 | zval tmp; 176 | ... 177 | zend_error(EG(exception) ? E_ERROR : E_RECOVERABLE_ERROR, "Object of class %s could not be converted to string", ZSTR_VAL(Z_OBJCE_P(op)->name)); 178 | return ZSTR_EMPTY_ALLOC(); 179 | } 180 | case IS_REFERENCE: 181 | op = Z_REFVAL_P(op); 182 | goto try_again; 183 | case IS_STRING: 184 | return zend_string_copy(Z_STR_P(op)); 185 | EMPTY_SWITCH_DEFAULT_CASE() 186 | } 187 | return NULL; 188 | } 189 | ``` 190 | 191 | ### 4.1.6 转换为数组 192 | 如果将一个null、integer、float、string、boolean 和 resource 类型的值转换为数组,将得到一个仅有一个元素的数组,其下标为 0,该元素即为此标量的值。换句话说,(array)$scalarValue 与 array($scalarValue) 完全一样。 193 | 194 | 如果一个 object 类型转换为 array,则结果为一个数组,数组元素为该对象的全部属性,包括public、private、protected,其中private的属性转换后的key加上了类名作为前缀,protected属性的key加上了"*"作为前缀,但是这个前缀并不是转为数组时单独加上的,而是类编译生成属性zend_property_info时就已经加上了,也就是说这其实是成员属性本身的一个特点,举例来看: 195 | ```c 196 | class test { 197 | private $a = 123; 198 | public $b = "bbb"; 199 | protected $c = "ccc"; 200 | } 201 | $obj = new test; 202 | print_r((array)$obj); 203 | ====================== 204 | Array 205 | ( 206 | [testa] => 123 207 | [b] => bbb 208 | [*c] => ccc 209 | ) 210 | ``` 211 | 转换时的处理: 212 | ```c 213 | ZEND_API void ZEND_FASTCALL convert_to_array(zval *op) 214 | { 215 | try_again: 216 | switch (Z_TYPE_P(op)) { 217 | case IS_ARRAY: 218 | break; 219 | case IS_OBJECT: 220 | ... 221 | if (Z_OBJ_HT_P(op)->get_properties) { 222 | //获取所有属性数组 223 | HashTable *obj_ht = Z_OBJ_HT_P(op)->get_properties(op); 224 | //将数组内容拷贝到新数组 225 | ... 226 | } 227 | case IS_NULL: 228 | ZVAL_NEW_ARR(op); 229 | //转为空数组 230 | zend_hash_init(Z_ARRVAL_P(op), 8, NULL, ZVAL_PTR_DTOR, 0); 231 | break; 232 | case IS_REFERENCE: 233 | zend_unwrap_reference(op); 234 | goto try_again; 235 | default: 236 | convert_scalar_to_array(op); 237 | break; 238 | } 239 | } 240 | 241 | //其他标量类型转array 242 | static void convert_scalar_to_array(zval *op) 243 | { 244 | zval entry; 245 | 246 | ZVAL_COPY_VALUE(&entry, op); 247 | //新分配一个数组,将原值插入数组 248 | ZVAL_NEW_ARR(op); 249 | zend_hash_init(Z_ARRVAL_P(op), 8, NULL, ZVAL_PTR_DTOR, 0); 250 | zend_hash_index_add_new(Z_ARRVAL_P(op), 0, &entry); 251 | } 252 | ``` 253 | ### 4.1.7 转换为对象 254 | 如果其它任何类型的值被转换成对象,将会创建一个内置类 stdClass 的实例:如果该值为 NULL,则新的实例为空;array转换成object将以键名成为属性名并具有相对应的值,数值索引的元素也将转为属性,但是无法通过"->"访问,只能遍历获取;对于其他值,会以scalar作为属性名。 255 | 256 | ```c 257 | ZEND_API void ZEND_FASTCALL convert_to_object(zval *op) 258 | { 259 | try_again: 260 | switch (Z_TYPE_P(op)) { 261 | case IS_ARRAY: 262 | { 263 | HashTable *ht = Z_ARR_P(op); 264 | ... 265 | //以key为属性名,将数组元素拷贝到对象属性 266 | object_and_properties_init(op, zend_standard_class_def, ht); 267 | break; 268 | } 269 | case IS_OBJECT: 270 | break; 271 | case IS_NULL: 272 | object_init(op); 273 | break; 274 | case IS_REFERENCE: 275 | zend_unwrap_reference(op); 276 | goto try_again; 277 | default: { 278 | zval tmp; 279 | ZVAL_COPY_VALUE(&tmp, op); 280 | object_init(op); 281 | //以scalar作为属性名 282 | zend_hash_str_add_new(Z_OBJPROP_P(op), "scalar", sizeof("scalar")-1, &tmp); 283 | break; 284 | } 285 | } 286 | } 287 | ``` 288 | ### 4.1.8 转换为资源 289 | 无法将其他类型转为资源。 290 | -------------------------------------------------------------------------------- /5/gc.md: -------------------------------------------------------------------------------- 1 | ## 5.2 垃圾回收 2 | 3 | ### 5.2.1 垃圾的产生 4 | 前面已经介绍过PHP变量的内存管理,即引用计数机制,当变量赋值、传递时并不会直接硬拷贝,而是增加value的引用数,unset、return等释放变量时再减掉引用数,减掉后如果发现refcount变为0则直接释放value,这是变量的基本gc过程,PHP正是通过这个机制实现的自动垃圾回收,但是有一种情况是这个机制无法解决的,从而因变量无法回收导致内存始终得不到释放,这种情况就是循环引用,简单的描述就是变量的内部成员引用了变量自身,比如数组中的某个元素指向了数组,这样数组的引用计数中就有一个来自自身成员,试图释放数组时因为其refcount仍然大于0而得不到释放,而实际上已经没有任何外部引用了,这种变量不可能再被使用,所以PHP引入了另外一个机制用来处理变量循环引用的问题。 5 | 6 | 下面看一个数组循环引用的例子: 7 | ```php 8 | $a = [1]; 9 | $a[] = &$a; 10 | 11 | unset($a); 12 | ``` 13 | `unset($a)`之前引用关系: 14 | 15 | ![gc_1](../img/gc_1.png) 16 | 17 | 注意这里$a的类型在`&`操作后已经转为引用,`unset($a)`之后: 18 | 19 | ![gc_2](../img/gc_2.png) 20 | 21 | 22 | 可以看到,`unset($a)`之后由于数组中有子元素指向`$a`,所以`refcount = 1`,此时是无法通过正常的gc机制回收的,但是$a已经已经没有任何外部引用了,所以这种变量就是垃圾,垃圾回收器要处理的就是这种情况,这里明确两个准则: 23 | 24 | >> 1) 如果一个变量value的refcount减少到0, 那么此value可以被释放掉,不属于垃圾 25 | 26 | >> 2) 如果一个变量value的refcount减少之后大于0,那么此zval还不能被释放,此zval可能成为一个垃圾 27 | 28 | 针对第一个情况GC不会处理,只有第二种情况GC才会将变量收集起来。另外变量是否加入垃圾检查buffer并不是根据zval的类型判断的,而是与前面介绍的是否用到引用计数一样通过`zval.u1.type_flag`记录的,只有包含`IS_TYPE_COLLECTABLE`的变量才会被GC收集。 29 | 30 | 目前垃圾只会出现在array、object两种类型中,数组的情况上面已经介绍了,object的情况则是成员属性引用对象本身导致的,其它类型不会出现这种变量中的成员引用变量自身的情况,所以垃圾回收只会处理这两种类型的变量。 31 | ```c 32 | #define IS_TYPE_COLLECTABLE 33 | ``` 34 | ```c 35 | | type | collectable | 36 | +----------------+-------------+ 37 | |simple types | | 38 | |string | | 39 | |interned string | | 40 | |array | Y | 41 | |immutable array | | 42 | |object | Y | 43 | |resource | | 44 | |reference | | 45 | ``` 46 | ### 5.2.2 回收过程 47 | 如果当变量的refcount减少后大于0,PHP并不会立即进行对这个变量进行垃圾鉴定,而是放入一个缓冲buffer中,等这个buffer满了以后(10000个值)再统一进行处理,加入buffer的是变量zend_value的`zend_refcounted_h`: 48 | ```c 49 | typedef struct _zend_refcounted_h { 50 | uint32_t refcount; //记录zend_value的引用数 51 | union { 52 | struct { 53 | zend_uchar type, //zend_value的类型,与zval.u1.type一致 54 | zend_uchar flags, 55 | uint16_t gc_info //GC信息,垃圾回收的过程会用到 56 | } v; 57 | uint32_t type_info; 58 | } u; 59 | } zend_refcounted_h; 60 | ``` 61 | 一个变量只能加入一次buffer,为了防止重复加入,变量加入后会把`zend_refcounted_h.gc_info`置为`GC_PURPLE`,即标为紫色,下次refcount减少时如果发现已经加入过了则不再重复插入。垃圾缓存区是一个双向链表,等到缓存区满了以后则启动垃圾检查过程:遍历缓存区,再对当前变量的所有成员进行遍历,然后把成员的refcount减1(如果成员还包含子成员则也进行递归遍历,其实就是深度优先的遍历),最后再检查当前变量的引用,如果减为了0则为垃圾。这个算法的原理很简单,垃圾是由于成员引用自身导致的,那么就对所有的成员减一遍引用,结果如果发现变量本身refcount变为了0则就表明其引用全部来自自身成员。具体的过程如下: 62 | 63 | (1) 从buffer链表的roots开始遍历,把当前value标为灰色(zend_refcounted_h.gc_info置为GC_GREY),然后对当前value的成员进行深度优先遍历,把成员value的refcount减1,并且也标为灰色; 64 | 65 | (2) 重复遍历buffer链表,检查当前value引用是否为0,为0则表示确实是垃圾,把它标为白色(GC_WHITE),如果不为0则排除了引用全部来自自身成员的可能,表示还有外部的引用,并不是垃圾,这时候因为步骤(1)对成员进行了refcount减1操作,需要再还原回去,对所有成员进行深度遍历,把成员refcount加1,同时标为黑色; 66 | 67 | (3) 再次遍历buffer链表,将非GC_WHITE的节点从roots链表中删除,最终roots链表中全部为真正的垃圾,最后将这些垃圾清除。 68 | 69 | 70 | ### 5.2.3 垃圾收集的内部实现 71 | 接下来我们简单看下垃圾回收的内部实现,垃圾收集器的全局数据结构: 72 | ```c 73 | typedef struct _zend_gc_globals { 74 | zend_bool gc_enabled; //是否启用gc 75 | zend_bool gc_active; //是否在垃圾检查过程中 76 | zend_bool gc_full; //缓存区是否已满 77 | 78 | gc_root_buffer *buf; //启动时分配的用于保存可能垃圾的缓存区 79 | gc_root_buffer roots; //指向buf中最新加入的一个可能垃圾 80 | gc_root_buffer *unused;//指向buf中没有使用的buffer 81 | gc_root_buffer *first_unused; //指向buf中第一个没有使用的buffer 82 | gc_root_buffer *last_unused; //指向buf尾部 83 | 84 | gc_root_buffer to_free; //待释放的垃圾 85 | gc_root_buffer *next_to_free; 86 | 87 | uint32_t gc_runs; //统计gc运行次数 88 | uint32_t collected; //统计已回收的垃圾数 89 | } zend_gc_globals; 90 | 91 | typedef struct _gc_root_buffer { 92 | zend_refcounted *ref; //每个zend_value的gc信息 93 | struct _gc_root_buffer *next; 94 | struct _gc_root_buffer *prev; 95 | uint32_t refcount; 96 | } gc_root_buffer; 97 | ``` 98 | `zend_gc_globals`是垃圾回收过程中主要用到的一个结构,用来保存垃圾回收器的所有信息,比如垃圾缓存区;`gc_root_buffer`用来保存每个可能是垃圾的变量,它实际就是整个垃圾收集buffer链表的元素,当GC收集一个变量时会创建一个`gc_root_buffer`,插入链表。 99 | 100 | `zend_gc_globals`这个结构中有几个关键成员: 101 | 102 | * __(1)buf:__ 前面已经说过,当refcount减少后如果大于0那么就会将这个变量的value加入GC的垃圾缓存区,buf就是这个缓存区,它实际是一块连续的内存,在GC初始化时一次性分配了10001个gc_root_buffer,插入变量时直接从buf中取出可用节点; 103 | * __(2)roots:__ 垃圾缓存链表的头部,启动GC检查的过程就是从roots开始遍历的; 104 | * __(3)first_unused:__ 指向buf中第一个可用的节点,初始化时这个值为1而不是0,因为第一个gc_root_buffer保留没有使用,有元素插入roots时如果first_unused还没有到达buf的尾部则返回first_unused给最新的元素,然后first_unused++,直到last_unused,比如现在已经加入了2个可能的垃圾变量,则对应的结构: 105 | 106 | ![](../img/zend_gc_1.png) 107 | 108 | * __(4)last_unused:__ 与first_unused类似,指向buf末尾 109 | * __(5)unused:__ GC收集变量时会依次从buf中获取可用的gc_root_buffer,这种情况直接取first_unused即可,但是有些变量加入垃圾缓存区之后其refcount又减为0了,这种情况就需要从roots中删掉,因为它不可能是垃圾,这样就导致roots链表并不是像buf分配的那样是连续的,中间会出现一些开始加入后面又删除的节点,这些节点就通过unused串成一个单链表,unused指向链表尾部,下次有新的变量插入roots时优先使用unused的这些节点,其次才是first_unused的,举个例子: 110 | ```php 111 | //示例1: 112 | $a = array(); //$a -> zend_array(refcount=1) 113 | $b = $a; //$a -> zend_array(refcount=2) 114 | //$b -> 115 | 116 | unset($b); //此时zend_array(refcount=1),因为refoucnt>0所以加入gc的垃圾缓存区:roots 117 | unset($a); //此时zend_array(refcount=0)且gc_info为GC_PURPLE,则从roots链表中删掉 118 | ``` 119 | 假如`unset($b)`时插入的是buf中第1个位置,那么`unset($a)`后对应的结构: 120 | 121 | ![](../img/zend_gc_2.png) 122 | 123 | 如果后面再有变量加入GC垃圾缓存区将优先使用第1个。 124 | 125 | 此GC机制可以通过php.ini中`zend.enable_gc`设置是否开启,如果开启则在php.ini解析后调用`gc_init()`进行GC初始化: 126 | ```c 127 | ZEND_API void gc_init(void) 128 | { 129 | if (GC_G(buf) == NULL && GC_G(gc_enabled)) { 130 | //分配buf缓存区内存,大小为GC_ROOT_BUFFER_MAX_ENTRIES(10001),其中第1个保留不被使用 131 | GC_G(buf) = (gc_root_buffer*) malloc(sizeof(gc_root_buffer) * GC_ROOT_BUFFER_MAX_ENTRIES); 132 | GC_G(last_unused) = &GC_G(buf)[GC_ROOT_BUFFER_MAX_ENTRIES]; 133 | //进行GC_G的初始化,其中:GC_G(first_unused) = GC_G(buf) + 1;从第2个开始的,第1个保留 134 | gc_reset(); 135 | } 136 | } 137 | ``` 138 | 在PHP的执行过程中,如果发现array、object减掉refcount后大于0则会调用`gc_possible_root()`将zend_value的gc头部加入GC垃圾缓存区: 139 | ```c 140 | ZEND_API void ZEND_FASTCALL gc_possible_root(zend_refcounted *ref) 141 | { 142 | gc_root_buffer *newRoot; 143 | 144 | //插入的节点必须是GC_BLACK,防止重复插入 145 | ZEND_ASSERT(EXPECTED(GC_REF_GET_COLOR(ref) == GC_BLACK)); 146 | 147 | newRoot = GC_G(unused); //先看下unused中有没有可用的 148 | if (newRoot) { 149 | //有的话先用unused的,然后将GC_G(unused)指向单链表的下一个 150 | GC_G(unused) = newRoot->prev; 151 | } else if (GC_G(first_unused) != GC_G(last_unused)) { 152 | //unused没有可用的,且buf中还有可用的 153 | newRoot = GC_G(first_unused); 154 | GC_G(first_unused)++; 155 | } else { 156 | //buf缓存区已满,这时需要启动垃圾检查程序了,遍历roots,将真正的垃圾释放 157 | //垃圾回收的动作就是在这触发的 158 | if (!GC_G(gc_enabled)) { 159 | return; 160 | } 161 | ... 162 | 163 | //启动垃圾回收过程 164 | gc_collect_cycles(); //即:zend_gc_collect_cycles() 165 | ... 166 | } 167 | 168 | //将插入的ref标为紫色,防止重复插入 169 | GC_TRACE_SET_COLOR(ref, GC_PURPLE); 170 | //注意:gc_info不仅仅只有颜色的信息,还会记录当前gc_root_buffer在整个buf中的位置 171 | //这样做的目的是可以直接根据zend_value的gc信息取到它的gc_root_buffer,便于进行删除操作 172 | GC_INFO(ref) = (newRoot - GC_G(buf)) | GC_PURPLE; 173 | newRoot->ref = ref; 174 | 175 | //GC_G(roots).next指向新插入的元素 176 | newRoot->next = GC_G(roots).next; 177 | newRoot->prev = &GC_G(roots); 178 | GC_G(roots).next->prev = newRoot; 179 | GC_G(roots).next = newRoot; 180 | } 181 | ``` 182 | 同一个zend_value只会插入一次,再次插入时如果发现其gc_info不是GC_BLACK则直接跳过。另外像上面示例1的情况,插入后如果后面发现其refcount减为0了则表明它可以直接被回收掉,这时需要把这个节点从roots链表中删除,删除的操作通过`GC_REMOVE_FROM_BUFFER()`宏操作: 183 | ```c 184 | #define GC_REMOVE_FROM_BUFFER(p) do { \ 185 | zend_refcounted *_p = (zend_refcounted*)(p); \ 186 | if (GC_ADDRESS(GC_INFO(_p))) { \ 187 | gc_remove_from_buffer(_p); \ 188 | } \ 189 | } while (0) 190 | 191 | ZEND_API void ZEND_FASTCALL gc_remove_from_buffer(zend_refcounted *ref) 192 | { 193 | gc_root_buffer *root; 194 | 195 | //GC_ADDRESS就是获取节点在缓存区中的位置,因为删除时输入是zend_refcounted 196 | //而缓存链表的节点类型是gc_root_buffer 197 | root = GC_G(buf) + GC_ADDRESS(GC_INFO(ref)); 198 | if (GC_REF_GET_COLOR(ref) != GC_BLACK) { 199 | GC_TRACE_SET_COLOR(ref, GC_PURPLE); 200 | } 201 | GC_INFO(ref) = 0; 202 | GC_REMOVE_FROM_ROOTS(root); //双向链表的删除操作 203 | ... 204 | } 205 | ``` 206 | 插入时如果发现垃圾缓存链表已经满了,则会启动垃圾回收过程:`zend_gc_collect_cycles()`,这个过程会对之前插入缓存区的变量进行判断是否是循环引用导致的真正的垃圾,如果是垃圾则会进行回收,回收的过程前面已经介绍过: 207 | ```c 208 | ZEND_API int zend_gc_collect_cycles(void) 209 | { 210 | ... 211 | //(1)遍历roots链表,对当前节点value的所有成员(如数组元素、成员属性)进行深度优先遍历把成员refcount减1 212 | gc_mark_roots(); 213 | 214 | //(2)再次遍历roots链表,检查各节点当前refcount是否为0,是的话标为白色,表示是垃圾,不是的话需要对还原(1),把refcount再加回去 215 | gc_scan_roots(); 216 | 217 | //(3)将roots链表中的非白色节点删除,之后roots链表中全部是真正的垃圾,将垃圾链表转到to_free等待释放 218 | count = gc_collect_roots(&gc_flags, &additional_buffer); 219 | ... 220 | 221 | //(4)释放垃圾 222 | current = to_free.next; 223 | while (current != &to_free) { 224 | p = current->ref; 225 | GC_G(next_to_free) = current->next; 226 | if ((GC_TYPE(p) & GC_TYPE_MASK) == IS_OBJECT) { 227 | //调用free_obj释放对象 228 | obj->handlers->free_obj(obj); 229 | ... 230 | } else if ((GC_TYPE(p) & GC_TYPE_MASK) == IS_ARRAY) { 231 | //释放数组 232 | zend_array *arr = (zend_array*)p; 233 | 234 | GC_TYPE(arr) = IS_NULL; 235 | zend_hash_destroy(arr); 236 | } 237 | current = GC_G(next_to_free); 238 | } 239 | ... 240 | } 241 | ``` 242 | 各步骤具体的操作不再详细展开,这里单独说明下value成员的遍历,array比较好理解,所有成员都在arData数组中,直接遍历arData即可,如果各元素仍是array、object或者引用则一直递归进行深度优先遍历;object的成员指的成员属性(不包括静态属性、常量,它们属于类而不属于对象),前面介绍对象的实现时曾说过,成员属性除了明确的在类中定义的那些外还可以动态创建,动态属性保存于zend_obejct->properties哈希表中,普通属性保存于zend_object.properties_table数组中,这样以来object的成员就分散在两个位置,那么遍历时是分别遍历吗?答案是否定的。 243 | 244 | 实际前面已经简单提过,在创建动态属性时会把全部普通属性也加到zend_obejct->properties哈希表中,指向原zend_object.properties_table中的属性,这样一来GC遍历object的成员时就可以像array那样遍历zend_obejct->properties即可,GC获取object成员的操作由get_gc(即:zend_std_get_gc())完成: 245 | ```c 246 | ZEND_API HashTable *zend_std_get_gc(zval *object, zval **table, int *n) 247 | { 248 | if (Z_OBJ_HANDLER_P(object, get_properties) != zend_std_get_properties) { 249 | *table = NULL; 250 | *n = 0; 251 | return Z_OBJ_HANDLER_P(object, get_properties)(object); 252 | } else { 253 | zend_object *zobj = Z_OBJ_P(object); 254 | 255 | if (zobj->properties) { 256 | //有动态属性 257 | *table = NULL; 258 | *n = 0; 259 | return zobj->properties; 260 | } else { 261 | //没有定义过动态属性,返回数组 262 | *table = zobj->properties_table; 263 | *n = zobj->ce->default_properties_count; 264 | return NULL; 265 | } 266 | } 267 | } 268 | ``` 269 | -------------------------------------------------------------------------------- /6/ts.md: -------------------------------------------------------------------------------- 1 | ## 6.1 介绍 2 | 在C语言中声明在任何函数之外的变量为全局变量,全局变量为各线程共享,不同的线程引用同一地址空间,如果一个线程修改了全局变量就会影响所有的线程。所以线程安全是指多线程环境下如何安全的获取公共资源。 3 | 4 | PHP的SAPI多数是单线程环境,比如cli、fpm、cgi,每个进程只启动一个主线程,这种模式下是不存在线程安全问题的,但是也有多线程的环境,比如Apache,或用户自己嵌入PHP实现的环境,这种情况下就需要考虑线程安全的问题了,因为PHP中有很多全局变量,比如最常见的:EG、CG,如果多个线程共享同一个变量将会冲突,所以PHP为多线程的应用模型提供了一个安全机制:Zend线程安全(Zend Thread Safe, ZTS)。 5 | 6 | ## 6.2 线程安全资源管理器 7 | PHP中专门为解决线程安全的问题抽象出了一个线程安全资源管理器(Thread Safe Resource Mananger, TSRM),实现原理比较简单:既然共用资源这么困难那么就干脆不共用,各线程不再共享同一份全局变量,而是各复制一份,使用数据时各线程各取自己的副本,互不干扰。 8 | 9 | ### 6.2.1 基本实现 10 | TSRM核心思想就是为不同的线程分配独立的内存空间,如果一个资源会被多线程使用,那么首先需要预先向TSRM注册资源,然后TSRM为这个资源分配一个唯一的编号,并把这种资源的大小、初始化函数等保存到一个`tsrm_resource_type`结构中,各线程只能通过TSRM分配的那个编号访问这个资源;然后当线程拿着这个编号获取资源时TSRM如果发现是第一次请求,则会根据注册时的资源大小分配一块内存,然后调用初始化函数进行初始化,并把这块资源保存下来供这个线程后续使用。 11 | 12 | TSRM中通过两个结构分别保存资源信息以及具体的资源:tsrm_resource_type、tsrm_tls_entry,前者是用来记录资源大小、初始化函数等信息的,具体分配资源内存时会用到,而后者用来保存各线程所拥有的全部资源: 13 | ```c 14 | struct _tsrm_tls_entry { 15 | void **storage; //资源数组 16 | int count; //拥有的资源数:storage数组大小 17 | THREAD_T thread_id; //所属线程id 18 | tsrm_tls_entry *next; 19 | }; 20 | 21 | typedef struct { 22 | size_t size; //资源的大小 23 | ts_allocate_ctor ctor; //初始化函数 24 | ts_allocate_dtor dtor; 25 | int done; 26 | } tsrm_resource_type; 27 | ``` 28 | 每个线程拥有一个`tsrm_tls_entry`结构,当前线程的所有资源保存在storage数组中,下标就是各资源的id。 29 | 30 | 另外所有线程的`tsrm_tls_entry`结构通过一个数组保存:tsrm_tls_table,这是个全局变量,所以操作这个变量时需要加锁。这个值在TSRM初始化时按照预设置的线程数分配,每个线程的tsrm_tls_entry结构在这个数组中的位置是根据线程id与预设置的线程数(tsrm_tls_table_size)取模得到的,也就是说有可能多个线程保存在tsrm_tls_table同一位置,所以tsrm_tls_entry是个链表,查找资源时首先根据:`线程id % tsrm_tls_table_size`得到一个tsrm_tls_entry,然后开始遍历链表比较thread_id确定是否是当前线程的。 31 | 32 | #### 6.2.1.1 初始化 33 | 在使用TSRM之前需要主动开启,一般这个步骤在sapi启动时执行,主要工作就是分配tsrm_tls_table、resource_types_table内存以及创建线程互斥锁,下面具体看下TSRM初始化的过程(以pthread为例): 34 | ```c 35 | TSRM_API int tsrm_startup(int expected_threads, int expected_resources, int debug_level, char *debug_filename) 36 | { 37 | pthread_key_create( &tls_key, 0 ); 38 | 39 | //分配tsrm_tls_table 40 | tsrm_tls_table_size = expected_threads; 41 | tsrm_tls_table = (tsrm_tls_entry **) calloc(tsrm_tls_table_size, sizeof(tsrm_tls_entry *)); 42 | ... 43 | //初始化资源的递增id,注册资源时就是用的这个值 44 | id_count=0; 45 | 46 | //分配资源类型数组:resource_types_table 47 | resource_types_table_size = expected_resources; 48 | resource_types_table = (tsrm_resource_type *) calloc(resource_types_table_size, sizeof(tsrm_resource_type)); 49 | ... 50 | //创建锁 51 | tsmm_mutex = tsrm_mutex_alloc(); 52 | } 53 | ``` 54 | #### 6.2.1.2 资源注册 55 | 初始化完成各模块就可以各自进行资源注册了,注册后TSRM会给注册的资源分配唯一id,之后对此资源的操作只能依据此id,接下来我们以EG为例具体看下其注册过程。 56 | ```c 57 | #ifdef ZTS 58 | ZEND_API int executor_globals_id; 59 | #endif 60 | 61 | int zend_startup(zend_utility_functions *utility_functions, char **extensions) 62 | { 63 | ... 64 | #ifdef ZTS 65 | ts_allocate_id(&executor_globals_id, sizeof(zend_executor_globals), (ts_allocate_ctor) executor_globals_ctor, (ts_allocate_dtor) executor_globals_dtor); 66 | 67 | executor_globals = ts_resource(executor_globals_id); 68 | ... 69 | #endif 70 | } 71 | ``` 72 | 资源注册调用`ts_allocate_id()`完成,此函数有4个参数有,第一个就是定义的资源id指针,注册之后会把分配的id写到这里,第二个是资源类型的大小,EG资源的结构是`zend_executor_globals`,所以这个值就是sizeof(zend_executor_globals),后面两个分别是资源的初始化函数以及销毁函数,因为TSRM并不关心资源的具体类型,分配资源时它只按照size大小分配内存,然后回调各资源自己定义的ctor进行初始化。 73 | ```c 74 | TSRM_API ts_rsrc_id ts_allocate_id(ts_rsrc_id *rsrc_id, size_t size, ts_allocate_ctor ctor, ts_allocate_dtor dtor) 75 | { 76 | //加锁,保证各线程串行调用此函数 77 | tsrm_mutex_lock(tsmm_mutex); 78 | 79 | //分配id,即id_count当前值,然后把id_count加1 80 | *rsrc_id = TSRM_SHUFFLE_RSRC_ID(id_count++); 81 | 82 | //检查resource_types_table数组当前大小是否已满 83 | if (resource_types_table_size < id_count) { 84 | //需要对resource_types_table扩容 85 | resource_types_table = (tsrm_resource_type *) realloc(resource_types_table, sizeof(tsrm_resource_type)*id_count); 86 | ... 87 | //把数组大小修改新的大小 88 | resource_types_table_size = id_count; 89 | } 90 | 91 | //将新注册的资源插入resource_types_table数组,下标就是分配的资源id 92 | resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].size = size; 93 | resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].ctor = ctor; 94 | resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].dtor = dtor; 95 | resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].done = 0; 96 | ... 97 | } 98 | ``` 99 | 到这里并没有结束,所有的资源并不是统一时机注册的,所以注册一个新资源时可能有线程已经分配先前注册的资源了,因此需要对各线程的storage数组进行扩容,否则storage将没有空间容纳新的资源。扩容的过程比较简单:遍历各线程的tsrm_tls_entry,检查storage当时是否有空闲空间,有的话跳过,没有的话则扩展。 100 | ```c 101 | for (i=0; icount < id_count) { 107 | int j; 108 | 109 | //将storage扩容 110 | p->storage = (void *) realloc(p->storage, sizeof(void *)*id_count); 111 | //分配并初始化新注册的资源,实际这里只会执行一次,不清楚为什么用循环 112 | //另外这里不分配内存也可以,可以放到使用时再去分配 113 | for (j=p->count; jstorage[j] = (void *) malloc(resource_types_table[j].size); 115 | if (resource_types_table[j].ctor) { 116 | //回调初始化函数进行初始化 117 | resource_types_table[j].ctor(p->storage[j]); 118 | } 119 | } 120 | p->count = id_count; 121 | } 122 | p = p->next; 123 | } 124 | } 125 | ``` 126 | 最后将锁释放,完成注册。 127 | 128 | #### 6.2.1.3 获取资源 129 | 资源的id在注册后需要保存下来,根据id可以通过`ts_resource()`获取到对应资源的值,比如EG,这里暂不考虑EG宏展开的结果,只分析最底层的根据资源id获取资源的操作。 130 | ```c 131 | zend_executor_globals *executor_globals; 132 | 133 | executor_globals = ts_resource(executor_globals_id); 134 | ``` 135 | 这样获取的`executor_globals`值就是各线程分离的了,对它的操作将不会再影响其它线程。根据资源id获取当前线程资源的过程:首先是根据线程id哈希得到当前线程的tsrm_tls_entry在tsrm_tls_table哪个槽中,然后开始遍历比较id,直到找到当前线程的tsrm_tls_entry,这个查找过程是需要加锁的,最后根据资源id从storage中对应位置取出资源的地址,这个时候如果发现当前线程还没有创建此资源则会从resource_types_table根据资源id取出资源注册时的大小、初始化函数,然后分配内存、调用初始化函数进行初始化并插入所属线程的storage中。 136 | ```c 137 | TSRM_API void *ts_resource_ex(ts_rsrc_id id, THREAD_T *th_id) 138 | { 139 | THREAD_T thread_id; 140 | int hash_value; 141 | tsrm_tls_entry *thread_resources; 142 | 143 | //step 1:获取线程id 144 | if (!th_id) { 145 | //获取当前线程通过specific data保存的tsrm_tls_entry,暂时忽略 146 | thread_resources = tsrm_tls_get(); 147 | if(thread_resources){ 148 | //找到线程的tsrm_tls_entry了 149 | TSRM_SAFE_RETURN_RSRC(thread_resources->storage, id, thread_resources->count); //直接返回 150 | } 151 | //pthread_self(),当前线程id 152 | thread_id = tsrm_thread_id(); 153 | }else{ 154 | thread_id = *th_id; 155 | } 156 | 157 | //step 2:查找线程tsrm_tls_entry 158 | tsrm_mutex_lock(tsmm_mutex); //加锁 159 | 160 | //实际就是thread_id % tsrm_tls_table_size 161 | hash_value = THREAD_HASH_OF(thread_id, tsrm_tls_table_size); 162 | //链表头部 163 | thread_resources = tsrm_tls_table[hash_value]; 164 | if (!thread_resources) { 165 | //当前线程第一次使用资源还未分配:先分配tsrm_tls_entry 166 | allocate_new_resource(&tsrm_tls_table[hash_value], thread_id); 167 | //分配完再次调用,这时候将走到下面的分支 168 | return ts_resource_ex(id, &thread_id); 169 | }else{ 170 | //遍历查找当前线程的tsrm_tls_entry 171 | do { 172 | //找到了 173 | if (thread_resources->thread_id == thread_id) { 174 | break; 175 | } 176 | if (thread_resources->next) { 177 | thread_resources = thread_resources->next; 178 | } else { 179 | //遍历到最后也没找到,与上面的一致,先分配再查找 180 | allocate_new_resource(&thread_resources->next, thread_id); 181 | return ts_resource_ex(id, &thread_id); 182 | } 183 | } while (thread_resources); 184 | } 185 | //解锁 186 | tsrm_mutex_unlock(tsmm_mutex); 187 | 188 | //step 3:返回资源 189 | TSRM_SAFE_RETURN_RSRC(thread_resources->storage, id, thread_resources->count); 190 | } 191 | ``` 192 | 首先是获取线程id,如果没有传的话就是当前线程,然后在tsrm_tls_table中查找当前线程的tsrm_tls_entry,不存在则表示当前线程第一次使用资源,则需要调用`allocate_new_resource()`为当前线程分配tsrm_tls_entry,并插入tsrm_tls_table,这个过程还会为当前已注册的所有资源分配内存: 193 | ```c 194 | static void allocate_new_resource(tsrm_tls_entry **thread_resources_ptr, THREAD_T thread_id) 195 | { 196 | (*thread_resources_ptr) = (tsrm_tls_entry *) malloc(sizeof(tsrm_tls_entry)); 197 | (*thread_resources_ptr)->storage = NULL; 198 | //根据已注册资源数分配storage数组大小,注意这里并不是分配为各资源分配空间 199 | if (id_count > 0) { 200 | (*thread_resources_ptr)->storage = (void **) malloc(sizeof(void *)*id_count); 201 | } 202 | (*thread_resources_ptr)->count = id_count; 203 | (*thread_resources_ptr)->thread_id = thread_id; 204 | 205 | //将当前线程的tsrm_tls_entry保存到线程本地存储(Thread Local Storage, TLS) 206 | tsrm_tls_set(*thread_resources_ptr); 207 | 208 | //为全部资源分配空间 209 | for (i=0; istorage[i] = (void *) malloc(resource_types_table[i].size); 212 | ... 213 | } 214 | ... 215 | } 216 | ``` 217 | 这里还用到了一个多线程中经常用到的一个东西:线程本地存储(Thread Local Storage, TLS),在创建完当前线程的tsrm_tls_entry后会把这个值保存到当前线程的TLS中(即:tsrm_tls_set(*thread_resources_ptr)操作),这样在`ts_resource()`中就可以通过`tsrm_tls_get()`直接取到了,节省加锁检索的时间。 218 | 219 | > __线程本地存储(Thread Local Storage, TLS):__ 我们知道在一个进程中,所有线程是共享同一个地址空间的。所以,如果一个变量是全局的或者是静态的,那么所有线程访问的是同一份,如果某一个线程对其进行了修改,也就会影响到其他所有的线程。不过我们可能并不希望这样,所以更多的推荐用基于堆栈的自动变量或函数参数来访问数据,因为基于堆栈的变量总是和特定的线程相联系的。TLS在各平台下实现方式不同,主要分为两类:静态TLS、动态TLS,pthread中pthread_setspecific()、pthread_getspecific()的实现就可以认为是动态TLS的实现。 220 | 221 | 比如tsrm_tls_table_size初始化时设置为了2,当前有2个thread:thread 1、thread 2,假如注册了CG、EG两个资源,则存储结构如下图: 222 | 223 | ![](../img/tsrm_tls_a.png) 224 | 225 | ### 6.2.2 Native-TLS 226 | 上一节我们介绍了资源的注册以及根据资源id获取资源的方法,那么PHP内核每次使用对应的资源时难道都需要调用`ts_resource()`吗?如果是这样的话那么多次在使用EG时实际都会调一次这个方法,相当于我们需要调用一个函数来获取一个变量,这在性能上是不可接受的,那么有什么办法解决呢? 227 | 228 | `ts_resource()`最核心的操作就是根据线程id获取各线程对应的storage数组,这也是最耗时的部分,至于接下来根据资源id从storage数组读取资源就是普通的内存读取了,这并不影响性能,所以解决上面那个问题的关键就在于 __尽可能的减少线程storage的检索__ 。这一节我们来分析下PHP是如果解决这个问题的,在介绍PHP7实现方式之前我们先看下PHP5.x的处理方式。 229 | 230 | PHP5的解决方式非常简单,我们还是以EG为例,EG在内核中随处可见,不是要减少对各线程storage的检索次数吗,那么我就只要检索过一次就把已获取的storage指针传给接下来调用的函数用,其它函数再一级级往下传,这样一来各函数如果发现storage通过参数传进来了就直接用,无需再检索了,也就是通过层层传递的方式减少解决这个问题的。这样以来岂不是每个函数都得带这么一个参数?调用别的函数也得把这个值带上?是的。即使这个函数自己不用它也得需要这个值,因为有可能调用别的函数的时候其它函数会用。 231 | 232 | 如果你对PHP5有所了解的话一定经常看到这两个宏:TSRMLS_DC、TSRMLS_CC,这两个宏就是用来传递storage指针的,TSRMLS_DC用在定义函数的参数中,实际上它就是一个普通的参数定义,TSRMLS_CC用在调用函数时,它就是一个普通的变量值,我们看下它的展开结果: 233 | ```c 234 | #define TSRMLS_DC , void ***tsrm_ls 235 | #define TSRMLS_CC , tsrm_ls 236 | ``` 237 | 它的用法是第一个检索到storage的函数把它的指针传递给了下面的函数,参数是tsrm_ls,后面的函数直接根据接收的参数使用获取再传给其它函数,当然也可以不传,那样的话就得重新调用ts_resource()获取了。现在我们再看下EG宏展开的结果: 238 | ```c 239 | # define EG(v) TSRMG(executor_globals_id, zend_executor_globals *, v) 240 | 241 | #define TSRMG(id, type, element) (((type) (*((void ***) tsrm_ls))[TSRM_UNSHUFFLE_RSRC_ID(id)])->element) 242 | ``` 243 | 比如:`EG(function_table) => (((zend_executor_globals *) (*((void ***) tsrm_ls))[executor_globals_id-1])->function_table)`,这样我们在传了tsrm_ls的函数中就可能读取内存使用了。 244 | 245 | PHP5的这种处理方式简单但是很不优雅,不管你用不用TSRM都不得不在函数中加上那两个宏,而且很容易遗漏。后来Anatol Belski在PHP的rfc提交了一种新的处理方式:[https://wiki.php.net/rfc/native-tls](https://wiki.php.net/rfc/native-tls),新的处理方式最终在PHP7版本得以实现,通过静态TLS将各线程的storage保存在全局变量中,各函数中使用时直接读取即可。 246 | 247 | linux下这种全局变量通过加上`__thread`定义,这样各线程更新这个变量就不会冲突了,实际这是gcc提供的,详细的内容这里不再展开,有兴趣的可以再查下详细的资料。举个例子: 248 | ```c 249 | #include 250 | #include 251 | #include 252 | #include 253 | 254 | __thread int num = 0; 255 | 256 | void* worker(void* arg){ 257 | while(1){ 258 | printf("thread:%d\n", num); 259 | sleep(1); 260 | } 261 | } 262 | 263 | int main(void) 264 | { 265 | pthread_t tid; 266 | int ret; 267 | 268 | if ((ret = pthread_create(&tid, NULL, worker, NULL)) != 0){ 269 | return 1; 270 | } 271 | 272 | while(1){ 273 | num = 4; 274 | printf("main:%d\n", num); 275 | sleep(1); 276 | } 277 | 278 | return 0; 279 | } 280 | ``` 281 | 这个例子有两个线程,其中主线程修改了全局变量num,但是并没有影响另外一个线程。 282 | 283 | PHP7中用于缓存各线程storage的全局变量定义在`Zend/zend.c`: 284 | ```c 285 | #ifdef ZTS 286 | //这些都是全局变量 287 | ZEND_API int compiler_globals_id; 288 | ZEND_API int executor_globals_id; 289 | static HashTable *global_function_table = NULL; 290 | static HashTable *global_class_table = NULL; 291 | static HashTable *global_constants_table = NULL; 292 | static HashTable *global_auto_globals_table = NULL; 293 | static HashTable *global_persistent_list = NULL; 294 | ZEND_TSRMLS_CACHE_DEFINE() //=>TSRM_TLS void *TSRMLS_CACHE = NULL; 展开后: __thread void *_tsrm_ls_cache = NULL; _tsrm_ls_cache就是各线程storage的地址 295 | #endif 296 | ``` 297 | 比如EG: 298 | ```c 299 | # define EG(v) ZEND_TSRMG(executor_globals_id, zend_executor_globals *, v) 300 | 301 | #define ZEND_TSRMG TSRMG_STATIC 302 | #define TSRMG_STATIC(id, type, element) (TSRMG_BULK_STATIC(id, type)->element) 303 | #define TSRMG_BULK_STATIC(id, type) ((type) (*((void ***) TSRMLS_CACHE))[TSRM_UNSHUFFLE_RSRC_ID(id)]) 304 | ``` 305 | EG(xxx)最终展开:((zend_executor_globals *) (*((void ***) _tsrm_ls_cache))[executor_globals_id-1]->xxx)。 306 | 307 | -------------------------------------------------------------------------------- /7/class.md: -------------------------------------------------------------------------------- 1 | ## 7.9 面向对象 2 | ### 7.9.1 定义内部类 3 | 在扩展中定义一个内部类的方式与函数类似,函数最终注册到EG(function_table),而类则最终注册到EG(class_table)符号表中,注册的过程首先是为类创建一个zend_class_entry结构,然后把这个结构插入EG(class_table),当然这个过程不需要我们手动操作,PHP提供了现成的方法和宏帮我们对zend_class_entry进行初始化以及注册。通常情况下会把内部类的注册放到module startup阶段,也就是定义在扩展的`PHP_MINIT_FUNCTION()`中,一个简单的类的注册只需要以下几行: 4 | ```c 5 | PHP_MINIT_FUNCTION(mytest) 6 | { 7 | //分配一个zend_class_entry,这个结构只在注册时使用,所以分配在栈上即可 8 | zend_class_entry ce; 9 | //对zend_class_entry进行初始化 10 | INIT_CLASS_ENTRY(ce, "MyClass", NULL); 11 | //注册 12 | zend_register_internal_class(&ce); 13 | } 14 | ``` 15 | 这样就成功定义了一个内部类,类名为"MyClass",只是这个类还没有任何的成员属性、成员方法,定义完成后重新编译、安装扩展,然后在PHP脚本中实例化这个类: 16 | ```php 17 | $obj = new MyClass(); 18 | 19 | var_dump($obj); 20 | ``` 21 | 结果将输出: 22 | ``` 23 | object(MyClass)#1 (0) { 24 | } 25 | ``` 26 | 注册时传入的zend_class_entry并不是最终插入class_table符号表的结构,zend_register_internal_class()中会重新分配,所以注册时的这个结构分配在栈上即可,此结构的成员不需要手动定义,PHP提供了宏供扩展使用,扩展只需要提供类的主要信息即可,常用的两个宏: 27 | ```c 28 | /** 29 | * 初始化zend_class_entry 30 | * class_container:zend_class_entry地址 31 | * class_name:类名 32 | * functions:成员方法数组 33 | */ 34 | #define INIT_CLASS_ENTRY(class_container, class_name, functions) \ 35 | INIT_OVERLOADED_CLASS_ENTRY(class_container, class_name, functions, NULL, NULL, NULL) 36 | 37 | /** 38 | * 初始化zend_class_entry,带namespace 39 | * class_container:zend_class_entry地址 40 | * ns:命名空间 41 | * class_name:类名 42 | * functions:成员方法数组 43 | */ 44 | #define INIT_NS_CLASS_ENTRY(class_container, ns, class_name, functions) \ 45 | INIT_CLASS_ENTRY(class_container, ZEND_NS_NAME(ns, class_name), functions) 46 | ``` 47 | 48 | ### 7.9.2 定义成员属性 49 | 50 | ### 7.9.3 定义成员方法 51 | 52 | ### 7.9.4 定义常量 53 | 54 | ### 7.9.5 类的实例化 55 | -------------------------------------------------------------------------------- /7/conf.md: -------------------------------------------------------------------------------- 1 | ## 7.5 运行时配置 2 | 3 | ### 7.5.1 全局变量(资源) 4 | 使用C语言开发程序时经常会使用全局变量进行数据存储,这就涉及前面已经介绍过的一个问题:线程安全,PHP设计了TSRM(即:线程安全资源管理器)用于解决这个问题,内核中频繁使用到的EG、CG等都是根据是否开启ZTS封装的宏,同样的,在扩展中也需要必须按照TSRM的规范定义全局变量,除非你的扩展不支持多线程的环境。 5 | 6 | PHP为扩展的全局变量提供了一种存储方式:每个扩展将自己所有的全局变量统一定义在一个结构体中,然后将这个结构体注册到TSRM中,这样扩展就可以像使用EG、CG那样访问这个结构体。 7 | 8 | 这个结构体的定义通过`ZEND_BEGIN_MODULE_GLOBALS(extension_name)`、`ZEND_END_MODULE_GLOBALS(extension_name)`两个宏完成,这两个宏必须成对出现,中间定义扩展需要的全局变量即可。 9 | ```c 10 | ZEND_BEGIN_MODULE_GLOBALS(mytest) 11 | zend_long open_cache; 12 | HashTable class_table; 13 | ZEND_END_MODULE_GLOBALS(mytest) 14 | ``` 15 | 展开后实际就是个普通的struct: 16 | ```c 17 | typedef struct _zend_mytest_globals { 18 | zend_long open_cache; 19 | HashTable class_table; 20 | }zend_mytest_globals; 21 | ``` 22 | 接着创建一个此结构体的全局变量,这时候就会涉及ZTS了,如果未开启线程安全直接创建普通的全局变量即可,如果开启线程安全了则需要向TSRM注册,得到一个唯一的资源id,这个操作也由专门的宏来完成:`ZEND_DECLARE_MODULE_GLOBALS(extension_name)`,展开后: 23 | ```c 24 | //ZTS:此时只是定义资源id,并没有向TSRM注册 25 | ts_rsrc_id mytest_globals_id; 26 | 27 | //非ZTS 28 | zend_mytest_globals mytest_globals; 29 | ``` 30 | 最后需要定义一个像EG、CG那样的宏用于访问扩展的全局资源结构体,这一步将使用`ZEND_MODULE_GLOBALS_ACCESSOR()`宏完成: 31 | ```c 32 | #define MYTEST_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(mytest, v) 33 | ``` 34 | 看起来是不是跟EG、CG的定义非常像?这个宏展开后: 35 | ```c 36 | //ZTS 37 | #define MYTEST_G(v) ZEND_TSRMG(mytest_globals_id, zend_mytest_globals *, v) 38 | 39 | //非ZTS 40 | #define MYTEST_G(v) (mytest_globals.v) 41 | ``` 42 | 接下来就可以在扩展中通过:MYTEST_G(opene_cache)、MYTEST_G(class_table)对结构体成员进行读写了。通常会把这个全局资源结构体及结构体的访问宏定义在头文件中,然后把全局变量的声明放到源文件中: 43 | ```c 44 | //php_mytest.h 45 | #define MYTEST_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(mytest, v) 46 | 47 | ZEND_BEGIN_MODULE_GLOBALS(mytest) 48 | zend_long open_cache; 49 | HashTable class_table; 50 | ZEND_END_MODULE_GLOBALS(mytest) 51 | 52 | //mytest.c 53 | ZEND_DECLARE_MODULE_GLOBALS(mytest) 54 | ``` 55 | > 在一个扩展中并不是只能定义一个全局变量结构,数目是不限制的。 56 | 57 | ### 7.5.2 php.ini配置 58 | php.ini是PHP主要的配置文件,解析时PHP将在这些地方依次查找该文件:当前工作目录、环境变量PHPRC指定目录、编译时指定的路径,在命令行模式下,php.ini的查找路径可以用`-c`参数替代。 59 | 60 | 该文件的语法非常简单:`配置标识符 = 值`。空白字符和用分号';'开始的行被忽略,[xxx]行也被忽略;配置标识符大写敏感,通常会用'.'区分不同的节;值可以是数字、字符串、PHP常量、位运算表达式。 61 | 62 | 关于php.ini的解析过程本节不作介绍,只从应用的角度介绍如何在一个扩展中获取一个配置项,通常会把php.ini的配置映射到一个变量,从而在使用时直接读取那个变量,也就是把所有的配置转化为了C语言中的变量,扩展中一般会把php.ini配置映射到上一节介绍的全局变量(资源),要想实现这个转化需要在扩展中为每一项配置设置映射规则: 63 | ```c 64 | PHP_INI_BEGIN() 65 | //每一项配置规则 66 | ... 67 | PHP_INI_END(); 68 | ``` 69 | 这两个宏实际只是把各配置规则组成一个数组,配置规则通过`STD_PHP_INI_ENTRY()`设置: 70 | ```c 71 | STD_PHP_INI_ENTRY(name,default_value,modifiable,on_modify,property_name,struct_type,struct_ptr) 72 | ``` 73 | * __name:__ php.ini中的配置标识符 74 | * __default_value:__ 默认值,注意不管转化后是什么类型,这里必须设置为字符串 75 | * __modifiable:__ 可修改等级,ZEND_INI_USER为可以在php脚本中修改,ZEND_INI_SYSTEM为可以在php.ini中修改,还有一个ZEND_INI_PERDIR,ZEND_INI_ALL表示三种都可以,通常情况下设置为ZEND_INI_ALL、ZEND_INI_SYSTEM即可 76 | * __on_modify:__ 函数指针,用于指定发现这个配置后赋值处理的函数,默认提供了5个:OnUpdateBool、OnUpdateLong、OnUpdateLongGEZero、OnUpdateReal、OnUpdateString、OnUpdateStringUnempty,支持可以自定义 77 | * __property_name:__ 要映射到的结构struct_type中的成员 78 | * __struct_type:__ 映射结构的类型 79 | * __struct_ptr:__ 映射结构的变量地址,发现配置后会 80 | 81 | > 除了STD_PHP_INI_ENTRY()这个宏还有一个类似的宏`STD_PHP_INI_BOOLEAN()`,用法一致,差别在于后者会自动把配置添加到phpinfo()输出中。 82 | 83 | 这个宏展开后生成一个`zend_ini_entry_def`结构: 84 | ```c 85 | typedef struct _zend_ini_entry_def { 86 | const char *name; 87 | int (*on_modify)(zend_ini_entry *entry, zend_string *new_value, void *mh_arg1, void *mh_arg2, void *mh_arg3, int stage); 88 | void *mh_arg1; //映射成员所在结构体的偏移:offsetof(type, member-designator)取到 89 | void *mh_arg2; //要映射到结构的地址 90 | void *mh_arg3; 91 | const char *value;//默认值 92 | void (*displayer)(zend_ini_entry *ini_entry, int type); 93 | int modifiable; 94 | 95 | uint name_length; 96 | uint value_length; 97 | } zend_ini_entry_def; 98 | ``` 99 | 比如将php.ini中的`mytest.opene_cache`值映射到`MYTEST_G()`结构中的open_cache,类型为zend_long,默认值109,则可以这么定义: 100 | ```c 101 | PHP_INI_BEGIN() 102 | STD_PHP_INI_ENTRY("mytest.open_cache", "109", PHP_INI_ALL, OnUpdateLong, open_cache, zend_mytest_globals, mytest_globals) 103 | PHP_INI_END(); 104 | ``` 105 | property_name设置的是要映射到的结构成员`mytest_globals->open_cache`,zend_mytest_globals、mytest_globals都是宏展开后的实际值,前者是结构体类型,后者是具体分配的变量,上面的定义展开后: 106 | ```c 107 | static const zend_ini_entry_def ini_entries[] = { 108 | { 109 | "mytest.open_cache", 110 | OnUpdateLong, 111 | (void *) XtOffsetOf(zend_mytest_globals, open_cache), //获取成员在结构体中的内存偏移 112 | (void*)&mytest_globals, 113 | NULL, 114 | "109", 115 | NULL, 116 | PHP_INI_ALL, 117 | sizeof("mytest.open_cache")-1, 118 | sizeof("109")-1 119 | }, 120 | { NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0, 0} 121 | } 122 | ``` 123 | > `XtOffsetOf()`这个宏在linux环境下展开就是`offsetof()`,用来获取一个结构体成员的offset,比如: 124 | > 125 | > #include 126 | > #include 127 | > 128 | > typedef struct{ 129 | > int id; 130 | > char *name; 131 | > }my_struct; 132 | > 133 | > int main(void) 134 | > { 135 | > printf("%d\n", (void*)offsetof(my_struct, name)); 136 | > return 0; 137 | > } 138 | > 139 | > 通过这个offset及结构体指针就可以读取这个成员:`(char*)my_sutct + offset`,等价于`my_sutct->name`。 140 | 141 | 定义完上面的配置映射规则后就可以进行映射了,这一步通过`REGISTER_INI_ENTRIES()`完成,这个宏展开后:`zend_register_ini_entries(ini_entries, module_number)`,ini_entries是`PHP_INI_BEGIN/END()`两个宏生成的配置映射规则数组,通常会把这个操作放到`PHP_MINIT_FUNCTION()`中,注意:此时php.ini已经解析到`configuration_hash`哈希表中,`zend_register_ini_entries()`将根据配置name查找这个哈希表,如果找到了表明用户在php.ini中配置了该项,然后将调用此规则指定的on_modify函数进行赋值,比如上面的示例将调用`OnUpdateLong()`处理,整体的流程: 142 | ```c 143 | ZEND_API int zend_register_ini_entries(const zend_ini_entry_def *ini_entry, int module_number) 144 | { 145 | zend_ini_entry *p; 146 | zval *default_value; 147 | HashTable *directives = registered_zend_ini_directives; 148 | 149 | while (ini_entry->name) { 150 | //分配zend_ini_entry结构 151 | p = pemalloc(sizeof(zend_ini_entry), 1); 152 | //zend_ini_entry初始化 153 | ... 154 | 155 | //添加到registered_zend_ini_directives,EG(ini_directives)也是指向此HashTable 156 | if (zend_hash_add_ptr(directives, p->name, (void*)p) == NULL) { 157 | ... 158 | } 159 | 160 | //zend_get_configuration_directive()最终将调用cfg_get_entry() 161 | //从configuration_hash哈希表中查找配置,如果没有找到将使用默认值 162 | default_value = zend_get_configuration_directive(p->name) 163 | ... 164 | if (p->on_modify) { 165 | //调用定义的赋值handler处理 166 | p->on_modify(p, p->value, p->mh_arg1, p->mh_arg2, p->mh_arg3, ZEND_INI_STAGE_STARTUP); 167 | } 168 | } 169 | } 170 | ``` 171 | `OnUpdateLong()`赋值处理: 172 | ```c 173 | ZEND_API ZEND_INI_MH(OnUpdateLong) 174 | { 175 | zend_long *p; 176 | #ifndef ZTS 177 | //存储结构的指针 178 | char *base = (char *) mh_arg2; 179 | #else 180 | char *base; 181 | //ZTS下需要向TSRM中获取存储结构的指针 182 | base = (char *) ts_resource(*((int *) mh_arg2)); 183 | #endif 184 | //指向结构体成员的位置 185 | p = (zend_long *) (base+(size_t) mh_arg1); 186 | //将值转为zend_long 187 | *p = zend_atol(ZSTR_VAL(new_value), (int)ZSTR_LEN(new_value)); 188 | return SUCCESS; 189 | } 190 | ``` 191 | 如果PHP提供的几个on_modify不能满足需求可以自定义on_modify函数,举个例子:将php.ini中的配置`mytest.class`插入MYTESY_G(class_table)哈希表,则可以在扩展中定义这样一个on_modify:`ZEND_INI_MH(OnUpdateAddArray)`,将php.ini映射到全局变量的完整代码: 192 | ```c 193 | //php_mytest.h 194 | #define MYTEST_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(mytest, v) 195 | 196 | ZEND_BEGIN_MODULE_GLOBALS(mytest) 197 | zend_long open_cache; 198 | HashTable class_table; 199 | ZEND_END_MODULE_GLOBALS(mytest) 200 | 201 | //自定义on_modify函数 202 | ZEND_API ZEND_INI_MH(OnUpdateAddArray); 203 | ``` 204 | ```c 205 | //mytest.c 206 | ZEND_DECLARE_MODULE_GLOBALS(mytest) 207 | 208 | PHP_INI_BEGIN() 209 | STD_PHP_INI_ENTRY("mytest.open_cache", "109", PHP_INI_ALL, OnUpdateLong, open_cache, zend_mytest_globals, mytest_globals) 210 | STD_PHP_INI_ENTRY("mytest.class", "stdClass", PHP_INI_ALL, OnUpdateAddArray, class_table, zend_mytest_globals, mytest_globals) 211 | PHP_INI_END(); 212 | 213 | ZEND_API ZEND_INI_MH(OnUpdateAddArray) 214 | { 215 | HashTable *ht; 216 | zval val; 217 | #ifndef ZTS 218 | char *base = (char *) mh_arg2; 219 | #else 220 | char *base; 221 | base = (char *) ts_resource(*((int *) mh_arg2)); 222 | #endif 223 | 224 | ht = (HashTable*)(base+(size_t) mh_arg1); 225 | ZVAL_NULL(&val); 226 | zend_hash_add(ht, new_value, &val); 227 | } 228 | 229 | PHP_MINIT_FUNCTION(mytest) 230 | { 231 | zend_hash_init(&MYTEST_G(class_table), 0, NULL, NULL, 1); 232 | //将php.ini解析到指定结构体 233 | REGISTER_INI_ENTRIES(); 234 | 235 | printf("open_cache %d\n", MYTEST_G(open_cache)); 236 | } 237 | 238 | zend_module_entry mytest_module_entry = { 239 | STANDARD_MODULE_HEADER, 240 | "mytest", 241 | NULL,//mytest_functions, 242 | PHP_MINIT(mytest), 243 | NULL,//PHP_MSHUTDOWN(mytest), 244 | NULL,//PHP_RINIT(mytest), 245 | NULL,//PHP_RSHUTDOWN(mytest), 246 | NULL,//PHP_MINFO(mytest), 247 | "1.0.0", 248 | STANDARD_MODULE_PROPERTIES 249 | }; 250 | 251 | #ifdef COMPILE_DL_TIMEOUT 252 | #ifdef ZTS 253 | ZEND_TSRMLS_CACHE_DEFINE() 254 | #endif 255 | ZEND_GET_MODULE(mytest) 256 | #endif 257 | ``` 258 | 本节主要介绍了如何将php.ini配置项解析到C语言变量中,总结下主要分为两步: 259 | * __定义解析规则:__ 通过PHP_INI_BEGIN()、PHP_INI_END()、STD_PHP_INI_ENTRY()配置 260 | * __执行规则映射:__ 由REGISTER_INI_ENTRIES()来完成,这个操作之后解析目的变量就可以使用了 261 | 262 | -------------------------------------------------------------------------------- /7/constant.md: -------------------------------------------------------------------------------- 1 | ## 7.8 常量 2 | 常量的具体实现前面章节已经介绍过,这里不再重复。PHP提供了很多用于常量注册的宏,可以在扩展的`PHP_MINIT_FUNCTION()`中定义: 3 | ```c 4 | //注册NULL常量 5 | #define REGISTER_NULL_CONSTANT(name, flags) \ 6 | zend_register_null_constant((name), sizeof(name)-1, (flags), module_number) 7 | 8 | //注册bool常量 9 | #define REGISTER_BOOL_CONSTANT(name, bval, flags) \ 10 | zend_register_bool_constant((name), sizeof(name)-1, (bval), (flags), module_number) 11 | 12 | //注册整形常量 13 | #define REGISTER_LONG_CONSTANT(name, lval, flags) \ 14 | zend_register_long_constant((name), sizeof(name)-1, (lval), (flags), module_number) 15 | 16 | //注册浮点型常量 17 | #define REGISTER_DOUBLE_CONSTANT(name, dval, flags) \ 18 | zend_register_double_constant((name), sizeof(name)-1, (dval), (flags), module_number) 19 | 20 | //注册字符串常量,str类型为char* 21 | #define REGISTER_STRING_CONSTANT(name, str, flags) \ 22 | zend_register_string_constant((name), sizeof(name)-1, (str), (flags), module_number) 23 | 24 | //注册字符串常量,截取指定长度,str类型为char* 25 | #define REGISTER_STRINGL_CONSTANT(name, str, len, flags) \ 26 | zend_register_stringl_constant((name), sizeof(name)-1, (str), (len), (flags), module_number) 27 | ``` 28 | 除了上面这些还有`REGISTER_NS_XXX`系列的宏用于带namespace的常量注册,另外如果这些类型不能满足需求,则可以通过`zend_register_constant(zend_constant *c)`注册,比如常量类型为数组。 29 | ```c 30 | PHP_MINIT_FUNCTION(mytest) 31 | { 32 | ... 33 | 34 | REGISTER_STRING_CONSTANT("MY_CONS_1", "this is a constant", CONST_CS | CONST_PERSISTENT); 35 | } 36 | ``` 37 | ```php 38 | echo MY_CONS_1; 39 | =========[output]========= 40 | this is a constant 41 | ``` 42 | 如果在扩展中需要用到其他扩展或内核定义的常量,则可以通过以下函数获取常量的值: 43 | ```c 44 | ZEND_API zval *zend_get_constant(zend_string *name); 45 | ZEND_API zval *zend_get_constant_str(const char *name, size_t name_len); 46 | ``` 47 | -------------------------------------------------------------------------------- /7/extension_intro.md: -------------------------------------------------------------------------------- 1 | ## 7.3 扩展的构成及编译 2 | 3 | ### 7.3.1 扩展的构成 4 | 扩展首先需要创建一个`zend_module_entry`结构,这个变量必须是全局变量,且变量名必须是:`扩展名称_module_entry`,内核通过这个结构得到这个扩展都提供了哪些功能,换句话说,一个扩展可以只包含一个`zend_module_entry`结构,相当于定义了一个什么功能都没有的扩展。 5 | ```c 6 | //zend_modules.h 7 | struct _zend_module_entry { 8 | unsigned short size; //sizeof(zend_module_entry) 9 | unsigned int zend_api; //ZEND_MODULE_API_NO 10 | unsigned char zend_debug; //是否开启debug 11 | unsigned char zts; //是否开启线程安全 12 | const struct _zend_ini_entry *ini_entry; 13 | const struct _zend_module_dep *deps; 14 | const char *name; //扩展名称,不能重复 15 | const struct _zend_function_entry *functions; //扩展提供的内部函数列表 16 | int (*module_startup_func)(INIT_FUNC_ARGS); //扩展初始化回调函数,PHP_MINIT_FUNCTION或ZEND_MINIT_FUNCTION定义的函数 17 | int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS); //扩展关闭时回调函数 18 | int (*request_startup_func)(INIT_FUNC_ARGS); //请求开始前回调函数 19 | int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS); //请求结束时回调函数 20 | void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS); //php_info展示的扩展信息处理函数 21 | const char *version; //版本 22 | ... 23 | unsigned char type; 24 | void *handle; 25 | int module_number; //扩展的唯一编号 26 | const char *build_id; 27 | }; 28 | ``` 29 | 这个结构包含很多成员,但并不是所有的都需要自己定义,经常用到的主要有下面几个: 30 | * __name:__ 扩展名称,不能重复 31 | * __functions:__ 扩展定义的内部函数entry 32 | * __module_startup_func:__ PHP在模块初始化时回调的hook函数,可以使扩展介入module startup阶段 33 | * __module_shutdown_func:__ 在模块关闭阶段回调的函数 34 | * __request_startup_func:__ 在请求初始化阶段回调的函数 35 | * __request_shutdown_func:__ 在请求结束阶段回调的函数 36 | * __info_func:__ php_info()函数时调用,用于展示一些配置、运行信息 37 | * __version:__ 扩展版本 38 | 39 | 除了上面这些需要手动设置的成员,其它部分可以通过`STANDARD_MODULE_HEADER`、`STANDARD_MODULE_PROPERTIES`宏统一设置,扩展提供的内部函数及四个执行阶段的钩子函数是扩展最常用到的部分,几乎所有的扩展都是基于这两部分实现的。有了这个结构还需要提供一个接口来获取这个结构变量,这个接口是统一的,扩展中通过`ZEND_GET_MODULE(extension_name)`完成这个接口的定义: 40 | ``` 41 | //zend_API.h 42 | #define ZEND_GET_MODULE(name) \ 43 | BEGIN_EXTERN_C()\ 44 | ZEND_DLEXPORT zend_module_entry *get_module(void) { return &name##_module_entry; }\ 45 | END_EXTERN_C() 46 | ``` 47 | 展开后可以看到,实际就是定义了一个get_module()函数,返回扩展zend_module_entry结构的地址,这就是为什么这个结构的变量名必须是`扩展名称_module_entry`这种格式的原因。 48 | 49 | 有了扩展的zend_module_entry结构以及获取这个结构的接口一个合格的扩展就编写完成了,只是这个扩展目前还什么都干不了: 50 | ```c 51 | #include "php.h" 52 | #include "php_ini.h" 53 | #include "ext/standard/info.h" 54 | 55 | zend_module_entry mytest_module_entry = { 56 | STANDARD_MODULE_HEADER, 57 | "mytest", 58 | NULL, //mytest_functions, 59 | NULL, //PHP_MINIT(mytest), 60 | NULL, //PHP_MSHUTDOWN(mytest), 61 | NULL, //PHP_RINIT(mytest), 62 | NULL, //PHP_RSHUTDOWN(mytest), 63 | NULL, //PHP_MINFO(mytest), 64 | "1.0.0", 65 | STANDARD_MODULE_PROPERTIES 66 | }; 67 | 68 | ZEND_GET_MODULE(mytest) 69 | ``` 70 | 编译、安装后执行`php -m`就可以看到my_test这个扩展了。 71 | 72 | ### 7.3.2 编译工具 73 | PHP提供了几个脚本工具用于简化扩展的实现:ext_skel、phpize、php-config,后面两个脚本主要配合autoconf、automake生成Makefile。在介绍这几个工具之前,我们先看下PHP安装后的目录结构,因为很多脚本、配置都放置在安装后的目录中,比如PHP的安装路径为:/usr/local/php7,则此目录的主要结构: 74 | ```c 75 | |---php7 76 | | |---bin //php编译生成的二进制程序目录 77 | | |---php //cli模式的php 78 | | |---phpize 79 | | |---php-config 80 | | |---... 81 | | |---etc //一些sapi的配置 82 | | |---include //php源码的头文件 83 | | |---php 84 | | |---main //PHP中的头文件 85 | | |---Zend //Zend头文件 86 | | |---TSRM //TSRM头文件 87 | | |---ext //扩展头文件 88 | | |---sapi //SAPI头文件 89 | | |---include 90 | | |---lib //依赖的so库 91 | | |---php 92 | | |---extensions //扩展so保存目录 93 | | |---build //编译时的工具、m4配置等,编写扩展是会用到 94 | | |---acinclude.m4 //PHP自定义的autoconf宏 95 | | |---libtool.m4 //libtool定义的autoconf宏,acinclude.m4、libtool.m4会被合成aclocal.m4 96 | | |---phpize.m4 //PHP核心configure.in配置 97 | | |---... 98 | | |---... 99 | | |---php 100 | | |---sbin //SAPI编译生成的二进制程序,php-fpm会放在这 101 | | |---var //log、run日志 102 | ``` 103 | 104 | #### 7.3.2.1 ext_skel 105 | 这个脚本位于PHP源码/ext目录下,它的作用是用来生成扩展的基本骨架,帮助开发者快速生成一个规范的扩展结构,可以通过以下命令生成一个扩展结构: 106 | ```c 107 | ./ext_skel --extname=扩展名称 108 | ``` 109 | 执行完以后会在ext目录下新生成一个扩展目录,比如extname是mytest,则将生成以下文件: 110 | ```c 111 | |---mytest 112 | | |---config.m4 //autoconf规则的编译配置文件 113 | | |---config.w32 //windows环境的配置 114 | | |---CREDITS 115 | | |---EXPERIMENTAL 116 | | |---include //依赖库的include头文件,可以不用 117 | | |---mytest.c //扩展源码 118 | | |---php_mytest.h //头文件 119 | | |---mytest.php //用于在PHP中测试扩展是否可用,可以不用 120 | | |---tests //测试用例,执行make test时将执行、验证这些用例 121 | | |---001.phpt 122 | ``` 123 | 这个脚本主要生成了编译需要的配置以及扩展的基本结构,初步生成的这个扩展可以成功的编译、安装、使用,实际开发中我们可以使用这个脚本生成一个基本结构,然后根据具体的需要逐步完善。 124 | ### 7.3.2.2 php-config 125 | 这个脚本为PHP源码中的/script/php-config.in,PHP安装后被移到安装路径的/bin目录下,并重命名为php-config,这个脚本主要是获取PHP的安装信息的,主要有: 126 | * __PHP安装路径__ 127 | * __PHP版本__ 128 | * __PHP源码的头文件目录:__ main、Zend、ext、TSRM中的头文件,编写扩展时会用到这些头文件,这些头文件保存在PHP安装位置/include/php目录下 129 | * __LDFLAGS:__ 外部库路径,比如:`-L/usr/bib -L/usr/local/lib` 130 | * __依赖的外部库:__ 告诉编译器要链接哪些文件,`-lcrypt -lresolv -lcrypt`等等 131 | * __扩展存放目录:__ 扩展.so保存位置,安装扩展make install时将安装到此路径下 132 | * __编译的SAPI:__ 如cli、fpm、cgi等 133 | * __PHP编译参数:__ 执行./configure时带的参数 134 | * ... 135 | 136 | 这个脚本在编译扩展时会用到,执行`./configure --with-php-config=xxx`生成Makefile时作为参数传入即可,它的作用是提供给configure.in获取上面几个配置,生成Makefile。 137 | 138 | #### 7.3.2.3 phpize 139 | 这个脚本主要是操作复杂的autoconf/automake/autoheader/autolocal等系列命令,用于生成configure文件,GNU auto系列的工具众多,这里简单介绍下基本的使用: 140 | 141 | __(1)autoscan:__ 在源码目录下扫描,生成configure.scan,然后把这个文件重名为为configure.in,可以在这个文件里对依赖的文件、库进行检查以及配置一些编译参数等。 142 | 143 | __(2)aclocal:__ automake中有很多宏可以在configure.in或其它.m4配置中使用,这些宏必须定义在aclocal.m4中,否则将无法被autoconf识别,aclocal可以根据configure.in自动生成aclocal.m4,另外,autoconf提供的特性不可能满足所有的需求,所以autoconf还支持自定义宏,用户可以在acinclude.m4中定义自己的宏,然后在执行aclocal生成aclocal.m4时也会将acinclude.m4加载进去。 144 | 145 | __(3)autoheader:__ 它可以根据configure.in、aclocal.m4生成一个C语言"define"声明的头文件模板(config.h.in)供configure执行时使用,比如很多程序会通过configure提供一些enable/disable的参数,然后根据不同的参数决定是否开启某些选项,这种就可以根据编译参数的值生成一个define宏,比如:`--enabled-xxx`生成`#define ENABLED_XXX 1`,否则默认生成`#define ENABLED_XXX 0`,代码里直接使用这个宏即可。比如configure.in文件内容如下: 146 | ```sh 147 | AC_PREREQ([2.63]) 148 | AC_INIT([FULL-PACKAGE-NAME], [VERSION], [BUG-REPORT-ADDRESS]) 149 | 150 | AC_CONFIG_HEADERS([config.h]) 151 | 152 | AC_ARG_ENABLE(xxx, "--enable-xxx if enable xxx",[ 153 | AC_DEFINE([ENABLED_XXX], [1], [enabled xxx]) 154 | ], 155 | [ 156 | AC_DEFINE([ENABLED_XXX], [0], [disabled xxx]) 157 | ]) 158 | 159 | AC_OUTPUT 160 | ``` 161 | 执行autoheader后将生成一个config.h.in的文件,里面包含`#undef ENABLED_XXX`,最终执行`./configure --enable-xxx`后将生成一个config.h文件,包含`#define ENABLED_XXX 1`。 162 | 163 | __(4)autoconf:__ 将configure.in中的宏展开生成configure、config.h,此过程会用到aclocal.m4中定义的宏。 164 | 165 | __(5)automake:__ 将Makefile.am中定义的结构建立Makefile.in,然后configure脚本将生成的Makefile.in文件转换为Makefile。 166 | 167 | 各步骤之间的转化关系如下图: 168 | 169 | ![](../img/autoconf.png) 170 | 171 | 编写PHP扩展时并不需要操作上面全部的步骤,PHP提供了两个编辑好的配置:configure.in、acinclude.m4,这两个配置是从PHP安装路径/lib/php/build目录下的phpize.m4、acinclude.m4复制生成的,其中configure.in中定义了一些PHP内核相关的配置检查项,另外这个文件会include每个扩展各自的配置:config.m4,所以编写扩展时我们只需要在config.m4中定义扩展自己的配置就可以了,不需要关心依赖的PHP内核相关的配置,在扩展所在目录下执行phpize就可以生成扩展的configure、config.h文件了。 172 | 173 | configure.in(phpize.m4): 174 | ```sh 175 | AC_PREREQ(2.59) 176 | AC_INIT(config.m4) 177 | ... 178 | #--with-php-config参数 179 | PHP_ARG_WITH(php-config,, 180 | [ --with-php-config=PATH Path to php-config [php-config]], php-config, no) 181 | 182 | PHP_CONFIG=$PHP_PHP_CONFIG 183 | ... 184 | #加载扩展配置 185 | sinclude(config.m4) 186 | ... 187 | AC_CONFIG_HEADER(config.h) 188 | 189 | AC_OUTPUT() 190 | ``` 191 | __phpize中的主要操作:__ 192 | 193 | __(1)phpize_check_configm4:__ 检查扩展的config.m4是否存在。 194 | 195 | __(2)phpize_check_build_files:__ 检查php安装路径下的lib/php/build/,这个目录下包含PHP自定义的autoconf宏文件acinclude.m4以及libtool;检查扩展所在目录。 196 | 197 | __(3)phpize_print_api_numbers:__ 输出PHP Api Version、Zend Module Api No、Zend Extension Api No信息。 198 | ```sh 199 | phpize_get_api_numbers() 200 | { 201 | # extracting API NOs: 202 | PHP_API_VERSION=`grep '#define PHP_API_VERSION' $includedir/main/php.h|$SED 's/#define PHP_API_VERSION//'` 203 | ZEND_MODULE_API_NO=`grep '#define ZEND_MODULE_API_NO' $includedir/Zend/zend_modules.h|$SED 's/#define ZEND_MODULE_API_NO//'` 204 | ZEND_EXTENSION_API_NO=`grep '#define ZEND_EXTENSION_API_NO' $includedir/Zend/zend_extensions.h|$SED 's/#define ZEND_EXTENSION_API_NO//'` 205 | } 206 | ``` 207 | __(4)phpize_copy_files:__ 将PHP安装位置/lib/php/build目录下的mkdep.awk scan_makefile_in.awk shtool libtool.m4四个文件拷到扩展的build目录下,然后将acinclude.m4 Makefile.global config.sub config.guess ltmain.sh run-tests*.php文件拷到扩展根目录,最后将acinclude.m4、build/libtool.m4合并到扩展目录下的aclocal.m4文件中。 208 | ```sh 209 | phpize_copy_files() 210 | { 211 | test -d build || mkdir build 212 | 213 | (cd "$phpdir" && cp $FILES_BUILD "$builddir"/build) 214 | (cd "$phpdir" && cp $FILES "$builddir") 215 | #acinclude.m4、libtool.m4合并到aclocal.m4 216 | (cd "$builddir" && cat acinclude.m4 ./build/libtool.m4 > aclocal.m4) 217 | } 218 | ``` 219 | __(5)phpize_replace_prefix:__ 将PHP安装位置/lib/php/build/phpize.m4拷贝到扩展目录下,将文件中的prefix替换为PHP安装路径,然后重命名为configure.in。 220 | ```sh 221 | phpize_replace_prefix() 222 | { 223 | $SED \ 224 | -e "s#/usr/local/php7#$prefix#" \ 225 | < "$phpdir/phpize.m4" > configure.in 226 | } 227 | ``` 228 | __(6)phpize_check_shtool:__ 检查/build/shtool。 229 | 230 | __(7)phpize_check_autotools:__ 检查autoconf、autoheader。 231 | 232 | __(8)phpize_autotools__ 执行autoconf生成configure,然后再执行autoheader生成config.h。 233 | 234 | ### 7.3.3 编写扩展的基本步骤 235 | 编写一个PHP扩展主要分为以下几步: 236 | * 通过ext目录下ext_skel脚本生成扩展的基本框架:`./ext_skel --extname`; 237 | * 修改config.m4配置:设置编译配置参数、设置扩展的源文件、依赖库/函数检查等等; 238 | * 编写扩展要实现的功能:按照PHP扩展的格式以及PHP提供的API编写功能; 239 | * 生成configure:扩展编写完成后执行phpize脚本生成configure及其它配置文件; 240 | * 编译&安装:./configure、make、make install,然后将扩展的.so路径添加到php.ini中。 241 | 242 | 最后就可以在PHP中使用这个扩展了。 243 | 244 | ### 7.3.4 config.m4 245 | config.m4是扩展的编译配置文件,它被include到configure.in文件中,最终被autoconf编译为configure,编写扩展时我们只需要在config.m4中修改配置即可,一个简单的扩展配置只需要包含以下内容: 246 | ```c 247 | PHP_ARG_WITH(扩展名称, for mytest support, 248 | Make sure that the comment is aligned: 249 | [ --with-扩展名称 Include xxx support]) 250 | 251 | if test "$PHP_扩展名称" != "no"; then 252 | PHP_NEW_EXTENSION(扩展名称, 源码文件列表, $ext_shared,, -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1) 253 | fi 254 | ``` 255 | PHP在acinclude.m4中基于autoconf/automake的宏封装了很多可以直接使用的宏,下面介绍几个比较常用的宏: 256 | 257 | __(1)PHP_ARG_WITH(arg_name,check message,help info):__ 定义一个`--with-feature[=arg]`这样的编译参数,调用的是autoconf的AC_ARG_WITH,这个宏有5个参数,常用的是前三个,分别表示:参数名、执行./configure是展示信息、执行--help时展示信息,第4个参数为默认值,如果不定义默认为"no",通过这个宏定义的参数可以在config.m4中通过`$PHP_参数名(大写)`访问,比如: 258 | ```sh 259 | PHP_ARG_WITH(aaa, aaa-configure, help aa) 260 | 261 | #后面通过$PHP_AAA就可以读取到--with-aaa=xxx设置的值了 262 | ``` 263 | __(2)PHP_ARG_ENABLE(arg_name,check message,help info):__ 定义一个`--enable-feature[=arg]`或`--disable-feature`参数,`--disable-feature`等价于`--enable-feature=no`,这个宏与PHP_ARG_WITH类似,通常情况下如果配置的参数需要额外的arg值会使用PHP_ARG_WITH,而如果不需要arg值,只用于开关配置则会使用PHP_ARG_ENABLE。 264 | 265 | __(3)AC_MSG_CHECKING()/AC_MSG_RESULT()/AC_MSG_ERROR():__ ./configure时输出结果,其中error将会中断configure执行。 266 | 267 | __(4)AC_DEFINE(variable, value, [description]):__ 定义一个宏,比如:`AC_DEFINE(IS_DEBUG, 1, [])`,执行autoheader时将在头文件中生成:`#define IS_DEBUG 1`。 268 | 269 | __(5)PHP_ADD_INCLUDE(path):__ 添加include路径,即:`gcc -Iinclude_dir`,`#include "file";`将先在通过-I指定的目录下查找,扩展引用了外部库或者扩展下分了多个目录的情况下会用到这个宏。 270 | 271 | __(6)PHP_CHECK_LIBRARY(library, function [, action-found [, action-not-found [, extra-libs]]]):__ 检查依赖的库中是否存在需要的function,action-found为存在时执行的动作,action-not-found为不存在时执行的动作,比如扩展里使用到线程pthread,检查pthread_create(),如果没找到则终止./configure执行: 272 | ```sh 273 | PHP_CHECK_LIBRARY(pthread, pthread_create, [], [ 274 | AC_MSG_ERROR([not find pthread_create() in lib pthread]) 275 | ]) 276 | ``` 277 | __(7)AC_CHECK_FUNC(function, [action-if-found], [action-if-not-found]):__ 检查函数是否存在。 278 | __(8)PHP_ADD_LIBRARY_WITH_PATH($LIBNAME, $XXX_DIR/$PHP_LIBDIR, XXX_SHARED_LIBADD):__ 添加链接库。 279 | 280 | __(9)PHP_NEW_EXTENSION(extname, sources [, shared [, sapi_class [, extra-cflags [, cxx [, zend_ext]]]]]):__ 注册一个扩展,添加扩展源文件,确定此扩展是动态库还是静态库,每个扩展的config.m4中都需要通过这个宏完成扩展的编译配置。 281 | 282 | 更多autoconf及PHP封装的宏大家可以在用到的时候再自行检索,同时ext目录下有大量的示例可供参考。 283 | -------------------------------------------------------------------------------- /7/hook.md: -------------------------------------------------------------------------------- 1 | ## 7.4 钩子函数 2 | PHP为扩展提供了5个钩子函数,PHP执行到不同阶段时回调各个扩展定义的钩子函数,扩展可以通过这些钩子函数介入到PHP生命周期的不同阶段中去,这些钩子函数的定义非常简单,PHP提供了对应的宏,定义完成后只需要设置`zend_module_entry`对应的函数指针即可。 3 | 4 | 前面已经介绍过PHP生命周期的几个阶段,这几个钩子函数执行的先后顺序:module startup -> request startup -> 编译、执行 -> request shutdown -> post deactivate -> module shutdown。 5 | 6 | ### 7.4.1 module_startup_func 7 | 这个函数在PHP模块初始化阶段执行,通常情况下,此过程只会在SAPI启动后执行一次。这个阶段可以进行内部类的注册,如果你的扩展提供了类就可以在此函数中完成注册;除了类还可以在此函数中注册扩展定义的常量;另外,扩展可以在此阶段覆盖PHP编译、执行的两个函数指针:zend_compile_file、zend_execute_ex,从而可以接管PHP的编译、执行,opcache的实现原理就是替换了zend_compile_file,从而使得PHP编译时调用的是opcache自己定义的编译函数,对编译后的结果进行缓存。 8 | 9 | 此钩子函数通过`PHP_MINIT_FUNCTION()`或`ZEND_MINIT_FUNCTION()`宏完成定义: 10 | ```c 11 | PHP_MINIT_FUNCTION(extension_name) 12 | { 13 | ... 14 | } 15 | ``` 16 | 展开后: 17 | ```c 18 | zm_startup_extension_name(int type, int module_number) 19 | { 20 | ... 21 | } 22 | ``` 23 | 最后通过`PHP_MINIT()`或`ZEND_MINIT()`宏将zend_module_entry的module_startup_func设置为上面定义的函数。 24 | ```c 25 | #define PHP_MINIT ZEND_MODULE_STARTUP_N 26 | #define ZEND_MINIT ZEND_MODULE_STARTUP_N 27 | 28 | #define ZEND_MODULE_STARTUP_N(module) zm_startup_##module 29 | ``` 30 | ### 7.4.2 request_startup_func 31 | 此函数在编译、执行之前回调,fpm模式下每一个http请求就是一个request,脚本执行前将首先执行这个函数。如果你的扩展需要针对每一个请求进行处理则可以设置这个函数,如:对请求进行filter、根据请求ip获取所在城市、对请求/返回数据加解密等。此函数通过`PHP_RINIT_FUNCTION()`或`ZEND_RINIT_FUNCTION()`宏定义: 32 | ```c 33 | PHP_RINIT_FUNCTION(extension_name) 34 | { 35 | ... 36 | } 37 | ``` 38 | 展开后: 39 | ```c 40 | zm_activate_extension_name(int type, int module_number) 41 | { 42 | ... 43 | } 44 | ``` 45 | 获取函数地址的宏:`PHP_RINIT()`或`ZEND_RINIT()`: 46 | ```c 47 | #define PHP_RINIT ZEND_MODULE_ACTIVATE_N 48 | #define ZEND_RINIT ZEND_MODULE_ACTIVATE_N 49 | 50 | #define ZEND_MODULE_ACTIVATE_N(module) zm_activate_##module 51 | ``` 52 | ### 7.4.3 request_shutdown_func 53 | 此函数在请求结束时被调用,通过`PHP_RSHUTDOWN_FUNCTION()`或`ZEND_RSHUTDOWN_FUNCTION()`宏定义: 54 | ```c 55 | PHP_RSHUTDOWN_FUNCTION(extension_name) 56 | { 57 | ... 58 | } 59 | ``` 60 | 函数地址通过`PHP_RSHUTDOWN()`或`ZEND_RSHUTDOWN()`获取: 61 | ```c 62 | #define PHP_RSHUTDOWN ZEND_MODULE_DEACTIVATE_N 63 | #define ZEND_RSHUTDOWN ZEND_MODULE_DEACTIVATE_N 64 | 65 | #define ZEND_MODULE_DEACTIVATE_N(module) zm_deactivate_##module 66 | ``` 67 | ### 7.4.4 post_deactivate_func 68 | 这个函数比较特殊,一般很少会用到,实际它也是在请求结束之后调用的,它比request_shutdown_func更晚执行: 69 | ```c 70 | void php_request_shutdown(void *dummy) 71 | { 72 | ... 73 | //调用各扩展的request_shutdown_func 74 | if (PG(modules_activated)) { 75 | zend_deactivate_modules(); 76 | } 77 | //关闭输出:发送http header 78 | php_output_deactivate(); 79 | 80 | //释放超全局变量:$_GET、$_POST... 81 | ... 82 | //关闭编译器、执行器 83 | zend_deactivate(); 84 | 85 | //调用每个扩展的post_deactivate_func 86 | zend_post_deactivate_modules(); 87 | ... 88 | } 89 | ``` 90 | 从上面的执行顺序可以看出,request_shutdown_func、post_deactivate_func是先后执行的,此函数通过`ZEND_MODULE_POST_ZEND_DEACTIVATE_D()`宏定义,`ZEND_MODULE_POST_ZEND_DEACTIVATE_N()`获取函数地址: 91 | ```c 92 | #define ZEND_MINIT ZEND_MODULE_STARTUP_N 93 | #define ZEND_MODULE_POST_ZEND_DEACTIVATE_N(module) zm_post_zend_deactivate_##module 94 | ``` 95 | ### 7.4.5 module_shutdown_func 96 | 模块关闭阶段回调的函数,与module_startup_func对应,此阶段主要可以进行一些资源的清理,通过`PHP_MSHUTDOWN_FUNCTION()`或`ZEND_MSHUTDOWN_FUNCTION()`定义: 97 | ```c 98 | PHP_MSHUTDOWN_FUNCTION(extension_name) 99 | { 100 | ... 101 | } 102 | ``` 103 | 通过`PHP_MSHUTDOWN()`或`ZEND_MSHUTDOWN()`获取函数地址: 104 | ```c 105 | #define PHP_MSHUTDOWN ZEND_MODULE_SHUTDOWN_N 106 | #define ZEND_MSHUTDOWN ZEND_MODULE_SHUTDOWN_N 107 | 108 | #define ZEND_MODULE_SHUTDOWN_N(module) zm_shutdown_##module 109 | ``` 110 | 7.4.6 小节 111 | 上面详细介绍了各个阶段定义的钩子函数的格式,使用gdb调试扩展时可以根据展开后实际的函数名称设置断点。这些钩子实际已经为扩展构造了一个整体的框架,通过这几个钩子扩展已经能实现很多功能了,后面我们介绍的很多内容都是在这几个函数中完成的,比如内部类的注册、常量注册、资源注册等。如果扩展名称为mytest,则最终定义的扩展: 112 | ```c 113 | PHP_MINIT_FUNCTION(mytest) 114 | { 115 | ... 116 | } 117 | 118 | PHP_RINIT_FUNCTION(mytest) 119 | { 120 | ... 121 | } 122 | 123 | PHP_RSHUTDOWN_FUNCTION(mytest) 124 | { 125 | ... 126 | } 127 | 128 | PHP_MSHUTDOWN_FUNCTION(mytest) 129 | { 130 | ... 131 | } 132 | 133 | zend_module_entry mytest_module_entry = { 134 | STANDARD_MODULE_HEADER, 135 | "mytest", 136 | NULL, //mytest_functions, 137 | PHP_MINIT(mytest), 138 | PHP_MSHUTDOWN(mytest), 139 | PHP_RINIT(mytest), 140 | PHP_RSHUTDOWN(mytest), 141 | NULL, //PHP_MINFO(mytest), 142 | "1.0.0", 143 | STANDARD_MODULE_PROPERTIES 144 | }; 145 | 146 | ZEND_GET_MODULE(mytest) 147 | ``` 148 | 149 | -------------------------------------------------------------------------------- /7/implement.md: -------------------------------------------------------------------------------- 1 | ## 7.2 扩展的实现原理 2 | PHP中扩展通过`zend_module_entry`这个结构来表示,此结构定义了扩展的全部信息:扩展名、扩展版本、扩展提供的函数列表以及PHP四个执行阶段的hook函数等,每一个扩展都需要定义一个此结构的变量,而且这个变量的名称格式必须是:`{module_name}_module_entry`,内核正是通过这个结构获取到扩展提供的功能的。 3 | 4 | 扩展可以在编译PHP时一起编译(静态编译),也可以单独编译为动态库,动态库需要加入到php.ini配置中去,然后在`php_module_startup()`阶段把这些动态库加载到PHP中: 5 | ```c 6 | int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_modules, uint num_additional_modules) 7 | { 8 | ... 9 | //根据php.ini注册扩展 10 | php_ini_register_extensions(); 11 | zend_startup_modules(); 12 | 13 | zend_startup_extensions(); 14 | ... 15 | } 16 | ``` 17 | 动态库就是在`php_ini_register_extensions()`这个函数中完成的注册: 18 | ```c 19 | //main/php_ini.c 20 | void php_ini_register_extensions(void) 21 | { 22 | //注册zend扩展 23 | zend_llist_apply(&extension_lists.engine, php_load_zend_extension_cb); 24 | //注册php扩展 25 | zend_llist_apply(&extension_lists.functions, php_load_php_extension_cb); 26 | 27 | zend_llist_destroy(&extension_lists.engine); 28 | zend_llist_destroy(&extension_lists.functions); 29 | } 30 | ``` 31 | extension_lists是一个链表,保存着根据`php.ini`中定义的`extension=xxx.so`取到的全部扩展名称,其中engine是zend扩展,functions为php扩展,依次遍历这两个数组然后调用`php_load_php_extension_cb()`或`php_load_zend_extension_cb()`进行各个扩展的加载: 32 | ```c 33 | static void php_load_php_extension_cb(void *arg) 34 | { 35 | #ifdef HAVE_LIBDL 36 | php_load_extension(*((char **) arg), MODULE_PERSISTENT, 0); 37 | #endif 38 | } 39 | ``` 40 | `HAVE_LIBDL`这个宏根据`dlopen()`函数是否存在设置的: 41 | ```sh 42 | #Zend/Zend.m4 43 | AC_DEFUN([LIBZEND_LIBDL_CHECKS],[ 44 | AC_CHECK_LIB(dl, dlopen, [LIBS="-ldl $LIBS"]) 45 | AC_CHECK_FUNC(dlopen,[AC_DEFINE(HAVE_LIBDL, 1,[ ])]) 46 | ]) 47 | ``` 48 | 接着就是最关键的操作了,`php_load_extension()`: 49 | ```c 50 | //ext/standard/dl.c 51 | PHPAPI int php_load_extension(char *filename, int type, int start_now) 52 | { 53 | void *handle; 54 | char *libpath; 55 | zend_module_entry *module_entry; 56 | zend_module_entry *(*get_module)(void); 57 | ... 58 | //调用dlopen打开指定的动态连接库文件:xx.so 59 | handle = DL_LOAD(libpath); 60 | ... 61 | //调用dlsym获取get_module的函数指针 62 | get_module = (zend_module_entry *(*)(void)) DL_FETCH_SYMBOL(handle, "get_module"); 63 | ... 64 | //调用扩展的get_module()函数 65 | module_entry = get_module(); 66 | ... 67 | //检查扩展使用的zend api是否与当前php版本一致 68 | if (module_entry->zend_api != ZEND_MODULE_API_NO) { 69 | DL_UNLOAD(handle); 70 | return FAILURE; 71 | } 72 | ... 73 | module_entry->type = type; 74 | //为扩展编号 75 | module_entry->module_number = zend_next_free_module(); 76 | module_entry->handle = handle; 77 | 78 | if ((module_entry = zend_register_module_ex(module_entry)) == NULL) { 79 | DL_UNLOAD(handle); 80 | return FAILURE; 81 | } 82 | ... 83 | } 84 | ``` 85 | `DL_LOAD()`、`DL_FETCH_SYMBOL()`这两个宏在linux下展开后就是:dlopen()、dlsym(),所以上面过程的实现就比较直观了: 86 | 87 | * (1)dlopen()打开so库文件; 88 | * (2)dlsym()获取动态库中`get_module()`函数的地址,`get_module()`是每个扩展都必须提供的一个接口,用于返回扩展`zend_module_entry`结构的地址; 89 | * (3)调用扩展的`get_module()`,获取扩展的`zend_module_entry`结构; 90 | * (4)zend api版本号检查,比如php7的扩展在php5下是无法使用的; 91 | * (5)注册扩展,将扩展添加到`module_registry`中,这是一个全局HashTable,用于全部扩展的zend_module_entry结构; 92 | * (6)如果扩展提供了内部函数则将这些函数注册到EG(function_table)中。 93 | 94 | 完成扩展的注册后,PHP将在不同的执行阶段依次调用每个扩展注册的当前阶段的hook函数。 95 | -------------------------------------------------------------------------------- /7/intro.md: -------------------------------------------------------------------------------- 1 | ## 7.1 概述 2 | 3 | 扩展是PHP的重要组成部分,它是PHP提供给开发者用于扩展PHP语言功能的主要方式。开发者可以用C/C++定义自己的功能,通过扩展嵌入到PHP中,灵活的扩展能力使得PHP拥有了大量、丰富的第三方组件,这些扩展很好的补充了PHP的功能、特性,使得PHP在web开发中得以大展身手。ext目录下有一个standard扩展,这个扩展提供了大量被大家所熟知的PHP函数:sleep()、usleep()、htmlspecialchars()、md5()、strtoupper()、substr()、array_merge()等等。 4 | 5 | C语言是PHP之母,作为世界上非常优秀的一门语言,自它诞生至今,C语言早就了大量优秀、知名的项目:Linux、Nginx、MySQL、PHP、Redis、Memcached等等,感谢里奇带给这个世界如此伟大的一份礼物。C语言的优秀也折射到PHP身上,但是PHP内核提供的功能终究有限,如果你发现PHP在某些方面已经满足不了你的需求了,那么不妨试试扩展。 6 | 7 | 常见的,扩展可以在以下几个方面有所作为: 8 | * __介入PHP的编译、执行阶段:__ 可以介入PHP框架执行的那5个阶段,比如opcache,就是重定义了编译函数 9 | * __提供内部函数:__ 可以定义内部函数扩充PHP的函数功能,比如array、date等操作 10 | * __提供内部类__ 11 | * __实现RPC客户端:__ 实现与外部服务的交互,比如redis、mysql等 12 | * __提升执行性能:__ PHP是解析型语言,在性能方面远不及C语言,可以将耗cpu的操作以C语言代替 13 | * ...... 14 | 15 | 当然扩展也不是万能,它只允许我们在PHP提供的框架之上进行一些特定的处理,同时限于SAPI的差异,扩展也必须要考虑到不同SAPI的实现特点。 16 | 17 | PHP中的扩展分为两类:PHP扩展、Zend扩展,对内核而言这两个分别称之为:模块(module)、扩展(extension),本章主要介绍是PHP扩展,也就是模块。 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP7内核剖析 2 | ```` 3 | 原创内容,转载请注明出处~ 4 | 5 | 代码版本:php-7.0.12 6 | ```` 7 | ## 反馈 8 | [交流&吐槽](https://github.com/pangudashu/php7-internal/issues/3) [错误反馈](https://github.com/pangudashu/php7-internal/issues/2) 9 | 10 | ## 纸质版 11 |
12 | 13 |
14 | 15 | [京东](https://item.jd.com/12267210.html) 16 | [当当](http://product.dangdang.com/25185400.html) 17 | 18 | ## 目录: 19 | * 第1章 PHP基本架构 20 | * 1.1 PHP简介 21 | * 1.2 PHP7的改进 22 | * [1.3 FPM](1/fpm.md) 23 | * [1.3.1 概述](1/fpm.md) 24 | * [1.3.2 基本实现](1/fpm.md) 25 | * [1.3.3 FPM的初始化](1/fpm.md) 26 | * [1.3.4 请求处理](1/fpm.md) 27 | * [1.3.5 进程管理](1/fpm.md) 28 | * [1.4 PHP执行的几个阶段](1/base_process.md) 29 | * 第2章 变量 30 | * [2.1 变量的内部实现](2/zval.md) 31 | * [2.2 数组](2/zend_ht.md) 32 | * [2.3 静态变量](2/static_var.md) 33 | * [2.4 全局变量](2/global_var.md) 34 | * [2.5 常量](2/zend_constant.md) 35 | * 第3章 Zend虚拟机 36 | * [3.1 PHP代码的编译](3/zend_compile.md) 37 | * [3.1.1 词法解析、语法解析](3/zend_compile_parse.md) 38 | * [3.1.2 抽象语法树编译流程](3/zend_compile_opcode.md) 39 | * [3.2 函数实现](3/function_implement.md) 40 | * [3.2.1 内部函数](3/function_implement.md) 41 | * 3.2.2 用户函数的实现 42 | * [3.3 Zend引擎执行流程](3/zend_executor.md) 43 | * 3.3.1 基本结构 44 | * 3.3.2 执行流程 45 | * 3.3.3 函数的执行流程 46 | * [3.3.4 全局execute_data和opline](3/zend_global_register.md) 47 | * 3.4 面向对象实现 48 | * [3.4.1 类](3/zend_class.md) 49 | * [3.4.2 对象](3/zend_object.md) 50 | * [3.4.3 继承](3/zend_extends.md) 51 | * [3.4.4 动态属性](3/zend_prop.md) 52 | * [3.4.5 魔术方法](3/zend_magic_method.md) 53 | * [3.4.6 类的自动加载](3/zend_autoload.md) 54 | * [3.5 运行时缓存](3/zend_runtime_cache.md) 55 | * 3.6 Opcache 56 | * 3.6.1 opcode缓存 57 | * 3.6.2 opcode优化 58 | * 3.6.3 JIT 59 | * 第4章 PHP基础语法实现 60 | * [4.1 类型转换](4/type.md) 61 | * [4.2 选择结构](4/if.md) 62 | * [4.3 循环结构](4/loop.md) 63 | * [4.4 中断及跳转](4/break.md) 64 | * [4.5 include/require](4/include.md) 65 | * [4.6 异常处理](4/exception.md) 66 | * 第5章 内存管理 67 | * [5.1 Zend内存池](5/zend_alloc.md) 68 | * [5.2 垃圾回收](5/gc.md) 69 | * 第6章 线程安全 70 | * [6.1 什么是线程安全](6/ts.md) 71 | * [6.2 线程安全资源管理器](6/ts.md) 72 | * 第7章 扩展开发 73 | * [7.1 概述](7/intro.md) 74 | * [7.2 扩展的实现原理](7/implement.md) 75 | * [7.3 扩展的构成及编译](7/extension_intro.md) 76 | * [7.3.1 扩展的构成](7/extension_intro.md) 77 | * [7.3.2 编译工具](7/extension_intro.md) 78 | * [7.3.3 编写扩展的基本步骤](7/extension_intro.md) 79 | * [7.3.4 config.m4](7/extension_intro.md) 80 | * [7.4 钩子函数](7/hook.md) 81 | * [7.5 运行时配置](7/conf.md) 82 | * [7.5.1 全局变量](7/conf.md) 83 | * [7.5.2 ini配置](7/conf.md) 84 | * [7.6 函数](7/func.md) 85 | * 7.6.1 内部函数注册 86 | * 7.6.2 函数参数解析 87 | * 7.6.3 引用传参 88 | * 7.6.4 函数返回值 89 | * 7.6.5 函数调用 90 | * [7.7 zval的操作](7/var.md) 91 | * [7.7.1 新生成各类型zval](7/var.md) 92 | * [7.7.2 获取zval的值及类型](7/var.md) 93 | * [7.7.3 类型转换](7/var.md) 94 | * [7.7.4 引用计数](7/var.md) 95 | * [7.7.5 字符串操作](7/var.md) 96 | * [7.7.6 数组操作](7/var.md) 97 | * [7.8 常量](7/constant.md) 98 | * 7.9 面向对象 99 | * 7.9.1 内部类注册 100 | * 7.9.2 定义成员属性 101 | * 7.9.3 定义成员方法 102 | * 7.9.4 定义常量 103 | * 7.9.5 类的实例化 104 | * 7.10 资源类型 105 | * 7.11 经典扩展解析 106 | * 7.8.1 Yaf 107 | * 7.8.2 Redis 108 | * 第8章 命名空间 109 | * [8.1 概述](8/namespace.md) 110 | * [8.2 命名空间的定义](8/namespace.md) 111 | * [8.2.1 定义语法](8/namespace.md) 112 | * [8.2.2 内部实现](8/namespace.md) 113 | * [8.3 命名空间的使用](8/namespace.md) 114 | * [8.3.1 基本用法](8/namespace.md) 115 | * [8.3.2 use导入](8/namespace.md) 116 | * [8.3.3 动态用法](8/namespace.md) 117 | 118 | ## 实现PHP新特性 119 | * [1、break/continue按标签中断语法实现](try/break.md) 120 | * 2、defer语法 121 | * 3、协程 122 | * 3.1 协程的原理 123 | * 3.2 上下文切换 124 | 125 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # PHP7-internal 2 | 3 | ## 目录 4 | 5 | * 第1章 PHP基本架构 6 | * 1.1 PHP简介 7 | * 1.2 PHP7的改进 8 | * [1.3 FPM](1/fpm.md) 9 | * [1.3.1 概述](1/fpm.md) 10 | * [1.3.2 基本实现](1/fpm.md) 11 | * [1.3.3 FPM的初始化](1/fpm.md) 12 | * [1.3.4 请求处理](1/fpm.md) 13 | * [1.3.5 进程管理](1/fpm.md) 14 | * [1.4 PHP执行的几个阶段](1/base_process.md) 15 | * 第2章 变量 16 | * [2.1 变量的内部实现](2/zval.md) 17 | * [2.2 数组](2/zend_ht.md) 18 | * [2.3 静态变量](2/static_var.md) 19 | * [2.4 全局变量](2/global_var.md) 20 | * [2.5 常量](2/zend_constant.md) 21 | * 第3章 Zend虚拟机 22 | * [3.1 PHP代码的编译](3/zend_compile.md) 23 | * [3.1.1 词法解析、语法解析](3/zend_compile_parse.md) 24 | * [3.1.2 抽象语法树编译流程](3/zend_compile_opcode.md) 25 | * [3.2 函数实现](3/function_implement.md) 26 | * [3.2.1 内部函数](3/function_implement.md) 27 | * 3.2.2 用户函数的实现 28 | * [3.3 Zend引擎执行流程](3/zend_executor.md) 29 | * 3.3.1 基本结构 30 | * 3.3.2 执行流程 31 | * 3.3.3 函数的执行流程 32 | * [3.3.4 全局execute_data和opline](3/zend_global_register.md) 33 | * 3.4 面向对象实现 34 | * [3.4.1 类](3/zend_class.md) 35 | * [3.4.2 对象](3/zend_object.md) 36 | * [3.4.3 继承](3/zend_extends.md) 37 | * [3.4.4 动态属性](3/zend_prop.md) 38 | * [3.4.5 魔术方法](3/zend_magic_method.md) 39 | * [3.4.6 类的自动加载](3/zend_autoload.md) 40 | * [3.5 运行时缓存](3/zend_runtime_cache.md) 41 | * 3.6 Opcache 42 | * 3.6.1 opcode缓存 43 | * 3.6.2 opcode优化 44 | * 3.6.3 JIT 45 | * 第4章 PHP基础语法实现 46 | * [4.1 类型转换](4/type.md) 47 | * [4.2 选择结构](4/if.md) 48 | * [4.3 循环结构](4/loop.md) 49 | * [4.4 中断及跳转](4/break.md) 50 | * [4.5 include/require](4/include.md) 51 | * [4.6 异常处理](4/exception.md) 52 | * 第5章 内存管理 53 | * [5.1 Zend内存池](5/zend_alloc.md) 54 | * [5.2 垃圾回收](5/gc.md) 55 | * 第6章 线程安全 56 | * [6.1 什么是线程安全](6/ts.md) 57 | * [6.2 线程安全资源管理器](6/ts.md) 58 | * 第7章 扩展开发 59 | * [7.1 概述](7/intro.md) 60 | * [7.2 扩展的实现原理](7/implement.md) 61 | * [7.3 扩展的构成及编译](7/extension_intro.md) 62 | * [7.3.1 扩展的构成](7/extension_intro.md) 63 | * [7.3.2 编译工具](7/extension_intro.md) 64 | * [7.3.3 编写扩展的基本步骤](7/extension_intro.md) 65 | * [7.3.4 config.m4](7/extension_intro.md) 66 | * [7.4 钩子函数](7/hook.md) 67 | * [7.5 运行时配置](7/conf.md) 68 | * [7.5.1 全局变量](7/conf.md) 69 | * [7.5.2 ini配置](7/conf.md) 70 | * [7.6 函数](7/func.md) 71 | * 7.6.1 内部函数注册 72 | * 7.6.2 函数参数解析 73 | * 7.6.3 引用传参 74 | * 7.6.4 函数返回值 75 | * 7.6.5 函数调用 76 | * [7.7 zval的操作](7/var.md) 77 | * [7.7.1 新生成各类型zval](7/var.md) 78 | * [7.7.2 获取zval的值及类型](7/var.md) 79 | * [7.7.3 类型转换](7/var.md) 80 | * [7.7.4 引用计数](7/var.md) 81 | * [7.7.5 字符串操作](7/var.md) 82 | * [7.7.6 数组操作](7/var.md) 83 | * [7.8 常量](7/constant.md) 84 | * 7.9 面向对象 85 | * 7.9.1 内部类注册 86 | * 7.9.2 定义成员属性 87 | * 7.9.3 定义成员方法 88 | * 7.9.4 定义常量 89 | * 7.9.5 类的实例化 90 | * 7.10 资源类型 91 | * 7.11 经典扩展解析 92 | * 7.8.1 Yaf 93 | * 7.8.2 Redis 94 | * 第8章 命名空间 95 | * [8.1 概述](8/namespace.md) 96 | * [8.2 命名空间的定义](8/namespace.md) 97 | * [8.2.1 定义语法](8/namespace.md) 98 | * [8.2.2 内部实现](8/namespace.md) 99 | * [8.3 命名空间的使用](8/namespace.md) 100 | * [8.3.1 基本用法](8/namespace.md) 101 | * [8.3.2 use导入](8/namespace.md) 102 | * [8.3.3 动态用法](8/namespace.md) 103 | 104 | ---- 105 | 106 | ## 附录 107 | * [附录1:break/continue按标签中断语法实现](try/break.md) 108 | * 附录2:defer推迟函数调用语法的实现 -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "title" : "PHP7内核剖析", 3 | "author" : "pangudashu", 4 | "description" : "PHP7内核剖析,基于PHP版本:php-7.0.12", 5 | "language" : "zh-hans", 6 | "gitbook" : ">=3.0.0" 7 | } -------------------------------------------------------------------------------- /img/EG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/EG.png -------------------------------------------------------------------------------- /img/align.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/align.png -------------------------------------------------------------------------------- /img/alloc_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/alloc_all.png -------------------------------------------------------------------------------- /img/ast_break_div.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/ast_break_div.png -------------------------------------------------------------------------------- /img/ast_class.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/ast_class.png -------------------------------------------------------------------------------- /img/ast_fetch_class.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/ast_fetch_class.png -------------------------------------------------------------------------------- /img/ast_for.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/ast_for.png -------------------------------------------------------------------------------- /img/ast_foreach.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/ast_foreach.png -------------------------------------------------------------------------------- /img/ast_function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/ast_function.png -------------------------------------------------------------------------------- /img/ast_function_op.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/ast_function_op.png -------------------------------------------------------------------------------- /img/ast_if.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/ast_if.png -------------------------------------------------------------------------------- /img/ast_namespace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/ast_namespace.png -------------------------------------------------------------------------------- /img/ast_switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/ast_switch.png -------------------------------------------------------------------------------- /img/ast_while.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/ast_while.png -------------------------------------------------------------------------------- /img/autoconf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/autoconf.png -------------------------------------------------------------------------------- /img/book.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/book.jpg -------------------------------------------------------------------------------- /img/break_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/break_run.png -------------------------------------------------------------------------------- /img/chunk_alloc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/chunk_alloc.png -------------------------------------------------------------------------------- /img/chunk_init.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/chunk_init.png -------------------------------------------------------------------------------- /img/defer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/defer.png -------------------------------------------------------------------------------- /img/defer_ast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/defer_ast.png -------------------------------------------------------------------------------- /img/defer_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/defer_call.png -------------------------------------------------------------------------------- /img/do_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/do_run.png -------------------------------------------------------------------------------- /img/exception_ast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/exception_ast.png -------------------------------------------------------------------------------- /img/exception_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/exception_run.png -------------------------------------------------------------------------------- /img/exception_run_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/exception_run_2.png -------------------------------------------------------------------------------- /img/executor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/executor.png -------------------------------------------------------------------------------- /img/fastcgi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/fastcgi.png -------------------------------------------------------------------------------- /img/for_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/for_run.png -------------------------------------------------------------------------------- /img/foreach_ref_struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/foreach_ref_struct.png -------------------------------------------------------------------------------- /img/foreach_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/foreach_run.png -------------------------------------------------------------------------------- /img/foreach_struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/foreach_struct.png -------------------------------------------------------------------------------- /img/free_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/free_map.png -------------------------------------------------------------------------------- /img/free_map_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/free_map_1.png -------------------------------------------------------------------------------- /img/free_slot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/free_slot.png -------------------------------------------------------------------------------- /img/func_exe_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/func_exe_call.png -------------------------------------------------------------------------------- /img/func_exe_eg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/func_exe_eg1.png -------------------------------------------------------------------------------- /img/func_exe_init.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/func_exe_init.png -------------------------------------------------------------------------------- /img/func_exe_send_var.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/func_exe_send_var.png -------------------------------------------------------------------------------- /img/func_exe_start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/func_exe_start.png -------------------------------------------------------------------------------- /img/function_dec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/function_dec.png -------------------------------------------------------------------------------- /img/gc_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/gc_1.png -------------------------------------------------------------------------------- /img/gc_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/gc_2.png -------------------------------------------------------------------------------- /img/if.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/if.png -------------------------------------------------------------------------------- /img/if_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/if_run.png -------------------------------------------------------------------------------- /img/include.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/include.png -------------------------------------------------------------------------------- /img/include_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/include_2.png -------------------------------------------------------------------------------- /img/include_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/include_3.png -------------------------------------------------------------------------------- /img/include_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/include_4.png -------------------------------------------------------------------------------- /img/include_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/include_5.png -------------------------------------------------------------------------------- /img/internal_func_param.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/internal_func_param.png -------------------------------------------------------------------------------- /img/loop_op.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/loop_op.png -------------------------------------------------------------------------------- /img/magic_function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/magic_function.png -------------------------------------------------------------------------------- /img/master_event_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/master_event_1.png -------------------------------------------------------------------------------- /img/my_wx2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/my_wx2.png -------------------------------------------------------------------------------- /img/namespace_com.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/namespace_com.png -------------------------------------------------------------------------------- /img/object_class_prop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/object_class_prop.png -------------------------------------------------------------------------------- /img/object_new_op.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/object_new_op.png -------------------------------------------------------------------------------- /img/oparray-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/oparray-1.png -------------------------------------------------------------------------------- /img/php.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/php.png -------------------------------------------------------------------------------- /img/php_function.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/php_function.jpg -------------------------------------------------------------------------------- /img/php_vs_c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/php_vs_c.png -------------------------------------------------------------------------------- /img/runtime_cache_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/runtime_cache_1.png -------------------------------------------------------------------------------- /img/switch_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/switch_run.png -------------------------------------------------------------------------------- /img/symbol_cv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/symbol_cv.png -------------------------------------------------------------------------------- /img/talk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/talk.png -------------------------------------------------------------------------------- /img/throw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/throw.png -------------------------------------------------------------------------------- /img/tsrm_tls_a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/tsrm_tls_a.png -------------------------------------------------------------------------------- /img/tsrm_tls_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/tsrm_tls_table.png -------------------------------------------------------------------------------- /img/var_T.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/var_T.png -------------------------------------------------------------------------------- /img/while_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/while_run.png -------------------------------------------------------------------------------- /img/worker_pool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/worker_pool.png -------------------------------------------------------------------------------- /img/worker_pool_struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/worker_pool_struct.png -------------------------------------------------------------------------------- /img/zend_ast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_ast.png -------------------------------------------------------------------------------- /img/zend_ast_class.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_ast_class.png -------------------------------------------------------------------------------- /img/zend_ast_echo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_ast_echo.png -------------------------------------------------------------------------------- /img/zend_ast_echo_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_ast_echo_p.png -------------------------------------------------------------------------------- /img/zend_class.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_class.png -------------------------------------------------------------------------------- /img/zend_class_function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_class_function.png -------------------------------------------------------------------------------- /img/zend_class_init.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_class_init.png -------------------------------------------------------------------------------- /img/zend_class_property.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_class_property.png -------------------------------------------------------------------------------- /img/zend_compile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_compile.png -------------------------------------------------------------------------------- /img/zend_compile2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_compile2.png -------------------------------------------------------------------------------- /img/zend_compile_process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_compile_process.png -------------------------------------------------------------------------------- /img/zend_dy_prop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_dy_prop.png -------------------------------------------------------------------------------- /img/zend_eg_class.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_eg_class.png -------------------------------------------------------------------------------- /img/zend_ex_op.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_ex_op.png -------------------------------------------------------------------------------- /img/zend_execute_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_execute_data.png -------------------------------------------------------------------------------- /img/zend_extends.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_extends.png -------------------------------------------------------------------------------- /img/zend_extends_merge_prop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_extends_merge_prop.png -------------------------------------------------------------------------------- /img/zend_gc_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_gc_1.png -------------------------------------------------------------------------------- /img/zend_gc_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_gc_2.png -------------------------------------------------------------------------------- /img/zend_global_ref.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_global_ref.png -------------------------------------------------------------------------------- /img/zend_global_var.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_global_var.png -------------------------------------------------------------------------------- /img/zend_hash_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_hash_1.png -------------------------------------------------------------------------------- /img/zend_heap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_heap.png -------------------------------------------------------------------------------- /img/zend_lookup_cv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_lookup_cv.png -------------------------------------------------------------------------------- /img/zend_op_array.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_op_array.png -------------------------------------------------------------------------------- /img/zend_op_array_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_op_array_2.png -------------------------------------------------------------------------------- /img/zend_op_array_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_op_array_3.png -------------------------------------------------------------------------------- /img/zend_parse_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_parse_1.png -------------------------------------------------------------------------------- /img/zend_parse_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_parse_2.png -------------------------------------------------------------------------------- /img/zend_property_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_property_info.png -------------------------------------------------------------------------------- /img/zend_ref.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_ref.png -------------------------------------------------------------------------------- /img/zend_static_ref.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_static_ref.png -------------------------------------------------------------------------------- /img/zend_vm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zend_vm.png -------------------------------------------------------------------------------- /img/zval_sep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangudashu/php7-internal/2a5f36fa3070460f6af3296caae9f872bac00e7f/img/zval_sep.png -------------------------------------------------------------------------------- /try/break.md: -------------------------------------------------------------------------------- 1 | # 附录1:break/continue按标签中断语法实现 2 | 3 | ## 1.1 背景 4 | 首先看下目前PHP中break/continue多层循环的情况: 5 | ```php 6 | //loop1 7 | while(...){ 8 | //loop2 9 | for(...){ 10 | //loop3 11 | foreach(...){ 12 | ... 13 | break 2; 14 | } 15 | 16 | } 17 | //loop2 end 18 | ... 19 | 20 | } 21 | ``` 22 | `break 2`表示要中断往上数两层也就是loop2这层循环,`break 2`之后将从loop2 end开始继续执行。PHP的break、continue只能根据数值中断对应的循环,当嵌套循环比较多的时候这种方式维护起来就变得很不方便,需要一层层的去数要中断的循环。 23 | 24 | 了解Go语言的读者应该知道在Go中可以按照标签中断,举个例子来看: 25 | ```go 26 | //test.go 27 | func main() { 28 | loop1: 29 | for i := 0; i < 2; i++ { 30 | fmt.Println("loop1") 31 | 32 | for j := 0; j < 5; j++ { 33 | fmt.Println(" loop2") 34 | if j == 2 { 35 | break loop1 36 | } 37 | } 38 | } 39 | } 40 | ``` 41 | `go run test.go`将输出: 42 | ``` 43 | loop1 44 | loop2 45 | loop2 46 | loop2 47 | ``` 48 | `break loop1`这种语法在PHP中是不支持的,接下来我们就对PHP进行改造,让PHP实现同样的功能。 49 | 50 | ## 1.2 实现 51 | 想让PHP支持类似Go语言那样的语法首先需要明确PHP中循环及中断语句的实现,关于这两部分内容前面《PHP基础语法实现》一章已经详细介绍过了,这里再简单概括下实现的关键点: 52 | * 不管是哪种循环结构,其编译时都生成了一个`zend_brk_cont_element`结构,此结构记录着这个循环break、continue要跳转的位置,以及嵌套的父层循环 53 | * break/continue编译时分为两个步骤:首先初步编译为临时opcode,此opcode记录着break/continue所在循环层以及要中断的层级(即:`break n`,默认n=1);然后在脚本全部编译完之后的pass_two()中,根据当前循环层及中断的层级n向上查找对应的循环层,最后根据查找到的要中断的循环`zend_brk_cont_element`结构得到对应的跳转位置,生成一条ZEND_JMP指令 54 | 55 | 仔细研究循环、中断的实现可以发现,这里面的关键就在于找到break/continue要中断的那层循环,嵌套循环之间是链表的结构,所以目前的查找就变得很容易了,直接从break/continue当前循环层向前移动n即可。 56 | 57 | 标签在内核中通过HashTable的结构保存(即:CG(context).labels),key就是标签名,标签会记录当前opcode的位置,我们要实现`break 标签`的语法需要根据标签取到循环,因此我们为标签赋予一种新的含义:循环标签,只有标签紧挨着循环的才认为是这种含义,比如: 58 | ```php 59 | loop1: 60 | for(...){ 61 | ... 62 | } 63 | ``` 64 | 标签与循环之间有其它表达式的则只能认为是普通标签: 65 | ```php 66 | loop1: 67 | $a = 123; 68 | for(...){ 69 | } 70 | ``` 71 | 既然要按照标签进行break、continue,那么很容易想到把中断的循环层级id保存到标签中,编译break/continue时先查找标签,再查找循环的`zend_brk_cont_element`即可,这样实现的话需要循环编译时将自己`zend_brk_cont_element`的存储位置保存到标签中,标签的结构需要修改,另外一个问题是标签编译不会生成任何opcode,循环结构无法直接根据上一条opcode判断它是不是 ***循环标签*** ,所以我们换一种方式实现,具体思路如下: 72 | 73 | * __(1)__ 循环结构开始编译前先编译一条空opcode(ZEND_NOP),用于标识这是一个循环,并把这个循环`zend_brk_cont_element`的存储位置记录在此opcode中 74 | * __(2)__ break编译时如果发现是一个标签,则从CG(context).labels)中取出标签结构,然后判断此标签的下一条opcode是否为ZEND_NOP,如果不是则说明这不是一个 ***>循环标签*** ,无法break/continue,如果是则取出循环结构 75 | * __(3)__ 得到循环结构之后的处理就比较简单了,但是此时还不能直接编译为ZEND_JMP,因为循环可能还未编译完成,break只能编译为临时opcode,这里可以把标签标记的循环存储位置记录在临时opcode中,然后在pass_two()中再重新获取,需要对pass_two()中的逻辑进行改动,为减少改动,这个地方转化一下实现方式:计算label标记的循环相对break所在循环的位置,也就是转为现有的`break n`,这样以来就无需对pass_two()进行改动了 76 | 77 | 接下来看下具体的实现,以for为例。 78 | 79 | __(1) 编译循环语句__ 80 | 81 | ```c 82 | void zend_compile_for(zend_ast *ast) 83 | { 84 | zend_ast *init_ast = ast->child[0]; 85 | zend_ast *cond_ast = ast->child[1]; 86 | zend_ast *loop_ast = ast->child[2]; 87 | zend_ast *stmt_ast = ast->child[3]; 88 | 89 | znode result; 90 | uint32_t opnum_start, opnum_jmp, opnum_loop; 91 | zend_op *mark_look_opline; 92 | 93 | //新增:创建一条空opcode,用于标识接下来是一个循环结构 94 | mark_look_opline = zend_emit_op(NULL, ZEND_NOP, NULL, NULL); 95 | 96 | zend_compile_expr_list(&result, init_ast); 97 | zend_do_free(&result); 98 | 99 | opnum_jmp = zend_emit_jump(0); 100 | 101 | zend_begin_loop(ZEND_NOP, NULL); 102 | 103 | //新增:保存当前循环的brk,同时为了防止与其它ZEND_NOP混淆,把op1标为-1 104 | mark_look_opline->op1.var = -1; 105 | mark_look_opline->extended_value = CG(context).current_brk_cont; 106 | ... 107 | } 108 | ``` 109 | 110 | __(2) 编译中断语句__ 111 | 112 | 首先明确一点:`break label`将被编译为以下语法结构: 113 | 114 | ![](../img/ast_break_div.png) 115 | 116 | `ZEND_AST_BREAK`只有一个子节点,如果是数值那么这个子节点类型为`ZEND_AST_ZVAL`,如果是标签则类型是`ZEND_AST_CONST`,`ZEND_AST_CONST`也有一个类型为`ZEND_AST_ZVAL`子节点。下面看下break/continue修改后的编译逻辑: 117 | ```c 118 | void zend_compile_break_continue(zend_ast *ast) 119 | { 120 | zend_ast *depth_ast = ast->child[0]; 121 | 122 | zend_op *opline; 123 | int depth; 124 | 125 | ZEND_ASSERT(ast->kind == ZEND_AST_BREAK || ast->kind == ZEND_AST_CONTINUE); 126 | 127 | if (CG(context).current_brk_cont == -1) { 128 | zend_error_noreturn(E_COMPILE_ERROR, "'%s' not in the 'loop' or 'switch' context", 129 | ast->kind == ZEND_AST_BREAK ? "break" : "continue"); 130 | } 131 | 132 | if (depth_ast) { 133 | 134 | switch(depth_ast->kind){ 135 | case ZEND_AST_ZVAL: //break 数值; 136 | { 137 | zval *depth_zv; 138 | 139 | depth_zv = zend_ast_get_zval(depth_ast); 140 | if (Z_TYPE_P(depth_zv) != IS_LONG || Z_LVAL_P(depth_zv) < 1) { 141 | zend_error_noreturn(E_COMPILE_ERROR, "'%s' operator accepts only positive numbers", 142 | ast->kind == ZEND_AST_BREAK ? "break" : "continue"); 143 | } 144 | 145 | depth = Z_LVAL_P(depth_zv); 146 | break; 147 | } 148 | case ZEND_AST_CONST://break 标签; 149 | { 150 | //获取label名称 151 | zend_string *label = zend_ast_get_str(depth_ast->child[0]); 152 | //根据label获取标记的循环,以及相对break所在循环的位置 153 | depth = zend_loop_get_depth_by_label(label); 154 | if(depth > 0){ 155 | goto SET_OP; 156 | } 157 | break; 158 | } 159 | default: 160 | zend_error_noreturn(E_COMPILE_ERROR, "'%s' operator with non-constant operand " 161 | "is no longer supported", ast->kind == ZEND_AST_BREAK ? "break" : "continue"); 162 | } 163 | } else { 164 | depth = 1; 165 | } 166 | 167 | if (!zend_handle_loops_and_finally_ex(depth)) { 168 | zend_error_noreturn(E_COMPILE_ERROR, "Cannot '%s' %d level%s", 169 | ast->kind == ZEND_AST_BREAK ? "break" : "continue", 170 | depth, depth == 1 ? "" : "s"); 171 | } 172 | 173 | SET_OP: 174 | opline = zend_emit_op(NULL, ast->kind == ZEND_AST_BREAK ? ZEND_BRK : ZEND_CONT, NULL, NULL); 175 | opline->op1.num = CG(context).current_brk_cont; 176 | opline->op2.num = depth; 177 | } 178 | ``` 179 | `zend_loop_get_depth_by_label()`这个函数用来计算标签标记的循环相对break/continue所在循环的层级: 180 | ```c 181 | int zend_loop_get_depth_by_label(zend_string *label_name) 182 | { 183 | zval *label_zv; 184 | zend_label *label; 185 | zend_op *next_opline; 186 | 187 | if(UNEXPECTED(CG(context).labels == NULL)){ 188 | zend_error_noreturn(E_COMPILE_ERROR, "can't find label:'%s' or it not mark a loop", ZSTR_VAL(label_name)); 189 | } 190 | 191 | // 1) 查找label 192 | label_zv = zend_hash_find(CG(context).labels, label_name); 193 | if(UNEXPECTED(label_zv == NULL)){ 194 | zend_error_noreturn(E_COMPILE_ERROR, "can't find label:'%s' or it not mark a loop", ZSTR_VAL(label_name)); 195 | } 196 | 197 | label = (zend_label *)Z_PTR_P(label_zv); 198 | 199 | // 2) 获取label下一条opcode 200 | next_opline = &(CG(active_op_array)->opcodes[label->opline_num]); 201 | if(UNEXPECTED(next_opline == NULL)){ 202 | zend_error_noreturn(E_COMPILE_ERROR, "can't find label:'%s' or it not mark a loop", ZSTR_VAL(label_name)); 203 | } 204 | 205 | int label_brk_offset, curr_brk_offset; //标签标识的循环、break当前所在循环 206 | int depth = 0; //break当前循环至标签循环的层级 207 | zend_brk_cont_element *brk_cont_element; 208 | 209 | if(next_opline->opcode == ZEND_NOP && next_opline->op1.var == -1){ 210 | label_brk_offset = next_opline->extended_value; 211 | curr_brk_offset = CG(context).current_brk_cont; 212 | 213 | brk_cont_element = &(CG(active_op_array)->brk_cont_array[curr_brk_offset]); 214 | //计算标签标记的循环相对位置 215 | while(1){ 216 | depth++; 217 | 218 | if(label_brk_offset == curr_brk_offset){ 219 | return depth; 220 | } 221 | 222 | curr_brk_offset = brk_cont_element->parent; 223 | if(curr_brk_offset < 0){ 224 | //label标识的不是break所在循环 225 | zend_error_noreturn(E_COMPILE_ERROR, "can't break/conitnue label:'%s' because it not mark a loop", ZSTR_VAL(label_name)); 226 | } 227 | } 228 | }else{ 229 | //label没有标识一个循环 230 | zend_error_noreturn(E_COMPILE_ERROR, "can't break/conitnue label:'%s' because it not mark a loop", ZSTR_VAL(label_name)); 231 | } 232 | 233 | return -1; 234 | } 235 | ``` 236 | 改动后重新编译PHP,然后测试新的语法是否生效: 237 | ```php 238 | //test.php 239 | 240 | loop1: 241 | for($i = 0; $i < 2; $i++){ 242 | echo "loop1\n"; 243 | 244 | for($j = 0; $j < 5; $j++){ 245 | echo " loop2\n"; 246 | if($j == 2){ 247 | break loop1; 248 | } 249 | } 250 | } 251 | ``` 252 | `php test.php`输出: 253 | ``` 254 | loop1 255 | loop2 256 | loop2 257 | loop2 258 | ``` 259 | 其它几个循环结构的改动与for相同,有兴趣的可以自己去尝试下。 260 | 261 | -------------------------------------------------------------------------------- /try/defer.md: -------------------------------------------------------------------------------- 1 | # 附录2:defer推迟函数调用语法的实现 2 | 3 | 使用过Go语言的应该都知道defer这个语法,它用来推迟一个函数的执行,在函数执行返回前首先检查当前函数内是否有推迟执行的函数,如果有则执行,然后再返回。defer是一个非常有用的语法,这个功能可以很方便的在函数结束前执行一些清理工作,比如关闭打开的文件、关闭连接、释放资源、解锁等等。这样延迟一个函数有以下两个好处: 4 | 5 | * (1) 靠近使用位置,避免漏掉清理工作,同时比放在函数结尾要清晰 6 | * (2) 如果有多处返回的地方可以避免代码重复,比如函数中有很多处return 7 | 8 | 在一个函数中可以使用多个defer,其执行顺序与栈类似:后进先出,先定义的defer后执行。另外,在返回之后定义的defer将不会被执行,只有返回前定义的才会执行,通过exit退出程序的情况也不会执行任何defer。 9 | 10 | 在PHP中并没有实现类似的语法,本节我们将尝试在PHP中实现类似Go语言中defer的功能。此功能的实现需要对PHP的语法解析、抽象语法树/opcode的编译、opcode指令的执行等环节进行改造,涉及的地方比较多,但是改动点比较简单,可以很好的帮助大家完整的理解PHP编译、执行两个核心阶段的实现。总体实现思路: 11 | 12 | * __(1)语法解析:__ defer本质上还是函数调用,只是将调用时机移到了函数的最后,所以编译时可以复用调用函数的规则,但是需要与普通的调用区分开,所以我们新增一个AST节点类型,其子节点为为正常函数调用编译的AST,语法我们定义为:`defer function_name()`; 13 | * __(2)opcode编译:__ 编译opcode时也复用调用函数的编译逻辑,不同的地方在于把defer放在最后编译,另外需要在编译return前新增一条opcode,用于执行return前跳转到defer开始的位置,在defer的最后也需要新增一条opcode,用于执行完defer后跳回return的位置; 14 | * __(3)执行阶段:__ 执行时如果发现是return前新增的opcode则跳转到defer开始的位置,同时把return的位置记录下来,执行完defer后再跳回return。 15 | 16 | 编译后的opcode指令如下图所示: 17 | 18 | ![](../img/defer.png) 19 | 20 | 接下来我们详细介绍下各个环节的改动,一步步实现defer功能。 21 | 22 | __(1)语法解析__ 23 | 24 | 想让PHP支持`defer function_name()`的语法首先需要修改的是词法解析规则,将"defer"关键词解析为token:T_DEFER,这样词法扫描器在匹配token时遇到"defer"将告诉语法解析器这是一个T_DEFER。这一步改动比较简单,PHP的词法解析规则定义在zend_language_scanner.l中,加入以下代码即可: 25 | ```c 26 | "defer" { 27 | RETURN_TOKEN(T_DEFER); 28 | } 29 | ``` 30 | 完成词法解析规则的修改后接着需要定义语法解析规则,这是非常关键的一步,语法解析器会根据配置的语法规则将PHP代码解析为抽象语法树(AST)。普通函数调用会被解析为ZEND_AST_CALL类型的AST节点,我们新增一种节点类型:ZEND_AST_DEFER_CALL,抽象语法树的节点类型为enum,定义在zend_ast.h中,同时此节点只需要一个子节点,这个子节点用于保存ZEND_AST_CALL节点,因此zend_ast.h的修改如下: 31 | ```c 32 | enum _zend_ast_kind { 33 | ... 34 | /* 1 child node */ 35 | ... 36 | ZEND_AST_DEFER_CALL 37 | .... 38 | } 39 | ``` 40 | 定义完AST节点后就可以在配置语法解析规则了,把defer语法解析为ZEND_AST_DEFER_CALL节点,我们把这条语法规则定义在"statement:"节点下,if、echo、for等语法都定义在此节点下,语法解析规则文件为zend_language_parser.y: 41 | ```c 42 | statement: 43 | '{' inner_statement_list '}' { $$ = $2; } 44 | ... 45 | | T_DEFER function_call ';' { $$ = zend_ast_create(ZEND_AST_DEFER_CALL, $2); } 46 | ; 47 | ``` 48 | 修改完这两个文件后需要分别调用re2c、yacc生成对应的C文件,具体的生成命令可以在Makefile.frag中看到: 49 | ```sh 50 | $ re2c --no-generation-date --case-inverted -cbdFt Zend/zend_language_scanner_defs.h -oZend/zend_language_scanner.c Zend/zend_language_scanner.l 51 | $ yacc -p zend -v -d Zend/zend_language_parser.y -oZend/zend_language_parser.c 52 | ``` 53 | 执行完以后将在Zend目录下重新生成zend_language_scanner.c、zend_language_parser.c两个文件。到这一步已经完成生成抽象语法树的工作了,重新编译PHP后已经能够解析defer语法了,将会生成以下节点: 54 | 55 | ![](../img/defer_ast.png) 56 | 57 | __(2)编译ZEND_AST_DEFER_CALL__ 58 | 59 | 生成抽象语法树后接下来就是编译生成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中加入以下几个成员: 60 | 61 | * __last_defer:__ 整形,记录当前编译的defer数 62 | * __defer_start_op:__ 整形,用于记录defer编译生成opcode指令的起始位置 63 | * __defer_call_array:__ 保存ZEND_AST_DEFER_CALL节点的数组,用于保存ast节点的地址 64 | 65 | ```c 66 | struct _zend_op_array { 67 | ... 68 | int last_defer; 69 | uint32_t defer_start_op; 70 | zend_ast **defer_call_array; 71 | } 72 | ``` 73 | 修改完数据结构后接着对应修改zend_op_array初始化的过程: 74 | ```c 75 | //zend_opcode.c 76 | void init_op_array(zend_op_array *op_array, zend_uchar type, int initial_ops_size) 77 | { 78 | ... 79 | op_array->last_defer = 0; 80 | op_array->defer_start_op = 0; 81 | op_array->defer_call_array = NULL; 82 | ... 83 | } 84 | ``` 85 | 完成依赖的这些数据结构的改造后接下来开始编写具体的编译逻辑,也就是编译ZEND_AST_DEFER_CALL的处理。抽象语法树的编译入口函数为zend_compile_top_stmt(),然后根据不同节点的类型进行相应的编译,我们在zend_compile_stmt()函数中对ZEND_AST_DEFER_CALL节点进行编译: 86 | ```c 87 | void zend_compile_stmt(zend_ast *ast) 88 | { 89 | ... 90 | switch (ast->kind) { 91 | ... 92 | case ZEND_AST_DEFER_CALL: 93 | zend_compile_defer_call(ast); 94 | break 95 | ... 96 | } 97 | } 98 | ``` 99 | 编译过程只是将ZEND_AST_DEFER_CALL的子节点(即:ZEND_AST_CALL)保存到zend_op_array->defer_call_array数组中,注意这里defer_call_array数组还没有分配内存,参考循环结构的实现,这里我们定义了一个函数用于数组的分配: 100 | ```c 101 | //zend_compile.c 102 | void zend_compile_defer_call(zend_ast *ast) 103 | { 104 | if(!ast){ 105 | return; 106 | } 107 | 108 | zend_ast **call_ast = NULL; 109 | //将普通函数调用的ast节点保存到defer_call_array数组中 110 | call_ast = get_next_defer_call(CG(active_op_array)); 111 | *call_ast = ast->child[0]; 112 | } 113 | 114 | //zend_opcode.c 115 | zend_ast **get_next_defer_call(zend_op_array *op_array) 116 | { 117 | op_array->last_defer++; 118 | op_array->defer_call_array = erealloc(op_array->defer_call_array, sizeof(zend_ast*)*op_array->last_defer); 119 | return &op_array->defer_call_array[op_array->last_defer-1]; 120 | } 121 | ``` 122 | 既然分配了defer_call_array数组的内存就需要在zend_op_array销毁时释放: 123 | ```c 124 | //zend_opcode.c 125 | ZEND_API void destroy_op_array(zend_op_array *op_array) 126 | { 127 | ... 128 | if (op_array->defer_call_array) { 129 | efree(op_array->defer_call_array); 130 | } 131 | ... 132 | } 133 | ``` 134 | 编译完整个脚本或函数后,最后还会编译一条ZEND_RETURN,也就是返回指令,相当于ret指令,注意:这条opcode并不是我们在脚本中定义的return语句的,而是PHP内核为我们加的一条指令,这就是为什么有些函数我们没有写return也能返回的原因,任何函数或脚本都会生成这样一条指令。我们缓存在zend_op_array->defer_call_array数组中defer就是要在这时进行编译,也就是把defer的指令编译在最后。内核最后编译返回的这条指令由zend_emit_final_return()方法完成,我们把defer的编译放在此方法的末尾: 135 | ```c 136 | //zend_compile.c 137 | void zend_emit_final_return(zval *zv) 138 | { 139 | ... 140 | ret = zend_emit_op(NULL, returns_reference ? ZEND_RETURN_BY_REF : ZEND_RETURN, &zn, NULL); 141 | ret->extended_value = -1; 142 | 143 | //编译推迟执行的函数调用 144 | zend_emit_defer_call(); 145 | } 146 | ``` 147 | 前面已经说过,defer本质上就是函数调用,所以编译的过程直接复用普通函数调用的即可。另外,在编译时把起始位置记录到zend_op_array->defer_start_op中,因为在执行return前需要知道跳转到什么位置,这个值就是在那时使用的,具体的用法稍后再作说明。编译时按照倒序的顺序进行编译: 148 | ```c 149 | //zend_compile.c 150 | void zend_emit_defer_call() 151 | { 152 | if (!CG(active_op_array)->defer_call_array) { 153 | return; 154 | } 155 | 156 | zend_ast *call_ast; 157 | zend_op *nop; 158 | znode result; 159 | uint32_t opnum = get_next_op_number(CG(active_op_array)); 160 | int defer_num = CG(active_op_array)->last_defer; 161 | 162 | //记录推迟的函数调用指令开始位置 163 | CG(active_op_array)->defer_start_op = opnum; 164 | 165 | while(--defer_num >= 0){ 166 | call_ast = CG(active_op_array)->defer_call_array[defer_num]; 167 | if (call_ast == NULL) { 168 | continue; 169 | } 170 | nop = zend_emit_op(NULL, ZEND_NOP, NULL, NULL); 171 | nop->op1.var = -2; 172 | //编译函数调用 173 | zend_compile_call(&result, call_ast, BP_VAR_R); 174 | } 175 | //compile ZEND_DEFER_CALL_END 176 | zend_emit_op(NULL, ZEND_DEFER_CALL_END, NULL, NULL); 177 | } 178 | ``` 179 | 编译完推迟的函数调用之后,编译一条ZEND_DEFER_CALL_END指令,该指令用于执行完推迟的函数后跳回return的位置进行返回,opcode定义在zend_vm_opcodes.h中: 180 | ```c 181 | //zend_vm_opcodes.h 182 | #define ZEND_DEFER_CALL_END 174 183 | ``` 184 | 还有一个地方你可能已经注意到,在逐个编译defer的函数调用前都生成了一条ZEND_NOP的指令,这个的目的是什么呢?开始的时候已经介绍过defer语法的特点,函数中定义的defer并不是全部执行,在return之后定义的defer是不会执行的,比如: 185 | ```go 186 | func main(){ 187 | defer fmt.Println("A") 188 | 189 | if 1 == 1{ 190 | return 191 | } 192 | 193 | defer fmt.Println("B") 194 | } 195 | ``` 196 | 这种情况下第2个defer就不会生效,因此在return前跳转的位置就不一定是zend_op_array->defer_start_op,有可能会跳过几个函数的调用,所以这里我们通过ZEND_NOP这条空指令对多个defer call进行隔离,同时为避免与其它ZEND_NOP指令混淆,增加一个判断条件:op1.var=-2。这样在return前跳转时就根据此前定义的defer数跳过部分函数的调用,如下图所示。 197 | 198 | ![](../img/defer_call.png) 199 | 200 | 到这一步我们已经完成defer函数调用的编译,此时重新编译PHP后可以看到通过defer推迟的函数调用已经被编译在最后了,只不过这个时候它们不能被执行。 201 | 202 | __(3)编译return__ 203 | 204 | 编译return时需要插入一条指令用于跳转到推迟执行的函数调用指令处,因此这里需要再定义一条opcode:ZEND_DEFER_CALL,在编译过程中defer call还未编译,因此此时还无法知道具体的跳转值。 205 | ```c 206 | //zend_vm_opcodes.h 207 | #define ZEND_DEFER_CALL 173 208 | #define ZEND_DEFER_CALL_END 174 209 | ``` 210 | PHP脚本中声明的return语句由zend_compile_return()方法完成编译,在编译生成ZEND_DEFER_CALL指令时还需要将当前已定义的defer数(即在return前声明的defer)记录下来,用于计算具体的跳转值。 211 | ```c 212 | void zend_compile_return(zend_ast *ast) 213 | { 214 | ... 215 | //在return前编译ZEND_DEFER_CALL:用于在执行retur前跳转到defer call 216 | if (CG(active_op_array)->defer_call_array) { 217 | defer_zn.op_type = IS_UNUSED; 218 | defer_zn.u.op.num = CG(active_op_array)->last_defer; 219 | zend_emit_op(NULL, ZEND_DEFER_CALL, NULL, &defer_zn); 220 | } 221 | 222 | //编译正常返回的指令 223 | opline = zend_emit_op(NULL, by_ref ? ZEND_RETURN_BY_REF : ZEND_RETURN, 224 | &expr_node, NULL); 225 | ... 226 | } 227 | ``` 228 | 除了这种return外还有一种我们上面已经提过的return,即PHP内核编译的return指令,当PHP脚本中没有声明return语句时将执行内核添加的那条指令,因此也需要在zend_emit_final_return()加上上面的逻辑。 229 | ```c 230 | void zend_emit_final_return(zval *zv) 231 | { 232 | ... 233 | //在return前编译ZEND_DEFER_CALL:用于在执行retur前跳转到defer call 234 | if (CG(active_op_array)->defer_call_array) { 235 | //当前return之前定义的defer数 236 | defer_zn.op_type = IS_UNUSED; 237 | defer_zn.u.op.num = CG(active_op_array)->last_defer; 238 | zend_emit_op(NULL, ZEND_DEFER_CALL, NULL, &defer_zn); 239 | } 240 | 241 | //编译返回指令 242 | ret = zend_emit_op(NULL, returns_reference ? ZEND_RETURN_BY_REF : ZEND_RETURN, &zn, NULL); 243 | ret->extended_value = -1; 244 | 245 | //编译推迟执行的函数调用 246 | zend_emit_defer_call(); 247 | } 248 | ``` 249 | __(4)计算ZEND_DEFER_CALL指令的跳转位置__ 250 | 251 | 前面我们已经完成了推迟调用函数以及return编译过程的改造,在编译完成后ZEND_DEFER_CALL指令已经能够知道具体的跳转位置了,因为推迟调用的函数已经编译完成了,所以下一步就是为全部的ZEND_DEFER_CALL指令计算跳转值。前面曾介绍过,在编译完成有一个pass_two()的环节,我们就在这里完成具体跳转位置的计算,并把跳转位置保存到ZEND_DEFER_CALL指令的操作数中,在执行阶段直接跳转到对应位置。 252 | 253 | ```c 254 | ZEND_API int pass_two(zend_op_array *op_array) 255 | { 256 | zend_op *opline, *end; 257 | ... 258 | //遍历opcode 259 | opline = op_array->opcodes; 260 | end = opline + op_array->last; 261 | while (opline < end) { 262 | switch (opline->opcode) { 263 | ... 264 | case ZEND_DEFER_CALL: //设置jmp 265 | { 266 | uint32_t defer_start = op_array->defer_start_op; 267 | //skip_defer为当前return之后声明的defer数,也就是不需要执行的defer 268 | uint32_t skip_defer = op_array->last_defer - opline->op2.num; 269 | //defer_opline为推迟的函数调用起始位置 270 | zend_op *defer_opline = op_array->opcodes + defer_start; 271 | uint32_t n = 0; 272 | 273 | while(n <= skip_defer){ 274 | if (defer_opline->opcode == ZEND_NOP && defer_opline->op1.var == -2) { 275 | n++; 276 | } 277 | defer_opline++; 278 | defer_start++; 279 | } 280 | 281 | //defer_start为opcode在op_array->opcodes数组中的位置 282 | opline->op1.opline_num = defer_start; 283 | //将跳转位置保存到操作数op1中 284 | ZEND_PASS_TWO_UPDATE_JMP_TARGET(op_array, opline, opline->op1); 285 | } 286 | break; 287 | } 288 | ... 289 | } 290 | ... 291 | } 292 | ``` 293 | 这里我们并没有直接编译为ZEND_JMP跳转指令,虽然ZEND_JMP可以跳转到后面的指令位置,但是最后的那条跳回return位置的指令(即:ZEND_DEFER_CALL_END)由于可能存在多个return的原因无法在编译期间确定具体的跳转值,只能在运行期间执行ZEND_DEFER_CALL时才能确定,所以需要在ZEND_DEFER_CALL指令的handler中将return的位置记录下来,执行ZEND_DEFER_CALL_END时根据这个值跳回。 294 | 295 | __(5)定义ZEND_DEFER_CALL、ZEND_DEFER_CALL_END指令的handler__ 296 | 297 | ZEND_DEFER_CALL指令执行时需要将return的位置保存下来,我们把这个值保存到zend_execute_data结构中: 298 | ```c 299 | //zend_compile.h 300 | struct _zend_execute_data { 301 | ... 302 | const zend_op *return_opline; 303 | ... 304 | } 305 | ``` 306 | opcode的handler定义在zend_vm_def.h文件中,定义完成后需要执行`php zend_vm_gen.php`脚本生成具体的handler函数。 307 | ```c 308 | ZEND_VM_HANDLER(173, ZEND_DEFER_CALL, ANY, ANY) 309 | { 310 | USE_OPLINE 311 | 312 | //1) 将return指令的位置保存到EX(return_opline) 313 | EX(return_opline) = opline + 1; 314 | 315 | //2) 跳转 316 | ZEND_VM_SET_OPCODE(OP_JMP_ADDR(opline, opline->op1)); 317 | ZEND_VM_CONTINUE(); 318 | } 319 | 320 | ZEND_VM_HANDLER(174, ZEND_DEFER_CALL_END, ANY, ANY) 321 | { 322 | USE_OPLINE 323 | 324 | ZEND_VM_SET_OPCODE(EX(return_opline)); 325 | ZEND_VM_CONTINUE(); 326 | } 327 | ``` 328 | 到目前为止我们已经完成了全部的修改,重新编译PHP后就可以使用defer语法了: 329 | ```php 330 | function shutdown($a){ 331 | echo $a."\n"; 332 | } 333 | function test(){ 334 | $a = 1234; 335 | defer shutdown($a); 336 | 337 | $a = 8888; 338 | 339 | if(1){ 340 | return "mid end\n"; 341 | } 342 | defer shutdown("9999"); 343 | return "last end\n"; 344 | } 345 | 346 | echo test(); 347 | ``` 348 | 执行后将显示: 349 | ```sh 350 | 8888 351 | mid end 352 | ``` 353 | 这里我们只实现了普通函数调用的方式,关于成员方法、静态方法、匿名函数等调用方式并未实现,留给有兴趣的读者自己去实现。 354 | 355 | 完整代码:[https://github.com/pangudashu/php-7.0.12](https://github.com/pangudashu/php-7.0.12) 356 | --------------------------------------------------------------------------------