============================ Zadanie 4: Maszyna wirtualna ============================ Data ogłoszenia: 08.06.2021 Termin oddania: 07.09.2021 (brak możliwości spóźnienia) Materiały dodatkowe =================== - przykładowe użycie KVM: :download:`exvm.c` - nagłówek przydatny do napisania rozwiązania: https://github.com/mwkmwkmwk/alienvm-tests/blob/main/alienvm.h - testy do zadania: https://github.com/mwkmwkmwk/alienvm-tests/ Wprowadzenie ============ W ostatnim czasie w pobliżu wydziału rozbił się statek kosmitów. Udało się nam wydobyć z niego nośniki danych zawierające programy na bardzo zaawansowane technicznie komputery kosmitów, lecz niestety nie przetrwał żaden działający komputer – nie możemy więc uruchomić ich programów. Jednak po wielu miesiącach inżynierii wstecznej w końcu udało nam się ustalić, jak działają interfejsy sprzętowe komputerów kosmitów. O komputerach kosmitów ====================== Wygląda na to, że kosmici są bardzo zaawansowani — używają 64-bitowych procesorów x86 i części standardowych peryferiów. Komputer kosmitów składa się z: - 1 (jednego) procesora x86 - procesor ma wbudowany lokalny APIC (o identyfikatorze 0, pod adresem ``0xfee00000``) - 1 (jednego) standardowego IO APIC (pod adresem ``0xfec00000``), wraz ze starym PIC dla kompatybilności wstecznej - standardowego układu 8254 PIT (podłączonego w standardowy sposób do PIC + IO APIC) - 16MB RAMu, zajmującego ciągły obszar zaczynający się od adresu 0 pamięci fizycznej - 64kiB ROMu zawierającego BIOS, pod adresem ``0xffff0000`` (wszystkie zapisy do tego obszaru są ignorowane) - 1 (jednego) dwukierunkowego portu szeregowego o wysokiej wydajności - 1 (jednego) urządzenia blokowego o wysokiej wydajności - 1 (jednego) jednokierunkowego portu debugowania - 1 (jednego) układu zarządzania maszyną Port szeregowy — wysył ---------------------- Część wysyłająca portu szeregowego działa na zasadzie DMA i używa bufora pierścieniowego. Wysył portu szeregowego używa też kilku rejestrów sterujących zmapowanych do pamięci oraz linii przerwania numer 3. MMIO ``0xe0000000``: ``SERIAL_OUT_DESC_PTR`` Rejestr 32-bitowy. Wskaźnik (fizyczny) na stronę pamięci zawierający dane kontrolne wysyłu portu szeregowego. Musi być wielokrotnością rozmiaru strony (4kiB). MMIO ``0xe0000004``: ``SERIAL_OUT_SETUP`` Rejestr 32-bitowy. Zapis do tego rejestru resetuje urządzenie i konfiguruje jego pracę. - bit 0: ``ENABLE`` — jeśli ustawione, część wysyłająca powinna po resecie rozpocząć pracę; jeśli wyzerowane, urządzenie nic nie robi. - bity 8-15: ``NPAGES_M1`` — rozmiar bufora urządzenia w stronach, pomniejszony o 1 (czyli wartość 3 w tym polu oznacza 4 strony) MMIO ``0xe0000008``: ``SERIAL_OUT_NOTIFY`` Rejestr 32-bitowy. Zapis do tego rejestru powiadamia urządzenie o tym, że system operacyjny wysłał nowe dane w buforze. Sama wartość zapisana tutaj jest ignorowana. Strona pamięci wskazywana przez ``SERIAL_OUT_DESC_PTR`` ma następujący format (wszystko to 32-bitowe słowa little-endian): - offset ``0x000 + i * 4, i <= NPAGES_M1``: ``BUFFER_PTR``, tablica wskaźników (fizycznych, 32-bitowych) na kolejne strony bufora danych urządzenia. Każdy wpis powinien być wielokrotnością 4kiB. Tablica ta powinna zostać wypełniona przez system operacyjny przed startem urządzenia, a potem nie powinna być zmieniana dopóki urządzenie jest aktywne. - offset ``0x800``: ``PUT`` — indeks bajtu w buforze, pod którym system operacyjny zapisałby kolejny bajt danych do wysyłu (czyli indeks pierwszego bajtu, który jeszcze nie został wypełniony i wysłany przez system). Powinien być zainicjowany przez system operacyjny (prawdopodobnie na 0) przed startem urządzenia, a potem może zostać zmieniony przez system operacyjny w dowolnym momencie. - offset ``0xc00``: ``GET`` — indeks bajtu w buforze, od którego urządzenie powinno wysyłać kolejne bajty. Powinien być zainicjowany przez system operacyjny (prawdopodobnie na 0) przed startem urządzenia, a potem może być modyfikowany tylko przez urządzenie. Urządzenie powinno działać następująco:: while (1) { if (desc.GET >= (NPAGES_M1 + 1) * 0x1000) error(); if (desc.PUT >= (NPAGES_M1 + 1) * 0x1000) error(); if (desc.GET == desc.PUT) czekaj na zapis do SERIAL_OUT_NOTIFY; else { // znajdź adres fizyczny bajtu do wysłania ptr = desc.BUFFER_PTR[desc.GET >> 12] + (desc.GET & 0xfff); byte = *(uint8_t*)ptr; send(byte); // zwiększ desc.GET o 1, zawijając modulo rozmiar bufora if (desc.GET == (NPAGES_M1 + 1) * 0x1000 - 1) desc.GET = 0; else desc.GET++; // wyzwól linię przerwania #3 (linia #3 w PIC oraz IO-APIC) trigger_edge_irq(3); } } Urządzenie może jednak zastosować pewne optymalizacje: - urządzenie może zakładać, że system operacyjny nie pisze do ``desc.GET``, gdy ono jest aktywne (czyli urządzeniu wolno mieć własną kopię tego indeksu w lokalnym rejestrze, pobraną na samym początku działania, a potem tylko kopiować wartość tego rejestru do pamięci) - urządzenie może wysłać wiele bajtów naraz, wykonując tylko raz zapis do ``desc.GET`` w pamięci i wyzwolenie przerwania - urządzenie może również zakładać, że system operacyjny zapisze rejestr MMIO ``SERIAL_OUT_NOTIFY`` zawsze po aktualizacji ``desc.PUT`` — może więc przechowywać lokalną kopię tego słowa w lokalnym rejestrze i aktualizować ją tylko w momencie zapisu tego rejestu MMIO. - urządzenie może zakładać, że ``BUFFER_PTR`` nie zostaną zmienione w trakcie pracy (może więc przechowywać lokalne kopie) Urządzenie musi jednak przestrzegać zasad: - urządzenie może przeczytać dane z bufora dopiero po tym, jak przeczyta wartość z ``desc.PUT``, która na to pozwala (nie wolno wczytywać danych na zapas) - urządzenie może zapisać ``desc.GET`` dopiero po wczytaniu odpowiednich danych z bufora - urządzenie może wyzwolić przerwanie dopiero po zapisie ``desc.GET`` Port szeregowy — odbiór ----------------------- Część odbierająca portu szeregowego działa na zasadzie DMA i używa bufora pierścieniowego. Odbiór portu szeregowego używa też kilku rejestrów sterujących zmapowanych do pamięci oraz linii przerwania numer 4. MMIO ``0xe0001000``: ``SERIAL_IN_DESC_PTR`` Rejestr 32-bitowy. Wskaźnik (fizyczny) na stronę pamięci zawierający dane kontrolne odbioru portu szeregowego. Musi być wielokrotnością rozmiaru strony (4kiB). MMIO ``0xe0001004``: ``SERIAL_IN_SETUP`` Rejestr 32-bitowy. Zapis do tego rejestru resetuje urządzenie i konfiguruje jego pracę. - bit 0: ``ENABLE`` — jeśli ustawione, część odbierająca powinna po resecie rozpocząć pracę; jeśli wyzerowane, urządzenie nic nie robi. - bity 8-15: ``NPAGES_M1`` — rozmiar bufora urządzenia w stronach, pomniejszony o 1 (czyli wartość 3 w tym polu oznacza 4 strony) MMIO ``0xe0001008``: ``SERIAL_IN_NOTIFY`` Rejestr 32-bitowy. Zapis do tego rejestru powiadamia urządzenie o tym, że system operacyjny odebrał dane z bufora. Sama wartość zapisana tutaj jest ignorowana. Strona pamięci wskazywana przez ``SERIAL_IN_DESC_PTR`` ma następujący format (wszystko to 32-bitowe słowa little-endian): - offset ``0x000 + i * 4, i <= NPAGES_M1``: ``BUFFER_PTR``, tablica wskaźników (fizycznych, 32-bitowych) na kolejne strony bufora danych urządzenia. Każdy wpis powinien być wielokrotnością 4kiB. Tablica ta powinna zostać wypełniona przez system operacyjny przed startem urządzenia, a potem nie powinna być zmieniana dopóki urządzenie jest aktywne. - offset ``0x800``: ``GET`` — indeks bajtu w buforze, od którego system operacyjny będzie czytał kolejny bajt. Urządzenie nie powinno pisać kolejnego bajtu do bufora (powinno zawiesić odbiór), gdyby zapis spowodował ``GET == PUT`` (przepełnienie bufora). Powinien być zainicjowany przez system operacyjny (prawdopodobnie na 0) przed startem urządzenia, a potem może zostać zmieniony przez system operacyjny w dowolnym momencie. - offset ``0xc00``: ``PUT`` — indeks bajtu w buforze, pod którym urządzenie ma zapisać kolejny odebrany bajt danych. Powinien być zainicjowany przez system operacyjny (prawdopodobnie na 0) przed startem urządzenia, a potem może być modyfikowany tylko przez urządzenie. Urządzenie powinno działać następująco:: while (1) { if (desc.GET >= (NPAGES_M1 + 1) * 0x1000) error(); if (desc.PUT >= (NPAGES_M1 + 1) * 0x1000) error(); next_put = desc.PUT + 1; if (next_put == (NPAGES_M1 + 1) * 0x1000) next_put = 0; if (desc.GET == next_put) czekaj na zapis do SERIAL_IN_NOTIFY; else { // znajdź adres fizyczny miejsca do którego zapisujemy odebrany bajt ptr = desc.BUFFER_PTR[desc.GET >> 12] + (desc.GET & 0xfff); byte = recv(); *(uint8_t*)ptr = byte; // zwiększ desc.GET o 1, zawijając modulo rozmiar bufora desc.PUT = next_put; // wyzwól linię przerwania #4 (linia #4 w PIC oraz IO-APIC) trigger_edge_irq(4); } } Urządzenie może jednak zastosować pewne optymalizacje: - urządzenie może zakładać, że system operacyjny nie pisze do ``desc.PUT``, gdy ono jest aktywne (czyli urządzeniu wolno mieć własną kopię tego indeksu w lokalnym rejestrze, pobraną na samym początku działania, a potem tylko kopiować wartość tego rejestru do pamięci) - urządzenie może odebrać wiele bajtów naraz, wykonując tylko raz zapis do ``desc.PUT`` w pamięci i wyzwolenie przerwania - urządzenie może również zakładać, że system operacyjny zapisze rejestr MMIO ``SERIAL_IN_NOTIFY`` zawsze po aktualizacji ``desc.GET`` — może więc przechowywać lokalną kopię tego słowa w lokalnym rejestrze i aktualizować ją tylko w momencie zapisu tego rejestu MMIO. - urządzenie może zakładać, że ``BUFFER_PTR`` nie zostaną zmienione w trakcie pracy (może więc przechowywać lokalne kopie) Urządzenie musi jednak przestrzegać zasad: - urządzenie może zapisać dane do bufora dopiero po tym, jak przeczyta wartość z ``desc.GET``, która na to pozwala - urządzenie może zapisać ``desc.PUT`` dopiero po zapisaniu odpowiednich danych do bufora - urządzenie może wyzwolić przerwanie dopiero po zapisie ``desc.PUT`` Urządzenie blokowe ------------------ Urządzenie blokowe działa na zasadzie DMA i używa bufora pierścieniowego. Używa też kilku rejestrów sterujący zmapowanych do pamięci oraz linii przerwania 5. MMIO ``0xe0002000``: ``BLOCK_DESC_PTR`` Rejestr 32-bitowy. Wskaźnik (fizyczny) na stronę pamięci zawierający dane kontrolne urządzenia blokowego. Musi być wielokrotnością rozmiaru strony (4kiB). MMIO ``0xe0002004``: ``BLOCK_SETUP`` Rejestr 32-bitowy. Zapis do tego rejestru resetuje urządzenie i konfiguruje jego pracę. - bit 0: ``ENABLE`` — jeśli ustawione, urządzenie blokowe powinno po resecie rozpocząć pracę; jeśli wyzerowane, urządzenie nic nie robi. - bity 8-14: ``NREQUESTS_M1`` — rozmiar kolejki żądań urządzenia pomniejszony o 1 (czyli wartość 3 w tym polu oznacza 4 żądania) MMIO ``0xe0002008``: ``BLOCK_NOTIFY`` Rejestr 32-bitowy. Zapis do tego rejestru powiadamia urządzenie o tym, że system operacyjny wysłał nowe żądania. Sama wartość zapisana tutaj jest ignorowana. MMIO ``0xe000200c``: ``BLOCK_CAPACITY`` Rejestr 32-bitowy, tylko do odczytu. Podaje rozmiar urządzenia blokowego w 4096-bajtowych blokach. Obsługa urządzenia blokowego odbywa się przez wysyłanie mu żądań w kolejce. Urządzenie odpowiada na każde z żądań przez odesłanie statusu żądania. Strona pamięci wskazywana przez ``BLOCK_DESC_PTR`` ma następujący format (wszystko to 32-bitowe słowa little-endian): - pierwsza połowa strony zawiera kolejkę żądań: - offset ``0x000 + i * 0x10, i <= NREQUESTS_M1``: ``REQ[i].BUFFER_PTR``, wskaźnik (fizyczny) na bufor danych dla danego żądania. Powinien być wielokrotnością 4kiB. Bufor ma zawsze dokładnie 4kiB danych. - offset ``0x004 + i * 0x10, i <= NREQUESTS_M1``: ``REQ[i].BLOCK_IDX``, indeks bloku dla danego żądania (liczony od 0). - offset ``0x008 + i * 0x10, i <= NREQUESTS_M1``: ``REQ[i].TYPE``, typ danego żądania: - ``0``: ``READ`` — żądanie odczytu bloku o indeksie ``BLOCK_IDX`` z urządzenia i zapisanie danych do bufora pod adresem ``BUFFER_PTR`` - ``1``: ``WRITE`` — żądanie zapisu bloku o indeksie ``BLOCK_IDX`` w urządzeniu danymi przeczytanymi z bufora pod adresem ``BUFFER_PTR`` - offset ``0x00c + i * 0x10, i <= NREQUESTS_M1``: ``REQ[i].STATUS``, wynik danego żądania (wypełniany przez urządzenie po zakończeniu obsługi żądania): - ``0``: ``SUCCESS`` — blok został zapisany bądź odczytany bez napotkania problemu - ``1``: ``INVALID_IDX`` — ``BLOCK_IDX`` jest większy bądź równy ``BLOCK_CAPACITY`` - ``2``: ``IO_ERROR`` — urządzenie blokowe napotkało błąd wejścia/wyjścia - offset ``0x800``: ``PUT`` — indeks kolejnego żądania w kolejce, które zostanie dopiero wysłane przez system operacyjny (czyli indeks pierwszego żądania, które jeszcze nie zostało wypełnione i wysłane przez system). Powinien być zainicjowany przez system operacyjny (prawdopodobnie na 0) przed startem urządzenia, a potem może zostać zmieniony przez system operacyjny w dowolnym momencie. - offset ``0xc00``: ``GET`` — indeks pierwszego żądania w kolejce, które nie zostało jeszcze przetworzone przez urządzenie. Powinien być zainicjowany przez system operacyjny (prawdopodobnie na 0) przed startem urządzenia, a potem może być modyfikowany tylko przez urządzenie. Urządzenie powinno działać następująco:: while (1) { if (desc.GET > NREQUESTS_M1) error(); if (desc.PUT > NREQUESTS_M1) error(); if (desc.GET == desc.PUT) czekaj na zapis do BLOCK_NOTIFY; else { int i = desc.GET; if (desc.REQ[i].TYPE == 0) { // odczyt REQ[i].STATUS = block_device_read(.offset = REQ[i].BLOCK_IDX * 4096, .size = 4096, .data_ptr = REQ[i].BUFFER_PTR); } else if (desc.REQ[i].TYPE == 1) { REQ[i].STATUS = block_device_write(.offset = REQ[i].BLOCK_IDX * 4096, .size = 4096, .data_ptr = REQ[i].BUFFER_PTR); } // zwiększ desc.GET o 1, zawijając modulo rozmiar bufora if (desc.GET == NREQUESTS_M1) desc.GET = 0; else desc.GET++; // wyzwól linię przerwania #5 (linia #5 w PIC oraz IO-APIC) trigger_edge_irq(5); } } Urządzenie może jednak zastosować pewne optymalizacje: - urządzenie może zakładać, że system operacyjny nie pisze do ``desc.GET``, gdy ono jest aktywne (czyli urządzeniu wolno mieć własną kopię tego indeksu w lokalnym rejestrze, pobraną na samym początku działania, a potem tylko kopiować wartość tego rejestru do pamięci) - urządzenie może przetworzyć wiele żądań naraz, wykonując tylko raz zapis do ``desc.GET`` w pamięci i wyzwolenie przerwania - urządzenie może również zakładać, że system operacyjny zapisze rejestr MMIO ``SERIAL_OUT_NOTIFY`` zawsze po aktualizacji ``desc.PUT`` — może więc przechowywać lokalną kopię tego słowa w lokalnym rejestrze i aktualizować ją tylko w momencie zapisu tego rejestu MMIO. Urządzenie musi jednak przestrzegać zasad: - urządzenie może przeczytać parametry żądania z bufora (i dane do zapisu) dopiero po tym, jak przeczyta wartość z ``desc.PUT``, która na to pozwala (nie wolno wczytywać żądań na zapas) - urządzenie może zapisać ``desc.GET`` dopiero po zapisaniu pola ``REQ[i].STATUS`` i (w przypadku odczytu) zapisaniu danych do bufora - urządzenie może wyzwolić przerwanie dopiero po zapisie ``desc.GET`` Port debugowania ---------------- Port debugowania składa się z jednego portu wejścia/wyjścia: Port ``0x800``: ``DEBUG_OUT`` Port obsługuje tylko 8-bitowe zapisy. Zapis dowolnego bajtu powoduje wysłanie go przez port debugowania maszyny. W naszym zadaniu, każdy zapisany tutaj bajt ma zostać natychmiast przekazany na ``stderr`` hypervisora. Układ zarządzania maszyną ------------------------- Układ zarządzania maszyną składa się z jednego portu wejścia/wyjścia: Port ``0x900``: ``SHUTDOWN`` Port obsługuje tylko 8-bitowe zapisy. Zapis dowolnego bajtu natychmiast wyłącza maszynę. Wartość zapisanego bajtu jest kodem diagnostycznym wyłączenia maszyny (w naszym zadaniu ma zostać przekazana dalej jako kod wyjścia z procesu hypervisora). Zadanie ======= Napisać hypervisor, który będzie wirtualizował komputery kosmitów. Hypervisor powinien używać interfejsu KVM do wirtualizacji procesora, kontrolerów przerwań i timerów, a resztę urządzeń peryferyjnych symulować w przestrzeni użytkownika. Wszystkie urządzenia obsługują DMA tylko z/do pamięci RAM (w przypadku próby użycia adresu z poza zakresu przeznaczonego na RAM, hypervisor powinien zakończyć wykonanie z błędem). Zasady oceniania ================ Za zadanie można uzyskać do 10 punktów. Na ocenę zadania składają się dwie części: - wynik testów (od 0 do 10 punktów) - ocena kodu rozwiązania (od 0 do -10 punktów) Punktacja --------- Punkty ujemne za kod można było dostać za: 1. Brak poprawnej obsługi błędów (-0.2) 2. Brak weryfikacji poprawnego zakresu wskaźników (-0.5) 3. Brak weryfikacji wyrównania adresu w obsłudze urządzenia blokowego (-0.2) 4. Niepoprawna obsługa wielokrotnego setup (brak możliwości wyłączenia urządzenia, wielokrotne uruchamianie wątków) (-0.3) 5. Brak użycia volatile przy dostępie do danych modyfikowanych w innym wątku (-0.5) Forma rozwiązania ================= Jako rozwiązanie należy wysłać paczkę zawierającą: - dowolną liczbę plików źródłowych z kodem rozwiązania - jeśli rozwiązanie jest napisane w języku kompilowanym, plik Makefile kompilujący rozwiązanie, lub odpowiadający plik z innego sensownego systemu budowania (np. cmake) - plik readme z krótkim opisem rozwiązania i instrukcjami kompilacji Rozwiązanie (po ew. kompilacji) powinno znajdować się w pliku wykonywalnym o nazwie ``avm``. Program powinien mieć następujący interfejs:: ./avm [] Wywołanie programu powinno uruchomić hypervisor. Program powinien zakończyć pracę, gdy maszyna wirtualna użyje portu ``SHUTDOWN``, bądź gdy wirtualizacja napotka błąd (np. brak dostępu do ``/dev/kvm``, błąd wewnętrzny KVM, nieznany adres MMIO, ...). W przypadku napotkania błędu, program powinien wypisać opis problemu na stderr (nie precyzujemy dokładnego formatu, ale powinien wskazywać na ogólne źródło błędu) i zakończyć się z kodem wykonania 127. Poza tym przypadkiem, program nie powinien wypisywać nic na stderr poza emulacją portu ``DEBUG_OUT``. Pierwszym (obowiązkowym) argumentem programu jest nazwa pliku zawierającego obraz BIOSu. Ten plik musi mieć długość dokładnie 65536 bajtów (program powinien się zakończyć błędem w przeciwnym wypadku). Drugim (opcjonalnym) argumentem programu jest nazwa pliku zawierającego obraz urządzenia blokowego. Ten plik musi mieć długość będącą wielokrotnością 4096 bajtóœ (program powinien zakończyć się błędem w przeciwnym wypadku). Program nie powinien pozwalać na zapis poza początkową długość obrazu. W przypadku braku tego argumentu, należy emulować urządzenie blokowe o długości 0. Port szeregowy maszyny wirtualnej powinien zostać podłączony do ``stdin`` i ``stdout`` programu. Rozwiązania prosimy nadsyłać na adres ``p.zuk@mimuw.edu.pl`` z kopią do ``mwk@mimuw.edu.pl``. Wskazówki ========= Rozwiązanie prawdopodobnie będzie wymagało utworzenia kilku wątków (1 na VCPU + co najmniej 1 na urządzenia wejścia/wyjścia). Szkic rozwiązania: 1. Otworzyć ``/dev/kvm``. 2. Sprawdzić zgodność numeru wersji (``KVM_GET_API_VERSION``). 3. Poznać rozmiar mapowania do vcpu (``KVM_GET_VCPU_MMAP_SIZE``). 4. Stworzyć maszynę wirtualną (``KVM_CREATE_VM``). 5. Wykonać ``KVM_SET_TSS_ADDR`` (proponuję parametr ``0xfffe8000``) i ``KVM_SET_IDENTITY_MAP_ADDR`` (proponuję parametr ``0xfffec000``), na wypadek uruchomienia hypervisora na starych procesorach Intela. 6. Stworzyć PIC+APIC (``KVM_CREATE_IRQCHIP``) oraz PIT (``KVM_CREATE_PIT2``). 7. Stworzyć anonimowe mapowanie na RAM i wpiąć je w VM (``KVM_SET_USER_MEMORY_REGION``). 8. Stworzyć mapowanie BIOSu i wpiąć je w VM (j/w). 9. Zainicjować wątki obsługjące urządzenia, ustanowić kanały komunikacji z nimi. 10. Stworzyć procesor (``KVM_CREATE_VCPU``). 11. Ustawić identyfikację procesora (najprościej skopiować dane z ``KVM_GET_SUPPORTED_CPUID`` prosto do ``KVM_GET_SUPPORTED_CPUID``) — **bez tego APIC nie będzie w pełni funkcjonalny**. 12. Zmapować blok kontrolny vcpu. 13. Rozpocząć wykonywanie vcpu przez uruchomienie ``KVM_RUN`` w pętli (nie jest wymagana inicjalizacja stanu procesora — KVM domyślnie resetuje nowo utworzony procesor). - Obsłużyć ``KVM_EXIT_IO`` przez emulację odpowiednich urządzeń (w razie nieznanego adresu należy zakończyć wykonanie hypervisora błędem) - Obsłużyć ``KVM_EXIT_MMIO`` przez emulację odpowiednich urządzeń (w razie nieznanego adresu należy zakończyć wykonanie hypervisora błędem) - Obsługa pozostałych wyjść z KVM nie jest wymagana (o ile używamy in-kernel irqchip) — w razie wystąpienia nieznanego wyjścia, należy zakończyć wykonanie hypervisora błędem. Zgłaszanie przerwania z wątku (lub wątków) urządzenia można robić albo za pomocą ``KVM_IRQ_LINE``, albo za pomocą eventfd w połączeniu z ``KVM_IRQFD``. Obsługę rejestrów ``NOTIFY`` można opcjonalnie usprawnić przez użycie ``KVM_IOEVENTFD``. W wątku (lub wątkach) urządzeń może się przydać ``poll`` lub podobny syscall, gdyż działające urządzenie musi jednocześnie oczekiwać na gotowość wejścia/wyjścia (choćby na stdin) oraz żądania zmiany stanu od vcpu (przez rejestr ``SETUP``). Mogą też przydać się pipe lub eventfd do komunikacji setupu między wątkiem vcpu a wątkiem urządzenia. Literatura ========== - ``Documentation/kvm/api.rst`` w źródłach kernela — dokumentacja API KVM - https://software.intel.com/content/www/us/en/develop/download/intel-64-and-ia-32-architectures-sdm-combined-volumes-1-2a-2b-2c-2d-3a-3b-3c-3d-and-4.html tom 3, rozdziały 23-33: jak działa wirtualizacja sprzętowa w procesorach Intela (dla ciekawych) - https://developer.amd.com/resources/developer-guides-manuals/ AMD64 Architecture Programmer's Manual Volume 2, rozdział 15: jak działa wirtualizacja sprzętowa w procesorach AMD (dla ciekawych)