Dlaczego SIGINT nie jest propagowany do procesu potomnego, gdy jest wysyłany do procesu macierzystego?

62

Biorąc pod uwagę proces powłoki (np. sh) I jego proces potomny (np. cat), W jaki sposób mogę zasymulować zachowanie Ctrl+ Cprzy użyciu identyfikatora procesu powłoki?


Oto, co próbowałem:

Uruchamianie, sha następnie cat:

[user@host ~]$ sh
sh-4.3$ cat
test
test

Przesyłanie SIGINTdo catz innego terminala:

[user@host ~]$ kill -SIGINT $PID_OF_CAT

cat odebrał sygnał i zakończył (zgodnie z oczekiwaniami).

Wysyłanie sygnału do procesu nadrzędnego nie działa. Dlaczego sygnał nie jest propagowany catpo wysłaniu do procesu nadrzędnego sh?

To nie działa:

[user@host ~]$ kill -SIGINT $PID_OF_SH
rob87
źródło
1
Powłoka pozwala ignorować sygnały SIGINT, które nie są wysyłane z klawiatury lub terminala.
konsolebox

Odpowiedzi:

86

Jak działa CTRL+C

Pierwszą rzeczą jest zrozumienie, jak działa CTRL+ C.

Po naciśnięciu przycisku CTRL+ Cemulator terminala wysyła znak ETX (koniec tekstu / 0x03).
TTY jest skonfigurowany w taki sposób, że po otrzymaniu tego znaku wysyła SIGINT do grupy procesów pierwszego planu terminala. Tę konfigurację można wyświetlić, robiąc sttyi patrząc intr = ^C;. Specyfikacja POSIX mówi, że po otrzymaniu INTR powinien wysłać SIGINT do grupy procesów pierwszego planu tego terminala.

Jaka jest grupa procesów pierwszego planu?

Więc teraz pytanie brzmi, jak określić, jaka jest grupa procesów pierwszego planu? Pierwszą grupą procesów jest po prostu grupa procesów, które będą odbierać wszelkie sygnały generowane przez klawiaturę (SIGTSTOP, SIGINT itp.).

Najprostszym sposobem ustalenia identyfikatora grupy procesów jest użycie ps:

ps ax -O tpgid

Druga kolumna będzie identyfikatorem grupy procesów.

Jak wysłać sygnał do grupy procesów?

Teraz, gdy wiemy, jaki jest identyfikator grupy procesów, musimy zasymulować zachowanie POSIX wysyłania sygnału do całej grupy.

Można to zrobić kill, umieszczając -przed identyfikatorem grupy.
Na przykład jeśli identyfikator grupy procesów to 1234, możesz użyć:

kill -INT -1234

 


Symuluj CTRL+, Cużywając numeru terminalu.

Tak więc powyższe obejmuje symulację CTRL+ Cjako proces ręczny. Ale co, jeśli znasz numer TTY i chcesz symulować CTRL+ Cdla tego terminala?

To staje się bardzo łatwe.

Załóżmy, że $ttyjest to terminal, na który chcesz celować (możesz to uzyskać, uruchamiając go tty | sed 's#^/dev/##'w terminalu).

kill -INT -$(ps h -t $tty -o tpgid | uniq)

Spowoduje to wysłanie SIGINT do dowolnej grupy procesów pierwszego planu $tty.

Patrick
źródło
6
Warto zauważyć, że sygnały, które pochodzą bezpośrednio z terminala, omijają sprawdzanie uprawnień, więc Ctrl + C zawsze dostarcza sygnały, chyba że wyłączysz je w atrybutach terminala, podczas gdy killpolecenie może się nie powieść.
Brian Bi
4
+1, dlasends a SIGINT to the foreground process group of the terminal.
andy
Warto wspomnieć, że grupa procesów dziecka jest taka sama jak później rodzic fork. Minimalny możliwy do uruchomienia
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
15

Jak mówi vinc17, nie ma powodu do tego. Po wpisaniu sekwencji klawiszy generujących sygnał (np. Ctrl+ C) Sygnał jest wysyłany do wszystkich procesów, które są podłączone do terminala (powiązane z nim). Nie ma takiego mechanizmu dla sygnałów generowanych przez kill.

Jednak polecenie takie jak

kill -SIGINT -12345

wyśle ​​sygnał do wszystkich procesów w grupie procesów 12345; patrz kill (1) i kill (2) . Elementy potomne powłoki zwykle znajdują się w grupie procesów powłoki (przynajmniej jeśli nie są asynchroniczne), więc wysyłanie sygnału do ujemnego PID powłoki może zrobić, co chcesz.


Ups

Jak wskazuje vinc17, nie działa to dla interaktywnych powłok. Oto alternatywa, która może działać:

kill -SIGINT - $ (echo $ (ps -p PID_of_shell o tpgid =))

ps -pPID_of_shellpobiera informacje o procesie dotyczące powłoki.  o tpgid=mówi, psaby wyświetlać tylko identyfikator grupy procesów terminalowych, bez nagłówka. Jeśli jest to mniej niż 10000, pswyświetli go z wiodącymi spacjami; $(echo …)to szybkie sztuczka zdejmować wiodącym (i końcowe) przestrzenie.

Dostałem to do pracy w pobieżnych testach na maszynie Debiana.

G-Man
źródło
1
Nie działa to, gdy proces jest uruchamiany w interaktywnej powłoce (której używa OP). Jednak nie mam odniesienia do tego zachowania.
vinc17
12

Pytanie zawiera własną odpowiedź. Wysłanie SIGINTdo catprocesu za pomocą killjest idealną symulacją tego, co dzieje się po naciśnięciu ^C.

Mówiąc dokładniej, znak przerwania ( ^Cdomyślnie) wysyła SIGINTdo każdego procesu w grupie procesów pierwszego planu terminala. Jeśli zamiast catwykonywania bardziej skomplikowanego polecenia obejmującego wiele procesów, musisz zabić grupę procesów, aby uzyskać taki sam efekt jak ^C.

Po uruchomieniu dowolnego polecenia zewnętrznego bez &operatora tła powłoka tworzy nową grupę procesów dla polecenia i powiadamia terminal, że ta grupa procesów jest teraz na pierwszym planie. Powłoka nadal znajduje się we własnej grupie procesów, która nie jest już na pierwszym planie. Następnie powłoka czeka na zakończenie polecenia.

To tam wydaje się, że padłeś ofiarą powszechnego nieporozumienia: idea, że ​​powłoka robi coś, aby ułatwić interakcję między procesami potomnymi i terminalem. To po prostu nieprawda. Po zakończeniu prac instalacyjnych (tworzenie procesu, ustawienie trybu terminalowego, tworzenie potoków i przekierowywanie innych deskryptorów plików oraz uruchamianie programu docelowego) powłoka po prostu czeka . To, co wpisujesz, catnie przechodzi przez powłokę, niezależnie od tego, czy jest to normalne wejście, czy znak specjalny generujący sygnał ^C. catProces ma bezpośredni dostęp do terminala za pośrednictwem własnych deskryptorów plików, a terminal ma możliwość wysyłania sygnałów bezpośrednio do catprocesu, ponieważ znajduje się na pierwszym planie grupa proces.

Po zakończeniu catprocesu powłoka zostanie powiadomiona, ponieważ jest rodzicem catprocesu. Następnie powłoka staje się aktywna i ponownie stawia się na pierwszym planie.

Oto ćwiczenie, które zwiększy twoje zrozumienie.

W wierszu poleceń powłoki w nowym terminalu uruchom następujące polecenie:

exec cat

Słowo execkluczowe powoduje wykonanie powłoki catbez tworzenia procesu potomnego. Powłoka jest zastąpiona przez cat. PID, który wcześniej należał do powłoki, teraz jest PID cat. Sprawdź to za pomocą psw innym terminalu. Wpisz kilka losowych linii i zobacz, że catpowtarza je z powrotem do Ciebie, co dowodzi, że nadal zachowuje się normalnie, mimo że nie ma procesu powłoki jako elementu nadrzędnego. Co się stanie, gdy ^Cteraz naciśniesz ?

Odpowiedź:

SIGINT jest dostarczany do procesu kota, który umiera. Ponieważ był to jedyny proces na terminalu, sesja kończy się, tak jakbyś powiedział „wyjdź” po znaku zachęty powłoki. W efekcie kot był przez chwilę twoją skorupą.


źródło
Pocisk zszedł z drogi. +1
Piotr Dobrogost
Nie rozumiem, dlaczego po exec catwciśnięciu ^Cnie wylądowałby tylko ^Cw kocie. Dlaczego miałby to zakończyć ten, catktóry teraz zastąpił powłokę? Ponieważ powłoka została zastąpiona, powłoka realizuje logikę wysyłania SIGINT do swoich dzieci po otrzymaniu ^C.
Steven Lu
Chodzi o to, że powłoka nie wysyła SIGINT do swoich dzieci. SIGINT pochodzi ze sterownika terminala i jest wysyłany do wszystkich procesów pierwszego planu.
3

Nie ma powodu, aby propagować SIGINTto dziecku. Ponadto system()specyfikacja POSIX mówi: „Funkcja system () powinna ignorować sygnały SIGINT i SIGQUIT i blokować sygnał SIGCHLD, czekając na zakończenie polecenia”.

Jeśli powłoka propaguje odebrane SIGINT, np. Po prawdziwym Ctrl-C, oznacza to, że proces potomny odbierze SIGINTsygnał dwukrotnie, co może mieć niepożądane zachowanie.

vinc17
źródło
Powłoka nie musi tego implementować system(). Ale masz rację, jeśli złapie sygnał (oczywiście, że tak), nie ma powodu, aby propagować go w dół.
goldilocks,
@Goldilocks Ukończyłem swoją odpowiedź, być może podając lepszy powód. Zauważ, że powłoka nie może wiedzieć, czy dziecko już otrzymało sygnał, stąd problem.
vinc17
1

setpgid Przykład minimalnej grupy procesów POSIX C.

Łatwiej jest to zrozumieć przy minimalnym uruchamialnym przykładzie bazowego API.

To ilustruje, w jaki sposób sygnał jest wysyłany do dziecka, jeśli dziecko nie zmieniło grupy procesów za pomocą setpgid.

main.c

#define _XOPEN_SOURCE 700
#include <assert.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

volatile sig_atomic_t is_child = 0;

void signal_handler(int sig) {
    char parent_str[] = "sigint parent\n";
    char child_str[] = "sigint child\n";
    signal(sig, signal_handler);
    if (sig == SIGINT) {
        if (is_child) {
            write(STDOUT_FILENO, child_str, sizeof(child_str) - 1);
        } else {
            write(STDOUT_FILENO, parent_str, sizeof(parent_str) - 1);
        }
    }
}

int main(int argc, char **argv) {
    pid_t pid, pgid;

    (void)argv;
    signal(SIGINT, signal_handler);
    signal(SIGUSR1, signal_handler);
    pid = fork();
    assert(pid != -1);
    if (pid == 0) {
        is_child = 1;
        if (argc > 1) {
            /* Change the pgid.
             * The new one is guaranteed to be different than the previous, which was equal to the parent's,
             * because `man setpgid` says:
             * > the child has its own unique process ID, and this PID does not match
             * > the ID of any existing process group (setpgid(2)) or session.
             */
            setpgid(0, 0);
        }
        printf("child pid, pgid = %ju, %ju\n", (uintmax_t)getpid(), (uintmax_t)getpgid(0));
        assert(kill(getppid(), SIGUSR1) == 0);
        while (1);
        exit(EXIT_SUCCESS);
    }
    /* Wait until the child sends a SIGUSR1. */
    pause();
    pgid = getpgid(0);
    printf("parent pid, pgid = %ju, %ju\n", (uintmax_t)getpid(), (uintmax_t)pgid);
    /* man kill explains that negative first argument means to send a signal to a process group. */
    kill(-pgid, SIGINT);
    while (1);
}

GitHub w górę .

Połącz z:

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -Wpedantic -o setpgid setpgid.c

Uruchom bez setpgid

Bez argumentów CLI setpgidnie jest wykonywane:

./setpgid

Możliwy wynik:

child pid, pgid = 28250, 28249
parent pid, pgid = 28249, 28249
sigint parent
sigint child

i program się zawiesza.

Jak widzimy, pgid obu procesów jest taki sam, ponieważ jest dziedziczony fork.

Następnie za każdym razem, gdy klikniesz:

Ctrl + C

Wyprowadza ponownie:

sigint parent
sigint child

To pokazuje, jak:

  • wysłać sygnał do całej grupy procesów za pomocą kill(-pgid, SIGINT)
  • Ctrl + C na terminalu domyślnie wysyła zabicie do całej grupy procesów

Wyjdź z programu, wysyłając inny sygnał do obu procesów, np. SIGQUIT za pomocą Ctrl + \.

Biegnij z setpgid

Jeśli uruchamiasz z argumentem, np .:

./setpgid 1

wtedy dziecko zmienia swój pgid, a teraz tylko jeden podpis jest drukowany za każdym razem tylko od rodzica:

child pid, pgid = 16470, 16470
parent pid, pgid = 16469, 16469
sigint parent

A teraz, kiedy uderzysz:

Ctrl + C

tylko rodzic odbiera również sygnał:

sigint parent

Nadal możesz zabić rodzica za pomocą SIGQUIT:

Ctrl + \

jednak dziecko ma teraz inny PGID i nie odbiera tego sygnału! Można to zobaczyć z:

ps aux | grep setpgid

Będziesz musiał zabić go jawnie za pomocą:

kill -9 16470

Wyjaśnia to, dlaczego istnieją grupy sygnałów: w przeciwnym razie cały szereg procesów pozostałoby do ręcznego czyszczenia przez cały czas.

Testowane na Ubuntu 18.04.

Ciro Santilli
źródło