基础知识
在详细讨论 PHP 7 的生命周期和运行模式之前,我们先了解一下基础知识,为深入理解 PHP 7 的原理做一个铺垫。由于 PHP 进程启动时需要对信号进行处理,首先我们了解一下信号的基本概念。如果读者对这部分内容有详细的了解,可以略过 7.1.1 节,直接从 7.1.2 节开始。
信号处理
PHP 7 生命周期中会涉及信号的处理,我们首先对 UNIX 信号的处理做一些了解。UNIX 信号有 1~63 个,其中编号为 1~31 的信号为传统 UNIX 支持的信号,是不可靠信号(非实时信号),编号为 32~63 的信号是后来扩充的,是可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队(多次发送),可能会造成信号丢失,而后者不会,具体如表7-1所示。

在以上列出的信号中:
-
程序不可捕获、阻塞或忽略的信号有
SIGKILL
和SIGSTOP
; -
不能恢复至默认动作的信号有
SIGILL
和SIGTRAP
; -
默认会导致进程流产的信号有
SIGABRT
、SIGBUS
、SIGFPE
、SIGILL
、SIGQUIT
、SIGSEGV
、SIGTRAP
、SIGXCPU
和SIGXFSZ
; -
默认会导致进程退出的信号有
SIGALRM
、SIGHUP
、SIGINT
、SIGKILL
、SIGPIPE
、SIGPROF
、SIGSYS
、SIGTERM
、SIGUSR1
、SIGUSR2
和SIGVTALRM
; -
默认会导致进程停止的信号有
SIGSTOP
、SIGTSTP
、SIGTTIN
和SIGTTOU
; -
默认进程忽略的信号有
SIGCHLD
、SIGPWR
、SIGURG
和SIGWINCH
。
在 PHP 7 进程启动时,会对一些信号进行屏蔽,另外 FPM 的 master
进程会监听一些信号,对 worker
进行处理。
信号处理还需要了解 3 个重要函数,如表7-2所示。

为了理解这 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);
}
}
代码说明以及程序执行结果如下。
-
为
SIGINT
、SIGRTMIN
、SIGQUIT
安装了信号处理器signal_handler
。信号处理器的逻辑主要是输出,如果是SIGQUIT
信号,输出并退出。 -
屏蔽了信号
SIGINT
、SIGRTMIN
,这时如果这两个信号进来,那么信号是一直阻塞的状态,也就是信号一直在排队,无法被信号处理器处理。由于SIGQUIT
信号没有被阻塞,所以随时可通过该信号终止进程。 -
进程会一直在
sigsuspend
处阻塞;如果产生两个SIGINT
信号(kbd:[Ctrl+C]),这时信号处理器会被调用,并提示 catch signal SIGINT:2,并且之后的信号等待队列清空;如果 10s 内产生两个SIGRTMIN
信号(kill -34 pid
),这时信号处理器会被调用,并提示 catch signal SIGRTMIN:34,但信号等待队列不清空。 -
一旦
sigsuspend
等到了信号到来,在调用完信号处理器函数(signal_handler
)后,sigsuspend
系统调用返回,并恢复屏蔽信号SIGINT
、SIGRTMIN
。
由此可以得出结论:
-
可靠信号(≥34)不会丢失,N 个可靠信号经过排队,在信号处理的时候仍然是 N 个。非可靠信号(<34)会丢失,N 个非可靠信号经过排队,在信号处理的时候是 1 个。
-
sigprocmask
系统调用是设置进程的信号掩码的。信号掩码的意义是,掩码中的信号会进入队列排队处理。 -
对于 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 的结构是我们分析 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所示。

整个 sapi_globals
大小为 560 字节,是在全局变量区分配的。该结构体在 PHP 7 的生命周期中大量使用,这里读者先对 sapi_globals
有一个整体的认识。
如图7-1所示,对于 FPM 模式,比较重要的部分是 sapi_request_info
request_info
,对应了 HTTP 协议中的很多字段。
掌握了信号处理、SAPI 的结构体以及 SG(v)
后,我们从 CLI 模式入手来详细了解 PHP 7 的生命周期。