乐趣区

PHP源码学习20190408-PHP中include的实现

baiyan

全部视频:https://segmentfault.com/a/11…

回顾 while 语法的实现

<?php
$a = 1;
while($a){}
  • 在上一篇笔记中我们知道,PHP 中的 while 语法所对应的指令执行过程如下图所示:

  • 那么现在回答一下上一篇文章结尾提出的问题:do-while 是如何实现的呢?
<?php
$a = 1;
do{$a = 0;}while($a);
  • 经过 gdb 调试,其最终的指令如下:

  • 第一个 ASSIGN 对应 $a = 1;
  • 第二个 ASSIGN 对应 $a = 0;
  • 第三个 JMPNZ 对应 do-while 循环体,注意这个箭头指向 $a = 0 对应的 ASSIGN 指令,代表每次循环都要重新执行一次 $a = - 这个 ASSIGN 指令
  • 第四个 RETURN 对应 PHP 虚拟机自动给脚本添加的返回值

include 语法的实现

  • 我们在面试中经常会被问到如下知识点:

    - include 和 require 有什么区别?- include 和 include_once 有什么区别(require 和 require_once)同理)
  • 以上两道题的答案相信大家都知道,第一个问题如果文件不存在,include 会情况下会发出警告而 require 会报 fatal error 并终止脚本运行;而第二个问题中的 include_once 带有缓存,如果之前加载过这个文件直接调用缓存中的文件,不会去二次加载文件,include_once 的性能更好。
  • 我们首先看一个例子:
  • 1.php:
<?php
$a = 1;
  • 2.php:
<?php
include "1.php";
$b = 2;
  • 那么我们通过 gdb 2.php 并分析它的 op_array,可以得出它的指令,一共有 3 条:
  • ZEND_INCLUDE_OR_EVAL_SPEC_CONST_HANDLER:表示 include “1.php”; 语句
  • ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER:表示 $b = 2;
  • ZEND_RETURN_SPEC_CONST_HANDLER:表示 PHP 虚拟机自动给脚本加的返回值
  • 接下来我们深入第一个 include 的 handler 处理函数:

static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INCLUDE_OR_EVAL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    USE_OPLINE
    zend_op_array *new_op_array;

    zval *inc_filename;

    SAVE_OPLINE();
    inc_filename = EX_CONSTANT(opline->op1); // 这里的 op1 就是字符串 1.php
    new_op_array = zend_include_or_eval(inc_filename, opline->extended_value);
    ...
  • 这个 handler 处理函数中核心为 zend_include_or_eval()这个函数,它返回一个新的 op_array:
static zend_never_inline zend_op_array* ZEND_FASTCALL zend_include_or_eval(zval *inc_filename, int type) /* {{{ */
{
    zend_op_array *new_op_array = NULL;
    zval tmp_inc_filename;

    ...
    } else {switch (type) {
{
            case ZEND_INCLUDE_ONCE:
            case ZEND_REQUIRE_ONCE: { // 此处带有缓存
                    zend_file_handle file_handle;
                    zend_string *resolved_path;

                    resolved_path = zend_resolve_path(Z_STRVAL_P(inc_filename), (int)Z_STRLEN_P(inc_filename));
                    if (resolved_path) {if (zend_hash_exists(&EG(included_files), resolved_path)) {goto already_compiled;}
                    } else {resolved_path = zend_string_copy(Z_STR_P(inc_filename));
                    }

                    if (SUCCESS == zend_stream_open(ZSTR_VAL(resolved_path), &file_handle)) {if (!file_handle.opened_path) {file_handle.opened_path = zend_string_copy(resolved_path);
                        }

                        if (zend_hash_add_empty_element(&EG(included_files), file_handle.opened_path)) { // 加入缓存的哈希表中
                            zend_op_array *op_array = zend_compile_file(&file_handle, (type==ZEND_INCLUDE_ONCE?ZEND_INCLUDE:ZEND_REQUIRE));
                            zend_destroy_file_handle(&file_handle);
                            zend_string_release(resolved_path);
                            if (Z_TYPE(tmp_inc_filename) != IS_UNDEF) {zend_string_release(Z_STR(tmp_inc_filename));
                            }
                            return op_array;
                        } else {zend_file_handle_dtor(&file_handle);
already_compiled:
                            new_op_array = ZEND_FAKE_OP_ARRAY;
                        }
                    } else {if (type == ZEND_INCLUDE_ONCE) {zend_message_dispatcher(ZMSG_FAILED_INCLUDE_FOPEN, Z_STRVAL_P(inc_filename));
                        } else {zend_message_dispatcher(ZMSG_FAILED_REQUIRE_FOPEN, Z_STRVAL_P(inc_filename));
                        }
                    }
                    zend_string_release(resolved_path);
                }
                break;
            case ZEND_INCLUDE:
            case ZEND_REQUIRE:
                new_op_array = compile_filename(type, inc_filename);  // 关键调用
                break;
    ...
    return new_op_array;
}
  • 在 ZEND_INCLUDE_ONCE 分支中可以观察到,如果是 include_once 或者 require_once 的 case,会先去缓存中查找,那么这个缓存是怎么实现的呢?最容易想到的就是 哈希表,key 为文件名,value 为文件内容,这样就可以直接从缓存中读取文件,不用再次加载文件了,提高效率。
  • 我们回到主题 include 语法,在 ZEND_INCLUDE 分支中我们可以看到,这里又调用了一个新的函数 compile_filename(),它返回一个新的 op_array。因为 include 是包含另一个外部文件,而 op_array 是一个脚本的指令集,所以需要新创建一个 op_array,存储另外一个文件的指令集,我们继续跟进 compile_filename():
zend_op_array *compile_filename(int type, zval *filename)
{
    zend_file_handle file_handle;
    zval tmp;
    zend_op_array *retval;
    zend_string *opened_path = NULL;
    
    ...
    retval = zend_compile_file(&file_handle, type); // 核心调用
    
    return retval;
}
  • 这个函数中还会继续调用 zend_compile_file()函数,它是一个函数指针,指向 compile_file()函数:
ZEND_API zend_op_array *compile_file(zend_file_handle *file_handle, int type)
{
    ...
    zend_op_array *op_array = NULL;

    if (open_file_for_scanning(file_handle)==FAILURE) {...} else {op_array = zend_compile(ZEND_USER_FUNCTION); // 核心调用
    }

    return op_array;
}
  • 我们可以看到,它最终调用了 zend_compile 函数。我们是不是对它很熟悉呢?没错,它就是 PHP 脚本编译的入口。随后,通过调用这个函数,就可以对引入的外部脚本 1.php 进行词法分析和语法分析等编译操作了。
  • 现在思考一个问题,这个函数返回一个 op_array,是引入的新的外部脚本 1.php 的 op_array,那么原来的旧脚本 2.php 的 op_array 的状态和数据应该如何存储呢?
  • 答案是继续往 zend_execute_data 栈中添加。当 include 脚本执行完成之后,出栈即可。同递归的原理一样,递归也是借助栈,当你不断递归的时候,数据不断入栈,到最后的递归终止条件的时候,逐步出栈即可,所以递归是非常慢的,效率极低。

其他

PHP 脚本的执行流程

  • 我们之前讲过,PHP 脚本的执行入口为 main 函数(我们代码层面无法看到,是虚拟机帮助我们加的)。从 main 函数进去之后,PHP 脚本的执行总共有 5 大阶段:
  • CLI 模式(command line interface,即命令行模式。如在命令行下执行脚本:php 1.php):
  • php_module_startup:模块初始化
  • php_request_startup:请求初始化
  • php_execute_script:执行脚本
  • php_request_shutdown:请求关闭
  • php_module_shutdown:模块关闭
  • CLI 模式下,运行一次就会直接退出,并不常驻内存,接下来看一下我们使用的最多的 FPM 模式,它常驻内存。一次请求到来,PHP-FPM 就要对其进行处理,所以在 php_request_startup、php_execute_script、php_request_shutdown 三个阶段会进行死循环,让 PHP-FPM 常驻内存,才能不断地处理一个个到来的请求。但是这样会有一个问题,每一个请求到来的时候,都会重新进行词法解析、语法解析 …… 效率是非常低的。为了解决这个问题,PHP 中我们常说的 opcache 就要粉墨登场了。它会把之前解析过的 opcode 缓存起来,下一次再遇到相同 opcode 的时候,就不用再次解析,提升性能。

初探 nginx+php-fpm 架构

  • 在 LNMP 架构下,前端的请求发来,先会通过 nginx 做代理,然后通过 fastcgi 协议,转发给上游的 php-fpm,由 php-fpm 真正地处理请求。
  • 我们知道,nginx 是多进程架构的反向代理 web 服务器,由一个 master 进程和多个 worker 进程组成:
  • master 进程:管理所有 worker 进程(如 worker 进程的创建、销毁)
  • worker 进程:负责处理客户端发来的请求
  • 当杀死 master 进程的时候,worker 进程依然存在,可以为客户端提供服务
  • 当杀死 worker 进程的时候(且当前没有其他 worker 进程),master 进程就会再创建 worker 进程,保证 nginx 服务正常运行
  • 下一篇文章我们就即将讲解 fastcgi 协议,逐步揭开 nginx+php-fpm 架构通信的神秘面纱
退出移动版