Как заставить дочерний процесс умереть после родительского выхода? - PullRequest
194 голосов
/ 12 ноября 2008

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


Некоторые похожие вопросы по stackoverflow:


Некоторые похожие вопросы по стеку потока для Windows :

Ответы [ 23 ]

171 голосов
/ 12 ноября 2008

Ребенок может попросить ядро ​​доставить SIGHUP (или другой сигнал), когда родитель умирает, указав параметр PR_SET_PDEATHSIG в prctl() системном вызове следующим образом:

prctl(PR_SET_PDEATHSIG, SIGHUP);

Подробнее см. man 2 prctl.

Редактировать: Это только для Linux

65 голосов
/ 10 января 2010

Я пытаюсь решить ту же проблему, и, поскольку моя программа должна работать на OS X, решение только для Linux у меня не сработало.

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

Когда родительский процесс умирает (по любой причине), родительский процесс ребенка становится процессом 1. Если дочерний процесс просто периодически опрашивает, он может проверить, равен ли его родитель 1. Если это так, дочерний процесс должен выйти.

Это не замечательно, но работает, и это проще, чем решения для опроса TCP-сокетов / файлов блокировки, предложенные в других разделах этой страницы.

34 голосов
/ 12 ноября 2008

Я достиг этого в прошлом, запустив «оригинальный» код в «child» и «порожденный» код в «parent» (т. Е. Вы изменили обычный смысл теста после fork()) , Затем поместите SIGCHLD в «порожденный» код ...

Может быть не возможно в вашем случае, но мило, когда это работает.

29 голосов
/ 29 октября 2010

Если вы не можете изменить дочерний процесс, вы можете попробовать что-то вроде следующего:

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 из чтения и продолжит убивать фоновый дочерний процесс.

14 голосов
/ 26 июня 2011

Ради полноты. В 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);
}
11 голосов
/ 29 апреля 2016

В Linux вы можете установить родительский сигнал смерти ребенка, например ::10000

#include <sys/prctl.h> // prctl(), PR_SET_PDEATHSIG
#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).

11 голосов
/ 12 ноября 2008

Есть ли у дочернего процесса канал к / от родительского процесса? Если это так, вы получите SIGPIPE при записи или EOF при чтении - эти условия могут быть обнаружены.

10 голосов
/ 01 мая 2014

Вдохновленный другим ответом здесь, я придумал следующее решение для всех 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)
7 голосов
/ 12 ноября 2008

Я не верю, что можно гарантировать, что используются только стандартные вызовы POSIX. Как и в реальной жизни, когда ребенок рождается, у него появляется собственная жизнь.

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

Например, ни один процесс не может поймать SIGKILL. Когда ядро ​​обрабатывает этот сигнал, оно убивает указанный процесс без какого-либо уведомления этому процессу.

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

Существует только способ Linux сделать это с prctl(2) - см. Другие ответы.

6 голосов
/ 21 мая 2013

Как указали другие люди, полагаясь на родительский 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)
    ;

Добавьте микро-сон по желанию, если вы не хотите опрашивать на полной скорости.

Этот вариант мне кажется проще, чем использование канала или использование сигналов.

...