1

前段时间我为自动 S/MIME 处理写了一个简单的 SMTP 门,现在它来测试。与邮件服务器一样,主进程为每个传入连接创建一个子进程。限制创建的子进程的数量是一个很好的做法——所以我做到了。

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

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

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

问题和一些代码。

  1. 是否有可能其他进程(即。init)正在收获其中的一些?
  2. 进程退出后不能变成僵尸吗?可以自动收割吗?
  3. 如何解决?也许有更好的方法来计算它们?

分叉一个孩子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()更好的错误处理能力——所以我不必检查它们的返回状态。

注意 2:我在 Debian Testing( kernel v3.10.11) 下使用gcc 4.8.2.

4

2 回答 2

0

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

有几个问题:

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

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

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

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

如果您因为信号中断其他信号而遇到问题,这就是sigaction's sa_maskfield 的用途。

于 2014-01-10T23:03:07.810 回答
0

我会自己回答。

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

似乎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()

异步信号安全

互斥锁函数不是异步信号安全的。这意味着 不应从信号处理程序调用它们。特别是,从信号处理程序调用 pthread_mutex_lock 或 pthread_mutex_unlock 可能会使调用线程死锁。

这很清楚。更重要的是调用函数,打印日期(日志消息)也是一个坏主意——使用fputs()没有异步信号安全。

如何正确执行?

考虑到在信号处理过程中会发生什么(即传递其他信号),很明显信号处理例程应该尽可能简洁。最好在处理程序中设置一个标志,并在主程序或专用线程中不时对其进行测试。我选择第二种解决方案。

废话不多说,来看一段代码。

信号处理看起来是这样的:

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;
}
于 2014-01-10T18:19:35.373 回答