如何正确计算分叉子进程的实际数量?

时间:2014-01-03 15:20:06

标签: c linux signals fork waitpid

前段时间我为自动S / MIME处理编写了一个简单的SMTP门,现在又进行了测试。对于邮件服务器而言,主进程会为每个传入连接分配一个子。限制创建的子进程的数量是一种很好的做法 - 所以我就这样做了。

在重负载(同时来自许多客户端的许多连接)期间,似乎子进程未正确计数 - 问题在于当子进程退出时减少计数器。几分钟的重负载计数器大于实际的子进程数(即5分钟后它等于14,但没有)。

我已经做了一些研究,但没有任何效果。所有僵尸进程都被收获,因此SIGCHLD处理似乎没问题。我认为这可能是一个同步问题,但添加互斥锁并将变量类型更改为volatile sig_atomic_t(就像现在一样)不会给出任何更改。这也不是信号屏蔽的问题,我尝试使用sigfillset(&act.sa_mask)屏蔽所有信号。

我注意到waitpid()有时会返回奇怪的PID值(非常大,如172915914)。

问题和一些代码。

  1. 其他流程(即init)是否有可能收获其中一些流程?
  2. 退出后进程不能成为僵尸吗?可以自动收回吗?
  3. 如何解决?也许有更好的方法来计算它们?
  4. main()中分拣孩子:

    volatile sig_atomic_t sproc_counter = 0;    /* forked subprocesses counter */
    
    /* S/MIME Gate main function */
    int main (int argc, char **argv)
    {
        [...]
    
        /* set appropriate handler for SIGCHLD */
        Signal(SIGCHLD, sig_chld);
    
        [...]
    
        /* SMTP Server's main loop */
        for (;;) {
    
            [...]
    
            /* check whether subprocesses limit is not exceeded  */
            if (sproc_counter < MAXSUBPROC) {
                if ( (childpid = Fork()) == 0) {    /* child process */
                    Close(listenfd);                /* close listening socket */
                    smime_gate_service(connfd);     /* process the request */
                    exit(0);
                }
                ++sproc_counter;
            }
            else
                err_msg("subprocesses limit exceeded, connection refused");
    
            [...]
        }
        Close(connfd);  /* parent closes connected socket */
    }
    

    信号处理:

    Sigfunc *signal (int signo, Sigfunc *func)
    {
        struct sigaction    act, oact;
    
        act.sa_handler = func;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
    
        if (signo == SIGALRM) {
    #ifdef  SA_INTERRUPT
            act.sa_flags |= SA_INTERRUPT;   /* SunOS 4.x */
    #endif
        }
        else {
    #ifdef  SA_RESTART
            act.sa_flags |= SA_RESTART;     /* SVR4, 44BSD */
    #endif
        }
        if (sigaction(signo, &act, &oact) < 0)
            return SIG_ERR;
    
        return oact.sa_handler;
    }
    
    Sigfunc *Signal (int signo, Sigfunc *func)
    {
        Sigfunc *sigfunc;
    
        if ( (sigfunc = signal(signo, func)) == SIG_ERR)
            err_sys("signal error");
        return sigfunc;
    }
    
    void sig_chld (int signo __attribute__((__unused__)))
    {
        pid_t pid;
        int stat;
    
        while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0) {
            --sproc_counter;
            err_msg("child %d terminated", pid);
        }
        return;
    }
    

    注意:所有以大写字母开头的函数(例如Fork()Close()Signal()等)的行为与小写字母相同朋友(fork()close()signal()等),但有更好的错误处理方式 - 因此我无需检查其返回状态。

    NOTE2 :我使用kernel v3.10.11在Debian Testing(gcc 4.8.2)下运行并编译它。

2 个答案:

答案 0 :(得分:0)

我会自己回答。

有几个理由不以这种方式计算子进程。首先,信号处理程序可能被另一个信号中断。我无法找到任何信息,这种情况发生时会发生什么。在libc手册页和this answer中有一些关于它的信息。但这可能不是问题。

volatile sig_atomic_t变量上的操作似乎不是真正的原子,它依赖于系统架构。例如,在 amd64 上,递减sproc_counter值的编译代码如下所示:

movl    sproc_counter(%rip), %eax
subl    $1, %eax
movl    %eax, sproc_counter(%rip)

正如您所看到的,有多达三个汇编指令!它绝对不是原子的,因此必须同步对sproc_counter的访问。

好的,但为什么添加互斥锁没有给出结果?答案在pthread_mutex_lock() / pthread_mutex_unlock()

的手册页上
  

ASYNC-SIGNAL SAFETY

     

互斥锁功能不是异步信号安全的。这意味着什么   不应该从信号处理程序中调用它们。特别是打电话   来自信号处理程序的pthread_mutex_lock或pthread_mutex_unlock可以   死锁调用线程。

这说清楚了。什么是更多的调用函数,打印日期(日志消息)也是一个坏主意 - 在那里使用fputs()不是异步信号安全。

如何正确完成?

考虑到信号处理过程中可能发生的事情(即传递其他信号),很明显信号处理程序应该尽可能简洁。 set a flag in handler要好一些,并在主程序或专用线程中不时进行测试。我选择了第二种解决方案。

没有更多的话,让我们看一些代码。

信号处理将是这样的:

void sig_chld (int signo __attribute__((__unused__)))
{
  sigchld_notify = 1;
}

main()例程:

volatile sig_atomic_t sigchld_notify = 0;                /* SIGCHLD notifier */
int sproc_counter = 0;                                   /* forked child process counter */
pthread_mutex_t sproc_mutex = PTHREAD_MUTEX_INITIALIZER; /* mutex for child process counter */

/* S/MIME Gate main function */
int main (int argc, char **argv)
{
    pthread_t guard_id;
    [...]

    /* start child process guard */
    if (0 != pthread_create(&guard_id, NULL, child_process_guard, NULL) )
        err_sys("pthread_create error");

    [...]

    /* SMTP Server's main loop */
    for (;;) {
        [...]

        /* check whether child processes limit is not exceeded */
        if (sproc_counter < MAXSUBPROC) {
            if ( (childpid = Fork()) == 0) { /* child process */
                Close(listenfd);             /* close listening socket */
                smime_gate_service(connfd);  /* process the request */
                exit(0);
            }
            pthread_mutex_lock(&sproc_mutex);
            ++sproc_counter;
            pthread_mutex_unlock(&sproc_mutex);
        }
        else
            err_msg("subprocesses limit exceeded, connection refused");

        Close(connfd); /* parent closes connected socket */
    }
} /* end of main() */

保护线程例程:

extern volatile sig_atomic_t sigchld_notify; /* SIGCHLD notifier */
extern int sproc_counter;                    /* forked child process counter */
extern pthread_mutex_t sproc_mutex;          /* mutex for child process counter */

void* child_process_guard (void* arg __attribute__((__unused__)))
{
    pid_t pid;
    int stat;

    for (;;) {
        if (0 == sigchld_notify) {
            usleep(SIGCHLD_SLEEP);
            continue;
        }

        while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0) {
            pthread_mutex_lock(&sproc_mutex);
            --sproc_counter;
            pthread_mutex_unlock(&sproc_mutex);
            err_msg("child %d terminated", pid);
        }
        sigchld_notify = 0;
    }
    return NULL;
}

答案 1 :(得分:0)

我认为信号方法可以修复,而创建一个线程会强迫你执行一个程序来处理连接。

有几个问题:

  • 如果创建流程并同时结束,则sproc_counter的更改可能会丢失。要解决此问题,请使用信号掩码(例如sigprocmask()pselect())以确保在主流操作sproc_counter时不调用处理程序,或使信号处理程序设置标志并执行waitpid(),计数器操作和登录主流程(但不在新线程中)。请注意,如果要在结束连接后直接避免为新连接或其他结束连接休眠,则flag方法仍需要信号掩码操作。

  • err_msg()可能不是异步信号安全的。我看到三个选项:

    • 使用上面提到的标志方法,或
    • 确保在SIGCHLD取消屏蔽时调用异步信号不安全函数,或
    • 从信号处理程序中删除调用。
  • 覆盖signal()可能会导致其他代码调用您的版本而不是标准版本。这可能会导致奇怪的行为。

  • 信号处理程序不保存和恢复errno的值。

如果由于信号中断其他信号而出现问题,那就是sigaction sa_mask字段的用途。