Non-Blocking I/O

In Posix-Systemen (Linux, Unix, MacOS X,…) basieren die I/O-Operationen hauptsächlich auf den Systemaufrufen read(..) und write(..). Die meisten anderen I/O-Operationen lassen sich darauf zurückführen und auch I/O von anderen Programmiersprachen als C dürfte letztlich indirekt zu read() und write() führen. read() ist eine Funktion, die einen (numerischen) Filedeskriptor, einen Pointer auf einen hinreichend großen Speicherbereich („Buffer“) und eine Größenangabe annimmt und im Idealfall die angegebene Anzahl von Bytes liest. Das funktioniert gemäß dem Unix-Prinzip „everything is a file“ ganz einfach mit Dateien (Files), Geräten (Devices), (Pseudo-)terminals, Pipes (Fifos), TCP-Streams etc. Wenn ein Fehler auftritt, wird ein negativer Wert zurückgegeben und man muss errno abfragen, um den Fehler zu finden. Den negativen Fehlercode direkt zurückzugeben wäre besser gewesen, aber das kann man nun nicht mehr ändern. Eine 0 bedeutet, dass man das Ende erreicht hat. Dies wird nicht durch den Inhalt der übertragenen Daten ausgedrückt, denn jede beliebige Bytesquenz wird akzeptiert und verarbeitet, sondern separat übermittel. Wenn weniger als die gewünschten Bytes da sind, dann wird entsprechend weniger zurückgegeben. Der Rückgabewert gibt an, wieviele Bytes tatsächlich gelesen wurdn. Das sind alles Situationen, in denen das read sofort anfangen kann, auch wenn die eigentliche Operation etwas länger dauern kann. Bei Dateien kann der Fall, dass 0 Bytes gelesen werden, nur am Dateiende und natürlich bei Fehlern auftreten. Bei Pipes und TCP-Streams ist aber möglich, dass man auf Daten wartet und keine da sind. Mit 0 Bytes gibt sich das read normalerweise nicht zufrieden und wwartet stattdessen auf Daten. Das ist eine vernünftige Sache, da es in der Regel das ist, was man will. So vereinfacht sich die Programmierung. Write verhält sich entsprechend, bei Pipes kann man auch nur schreiben, wenn auf der Gegenseite die Daten abgenommen werden. Ein Prinzip das heute in der Scala- und Akka-Welt als die größte Innovation des Jahres gerühmt wird und den Namen „Backpressure“ bekommen hat.

Non-Blocking funktioniert auch mit read(). Mit open() oder nachträglich mit fcntl() wird eingestellt, dass das I/O non-blocking ist. read() versucht nun, Bytes zu lesen und wenn das klappt ist alles wie beim blocking I/O. Wenn das read() keine Daten findet, wartet es nicht auf Daten, sondern kommt sofort mit -1 zurück und in errno steht EAGAIN oder EWOULDBLOCK o.ä. write() entsprechend.

Nun hätte man die Möglichkeit, regelmäßig einen Filedescriptor abzufragen und wenn Daten kommen, darauf zu reagieren und sonst etwas anderes zu tun. Ohne Multithreading…

Interessant ist der Fall, dass man mehrere Filedescriptoren gleichzeitig hat und von diesen Daten lesen oder schreiben will. Man weiß nicht wann welche Zugriffe möglich sind. Das wäre mit einem Thread pro Filedescriptor machbar. Oder mit non-Blocking-I/O und Polling, also einer Schleife, die dauernd alle Filedescriptoren durchprobiert.. Besser ist es aber select() oder pselect() oder poll() zu verwenden, die semantisch sehr ähnlich funktionieren, aber aus historischen Gründen koexistieren. Wer eigenen Code schreibt, kann sich eine der drei Funktionen aussuchen und damit arbeiten, wer aber Code von anderen Leuten lesen und ändern muss, sollte alle drei kennen. Dies ist eine sehr elegante Lösung, weil man noch ein Timeout dazunehmen kann. Die Spezialfälle 0 und unendlich für das Timeout werden auch sinnvoll unterstützt.

Das „non-Blocking-Prinzip“ wird bei der Posix-Systemprogrammierung oft angeboten, z.B. beim Locken von Mutexen, Semaphoren oder beim File-Locking.

Man sollte non-blocking-I/O nicht mit assynchronem I/O verwechseln. Dazu kommt vielleicht ein anderer Artikel..

Share Button

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*