Speicherverwaltung
Jeder Programmierer hätte am liebsten einen privaten, unendlich großen, unendlich schnellen Speicher, der dazu noch nicht flüchtig ist, das heißt, dessen Inhalt nicht verloren geht, wenn die Stromversorgung unterbrochen wird. Wenn wir schon dabei sind, könnte er eigentlich auch noch billig sein. Dummerweise funktioniert realer Speicher zurzeit so nicht (Tanenbaum & Bos 2016:2381)).
Speicherhierarchie
Die folgende Grafik zeigt das Prinzip der Speicherhierarchie:
Je schneller Speicher ist, desto teurer und kleiner ist er. Das war schon immer so und es ist nicht absehbar, dass sich das bald ändern sollte.
Klar, heutige SSD's sind schon deutlich schneller als die (immer noch erhältlichen) Festplatten. Aber deren Geschwindigkeiten sind kein Vergleich zum Arbeitsspeicher (RAM) oder zu den Registern in den Prozessoren.
Darstellung von Arbeitsspeicher
Damit ein Prozessor sinnvoll arbeiten kann, benötigt er zwar eine Vielzahl von Registern, aber die sind in ihrer Anzahl eben auch sehr begrenzt. Aus diesem Grund muss ein Prozessor und dementsprechend ein Betriebssystem zu einem wichtigen Teil mit dem Arbeitsspeicher (RAM) arbeiten.
In Lehrbüchern wird der Arbeitsspeicher meist auf zwei Arten gezeichnet:
Im linken Bild wird der Speicher so dargestellt, dass die kleinste Speicheradresse unten dargestellt wird und die Speicheradressen nach oben hin zunehmen. Im rechten Bild ist es gerade umgekehrt. Man findet beide Versionen. Lassen Sie sich dadurch nicht verwirren😂.
Adressierung des Arbeitsspeichers
Der Arbeitsspeicher von üblichen Personal Computern wird Byte-weise angesprochen.
Sie können sich den Arbeitsspeicher wie folgt vorstellen:
Auffallend ist, dass an jeder Speicheradresse ein ganzes Byte, also 8 Bit, vorhanden sind. Ein einzelnes Bit können Sie nicht ansprechen. Das rot markierte Bit ist nicht direkt ansprechbar resp. es gibt keine Adresse, welche dieses Bit ändern kann.
Bei heutigen Prozessoren speichern Sie aber in der Regel nicht nur eine 1 Byte grosse Zahl ab, sondern häufig ist die Zahl 32Bit oder 64Bit lang.
Speichern Sie an der Speicheradresse 0x0 eine 32Bit lange Zahl ab, dann überschreiben Sie sämtliche 4 Byte in der folgenden Darstellung:
Welches ist nun die nächstfreie Speicheradresse? Was denken Sie?
Hexadezimale Zahlen
Noch ein kleiner Hint dazu. Sie lesen hier immer von Adressen wie 0x100 oder 0x200 oder 0x1000.
Erinnern Sie sich an das hexadezimale Zahlensystem zurück?
- 0x10 entspricht der dezimalen Zahl 16
- 0x20 entsprechen zwei Mal 16, also 32
- 0x30 entsprechen drei Mal 16, also 48, etc.
- 0x100 entsprechen der dezimalen Zahl 256
- 0x200 der Zahl 512
- 0x400 der Zahl 1024, etc.
Abstraktionen
Wenn wir über den Aufbau des Arbeitsspeichers sprechen, müssen wir wiederum über Abstraktionen sprechen.
Das Konzept der Abstraktion haben Sie bereits im Kapitel Was ist ein Betriebssystem und wozu dient es? kennengelernt und tauchte anschliessend immer wieder auf.
Auch beim der Speicherverwaltung (in der Regel Verwaltung des Arbeitsspeichers) müssen Abstraktionen geschaffen werden. Sie als Entwickler*in haben keine Lust, direkt physische Speicheradressen anzusprechen, wenn Sie in Python folgende Zeile schreiben:
a = 3
Das Konzept der Variablen in Python ist übrigens auch eine Abstraktion. Ihnen ist es egal, wo diese Zahl 3 im Speicher gespeichert wird. Sie gehen aber davon aus, dass das Python-Programm den Speicherort wiederfindet.
Systeme ohne Speicherabstraktion
Bei den ersten Arbeitsspeichern ab ca. 1955 gab es noch überhaupt keine Abstraktion des Arbeitsspeichers, sondern jede physische Adresse wurde direkt angesprochen.
Sie haben das folgende Bild im Video zur Prozessverwaltung gesehen:
Auch die ersten PCs mit den ersten Intel Prozessoren (8086) kannten noch keine Speicherabstraktion. Da konnten Sie in Ihrem Assembler- oder C-Programm jede beliebige Speicheradresse mit einem beliebigen Wert überschreiben. So waren Sie in der Lage, ohne Rücksicht auf Verluste, Speicheradressen beispielsweise vom Betriebssystem oder anderen geladenen Programmen mit irgendwelchen Werten zu überschreiben. Das konnte ziemlich ungesund sein…
Tanenbaum nennt drei mögliche Varianten, wie beispielsweise das Betriebssystem geladen wurde:
Es gab einige Betriebssysteme (vor allem im Heimcomputer-Bereich, also z.B. Commodore C64, Schneider CPC, etc.), die funktionierten nach der Methode b: da war das Betriebssystem im ROM gespeichert. Sobald man den Computer einschaltete, bootete der Computer vom ROM.
Der Vorteil hier war, dass das ROM (weil eben READ ONLY MEMORY) nicht überschrieben werden konnte.
Andere Betriebsssyteme wie MS-DOS wurden von Diskette in den Arbeitsspeicher geladen (Modell a und b). Hier lief man in der Tat die Gefahr, dass ein Anwendungsprogramm auch die Speicheradressen des Betriebssystems überschreiben konnte. So konnte es gelegentlich passieren, dass der Computer entweder blockiert war oder einfach neu bootete.
Bei solchen Betriebssystemen war es nahezu unmöglich, dass man zwei Programme im Speicher halten konnte.
Ein Programm wurde beispielsweise an die Adresse 0x8000 (dezimal 32786) geladen. Es war häufig unklar, wie viel Arbeitsspeicher das Programm benötigen würde. So konnte man nicht einfach ein weiteres Programm an der Adresse 0x900 (4KB weiter oben) laden. Zudem war absolut unklar, in welche Speicheradressen ein Programm Daten hineinschrieb. So kann das eine Programm in den Speicherbereich des anderen Programms reinschreiben.
Dementsprechend blieb einem nichts anderes übrig, als der ganze Arbeitsspeicher dem Anwendungsprogramm zur Verfügung zu stellen. Der Prozessor und das Betriebssystem boten in diesen ersten Versionen keinen Speicherschutz an.
Systeme mit Speicherabstraktion und Speicherschutz
Durch die Einführung einer Speicherabstraktion war man in der Lage, einen Speicherschutz umzusetzen.
Das Konzept des Adressraums
Horst Schirmeier hat in seiner Vorlesung das "fence register" beschrieben. Um beim obigen Bild bei den Modellen a und c zumindest das Betriebssystem im unteren Speicherbereich vor versehentlichem Überschreiben zu schützen, wurde beim Aufruf einer Speicheradresse automatisch der Wert des "fence registers" dazu addiert. So konnte verhindert werden, dass z.B. die Speicheradresse 0x0 überschrieben wurde (weil eben automatisch z.B. der Wert 0x300 addiert wurde). Die tiefste Adresse, die ein Anwendungsprogramm so überschreiben konnte, war 0x300 (dezimal 768).
Noch ein Beispiel: in Assembler haben wir beispielsweise den folgenden Befehl:
JMP 34
Das heisst, das Programm soll an die Speicheradresse 34 springen.
Wird das "fence register" mit dem Wert 10 verwendet, springt das Programm eben nicht an die Stelle 34, sondern an die Stelle 44. Das wird automatisch addiert.
Einfacher Speicherschutz durch ein Base-Register
Das folgende Video zeigt einen einfachen Speicherschutz mit Hilfe des Base-Registers. Das entspricht dem "fence"-Register von Schirmeier.
Weiter hinten im Video hat er den Mehrprogrammbetrieb beschrieben, in dem ein Programm A und ein Programm B in den Speicher geladen werden können:
Laufen konnte aber immer nur ein einziges Programm. Denken Sie an das Modul "Rechnerarchitektur" zurück. Da hatten wir einen sog. "instruction pointer", welcher auf die Speicherstelle des jeweils nächsten Befehls zeigt. Das konnte zu dieser Zeit nur eine einzige Speicheradresse sein.
Das "fence" Register reicht nicht mehr. Wir brauchen nun zwei Register, um die untere und die obere Grenze des Speicherbereichs für die beiden (oder mehr) Programme anzugeben.
Schirmeier spricht von den beiden "bound registers", Tanenbaum spricht von "base register" und "limit register". Beide meinen das Gleiche.
Um nun vom Prozess A auf den Prozess B umzuschalten, mussten die betroffenen Register (stack pointer, instruction pointer, bounds register) irgendwo auf der Festplatte gespeichert werden. Dann konnten die Registerinhalte des Prozesses B von der Festplatte geladen werden. Und so konnte das Programm B abgearbeitet werden. Irgendwann wollte man wieder auf Programm A zugreifen und man musste die Register wieder sichern und die anderen wieder laden.
Das Limit-Register als Speicherschutz
Hier sehen Sie einen kurzen Überblick über das Limit-Register:
Die MMU für einen virtuellen Speicher
Die MMU, früher ein eigener Chip auf dem Motherboard, heute ein Teil der CPU, bietet einen verbesserten Speicherschutz und man kann auf Base- und Limit-Register verzichten.
Der physikalische Speicher ist nun überhaupt nicht mehr sichtbar für die einzelnen Prozesse, sondern wir arbeiten nur noch mit virtuellem Speicher. Dazu benötigen wir die sog. "Memory management unit, also die MMU":
Beim Konzept des virtuellen Speichers kommt häufig die Technik des Pagings zum Einsatz. Hier wird der physische Speicher in gleich grosse Seiten aufgeteilt. Prozesse greifen nicht mehr auf physische Adressen zu, sondern auf Seitenadressen. Im folgenden Video sehen Sie, wie man das umrechnet:
Virtueller Speicher geht noch weiter. Es spielt schlussendlich keine Rolle, wenn ein Prozess im RAM 16GB benötigt, wir aber nur einen Arbeitsspeicher von 4GB haben. Teile des gerade nicht benötigten Speichers kann ausgelagert werden, z.B. auf die Festplatte. Schauen Sie das folgende Video zur Klärung:
David Black-Schaffer bietet zu Virtual Memory eine ganze Playlist: