编程

PHP 扩展设计之:PHP 生命周期详解

9 2026-09-24 17:57:00

理解 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 内存管理器会通过它跟踪你分配的内存,当请求关闭时,如果(你本不应忘记)忘记释放请求绑定的内存,它将尝试释放这些内存。

在此处,你不应使用持久动态内存,即 libcmalloc() 或 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 源代码,了解它们何时以及如何被触发。