# 学习 PHP 生命周期

PHP的生命周期是一个很复杂的过程，其生命周期应该被热衷于使用它的人所掌握。主要内容如下:

PHP 启动。如果运行的是 CLI 或者 FPM，它将运行 C `main()`。如果作为模块运行到网络服务器，像使用 apxs2 SAPI (Apache 2)，则 PHP 在 Apache 启动后不久启动，并开始运行其模块的启动序列，PHP 就是其中之一。在内部称启动为**模块启动步骤**。我们也将其缩写为**MINIT**步骤。

一旦启动，PHP 将等待处理一个/几个请求。当我们谈论 PHP CLI时，将只有一个请求：当前脚本要运行。但是，当我们谈论 Web 环境时——应该是 PHP-FPM 或 Web 服务器模块——PHP 可以一个接一个地处理多个请求。这完全依赖于你如何配置你的 Web 服务器：你可以告诉它处理无限数量的请求，或在关闭并回收该过程之前处理特定数量的请求。每次一个新的请求在线程中要处理时，PHP 就会运行**请求启动步骤**。我们称之为 **RINIT**。

请求得到处理，（可能）生成了一些内容，OK。是时候关闭请求，并准备好处理另一个请求。关闭请求调用**请求关闭步骤**。我们称之为**RSHUTDOWN**。·

当处理完X个请求（一个，几十个，数千个等），PHP 最后会自行关闭，然后结束。关闭 PHP 进程称为**模块关闭步骤**。缩写为 **MSHUTDOWN**。

如果我们可以画出这些步骤，则可能会得到以下信息：

![../../_images/php_classic_lifetime.png](https://cdn.learnku.com/uploads/images/202002/22/1/Rp1jpYnf6G.png!large)

## 并行模型

在 CLI 环境，任何事都很容易：一个进程处理一个请求：它会启动一个单独的 PHP 脚本，然后结束。CLI 环境是 Web 环境的一种特殊化，它更为复杂。

为了同时处理多个请求，你必须运行并行模型。在 PHP 中存在两种：
-   The process-based model 基于进程的模型
-   The thread-based model 基于线程的模型

使用基于进程的模型，操作系统将每个 PHP 解释器隔离到自己的进程中。这种模型在 Unix 非常普遍。每个请求都有它自己的进程。PHP-CLI、PHP-FPM 和 PHP-CGI 使用该模型。

在基于线程的模型中，每个 PHP 解释器都使用线程库隔离到线程中。这个模型主要用在 Windows 操作系统，但也可以用在大多数的 Unix中。要求 PHP 和其扩展在 ZTS 模式下[被构建](http://www.phpinternalsbook.com/php7/build_system/building_extensions.html)。

这是基于进程的模型：

![../../_images/php_lifetime_process.png](https://cdn.learnku.com/uploads/images/202002/22/1/lKMpVyzv7A.png!large)

这是基于线程的模型：

![../../_images/php_lifetime_thread.png](https://cdn.learnku.com/uploads/images/202002/22/1/76fLC1ZIg9.png!large)

> 注意
> 
> 作为扩展开发者，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 内存管理](http://www.phpinternalsbook.com/php7/memory_management/zend_memory_manager.html) 分配，但是会使用永久分配。不是 `emalloc()`，而是`pemalloc()`。否则会导致崩溃。

在 `MINIT()`中，执行引擎仍未启动，所以不要在没有特别注意的情况下，尝试访问其任何结构。

如果你需要为你的扩展注册 INI 入口，则`MINIT()` 是正确的做法。

如果你要为以后使用而注册只读[zend_strings](http://www.phpinternalsbook.com/php7/internal_types/strings.html)，请使用持久分配了。

如果你需要分配的对象在处理请求时会写入，那么你必须复制它们的内存分配到该请求的线程专用池。记住，你只可以在`MINIT()`中安全地写入全局空间。

> 注意
> 
> 内存管理、分配和调试是[内存管理](http://www.phpinternalsbook.com/php7/memory_management.html)的部分章节。

在 [php_module_startup()](https://github.com/php/php-src/blob/3704947696fe0ee93e025fa85621d297ac7a1e4d/main/main.c#L2009)函数中，通过`zend_startup_modules()`触发 `MINIT()`。

### 模块终止：MSHUTDOWN()

这是 PHP 进程终止步骤。很容易， 基本上，你在这里运行了与`MINIT()`中使用的相反的操作。你释放了资源，取消 INI 设置的注册等等。

再次注意：执行引擎是关闭的，所以你不应在此处访问其任何变量。

由于你在此处不需要请求，所以不应使用[Zend 内存管理](http://www.phpinternalsbook.com/php7/memory_management/zend_memory_manager.html) 的`efree()` 或类似函数去释放资源，但对于释放持久分配，使用`pefree()`。

 在[php_module_shutdown()](https://github.com/php/php-src/blob/3704947696fe0ee93e025fa85621d297ac7a1e4d/main/main.c#L2335)函数中，由`zend_shutdown()`的`zend_destroy_modules()`中触发`MSHUTDOWN()`。

### 请求初始化: RINIT()

刚刚看过的请求，PHP 将在这里处理它。在`RINIT()`中，你引导了处理该精确请求所需的资源。PHP 是一种无共享架构，它提供了[内存管理](http://www.phpinternalsbook.com/php7/memory_management.html)功能。

在 `RINIT()`中，如果需要分配动态内存，你将使用[Zend 内存管理器](http://www.phpinternalsbook.com/php7/memory_management/zend_memory_manager.html)。你将调用 `emalloc()`。[Zend 内存管理器](http://www.phpinternalsbook.com/php7/memory_management/zend_memory_manager.html) 追踪你通过它分配的内存，当请求关闭时，如果你忘记这么做，它将尝试释放请求绑定的内存（你不应这么做）。

在这里，你不应请求持久的动态内存，即 libc 的`malloc()` 或Zend 的`pemalloc()`。如果你在这里请求持久内存，并且忘记释放它，则将造成泄露，并且随着 PHP 处理越来越多的请求而堆积，最终导致进程崩溃(Kernel OOM) ，并且导致机器内存不足。

另外，务必注意不要在这里写入全局空间。如果 PHP 作为选定的并行模型运行到线程中，那么你将修改每个线程池中的上下文（所有与你的请求并行处理的请求），并且如果你没有锁定内存，也可能触发竞争条件。如果你需要全局，你必须保护它们。

> 注意
> 
> 全局范围管理解释在[专用章节](http://www.phpinternalsbook.com/php7/extensions_design/globals_management.html)。

在[php_request_startup()](https://github.com/php/php-src/blob/3704947696fe0ee93e025fa85621d297ac7a1e4d/main/main.c#L1558)函数中，通过`zend_activate_module()`触发`RINIT()`。

### 请求终止： RSHUTDOWN()

这是 PHP 请求终止步骤。PHP 刚结束处理其请求，现在来清理其部分作为无共享架构的内存。接下来的请求不应记住当前请求的任何内容。很容易，基本上，你在此处执行了与`RINIT()`使用的相反的操作。你释放了请求绑定的资源。

由于你在此处使用了请求，你应使用 Zend 内存管理器的`efree()`或类似方式释放资源。如果你忘记释放并且造成泄露，在调试版本下，内存管理器将在进程*stderr*上记录关于泄露的指针的日记，并且将为你释放它们。

给你个主意，`RSHUTDOWN()`将被调用：

-   执行用户区关闭功能后 (`register_shutdown_function()`)
-   在调用每个对象析构函数之后
-   PHP 输出缓冲区刷新之后
-   禁用 max_execution_time 之后

在[php_request_shutdown()](https://github.com/php/php-src/blob/3704947696fe0ee93e025fa85621d297ac7a1e4d/main/main.c#L1758)函数中，通过`zend_deactivate_modules()`触发`RSHUTDOWN()`。

### Post 请求终止： PRSHUTDOWN()

这个钩子很少使用。它在 `RSHUTDOWN()`之后调用，但是中间还会运行一些额外的引擎代码。
尤其是在 Post-RSHUTDOWN 中：

-   PHP 输出缓冲区已关闭，并且它的处理程序已刷新
-   PHP 超全局已经销毁
-   执行引擎已经关闭

这个钩子很少使用。在[php_request_shutdown()](https://github.com/php/php-src/blob/3704947696fe0ee93e025fa85621d297ac7a1e4d/main/main.c#L1758)函数中，通过`zend_post_deactivate_modules()`，在`RSHUTDOWN()`之后被触发。

### 全局初始化： GINIT()

线程库每次弹出线程时都会调用该钩子。如果你使用多进程，当 PHP 启动，仅在触发 `MINIT()` 之前调用此函数。

这里不讲太多细节，只需在这里简单地初始化全局变量，通常初始化为0。全局管理将在专用章节详细说明。

记住，全局变量不会在每次请求后清理。如果你需要为每次新的请求重置它们（可能），那么你必须将这样地进程放到`RINIT()`中。

> 注意
> 
> 全局范围管理在[专用章节](http://www.phpinternalsbook.com/php7/extensions_design/globals_management.html)详细介绍。

### 全局终止： GSHUTDOWN()

在线程库中，每当线程终止时都会调用该钩子。如果你使用多线程，该函数将在 PHP 终止期间（在`MSHUTDOWN()`）被调用一次。

在这里不提供太多细节，你只需简单地在这里取消初始化你的全局变量，通常你不必做什么，但如果在构建全局（`GINIT()`）时分配了资源，在这里的步骤你应该释放它们。

全局管理将在专用章节详细介绍。

记住，全局变量在每次请求后不会清除。即`GSHUTDOWN()`不会作为`RSHUTDOWN()`的一部分被调用。

> 注意
> 
> 全局范围管理在[专用章节](http://www.phpinternalsbook.com/php7/extensions_design/globals_management.html)有详细介绍。

### 信息收集: MINFO()

该钩子很特殊，它永远不会被引擎自动触发，只有你询问它有关扩展的信息时才会触发。典型的例子是调用`phpinfo()`。然后运行此函数，并将有关当前扩展的特殊信息打印到流中。

简而言之，`phpinfo()` 展示信息。

该函数也可以通过 CLI 使用反射开关之一调用，例如`php --ri pib` 或通过用户区调用`ini_get_all()`。

你可以将其留空，在这种情况下，只有扩展的名字显示，没有其他（可能不会显示 INI 设置，因为这是 MINFO() 的一部分）。

### 关于 PHP 生命周期的思考

![../../_images/php_extensions_lifecycle.png](https://cdn.learnku.com/uploads/images/202002/22/1/tiNhxaObbD.png!large)

你可能已经发现了，`RINIT()` 和 `RSHUTDOWN()` 尤其重要，因为它们在扩展中被触发成千上万次。如果 PHP 步骤是关于 Web (不是 CLI)，并且已经配置为可以处理无数次请求，那么你的 `RINIT()/RSHUTDOWN()` 组将被无数次调用。

我们想要再次引起你对内存管理的关注。在处理请求时（在`RINIT()` 和 `RSHUTDOWN()`之间），你最终泄露的小字节，将对满载服务器产生严重影响。这就是为什么建议你使用 [Zend 内存管理器](http://www.phpinternalsbook.com/php7/memory_management/zend_memory_manager.html) 进行此类分配，并且准备好[调试内存布局](http://www.phpinternalsbook.com/php7/memory_management/memory_debugging.html)。作为无共享架构的一部分，PHP 在每次请求最后都会忘记并释放请求内存，这是 PHP 的内部设计。

另外，如果你的崩溃信号是 SIGSEGV (坏内存访问)，则整个进程会崩溃。如果 PHP 是使用线程作为多进程引擎，那么你所有其他线程也将崩溃，甚至可能造成服务器崩溃。

> 注意
> 
> 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 源代码查看，并发现何时和如何触发它们。
