Kapitel 13. mmap und DMA

Inhalt
Speicherverwaltung in Linux
Die Geräteoperation mmap
Die kiobuf-Schnittstelle
Direct Memory Access und Bus Mastering
Abwärtskompatibilität
Schnellreferenz

In diesem Kapitel tauchen wir in die Interna der Speicherverwaltung von Linux ein, wobei wir uns besonders auf die Techniken konzentrieren, die für Autoren von Gerätetreibern interessant sind. Der Stoff in diesem Kapitel ist etwas kompliziert, und nicht jeder muß ihn verstehen. Gleichwohl kann man viele Aufgaben nur erledigen, wenn man tiefer in die Speicherverwaltung einsteigt; außerdem ermöglicht dieses Kapitel einen interessanten Einblick in einen wichtigen Bestandteil des Kernels.

Das Material in diesem Kapitel teilt sich in drei Teile auf. Der erste behandelt die Implementation des Systemaufrufs mmap, mit dem man Gerätespeicher direkt in den Adreßraum eines Benutzerprozesses einblenden kann. Anschließend behandeln wir den kiobuf-Mechanismus des Kernels, mit dem man aus dem Kernel-Space direkten Zugriff auf den Benutzer-Speicher bekommt. Mit dem kiobuf-System kann man für manche Gerätearten “rohe” I/O implementieren. Der letzte Abschnitt behandelt dann I/O-Operationen mit direktem Speicherzugriff (Direct Memory Access, DMA), mit dem Peripheriegeräte direkten Zugriff auf den Systemspeicher bekommen.

Natürlich muß man für alle diese Techniken verstehen, wie die Speicherverwaltung unter Linux funktioniert, weswegen wir mit einem Überblick über dieses Subsystem anfangen.

Speicherverwaltung in Linux

Anstatt hier die Theorie der Speicherverwaltung in Betriebssystemen zu beschreiben, versuchen wir in diesem Abschnitt, die Hauptmerkmale von Linux anzugehen. Obwohl Sie kein Experte in Sachen virtueller Speicher unter Linux sein müssen, um mmap zu implementieren, ist ein grundlegender Überblick darüber, wie die Dinge funktionieren, nützlich. Wir beginnen mit einer etwas längeren Beschreibung der Datenstrukturen, die der Kernel zur Verwaltung des Speichers verwendet. Wenn wir die notwendigen Hintergrundinformationen behandelt haben, können wir anfangen, mit diesen Strukturen zu arbeiten.

Adreßtypen

Linux ist natürlich ein System mit virtuellem Speicher, was bedeutet, daß die Adressen, die Benutzerprogramme zu sehen bekommen, nicht direkt mit den physikalischen Adressen übereinstimmen, die von der Hardware verwendet werden. Virtueller Speicher führt eine zusätzliche Abstraktionsschicht ein, was einige nette Dinge ermöglicht. Mit virtuellem Speicher können Programme im System deutlich mehr Speicher allozieren, als physikalisch verfügbar ist; selbst ein einziger Prozeß kann einen virtuellen Adreßraum haben, der größer ist als der physikalische Speicher des Systems. Mit virtuellem Speicher kann man auch eine Reihe von Tricks mit dem Adreßraum des Prozesses anstellen, darunter das Einblenden von Gerätespeicher.

Bisher haben wir über virtuelle und physikalische Adressen gesprochen, aber eine ganze Reihe von Details unberücksichtigt gelassen. Das Linux-System arbeitet mit verschiedenen Adreßtypen, die alle eine eigene Semantik haben. Leider steht im Kernel-Code nicht immer ausdrücklich, welcher Adreßtyp in einer bestimmten Situation verwendet wird, so daß man als Programmierer vorsichtig sein muß.

Abbildung 13-1. Adreßtypen in Linux

Hier folgt eine Liste der in Linux verwendeten Adreßtypen. Abbildung 13-1 zeigt, wie sich diese Adreßtypen zum physikalischen Speicher verhalten.

virtuelle User-Adressen

Dies sind die normalen Adressen, die User-Space-Programme zu sehen bekommen. Diese Adressen sind entweder 32 oder 64 Bit breit, je nach zugrundeliegender Hardware-Architektur. Jeder Prozeß hat einen eigenen virtuellen Adreßraum.

physikalische Adressen

Die Adressen, die zwischen dem Prozessor und dem Systemspeicher verwendet werden. Physikalische Adressen sind 32 oder 64 Bit breit; selbst 32-Bit-Systeme können in manchen Situationen 64 Bit breite physikalische Adressen verwenden.

Bus-Adressen

Die Adressen, die zwischen den Peripherie-Bussen und dem Speicher verwendet werden. Oftmals sind dies die gleichen Adressen wie die vom Prozessor verwendeten physikalischen Adressen, aber dies muß nicht so sein. Bus-Adressen sind natürlich stark architekturabhängig.

logische Kernel-Adressen

Diese bilden den normalen Adreßraum des Kernels. Diese Adressen bilden den größten Teil oder sogar den gesamten Hauptspeicher ab und werden oft wie physikalische Adressen behandelt. Auf den meisten Architekturen sind logische Adressen und die zugehörigen physikalischen Adressen nur um einen konstanten Offset verschieden. Logische Adressen werden normalerweise in Variablen des Typs unsigned long oder void* gespeichert. Von kmalloc zurückgegebener Speicher hat eine logische Adresse.

virtuelle Kernel-Adressen

Diese Adressen unterscheiden sich von logischen Adressen dadurch, daß sie nicht notwendigerweise eine direkte Abbildung auf physikalische Adressen haben. Alle logischen Adressen sind virtuelle Kernel-Adressen; Speicher, der mit vmalloc alloziert wurde, hat ebenfalls eine virtuelle Adresse (aber keine direkte physikalische Abbildung). Die Funktion kmap, die wir später in diesem Kapitel beschreiben, gibt ebenfalls virtuelle Adressen zurück. Virtuelle Adressen werden normalerweise in Zeiger-Variablen gespeichert.

Wenn Sie eine logische Adresse haben, dann können Sie mit dem Makro __pa() (definiert in <asm/page.h>) die zugehörige physikalische Adresse bekommen. Physikalische Adressen können mit __va() zurück auf logische Adressen abgebildet werden, aber nur bei Seiten im niedrigen Speicher.

Unterschiedliche Kernel-Funktionen benötigen unterschiedliche Adressen. Es wäre schön, wenn es unterschiedliche C-Typen gäbe, so daß der verlangte Adreßtyp explizit gemacht würde, aber dieses Glück ist uns nicht zuteil geworden. Wir werden es in diesem Kapitel jeweils deutlich machen, welche Adressen wo verwendet werden.

Hoher und niedriger Speicher

Der Unterschied zwischen logischen und virtuellen Kernel-Adressen wird auf 32-Bit-Systemen mit viel Speicher deutlich. Mit 32 Bits kann man bis zu 4 GByte Speicher adressieren. Linux ist aufgrund der Konfiguration des virtuellen Adreßraums bis vor kurzem auf 32-Bit-Systemen auf deutlich weniger Speicher beschränkt gewesen. Das System konnte nicht mehr Speicher verwalten, als es logische Adressen einrichten konnte, weil es direkt eingeblendete Kernel-Adressen für sämtlichen Speicher brauchte.

Jüngere Entwicklungen haben die Begrenzungen beim Speicher beseitigt; 32-Bit-Systeme können nun mit deutlich über 4 GByte Speicher arbeiten (natürlich sofern der Prozessor selbst soviel Speicher adressieren kann). Die Einschränkung, wieviel Speicher direkt mit logischen Adressen eingeblendet werden kann, bleibt aber. Nur der untere Teil des Speichers (bis zu 1 oder 2 GByte, je nach Hardware und Kernel-Konfiguration) hat logische Adressen, der Rest (hoher Speicher) nicht. Hoher Speicher kann 64 Bit breite physikalische Adressen benötigen, weswegen der Kernel explizite virtuelle Adreßeinblendungen einrichten muß, um den hohen Speicher zu manipulieren. Daher funktionieren viele Kernel-Funktionen nur mit niedrigem Speicher; hoher Speicher ist normalerweise für die Seiten von User Space-Prozessen reserviert.

Der Begriff “hoher Speicher” kann für manche Leute verwirrend sein, insbesondere weil er in der PC-Welt auch noch andere Bedeutungen hat. Daher definieren wir die Begriffe hier:

Niedriger Speicher

Speicher, für den logische Adressen im Kernel-Space existieren. Auf den allermeisten Systemen, mit denen Sie es zu tun haben werden, besteht der gesamte Speicher aus niedrigem Speicher.

Hoher Speicher

Speicher, für den keine logischen Adressen existieren, weil das System mehr physikalischen Speicher enthält, als mit 32 Bits adressiert werden kann.

Auf i386-Systemen liegt die Grenze zwischen niedrigem und hohen Speicher normalerweise knapp unter 1 GByte. Diese Grenze hat absolut nichts mit der alten 640-KByte-Einschränkung auf dem originalen PC zu tun, sondern ist eine Einschränkung des Kernels selbst, der den 32-Bit-Adreßraum zwischen dem Kernel-Space und dem User-Space aufteilt.

Wir werden Einschränkungen bei der Arbeit mit hohem Speicher an den passenden Stellen in diesem Kapitel erwähnen.

Die Speichertabelle und struct page

Historisch gesehen hat der Kernel logische Adressen verwendet, um auf explizite Seiten im Speicher zu verweisen. Das Hinzufügen der Unterstützung hohen Speichers hat aber ein offensichtliches Problem mit diesem Ansatz freigelegt — logische Adressen stehen für hohen Speicher nicht zur Verfügung. Kernel-Funktionen, die mit Speicher arbeiten, verwenden daher in zunehmendem Maße Zeiger auf struct page. Diese Datenstruktur verwaltet so ziemlich alles, was der Kernel über physikalischen Speicher wissen muß; es gibt eine struct page für jede physikalische Seite im System. Zu den Feldern dieser Struktur gehören:

atomic_t count;

Die Anzahl der Referenzen auf diese Seite. Wenn der Zähler auf Null fällt, wird die Seite an die Liste der freien Seiten zurückgegeben.

wait_queue_head_t wait;

Eine Liste der Prozesse, die auf diese Seite warten. Prozesse können auf eine Seite warten, wenn eine Kernel-Funktion diese aus irgendeinem Grund gesperrt hat. Treiber müssen sich darüber aber normalerweise keine Gedanken machen.

void *virtual;

Die virtuelle Kernel-Adresse der Seite, sofern diese eingeblendet ist; NULL ansonsten. Seiten im niedrigen Speicher sind normalerweise eingeblendet, Seiten im hohen Speicher üblicherweise nicht.

unsigned long flags;

Eine Menge von Bit-Flags, die den Status der Seite beschreiben. Dazu gehören PG_locked, das angibt, daß die Seite im Speicher gesperrt ist, und PG_reserved, das verhindert, daß das Speicherverwaltungssystem die Seite überhaupt anfaßt.

In struct page finden sich viele Informationen, die aber ein Teil der schwarzen Magie der Speicherverwaltung und für Treiber-Autoren nicht relevant sind.

Der Kernel verwaltet ein oder mehrere Arrays von struct page-Einträgen, die den gesamten physikalischen Speicher im System beschreiben. Auf den meisten Systemen ist dies ein einziges Array namens mem_map. Auf manchen Systemen ist die Situation aber komplizierter. Auf Systemen mit uneinheitlichem Speicherzugriff ("Nonuniform Memory Access", NUMA) und solchen mit weit auseinandergerissenem physikalischem Speicher kann es mehr als ein Speichertabellen-Array geben. Daher sollte Code, der portabel sein soll, den Zugriff auf dieses Array nach Möglichkeit vermeiden. Glücklicherweise kann man normalerweise einfach mit den struct page-Zeigern arbeiten, ohne sich darüber Gedanken machen zu müssen, woher diese kommen.

Manche Funktionen sind für die Konvertierung zwischen struct page-Zeigern und virtuellen Adressen gedacht:

struct page *virt_to_page(void *kaddr);

Dieses Makro (definiert in <asm/page.h>) erwartet eine logische Kernel-Adresse und gibt den zugehörigen struct page-Zeiger zurück. Weil es eine logische Adresse benötigt, funktioniert es nicht mit Speicher von vmalloc oder hohem Speicher.

void *page_address(struct page *page);

Gibt die virtuelle Kernel-Adresse dieser Seite zurück, wenn eine solche Adresse existiert. Bei hohem Speicher existiert die Adresse nur, wenn die Seite eingeblendet worden ist.

#include <linux/highmem.h>, void *kmap(struct page *page);, void kunmap(struct page *page);

kmap gibt eine virtuelle Kernel-Adresse für beliebige Seiten im System zurück. Bei Seiten im niedrigen Speicher ist das einfach die logische Adresse der Seite, bei Seiten im hohen Speicher erzeugt kmap eine spezielle Einblendung. Mit kmap erzeugte Einblendungen sollten immer mit kunmap wieder freigegeben werden. Es steht nur eine begrenzte Anzahl solcher Einblendungen zur Verfügung, so daß man diese nicht zu lange festhalten sollte. kmap-Aufrufe sind additiv; wenn also zwei oder mehr Funktionen kmap auf der gleichen Seite aufrufen, dann passiert das Richtige. Beachten Sie auch, daß kmap schlafen kann, wenn keine Einblendungen zur Verfügung stehen.

> > Wir werden einige dieser Funktionen noch in Aktionen sehen, wenn wir zum Beispiel-Code kommen.

Seitentabellen

Wenn ein Programm eine virtuelle Adresse nachschlägt, muß die CPU die Adresse in eine physikalische Adresse konvertieren, um auf den physikalischen Speicher zuzugreifen. Dies geschieht normalerweise durch das Aufteilen der Adresse in Bitfelder. Jedes Bitfeld wird als Index in ein Array, die sogenannte Seitentabelle verwendet, um entweder die Adresse der nächsten Tabelle oder die Adresse der physikalischen Seite mit der virtuellen Adresse zu bekommen.

Der Linux-Kernel verwaltet drei Ebenen von Seitentabellen, um virtuelle Adressen auf physikalische Adressen einzublenden. Diese mehrfachen Ebenen erlauben es, daß der Speicherbereich nur dünn besetzt ist; moderne Systeme breiten einen Prozeß über einen großen Bereich virtuellen Speichers aus. Das ist auch sinnvoll, weil damit eine große Flexibilität zur Laufzeit besteht.

Beachten Sie, daß Linux auch auf solcher Hardware ein Drei-Ebenen-System verwendet, die nur zwei Ebenen von Seitentabellen unterstützt, desgleichen auf Hardware, die ein anderes Verfahren verwendet, um virtuelle Adressen auf physikalische Adressen abzubilden. Durch die Verwendung dreier Ebenen in einer prozessorunabhängigen Implementation kann Linux sowohl Zwei-Ebenen- als auch Drei-Ebenen-Prozessoren (wie die Alpha-Prozessoren) unterstützen, ohne daß der Code durch zu viele #ifdef-Anweisungen verunstaltet wird. Diese konservative Implementation führt nicht zu mehr Verwaltungsaufwand, wenn der Kernel auf Zwei-Ebenen-Prozessoren läuft, weil der Kernel die unbenutzte Ebene ohnehin wegoptimiert.

Schauen wir uns aber nun die Datenstrukturen an, die für das Ein- und Auslagern (Paging) verwendet werden.

Die folgende Liste faßt die Implementation der drei Ebenen in Linux zusammen; in Abbildung 13-2 wird das auch grafisch dargestellt:

Abbildung 13-2. Die drei Ebenen der Linux-Seitentabellen

Page Directory (PGD)

Die Seitentabelle der obersten Ebene. Es handelt sich dabei um ein Array von pgd_t-Elementen, von denen jedes auf eine Seitentabelle der zweiten Ebene zeigt. Jeder Prozeß hat ein eigenes Page Directory, außerdem gibt es eines für den Kernel-Space. Sie können sich das Page Directory als an Seiten ausgerichtetes Array von Daten des Typs pfd_t vorstellen.

Page mid-level Directory (PMD)

Die Tabelle auf der zweiten Ebene. Ein PMD ist ein an Seiten ausgerichtetes Array von pmd_t-Elementen. Ein pmd_t ist dabei ein Zeiger auf die Seitentabelle der dritten Ebene. Zwei-Ebenen-Prozessoren haben keine physikalische PMD; sie deklarieren ihre PMD als Array mit einem einzigen Element, dessen Wert die PMD selbst ist — wir werden uns später noch ansehen, wie das in C gelöst ist und wie der Compiler diese Ebene hinwegoptimiert.

Page Table (Seitentabelle)

Auch bei der Page Table handelt es sich um ein an Seiten ausgerichtetes Array; dieses Mal haben die Elemente den Typ pte_t. Ein pte_t enthält die physikalische Adresse der Datenseite.

Die in dieser Liste eingeführten Typen sind in <asm/page.h> deklariert. Diese Datei muß in jeder Quelldatei, bei der es um Paging geht, eingebunden werden.

Der Kernel muß während der normalen Programmausführung nichts in den Seitentabellen nachschlagen, weil das in der Hardware geschieht. Trotzdem muß der Kernel alles so einrichten, daß die Hardware ihre Aufgabe auch erfüllen kann. Er muß die Seitentabellen aufbauen und darin nachsehen, wenn der Prozessor einen “Page Fault” meldet, also wenn eine vom Prozessor benötigte virtuelle Adresse nicht im Speicher steht. Auch Gerätetreiber müssen Seitentabellen aufbauen und Faults behandeln können, wenn sie mmap implementieren.

Es ist interessant zu sehen, wie die Software-Speicherverwaltung die gleichen Seitentabellen verwendet wie die CPU selbst. Wenn eine CPU keine Seitentabellen implementiert, dann wird der Unterschied in den untersten Schichten des architekturabhängigen Codes versteckt. Sie können daher in der Linux-Speicherverwaltung immer über Drei-Ebenen-Seitentabellen sprechen, unabhängig davon, ob die Hardware Drei-Ebenen-Seitentabellen kennt oder nicht. Ein Beispiel einer CPU-Familie, die keine Seitentabellen verwendet, ist der PowerPC. PowerPC-Designer haben einen Hash-Algorithmus implementiert, der virtuelle Adressen auf eine Seitentabelle mit einer Ebene abbildet. Wenn auf eine Seite zugegriffen wird, die sich bereits im Speicher befindet, aber deren physikalische Adresse nicht mehr im CPU-Cache liegt, muß die CPU den Speicher nur einmal lesen und nicht zwei- oder dreimal wie beim Ansatz mit mehreren Seitentabellen. Der Hash-Algorithmus macht es wie Tabellen auf mehreren Ebenen möglich, die Verwendung des Speichers beim Einblenden virtueller Adressen auf physikalische Adressen zu reduzieren.

Unabhängig von dem von der CPU verwendeten Mechanismus basiert die Linux-Software-Implementation auf Seitentabellen mit drei Ebenen, und die folgenden Symbole werden verwendet, um auf die Seitentabellen zuzugreifen. Sowohl <asm/page.h> als auch <asm/pgtable.h> müssen eingebunden werden, damit alle zur Verfügung stehen.

PTRS_PER_PGD, PTRS_PER_PMD, PTRS_PER_PTE

Die Größe jeder Tabelle. Auf Zwei-Ebenen-Prozessoren ist PTRS_PER_PMD 1, damit die mittlere Ebene nicht benutzt wird.

unsigned long pgd_val(pgd_t pgd), unsigned long pmd_val(pmd_t pmd), unsigned long pte_val(pte_t pte)

Diese drei Makros werden benutzt, um den unsigned long-Wert aus dem getypten Datenelement herauszuholen. Der tatsächlich verwendete Typ ist von der zugrundeliegenden Architektur und der Kernel-Konfiguration abhängig; normalerweise ist das ein unsigned long oder — auf 32-Bit-Prozessoren, die hohen Speicher unterstützen, ein unsigned long long. SPARC64-Prozessoren verwenden unsigned int. Die Makros helfen dabei, eine strenge Typenprüfung im Quellcode zu verwenden, ohne zusätzlichen Rechenaufwand zu produzieren.

pgd_t * pgd_offset(struct mm_struct * mm, unsigned long address), pmd_t * pmd_offset(pgd_t * dir, unsigned long address), pte_t * pte_offset(pmd_t * dir, unsigned long address)

Diese Inline-Funktionen[1] werden dazu benutzt, die zu address gehörenden pgd-, pmd- und pte -Einträge nachzuschlagen. Das Nachschlagen von Seitentabellen beginnt mit einem Zeiger auf struct mm_struct. Der Zeiger, der zur Speichertabelle des aktuellen Prozesses gehört, steht in current->mm. Der Zeiger auf den Kernel-Space ist in init_mm enthalten. Zwei-Ebenen-Prozessoren definieren pmd_offset(dir,add) als (pmd_t *)dir. Funktionen, die Seitentabellen absuchen, sind immer als inline deklariert, und der Compiler optimiert jedes Nachschlagen in pmd hinweg.

struct* page pte_page(pte_t pte)

Diese Funktion gibt einen Zeiger auf den struct page-Eintrag der Seite in diesem Seitentabellen-Eintrag zurück. Code, der mit Seitentabellen arbeitet, sollte normalerweise pte_page anstelle von pte_val verwenden, weil pte_page das prozessorabhängige Format des Seitentabellen-Eintrags abdeckt und den struct page-Zeiger zurückgibt, den man normalerweise braucht.

pte_present(pte_t pte)

Dieses Makro gibt einen Booleschen Wert zurück, der angibt, ob die Seite sich derzeit im Speicher befindet. Dies ist die meistbenutzte der Funktionen, die auf die unteren Bits in pte zugreifen — also gerade auf diejenigen Bits, die von pte_page verworfen werden. Seiten können natürlich nicht vorhanden sein, wenn der Kernel sie auf die Festplatte ausgelagert hat (oder wenn sie überhaupt noch nicht geladen worden sind). Die Seitentabellen selbst sind jedoch immer da (zumindest in der aktuellen Implementation von Linux). Das macht den Kernel-Code einfacher, weil pgd_offset und verwandte Funktionen nie fehlschlagen können; auf der anderen Seite hält selbst ein Prozeß mit einer “residenten Speichergröße” von Null seine Seitentabellen im Speicher und verwendet so Speicher, der anderweitig besser verwendet werden könnte.

Jeder Prozeß im System hat eine struct mm_struct-Struktur, die die Seitentabellen des Prozesses und vieles andere enthält, und außerdem ein Spinlock namens page_table_lock, das während des Traversierens oder Veränderns gehalten werden sollte.

Um als Fachmann für die Speicherverwaltungsalgorithmen von Linux zu gelten, müssen Sie aber mehr als nur diese Liste von Funktionen kennen. Echte Speicherverwaltung ist sehr viel komplexer und muß sich auch mit anderen Komplikationen wie der Kohärenz des Caches auseinandersetzen. Die obige Liste sollte aber ausreichend sein, um Ihnen einen Eindruck davon zu geben, wie die Seitenverwaltung implementiert ist; mehr müssen Sie auch als Autor von Gerätetreibern nicht wissen, wenn Sie ab und zu mit Seitentabellen arbeiten wollen. Sie bekommen weitere Informationen aus den Bäumen include/asm und mm in den Kernel-Quellen.

Virtuelle Speicherbereiche (Virtual Memory Areas)

Das Paging ist die unterste Ebene der Speicherverwaltung, aber es ist noch etwas mehr nötig, um die Ressourcen des Rechners effizient zu nutzen. Der Kernel benötigt einen Mechanismus auf einer höheren Ebene, um zu regeln, wie ein Prozeß seinen Speicher sieht. Dieser Mechanismus wird in Linux als virtuelle Speicherbereiche (Virtual Memory Areas) bezeichnet, die wir im folgenden VMAs oder einfach Bereiche nennen werden.

Ein Bereich ist eine homogene Region im virtuellen Speicher eines Prozesses, ein zusammenhängender Bereich von Adressen, die gleiche Schalter für die Zugriffsrechte haben. Er entspricht damit ungefähr dem Konzept eines Segments, läßt sich aber besser als “Speicherobjekt mit eigenen Eigenschaften” beschreiben. Die Speichertabelle eines Prozesses besteht aus:

  • einem Bereich für den ausführbaren Code des Programms (oft Text genannt).

  • je einem Bereich für alle Daten, darunter die initialisierten Daten (die zu Beginn der Ausführung einen explizit zugewiesenen Wert haben), uninitialisierte Daten (BSS)[2] und den Stack des Programms

  • einem Bereich für alle aktiven Speicher-Einblendungen

Die Speicherbereiche eines Prozesses sind in proc/pid/maps zu sehen (wobei Sie pid natürlich durch die Prozeß-ID ersetzen müssen). /proc/self ist ein Sonderfall von /proc/pid, der immer auf den aktuellen Prozeß verweist. Hier sehen Sie als Beispiel einige Speicher-Tabellen, zu denen wir hinter dem Doppelkreuz einige kurze Kommentare hinzugefügt haben:


08048000-0804e000 r-xp 00000000 08:01 51297      /sbin/init  # Text
0804e000-08050000 rw-p 00005000 08:01 51297      /sbin/init  # Daten
08050000-08054000 rwxp 00000000 00:00 0          # auf 0 eingeblendetes BSS
40000000-40013000 r-xp 00000000 08:01 39003      /lib/ld-2.1.3.so # Text
40013000-40014000 rw-p 00012000 08:01 39003      /lib/ld-2.1.3.so # Daten
40014000-40015000 rw-p 00000000 00:00 0          # BSS für ld.so
4001b000-40108000 r-xp 00000000 08:01 39006      /lib/libc-2.1.3.so # Text
40108000-4010c000 rw-p 000ec000 08:01 39006      /lib/libc-2.1.3.so # Daten
4010c000-40110000 rw-p 00000000 00:00 0          # BSS für libc.so
bfffe000-c0000000 rwxp fffff000 00:00 0          # auf 0 eingeblendeter Stack

morgana.root# rsh wolf head /proc/self/maps  #### alpha-axp: static ecoff
000000011fffe000-0000000120000000 rwxp 0000000000000000 00:00 0     # Stack
0000000120000000-0000000120014000 r-xp 0000000000000000 08:03 2844  # Text
0000000140000000-0000000140002000 rwxp 0000000000014000 08:03 2844  # Daten
0000000140002000-0000000140008000 rwxp 0000000000000000 00:00 0     # BSS

Die Felder in jeder Zeile haben die folgende Bedeutung:


start-end perm offset major:minor inode image.

Jedes Feld in /proc/*/maps (mit Ausnahme des Image-Namens) gehört zu einem Feld in struct vm_area_struct und wird in der folgenden Liste beschrieben.

start, end

Die erste und letzte virtuelle Adresse dieses Speicherbereichs

perm

Eine Bitmaske, die die Zugriffsrechte für das Lesen, Schreiben und Ausführen des Speicherbereichs enthält. Mit diesen Rechten wird angegeben, was der Prozeß mit den zu diesem Bereich gehörenden Seiten machen darf. Das letzte Zeichen in diesem Feld ist entweder p für “private” oder s für “shared”.

offset

Gibt an, wo der Speicherbereich in der eingeblendeten Datei beginnt. Der Wert 0 bedeutet natürlich, daß die erste Seite des Speicherbereichs der ersten Seite der Datei entspricht.

major, minor

Die Major- und Minor-Nummern des Geräts, das die eingeblendete Datei enthält. Verwirrenderweise verweisen die Major- und Minor-Nummern bei Geräte-Einblendungen auf die Gerätedatei, die vom Benutzer geöffnet wurde, und nicht auf das Gerät selbst.

inode

Die Inode-Nummer der eingeblendeten Datei

image

Der Name der Datei (normalerweise ein ausführbares Image), die eingeblendet worden ist.

Ein Treiber, der die Methode mmap implementiert, muß eine VMA-Struktur im Adreßraum des Prozesses ausfüllen, der das Gerät einblendet. Der Treiberprogrammierer sollte daher wenigstens grundlegende Kenntnisse über VMAs besitzen, um diese benutzen zu können.

Schauen wir uns jetzt die wichtigsten Felder in struct vm_area_struct (definiert in <linux/mm.h>) an. Diese Felder können von Gerätetreibern in ihrer mmap-Implementation benutzt werden. Beachten Sie, daß der Kernel Listen und Bäume von VMAs verwaltet, um das Nachschlagen von Bereichen zu optimieren. Mehrere Felder in vm_area_struct existieren zur Unterstützung dieser Verwaltung. VMAs können nicht beliebig von Treibern erzeugt werden, weil sonst diese Strukturen nicht mehr korrekt sind. Die wichtigsten Felder von VMAs sehen wie folgt aus (beachten Sie die Ähnlichkeit zwischen diesen Feldern und der /proc-Ausgabe, die wir gerade gesehen haben):

unsigned long vm_start;, unsigned long vm_end;

Der von dieser VMA abgedeckte Adreßbereich. Diese Felder sind die ersten beiden Felder, die in /proc/*/maps angezeigt werden.

struct file *vm_file;

Ein Zeiger auf die struct file-Struktur, die zu diesem Bereich gehört (wenn es eine solche gibt).

unsigned long vm_pgoff;

Der Offset der Area in der Datei in Seiten. Wenn eine Datei oder ein Gerät eingeblendet wird, ist dies die Dateiposition der ersten in diesem Bereich eingeblendeten Seite.

unsigned long vm_flags;

Eine Menge von Flags, die diesen Bereich beschreiben. Für Programmierer von Gerätetreibern sind die Flags VM_IO und VM_RESERVED am interessantesten. VM_IO kennzeichnet einen VMA als einen in den Speicher eingeblendeten I/O-Bereich. Dieses Flag verhindert unter anderem, daß dieser Bereich in Core Dumps des Prozesses enthalten ist. VM_RESERVED fordert das Speicherverwaltungssystem auf, diesen VMA nicht auszulagern; dieses Flag sollte in den meisten Geräteeinblendungen gesetzt sein.

struct vm_operations _struct *vm_ops;

Eine Reihe von Funktionen, die der Kernel aufrufen kann, um diesen Speicherbereich zu manipulieren. Das Vorhandensein dieses Feldes deutet darauf hin, daß ein Speicherbereich genau wie die struct file, die wir in diesem Buch ständig verwendet haben, ein Kernel-"Objekt" ist.

void *vm_private_data;

Ein Feld, in dem der Treiber seine eigenen Informationen abspeichern kann.

Wie struct vm_area_struct ist vm_operations_struct in <linux/mm.h> definiert und enthält die unten aufgeführten Operationen. Diese Operationen sind die einzigen, die nötig sind, um die Speicherbedürfnisse des Prozesses zu befriedigen. Sie werden in der Reihenfolge aufgeführt, in der sie auch deklariert sind. Weiter unten in diesem Kapitel werden wir einige dieser Funktionen implementieren und sie dabei noch ausführlicher beschreiben.

void (*open)(struct vm_area_struct *vma);

Die open-Methode wird vom Kernel aufgerufen, um dem Subsystem, das die VMA implementiert, die Möglichkeit zu geben, den Bereich zu initialisieren, Referenzzähler anzupassen usw. Diese Methode wird jedesmal aufgerufen, wenn eine neue Referenz auf die VMA angelegt wird (etwa wenn ein Prozeß forkt). Die einzige Ausnahme besteht beim ersten Erzeugen der VMA durch mmap: In diesem Fall wird die mmap-Methode des Treibers anstelle von open aufgerufen.

void (*close)(struct vm_area_struct *vma);

Wenn ein Bereich zerstört wird, ruft der Kernel seine close-Methode auf. Es gibt übrigens bei VMAs keinen Verwendungszähler; der Bereich wird nur je einmal pro Prozeß geöffnet und geschlossen.

void (*unmap)(struct vm_area_struct *vma,  unsigned long addr, size_t len);

Der Kernel ruft diese Methode auf, um die Einblendung eines Teils oder auch des ganzen Bereichs aufzuheben. Wenn der gesamte Bereich betroffen ist, dann ruft der Kernel unmittelbar nach der Rückkehr von vm_ops->unmap auch vm_ops->close auf.

void (*protect)(struct vm_area_struct *vma, unsigned long, size_t, unsigned int newprot);

Diese Methode soll den Schutz des Speicherbereiches ändern, wird aber derzeit nicht verwendet. Der Speicherschutz wird von den Seitentabellen erledigt, und der Kernel erzeugt die Einträge in diesen Tabellen separat.

int (*sync)(struct vm_area_struct *vma, unsigned long, size_t, unsigned int flags);

Diese Methode wird vom Systemaufruf sync aufgerufen, um einen veränderten Speicherabschnitt auf das Speichermedium zu schreiben. Der Rückgabewert sollte 0 im Erfolgsfall und sonst negativ sein.

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

Wenn ein Prozeß versucht, auf eine Seite zuzugreifen, die zu einem gültigen VMA gehört, aber derzeit nicht im Speicher ist, dann wird die Methode nopage aufgerufen, sofern sie für den entsprechenden Bereich definiert ist. Sie gibt den struct page-Zeiger der physikalischen Seite zurück, nachdem diese gegebenenfalls von einem Sekundärspeicher eingelesen worden ist. Wenn die Methode für den Bereich nicht definiert ist, dann alloziert der Kernel eine leere Seite. Das dritte Argument, write_access, wird für “keine gemeinsame Nutzung” verwendet. Ein von Null verschiedener Wert bedeutet, daß die Seite dem aktuellen Prozeß gehören muß, während Null angibt, daß eine gemeinsame Nutzung möglich ist.

struct page *(*wppage)(struct vm_area_struct *vma, unsigned long address, struct page *page);

Die Methode bearbeitet Seitenfehler bei “schreibgeschützten” Seiten, wird aber derzeit nicht verwendet. Der Kernel behandelt Versuche, auf eine schreibgeschützte Seite zu schreiben, selbst, ohne die bereichsspezifische Funktion aufzurufen. Seitenfehler bei schreibgeschützten Seiten werden dazu verwendet, um copy-on-write zu implementieren. Eine private Seite kann so lange von mehreren Prozessen gemeinsam genutzt werden, bis ein Prozeß darauf schreibt. In diesem Fall wird die Seite kopiert, und der Prozeß schreibt auf seine eigene Kopie der Seite. Wenn der gesamte Bereich als schreibgeschützt gekennzeichnet ist, dann wird dem Prozeß das Signal SIGSEGV gesendet und die Copy-on-write-Operation nicht durchgeführt.

int (*swapout)(struct page *page, struct file *file);

Diese Methode wird aufgerufen, wenn eine Seite zum Auslagern ausgewählt worden ist. Der Rückgabewert 0 zeigt eine erfolgreiche Ausführung an, jeder andere Wert einen Fehler. Im Fehlerfall bekommt der Prozeß, dem die Seite gehört, ein SIGBUS-Signal geschickt. Es ist höchst unwahrscheinlich, daß je ein Treiber swapout implementieren muß; Geräte-Einblendungen sind nichts, was der Kernel einfach so auf die Festplatte schreiben könnte.

Damit haben wir unseren Überblick über die Datenstrukturen in der Linux-Speicherverwaltung abgeschlossen. Wir können jetzt mit der Implementation des Systemaufrufs mmap weitermachen.

Fußnoten

[1]

Übrigens sind diese Funktionen auf 32-Bit-SPARC-Prozessoren nicht inline, sondern richtige, als extern deklarierte Funktionen, die nicht an modularisierten Code exportiert werden. Daher können Sie diese Funktionen nicht in einem Modul auf SPARCs verwenden, werden das aber normalerweise auch nicht tun müssen.

[2]

Der Name BSS ist ein historisches Überbleibsel, das von einem alten Assembler-Operator namens “Block Started by Symbol” stammt. Das BSS-Segment wird in ausführbaren Dateien nicht auf der Festplatte abgespeichert; der Kernel blendet die Null-Seite auf den Adreßbereich von BSS ein.