Ich kann sehen, warum du verwirrt bist. Das Diagramm ist etwas verwirrend und möglicherweise falsch.
Lassen Sie uns zunächst überlegen, warum ein Kernel einen Speicherzuweiser unterhalb der Seitenebene benötigt. Dies ist wahrscheinlich bereits etwas, das Sie meistens kennen, aber ich werde es der Vollständigkeit halber durchgehen.
Seiten sind die typische "Einheit" von Speicheroperationen. Wenn eine User-Space-Anwendung Speicher zuweist oder eine Datei oder ähnliches speichert, erhält sie normalerweise ein Vielfaches der Maschinenseitengröße. Es gibt einige bemerkenswerte Ausnahmen; Windows verwendet 64 KB als Zuweisungseinheit für den virtuellen Speicher, unabhängig von der Seitengröße der CPU. Lassen Sie es uns trotzdem so sehen.
Auf einer modernen CPU verfügt der Benutzerbereichscode über einen flachen Adressraum. Dies ist tatsächlich eine Illusion, die vom virtuellen Speichersystem bereitgestellt wird. Das Betriebssystem stellt Seiten von überall im RAM bereit (oder möglicherweise überhaupt nicht im RAM, im Fall von ausgelagertem Speicher oder Dateien mit Speicherzuordnung) und ordnet sie einem zusammenhängenden virtuellen Adressraum zu.
Der Sinn all dessen ist, dass abgesehen von einigen Sonderfällen für das Betriebssystem selbst (möglicherweise DMA-Puffer, möglicherweise einige spezielle Datenstrukturen, die beim Booten eingerichtet wurden, oh und das Kernel-Image selbst) der Betriebssystem-Kernel wahrscheinlich nie muss Verwalten Sie jeden RAM-Block, der größer als eine Seite ist. Dies vereinfacht die Dinge enorm, da jede Zuordnung und Freigabe auf den Seiten gleich groß ist. Außerdem wird die externe Fragmentierung auf Makroebene effektiv beseitigt.
Kernel müssen jedoch auch einige eigene Datenstrukturen implementieren, und dafür benötigen sie eine andere Art von Speicherzuweiser. Diese Datenstrukturen können normalerweise als Sammlung einzelner Objekte betrachtet werden (z. B. kann ein Objekt ein "Thread" oder ein "Mutex" sein). Die Größe dieser Objekte ist normalerweise viel kleiner als eine Seite.
So kann beispielsweise ein Objekt, das die Sicherheitsanmeldeinformationen eines Prozesses darstellt (z. B. die Benutzer- und Gruppen-ID in POSIX), nur etwa 16 Byte groß sein, während ein "Prozess" oder "Thread" möglicherweise bis zu 16 Byte lang ist 1kb groß. Natürlich möchten Sie für diese kleinen Datensätze nicht eine ganze Seite verwenden. Daher sollten Sie oben auf den Seiten einen Allokator implementieren.
Das untergeordnete Zuordnungssystem muss viele der gleichen Probleme wie das Zuweisungssystem auf Seitenebene erfüllen: Es muss relativ schnell sein (auch auf Multicore-Systemen), Sie möchten die Fragmentierung minimieren und so weiter. Noch wichtiger ist jedoch, dass es je nach Art der gespeicherten Datenstruktur einstellbar und konfigurierbar sein sollte.
Einige Datenstrukturen sind von Natur aus "Cache-ähnlich". Beispielsweise verwalten viele Betriebssysteme einen Cache mit Pfadnamen zu Dateisystemobjekten, um lange Ketten der Verzeichnissuche zu vermeiden (in Unix-Sprache als "Namenscache" oder "Namei-Cache" bezeichnet). Diese Objekte werden nur für die Leistung und nicht für die Korrektheit benötigt. Sie können also (theoretisch) einfach eine ganze Seite voller Einträge vergessen, wenn der Speicher knapp ist und Sie einen Seitenrahmen schnell freigeben müssen.
Andere Datenstrukturen können auf die Festplatte übertragen werden, wenn der Speicher knapp ist und Sie sie nicht bald benötigen. Aber Sie möchten das nicht mit Datenstrukturen tun, die den Austausch oder das virtuelle Speichersystem steuern!
Einige Datenstrukturen können ohne Nachteile im Speicher verschoben werden (z. B. wenn niemand mit einem Zeiger auf sie verweist), sodass sie sich selbst "komprimieren" können, um bei Bedarf eine Fragmentierung zu vermeiden.
Die Hauptidee des Plattenzuordners ist also, dass auf einer Seite nur Datenstrukturen desselben "Typs" gespeichert werden sollen. Dies markiert alle Kästchen: Jedes Objekt auf einer Seite hat dieselbe Größe, sodass keine externe Fragmentierung erfolgt. Objekte desselben "Typs" haben dieselben Leistungsanforderungen und dieselbe Semantik.
Übrigens ist es eine ähnliche Geschichte mit Zuordnung. Für einige Objekttypen ist es wahrscheinlich in Ordnung zu warten, wenn nicht sofort Speicher verfügbar ist, um dieses Objekt zuzuweisen. Ein Beispiel könnte ein Objekt sein, das eine geöffnete Datei darstellt. Das Öffnen einer Datei ist im besten Fall eine teure Operation, sodass ein längeres Warten nicht so weh tut.
Bei anderen Objekttypen (z. B. einem Objekt, das ein Echtzeitereignis darstellt, das in einer bestimmten Zeit eintreten muss) möchten Sie wirklich nicht warten. Daher ist es sinnvoll, einige Objekttypen zu stark zuzuweisen (z. B. einige freie Seiten in Reserve zu haben), damit Anforderungen ohne Wartezeit erfüllt werden können.
Grundsätzlich erlauben Sie jedem Objekttyp, einen eigenen Allokator zu haben, der für die Anforderungen dieses Objekts konfiguriert werden kann. Diese Zuordnungen pro Objekt werden verwirrenderweise als "Caches" bezeichnet. Sie ordnen einen Cache pro Objekttyp zu. (Ja, normalerweise implementieren Sie auch einen "Cache von Caches".) In jedem Cache werden nur Objekte desselben Typs gespeichert (z. B. nur Thread-Strukturen oder nur Adressraumstrukturen).
Jeder Cache verwaltet wiederum "Platten". Eine Platte ist ein Seitenrahmen, der ein Array von Objekten des gleichen Typs enthält. Die Platten können "voll" (alle verwendeten Objekte), "leer" (keine verwendeten Objekte) oder "teilweise" (einige verwendete Objekte) sein.
Teilplatten sind wahrscheinlich die interessantesten, da der Plattenverteiler für jede Teilplatte eine freie Liste führt. (Vollplatten und leere Platten benötigen keine freie Liste.) Objekte werden zuerst von Teilplatten (und wahrscheinlich zuerst von den "vollsten" Teilplatten zuerst) zugewiesen, um zu vermeiden, dass nicht benötigte Seiten zugewiesen werden.
Das Schöne an der Plattenzuweisung ist, dass alle diese Zuweisungsrichtlinienoptionen (sowie die Speichersemantik) für jede Art von Objekt optimiert werden können. Einige Caches enthalten möglicherweise einen Pool leerer Platten, andere möglicherweise nicht. Einige können möglicherweise in einen sekundären Speicher ausgelagert werden, andere möglicherweise nicht.
Linux verfügt über drei verschiedene Arten von Slab-Allokatoren, je nachdem, ob Sie Kompaktheit, Cache-Freundlichkeit oder Geschwindigkeit benötigen oder nicht. Vor ein paar Jahren gab es eine gute Präsentation dazu, die die Kompromisse gut erklärt.
Der Solaris-Plattenzuweiser ( Einzelheiten siehe Papier ) enthält einige weitere Details, um die Leistung noch weiter zu steigern. Zunächst wird in Solaris alles mit der Zuweisung von Platten erledigt, einschließlich der Zuweisung von Seitenrahmen. (Dies ist die Solaris-Lösung zum Zuweisen von Objekten, die größer als eine halbe Seite sind.) Sie verwaltet kleinere Objekte, indem sie die Plattenzuordnungen in den der Platte zugewiesenen Speicherplatz verschachtelt.
Einige Objekte in Solaris erfordern eine komplexe und teure Konstruktion und Zerstörung (z. B. Objekte mit Kernel-Sperre) und können daher "teilweise frei" sein (dh konstruiert, aber nicht zugewiesen). Solaris optimiert auch die Zuweisung freier Slabs, indem freie Listen pro CPU verwaltet werden, um sicherzustellen, dass einige Vorgänge vollständig wartungsfrei sind.
Zur Unterstützung für allgemeine Zwecke Zuordnung (Größe zB für Arrays , die nicht zur Compile-Zeit bekannt), die meisten macrokernel-Typ Betriebssysteme haben auch Caches , das Objekt darstellen Größen eher als Objekt - Typen . FreeBSD verwaltet beispielsweise Caches für unbekannte Objekte, deren Größe Potenzen von 2 Bytes von 4 bis 256 beträgt.
Ich hoffe, Sie können sehen, dass die Plattenzuweisung ein sehr flexibler Rahmen ist, der auf die Bedürfnisse verschiedener Arten von Daten abgestimmt werden kann. Es konkurriert nicht mit Paging, sondern ergänzt es (obwohl in Solaris Seitenrahmen mit Platten zugewiesen werden).
Ich hoffe das hilft. Lassen Sie mich wissen, wenn etwas geklärt werden muss.