Schreibt eine Anwendung auf die Festplatte, sind Verzögerungen programmiert: Das langsame Medium bremst die ganze Applikation und macht anderweitig erzielte Geschwindigkeitsvorteile zunichte. Abhilfe soll in diesem Fall der asynchrone Zugriffsmodus (Async-I/O) schaffen: Während das Ein- oder Ausgabesystem noch mit dem Lesen oder Schreiben beschäftigt ist, bereitet die Applikation bereits den nächsten I/O-Auftrag vor (Abbildung 1).
Im Unterschied dazu besitzen klassische Unix-Systeme nur den synchronen Zugriffsmodus, bei dem eine Applikation auf das Ende der Ein- und Ausgabe wartet. Will ein Programm trotz eines laufenden Auftrags weiterarbeiten, muss es einen zweiten Thread starten. Nach diesem Prinzip funktioniert die Posix-Variante des asynchronen Zugriffs[1]. Deren Nachteil ist, dass mit jedem I/O-Auftrag der Scheduler aktiv werden muss. Unnötige Performance-Einbußen sind die Folge.
Neben der Posix-Lösung gab es für Linux 2.4 bereits Patches, die den gewünschten Zugriffsmodus direkt im Kernel realisieren. Kernel 2.6 hat ihn schließlich eingebaut. Dennoch setzen bisher nur wenige Applikationen Async- I/O ein, denn es ist kaum dokumentiert, wie sich diese Zugriffsart nutzen lässt. Außerdem bringt die Implementierung in Kernel 2.6 nur teilweise die erwarteten Performance-Vorteile.
Zudem ist die Implementierung nicht vollständig und ihre Interfaces sind nicht allgemein verwendbar. Kein Wunder, dass außer Oracle kaum jemand den asynchronen Zugriff verwendet und das OSDL potenzielle Nutzer sucht[2].
Neue Systemcalls
Als User-API für Async-I/O implementiert Linux insgesamt fünf Systemaufrufe: »io_setup()«, »io_destroy()«, »io_ submit()«, »io_getevents()« sowie »io_ cancel()«. Mit »io_setup()« erzeugt die Applikation im Kernel einen Async-I/O-Kontext. Diesem Kontext ordnet der Kernel später I/O-Aufträge zu, sodass Anwendungen den Auftragsfortschritt erfragen können. Ein so genannter »iocb« (Input Output Control Block) beschreibt den Auftrag.
Der Systemcall »io_submit()« übergibt dem Kernel die »iocb«-Blöcke zur Abarbeitung. Die Applikation muss ihrerseits für jeden Auftrag Speicher bereitstellen, aus dem der Kernel die Daten bei einem Auftrag entnimmt respektive dort ablegt.
Speicher ausrichten
Dieser Speicherbereich muss bei asynchronem Zugriff Sector-aligned sein, also auf eine Sektorgröße ausgerichtet (genauer: ein Vielfaches von 512). Den notwendigen Speicher reserviert am einfachsten die Funktion »int posix_memalign (void **memptr, size_t alignment, size_t size)«, siehe Listing 1, Zeilen 31 und 32. Die Funktion »io_cancel()« beendet einzelne Aufträge wieder.
Schließlich benötigen Applikation und Kernel noch eine Datenstruktur, die den Auftragsstatus verwaltet: »struct io_ event«. Eine Liste dieser Event-Datenstrukturen übergibt »io_getevents()« dem Kernel. Der Systemcall blockiert aufrufende Rechenprozesse so lange, bis eine minimale Anzahl von Aufträgen abgearbeitet oder die angegebene Timeout-Zeit abgelaufen ist.
Eine Unterstützung für die vorgestellten Systemcalls sucht man in der Standard-C-Bibliothek vergebens. Wer in seiner Applikation asynchron zugreifen möchte, muss die Aufrufe per »_syscall X«-Makro selbst programmieren (siehe[3]) oder die LibAIO herunterladen und installieren (siehe[4]).
Abbildung 1: Beim synchronen Zugriff (1) schläft die Applikation, bis der Treiber die Daten zum oder vom Speicher transferiert hat. Beim asynchronen Zugriff (2) arbeiten Applikation sowie Treiber und Kernel parallel.