CLI模式的生命周期

从版本 4.3.0 开始,PHP 支持一种新类型的 CLI SAPI, CLI 意为 Command Line Interface,即命令行接口。顾名思义,该 CLI SAPI 模块主要用于 PHP 的外壳应用开发。

在 CLI 模式下,PHP 的执行过程主要分为 5 大阶段,分别是模块初始化、请求初始化、执行、请求关闭和模块关闭。这 5 个阶段分别对应 php_module_startupphp_request_startupphp_execute_scriptphp_request_shutdown 以及 php_module_shutdown,具体如图7-2所示。

image 2024 06 09 21 06 31 223
Figure 1. 图7-2 PHP 7生命周期示意图

下面我们分别从这 5 个阶段详细阐述一下 PHP 7 的生命周期。

模块初始化阶段

在模块初始化阶段之前,首先调用 sapi_startup(sapi_module),对 sapi_model 进行一些初始化工作,其中 sapi_model 对应的是 7.1.2 节中 _sapi_module_struct 的实现。以 CLI 模式为例,其对应的 sapi_model 如下:

static sapi_module_struct cli_sapi_module = {
    "cli",                                /* 名字为cli */
    "Command Line Interface",             /* 具体名字为Command Line Interface */
    php_cli_startup,                      /*模块启动调用的函数*/
    php_module_shutdown_wrapper,          /*模块关闭调用的函数*/
    //…代码省略…//

通过调用 sapi_modelstartup 函数,CLI 调用了 php_cli_startup 函数,该函数又调用了 php_module_startup 函数,也就是对应的模块初始化,调用代码如下:

static int php_cli_startup(sapi_module_struct *sapi_module) /* {{{ */
{
    if (php_module_startup(sapi_module, NULL, 0)==FAILURE) {
        return FAILURE;
    }
    return SUCCESS;
}

接下来我们看一下 php_module_startup 的具体功能,如图7-3所示。

image 2024 06 09 21 59 09 895
Figure 2. 图7-3 模块初始化流程图

对于图7-3,我们具体分析一下各函数的作用:

  1. 调用 sapi_initialize_empty_request 函数。

    SAPI_API void sapi_initialize_empty_request(void)
    {
        SG(server_context) = NULL;
        SG(request_info).request_method = NULL;
        SG(request_info).auth_digest = SG(request_info).auth_user = SG(request_info).
            auth_password = NULL;
        SG(request_info).content_type_dup = NULL;
    }

    可以看出,这个函数的主要工作是对 sapi_globals 中的成员变量进行初始化。

  2. 调用 sapi_activate 函数。

    SAPI_API void sapi_activate(void)
    {
        zend_llist_init(&SG(sapi_headers).headers, sizeof(sapi_header_struct), (void (*)
            (void *)) sapi_free_header, 0);
        SG(sapi_headers).send_default_content_type = 1;
        SG(sapi_headers).http_status_line = NULL;
        SG(sapi_headers).mimetype = NULL;
        //…省略代码…//
        if (sapi_module.activate) {
            sapi_module.activate(); //调用sapi_module对应的activate函数
        }
        if (sapi_module.input_filter_init) {
            sapi_module.input_filter_init();
        //调用sapi_module对应的input_filter_init函数
        }
    }

    函数前半部分的主要工作还是初始化 SG 相关变量;函数的最后调用了 sapi_module 对应的 activate 方法和 input_filter_init 函数,对于不同运行模式可以自定义这些函数的实现。以 CLI 模式为例,这两个函数都是 NULL

  3. 调用 php_output_startup 函数,实现如下:

    PHPAPI void php_output_startup(void)
    {
        ZEND_INIT_MODULE_GLOBALS(output, php_output_init_globals, NULL);
        zend_hash_init(&php_output_handler_aliases, 8, NULL, NULL, 1);
        zend_hash_init(&php_output_handler_conflicts, 8, NULL, NULL, 1);
        zend_hash_init(&php_output_handler_reverse_conflicts,  8,  NULL,  reverse_
            conflict_dtor, 1);
        php_output_direct = php_output_stdout;
    }

    这部分代码中,首先使用宏定义对 output_globals 进行初始化,我们具体看一下这个宏:

    #define ZEND_INIT_MODULE_GLOBALS(module_name, globals_ctor, globals_dtor)      \
        globals_ctor(&module_name##_globals);

    大家知道,宏是替换,其中 globals_ctorphp_output_init_globals, module_nameoutput,做完替换后,代码如下:

    php_output_init_globals(&output_globals);

    函数 php_output_init_globals 实现如下:

    static inline void php_output_init_globals(zend_output_globals *G)
    {
        ZEND_TSRMLS_CACHE_UPDATE();
        memset(G, 0, sizeof(*G));
    }

    该函数通过 memsetoutput_globals 进行了初始化,其中 output_globals 是一个全局变量,对应的取值宏为 OG(v)

    # define OG(v) (output_globals.v)

    output_globals 对应的结构体为 zend_output_globals,同样是使用宏进行定义的:

    ZEND_BEGIN_MODULE_GLOBALS(output)
        zend_stack handlers;
        php_output_handler *active;
        php_output_handler *running;
        const char *output_start_filename;
        int output_start_lineno;
        int flags;
    ZEND_END_MODULE_GLOBALS(output)
    //宏定义如下:
    #define ZEND_BEGIN_MODULE_GLOBALS(module_name)          \
        typedef struct _zend_##module_name##_globals {
    #define ZEND_END_MODULE_GLOBALS(module_name)            \
        } zend_##module_name##_globals;
    image 2024 06 09 23 34 44 903
    Figure 3. 图7-4 output_globals的结构示意图

    因此,全局变量 output_globals 的结构如图7-4所示。

    output_globals 也是在全局变量区分配的,大小为 56 字节。php_output_startup 函数对 output_globals 初始化后,分别对 php_output_handler_aliasesphp_output_handler_conflictsphp_output_handler_reverse_conflicts 这 3 个 HashTable 进行初始化。接着将 php_output_stdout 赋值给 php_output_direct,其中 php_output_stdout 函数实现如下:

    static  size_t  php_output_stdout(const  char
        *str, size_t str_len)
    {
        fwrite(str, 1, str_len, stdout);
        return str_len;
    }

    该函数的作用是调用 fwrite 函数,输出字符串到 stdout 中。

    调用 php_startup_ticks 函数,对 PG(tick_functions) 进行初始化,这里又出现一个宏定义,对应的是 core_globals,它的结构如图7-5所示。

    image 2024 06 09 23 37 51 037
    Figure 4. 图7-5 core_globals的结构示意图

    同样,core_globals 也是在全局变量区申请的,维护了比较多的变量,其大小为 656 字节。后面的分析会经常用到这个全局变量。

  4. 调用 gc_globals_ctor 函数,对 gc_globals 进行初始化,这部分在第 3 章已做了详细阐述,这里不再展开叙述。

  5. 调用 zend_startup 函数。

    • ① 调用 start_memory_manager 初始化内存管理,这部分在第 9 章会详细讨论。

    • ② 调用 virtual_cwd_startup 初始化 cwd_globals,其中 cwd_globals 的结构如图7-6所示。

      image 2024 06 09 23 44 01 133
      Figure 5. 图7-6 cwd_globals的结构示意图
    • ③ 调用 zend_startup_extensions_mechanism 启动扩展机制。

    • ④ 设置一些使用函数或者值,具体如下:

      /* Set up utility functions and values */
      zend_error_cb  =  utility_functions->error_
          function;
      zend_printf  =  utility_functions->printf_
          function;
      zend_write = (zend_write_func_t) utility_functions->write_function;
      zend_fopen = utility_functions->fopen_function;
      if (! zend_fopen) {
          zend_fopen = zend_fopen_wrapper;
      }
      zend_stream_open_function = utility_functions->stream_open_function;
      zend_message_dispatcher_p = utility_functions->message_handler;
      zend_get_configuration_directive_p  =  utility_functions->get_configuration_
          directive;
      zend_ticks_function = utility_functions->ticks_function;
      zend_on_timeout = utility_functions->on_timeout;
      zend_vspprintf = utility_functions->vspprintf_function;
      zend_vstrpprintf = utility_functions->vstrpprintf_function;
      zend_getenv = utility_functions->getenv_function;
      zend_resolve_path = utility_functions->resolve_path_function;
      zend_interrupt_function = NULL;
    • ⑤ 设置词法和语法解析的入口函数 compile_file 以及执行的入口函数 execute_ex

      zend_compile_file = compile_file;
      zend_execute_ex = execute_ex;

      PHP 7 的 “编译” 入口是函数 compile_file,这是词法和语法解析的入口;而对 opcodes 进行执行的入口是 execute_ex 函数。

    • ⑥ 调用 zend_init_opcodes_handlers 方法,初始化 Zend 虚拟机的 4597 个 handler。这部分内容具体会在第 11 章展开叙述。

    • ⑦ 对 CG(function_table)CG(class_table)CG(auto_globals) 以及 EG(zend_constants) 进行初始化:

      GLOBAL_FUNCTION_TABLE = (HashTable *) malloc(sizeof(HashTable));
      GLOBAL_CLASS_TABLE = (HashTable *) malloc(sizeof(HashTable));
      GLOBAL_AUTO_GLOBALS_TABLE = (HashTable *) malloc(sizeof(HashTable));
      GLOBAL_CONSTANTS_TABLE = (HashTable *) malloc(sizeof(HashTable));
      
      zend_hash_init_ex(GLOBAL_FUNCTION_TABLE, 1024, NULL, ZEND_FUNCTION_DTOR, 1, 0);
      zend_hash_init_ex(GLOBAL_CLASS_TABLE, 64, NULL, ZEND_CLASS_DTOR, 1, 0);
      zend_hash_init_ex(GLOBAL_AUTO_GLOBALS_TABLE, 8, NULL, auto_global_dtor, 1, 0);
      zend_hash_init_ex(GLOBAL_CONSTANTS_TABLE, 128, NULL, ZEND_CONSTANT_DTOR, 1, 0);
    • ⑧ 调用 ini_scanner_globals_ctorini_scanner_globals 进行初始化,这部分会在第 8 章详细展开叙述。

    • ⑨ 调用 php_scanner_globals_ctor 对全局变量 language_scanner_globals 进行初始化,对应的宏是 LANG_SCNG(v),会在词法分析中记录一些关键信息,其结构如下。

    • 调用 zend_set_default_compile_time_values 函数,设置编译时的一些值;同时将 error_reporting 默认设置为 E_ALL & ~E_NOTICE

    • 调用 zend_interned_strings_init 函数,初始化内部字符串,见第 4 章。

    • 调用 zend_startup_builtin_functions 函数,初始化内部函数,见第 14 章内部函数相关内容。

    • 调用 zend_register_standard_constants 函数注册常量:

      REGISTER_MAIN_LONG_CONSTANT("E_ERROR", E_ERROR, CONST_PERSISTENT | CONST_CS);
      REGISTER_MAIN_LONG_CONSTANT("E_RECOVERABLE_ERROR",  E_RECOVERABLE_ERROR,  CONST_PERSISTENT | CONST_CS);
      REGISTER_MAIN_LONG_CONSTANT("E_WARNING", E_WARNING, CONST_PERSISTENT | CONST_CS);
    • 调用 zend_register_auto_global 函数,将 GLOBALS 添加到 CG(auto_globals) 变量表中。

    • 调用 zend_init_rsrc_plist 函数,初始化持久化符号表。

    • 调用 zend_init_exception_opzend_init_call_trampoline_op 函数,分别初始化 EG(exception_op)EG(call_trampoline_op)

    • 调用 zend_ini_startup 函数,初始化与配置文件 php.ini 解析相关的变量,具体会在第 8 章阐述。

  6. 调用 zend_register_list_destructors_ex 函数,注册析构函数 list

  7. 调用 php_binary_init 函数,获取 PHP 执行的二进制程序的路径。

  8. 调用 php_output_register_constants 函数,初始化输出相关的预定义常量,代码如下:

    PHPAPI void php_output_register_constants(void)
    {
        REGISTER_MAIN_LONG_CONSTANT("PHP_OUTPUT_HANDLER_START",  PHP_OUTPUT_HANDLER_
            START, CONST_CS | CONST_PERSISTENT);
        REGISTER_MAIN_LONG_CONSTANT("PHP_OUTPUT_HANDLER_WRITE",  PHP_OUTPUT_HANDLER_
            WRITE, CONST_CS | CONST_PERSISTENT);
        //代码省略//
  9. 调用 php_rfc1867_register_constant 注册文件上传相关的预定义常量。

  10. 调用 php_init_config 函数,会先读取 php.ini 文件,然后调用 zend_parse_ini_file 进行解析,并注册。

  11. 调用 zend_register_standard_ini_entries 函数,注册 ini 相关的变量。

  12. 调用 php_startup_auto_globals 函数,注册全局变量,如 _GET/_POST 等,代码如下:

    void php_startup_auto_globals(void)
    {
        zend_register_auto_global(zend_string_init("_GET",  sizeof("_GET")-1,  1),  0,
            php_auto_globals_create_get);
        zend_register_auto_global(zend_string_init("_POST", sizeof("_POST")-1, 1), 0,
            php_auto_globals_create_post);
        zend_register_auto_global(zend_string_init("_COOKIE", sizeof("_COOKIE")-1, 1), 0,
            php_auto_globals_create_cookie);
        zend_register_auto_global(zend_string_init("_SERVER", sizeof("_SERVER")-1, 1),
            PG(auto_globals_jit), php_auto_globals_create_server);
        zend_register_auto_global(zend_string_init("_ENV",  sizeof("_ENV")-1,  1),
            PG(auto_globals_jit), php_auto_globals_create_env);
        zend_register_auto_global(zend_string_init("_REQUEST",  sizeof("_REQUEST")-1,
            1), PG(auto_globals_jit), php_auto_globals_create_request);
        zend_register_auto_global(zend_string_init("_FILES", sizeof("_FILES")-1, 1), 0,
            php_auto_globals_create_files);
    }
  13. 初始化 SAPI 对于不同类型内容的处理函数,对应函数为 php_startup_sapi_content_types

    int php_startup_sapi_content_types(void)
    {
        sapi_register_default_post_reader(php_default_post_reader);
        sapi_register_treat_data(php_default_treat_data);
        sapi_register_input_filter(php_default_input_filter, NULL);
        return SUCCESS;
    }
  14. 函数 php_register_internal_extensionsphp_register_extensions_bc 分别为注册内部扩展和附加 PHP 扩展。

  15. zend_startup_extensionszend_startup_modules 启动扩展与模块。

  16. 对在 php.ini 中设置的禁用函数和禁用类进行设置,函数分别是 php_disable_functionsphp_disable_classes

模块初始化阶段做的事情比较多,对于 FPM 模式,进程启动后只会进行一次模块初始化,进而进入循环,进行请求的初始化。同样对于 CLI 模式,模块初始化完成后,也是进入请求初始化阶段。

请求初始化阶段

请求初始化阶段的函数入口为 php_request_startup,其执行过程如图7-7所示。

image 2024 06 09 23 57 32 687
Figure 6. 图7-7 请求初始化阶段的执行过程

对于图7-7,我们具体分析一下各函数的作用。

  1. 调用 php_output_activate 函数,重置 output_globals,初始化输出 handler 的栈,并把 OG(flags) 置为使用中:

    memset(&output_globals, 0, sizeof(zend_output_globals));
    zend_stack_init(&OG(handlers), sizeof(php_output_handler *));
    OG(flags) |= PHP_OUTPUT_ACTIVATED;
  2. 调用 zend_activate 函数:

    • gc_reset 函数初始化垃圾回收相关变量和函数。

    • init_compile 函数初始化编译器以及 CG

    • init_executor 函数初始化执行器以及 EG

    • startup_scanner 函数初始化扫描器以及 SCNG

  3. 调用 sapi_activate 函数,对 SG 进行初始化。

  4. 调用 zend_signal_activate 函数,对一些信号进行处理。

  5. 调用 zend_activate_modules 函数,回调各扩展的定义的 request_startup 钩子函数。

完成请求初始化后,进入核心的执行阶段。

执行阶段

执行阶段的入口函数是 php_execute_script,该函数会调用 zend_execute_scripts,该函数通过调用 compile_file 对 PHP 代码进行词法和语法分析,生成 AST,进而生成 op_array。执行阶段的执行过程如图7-8所示。

image 2024 06 10 00 03 31 998
Figure 7. 图7-8 执行阶段的执行过程

这部分内容非常关键,我们会分章详细展开。在 zend_compile 中,首先通过函数 zendparse 进行词法和语法分析,生成抽象语法树,然后调用 init_op_arrayzend_compile_top_stmtpass_two 函数将抽象语法树转为 op_array,进一步调用 zend_executeZend 虚拟机中执行。

执行阶段的词法和语法分析会在第 11 章详细展开,而对 op_array 的执行会在第 12 章展开。执行阶段完成后,会进入请求关闭阶段。

请求关闭阶段

请求关闭阶段的入口函数为 php_request_shutdown,整个阶段分成了 16 步,如图7-9所示。

image 2024 06 10 00 05 21 342
Figure 8. 图7-9 请求关闭阶段函数调用图

请求关闭阶段,一共有 16 个过程,PHP 7 源码对此有清晰的注释,主要工作如下。

  1. 调用各模块中注册的关闭函数和析构函数。

  2. 将输出缓冲器中的内容输出。

  3. 调用所有扩展注册的钩子 RSHUTDOWN 函数。

  4. 销毁 request 相关的全局变量,关闭编译器和执行器。

  5. 还原 ini 配置。

完成这些工作后,FPM 模式会循环等待请求到来,继续进行请求的初始化,而 CLI 模式将进入最后一个阶段,即模块关闭阶段。

模块关闭阶段

模块关闭阶段的入口函数为 php_module_shutdown,这个阶段与模块初始化阶段基本是相反的,用于对各种初始化的变量进行销毁。具体执行过程如图7-10所示。

image 2024 06 10 00 06 49 262
Figure 9. 图7-10 模块关闭阶段

主要工作如下:

  1. 调用加载模块对应的 flush 函数,清理持久化符号表,销毁所有模块;

  2. 关闭与 php.ini 配置文件解析相关的变量和函数;

  3. 关闭内存管理和垃圾回收机制;

  4. 关闭 output 输出相关的信息;

  5. 销毁 core_globals

到此,PHP 7 生命周期的 5 个阶段,我们整体过了一下,并了解了每个阶段的主要工作,同时建议读者使用 gdb 在 CLI 模式下,按照本节给出的函数调用关系,从 main 函数开始,一步一步地调试一下,能更深刻地理解整个 PHP 7 的生命周期。

动手使用 gdb 按照函数调用关系一步一步地跟踪,能得到比书中更多的收获,也能更好地阅读和理解 PHP 7 的源码。

其它工作

当我们在 CLI 模式下执行 PHP 的时候,可以输入特定参数执行特定的工作,比如 php -v php -l 等,其全部定义在函数 php_cli_usage 中,代码如下:

printf( "Usage: %s [options] [-f] <file> [--] [args...]\n"
        "   %s [options] -r <code> [--] [args...]\n"
        "   %s [options] [-B <begin_code>] -R <code> [-E <end_code>] [--] [args...]\n"
        "   %s [options] [-B <begin_code>] -F <file> [-E <end_code>] [--] [args...]\n"
        "   %s [options] -S <addr>:<port> [-t docroot]\n"
        "   %s [options] -- [args...]\n"
        "   %s [options] -a\n"
        "\n"
        "  -c <path>|<file> Look for php.ini file in this directory\n"
        "  -n No configuration (ini) files will be used\n"
    //代码省略//

下面我们以 php -l 为例来看一下其具体工作,代码如下:

PHPAPI int php_lint_script(zend_file_handle *file)
{
    zend_op_array *op_array;
    int retval = FAILURE;

    zend_try {
        op_array = zend_compile_file(file, ZEND_INCLUDE);
        zend_destroy_file_handle(file);

        if (op_array) {
            destroy_op_array(op_array);
            efree(op_array);
            retval = SUCCESS;
        }
    } zend_end_try();
    if (EG(exception)) {
        zend_exception_error(EG(exception), E_ERROR);
    }

    return retval;
}

可以看出,这个命令调用了 zend_compile_file 做词法和语法分析,以校验语法的正确性。

到这里,我们了解了 PHP 7 生命周期的 5 大阶段,分别是模块初始化阶段、请求初始化阶段、执行阶段、请求关闭阶段以及模块关闭阶段。模块初始化阶段会调用扩展注册的钩子函数,会调用不同模式对应的初始化函数,这样方便了开发者开发扩展,以及各种不同模式的开发,比如常见的 CLI 模式和 FPM 模式,以及其他模式的开发,这些模式都是基于 SAPI 实现的。接下来我们分析 FPM 模式下的生命周期。