学习 PHP 生命周期

PHP 是一个复杂的机器,任何想了解 PHP 如何运行的人都应该了解它的生命周期。主要顺序如下:

PHP 启动。如果运行 CLI 或 FPM,则运行其 C main()。如果作为模块运行到 Web 服务器中,例如使用 apxs2 SAPI (Apache 2),则 PHP 在 Apache 本身启动后不久启动,并开始运行其模块的启动顺序,PHP 就是其中之一。启动,在内部称为模块启动步骤。我们也将其缩写为 MINIT 步骤。

一旦启动,PHP 就会等待处理一个/多个请求。当我们谈论 PHP CLI 时,只有一个请求:要运行的当前脚本。但是,当我们谈论 Web 环境时(应该是 PHP-FPM 还是 Web 服务器模块),PHP 可以一个接一个地处理多个请求。这完全取决于您如何配置 Web 服务器:您可以告诉它处理无限数量的请求,或者在关闭并回收进程之前处理特定数量的请求。每次有新的请求到达并需要处理时,PHP 都会运行 请求启动 步骤。我们称之为 RINIT

请求已处理,(可能)生成了一些内容,好了。是时候关闭请求并准备最终处理另一个请求了。关闭请求称为 请求关闭步骤。我们称之为 RSHUTDOWN

在处理了 X 个请求(一个、几十个、数千个等)后,PHP 最终会自行关闭并死亡。关闭 PHP 进程称为 模块关闭步骤。我们将其缩写为 MSHUTDOWN

如果我们绘制这些步骤,可能会得到类似以下内容:

image 2024 07 20 23 57 25 851

并行模型

在 CLI 环境中,一切都很简单:一个 PHP 进程将处理一个请求:它将启动一个单独的 PHP 脚本,然后死亡。CLI 环境是 Web 环境的一个特化,它更为复杂。

要同时处理多个请求,您需要运行并行模型。PHP 中存在两种并行模型:

  • 基于进程的模型

  • 基于线程的模型

使用基于进程的模型,每个 PHP 解释器都被操作系统隔离到自己的进程中。这种模型在 Unix 下非常常见。每个请求都进入自己的进程。PHP-CLI、PHP-FPM 和 PHP-CGI 都使用此模型。

使用基于线程的模型,每个 PHP 解释器都使用线程库隔离到一个线程中。此模型主要用于 Microsoft Windows 操作系统,但也可以用于大多数 Unix。这要求在 ZTS 模式下构建 PHP 及其扩展。

以下是基于进程的模型:

image 2024 07 21 00 00 29 803

这是基于线程的模型:

image 2024 07 21 00 01 20 466

作为扩展开发者,你无法选择 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;
};

现在让我们看看你应该在这些钩子中写什么样的代码。

模块初始化: MINIT()

这是 PHP 进程启动步骤。在扩展的 MINIT() 中,您将加载和分配每个未来请求所需的任何持久对象或信息。对于其中的大部分,这些分配将针对只读对象。

MINIT() 中,尚未弹出任何线程或进程,因此您可以完全访问全局变量而不受任何保护。此外,您不能分配请求绑定的内存,因为请求尚未启动。您永远不会在 MINIT() 步骤中使用 Zend Memory Manager 分配,而是使用持久分配。不是 emalloc(),而是 pemalloc()。不这样做会导致崩溃。

MINIT() 中,执行引擎尚未启动,因此请注意不要在没有特别注意的情况下尝试访问其任何结构。

如果您需要为扩展注册 INI 条目,MINIT() 是执行此操作的正确步骤。

如果您需要注册只读 zend_strings 以供进一步使用,那么现在是时候这样做了(使用持久分配)。

如果您需要分配将在处理请求时写入的对象,那么您需要将其内存分配复制到请求的特定线程池中。请记住,您只能在进入 MINIT() 时安全地写入全局空间。

内存管理、分配和调试;是内存管理章节的一部分。

MINIT()php_module_startup() 函数中的 zend_startup_modules() 触发。

模块终止:MSHUTDOWN()

这是 PHP 进程关闭步骤。非常简单,您在这里执行的操作与 MINIT() 中执行的操作完全相反。释放资源、取消注册 INI 设置等。

再次注意:执行引擎已关闭,因此您不应访问其任何变量(但您在这里不需要访问)。

由于您不在此处的请求中,因此您不应使用 Zend Memory Manager efree() 或类似方法释放资源,但应释放持久分配,即 pefree()

MSHUTDOWN()php_module_shutdown() 函数中的 zend_shutdown() 中的 zend_destroy_modules() 触发。

请求初始化:RINIT()

刚刚出现一个请求,PHP 即将在此处理它。在 RINIT() 中,您将引导处理该精确请求所需的资源。PHP 是一种无共享架构,并且按原样提供内存管理功能。

RINIT() 中,如果您需要分配动态内存,则可以使用 Zend Memory Manager。您将调用 emalloc()。Zend Memory Manager 会跟踪您通过它分配的内存,当请求关闭时,如果您忘记这样做(您不应该这样做),它将尝试释放请求绑定的内存。

您不应该在这里要求持久动态内存,又名 libcmalloc() 或 Zend 的 pemalloc()。如果您在这里要求持久内存,但忘记释放它,那么您将创建泄漏,这些泄漏将在 PHP 处理越来越多的请求时堆积起来,最终导致进程崩溃(内核 OOM)并使机器内存不足。

此外,请特别注意不要在此处写入全局空间。如果 PHP 按照所选的并行模型运行到线程中,那么您将修改池中每个线程的上下文(与您的请求并行处理的每个其他请求),并且如果您不锁定内存,还可能触发竞争条件。如果您需要全局变量,则需要保护它们。

全局范围管理已在专门章节中进行说明。

RINIT()php_request_startup() 函数中的 zend_activate_module() 触发。

请求终止:RSHUTDOWN()

这是 PHP 请求关闭步骤。PHP 刚刚处理完其请求,现在它清理部分内存作为无共享架构。接下来的请求不应该记住当前请求中的任何内容。很简单,您基本上在这里执行与 RINIT() 中使用的完全相反的操作。您释放请求绑定的资源。

当您在此处处于请求中时,您应该使用 Zend Memory Manager efree() 或类似方法释放资源。如果您忘记释放并泄漏,在调试版本中,内存管理器将在进程 stderr 上推送有关您正在泄漏的指针的日志,并为您释放它们。

为了让您有个概念,RSHUTDOWN() 被调用:

  • 在执行用户空间关闭函数 (register_shutdown_function()) 之后

  • 在调用每个对象析构函数之后

  • 在刷新 PHP 输出缓冲区之后

  • 在禁用 max_execution_time 之后

RSHUTDOWN()php_request_shutdown() 函数中的 zend_deactivate_modules() 触发。

请求终止后:PRSHUTDOWN()

此钩子很少使用。它在 RSHUTDOWN() 之后调用,但中间会运行一些额外的引擎代码。

特别是在 RSHUTDOWN 后:

  • PHP 输出缓冲区已关闭,其处理程序已刷新

  • PHP 超全局变量已被销毁

  • 执行引擎已关闭

此钩子很少使用。它在 RSHUTDOWN() 之后不久由 php_request_shutdown() 函数中的 zend_post_deactivate_modules() 触发。

全局初始化:GINIT()

每次线程库弹出一个线程时,都会调用此钩子。如果您使用进程作为多处理工具,则此函数只会在 PHP 启动时(即在触发 MINIT() 之前)调用一次。

这里不提供太多细节,您只需在此处初始化全局变量,通常将其初始化为零值。全局变量管理将在其专门章节中介绍。

请记住,全局变量不会在每次请求后清除。如果您需要为每个新请求重置它们(很可能),那么您需要将这样的过程放入 RINIT() 中。

全局范围管理已在专门章节中进行说明。

全局终止:GSHUTDOWN()

每次线程从 Threading 库中死亡时,都会调用此钩子。如果您将进程用作多处理设施,则此函数仅被调用一次,作为 PHP 关闭的一部分(在 MSHUTDOWN() 期间)。

这里不提供太多细节,您只需在此处取消初始化全局变量,通常您无需执行任何操作,但如果您在构造全局变量(GINIT())时分配了资源,则此处是您应该释放它们的步骤。

全局变量管理将在其专门章节中介绍。

请记住,全局变量不会在每次请求后清除;也就是说,GSHUTDOWN() 不会作为 RSHUTDOWN() 的一部分被调用。

全局范围管理已在专门章节中进行说明。

信息收集:MINFO()

该钩子很特殊,因为它永远不会被引擎自动触发,只有当您询问有关扩展的信息时才会触发。典型的用例是调用 phpinfo()。然后运行此函数,并期望它将有关当前扩展的特殊信息打印到流中。

简而言之,phpinfo() 面板信息。

也可以通过 CLI 调用此函数,使用其中一个反射开关(例如 php --ri pib)或通过用户空间调用 ini_get_all() 例如您可以将其留空,在这种情况下,只会显示扩展的名称,而不会显示任何其他内容(可能的 INI 设置不会显示,因为这发生在 MINFO() 中)。

关于 PHP 生命周期的思考

image 2024 07 21 15 10 11 359

您可能已经发现,RINIT()RSHUTDOWN() 尤其重要,因为它们可能会在您的扩展程序上触发数千次。如果 PHP 设置与 Web 有关(而非 CLI),并且已配置为可以处理无限数量的请求,那么您的 RINIT()/RSHUTDOWN() 组合将被无限次调用。

我们想再次引起您对内存管理的关注。在处理请求时(在 RINIT()RSHUTDOWN() 之间)最终会泄漏的微小字节将对满载服务器产生严重影响。这就是为什么建议您使用 Zend Memory Manager 进行此类分配并准备好调试内存布局的原因。作为无共享架构的一部分,PHP 会在每个请求结束时忘记并释放请求内存,这是 PHP 的内部设计。

此外,如果您因 SIGSEGV 信号(错误的内存访问)而崩溃,则整个过程都会崩溃。如果 PHP 设置使用线程作为多处理引擎,则会导致其他每个线程崩溃,甚至可能导致 Web 服务器崩溃。

C 语言不是 PHP 语言。使用 C 语言,程序中的错误和失误很可能会导致程序崩溃和终止。

通过覆盖函数指针挂钩

现在您知道引擎何时会触发您的代码,还有值得注意的函数指针您可以替换以挂接到引擎中。由于这些指针是全局变量,您可以在 MINIT() 步骤中将它们替换 ,然后在 MSHUTDOWN() 中将它们恢复。

感兴趣的是:

  • AST, Zend/zend_ast.h:

    • void (*zend_ast_process_t)(zend_ast *ast)

  • 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)

  • 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)

  • GC, Zend/zend_gc.h:

    • int (*gc_collect_cycles)(void)

  • TSRM, TSRM/TSRM.h:

    • void (*tsrm_thread_begin_func_t)(THREAD_T thread_id)

    • void (*tsrm_thread_end_func_t)(THREAD_T thread_id)

  • Error, Zend/zend.h:

    • void (*zend_error_cb)(int type, const char *error_filename, const uint error_lineno, const char *format, va_list args)

  • Exceptions, Zend/zend_exceptions.h:

    • void (*zend_throw_exception_hook)(zval *ex)

  • 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)

还有其他扩展,但上述扩展是您在设计 PHP 扩展时可能需要的最重要的扩展。由于它们的名称是不言自明的,因此无需详细说明每一个。

如果您需要更多信息,您可以在 PHP 源代码中查找它们,并了解它们的触发时间和方式。