Warum wird SIGINT nicht an den untergeordneten Prozess weitergegeben, wenn es an den übergeordneten Prozess gesendet wird?

62

Wie kann ich bei einem gegebenen Shell-Prozess (zB sh) und seinem untergeordneten Prozess (zB cat) das Verhalten von Ctrl+ Cmithilfe der Prozess-ID der Shell simulieren ?


Das habe ich versucht:

Laufen shund dann cat:

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

Senden SIGINTan catvon einem anderen Terminal:

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

cat empfing das Signal und beendete (wie erwartet).

Das Senden des Signals an den übergeordneten Prozess scheint nicht zu funktionieren. Warum wird das Signal nicht weitergegeben, catwenn es an den übergeordneten Prozess gesendet wird sh?

Das funktioniert nicht:

[user@host ~]$ kill -SIGINT $PID_OF_SH
rob87
quelle
1
Die Shell kann SIGINT-Signale ignorieren, die nicht von der Tastatur oder dem Terminal gesendet werden.
Konsolebox

Antworten:

86

Wie CTRL+ Cfunktioniert

Das Erste ist zu verstehen, wie CTRL+ Cfunktioniert.

Wenn Sie CTRL+ drücken C, sendet Ihr Terminalemulator ein ETX-Zeichen (Textende / 0x03).
Das TTY ist so konfiguriert, dass beim Empfang dieses Zeichens ein SIGINT an die Vordergrundprozessgruppe des Terminals gesendet wird. Diese Konfiguration kann durch Ausführen sttyund Betrachten angezeigt werden intr = ^C;. Die POSIX-Spezifikation besagt, dass beim Empfang von INTR ein SIGINT an die Vordergrundprozessgruppe dieses Terminals gesendet werden soll.

Was ist die Vordergrundprozessgruppe?

Nun stellt sich die Frage, wie Sie die Vordergrundprozessgruppe bestimmen. Die Vordergrundprozessgruppe ist einfach die Gruppe von Prozessen, die alle von der Tastatur erzeugten Signale empfangen (SIGTSTOP, SIGINT usw.).

Die einfachste Methode zum Ermitteln der Prozessgruppen-ID ist die Verwendung von ps:

ps ax -O tpgid

Die zweite Spalte ist die Prozessgruppen-ID.

Wie sende ich ein Signal an die Prozessgruppe?

Nachdem wir die Prozessgruppen-ID kennen, müssen wir das POSIX-Verhalten beim Senden eines Signals an die gesamte Gruppe simulieren.

Dies können Sie tun, killindem Sie -der Gruppen-ID ein voranstellen.
Wenn Ihre Prozessgruppen-ID beispielsweise 1234 lautet, würden Sie Folgendes verwenden:

kill -INT -1234

 


Simulieren Sie CTRL+ Cmit der Terminalnummer.

In diesem Abschnitt wird beschrieben, wie Sie CTRL+ Cals manuellen Prozess simulieren . Aber was ist, wenn Sie die TTY-Nummer kennen und CTRL+ Cfür dieses Terminal simulieren möchten ?

Das wird sehr einfach.

Nehmen wir an, es $ttyist das Terminal, auf das Sie abzielen möchten (Sie können dies erreichen, indem Sie es tty | sed 's#^/dev/##'im Terminal ausführen).

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

Dadurch wird ein SIGINT an die Vordergrundprozessgruppe gesendet $tty.

Patrick
quelle
6
Es sollte darauf hingewiesen werden, dass Signale, die direkt von der Terminal-Bypass-Berechtigungsprüfung stammen, so dass Strg + C immer Signale liefert, es sei denn, Sie deaktivieren sie in den Terminal-Attributen, während ein killBefehl möglicherweise fehlschlägt.
Brian Bi
4
+1, fürsends a SIGINT to the foreground process group of the terminal.
andy
Erwähnenswert ist, dass die Prozessgruppe des Kindes dieselbe ist wie die des nachfolgenden Elternteils fork. Minimales lauffähiges C-Beispiel unter: unix.stackexchange.com/a/465112/32558
Ciro Santilli
15

Wie vinc17 sagt, gibt es dafür keinen Grund. Wenn Sie eine signalerzeugende Tastenfolge eingeben (z. B. Ctrl+ C), wird das Signal an alle Prozesse gesendet, die mit dem Terminal verbunden sind. Es gibt keinen solchen Mechanismus für Signale, die von erzeugt werden kill.

Allerdings mag ein Befehl

kill -SIGINT -12345

sendet das Signal an alle Prozesse in Prozessgruppe 12345; siehe kill (1) und kill (2) . Untergeordnete Elemente einer Shell befinden sich normalerweise in der Prozessgruppe der Shell (zumindest, wenn sie nicht asynchron sind). Wenn Sie also das Signal an die negative PID der Shell senden, können Sie die gewünschten Aktionen ausführen.


Hoppla

Wie vinc17 betont, funktioniert dies nicht für interaktive Shells. Hier ist eine Alternative, die funktionieren könnte :

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

ps -pPID_of_shellRuft Prozessinformationen für die Shell ab.  o tpgid=Weist psan, nur die Terminalprozessgruppen-ID ohne Header auszugeben. Wenn dies weniger als 10000 ist, pswird es mit führenden Leerzeichen angezeigt. das $(echo …)ist ein schneller Trick (und nachlauf) Räume abzustreifen führt.

Ich brachte dies dazu, in flüchtigen Tests auf einem Debian-Rechner zu arbeiten.

G-Man
quelle
1
Dies funktioniert nicht, wenn der Prozess in einer interaktiven Shell gestartet wird (was das OP verwendet). Ich habe jedoch keine Referenz für dieses Verhalten.
Vinc17
12

Die Frage enthält eine eigene Antwort. Das Senden der SIGINTan den catProzess mit killist eine perfekte Simulation dessen, was passiert, wenn Sie drücken ^C.

Genauer gesagt wird das Interrupt-Zeichen ( ^Cstandardmäßig) SIGINTan jeden Prozess in der Vordergrundprozessgruppe des Terminals gesendet. Wenn catSie keinen komplizierten Befehl mit mehreren Prozessen ausführen würden, müssten Sie die Prozessgruppe beenden, um den gleichen Effekt wie zu erzielen ^C.

Wenn Sie einen externen Befehl ohne den &Hintergrundoperator ausführen , erstellt die Shell eine neue Prozessgruppe für den Befehl und benachrichtigt das Terminal, dass diese Prozessgruppe jetzt im Vordergrund steht. Die Shell befindet sich noch in einer eigenen Prozessgruppe, die nicht mehr im Vordergrund steht. Dann wartet die Shell darauf, dass der Befehl beendet wird.

An dieser Stelle scheinen Sie durch ein häufiges Missverständnis zum Opfer gefallen zu sein: Die Idee, dass die Shell etwas unternimmt, um die Interaktion zwischen ihren untergeordneten Prozessen und dem Terminal zu erleichtern. Das stimmt einfach nicht. Sobald das Setup abgeschlossen ist (Prozesserstellung, Terminalmoduseinstellung, Erstellen von Pipes und Umleiten anderer Dateideskriptoren sowie Ausführen des Zielprogramms), wartet die Shell nur noch . Was Sie cateingeben, durchläuft nicht die Shell, egal ob es sich um eine normale Eingabe oder ein signalerzeugendes Sonderzeichen handelt ^C. Der catProzess hat direkten Zugriff auf das Terminal über seine eigenen Dateideskriptoren, und das Terminal kann Signale direkt an den catProzess senden , da dies die Vordergrundprozessgruppe ist.

Nachdem der catProzess beendet ist, wird die Shell benachrichtigt, da sie der übergeordnete catProzess ist. Dann wird die Shell aktiv und rückt sich wieder in den Vordergrund.

Hier ist eine Übung, um Ihr Verständnis zu verbessern.

Führen Sie an der Shell-Eingabeaufforderung in einem neuen Terminal den folgenden Befehl aus:

exec cat

Das execSchlüsselwort bewirkt, dass die Shell ausgeführt wird, catohne einen untergeordneten Prozess zu erstellen. Die Schale wird durch ersetzt cat. Die PID, die früher zur Shell gehörte, ist jetzt die PID von cat. Überprüfen Sie dies mit psin einem anderen Terminal. Geben Sie einige zufällige Zeilen ein und stellen catSie fest, dass sie sich normal verhalten, obwohl Sie keinen Shell-Prozess als übergeordnetes Element haben. Was passiert, wenn Sie ^Cjetzt drücken ?

Antworten:

SIGINT wird an den cat-Prozess übergeben, der stirbt. Da dies der einzige Prozess auf dem Terminal war, wird die Sitzung beendet, als ob Sie an einer Shell-Eingabeaufforderung "exit" gesagt hätten. In der Tat Katze war Shell für eine Weile.


quelle
Die Muschel ist aus dem Weg geraten. +1
Piotr Dobrogost
Ich verstehe nicht, warum nach dem exec catPressen ^Cnicht einfach ^Cin Katze landen würde . Warum sollte er das beenden, catwas jetzt die Shell ersetzt hat? Da die Shell ersetzt wurde, implementiert die Shell die Logik, SIGINT beim Empfang an ihre Kinder zu senden ^C.
Steven Lu
Der Punkt ist , dass die Schale nicht der Fall ist SIGINT seine Kinder schicken. Der SIGINT kommt vom Terminal-Treiber und wird an alle Vordergrundprozesse gesendet.
3

Es gibt keinen Grund, das SIGINTan das Kind weiterzugeben. Darüber hinaus heißt es in der system()POSIX-Spezifikation: "Die system () -Funktion soll die Signale SIGINT und SIGQUIT ignorieren und das Signal SIGCHLD blockieren, während auf die Beendigung des Befehls gewartet wird."

Wenn die Shell den Empfang SIGINTweiterleitet, z. B. nach einer echten Strg-C-Anweisung, würde dies bedeuten, dass der untergeordnete Prozess das SIGINTSignal zweimal empfängt , was zu unerwünschtem Verhalten führen kann.

vinc17
quelle
Die Shell muss dies nicht mit implementieren system(). Aber Sie haben Recht, wenn es das Signal empfängt (offensichtlich auch), gibt es keinen Grund, es nach unten zu verbreiten.
Goldlöckchen
@goldilocks Ich habe meine Antwort vervollständigt und vielleicht einen besseren Grund angegeben. Beachten Sie, dass die Shell nicht wissen kann, ob das Kind das Signal bereits empfangen hat, daher das Problem.
Vinc17
1

setpgid POSIX C-Prozessgruppen-Minimalbeispiel

Ein minimal ausführbares Beispiel für die zugrunde liegende API könnte das Verständnis erleichtern.

Dies zeigt, wie das Signal an das untergeordnete Element gesendet wird, wenn das untergeordnete Element seine Prozessgruppe nicht mit geändert hat setpgid.

Haupt 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 Upstream .

Kompilieren mit:

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

Laufen Sie ohne setpgid

Ohne CLI-Argumente setpgidwird Folgendes nicht ausgeführt:

./setpgid

Mögliches Ergebnis:

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

und das Programm hängt.

Wie wir sehen können, ist die pgid beider Prozesse dieselbe, da sie vererbt wird fork.

Dann, wann immer Sie schlagen:

Ctrl + C

Es gibt wieder aus:

sigint parent
sigint child

Dies zeigt, wie:

  • um mit ein Signal an eine ganze Prozessgruppe zu senden kill(-pgid, SIGINT)
  • Strg + C auf dem Terminal sendet standardmäßig einen Kill an die gesamte Prozessgruppe

Beenden Sie das Programm, indem Sie an beide Prozesse ein anderes Signal senden, z Ctrl + \. B. SIGQUIT mit .

Laufen Sie mit setpgid

Wenn Sie mit einem Argument laufen, zB:

./setpgid 1

dann ändert das Kind seine pgid, und jetzt wird jedes Mal nur ein einziges Zeichen vom Elternteil gedruckt:

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

Und jetzt, wann immer Sie schlagen:

Ctrl + C

Nur der Elternteil empfängt das Signal ebenfalls:

sigint parent

Sie können die Eltern weiterhin wie bisher mit einem SIGQUIT töten:

Ctrl + \

Das Kind hat jetzt jedoch eine andere PGID und empfängt dieses Signal nicht! Dies ist ersichtlich aus:

ps aux | grep setpgid

Du musst es explizit töten mit:

kill -9 16470

Dies macht deutlich, warum Signalgruppen existieren: Andernfalls würden wir eine Reihe von Prozessen übrig haben, die ständig manuell gereinigt werden müssten.

Getestet unter Ubuntu 18.04.

Ciro Santilli ist ein Schauspieler
quelle