Как заставить дочерний процесс умереть после родительского выхода?
Предположим, у меня есть процесс, который порождает ровно один дочерний процесс. Теперь, когда родительский процесс завершается по какой-либо причине (обычно или ненормально, из-за kill, ^C, сбоя подтверждения или чего-либо еще), я хочу, чтобы дочерний процесс умер. Как это сделать правильно?
Некоторые похожие вопросы по stackru:
- (спрашивается ранее) Как я могу заставить дочерний процесс завершать работу, если родительский процесс завершается?
- (спрашивается позже) Дочерние процессы, созданные с помощью fork(), автоматически уничтожаются при уничтожении родителя?
Несколько похожих вопросов о stackru для Windows:
23 ответа
Ребенок может попросить ядро доставить SIGHUP
(или другой сигнал), когда родитель умирает, указав опцию PR_SET_PDEATHSIG
в prctl()
Syscall, как это:
prctl(PR_SET_PDEATHSIG, SIGHUP);
Увидеть man 2 prctl
для деталей.
Редактировать: это только для Linux
Я пытаюсь решить ту же проблему, и поскольку моя программа должна работать на OS X, решение только для Linux не сработало для меня.
Я пришел к тому же выводу, что и другие люди на этой странице, - нет POSIX-совместимого способа уведомления ребенка о смерти родителя. Таким образом, я запутался в следующей лучшей вещи - проведении детского опроса.
Когда родительский процесс умирает (по любой причине), родительский процесс дочернего процесса становится процессом 1. Если дочерний процесс просто периодически опрашивает, он может проверить, равен ли его родительский 1. Если это так, дочерний процесс должен завершиться.
Это не очень хорошо, но это работает, и это проще, чем решения опроса TCP-сокетов / файлов блокировки, предложенные в другом месте на этой странице.
В Linux вы можете установить родительский сигнал смерти у ребенка, например:
#include <sys/prctl.h>
#include <signal.h> // signals
#include <unistd.h> // fork()
#include <stdio.h> // perror()
// ...
pid_t ppid_before_fork = getpid();
pid_t pid = fork();
if (pid == -1) { perror(0); exit(1); }
if (pid) {
; // continue parent execution
} else {
int r = prctl(PR_SET_PDEATHSIG, SIGTERM);
if (r == -1) { perror(0); exit(1); }
// test in case the original parent exited just
// before the prctl() call
if (getppid() != ppid_before_fork)
exit(1);
// continue child execution ...
Обратите внимание, что хранение идентификатора родительского процесса до разветвления и тестирование его в потомке после prctl()
устраняет состояние гонки между prctl()
и выход из процесса, который вызвал ребенка.
Также обратите внимание, что родительский сигнал смерти ребенка очищается у вновь созданных детей самостоятельно. Это не зависит от execve()
,
Этот тест можно упростить, если мы уверены, что системный процесс, отвечающий за усыновление всех сирот, имеет PID 1:
pid_t pid = fork();
if (pid == -1) { perror(0); exit(1); }
if (pid) {
; // continue parent execution
} else {
int r = prctl(PR_SET_PDEATHSIG, SIGTERM);
if (r == -1) { perror(0); exit(1); }
// test in case the original parent exited just
// before the prctl() call
if (getppid() == 1)
exit(1);
// continue child execution ...
Опираясь на этот системный процесс init
а наличие PID 1 не переносимо. POSIX.1-2008 определяет:
Идентификатор родительского процесса всех существующих дочерних процессов и процессов-зомби вызывающего процесса должен быть установлен на идентификатор процесса системного процесса, определенного реализацией. То есть эти процессы должны наследоваться специальным системным процессом.
Традиционно системный процесс, принимающий всех сирот, - это PID 1, т. Е. Init - предок всех процессов.
В современных системах, таких как Linux или FreeBSD, эту роль может выполнять другой процесс. Например, в Linux процесс может вызвать prctl(PR_SET_CHILD_SUBREAPER, 1)
установить себя как системный процесс, который наследует всех сирот любого из его потомков (см. пример на Fedora 25).
Я добился этого в прошлом, запустив "оригинальный" код в "дочернем" и "порожденный" код в "родительском" (то есть: вы переворачиваете обычный смысл теста после fork()
). Затем поместите SIGCHLD в "порожденный" код...
Может быть не возможно в вашем случае, но мило, когда это работает.
Если вы не можете изменить дочерний процесс, вы можете попробовать что-то вроде следующего:
int pipes[2];
pipe(pipes)
if (fork() == 0) {
close(pipes[1]); /* Close the writer end in the child*/
dup2(0, pipes[0]); /* Use reader end as stdin */
exec("sh -c 'set -o monitor; child_process & read dummy; kill %1'")
}
close(pipes[0]); /* Close the reader end in the parent */
Это запускает дочерний процесс из процесса оболочки с включенным управлением заданиями. Дочерний процесс создается в фоновом режиме. Оболочка ждет новой строки (или EOF), а затем убивает дочернего элемента.
Когда родитель умирает, независимо от причины, он закрывает конец трубы. Дочерняя оболочка получит EOF из чтения и продолжит убивать фоновый дочерний процесс.
Ради полноты. В macOS вы можете использовать kqueue:
void noteProcDeath(
CFFileDescriptorRef fdref,
CFOptionFlags callBackTypes,
void* info)
{
// LOG_DEBUG(@"noteProcDeath... ");
struct kevent kev;
int fd = CFFileDescriptorGetNativeDescriptor(fdref);
kevent(fd, NULL, 0, &kev, 1, NULL);
// take action on death of process here
unsigned int dead_pid = (unsigned int)kev.ident;
CFFileDescriptorInvalidate(fdref);
CFRelease(fdref); // the CFFileDescriptorRef is no longer of any use in this example
int our_pid = getpid();
// when our parent dies we die as well..
LOG_INFO(@"exit! parent process (pid %u) died. no need for us (pid %i) to stick around", dead_pid, our_pid);
exit(EXIT_SUCCESS);
}
void suicide_if_we_become_a_zombie(int parent_pid) {
// int parent_pid = getppid();
// int our_pid = getpid();
// LOG_ERROR(@"suicide_if_we_become_a_zombie(). parent process (pid %u) that we monitor. our pid %i", parent_pid, our_pid);
int fd = kqueue();
struct kevent kev;
EV_SET(&kev, parent_pid, EVFILT_PROC, EV_ADD|EV_ENABLE, NOTE_EXIT, 0, NULL);
kevent(fd, &kev, 1, NULL, 0, NULL);
CFFileDescriptorRef fdref = CFFileDescriptorCreate(kCFAllocatorDefault, fd, true, noteProcDeath, NULL);
CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack);
CFRunLoopSourceRef source = CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, fdref, 0);
CFRunLoopAddSource(CFRunLoopGetMain(), source, kCFRunLoopDefaultMode);
CFRelease(source);
}
Вдохновленный другим ответом здесь, я придумал следующее решение для всех POSIX. Общая идея состоит в том, чтобы создать промежуточный процесс между родителем и потомком, который имеет одну цель: заметить, когда родитель умирает, и явно убить потомка.
Этот тип решения полезен, когда код в дочернем элементе не может быть изменен.
int p[2];
pipe(p);
pid_t child = fork();
if (child == 0) {
close(p[1]); // close write end of pipe
setpgid(0, 0); // prevent ^C in parent from stopping this process
child = fork();
if (child == 0) {
close(p[0]); // close read end of pipe (don't need it here)
exec(...child process here...);
exit(1);
}
read(p[0], 1); // returns when parent exits for any reason
kill(child, 9);
exit(1);
}
Есть два небольших предостережения с этим методом:
- Если вы намеренно убьете промежуточный процесс, тогда ребенок не будет убит после смерти родителя.
- Если дочерний процесс завершается раньше, чем родительский, то промежуточный процесс попытается уничтожить исходный дочерний pid, который теперь может ссылаться на другой процесс. (Это можно исправить с помощью большего количества кода в промежуточном процессе.)
Кроме того, фактический код, который я использую, находится на Python. Вот для полноты картины:
def run(*args):
(r, w) = os.pipe()
child = os.fork()
if child == 0:
os.close(w)
os.setpgid(0, 0)
child = os.fork()
if child == 0:
os.close(r)
os.execl(args[0], *args)
os._exit(1)
os.read(r, 1)
os.kill(child, 9)
os._exit(1)
os.close(r)
Есть ли у дочернего процесса канал к / от родительского процесса? Если это так, вы получите SIGPIPE при записи или EOF при чтении - эти условия могут быть обнаружены.
Я не верю, что можно гарантировать, что используются только стандартные вызовы POSIX. Как и в реальной жизни, когда ребенок рождается, у него появляется собственная жизнь.
Для родительского процесса возможно перехватить большинство возможных событий завершения и попытаться уничтожить дочерний процесс в этот момент, но всегда есть некоторые, которые не могут быть перехвачены.
Например, ни один процесс не может поймать SIGKILL
, Когда ядро обрабатывает этот сигнал, оно убивает указанный процесс без какого-либо уведомления этого процесса.
Продлить аналогию - единственный другой стандартный способ сделать это - ребенок совершить самоубийство, когда обнаружит, что у него больше нет родителя.
Существует только Linux способ сделать это с prctl(2)
- увидеть другие ответы.
Это решение сработало для меня:
- Передайте stdin pipe потомку - вам не нужно записывать данные в поток.
- Ребенок читает бесконечно от стандартного ввода до конца. EOF сигнализирует, что родитель ушел.
- Это надежный и портативный способ определить, когда родитель ушел. Даже если родительский сбой, ОС закроет канал.
Это был процесс рабочего типа, существование которого имело смысл только тогда, когда родитель был жив.
Некоторые постеры уже упоминали трубы и kqueue
, На самом деле вы также можете создать пару подключенных доменных сокетов Unix с помощью socketpair()
вызов. Тип сокета должен быть SOCK_STREAM
,
Предположим, у вас есть два дескриптора файлов сокетов fd1, fd2. Сейчас fork()
создать дочерний процесс, который унаследует fds. В родительском вы закрываете fd2, а в дочернем вы закрываете fd1. Теперь каждый процесс может poll()
оставшиеся открытые FD на свой конец для POLLIN
событие. Пока каждая сторона явно не close()
его FD в течение нормальной жизни, вы можете быть уверены, что POLLHUP
флаг должен указывать на завершение другого (независимо от того, чист он или нет). После уведомления об этом событии ребенок может решить, что ему делать (например, умереть).
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <poll.h>
#include <stdio.h>
int main(int argc, char ** argv)
{
int sv[2]; /* sv[0] for parent, sv[1] for child */
socketpair(AF_UNIX, SOCK_STREAM, 0, sv);
pid_t pid = fork();
if ( pid > 0 ) { /* parent */
close(sv[1]);
fprintf(stderr, "parent: pid = %d\n", getpid());
sleep(100);
exit(0);
} else { /* child */
close(sv[0]);
fprintf(stderr, "child: pid = %d\n", getpid());
struct pollfd mon;
mon.fd = sv[1];
mon.events = POLLIN;
poll(&mon, 1, -1);
if ( mon.revents & POLLHUP )
fprintf(stderr, "child: parent hung up\n");
exit(0);
}
}
Вы можете попробовать скомпилировать приведенный выше код для проверки концепции и запустить его в терминале, например ./a.out &
, У вас есть примерно 100 секунд, чтобы поэкспериментировать с уничтожением родительского PID различными сигналами, иначе он просто выйдет. В любом случае, вы должны увидеть сообщение "child: parent Hangable".
По сравнению с методом с использованием SIGPIPE
обработчик, этот метод не требует попытки write()
вызов.
Этот метод также симметричен, то есть процессы могут использовать один и тот же канал для мониторинга существования друг друга.
Это решение вызывает только функции POSIX. Я пробовал это в Linux и FreeBSD. Я думаю, что это должно работать на других Unix, но я действительно не проверял.
Смотрите также:
unix(7)
справочных страниц по Linux,unix(4)
для FreeBSD,poll(2)
,socketpair(2)
,socket(7)
в линуксе
Как отмечали другие люди, полагаться на родительский pid, который станет 1, когда родительский выход является непереносимым. Вместо ожидания определенного идентификатора родительского процесса, просто подождите, пока идентификатор изменится:
pit_t pid = getpid();
switch (fork())
{
case -1:
{
abort(); /* or whatever... */
}
default:
{
/* parent */
exit(0);
}
case 0:
{
/* child */
/* ... */
}
}
/* Wait for parent to exit */
while (getppid() != pid)
;
Добавьте микро-сон по желанию, если вы не хотите опрашивать на полной скорости.
Этот вариант кажется мне более простым, чем использование канала или использование сигналов.
Установите обработчик ловушек, чтобы поймать SIGINT, который убивает ваш дочерний процесс, если он еще жив, хотя другие авторы верны, что он не поймает SIGKILL.
Откройте файл.lock с монопольным доступом и попытайтесь открыть его дочерним опросом - если открытие завершится успешно, дочерний процесс должен завершиться.
Другой способ сделать это для Linux - создать родителя в новом пространстве имен PID. Тогда это будет PID 1 в этом пространстве имен, и когда он выйдет из него, все его дети будут немедленно убиты SIGKILL
,
К сожалению, для создания нового пространства имен PID вам необходимо иметь CAP_SYS_ADMIN
, Но этот метод очень эффективен и не требует никаких реальных изменений для родителя или потомков после первоначального запуска родителя.
Смотрите clone (2), pid_namespaces (7) и unshare (2).
Я думаю, что быстрый и грязный способ - создать канал между дочерним элементом и родительским элементом. Когда родитель выходит, дети получат SIGPIPE.
Исторически в UNIX v7 система процессов обнаружила потерю процессов, проверив идентификатор родительского процесса. Как я уже сказал, исторически init(8)
Системный процесс - это особый процесс только по одной причине: он не может умереть. Он не может умереть, потому что алгоритм ядра, чтобы иметь дело с назначением нового идентификатора родительского процесса, зависит от этого факта. когда процесс выполняет его exit(2)
вызов (посредством системного вызова процесса или внешней задачи как отправка ему сигнала или тому подобное) ядро переназначает всем дочерним элементам этого процесса идентификатор процесса init в качестве идентификатора их родительского процесса. Это приводит к самому легкому тестированию и наиболее портативному способу узнать, стал ли процесс потерянным. Просто проверьте результат getppid(2)
системный вызов и, если это идентификатор процесса init(2)
процесс затем процесс стал сиротой перед системным вызовом.
При таком подходе возникают две проблемы, которые могут привести к проблемам:
- Во-первых, у нас есть возможность изменить
init
процесс для любого пользовательского процесса, так как мы можем гарантировать, что процесс init всегда будет родительским для всех потерянных процессов? Ну, вexit
В коде системного вызова есть явная проверка, чтобы увидеть, является ли процесс, выполняющий вызов, процессом init (процесс с pid, равным 1), и если это так, ядро паникует (оно больше не должно поддерживать иерархию процесса) поэтому процессу init не разрешается делатьexit(2)
вызов. - во-вторых, в базовом тесте, описанном выше, есть состояние гонки. Идентификатор процесса инициализации предполагается исторически
1
, но это не гарантируется подходом POSIX, который утверждает (как показано в другом ответе), что для этой цели зарезервирован только системный идентификатор процесса. Практически ни одна реализация posix не делает этого, и вы можете предположить, что в оригинальных системах, производных от Unix,1
как ответgetppid(2)
Системного вызова достаточно, чтобы предположить, что процесс потерян. Еще один способ проверить это сделатьgetppid(2)
сразу после разветвления и сравните это значение с результатом нового вызова. Это просто не работает во всех случаях, так как оба вызова не являются атомарными, и родительский процесс может умереть послеfork(2)
и до первогоgetppid(2)
системный вызов. Процессparent id only changes once, when its parent does an
Выход (2)call, so this should be enough to check if the
getppid (2)result changed between calls to see that parent process has exit. This test is not valid for the actual children of the init process, because they are always children of
init (8) `, но вы можете также предположить, что эти процессы не имеют родителя (кроме случаев, когда вы заменяете в системе процесс init)
Если вы отправляете сигнал на pid 0, используя, например,
kill(0, 2); /* SIGINT */
этот сигнал отправляется всей группе процессов, таким образом, эффективно убивая ребенка.
Вы можете легко проверить это с чем-то вроде:
(cat && kill 0) | python
Если вы затем нажмите ^D, вы увидите текст "Terminated"
как указание на то, что интерпретатор Python действительно был убит, а не просто закрыт из-за закрытия stdin.
В случае, если это имеет отношение к кому-либо еще, когда я порождаю экземпляры JVM в разветвленных дочерних процессах из C++, единственный способ заставить экземпляры JVM правильно завершиться после завершения родительского процесса - это сделать следующее. Надеюсь, кто-то может оставить отзыв в комментариях, если это не лучший способ сделать это.
1) Позвонить prctl(PR_SET_PDEATHSIG, SIGHUP)
на разветвленном дочернем процессе, как было предложено до запуска приложения Java через execv
, а также
2) Добавьте хук отключения к приложению Java, которое опрашивает, пока его родительский PID не станет равным 1, затем выполните хард Runtime.getRuntime().halt(0)
, Опрос выполняется путем запуска отдельной оболочки, которая запускает ps
команда (См.: Как мне найти мой PID в Java или JRuby в Linux?).
РЕДАКТИРОВАТЬ 130118:
Кажется, это не было надежным решением. Я все еще немного пытаюсь понять нюансы происходящего, но я все еще иногда получал бесхозные процессы JVM при запуске этих приложений в сеансах screen/SSH.
Вместо того, чтобы опрашивать PPID в приложении Java, я просто заставил хук выключения выполнить очистку с последующей полной остановкой, как описано выше. Тогда я убедился, чтобы призвать waitpid
в родительском приложении C++ на порожденном дочернем процессе, когда пришло время завершать все. Это кажется более надежным решением, так как дочерний процесс гарантирует, что он завершается, в то время как родитель использует существующие ссылки, чтобы убедиться, что его дочерние элементы завершаются. Сравните это с предыдущим решением, в котором родительский процесс завершался всякий раз, когда ему было удобно, и когда дети пытались выяснить, не осиротели ли они до завершения.
Под POSIX, exit()
, _exit()
а также _Exit()
функции определены для:
- Если процесс является процессом управления, сигнал SIGHUP должен отправляться каждому процессу в группе процессов переднего плана управляющего терминала, принадлежащего вызывающему процессу.
Таким образом, если вы организуете, чтобы родительский процесс был управляющим процессом для его группы процессов, дочерний процесс должен получить сигнал SIGHUP при выходе из родительского процесса. Я не совсем уверен, что происходит, когда родительский сбой, но я думаю, что это происходит. Конечно, для случаев без сбоев все должно работать нормально.
Обратите внимание, что вам, возможно, придется прочитать довольно много мелкого шрифта - включая раздел "Базовые определения (определения)", а также информацию о системных службах для exit()
а также setsid()
а также setpgrp()
- чтобы получить полную картину. (Я бы тоже!)
Мне удалось создать портативное решение без опроса с 3 процессами, злоупотребив терминальным управлением и сеансами. Это умственная мастурбация, но работает.
Хитрость заключается в следующем:
- процесс А запущен
- процесс A создает канал P (и никогда не читает из него)
- процесс А превращается в процесс Б
- процесс B создает новую сессию
- процесс B выделяет виртуальный терминал для этого нового сеанса
- процесс B устанавливает обработчик SIGCHLD, чтобы умереть, когда ребенок выходит
- процесс B устанавливает обработчик SIGPIPE
- процесс B переходит в процесс C
- процесс C делает все, что ему нужно (например, exec() - неизмененный двоичный файл или выполняет любую логику)
- процесс B записывает в канал P (и блокирует таким образом)
- процесс A wait() в процессе B и завершается, когда он умирает
Сюда:
- если процесс A умирает: процесс B получает SIGPIPE и умирает
- если процесс B умирает: процесс A ожидания () возвращается и умирает, процесс C получает SIGHUP (потому что, когда лидер сеанса сеанса с подключенным терминалом умирает, все процессы в группе процессов переднего плана получают SIGHUP)
- если процесс C умирает: процесс B получает SIGCHLD и умирает, поэтому процесс A умирает
Недостатки:
- процесс C не может обработать SIGHUP
- Процесс C будет запущен в другом сеансе
- процесс C не может использовать API сеанса / группы процессов, потому что он сломает хрупкую настройку
- создание терминала для каждой такой операции не самая лучшая идея
Я нашел 2 решения, оба не идеальные.
1. Убить всех детей с помощью kill(-pid) при получении сигнала SIGTERM.
Очевидно, что это решение не может обрабатывать "kill -9", но оно работает для большинства случаев и очень просто, потому что ему не нужно запоминать все дочерние процессы.
var childProc = require('child_process').spawn('tail', ['-f', '/dev/null'], {stdio:'ignore'});
var counter=0;
setInterval(function(){
console.log('c '+(++counter));
},1000);
if (process.platform.slice(0,3) != 'win') {
function killMeAndChildren() {
/*
* On Linux/Unix(Include Mac OS X), kill (-pid) will kill process group, usually
* the process itself and children.
* On Windows, an JOB object has been applied to current process and children,
* so all children will be terminated if current process dies by anyway.
*/
console.log('kill process group');
process.kill(-process.pid, 'SIGKILL');
}
/*
* When you use "kill pid_of_this_process", this callback will be called
*/
process.on('SIGTERM', function(err){
console.log('SIGTERM');
killMeAndChildren();
});
}
Таким же образом, вы можете установить обработчик 'exit', как описано выше, если вы вызываете process.exit где-нибудь. Примечание: Ctrl+C и внезапный сбой были автоматически обработаны ОС для уничтожения группы процессов, поэтому здесь больше нет.
2. Используйте chjj / pty.js для запуска вашего процесса с подключенным управляющим терминалом.
Если вы в любом случае даже уничтожите текущий процесс, даже убив -9, все дочерние процессы также будут автоматически уничтожены (ОС?). Я предполагаю, что поскольку текущий процесс удерживает другую сторону терминала, поэтому, если текущий процесс умирает, дочерний процесс получит SIGPIPE, поэтому он умрет.
var pty = require('pty.js');
//var term =
pty.spawn('any_child_process', [/*any arguments*/], {
name: 'xterm-color',
cols: 80,
rows: 30,
cwd: process.cwd(),
env: process.env
});
/*optionally you can install data handler
term.on('data', function(data) {
process.stdout.write(data);
});
term.write(.....);
*/
Если родитель умирает, PPID сирот изменится на 1 - вам нужно только проверить свой PPID. В некотором смысле, это опрос, упомянутый выше. вот кусок оболочки для этого:
check_parent () {
parent=`ps -f|awk '$2=='$PID'{print $3 }'`
echo "parent:$parent"
let parent=$parent+0
if [[ $parent -eq 1 ]]; then
echo "parent is dead, exiting"
exit;
fi
}
PID=$$
cnt=0
while [[ 1 = 1 ]]; do
check_parent
... something
done
Несмотря на то, что прошло 7 лет, я только что столкнулся с этой проблемой, так как я запускаю приложение SpringBoot, которому нужно запускать webpack-dev-server во время разработки и нужно убить его, когда бэкэнд-процесс останавливается.
Я пытаюсь использовать Runtime.getRuntime().addShutdownHook
но это работало на Windows 10, но не на Windows 7.
Я изменил его, чтобы использовать выделенный поток, который ожидает завершения процесса или для InterruptedException
который, кажется, работает правильно на обеих версиях Windows.
private void startWebpackDevServer() {
String cmd = isWindows() ? "cmd /c gradlew webPackStart" : "gradlew webPackStart";
logger.info("webpack dev-server " + cmd);
Thread thread = new Thread(() -> {
ProcessBuilder pb = new ProcessBuilder(cmd.split(" "));
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
pb.directory(new File("."));
Process process = null;
try {
// Start the node process
process = pb.start();
// Wait for the node process to quit (blocking)
process.waitFor();
// Ensure the node process is killed
process.destroyForcibly();
System.setProperty(WEBPACK_SERVER_PROPERTY, "true");
} catch (InterruptedException | IOException e) {
// Ensure the node process is killed.
// InterruptedException is thrown when the main process exit.
logger.info("killing webpack dev-server", e);
if (process != null) {
process.destroyForcibly();
}
}
});
thread.start();
}
Я передал родительский идентификатор окружения дочернему элементу, а затем периодически проверял, существует ли /proc/$ppid от дочернего элемента.