(nginx源码系列六)--sigsuspend的学习

原创内容,转载请注明出处

Posted by Weakyon Blog on May 14, 2015

twemproxy的多进程改造计划中参考了nginx的设计,然而nginx有一段让我看不太明白。

这篇文章总结的很好

sigsuspend sigprocmask函数的使用方法

sigsuspend将新的信号集阻塞操作和pause操作组合在一起成为原子操作

做了以下的操作

(1) 设置新的mask阻塞当前进程;

(2) 收到信号,恢复原先mask;

(3) 调用该进程设置的信号处理函数;

(4) 待信号处理函数返回后,sigsuspend返回。

现在再来回顾一下我当时疑惑的代码段

void
ngx_master_process_cycle(ngx_cycle_t *cycle)
{
    //...
    sigemptyset(&set);
    sigaddset(&set, SIGCHLD);
    sigaddset(&set, SIGALRM);
    sigaddset(&set, SIGIO);
    sigaddset(&set, SIGINT);
    sigaddset(&set, ngx_signal_value(NGX_RECONFIGURE_SIGNAL));
    sigaddset(&set, ngx_signal_value(NGX_REOPEN_SIGNAL));
    sigaddset(&set, ngx_signal_value(NGX_NOACCEPT_SIGNAL));
    sigaddset(&set, ngx_signal_value(NGX_TERMINATE_SIGNAL));
    sigaddset(&set, ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
    sigaddset(&set, ngx_signal_value(NGX_CHANGEBIN_SIGNAL));

    if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {
        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                      "sigprocmask() failed");
    }    

    sigemptyset(&set);

    ngx_start_worker_processes(cycle, ccf->worker_processes,
                               NGX_PROCESS_RESPAWN);
    //...
    for ( ;; ) {
        //...
        sigsuspend(&set);
        //...
        if (ngx_reconfigure) {
            ngx_start_worker_processes(cycle, ccf->worker_processes,
                                           NGX_PROCESS_RESPAWN);
        }
        //...
    }
    //...
}

首先初始化了一个空集set,往里面添加了信号形成阻塞的信号集合,然后运行sigprocmask(SIG_BLOCK, &set, NULL)阻塞这些信号

临界区(在sigprocmask之后和sigsuspend之前的代码)中如果出现了信号集的信号都会被阻塞

临界区之后运行的sigsuspend(&set)处理的信号集合是个空集

sigsuspend将不会阻塞任何信号,如果在临界区中就发生信号会执行信号函数,否则就等待任何信号的发生,然后往下运行

这里要提一下临界区的代码,主要就是一句ngx_start_worker_processes。

ngx_start_worker_processed中会fork出子进程,每个子进程初始化时会调用ngx_worker_process_init

static void
ngx_worker_process_init(ngx_cycle_t *cycle, ngx_int_t worker)
{
    //...
    sigemptyset(&set);

    if (sigprocmask(SIG_SETMASK, &set, NULL) == -1) {
        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                      "sigprocmask() failed");
    }
}

可以看到,在fork出进程后初始化,将继承的阻塞信号集设置为空集,也就是不在阻塞这些信号

这样做是怕fork前信号函数执行,例如在临界区执行reload信号执行函数,导致逻辑复杂化,现在的逻辑就很清晰,必须全部fork完毕以后才能reload。

所以说twemproxy的多进程改造也应该学习这样的过程,把fork作为临界区来处理。


sigsuspend在APUE这本圣经里面有个案例是当作同步来使用的。我觉得这个例子也很好。

我稍微补充了一下内容和书中有一些区别

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <signal.h>

static volatile sig_atomic_t sigflag;
static sigset_t newmask,oldmask,zeromask;

static void printcharacter(const char* str)
{
    const char *ptr;
    setbuf(stdout,NULL);
    for(ptr=str;*ptr!='\0';ptr++)
        putc(*ptr,stdout);
}
static void sig_usr(int signo)
{
    sigflag = 1;
}
void TELL_WAIT(void)
{
    signal(SIGUSR1,sig_usr);  //设置信号
    signal(SIGUSR2,sig_usr);
    sigemptyset(&zeromask);
    sigemptyset(&newmask);
    sigaddset(&newmask,SIGUSR1);
    sigaddset(&newmask,SIGUSR2);
    sigprocmask(SIG_BLOCK,&newmask,&oldmask);  //把SIGUSR1和SIGUSR2设为阻塞
}
void TELL_PARENT(pid_t pid)
{
    kill(pid,SIGUSR2); //向子进程发送信号SIGUSR2
}
void TELL_CHILD(pid_t pid)
{
    kill(pid,SIGUSR1); //向父进程发送信号SIGUSR1
}
void WAIT_PARENT(void)
{
    while(sigflag == 0)
      sigsuspend(&zeromask);//将进程挂起。任意等待信号到来,信号处理程序返回
    sigflag = 0;
    sigprocmask(SIG_SETMASK,&oldmask,NULL);
}
void WAIT_CHILD(void)
{
    while(sigflag == 0)
      sigsuspend(&zeromask);  //将进程挂起。任意等待信号到来,信号处理程序返回
    sigflag = 0;
    sigprocmask(SIG_SETMASK,&oldmask,NULL);
}

int main()
{
    pid_t   pid;
    TELL_WAIT();
    pid = fork();
    switch(pid)
    {
    case -1:
        perror("fork() error");
        exit(-1);
    case 0:
        printcharacter("output from child prcess.\n");
        TELL_PARENT(getppid());
        WAIT_PARENT();
        printcharacter("output2 from child prcess.\n");
        break;
    default:
        WAIT_CHILD();
        printcharacter("output from parent prcess.\n");
        TELL_CHILD(pid);
    }
    exit(0);
}

父进程将等待子进程的信号,输出字串,随后发送子进程信号证明自己收到信号。

子进程输出字串,随后发送信号给父进程,然后等待父进程的信号后再次输出。

这样的同步方式使得字串永远按以下顺序输出不会乱序。

[root@localhost.localdomain ~]# ./signal_test 
output from child prcess.
output from parent prcess.
output2 from child prcess.

这种同步方式是很有启发性的。

例如子进程是父进程的一个阻塞操作的异步操作,父进程通过管道将处理的对象放入子进程的等待处理队列,子进程通过信号来告知父进程,已经全部处理完毕。

14 May 2015