Kapitel 12. Das Laden von Block-Treibern

Inhalt
Den Treiber registrieren
Die Header-Datei blk.h
Anfragen bearbeiten: Eine einfache Einführung
Anfragen bearbeiten: Das Ganze noch einmal genauer
Wie das Einhängen und Aushängen funktioniert
Die ioctl-Methode
Herausnehmbare Geräte
Partitionierbare Geräte
Interrupt-gesteuerte Block-Treiber
Abwärtskompatibilität
Schnellreferenz

Bisher haben wir nur über Zeichen-Treiber gesprochen. Wie wir aber bereits erwähnt haben, sind diese aber nicht die einzige Art von Treibern in einem Linux-System. Wir wenden unsere Aufmerksamkeit jetzt Block-Treibern zu. Block-Treiber ermöglichen den Zugriff auf blockorientierte Geräte, also solche Geräte, die Daten in Blocks fester Größe transportieren, auf die nach Belieben zugegriffen werden kann. Das klassische Block-Gerät ist natürlich das Festplattenlaufwerk, aber es gibt auch andere.

Die Zeichen-Treiber-Schnittstelle ist relativ sauber und einfach zu verwenden, aber die Block-Schnittstelle ist leider etwas unsortierter. Kernel-Entwickler lieben es, sich darüber zu beklagen. Für diese Situation gibt es zwei Gründe. Der eine ist historisch bedingt — die Block-Schnittstelle ist in jeder Linux-Version seit der allerersten vorhanden gewesen und es hat sich herausgestellt, daß sie schwer zu verändern ist. Der andere Grund ist die Performance. Ein langsamer Zeichen-Treiber ist nicht schön, aber ein langsamer Block-Treiber zieht das ganze System hinab. Daher ist das Design der Block-Schnittstelle oft mehr von der Geschwindigkeit als von etwas anderem beeinflußt worden.

Die Block-Treiber-Schnittstelle hat sich mit der Zeit verändert. Wie im Rest dieses Buches behandeln wir in diesem Kapitel die Schnittstelle aus dem Kernel 2.4 und gehen am Ende auf die Änderungen ein. Die Beispiel-Treiber funktionieren aber mit allen Kerneln von 2.0 bis 2.4.

In diesem Kapitel führen wir für unsere Entdeckungsreise durch die Block-Treiber zwei neue Beispiele ein. Das erste, sbull (Simple Block Utility for Loading Localities) implementiert ein Block-Gerät im Systemspeicher und ist damit im wesentlichen eine RAM-Disk. Später schauen wir uns noch eine Variante namens spull an, an der wir lernen, wie man mit Partitionstabellen umgeht.

Wie immer gehen diese Beispiel-Treiber über viele der Probleme in echten Block-Treibern großzügig hinweg; sie sollen die Schnittstelle demonstrieren, mit der solche Treiber arbeiten müssen. Echte Treiber müssen mit der Hardware umgehen, weswegen das in Kapitel 8> und Kapitel 9> behandelte Material hier ebenfalls nützlich ist.

Noch eine kurze Anmerkung zur Technologie: Das Wort Block bezeichnet in diesem Buch einen vom Kernel definierten Datenblock. Die Größe von Blocks kann auf verschiedenen Festplatten unterschiedlich sein, ist aber immer eine Zweierpotenz. Ein Sektor ist eine Dateneinheit fester Größe, die von der zugrundeliegenden Hardware bestimmt wird. Sektoren sind fast immer 512 Bytes lang.

Den Treiber registrieren

Wie Zeichen-Treiber werden auch Block-Treiber im Kernel durch Major-Nummern identifiziert. Die Major-Nummern von Block-Treibern sind aber völlig von denen von Zeichen-Treibern getrennt. Ein Block-Gerät mit der Major-Nummer 32 kann problemlos mit einem Zeichen-Gerät mit derselben Nummer koexistieren, weil die beiden Bereiche getrennt sind.

Mit den folgenden Funktionen werden Block-Treiber registriert und deregistriert:



int register_blkdev(unsigned int major, const char *name,
                    struct block_device_operations *bdops);
int unregister_blkdev(unsigned int major, const char *name);

Die Argumente haben die gleiche Bedeutung wie bei Zeichen-Treibern; auch die dynamische Zuweisung von Major-Nummern funktioniert genauso. Daher registriert sich sbull fast genau so wie scull:


 
result = register_blkdev(sbull_major, "sbull", &sbull_fops);
if (result < 0) {
    printk(KERN_WARNING "sbull: kann keine Major-Nummer bekommen %d\n",sbull_major);
    return result;
}
if (sbull_major == 0) sbull_major = result; /* dynamisch */
major = sbull_major; /* major wird spaeter noch verwendet */

Hier enden die Ähnlichkeiten aber auch schon. Ein Unterschied ist bereits offensichtlich: register_chrdev erwartete einen Zeiger auf eine file_operations-Struktur, während register_blkdev eine Struktur des Typs block_device_operations verwendet — das ist so seit der Kernel-Version 2.3.38. Die Struktur wird in den Block-Treibern manchmal immer noch mit fops bezeichnet; wir werden sie bdops nennen, um den Inhalt der Struktur besser wiederzugeben und der vorgeschlagenen Benennung zu folgen. Die Definition dieser Struktur lautet:


struct block_device_operations {
    int (*open) (struct inode *inode, struct file *filp);
    int (*release) (struct inode *inode, struct file *filp);
    int (*ioctl) (struct inode *inode, struct file *filp,
                    unsigned command, unsigned long argument);
    int (*check_media_change) (kdev_t dev);
    int (*revalidate) (kdev_t dev);
};






Die hier genannten Methoden open, release und ioctl sind genau die gleichen wie bei Zeichen-Geräten. Die anderen beiden sind blockgerätespezifisch und werden weiter hinten in diesem Kapitel besprochen. Beachten Sie, daß es in dieser Struktur kein owner-Feld gibt; Block-Treiber müssen auch im 2.4-Kernel ihren Verwendungszähler manuell pflegen.

Die bdops-Struktur von sbull sieht folgendermaßen aus:


struct block_device_operations sbull_bdops = {
    open:               sbull_open,
    release:            sbull_release,
    ioctl:              sbull_ioctl,
    check_media_change: sbull_check_change,
    revalidate:         sbull_revalidate,
};

Beachten Sie, daß es in dieser Struktur keine Lese- oder Schreiboperationen gibt. Alle I/O mit Block-Geräten wird normalerweise vom System gebuffert (die einzige Ausnahme sind “rohe” Geräte, die wir im nächsten Kapitel behandeln); Benutzerprozesse führen keine direkten I/O-Operationen auf diesen Geräten durch. Der User Mode-Zugriff auf Block-Geräte erfolgt normalerweise implizit durch Dateisystem-Operationen, die eindeutig von der I/O-Pufferung profitieren. Aber auch “direkte” I/O mit Block-Geräten, etwa beim Erzeugen von Dateisystemen, geht durch den Buffer-Cache von Linux.[1] Als Folge davon stellt der Kernel einen einzigen Satz von Lese- und Schreibfunktionen für Block-Geräte bereit, um die sich die Treiber nicht kümmern müssen.

Natürlich muß ein Block-Treiber auch irgendwo einen Mechanismus bereitstellen, mit dem am Ende blockorientierte I/O mit dem Gerät durchgeführt wird. Unter Linux heißt die für solche I/O-Operationen verwendete Methode request und ist das Gegenstück zur Funktion “strategy”, die man auf vielen Unix-Systemen findet. Die request-Methode kümmert sich sowohl um Lese- als auch um Schreiboperationen und kann ziemlich komplex sein. Wir werden uns die Details in Kürze ansehen.

Zur Registration von Block-Geräten müssen wir dem Kernel aber mitteilen, wo sich unsere request-Methode befindet. Diese steht aus historischen und Performance-Gründen nicht in der Struktur block_device_operations, sondern ist statt dessen der Warteschlange der ausstehenden I/O-Operationen des Geräts zugeordnet. Default-mäßig gibt es eine solche Warteschlange für jede Major-Nummer. Ein Block-Gerät muß diese Warteschlange mit blk_init_queue initialisieren. Die Initialisierung und das Aufräumen der Warteschlange ist folgendermaßen definiert:


#include <linux/blkdev.h>
blk_init_queue(request_queue_t *queue, request_fn_proc *request);
blk_cleanup_queue(request_queue_t *queue);

Die init-Funktion richtet die Warteschlange ein und bindet die request-Funktion des Treibers (die als zweiter Parameter übergeben wird) an die Warteschlange. Wenn das Modul abgeräumt wird, muß man blk_cleanup_queue aufrufen. Der sbull-Treiber initialisiert seine Warteschlange mit folgendem Code:


blk_init_queue(BLK_DEFAULT_QUEUE(major), sbull_request);

Jedes Gerät hat eine Anfrage-Warteschlange, die es default-mäßig verwendet; das Makro BLK_DEFAULT_QUEUE(major) wird dazu verwendet, die Schlange bei Bedarf anzugeben. Dieses Makro schlägt in einem globalen Array von blk_dev_struct-Strukturen namens blk_dev nach, das vom Kernel verwaltet und mit der Major-Nummer indiziert wird. Die Struktur sieht wie folgt aus:


struct blk_dev_struct {
    request_queue_t        request_queue;
    queue_proc                *queue;
    void                *data;
};

Das Feld request_queue enthält die I/O-Anfrage-Warteschlange, die wir gerade initialisiert haben. Wir schauen uns das Feld queue in Kürze an. Das Feld data kann vom Treiber für eigene Daten verwendet werden, aber nur wenige Treiber machen davon Gebrauch.

Abbildung 12-1 stellt die Hauptschritte grafisch dar, die ein Treibermodul benutzt, um sich beim Kernel an- und abzumelden. Wenn Sie diese Abbildung mit Abbildung 2-1 vergleichen, sollten die Ähnlichkeiten und Unterschiede klar sein.

Abbildung 12-1. Registrieren eines Block-Gerätetreibers

Zusätzlich zu blk_dev enthalten mehrere andere globale Arrays Informationen über Block-Treiber. Diese werden mit der Major- und manchmal auch mit der Minor-Nummer indiziert. Sie sind in drivers/block/ll_rw_block.c deklariert und beschrieben.

int blk_size[][];

Dieses Array wird mit der Major- und der Minor-Nummer indiziert. Es beschreibt die Größe jedes Geräts in Kilobytes. Wenn blk_size[major] NULL ist, dann wird die Größe des Gerätes nicht überprüft (d. h. der Kernel könnte Datenübertragungen über das Ende des Gerätes hinaus anfordern).

int blksize_size[][];

Die von jedem Gerät verwendete Block-Größe in Bytes. Wie das vorige Array wird auch dieses mit der Major- und der Minor-Nummer indiziert. Wenn blksize_size[major] ein Null-Zeiger ist, wird eine Block-Größe von BLOCK_SIZE (derzeit 1 KByte) angenommen. Die Block-Größe des Gerätes muß eine Zweierpotenz sein, weil der Kernel Bit-Shift-Operatoren verwendet, um Offsets in Block-Nummern zu konvertieren.

int hardsect_size[][];

Wie die anderen Strukturen wird auch diese mit der Major- und der Minor-Nummer indiziert. Der Default-Wert der Hardware-Sektorengröße ist 512 Bytes. In den 2.2- und 2.4-Kerneln werden auch andere Sektorgrößen unterstützt, die aber immer eine Zweierpotenz größer oder gleich 512 Bytes sein müssen.

int read_ahead[];, int max_readahead[][];

Diese Arrays definieren die Anzahl der Sektoren, die der Kernel im voraus lesen soll, wenn eine Daten sequentiell eingelesen wird. read_ahread gilt für alle Geräte des jeweiligen Typs und wird mit der Major-Nummer indiziert. max_readahead gilt für bestimmte Geräte und wird sowohl mit der Major- als auch mit der Minor-Nummer indiziert.

Das Lesen von Daten, bevor ein Prozeß danach fragt, verbessert die Performance des Systems und den Durchsatz insgesamt. Ein langsameres Gerät könnte einen größeren Read-Ahead-Wert angeben, während schnellere Geräte auch einen kleineren Wert verwenden könnten. Je größer der Read-Ahead-Wert ist, je mehr Speicher verwendet der Buffer-Cache.

Der Hauptunterschied zwischen den beiden Arrays ist folgender: read_ahead wird auf der Ebene der Block-I/O benutzt und steuert, wie viele Blocks sequentiell von der Festplatte vor der aktuellen Anfrage gelesen werden können. max_readahead arbeitet auf Dateisystem-Ebene und bezeichnet Blocks in der Datei, die möglicherweise nicht sequentiell auf der Festplatte liegt. Die Kernel-Entwicklung geht dahin, das Vorauslesen auf der Dateisystem-Ebene und nicht auf der Block-I/O-Ebene zu implementieren. Im 2.4-Kernel geschieht das aber noch auf beiden Ebenen, weswegen beide Arrays verwendet werden.

Es gibt einen read_ahead[]-Wert pro Major-Nummer, der für alle zugehörigen Minor-Nummern gilt. max_readahead enthält dagegen einen Wert für jedes Gerät. Die Werte können über die ioctl-Methode des Treibers geändert werden; Festplattentreiber verwenden normalerweise einen read_ahead-Wert von acht Sektoren, also 4 KByte. Der Wert max_readahead wird dagegen selten von Treibern verändert; der Default ist MAX_READAHEAD, derzeit 31 Seiten.

int max_sectors[][];

Dieses Array beschränkt die maximale Größe einer einzigen Anfrage. Es sollte normalerweise auf die größte Datenmenge gesetzt werden, mit der Ihre Hardware umgehen kann.

int max_segments[];

Dieses Array steuert die Anzahl einzelner Segmente, die in einer Cluster-Anfrage vorkommen können. Es wurde aber kurz vor der Freigabe des 2.4-Kernels entfernt. (Siehe “the Section called Cluster-Anfragen” weiter hinten in diesem Kapitel für Informationen über Cluster-Anfragen.)

Das sbull-Gerät erlaubt es Ihnen, diese Werte beim Laden zu setzen, woraufhin sie für alle Minor-Nummern im Beispiel-Treiber gelten. Die Variablennamen und deren Default-Werte in sbull lauten:

size=2048 (Kilobyte)

Jede von sbull erzeugte RAM-Disk belegt 2 MByte RAM.

blksize=1024 (Bytes)

Der vom Modul verwendete Software-“Block” ist ein KByte groß, wie der System-Default.

hardsect=512 (Byte)

Die Sektorengröße von sbull ist der normale Halb-KByte-Wert.

rahead=2 (Sektoren)

Weil die RAM-Disk ein schnelles Gerät ist, ist der Default-Vorauslesewert klein.

Das sbull-Gerät erlaubt es Ihnen auch, die Anzahl der zu installierenden Geräte auszuwählen. devs, die Anzahl der Geräte hat einen Default von 2, was einen Default-Speicherverbrauch von 4 MByte bedeutet — zwei RAM-Disks mit je 2 MByte.

Die Implementation von init_module im sbull-Gerät sieht folgendermaßen aus (wobei die Registration der Major-Nummer und die Fehlerbehandlung hier weggelassen ist):


 
read_ahead[major] = sbull_rahead;
result = -ENOMEM; /* fuer moegliche Fehler */

sbull_sizes = kmalloc(sbull_devs * sizeof(int), GFP_KERNEL);
if (!sbull_sizes)
    goto fail_malloc;
for (i=0; i < sbull_devs; i++) /* alle haben die gleiche Groesse */
    sbull_sizes[i] = sbull_size;
blk_size[major]=sbull_sizes;

sbull_blksizes = kmalloc(sbull_devs * sizeof(int), GFP_KERNEL);
if (!sbull_blksizes)
    goto fail_malloc;
for (i=0; i < sbull_devs; i++) /* alle haben die gleiche Blockgroesse */
    sbull_blksizes[i] = sbull_blksize;
blksize_size[major]=sbull_blksizes;

sbull_hardsects = kmalloc(sbull_devs * sizeof(int), GFP_KERNEL);
if (!sbull_hardsects)
    goto fail_malloc;
for (i=0; i < sbull_devs; i++) /* alle haben die gleiche Sektorengroesse */
    sbull_hardsects[i] = sbull_hardsect;
hardsect_size[major]=sbull_hardsects;

Aus Platzgründen ist hier der Code zur Fehlerbehandlung (das Ziel des fail_malloc-gotos) weggelassen worden; hier wird einfach alles, was bereits erfolgreich alloziert wurde, wieder freigegeben, das Gerät deregistriert und ein Fehler zurückgemeldet.

Schließlich muß noch jedes vom Treiber bereitgestellte “Platten”-Gerät registriert werden. sbull ruft dazu die notwendige Funktion (register_disk) folgendermaßen auf:


for (i = 0; i < sbull_devs; i++)
        register_disk(NULL, MKDEV(major, i), 1, &sbull_bdops,
                        sbull_size << 1);

Im 2.4.0-Kernel macht register_disk nichts, wenn es so aufgerufen wird. Der Hauptzweck von register_disk ist das Einrichten der Partitionstabelle, was von sbull nicht unterstützt wird. Alle Block-Treiber rufen aber diese Funktion auf, ob sie nun Partitionen unterstützen oder nicht. Dies deutet darauf hin, daß dieser Aufruf in der Zukunft obligatorisch werden wird. Ein Block-Treiber ohne Partitionen funktioniert in 2.4.0 auch ohne diesen Aufruf, aber es ist sicherer, ihn einzubauen. Wir schauen uns register_disk später in diesem Kapitel noch detailliert an, wenn wir Partitionen behandeln.

Die Funktion zum Aufräumen sieht so aus:


 
for (i=0; i<sbull_devs; i++)
    fsync_dev(MKDEV(sbull_major, i)); /* Geraete herausschreiben */
unregister_blkdev(major, "sbull");
/*
 * Abfrage-Schlange zurechtbasteln
 */
blk_cleanup_queue(BLK_DEFAULT_QUEUE(major));

/* Globale Arrays aufraeumen */
read_ahead[major] = 0;
kfree(blk_size[major]);
blk_size[major] = NULL;
kfree(blksize_size[major]);
blksize_size[major] = NULL;
kfree(hardsect_size[major]);
hardsect_size[major] = NULL;

Der Aufruf von fsync_dev ist notwendig, damit alle Referenzen auf das Gerät in den diversen Caches des Kernel freigegeben werden. fsync_dev ist übrigens die Implementation, die die Arbeit hinter block_fsync erledigt, der “fsync”-Methode von Block-Geräten.

Fußnoten

[1]

In der 2.3-Entwicklungsserie wurde rohe I/O hinzugefügt, was es Benutzerprozessen ermöglicht, direkt auf Block-Geräte zuzugreifen, ohne über den Buffer-Cache zu gehen. Block-Treiber wissen aber nichts von roher I/O, weswegen wir das bis zum nächsten Kapitel zurückstellen.