10. cvičenie – sieť

Na poslednom cvičení napíšete ovládač pre sieťovú kartu (angl. network interface cardnic).

Stiahnite si kód pre cvičenie do repozitára a prepnite sa na vetvu net:

Riešenia úloh z tohto cvičenia ukladajte do vetvy net. Túto vetvu budeme kontrolovať v rámci priebežnej kontroly repozitárov. Viac informácií k hodnoteniu repozitárov nájdete na príslušnej stránke.

Pozadie

Predtým, než sa pustíte do písania kódu, prečítajte si kapitolu 5 Interrupts and device drivers z xv6 knižky.

Na spracovanie sieťovej komunikácie budete používať sieťové zariadenie E1000. xv6 (a zároveň ovládač) bude vidieť toto zariadenie ako skutočnú hardvérovú kartu pripojenú k lokálnej sieti Ethernet (LAN). V skutočnosti bude ale táto karta spoločne so sieťou emulovaná emulátorom QEMU. V tejto sieti má xv6 (hosť) napevno nastavenú adresu 10.0.2.15 a hostiteľ (tam, kde beží QEMU) má priradenú adresu 10.0.2.2. Keď xv6 odošle paket na adresu 10.0.2.2, QEMU ho doručí príslušnej aplikácii na hostiteľskom počítači.

Viac o emulácii siete v QEMU sa môžete dokčítať v dokumentácii. Nie je to ale povinné, keďže Makefile aktivuje sieťovú kartu E1000 a nakonfiguruje sieť pre QEMU automaticky.

Okrem vyššie uvedeného sa Makefile postará o záznam všetkých prichádzajúcich a odchádzajúcich paketov z QEMU do súboru packets.pcap v koreňovom priečinku xv6. Počas práce na cvičení môže byť nápomocné pozrieť si jeho obsah a porovnať, či xv6 vysiela a prijíma pakety, ktoré očakávate. Na zobrazenie paketov vykonajte (na vývojovom OS):

V repozitári pribudlo niekoľko súborov. Súbor kernel/e1000.c obsahuje inicializačný kód pre E1000 a taktiež prázdne funkcie pre odosielanie a prijímanie paketov, ktoré musíte doplniť. kernel/e1000_dev.h obsahuje definície pre registre a príznaky zariadenia E1000, ktoré sú popísané v programátorskej príručke E1000. kernel/net.c a kernel/net.h obsahujú jednoduchý protokolový zásobník, ktorý implementuje protokoly IP, UDP a ARP. Tieto súbory taktiež obsahujú kód dátovej štruktúry mbuf, ktorá slúži na flexibilné ukladanie paketov. Posledný súbor, kernel/pci.c, obsahuje kód ktorý vyhľadá E1000 na PCI zbernici pri štarte xv6.

Zadanie úlohy(ťažká)

Vašou úlohou je dokončiť e1000_transmit() a e1000_recv() v súbore kernel/e1000.c tak, aby ovládač mohol posielať a prijímať pakety. Riešenie je hotové, ak v make grade prejdú všetky testy úspešne.
Počas písania kódu vám bude určite užitočná programátorská príručka. Nižšie vyberáme sekcie dôležité pre toto cvičenie:

  • Sekcia 2 popisuje celé zariadenie vo všeobecnosti (treba rýchlo prelistovať).
  • Sekcia 3.2 popisuje prijímanie paketu.
  • Sekcie 3.3 a 3.4 popisujú odosielanie paketu.
  • Sekcia 13 popisuje registre zariadenia E1000.
  • Sekcia 14 popisuje inicializáciu zariadenia (pozrite pri študovaní funkcie e1000_init()).

Prelistujte si programátorskú príručku E1000. Táto príručka popisuje niekoľko príbuzných sieťových radičov. QEMU emuluje radič Intel 82540EM. Prelistuje si kapitolu 2 na rýchle zoznámenie so zariadením (detaily vás nemusia zaujímať). Na napísanie ovládača budete potrebovať hlavne kapitolu 3. K nahliadnutiu sa budú hodiť tiež kapitoly 14, 4.1 (bez podsekcií) a 13 (konštanty, ktoré používa E1000, sú už definované v kernel/e1000_dev.h). Zvyšné kapitoly popisujú komponenty E1000, s ktorými nebudete pracovať. Na začiatok nemusíte príručku čítať detailne. Stačí, aby ste mali prehľad, čo je kde v príručke, aby ste to mohli neskôr nájsť. E1000 má veľa pokročilých funkcionalít, ktoré môžete ignorovať.

Funkcia e1000_init() v súbore e1000.c nakonfiguruje E1000, aby zapísal prijaté pakety do RAM a taktiež z nej čítal pakety na odoslanie. Táto technika sa volá DMA (angl. direct memory access – priamy prístup do pamäte), čo znamená, že sieťová karta zapisuje/číta pakety priamo do/z RAM.

Keďže zhluky paketov môžu doraziť rýchlejšie, ako ich ovládač stihne spracovať, funkcia e1000_init() definuje buffre pre sieťové zariadenie, do ktorej bude zariadenie ukladať prichádzajúce pakety. Každý jeden buffer je popísaný elementom poľa deskriptorov v pamäti. Deskriptor obsahuje adresu buffra, kde môže E1000 zapísať prijatý paket. Štruktúra struct rx_desc uvádza formát dekriptora. Pole týchto deskriptorov sa nazýva prijímací okruh (angl. receive ring) alebo prijímacia fronta. Keď sieťová karta alebo ovládač dosiahnu koniec poľa, pokračujú od jeho začiatku. Z tohto dôvodu môžeme hovoriť o kruhovom buffri. Funkcia e1000_init() alokuje buffre mbuf na ukladanie paketov pomocou funkcie mbufalloc(). Analogicky existuje tiež odosielací okruh (angl. transmit ring), do ktorého ovládač zapisuje pakety na odoslanie. Tieto kruhové buffre majú veľkosti RX_RING_SIZE a TX_RING_SIZE.

Keď chce protokolový zásobník v súbore net.c odoslať paket, zavolá funkciu e1000_transmit() s buffrom, ktorý obsahuje paket na odoslanie. V tejto funkcii budete musieť uložiť smerník na dáta paketu do deskriptora v TX (odosielacom) okruhu. Štruktúra struct tx_desc uvádza formát deskriptora. Musíte tiež zabezpečiť, aby každý mbuf bol eventuálne uvoľnený po tom, čo E1000 dokončí odosielanie paketu (paket bol odoslaný, ak je bit E1000_TXD_STAT_DD v deskriptore nastavený na 1).

Po prijatí paketu z ethernetu ho E1000 uloží do pamäte, na ktorú ukazuje ďalší voľný deskriptor RX (prijímacieho) okruhu a vygeneruje prerušenie. Vaša implementácia funkcie e1000_recv() musí prečítať údaje z okruhu RX a doručiť mbuf protokolovému zásobníku (v net.c) zavolaním funkcie net_rx(). Potom musíte alokovať nový mbuf a vložiť ho do deskriptora, aby E1000 neskôr v deskriptore našiel pripravený prázdny buffer, keď sa k nemu opäť dostane.

Okrem čítania a zapisovania okruhov deskriptorov v RAM bude musieť váš ovládač pracovať s namapovanými riadiacimi registrami E1000. Na základe stavu týchto registrov môže ovládač zistiť, kedy sú dostupné prijaté pakety, alebo pomocou nastavenia registrov môže oznámiť zariadeniu, že sú nejaké pakety pripravené na odoslanie. Globálna premenná regs obsahuje smerník na prvý riadiaci register E1000. Zvyšné registre viete získať indexovaním regs ako pole. Odporúčame vám využiť indexy E1000_RDT a E1000_TDT.

Na otestovanie ovládača spustite make server v jednom okne, v druhom spustite make qemu a potom spustite nettests v xv6. Prvý test vyskúša odoslať UDP paket hostiteľskému operačnému systému na porte pre užívateľský program, ktorý ste spustili príkazom make server. Bez implementácie vyššie uvedených funkcií E1000 paket neodošle.

Po úspešnej implementácii ovládač E1000 odošle paket, QEMU ho doručí hostiteľskému počítaču, make server ho prijme a odošle paket s odpoveďou, ktorý prijme E1000 a dostane sa až do programu nettests. Predtým, ako hostiteľ pošle odpoveď, pošle najprv ARP požiadavku pre xv6, aby zistil 48-bitovú MAC adresu, ktorú vlastní xv6. Hostiteľ očakáva, že xv6 odpovie ARP odpoveďou. O toto sa postará kód v kernel/net.c. Ak je všetko v poriadku, nettests vypíše „testing ping: OK“ a make server vypíše „a message from xv6!“.

Príkaz tcpdump -XXnr packets.pcap by mal vypísať podobný výstup (bude o niečo dlhší):

Váš výstup by mal obsahovať reťazce „ARP, Request“, „ARP, Reply“, „UDP“, „a.message.from.xv6“ a „this.is.the.host“.

Program nettests vykoná nejaké ďalšie testy, ktoré končia vyslaním DNS požiadavky na (skutočný) internet jednému mennému serveru spoločnosti Google (na tento test teda potrebujete aktívne pripojenie k internetu). Prejsť by ste mali všetkými testami, tu je ukážkový výstup:

Nezabudnite skontrolovať, že úspešne prejdú aj testy make grade.

Pomôcky:

Začnite pridaním výpisov do funkcií e1000_transmit() a e1000_recv(). Spustite server pomocou make server a v xv6 vykonajte príkaz nettests. Mali by ste vidieť, že nettests vygeneruje volanie e1000_transmit().

Pomôcky pre e1000_transmit:

  • Najprv si vyžiadajte od E1000 index okruhu TX, na ktorom očakáva ďalší paket. Urobíte to prečítaním riadiaceho registra E1000_TDT.
  • Skontrolujte, či okruh nepretiekol. Ak je bit E1000_TXD_STAT_DD v deskriptore s indexom E1000_TDT nulový, E1000 ešte nedokončil predchádzajúci prenos, takže vráťte chybovú hodnotu.
  • V opačnom prípade zavolajte mbuffree() a uvoľnite buffer, ktorý bol odoslaný týmto deskriptorom niekedy v minulosti (ak tam nejaký je).
  • Potom vyplňte deskriptor. Položka m->head ukazuje na obsah paketu v pamäti a m->len je veľkosť paketu. Nastavte potrebné cmd príznaky (pozrite si sekciu 3.3 programátorskej príručky) a uložte smerník na mbuf pre neskoršie uvoľnenie.
  • Nakoniec aktualizujte pozíciu okruhu pridaním jednotky k E1000_TDT modulo TX_RING_SIZE.
  • Ak e1000_transmit() pridal mbuf úspešne do okruhu, vráťte nulu. Pri zlyhaní (napr. ak nie je dostupný žiadny voľný deskriptor) vráťte -1, aby volajúca funkcia vedela, že má mbuf uvoľniť.

Pomôcky pre e1000_recv:

  • Najprv vyžiadajte od E1000 index okruhu, na ktorom čaká ďalší prijatý paket (ak existuje). Dosiahnete to prečítaním riadiaceho registra E1000_RDT a pripočítaním jednotky modulo RX_RING_SIZE.
  • Potom skontrolujte, či je dostupný nový paket prečítaním bitu E1000_RXD_STAT_DD v sekcii status deskriptora. Ak nie, ukončite funkciu.
  • V opačnom prípade nastavte položku buffra m->len na veľkosť paketu, ktorá je uvedená v deskriptore. Doručte paket protokolovému zásobníku funkciou net_rx().
  • Alokujte nový mbuf zavolaním funkcie mbufalloc(), aby ste nahradili ten, ktorý ste práve odoslali do net_rx(). Nastavte dátový smerník (m->head) v deskriptore a jeho status vynulujte.
  • Nakoniec nastavte hodnotu registra E1000_RDT na index posledne spracovaného deskriptora.
  • e1000_init() alokuje buffre pre okruh RX. Môže sa vám hodiť pozrieť sa, ako to robí a požičať si nejaký kód.
  • Po nejakom čase počet prijatých paketov prekročí veľkosť okruhu (16). Váš kód musí túto situáciu zvládnuť.

Na úspešné riešenie cvičenia budete potrebovať zámky, keďže E1000 môže používať viac procesov súčasne, prípadne pri jeho používaní z vlákna jadra môže nastať výpadok.

Po úspešnom dokončení úlohy nezabudnite vytvoriť commit! Aby ste prešli posledným testom time, musíte vytvoriť nový súbor time.txt, v ktorom uvediete počet hodín, ktorý ste strávili nad zadaním ako celé číslo. Na záver aj túto zmenu commitnite do repozitára. Týmto je cvičenie ukončené.

Voliteľné úlohy

Vylepšenia niektorých voliteľných úloh sú merateľné/testovateľné iba na skutočnom vysoko výkonnom hardvéri, teda na architektúre x86.

  • Protokolový zásobník xv6 používa prerušenia na detekciu prichádzajúcich paketov, ale nie na notifikáciu odchádzajúcich paketov. Sofistikovanejšia stratégia by umiestnila odchádzajúce pakety do softvérovej fronty a odosielala ich sieťovému zariadeniu iba v limitovanom počte. Po TX prerušení je možné opätovne naplniť odchádzajúci okruh. Použitím tejto techniky je možné prioritizovať rozdielne typy odchádzajúcej prevádzky. (ľahká)
  • Protokol ARP je podporovaný iba čiastočne. Implementujte plnú ARP kešku a napojte ju do net_tx_eth(). (stredná)
  • E1000 podporuje niekoľko okruhov RX a TX. Nakonfigurujte E1000, aby používal jeden okruh pre každé jadro CPU a upravte protokolový zásobník, aby podporoval viacero okruhov. Vďaka tomu je možné ddosiahnuť vyššiu priepustnosť siete a znížiť súperenie o zámky. (stredná), ale zložitá na testovanie/meranie.
  • Funkcia sockrecvudp() vyhľadáva cieľový soket pomocou zreťazeného zoznamu, čo je pri väčšom počte otvorených soketov neefektívne. Použite hašovaciu tabuľku a RCU mechanizmus na zvýšenie výkonu. (easy), ale seriózna implementácia je zložitá na testovanie/meranie.
  • Protokol ICMP poskytuje upozornenia o zlyhaní komunikácie. Detegujte tieto upozornenia a presmeruje ich do rozhrania systémových volaní soketov ako chyby.
  • E1000 podporuje funkcionalitu offloadingu (presunutia výpočtov z CPU do sieťovej karty) niekoľkých typov, vrátane výpočtu kontrolnej sumy, RSC a GRO. Použitie jeden alebo viacero z týchto offloadingov na zvýšenie priepustnosti siete. (stredná), ale zložitá na testovanie/meranie.
  • Protokolový zásobník v tomto cvičení je náchylný na livelock. Navrhnite a implementujte jeho riešenie podľa článku. (stredná), ale ťažká na testovanie.
  • Implementujte UDP server pre xv6. (stredná)
  • Implementujte minimálny TCP zásobník a stiahnite webovú stránku. (ťažká)