Эквивалент Waitpid с тайм-аутом?

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

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

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

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

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


Обновлено:

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

С 2008 года произошло много событий, и стоит знать о том, что теперь доступна возможность signalfd (). Это можно использовать с poll() или select(). Только берегитесь этого: stackoverflow.com/questions/8398298/handling-sigchld

Alexis Wilke 26.06.2016 19:06
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
51
1
31 543
10
Перейти к ответу Данный вопрос помечен как решенный

Ответы 10

Ответ принят как подходящий

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

Что определяет, какой поток обрабатывает сигнал? Как я могу быть уверен, что это поток, который этим занимается? Это сигнал тревоги был вызван в каком-то потоке, так что поток обрабатывает сигнал?

Greg Rogers 12.11.2008 00:39

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

Greg Rogers 12.11.2008 00:44

Вероятно, неплохо иметь только один поток, который получает сигналы, гарантируя, что все другие потоки маскируют сигнал с помощью sigprocmask или аналогичного

MarkR 12.11.2008 01:16

примечание для всех, кто читает приведенный выше комментарий: используйте pthread_sigmask, а не sigprocmask

Greg Rogers 12.11.2008 01:41

На самом деле не делай этого. Вы можете потерять потомков, если waitpid () получит потомок, но SIGALRM сработает до того, как ядро ​​вернется. Во многих unix-версиях также есть ошибки, и даже в идеальном случае EINTR не работает правильно.

geocar 12.12.2008 05:52

Это настолько проще, что это должен быть правильный способ сделать это. Если ваша ОС сломана, исправьте ее или получите лучшую.

Sam Watkins 30.08.2016 05:42

I can use a signal handler for SIGCHLD, and in the signal handler do whatever I was going to do when a child exits, or send a message to a different thread to do some action. But using a signal handler obfuscates the flow of the code a little bit.

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

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

Что ж, вы можете проделать трюк с самотеком, и вместо этого waitpid-thread действительно будет блокировать выбор канала. Затем, когда он получит SIGCHLD, пусть он запишет байт в канал, который проснется.

wnoise 12.11.2008 19:43

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

Не смешивайте alarm() с wait(). Таким образом вы можете потерять информацию об ошибке.

Используйте трюк с самотрубкой. Это превращает любой сигнал в событие select()able:

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().

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

Evan Teran 11.12.2008 06:53

Если двое детей умрут, вы не обязательно получите два уведомления SIGCHLD. Вы делаете канал неблокирующим на случай, если поступит слишком много SIGCHLD (примерно PIPE_BUF).

geocar 12.12.2008 05:46

Циклы также помогают защитить от слишком большого количества SIGCHLD, и хотя в идеале данные всегда будут после завершения выбора, read () будет блокироваться до тех пор, пока sizeof (фиктивные) байты не будут заполнены, если он не помечен как неблокирующий для чтения.

geocar 12.12.2008 05:49

Я собирался обвинить ваш выбор неблокирующего режима, так как это означает, что если будет больше, чем PIPE_BUF SIGCHLD, некоторые из их write() будут потеряны, но теперь я вижу, что вы на самом деле не заботитесь о том, чтобы читать точно правильное количество байтов обратно из этой трубы. Хороший!

j_random_hacker 06.09.2012 19:10

Но мне интересно: зачем включать бездействующий act.sa_flags |= 0;?

j_random_hacker 06.09.2012 19:11

@j_random_hacker - наверное, это опечатка.

geocar 08.09.2012 03:41

Поскольку (я так понимаю) вы автор, как вы думаете, что это должно было быть вместо этого? Не могли бы вы вынуть его, если не понимаете, для чего он нужен? Это просто сбивает с толку банкомат. Спасибо!

j_random_hacker 08.09.2012 05:53

Разве signalfd(2) не был бы правильным способом «превратить любой сигнал в событие select()able»?

cibyr 28.06.2013 09:54

В моем быстром эксперименте select() возвращает -1 с errno == EINTR. Разве мы не должны ожидать такого поведения? Тогда как в вашем коде мы дойдем до waitpid?

Jonathon Reinhart 03.10.2013 07:48

@JonathonReinhart - Вы должны использовать SA_RESTART, если можете, но в противном случае вы можете перезапустить select() самостоятельно.

geocar 25.10.2013 19:30

В общем, вы можете захотеть сохранить и восстановить errno в обработчике сигналов, чтобы предотвратить его перезапись.

Vegard 27.10.2014 13:42

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

gps 29.12.2017 00:17

что, если вы хотите дождаться процесса, который не является дочерним?

Joseph Garvin 10.09.2019 20:13

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

Это довольно хорошо, только вам нужно замаскировать сигнал, когда вы не находитесь в вызове select(), и это сложно (у вас будет состояние гонки между вызовами unmask + select).

Alexis Wilke 26.06.2016 19:02

@AlexisWilke, правда. signalfd - лучшая альтернатива Linux. Использование glib может упростить решение, предложенное @geocar. И подход этого ответа может быть улучшен с использованием sigtimedwait, хотя вам все равно нужно замаскировать сигналы, чтобы мы не пропустили их. Думаю, в случае signalfd вы должны создать его до создания потомков.

ony 01.07.2016 10:23

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

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

На удивление просто :)

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

Не уверен, буду ли я его использовать (у меня точно такая же проблема, что и у OP), но, блин, это очень крутой трюк! Престижность (просто интересно, почему за него не проголосовали больше)

Aktau 18.04.2013 12:14

Есть ли способ сделать функцию do_work вызовом system ()? Мне нужны тонкости того, что может предложить оболочка (глобализация, конвейерная обработка), но вызов system () заставляет ее продолжать работу, если рабочая вилка будет убита.

Brian Schlenker 03.12.2013 21:58

Может показаться, что это не гарантирует соблюдения тайм-аута, поскольку последний измеряется с момента запуска процесса timeout_pid (). Однако задержка между вызовом timeout_pid = fork () и фактическим запуском процесса timeout_pid произвольна.

user1284631 07.08.2014 17:24

Разве все планирование ЦП, строго говоря, не произвольно в большинстве операционных систем (не в реальном времени)? То есть у вас может быть произвольно длительная задержка планирования сразу после запуска вашего воркера, независимо от того, какой механизм тайм-аута вы используете. Тем не менее, это решение действительно может быть несколько менее точным, чем многие другие.

mpartel 08.08.2014 00:30

При использовании signal(SIGCHLD, SIG_IGN); при таком подходе возникает экзотическая ситуация, которая может вызвать проблемы. Подробнее см. здесь.

DrP3pp3r 25.11.2014 17:34

В силу обстоятельств мне абсолютно необходимо, чтобы это выполнялось в основном потоке, и было не очень просто использовать трюк 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 (), используя timerfd. Я действительно не теряю никакой независимости от платформы, поскольку я все равно использовал epoll, и я получаю дополнительный сон, потому что я знаю, что нечестивая комбинация многопоточности и сигналов UNIX не повредит моей программе снова.

Не могли бы вы уделить время опубликованию окончательного решения - решения epoll?

Alex 03.06.2019 13:30

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

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

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

Хорошо, пример кода Марка Эдингтона - здесь:

/* 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;
}

Как никто другой не может оценить этот ответ? :)

netigger 13.03.2015 17:39

Это был бы лучший ответ с более подробной информацией и примером кода. Я использовал этот метод. Вот сообщение в блоге с хорошим примером: linuxprogrammingblog.com/code-examples/…

Mark Edington 26.08.2016 04:50

@Mark Edington Спасибо за ваше внимание. Я обновил ответ, упомянув ваш пример.

osexp2003 29.08.2016 04:25

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

David Roundy 15.08.2018 21:37

@DavidRoundy спасибо, что поделились. Я не тестировал несколько детей, будет ли сигнализироваться сигнализация выхода любого из детей? Если это так, то можно использовать waitpid, чтобы узнать, какой дочерний элемент завершен.

osexp2003 17.08.2018 07:20

@ osexp2003, пожалуйста, взгляните на stackoverflow.com/questions/64369771/…, я пробовал, но он не работает

paramikoooo 15.10.2020 14:54

Вместо прямого вызова waitpid () вы можете вызвать sigtimedwait () с помощью SIGCHLD (который будет отправлен родительскому процессу после выхода дочернего процесса) и дождаться, когда он будет доставлен в текущий поток, как указано в названии функции, параметр тайм-аута поддерживается.

пожалуйста, проверьте следующий фрагмент кода для подробностей


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

что, если вы хотите сделать это для произвольного pid, который не является дочерним?

Joseph Garvin 10.09.2019 20:12

Если ваша программа работает только на современных ядрах Linux (5.3 или новее), предпочтительным способом является использование pidfd_open (https://lwn.net/Articles/789023/https://man7.org/linux/man-pages/man2/pidfd_open.2.html).

Этот системный вызов возвращает файловый дескриптор, представляющий процесс, и затем вы можете использовать select, poll или epoll, точно так же, как вы ожидаете от других типов файловых дескрипторов.

Например,

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

Другие вопросы по теме