Kapitel 7. Speicher ergattern

Inhalt
Die ganze Wahrheit über kmalloc
Lookaside-Caches
get_ free_ page und Freunde
vmalloc und Freunde
Allozierung während des Bootens
Abwärtskompatibilität
Schnellreferenz

Bisher haben wir immer kmalloc und kfree zur Speicherverwaltung verwendet. Im Linux-Kernel gibt es aber noch viele weitere Funktionen zum Allozieren von Speicher. In diesem Kapitel schauen wir uns andere Möglichkeiten an, Speicher in Gerätetreibern zu verwenden, und betrachten, wie man die Speicherressourcen seines Systems am besten nutzt. Dabei interessiert es uns noch nicht, wie die verschiedenen Architekturen den Speicher dann tatsächlich verwalten. Module haben mit Fragen der Segmentierung, Seitenaufteilung usw. nichts zu tun, denn der Kernel stellt für die Treiber ein einheitliches Interface zur Speicherverwaltung bereit. Außerdem werden wir in diesem Kapitel auch nicht die internen Details der Speicherverwaltung beschreiben, sondern uns das für den Abschnitt “the Section called Speicherverwaltung in Linux in Kapitel 13” in Kapitel 13 aufheben.

Die ganze Wahrheit über kmalloc

Der Allokationsmechanismus von kmalloc ist ein mächtiges Werkzeug, das wegen seiner Ähnlichkeit zu malloc leicht erlernt werden kann. Die Funktion ist schnell — sofern sie nicht blockiert — und leert den erworbenen Speicher nicht; der allozierte Bereich enthält immer noch den vorherigen Inhalt. Er ist auch im physikalischen Speicher zusammenhängend. In den nächsten Abschnitten werden wir uns detailliert mit kmalloc auseinandersetzen, damit Sie diese Funktion mit den anderen Speicherallokationstechniken vergleichen können, die später vorgestellt werden.

Das Argument flags

Das erste Argument von kmalloc ist die Größe des zu allozierenden Blocks. Das zweite Argument, die Allozierungs-Flags, ist viel interessanter, da es das Verhalten von kmalloc in vielerlei Hinsicht beeinflußt.

Das am häufigsten verwendete Flag GFP_KERNEL bedeutet, daß die Allokation (die intern von get_free_pages ausgeführt wird, daher das Präfix GFP_) im Namen eines Prozesses ausgeführt wird, der im Kernel-Space läuft. Mit anderen Worten: Die aufrufende Funktion führt einen Systemaufruf im Namen eines Prozesses aus. Wenn GFP_KERNEL verwendet wird, kann kmalloc den aufrufenden Prozeß schlafen legen, wenn der Speicher knapp geworden ist. Eine Funktion, die Speicher mit GFP_KERNEL alloziert, muß daher reentrant sein. Während der aktuelle Prozeß schläft, unternimmt der Kernel alles Notwendige, um eine Speicherseite zu organisieren, entweder durch Entleeren von Puffern (“flushen”) oder durch Auslagern von Speicher eines Benutzer-Prozesses.

GFP_KERNEL ist nicht immer die richtige Wahl, denn mitunter wird kmalloc außerhalb von Prozeß-Kontexten aufgerufen, beispielsweise aus Interrupt-Handlern, Task-Schlangen und Kernel-Timern. In diesem Fall sollte der current-Prozeß nicht schlafen gelegt werden. Hier muß GFP_ATOMIC als Flag verwendet werden. Der Kernel versucht normalerweise, einige freie Seiten beiseite zu legen, um eine atomare Allokation zu ermöglichen. Wenn GFP_ATOMIC verwendet wird, kann kmalloc auch noch die letzte freie Seite verwenden. Wenn diese aber auch bereits vergeben ist, schlägt die Allokation fehl.

Neben oder zusätzlich zu GFP_KERNEL und GFP_ATOMIC können auch noch andere Flags verwendet werden, auch wenn diese beiden die Bedürfnisse der meisten Gerätetreiber bereits abdecken. Die Flags sind alle in <linux/mm> aufgelistet: Einzelnen Flags ist ein doppelter Unterstrich vorangestellt wie bei __GFP_DMA. Gruppen von Flags fehlt dieses Präfix; diese werden manchmal auch Allokationsprioritäten genannt.

GFP_KERNEL

Normale Allokation von Kernel-Speicher. Kann schlafen.

GFP_BUFFER

Wird zur Verwaltung des Buffer-Caches verwendet; mit dieser Priorität kann der Allozierer schlafen. Sie unterscheidet sich von GFP_KERNEL dadurch, daß weniger Versuche gemacht werden, Speicher durch das Herausschreiben von Seiten auf die Festplatte freizumachen. Dadurch sollen Deadlocks vermieden werden, wenn die I/O-Subsysteme selbst Speicher benötigen.

GFP_ATOMIC

Wird dazu verwendet, Speicher aus Interrupt-Handlern und anderem Code außerhalb von Prozeß-Kontexten zu allozieren. Schläft nie.

GFP_USER

Kann Speicher für den Benutzer allozieren. Kann schlafen und hat eine niedrige Priorität.

GFP_HIGHUSER

Wie GFP_USER, alloziert aber aus dem hohen Speicher, sofern es einen solchen gibt. Hoher Speicher wird im nächsten Unterabschnitt besprochen.

_ _GFP_DMA

Dieses Flag fordert Speicher an, der für DMA-Übertragungen von oder zu Geräten geeignet ist. Die genaue Bedeutung ist plattformabhängig; das Flag kann mit GFP_KERNEL oder GFP_ATOMIC ODER-verknüpft werden.

_ _GFP_HIGHMEM

Dieses Flag fordert hohen Speicher an, ein plattformabhängiges Feature, das auf Plattformen, die es nicht unterstützen, ignoriert wird. Es ist ein Bestandteil von GFP_HIGHUSER und wird selten woanders verwendet.

Speicherzonen

Sowohl _ _GFP_DMA als auch _ _GFP_HIGHMEM sind plattformabhängig, auch wenn man sie auf allen Plattformen verwenden kann.

Die Version 2.4 des Kernels kennt drei Speicherzonen: DMA-fähigen Speicher, normalen Speicher und hohen Speicher. Die Allokation geschieht normalerweise in der normalen Zone, aber das Setzen eines der genannten Bits führt dazu, daß Speicher aus einer anderen Zone alloziert wird. Dahinter steht der Gedanke, daß diese Abstraktion auf alle Computer-Plattformen paßt, die spezielle Speicherbereiche kennen (anstatt das gesamte RAM gleich zu behandeln).

DMA-fähiger Speicher ist der einzige Speicher, der bei DMA-Datenübertragungen mit Peripherie-Geräten verwendet werden kann. Diese Einschränkung tritt auf, wenn der Adreß-Bus, mit dem Peripherie-Geräte mit dem Prozessor verbunden werden, gegenüber dem Adreß-Bus, mit dem auf das RAM zugegriffen wird, eingeschränkt ist. Auf der x86-Plattform können Geräte auf dem ISA-Bus beispielsweise nur auf den Speicherbereich von 0 bis 16 MByte zugreifen. Andere Plattformen haben ähnliche Bedürfnisse, wenn auch meist weniger zwingende als der ISA-Bus.[1]

Hoher Speicher benötigt bei der Adressierung eine Sonderbehandlung. Er wurde in die Speicherverwaltung des Kernels eingeführt, als während der 2.3-Entwicklung die Unterstützung für die Pentium II Virtual Memory Extension implementiert wurde, um auf bis zu 64 GByte Speicher zugreifen zu können. Hoher Speicher ist ein Konzept, das es nur auf den x86- und SPARC-Plattformen gibt; die beiden Implementationen sind grundverschieden.

Immer wenn eine neue Seite alloziert wird, um die kmalloc-Anfrage abzudecken, baut der Kernel eine Liste der Zonen auf, die zur Suche verwendet werden können. Wenn __GFP_DMA angegeben wird, wird nur die DMA-Zone durchsucht: Wenn kein Speicher in den unteren Adressen zur Verfügung steht, schlägt die Allokation fehl. Ist kein spezielles Flag angegeben, wird sowohl normaler als auch DMA-Speicher durchsucht; wenn __GFP_HIGHMEM angegeben wird, dann werden alle drei Zonen nach einer freien Seite durchsucht.

Kennt die Plattform keinen hohen Speicher oder ist dieser in der Kernel-Konfiguration abgeschaltet worden, dann wird __GFP_HIGHMEM als 0 definiert und hat keine Wirkung.

> Der Mechanismus der Speicherzonen ist in mm/page_alloc.c implementiert, während die Initialisierung der Zonen in plattformspezifischen Dateien geschieht, normalerweise in mm/init.c im arch-Baum. Wir kommen auf diese Themen noch in Kapitel 13> zurück.

Das Argument size

Der Kernel verwaltet den physikalischen Speicher des Systems, der nur in Einheiten von Speicherseiten verfügbar ist. Als Folge davon sieht kmalloc ziemlich anders aus als eine typische User-Space-malloc-Implementation. Eine einfache, Heap-orientierte Allokationstechnik würde schnell in Schwierigkeiten geraten; es wäre schwierig, um die Seitengrenzen herumzuarbeiten. Daher verwendet der Kernel spezielle seitenorientierte Allokationstechniken, um das System-RAM bestmöglich auszunutzen.

Linux bearbeitet Speicherallokationen indem es eine Menge von Pools von Speicherobjekten fester Größe erzeugt. Allokationsanforderungen werden bearbeitet, indem ein Pool gesucht wird, der entsprechend große Objekte enthält, und dann wird ein ganzes Speicherstück zurückgegeben. Das Speicherverwaltungsverfahren ist reichlich komplex und seine Details sind normalerweise für Autoren von Gerätetreibern überhaupt nicht von Interesse. Schließlich kann sich die Implementation ändern — wie das im Kernel 2.1.38 der Fall gewesen ist —, ohne daß sich dadurch die Schnittstelle, die der Rest des Kernels sieht, ändert.

Treiberentwickler sollten aber im Hinterkopf behalten, daß der Kernel nur bestimmte vordefinierte Byte-Arrays fester Größe allozieren kann. Wenn Sie nach einer beliebigen Speichermenge fragen, bekommen Sie wahrscheinlich etwas mehr, als Sie wollten, möglicherweise sogar doppelt soviel. Außerdem darf man nicht vergessen, daß die kleinste Speichermenge, mit der kmalloc umgehen kann, 32 oder 64 ist, je nach Seitengröße der jeweiligen Architektur.

Die verfügbaren Datengrößen sind im Regelfall Zweierpotenzen. Im 2.0-Kernel waren die verfügbaren Größen aufgrund der vom Verwaltungssystem hinzugefügten Steuer-Flags etwas kleiner als die Zweierpotenzen. Wenn Sie dies im Kopf behalten, werden Sie Ihren Speicher effizienter ausnutzen. Wenn Sie beispielsweise einen Puffer von etwa 2000 Bytes benötigen und Linux 2.0 verwenden, dann fordern Sie besser 2000 als 2048 Bytes an. Das Anfordern einer exakten Zweierpotenz ist das Schlimmste, was in Kerneln vor 2.1.38 passieren kann: Der Kernel wird doppelt soviel wie angefordert allozieren. Deswegen verwendet scull 4000 Bytes per Quantum und nicht 4096.

Sie finden die genauen Werte für die Allokationsblocks in mm/kmalloc.c (im Kernel 2.0) oder in mm/slab.c (in neueren Kerneln), aber denken Sie daran, daß sich diese ohne Vorwarnung ändern können. Der Trick, etwas weniger als 4 KByte zu allozieren, funktioniert bei scull in allen 2.x-Kerneln, muß aber in der Zukunft nicht notwendigerweise optimal sein.

Auf jeden Fall kann kmalloc maximal 128 KByte allozieren, bei 2.0-Kerneln geringfügig weniger. Wenn Sie mehr als ein paar Kilobytes benötigen, gibt es aber bessere Möglichkeiten, den Speicher anzufordern. Diese werden unten beschrieben.

Fußnoten

[1]

Interessanterweise gilt diese Grenze nur für den ISA-Bus; ein x86-Gerät, das an den PCI-Bus angeschlossen wird, kann DMA-Transfers mit dem gesamten Speicher durchführen.