Na poslednom cvičení napíšete ovládač pre sieťovú kartu (angl. network interface card – nic).
Ak chcete pracovať s počítačmi v učebni, postupujte podľa sekcie Stiahnutie repozitára na cvičení na stránke Synchronizácia repozitára.
Stiahnite si kód pre cvičenie do repozitára a prepnite sa na vetvu net:
1 2 3 |
$ git fetch $ git switch net $ make clean |
Riešenia úloh z tohto cvičenia ukladajte do vetvy net.
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 dočí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):
1 |
tcpdump -XXnr packets.pcap |
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.
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.
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 konkrétne 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
popisuje 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 by mal ovládač zapísať 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
popisuje 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. Ak práve neprebieha prerušenie zariadenia E1000, zariadenie požiada PLIC o vyvolanie prerušenia hneď ako budú povolené prerušenia. 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ší):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
reading from file packets.pcap, link-type EN10MB (Ethernet) 15:27:40.861988 IP 10.0.2.15.2000 > 10.0.2.2.25603: UDP, length 19 0x0000: ffff ffff ffff 5254 0012 3456 0800 4500 ......RT..4V..E. 0x0010: 002f 0000 0000 6411 3eae 0a00 020f 0a00 ./....d.>....... 0x0020: 0202 07d0 6403 001b 0000 6120 6d65 7373 ....d.....a.mess 0x0030: 6167 6520 6672 6f6d 2078 7636 21 age.from.xv6! 15:27:40.862370 ARP, Request who-has 10.0.2.15 tell 10.0.2.2, length 28 0x0000: ffff ffff ffff 5255 0a00 0202 0806 0001 ......RU........ 0x0010: 0800 0604 0001 5255 0a00 0202 0a00 0202 ......RU........ 0x0020: 0000 0000 0000 0a00 020f .......... 15:27:40.862844 ARP, Reply 10.0.2.15 is-at 52:54:00:12:34:56, length 28 0x0000: ffff ffff ffff 5254 0012 3456 0806 0001 ......RT..4V.... 0x0010: 0800 0604 0002 5254 0012 3456 0a00 020f ......RT..4V.... 0x0020: 5255 0a00 0202 0a00 0202 RU........ 15:27:40.863036 IP 10.0.2.2.25603 > 10.0.2.15.2000: UDP, length 17 0x0000: 5254 0012 3456 5255 0a00 0202 0800 4500 RT..4VRU......E. 0x0010: 002d 0000 0000 4011 62b0 0a00 0202 0a00 .-....@.b....... 0x0020: 020f 6403 07d0 0019 3406 7468 6973 2069 ..d.....4.this.i 0x0030: 7320 7468 6520 686f 7374 21 s.the.host! |
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:
1 2 3 4 5 6 7 8 9 |
$ nettests nettests running on port 25603 testing ping: OK testing single-process pings: OK testing multi-process pings: OK testing DNS DNS arecord for pdos.csail.mit.edu. is 128.52.129.126 DNS OK all tests passed. |
Nezabudnite skontrolovať, že úspešne prejdú aj testy make grade.
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
:
E1000_TDT
.
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.
mbuffree()
a uvoľnite buffer, ktorý bol odoslaný týmto deskriptorom niekedy v minulosti (ak tam nejaký je).
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.
E1000_TDT
modulo TX_RING_SIZE
.
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
:
E1000_RDT
a pripočítaním jednotky modulo RX_RING_SIZE
.
E1000_RXD_STAT_DD
v sekcii status
deskriptora. Ak nie, ukončite funkciu.
m->len
na veľkosť paketu, ktorá je uvedená v deskriptore. Doručte paket protokolovému zásobníku funkciou net_rx()
.
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.
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.
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é.
Prvý riešiteľ úlohy môže dostať bonusové body po kontrole riešenia. Informujte sa u prednášajúceho alebo cvičiaceho. Voliteľnú úlohú môžete riešiť až po vyriešení všetkých úloh základnej časti cvičenia! Riešenia prijímame iba do 17.12.2023.
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.
net_tx_eth()
. (stredná)
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. (ľahká, ale seriózna implementácia je zložitá na testovanie/meranie.)