Представьте, что у меня есть процесс, запускающий несколько дочерних процессов. Родитель должен знать, когда ребенок уходит.
Я могу использовать waitpid, но тогда, если / когда родителю нужно выйти, я не могу сказать потоку, который заблокирован в waitpid, изящно выйти и присоединиться к нему. Приятно, когда все происходит само по себе, но это может не иметь большого значения.
Я могу использовать waitpid с WNOHANG, а затем спать в течение некоторого произвольного времени, чтобы предотвратить ожидание занятости. Однако тогда я могу только знать, выходил ли ребенок время от времени. В моем случае это может быть не очень важно, чтобы я знал, когда ребенок сразу уходит, но я хотел бы знать как можно скорее ...
Я могу использовать обработчик сигнала для SIGCHLD, и в обработчике сигнала делать то, что я собирался сделать, когда дочерний элемент выходит, или отправить сообщение в другой поток, чтобы выполнить какое-то действие. Но использование обработчика сигналов немного запутывает поток кода.
Что я действительно хотел бы сделать, так это использовать waitpid на некотором таймауте, скажем, 5 секунд. Поскольку выход из процесса не является критичной по времени операцией, я могу лениво сигнализировать потоку о выходе, в то время как все остальное время он блокируется в waitpid, всегда готовый отреагировать. В линуксе такой вызов есть? Какой из вариантов лучше?
Обновлено:
Другой метод, основанный на ответах, - заблокировать SIGCHLD во всех потоках с pthread \ _sigmask(). Затем в одном потоке продолжайте звонить sigtimedwait(), пока ищите SIGCHLD. Это означает, что я могу тайм-аут для этого вызова и проверить, должен ли поток выйти, а если нет, оставаться заблокированным в ожидании сигнала. Как только SIGCHLD доставлен в этот поток, мы можем немедленно отреагировать на него в очереди потока ожидания, без использования обработчика сигнала.





Функция может быть прервана сигналом, поэтому вы можете установить таймер перед вызовом waitpid (), и он выйдет с EINTR при срабатывании сигнала таймера. Обновлено: это должно быть так же просто, как вызов alarm (5) перед вызовом waitpid ().
Что определяет, какой поток обрабатывает сигнал? Как я могу быть уверен, что это поток, который этим занимается? Это сигнал тревоги был вызван в каком-то потоке, так что поток обрабатывает сигнал?
На странице руководства для signal, кажется, говорится, что результат не указан, что означает, что он не может быть обработан правильным потоком и приведет к неправильным результатам.
Вероятно, неплохо иметь только один поток, который получает сигналы, гарантируя, что все другие потоки маскируют сигнал с помощью sigprocmask или аналогичного
примечание для всех, кто читает приведенный выше комментарий: используйте pthread_sigmask, а не sigprocmask
На самом деле не делай этого. Вы можете потерять потомков, если waitpid () получит потомок, но SIGALRM сработает до того, как ядро вернется. Во многих unix-версиях также есть ошибки, и даже в идеальном случае EINTR не работает правильно.
Это настолько проще, что это должен быть правильный способ сделать это. Если ваша ОС сломана, исправьте ее или получите лучшую.
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, пусть он запишет байт в канал, который проснется.
Если вы все равно собираетесь использовать сигналы (согласно предложению Стива), вы можете просто отправить сигнал вручную, когда захотите выйти. Это приведет к тому, что 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().
кажется интересной концепцией. вопрос, а зачем делать трубу неблокирующей? а зачем вам петли после выбора? Разве всегда не должно быть данными, когда выбор завершается успешно?
Если двое детей умрут, вы не обязательно получите два уведомления SIGCHLD. Вы делаете канал неблокирующим на случай, если поступит слишком много SIGCHLD (примерно PIPE_BUF).
Циклы также помогают защитить от слишком большого количества SIGCHLD, и хотя в идеале данные всегда будут после завершения выбора, read () будет блокироваться до тех пор, пока sizeof (фиктивные) байты не будут заполнены, если он не помечен как неблокирующий для чтения.
Я собирался обвинить ваш выбор неблокирующего режима, так как это означает, что если будет больше, чем PIPE_BUF SIGCHLD, некоторые из их write() будут потеряны, но теперь я вижу, что вы на самом деле не заботитесь о том, чтобы читать точно правильное количество байтов обратно из этой трубы. Хороший!
Но мне интересно: зачем включать бездействующий act.sa_flags |= 0;?
@j_random_hacker - наверное, это опечатка.
Поскольку (я так понимаю) вы автор, как вы думаете, что это должно было быть вместо этого? Не могли бы вы вынуть его, если не понимаете, для чего он нужен? Это просто сбивает с толку банкомат. Спасибо!
Разве signalfd(2) не был бы правильным способом «превратить любой сигнал в событие select()able»?
В моем быстром эксперименте select() возвращает -1 с errno == EINTR. Разве мы не должны ожидать такого поведения? Тогда как в вашем коде мы дойдем до waitpid?
@JonathonReinhart - Вы должны использовать SA_RESTART, если можете, но в противном случае вы можете перезапустить select() самостоятельно.
В общем, вы можете захотеть сохранить и восстановить errno в обработчике сигналов, чтобы предотвратить его перезапись.
Предостережение: все, что использует сигналы, принимает на себя ответственность за глобальное состояние процесса и предполагает сотрудничество со всеми другими библиотеками, используемыми в том же процессе.
что, если вы хотите дождаться процесса, который не является дочерним?
Я думал, что 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).
@AlexisWilke, правда. signalfd - лучшая альтернатива Linux. Использование glib может упростить решение, предложенное @geocar. И подход этого ответа может быть улучшен с использованием sigtimedwait, хотя вам все равно нужно замаскировать сигналы, чтобы мы не пропустили их. Думаю, в случае signalfd вы должны создать его до создания потомков.
Форк промежуточного потомка, который разветвляет реальный потомок и процесс тайм-аута и ожидает всех (обоих) своих потомков. Когда один выйдет, он убьет другого и выйдет.
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), но, блин, это очень крутой трюк! Престижность (просто интересно, почему за него не проголосовали больше)
Есть ли способ сделать функцию do_work вызовом system ()? Мне нужны тонкости того, что может предложить оболочка (глобализация, конвейерная обработка), но вызов system () заставляет ее продолжать работу, если рабочая вилка будет убита.
Может показаться, что это не гарантирует соблюдения тайм-аута, поскольку последний измеряется с момента запуска процесса timeout_pid (). Однако задержка между вызовом timeout_pid = fork () и фактическим запуском процесса timeout_pid произвольна.
Разве все планирование ЦП, строго говоря, не произвольно в большинстве операционных систем (не в реальном времени)? То есть у вас может быть произвольно длительная задержка планирования сразу после запуска вашего воркера, независимо от того, какой механизм тайм-аута вы используете. Тем не менее, это решение действительно может быть несколько менее точным, чем многие другие.
При использовании signal(SIGCHLD, SIG_IGN); при таком подходе возникает экзотическая ситуация, которая может вызвать проблемы. Подробнее см. здесь.
В силу обстоятельств мне абсолютно необходимо, чтобы это выполнялось в основном потоке, и было не очень просто использовать трюк 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?
Это интересный вопрос. Я обнаружил, что 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;
}
Как никто другой не может оценить этот ответ? :)
Это был бы лучший ответ с более подробной информацией и примером кода. Я использовал этот метод. Вот сообщение в блоге с хорошим примером: linuxprogrammingblog.com/code-examples/…
@Mark Edington Спасибо за ваше внимание. Я обновил ответ, упомянув ваш пример.
Этот ответ выглядит так, как будто он будет работать, только если есть только один дочерний процесс. Если задействовано несколько детей, то наверняка перекрестятся провода, и вы не узнаете, закончит ли ребенок, которого вы ждали.
@DavidRoundy спасибо, что поделились. Я не тестировал несколько детей, будет ли сигнализироваться сигнализация выхода любого из детей? Если это так, то можно использовать waitpid, чтобы узнать, какой дочерний элемент завершен.
@ osexp2003, пожалуйста, взгляните на stackoverflow.com/questions/64369771/…, я пробовал, но он не работает
Вместо прямого вызова 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;
}
что, если вы хотите сделать это для произвольного pid, который не является дочерним?
Если ваша программа работает только на современных ядрах 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;
С 2008 года произошло много событий, и стоит знать о том, что теперь доступна возможность signalfd (). Это можно использовать с
poll()илиselect(). Только берегитесь этого: stackoverflow.com/questions/8398298/handling-sigchld