Предположим, что у меня есть процесс, который порождает ровно один дочерний процесс. Теперь, когда родительский процесс выходит по какой-либо причине (обычно или ненормально, путем kill, ^ C, утверждает отказ или что-то еще), я хочу, чтобы дочерний процесс умирал. Как это сделать правильно?
Некоторые похожие вопросы о stackoverflow:
Некоторые похожие вопросы о stackoverflow для Windows:
Ребенок может попросить ядро доставить SIGHUP
(или другой сигнал), когда родитель умирает, указав опцию PR_SET_PDEATHSIG
в prctl()
syscall, как это:
prctl(PR_SET_PDEATHSIG, SIGHUP);
Подробнее см. man 2 prctl
.
Изменить: это Linux-only
Я пытаюсь решить ту же проблему, и поскольку моя программа должна работать на OS X, решение для Linux не работает для меня.
Я пришел к тому же выводу, что и другие люди на этой странице, - нет способа распознавания дочернего элемента, совместимого с POSIX, когда умирает родитель. Таким образом, я упустил следующую лучшую вещь - опрос ребенка.
Когда родительский процесс умирает (по какой-либо причине) дочерний родительский процесс становится процессом 1. Если ребенок просто периодически проверяет, он может проверить, является ли его родительский 1. Если это так, ребенок должен выйти.
Это не очень удобно, но он работает, и это проще, чем решения опроса TCP/lockfile, предложенные в другом месте на этой странице.
gettpid()
не становится 1, а получает pid
планировщика зоны (процесс zsched
).
Я в прошлом этого добился, запустив "оригинальный" код в "дочернем" и "порожденном" коде в "родительском" (то есть: вы отменили обычный смысл теста после 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);
}
В 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()
устраняет условие расы между 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).
Есть ли у дочернего процесса канал в/из родительского процесса? Если это так, вы получите SIGPIPE при записи или получите EOF при чтении - эти условия могут быть обнаружены.
Вдохновленный другим ответом здесь, я придумал следующее решение all-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);
}
Существует два небольших оговорки:
Как в стороне, фактический код, который я использую, находится в 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)
Я не считаю возможным гарантировать, что использование только стандартных вызовов POSIX. Как и в реальной жизни, когда ребенок порожден, у него есть своя жизнь.
возможно, чтобы родительский процесс мог поймать наиболее возможные события завершения и попытаться убить дочерний процесс в этой точке, но всегда есть некоторые, которые не могут быть пойманы.
Например, ни один процесс не может поймать SIGKILL
. Когда ядро обрабатывает этот сигнал, он убьет указанный процесс без уведомления об этом процессе.
Чтобы расширить аналогию - единственный другой стандартный способ сделать это, чтобы ребенок совершил самоубийство, когда обнаружил, что у него больше нет родителя.
Существует только Linux-способ сделать это с помощью prctl(2)
- см. другие ответы.
Как отмечали другие люди, полагаясь на родительский pid, чтобы стать 1, когда родительские выходы не переносятся. Вместо того, чтобы ждать определенного идентификатора родительского процесса, просто подождите, пока ID изменится:
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)
;
Добавьте желаемый микроспаст, если вы не хотите опроса на полной скорости.
Этот параметр кажется мне более простым, чем использование канала или использование сигналов.
getpid()
выполняется в родительском getpid()
перед вызовом fork()
. Если родитель умирает до этого, ребенка не существует. Может случиться так, что ребенок некоторое время живет с родителем.
Установите обработчик ловушек, чтобы поймать SIGINT, который убивает ваш дочерний процесс, если он еще жив, хотя другие плакаты верны, что он не поймает SIGKILL.
Откройте .lockfile с эксклюзивным доступом и попросите его опросить его, пытаясь открыть его - если открытое завершено успешно, дочерний процесс должен выйти
Это решение работало для меня:
Это было для процесса рабочего типа, существование которого имело смысл только тогда, когда родитель был жив.
Я думаю, что быстрый и грязный способ - создать канал между дочерним и родительским. Когда родительский выход, дети получат SIGPIPE.
В некоторых плакатах уже упоминаются трубы и 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 hung up".
По сравнению с методом, использующим обработчик SIGPIPE
, этот метод не требует попыток вызова write()
.
Этот метод также является симметричным, то есть процессы могут использовать один и тот же канал для контроля друг за другом.
Это решение вызывает только функции POSIX. Я попробовал это в Linux и FreeBSD. Я думаю, что он должен работать на других Unix, но я действительно не тестировал.
См. также:
unix(7)
справочных страниц Linux, unix(4)
для FreeBSD, poll(2)
, socketpair(2)
, socket(7)
в Linux.Несмотря на то, что прошло 7 лет, я просто столкнулся с этой проблемой, так как я запускаю приложение SpringBoot, которое нужно запустить webpack-dev-server во время разработки, и его нужно убить, когда заканчивается процесс backend.
Я пытаюсь использовать 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();
}
В случае, если это относится к кому-либо еще, когда я запускаю экземпляры JVM в разветвленных дочерних процессах из С++, единственный способ, с помощью которого я мог бы привести экземпляры JVM к правильному завершению после завершения родительского процесса, заключался в следующем. Надеюсь, кто-то может дать отзыв в комментариях, если это не лучший способ сделать это.
1) Вызовите prctl(PR_SET_PDEATHSIG, SIGHUP)
в развернутом дочернем процессе, как было предложено перед запуском приложения Java через execv
, и
2) Добавьте привязку выключения к приложению Java, которое опросит, пока его родительский PID не станет равным 1, затем выполните сложный Runtime.getRuntime().halt(0)
. Опрос выполняется путем запуска отдельной оболочки, которая запускает команду ps
(см. Как найти мой PID в Java или JRuby в Linux?).
EDIT 130118:
Кажется, это не было надежным решением. Я все еще немного пытаюсь понять нюансы того, что происходит, но я все еще иногда получал сиротские процессы JVM при запуске этих приложений в сеансах экрана /SSH.
Вместо опроса для PPID в приложении Java, я просто выполнил очистку выключения, после чего остановился, как указано выше. Затем я убедился в вызове waitpid
в родительском приложении С++ для порожденного дочернего процесса, когда пришло время прекратить все. Это кажется более надежным решением, так как дочерний процесс обеспечивает его завершение, в то время как родитель использует существующие ссылки, чтобы убедиться, что его дети завершатся. Сравните это с предыдущим решением, в котором родительский процесс прекращался, когда он был доволен, и дети пытались выяснить, остались ли они сиротами до завершения.
PID equals 1
ожидание недействительно. Новый родитель может быть другим PID. Вы должны проверить, изменяется ли он от исходного родителя (getpid () перед fork ()) на нового родителя (getppid () в потомке, который не равен getpid () при вызове перед fork ()).
Если вы отправляете сигнал на pid 0, используя, например,
kill(0, 2); /* SIGINT */
этот сигнал отправляется всей группе процессов, тем самым эффективно убивая ребенка.
Вы можете легко протестировать его с помощью:
(cat && kill 0) | python
Если вы затем нажмете ^ D, вы увидите текст "Terminated"
как указание на то, что интерпретатор Python действительно был убит, а не просто вышел из-за закрытия stdin.
(echo -e "print(2+2)\n" & kill 0) | sh -c "python -"
радостно печатает 4 вместо завершенных
В POSIX функции exit()
, _exit()
и _exit()
определены как:
Итак, если вы планируете, чтобы родительский процесс был управляющим процессом для своей группы процессов, ребенок должен получить сигнал SIGHUP, когда родительский выход завершен. Я не совсем уверен, что это происходит, когда родительский сбой, но я думаю, что это так. Конечно, для случаев без сбоев он должен работать нормально.
Обратите внимание, что вам, возможно, придется прочитать довольно много мелкого шрифта, включая раздел "Базовые определения (определения)", а также информацию "Системные службы" для exit()
и setsid()
и setpgrp()
-, чтобы получить полный картина. (Я бы тоже!)
Другим способом сделать это, что является конкретным Linux, является создание родителя в новом пространстве имен PID. Тогда он будет PID 1 в этом пространстве имен, и когда он выйдет из него, все его дети будут немедленно убиты с помощью SIGKILL
.
К сожалению, для создания нового пространства имен PID необходимо иметь CAP_SYS_ADMIN
. Но этот метод очень эффективен и не требует реальных изменений родителя или детей за пределами первоначального запуска родителя.
См. clone (2), pid_namespaces (7) и unshare (2).
Исторически, из UNIX v7 система процессов обнаружила сиротность процессов, проверяя родительский идентификатор процесса. Как я уже сказал, исторический процесс init(8)
- это особый процесс только по одной причине: он не может умереть. Он не может умереть, потому что алгоритм ядра, связанный с назначением нового идентификатора родительского процесса, зависит от этого факта. когда процесс выполняет свой вызов exit(2)
(посредством системного вызова процесса или внешней задачи, отправляя ему сигнал или тому подобное), ядро переназначает всех дочерних элементов этого процесса идентификатором процесса инициализации как своего родительского идентификатора процесса. Это приводит к самому простому испытанию и наиболее переносимому способу узнать, получил ли процесс сирота. Просто проверьте результат системного вызова getppid(2)
и, если он является идентификатором процесса процесса init(2)
, тогда процесс получил сироту перед системным вызовом.
Из этого подхода возникают две проблемы, которые могут привести к проблемам:
init
на любой пользовательский процесс, поэтому как мы можем гарантировать, что процесс init всегда будет родителем всех сиротских процессов? Ну, в системном коде exit
есть явная проверка, чтобы увидеть, является ли процесс, выполняющий вызов, процессом init (процесс с pid равным 1), и если это произойдет, ядро паники (оно не должно быть способным чтобы поддерживать иерархию процессов), поэтому для процесса init не разрешается вызов exit(2)
.1
, но это не оправдывается подходом POSIX, который указывает (как показано в другом ответе), что для этой цели зарезервирован только идентификатор системного процесса. Практически ни одна реализация posix не делает этого, и вы можете предположить, что в исходных системах, основанных на unix, наличие 1
в качестве ответа системного вызова getppid(2)
достаточно, чтобы предположить, что процесс является сиротой. Другой способ проверить - сделать getppid(2)
сразу после fork и сравнить это значение с результатом нового вызова. Это просто не работает во всех случаях, так как оба вызова не являются атомарными вместе, и родительский процесс может умереть после fork(2)
и перед первым системным вызовом getppid(2)
. Процесс parent id only changes once, when its parent does an
exit (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)Мне удалось сделать переносное решение без опроса с тремя процессами, злоупотребляя терминальным управлением и сеансами. Это умственная мастурбация, но работает.
Трюк:
Таким образом:
Недостатки:
Я нашел 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