php-fpm与fastcgi、php-cgi之间的关系及源码解析

前言

关于FastCGI、php-cgi、php-fpm的区别是什么,各自有什么用途,以及相互间的关系是什么,查阅相关资料,可谓是众说纷纭,莫衷一是:

说法一:fastcgi是一个协议,php-fpm实现了这个协议;
说法二:php-fpm是FASTCGI进程的管理器,用来管理fastcgi进程的;
说法三:php-fpm是php内核的一个补丁;
说法四:修改了php.ini配置文件后,没办法平滑重启,所以就诞生了php-fpm;
说法五:php-cgi是php自带的FASTCGI管理器;

一、什么是 CGI

CGI是干嘛的?通俗的讲,CGI是为了保证Web Server传递过来的数据是标准格式的,方便CGI程序的编写者

Web Server(比如说Nginx)只是内容的分发者。比如,如果请求/index.html,那么Web Server会去文件系统中找到这个文件,发送给浏览器,这里分发的是静态数据。好了,如果现在请求的是/index.php,根据配置文件,Nginx知道这个不是静态文件,需要去找PHP解析器来处理,那么他会把这个请求简单处理后交给PHP解析器。Nginx会传哪些数据给PHP解析器呢?url要有吧,查询字符串也得有吧,POST数据也要有,HTTP header不能少吧,好的,CGI就是规定要传哪些数据、以什么样的格式传递给后方处理这个请求的协议。

当Web Server收到/index.php这个请求后,会启动对应的CGI程序,这里就是PHP的解析器。接下来PHP解析器会解析php.ini文件,初始化执行环境,然后处理请求,再以规定CGI规定的格式返回处理后的结果,退出进程。web server再把结果返回给浏览器。

二、FastCGI又是什么呢?

CGI是个协议,跟进程什么的没关系。Fastcgi是CGI的升级版,一种语言无关的协议,FastCGI是用来提高CGI程序性能的(从字面意思来能好理解)

标准的CGI对每个请求都会执行这些步骤,所以处理每个请求的时间会比较长。这明显不合理嘛!那么FastCGI是怎么做的呢?首先,FastCGI会先启一个master,解析配置文件,初始化执行环境,然后再启动多个worker。当请求过来时,master会传递给一个worker,然后立即可以接受下一个请求。这样就避免了重复的劳动,效率自然是高。而且当worker不够用时,master可以根据配置预先启动几个worker等着;当然空闲worker太多时,也会停掉一些,这样就提高了性能,也节约了资源。这就是"FastCGI"的对进程的管理(先姑且这么说)

三、php-fpm是什么?

是一个实现了FastCGI(协议)的程序

我们知道web服务器与PHP应用之间通过SAPI接口进行交互数据。PHP提供了多种SAPI接口,例如 apache2hander、fastcgi、cli等等。

大家都知道,PHP的解释器是php-cgi。php-cgi只是个CGI程序,他自己本身只能解析请求,返回结果,不会管理进程,所以就出现了一些能够调度php-cgi进程的程序,比如说由lighthttpd分离出来的spawn-fcgi。而php-fpm也是这么个东西。

php-fpm的管理对象是php-cgi,但不能说php-fpm是FastCGI进程的管理器,因为前面说了FastCGI是个协议,似乎没有这么个进程存在,就算存在php-fpm也管理不了他(至少目前是)。他负责管理一个进程池,来处理来自Web服务器的请求。

php-fpm是一种master(主)/worker(子)多进程架构,与nginx设计风格有点类似。master进程主要负责CGI及PHP环境初始化、事件监听、子进程状态等等,worker进程负责处理php请求。

php-fpm是PHP内核的一个补丁? 以前是正确的,因为最开始的时候php-fpm没有包含在PHP内核里面,要使用这个功能,需要找到与源码版本相同的php-fpm对内核打补丁,然后再编译。后来PHP(5.3以后)内核集成了php-fpm,编译时加上–enalbe-fpm这个参数即可。

对于php.ini文件的修改,php-cgi进程是没办法平滑重启的,有了php-fpm后,就把平滑重启成为了一种可能,php-fpm对此的处理机制是新的worker用新的配置,已经存在的worker处理完手上的活就可以歇着了,通过这种机制来平滑过度的。

四、php-fpm源码解析

4.1 进程管理模式

PHP-FPM由1个master进程和N个worker进程组成。其中,Worker进程由master进程fork而来。

PHP-FPM有3种worker进程管理模式。

  1. Static:静态模式,启动时分配固定的worker进程。初始化调用fpm_children_make(wp,0,0,1)函数fork出pm.max_children数量的worker进程,后续不再动态增减worker进程数量
  2. Dynamic:动态模式,启动时分配固定的进程。伴随着请求数增加,在设定的浮动范围调整worker进程。初始化时调用fpm_children_make(wp,0,0,1)函数fork出pm.start_servers数量的worker进程,然后由每隔1秒触发的心跳事件fpm_pctl_perform_idle_server_maintenance()来维护空闲woker进程数量:空闲worker进程数量若多于pm.max_spare_servers则kill进程,若少于pm.min_spare_servers则fork进程。
  3. Ondemand:按需分配,当收到用户请求时fork worker进程。初始化时不生成worker进程,但注册事件ondemand_event监听listening_socket。当listen_socket收到request,先检查是否存在已生成的空闲的worker进程,若存在就使用这个空闲进程,否则fork一个新的进程。每隔1秒触发的心跳事件fpm_pctl_perform_idle_server_maintenance()会kill掉空闲时间超过pm.process_idle_timeout的worker进程

4.2 PHP-FPM 运行原理

master进程
master进程工作流程分为4个阶段,如下图:
在这里插入图片描述

  1. cgi初始化阶段:分别调用fcgi_init()sapi_startup()函数,注册进程信号以及初始化sapi_globals全局变量。
  2. php环境初始化阶段:由cgi_sapi_module.startup 触发。实际调用php_cgi_startup函数,而php_cgi_startup内部又调用php_module_startup执行。 php_module_startup主要功能:a).加载和解析php配置;b).加载php模块并记入函数符号表(function_table);c).加载zend扩展 ; d).设置禁用函数和类库配置;e).注册回收内存方法;
  3. php-fpm初始化阶段:执行fpm_init()函数。负责解析php-fpm.conf文件配置,获取进程相关参数(允许进程打开的最大文件数等),初始化进程池及事件模型等操作。
  4. php-fpm运行阶段:执行fpm_run() 函数,运行后主进程发生阻塞。该阶段分为两部分:fork子进程 和 循环事件。fork子进程部分交由fpm_children_create_initial函数处理( 注:ondemand模式在fpm_pctl_on_socket_accept函数创建)。循环事件部分通过fpm_event_loop函数处理,其内部是一个死循环,负责事件的收集工作。

worker进程
worker进程分为 接收客户端请求、处理请求、请求结束三个阶段。
在这里插入图片描述

  1. 接收客户端请求:执行fcgi_accept_request函数,其内部通过调用accept 函数获取客户端请求。
//请求锁
FCGI_LOCK(req->listen_socket);
req->fd = accept(listen_socket, (struct sockaddr *)&sa, &len);
//释放锁
FCGI_UNLOCK(req->listen_socket);12345

从上面的代码,可以注意到accept之前有一个请求锁的操作,这么设计是为了避免请求出现“惊群”的现象。当然,这是一个可选的选项,可以取消该功能。

  1. 处理请求阶段:首先,分别调用fpm_request_info、php_request_startup获取请求内容及注册全局变量($_GET、$_POST、$_SERVER、$_ENV、$_FILES);然后根据请求信息调用php_fopen_primary_script访问脚本文件;最后交给php_execute_script执行。php_execute_script内部调用zend_execute_scripts方法将脚本交给zend引擎处理。
  2. 请求结束阶段:执行php_request_shutdown函数。此时 回调register_shutdown_function注册的函数及__destruct()方法,发送响应内容、释放内存等操作。

总结
php-fpm采用master/worker架构设计, master进程负责CGI、PHP公共环境的初始化及事件监听操作。worker进程负责请求的处理功能。在worker进程处理请求时,无需再次初始化PHP运行环境,这也是php-fpm性能优异的原因之一。

4.3 标准IO

FastCGI的典型流程如下:

(1) web server(例如nginx或apache)接受到一个请求。然后,web server通过unix域socket或TCP socket连接到FastCGI应用。

(2) FastCGI应用可以选择接受或拒绝这个连接。如果接受了连接,FastCGI应用会试图从stream中读取到一个packet

(3) Web server发送的第一个packet是BEGIN_REQUEST packet。BEGIN_REQUEST packet包含一个独一无二的request ID。所有该request的后续packet都被这个ID标记。

Unix系统中,标准输入的文件描述符是0,标准输出的文件描述符是1,标准错误输出的文件描述符是2,宏定义如下:

#define STDIN_FILENO  0
#define STDOUT_FILENO  1
#define STDERR_FILENO  2

PHP-FPM重定向了这三个标准IO。

在master进程中,STDIN_FILENO(0)和STDOUT_FILENO(1)均重定向到”/dev/null”,STDERR_FILENO(2)重定向到error_log。

在worker进程中,STDIN_FILENO(0)重定向到listening_socket。如果catch_workers_output为no的话,STDOUT_FILENO(1)和STDERR_FILENO(2)均重定向到”/dev/null”。否则,STDOUT_FILENO(1)重定向到1个pipe的写端,而这个pipe的fd读端保存于master进程child链表对应的child节点结构的fd_stdout元素上。同样的,STDERR_FILENO(2)也重定向到1个pipe的写端,而这个pipe的读端fd保存于master进程child链表对应的child节点结构的fd_stderr元素上。以上两个位于master进程的pipe读端由master进程的reactor进行监听。

4.4 进程间通信模型

PHP-FPM中的进程间通信主要分为

4.4.1 Master进程和worker进程之间的通信

前面讲过master进程和worker进程间有两条pipe。Worker进程向STDOUT_FILE或STDERR_FILENO中写信息,master进程收到信息后写入log。Master进程用reactor监听两个pipe

4.4.2 Web server与worker进程之间的通信

Worker进程阻塞在accept(listening socket)监听web server

4.4.3 Web server与master进程之间的通信

当pm模式是ondemand时,master进程会在reactor注册listening_socket的监听事件。当有request到来,master进程将生成一个worker进程
在这里插入图片描述
PHP-FPM采用的进程模型是进程池。Worker进程继承由master进程socket(),bind(),listen()的socket fd并直接阻塞在accept()上。当有一个request到来,进程池中的一个worker进程接受request。当这个worker进程完成执行,就会返回进程池等待新的request。这事实上是leader/follower模式。在leader/follower模式中,仅有leader阻塞等待,其他进程都在sleep。同样的,在FPM中,由于linux内核解决了accept()的惊群问题,新request同样只会唤醒一个worker进程。在这里,leader的继任是由linux内核决定的(当然,你也可以用mutex守卫accept代码段来确保leader只有一位)。

Worker进程处理所有IO和逻辑。Master进程负责worker进程的生成和销毁。Master进程的Reactor注册了三个可读事件和四个定时器事件。当pm是ondemand时,额外注册一个可读事件。三个可读事件分别是1个信号事件,2个pipe事件。

Fpm_event_s 结构:

struct fpm_event_s {
    int fd;
    struct timeval timeout;
    struct timeval frequency;
    void (*callback)(struct fpm_event_s *, short, void *);
    void *arg;
    int flags;
    int index;  
    short which;
};

Flags代表该事件的类型。FPM中flags的值有三种:

FPM_EV_READ : 可读事件
FPM_EV_PERSIST : 心跳事件
FPM_EV_READ | FPM_EV_EDGE : 边缘触发的可读事件

Which代表该事件位于哪个事件队列。其值有两种:

FPM_EV_READ : 位于可读事件队列
FPM_EV_TIMEOUT : 位于定时器事件队列

事件队列的结构是双向链表

typedef struct fpm_event_queue_s {
    struct fpm_event_queue_s *prev;
    struct fpm_event_queue_s *next;
    struct fpm_event_s *ev;
} fpm_event_queue;

static struct fpm_event_queue_s *fpm_event_queue_timer = NULL;
static struct fpm_event_queue_s *fpm_event_queue_fd = NULL;  

fpm_event_queue_timer是定时器事件队列,fpm_event_queue_fd是可读事件队列。定时器事件队列并没有采用最小堆,红黑树或事件轮等结构,因为这个队列非常小,没有必要使用这些复杂结构。但是如果把定时器事件队列改为升序链表,对性能应该会有提升。

Fd和index仅在可读事件中使用。fd表示被监听的文件描述符。Index的值与使用哪个IO复用API有关。在epoll和select中,index的值等于fd的值。在poll中,index是该fd在描述符集fds[]中位置的下标。在心跳事件中,fd == -1,index == -1。

Struct timeval timeout和struct timeval frequency仅在心跳事件中使用。frequency表示每隔多少时间触发一次心跳事件,Timeout表示下一次触发心跳事件的时刻,通常由now与frequency相加而得。在可读事件中,这两个结构不设置。

Signal_fd_event事件
先来看fpm中的信号处理。

int fpm_signals_init_main() {
    struct sigaction act;
    /* create socketpair*/
    if (0 > socketpair(AF_UNIX, SOCK_STREAM, 0, sp)) {
        zlog(ZLOG_SYSERROR, "failed to init signals: socketpair()");
        return -1;
    }

    /*将两个socket设为NONBLOCK*/
    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;
    }
    /*如果程序成功地运行完毕,则自动关闭这两个fd*/
    if (0 > fcntl(sp[0], F_SETFD, FD_CLOEXEC) || 0 > fcntl(sp[1], F_SETFD, FD_CLOEXEC)) {
      zlog(ZLOG_SYSERROR, "falied to init signals: fcntl(F_SETFD, FD_CLOEXEC)");
      return -1;
    }
  
    memset(&act, 0, sizeof(act));
    /* 将信号处理函数设为sig_handler*/
    act.sa_handler = sig_handler;
    /* 将所有信号加入信号集*/
    sigfillset(&act.sa_mask);
  
    /* 更改指定信号的action */
    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;
}

master进程注册了SIGTERM,SIGINT,SIGUSR1,SIGUSR2,SIGCHLD,SIGQUIT,并创建了socketpair sp[]。当收到这些信号时,master进程将向sp[1]中写一个代表该信号的字符。

[SIGTERM] = ‘T’,
[SIGINT] = ‘I’,
[SIGUSR1] = ‘1’,
[SIGUSR2] = ‘2’,
[SIGQUIT] = ‘Q’,
[SIGCHLD] = ‘C’

Sp[0]就是Signal_fd_event事件监听的fd。该事件的回调函数对不同的信号(从sp[0]读到的代表信号的字符)做出不同的反应。

信号SIGCHLD:调用fpm_children_bury();该函数调用waitpid()分析子进程的status,根据情况决定是否重启子进程。

信号SIGINT , SIGTERM:调用fpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET);把fpm的状态改为terminating

信号SIGQUIT:调用fpm_pctl(FPM_PCTL_STATE_FINISHING, FPM_PCTL_ACTION_SET); 把fpm的状态改为finishing

信号SIGUSR1:调用fpm_stdio_open_error_log(1)重启error log file,调用fpm_log_open(1)重启access log file 并重启所有子进程

信号SIGUSR2:调用fpm_pctl(FPM_PCTL_STATE_RELOADING, FPM_PCTL_ACTION_SET); 把fpm的状态改为reloading

子进程的信号处理

子进程关闭了socketpair,并把SIGTERM,SIGINT,SIGUSR1,SIGUSR2,SIGHLD重新设为默认动作,把SIGQUIT设为soft quit。

int fpm_signals_init_child() {
    struct sigaction act, act_dfl;
    memset(&act, 0, sizeof(act));
    memset(&act_dfl, 0, sizeof(act_dfl));
    act.sa_handler = &sig_soft_quit;
    // 当system call或library function阻塞时一个信号到来。系统默认会返回错误并设置errno为EINTR.这里设为自动重启

    act.sa_flags |= SA_RESTART;
    act_dfl.sa_handler = 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)) {
        zlog(ZLOG_SYSERROR, "failed to init child signals: sigaction()");
        return -1;
    }
    zend_signal_init();
    return 0;
}

其一是可读事件队列。其二是定时器(timer)队列。

posted @ 2019-06-11 16:16  南山道士  阅读(173)  评论(0编辑  收藏  举报