Zadanie 3: sterownik urządzenia Final HardDoom™

Data ogłoszenia: 04.05.2021

Termin oddania: 08.06.2021 (ostateczny 22.06.2021)

Materiały dodatkowe

Wprowadzenie

Zadanie polega na napisaniu sterownika do urządzenia Final HardDoom™, będącego akceleratorem grafiki dedykowanym dla gry Final Doom. Urządzenie dostarczane jest w postaci zmodyfikowanej wersji qemu.

Urządzenie powinno być dostępne dla użytkownika w formie urządzenia znakowego. Dla każdego urządzenia Final HardDoom™ obecnego w systemie należy utworzyć urządzenie znakowe /dev/fdoomX, gdzie X to numer kolejny urządzenia Final HardDoom™, zaczynając od 0.

Interfejs urządzenia znakowego

Urządzenie /dev/fdoom* służy do tworzenie zasobów Final HardDoom™ oraz wysyłania poleceń do urządzenia Final HardDoom™. Powinno ono obsługiwać następujące operacje:

  • open: alokuje nowy kontekst urządzenia, który będzie służył do wysyłania poleceń.

  • close: w oczywisty sposób.

  • ioctl(FDOOMDEV_IOCTL_CREATE_BUFFER): tworzy nowy bufor, który będzie widoczny dla urządzenia. Jako parametr tego wywołania przekazywany jest rozmiar bufora oraz przeskok bufora (który może być zerem, jeśli bufor nie jest płaszczyzną). Wynikiem tego wywołania jest nowy deskryptor pliku odnoszący się do utworzonego bufora. Utworzony bufor powinien być wypełniony zerami. Tak utworzony bufor będzie można później zmapować do przestrzeni programu użytkownika oraz wykorzystać przy wysyłaniu poleceń do urządzenia. Jeśli żądany rozmiar lub przeskok jest większy od maksymalnego rozmiaru wspieranego przez urządzenie (4MiB), bądź przeskok nie jest wielokrotnością 64, należy zwrócić błąd EINVAL.

  • ioctl(FDOOMDEV_IOCTL_RUN): uruchamia bądź kolejkuje uruchomienie zadania na urządzeniu. Parametrami są:

    • deskryptor pliku wskazujący na bufor zawierający polecenia

    • wskaźnik na początek bloku poleceń (wewnątrz powyższego bufora)

    • długośc bloku poleceń (w bajtach)

    • liczba dodatkowych buforów, które są używane w danym bloku poleceń (nie więcej niż 60)

    • deskryptory plików dodatkowych buforów (powinny one być widoczne pod kolejnymi numerami slotów, zaczynając od 0)

    Jeśli operacja się powiedzie, należy zwrócić 0. Jeśli któryś deskryptor pliku nie jest buforem utworzonym dla tego urządzenia Final HardDoom™, należy zwrócić bład EINVAL. Jeśli podany wskaźnik bądź rozmiar nie są wielokrotnością 4, należy zwrócić błąd EINVAL. Jeśli liczba dodatkowych buforów jest większa niż 60, należy zwrócić błąd EINVAL. Jeśli na kontekście wystąpił wcześniej błąd wykonania, można zwrócić błąd EIO.

  • ioctl(FDOOMDEV_IOCTL_WAIT): czeka na zakończenie wykonywania przesłanych wcześniej zadań. Parametr określa, na ile zadań do tyłu chcemy czekać — jeśli wartością parametru jest 13, należy czekać na zakończenie wszystkich zadań wysłanych na danym kontekście, z wyjątkiem ostatnich 13tu zadań. Aby poczekać na zakończenie wszystkich zadań wysłanych na danym kontekście, użytkownik wywołuje tą funkcję z parametrem 0. Operacja powinna zwrócić 0 w przypadku sukcesu, bądź EIO jeśli na tym kontekście nastąpił błąd wykonania.

Na buforach, poza użyciem w ioctl(FDOOMDEV_IOCTL_RUN), można wykonywać następujące operacje:

  • mmap: zmapowanie do programu użytkownika (tylko z flagą MAP_SHARED).

  • ioctl(FDOOMDEV_IOCTL_BUFFER_RESIZE): zwiększa bufor do podanego rozmiaru. Jeśli żądany rozmiar jest większy od maksymalnego rozmiaru wspieranego przez urządzenie (4MiB), należy zwrócić błąd EINVAL. Jeśli żądany rozmiar jest mniejszy od obecnego bufora, można zignorować polecenie i zwrócić 0.

Należy pamiętać, że użytkownik może wysłać niepoprawne polecenia. W przypadku, gdy wykonanie zadania spowoduje błąd (urządzenie zgłosi przerwanie *_ERROR bądź PAGE_FAULT_*), należy oznaczyć kontekst, który wysłał to zadanie jako “spalony” i zwracać błąd EIO na wszystkich wywołaniach FDOOMDEV_IOCTL_RUN i FDOOMDEV_IOCTL_WAIT na danym kontekście od tego momentu, po czym zresetować urządzenie i kontynuować przetwarzanie zadań wysłanych przez inne konteksty.

Sterownik powinien rejestrować swoje urządzenia w sysfs, aby udev automatycznie utworzył pliki urzadzeń o odpowiednich nazwach w /dev. Numery major i minor dla tych urządzeń są dowolne (majory powinny być alokowane dynamicznie).

Plik nagłówkowy z odpowiednimi definicjami można znaleźć tutaj: https://github.com/mwkmwkmwk/fharddoom/blob/main/fdoomdev.h

Sterownik może przyjąć ograniczenie do 256 urządzeń w systemie.

Założenia interakcji ze sprzętem

Można założyć, że przed załadowaniem sterownika, urządzenie ma stan jak po resecie sprzętowym. Urządzenie należy też w takim stanie zostawić przy odładowaniu sterownika.

Pełnowartościowe rozwiązanie powinno działać asynchronicznie i używać automatycznego wczytywania poleceń — operacje FDOOMDEV_IOCTL_RUN powinny dopisywać zadanie do bufora CMD_MAIN i wracać natychmiast do przestrzeni użytkownika bez oczekiwania na zakończenie zadania (ale jeśli bufor CMD_MAIN jest już pełny, dopuszczalne jest oczekiwanie na zwolnienie miejsca w nim). Oczekiwanie na zakończenie poleceń powinno być wykonane dopiero przy wywołaniu FDOOMDEV_IOCTL_WAIT.

Zasady oceniania

Za zadanie można uzyskać do 10 punktów. Na ocenę zadania składają się trzy części:

  • pełne wykorzystanie urządzenia (od 0 do 2 punktów):

    • operacja asynchroniczna — FDOOMDEV_IOCTL_RUN na urządzeniu nie czeka na zakończenie wysłanego zadania ani zadań wysłanych wcześniej (z rozsądnymi ograniczeniami), FDOOMDEV_IOCTL_WAIT czeka tylko na tyle zadań ile musi: 1p

    • użycie automatycznego wczytywania poleceń (CMD_MAIN_GET/PUT) zamiast ręcznego (CMD_MANUAL_FEED): 1p

  • wynik testów (od 0 do 8 punktów)

  • ocena kodu rozwiązania (od 0 do -10 punktów)

Punktacja

Najczęstsze błędy - spis oznaczeń w USOSwebie:

  1. Brak README (-0.1)

  2. Brak obsługi czekania gdy brakuje miejsca na zgłaszane polecenia (-0.2)

  3. Brak sprawdzania błędu przy alokacji pamięci (-0.5)

  4. Aktywne oczekiwanie na zakończenie zadań (-0.5)

  5. Niepoprawne użycie locków (podwójne wywołanie mutex_unlock, etc.) (-0.2)

  6. Nieprawidłowa obsługa wielu wątków (-0.5)

  7. Brak obsługi suspend/resume (-0.3)

  8. Brak adekwatnych zmian w konfiguracji urządzenia przy suspend / resume (-0.1)

  9. Brak oczekiwania na zakończenie zadań przy suspend (-0.1)

  10. Nieprawidłowa implementacja wykorzystująca automatyczne wczytywanie poleceń (-1.0)

  11. Brak pełnego wyłączania urządzenia przy błędzie podczas inicjalizacji (-0.1)

  12. Wyciek pamięci (-0.2)

  13. Potencjalne spanie (kmalloc z GFP_KERNEL etc.) podczas trzymania spinlocka (-0.5)

  14. Niepoprawne bindowanie buforów (-0.0; ujęte w pkt. za testy)

  15. Niepełna konfiguracja urządzenia PCI (-0.0; ujęte w pkt. za testy)

  16. Niepoprawne ustawianie PRESENT w PTE (-0.0; ujęte w pkt. za testy)

  17. Brak zwracania EIO we wszystkich wymaganych przypadkach (-0.5)

  18. Bufor odwołuje się do urządzenia poprzez kontekst (który może istnieć krócej) (-0.5)

Forma rozwiązania

Sterownik powinien zostać zrealizowany jako moduł jądra Linux w wersji 5.11.2. Moduł zawierający sterownik powinien nazywać się fharddoom.ko. Jako rozwiązanie należy dostarczyć paczkę zawierającą:

  • źródła modułu

  • pliki Makefile i Kbuild pozwalające na zbudowanie modułu

  • krótki opis rozwiązania

Paczka powinna nazywać się ab123456.tar.gz (gdzie ab123456 jest loginem na students) i po rozpakowaniu tworzyć katalog ab123456 ze źródłami. Rozwiązania prosimy nadsyłać na adres p.zuk@mimuw.edu.pl z kopią do mwk@mimuw.edu.pl. Prosimy o umieszczenie [ZSO] w tytule wiadomości.

QEMU

Do użycia urządzenia Final HardDoom™ wymagana jest zmodyfikowana wersja qemu, dostępna w wersji źródłowej.

Aby skompilować zmodyfikowaną wersję qemu, należy:

  1. Sklonować repozytorium https://github.com/mwkmwkmwk/qemu

  2. git checkout fharddoom

  3. Upewnić się, że są zainstalowane zależności: ncurses, libsdl, curl, a w niektórych dystrybucjach także ncurses-dev, libsdl-dev, curl-dev (nazwy pakietów mogą się nieco różnić w zależności od dystrybucji)

  4. Uruchomić ./configure z opcjami wedle uznania (patrz ./configure --help). Oficjalna binarka była kompilowana z:

    --target-list=x86_64-softmmu
    
  5. cd build

  6. Wykonać make (lub ninja, jeśli mamy zainstalowane)

  7. Zainstalować wykonując make install, lub uruchomić bezpośrednio (binarka to build/qemu-system-x86_64).

Aby zmodyfikowane qemu emulowało urządzenie Final HardDoom™, należy przekazać mu opcję -device fharddoom. Przekazanie tej opcji kilka razy spowoduje emulację kilku instancji urządzenia.

Aby dodać na żywo (do działającego qemu) urządzenie Final HardDoom™, należy:

  • przejść do trybu monitora w qemu (Ctrl+Alt+2 w oknie qemu)

  • wpisać device_add fharddoom

  • przejść z powrotem do zwykłego ekranu przez Ctrl-Alt-1

  • wpisać echo 1 > /sys/bus/pci/rescan, aby linux zauważył

Aby udać usunięcie urządzenia:

echo 1 > /sys/bus/pci/devices/0000:<idurządzenia>/remove

Program testowy

Do testowania sterownika przygotowaliśmy zmodyfikowaną wersję prboom-plus, który jest uwspółcześnioną wersją silnika gry Final Doom. Aby go uruchomić, należy:

  • pobrać źródła z repozytorium https://github.com/mwkmwkmwk/prboom-plus

  • wybrać branch fdoomdev

  • skompilować źródła i zainstalować program (bez instalacji, program nie będzie w stanie znaleźć swojego pliku z danymi, prboom-plus.wad):

    • ./bootstrap

    • ./configure --prefix=$HOME

    • make

    • make install

  • pobrać plik z danymi gry. Można użyć dowolnego z następujących plików:

    • freedoom1.wad lub freedoom2.wad z projektu Freedoom (https://freedoom.github.io/) – klon gry Doom dostępny na wolnej licencji.

    • doom.wad, doom2.wad, tnt.wad, lub plutonia.wad z pełnej wersji oryginalnej gry, jeśli zakupiliśmy taką.

    • doom1.wad z wersji shareware oryginalnej gry.

  • załadować nasz sterownik i upewnić się, że mamy dostęp do /dev/fdoom0

  • uruchomić X11, a w nim grę: $HOME/bin/prboom-plus -iwad <dane_gry.wad>

  • w menu Options -> General -> Video mode wybrać opcję “doomdev” (domyślne ustawienie “8bit” wybiera renderowanie programowe w trybie bardzo podobnym do naszego urządzenia i można go użyć do porównania wyników).

Aby w grze działał dźwięk, należy przekazać do qemu opcję -soundhw hda i przy kompilacji jądra włączyć stosowny sterownik (Device Drivers -> Sound card support -> HD-Audio).

Wskazówki

Do tworzenia plików dla buforów polecamy użyć funkcji anon_inode_getfile lub anon_inode_getfd. Aby dostać strukturę file z deskryptora pliku, możemy użyć fdget i fdput. Aby sprawdzić, czy przekazana nam struktura jest odpowiedniego typu, wystarczy porównać jej wskaźnik na strukturę file_operations z naszą.

Jeżeli popsujemy konfigurację prboom-plus tak, że przestanie się uruchamiać w stopniu wystarczającym do zmiany ustawień, możemy znaleźć jego plik konfiguracyjny w $HOME/.prboom-plus/prboom-plus.cfg. Usunięcie go spowoduje przywrócenie ustawień domyślnych.

Użycie FENCE_WAIT

W przypadku użycia bufora CMD_MAIN, prawdopodobnie nie chcemy otrzymywać przerwań po każdym zakończeniu zadania — dobrym pomysłem jest więc użycie przerwania FENCE_WAIT by czekać tylko na wybane zadania. Ponieważ możemy chcieć jednocześnie czekać na zakończenie kilku operacji, sterownik musi mieć listę aktywnych oczekiwań i ustawić FENCE_WAIT na najwcześniejszą z nich. W szczególności, prawdopodobnie będzie konieczne oczekiwanie w następujących przypadkach:

  • wykonanie FDOOMDEV_IOCTL_WAIT

  • brak wolnego miejsca w buforze CMD_MAIN przy wywołaniu FDOOMDEV_IOCTL_RUN (trzeba czekać na zakończenie najstarszego wysłanego zadania)

  • wywołanie suspend (trzeba czekać na zakończenie najnowszego wysłanego zadania, czyli na opróżnienie bufora)

Implementacja mmap

Aby obsłużyć wywołanie mmap na buforach, należy:

  1. Napisać nasz callback mmap do struktury file_operations, który ustawi pole vm_ops w podanym vma na naszą strukturę z callbackami.

  2. W naszej strukturze vm_operations_struct wypełnić callback fault

  3. W callbacku fault:

    • zweryfikować, że pgoff mieści się w rozmiarze bufora (jeśli nie, zwrócić VM_FAULT_SIGBUS)

    • wziąć adres wirtualny (w jądrze) odpowiedniej strony bufora i przekształcić go przez virt_to_page na struct page *

    • zwiększyć licznik referencji do tej strony (get_page)

    • wstawić wskaźnik na tą strukturę do otrzymanej struktury vm_fault (pole page)

    • zwrócić 0