Major- und Minor-Nummern

Auf Zeichen-Geräte wird über Namen (oder Nodes, dt. Knoten) im Dateisystem zugegriffen. Diese Namen nennt man “spezielle Dateien” oder “Gerätedateien” oder einfach “Knoten im Dateisystem-Baum”; normalerweise liegen sie im /dev-Verzeichnis. Gerätedateien sind besondere Dateien, die durch ein “c” in der ersten Spalte der Ausgabe von ls -l kenntlich gemacht werden. Auch Block-Geräte stehen in /dev, werden aber durch ein “b” gekennzeichnet. Auch wenn einige der folgenden Informationen auch auf Block-Geräte zutreffen, werden wir uns hier auf Zeichen-Treiber konzentrieren.

Wenn Sie den Befehl ls -l eingeben, werden Sie bei jeder Gerätedatei zwei durch ein Komma getrennte Zahlen direkt vor dem Modifikationsdatum sehen, wo sonst die Dateilänge steht. Diese Zahlen sind die Major- und Minor-Nummern des jeweiligen Gerätes. Das folgende Listing zeigt einige wenige Geräte, wie sie auf unserem System aussehen. Die Major-Nummern sind 1, 4, 7 und 10, die Minor-Nummern 1, 3, 5, 64, 65 und 129.


 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
 crw------- 1 root   sys     7, 1   Feb 23 1999  vcs1
 crw------- 1 root   sys     7, 129 Feb 23 1999  vcsa1
 crw-rw-rw- 1 root   root    1, 5   Feb 23 1999  zero

Die Major-Nummer gibt an, welcher Treiber zu diesem Gerät gehört. Beispielsweise werden sowohl /dev/null als auch /dev/zero vom Treiber 1 verwaltet, während alle virtuellen Konsolen und seriellen Terminals vom Treiber 4 verwaltet werden; gleichermaßen ist der Treiber 7 für die Geräte vcs1 und vcsa1 zuständig. Der Kernel verwendet die Major-Nummer, um einem Gerät den passenden Treiber zuzuordnen.

Die Minor-Nummer wird nur vom durch die Major-Nummer angegebenen Gerätetreiber benutzt; andere Teile des Kernels betrachten sie nicht und geben sie lediglich an den Treiber weiter. Es ist nicht ungewöhnlich, daß ein Treiber mehrere Geräte kontrolliert (wie im obigen Beispiel) — die Minor-Nummer ist eine Möglichkeit für den Treiber, zwischen den Geräten zu unterscheiden.

In der Version 2.4 des Kernels kam ein neues (optionales) Feature hinzu, das Device Filesystem (Geräte-Dateisystem, devfs). Wenn dieses Dateisystem verwendet wird, dann ist die Verwaltung der Gerätedateien einfacher und deutlich anders. Auf der anderen Seite bringt das neue Dateisystem auch mehrere für den Benutzer sichtbare Inkompatibilitäten mit sich und ist von den Distributoren derzeit noch nicht als Default eingesetzt worden. Die obenstehende Beschreibung und die folgenden Anweisungen über das Hinzufügen eines neuen Treibers und einer speziellen Datei gehen davon aus, daß devfs nicht vorhanden ist. Wir füllen diese Lücke später in diesem Kapitel, im Abschnitt "the Section called Das Device-Dateisystem>".

Wenn devfs nicht verwendet wird, muß einem neuen Treiber eine Major-Nummer zugewiesen werden, um ihn zum System hinzuzufügen. Diese Zuweisung sollte während der Initialisierung des Treibers (bzw. des Moduls) vorgenommen werden, indem die folgende Funktion aufgerufen wird, die in <linux/fs.h> definiert ist:


int register_chrdev(unsigned int major, const char *name,
                    struct file_operations *fops);

Der Rückgabewert ist ein Fehlercode. Ein negativer Rückgabewert zeigt einen Fehler an; eine Null oder ein positiver Rückgabewert bedeuten eine erfolgreiche Ausführung. Das Argument major ist die angeforderte Major-Nummer, name ist der Name des Gerätes, wie er in /proc/devices erscheinen soll, und fops ist ein Zeiger auf ein Array aus Funktionszeigern, über die die Funktionen des Treibers aufgerufen werden (siehe the Section called Datei-Operationen weiter unten in diesem Kapitel).

Die Major-Nummer ist eine kleine Integer-Zahl, die als Index in ein statisches Array von Zeichen-Treibern verwendet wird. > weiter hinten in diesem Kapitel beschreibt, wie man eine Major-Nummer auswählt. Die 2.0-Kernel unterstützten 128 Geräte; in 2.2 und 2.4 wurde dieser Wert auf 256 erweitert (wobei die Werte 0 und 255 für zukünftige Einsatzzwecke vorgesehen sind). Auch Minor-Nummern bestehen aus acht Bits, sie werden aber nicht an register_chrdev übergeben, weil sie ja, wie bereits gesagt, nur vom Treiber verwendet werden. Es gibt einen gewaltigen Druck von seiten der Entwicklergemeinde, die Anzahl der im Kernel möglichen Geräte heraufzusetzen; die Unterstützung von Gerätenummern mit wenigstens 16 Bits ist eines der Ziele in der 2.5-Entwicklung.

Wenn der Treiber erst einmal in der Kernel-Tabelle registriert ist, wird jedesmal, wenn eine Operation auf einer Zeichen-Datei durchgeführt wird, deren Major-Nummer mit der des Treibers übereinstimmt, vom Kernel die korrekte Funktion im Treiber aufgerufen. Dazu wird die Adresse aus der file_operations-Sprungtabelle genommen. Aus diesem Grund sollte der an register_chrdev übergebene Zeiger auf eine globale Struktur im Treiber zeigen und nicht auf eine lokale Struktur in der Initialisierungsfunktion des Moduls.

Als nächstes stellt sich die Frage, wie man Programmen den Namen bekanntgibt, unter dem sie auf ihren Treiber zugreifen können. Ein solcher Name muß im /dev-Verzeichnis stehen und mit der Major- und Minor-Nummer des Treibers verknüpft sein.

Der Befehl, um einen Knoten im Dateisystem zu erzeugen, lautet mknod und kann nur vom Superuser ausgeführt werden. Der Befehl erwartet neben dem Namen der zu erzeugende Knoten drei Argumente. Beispielsweise erzeugt der Befehl


mknod /dev/scull0 c 254 0

ein Zeichen-Gerät (c), dessen Major-Nummer 254 und dessen Minor-Nummer 0 ist. Minor-Nummern sollten im Bereich von 0 bis 255 liegen, weil sie aus historischen Gründen manchmal in nur einem Byte abgespeichert werden. Es gibt gute Gründe, den Bereich der verfügbaren Minor-Nummern zu erweitern, aber zur Zeit gilt noch die 8-Bit-Grenze.

Bitte beachten Sie, daß die spezielle Gerätedatei nach ihrer Erzeugung durch mknod wie alle anderen Informationen auf der Festplatte verbleibt, sofern sie nicht ausdrücklich wieder entfernt wird. Sie können das in diesem Beispiel erzeugte Gerät mit rm /dev/scull0 wieder entfernen.

Dynamische Zuweisung von Major-Nummern

Manche Major-Nummern werden den gängigsten Geräten statisch zugewiesen. Eine Liste solcher Geräte finden Sie im Kernel-Quellenbaum in der Datei documentation/devices.txt. Weil viele Nummern schon vergeben sind, kann es schwierig sein, eine eindeutige Nummer für einen neuen Treiber zu wählen, weil es sehr viel mehr spezielle Treiber als Major-Nummern gibt. Sie könnten eine der für “experimentelle oder lokale Verwendung” gekennzeichneten Nummern verwenden,[1] aber wenn Sie mit mehreren “lokalen” Treibern arbeiten oder Ihren Treiber auch anderen zur Verfügung stellen, haben Sie wieder das gleiche Problem.

Glücklicherweise (obwohl das weniger auf Glück als vielmehr auf der Genialität eines Entwicklers beruht) kann man eine dynamisch zugewiesene Major-Nummer anfordern. Wenn das Argument major beim Aufruf von register_chardev auf Null gesetzt ist, wählt die Funktion eine freie Nummer aus und gibt diese zurück. Die Major-Nummer ist immer positiv und kann daher nicht mit Fehlercodes verwechselt werden. Bitte beachten Sie die Unterschiede im Verhalten in den beiden Fällen: Die Funktion gibt die allozierte Major-Nummer zurück, wenn der Aufrufer eine dynamische Nummer angefordert hat, aber 0 (nicht die Major-Nummer), wenn eine vordefinierte Major-Nummer erfolgreich registriert werden konnte.

Bei privaten Treibern raten wir Ihnen dringend, die dynamische Zuweisung von Major-Nummern zu verwenden, anstatt eine willkürliche Nummer aus den gerade freien zu verwenden. Wenn Ihr Treiber dagegen allgemein verwendet werden und in den offiziellen Kernel aufgenommen werden soll, dann sollten Sie sich eine für Sie reservierte Major-Nummer zuweisen lassen.

Der Nachteil der dynamischen Zuweisung besteht darin, daß Sie die Geräteknoten nicht vorher anlegen können, weil nicht garantiert ist, daß Ihr Treiber jedesmal dieselbe Nummer erhält. Sie können also das in Kapitel 11 verwendete Laden-bei-Bedarf nicht verwenden. Bei normalen Treibern ist das aber kaum ein Problem, weil Sie die Nummer aus /proc/devices auslesen können, wenn sie einmal zugewiesen worden ist.

Um einen Treiber mit einer dynamischen Major-Nummer zu laden, muß der Aufruf von insmod daher durch ein einfaches Skript ersetzt werden, das nach dem Aufruf von insmod die Datei /proc/devices ausliest, um dann die spezielle(n) Gerätedatei(en) zu erzeugen.

Eine typische /proc/devices-Datei sieht folgendermaßen aus:


Character devices:
 1 mem
 2 pty
 3 ttyp
 4 ttyS
 6 lp
 7 vcs
 10 misc
 13 input
 14 sound
 21 sg
180 usb

Block devices:
 2 fd
 8 sd
 11 sr
 65 sd
 66 sd

Das Skript, das ein Modul mit dynamisch zugewiesenen Major-Nummern laden soll, kann also ein Hilfsprogramm wie awk verwenden, um die Informationen aus /proc/devices auszulesen und damit die Dateien in /dev zu erzeugen.

Das folgende Skript scull_load ist ein Bestandteil der scull-Distribution. Der Benutzer eines Treibers, der in Form eines Moduls ausgeliefert wird, kann solch ein Skript aus der rc.local-Datei des Systems oder bei Bedarf, also wenn das Modul gebraucht wird, aufrufen. Es gibt auch noch eine dritte Möglichkeit: die Verwendung von kerneld.


 
#!/bin/sh
module="scull"
device="scull"
group="wheel"
mode="664"

# insmod mit allen übergebenen Parametern aufrufen
# dabei den Pfad angeben, weil neuere modutils defaultmäßig nicht in . suchen
/sbin/insmod -f $module $* || exit 1

# alte Nodes entfernen
rm -f /dev/${device}[0-3]

major=`cat /proc/devices | awk "\\$2==\"$module\" {print \\$1}"` /proc/devices`

mknod /dev/${device}0 c $major 0
mknod /dev/${device}1 c $major 1
mknod /dev/${device}2 c $major 2
mknod /dev/${device}3 c $major 3

# passende Gruppe und Zugriffsrechte zuweisen und die Gruppe ändern
# Nicht alle Distributionen enthalten staff; auf manchen muß "wheel" verwendet werden
group="staff"
grep '⁁staff:' /etc/group > /dev/null || group="wheel"

chgrp $group /dev/${device}[0-3]
chmod $mode /dev/${device}[0-3]

Das Skript kann an andere Treiber angepaßt werden, indem man die Variablen anders belegt und die mknod-Zeilen entsprechend modifiziert. Das Skript erzeugt vier Geräte, weil das der Vorgabewert in den scull-Quellen ist.

Die letzten paar Zeilen des Skripts werden Ihnen vielleicht merkwürdig vorkommen: Warum sollte man die Gruppe und die Zugriffsrechte eines Gerätes ändern? Der Grund dafür besteht darin, daß das Skript vom Superuser ausgeführt werden muß und daher neu angelegte Knoten auch diesem gehören. Die Zugriffsrechte sind per Default so, daß nur der Superuser Schreibzugriff, daß aber jeder Lesezugriff hat. Ein Geräteknoten benötigt aber normalerweise andere Zugriffsrechte, weswegen diese verändert werden müssen. Als Default bekommt in unserem Skript eine Gruppe von Benutzern Zugriffsrechte, aber Sie brauchen möglicherweise etwas anderes. Im Abschnitt the Section called Zugriffskontrolle auf Gerätedateien in Kapitel 5 in Kapitel 5 werden wir im Code von sculluid zeigen, wie ein Gerätetreiber selbst Zugriffsrechte auf Geräte implementieren kann. Es steht dann ein Skript namens scull_unload zur Verfügung, um das /dev-Verzeichnis aufzuräumen und das Modul zu entfernen.

Als Alternative zu einem Skriptenpaar zum Laden und Entladen könnten Sie ein init-Skript schreiben, das in das Verzeichnis kommt, das Ihre Distribution für diese Skripten vorsieht.[2] In den scull-Quellen finden Sie ein ziemlich vollständiges und konfigurierbares Beispiel für ein init-Skript namens scull.init. Es versteht die üblichen Argumente (“start”, “stop” und “restart”) und übernimmt die Rolle sowohl von scull_load als auch von scull_unload.

Wenn es Ihnen zu aufwendig ist, immer wieder die /dev-Knoten zu erzeugen und wieder zu löschen, haben wir eine Lösung für Sie: Wenn Sie immer nur einen Treiber laden und entladen, dann können Sie nach dem ersten Erzeugen der Spezialdateien einfach rmmod und insmod verwenden: Dynamische Nummern werden nicht zufällig zugewiesen; Sie können davon ausgehen, wieder die gleiche Nummer zu bekommen, wenn Sie in der Zwischenzeit nichts mit anderen (dynamischen) Modulen machen. In der Entwicklungsphase ist es sicher sinnvoll, längliche Skripte zu vermeiden. Unser Trick funktioniert aber natürlich leider nur solange, wie nicht mehr als ein Treiber beteiligt ist.

Unserer Meinung nach ist es am besten, die dynamische Zuweisung von Major-Nummern als Default vorzusehen, sich aber die Hintertür offenzuhalten, die Major-Nummer zur Lade- oder gar zur Kompilierzeit anzugeben. Die Implementation von scull verwendet eine globale Variable, scull_major, die die gewählte Nummer enthält. Diese Variable wird mit SCULL_MAJOR aus scull.h initialisiert. Der Default-Wert von SCULL_MAJOR ist 0, steht also für dynamische Zuweisung verwenden. Der Benutzer kann diesen Default übernehmen oder eine bestimmte Major-Nummer wählen, indem er entweder das Makro vor dem Kompilieren verändert oder einen Wert für scull_major auf der Kommandozeile angibt. Schließlich kann der Benutzer auch mit dem Skript scull_load auf der Kommandozeile Argumente für insmod übergeben.[3]

Wir verwenden in scull.c den folgenden Code, um eine Major-Nummer zu bekommen:


 
result = register_chrdev(scull_major, "scull", &scull_fops);
if (result < 0) {
    printk(KERN_WARNING "scull: kann Major-Nummer nicht bekommen %d\n",scull_major);
    return result;
}
if (scull_major == 0) scull_major = result; /* dynamisch */

Einen Treiber aus dem System entfernen

Wenn ein Modul aus dem System entfernt wird, sollte die Major-Nummer freigegeben werden. Das geschieht mit der folgenden Funktion, die von cleanup_module aufgerufen wird:


int unregister_chrdev(unsigned int major, const char *name);

Die Argumente sind die freizugebende Major-Nummer und der Name des dazugehörenden Geräts. Der Kernel vergleicht den Namen mit dem für diese Nummer registrierten Namen (sofern vorhanden). Wenn diese nicht übereinstimmen, wird -EINVAL zurückgegeben. Das geschieht auch wenn die Major-Nummer nicht im erlaubten Bereich für Major-Nummern liegt.

Wenn die Ressource nicht in der Aufräumfunktion freigegeben wird, hat das unschöne Nebeneffekte. /proc/devices wird beim nächsten Leseversuch einen Fehler erzeugen, weil einer der Namensstrings noch auf den Speicher des Moduls verweist, der aber längst nicht mehr gültig ist. Diese Art von Fehler wird als Oops bezeichnet, weil der Kernel diese Meldung ausgibt, wenn er versucht, auf eine ungültige Adresse zuzugreifen.[4]

Wenn Sie den Treiber entladen, ohne die Major-Nummer zu deregistrieren, kommen Sie aus dieser Situation schwerlich wieder heraus, denn der Aufruf von strcmp in unregister_chrdev muß den Zeiger (name) auf das ursprüngliche Modul verwenden. Wenn Sie jemals vergessen, eine Major-Nummer zu deregistrieren, müssen Sie sowohl das gleiche Modul als auch eines zum Deregistrieren entladen. Das fehlerhafte Modul bekommt mit etwas Glück die gleiche Adresse, und der String name liegt an der gleichen Stelle, wenn Sie den Code nicht verändert haben. Die sichere Alternative ist natürlich, das System zu rebooten.

Über das Entladen des Moduls hinaus müssen Sie oft die Geräteknoten des entladenen Treibers entfernen. Wenn die Geräteknoten während des Ladens erzeugt wurden, dann kann man ein einfaches Skript schreiben, das diese beim Entladen entfernt. Für unser Beispiel-Gerät macht das das Skript scull_unload; alternativ dazu können Sie auch scull.init stop aufrufen.

Wenn dynamische Gerätedateien nicht aus /dev entfernt werden, kann es unerwartete Fehler geben: Ein überschüssiger /dev/framegrabber-Eintrag auf dem Rechner eines Entwicklers kann einen Monat später zu einem Feueralarm-Gerät gehören, wenn beide Treiber eine dynamische Zuweisung verwenden, um an eine Major-Nummer zu kommen. Wenn beim Öffnen von /dev/framegrabber “No such file or directory” erscheint, dann ist das sicherlich angenehmer als das, was der neue Treiber erzeugen würde.

dev_t und kdev_t

Bisher haben wir über die Major-Nummer gesprochen. Jetzt wird es Zeit, die Minor-Nummern zu untersuchen und zu sehen, wie der Treiber die Minor-Nummer verwendet, um zwischen verschiedenen Geräten zu unterscheiden.

Jedesmal, wenn der Kernel einen Gerätetreiber aufruft, teilt er diesem mit, um welches Gerät es geht. Die Major- und Minor-Nummern werden in einem einzigen Datentyp zusammengefaßt, der vom Treiber verwendet wird, um ein bestimmtes Gerät zu identifizieren. Die kombinierte Gerätenummer (also die aneinandergehängte Major- und Minor-Nummer) steht im Feld i_rdev der inode-Struktur, die wir später kennenlernen werden. Manche Treiberfunktionen bekommen einen Zeiger auf struct inode als erstes Argument übergeben. Wenn Sie diesen Zeiger (wie die meisten Treiber-Autoren) inode nennen, dann kann die Funktion durch Zugriff auf inode->i_rdev die Gerätenummer herausbekommen.

Traditionell wird unter Unix dev_t (device type) deklariert, um die Gerätenummern abzuspeichern. Früher war das ein 16-Bit-Integer-Wert, der in <sys/types.h> definiert war. Heutzutage braucht man manchmal mehr als 256 Minor-Nummern, aber da es Applikationen gibt, die die Interna von dev_t “kennen” und nicht mehr laufen würden, wenn diese Struktur geändert werden würde, kann man das nicht so einfach ändern. Die Grundlagen für größere Gerätenummern sind zwar vorhanden, die Nummern selbst sind aber immer noch 16-Bit-Integer-Zahlen.

Im Linux-Kernel wird jedoch ein neuer Typ, kdev_t, verwendet. Dieser neue Typ ist für alle Kernel-Funktionen eine Blackbox. Benutzerprogramme wissen nicht einmal von der Existenz dieses Typs, und Kernel-Funktionen wissen nicht, was sich dahinter verbirgt. Dadurch, daß kdev_t versteckt bleibt, kann dieser Typ nach Belieben von einer Kernel-Version zur nächsten geändert werden, ohne daß gleich alle Gerätetreiber angepaßt werden müssen.

Die Informationen über kdev_t sind in <linux/kdev_t.h> versteckt. Diese Datei besteht zum größten Teil aus Kommentaren. Wenn Sie sich für die Überlegungen hinter dem Code interessieren, dann lohnt es sich, diese Header-Datei einmal durchzulesen. Sie müssen diese Datei nicht explizit in Ihre Treiber einbinden, weil <linux/fs.h> das schon für Sie macht.

Sie können mit den folgenden Makros und Funktionen auf kdev_t-Variablen operieren:

MAJOR(kdev_t dev);

Holt die Major-Nummer aus einer kdev_t-Struktur.

MINOR(kdev_t dev);

Holt die Minor-Nummer.

MKDEV(int ma, int mi);

Erzeugt aus Major- und Minor-Nummer einen kdev_t.

kdev_t_to_nr(kdev_t dev);

Konvertiert einen kdev_t-Typ in eine Zahl (einen dev_t).

to_kdev_t(int dev);

Konvertiert eine Zahl in einen kdev_t. Beachten Sie, daß dev_t im Kernel-Modus nicht definiert ist, weswegen int verwendet wird.

Solange Ihr Code diese Operationen verwendet, um mit Gerätenummern umzugehen, sollte er auch dann noch funktionieren, wenn sich die internen Datenstrukturen verändern.

Fußnoten

[1]

Major-Nummern in den Bereichen 60 bis 63, 120 bis 127 und 240 bis 254 sind für die lokale und experimentelle Verwendung reserviert; kein richtiges Gerät wird eine dieser Nummern zugewiesen bekommen.

[2]

Wo die init-Skripten liegen, unterscheidet sich deutlich von Distribution zu Distribution; am häufigsten werden /etc/init.d, /etc/rc.d/init.d und /sbin/init.d verwendet. Wenn Ihr Skript schon beim Booten ausgeführt werden soll, müssen Sie außerdem im passenden Runlevel-Verzeichnis (d.h. ../rc3.d) einen Link darauf anlegen.

[3]

Das init-Skript scull.init akzeptiert keine Treiberoptionen auf der Kommandozeile, unterstützt aber eine Konfigurationsdatei, weil es für die automatische Verwendung beim Booten und Herunterfahren gedacht ist.

[4]

Das Wort “Oops” wird von Linux-Freaks sowohl als Substantiv als auch als Verb gebraucht.