Pomocou tohto cvičenia by sa vám malo rozjasniť, ako funguje ukladanie stavu procesu počas prepnutia kontextu a systémových volaní. Vašou úlohou bude implementovať prepínanie medzi vláknami v module pre užívateľské vlákna a alarm, ktorý bude odosielať programom udalosti podobné prerušeniam.
Než sa pustíte do písania kódu, prečítajte si kapitolky Chapter 4: Traps and device drivers a Chapter 6: Scheduling z xv6 knižky, preštudujte príslušný kód a prepnite sa do správnej vetvy:
1 2 |
$ git fetch $ git checkout syscall |
Na toto cvičenie si musíte naštudovať trošku RISC-V assembly. Vo svojom repozitári nájdete súbor user/call.c. Preštudujte si ho (14 riadkov kódu). Po spustení príkazu make fs.img bude skompilovaný a okrem samotného spustiteľného súboru bude vytvorená aj disassemblovaná verzia programu user/call.asm. Tento súbor obsahuje kód assembly tohoto programu spolu s príslušnými riadkami jazyka C.
Prečítajte si kód v call.asm pre jednotlivé funkcie g(), f() a main(). Popis inštrukcií nájdete v doc/riscv-spec-v2.2.pdf. Po preštudovaní kódu odpovedajte na nasledovné otázky:
V tejto časti cvičenia navrhnete a implementujete mechanizmus prepnutia kontextu pre užívateľské vlákna. Na začiatok máte pripravené dva súbory: user/uthread.c a user/uthread_switch.S. Do súboru Makefile sme pridali pravidlo na skompilovanie programu uthread. Tento program už obsahuje väčšinu potrebného kódu,
vrátanie testu s troma vláknami. Do programu budete musieť doplniť kód na vytvorenie nového vlákna a prepínanie medzi vláknami.
Vašou úlohou je navrhnúť spôsob, ktorým budete vytvárať vlákna a ukladať/obnovovať registre na prepínanie medzi vláknami. Potom tento spôsob implementujte.
Keď budete hotoví, spustite program uthread v xv6. Výstup by mal vyzerať nasledovne (až na poradie vlákien, ktoré sa môže líšiť):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
~/classes/6828/xv6$ make qemu ... $ uthread thread_a started thread_b started thread_c started thread_c 0 thread_a 0 thread_b 0 thread_c 1 thread_a 1 thread_b 1 ... thread_c 99 thread_a 99 thread_b 99 thread_c: exit after 100 thread_a: exit after 100 thread_b: exit after 100 thread_schedule: no runnable threads $ |
Tento výstup pochádza z troch testovacích vlákien. Každé z nich obsahuje cyklus, ktorý vypíše riadok a vzdá sa procesora pre iné vlákna.
Pri nezmenenom kóde neuvidíte žiadny výstup.
Musíte dokončiť funkciu thread_create(), ktorá vytvorí správne inicializované vlákno tak, aby keď plánovač prepne do tohoto vlákna po prvý raz, thread_switch() sa vráti do funkcie určenej argumentom func na zásobníku vlákna. Takže je potrebné správne inicializovať tie údajové štruktúry vlákna, z ktorých sa vo funkcii thread_switch() obnovuje hodnota registrov PC (program counter) a SP (stack pointer). Musíte sa rozhodnúť, kde uložiť/obnoviť registre. Existuje viacero možných riešení. Odporúčame, aby ste sa inšpirovali prepínaním kontextu vlákien jadra. K vášmu riešeniu musíte upraviť štruktúru thread. Vo funkcii thread_schedule() musíte zavolať thread_switch(); môžete jej poslať ľubovoľné argumenty, ktoré potrebujete, ale jej hlavným cieľom je prepnúť z vlákna t do vlákna next_thread.
Pomôcky:
1 2 3 |
(gdb) file user/_uthread Reading symbols from user/_uthread... (gdb) b thread.c:60 |
Po spustení shellu xv6, zadajte uthread a gdb zastaví na riadku vo funkcii thread_switch(). Teraz môžete preskúmať stav programu uthread:
1 |
(gdb) p/x *next_thread |
Pomocou x môžete zobraziť obsah pamäte:
1 |
(gdb) x/x next_thread->stack |
Krokujte inštrukciami assembly pomocou step instruction:
1 |
(gdb) si |
On-line dokumentáciu pre gdb nájdete tu.
V tejto časti cvičenia do xv6 pridáte funkcionalitu, ktorá periodicky informuje proces o tom, ako napr. zaťažuje procesor. Takáto funkcionalita sa hodí pre procesy, ktoré chcú obmedziť využívanie CPU alebo pre procesy, ktoré potrebujú vykonávať periodicky nejakú procedúru. Ináč povedané, budete implementovať primitívnu formu užívateľskej obsluhy prerušení/výpadkov; niečo podobné môžete napríklad využiť na obsluhu výpadkov stránok v aplikácii. Vaše riešenie je kompletné, ak prejde testami alarmtest a usertests.
Vytvorte nové systémové volanie sigalarm(interval, handler). Po tom, ako aplikácia zavolá sigalarm(n, fn), bude pravidelne každých n tikov procesora zavolaná funkcia fn. Po návrate z fn musí aplikácia pokračovať na tom mieste užívateľskej aplikácie, kde bola prerušená spustením funkcie fn. Pozn.: tik je nezávislá jednotka, jej veľkosť je určená hardvérovým časovačom.
Aby ste mohli svoju implementáciu otestovať, musíte mať implementované systémové volania sigalarm a sigreturn (viď. nižšie). V repozitári nájdete súbor user/alarmtest.c. Pridajte ho do Makefile, aby bol kompilovaný.
alarmtest() zavolá sigalarm(2, periodic) vo funkcii test0(). Týmto vyžiada od jadra, aby zavolalo funkciu periodic() každé 2 tiky. Assembly kód programu v
súbore user/alarmtest.asm sa vám môže hodiť na ladiace účely. Korektné riešenie by malo mať nasledovný výstup pre alarmtest (vrátane úspešných usertests):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$ alarmtest test0 start ......................................alarm! test0 passed test1 start ..alarm! ..alarm! ..alarm! .alarm! ..alarm! ..alarm! ..alarm! ..alarm! ..alarm! ..alarm! test1 passed $ usertests ... ALL TESTS PASSED $ |
Vašou prvou výzvou je zariadiť, aby obsluha alarmu bola vyvolaná, keď uplynie stanovený interval tikov. Na to musíte upraviť funkciu usertrap() v kernel/trap.c. Pýtate sa, ako ju upraviť? Musíte pochopiť, ako fungujú systémové volania (teda kód v kernel/trampoline.S a kernel/trap.c). Ktorý register obsahuje adresu užívateľskej inštrukcie, do ktorej sa má systémové volanie vrátiť?
Riešenie pozostáva iba z niekoľkých riadkov kódu, ale musíte nad nimi dobre porozmýšľať. Testovací súbor alarmtest.c neupravujte.
Začnite úpravou jadra, aby skočil na obsluhu alarmu v užívateľskom priestore — v test0 vypíše alarm!. Zatiaľ vás nemusí zaujímať, čo sa stane po tomto výpise; je v poriadku, ak program padne. Pomôcky:
1 2 |
int sigalarm(int ticks, void (*handler)()); int sigreturn(void); |
1 |
if(which_dev == 2) ... |
1 |
make CPUS=1 qemu-gdb |
S veľkou pravdepodobnosťou vám alarmtest padá v jednom z testov, prípadne po vypísaní alarm!, alebo alarmtest skončí bez výpisu test1 passed. Aby ste to opravili, musíte po ukončení obsluhy alarmu vrátiť riadenie na inštrukciu užívateľského programu, ktorá bola prerušená časovačom. Obsah registrov musí byť navrátený do stavu, v akom bol
pred prerušením, aby mohol program pokračovať po obsluhe alarmu. Taktiež musíte resetovať počítadlo alarmu pre daný proces, aby bol po stanovenom počte tikov
znovu vyvolaný.
Aby sa vám lepšie s touto úlohou začínalo, alarmový systém sme navrhli tak, aby každá obslužná funkcia alarmu zavolala systémové volanie sigreturn po dokončení obsluhy. V praxi to môžete vidieť vo funkcii periodic() v alarmtest.c. To znamená, že môžete pridať kód do usertrap() a sys_sigreturn(), ktorý pomáha riadne obnoviť proces po obsluhe alarmu.
Pomôcky:
Keď prejdete testami test0 a test1, spustite usertests a skontrolujte, či ste nepoškodili nejaké iné časti jadra.
Týmto je cvičenie ukončené. Skontrolujte, či váš kód prechádza všetkými make grade testami. Na záver commitnite svoje zmeny do repozitára.
Náš modul pre vlákna nespolupracuje pekne s operačným systémom. Napríklad, ak jedno užívateľské vlákno blokuje v systémovom volaní, iné užívateľské vlákna nebudú bežať, pretože plánovač užívateľských vlákien nevie o tom, že jedno vlákno bolo odplánované plánovačom xv6. Iný príklad: dve užívateľské vlákna nebudú bežať konkurentne na rôznych jadrách, pretože plánovač xv6 nevie o tom, že existujú viaceré vlákna, ktoré môžu bežať paralelne. Poznámka: ak by dve užívateľské vlákna mali bežať naozaj paralelne, táto implementácia by nefungovala z dôvodu viacerých súbehov (napr. dve rôzne vlákna na rôznych procesoroch môžu zavolať thread_schedule() konkurentne, vybrať rovnaké vlákno a bežať spolu na rôznych procesoroch.)
Tieto problémy je možné vyriešiť viacerými spôsobmi. Jedným z nich je použiť aktivácie plánovača; iné je použiť jedno vlákno jadra pre každý užívateľský proces (takto to robí Linux). Implementujte jeden z týchto spôsobov. Ľahké to nebude; budete musieť implementovať vyčistenie TLB pri aktualizácii tabuliek stránok pre
viacvláknový užívateľský proces.
Pridajte zámky, podmienené premenné, bariéry, etc. do vášho modulu vlákien.