Linux

Как сделать так, чтобы дочерний процесс завершался после завершения родительского

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

 

Ответ 1

В Linux вы можете установить для дочернего процесса сигнал завершения родительского процесса, например так:

#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) {

    ; // продолжить выполнение родительского процесса

} else {

    int r = prctl(PR_SET_PDEATHSIG, SIGTERM);

    if (r == -1) { perror(0); exit(1); }

    // проверка на случай, если исходный родитель завершился непосредственно

    // перед вызовом prctl()

    if (getppid() != ppid_before_fork)

        exit(1);

    // продолжить выполнение дочернего ...

Обратите внимание, что сохранение идентификатора родительского процесса перед fork и его тестирование в дочернем процессе после prctl() устраняет состояние гонки между вызовом prctl() и завершением процесса, вызвавшего дочерний процесс.

Также обратите внимание, что сигнал завершения родителя сбрасывается во вновь созданных собственных дочерних элементах. На него не влияет файл execve().

Этот тест можно упростить, если вы уверены, что системный процесс, отвечающий за привязку всех дочерних процессов, имеет PID 1:

pid_t pid = fork();

if (pid == -1) { perror(0); exit(1); }

if (pid) {

    ; // продолжить выполнение родительского процесса

} else {

    int r = prctl(PR_SET_PDEATHSIG, SIGTERM);

    if (r == -1) { perror(0); exit(1); }

    // проверка на случай, если исходный родитель завершился непосредственно

    // перед вызовом prctl()

    if (getppid() == 1)

        exit(1);

    // продолжить выполнение дочернего ...

Однако полагаться на то, что этот системный процесс имеет PID 1, не совсем корректно.

В POSIX.1-2008 указано:

Идентификатор родительского процесса всех существующих дочерних процессов и зомби-процессов вызывающего процесса должен быть установлен на идентификатор процесса системного процесса, определенного реализацией. То есть эти процессы должны быть унаследованы специальным системным процессом.

Традиционно системный процесс, принимающий всех дочерних, - это PID 1, то есть init, который является предком всех процессов.

В современных системах, таких как Linux или FreeBSD, эту роль может выполнять другой процесс. Например, в Linux процесс может вызывать, prctl(PR_SET_CHILD_SUBREAPER, 1), чтобы установить себя как системный процесс, который наследует все дочерние процессы и  все процессы своих потомков.

 

Ответ 2

Для полноты картины: в 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);

    // принять меры в случае завершения процесса здесь

    unsigned int dead_pid = (unsigned int)kev.ident;

    CFFileDescriptorInvalidate(fdref);

    CFRelease(fdref); // CFFileDescriptorRef больше не используется в этом примере

    int our_pid = getpid();

    // когда завершается наш родитель, завершаемся и мы... 

    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);

}



Ответ 3

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

Этот тип решения полезен, когда код в дочернем элементе нельзя изменить.

int p[2];

pipe(p);

pid_t child = fork();

if (child == 0) {

    close(p[1]);

    setpgid(0, 0); // не позволяет по «^C» в родителе остановить этот процесс

    child = fork();

    if (child == 0) {

        close(p[0]); // закрыть канал для чтения (здесь он не нужен)

        exec(...дочерний процесс здесь...);

        exit(1);

    }

    read(p[0], 1); // возврат, когда родитель завершается по любой причине

    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)

 

Ответ 4

В предыдущих ответах уже упоминали каналы и kqueue. На самом деле вы также можете создать пару соединенных сокетов домена Unix с помощью вызова socketpair(). Тип сокета должен быть SOCK_STREAM.

Предположим, у вас есть два дескриптора файлов сокетов fd1, fd2. Теперь выполните fork() для создания дочернего процесса, который унаследует fd. В родительском процессе вы закрываете fd2, а в дочернем fd1. Теперь каждый процесс может запрашивать оставшийся открытым fd на своей стороне на предмет события POLLIN. До тех пор, пока каждая сторона явно не закроет свой 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] для родительского, sv[1] для дочернего */

    socketpair(AF_UNIX, SOCK_STREAM, 0, sv);

    pid_t pid = fork();

    if ( pid > 0 ) { 

         /* родительский */

        close(sv[1]);

        fprintf(stderr, "parent: pid = %d\n", getpid());

        sleep(100);

        exit(0);

     } else {

 

        /* дочерний */

        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-подобных системах.

Схожие статьи

Linux

Действительно ли изменение номера порта по умолчанию повышает безопасность?

Установка Linux и Ubuntu на Mac: пошаговая инструкция
Linux

Установка Linux и Ubuntu на Mac: пошаговая инструкция

Linux

Может ли scp рекурсивно копировать каталоги?

Linux

Как узнать максимальное количество потоков на процесс в Linux?

×