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?
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):
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 );
L'analyse:
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).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.
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);
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.