SAPI是PHP框架的接口层,它是进入PHP内部的入口。PHP中实现的SAPI有很多,本章选取了3个比较典型的SAPI:Cli、Fpm、Embed。其中Cli和Fpm是完整实现的应用程序,它们有定义自己的main函数,方便我们从入口开始逐步分析PHP的处理,尤其是单进程的Cli,非常方便调试,本书后面章节基本都是以Cli模式为例的。
不同SAPI的实现会有差异,但它们都是围绕PHP的生命周期实现的,在分析SAPI的实现时,我们将以PHP生命周期的几个阶段为主线,这样更有助于理解。尽管SAPI中并不涉及PHP具体的内部实现,但它是PHP框架最外层的接口,了解清楚它们的实现是探索PHP内核的第一步。
2.1 Cli
Cli(Command Line Interface),即命令行接口,用于在命令行下执行PHP脚本,就像Shell那样,它是执行PHP脚本最简便的一种方式。Cli SAPI最先是随PHP4.2.0版本发布的,但当时只是一个实验性的版本,需要在运行./configure时加上–enable-cli参数。从PHP4.3.0版本开始,Cli SAPI成为了正式模块,–enable-cli参数会被默认设为on,也可以用参数–disable-cli来屏蔽。
Cli模式通过执行编译的PHP二进制程序即可启动,它定义了很多命令行参数,比如:直接执行PHP代码(-r参数)、输出PHP版本(-v参数)、指定php.ini配置(-c参数)。直接在PHP命令后加PHP脚本则执行该脚本:
2.1.1 执行流程
Cli是单进程模式,处理完请求后就直接关闭了,生命周期先后经历module startup、request startup、execute script、request shutdown、module shutdown,其执行流程比较简单,关键的处理过程如下:
Cli SAPI的main函数位于/sapi/cli/php_cli.c中,执行时首先解析命令行参数,然后初始化sapi_module_struct,从结构体名称可以看出,它是记录SAPI信息的主要结构,这个结构中有几个函数指针,它们是内核定义的操作接口的具体实现,用来告诉内核如何读取、输出数据。
static sapi_module_struct cli_sapi_module = {
"cli", /* name */
"Command Line Interface", /* pretty name */
php_cli_startup, /* startup */
php_module_shutdown_wrapper, /* shutdown */
// 请求初始化函数,Cli没有定义
NULL, /* activate */
// 请求收尾处理函数,Cli中:fflush(stdout)
sapi_cli_deactivate, /* deactivate */
// 输出数据函数,Cli默认是到标准输出
sapi_cli_ub_write, /* unbuffered write */
sapi_cli_flush, /* flush */
NULL, /* get uid */
NULL, /* getenv */
// 错误处理函数
php_error, /* error handler */
// 调用header()函数的处理handler,Cli下是个空函数
sapi_cli_header_handler, /* header handler */
// 发送header时的函数
sapi_cli_send_headers, /* send headers handler */
sapi_cli_send_header, /* send header handler */
// 获取POST数据的函数
NULL, /* read POST data */
// 获取cookie的函数,Cli下是个空函数
sapi_cli_read_cookies, /* read Cookies */
// 向$_SERVER中注册变量的函数
sapi_cli_register_variables, /* register server variables */
sapi_cli_log_message, /* Log message */
NULL, /* Get request time */
NULL, /* Child terminate */
STANDARD_SAPI_MODULE_PROPERTIES
};
在完成参数的解析及sapi_module_struct的基本初始化后,接下来进入module startup阶段。
if (sapi_module->startup(sapi_module) == FAILURE) {
exit_status = 1;
goto out;
}
前面cli_sapi_module变量中定义的startup函数为php_cli_startup(),这个函数非常简单,直接调用了php_module_startup(),此函数的处理第1章已经介绍过,不再赘述。
static int php_cli_startup(sapi_module_struct *sapi_module)
{
if (php_module_startup(sapi_module, NULL, 0) == FAILURE) {
return FAILURE;
}
return SUCCESS;
}
在module startup阶段处理完成后,接下来进入请求初始化阶段:
// zend_first_try { ... } zend_end_try();是PHP内核用于错误处理的特殊结构
// zend_first_try和zend_end_try是C语言中的宏
// 此用法类似try catch,但专门适用于PHP的Zend引擎
zend_first_try {
if (sapi_module == &cli_sapi_module) {
exit_status = do_cli(argc, argv);
} else {
// 内置Web服务器的处理,下一节再单独介绍
exit_status = do_cli_server(argc, argv);
}
} zend_end_try();
do_cli()将完成请求的处理,此函数一开始对使用到的命令行参数进行解析,如果是一些查询系统信息之类的请求(如-v打印版本号、-m打印已安装的PHP模块(即扩展)、-i打印PHP的详细配置信息),则不需要经历PHP请求的生命周期,这里会单独处理,下面看一下执行PHP脚本请求时的处理:
zend_file_handle file_handle;
...
if (script_file) {
//fopen请求的脚本文件
if (cli_seek_file_begin(&file_handle, script_file, &lineno) != SUCCESS) {
...
}
}
//输入类型为ZEND_HANDLE_FP,也就是FILE*
file_handle.type = ZEND_HANDLE_FP;
PHP脚本执行时的输入形式有很多种,比如文件路径(filepath)、文件句柄(FILE)、文件描述符(fd)等,zend_file_handle结构就是用来定义不同输入形式的,这样可以统一PHP执行函数的输入参数:
typedef struct _zend_file_handle {
union {
int fd; //文件描述符
FILE *fp; //文件句柄
zend_stream stream; //zend封装的stream
} handle;
const char *filename; //文件路径
zend_string *opened_path;
//用于区分是哪种类型的输入形式:ZEND_HANDLE_FILENAME、ZEND_HANDLE_FD、
//ZEND_HANDLE_FP、ZEND_HANDLE_STREAM、ZEND_HANDLE_MAPPED
zend_stream_type type;
zend_bool free_filename;
} zend_file_handle;
Cli中此处使用的是文件句柄,在Linux环境下也就是fopen()打开的一个文件,这样内核就可以直接读取PHP脚本代码了。定义好请求的输入结构后将进行请求初始化操作,即request startup阶段,然后开始PHP脚本的执行:
//request startup阶段
if (php_request_startup()==FAILURE) {
*arg_excp = arg_free;
fclose(file_handle.handle.fp);
PUTS("Could not startup.\n");
goto err;
}
...
switch (behavior) {
case PHP_MODE_STANDARD:
...
//执行
php_execute_script(&file_handle);
...
break;
case ... //其他执行模式,比如php -r "php代码",此处的php代码不需要<? ?>标签
}
完成脚本的处理后进入request shutdown阶段:
//do_cli:
out:
if (request_started) {
php_request_shutdown((void *) 0);
}
do_cli()完成后回到main()函数中,进入module shutdown阶段,最后进程退出,这就是Cli下执行一个脚本的生命周期:
//main:
out:
if (module_started) {
php_module_shutdown();
}
if (sapi_started) {
sapi_shutdown();
}
2.1.2 内置Web服务器
从PHP5.4.0起,Cli SAPI提供了一个内置的Web服务器,这个内置的Web服务器主要用于本地开发使用,不可用于线上产品环境。URI请求会被发送到PHP所在的工作目录(Working Directory)进行处理,除非你使用了-t参数指定的目录。
如果请求未指定执行哪个PHP文件,则默认执行目录内的index.php或index.html。如果这两个文件都不存在,服务器会返回404错误。
当你在命令行启动这个Web Server时,如果指定了一个PHP文件,则这个文件会作为一个“路由”脚本,意味着每次请求都会先执行这个脚本。如果这个脚本返回FALSE,那么直接返回请求的文件(例如请求静态文件不做任何处理)。
实际上,这个内置的Web服务器是一个独立的SAPI,它有自己的sapi_module_struct结构,也就是说Cli定义了两个SAPI。Cli的这个功能很少使用,所以其具体实现不再展开。
2.2 Fpm
Fpm(FastCGI Process Manager)是PHP FastCGI运行模式的一个进程管理器,从它的定义可以看出,Fpm的核心功能是进程管理。
FastCGI是Web服务器(如Nginx、Apache)和处理程序之间的一种通信协议,它是与HTTP类似的一种应用层通信协议。
在网络应用场景下,PHP并没有像Golang那样实现HTTP网络库,而是实现了FastCGI协议,然后与Web服务器配合实现了HTTP的处理,Web服务器来处理HTTP请求,然后将解析的结果再通过FastCGI协议转发给处理程序,处理程序处理完成后将结果返回给Web服务器,Web服务器再返回给用户,如图2-1所示:
PHP实现了FastCGI协议的处理,但没有实现具体的网络处理,比较常用的网络处理模型有以下两种:
1.多进程模型:由一个主进程和多个子进程组成,主进程负责管理子进程,基本的网络事件由各个子进程处理,Nginx采用的就是这种模型。
2.多线程模型:与多进程类似,只是它是线程粒度,这种模型通常会由主线程监听、接收请求,然后交由子线程处理,memcached就是这种模式;但有的多线程模型也是采用多进程那种模式——主线程只负责管理子线程而不处理网络事件,各个子线程监听、接收、处理请求,memcached使用UDP协议时采用的是这种模式。
进程拥有独立的地址空间及资源,而线程没有,线程之间共享进程的地址空间及资源,所以在资源管理上多进程模型比较简单,而多线程模型则需要考虑不同线程之间的资源冲突,也就是线程安全。
2.2.1 基本实现
Fpm是一种多进程模型,它由一个master进程和多个worker进程组成。master进程启动时会创建一个socket,但是不会接收、处理请求,而是由fork出的worker子进程完成请求的接收及处理。
master进程的主要工作是管理worker进程,负责fork或杀掉worker进程,比如当请求比较多worker进程处理不过来时,master进程会尝试fork新的worker进程进行处理,而当空闲worker进程比较多时则会杀掉部分子进程,避免占用、浪费系统资源。
worker进程的主要工作是处理请求,每个worker进程会竞争地Accept请求,接收成功后解析FastCGI,然后执行相应的脚本,处理完成后关闭请求,继续等待新的连接,这就是一个worker进程的生命周期。从worker进程的生命周期可以看到:一个worker进程只能处理一个请求,只有将一个请求处理完后才会处理下一个请求。这与Nginx的事件模型有很大的区别,Nginx的子进程通过epoll管理套接字,如果一个请求数据还未发送完成则会处理下一个请求,即一个进程会同时连接多个请求,它是非阻塞的模型,只处理活跃的套接字。Fpm的这种处理模式大大简化了PHP的资源管理,使得在Fpm模式下不需要考虑并发导致的资源冲突。
master进程与worker进程之间不会直接进行通信,master通过共享内存获取worker进程的信息,比如worker进程当前状态、已处理请求数等,master进程通过发送信号的方式杀掉worker进程。
Fpm可以同时监听多个端口,每个端口对应一个worker pool,每个pool下对应多个worker进程,类似Nginx中server的概念,这些归属不同pool的worker进程仍由一个master管理。worker pool的结构为fpm_worker_pool_s,pool之间构成一个链表:
struct fpm_worker_pool_s {
//指向下一个worker pool
struct fpm_worker_pool_s *next;
//php-fpm.conf配置:pm、max_children、start_servers...
//pm参数用于设置子进程管理方式,可以是
//1)static:固定数量子进程,数量由max_children选项指定,适用于服务器资源充足且请求量稳定的情况
//2)dynamic:按需动态调整子进程数量,空闲进程数量由min_spare_servers和max_spare_servers
// 选项指定,并且在启动服务时会创建start_servers个子进程,此模式下最大的子进程数量
// 同样由max_children选项指定,适用于请求量波动性比较大的情况
//3)ondemand:不保留空闲子进程,请求来时才创建子进程,适用于低流量服务器
struct fpm_worker_pool_config_s *config;
//监听的套接字
int listening_socket;
...
//当前pool的worker链表,每个worker对应一个fpm_child_s结构
struct fpm_child_s *children;
//当前pool的worker运行总数
int running_children;
int idle_spawn_rate;
int warn_max_children;
//记录worker的运行信息,比如空闲、忙碌worker数
struct fpm_scoreboard_s *scoreboard;
...
}
在php-fpm.conf中通过[pool name]声明一个worker pool,每个pool各自配置监听的地址、进程管理方式、worker进程数等:
上图配置了两个worker pool,分别监听9000、9001端口,pool下的worker进程监听所属pool的端口,如图2-2所示:
2.2.2 Fpm的初始化
Fpm的main函数位于文件/sapi/fpm/fpm_main.c中。Fpm在启动后首先会进行SAPI的注册操作;接着会进入PHP生命周期的module startup阶段,在这个阶段会调用各个扩展定义的MINT钩子函数。然后会进行一系列的初始化操作,最后master、worker进程进入不同的处理环节。
int main(int argc, char *argv[])
{
...
//注册SAPI:将全局变量sapi_module设置为cgi_sapi_module
sapi_startup(&cgi_sapi_module);
...
//执行php_module_startup()
if (cgi_sapi_module.startup(&cgi_sapi_module) == FAILURE) {
return FPM_EXIT_SOFTWARE;
}
...
//初始化
if (0 > fpm_init(...)){
...
}
...
fpm_is_running = 1;
//后面都是worker进程的操作,master进程不会走到下面
fcgi_fd = fpm_run(&max_requests);
parent = 0;
...
}
fpm_init()方法中将完成以下几个关键操作:
1.fpm_conf_init_main()
解析php-fpm.conf配置文件,为每个worker pool分配一个fpm_worker_pool_s结构,各worker pool的配置在解析后保存到fpm_worker_pool_s->config中,下面是config中的几个常用配置:
struct fpm_worker_pool_config_s {
char *name; //pool名称,即配置:[pool name]
char *user; //Fpm的启动用户,配置:user
char *group; //配置:group
char *listen_address; //监听的地址,配置:listen
...
int pm; //进程模型:static、dynamic、ondemand
int pm_max_children; //最大worker进程数
int pm_start_servers; //启动时初始化的worker数
int pm_min_spare_servers; //最小空闲worker数,如果空闲worker数量低于此值,则会fork出更多worker
int pm_max_spare_servers; //最大空闲worker数
int pm_process_idle_timeout; //worker空闲时间
int pm_max_requests; //worker处理的最多请求数,超过这个值worker将被kill
...
}
2.fpm_scoreboard_init_main()
分配用于记录worker进程运行信息的结构,此结构分配在共享内存上,master正是通过这种方式获取worker的运行信息。会为每个worker pool分配一个fpm_scoreboard_s结构;会为pool下的每个worker进程分配一个fpm_scoreboard_proc_s结构,这些结构的地址保存在fpm_scoreboard_s->procs数组中,worker链表fpm_child_s中的节点结构里有一个scoreboard_i成员,该成员是该worker的运行信息在fpm_scoreboard_s->procs数组中所在位置的索引,各结构的对应关系如图2-3所示:
3.fpm_signals_init_main()
这一步会通过socketpair()创建一个管道,这个管道并不是用于master和worker进程通信的,它只在master进程中使用,具体用途稍后介绍。同时,设置master的信号处理函数为sig_handler(),当master收到SIGTERM、SIGINT、SIGUSR1、SIGUSR2、SIGCHLD、SIGQUIT信号时将调用sig_handler()进行处理,在此函数中会把收到的信号写入在fpm_signals_init_main()中创建的管道。
static int sp[2];
int fpm_signals_init_main()
{
struct sigaction act;
//创建一个全双工管道
if (0 > socketpair(AF_UNIX, SOCK_STREAM, 0, sp)) {
return -1;
}
//注册信号处理handler
act.sa_handler = sig_handler;
sigfillset(&act.sa_mask);
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)) {
return -1;
}
return 0;
}
static void sig_handler(int signo)
{
static const char sig_chars[NSIG + 1] = {
[SIGTERM] = 'T',
[SIGINT] = 'I',
[SIGUSR1] = '1',
[SIGUSR2] = '2',
[SIGQUIT] = 'Q',
[SIGCHLD] = 'C',
};
char s;
...
s = sig_chars[signo];
//将信号通知写入管道sp[1]端
write(sp[1], &s, sizeof(s));
...
}
4.fpm_sockets_init_main()
创建每个worker pool的socket套接字,启动后worker将监听此socket来接收请求。
5.fpm_event_init_main()
启动master的事件管理,Fpm实现了一个事件管理器用于管理IO、定时事件,其中IO事件根据不同平台选择kqueue、epoll、poll、select等管理,定时事件就是定时器,一定时间后触发某个事件。
fpm_init()中主要的处理就是上面介绍的几个init过程,完成这些初始化操作后就是最关键的fpm_run()操作了,此环节将fork子进程,启动进程管理器,执行后master进程将不会返回main()函数,之后各worker进程会返回,也就是说main()函数中调用fpm_run()之后的操作均是worker进程的。
int fpm_run(int *max_requests)
{
struct fpm_worker_pool_s *wp;
//遍历worker pool
for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
//调用fpm_children_make() fork子进程
is_parent = fpm_children_create_initial(wp);
if (!is_parent) {
//fork出的worker进程
goto run_child;
}
}
//master进程将进入event循环,不再往下走
fpm_event_loop(0);
run_child: //只有worker进程会到这里
*max_requests = fpm_globals.max_requests;
return fpm_globals.listening_socket; //返回监听的套接字
}
由此fpm_run()的处理来看,在fork出worker进程后,子进程将返回main()中,而master进程进入fpm_event_loop(),这是一个事件循环。
2.2.3 worker——请求处理
worker进程返回main()函数后将继续向下执行,此后的流程就是worker进程不断Accept请求,有请求到达后将读取并解析FastCGI协议的数据,解析完成后开始执行PHP脚本,执行完成后关闭请求,继续监听等待新的请求到达。关于FastCGI协议的解析不是本书的重点,这里不再介绍。各worker处理请求的周期如图2-4所示:
上图中:
1.等待请求:worker进程阻塞在fcgi_accept_request()中等待请求。
2.解析请求:fastcgi请求到达后被worker接收,然后开始接收并解析请求数据,直到request数据完全到达。
3.请求初始化:执行php_request_startup(),此阶段会调用每个扩展的PHP_RINIT_FNCTION()。
4.执行PHP脚本:由php_execute_script()完成PHP脚本的编译、执行工作。
5.关闭请求:请求完成后执行php_request_shutdown(),此阶段会调用每个扩展的PHP_RSHUTDOWN_FUNCTION(),然后进入步骤1等待下一个请求。
具体的处理如下:
int main(int argc, char *argv[])
{
...
//worker在调用后返回监听的socket fd
fcgi_fd = fpm_run(&max_requests);
parent = 0;
//初始化fastcgi请求
request = fpm_init_request(fcgi_fd);
//worker进程将阻塞在这,等待请求
while (EXPECTED(fcgi_accept_request(request) >= 0) {
SG(server_context) = (void *)request;
init_request_info();
//请求开始
if (UNEXPECTED(php_request_startup() == FAILURE)) {
...
}
...
fpm_request_executing();
//编译、执行PHP脚本
php_execute_script(&file_handle);
...
//请求结束
php_request_shutdown((void *)0);
...
}
...
//worker进程退出,进入module shutdown阶段
php_module_shutdown();
...
}
worker进程的fpm_scoreboard_proc_s->request_stage被用于记录worker当前所处的阶段,master进程也是通过这个值来获取worker的状态,一次请求过程中这个值将先后被设置为以下值:
1.FPM_REQUEST_ACCEPTING:等待请求阶段;
2.FPM_REQUEST_READING_HEADERS:读取fastcgi请求header阶段。
3.FPM_REQUEST_INFO:获取请求信息阶段,此阶段会将请求的method、query string、request uri等信息保存到各worker进程的fpm_scoreboard_proc_s结构中,此操作需要加锁,因为master进程也会操作此结构。
4.FPM_REQUEST_EXECUTING:执行PHP脚本阶段。
5.FPM_REQUEST_END:没有使用。
6.FPM_REQUEST_FINISHED:请求处理完成。
通过gdb可以清楚地追踪到一个请求的完整处理流程,为了方便找到处理的worker进程,可以将worker数设置为1,attach接管worker进程后通过bt(打印进程当前的调用栈)可以看到,worker进程阻塞在fcgi_accept_request()上:
2.2.4 master——进程管理
master在调用fpm_run()后不再返回,而是进入一个事件循环中,此后master将始终围绕着几个事件进行处理。我们首先介绍一下Fpm三种不同的进程管理方式,具体要使用哪种模式可以在conf配置中通过pm指定,例如:pm = dynamic:
1.静态模式(static):这种方式比较简单,在启动时master根据pm.max_children配置fork出相应数量的worker进程,也就是worker进程数是固定不变的。
2.动态模式(dynamic):这种模式比较常用,在Fpm启动时会根据pm.start_servers配置初始化一定数量的worker。运行期间如果master发现空闲worker数低于pm.min_spare_servers配置数(表示请求比较多,worker处理不过来了)则会fork worker进程,但总的worker数不能超过pm.max_children;如果master发现空闲worker数超过了pm.max_spare_servers(表示闲着的worker太多了)则会杀掉一些worker,避免占用过多资源,master通过这4个值来动态控制worker的数量。
3.按需模式(ondemand):这种模式很像传统cgi,在启动时不分配worker进程,等来了请求后再fork子进程进行处理。总的worker数不超过pm.max_children,处理完成后worker进程不会立即退出,当空闲时间超过pm.process_idle_timeout后再退出。
master进程进入fpm_event_loop()事件循环,在这个方法中master将循环处理master注册的几个IO及定时器事件,当有事件触发时将回调具体的handler进行处理:
void fpm_event_loop(int err) {
//注册IO、定时器事件,稍后再作说明
...
//进入事件循环,master进程将阻塞在此
while (1) {
...
//等待IO事件
ret = module->wait(fpm_event_queue_fd, timeout);
...
//检查定时器事件
...
}
}
以下是master注册的几个重要事件:
1.信号事件
前面已经介绍过,fpm_init()阶段时分配了一个sp管道,当master收到SIGTERM、SIGINT、SIGUSR1、SIGUSR2、SIGCHLD、SIGQUIT信号时会把对应的信号写到sp[1]管道中,然后master在fpm_event_loop()中注册了此管道可读的事件:
static struct fpm_event_s signal_fd_event;
// 设置监听的fd、事件触发类型、回调函数
fpm_event_set(&signal_fd_event, fpm_signals_get_fd(), FPM_EV_READ, &fpm_got_signal, NULL);
// 注册事件
fpm_event_add(&signal_fd_event, 0);
fpm_signals_get_fd()返回的是sp[0],所以这里就是注册了一个sp[0]可读的事件,当sp[0]可读时将回调fpm_got_signal()进行处理。总体来看,当向master发送信号时,首先信号handler会把信号通知发送到sp[1]管道,然后出发sp[0]可读事件,回调fpm_got_signal()进行处理,数据流如图2-5所示:
fpm_got_signal()根据不同的信号进行相应的处理,不同信号值对应的处理逻辑如下:
(1)SIGNAL/SIGTERM/SIGQUIT:退出Fpm,在master收到退出信号后将向所有worker进程发送退出信号,通知worker退出,然后master退出。
(2)SIGUSR1:重新加载日志文件,生产环境中通常会根据时间对日志进行切割,切割后会生成一个新的日志文件,如果进程不重新加载文件,则无法继续写入日志(应该指的是还是往被切割的旧日志文件里写入,不会往新日志文件里写),这时就需要向master发送一个USR1信号,告诉master重新加载日志文件。
(3)SIGUSR2:重启Fpm,首先master会向所有worker进程发送退出信号,等全部worker成功退出后,master会调用execvp()重新启动一个新的Fpm,最后旧的master退出。
(4)SIGCHLD:这个信号是子进程退出时操作系统发送给父进程的,子进程退出时,操作系统将子进程置为僵尸状态,这个进程称为僵尸进程,它只保留最小的一些内核数据结构,以便父进程查询子进程的退出状态,只有当父进程调用wait()或waitpid()函数查询子进程退出状态后子进程才终止,Fpm中当worker进程因为异常原因(比如coredump了)退出而非master主动杀掉时,master将收到此信号(这句话有点问题,不管子进程怎么终止,父进程总会收到SIGCHLD信号),这时父进程将调用waitpid()保证worker退出,然后检查是不是需要重新fork新的worker。
可通过gdb进行调试,通过attach命令接管master进程,然后向master发送不同的信号:
上图中,先使用break命令在fpm_got_signal函数处打上断点,然后attach master进程,然后c命令从断点处继续执行。
2.进程检查定时器
fpm_event_loop()中调用fpm_pctl_perform_idle_server_maintenance_heartbeat()注册了一个定时器:
// fpm_event_loop:
if (!err) { // err传的是0:fpm_event_loop(0)
fpm_pctl_perform_idle_server_maintenance_heartbeat(NULL, 0, NULL);
// log
}
这个定时器是用来定期检查worker进程数的,master通过这个定时器每隔一定时间检查worker的数量,根据不同策略(static/dynamic/ondemand)的配置决定是否需要fork或kill进程。
void fpm_pctl_perform_idle_server_maintenance_heartbeat(struct fpm_event_s *ev,
short which, void *arg) {
static struct fpm_event_s heartbeat;
struct timeval now;
// 定时器触发时回调的逻辑
...
// 此后的逻辑只在注册时执行一次,以后不会再到达这里
// 设置定时器,回调函数也是本函数
fpm_event_set_timer(&heartbeat, FPM_EV_PERSIST,
&fpm_pctl_perform_idle_server_maintenance_heartbeat, NULL);
// 注册定时器,第二个参数是每隔多少ms触发
fpm_event_add(&heartbeat, FPM_IDLE_SERVER_MAINTENANCE_HEARTBEAT);
}
从代码可以看出,这里注册了一个定时器,其回调函数是fpm_pctl_perform_idle_server_maintenance_heartbeat(),也就是自己,触发间隔为FPM_IDLE_SERVER_MAINTENANCE_HEARTBEAT(即1000ms)。定时器触发时的逻辑如下所示:
// fpm_pctl_perform_idle_server_maintenance_heartbeat:
if (which == FPM_EV_TIMEOUT) { // 确认是一个定时事件
fpm_clock_get(&now);
// fpm_pctl_can_spawn_children()检查master当前状态是否正常,比如master收到了退出信号
// 这时候就不需要再进行fork、kill操作了
if (fpm_pctl_can_spawn_children()) {
// 具体的worker检查逻辑
fpm_pctl_perform_idle_server_maintenance(&now);
...
}
return;
}
static静态模式不会动态地管理worker进程,所以fpm_pctl_perform_idle_server_maintenance()主要是针对dynamic、ondemand两种模式的。检查的过程就是遍历所有worker pool,根据不同的策略配置进行不同的处理,具体的worker控制策略前面介绍过了,这里是其具体实现。
检查每个worker pool时,首先根据此pool下所有worker的状态计算出处于空闲、忙碌状态的worker数,这是通过每个worker进程的fpm_scoreboard_proc_s->request_state状态判断的:
// fpm_pctl_perform_idle_server_maintenance
struct fpm_child_s *last_idle_child = NULL; // 空闲时间最久的worker
int idle = 0; // 空闲worker数
int active = 0; // 忙碌worker数
for (child = wp->children; child; child = child->next) {
// 根据worker进程的fpm_scoreboard_proc_s->request_stage判断
if (fpm_request_is_idle(child)) {
// 找空闲时间最久的worker
...
idle++
} else {
active++;
}
}
接着根据不同的pm模式分别处理,如果是按需模式(ondemand),则会判断空闲时间最久的那个worker的空闲时间是否达到pm.process_idle_timeout阈值,到了则kill掉该worker:
// fpm_pctl_perform_idle_server_maintenance
if (wp->config->pm == PM_STYLE_ONDEMAND) {
if (!last_idle_child) continue;
...
if (last.tv_sec < now.tv_sec - wp->config->pm_process_idle_timeout) {
// 如果空闲时间最长的worker空闲时间超过了process_idle_timeout,则杀掉该worker
last_idle_child->idle_kill = 1;
fpm_pctl_kill(last_idle_child->pid, FPM_PCTL_QUIT);
}
continue;
}
从处理过程可以看到,ondemand模式下每次只kill空闲时间最久的那个worker,如果有多个worker都达到了pm.process_idle_timeout的阈值,则需要等到下一个周期进行处理。
如果是动态模式(dynamic),首先检查空闲worker数是否超过了pm.max_spare_servers配置的数量,若超了则杀掉空闲时间最久的那个,同样,这里每个周期只会kill一个worker;接着检查空闲worker数是否低于pm.min_spare_servers,如果低于则需要fork更多的worker进行补充,但总的worker数不能超过pm.max_children。在fork过程中会通过idle_spawn_rate这个值控制频率,默认一个周期内只会fork一个进程,但如果发现几个周期一直在fork,则说明worker数远远不足,这时就会把fork数翻一倍,上限是32:
// fpm_pctl_perform_idle_server_maintenance
if (wp->config->pm != PM_STYLE_DYNAMIC) continue;
if (idle > wp->config->pm_max_spare_servers && last_idle_child) {
// 空闲worker太多了,杀掉
last_idle_child->idle_kill = 1;
fpm_pctl_kill(last_idle_child->pid, FPM_PCTL_QUIT);
wp->idle_spawn_rate = 1;
continue;
}
if (idle < wp->config->pm_min_spare_servers) {
// 空闲worker太少了,如果总worker数未达到max数则fork
if (wp->running_children >= wp->config->pm_max_children) {
// worker总数已达上限
continue;
}
// 确定要fork多少个worker
children_to_fork = MIN(wp->idle_spawn_rate, wp->config->pm_min_spare_servers - idle);
// 确保不超过worker上限
children_to_fork = MIN(children_to_fork,
wp->config->pm_max_children - wp->running_children);
// fork
fpm_children_make(wp, 1, children_to_fork, 1);
// 将idle_spawn_rate翻一倍,这样下个周期内一次fork数就会翻倍,如果下个周期内空闲worker数达到
// min_spare_servers则会把这个值重置为1
if (wp->idle_spawn_rate < FPM_MAX_SPAWN_RATE) {
wp->idle_spawn_rate *= 2;
}
continue;
}
wp->idle_spawn_rate = 1;
3.执行超时检查定时器
php-fpm.conf中有一个request_terminate_timeout配置项,如果worker处理一个请求的总时长超过了这个值,那么master会向此worker进程发送kill -TERM信号杀掉worker进程,此配置单位为秒,默认值为0,表示关闭此机制。
这个功能也是通过定时器实现的,master每隔一定时间检查所有处理中的worker,如果发现其处理时间达到阈值则杀掉这个worker。另外,Fpm记录的slow log也是通过这个定时器完成的。
// fpm_event_loop
if (fpm_globals.heartbeat > 0) {
fpm_pctl_heartbeat(NULL, 0, NULL);
}
fpm_pctl_heartbeat()中定义了一个定时器,每隔fpm_globals.heartbeat毫秒触发一次,这个时间是根据pm_request_terminate_timeout计算得到的,回调函数也是自己:
// fpm_conf_process_all_pools
// 设定触发周期
fpm_globals.heartbeat = fpm_globals.heartbeat ?
MIN(fpm_globals.heartbeat, wp->config->request_terminate_timeout * 1000) / 3) :
(wp->config->request_terminate_timeout * 1000) / 3;
// 注册定时器(定时器回调handler)
void fpm_pctl_heartbeat(struct fpm_event_s *ev, short which, void *arg) {
static struct fpm_event_s heartbeat;
struct timeval now;
if (which == FPM_EV_TIMEOUT) {
// 这个函数也是回调函数,回调时将走到这个分支
fpm_clock_get(&now);
fpm_pctl_check_request_timeout(&now);
return;
}
// 下面的逻辑只在注册时执行一次
// 注册定时器,回调函数是自己,每隔fpm_globals.heartbeat触发一次
fpm_event_set_timer(&heartbeat, FPM_EV_PERSIST, &fpm_pctl_heartbeat, NULL);
fpm_event_add(&heartbeat, fpm_globals.heartbeat);
}
定时器触发时将调用fpm_pctl_check_request_timeout()进行处理,处理逻辑也比较简单,遍历worker pool,然后逐个判断worker处理中的请求是否超时,此判断根据请求处理开始时间fpm_scoreboard_proc_s->accepted完成,如果超时则kill:
// fpm_pctl_check_request_timeout
// 每个worker pool
int terminate_timeout = wp->config->request_terminate_timeout;
int slowlog_timeout = wp->config->request_slowlog_timeout;
struct fpm_child_s *child;
if (terminate_timeout || slowlog_timeout) {
// 遍历当前worker pool下的所有worker
for (child = wp->children; child; child = child->next) {
// 检查当前worker处理的请求是否超时
fpm_request_check_timed_out(child, now, terminate_timeout, slowlog_timeout);
}
}
以上就是master进程主要的处理,除了介绍的这些事件,还有一个IO事件没有提到,这个事件只在ondemand模式下使用。因为ondemand模式下Fpm启动时不会预创建worker,有请求时才会生成子进程,所以有请求到达时需要通知master进程进行fork。这个事件是在fpm_children_create_initial()时注册的,事件处理函数为fpm_pctl_on_socket_accept(),具体逻辑不再展开,比较好理解。
2.3 Embed
前面介绍的Cli、Fpm都是完整的应用程序,它们有定义自己的main函数,其应用场景是固定的,另外两个没有介绍的SAPI(litespeed、apache2handler)是配合其他应用使用的。如果我们在自己的第三方程序中也想使用PHP,比如开发一个路由器,其中想嵌入PHP实现的Web配置的功能,难道要重新开发一个SAPI,然后提供给第三方应用使用?PHP提供了一个用于这类应用场景下的SAPI,那就是Embed,它在编译后就是普通的库文件(可以选择编译为静态库、共享库),我们可以在其他C/C++应用中调用PHP提供的API,甚至可以提供给其他语言使用。
2.3.1 实现
Embed的实现的逻辑非常简单,只是把PHP生命周期的几个处理函数进行了封装,它对外提供了两个API。
1.php_embed_init()
这个接口主要进行PHP框架的初始化操作,比如启动TSRM(Thread Safety Resource Management,线程安全资源管理)、初始化SAPI、初始化信号处理,还完成了php_module_startup()、php_request_startup()操作。
// EMBED_SAPI_API是一个宏,用来定义如何导出PHP嵌入式中的符号,使其在不同平台、编译器环境下都能被外部访问
EMBED_SAPI_API int php_embed_init(int argc, char **argv) {
// ZTS宏代表Zend Thread Safety,用来控制是否开启线程安全模式
#ifdef ZTS
// 启动SAPI
tsrm_startup(1, 1, 0, NULL);
(void)ts_resource(0);
ZEND_TSRMLS_CACHE_UPDATE();
#endif
// 初始化SAPI
sapi_startup(&php_embed_module);
...
// 进入module startup阶段
if (php_embed_module.startup(&php_embed_module) == FAILURE) {
return FAILURE;
}
...
// 请求request startup阶段
if (php_request_startup() == FAILURE) {
return FAILURE;
}
...
}
在第三方应用中嵌入PHP时首先要调用这个接口,然后就可以使用PHP/Zend提供的API完成PHP脚本的执行了。
2.php_embed_shutdown()
此接口与php_embed_init()对应,主要完成PHP框架的关闭收尾工作,包括request shutdown、module shutdown两个阶段的操作。
EMBED_SAPI_API void php_embed_shutdown(void) {
// 关闭请求,request shutdown阶段
php_request_shutdown((void *)0);
// 关闭模块,module shuwdown阶段
php_module_shutdown();
sapi_shutdown();
#ifdef ZTS
tsrm_shutdown();
#endif
...
}
2.3.2 使用
下例在C程序中嵌入了PHP,使用的是共享库,这个例子中C程序将调用PHP完成一个普通PHP脚本的执行。
// my_main.c
#include <php/sapi/embed/php_embed.h>
int main(int argc, char **argv) {
zend_file_handle file_handle;
// PHP框架初始化
php_embed_init(argc, argv);
file_handle.type = ZEND_HANDLE_FILENAME;
file_handle.filename = "call.php";
// execute php script
php_execute_script(&file_handle);
// 关闭PHP框架
php_embed_shutdown();
return 0;
}
// call.php
function my_func($a, $b) {
return $a + $b;
}
echo my_func(100, 200);
编译时需要使用-I指定PHP的头文件目录,包括PHP、Zend、SAPI、TSRM的;通过-l及-L指定共享库的目录及名称。另外,还需要通过–rpath指定程序在运行时查找共享库的位置。假如PHP安装路径为/usr/local/php7,则编译参数为:
编译后执行:
gcc编译参数-I、-L只是在编译时指定了库文件,与执行时无关,执行时会默认去/usr/lib目录下搜索.so,可通过环境变量LD_LIBRARY_PATH指定.so文件的搜索目录,也可以将搜索目录添加到/etc/ld.so.conf配置中。
处理以上方式,还可在编译时通过–rpath指定.so文件的运行时搜索目录,需要加到-Wl,
后面,-Wl,
后面的内容是传递给linker的option,如上面的-Wl,--rpath /usr/local/php7/lib
,它的优先级在LD_LIBRARY_PATH、/etc/ld.so.conf之上。