PHP 扩展设计之:PHP 生命周期详解
理解 PHP 的运行周期
PHP 是一台复杂的机器,任何想了解它是如何运作的,都需要理解它的生命周期。其主要运行顺序如下:
PHP 启动。如果运行的是 CLI 或者 FPM,则就会运行其 C main() 函数。如果以网络服务器模块的方式运行,比如 apxs2 SAPI (Apache 2),PHP 在 Apache 启动后不久即启动一系列其模块,PHP 是其中的一个模块。启动,内部称之为模块启动(module statup)步骤,简称为 MINIT 步骤。
启动后,PHP 等待处理一个/多个请求。如果我们谈论的是 PHP CLI,它只有一个请求:即当前运行的脚本。而当我们谈论的是 WEB 环境时—PHP-FPM 或者 Web 服务器模块—PHP 都可以连续处理多个请求。它取决于你如何配置你的 Web 服务器:你可以让他服务于无限数量的请求,或者在处理完特定数量的请求后关闭并重新启动进程。每当有新的请求到达需要处理时,PHP都会执行一个请求启动步骤。我们称之为 RINT((Request Initiation)。
请求已处理,部分内容(可能)已生成,好的。是时候关闭这个请求并准备处理下一个请求了。关闭一个请求的步骤被称为请求关闭(Request Shutdown)步骤。我们称之为 RSHUTDOWN。
在处理了 X 个请求(一个、几十个、几千个等)之后,PHP 最终将自行关闭并退出。关闭 PHP 进程这一步骤被称为模块关闭步骤。我们将其简称为 MSHUTDOWN。
如果我们采取这些步骤,可能会得到这样的结果:

并行化模型
在命令行界面(CLI)环境中,一切都很简单:一个 PHP 进程处理一个请求:它会启动一个单独的 PHP 脚本,然后退出。CLI 环境是 Web 环境的一种特例,而 Web 环境则更为复杂。
为了同时处理多个请求,你需要运行一个并行模型。在 PHP 中存在两种并行模型:
- 基于进程的模型
- 基于线程的模型
使用基于进程的模型时,操作系统会将每个 PHP 解释器隔离到其自己的进程中。这种模型在 Unix 下非常常见。每个请求都会进入其自己的进程。PHP-CLI、PHP-FPM 和 PHP-CGI 都使用这种模型。
在基于线程的模型中,每个 PHP 解释器都被隔离到一个线程中,这是通过使用线程库来实现的。这种模型主要在 Microsoft Windows 操作系统下使用,但也可以用于大多数 Unix 系统。这要求 PHP 及其扩展是在 ZTS 模式下构建的。
以下是基于流程的模型:

以下是基于线程的模型:

注意
作为扩展开发人员,PHP 的多进程模块并非由你选择,而是你必须支持它。你必须接受你的扩展可能在多线程环境中运行的事实,尤其是在 Windows 平台上,并且你必须针对这一环境进行编程。
PHP 扩展钩子
如你所料,PHP 引擎会在多个生命周期点触发你的扩展。我们称这些点为钩子函数。你的扩展在向引擎注册时,可以通过声明函数钩子来表明对特定生命周期点的兴趣。
一旦你分析 PHP 扩展结构,即 zend_module_entry 结构,就能清楚地看到这些挂钩:
struct _zend_module_entry {
unsigned short size;
unsigned int zend_api;
unsigned char zend_debug;
unsigned char zts;
const struct _zend_ini_entry *ini_entry;
const struct _zend_module_dep *deps;
const char *name;
const struct _zend_function_entry *functions;
int (*module_startup_func)(INIT_FUNC_ARGS);/* MINIT() */
int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);/* MSHUTDOWN() */
int (*request_startup_func)(INIT_FUNC_ARGS); /* RINIT() */
int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS); /* RSHUTDOWN() */
void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS); /* PHPINFO() */
const char *version;
size_t globals_size;
#ifdef ZTS
ts_rsrc_id* globals_id_ptr;
#else
void* globals_ptr;
#endif
void (*globals_ctor)(void *global); /* GINIT() */
void (*globals_dtor)(void *global); /* GSHUTDOWN */
int (*post_deactivate_func)(void); /* PRSHUTDOWN() */
int module_started;
unsigned char type;
void *handle;
int module_number;
const char *build_id;
};现在,让我们来看看在这些钩子中应该编写什么样的代码。
模块初始化(Module initialization): MINIT()
这是 PHP 进程的启动步骤。在你的扩展的 MINIT() 函数中,你将加载并分配所有未来请求所需的持久对象或信息。其中大部分分配的对象将是只读的。
在 MINIT() 函数中,尚未有任何线程或进程弹出,因此你可以完全访问全局变量,而无需任何保护。此外,由于请求尚未开始,你不得分配请求绑定的内存。在 MINIT() 步骤中,你永远不要使用 Zend 内存管理器分配,而要使用持久分配。不要使用 emalloc(),而要使用 pemalloc()。否则会导致崩溃。
在 MINIT() 函数中,执行引擎尚未启动,因此请注意,不要在没有特别小心的情况下尝试访问其任何结构。
如果你需要为你的扩展注册 INI 条目,那么 MINIT() 函数就是执行此操作的正确步骤。
如果你需要注册只读的 zend_strings 以供后续使用,那么现在就应该在这里进行注册(使用持久化分配)。
如果你需要在处理请求时分配将被写入的对象,那么你需要将这些对象的内存分配复制到该请求的线程特定池中。请记住,只有在 MINIT() 函数中,你才能安全地写入全局空间
注意
内存管理、分配和调试是内存管理章节的一部分。
MINIT() 函数是由 php_module_startup() 函数中的 zend_startup_modules() 函数触发的。
模块终止: MSHUTDOWN()
这是 PHP 进程关闭的步骤。很简单,你基本上在这里执行的操作与你之前在 MINIT() 中使用的完全相反。你需要释放资源,注销 INI 设置等。
再次提醒:执行引擎已关闭,因此你不应访问其任何变量(但在此处你也不需要访问)。
由于在此处不在请求之中,因此不应使用 Zend 内存管理器 efree() 或类似函数来释放资源,而应使用持久分配释放函数,即 pefree()。
MSHUTDOWN() 函数是由 php_module_shutdown() 函数中的 zend_shutdown() 函数中的 zend_destroy_modules() 函数触发的。
请求初始化(Request initialization): RINIT()
一个请求刚刚出现,PHP 即将在此处理它。在 RINIT() 函数中,你会引导处理该特定请求所需的资源。PHP 采用无共享架构,并且按原样提供内存管理功能。
在 RINIT() 函数中,如果需要分配动态内存,则将使用 Zend 内存管理器。你将调用 emalloc() 函数。Zend 内存管理器会通过它跟踪你分配的内存,当请求关闭时,如果(你本不应忘记)忘记释放请求绑定的内存,它将尝试释放这些内存。
在此处,你不应使用持久动态内存,即 libc 的 malloc() 或 Zend 的 pemalloc()。如果你在此处使用持久化内存,并且忘记释放它,那么随着 PHP 处理越来越多的请求,将会产生内存泄漏,最终导致进程崩溃(内核内存溢出)并耗尽机器内存。
此外,请务必小心,不要在此处写入全局空间。如果 PHP 以所选的并行模型运行在线程中,那么你将需要为池中的每个线程(即与你的请求并行处理的其他每个请求)修改上下文,并且如果不锁定内存,还可能引发竞争条件。如果需要使用全局变量,则必须对其进行保护。
注意
全局作用域管理被单独列为一章进行解释。
RINIT() 是由 php_request_startup() 函数中的 zend_activate_module() 触发的。
请求终止: RSHUTDOWN()
这是 PHP 请求关闭步骤。PHP 刚刚处理完其请求,现在它按照无共享架构清理部分内存。即将到来的请求不应记住当前请求的任何内容。很简单,你基本上是在执行与 RINIT() 中完全相反的操作。你释放了与请求相关的资源。
如果你在此处使用请求,应使用 Zend 内存管理器 efree() 或类似函数来释放资源。如果你忘记释放资源导致内存泄漏,在调试版本中,内存管理器会在进程的标准错误输出(stderr)中记录关于你正在泄漏的指针的日志,并为你释放这些指针。
为了让你有个概念,我们调用 RSHUTDOWN():
- 在执行了用户空间关闭函数(
register_shutdown_function())之后 - 在所有对象的析构函数都被调用之后
- PHP 输出缓冲区刷新后
- 在禁用
max_execution_time之后
RSHUTDOWN() 是由 php_request_shutdown() 函数中的 zend_deactivate_modules() 触发的。
请求后终止(Request Post termination): PRSHUTDOWN()
这个钩子很少使用。它是在 RSHUTDOWN() 之后调用的,但中间会运行一些额外的引擎代码。
尤其是在后 RSHUTDOWN 阶段:
- PHP 输出缓冲区已关闭,其处理程序已刷新
- PHP 超全局变量已被销毁
- 执行引擎已关闭
这个钩子很少使用。它是在 RSHUTDOWN() 之后稍晚一点触发的,具体是由 php_request_shutdown() 函数中的 zend_post_deactivate_modules() 函数触发的
全局初始化: GINIT()
每当线程库弹出一个线程时,就会调用此钩子。如果你使用进程作为多处理工具,则此函数仅在 PHP 启动时调用一次,即在触发 MINIT() 之前。
这里不提供过多细节,你只需在此处初始化全局变量,通常将其值设为零。全局变量的管理将在专门的章节中介绍。
请记住,全局变量并不会在每次请求后被清除。如果你需要为每个新请求重置它们(很可能需要),那么你就需要在 RINIT() 函数中加入这样的程序。
注意
全局作用域管理在专有章节中进行解释。
全局终止: GSHUTDOWN()
每次线程从线程库中退出时,都会调用此挂钩函数。如果使用进程作为多处理工具,则此函数仅在 PHP 关闭时(在 MSHUTDOWN() 期间)调用一次。
这里不提供过多细节,你只需在此处对全局变量进行去初始化即可。通常来说,你无需执行任何操作,但如果你在构造全局变量(GINIT())时分配了资源,那么此处就是你释放这些资源的步骤。
全局管理将在其专门的章节中介绍。
请记住,全局变量并不会在每次请求后被清除;也就是说,GSHUTDOWN() 函数并不会作为 RSHUTDOWN() 函数的一部分被调用。
Note
全局作用域管理在专有章节中进行解释。
信息收集: MINFO()
这个钩子很特别,因为它永远不会由引擎自动触发,而只有在你向它询问有关扩展的信息时才会触发。典型的用例是调用 phpinfo() 函数。然后,这个函数会被运行,并预期会在流中打印出有关当前扩展的特殊信息。
简而言之,就是 phpinfo() 面板信息。
此函数还可以通过命令行界面(CLI)调用,使用诸如 php --ri pib 之类的反射开关,或者通过用户层调用 ini_get_all() 函数
你可以将其留空,在这种情况下,只会显示扩展名的名称,而不会显示其他任何内容(由于这是 MINFO() 函数的一部分,因此不会显示可能的 INI 设置)。
Thoughts on PHP lifecycle

你可能已经注意到,RINIT() 和 RSHUTDOWN() 尤其重要,因为它们在你的扩展中可能会被触发数千次。如果 PHP 设置是针对 Web(而非命令行界面(CLI))的,并且已经配置为可以处理无限数量的请求,那么你的 RINIT()/RSHUTDOWN()组合将被无限次调用。
我们想再次提醒你注意内存管理。在处理请求(在 RINIT() 和 RSHUTDOWN() 之间)时,即使只是泄露了一点点字节,也会对满负荷的服务器产生巨大影响。因此,建议你在进行此类分配时使用 Zend 内存管理器,并随时准备调试内存布局。作为无共享架构的一部分,PHP 会在每个请求结束时忘记并释放请求内存,这是 PHP 的内部设计。
此外,如果由于 SIGSEGV 信号(错误的内存访问)导致崩溃,整个进程都会崩溃。如果 PHP 配置使用了线程作为多处理引擎,那么其他所有线程都会随之崩溃,甚至可能导致 Web 服务器崩溃。
注意
C 语言不是 PHP 语言。使用 C 语言时,程序中的错误和失误极有可能导致程序崩溃并终止。
通过覆盖函数指针进行挂钩
现在你知道引擎何时会触发你的代码,还存在一些明显的函数指针,你可以替换它们以挂接到引擎中。由于这些指针是全局变量,你可以在 MINIT() 步骤中替换它们,并在 MSHUTDOWN() 步骤中将其放回原位。
你可能感兴趣的内容有:
Lifetime, Zend/zend.h:
void (*zend_on_timeout)(int seconds)
void (*zend_interrupt_function)(zend_execute_data *execute_data)
void (*zend_ticks_function)(int ticks)Exceptions, Zend/zend_exceptions.h:
void (*zend_throw_exception_hook)(zval *ex)Error, Zend/zend.h:
void (*zend_error_cb)(int type, const char *error_filename, const uint error_lineno, const char *format, va_list args)TSRM, TSRM/TSRM.h:
void (*tsrm_thread_begin_func_t)(THREAD_T thread_id)
void (*tsrm_thread_end_func_t)(THREAD_T thread_id)GC, Zend/zend_gc.h:
int (*gc_collect_cycles)(void)Executor, Zend/zend_execute.h:
void (*zend_execute_ex)(zend_execute_data *execute_data)
void (*zend_execute_internal)(zend_execute_data *execute_data, zval *return_value)Compiler, Zend/zend_compile.h:
zend_op_array *(*zend_compile_file)(zend_file_handle *file_handle, int type)
zend_op_array *(*zend_compile_string)(zval *source_string, char *filename)AST, Zend/zend_ast.h:
void (*zend_ast_process_t)(zend_ast *ast) (*zend_interrupt_function)(zend_execute_data *execute_data)
void (*zend_ticks_function)(int ticks)Exceptions, Zend/zend_exceptions.h:
void (*zend_throw_exception_hook)(zval *ex)Error, Zend/zend.h:
void (*zend_error_cb)(int type, const char *error_filename, const uint error_lineno, const char *format, va_list args)TSRM, TSRM/TSRM.h:
void (*tsrm_thread_begin_func_t)(THREAD_T thread_id)
void (*tsrm_thread_end_func_t)(THREAD_T thread_id)GC, Zend/zend_gc.h:
int (*gc_collect_cycles)(void)Executor, Zend/zend_execute.h:
void (*zend_execute_ex)(zend_execute_data *execute_data)
void (*zend_execute_internal)(zend_execute_data *execute_data, zval *return_value)Compiler, Zend/zend_compile.h:
zend_op_array *(*zend_compile_file)(zend_file_handle *file_handle, int type)
zend_op_array *(*zend_compile_string)(zval *source_string, char *filename)AST, Zend/zend_ast.h:
void (*zend_ast_process_t)(zend_ast *ast)虽然还有其他存在的,但上述这些是设计 PHP 扩展时你可能用到的最重要的。由于它们的名称已经足够说明问题,所以无需对每个进行详细解释。
如果你需要更多信息,可以查看 PHP 源代码,了解它们何时以及如何被触发。