So etwas müsste ja offensichtlich nicht gehen, man kann ja nur das belegen, was man hat…
Ich schreibe hier mal wieder mit Blick auf die Linux-Speicherverwaltung, die ich am besten kenne, aber die Ideen stammen teilweise von früheren Unix-Systemen.
Ein System hat einen physikalischen Speicher (RAM) von einer bestimmten Größe, heute meistens ein paar Gigabyte, aber mein erstes Linux lief in den frühen 90er-Jahren auch mit 4 Megabyte irgendwie. Oft wird das noch ergänzt durch einen sogenannten Swap- oder Paging-Bereich. Das bedeutet, dass man mehr Speicher belegen kann, als da ist und der weniger genutzte Bereich wird auf die Festplatte ausgelagert. Im Falle von ausführbaren Programmen reicht es dafür oft, diese zu memory-mappen, das heißt, dass man die Stelle auf der Platte, wo das Binary steht, verwendet und von dem Programm jeweils nur die aktuell benötigten Teile in den Hauptspeicher lädt, statt das im Swap-Bereich zu duplizieren.
Schön ist es immer, wenn man diesen Swap-Bereich nicht braucht, denn dann läuft alles schneller und RAM ist ja so billig geworden. Gerade wenn man nur SSDs hat, ist das durchaus eine valable Überlegung, weil die SSDs nicht so viel billiger als RAM sind und weil die SSDs durch das besonders häufige Schreiben bei Verwendung als Swap relativ schnell aufgebraucht werden. Mit guten SSDs sollte das durchaus noch brauchbar funktionieren, weil deren Lebensdauer so gestiegen ist, aber dafür muss man schon genau schauen, dass man solche mit vielen Schreibzyklen und einer hohen Schreibgeschwindigkeit kauft und die sind dann wirklich nicht mehr so billig.
Letztlich funktioniert es aber mit magnetischen Festplatten oder mit SSD prinzipiell genauso, solange die Hardware mitmacht. Einen Teil des belegten Speichers im Swap zu haben, ist nicht unbedingt falsch, denn viele Programme belegen Speicherbereiche und benutzen die nur sehr selten. Und weil in der Zeit, wo Speicherbereiche in den Swap-Bereich ein- und ausgelagert werden, andere Threads noch weiterrechnen können, kann es durchaus auch sein, dass man mit einem Teil der häufiger gebrauchten Speicherbereiche im Swap noch eine passable Performance erreichen kann. Das ist vor allem für seltener gebrauchte oft Software vertretbar, wobei man aber den Anwendungsfall genau anschauen muss. Zum Beispiel muss eine aufwendige Jahresendverarbeitung gut geplant werden, weil sie zu einem bestimmten Zeitpunkt abgeschlossen sein soll und vielleicht besonders viele Ressourcen benötigt.
Den Swap-Bereich und den physikalischen Speicher zusammen nennt man virtuellen Speicher. Von diesem wird noch ein Teil durch das Betriebssystem belegt, so dass man den Rest zur Verfügung hat. Nun kann man unter Linux diesen virtuellen Speicher tatsächlich überbelegen. Das nennt sich „overcommit“. Dabei wird in den Kernel-Einstellungen ein Parameter entsprechend gesetzt.
Man kann abfragen, welche overcommit-Strategie eingeschaltet ist:
$ cat /proc/sys/vm/overcommit_memory
0
0 bedeutet, dass nur Speicheranforderungen, die nach einer Heuristik plausibel ist, erlaubt wird, 1 bedeutet, dass alle Speicheranforderungen grundsätzlich erlaubt werden.
$ cat /proc/sys/vm/overcommit_ratio
50
bedeutet, dass eine Überbelegung von 50%, also ein Faktor von bis zu 1.5 möglich ist.
Mit sysctl als root oder durch schreiben in diese Pseudo-Dateien kann man das einmalig ändern, mit Einträgen in /etc/sysctl.conf für die Zukunft ab dem nächsten reboot.
Ein paar Fragen, die sich aufdrängen sind:
- Wie kann das überhaupt funktionieren?
- Wofür braucht man das?
- Was sind die Risiken?
Es gibt verschiedene Möglichkeiten, Speicher anzufordern, aber es reduziert sich im wesentlichen auf malloc
und Konsorten, fork
und exec
.
malloc(), calloc(), sbrk(), realloc() und einige andere fordern einen Speicherbereich an, den ein Programm benutzen möchte. Solange da nichts reingeschrieben wurde, brauchen diese Bereiche aber nicht wirklich zur Verfügung gestellt werden. Erst wenn tatsächlich die Zugriffe darauf erfolgen, ist das nötig. Das soll von älteren Fortran-Versionen kommen. Dort hat man Matrizen und Vektoren statisch allozieren müssen, es musste also im Programm hardcodiert sein, wie groß sie sind. Für kleinere Matrizen hat man einfach nur ein Rechteck in der linken oberen Ecke benutzt. So haben Fortran-Programme früher viel mehr Speicher angefordert, als sie wirklich brauchten und RAM war damals sehr teuer und auch der Plattenplatz für Swap war teuer. So konnte man mit entsprechender Einschätzung der Charakteristik der laufenden Programme mit overcommit das System tunen und mehr damit machen. Auch heute dürften noch viele Programme Speicher allozieren, den sie nicht brauchen, auch wenn das sich heute leichter umgehen ließe als mit Fortran66. Ein spezieller Fall sind Java-Programme, die einen recht großen, konfigurierbaren Speicherbereich anfordern und den dann weitgehend selber verwalten.
Der andere Fall ist das Starten von Prozessen. Ein neuer Prozess braucht Speicher. Nun wird unter Linux ein Prozess in zwei Schritten gestartet. Mit fork() wird ein laufender Prozess dupliziert. Wenn man also von einem großen Prozess aus ein fork() aufruft, braucht man potentiell sehr viel memory. Das wird aber nicht so heiß gegessen wie gekocht. Denn Linux kennt einen Mechanismus „copy on write“, das heißt, die Speicherbereiche werden von beiden Prozessen gemeinsam benutzt, bis einer der Prozesse dort etwas schreibt. Das darf der andere nicht sehen, weshalb der eine Speicherblock vorher dupliziert werden muss, so dass jeder Prozess seinen eigenen hat. fork() lädt also durchaus dazu ein, mit overcommit verwendet zu werden. Um nun einen anderen Prozess zu starten, wird exec() aufgerufen. Dann wird ein neues Programm im laufenden Prozess ausgeführt und dabei fast der gesamte Speicher des Prozesses neu initialisiert. Das kann Memory freigeben oder zusätzlich in Anspruch nehmen. Erhalten bleiben bei exec() zum Beispiel die Umgebungsvariablen und die offenen Dateien und Verbindungen.
Wenn nun aber das ganze angeforderte Memory den virtuellen Speicher überschreitet und auch tatsächlich verwendet wird? Dann tritt der out-of-memory-Killer auf den Plan und beendet einige Prozesse, bis der Speicher wieder ausreicht. Es ist also wichtig, dass der Systemadministrator seine Systeme kennt und beobachtet, wenn diese nicht krass überdimensioniert sind. So oder so führt es zu Problemen, wenn man den virtuellen Speicher vollständig ausschöpft, weil viele Programme bei scheiternden mallocs(), forks() u.s.w. auch nicht mehr korrekt funktionieren können. Damit meine ich nicht unbedingt, dass man sich jeden Tag auf allen Systemen einloggt, um zu schauen, wie es denen geht, sondern eher, dass man Skripte oder Werkzeuge verwendet, um die Daten zu erfassen und zu aggregieren.
Ich habe einmal für ein paar Monate etwa 1’000 Rechner administriert. Wenn man sich in jeden einloggen muss und dabei pro Rechner etwa 10 min verbringt, sind das 10’000 Minuten, also etwa 167 Stunden. Man wäre allein damit also einen Monat lang voll beschäftigt. Benötigt wurden weitgehende Automatisierungen, die es ermöglichten, die Aufgabe in etwa 20 Stunden pro Woche zu bewältigen. Ja, es waren eine sehr homogene Rechnerlandschaft, sonst wäre der Aufwand sicher größer gewesen.