本章将对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信号对照表
在以上列出的信号中:
- 程序不可捕获、阻塞或忽略的信号有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所示。
表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);
}
}
代码说明以及程序执行结果如下。
- 为SIGINT、SIGRTMIN、SIGQUIT安装了信号处理器signal_handler。信号处理器的逻辑主要是输出,如果是SIGQUIT信号,输出并退出。
- 屏蔽了信号SIGINT、SIGRTMIN,这时如果这两个信号进来,那么信号是一直阻塞的状态,也就是信号一直在排队,无法被信号处理器处理。由于SIGQUIT信号没有被阻塞,所以随时可通过该信号终止进程。
- 进程会一直在sigsuspend处阻塞;如果产生两个SIGINT信号(Ctrl+C),这时信号处理器会被调用,并提示catch signal SIGINT:2,并且之后的信号等待队列清空;如果10s内产生两个SIGRTMIN信号(kill-34 pid),这时信号处理器会被调用,并提示catch signalSIGRTMIN:34,但信号等待队列不清空。
- 一旦sigsuspend等到了信号到来,在调用完信号处理器函数(signal_handler)后,sigsuspend系统调用返回,并恢复屏蔽信号SIGINT、SIGRTMIN。
由此可以得出结论:
- 可靠信号(≥34)不会丢失,N个可靠信号经过排队,在信号处理的时候仍然是N个。非可靠信号(<34)会丢失,N个非可靠信号经过排队,在信号处理的时候是1个。
- sigprocmask系统调用是设置进程的信号掩码的。信号掩码的意义是,掩码中的信号会进入队列排队处理。
- 对于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,我们具体分析一下各函数的作用:
-
调用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中的成员变量进行初始化。
-
调用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。
-
调用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字节。后面的分析会经常用到这个全局变量。
-
调用gc_globals_ctor函数,对gc_globals进行初始化,这部分在第3章已做了详细阐述,这里不再展开叙述。
-
调用zend_startup函数。
图7-6 cwd_globals的结构示意图- 调用start_memory_manager初始化内存管理,这部分在第9章会详细讨论。
- 调用virtual_cwd_startup初始化cwd_globals,其中cwd_globals的结构如图7-6所示。
- 调用zend_startup_extensions_mechanism启动扩展机制。
- 设置一些使用函数或者值,具体如下:
/* 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;- 设置词法和语法解析的入口函数compile_file以及执行的入口函数execute_ex:
zend_compile_file = compile_file; zend_execute_ex = execute_ex;
注意
PHP 7的“编译”入口是函数compile_file,这是词法和语法解析的入口;而对opcodes进行执行的入口是execute_ex函数。
- 调用zend_init_opcodes_handlers方法,初始化Zend虚拟机的4597个handler。这部分内容具体会在第11章展开叙述。
- 对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);-
调用ini_scanner_globals_ctor对ini_scanner_globals进行初始化,这部分会在第8章详细展开叙述。
-
调用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章阐述。
-
调用zend_register_list_destructors_ex函数,注册析构函数list。
-
调用php_binary_init函数,获取PHP执行的二进制程序的路径。
-
调用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); //代码省略// -
调用php_rfc1867_register_constant注册文件上传相关的预定义常量。
-
调用php_init_config函数,会先读取php.ini文件,然后调用zend_parse_ini_file进行解析,并注册。
-
调用zend_register_standard_ini_entries函数,注册ini相关的变量。
-
调用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); } -
初始化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; } -
函数php_register_internal_extensions和php_register_extensions_bc分别为注册内部扩展和附加PHP扩展。
-
zend_startup_extensions和zend_startup_modules启动扩展与模块。
-
对在php.ini中设置的禁用函数和禁用类进行设置,函数分别是php_disable_functions和php_disable_classes。
模块初始化阶段做的事情比较多,对于FPM模式,进程启动后只会进行一次模块初始化,进而进入循环,进行请求的初始化。同样对于CLI模式,模块初始化完成后,也是进入请求初始化阶段。
7.2.2 请求初始化阶段
请求初始化阶段的函数入口为php_requet_startup,其执行过程如图7-7所示。
图7-7 请求初始化阶段的执行过程
对于图7-7,我们具体分析一下各函数的作用。
-
调用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; -
调用zend_activate函数:
① gc_reset函数初始化垃圾回收相关变量和函数。 ② init_compile函数初始化编译器以及CG。 ③ init_executor函数初始化执行器以及EG。 ④ startup_scanner函数初始化扫描器以及SCNG。
-
调用sapi_activate函数,对SG进行初始化。
-
调用zend_signal_activate函数,对一些信号进行处理。
-
调用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源码对此有清晰的注释,主要工作如下。
- 调用各模块中注册的关闭函数和析构函数。
- 将输出缓冲器中的内容输出。
- 调用所有扩展注册的钩子RSHUTDOWN函数。
- 销毁request相关的全局变量,关闭编译器和执行器。
- 还原ini配置。
完成这些工作后,FPM模式会循环等待请求到来,继续进行请求的初始化,而CLI模式将进入最后一个阶段,即模块关闭阶段。
7.2.5 模块关闭阶段
模块关闭阶段的入口函数为php_module_shutdown,这个阶段与模块初始化阶段基本是相反的,用于对各种初始化的变量进行销毁。具体执行过程如图7-10所示。
图7-10 模块关闭阶段
主要工作如下:
- 调用加载模块对应的flush函数,清理持久化符号表,销毁所有模块;
- 关闭与php.ini配置文件解析相关的变量和函数;
- 关闭内存管理和垃圾回收机制;
- 关闭output输出相关的信息;
- 销毁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所示。
- 调用php_module_startup,加载所有模块。
- 进入循环,调用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系统调用已经不存在“惊群”了。
- 进入php_execute_script,对脚本执行编译。
- 调用php_request_shutdown关闭请求,继续进入循环。
- 如果进程退出,调用php_module_shutdown关闭所有模块。
- 如果请求次数大于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进程的。
-
进程创建
我们以Nginx+PHP-FPM方式为例,讲一下整个Web请求的过程。一般情况下,Nginx会根据服务器的CPU内核数设置worker的进程数,而PHP-FPM的进程有三种设置方式:static、dynamic和ondemand,可以在php-fpm.conf里面设置:
pm = static //其他:dynamic或者ondemand-
static模式 static模式始终会保持一个固定数量的子进程,这个数量由pm.max_children定义,比如线上,我们可以将其设置为512个worker,我们可以观察PHP-FPM的进程空闲数,如图7-12所示。
图7-12 PHP-FPM空闲数从图7-12可以看出,随着请求量的变化,PHP-FPM的空闲数也发生了变化。
-
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,则会被杀掉。
-
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进程的创建过程讨论完进程创建的过程,下面分析一下进程是如何管理的。
-
-
进程管理
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; }该函数主要做了两件事情。
- 创建了一个双向的管道sp,并将其设置为非阻塞模式。
- 设置了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; }从代码中可以看出以下几点。
-
对于SIGCHLD信号,该信号是由worker退出时发送的,master进程收到这个信号后调用fpm_children_bury函数对worker进程进行善后工作;同时调用fpm_children_make函数按照不同模式启动worker进程。
-
对于SIGUSR1信号,调用的是fpm_log_open函数,重新打开日志文件,然后fpm_pctl_kill_all杀掉worker进程;这时候又会收到SIGCHLD信号,进行步骤1。
注意
在大流量请求的情况下,切分日志时,会向php-fpm发送SIGUSR1信号,此时会有批量的worker进程被杀死,在重启完毕前,worker进程数会瞬间变少,这时候会出现请求响应变慢的情况。
-
对于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,释放资源等操作,做到了“软”关闭。
-
计分板
为了熟练地掌握各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_s *procs[]; //计分板详情 }; 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个函数来统计计分。
-
fpm_scoreboard_update函数:修改计分板里的各指标,为了保证原子性,使用了锁机制fpm_spinlock,分别对两种action进行处理:
#define FPM_SCOREBOARD_ACTION_SET 0 //重置操作 #define FPM_SCOREBOARD_ACTION_INC 1 //增加操作在FastCGI处理的每个阶段,调用该函数更新worker的计分板的数值。
-
fpm_scoreboard_proc_acquire函数:获取统计单元,调用的函数是fpm_scoreboard_proc_get,这里也用到了锁机制,但是跟update对应的锁不一样。
-
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 网络编程
-
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请求。
-
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协议的内容。
-
消息类型
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分别表示请求的开始和结束,与整个协议相关。
-
消息头
以上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;其中:
- version标识FastCGI协议版本。
- type标识FastCGI记录类型。
- requestId标识消息所属的FastCGI请求,计算方式如下:
(requestIdB1 << 8) + requestIdB0所以requestId的范围为0~216-1,也就是0~65535。
- contentLength是标识消息的contentData组件的字节数,计算方式跟requestId类似,范围同样是0~65535。
(contentLengthB1 << 8) | contentLengthB0- paddingLength是标识消息的paddingData组件的字节数,范围是0~255;协议通过paddingData提供给发送者填充发送的记录的功能,并且方便接受者通过paddingLength快速地跳过paddingData。填充的目的是允许发送者更有效地处理保持对齐的数据。如果内容的长度超过65535字节怎么办?答案是可以分成多个消息发送。
-
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) + roleB0PHP 7处理了3种角色,分别是FCGI_RESPONDER、FCGI_AUTHORIZER和FCGI_FILTER。
flags和FCGI_KEEP_CONN如果为0,则在对本次请求响应后关闭连接;如果非0,则在对本次请求响应后不会关闭连接。
-
名-值对
对于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的长度。
-
请求协议
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 == 987paddingLength=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协议的消息。
-
响应协议
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有更全面的了解。