PHP - FPM信号机制

熟悉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)处理,子信号的信号处理流程位于主进程中。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值