Zwei Sichten auf den Speicher
Die Speicherverwaltung arbeitet mit zwei Arten von Adressen:
- Physische Adressen bezeichnen echte Speicherzellen im RAM. Der Kernel teilt den physischen Speicher in Seitenrahmen (page frames) von meist 4 KiB auf und verwaltet für jeden Rahmen eine kleine Struktur (
struct page). - Virtuelle Adressen sind die Adressen, die Programme (und auch der Kernel selbst) tatsächlich benutzen. Jeder Prozess hat seinen eigenen virtuellen SeitenSeite (Page)Kleinste Einheit, in der die MMU Speicher verwaltet, auf x86-64 meist 4 KiB groß.-Adressraum (Kapitel 2).
Die Übersetzung zwischen beiden erledigt die MMUMMU (Memory Management Unit)Hardware-Baustein der CPU, der virtuelle in physische Adressen übersetzt und dabei Zugriffsrechte prüft. in Hardware, bei jedem einzelnen Speicherzugriff. Der Kernel gibt nur die Regeln vor, indem er Seitentabellen anlegt. Diese Trennung bringt drei große Vorteile:
- Isolation: Ein Prozess kann den Speicher eines anderen gar nicht adressieren.
- Flexibilität: Zusammenhängende virtuelle Bereiche dürfen auf beliebig verstreute physische Seiten abgebildet werden.
- Bedarfsorientierung: Seiten müssen erst existieren, wenn sie benutzt werden, und können vorübergehend ausgelagert sein.
Seitentabellen und Adressübersetzung
Eine einfache Tabelle „virtuelle Seite → physischer Rahmen“ für 128 TiB Adressraum wäre gigantisch: 2³⁵ Einträge à 8 Byte, also 256 GiB pro Prozess. Die Lösung sind mehrstufige Seitentabellen. Sie bilden einen Baum, von dem nur die tatsächlich benutzten Zweige existieren müssen.
-
Schritt 1 von 7
Die Adresse wird zerlegt
Von den 64 Bit der Adresse 0x00007f3a1c2b5e48 sind 48 relevant. Die unteren 12 Bit sind der Offset innerhalb einer 4-KiB-Seite. Die übrigen 36 Bit bilden vier Indizes à 9 Bit – je einer pro Ebene der Seitentabellen. 9 Bit reichen für 512 Einträge, und 512 Einträge à 8 Byte füllen genau eine 4-KiB-Seite.
-
Schritt 2 von 7
Ebene 4: CR3 zeigt auf die oberste Tabelle
Das CPU-Register CR3 enthält die physische Adresse der obersten Tabelle (PGD) des laufenden Prozesses – beim Kontextwechsel wird genau dieses Register umgeschaltet. Bits 47–39 der Adresse (hier 254) wählen den Eintrag, der auf die nächste Tabelle zeigt.
-
Schritt 3 von 7
Ebene 3: Page Upper Directory
Bits 38–30 (hier 232) wählen in dieser Tabelle den Eintrag für die nächste Ebene. Ein Eintrag hier könnte alternativ direkt auf eine 1-GiB-Seite zeigen (Huge Page).
-
Schritt 4 von 7
Ebene 2: Page Middle Directory
Bits 29–21 (hier 225) wählen den nächsten Eintrag. Auf dieser Ebene sind 2-MiB-Seiten möglich – Transparent Huge Pages nutzen das, um Speicher mit einem einzigen Eintrag abzudecken.
-
Schritt 5 von 7
Ebene 1: der Seitentabelleneintrag
Bits 20–12 (hier 181) wählen den eigentlichen Seitentabelleneintrag (PTE). Er enthält die Nummer des physischen Seitenrahmens und Flags: vorhanden (Present), beschreibbar, für den Benutzermodus zugänglich, nicht ausführbar (NX), zuletzt benutzt (Accessed), verändert (Dirty). Fehlt das Present-Bit oder passen die Rechte nicht, löst die MMU einen Seitenfehler aus.
-
Schritt 6 von 7
Rahmen plus Offset ergibt die physische Adresse
Physische Rahmenadresse aus dem PTE plus Offset 0xe48 aus den unteren 12 Bit ergibt die physische Adresse des gesuchten Bytes. Erst jetzt kann der eigentliche Speicherzugriff stattfinden.
-
Schritt 7 von 7
Der TLB merkt sich das Ergebnis
Vier zusätzliche Speicherzugriffe pro Zugriff wären viel zu langsam. Der Translation Lookaside Buffer (TLB) speichert deshalb kürzlich benutzte Übersetzungen. Trifft der nächste Zugriff dieselbe Seite, entfällt die gesamte Tabellenwanderung. Ändert der Kernel Seitentabellen, muss er die betroffenen TLB-Einträge ungültig machen – auf allen CPUs, die sie zwischengespeichert haben könnten.
Linux beschreibt die Ebenen architekturunabhängig als PGD → P4D → PUD → PMD → PTE. Bei 4-stufigen Tabellen wird die P4D-Ebene einfach „durchgereicht“. Mit 5-stufigen Tabellen (57-Bit-Adressen) ist sie echt vorhanden. Auf Architekturen mit weniger Ebenen fallen entsprechend Ebenen weg. Der übrige Kernel-Code muss davon nichts wissen.
Tiefer eintauchenHuge Pages: weniger Ebenen, weniger TLB-Einträge
Ein Eintrag auf PMD-Ebene kann statt auf eine weitere Tabelle direkt auf eine 2-MiB-Seite zeigen, ein PUD-Eintrag sogar auf eine 1-GiB-Seite. Solche Huge PagesHuge PageSpeicherseite, die größer als die Standardgröße ist, auf x86-64 2 MiB oder 1 GiB. Spart Einträge in Seitentabellen und im TLB. sparen eine oder zwei Ebenen der Tabellenwanderung und vor allem TLB-Einträge: Ein einziger Eintrag deckt 512-mal so viel Speicher ab. Das bringt Datenbanken, virtuellen Maschinen und wissenschaftlichen Anwendungen mit großen Datenmengen messbar mehr Leistung.
Linux bietet zwei Wege:
- hugetlbfs: Huge Pages werden beim Start reserviert und von Anwendungen ausdrücklich angefordert.
- Transparent Huge Pages (THP): Der Kernel verwendet automatisch 2-MiB-Seiten, wenn ein passender, ausgerichteter Bereich vorhanden ist, und fasst kleine Seiten im Hintergrund zusammen (
khugepaged). Die Einstellung steht in/sys/kernel/mm/transparent_hugepage/enabled.
Seit einigen Versionen gibt es zusätzlich Folios: Der Kernel verwaltet zusammenhängende Gruppen von Seiten als eine Einheit (struct folio), etwa 16 oder 64 KiB im Page Cache. Das verringert den Verwaltungsaufwand, auch ohne echte Huge Pages.
Der TLB und seine Kosten
Weil jeder Zugriff ohne Cache bis zu vier zusätzliche Speicherzugriffe erfordern würde, ist der TLBTLB (Translation Lookaside Buffer)Kleiner, sehr schneller Cache in der CPU, der kürzlich benutzte Übersetzungen von virtuellen in physische Seitenadressen speichert. entscheidend für die Leistung. Er hat nur einige hundert bis wenige tausend Einträge, deckt mit 4-KiB-Seiten also nur wenige Megabyte direkt ab. Programme, die zufällig auf große Datenmengen zugreifen, leiden deshalb unter TLB-Misses.
Ändert der Kernel eine Seitentabelle, muss er zwischengespeicherte Übersetzungen ungültig machen. Auf Mehrkernsystemen bedeutet das oft einen TLB-Shootdown: Die CPU schickt allen anderen CPUs, die den Adressraum gerade benutzen, einen Interrupt, damit auch sie ihre Einträge verwerfen. Das ist einer der Gründe, warum Operationen wie munmap() auf großen Systemen teuer werden können.
Der Adressraum eines Prozesses
Der virtuelle Adressraum eines Prozesses wird durch struct mm_struct beschrieben. Er besteht aus einer Reihe von VMAsVMA (Virtual Memory Area)Zusammenhängender Bereich im Adressraum eines Prozesses mit einheitlichen Rechten und gleicher Herkunft, etwa der Code eines Programms, der Heap oder eine eingeblendete Datei (struct vm_area_struct). (virtual memory areas, struct vm_area_struct). Jede VMA ist ein zusammenhängender Bereich mit einheitlichen Eigenschaften:
| Eigenschaft | Beispiele |
|---|---|
| Lage | Start- und Endadresse, seitenweise ausgerichtet |
| Rechte | lesen, schreiben, ausführen (r, w, x) |
| Herkunft | dateibasiert (Programmcode, Bibliotheken, mmap einer Datei) oder anonym (Heap, Stack, malloc) |
| Teilung | privat (Änderungen per Copy-on-Write) oder gemeinsam (MAP_SHARED) |
Genau diese Liste zeigt /proc/<pid>/maps. Seit Linux 6.1 verwaltet der Kernel die VMAs in einem Maple Tree, einem für diesen Zweck entwickelten B-Baum, der schnelles Suchen und Iterieren erlaubt und sich gut für nebenläufige Zugriffe eignet.
Wichtig: Eine VMA ist nur eine Zusage. mmap() oder malloc() legen zunächst nur eine VMA an. Physischer Speicher wird dabei nicht belegt. Den bekommt der Prozess erst beim ersten Zugriff, über einen Seitenfehler.
Seitenfehler
Ein SeitenfehlerSeitenfehler (Page Fault)Ausnahme, die die MMU auslöst, wenn für eine Adresse kein gültiger Seitentabelleneintrag existiert oder die Rechte nicht passen. Meist kein Fehler, sondern Anlass für den Kernel, Speicher bereitzustellen. tritt auf, wenn die MMU für eine Adresse keinen gültigen Eintrag findet oder die Rechte nicht ausreichen. Die CPU unterbricht den Befehl, legt die betroffene Adresse in das Register CR2 und springt in den Kernel:
Man unterscheidet zwei Arten:
- Minor Faults lassen sich ohne Zugriff auf einen Datenträger auflösen. Beispiele sind eine neue, mit Nullen gefüllte Seite, eine Copy-on-Write-Kopie oder eine Datei-Seite, die schon im Page Cache liegt. Sie kosten Mikrosekunden.
- Major Faults müssen auf einen Datenträger warten, weil die Seite erst aus einer Datei oder dem Swap gelesen werden muss. Sie kosten je nach Datenträger Mikrosekunden (NVMe) bis Millisekunden (Festplatte), und der Task schläft so lange.
Die zentrale Funktion ist handle_mm_fault in mm/memory.c. Sie wird nicht nur von der CPU-Ausnahme aufgerufen, sondern auch, wenn der Kernel selbst Seiten eines Prozesses braucht, etwa bei copy_from_user() (Kapitel 4).
Tiefer eintauchenWie der Kernel die VMA sperrt
Bei der Behandlung eines Seitenfehlers muss die VMA stabil bleiben: Ein anderer Thread desselben Prozesses könnte sie gleichzeitig mit munmap() entfernen. Früher schützte eine einzige Sperre pro Adressraum (mmap_lock) alle VMAs. Bei Programmen mit vielen Threads war das ein Engpass.
Seit Linux 6.4 versucht der Kernel zuerst, nur die betroffene VMA zu sperren (lock_vma_under_rcu()). Die Suche geschieht dabei RCU-geschützt im Maple Tree (Kapitel 9). Nur wenn das nicht klappt, fällt er auf die globale Sperre zurück. Seitenfehler in verschiedenen Bereichen desselben Prozesses können so parallel bearbeitet werden.
Physischen Speicher verteilen
Der Kernel hat viele „Kunden“ für Speicher: Prozesse, den Dateicache, Netzwerkpuffer, Treiber und seine eigenen Datenstrukturen. Dafür gibt es mehrere Schichten von Allokatoren:
Knoten und Zonen
Physischer Speicher ist zunächst nach NUMA-Knoten gegliedert. Auf Systemen mit mehreren Prozessorsockeln ist jedem Sockel „sein“ Speicher zugeordnet, auf den er schneller zugreift als auf den der anderen. Der Kernel versucht, Speicher möglichst auf dem Knoten der anfragenden CPU zu vergeben.
Jeder Knoten ist weiter in Zonen unterteilt, weil nicht jeder Speicher für jeden Zweck taugt:
| Zone | Zweck |
|---|---|
ZONE_DMA | die ersten 16 MiB, für sehr alte Geräte mit eingeschränkter Adressierung |
ZONE_DMA32 | die ersten 4 GiB, für Geräte, die nur 32-Bit-Adressen beherrschen |
ZONE_NORMAL | der gesamte übrige Speicher |
ZONE_MOVABLE | nur verschiebbare Seiten, wichtig für das Entfernen von Speicher im Betrieb und für Huge Pages |
Der Buddy-Allocator
Die unterste Schicht verwaltet physische Seiten in Blöcken von 2ⁿ zusammenhängenden Seiten. Wie sie das schnell und ohne starke Zersplitterung schafft, zeigt der folgende Ablauf:
-
Schritt 1 von 6
Ausgangslage
Der Allocator verwaltet freien Speicher in Blöcken von 2ⁿ Seiten – n heißt Ordnung und reicht von 0 (eine Seite, 4 KiB) bis 10 (1024 Seiten, 4 MiB). Für jede Ordnung gibt es eine Freiliste. Hier ist ein einziger freier Block der Ordnung 4 (16 Seiten) vorhanden.
-
Schritt 2 von 6
Anforderung: eine Seite – Block halbieren
Jemand fordert eine einzelne Seite an (Ordnung 0). In den Freilisten der Ordnungen 0 bis 3 ist nichts, also wird der Block der Ordnung 4 halbiert. Die beiden Hälften heißen Buddies; die rechte kommt in die Freiliste der Ordnung 3.
-
Schritt 3 von 6
Weiter halbieren
Die linke Hälfte (8 Seiten) ist immer noch zu groß und wird erneut geteilt. Ein Block der Ordnung 2 landet in der Freiliste.
-
Schritt 4 von 6
Und noch einmal
Aus 4 Seiten werden zwei Blöcke à 2 Seiten. Einer davon wandert in die Freiliste der Ordnung 1.
-
Schritt 5 von 6
Zuteilung
Der letzte Block wird in zwei Einzelseiten geteilt: Eine wird vergeben, ihr Buddy kommt in die Freiliste der Ordnung 0. Die Zuteilung hat nur wenige Schritte gekostet – höchstens so viele, wie es Ordnungen gibt.
-
Schritt 6 von 6
Freigabe: Buddies verschmelzen
Wird die Seite freigegeben, prüft der Allocator, ob ihr Buddy ebenfalls frei ist. Wenn ja, verschmelzen beide zu einem Block der nächsthöheren Ordnung – und das wiederholt sich, solange der jeweilige Buddy frei ist. Hier entsteht wieder der ursprüngliche Block mit 16 Seiten. Die Adresse eines Buddies lässt sich übrigens mit einem einzigen XOR berechnen.
Im Kernel-Code fordert man Seiten mit alloc_pages(gfp, order) an. Der erste Parameter, die GFP-Flags (get free pages), beschreibt, unter welchen Bedingungen die Anforderung erfüllt werden darf:
| Flag | Bedeutung |
|---|---|
GFP_KERNEL | Normalfall im Prozesskontext. Darf schlafen, Speicher zurückgewinnen und I/O auslösen. |
GFP_ATOMIC | im Interruptkontext oder unter einem Spinlock. Darf nicht schlafen, greift notfalls auf Reserven zu. |
GFP_USER / GFP_HIGHUSER | Seiten für den User Space |
GFP_NOIO, GFP_NOFS | darf keine I/O bzw. keine Dateisystem-Operation auslösen (verhindert Verklemmungen) |
SLUB: kleine Objekte
Die meisten Datenstrukturen des Kernels sind viel kleiner als eine Seite: eine task_struct, ein Inode, ein Netzwerkpuffer-Kopf. Für sie gibt es den Slab-Allocator, seit Linux 6.8 ausschließlich in der Variante SLUB:
- Für jeden häufig benutzten Objekttyp gibt es einen Cache (
kmem_cache_create()), der Seiten vom Buddy-Allocator holt und in gleich große Objekte aufteilt. - Freigegebene Objekte kommen in den Cache zurück und stehen sofort wieder zur Verfügung, ohne dass Seiten zurückgegeben werden müssen.
- Jede CPU hat eigene Listen, sodass die meisten Anforderungen ganz ohne Sperren auskommen.
Für Objekte ohne eigenen Cache gibt es kmalloc(size, gfp), das auf Caches für feste Größenklassen (8, 16, 32, … Byte) zurückgreift. kmalloc() liefert physisch zusammenhängenden Speicher.
vmalloc()
Braucht der Kernel einen großen Puffer, der nur virtuell zusammenhängend sein muss, nimmt er vmalloc(). Es holt einzelne Seiten vom Buddy-Allocator, die physisch verstreut sein dürfen, und blendet sie in einen zusammenhängenden Bereich des Kernel-Adressraums ein. Das ist langsamer als kmalloc(), scheitert aber nicht an Zersplitterung. Kernel-Module werden zum Beispiel in einen solchen Bereich geladen. kvmalloc() versucht zuerst kmalloc() und weicht bei Bedarf auf vmalloc() aus.
Der Page Cache
Linux verwendet freien Arbeitsspeicher, statt ihn ungenutzt zu lassen: als Page CachePage CacheTeil des Arbeitsspeichers, in dem der Kernel Dateiinhalte zwischenspeichert, um Zugriffe auf Datenträger zu vermeiden. für Dateiinhalte. Jedes read() und jeder Seitenfehler auf eine eingeblendete Datei geht zuerst dorthin:
- Lesen: Liegt die Seite schon im Cache, sind keine Datenträgerzugriffe nötig. Liest ein Programm eine Datei der Reihe nach, lädt der Kernel vorausschauend weitere Seiten (Readahead).
- Schreiben:
write()ändert zunächst nur die Seite im Cache und markiert sie als dirty. Kernel-Threads schreiben solche Seiten später gesammelt zurück (Writeback), spätestens nach etwa 30 Sekunden oder wenn zu viele Seiten dirty sind. Wer sicher sein will, dass Daten auf dem Datenträger angekommen sind, ruftfsync()auf.
Weil Programme und Dateien über denselben Cache laufen, teilen sich auch mehrere Prozesse, die dieselbe Bibliothek benutzen, deren Seiten im RAM. Die Bibliothek liegt nur einmal im Speicher, egal wie viele Programme sie einblenden.
Wenn der Speicher knapp wird
Der Kernel versucht, immer einen gewissen Vorrat an freien Seiten zu halten. Wird er knapp, beginnt die Speicherrückgewinnung (reclaim):
- Saubere Datei-Seiten aus dem Page Cache werden einfach verworfen, denn sie lassen sich jederzeit neu von der Datei lesen.
- Dirty Datei-Seiten werden erst zurückgeschrieben, dann verworfen.
- Anonyme Seiten (Heap, Stack) haben keine Datei hinter sich. Sie können nur in den SwapSwapBereich auf einem Datenträger (oder komprimiert im RAM), in den der Kernel selten benutzte anonyme Speicherseiten auslagert, um Arbeitsspeicher freizumachen. ausgelagert werden, also auf eine Swap-Partition, in eine Swap-Datei oder komprimiert in den RAM (zram, zswap).
Welche Seiten gehen, entscheidet der Kernel anhand von LRU-Listen (least recently used): Seiten, auf die lange nicht zugegriffen wurde, sind zuerst dran. Seit Linux 6.1 gibt es dafür zusätzlich den genaueren Multi-Gen LRU, der Seiten nach mehreren „Generationen“ ihres letzten Zugriffs einteilt. Die Arbeit erledigt im Hintergrund der Kernel-Thread kswapd. Reicht das nicht, muss der anfordernde Task selbst Speicher zurückgewinnen (direct reclaim) und wird dabei spürbar langsamer.
Overcommit und der OOM-Killer
Linux sagt standardmäßig mehr Speicher zu, als physisch vorhanden ist (Overcommit), weil viele Programme reservierten Speicher nie vollständig nutzen. Das Verhalten steuert /proc/sys/vm/overcommit_memory:
| Wert | Verhalten |
|---|---|
0 | Heuristik (Standard): offensichtlich unerfüllbare Anforderungen werden abgelehnt |
1 | immer zusagen |
2 | strikt: nur so viel zusagen, wie Swap plus ein einstellbarer Anteil des RAM hergeben |
Geht die Rechnung nicht auf und lässt sich auch durch Rückgewinnung nichts mehr freimachen, greift der OOM-KillerOOM-KillerNotmechanismus des Kernels: Lässt sich trotz aller Maßnahmen kein Speicher mehr freimachen, beendet er gezielt einen Prozess, meist den mit dem größten Speicherverbrauch. (out of memory) ein. Er bewertet alle Prozesse, vor allem nach ihrem Speicherverbrauch (oom_badness), und beendet den mit der höchsten Punktzahl mit SIGKILL. Über /proc/<pid>/oom_score_adj (−1000 bis 1000) lässt sich das beeinflussen. −1000 schützt einen Prozess vollständig.
In Systemen mit cgroups gibt es den OOM-Killer auch pro Gruppe: Überschreitet ein Container sein Speicherlimit, wird ein Prozess innerhalb dieses Containers beendet, nicht irgendwo im System. Viele Distributionen setzen außerdem systemd-oomd ein, das schon früher eingreift, wenn das System unter anhaltendem Speicherdruck steht.
free richtig lesen
Ein häufiges Missverständnis: „Linux frisst meinen Speicher, free zeigt kaum etwas frei!“ Ein Blick auf die Spalten klärt das:
total used free shared buff/cache available
Mem: 31Gi 9,2Gi 1,1Gi 1,0Gi 21Gi 21Gi
Swap: 8,0Gi 512Mi 7,5Gi
- free: wirklich ungenutzter Speicher. Er ist auf einem gesunden System klein, weil ungenutzter Speicher verschwendeter Speicher ist.
- buff/cache: Page Cache und Kernel-Caches. Der größte Teil davon lässt sich bei Bedarf sofort freigeben.
- available: die Schätzung des Kernels, wie viel Speicher für neue Programme verfügbar ist, ohne auszulagern. Das ist die aussagekräftige Zahl.
Selbst ausprobieren
Selbst ausprobieren: Speicher untersuchen
# Überblick und Details
free -h
head -n 25 /proc/meminfo
# Seitengröße
getconf PAGESIZE
# Adressraum der Shell: VMAs mit Rechten und Herkunft
cat /proc/$$/maps
# Wie viel davon liegt tatsächlich im RAM? (RSS je VMA, kompakt)
pmap -x $$ | tail -n 5
# Freie Blöcke je Ordnung und Zone (Spalten: Ordnung 0 bis 10)
cat /proc/buddyinfo
# Die größten Slab-Caches des Kernels
sudo slabtop -o | head -n 15
# Seitenfehler eines Programms zählen (minor und major; benötigt das Paket „time“)
/usr/bin/time -v ls -R /usr/share > /dev/null 2> /tmp/zeit.txt; grep -i "page faults" /tmp/zeit.txt
# Speicherdruck und Auslagerung live (Spalten si/so = Swap rein/raus)
vmstat 1 5
# OOM-Bewertung der eigenen Shell
cat /proc/$$/oom_score /proc/$$/oom_score_adjSelbst ausprobieren: Den Page Cache spüren
# Eine 1-GiB-Testdatei anlegen
dd if=/dev/urandom of=/tmp/testdatei bs=1M count=1024
# Page Cache leeren (nur zum Testen – verlangsamt das System kurzzeitig)
sync; echo 3 | sudo tee /proc/sys/vm/drop_caches
# Erster Lesevorgang: vom Datenträger
time cat /tmp/testdatei > /dev/null
# Zweiter Lesevorgang: aus dem Page Cache – deutlich schneller
time cat /tmp/testdatei > /dev/null
rm /tmp/testdateiZwischen den beiden Läufen kannst du mit free -h beobachten, wie buff/cache um etwa 1 GiB wächst.