Datei-Operationen

In den nächsten Abschnitten schauen wir uns die verschiedenen Operationen an, die ein Treiber auf den von ihm verwalteten Geräten ausführen kann. Ein offenes Gerät wird intern über eine file-Struktur identifiziert, und der Kernel benutzt die Struktur file_operations, um auf die Funktionen des Treibers zuzugreifen. Diese Struktur, die in <linux/fs.h> definiert wird, ist ein Array aus Funktionszeigern. Jede Datei hat ihren eigenen Satz von Funktionen (indem ein Feld namens f_op aufgenommen wird, das auf eine file_operations-Struktur verweist). Die Operationen sind vor allem dafür zuständig, die Systemaufrufe zu implementieren, und heißen daher open, read usw. Wir können die Datei als “Objekt” und die darauf operierenden Funktionen als “Methoden” ansehen und so die Terminologie der objektorientierten Programmierung verwenden, mit der Aktionen bezeichnet werden, die von einem Objekt auf sich selbst deklariert werden. Hier stoßen wir zum erstenmal auf objekt-orientiertes Design im Linux-Kernel. Davon werden wir später noch mehr sehen.

Per Konvention wird eine file_operations-Struktur oder ein Zeiger darauf fops (oder so ähnlich) genannt; wir haben so einen Zeiger schon als Argument im Aufruf von register_chrdev kennengelernt. Jedes Feld der Struktur muß auf die Funktion im Treiber verweisen, die die jeweilige Operation implementiert, oder auf NULL bleiben, wenn die Aktion nicht unterstützt wird. Was der Kernel genau macht, wenn dort ein NULL-Zeiger steht, ist von Funktion zu Funktion unterschiedlich, wie die Liste im nächsten Abschnitt zeigen wird.

Die Struktur file_operations ist langsam gewachsen, während neue Funktionalität zum Kernel hinzugefügt wurde. Das Hinzufügen neuer Operationen kann natürlich zu Portabilitätsproblemen bei Gerätetreibern führen. Instantiierungen der Struktur in jedem Treiber erfolgten früher in Standard-C-Syntax, und neue Operationen wurden normalerweise am Ende der Struktur hinzugefügt. Ein einfaches Neukompilieren der Treiber setzte damit einen NULL-Wert für die neue Operation ein und wählte so das Default-Verhalten aus, was man normalerweise genau erreichen wollte.

Inzwischen sind die Kernel-Entwickler auf ein “Tag”-Initialisierungsformat übergegangen, das die Initialisierung von Strukturfeldern anhand ihres Namens ermöglicht und so die meisten Probleme mit geänderten Datenstrukturen vermeidet. Die Tag-Initialisierung ist aber kein Standard-C, sondern eine (nützliche) Spezialerweiterung des GNU-Compilers. Wir werden uns gleich ein Beispiel für eine Tag-Initialisierung einer Struktur anschauen.

Die folgende Liste führt alle Operationen auf, die eine Applikation auf einem Gerät ausführen kann. Wir haben uns bemüht, die Liste so kurz zu halten, daß sie als Referenz verwendet werden kann. Jede Operation ist nur kurz zusammengefaßt. Außerdem finden Sie das Default-Verhalten des Kernels, wenn ein NULL-Zeiger verwendet wird. Sie können diese Liste beim ersten Lesen überspringen und später darauf zurückkommen.

Nachdem eine weitere wichtige Datenstruktur (die file-Struktur) beschrieben worden ist, erklärt der Rest des Kapitels die Aufgabe der wichtigsten Operationen, gibt Hinweise, weist auf Fallen hin und enthält echte Code-Beispiele. Wir werden die komplexeren Operationen in einem späteren Kapitel besprechen, denn es fehlen uns jetzt noch Kenntnisse über die Speicherverwaltung, über blockierende Operationen und über asynchrone Benachrichtigung.

Die folgende Liste zeigt, welche Operationen in welcher Reihenfolge in der 2.4-Serie des Kernels in struct file_operations stehen. Obwohl es kleinere Unterschiede zwischen 2.4 und früheren Kerneln gibt, behandeln wir diese erst weiter hinten in diesem Kapitel und beschränken uns hier auf 2.4. Der Rückgabewert der einzelnen Operationen ist im Erfolgsfall 0 oder ein negativer Fehlercode, um einen Fehler anzuzeigen, wenn nichts anderes angegeben ist.

loff_t (*llseek) (struct file *, loff_t, int);

Die Methode llseek wird verwendet, um die aktuelle Lese/Schreib-Position in einer Datei zu verändern; die neue Position wird als (positiver) Rückgabewert zurückgegeben. loff_t ist ein “langer Offset” und selbst auf 32-Bit-Plattformen mindestens 64 Bits breit. Fehler werden durch einen negativen Rückgabewert gemeldet. Wenn die Funktion für diesen Treiber nicht existiert, schlagen Positionierungen relativ zum Dateiende fehl, andere Positionierungen funktionieren trotzdem; in diesem Fall wird der Positionszeiger in der file-Struktur (beschrieben in the Section called Die Struktur file) verändert.

ssize_t (*read) (struct file *, char *, size_t, loff_t *);

Wird verwendet, um Daten vom Gerät zu lesen. Steht an dieser Position ein NULL-Zeiger, dann schlägt der read-Systemaufruf mit -EINVAL (“Invalid argument”) fehl. Ein nicht-negativer Rückgabewert gibt die Anzahl der erfolgreich eingelesenen Zeichen an (der Rückgabewert ist ein “vorzeichenbehafteter Größentyp”, normalerweise der native Integer-Typ der Zielplattform).

ssize_t (*write) (struct file *, const char *, size_t, loff_t * int);

Schickt Daten an das Gerät. Wenn diese Funktion nicht existiert, wird vom Systemaufruf write -EINVAL zurückgegeben. Wenn der Rückgabewert nicht negativ ist, enthält er die Anzahl der erfolgreich geschriebenen Bytes.

int (*readdir) (struct file *, void *, filldir_t);

Dieses Feld sollte bei Geräteknoten NULL sein; es wird nur zum Lesen von Verzeichnissen verwendet und ist nur bei Dateisystemen sinnvoll.

unsigned int (*poll) (struct file *, struct poll_table_struct *);

Die Methode poll ist das Backend zweier Systemaufrufe, poll und select, die dazu verwendet werden, um abzufragen, ob ein Gerät lesbar oder beschreibbar ist oder sich in einem besonderen Zustand befindet. Beide Systemaufrufe können blockieren, bis ein Gerät lesbar oder beschreibbar wird. Wenn ein Treiber keine poll-Methode definiert, dann wird angenommen, daß das Gerät sowohl lesbar als auch beschreibbar ist und sich in keinem besonderen Zustand befindet. Der Rückgabewert ist eine Bitmaske, die den Zustand des Geräts beschreibt.

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

Der ioctl-Systemaufruf ermöglicht es Programmen, gerätespezifische Befehle abzusetzen (wie das Formatieren einer Diskettenspur, wobei es sich ja weder um Lesen noch um Schreiben handelt). Einige weitere ioctl-Aufrufe werden vom Kernel erkannt, ohne auf die fops-Tabelle zuzugreifen. Wenn das Gerät keinen Einsprungpunkt für ioctl enthält, gibt der Systemaufruf bei jeder nicht vordefinierten Anfrage einen Fehler (-ENOTTY, “No such ioctl for device”) zurück. Ein nicht-negativer Rückgabewert wird an das aufrufende Programm zurückgegeben, um eine erfolgreiche Ausführung mitzuteilen.

int (*mmap) (struct file *, struct vm_area_struct *);

mmap wird verwendet, um eine Abbildung von Gerätespeicher auf den Adreßraum des Prozesses anzufordern. Wenn das Gerät diese Methode nicht implementiert, gibt der Systemaufruf mmap -ENODEV zurück.

int (*open) (struct inode *, struct file *);

Obwohl dies immer die erste Operation ist, die auf einer Gerätedatei ausgeführt wird, muß ein Treiber nicht unbedingt eine entsprechende Methode deklarieren. Wenn dieser Eintrag NULL ist, dann gelingt das Öffnen des Gerätes immer; das wird Ihrem Treiber allerdings nicht mitgeteilt.

int (*flush) (struct file *);

Die flush-Operation wird aufgerufen, wenn ein Prozeß seine Kopie eines Dateideskriptors für ein Gerät schließt; die Methode sollte dann alle ausstehenden Operationen auf dem Gerät ausführen (und auf deren Beendigung warten). Verwechseln Sie dies nicht mit der fsync-Operation, die von Anwenderprogrammen angefordert wird. flush wird derzeit nur vom NFS-Code (dem Network File System) verwendet. Wenn flush NULL ist, wird die Methode einfach nicht aufgerufen.

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

Diese Operation wird aufgerufen, wenn die file-Struktur freigegeben wird. Wie open kann auch release fehlen.[1]

int (*fsync) (struct inode *, struct dentry *, int);

Diese Methode ist das Backend des Systemaufrufs fsync, den ein Anwender verwendet, um ausstehende Daten herauszuschreiben. Wenn diese Methode vom Treiber nicht implementiert wird, gibt dieser -EINVAL zurück.

int (*fasync) (int, struct file *, int);

Diese Operation wird benutzt, um dem Gerät eine Änderung seines FASYNC-Schalters mitzuteilen. Die asynchrone Benachrichtigung ist ein fortgeschrittenes Thema und wird in Kapitel 5 behandelt. Wenn der Treiber die asynchrone Benachrichtigung nicht unterstützt, kann dieses Feld NULL sein.

ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);, ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);

Diese beiden Methoden wurden spät im 2.3-Entwicklungszyklus hinzugefügt und implementieren sogenannte scatter/gather-Lese- und Schreiboperationen. Applikationen müssen von Zeit zu Zeit einzelne Lese- oder Schreib-Operationen durchführen, bei denen mehrere Speicherbereiche betroffen sind. Diese Systemaufrufe erlauben dies, ohne die Daten zusätzlich kopieren zu müssen.

struct module *owner;

Dieses Feld ist keine Methode wie alles andere in der Struktur file_operations. Es handelt sich vielmehr um einen Zeiger auf das Modul, dem diese Struktur “gehört”. Dies wird vom Kernel verwendet, um den Verwendungszähler des Moduls zu pflegen.

Der Gerätetreiber scull implementiert nur die wichtigsten Methoden und verwendet das Tag-Format, um seine file_operations-Struktur zu deklarieren:


 

struct file_operations scull_fops = {
 llseek:  scull_llseek,
 read:  scull_read,
 write:  scull_write,
 ioctl:  scull_ioctl,
 open:  scull_open,
 release: scull_release,
};

Diese Deklaration verwendet die Tag-Initialisierungssyntax, die wir oben beschrieben haben. Diese Syntax ist vorzuziehen, weil sie Treiber portabler in Hinblick auf Änderungen in der Strukturdefinition macht, und sie macht den Code auch kompakter und besser lesbar. Die Tag-Initialisierung erlaubt das Umstellen von Struktur-Mitgliedern; in manchen Fällen konnten maßgebliche Performance-Verbesserungen erreicht werden, indem häufig verwendete Struktur-Mitglieder in die gleiche Zeile des Hardware-Caches gestellt wurden.

Außerdem muß das owner-Feld der file_operations-Struktur gesetzt werden. Oftmals werden Sie dies zusammen mit der restlichen Initialisierung in der folgenden Tag-Syntax vorfinden:


 owner: THIS_MODULE,

Dieser Ansatz funktioniert aber nur auf 2.4-Kerneln. Portabler ist die Verwendung des Makros SET_MODULE_OWNER, das in <linux/module.h> definiert ist. scull führt diese Initialisierung folgendermaßen durch:


 SET_MODULE_OWNER(&scull_fops);

Dieses Makro funktioniert auf allen Strukturen, die ein owner-Feld haben; wir werden auf dieses Feld noch in anderem Zusammenhang zurückkommen.

Fußnoten

[1]

Beachten Sie, daß release nicht jedesmal aufgerufen wird, wenn ein Prozeß close aufruft. Immer wenn eine file-Struktur gemeinsam genutzt wird (etwa nach einem fork oder dup), wird release nicht aufgerufen, bis nicht alle Kopien geschlossen sind. Wenn Sie ausstehende Daten herausschreiben müssen, wenn eine der Kopien geschlossen wird, sollten Sie die flush-Methode verwenden.