Pourquoi SIGINT n'est-il pas propagé au processus enfant lorsqu'il est envoyé à son processus parent?


62

Étant donné un processus shell (par exemple sh) et son processus enfant (par exemple cat), comment puis-je simuler le comportement de Ctrl+ en Cutilisant l'ID de processus du shell?


C'est ce que j'ai essayé:

Courir shet ensuite cat:

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

Envoi SIGINTà catpartir d'un autre terminal:

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

cat reçu le signal et terminé (comme prévu).

L'envoi du signal au processus parent ne semble pas fonctionner. Pourquoi le signal n'est-il pas propagé catlorsqu'il est envoyé à son processus parent sh?

Cela ne fonctionne pas:

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

1
Le shell peut ignorer les signaux SIGINT non envoyés depuis le clavier ou le terminal.
konsolebox

Réponses:


86

Comment CTRL+ Cfonctionne

La première chose à faire est de comprendre comment CTRL+ Cfonctionne.

Lorsque vous appuyez sur CTRL+ C, votre émulateur de terminal envoie un caractère ETX (fin du texte / 0x03).
Le téléscripteur est configuré de sorte que, lorsqu'il reçoit ce caractère, il envoie un signal SIGINT au groupe de processus de premier plan du terminal. Cette configuration peut être visualisée en faisant sttyet en regardant intr = ^C;. La spécification POSIX indique que, lorsque l’INTR est reçu, il doit envoyer un SIGINT au groupe de processus de premier plan de ce terminal.

Quel est le groupe de processus de premier plan?

La question est donc de savoir comment déterminer le groupe de processus de premier plan. Le groupe de processus de premier plan est simplement le groupe de processus qui recevra les signaux générés par le clavier (SIGTSTOP, SIGINT, etc.).

Le moyen le plus simple de déterminer l'ID du groupe de processus consiste à utiliser ps:

ps ax -O tpgid

La deuxième colonne sera l'ID du groupe de processus.

Comment envoyer un signal au groupe de processus?

Maintenant que nous connaissons l'identifiant du groupe de processus, nous devons simuler le comportement POSIX consistant à envoyer un signal à l'ensemble du groupe.

Cela peut être fait killen mettant un -devant l'ID du groupe.
Par exemple, si votre ID de groupe de processus est 1234, vous utiliseriez:

kill -INT -1234

 


Simulez CTRL+ en Cutilisant le numéro de terminal.

Ce qui précède explique donc comment simuler CTRL+ en Ctant que processus manuel. Mais que se passe-t-il si vous connaissez le numéro de téléscripteur et souhaitez simuler CTRL+ Cpour ce terminal?

Cela devient très facile.

Supposons que $ttyc'est le terminal que vous souhaitez cibler (vous pouvez l'obtenir en le lançant tty | sed 's#^/dev/##'dans le terminal).

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

Cela enverra un SIGINT à quelque groupe de processus d’avant-plan $tty.


6
Il convient de souligner que les signaux provenant directement du terminal contournent la vérification des autorisations. Ctrl + C réussit donc toujours à délivrer des signaux, à moins que vous ne le désactiviez dans les attributs du terminal, alors qu'une killcommande pourrait échouer.
Brian Bi

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

Il convient de mentionner que le groupe de processus de l’enfant est identique à celui du parent après fork. Exemple de runnable
Ciro Santilli a publié

15

Comme dit vinc17, il n'y a aucune raison pour que cela se produise. Lorsque vous tapez une séquence de touches générant un signal (par exemple, Ctrl+ C), le signal est envoyé à tous les processus liés au terminal (associés). Un tel mécanisme n'existe pas pour les signaux générés par kill.

Cependant, une commande comme

kill -SIGINT -12345

enverra le signal à tous les processus du groupe de processus 12345; voir kill (1) et kill (2) . Les enfants d'un shell font généralement partie du groupe de processus du shell (du moins s'ils ne sont pas asynchrones). L'envoi du signal au négatif du PID du shell peut donc faire ce que vous voulez.


Oops

Comme le fait remarquer vinc17, cela ne fonctionne pas pour les shells interactifs. Voici une alternative qui pourrait fonctionner:

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

ps -pPID_of_shellobtient des informations de processus sur le shell.  o tpgid=indique psde ne produire que l'ID du groupe de processus terminal, sans en-tête. S'il est inférieur à 10 000, psl'affichera avec le ou les espaces de début; la $(echo …)est une tour rapide à dépouiller avant (et arrière) espaces.

J'ai réussi à ce que cela fonctionne lors de tests superficiels sur une machine Debian.


1
Cela ne fonctionne pas lorsque le processus est démarré dans un shell interactif (c'est ce que l'OP utilise). Je n'ai pas de référence pour ce comportement, cependant.
vinc17

12

La question contient sa propre réponse. L' envoi du SIGINTau catprocessus avec killune simulation parfaite de ce qui se passe lorsque vous appuyez sur ^C.

Pour être plus précis, le caractère d'interruption ( ^Cpar défaut) envoie SIGINTà chaque processus du groupe de processus de premier plan du terminal. Si au lieu de catvous exécutez une commande plus complexe impliquant plusieurs processus, vous devez supprimer le groupe de processus pour obtenir le même effet ^C.

Lorsque vous exécutez une commande externe sans &opérateur d'arrière - plan, le shell crée un nouveau groupe de processus pour la commande et informe le terminal que ce groupe de processus est maintenant au premier plan. Le shell est toujours dans son propre groupe de processus, qui n'est plus au premier plan. Ensuite, le shell attend que la commande se ferme.

C’est là que vous semblez être devenu la victime d’une idée fausse courante: l’idée que le shell fait quelque chose pour faciliter l’interaction entre ses processus enfants et le terminal. Ce n'est tout simplement pas vrai. Une fois que le travail d’installation est terminé (création de processus, paramétrage du mode terminal, création de canaux et redirection d’autres descripteurs de fichier, et exécution du programme cible), le shell attend . Ce que vous saisissez n’entre catpas dans le shell, qu’il s’agisse d’une entrée normale ou d’un caractère spécial générateur de signaux, par exemple ^C. Le catprocessus a un accès direct au terminal par le biais de ses propres descripteurs de fichier et le terminal peut envoyer des signaux directement au catprocessus car il s'agit du groupe de processus de premier plan.

Une fois le catprocessus terminé, le shell en sera informé, car il s'agit du parent du catprocessus. Ensuite, le shell devient actif et se place à nouveau au premier plan.

Voici un exercice pour augmenter votre compréhension.

À l'invite du shell d'un nouveau terminal, exécutez la commande suivante:

exec cat

Le execmot clé provoque l'exécution du shell catsans créer de processus enfant. La coquille est remplacée par cat. Le PID qui appartenait auparavant à la coquille est maintenant le PID de cat. Vérifiez ceci psdans un autre terminal. Tapez quelques lignes aléatoires et voyez- catles vous les répéter, ce qui prouve qu'il se comporte toujours normalement bien qu'il ne s'agisse pas d'un processus shell en tant que parent. Que se passera-t-il quand vous appuierez ^Cmaintenant?

Répondre:

SIGINT est livré au processus cat, qui meurt. Comme c'était le seul processus sur le terminal, la session se terminait, comme si vous aviez dit "exit" à l'invite du shell. En fait, le chat a été votre coquille pendant un moment.


La coquille s'est échappée. +1
Piotr Dobrogost

Je ne comprends pas pourquoi après avoir exec catappuyé ^Cne pas atterrir ^Cdans chat. Pourquoi voudrait-il terminer le catqui a maintenant remplacé la coquille? Depuis que le shell a été remplacé, le shell est la chose qui implémente la logique d’envoi de SIGINT à ses enfants lors de la réception ^C.
Steven Lu

Le fait est que le shell n'envoie pas SIGINT à ses enfants. Le SIGINT provient du pilote de terminal et est envoyé à tous les processus de premier plan.

3

Il n'y a aucune raison de propager le SIGINTà l'enfant. De plus, la system()spécification POSIX indique: "La fonction system () doit ignorer les signaux SIGINT et SIGQUIT et doit bloquer le signal SIGCHLD en attendant la fin de la commande."

Si le shell propage la réception SIGINT, par exemple après une vraie touche Ctrl-C, cela signifie que le processus enfant recevra le SIGINTsignal deux fois, ce qui peut avoir un comportement indésirable.


Le shell n'a pas à implémenter cela avec system(). Mais vous avez raison, si le signal est capté (bien évidemment), il n’ya aucune raison de le propager vers le bas.
goldilocks

@goldilocks J'ai complété ma réponse, en donnant peut-être une meilleure raison. Notez que le shell ne peut pas savoir si l’enfant a déjà reçu le signal, d’où le problème.
vinc17

1

setpgid Exemple minimal de groupe de processus POSIX C

Il serait peut-être plus facile de comprendre avec un exemple minimal exécutable de l’API sous-jacente.

Ceci illustre comment le signal est envoyé à l'enfant, si l'enfant n'a pas changé de groupe de processus avec setpgid.

principal 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 en amont .

Compiler avec:

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

Courir sans setpgid

Sans aucun argument CLI, setpgidn'est pas fait:

./setpgid

Résultat possible:

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

et le programme se bloque.

Comme nous pouvons le constater, le pgid des deux processus est le même, car il est hérité d'un bout à l'autre fork.

Puis chaque fois que vous frappez:

Ctrl + C

Il affiche à nouveau:

sigint parent
sigint child

Cela montre comment:

  • pour envoyer un signal à un groupe de processus complet avec kill(-pgid, SIGINT)
  • Ctrl + C sur le terminal envoie un kill à tout le groupe de processus par défaut

Quittez le programme en envoyant un signal différent aux deux processus, par exemple SIGQUIT avec Ctrl + \.

Courir avec setpgid

Si vous utilisez un argument, par exemple:

./setpgid 1

alors l'enfant change de pgid, et maintenant, un seul sigint est imprimé à chaque fois à partir du parent uniquement:

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

Et maintenant, chaque fois que vous frappez:

Ctrl + C

seul le parent reçoit également le signal:

sigint parent

Vous pouvez toujours tuer le parent comme avant avec un SIGQUIT:

Ctrl + \

Cependant, l'enfant a maintenant un autre PGID et ne reçoit pas ce signal! Cela peut être vu de:

ps aux | grep setpgid

Vous devrez le tuer explicitement avec:

kill -9 16470

Cela explique pourquoi les groupes de signaux existent: sinon, nous aurions un tas de processus à nettoyer manuellement tout le temps.

Testé sur Ubuntu 18.04.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.