7. cvičenie – zámky

Zámky

Na tomto cvičení získate skúsenosti s redizajnovaním kódu s cieľom zvýšenia paralelizmu. Častým symptómom slabého paralelizmu na viacjadrových strojoch je veľké súperenie o zámky. Súperenie nastáva, keď jedno vlákno chce pristúpiť k zámku, ktorý je zamknutý iným vláknom. Zlepšiť paralelizmus je možné zmenou dátových štruktúr a stratégií zamykania tak, aby bolo súperenie znížené. Na tomto cvičení to urobíte pre pamäťový alokátor v xv6 a cache blokov.

Pred písaním kódu si prečítajte tieto časti z xv6 knižky:

  • Kapitola 7: Locking a príslušný kód.
  • Sekcia 3.5: „Code: Physical memory allocator“

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.

Prepnite sa do vetvy lock:

Riešenia úloh z tohto cvičenia ukladajte do vetvy lock.

Pamäťový alokátor (stredná)

Program user/kalloctest zaťaží pamäťový alokátor xv6: tri procesy zväčšujú a zmenšujú svoj adresný priestor, čo vyvolá veľa volaní funkcií kalloc() a kfree(). Tieto dve funkcie zamykajú zámok kmem.lock. kalloctest vypisuje počet iterácií vo funkcii acquire, počas ktorých kontroluje, či je zámok zamknutý iným vláknom (#test-and-set). Vypisuje ich pre zámok kmem a ešte niekoľko ďalších zámkov. Vypísaný počet iterácií acquire vyjadruje približnú mieru súperenia o zámky. Výstup kalloctest pred začatím cvičenia vyzerá takto (počty sa môžu na vašom PC odlišovať):

Pravdepodobne budete vidieť iné počty ako uvedené vyššie a taktiež rozdielne poradie piatich zámkov s najväčším súperením.

Funkcia acquire() si pre každý zámok ukladá počet jej volaní a počet cyklov, počas ktorých sa pokúsila získať zámok, ale nepodarilo sa jej to. kalloctest zavolá systémové volanie, ktoré vypíše tieto počty pre zámky kmem a bcache (ktoré sú hlavným bodom záujmu tohto cvičenia) a pre 5 najviac zaťažených zámkov. Ak nastáva súperenie o zámky, počet cyklov bude vysoký. Systémové volanie vracia súčet cyklov pre zámky kmem a bcache.

Na toto cvičenie musíte použiť nezaťažený stroj s viacerými jadrami. Ak používate virtuálny stroj, uistite sa, že využíva aspoň 2 jadrá vášho fyzického procesora. Stroj musí byť nezaťažený, ináč výpisy kalloctestu nebudú mať význam.

Súperenie o zámky v kallocteste je spôsobené najmä tým, že kalloc() má jeden zoznam voľných stránok chránený jedným zámkom. Na odstránenie súperenia o zámky musíte zmeniť návrh alokátora, aby nepoužíval iba jeden zoznam s jedným zámkom. Základnou ideou nového návrhu je použiť jeden zoznam na jedno CPU, každé s vlastným zámkom. Alokácie a uvoľnenia stránok budú vďaka tomu môcť prebiehať na rôznych CPU paralelne. Najväčšou výzvou je prípad, kedy jednému CPU dôjdu stránky, zatiaľ čo iné CPU ich ešte majú. Vtedy musí jeden CPU ukradnúť niekoľko stránok CPU, ktoré ešte nejaké stránky má. Proces kradnutia môže spôsobiť nejaké súperenie o zámky, ale v menšej miere, ako je tomu teraz.

Vašou úlohou je implementovať zoznamy voľných stránok pre každé CPU a proces kradnutia, keď nejaké CPU minie všetky svoje stránky. Názvy zámkov, ktoré vytvoríte musia začínať na kmem. To znamená, že pre každý zámok zavoláte funkciu initlock() a ako argument použijete meno, ktoré začína na kmem. Spustením kalloctest môžete zistiť, či vaša implementácia zmiernila súperenie o zámky. Či je možné alokovať celú fyzickú pamäť overíte testom usertests skbrkmuch. Výstup by mal vyzerať podobne ako je uvedené nižšie, ale konkrétne čísla sa môžu líšiť. Počty súperení by mali byť nižšie. Skontrolujte tiež, či prejdete testami usertests -q. make grade by mal hlásiť, že ste prešli všetkými testami kalloctests.

Pomôcky:

  • Použite konštantu NCPU definovanú v kernel/param.h.
  • Funkcia freerange() môže prideliť všetku voľnú pamäť tomu CPU, ktoré ju zavolá.
  • Funkcia cpuid() vracia číslo aktuálneho CPU, ale volať ju a používať jej návratovú hodnotu môžete len vtedy, keď sú vypnuté prerušenia. Použite push_off() a pop_off() na vypnutie a zapnutie prerušení.
  • Ak chcete nejakým spôsobom formátovať reťazce (pre názvy zámkov), pozrite sa do súboru kernel/sprintf.c. Nie je ale žiadny problém, ak všetky zámky pomenujete kmem.
  • Voliteľne môžete otestovať vaše riešenie detektorom súbehov:

    Test kalloctest môže zlyhať, ale nemali by ste vidieť žiadne súbehy. Ak detektor súbehov deteguje súbeh, vypíše dva výpisy zásobníka, ktoré vyzerajú takto::

    Názvy funkcií s číslami riadkov k týmto výpisom môžete získať pomocou príkzu addr2line:

    Detektor súbehov nemusíte spúšťať, ale môže sa vám hodiť. Pri zapnutom detektore súbehov je systém xv6 výrazne spomalený, preto v ňom asi nebudete chcieť spúšťať usertests.

Po úspešnom dokončení úlohy nezabudnite vytvoriť commit!

Read-write zámok (stredná)

Druhá časť zadania je nezávislá od prvej. Na tejto úlohe môžete začať pracovať bez toho, aby ste mali hotovú prvú úlohu.

Funkcie sys_pause() a sys_uptime() v xv6 čítajú globálnu premennú ticks. Keďže táto premenná môže byť konkurentne upravovaná funkciou clockintr(), spomenuté dve funkcie musia pred prečítaním premennej ticks získať zámok tickslock. Vďaka tomu nemôže funkcia clockintr() konkurentne upravovať premennú ticks, ale zároveň viacero jadier nemôže čítať premennú ticks naraz, čo by bolo v poriadku (konkurentné čítanie hodnotu nemodifikuje). Pri čítaní je teda spinlock zbytočný a znižuje priepustnosť systému.

Štandardné riešenie tohto problému je použitie read-write zámku. Read-write zámky rozlišujú dva druhy držiteľov zámku: čitateľov a zapisovateľov. Zámok môže držať naraz najviac jeden zapisovateľ (ak ho drží zapisovateľ, čitateľ ho nemôže získať), ale ľubovoľný počet čitateľov (ak ho nedrží zapisovateľ). Typické rozhranie read-write zámku vyzerá takto (pozri kernel/defs.h):

Jeden nepatrný problém, ku ktorému pri read-write zámkoch dochádza je, že ak jeden zámok drží veľa čitateľov, zapisovatelia sa k zámku nikdy nemusia dostať. Inými slovami, aj napriek tomu, že čitatelia získavajú a uvoľňujú zámok, nikdy nenastane situácia, pri ktorej zámok nedrží ani jeden čitateľ. Zapisovateľ teda nikdy nemôže získať zámok. Na vyriešenie tohto problému read-write zámky obvykle implementujú schému priority zapisovateľov: akonáhle sa zapisovateľ pokúsi získať zámok, nasledujúci čitatelia musia počkať, až kým zapisovateľ získa a uvoľní zámok (samozrejme, pred tým musí počkať, kým všetci aktuálni čitatelia uvoľnia zámok).

Implementujte read-write spinlock pre xv6 podľa rozhrania a sémantiky popísanej vyššie. Make sure that readers do not starve writers: if there’s any pending writers, no more readers can acquire the lock.

Vyplňte útržky funkcií rozhrania pre read-write spinlock v súbore kernel/spinlock.c a upravte definíciu štruktúry struct rwspinlock v súbore kernel/spinlock.h.

Implementáciu otestujte spustením testu rwlktest. Mali by ste dostať podobný výstup:

Skontrolujte tiež, či usertests -q prejdú. Po dokončení úlohy by mali prejsť všetky testy make grade.

Pomôcky:

  • Najprv si pozrite testovacie systémové volanie sys_rwlktest() v súbore kernel/spinlock.c; implementáciu by ste mali byť schopní overiť priebežne po krokoch.
  • Ak spustíte test rwlktest v xv6 bez úpravy súboru kernel/spinlock.c, kernel vypíše panic: acquire pretože pôvodná implementácia spinlocku povoľuje držať zámok len jednému vláknu naraz. Volania funkcií acquire() a release() v write/read_acquire_inner a write/read_release_inner musíte nahradiť vašou vlastnou implementáciou.
  • Dávajte pozor na možné prelínania operácií. Napríklad, ak čitateľ nevidí žiadneho čakajúceho zapisovateľa a chce získať zámok na čítanie, máte istotu, že môže bezpečne získať zámok?
  • K úlohe si prečítajte manuál o vstavaných funkciách kompilátora gcc pre atomické operácie na adrese https://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html.

Spustite príkaz make grade, aby ste overili správnosť svojho riešenia. 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á úloha

Nie je 🙁