PHP-FPM 启动分析
上一篇简单介绍了 PHP-FPM 的角色,这一篇将详细解析 PHP-FPM 启动的过程。
它的源代码在 php-src/sapi/fpm 目录中,因为 php 是用 C 语言写的,所以第一步就是找到 main() 函数。fpm_main.c 文件的 1571 行开始便是 main 函数的主体:
int main(int argc, char *argv[])
{
int exit_status = FPM_EXIT_OK;
int cgi = 0, c, use_extended_info = 0;
zend_file_handle file_handle;
/* temporary locals */
int orig_optind = php_optind;
char *orig_optarg = php_optarg;
int ini_entries_len = 0;
int max_requests = 500; // 默认最大请求数
int requests = 0;
int fcgi_fd = 0;
fcgi_request *request;
char *fpm_config = NULL;
char *fpm_prefix = NULL;
char *fpm_pid = NULL;
int test_conf = 0;
int force_daemon = -1;
int force_stderr = 0;
int php_information = 0;
int php_allow_to_run_as_root = 0;
...
}
上面这段是 main 函数变量初始化部分的代码,这里主要关注 max_requests, requests, fcgi_fd , *request 这几个变量。
首先是 max_requests,配置过 php-fpm 的开发者都应该知道可以在配置文件中配置 pm.max_requests 的值,在文档中有详细的说明它的作用:
设置每个子进程重生之前服务的请求数。对于可能存在内存泄漏的第三方模块来说是非常有用的。如果设置为 '0' 则一直接受请求,等同于 PHP_FCGI_MAX_REQUESTS 环境变量。默认值:0。
可以看到它的默认值是 500,按照文档中所描述的,如果处理的请求数达到 500 之后,会触发改进程的 “重生”,后面我们会介绍这个过程。
继续往下看:
...
zend_signal_startup();
sapi_startup(&cgi_sapi_module);
cgi_sapi_module.php_ini_path_override = NULL;
cgi_sapi_module.php_ini_ignore_cwd = 1;
...
这一段主要是初始化 sapi 的代码,调用 sapi_startup 函数,传入 &cgi_sapi_module。因为 fpm 实际上是一个 sapi 的 module, 而 sapi 的 module 是被定义好的一个数据结构,cgi_sapi_module 的初始化可以在 fpm_main.c 中找到:
static sapi_module_struct cgi_sapi_module = {
"fpm-fcgi", /* name */
"FPM/FastCGI", /* pretty name */
php_cgi_startup, /* startup */
php_module_shutdown_wrapper, /* shutdown */
sapi_cgi_activate, /* activate */
sapi_cgi_deactivate, /* deactivate */
sapi_cgibin_ub_write, /* unbuffered write */
sapi_cgibin_flush, /* flush */
NULL, /* get uid */
sapi_cgibin_getenv, /* getenv */
php_error, /* error handler */
NULL, /* header handler */
sapi_cgi_send_headers, /* send headers handler */
NULL, /* send header handler */
sapi_cgi_read_post, /* read POST data */
sapi_cgi_read_cookies, /* read Cookies */
sapi_cgi_register_variables, /* register server variables */
sapi_cgi_log_message, /* Log message */
NULL, /* Get request time */
NULL, /* Child terminate */
STANDARD_SAPI_MODULE_PROPERTIES
};
可以看出 cgi_sapi_module 的数据类型是 sapi_module_struct,这个数据类型是 PHP 的 SAPI 中定义的,是类似于 OOP 中 class 的东西。而这个 sapi_startup 函数做的主要事情是分配互斥量(tsrm_mutex_alloc)。互斥量主要为针对多线程准备的,而 fastcgi 模式运行 PHP 都是单线程,所以不存在多线程中出现临界资源的使用问题。
在此之后的很长一部分代码都是处理命令行模式运行时的输入参数,这一段先略过,直接跳到 cgi_sapi_module 的 startup 部分:
/* startup after we get the above ini override se we get things right */
if (cgi_sapi_module.startup(&cgi_sapi_module) == FAILURE) {
#ifdef ZTS
tsrm_shutdown();
#endif
return FPM_EXIT_SOFTWARE;
}
根据上面提到的 sapi_module_struct 的定义和 cgi_sapi_module 初始化的结果,不难看出 startup 调用的实际上是 fpm_main.c 中 php_cgi_startup 函数。而 php_cgi_startup 函数中主要做的事情就是调用 php 的 main.c 中定义的 php_module_startup 函数。像这种调用方式在 php 的实现中非常常见,保证了代码的鲁棒性。至于 php_module_startup 都干了哪些事情,后面再详细介绍,简而言之,该函数将会读取 php.ini 中的配置初始化 php 解释运行环境,主要包括:
- php 核心配置
- zend 配置
- php 扩展初始化及启动
static int php_cgi_startup(sapi_module_struct *sapi_module) /* {{{ */
{
if (php_module_startup(sapi_module, &cgi_module_entry, 1) == FAILURE) {
return FAILURE;
}
return SUCCESS;
}
到目前为止,需要的东西都初始化过了,该进入 fpm 的正题了:
if (0 > fpm_init(argc, argv, fpm_config ? fpm_config : CGIG(fpm_config), fpm_prefix, fpm_pid, test_conf, php_allow_to_run_as_root, force_daemon, force_stderr)) {
if (fpm_globals.send_config_pipe[1]) {
int writeval = 0;
zlog(ZLOG_DEBUG, "Sending \"0\" (error) to parent via fd=%d", fpm_globals.send_config_pipe[1]);
zend_quiet_write(fpm_globals.send_config_pipe[1], &writeval, sizeof(writeval));
close(fpm_globals.send_config_pipe[1]);
}
return FPM_EXIT_CONFIG;
}
if (fpm_globals.send_config_pipe[1]) {
int writeval = 1;
zlog(ZLOG_DEBUG, "Sending \"1\" (OK) to parent via fd=%d", fpm_globals.send_config_pipe[1]);
zend_quiet_write(fpm_globals.send_config_pipe[1], &writeval, sizeof(writeval));
close(fpm_globals.send_config_pipe[1]);
}
fpm_is_running = 1;
fcgi_fd = fpm_run(&max_requests);
parent = 0;
/* onced forked tell zlog to also send messages through sapi_cgi_log_fastcgi() */
zlog_set_external_logger(sapi_cgi_log_fastcgi);
/* make php call us to get _ENV vars */
php_php_import_environment_variables = php_import_environment_variables;
php_import_environment_variables = cgi_php_import_environment_variables;
/* library is already initialized, now init our request */
request = fpm_init_request(fcgi_fd);
首先便是 fpm_init,然后是 fpm_run,最后是 fpm_init_request。
int fpm_init(int argc, char **argv, char *config, char *prefix, char *pid, int test_conf, int run_as_root, int force_daemon, int force_stderr) /* {{{ */
{
fpm_globals.argc = argc;
fpm_globals.argv = argv;
if (config && *config) {
fpm_globals.config = strdup(config);
}
fpm_globals.prefix = prefix;
fpm_globals.pid = pid;
fpm_globals.run_as_root = run_as_root;
fpm_globals.force_stderr = force_stderr;
if (0 > fpm_php_init_main() ||
0 > fpm_stdio_init_main() ||
0 > fpm_conf_init_main(test_conf, force_daemon) ||
0 > fpm_unix_init_main() ||
0 > fpm_scoreboard_init_main() ||
0 > fpm_pctl_init_main() ||
0 > fpm_env_init_main() ||
0 > fpm_signals_init_main() ||
0 > fpm_children_init_main() ||
0 > fpm_sockets_init_main() ||
0 > fpm_worker_pool_init_main() ||
0 > fpm_event_init_main()) {
if (fpm_globals.test_successful) {
exit(FPM_EXIT_OK);
} else {
zlog(ZLOG_ERROR, "FPM initialization failed");
return -1;
}
}
if (0 > fpm_conf_write_pid()) {
zlog(ZLOG_ERROR, "FPM initialization failed");
return -1;
}
fpm_stdio_init_final();
zlog(ZLOG_NOTICE, "fpm is running, pid %d", (int) fpm_globals.parent_pid);
return 0;
}
不难看出,fpm_init 返回 -1 时,整个程序将会退出,当有错误发生但是 fpm_globals 中标记了 test_successful 时,会使用 exit(0) 退出,因为配置文件没有错误,而成功时会返回 0。
fpm 初始化的过程分 13 步,分别是:
- fpm_php_init_main() 注册进程清理方法
- fpm_stdio_init_main() 验证 /dev/null 是否可读写
- fpm_conf_init_main(test_conf, force_daemon) 校验并加载配置文件
- fpm_unix_init_main() 检查 unix 运行环境
- fpm_scoreboard_init_main() 初始化“进程记分牌”
- fpm_pctl_init_main() 进程管理相关初始化
- fpm_env_init_main() ?
- fpm_signals_init_main() 设置信号处理方式
- fpm_children_init_main() 初始化子进程,注册进程清理方法
- fpm_sockets_init_main() 初始化 sockets
- fpm_worker_pool_init_main() 注册 worker pool 清理方法
- fpm_event_init_main() 注册 event 清理方法
可以说相当复杂的一个过程,只要有一个函数无法返回 0,程序将直接退出。在完成这一系列的操作之后,fpm 会向配置文件中指定的 pid 文件写入 master 进程id。整个过程其实是为了启动 php-fpm 进程做初始化的工作,到这一步 php-fpm 还是无法接收请求的。
if (0 > fpm_conf_write_pid()) {
zlog(ZLOG_ERROR, "FPM initialization failed");
return -1;
}