\input{confighandout} \subsection{Linux-Treiber im Userspace} Das ab Linux 2.6.23 im stabilen Kernel verfügbare Userspace-I/O-Framework (UIO) ermöglicht, bestimmte Treiber fast vollständig in den Userspace zu verlagern. Dies vereinfacht die Entwicklung und Wartung von Treibern, außerdem ergeben sich Möglichkeiten, die besonders Industriebetrieben entgegen kommen. Eine im Automatisierungsbereich tätige Firma hat eine PCI-I/O-Karte entwickelt. Sie soll in einer unter Linux laufenden Maschinensteuerung eingesetzt werden. Ein Linux-Kerneltreiber muss her. Kein Problem, denkt man sich. Immerhin stehen sowohl der Kernel-Sourcecode als auch die nötigen Entwicklungstools frei zur Verfügung. So oder so ähnlich hat schon so manche Linux-Horror-Story begonnen. Trotz benutzerfreundlicher Kernel-Subsysteme und Unterstützung durch Mailinglisten bedeutet das Schreiben eines Kerneltreibers für Linux-Einsteiger eine nicht zu unterschätzende Einarbeitungszeit. Industrielle I/O-Hardware ist oft so speziell, dass die Subsysteme des Kernels dem Programmierer gerade einmal das Erkennen der PCI-Karte abnehmen können. Gleichzeitig verfügen solche Karten oft über sehr komplexe Funktionen, so dass der Treiber immer umfangreicher wird. Treibercode mit mehreren tausend Codezeilen ist keine Seltenheit. Doch auch wenn diese Hürde genommen, der Treiber erfolgreich debugged und getestet wurde, gibt es ein noch größeres Problem: Die Wartung des Treibers. Denn ein so großer Treiber verwendet zweifellos eine große Zahl interner Kernel-Funktionen und -Makros. Dieses Kernel-interne API ist stets in Bewegung. Wer einen Treiber für 2.6.30 geschrieben hat, hat keinerlei Garantie, dass der unter 2.6.31 noch fehlerfrei kompiliert. Wer seinen Code mehrere Kernel-Versionen lang liegen lässt und dann upgraden will, hat höchst wahrscheinlich ein paar unerfreuliche Tage vor sich. Das zuletzt genannte Problem löst sich in Luft auf, wenn es gelingt, die Kernel-Entwickler, an der Spitze Linus Torvalds, dazu zu bewegen, den Code in den stabilen Mainline-Kernel aufzunehmen. Denn dann sind die Entwickler, die etwas am internen API ändern, verpflichtet, für die Anpassung aller betroffenen Module zu sorgen. Für unsere Automatisierungsfirma würde das bedeuten, dass die Linux-Community ihnen gratis diese Wartungsarbeit abnehmen würde. Sie und ihre Kunden könnten immer die neueste Kernelversion verwenden und würden ihren Treiber kompilierfähig vorfinden. Leider ist dies aus mehreren Gründen oft nicht machbar. Erstens werden die Kernel-Entwickler die Aufnahme eines großen Treibers, der nicht von allgemeinem Interesse ist, schlicht verweigern. Der Umfang des Kernelcodes würde sonst ins Unermessliche wachsen. Zweitens geht der Aufnahme von Code in den Mainline-Kernel gewöhnlich eine langwierige Review-Prozedur voraus. Dabei wird nicht nur Coding-Style begutachtet, es kann sich durchaus auch um Kritik an grundlegenden Vorgehensweisen handeln. Erst wenn alle Kritiker zufrieden gestellt sind, wird der Code in den Kernel aufgenommen. Darüber können Wochen vergehen. Die wenigsten Industriebetriebe genehmigen ihren Programmierern diesen Zusatzaufwand, da der Treiber in ihrer Maschine ja schon läuft. Drittens haben manche Firmen Probleme mit der GPL. Code, der im Kernel einkompiliert wird, muss dieser Lizenz genügen, das heißt, der Sourcecode muss öffentlich zugänglich sein. Firmen haben in der Vergangenheit zahlreiche (meist illegale) Tricks angewandt, um dieser Bedingung der GPL zu entgehen und ihren Kernelcode unter Verschluss zu halten. \subsubsection{UIO löst Probleme} Die geschilderte Problematik führte bereits vor längerer Zeit zu Überlegungen, zumindest Teile eines Treibers in den Userspace zu verlagern. I/O-Hardware besteht in der Regel aus einem Speicherbereich, in dem sich RAM oder Register befinden. Linux ist von Haus aus in der Lage, einen solchen Speicherbereich per \cmd{mmap()} in den Userspace zu mappen. Damit kann man bereits alle Register der Hardware erreichen. Dies ist aber erst die halbe Miete: In der Regel erzeugen I/O-Karten Interrupts, und diese lassen sich nur im Kernel bearbeiten. Ausserdem muss man in einem allgemein verwendbaren System der Tatsache Rechnung tragen, dass eventuell mehrere (gleiche oder verschiedene) I/O-Karten im System vorhanden sein können. Das Userspace-I/O-Framework (UIO) löst diese Probleme einfach und elegant. Das für den Treiber immer noch benötigte Kernelmodul beschränkt sich auf einen minimalen Interrupt-Handler sowie das Anmelden des Treibers beim UIO-Framework. Letzteres erzeugt ein Device-File unter /dev sowie einige dokumentierte Standardfiles im sysfs unter /sys/class/uio. Ausserdem stellt das UIO-Framework bereits die für den Zugriff auf das Device-File benötigten Funktionen wie \cmd{open()}, \cmd{read()} und \cmd{mmap()} zur Verfügung. Im kundenspezifischen Kerneltreiber wird lediglich eine Struktur vom Typ \cmd{struct uio\_info} mit einigen Informationen gefüllt und damit die Funktion \cmd{uio\_register\_device()} aufgerufen. Den Rest erledigt das Framework. Durch dieses Design kann das hardwarespezifische Kernelmodul sehr klein ausfallen: das seit Kernel 2.6.23 enthaltene Modul für eine PCI-Feldbuskarte (Hilscher CIF) besteht aus gerade einmal 156 Zeilen, inklusive Leerzeilen und Kommentaren. Durch die sehr überschaubare Codegröße ist ein solches Modul schnell geschrieben, übersichtlich, wenig fehleranfällig, leicht wartbar und kann wesentlich leichter in den Mainline-Kernel integriert werden. Der Review-Prozess ist viel einfacher, da auch Kernelentwickler, die nicht über die passende Hardware verfügen, das Modul bezüglich Fehlerfreiheit beurteilen können. Nach dem Laden des Kernelmoduls kann man vom Userspace aus das Device-File (z.B. \cmd{/dev/uio0}) öffnen und sich per \cmd{mmap()}-Aufruf Zugriff auf den Speicher der Hardware verschaffen. Man wird zunächst durch geeignetes Beschreiben von Registern die Hardware initialisieren, so dass sie die gewünschte Funktion ausführt und Interrupts generiert. Auf Interrupts wartet man, indem man mit einem blockierenden \cmd{read()}-Aufruf einen 32-bit-Wert aus dem Device-File liest. Der Aufruf kehrt zurück, sobald ein Interrupt eintrifft. Der gelesene 32-bit-Wert gibt die Gesamtzahl der bisher aufgetretenen Interrupts an. Der Wert sollte idealerweise um eins größer sein als beim letzten Mal. Ist er noch größer, so hat man Interrupts verpasst. Man beachte, dass der Interrupt-Handler im Kernelmodul den Interrupt bereits quittiert haben muss. Im Fall eines pegelgetriggerten Interrupts, wie er bei PCI-Karten üblich ist, würde sich der Rechner sonst sang- und klanglos aufhängen. Bei Rückkehr des blockierenden \cmd{read()}-Aufrufs kann man sich also darauf beschränken, Daten zu lesen und zu schreiben, Statusregister zu lesen, neue Parameter zu setzen oder ähnliche Aufgaben, die beim Auftreten eines Interrupts anfallen. Falls die entsprechende Routine damit bis zum Eintreffen des nächsten Interrupts nicht fertig wird, so ist das zumindest softwaretechnisch kein Problem: Anhand des gelesenen Werts beim nächsten \cmd{read()} erkennen Sie, wie viele Interrupts Sie verpasst haben. \subsubsection{GPL oder nicht GPL} Der Linux-Kernel einschließlich aller enthaltenen Module ist unter der GPL lizensiert. Programme im Linux-Userspace unterliegen solchen Einschränkungen nicht. Das gilt auch für UIO-Treiber. Mit UIO existiert daher erstmals eine Kernel-Schnittstelle, die es erlaubt, einen interruptfähigen Treiber zu schreiben, der größtenteils nicht der GPL unterliegt (das kleine Kernelmodul muss natürlich immer noch GPL-lizensiert werden). Diese Tatsache hat für einigen Wirbel in der Linux-Szene gesorgt. Das UIO-Framework wurde von Mitarbeitern der Firma Linutronix entwickelt und an Greg Kroah-Hartman, den Maintainer des Linux driver core, weiter gereicht. Kroah-Hartman versuchte am 13.12.2006 erstmals, UIO im Mainline-Kernel unterzubringen. Linus Torvalds lehnte die Aufnahme damals ab, hauptsächlich aufgrund von Missverständnissen bezüglich eines beigefügten Demo-Treibers. In der Folge entstand eine wochenlange Diskussion, die schnell vom eigentlichen Thema abwich und sich hauptsächlich mit der Lizensierungs-Problematik beschäftigte. Dabei lag es in erster Linie gar nicht in der Absicht der UIO-Entwickler, proprietäre Treiber zu ermöglichen. Dies ist eigentlich ein Nebeneffekt, der sich aus der Tatsache ergibt, dass man für Userspace-Software unter Linux seit eh und je die Lizenz frei wählen darf. Dennoch mag dieser Punkt in einigen Firmen für Erleichterung sorgen. Wer etwa bestimmtes Know-How in einem FPGA untergebracht hat, der möchte nicht unter dem Zwang stehen, den Code des zugehörigen Treibers öffentlich zu machen. Manchmal kann dies sogar durch den Endkunden vertraglich untersagt sein, zum Beispiel bei Code, der für die Einhaltung gesetzlich vorgeschriebener Werte sorgt, bei militärischen oder sonst sicherheitsrelevanten Anwendungen. Bisher waren solche Firmen gezwungen, Codeänderungen durch technische Klimmzüge zu verhindern, bewusst gegen die GPL zu verstoßen oder ganz auf Linux zu verzichten. Durch UIO wird an dieser Stelle ein gangbarer und legaler Ausweg geschaffen. \subsubsection{Konventionelle Treiber...} Bisherige Gerätetreiber sind vollständig im Kernel integriert. Die Schnittstelle zum Userspace besteht typischerweise aus einer Gerätedatei, auf die mit den üblichen Funktionen wie \cmd{open()}, \cmd{close()}, \cmd{read()}, \cmd{write()} und \cmd{ioctl()} zugegriffen werden kann. \begin{figure}[h] \centering \includegraphics[width=0.8\textwidth]{images/konventioneller-treiber_de.png} \caption{Aufbau eines konventionellen Treibers} \label{img:konvtreiber} \end{figure} Alle diese Funktionen müssen vollständig im Treiber implementiert werden und führen je nach Gerät mehr oder weniger komplexe Operationen mit der Hardware aus. Bei industriellen I/O-Karten liegt meist ein Schwerpunkt auf der \cmd{ioctl()}-Funktion. Die Ein- und Ausgabe-Funktionaltät der Karte ist oft so komplex, dass man sie nicht auf einfache \cmd{read()}- und \cmd{write()}-Operationen abbilden kann. Ausserdem haben solche Karten in der Regel zahlreiche Parameter und Optionen, die auf einen bestimmten Wert gesetzt oder ein- und ausgeschaltet werden wollen. Man findet daher häufig Treiber für derartige Industrie-Karten, die 50 oder mehr ioctl-Unterfunktionen implementieren. \begin{figure}[h] \centering \includegraphics[width=0.8\textwidth]{images/ioctl-vs-uio_de.png} \caption{Vergleich von \cmd{ioctl()}-basierten Sytemen mit UIO} \label{img:ioctl} \end{figure} Dies führt oft dazu, dass letzlich mehr Datenaustausch mit der Karte per \cmd{ioctl()} abgewickelt wird als über die eigentlich dafür vorgesehenen \cmd{read()}- und \cmd{write()}-Funktionen. Auch aus Performance-Gründen ist diese Vorgehensweise alles Andere als optimal. Wenn beispielsweise \cmd{ioctl()} dazu verwendet wird, einen 32-Bit-Wert in ein Register der Hardware zu schreiben, dann packt das Anwenderprogramm im Userspace zunächst den Wert auf den Stack und ruft über die libc \cmd{ioctl()} auf. Per syscall landet der Aufruf im Kernel. Dort sucht das Virtual File System (VFS) den Pointer auf die für das jeweilige Gerät zuständige \cmd{ioctl()}-Funktion und ruft diese auf. Da die übergebenen Parameter im User-Speicher liegen, muss sie die \cmd{ioctl()}-Implementierung zunächst mit Hilfe der Kernel-Funktion \cmd{copy\_from\_user()} in den Kernel-Speicher holen. Jetzt endlich kann der übergebene Wert in das Hardware-Register geschrieben werden. Falls der Aufruf auch Werte zurückgeben soll, müssen diese entsprechend erst mit \cmd{copy\_to\_user()} in den übergebenen Puffer geschrieben werden. Letztlich handelt es sich bei einem derartigen Konzept um einen Missbrauch von \cmd{ioctl()}. Die Funktion war ursprünglich nur dazu gedacht, gelegentlich einen Parameter einer (Geräte-)Datei zu ändern. Für häufigen, schnellen Datenaustausch ist sie denkbar ungeeignet und vor allem auch unnötig, wie wir weiter unten sehen werden. Die Kernel-Entwickler stehen daher einer \cmd{ioctl()}-basierten Treiberimplementierung äußerst kritisch gegenüber. Ein Treiber für eine exotische Karte, der 50 neue \cmd{ioctl()}-Funktionen einführen will, wird wahrscheinlich schon aus diesem Grund nicht in den Mainline-Kernel aufgenommen. Ein weiteres Problem ergibt sich aus der Größe des Treibercodes. Ein paar tausend (nicht-triviale) Zeilen sind schnell beieinander. Obwohl auch für die Kernel-Entwicklung zahlreiche mächtige Hilfsmittel zur Fehlersuche existieren, erfordert das Debugging doch erheblich mehr an Spezialwissen als im Userspace. In einem Kerneltreiber können einfache Fehler den Rechner so lahm legen, dass nur noch der Griff zum Resettaster Rettung bringt. In einem Industriebetrieb verfügen häufig auch die ansonsten sehr qualifizierten Programmierer nur über wenig Erfahrung mit Linux-Kernelprogrammierung. Mit den üblicherweise recht eingeschränkten Ressourcen an Zeit und Personal entstehen dann Treiber, die ebenso fehlerträchtig wie unübersichtlich sind und keinerlei Chance auf Aufnahme in den offiziellen Mainline-Kernel haben. Dadurch haben die Programmierer dann während der gesamten Laufzeit des Produkts zusätzlich das Problem, diesen wackligen Treiber out-of-tree warten zu müssen. \subsubsection{...und UIO-Treiber} Sieht man sich Gerätetreiber etwas genauer an, so stellt man fest, dass es stets wiederkehrende Aufgabenstellungen gibt. Zum einen hat die Hardware einen oder mehrere Speicherbereiche, auf die man zugreifen möchte. Zum anderen erzeugt die Hardware Interrupts, auf die reagiert werden muss. Das UIO-Framework enthält den Code für diese Standardaufgaben und vermeidet so, dass Programmierer ständig ein nicht ganz einfaches Rad neu erfinden müssen. \begin{figure}[h] \centering \includegraphics[width=0.8\textwidth]{images/uio-treiber_de.png} \caption{Aufbau eines konventionellen Treibers} \label{img:konvtreiber} \end{figure} Um Speicherbereiche verwenden zu können, muss man dem UIO-Framework lediglich deren Startadresse und Länge angeben. UIO stellt dann bereits eine \cmd{mmap()}-Funktion zur Verfügung, die es erlaubt, die Speicherbereiche in den Userspace zu mappen. Im Falle einer PCI-Karte erhalten Sie die Informationen über Startadresse und Länge bereits vom PCI-Subsystem. Zur Behandlung von Interrupts muss lediglich ein minimaler Interrupt-Handler geschrieben werden. Um Interrupt-Sharing zu ermöglichen, sollte der Handler erkennen, ob es die eigene Karte war, die den Interrupt ausgelöst hat. Falls nein, gibt er einfach IRQ\_NONE zurück. Falls ja, wird er den Interrupt bei der Hardware quittieren und dafür sorgen, dass die Interrupt-Leitung wieder ihren inaktiven Zustand annimmt. Auch hier muss man lediglich die Nummer des Interrupts und einen Pointer auf den Handler angeben, den Rest erledigt das Framework. Zu den weiteren Dienstleistungen des UIO-Frameworks gehört das Anlegen und Behandeln von Dateien im sysfs. Unter \cmd{/sys/class/uio} finden sich Informationen zu allen angemeldeten UIO-Geräten. Dazu gehören der Name des Geräts, Version seines Kernelmoduls, Adressen und Größen seiner Speicherbereiche, Anzahl aufgetretener Interrupts und Ähnliches. Diese (dokumentierten) sysfs-Dateien ermöglichen es dem Userspace-Teil des Treibers, gezielt nach seinem Gerät zu suchen und beispielsweise die Größen der Speicherbereiche korrekt zu erfragen, anstatt sich auf fest kodierte Werte zu verlassen. Insgesamt enthält das UIO-Framework etwa 900 Zeilen nicht-trivialen Code, die man für eine ähliche Funktionalität sonst selbst schreiben müsste. Dagegen kommt das zu erstellende gerätespezifische Modul auf etwa 150 Zeilen Code, der zudem recht einfach ist und leicht von schon existierenden Treibern abgeleitet werden kann. Dadurch haben auch weniger erfahrene Programmierer die Chance, ein sauberes, übersichtliches und stabiles Modul für ihre Hardware zu produzieren. Es bleibt auch jederzeit die Möglichkeit, sich dieses Modul von einem erfahrenen Dienstleister erstellen zu lassen. Auf diese Weise ist es möglich, eigene Treiber zu entwickeln, ohne eine einzige Zeile Kernel-Code schreiben zu müssen. Der ganze Rest, also die komplette eigentliche Funktionalität des Treibers, wird im Userspace erledigt. Der Treiber kann als Shared Library, als Bestandteil eines Anwenderprogramms oder in bester Unix-Manier als Dämon realisiert werden. Die Programmierer des Treibers benötigen nicht mehr Kenntnisse und Tools, als sie zur Programmierung ihrer Applikation ohnehin benötigen. Im Beispiel oben haben wir gesehen, welchen Aufwand es bedeutet, einen einzelnen Wert per \cmd{ioctl()} in ein Hardware-Register zu befördern. Im Falle von UIO ist diese Operation eine einfache Zuweisung an die betreffende Stelle des gemappten Speichers. Wenn \cmd{a} der Zeiger auf den Speicher ist und an die relative Adresse 1000 der Wert 5 geschrieben werden soll, so schreibt man ohne Umschweife \cmd{a[1000]=5}. Jegliches Hin- und Herkopieren entfällt, so dass sich bei schnellen Anwendungen ein echter Performance-Vorteil gegenüber einem Kerneltreiber mit \cmd{ioctl()}-Konzept ergibt. Es ist also keinesfalls so, dass man für den Userspace-Komfort Geschwindigkeits-Nachteile in Kauf nehmen müsste. Was sich ebenfalls verbessert, ist das Handling von Updates. Bei einem reinen Kernel-Treiber ist ein Update des Kernels oder zumindest der Module nötig, was in der Regel root-Rechte des Anwenders erfordert und bei Fehlern zu einem völlig unbrauchbaren System führt. Bei einem UIO-Treiber unterscheidet sich ein Update des Treibers durch nichts von einem Update der Anwender-Applikation. \subsubsection{Wartungsvertrag} Ein Treiber will gewartet sein. Bei industriellen Geräten findet man oft die Situation, dass ein Gerät über einen längeren Zeitraum mit der selben Kernelversion ausgeliefert wird. Möchte man später, etwa nach 6 Monaten oder einem Jahr, beispielsweise im Zuge eines allgemeinen Software-Updates auf einen aktuellen Kernel wechseln, so sind oft deutliche Änderungen am Kerneltreiber nötig. Konventionelle Treiber beinhalten zwangsläufig viele Aufrufe von internen Kernelfunktionen. Im Gegensatz zur stabilen syscall-Schnittstelle kann sich dieses interne Kernel-API jederzeit ändern. Sie müssen damit rechnen, dass Funktionen, die ihr Treiber verwendet, plötzlich nicht mehr existieren oder andere Parameter haben. Entsprechend hoch ist der Wartungsaufwand für einen solchen Treiber, wenn er nicht im Mainline-Kernel enthalten ist. UIO macht ihnen auch hier das Leben leichter. Das Framework übernimmt die üble Kleinarbeit, die etwa beim Umgang mit sysfs, Interrupt-Handling oder Memory-Mapping anfällt. Es schirmt sie sozusagen von den wilden Seiten des Kernels ab, indem es ihnen erlaubt, mit etwa einem halben Dutzend einfacher Funktionen auszukommen. Angesichts der geringen Codegröße eines UIO-Kernelmoduls sind selbst bei Änderungen an diesen Funktionen die Wartungsarbeiten sehr überschaubar und schnell ausgeführt. Betrachtet man als Beispiel den Treiber für die Hilscher CIF-Karte \cmd{(drivers/uio/uio\_cif.c)}, dann fällt auf, dass für die Einbindung in das UIO-Framework lediglich zwei Funktionen (\cmd{uio\_register\_device} und \cmd{uio\_unregister\_device}) sowie eine Datenstruktur (\cmd{struct uio\_info}) benötigt werden. Ansonsten werden lediglich 7 Funktionen des PCI-Subsystems sowie 6 sonstige Funktionen wie \cmd{kzalloc()} benutzt. Alle diese Funktionen sind einfach und gängig. Das sie sich ändern, ist möglich, aber nicht sehr wahrscheinlich. Dennoch sollten sie den Kernelteil ihres Treibers möglichst nicht ausserhalb des offiziellen Mainline-Kernels pflegen wollen. Schreiben sie sauberen Code entsprechend dem Kernel-Coding-Style und zögern sie nicht, auf der Linux-Kernel-Mailingliste um die Aufnahme desselben zu bitten. Die Mühe lohnt sich: Zukünftige Kernel haben die Unterstützung für ihr Produkt schon eingebaut, Versionskonflikte oder das mühsame Kernel-Patchen entfällt. \subsubsection{Los geht's...} Wie sieht nun ein UIO-Treiber konkret aus? Betrachten wir dazu einen Treiber für eine PCI-Karte. Die folgenden Beispiele sind Auszüge aus dem bereits erwähnten Treiber für die Hilscher CIF Feldbuskarte. Er befindet sich seit 2.6.23-rc1 im Mainline-Kernel. Der Treiber unterscheidet sich zunächst nur wenig von einem normalen PCI-Kartentreiber. Anhand einer Tabelle mit PCI-ID-Nummern wird dem PCI-Subsystem mitgeteilt, für welche Karten sich der Treiber zuständig fühlt. Wird beim Booten eine solche Karte gefunden, so lädt das PCI-Subsystem den Treiber und ruft dessen probe-Funktion auf. Diese muss Karte und Treiber-Variablen initialisieren und das Ergebnis dieser Bemühungen zurückmelden. Konnten Treiber und Karte erfolgreich initialisiert werden, so haben sie dem System ein betriebsbereites PCI-Device hinzugefügt. Bei einem UIO-Treiber besteht ihre Hauptarbeit darin, eine Struktur vom Typ \cmd{struct uio\_info} zu erzeugen und auszufüllen. Beginnen wir mit Name und Version ihres Treibers. Dies sind beliebige strings, die später über sysfs-Dateien abgefragt werden können: \begin{lstlisting} /* Struktur anlegen */ struct uio_info *info; info = kzalloc(sizeof(struct uio_info), GFP_KERNEL); /* Name und Version setzen */ info->name = "Meine UIO Karte"; info->version = "0.0.1"; \end{lstlisting} Als Nächstes kann der von der Karte verwendete Interrupt eingetragen werden. Freundlicherweise übergibt der Kernel der probe-Funktion einen Pointer (hier \emph{dev} genannt) auf eine Struktur vom Typ \cmd{struct pci\_dev}, in der derartige Angaben schon vom PCI-Subsystem eingetragen wurden. Unsere Aufgabe ist also leicht zu erfüllen: \begin{lstlisting} info->irq = dev->irq; \end{lstlisting} Ausserdem kann man dem UIO-Framework mitteilen, dass man einen "Fast"-Interrupthandler möchte und für Interrupt-Sharing vorbereitet ist: \begin{lstlisting} info->irq_flags = IRQF_DISABLED | IRQF_SHARED; \end{lstlisting} Die Adresse des (noch zu erstellenden) Interrupt-Handlers darf natürlich nicht fehlen: \begin{lstlisting} info->handler = mein_irq_handler; \end{lstlisting} Jetzt wäre noch der Speicher der PCI-Karte anzugeben. Nehmen wir an, die Karte habe nur einen solchen Bereich. Wir erfahren Adresse und Größe des Bereichs wieder mit Hilfe des Pointers \emph{dev}. Zusätzlich geben wir an, dass es sich um physikalischen Speicher handelt: \begin{lstlisting} info->mem[0].addr = pci_resource_start(dev, 0); info->mem[0].size = pci_resource_len(dev, 0); info->mem[0].memtype = UIO_MEM_PHYS; \end{lstlisting} Die hier eingetragene physikalische Adresse kann nicht direkt für Zugriffe verwendet werden. Sie wird vom UIO-Framework beim \cmd{mmap()}-Aufruf in den Userspace gemappt. Allerdings wird man sicherlich auch in der Interrupt-Routine Zugriff auf den Speicher benötigen. Dazu mappen wir die Adresse in einen vom Kernel verwendbaren Bereich: \begin{lstlisting} info->mem[0].internal_addr = ioremap(pci_resource_start(dev, 0), pci_resource_len(dev, 0) ); \end{lstlisting} Damit haben wir unserer Pflicht Genüge getan und können unser Device beim UIO-Framework registrieren: \begin{lstlisting} uio_register_device(&dev->dev, info); \end{lstlisting} Das war's. Ein neues UIO-Device hat das Licht der Welt erblickt. Es gibt jetzt eine Gerätedatei \cmd{/dev/uio0} sowie verschiedene Dateien im Verzeichnis \cmd{/sys/class/uio/uio0/}, die die in \cmd{struct uio\_info} enthaltenen Daten wiedergeben. Natürlich wird man in der Realität dem obigen Beispielcode noch Fehlerüberprüfungen hinzufügen, dies wurde hier der Übersichtlichkeit halber weggelassen. Damit das Ganze funktioniert, muss natürlich noch der oben angegebene Interrupt-Handler implementiert werden. Sein Gerüst sieht so aus: \begin{lstlisting} static irqreturn_t mein_irq_handler(int irq, struct uio_info *dev_info) { void *ptr = dev_info->mem[0].internal_addr; if (Interrupt_kam_nicht_von_meiner_Karte) return IRQ_NONE; \end{lstlisting} Hier sind Zugriffe mit Hilfe von ioread/iowrite möglich: \begin{lstlisting} ein_byte = ioread8(ptr + MEIN_REGISTER_OFFSET); iowrite8(ein_byte | INTERRUPT_AUS, ptr + NOCH_EIN_OFFSET); Hier muss sichergestellt sein, dass die Interruptleitung wieder inaktiv ist! return IRQ_HANDLED; } \end{lstlisting} Jetzt ist der Kernel-Teil des Treibers komplett. Beispiele für vollständige Treiber finden sich im Verzeichnis \cmd{drivers/uio/} in den Kernel-Quellen oder auf dem Server des Open Source Automation Development Lab (www.osadl.org). Ausführliche Dokumentation findet sich auch in den Kernel-Quellen in \cmd{Documentation/DocBook/uio-howto.tmpl}; mit \cmd{make htmldocs} erstellt man daraus eine lesbare Form. \subsubsection{Testen mit lsuio} Hat man einen UIO-Kerneltreiber geladen, so führt der erste Weg ins Verzeichnis \cmd{/sys/class/uio/uio0}, wo man überprüft, ob alle Dateien die erwarteten Werte enthalten. Dies kann auf Dauer recht lästig sein, besonders, wenn man mehrere UIO-Devices gleichzeitig testen will. Hier hilft ein kleines Tool namens \cmd{lsuio}, das alle diese Werte übersichtlich auflistet. Der Sourcecode dieses Tools findet sich auf dem Server des OSADL. Nach dem Auspacken des Archivs erstellt das übliche \begin{lstlisting} ./configure && make && make install \end{lstlisting} das Tool. Dabei wird auch eine manpage installiert, die die möglichen Optionen des Programms erläutert. Zum Beispiel sorgt die Option -m dafür, dass \cmd{lsuio} testweise überprüft, ob der Speicher der Karte problemlos in den Userspace gemappt werden kann. Der recht übersichtliche Sourcecode von \cmd{lsuio} (er besteht nur aus zwei C-Dateien) kann im Übrigen als Beispiel für eigene Anwendungen dienen. Der Userspace-Teil des Treibers sollte nämlich nie eine bestimmte Version des Kerneltreibers voraussetzen. Ebenso sollte er nicht als selbstverständlich voraussetzen, dass das eigene Device immer als \cmd{/dev/uio0} erscheint. Durch Überprüfen der Dateien \cmd{name} und \cmd{version} in den Verzeichnissen unterhalb von \cmd{/sys/class/uio} kann man diese Informationen auf sichere Weise gewinnen. \subsubsection{Weiter geht's im Userspace} Der Userspace-Teil des Treibers wird zunächst über das sysfs den Namen des zuständigen Device-Files ermitteln. Im Folgenden wird der Einfachkeit halber \cmd{/dev/uio0} angenommen. Ausserdem wird er überprüfen, ob die Version des Kerneltreibers den Erwartungen entspricht. Wenn Alles im grünen Bereich ist, wird \cmd{/dev/uio0} geöffnet: \begin{lstlisting} int fd = open("/dev/uio0", O_RDWR); \end{lstlisting} Jetzt müssen wir uns Zugriff auf den Speicher der Karte verschaffen. Nehmen wir an, die Karte verfügt über einen 4kByte großen Bereich, auf den wir zugreifen wollen: \begin{lstlisting} void* map_addr = mmap( NULL, 4096, PROT_READ, MAP_SHARED, fd, 0); \end{lstlisting} Jetzt enthält \cmd{map\_addr} einen Zeiger, mit dem direkt auf den Speicher der Karte zugegriffen werden kann. Der Treiber wird jetzt die Register der Karte mit sinnvollen Werten beschreiben. Zuletzt wird er dafür sorgen, dass die Karte Interrupts generiert. Auf das Eintreffen von Interrupts wartet der Treiber, indem er in einer Schleife einen blockierenden \cmd{read()}-Aufruf für das Device-File ausführt. Dabei liest er einen vorzeichenbehafteten 32-bit-Wert. Da \cmd{read()} standardmäßig blockiert, wenn bei \cmd{open()} kein \cmd{O\_NONBLOCK} angegeben wurde, sieht das einfach so aus: \begin{lstlisting} s32 irq_count; while (read(fd, &irq_count, 4)==4) { /* Interrupt-Aktionen */ } \end{lstlisting} Natürlich sollte die Schleife noch eine Abbruchbedingung haben, damit das Programm irgendwann einmal beendet werden kann. Der in \cmd{irq\_count} gelieferte Wert ist die Gesamtzahl der aufgetretenen Interrupts. Meist wird man sich diesen Wert merken wollen, damit man beim nächsten Interrupt überprüfen kann, ob der Wert genau um eins größer geworden ist. Will man gleichzeitig auf mehrere Filedeskriptoren warten, z.B. auf Interrupts des UIO-Device und auf das Eintreffen von Bytes in einem Netzwerk-Socket, so kann man auch \cmd{select()} verwenden. Insgesamt ist der Userspace-Teil des Treibers sehr Entwickler-freundlich. Unter der Annahme, dass man die Karte nicht kaputt programmieren kann, steht Experimenten kaum etwas im Weg. Sie können jedenfalls keine schlimmeren Abstürze verursachen als beim Programmieren jeder anderen Anwender-Applikation. \subsubsection{UIO oder nicht UIO} Sollen jetzt in Zukunft alle Treiber mittels UIO realisiert werden? Sicher nicht. UIO eignet sich sehr gut für Hardware, die Interrupts erzeugt, deren Register durch Memory-Mapping in den Userspace gebracht werden können, und die vor allem nur wenig mit anderen Subsystemen des Kernels zu tun haben. Letzteres ist leicht einzusehen, da die Interaktion mit anderen Subsystemen nur innerhalb des Kernel erfolgen kann. Wollte man etwa den Treiber für einen SATA-Controller mit UIO realisieren, so müßte dieser ja ein gültiges Block-Device auf die Beine stellen. Hätte man den dazu nötigen Code, der eng mit anderen Kernel-Teilen zusammenarbeiten würde, fertiggestellt, so würde man bemerken, dass für den Userspace nicht mehr viel übrig bleibt. Auch Hardware, die keine Interrupts erzeugt, ist kein idealer Kandidat für UIO, da die saubere Handhabung von Interrupts die eigentliche Besonderheit des Frameworks darstellt. Das reine Mapping von Speicher in den Userspace kann man auch ohne UIO haben. Allerdings bietet eine Implementierung mit Hilfe von UIO immer noch den Vorteil, eine standardisierte und dokumentierte sysfs-Schnittstelle gratis dazu zu bekommen. Eine weitere Einschränkung besteht darin, dass UIO in seiner aktuellen Version kein DMA unterstützt. Die Entwickler planen, dies in einer zukünftigen Version nachzureichen, falls sich Anwendungen dafür finden. Ein naheliegendes Beispiel sind natürlich Treiber für Grafikkarten, allerdings hat sich hier durch die weite Verbreitung von X.org bereits ein anderer Standard etabliert, der im Übrigen teilweise ähnliche Ansätze wie UIO aufweist. Aber auch I/O-Karten, beispielsweise schnelle A/D-Wandler, könnten DMA benötigen. Die Zukunft wird zeigen, ob an dieser Stelle Nachfrage besteht. \subsubsection{Fazit} Durch das seit Kernel-Version 2.6.23 in Linux verfügbare Userspace I/O-Framework wird es besonders für Industriebetriebe erheblich einfacher, Treiber für ihre I/O-Karten zu schreiben und zu warten. Der Kernel-Teil des Treibers beschränkt sich auf ein absolutes Minimum, und das UIO-Framework entkoppelt ihn wirkungsvoll von der Vielzahl an internen Kernelfunktionen, die herkömmliche Treiber verwenden müssen. Die Entwicklung der eigentlichen Treiber-Funktionalität erfolgt bequem im Userspace, unter Verwendung der für die Applikationsentwicklung gewohnten Tools und Bibliotheken. Hat man sich einmal für UIO entschieden, so sollte man unbedingt anstreben, den Kernelteil des Treibers im Mainline-Kernel unterzubringen. Auf diese Weise erspart man sich und seinen Kunden das ständige Patchen des Kernels und die Pflege eines out-of-tree Moduls. Verhilft man seinem Kernel zusätzlich zu Echtzeit-Fähigkeiten, indem man den Realtime-Preemption-Patch einspielt, so erhält man ein einfach zu programmierendes und vollständig aus Open Source-Software bestehendes System für die Automatisierungstechnik. \input{tailhandout}