Sie befinden sich hier: Termine » Prüfungsfragen und Altklausuren » Hauptstudiumsprüfungen » Lehrstuhl 4 » Verlauf   (Übersicht)

Prüfer: V. Sieh
Beisitzer: Bernhard
ECTS: 7,5

Die Atmosphäre war entspannt und locker. Die Reaktionen auf die Erklärungen waren schwer deutbar. Ich hatte oft den Eindruck, die Antwort wäre nicht ganz richtig gewesen. Allerdings wurde ich anschließend nicht korrigiert und auch am Ende für keine Antwort kritisiert. Trotz ein paar Hängern bei unerwarteten Fragen, gab es noch eine 1,0.

Verlauf

P: Was gibt es für verschiedene Virtualisierungsarten und wie verwendet man sie?
S:

  • Interpretieren/JIT: Verschiedene Gast-Host Architektur. Gast Instruktionen werden in Software Instruktionen nachgebaut.
  • HW-basiert: Gast und Host haben die selbe Architektur. Der Gast läuft im User-Mode, mit Hardware Erweiterung werden kritische Instruktionen abfangen.
  • Paravirtualisiert: Man weiß, dass das Betriebssystem virtualisiert werden soll. Man passt es an, sodass es ohne Hardware-Extention und Protection Faults im User-Mode laufen kann.
  • Hypervisor: Man will auf einem OS nur ander OSs laufen lassen, deswegen soll es möglichst kompakt sein und optimiert für die Verwaltung.
  • OS-basiert: Man will meherere Betriebssysteme der gleichen Kernel laufen lassen. Dazu muss man kein OS auf OS laufen lassen. Man hat mehrere OS Instanzen, die denken sie wären alleine.
  • Biblitheksbasiert: Man will nur ein Programm auf einem anderen OS laufen lassen. Dafür baur man die ABI nach.

P: Wie funktioniert OS-basierte Virtualisierung?
S: Man verändert die Verwaltungsbefehle für Prozesse und User. Die Instanznummer wird als Präfix davor geschrieben.

P: Was macht man neben den Prozessen noch, was muss man schützen?
S: Man schützt auch noch die Dateien, in dem man für jede Instanz Kopien der Dateien erstellt, die sie verändert.

P: Wie ist das bei fork()?
S: Eine Instanz darf nicht zu viele Prozesse starten, das muss in fork überwacht werden.
Das gleiche muss beim RAM und Festplatte passieren. Zuerst kann man den anderen Instanzen ungenutzten Speicher wegnehmen, aber irgendwann muss Schluss sein.

P: In der Übung haben wir einen Interpreter programmiert, warum verwendet man sowas nicht immer?
S: Weil er 100-1000 mal langsamer ist als das Originalsystem.

P: Warum ist der so viel langsamer? Wenn ein Add emuliert werden soll, gibt es bei x86 doch einen entsprechenden Befehl. Warum nimmt man den nicht einfach?
S: Die Register vom Host passen normalerweise nicht zu denen von Gast.
Zuerst müssen die Operanden in Register vom Gast geladen werden.
Eventuell (bei CISC) kann ein Operand aus dem Speicher kommen, dann müssen noch viele Checks gemacht werden.
Danach wird die Addition ausgeführt.
Dann muss das Ergebnis wieder in Gast Register oder Speicher geschrieben werden. Hier sind auch wieder Checks nötig.

P: Was gibt es denn alles für Checks bei Speicherzugriff
S: Zuerst die Segmentierung. Es wird überprüft, ob das Wort die Grenzen des Segemnts einhält.
Außerdem werden Lese/Schreibe/Ausführungsrechte geprüft.
Dann muss noch eventuell noch das Alignment und Watchpoints überprüft werden.
Die MMU muss die Adresse aus dem TLB laden oder sogar die Pagetabelle durchgehen.
Dann müssen die Lese/Schreib-Rechte der Page geprüft werden.

P: Wie kann man das im Emulator beschleunigen?
S: Man baut sich selber einen TLB. Dann kann man direkt den Host pointer für eine gast Page laden.
Außerdem kann man mit dem TLB auch Alignment und Rechte überprüfen.
Wenn man den TLB für Pages macht, hat man Bits 0-11 frei.
Man maskiert den Eingabepointer, sodass das Bits 3-11 wegfallen.
Wenn von den ersten 2/3 Bits welche gesetzt sind, ist das Aligntment falsch.
Die restlichen Bits kann man für Rechtechecks nutzen.
Man kann z.B. bei JIT Codepages ein Bit setzen, sodass beim Schreibzugriff der Lookup fehlschägt. Dann kommt man in den Slow-Case und erkennt, dass in Code geschrieben wird. Dann kann mman den jeweiligen Block invalidieren.
Das gleiche kann man für Lese- und Schreibrechte machen wenn man getrennte Lese/Schreib/Ausführ-TLBs hat.
Wenn man auch noch die Privilegien-Checks haben will, macht man am besten einen mehrdimensionalen TLB.
Dann hat man pro Ring und Segment und Zugriffsart eine TLB.
Wenn man z.B. versucht im User-Mode Kernel-Code auszuführen, dann schlägt das fehl.
In dem Fall stehen die Kernelpages sowieso nicht in diesem TLB, weil sie noch nie im User-Mode ausgeführt wurden.

P: Was kann man neben dem Speicher TLB noch machen?
S: Man kann auch Alignment und Debug Checks mit dem JIT in die Code Blöcke kompilieren.

P: Ich meine eher I/O Geräte.
S: Dann speichert man statt Pointern Callbacks im TLB.

P: Was machen die Callbacks genau?
S: Wenn man z.B. in einen Port schreibt, um ein Pixel anzuzeigen, wird der entsprechende Callback aufgerufen. Der emuliert das Geräteverhalten und aktualisiert den Zustand.

P: Und woher kommen die Callbacks?
S: Die werden beim Start des Systems registriert. Dann weiß man welche Geräte tatsächlich angeschlossen sind.
Man hat für jeden Komponenten ein Objekt, das registriert seine Callbacks am Bus.

P: Aber wenn sich jetzt ein Gerät am ISA-Bus registriert, woher weiß der CPU das?
S: Was?

P: malt Bus-Hierarchie mit CPU-Bus, PCI-Bus etc. auf
S: Der CPU gibt dem Bus den Port, der CPU-Bus schaut, zu welchem Unterbus/Gerät der gehört.
Dann gibt der Unterbus das passende Callback an den CPU-Bus, der gibt es an den CPU und der führt es aus.

P: Geht das noch besser?
S: Man könnte das Endgerät-Callback im CPU Bus cachen. (nicht sicher, ob das die gewollte Antwort war)

P: Intern hat ein Komponent ja auch Busse.
S: Ja, allerdings sind die fest und es ist zur Compilezeit schon bekannt was wo angeschlossen ist.
Deswegen braucht man keine Callbacks und kann Funktionen direkt aufrufen. Im Idealfall werden sie dann ge-inlined, sodass keine Calls mehr nötig sind.

P: Wie funktioniert denn die JVM?
S: Die JVM im speziellen oder eine JIT-optimierte Sprache?

P: JIT-optimierte Sprache am Beispiel der JVM.
S: Die Anwendung arbeitet nur auf dem Stack und es werden sichere Pointer verwendet.
Sie bekommt nur Pointer von der JVM und kann sie nicht verändern.
Außerdem werden bei Recheninstruktionen keine Flags gesetzt. Es gibt nur die CMP Instruktion, die die Flags setzt.
Deswegen muss sich der JIT keine Gedanken machen, wann er die Flags berechnen muss.

Es gibt keine berechneten Sprünge und keinen selbstmodifizierenden Code, deswegen könnte sogar ahead-of-time kompiliert werden.
Das macht man allerdings nicht, denn mit JIT muss man nur den tatsächlich verwendeten Code kompilieren.
Außerdem kann man Laufzeitwissen verwenden, um Blöcke zu optimieren.
Man kann man z.B. einen Counter in einen Block kompilieren. Wenn man merkt, dass der Block häufig ausgeführt wird, kann man ihn noch einmal kompilieren und besser optimieren.

P: Wie genau funktionieren die sicheren Pointer?
S: Das bedeutet, es gibt keine Pointerarithmetik, sodass man keine Rechteprüfungen machen muss.
Die Anwendung bekommt einen korrekten Pointer von der VM und weil sie daran nichts ändern kann, müssen keine Überprüfungen durchgeführt werden.
Weil die Pointer typisiert sind, ist auch die Größe bekannt und man braucht auch keine Bounds- und Alignment-Checks mehr.

P: Wie funktionieren Exceptions in der JVM?
S: Es gibt try/catch Blöcke. Tritte eine Exception auf, wird sofort in den catch-Block gesprungen. Man muss also keine Instruktion wiederholen oder Zustand wiederherstellen.
Es reicht, wenn man zum Verabreitungs-Abschnitt im Block springt und dann PC, Flags etc. setzt. Das ist dann nicht wesentlich aufwendiger als ein normales return.

P: Wie werden die switch-Befehle dann ohne berechneter Sprünge übersetzt?
S: Man kann keine Table-Switch verwenden. Stattdessen kann man eine if-Kaskade oder binäre Suche verwenden.

P: Ein „if“ ist doch auch ein berechneter Sprung.
S: Ja, allerdings kennt man das Sprungziel ahead-of-time. Dadurch kennt man trotzdem noch alle Pfade durch den Code, die genommen werden können.

P: Was wird jetzt noch zur Laufzeit gemacht?
S: Bei Java gibt es den Bytecode Checker. Der durchläuft das Programm auf allen möglichen Wegen.
Es gibt zu jeder Intruktion Vor- und Nachbedingungen. Vor einer Addition müssen z.B. zwei Integer auf dem Stack liegen und danach liegt ein Integer auf dem Stack.
Man überprüft dann bei jeder Instruktion für alle Pfade, die zu ihr führen können, ob der Stack und die Typen passen.
Während der Ausführung muss man nur noch Array Bounds und Downcasts überprüfen. Null-Pointer exceptions werden über Segmentation-Faults des Host-Systems erzeugt.

P: Was kann man den bei einem return noch optimieren?
S: Wenn man bei einem Sprung weiß, wo er hinführen wird, kann man direkt den nächsten Block anspringen, solange er schon kompiliert ist.
(nicht sicher, ob das die gewollte Antwort war)

P: Was passiert bei loops in einem Block?
S: Man kann entweder einen Timer programmieren, sodass man bei einer Endlosschleife unterbrochen wird, oder eine Counter in die Schleife einfügen.

P: Das sind allgemeine JIT Lösungen. Wie macht das die JVM?
S: Das weiß ich nicht.

P: Weil die JVM nur eine Applikation simuliert, muss sie nicht zwangsweise wieder in die Hauptschleife zurückkehren.
Wenn die Applikation in einer Endlosschleife ist, dass hängt sie einfach fest.