Virtuelle Maschinen
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
:
- Value mit PortNr. an Bus geben
- Bus callt alle mit Callback registrierte Komponenten. Jede prüft ob sie angesprochen ist.
- Value mit PortNr. an Bus geben
Speicherhierarchie
Bei einem Speicherzugriff müssen viele einzelne Schritte durchlaufen werden:
- Segmentierungseinheit
- ist Segment ein NULL-Segment? ⇒
Protection Fault
- ist Segment nicht schreibbar ⇒
Protection Fault
- ist Adresse plus Operandengröße innerhalb/außerhalb des
%es
-Segments? - lineare Adresse berechnen (virtuelle Adresse plus Segmentbasisadresse)
- ist Segment ein NULL-Segment? ⇒
- Alignment-Check
- ist der Operand nicht korrekt aligned? ⇒
Alignment Fault
- Debug-Einheit
ist Adresse ein Watchpoint? wenn ja und
- Operandengröße entspricht Watchpointgröße
- Schreib/Lese-Richtung stimmt
⇒
Debug Trap
- Operandengröße entspricht Watchpointgröße
- MMU
- TLB
- physische Adresse berechnen
- Zugriffskontrolle
- Access/Dirty-Bits setzen
- TLB
- Caches
- Caches weglassen
- bringen eh keinen Mehrwert
- bringen eh keinen Mehrwert
- JIT
- Segmentierungscheck nur dazu kompilieren wenn Segmentlimit wirklich limitiert (≠ 4GiB) (ist er z.B in MSDOS)
- Segmentbasiadresse nur dazu kompilieren wenn ≠ 0
- Alignmentcheck nur dazu kompilieren wenn eingeschaltet (meistens der Fall)
- selbiges für Watchpoints usw.
- Segmentierungscheck nur dazu kompilieren wenn Segmentlimit wirklich limitiert (≠ 4GiB) (ist er z.B in MSDOS)
- MMU
- in TLB direkt Pointer zu emulierten Speicherseiten eintragen
- 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):
/* ring seg offset] */ static uint8_t *mem[4] [6] [1 << 32];
daher
#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; } }
Da bis auf Seitengrenzen
get_ptr(ring, seg, offset + 1) \eq get_ptr(ring, seg, offset) + 1
gilt:
#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; } }
- über Parameter des Speicherzugriffs indiziert wird (
JIT
Emulation ist in der Regel zu langsam:
z.B. ein Assemblerbefehl wie
add eax [esp + 0x4]
- Liegt Strom an?
- Hat der letzte Befehl eine Exception ausgelöst?
- Liegt ein System-Management-Interrupt vor?
- Liegt ein Interrupt vor?
- 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
- liegen immer die gleichen Typen von Argumenten auf dem Stack
Für Hardware aber ungeignet:
- Garbage Collection
- Code-Prüfung
Exceptions
Exceptions lassen keine linearen Blöcke zu
Rückkehr in die Hauptschleife
Dann greifen aber einige Optimierungen nicht mehr:
- IP muss u.U. auch im Block gesetzt sein
- Flags müssen u.U. berechnet werden
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
oderout
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
Möglichkeiten:
mov
auf Funktionsaufrufe abbilden (schlechte Performance bei Grafikoperationen)mov
in Grafikspeicher schreiben lassen, Bildschirm-Update per Polling- spezielle Grafiktreiber verwenden:
z.Bmov
in Buffer, dann Grafikkarter Pointer übergeben (nur ein exit-Event)
Interrupts
cli
, popf
sehr häufig, also viel Umschaltzeit.
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.
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
Für %cr3
in Hardware unkritische Werte bereitstellen, die nicht Exception generieren.
Sonst wird Exception generiert (langsam).
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
- physischer Speicher
- eine Datei
- virtueller Speicher
- eine gemappte Datei
- Interrupts
- Signale
- Real-Time-Clock/Timer
- System-Clock, Timer
- Konsole
- Terminal oder GUI
- Netzwerkkarte
- Socket
MMU
MMU emulieren mittels mmap
und munmap
.
- 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
Interrupts
Statt Interrupts Signale verwenden.
- Programmieren der Signale
sigaction
- Setzen des Signal-/Supervisor-Stacks
sigstack
- Enable/Disable der Signale
sicprocmask
- Interrupts werden sehr häufig ein-/ausgeschaltet
sicprocmask
ist langsam
Signale immer zulassen. Der Signal-Handler wird aber nur aufgerufen, wenn virtuelle Interrupts eingeschaltet sind.
Sonst Signale als deferred vormerken.
Exceptions/System-Calls
- Exceptions
- gehen zunächst an den Host statt an Guest
- normalerweiße sendet OS Exceptions an Applikation
- gehen zunächst an den Host statt an Guest
- System-Calls
- gehen an Host
- werden nicht wie Exceptions weitergeleitet
- gehen an Host
Umleitung von System-Calls via ptrace
(möglich, aber langsam)
Zeit
Zeitmessung in paravirtualiserten Systemen sind ungenau:
setitimer
kann maximal 100hz, also ist eine virtuelle System-Clock ungenau- Guest kann ge-scheduled werden
Beispiel User-Mode Linux
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
- Page-Tabelle verwalten
- VMs verwalten
Der Hypervisor kann also sehr kompakt implementiert werden (~ wenige KiB)
Hypervisor-Calls werden oft kurz nacheinander aufgerufen, also mehrere Calls zusammenfassen (multicall)
Speicher
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
Umrechnungstabellen, eingetragen wird beim Eintragen der Pagenummer in die Pagetabelle.
MMU
Hypervisor muss Richtigkeit der Pagetabellen überprüfen. Sonst u.U. Zugriff von einer VM auf Speicher einer Anderen möglich.
- Ä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
IO
Möglichkeiten:
- Hypervisor-System-Calls
- Guest unabhängig von Hardware
- Geräte können geteilt werden
- Hypervisor u.U. sehr groß
- Performance nicht optimal
- Guest unabhängig von Hardware
- 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
- Optimale Performance
- 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
- Hypervisor klein und portabel
Domain {0,N}
- Domain 0
- Domain mit IO-Permissions
- muss zuerst gestartet werden
- kann weitere Domains starten
- muss zuerst gestartet werden
- Domain N
- alle anderen Domains
Datentransfer muss sehr schnell sein.
Daten werden nicht kopiert:
- Schreiben
- Domain N schreibt Daten in Buffer
- Physikalische Adresse des Buffers wird mit Schreibauftrag über Hypervisor an Domain 0 gereicht
- Domain 0 nutzt phys. Adress zum Schreiben per DMA
- Domain 0 schickt Meldung zurück
- Domain N schreibt Daten in Buffer
- Lesen
- Domain N schickt phys. Adress eines Buffers mit Leseauftrag über Hypervisor an Domain 0
- Domain 0 liest per DMA in den Buffer und schickt Meldung zurück
- Domain N kann Daten lesen
- Domain N schickt phys. Adress eines Buffers mit Leseauftrag über Hypervisor an Domain 0
Betriebssystem-basierte Virtualisierung
mehrere VMs mit gleichem Betriebssystem ⇒ unnötiger Overhead
aber pro VM:
- eigene Dateien
- ⇒
chroot
- gesharte Systemdateien
- ⇒
read-only
mounten mitcopy-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
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 ABI
- 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
)
Entweder
- Wrapper um Programm, der lädt, linkt und startet (
wine
) - Betriebssytem stellt fremde System-Calls bereit (in FreeBSD: Personalities, Execution Domain)
- Wrapper um Programm, der lädt, linkt und startet (
Wine
Tja, ich wüsste auch gern mehr.