blog · git · desktop · images · contact & privacy · gopher


Vererbte Puffer

Eigentlich wollte ich nur eine recht einfache Funktionalität in meinen dwm einbauen. Am Ende bin ich hierbei gelandet:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

void
spawn(void)
{
    if (fork() == 0)
    {
        exit(EXIT_SUCCESS);
    }
}

int
main(int argc, char **argv)
{
    printf("foooooo\n");
    spawn();
    spawn();
    spawn();
    spawn();
    spawn();
    exit(EXIT_SUCCESS);
}

Ganz normal übersetzen:

$ gcc -Wall -Wextra -o bla bla.c

Was ist wohl die Ausgabe, wenn man „./bla“ aufruft?

$ ./bla
foooooo

Okay. Würde man naiverweise ja auch erwarten. Er gibt „foooooo“ aus und forkt dann ein paar Mal, ohne dabei aber wirklich etwas zu tun. Und jetzt die Quizfrage: Was passiert bei folgendem Aufruf?

$ ./bla >omg; cat omg

Man würde doch eigentlich dasselbe erwarten, nicht wahr? Das Programm ist ja unverändert, es soll lediglich die Ausgabe von der Shell in eine Datei umgeleitet und diese danach ausgegeben werden. Man erhält (zumindest unter Linux mit GNU – wie es sonst so ist, habe ich nicht ausprobiert) aber folgende Ausgabe:

$ ./bla >omg; cat omg
foooooo
foooooo
foooooo
foooooo
foooooo
foooooo

Nanu? Ich habe einige Stunden gebraucht, um herauszufinden, wie das zustande kommt.

Mehrere Dinge fallen zusammen: Zum einen ist die Ausgabe gepuffert, wenn nicht auf ein Terminal geschrieben ist. Beim ersten Aufruf ohne Umleitung wird direkt auf ein Terminal geschrieben, da gibt es nur einen Zeilenpuffer. Beim zweiten Aufruf ist der Puffer deutlich größer. Zum anderen erbt ein Prozess bei einem „fork()“ die offenen Dateien seines Elternprozesses – inklusive der Puffer. Aber das alleine erklärt das Verhalten immer noch nicht.

exit()“ ist eine erstaunlich komplexe Funktion, die einen Haufen Hooks ausführt. Ich habe die genaue Stelle nicht finden können, aber irgendwo wird „exit()“ ein „fflush(stdout)“ ausführen. Und genau das ist das Problem. Die ganzen geforkten Prozesse haben alle noch ein „foooooo“ in ihrem Puffer, was durch „exit()“ dann geschrieben wird. Von jedem der Prozesse.

Man kann probeweise folgende Ersetzungen vornehmen:

Wenn man es weiß, ist es trivial. Für mich war es das „WTF des Monats“, weil es für eine halbe Ewigkeit so aussah, als würde dwm Dinge tun, die es eigentlich gar nicht tun sollte: Wie so viele Leute spicke ich gerne Code mit einigen „printf()“, um zu sehen, was passiert. Ist einfacher, als einen Debugger anzuwerfen. Außerdem leite ich das STDOUT von dwm in eine Logfile um. Es sah dann so aus, als würde dwm völlig abstruse Codepfade betreten, wenn ich ein XTerm aufrufe. Natürlich war mit dwm alles in Ordnung, im Zuge des XTerm-Aufrufs kam es lediglich zu oben beschriebener Pufferanomalie nach Forks …

Drum merke: Debugausgaben immer flushen oder nach STDERR schreiben.