【问题标题】:How to properly count an actual number of forked child processes?如何正确计算分叉子进程的实际数量?
【发布时间】:2014-01-03 15:20:06
【问题描述】:

前段时间,我为自动 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() 等),但有更好的错误处理——所以我不必检查它们的返回状态。

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

【问题讨论】:

  • 考虑让您的代码定期调用 sig_chld(),例如在线程中。而不是在信号处理函数中。当出现大量信号时,像您这样的信号处理程序可能无法正确完成。这似乎是你的问题。
  • fork 失败时,您的Fork 函数会做什么?
  • 它打印一条错误消息并退出调用exit(1)

标签: c linux signals fork waitpid


【解决方案1】:

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

有几个问题:

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

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

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

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

如果您因为信号中断其他信号而遇到问题,这就是sigactionsa_mask 字段的用途。

【讨论】:

  • 是的,操纵信号掩码的方法效果很好!而且要简单得多。谢谢你的回答。
【解决方案2】:

我会自己回答。

不以这种方式计算子进程有几个原因。首先,信号处理程序可能被另一个信号中断。我找不到任何信息,当这种情况发生时实际发生了什么。在 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()的手册页上:

异步信号安全

互斥函数不是异步信号安全的。这意味着 不应从信号处理程序调用它们。特别是,调用 来自信号处理程序的 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;
}

【讨论】:

  • 请注意,smime_gate_service() 只能使用异步信号安全功能,例如 execve(),如果您这样做的话。这是因为fork()只复制新进程中的调用线程,而其他线程可能拥有锁(包括系统内部使用的锁)。
  • 感谢您的评论,我没想到分叉和线程如何干扰。但这不是问题——smime_gate_service() 是单线程的,不执行分叉,也没有任何信号处理。没有比您在这段代码摘录中看到的更多的线程和分叉。最好只复制主线程——因为这是我唯一需要的。
  • 仍然可能存在问题,例如,如果在子进程保护线程位于err_msg() 时发生fork,并且子进程稍后也使用err_msg()。数据结构可能不一致,或者您可能正在等待子进程中不存在的线程解锁某些东西(特别是 stdio FILE 对象被指定为锁定)。
  • 啊,现在我明白了,但那是一种可怕的景象——更糟糕的是,它是真实的!你认为扩大关键部分就足够了吗? (main() 内部有fork()child_process_guard() 内部有err_msg()waitpid()usleep() 对多线程分叉安全吗?
猜你喜欢
  • 2013-07-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-12-07
  • 1970-01-01
  • 1970-01-01
  • 2021-03-12
  • 1970-01-01
相关资源
最近更新 更多