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


Woher kommen „/dev/stdout“ und Gefährten?

Kürzlich hab’ ich die Empfehlung aufgeschnappt, zur Ausgabe von Fehlern in Shellskripten „>/dev/stderr“ statt „>&2“ zu benutzen. Der Grund war ganz einfach, dass es lesbarer ist – daran besteht auch wenig Zweifel. So ganz wohl war mir dabei aber nicht, da ich nicht genau wusste, wie /dev/stderr eigentlich zustande kommt und ob es immer verfügbar sind.

Es geht um diese drei symbolischen Links:

lrwxrwxrwx 1 root root 15 Aug 26 20:49 /dev/stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root 15 Aug 26 20:49 /dev/stdout -> /proc/self/fd/1
lrwxrwxrwx 1 root root 15 Aug 26 20:49 /dev/stderr -> /proc/self/fd/2

Zuerst einmal ist /dev heutzutage meistens als devtmpfs gemountet. Das heißt, nach dem Ausschalten des Rechners ist sein Inhalt weg. Das lässt sich leicht nachvollziehen, indem man ein Live-System bootet, die Partition mountet und einfach nachschaut. Bei meinem Arch sehe ich dann in /dev gerade einmal console, null und zero. Sonst nichts.

Das ist relevant, da es bedeutet, dass meine Kandidaten bei jedem Boot neu erstellt werden müssen. Früher™, in der Zeit noch vor devfs hätten sie noch einfach während der Installation angelegt worden sein können und fertig.

Udev liegt heute natürlich nahe, wenn es um /dev geht. Und tatsächlich wird man auch fündig, denn der udevd erstellt die fraglichen Links bei seinem Start:

static void static_dev_create_links(struct udev *udev, DIR *dir)
{
    /* ... */

    static const struct stdlinks stdlinks[] = {
        { "core", "/proc/kcore" },
        { "fd", "/proc/self/fd" },
        { "stdin", "/proc/self/fd/0" },
        { "stdout", "/proc/self/fd/1" },
        { "stderr", "/proc/self/fd/2" },
    };

    /* ... */
}

Betonung liegt auf heute. Die Git-History verrät, dass das noch im Mai 2010 nicht so war. Da das noch gar nicht so lange her ist, hat mich interessiert, wie das denn vorher war.

Bei den Initscripts von Arch gibt es einen passenden Commit aus diesem Zeitraum. Dort ist nur eine einfache cp-Operation zu sehen, denn die Links wurden lediglich kopiert – zu finden sind sie ursprünglich im udev-Paket (vor Version 155). Davon kann man sich zum Beispiel bei der Arch Rollback Machine überzeugen:

$ tar -tvf udev-151-3-i686.pkg.tar.gz | egrep 'std(in|out|err)'
lrwxrwxrwx root/root         0 2010-02-11 08:07 lib/udev/devices/stdin -> /proc/self/fd/0
lrwxrwxrwx root/root         0 2010-02-11 08:07 lib/udev/devices/stdout -> /proc/self/fd/1
lrwxrwxrwx root/root         0 2010-02-11 08:07 lib/udev/devices/stderr -> /proc/self/fd/2

Weiter wollte ich in der Geschichte dann auch nicht zurückgehen. ;) An der Stelle sei aber angemerkt, dass ich es erstaunlich und großartig finde, wie gut sich diese Umstände dank Git und großen Festplatten (bei der ARM) noch rekonstruieren lassen.

Da die Herkunft nun erschöpfend geklärt ist, bleibt die Frage nach der Verfügbarkeit. Im FHS 2.3 findet sich nichts in der Richtung, aber bei POSIX.1-2008:

The system may provide non-standard extensions. These are features not required by POSIX.1-2008 and may include, but are not limited to:

Die Links können also existieren. Sie müssen aber nicht. Ganz davon abgesehen, dass sich sowieso nicht jeder an POSIX hält. FreeBSD 8 hat sie beispielsweise und auch das leicht ranzige SuSE auf unseren Uni-Rechnern (dort läuft immerhin noch Vim 6.4). Das kleine Embedded Linux auf meiner FritzBox 7170 aber nicht. Nun kann man freilich argumentieren, ob das geeignete Maßstäbe sind. So oder so zeigt es aber, dass man nicht unbedingt blind davon ausgehen kann, dass /dev/stderr existiert.

Und es gibt noch einen echten Unterschied bei der Ausführung. „>&2“ nimmt sich File-Deskriptor 2 und schreibt dort hinein. „>/dev/stderr“ hingegen öffnet die „Datei“ /dev/stderr und benötigt daher auch im Hintergrund ein vollständiges Filesystem-Lookup dieses Namens. Was Syscalls angeht, sieht das (auf’s Wesentliche reduziert) so aus:

$ strace -tt dash -c 'echo hi >&2'
...
22:55:54.640636 close(1)                = 0
22:55:54.641599 dup2(2, 1)              = 1
22:55:54.641711 write(1, "hi\n", 3)     = 3
...

Und beim Link:

$ strace -tt dash -c 'echo hi >/dev/stderr'
...
22:57:57.832187 open("/dev/stderr", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0666) = 3
22:57:57.832354 close(1)                = 0
22:57:57.832462 dup2(3, 1)              = 1
22:57:57.832519 close(3)                = 0
22:57:57.832588 write(1, "hi\n", 3)     = 3
...

Die Performance-Nachteile sollten heute nicht im messbaren Bereich liegen und auch ein möglicher „Angriff“ (der Link /dev/stderr könnte auf etwas ganz anderes zeigen, aber Deskriptor 2 ist immer Deskriptor 2) ist wohl vernachlässigbar. Interessant ist es aber trotzdem.