Für den nächsten Kernel sieht Linus Torvalds optional ein besonderes Userspace-Treibermodell vor, das auch proprietäre Treiber einbinden könnte. Doch schon die Tücken der Technik könnten den Durchbruch in der Praxis verhindern.
Jahrelang versuchten mehrere Entwickler Linus Torvalds von der Notwendigkeit eines Programmier-Interface für so genannte Userspace-Treiber zu überzeugen – ohne Erfolg. Ein Userspace-Treiber soll – wie jeder andere auch – Anwendungsprogrammen Schnittstellen für den Hardwarezugriff bereitstellen, er steuert die Hardware aber von der Applikationsebene, arbeitet also im nicht-privilegierten Modus.
Doch jetzt hat Linus seine Blockade aufgegeben: Er erlaubt in dem kommenden Kernel 2.6.23 solche Userspace-Treiber und stellt dafür ein Interface im Kernel bereit [1]. Der Code geht auf Greg Kroah-Hartman und sein Interface Industrial IO zurück [2]. In den einschlägigen Foren diskutieren nun Linux-Anhänger, ob einerseits vielleicht bald GPL-allergische Firmen endlich Binärtreiber für ihre Hardware schreiben und ob andererseits Linux damit langsam zu der von Linus Torvalds stets abgelehnten Mikrokernel-Architektur mutiere. Denn, so die These der reinen Informatik-Lehre, je mehr Code im Userspace, desto sicherer und stabiler das System.
Um einen Userspace-Treiber für Linux zu schreiben, muss der Entwickler allerdings im Kernel anfangen. Der dort erforderliche Code verbindet das reale Gerät mit dem Userspace-Teil des Treibers (siehe Abbildung 1). Dieser Part sorgt für die Vergabe einer Minornummer und reserviert bei Bedarf Hardwareressourcen. Außerdem legt der Kernel Pseudodateien im Sys-Filesystem an, über die sich der Userspace-Part über die Adressen für den späteren Zugriff informiert.

Abbildung 1: Neben dem Userpart benötigt der Userspace-Treiber immer auch einen Kernelpart. Wie eine Applikation auf die angebundene Hardware zugreift, ist nicht festgelegt.
Userland greift durch
Für den Hardwarezugriff blendet der Userpart nämlich die Speicherbereiche der Hardware per Mmap in seinen Adressraum ein. Danach darf er die betreffenden Register lesen und schreiben. Falls die Hardware Interrupts erzeugt, muss der Kernelteil zudem eine Interrupt-Service-Routine (ISR) aufweisen.
Das Userland kann sich über das Auslösen eines Interrupts informieren lassen, indem es blockierend von einer Gerätedatei (»/dev/uio0«) liest, die der Kernel erzeugt. Sobald ein Interrupt eintrifft, weckt das den wartenden Rechenprozess im Userspace. Die eigentlichen Daten tauschen beide Treiberteile dann über die eingeblendeten Hardwareregister (Adressen) aus.
Das Userspace-Treibermodell spezifiziert also im Wesentlichen nur das Einblenden von Hardwareressourcen in den Adressenraum einer Applikation und die Benachrichtigung über ausgelöste Events (Interrupts) – per Blocking Read oder über »select()« auf eine Gerätedatei. Wie letztlich Applikationen über einen Userspace-Treiber auf das Gerät zugreifen, bleibt komplett dem Treiberentwickler überlassen (siehe Abbildung 1). Folglich nennen die Kernelentwickler den Mechanismus auch nur User I/O (UIO).
Der Kernelpart jongliert mit drei Objekten
Listing 1 zeigt den Kernelpart eines minimalistischen Userspace-Treibers ohne Interruptbehandlung. Insgesamt füllt der Code drei Datenstrukturen (Objekte) mit Inhalt und übergibt sie dem Kernel: Das Gerätemodell respektive Sys-Filesystem benötigt ein Treiber- (»struct device_driver«) und ein Geräteobjekt (zum Beispiel »struct platform_device«), das UIO-Subsystem ein Info-Objekt »uio_info« (Abbildung 2).
|
Listing 1: |
|---|
01 #include <linux/module.h>
02 #include <linux/platform_device.h>
03 #include <linux/uio_driver.h>
04
05 struct uio_info kpart_info = {
06 .name = "kpart",
07 .version = "0.1",
08 .irq = UIO_IRQ_NONE,
09 };
10
11 static int drv_kpart_probe(struct device *dev);
12 static int drv_kpart_remove(struct device *dev);
13 static struct device_driver uio_dummy_driver = {
14 .name = "kpart",
15 .bus = &platform_bus_type,
16 .probe = drv_kpart_probe,
17 .remove = drv_kpart_remove,
18 };
19
20 static int drv_kpart_probe(struct device *dev)
21 {
22 printk("drv_kpart_probe( %p )n", dev );
23 kpart_info.mem[0].addr = (unsigned long)kmalloc(512, GFP_KERNEL);
24 if( kpart_info.mem[0].addr==0 )
25 return -ENOMEM;
26 kpart_info.mem[0].memtype = UIO_MEM_LOGICAL;
27 kpart_info.mem[0].size = 512;
28 if( uio_register_device(dev,&kpart_info) )
29 return -ENODEV;
30 return 0;
31 }
32
33 static int drv_kpart_remove(struct device *dev)
34 {
35 uio_unregister_device(&kpart_info);
36 return 0;
37 }
38
39 static struct platform_device *uio_dummy_device;
40 static int __init uio_kpart_init(void)
41 {
42 uio_dummy_device = platform_device_register_simple("kpart", -1,
43 NULL, 0);
44 return driver_register(&uio_dummy_driver);
45 }
46
47 static void __exit uio_kpart_exit(void)
48 {
49 platform_device_unregister(uio_dummy_device);
50 driver_unregister(&uio_dummy_driver);
51 }
52
53 module_init( uio_kpart_init );
54 module_exit( uio_kpart_exit );
55
56 MODULE_LICENSE("GPL");
|
Wer für das Sys-FS-Treiberobjekt die Methoden »probe()« und »remove()« implementiert, kann hier die Hardware-Adressen identifizieren und ins Info-Objekt eintragen. Bei einer PCI-Hardware eignen sich zum Identifizieren der Adresslagen die aus [3] bekannten Kernelfunktionen. In diesem Fall lässt sich auch statt des Plattform-Device ein PCI-Device instanziieren. Ein Aufruf der Funktion »uio_register_device()« (Syntax siehe Tabelle 1) übergibt das initialisierte Info-Objekt schließlich dem UIO-Subsystem.
|
Tabelle 1: Funktionen des |
|---|
Leistungsshow
Im Rahmen dieser Registrierung überprüft das UIO-Subsystem, ob es im Gerätemodell die Geräteklasse »uio« bereits gibt. Ist dies nicht der Fall, legt es die Klasse an. Außerdem stellt das UIO-Subsystem sicher, dass das Modul eine Majornummer zugewiesen bekommen hat. Danach reserviert das Subsystem für den neuen Treiber eine Minornummer; Udev erzeugt die zugehörige Gerätedatei, zum Beispiel »/dev/uio0«.
Schließlich legt die Routine die entsprechenden Attributdateien an und hängt – falls vom Treiberentwickler implementiert – die Interrupt-Service-Routine ein. Um diesen Kernelcode zu schreiben, benötigt der Programmierer also recht gute Kenntnisse über den Kernel, besonders im Umgang mit dem Gerätemodell, dem Sys-FS und der Implementierung von Interrupt-Service-Routinen.
Für jeden Treiber, der sich per »uio_register_device()« beim UIO-Subsystem anmeldet, legt dieses – wie oben gerade beschrieben – eine Gerätedatei nach dem Schema »/dev/uio0«, »/dev/uio1«, »/dev/uio2« und so weiter an. Jetzt ist es die Aufgabe des Userparts festzustellen, hinter welcher der gelisteten Gerätedateien sich die betreffende Hardware befindet. Dazu kann sie wieder einmal die Pseudodateien im Sys-Filesystem nutzen (Abbildung 3).

Abbildung 3: Sobald der Kernelpart eines Userspace-Treibers geladen ist, legt UIO Informationen im Sys-Filesystem an. Für den Zugriff auf die Hardware muss sich der Userpart die Informationen holen und sie auswerten.
Unter dem Pfad »/sys/class/uio/uioX« (»X« durch »0«, »1« und so weiter ersetzen) lässt sich die Datei »name« auslesen, die den hoffentlich eindeutigen Treibernamen enthält. Unter dem entsprechenden Verzeichnis findet der Userpart dann die vom Kernelpart abgelegten Informationen bezüglich der Adressbereiche. Die blendet der Userpart per »mmap()« in den eigenen Adressraum ein. Den dafür benötigten Filedeskriptor erhält, wer die zugehörige Gerätedatei, zum Beispiel »/dev/uio0«, öffnet. War »mmap()« erfolgreich, darf der Userpart (siehe Beispielcode in Listing 2) auf die Hardwareregister zugreifen.
|
Listing 2: |
|---|
01 #include <stdio.h>
02 #include <fcntl.h>
03 #include <stdlib.h>
04 #include <unistd.h>
05 #include <sys/mman.h>
06
07 #define UIO_DEV "/dev/uio0"
08 #define UIO_ADDR "/sys/class/uio/uio0/maps/map0/addr"
09 #define UIO_SIZE "/sys/class/uio/uio0/maps/map0/size"
10
11 static char uio_addr_buf[16], uio_size_buf[16];
12
13 int main( int argc, char **argv )
14 {
15 int uio_fd, addr_fd, size_fd;
16 int uio_size;
17 void *uio_addr, *access_address;
18
19 addr_fd = open( UIO_ADDR, O_RDONLY );
20 size_fd = open( UIO_SIZE, O_RDONLY );
21 uio_fd = open( UIO_DEV, O_RDONLY );
22 if( addr_fd<0 || size_fd<0 || uio_fd<0 ) {
23 fprintf(stderr,"Kann UIO-Dateien nicht öffnen...n");
24 return -1;
25 }
26
27 read( addr_fd, uio_addr_buf, sizeof(uio_addr_buf) );
28 read( size_fd, uio_size_buf, sizeof(uio_size_buf) );
29 uio_addr = (void *)strtoul( uio_addr_buf, NULL, 0 );
30 uio_size = (int)strtol( uio_size_buf, NULL, 0 );
31
32 access_address = mmap(NULL, uio_size,
33 PROT_READ, MAP_SHARED, uio_fd, 0);
34 printf("Auf die HW-Adresse %p (Länge %d) "
35 "kann über die logischer Adresse %p"
36 " zugegriffen werdenn", uio_addr,
37 uio_size, access_address);
38
39 // Ab hier kann der Zugriff auf die Hardware-Register erfolgen...
40 // ...
41 return 0;
42 }
|
Eine Routine, die über das Auftreten von Interrupts informiert werden will, ruft »select()« oder »read()« im nicht-blockierenden Modus auf. »read()« liefert übrigens die Anzahl der aufgetretenen Events (Interrupts) als 4-Byte-Wert zurück. Zur Sicherheit noch mal: Der Mechanismus funktioniert nur dann, wenn man vom Kernel genau 4 Bytes mit dem Datentyp »s32« anfordert! Ein Zugriff über das Kommando »cat« beispielsweise, das grundsätzlich 4096 Bytes anfordert, geht schief.
Die ISR ist schnell gemacht
Die im Kernel hierzu notwendigerweise zu kodierende Interrupt-Service-Routine darf schlank ausfallen. Sie muss meist nur feststellen, ob die eigene Hardware den Interrupt ausgelöst hat, und wenn ja, den Interrupt Hardware-seitig quittieren und »IRQ_HANDLED« zurückgeben. Wer innerhalb des Kernels auf einem anderen Wege den schlafenden User-Part aufwecken will, ruft die Funktion »uio_event_notify()« auf.
Selbst Hand anlegen
Wer das neue Interface ausprobieren will, muss sich einen sehr neuen Kernel zulegen, zum Beispiel 2.6.23-rc3, und in dessen Kernelkonfiguration das UIO-Subsystem unter »Device Drivers | Userspace I/O | Userspace I/O Drivers« einschalten (siehe Abbildung 4). Ist UIO im Kernel als Modul übersetzt, lädt Root es durch Aufruf von »modprobe uio«.

Abbildung 4: Die ab Kernel 2.6.23 geplanten Userspace-Treiber sind in der Kernelkonfiguration anfangs deaktiviert.
Danach geht es daran, den Userspace-Treiber zu generieren, also Kernelmodul und den Userspace-Part. Falls die Quelldateien von Listing 1, 2 und 3 (Makefile) im Verzeichnis »/tmp/uio« liegen, funktionieren Erzeugen und Testen wie in Abbildung 5 dargestellt.
|
Listing 3: Makefile für |
|---|
01 ifneq ($(KERNELRELEASE),) 02 obj-m := kernel_part.o 03 04 else 05 KDIR := /lib/modules/$(shell uname -r)/build 06 PWD := $(shell pwd) 07 08 default: 09 $(MAKE) -C $(KDIR) M=$(PWD) modules 10 endif 11 12 clean: 13 rm -f *.ko *.o user_part 14 15 user_part: user_part.c 16 cc -g -Wall user_part.c -o user_part |

Abbildung 5: Der Beispielcode demonstriert das Zusammenspiel zwischen Kernel- und Userpart des Userspace-Treibers.
Um das Beispiel überschaubar zu halten, koppelt es weder eine PCI-Hardware an noch verwendet es Interrupts. Informationen dazu gibt es unter [3] und [4]. Der letzte Link liefert zudem Hinweise, dass und wie der Programmierer eigene »open()«-, »release()«- und »mmap()«-Funktionen im Kernel einhängt, um beispielsweise einen ausgefeilten Zugriffsschutz zu realisieren. Fragen bezüglich der Sicherheit sind nämlich nicht wirklich gelöst.
Auch die Autoren des UIO-Subsystems haben Beispielcode veröffentlicht. Anders als in der Kerneldokumentation angegeben [4] befindet sich aber kein Template-Treiber in den Kernelquellen. Der auf Kernel 2.6.23-rc3 angepasste Treiber »uio_dummy.c« mit dem zugehörigen Userpart »uio_events.c« lässt sich aber über [5] herunterladen.
Was soll’s?
Fazit: Mit der Aufnahme des UIO-Subsystems in den aktuellen Kernel vollzieht Linus Torvalds nicht die große Wendung in Sachen Userspace-Treiber. Der keine, 1000 Zeilen umfassende Code des UIO-Subsystems bietet bei genauer Betrachtung technisch kaum mehr, als mit Mmap (siehe [6]) und normalen Treiberinterfaces schon vorher möglich war. Das Einbinden von Hardware in die vorhandenen Kernelsubsysteme, beispielsweise für Netzwerk- oder Blockgerätetreiber, ist überhaupt nicht vorgesehen (siehe Kasten “Vor- und Nachteile des Userspace-Treibermodells”).
Die eingeschränkte Funktionalität, das Fehlen eines definierten Interface zwischen Applikation und Userspace-Treiber und das notwendige umfangreiche Wissen über den Kernel lassen eine große Verbreitung außerhalb des ursächlichen Einsatzes für Embedded Linux als unwahrscheinlich erscheinen. (jk)
|
Vor- und Nachteile des |
|---|
|
Vorteile:
Nachteile:
|
|
Infos |
|---|
|
[1] Linus Torvalds Ankündigung zum Kernel 2.6.23-rc1: [http://article.gmane.org/gmane.linux.kernel/559066] [2] Simple userspace interface for PCI drivers: [http://www.uwsg.indiana.edu/hypermail/linux/kernel/0608.3/1908.html] [3] Eva-Katharina Kunst, Jürgen Quade, “Linux-Treiber entwickeln”: Dpunkt-Verlag, 2. Auflage, Januar 2006 [4] Hans-Jürgen Koch, “The Userspace I/O-Howto”: [/usr/src/linux-2.6.23-rc3/Documentation/DocBook/uio-howto.tmpl] [5] Vollständige Listings zum Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2007/11/Kern-Technik] [6] Eva-Katharina Kunst, Jürgen Quade, “Kern-Technik”, Folge 32: Linux-Magazin 03/07, S. 104 |
|
Die Autoren |
|---|
|
Eva-Katharina Kunst, Journalistin, und Jürgen Quade, Professor an der Hochschule Niederrhein, sind seit den Anfängen von Linux Fans von Open Source. Unter dem Titel “Linux Treiber entwickeln” haben sie zusammen ein Buch zum Kernel 2.6 veröffentlicht. |







