Эквивалент Waitpid с таймаутом?
Представьте, что у меня есть процесс, который запускает несколько дочерних процессов. Родитель должен знать, когда выйдет ребенок.
Я могу использовать waitpid
, но тогда, когда/когда родитель должен выйти, я не могу сказать поток, который заблокирован в waitpid
, чтобы законно выйти и присоединиться к нему. Приятно, что вещи очищают себя, но это может быть не так уж и важно.
Я могу использовать waitpid
с WNOHANG
, а затем спать в течение некоторого произвольного времени, чтобы предотвратить ожидание ожидания. Однако тогда я могу только знать, выходил ли ребенок так часто. В моем случае это может быть не очень критично, что я знаю, когда ребенок сразу уходит, но я хотел бы знать как можно скорее...
Я могу использовать обработчик сигнала для SIGCHLD
, а в обработчике сигналов делать то, что я собирался делать, когда ребенок выходит, или отправить сообщение в другой поток, чтобы сделать какое-то действие. Но использование обработчика сигнала немного запутывает поток кода.
Что я действительно хотел бы сделать, это использовать waitpid
в некоторый тайм-аут, скажем 5 секунд. Поскольку выход из процесса не является критическим по времени операцией, я могу лениво сигнализировать о выходе потока, сохраняя при этом, что он заблокирован в waitpid
в остальное время, всегда готов реагировать. Есть ли такой вызов в Linux? Из альтернатив, какой из них лучше?
EDIT:
Другим методом, основанным на ответах, будет блокировать SIGCHLD
во всех потоках с помощью pthread
\_sigmask()
. Затем в одном потоке продолжайте звонить sigtimedwait()
, ища SIGCHLD
. Это означает, что я могу отключить этот вызов и проверить, должен ли поток выйти, а если нет, оставаться заблокированным в ожидании сигнала. Как только a SIGCHLD
доставлен в этот поток, мы можем сразу реагировать на него и в строке потока ожидания, не используя обработчик сигнала.
Ответы
Ответ 1
Функция может быть прервана сигналом, поэтому вы можете установить таймер перед вызовом waitpid(), и он выйдет с EINTR, когда сигнал таймера будет поднят. Изменить: это должно быть так же просто, как вызвать будильник (5) перед вызовом waitpid().
Ответ 2
Не смешивайте 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()
вы можете видеть, как вы можете контролировать таймаут и даже смешивать с другим IO select()
.
Ответ 3
Вставьте промежуточного ребенка, который разворачивает реального ребенка и процесс ожидания и ждет всех (обоих) его детей. Когда кто-то выйдет, он убьет другого и выйдет.
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);
Удивительно просто:)
Вы даже можете исключить промежуточного дочернего элемента, если вы уверены, что ни один другой модуль в программе не разжигает дочерние процессы.
Ответ 4
Это интересный вопрос.
Я нашел sigtimedwait.
EDIT 2016/08/29:
Спасибо за предложение Марка Эдингтона. Я проверил ваш пример на Ubuntu 16.04, он работает так, как ожидалось.
Примечание: это работает только для дочерних процессов. Жаль, что в Linux/Unix нет эквивалентного способа Window 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;
}
Ответ 5
Если вы собираетесь использовать сигналы в любом случае (согласно предложению Стива), вы можете просто отправить сигнал вручную, когда хотите выйти. Это заставит waitpid возвращать EINTR, и поток может выйти. Нет необходимости в периодическом аварийном/перезапуске.
Ответ 6
Я думал, что 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
и предотвращения прерываний от ненужных сигналов.
Ответ 7
Из-за обстоятельств я абсолютно нуждался в этом, чтобы работать в главном потоке, и было не очень просто использовать трюк с самописетом или 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);
}
EDIT: с тех пор я перешел к использованию решения, которое хорошо интегрируется с существующим eventollup на основе epoll(), используя timerfd. Я не теряю никакой независимости от платформы, так как в любом случае я использую epoll, и я получаю дополнительный сон, потому что я знаю, что нечестивая комбинация многопоточных и UNIX-сигналов больше не повредит моей программе.
Ответ 8
Я могу использовать обработчик сигналов для SIGCHLD, а в обработчике сигналов делать то, что я собираюсь делать, когда ребенок выходит, или отправить сообщение в другой поток, чтобы сделать какое-то действие. Но использование обработчика сигнала немного запутывает поток кода.
Чтобы избежать условий гонки, вам следует избегать делать что-либо более сложное, чем изменять флажок volatile в обработчике сигналов.
Я думаю, что лучший вариант в вашем случае - отправить сигнал родителям. waitpid() затем установит errno в EINTR и вернется. На этом этапе вы проверяете возвращаемое значение waitpid и errno, заметите, что вам был отправлен сигнал и предприняли соответствующие действия.
Ответ 9
Вместо прямого вызова 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.