printf-Anomalie nach “fork()”

Lesezeit: 6 Minuten

Benutzeravatar von pechenie
pechenie

Betriebssystem: Linux, Sprache: reines C

Ich mache Fortschritte beim Erlernen der C-Programmierung im Allgemeinen und der C-Programmierung unter UNIX in einem speziellen Fall.

Ich habe ein seltsames (für mich) Verhalten der festgestellt printf() Funktion nach Verwendung von a fork() Anruf.

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 tauchte die zweite „Hello“-Zeichenfolge in der Ausgabe des untergeordneten Elements auf?

Ja, es ist genau das, was die Eltern gedruckt haben, als es angefangen hat, mit den Eltern pid.

Aber! Wenn wir a setzen \n Zeichen am Ende jeder Zeichenfolge 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 das Verhalten korrekt oder handelt es sich um einen Fehler?

Benutzeravatar von Jonathan Leffler
Jonathan Leffler

Ich merke das <system.h> ist ein nicht standardmäßiger Header; Ich habe es durch ersetzt <unistd.h> und der Code sauber kompiliert.

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

Sie könnten dies in Ihrem überarbeiteten Programm demonstrieren, indem Sie die Ausgabe Ihres Programms beispielsweise an cat. Auch 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 eine Menge Informationen.

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


Zeile-für-Zeile-Analyse, wie gewünscht (geschweifte 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 kein Zeilenumbruch steht und die Ausgabe im Line-Buffered-Modus (bzw. Full-Buffered-Modus) läuft, erscheint auf dem Terminal nichts.
  2. Gibt uns zwei getrennte Prozesse mit genau demselben Material im stdout-Puffer.
  3. Das Kind hat pid == 0 und führt die Zeilen 4 und 5 aus; der Elternteil hat einen Wert ungleich null für pid (einer der wenigen Unterschiede zwischen den beiden Prozessen – Rückgabewerte von getpid() und getppid() sind noch zwei).
  4. Fügt einen Zeilenumbruch und „I was forked! :D“ zum Ausgabepuffer des untergeordneten Elements hinzu. Die erste Ausgabezeile erscheint auf dem Terminal; 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 von main. An diesem Punkt werden die Restdaten im stdout-Puffer geleert. Dadurch bleibt die Ausgabeposition am Ende einer Zeile, da kein Zeilenumbruch vorhanden ist.
  6. Der Elternteil kommt hierher.
  7. Die Eltern warten darauf, dass das Kind stirbt.
  8. Der Elternteil fügt einen Zeilenumbruch hinzu und “1345 wurde gegabelt!” zum Ausgangspuffer. Der Zeilenumbruch spült die „Hallo“-Nachricht in die Ausgabe, nach der unvollständigen Zeile, die vom untergeordneten Element generiert wurde.

Der Elternteil wird nun normal durch die Rückkehr am Ende von main beendet, und die Restdaten werden geleert; Da am Ende immer noch kein Zeilenumbruch steht, befindet sich die Cursorposition hinter dem Ausrufezeichen, und der Shell-Prompt erscheint in derselben Zeile.

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 Gesamtbild ist klar. Hinzufügen von Zeilenumbrüchen am Ende der printf() -Anweisungen (was sehr schnell zur Standardpraxis wird) verändert die Ausgabe stark:

#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 geht, zeilengepuffert ist, sodass die Zeile „Hello“ vor dem erscheint fork() und es gab nur das eine Exemplar. Wenn der Ausgang weitergeleitet wird cates ist vollständig gepuffert, sodass vor dem nichts angezeigt wird fork() und beide Prozesse haben die ‘Hallo’-Zeile im Puffer, die geleert werden soll.

  • 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. Trotzdem danke!

    – Pechenie

    28. März 2010 um 6:40 Uhr

  • SEHR beeindruckende Erklärung! Vielen Dank, endlich habe ich es klar verstanden! PS: Ich habe zuvor eine Stimme für Sie abgegeben, und jetzt habe ich dummerweise noch einmal auf den “Pfeil nach oben” geklickt, sodass die Stimme verschwunden ist. Aber ich kann es Ihnen nicht noch einmal geben, weil “die Antwort zu alt ist” 🙁 PPS: Ich habe Ihnen in einer anderen Frage eine Stimme gegeben. Und noch einmal vielen Dank!

    – Pechenie

    28. März 2010 um 17:43 Uhr

Der Grund dafür ist, dass ohne die \n am Ende des Formatstrings wird der Wert nicht sofort auf dem Bildschirm ausgegeben. Stattdessen wird es innerhalb des Prozesses gepuffert. Dies bedeutet, dass es erst nach der Fork-Operation tatsächlich gedruckt wird, daher wird es zweimal gedruckt.

Hinzufügen der \n erzwingt jedoch, dass der Puffer geleert und auf dem Bildschirm ausgegeben wird. Dies geschieht vor der Gabelung und wird daher nur einmal gedruckt.

Sie können dies erzwingen, indem Sie die verwenden fflush Methode. Zum Beispiel

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

  • fflush(stdout); Scheint hier imo die richtigere Antwort zu sein.

    – Oxagast

    29. September 2017 um 6:52 Uhr


fork() erstellt effektiv eine Kopie des Prozesses. Wenn, bevor Sie anrufen fork(), es hatte Daten, die gepuffert wurden, haben sowohl das übergeordnete als auch das untergeordnete Element dieselben gepufferten Daten. Das nächste Mal, wenn jeder von ihnen etwas tut, um seinen Puffer zu leeren (z. B. das Drucken eines Zeilenumbruchs im Fall einer Terminalausgabe), sehen Sie diese gepufferte Ausgabe zusätzlich zu jeder neuen Ausgabe, die von diesem Prozess erzeugt wird. Wenn Sie also stdio sowohl für Eltern als auch für Kinder verwenden, sollten Sie dies tun fflush vor dem Forken, um sicherzustellen, dass keine gepufferten Daten vorhanden sind.

Oft wird das Kind nur zum Rufen verwendet exec* Funktion. Da dies das vollständige untergeordnete Prozessabbild (einschließlich aller Puffer) ersetzt, ist dies technisch nicht erforderlich fflush wenn das wirklich alles ist, was Sie in dem Kind tun werden. Wenn es jedoch möglicherweise gepufferte Daten gibt, sollten Sie vorsichtig sein, wie ein Ausführungsfehler behandelt wird. Vermeiden Sie insbesondere, den Fehler mit einer stdio-Funktion (write ist ok) und dann anrufen _exit (oder _Exit) anstatt anzurufen exit oder einfach zurückgeben (wodurch alle gepufferten Ausgaben geleert werden). Oder vermeiden Sie das Problem ganz, indem Sie vor dem Gabeln spülen.

1420170cookie-checkprintf-Anomalie nach “fork()”

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy