#+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.