Waitpid相当于超时?

时间:2008-11-11 21:19:59

标签: c++ c linux

想象一下,我有一个启动多个子进程的进程。父母需要知道孩子什么时候退出。

我可以使用waitpid,但是如果/当父需要退出时,我无法告诉waitpid中被阻止的线程正常退出并加入它。让事情自己清理是件好事,但这可能不是什么大不了的事。

我可以将waitpidWNOHANG一起使用,然后休眠一段时间以防止忙碌等待。然而,我只能知道一个孩子是否经常退出。在我的情况下,我知道一个孩子何时立即离开可能并不是非常关键,但我想尽快知道......

我可以使用SIGCHLD的信号处理程序,并在信号处理程序中执行当子项退出时要执行的任何操作,或者将消息发送到其他线程以执行某些操作。但是使用信号处理程序会稍微混淆代码流。

我真正想做的是在一些超时时使用waitpid,比如5秒。由于退出进程不是一个时间关键的操作,我可以懒惰地通知线程退出,同时在waitpid其余时间仍然阻塞它,随时准备做出反应。 在linux中有这样的调用吗?在其他选择中,哪一个最好?


编辑:

基于回复的另一种方法是在SIGCHLD \ pthread的所有主题中阻止_sigmask()。然后在一个帖子中,在寻找sigtimedwait()的同时继续致电SIGCHLD。这意味着我可以超时调用并检查线程是否应该退出,如果没有,则保持阻塞状态等待信号。一旦SIGCHLD被传递到该线程,我们就可以立即对它做出反应,并且在等待线程中,而不使用信号处理程序。

10 个答案:

答案 0 :(得分:41)

请勿将alarm()wait()混合使用。您可以通过这种方式丢失错误信息。

使用自管技巧。这会将任何信号转换为select()能够发生的事件:

int selfpipe[2];
void selfpipe_sigh(int n)
{
    int save_errno = errno;
    (void)write(selfpipe[1], "",1);
    errno = save_errno;
}
void selfpipe_setup(void)
{
    static struct sigaction act;
    if (pipe(selfpipe) == -1) { abort(); }

    fcntl(selfpipe[0],F_SETFL,fcntl(selfpipe[0],F_GETFL)|O_NONBLOCK);
    fcntl(selfpipe[1],F_SETFL,fcntl(selfpipe[1],F_GETFL)|O_NONBLOCK);
    memset(&act, 0, sizeof(act));
    act.sa_handler = selfpipe_sigh;
    sigaction(SIGCHLD, &act, NULL);
}

然后,类似waitpid的函数如下所示:

int selfpipe_waitpid(void)
{
    static char dummy[4096];
    fd_set rfds;
    struct timeval tv;
    int died = 0, st;

    tv.tv_sec = 5;
    tv.tv_usec = 0;
    FD_ZERO(&rfds);
    FD_SET(selfpipe[0], &rfds);
    if (select(selfpipe[0]+1, &rfds, NULL, NULL, &tv) > 0) {
       while (read(selfpipe[0],dummy,sizeof(dummy)) > 0);
       while (waitpid(-1, &st, WNOHANG) != -1) died++;
    }
    return died;
}

您可以在selfpipe_waitpid()中看到如何控制超时,甚至可以与其他基于select()的IO混合使用。

答案 1 :(得分:29)

分叉一个中间子,它分叉真正的子进程和超时进程并等待其所有(两个)子进程。当一个人退出时,它会杀死另一个并退出。

pid_t intermediate_pid = fork();
if (intermediate_pid == 0) {
    pid_t worker_pid = fork();
    if (worker_pid == 0) {
        do_work();
        _exit(0);
    }

    pid_t timeout_pid = fork();
    if (timeout_pid == 0) {
        sleep(timeout_time);
        _exit(0);
    }

    pid_t exited_pid = wait(NULL);
    if (exited_pid == worker_pid) {
        kill(timeout_pid, SIGKILL);
    } else {
        kill(worker_pid, SIGKILL); // Or something less violent if you prefer
    }
    wait(NULL); // Collect the other process
    _exit(0); // Or some more informative status
}
waitpid(intermediate_pid, 0, 0);

非常简单:)

如果您确定程序中没有其他模块正在自行填充子进程,您甚至可以省略中间孩子。

答案 2 :(得分:15)

这是一个有趣的问题。 我发现sigtimedwait可以做到。

编辑2016/08/29: 感谢Mark Edington的建议。我在Ubuntu 16.04上测试了你的例子,它按预期工作。

注意:这仅适用于子进程。遗憾的是,Linux / Unix中似乎没有Window的WaitForSingleObject(unrelated_process_handle,timeout)的等效方式,以便在超时时通知无关的进程终止。

好的,Mark Edington的示例代码是here

/* The program creates a child process and waits for it to finish. If a timeout
 * elapses the child is killed. Waiting is done using sigtimedwait(). Race
 * condition is avoided by blocking the SIGCHLD signal before fork().
 */
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

static pid_t fork_child (void)
{
    int p = fork ();

    if (p == -1) {
        perror ("fork");
        exit (1);
    }

    if (p == 0) {
        puts ("child: sleeping...");
        sleep (10);
        puts ("child: exiting");
        exit (0);
    }

    return p;
}

int main (int argc, char *argv[])
{
    sigset_t mask;
    sigset_t orig_mask;
    struct timespec timeout;
    pid_t pid;

    sigemptyset (&mask);
    sigaddset (&mask, SIGCHLD);

    if (sigprocmask(SIG_BLOCK, &mask, &orig_mask) < 0) {
        perror ("sigprocmask");
        return 1;
    }

    pid = fork_child ();

    timeout.tv_sec = 5;
    timeout.tv_nsec = 0;

    do {
        if (sigtimedwait(&mask, NULL, &timeout) < 0) {
            if (errno == EINTR) {
                /* Interrupted by a signal other than SIGCHLD. */
                continue;
            }
            else if (errno == EAGAIN) {
                printf ("Timeout, killing child\n");
                kill (pid, SIGKILL);
            }
            else {
                perror ("sigtimedwait");
                return 1;
            }
        }

        break;
    } while (1);

    if (waitpid(pid, NULL, 0) < 0) {
        perror ("waitpid");
        return 1;
    }

    return 0;
}

答案 3 :(得分:6)

该功能可以通过信号中断,因此您可以在调用waitpid()之前设置定时器,并在定时器信号被提升时以EINTR退出。编辑:它应该像调用waitpid()之前调用alarm(5)一样简单。

答案 4 :(得分:2)

如果您要使用信号(根据Steve的建议),您可以在想要退出时手动发送信号。这将导致waitpid返回EINTR,然后线程可以退出。无需定期报警/重启。

答案 5 :(得分:2)

我认为select会在孩子发出EINTR信号时返回SIGCHLD。 我相信这应该有效:

while(1)
{
  int retval = select(0, NULL, NULL, NULL, &tv, &mask);
  if (retval == -1 && errno == EINTR) // some signal
  { 
      pid_t pid = (waitpid(-1, &st, WNOHANG) == 0);
      if (pid != 0) // some child signaled
  }
  else if (retval == 0)
  {
      // timeout
      break;
  }
  else // error
}

注意:您可以使用pselect覆盖当前sigmask并避免中断不需要的信号。

答案 6 :(得分:2)

由于情况,我绝对需要在主线程中运行,并且使用自管道技巧或eventfd并不是很简单,因为我的epoll循环在另一个线程中运行。所以我通过将其他堆栈溢出处理程序整理在一起来想出这个。请注意,通常以其他方式执行此操作会更安全,但这很简单。如果有人关心它是如何真的非常糟糕那么我就是全部耳朵。

注意:绝对有必要阻止任何线程中的信号处理,除了你要运行它的那个。我默认这样做,因为我认为处理随机线程中的信号很麻烦

static void ctlWaitPidTimeout(pid_t child, useconds_t usec, int *timedOut) {
    int rc = -1;

    static pthread_mutex_t alarmMutex = PTHREAD_MUTEX_INITIALIZER;

    TRACE("ctlWaitPidTimeout: waiting on %lu\n", (unsigned long) child);

    /**
     * paranoid, in case this was called twice in a row by different
     * threads, which could quickly turn very messy.
     */
    pthread_mutex_lock(&alarmMutex);

    /* set the alarm handler */
    struct sigaction alarmSigaction;
    struct sigaction oldSigaction;

    sigemptyset(&alarmSigaction.sa_mask);
    alarmSigaction.sa_flags   = 0;
    alarmSigaction.sa_handler = ctlAlarmSignalHandler;
    sigaction(SIGALRM, &alarmSigaction, &oldSigaction);

    /* set alarm, because no alarm is fired when the first argument is 0, 1 is used instead */
    ualarm((usec == 0) ? 1 : usec, 0);

    /* wait for the child we just killed */
    rc = waitpid(child, NULL, 0);

    /* if errno == EINTR, the alarm went off, set timedOut to true */
    *timedOut = (rc == -1 && errno == EINTR);

    /* in case we did not time out, unset the current alarm so it doesn't bother us later */
    ualarm(0, 0);

    /* restore old signal action */
    sigaction(SIGALRM, &oldSigaction, NULL);

    pthread_mutex_unlock(&alarmMutex);

    TRACE("ctlWaitPidTimeout: timeout wait done, rc = %d, error = '%s'\n", rc, (rc == -1) ? strerror(errno) : "none");
}

static void ctlAlarmSignalHandler(int s) {
    TRACE("ctlAlarmSignalHandler: alarm occured, %d\n", s);
}

编辑:我已经过渡到使用一个与我现有的基于epoll()的eventloop很好地集成的解决方案,使用timerfd。因为我正在使用epoll,所以我并没有真正失去任何平台独立性,而且我获得了额外的睡眠,因为我知道多线程和UNIX信号的不合理组合不会再次伤害我的程序。

答案 7 :(得分:1)

  

我可以为SIGCHLD使用信号处理程序,并在信号处理程序中执行当子项退出时要执行的任何操作,或者将消息发送到其他线程以执行某些操作。但是使用信号处理程序会稍微混淆代码流。

为了避免竞争条件,你应该避免做一些比改变信号处理程序中的volatile标志更复杂的事情。

我认为您的最佳选择是向父母发送信号。 waitpid()然后将errno设置为EINTR并返回。此时,您检查waitpid返回值和errno,注意您已收到信号并采取适当的措施。

答案 8 :(得分:0)

您可以直接使用SIGCHLD调用sigtimedwait(),而不是直接调用waitpid(),它会在子项退出后发送到父进程),然后等待它被传递到当前线程,就像函数名建议的那样:支持timeout参数。

请检查以下代码段以获取详细信息


static bool waitpid_with_timeout(pid_t pid, int timeout_ms, int* status) {
    sigset_t child_mask, old_mask;
    sigemptyset(&child_mask);
    sigaddset(&child_mask, SIGCHLD);

    if (sigprocmask(SIG_BLOCK, &child_mask, &old_mask) == -1) {
        printf("*** sigprocmask failed: %s\n", strerror(errno));
        return false;
    }

    timespec ts;
    ts.tv_sec = MSEC_TO_SEC(timeout_ms);
    ts.tv_nsec = (timeout_ms % 1000) * 1000000;
    int ret = TEMP_FAILURE_RETRY(sigtimedwait(&child_mask, NULL, &ts));
    int saved_errno = errno;

    // Set the signals back the way they were.
    if (sigprocmask(SIG_SETMASK, &old_mask, NULL) == -1) {
        printf("*** sigprocmask failed: %s\n", strerror(errno));
        if (ret == 0) {
            return false;
        }
    }
    if (ret == -1) {
        errno = saved_errno;
        if (errno == EAGAIN) {
            errno = ETIMEDOUT;
        } else {
            printf("*** sigtimedwait failed: %s\n", strerror(errno));
        }
        return false;
    }

    pid_t child_pid = waitpid(pid, status, WNOHANG);
    if (child_pid != pid) {
        if (child_pid != -1) {
            printf("*** Waiting for pid %d, got pid %d instead\n", pid, child_pid);
        } else {
            printf("*** waitpid failed: %s\n", strerror(errno));
        }
        return false;
    }
    return true;
}

引用:https://android.googlesource.com/platform/frameworks/native/+/master/cmds/dumpstate/DumpstateUtil.cpp#46

答案 9 :(得分:0)

如果您的程序仅在现代Linux内核(5.3或更高版本)上运行,则首选方法是使用pidfd_openhttps://lwn.net/Articles/789023/ https://man7.org/linux/man-pages/man2/pidfd_open.2.html)。

此系统调用返回代表进程的文件描述符,然后您可以selectpollepoll进行操作,就像等待其他类型的文件描述符一样。< / p>

例如,

int fd = pidfd_open(pid, 0);
struct pollfd pfd = {fd, POLLIN, 0};
poll(&pfd, 1, 1000) == 1;