Open-Source-Programmierer wissen es längst: Vorhandene Software nutzen, um damit neue Software zu gestalten, lohnt sich. Das als Software-Reuse bezeichnete Vorgehen vermeidet Doppelarbeit, Fehler und Unvollständigkeiten. Die Effizienz-Programmierer setzen dabei auf Bibliotheken, Frameworks und Templates. Hinzu kommen die Entwurfsmuster, die eine grundsätzliche, Programmiersprachen- und Umgebungs-unabhängige Beschreibung eines Problems geben.
Das Linux-Magazin und insbesondere die Kern-Technik-Serie versucht Programmierern das Leben zu erleichtern und stellt allgemein gehaltene und vielfältig nutzbare Codeschnipsel vor, die sich in eigene Entwicklungen einbauen lassen. So hat die vorige Folge beispielsweise ein Template behandelt, das den Kernel um eigenen Code erweitert [1].
Diese Kern-Technik ergänzt die Sammlung um weitere Funktionstemplates und Entwurfsmuster. Denn für einen neuen Treiber sind immer wieder bestimmte Funktionen zu implementieren, die einen ähnlichen Aufbau haben (siehe Abbildung 1). Bei den zu implementierenden Routinen handelt es sich um die wesentlichen Zugriffsfunktionen »driver_open()«, »driver_close()«, »driver_read()«, »driver_write()« und »driver_poll()«. Dazu gehören aber auch die Funktionen »template_suspend()« und »template_resume()« für das Powermanagement.
Abbildung 1: Die Grundfunktionen zum Einbinden eines Moduls in den Kern, zum Datenzugriff und zum Powermanagement muss jeder Treiber implementieren.
Wenn ein Treiberentwickler die Funktionen schreibt, sollte er sowohl blockierende als auch nicht-blockierende Zugriffe ins Auge fassen. Das führt letztlich zur hohen Komplexität der ansonsten recht einfach strukturierten Lese- und Schreibfunktionen (siehe Abbildung 2).
Abbildung 2: Falls Treiber mehrere Zugriffsarten unterstützen, ist die Implemention nicht mehr trivial, da es diverse Fälle zu berücksichtigen gilt.
Geben und nehmen
Die Lese- oder Schreibfunktion eines Treibers aktiviert der Kernel dann, wenn eine Applikation die Systemcalls »read()« oder »write()« für die zugehörige Gerätedatei aufruft. Falls ständig Daten zum Lesen bereitliegen oder der Kernel sie direkt schreiben darf, sind die zu implementierenden Aktionen gradlinig: Beim »read()« transferiert der Treiber die Daten von der Hardware in den Kernelspace und von dort schließlich in den Speicherbereich der Applikation. Der Syscall »write()« macht es umgekehrt: Er transferiert die Daten aus dem Speicher der Applikation in den Kernel und von hier in die Hardware (siehe Abbildung 3).
Abbildung 3: Das Grundprinzip jedes Treibers ist ein Datentransport in zwei Zügen: Erst koordiniert er den Datentransfer von der Hardware in den Kernelspace, danach kopiert er sie aus dem Kernel in den Userspace.
Ist der Hardwarezugriff jedoch nicht sofort möglich, weil beispielsweise die Hardware noch mit anderen Aufgaben beschäftigt ist, wird es komplizierter. Dann muss der Treiber nämlich die in den Aufrufparametern übergebene Zugriffsart auswerten.
Warten auf die Hardware
Als Default implementiert ein Treiber einen blockierenden Zugriff. Dabei legt er die Applikation und damit die im gleichen Kontext laufende Treiberfunktion so lange (und immer wieder) in den Zustand Schlafen, bis er endlich Daten transferieren kann. Beim nicht-blockierenden Zugriff legt die Treiberfunktion die aufrufende Applikation nicht schlafen, sondern signalisiert ihr mit dem Rückgabewert »-EAGAIN«, dass sie zurzeit nicht lesen oder schreiben darf und sie es später erneut probieren soll.
Der Code, der prüft, ob sich Daten unmittelbar lesen oder geschreiben lassen, stellt einen kritischen Abschnitt dar, den es beispielsweise über Spinlocks zu schützen gilt. Das ist alles andere als trivial und in der Vergangenheit sehr häufig falsch implementiert worden. Daher hat Linus Torvalds das für eine korrekte Implementierung zugrunde liegende Entwurfsmuster in Form eines Makros realisiert.
Das Makro implementiert die in Abbildung 2 sichtbare Schleife. Für den Entwickler heißt das, im Wesentlichen nur noch eine Wait-Queue und den C-Code für eine Bedingung zu implementieren. Anhand der Bedingung muss erkenntlich sein, ob - im Fall des Lesezugriffs - Daten für die Applikation bereitliegen oder nicht. Beim »write()« gibt die Bedingung darüber Auskunft, ob der Treiber Daten schreiben kann. Die Wait-Queue ist ein Objekt, das für das eigentliche Schlafenlegen und Aufwecken notwendig ist. Linux initialisiert es beispielsweise beim Laden des Moduls in der Funktion »template_init()« [2].