Einen Treiber in den Kernel einzubinden ist leider wenig intuitiv. Deshalb gibt es hier ein Grundgerüst als Ausgangspunkt. Es zeigt, wie der Kernel dem Treiber Gerätenummern zuweist und der Gerätedateiverwalter Udev eigenständig die zugehörigen Gerätedateien anlegt .
Das einfachste Programm des Kernels, sein “Hello World”, ist übersichtlich: 16 Zeilen Code, zwei Funktionen und ein passendes Makefile reichen aus, um Selbstgeschriebenes mit dem Kernel zu verknüpfen (Listing 1). Als Ergebnis steigt im Syslog der vom C-Erfinder Brian Kernighan entwickelte Willkommensgruß aus den Tiefen des Betriebssystem auf (Abbildung 1).
|
Listing 1: Hallo Kernelwelt |
|---|
01 #include <linux/module.h>
02
03 static int __init template_init(void)
04 {
05 printk("Hello worldn");
06 return 0;
07 }
08
09 static void __exit template_exit(void)
10 {
11 return;
12 }
13
14 module_init(template_init);
15 module_exit(template_exit);
16 MODULE_LICENSE("GPL");
|
Der kurze Code ist leicht nachzuvollziehen. Die erste Funktion (Zeile 3) ruft der Prozessor beim Laden, die zweite beim Entladen des Programms auf (Zeile 9). »printk()« (Zeile 5) steht für “Print Kernel” und entspricht »printf()« in anderen Programmen. Für den Kernel-Neuling bleiben lediglich die ungewöhnlichen Schlüsselwörter »__init« (Zeile 3) und »__exit« (Zeile 9) und die Makros »module_init()«, »module_exit()« und »MODULE_LICENSE()« (Zeilen 14 bis 16 ) erklärungsbedürftig.
Hallo Kernelwelt
»__init« klassifiziert die zugehörige Funktion als eine Initialisierungsfunktion, die nur ein einziges Mal in Aktion tritt, nämlich beim Laden des Code. Linux entfernt sie nach dem Aufruf und gibt den ansonsten unnütz belegten Speicher frei. Eine über »__exit« kategorisierte Funktion wandert unter bestimmten Umständen gar nicht erst in den Hauptspeicher. In beiden Fällen optimiert der Kernel den Programmablauf.
Die beiden Makros »module_init()« und »module_exit()« erlauben es dem Programmierer, die Namen der Modul-Initialisierungs- und Deinitialisierungsfunktion frei zu wählen. Der Programmtext des entstehenden Kernelmoduls wird dadurch besser lesbar. »MODULE_LICENSE()« schließlich verknüpft den Treibercode mit der vom Programmierer gewählten Lizenz (Zeile 16). Mit einer proprietären Lizenz steht ihm nur eingeschränkte Kernelfunktionalität zur Verfügung. Wer sich aber zur freien Software bekennt, greift auf alle vom Kernel bereitgestellten Routinen zu.
Garniert mit ein paar weiteren Codezeilen mutiert das vorliegende Hello-World-Kernelmodul schnell zu einem Gerätetreiber, auf den sein Schöpfer mit den Applikationsfunktionen »open()«, »close()«, »read()« und »write()« zugreift. Dazu erstellt er die Liste »struct file_operations« (Listing 2, Zeile 6), die die Adressen der im Treiber aufzurufenden Funktionen »driver_open()«, »driver_close()«, »driver_read()« und »driver_write()« zusammenfasst. Mit dieser Liste meldet sich der Kernelprogrammierer beim IO-Subsystem des Kernels an, indem er die Funktion »register_chrdev()« (Zeile 11) aufruft und eine Treiber-Identifikationsnummer angibt. Das spätere Abmelden übernimmt die Funktion »unregister_chrdev()« in Zeile 19.
Arbeitslos
Die Liste kann – wie in diesem Beispiel – leer sein. Dann erfüllt der Treiber einfach keine Funktion. Die erwähnte Identifikationsnummer oder Majornummer verbindet sich im Applikationsbereich durch Aufruf des Kommandos »mknod« mit einer Gerätedatei. Den Namen dieser Gerätedatei bekommt die Applikationsfunktion »open()« als ersten Parameter übergeben (Abbildung 2).

Abbildung 1: Mit einem geeigneten Makefile lässt sich aus dem Code von Listing 1 ein Kernelmodul generieren. Das Kommando »insmod« lädt das Modul in den Kern. Bei Erfolg grüßt der Kernel die Welt in seinen Messages mit einem Hallo.
Mit Kernel 2.6 hat Linus Torvalds die antiquierten und in ihrer Zahl begrenzten Majornummern gegen moderne Gerätenummern ausgetauscht. Für sie baute der Treiber-Chef Greg Kroah-Hartman ein neues Interface, mit dem sich Treiber beim IO-Subsystem des Kernels anmelden. Das neue Interface wirkt offenbar auf viele Entwickler weder intuitiv noch elegant. Sie greifen daher weiterhin auf die eben genannte und bewährte »register _chrdev()«-Methode zurück. Auch wenn alles wie hier beschrieben programmierbar und funktionstüchtig ist, so ist dies nicht im Sinne der Erfinder, moderne Treibereinbindung sieht anders aus.
Modern mit Udev
Listing 3 stellt ein universell einsetzbares Treibergerüst nach dem aktuellen Verfahren dar. Für diese von Kernelhackern empfohlene Methode reserviert der Aspirant zunächst einen Bereich von Gerätenummern (siehe Abbildung 3) – wenn nur ein Gerätetyp avisiert ist, dann ist auch nur eine Gerätenummer nötig. Zum Reservieren stehen die Funktionen »alloc_chrdev_region()« und »register_chrdev_region()« zur Verfügung (die Funktionen erklärt kurz Tabelle 1 am Ende des Artikels).
|
Listing 2: Der erste Treiber |
|---|
01 #include <linux/module.h>
02 #include <linux/fs.h>
03
04 static int major;
05
06 static struct file_operations fops = {
07 };
08
09 static int __init template_init(void)
10 {
11 if ((major = register_chrdev(0, "template",
12 &fops)) == 0) {
13 return -EIO;
14 }
15 return 0;
16 }
17
18 static void __exit template_exit(void)
19 {
20 unregister_chrdev(major, "template");
21 return;
22 }
23
24 module_init(template_init);
25 module_exit(template_exit);
26 MODULE_LICENSE("GPL");
|
Der Unterschied: Bei »alloc_chrdev_region()« teilt der Kernel dem Treiber einen Bereich von Gerätenummern zu. Bei »register-chrdev_region()« fordert der Treiber hingegen selbst einen bestimmten Bereich an. Dank des neuen Gerätedatei-Verwalters Udev [1], der automatisch Gerätedateien erzeugt, ist es aber kaum noch nötig, mit vorbestimmten Gerätenummern zu arbeiten. Die Funktion »alloc_chrdev_region()« gilt daher als erste Wahl (ab Zeile 20).

Abbildung 2: Die Majornummer ist der Angelpunkt für die Auswahl der aufzurufenden Treiberfunktion. Sie verbindet die Gerätedatei mit der Start-Funktion.
Mit der einen Gerätenummer oder einem Bereich davon meldet der Programmierer den Treiber beim Kernel an. Dazu ist Speicher zu reservieren und zu initialisieren, im Fachjargon: Das Treiberprojekt ist zu instanzieren. Die Funktionen »cdev_alloc()« oder »kmalloc()« dienen wahlweise dazu, Speicher zu reservieren. Falls »kmalloc()« zum Einsatz kommt, findet die Initialisierung mit der Funktion »cdev_init()« statt. Sie nimmt als Parameter neben dem Treiberobjekt die Liste der Treibermethoden (»struct file_operations«) entgegen. Wenn der Programmierer hingegen »cdev_alloc()« verwendet, ordnet er die Liste der Treibermethoden direkt dem zugehörigen Strukturelement zu (Zeile 25). Um das Entladen des Treibers zu unterbinden, so lange dieser noch genutzt wird, weist er außerdem dem Attribut »owner« das vom Kernel vordefinierte Makro »THIS_MODULE« zu (Zeile 30). »THIS_MODULE« repräsentiert die Adresse des Modulobjekts, womit der Entladeschutz in Kraft tritt.
Einlass gewähren
Jetzt ist das Treiberobjekt so weit vorbereitet, um es dem Kernel durch Aufruf der Funktion »cdev_add()« zu übergeben (ab Zeile 32). Läuft diese Funktion erfolgreich ab, ist der Treiber im System etabliert. Andernfalls ist das Aufräumen des Speichers angesagt: »kobject_put()« (ab Zeile 45) gibt das Treiberobjekt frei (siehe Kasten “Kobjects – Kernelobjekte”) und die Funktion »unregister_chrdev_region()« (ab Zeile 47) die Gerätenummer.

Abbildung 3: Ein Treibergrundgerüst lässt sich klassisch oder nach der weniger intuitiven, aber modernen Variante aufbauen.
Apropos Aufräumen: Am Ende, beim Entladen des Treibers, laufen alle seine Aktionen rückwärts ab. Als Erstes meldet die Funktion »cdev_del()« den Treiber beim Kernel ab (Zeile 61). Hier ist darauf zu achten, dass der Speicher des zugehörigen Treiberobjekts sofort mit dem Aufruf der Funktion frei wird, sofern kein anderes Kernelobjekt noch darauf zugreift. Keinesfalls sollte explizit die Funktion »kobject_put()« in den Editor wandern: »cdev_del()« verwendet intern bereits diese Funktion, der Speicher würde also zweimal mit kaum vorhersehbaren Effekten freigegeben. Als Zweites gibt ab Zeile 62 die Funktion »unregister_chrdev_region()« den Bereich der Gerätenummern wieder frei.
Nicht vergessen: Gerätedatei-Eintrag
Obwohl der Treiber im System verankert ist, kann eine Applikation noch nicht auf ihn zugreifen. Dafür muss erst ein Gerätedatei-Eintrag vorhanden sein. Da das hier verwendete Treibergerüst universell einsetzbar sein soll, lässt es sich die Gerätenummer vom System zuteilen. Daher muss auch der Eintrag als Gerätedatei dynamisch erfolgen.
|
Listing 3: Modernes |
|---|
01 #include <linux/module.h>
02 #include <linux/fs.h>
03 #include <linux/cdev.h>
04 #include <linux/device.h>
05
06 #define TEMPLATE "template"
07
08 static dev_t template_dev_number;
09 static struct cdev *driver_object;
10 struct class *template_class;
11 static int device_type_nr=1;
12 static struct file_operations fops = {
13 // Adressen der Treiberfunktionen ablegen
14 };
15
16 static int __init template_init(void)
17 {
18 int min;
19 // Als Treiber anmelden
20 if (alloc_chrdev_region(
21 &template_dev_number, 0,
22 device_type_nr, TEMPLATE) < 0)
23 return -EIO;
24 // Anmeldeobjekt reservieren
25 driver_object = cdev_alloc();
26
27 if (driver_object == NULL )
28 goto free_device_number;
29
30 driver_object->owner = THIS_MODULE;
31 driver_object->ops = &fops;
32 if (cdev_add(driver_object,
33 template_dev_number, 1))
34 goto free_cdev;
35
36 // Gerätedateien anlegen
37 template_class = class_create(THIS_MODULE,
38 TEMPLATE);
39 for (min = 0; min < device_type_nr; min++)
40 device_create(template_class, NULL,
41 template_dev_number + min,
42 NULL, "%s%d", TEMPLATE, min);
43 return 0;
44
45 free_cdev:
46 kobject_put(&driver_object->kobj);
47 free_device_number:
48 unregister_chrdev_region(
49 template_dev_number, 1);
50 return -EIO;
51 }
52
53 static void __exit template_exit(void)
54 {
55 int min;
56
57 for (min = 0; min < device_type_nr; min++)
58 device_destroy(template_class,
59 template_dev_number + min);
60 class_destroy(template_class);
61 cdev_del(driver_object);
62 unregister_chrdev_region(
63 template_dev_number, device_type_nr);
64 return;
65 }
66
67 module_init(template_init);
68 module_exit(template_exit);
69 MODULE_LICENSE("GPL");
|
Diese Aufgabe übernimmt seit einiger Zeit der Daemon Udevd. Allerdings erwartet er, dass der Treiber dabei mit anpackt. Dieser kennt seine Gerätenummern und erstellt für jede gewünschte Gerätedatei ein Verzeichnis unterhalb des Systemverzeichnisbaums »/sys/class/«. Udevd überwacht hier ständig die Einträge. Sobald ein neues Unterverzeichnis mit dem Namen des unterstützten Gerätetyps und darin eine neue Datei mit dem Namen »dev« auftaucht, wird – gemäß Konfiguration – vom Udevd eine entsprechende Gerätedatei angelegt.
Praktischerweise kann der Name des Unterverzeichnisses die Form eines Formatstrings inklusive der zugehörigen Parameter annehmen, so wie es jeder Programmierer von der Funktion »printf()« kennt. Wer den modernen Weg der Gerätenummern zugunsten des alten Schemas links liegen ließ, erzeugt mit Hilfe des Makros »MKDEV()« aus der Major- und Minornummer die Gerätenummer. Das Aufräumen erfolgt wieder in umgekehrter Reihenfolge: Als Erstes baut »device_destroy()« die Geräte-Einträge wieder zurück, dann entfernt »class_destroy()« die Sysfs-Klasse (Zeilen 57 bis 60).
Was bleibt
Listing 3 integriert den Treiber in den Linux-Kernel und informiert den Udev-Daemon genug, damit er die Gerätedatei anlegt. Nun fehlen noch die eigentlichen Treiberfunktionen, die gefragt sind, wenn eine Applikation »open()«, »close()«, »read()« oder »write()« aufruft. Die durch die Namen repräsentierten Adressen der zugehörigen Treiberfunktionen »driver_open()«, »driver_read()«, »driver_write()« oder »driver_close()« gehören in Zeile 13. Ebenfalls muss Powermanagement noch in den Programmtext wandern.
Leider ist das Sys-Filesystem nicht frei von Komplikationen. Der Programmierer ist nämlich fürs Erstellen und Löschen der gewünschten Device-Dateien zuständig. Zudem muss er sie in das richtige Sys-FS-Verzeichnis unterhalb des Klassenordners einsortieren. Im einfachsten Fall legt er dazu mit »class_create()« (Zeile 37) sein eigenes Klassenverzeichnis im Sys-FS an, in das er die Gerätedatei-Einträge schreibt. Die Funktion erzeugt das Klassenobjekt und gibt eine Referenz darauf zurück. Die Funktion »device_create()« hinterlässt für jedes Gerät einen Sys-FS-Eintrag. Sie braucht als Argumente die Adresse des Klassenobjekts, die Gerätenummer und den Namen, den die Gerätedatei später tragen soll.
Soll der Treiber mehrere Gerätetypen unterstützen, übernimmt das die Variable »device_type_nr«. Wer Hardware anvisiert, die etwa über PCI oder USB angeschlossen ist, meldet den Treiber noch beim zugehörigen Kernelsubsystem für PCI respektive USB an. Schließlich möge jeder Schreiber das Treibergerüst personalisieren, indem er den allgemeinen Namen »template« gegen einen sprechenderen austauscht (Zeile 6). Nun steht umfangreichen Kernel-Messages – oder auch sinnvollen Modulen – nichts mehr im Wege: Happy Hacking! (ake/mg)
|
Kobjects – |
|---|
|
Der Linux-Kernel stellt Anwendungsprogrammen über das Sys-Filesystem eine Sicht auf die vorhandene Hardware und auf die sie bedienenden Softwarekomponenten (Treiber) zur Verfügung. Die vorhandene Hardware wird durch Ordner und Dateien modelliert, die im Kern selbst über Objekte repräsentiert sind. Diese Objekte ordnen sich sämtlich um das Kernelobjekt »kobject« herum an. Objektorientiert ausgedrückt: Sys-Filesystem-Objekte leiten sich von Kobject ab. Das Kobject enthält die zentralen gemeinsamen Attribute, allen voran einen Referenzzähler, der die Freigabe des zum Objekt gehörenden Speichers koordiniert. Die Funktion »kobject_get()« inkrementiert den Zähler, ihre Schwester »kobject_put()« dekrementiert den Zähler und führt bei Bedarf die Freigabe durch. |
|
Infos: |
|---|
|
[1] Oliver Frommel, Jens-Christoph Brendel, “Wie dynamisches Gerätemanagement mit Udev funktioniert”: Linux-Magazin 09/06,S. 32 |
|
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. |






