Datendienst

Anwendungen verbringen viel Zeit damit, auf Daten von der Festplatte oder aus dem Netz zu warten. Laufen Ein- und Ausgaben im Hintergrund ab, kann ein Programm die Zeit für andere Aufgaben nutzen.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 5 Min.
Von
  • Michael Riepe

Zwar lesen und schreiben Festplatten schneller als je zuvor, von Solid State Disks ganz zu schweigen. Für den Prozessor, dessen Arbeitsschritte kürzer sind als eine Nanosekunde, vergeht jedoch eine kleine Ewigkeit, bis die gewünschten Daten im RAM landen: zehntausend Taktzyklen beim Lesen von einer schnellen SSD, zehn Millionen, wenn die Daten von einer Festplatte kommen.

TCP-Verbindungen arbeiten ebenfalls nicht gerade rasant, doch dort kann die Anwendung mit select oder poll nachsehen, ob Daten angekommen sind, und in der Zwischenzeit andere Aufgaben erledigen. Bei gewöhnlichen Dateien behaupten die Systemaufrufe jedoch grundsätzlich, dass Daten zum Abholen bereitstünden. Hat man Glück, stehen sie bereits im Cache, und das nachfolgende read kehrt schnell wieder zurück. Andernfalls muss die Anwendung einige Millisekunden warten, bis die Daten ihren Weg von der Festplatte in den Arbeitsspeicher gefunden haben.

Solche Verzögerungen kann der Programmierer vermeiden, indem er Ein- und Ausgaben im Hintergrund ausführen lässt, etwa in einem separaten Thread. Verwendet er gleich mehrere, kommt das unter Umständen obendrein der Performance zugute, insbesondere bei wahlfreien (Random-)Zugriffen. Allerdings handelt sich der Entwickler zusätzliche Schwierigkeiten ein, denn er muss die Threads verwalten und synchronisieren – eine mühsame und fehlerträchtige Aufgabe. Es gibt jedoch vorgefertigte Routinen, die ihm einen Teil der Arbeit abnehmen.

Unter Windows kann man Dateien mit dem Flag FILE_FLAG_OVERLAPPED für asynchrone Lese- und Schreiboperationen öffnen. Den Systemaufrufen ReadFile und WriteFile muss man dann einen Zeiger auf eine OVERLAPPED-Struktur mitgeben. Die enthält den gewünschten Datei-Offset und ein Handle zu einem Event, mit dem der Kernel die Anwendung benachrichtigt, sobald der Vorgang abgeschlossen ist. Die Funktionen kehren sofort zurück, sodass die Anwendung in der Zwischenzeit andere Dinge erledigen kann. ReadFileEx und WriteFileEx arbeiten ähnlich, melden den Vollzug jedoch über eine als Argument übergebene Callback-Funktion (siehe „Alle Links“ [a, b]).

Ganz so einfach ist es unter Unix/Linux nicht. Die Systemaufrufe read und write arbeiten grundsätzlich synchron. Für asynchrones Lesen und Schreiben definiert der Posix-Standard eine separate Schnittstelle, die aus der Header-Datei aio.h und acht Funktionen besteht [c]. Zum Lesen und Schreiben dienen aio_read und aio_write, die als Argument jeweils eine aiocb-Struktur erwarten. Die enthält außer den Angaben, die auch read respektive pread erfordern – File Descriptor, Offset, Speicheradresse und Zahl der zu übertragenden Bytes – die Felder aio_reqprio und aio_sigevent. In Letzterem kann die Anwendung festlegen, wie sie vom Vollzug benachrichtigt werden möchte: per Signal, per Callback in einem separaten Thread oder gar nicht. aio_reqprio dient dazu, die Priorität einer Anfrage zu senken. Der Wert 0 steht für die höchste, positive Werte für eine niedrigere Priorität. Der Maximalwert ist systemabhängig und lässt sich mit sysconf(_SC_AIO_PRIO_DELTA_MAX) erfragen.

Verzichtet der Programmierer auf Vollzugsmeldungen, muss er den Status eines Requests explizit prüfen. Dazu kann er die Funktion aio_error nutzen, die bei laufenden Operationen EINPROGRESS zurückgibt. Andernfalls liefert sie entweder 0 – Operation erfolgreich – oder einen Fehlercode. aio_return gibt das Resultat einer erfolgreichen Operation zurück – wie bei read oder write die Zahl der übertragenen Bytes. Mit aio_suspend kann die Anwendung auf die Beendigung einer oder mehrerer Operationen warten. aio_cancel versucht, einen oder mehrere Aufträge zu stornieren; eine Garantie, dass das funktioniert, gibt es leider nicht.

Zusätzlich zur Posix-Schnittstelle bringen manche Betriebssysteme ihre eigene mit, teils aus historischen Gründen, teils weil sie performanter ist. Unter Linux etwa ist die Posix-Variante mithilfe von Threads in der C-Laufzeitbibliothek glibc umgesetzt. Für viele Anwendungen genügt das, doch für manche ist die Implementierung nicht schnell genug, wie der Autor beim Testen schneller SSDs leidvoll erfahren musste. Wer mehr Performance benötigt, muss die Kernel-basierte Variante nutzen, die sich vom Posix-Modell deutlich unterscheidet und leider obendrein mit diversen Einschränkungen behaftet ist [d, e]. So arbeitet sie nur mit Dateien und Blockgeräten und setzt voraus, dass diese mit dem Flag O_DIRECT geöffnet sind. Außerdem lassen sich nur vollständige Sektoren lesen und schreiben, und die Adresse des Lese- respektive Schreibpuffers muss entsprechend ausgerichtet sein. Datenbanken oder Festplatten-Benchmarks wie fio [f] sind damit jedoch durchaus zufrieden. Letzterer kann auch die Solaris-eigene Schnittstelle für Asynchronous I/O [g], die Windows-Variante oder das Posix-Interface nutzen, sodass sich die Performance der unterschiedlichen Ansätze vergleichen lässt.

Alle Links: www.ix.de/ix1212162 (mr)