8.1. Blockorientierte Gerätetreiber

Festplatten, Disketten, Flashcards, USB-Sticks, DVDs, CD-Roms oder auch Ramdisks ist gemeinsam, dass sie Informationen in Blöcken ablegen. Daher nennt man diese Geräte auch Blockgeräte beziehungsweise Block-Devices. Unter ihnen bestehen noch weitere Übereinstimmungen, die zugleich wichtige Unterscheidungsmerkmale zu den bisher behandelten zeichenorientierten Geräten darstellen:

Zur Vereinfachung haben die Entwickler festgelegt, dass jeder Block genau 512 Byte groß ist. Ein solcher Block wird Sektor genannt. Da ohnehin nur auf Blöcke, und nicht auf einzelne Bytes, direkt zugegriffen werden kann, wird die Blocknummer (Sektornummer) zur Auswahl (Adressierung) eines Blockes verwendet. Die ersten 512 Bytes eines Hintergrundspeichers entsprechen damit dem Sektor (dem Block) mit der Nummer »0«, Bytes 512 bis 1023 dem Sektor mit der Nummer »1« und so weiter.

Um ein einzelnes Byte von einem Blockgerät zu lesen, muss zunächst berechnet werden, in welchem Sektor sich das Byte befindet. Danach wird der entsprechende Sektor in den Hauptspeicher kopiert und das Byte aus dieser Kopie extrahiert (im Hauptspeicher kann ja schließlich auf einzelne Bytes zugegriffen werden). Der Schreibvorgang ist leicht komplizierter. Da immer nur komplette Blöcke geschrieben werden können, muss zunächst der Block, in dem ein Byte geändert werden soll, in den Hauptspeicher gelesen werden; die Modifikation an der Kopie wird vorgenommen und schließlich wird die modifizierte Kopie auf das Gerät zurückgeschrieben. Allerdings erfolgt das Zurückschreiben in den meisten Fällen aus Performance-Gründen nicht sofort. Vielmehr wartet das System eine Weile ab, ob die Applikation nicht noch mehr Bytes in diesem Block schreibt. Aufeinander folgende Schreibaufträge der Applikation resultieren daher sehr oft in nur einen physikalischen Schreibauftrag an die Platte.

Eine Applikation greift somit nicht – anders als bei zeichenorientierten Geräten – direkt über den Treiber auf die Daten zu. Vielmehr wird wie beschrieben der Lese- oder Schreibauftrag der Applikation durch das Betriebssystem vorverarbeitet. Dank des vom Betriebssystem zwischengeschalteten Filesystems (z.B. »ext3«) muss sich der Anwender keine Blocknummern merken, sondern kann seine Daten in Dateien und Verzeichnissen organisieren.

Dass nicht die Applikation direkt, sondern der Betriebssystemkern in Form des Blockgeräte-Subsystems auf den Blockgerätetreiber zugreift, vereinfacht die Erstellung eines solchen Treibers. Die Hauptaufgabe besteht damit darin, eine Funktion zur Verfügung zu stellen, die Blöcke vom Gerät in den Hauptspeicher bzw. umgekehrt transferiert (Request-Funktion genannt).

Abbildung 8-1. Blockorientierter Datenaustausch zwischen Haupt- und Hintergrundspeicher

Dass allerdings der Kernel die Zugriffe auf das Blockgerät (den Hintergrundspeicher) zu optimieren versucht, macht die Entwicklung wieder komplexer. Der Kernel reicht nämlich die Transferwünsche (Requests) nicht direkt an die Transferfunktion des Treibers weiter, sondern sammelt und sortiert die Aufträge (nach Reihenfolge der Blocknummern) vorher, so dass sie performanter bearbeitet werden können. Natürlich hat der Treiberentwickler die Möglichkeit, diese Optimierung entweder zu steuern oder komplett selbst durchzuführen.

Außerdem kann bereits ein einzelner Auftrag den Transfer mehrerer, sequentieller Sektoren beinhalten. Mehrere, aufeinander folgende Sektoren des Hintergrundspeichers werden aber nicht unbedingt sequentiell im Hauptspeicher abgelegt. Daher reicht bei umfangreicheren Transfers die Angabe einer Hauptspeicheradresse nicht aus (siehe Abbildung Blockorientierter Datenaustausch zwischen Haupt- und Hintergrundspeicher). Vielmehr übergibt das Blocklayer-Subsystem eine Reihe von Speicherblöcken (Pages), in die die Daten geschrieben werden sollen beziehungsweise aus denen die Daten kommen (Bio-Struktur, siehe Kapitel Grundlegendes zu BIO-Blöcken).

Die Quelle oder das Ziel eines Transfers als Page anzugeben hat den Vorteil, dass damit nicht festgelegt ist, ob sich die Seite im User- oder im Kernel-Space befindet.

Der Blockgerätetreiber muss im Wesentlichen drei Komponenten bereitstellen:

  1. Die Transferfunktion, die den Datenaustausch zwischen der Hardware (Festplatte) und der Speicherseite (Page) realisiert.

  2. Eine Beschreibung des Blockgerätes, des Hintergrundspeichers. Hierzu gehört beispielsweise die Größe des Speichers in Blöcken. Für die Beschreibung steht die Struktur struct gendisk zur Verfügung.

  3. Eine Request-Queue (die Instanz einer Datenstruktur), in die der Kernel seine Aufträge an den Treiber ablegt.

8.1.1. Bevor es richtig losgeht ...

Zu den ersten Aufgaben des Treibers gehört:

Als Erstes muss der Treiber eine Request-Queue anlegen. In dieser legt das System die Aufträge an den Treiber ab. Dazu muss die Funktion blk_init_queue aufgerufen werden. Als Parameter werden die Adresse der Funktion, die im Fall eines Requests aufgerufen werden soll (siehe Daten kerneloptimiert transferieren) und ein Spinlock, über das die Queue vor parallelen Zugriffen geschützt wird, übergeben.

Des Weiteren muss der Treiber das Blockgerät, also die »Disk«, beim Blockgeräte-Subsystem durch Aufruf von add_disk anmelden. add_disk bekommt als Parameter eine Datenstruktur vom Typ struct gendisk mit, die jedoch erst über die Funktion alloc_disk definiert (angelegt) wird und deren Elemente initialisiert werden.

Die Funktion alloc_disk hat als einzigen Parameter die Anzahl der Minor-Nummern, über die das Gerät später angesprochen werden kann. Im Regelfall entspricht die Anzahl der Minor-Nummern der Anzahl der auf dem Gerät anlegbaren Partitionen. Wird nur eine Minor-Nummer verwendet, hat das Gerät genau eine Partition, was wiederum bedeutet, dass auf dem Gerät keine besondere Partitionierungsinformation abgelegt werden muss (sämtliche Blöcke stehen der einen Partition zur Verfügung).

Die wichtigsten, zu parametrierenden Elemente der struct gendisk sind:

Während die meisten Elemente über normale Zuweisungen belegt werden, wird der auf dem Gerät vorhandene Speicherplatz über das Makro set_capacity gesetzt. Die Kapazität wird in Sektoren (also in 512-Byte-Blöcken) angegeben. Eine möglicherweise notwendige Umrechnung zwischen Byte und Blöcken erfolgt durch Division der Größe in Byte mit 512. Eine solche Division wird rechentechnisch am einfachsten durch ein Rechtsschieben um 9 Bit realisiert.

Die struct block_device_operations entspricht von ihrer Bedeutung her der bekannten struct file_operations. Hier kann beispielsweise die Adresse einer eigenen Open oder auch Release-Funktion hinterlegt werden. Auch wenn keine Adresse hinterlegt wird, muss die Struktur angelegt werden und das Feld .owner mit dem Makro THIS_MODULE initialisiert werden!

Ein Blockgeräte-Treiber kann sich durch Aufruf der Funktion register_blkdev beim Blockgeräte-Subsystem anmelden. Das wird nur dann notwendig, wenn zum einen ein Eintrag im Proc-Filesystem (unter /proc/devices) erstellt oder/und wenn die Major-Nummer dynamisch zugewiesen werden soll. Sonstige Aufgaben, die die Funktion früher einmal zu erledigen hatte, hat die Funktion add_disk übernommen.

Die Initialisierung des Blockgerätetreibers wird im Beispiel Ein einfacher Ramdisk-Treiber in der Funktion mod_init demonstriert.

Bei der Deinitialisierung wird wieder aufgeräumt. Der Treiber meldet beim Blockgerätesubsystem zunächst die Platte (put_disk) und danach sich selbst ab (unregister_blkdev). Er gibt noch die bei der Initialisierung reservierten Ressourcen für die Platte (del_gendisk) und für die Queue (blk_cleanup_queue) frei. Die Freigabe der Queue darf erst erfolgen, nachdem die struct gendisc freigegeben wurde.

Die Aufräumarbeiten des Blockgerätetreibers werden in Beispiel Ein einfacher Ramdisk-Treiber in der Funktion mod_exit durchgeführt.

8.1.2. Daten kerneloptimiert transferieren

Die eigentliche Aufgabe des Treibers besteht darin, Daten zwischen Gerät (Hintergrundspeicher) und Page (Hauptspeicher) zu transferieren.

Abbildung 8-2. Blockgerätetreiber können sich an zwei Stellen einklinken

Prinzipiell gibt es zwei Arten den Datentransfer im Treiber zu realisieren (siehe Abbildung Blockgerätetreiber können sich an zwei Stellen einklinken):

  1. direkt und

  2. kerneloptimiert.

Normalerweise werden Aufträge (Requests) über die Request-Queue gesammelt. Zu einem vom Kernel bestimmten Zeitpunkt wird die Request-Queue und mit ihr die bis dato gesammelten Aufträge der Treiberfunktion übergeben. Diese Variante wird mit »kerneloptimiert« bezeichnet, weil der Kernel die Requests in der Liste so sortiert, dass der Treiber möglichst optimal die Daten transferieren kann. Die vom Treiber zur Verfügung zu stellende Request-Funktion wird per blk_init_queue spezifiziert.

Für virtuelle Blockgeräte – wie beispielsweise für eine Ramdisk – ist dieses Verfahren weniger geeignet. Für solche Geräte bedeutet das Erstellen und Sortieren einer Request-Queue unnötigen Overhead. Die Treiber klinken sich daher »eine Etage höher« ein und tun vor dem Kernel so, als übernehmen sie das Erstellen und Sortieren einer Request-Queue selbst. Dazu wird eine Make-Request-Funktion erstellt und per blk_queue_make_request spezifiziert. Dieses Vorgehen ist detailliert im Kapitel Treiberoptimierter Datentransfer beschrieben.

Die Requests, die innerhalb der Request-Queue dem Treiber übergeben werden, enthalten im Wesentlichen die folgenden Parameter:

Abbildung 8-3. Die Request-Datenstruktur

Der eigentliche Datentransfer wird in der Request-Funktion (Funktion vom Typ request_fn_proc) abgewickelt. Die Adresse ist bei der Initialisierung beim Aufruf der Funktion blk_init_queue angegeben worden. Ebenfalls bei der Initialisierung wurde ein Spinlock übergeben, welches für den Schutz der Auftragsliste zuständig ist. Wenn die Request-Funktion aufgerufen wird, wird das Spinlock schon durch das Blockgerätesubsystem gehalten. Der Treiber hat damit exklusiven Zugriff auf die Auftragsliste.

Abbildung 8-4. Struktogramm der Transferfunktion eines Blockgerätetreibers

Die Request-Funktion bekommt eine Liste von Aufträgen übergeben, die abgearbeitet werden müssen (siehe Struktogramm der Transferfunktion eines Blockgerätetreibers). Im einfachen Fall bearbeitet die Funktion Auftrag für Auftrag. Ein weiterer Funktionsparameter – ein Flag-Element – kennzeichnet den Auftragstyp. Die verschiedenen Auftragstypen sind in der Header-Datei <linux/blkdev.h> beschrieben. Mit Hilfe des Makros blk_fs_request kann überprüft werden, ob es sich um einen normalen Schreib- bzw. Leseauftrag (Auftrag des Filesystems) handelt.

Der Zugriff auf die Transferrichtung erfolgt über das Makro rq_data_dir. Das Ergebnis dieses Makros kann mit der Konstanten (definiert in <linux/fs.h>) READ bzw. WRITE verglichen werden.

Einen Auftrag entnimmt man im einfachsten Fall der Auftragsliste (der Request-Queue) über die Funktion elv_next_request. Diese Funktion gibt jeweils einen Zeiger auf den nächsten Auftrag (struct request *) zurück und erhält dazu als Parameter die Adresse der Auftragsliste (Request-Queue).

Dass ein einzelner Auftrag (Request) bearbeitet worden ist, wird dem System mit Hilfe der Funktion end_request mitgeteilt. Der erste Parameter ist der Auftrag selbst, der zweite Parameter (»uptodate« genannt) spezifiziert, ob der Auftrag bearbeitet wurde (Wert ungleich »0«) oder nicht (Wert gleich »0«).

Auf Basis der bisherigen Kenntnisse lässt sich bereits ein einfacher Blockgerätetreiber erstellen. Beispiel Ein einfacher Ramdisk-Treiber führt die Implementierung einer Ramdisk vor – auch wenn die Methode nicht optimal für diesen Einsatz ist. Anstelle der bisher eingeführten Funktion kmalloc zur Speicherreservierung wird hier vmalloc verwendet. Mit Hilfe dieser Funktion ist der Entwickler in der Lage, auch größere Speicherbereiche zu allozieren.

Beispiel 8-1. Ein einfacher Ramdisk-Treiber

#include <linux/fs.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/blkdev.h>

#define BD_MAJOR 242
#define SIZE_IN_KBYTES 256

MODULE_LICENSE("GPL");

static struct gendisk *disk;
static struct request_queue *bdqueue;
static char *mempool;
static spinlock_t bdlock = SPIN_LOCK_UNLOCKED;

static void bd_request( request_queue_t *q )
{
    struct request *req;
    unsigned long start, to_copy;

    printk("bd_request()\n");
    while((req=elv_next_request(q))!=NULL ) {
        if( !blk_fs_request(req) ) {
            end_request( req, 0 );
            continue;
        }
        to_copy  = req->current_nr_sectors<<9;
        start = req->sector<<9;
        spin_unlock_irq(q->queue_lock);
        if( (start+to_copy)<=(SIZE_IN_KBYTES*1024) ) {
            if (rq_data_dir(req) == READ) {
                memcpy(req->buffer, mempool+start, to_copy);
            } else {
                memcpy(mempool+start, req->buffer, to_copy);
            }
        } else {
            printk("%ld not in range...\n", start+to_copy);
        }
        spin_lock_irq(q->queue_lock);
        end_request(req,1);
    }
}

static struct block_device_operations bdops = {
    .owner = THIS_MODULE,
};

static int __init mod_init(void)
{
    if (register_blkdev(BD_MAJOR, "bdsample" )) {
        printk("blockdevice: Majornummer %d not free.", BD_MAJOR);
        return -EIO;
    }
    if( !(mempool=vmalloc(SIZE_IN_KBYTES*1024)) ) {
        printk("vmalloc failed ...\n");
        goto out_no_mem;
    }

    if( !(disk=alloc_disk(1)) ) {
        printk("alloc_disk failed ...\n");
        goto out;
    }
    disk->major = BD_MAJOR;
    disk->first_minor = 0;
    if( (bdqueue=blk_init_queue(&bd_request,&bdlock))==NULL )
        goto out;
    blk_queue_hardsect_size( bdqueue, 512 );
    disk->queue = bdqueue;
    sprintf(disk->disk_name, "bd0");
    set_capacity( disk, (SIZE_IN_KBYTES * 1024)>>9 ); // in 512 Byte Bloecke
    disk->fops = &bdops;
    add_disk( disk );
    return 0;
out:
    vfree( mempool );
out_no_mem:
    unregister_blkdev(BD_MAJOR, "bdsample" );
    return -EIO;
}

static void __exit mod_exit(void)
{
    del_gendisk(disk);
    put_disk( disk );
    blk_cleanup_queue( bdqueue );
    vfree( mempool );
    unregister_blkdev(BD_MAJOR, "bdsample" );
}

module_init( mod_init );
module_exit( mod_exit );

Ist der Treiber erfolgreich übersetzt (kompiliert) und geladen worden, kann das neue Gerät verwendet werden. Dazu muss zunächst die zugehörige Gerätedatei erstellt werden. Anschließend können per cat Daten in das Blockgerät geschrieben und wieder ausgelesen werden:

root@obile# mknod bdevtest b 242 0
root@obile# cat /etc/motd >bdevtest
root@obile# cat <bdevtest

Achtung: Da unsere Ramdisk bisher kein Filesystem besitzt, wird keine Längeninformation mit abgespeichert. Somit wird beim Auslesen die komplette Ramdisk ausgegeben. Besser ist es, die Ramdisk zunächst mit einem Dateisystem zu versehen und erst anschließend zu verwenden:

root@obile# mknod bdevtest b 242 0
root@obile# mke2fs bdevtest
root@obile# mount bdevtest /mnt

Jetzt kann auf /mnt – wie auf andere Dateisysteme auch – zugegriffen werden.

Das Spinlock wird während der Abarbeitung der Request-Funktion gehalten. Will der Kernel während dieser Zeit weitere Aufträge in die Queue einhängen, ist er regelrecht ausgebremst. Daher sollte innerhalb der Request-Funktion das Spinlock möglichst schnell wieder freigegeben werden und nur dann genommen werden, wenn die Request-Queue modifiziert wird. Falls das Spinlock freigegeben wird, dürfen Sie nicht vergessen, es wieder zu reservieren, damit die vom Kernel anschließend durchgeführte Freigabe erfolgreich ist.

8.1.3. Grundlegendes zu BIO-Blöcken

Die Request-Parameter spiegeln den Zugriffswunsch des Blockgeräte-Subsystems wider. Aufeinander folgende Anfragen an ein Gerät werden dabei durch eine einzige Request-Struktur abgebildet. Die innerhalb des Gerätes aufeinander folgenden Blöcken werden aber nur in den seltensten Fällen auch innerhalb des Speichers hintereinander liegen. Die Adressen der zum Request gehörigen Speicheradressen werden in der BIO-Struktur (Block Ein-/Ausgabestruktur) spezifiziert.

Abbildung 8-5. Die Block-IO-Datenstruktur

Die BIO-Struktur selbst enthält zunächst:

  1. Die Transferrichtung: Zugriff über das Makro bio_data_dir. Dieses Makro gibt READ oder WRITE zurück.

  2. Die Anzahl zu kopierender Bytes. Diese Information ist im Feld bio->bi_size abgelegt.

  3. Die Geräteadresse als Sektornummer. Diese Information ist im Feld bio->bi_sector abgelegt.

Darüber hinaus existiert ein Feld von einzelnen I/O-Pages, auch Segmente genannt. Der Zugriff auf die Adresse eines solchen Segments erfolgt über das Makro bio_data.

Das funktioniert nur, falls das Segment auch »gemappt« ist, sich also nicht im High-Memory-Bereich befindet. Da es mehrere Segmente pro BIO-Struktur geben kann, müssen alle Segmente ausgewertet werden. Das führt dazu, dass eine Applikation drei ineinander geschachtelte Schleifen durchlaufen muss:

    while((req=elv_next_request(q))!=NULL ) {
        rq_for_each_bio( bio, req ) {
            bio_for_each_segment( bvec, bio, i ) {
                // hier die Verarbeitung des Segments
            }
        }
    }

»Q« stellt die Request-Queue dar, »bio« ist ein Zeiger auf eine Biostruktur, die im Rahmen der Schleife belegt wird, »bvec« kennzeichnet einen Zeiger auf das Feld der Segmente und »i« ist ein Zähler für die einzelnen Elemente.

Der bereits vorgestellte einfache Ramdisk-Treiber lässt sich unter Verwendung der BIO-Struktur wie im Beispiel Verwendung von BIO-Blöcken realisieren.

Beispiel 8-2. Verwendung von BIO-Blöcken

#include <linux/fs.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/blkdev.h>

#define BD_MAJOR 242
#define SIZE_IN_KBYTES 256

MODULE_LICENSE("GPL");

static struct gendisk *disk;
static struct request_queue *bdqueue;
static char *mempool;
static spinlock_t bdlock = SPIN_LOCK_UNLOCKED;

static struct block_device_operations bops = {
    .owner           = THIS_MODULE,
};

static void reqfn( request_queue_t *q )
{
    struct request *req;
    int segnr=0;
    char *kaddr, *maddr;
    struct bio *bio=NULL;
    struct bio_vec *bvec=NULL;

    while((req=elv_next_request(q))!=NULL ) {
        if( !blk_fs_request(req) ) {
            end_request( req, 0 );
            continue;
        }
        rq_for_each_bio(bio, req ) {
            bio_for_each_segment( bvec, bio, segnr ) {
                kaddr = page_address(bvec->bv_page) + bvec->bv_offset;

                maddr = mempool + (512 * bio->bi_sector);
                if( bio_data_dir( bio )==READ ) {
                    memcpy( kaddr, maddr, bvec->bv_len );
                } else {
                    memcpy( maddr, kaddr, bvec->bv_len );
                }
            }
        }
        end_request( req, 1 );
    }
}

static int __init mod_init(void)
{
    if (register_blkdev(BD_MAJOR, "bdsample" )) {
        printk("blockdevice: Majornummer %d not available.", BD_MAJOR);
        return -EIO;
    }
    if( !(mempool=vmalloc(SIZE_IN_KBYTES*1024)) ) {
        printk("vmalloc failed ...\n");
        goto out_no_mem;
    }
    if( !(disk=alloc_disk(1)) ) {
        printk("alloc_disk failed ...\n");
        goto out;
    }
    disk->major = BD_MAJOR;
    disk->first_minor = 0;
    disk->fops = &bops;
    if( (bdqueue=blk_init_queue(reqfn,&bdlock))==NULL ) {
        printk( KERN_ERR "blk_init_queue failed\n" );
        goto out;
    }
    blk_queue_hardsect_size( bdqueue, 512 );
    disk->queue = bdqueue;
    sprintf(disk->disk_name, "bd0");
    set_capacity( disk, (SIZE_IN_KBYTES * 1024)>>9 ); // in 512 Byte blocks
    add_disk( disk );
    return 0;
out:
    vfree( mempool );
out_no_mem:
    unregister_blkdev(BD_MAJOR, "bdsample" );
    return -EIO;
}

static void __exit mod_exit(void)
{
    unregister_blkdev(BD_MAJOR, "bdsample" );
    del_gendisk(disk);
    put_disk( disk );
    blk_cleanup_queue( bdqueue ); // has to be the last statement
    vfree( mempool );
}

module_init( mod_init );
module_exit( mod_exit );

8.1.4. Treiberoptimierter Datentransfer

Einige Treiber benötigen die Optimierung durch den Kernel nicht. Der Treiber für eine Ramdisk beispielsweise kopiert die Blöcke einfach aus dem Buffercache in seinen Speicher und zurück – falls er nicht ohnehin soweit optimiert ist, dass er den Buffercache selbst als Speicher nutzt.

Die Optimierung der Requests, also das Sortieren der Auftragsliste, kann umgangen werden, indem der Treiber die __make_request-Funktion des Kernels durch seine eigene MakeRequest-Funktion ersetzt. Das Ersetzen wird durch Aufruf der Funktion blk_queue_make_request erreicht. Der erste Parameter der Funktion ist die Request-Queue, der zweite die Funktion, die im Fall einer Anfrage aufgerufen werden soll. Im einfachsten Fall übernimmt diese Funktion die Anfrage des Kerns – die in einem Bio-Block übergeben wird – und führt den dort übergebenen Auftrag aus. Natürlich kann der Auftrag innerhalb der Funktion gesammelt und später mit anderen Aufträgen kombiniert weiterverarbeitet werden. Eine auf diese Weise implementierte Ramdisk zeigt Beispiel Blockgerätetreiber ohne Request-Queue.

Beispiel 8-3. Blockgerätetreiber ohne Request-Queue

#include <linux/init.h>
#include <linux/blkdev.h>

#define BD_MAJOR 242
#define SIZE_IN_KBYTES 64

MODULE_LICENSE("GPL");

static struct gendisk *disk;
static struct request_queue *bdqueue;
static char *mempool;

static struct block_device_operations bops;

static int bd_make_request( request_queue_t *q, struct bio *bio )
{
    char *kaddr, *maddr;
    struct bio_vec *bvec;
    int segnr;

    printk("bd_make_request(%p)\n", bio);
    blk_queue_bounce( q, &bio ); // higmemory-support
    bio_for_each_segment( bvec, bio, segnr ) {
        kaddr = bio_data(bio);
        maddr = mempool + (512 * bio->bi_sector);

        if( bio_data_dir( bio )==READ || bio_data_dir( bio )==READA ) {
            printk("read: %p - %p len=%d\n",kaddr,maddr,bio->bi_size);
            memcpy( kaddr, maddr, bio->bi_size );
        } else {
            printk("write: %p - %p len=%d\n",maddr,kaddr,bio->bi_size);
            memcpy( maddr, kaddr, bio->bi_size );
        }
    }
    bio_endio( bio, bio->bi_size, 0 );
    return 0;
}

static int __init mod_init(void)
{
    printk("mod_init called\n");
    if (register_blkdev(BD_MAJOR, "bdsample" )) {
        printk("blockdevice: Majornummer %d not free.", BD_MAJOR);
        return -EIO;
    }
    if( !(mempool=kmalloc(SIZE_IN_KBYTES*1024,GFP_KERNEL)) ) {
        printk("kmalloc failed ...\n");
        goto out_no_mem;
    }
    bdqueue = blk_alloc_queue( GFP_KERNEL );
    blk_queue_make_request( bdqueue, bd_make_request );
    disk = alloc_disk(1);
    if( !disk ) {
        printk("alloc_disk failed ...\n");
        goto out;
    }
    disk->major = BD_MAJOR;
    disk->first_minor = 0;
    disk->fops = &bops;
    disk->queue = bdqueue;
    sprintf(disk->disk_name, "bd0");
    set_capacity( disk, (SIZE_IN_KBYTES * 1024)>>9 ); // in 512 Byte Bloecke
    add_disk( disk );
    return 0;
out:
    kfree( mempool );
out_no_mem:
    unregister_blkdev(BD_MAJOR, "bdsample" );
    return -EIO;
}

static void __exit mod_exit(void)
{
    printk("ModExit called\n");
    unregister_blkdev(BD_MAJOR, "bdsample" );
    del_gendisk(disk);
    put_disk( disk );
    blk_cleanup_queue( bdqueue );
    kfree( mempool );
}

module_init( mod_init );
module_exit( mod_exit );
/* vim: set ts=4 ic aw sw=4: */

Wählt der Treiberentwickler diese Variante, muss er eine Besonderheit beachten: Die in den Bio-Blöcken übergebenen Speicherseiten können sich nämlich durchaus im High-Memory-Bereich befinden. Da auf diesen Bereich nicht direkt zugegriffen werden kann, muss die Speicherseite erst in einen zugreifbaren Bereich kopiert werden (oder in diesen Bereich gemappt werden). Dazu dient die Funktion blk_queue_bounce.

Wird anstelle der Request-Funktion eine Make-Request-Funktion verwendet, muss der Programmierer die Request-Queue selbst erzeugen. Dazu ruft er die Funktion blk_alloc_queue auf.


Lizenz