summaryrefslogtreecommitdiff
path: root/kernel-devel/uio-driver/handout_uio-driver_de.tex
blob: f25b53825f2f69a1a04f5488982797e9d28e0015 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
\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}