Das Device-Dateisystem

Wie bereits am Anfang des Kapitels angedeutet, enthalten die neueren Versionen des Linux-Kernels ein spezielles Dateisystem für Geräte-Einsprungpunkte. Dieses Dateisystem gibt es seit einiger Zeit als unoffiziellen Patch. In den offiziellen Quell-Baum wurde es mit Version 2.3.46 aufgenommen. Eine Rückportierung auf 2.2 steht ebenfalls zur Verfügung, ist aber nicht in den offiziellen 2.2-Kerneln enthalten.

Auch wenn die Verwendung dieses speziellen Dateisystems derzeit noch nicht besonders weit verbreitet ist, bieten die neuen Features doch einige Vorteile für Autoren von Gerätetreibern. Deswegen verwendet unsere Version von scull das devfs, wenn dieses auf dem Zielsystem vorhanden ist. Das Modul verwendet Konfigurationsinformationen des Kernels zur Compile-Zeit, um herauszufinden, ob bestimmte Features eingeschaltet worden sind; in diesem Fall geht es darum festzustellen, ob CONFIG_DEVFS_FS definiert ist oder nicht.

Die wichtigsten Vorteile von devfs sind:

Als Folge daraus ist es nicht notwendig, beim Laden oder Entladen eines Moduls ein Skript auszuführen, das die Gerätedateien erzeugt, weil der Treiber durch die Verwaltung seiner eigenen Gerätedateien autonom ist.

Um Geräte zu erzeugen und zu entfernen, sollte der Treiber die folgenden Funktionen aufrufen:


#include <linux/devfs_fs_kernel.h>

devfs_handle_t devfs_mk_dir (devfs_handle_t dir,
  const char *name, void *info);

devfs_handle_t devfs_register (devfs_handle_t dir,
  const char *name, unsigned int flags,
  unsigned int major, unsigned int minor,
  umode_t mode, void *ops, void *info);

  void devfs_unregister (devfs_handle_t de);

Die devfs-Implementation enthält noch weitere Funktionen zur Verwendung durch Kernel-Code. Diese ermöglichen die Erzeugung symbolischer Links, den Zugriff auf die internen Datenstrukturen, um devfs_handle_t-Elemente aus Inodes zu bekommen, sowie weitere Aufgaben. Wir behandeln diese Funktionen hier nicht, weil sie entweder nicht wichtig oder aber schwer zu verstehen sind. Neugierige Leser finden weitere Informationen in der Header-Datei.

Die diversen Argumente der Funktionen zum Registrieren und Deregistrieren lauten:

dir

Das Eltern-Verzeichnis, in dem die neue Gerätedatei erzeugt werden soll. Die meisten Treiber werden hier NULL angeben, um die Dateien im Verzeichnis /dev zu erzeugen. Um ein eigenes Verzeichnis zu erzeugen, sollte der Treiber devfs_mk_dir verwenden.

name

Der Name des Geräts ohne das führende /dev. Der Name kann Schrägstriche enthalten, wenn das Gerät in einem Unterverzeichnis stehen soll; das Unterverzeichnis wird dann während der Registrierung erzeugt. Alternativ dazu können Sie auch einen gültigen dir-Zeiger auf das gewünschte Unterverzeichnis angeben.

flags

Eine Bitmaske mit devfs-Flags. DEVFS_FL_DEFAULT kann eine gute Wahl sein; DEVFS_FL_AUTO_DEVNUM ist das Flag für die automatische Zuweisung von Major- und Minor-Nummern. Welche Flags es gibt und was sie bedeuten, beschreiben wir später.

major, minor

Die Major- und Minor-Nummern des Geräts. Sie werden nicht verwendet, wenn DEVFS_FL_AUTO_DEVNUM in den Flags angegeben worden ist.

mode

Die Zugriffsrechte auf das neue Gerät.

ops

Ein Zeiger auf die Dateioperationsstruktur des Geräts.

info

Ein Default-Wert für filp->private_data. Das Dateisystem initialisiert den Zeiger mit diesem Wert, wenn das Gerät geöffnet wird. Der an devfs_mk_dir übergebene info-Zeiger wird nicht von devfs verwendet und dient als “Benutzerdaten”-Zeiger.

de

Ein “devfs-Eintrag”, der aus einem früheren Aufruf von devfs_register stammt.

Diese Flags werden verwendet, um bestimmte Eigenschaften der zu erzeugenden Gerätedatei auszuwählen. Sie sind zwar kurz und deutlich in <linux/devfs_fs_kernel.h> dokumentiert, aber es lohnt sich trotzdem, sie hier zu beschreiben.

DEVFS_FL_NONE, DEVFS_FL_DEFAULT

Das zuerst genannte Symbol ist einfach 0 und sollte verwendet werden, um den Code besser lesbar zu machen. Das zweite Makro ist derzeit als DEVFS_FL_NONE definiert, ist aber eine gute Wahl, um aufwärtskompatibel mit zukünftigen Implementationen des Dateisystems zu sein.

DEVFS_FL_AUTO_OWNER

Dieses Flag läßt das Gerät so aussehen, als gehörte es der letzten Benutzer-/Gruppen-ID, die es geöffnet hat; außerdem ist es les- und schreibbar für alle, wenn es derzeit von keinem Prozeß geöffnet ist. Dieses Feature ist für TTY-Gerätedateien nützlich, aber auch für Gerätetreiber interessant, die den gleichzeitigen Zugriff auf ein Gerät verhindern müssen, das nicht gemeinsam genutzt werden kann. Um solche Fragen kümmern wir uns in Kapitel 5>.

DEVFS_FL_SHOW_UNREG, DEVFS_FL_HIDE

Das erste Flag legt fest, daß die Gerätedatei bei der Deregistrierung nicht aus /dev entfernt wird; das zweite bestimmt, daß die Datei nie in /dev erscheint. Diese Flags sind für normale Geräte üblicherweise nicht notwendig.

DEVFS_FL_AUTO_DEVNUM

Alloziert automatisch eine Gerätenummer für dieses Gerät. Die Nummer gehört auch nach der Deregistrierung des devfs-Eintrags zum Gerät. Wenn der Treiber also erneut geladen wird, bevor das System heruntergefahren worden ist, bekommt er das gleiche Paar aus Major- und Minor-Nummer.

DEVFS_FL_NO_PERSISTENCE

Den Eintrag nicht verfolgen, nachdem er entfernt worden ist. Dieses Flag spart nach dem Entfernen des Moduls etwas Systemspeicher, allerdings auf Kosten der Persistenz von Gerätemerkmalen über das Entladen und erneute Laden hinweg. Die persistenten Features sind die Zugriffsrechte, die Eigentümerschaft und die Major- und Minor-Nummern.

Es ist möglich, die zu einem Gerät gehörenden Flags zur Laufzeit abzufragen oder zu verwenden. Dazu dienen die beiden folgenden Funktionen:


int devfs_get_flags (devfs_handle_t de, unsigned int *flags);
int devfs_set_flags (devfs_handle_t de, unsigned int flags);

devfs in der Praxis

Weil devfs zu ernsthaften Inkompatibilitäten hinsichtlich Gerätenamen im User-Space führt, benutzen es nicht alle Systeme. Unabhängig davon, wie dieses neue Feature von den Linux-Benutzern akzeptiert wird, ist es nicht so wahrscheinlich, daß Sie in näherer Zeit reine devfs-Treiber schreiben werden, weswegen Sie auch das “alte” Verfahren mit dem Erzeugen von Dateien und Zugriffsrechten im User-Space und mit der Verwendung von Major- und Minor-Nummern im Kernel-Space unterstützen müssen.

Der Code zur Initialisierung eines Gerätetreibers, der nur funktioniert, wenn devfs installiert ist, ist eine Untermenge des Codes, den Sie für beide Umgebungen brauchen, so daß wir Ihnen hier nur die Dual-Mode-Initialisierung zeigen. Anstatt einen speziellen Treiber zu schreiben, der devfs verwendet, fügen wir einfach devfs-Unterstützung zum scull-Treiber hinzu. Wenn Sie scull in einen Kernel laden, der devfs verwendet, dann müssen Sie insmod direkt aufrufen, anstatt das Skript scull_load zu verwenden.

Wir haben uns entschieden, ein Verzeichnis zu erzeugen, das alle Gerätedateien von scull enthalten soll, weil die Struktur von devfs in hohem Maße hierarchisch ist und es keinen Grund gibt, sich nicht an diese Konvention zu halten. Außerdem können wir so zeigen, wie ein Verzeichnis erzeugt und entfernt wird.

Der folgende Code in scull_init kümmert sich um die Geräteerzeugung und verwendet ein Feld namens handle in der Gerätestruktur, um sich zu merken, welche Geräte bereits registriert worden sind:


 /* Wenn wir devfs haben, /dev/scull erzeugen, wo die Dateien hinkommen */
 scull_devfs_dir = devfs_mk_dir(NULL, "scull", NULL);
 if (!scull_devfs_dir) return -EBUSY; /* Problem */

 for (i=0; i < scull_nr_devs; i++) {
  sprintf(devname, "%i", i);
  devfs_register(scull_devfs_dir, devname,
      DEVFS_FL_AUTO_DEVNUM,
      0, 0, S_IFCHR | S_IRUGO | S_IWUGO,
      &scull_fops,
      scull_devices+i);
 }

Dieser Code wird von den folgenden Zeilen aus scull_cleanup vervollständigt:


 if (scull_devices) {
  for (i=0; i<scull_nr_devs; i++) {
   scull_trim(scull_devices+i);
   /* Die folgende Zeile ist nur für devfs */
   devfs_unregister(scull_devices[i].handle);
  }
  kfree(scull_devices);
 }

 /* auch das ist nur für devfs */
 devfs_unregister(scull_devfs_dir);

Ein Teil der obenstehenden Code-Fragmente wird mit #ifdef CONFIG_DEVFS_FS umgeben. Wenn das Feature im aktuellen Kernel nicht eingeschaltet ist, verwendet scull weiterhin register_chrdev.

Um beide Umgebungen zu unterstützen, ist jetzt nur noch die Initialisierung von filp->f_ops und filp->private_data in der Gerätemethode open erforderlich. Der erste Zeiger wird so gelassen, wie er ist, weil die korrekten Datei-Operationen bereits in devfs_register angegeben worden sind. Der zweite Zeiger muß nur von der open-Methode initialisiert werden, wenn er NULL ist, denn nur dann wird devfs nicht verwendet.


 /*
  * Wenn die privaten Daten nicht gueltig sind, dann verwenden wir kein
  * devfs und setzen also den Typ (aus der Minor-Nummer), um einen neuen
  * Wert für f_op anzugeben.
  */
 if (!filp->private_data && type) {
  if (type > SCULL_MAX_TYPE) return -ENODEV;
  filp->f_op = scull_fop_array[type];
  return filp->f_op->open(inode, filp); /* an open weiterleiten */
 }

 /* Typ 0, Geraetenummer ueberpruefen (wenn private_data nicht gültig ist) */
 dev = (Scull_Dev *)filp->private_data;
 if (!dev) {
  if (num >= scull_nr_devs) return -ENODEV;
  dev = &scull_devices[num];
  filp->private_data = dev; /* für andere Methoden */
 }

Mit dem oben gezeigten Code kann das scull-Modul in einem System, das devfs verwendet, geladen werden. ls -l /dev/scull zeigt dann die folgende Ausgabe an:


crw-rw-rw- 1 root  root  144,  1 Jan 1 1970 0
crw-rw-rw- 1 root  root  144,  2 Jan 1 1970 1
crw-rw-rw- 1 root  root  144,  3 Jan 1 1970 2
crw-rw-rw- 1 root  root  144,  4 Jan 1 1970 3
crw-rw-rw- 1 root  root  144,  5 Jan 1 1970 pipe0
crw-rw-rw- 1 root  root  144,  6 Jan 1 1970 pipe1
crw-rw-rw- 1 root  root  144,  7 Jan 1 1970 pipe2
crw-rw-rw- 1 root  root  144,  8 Jan 1 1970 pipe3
crw-rw-rw- 1 root  root  144, 12 Jan 1 1970 priv
crw-rw-rw- 1 root  root  144,  9 Jan 1 1970 single
crw-rw-rw- 1 root  root  144, 10 Jan 1 1970 user
crw-rw-rw- 1 root  root  144, 11 Jan 1 1970 wuser

Die Funktion der einzelnen Dateien ist die gleiche wie beim “normalen” scull-Modul; der einzige Unterschied besteht in den Pfadnamen der Gerätedateien: Was vorher /dev/scull0 war, ist jetzt /dev/scull/0.

Portabilitätsprobleme mit devfs

Die Quelldateien von scull sind ein wenig komplizierter, weil sie unter Linux 2.0, 2.2 und 2.4 kompilierbar sein und laufen sollen. Diese Portabilitätsanforderungen führen zu mehreren Fällen bedingter Kompilierung auf der Basis von CONFIG_DEVFS_FS.

Glücklicherweise sind sich die meisten Entwickler darüber einig, daß #ifdef-Konstrukte von Übel sind, wenn sie im Rumpf von Funktionsdefinitionen (im Gegensatz zu Header-Dateien) stehen. Daher brauchen wir zusätzliche Mechanismen, wenn wir trotz der Verwendung von devfs #ifdefs in unserem Code vermeiden wollen. Gleichwohl findet sich noch bedingte Kompilation in scull, weil ältere Versionen der Kernel-Header-Dateien keine andere Unterstützung enthalten.

Wenn Ihr Code nur mit der Version 2.4 des Kernels verwendet werden soll, können Sie bedingte Kompilierung durch das Aufrufen von Kernel-Funktionen vermeiden, die den Treiber auf beide Arten initialisieren; eine der beiden Initialisierungen macht dabei gar nichts und meldet nur eine erfolgreiche Ausführung zurück. Hier folgt ein Beispiel, wie eine solche Initialisierung aussehen kann:


 #include <devfs_fs_kernel.h>

 int init_module()
 {
  /* Major-Nummer anfordern, macht nichts, wenn devfs verwendet wird */
  result = devfs_register_chrdev(major, "name", &fops);
  if (result < 0) return result;

  /* mit devfs registrieren, macht nichts, wenn devfs nicht verwendet wird */
  devfs_register(NULL, "name", /* .... */ );
  return 0;
 }

Sie können ähnliche Tricks in Ihren eigenen Header-Dateien verwenden, solange Sie dabei aufpassen, keine Funktionen umzudefinieren, die schon von den Kernel-Header-Dateien verwendet werden. Das Beseitigen von bedingter Kompilierung ist eine gute Sache, weil die Lesbarkeit des Codes verbessert und die Wahrscheinlichkeit von Fehlern verringert wird, denn der Compiler parst die gesamte Eingabedatei. Immer, wenn eine/die bedingte Kompilierung verwendet wird, besteht die Gefahr, daß sich Tipp- oder andere Fehler einschleichen, die unentdeckt bleiben, weil sie an einer Stelle stehen, die vom C-Präprozessor aufgrund eines #ifdef verworfen wird.

Hier zeigen wir als Beispiel, wie scull.h die bedingte Kompilierung im Aufräum-Teil des Programms vermeidet. Dieser Code ist über alle Kernel-Versionen hinweg portabel, weil er sich nicht darauf verläßt, daß devfs in den Header-Dateien bekannt ist:


#ifdef CONFIG_DEVFS_FS /* nur, wenn verwendet, um Fehler in 2.0 zu vermeiden */
#include <linux/devfs_fs_kernel.h>
#else
 typedef void * devfs_handle_t; /* #ifdef in der Struktur vermeiden */
#endif

> > > > In sysdep.h wird nichts definiert, weil es sehr schwer ist, solche Hacks generisch genug zu implementieren, um allgemein verwendbar zu sein. Jeder Treiber sollte seine eigenen Bedürfnisse abdecken, um eine exzessive Verwendung von #ifdef im Funktionscode zu vermeiden. Wir haben uns außerdem entschlossen, im Beispiel-Code zu diesem Buch (mit der Ausnahme von scull), devfs nicht zu unterstützen. Wir hoffen, daß dieser Abschnitt hier ausreichend ist, um Ihnen das weitere Entdecken von devfs zu ermöglichen, wenn Sie das wünschen. Wir haben die devfs-Unterstützung aus den weiteren Beispielen herausgelassen, um den Code einfach zu halten.