#+title: Virtuelle Maschinen
#+author: Florian Guthmann
#+email: florian.guthmann@fau.de
#+options: num:nil \n:t html-style:nil html5-fancy:t html-postamble:nil toc:nil
#+html_doctype: xhtml5
#+html_head:
#+html_head:
#+html_mathjax: path:/~oc45ujef/mathjax/es5/tex-mml-chtml.js
#+MACRO: abbr @@html:$1@@
Eine Zusammenfassung, die versucht die grundlegenden Ideen aus der Vorlesung zu übermitteln (Was angesichts des fehlenden Skripts nicht trivial ist).
* Emulation
** Generelle Performance
- Operationen dekodieren :: Operationen müssen jedes mal dekodiert werden
- Flags :: Berechnung ist teuer
- Speicherzugriff :: Bus ist langsam (mehrere Buslevel...)
z.B. =out=:
1. Value mit PortNr. an Bus geben
2. Bus callt alle mit Callback registrierte Komponenten. Jede prüft ob sie angesprochen ist.
** Speicherhierarchie
Bei einem Speicherzugriff müssen viele einzelne Schritte durchlaufen werden:
- Segmentierungseinheit ::
- ist Segment ein NULL-Segment? \Rightarrow =Protection Fault=
- ist Segment nicht schreibbar \Rightarrow =Protection Fault=
- ist Adresse plus Operandengröße innerhalb/außerhalb des =%es=-Segments?
- lineare Adresse berechnen (virtuelle Adresse plus Segmentbasisadresse)
- Alignment-Check :: ist der Operand nicht korrekt aligned? \Rightarrow =Alignment Fault=
- Debug-Einheit :: ist Adresse ein Watchpoint? wenn ja und
- Operandengröße entspricht Watchpointgröße
- Schreib/Lese-Richtung stimmt
\Rightarrow =Debug Trap=
- MMU ::
- TLB
- physische Adresse berechnen
- Zugriffskontrolle
- Access/Dirty-Bits setzen
- Caches ::
#+begin_solution
- Caches weglassen ::
- bringen eh keinen Mehrwert
- JIT ::
- Segmentierungscheck nur dazu kompilieren wenn Segmentlimit wirklich limitiert (\neq 4GiB) (ist er z.B in MSDOS)
- Segmentbasiadresse nur dazu kompilieren wenn \neq 0
- Alignmentcheck nur dazu kompilieren wenn eingeschaltet (meistens der Fall)
- selbiges für Watchpoints usw.
- MMU ::
- in TLB direkt Pointer zu emulierten Speicherseiten eintragen
- Pseudo-TLB ::
Wir benutzen Cache der
- über Parameter des Speicherzugriffs indiziert wird (=Read/Write/Execute=, Privilegstufe, Segment-Nummer, Segment-Offset)
- direkte Pointer auf emulierten Speicher enthält
Die naive Implementierung braucht zuviel Speicher (hier ~ 766 GiB):
#+begin_src c
/* ring seg offset] */
static uint8_t *mem[4] [6] [1 << 32];
#+end_src
daher
#+begin_src c
#define COUNT 4096
static struct entry {
uint32_t offset;
uint8_t *ptr;
} mem[4][6][COUNT]
void store (int ring, int seg, uint32_t offset, unint8_t val) {
int hash = offset % COUNT;
struct entry *ent = &mem[ring][seg][hash];
uint8_t *ptr;
if (ent->offset != offset) {
ent->offset = offset;
ent->ptr = get_ptr(ring, seg, offset);
}
ptr = ent->ptr;
if ( ptr == NULL) {
store_slow(ring, seg, offset, val);
} else {
*ptr = val;
}
}
#+end_src
Da bis auf Seitengrenzen =get_ptr(ring, seg, offset + 1) \eq get_ptr(ring, seg, offset) + 1= gilt:
#+begin_src c
#define MASK 0x1f
void store (int ring, int seg, uint32_t offset, uint8_t val) {
int hash = (offset / (MASK + 1)) % COUNT;
struct entry *ent = &mem[ring][seg][hash];
uint8_t *ptr;
if (ent->offset != offset & ~MASK) {
ent->offset = offset & ~MASK;
ent->ptr = get_ptr(ring, seg, offset & ~MASK);
}
ptr = ent->ptr;
if (ptr == NULL) {
store_slow(ring, seg, offset, val);
} else {
*(ptr + (offset & MASK)) = val;
}
}
#+end_src
#+end_solution
* JIT
Emulation ist in der Regel zu langsam:
z.B. ein Assemblerbefehl wie
#+begin_src asm
add eax [esp + 0x4]
#+end_src
1. Liegt Strom an?
2. Hat der letzte Befehl eine Exception ausgelöst?
3. Liegt ein System-Management-Interrupt vor?
4. Liegt ein Interrupt vor?
5. Ist die CPU im HALT-Zustand?
** Optimierungen
- Instruktionen werden nur einmal dekodiert
- IP wird nur am Ende eines Basisblocks inkrementiert
- Lazy Flags
** Beispiel JVM
Die JVM ist optimiert auf gute JIT-Performance:
- keine Interrupts (statdessen Multithreading)
- keine berechneten Sprünge
- kein selbstmodifizierender Code
- keine MMU, keine Segmentierung, sichere Pointer:
Wenn eine Code-Zeile ausgeführt wird
- liegen immer die gleichen Typen von Argumenten auf dem Stack
- liegen immer die gleichen Typen in den lokalen Variablen
Für Hardware aber ungeignet:
- Garbage Collection
- Code-Prüfung
** Exceptions
#+begin_problem
Exceptions lassen keine linearen Blöcke zu
#+end_problem
#+begin_solution
Rückkehr in die Hauptschleife
#+end_solution
#+begin_problem
Dann greifen aber einige Optimierungen nicht mehr:
- IP muss u.U. auch im Block gesetzt sein
- Flags müssen u.U. berechnet werden
#+end_problem
* Hardware-basierte Virtualisierung
kritische Instruktionen:
- =inb=, =outb= :: Ein-/Ausgabe muss emuliert werden
- =cli=, =sti= :: sollen /virtuelle/ Interrupt-Flags setzen
- =mov *, %cr= :: muss überprüft werden
- =popf= :: verhält sich in User/Supervisor-Mode unterschiedlich
- =sidt=, =sgdt= :: nicht priviligiert, fragt aber emulierte Informationen ab
** Pacifica/Vanderpool
x86-Erweiterungen die kritische Instruktionen filtern.
=vmlaunch= und =vmresume= starten CPU-Emulation, wechseln CPU-Zustand
Ein =exit=-Event tritt auf, wenn
- =in= oder =out=
- =hlt=
- Control-Register wird beschrieben
- Interrupt tritt auf
Dann wird der aktuelle Zustand (=rsp=, =rip=, =rflags=, =cr*=, Segmentregister) der emulierten CPU gespeichert und der Host geladen.
Problematisch ist dabei die *Umschaltzeit*.
*** Zugriff auf die Grafikkarte:
- viele =mov=-Instruktionen, die überprüft werden müssen
- also sehr viele Moduswechsel
#+begin_solution
Möglichkeiten:
- =mov= auf Funktionsaufrufe abbilden (schlechte Performance bei Grafikoperationen)
- =mov= in Grafikspeicher schreiben lassen, Bildschirm-Update per Polling
- spezielle Grafiktreiber verwenden:
z.B =mov= in Buffer, dann Grafikkarter Pointer übergeben (nur ein exit-Event)
#+end_solution
*** Interrupts
=cli=, =popf= sehr häufig, also viel Umschaltzeit.
#+begin_solution
Also Interrupt-Enable-Flag der VM merken, Host unberührt lassen. Interrupts gehen an den Host.
Bei =vmresume= können Interrupts für die VM übergeben werden.
#+end_solution
*** Exceptions/System-Calls
in der VM selbst, da extra Register für VM existiert.
*** Segmentierung
zusätzliche Privilege-Level für Guest/Host
*** MMU
Kritisch ist
- =mov *, %cr3= :: Page-Directory-Basis-Register laden
- =mov *, %cr{0,4}= :: selten
#+begin_solution
Für =%cr3= in Hardware unkritische Werte bereitstellen, die nicht Exception generieren.
Sonst wird Exception generiert (langsam).
#+end_solution
* Paravirtualisierung
Guest-OS wird portiert auf spezielle /Hardware/ (Supervisor)
Supervisor-instruktionen müssen also ersetzt werden. z.B in x86:
- =in=, =out= :: Ein-/Ausgabe
- =mov *, %cr3= :: Pointer auf Pagetabelle laden
- =cli=, =sti= :: Disable/Enable Interrupts
- =pushf=, =popf= :: Interrupt Status speichern/laden
- =lidt= :: Pointer auf Interrupttabelle laden
#+begin_solution
- physischer Speicher :: eine Datei
- virtueller Speicher :: eine gemappte Datei
- Interrupts :: Signale
- Real-Time-Clock/Timer :: System-Clock, Timer
- Konsole :: Terminal oder GUI
- Netzwerkkarte :: Socket
#+end_solution
** MMU
#+begin_idea
MMU emulieren mittels =mmap= und =munmap=.
#+end_idea
#+begin_problem
- viele (langsame) System-Calls nötig für Kontextwechsel
- nicht beliebig viele Mappings möglich (Limitierung im Linux-Kernel)
- Host-OS belegt Teil des virtuellen Adressraums, also muss Guest verschoben werden.
- Guest läuft im User-Modus, Guestapplikationen laufen im User-Modus, also keine *Kernel Protection*
#+end_problem
** Interrupts
#+begin_idea
Statt Interrupts Signale verwenden.
- Programmieren der Signale :: =sigaction=
- Setzen des Signal-/Supervisor-Stacks :: =sigstack=
- Enable/Disable der Signale :: =sicprocmask=
#+end_idea
#+begin_problem
- Interrupts werden sehr häufig ein-/ausgeschaltet
- =sicprocmask= ist langsam
#+end_problem
#+begin_solution
Signale immer zulassen. Der Signal-Handler wird aber nur aufgerufen, wenn virtuelle Interrupts eingeschaltet sind.
Sonst Signale als /deferred/ vormerken.
#+end_solution
** Exceptions/System-Calls
- Exceptions ::
- gehen zunächst an den Host statt an Guest
- normalerweiße sendet OS Exceptions an Applikation
- System-Calls ::
- gehen an Host
- werden nicht wie Exceptions weitergeleitet
#+begin_solution
Umleitung von System-Calls via =ptrace= (möglich, aber langsam)
#+end_solution
** Zeit
#+begin_problem
Zeitmessung in paravirtualiserten Systemen sind ungenau:
- =setitimer= kann maximal 100hz, also ist eine virtuelle System-Clock ungenau
- Guest kann ge-scheduled werden
#+end_problem
** Beispiel User-Mode Linux
#+attr_html: :border 2 :rules all :frame border
| Vorteile | Nachteile |
|--------------------------+-----------------------------------|
| integriert im Linux-Kern | nur mit bestimmter Versionsnummer |
| einfach konfigurierbar | nur Text-Modus |
| relativ effizient | nur eingeschränkt konfigurierbar |
** Hypervisor
- Hypervisor :: abgespecktes Host-OS
- Domain :: modifiziertes Guest-OS mit Guestapplikationen
Im Allgemeinen muss der Hypervisor nur wenige Calls unterstützen:
- Ersatz für priviligierte Instruktionen
- Page-Tabelle verwalten
- Segment-Tabelle verwalten
- Exception/Interrupt/System-Call-Verwaltung
- Ein/Ausgabe
- VMs verwalten
Der Hypervisor kann also sehr kompakt implementiert werden (~ wenige KiB)
#+begin_idea
Hypervisor-Calls werden oft kurz nacheinander aufgerufen, also mehrere Calls zusammenfassen (*multicall*)
#+end_idea
** Speicher
#+begin_problem
VMs sollen soviel Speicher reservieren, wie sie gerade wirklich benötigen (*balloon memory*).
Der Speicher wird dadurch fragmentiert.
Betriebssysteme gehen i.d.R. von kontinuierlichem Speicher aus
#+end_problem
#+begin_solution
Umrechnungstabellen, eingetragen wird beim Eintragen der Pagenummer in die Pagetabelle.
#+end_solution
** MMU
#+begin_problem
Hypervisor muss Richtigkeit der Pagetabellen überprüfen. Sonst u.U. Zugriff von einer VM auf Speicher einer Anderen möglich.
#+end_problem
#+begin_solution
- Ändern der Pagetabellen über Hypervisor-Calls
- Überprüfen der Tabelle bevor das Pagetabellenbasisregister (=%cr3=) gesetzt wird.
Pagetabelle ab da read-only. Beim Schreiben Unterbaum abschneiden und Pagetabelle wieder read-write.
- Guest ändert nur seine Tabellen. Host passt an bei
- Page-Fault :: Host trägt neue Page ein
- Hypervisor-Call :: Host trägt Page aus
#+end_solution
** IO
#+begin_solution
Möglichkeiten:
- Hypervisor-System-Calls ::
- Guest unabhängig von Hardware
- Geräte können geteilt werden
- Hypervisor u.U. sehr groß
- Performance nicht optimal
- Hypervisor gibt VMs Kontrolle ::
- Optimale Performance
- Hypervisor klein und portabel
- Treiber in Guest eh schon vorhanden
- VMs können sich gegenseitig stören
- Geräte können nicht geteilt werden
- Hypervisor gibt /einer/ VM (Domain 0) Kontrolle ::
- Hypervisor klein und portabel
- Treiber in Guest eh schon vorhanden
- Geräte können geteilt werden
- Performance nicht optimal
#+end_solution
*** Domain {0,N}
- Domain 0 :: Domain mit IO-Permissions
- muss zuerst gestartet werden
- kann weitere Domains starten
- Domain N :: alle anderen Domains
#+begin_problem
Datentransfer muss /sehr schnell/ sein.
#+end_problem
#+begin_solution
Daten werden nicht kopiert:
- Schreiben ::
1. Domain N schreibt Daten in Buffer
2. Physikalische Adresse des Buffers wird mit Schreibauftrag über Hypervisor an Domain 0 gereicht
3. Domain 0 nutzt phys. Adress zum Schreiben per {{{abbr(DMA, Direct Memory Access)}}}
4. Domain 0 schickt Meldung zurück
- Lesen ::
1. Domain N schickt phys. Adress eines Buffers mit Leseauftrag über Hypervisor an Domain 0
2. Domain 0 liest per DMA in den Buffer und schickt Meldung zurück
3. Domain N kann Daten lesen
#+end_solution
* Betriebssystem-basierte Virtualisierung
mehrere VMs mit gleichem Betriebssystem \Rightarrow unnötiger Overhead
aber pro VM:
- eigene Dateien :: \Rightarrow =chroot=
- gesharte Systemdateien :: \Rightarrow =read-only= mounten mit =copy-on-write=
- IPC :: eigene IPC-Objekte
- Prozesse :: eigene Prozess-Liste, =init=
- User :: eigene User
- IP :: eigene IP-{Adresse, Ports}
außerdem
- VMs müssen gestartet / gestoppt werden können
#+attr_html: :border 2 :rules all :frame border
| Vorteile | Nachteile |
|-------------------+---------------------------------|
| geringer Overhead | nur gleiches OS |
| Resource-sharing | Resourcenbeschränkung schwierig |
| Plattenspeicher | Updates |
| Speicher | Abschottung u.U unsicher |
* Bibliotheksbasierte Virtualisierung
Um Applikation auszuführen muss nur {{{abbr(ABI, Application Binary Interface)}}}
- CPU-Instruktionen :: müssen passen
- Memory-Layout :: Speicher muss richtig gemappt werden können
(z.B in Windows ist <1MB und >2GiB nicht erlaubt, Linux >3GiB)
- Betriebssystem-Interface :: System-Calls umbiegen (entfällt wenn =libc=)
#+begin_solution
Entweder
- Wrapper um Programm, der lädt, linkt und startet (=wine=)
- Betriebssytem stellt fremde System-Calls bereit (in FreeBSD: Personalities, Execution Domain)
#+end_solution
** Wine
Tja, ich wüsste auch gern mehr.