Zadanie 4: Maszyna wirtualna

Data ogłoszenia: 08.06.2021

Termin oddania: 07.09.2021 (brak możliwości spóźnienia)

Materiały dodatkowe

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_IDXBLOCK_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 <bios.bin> [<drive.img>]

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