printf Anomalie nach "fork ()"

77

Betriebssystem: Linux, Sprache: pure C.

Ich lerne C-Programmierung im Allgemeinen und C-Programmierung unter UNIX in einem speziellen Fall.

Ich habe printf()nach einem fork()Aufruf ein seltsames (für mich) Verhalten der Funktion festgestellt .

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;
}

Ausgabe

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

Warum ist die zweite "Hallo" -String in der Ausgabe des Kindes aufgetreten?

Ja, es ist genau das, was der Elternteil beim Start mit dem des Elternteils gedruckt hat pid.

Aber! Wenn wir \nam Ende jeder Zeichenfolge ein Zeichen einfügen, erhalten wir die erwartete Ausgabe:

#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;
}

Ausgabe :

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

Warum passiert das? Ist es korrektes Verhalten oder ist es ein Fehler?

Pechenie
quelle

Antworten:

91

Ich stelle fest, dass dies <system.h>ein nicht standardmäßiger Header ist. Ich habe es durch ersetzt <unistd.h>und den Code sauber kompiliert.

Wenn die Ausgabe Ihres Programms an ein Terminal (Bildschirm) geht, wird es zeilengepuffert. Wenn die Ausgabe Ihres Programms an eine Pipe geht, ist sie vollständig gepuffert. Sie können den Puffermodus mit der Standard-C-Funktion setvbuf()und den Modi _IOFBF(vollständige Pufferung), _IOLBF(Zeilenpufferung) und _IONBF(keine Pufferung) steuern.

Sie können dies in Ihrem überarbeiteten Programm demonstrieren, indem Sie beispielsweise die Ausgabe Ihres Programms an Folgendes weiterleiten cat. Selbst mit den Zeilenumbrüchen am Ende der printf()Zeichenfolgen würden Sie die doppelte Information sehen. Wenn Sie es direkt an das Terminal senden, sehen Sie nur die eine Menge Informationen.

Die Moral der Geschichte ist es, vorsichtig zu sein, fflush(0);um alle E / A-Puffer vor dem Gabeln zu leeren.


Zeilenweise Analyse nach Bedarf (Klammern usw. entfernt - und führende Leerzeichen vom Markup-Editor entfernt):

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

Die Analyse:

  1. Kopiert "Hallo, meine PID ist 1234" in den Puffer für die Standardausgabe. Da am Ende keine neue Zeile vorhanden ist und die Ausgabe im zeilengepufferten Modus (oder im voll gepufferten Modus) ausgeführt wird, wird auf dem Terminal nichts angezeigt.
  2. Gibt uns zwei separate Prozesse mit genau demselben Material im Standardpuffer.
  3. Das Kind hat pid == 0und führt die Zeilen 4 und 5 aus; Das übergeordnete Element hat einen Wert ungleich Null für pid(einer der wenigen Unterschiede zwischen den beiden Prozessen - Rückgabewerte von getpid()und getppid()sind zwei weitere).
  4. Fügt dem Ausgabepuffer des Kindes eine neue Zeile und "Ich wurde gegabelt !: D" hinzu. Die erste Ausgabezeile wird auf dem Terminal angezeigt. Der Rest wird im Puffer gehalten, da die Ausgabe zeilengepuffert ist.
  5. Alles hält für 3 Sekunden an. Danach verlässt das Kind normal durch die Rückkehr am Ende der Hauptleitung. Zu diesem Zeitpunkt werden die Restdaten im Standardpuffer gelöscht. Dadurch bleibt die Ausgabeposition am Ende einer Zeile, da keine neue Zeile vorhanden ist.
  6. Der Elternteil kommt hierher.
  7. Der Elternteil wartet darauf, dass das Kind mit dem Sterben fertig ist.
  8. Der Elternteil fügt eine neue Zeile hinzu und "1345 wurde gegabelt!" zum Ausgabepuffer. Die neue Zeile löscht die Nachricht "Hallo" nach der vom Kind generierten unvollständigen Zeile in die Ausgabe.

Das übergeordnete Element wird jetzt normal durch die Rückgabe am Ende von main beendet, und die verbleibenden Daten werden gelöscht. Da am Ende noch keine neue Zeile steht, befindet sich die Cursorposition hinter dem Ausrufezeichen und die Shell-Eingabeaufforderung wird in derselben Zeile angezeigt.

Was ich sehe ist:

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: 

Die PID-Nummern sind unterschiedlich - aber das allgemeine Erscheinungsbild ist klar. Das Hinzufügen von Zeilenumbrüchen am Ende der printf()Anweisungen (was sehr schnell zur Standardpraxis wird) verändert die Ausgabe erheblich :

#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;
}

Ich bekomme jetzt:

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:

Beachten Sie, dass die Ausgabe, wenn sie an das Terminal gesendet wird, zeilengepuffert ist, sodass die Zeile "Hallo" vor dem angezeigt wird fork()und nur eine Kopie vorhanden war. Wenn die Ausgabe an weitergeleitet wird, catist sie vollständig gepuffert, sodass vor dem fork()und beide Prozesse die Zeile "Hallo" im zu löschenden Puffer angezeigt wird.

Jonathan Leffler
quelle
OK ich habe es. Aber ich kann mir immer noch nicht erklären, warum der "Puffermüll" am Ende der neu gedruckten Zeile in der Ausgabe des Kindes erscheint. Aber warte, jetzt bezweifle ich, dass es wirklich die Ausgabe von CHILD ist. Oh, kannst du erklären, warum die Ausgabe genau so aussieht (neue Zeichenfolge VOR der alten), Schritt für Schritt, also wäre ich sehr dankbar. Trotzdem danke!
Pechenie
Sehr beeindruckende Erklärung! Vielen Dank, endlich habe ich es klar verstanden! PS: Ich habe vorher für Sie abgestimmt, und jetzt habe ich noch einmal dumm auf den "Aufwärtspfeil" geklickt, sodass die Abstimmung verschwunden ist. Aber ich kann es dir nicht noch einmal geben, weil "die Antwort zu alt ist" :( PPS: Ich habe dir eine Stimme in einer anderen Frage gegeben. Und nochmals
vielen
27

Der Grund dafür ist, dass \nder Wert ohne die am Ende der Formatzeichenfolge nicht sofort auf dem Bildschirm gedruckt wird. Stattdessen wird es innerhalb des Prozesses gepuffert. Dies bedeutet, dass es erst nach dem Gabelvorgang gedruckt wird, sodass Sie es zweimal drucken lassen.

Durch Hinzufügen des \nobwohl wird der Puffer geleert und auf dem Bildschirm ausgegeben. Dies geschieht vor der Gabel und wird daher nur einmal gedruckt.

Sie können dies mithilfe der fflushMethode erzwingen . Zum Beispiel

printf( "Hello, my pid is %d", getpid() );
fflush(stdout);
JaredPar
quelle
1
fflush(stdout);Scheint hier imo die richtigere Antwort zu sein.
Oxagast
5

fork()erstellt effektiv eine Kopie des Prozesses. Wenn vor dem Aufruf fork()Daten gepuffert waren, haben sowohl das übergeordnete als auch das untergeordnete Element dieselben gepufferten Daten. Wenn jeder von ihnen das nächste Mal etwas unternimmt, um seinen Puffer zu leeren (z. B. das Drucken einer neuen Zeile bei Terminalausgabe), wird diese gepufferte Ausgabe zusätzlich zu jeder neuen Ausgabe angezeigt, die durch diesen Prozess erzeugt wird. Wenn Sie also stdio sowohl für Eltern als auch für Kinder verwenden fflushmöchten, sollten Sie dies vor dem Verzweigen tun, um sicherzustellen, dass keine gepufferten Daten vorhanden sind.

Oft wird das Kind nur zum Aufrufen einer exec*Funktion verwendet. Da dies das gesamte untergeordnete Prozessabbild (einschließlich aller Puffer) ersetzt, ist dies technisch nicht erforderlich, fflushwenn dies wirklich alles ist, was Sie im untergeordneten Prozess tun werden. Wenn jedoch möglicherweise gepufferte Daten vorhanden sind, sollten Sie vorsichtig sein, wie ein Exec-Fehler behandelt wird. Vermeiden Sie es insbesondere, den Fehler mit einer beliebigen stdio-Funktion auf stdout oder stderr zu drucken ( writeist in Ordnung), und rufen Sie dann _exit(oder _Exit) auf, anstatt aufzurufen exitoder nur zurückzukehren (wodurch alle gepufferten Ausgaben gelöscht werden). Oder vermeiden Sie das Problem vollständig, indem Sie vor dem Gabeln spülen.

mark4o
quelle