Pipe-streaming
« | 27 Oct 2024 | »Individuelle Technologien brauchen individuelle APIs. Doch, wenn es möglich ist, unterschiedliche Technologien unter eine API zu stellen, dann entstehen ungeahnte Möglichkeiten.
Und das gilt auch für Pipes.
Während unter Unix alles eine Datei ist,
führt Windows zusätzliche
APIs ein, wenn man mit Pipes arbeiten möchte.
Um diese Individualitäten auszugleichen, habe ich neben der gate_pipe
Abstraktion nun auch den Pipestream eingeführt, doch wie sich zeigt,
gibt es da einige Tücken, auf die man achten muss.
Blockaden und Fehler
Pipes verhalten sich sowohl unter Windows und Linux recht seltsam, wenn sie geschlossen werden, fehlerhaft sind oder gerade nichts anzubieten haben.
Ein read oder write an einem Ende erfordert entweder ein write / read
auf der anderen Seite, oder die Funktion blockiert … außer man hat
Puffer aktiviert, wo dann ein paar Bytes ohne Blockaden geschrieben werden,
ohne dass sie sofort gelesen werden müssen.
Schlimmer ist das Schließen von Pipes, die dann entweder per Signal, oder speziellem Fehlercode die Gegenseite abbrechen lassen.
Da Lesefunktionen quasi für immer blockieren können, ist nebem dem
klassischen Lesen und Schreiben auch noch eine is_data_available()
Funktion notwendig. So kann ein Programm den aktuellen Status prüfen und
im Falle eines Leerlaufes etwas anderes tun.
Bei Prozessen, die über “Named Pipes” kommunizieren, ist das besonders wichtig, denn nur so kann man feststellen, ob ein Prozess noch läuft, auf dessen Daten man per Pipe wartet.
Win32 Implementierung
CreatePipe()
schafft zwei Enden einer anonymen Pipe. Interessanter sind aber benannte Pipes,
die mit
CreateNamedPipe()
angelegt werden. Sie müssen im Namensraum \\.\Pipe\ liegen und
eindeutig sein, z.B.:
\\.\Pipe\my_named_pipe.
Danach kann dieser Pfad beliebig oft mit
CreateFile()
zum Lesen oder Schreiben geöffnet werden.
Ob Daten zum Lesen verfügbar sind, ermittelt
PeekNamedPipe().
Tatsächlich gelesen wird mit
ReadFile(),
wobei im Fehlerfall ein ERROR_BROKEN_PIPE einem “End-of-stream” entspricht.
Geschrieben werden Daten wie bei Dateien mit WriteFile().
POSIX Implementierung
pipe() erzeugt anonyme Pipes, aber nur mkfifo() lässt uns eine benannte Pipe anlegen. Einmal angelegt kann diese Pipe dann mit open() zum Lesen oder Schreiben geöffnet werden.
Leseaktivitäten lassen sich wie bei Sockets mit select() feststellen.
Zum Lesen und Schreiben sind wie bei Dateien einfach
read()
und
write()
zu benutzen. Doch bei write() muss auch auf das Signal SIGPIPE beachtet
werden, denn dieses wird gesendet, wenn die Pipe bereits geschlossen ist,
während ein write() ausgeführt wird.
Vergisst man darauf, wird der eigene Prozess vom Signal-Händler gekillt.
Auf den Namen kommt es an
Unter Windows gibt es bestimmte Konventionen, wie ein Pipe-Name aussehen soll. Unter Linux muss der Pipe-Pfad ein beliebiger gültiger Dateisystempfad sein, denn genau dort wird die Pipe angelegt.
Um plattformunabhängig zu sein, erstellt das GATE-Framework einen zufälligen,
aber eindeutigen Namen bei jedem Erzeugungsaufruf.
Eben diesen Namen kann man dann auch für die Gegenseite benutzen.
Vererbung
Und am Ende braucht man auch noch eine Möglichkeit eine Pipe per “ID” an ein
Kind zu vererben. Hierzu wird das Pipe-Handle bzw. der File-Deskriptor
dupliziert und sichergestellt, dass er an Kindprozesse vererbt werden kann.
Unter Windows geschieht dies mit
DuplicateHandle()
mit dem bInheritHandle Parameter und in POSIX-Umgebungen, wo grundsätzlich
alles vererbt werden kann, muss das Duplikat vom automatischen Schließen beim
fork()/exec() ausgenommen werden.
Fazit
Pipes … diese Dinge erscheinen so einfach und klein, haben mir aber durch ihre unterschiedlichen Eigenschaften auf jeder Plattform jede Menge Steine in den Weg gelegt, als ich eine einheitliche Schnittstelle für die Arbeit mit ihnen aufsetzen wollte.
Zumindest ist jetzt mal ein Prototyp im GATE Framework etabliert. Doch bis dieser in allen Details eingesetzt und getestet werden kann, wird wohl noch etwas Zeit vergehen.