[LINUX] Différence entre le processus de premier plan et le processus d'arrière-plan compris par principe

Vous ne pouvez pas courir en arrière-plan?

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.

Processus de premier plan et d'arrière-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.

Meilleure exécution en arrière-plan

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.

mods bash

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.

Code source référencé

commandes proc: procps-3.2.8 GNU bash, version 4.1.2 Le processeur est x86_64.

OS CentOS release 6.8

compilateur

gcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-17)

Recommended Posts

Différence entre le processus de premier plan et le processus d'arrière-plan compris par principe
Quelle est la différence entre «pip» et «conda»?
À propos de la différence entre "==" et "is" en python
À propos de la différence entre PostgreSQL su et sudo
Quelle est la différence entre Unix et Linux?
Prise en compte de la différence entre la courbe ROC et la courbe PR
Différence approximative entre Unicode et UTF-8 (et ses compagnons)
BERT peut-il comprendre la différence entre «Ame (bonbons)» et «Ame (pluie)»?
Quelle est la différence entre usleep, nanosleep et clock_nanosleep?
Comment utiliser argparse et la différence entre optparse
Quelle est la différence entre les liens symboliques et les liens durs?
Différence entre processus et travail
Différence entre "categorical_crossentropy" et "sparse_categorical_crossentropy"
Comprendre la différence entre l'affectation cumulative aux variables et l'affectation cumulative aux objets
Différence entre régression et classification
Différence entre np.array et np.arange
Différence entre MicroPython et CPython
Différence entre ps a et ps -a
Différence entre return et print-Python
J'ai étudié le comportement de la différence entre lien dur et lien symbolique
Différence entre Ruby et Python Split
Différence entre java et python (mémo)
Différence entre list () et [] en Python
Différence entre SQLAlchemy filter () et filter_by ()
Différence entre == et est en python
Mémorandum (différence entre csv.reader et csv.dictreader)
(Remarque) Différence entre la passerelle et la passerelle par défaut
Différence entre tri et tri (mémorial)
[Python] Différence entre fonction et méthode
Différence entre SQLAlchemy flush () et commit ()
Python - Différence entre exec et eval
[Python] Différence entre randrange () et randint ()
[Python] Différence entre trié et trié (Colaboratoire)
[Introduction à Python] Quelle est la différence entre une liste et un taple?
[Xg boost] Différence entre softmax et softprob
[Django ORM] Différence entre values () et only ()
Différences dans la relation entre PHP et Python enfin et quitter
Différence entre @classmethod et @staticmethod en Python
Différence entre append et + = dans la liste Python
Différence entre non local et global en Python
Différence entre la régression linéaire, la régression Ridge et la régression Lasso
[Python] Différence entre la méthode de classe et la méthode statique
Compilation et exécution Java comprises par CLI
Différence entre le fichier env_file docker-compose et le fichier .env
La relation subtile entre Gentoo et pip
[Python Iroha] Différence entre List et Tuple
[python] Différence entre la sortie rand et randn
Différence de vitesse entre wsgi, bouteille et flacon
Différence entre numpy.ndarray et list (dimension, taille)
Différence entre ls -l et la commande cat
Vérification de la différence et de la compatibilité entre keras et tf.keras # 1