Эквивалент ожидания с тайм-аутом? - PullRequest
47 голосов
/ 12 ноября 2008

Представьте, что у меня есть процесс, который запускает несколько дочерних процессов. Родитель должен знать, когда ребенок выходит.

Я могу использовать waitpid, но тогда, если / когда родитель должен выйти, я не могу сказать потоку, который заблокирован в waitpid, чтобы выйти изящно и присоединиться к нему. Приятно, чтобы вещи сами убирались, но это может быть не так уж важно.

Я могу использовать waitpid с WNOHANG, а затем поспать в течение некоторого произвольного времени, чтобы предотвратить занятое ожидание. Однако тогда я могу только знать, выходил ли ребенок так часто. В моем случае, может быть, не очень важно, чтобы я знал, когда ребенок сразу же выходит, но я хотел бы знать как можно скорее ...

Я могу использовать обработчик сигнала для SIGCHLD, а в обработчике сигнала делать то, что я собирался делать при выходе из дочернего процесса, или отправлять сообщение в другой поток, чтобы выполнить какое-либо действие. Но использование обработчика сигнала немного запутывает поток кода.

Что я действительно хотел бы сделать, так это использовать waitpid в течение некоторого времени ожидания, скажем, 5 секунд. Поскольку выход из процесса не является критичной ко времени операцией, я могу лениво дать сигнал потоку на выход, в то же время оставляя его заблокированным в waitpid, всегда готовым реагировать. Есть ли такой вызов в Linux? Какой из вариантов лучше?


EDIT:

Другой метод, основанный на ответах, заключается в блокировке SIGCHLD во всех потоках с pthread \ _sigmask(). Затем в одном потоке продолжайте звонить sigtimedwait(), ища SIGCHLD. Это означает, что я могу установить тайм-аут на этот вызов и проверить, должен ли поток завершиться и, если нет, остаться заблокированным в ожидании сигнала. Как только SIGCHLD доставляется в этот поток, мы можем немедленно реагировать на него и в очереди потока ожидания, не используя обработчик сигнала.

Ответы [ 8 ]

41 голосов
/ 14 ноября 2008

Не смешивайте 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.

29 голосов
/ 05 ноября 2011

Форка промежуточного потомка, который разветвляет реального потомка и процесс тайм-аута и ожидает всех (обоих) своих потомков. Когда один выходит, он убивает другого и выходит.

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);

Удивительно просто:)

Вы даже можете опустить промежуточный дочерний элемент, если вы уверены, что ни один другой модуль в программе не блокирует собственные дочерние процессы.

15 голосов
/ 24 ноября 2013

Это интересный вопрос. Я обнаружил, что sigtimedwait может это сделать.

РЕДАКТИРОВАТЬ 2016/08/29: Спасибо за предложение Марка Эдингтона. Я протестировал ваш пример на Ubuntu 16.04, он работает как положено.

Примечание: это работает только для дочерних процессов. Жаль, что в Linux / Unix не существует эквивалентного способа окна WaitForSingleObject (unrelated_process_handle, timeout) для получения уведомлений о завершении несвязанного процесса.

ОК, пример кода Марка Эдингтона: здесь :

/* 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;
}
6 голосов
/ 12 ноября 2008

Функция может быть прервана сигналом, поэтому вы можете установить таймер перед вызовом waitpid (), и он выйдет с EINTR, когда сигнал таймера будет повышен. Изменить: Это должно быть так же просто, как вызов будильника (5) перед вызовом waitpid ().

2 голосов
/ 18 апреля 2013

В силу обстоятельств мне абсолютно необходимо, чтобы это выполнялось в главном потоке, и использовать трюк self-pipe или 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 больше не повредит моей программе.

2 голосов
/ 19 октября 2011

Я думал, что 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 и избежать прерываний от ненужных сигналов.

2 голосов
/ 12 ноября 2008

Если вы все равно собираетесь использовать сигналы (согласно предложению Стива), вы можете просто отправить сигнал вручную, когда захотите выйти. Это приведет к тому, что waitpid вернет EINTR, и поток может выйти. Нет необходимости в периодической сигнализации / перезапуске.

1 голос
/ 12 ноября 2008

Я могу использовать обработчик сигнала для SIGCHLD, а в обработчике сигнала делать то, что я собирался делать, когда выходит ребенок, или отправлять сообщение в другой поток, чтобы выполнить какое-либо действие. Но использование обработчика сигнала немного запутывает поток кода.

Чтобы избежать состояния гонки, вы должны избегать более сложных действий, чем изменение флага энергозависимости в обработчике сигналов.

Я думаю, что лучший вариант в вашем случае - отправить сигнал родителю. waitpid () установит для errno значение EINTR и вернется. В этот момент вы проверяете возвращаемое значение waitpid и ошибаетесь, замечаете, что вам отправлен сигнал, и предпринимаете соответствующие действия.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...