Letzte Woche waren die virtuellen Adressierungen, die virtuelle
Speicherwaltung relativ vorangeschritten.
Zwei, drei Folien kurz zum Wiederholen, dann machen wir da weiter, wo
wir aufgehört hatten.
Kurz wiederholen, weil es geht halt eben noch mit der Speicherwaltung
weiter.
Also wir hatten auf jeden Fall gesehen, dass es quasi zur virtuellen
Speicherwaltung einmal eine virtuelle Adresse gibt, davon redet die
CPU, davon redet der Assembler-Code und dann gibt es eine
physikalische Adresse, das ist die, die wirklich der hardwaremäßige
Speicher, der, der da ist, der deren Sockel, was auch immer, bieten
kann.
Und dazwischen, also die Idee ist, der virtuelle Speicher ist halt
viel größer.
Also der kann einen Adressraum haben, der viel größer ist als was
wirklich physikalisch vorhanden ist.
Das ist so die Hauptidee.
Es gibt weitere, aber die Hauptidee.
Naja, und dann muss man halt eben übersetzen von der virtuellen
Adresse auf die physikalische Adresse.
Dafür gibt es die Übersetzungstabelle.
Dafür würden jetzt die obersten Bits von der virtuellen Adresse
genommen als Input in die Tabelle.
Die Tabelle sagt dann im Normalfall, ja, der Teil ist gerade im
Hauptspeicher und zwar an folgender Startadresse.
Und zu dieser Startadresse, hier unten die physikalische
Seitenadresse, kommt dann noch ein Offset drauf.
Einfach das wievielte Byte in diesem Bereich ist denn gemeint.
Oder die Tabelle kann sagen, nee, ist gerade nicht im Speicher.
Ich hole es kurz, frag mich gleich nochmal.
Gut, und dann hätten wir gesehen, es gibt zwei verschiedene Arten von
diesen virtuellen Adressierungen.
Es gibt Segmente und es gibt Seiten.
Und wir werden am Ende noch sehen, man kann das auch kombinieren.
Das ist sogar normal.
Aber erst mal Segmente.
Segmente, da ist die Idee, dass man logisch, also aus
Programmierersicht, logisch zusammenhängende große Blöcke hat.
Es gibt das Code-Segment, da steht der ganze Code drin, also die
Befehle, die Assembler-Befehle.
Es gibt das Daten-Segment, da stehen die ganzen Daten drin, also die
Initialisierungsdaten, der Heap, also was mit malloc oder mit new
dynamisch alloziiert wird.
Das steht alles im Daten-Segment drin.
Es gibt das Stack-Segment, da stehen die Stack-Einträge drin.
Und weil das manchmal nicht alles reicht, gibt es halt häufig noch
weitere Segmente.
Das Extra-Segment ist so ein Name zum Beispiel.
Ist einfach für irgendwas, wenn man mehr Platz braucht.
Gut.
Ja, und die Segmente sind halt eben so groß, wie halt benötigt.
Also im Code-Segment steht der komplette Code drin.
Also es ist halt ziemlich groß und variabel groß.
Nicht das Gegenteilige, aber ein ganz anderes Verfahren, ist das
Seiten -Verfahren.
Da gibt es Seiten, alle sind gleich groß.
Normalerweise sind alle gleich groß.
Man entscheidet sich auf eine Größe.
Es gibt Systeme, wo es dann auch vielleicht zwei verschiedene Größen
gibt.
Aber da haben wir mal nur eine Größe.
Man entscheidet sich auf eine Größe, die ist es dann halt eben.
Und die ist klein.
Also im Vergleich zu den Segmenten ist die klein.
Wir reden hier so von der Kilobyte-Gegend.
Vier Kilobyte ist eine typische Zahl.
Diese 52 und 56 Byte, die hier stehen, man kann auch wirklich kleine
Seiten nehmen, wenn man möchte.
Aber normal ist es heute nicht mehr.
Die meisten sind vier Kilobyte.
Und dann gibt es halt eben hier bei ganz wenigen Ausnahmen, Spark habe
ich eine gefunden, da gibt es halt eben auch acht Kilobyte-Seiten als
Standard, was supported wird.
Und dann gibt es auch zusätzlich andere Seitengrößen.
Also es gibt Systeme, die unterstützen verschiedene große
Seitenformate.
Und da heißen die Dinge für gewöhnlich große Seiten oder große Seiten.
Also huge ist das kleinere der beiden.
Und die sind dann häufig so ein Megabyte bis vier Megabyte.
Das ist je nach System unterschiedlich.
Und large sind dann die nochmal größeren.
Und das können dann wirklich Seiten sein, die im Gigabyte-Bereich
sind.
Das ist dann nicht mehr normal.
Das ist dann bei Server-Prozessoren, wo wirklich sehr, sehr, sehr
große physikalische Hauptspeicher da sind.
Da gibt es dann auch häufig sehr, sehr, sehr große Seiten.
Es gibt Systeme, die zwei von diesen Verfahren unterstützen.
Drei wüsste ich jetzt nicht.
Könnte es theoretisch geben.
Viele nur eins, manche zwei.
Also die meisten unterstützen normale Seiten und vielleicht noch huge
Pages.
Ist eine Frage, was das Betriebssystem unterstützt und was ja das
unterstützt.
Beide müssen es können.
Gut.
Ja, und wie gesagt, die sind alle gleich groß.
Also abgesehen von diesen verschiedenen Größen.
Aber sagen wir mal, alle vier Kilobyte große Seiten.
Somit ist dann halt eben die eine Anwendung nicht mehr logisch
unterteilt.
Das Code-Segment ist ein Block, sondern es ist einfach nach vier
Kilobyte Schnipseln aufgestückelt.
Und es hat verschiedene Vor- und Nachteile, die sehen wir in zwei
Folien.
Das ist Wiederholung kurz noch.
Zumindest Segmente und Seiten.
Und da gibt es verschiedene Strategien, verschiedene Probleme,
verschiedene Sachen, die man entscheiden muss.
Die gucken wir nicht nochmal an, aber einfach nochmal als Erinnerung,
welche es überhaupt gab.
Der Einlagerungszeitpunkt, wann werden die Segmente oder Seiten
eingelagert?
Das normale ist on demand.
Wenn irgendjemand danach fragt, dann lagert man es ein.
Bei Segmenten ist es zum Beispiel so, das Code-Segment, das brauche
ich ja, um das Programm auszuführen.
Das Code-Segment wird quasi immer, wenn die Anwendung startet, dann
wird es einmal eingelagert, dann ist es drin.
Es kann nicht verdrängt werden, weil das Programm braucht es ja.
Und die Segmente sind halt ganz oder gar nicht da.
Also das Code-Segment, wenn es einmal eingelagert ist, das bleibt
komplett drin.
Bei Seiten ist es anders.
Da kann man ja nach vier Kilobyte Täppchen, wie man will, ein- und
auslagern.
Und da ist halt eben schon ein Unterschied.
Da werden auch dauernd zwischendurch einzelne Seiten eingelagert,
entsprechend auch wieder ausgelagert, wenn der Platz ausgeht, also der
Platz im physikalischen Speicher.
Das On-Demand ist das Normale, also wenn eine Seite angefordert wird,
dann lädt man sie ein.
Und dann gibt es aber auch Systeme, die sind ein bisschen cleverer,
meistens softwareimplementiert, die gucken sich an, jetzt hat er die
Seite gebraucht, jetzt hat er die nächste gebraucht, also eher die
Anwendung, die nächste gebraucht.
Oh, der scheint linear über ein Array zu lesen.
Ich hole mal die nächsten drei Seiten auch schon rein, der braucht sie
wahrscheinlich auch.
Also die werden dann reingeholt spekulativ, obwohl sie noch nicht
angefordert worden sind.
Also das heißt Prefetching, also vorab, was in den Speicherladen, was
voraussichtlich demnächst gebraucht wird.
Dann kann man da, wenn es dann gebraucht wird, ist es schon da, man
hat Zeit gewonnen.
Zuweisungsproblem ist die Frage, wohin in den physikalischen
Hauptspeicher soll eine Seite, soll ein Segment platziert werden.
Bei den Seiten ist das relativ egal, weil die Seiten sind alle gleich
groß.
Bei den Segmenten ist es ein großer Unterschied, weil die Segmente
sind variabel groß und dann kann es halt eben Lücken zwischen den
Segmenten geben, die man nicht mehr sinnvoll füllen kann.
Weil die Seiten alle gleich groß sind, kann es da keine Lücken
dazwischen geben.
Es kann Lücken geben, aber in die Lücke passt immer mindestens eine
Seite rein, weil alle Seiten gleich groß sind.
Ersetzungsproblem, irgendwann ist der physikalische Hauptspeicher voll
oder zumindest es passt die neue Seite, Segment, die gewünscht wurde,
nicht mehr rein.
Dann muss man anfangen, Sachen zu verdrängen und das ist einfach die
Frage, welches soll verdrängt werden.
Also da gab es verschiedene Strategien zu, die wurden letztes Mal
gesehen.
Ja und das war, wurde mir gesagt, die Folie, mit der aufgehört wurde.
Als Zusammenfassung einfach nochmal Segmente gegen Seiten.
Segmente, variable Größe, Code-Segment ist so groß, Segment ist viel
kleiner.
Logische Abbildung, also das Code am Stück, geringer Datentransfer.
Hiermit ist einmal gemeint, der Datentransfer passiert selten.
Beim Code-Segment passiert er einmal, nämlich wenn die Anwendung
gestartet wird, dann muss das Code-Segment da sein, danach ist es
immer dort, also muss nicht mehr neu, also einmal nur, also selten.
Und es ist auch vom Datenvolumen, was übertragen werden muss, weniger.
Weil, wenn ich das Code-Segment halt, naja es ist häufig weniger, mein
Gott, das ist immer so, pauschale Aussagen sind immer schwierig, es
ist häufig weniger.
Wenn ich das Code-Segment einmal reingeladen habe und ich weiß, ich
kann es sinnvollerweise niemals verdrängen und niemals wieder, also
dann brauch ich es auch niemals wieder reinladen, dann habe ich es
halt eben nur einmal angefasst.
Bei Seiten kann es sehr gut passieren, dass ich ein und dieselbe Seite
während der Anwendungslaufzeit mehrmals aus- und wieder einlagere,
also habe ich es mehrmals angefasst, also habe ich eventuell auch mehr
Daten übertragen.
Warum habe ich nicht immer mehr Daten übertragen?
Also ich musste meine Pauschalisierung ja gerade zurücknehmen.
Könnte ja sein, dass im Code-Segment eine Menge Code drin stand, der
gar nicht benutzt wurde, der gar nicht ausgeführt worden ist.
Eine riesige Bibliothek, die ist da, aber abhängig von Eingabedaten,
wird sie gebraucht oder nicht.
Wenn sie nicht gebraucht wird, hätten wir sie nicht reinladen müssen.
Aber bei Segmenten wird das ganze Code-Segment reingeladen.
Bei Seiten wird dann vielleicht nur der Teil reingeladen, der
gebraucht wird.
Also da könnte es auch sein, dass die Seitenverwaltung weniger Volumen
transferiert.
Externe Fragmentierung ist eine Eigenschaft von der Segmentierung.
Es gibt externe und unten sehen wir bei den Seiten auch die interne
Fragmentierung.
Externe Fragmentierung heißt immer, ich habe irgendein Gesamtding, von
dem ich rede, hier der physikalische Hauptspeicher, und in dieses
Gesamtding mache ich kleinere Einheiten rein.
Und wenn ich Lücken zwischen den Einheiten haben kann, zwischen den
Segmenten haben kann, die ich nicht nutzen kann, weil das neue
gewünschte Segment nicht dazwischen passt, muss ich gleich noch
korrigieren, aber dann habe ich externe Segmentierung.
Korrigieren muss ich es im Sinne von, wenn ich eine Lücke habe, wo es
nicht reinpasst, aber die Summe aller Lücken, die da sind, das hätte
schon gereicht.
Also kapazitätsmäßig ist genug da, aber keine der Lücken ist groß
genug, damit das Segment reinpasst.
Dann habe ich externe Fragmentierung.
Wenn die Gesamtsumme des freien Speichers nicht reicht, das ist keine
Segmentierung, das passt halt einfach nicht.
Interne Fragmentierung ist anders.
Interne Fragmentierung ist, ich habe diese gleich großen Seiten und
ich benutze sie vielleicht nicht voll.
Wenn ich eine 4 Kilobyte Seite habe, von der ich nur 10 Byte benutze,
dann ist der Rest verschwendet.
Keine andere Seite kann das benutzen.
Das ist also ein Verschnitt innerhalb von der Seite.
Das ist interne Fragmentierung.
Bei dem Segmentieren kann ich keine interne Fragmentierung haben, weil
die Segmente sind variabel groß.
Die mache ich genau so groß, wie ich sie haben will.
Beim Code-Segment weiß ich statisch vorab.
Beim Daten-Segment kann ich es vergrößern, wenn ich es brauche.
Das Text-Segment genauso.
Kann ich vergrößern, wenn ich es brauche.
Gut, also externe Fragmentierung, interne Fragmentierung.
Dann haben wir noch die Seitenverfahren.
Feste Größe, wie gesagt.
Geringerer Verwaltungsaufwand.
Hauptspeicherauslastung.
Das ist genau die Folge von dem Vermeiden der externen Fragmentierung.
Weil ich halt keine externe Fragmentierung habe, kann ich den
Hauptspeicher...
Ich habe mehr sinnvolle Daten da, weil ich halt eben keine Lücken
zwischen den Segmenten haben kann.
Ich habe keine Segmente, ich habe nur Seiten.
Und der Verschnitt innerhalb einer Seite ist überschaubar.
Wenn ich eine Seite habe, die ein Gigabyte groß ist, okay, dann wird
mein Verschnitt ziemlich gewaltig sein.
Also meistens zumindest.
Bei vier Kilobyte-Seiten kann ich halt pro Seite maximal vier Kilobyte
verschwenden.
Also vier Kilobyte minus ein Byte.
Das ist okay, das ist weniger.
Und deswegen habe ich halt eine bessere Auslastung.
Häufiger Datentransfer.
Also wirklich, wie häufig passiert der?
Weil halt eben dauernd zwischendurch ausgelagert werden kann.
Und halt eben auch möglicherweise das Volumen.
Also ich bin da eigentlich bei den Seiten flexibler, weil ich viel
kleinere Schnipsel habe.
Und ich bin flexibler, obwohl ich eine feste Größe habe.
Genauer sogar, weil ich eine feste Größe habe.
Ist ein bisschen verwirrend am Anfang.
Ich habe eine variable Größe, aber die lässt mir eigentlich weniger
Möglichkeiten.
Die feste Größe bei den Seiten erlaubt da fast eine größere
Flexibilität, weil ich mehr Seiten habe, mit denen ich jonglieren
kann, die ich irgendwo platzieren kann im Speicher.
Gut.
Ja, machen wir weiter.
Also Seitensegmente, klar.
Wir hatten irgendwie mal angefangen, mit der Speicherhierarchie zu
gucken.
Wir hatten gesehen, da ist eine CPU, da sind Caches, da ist
Hauptspeicher, da ist Hintergrundspeicher.
Und jetzt hatten wir halt eben noch die Seitenverwaltung dazu gebaut.
Vielleicht hier nochmal, das hatten wir schon gesehen, das Bild in der
früheren Folie irgendwann mal.
Die CPU macht halt eben Einzelzugriffe, ein Byte oder maximal den
Datenbus, 32-Bit, 44-Bit.
Greift sie auf den L1-Cache zu, dann kannst du eine Hierarchie von
Cachen geben.
Zum Beispiel kannst du einen zweiten Cache geben.
Da wird dann immer auf ganzen Blöcken, also Cache-Zeilen, große Blöcke
vom L2-Cache angefordert.
Und der macht wieder blockweise Zugriff vom Hauptspeicher.
Und ja, wenn wir jetzt virtuelle Speicherverwaltung haben, dann haben
wir auch einen Hintergrundspeicher, Festplatte, was auch immer,
Bänder.
Und da werden dann meist noch größere Blöcke vom Hintergrundspeicher
in den Hauptspeicher eingeladen.
Also Seitensegmente, das wäre genau was vom Hintergrundspeicher in den
Hauptspeicher kommt.
Jetzt haben wir am Anfang gesehen, nochmal in der Wiederholung, wir
müssen irgendwo die Übersetzungstabelle haben.
Wir müssen irgendwo die Tabelle haben, die übersetzt von der
virtuellen Adresse in die physikalische Adresse.
Und die Frage hier ist eigentlich, wo?
Wo machen wir diese Übersetzung?
Und an diesem Bild kann man sehen, es gibt da mehrere potenzielle
Möglichkeiten, wo man das machen könnte.
Also Inhalt der CPU ist Unsinn.
Die CPU arbeitet auf ihren Assemblerbefehlen, die arbeitet mit dem,
was der Programmierer gemacht hat.
Dem war es egal, ob er jetzt direkt mit physikalischen Adressen redet
oder virtuellen Adressen.
Das geht dem Assemblercode nichts an.
Der hat irgendwelche Adressen.
Also Inhalt der CPU macht nicht so viel Sinn.
Aber zwischen CPU und L1 Cache könnten wir es machen.
Zwischen L1 und L2 Cache könnten wir es machen.
Zwischen L2 und Hauptspeicher wäre der spätestmögliche Moment.
Weil der Hauptspeicher, da brauchen wir die physikalischen Adressen.
Also da muss spätestens die Besetzung gemacht werden.
Hier hinten macht keinen Sinn, weil ich halt eben schon hier vorne die
physikalischen Adressen benötige.
Und es gibt jetzt keine goldene Antwort, es ist da, Punkt, sondern es
gibt verschiedene Antworten.
Man kann es wirklich an all diesen Orten machen und man kann es auch
so ein bisschen verteilt machen.
Gucken wir uns die Varianten mal an.
Die erste Variante nennt sich virtueller Cache.
Natürlich Blödsinn.
Das ist kein virtueller Cache.
Das ist ein echter, wirklich vorhandener Cache.
Aber er cached die virtuellen Adressen.
Das ist damit gemeint.
Also wir hatten hier ja vorne zwischen CPU und L1 Cache.
Also aus der CPU kommen die virtuellen Adressen raus.
Und wenn ich die benutze im Cache, um sie als Text zu speichern.
Der Cache hat ja diesen Text und die Daten und so weiter.
Also wenn ich die virtuellen Adressen benutze, dann nenne ich es, also
nennt irgendjemand anders es, einen virtuellen Cache.
Und sinnvoller ist es, ein realer Cache, der virtuelle Adressen
cached.
Das heißt, das Wort hatten wir jetzt noch nicht wiederholt, das hat
man aber letztes Mal gesehen.
MMU ist dieses Ding, was die Übersetzung macht.
Das ist die Memory Management Unit, die macht die Übersetzung von
virtuell auf logisch.
Und hier könnten wir den Cache haben zwischen der CPU und der MMU.
Also sowas wie CPU, L1 Cache, MMU und danach was auch immer.
Hauptspeicher, vielleicht noch ein Cache, keine Ahnung.
Und da werden dann die höchsten Bits von der logischen Adresse, das
ist in diesem Zusammenhang dasselbe wie die virtuelle Adresse.
Ich hätte es, glaube ich, schon mal angedeutet in einer anderen
Vorlesung.
Man unterscheidet da häufig, wenn ich einen externen Speicher habe,
extern im Sinne von hier, einen Hintergrundspeicher.
Wenn ich so einen Hintergrundspeicher habe und ich wirklich auf den
Hintergrundspeicher auslagern kann, dann nennt die Betriebssystemler
das Ganze eine virtuelle Adresse.
Und wenn es den nicht gibt, also wenn es keinen Hintergrundspeicher
gibt, kann ich aber trotzdem noch einen Unterschied haben zwischen der
Adresse, von der die CPU redet und die Adresse, die im physikalischen
DRAM ist und dann wird es logische Adresse genannt.
Der Unterschied ist für Betriebssystembauer sehr wichtig, für uns ist
er gerade relativ egal, deswegen in dem Moment hier logische Adresse,
virtuelle Adresse, ist unserer Sicht hier erstmal gleichwertig.
Also die höchstwertigsten Bits von der logischen virtuellen Adresse
werden dann als Text genutzt, weil halt eben der Cache gerade
dazwischen ist.
Bildlich hier, wir haben die CPU, die kommt mit ihrer logischen oder
virtuellen Adresse.
Und fragt den Cache.
Und der Cache kann dann sagen, ich hab's, ich hab's nicht, wie auch
immer.
Wenn er es nicht hat, also wenn er es hat, erledigt, geschenkt.
Wenn er es nicht hat, wenn er den Antrag nicht hat, also ein Miss,
dann muss danach die MMU befragt werden.
Die MMU muss eine Übersetzung machen, von wo im Speicher muss ich
eigentlich nachgucken, also Übersetzung von logischer auf
physikalische Adresse und danach kann ich dann den Hauptspeicher
fragen und dann geht's wieder zurück, kann im Cache eingelagert
werden, kann an die CPU zurückgeliefert werden.
Was man sich vielleicht auch nochmal hier klar machen sollte, MMU ist
prinzipiell ziemlich langsam.
MMU hat eine Übersetzungstabelle.
Diese Tabelle wird benötigt, um von virtuell nach physikalisch, das
ist klar.
Die Tabelle ist groß.
Also je nachdem, wie groß mein Adressraum ist, kann die Tabelle ganz
schön groß sein.
Die Tabelle selbst liegt im Hauptspeicher.
Das heißt, damit die MMU eine Übersetzung machen kann, muss sie selbst
erstmal den Hauptspeicher befragen.
Die MMU ist quasi nur ein Stück Hardware, das weiß, wo im
Hauptspeicher liegt denn die Tabelle.
Und wenn dann die CPU mit einer virtuellen Adresse ankommt und fragt,
dann weiß die MMU, wo im Hauptspeicher ist die entsprechende Tabelle,
fragt den Hauptspeicher, gibt mal die Übersetzung und dann weiß die
MMU wieder, ah, dann muss ich diese physikalische Adresse zugreifen.
Und dann fragt dann nochmal den Hauptspeicher, wo die echten Daten
sind.
Also es sind mehrere Zugriffe, in dem Fall, dass ich jetzt gesagt
habe, es war ein zusätzlicher Zugriff, es könnten mehrere sein.
Wir sehen ganz am Ende noch ein etwas komplexeres Beispiel.
Das heißt, MMU ist nicht schnell.
Das heißt, der Vorteil von dieser Variante hier ist, ich kann den
Cache befragen, ohne die MMU zu bemühen.
Und wenn ich Glück habe und ein Cache ein Hit ist, dann brauche ich
die MMU auch gar nicht erst nerven.
Dann habe ich halt eben viel schneller meine Antwort da.
Das andere Verfahren hat gerade diesen Vorteil nicht.
Wir gucken uns das erstmal an, dann überlegen wir uns, wofür das
überhaupt gut sein soll.
Es ist für was gut.
Da ist es halt einfach auf der anderen Seite.
Ich habe erst die CPU, also erst die CPU, dann habe ich erst die MMU
und danach den Cache.
Heißt somit also physikalischer Cache.
Also wirklich real vorhandener Cache, der die physikalischen Adressen
taggt.
Das heißt, hier muss immer erst die MMU gefragt werden.
Die MMU muss immer erst den Hauptspeicher fragen.
Gib mir mal die Tabelle an Eintrag so und so, damit die MMU danach die
Übersetzung machen kann.
Und dann weiß die MMU die physikalische Adresse und mit der
physikalischen Adresse kann sie danach den Cache fragen.
Also selbst im Fall eines Hits, ein Cache-Hit, habe ich trotzdem einen
Speicherzugriff gemacht.
Das ist suboptimal.
Wofür könnte es trotzdem hilfreich sein?
Naja, da muss man jetzt ein bisschen ausholen.
Wenn wir uns jetzt überlegen, wir hätten Systeme, wo nur eine
Anwendung läuft, dann ist dieses Verfahren hier relativ wenig
vorteilhaft.
Wenn wir uns überlegen, wir hätten ein System, wo mehrere Anwendungen
laufen, dann müssen wir uns Gedanken machen.
Wenn wir mehrere Anwendungen laufen, ich gehe nochmal zurück zu dem
vorigen Bild von dem virtuellen Cache, wenn wir mehrere Anwendungen
laufen haben, die laufen ja nicht gleichzeitig, die laufen
nacheinander, dann läuft erst ein Stück von einer Anwendung, dann
wechsle ich auf Anwendung B, dann wieder zurück auf A, dann wieder
zurück nach B, hin und her.
Bei so einem Wechsel von Anwendung A auf Anwendung B, passiert so alle
ein bis zehn Millisekunden.
Also aus Sicht von einem Menschen relativ zügig.
Aus Sicht von einer CPU ist das ganz schön lange.
Bei so einem Wechsel muss ich den Kontext retten.
Kontext retten heißt auf jeden Fall mal, das was Anwendung A gerade im
Registerpfeil stehen hat, das muss ich retten, weil ich es später
wieder herstellen muss, damit Anwendung A da weitermachen kann, wo sie
aufgehört hat.
Zum Kontext gehört aber noch viel mehr.
Und zwar, der Cache hat ja jetzt Sachen gecached, die Anwendung A
gehört haben.
Insbesondere hat er Sachen gecached, wo Anwendung A gesagt hat,
virtuelle Adresse so und so soll abgebildet werden, auf physikalische
Adresse so und so.
Anwendung B könnte jetzt die gleiche virtuelle Adresse benutzen.
Anwendung B hat einen völlig eigenen Adressraum, kann die gleiche
virtuelle Adresse benutzen, meint damit aber eine völlig andere
physikalische Adresse.
Also die Übersetzungstabelle ist anwendungsspezifisch.
Die eine Anwendung hat ihre eigene Tabelle, deswegen kann man die MMU
auch umschalten.
Die MMU kann quasi gesagt werden, benutzt mal die Tabelle, benutzt mal
die Tabelle, weil eben eine andere Anwendung dran ist.
Das heißt, in dem Cache stehen aus Sicht von Anwendung B falsche Daten
drin.
Anwendung A hat da Sachen in den Cache eingefügt mit der virtuellen
Adresse von Anwendung A.
Wenn jetzt Anwendung B dran kommt, dann muss ich den Cache leeren.
Die Sachen, die im Cache drinstehen mit der virtuellen Adresse, sind
nicht mehr die richtigen.
Das heißt, erstmal alles, was dirty ist, also im Sinne von, es ist im
Cache modifiziert, aber noch nicht in den Hautsprecher
zurückgeschrieben worden, muss erstmal in den Hautsprecher geschrieben
werden beim Kontextwechsel.
Bevor ich zu Anwendung B wechseln kann, muss ich alles in den
Hautsprecher zurückschreiben.
Alles, was sonst noch im Cache drinsteht, muss invalidiert werden,
weil es hatte die Übersetzungstabelle von Anwendung A benutzt.
Das heißt, wenn Anwendung B dran kommt, ist der Cache leer.
Ist komplett nichts drin.
Aus Konzeptgründen muss ich den Cache flaschen, also leer machen.
Das brauche ich hier nicht.
Hier werden die physikalischen Adressen gecached.
Das heißt, der MMU-Eintrag, der ändert sich.
Also beim Kontextwechsel von Anwendung A auf Anwendung B wird der MMU
gesagt, benutze jetzt nicht mehr diese Seitentabelle, benutze jetzt
eine andere Seitentabelle, nämlich die von Anwendung B.
Aber die physikalischen Adressen, die gecached sind, bleiben die
gleichen.
Das heißt auf jeden Fall, der Cache muss nicht geflascht werden, also
ich spare mir Zeit, weil ich nicht flaschen muss.
Ich muss nicht dirty Cache-Lines in den Speicher zurückschreiben, die
können drinbleiben.
Und vielleicht habe ich sogar Glück, dass Anwendung B zum gewissen
Grad die gleichen Sachen fragt wie Anwendung A.
Wann könnte das denn passieren?
Also es sind verschiedene Anwendungen, die haben ihre verschiedenen
Datenstrukturen, verschiedene alles Mögliche.
Aber quasi jede Anwendung benutzt eine Standardbibliothek.
Print F oder C Out oder System Out, Print Line oder was auch immer.
Also irgendeine Standardbibliothek, die typische Aufgaben macht.
Naja, und die meisten Betriebssysteme laden die nur einmal.
Ja, also das ist quasi so, wenn ich einen C-Code schreibe, Java,
Basic, whatever.
C-Code schreibe, dann habe ich einen Compiler, der macht aus dem C
-Datei eine Assembler-Datei.
Dann habe ich einen Assembler, der macht aus der Assembler-Datei eine
Maschine, eine Binary-Datei, aber nur für die eine C-Datei.
Meistens habe ich ja mehrere C-Dateien.
Und dann habe ich einen Linker und der Linker bindet die ganzen
zusammen.
Und der Linker bindet außerdem noch die Bibliotheken da rein.
Da gibt es zwei verschiedene Varianten.
Es gibt dynamisches Linken und statisches Linken.
Statisches Linken heißt, alle Bibliotheken, die benutzt werden, werden
in ein großes, wirklich ziemlich großes Binary zusammengebunden.
Dynamisches Linken heißt, wenn die Anwendung eine Bibliothek benutzt,
dann wird dann nur vermerkt, die Bibliothek wird gebraucht.
Aber sie ist im Code überhaupt nicht da.
In der Binary von der Anwendung ist die Bibliothek nicht da.
Sondern die wird nur einmal vom Betriebssystem in den Speicher
geladen.
Und alle Anwendungen benutzen die gleiche Bibliothek.
Das hat den Vorteil, quasi alle Anwendungen brauchen die Bibliothek.
Also die Standard Library bei C wäre so ein Beispiel.
Alle Anwendungen brauchen die.
Warum sollen alle Anwendungen ihre eigene Bibliothek mitbringen?
Also die habe ich dann nur einmal im Speicher.
Der Linker hat nur gesagt, die wird gebraucht.
Und das Betriebssystem am Starten der Anwendung muss gucken, wo ist
denn die Bibliothek gerade und das der Anwendung mitteilen.
Und das war jetzt die Vorrede für, wann könnte es Sinn machen, wann
könnten zwei verschiedene Anwendungen die gleiche physikalische
Adresse brauchen.
Naja, wenn sie auf die Bibliothek zugreifen und die Bibliothek
dynamisch gelinkt war.
Wenn Printf, um bei C zu bleiben, nur einmal im Speicher steht, machen
bestimmt beide Anwendungen irgendwann mal Printf.
Oder irgendwas anderes von der Bibliothek.
Und das wäre dann, also jetzt am Beispiel von einem Code, von
Befehlen, vom Code-Cache, wäre das ein Hit in dem Cache, weil Printf
von der Anwendung A vielleicht noch drin ist.
Anwendung B braucht es auch.
Super, Cache-Hit.
Das ist der Grund, wo dieser physikalische Cache Vorteile hat.
Ja, nochmal die normalen Beispiele, also normale Unterschiede vor
Nachteile.
Virtueller Cache, ich brauche die MMU nicht anfragen.
Am Bild nochmal hier.
Ich habe erst den Cache, danach die MMU.
Also bei Cache-Hit brauche ich die MMU nicht fragen, klarer Vorteil.
Weitere, also physikalischer Cache, anderer Vorteil.
Die Adresse ist kleiner.
Bei einer virtuellen Adresse habe ich häufig nur 48-Bit, 44-Bit
virtuelle Adresse, die muss ja im Tag gespeichert werden.
Der Tag ist entsprechend mehr Bits groß.
Wenn ich einen physikalischen Cache habe, ist die Adresse, die
physikalische Adresse ist für gewöhnlich kleiner.
Muss nicht, kann auch gleich groß sein, könnte kleiner sein, gibt es
aber glaube ich nicht.
Aber normalerweise ist sie größer, also die physikalische ist kleiner.
Das heißt, ich muss weniger Bits als Tag speichern als beim
virtuellen.
Also das ist ein Vorteil, der Tag Storage, hieß es glaube ich, ist
kleiner.
Dann stand hier noch ein weiterer Vorteil, aber der ist ancient.
Da steht, befindet sich die MMU auf dem Prozessor-Chip?
So kann man, wenn man will, nur den physikalischen Cache außerhalb des
Chips erweitern.
Das ist sicherlich irgendwann mal interessant gewesen, als es wirklich
einen Chip gab.
Da war die CBU und ein Chip, das war der Cache und ein Chip, das war
die MMU oder irgendwie sowas.
Für solche Systeme mag so ein Vorteil potenziell sinnvoll sein.
Aber alle heutigen Systeme, sowohl CBU, wie auch MMU, wie auch Cache,
das ist alles ein Chip.
Dieser Vorteil ist bei aktuellen Systemen einfach nicht mehr existent.
Konzeptionell okay, gerne in der Praxis heute nicht mehr beobachtbar.
Gucken wir uns mal ein Beispiel an.
Also ein Speicher für segmentorientierte Speichererwaltung.
Also wir hatten schon gesehen, Segmente, große logisch
zusammenhängende Bereiche, Code-Segment, Datensegment.
Maximaler Segment länger, also die Längen sind limitiert.
Wir werden gleich sehen, warum.
Also es gibt eine Obergrenze der Längen.
Und manche haben ja wirklich bis zu 4 Gigabyte.
Also das reicht für viele Sachen, aber nicht für alle.
Server hören irgendwann auf, lustig zu sein.
Aber meistens reichen ja 4 Gigabyte.
Und am x86 gucken wir uns das einfach mal an einem Beispiel an.
Wir haben eine Adresse, kommt oben rein, also die virtuelle, logische
Adresse.
In dem Fall offenbar eine 48-Bit-Adresse.
Und da gibt es jetzt ein Ding vorne weg, das heißt Selektor.
Und dann gibt es noch einen 32-Bit großen Offset dahinter.
Ja und dann gibt es jetzt mehrere Zugriffe auf den Hauptspeicher, das
ist hier das Ding, um letztendlich einen Speicherzugriff zu machen.
Diese 16 Bit vom Selektor, der werden erstmal benutzt, um hier in der
Segment -Deskriptortabelle nachzuschauen.
Jetzt kommen eine Menge hässliche Wörter.
Die Segment-Deskriptortabelle ist also irgendein Ding, das enthält
Informationen, wo sind denn die Segmente.
Das Ding selbst hat eine Basisadresse, die ist hier nicht
eingezeichnet.
Es wird gewöhnlich in irgendeinem Register gespeichert, der sagt, wo
im Speicher fängt diese Tabelle an.
Und die 16 Bit, die hier reingepurzelt kommen, werden als Offset
benutzt.
Ja unten ist 0, werden als Offset benutzt.
Wie viel weit weg von der Basisadresse ist der Eintrag, der mich jetzt
gerade interessiert.
Und an dem Eintrag steht halt irgendwas.
Da steht unter anderem die Segment-Basisadresse.
Das heißt, ich habe jetzt in dieser Segment-Deskriptortabelle einen
Eintrag rausgefischt und der beschreibt ein Segment.
Und unter anderem sagt der auch, wo im Hauptspeicher fängt dieses
Segment eigentlich an.
Also quasi wie Pointer, der zeigt auf den Anfang von diesem Segment,
die Basisadresse von dem Segment.
Und dann werden halt einfach nur noch der 32-Bit-Offset, der da oben
steht, drauf addiert.
Und dann kommt letztendlich eine physikalische Adresse raus.
Und bei x86 ist alles ein bisschen anders, deswegen heißt es hier
jetzt erstmal lineare Adresse.
Das ist meistens was anderes als die physikalische Adresse.
Warum kann man hier noch nicht so richtig gut sehen, das sehen wir auf
einer späteren Folie.
Aber für den Moment kann man sich vorstellen, das wäre eine
physikalische Adresse.
Warum es eine lineare ist, sehen wir später.
Und damit habe ich jetzt quasi innerhalb von meinem Segment, also ich
habe hier Segment-Deskriptor, Basisadresse für das Segment, da habe
ich mein Segment gefunden.
Und innerhalb von dem Segment habe ich jetzt durch Addieren des
Offsets die physikalische Adresse gefunden, wo das Wort, was ich haben
wollte, tatsächlich liegt.
Also es geht ein bisschen hin und her.
Ja, so würde die Adressierung zum Beispiel bei dem Teil von dem x86
aussehen.
Was steht alles drin in diesem Segment-Deskriptor?
Segment-Deskriptor war das Ding, was wir hier haben.
Das ist die Segment-Deskriptor-Tabelle und das ist der Segment
-Deskriptor.
Da steht halt eben, wie hier angedeutet, schon drin die Segment
-Basisadresse.
Haben wir hier auch, Segment-Basisadresse.
Da steht noch mehr drin.
Da steht auch noch drin, wie groß ist das Segment.
Und das ist letztendlich der Grund, warum die Segmente eine limitierte
Größe haben.
Ich habe in diesem Deskriptor eine gewisse Menge an Bits vorgesehen,
um die Größe zu kodieren.
Und ja, damit ist doch klar, es gibt eine maximale Größe von diesen
Segmenten.
Warum brauche ich diese Angabe der Größe?
Das ist im Wesentlichen... also ich brauche es nicht.
Geht auch ohne, aber es erzeugt Probleme, wenn ich es nicht habe.
Und zwar, angenommen, ich habe irgendeinen Code, der ist erstaunlich
schlecht programmiert und der würde dazu führen, dass ich über mein
eigenes Segment hinauslaufe.
Beispiel, ich habe einen Stack.
Der Stack hat eine gewisse Größe.
Dann habe ich vielleicht irgendein rekursives Programm, was unter
gewissen Bedingungen nicht ordentlich oder zu spät terminiert.
Dann wird der Stack immer größer, größer, größer, größer.
Irgendwann würde er aus dem Stack-Segment rauslaufen.
Würde also der Stack mehr Speicher benötigen, als im Stack-Segment
vorgesehen war.
Das kann man machen.
Das wird aber ärgerlich für alles, was nach dem Stack-Segment im
Speicher lag.
Das wird einfach gnadenlos überschrieben.
Und das wird irgendwann sehr unangenehme Probleme nach sich ziehen.
Und deswegen gibt es hier eine Größenangabe, die sagt, das Stack
-Segment ist nur so und so groß.
Und jetzt kann man darauf reagieren.
Wenn jetzt quasi ein Zugriff gemacht wird, der über das Stack-Segment,
also egal welches, aber jetzt als Beispiel das Stack-Segment
hinausgeht, würde das erkannt werden von der Übersetzung.
Das würde quasi von dieser Addition hier, das Ergebnis davon würde
geprüft werden, war das jetzt innerhalb von dem Limit oder nicht.
Oder einfach nur das Offset.
War das Offset innerhalb der hier als Limit angegebenen Grenze oder
nicht.
Und wenn es darüber hinausgeht, kann man das Ganze abbrechen.
Kann man sagen, die Anwendung hat was versucht, was sie nicht hätte
machen sollen, nach ihrer eigenen Definition von größerem Stack
-Segment.
Um die anderen Anwendungen zu schützen, verbieten wir das.
Wir, das Betriebssystem.
Und beenden die einfach.
Also, die Größe ist da drin.
Und dann gibt es noch weitere Sachen bezüglich Zugriffsrechte.
Da kommen später noch ein paar Beispiele zu.
Aber hier kann man vielleicht schon mal sagen, Zugriffsrechte können
zum Beispiel so was sein wie, ist das Segment lesbar, ist es
schreibbar, ist es ausführbar.
Das sind die typischen Sachen.
Lesbar ist vielleicht nicht so typisch.
Aber schreibbar ist eine typische Sache.
Also das Code-Segment ist für gewöhnlich nicht schreibbar.
Also lesen muss ich es quasi immer eigentlich.
F0 muss ich nicht lesen.
Darf ich aber auch.
Egal, also das Code-Segment ist meistens nur lesbar.
Das darf ich nicht modifizieren.
Aber es ist ausführbar.
Ich darf das benutzen, um in die Instruction-Fetch-Phase Befehle
reinzuholen.
Umgekehrt, das Stack-Segment ist für gewöhnlich lesbar und schreibbar.
Also es ist schreibbar, aber es ist nicht ausführbar.
Es gibt ja diese typischen, völlig anderes Thema, Buffer-Overflow
-Attacken, wo quasi dafür gesorgt wird, durch ein bösartiges Programm,
dass ein Array, was als lokale Variable auf dem Stack abgelegt wird,
überläuft.
Also dass in das Array mehr reingeschrieben wird, als das Array
eigentlich groß war, als Platz reserviert war für das Array.
Und somit schreibt man außerhalb des eigentlichen Stack-Frames Daten.
Und außerhalb des eigentlichen Stack-Frames kommt irgendwann die
Rücksprung -Adresse, wo die Funktion wieder hin zurück will.
Die kann man damit überschreiben und dann kann man irgendwo
hinspringen, wo bösartiger Code ist.
Und den Code kann man, wo kommt der Code her?
Den Code schreibt man sich selber in das Stack-Segment rein, also den
schreibt man auch in dieses Array rein.
Das heißt, man springt dann an eine Adresse, also die Rücksprung
-Adresse wird ersetzt mit einer Adresse auf dem Stack und da habe ich
selbst, ich, der böse Angreifer, meinen Code hingeschrieben.
Würde aber dazu führen, dass ich das Programm dort weiter ausführe, wo
Adressen des Stack-Segmentes sind.
Aber wenn das Stack-Segment als nicht-executable markiert ist, dann
darf ich keinen Code ausführen, der im Stack liegt.
Ist ja auch für normale Funktionsausführungen gar nicht notwendig.
Und deswegen mit diesem no-executable-Bit kann man dafür sorgen, dass
Buffer -Overflow-Attacken in der einfachsten Version vermieden werden
können.
Und dann haben die sich andere Attacken ausgedacht und dann ging das
eine Weile hin und her.
Zumindest dafür sind dieses no-execute- und nicht-überschreibbar-Flex
als Zugriffsrechte zum Beispiel für jedes Segment einzeln angehbar.
Wir hatten noch nicht genug Wörter, wir brauchen noch mehr Wörter.
Wir hatten ein Segment-Deskriptor, lass uns mal vom Segment-Selektor
reden.
Segment -Selektor, das ist ein Auswahl-Dingsy.
Der Selected, der wählt aus.
Könnte man sagen, ist ein Teil von dem Deskriptor.
Aber wir sind hier gerade auf dem Weg zu einem Beispiel von einem sehr
alten System und da gab es nur den Selektor.
Der Selektor wählt quasi aus, welches Segment ist gemeint.
Das ist dasselbe wie die Basisadresse.
Also das ist quasi, wo ist die Basisadresse von dem Segment.
Im Wesentlichen könnte man sagen, das Teil hier, das ist auch der
Selektor.
In Wirklichkeit ist es ein Deskriptor, ein Deskriptor ist eine
Basisadresse und noch mehr.
Der Selektor ist nur die Basisadresse.
Also der Selektor wählt aus, wo fängt das Segment an.
Was ist die Basisadresse von dem Segment.
Dazu brauche ich ein Offset, genau wie vorhin geschenkt.
Und jetzt ist es so, wie viele verschiedene Segmente wird es denn wohl
geben in einem Programm.
Naja, es sind nicht viele.
Ich habe die meisten, hier unten stehen sie auch, schon gesagt.
Es gibt das Code-Segment, das Text-Segment, das Daten-Segment.
Das reicht für die meisten.
Und weil es eben manchmal nicht reicht, weil die Segmente limitiert in
der Größe sind, habe ich noch zu wenig Segmente für beliebigen Nutzen.
Wenn ich merke, mein Stack ist nicht groß genug, mein Daten-Segment
ist nicht groß genug, dann kann ich diese Extrasegmente, also hier
mehrere verschiedene Extrasegmente, nutzen für was auch immer.
Aber selbst wenn ich die benutzen sollte, viel sind es nicht.
Und meistens sind es nur drei.
Code-Segment, Text-Segment, Daten-Segment.
Das heißt, muss ich wirklich jedes Mal den Segment-Deskriptor, der ja
im Hauptspeicher liegt, fragen, wo beginnt mein Code-Segment, wo
beginnt mein Daten-Segment, wo beginnt mein Stack-Segment?
Könnte ich nicht einfach sagen, ich habe drei Register in der CPU, das
eine nenne ich Code-Segment-Register, das andere nenne ich Daten
-Segment -Register und beim dritten könnt ihr raten, wie es heißen
könnte.
Und damit quasi mir merken, was die häufig benutzten Segmente sind,
dann spare ich mir diesen Speicherzugriff, um zu fragen, wo ist mein
Segment.
Und dann ist der Segment-Selector quasi nur noch, welches von diesen
Registern soll benutzt werden.
Naja, wenn die Instruction-Fetch-Stufe was will, dann wird es wohl das
Code -Segment sein.
Und bei der Speicherstufe muss man schon ein bisschen gucken.
Da muss man gucken, ist es eher eine Stack-Operation oder eher eine
Daten -Segment-Hauptspeicher-Operation, welches der beiden Segmente
soll da benutzt werden.
Und die sonstigen Informationen, wenn man noch weitere hat, wie Größe
oder Zugriffsschutz, die kann man dann sogar in so einer Art Cache
ablegen, wo von dem Segment-Deskriptor die zuletzt benötigten
sonstigen Informationen zwischengespeichert werden, sodass man das
alles ohne Hauptspeicherzugriff abarbeiten kann.
Ein Beispiel würde das so aussehen.
Ich habe quasi in der MMU selbst diese Register, die Segment-Register,
wo quasi Teile von der Segment-Deskriptoren-Tabelle, wie hieß das
Monster?
Ja, Segment-Deskriptortabelle, das kommt mir mal nah dran.
Also wo Teile von der Segment-Deskriptortabelle einfach in diesen
Registern abgespeichert sind, nämlich gerade wo der Anfang ist.
Und dann kann die MMU selber wissen, wo die Segmente anfangen, ohne
den Hauptspeicher zu fragen.
Naja, und da werden jetzt ein paar von diesen Segment-Registern, die
zeigen dann irgendwo hin.
Zum Beispiel das Code-Segment hier zeigt dann auf irgendeine Adresse
vom Speicher.
Wichtig ist hier immer, das ist von dem Pfeil hier angedeutet, der
zeigt immer, also wenn der Speicher jetzt nach oben wächst, dann zeigt
er immer auf den unteren Rand, also auf die Basisadresse von diesem
Segment.
Und das Segment wächst dann nach oben, also plus den Offset.
Dann haben wir halt genauso noch einen Datenblock und einen Stack und
noch einen Datenblock und noch einen Datenblock.
Warum nicht?
Und irgendwann kommt ein, also wenn ich jetzt ein Modul interpretieren
möchte als Task, keine Ahnung, was es sonst sein könnte.
Also sagen wir mal Task A und Task B.
Wenn irgendwann ein anderer Task drankommt, dann muss ich quasi nur
hier die paar Register austauschen.
Die muss dann einfach aus dem Speicher laden, warum nicht Teil vom
Kontext.
Danach kann der sofort loslaufen.
Gut, damit das funktioniert, damit ich hier quasi garantiert ohne
Hauptspeicherzugriff bei der Segmentübersetzung arbeiten kann, habe
ich eine Bedingung und die Bedingung sagt im Wesentlichen, wenn in dem
Segmentregister für das Code-Segment was eingetragen ist, dann muss
das auch stimmen.
Das ist naiv, aber im Sinne von, dann muss es auch wirklich im
Hauptspeicher sein.
Bei anderen Segmenten, wo ich wirklich die Deskriptortabelle nehme, in
der Deskriptortabelle habe ich viel mehr Informationen.
Hier habe ich nur die Basisadresse und die muss dann auch wirklich
vorhanden sein im Speicher.
Bei einer echten Deskriptortabelle kann ich noch sagen und es ist
gerade nicht da.
Und dann weiß das System, gut, dann müssen wir dieses Segment erstmal
reinladen.
Die Möglichkeit habe ich hier nicht, weil ich nur dieses eine Register
habe.
Das hat ein bisschen abgespeckt.
Das heißt, hier dürfen nur Werte drinstehen für Sachen, die auch
wirklich gerade im physikalischen Hauptspeicher drin sind.
Und das Wort dafür heißt dann Working Set im Sinne von, das, was in
den Segmenten gerade eingetragen ist, das sind wirklich die Sachen,
mit denen die Anwendung gerade arbeitet.
Das sind ja meist nur diese drei Segmente, vielleicht ein paar mehr
und mehr braucht die Anwendung halt einfach nicht.
Und deswegen dieser bei wenig Segmente überschaubar beschreibbare
Working Set, den kann man in diesem Register hinterlegen.
Gut, gucken wir uns an, wie es bei alten x86 Prozessoren waren, um
danach zu sehen, wie es bei aktuelleren, zum Teil zu sehen, wie es bei
aktuelleren ist.
Es gibt verschiedene Adressierungsmodi und bei x86 ist ein verdammt
altes Konzept.
Und damals gab es halt einfach keine wirklich großen Hauptspeicher.
Also damals hat man optimiert für das, was damals war.
Und heute muss man so ein bisschen Legacy-Code oder Legacy-Konzepte
mit sich rumschleppen, damit die alten Sachen, auch wenn die heute
kein Mensch mehr benutzt, aber prinzipiell noch funktionieren würden.
Und deswegen gibt es verschiedene Adressierungsmodi.
Und der eine ist hier der Real Mode und der andere heißt Protected
Mode, den sehen wir gleich noch.
Aber zum x86, das ist richtig.
Der Real Adressierungsmodus war zu einer Zeit, wo es halt eben
Speicherschutz, für wen?
Das war alles sehr kleine, einfache Systeme und vor allem auch kleine
Adressen.
Also die Adressen könnten prinzipiell schon 32-bit, also die waren 32
-bitig, aber der Hauptspeicher war nicht so groß.
Der Hauptspeicher war hier nur 20-bit physikalische Adresse, die
wirklich nach außen zum Speicher ging, weil mehr brauchte man sowieso
nicht.
Und deswegen ist es hier so, also der einzige Unterschied, hier hat
man eigentlich wieder dasselbe, nur man hat irgendwie eine
Basisadresse, die kommt rein, man hat einen Offset, der kommt rein,
die werden addiert und das kommt dann die Ergebnisadresse raus.
Das ist alles mal dasselbe.
Der Unterschied ist, dieser Segment Selector ist genau wie ich es
gerade gesagt habe, der kommt nicht von der Deskriptortabelle, die
irgendwo im Hauptspeicher nachgeholt werden muss, sondern der kommt
von diesen Registern in der MMU.
Der kommt direkt von den Registern da reingesetzt, ich muss niemanden
sonst fragen und da wird dann einfach der Offset drauf addiert.
Das ist eigentlich der Grund, wofür man das damals so gemacht hatte.
Und das musste man dann erweitern, als man mehr Informationen,
Speicherschutz, Größe als mehr als 20-Bit-Adress, physikalische
Speicher da war, da musste man das dann erweitern auf richtige
Deskriptortabellen.
Und dieser Segment Selector in dem Sinne gibt es nur bei den alten
Systemen und die neueren haben alle Segment Deskriptoren, wo dann eben
noch viel mehr Informationen dabei sind.
Bei heutigen Systemen ist es dann eher so, dass die Speicher einfach
viel größer sind.
Ich habe einen virtuellen Adressraum, der kann gerne mal im Terabyte
-Bereich von der Größe liegen und den habe ich eigentlich nicht
einmal, sondern den habe ich für jede Anwendung.
Es ist klar, die passen nicht in den physikalischen Speicher rein,
aber ein Teil davon.
Häufig bei einer 32-Bit-CPU muss ich ihn abbilden auf einem maximal 4
-Gigabyte -großen physikalischen Adressraum.
Und wenn ich sogar eine 34-Bit-CPU habe, oder größer als 32 zumindest,
kann ich auch einen größeren physikalischen Adressraum haben.
Wie wir es gerade gesehen haben, ist es eben nicht mehr wie im alten
System der Segment Selector, sondern wir haben jetzt diesen
komplexeren Deskriptor in der entsprechenden Tabelle und der Zugriff
sieht dann wie folgt aus.
Das Bild ist wie vorhin, nur anders gemalt.
Ich nehme den Selector, mit dem Selector frage ich, wo in der
Deskriptortabelle, ich brauche einen Anfangspointer irgendwo, wo in
der Deskriptortabelle finde ich mein Element.
Das ist die Basadresse von dem Segment und die addiere ich mit dem
Offset.
Unterschied zu dem vorherigen Bild ist, woher kommt die Basisadresse.
Vorhin war die quasi fest, das war der Selector, jetzt ist sie
variabel, sie kommt aus der Tabelle aus dem Hauptspeicher.
Und dann weiß ich wieder, wo im Hauptspeicher...
Also diese Basisadresse, die hier rauskommt, die zeigt quasi auf den
Anfang hiervon und plus den Offset, das ergibt dann, wo der Operand
wirklich ist.
Okay, Segmente.
Und jetzt nochmal Seiten.
Wir hatten schon gesehen, verschieden groß, bis zu 4 KB, bis zu 8,
meistens 4.
Und wir haben einen Raster.
Wir haben einen Raster, einfach weil alle Seiten gleich groß sind.
Jetzt wieder bei dem Beispiel, das wir gerade gesehen haben,
weitergemacht.
Der X86 benutzt eine Kombination aus Segmenten und Seiten.
Der benutzt beides.
Irgendwann haben die mal auf Seiten gewechselt, die hatten aber vorher
Segmente und danach haben sie gesagt, ach Gott, jetzt damit es zu
allem kompatibel ist, machen wir einfach beides.
Was ein bisschen komplizierter wird in der ganzen Umrechnung, aber
ziemlich gut funktioniert, auch bis heute, bis zu den heutigen
Systemen, so genutzt wird.
Das heißt, das Bild wird langsam immer unangenehmer.
Das war noch nicht mal das letzte davon.
Wir haben jetzt wieder eine Adresse, ganz oben.
Wir haben als Kontext von der Anwendung, wo ist von der Anwendung die
Segment -Tabelle?
Die ist hier.
Also jede Anwendung hat ihre eigene Segment-Tabelle und die Anwendung,
die gerade da ist, das steht in dem Register, Teil des Kontextes der
Anwendung, wo ist sie?
Hier ist sie.
Plus innerhalb von dieser Segment-Tabelle, welches Segment soll es
denn sein?
Ja, hier ist 0 tatsächlich oben.
Also plus so viel nach unten, dann haben wir den M10-Eintrag.
Vorhin, als wir nur Segmente alleine angeschaut haben, stand hier
jetzt schon die physikalische Adresse drin, wir waren fertig.
Ja, ganz so einfach ist es hier jetzt nicht, sondern hier steht ja
dessen etwas anderes drin.
Hier steht dann drin, wo ist die Seiten-Tabelle?
Wir hatten jetzt gerade die Segment, also das hier ist die Segment
-Tabelle und die enthält die Information, wo ist für dieses Segment
die entsprechende Seitentabelle?
Das heißt, das Code-Segment, was variabel groß sein kann, ist
unterteilt in ganz viele einzelne 4-Kilobyte-Seiten.
Und wo diese 4-Kilobyte-Seiten sind, also wenn das jetzt hier das Code
-Segment, der Deskriptor für das Code-Segment wäre, wo diese Tabelle
ist, die sagt, welche Seiten sind gerade da, welche Seiten sind gerade
nicht da, wo die Tabelle ist, das sagt der Wert, der Wert zeigt also
hier im Hauptspeicher...
Also nochmal, die Tabelle, diese beiden Tabellen, die stehen beide im
Hauptspeicher.
Also das hier ist die Hauptspeicher-Adresse, wo ist die Seitentabelle
für dieses Segment?
Und da jetzt wieder plus den Offset.
Der Offset ist halt eben die, wievielte Seite in dieser Seiten-Tabelle
soll es denn sein?
Und hier habe ich jetzt die richtige Seite und das ist jetzt, nein,
immer noch nicht die physikalische Adresse.
Aber jetzt kommt nicht mehr viel was.
Jetzt kommt nämlich nur noch, ich habe hier die Seite, die 4-Kilobyte
-große Seite zum Beispiel, ich habe die Anfangsadresse der 4-Kilobyte
-großen Seite, jetzt muss ich nur noch den Offset dazu tüddeln.
Und Tüddeln ist in dem Fall nicht Addieren, wie hier, sondern Seiten
haben eine elegante Eigenschaft, da die Seitengröße geschickterweise
als Zweierpotenz gewählt wird, also als 4-Kilobyte zum Beispiel, ist
eine Zweierpotenz, gewählt wird, ist hier garantiert, weil alles an
dem Raster ausgerechnet ist, die Seitenbasisadresse wird die letzten,
Log 2 von 4-Kilobyte, 12 vielleicht, ja 12, die letzten 12 Bit
leerlassen, im Sinne von die werden 0 sein.
Und deswegen brauche ich die Bytes-Nummer, also welches der 2 hoch 12
-Byte, innerhalb meiner 2 hoch 12-Byte-großen Seite, das brauche ich
nicht Addieren, das brauche ich hier nur Konkatenieren, im Sinne von,
hier standen nur Nullen drin, letztendlich die kann man einfach
weglassen, weil man weiß, dass da nur Nullen drinsteht, und hier kann
ich dann einfach die letzten 12 Bit, wie sie von oben kommen, 1 zu 1,
hinten dran kleben, dann habe ich meine Gesamtadresse.
Am Anfang die Page, und da hinten der Offset, das wievielte Byte in
der Page.
Ja, ich habe irgendwie so ein bisschen die Vorteile von Byben
kombiniert, durch dieses Verfahren.
Ich kann jetzt zum Beispiel für jedes Segment Zugriffsschutz sagen,
das Code-Segment ist weiterhin nur readable, nicht schreibbar, und
ausführbar natürlich, also das Code-Segment ist nicht schreibbar, aber
ausführbar, und das überträgt sich dann auf alle Seiten vom Code
-Segment.
Ich muss diese Informationen also nur einmal sparen für das Code
-Segment.
Das erbt dann an alle Seiten, die im Code-Segment drinstehen.
Plus, ich habe von dem Paging die Vorteile, ich kann sehr fein
granular jede 4 Kilobyte Seite einzeln hin und her schaufeln, ich habe
keine externe Fragmentierung mehr, ich habe mir dafür ein bisschen
interne Fragmentierung wieder eingekauft.
Also irgendwie zum gewissen Grad die Vorteile der beiden Konzepte
verbunden, aber jetzt habe ich zwei Tabellen.
Also, ich habe sehr viele Tabellen, aber ich habe zwei Tabellen, die
ich für jeden Zugriff zugreifen muss.
Ich habe also eine Segment-Tabelle, eine pro Anwendung, und dann habe
ich nochmal ganz viele Seitentabellen, eine pro Segment, die alle
irgendwo im Speicherraum stehen, und zwei davon muss ich nacheinander
zugreifen, das heißt, jeder Speicherzugriff würde jetzt eigentlich mal
drei Speicherzugriffe sein.
Einmal Segment-Tabelle fragen, Seitentabelle fragen, und dann komme
ich erst an meine Daten ran.
Das ist der Nachteil.
Ich habe das alles ein bisschen komplizierter gemacht.
Es hat Ihnen aber nicht gereicht, Sie haben es später noch
komplizierter gemacht.
Das sehen wir aber gleich noch.
Ja, hier ist nochmal das Ganze schematisch etwas einfacher gesehen.
Wir haben also vorhin am Anfang die logische Adresse, könnte die
virtuelle Adresse sein, ist an der Stelle relativ egal, also wie man
es nennt.
Da geht erstmal eine Einheit, die Segment-Einheit, das ist genau das
Ding, was wir gerade gesehen haben, die Diskritorentabelle.
Da wurde doch spontan ein P da rein, oder?
Die Diskritorentabelle, mit der das Segment ausgerechnet, also das
Segment nachgeschaut wird, und das erklärt jetzt, ich habe das Wort
vorhin schon mal, es tauchte vorhin auf, das erklärt jetzt, was dieser
Begriff hier heißen soll, lineare Adresse.
Normalerweise, Segment alleine, war ja das Ergebnis von dem Segment
-Lookup direkt die physikalische Adresse.
Dadurch, dass wir jetzt ein mehrstufiges Verfahren haben, gibt es halt
zwischendurch auch noch irgendeine andere Adresse.
Naja, das Kind braucht einen Namen.
Und das haben sie halt eben lineare Adresse genannt.
Das ist einfach das Ding, was schon weiß, welches Segment es ist,
schon weiß, wo das Segment ist, aber noch nicht weiß, welche Page,
also wo die Page die gefragt ist.
Welche Page und wo die Page ist, weiß es noch nicht, weil es die
Adresse, bevor es in die Paging-Einheit reingeht.
Ihr habt ja gerade gesehen, dass es zwei verschiedene Einheiten sind.
Und das Ding dazwischen heißt lineare Adresse.
Die geht dann in die Paging-Einheit rein, also sie guckt im
Hauptspeicher nach, welche Seitentabelle ist es, und dann befindet
sich in der Seitentabelle der Offset und so weiter und so weiter.
Und dann geht es in die physikalische Adresse rein.
Achso, ja gut, das ist ab dem 3, glaube ich, 86 so.
Oder vielleicht sogar schon...
Ja, ich glaube, zum 30, glaube ich.
Und später wurde das Konzept im Wesentlichen erstmal beibehalten.
Die Bitgrößen haben sich geändert, je nachdem, was gerade wirklich ein
Speicher ist.
Das ist ja mehr verfügbar geworden, als es ursprünglich mal gab.
Je nachdem, wie viel gerade sinnvoll benötigt, erforderlich war, haben
sich die Bitgrößen geändert.
Aber das Konzept ist immer das gleiche geblieben.
Ach ja, die Seitenverwaltung ist prinzipiell optional.
Wir hatten gesehen, bei der Real Address, beim 286 gab es den.
Hatten wir vorhin gesehen, diese Real Address statt Protected Mode.
Der hätte nur die Segmentierung benutzt.
Das kann man einfach sagen, dann ist bitte schön, das, was aus der
Segment -Tabelle rauspurzelt, direkt als physikalische Adresse zu
benutzen.
Können also quasi die Einheit hier bypassen für alte Systeme.
Ist halt eben in diesem Kompatibilitätsmodus noch mit drin.
Wird aber eigentlich nicht mehr wirklich.
Also abwärtskompatibel deswegen, aber in der Wirklichkeit nicht mehr
wirklich benötigt.
Gut, jetzt hatte ich schon gesagt, das ist jetzt komplizierter
geworden.
Aber das reicht den Leuten ja nicht.
Man kann es ja noch komplizierter machen.
Es gibt aber auch einen Grund dafür.
Der Grund ist im Wesentlichen wie folgt.
Obwohl, erstmal schauen wir uns an, wie es komplizierter gemacht
worden ist.
Danach ist es sehr gut, es einfacher zu erklären.
Und zwar, das Zauberwort heißt zweistufige Seitenwechsel,
Seitenverfahren.
Also wir hatten ja gerade schon ein zweistufiges Verfahren.
Wir hatten erst die Segmente und dann die Seiten.
Und jetzt sind die Seiten aufgeteilt in noch mal zwei Teile.
Also es ist insgesamt dreistufig.
Erst die Segmente, dann die erste Seiten-Dingsy, dann die zweite
Seiten -Dingsy und dann habe ich die physikalische Adresse.
Das erklärt es eigentlich schon, aber letztendlich kann man es sich
noch mal angucken.
Ich habe jetzt hier die lineare Adresse.
Das ist die, die nach dem Segment-Dingsy rauskommt.
Also das Segment-Übersetzen habe ich schon gemacht.
Das war hier der Zwischenschritt.
Ich habe die lineare Adresse und mit der mache ich jetzt weiter.
Und jetzt ist auch diese Paging-Einheit zweistufig geworden.
Und zwar, prinzipiell wie immer, wenn es zweistufig ist, heißt das,
ich habe zwei Tabellen.
Die Tabellen sind auch beide wieder im Hauptspeicher.
Und die frage ich nacheinander.
Naja, und wenn ich die frage, muss ich irgendwie immer zwei Fragen
beantworten.
Ich brauche eine Basisadresse und ich brauche dann den Offset-Inhalt
der Tabelle.
Das ist dasselbe wie immer.
Für die erste Tabelle kommt die Basisadresse aus dem Kontextregister.
Das heißt, je nach Anwendung wird halt eben gesagt, da fängt die erste
Tabelle an.
Und dann gibt es in der linearen Adresse einen Teil des
Adressbereichs, der sagt dann, welcher Eintrag innerhalb der Adresse.
Und weil wir hier von Seiten reden, die alle schön zweipotenzmäßig
groß sind, können wir hier, wir müssen hier nicht addieren, wir können
die Sachen einfach kompatinieren.
Also hier sind ein paar Bits drin, die die Basisadresse hier angeben.
Davon sind die letzten Bits quasi garantiert auf Null gesetzt.
Das muss dieses Konzept einfach sicherstellen.
Und dann kann ich die Bits, die hier drin stehen, einfach hier hinten
reinschreiben, also kompatinieren.
Das ergibt dann meine Gesamtadresse und die greife ich im
Hauptspeicher zu.
Also das hier ist ein Load auf dem Hauptspeicher.
Und da kommt was raus.
Und das, was da rauskommt, ist eine Basisadresse für die nächste
Seitentabelle.
Das gleiche nochmal.
Ich nehme ein paar Bits hiervon.
Von dieser Basisadresse hier ist garantiert, dass die letzten Bits
Null sind.
Deswegen kann ich das, was hier von hier oben kommt, einfach drauf
kompatinieren.
Da kommt wieder ein Eintrag raus, das wieder garantiert, dass die
letzten Bits Null sind.
Deswegen kann ich es kompatinieren mit dem, was von hier kommt.
Und dann habe ich irgendwann mal die Adresse.
Also das ist dann wieder die Basisadresse von der Seite im
Hauptspeicher.
Das ist dann der Offset.
Und jetzt habe ich hier mein Wort in meiner Seite.
Ja, also abgesehen davon, dass es immer mehr Linien und Pfeile und
Tabellen gibt.
Das sind übrigens viele Tabellen, also muss man mal überlegen.
Das ist ein sogenanntes Seitentabellenverzeichnis.
Also das ist keine Seitentabelle, sondern das ist ein Verzeichnis für
alle Seitentabellen.
Ein so ein Verzeichnis pro Anwendung.
Und dann habe ich hier jeder Eintrag, potenziell jeder Eintrag im
Verzeichnis, zeigt auf eine Seitentabelle.
Es kann also verdammt viele Seitentabellen geben.
Und von ganz vorne gesehen nochmal ein Segmentdeskriptor pro
Anwendung, ein Seitentabellenverzeichnis pro Segment und hier ganz
viele Seitentabellen, also eine Seitentabelle pro Eintrag hiervon.
Also eine Riesenmenge an Tabellen.
Klar ist, das dauert immer länger.
Ich habe immer mehr Hauptspeicherzugriffe, bis ich überhaupt weiß, was
die physikalische Adresse ist.
Wofür ist das denn jetzt gut?
Der Grund ist im Wesentlichen folgender.
Weil die Speicher immer größer geworden sind, sowohl der physikalische
Hauptspeicher als auch, was die Anwendungen gefordert haben, an
virtuellen Adressgrößen.
Weil die immer größer geworden sind, sind auch die notwendigen
Seitentabellen immer größer geworden.
Wenn ich 4 KB Seiten habe und damit einen 32-Bit-Adressraum abdecken
möchte, den eine Anwendung ja gerne haben kann, dann sind das eine
ganze Menge 4 KB Seiten.
Das sind 2 hoch 32 minus 2 hoch 12.
Also 2 hoch 32 minus 12 Einträge.
Und die müssen alle im Hauptspeicher stehen.
Das sind eine ganze Menge.
Nun wird aber gar nicht jede Anwendung die kompletten 4 KB virtuellen
Adresse benötigen.
Aber vorhanden sein müssen sie theoretisch.
Und das ist die Idee, weswegen man das zweistufig macht.
Nicht jeder Eintrag im Seitentabellenverzeichnis ist gültig.
Wenn eine Anwendung nur ein paar tausend Seiten braucht, dann muss ich
hier nicht in jedem Seitentabellenverzeichniseintrag eine komplette
Seitentabelle hinterlegen.
Vielleicht hat meine Anwendung nur 5 Seitentabellen.
Das heißt, in dem Seitentabellenverzeichnis haben nur 5 Einträge die
Information gültig.
Hier ist wirklich eine Seitentabelle hinter, da musst du mal
hingucken.
Und die ganzen anderen Einträge, da steht Invalid.
Diese Seite gibt es nicht.
Die hättest du nicht anfragen sollen.
Wenn du es doch machst, beende dich bitte.
Und damit sind entsprechend viel weniger Seitentabellen.
Die Seitentabellen sind kleiner, dafür gibt es potenziell ganz viele,
aber in Wirklichkeit gibt es nicht ganz viele, sondern nur ein paar.
Und zwar nur so viel, wie wirklich gebraucht werden.
Jetzt ist das irgendwie angekommen.
Wenn ich nur einstufiges hätte, hätte ich eine Riesentabelle, wo
prinzipiell alle Seiten reinpassen können müssten.
Und da könnte es Einträge geben, die Seite ist gültig oder nicht.
Aber auf Granulatität von jeder einzelnen Seite.
Ich muss erstmal eine Tabelle haben, wo alle Seiten reinpassen würden.
Die Tabelle ist groß.
Stattdessen habe ich jetzt zweistufig.
Ich habe quasi hier das Level, wo ich sagen kann, die Seite ist nicht
da, kann ich im Verzeichnis schon machen.
Und dadurch gibt es viele von diesen Seitentabellen überhaupt nicht.
Und dadurch spare ich Speicherplatz.
Weil das ist ja was, was wirklich im Hausspeicher die ganze Zeit
verfügbar sein muss.
Zumindest solange der Task gerade läuft.
Wenn der Task nicht läuft, kann ich es auslagern, aber das kostet auch
echt Zeit, diese ganzen Sachen hin und her zu kopieren.
Meistens lässt man es im Hausspeicher.
Dafür zweistufige Seitenwechselfahren.
Zuvor mit dem Segment, dreistufig.
Ja, war schon angedeutet, aber es kann immer passieren, dass irgendein
Segment, irgendeine Seite potenziell gültig, aber gerade nicht da ist.
Gerade nicht im physikalen Schnauzug der vorhanden ist.
Und dann wird immer eine Unterbrechung ausgelöst.
Also es gibt einmal den Fall, dass eine Seite angefragt wird, die
hätte die Anwendung gar nicht anfragen dürfen.
Es gibt auch eine Unterbrechung, die gewöhnlich dafür sorgt, dass die
Anwendung beendet wird.
Und dann gibt es den Fall, dass eine Anwendung eine Seite anfragt oder
ein Segment.
Das ist gerade nicht im Speicher.
Die durfte das anfragen, aber es ist leider gerade ausgelagert.
Es gibt auch eine Unterbrechung und die Unterbrechung ist Händler,
muss es dann quasi einlagern und danach kann die Anwendung die gleiche
Anfrage nochmal stellen.
Und wie erkennt man, ob das da ist?
Na gut, da gibt es einfach in jedem Eintrag irgendwie ein Bit, was
sagt, ob es da ist.
Also sprich, in der Segment-Tabelle muss es irgendwie ein Bit geben,
was für jeden Eintrag in der Segment-Tabelle, die Einträge in diesen
Segment -Deskriptoren, das also für jeden Segment-Deskriptor zeigt, ob
dieses Segment gerade da ist oder nicht.
Zum Beispiel das Code-Segment, Daten-Segment, Stack-Segment kann man
mal von Anfang an laden, aber wenn irgendwann mal das Extra-Segment
gebraucht wird, das wurde vielleicht nicht von Anfang an gebraucht,
dann wird halt eben beim ersten Zugriff gemerkt, extra Segment gerade
nicht da, dann wird es erstmal reingeschafft.
Und eigentlich genau das Gleiche bei den Seitenfehlern.
Wenn eine Seite gerade nicht da ist, wird auch ein Bit gesetzt, das
ist prinzipiell erlaubt.
Es gibt eine Art von Übersetzung, aber die ist momentan auf
Festplatte, nicht im Hauptspeicher, holt es erstmal rein.
Das wird dann von diesem Unterbrechungshändler gemacht.
Wie der Händler funktioniert, sehen wir nächste Woche.
Da kommt ein Beispiel, wie so Interrupts und Traps an sich
funktionieren.
Jetzt hatten wir begründet, warum es diese vielen mehrstufigen Sachen
gibt, mit Seiten und noch Seiten und Segmente und das wurde ja immer
langsam.
Also wir hatten die Vorteile gesehen, Sprecherplätze und sowas, aber
der Nachteil ist ganz klar, jeder Hauptspeicherzugriff braucht jetzt,
in dem Beispiel hier drei, aber mit dem Segment davor eigentlich vier
Hauptspeicherzugriff.
Hier sind es deswegen drei, weil wir hier schon mit der linearen
Adresse anfangen.
Eigentlich sind noch die virtuellen Adresse davor, die erstmal durch
den Hauptspeicherzugriff in eine lineare umgewandelt werden muss.
Das ist langsam.
Naja, wie kann man das beschleunigen?
Die Titelzeile der Folie deutet das schon ziemlich deutlich an.
Cachen kann man alles mögliche.
Man muss nicht nur Instruktionen und Daten cachen, man kann alles
cachen.
Man kann auch so eine Kaskade von Übersetzungen cachen.
Dann baut man sich einfach so ein Ding dazwischen und damit es
spannender klingt, nennt man das Translation Lookaside Buffer.
Letztendlich ist es einfach nur ein Cache für diese Hierarchie von
Segmenttabelle, Seitentabellenverzeichnis, Seitentabelle, von diesen
ganzen Zugriffen, was am Ende bei rauskommt, wenn man einmal diese
aufwendige Übersetzung gemacht hat, dann kann man sich die doch mal
merken.
Weil die Chance, dass in naher Zeit ein Zugriff auf das Wort danach
oder ein paar Wörter danach in der gleichen Seite passiert, ist
eigentlich ziemlich hoch.
Da muss man diese ganze Übersetzung nicht immer wieder neu machen.
Die kann man sich merken.
Und das ist einfach nur ein Cache, ein voll assoziativer Cache, da
haben sie echt Fläche investiert, die sich diesen Zugriff merkt.
Und das ist wie bei jedem Cache, der wird gefragt mit einer Adresse.
Hier setzt sich die Adresse jetzt bildlich aus zwei Teilen zusammen,
aber es kommt eine Adresse rein, nämlich diese Bits hier kommen quasi
als eine Adresse rein.
Wenn es normal berechnet wird, deswegen ist es hier getrennt, wenn es
normal berechnet wird, gibt es dann eben die eine Hälfte für die eine
Tabelle, die andere für die andere Tabelle, aber aus Sicht von dem
Cache ist es einfach eine große Adresse, hier von Bit 11 bis 31.
Das geht als Anfrage rein, voll assoziativ heißt, gibt keinen Index,
also es könnte irgendwo hier drin stehen, muss man mit allen Tags
vergleichen.
Wenn man Glück hat, Hit, ist es gefunden und die Daten, die dann
rauspurzeln, das ist genau dasselbe, was hier nach der ganzen
Übersetzungskaskade rausgepurzelt wäre.
Deswegen kann man das dann direkt konkretisieren mit dem Offset
innerhalb der Seite.
Das spart Zeit, das spart nicht nur Zeit, das spart vor allem auch
zwei Speicherzugriffe.
Diese beiden Speicherzugriffe sind übrigens deswegen noch ziemlich
ärgerlich, weil die für gewöhnlich an völlig verschiedene Adressen im
Hauptspeicher gehen, also an nicht notwendigerweise benachbarte
Adressen.
Ich hatte ja mal angedeutet, dass es im DRAM-Hauptspeicher diese Row
-Buffer gibt, also es gibt so zwei D-Array, das ist der DRAM, davon
gibt es eine Zeile und die Zeile, die gerade in dem Row-Buffer drin
ist, die kann man ziemlich schnell zugreifen.
Aber die Chance, dass diese beiden Einträge hier, die ja nacheinander
gebraucht werden, in der gleichen Zeile sind, ist quasi null.
Das heißt, man muss erst vom DRAM die eine Zeile des DRAMs in den Row
-Buffer kopieren, dann kann man darauf lesen, dann muss es wieder
zurückgeschrieben werden in den DRAM.
Also auch wenn man es nicht modifiziert, man muss es in den DRAM
zurückschreiben, weil der DRAM so gebaut ist, das Lesen ist
destruktiv.
Wenn man es ausgelesen hat, ist es im DRAM kaputt gegangen, weil es
quasi nur mit einer kleinen Ladung, einer kleinen Kapazität
gespeichert worden ist, die ist danach entladen.
Man muss es wieder zurückschreiben in den DRAM, danach kann man eine
andere Zeile, die hier in den Row-Buffer rein kopieren, kann darauf
lesen.
Also das dauert alles Ewigkeiten.
Also nicht nur zwei Zugriffe, sondern auch eine ganze Menge DRAM
-Operationen.
Also hier bringt ein Hit unglaubliche Vorteile.
Ja, das ist eigentlich nur die Idee von diesem Translation Locus Site
Buffer.
Vollassoziativ, hatte ich schon gesagt, das muss man nicht, aber ist
üblich.
Der ist nicht allzu groß und da kann man sich das leisten.
Ja, und die zuletzt benutzten Einträge sind einfach da.
Trefferquote 90% ist so ein typischer Wert und die können halt einfach
viel, viel, viel schneller abgearbeitet werden.
Ja, bevor ich zu den Schutzmechanismen kurz noch komme, wollte ich
noch was sagen, das habe ich vorhin vergessen.
Wo wäre diese Folie?
Ja, und zwar bei dem Ort.
Wir hatten hier verschiedene Orte gesehen.
Wir hatten gesehen, der Cache kann vor der MMU sein, der Cache kann
nach der MMU sein.
In dem Bild ist hier jetzt nur angedeutet, als gäbe es einen Cache.
Naja, in Wirklichkeit gibt es mehrere Caches.
Also man muss das verallgemeinern.
In Wirklichkeit gibt es mehrere Caches und die MMU ist dann eher vor
dem ersten oder zwischen dem ersten und dem zweiten oder nach dem
zweiten oder vielleicht gibt es sogar noch einen dritten.
Also das ist die eine Verallgemeinerung.
Und dann gibt es noch, das wollte ich eigentlich sagen, eine ganz
andere Variante.
Und das ist auch die übliche.
Also von den beiden ist keine die übliche.
Und die Idee für diese Verallgemeinerung, da kommt man nicht so
einfach drauf.
Aber wenn man sich nochmal überlegt, wie ein Cache funktioniert, da
gibt es, die Adresse wird irgendwie als Input für den Cache benutzt.
Okay, aber sie wird für verschiedene Sachen benutzt.
Und zwar ignorieren wir jetzt mal den Vollassoziativen und überlegen
wir uns einen DirectMapped oder einen N-Wege-Assoziativ.
Sagen wir mal DirectMapped, einfacher für die Überlegung.
Im DirectMapped werde ich Teile von der Adresse für das eine benutzen
und Teile von der Adresse für was anderes.
Und zwar habe ich beim DirectMapped erstmal einen Teil der Adresse,
den ich als Index benutze.
Welche Zeile im DirectMapped-Cache soll es sein?
Oder beim N-Wege welcher Satz?
Also ich habe einen Index und danach, nachdem ich den Index benutzt
habe, kann ich von dem DirectMapped diese eine Zeile auslesen.
Da kommt der Tag bei raus.
Und danach kann ich den Tag, der in der Zeile drin stand, mit dem Tag
vergleichen, der von der Adresse stammt.
Also zwei Sachen, Index und Tag.
Und die werden nicht gleichzeitig benötigt, die werden zeitlich
nacheinander benötigt.
Und das ist die Idee für das Verfahren, wie es eigentlich gemacht
wird.
Und zwar heißt das, und am Namen kann man schon so ein bisschen raten,
wie es funktionieren könnte, es heißt Virtually Indexed Physically
Tagged.
Das heißt, es gibt eine virtuelle Adresse, es gibt eine physikalische
Adresse, es gibt ein Index, es gibt ein Tag.
Bei den Verfahren hier ist es sozusagen, hier wird die physikalische
Adresse sowohl für den Index als auch für den Tag von dem Speicher
benutzt.
Bei dem Verfahren hier wird die logische Adresse, also Virtually, die
virtuelle Adresse sowohl für den Index als auch für den Tag benutzt.
Und das andere ist jetzt eine Kombination von den beiden.
Und da gibt es jetzt verschiedene Arten, wie man das kombinieren
könnte.
Man könnte erst das eine, dann das andere oder umgekehrt machen.
Eine der beiden Varianten macht Sinn, die andere nicht.
Hier kann man schon ahnen, die virtuelle Adresse ist sofort verfügbar,
denn die ist vor der MMU.
MMU dauert vielleicht lange, wenn der Translation, Lookaside, Buffer
und Hit hat, okay, geht schnell, aber wenn der Miss hat, dauert das
ehrenamtlich lange.
Aber die virtuelle Adresse ist sofort vorhanden.
Also benutze ich die virtuelle Adresse, um bei meinem Cache zu sagen,
diese Zeile beim Dynamic Map oder beim N-Wege-Assoziativen, dieser
Satz.
Dann kann ich schon mal beim Cache aus dem Tag Array vom Cache diese
Zeile laden.
Da kommt dann das Tag raus.
Und dieses Tag muss ich jetzt vergleichen mit der physikalischen.
Also während ich hier das Tag Array lesend befrage, kann gleichzeitig
die MMU die Übersetzung machen und im Idealfall, wenn der hier einen
Hit hat, im Translation, Lookaside, Buffer, dann kommt die
physikalische gerade rechtzeitig, um danach den Tag aus der
physikalischen zu nehmen und zu verglichen werden mit dem Tag, der
hier drin steht.
Also die Idee ist, dass man das eine virtuell, das andere physikalisch
macht.
Warum?
Weil man erst das eine, dann das andere braucht.
Was ist der Vorteil?
Naja, der Vorteil ist, ich kann den Cache hier vorne stehen lassen,
also vor der MMU.
Das ist gut, weil ich muss nicht jedes Mal die MMU fragen.
Wenn ein Cache schickt, ist das wirklich schnell.
Das ist der Vorteil.
Und der Nachteil, den ich vorhin hatte, der ist weg, größtenteils.
Der Nachteil, den ich vorher hatte, ist, wenn die Anwendung wechselt,
wenn ich einen Kontextwechsel habe von Anwendung A auf Anwendung B,
musste ich vorhin den Cache flaschen, weil Anwendung A und B mit der
gleichen virtuellen Adresse völlig verschiedene physikalische Adressen
meinen könnten.
Aber weil ich die Texte, die hier drin gespeichert sind, von der
physikalischen Adresse nehme, kann ich jetzt das auseinanderhalten.
Auch wenn die gleiche virtuelle Adresse bei Anwendung A eine andere
physikalische meint als bei Anwendung B, kann ich das jetzt sauber
trennen, weil hier als Tag die physikalische Adresse genannt wird.
Das heißt, ich muss bei dem Taskwechsel den Cache nicht mehr flaschen.
Und das ist letztendlich der Grund, weswegen dieses Virtually Indexed,
Physically Tagged normalerweise benutzt wird.
Das umgekehrte Physically Indexed, Virtually Tagged, kann man sich mal
überlegen, das vereinigt quasi die Nachteile von beiden Verfahren, ist
also ziemlich witzlos.
Kommen wir noch zu Schutzmechanismen.
Schutzmechanismen.
Jetzt hatte ich vorhin schon angedeutet, es gibt solche
Schutzmechanismen, die einfach sagen, executable, ja, nein,
schreibbar, ja, nein, solche Sachen.
Und es gibt darüber hinaus aber noch weitere Schutzmechanismen in
Prozessoren in enger Zusammenarbeit mit dem Betriebssystem, die im
Wesentlichen versuchen, dafür zu sorgen, dass sich die Anwendungen
nicht gegenseitig kaputt machen.
Das heißt, die Schutzmechanismen wie no execute und nicht schreibbar,
das sorgt dafür, dass eine Anwendung alleine für sich selbst laufend
schon mal keinen Blödsinn machen kann.
Die kann nicht irgendwas ausführen, was nicht ausführbar ist.
Die kann nicht irgendwas schreiben, was nicht schreibbar ist.
Aber sie könnte nur no execute und writeable, nur diese
Schutzmechanismen gegeben, könnte sie gerne Code von einer anderen
Anwendung ausführen oder Daten von einer anderen Anwendung
überschreiben.
Das ist normalerweise auch nicht gewünscht.
Also Isolation der Anwendungen voneinander und insbesondere Isolation
von den Anwendungen gen zum Betriebssystem.
Die Anwendung sollte auch nicht in der Lage sein, das Betriebssystem
kaputt zu machen.
Und dafür gibt es verschiedene Ebenen, verschiedene Schutzebenen.
Einfach nur durchnummeriert der mit der höchsten Priorität, das ist
das Betriebssystem.
Und dann gibt es halt eben verschiedene Ebenen danach, es könnten
vielleicht Treiber sein und irgendwann kommt mal ein Laufzeitsystem
und dann kommt mal die Anwendung.
Die Anwendung ist das Letzte.
Das ist das am wenigsten Wichtige.
Ist zwar das, was der Nutzer sieht, aber das ist das am wenigsten
Vertrauenswürdigste.
Es könnte von überall gekommen sein.
Und da werden dann quasi diese Ebenen getrennt.
Hier haben wir zum Beispiel eine Version mit vier Ebenen und die
Ebenen werden hier auf dem Level der Segmente verwaltet.
Die vertrauenswürdigste Ebene wäre dann das Betriebssystem und Ebene
drei wäre dann die Anwendung.
Das könnte von überall, habe ich runtergeladen, lasse ich mal laufen,
dem kann ich nicht trauen.
Und da gibt es jetzt quasi, ja gut, so könnte man das erstmal
darstellen, Betriebssystem Kern, höchste Ebene, Basic Input Output
System, also Treiber, I.O.
Geräte, Kommandointerpreter, Anwendungen, also verschiedene Ebenen.
Und jetzt gibt es halt Regeln, die sagen, auf was darf man zugreifen.
Und diese Regeln muss man sich ausdenken und danach muss man sie
umsetzen.
Das Umsetzen ist wieder in Hardware gemacht und deswegen ist das hier
einfach mal aufgelistet.
Und die Regeln, das sind die obersten beiden Punkte.
Also ein Prozess, eine Anwendung darf nur auf Daten zugreifen, die
höchstens, das klingt komisch, aber die höchstens so vertrauenswürdig
sind wie die Anwendung selbst.
Warum soll ich nur auf Daten, ich meine, wenn da andere Daten sind,
die noch vertrauenswürdiger sind, warum soll ich darauf nicht
zugreifen?
Es liegt daran, dass Zugreifen eben auch Schreiben heißen kann.
Ich darf keine Daten überschreiben, die jemandem gehören, der in der
Hierarchie über mir steht.
Also ich darf keine Daten vom Betriebssystem überschreiben.
Ich darf keine Daten vom Treiber überschreiben.
Ich als Anwendung darf sowas nicht.
Deswegen, ich meine, Lesen könnte man sich überlegen, ja, warum nicht.
Aber letztendlich sind auch im Betriebssystem interne Informationen
verfügbar, Informationen über andere Anwendungen.
Die gehen mich nichts an, mich als irgendeine von den Anwendungen.
Deswegen darf ich die halt eben auch nicht lesen.
Die darf ich nicht zugreifen.
Ich darf nur auf Daten zugreifen, die weniger vertrauenswürdig sind.
Also das Betriebssystem darf gerne auf die Daten zugreifen von der
Anwendung.
Macht es normalerweise nur in einer Situation, nicht im
Kontextwechsel.
Im Kontextwechsel muss es die Daten ja nehmen, irgendwohin retten und
später wiederherstellen.
Also da geht es nicht anders.
Aber das ist auch okay, wenn man dem Betriebssystem vertraut.
Und das hatte ja den höchsten Trust Level gehabt.
Das ist eine Annahme, wenn man dem Betriebssystem vertraut, also wenn,
dann ist es auch kein Problem, dass Betriebssysteme auf die
Anwendungsdaten zugreifen können.
Umgekehrt, das war Daten, umgekehrt Code.
Ein Prozess darf nur Code benutzen, der mindestens so vertrauenswürdig
ist wie er selbst.
Also höchstens und mindestens, gerade umgekehrt.
Das heißt insbesondere, das ist auch wieder gut, das ist sehr sinnvoll
so rum, das heißt insbesondere, wenn ich eine Anwendung bin, dann darf
ich Funktionen ausführen, die Teil des Betriebssystems sind.
Das ist gut.
Also wenn ich ein Synchronisierungsmechanismus habe, eine Semaphore,
ein Log, irgendwas, um mich zu synchronisieren mit einer anderen
Anwendung.
Wenn ich Code ausführe vom USB-Treiber, naja, der Treiber ist
prioritätsmäßig über mir.
Also der ist vertrauenswürdiger.
Das heißt, solchen Code muss ich ausführen können, sonst kann ich ja
die Dienste, die das Betriebssystem und die Treiber anbieten,
überhaupt nicht wahrnehmen.
Umgekehrt, das Betriebssystem sollte nicht Code ausführen von einem
Treiber.
Wenn das Betriebssystem Code ausführen würde von einem Treiber, der
Treiber ist Blödsinn, ist ja weniger vertrauenswürdig, der kommt von
einem externen Hersteller, der Treiber macht Blödsinn und stürzt ab,
dann würde das Betriebssystem als Aufrufer von dem Code mit abstürzen.
Was ja auch in der Vergangenheit häufig genug passiert ist.
Jetzt ist es so, dass nur die Anwendung rüft den Code vom Treiber auf.
Wenn der Treiber Blödsinn ist, der dann stürzt die Anwendung ab.
Who cares?
Das System läuft zumindest weiter.
Also der Benutzer kehrt, aber zumindest das System läuft weiter, nur
die eine Anwendung ist betroffen.
Das ist der Grund für diese Reihenfolgen, weswegen bei Daten nur auf
Höchstens so Vertrauenswürdige und bei Code darf man nur ausführen,
der mindestens so vertrauenswürdig ist.
Genauso darf natürlich das Betriebssystem nicht Code von der Anwendung
ausführen.
Das wäre sehr fahrlässig.
Ja, wären wir für heute eigentlich fertig.
Das nächste Kapitel fange ich jetzt nicht mehr an.
Hätten wir die virtuelle Speicherwaltung abgeschlossen, haben gesehen,
dass es diese ganzen verschiedenen Arten gibt.
Es gibt Segmente, es gibt Seiten, es gibt die Kombination von beiden,
es gibt auch mehrstufige Seitenverfahren, es gibt Schutzmechanismen,
die ganzen Tabellen.
Man kann das auch wieder cachen, man kann diese ganzen Übersetzungen
cachen, haben wir gesehen.
Das sind so die Grundzüge von der virtuellen Speicherwaltung.
Details gibt es überall noch weitere zu finden, aber im Wesentlichen
haben wir jetzt gesehen, wie das funktioniert.
Und nächste Woche gucken wir uns dann an, wie ein Buszugriff
eigentlich funktioniert.
Bisher hätten wir immer angenommen, irgendwie kommunizieren die, aber
in Wirklichkeit ist da irgendwie ein Bus dazwischen, wo mehrere
Zugriffe sich über einen physikalischen Kanal kommunizieren müssen.
Das gucken wir uns an und gucken uns an, wie Unterbrechungen
funktionieren und machen bei diesem Thema weiter.
Dankeschön und bis nächste Woche.