Warum muss stdout explizit geleert werden, wenn es in eine Datei umgeleitet wird?

Lesezeit: 6 Minuten

Patricks Benutzeravatar
Patrick

Das Verhalten von printf() scheint ortsabhängig zu sein stdout.

  1. Wenn stdout wird dann an die Konsole gesendet printf() ist zeilengepuffert und wird geleert, nachdem ein Zeilenumbruch ausgegeben wurde.
  2. Wenn stdout in eine Datei umgeleitet wird, wird der Puffer nicht geleert, es sei denn fflush() wird genannt.
  3. Außerdem, wenn printf() vorher verwendet wird stdout wird in die Datei umgeleitet, nachfolgende Schreibvorgänge (in die Datei) werden zeilengepuffert und nach dem Zeilenumbruch geleert.

Wann ist stdout zeilengepuffert, und wann fflush() muss angerufen werden?

Minimales Beispiel von jedem:

void RedirectStdout2File(const char* log_path) {
    int fd = open(log_path, O_RDWR|O_APPEND|O_CREAT,S_IRWXU|S_IRWXG|S_IRWXO);
    dup2(fd,STDOUT_FILENO);
    if (fd != STDOUT_FILENO) close(fd);
}

int main_1(int argc, char* argv[]) {
    /* Case 1: stdout is line-buffered when run from console */
    printf("No redirect; printed immediately\n");
    sleep(10);
}

int main_2a(int argc, char* argv[]) {
    /* Case 2a: stdout is not line-buffered when redirected to file */
    RedirectStdout2File(argv[0]);
    printf("Will not go to file!\n");
    RedirectStdout2File("/dev/null");
}
int main_2b(int argc, char* argv[]) {
    /* Case 2b: flushing stdout does send output to file */
    RedirectStdout2File(argv[0]);
    printf("Will go to file if flushed\n");
    fflush(stdout);
    RedirectStdout2File("/dev/null");
}

int main_3(int argc, char* argv[]) {
    /* Case 3: printf before redirect; printf is line-buffered after */
    printf("Before redirect\n");
    RedirectStdout2File(argv[0]);
    printf("Does go to file!\n");
    RedirectStdout2File("/dev/null");
}

  • Sie sollten bei allen Systemaufrufen nach Fehlern suchen.

    – Irgendein Programmierer-Typ

    18. Dezember 2012 um 12:33 Uhr

  • @Joachim Danke für deinen Vorschlag. Ich entferne hier nur den Prüfcode, um den Code besser lesbar zu machen. Fehler beim Überprüfen der Dosis behebt mein Problem nicht.

    – Patrick

    18. Dezember 2012 um 14:08 Uhr

  • Ich schätze also, keine der Funktionen gibt tatsächlich einen Fehler zurück?

    – Irgendein Programmierer-Typ

    18. Dezember 2012 um 14:29 Uhr

Benutzeravatar von Nicholas Wilson
Nicolas Wilson

Spülung für stdout wird durch sein Pufferverhalten bestimmt. Die Pufferung kann auf drei Modi eingestellt werden: _IOFBF (volle Pufferung: wartet bis fflush() wenn möglich), _IOLBF (Zeilenpufferung: Zeilenumbruch löst automatisches Leeren aus) und _IONBF (direktes Schreiben wird immer verwendet). „Die Unterstützung für diese Merkmale ist implementierungsdefiniert und kann über die beeinflusst werden setbuf() und setvbuf() Funktionen.” [C99:7.19.3.3]

„Beim Programmstart sind drei Textströme vordefiniert und müssen nicht explizit geöffnet werden – Standardeingabe (zum Lesen der konventionellen Eingabe), Standardausgabe (zum Schreiben der konventionellen Ausgabe) und Standardfehler (zum Schreiben der Diagnoseausgabe). Standardfehlerstrom ist nicht vollständig gepuffert; die Standardeingabe- und Standardausgabeströme sind vollständig gepuffert, wenn und nur wenn festgestellt werden kann, dass sich der Strom nicht auf ein interaktives Gerät bezieht.” [C99:7.19.3.7]

Erklärung des beobachteten Verhaltens

Was also passiert, ist, dass die Implementierung etwas Plattformspezifisches tut, um zu entscheiden, ob stdout wird zeilengepuffert. In den meisten libc-Implementierungen wird dieser Test durchgeführt, wenn der Stream zum ersten Mal verwendet wird.

  1. Verhalten Nr. 1 lässt sich leicht erklären: Wenn der Stream für ein interaktives Gerät bestimmt ist, wird er zeilengepuffert, und die printf() wird automatisch gespült.
  2. Fall Nr. 2 wird jetzt auch erwartet: Wenn wir zu einer Datei umleiten, ist der Stream vollständig gepuffert und wird nicht geleert, außer mit fflush()es sei denn, Sie schreiben Unmengen von Daten darauf.
  3. Schließlich verstehen wir Fall #3 auch für Implementierungen, die die Überprüfung des zugrunde liegenden fd nur einmal durchführen. Weil wir die Initialisierung des Puffers von stdout im ersten erzwungen haben printf(), stdout hat den zeilengepufferten Modus übernommen. Wenn wir fd auslagern, um in die Datei zu gehen, ist es immer noch zeilengepuffert, sodass die Daten automatisch geleert werden.

Einige tatsächliche Implementierungen

Jede libc hat Spielraum, wie sie diese Anforderungen interpretiert, da C99 nicht spezifiziert, was ein “interaktives Gerät” ist, und dies auch nicht tut stdio-Eintrag von POSIX Erweitern Sie dies (über die Anforderung hinaus, dass stderr zum Lesen geöffnet sein muss).

  1. Glibc. Sehen filedoalloc.c:L111. Hier verwenden wir stat() um zu testen, ob fd ein tty ist, und stellen Sie den Puffermodus entsprechend ein. (Dies wird von fileops.c aufgerufen.) stdout hat zunächst einen Nullpuffer und wird bei der ersten Verwendung des Streams basierend auf den Eigenschaften von fd 1 zugewiesen.

  2. BSD-libc. Sehr ähnlicher, aber viel saubererer Code folgt! Sehen diese Zeile in makebuf.c

  • Vielen Dank für Ihren Vorschlag zur Fehlersuche und Leckbeseitigung. Aber ich verstehe immer noch nicht, warum ich hier spülen muss. Und warum das Drucken einer Nachricht vor dem Umleiten das Problem beheben kann.

    – Patrick

    18. Dezember 2012 um 14:05 Uhr


  • @Patrick Ah, ich verstehe. In diesem Fall betrügen Sie dieses Problem: stackoverflow.com/questions/1716296/…

    – Nicolas Wilson

    18. Dezember 2012 um 14:15 Uhr

  • Danke für deine Antwort. Aber ich habe “\n” in meinem Code, sollte es Flush auslösen?

    – Patrick

    18. Dezember 2012 um 14:21 Uhr

  • Ok, letzte Antwort! fsync, fstat, ftruncate usw. sind eine Familie von Funktionen (in Abschnitt 2 von man); völlig unabhängig von fopen, fprintf, fflush (Abschnitt 3). fsync befindet sich im Kernel und weiß nichts über die stdio-Puffer von libc, die sich in Ihrer Anwendung befinden.

    – Nicolas Wilson

    19. Dezember 2012 um 8:49 Uhr

  • Ich verstehe es jetzt. close ist eine Systemfunktion, sie kennt auch den libc-Puffer nicht.

    – Patrick

    21. Dezember 2012 um 11:41 Uhr


Sie kombinieren fälschlicherweise gepufferte und ungepufferte IO-Funktionen. Eine solche Kombination muss sehr sorgfältig durchgeführt werden, insbesondere wenn der Code portabel sein muss. (und es ist schlecht, nicht portierbaren Code zu schreiben …)
Es ist sicherlich am besten zu vermeiden, gepufferte und ungepufferte IO auf demselben Dateideskriptor zu kombinieren.

Gepufferte IO: fprintf(), fopen(), fclose(), freopen()

Ungepufferte IO: write(), open(), close(), dup()

Wenn Sie verwenden dup2() um stdout umzuleiten. Der Funktion ist der gefüllte Puffer nicht bekannt fprintf(). Also wann dup2() schließt den alten Deskriptor 1, es löscht den Puffer nicht und der Inhalt könnte in eine andere Ausgabe geräumt werden. In Ihrem Fall wurde es an 2a gesendet /dev/null.

Die Lösung

In Ihrem Fall ist es am besten, zu verwenden freopen() Anstatt von dup2(). Das löst alle Ihre Probleme:

  1. Es spült die Puffer des Originals FILE Strom. (Fall 2a)
  2. Es stellt den Puffermodus entsprechend der neu geöffneten Datei ein. (Fall 3)

Hier ist die korrekte Implementierung Ihrer Funktion:

void RedirectStdout2File(const char* log_path) {
    if(freopen(log_path, "a+", stdout) == NULL) err(EXIT_FAILURE, NULL);
}

Leider können Sie mit gepuffertem IO die Berechtigungen einer neu erstellten Datei nicht direkt festlegen. Sie müssen andere Aufrufe verwenden, um die Berechtigungen zu ändern, oder Sie können nicht portierbare glibc-Erweiterungen verwenden. Siehe die fopen() man page.

Sie sollten den Dateideskriptor nicht schließen, also entfernen close(fd) und schließen
stdout_bak_fd
wenn Sie möchten, dass die Nachricht nur in der Datei gedruckt wird.

1389010cookie-checkWarum muss stdout explizit geleert werden, wenn es in eine Datei umgeleitet wird?

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

Privacy policy