PHP - FPM信号机制

来源:互联网 发布:阿里云栖大会2016 编辑:程序博客网 时间:2024/06/15 11:16

熟悉PHP-FPM的童鞋应该都知道它采用master/worker多进程架构设计,我们可以通过执行”xxx/sbin/php-fpm stop”或”xxx/sbin/php-fpm reload”停止或重新加载fpm。那么它究竟是怎样运作呢?

简单来说,其运用了信号机制来实现相应的功能。当我们执行”stop”命令时,系统向fpm进程发送停止信号,当我们执行”reload”命令时,系统向fpm进程发送SIGUSR2信号。fpm的信号分为两块:主进程(master)信号和子进程(worker)信号。下面将逐一介绍。

主进程信号

主进程信号的初始化流程:fpm_init->fpm_signals_init_main

static int sp[2];int fpm_signals_init_main(){    struct sigaction act;    //创建管道sp    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;    }    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;    }    //重置act    memset(&act, 0, sizeof(act));    //设置信号回调函数    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)) {        zlog(ZLOG_SYSERROR, "failed to init signals: sigaction()");        return -1;    }    return 0;}

上面的代码分为两部分:

  • 定义一个双向通信管道sp,并将管道设置为非阻塞模式;
  • 为主进程设置SIGTERMSIGINTSIGUSR1SIGUSR2SIGCHLDSIGQUIT信号回调函数sig_handler

接着分析sig_handler

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;    int saved_errno;    //确保主进程执行    if (fpm_globals.parent_pid != getpid()) {        return;    }    saved_errno = errno;    s = sig_chars[signo];    //主进程向sp[1]管道写入信号数据    write(sp[1], &s, sizeof(s));    errno = saved_errno;}

从上面的代码可以看出,当触发信号时,系统向sp[1]发送对应的信号数据,即sig_chars[signo]。那么fpm如何从sp管道提取信号数据呢?

fpm定义了一个sp[0]的IO读取事件:

int fpm_signals_get_fd(){    return sp[0];}void fpm_event_loop(int err){    static struct fpm_event_s signal_fd_event;    /* sanity check */    //非主进程 则跳过    if (fpm_globals.parent_pid != getpid()) {        return;    }    //注册信号IO时间    fpm_event_set(&signal_fd_event, fpm_signals_get_fd(), FPM_EV_READ, &fpm_got_signal, NULL);    fpm_event_add(&signal_fd_event, 0);    //...省略部分代码...}

上面注册sp[0]读操作事件,回调函数为fpm_got_signal

static void fpm_got_signal(struct fpm_event_s *ev, short which, void *arg){    char c;    int res, ret;    int fd = ev->fd;    do {        do {            res = read(fd, &c, 1);        } while (res == -1 && errno == EINTR);        if (res <= 0) {            if (res < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {                zlog(ZLOG_SYSERROR, "unable to read from the signal pipe");            }            return;        }        switch (c) {            case 'C' :                                  zlog(ZLOG_DEBUG, "received SIGCHLD");                //监听子进程退出信号 fpm/fpm_children.c                //该信号由子进程发出                fpm_children_bury();                break;            case 'I' :                                 zlog(ZLOG_DEBUG, "received SIGINT");                zlog(ZLOG_NOTICE, "Terminating ...");                //发送SIGINT信号,主进程和子进程退出                fpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET);                break;            case 'T' :                                  zlog(ZLOG_DEBUG, "received SIGTERM");                zlog(ZLOG_NOTICE, "Terminating ...");                //发送SIGTERM信号,主进程和子进程退出                fpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET);                break;            case 'Q' :                                  zlog(ZLOG_DEBUG, "received SIGQUIT");                zlog(ZLOG_NOTICE, "Finishing ...");                //发送SIGQUIT信号,主进程和子进程退出                fpm_pctl(FPM_PCTL_STATE_FINISHING, FPM_PCTL_ACTION_SET);                break;            case '1' :                                  zlog(ZLOG_DEBUG, "received SIGUSR1");                if (0 == fpm_stdio_open_error_log(1)) {                    zlog(ZLOG_NOTICE, "error log file re-opened");                } else {                    zlog(ZLOG_ERROR, "unable to re-opened error log file");                }                //重新打开日志,重启子进程                ret = fpm_log_open(1);                if (ret == 0) {                    zlog(ZLOG_NOTICE, "access log file re-opened");                } else if (ret == -1) {                    zlog(ZLOG_ERROR, "unable to re-opened access log file");                }                break;            case '2' :                                zlog(ZLOG_DEBUG, "received SIGUSR2");                zlog(ZLOG_NOTICE, "Reloading in progress ...");                //重启fpm                fpm_pctl(FPM_PCTL_STATE_RELOADING, FPM_PCTL_ACTION_SET);                break;        }        if (fpm_globals.is_child) {            break;        }    } while (1);    return;}

fpm_got_signal执行read(fd, &c, 1)读取sp[0]管道内容,根据具体的内容执行与之对应的操作。
fpm_children_bury:子进程退出信号处理。通过waitpid收集子进程退出信号,删除该子进程相关配置信息并释放该子进程使用的资源等操作。
fpm_log_open:重新打开日志文件,重启子进程。
fpm_pctl:该函数有两个参数,第一个参数表示状态值,第二个参数表示操作类型。当第二个参数值为FPM_PCTL_ACTION_SET时,设置fpm当前状态;然后执行fpm_pctl_action_next函数处理信号对应的操作。

static void fpm_pctl_action_next(){    int sig, timeout;    //判断是否有子进程在运行    if (!fpm_globals.running_children) {        //主进程退出        fpm_pctl_action_last();    }    if (fpm_signal_sent == 0) {        if (fpm_state == FPM_PCTL_STATE_TERMINATING) {            sig = SIGTERM;        } else {            sig = SIGQUIT;        }        //首次调用使用fpm.conf配置`process_control_timeout`参数值作为超时时间        timeout = fpm_global_config.process_control_timeout;    } else {        //FPM_PCTL_ACTION_TIMEOUT        if (fpm_signal_sent == SIGQUIT) {            sig = SIGTERM;        } else {            sig = SIGKILL;        }        timeout = 1;    }    //向所有子进程发送sig信号    fpm_pctl_kill_all(sig);    fpm_signal_sent = sig;    //注册定时事件监控子进程是否全部退出    fpm_pctl_timeout_set(timeout);}

fpm_pctl_action_next向子进程发出kill信号后,通过fpm_pctl_timeout_set(timeout)注册定时事件检查子进程kill情况,当所有子进程都已退出时,则执行fpm_pctl_action_last()

static void fpm_pctl_action_last(){    switch (fpm_state) {        //SIGUSR2        case FPM_PCTL_STATE_RELOADING:            fpm_pctl_exec();            break;        //SIGQUIT        case FPM_PCTL_STATE_FINISHING:        case FPM_PCTL_STATE_TERMINATING:            fpm_pctl_exit();            break;    }}
  • 对于SIGUSR2信号,执行fpm_pctl_exec函数。该函数内部调用C语言execvp函数启动fpm。
  • 对应SIGQUIT、SIGINT、SIGTREM信号,执行fpm_pctl_exit函数实现主进程退出。

以上就是主进程信号的处理流程,下面介绍子进程信号功能。

子进程信号

子进程信号的初始化流程:fpm_child_init->fpm_signals_init_child

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;    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)) {        printf("failed to init child signals: sigaction()\n");        return -1;    }    return 0;}

子进程设置SIGTERMSIGINTSIGUSR1SIGUSR2SIGCHLDSIGQUIT6个信号处理方式。
其中SIGTERMSIGINTSIGUSR1SIGUSR2SIGCHLD的信号回调函数为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;}

此处有一个较难理解的地方:close(0);它的作用是关闭子进程accept客户端请求的fd描述符。当子进程初始化时,系统将wp->listening_socket通过dup2函数绑定到子进程的STDIN_FILENO设备,STDIN_FILENO对应的FD值为0。
fpm_php_soft_quit函数实现子进程的软退出。代码如下:

void fpm_php_soft_quit(){    fcgi_set_in_shutdown(1);}void fcgi_set_in_shutdown(int new_value){    in_shutdown = new_value;}

fpm_php_soft_quit(),将in_shutdown值设为1,in_shutdown控制子进程accept客户端请求操作,当in_shutdown==1时,表明不再accept请求,则子进程会exit,关闭cgi,释放资源等操作。

总结

总体来说,FPM的信号机制简洁、清晰。主进程通过管道来传递信号,注册异步I0事件获取信号内容,通过事件回调来实现信号对应的具体操作。子进程除了SIGQUIT信号实现了软退出,其他5个信号都是使用系统默认函数(SIG_DFL)处理,子信号的信号处理流程位于主进程中。

0 0