Die kiobuf-Schnittstelle

Ab Version 2.3.12 unterstützt der Linux-Kernel eine I/O-Abstraktion namens Kernel-I/O-Puffer oder kiobuf. Die kiobuf-Schnittstelle soll einen großen Teil der Komplexität der virtuellen Speicherverwaltung vor den Gerätetreibern (und anderen Teilen im System, die I/O betreiben) verstecken. Es sind noch viele Features für kiobufs geplant, aber der hauptsächliche Verwendungszweck im 2.4-Kernel ist das Erleichtern der Einblendung von User-Space-Buffern in den Kernel.

Die kiobuf-Struktur

Sämtlicher Code, der mit kiobufs arbeitet, muß <linux/iobuf.h> einbinden. Diese Datei definiert struct kiobuf, das Herzstück der kiobuf-Schnittstelle. Diese Struktur beschreibt ein Array von Seiten, die eine I/O-Operation bilden. Dort finden sich unter anderem folgende Felder:

int nr_pages;

Die Anzahl der Seiten in diesem kiobuf

int length;

Die Anzahl der Datenbytes im Puffer

int offset;

Der Offset bis zum ersten gültigen Byte im Puffer

struct page **maplist;

Ein Array von page-Strukturen; eine je Seite mit Daten im kiobuf

Der Schlüssel zur kiobuf-Schnittstelle ist das Array maplist. Funktionen, die auf in einem kiobuf gespeicherten Seiten operieren, arbeiten direkt mit den page-Strukturen — der gesamte Umweg der virtuellen Speicherverwaltung ist aus dem Weg geräumt worden. Diese Implementation erlaubt es Treibern, unabhängig von den Komplexitäten der Speicherverwaltung zu arbeiten, und macht das Leben ganz allgemein deutlich einfacher.

Ein kiobuf muß vor der Verwendung initialisiert werden. Man initialisiert nur selten einen einzelnen kiobuf, aber wenn das notwendig ist, dann kann man das mit kiobuf_init machen:


void kiobuf_init(struct kiobuf *iobuf);



Normalerweise werden kiobufs in Gruppen als Bestandteil eines Kernel-I/O-Vektors (kiovec) alloziert. Ein kiovec kann durch Aufruf von alloc_kiovec in einem Schritt alloziert und initialisiert werden:


int alloc_kiovec(int nr, struct kiobuf **iovec);

Der Rückgabewert ist wie üblich 0 oder ein Fehler-Code. Wenn Ihr Code die kiovec-Struktur nicht mehr braucht, sollte er sie natürlich an das System zurückgeben:


void free_kiovec(int nr, struct kiobuf **);

Der Kernel stellt ein Funktionspaar zum Sperren und Entsperren der in einem kiovec eingeblendeten Seiten bereit:


int lock_kiovec(int nr, struct kiobuf *iovec[], int wait);
int unlock_kiovec(int nr, struct kiobuf *iovec[]);

Für die meisten Anwendungen von kiobufs in Gerätetreibern ist das Sperren eines kiovecs aber nicht notwendig.

Einblenden von User-Space-Buffern und rohe I/O

Unix-Systeme stellen seit langem eine “rohe” Schnittstelle zu manchen Geräten — insbesondere zu Block-Geräten — bereit, die I/O direkt aus einem User-Space-Buffer durchführt und das Kopieren der Daten durch den Kernel hindurch vermeidet. In manchen Fällen verbessert dies die Performance deutlich, insbesondere wenn die übertragenen Daten in der nahen Zukunft nicht mehr benötigt werden. Beispielsweise lesen Festplatten-Backups einen großen Teil der Daten genau einmal und vergessen sie dann. Backups über eine rohe Schnittstelle füllen den Puffer Cache des Systems nicht mit nutzlosen Daten.

Der Linux-Kernel enthielt traditionell keine rohe Schnittstelle. Dafür gibt es eine Reihe von Gründen. Aber mit zunehmender Beliebtheit von Linux werden immer mehr Applikationen (wie große Datenbanksysteme) portiert, die erwarten, rohe I/O durchführen zu können. Daher ist in der 2.3-Entwicklungsserie endlich rohe I/O hinzugekommen; dieses Bedürfnis war auch die treibende Kraft bei der Entwicklung der kiobuf-Schnittstelle.

Rohe I/O bringt nicht immer die gewaltigen Performance-Gewinne mit sich, die manche Leute erwarten; Autoren von Gerätetreibern sollten jetzt nicht einfach diese Fähigkeit anbieten, nur weil das möglich ist. Es kann kompliziert sein, eine rohe Übertragung einzurichten, und die Vorteile des Pufferns von Daten im Kernel gehen auch verloren. Denken Sie beispielsweise daran, daß rohe I/O-Operationen fast immer synchron sein müssen — der write-Systemaufruf darf nicht zurückkehren, bevor die Operation vollständig abgearbeitet ist. Linux hat derzeit keine Mechanismen, die es User-Space-Programmen erlauben würden, eine sichere asynchrone rohe I/O auf einem User-Buffer durchzuführen.

In diesem Beispiel fügen wir rohe I/O zum Beispiel-Treiber sbull hinzu. Wenn kiobufs zur Verfügung stehen, registriert sbull gleich zwei Geräte. Das Block-Gerät sbull haben wir uns in Kapitel 12 genau angeschaut. Was wir dort aber nicht gesehen haben, war ein zweites, zeichenorientiertes Gerät (namens sbullr), das rohen Zugriff auf die RAM-Disk ermöglicht. Daher greifen /dev/sbull0 und /dev/sbullr0 auf den gleichen Speicher zu, ersteres über den traditionellen, gepufferten Modus, letzteres mit rohem Zugriff über den kiobuf-Mechanismus.

Man sollte noch erwähnen, daß Block-Treiber auf Linux-Systemen diese Art von Schnittstelle nicht anbieten müssen. Das rohe Gerät in drivers/char/raw.c stellt diese Fähigkeit auf eine elegante, allgemeine Art und Weise für alle Block-Geräte bereit. Die Block-Treiber müssen nicht einmal wissen, daß sie rohe I/O betreiben. Der Code für die rohe I/O in sbull ist im wesentlichen eine Vereinfachung des rohen Geräte-Codes zu Demonstrationszwecken.

Rohe I/O auf ein Block-Gerät muß immer an Sektorengrenzen ausgerichtet und die Länge ein Vielfaches der Sektorgröße sein. Andere Geräte wie etwa Bandlaufwerke haben diese Einschränkungen möglicherweise nicht. sbullr verhält sich wie ein Block-Gerät und erzwingt die Ausrichtungs- und Längenanforderungen. Dazu werden einige Symbole definiert:


#  define SBULLR_SECTOR 512  /* hierauf bestehen */
#  define SBULLR_SECTOR_MASK (SBULLR_SECTOR - 1)
#  define SBULLR_SECTOR_SHIFT 9

Das rohe Gerät sbullr wird nur dann registriert, wenn die Hardware-Sektorengröße gleich SBULLR_SECTOR ist. Es gibt keinen Grund, warum man größere Hardware-Sektoren nicht unterstützen sollte, aber das würde den Beispiel-Code nur unnötig verkomplizieren.

Die Implementation von sbullr fügt dem existierenden sbull-Code nur wenig hinzu. Insbesondere können die open- und close-Methoden von sbull ohne Veränderung übernommen werden. Weil sbullr ein Zeichen-Gerät ist, braucht es aber read- und write-Methoden. Beide verwenden eine einzige Übertragungsfunktion und sind folgendermaßen definiert:


ssize_t sbullr_read(struct file *filp, char *buf, size_t size,
                    loff_t *off)
{
    Sbull_Dev *dev = sbull_devices +
                    MINOR(filp->f_dentry->d_inode->i_rdev);
    return sbullr_transfer(dev, buf, size, off, READ);
}

ssize_t sbullr_write(struct file *filp, const char *buf, size_t size,
                loff_t *off)
{
    Sbull_Dev *dev = sbull_devices +
                    MINOR(filp->f_dentry->d_inode->i_rdev);
    return sbullr_transfer(dev, (char *) buf, size, off, WRITE);
}

Die Funktion sbullr_transfer kümmert sich um das gesamte Einrichten und Abbauen und läßt die eigentliche Übertragung dann von noch einer weiteren Funktion ausführen. Der Code sieht wie folgt aus:


static int sbullr_transfer (Sbull_Dev *dev, char *buf, size_t count,
                loff_t *offset, int rw)
{
    struct kiobuf *iobuf;
    int result;

    /* Nur Block-Ausrichtung und -Groeße erlaubt */
    if ((*offset & SBULLR_SECTOR_MASK) || (count & SBULLR_SECTOR_MASK))
        return -EINVAL;
    if ((unsigned long) buf & SBULLR_SECTOR_MASK)
        return -EINVAL;

    /* Einen I/O-Vektor allozieren */
    result = alloc_kiovec(1, &iobuf);
    if (result)
        return result;

    /* Den User-I/O-Buffer einblenden und die I/O durchführen. */
    result = map_user_kiobuf(rw, iobuf, (unsigned long) buf, count);
    if (result) {
        free_kiovec(1, &iobuf);
        return result;
    }
    spin_lock(&dev->lock);
    result = sbullr_rw_iovec(dev, iobuf, rw,
                    *offset >> SBULLR_SECTOR_SHIFT,
                    count >> SBULLR_SECTOR_SHIFT);
    spin_unlock(&dev->lock);

    /* Aufraeumen und zurueckkehren. */
    unmap_kiobuf(iobuf);
    free_kiovec(1, &iobuf);
    if (result > 0)
        *offset += result << SBULLR_SECTOR_SHIFT;
    return result << SBULLR_SECTOR_SHIFT;
}

Nach einigen Sicherheitsüberprüfungen erzeugt der Code mit alloc_kiovec einen kiovec (der einen einzelnen kiobuf enthält). Dann wird dieser kiovec verwendet, um den User-Buffer durch Aufruf von map_user_kiobuf einzublenden:


int map_user_kiobuf(int rw, struct kiobuf *iobuf,
                    unsigned long address, size_t len);

Das Ergebnis dieses Aufrufs ist, wenn alles gut geht, daß der Puffer an der angegebenen (User-virtuellen) Adresse address mit der Länge len in den angegebenen iobuf eingeblendet worden ist. Diese Operation kann schlafen, weil es möglich ist, daß der User-Buffer erst in den Speicher geholt werden muß.

Ein auf diese Weise eingeblendeter kiobuf muß irgendwann natürlich auch wieder ausgeblendet werden, damit die Referenzzähler der Seiten korrekt bleiben. Dies geschieht, wie im Code zu sehen ist, durch Übergeben des kiobuf an unmap_kiobuf.

Bisher haben wir gesehen, wie man einen kiobuf für I/O vorbereitet, aber nicht, wie man die I/O dann am Ende durchführt. Im letzten Schritt gehen wir alle Seiten im kiobuf durch und führen die gewünschten Datenübertragungen aus; in sbullr macht das die Funktion sbullr_rw_iovec. Im wesentlichen geht diese Funktion alle Seiten nacheinander durch, zerlegt jede Seite in sektorengroße Stückchen und übergibt diese mittels einer unechten request-Struktur an sbull_transfer:


static int sbullr_rw_iovec(Sbull_Dev *dev, struct kiobuf *iobuf, int rw,
                int sector, int nsectors)
{
    struct request fakereq;
    struct page *page;
    int offset = iobuf->offset, ndone = 0, pageno, result;

    /* I/O auf jedem Sektor durchfuehren */
    fakereq.sector = sector;
    fakereq.current_nr_sectors = 1;
    fakereq.cmd = rw;

    for (pageno = 0; pageno < iobuf->nr_pages; pageno++) {
        page = iobuf->maplist[pageno];
        while (ndone < nsectors) {
            /* Eine unechte request-Struktur für die Operation zusammenbasteln */
            fakereq.buffer = (void *) (kmap(page) + offset);
            result = sbull_transfer(dev, &fakereq);
            kunmap(page);
            if (result == 0)
                return ndone;
            /* Und zum naechsten */
            ndone++;
            fakereq.sector++;
            offset += SBULLR_SECTOR;
            if (offset >= PAGE_SIZE) {
                offset = 0;
                break;
            }
        }
    }
    return ndone;
}

Das Feld nr_pages der Struktur kiobuf teilt uns hier mit, wie viele Seiten übertragen werden müssen. Über das Array maplist kommen wir dann an die einzelnen Seiten heran und müssen diese nur noch durchlaufen. Beachten Sie aber, daß kmap verwendet wird, um eine Kernel-virtuelle Adresse für jede Seite zu bekommen;. Auf diese Weise funktioniert die Funktion auch dann noch, wenn sich der User-Buffer im hohen Speicher befindet.

Einige schnelle Tests mit dem Kopieren von Daten zeigen, daß eine Kopie von oder aus dem sbullr-Gerät etwa zwei Drittel der Systemzeit wie die entsprechende Operation auf dem sbull-Gerät benötigt. Diese Einsparung erzielt man durch das Vermeiden der zusätzlichen Kopie durch den Puffer Cache. Wenn die gleichen Daten mehrfach gelesen werden, verschwindet die Ersparnis — insbesondere bei echter Hardware. Roher Gerätezugriff ist oft nicht der beste Ansatz, kann aber für manche Applikationen eine deutliche Verbesserung bedeuten.

> > > > > > > > Obwohl kiobufs in der Kernel-Entwickler-Gemeinde ein kontroverses Thema bleiben, besteht ein Interesse daran, sie in einem größeren Kontext zu nutzen. Beispielsweise gibt es einen Patch, der Unix-Pipes mit kiobufs implementiert — Daten werden direkt vom Adreßraum eines Prozesses in den Adreßraum eines anderen Prozesses kopiert, ohne daß im Kernel etwas gepuffert wird. Außerdem gibt es einen Patch, mit dem man einen kiobuf leicht dazu verwenden kann, virtuellen Kernel-Speicher in den Adreßraum eines Prozesses einzublenden, was die oben gezeigte nopage-Implementation unnötig machen würde.