4. cvičenie – výnimky

Výnimky

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:

  • kernel/trampoline.S: kód assembly, ktorý sa používa pri prechode z užívateľského prostredia do priestoru jadra a späť
  • kernel/trap.c: obsluha všetkých prerušení

Na začiatok sa prepnite do vetvy trap:

Assembly architektúry RISC-V (ľahká)

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 na stránke MIT. Po preštudovaní kódu odpovedajte na tieto otázky (aby vám zbehli testy, mali by ste ich uložiť do súboru answers-traps.txt).

Ktoré registre obsahujú argumenty funkcií? V ktorom registri je napríklad uložené číslo 13 (argument funkcie printf())?
Kde je volanie funkcie f() z funkcie main()? Kde je volanie funkcie g()? (Pomôcka: Kompilátor môže funkcie inlinovať).
Na ktorej adrese sa nachádza funkcia printf()?
Aká hodnota je v registri ra hneď po jalr do funkcie printf() vo funkcii main()?

Spustite tento kód:

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?

Backtrace (stredná)

Počas ladenia je dobré mať backtrace: zoznam vnorených volaní funkcií na zásobníku, ktoré boli volané pred výskytom chyby.

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 vyzerať takto:

Po teste bttest ukončite qemu. Adresy vo vašom termináli sa môžu o niečo líšiť, ale ak spustíte addr2line -e kernel/kernel (alebo riscv64-unknown-elf-addr2line -e kernel/kernel) a takto prekopírujete vyššie adresy:

Na výstupe budete vidieť:

Do každého zásobníkového rámca kompilátor vloží rámcový smerník, ktorý ukazuje na adresu rámcového smerníka volajúceho. Vo vašej funkcii backtrace použijete tieto rámcové smerníky na postupné prejdenie zásobníka a vypíšete návratové hodnoty každého zásobníkového rámca.

Pomôcky:

  • Prototyp funkcie backtrace() pridajte do kernel/defs.h, aby ste ju mohli zavolať zo sys_sleep().
  • Kompilátor GCC ukladá rámcový smerník práve vykonávanej funkcie do registra s0. Do kernel/riscv.h pridajte túto funkciu:

    a zavolajte ju v backtrace(), aby ste získali aktuálny rámcový smerník. Táto funkcia používa in-line assembly kód na prečítanie registras0.
  • Tieto poznámky obsahujú diagramy s rozložením zásobníkových rámcov. Všimnite si, že návratové adresy sú na pevnom offsete (-8) od rámcového smerníka zásobníkového rámca. Uložený zásobníkový smerník je podobne na pevnom offsete (-16) od rámcového smerníka.
  • Xv6 alokuje jednu stránku pre každý zásobník. Adresa zásobníka je teda zarovnaná na stránku. Hornú a dolnú adresu zásobníka viete vypočítať makrami PGROUNDDOWN(fp) a PGROUNDUP(fp) (viď kernel/riscv.h. Tieto čísla sú užitočné, aby backtrace vedel ukončiť svoj cyklus.

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.

Alarm (ťažká)

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á signalarm(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ých usertests):

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.

test0: vyvolanie obslužnej funkcie

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:

  • Už sme to napísali vyššie, ale pre istotu zopakujeme — musíte upraviť Makefile, aby bol alarmtest.c skompilovaný ako užívateľský program.
  • Do user/user.h pridajte tieto deklarácie (a zamyslite sa nad tým, čo znamenajú):

  • Upravte user/usys.pl (tento skript generuje súbor user/usys.S, kernel/syscall.h, and kernel/syscall.c) aby mohol alarmtest vyvolať systémové volanie sigalarm a sigreturn (nemusíte sa kvôli tomu učiť ?, stačí nahliadnuť do súboru).
  • Súbor kernel/syscall.c by ste mali rozšíriť o externé deklarácie funkcií sys_sigreturn() a sys_sigalarm(), a taktiež tabuľku obslužných funkcií systémových volaní. Spolu s tým súvisí úprava súboru kernel/syscall.h: treba priradiť číselné hodnoty novým systémovým volaniam.
  • Zatiaľ stačí, ak sys_sigreturn() (funkciu definujte v súbore kernel/sysproc.c) vráti nulu.
  • Funkcia sys_sigalarm() (tiež v tom istom súbore) by mala uložiť interval alarmu a smerník na obslužnú funkciu v nových poliach štruktúry proc (v kernel/proc.h) — musíte ich tam vytvoriť.
  • Musíte si tiež niekde ukladať koľko tikov uplynulo od posledného volania obslužnej funkcie (resp. koľko tikov zostáva do ďalšieho alarmu). Najvhodnejším miestom sa nám opäť zdá štruktúra proc (viď. bod vyššie). Polia štruktúry proc môžete inicializovať vo funkcii allocproc() v proc.c.
  • Ako počítať tiky? Hardvérové hodiny neustále tikajú a vyvolávajú prerušenia, ktoré sú obslúžené v usertrap(); mali by ste tam pridať nejaký kód
  • Počet tikov procesu by ste mali upraviť, len ak je proces prerušený časovačom; vyzerať by to malo nejako takto:

  • Funkciu obsluhy vykonajte iba vtedy, ak proces požiadal o alarm. Ale pozor, adresa funkcie môže byť aj nulová (napr. v alarmtest.asm, funkcia periodic() je na adrese 0).
  • 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ť?

  • S gdb sa ťažšie ladí kód výnimiek s viacerými procesormi. Uľahčite si prácu a povedzte qemu, aby použil iba jeden procesor:
  • Máte to hotové, ak alarmtest vypíše alarm!

test1/test2(): vrátenie do prerušeného kódu

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:

  • Vo vašom riešení musíte uložiť a obnoviť registre—ktoré registre musíte uložiť a obnoviť, aby mohol prerušený kód bezproblémovo pokračovať? (Pomôcka: je ich fakt veľa).
  • Upravte 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.
  • Nevolajte obsluhu, ak obsluha ešte prebieha! Testuje to test2.

Keď prejdete testami test0, test1 a test2, 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. Aby ste mali plný počet bodov, vytvorte súbor answers-traps.txt s odpoveďami na otázky v zadaní. 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 commitnite svoje zmeny do repozitára.

Voliteľné úlohy

  • Vypíšte názvy funkcií a čísel riadkov vo funkcii backtrace() namiesto číselných adries (ťažká).