Grundlagen


... [ Seminar Linux und Apache ] ... [ Thema Gerätetreiber unter Linux 2.4 ] ... [ Charactergerätetreiber ] ...

Übersicht: Grundlagen


Der Kernel

In Unix-Systemen ist der Kernel dafür zuständig, die Anfragen für Speicherplatz, Rechenzeit und andere Systemressourcen zu verwalten.
Eine genaue Abgrenzung der Aufgaben ist nicht immer möglich, aber grob stellt es sich wie folgt dar:


Der Kernel [1]

kernel.gif nicht gefunden


Systemcall-Interface: ermöglicht Applikationen den Zugriff auf die Dienste des Betriebssystems
Prozessmanagement: zuständig für Multitasking, Scheduling (s. Seminarthema 1)
Speicherverwaltung: Adressumsetzung, Speicherschutz, Realisierung des virtuellen Speichers
I/O-Subsystem: Dateisystem, Zugriff aif Peripherie, Geräte werden wie Dateien behandelt
Gerätetreiber: Zugriff auf die Hardware
Networking: Netzwerkoperationen sind meist nicht prozessspezifisch, weill ankommende Pakete asynchrone Ereignisse sind


Kerneltreiber oder Treibermodul?

Treiber unter Linux sind relativ einfach und schnell zu entwickeln. Es ist aber zu beachten, dass mit jeder neuen Kernel-Version leichte Modifikationen an den Schnittstellen vorgenommen werden, die damit auch Modifikationen an den Treibern notwendig machen.

Das Laden unversionierter Module zu einem versionierten Kernel mit modutils ist nicht mehr möglich.
Jedes Modul definiert ein Symbol __module_kernel_version, das von insmod gegen die Versionsnummer des Kernels geprüft wird. Ist <linux/module.h> über ein Include eingebunden, wird das Symbol vom Compiler definiert.

Folgender Code ermöglicht versionsbedingte Kompilierung:
    #if LINUX_VERSION_CODE < KERNEL_VERSION(2,4,0)
        alte Version
    #else
        neue Version
    #endif
Ein Treiber kann entweder als Kerneltreiber oder als Treibermodul realisiert werden. Was er letztlich ist, hängt von verschiedenen Faktoren ab.

Als Kerneltreiber ist der Treiber fester Bestandteil des Betriebssystems.
Ob er fester Teil sein muß, hängt vom Betriebssystem selbst und von den Aufgaben des Treibers ab.
Handelt es sich um Betriebssysteme für einfache Hardwareplattformen oder um solche mit festen Aufgaben, wie man sie oft im Umfeld von eingebetteten Systemen findet, fehlt häufig die Möglichkeit, Treiber während des Betriebs nachzuladen.
Einige Treiber müssen aber fest in den Betriebssystemkern eingebunden werden, auch wenn das dynamische Nachladen möglich ist. Das betrifft alle Treiber, die zum Hochfahren des Systems notwendig sind, wie Platten-, Disketten-, CD-ROM-Zugriffe, Tastatur oder Bildschirm.
Zuletzt ist es sinnvoll, einen Treiber als Kerneltreiber zu realisieren, wenn er sowieso ständig aktiv sein muß, da auf das entsprechende Gerät dauernd zugegriffen wird.

Wenn diese Argumente nicht zutreffen, sollte man Treiber möglichst als Modul realisieren. Das schont die Systemressourcen und spart das zeitaufwendige Neubooten, speziell beim Testen.
Außerdem muß ein Kerneltreiber als offizieller Bestandteil des Kernels akzeptiert und somit weitergereicht werden, oder es muß jedes Mal ein Patch herausgebracht werden.
Jedes Stückchen Code, das zur Laufzeit in den Kernel eingebunden werden kann, ist ein Modul.
Der Linux-Kernel unterstützt mehrere Typen (oder Klassen) von Modulen, wozu u.a., aber NICHT ausschließlich, Gerätetreiber gehören.
Jedes Modul besteht aus Objektcode, der dynamisch mit dem laufenden Kernel verlinkt werden kann. Das geschieht mit Hilfe von insmod, rmmod entlädt das Modul wieder.
Diese Grafik verdeutlicht das Geschehen:


insmod und rmmod [1]

modul.gif nicht gefunden

Beim Laden eines Moduls per insmod wird automatisch init_module() aufgerufen, eine vom Modul zur Verfügung gestellte Initialisierungsfunktion.
Das Modul kann sich jetzt als Treiber beim Betriebssystem registrieren.
Es ist auch möglich, Parameter zu übergeben, z.B. IO-Adressen, Interrupts oder eine bestimmte Betriebsart.

Beim Entladen per rmmod wird cleanup_module() aufgerufen. Danach sollte ein Treiber noch aufräumen und alloziierte Ressourcen freigeben.

Welche Module gerade im Kernel geladen sind, kann man sich über lsmod anzeigen lassen.

Folgender Code stellt ein einfaches Modul dar, das nichts spektakuläres macht:

    #include <linux/fs.h>
    #include <linux/version.h>
    #include <linux/module.h>

    static int init_module(void)
    {
      printk("<1>init_module called\n");
      return 0;
    }

    static void cleanup_module(void)
    {
      printk("<1>cleanup_module called\n");
    }
printk ist im Kernel definiert und verhält sich ähnlich wie printf. Der Kernel braucht eine eigene Funktion, weil er ohne die C-Library auskommen muß. Das Modul kann die Funktion aufrufen, weil es durch das Linken alle public symbols des Kernels benutzen kann.
<1> stellt die Prorität der Meldung dar. Je kleiner die Zahl, desto höher die Priorität. Der Default-Wert führt u.U. dazu, dass die Meldung nicht auf der Konsole angezeigt wird.
Speichert man diesen Code unter mod.c und kompiliert diesen mit gcc -O -DMODULE -D__KERNEL__ -Wall -I/usr/src/linux/include -c mod.c, kann man das Modul laden und entladen. Das sieht ungefähr so aus:
    root#gcc -O -DMODULE -D__KERNEL__ -Wall -I/usr/src/linux/include -c mod.c
    root#insmod mod.o
    init_module called
    root#rmmod mod.o
    cleanup_module called
    root#
Die Ausgabe von printk sieht man nur auf einer Textkonsole, benutzt man z.B. ein xterm, geht die Ausgabe je nach Konfiguration von syslogd in die System-Logfiles wie /var/log/messages.

Damit man diesen Code auch für einen Kerneltreiber benutzen kann, braucht man ein paar Änderungen.
    #include <linux/fs.h>
    #include <linux/version.h>
    #include <linux/module.h>
    #include <linux/init.h>

    static int __init ModInit(void)
    {
      printk(''init_module called\n'');
      return 0;
    }

    static void __exit ModExit(void)
    {
      printk(''cleanup_module called\n'');
    }

    module_init( ModInit );
    module_exit( ModExit );
Die Makros module_init und module_exit sind in linux/init.h definiert. Wird der Code als Modul kompiliert, expandieren die Makros zu init_module. Ansonsten expandieren ModInit und ModExit so, dass sie beim Hoch- bzw. Runterfahren des Kernels aufgerufen werden.
Das Attribut __init sorgt dafür, dass die Initialisierungsfunktion 'weggeworfen' wird und ihr Speicherplatz wieder freigegeben wird, wenn die Initialisierung abgeschlossen ist.
__exit bewirkt, dass die so markierte Funktion ausgelassen wird.
Beide haben in Modulen keine Auswirkungen.

Das Generieren eines Moduls mit Hilfe eines Makefiles könnte so aussehen:
    CFLAGS=-O -DMODULE -D__KERNEL__ -Wall -I/usr/src/linux/include

    all:    mod.o

    mod.o:  mod.c
            $(CC) $(CFLAGS) -c mod.c

    clean:  rm -f *~ mod.o

Noch ein Wort um Kernel Symbol Table. Die Tabelle enthält die Adresse globaler Kernel Items - Funktionen und Variablen -, die man zur Implementation von Treibermodulen braucht. Man kann die Tabelle in /proc/ksyms anschauen. Beim Laden eines Moduls wird jedes exportierte Symbol Teil dieser Tabelle.


Hardwarezugriffe

Hardware kann abhängig vom Prozessor auf zwei Arten integriert werden.

Bei Memory Mapped I/O sollte nur über Funktionen zugegriffen werden, auch wenn bei einigen Plattformen der direkte Zugriff z.B. über einen Pointer möglich ist, und es für den Programmierer keinen Unterschied zwischen Memory Mapped I/O und sonstigem Seicher auf einer PC-Plattform gibt. Dazu gibt es die im Headerfile asm/io.h definierten Makros readb, readw, readl, writeb, writew und writel. Um z.B. ein einzelnes Byte zu lesen, benutzt man readb. Auf einer PC-Plattform wird der Zugriff zu einer normalen Zuweisung expandiert.

Um größere Speicherbereiche zu kopieren bzw. zu initialisieren gibt es ebenfalls Makros: memset_io, memcpy_fromio, memcpy_toio, die letzlich auf memcpy und memset abgebildet werden.

Einige Prozessor-Architekturen, z.B. Intel, stellen für den Portzugriff auf IO-Ports eigene Befehle zur Verfügung. Auf diese Hardware kann nur über Makros zugegriffen werden, die sich in drei Gruppen teilen: Makros für den normalen Zugriff auf IO-Ports (inb, inw, inl, outb, outw, outl), Makros für einen verlangsamten Zugriff, die nach dem Zugriff eine Pause einfügen (inb_p, inw_p, inl_p, outb_p, outw_p, outl_p) und Makros für den wiederholten Zugriff auf IO-Ports (String-Funktionen) (insb, insw, insl, outsb, outsw, outsl).


Ressourcen-Management

Die Ressourcenverwaltung ist eine der wichtigesten Aufgaben des Betriebssystems, um zu vermeiden, dass Ressourcen unerlaubterweise mehrfach benutzt werden.

Ressourcen im Sinne von Gerätetreibern sind z.B. IO-Ports, Interrupts und Speicherbereiche.

Vor einem Zugriff muß überprüft werden, ob die anzusprechende Ressource überhaupt zur Verfügung steht. Erst dann kann sie angefordert werden.

Dazu stehen die folgenden Funktionen zur Verfügung: Code-Beispiel:
    #include <linux/ioport.h>
    #include <linux/errno.h>
    static int my_detect(unsigned int port,unsigned int range){
      int err;
      if ((err=check_region(port,range))<0)return err;/*busy */
      if (my_probe_hw(port,range)!=0)return -ENODEV;/*not found */
      request_region(port,range,"skull");/*"Can ít fail"*/
      return 0;
    }

    static void my_release(unsigned int port,unsigned int range){
      release_region(port,range);
    }
Im Beispiel wird zunächst geprüft, ob die angeforderten Ports verfügbar sind, wenn nicht (negativer return-Code), ist es nicht erforderlich, nach der Hardware zu suchen.
my_probe_hw wird hier nicht angegeben, da diese Funktion geräteabhängig ist. Die Reservierung sollte niemals scheitern, weil der Kernel nur ein Modul zur Zeit lädt. Es ist daher eigentlich unmöglich, dass andere Module zwischenzeitlich die angeforderten Ports belegen. Außerdem existieren diverse Makros wie das bereits erwähnte __init und __exit.


User- und Kernel-Space

Ein Modul läuft im sogenannten Kernel-Space, Applikationen hingegen im User-Space. Der jeweils andere Speicherbereich ist tabu.
Um aus einer Applikation heraus Daten von einem Treiber zu lesen und über den Treiber zu schreiben, werden die Daten zwischen user- und Kernel-Space kopiert. Dazu stehen die Funktionen copy_from_user( unsigned long to, unsigned long from, unsigned long len) und copy_to_user( unsigned long to, unsigned long from, unsigned long len) zur Verfügung. Die Funktionen überprüfen, ob der Zugriff auf die jeweiligen Speicheradressen erlaubt ist. Sie liefern als Resultat die Anzahl der Bytes, die NICHT kopiert wurden. Kommt also ein Wert grösser Null zurück, gab es einen Fehler. Der Treiber sollte dann -EFAULT zurückliefern.


... [ Seminar Linux und Apache] ... [ Thema Gerätetreiber unter Linux 2.4] ... [ nach oben ] ... [ Charactergerätetreiber ] ...