Pourquoi un programme avec fork () imprime-t-il parfois sa sortie plusieurs fois?

50

Le programme 1 Hello worldest imprimé une seule fois, mais lorsque je le supprime \net le lance (programme 2), la sortie est imprimée 8 fois. Quelqu'un peut-il m'expliquer s'il vous plaît la signification de \nici et comment cela affecte le fork()?

Programme 1

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("hello world...\n");
    fork();
    fork();
    fork();
}

Sortie 1:

hello world... 

Programme 2

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("hello world...");
    fork();
    fork();
    fork();
}

Sortie 2:

hello world... hello world...hello world...hello world...hello world...hello world...hello world...hello world...
lmaololrofl
la source
10
Essayez d'exécuter le programme 1 avec la sortie dans un fichier ( ./prog1 > prog1.out) ou un tuyau ( ./prog1 | cat). Préparez-vous à avoir l'esprit brisé. :-) ⁠
G-Man dit 'Réintégrez Monica' le
Questions-réponses pertinentes couvrant une autre variante de ce problème: le système C ("bash") ignore stdin
Michael Homer
13
Cela a rassemblé quelques votes serrés, de sorte qu'un commentaire à ce sujet: les questions sur "API C et interfaces système" sont explicitement autorisées . Les problèmes de mise en mémoire tampon sont fréquents également dans les scripts shell, et fork()sont quelque peu spécifiques à Unix également.
ilkkachu
@ilkkachu en fait, si vous lisez ce lien et que vous cliquez sur la méta-question à laquelle il fait référence, cela indique clairement qu'il s'agit d'un sujet hors sujet. Ce n'est pas parce que quelque chose est en C, et que Unix en a, que le sujet est abordé.
Patrick
@ Patrick, en fait, je l'ai fait. Et je pense toujours que cela correspond à la clause "dans la limite du raisonnable", mais bien sûr, ce n'est que moi.
Ilkkachu

Réponses:

93

Lors de la sortie sur une sortie standard à l'aide de la printf()fonction de la bibliothèque C , la sortie est généralement mise en mémoire tampon. La mémoire tampon n'est pas vidée jusqu'à ce que vous sortiez une nouvelle ligne, appelez fflush(stdout)ou quittez le programme (pas via l'appel _exit()). Le flux de sortie standard est par défaut mis en mémoire tampon de ligne de cette manière lorsqu'il est connecté à un TTY.

Lorsque vous branchez le processus dans "Programme 2", les processus enfants héritent de toutes les parties du processus parent, y compris le tampon de sortie non vidé. Cela copie efficacement le tampon non vidé dans chaque processus enfant.

Lorsque le processus se termine, les tampons sont vidés. Vous démarrez un total de huit processus (y compris le processus d'origine) et le tampon non vidé sera vidé à la fin de chaque processus.

C'est huit parce qu'à chaque fois, fork()vous obtenez deux fois le nombre de processus que vous aviez avant fork()(puisqu'ils sont inconditionnels), et vous en avez trois (2 3 = 8).

Kusalananda
la source
14
Connexes: vous pouvez vous mainavec _exit(0)juste faire un appel système de sortie sans tampons de rinçage, puis il sera imprimé zéro fois sans retour à la ligne. ( L'implémentation syscall de exit () et comment _exit (0) (sortie par syscall) m'empêche de recevoir du contenu stdout? ). Ou vous pouvez diriger Program1 dans catou rediriger vers un fichier et le voir être imprimé 8 fois. (stdout est mis en mémoire tampon par défaut lorsqu'il ne s'agit pas d'un téléscripteur). Ou ajouter un fflush(stdout)à l'affaire no-newline avant le 2 fork()...
Peter Cordes
17

Cela n'affecte en rien la fourche.

Dans le premier cas, vous vous retrouvez avec 8 processus sans rien à écrire, car le tampon de sortie a déjà été vidé (en raison de la \n).

Dans le second cas, vous avez encore 8 processus, chacun avec un tampon contenant "Hello world ..." et le tampon est écrit à la fin du processus.

edc65
la source
12

@Kusalananda a expliqué pourquoi la sortie est répétée . Si vous êtes curieux de savoir pourquoi la sortie est répétée 8 fois et pas seulement 4 fois (le programme de base + 3 fourchettes):

int main()
{
    printf("hello world...");
    fork(); // here it creates a copy of itself --> 2 instances
    fork(); // each of the 2 instances creates another copy of itself --> 4 instances
    fork(); // each of the 4 instances creates another copy of itself --> 8 instances
}
Honza Zidek
la source
2
c'est basique de fork
Prévt_Yadav
3
@ Debian_yadav n'est probablement évident que si vous connaissez ses implications. Comme vider les tampons stdio , par exemple.
Roaima
2
@Debian_yadav: en.wikipedia.org/wiki/False_consensus_effect - pourquoi devrions-nous poser des questions si tout le monde sait tout?
Honza Zidek
8
@ Debian_yadav Je ne peux pas lire l'esprit du PO alors je ne le sais pas. Quoi qu’il en soit, stackexchange est un endroit où d’autres aussi cherchent des connaissances et je pense que ma réponse peut être un complément utile à la bonne réponse de Kulasandra. Ma réponse ajoute quelque chose (basique mais utile), comparée à celle de l'edc65 qui ne fait que répéter ce que Kulasandra a dit 2 heures avant lui.
Honza Zidek
2
Ceci est juste un court commentaire à une réponse, pas une réponse réelle. La question demande "plusieurs fois" pas pourquoi c'est exactement 8.
pipe
3

L'arrière-plan important ici est qu'il stdoutest nécessaire que la norme mette en mémoire tampon la configuration par défaut.

Cela provoque un \nvidage de la sortie.

Comme le deuxième exemple ne contient pas de nouvelle ligne, la sortie n'est pas vidée et, en tant que fork()copie du processus entier, elle copie également l'état du stdouttampon.

Maintenant, ces fork()appels dans votre exemple créent 8 processus au total - tous avec une copie de l'état du stdouttampon.

Par définition, tous ces processus appellent exit()lors du retour depuis main()et des exit()appels fflush()suivis par fclose()sur tous les flux stdio actifs . Cela inclut stdoutet par conséquent, vous voyez le même contenu huit fois.

Il est recommandé d’appeler fflush()tous les flux avec une sortie en attente avant d’appeler fork()ou de laisser l’appel enfant appelé explicitement _exit()qui ne fait que quitter le processus sans vider les flux stdio.

Notez que l'appel exec()ne vide pas les tampons stdio. Vous pouvez donc ne pas vous soucier des tampons stdio si vous appelez (après l'appel fork()) exec()et si vous échouez _exit().

BTW: Pour comprendre ce que la mise en mémoire tampon incorrecte peut causer, voici un ancien bogue sous Linux qui a été récemment corrigé:

La norme nécessite de ne stderrpas mettre la stderrmémoire tampon par défaut, mais Linux l'a ignoré et a fait en sorte que la ligne soit tamponnée et (pire encore) complètement mise en mémoire tampon au cas où stderr serait redirigé via un tube. Les programmes écrits pour UNIX produisaient donc des données sans nouvelle ligne trop tard sous Linux.

Voir le commentaire ci-dessous, il semble être corrigé maintenant.

Voici ce que je fais pour contourner ce problème Linux:

    /* 
     * Linux comes with a broken libc that makes "stderr" buffered even 
     * though POSIX requires "stderr" to be never "fully buffered". 
     * As a result, we would get garbled output once our fork()d child 
     * calls exit(). We work around the Linux bug by calling fflush() 
     * before fork()ing. 
     */ 
    fflush(stderr); 

Ce code ne nuit pas aux autres plates-formes, car appeler fflush()sur un flux qui vient d'être vidé est un noop.

schily
la source
2
Non, stdout doit être intégralement mis en mémoire tampon, sauf s'il s'agit d'un périphérique interactif. Dans ce cas, il n'est pas spécifié, mais en pratique, il est ensuite mis en mémoire tampon. stderr doit ne pas être entièrement tamponné. Voir pubs.opengroup.org/onlinepubs/9699919799.2018edition/functions/…
Stéphane Chazelas
Ma page de manuel pour setbuf(), sur Debian ( celle de man7.org est similaire ), indique que "Le flux d'erreur standard stderr est toujours désactivé par défaut." et un simple test semble agir de cette manière, que la sortie soit dirigée vers un fichier, un canal ou un terminal. Avez-vous des références pour quelle version de la bibliothèque C ferait autrement?
ilkkachu
4
Linux est un noyau, stdio buffering est une fonctionnalité utilisateur, le noyau n'y est pas impliqué. Il existe un certain nombre d'implémentations libc disponibles pour les noyaux Linux, la plus courante dans les systèmes de type serveur / station de travail est l'implémentation GNU, avec laquelle stdout est tamponné (mémoire tampon si tty) et stderr est non tamponné.
Stéphane Chazelas
1
@schily, juste le test que j'ai exécuté: paste.dy.fi/xk4 . J'ai eu le même résultat avec un système horriblement obsolète.
Ilkkachu
1
@schily Ce n'est pas vrai. Par exemple, j'écris ce commentaire en utilisant Alpine Linux, qui utilise musl à la place.
NieDzejkob