web-dev-qa-db-fra.com

anomalie printf après "fork ()"

OS: Linux, Langue: C pur

J'avance dans l'apprentissage de la programmation C en général, et de la programmation C sous UNIX dans un cas particulier.

J'ai détecté un comportement étrange (pour moi) de la fonction printf() après avoir utilisé un appel fork().

Code

#include <stdio.h>
#include <system.h>

int main()
{
    int pid;
    printf( "Hello, my pid is %d", getpid() );

    pid = fork();
    if( pid == 0 )
    {
            printf( "\nI was forked! :D" );
            sleep( 3 );
    }
    else
    {
            waitpid( pid, NULL, 0 );
            printf( "\n%d was forked!", pid );
    }
    return 0;
}

Sortie

Hello, my pid is 1111
I was forked! :DHello, my pid is 1111
2222 was forked!

Pourquoi la deuxième chaîne "Hello" s'est-elle produite dans la sortie de l'enfant?

Oui, c'est exactement ce que le parent a imprimé quand il a commencé, avec le pid du parent.

Mais! Si nous plaçons un \n caractère à la fin de chaque chaîne on obtient la sortie attendue:

#include <stdio.h>
#include <system.h>

int main()
{
    int pid;
    printf( "Hello, my pid is %d\n", getpid() ); // SIC!!

    pid = fork();
    if( pid == 0 )
    {
            printf( "I was forked! :D" ); // removed the '\n', no matter
            sleep( 3 );
    }
    else
    {
            waitpid( pid, NULL, 0 );
            printf( "\n%d was forked!", pid );
    }
    return 0;
}

Sortie:

Hello, my pid is 1111
I was forked! :D
2222 was forked!

Pourquoi cela arrive-t-il? Est-ce un comportement correct ou est-ce un bug?

64
pechenie

Je note que <system.h> Est un en-tête non standard; Je l'ai remplacé par <unistd.h> Et le code compilé proprement.

Lorsque la sortie de votre programme va vers un terminal (écran), elle est mise en mémoire tampon de ligne. Lorsque la sortie de votre programme va dans un tube, elle est entièrement tamponnée. Vous pouvez contrôler le mode de mise en mémoire tampon par la fonction C standard setvbuf() et le _IOFBF (Mise en mémoire tampon complète), _IOLBF (Mise en mémoire tampon de ligne) et _IONBF (Non tampons).

Vous pouvez le démontrer dans votre programme révisé en canalisant la sortie de votre programme vers, par exemple, cat. Même avec les sauts de ligne à la fin des chaînes printf(), vous verriez la double information. Si vous l'envoyez directement au terminal, vous ne verrez alors qu'un seul lot d'informations.

La morale de l'histoire est de faire attention à appeler fflush(0); pour vider tous les tampons d'E/S avant de bifurquer.


Analyse ligne par ligne, comme demandé (accolades etc. supprimées - et espaces de tête supprimés par l'éditeur de balisage):

  1. printf( "Hello, my pid is %d", getpid() );
  2. pid = fork();
  3. if( pid == 0 )
  4. printf( "\nI was forked! :D" );
  5. sleep( 3 );
  6. else
  7. waitpid( pid, NULL, 0 );
  8. printf( "\n%d was forked!", pid );

L'analyse:

  1. Copie "Bonjour, mon pid est 1234" dans le tampon pour la sortie standard. Puisqu'il n'y a pas de nouvelle ligne à la fin et que la sortie s'exécute en mode ligne tamponnée (ou en mode mémoire tampon complète), rien n'apparaît sur le terminal.
  2. Nous donne deux processus distincts, avec exactement le même matériau dans le tampon standard.
  3. L'enfant a pid == 0 Et exécute les lignes 4 et 5; le parent a une valeur non nulle pour pid (l'une des rares différences entre les deux processus - les valeurs de retour de getpid() et getppid() sont deux autres).
  4. Ajoute un retour à la ligne et "J'étais fourchu!: D" dans le tampon de sortie de l'enfant. La première ligne de sortie apparaît sur le terminal; le reste est conservé dans le tampon car la sortie est mise en mémoire tampon de ligne.
  5. Tout s'arrête pendant 3 secondes. Après cela, l'enfant sort normalement par le retour en fin de principal. À ce stade, les données résiduelles dans le tampon de sortie standard sont vidées. Cela laisse la position de sortie à la fin d'une ligne car il n'y a pas de nouvelle ligne.
  6. Le parent vient ici.
  7. Le parent attend que l'enfant ait fini de mourir.
  8. Le parent ajoute une nouvelle ligne et "1345 était fourchu!" vers le tampon de sortie. La nouvelle ligne vide le message "Bonjour" dans la sortie, après la ligne incomplète générée par l'enfant.

Le parent quitte maintenant normalement par le retour à la fin du principal et les données résiduelles sont vidées; comme il n'y a toujours pas de nouvelle ligne à la fin, la position du curseur se trouve après le point d'exclamation et l'invite du shell apparaît sur la même ligne.

Ce que je vois c'est:

Osiris-2 JL: ./xx
Hello, my pid is 37290
I was forked! :DHello, my pid is 37290
37291 was forked!Osiris-2 JL: 
Osiris-2 JL: 

Les numéros PID sont différents - mais l'apparence générale est claire. L'ajout de sauts de ligne à la fin des instructions printf() (qui devient très rapidement une pratique standard) modifie considérablement la sortie:

#include <stdio.h>
#include <unistd.h>

int main()
{
    int pid;
    printf( "Hello, my pid is %d\n", getpid() );

    pid = fork();
    if( pid == 0 )
        printf( "I was forked! :D %d\n", getpid() );
    else
    {
        waitpid( pid, NULL, 0 );
        printf( "%d was forked!\n", pid );
    }
    return 0;
}

Je reçois maintenant:

Osiris-2 JL: ./xx
Hello, my pid is 37589
I was forked! :D 37590
37590 was forked!
Osiris-2 JL: ./xx | cat
Hello, my pid is 37594
I was forked! :D 37596
Hello, my pid is 37594
37596 was forked!
Osiris-2 JL:

Notez que lorsque la sortie est envoyée au terminal, elle est mise en mémoire tampon, de sorte que la ligne "Bonjour" apparaît avant la fork() et il n'y avait qu'une seule copie. Lorsque la sortie est dirigée vers cat, elle est entièrement tamponnée, donc rien n'apparaît avant la fork() et les deux processus ont la ligne 'Hello' dans le tampon à vider.

82
Jonathan Leffler

La raison en est que sans le \n à la fin de la chaîne de formatage, la valeur n'est pas immédiatement imprimée à l'écran. Au lieu de cela, il est mis en mémoire tampon dans le processus. Cela signifie qu'il n'est réellement imprimé qu'après l'opération de fourche, ce qui vous permet de l'imprimer deux fois.

Ajout du \n force cependant le vidage du tampon et sa sortie à l'écran. Cela se produit avant la fourche et n'est donc imprimé qu'une seule fois.

Vous pouvez forcer cela à se produire en utilisant la méthode fflush. Par exemple

printf( "Hello, my pid is %d", getpid() );
fflush(stdout);
25
JaredPar

fork() crée efficacement une copie du processus. Si, avant d'appeler fork(), il avait des données qui ont été mises en mémoire tampon, le parent et l'enfant auront les mêmes données mises en mémoire tampon. La prochaine fois que chacun d'eux fera quelque chose pour vider sa mémoire tampon (comme imprimer une nouvelle ligne dans le cas d'une sortie de terminal), vous verrez cette sortie mise en mémoire tampon en plus de toute nouvelle sortie produite par ce processus. Donc, si vous allez utiliser stdio à la fois dans le parent et dans l'enfant, vous devez fflush avant de bifurquer, pour vous assurer qu'il n'y a pas de données tamponnées.

Souvent, l'enfant n'est utilisé que pour appeler un exec* une fonction. Étant donné que cela remplace l'image de processus enfant complète (y compris les tampons), il n'est techniquement pas nécessaire de fflush si c'est vraiment tout ce que vous allez faire chez l'enfant. Cependant, s'il peut y avoir des données en mémoire tampon, vous devez faire attention à la façon dont une erreur d'exécution est gérée. En particulier, évitez d'imprimer l'erreur sur stdout ou stderr en utilisant une fonction stdio (write est ok), puis appelez _exit (ou _Exit) plutôt que d'appeler exit ou simplement de retourner (ce qui videra toute sortie mise en mémoire tampon). Ou évitez complètement le problème en rinçant avant de bifurquer.

5
mark4o