基础知识

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

信号处理

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

image 2024 06 09 16 22 22 257
Figure 1. 表7-1 UNIX信号对照表

在以上列出的信号中:

  1. 程序不可捕获、阻塞或忽略的信号有 SIGKILLSIGSTOP

  2. 不能恢复至默认动作的信号有 SIGILLSIGTRAP

  3. 默认会导致进程流产的信号有 SIGABRTSIGBUSSIGFPESIGILLSIGQUITSIGSEGVSIGTRAPSIGXCPUSIGXFSZ

  4. 默认会导致进程退出的信号有 SIGALRMSIGHUPSIGINTSIGKILLSIGPIPESIGPROFSIGSYSSIGTERMSIGUSR1SIGUSR2SIGVTALRM

  5. 默认会导致进程停止的信号有 SIGSTOPSIGTSTPSIGTTINSIGTTOU

  6. 默认进程忽略的信号有 SIGCHLDSIGPWRSIGURGSIGWINCH

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

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

image 2024 06 09 16 24 51 103
Figure 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);

// 通过这种方式,程序能够在每 10 秒挂起等待信号到达,并在信号到达时进行相应处理。这样可以在处理长时间运行任务时,响应用户发送的中断信号
int main(void){
    //设置信号掩码,屏蔽信号:SIGINT(2 非可靠信号Ctrl+C )、SIGRTMIN(34 可靠信号)
    sigset_t set;
    sigemptyset(&set); // 清除集合中的所有信号
    sigaddset(&set, SIGINT); // 添加 SIGINT 到 集合
    sigaddset(&set, SIGRTMIN); // 添加 SIGRTMIN 到 集合
    /*
    int how: 指定如何修改信号屏蔽字,有以下几种取值:

        SIG_BLOCK: 将 set 中指定的信号加入到当前信号屏蔽字中,即阻塞这些信号。
        SIG_UNBLOCK: 从当前信号屏蔽字中移除 set 中指定的信号,即解除阻塞这些信号。
        SIG_SETMASK: 将当前信号屏蔽字设置为 set 指定的信号集。

    const sigset_t *set: 指向一个 sigset_t 类型的信号集,该信号集指定要修改的信号。

    sigset_t *oldset: 如果不为 NULL,则存储修改之前的信号屏蔽字。
     */
    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); // 将 SIGINT 信号与 sa 结构体中的信号处理程序关联起来
    sigaction(SIGRTMIN, &sa, NULL); // 将 SIGRTMIN 信号与 sa 结构体中的信号处理程序关联起来
    sigaction(SIGQUIT, &sa, NULL); // 将 SIGQUIT 信号与 sa 结构体中的信号处理程序关联起来

    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 会解除对 SIGINT 和 SIGRTMIN 的屏蔽,并挂起进程,直到信号到达并被处理。
            // 处理完信号后,sigsuspend 返回并继续屏蔽这些信号
            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. SIGINTSIGRTMINSIGQUIT 安装了信号处理器 signal_handler。信号处理器的逻辑主要是输出,如果是 SIGQUIT 信号,输出并退出。

  2. 屏蔽了信号 SIGINTSIGRTMIN,这时如果这两个信号进来,那么信号是一直阻塞的状态,也就是信号一直在排队,无法被信号处理器处理。由于 SIGQUIT 信号没有被阻塞,所以随时可通过该信号终止进程。

  3. 进程会一直在 sigsuspend 处阻塞;如果产生两个 SIGINT 信号(kbd:[Ctrl+C]),这时信号处理器会被调用,并提示 catch signal SIGINT:2,并且之后的信号等待队列清空;如果 10s 内产生两个 SIGRTMIN 信号(kill -34 pid),这时信号处理器会被调用,并提示 catch signal SIGRTMIN:34,但信号等待队列不清空。

  4. 一旦 sigsuspend 等到了信号到来,在调用完信号处理器函数(signal_handler)后,sigsuspend 系统调用返回,并恢复屏蔽信号 SIGINTSIGRTMIN

由此可以得出结论:

  1. 可靠信号(≥34)不会丢失,N 个可靠信号经过排队,在信号处理的时候仍然是 N 个。非可靠信号(<34)会丢失,N 个非可靠信号经过排队,在信号处理的时候是 1 个。

  2. sigprocmask 系统调用是设置进程的信号掩码的。信号掩码的意义是,掩码中的信号会进入队列排队处理。

  3. 对于 2)中进入队列的信号,进程可以通过 sigsuspend(&newMask) 从队列中取出阻塞的信号。

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

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

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

SAPI核心结构SG(v)

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

# define SG(v) (sapi_globals.v)

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

image 2024 06 09 21 03 31 818
Figure 3. 图7-1 sapi_globals的结构示意图

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

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

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