2016-11-08 18 views
6

Jest to kontynuacja How to prevent SIGINT in child process from propagating to and killing parent process?Wywołanie zabitych w procesie potomnym z SIGTERM kończy proces nadrzędny, ale nazywając go SIGKILL utrzymuje przy życiu rodzica

W powyższym pytaniu, dowiedziałem się, że nie był SIGINT pęcherzykami się z dziecko do rodzica, ale raczej jest wysyłane do całej grupy procesów na pierwszym planie, co oznacza, że ​​musiałem napisać procedurę obsługi sygnału, aby uniemożliwić rodzicowi wyjście, gdy uderzę CTRL + C.

Próbowałem to zaimplementować, ale tu jest problem. Odnośnie systemu operacyjnego kill, które wywołuję, aby zakończyć dziecko, jeśli przejdę w SIGKILL, wszystko działa zgodnie z oczekiwaniami, ale jeśli przejdę w SIGTERM, to również kończy proces nadrzędny, wyświetlając w wierszu poleceń później komunikat Terminated: 15.

Mimo że SIGKILL działa, chcę użyć SIGTERM, ponieważ wydaje się być lepszym pomysłem z tego, co przeczytałem na ten temat, dając proces, który sygnalizuje, by zakończyć szansę na oczyszczenie.

Poniższy kod jest okrojona przykładem tego, co wymyśliłem

#include <stdio.h> 
#include <signal.h> 
#include <stdlib.h> 
#include <unistd.h> 

pid_t CHILD = 0; 
void handle_sigint(int s) { 
    (void)s; 
    if (CHILD != 0) { 
    kill(CHILD, SIGTERM); // <-- SIGKILL works, but SIGTERM kills parent 
    CHILD = 0; 
    } 
} 

int main() { 
    // Set up signal handling 
    char str[2]; 
    struct sigaction sa = { 
    .sa_flags = SA_RESTART, 
    .sa_handler = handle_sigint 
    }; 
    sigaction(SIGINT, &sa, NULL); 

    for (;;) { 
    printf("1) Open SQLite\n" 
      "2) Quit\n" 
      "-> " 
     ); 
    scanf("%1s", str); 
    if (str[0] == '1') { 
     CHILD = fork(); 
     if (CHILD == 0) { 
     execlp("sqlite3", "sqlite3", NULL); 
     printf("exec failed\n"); 
     } else { 
     wait(NULL); 
     printf("Hi\n"); 
     } 
    } else if (str[0] == '2') { 
     break; 
    } else { 
     printf("Invalid!\n"); 
    } 
    } 
} 

My zgaduje, dlaczego tak się dzieje byłoby coś przechwytuje SIGTERM i zabija całą grupę procesów. Podczas gdy używam SIGKILL, nie mogę przechwycić sygnału, więc moje wywołanie zabójstwa działa zgodnie z oczekiwaniami. To tylko ukłucie w ciemności.

Czy ktoś mógłby wyjaśnić, dlaczego tak się dzieje?

Jak zauważyłem, nie jestem zachwycony moją funkcją handle_sigint. Czy istnieje bardziej standardowy sposób zabijania interaktywnego procesu potomnego?

+2

Dobrze zadane pytanie, że jestem zbyt śpiący, żeby się bawić. Zobacz także, jak maski sygnałowe są propagowane z rodzica na dziecko; zasady są tak skomplikowane, że musiałem je każdorazowo wyszukiwać. Twoja hipoteza "nie może przechwycić" jest dobra, ale prawdopodobnie błędna, ponieważ jądro tasuje sygnały w oparciu o to, o co prosiła; procesy nie uczestniczą. – msw

+1

Byłbym bardzo zaskoczony, gdyby sqlite nie uruchomił własnej grupy procesów i gwarantuję, że zmienia ona własną maskę sygnału. – msw

+2

@msw: Nie przejmowałem się sprawdzaniem ze źródeł, ale używając mojego przykładowego programu poniżej, mogę stwierdzić, że 'sqlite3' nie blokuje' HUP', 'TERM',' QUIT', 'USR1' lub' Sygnały USR2'; i wygląda na to, że 'INT' jest obsługiwane lub ignorowane (tak jak jest pokazane na terminalu jako'^C', jeśli wysłane przez 'kill' gdzie indziej). Pamiętaj, że zawsze możesz użyć np. 'kill -HUP $ (ps-sqlite3 -o pid =)' aby wysłać sygnał HUP do wszystkich uruchomionych procesów 'sqlite3'; Właśnie tego użyłem do testowania. –

Odpowiedz

9

Masz zbyt wiele błędów w swoim kodzie (nie kasując maski sygnału na struct sigaction), aby każdy mógł wyjaśnić efekty, które widzisz.

Zamiast tego, należy rozważyć następującą roboczą przykładowy kod, powiedzmy example.c:

#define _POSIX_C_SOURCE 200809L 
#include <stdlib.h> 
#include <unistd.h> 
#include <sys/types.h> 
#include <sys/wait.h> 
#include <signal.h> 
#include <string.h> 
#include <stdio.h> 
#include <errno.h> 

/* Child process PID, and atomic functions to get and set it. 
* Do not access the internal_child_pid, except using the set_ and get_ functions. 
*/ 
static pid_t internal_child_pid = 0; 
static inline void set_child_pid(pid_t p) { __atomic_store_n(&internal_child_pid, p, __ATOMIC_SEQ_CST); } 
static inline pid_t get_child_pid(void) { return __atomic_load_n(&internal_child_pid, __ATOMIC_SEQ_CST); } 

static void forward_handler(int signum, siginfo_t *info, void *context) 
{ 
    const pid_t target = get_child_pid(); 

    if (target != 0 && info->si_pid != target) 
     kill(target, signum); 
} 

static int forward_signal(const int signum) 
{ 
    struct sigaction act; 

    memset(&act, 0, sizeof act); 
    sigemptyset(&act.sa_mask); 
    act.sa_sigaction = forward_handler; 
    act.sa_flags = SA_SIGINFO | SA_RESTART; 

    if (sigaction(signum, &act, NULL)) 
     return errno; 

    return 0; 
} 

int main(int argc, char *argv[]) 
{ 
    int status; 
    pid_t p, r; 

    if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) { 
     fprintf(stderr, "\n"); 
     fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]); 
     fprintf(stderr, "  %s COMMAND [ ARGS ... ]\n", argv[0]); 
     fprintf(stderr, "\n"); 
     return EXIT_FAILURE; 
    } 

    /* Install signal forwarders. */ 
    if (forward_signal(SIGINT) || 
     forward_signal(SIGHUP) || 
     forward_signal(SIGTERM) || 
     forward_signal(SIGQUIT) || 
     forward_signal(SIGUSR1) || 
     forward_signal(SIGUSR2)) { 
     fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno)); 
     return EXIT_FAILURE; 
    } 

    p = fork(); 
    if (p == (pid_t)-1) { 
     fprintf(stderr, "Cannot fork(): %s.\n", strerror(errno)); 
     return EXIT_FAILURE; 
    } 

    if (!p) { 
     /* Child process. */ 

     execvp(argv[1], argv + 1); 

     fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno)); 
     return EXIT_FAILURE; 
    } 

    /* Parent process. Ensure signals are reflected. */   
    set_child_pid(p); 

    /* Wait until the child we created exits. */ 
    while (1) { 
     status = 0; 
     r = waitpid(p, &status, 0); 

     /* Error? */ 
     if (r == -1) { 
      /* EINTR is not an error. Occurs more often if 
       SA_RESTART is not specified in sigaction flags. */ 
      if (errno == EINTR) 
       continue; 

      fprintf(stderr, "Error waiting for child to exit: %s.\n", strerror(errno)); 
      status = EXIT_FAILURE; 
      break; 
     } 

     /* Child p exited? */ 
     if (r == p) { 
      if (WIFEXITED(status)) { 
       if (WEXITSTATUS(status)) 
        fprintf(stderr, "Command failed [%d]\n", WEXITSTATUS(status)); 
       else 
        fprintf(stderr, "Command succeeded [0]\n"); 
      } else 
      if (WIFSIGNALED(status)) 
       fprintf(stderr, "Command exited due to signal %d (%s)\n", WTERMSIG(status), strsignal(WTERMSIG(status))); 
      else 
       fprintf(stderr, "Command process died from unknown causes!\n"); 
      break; 
     } 
    } 

    /* This is a poor hack, but works in many (but not all) systems. 
     Instead of returning a valid code (EXIT_SUCCESS, EXIT_FAILURE) 
     we return the entire status word from the child process. */ 
    return status; 
} 

skompilować go za pomocą np

gcc -Wall -O2 example.c -o example 

i uruchomić za pomocą np.

./example sqlite3 

Zauważysz, że Ctrl + C nie przerywa sqlite3 - ale potem znowu, to nie robi nawet jeśli były do ​​uruchomienia sqlite3 bezpośrednio -; zamiast tego po prostu widzisz na ekranie ^C. Wynika to z faktu, że sqlite3 ustawia terminal tak, aby nie powodował sygnału, i jest interpretowany jako normalny sygnał wejściowy.

Można wyjść z sqlite3 pomocą polecenia .quit lub naciskając Ctrl + D na początku wiersza.

Zobaczysz, że oryginalny program wyświetli następnie linię Command ... [], zanim powróci do linii poleceń. Zatem proces macierzysty nie jest zabijany/uszkadzany/niepokojony przez sygnały.

Możesz użyć ps f, aby przyjrzeć się drzewu procesów terminalowych i w ten sposób dowiedzieć się PID procesów rodzica i dziecka, i wysłać sygnały do ​​jednego, aby obserwować, co się dzieje.

Zauważ, że ponieważ SIGSTOP sygnał nie może zostać złapany, zablokowane lub ignorowane, byłoby nietrywialna odzwierciedlać sygnały sterujące robota (jak w przypadku korzystania Ctrl + Z). W celu prawidłowej kontroli zadań proces nadrzędny musiałby ustawić nową sesję i grupę procesów i tymczasowo odłączyć się od terminala. To również jest całkiem możliwe, ale nieco wykracza poza zakres zastosowania, ponieważ wymaga dość szczegółowego zachowania sesji, grup procesów i terminali w celu prawidłowego zarządzania.

Zdekonstruuj powyższy przykładowy program.

Sam program przykładowy najpierw instaluje niektóre reflektory sygnałowe, następnie wyświetla proces potomny, a proces potomny wykonuje polecenie sqlite3. (Można speficy dowolny plik wykonywalny i wszelkie parametry struny do programu).

zmiennej internal_child_pid i set_child_pid() i get_child_pid() funkcje, są wykorzystywane do zarządzania procesem dziecko atomowo. __atomic_store_n() i __atomic_load_n() są wbudowanymi kompilatorami; dla GCC, see here w celu uzyskania szczegółowych informacji. Unikają problemu z sygnałem, który pojawia się, gdy pid dziecka jest tylko częściowo przypisany. W niektórych typowych architekturach nie może się to zdarzyć, ale jest to uważany za staranny przykład, więc dostępy do atomów są używane w celu zapewnienia, że ​​tylko całkowita (stara lub nowa) wartość jest widoczna. Możemy całkowicie ich nie używać, jeśli zamiast tego tymczasowo zablokujemy powiązane sygnały podczas przejścia. Ponownie zdecydowałem, że dostęp atomowy jest prostszy i może być interesujący w praktyce.

Funkcja forward_handler() uzyskuje proces PID procesu potomnego atomicznie, następnie sprawdza, czy jest niezerowa (że wiemy, że mamy proces potomny) i że nie przekazujemy sygnału wysłanego przez proces potomny (tylko po to, aby upewnić się, że nie działamy) • spowodować burzę sygnałową, bombardowanie się sygnałami). Różne pola w strukturze siginfo_t są wymienione na stronie podręcznika użytkownika man 2 sigaction.

Funkcja forward_signal() instaluje powyższy moduł obsługi dla określonego sygnału signum. Zauważ, że najpierw używamy memset(), aby usunąć całą strukturę do zera. Wyczyszczenie go w ten sposób zapewnia zgodność w przyszłości, jeśli część wypełnienia w strukturze zostanie przekształcona w pola danych.

Pole .sa_mask w struct sigaction jest nieuporządkowanym zestawem sygnałów. Sygnały ustawione w masce są blokowane przed dostarczeniem w wątku, który wykonuje procedurę obsługi sygnału. (Dla powyższego programu przykładowego możemy bezpiecznie powiedzieć, że te sygnały są blokowane podczas działania programu obsługi sygnału, po prostu w programach wielowątkowych sygnały są blokowane tylko w określonym wątku używanym do uruchomienia programu obsługi.)

Ważne jest użycie sigemptyset(&act.sa_mask), aby wyczyścić maskę sygnału. Samo ustawienie zerowej struktury nie jest wystarczające, nawet jeśli działa (prawdopodobnie) w praktyce na wielu maszynach. (Nie wiem, nawet nie sprawdziłem, wolę solidne i niezawodne przez leniwy i kruchy każdego dnia!)

Używane flagi obejmują SA_SIGINFO, ponieważ przewodnik używa formularza trzyargumentowego (i używa pola si_pid z). SA_RESTART flaga jest tylko tam, ponieważ OP chciał z niej skorzystać; oznacza to po prostu, że jeśli to możliwe, biblioteka C i jądro starają się uniknąć błędu zwracania errno == EINTR, jeśli sygnał jest dostarczany za pomocą wątku aktualnie blokującego się w syscall (jak wait()). Możesz usunąć flagę SA_RESTART i dodać debugowanie fprintf(stderr, "Hey!\n"); w odpowiednim miejscu pętli w procesie nadrzędnym, aby zobaczyć, co się wtedy stanie.

Funkcjazwróci 0, jeśli nie wystąpi błąd, lub -1 z errno ustawionym inaczej. Funkcja forward_signal() zwraca 0, jeśli forward_handler została pomyślnie przypisana, ale w innym przypadku jest niezerowa wartość errno. Niektórym nie podoba się ten rodzaj wartości zwracanej (wolą po prostu zwrócić -1 za błąd, a nie samą wartość errno), ale jestem z jakiegoś nierozsądnego powodu, który polubił ten idiom. Zmień go, jeśli chcesz, za wszelką cenę.

Teraz przejdziemy do main().

Jeśli uruchomisz program bez parametrów lub z jednym parametrem -h lub --help, wydrukuje on podsumowanie użytkowania. Ponownie, robienie tego w ten sposób jest czymś, co lubię - getopt() i getopt_long() są częściej używane do analizowania opcji wiersza poleceń. Dla tego rodzaju trywialnego programu po prostu zakodowałem testy parametrów.

W tym przypadku celowo pozostawiłem bardzo krótki użytek. Byłoby naprawdę znacznie lepiej z dodatkowym akapitem o dokładnie tym, co robi program . Tego typu teksty - a zwłaszcza komentarze w kodzie (wyjaśniające intencję , idea tego, co kod powinien zrobić, zamiast opisywać, co właściwie robi kod) - są bardzo ważne. Minęło już ponad dwie dekady, odkąd po raz pierwszy dostałem zapłatę za napisanie kodu, a ja wciąż uczę się komentować - lepiej opisuję intencję - mój kod, więc myślę, że im wcześniej zacznie się nad tym pracować, lepszy.

Część powinna być znana. Jeśli zwróci -1, widelec się nie powiódł (prawdopodobnie ze względu na ograniczenia lub niektóre z nich), dlatego bardzo dobrym pomysłem jest wydrukowanie komunikatu errno. Wartością zwracaną będzie 0 w elemencie podrzędnym, a identyfikator procesu potomnego w procesie nadrzędnym.

Funkcja execlp() pobiera dwa argumenty: nazwa pliku binarnego (katalogi określone w zmiennej środowiskowej PATH będą użyte do wyszukania takiego pliku binarnego), a także tablica wskaźników do argumentów tego pliku binarnego . Pierwszym argumentem będzie argv[0] w nowym pliku binarnym, tj. Sama nazwa polecenia.

Wywołanie execlp(argv[1], argv + 1); jest bardzo proste do przeanalizowania, jeśli porównać go z powyższym opisem. argv[1] określa nazwę pliku binarnego, który ma zostać wykonany. argv + 1 jest zasadniczo równoważne z (char **)(&argv[1]), tj. Jest to tablica wskaźników rozpoczynających się od argv[1] zamiast argv[0]. Po raz kolejny po prostu lubię idiom execlp(argv[n], argv + n), ponieważ pozwala on wykonywać inne polecenie określone w linii poleceń bez martwienia się o parsowanie wiersza poleceń, lub wykonywanie go przez powłokę (co czasami wręcz niepożądane).

Na stronie man man 7 signal wyjaśniono, co dzieje się z procedurami obsługi sygnałów pod numerem fork() i exec(). W skrócie, programy obsługi sygnału są dziedziczone po fork(), ale resetowane są do wartości domyślnych exec(). Na szczęście jest to dokładnie to, czego chcemy, tutaj.

Gdybyśmy najpierw rozwidlili, a następnie zainstalowali manipulatory sygnałów, mielibyśmy okno, w którym proces potomny już istnieje, ale rodzic nadal ma domyślne dyspozycje (głównie zakończenie) dla sygnałów.

Zamiast tego możemy po prostu zablokować te sygnały za pomocą np. sigprocmask() w procesie macierzystym przed rozwidleniem. Blokowanie sygnału oznacza, że ​​jest on ustawiony na "wait"; nie zostanie dostarczony, dopóki sygnał nie zostanie odblokowany. W procesie potomnym sygnały mogą pozostać zablokowane, ponieważ dyspozycje sygnałów są zresetowane do wartości domyślnych ponad exec(). W procesie nadrzędnym moglibyśmy - lub przed rozwidleniem - nie mieć znaczenia - zainstalować moduły obsługi sygnałów, a na końcu odblokować sygnały. W ten sposób nie potrzebowalibyśmy atomowych rzeczy, ani nawet sprawdziliśmy, czy pid dziecka nie ma wartości zerowej, ponieważ pid dziecka będzie ustawiony na jego rzeczywistą wartość na długo przed dostarczeniem jakiegokolwiek sygnału!

Pętla while jest w zasadzie tylko pętla wokół rozmowy waitpid(), aż do dokładnego procesu potomnego zaczęliśmy wyjścia, albo coś dziwnego dzieje się (proces dziecko znika jakoś). Ta pętla zawiera dość staranne sprawdzanie błędów, a także poprawne przekazanie komunikatu, jeśli programy obsługi sygnałów miały zostać zainstalowane bez flag SA_RESTART.

Jeśli dziecko przetwarza rozwidlone wyjścia, sprawdzamy stan wyjścia i/lub przyczynę, dla których zginął, i drukujemy komunikat diagnostyczny na standardowy błąd.

Na koniec program kończy się okropnym hackowaniem: zamiast zwracać EXIT_SUCCESS lub EXIT_FAILURE, zwracamy całe słowo statusu, które uzyskaliśmy przy pomocy waitpid, kiedy proces potomny zakończył działanie. Powodem, dla którego go zostawiłem, jest to, że czasami używa się go w praktyce, gdy chcesz zwrócić ten sam lub podobny kod statusu wyjścia, co zwracany proces potomny. Tak, to jest dla ilustracji. Jeśli kiedykolwiek znajdziesz się w sytuacji, w której twój program powinien zwracać ten sam status wyjścia, co proces potomny, to rozwidla się i wykonuje, to jest jeszcze lepsze niż ustawienie maszyny, aby proces zabił się z tym samym sygnałem, który zabił dziecko. proces. Wystarczy umieścić tam wyraźny komentarz, jeśli kiedykolwiek będziesz tego potrzebował, oraz notatkę w instrukcji instalacji, aby ci, którzy skompilowali program na architekturach, gdzie może to być niechciane, mogą to naprawić.

+0

To jest niesamowite ... Nadal przetwarzam to wszystko, ale dziękuję za napisanie tego wszystkiego. Jestem skłonny zaczekać, aż będę mógł dać nagrodę, a następnie dodać ją również. – m0meni

+1

@ AR7: Nie ma potrzeby. Napisałem to w nadziei, że twoje pytanie będzie takie, na które inni się natkną, a także znajdzie wyjaśniony przykład przydatny.Jest dużo do zrobienia od razu, ale jest to również coś, co możesz ulepszyć i bawić się, jeśli przebrniesz przez całą tę ścianę tekstu ... Jeśli masz jakieś pytania lub problemy z przykładem lub jego zachowaniem, po prostu dodaj go jako komentarz, więc otrzymuję powiadomienie; Jestem sporadycznie online. –

+0

W pełni zamierzam przebrnąć przez to wiele razy ... jeszcze raz dziękuję i na pewno dam ci znać, jeśli będę miał jakieś pytania, gdy zrozumiem je całkowicie. – m0meni

Powiązane problemy