Soudainement, savez-vous pourquoi le traitement sur un périphérique de type Unix entraîne ce qui suit? En outre, y a-t-il un moyen d'empêcher que cela se produise?
python
$ top &
[1] 11424
$ jobs -l
[1]+11424 Arrêté(Sortie de borne) top
Lorsque vous travaillez avec le système d'exploitation dans le shell, vous entendez souvent les termes processus d'arrière-plan et de premier plan.
Si vous êtes ingénieur, vous n'en avez probablement pas entendu parler, et vous connaissez probablement la différence, mais vous n'avez probablement pas eu beaucoup d'occasions de le comprendre systématiquement. ??
À l'origine, j'écrivais un article avec l'intention de poster sur le processus démon sur Qiita, mais en raison de la nature du processus démon, je dois mentionner le processus d'arrière-plan, j'ai donc fait une entrée avec l'explication. Je pensais que ce serait compliqué, alors cette fois je me suis concentré sur le processus de fond.
Ce qui précède est le comportement lorsque le programme top est exécuté en arrière-plan. top est un programme qui surveille les ressources système en temps réel, mais si vous l'exécutez en arrière-plan, le processus s'arrêtera immédiatement. Bien sûr, c'est parce qu'il est mis en œuvre, mais pourquoi s'arrête-t-il?
Qu'est-ce qu'un processus d'arrière-plan en premier lieu? Et qu'est-ce qu'un processus de premier plan?
Tout d'abord, clarifions la différence entre le processus d'arrière-plan et le processus de premier plan.
Normalement, le processus lancé par l'utilisateur doit être démarré (forké) par le shell (dans ce cas, en supposant l'interface de ligne de commande), donc lors de l'utilisation du pilote de terminal à partir d'un périphérique tel qu'un clavier, Ctrl + C
Vous pouvez notifier le processus de SIGINT en entrant.
Et le processus notifié de SIGINT se terminera dans la plupart des situations, en fonction de l'implémentation.
Le groupe de processus qui est prêt à accepter l'entrée du terminal de cette manière est appelé le groupe de processus de premier plan, et tous les autres groupes de processus qui ne sont pas sont appelés le groupe de processus d'arrière-plan.
Alternativement, il est simplement appelé processus de premier plan ou processus d'arrière-plan.
Cette différence est un peu déroutante, mais en général, lorsque vous dites que vous voulez faire quelque chose en arrière-plan, cela signifie souvent que le processus de premier plan est un shell. Il y a de nombreux cas parce qu'il y a des cas où ce n'est pas le cas.
Créons un programme pour vérifier l'état.
python
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main (int c, char *argv[]) {
for (;;){
fputs("test\n", stdout);
//0 is file descriptor associated with the streams stdin.
printf("stdin is %d\n", (int)tcgetpgrp(0));
//1 is file descriptor associated with the streams stdout.
printf("stdout is %d\n", (int)tcgetpgrp(1));
sleep(5);
}
return EXIT_SUCCESS;
}
La fonction tcgetpgrp prend un descripteur de fichier comme argument et obtient l'ID de groupe de processus du processus de premier plan correspondant. Cette fois, nous transmettons les descripteurs de fichier d'entrée et de sortie standard.
Pour le descripteur de fichier, veuillez vous référer à [Hacking the Linux file descriptor] que j'ai écrit auparavant.
python
$ ./a.out
Exécutez le fichier exécutable compilé au premier plan. Lancer au premier plan est juste une question de course.
python
test
stdin is 10455
stdout is 10455
test
stdin is 10455
stdout is 10455
Ensuite, le test de chaîne de caractères et l'ID de processus du groupe de processus de premier plan du terminal référencé dans l'entrée standard et la sortie standard sont affichés à l'écran.
Étant donné que ce processus n'a pas de condition d'arrêt dans le logiciel, il ne se terminera pas pour toujours à moins qu'un signal tel que SIGINT ne soit notifié au processus de l'extérieur.
Si vous vérifiez avec ps -jf f
,
python
UID PID PPID PGID SID C STIME TTY STAT TIME CMD
tajima 10360 10359 10360 10360 0 21:55 pts/1 Ss 0:00 -bash
tajima 10455 10360 10455 10360 0 22:25 pts/1 S+ 0:00 \_ ./a.out
Puisque le PGID en cours d'exécution est 10455, nous savons que ./a.out est le processus de premier plan. En d'autres termes, il est possible de notifier, d'entrer et de sortir des signaux du terminal vers le processus ./a.out.
J'ai tapé «Ctrl + C» sur le clavier comme essai, et quand j'ai notifié SIGINT, cela s'est terminé.
Maintenant, exécutons-le en arrière-plan.
python
$ ./a.out &
Si vous ajoutez & à la fin, le shell exécutera le programme cible en arrière-plan.
python
test
stdin is 10360
stdout is 10360
test
stdin is 10360
stdout is 10360
Bien sûr, l'ID du groupe de processus a changé, mais il n'est pas exagéré de dire que c'est la même chose que d'exécuter un programme au premier plan sur l'écran.
Si vous vérifiez avec ps -jf f
,
python
UID PID PPID PGID SID C STIME TTY STAT TIME CMD
tajima 10360 10359 10360 10360 0 21:55 pts/1 Ss+ 0:00 -bash
tajima 10460 10360 10460 10360 0 22:52 pts/1 S 0:00 \_ ./a.out
Puisque le PGID du groupe de processus de premier plan du terminal était 10360, nous savons que le bash (shell) est le processus de premier plan. Et vous pouvez voir que ./a.out est le processus d'arrière-plan. En d'autres termes, il est possible de notifier et d'entrer des signaux dans le processus bash à partir du terminal.
Si vous essayez de saisir Ctrl + C
à partir du clavier et notifiez SIGINT, il apparaîtra comme s'il s'était terminé pendant un moment. Mais bientôt
python
test
stdin is 10360
stdout is 10360
test
stdin is 10360
stdout is 10360
Est répété à l'infini.
C'est parce que le précédent Ctrl + C
a été notifié à bash, pas à ./a.out.
Pour faire de ce processus un processus de premier plan, c'est-à-dire pour lier l'entrée / la sortie du terminal à ./a.out, recherchez le numéro de travail avec la commande intégrée shell (bash) jobs
et trouvez ce numéro dans fg. Passez au processus de premier plan avec.
En passant, le processus est géré par le système d'exploitation, mais le travail est géré par le shell en cours d'exécution.
python
$ jobs
[1]Fonctionnement./a.out &
$ fg %1
Si tu fais
python
test
stdin is 10460
stdout is 10460
test
stdin is 10460
stdout is 10460
Comme vous pouvez le voir, l'ID de groupe de processus du processus de premier plan est devenu ./a.out.
Alors, en interne, comment l'état du processus change-t-il lorsque le processus d'arrière-plan est forcé au premier plan par fg?
Plus tôt, j'ai expliqué que la fonction appelée tcgetpgrp est une fonction permettant d'obtenir l'ID de groupe de processus du processus de premier plan, mais au contraire, il existe également une API appelée fonction tcsetpgrp, qui spécifie le terminal correspondant au descripteur de fichier en tant que groupe de processus spécifique. Il a pour rôle de le définir dans l'ID.
Écrivons du code qui transforme le processus de premier plan en processus d'arrière-plan sans l'aide du shell.
Le fait est que l'ID de groupe de processus du shell doit être l'ID de processus de premier plan.
Tout d'abord, recherchez l'ID de groupe de processus du shell actuel.
python
PID PGID SID TTY STAT TIME COMMAND
15243 15243 15243 pts/1 Ss 0:00 -bash
25023 25023 15243 pts/1 R+ 0:00 \_ ps -j f
Gardez cela à l'esprit car le PGID est 15243.
python
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main (int c, char *argv[]) {
for (;;){
fputs("test\n", stdout);
//0 is file descriptor associated with the streams stdin.
printf("stdin is %d\n", (int)tcgetpgrp(0));
//1 is file descriptor associated with the streams stdout.
printf("stdout is %d\n", (int)tcgetpgrp(1));
sleep(5);
if (tcsetpgrp(0, 15243) < 0) {
perror("tcsetpgrp() for stdin error");
}
if (tcsetpgrp(1, 15243) < 0) {
perror("tcsetpgrp() for stdout error");
}
}
return EXIT_SUCCESS;
Si je le fais simplement de cette façon, je me demande si le premier plan va se transformer en processus d'arrière-plan après 5 secondes, mais cela ne fonctionne pas.
Lorsqu'un processus qui est devenu un processus d'arrière-plan après la réussite de tcsetpgrp essaie d'écrire sur le terminal par fputs, un signal SIGTTOU est envoyé, mais ce comportement par défaut est que le processus est arrêté car le processus est arrêté. Parce que ce sera fait.
python
$jobs -l
[1]+29669 Arrêté(Sortie de borne) ./a.out
Cependant, il y a une autre petite raison d'expliquer cela, dont je parlerai plus tard.
Voici l'explication de la première question.
Revenons sur la première question. Essayez d'exécuter la commande top en arrière-plan pour comprendre l'état actuel du système.
python
$ top &
Si vous vérifiez l'état du processus dans cet état,
jobs -l
[1]+6432 arrêté(Sortie de borne) top
Vous pouvez voir qu'il a été arrêté par la sortie du terminal. Cela signifie que la notification SIGTTOU a arrêté le processus.
Vérifions le code source de top.
top.c3342-3363
int main (int dont_care_argc, char *argv[])
{
(void)dont_care_argc;
before(*argv);
windows_stage1(); // top (sic) slice
configs_read(); // > spread etc, <
parse_args(&argv[1]); // > lean stuff, <
whack_terminal(); // > onions etc. <
windows_stage2(); // as bottom slice
// +-------------+
// +-------------+
signal(SIGALRM, end_pgm);
signal(SIGHUP, end_pgm);
signal(SIGINT, end_pgm);
signal(SIGPIPE, end_pgm);
signal(SIGQUIT, end_pgm);
signal(SIGTERM, end_pgm);
signal(SIGTSTP, suspend);
signal(SIGTTIN, suspend);
signal(SIGTTOU, suspend);
signal(SIGCONT, wins_resize_sighandler);
signal(SIGWINCH, wins_resize_sighandler);
Cela se produit dans une fonction appelée whack_terminal dans la fonction principale.
top.c1932-1954
static void whack_terminal (void)
{
struct termios newtty;
if (Batch) {
setupterm("dumb", STDOUT_FILENO, NULL);
return;
}
setupterm(NULL, STDOUT_FILENO, NULL);
if (tcgetattr(STDIN_FILENO, &Savedtty) == -1)
std_err("failed tty get");
newtty = Savedtty;
newtty.c_lflag &= ~(ICANON | ECHO);
newtty.c_oflag &= ~(TAB3);
newtty.c_cc[VMIN] = 1;
newtty.c_cc[VTIME] = 0;
Ttychanged = 1;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &newtty) == -1) {
putp(Cap_clr_scr);
std_err(fmtmk("failed tty set: %s", strerror(errno)));
}
tcgetattr(STDIN_FILENO, &Rawtty);
Il est provoqué par l'exécution d'une fonction appelée tcsetattr qui change la valeur d'attribut du terminal. Si cette fonction est exécutée comme processus d'arrière-plan, SIGTTOU enverra le processus et l'exécution sera suspendue.
Ainsi, par exemple, si vous modifiez le code source de top comme indiqué ci-dessous et que vous le compilez, vous aurez une commande top qui peut être exécutée en continu même en arrière-plan.
top.c1932-1954
int main (int dont_care_argc, char *argv[])
{
signal(SIGTTOU, SIG_IGN);
signal(SIGTTIN, SIG_IGN);
(void)dont_care_argc;
before(*argv);
windows_stage1(); // top (sic) slice
configs_read(); // > spread etc, <
parse_args(&argv[1]); // > lean stuff, <
whack_terminal(); // > onions etc. <
windows_stage2(); // as bottom slice
// +-------------+
// +-------------+
signal(SIGALRM, end_pgm);
signal(SIGHUP, end_pgm);
signal(SIGINT, end_pgm);
signal(SIGPIPE, end_pgm);
signal(SIGQUIT, end_pgm);
signal(SIGTERM, end_pgm);
signal(SIGTSTP, suspend);
//signal(SIGTTIN, suspend);
//signal(SIGTTOU, suspend);
signal(SIGCONT, wins_resize_sighandler);
signal(SIGWINCH, wins_resize_sighandler);
Il indique simplement au processus d'ignorer les signaux SIGTTOU et SIGTTIN et commente l'exécution de la suspension du gestionnaire de signaux. Si vous construisez top dans cet état,
python
$ top &
L'état des ressources système est périodiquement craché sur la sortie standard de manière asynchrone sans s'arrêter. Je ne pense pas que ce soit pratique en général, mais je l'aime en l'appelant infini top. Si vous êtes intéressé, essayez-le.
Par ailleurs, en général, un autre cas où le processus d'arrière-plan s'arrête est supposé lorsque le terminal est prêt à accepter l'entrée. Ceci est notifié de SIGTTIN, mais son comportement par défaut est le même arrêt de processus que SIGTTOU. Vérifions cela avec un exemple de code simple.
input.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main (int argc, char *argv[]) {
char a[10];
if (fgets(a, sizeof(a), stdin) == NULL) {
fprintf(stderr, "invalid input string.\n");
exit(EXIT_FAILURE);
}
a[strlen(a)-1] = '\0';
printf("%s\n", a);
return EXIT_SUCCESS;
}
Exécutez ceci en arrière-plan.
python
$ ./a.out &
Lorsque je vérifie l'état du processus,
python
$ jobs -l
[1]+6490 Arrêté(Entrée de borne) ./a.out
Il est arrêté par l'entrée du terminal (SIGTTIN) comme.
Vérifions SIGTTOU avec un programme simple au cas où. Dans l'exemple du haut ci-dessus, j'ai expliqué que la notification de SIGTTOU aux processus d'arrière-plan provoque l'arrêt du processus, ce qui devrait être le cas.
out.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main (int argc, char *argv[]) {
char a[10] = "test";
for (;;) {
printf("%s\n", a);
sleep(3);
}
return EXIT_SUCCESS;
}
Lorsque j'exécute ce programme en arrière-plan,
python
$ ./a.out &
test
test
jobs -l
[1]+6562 en cours d'exécution./a.out &
Le processus est en cours d'exécution et ne s'arrête pas. Qu'est-ce que ça veut dire?
En fait, SIGTTOU n'est pas aussi simple que SIGTTIN, et il nécessite des paramètres qui entrent dans les informations de contrôle du pilote de terminal.
Le groupe de traitement du noyau qui contrôle le flux de données entre le processus et le terminal externe est appelé pilote de terminal (pilote tty), et le comportement lié à SIGTTOU change en fonction du paramétrage de ce pilote de terminal.
Vous pouvez vérifier les paramètres actuels du terminal avec la commande stty.
python
$ stty
speed 9600 baud; line = 0;
eol = M-^?; eol2 = M-^?;
-brkint ixany
-echok
En fonction de l'environnement, SIGTTOU est envoyé au processus lorsque le programme est exécuté en arrière-plan uniquement lorsque l'indicateur d'arrêt est défini dans ce paramètre.
Pour définir tostop avec la commande précédente, procédez comme suit.
python
$ stty tostop
$ stty
speed 9600 baud; line = 0;
eol = M-^?; eol2 = M-^?;
-brkint ixany
-echok tostop
Un paramètre appelé tostop a été ajouté. Dans cet état, essayez d'exécuter le programme précédent.
python
$ ./a.out &
jobs -l
[1]+11236 Arrêté(Sortie de borne) ./a.out
De cette manière, contrairement à la précédente, le processus est arrêté en détectant la sortie vers le terminal. Par conséquent, dans un environnement où l'indicateur tostop est défini depuis le début, vous devriez être en mesure de confirmer qu'il s'arrêtera à la sortie du terminal pendant l'exécution en arrière-plan sans aucun paramètre spécial.
Pour annuler le paramètre avec la commande stty, ajoutez le nom du drapeau.
python
$ stty -tostop
$ stty
speed 9600 baud; line = 0;
eol = M-^?; eol2 = M-^?;
-brkint ixany
-echok
Maintenant, définissons-le dans le programme sans utiliser stty.
out2.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <signal.h>
int main (int argc, char *argv[]) {
struct termios newtty, oldtty;
if (tcgetattr(1, &oldtty) == -1) {
fprintf(stderr, "tcgetattr() failed.\n");
exit(EXIT_FAILURE);
}
newtty = oldtty;
newtty.c_lflag |= TOSTOP;
if (tcsetattr(1, TCSAFLUSH, &newtty) == -1) {
fprintf(stderr, "tcsetattr() failed.\n");
exit(EXIT_FAILURE);
}
char a[10] = "test";
for (;;) {
printf("%s\n", a);
sleep(3);
}
return EXIT_SUCCESS;
}
Le pilote de terminal est défini et acquis à l'aide d'un type de données appelé structure termios. Ce code est similaire à la partie de réglage du code supérieur introduit plus tôt, mais je pense qu'il est plus facile de comprendre pourquoi le processus d'arrière-plan s'est arrêté en raison de la sortie vers le terminal.
Le fait est qu'après l'acquisition des paramètres actuels du pilote de terminal, le drapeau TOSTOP est défini dessus et le pilote de terminal est à nouveau défini.
Au fait, je n'ai pas réussi à faire un processus d'arrière-plan à partir d'un processus de premier plan par la force en raison de la notification de SIGTTOU, donc je vais modifier bash.
L'opération sera différente selon le shell, mais dans le cas de bash, le traitement de contrôle du signal est effectué dans la partie suivante.
jobs.c1904-1910
void
default_tty_job_signals ()
{
set_signal_handler (SIGTSTP, SIG_DFL);
set_signal_handler (SIGTTIN, SIG_DFL);
set_signal_handler (SIGTTOU, SIG_DFL);
}
Par conséquent, modifiez le processus lors de la création d'un processus enfant comme suit.
jobs.c1762-1767
if (pipeline_pgrp == shell_pgrp)
ignore_tty_job_signals ();
else
default_tty_job_signals ();
set_signal_handler (SIGTTOU, SIG_IGN);
Mettez set_signal_handler (SIGTTOU, SIG_IGN) ici et construisez bash. Puisque SIGTTIN et SIGTSTP ne sont pas particulièrement pertinents cette fois, Ignorer n'est pas spécifié.
En faisant cela, le processus qui a été arrêté plus tôt sera démarré au premier plan arrêté, puis exécuté en arrière-plan. L'entrée du terminal n'atteint pas le processus démarré.
Cependant, en l'état, l'entrée et la sortie du terminal n'atteignent pas le processus démarré, on ne peut donc pas dire qu'il s'exécute strictement en arrière-plan, mais la principale différence entre l'exécution au premier plan et en arrière-plan est Je pense que tu comprends.
Pour l'implémenter pleinement, vous devez comprendre comment fonctionne le contrôle des tâches, mais j'aimerais également l'explorer séparément.
Cette fois, nous avons abordé le processus de premier plan et le processus d'arrière-plan. La prochaine fois, sur cette base, j'aborderai le processus démon.
commandes proc: procps-3.2.8 GNU bash, version 4.1.2 Le processeur est x86_64.
OS CentOS release 6.8
gcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-17)
Recommended Posts