CLI模式的生命周期
从版本 4.3.0 开始,PHP 支持一种新类型的 CLI SAPI, CLI 意为 Command Line Interface,即命令行接口。顾名思义,该 CLI SAPI 模块主要用于 PHP 的外壳应用开发。
在 CLI 模式下,PHP 的执行过程主要分为 5 大阶段,分别是模块初始化、请求初始化、执行、请求关闭和模块关闭。这 5 个阶段分别对应 php_module_startup
、php_request_startup
、php_execute_script
、php_request_shutdown
以及 php_module_shutdown
,具体如图7-2所示。

下面我们分别从这 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_model
的 startup
函数,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所示。

对于图7-3,我们具体分析一下各函数的作用:
-
调用
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
中的成员变量进行初始化。 -
调用
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
。 -
调用 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_ctor
为php_output_init_globals
,module_name
为output
,做完替换后,代码如下: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)); }
该函数通过
memset
对output_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;
Figure 3. 图7-4 output_globals的结构示意图因此,全局变量 output_globals 的结构如图7-4所示。
output_globals
也是在全局变量区分配的,大小为 56 字节。php_output_startup
函数对output_globals
初始化后,分别对php_output_handler_aliases
、php_output_handler_conflicts
和php_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所示。Figure 4. 图7-5 core_globals的结构示意图同样,
core_globals
也是在全局变量区申请的,维护了比较多的变量,其大小为 656 字节。后面的分析会经常用到这个全局变量。 -
调用
gc_globals_ctor
函数,对gc_globals
进行初始化,这部分在第 3 章已做了详细阐述,这里不再展开叙述。 -
调用
zend_startup
函数。-
① 调用
start_memory_manager
初始化内存管理,这部分在第 9 章会详细讨论。 -
② 调用
virtual_cwd_startup
初始化cwd_globals
,其中cwd_globals
的结构如图7-6所示。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_ctor
对ini_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_op
和zend_init_call_trampoline_op
函数,分别初始化EG(exception_op)
、EG(call_trampoline_op)
。 -
调用
zend_ini_startup
函数,初始化与配置文件 php.ini 解析相关的变量,具体会在第 8 章阐述。
-
-
调用
zend_register_list_destructors_ex
函数,注册析构函数list
。 -
调用
php_binary_init
函数,获取 PHP 执行的二进制程序的路径。 -
调用
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); //代码省略//
-
调用
php_rfc1867_register_constant
注册文件上传相关的预定义常量。 -
调用
php_init_config
函数,会先读取php.ini
文件,然后调用zend_parse_ini_file
进行解析,并注册。 -
调用
zend_register_standard_ini_entries
函数,注册ini
相关的变量。 -
调用
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); }
-
初始化
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; }
-
函数
php_register_internal_extensions
和php_register_extensions_bc
分别为注册内部扩展和附加 PHP 扩展。 -
zend_startup_extensions
和zend_startup_modules
启动扩展与模块。 -
对在
php.ini
中设置的禁用函数和禁用类进行设置,函数分别是php_disable_functions
和php_disable_classes
。
模块初始化阶段做的事情比较多,对于 FPM 模式,进程启动后只会进行一次模块初始化,进而进入循环,进行请求的初始化。同样对于 CLI 模式,模块初始化完成后,也是进入请求初始化阶段。
请求初始化阶段
请求初始化阶段的函数入口为 php_request_startup
,其执行过程如图7-7所示。

对于图7-7,我们具体分析一下各函数的作用。
-
调用
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;
-
调用
zend_activate
函数:-
①
gc_reset
函数初始化垃圾回收相关变量和函数。 -
②
init_compile
函数初始化编译器以及CG
。 -
③
init_executor
函数初始化执行器以及EG
。 -
④
startup_scanner
函数初始化扫描器以及SCNG
。
-
-
调用
sapi_activate
函数,对SG
进行初始化。 -
调用
zend_signal_activate
函数,对一些信号进行处理。 -
调用
zend_activate_modules
函数,回调各扩展的定义的request_startup
钩子函数。
完成请求初始化后,进入核心的执行阶段。
执行阶段
执行阶段的入口函数是 php_execute_script
,该函数会调用 zend_execute_scripts
,该函数通过调用 compile_file
对 PHP 代码进行词法和语法分析,生成 AST
,进而生成 op_array
。执行阶段的执行过程如图7-8所示。

这部分内容非常关键,我们会分章详细展开。在 zend_compile
中,首先通过函数 zendparse
进行词法和语法分析,生成抽象语法树,然后调用 init_op_array
、zend_compile_top_stmt
和 pass_two
函数将抽象语法树转为 op_array
,进一步调用 zend_execute
在 Zend
虚拟机中执行。
执行阶段的词法和语法分析会在第 11 章详细展开,而对 op_array
的执行会在第 12 章展开。执行阶段完成后,会进入请求关闭阶段。
请求关闭阶段
请求关闭阶段的入口函数为 php_request_shutdown
,整个阶段分成了 16 步,如图7-9所示。

请求关闭阶段,一共有 16 个过程,PHP 7 源码对此有清晰的注释,主要工作如下。
-
调用各模块中注册的关闭函数和析构函数。
-
将输出缓冲器中的内容输出。
-
调用所有扩展注册的钩子
RSHUTDOWN
函数。 -
销毁
request
相关的全局变量,关闭编译器和执行器。 -
还原
ini
配置。
完成这些工作后,FPM 模式会循环等待请求到来,继续进行请求的初始化,而 CLI 模式将进入最后一个阶段,即模块关闭阶段。
模块关闭阶段
模块关闭阶段的入口函数为 php_module_shutdown
,这个阶段与模块初始化阶段基本是相反的,用于对各种初始化的变量进行销毁。具体执行过程如图7-10所示。

主要工作如下:
-
调用加载模块对应的
flush
函数,清理持久化符号表,销毁所有模块; -
关闭与
php.ini
配置文件解析相关的变量和函数; -
关闭内存管理和垃圾回收机制;
-
关闭
output
输出相关的信息; -
销毁
core_globals
。
到此,PHP 7 生命周期的 5 个阶段,我们整体过了一下,并了解了每个阶段的主要工作,同时建议读者使用 gdb
在 CLI 模式下,按照本节给出的函数调用关系,从 main 函数开始,一步一步地调试一下,能更深刻地理解整个 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 模式下的生命周期。