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ć.
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
Byłbym bardzo zaskoczony, gdyby sqlite nie uruchomił własnej grupy procesów i gwarantuję, że zmienia ona własną maskę sygnału. – msw
@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. –