Jak sprawić, by proces potomny umierał po wyjściu rodziców?

209

Załóżmy, że mam proces, który odradza dokładnie jeden proces potomny. Teraz, gdy proces nadrzędny kończy działanie z jakiegokolwiek powodu (normalnie lub nienormalnie, przez zabicie, ^ C, stwierdzenie niepowodzenia lub cokolwiek innego) chcę, aby proces potomny umarł. Jak to zrobić poprawnie?


Kilka podobnych pytań na temat stackoverflow:


Kilka podobnych pytań na temat stackoverflow dla Windows :

Paweł Hajdan
źródło

Odpowiedzi:

189

Dziecko może poprosić jądro o dostarczenie SIGHUP(lub inny sygnał), gdy umiera rodzic, określając opcję PR_SET_PDEATHSIGw prctl()syscall:

prctl(PR_SET_PDEATHSIG, SIGHUP);

Zobacz man 2 prctlszczegóły.

Edycja: dotyczy tylko systemu Linux

qrdl
źródło
5
To kiepskie rozwiązanie, ponieważ rodzic mógł już umrzeć. Warunki wyścigu. Prawidłowe rozwiązanie: stackoverflow.com/a/17589555/412080
Maxim Egorushkin
16
Nazywanie słabej odpowiedzi nie jest zbyt przyjemne - nawet jeśli nie odnosi się do warunków wyścigu. Zobacz moją odpowiedź na temat korzystania prctl()w sposób wolny od wyścigów. Przy okazji odpowiedź połączona przez Maxima jest nieprawidłowa.
maxschlepzig
4
To tylko zły anser. Wyśle sygnał do procesu potomnego w momencie, gdy umiera wątek, który wywołał rozwidlenie, a nie kiedy umiera proces macierzysty.
Lothar,
2
@ Lothar Byłoby miło zobaczyć jakiś dowód. man prctlmówi: Ustaw sygnał śmierci procesu nadrzędnego procesu wywołującego na arg2 (wartość sygnału z zakresu 1..maks. lub 0, aby usunąć). Jest to sygnał, który otrzyma proces wywoływania, gdy jego rodzic umrze. Ta wartość jest usuwana dla potomka rozwidlenia (2) i (od Linuksa 2.4.36 / 2.6.23) podczas wykonywania pliku binarnego set-user-ID lub set-group-ID.
qrdl
1
@maxschlepzig Dzięki za nowy link. Wygląda na to, że poprzedni link jest nieprawidłowy. Nawiasem mówiąc, po latach wciąż nie ma interfejsu API do ustawiania opcji po stronie nadrzędnej. Jaka szkoda.
rox
68

Próbuję rozwiązać ten sam problem, a ponieważ mój program musi działać w systemie OS X, rozwiązanie tylko dla systemu Linux nie zadziałało.

Doszedłem do tego samego wniosku, co inni ludzie na tej stronie - nie ma zgodnego z POSIX sposobu powiadamiania dziecka o śmierci rodzica. Więc wymyśliłem kolejną najlepszą rzecz - przeprowadzenie ankiety dla dzieci.

Kiedy proces nadrzędny umiera (z dowolnego powodu) proces nadrzędny dziecka staje się procesem 1. Jeśli dziecko po prostu okresowo odpytuje, może sprawdzić, czy jego rodzic ma 1. Jeśli tak, dziecko powinno wyjść.

To nie jest świetne, ale działa i jest łatwiejsze niż rozwiązania do sondowania gniazd / blokad plików TCP sugerowane gdzie indziej na tej stronie.

Schof
źródło
6
Doskonałe rozwiązanie Kontynuuje wywoływanie getppid (), aż zwróci 1, a następnie zakończy działanie. To jest dobre i teraz też go używam. Przydałoby się rozwiązanie inne niż pollig. Dziękuję Schof.
neoneye
10
Tylko dla informacji, w Solarisie, jeśli jesteś w strefie, gettpid()nie zmienia się na 1, ale pobiera pidharmonogram (proces zsched) strefy .
Patrick Schlüter,
4
Jeśli ktoś się zastanawia, w systemach Android pid wydaje się być 0 (proces System pid) zamiast 1, gdy umiera rodzic.
Rui Marques
2
Aby mieć bardziej niezawodny i niezależny od platformy sposób robienia tego, przed fork () po prostu getpid (), a jeśli getppid () od potomka jest inny, wyjdź.
Sebastien,
2
To nie działa, jeśli nie kontrolujesz procesu potomnego. Na przykład pracuję nad poleceniem, które otacza find (1), i chcę się upewnić, że znalezisko zostanie zabite, jeśli opakowanie z jakiegoś powodu umrze.
Lucretiel,
34

Osiągnąłem to w przeszłości, uruchamiając „oryginalny” kod w „dziecku” i „spawnowany” kod w „rodzicu” (to znaczy: odwracasz zwykły sens testu po fork()). Następnie pułapkę SIGCHLD w „spawnowanym” kodzie ...

Może nie być możliwe w twoim przypadku, ale urocze, gdy działa.

dmckee --- były kot moderator
źródło
Bardzo fajne rozwiązanie, dzięki! Obecnie akceptowany jest bardziej ogólny, ale twój jest bardziej przenośny.
Paweł Hajdan,
1
Ogromny problem z wykonywaniem pracy w obiekcie nadrzędnym polega na tym, że zmienia się proces nadrzędny. W przypadku serwera, który musi działać „na zawsze”, nie jest to opcja.
Alexis Wilke
29

Jeśli nie możesz zmodyfikować procesu potomnego, możesz spróbować czegoś takiego:

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 */

Spowoduje to uruchomienie potomka z poziomu procesu powłoki z włączoną kontrolą zadań. Proces potomny pojawia się w tle. Powłoka czeka na nową linię (lub EOF), a następnie zabija dziecko.

Kiedy rodzic umiera - bez względu na przyczynę - zamyka koniec rury. Powłoka podrzędna pobierze EOF z odczytu i zabije proces potomny w tle.

Phil Rutschman
źródło
2
Ładne, ale pięć wywołań systemowych, a sh pojawiający się w dziesięciu wierszach kodów pozwala mi nieco sceptycznie podchodzić do tego fragmentu kodu.
Oleiade
+1. Możesz uniknąć dup2przejęcia stdin, używając read -uflagi do odczytu z określonego deskryptora pliku. Dodałem również setpgid(0, 0)dziecko, aby zapobiec jego wyjściu po naciśnięciu ^ C w terminalu.
Greg Hewgill
Kolejność argumentów dup2()połączenia jest niepoprawna. Jeśli chcesz użyć pipes[0]jako standard, musisz dup2(pipes[0], 0)zamiast pisać dup2(0, pipes[0]). To dup2(oldfd, newfd)tam połączenie zamyka poprzednio otwarte newfd.
maxschlepzig
@Oleiade, zgadzam się, tym bardziej, że spawnowany sh robi kolejny widelec, aby wykonać prawdziwy proces potomny ...
maxschlepzig 30.04.16
16

W systemie Linux możesz zainstalować nadrzędny sygnał śmierci u dziecka, np .:

#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 ...

Należy pamiętać, że przechowywanie identyfikatora procesu nadrzędnego przed rozwidleniem i testowanie go w potomku po prctl()wyeliminowaniu warunku wyścigu między prctl()zakończeniem procesu, który wywołał dziecko.

Należy również pamiętać, że sygnał śmierci rodzica dziecka jest usuwany przez nowo utworzone dzieci. Nie ma na to wpływu execve().

Ten test można uprościć, jeśli mamy pewność, że proces systemowy odpowiedzialny za przyjęcie wszystkich sierot ma 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 ...

Jednak poleganie na tym procesie systemowym initi posiadanie PID 1 nie jest przenośne. POSIX.1-2008 określa :

Identyfikator procesu nadrzędnego wszystkich istniejących procesów potomnych i procesów zombie procesu wywołującego zostanie ustawiony na identyfikator procesu procesu systemowego zdefiniowanego w ramach implementacji. Oznacza to, że procesy te będą dziedziczone przez specjalny proces systemowy.

Tradycyjnie proces systemowy przyjmujący wszystkie sieroty to PID 1, tj. Init - który jest przodkiem wszystkich procesów.

W nowoczesnych systemach, takich jak Linux czy FreeBSD, inny proces może mieć taką rolę. Na przykład w systemie Linux proces może zostać wywołany, prctl(PR_SET_CHILD_SUBREAPER, 1)aby ustanowić się jako proces systemowy, który dziedziczy wszystkie sieroty każdego z jego potomków (por. Przykład w Fedorze 25).

maxschlepzig
źródło
Nie rozumiem „Ten test można uprościć, jeśli jesteśmy pewni, że dziadek jest zawsze procesem inicjującym”. Kiedy proces nadrzędny umiera, proces staje się dzieckiem procesu init (pid 1), a nie dzieckiem dziadka, prawda? Test zawsze wydaje się poprawny.
Johannes Schaub - litb
ciekawe, dzięki. chociaż nie rozumiem, co to ma wspólnego z dziadkiem.
Johannes Schaub - litb
1
@ JohannesSchaub-litb, nie zawsze możesz założyć, że osoba sprawująca proces będzie init(8)procesem ... jedyne, co możesz założyć, to to, że kiedy proces nadrzędny umiera, to jego identyfikator nadrzędny zmieni się. Zdarza się to raz w życiu procesu ... i to wtedy, gdy umiera rodzic procesu. Jest tylko jeden główny wyjątek od tej zasady i dotyczy init(8)dzieci, ale jesteś chroniony przed tym, jak init(8)nigdy exit(2)(w tym przypadku jądra panikuje)
Luis Colorado
1
Niestety, jeśli dziecko rozwiąże wątek, a następnie wyjście z wątku, proces potomny otrzyma SIGTERM.
rox
14

Dla kompletności. W systemie macOS możesz użyć 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);
}
neoneye
źródło
Możesz to zrobić za pomocą nieco ładniejszego API, używając źródeł wysyłki z DISPATCH_SOURCE_PROC i PROC_EXIT.
Russbishop
Z jakiegokolwiek powodu powoduje to, że mój komputer Mac wpada w panikę. Uruchomienie procesu z tym kodem ma około 50% szansy na jego zawieszenie, powodując, że fani obracają się w tempie, którego nigdy wcześniej nie słyszałem (superszybko), a potem Mac się wyłącza. BĄDŹ BARDZO OSTROŻNY Z TYM KODEM .
Qix - MONICA MISTREATEDED
Wygląda na to, że w moim systemie macOS proces potomny kończy się automatycznie po wyjściu z rodzica. Nie wiem dlaczego.
Yi Lin Liu
@YiLinLiu iirc I używany NSTasklub posix spawn. Zobacz startTaskfunkcję w moim kodzie tutaj: github.com/neoneye/newton-commander-browse/blob/master/Classes/…
neoneye
11

Czy proces potomny ma potok do / z procesu macierzystego? Jeśli tak, otrzymasz SIGPIPE podczas pisania lub otrzymasz EOF podczas czytania - te warunki można wykryć.

MarkR
źródło
1
Odkryłem, że nie stało się to niezawodnie, przynajmniej w OS X.
Schof
Przyszedłem tutaj, aby dodać tę odpowiedź. To zdecydowanie rozwiązanie, którego użyłbym w tym przypadku.
Dolda2000
Uwaga: systemd domyślnie wyłącza SIGPIPE w usługach, którymi zarządza, ale nadal możesz sprawdzić zamknięcie potoku. Zobacz freedesktop.org/software/systemd/man/systemd.exec.html pod IgnoreSIGPIPE
jdizzle
11

Zainspirowany inną odpowiedzią tutaj, wymyśliłem następujące rozwiązanie all-POSIX. Ogólna idea polega na utworzeniu pośredniego procesu między rodzicem a dzieckiem, który ma jeden cel: Powiadom, kiedy rodzic umiera, i wyraźnie zabij dziecko.

Ten rodzaj rozwiązania jest przydatny, gdy nie można zmodyfikować kodu w obiekcie potomnym.

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

Istnieją dwie małe zastrzeżenia dotyczące tej metody:

  • Jeśli celowo zabijesz proces pośredni, dziecko nie zostanie zabite, gdy rodzic umrze.
  • Jeśli dziecko wyjdzie przed rodzicem, wówczas proces pośredni będzie próbował zabić pierwotny podrzędny pid, co może teraz odnosić się do innego procesu. (Można to naprawić, dodając więcej kodu w procesie pośrednim).

Nawiasem mówiąc, faktyczny kod, którego używam, znajduje się w Pythonie. Tutaj jest dla kompletności:

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)
Greg Hewgill
źródło
Zauważ, że jakiś czas temu, w systemie IRIX, użyłem schematu nadrzędny / podrzędny, w którym miałem między nimi potok, a czytanie z potoku generowało POWIĘKSZENIE, jeśli jedno z nich umarło. W ten sposób zabijałem dzieci z widelca (), bez potrzeby pośredniego procesu.
Alexis Wilke
Myślę, że twoje drugie zastrzeżenie jest złe. Identyfikator pid dziecka jest zasobem należącym do jego rodzica i nie można go zwolnić / ponownie użyć, dopóki rodzic (proces pośredni) nie poczeka na nim (lub zakończy działanie i pozwoli na to poczekać init).
R .. GitHub ZATRZYMAJ LÓD
7

Nie sądzę, że można zagwarantować, że używa się tylko standardowych wywołań POSIX. Podobnie jak w prawdziwym życiu, po odrodzeniu dziecko ma swoje własne życie.

To jest możliwe, proces nadrzędny złapać większość możliwych zdarzeń terminacji i próbować zabić procesu potomnego w tym momencie, ale zawsze jest pewne, że nie można złapać.

Na przykład żaden proces nie może złapać SIGKILL. Kiedy jądro obsługuje ten sygnał, zabije określony proces bez żadnego powiadomienia o tym procesie.

Aby rozszerzyć analogię - jedynym innym standardowym sposobem jest popełnienie samobójstwa przez dziecko, gdy stwierdzi, że nie ma już rodzica.

Można to zrobić tylko na Linuksie prctl(2)- zobacz inne odpowiedzi.

Alnitak
źródło
6

Jak zauważyli inni ludzie, poleganie na pid rodzica, który ma wartość 1, gdy rodzic wychodzi, jest nieprzenośne. Zamiast czekać na określony identyfikator procesu nadrzędnego, wystarczy poczekać na zmianę identyfikatora:

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

Dodaj mikro-sen według potrzeb, jeśli nie chcesz sondować z pełną prędkością.

Ta opcja wydaje mi się prostsza niż używanie potoku lub poleganie na sygnałach.

użytkownik2168915
źródło
Niestety to rozwiązanie nie jest niezawodne. Co się stanie, jeśli proces nadrzędny umrze przed uzyskaniem wartości początkowej? Dziecko nigdy nie wyjdzie.
dgatwood
@dgatwood, co masz na myśli?!? Pierwszy getpid()odbywa się u rodzica przed wywołaniem fork(). Jeśli rodzic umrze wcześniej, dziecko nie istnieje. To, co może się zdarzyć, to dziecko przez jakiś czas żyjące z rodzicem.
Alexis Wilke
W tym nieco wymyślonym przykładzie działa, ale w kodzie rzeczywistym po rozwidleniu prawie zawsze następuje exec, a nowy proces musi zacząć od nowa, prosząc o jego PPID. W okresie między tymi dwoma czekami, jeśli rodzic odejdzie, dziecko nie miałoby pojęcia. Ponadto jest mało prawdopodobne, abyś miał kontrolę zarówno nad kodem nadrzędnym, jak i podrzędnym (w przeciwnym razie możesz po prostu przekazać PPID jako argument). Jako ogólne rozwiązanie to podejście nie działa zbyt dobrze. I realistycznie, gdyby system operacyjny podobny do UNIX-a wyszedł bez inicjacji 1, tak wiele rzeczy by się popsuło, że nie wyobrażam sobie, żeby ktokolwiek to robił.
dgatwood
pass parent pid jest argumentem wiersza poleceń podczas wykonywania exec dla dziecka.
Nish
2
Sondowanie przy pełnej prędkości jest szalone.
maxschlepzig
4

Zainstaluj moduł obsługi pułapek, aby złapać SIGINT, który zabija proces potomny, jeśli nadal żyje, chociaż inne plakaty mają rację, że nie złapie SIGKILL.

Otwórz plik .lock z wyłącznym dostępem i poproś, aby sondowanie potomne próbowało go otworzyć - jeśli otwarcie się powiedzie, proces potomny powinien wyjść

Ana Betts
źródło
Lub dziecko może otworzyć plik blokujący w osobnym wątku, w trybie blokowania, w którym to przypadku może to być całkiem miłe i czyste rozwiązanie. Prawdopodobnie ma jednak pewne ograniczenia w zakresie przenoszenia.
Jean
4

To rozwiązanie działało dla mnie:

  • Przekaż stdin potok do dziecka - nie musisz zapisywać żadnych danych w strumieniu.
  • Dziecko czyta w nieskończoność od standardowego do EOF. EOF sygnalizuje, że rodzic zniknął.
  • Jest to niezawodny i przenośny sposób na wykrycie odejścia rodzica. Nawet jeśli rodzic ulegnie awarii, system operacyjny zamknie potok.

Dotyczyło to procesu typu robotniczego, którego istnienie miało sens tylko wtedy, gdy rodzic żył.

joonas.fi
źródło
@SebastianJylanki Nie pamiętam, czy próbowałem, ale prawdopodobnie działa, ponieważ prymitywy (strumienie POSIX) są dość standardowe we wszystkich systemach operacyjnych.
joonas.fi
3

Myślę, że szybkim i brudnym sposobem jest stworzenie rury między dzieckiem a rodzicem. Kiedy rodzic wyjdzie, dzieci otrzymają SIGPIPE.

Yefei
źródło
SIGPIPE nie jest wysyłany przy zamknięciu potoku, jest wysyłany tylko wtedy, gdy dziecko próbuje do niego napisać.
Alcaro,
3

Niektóre plakaty wspominały już o fajkach i kqueue. W rzeczywistości możesz również stworzyć parę połączonych gniazd domeny Unix przez socketpair()połączenie. Typ gniazda powinien być SOCK_STREAM.

Załóżmy, że masz dwa deskryptory plików gniazda fd1, fd2. Teraz, fork()aby utworzyć proces potomny, który odziedziczy fds. W rodzicu zamykasz Fd2, au dziecka zamykasz Fd1. Teraz każdy proces może poll()pozostać otwarty fd na swoim końcu dla POLLINzdarzenia. Tak długo, jak każda strona nie ma wyraźnie close()swojego fd podczas normalnego życia, możesz być całkiem pewien, że POLLHUPflaga powinna wskazywać zakończenie drugiej strony (bez względu na to, czy jest czyste czy nie). Po powiadomieniu o tym zdarzeniu dziecko może zdecydować, co robić (np. Umrzeć).

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

Możesz spróbować skompilować powyższy kod proof-of-concept i uruchomić go w podobnym terminalu ./a.out &. Masz około 100 sekund na eksperymentowanie z zabijaniem macierzystego PID różnymi sygnałami, albo po prostu wyjdzie. W obu przypadkach powinien zostać wyświetlony komunikat „dziecko: rodzic rozłączył się”.

W porównaniu z metodą używającą modułu SIGPIPEobsługi ta metoda nie wymaga próby write()wywołania.

Ta metoda jest również symetryczna , tzn. Procesy mogą używać tego samego kanału do wzajemnego monitorowania swojego istnienia.

To rozwiązanie wywołuje tylko funkcje POSIX. Próbowałem tego w systemie Linux i FreeBSD. Myślę, że to powinno działać na innych Uniksach, ale tak naprawdę nie testowałem.

Zobacz też:

  • unix(7)stron man Linux, unix(4)FreeBSD, poll(2), socketpair(2), socket(7)w systemie Linux.
Cong Ma
źródło
Bardzo fajnie, naprawdę zastanawiam się, czy to ma jakieś problemy z niezawodnością. Czy przetestowałeś to w produkcji? Z różnymi aplikacjami?
Aktau
@Aktau, używałem odpowiednika tej sztuczki w języku Python w programie dla systemu Linux. Potrzebowałem tego, ponieważ logika pracy dziecka polega na tym, że „najlepiej jak najlepiej wyczyścić po wyjściu rodzica, a następnie również wyjściu”. Jednak tak naprawdę nie jestem pewien co do innych platform. Fragment C działa w systemach Linux i FreeBSD, ale to wszystko, co wiem ... Są też przypadki, w których powinieneś być ostrożny, na przykład nadrzędny rozwidlenie lub nadrzędny rezygnacja z fd przed prawdziwym wyjściem (tworząc w ten sposób okno czasowe dla warunki wyścigu).
Cong Ma
@Aktau - To będzie całkowicie niezawodne.
Wszechobecny
1

Zgodnie z POSIX , to exit(), _exit()i _Exit()funkcje są zdefiniowane:

  • Jeżeli proces jest procesem kontrolnym, sygnał SIGHUP zostanie wysłany do każdego procesu w grupie procesów pierwszego planu terminala sterującego należącego do procesu wywołującego.

Jeśli więc ustawisz, że proces nadrzędny jest procesem kontrolnym dla jego grupy procesów, dziecko powinno otrzymać sygnał SIGHUP, gdy rodzic wyjdzie. Nie jestem absolutnie pewien, że tak się stanie, gdy rodzic się zawiesi, ale myślę, że tak się dzieje. Z pewnością w przypadku spraw niezwiązanych z awariami powinno działać dobrze.

Należy pamiętać, że może trzeba przeczytać sporo drobnym drukiem - w tym dziale Baza Definicje (Definicje), jak również informacje systemowe Usługi dla exit()a setsid()i setpgrp()- aby uzyskać pełny obraz. (Więc mógłbym!)

Jonathan Leffler
źródło
3
Hmm Dokumentacja jest w tym względzie niejasna i sprzeczna, ale wydaje się, że proces nadrzędny musi być procesem wiodącym sesji, a nie tylko grupą procesów. Wiodącym procesem dla sesji był zawsze login, a przejmowanie mojego procesu jako procesu wiodącego dla nowej sesji w tej chwili przekraczało moje możliwości.
Schof
2
SIGHUP skutecznie jest wysyłany do procesów potomnych tylko wtedy, gdy wychodzącym procesem jest powłoka logowania. opengroup.org/onlinepubs/009695399/functions/exit.html „Zakończenie procesu nie kończy bezpośrednio jego potomków. Wysłanie sygnału SIGHUP, jak opisano poniżej, pośrednio kończy dzieci / w niektórych okolicznościach /.”
Rob K
1
@Rob: poprawne - tak mówi również cytowany przeze mnie cytat: że tylko w niektórych okolicznościach proces potomny otrzymuje PODSUMOWANIE. Mówienie, że jest to tylko powłoka logowania, która wysyła SIGHUP, jest zdecydowanie nadmiernym uproszczeniem, chociaż jest to najczęstszy przypadek. Jeśli proces z wieloma potomkami zostanie ustawiony jako proces kontrolujący dla niego i jego dzieci, wówczas SIGHUP zostanie (dogodnie) wysłany do swoich dzieci, gdy mistrz umrze. OTOH, procesy rzadko sprawiają tyle kłopotów - więc bardziej wybieram nitki niż podnoszę naprawdę znaczący spór.
Jonathan Leffler,
2
Wygłupiałem się z nim przez kilka godzin i nie mogłem go uruchomić. Ładnie poradziłoby sobie ze sprawą, w której mam demona z niektórymi dziećmi, które wszystkie muszą umrzeć, gdy rodzic wyjdzie.
Rob K
1

Jeśli wysyłasz sygnał do pid 0, używając na przykład

kill(0, 2); /* SIGINT */

sygnał ten jest wysyłany do całej grupy procesów, co skutecznie zabija dziecko.

Możesz to łatwo przetestować za pomocą czegoś takiego:

(cat && kill 0) | python

Jeśli następnie naciśniesz ^ D, zobaczysz tekst "Terminated"jako wskazówkę, że interpreter Pythona rzeczywiście został zabity, a nie tylko zakończony z powodu zamknięcia standardowego wejścia.

Thorbiörn Fritzon
źródło
1
(echo -e "print(2+2)\n" & kill 0) | sh -c "python -" szczęśliwie drukuje 4 zamiast Terminated
Kamil Szot,
1

W przypadku, gdy ma to znaczenie dla kogokolwiek innego, kiedy spawnuję instancje JVM w rozwidlonych procesach potomnych z C ++, jedynym sposobem, aby uzyskać prawidłowe zakończenie instancji JVM po zakończeniu procesu nadrzędnego, było wykonanie następujących czynności. Mam nadzieję, że ktoś może przekazać opinię w komentarzach, jeśli nie byłby to najlepszy sposób.

1) Wywołaj prctl(PR_SET_PDEATHSIG, SIGHUP)rozwidlony proces potomny zgodnie z sugestią przed uruchomieniem aplikacji Java za pomocą execv, i

2) Dodaj hak zamykający do aplikacji Java, która odpytuje, aż jej nadrzędny PID wyniesie 1, a następnie wykonaj trudne Runtime.getRuntime().halt(0). Odpytywanie odbywa się poprzez uruchomienie osobnej powłoki, która uruchamia pspolecenie (patrz: Jak znaleźć mój PID w Javie lub JRuby w Linuksie? ).

EDYCJA 130118:

Wygląda na to, że nie było to solidne rozwiązanie. Wciąż trochę próbuję zrozumieć niuanse tego, co się dzieje, ale czasami czasami otrzymywałem osierocone procesy JVM podczas uruchamiania tych aplikacji w sesjach screen / SSH.

Zamiast odpytywania o PPID w aplikacji Java, po prostu kazałem hakowi zamykającemu wykonać czyszczenie, po czym nastąpiło twarde zatrzymanie, jak powyżej. Potem upewniłem się, że wywołałem waitpidw aplikacji nadrzędnej C ++ w odrodzonym procesie potomnym, kiedy nadszedł czas, aby zakończyć wszystko. Wydaje się, że jest to bardziej niezawodne rozwiązanie, ponieważ proces potomny zapewnia jego zakończenie, podczas gdy rodzic używa istniejących referencji, aby upewnić się, że jego potomki się kończą. Porównaj to z poprzednim rozwiązaniem, w którym proces nadrzędny zakończył się, gdy tylko zechce, a dzieci próbowały dowiedzieć się, czy zostały osierocone przed zakończeniem.

jasterm007
źródło
1
PID equals 1Czekanie nie jest ważna. Nowym rodzicem może być inny PID. Powinieneś sprawdzić, czy zmienia się z pierwotnego elementu nadrzędnego (getpid () przed rozwidleniem ()) na nowego elementu nadrzędnego (getppid () w dziecku nie jest równy getpid () po wywołaniu przed rozwidleniem ()).
Alexis Wilke
1

Innym sposobem na to, specyficznym dla Linuksa, jest utworzenie elementu nadrzędnego w nowej przestrzeni nazw PID. Będzie to PID 1 w tej przestrzeni nazw, a kiedy wyjdzie, wszystkie dzieci zostaną natychmiast zabite SIGKILL.

Niestety, aby utworzyć nową przestrzeń nazw PID, musisz mieć CAP_SYS_ADMIN. Ale ta metoda jest bardzo skuteczna i nie wymaga żadnej realnej zmiany dla rodzica ani dzieci poza początkowym uruchomieniem rodzica.

Zobacz clone (2) , pid_namespaces (7) i unshare (2) .

Wszelaki
źródło
Muszę edytować w inny sposób. Można użyć programu prctl, aby proces działał jako proces inicjujący dla wszystkich dzieci i wnuków oraz prawnuków itp.
Omnifarious
0

Jeśli rodzic umiera, PPID sierot zmienia się na 1 - wystarczy tylko sprawdzić swój własny PPID. W pewnym sensie jest to odpytywanie, wspomniane powyżej. oto kawałek skorupy do tego:

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
alex K
źródło
0

Znalazłem 2 rozwiązania, oba nie są idealne.

1. Zabij wszystkie dzieci zabijając (-pid) po otrzymaniu sygnału SIGTERM.
Oczywiście to rozwiązanie nie obsługuje „zabicia -9”, ale działa w większości przypadków i jest bardzo proste, ponieważ nie musi pamiętać wszystkich procesów potomnych.


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

W ten sam sposób możesz zainstalować program obsługi wyjścia tak jak powyżej, jeśli gdzieś wywołasz proces.exit. Uwaga: Ctrl + C i nagła awaria zostały automatycznie przetworzone przez system operacyjny w celu zabicia grupy procesów, więc nie więcej tutaj.

2. Użyj chjj / pty.js, aby spawnować proces z podłączonym terminalem kontrolnym.
Kiedy zabijesz obecny proces, a nawet zabijesz -9, wszystkie procesy potomne również zostaną automatycznie zabite (przez system operacyjny?). Wydaje mi się, że ponieważ bieżący proces ma inną stronę terminala, więc jeśli aktualny proces umrze, proces potomny otrzyma SIGPIPE i tak umrze.


    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(.....);
    */
osexp2003
źródło
0

Udało mi się stworzyć przenośne rozwiązanie bez odpytywania z 3 procesami, nadużywając kontroli terminali i sesji. To jest masturbacja mentalna, ale działa.

Sztuką jest:

  • proces A został uruchomiony
  • proces A tworzy potok P (i nigdy z niego nie czyta)
  • proces A przechodzi do procesu B
  • proces B tworzy nową sesję
  • proces B przydziela terminal wirtualny dla tej nowej sesji
  • proces B instaluje moduł obsługi SIGCHLD, aby umierał, gdy dziecko kończy pracę
  • proces B ustawia moduł obsługi SIGPIPE
  • proces B przechodzi do procesu C
  • proces C robi wszystko, czego potrzebuje (np. exec () s niezmodyfikowanego pliku binarnego lub uruchamia dowolną logikę)
  • proces B zapisuje do potoku P (i blokuje w ten sposób)
  • proces A wait () s na procesie B i kończy działanie po jego śmierci

W ten sposób:

  • jeśli proces A umiera: proces B otrzymuje SIGPIPE i umiera
  • jeśli proces B umiera: proces A czeka () powraca i umiera, proces C otrzymuje SIGHUP (ponieważ gdy lider sesji sesji z terminalem podłączonym umiera, wszystkie procesy w grupie procesów pierwszego planu otrzymują SIGHUP)
  • jeśli proces C umiera: proces B otrzymuje SIGCHLD i umiera, więc proces A umiera

Niedociągnięcia:

  • proces C nie może obsłużyć SIGHUP
  • proces C zostanie uruchomiony w innej sesji
  • proces C nie może korzystać z interfejsu API sesji / grupy procesów, ponieważ spowoduje to uszkodzenie kruchej konfiguracji
  • utworzenie terminala dla każdej takiej operacji nie jest najlepszym pomysłem
Marek Dopiera
źródło
0

Mimo, że minęło 7 lat, właśnie natknąłem się na ten problem, ponieważ uruchamiam aplikację SpringBoot, która musi uruchamiać webpack-dev-server podczas programowania i musi go zabić po zatrzymaniu procesu zaplecza.

Próbuję użyć, Runtime.getRuntime().addShutdownHookale działało to w systemie Windows 10, ale nie w systemie Windows 7.

Zmieniłem go, aby używał dedykowanego wątku, który czeka na zakończenie procesu lub dla InterruptedExceptionktórego wydaje się działać poprawnie w obu wersjach systemu 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();
}
Ido Ran
źródło
0

Historycznie, od UNIX v7, system procesów wykrył osierocenie procesów poprzez sprawdzenie nadrzędnego identyfikatora procesu. Jak mówię, historycznie init(8)proces systemowy jest procesem specjalnym tylko z jednego powodu: nie może umrzeć. Nie może umrzeć, ponieważ algorytm jądra zajmujący się przypisywaniem nowego identyfikatora procesu nadrzędnego zależy od tego faktu. kiedy proces wykonuje swoje exit(2)wywołanie (za pomocą wywołania systemowego procesu lub zadania zewnętrznego, wysyłając mu sygnał itp.), jądro ponownie przypisuje wszystkim elementom potomnym tego procesu identyfikator procesu inicjującego jako identyfikator procesu nadrzędnego. Prowadzi to do najłatwiejszego testu i najbardziej przenośnego sposobu sprawdzenia, czy proces został osierocony. Wystarczy sprawdzić wynik getppid(2)wywołania systemowego i czy jest to identyfikator procesuinit(2) proces, a następnie proces został osierocony przed wywołaniem systemowym.

Z tego podejścia wynikają dwa problemy, które mogą prowadzić do problemów:

  • po pierwsze, mamy możliwość zmiany initprocesu na dowolny proces użytkownika, więc w jaki sposób możemy zapewnić, że proces init zawsze będzie nadrzędny dla wszystkich procesów osieroconych? Cóż, w exitkodzie wywołania systemowego jest wyraźne sprawdzenie, czy proces wykonujący wywołanie jest procesem init (proces z pid równym 1), a jeśli tak jest, jądro wpadnie w panikę (nie powinno już być w stanie utrzymać hierarchia procesów), więc proces inicjujący nie może wykonywać exit(2)połączenia.
  • po drugie, w podstawowym teście ujawniono powyżej warunki wyścigu. Historycznie zakłada się 1, że identyfikator procesu początkowego jest , ale nie jest to uzasadnione w podejściu POSIX, który stwierdza (ujawniony w innej odpowiedzi), że tylko identyfikator procesu systemu jest zarezerwowany do tego celu. Prawie żadna implementacja posixa tego nie robi i można założyć w oryginalnych systemach uniksowych, że 1jako odpowiedź na getppid(2)wywołanie systemowe wystarczy, aby założyć, że proces jest osierocony. Innym sposobem sprawdzenia jest wykonanie getppid(2)tuż po rozwidleniu i porównanie tej wartości z wynikiem nowego wywołania. To po prostu nie działa we wszystkich przypadkach, ponieważ oba wywołania nie są atomowe razem, a proces nadrzędny może umrzeć po fork(2)pierwszym getppid(2)wywołaniu systemowym i przed nim . parent id only changes once, when its parent does anWyjście procesu (2) call, so this should be enough to check if thegetppid (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 ofinit (8) `, ale możesz bezpiecznie założyć, że te procesy również nie mają rodzica (z wyjątkiem sytuacji, gdy w systemie zastąpisz proces init)
Luis Colorado
źródło
-1

Przekazałem rodzicowi pid za pomocą środowiska dla dziecka, a następnie okresowo sprawdzałem, czy / proc / $ ppid istnieje od dziecka.

Siergiej Kirjanow
źródło