Unix-Kernels speichern für jeden Prozess eine Liste von offenen Dateien. Diese Liste können die Prozesse nicht direkt einsehen; sie verwenden stattdessen Dateideskriptoren (file descriptors oder FDs) – int
s, die einen Index in diese Tabelle darstellen.
Es gibt konventionell drei „besondere“ FDs: stdin
(0), stdout
(1) und stderr
(2). Prinzipiell unterscheiden sie sich nicht von anderen FDs, doch ihre zweckmäßige Verwendung wird „stark empfohlen“, und die meisten Programmierumgebungen erleichtern das (z.B. schreibt Cs printf
auf stdout
, und Java bietet ein „fertig geöffnetes“ stdout
über java.lang.System.out
an).
Unter Unix werden Prozesse nicht gestartet, sondern „geklont und ersetzt“. Der Systemaufruf fork
sorgt dafür, dass ein Klon des laufenden Prozesses erstellt wird und an derselben Stelle wie der ursprüngliche weiterläuft. Hierbei werden offene Dateideskriptoren übernommen. Die Ersetzung erfolgt durch einen exec
-Systemaufruf; unter Linux etwa baut die exec
-Funktionsfamilie auf dem execve
-Systemaufruf auf. Auch hier bleiben die Dateideskriptoren offen – die Ausnahme bilden FDs, bei denen die Option close-on-exec (O_CLOEXEC
) gesetzt ist; diese werden geschlossen. (In der Unix-Welt gilt es als Faux-Pas, die drei wohlbekannten FDs mit dem close-on-exec-Flag zu belegen.)
Grundsätzlich gilt also: FDs werden geerbt. Natürlich ist das nicht immer zweckmäßig.
Wird eine Datei geöffnet (Systemaufruf open
), erhält sie normalerweise den niedrigsten freien Dateideskriptor. Der Systemaufruf dup2
kann für die gewünschte Zuordnung sorgen, indem er zwei Dateideskriptoren (oldfd
und newfd
) entgegennimmt und Folgendes macht:
- Falls nötig, schließt er
newfd
(die aktuelle Belegung des gewünschten Dateideskriptors). - Er veranlasst, dass
newfd
auf dieselbe offene Datei zeigt wieoldfd
.
Damit zeigen jetzt sowohl oldfd
als auch newfd
auf dieselbe Datei. Falls meine frisch zum Lesen geöffnete Textdatei den Dateideskriptor 6 erhalten hat, kann ich dafür sorgen, dass sie auch den Dateideskriptor 0 bekommt (und damit zu meinem neuen stdin
wird – bzw. zum stdin
des Prozesses, den ich gleich exec
en werde).
Jetzt stellt sich natürlich die Frage: wo zeigt stdin
hin, wenn ich ein Programm vom Terminalprogramm aus starte?
Beim Öffnen eines neuen Terminalfensters macht ein Terminalprogramm üblicherweise Folgendes:
- Es bittet das Betriebssystem um ein neues virtuelles Terminal. (Die hierzu benötigten Aufrufe können von Unix-Variante zu Unix-Variante unterschiedlich sein.) Es erhält Zugriff auf zwei Dateien unter
/dev
(eine Master-Datei und eine Slave-Datei), die zuvor ggf. automatisch erstellt werden. - Das Terminalprogramm
fork
t sich. - Der Vaterprozess schließt die Slave-Datei und bereitet sich auf die Kommunikation über die Master-Datei vor.
- Der Kindprozess schließt die Master-Datei, biegt die drei wohlbekannten Dateideskriptoren auf die Slave-Datei um, und
exec
t das gewünschte Programm (meist eine Shell).
Jedes Mal, wo das Programm (etwa die Shell) nun auf stdout
oder stderr
schreibt, geht der Inhalt in die Slave-Datei hinein und kommt bei der Master-Datei (und damit beim lesenden Terminalfensterprozess) wieder raus. Bei einer Eingabe am Terminalfenster wiederum wird diese in die Master-Datei geschrieben, kommt aus der Slave-Datei heraus, und wird ans stdin
des Programms geleitet.
Die Bourne-Shell sh
sowie ihre Nachfolgerinnen (bash
, zsh
, dash
, ksh
, ...) erlauben das Umbiegen von Dateideskriptoren mit einer relativ einfachen Syntax:
< dateiname
öffnetdateiname
zum Lesen und biegt sie auf Dateideskriptor 0 um> dateiname
öffnetdateiname
zum (Über-)Schreiben und biegt sie auf Dateideskriptor 1 um2> dateiname
öffnetdateiname
zum (Über-)Schreiben und biegt sie auf Dateideskriptor 2 um>> dateiname
öffnetdateiname
zum Anhängen und biegt sie auf Dateideskriptor 1 um2>> dateiname
öffnetdateiname
zum Anhängen und biegt sie auf Dateideskriptor 2 um6< dateiname
öffnetdateiname
zum Lesen und biegt sie auf Dateideskriptor 6 um7>> dateiname
öffnetdateiname
zum Anhängen und biegt sie auf Dateideskriptor 7 um
Hier gilt aber: was nicht umgebogen wird, wird vererbt. Falls mein Terminalprogramm nun für meine Shell das virtuelle Terminal /dev/pty/2
zugewiesen bekommen hat, und ich starte myprog
mit dem Befehl myprog >/dev/null
, ergibt sich folgende Situation:
stdin
wird von der Shell geerbt und zeigt auf/dev/pty/2
.stdout
wurde von der Shell auf/dev/null
umgebogen und zeigt also dorthin.stderr
wird von der Shell geerbt und zeigt auf/dev/pty/2
.
Manche Dateideskriptoren sind seekbar, d.h. es kann eine seek
-Funktion (z.B. lseek
) darauf verwendet werden, um die aktuelle Lese-/Schreib-Position innerhalb der Datei zu verändern. Dies trifft natürlich nur auf Dateien mit Random-Access-Möglichkeit zu, etwa reguläre Dateien oder Festplatten-Gerätedateien. Andere Dateitypen, etwa Pipes oder virtuelle Terminals, erlauben dies nicht. (Bandlaufwerke sind ein interessanter Fall: meist sind sie seekbar, doch die seek
-Operation ist extrem zeitaufwändig und damit möglichst zu vermeiden.)
Seekbarkeit ist keine Eigenschaft eines FDs, sondern eine Eigenschaft der dahinterliegenden Datei. Folglich gilt etwa: wenn die Datei, auf die stdin
zeigt, seekbar ist, ist stdin
seekbar.