Anfragen bearbeiten: Eine einfache Einführung

Die wichtigste Funktion in einem Block-Treiber ist die Funktion request, die die Low-Level-Operationen durchführt, die zum Lesen und Schreiben von Daten benötigt werden. In diesem Abschnitt beschreiben wir das grundlegende Design der request-Prozedur.

Die Anfrage-Warteschlange

Wenn der Kernel eine Datenübertragung vorsieht, stellt er eine Anfrage in eine Warteschlange, die so sortiert ist, daß die Performance des Systems maximiert wird. Die Warteschlange mit den Anfragen wird dann an die request-Funktion des Treibers übergeben, die folgenden Prototyp hat:


void request_fn(request_queue_t *queue);

Die request-Funktion sollte für jede Anfrage in der Warteschlange die folgenden Aufgaben ausführen:

  1. Die Zulässigkeit der Anfrage überprüfen. Dieser Test geschieht durch das in blk.h definierte Makro INIT_REQUEST; hier wird nach Problemen gesucht, die auf einen Fehler in der Anfrage-Warteschlangen-Verarbeitung des Systems hinweisen könnten.

  2. Die Datenübertragung selbst durchführen. Die Variable CURRENT (eigentlich ein Makro) liefert die Details der aktuellen Anfrage. CURRENT ist ein Zeiger auf eine struct request-Struktur, deren Felder im nächsten Abschnitt beschrieben werden.

  3. Die gerade bearbeitete Anfrage aufräumen. Diese Operation wurd von end_request erledigt. Das ist eine statische Funktion, deren Code in blk.h steht. end_request kümmert sich um die Abarbeitung der Anfrage-Warteschlange und weckt die Prozesse, die auf I/O-Operationen warten. Außerdem wird hier die Variable CURRENT auf die nächste noch nicht erledigte Anfrage umgesetzt. Der Treiber übergibt der Funktion ein Argument: 1 im Erfolgsfall und 0 im Fehlerfall. Wenn end_request mit dem Argument 0 aufgerufen wird, wird via printk eine “I/O Error”-Meldung in das Systemprotokoll geschrieben.

  4. Zurück zum Anfang springen und die nächste Anfrage bearbeiten.

Anhand dieser Beschreibung kann man eine minimale request-Funktion schreiben, die noch keine Daten überträgt:


void sbull_request(request_queue_t *q)
{
    while(1) {
        INIT_REQUEST;
        printk("<1>request %p: cmd %i sec %li (nr. %li)\n", CURRENT,
               CURRENT->cmd,
               CURRENT->sector,
               CURRENT->current_nr_sectors);
        end_request(1); /* erfolgreich */
    }
}

Obwohl dieser Code nichts weiter tut, als Meldungen auszugeben, ermöglicht das Ausführen dieser Funktion einen guten Einblick in das grundlegende Design der Datenübertragung. Außerdem können Sie hier einige der Makros in <linux/blk.h> in Aktion sehen. Beachten Sie zunächst, daß die while-Schleife aussieht, als würde sie nie beendet werden. In Wirklichkeit enthält aber das INIT_REQUEST-Makro eine return-Anweisung, die ausgeführt wird, wenn keine weiteren Anfragen vorliegen. Die Schleife iteriert also über die Warteschlange mit den ausstehenden Anfragen und springt dann aus der request-Funktion zurück. Zweitens beschreibt das CURRENT-Makro immer die zu verarbeitende Anfrage. Wir schauen uns die Details von CURRENT im nächsten Abschnitt an.

Ein Block-Treiber, der die gerade gezeigte request-Funktion verwendet, wird sogar funktionieren — eine kurze Zeit lang. Es ist möglich, ein Dateisystem auf dem Gerät anzulegen und darauf zuzugreifen, solange die Daten im Buffer-Cache des Systems verbleiben.

Diese leere (aber geschwätzige) Funktion kann auch in sbull ausgeführt werden, indem man beim Kompilieren das Symbol SBULL_EMPTY_REQUEST angibt. Wenn Sie verstehen wollen, wie der Kernel mit verschiedenen Block-Größen umgeht, können Sie mit dem Parameter blksize= auf der Kommandozeile von insmod experimentieren. Die leere request-Funktion zeigt die interne Verarbeitung des Kernels, indem sie die Details jeder Anfrage ausgibt.

Die request-Funktion hat eine wichtige Einschränkung: Sie muß atomar sein. request wird normalerweise nicht als direkte Reaktion auf Benutzeranfragen aufgerufen und läuft nicht im Kontext eines bestimmten Prozesses. Sie kann zur Interrupt-Zeit, von Tasklets oder diversen anderen Stellen aufgerufen werden. Daher darf sie bei der Bearbeitung ihrer Aufgaben nicht schlafen. >

Die eigentliche Datenübertragung durchführen

Um zu verstehen, wie man eine funktionierende request-Funktion für sbull schreiben kann, müssen wir uns anschauen, wie der Kernel eine Anfrage in einer struct request-Struktur beschreibt. Diese Struktur ist in <linux/blkdev.h> definiert. Indem er auf die Felder in dieser Struktur zugreift (was üblicherweise über CURRENT geschieht), kann der Treiber alle notwendigen Informationen über die Datenübertragung zwischen dem Buffer-Cache und dem phyiskalischen Block-Gerät bekommen.[1] CURRENT ist einfach ein Zeiger auf blk_dev[MAJOR_NR].request_queue. Die folgenden Felder einer Anfrage enthalten Informationen, die für die request-Funktion nützlich sind:

kdev_t rq_dev;

Das Gerät, auf das von der Anfrage zugegriffen wird. Default-mäßig wird für alle von einem Treiber verwalteten Geräte die gleiche request-Funktion verwendet. Eine einzige request-Funktion deckt alle Minor-Nummern ab; rq_dev kann dazu verwendet werden, das aktuelle Minor-Gerät herauszufinden. Das Makro CURRENT_DEV ist einfach als DEVICE_NR(CURRENT->rq_dev) definiert.

int cmd;

Dieses Feld beschreibt die durchzuführende Operation; es ist entweder READ (vom Gerät) oder WRITE (auf das Gerät).

unsigned long sector;

Die Nummer des ersten in dieser Anfrage zu übertragenden Sektors.

unsigned long current_nr_sectors;, unsigned long nr_sectors;

Die Anzahl der in der aktuellen Anfrage zu übertragenden Sektoren. Der Treiber sollte current_nr_sectors verwenden und nr_sectors ignorieren (das wird hier nur aus Gründen der Vollständigkeit aufgeführt). In "the Section called Cluster-Anfragen>" weiter hinten in diesem Kapitel finden Sie nähere Informationen zu nr_sectors.

char *buffer;

Der Bereich im Buffer-Cache, in den die Daten geschrieben (cmd==READ) oder aus dem die Daten gelesen (cmd==WRITE) werden sollen.

struct buffer_head *bh;

Die Struktur, die den ersten Puffer in der Liste für diese Anfrage beschreibt. Puffer-Köpfe werden in der Verwaltung des Buffer-Cache verwendet; wir schauen sie uns in “the Section called Die request-Struktur und der Buffer-Cache>” kurz genauer an.

Es gibt noch andere Felder in der Struktur, die aber hauptsächlich zur internen Verwendung im Kernel gedacht sind; Treiber sollten sie nicht verwenden.

Die Implementation der funktionierenden request-Funktion im sbull-Gerät folgt hier. In diesem Code hat Sbull_Dev die gleiche Aufgabe wie Scull_Dev, das in "the Section called Die Verwendung von Speicher in scull in Kapitel 3" in Kapitel 3> eingeführt wurde.


void sbull_request(request_queue_t *q)
{
    Sbull_Dev *device;
    int status;

    while(1) {
        INIT_REQUEST;  /* springt zurueck, wenn die Warteschlange leer ist */

        /* Welches "Geraet" verwenden wir? */
        device = sbull_locate_device (CURRENT);
        if (device == NULL) {
            end_request(0);
            continue;
        }

        /* Uebertragung durchfuehren und aufraeumen. */
        spin_lock(&device->lock);
        status = sbull_transfer(device, CURRENT);
        spin_unlock(&device->lock);
        end_request(status);
    }
}

Diese Code unterscheidet sich nur wenig von der zuvor gezeigten leeren Version; er beschränkt sich auf die Verwaltung der Anfrage-Warteschlange und delegiert die eigentliche Arbeit an andere Funktionen. Die erste davon, sbull_locate_device, schaut sich die Geräte-Nummer in der Anfrage an und sucht dazu die passende Sbull_Dev-Struktur heraus:


static Sbull_Dev *sbull_locate_device(const struct request *req)
{
    int devno;
    Sbull_Dev *device;

    /* Ueberpruefen, ob die Minor-Nummer gueltig ist */
    devno = DEVICE_NR(req->rq_dev);
    if (devno >= sbull_devs) {
        static int count = 0;
        if (count++ < 5) /* die Meldung hoechstens fuenfmal ausgeben */
            printk(KERN_WARNING "sbull: request for unknown device\n");
        return NULL;
    }
    device = sbull_devices + devno; /* Aus dem Geraete-Array holen */
    return device;
}

Das einzige “Merkwürdige” an dieser Funktion ist die bedingte Anweisung, die die Anzahl der gemeldeten Fehler auf fünf beschränkt. Dies soll verhindern, daß die Systemprotokolle mit zu vielen Meldungen zugemüllt werden, weil end_request(0) bereits eine “I/O error”-Meldung ausgibt, wenn die Anfrage fehlschlägt. Der static-Zähler ist eine gängige Möglichkeit, um die Anzahl der gemeldeten Fehler zu beschränken, und wird im Kernel mehrfach verwendet.

Die eigentliche I/O der Anfrage wird von sbull_transfer erledigt:


static int sbull_transfer(Sbull_Dev *device, const struct request *req)
{
    int size;
    u8 *ptr;

    ptr = device->data + req->sector * sbull_hardsect;
    size = req->current_nr_sectors * sbull_hardsect;

    /* Sicherstellen, daß die Uebertragung in das Geraet paßt. */
    if (ptr + size > device->data + sbull_blksize*sbull_size) {
        static int count = 0;
        if (count++ < 5)
            printk(KERN_WARNING "sbull: request past end of device\n");
        return 0;
    }

    /* Sieht gut aus, uebertragen. */
    switch(req->cmd) {
        case READ:
            memcpy(req->buffer, ptr, size); /* von sbull in den Puffer */
            return 1;
        case WRITE:
            memcpy(ptr, req->buffer, size); /* aus dem Puffer nach sbull */
            return 1;
        default:
            /* kann nicht vorkommen */
            return 0;
    }
}

Weil sbull nur eine RAM-Disk ist, beschränkt sich die “Datenübertragung” auf einen memcpy-Aufruf.

Fußnoten

[1]

Es müssen übrigens nicht alle an einen Block-Treiber übergebenen Blocks im Buffer-Cache liegen; das würde aber über den Umfang dieses Kapitels hinaus führen.