Die Geräteoperation mmap

Das Einblenden von Speicher (memory mapping) ist eines der interessantesten Merkmale moderner Unix-Systeme. Bei Treibern kann diese Funktionalität eingesetzt werden, um Anwenderprogrammen direkten Zugriff auf den Gerätespeicher zu geben.

Ein sehr gutes Beispiel für die Verwendung von mmap bekommen Sie, wenn Sie sich einen Teil des virtuellen Speicherbereiches des X Window System-Servers anschauen:

cat /proc/731/maps
08048000-08327000 r-xp 00000000 08:01 55505    /usr/X11R6/bin/XF86_SVGA
08327000-08369000 rw-p 002de000 08:01 55505    /usr/X11R6/bin/XF86_SVGA
40015000-40019000 rw-s fe2fc000 08:01 10778    /dev/mem
40131000-40141000 rw-s 000a0000 08:01 10778    /dev/mem
40141000-40941000 rw-s f4000000 08:01 10778    /dev/mem
     ...

Die vollständige Liste der VMAs des X Servers ist recht lang, aber die meisten Einträge sind hier gar nicht von Interesse. Wir sehen hier aber drei separate Einblendungen von /dev/mem, die uns einen kleinen Einblick in die Art und Weise gestatten, wie der X Server mit der Grafikkarte zusammenarbeitet. Die erste Einblendung zeigt einen Bereich von 16 KByte, der an der Adresse fe2fc000 eingeblendet ist. Diese Adresse liegt weit über der höchsten RAM-Adresse des Systems; es handelt sich statt dessen um einen Speicherbereich auf einem PCI-Gerät (der Grafikkarte). Vermutlich ist das ein Steuerbereich für die Karte. Die mittlere Einblendung befindet sich bei a0000, der Standardposition für den Videospeicher im 640-KByte-ISA-Loch. Die letzte /dev/mem-Einblendung liegt bei f4000000 und ist recht groß; das ist der Videospeicher selbst. Diese Regionen kann man auch in /proc/iomem sehen:

000a0000-000bffff : Video RAM area
f4000000-f4ffffff : Matrox Graphics, Inc. MGA G200 AGP
fe2fc000-fe2fffff : Matrox Graphics, Inc. MGA G200 AGP

Das Einblenden eines Gerätes erfolgt dadurch. daß ein Bereich von Adressen im User-Space mit dem Gerätespeicher verknüpft wird. Immer, wenn das Programm in den zugewiesenen Adreßbereich schreibt oder daraus liest, greift es in Wirklichkeit auf das Gerät zu. Im Beispiel mit dem X Server bekommt man durch die Verwendung von mmap schnellen und einfachen Zugriff auf den Speicher der Grafikkarte. In einer Performance-kritischen Anwendung wie dieser macht der direkte Zugriff einen großen Unterschied aus.

Wie Sie sich vielleicht schon denken können, ist nicht jedes Gerät für die mmap-Abstraktion geeignet; sie ist beispielsweise bei seriellen Ports und anderen datenstrom-orientierten Geräten wenig sinnvoll. Eine weitere Beschränkung von mmap besteht darin, daß die Granularität PAGE_SIZE ist. Der Kernel kann virtuelle Adressen nur auf der Ebene der Seitentabellen verteilen, weswegen der eingeblendete Speicherbereich ein Vielfaches von PAGE_SIZE sein muß und im physikalischen Speicher an einer Adresse beginnen muß, die ebenfalls ein Vielfaches von PAGE_SIZE ist. Der Kernel kümmert sich darum, indem er einen Bereich geringfügig größer macht, wenn die Größe kein Vielfaches der Seitengröße ist.

Diese Einschränkungen sind für Treiber nicht besonders unangenehm, weil das Programm, das auf das Gerät zugreift, ohnehin geräteabhängig ist. Es muß wissen, wie es den eingeblendeten Speicherbereich interpretieren soll, so daß die Ausrichtung an PAGE_SIZE kein Problem ist. Wenn Sie ISA-Karten in Nicht-x86-Rechner stecken, gibt es dagegen eine größere Einschränkung, weil die Hardware-Sicht dieser Rechner auf den ISA-Speicher möglicherweise nicht zusammenhängend ist. Beispielsweise sehen manche Alpha-Computer den ISA-Speicher als verstreute Menge von 8-Bit-, 16-Bit- und 32-Bit-Elementen ohne direkte Einblendung. In diesem Fall können Sie mmap überhaupt nicht verwenden. Die inkompatiblen Spezifikationen für den Datentransfer auf den beiden Systemen sind der Grund dafür, daß diese Abbildung von ISA-Adressen auf Alpha-Adressen nicht möglich ist. Während frühe Alpha-Systeme nur 32-Bit- und 64-Bit-Speicherzugriffe durchführen konnten, sind auf dem ISA-Bus nur 8-Bit- und 16-Bit-Übertragungen möglich; es gibt keine Möglichkeit, ein Protokoll transparent auf das jeweils andere abzubilden.

Es gibt gute Gründe dafür, mmap zu verwenden, wenn das möglich ist. Beispielsweise überträgt ein Programm wie der X Server große Mengen an Daten aus dem Videospeicher. Das Einblenden des Videospeichers in den User Space verbessert den Durchsatz dramatisch im Vergleich zu einer Implementation mit lseek/write. Ein Programm, das ein PCI-Gerät steuert, ist ein weiteres typisches Beispiel. Die meisten PCI-Peripherie-Geräte bilden ihre Kontrollregister auf eine Speicheradresse ab, und eine anspruchsvolle Anwendung könnte es vorziehen, direkt auf die Register zuzugreifen, anstatt immer wieder ioctl aufrufen zu müssen.

Die Methode mmap ist ein Teil der Struktur file_operations und wird aufgerufen, wenn der Systemaufruf mmap ausgeführt wird. Bei mmap erledigt der Kernel schon einen großen Teil der Arbeit, bevor die eigentliche Methode aufgerufen wird; daher sieht der Prototyp der Methode ziemlich anders aus als der Systemaufruf. Das ist anders als bei Aufrufen wie ioctl und select, bei denen der Kernel nicht so besonders viel tut, bevor er die Methode aufruft.

Der Systemaufruf ist folgendermaßen deklariert (und wird in der Man-Page mmap(2) beschrieben):


mmap (caddr_t addr, size_t len, int prot, int flags, int fd,
      off_t offset)

Die Datei-Operation ist dagegen deklariert:


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

deklariert. Das Argument filp in dieser Methode ist das gleiche wie das, das in Kapitel 3 eingeführt wurde, während vma Informationen über den virtuellen Adreßbereich enthält, über den auf das Gerät zugegriffen wird. Der Kernel hat bereits einen Großteil der Arbeit erledigt; um mmap zu implementieren, muß der Treiber lediglich passende Seitentabellen für den Adreßbereich aufbauen und wenn nötig vma->vm_ops durch eine neue Menge an Operationen ersetzen.

Es gibt zwei Möglichkeiten, Seitentabellen aufzubauen: alles auf einmal mit einer Funktion namens remap_page_range erledigen oder seitenweise mit dem nopage-VMA-Ansatz arbeiten. Beide Methoden haben ihre Vorteile. Wir fangen hier mit der "Alles auf einmal"-Methode an, die einfacher ist. Dann machen wir die Sache komplizierter, um auch die Anforderungen von realen Applikationen zu erfüllen.

remap_page_range verwenden

Das Aufbauen neuer Seitentabellen, um einen Bereich physikalischer Adressen einzublenden, wird durch remap_page_range erledigt, das den folgenden Prototyp hat:


int remap_page_range(unsigned long virt_add, unsigned long phys_add,
                     unsigned long size, pgprot_t prot);

Der Rückgabewert der Funktion ist wie üblich 0 oder ein negativer Fehler-Code. Schauen wir uns die genaue Bedeutung der Argumente der Funktion an:

virt_add

Die virtuelle User-Adresse, an der die Einblendung anfangen soll. Die Funktion baut die Seitentabellen für den virtuellen Adreßbereich zwischen virt_add und virt_add+size auf.

phys_add

Die physikalische Adresse, an der die virtuelle Adresse eingeblendet werden soll. Die Funktion betrifft physikalische Adressen von phys_add bis phys_add+size.

size

Die Größe des einzublendenden Bereichs in Bytes.

prot

Der für die neue VMA gewünschte “Schutz”. Treiber können (und sollten) den in vma->vm_page_prot stehenden Wert verwenden.

Die Argumente von remap_page_range sind ziemlich offensichtlich; die meisten stehen Ihnen bereits in der VMA zur Verfügung, wenn Ihre mmap-Methode aufgerufen wird. Kompliziert wird es lediglich, wenn Caching ins Spiel kommt: Referenzen auf Gerätespeicher sollten normalerweise nicht vom Prozessor zwischengespeichert werden. Oft richtet das BIOS des Systems das schon korrekt ein, aber es ist auch möglich, das Caching bestimmter VMAs über das “Schutz”-Feld abzuschalten. Leider ist das Abschalten des Cachings auf dieser Ebene stark prozessorabhängig. Neugierige Leser können einen Blick auf die Funktion pgprot_noncached in drivers/char/mem.c werfen, um zu sehen, was dazugehört. Wir werden das hier nicht weiter erläutern.

Eine einfache Implementation

Wenn Ihr Treiber eine einfache, lineare Einblendung von Gerätespeicher in den User-Adreßraum benötigt, dann kommen Sie mit remap_page_range fast schon vollständig aus. Der folgende Code stammt aus drivers/char/mem.c und zeigt Ihnen, wie diese Aufgabe in einem typischen Modul namens simple (Simple Implementation Mapping Pages with Little Enthusiasm) erledigt wird:


 #include <linux/mm.h>

 int simple_mmap(struct file *filp, struct vm_area_struct *vma)
 {
     unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;

     if (offset >= _&thinsp;_pa(high_memory) || (filp->f_flags & O_SYNC))
         vma->vm_flags |= VM_IO;
     vma->vm_flags |= VM_RESERVED;

     if (remap_page_range(vma->vm_start, offset,
            vma->vm_end-vma->vm_start, vma->vm_page_prot))
         return -EAGAIN;
     return 0;
 }

Der /dev/mem-Code überprüft, ob der gewünschte Offset (der in vma->vm_pgoff gespeichert ist), jenseits des physikalischen Speichers liegt; wenn das der Fall ist, wird das VMA-Flag VM_IO gesetzt, um den Bereich als I/O-Speicher zu kennzeichnen. Das Flag VM_RESERVED wird immer gesetzt, um das System daran zu hindern, diesen Bereich auszulagern. Anschließend muß man einfach nur noch remap_page_range aufrufen, um die notwendigen Seitentabellen zu erzeugen.

VMA-Operationen hinzufügen

Wie wir bereits gesehen haben, enthält die Struktur vm_area_struct einen Satz von Operationen, der auf die VMA angewendet werden kann. Wir schauen uns diese Operationen jetzt in vereinfachter Form an; ein detaillierteres Beispiel folgt später.

Wir werden hier open- und close-Operationen für unsere VMA bereitstellen. Diese Operationen werden immer dann aufgerufen, wenn ein Prozeß die VMA öffnet oder schließt. Insbesondere wird die open-Methode jedesmal aufgerufen, wenn ein Prozeß sich mit fork verzweigt und eine neue Referenz auf die VMA erzeugt. Die VMA-Methoden open und close werden zusätzlich zur Verarbeitung durch den Kernel aufgerufen, müssen also die da bereits getane Arbeit nicht wiederholen. Sie existieren, um dem Treiber die Möglichkeit zu geben, eventuell notwendige zusätzliche Verarbeitungsschritte durchzuführen.

Wir werden diese Methoden verwenden, um den Verwendungszähler des Moduls zu inkrementieren, wenn die VMA geöffnet wird, und ihn zu dekrementieren, wenn sie geschlossen wird. In modernen Kerneln ist das nicht unbedingt notwendig; der Kernel ruft die release-Methode des Treibers so lange nicht auf, wie die VMA geöffnet bleibt; der Verwendungszähler des Moduls geht also nicht auf 0, solange nicht alle Referenzen auf die VMA geschlossen sind. Der 2.0-Kernel macht das aber nicht, weswegen portabler Code immer noch den Verwendungszähler pflegen sollte.

Wir überschreiben also den Default-Wert von vma->vm_ops mit Operationen, die den Verwendungszähler pflegen. Der Code ist ziemlich einfach — eine vollständige mmap-Implementation für ein modularisiertes /dev/mem sieht folgendermaßen aus:


void simple_vma_open(struct vm_area_struct *vma)
{ MOD_INC_USE_COUNT; }

void simple_vma_close(struct vm_area_struct *vma)
{ MOD_DEC_USE_COUNT; }

static struct vm_operations_struct simple_remap_vm_ops = {
    open:  simple_vma_open,
    close: simple_vma_close,
};

int simple_remap_mmap(struct file *filp, struct vm_area_struct *vma)
{
    unsigned long offset = VMA_OFFSET(vma);

    if (offset >= _ _pa(high_memory) || (filp->f_flags & O_SYNC))
        vma->vm_flags |= VM_IO;
    vma->vm_flags |= VM_RESERVED;

    if (remap_page_range(vma->vm_start, offset, vma->vm_end-vma->vm_start,
                vma->vm_page_prot))
        return -EAGAIN;

    vma->vm_ops = &simple_remap_vm_ops;
    simple_vma_open(vma);
    return 0;
}

Dieser Code verläßt sich darauf, daß der Kernel das Feld vm_ops im neu erzeugten Bereich mit NULL initialisiert, bevor er f_op->mmap aufruft. Der gerade gezeigte Code überprüft den aktuellen Wert des Zeigers als Sicherheitsmaßnahme, falls sich in zukünftigen Kerneln etwas ändern sollte.

Das merkwürdige Makro VMA_OFFSET, das in diesem Code auftaucht, wird dazu verwendet, die Unterschiede in den vma-Strukturen einzelner Kernel-Versionen zu verstecken. Weil der Offset in Version 2.4 in Seiten, in 2.2 und früheren Versionen aber in Bytes ausgedrückt wird, deklariert <sysdep.h> dieses Makro, um den Unterschied transparent zu machen (das Ergebnis wird dann in Bytes angegeben).

Speicher mit nopage einblenden

Auch wenn remap_page_range für viele — wenn nicht für die meisten — mmap-Implementationen in Treibern ausreichend ist, braucht man manchmal doch ein wenig mehr Flexibilität. In solchen Situationen kann eine Implementation mit der VMA-Methode nopage angebracht sein.

Die Methode nopage hat, wie Sie sich vielleicht erinnern werden, den folgenden Prototyp:


 struct page (*nopage)(struct vm_area_struct *vma,
                     unsigned long address, int write_access);

Wenn ein Benutzer versucht, auf eine Seite in einer VMA zuzugreifen, die sich nicht im Speicher befindet, wird die zugehörige nopage-Funktion aufgerufen. Der Parameter address enthält die virtuelle Adresse, die den Fehler verursacht hat, abgerundet auf den Seitenanfang. Die Funktion nopage muß dann den struct page-Zeiger, der auf die gewünschte Seite verweist, finden und zurückgeben. Außerdem muß die Funktion den Verwendungszähler der zurückgegebenen Seite durch den Aufruf des Makros get_page inkrementieren:


 get_page(struct page *pageptr);

Dieser Schritt ist notwendig, damit die Referenzzähler eingeblendeter Seiten korrekt bleiben. Der Kernel verwaltet diesen Zähler für jede Seite. Wenn der Zähler auf 0 geht, weiß der Kernel, daß die Seite in die Liste der freien Seiten gestellt werden kann. Wenn die Einblendung einer VMA aufgehoben wird, dekrementiert der Kernel den Verwendungszähler für jede Seite im Bereich. Wenn Ihr Treiber den Verwendungszähler beim Hinzufügen einer Seite zum Bereich nicht inkrementiert, fällt der Verwendungszähler zu früh auf 0 und die Integrität des Systems gerät in Gefahr.

Eine Situation, in der der nopage-Ansatz nützlich ist, kann durch den Systemaufruf mremap hervorgerufen werden, der von Applikationen verwendet wird, um die Grenzadressen eines eingeblendeten Bereichs zu ändern. Wenn der Treiber mit mremap umgehen können soll, funktioniert die vorhergehende Implementation nicht korrekt, weil der Treiber nicht wissen kann, daß sich der eingeblendete Bereich geändert hat.

Die Linux-Implementation von mremap benachrichtigt den Treiber nicht über Änderungen im eingeblendeten Bereich. Der Treiber wird allerdings durchaus benachrichtigt, wenn die Größe des Bereichs mit der Methode unmap reduziert wird, aber es erfolgt keine Benachrichtigung, wenn die Größe ansteigt.

Die grundlegende Idee, warum ein Treiber über eine Verkleinerung benachrichtigt wird, besteht darin, daß der Treiber (oder das Dateisystem, das eine normale Datei in den Speicher einblendet) wissen muß, wenn die Einblendung eines Bereichs aufgehoben wird, um passende Maßnahmen (wie etwa das Herausschreiben von Seiten auf die Festplatte) ergreifen zu können. Das Wachsen des eingeblendeten Bereichs hat dagegen für den Treiber so lange keine Bedeutung, bis das Programm, das mremap aufruft, auf die neuen virtuellen Adressen zugreift. Im wirklichen Leben ist es nicht ungewöhnlich, Bereiche einzublenden, die nie verwendet werden (wie etwa unbenutzte Abschnitte im Programm-Code). Der Linux-Kernel benachrichtigt daher den Treiber nicht, wenn der eingeblendete Bereich wächst, weil sich die nopage-Methode eine nach der anderen um die Seiten kümmert, wenn auf diese tatsächlich zugegriffen wird.

Der Treiber wird also mit anderen Worten nicht benachrichtigt, wenn eine Einblendung wächst, weil nopage das später erledigt, ohne daß der Speicher benutzt werden muß, bevor er wirklich gebraucht wird. Diese Optimierung ist vor allem für normale Dateien gedacht, deren Einblendung echtes RAM verbraucht.

Die Methode nopage muß daher implementiert werden, wenn Sie den Systemaufruf mremap unterstützen wollen. Aber wenn Sie schon einmal nopage haben, dann können sie dies mit einigen Einschränkungen (die wir später beschreiben) auch ausgiebig verwenden. Diese Methode wird im nächsten Code-Fragment gezeigt. In dieser Implementation von mmap ersetzt die Gerätemethode nur vma->vm_ops. Die Methode nopage kümmert sich um das seitenweise erneute Einblenden und gibt die Adresse ihrer struct page-Struktur zurück. Weil wir hier nur ein Fenster auf den physikalischen Speicher implementieren, ist der verbleibende Schritt einfach — wir müssen nur einen Zeiger auf die struct page für die gewünschte Adresse finden und zurückgeben.

Eine Implementation von /dev/mem, die nopage verwendet, sieht folgendermaßen aus:


struct page *simple_vma_nopage(struct vm_area_struct *vma,
                unsigned long address, int write_access)
{
    struct page *pageptr;
    unsigned long physaddr = address - vma->vm_start + VMA_OFFSET(vma);
    pageptr = virt_to_page(_ _va(physaddr));
    get_page(pageptr);
    return pageptr;
}

int simple_nopage_mmap(struct file *filp, struct vm_area_struct *vma)
{
    unsigned long offset = VMA_OFFSET(vma);

    if (offset >= _ _pa(high_memory) || (filp->f_flags & O_SYNC))
        vma->vm_flags |= VM_IO;
    vma->vm_flags |= VM_RESERVED;

    vma->vm_ops = &simple_nopage_vm_ops;
    simple_vma_open(vma);
    return 0;
}

Weil wir wieder einmal einfach Hauptspeicher einblenden, muß die Funktion nopage nur die korrekte struct page für die nicht vorhandene Adresse suchen und deren Referenzzähler erhöhen. Also muß folgendes erledigt werden: Wir müssen die gewünschte physikalische Adresse berechnen, diese Adresse dann mit __va in eine logische Adresse umwandeln und anschließend diese logische Adresse mit virt_to_page in eine struct page umwandeln. Es wäre auch möglich, direkt von der physikalischen Adresse zur struct page zu gelangen, aber der Code dafür wäre nur schwer über verschiedene Architekturen hinweg portabel zu halten. Solcher Code wäre aber notwendig, wenn wir versuchen würden, hohen Speicher einzublenden, der, wie Sie sich erinnern werden, keine logischen Adressen hat. simple ist, wie der Name sagt, einfach und kümmert sich deswegen nicht um diese (seltenen) Fälle.

Wenn die Methode nopage auf NULL belassen wird, blendet der Kernel-Code, der sich um Seitenfehler kümmert, die Zero-Page auf die fehlende virtuelle Adresse ein. Die Zero-Page ist eine Copy-on-write-Seite, die als Null gelesen wird und die beispielsweise verwendet wird, um das BSS-Segment einzublenden. Wenn daher ein Prozeß einen eingeblendeten Bereich durch Aufruf von mremap erweitert und der Treiber nopage nicht implementiert hat, dann bekommt der Prozeß die Zero-Page anstelle eines Segmentationsfehlers.

Die Methode nopage gibt normalerweise einen Zeiger auf eine struct page zurück. Wenn aus irgendeinem Grund keine normale Seite zurückgegeben werden kann (etwa weil die angeforderte Adresse jenseits des Speicherbereichs des Geräts liegt), dann kann auch NOPAGE_:SIGBUS zurückgegeben werden, um den Fehler anzuzeigen. Außerdem kann nopage NOPAGE_OOM zurückgeben, um Fehler aufgrund mangelnder Ressourcen zu melden.

> > > > Beachten Sie, daß diese Implementation für ISA-Speicherbereiche funktioniert, aber nicht für solche auf dem PCI-Bus. PCI-Speicher wird oberhalb der höchsten Adresse des Systemspeichers eingeblendet, und es gibt in der System-Speichertabelle keine Einträge für diese Adressen. Und weil es keine struct page gibt, deren Zeiger zurückgegeben werden könnte, kann nopage in diesen Situationen nicht verwendet werden; Sie müssen statt dessen remap_page_range benutzen.

Umblenden bestimmter I/O-Regionen

Alle Beispiele, die wir bisher gesehen haben, sind Reimplementationen von /dev/mem; Sie blenden physikalische Adressen erneut in den User-Space ein. Der typische Treiber will dagegen nur einen kleinen Adreßbereich einblenden, der zu seinem Peripherie-Gerät gehört, aber nicht den gesamten Speicher. Um nur eine Teilmenge des gesamten Speicherbereichs auf den User-Space einzublenden, muß der Treiber nur etwas an den Offsets drehen. Die folgenden Zeilen erledigen das für einen Treiber, der einen Bereich von simple_region_size Bytes einblendet, welcher bei der physikalischen Adresse simple_region_start beginnt (was an einer Seitengrenze liegen sollte).


unsigned long off = vma->vm_pgoff << PAGE_SHIFT;
unsigned long physical = simple_region_start + off;
unsigned long vsize = vma->vm_end - vma->vm_start;
unsigned long psize = simple_region_size - off;

if (vsize > psize)
    return -EINVAL; /*  Speicherbereich zu groß */
remap_page_range(vma_>vm_start, physical, vsize, vma->vm_page_prot);

Neben dem Berechnen der Offsets überprüft dieser Code, ob das Programm versucht, mehr Speicher einzublenden, als sich im I/O-Bereich des Zielgeräts befindet, und meldet in diesem Fall einen Fehler zurück. psize ist in diesem Code die physikalische I/O-Größe, die nach Angabe des Offsets noch übrigbleibt; vsize ist die angeforderte Größe des virtuellen Speichers; die Funktion weigert sich, Adressen einzublenden, die außerhalb des erlaubten Speicherbereichs liegen.

Beachten Sie, daß der Benutzer-Prozeß immer mremap verwenden kann, um seine Einblendung zu erweitern, möglicherweise sogar über das Ende des physikalischen Geräts hinaus. Wenn Ihr Treiber keine nopage-Methode hat, wird er davon nie benachrichtigt, und der zusätzliche Bereich wird auf die Zero-Page eingeblendet. Als Autor vom Gerätetreibern wollen Sie ein solches Verhalten vermutlich vermeiden; das Einblenden der Zero-Page auf das Ende Ihres Bereichs ist nicht nur eindeutig etwas Schlechtes, sondern auch vermutlich nicht das, was der Programmierer wollte.

Am einfachsten kann man ein Erweitern der Einblendung durch das Implementieren einer einfachen nopage-Methode verhindern, die dem Prozeß, in dem der Seitenfehler aufgetreten ist, immer ein Bus-Signal schickt. Eine solche Methode würde folgendermaßen aussehen:


struct page *simple_nopage(struct vm_area_struct *vma,
                           unsigned long address, int write_access);
{ return NOPAGE_SIGBUS; /* SIGBUS schicken */}

RAM umblenden

Eine gründlichere Implementation könnte natürlich kontrollieren, ob sich der gewünschte Bereich im Gerät befindet, und falls dem so ist, das Umblenden durchführen. Aber auch hier funktioniert nopage nicht bei PCI-Speicherbereichen, so daß Erweiterungen von PCI-Einblendungen nicht möglich sind. Unter Linux wird eine Seite mit physikalischen Adressen in der Speichertabelle als “reserviert” markiert, um zu kennzeichnen, daß sie für die Speicherverwaltung nicht zur Verfügung steht. Auf dem PC gilt das beispielsweise für den Bereich von 640 KByte bis 1 MByte sowie für die Seiten, die den Kernel-Code selbst enthalten.

Es ist eine interessante Beschränkung von remap_page_range, daß mit dieser Funktion nur Zugriff auf reservierte Seiten und physikalische Adressen oberhalb des physikalischen Speichers möglich ist. Reservierte Seiten sind im Speicher gesperrt, und nur diese können sicher in den User-Space eingeblendet werden. Diese Einschränkung ist eine grundlegende Bedingung für die Systemstabilität.

Daher läßt remap_page_range es nicht zu, konventionelle Adressen umzublenden — wozu auch diejenigen gehören, die Sie von get_free_page bekommen. Statt dessen bekommen Sie die Zero-Page. Gleichwohl macht die Funktion aber alles, was Hardware-Treiber von ihr benötigen, weil sie hohe PCI-Puffer und ISA-Speicher umblenden kann.

Die Einschränkungen von remap_page_range kann man mit mapper beobachten, einem der Beispiel-Programme auf dem FTP-Server von O'Reilly. mapper ist ein einfaches Hilfsprogramm, das verwendet werden kann, um schnell den Systemaufruf mmap zu testen. Es blendet nur-lesbare Bereiche einer Datei auf Basis der Kommandozeilenoptionen ein und gibt den eingeblendeten Bereich auf der Standardausgabe aus. Die untenstehende Ausgabe zeigt, daß /dev/mem die physikalische Seite an der Adresse 64 KByte nicht einblendet — statt dessen sehen wir eine Seite voller Nullen (der verwendete Rechner ist in diesem Beispiel ein PC, aber auf anderen Plattformen wäre das Ergebnis das gleiche):


morgana.root# ./mapper /dev/mem 0x10000 0x1000 | od -Ax -t x1
mapped "/dev/mem" from 65536 to 69632
000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
*
001000

Die Unfähigkeit von remap_page_range, mit RAM umzugehen, legt schon nahe, daß ein Gerät wie scullp mmap nicht so einfach implementieren kann, weil sein Gerätespeicher im konventionellen RAM und nicht im I/O-Speicher liegt. Glücklicherweise gibt es einen relativ einfachen Ausweg für alle Treiber, die RAM in den User-Space einblenden müssen; dazu wird die nopage-Methode verwendet, wie schon gezeigt wurde.

RAM mit der nopage-Methode umblenden

Echtes RAM blendet man in den User-Space durch Verwendung von vm_ops->nopage ein, um Seitenfehler einen nach dem anderen zu behandeln. Eine Beispiel-Implementation ist ein Teil des scullp-Moduls, das in Kapitel 7 eingeführt wurde.

scullp ist ein seitenorientiertes Zeichen-Gerät. Weil es seitenorientiert ist, kann es mmap auf seinem Speicher implementieren. Der Code dafür verwendet einige der Konzepte, die in “the Section called Speicherverwaltung in Linux” eingeführt wurden.

Aber bevor wir den Code untersuchen, schauen wir uns die Design-Überlegungen der mmap-Implementation von scullp an.

  • scullp gibt den Gerätespeicher nicht frei, solange das Gerät noch eingeblendet ist. Das ist mehr eine Frage der Policy als eine Anforderung. scullp unterscheidet sich hier von scull und ähnlichen Geräten, die beim Öffnen zum Schreiben auf eine Länge von Null verkürzt werden. Die Weigerung, ein eingeblendetes scullp-Gerät freizugeben, ermöglicht es einem Prozeß, Speicherbereiche zu überschreiben, die gerade von einem anderen Prozeß eingeblendet sind, so daß Sie damit testen können, wie Prozesse und Gerätespeicher interagieren. Um ein eingeblendetes Gerät nicht freizugeben, muß der Treiber die aktiven Einblendungen mitzählen. Dafür wird das vmas-Feld in der Gerätestruktur benutzt.

  • Speichereinblendungen werden nur dann vorgenommen, wenn der Parameter order nicht Null ist. Dieser Parameter beeinflußt den Aufruf von get_free_pages (siehe den Abschnitt “the Section called get_ free_ page und Freunde in Kapitel 7” in Kapitel 7). Diese Wahl wird durch die Interna von get_free_pages, dem von scullp verwendeten Allokationsmechanismus, vorgegeben. Um die Allokations-Performance zu maximieren, verwaltet der Linux-Kernel eine Liste für jede Allokationsgrößenordnung, und nur der Seitenzähler der ersten Seite in einem Bereich wird von get_free_pages inkrementiert und von free_pages dekrementiert. Die Methode mmap wird im scullp-Gerät abgeschaltet, wenn die Allokationsgrößenordnung größer als Null ist, weil nopage nur mit einzelnen Seiten anstelle von Seiten-Clustern arbeitet. Lesen Sie in “” in Kapitel 7 nach, wenn Sie sich nicht mehr sicher sind, wie das mit scullp und der Allokationsgrößenordnung für den Speicher war.

Diese letzte Entscheidung wurde hauptsächlich deswegen getroffen, um den Code einfach zu halten. Es ist möglich, mmap auch für Mehrseiten-Allokationen korrekt zu implementieren, indem die Verwendungszähler der Seiten manipuliert werden, aber das würde nur das Beispiel komplexer machen, ohne interessante Informationen einzuführen.

Wenn mit dem Code RAM entsprechend der dargelegten Regeln eingeblendet werden soll, dann müssen die Methoden open, close und nopage implementiert werden; außerdem muß auf die Speichertabelle zugegriffen werden, um die Verwendungszähler der Seiten zu aktualisieren.

Diese Implementation von scullp_mmap ist sehr kurz, weil alles Interessante von nopage gemacht wird:


 
int scullp_mmap(struct file *filp, struct vm_area_struct *vma)
{
    struct inode *inode = INODE_FROM_F(filp);

    /* Einblendung verweigern, wenn order nicht 0 ist */
    if (scullp_devices[MINOR(inode->i_rdev)].order)
        return -ENODEV;

    /* hier nichts machen, nopage kuemmert sich schon darum */
    vma->vm_ops = &scullp_vm_ops;
    vma->vm_flags |= VM_RESERVED;
    vma->vm_private_data = scullp_devices + MINOR(inode->i_rdev);
    scullp_vma_open(vma);
    return 0;
}

Die Bedingungen am Anfang verhindern das Einblenden von Geräten mit einer Allokationsgrößenordnung ungleich 0. Die Operationen von scullp werden im vm_ops-Feld gespeichert; ein Zeiger auf die Gerätestruktur wird im Feld vm_private_data abgelegt. Am Ende wird vm_ops->open aufgerufen, um den Verwendungszähler des Moduls und den Zähler für die aktiven Einblendungen des Gerätes zu aktualisieren.

open und close kümmern sich lediglich um diese Zähler und sind folgendermaßen definiert:


 
void scullp_vma_open(struct vm_area_struct *vma)
{
    ScullP_Dev *dev = scullp_vma_to_dev(vma);

    dev->vmas++;
    MOD_INC_USE_COUNT;
}

void scullp_vma_close(struct vm_area_struct *vma)
{
    ScullP_Dev *dev = scullp_vma_to_dev(vma);

    dev->vmas--;
    MOD_DEC_USE_COUNT;
}

Die Funktion sculls_vma_to_dev gibt einfach nur den Inhalt des Feldes vm_private_data zurück. Es handelt sich dabei um eine separate Funktion, weil Kernel-Versionen vor 2.4 dieses Feld nicht hatten, weswegen andere Verfahren zum Holen dieses Zeigers notwendig sind. Siehe dazu auch “the Section called Abwärtskompatibilität” am Ende dieses Kapitels.

Die Hauptarbeit wird dann von nopage erledigt. In der Implementation von scullp wird address verwendet, um einen Offset in das Gerät zu berechnen. Mit diesem Offset wird dann die korrekte Seite im Speicherbaum von scullp nachgeschlagen.


 
struct page *scullp_vma_nopage(struct vm_area_struct *vma,
                                unsigned long address, int write)
{
    unsigned long offset;
    ScullP_Dev *ptr, *dev = scullp_vma_to_dev(vma);
    struct page *page = NOPAGE_SIGBUS;
    void *pageptr = NULL; /* Default ist "fehlt" */

    down(&dev->sem);
    offset = (address - vma->vm_start) + VMA_OFFSET(vma);
    if (offset >= dev->size) goto out; /* Außerhalb des Bereichs */

    /*
     * Jetzt das scullp-Geraet aus der Liste holen, danach die
     * Seite. Wenn das Geraet Loecher hat, dann bekommt der Prozess ein
     * SIGBUS, wenn auf das Loch zugegriffen wird.
     */
    offset >>= PAGE_SHIFT; /* Offset ist eine Anzahl von Seiten */
    for (ptr = dev; ptr && offset >= dev->qset;) {
        ptr = ptr->next;
        offset -= dev->qset;
    }
    if (ptr && ptr->data) pageptr = ptr->data[offset];
    if (!pageptr) goto out; /* Loch oder Dateiende */
    page = virt_to_page(pageptr);

    /* Seite bekommen, Zaehler inkrementieren */
    get_page(page);
out:
    up(&dev->sem);
    return page;
}

scullp verwendet mit get_free_pages allozierten Speicher. Dieser Speicher wird mit logischen Adressen angesprochen, weswegen scullp_nopage lediglich virt_to_page aufrufen muß, um einen struct page-Zeiger zu bekommen.

Das Gerät scullp funktioniert nun wie erwartet, wie Sie in der folgenden Ausgabe von mapper sehen können. Wir zeigen hier eine Verzeichnisausgabe von /dev (die lang ist) auf das scullp-Gerät, sowie anschließend die Verwendung des Hilfsprogramms mapper, um uns die Bestandteile dieses Listings mit mmap anschauen zu können.


morgana% ls -l /dev > /dev/scullp
morgana% ./mapper /dev/scullp 0 140
mapped "/dev/scullp" from 0 to 140
total 77
-rwxr-xr-x    1 root     root        26689 Mar  2  2000 MAKEDEV
crw-rw-rw-    1 root     root      14,  14 Aug 10 20:55 admmidi0
morgana% ./mapper /dev/scullp 8192 200
mapped "/dev/scullp" from 8192 to 8392
0
crw———-    1 root     root     113,   1 Mar 26  1999 cum1
crw———-    1 root     root     113,   2 Mar 26  1999 cum2
crw———-    1 root     root     113,   3 Mar 26  1999 cum3

Umblenden virtueller Adressen

Obwohl es selten notwendig ist, ist es doch interessant zu sehen, wie ein Treiber eine virtuelle Adresse mit mmap in den User-Space einblenden kann. Unter dem Begriff "virtuelle Adresse" verstehen wir hier eine von Funktionen wie vmalloc oder kmap zurückgegebene Adresse, also eine Adresse, die auf die Seitentabellen des Kernels eingeblendet wird. Der Code in diesem Abschnitt stammt aus scullv, dem Modul, das wie scullp arbeitet, seinen Speicher aber über vmalloc bezieht.

Der größte Teil der Implementation von scullv entspricht der, die wir für scullp schon gesehen haben, mit der Ausnahme, daß der order-Parameter nicht überprüft werden muß, weil vmalloc die Seiten einzeln alloziert, denn Einzelallokationen sind mit sehr viel größerer Wahrscheinlichkeit erfolgreich als Mehrfachallokationen. Daher gibt es bei der Benutzung von vmalloc kein Problem mit der Allokationsgrößenordnung.

Der größte Teil der Arbeit von vmalloc besteht im Aufbauen der Seitentabellen, über die auf die allozierten Seiten als zusammenhängender Adreßbereich zugegriffen werden kann. Die Methode nopage muß dagegen die Seitentabellen wieder auseinandernehmen, um dem Aufrufer einen struct page-Zeiger übergeben zu können. Daher muß die nopage-Implementation von scullv die Seitentabellen absuchen, um die zu einer Seite gehörende physikalische Adresse zu ermitteln.

Die Funktion ähnelt, bis auf das Ende, der von scullp. Daher enthält dieser Code-Ausschnitt nur den Teil von nopage, der sich von scullp unterscheidet.


 
pgd_t *pgd; pmd_t *pmd; pte_t *pte;
unsigned long lpage;

  /*
   * Nach dem Nachschlagen von scullv ist "page" jetzt die Adresse
   * der Seite, die der aktuelle Prozess benoetigt. Weil es sich um
   * eine vmalloc-Adresse handelt, holen wir zuerst den unsigned
   * long-Wert, der in den Seitentabellen nachgeschlagen werden
   * soll.
   */
lpage = VMALLOC_VMADDR(pageptr);
spin_lock(&init_mm.page_table_lock);
pgd = pgd_offset(&init_mm, lpage);
pmd = pmd_offset(pgd, lpage);
pte = pte_offset(pmd, lpage);
page = pte_page(*pte);
spin_unlock(&init_mm.page_table_lock);

/* alles klar, jetzt den Zaehler inkrementieren */
get_page(page);
out:
up(&dev->sem);
return page;

Auf die Seitentabellen wird mit den am Anfang dieses Kapitels eingeführten Funktionen zugegriffen. Das zu diesem Zweck verwendete Seitenverzeichnis wird in der Speicherstruktur des Kernel-Space, init_mm, abgelegt. Beachten Sie, daß sich scullv die page_table_lock-Sperre holt, bevor der Treiber die Seitentabellen traversiert. Wenn diese Sperre nicht gehalten werden würde, könnte ein anderer Prozessor die Seitentabelle verändern, während scullv gerade mitten beim Nachschlagen wäre, was zu fehlerhaften Ergebnissen führen kann.

Das Makro VMALLOC_VMADDR(pageptr) gibt den korrekten unsigned long-Wert einer vmalloc-Adresse zurück, der zum Nachschlagen in einer Seitentabelle verwendet wird. Beachten Sie, daß ein einfaches Casting des Wertes in älteren Kerneln als 2.1 wegen eines Fehlers in der Speicherverwaltung nicht ausreichen würde. Die Speicherverwaltung der x86-Plattform wurde in der Version 2.1.1 geändert, und VMALLOC_VMADDR ist dort nun genauso die Identitätsfunktion, wie es das auf allen Plattformen schon immer gewesen ist. Um den Code portabel zu halten, sollte man dieses aber trotzdem immer noch verwenden.

Analog zu dieser Diskussion wollen Sie vielleicht auch versuchen, die von ioremap zurückgegebenen Adressen in den User-Space einzublenden. Das ist ziemlich einfach möglich, weil Sie remap_page_range direkt verwenden können, ohne Methoden implementieren zu müssen, die auf virtuelle Speicherbereiche zugreifen. remap_page_range kann also mit anderen Worten bereits dafür verwendet werden, um neue Seitentabellen aufzubauen, die I/O-Speicher in den User-Space einblenden; es ist nicht nötig, in den von vremap erzeugten Seitentabellen des Kernels nachzuschlagen, wie wir das in scullv gemacht haben.