diff --git a/1/fpm.md b/1/fpm.md new file mode 100644 index 0000000..cea236c --- /dev/null +++ b/1/fpm.md @@ -0,0 +1,354 @@ +## 1.3 FPM +### 1.3.1 概述 +FPM(FastCGI Process Manager)是PHP FastCGI运行模式的一个进程管理器,从它的定义可以看出,FPM的核心功能是进程管理,那么它用来管理什么进程呢?这个问题就需要从FastCGI说起了。 + +FastCGI是Web服务器(如:Nginx、Apache)和处理程序之间的一种通信协议,它是与Http类似的一种应用层通信协议,注意:它只是一种协议! + +前面曾一再强调,PHP只是一个脚本解析器,你可以把它理解为一个普通的函数,输入是PHP脚本。输出是执行结果,假如我们想用PHP代替shell,在命令行中执行一个文件,那么就可以写一个程序来嵌入PHP解析器,这就是cli模式,这种模式下PHP就是普通的一个命令工具。接着我们又想:能不能让PHP处理http请求呢?这时就涉及到了网络处理,PHP需要接收请求、解析协议,然后处理完成返回请求。在网络应用场景下,PHP并没有像Golang那样实现http网络库,而是实现了FastCGI协议,然后与web服务器配合实现了http的处理,web服务器来处理http请求,然后将解析的结果再通过FastCGI协议转发给处理程序,处理程序处理完成后将结果返回给web服务器,web服务器再返回给用户,如下图所示。 + +![](../img/fastcgi.png) + +PHP实现了FastCGI协议的解析,但是并没有具体实现网络处理,一般的处理模型:多进程、多线程,多进程模型通常是主进程只负责管理子进程,而基本的网络事件由各个子进程处理,nginx、fpm就是这种模式;另一种多线程模型与多进程类似,只是它是线程粒度,通常会由主线程监听、接收请求,然后交由子线程处理,memcached就是这种模式,有的也是采用多进程那种模式:主线程只负责管理子线程不处理网络事件,各个子线程监听、接收、处理请求,memcached使用udp协议时采用的是这种模式。 + +### 1.3.2 基本实现 +概括来说,fpm的实现就是创建一个master进程,在master进程中创建并监听socket,然后fork出多个子进程,这些子进程各自accept请求,子进程的处理非常简单,它在启动后阻塞在accept上,有请求到达后开始读取请求数据,读取完成后开始处理然后再返回,在这期间是不会接收其它请求的,也就是说fpm的子进程同时只能响应一个请求,只有把这个请求处理完成后才会accept下一个请求,这一点与nginx的事件驱动有很大的区别,nginx的子进程通过epoll管理套接字,如果一个请求数据还未发送完成则会处理下一个请求,即一个进程会同时连接多个请求,它是非阻塞的模型,只处理活跃的套接字。 + +fpm的master进程与worker进程之间不会直接进行通信,master通过共享内存获取worker进程的信息,比如worker进程当前状态、已处理请求数等,当master进程要杀掉一个worker进程时则通过发送信号的方式通知worker进程。 + +fpm可以同时监听多个端口,每个端口对应一个worker pool,而每个pool下对应多个worker进程,类似nginx中server概念。 + +![](../img/worker_pool.png) + +在php-fpm.conf中通过`[pool name]`声明一个worker pool: +``` +[web1] +listen = 127.0.0.1:9000 +... + +[web2] +listen = 127.0.0.1:9001 +... +``` +启动fpm后查看进程:ps -aux|grep fpm +```c +root 27155 0.0 0.1 144704 2720 ? Ss 15:16 0:00 php-fpm: master process (/usr/local/php7/etc/php-fpm.conf) +nobody 27156 0.0 0.1 144676 2416 ? S 15:16 0:00 php-fpm: pool web1 +nobody 27157 0.0 0.1 144676 2416 ? S 15:16 0:00 php-fpm: pool web1 +nobody 27159 0.0 0.1 144680 2376 ? S 15:16 0:00 php-fpm: pool web2 +nobody 27160 0.0 0.1 144680 2376 ? S 15:16 0:00 php-fpm: pool web2 +``` +具体实现上worker pool通过`fpm_worker_pool_s`这个结构表示,多个worker pool组成一个单链表: +```c +struct fpm_worker_pool_s { + struct fpm_worker_pool_s *next; //指向下一个worker pool + struct fpm_worker_pool_config_s *config; //conf配置:pm、max_children、start_servers... + int listening_socket; //监听的套接字 + ... + + //以下这个值用于master定时检查、记录worker数 + struct fpm_child_s *children; //当前pool的worker链表 + int running_children; //当前pool的worker运行总数 + int idle_spawn_rate; + int warn_max_children; + + struct fpm_scoreboard_s *scoreboard; //记录worker的运行信息,比如空闲、忙碌worker数 + ... +} +``` +### 1.3.3 FPM的初始化 +接下来看下fpm的启动流程,从`main()`函数开始: +```c +//sapi/fpm/fpm/fpm_main.c +int main(int argc, char *argv[]) +{ + ... + //注册SAPI:将全局变量sapi_module设置为cgi_sapi_module + sapi_startup(&cgi_sapi_module); + ... + //执行php_module_starup() + if (cgi_sapi_module.startup(&cgi_sapi_module) == FAILURE) { + return FPM_EXIT_SOFTWARE; + } + ... + //初始化 + if(0 > fpm_init(...)){ + ... + } + ... + fpm_is_running = 1; + + fcgi_fd = fpm_run(&max_requests);//后面都是worker进程的操作,master进程不会走到下面 + parent = 0; + ... +} +``` +`fpm_init()`主要有以下几个关键操作: + +__(1)fpm_conf_init_main():__ + +解析php-fpm.conf配置文件,分配worker pool内存结构并保存到全局变量中:fpm_worker_all_pools,各worker pool配置解析到`fpm_worker_pool_s->config`中。 + +__(2)fpm_scoreboard_init_main():__ 分配用于记录worker进程运行信息的共享内存,按照worker pool的最大worker进程数分配,每个worker pool分配一个`fpm_scoreboard_s`结构,pool下对应的每个worker进程分配一个`fpm_scoreboard_proc_s`结构,各结构的对应关系如下图。 + +![](../img/worker_pool_struct.png) + +__(3)fpm_signals_init_main():__ +```c +static int sp[2]; + +int fpm_signals_init_main() +{ + struct sigaction act; + + //创建一个全双工管道 + if (0 > socketpair(AF_UNIX, SOCK_STREAM, 0, sp)) { + return -1; + } + //注册信号处理handler + act.sa_handler = sig_handler; + sigfillset(&act.sa_mask); + if (0 > sigaction(SIGTERM, &act, 0) || + 0 > sigaction(SIGINT, &act, 0) || + 0 > sigaction(SIGUSR1, &act, 0) || + 0 > sigaction(SIGUSR2, &act, 0) || + 0 > sigaction(SIGCHLD, &act, 0) || + 0 > sigaction(SIGQUIT, &act, 0)) { + return -1; + } + return 0; +} +``` +这里会通过`socketpair()`创建一个管道,这个管道并不是用于master与worker进程通信的,它只在master进程中使用,具体用途在稍后介绍event事件处理时再作说明。另外设置master的信号处理handler,当master收到SIGTERM、SIGINT、SIGUSR1、SIGUSR2、SIGCHLD、SIGQUIT这些信号时将调用`sig_handler()`处理: +```c +static void sig_handler(int signo) +{ + static const char sig_chars[NSIG + 1] = { + [SIGTERM] = 'T', + [SIGINT] = 'I', + [SIGUSR1] = '1', + [SIGUSR2] = '2', + [SIGQUIT] = 'Q', + [SIGCHLD] = 'C' + }; + char s; + ... + s = sig_chars[signo]; + //将信号通知写入管道sp[1]端 + write(sp[1], &s, sizeof(s)); + ... +} +``` +__(4)fpm_sockets_init_main()__ + +创建每个worker pool的socket套接字。 + +__(5)fpm_event_init_main():__ + +启动master的事件管理,fpm实现了一个事件管理器用于管理IO、定时事件,其中IO事件通过kqueue、epoll、poll、select等管理,定时事件就是定时器,一定时间后触发某个事件。 + +在`fpm_init()`初始化完成后接下来就是最关键的`fpm_run()`操作了,此环节将fork子进程,启动进程管理器,另外master进程将不会再返回,只有各worker进程会返回,也就是说`fpm_run()`之后的操作均是worker进程的。 +```c +int fpm_run(int *max_requests) +{ + struct fpm_worker_pool_s *wp; + for (wp = fpm_worker_all_pools; wp; wp = wp->next) { + //调用fpm_children_make() fork子进程 + is_parent = fpm_children_create_initial(wp); + + if (!is_parent) { + goto run_child; + } + } + //master进程将进入event循环,不再往下走 + fpm_event_loop(0); + +run_child: //只有worker进程会到这里 + + *max_requests = fpm_globals.max_requests; + return fpm_globals.listening_socket; //返回监听的套接字 +} +``` +在fork后worker进程返回了监听的套接字继续main()后面的处理,而master将永远阻塞在`fpm_event_loop()`,接下来分别介绍master、worker进程的后续操作。 + +### 1.3.4 请求处理 +`fpm_run()`执行后将fork出worker进程,worker进程返回`main()`中继续向下执行,后面的流程就是worker进程不断accept请求,然后执行PHP脚本并返回。整体流程如下: + +* __(1)等待请求:__ worker进程阻塞在fcgi_accept_request()等待请求; +* __(2)解析请求:__ fastcgi请求到达后被worker接收,然后开始接收并解析请求数据,直到request数据完全到达; +* __(3)请求初始化:__ 执行php_request_startup(),此阶段会调用每个扩展的:PHP_RINIT_FUNCTION(); +* __(4)编译、执行:__ 由php_execute_script()完成PHP脚本的编译、执行; +* __(5)关闭请求:__ 请求完成后执行php_request_shutdown(),此阶段会调用每个扩展的:PHP_RSHUTDOWN_FUNCTION(),然后进入步骤(1)等待下一个请求。 + +```c +int main(int argc, char *argv[]) +{ + ... + fcgi_fd = fpm_run(&max_requests); + parent = 0; + + //初始化fastcgi请求 + request = fpm_init_request(fcgi_fd); + + //worker进程将阻塞在这,等待请求 + while (EXPECTED(fcgi_accept_request(request) >= 0)) { + SG(server_context) = (void *) request; + init_request_info(); + + //请求开始 + if (UNEXPECTED(php_request_startup() == FAILURE)) { + ... + } + ... + + fpm_request_executing(); + //编译、执行PHP脚本 + php_execute_script(&file_handle); + ... + //请求结束 + php_request_shutdown((void *) 0); + ... + } + ... + //worker进程退出 + php_module_shutdown(); + ... +} +``` +worker进程一次请求的处理被划分为5个阶段: + +* __FPM_REQUEST_ACCEPTING:__ 等待请求阶段 +* __FPM_REQUEST_READING_HEADERS:__ 读取fastcgi请求header阶段 +* __FPM_REQUEST_INFO:__ 获取请求信息阶段,此阶段是将请求的method、query stirng、request uri等信息保存到各worker进程的fpm_scoreboard_proc_s结构中,此操作需要加锁,因为master进程也会操作此结构 +* __FPM_REQUEST_EXECUTING:__ 执行请求阶段 +* __FPM_REQUEST_END:__ 没有使用 +* __FPM_REQUEST_FINISHED:__ 请求处理完成 + +worker处理到各个阶段时将会把当前阶段更新到`fpm_scoreboard_proc_s->request_stage`,master进程正是通过这个标识判断worker进程是否空闲的。 + +### 1.3.5 进程管理 +这一节我们来看下master是如何管理worker进程的,首先介绍下三种不同的进程管理方式: + +* __static:__ 这种方式比较简单,在启动时master按照`pm.max_children`配置fork出相应数量的worker进程,即worker进程数是固定不变的 +* __dynamic:__ 动态进程管理,首先在fpm启动时按照`pm.start_servers`初始化一定数量的worker,运行期间如果master发现空闲worker数低于`pm.min_spare_servers`配置数(表示请求比较多,worker处理不过来了)则会fork worker进程,但总的worker数不能超过`pm.max_children`,如果master发现空闲worker数超过了`pm.max_spare_servers`(表示闲着的worker太多了)则会杀掉一些worker,避免占用过多资源,master通过这4个值来控制worker数 +* __ondemand:__ 这种方式一般很少用,在启动时不分配worker进程,等到有请求了后再通知master进程fork worker进程,总的worker数不超过`pm.max_children`,处理完成后worker进程不会立即退出,当空闲时间超过`pm.process_idle_timeout`后再退出 + +前面介绍到在`fpm_run()`master进程将进入`fpm_event_loop()`: +```c +void fpm_event_loop(int err) +{ + //创建一个io read的监听事件,这里监听的就是在fpm_init()阶段中通过socketpair()创建管道sp[0] + //当sp[0]可读时将回调fpm_got_signal() + fpm_event_set(&signal_fd_event, fpm_signals_get_fd(), FPM_EV_READ, &fpm_got_signal, NULL); + fpm_event_add(&signal_fd_event, 0); + + //如果在php-fpm.conf配置了request_terminate_timeout则启动心跳检查 + if (fpm_globals.heartbeat > 0) { + fpm_pctl_heartbeat(NULL, 0, NULL); + } + //定时触发进程管理 + fpm_pctl_perform_idle_server_maintenance_heartbeat(NULL, 0, NULL); + + //进入事件循环,master进程将阻塞在此 + while (1) { + ... + //等待IO事件 + ret = module->wait(fpm_event_queue_fd, timeout); + ... + //检查定时器事件 + ... + } +} +``` +这就是master整体的处理,其进程管理主要依赖注册的几个事件,接下来我们详细分析下这几个事件的功能。 + +__(1)sp[1]管道可读事件:__ + +在`fpm_init()`阶段master曾创建了一个全双工的管道:sp,然后在这里创建了一个sp[0]可读的事件,当sp[0]可读时将交由`fpm_got_signal()`处理,向sp[1]写数据时sp[0]才会可读,那么什么时机会向sp[1]写数据呢?前面已经提到了:当master收到注册的那几种信号时会写入sp[1]端,这个时候将触发sp[0]可读事件。 + +![](../img/master_event_1.png) + +这个事件是master用于处理信号的,我们根据master注册的信号逐个看下不同用途: + +* __SIGINT/SIGTERM/SIGQUIT:__ 退出fpm,在master收到退出信号后将向所有的worker进程发送退出信号,然后master退出 +* __SIGUSR1:__ 重新加载日志文件,生产环境中通常会对日志进行切割,切割后会生成一个新的日志文件,如果fpm不重新加载将无法继续写入日志,这个时候就需要向master发送一个USR1的信号 +* __SIGUSR2:__ 重启fpm,首先master也是会向所有的worker进程发送退出信号,然后master会调用execvp()重新启动fpm,最后旧的master退出 +* __SIGCHLD:__ 这个信号是子进程退出时操作系统发送给父进程的,子进程退出时,内核将子进程置为僵尸状态,这个进程称为僵尸进程,它只保留最小的一些内核数据结构,以便父进程查询子进程的退出状态,只有当父进程调用wait或者waitpid函数查询子进程退出状态后子进程才告终止,fpm中当worker进程因为异常原因(比如coredump了)退出而非master主动杀掉时master将受到此信号,这个时候父进程将调用waitpid()查下子进程的退出,然后检查下是不是需要重新fork新的worker + +具体处理逻辑在`fpm_got_signal()`函数中,这里不再罗列。 + +__(2)fpm_pctl_perform_idle_server_maintenance_heartbeat():__ + +这是进程管理实现的主要事件,master启动了一个定时器,每隔1s触发一次,主要用于dynamic、ondemand模式下的worker管理,master会定时检查各worker pool的worker进程数,通过此定时器实现worker数量的控制,处理逻辑如下: +```c +static void fpm_pctl_perform_idle_server_maintenance(struct timeval *now) +{ + for (wp = fpm_worker_all_pools; wp; wp = wp->next) { + struct fpm_child_s *last_idle_child = NULL; //空闲时间最久的worker + int idle = 0; //空闲worker数 + int active = 0; //忙碌worker数 + + for (child = wp->children; child; child = child->next) { + //根据worker进程的fpm_scoreboard_proc_s->request_stage判断 + if (fpm_request_is_idle(child)) { + //找空闲时间最久的worker + ... + idle++; + }else{ + active++; + } + } + ... + //ondemand模式 + if (wp->config->pm == PM_STYLE_ONDEMAND) { + if (!last_idle_child) continue; + + fpm_request_last_activity(last_idle_child, &last); + fpm_clock_get(&now); + if (last.tv_sec < now.tv_sec - wp->config->pm_process_idle_timeout) { + //如果空闲时间最长的worker空闲时间超过了process_idle_timeout则杀掉该worker + last_idle_child->idle_kill = 1; + fpm_pctl_kill(last_idle_child->pid, FPM_PCTL_QUIT); + } + continue; + } + //dynamic + if (wp->config->pm != PM_STYLE_DYNAMIC) continue; + if (idle > wp->config->pm_max_spare_servers && last_idle_child) { + //空闲worker太多了,杀掉 + last_idle_child->idle_kill = 1; + fpm_pctl_kill(last_idle_child->pid, FPM_PCTL_QUIT); + wp->idle_spawn_rate = 1; + continue; + } + if (idle < wp->config->pm_min_spare_servers) { + //空闲worker太少了,如果总worker数未达到max数则fork + ... + } + } +} +``` +__(3)fpm_pctl_heartbeat():__ + +这个事件是用于限制worker处理单个请求最大耗时的,php-fpm.conf中有一个`request_terminate_timeout`的配置项,如果worker处理一个请求的总时长超过了这个值那么master将会向此worker进程发送`kill -TERM`信号杀掉worker进程,此配置单位为秒,默认值为0表示关闭此机制,另外fpm打印的slow log也是在这里完成的。 +```c +static void fpm_pctl_check_request_timeout(struct timeval *now) +{ + struct fpm_worker_pool_s *wp; + + for (wp = fpm_worker_all_pools; wp; wp = wp->next) { + int terminate_timeout = wp->config->request_terminate_timeout; + int slowlog_timeout = wp->config->request_slowlog_timeout; + struct fpm_child_s *child; + + if (terminate_timeout || slowlog_timeout) { + for (child = wp->children; child; child = child->next) { + //检查当前当前worker处理的请求是否超时 + fpm_request_check_timed_out(child, now, terminate_timeout, slowlog_timeout); + } + } + } +} +``` +除了上面这几个事件外还有一个没有提到,那就是ondemand模式下master监听的新请求到达的事件,因为ondemand模式下fpm启动时是不会预创建worker的,有请求时才会生成子进程,所以请求到达时需要通知master进程,这个事件是在`fpm_children_create_initial()`时注册的,事件处理函数为`fpm_pctl_on_socket_accept()`,具体逻辑这里不再展开,比较容易理解。 + +到目前为止我们已经把fpm的核心实现介绍完了,事实上fpm的实现还是比较简单的。 diff --git a/2/global_var.md b/2/global_var.md index a6e90fc..e9515d4 100644 --- a/2/global_var.md +++ b/2/global_var.md @@ -1,7 +1,5 @@ ## 2.4 全局变量 -PHP中在函数、类之外直接定义的变量可以在函数、类成员方法中通过global关键词引入使用,这些变量称为:全局变量。 - -这些直接在PHP中定义的变量(包括include、require文件中的)相对于函数、类方法而言它们是全局变量,但是对自身执行域zend_execute_data而言它们是普通的局部变量,自身执行时它们与普通变量的读写方式完全相同。 +PHP中把定义在函数、类之外的变量称之为全局变量,也就是定义在主脚本中的变量,这些变量可以在函数、成员方法中通过global关键字引入使用。 ```php function test() { @@ -64,7 +62,7 @@ global $id; // 相当于:$id = & EG(symbol_table)["id"]; ![](../img/zend_global_ref.png) ### 2.4.3 超全局变量 -全部变量除了通过global引入外还有一类特殊的类型,它们不需要使用global引入而可以直接使用,这些全局变量称为:超全局变量。 +全局变量除了通过global引入外还有一类特殊的类型,它们不需要使用global引入而可以直接使用,这些全局变量称为:超全局变量。 超全局变量实际是PHP内核定义的一些全局变量:$GLOBALS、$_SERVER、$_REQUEST、$_POST、$_GET、$_FILES、$_ENV、$_COOKIE、$_SESSION、argv、argc。 diff --git a/2/static_var.md b/2/static_var.md index d382cb4..c134cf2 100644 --- a/2/static_var.md +++ b/2/static_var.md @@ -87,7 +87,7 @@ if (by_ref) { * __ZEND_FETCH_W:__ 这条opcode对应的操作是创建一个IS_INDIRECT类型的zval,指向static_variables中对应静态变量的zval * __ZEND_ASSIGN_REF:__ 它的操作是引用赋值,即将一个引用赋值给CV变量 -通过上面两条opcode可以确定静态变量的读写过程:首先根据变量名在static_variables中取出对应的zval,然后将它修改为引用类型并赋值给局部变量,也就是说`static $count = 4;`包含了两个操作,严格的将`$count`并不是真正的静态变量,它只是一个指向静态变量的局部变量,执行时实际操作是:`$count = & static_variables["count"];`。上面例子$count与static_variables["count"]间的关系如图所示。 +通过上面两条opcode可以确定静态变量的读写过程:首先根据变量名在static_variables中取出对应的zval,然后将它修改为引用类型并赋值给局部变量,也就是说`static $count = 4;`包含了两个操作,严格的说`$count`并不是真正的静态变量,它只是一个指向静态变量的局部变量,执行时实际操作是:`$count = & static_variables["count"];`。上面例子$count与static_variables["count"]间的关系如图所示。 ![](../img/zend_static_ref.png) diff --git a/2/zend_constant.md b/2/zend_constant.md index fad305c..eca3f65 100644 --- a/2/zend_constant.md +++ b/2/zend_constant.md @@ -8,7 +8,7 @@ PHP中的常量通过`define()`函数定义: define('CONST_VAR_1', 1234); ``` ### 2.5.1 常量的存储 -在内核中常量存储在`EG(zend_constant)`哈希表中,访问时也是根据常量名直接到哈希表中查找,其实现比较简单。 +在内核中常量存储在`EG(zend_constants)`哈希表中,访问时也是根据常量名直接到哈希表中查找,其实现比较简单。 常量的数据结构: ```c diff --git a/2/zend_ht.md b/2/zend_ht.md index 335c700..56c818c 100644 --- a/2/zend_ht.md +++ b/2/zend_ht.md @@ -38,7 +38,7 @@ struct _zend_array { dtor_func_t pDestructor; }; ``` -HashTable中有两个非常相近的值:`nNumUsed`、`nNumOfElements`,`nNumOfElements`表示哈希表已有元素数,那这个值不跟`nNumUsed`一样吗?为什么要定义两个呢?实际上它们有不同的含义,当将一个元素从哈希表删除时并不会将对应的Bucket移除,而是将Bucket存储的zval标示为`IS_UNDEF`,只有扩容时发现nNumOfElements与nNumUsed相差达到一定数量(这个数量是:`ht->nNumUsed - ht->nNumOfElements > (ht->nNumOfElements >> 5)`)时才会将已删除的元素全部移除,重新构建哈希表。所以`nNumUsed`>=`nNumOfElements`。 +HashTable中有两个非常相近的值:`nNumUsed`、`nNumOfElements`,`nNumOfElements`表示哈希表已有元素数,那这个值不跟`nNumUsed`一样吗?为什么要定义两个呢?实际上它们有不同的含义,当将一个元素从哈希表删除时并不会将对应的Bucket移除,而是将Bucket存储的zval修改为`IS_UNDEF`,只有扩容时发现nNumOfElements与nNumUsed相差达到一定数量(这个数量是:`ht->nNumUsed - ht->nNumOfElements > (ht->nNumOfElements >> 5)`)时才会将已删除的元素全部移除,重新构建哈希表。所以`nNumUsed`>=`nNumOfElements`。 HashTable中另外一个非常重要的值`arData`,这个值指向存储元素数组的第一个Bucket,插入元素时按顺序 __依次插入__ 数组,比如第一个元素在arData[0]、第二个在arData[1]...arData[nNumUsed]。PHP数组的有序性正是通过`arData`保证的,这是第一个与普通散列表实现不同的地方。 @@ -61,6 +61,8 @@ unset($arr["c"]); ![](../img/zend_hash_1.png) +> 图中Bucket的zval.u2.next默认值应该为-1,不是0 + ### 2.2.2 映射函数 映射函数(即:散列函数)是散列表的关键部分,它将key与value建立映射关系,一般映射函数可以根据key的哈希值与Bucket数组大小取模得到,即`key->h % ht->nTableSize`,但是PHP却不是这么做的: ```c @@ -155,7 +157,7 @@ static void ZEND_FASTCALL zend_hash_do_resize(HashTable *ht) ``` ### 2.2.6 重建散列表 -当删除元素达到一定数量或扩容后都需要重建散列表,因为value在Bucket位置移动了或哈希数组nTableSize变化了导致key与value的映射关系改变,重建过程实际就是遍历Bucket数组中的value,然后重新计算映射值更新到散列表,除了更新散列表之外,这里还有一个重要的处理:移除已删除的value,开始的时候我们说过,删除value时只是将value的type表为了IS_UNDEF,并没有实际从Bucket数组中删除,如果这些value一直存在那么将浪费很多空间,所以这里会把它们移除,操作的方式也比较简单:将后面未删除的value依次前移,具体过程如下: +当删除元素达到一定数量或扩容后都需要重建散列表,因为value在Bucket位置移动了或哈希数组nTableSize变化了导致key与value的映射关系改变,重建过程实际就是遍历Bucket数组中的value,然后重新计算映射值更新到散列表,除了更新散列表之外,这里还有一个重要的处理:移除已删除的value,开始的时候我们说过,删除value时只是将value的type设置为IS_UNDEF,并没有实际从Bucket数组中删除,如果这些value一直存在那么将浪费很多空间,所以这里会把它们移除,操作的方式也比较简单:将后面未删除的value依次前移,具体过程如下: ```c //zend_hash.c ZEND_API int ZEND_FASTCALL zend_hash_rehash(HashTable *ht) diff --git a/2/zval.md b/2/zval.md index afe479b..dd82bc0 100644 --- a/2/zval.md +++ b/2/zval.md @@ -13,7 +13,7 @@ $b = 1; ### 2.1.1 变量的基础结构 ```c -//zend_type.h +//zend_types.h typedef struct _zval_struct zval; typedef union _zend_value { @@ -60,7 +60,7 @@ struct _zval_struct { }; ``` `zval`结构比较简单,内嵌一个union类型的`zend_value`保存具体变量类型的值或指针,`zval`中还有两个union:`u1`、`u2`: -* __u1:__ 它的意义比较直观,变量的类型就通过`u1.type`区分,另外一个值`type_flags`为类型掩码,在变量的内存管理、gc机制中会用到,第三部分会详细分析,至于后面两个`const_flags`、`reserved`暂且不管 +* __u1:__ 它的意义比较直观,变量的类型就通过`u1.v.type`区分,另外一个值`type_flags`为类型掩码,在变量的内存管理、gc机制中会用到,第三部分会详细分析,至于后面两个`const_flags`、`reserved`暂且不管 * __u2:__ 这个值纯粹是个辅助值,假如`zval`只有:`value`、`u1`两个值,整个zval的大小也会对齐到16byte,既然不管有没有u2大小都是16byte,把多余的4byte拿出来用于一些特殊用途还是很划算的,比如next在哈希表解决哈希冲突时会用到,还有fe_pos在foreach会用到...... 从`zend_value`可以看出,除`long`、`double`类型直接存储值外,其它类型都为指针,指向各自的结构。 @@ -259,7 +259,7 @@ $a,$b -> zend_string_1(refcount=0,val="hi~") |reference | Y | ``` simple types很显然用不到,不再解释,string、array、object、resource、reference有引用计数机制也很容易理解,下面具体解释下另外两个特殊的类型: -* __interned string:__ 内部字符串,这是种什么类型?我们在PHP中写的所有字符都可以认为是这种类型,比如function name、class name、variable name、静态字符串等等,我们这样定义:`$a = "hi~;"`后面的字符串内容是唯一不变的,这些字符串等同于C语言中定义在静态变量区的字符串:`char *a = "hi~";`,这些字符串的生命周期为request期间,request完成后会统一销毁释放,自然也就无需在运行期间通过引用计数管理内存。 +* __interned string:__ 内部字符串,这是种什么类型?我们在PHP中写的所有字符都可以认为是这种类型,比如function name、class name、variable name、静态字符串等等,我们这样定义:`$a = "hi~";`后面的字符串内容是唯一不变的,这些字符串等同于C语言中定义在静态变量区的字符串:`char *a = "hi~";`,这些字符串的生命周期为request期间,request完成后会统一销毁释放,自然也就无需在运行期间通过引用计数管理内存。 * __immutable array:__ 只有在用opcache的时候才会用到这种类型,不清楚具体实现,暂时忽略。 @@ -278,7 +278,7 @@ $b[] = 3; ![zval_sep](../img/zval_sep.png) -不是所有类型都可以copy的,比如对象、资源,实时上只有string、array两种支持,与引用计数相同,也是通过`zval.u1.type_flag`标识value是否可复制的: +不是所有类型都可以copy的,比如对象、资源,事实上只有string、array两种支持,与引用计数相同,也是通过`zval.u1.type_flag`标识value是否可复制的: ```c #define IS_TYPE_COPYABLE (1<<4) ``` diff --git a/3/function_implement.md b/3/function_implement.md index 5373d41..ef4bcb5 100644 --- a/3/function_implement.md +++ b/3/function_implement.md @@ -8,7 +8,7 @@ function my_func(){ ... } ``` -汇编中函数对应的是一组独立的汇编指令,然后通过call指令实现函数的调用,前面已经说过PHP编译的结果是opcode数组,与汇编指令对应,PHP用户自定义函数的实现就是将函数编译为独立的opcode数组,调用时分配独立的执行栈依次执行opcode,所以自定义函数对于zend而言并没有什么特别之处,只是将opcode进行了打包封装,实际PHP脚本中函数之外的指令整个可以认为是一个函数(或者理解为main函数更直观)。 +汇编中函数对应的是一组独立的汇编指令,然后通过call指令实现函数的调用。前面已经说过PHP编译的结果是opcode数组,与汇编指令对应。PHP用户自定义函数的实现就是将函数编译为独立的opcode数组,调用时分配独立的执行栈依次执行opcode,所以自定义函数对于zend而言并没有什么特别之处,只是将opcode进行了打包封装。PHP脚本中函数之外的指令,整个可以认为是一个函数(或者理解为main函数更直观)。 ```php /* function main(){ */ @@ -287,7 +287,7 @@ $greet = function($name) $greet('World'); $greet('PHP'); ``` -这里提函数函数只是想说明编译函数时那个use的用法: +这里提匿名函数只是想说明编译函数时那个use的用法: __匿名函数可以从父作用域中继承变量。 任何此类变量都应该用 use 语言结构传递进去。__ diff --git a/3/zend_class.md b/3/zend_class.md index e0bc8f5..62eea77 100644 --- a/3/zend_class.md +++ b/3/zend_class.md @@ -1,7 +1,7 @@ ### 3.4.1 类 类是现实世界或思维世界中的实体在计算机中的反映,它将某些具有关联关系的数据以及这些数据上的操作封装在一起。在面向对象中类是对象的抽象,对象是类的具体实例。 -在PHP中类编译阶段的产物,而对象是运行时产生的,它们归属于不同阶段。 +在PHP中类是编译阶段的产物,而对象是运行时产生的,它们归属于不同阶段。 PHP中我们这样定义一个类: ```php @@ -416,7 +416,7 @@ void zend_compile_class_const_decl(zend_ast *ast) zend_class_entry *ce = CG(active_class_entry); uint32_t i; - for (i = 0; i < list->children; ++i) { //不清楚这个地方为什么要用list,试了几个例子这个节点都只有一个child,即for只循环一次 + for (i = 0; i < list->children; ++i) { //const声明了多个常量,遍历编译每个子节点 zend_ast *const_ast = list->child[i]; zend_ast *name_ast = const_ast->child[0]; //常量名节点 zend_ast *value_ast = const_ast->child[1];//常量值节点 @@ -446,7 +446,6 @@ void zend_compile_prop_decl(zend_ast *ast) zend_class_entry *ce = CG(active_class_entry); uint32_t i, children = list->children; - //也不清楚这里为啥用循环,测试的情况child只有一个 for (i = 0; i < children; ++i) { zend_ast *prop_ast = list->child[i]; //这个节点类型为:ZEND_AST_PROP_ELEM zend_ast *name_ast = prop_ast->child[0]; //属性名节点 diff --git a/3/zend_compile_opcode.md b/3/zend_compile_opcode.md index f48e7da..6969c1a 100644 --- a/3/zend_compile_opcode.md +++ b/3/zend_compile_opcode.md @@ -245,7 +245,7 @@ int main() return 0; } ``` -我们知道name的值分配在栈上,而"pangudashu"分配在常量区,那么"name"变量名分配在哪呢? +我们知道指针name分配在栈上,而"pangudashu"分配在常量区,那么"name"变量名分配在哪呢? 实际上C里面是不会存变量名称的,编译的过程会将变量名替换为偏移量表示:`ebp - 偏移量`或`esp + 偏移量`,将上面的代码转为汇编: ```c @@ -272,7 +272,7 @@ main: ![php vs c](../img/php_vs_c.png) -在编译时就可确定且不会改变的量称为字面量,也称作常量(IS_CONST),这些值在编译阶段就已经分配zval,保存在`zend_op_array->literals`数组中(对应c程序的常量内存区),访问时通过`_zend_op_array->literals + 偏移量`读取,举个例子: +在编译时就可确定且不会改变的量称为字面量,也称作常量(IS_CONST),这些值在编译阶段就已经分配zval,保存在`zend_op_array->literals`数组中(对应c程序的常量存储区),访问时通过`_zend_op_array->literals + 偏移量`读取,举个例子: ```c kind) { case xxx: ... - break; + break; case ZEND_AST_ECHO: zend_compile_echo(ast); break; - ... + ... default: { znode result; @@ -516,7 +516,7 @@ void zend_compile_expr(znode *result, zend_ast *ast) ``` >> __第3步:__ 上面两步已经分别生成了变量赋值的op1、op2,下面就是根据这俩值生成opcode的过程。 ```c -tatic zend_op *zend_emit_op(znode *result, zend_uchar opcode, znode *op1, znode *op2) +static zend_op *zend_emit_op(znode *result, zend_uchar opcode, znode *op1, znode *op2) { zend_op *opline = get_next_op(CG(active_op_array)); //当前zend_op_array下生成一条新的指令 opline->opcode = opcode; diff --git a/3/zend_executor.md b/3/zend_executor.md index d589492..b1ed95d 100644 --- a/3/zend_executor.md +++ b/3/zend_executor.md @@ -62,7 +62,7 @@ ZEND_API zend_executor_globals executor_globals; struct _zend_execute_data { const zend_op *opline; //指向当前执行的opcode,初始时指向zend_op_array起始位置 zend_execute_data *call; /* current call */ - zval *return_value; //返回值指针 */ + zval *return_value; //返回值指针 zend_function *func; //当前执行的函数(非函数调用时为空) zval This; //这个值并不仅仅是面向对象的this,还有另外两个值也通过这个记录:call_info + num_args,分别存在zval.u1.reserved、zval.u2.num_args zend_class_entry *called_scope; //当前call的类 diff --git a/3/zend_global_register.md b/3/zend_global_register.md new file mode 100644 index 0000000..9c9462e --- /dev/null +++ b/3/zend_global_register.md @@ -0,0 +1,173 @@ +### 3.3.4 全局execute_data和opline +Zend执行器在opcode的执行过程中,会频繁的用到execute_data和opline两个变量,execute_data为zend_execute_data结构,opline为当前执行的指令。普通的处理方式在执行每条opcode指令的handler时,会把execute_data地址作为参数传给handler使用,使用时先从当前栈上获取execute_data地址,然后再从堆上获取变量的数据,这种方式下Zend执行器展开后是下面这样: +```c +ZEND_API void execute_ex(zend_execute_data *ex) +{ + zend_execute_data *execute_data = ex; + + while (1) { + int ret; + + if (UNEXPECTED((ret = ((opcode_handler_t)execute_data->opline->handler)(execute_data)) != 0)) { + if (EXPECTED(ret > 0)) { + execute_data = EG(current_execute_data); + } else { + return; + } + } + } +} +``` +执行器实际是一个大循环,从第一条opcode开始执行,execute_data->opline指向当前执行的指令,执行完以后指向下一条指令,opline类似eip(或rip)寄存器的作用。通过这个循环,ZendVM完成opcode指令的执行。opcode执行完后以后指向下一条指令的操作是在当前handler中完成,也就是说每条执行执行完以后会主动更新opline,这里会有下面几个不同的动作: +```c +#define ZEND_VM_CONTINUE() return 0 +#define ZEND_VM_ENTER() return 1 +#define ZEND_VM_LEAVE() return 2 +#define ZEND_VM_RETURN() return -1 +``` +ZEND_VM_CONTINUE()表示继续执行下一条opcode;ZEND_VM_ENTER()/ZEND_VM_LEAVE()是调用函数时的动作,普通模式下ZEND_VM_ENTER()实际就是return 1,然后execute_ex()中会将execute_data切换到被调函数的结构上,对应的,在函数调用完成后ZEND_VM_LEAVE()会return 2,再将execute_data切换至原来的结构;ZEND_VM_RETURN()表示执行完成,返回-1给execute_ex(),比如exit,这时候execute_ex()将退出执行。下面看一个具体的例子: +```php +$a = "hi~"; +echo $a; +``` +执行过程如下图所示: + +![](../img/executor.png) + +以ZEND_ASSIGN这条赋值指令为例,其handler展开前如下: +```c +static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ASSIGN_SPEC_CV_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS) +{ + USE_OPLINE + ... + ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); +} +``` +所有opcode的handler定义格式都是相同的,其参数列表通过ZEND_OPCODE_HANDLER_ARGS宏定义,展开后实际只有一个execute_data,展开后: +```c +static int ZEND_ASSIGN_SPEC_CV_CONST_HANDLER(zend_execute_data *execute_data) +{ + //USE_OPLINE + const zend_op *opline = execute_data->opline; + ... + + //ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION() + execute_data->opline = execute_data->opline + 1; + return 0; +} +``` +从这个例子可以很清楚的看到,执行完以后会将execute_data->opline加1,也就是指向下一条opcode,然后返回0给execute_ex(),接着执行器在下一次循环时执行下一条opcode,依次类推,直至所有的opcode执行完成。这个处理过程比较简单,并没有不好理解的地方,而且整个过程看起来也都那么顺理成章。PHP7针对execute_data、opline两个变量的存储位置进行了优化,那就是使用全局寄存器保存这两个变量的地址,以实现更高效率的读取。这种方式下execute_data、opline直接从寄存器读取地址,在性能上大概有5%的提升(官方说法)。在分析PHP7的优化之前,我们先简单介绍下什么是寄存器变量。 + +寄存器变量存放在CPU的寄存器中,使用时,不需要访问内存直接从寄存器中读写,与存储在内存中的变量相比,寄存器变量具有更快的访问速度,在计算机的存储层次中,寄存器的速度最快,其次是内存,最慢的是硬盘。C语言中使用关键字register来声明局部变量为寄存器变量,需要注意的是,只有局部自动变量和形式参数才能够被定义为寄存器变量,全局变量和局部静态变量都不能被定义为寄存器变量。而且,一个计算机中寄存器数量是有限的,一般为2到3个,因此寄存器变量的数量不能太多。对于在一个函数中说明的多于2到3个的寄存器变量,C编译程序会自动地将寄存器变量变为自动变量。 受硬件寄存器长度的限制,寄存器变量只能是char、int或指针型,而不能使其他复杂数据类型。由于register变量使用的是硬件CPU中的寄存器,寄存器变量无地址,所以不能使用取地址运算符"&"求寄存器变量的地址。 + +GCC从4.8.0版本开始支持了另外一项特性:全局寄存器变量(Global Register Variables,[详细介绍](https://gcc.gnu.org/onlinedocs/gcc-6.1.0/gcc/Global-Register-Variables.html)),也就是可以把全局变量定义为寄存器变量,从而可以实现函数间共享数据。可以通过下面的语法告诉编译器使用寄存器来保存数据: +```c +register int *foo asm ("r12"); //r12、%r12 +``` +或者: +```c +register int *foo __asm__ ("r12"); //r12、%r12 +``` +这里r12就是指定使用的寄存器,它必须是运行平台上有效的寄存器,这样就可以像使用普通的变量一样使用foo,但是foo同样没有地址,也就是无法通过&获取它的地址,在gdb调试时也无法使用foo符号,只能使用对应的寄存器获取数据。举个例子来看: +```c +//main.c +#include + +typedef struct _execute_data { + int ip; +}zend_execute_data; + + +register zend_execute_data* execute_data __asm__ ("%r14"); + +int main(void) +{ + execute_data = (zend_execute_data *)malloc(sizeof(zend_execute_data)); + execute_data->ip = 9999; + + return 0; +} +``` +编译:`$ gcc -o main -g main.c`,然后通过gdb看下: +```sh +$ gdb main +(gdb) break main +(gdb) r +Starting program: /home/qinpeng/c/php/main + +Breakpoint 1, main () at main.c:12 +12 execute_data = (zend_execute_data *)malloc(sizeof(zend_execute_data)); +(gdb) n +13 execute_data->ip = 9999; +(gdb) n +15 return 0; +``` +这时我们就无法再像普通变量那样直接使用execute_data访问数据,只能通过r14寄存器读取: +```sh +(gdb) p execute_data +Missing ELF symbol "execute_data". +(gdb) info register r14 +r14 0x601010 6295568 +(gdb) p ((zend_execute_data *)$r14)->ip +$3 = 9999 +``` +了解完全局寄存器变量,接下来我们再回头看下PHP7中的用法,处理也比较简单,就是在execute_ex()执行各opcode指令的过程中,不再将execute_data作为参数传给handler,而是通过寄存器保存execute_data及opline的地址,handler使用时直接从全局变量(寄存器)读取,执行完再把下一条指令更新到全局变量。 + +该功能需要GCC 4.8+支持,默认开启,可以通过 --disable-gcc-global-regs 编译参数关闭。以x86_64为例,execute_data使用r14寄存器,opline使用r15寄存器: +```c +//file: zend_execute.c line: 2631 +# define ZEND_VM_FP_GLOBAL_REG "%r14" +# define ZEND_VM_IP_GLOBAL_REG "%r15" + +//file: zend_vm_execute.h line: 315 +register zend_execute_data* volatile execute_data __asm__(ZEND_VM_FP_GLOBAL_REG); +register const zend_op* volatile opline __asm__(ZEND_VM_IP_GLOBAL_REG); +``` +execute_data、opline定义为全局变量,下面看下execute_ex()的变化,展开后: +```c +ZEND_API void execute_ex(zend_execute_data *ex) +{ + const zend_op *orig_opline = opline; + zend_execute_data *orig_execute_data = execute_data; + + //将当前execute_data、opline保存到全局变量 + execute_data = ex; + opline = execute_data->opline + + while (1) { + ((opcode_handler_t)opline->handler)(); + + if (UNEXPECTED(!opline)) { + execute_data = orig_execute_data; + opline = orig_opline; + + return; + } + } +} +``` +这个时候调用各opcode指令的handler时就不再传入execute_data的参数了,handler使用时直接从全局变量读取,仍以上面的赋值ZEND_ASSIGN指令为例,handler展开后: +```c +static int ZEND_ASSIGN_SPEC_CV_CONST_HANDLER(void) +{ + ... + + //ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION() + opline = execute_data->opline + 1; + return; +} +``` +当调用函数时,会把execute_data、opline更新为被调函数的,然后回到execute_ex()开始执行被调函数的指令: +```c +# define ZEND_VM_ENTER() execute_data = EG(current_execute_data); LOAD_OPLINE(); ZEND_VM_CONTINUE() +``` +展开后: +```c +//ZEND_VM_ENTER() +execute_data = execute_data->current_execute_data; +opline = execute_data->opline; +return; +``` +这两种处理方式并没有本质上的差异,只是通过全局寄存器变量提升了一些性能。 + +> __Note:__ automake编译时的命令是cc,而不是gcc,如果更新gcc后发现PHP仍然没有支持这个特性,请检查下cc是否指向了新的gcc diff --git a/3/zend_object.md b/3/zend_object.md index cf44ef7..756d2fa 100644 --- a/3/zend_object.md +++ b/3/zend_object.md @@ -437,7 +437,7 @@ static int zend_std_compare_objects(zval *o1, zval *o2) "==="的比较通过函数`zend_is_identical()`处理,比较简单,这里不再展开。 #### 3.4.2.6 对象的销毁 -object与string、array等类型不同,它是个符合类型,所以它的销毁过程更加复杂,赋值、函数调用结束或主动unset等操作中如果发现object引用计数为0则将触发销毁动作。 +object与string、array等类型不同,它是个复合类型,所以它的销毁过程更加复杂,赋值、函数调用结束或主动unset等操作中如果发现object引用计数为0则将触发销毁动作。 ```php //情况1 $obj1 = new my_function(); diff --git a/4/break.md b/4/break.md index e6d8b7b..47ec5b8 100644 --- a/4/break.md +++ b/4/break.md @@ -19,7 +19,15 @@ typedef struct _zend_brk_cont_element { ``` cont记录的是当前循环判断条件opcode起始位置,brk记录的是当前循环结束的位置,parent记录的是父层循环`zend_brk_cont_element`结构的存储位置,也就是说多层嵌套循环会生成一个`zend_brk_cont_element`的链表,每层循环编译结束时更新自己的`zend_brk_cont_element`结构,所以break、continue的处理过程实际就是根据跳出的层级索引到那一层的`zend_brk_cont_element`结构,然后得到它的cont、brk进行相应的opcode跳转。 -各循环的`zend_brk_cont_element`结构保存在`zend_op_array->brk_cont_array`数组中,实际这个数组在编译前就已经分配好了,编译各循环时依次申请一个`zend_brk_cont_element`,`zend_op_array->last_brk_cont`记录此数组第一个可用位置,每申请一个元素last_brk_cont就相应的增加1,parent记录的就是父层循环在`zend_op_array->brk_cont_array`中的位置。 +各循环的`zend_brk_cont_element`结构保存在`zend_op_array->brk_cont_array`数组中,编译各循环时依次申请一个`zend_brk_cont_element`,`zend_op_array->last_brk_cont`记录此数组第一个可用位置,每申请一个元素last_brk_cont就相应的增加1,然后将数组扩容,parent记录的就是父层循环结构在该数组中的存储位置。 +```c +zend_brk_cont_element *get_next_brk_cont_element(zend_op_array *op_array) +{ + op_array->last_brk_cont++; + op_array->brk_cont_array = erealloc(op_array->brk_cont_array, sizeof(zend_brk_cont_element)*op_array->last_brk_cont); + return &op_array->brk_cont_array[op_array->last_brk_cont-1]; +} +``` 示例: ```php @@ -121,8 +129,10 @@ static uint32_t zend_get_brk_cont_target(const zend_op_array *op_array, const ze int array_offset = opline->op1.num;//break、continue所属循环zend_brk_cont_element的存储下标 zend_brk_cont_element *jmp_to; do { + //从break/continue所在循环层开始 jmp_to = &op_array->brk_cont_array[array_offset]; if (nest_levels > 1) { + //如果还没到要跳出的层数则接着跳到上层 array_offset = jmp_to->parent; } } while (--nest_levels > 0); diff --git a/4/include.md b/4/include.md index 8df3fd2..3e54ec8 100644 --- a/4/include.md +++ b/4/include.md @@ -94,7 +94,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INCLUDE_OR_EVAL_SPEC_CONST_HAN ![](../img/include_2.png) -> 注意:这里include文件中定义的var_1实际是替换了原文件中的变量,也就是只有一个var_1,所以此处zend_array的引用是1而不是2 +> 注意:这里include文件中定义的var_2实际是替换了原文件中的变量,也就是只有一个var_2,所以此处zend_array的引用是1而不是2 接下来就是被包含文件的执行,执行到`$var_2 = array()`时,将原array(1,2,3)引用减1变为0,这时候将其释放,然后将新的value:array()赋给$var_2,这个过程就是普通变量的赋值过程,注意此时调用文件中的$var_2仍然指向被释放掉的value,此时的内存关系: diff --git a/4/loop.md b/4/loop.md index 19ce394..f698d0e 100644 --- a/4/loop.md +++ b/4/loop.md @@ -241,7 +241,7 @@ foreach($arr as $k=>$v){ 了解了foreach的实现、运行机制我们再回头看下其编译过程: -* __(1)__ 编译"拷贝"数组/对象操作的opcode:`ZEND_FE_RESET_R`,如果value是引用则是`ZEND_FE_RESET_RW`,执行时如果发现数组或对象属性为空则直接跳出遍历,所以这条opcode还需要知道跳出的位置,这个位置需要编译完foreach以后才能确定; +* __(1)__ 编译拷贝数组、对象操作的指令:ZEND_FE_RESET_R,如果value是引用则是ZEND_FE_RESET_RW。执行时如果发现遍历的变量不是数组、对象,则抛出一个warning,然后跳出循环,所以这条指令还需要知道跳出的位置,这个位置需要编译完foreach以后才能确定; * __(2)__ 编译fetch数组/对象当前单元key、value的opcode:`ZEND_FE_FETCH_R`,如果是引用则是`ZEND_FE_FETCH_RW`,此opcode还需要知道当遍历已经到达数组末尾时跳出遍历的位置,与步骤(1)的opcode相同,另外还有一个关键操作,前面已经说过遍历的key、value实际就是普通的局部变量,它们的内存存储位置正是在这一步分配确定的,分配过程与普通局部变量的过程完全相同,如果value不是一个CV变量(比如:foreach($arr as $v["xx"]){...})则还会编译其它操作的opcode; * __(3)__ 如果foreach定义了key则编译一条赋值opcode,此操作是对key进行赋值; * __(4)__ 编译循环体statement; diff --git a/5/zend_alloc.md b/5/zend_alloc.md index 5fe35aa..df3fa05 100644 --- a/5/zend_alloc.md +++ b/5/zend_alloc.md @@ -151,7 +151,56 @@ static void *zend_mm_alloc_huge(zend_mm_heap *heap, size_t size ZEND_FILE_LINE_D return ptr; } ``` -huge的分配过程还是比较简单的。 +huge的分配实际就是分配多个chunk,chunk的分配也是large、small内存分配的基础,它是ZendMM向系统申请内存的唯一粒度。在申请chunk内存时有一个关键操作,那就是将内存地址对齐到ZEND_MM_CHUNK_SIZE,也就是说申请的chunk地址都是ZEND_MM_CHUNK_SIZE的整数倍,注意:这里说的内存对齐值并不是系统的字节对齐值,所以需要在申请后自己调整下。ZendMM的处理方法是:先按实际要申请的内存大小申请一次,如果系统分配的地址恰好是ZEND_MM_CHUNK_SIZE的整数倍那么就不需要调整了,直接返回使用;如果不是ZEND_MM_CHUNK_SIZE的整数倍,ZendMM会把这块内存释放掉,然后按照"实际要申请的内存大小+ZEND_MM_CHUNK_SIZE"的大小重新申请一块内存,多申请的ZEND_MM_CHUNK_SIZE大小的内存是用来调整的,ZendMM会从系统分配的地址向后偏移到ZEND_MM_CHUNK_SIZE的整数倍位置,调整完以后会把多余的内存再释放掉,如下图所示,虚线部分为alignment大小的内容,灰色部分为申请的内容大小,系统返回的地址为ptr1,而实际使用的内存是从ptr2开始的。 + +![](../img/chunk_alloc.png) + +下面看下chunk的具体分配过程: +```c +//size为申请内存的大小,alignment为内存对齐值,一般为ZEND_MM_CHUNK_SIZE +static void *zend_mm_chunk_alloc_int(size_t size, size_t alignment) +{ + //向系统申请size大小的内存 + void *ptr = zend_mm_mmap(size); + if (ptr == NULL) { + return NULL; + } else if (ZEND_MM_ALIGNED_OFFSET(ptr, alignment) == 0) {//判断申请的内存是否为alignment的整数倍 + //是的话直接返回 + return ptr; + }else{ + //申请的内存不是按照alignment对齐的,注意这里的alignment并不是系统的字节对齐值 + size_t offset; + + //将申请的内存释放掉重新申请 + zend_mm_munmap(ptr, size); + //重新申请一块内存,这里会多申请一块内存,用于截取到alignment的整数倍,可以忽略REAL_PAGE_SIZE + ptr = zend_mm_mmap(size + alignment - REAL_PAGE_SIZE); + //offset为ptr距离上一个alignment对齐内存位置的大小,注意不能往前移,因为前面的内存都是分配了的 + offset = ZEND_MM_ALIGNED_OFFSET(ptr, alignment); + if (offset != 0) { + offset = alignment - offset; + zend_mm_munmap(ptr, offset); + //偏移ptr,对齐到alignment + ptr = (char*)ptr + offset; + alignment -= offset; + } + if (alignment > REAL_PAGE_SIZE) { + zend_mm_munmap((char*)ptr + size, alignment - REAL_PAGE_SIZE); + } + return ptr; + } +} +``` +这个过程中用到了一个宏: +```c +#define ZEND_MM_ALIGNED_OFFSET(size, alignment) \ + (((size_t)(size)) & ((alignment) - 1)) +``` +这个宏的作用是计算按alignment对齐的内存地址距离上一个alignment整数倍内存地址的大小,alignment必须为2的n次方,比如一段n*alignment大小的内存,ptr为其中一个位置,那么就可以通过位运算计算得到ptr所属内存块的offset: + +![](../img/align.png) + +这个位运算是因为alignment为2^n,所以可以通过alignment取到最低位的位置,也就是相对上一个整数倍alignment的offset,实际如果不用运算的话可以通过:`offset = (ptr/alignment取整)*alignment - ptr`得到,这个更容易理解些。 #### 5.1.3.2 Large分配 大于3/4的page_size(4KB)且小于等于511个page_size的内存申请,也就是一个chunk的大小够用(之所以是511个page而不是512个是因为第一个page始终被chunk结构占用),__如果申请多个page的话 分配的时候这些page都是连续的__ 。 @@ -352,7 +401,9 @@ ZEND_API void ZEND_FASTCALL _efree(void *ptr ZEND_FILE_LINE_DC ZEND_FILE_LINE_OR static zend_always_inline void zend_mm_free_heap(zend_mm_heap *heap, void *ptr ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC) { - size_t page_offset = ZEND_MM_ALIGNED_OFFSET(ptr, ZEND_MM_CHUNK_SIZE); //根据内存地址及对齐值判断内存地址偏移量是否为0,是的话只有huge情况符合,page、slot分配出的内存地址偏移量一定是>=ZEND_MM_CHUNK_SIZE的,因为第一页始终被chunk自身结构占用,不可能分配出去 + //根据内存地址及对齐值判断内存地址偏移量是否为0,是的话只有huge情况符合,page、slot分配出的内存地>址偏移量一定是>=ZEND_MM_CHUNK_SIZE的,因为第一页始终被chunk自身结构占用,不可能分配出去 + //offset就是ptr距离当前chunk起始位置的偏移量 + size_t page_offset = ZEND_MM_ALIGNED_OFFSET(ptr, ZEND_MM_CHUNK_SIZE); if (UNEXPECTED(page_offset == 0)) { if (ptr != NULL) { @@ -360,6 +411,7 @@ static zend_always_inline void zend_mm_free_heap(zend_mm_heap *heap, void *ptr Z zend_mm_free_huge(heap, ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC); } } else { //page或slot,根据chunk->map[]值判断当前page的分配类型 + //根据ptr获取chunk的起始位置 zend_mm_chunk *chunk = (zend_mm_chunk*)ZEND_MM_ALIGNED_BASE(ptr, ZEND_MM_CHUNK_SIZE); int page_num = (int)(page_offset / ZEND_MM_PAGE_SIZE); zend_mm_page_info info = chunk->map[page_num]; @@ -377,6 +429,8 @@ static zend_always_inline void zend_mm_free_heap(zend_mm_heap *heap, void *ptr Z } } ``` +释放的内存地址可能是chunk中间的任意位置,因为chunk分配时是按照ZEND_MM_CHUNK_SIZE对齐的,也就是chunk的起始内存地址一定是ZEND_MM_CHUNK_SIZE的整数倍,所以可以根据chunk上的任意位置知道chunk的起始位置。 + 释放page的过程有一个地方值得注意,如果释放后发现当前chunk所有page都已经被释放则可能会释放所在chunk,还记得heap->cached_chunks吗?内存池会维持一定的chunk数,每次释放并不会直接销毁而是加入到cached_chunks中,这样下次申请chunk时直接就用了,同时为了防止占用过多内存,cached_chunks会根据每次request请求计算的chunk使用均值保证其维持在一定范围内。 每次request请求结束会对内存池进行一次清理,检查cache的chunk数是否超过均值,超过的话就进行清理,具体的操作:`zend_mm_shutdown`,这里不再展开。 diff --git a/7/class.md b/7/class.md new file mode 100644 index 0000000..3047459 --- /dev/null +++ b/7/class.md @@ -0,0 +1,54 @@ +## 7.9 面向对象 +### 7.9.1 定义内部类 +在扩展中定义一个内部类的方式与函数类似,函数最终注册到EG(function_table),而类则最终注册到EG(class_table)符号表中,注册的过程首先是为类创建一个zend_class_entry结构,然后把这个结构插入EG(class_table),当然这个过程不需要我们手动操作,PHP提供了现成的方法和宏帮我们对zend_class_entry进行初始化以及注册。通常情况下会把内部类的注册放到module startup阶段,也就是定义在扩展的`PHP_MINIT_FUNCTION()`中,一个简单的类的注册只需要以下几行: +```c +PHP_MINIT_FUNCTION(mytest) +{ + //分配一个zend_class_entry,这个结构只在注册时使用,所以分配在栈上即可 + zend_class_entry ce; + //对zend_class_entry进行初始化 + INIT_CLASS_ENTRY(ce, "MyClass", NULL); + //注册 + zend_register_internal_class(&ce); +} +``` +这样就成功定义了一个内部类,类名为"MyClass",只是这个类还没有任何的成员属性、成员方法,定义完成后重新编译、安装扩展,然后在PHP脚本中实例化这个类: +```php +$obj = new MyClass(); + +var_dump($obj); +``` +结果将输出: +``` +object(MyClass)#1 (0) { +} +``` +注册时传入的zend_class_entry并不是最终插入class_table符号表的结构,zend_register_internal_class()中会重新分配,所以注册时的这个结构分配在栈上即可,此结构的成员不需要手动定义,PHP提供了宏供扩展使用,扩展只需要提供类的主要信息即可,常用的两个宏: +```c +/** + * 初始化zend_class_entry + * class_container:zend_class_entry地址 + * class_name:类名 + * functions:成员方法数组 + */ +#define INIT_CLASS_ENTRY(class_container, class_name, functions) \ + INIT_OVERLOADED_CLASS_ENTRY(class_container, class_name, functions, NULL, NULL, NULL) + +/** + * 初始化zend_class_entry,带namespace + * class_container:zend_class_entry地址 + * ns:命名空间 + * class_name:类名 + * functions:成员方法数组 + */ +#define INIT_NS_CLASS_ENTRY(class_container, ns, class_name, functions) \ + INIT_CLASS_ENTRY(class_container, ZEND_NS_NAME(ns, class_name), functions) +``` + +### 7.9.2 定义成员属性 + +### 7.9.3 定义成员方法 + +### 7.9.4 定义常量 + +### 7.9.5 类的实例化 diff --git a/7/extension_intro.md b/7/extension_intro.md index f6aafc8..020a179 100644 --- a/7/extension_intro.md +++ b/7/extension_intro.md @@ -270,7 +270,7 @@ __(5)PHP_ADD_INCLUDE(path):__ 添加include路径,即:`gcc -Iinclude_dir`, __(6)PHP_CHECK_LIBRARY(library, function [, action-found [, action-not-found [, extra-libs]]]):__ 检查依赖的库中是否存在需要的function,action-found为存在时执行的动作,action-not-found为不存在时执行的动作,比如扩展里使用到线程pthread,检查pthread_create(),如果没找到则终止./configure执行: ```sh -PHP_ADD_INCLUDE(pthread, pthread_create, [], [ +PHP_CHECK_LIBRARY(pthread, pthread_create, [], [ AC_MSG_ERROR([not find pthread_create() in lib pthread]) ]) ``` diff --git a/7/func.md b/7/func.md index 0807f69..4c4a47c 100644 --- a/7/func.md +++ b/7/func.md @@ -67,7 +67,7 @@ const zend_function_entry mytest_functions[] = { #define ZEND_FENTRY(zend_name, name, arg_info, flags) { #zend_name, name, arg_info, (uint32_t) (sizeof(arg_info)/sizeof(struct _zend_internal_arg_info)-1), flags }, #define ZEND_FN(name) zif_##name ``` -最后将`zend_module_entry->functions`设置为`timeout_functions`即可: +最后将`zend_module_entry->functions`设置为`mytest_functions`即可: ```c zend_module_entry mytest_module_entry = { STANDARD_MODULE_HEADER, @@ -123,7 +123,7 @@ PHP_FUNCTION(my_func_1) ![](../img/internal_func_param.png) -注意:解析时除了整形、浮点型、布尔型是直接硬拷贝value外,其它解析到的变量只能是指针,arr为zend_execute_data上param_1的地址,即:`zval *arr = ¶m_1`,所以图中arr、param_1之间用的不是箭头指向,也就是说参数始终存储在zend_execute_data上,解析获取的是这些参数的地址。`zend_parse_parameters()`调用了`zend_parse_va_args()`进行处理,简单看下解析过程: +注意:解析时除了整形、浮点型、布尔型是直接硬拷贝value外,其它解析到的变量只能是指针,arr为zend_execute_data上param_1的地址,即:`zval *arr = ¶m_1`,也就是说参数始终存储在zend_execute_data上,解析获取的是这些参数的地址。`zend_parse_parameters()`调用了`zend_parse_va_args()`进行处理,简单看下解析过程: ```c //va就是定义的要解析到的各个变量的地址 static int zend_parse_va_args(int num_args, const char *type_spec, va_list *va, int flags) @@ -229,7 +229,7 @@ static zend_always_inline int zend_parse_arg_long(zval *arg, zend_long *dest, ze return 1; } ``` -> __Note:__ "l"与"L"的区别在于,当传参不是整形且转为整形后超过了整形的大小范围时,"L"将值调整为整形的最大或最小值,而"l"将报错,比如传的参数是字符串"9223372036854775808",转整形后超过了unsigned int64的最大值:0xFFFFFFFFFFFFFFFF,"L"将解析为0xFFFFFFFFFFFFFFFF。 +> __Note:__ "l"与"L"的区别在于,当传参不是整形且转为整形后超过了整形的大小范围时,"L"将值调整为整形的最大或最小值,而"l"将报错,比如传的参数是字符串"9223372036854775808"(0x7FFFFFFFFFFFFFFF + 1),转整形后超过了有符号int64的最大值:0x7FFFFFFFFFFFFFFF,所以如果是"L"将解析为0x7FFFFFFFFFFFFFFF。 #### 7.6.2.2 布尔型:b 通过"b"标识符表示将传入的参数解析为布尔型,解析到的变量必须是zend_bool: @@ -355,7 +355,7 @@ callable指函数或成员方法,如果参数是函数名称字符串、array( zend_fcall_info callable; //注意,这两个结构不能是指针 zend_fcall_info_cache call_cache; -if(zend_parse_parameters( +if(zend_parse_parameters(ZEND_NUM_ARGS(), "f", &callable, &call_cache) == FAILURE){ RETURN_FALSE; } ``` @@ -376,8 +376,25 @@ my_func_1(array($object, 'method')); #### 7.6.2.11 其它标识符 除了上面介绍的这些解析符号以外,还有几个有特殊用法的标识符:"|"、"+"、"*",它们并不是用来表示某种数据类型的。 -* __|:__ 表示此后的参数为可选参数,可以不传,比如解析规则为:"al|b",则可以传2个或3个参数,如果是:"alb",则必须传3个,否则将报错; -* __+/*:__ 用于可变参数,注意这里与PHP函数...的用法不太一样,PHP中可以把函数最后一个参数前加...,表示调用时可以传多个参数,这些参数都会插入...参数的数组中,"*/+"也表示这个参数是可变的,但内核中只能接收一个值,即使传了多个后面那些也解析不到,"*"、"+"的区别在于"*"表示可以不传可变参数,而"+"表示可变参数至少有一个。 +* __|:__ 表示此后的参数为可选参数,可以不传,比如解析规则为:"al|b",则可以传2个或3个参数,如果是:"alb",则必须传3个,否则将报错 +* __+、* :__ 用于可变参数,`+、*`的区别在于 * 表示可以不传可变参数,而 + 表示可变参数至少有一个。可变参数将被解析到zval数组,可以通过一个整形参数,用于获取具体的数量,例如: +```c +PHP_FUNCTION(my_func_1) +{ + zval *args; + int argc; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "+", &args, &argc) == FAILURE) { + return; + } + //... +} +``` +argc获取的就是可变参数的数量,args为参数数组,指向第一个参数,可以通过args[i]获取其它参数,比如这样传参: +```php +my_func_1(array(), 1, false, "ddd"); +``` +那么传入的4个参数就可以在解析后通过args[0]、args[1]、args[2]、args[3]获取。 ### 7.6.3 引用传参 上一节介绍了如何在内部函数中解析参数,这里还有一种情况没有讲到,那就是引用传参: @@ -497,7 +514,7 @@ echo $a; ### 7.6.4 函数返回值 调用内部函数时其返回值指针作为参数传入,这个参数为`zval *return_value`,如果函数有返回值直接设置此指针即可,需要特别注意的是设置返回值时需要增加其引用计数,举个例子来看: - ```c +```c PHP_FUNCTION(my_func_1) { zval *arr; @@ -510,8 +527,6 @@ PHP_FUNCTION(my_func_1) Z_ADDREF_P(arr); //设置返回值为数组: - //return_value->u1.type = IS_ARRAY; - //return_value->value->arr = arr->value->arr; ZVAL_ARR(return_value, Z_ARR_P(arr)); } ``` diff --git a/7/implement.md b/7/implement.md index bebf3d4..7c84d1c 100644 --- a/7/implement.md +++ b/7/implement.md @@ -1,5 +1,5 @@ ## 7.2 扩展的实现原理 -PHP中扩展通过`zend_module_entry`这个结构来表示,此结构定义了扩展的全部信息:扩展名、扩展版本、扩展提供的函数列表以及PHP四个执行阶段的hook函数等,每一个扩展都需要定义一个此结构的变量,而且这个变量的名称格式必须是:`{mudule_name}_module_entry`,内核正是通过这个结构获取到扩展提供的功能的。 +PHP中扩展通过`zend_module_entry`这个结构来表示,此结构定义了扩展的全部信息:扩展名、扩展版本、扩展提供的函数列表以及PHP四个执行阶段的hook函数等,每一个扩展都需要定义一个此结构的变量,而且这个变量的名称格式必须是:`{module_name}_module_entry`,内核正是通过这个结构获取到扩展提供的功能的。 扩展可以在编译PHP时一起编译(静态编译),也可以单独编译为动态库,动态库需要加入到php.ini配置中去,然后在`php_module_startup()`阶段把这些动态库加载到PHP中: ```c diff --git a/7/var.md b/7/var.md index 9869898..e615727 100644 --- a/7/var.md +++ b/7/var.md @@ -64,7 +64,66 @@ zval的类型通过`Z_TYPE(zval)`、`Z_TYPE_P(zval*)`两个宏获取,这个值 #define Z_PTR(zval) (zval).value.ptr #define Z_PTR_P(zval_p) Z_PTR(*(zval_p)) ``` -### 7.7.3 引用计数 +### 7.7.3 类型转换 +```c +//将原类型转为特定类型,会更改原来的值 +ZEND_API void ZEND_FASTCALL convert_to_long(zval *op); +ZEND_API void ZEND_FASTCALL convert_to_double(zval *op); +ZEND_API void ZEND_FASTCALL convert_to_long_base(zval *op, int base); +ZEND_API void ZEND_FASTCALL convert_to_null(zval *op); +ZEND_API void ZEND_FASTCALL convert_to_boolean(zval *op); +ZEND_API void ZEND_FASTCALL convert_to_array(zval *op); +ZEND_API void ZEND_FASTCALL convert_to_object(zval *op); + +#define convert_to_cstring(op) if (Z_TYPE_P(op) != IS_STRING) { _convert_to_cstring((op) ZEND_FILE_LINE_CC); } +#define convert_to_string(op) if (Z_TYPE_P(op) != IS_STRING) { _convert_to_string((op) ZEND_FILE_LINE_CC); } + +//获取格式化为long的值,不会更改原来的值,op类型为zval*,返回值为zend_long +#define zval_get_long(op) _zval_get_long((op)) +//获取格式化为double的值,返回值double +#define zval_get_double(op) _zval_get_double((op)) +//获取格式化为string的值,返回值zend_string * +#define zval_get_string(op) _zval_get_string((op)) + +//字符串转整形 +ZEND_API int ZEND_FASTCALL zend_atoi(const char *str, int str_len); +ZEND_API zend_long ZEND_FASTCALL zend_atol(const char *str, int str_len); + +//判断是否为true +#define zval_is_true(op) \ + zend_is_true(op) +``` +### 7.7.4 引用计数 +在扩展中操作与PHP用户空间相关的变量时需要考虑是否需要对其引用计数进行加减,比如下面这个例子: +```php +function test($arr){ + return $arr; +} + +$a = array(1,2); +$b = test($a); +``` +如果把函数test()用内部函数实现,这个函数接受了一个PHP用户空间传入的数组参数,然后又返回并赋值给了PHP用户空间的另外一个变量,这个时候就需要增加传入数组的refcount,因为这个数组由PHP用户空间分配,函数调用前refcount=1,传到内部函数时相当于赋值给了函数的参数,因此refcount增加了1变为2,这次增加在函数执行完释放参数时会减掉,等返回并赋值给$b后此时共有两个变量指向这个数组,所以内部函数需要增加refcount,增加的引用是给返回值的。test()翻译成内部函数: +```c +PHP_FUNCTION(test) +{ + zval *arr; + + if(zend_parse_parameters(ZEND_NUM_ARGS(), "a", &arr) == FAILURE){ + RETURN_FALSE; + } + //如果注释掉下面这句将导致core dumped + Z_TRY_ADDREF_P(arr); + RETURN_ARR(Z_ARR_P(arr)); +} +``` +那么在哪些情况下需要考虑设置引用计数呢?一个关键条件是:操作的是与PHP用户空间相关的变量,包括对用户空间变量的修改、赋值,要明确的一点是引用计数是用来解决多个变量指向同一个value问题的,所以在PHP中来回传递zval的时候就需要考虑下是不是要修改引用计数,下面总结下PHP中常见的会对引用计数进行操作的情况: +* __(1)变量赋值:__ 变量赋值是最常见的情况,一个用到引用计数的变量类型在初始赋值时其refcount=1,如果后面把此变量又赋值给了其他变量那么就会相应的增加其引用计数 +* __(2)数组操作:__ 如果把一个变量插入数组中那么就需要增加这个变量的引用计数,如果要删除一个数组元素则要相应的减少其引用 +* __(3)函数调用:__ 传参实际可以当做普通的变量赋值,将调用空间的变量赋值给被调函数空间的变量,函数返回时会销毁函数空间的变量,这时又会减掉传参的引用,这两个过程由内核完成,不需要扩展自己处理 +* __(4)成员属性:__ 当把一个变量赋值给对象的成员属性时需要增加引用计数 + +PHP中定义了以下宏用于引用计数的操作: ```c //获取引用数:pz类型为zval* #define Z_REFCOUNT_P(pz) zval_refcount_p(pz) @@ -96,7 +155,22 @@ zval的类型通过`Z_TYPE(zval)`、`Z_TYPE_P(zval*)`两个宏获取,这个值 #define Z_TRY_ADDREF(z) Z_TRY_ADDREF_P(&(z)) #define Z_TRY_DELREF(z) Z_TRY_DELREF_P(&(z)) ``` -### 7.7.4 字符串操作 +这些宏操作类型都是zval或zval*,如果需要操作具体value的引用计数可以使用以下宏: +```c +//直接获取zend_value的引用,可以直接通过这个宏修改value的refcount +#define GC_REFCOUNT(p) (p)->gc.refcount +``` +另外还有几个常用的宏: +```c +//判断zval是否用到引用计数机制 +#define Z_REFCOUNTED(zval) ((Z_TYPE_FLAGS(zval) & IS_TYPE_REFCOUNTED) != 0) +#define Z_REFCOUNTED_P(zval_p) Z_REFCOUNTED(*(zval_p)) + +//根据zval获取value的zend_refcounted头部 +#define Z_COUNTED(zval) (zval).value.counted +#define Z_COUNTED_P(zval_p) Z_COUNTED(*(zval_p)) +``` +### 7.7.5 字符串操作 PHP中字符串(即:zend_string)操作相关的宏及函数: ```c //创建zend_string @@ -145,8 +219,10 @@ zend_bool zend_string_equals(zend_string *s1, zend_string *s2); #define ZSTR_H(zstr) (zstr)->h //获取字符串哈希值 #define ZSTR_HASH(zstr) zend_string_hash_val(zstr) //计算字符串哈希值 ``` -### 7.7.5 数组操作 -#### 7.7.5.1 创建数组 +除了上面这些,还有很多字符串大小转换、字符串比较的API定义在zend_operators.h中,这里不再列举。 + +### 7.7.6 数组操作 +#### 7.7.6.1 创建数组 创建一个新的HashTable分为两步:首先是分配zend_array内存,这个可以通过`ZVAL_NEW_ARR()`宏分配,也可以自己直接分配;然后初始化数组,通过`zend_hash_init()`宏完成,如果不进行初始化数组将无法使用。 ```c #define zend_hash_init(ht, nSize, pHashFunction, pDestructor, persistent) \ @@ -166,7 +242,7 @@ uint32_t size; ZVAL_NEW_ARR(&array); zend_hash_init(Z_ARRVAL(array), size, NULL, ZVAL_PTR_DTOR, 0); ``` -#### 7.7.5.2 插入、更新元素 +#### 7.7.6.2 插入、更新元素 数组元素的插入、更新主要有三种情况:key为zend_string、key为普通字符串、key为数值索引,相关的宏及函数: ```c // 1) key为zend_string @@ -220,7 +296,7 @@ zend_hash_init(Z_ARRVAL(array), size, NULL, ZVAL_PTR_DTOR, 0); #define zend_hash_next_index_insert_new(ht, pData) \ _zend_hash_next_index_insert_new(ht, pData ZEND_FILE_LINE_CC) ``` -#### 7.7.5.3 查找元素 +#### 7.7.6.3 查找元素 ```c //根据zend_string key查找数组元素 ZEND_API zval* ZEND_FASTCALL zend_hash_find(const HashTable *ht, zend_string *key); @@ -242,7 +318,7 @@ ZEND_API zend_bool ZEND_FASTCALL zend_hash_index_exists(const HashTable *ht, zen //与zend_hash_num_elements()类似,会有一些特殊处理 ZEND_API uint32_t zend_array_count(HashTable *ht); ``` -#### 7.7.5.4 删除元素 +#### 7.7.6.4 删除元素 ```c //删除key ZEND_API int ZEND_FASTCALL zend_hash_del(HashTable *ht, zend_string *key); @@ -254,7 +330,7 @@ ZEND_API int ZEND_FASTCALL zend_hash_str_del_ind(HashTable *ht, const char *key, ZEND_API int ZEND_FASTCALL zend_hash_index_del(HashTable *ht, zend_ulong h); ZEND_API void ZEND_FASTCALL zend_hash_del_bucket(HashTable *ht, Bucket *p); ``` -#### 7.7.5.5 遍历 +#### 7.7.6.5 遍历 数组遍历类似foreach的用法,在扩展中可以通过如下的方式遍历: ```c zval *val; @@ -298,7 +374,7 @@ ZEND_HASH_FOREACH_VAL(ht, val) { _key = _p->key; \ _val = _z; ``` -#### 7.7.5.6 其它操作 +#### 7.7.6.6 其它操作 ```c //合并两个数组,将source合并到target,overwrite为元素冲突时是否覆盖 #define zend_hash_merge(target, source, pCopyConstructor, overwrite) \ @@ -313,7 +389,7 @@ ZEND_API HashTable* ZEND_FASTCALL zend_array_dup(HashTable *source); ``` 数组排序,compare_func为typedef int (*compare_func_t)(const void *, const void *),需要自己定义比较函数,参数类型为Bucket*,renumber表示是否更改键值,如果为1则会在排序后重新生成各元素的h。PHP中的sort()、rsort()、ksort()等都是基于这个函数实现的。 -#### 7.7.5.7 销毁数组 +#### 7.7.6.7 销毁数组 ```c ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht); ``` diff --git a/8/namespace.md b/8/namespace.md new file mode 100644 index 0000000..58ac9fe --- /dev/null +++ b/8/namespace.md @@ -0,0 +1,439 @@ +## 8.1 概述 +什么是命名空间?从广义上来说,命名空间是一种封装事物的方法。在很多地方都可以见到这种抽象概念。例如,在操作系统中目录用来将相关文件分组,对于目录中的文件来说,它就扮演了命名空间的角色。具体举个例子,文件 foo.txt 可以同时在目录/home/greg 和 /home/other 中存在,但在同一个目录中不能存在两个 foo.txt 文件。另外,在目录 /home/greg 外访问 foo.txt 文件时,我们必须将目录名以及目录分隔符放在文件名之前得到 /home/greg/foo.txt。这个原理应用到程序设计领域就是命名空间的概念。(引用自php.net) + +命名空间主要用来解决两类问题: +* 用户编写的代码与PHP内部的或第三方的类、函数、常量、接口名字冲突 +* 为很长的标识符名称创建一个别名的名称,提高源代码的可读性 + +PHP命名空间提供了一种将相关的类、函数、常量和接口组合到一起的途径,不同命名空间的类、函数、常量、接口相互隔离不会冲突,注意:PHP命名空间只能隔离类、函数、常量和接口,不包括全局变量。 + +接下来的两节将介绍下PHP命名空间的内部实现,主要从命名空间的定义及使用两个方面分析。 + +## 8.2 命名空间的定义 +### 8.2.1 定义语法 +命名空间通过关键字namespace 来声明,如果一个文件中包含命名空间,它必须在其它所有代码之前声明命名空间,除了declare关键字以外,也就是说除declare之外任何代码都不能在namespace之前声明。另外,命名空间并没有文件限制,可以在多个文件中声明同一个命名空间,也可以在同一文件中声明多个命名空间。 +```php +namespace com\aa; + +const MY_CONST = 1234; +function my_func(){ /* ... */ } +class my_class { /* ... */ } +``` +另外也可以通过{}将类、函数、常量封装在一个命名空间下: +```php +namespace com\aa{ + const MY_CONST = 1234; + function my_func(){ /* ... */ } + class my_class { /* ... */ } +} +``` +但是同一个文件中这两种定义方式不能混用,下面这样的定义将是非法的: +```php +namespace com\aa{ + /* ... */ +} + +namespace com\bb; +/* ... */ +``` +如果没有定义任何命名空间,所有的类、函数和常量的定义都是在全局空间,与 PHP 引入命名空间概念前一样。 + +### 8.2.2 内部实现 +命名空间的实现实际比较简单,当声明了一个命名空间后,接下来编译类、函数和常量时会把类名、函数名和常量名统一加上命名空间的名称作为前缀存储,也就是说声明在命名空间中的类、函数和常量的实际名称是被修改过的,这样来看他们与普通的定义方式是没有区别的,只是这个前缀是内核帮我们自动添加的,例如: +```php +//ns_define.php +namespace com\aa; + +const MY_CONST = 1234; +function my_func(){ /* ... */ } +class my_class { /* ... */ } +``` +最终MY_CONST、my_func、my_class在EG(zend_constants)、EG(function_table)、EG(class_table)中的实际存储名称被修改为:com\aa\MY_CONST、com\aa\my_func、com\aa\my_class。 + +下面具体看下编译过程,namespace语法被编译为ZEND_AST_NAMESPACE类型的语法树节点,它有两个子节点:child[0]为命名空间的名称、child[1]为通过{}方式定义时包裹的语句。 + +![](../img/ast_namespace.png) + +此节点的编译函数为zend_compile_namespace(): +```c +void zend_compile_namespace(zend_ast *ast) +{ + zend_ast *name_ast = ast->child[0]; + zend_ast *stmt_ast = ast->child[1]; + zend_string *name; + zend_bool with_bracket = stmt_ast != NULL; + + //检查声明方式,不允许{}与非{}混用 + ... + + if (FC(current_namespace)) { + zend_string_release(FC(current_namespace)); + } + + if (name_ast) { + name = zend_ast_get_str(name_ast); + + if (ZEND_FETCH_CLASS_DEFAULT != zend_get_class_fetch_type(name)) { + zend_error_noreturn(E_COMPILE_ERROR, "Cannot use '%s' as namespace name", ZSTR_VAL(name)); + } + //将命名空间名称保存到FC(current_namespace) + FC(current_namespace) = zend_string_copy(name); + } else { + FC(current_namespace) = NULL; + } + + //重置use导入的命名空间符号表 + zend_reset_import_tables(); + ... + if (stmt_ast) { + //如果是通过namespace xxx { ... }这种方式声明的则直接编译{}中的语句 + zend_compile_top_stmt(stmt_ast); + zend_end_namespace(); + } +} +``` +从上面的编译过程可以看出,命名空间定义的编译过程非常简单,最主要的操作是把FC(current_namespace)设置为当前定义的命名空间名称,FC()这个宏为:CG(file_context),前面曾介绍过,file_context是在编译过程中使用的一个结构: +```c +typedef struct _zend_file_context { + zend_declarables declarables; + znode implementing_class; + + //当前所属namespace + zend_string *current_namespace; + //是否在namespace中 + zend_bool in_namespace; + //当前namespace是否为{}定义 + zend_bool has_bracketed_namespaces; + + //下面这三个值在后面介绍use时再说明,这里忽略即可 + HashTable *imports; + HashTable *imports_function; + HashTable *imports_const; +} zend_file_context; +``` +编译完namespace声明语句后接着编译下面的语句,此后定义的类、函数、常量均属于此命名空间,直到遇到下一个namespace的定义,接下来继续分析下这三种类型编译过程中有何不同之处。 + +__(1)编译类、函数__ + +前面章节曾详细介绍过函数、类的编译过程,总结下主要分为两步:第1步是编译函数、类,这个过程将分别生成一条ZEND_DECLARE_FUNCTION、ZEND_DECLARE_CLASS的opcode;第2步是在整个脚本编译的最后执行zend_do_early_binding(),这一步相当于执行ZEND_DECLARE_FUNCTION、ZEND_DECLARE_CLASS,函数、类正是在这一步注册到EG(function_table)、EG(class_table)中去的。 + +在生成ZEND_DECLARE_FUNCTION、ZEND_DECLARE_CLASS两条opcode时会把函数名、类名的存储位置通过操作数记录下来,然后在zend_do_early_binding()阶段直接获取函数名、类名作为key注册到EG(function_table)、EG(class_table)中,定义在命名空间中的函数、类的名称修改正是在生成ZEND_DECLARE_FUNCTION、ZEND_DECLARE_CLASS时完成的,下面以函数为例看下具体的处理: +```c +//函数的编译方法 +void zend_compile_func_decl(znode *result, zend_ast *ast) +{ + ... + //生成函数声明的opcode:ZEND_DECLARE_FUNCTION + zend_begin_func_decl(result, op_array, decl); + + //编译参数、函数体 + ... +} +``` +```c +static void zend_begin_func_decl(znode *result, zend_op_array *op_array, zend_ast_decl *decl) +{ + ... + //获取函数名称 + op_array->function_name = name = zend_prefix_with_ns(unqualified_name); + lcname = zend_string_tolower(name); + + if (FC(imports_function)) { + //如果通过use导入了其他命名空间则检查函数名称是否已存在 + } + .... + //生成一条opcode:ZEND_DECLARE_FUNCTION + opline = get_next_op(CG(active_op_array)); + opline->opcode = ZEND_DECLARE_FUNCTION; + //函数名的存储位置记录在op2中 + opline->op2_type = IS_CONST; + LITERAL_STR(opline->op2, zend_string_copy(lcname)); + ... +} +``` +函数名称通过zend_prefix_with_ns()方法获取: +```c +zend_string *zend_prefix_with_ns(zend_string *name) { + if (FC(current_namespace)) { + //如果当前是在namespace下则拼上namespace名称作为前缀 + zend_string *ns = FC(current_namespace); + return zend_concat_names(ZSTR_VAL(ns), ZSTR_LEN(ns), ZSTR_VAL(name), ZSTR_LEN(name)); + } else { + return zend_string_copy(name); + } +} +``` +在zend_prefix_with_ns()方法中如果发现FC(current_namespace)不为空则将函数名加上FC(current_namespace)作为前缀,接下来向EG(function_table)注册时就使用修改后的函数名作为key,类的情况与函数的处理方式相同,不再赘述。 + +__(2)编译常量__ + +常量的编译过程与函数、类基本相同,也是在编译过程获取常量名时检查FC(current_namespace)是否为空,如果不为空表示常量声明在namespace下,则为常量名加上FC(current_namespace)前缀。 + +总结下命名空间的定义:编译时如果发现定义了一个namespace,则将命名空间名称保存到FC(current_namespace),编译类、函数、常量时先判断FC(current_namespace)是否为空,如果为空则按正常名称编译,如果不为空则将类名、函数名、常量名加上FC(current_namespace)作为前缀,然后再以修改后的名称注册。整个过程相当于PHP帮我们补全了类名、函数名、常量名。 + +## 8.3 命名空间的使用 +### 8.3.1 基本用法 +上一节我们知道了定义在命名空间中的类、函数和常量只是加上了namespace名称作为前缀,既然是这样那么在使用时加上同样的前缀是否就可以了呢?答案是肯定的,比如上面那个例子:在com\aa命名空间下定义了一个常量MY_CONST,那么就可以这么使用: +```php +include 'ns_define.php'; + +echo \com\aa\MY_CONST; +``` +这种按照实际类名、函数名、常量名使用的方式很容易理解,与普通的类型没有差别,这种以"\"开头使用的名称称之为:完全限定名称,类似于绝对目录的概念,使用这种名称PHP会直接根据"\"之后的名称去对应的符号表中查找(namespace定义时前面是没有加"\"的,所以查找时也会去掉这个字符)。 + +除了这种形式的名称之外,还有两种形式的名称: +* __非限定名称:__ 即没有加任何namespace前缀的普通名称,比如my_func(),使用这种名称时如果当前有命名空间则会被解析为:currentnamespace\my_func,如果当前没有命名空间则按照原始名称my_func解析 +* __部分限定名称:__ 即包含namespace前缀,但不是以"\"开始的,比如:aa\my_func(),类似相对路径的概念,这种名称解析规则比较复杂,如果当前空间没有使用use导入任何namespace那么与非限定名称的解析规则相同,即如果当前有命名空间则会把解析为:currentnamespace\aa\my_func,否则解析为aa\my_func,使用use的情况后面再作说明 + +### 8.3.2 use导入 +使用一个命名空间中的类、函数、常量虽然可以通过完全限定名称的形式访问,但是这种方式需要在每一处使用的地方都加上完整的namespace名称,如果将来namespace名称变更了就需要所有使用的地方都改一遍,这将是很痛苦的一件事,为此,PHP提供了一种命名空间导入/别名的机制,可以通过use关键字将一个命名空间导入或者定义一个别名,然后在使用时就可以通过导入的namespace名称最后一个域或者别名访问,不需要使用完整的名称,比如: +```php +//ns_define.php +namespace aa\bb\cc\dd; + +const MY_CONST = 1234; +``` +可以采用如下几种方式使用: +```php +//方式1: +include 'ns_define.php'; + +use aa\bb\cc\dd; + +echo dd\MY_CONST; +``` +```php +//方式2: +include 'ns_define.php'; + +use aa\bb\cc; + +echo cc\dd\MY_CONST; +``` +```php +//方式3: +include 'ns_define.php'; + +use aa\bb\cc\dd as DD; + +echo DD\MY_CONST; +``` +```php +//方式4: +include 'ns_define.php'; + +use aa\bb\cc as CC; + +echo CC\dd\MY_CONST; +``` +这种机制的实现原理也比较简单:编译期间如果发现use语句 ,那么就将把这个use后的命名空间名称插入一个哈希表:FC(imports),而哈希表的key就是定义的别名,如果没有定义别名则key使用按"\"分割的最后一节,比如方式2的情况将以cc作为key,即:FC(imports)["cc"] = "aa\bb\cc\dd";接下来在使用类、函数和常量时会把名称按"\"分割,然后以第一节为key查找FC(imports),如果找到了则将FC(imports)中保存的名称与使用时的名称拼接在一起,组成完整的名称。实际上这种机制是把完整的名称切割缩短然后缓存下来,使用时再拼接成完整的名称,也就是内核帮我们组装了名称,对内核而言,最终使用的都是包括完整namespace的名称。 + +![](../img/namespace_com.png) + +use除了上面介绍的用法外还可以导入一个类,导入后再使用类就不需要加namespace了,例如: +```php +//ns_define.php +namespace aa\bb\cc\dd; + +class my_class { /* ... */ } +``` +```php +include 'ns_define.php'; +//导入一个类 +use aa\bb\cc\dd\my_class; +//直接使用 +$obj = new my_class(); +var_dump($obj); +``` +use的这两种用法实现原理是一样的,都是在编译时通过查找FC(imports)实现的名称补全。从PHP 5.6起,use又提供了两种针对函数、常量的导入,可以通过`use function xxx`及`use const xxx`导入一个函数、常量,这种用法的实现原理与上面介绍的实际是相同,只是在编译时没有保存到FC(imports),zend_file_context结构中的另外两个哈希表就是在这种情况下使用的: +```c +typedef struct _zend_file_context { + ... + //用于保存导入的类或命名空间 + HashTable *imports; + //用于保存导入的函数 + HashTable *imports_function; + //用于保存导入的常量 + HashTable *imports_const; +} zend_file_context; +``` +简单总结下use的几种不同用法: +* __a.导入命名空间:__ 导入的名称保存在FC(imports)中,编译使用的语句时搜索此符号表进行补全 +* __b.导入类:__ 导入的名称保存在FC(imports)中,与a不同的是不会根据"\"切割后的最后一节检索,而是直接使用类名查找 +* __c.导入函数:__ 通过`use function`导入到FC(imports_function),补全时先查找FC(imports_function),如果没有找到则继续按照a的情况处理 +* __d.导入常量:__ 通过`use const`导入到FC(imports_const),补全时先查找FC(imports_const),如果没有找到则继续按照a的情况处理 + +```php +use aa\bb; //导入namespace +use aa\bb\MY_CLASS; //导入类 +use function aa\bb\my_func; //导入函数 +use const aa\bb\MY_CONST; //导入常量 +``` +接下来看下内核的具体实现,首先看下use的编译: +```c +void zend_compile_use(zend_ast *ast) +{ + zend_string *current_ns = FC(current_namespace); + //use的类型 + uint32_t type = ast->attr; + //根据类型获取存储哈希表:FC(imports)、FC(imports_function)、FC(imports_const) + HashTable *current_import = zend_get_import_ht(type); + ... + //use可以同时导入多个 + for (i = 0; i < list->children; ++i) { + zend_ast *use_ast = list->child[i]; + zend_ast *old_name_ast = use_ast->child[0]; + zend_ast *new_name_ast = use_ast->child[1]; + //old_name为use后的namespace名称,new_name为as定义的别名 + zend_string *old_name = zend_ast_get_str(old_name_ast); + zend_string *new_name, *lookup_name; + + if (new_name_ast) { + //如果有as别名则直接使用 + new_name = zend_string_copy(zend_ast_get_str(new_name_ast)); + } else { + const char *unqualified_name; + size_t unqualified_name_len; + if (zend_get_unqualified_name(old_name, &unqualified_name, &unqualified_name_len)) { + //按"\"分割,取最后一节为new_name + new_name = zend_string_init(unqualified_name, unqualified_name_len, 0); + } else { + //名称中没有"\":use aa + new_name = zend_string_copy(old_name); + } + } + //如果是use const则大小写敏感,其它用法都转为小写 + if (case_sensitive) { + lookup_name = zend_string_copy(new_name); + } else { + lookup_name = zend_string_tolower(new_name); + } + ... + if (current_ns) { + //如果当前是在命名空间中则需要检查名称是否冲突 + ... + } + + //插入FC(imports/imports_function/imports_const),key为lookup_name,value为old_name + if (!zend_hash_add_ptr(current_import, lookup_name, old_name)) { + ... + } + } +} +``` +从use的编译过程可以看到,编译时的主要处理是把use导入的名称以别名或最后分节为key存储到对应的哈希表中,接下来我们看下在编译使用类、函数、常量的语句时是如何处理的。使用的语法类型比较多,比如类的使用就有new、访问静态属性、调用静态方法等,但是不管什么语句都会经历获取类名、函数名、常量名这一步,类名的补全就是在这一步完成的。 + +__(1)补全类名__ + +编译时通过zend_resolve_class_name()方法进行类名补全,如果没有任何namespace那么就返回原始的类名,比如编译`new my_class()`时,首先会把"my_class"传入该函数,如果查找FC(imports)后发现是一个use导入的类则把补全后的完整名称返回,然后再进行后续的处理。 +```c +zend_string *zend_resolve_class_name(zend_string *name, uint32_t type) +{ + char *compound; + //"namespace\xxx\类名"这种用法表示使用当前命名空间 + if (type == ZEND_NAME_RELATIVE) { + return zend_prefix_with_ns(name); + } + + //完全限定的形式:new \aa\bb\my_class() + if (type == ZEND_NAME_FQ || ZSTR_VAL(name)[0] == '\\') { + if (ZSTR_VAL(name)[0] == '\\') { + name = zend_string_init(ZSTR_VAL(name) + 1, ZSTR_LEN(name) - 1, 0); + } else { + zend_string_addref(name); + } + ... + return name; + } + + //如果当前脚本有通过use导入namespace + if (FC(imports)) { + compound = memchr(ZSTR_VAL(name), '\\', ZSTR_LEN(name)); + if (compound) { + // 1) 没有直接导入一个类的情况,用法a + //名称中包括"\",比如:new aa\bb\my_class() + size_t len = compound - ZSTR_VAL(name); + //根据按"\"分割后的最后一节为key查找FC(imports) + zend_string *import_name = + zend_hash_find_ptr_lc(FC(imports), ZSTR_VAL(name), len); + //如果找到了表示通过use导入了namespace + if (import_name) { + return zend_concat_names( + ZSTR_VAL(import_name), ZSTR_LEN(import_name), ZSTR_VAL(name) + len + 1, ZSTR_LEN(name) - len - 1); + } + } else { + // 2) 通过use导入一个类的情况,用法b + //直接根据原始类名查找 + zend_string *import_name + = zend_hash_find_ptr_lc(FC(imports), ZSTR_VAL(name), ZSTR_LEN(name)); + + if (import_name) { + return zend_string_copy(import_name); + } + } + } + //没有使用use或没命中任何use导入的namespace,按照基本用法处理:如果当前在一个namespace下则解释为currentnamespace\my_class + return zend_prefix_with_ns(name); +} +``` +此方法除了类的名称后还有一个type参数,这个参数是解析语法是根据使用方式确定的,共有三种类型: +* __ZEND_NAME_NOT_FQ:__ 非限定名称,也就是普通的类名,没有加namespace,比如:new my_class() +* __ZEND_NAME_RELATIVE:__ 相对名称,强制按照当前所属命名空间解析,使用时通过在类前加"namespace\xx",比如:new namespace\my_class(),如果当前是全局空间则等价于:new my_class,如果当前命名空间为currentnamespace,则解析为"currentnamespace\my_class" +* __ZEND_NAME_FQ:__ 完全限定名称,即以"\"开头的 + +__(2)补全函数名、常量名__ + +函数与常量名称的补全操作是相同的: +```c +//补全函数名称 +zend_string *zend_resolve_function_name(zend_string *name, uint32_t type, zend_bool *is_fully_qualified) +{ + return zend_resolve_non_class_name( + name, type, is_fully_qualified, 0, FC(imports_function)); +} +//补全常量名称 +zend_string *zend_resolve_const_name(zend_string *name, uint32_t type, zend_bool *is_fully_qualified) + return zend_resolve_non_class_name( + name, type, is_fully_qualified, 1, FC(imports_const)); +} +``` +可以看到函数与常量最终调用同一方法处理,不同点在于传入了各自的存储哈希表: +```c +zend_string *zend_resolve_non_class_name( + zend_string *name, uint32_t type, zend_bool *is_fully_qualified, + zend_bool case_sensitive, HashTable *current_import_sub +) { + char *compound; + *is_fully_qualified = 0; + //完整名称,直接返回,不需要补全 + if (ZSTR_VAL(name)[0] == '\\') { + *is_fully_qualified = 1; + return zend_string_init(ZSTR_VAL(name) + 1, ZSTR_LEN(name) - 1, 0); + } + //与类的用法相同 + if (type == ZEND_NAME_RELATIVE) { + *is_fully_qualified = 1; + return zend_prefix_with_ns(name); + } + //current_import_sub如果是函数则为FC(imports_function),否则为FC(imports_const) + if (current_import_sub) { + //查找FC(imports_function)或FC(imports_const) + ... + } + //查找FC(imports) + compound = memchr(ZSTR_VAL(name), '\\', ZSTR_LEN(name)); + ... + + return zend_prefix_with_ns(name); +} +``` +可以看到,函数与常量的的补全逻辑只是优先用原始名称去FC(imports_function)或FC(imports_const)查找,如果没有找到再去FC(imports)中匹配。如果我们这样导入了一个函数:`use function aa\bb\my_func;`,编译`my_func()`会在FC(imports_function)中根据"my_func"找到"aa\bb\my_func",从而使用完整的这个名称。 + +### 8.3.3 动态用法 +前面介绍的这些命名空间的使用都是名称为CONST类型的情况,所有的处理都是在编译环节完成的,PHP是动态语言,能否动态使用命名空间呢?举个例子: +```php +$class_name = "\aa\bb\my_class"; +$obj = new $class_name; +``` +如果类似这样的用法只能只用完全限定名称,也就是按照实际存储的名称使用,无法进行自动名称补全。 + diff --git a/README.md b/README.md index 60435e4..566fac0 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,27 @@ 代码版本:php-7.0.12 ```` +## 反馈 +[交流&吐槽](https://github.com/pangudashu/php7-internal/issues/3) [错误反馈](https://github.com/pangudashu/php7-internal/issues/2) + +## 纸质版 +
+ +
+ +[京东](https://item.jd.com/12267210.html) +[当当](http://product.dangdang.com/25185400.html) + ## 目录: * 第1章 PHP基本架构 * 1.1 PHP简介 * 1.2 PHP7的改进 - * 1.3 PHP内核的组成 + * [1.3 FPM](1/fpm.md) + * [1.3.1 概述](1/fpm.md) + * [1.3.2 基本实现](1/fpm.md) + * [1.3.3 FPM的初始化](1/fpm.md) + * [1.3.4 请求处理](1/fpm.md) + * [1.3.5 进程管理](1/fpm.md) * [1.4 PHP执行的几个阶段](1/base_process.md) * 第2章 变量 * [2.1 变量的内部实现](2/zval.md) @@ -27,6 +43,7 @@ * 3.3.1 基本结构 * 3.3.2 执行流程 * 3.3.3 函数的执行流程 + * [3.3.4 全局execute_data和opline](3/zend_global_register.md) * 3.4 面向对象实现 * [3.4.1 类](3/zend_class.md) * [3.4.2 对象](3/zend_object.md) @@ -35,6 +52,10 @@ * [3.4.5 魔术方法](3/zend_magic_method.md) * [3.4.6 类的自动加载](3/zend_autoload.md) * [3.5 运行时缓存](3/zend_runtime_cache.md) + * 3.6 Opcache + * 3.6.1 opcode缓存 + * 3.6.2 opcode优化 + * 3.6.3 JIT * 第4章 PHP基础语法实现 * [4.1 类型转换](4/type.md) * [4.2 选择结构](4/if.md) @@ -69,20 +90,35 @@ * [7.7 zval的操作](7/var.md) * [7.7.1 新生成各类型zval](7/var.md) * [7.7.2 获取zval的值及类型](7/var.md) - * [7.7.3 引用计数](7/var.md) - * [7.7.4 字符串操作](7/var.md) - * [7.7.5 数组操作](7/var.md) + * [7.7.3 类型转换](7/var.md) + * [7.7.4 引用计数](7/var.md) + * [7.7.5 字符串操作](7/var.md) + * [7.7.6 数组操作](7/var.md) * [7.8 常量](7/constant.md) * 7.9 面向对象 - * 7.6.1 扩展中创建对象 - * 7.6.2 创建内部类 + * 7.9.1 内部类注册 + * 7.9.2 定义成员属性 + * 7.9.3 定义成员方法 + * 7.9.4 定义常量 + * 7.9.5 类的实例化 * 7.10 资源类型 * 7.11 经典扩展解析 * 7.8.1 Yaf * 7.8.2 Redis - * 7.8.3 Memcached - -## 反馈 -[交流&吐槽](https://github.com/pangudashu/php7-internal/issues/3) [错误反馈](https://github.com/pangudashu/php7-internal/issues/2) - +* 第8章 命名空间 + * [8.1 概述](8/namespace.md) + * [8.2 命名空间的定义](8/namespace.md) + * [8.2.1 定义语法](8/namespace.md) + * [8.2.2 内部实现](8/namespace.md) + * [8.3 命名空间的使用](8/namespace.md) + * [8.3.1 基本用法](8/namespace.md) + * [8.3.2 use导入](8/namespace.md) + * [8.3.3 动态用法](8/namespace.md) + +## 实现PHP新特性 + * [1、break/continue按标签中断语法实现](try/break.md) + * 2、defer语法 + * 3、协程 + * 3.1 协程的原理 + * 3.2 上下文切换 diff --git a/SUMMARY.md b/SUMMARY.md new file mode 100644 index 0000000..d26efd0 --- /dev/null +++ b/SUMMARY.md @@ -0,0 +1,108 @@ +# PHP7-internal + +## 目录 + +* 第1章 PHP基本架构 + * 1.1 PHP简介 + * 1.2 PHP7的改进 + * [1.3 FPM](1/fpm.md) + * [1.3.1 概述](1/fpm.md) + * [1.3.2 基本实现](1/fpm.md) + * [1.3.3 FPM的初始化](1/fpm.md) + * [1.3.4 请求处理](1/fpm.md) + * [1.3.5 进程管理](1/fpm.md) + * [1.4 PHP执行的几个阶段](1/base_process.md) +* 第2章 变量 + * [2.1 变量的内部实现](2/zval.md) + * [2.2 数组](2/zend_ht.md) + * [2.3 静态变量](2/static_var.md) + * [2.4 全局变量](2/global_var.md) + * [2.5 常量](2/zend_constant.md) +* 第3章 Zend虚拟机 + * [3.1 PHP代码的编译](3/zend_compile.md) + * [3.1.1 词法解析、语法解析](3/zend_compile_parse.md) + * [3.1.2 抽象语法树编译流程](3/zend_compile_opcode.md) + * [3.2 函数实现](3/function_implement.md) + * [3.2.1 内部函数](3/function_implement.md) + * 3.2.2 用户函数的实现 + * [3.3 Zend引擎执行流程](3/zend_executor.md) + * 3.3.1 基本结构 + * 3.3.2 执行流程 + * 3.3.3 函数的执行流程 + * [3.3.4 全局execute_data和opline](3/zend_global_register.md) + * 3.4 面向对象实现 + * [3.4.1 类](3/zend_class.md) + * [3.4.2 对象](3/zend_object.md) + * [3.4.3 继承](3/zend_extends.md) + * [3.4.4 动态属性](3/zend_prop.md) + * [3.4.5 魔术方法](3/zend_magic_method.md) + * [3.4.6 类的自动加载](3/zend_autoload.md) + * [3.5 运行时缓存](3/zend_runtime_cache.md) + * 3.6 Opcache + * 3.6.1 opcode缓存 + * 3.6.2 opcode优化 + * 3.6.3 JIT +* 第4章 PHP基础语法实现 + * [4.1 类型转换](4/type.md) + * [4.2 选择结构](4/if.md) + * [4.3 循环结构](4/loop.md) + * [4.4 中断及跳转](4/break.md) + * [4.5 include/require](4/include.md) + * [4.6 异常处理](4/exception.md) +* 第5章 内存管理 + * [5.1 Zend内存池](5/zend_alloc.md) + * [5.2 垃圾回收](5/gc.md) +* 第6章 线程安全 + * [6.1 什么是线程安全](6/ts.md) + * [6.2 线程安全资源管理器](6/ts.md) +* 第7章 扩展开发 + * [7.1 概述](7/intro.md) + * [7.2 扩展的实现原理](7/implement.md) + * [7.3 扩展的构成及编译](7/extension_intro.md) + * [7.3.1 扩展的构成](7/extension_intro.md) + * [7.3.2 编译工具](7/extension_intro.md) + * [7.3.3 编写扩展的基本步骤](7/extension_intro.md) + * [7.3.4 config.m4](7/extension_intro.md) + * [7.4 钩子函数](7/hook.md) + * [7.5 运行时配置](7/conf.md) + * [7.5.1 全局变量](7/conf.md) + * [7.5.2 ini配置](7/conf.md) + * [7.6 函数](7/func.md) + * 7.6.1 内部函数注册 + * 7.6.2 函数参数解析 + * 7.6.3 引用传参 + * 7.6.4 函数返回值 + * 7.6.5 函数调用 + * [7.7 zval的操作](7/var.md) + * [7.7.1 新生成各类型zval](7/var.md) + * [7.7.2 获取zval的值及类型](7/var.md) + * [7.7.3 类型转换](7/var.md) + * [7.7.4 引用计数](7/var.md) + * [7.7.5 字符串操作](7/var.md) + * [7.7.6 数组操作](7/var.md) + * [7.8 常量](7/constant.md) + * 7.9 面向对象 + * 7.9.1 内部类注册 + * 7.9.2 定义成员属性 + * 7.9.3 定义成员方法 + * 7.9.4 定义常量 + * 7.9.5 类的实例化 + * 7.10 资源类型 + * 7.11 经典扩展解析 + * 7.8.1 Yaf + * 7.8.2 Redis +* 第8章 命名空间 + * [8.1 概述](8/namespace.md) + * [8.2 命名空间的定义](8/namespace.md) + * [8.2.1 定义语法](8/namespace.md) + * [8.2.2 内部实现](8/namespace.md) + * [8.3 命名空间的使用](8/namespace.md) + * [8.3.1 基本用法](8/namespace.md) + * [8.3.2 use导入](8/namespace.md) + * [8.3.3 动态用法](8/namespace.md) + +---- + +## 附录 + * [附录1:break/continue按标签中断语法实现](try/break.md) + * 附录2:defer推迟函数调用语法的实现 \ No newline at end of file diff --git a/book.json b/book.json new file mode 100644 index 0000000..26f051e --- /dev/null +++ b/book.json @@ -0,0 +1,7 @@ +{ + "title" : "PHP7内核剖析", + "author" : "pangudashu", + "description" : "PHP7内核剖析,基于PHP版本:php-7.0.12", + "language" : "zh-hans", + "gitbook" : ">=3.0.0" +} \ No newline at end of file diff --git a/img/EG.png b/img/EG.png index 64efe82..0dcd16c 100644 Binary files a/img/EG.png and b/img/EG.png differ diff --git a/img/align.png b/img/align.png new file mode 100644 index 0000000..63d6e12 Binary files /dev/null and b/img/align.png differ diff --git a/img/ast_break_div.png b/img/ast_break_div.png new file mode 100644 index 0000000..60f1c24 Binary files /dev/null and b/img/ast_break_div.png differ diff --git a/img/ast_namespace.png b/img/ast_namespace.png new file mode 100644 index 0000000..3396f8f Binary files /dev/null and b/img/ast_namespace.png differ diff --git a/img/book.jpg b/img/book.jpg new file mode 100644 index 0000000..89fc991 Binary files /dev/null and b/img/book.jpg differ diff --git a/img/chunk_alloc.png b/img/chunk_alloc.png new file mode 100644 index 0000000..049b76d Binary files /dev/null and b/img/chunk_alloc.png differ diff --git a/img/defer.png b/img/defer.png new file mode 100644 index 0000000..08c48e5 Binary files /dev/null and b/img/defer.png differ diff --git a/img/defer_ast.png b/img/defer_ast.png new file mode 100644 index 0000000..db1da70 Binary files /dev/null and b/img/defer_ast.png differ diff --git a/img/defer_call.png b/img/defer_call.png new file mode 100644 index 0000000..37c7632 Binary files /dev/null and b/img/defer_call.png differ diff --git a/img/executor.png b/img/executor.png new file mode 100644 index 0000000..dfbb461 Binary files /dev/null and b/img/executor.png differ diff --git a/img/fastcgi.png b/img/fastcgi.png new file mode 100644 index 0000000..6240156 Binary files /dev/null and b/img/fastcgi.png differ diff --git a/img/master_event_1.png b/img/master_event_1.png new file mode 100644 index 0000000..f6b8573 Binary files /dev/null and b/img/master_event_1.png differ diff --git a/img/my_wx2.png b/img/my_wx2.png new file mode 100644 index 0000000..bd1a48f Binary files /dev/null and b/img/my_wx2.png differ diff --git a/img/namespace_com.png b/img/namespace_com.png new file mode 100644 index 0000000..7c83a7a Binary files /dev/null and b/img/namespace_com.png differ diff --git a/img/talk.png b/img/talk.png new file mode 100644 index 0000000..f6e884a Binary files /dev/null and b/img/talk.png differ diff --git a/img/worker_pool.png b/img/worker_pool.png new file mode 100644 index 0000000..8f4551c Binary files /dev/null and b/img/worker_pool.png differ diff --git a/img/worker_pool_struct.png b/img/worker_pool_struct.png new file mode 100644 index 0000000..23068cc Binary files /dev/null and b/img/worker_pool_struct.png differ diff --git a/try/break.md b/try/break.md new file mode 100644 index 0000000..adafa30 --- /dev/null +++ b/try/break.md @@ -0,0 +1,260 @@ +# 附录1:break/continue按标签中断语法实现 + +## 1.1 背景 +首先看下目前PHP中break/continue多层循环的情况: +```php +//loop1 +while(...){ + //loop2 + for(...){ + //loop3 + foreach(...){ + ... + break 2; + } + + } + //loop2 end + ... + +} +``` +`break 2`表示要中断往上数两层也就是loop2这层循环,`break 2`之后将从loop2 end开始继续执行。PHP的break、continue只能根据数值中断对应的循环,当嵌套循环比较多的时候这种方式维护起来就变得很不方便,需要一层层的去数要中断的循环。 + +了解Go语言的读者应该知道在Go中可以按照标签中断,举个例子来看: +```go +//test.go +func main() { +loop1: + for i := 0; i < 2; i++ { + fmt.Println("loop1") + + for j := 0; j < 5; j++ { + fmt.Println(" loop2") + if j == 2 { + break loop1 + } + } + } +} +``` +`go run test.go`将输出: +``` +loop1 + loop2 + loop2 + loop2 +``` +`break loop1`这种语法在PHP中是不支持的,接下来我们就对PHP进行改造,让PHP实现同样的功能。 + +## 1.2 实现 +想让PHP支持类似Go语言那样的语法首先需要明确PHP中循环及中断语句的实现,关于这两部分内容前面《PHP基础语法实现》一章已经详细介绍过了,这里再简单概括下实现的关键点: +* 不管是哪种循环结构,其编译时都生成了一个`zend_brk_cont_element`结构,此结构记录着这个循环break、continue要跳转的位置,以及嵌套的父层循环 +* break/continue编译时分为两个步骤:首先初步编译为临时opcode,此opcode记录着break/continue所在循环层以及要中断的层级(即:`break n`,默认n=1);然后在脚本全部编译完之后的pass_two()中,根据当前循环层及中断的层级n向上查找对应的循环层,最后根据查找到的要中断的循环`zend_brk_cont_element`结构得到对应的跳转位置,生成一条ZEND_JMP指令 + +仔细研究循环、中断的实现可以发现,这里面的关键就在于找到break/continue要中断的那层循环,嵌套循环之间是链表的结构,所以目前的查找就变得很容易了,直接从break/continue当前循环层向前移动n即可。 + +标签在内核中通过HashTable的结构保存(即:CG(context).labels),key就是标签名,标签会记录当前opcode的位置,我们要实现`break 标签`的语法需要根据标签取到循环,因此我们为标签赋予一种新的含义:循环标签,只有标签紧挨着循环的才认为是这种含义,比如: +```php +loop1: +for(...){ + ... +} +``` +标签与循环之间有其它表达式的则只能认为是普通标签: +```php +loop1: +$a = 123; +for(...){ +} +``` +既然要按照标签进行break、continue,那么很容易想到把中断的循环层级id保存到标签中,编译break/continue时先查找标签,再查找循环的`zend_brk_cont_element`即可,这样实现的话需要循环编译时将自己`zend_brk_cont_element`的存储位置保存到标签中,标签的结构需要修改,另外一个问题是标签编译不会生成任何opcode,循环结构无法直接根据上一条opcode判断它是不是 ***循环标签*** ,所以我们换一种方式实现,具体思路如下: + +* __(1)__ 循环结构开始编译前先编译一条空opcode(ZEND_NOP),用于标识这是一个循环,并把这个循环`zend_brk_cont_element`的存储位置记录在此opcode中 +* __(2)__ break编译时如果发现是一个标签,则从CG(context).labels)中取出标签结构,然后判断此标签的下一条opcode是否为ZEND_NOP,如果不是则说明这不是一个 ***>循环标签*** ,无法break/continue,如果是则取出循环结构 +* __(3)__ 得到循环结构之后的处理就比较简单了,但是此时还不能直接编译为ZEND_JMP,因为循环可能还未编译完成,break只能编译为临时opcode,这里可以把标签标记的循环存储位置记录在临时opcode中,然后在pass_two()中再重新获取,需要对pass_two()中的逻辑进行改动,为减少改动,这个地方转化一下实现方式:计算label标记的循环相对break所在循环的位置,也就是转为现有的`break n`,这样以来就无需对pass_two()进行改动了 + +接下来看下具体的实现,以for为例。 + +__(1) 编译循环语句__ + +```c +void zend_compile_for(zend_ast *ast) +{ + zend_ast *init_ast = ast->child[0]; + zend_ast *cond_ast = ast->child[1]; + zend_ast *loop_ast = ast->child[2]; + zend_ast *stmt_ast = ast->child[3]; + + znode result; + uint32_t opnum_start, opnum_jmp, opnum_loop; + zend_op *mark_look_opline; + + //新增:创建一条空opcode,用于标识接下来是一个循环结构 + mark_look_opline = zend_emit_op(NULL, ZEND_NOP, NULL, NULL); + + zend_compile_expr_list(&result, init_ast); + zend_do_free(&result); + + opnum_jmp = zend_emit_jump(0); + + zend_begin_loop(ZEND_NOP, NULL); + + //新增:保存当前循环的brk,同时为了防止与其它ZEND_NOP混淆,把op1标为-1 + mark_look_opline->op1.var = -1; + mark_look_opline->extended_value = CG(context).current_brk_cont; + ... +} +``` + +__(2) 编译中断语句__ + +首先明确一点:`break label`将被编译为以下语法结构: + +![](../img/ast_break_div.png) + +`ZEND_AST_BREAK`只有一个子节点,如果是数值那么这个子节点类型为`ZEND_AST_ZVAL`,如果是标签则类型是`ZEND_AST_CONST`,`ZEND_AST_CONST`也有一个类型为`ZEND_AST_ZVAL`子节点。下面看下break/continue修改后的编译逻辑: +```c +void zend_compile_break_continue(zend_ast *ast) +{ + zend_ast *depth_ast = ast->child[0]; + + zend_op *opline; + int depth; + + ZEND_ASSERT(ast->kind == ZEND_AST_BREAK || ast->kind == ZEND_AST_CONTINUE); + + if (CG(context).current_brk_cont == -1) { + zend_error_noreturn(E_COMPILE_ERROR, "'%s' not in the 'loop' or 'switch' context", + ast->kind == ZEND_AST_BREAK ? "break" : "continue"); + } + + if (depth_ast) { + + switch(depth_ast->kind){ + case ZEND_AST_ZVAL: //break 数值; + { + zval *depth_zv; + + depth_zv = zend_ast_get_zval(depth_ast); + if (Z_TYPE_P(depth_zv) != IS_LONG || Z_LVAL_P(depth_zv) < 1) { + zend_error_noreturn(E_COMPILE_ERROR, "'%s' operator accepts only positive numbers", + ast->kind == ZEND_AST_BREAK ? "break" : "continue"); + } + + depth = Z_LVAL_P(depth_zv); + break; + } + case ZEND_AST_CONST://break 标签; + { + //获取label名称 + zend_string *label = zend_ast_get_str(depth_ast->child[0]); + //根据label获取标记的循环,以及相对break所在循环的位置 + depth = zend_loop_get_depth_by_label(label); + if(depth > 0){ + goto SET_OP; + } + break; + } + default: + zend_error_noreturn(E_COMPILE_ERROR, "'%s' operator with non-constant operand " + "is no longer supported", ast->kind == ZEND_AST_BREAK ? "break" : "continue"); + } + } else { + depth = 1; + } + + if (!zend_handle_loops_and_finally_ex(depth)) { + zend_error_noreturn(E_COMPILE_ERROR, "Cannot '%s' %d level%s", + ast->kind == ZEND_AST_BREAK ? "break" : "continue", + depth, depth == 1 ? "" : "s"); + } + +SET_OP: + opline = zend_emit_op(NULL, ast->kind == ZEND_AST_BREAK ? ZEND_BRK : ZEND_CONT, NULL, NULL); + opline->op1.num = CG(context).current_brk_cont; + opline->op2.num = depth; +} +``` +`zend_loop_get_depth_by_label()`这个函数用来计算标签标记的循环相对break/continue所在循环的层级: +```c +int zend_loop_get_depth_by_label(zend_string *label_name) +{ + zval *label_zv; + zend_label *label; + zend_op *next_opline; + + if(UNEXPECTED(CG(context).labels == NULL)){ + zend_error_noreturn(E_COMPILE_ERROR, "can't find label:'%s' or it not mark a loop", ZSTR_VAL(label_name)); + } + + // 1) 查找label + label_zv = zend_hash_find(CG(context).labels, label_name); + if(UNEXPECTED(label_zv == NULL)){ + zend_error_noreturn(E_COMPILE_ERROR, "can't find label:'%s' or it not mark a loop", ZSTR_VAL(label_name)); + } + + label = (zend_label *)Z_PTR_P(label_zv); + + // 2) 获取label下一条opcode + next_opline = &(CG(active_op_array)->opcodes[label->opline_num]); + if(UNEXPECTED(next_opline == NULL)){ + zend_error_noreturn(E_COMPILE_ERROR, "can't find label:'%s' or it not mark a loop", ZSTR_VAL(label_name)); + } + + int label_brk_offset, curr_brk_offset; //标签标识的循环、break当前所在循环 + int depth = 0; //break当前循环至标签循环的层级 + zend_brk_cont_element *brk_cont_element; + + if(next_opline->opcode == ZEND_NOP && next_opline->op1.var == -1){ + label_brk_offset = next_opline->extended_value; + curr_brk_offset = CG(context).current_brk_cont; + + brk_cont_element = &(CG(active_op_array)->brk_cont_array[curr_brk_offset]); + //计算标签标记的循环相对位置 + while(1){ + depth++; + + if(label_brk_offset == curr_brk_offset){ + return depth; + } + + curr_brk_offset = brk_cont_element->parent; + if(curr_brk_offset < 0){ + //label标识的不是break所在循环 + zend_error_noreturn(E_COMPILE_ERROR, "can't break/conitnue label:'%s' because it not mark a loop", ZSTR_VAL(label_name)); + } + } + }else{ + //label没有标识一个循环 + zend_error_noreturn(E_COMPILE_ERROR, "can't break/conitnue label:'%s' because it not mark a loop", ZSTR_VAL(label_name)); + } + + return -1; +} +``` +改动后重新编译PHP,然后测试新的语法是否生效: +```php +//test.php + +loop1: +for($i = 0; $i < 2; $i++){ + echo "loop1\n"; + + for($j = 0; $j < 5; $j++){ + echo " loop2\n"; + if($j == 2){ + break loop1; + } + } +} +``` +`php test.php`输出: +``` +loop1 + loop2 + loop2 + loop2 +``` +其它几个循环结构的改动与for相同,有兴趣的可以自己去尝试下。 + diff --git a/try/defer.md b/try/defer.md new file mode 100644 index 0000000..761d149 --- /dev/null +++ b/try/defer.md @@ -0,0 +1,355 @@ +# 附录2:defer推迟函数调用语法的实现 + +使用过Go语言的应该都知道defer这个语法,它用来推迟一个函数的执行,在函数执行返回前首先检查当前函数内是否有推迟执行的函数,如果有则执行,然后再返回。defer是一个非常有用的语法,这个功能可以很方便的在函数结束前执行一些清理工作,比如关闭打开的文件、关闭连接、释放资源、解锁等等。这样延迟一个函数有以下两个好处: + +* (1) 靠近使用位置,避免漏掉清理工作,同时比放在函数结尾要清晰 +* (2) 如果有多处返回的地方可以避免代码重复,比如函数中有很多处return + +在一个函数中可以使用多个defer,其执行顺序与栈类似:后进先出,先定义的defer后执行。另外,在返回之后定义的defer将不会被执行,只有返回前定义的才会执行,通过exit退出程序的情况也不会执行任何defer。 + +在PHP中并没有实现类似的语法,本节我们将尝试在PHP中实现类似Go语言中defer的功能。此功能的实现需要对PHP的语法解析、抽象语法树/opcode的编译、opcode指令的执行等环节进行改造,涉及的地方比较多,但是改动点比较简单,可以很好的帮助大家完整的理解PHP编译、执行两个核心阶段的实现。总体实现思路: + +* __(1)语法解析:__ defer本质上还是函数调用,只是将调用时机移到了函数的最后,所以编译时可以复用调用函数的规则,但是需要与普通的调用区分开,所以我们新增一个AST节点类型,其子节点为为正常函数调用编译的AST,语法我们定义为:`defer function_name()`; +* __(2)opcode编译:__ 编译opcode时也复用调用函数的编译逻辑,不同的地方在于把defer放在最后编译,另外需要在编译return前新增一条opcode,用于执行return前跳转到defer开始的位置,在defer的最后也需要新增一条opcode,用于执行完defer后跳回return的位置; +* __(3)执行阶段:__ 执行时如果发现是return前新增的opcode则跳转到defer开始的位置,同时把return的位置记录下来,执行完defer后再跳回return。 + +编译后的opcode指令如下图所示: + +![](../img/defer.png) + +接下来我们详细介绍下各个环节的改动,一步步实现defer功能。 + +__(1)语法解析__ + +想让PHP支持`defer function_name()`的语法首先需要修改的是词法解析规则,将"defer"关键词解析为token:T_DEFER,这样词法扫描器在匹配token时遇到"defer"将告诉语法解析器这是一个T_DEFER。这一步改动比较简单,PHP的词法解析规则定义在zend_language_scanner.l中,加入以下代码即可: +```c +"defer" { + RETURN_TOKEN(T_DEFER); +} +``` +完成词法解析规则的修改后接着需要定义语法解析规则,这是非常关键的一步,语法解析器会根据配置的语法规则将PHP代码解析为抽象语法树(AST)。普通函数调用会被解析为ZEND_AST_CALL类型的AST节点,我们新增一种节点类型:ZEND_AST_DEFER_CALL,抽象语法树的节点类型为enum,定义在zend_ast.h中,同时此节点只需要一个子节点,这个子节点用于保存ZEND_AST_CALL节点,因此zend_ast.h的修改如下: +```c +enum _zend_ast_kind { + ... + /* 1 child node */ + ... + ZEND_AST_DEFER_CALL + .... +} +``` +定义完AST节点后就可以在配置语法解析规则了,把defer语法解析为ZEND_AST_DEFER_CALL节点,我们把这条语法规则定义在"statement:"节点下,if、echo、for等语法都定义在此节点下,语法解析规则文件为zend_language_parser.y: +```c +statement: + '{' inner_statement_list '}' { $$ = $2; } + ... + | T_DEFER function_call ';' { $$ = zend_ast_create(ZEND_AST_DEFER_CALL, $2); } +; +``` +修改完这两个文件后需要分别调用re2c、yacc生成对应的C文件,具体的生成命令可以在Makefile.frag中看到: +```sh +$ re2c --no-generation-date --case-inverted -cbdFt Zend/zend_language_scanner_defs.h -oZend/zend_language_scanner.c Zend/zend_language_scanner.l +$ yacc -p zend -v -d Zend/zend_language_parser.y -oZend/zend_language_parser.c +``` +执行完以后将在Zend目录下重新生成zend_language_scanner.c、zend_language_parser.c两个文件。到这一步已经完成生成抽象语法树的工作了,重新编译PHP后已经能够解析defer语法了,将会生成以下节点: + +![](../img/defer_ast.png) + +__(2)编译ZEND_AST_DEFER_CALL__ + +生成抽象语法树后接下来就是编译生成opcodes的操作,即从AST->Opcodes。编译ZEND_AST_DEFER_CALL节点时不能立即进行编译,需要等到当前脚本或函数全部编译完以后再进行编译,所以在编译过程需要把ZEND_AST_DEFER_CALL节点先缓存下来,参考循环结构编译时生成的zend_brk_cont_element的存储位置,我们也把ZEND_AST_DEFER_CALL节点保存在zend_op_array中,通过数组进行存储,将ZEND_AST_DEFER_CALL节点依次存入该数组,zend_op_array中加入以下几个成员: + +* __last_defer:__ 整形,记录当前编译的defer数 +* __defer_start_op:__ 整形,用于记录defer编译生成opcode指令的起始位置 +* __defer_call_array:__ 保存ZEND_AST_DEFER_CALL节点的数组,用于保存ast节点的地址 + +```c +struct _zend_op_array { + ... + int last_defer; + uint32_t defer_start_op; + zend_ast **defer_call_array; +} +``` +修改完数据结构后接着对应修改zend_op_array初始化的过程: +```c +//zend_opcode.c +void init_op_array(zend_op_array *op_array, zend_uchar type, int initial_ops_size) +{ + ... + op_array->last_defer = 0; + op_array->defer_start_op = 0; + op_array->defer_call_array = NULL; + ... +} +``` +完成依赖的这些数据结构的改造后接下来开始编写具体的编译逻辑,也就是编译ZEND_AST_DEFER_CALL的处理。抽象语法树的编译入口函数为zend_compile_top_stmt(),然后根据不同节点的类型进行相应的编译,我们在zend_compile_stmt()函数中对ZEND_AST_DEFER_CALL节点进行编译: +```c +void zend_compile_stmt(zend_ast *ast) +{ + ... + switch (ast->kind) { + ... + case ZEND_AST_DEFER_CALL: + zend_compile_defer_call(ast); + break + ... + } +} +``` +编译过程只是将ZEND_AST_DEFER_CALL的子节点(即:ZEND_AST_CALL)保存到zend_op_array->defer_call_array数组中,注意这里defer_call_array数组还没有分配内存,参考循环结构的实现,这里我们定义了一个函数用于数组的分配: +```c +//zend_compile.c +void zend_compile_defer_call(zend_ast *ast) +{ + if(!ast){ + return; + } + + zend_ast **call_ast = NULL; + //将普通函数调用的ast节点保存到defer_call_array数组中 + call_ast = get_next_defer_call(CG(active_op_array)); + *call_ast = ast->child[0]; +} + +//zend_opcode.c +zend_ast **get_next_defer_call(zend_op_array *op_array) +{ + op_array->last_defer++; + op_array->defer_call_array = erealloc(op_array->defer_call_array, sizeof(zend_ast*)*op_array->last_defer); + return &op_array->defer_call_array[op_array->last_defer-1]; +} +``` +既然分配了defer_call_array数组的内存就需要在zend_op_array销毁时释放: +```c +//zend_opcode.c +ZEND_API void destroy_op_array(zend_op_array *op_array) +{ + ... + if (op_array->defer_call_array) { + efree(op_array->defer_call_array); + } + ... +} +``` +编译完整个脚本或函数后,最后还会编译一条ZEND_RETURN,也就是返回指令,相当于ret指令,注意:这条opcode并不是我们在脚本中定义的return语句的,而是PHP内核为我们加的一条指令,这就是为什么有些函数我们没有写return也能返回的原因,任何函数或脚本都会生成这样一条指令。我们缓存在zend_op_array->defer_call_array数组中defer就是要在这时进行编译,也就是把defer的指令编译在最后。内核最后编译返回的这条指令由zend_emit_final_return()方法完成,我们把defer的编译放在此方法的末尾: +```c +//zend_compile.c +void zend_emit_final_return(zval *zv) +{ + ... + ret = zend_emit_op(NULL, returns_reference ? ZEND_RETURN_BY_REF : ZEND_RETURN, &zn, NULL); + ret->extended_value = -1; + + //编译推迟执行的函数调用 + zend_emit_defer_call(); +} +``` +前面已经说过,defer本质上就是函数调用,所以编译的过程直接复用普通函数调用的即可。另外,在编译时把起始位置记录到zend_op_array->defer_start_op中,因为在执行return前需要知道跳转到什么位置,这个值就是在那时使用的,具体的用法稍后再作说明。编译时按照倒序的顺序进行编译: +```c +//zend_compile.c +void zend_emit_defer_call() +{ + if (!CG(active_op_array)->defer_call_array) { + return; + } + + zend_ast *call_ast; + zend_op *nop; + znode result; + uint32_t opnum = get_next_op_number(CG(active_op_array)); + int defer_num = CG(active_op_array)->last_defer; + + //记录推迟的函数调用指令开始位置 + CG(active_op_array)->defer_start_op = opnum; + + while(--defer_num >= 0){ + call_ast = CG(active_op_array)->defer_call_array[defer_num]; + if (call_ast == NULL) { + continue; + } + nop = zend_emit_op(NULL, ZEND_NOP, NULL, NULL); + nop->op1.var = -2; + //编译函数调用 + zend_compile_call(&result, call_ast, BP_VAR_R); + } + //compile ZEND_DEFER_CALL_END + zend_emit_op(NULL, ZEND_DEFER_CALL_END, NULL, NULL); +} +``` +编译完推迟的函数调用之后,编译一条ZEND_DEFER_CALL_END指令,该指令用于执行完推迟的函数后跳回return的位置进行返回,opcode定义在zend_vm_opcodes.h中: +```c +//zend_vm_opcodes.h +#define ZEND_DEFER_CALL_END 174 +``` +还有一个地方你可能已经注意到,在逐个编译defer的函数调用前都生成了一条ZEND_NOP的指令,这个的目的是什么呢?开始的时候已经介绍过defer语法的特点,函数中定义的defer并不是全部执行,在return之后定义的defer是不会执行的,比如: +```go +func main(){ + defer fmt.Println("A") + + if 1 == 1{ + return + } + + defer fmt.Println("B") +} +``` +这种情况下第2个defer就不会生效,因此在return前跳转的位置就不一定是zend_op_array->defer_start_op,有可能会跳过几个函数的调用,所以这里我们通过ZEND_NOP这条空指令对多个defer call进行隔离,同时为避免与其它ZEND_NOP指令混淆,增加一个判断条件:op1.var=-2。这样在return前跳转时就根据此前定义的defer数跳过部分函数的调用,如下图所示。 + +![](../img/defer_call.png) + +到这一步我们已经完成defer函数调用的编译,此时重新编译PHP后可以看到通过defer推迟的函数调用已经被编译在最后了,只不过这个时候它们不能被执行。 + +__(3)编译return__ + +编译return时需要插入一条指令用于跳转到推迟执行的函数调用指令处,因此这里需要再定义一条opcode:ZEND_DEFER_CALL,在编译过程中defer call还未编译,因此此时还无法知道具体的跳转值。 +```c +//zend_vm_opcodes.h +#define ZEND_DEFER_CALL 173 +#define ZEND_DEFER_CALL_END 174 +``` +PHP脚本中声明的return语句由zend_compile_return()方法完成编译,在编译生成ZEND_DEFER_CALL指令时还需要将当前已定义的defer数(即在return前声明的defer)记录下来,用于计算具体的跳转值。 +```c +void zend_compile_return(zend_ast *ast) +{ + ... + //在return前编译ZEND_DEFER_CALL:用于在执行retur前跳转到defer call + if (CG(active_op_array)->defer_call_array) { + defer_zn.op_type = IS_UNUSED; + defer_zn.u.op.num = CG(active_op_array)->last_defer; + zend_emit_op(NULL, ZEND_DEFER_CALL, NULL, &defer_zn); + } + + //编译正常返回的指令 + opline = zend_emit_op(NULL, by_ref ? ZEND_RETURN_BY_REF : ZEND_RETURN, + &expr_node, NULL); + ... +} +``` +除了这种return外还有一种我们上面已经提过的return,即PHP内核编译的return指令,当PHP脚本中没有声明return语句时将执行内核添加的那条指令,因此也需要在zend_emit_final_return()加上上面的逻辑。 +```c +void zend_emit_final_return(zval *zv) +{ + ... + //在return前编译ZEND_DEFER_CALL:用于在执行retur前跳转到defer call + if (CG(active_op_array)->defer_call_array) { + //当前return之前定义的defer数 + defer_zn.op_type = IS_UNUSED; + defer_zn.u.op.num = CG(active_op_array)->last_defer; + zend_emit_op(NULL, ZEND_DEFER_CALL, NULL, &defer_zn); + } + + //编译返回指令 + ret = zend_emit_op(NULL, returns_reference ? ZEND_RETURN_BY_REF : ZEND_RETURN, &zn, NULL); + ret->extended_value = -1; + + //编译推迟执行的函数调用 + zend_emit_defer_call(); +} +``` +__(4)计算ZEND_DEFER_CALL指令的跳转位置__ + +前面我们已经完成了推迟调用函数以及return编译过程的改造,在编译完成后ZEND_DEFER_CALL指令已经能够知道具体的跳转位置了,因为推迟调用的函数已经编译完成了,所以下一步就是为全部的ZEND_DEFER_CALL指令计算跳转值。前面曾介绍过,在编译完成有一个pass_two()的环节,我们就在这里完成具体跳转位置的计算,并把跳转位置保存到ZEND_DEFER_CALL指令的操作数中,在执行阶段直接跳转到对应位置。 + +```c +ZEND_API int pass_two(zend_op_array *op_array) +{ + zend_op *opline, *end; + ... + //遍历opcode + opline = op_array->opcodes; + end = opline + op_array->last; + while (opline < end) { + switch (opline->opcode) { + ... + case ZEND_DEFER_CALL: //设置jmp + { + uint32_t defer_start = op_array->defer_start_op; + //skip_defer为当前return之后声明的defer数,也就是不需要执行的defer + uint32_t skip_defer = op_array->last_defer - opline->op2.num; + //defer_opline为推迟的函数调用起始位置 + zend_op *defer_opline = op_array->opcodes + defer_start; + uint32_t n = 0; + + while(n <= skip_defer){ + if (defer_opline->opcode == ZEND_NOP && defer_opline->op1.var == -2) { + n++; + } + defer_opline++; + defer_start++; + } + + //defer_start为opcode在op_array->opcodes数组中的位置 + opline->op1.opline_num = defer_start; + //将跳转位置保存到操作数op1中 + ZEND_PASS_TWO_UPDATE_JMP_TARGET(op_array, opline, opline->op1); + } + break; + } + ... + } + ... +} +``` +这里我们并没有直接编译为ZEND_JMP跳转指令,虽然ZEND_JMP可以跳转到后面的指令位置,但是最后的那条跳回return位置的指令(即:ZEND_DEFER_CALL_END)由于可能存在多个return的原因无法在编译期间确定具体的跳转值,只能在运行期间执行ZEND_DEFER_CALL时才能确定,所以需要在ZEND_DEFER_CALL指令的handler中将return的位置记录下来,执行ZEND_DEFER_CALL_END时根据这个值跳回。 + +__(5)定义ZEND_DEFER_CALL、ZEND_DEFER_CALL_END指令的handler__ + +ZEND_DEFER_CALL指令执行时需要将return的位置保存下来,我们把这个值保存到zend_execute_data结构中: +```c +//zend_compile.h +struct _zend_execute_data { + ... + const zend_op *return_opline; + ... +} +``` +opcode的handler定义在zend_vm_def.h文件中,定义完成后需要执行`php zend_vm_gen.php`脚本生成具体的handler函数。 +```c +ZEND_VM_HANDLER(173, ZEND_DEFER_CALL, ANY, ANY) +{ + USE_OPLINE + + //1) 将return指令的位置保存到EX(return_opline) + EX(return_opline) = opline + 1; + + //2) 跳转 + ZEND_VM_SET_OPCODE(OP_JMP_ADDR(opline, opline->op1)); + ZEND_VM_CONTINUE(); +} + +ZEND_VM_HANDLER(174, ZEND_DEFER_CALL_END, ANY, ANY) +{ + USE_OPLINE + + ZEND_VM_SET_OPCODE(EX(return_opline)); + ZEND_VM_CONTINUE(); +} +``` +到目前为止我们已经完成了全部的修改,重新编译PHP后就可以使用defer语法了: +```php +function shutdown($a){ + echo $a."\n"; +} +function test(){ + $a = 1234; + defer shutdown($a); + + $a = 8888; + + if(1){ + return "mid end\n"; + } + defer shutdown("9999"); + return "last end\n"; +} + +echo test(); +``` +执行后将显示: +```sh +8888 +mid end +``` +这里我们只实现了普通函数调用的方式,关于成员方法、静态方法、匿名函数等调用方式并未实现,留给有兴趣的读者自己去实现。 + +完整代码:[https://github.com/pangudashu/php-7.0.12](https://github.com/pangudashu/php-7.0.12)