Abwärtskompatibilität

Wir haben in diesem Kapitel bisher die Programmierschnittstelle der Version 2.4 des Linux-Kernels beschrieben. Leider hat sich diese Schnittstelle im Laufe der Kernel-Entwicklung deutlich verändert. Diese Änderungen sind für sich genommen alle Verbesserungen, bringen aber wieder einmal Schwierigkeiten für diejenigen mit, die Treiber schreiben wollen, die mit verschiedenen Kernel-Versionen kompatibel sind.

Was dieses Kapitel angeht, so gibt es nur wenige sichtbare Unterschiede zwischen den Versionen 2.4 und 2.2. In Version 2.2 wurden allerdings viele der Prototypen der file_operations-Methoden im Vergleich zur Version 2.0 geändert; außerdem wurde der Zugriff auf den User-Space deutlich verändert (und vereinfacht). Der Semaphor-Mechanismus war in Linux 2.0 noch nicht so weit entwickelt. Und schließlich wurde in der 2.1-Entwicklungsserie der Verzeichniseintrag-Cache (dentry cache) eingeführt.

Änderungen in der Struktur für die Dateioperationen

Eine Reihe von Faktoren haben die Änderungen in den file_operations-Methoden vorangetrieben. Die seit langem bestehende Grenze von 2 GByte für die Dateigröße brachte selbst zu Zeiten von Linux 2.0 Probleme mit sich. Daher wurde ab der 2.1-Serie der Typ loff_t, ein 64-Bit-Wert, verwendet, um Dateipositionen und Längen anzugeben. Die Unterstützung großer Dateien war vor der Version 2.4 noch nicht vollständig in den Kernel integriert, aber ein großer Teil der Basisarbeit wurde schon vorher erledigt und mußte von Treiber-Entwicklern berücksichtigt werden.

Eine weitere in der 2.1-Entwicklung eingeführte Änderung war das Hinzufügen des Zeigerarguments f_pos zu den Methoden read und write. Diese Änderung wurde vorgenommen, um die POSIX-Systemaufrufe pread und pwrite zu unterstützen, die explizit den Datei-Offset angeben, von dem Daten gelesen oder zu dem Daten geschrieben werden sollen. Ohne diese Systemaufrufe kann es bei Multithreading-Programmen zu Race Conditions kommen, wenn Daten in Dateien verschoben werden.

Fast alle Methoden in Linux 2.0 erwarteten ein explizites Zeigerargument inode. In der 2.1-Reihe wurde dieser Parameter aus mehreren Methoden entfernt, weil er selten benötigt wurde. Wenn Sie ihn noch brauchen, können Sie ihn immer noch aus dem filp-Argument gewinnen.

Als Ergebnis daraus sahen die Prototypen der häufig verwendeten Methoden in file_operations in 2.0 so aus:

int (*lseek) (struct inode *, struct file *, off_t, int);

Beachten Sie, daß diese Methode in Linux 2.0 nicht llseek, sondern lseek heißt. Die Namensänderung soll anzeigen, daß Positionierungsoperationen jetzt mit 64-Bit-Offset-Werten vorgenommen werden können.

int (*read) (struct inode *, struct file *, char *, int);, int (*write) (struct inode *, struct file *, const char *, int);

Wie erwähnt hatten diese Funktionen in Linux 2.0 den inode-Zeiger als Argument, dafür aber kein Positionsargument.

void (*release) (struct inode *, struct file *);

Im 2.0-Kernel konnte die Methode release nicht fehlschlagen und gab deswegen void zurück.

Es hat noch viele weitere Änderungen an der Struktur file_operations gegeben; wir werden diese in den folgenden Kapiteln behandeln, wenn sie für uns relevant werden. Es lohnt sich aber gleichwohl auch jetzt schon, sich anzuschauen, wie portabler Code geschrieben werden kann, der die bisher geschriebenen Änderungen abdeckt. Die Änderungen an diesen Methoden sind umfassend, und es gibt keine einfache, elegante Möglichkeit, sie zu behandeln.

Der Beispiel-Code berücksichtigt diese Änderungen mit kleinen Wrapper-Funktionen, die zwischen der alten und der neuen API “übersetzen”. Diese Wrapper werden nur verwendet, wenn mit 2.0-Header-Dateien kompiliert wird, und ersetzen die “echten” Geräte-Methoden in der Struktur file_operations. Der folgende Code implementiert die Wrapper für den scull-Treiber:


/*
 * Die folgenden Wrapper bringen den Code mit 2.0-Kerneln zum Laufen
 */
#ifdef LINUX_20
int scull_lseek_20(struct inode *ino, struct file *f,
    off_t offset, int whence)
{
 return (int)scull_llseek(f, offset, whence);
}

int scull_read_20(struct inode *ino, struct file *f, char *buf,
   int count)
{
 return (int)scull_read(f, buf, count, &f->f_pos);
}

int scull_write_20(struct inode *ino, struct file *f, const char *b,
   int c)
{
 return (int)scull_write(f, b, c, &f->f_pos);
}

void scull_release_20(struct inode *ino, struct file *f)
{
 scull_release(ino, f);
}

/* "Echte" Namen in 2.0-Namen umdefinieren */
#define scull_llseek scull_lseek_20
#define scull_read scull_read_20
#define scull_write scull_write_20
#define scull_release scull_release_20
#define llseek lseek
#endif /* LINUX_20 */

Das Umdefinieren der Namen auf diese Weise kann auch Struktur-Elemente abdecken, deren Namen sich mit der Zeit geändert haben (wie etwa im Falle der Änderung von lseek in llseek).

Wir müssen wahrscheinlich nicht extra sagen, daß Sie solche Umdefinitionen von Namen mit Vorsicht vornehmen sollten; diese Zeilen sollten vor der Definition der Struktur file_operations, aber nach einer Veränderung dieser Namen stehen.

Es gibt noch zwei weitere Inkompatibilitäten im Zusammenhang mit der Struktur file_operations. So wurde die Methode flush während der 2.1-Entwicklung hinzugefügt. Treiber-Entwickler müssen sich fast nie um diese Methode kümmern, aber ihre reine Anwesenheit mitten in der Struktur kann trotzdem noch zu Problemen führen. Sie können es am besten vermeiden, sich mit der flush-Methode auseinanderzusetzen, wenn Sie die markierte Initialisierungssyntax verwenden, wie wir das in allen Beispieldateien getan haben.

Der andere Unterschied besteht darin, wie ein inode-Zeiger aus der filp-Struktur geholt wird. Während moderne Kernel eine dentry-Struktur (directory entry, Verzeichniseintrag) verwenden, gab es eine solche Struktur im Kernel 2.0 nicht. Daher definiert sysdep.h ein Makro, das für den portablen Zugriff auf inode von einem filp verwendet werden soll.


#ifdef LINUX_20
# define INODE_FROM_F(filp) ((filp)->f_inode)
#else
# define INODE_FROM_F(filp) ((filp)->f_dentry->d_inode)
#endif

Der Verwendungszähler von Modulen

> > In 2.2 und früheren Kerneln gab es von seiten des Linux-Kernels keine Unterstützung für Module bei der Verwaltung des Verwendungszählers. Die Module mußten diese Arbeit selbst erledigen. Dieser Ansatz war fehlerträchtig und machte viel doppelte Arbeit notwendig.

Code, der portabel sein soll, muß aber auch mit dieser alten Vorgehensweise zurechtkommen. Das bedeutet, daß der Verwendungszähler weiterhin inkrementiert werden muß, wenn eine neue Referenz auf das Modul angelegt wird, und daß er dekrementiert werden muß, wenn diese Referenz wieder verschwindet. Portabler Code muß auch das Problem umgehen, daß das Feld owner in der Struktur file_operations in früheren Kernel-Versionen nicht existiert. Das ist am einfachsten möglich, wenn Sie SET_MODULE_OWNER verwenden, anstatt auf das Feld owner zuzugreifen. In sysdep.h stellen wir eine leere Version von SET_FILE_OWNER für Kernel bereit, die nicht über dieses Feld verfügen.

Änderungen in der Unterstützung von Semaphoren

Die Semaphor-Unterstützung war im 2.0-Kernel noch weniger weit entwickelt; ganz allgemein war die Unterstützung für SMP-Systeme zu diesem Zeitpunkt noch recht primitiv. Treiber, die nur für diese Kernel-Version geschrieben wurden, müssen gar keine Semaphore verwenden, weil nur jeweils eine CPU Kernel-Code ausführen konnte. Dennoch mag es hier die Notwendigkeit für Semaphore geben, und es schadet allemal nicht, den vollständigen Schutz späterer Kernel-Versionen zu haben.

Die meisten der in diesem Kapitel behandelten Semaphor-Funktionen existierten auch schon im Kernel 2.0. Die einzige Ausnahme bildet die Funktion sema_init; im Kernel 2.0 mußten Entwickler die Semaphore manuell initialisieren. Die Header-Datei sysdep.h deckt dieses Problem durch Definition einer Version von sema_init ab, die bei der Verwendung eines 2.0-Kernels benutzt wird:


#ifdef LINUX_20
# ifdef MUTEX_LOCKED /* Nur, wenn semaphore.h eingebunden ist */
  extern inline void sema_init (struct semaphore *sem, int val)
  {
   sem->count = val;
   sem->waking = sem->lock = 0;
   sem->wait = NULL;
  }
# endif
#endif /* LINUX_20 */

Änderungen im Zugriff auf den User-Space

Schließlich änderte sich am Anfang der 2.1-Reihe der Zugriff auf den User-Space total. Die neue Schnittstelle hat ein besseres Design und nutzt die Hardware für den sicheren Zugriff auf User Space-Speicher sehr viel besser aus. Aber natürlich hat sich die Schnittstelle geändert. Im 2.0-Kernel sahen die Funktionen für den Speicherzugriff folgendermaßen aus:


 void memcpy_fromfs(void *to, const void *from, unsigned long count);
 void memcpy_tofs(void *to, const void *from, unsigned long count);

Die Namen dieser Funktionen stammen aus der historischen Verwendung des FS-Segment-Registers auf dem i386. Beachten Sie, daß diese Funktionen keinen Rückgabewert haben; wenn der Benutzer eine ungültige Adresse angibt, dann schlägt das Kopieren der Daten stillschweigend fehl. sysdep.h versteckt die Umbenennung und erlaubt Ihnen das portable Aufrufen von copy_to_user und copy_from_user.