web-dev-qa-db-fra.com

Comment Ctrl-C met fin à un processus enfant?

J'essaie de comprendre comment CTRL+C met fin à un processus enfant mais pas parent. Je vois ce comportement dans certains shells de script comme bash où vous pouvez démarrer un processus de longue durée puis le terminer en entrant CTRL-C et le contrôle revient au Shell.

Pourriez-vous expliquer comment cela fonctionne et en particulier pourquoi le processus parent (Shell) n'est-il pas terminé?

Le Shell doit-il faire un traitement spécial de CTRL+C événement et si oui que fait-il exactement?

61
vitaut

Les signaux par défaut sont gérés par le noyau. Les anciens systèmes Unix avaient 15 signaux; maintenant, ils en ont plus. Tu peux vérifier </usr/include/signal.h> (ou kill -l). CTRL+C est le signal avec le nom SIGINT.

L'action par défaut pour gérer chaque signal est également définie dans le noyau et met généralement fin au processus qui a reçu le signal.

Tous les signaux (mais SIGKILL) peuvent être gérés par programme.

Et voici ce que fait Shell:

  • Lorsque le Shell s'exécute en mode interactif, il dispose d'un traitement de signal spécial pour ce mode.
  • Lorsque vous exécutez un programme, par exemple find, le shell:
    • forks lui-même
    • et pour l'enfant définir le traitement du signal par défaut
    • remplacer l'enfant avec la commande donnée (par exemple avec find)
    • lorsque vous appuyez sur CTRL+C, le shell parent gère ce signal mais l'enfant le recevra - avec l'action par défaut - se terminera. (l'enfant peut également mettre en œuvre le traitement du signal)

Vous pouvez également utiliser des signaux trap dans votre script Shell ...

Et vous pouvez également définir la gestion du signal pour votre shell interactif, essayez de saisir ceci en haut de vous ~/.profile. (Assurez-vous que vous êtes déjà connecté et testez-le avec un autre terminal - vous pouvez vous verrouiller)

trap 'echo "Dont do this"' 2

Maintenant, chaque fois que vous appuyez sur CTRL+C dans votre Shell, il imprimera un message. N'oubliez pas de supprimer la ligne!

Si vous êtes intéressé, vous pouvez vérifier l'ancien /bin/sh gestion du signal dans le code source ici .

Dans ce qui précède, il y avait quelques fausses informations dans les commentaires (maintenant supprimés), donc si quelqu'un intéressé ici est un très bon lien - comment fonctionne le traitement du signal .

62
jm666

Tout d'abord, lisez l'article Wikipedia sur l'interface du terminal POSIX tout au long.

Le signal SIGINT est généré par la discipline de ligne du terminal et diffusé à tous les processus du groupe de processus de premier plan du terminal . Votre shell a déjà créé un nouveau groupe de processus pour la commande (ou pipeline de commandes) que vous avez exécutée et a indiqué au terminal que ce groupe de processus est son groupe de processus de premier plan (le terminal). Chaque pipeline de commandes simultanées a son propre groupe de processus, et le pipeline de commandes de premier plan est celui avec le groupe de processus que le shell a programmé dans le terminal comme groupe de processus de premier plan du terminal. La commutation des "travaux" entre le premier plan et l'arrière-plan est (quelques détails de côté) une question de Shell indiquant au terminal quel groupe de processus est maintenant le premier plan.

Le processus Shell lui-même se trouve dans un autre groupe de processus qui lui est propre et ne reçoit donc pas le signal lorsqu'un de ces groupes de processus est au premier plan. C'est si simple.

21
JdeBP

Le terminal envoie le signal INT (interruption) au processus qui est actuellement connecté au terminal. Le programme le reçoit ensuite et peut choisir de l'ignorer ou de quitter.

Aucun processus n'est forcément fermé de force (bien que par défaut, si vous ne gérez pas sigint, je crois que le comportement est d'appeler abort(), mais je devrais le rechercher).

Bien sûr, le processus en cours d'exécution est isolé du shell qui l'a lancé.

Si vous vouliez le shell parent pour aller, lancez votre programme avec exec:

exec ./myprogram

De cette façon, le shell parent est remplacé par le processus enfant

7
sehe

setpgid Exemple minimal de groupe de processus POSIX C

Il pourrait être plus facile à comprendre avec un exemple exécutable minimal de l'API sous-jacente.

Ceci illustre comment le signal est envoyé à l'enfant, si l'enfant n'a pas changé son 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

Exécuter sans setpgid

Sans aucun argument CLI, setpgid ne se fait pas:

./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 voir, le pgid des deux processus est le même, car il est hérité de fork.

Ensuite, chaque fois que vous touchez:

Ctrl + C

Il génère à nouveau:

sigint parent
sigint child

Cela montre comment:

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

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

Exécutez avec setpgid

Si vous exécutez avec un argument, par exemple:

./setpgid 1

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

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

Et maintenant, chaque fois que vous touchez:

Ctrl + C

seul le parent reçoit également le signal:

sigint parent

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

Ctrl + \

cependant, l'enfant a maintenant un PGID différent et ne reçoit pas ce signal! Cela peut être vu à partir de:

ps aux | grep setpgid

Vous devrez le tuer explicitement avec:

kill -9 16470

Cela montre clairement pourquoi les groupes de signaux existent: sinon nous obtiendrions un tas de processus restant à nettoyer manuellement tout le temps.

Testé sur Ubuntu 18.04.

CTRL+C est une carte de la commande kill. Lorsque vous appuyez dessus, kill envoie un signal SIGINT, ce qui interrompt le processus.

Tuer: http://en.wikipedia.org/wiki/Kill_ (commande)

SIGINT: http://en.wikipedia.org/wiki/SIGINT_ (POSIX)

1
Brice Favre