V tomto labe sa dozviete, ako sú implementované systémové volania pomocou výnimiek. Prvá časť cvičenia sú zahrievacie úlohy so zásobníkmi a v druhej časti implementujete obsluhu výnimiek z užívateľského prostredia.
Predtým, ako začnete programovať, prečítajte si štvrtú kapitolu xv6 knižky a pozrite si príslušné súbory:
Na začiatok sa prepnite do vetvy trap:
1 2 3 |
$ git fetch $ git switch traps # alebo git checkout traps $ make clean |
Riešenia úloh z tohto cvičenia ukladajte do vetvy traps.
Na úvod budete musieť pochopiť niekoľko inštrukcií RISC-V. V repozitári nájdete súbor user/call.c. Skompilovať ho môžete príkazom make fs.img. Týmto tiež vytvoríte disassemblovanú verziu programu v súbore user/call.asm.
Prečítajte kód v súbore call.asm funkcií g()
, f()
, and main()
. Stiahnite si manuál k inštrukciám RISC-V zo sekcie literatúra stránky predmetu. Po preštudovaní kódu odpovedajte na tieto otázky do príslušného súboru podľa testovacieho skriptu.
printf()
)?f()
z funkcie main()
? Kde je volanie funkcie g()
? (Pomôcka: Kompilátor môže funkcie inlinovať).printf()
?ra
hneď po jalr
do funkcie printf()
vo funkcii main()
?Spustite tento kód:
1 2 |
unsigned int i = 0x00646c72; printf("H%x Wo%s", 57616, &i); |
Aký je jeho výstup? Tu je ASCII tabuľka, ktorá mapuje bajty na znaky.
Aký bude výstup závisí od toho, či je architektúra RISC-V little-endian. Ak by bol RISC-V big-endian, akú hodnotu by ste museli priradiť premennej i
, aby bol výstup totožný? Bolo by tiež potrebné zmeniť hodnotu 57616
?
Tu je vysvetlenie druhov endianít a tu je prvoaprílová verzia.
Čo bude na výpise za 'y='
v nižšie uvedenom kóde? (poznámka: nie je to žiadna špecifická hodnota.) Prečo sa to deje?
1 |
printf("x=%d y=%d", 3); |
Odpovede na otázky zapíšte do príslušných súborov podľa testovacieho skriptu.
Počas ladenia je dobré mať backtrace: zoznam vnorených volaní funkcií na zásobníku, ktoré boli volané pred výskytom chyby. Vypísať backtrace môžeme vďaka kompilátoru, ktorý generuje strojový kód, ktorý na zásobníku uchováva zásobníkové rámce, ktoré prislúchajú k volaným funkciám v aktuálnom reťazci volaní. Každý zásobníkový rámec obsahuje návratovú adresu a rámcový smerník na zásobníkový rámec volajúcej funkcie. Register s0 obsahuje smerník na aktuálny zásobníkový rámec (ukazuje na adresu uloženej návratovej adresy na zásobníku plus 8 bajtov). Vaša funkcia backtrace
by mala použiť zásobníkové rámce, aby prešla celý zásobník a vypísala uložené návratové adresy v každom zásobníkovom rámci.
Implementujte funkciu backtrace()
v súbore kernel/printf.c. Vložte volanie tejto funkcie do sys_sleep()
a potom spustite bttest, ktorý zavolá sys_sleep
. Výstup by mal byť zoznam návratových adries v takomto tvare (vaše čísla budú pravdepodobne odlišné):
1 2 3 4 |
backtrace: 0x0000000080002130 0x000000008000200c 0x0000000080001c98 |
Po teste bttest ukončite qemu. V termináli spustite addr2line -e kernel/kernel (alebo riscv64-unknown-elf-addr2line -e kernel/kernel) a takto prekopírujete adresy z vášho backtrace:
1 2 3 4 5 |
$ addr2line -e kernel/kernel 0x0000000080002130 0x000000008000200c 0x0000000080001c98 Ctrl-D |
Na výstupe budete vidieť niečo podobné (čísla riadkov sa môžu líšiť):
1 2 3 |
kernel/sysproc.c:63 kernel/syscall.c:144 kernel/trap.c:76 |
Pomôcky:
backtrace()
pridajte do kernel/defs.h, aby ste ju mohli zavolať zo sys_sleep()
.
1 2 3 4 5 6 7 |
static inline uint64 r_fp() { uint64 x; asm volatile("mv %0, s0" : "=r" (x) ); return x; } |
backtrace()
, aby ste získali aktuálny rámcový smerník. Funkcia r_fp()
používa in-line assembly kód na prečítanie registra s0.backtrace
musí rozpoznať posledný zásobníkový rámec a ukončiť výpis. Môžete k tomu využiť fakt, že každý zásobník jadra je reprezentovaný presne jednou stránkou pamäte (so zarovnaním na stránku), takže všetky zásobníkové rámce sú uložené v tej istej stránke. Na zistenie stránky prislúchajúcej k smerníkovému rámcu použite makro PGROUNDDOWN(fp)
(viď kernel/riscv.h). Pamätajte, že pri prechádzaní zásobníkových rámcov smerom k starším volaniam idete k vyšším adresám.Keď budete mať backtrace()
funkčný, zavolajte ho z funkcie panic
v kernel/printf.c. Takto budete môcť vidieť backtrace jadra vždy, keď spanikári.
Po úspešnom dokončení úlohy nezabudnite vytvoriť commit!
V tejto časti cvičenia pridáte do xv6 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. Ak aplikácia zavolá sigalarm(0, 0)
, jadro musí prestať generovať volania alarmu.
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ého zbehnutia usertests -q):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
$ alarmtest test0 start ........alarm! test0 passed test1 start ...alarm! ..alarm! ...alarm! ..alarm! ...alarm! ..alarm! ...alarm! ..alarm! ...alarm! ..alarm! test1 passed test2 start ................alarm! test2 passed test3 start test3 passed $ usertests -q ... ALL TESTS PASSED $ |
Riešenie pozostáva iba z niekoľkých riadkov kódu, ale musíte nad nimi dobre porozmýšľať. Váš kód si môžete otestovať pomocou programu alarmtest.c. Môžete ho upraviť na testovacie účely, ale pred riadnym testovaním ho musíte vrátiť do pôvodného stavu.
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:
Do user/user.h pridajte tieto deklarácie (a zamyslite sa nad tým, čo znamenajú):
1 2 |
int sigalarm(int ticks, void (*handler)()); int sigreturn(void); |
Počet tikov procesu by ste mali upraviť, len ak je proces prerušený časovačom; vyzerať by to malo nejako takto:
1 |
if(which_dev == 2) ... |
Musíte zariadiť, aby obsluha alarmu bola vyvolaná, keď uplynie stanovený interval tikov. Na to musíte upraviť funkciu usertrap() v kernel/trap.c. 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ť?
1 |
make CPUS=1 qemu-gdb |
S veľkou pravdepodobnosťou vám alarmtest padá v teste 0 alebo 1, 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:
usertrap()
tak, aby ste pred spustením funkcie alarmu uložili dostatočné množstvo informácií o stave užívateľského vlákna do struct proc
, aby ste v obsluhe systémového volania sigreturn()
dokázali korektne obnoviť beh prerušeného užívateľského kódu.sigreturn
je systémové volanie a jeho návratová hodnota je uložená v registri a0.
Keď prejdete testami test0, test1, test2 a test3, spustite usertests -q a skontrolujte, či ste nepoškodili nejaké iné časti jadra.
Po úspešnom dokončení úlohy nezabudnite vytvoriť commit!
Týmto je cvičenie ukončené. Skontrolujte, či váš kód prechádza všetkými make grade testami. 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.
backtrace()
namiesto číselných adries (ťažká).