open und release

Nachdem wir uns jetzt die Felder kurz angeschaut haben, benutzen wir sie in echten scull-Funktionen.

Die Methode open

Die Methode open wird von Treibern bereitgestellt, um mit Initialisierungen spätere Operationen vorzubereiten. Außerdem erhöht open normalerweise den Verwendungszähler des Gerätes, damit das Modul nicht entladen wird, bevor die Datei wieder geschlossen wird. Der Zähler, der in Abschnitt in Kapitel 2 beschrieben wird, wird dann von der Methode release wieder heruntergezählt.

In den meisten Treibern macht open folgendes:

ark=dash>

In scull hängen die meisten der obengenannten Aufgaben von der Minor-Nummer des geöffneten Gerätes ab. Daher muß als erstes das Gerät, um das es geht, identifiziert werden. Dazu sehen wir uns inode->i_rdev an.

Wir haben bereits darüber gesprochen, daß der Kernel die Minor-Nummer des Gerätes selbst nicht verwendet, so daß der Treiber diese nach Belieben verwenden kann. In der Praxis werden verschiedene Minor-Nummern verwendet, um auf verschiedene Geräte zuzugreifen oder um ein und dasselbe Gerät auf verschiedene Arten zu öffnen. Beispielsweise verweisen /dev/st0 (Minor-Nummer 0) und /dev/st1 (Minor-Nummer 1) auf verschiedene SCSI-Bandlaufwerke, aber /dev/nst0 (Minor-Nummer 128) ist das gleiche physikalische Gerät wie /dev/st0, verhält sich aber anders (es spult das Band nicht zurück, wenn das Gerät geschlossen wird). Alle Gerätedateien für Bandlaufwerke haben unterschiedliche Minor-Nummern, damit der Treiber sie auseinanderhalten kann.

Ein Treiber kennt selbst nie den Namen des Gerätes, das gerade geöffnet wird, nur die Gerätenummer. Benutzer können sich das zunutze machen, indem sie zur Bequemlichkeit Aliasnamen für Geräte anlegen. Wenn Sie zwei Gerätedateien mit dem gleichen Paar aus Minor- und Major-Nummer anlegen, dann sind das die gleichen Geräte, die Dateien können nicht unterschieden werden. Den gleichen Effekt erreichen Sie mit einem symbolischen oder harten Link; das ist auch die empfohlene Vorgehensweise.

Der scull-Treiber verwendet Minor-Nummern folgendermaßen: Das höherwertige Nibble (vier Bits) identifiziert den Typ (die Personality) des Gerätes, und das niederwertige Nibble dient zur Unterscheidung zwischen den einzelnen Geräten, wenn der Typ mehr als eine Geräteinstanz unterstützt (scull0-3 und scullpipe0-3). scull0 unterscheidet sich also von scullpipe0 im höherwertigen Nibble, scull0 und scull1 im niederwertigen Nibble.[1] Zwei Makros (TYPE und NUM) werden definiert, um die Bits aus der Gerätenummer zu gewinnen:


#define TYPE(dev) (MINOR(dev) >> 4) /* oberes Nibble */
#define NUM(dev) (MINOR(dev) & 0xf) /* unteres Nibble */

Zu jedem Gerätetyp definiert scull eine spezifische Struktur file_operations, die beim Öffnen in filp->f_op eingesetzt wird. Der folgende Code zeigt, wie das Aufteilen in Bits und mehrfache fops implementiert werden:


struct file_operations *scull_fop_array[]={
 &scull_fops,  /* type 0 */
 &scull_priv_fops, /* type 1 */
 &scull_pipe_fops, /* type 2 */
 &scull_sngl_fops, /* type 3 */
 &scull_user_fops, /* type 4 */
 &scull_wusr_fops /* type 5 */
};
#define SCULL_MAX_TYPE 5

/* In scull_open wird fop_array entsprechend TYPE(dev) verwendet */
 int type = TYPE(inode->i_rdev);

  if (type > SCULL_MAX_TYPE) return -ENODEV;
  filp->f_op = scull_fop_array[type];

Der Kernel ruft open entsprechend der Major-Nummer auf, und scull verwendet dann die Minor-Nummer in den oben definierten Makros. TYPE wird als Index in das Array scull_fop_array verwendet, um den richtigen Satz von Methoden für den gerade geöffneten Gerätetyp zu bekommen.

In scull haben wir die korrekte file_operations-Struktur entsprechend des Typs der Minor-Nummer an filp->f_op zugewiesen. Dann wird die in den neuen fops definierte open-Methode aufgerufen. Normalerweise ruft ein Treiber seine eigenen fops nicht auf, diese werden vom Kernel verwendet, um die richtige Treiber-Methode aufzurufen. Aber wenn die open-Methode mit verschiedenen Gerätetypen umgehen können muß, dann kann es doch sinnvoll sein, fops->open aufzurufen, nachdem der fops-Zeiger entsprechend der Minor-Nummer des geöffneten Gerätes modifiziert wurde.

Der eigentliche Code von scull_open steht hier. Er verwendet die oben definierten Makros TYPE und NUM, um die Minor-Nummer aufzuteilen:


int scull_open(struct inode *inode, struct file *filp)
{
 Scull_Dev *dev; /* Geraeteinformation */
 int num = NUM(inode->i_rdev);
 int type = TYPE(inode->i_rdev);

 /*
  * Wenn private_data nicht gueltig ist, dann verwenden wir kein devfs
  * und benutzen deswegen den Typ (aus der Minor-Nr.), um ein f_op
  * auszuwaehlen
  */
 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 spezifisches open
                                               * weiterleiten */
 }

 /* Typ 0, Geraetenummer ueberpruefen (sofern private_data nicht gueltig ist) */
 dev = (Scull_Dev *)filp->private_data;
 if (!dev) {
  if (num >= scull_nr_devs) return -ENODEV;
  dev = &scull_devices[num];
  filp->private_data = dev; /* for other methods */
 }

 MOD_INC_USE_COUNT; /* Moeglicherweise vor dem Schlafen */
 /* jetzt die Laenge des Geraets auf 0 setzen, wenn es nur zum Schreiben
    geöffnet wurde */
 if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) {
  if (down_interruptible(&dev->sem)) {
   MOD_DEC_USE_COUNT;
   return -ERESTARTSYS;
  }
  scull_trim(dev); /* Fehler ignorieren */
  up(&dev->sem);
 }

 return 0;   /* Erfolg */
}

Hier sind wahrscheinlich noch einige Erklärungen angebracht. Die Datenstruktur, in der der Speicherbereich untergebracht wird, heißt Scull_dev und wird gleich erläutert. Die globalen Variablen scull_nr_devs und scull_devices[] (alle in Kleinbuchstaben) geben die Anzahl der verfügbaren Geräte und das eigentliche Array von Zeigern in Scull_dev an.

Die Aufrufe von down_interruptible und up können Sie im Moment ignorieren, wir kommen darauf in Kürze zurück.

Der Code sieht ziemlich dürftig aus, weil er beim open nicht auf bestimmte Geräte eingeht. Das muß auch nicht sein, weil die Geräte scull0-3 vom Design her global und persistent sind. Insbesondere gibt es kein “Initialisieren des Gerätes beim ersten Öffnen”, weil wir keinen Zähler der geöffneten sculls mitführen, nur den Verwendungszähler des Moduls.

In Anbetracht der Tatsache, daß der Kernel den Verwendungszähler des Moduls über das owner-Feld in der Struktur file_operations verwalten kann, wundern Sie sich vielleicht, warum wir das hier manuell machen. Das liegt daran, daß Module in älteren Kerneln die gesamte Verwaltungsarbeit selbst machen mußten; es gab den owner-Mechanismus damals noch nicht. Um zu älteren Kerneln portabel zu sein, implementiert scull seinen eigenen Verwendungszähler. Dieses Verhalten führt dazu, daß der Verwendungszähler auf 2.4-Systemen zu groß ist, was aber kein Problem ist, denn er fällt gleichwohl auf Null zurück, wenn das Modul nicht verwendet wird.

Die einzige reale Operation, die hier auf dem Gerät vorgenommen wird, ist das Verkürzen auf eine Länge von Null, wenn das Gerät zum Schreiben geöffnet wird. Das Überschreiben des Gerätes mit einer kürzeren Datei führt zu einem kleineren Gerätedatenbereich, ähnlich wie das Öffnen einer normalen Datei zum Schreiben die Datei auf Null verkürzt. Die Operation tut nichts, wenn das Gerät zum Lesen geöffnet ist.

Später, wenn wir uns die anderen Personalities von scull anschauen, werden wir noch sehen, wie richtige Initialisierungen funktionieren.

Die Methode release

Die Aufgabe der Methode release ist das Gegenstück zu open. Manchmal wird die Methodenimplementation auch device_close anstelle von device_release genannt. In jedem Falle sollte sie aber folgende Aufgaben erfüllen:

ark=dash>

Die grundlegende Form von scull muß das Gerät nicht herunterfahren, so daß der benötigte Code minimal ist:[2]


 
int scull_release (struct inode *inode, struct file *filp)
{
    MOD_DEC_USE_COUNT;
    return 0;
}

Das Dekrementieren des Verwendungszählers ist wichtig, weil der Kernel das Modul nicht entladen kann, solange der Zähler nicht auf Null zurückgeht.

Wie kann der Zähler konsistent bleiben, wenn eine Datei manchmal geschlossen wird, ohne vorher geöffnet worden zu sein? Wir wissen alle, daß dup und fork aus einer geöffneten Datei zwei machen, ohne open aufzurufen, aber jede dieser Dateien wird beim Beenden des Programms wieder geschlossen. Beispielsweise öffnen die meisten Programme ihre stdin-Datei (oder ihr stdin-Gerät) nicht, aber alle schließen sie letztendlich.

Die Antwort ist einfach: Nicht jeder close-Systemaufruf verursacht einen Aufruf der Methode release. Nur diejenigen, die wirklich die Gerätedatenstruktur freigeben, rufen auch die Methode auf — daher auch der Name. Der Kernel verwaltet einen Zähler, der registriert, wie oft eine file-Struktur verwendet wird. Weder fork noch dup erzeugen eine neue file-Struktur (das tut nur open); sie inkrementieren nur den Zähler in der existierenden Struktur.

Der Systemaufruf close führt die Methode release nur dann aus, wenn der Zähler der Struktur auf Null fällt, was genau dann passiert, wenn die Struktur zerstört wird. Dieser Zusammenhang zwischen der Methode release und dem Systemaufruf close garantiert, daß der Verwendungszähler von Modulen immer konsistent ist.

Beachten Sie, daß die Methode flush immer dann aufgerufen wird, wenn eine Applikation close aufruft. Nur wenige Treiber implementieren flush aber, weil es normalerweise nichts beim Schließen zu tun gibt, solange release nicht im Spiel ist.

Wie Sie sich vielleicht vorstellen können, gilt all das hier Gesagte auch, wenn die Applikation beendet wird, ohne explizit ihre offenen Dateien zu schließen: Der Kernel schließt automatisch alle offenen Dateien beim Beenden des Prozesses, indem er den Systemaufruf close intern aufruft.

Fußnoten

[1]

Die Aufteilung in verschiedene Bits ist eine typische Anwendungsweise für Minor-Nummern. Der IDE-Treiber verwendet beispielsweise die oberen beiden Bits für die Plattennummer und die unteren sechs Bits für die Partitionsnummer.

[2]

Die anderen Arten des Gerätes werden von anderen Funktionen geschlossen, weil scull_open andere Werte in filp->f_op eingesetzt hat. Diese Funktionen werden wir später sehen.