Perché SIGINT non viene propagato al processo figlio quando viene inviato al processo principale?


62

Dato un processo di shell (ad esempio sh) e il suo processo figlio (ad esempio cat), come posso simulare il comportamento di Ctrl+ Cusando l'ID di processo della shell?


Questo è quello che ho provato:

In esecuzione she quindi cat:

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

Invio SIGINTad catun altro terminale:

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

cat ricevuto il segnale e terminato (come previsto).

L'invio del segnale al processo genitore non sembra funzionare. Perché il segnale non viene propagato a catquando viene inviato al suo processo genitore sh?

Questo non funziona:

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

1
La shell ha un modo per ignorare i segnali SIGINT non inviati dalla tastiera o dal terminale.
Konsolebox

Risposte:


86

Come CTRL+ Cfunziona

La prima cosa è capire come CTRL+ Cfunziona.

Quando si preme CTRL+ C, l'emulatore di terminale invia un carattere ETX (end-of-text / 0x03).
Il TTY è configurato in modo tale che quando riceve questo carattere, invia un SIGINT al gruppo di processi in primo piano del terminale. Questa configurazione può essere visualizzata facendo sttye guardando intr = ^C;. La specifica POSIX afferma che quando viene ricevuto INTR, dovrebbe inviare un SIGINT al gruppo di processi in primo piano di quel terminale.

Cos'è il gruppo di processi in primo piano?

Quindi, ora la domanda è: come si determina quale sia il gruppo di processi in primo piano? Il gruppo di processi in primo piano è semplicemente il gruppo di processi che riceverà qualsiasi segnale generato dalla tastiera (SIGTSTOP, SIGINT, ecc.).

Il modo più semplice per determinare l'ID del gruppo di processi è utilizzare ps:

ps ax -O tpgid

La seconda colonna sarà l'ID del gruppo di processi.

Come posso inviare un segnale al gruppo di processo?

Ora che sappiamo qual è l'ID del gruppo di processi, dobbiamo simulare il comportamento POSIX dell'invio di un segnale all'intero gruppo.

Questo può essere fatto killmettendo un -davanti all'ID del gruppo.
Ad esempio, se l'ID del gruppo di processi è 1234, utilizzare:

kill -INT -1234

 


Simula CTRL+ Cusando il numero del terminale.

Quindi quanto sopra illustra come simulare CTRL+ Ccome processo manuale. Ma cosa succede se si conosce il numero TTY e si desidera simulare CTRL+ Cper quel terminale?

Questo diventa molto semplice.

Supponiamo che $ttysia il terminale a cui vuoi indirizzare (puoi farlo eseguendo tty | sed 's#^/dev/##'nel terminale).

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

Questo invierà un SIGINT a qualunque sia il gruppo di processi in primo piano di $tty.


6
Vale la pena sottolineare che i segnali che provengono direttamente dal controllo delle autorizzazioni di bypass del terminale, quindi Ctrl + C riesce sempre a fornire segnali a meno che non venga disattivato negli attributi del terminale, mentre un killcomando potrebbe non riuscire.
Brian Bi

4
+1, persends a SIGINT to the foreground process group of the terminal.
andy,

Vale la pena ricordare che il gruppo di processo del bambino è lo stesso del genitore dopo fork. Esempio C eseguibile minimo su: unix.stackexchange.com/a/465112/32558
Ciro Santilli 27 改造 中心 法轮功 六四 事件

15

Come dice vinc17, non c'è motivo per cui ciò accada. Quando si digita una sequenza di tasti generatori di segnale (ad es. Ctrl+ C), Il segnale viene inviato a tutti i processi collegati al terminale (associato). Non esiste un tale meccanismo per i segnali generati da kill.

Tuttavia, un comando simile

kill -SIGINT -12345

invierà il segnale a tutti i processi nel gruppo di processo 12345; vedi kill (1) e kill (2) . I figli di una shell sono in genere nel gruppo di processi della shell (almeno, se non sono asincroni), quindi inviare il segnale al negativo del PID della shell può fare quello che vuoi.


Spiacenti

Come sottolinea vinc17, questo non funziona con le shell interattive. Ecco un'alternativa che potrebbe funzionare:

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

ps -pPID_of_shellottiene informazioni di processo sulla shell.  o tpgid=indica psdi generare solo l'ID del gruppo di processi del terminale, senza intestazione. Se questo è inferiore a 10000, psverrà visualizzato con uno o più spazi iniziali; la $(echo …)è un breve trucco per togliere leader (e finali) spazi.

Ho fatto in modo che funzionasse nei test rapidi su una macchina Debian.


1
Questo non funziona quando il processo viene avviato in una shell interattiva (che è ciò che utilizza l'OP). Non ho un riferimento per questo comportamento, però.
vinc17,

12

La domanda contiene una sua risposta. L'invio SIGINTal catprocesso con killè una perfetta simulazione di ciò che accade quando si preme ^C.

Per essere più precisi, il carattere di interruzione ( ^Cper impostazione predefinita) invia SIGINTa ogni processo nel gruppo di processi in primo piano del terminale. Se invece di cateseguire un comando più complicato che coinvolge più processi, dovresti uccidere il gruppo di processi per ottenere lo stesso effetto di ^C.

Quando si esegue un comando esterno senza l' &operatore in background, la shell crea un nuovo gruppo di processi per il comando e notifica al terminale che questo gruppo di processi è ora in primo piano. La shell è ancora nel suo gruppo di processi, che non è più in primo piano. Quindi la shell attende che il comando termini.

È lì che sembra che tu sia diventato vittima di un malinteso comune: l'idea che la shell stia facendo qualcosa per facilitare l'interazione tra i suoi processi figlio e il terminale. Questo non è vero. Una volta terminato il lavoro di installazione (creazione del processo, impostazione della modalità terminale, creazione di pipe e reindirizzamento di altri descrittori di file ed esecuzione del programma di destinazione) la shell attende . Ciò in cui digiti catnon sta attraversando la shell, sia che si tratti di un input normale o di un carattere speciale che genera segnali ^C. Il catprocesso ha accesso diretto al terminale attraverso i propri descrittori di file e il terminale ha la capacità di inviare segnali direttamente al catprocesso perché è il gruppo di processi in primo piano.

Dopo la fine del catprocesso, la shell verrà notificata, poiché è il genitore del catprocesso. Quindi la shell diventa attiva e si mette di nuovo in primo piano.

Ecco un esercizio per aumentare la tua comprensione.

Al prompt della shell in un nuovo terminale, eseguire questo comando:

exec cat

La execparola chiave fa eseguire la shell catsenza creare un processo figlio. La shell è sostituita da cat. Il PID che in precedenza apparteneva alla shell è ora il PID di cat. Verificare questo con psun altro terminale. Digita alcune righe casuali e vedi che le catripropone, dimostrando che si sta ancora comportando normalmente nonostante non abbia un processo shell come genitore. Cosa succederà quando premi ^Cora?

Risposta:

SIGINT viene consegnato al processo del gatto, che muore. Poiché era l'unico processo sul terminale, la sessione termina, proprio come se avessi detto "esci" al prompt della shell. In effetti il ​​gatto è stato il tuo guscio per un po '.


La shell si è fatta da parte. +1
Piotr Dobrogost,

Non capisco perché dopo aver exec catpremuto ^Cnon sarebbe semplicemente atterrato ^Cnel gatto. Perché dovrebbe terminare quello catche ora ha sostituito la shell? Da quando la shell è stata sostituita, la shell è la cosa che implementa la logica di inviare SIGINT ai suoi figli dopo averlo ricevuto ^C.
Steven Lu,

Il punto è che la shell non invia SIGINT ai suoi figli. SIGINT proviene dal driver del terminale e viene inviato a tutti i processi in primo piano.

3

Non c'è motivo di propagare il SIGINTal bambino. Inoltre la system()specifica POSIX dice: "La funzione system () deve ignorare i segnali SIGINT e SIGQUIT e deve bloccare il segnale SIGCHLD, in attesa che il comando termini."

Se la shell propagasse il ricevuto SIGINT, ad esempio seguendo un vero Ctrl-C, ciò significherebbe che il processo figlio riceverà il SIGINTsegnale due volte, il che potrebbe avere un comportamento indesiderato.


La shell non deve implementarlo con system(). Ma hai ragione, se cattura il segnale (ovviamente lo fa), non c'è motivo di propagarlo verso il basso.
Riccioli d'oro,

@goldilocks Ho completato la mia risposta, forse dando una ragione migliore. Si noti che la shell non può sapere se il bambino ha già ricevuto il segnale, quindi il problema.
vinc17,

1

setpgid Esempio minimo del gruppo di processi POSIX C.

Potrebbe essere più semplice da comprendere con un esempio eseguibile minimo dell'API sottostante.

Questo illustra come il segnale viene inviato al bambino, se il bambino non ha cambiato il suo gruppo di processi con 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 a monte .

Compilare con:

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

Corri senza setpgid

Senza alcun argomento CLI, setpgidnon viene eseguito:

./setpgid

Possibile risultato:

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

e il programma si blocca.

Come possiamo vedere, il pgid di entrambi i processi è lo stesso, in quanto viene ereditato fork.

Quindi ogni volta che colpisci:

Ctrl + C

Emette di nuovo:

sigint parent
sigint child

Questo mostra come:

  • per inviare un segnale a un intero gruppo di processi con kill(-pgid, SIGINT)
  • Ctrl + C sul terminale invia un kill a tutto il gruppo di processo per impostazione predefinita

Chiudere il programma inviando un segnale diverso a entrambi i processi, ad esempio SIGQUIT con Ctrl + \.

Corri con setpgid

Se corri con un argomento, ad esempio:

./setpgid 1

quindi il bambino cambia il suo pgid, e ora solo un singolo sigillo viene stampato ogni volta solo dal genitore:

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

E ora, ogni volta che colpisci:

Ctrl + C

solo il genitore riceve anche il segnale:

sigint parent

Puoi ancora uccidere il genitore come prima con un SIGQUIT:

Ctrl + \

tuttavia il bambino ora ha un PGID diverso e non riceve quel segnale! Questo può essere visto da:

ps aux | grep setpgid

Dovrai ucciderlo esplicitamente con:

kill -9 16470

Questo chiarisce il motivo per cui esistono gruppi di segnali: altrimenti avremmo lasciato un mucchio di processi da pulire manualmente tutto il tempo.

Testato su Ubuntu 18.04.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.