Warum gibt ein Programm mit fork () seine Ausgabe manchmal mehrmals aus?

50

In Programm 1 Hello worldwird nur einmal gedruckt, aber wenn ich es entferne \nund ausführe (Programm 2), wird die Ausgabe 8 Mal gedruckt. Kann mir bitte jemand erklären, welche Bedeutung das \nhier hat und wie sich das auswirkt fork()?

Programm 1

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

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

Ausgang 1:

hello world... 

Programm 2

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

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

Ausgang 2:

hello world... hello world...hello world...hello world...hello world...hello world...hello world...hello world...
lmaololrofl
quelle
10
Versuchen Sie, Programm 1 mit einer Ausgabe in eine Datei ( ./prog1 > prog1.out) oder eine Pipe ( ./prog1 | cat) auszuführen . Bereite dich darauf vor, deinen Verstand durchzubrennen. :-) ⁠
G-Man sagt, dass Monica am
Relevante Fragen und Antworten zu einer anderen Variante dieses Problems: C-System ("bash") ignoriert stdin
Michael Homer
13
Dies hat einige enge Stimmen gesammelt, daher ein Kommentar dazu: Fragen zu "UNIX C-API und Systemschnittstellen" sind ausdrücklich erlaubt . Pufferungsprobleme treten häufig auch in Shell-Skripten auf und fork()sind auch etwas unix-spezifisch. Daher scheint dies für unix.SE ein ziemlich aktuelles Thema zu sein.
Ilkkachu
@ilkkachu Wenn Sie diesen Link lesen und auf die Meta-Frage klicken, auf die er verweist, wird deutlich, dass dies kein Thema ist. Nur weil etwas C ist und Unix C hat, wird es nicht zum Thema.
Patrick
@ Patrick, eigentlich habe ich. Und ich denke immer noch, dass es zu der Klausel "In Reason" passt, aber das bin natürlich nur ich.
Ilkkachu

Antworten:

93

Bei der Ausgabe auf Standardausgabe mit der printf()Funktion der C-Bibliothek wird die Ausgabe normalerweise gepuffert. Der Puffer wird erst geleert, wenn Sie eine neue Zeile ausgeben, fflush(stdout)das Programm aufrufen oder beenden (jedoch nicht durch Aufrufen _exit()). Der Standardausgabestream wird auf diese Weise standardmäßig zeilenweise gepuffert, wenn er mit einem TTY verbunden ist.

Wenn Sie den Prozess in "Programm 2" verzweigen, erben die untergeordneten Prozesse alle Teile des übergeordneten Prozesses, einschließlich des nicht leeren Ausgabepuffers. Dadurch wird der nicht gelöschte Puffer effektiv auf jeden untergeordneten Prozess kopiert.

Wenn der Prozess beendet ist, werden die Puffer geleert. Sie starten insgesamt acht Prozesse (einschließlich des ursprünglichen Prozesses), und der nicht geleerte Puffer wird nach Beendigung jedes einzelnen Prozesses geleert.

Es ist acht, weil Sie bei jedem fork()die doppelte Anzahl von Prozessen erhalten, die Sie vor dem hatten fork()(da sie bedingungslos sind), und Sie haben drei von diesen (2 3 = 8).

Kusalananda
quelle
14
Verwandte: Sie können am Ende mainmit _exit(0)nur einen Ausgang Systemaufruf ohne Spülung Puffer zu machen, und dann wird es Null mal ohne eine neue Zeile gedruckt werden. ( Syscall-Implementierung von exit () und How come _exit (0) (wird von syscall beendet) verhindert, dass ich Standardinhalte erhalte? ). Oder Sie catleiten Program1 in eine Datei um oder leiten es in eine Datei um und sehen, dass es 8 Mal gedruckt wird. (stdout ist standardmäßig voll gepuffert, wenn es kein TTY ist). Oder fügen Sie fflush(stdout)dem No-Newline-Fall vor dem 2. fork()...
Peter Cordes
17

Es beeinflusst die Gabel in keiner Weise.

Im ersten Fall haben Sie 8 Prozesse, bei denen Sie nichts zu schreiben haben, da der Ausgabepuffer (aufgrund der \n) bereits geleert wurde .

Im zweiten Fall haben Sie noch 8 Prozesse, jeder mit einem Puffer, der "Hallo Welt ..." enthält, und der Puffer wird am Ende des Prozesses geschrieben.

edc65
quelle
12

@ Kusalananda erklärte, warum die Ausgabe wiederholt wird . Wenn Sie neugierig sind, warum die Ausgabe 8 Mal und nicht nur 4 Mal wiederholt wird (das Basisprogramm + 3 Gabeln):

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
quelle
2
Dies ist die Grundlage der Gabel
Prvt_Yadav
3
@ Debian_yadav wahrscheinlich nur dann offensichtlich, wenn Sie mit den Auswirkungen vertraut sind. Zum Beispiel das Spülen von Stdio- Puffern.
Roaima
2
@Debian_yadav: en.wikipedia.org/wiki/False_consensus_effect - warum sollten wir Fragen stellen, wenn jeder alles weiß?
Honza Zidek
8
@Debian_yadav Ich kann die Gedanken des OP nicht lesen, daher weiß ich es nicht. Auf jeden Fall ist StackExchange ein Ort, an dem auch andere nach Wissen suchen, und ich denke, meine Antwort kann eine nützliche Ergänzung zu Kulasandras guter Antwort sein. Meine Antwort fügt etwas hinzu (grundlegend, aber nützlich), verglichen mit der des edc65, die nur das wiederholt, was Kulasandra 2 Stunden vor ihm gesagt hat.
Honza Zidek
2
Dies ist nur ein kurzer Kommentar zu einer Antwort, keine tatsächliche Antwort. Die Frage fragt nach "mehrmals" nicht, warum es genau 8 ist.
Pipe
3

Der wichtige Hintergrund hierbei ist, dass stdoutdie Leitung standardmäßig gepuffert sein muss .

Dies bewirkt, dass a \nden Ausgang spült.

Da das zweite Beispiel keine neue Zeile enthält, wird die Ausgabe nicht geleert und fork()kopiert als Kopie des gesamten Prozesses auch den Zustand des stdoutPuffers.

Mit diesen fork()Aufrufen in Ihrem Beispiel werden insgesamt 8 Prozesse erstellt - alle mit einer Kopie des Status des stdoutPuffers.

Per Definition rufen alle diese Prozesse exit()bei der Rückkehr von main()und bei exit()Aufrufen fflush()gefolgt von fclose()allen aktiven stdio- Streams auf. Dies schließt ein stdoutund als Ergebnis wird derselbe Inhalt achtmal angezeigt.

Es ist empfehlenswert, vor dem Aufrufen fflush()alle Streams mit ausstehender Ausgabe aufzurufen fork()oder den untergeordneten Zweig explizit aufrufen zu lassen, der _exit()den Prozess nur beendet, ohne die stdio-Streams zu leeren.

Beachten Sie, dass beim Aufrufen exec()die Stdio-Puffer nicht geleert werden. Daher ist es in Ordnung, sich nicht um die Stdio-Puffer zu kümmern, wenn Sie (nach dem Aufrufen fork()) anrufen exec()und (falls dies fehlschlägt) anrufen _exit().

Übrigens: Um zu verstehen, dass eine falsche Pufferung dazu führen kann, ist hier ein früherer Fehler in Linux, der kürzlich behoben wurde:

Der Standard muss stderrstandardmäßig nicht stderrgepuffert sein , aber Linux hat dies ignoriert und die Zeilen gepuffert und (noch schlimmer) vollständig gepuffert, falls stderr über eine Pipe umgeleitet wurde. Programme, die für UNIX geschrieben wurden, gaben also unter Linux Dinge ohne Zeilenumbruch zu spät aus.

Siehe Kommentar unten, es scheint jetzt behoben zu sein.

Folgendes tue ich, um dieses Linux-Problem zu umgehen:

    /* 
     * 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); 

Dieser Code schadet auf anderen Plattformen nicht, da das Aufrufen fflush()eines gerade gelöschten Streams ein Noop ist.

schily
quelle
2
Nein, stdout muss vollständig gepuffert sein, es sei denn, es ist ein interaktives Gerät. In diesem Fall ist es nicht spezifiziert, aber in der Praxis ist es dann zeilengepuffert. stderr muss nicht vollständig gepuffert sein. Siehe pubs.opengroup.org/onlinepubs/9699919799.2018edition/functions/…
Stéphane Chazelas
In meiner Manpage für setbuf()Debian ( die auf man7.org sieht ähnlich aus ) heißt es: "Der Standard-Fehlerstrom stderr ist standardmäßig immer ungepuffert." und ein einfacher Test scheint so zu verfahren, unabhängig davon, ob die Ausgabe an eine Datei, eine Pipe oder ein Terminal gesendet wird. Haben Sie eine Referenz für welche Version der C-Bibliothek, die sonst tun würde?
Ilkkachu
4
Linux ist ein Kernel, stdio buffering ist ein Userland Feature, der Kernel ist dort nicht involviert. Für Linux-Kernel stehen eine Reihe von libc-Implementierungen zur Verfügung. Die häufigste in Systemen vom Typ Server / Workstation ist die GNU-Implementierung, bei der stdout voll gepuffert ist (zeilengepuffert, wenn tty) und stderr ungepuffert ist.
Stéphane Chazelas
1
@schily, nur der Test, den ich ausgeführt habe: paste.dy.fi/xk4 . Das gleiche Ergebnis habe ich auch mit einem schrecklich veralteten System erzielt.
Ilkkachu
1
@schily Das stimmt nicht. Zum Beispiel schreibe ich diesen Kommentar mit Alpine Linux, das stattdessen musl verwendet.
NieDzejkob