Étant donné un processus Shell (par exemple sh
) et son processus enfant (par exemple cat
), comment puis-je simuler le comportement de Ctrl+C en utilisant l'ID de processus de Shell?
C'est ce que j'ai essayé:
Exécuter sh
puis cat
:
[user@Host ~]$ sh
sh-4.3$ cat
test
test
Envoi de SIGINT
vers cat
depuis un autre terminal:
[user@Host ~]$ kill -SIGINT $PID_OF_CAT
cat
a reçu le signal et s'est terminé (comme prévu).
L'envoi du signal au processus parent ne semble pas fonctionner. Pourquoi le signal n'est-il pas propagé à cat
lorsqu'il est envoyé à son processus parent sh
?
Cela ne fonctionne pas:
[user@Host ~]$ kill -SIGINT $PID_OF_SH
La première chose est de comprendre comment CTRL+C travaux.
Lorsque vous appuyez sur CTRL+C, votre émulateur de terminal envoie un caractère ETX (fin de texte/0x03).
Le TTY est configuré de telle sorte que lorsqu'il reçoit ce caractère, il envoie un SIGINT au groupe de processus de premier plan du terminal. Cette configuration peut être visualisée en faisant stty -a
et en regardant intr = ^C;
. Le spécification POSIX indique que lorsque INTR est reçu, il doit envoyer un SIGINT au groupe de processus de premier plan de ce terminal.
Donc, maintenant la question est, comment déterminez-vous ce qu'est 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 (SIGTSTP, 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.
Maintenant que nous savons ce qu'est l'ID de groupe de processus, nous devons simuler le comportement POSIX de l'envoi d'un signal à l'ensemble du groupe.
Cela peut être fait avec kill
en mettant un -
devant l'ID du groupe.
Par exemple, si votre ID de groupe de processus est 1234, vous utiliseriez:
kill -INT -1234
Donc, ce qui précède couvre comment simuler CTRL+C comme un processus manuel. Mais que faire si vous connaissez le numéro ATS et que vous souhaitez simuler CTRL+C pour ce terminal?
Cela devient très simple.
Assumons $tty
est le terminal que vous souhaitez cibler (vous pouvez l'obtenir en exécutant tty | sed 's#^/dev/##'
dans le terminal).
kill -INT -$(ps h -t $tty -o tpgid | uniq)
Cela enverra un SIGINT à n'importe quel groupe de processus de premier plan $tty
est.
Comme dit vinc17 , il n'y a aucune raison que cela se produise. Lorsque vous saisissez une séquence de touches générant un signal (par exemple, Ctrl+C), le signal est envoyé à tous les processus attachés (associés) au terminal. Il n'existe aucun mécanisme de ce type 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 sont généralement dans le groupe de processus du shell (au moins, s'ils ne sont pas asynchrones), donc envoyer le signal au négatif du PID du shell peut faire ce que vous voulez.
Comme souligne vinc17 , cela ne fonctionne pas pour les shells interactifs. Voici une alternative qui pourrait fonctionner:
kill -SIGINT - $ (echo $ (ps -pPID_of_Shell o tpgid =))
ps -pPID_of_Shell
Obtient des informations sur le processus sur le shell. o tpgid=
Indique à ps
de ne sortir que l'ID du groupe de processus du terminal, sans en-tête. Si elle est inférieure à 10000, ps
l'affichera avec le ou les espaces de tête; la $(echo …)
est une astuce rapide pour supprimer les espaces de début (et de fin).
J'ai réussi à faire fonctionner cela sur des tests superficiels sur une machine Debian.
La question contient sa propre réponse. L'envoi du SIGINT
au processus cat
avec kill
est une simulation parfaite de ce qui se passe lorsque vous appuyez sur Ctrl+C.
Pour être plus précis, le caractère d'interruption (^C
par défaut) envoie SIGINT
à chaque processus du groupe de processus de premier plan du terminal. Si au lieu de cat
vous exécutiez une commande plus compliquée impliquant plusieurs processus, vous devez tuer le groupe de processus pour obtenir le même effet que ^C
.
Lorsque vous exécutez une commande externe sans le &
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 termine.
C'est là que vous semblez être devenu la victime d'une idée fausse commune: 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 qu'il a fait le travail de configuration (création de processus, paramétrage du mode terminal, création de canaux et redirection d'autres descripteurs de fichiers, et exécution du programme cible) le Shell attend juste . Ce que vous tapez dans cat
ne passe pas par le shell, que ce soit une entrée normale ou un caractère spécial générateur de signal comme ^C
. Le processus cat
a un accès direct au terminal via ses propres descripteurs de fichier, et le terminal a la possibilité d'envoyer des signaux directement au processus cat
car il s'agit du groupe de processus de premier plan. Le Shell s'est éloigné.
Après la fin du processus cat
, le shell sera averti, car il est le parent du processus cat
. Ensuite, le Shell devient actif et se remet au premier plan.
Voici un exercice pour augmenter votre compréhension.
À l'invite du shell dans un nouveau terminal, exécutez cette commande:
exec cat
Le mot clé exec
oblige le shell à exécuter cat
sans créer de processus enfant. Le shell est remplacé par cat
. Le PID qui appartenait autrefois au Shell est maintenant le PID de cat
. Vérifiez cela avec ps
dans un autre terminal. Tapez quelques lignes aléatoires et voyez que cat
vous les répète, ce qui prouve qu'il se comporte toujours normalement même s'il n'a pas de processus Shell en tant que parent. Que se passera-t-il lorsque vous appuyez sur Ctrl+C maintenant?
Réponse:
SIGINT est livré au processus du chat, qui meurt. Comme il s'agissait du seul processus sur le terminal, la session se termine, comme si vous aviez dit "quitter" à une invite du shell. En effet, le chat était votre Shell pendant un certain temps.
Il n'y a aucune raison de propager le SIGINT
à l'enfant. De plus, la spécification system()
POSIX dit: "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 le SIGINT
reçu, par exemple suivre un vrai Ctrl+C, cela signifierait que le processus enfant recevrait le signal SIGINT
deux fois, ce qui pourrait avoir un comportement indésirable.
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);
}
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:
kill(-pgid, SIGINT)
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 uniquement par le parent:
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 à nettoyer manuellement tout le temps.
Testé sur Ubuntu 18.04.