Charactergerätetreiber


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

Übersicht: Charactergerätetreiber


Definition

Ein Charactergerätetreiber wird über einen Bytestrom angesprochen, wie eine Datei. Es wird zeichenweise gelesen. Bereits gelesene Zeichen können normalerweise nicht noch einmal gelesen werden.
Ein zugehöriger Treiber muß dieses Verhalten ermöglichen; in der Regel implementiert er mindestens die Systemaufrufe open, close, read und write.
Beispiel: Tastatur, Textkonsole (/dev/console) und die seriellen Ports (/dev/ttyS0 usw.)
Character devices werden über Knoten im Dateisystem angesprochen, wie /dev/tty1 and /dev/lp0. Der Unterschied zu regulären Dateien ist der, dass man in einer solchen Datei vorwärts und rückwärts springen kann. Character devices hingegen kann man meist nur sequentiell durchlaufen. Ausnahmebeispiel sind char devices, die wie Datenfelder aussehen, in denen man sich bewegen kann, wie framegrabbers, bei denen die Applikation auf das ganze Image mit mmap oder lseek zugreifen kann.


Major & Minor Numbers

Charactergeräte werden im Dateisystem über Namen angesprochen. Diese Namen werden special files oder device files genannt und befinden sich in /dev. Beim Aufruf von ls -l sind sie in der Ausgabe durch ein 'c' in der ersten Spalte gekennzeichnet, Blockgerätetreiber analog durch 'b'. Vor dem Datum der letzten Änderung befinden sich zwei durch Komma getrennte Zahlen, die major und die minor device number.
    crw-rw-rw-1 root root 1,3 Feb 23 1999 null
    crw-------1 root root 10,1 Feb 23 1999 psaux
    crw-------1 rubini tty 4,1 Aug 16 22:22 tty1
    crw-rw-rw-1 root dialout 4,64 Jun 30 11:19 ttyS0
    crw-rw-rw-1 root dialout 4,65 Aug 16 00:00 ttyS1
Die major number identifiziert den Treiber, die minor number wird vom Treiber genutzt, um die Geräte aueinanderzuhalten. Wird das DevFS verwendet, gilt das nicht.
Die Major-Nummer ist ein Integerwert zwischen 1 und 254 (0 und 255 sind reserviert), der als Index in einem statischen Array von Charactertreibern dient. Minor-Nummern sollten ebenfalls zwischen 0 und 255 liegen, da sie manchmal in einem Byte gespeichert werden.

Ohne DevFS muß einem neuen Treiber eine major number zugewiesen werden. Das sollte bei der Initialisierung durch Aufruf der in <linux/fs.h> definierten Funktion
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
geschehen.
Ist der Rückgabewert größer oder gleich 0, war die Aktion erfolgreich, bei negativem Wert gab es einen Fehler.

Die Major-Nummer sollte dynamisch zugeteilt werden und nicht fest codiert. Zunächst sind einige Nummern fest zugeteilt (s. Documentation/devices.txt), zum anderen kann ein Treiber nicht wissen, welche Nummern belegt sind.
Die dynamische Zuteilung erfolgt, wenn beim obigen Funktionsaufruf das Argument major auf 0 gesetzt ist. Der (positive) Rückgabewert ist die zugeteilte Nummer, wird 0 zurückgegeben, wurde ein Gerät mit einer festen Major-Nummer registriert!
Auf der User-Ebene wird nach dem Laden des Treibers ein Programm (Skript) gestartet, welches über den dem Skript bekannten Treibernamen aus /proc/devices die vom System vergebene Major-Nummer ausliest und ein Gerätefile mit dieser Major-Nummer anlegt. In eingebetteten Systemen läßt man oftmals, um Speicherplatz zu sparen, das /proc-Device weg. In diesem Fall gibt man die Major-Nummer einfach fest vor.
Ein Skript könnte folgendermassen aussehen:
    #!/bin/sh
    module="my_test"
    device="my_test"
    mode="664"
    #invoke insmod with all arguments we were passed
    #and use a pathname,as newer modutils don t look in .by default
    /sbin/insmod -f ./$module.o $*||exit 1
    #remove stale nodes
    rm -f /dev/${device}[0 ]
    major=awk "\\$2==\"$module \"{print \\$1}"/proc/devices 
    mknod /dev/${device}0 c $major 0
    #give appropriate group/permissions,and change the group.
    #Not all distributions have staff;some have "wheel"instead.
    group="staff"
    grep staff: //etc/group >/dev/null ||group="wheel"
    chgrp $group /dev/${device}[0 ]
    chmod $mode /dev/${device}[0 ]
    
mknod erzeugt den Knoten im Dateisystem.
Beim Entladen des Moduls muß die Major-Nummer freigegeben werden. Das sollte in der cleanup-Funktion passieren.
int unregister_chrdev(unsigned int major, const char *name);
erledigt die Arbeit.
Ein Treiber darf natürlich nur entladen werden, wenn keine Applikation mehr darauf zugreift. Alle open müssen durch ein close abgeschlossen worden sein. Dazu wird die Anzahl der open-Aufrufe gezählt. Linux benutzt dazu die Makros MOD_INC_USE_COUNT bzw. MOD_DEC_USE_COUNT, die durch open bzw. release aktualisiert werden müssen. Bei der Entwicklung des Treibers kann es manchmal Sinn machen, diese Makros auszukommentieren, da sich sonst bei einem Fehler im Treiber das Modul nicht mehr entladen läßt und das System neu gebootet werden muß.

Gemeinsam mit der Major-Nummer steht die Minor-Nummer im Feld i_rdev der Struktur inode. Einige Treiberfunktionen erhalten als erstes Argument einen Zeiger auf diese Struktur, so dass über inode->i_rdev auf die Gerätenummern zugegriffen werden kann.
Die Gerätenummern werden in Linux im Datentyp kdev_t gespeichert, der für Kernelfunktionen eine black box ist. Folgende Makros und Funktionen gibt es für den Zugriff:
MAJOR(kdev_t dev);: liefert die Major-Nummer
MINOR(kdev_t dev);: liefert die Minor-Nummer
MKDEV(int ma,int mi);: erzeugt ein neues kdev_t mit Major- und Minornummer
kdev_t_to_nr(kdev_t dev);: Konvertierung in eine Zahl
to_kdev_t(int dev);: Konvertierung einer Zahl in ein kdev_t


Treiberfunktionen

Auf die Treiberfunktionen greift der Kernel über die Struktur file_operations zu.
Daneben gibt es noch einige weitere Funktionen, um die wir uns aber nicht kümmern wollen.

Die Namen für die einzelnen Funktionen kann man beliebig festlegen; wird z.B. ein read durchgeführt, wird vom Betriebssystem die Funktion aufgerufen, die bei der Initialisierung von file_operations an zweiter Stelle aufgeführt wurde. Nicht implementierte Funktionen werden durch NULL gekennzeichnet. Alternativ kann auch das sogenannte tagged format bei der Deklaration der Struktur verwendet werden:
    struct file_operations my_fops ={
      llseek:my_llseek,
      read:my_read,
      write:my_write,
      ioctl:my_ioctl,
      open:my_open,
      release:my_release,
    };
Dadurch wird der Code lesbarer und er ist weniger anfällig gegenüber Änderungen in der Strukturdefinition.
Außerdem muß noch das Feld owner in der Struktur gesetzt werden. Häufig wird es mit dem Rest der Struktur initialisiert. Im tagged format sieht da so aus:
owner:THIS_MODULE.
Da das nur unter Kernel 2.4 funktioniert, kann man es portabler auch über ein Makro machen, das in <linux/module.h> definiert ist:
SET_MODULE_OWNER(&my_fops);

In den Funktionen wird häufig auf file zugegriffen. Dabei handelt es sich um eine Struktur, die (ganz allgemein, nicht nur treiberbezogen) offene Dateien repräsentiert. Sie ist nicht zu verwechseln mit FILE aus der C-Bibliothek oder einer disk file, die durch inode dargestellt wird.
Die Struktur wird bei open durch den Kernel erzeugt und bis zum letzten close an jede Funktion weitergegeben, die auf der Datei operiert. Wenn alle Instanzen der Datei geschlossen sind, wird die Struktur vom Kernel freigegeben.
Diese Struktur enthält dateispezifische Informationen, wie z.B. den Zugriffsmodus oder die zugeordneten Dateioperationen aus file_operations.

Der folgende Programmcode zeigt den einfachsten Linux-Treiber [3]:
    /* vim: set ts=4: */
    /* Minitreiber */
    #include <linux/fs.h>
    #include <linux/version.h>
    #include <linux/module.h>
    #include <linux/init.h>

    #define MINI_MAJOR 240

    static char *Version =
           ''$Id: treiber3.htm,v 1.1 2001/11/15 16:41:34 si Exp $'';

    static struct file_operations MiniFops = {
      #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,0)
          THIS_MODULE,
      #endif
      NULL,  /* llseek */
      NULL,  /* read */
      NULL,  /* write */
      NULL,  /* readdir */
      NULL,  /* poll */
      NULL,  /* ioctl */
      NULL,  /* mmap */
      NULL,  /* open */
      NULL,  /* flush */
      NULL,  /* release */
      NULL,  /* fsync */
      NULL,  /* fasync */
      NULL,  /* lock */
    };

    static int __init MiniInit(void)
    {
      if(register_chrdev(MINI_MAJOR, "Mini-Driver", &MiniFops) == 0) {
        printk("%s\n", Version );
        return 0;
      };
      printk("mini: unable to get major %d\n",MINI_MAJOR);
      return( -EIO );
    }

    static void __exit MiniExit(void)
    {
      printk("cleanup_module called\n");
      unregister_chrdev(MINI_MAJOR,"Mini-Driver");
    }

    module_init( MiniInit );
    module_exit( MiniExit );



Debugging

Die einfachste Methode des Debugging ist die Benutzung von printk, um Nachrichten auf die laufende Konsole auszugeben. Als Parameter kann man diverse Makros übergeben, um die Nachrichten zu klassifizieren, für Debugging z.B. KERN_DEBUG.

Das Proc-Device bietet eine elegante Möglichkeit, Applikationen treiberinterne Informationen zur Verfügung zu stellen. Im Kernel 2.4 wurde es leicht modifiziert.
Es verwendet jetzt die Funktionen create_proc_entry und remove_proc_entry. Erstere erzeugt eine Struktur vom Type proc_dir_entry, die durch die zweite Funktion wieder freigegeben wird.
Beim Lesen des Proc-Devices ruft der Kernel die Funktion read_proc vom Typ read_proc_t auf, wird darauf geschrieben, wird write_proc vom Typ write_proc_t aufgerufen.
Um ein Proc-Device zur Verfügung zu stellen, muß create_proc_entry in init_module aufgerufen werden, in cleanup_module analog remove_proc_entry. Der Treiber muß außerdem eine Lesefunktoin read_proc zur Verfügung stellen und im Struct proc_dir_entry mit create_proc_read_entry eintragen.

int read_proc(char *buffer, char **start, off_t offset, int len, int *eof, void *private);

Die Lesefunktion bekommt den Puffer buffer übergeben, der eine Speicherseitengrösse len (mind. 4096 Byte) hat. In diesen Puffer werden die über das Proc-Device auszugebenden Informationen geschrieben. Ist vom Kernel ein Offset angegeben, ab dem er erst die Informationen erwartet, kann read_proc entweder die Daten erst ab dem Offset an den Anfang des Buffers kopieren, oder aber alle Daten schreiben und dem Kernel den Beginn der eigentlichen Daten über den Parameter start mitteilen. Wenn alle Daten erfolgreich geschrieben wurden, setzt der Treiber eof. private wird bei der Initialisierung angegeben, wenn die Lesefunktion mehrfach verwendet werden kann. der Rückgabewert ist die Anzahl der geschriebenen Bytes.

Zum Debuggen kann man außerdem ioctl-Kommandos benutzen. Manchmal hilft auch schon die einfache Beobachtung des Applikationsverhaltens.
Debuggingtools sind auch anwendbar, aber zeitaufwendig.


Änderungen im neuen Kernel

Die größte Verbesserung ist bei der Unterstützung von Tastaturen und Mäusen zu finden. Linux unterstützte bisher serielle und PS/2, jetzt kommt USB dazu. Außerdem unterstützt Linux auf einigen Systemen Tastaturen, die nicht vom BIOS initialisiert werden.
Es gibt einige neue Treiber für Multi-Port-Karten. Die Nutzung der seriellen Ports soll dadurch einfacher werden.

Seit Linux 2.2 wird an Support für sogenannte WinModems( oder soft Modems) gearbeitet. Damit werden Modems bezeichnet, die zum größten Teil aus Software bestehen und die häufig nur Treiber für Windows besitzen. Die ersten Erfolge sind jetzt benutzbar, auch wenn es noch dauern wird, bis die Unterstützung der meisten Geräte läuft.

Das Parallelport-Subsystem wurde in grossen Teilen überarbeitet. Die sogenannten generischen parallelen Geräte werden jetzt unterstützt. Programme, die auf die Parallelports auf ungewöhnliche Weise zugreifen, oder die den Port nur auf PnP-Informationen prüfen, können diese Funktionalität benutzen.
Durch die Überarbeitung können Linux-User alle enhanced modes der Parallelports nutzen, inklusive UDMA (für schnelleres I/O), sofern die Hardware das kann.
Der neue Kernel macht es auch möglich, alle Nachrichten von der Konsole auf ein Parallelportgerät wie einen Drucker umzuleiten.

Die Unterstützung von Soundkarten, TV- und Radio-Tunern und ähnlichen Geräten hat keinen so grossen Sprung gemacht wie unter Linux 2.2. Es gibt Updates und einige neue Treiber für Sound- und Videokarten, inklusive full duplex.
Das Sound-Subsystem wird derzeit überarbeitet und kommt eventuell mit Kernel 2.6.


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