第7章 生命周期

430 阅读36分钟

本章将对PHP 7的生命周期进行详细的探讨。PHP 7的生命周期主要分为5大阶段,我们会对每个阶段进行细致的研究和阐述,以理解PHP代码的整个执行过程,从而对PHP 7的执行有一个全局的认识。另外,PHP 7有多种模式运行,比如常用的CLI(命令行)模式、FPM模式,以及CGI模式、embed模式、Apache2Handler模式、litespeed模式等,本章主要对CLI模式和FPM模式进行展开,讨论一下各种模式下PHP代码是如何执行的,相信读者学习完本章,会对PHP 7的运行模式有更深刻的理解。

7.1 基础知识

在详细讨论PHP 7的生命周期和运行模式之前,我们先了解一下基础知识,为深入理解PHP 7的原理做一个铺垫。由于PHP进程启动时需要对信号进行处理,首先我们了解一下信号的基本概念。如果读者对这部分内容有详细的了解,可以略过7.1.1节,直接从7.1.2节开始。

7.1.1 信号处理

PHP 7生命周期中会涉及信号的处理,我们首先对UNIX信号的处理做一些了解。UNIX信号有1~63个,其中编号为1~31的信号为传统UNIX支持的信号,是不可靠信号(非实时信号),编号为32~63的信号是后来扩充的,是可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队(多次发送),可能会造成信号丢失,而后者不会,具体如表7-1所示。

表7-1 UNIX信号对照表

在以上列出的信号中:

  1. 程序不可捕获、阻塞或忽略的信号有SIGKILL和SIGSTOP;
  2. 不能恢复至默认动作的信号有SIGILL和SIGTRAP;
  3. 默认会导致进程流产的信号有SIGABRT、SIGBUS、SIGFPE、SIGILL、SIGQUIT、SIGSEGV、SIGTRAP、SIGXCPU和SIGXFSZ;
  4. 默认会导致进程退出的信号有SIGALRM、SIGHUP、SIGINT、SIGKILL、SIGPIPE、SIGPROF、SIGSYS、SIGTERM、SIGUSR1、SIGUSR2和SIGVTALRM;
  5. 默认会导致进程停止的信号有SIGSTOP、SIGTSTP、SIGTTIN和SIGTTOU;
  6. 默认进程忽略的信号有SIGCHLD、SIGPWR、SIGURG和SIGWINCH。

在PHP 7进程启动时,会对一些信号进行屏蔽,另外FPM的master进程会监听一些信号,对worker进行处理。

信号处理还需要了解3个重要函数,如表7-2所示。

表7-2 UNIX信号处理函数

为了理解这3个函数,我们编写代码如下:

#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>

void signal_handler(int signo);

int main(void){
    //设置信号掩码,屏蔽信号:SIGINT(2 非可靠信号Ctrl+C )、SIGRTMIN(34 可靠信号)
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGRTMIN);
    sigprocmask(SIG_BLOCK, &set, NULL);

    //为以下信号安装信号处理器:SIGINT(2 非可靠信号Ctrl+C )、SIGRTMIN(34 可靠信号)、
    //SIGQUIT(3 非可靠信号Ctrl+\)
    struct sigaction sa;
    memset(&sa,0, sizeof(struct sigaction));
    sa.sa_handler = signal_handler;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGRTMIN, &sa, NULL);
    sigaction(SIGQUIT, &sa, NULL);

    int count = 0;
    while(1){
        if(count >= 100){ //休眠100s后,退出
            break;
        }
        printf("sleep ..\n");
        sleep(1);
        if(count > 0 && count%10 == 0){
        //每10s,接收一次信号,接收之后继续屏蔽信号SIGINT、SIGRTMIN
            printf("挂起等待信号..\n");
            sigemptyset(&set);
            sigsuspend(&set);
        }
        count++;
    }
}

void signal_handler(int signo){
//由于信号掩码的设置,该信号处理器被调用的时候,不会被SIGINT、SIGRTMIN打断、干扰
    if(signo == SIGINT){
        printf("catch signal SIGINT:%d\n", signo);
    }else if(signo == SIGRTMIN){
        printf("catch signal SIGRTMIN:%d\n", signo);
    }else if(signo == SIGQUIT){
        printf("catch signal SIGQUIT:%d, exit..\n", signo);
        exit(0);
    }else{
        printf("catch signal :%d\n", signo);
    }
}

代码说明以及程序执行结果如下。

  1. 为SIGINT、SIGRTMIN、SIGQUIT安装了信号处理器signal_handler。信号处理器的逻辑主要是输出,如果是SIGQUIT信号,输出并退出。
  2. 屏蔽了信号SIGINT、SIGRTMIN,这时如果这两个信号进来,那么信号是一直阻塞的状态,也就是信号一直在排队,无法被信号处理器处理。由于SIGQUIT信号没有被阻塞,所以随时可通过该信号终止进程。
  3. 进程会一直在sigsuspend处阻塞;如果产生两个SIGINT信号(Ctrl+C),这时信号处理器会被调用,并提示catch signal SIGINT:2,并且之后的信号等待队列清空;如果10s内产生两个SIGRTMIN信号(kill-34 pid),这时信号处理器会被调用,并提示catch signalSIGRTMIN:34,但信号等待队列不清空。
  4. 一旦sigsuspend等到了信号到来,在调用完信号处理器函数(signal_handler)后,sigsuspend系统调用返回,并恢复屏蔽信号SIGINT、SIGRTMIN。

由此可以得出结论:

  1. 可靠信号(≥34)不会丢失,N个可靠信号经过排队,在信号处理的时候仍然是N个。非可靠信号(<34)会丢失,N个非可靠信号经过排队,在信号处理的时候是1个。
  2. sigprocmask系统调用是设置进程的信号掩码的。信号掩码的意义是,掩码中的信号会进入队列排队处理。
  3. 对于2)中进入队列的信号,进程可以通过sigsuspend(&newMask)从队列中取出阻塞的信号。

注意

信号分为可靠信号和非可靠信号,非可靠信号发送多次会丢失,只保留1个。


了解了信号以及信号的处理函数,我们接下来讨论一个重要的概念——SAPI。SAPI提供了一个接口,使得PHP可以和其他应用交互数据。只要按照SAPI的接口规范,就可以编写不同的运行模式。

7.1.2 SAPI

简介SAPI(Server Application Programimg Interface,服务端应用编程接口)相当于PHP外部环境的代理器。PHP可以应用在终端上,也可以应用在Web服务器中,应用在终端上的SAPI就叫作CLI SAPI,应用在Web服务器中的就叫作CGI SAPI。

SAPI有一个非常核心的数据结构——_sapi_module_struct,它是在文件main/SAPI.h中定义的,定义如下:

struct _sapi_module_struct {
    char *name; // 名字,如cli、 fpm-fcgi等
    char *pretty_name; // 更易理解的名字,比如fpm-fcgi对应的为FPM/FastCGI
    int (*startup)(struct _sapi_module_struct *sapi_module);
    //模块启动时调用的函数
    int (*shutdown)(struct _sapi_module_struct *sapi_module);
    //模块结束时调用的函数
    int (*activate)(void); // 处理request时,激活需要调用的函数指针
    int (*deactivate)(void); // 处理完request时,使要调用的函数指针无效
    size_t (*ub_write)(const char *str, size_t str_length);
    // 这个函数指针用于输出数据
    void (*flush)(void *server_context); // 刷新缓存的函数指针
    zend_stat_t *(*get_stat)(void); // 判断对执行文件是否有执行权限
    char *(*getenv)(char *name, size_t name_len); // 获取环境变量的函数指针
    void (*sapi_error)(int type, const char *error_msg, ...)
        ZEND_ATTRIBUTE_FORMAT(printf, 2, 3); // 错误处理函数指针
    int (*header_handler)(sapi_header_struct *sapi_header,
        sapi_header_op_enum op, sapi_headers_struct *sapi_headers);
        //调用header()时被调用的函数指针
    int (*send_headers)(sapi_headers_struct *sapi_headers);
    // 发送全部header的函数指针
    void (*send_header)(sapi_header_struct *sapi_header, void *server_context);
    // 发送某一个header的函数指针
    size_t (*read_post)(char *buffer, size_t count_bytes);
    // 获取HTTP POST中数据的函数指针
    char *(*read_cookies)(void);  // 获取cookie中数据的函数指针
    void (*register_server_variables)(zval *track_vars_array);
    // 从$_SERVER中获取变量的函数指针
    void (*log_message)(char *message, int syslog_type_int);
    // 输出错误信息函数指针
    double (*get_request_time)(void); // 获取请求时间的函数指针
    void (*terminate_process)(void);  // 调用exit退出时的函数指针
    char *php_ini_path_override;  // PHP的ini文件被复写的地址

    void (*default_post_reader)(void); //负责解析POST数据的函数指针
    void (*treat_data)(int arg, char *str, zval *destArray);
    // 对数据进行处理的函数指针
    char *executable_location; // 执行的地理位置
    int php_ini_ignore; // 是否不使用任何ini配置文件
    int php_ini_ignore_cwd; // 忽略当前路径的php.ini
    int (*get_fd)(int *fd); // 获取执行文件的fd的函数指针
    int (*force_http_10)(void); // 强制使用http 1.0版本的函数指针
    int (*get_target_uid)(uid_t *); // 获取执行程序的uid的函数指针
    int (*get_target_gid)(gid_t *); // 获取执行程序的gid的函数指针
    unsigned int (*input_filter)(int arg, char *var, char **val, size_t val_len,
        size_t *new_val_len);
    // 对输入进行过滤的函数指针。比如将输入参数填充到自动全局变量$_GET、$_POST、$_COOKIE中
    void (*ini_defaults)(HashTable *configuration_hash);
    // 默认的ini配置的函数指针,把ini配置信息存在HashTable中
    int phpinfo_as_text; // 是否输出phpinfo信息

    char *ini_entries; // 执行时附带的ini配置,可以使用php -d设置
    const zend_function_entry *additional_functions;
    // 每个SAPI模块特有的一些函数注册,比如cli的cli_get_process_title
    unsigned int (*input_filter_init)(void);
};

对于_sapi_module_struct这个结构体,每种模式都定义了这个结构体的实现,比如在FPM中:

static sapi_module_struct cgi_sapi_module = {
    "fpm-fcgi",
    "FPM/FastCGI",
    ……

在CLI里面同样有定义:

static sapi_module_struct cli_sapi_module = {
    "cli",
    "Command Line Interface",
    ……

对于每种模式定义的sapi_module_struct,在PHP的生命周期中,会调用其中定义的函数指针来实现各自的功能。以FPM模式下的sapi_cgi_read_cookies为例,调用这个函数可以读取cookie的信息:

static char *sapi_cgi_read_cookies(void) /* {{{ */
{
    fcgi_request *request = (fcgi_request*) SG(server_context);

    return FCGI_GETENV(request, "HTTP_COOKIE");
}

注意

CLI和FPM都是基于SAPI的实现,都定义了sapi_module_struct结构。


SAPI的结构是我们分析PHP 7生命周期的基础,另外还有一个重要的数据结构——sapi_globals,其对应的宏为SG(v),这个结构体中的变量跟生命周期相关,下面我们详细阐述sapi_globals。

7.1.3 SAPI核心结构SG(v)

宏定义SG(v)用于取sapi_globals成员变量的值,代码如下:

# define SG(v) (sapi_globals.v)

sapi_globals对应的结构体为sapi_globals_struct,其结构如图7-1所示。

图7-1 sapi_globals的结构示意图

整个sapi_globals大小为560字节,是在全局变量区分配的。该结构体在PHP 7的生命周期中大量使用,这里读者先对sapi_globals有一个整体的认识。

如图7-1所示,对于FPM模式,比较重要的部分是sapi_request_inforequest_info,对应了HTTP协议中的很多字段。

掌握了信号处理、SAPI的结构体以及SG(v)后,我们从CLI模式入手来详细了解PHP 7的生命周期。

7.2 CLI模式的生命周期

从版本4.3.0开始,PHP支持一种新类型的CLI SAPI, CLI意为CommandLine 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所示。

图7-2 PHP 7生命周期示意图

下面我们分别从这5个阶段详细阐述一下PHP 7的生命周期。

7.2.1 模块初始化阶段

在模块初始化阶段之前,首先调用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 模块初始化流程图

对于图7-3,我们具体分析一下各函数的作用:

  1. 调用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中的成员变量进行初始化。

  2. 调用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。

  3. 调用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;
    

    图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所示。

    图7-5 core_globals的结构示意图

    同样,core_globals也是在全局变量区申请的,维护了比较多的变量,其大小为656字节。后面的分析会经常用到这个全局变量。

  4. 调用gc_globals_ctor函数,对gc_globals进行初始化,这部分在第3章已做了详细阐述,这里不再展开叙述。

  5. 调用zend_startup函数。

    图7-6 cwd_globals的结构示意图

    1. 调用start_memory_manager初始化内存管理,这部分在第9章会详细讨论。
    2. 调用virtual_cwd_startup初始化cwd_globals,其中cwd_globals的结构如图7-6所示。
    3. 调用zend_startup_extensions_mechanism启动扩展机制。
    4. 设置一些使用函数或者值,具体如下:
    /* 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;
    
    1. 设置词法和语法解析的入口函数compile_file以及执行的入口函数execute_ex:
    zend_compile_file = compile_file;
    zend_execute_ex = execute_ex;
    

    注意

    PHP 7的“编译”入口是函数compile_file,这是词法和语法解析的入口;而对opcodes进行执行的入口是execute_ex函数。


    1. 调用zend_init_opcodes_handlers方法,初始化Zend虚拟机的4597个handler。这部分内容具体会在第11章展开叙述。
    2. 对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);
    
    1. 调用ini_scanner_globals_ctor对ini_scanner_globals进行初始化,这部分会在第8章详细展开叙述。

    2. 调用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章阐述。
  6. 调用zend_register_list_destructors_ex函数,注册析构函数list。

  7. 调用php_binary_init函数,获取PHP执行的二进制程序的路径。

  8. 调用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);
        //代码省略//
    
  9. 调用php_rfc1867_register_constant注册文件上传相关的预定义常量。

  10. 调用php_init_config函数,会先读取php.ini文件,然后调用zend_parse_ini_file进行解析,并注册。

  11. 调用zend_register_standard_ini_entries函数,注册ini相关的变量。

  12. 调用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);
    }
    
  13. 初始化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;
    }
    
  14. 函数php_register_internal_extensions和php_register_extensions_bc分别为注册内部扩展和附加PHP扩展。

  15. zend_startup_extensions和zend_startup_modules启动扩展与模块。

  16. 对在php.ini中设置的禁用函数和禁用类进行设置,函数分别是php_disable_functions和php_disable_classes。

模块初始化阶段做的事情比较多,对于FPM模式,进程启动后只会进行一次模块初始化,进而进入循环,进行请求的初始化。同样对于CLI模式,模块初始化完成后,也是进入请求初始化阶段。

7.2.2 请求初始化阶段

请求初始化阶段的函数入口为php_requet_startup,其执行过程如图7-7所示。

图7-7 请求初始化阶段的执行过程

对于图7-7,我们具体分析一下各函数的作用。

  1. 调用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;
    
  2. 调用zend_activate函数:

    ① gc_reset函数初始化垃圾回收相关变量和函数。 ② init_compile函数初始化编译器以及CG。 ③ init_executor函数初始化执行器以及EG。 ④ startup_scanner函数初始化扫描器以及SCNG。

  3. 调用sapi_activate函数,对SG进行初始化。

  4. 调用zend_signal_activate函数,对一些信号进行处理。

  5. 调用zend_activate_modules函数,回调各扩展的定义的request_startup钩子函数。

完成请求初始化后,进入核心的执行阶段。

7.2.3 执行阶段

执行阶段的入口函数是php_execute_script,该函数会调用zend_execute_scripts,该函数通过调用compile_file对PHP代码进行词法和语法分析,生成AST,进而生成op_array。执行阶段的执行过程如图7-8所示。

图7-8 执行阶段的执行过程

这部分内容非常关键,我们会分章详细展开。在zend_compile中,首先通过函数zendparse进行词法和语法分析,生成抽象语法树,然后调用init_op_array、zend_compile_top_stmt和pass_two函数将抽象语法树转为op_array,进一步调用zend_execute在Zend虚拟机中执行。

执行阶段的词法和语法分析会在第11章详细展开,而对op_array的执行会在第12章展开。执行阶段完成后,会进入请求关闭阶段。

7.2.4 请求关闭阶段

请求关闭阶段的入口函数为php_request_shutdown,整个阶段分成了16步,如图7-9所示。

图7-9 请求关闭阶段函数调用图

请求关闭阶段,一共有16个过程,PHP 7源码对此有清晰的注释,主要工作如下。

  1. 调用各模块中注册的关闭函数和析构函数。
  2. 将输出缓冲器中的内容输出。
  3. 调用所有扩展注册的钩子RSHUTDOWN函数。
  4. 销毁request相关的全局变量,关闭编译器和执行器。
  5. 还原ini配置。

完成这些工作后,FPM模式会循环等待请求到来,继续进行请求的初始化,而CLI模式将进入最后一个阶段,即模块关闭阶段。

7.2.5 模块关闭阶段

模块关闭阶段的入口函数为php_module_shutdown,这个阶段与模块初始化阶段基本是相反的,用于对各种初始化的变量进行销毁。具体执行过程如图7-10所示。

图7-10 模块关闭阶段

主要工作如下:

  1. 调用加载模块对应的flush函数,清理持久化符号表,销毁所有模块;
  2. 关闭与php.ini配置文件解析相关的变量和函数;
  3. 关闭内存管理和垃圾回收机制;
  4. 关闭output输出相关的信息;
  5. 销毁core_globals。

到此,PHP 7生命周期的5个阶段,我们整体过了一下,并了解了每个阶段的主要工作,同时建议读者使用gdb在CLI模式下,按照本节给出的函数调用关系,从main函数开始,一步一步地调试一下,能更深刻地理解整个PHP 7的生命周期。


提示

动手使用gdb按照函数调用关系一步一步地跟踪,能得到比书中更多的收获,也能更好地阅读和理解PHP 7的源码。


7.2.6 其他工作

当我们在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模式下的生命周期。

7.3 FPM模式的生命周期

图7-11 FPM模式的生命周期

FPM(FastCGI Process Manager)是一个FastCGI进程管理器,对于PHP 5.3.3之前的PHP来说,它只是一个补丁包。从PHP 5.3.3开始,PHP集成了PHP-FPM。PHP-FPM提供了更好的PHP进程管理方式,可以有效控制内存和进程,支持平滑重启PHP及重载PHP配置。

与CLI模式类似,FPM模式的生命周期也有5个阶段,但是又与CLI模式的生命周期不同,因为FPM是常驻内存的进程,所以其模块初始化只做一次,便进入循环,而模块关闭在进程退出时也只做一次,如图7-11所示。

  1. 调用php_module_startup,加载所有模块。
  2. 进入循环,调用fcgi_accept_request实际调用的是accept,阻塞等待请求;如果有请求进来,会被唤起,进入php_request_startup,初始化请求。为了防止多个进程对accept进行抢占,出现“惊群”情况,增加了锁机制:
FCGI_LOCK(req->listen_socket);
req->fd = accept(listen_socket, (struct sockaddr *)&sa, &len);
FCGI_UNLOCK(req->listen_socket);

但是细心的读者可以发现,FCGI_LOCK/FCGI_UNLOCK在Linux下已经没有实现了:

#  define FCGI_LOCK(fd)
#  define FCGI_UNLOCK(fd)

这是因为在Linux 2.6内核上,阻塞版本的accept系统调用已经不存在“惊群”了。

  1. 进入php_execute_script,对脚本执行编译。
  2. 调用php_request_shutdown关闭请求,继续进入循环。
  3. 如果进程退出,调用php_module_shutdown关闭所有模块。
  4. 如果请求次数大于max_requests,则跳转5。

注意

在Linux 2.6内核上,阻塞版本的accept系统调用已经不存在“惊群”了。大家可以写一个简单的程序测试下,并在父进程中绑定、监听,然后fork出子进程,所有的子进程都会尝试接受(accept)这个监听句柄。这样,当新连接过来时,大家会发现,仅有一个子进程返回新建的连接,其他子进程继续休眠在accept调用上,没有被唤醒。


了解了FPM的生命周期,下面具体分析一下整个过程。

7.3.1 多进程管理

PHP-FPM是多进程的服务,其中有一个master进程(做管理工作)和多个worker进程(处理数据请求)。下面我们从多进程管理角度对PHP-FPM展开阐述,首先讨论master进程和worker进程是如何创建的,然后讨论进程之间是如何通信的,比如worker进程意外退出,master进程是如何感知并重新创建新的worker进程的。

  1. 进程创建

    我们以Nginx+PHP-FPM方式为例,讲一下整个Web请求的过程。一般情况下,Nginx会根据服务器的CPU内核数设置worker的进程数,而PHP-FPM的进程有三种设置方式:static、dynamic和ondemand,可以在php-fpm.conf里面设置:

    pm = static   //其他:dynamic或者ondemand
    
    1. static模式 static模式始终会保持一个固定数量的子进程,这个数量由pm.max_children定义,比如线上,我们可以将其设置为512个worker,我们可以观察PHP-FPM的进程空闲数,如图7-12所示。

      图7-12 PHP-FPM空闲数

      从图7-12可以看出,随着请求量的变化,PHP-FPM的空闲数也发生了变化。

    2. dynamic模式

      子进程的数量是动态变化的。启动时,会生成固定数量的子进程,可以理解成最小子进程数,通过pm.start_servers控制,而最大子进程数则由pm.max_children控制,子进程数会在pm.start_servers~pm.max_children范围内变化,另外,闲置的子进程数还可以由pm.min_spare_servers和pm.max_spare_servers两个配置参数控制。换句话说,闲置的子进程也可以有最小数目和最大数目,而如果闲置的子进程超出了pm.max_spare_servers,则会被杀掉。

    3. ondemand模式

      这种模式和dynamic模式正好相反,把内存放在第一位,每个闲置进程在持续闲置了pm.process_idle_timeout秒后就会被杀掉。有了这个模式,到了服务器低峰期,内存自然会降下来,如果服务器长时间没有请求,就只会有一个PHP-FPM主进程,当然其弊端是,遇到高峰期或者pm.process_idle_timeout的值太小的话,无法避免服务器频繁创建进程的问题。

    3种模式对应的定义如下:

    enum {
        PM_STYLE_STATIC = 1,
        PM_STYLE_DYNAMIC = 2,
        PM_STYLE_ONDEMAND = 3
    };
    

    我们了解了PHP-FPM的3种运行模式,接下来继续介绍整个webserver的运行过程,如图7-13所示。

    图7-13 Client/Nginx/PHP-FPM通信示意图

    从图7-13可以看到,Client通过HTTP方式请求Nginx,请求由Nginx的worker进行处理,转成对应的FastCGI,请求FPM, accept由FPM的worker进程处理,执行完毕再返回给Nginx, Nginx再进一步返回给Client。

    下面我们详细讨论一下PHP-FPM进程是怎么启动的。我们使用gdb来启动PHP-FPM,其中PHP-FPM在sbin目录下:

    gdb sbin/php-fpm
    (gdb) b main
    (gdb) r -y etc/php-fpm.conf
    

    在main函数入口处增加断点,然后使用r -y etc/php-fpm.conf指定加载的配置文件,此时启动的进程并不是master进程,而是calling process进程,callingprocess进程会fork出master进程,并退出。为了能够跟随master进程,我们使用gdb里面的命令,以对子进程进行跟踪:

    (gdb) set follow-fork-mode child
    

    输入c命令(continue):

    (gdb) c
    Continuing.
    Breakpoint 2, 0x0000003dcbcacd14 in fork () from /lib64/libc.so.6
    (gdb) bt
    #0  0x0000003dcbcacd14 in fork () from /lib64/libc.so.6
    #1  0x0000000000aa234b in fpm_unix_init_main () at /root/php7/book/php-7.1.0/sapi/
        fpm/fpm/fpm_unix.c:495
    #2  0x0000000000a8c04f in fpm_init (argc=3, argv=0x7fffffffe178, config=0x7fffffffe479
        "etc/php-fpm.conf", prefix=0x0, pid=0x0, test_conf=0, run_as_root=0, force_
        daemon=-1,
        force_stderr=0) at /root/php7/book/php-7.1.0/sapi/fpm/fpm/fpm.c:61
    #3  0x0000000000a997a5 in main (argc=3, argv=0x7fffffffe178) at /root/php7/book/
        php-7.1.0/sapi/fpm/fpm/fpm_main.c:1
    (gdb) n
    Single stepping until exit from function fork,
    which has no line number information.
    [New process 16299]
    [Thread debugging using libthread_db enabled]
    [Switching to Thread 0x7ffff7fe0700 (LWP 16299)]
    0x0000003dcc005810 in __nptl_set_robust () from /lib64/libpthread.so.0
    

    我们可以看到,calling process会在fpm-init函数中将master进程fork出来,同时自己退出。对应的代码如下:

    pid_t pid = fork();
    switch (pid) {
        case -1 : /* error */
            zlog(ZLOG_SYSERROR, "failed to daemonize");
            return -1;
        case 0 : /* children */
            close(fpm_globals.send_config_pipe[0]);
            /* close the read side of the pipe */
            break;
        default : /* parent */
            close(fpm_globals.send_config_pipe[1]);
            /* close the write side of the pipe */
            ……
            exit(FPM_EXIT_SOFTWARE);
    

    我们知道,对于父进程(calling process), fork返回的pid是master进程的pid,会走到default逻辑中,最终会退出进程;而master进程会在fpm_run函数中fork子进程(worker进程), gdb信息如下:

    Breakpoint 2, 0x0000003dcbcacd14 in fork () from /lib64/libc.so.6
    (gdb) bt
    #0  0x0000003dcbcacd14 in fork () from /lib64/libc.so.6
    #1  0x0000000000a8cfc1 in fpm_children_make (wp=0x1315430, in_event_loop=0, nb_to_
        spawn=0, is_debug=1) at /root/php7/book/php-7.1.0/sapi/fpm/fpm/fpm_children.c:400
    #2  0x0000000000a8d259 in fpm_children_create_initial (wp=0x1315430) at /root/php7/
        book/php-7.1.0/sapi/fpm/fpm/fpm_children.c:453
    #3  0x0000000000a8c1a2 in fpm_run (max_requests=0x7fffffffdf3c) at /root/php7/book/
        php-7.1.0/sapi/fpm/fpm/fpm.c:101
    #4  0x0000000000a998c8 in main (argc=3, argv=0x7fffffffe178) at /root/php7/book/
        php-7.1.0/sapi/fpm/fpm/fpm_main.c:1
    

    在函数fpm_children_make中,我们可以看到static、dynamic、ondemand这3种模式的不同之处,代码如下:

    if (wp->config->pm == PM_STYLE_DYNAMIC) {
        /* dynamic模式下,先启动pm_start_servers数量的worker进程,根据请求动态变化 */
            if (! in_event_loop) { /* starting */
                max = wp->config->pm_start_servers;
            } else {
                max = wp->running_children + nb_to_spawn;
            }
        } else if (wp->config->pm == PM_STYLE_ONDEMAND) {
        /* ondemand模式下,启动时并不创建worker进程,按需启动 */
            if (! in_event_loop) { /* starting */
            max = 0; /* do not create any child at startup */
        } else {
            max = wp->running_children + nb_to_spawn;
        }
    } else { /* PM_STYLE_STATIC */
    /* static模式下,启动固定数量的worker进程 */
        max = wp->config->pm_max_children;
    }
    

    从代码中可以看出,在static模式下,会走到最后一个else,进程数为pm_max_children;在dynamic模式下,启动时,进程数为pm_start_servers,而在ondemand模式下,启动时,进程数为0。

    接下来,master会根据需要启动的子进程数进行fork,代码如下:

    /*
        * fork children while:
        *   - fpm_pctl_can_spawn_children : FPM is running in a NORMAL state (aka not
            restart, stop or reload)
        *    - wp->running_children < max  : there is less than the max process for
            the current pool
          *    - (fpm_global_config.process_max < 1 || fpm_globals.running_children <
            fpm_global_config.process_max):
        *     if fpm_global_config.process_max is set, FPM has not fork this number of
            processes (globaly)
        */
        while (fpm_pctl_can_spawn_children() && wp->running_children < max && (fpm_
            global_config.process_max  <  1  ||  fpm_globals.running_children  <  fpm_
            global_config.process_max)) {
            warned = 0;
            child = fpm_resources_prepare(wp);
            if (! child) {
                return 2;
            }
        pid = fork();
    

    到这里我们明白了,php-fpm启动时,首先启动一个calling process,然后由calling process创建master进程,master进程根据需要创建的子进程数创建work进程,其中master进程的title为php-fpm: master process,而worker进程的名称为php-fpm: pool name,其中name在php-fpm.conf里面设置:

    ; pool name ('www' here)
    [www]
    

    子进程修改名称的代码在函数fpm_env_init_child中:

    char *title;
    spprintf(&title, 0, "pool %s", wp->config->name);
    fpm_env_setproctitle(title);
    efree(title);
    

    整个php-fpm进程的创建过程如图7-14所示。

    图7-14 PHP-FPM进程的创建过程

    讨论完进程创建的过程,下面分析一下进程是如何管理的。

  2. 进程管理

    woker创建完成后,对请求的处理工作都会由worker进程来进行,而master进程负责对worker进程的监控和管理,比如php-fpm reload和php-fpm stop分别用来重新加载和停止FPM。这部分工作是通过信号机制进行的,比如我们执行reload命令时,对主进程发送了SIGUSR2信号。下面我们对PHP-FPM中的master进程和worker进程的信号分别进行阐述。

    首先说一下master进程的信号,其初始化工作是在fpm_init中实现的,具体函数为fpm_signals_init_main,对应的代码如下:

    int fpm_signals_init_main() /* {{{ */
    {
        struct sigaction act;
        //创建管道并设置为非阻塞模式
        if (0 > socketpair(AF_UNIX, SOCK_STREAM, 0, sp)) {
            zlog(ZLOG_SYSERROR, "failed to init signals: socketpair()");
            return -1;
        }
        if (0 > fd_set_blocked(sp[0], 0) || 0 > fd_set_blocked(sp[1], 0)) {
            zlog(ZLOG_SYSERROR, "failed to init signals: fd_set_blocked()");
            return -1;
        }
        //代码省略//
    
        memset(&act, 0, sizeof(act));
        act.sa_handler = sig_handler; //设置信号函数
        sigfillset(&act.sa_mask);
        //注册SIGTERM、SIGINT、SIGUSR1、SIGUSR2、SIGCHLD、SIGQUIT信号
        if (0 > sigaction(SIGTERM,  &act, 0) ||
            0 > sigaction(SIGINT,   &act, 0) ||
            0 > sigaction(SIGUSR1,  &act, 0) ||
            0 > sigaction(SIGUSR2,  &act, 0) ||
            0 > sigaction(SIGCHLD,  &act, 0) ||
            0 > sigaction(SIGQUIT,  &act, 0)) {
                zlog(ZLOG_SYSERROR, "failed to init signals: sigaction()");
                return -1;
        }
        return 0;
    }
    

    该函数主要做了两件事情。

    1. 创建了一个双向的管道sp,并将其设置为非阻塞模式。
    2. 设置了SIGTERM、SIGINT、SIGUSR1、SIGUSR2、SIGCHLD、SIGQUIT信号的回调函数sig_handler,该函数的实现如下:
    static void sig_handler(int signo) /* {{{ */
    {
        //对几种信号,使用char来表示
        static const char sig_chars[NSIG + 1] = {
            [SIGTERM] = 'T',
            [SIGINT]  = 'I',
            [SIGUSR1] = '1',
            [SIGUSR2] = '2',
            [SIGQUIT] = 'Q',
            [SIGCHLD] = 'C'
        };
        char s;
        int saved_errno;
        //保证是master进程
        if (fpm_globals.parent_pid ! = getpid()) {
            return;
        }
    
        saved_errno = errno;
        s = sig_chars[signo];
        //写入之前创建的管道的1端口
        zend_quiet_write(sp[1], &s, sizeof(s));
        errno = saved_errno;
    }
    

    从上述代码中,我们可以看到,当master进程收到信号时,会将其转换为对应的char,然后将char写入管道的一端,那么谁读取呢?答案是fpm_event_loop函数,代码如下:

       void fpm_event_loop(int err) /* {{{ */
       {
           static struct fpm_event_s signal_fd_event;
       
           //保证是master进程
           if (fpm_globals.parent_pid ! = getpid()) {
               return;
           }
           //其中fpm_signals_get_fd()获取的是sp[0],注册的回调函数为fpm_got_signal
           fpm_event_set(&signal_fd_event, fpm_signals_get_fd(), FPM_EV_READ, &fpm_got_
               signal, NULL);
    

    从代码中可以看出,该函数从管道的另一端读取数据,并回调函数fpm_got_signal。fpm_got_signal的实现如下:

    static void fpm_got_signal(struct fpm_event_s *ev, short which, void *arg) /* {{{ */
    {
        //代码省略//
        do {
            res = read(fd, &c, 1);
        //代码省略//
        switch (c) {
            case 'C' :                   /* SIGCHLD */
            //这个信号由worker进程发出,对相应的worker进程做一些善后工作
            fpm_children_bury();
            break;
            case 'I' :                   /* SIGINT  */
            //收到SIGINT信号,master进程和worker进程退出
            fpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET);
            break;
            case 'T' :                   /* SIGTERM */
            // 收到SIGTERM信号,master进程和worker进程退出
            fpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET);
            break;
            case 'Q' :                   /* SIGQUIT */
            // 收到SIGQUIT信号,master进程和worker进程退出
            fpm_pctl(FPM_PCTL_STATE_FINISHING, FPM_PCTL_ACTION_SET);
            break;
            case '1' :                   /* SIGUSR1 */
            //代码省略//
            //收到SIGUSR1信号,重新打开日志文件,并重启worker进程
            ret = fpm_log_open(1);
            //代码省略//
            break;
            case '2' :                   /* SIGUSR2 */
            //重启worker进程
            fpm_pctl(FPM_PCTL_STATE_RELOADING, FPM_PCTL_ACTION_SET);
            break;
            }
            //代码省略//
        } while (1);
        return;
    }
    

    从代码中可以看出以下几点。

    1. 对于SIGCHLD信号,该信号是由worker退出时发送的,master进程收到这个信号后调用fpm_children_bury函数对worker进程进行善后工作;同时调用fpm_children_make函数按照不同模式启动worker进程。

    2. 对于SIGUSR1信号,调用的是fpm_log_open函数,重新打开日志文件,然后fpm_pctl_kill_all杀掉worker进程;这时候又会收到SIGCHLD信号,进行步骤1。


      注意

      在大流量请求的情况下,切分日志时,会向php-fpm发送SIGUSR1信号,此时会有批量的worker进程被杀死,在重启完毕前,worker进程数会瞬间变少,这时候会出现请求响应变慢的情况。


    3. 对于SIGINT、SIGTERM、SIGQUIT和SIGUSR2信号,调用的都是fpm_pctl函数,该函数有两个参数,第一个参数表示状态值,第二个参数表示操作类型,对应代码如下:

      enum {
          FPM_PCTL_STATE_UNSPECIFIED,
          FPM_PCTL_STATE_NORMAL,
          FPM_PCTL_STATE_RELOADING,
          FPM_PCTL_STATE_TERMINATING,
          FPM_PCTL_STATE_FINISHING
      };
      
      enum {
          FPM_PCTL_ACTION_SET,
          FPM_PCTL_ACTION_TIMEOUT,
          FPM_PCTL_ACTION_LAST_CHILD_EXITED
      };
      

      收到SIGUSR2信号,执行fpm_pctl_exec函数,该函数内部调用C语言execvp函数启动FPM。收到SIGQUIT、SIGINT、SIGTREM信号,执行fpm_pctl_exit函数实现主进程的退出。

      到此我们了解了master进程对信号的处理工作,接下来我们讨论一下worker进程的信号处理,其实现是在函数中调用fpm_signals_init_child,具体代码如下:

      int fpm_signals_init_child() /* {{{ */
      {
      //代码省略//
      act.sa_handler = &sig_soft_quit; //信号回调函数
      act.sa_flags |= SA_RESTART;
      
      act_dfl.sa_handler = SIG_DFL; //信号回调函数为SIG_DFL,默认处理
      //关闭继承的管道的两端
      close(sp[0]);
      close(sp[1]);
      
      if (0 > sigaction(SIGTERM,  &act_dfl,  0) ||
          0 > sigaction(SIGINT,   &act_dfl,  0) ||
          0 > sigaction(SIGUSR1,  &act_dfl,  0) ||
          0 > sigaction(SIGUSR2,  &act_dfl,  0) ||
          0 > sigaction(SIGCHLD,  &act_dfl,  0) ||
          0 > sigaction(SIGQUIT,  &act,      0)) {
                  return -1;
          }
          //…省略代码…//
      }
      

      可以看出,SIGTERM、SIGINT、SIGUSR1、SIGUSR2、SIGCHLD的信号回调函数为SIG_DFL,即默认处理;而SIGQUIT的信号回调函数为sig_soft_quit,其实现如下:

      static void sig_soft_quit(int signo) /* {{{ */
      {
          int saved_errno = errno;
      
          close(0);
          if (0 > socket(AF_UNIX, SOCK_STREAM, 0)) {
              zlog(ZLOG_WARNING, "failed to create a new socket");
          }
          fpm_php_soft_quit();
          errno = saved_errno;
      }
      void fpm_php_soft_quit() /* {{{ */
      {
          fcgi_terminate();
      }
      void fcgi_terminate(void)
      {
          in_shutdown = 1;
      }
      

    从代码中可以看出,该函数会将in_shutdown值设为1,而in_shutdown控制子进程接收客户端请求操作,当in_shutdown等于1的时候,表明不再接收请求,则子进程会退出,关闭CGI,释放资源等操作,做到了“软”关闭。

  3. 计分板

    为了熟练地掌握各woker进程的工作情况,FPM提供了一个计分板的功能,其核心结构体为fpm_scoreboard_s和fpm_scoreboard_proc_s,具体定义如下:

    struct fpm_scoreboard_s {
        union { //保证原子性的锁机制
                atomic_t lock;
                char dummy[16];
        };
        char pool[32]; //worker名称
        int pm; //运行模式
        time_t start_epoch; //开始的时间
        int idle; //process的空闲数
        int active; //process的活跃数(工作中的)
        int active_max; //最大活跃数
        unsigned long int requests; //请求次数
        unsigned int max_children_reached; // 达到最大进程数限制的次数
        int lq; // 当前listen queue的请求数
        int lq_max; //listen queue的大小
        unsigned int lq_len; //listen queue的长度
        unsigned int nprocs; //process的总数
        int free_proc; //从process的列表遍历下一个空闲对象的开始下标
        unsigned long int slow_rq; //慢请求数
        struct fpm_scoreboard_proc_sprocs[]; //计分板详情
    };
    
    struct fpm_scoreboard_proc_s {
        union { //保证原子性的锁机制
            atomic_t lock;
            char dummy[16];
        };
        int used; //是否被使用
        time_t start_epoch;
        pid_t pid; //进程id
        unsigned long requests;
        enum fpm_request_stage_e request_stage; //处理请求阶段,会在7.4.3节阐述
        struct timeval accepted;      //accept请求的时间
        struct timeval duration;      //脚本执行的时间
        time_t accepted_epoch;        //accept请求时间戳(秒)
        struct timeval tv;            //活跃时间
        char request_uri[128];        //请求URI
        char query_string[512];       //请求参数
        char request_method[16];      //请求方法
        size_t content_length;        //请求长度
        char script_filename[256];
        char auth_user[32];
    #ifdef HAVE_TIMES
        struct tms cpu_accepted;
        struct timeval cpu_duration;
        struct tms last_request_cpu;
        struct timeval last_request_cpu_duration;
    #endif
        size_t memory; //内存使用大小
    };
    

    从代码可以看出,fpm_scoreboard_s结构记录FPM所有worker进程的汇总统计信息,而fpm_scoreboard_proc_s对应的是各worker进程的详细信息。FPM提供了3个函数来统计计分。

    1. fpm_scoreboard_update函数:修改计分板里的各指标,为了保证原子性,使用了锁机制fpm_spinlock,分别对两种action进行处理:

      #define FPM_SCOREBOARD_ACTION_SET 0 //重置操作
      #define FPM_SCOREBOARD_ACTION_INC 1 //增加操作
      

      在FastCGI处理的每个阶段,调用该函数更新worker的计分板的数值。

    2. fpm_scoreboard_proc_acquire函数:获取统计单元,调用的函数是fpm_scoreboard_proc_get,这里也用到了锁机制,但是跟update对应的锁不一样。

    3. fpm_scoreboard_proc_release函数:与acquire对应,释放统计单元。

    这3个函数如何使用呢?举个例子,在FastCGI读取Header阶段,调用函数fpm_request_reading_headers:

    void fpm_request_reading_headers() /* {{{ */
    {
        struct fpm_scoreboard_proc_s *proc;
        //代码省略//
        //获取统计单元
        proc = fpm_scoreboard_proc_acquire(NULL, -1, 0);
        //代码省略//
        //修改统计单元信息
        proc->request_stage = FPM_REQUEST_READING_HEADERS;
        proc->tv = now;
        proc->accepted = now;
        proc->accepted_epoch = now_epoch;
    #ifdef HAVE_TIMES
        proc->cpu_accepted = cpu;
    #endif
        proc->requests++;
        proc->request_uri[0] = '\0';
        proc->request_method[0] = '\0';
        proc->script_filename[0] = '\0';
        proc->query_string[0] = '\0';
        proc->auth_user[0] = '\0';
        proc->content_length = 0;
        //释放统计单元
        fpm_scoreboard_proc_release(proc);
        /* idle--, active++, request++ */
        //更新计分板
        fpm_scoreboard_update(-1, 1, 0, 0, 1, 0, 0, FPM_SCOREBOARD_ACTION_INC, NULL);
    }
    

    从代码中可以看出,不同的阶段会分别调用这3个函数来更新计分,这样可以准确地获知worker的运行状态,如空闲数、请求执行时间等,提供了监控系统健康状态的手段,图7-12就是基于计分板绘制的PHP-FPM空闲数的图。

    了解了进程创建的过程、进程的管理,以及对worker进程的计分策略,下面我们具体分析一下worker进程是如何工作的。

7.3.2 网络编程

  1. Socket创建

    calling process进程调用fpm_init中的fpm_unix_init_main函数fork出master进程,master进程调用fpm_sockets_init_main函数进行网络的监听,其具体实现如下:

    /* create all required sockets */
    for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
        switch (wp->listen_address_domain) {
        case FPM_AF_INET :
            wp->listening_socket = fpm_socket_af_inet_listening_socket(wp);
                break;
    
        case FPM_AF_UNIX :
            if (0 > fpm_unix_resolve_socket_premissions(wp)) {
                return -1;
            }
            wp->listening_socket = fpm_socket_af_unix_listening_socket(wp);
                break;
        }
        //代码省略//
    }
    

    从代码中可以看出,在Linux中,Nginx服务器和PHP-FPM可以通过TCPSocket和UNIX Socket两种方式实现。其中,UNIX Socket是一种终端,可以使同一台操作系统上的两个或多个进程进行数据通信。这种方式需要在Nginx配置文件中填写PHP-FPM的pid文件位置,效率要比TCP Socket高。TCP Socket的优点是可以跨服务器,当Nginx和PHP-FPM不在同一台机器上时,只能使用这种方式。配置方式如下:

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; ;
        fastcgi_pass 127.0.0.1:9000; //TCP socket
        #fastcgi_pass unix:/var/run/php7-fpm.sock; //UNIX socket
        fastcgi_index index.php;
    }
    

    master进程会创建Socket,而worker进程会通过创建的fd来accept请求。

  2. accept请求

    根据上文的描述,FPM的生命周期会进入循环中,代码如下:

    zend_first_try {
            //循环开始
            while (EXPECTED(fcgi_accept_request(request) >= 0)) {//accept阻塞等待请求
            //代码省略//
            //初始化request
            init_request_info();
    
            fpm_request_info();
    
            /** 代码省略,主要处理40x响应 **/
            //解析FastCGI协议
            fpm_request_executing();
            //执行PHP脚本
            php_execute_script(&file_handle);
    
            fpm_request_end();
            fpm_log_write(NULL);
    
            php_request_shutdown((void *) 0);
    
            //超过最大执行次数,退出
            requests++;
            if (UNEXPECTED(max_requests && (requests == max_requests))) {
                cgi_finish_request(request, 1);
                break;
            }
        /* 循环结束 */
    }
    

    从代码中可以非常清晰地看出,worker进程会进入循环,当没有请求时,会阻塞在fcgi_accept_request,让出CPU资源,成为空闲进程,当请求到达时会有一个worker进程抢到并处理,进入FasCGI的处理阶段,下面通过对FastCGI协议的阐述来理解FPM的工作。

7.3.3 FastCGI协议

FastCGI是一种协议,它是建立在CGI/1.1基础之上的,把CGI/1.1里面要传递的数据通过FastCGI协议定义的顺序和格式进行传递。为了更好地理解FPM的工作,下面具体阐述一下FastCGI协议的内容。

  1. 消息类型

    FastCGI协议分为10种类型,具体定义如下:

    typedef enum _fcgi_request_type {
        FCGI_BEGIN_REQUEST  =  1, /* [in] */
        FCGI_ABORT_REQUEST  =  2, /* [in]  (not supported) */
        FCGI_END_REQUEST         =  3, /* [out] */
        FCGI_PARAMS       =  4, /* [in]  environment variables  */
        FCGI_STDIN        =  5, /* [in]  post data   */
        FCGI_STDOUT       =  6, /* [out] response   */
        FCGI_STDERR       =  7, /* [out] errors     */
        FCGI_DATA         =  8, /* [in]  filter data (not supported) */
        FCGI_GET_VALUES          =  9, /* [in]  */
        FCGI_GET_VALUES_RESULT = 10  /* [out] */
    } fcgi_request_type;
    

    整个FastCGI是二进制连续传递的,定义了一个统一结构的消息头,用来读取每个消息的消息体,方便消息包的切割。一般情况下,最先发送的是FCGI_BEGIN_REQUEST类型的消息,然后是FCGI_PARAMS和FCGI_STDIN类型的消息,当FastCGI响应处理完后,将发送FCGI_STDOUT和FCGI_STDERR类型的消息,最后以FCGI_END_REQUEST表示请求的结束。FCGI_BEGIN_REQUEST和FCGI_END_REQUEST分别表示请求的开始和结束,与整个协议相关。

  2. 消息头

    以上10种类型的消息都是以一个消息头开始的,其结构体定义如下:

    typedef struct _fcgi_header {
        unsigned char version;
        unsigned char type;
        unsigned char requestIdB1;
        unsigned char requestIdB0;
        unsigned char contentLengthB1;
        unsigned char contentLengthB0;
        unsigned char paddingLength;
        unsigned char reserved;
    } fcgi_header;
    

    其中:

    1. version标识FastCGI协议版本。
    2. type标识FastCGI记录类型。
    3. requestId标识消息所属的FastCGI请求,计算方式如下:
    (requestIdB1 << 8) + requestIdB0
    

    所以requestId的范围为0~216-1,也就是0~65535。

    1. contentLength是标识消息的contentData组件的字节数,计算方式跟requestId类似,范围同样是0~65535。
    (contentLengthB1 << 8) | contentLengthB0
    
    1. paddingLength是标识消息的paddingData组件的字节数,范围是0~255;协议通过paddingData提供给发送者填充发送的记录的功能,并且方便接受者通过paddingLength快速地跳过paddingData。填充的目的是允许发送者更有效地处理保持对齐的数据。如果内容的长度超过65535字节怎么办?答案是可以分成多个消息发送。
  3. FCGI_BEGIN_REQUEST

    FCGI_BEGIN_REQUEST的结构体定义如下:

    typedef struct _fcgi_begin_request {
        unsigned char roleB1;
        unsigned char roleB0;
        unsigned char flags;
        unsigned char reserved[5];
    } fcgi_begin_request;
    

    其中,role代表的是Web服务器期望应用扮演的角色,计算方式如下:

    (roleB1 << 8) + roleB0
    

    PHP 7处理了3种角色,分别是FCGI_RESPONDER、FCGI_AUTHORIZER和FCGI_FILTER。

    flags和FCGI_KEEP_CONN如果为0,则在对本次请求响应后关闭连接;如果非0,则在对本次请求响应后不会关闭连接。

  4. 名-值对

    对于type为FCGI_PARAMS类型,FastCGI协议提供了名-值对来很好地满足读写可变长度的name和value,格式如下:

    nameLength+valueLength+name+value
    

    为了节省空间,对于0~127长度的值,Length使用了一个char来表示,第一位为0,对于大于127的长度的值,Length使用了4个char来表示,第一位为1。具体如图7-15所示。

    图7-15 名和值长度示意图

    长度计算代码如下:

    if (UNEXPECTED(name_len >= 128)) {
        if (UNEXPECTED(p + 3 >= end)) return 0;
        name_len = ((name_len & 0x7f) << 24);
        name_len |= (*p++ << 16);
        name_len |= (*p++ << 8);
        name_len |= *p++;
    }
    

    这样可以表达0~231的长度。

  5. 请求协议

    FastCGI协议的定义结构体如下:

    typedef struct _fcgi_begin_request_rec {
        fcgi_header hdr;
        fcgi_begin_request body;
    } fcgi_begin_request_rec;
    

    分析完FastCGI的协议,我们整体掌握了请求的FastCGI消息的内容,我们通过访问对应的接口,采用gdb抓取其中的内容。

    首先我们修改php-fpm.conf的参数,保证只启动一个worker:

    pm.max_children = 1
    

    重新启动PHP-FPM:

    ./sbin/php-fpm -y etc/php-fpm.conf
    

    对worker进行gdb:

    ps aux | grep php-fpm
    root     30014  0.0  0.0142308  4724 ?         Ss   Nov26   0:03 php-fpm: master
        process (etc/php-fpm.conf)
    chenlei   30015  0.0  0.0142508  5500 ?        S    Nov26   0:00 php-fpm: pool www
    gdb -p 30015
    (gdb) b fcgi_read_request
    

    通过浏览器访问Nginx, Nginx转发到PHP-FPM的worker上,根据gdb可以输出FastCGI消息的内容:

    (gdb) b fcgi_read_request
    

    对于第一个消息,内容如图7-16所示。

    图7-16 FCGI_BEGIN_REQUEST包头示意图

    其中,type对应的是FCGI_BEGIN_REQUEST, requestid为1,长度为8,恰好是fcgi_begin_request结构体的大小,内容如图7-17所示。

    图7-17 FCGI_BEGIN_REQUEST示意图

    role对应的是FCGI_RESPONDER。继续往下读,得到的消息内容如图7-18所示。

    图7-18 FCGI_BEGIN_REQUEST包头示意图

    其中,type对应的是FCGI_PARAMS, requestid为1,长度为

    (contentLengthB1 << 8) | contentLengthB0  == 987
    

    paddingLength=5,而987+5=992,恰好是8的倍数。根据contentLength+paddingLength向后读取992长度的字节流,输出如下:

    (gdb) p *p@987
    $1 =  "\017TSCRIPT_FILENAME/home/xiaoju/webroot/beatles/application/mis/mis/src/index.
        php/admin/operation/index\f\016QUERY_STRINGactivity_id=89\016\003REQUEST_
        METHODGET\f\000CONTENT_TYPE\016\000CONTENT_LENGTH\v  SCRIPT_NAME/index.php/
        admin/operation/index\v%REQUEST_URI/admin/operation/index? activity_id=89\f
        DOCUMENT_URI/index.php/admin/operation/index\r4DOCUMENT_ROOT/home/xiaoju/
        webroot/beatles/application/mis/mis/src\017\bSERVER_PROTOCOLHTTP/1.1\021\
        aGATEWAY_INTERFACECGI/1.1\017\vSERVER_SOFTWAREnginx/1.2.5\v\rREMOTE_
        A D D R172.22.32.131\v\005R E M O T E_P O R T50973\v\f S E R V E R_A D D R10.94.98.116\
        v\004SERVER_PORT8085\v\000SERVER_NAME\017\003REDIRECT_STATUS200\t\021HTTP_
        HOST10.94.98.116:8085\017\nHTTP_CONNECTIONkeep-alive\017xHTTP_USER_
        AGENTMozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML,
          like Gecko) Chrome/62.0.3202.94 Safari/537.36\036\001HTTP_UPGRADE_INSECURE_
        REQUESTS1\vUHTTP_ACCEPTtext/html, application/xhtml+xml, application/
        xml; q=0.9, image/webp, image/apng, */*; q=0.8\024\rHTTP_ACCEPT_ENCODINGgzip,
        deflate\024\027HTTP_ACCEPT_LANGUAGEzh-CN, zh; q=0.9, en; q=0.8"
    

    根据名-值对的长度规则,我们可以看出,FastCGI协议封装了类似于HTTP协议的键-值对。读取完毕后,继续跟踪消息,输出可以得出如图7-19所示的消息。

    图7-19 FCGI_BEGIN_REQUEST包头示意图

    其中,type对应的是FCGI_PARAMS, requestid为1,长度为0,此时完成了FastCGI协议消息的读取过程。下面介绍处理完请求后返回给Nginx的FastCGI协议的消息。

  6. 响应协议

    fcgi_finish_request调用fcgi_flush, fcgi_flush中封装一个FCGI_END_REQUEST消息体,再通过safe_write写入Socket连接的客户端描述符。

    int fcgi_flush(fcgi_request *req, int close)
    {
        int len;
    
        close_packet(req);
        len = (int)(req->out_pos - req->out_buf);
    
        if (close) {
            fcgi_end_request_rec *rec = (fcgi_end_request_rec*)(req->out_pos);
            //创建FCGI_END_REQUEST的头
            fcgi_make_header(&rec->hdr,  FCGI_END_REQUEST,  req->id,  sizeof(fcgi_end_
                request));
            //写入appStatus
            rec->body.appStatusB3 = 0;
            rec->body.appStatusB2 = 0;
            rec->body.appStatusB1 = 0;
            rec->body.appStatusB0 = 0;
            //修改protocolStatus为FCGI_REQUEST_COMPLETE;
            rec->body.protocolStatus = FCGI_REQUEST_COMPLETE;
            len += sizeof(fcgi_end_request_rec);
        }
    
        if (safe_write(req, req->out_buf, len) ! = len) {
            req->keep = 0;
            req->out_pos = req->out_buf;
            return 0;
        }
    
        req->out_pos = req->out_buf;
        return 1;
    }
    

到此我们就完全掌握了FastCGI协议。整个FPM模式实际上是多进程模式,首先由calling process进程fork出master进程,master进程会创建Socket,然后fork出worker进程,worker进程会在accept处阻塞等待,请求过来时,由其中一个worker进程处理,按照FastCGI模式进行各阶段的读取,然后解析PHP并执行,最后按照FastCGI协议返回数据,继续进入accept处阻塞等待。另外,FPM建立了计分板机制,可以关注全局和每个woker的工作情况,方便使用者监控。

除了CLI模式和FPM模式,还有很多其他基于SAPI的模式,下面我们简单介绍下。

7.4 其他模式

7.4.1 CGI模式

CGI即通用网关接口(Common Gateway Interface),通俗地讲,CGI就是将Web服务器和PHP执行程序连接起来,把接收的指令传递给PHP执行程序,再把服务器执行程序的结果返还给Web服务器,如图7-20所示。

对于每一个用户请求,都会先创建CGI的子进程,然后处理请求,处理完后结束这个子进程,这就是fork-and-execute模式。用户请求数量非常多会大量挤占系统的资源(如内存、CPU时间等),造成效率低下。所以,对于采用CGI模式的服务器,有多少连接请求,就会有多少CGI子进程,子进程反复加载也是导致CGI性能低下的主要原因,这也是FastCGI出现的原因。

7.4.2 Embed模式

PHP提供了一个Embed SAPI,也就是说,PHP允许在C/C++语言中调用PHP/ZE提供的函数,编译时增加--enable-embed生成。该模式对外提供了两个API,即php_embed_init和php_embed_shutdown。php_embed_ini用于完成模块初始化和请求初始化,php_embed_shutdown用于完成请求关闭和模块关闭工作。实现非常简单,可以在C程序里面调用PHP,具体可以参考博客《使用PHP EmbedSAPI实现Opcodes查看器》一文,这里不再展开。

7.4.3 PHPDBG模式

PHPDBG是一个PHP的SAPI模块,可以在不修改代码和不影响性能的情况下控制PHP的运行环境。PHPDBG的目标是成为一个轻量级、强大、易用的PHP调试平台,从PHP 5.6开始集成。

PHPDBG提供了类似GDB的功能,支持单步调试,可以灵活地打断点,可以查看类方法、函数、文件的行、内存地址、opcode等,可直接调用PHP的eval,另外还支持远程debug。

#phpdbg
prompt> exec ./test.php
prompt> b func#2
prompt> r
prompt> info break

感兴趣的读者可以进一步研究一下PHPDBG模式的实现,这里不再展开。

7.5 本章小结

本章介绍了基于SAPI的PHP 7的生命周期,主要有模块初始化、请求初始化、执行、请求关闭和模块关闭5个阶段。各种模式基于SAPI规范开发,本章详细介绍了CLI模式和FPM模式的实现,同时对FastCGI协议做了详细的说明。相信读者读完本章,会对PHP 7的生命周期有比较深刻的理解,同时也会对日常工作中经常使用的FPM有更全面的了解。