Laborator 3: Exerciții

Pregătirea laboratorului

Pentru rezolvarea laboratorului, vom lucra în același director din care pornim mașina virtuală (~/so2/linux/tools/labs).

Pașii de rezolvare sunt următorii:

  • pregătirea scheletului de laborator
  • compilarea modulelor de Kernel
  • copierea modulelor pe mașina virtuală
  • pornirea mașinii virtuale și testarea modulelor

Pregătirea scheletului de laborator

Scheletul de laborator este generat din sursele din directorul tools/labs/templates. Putem genera scheletele pentru toate laboratoarele folosind următoarea comanda:

tools/labs $ make skels

Pentru a genera scheletul pentru un singur laborator, vom folosi variabila de mediu LABS:

tools/labs $ make clean
tools/labs $ LABS=<lab name> make skels

Numele laboratorului curent este kernel_api.

Similar, putem genera și scheletul pentru un singur exercițiu, atribuind valoarea <lab_name>/<task_name> variabilei LABS.

Scheletul este generat în directorul tools/labs/skels.

Compilarea modulelor

Comanda make build compilează toate modulele din directorul skels.

student@eg106:~/so2/linux/tools/labs$ make build
echo "# autogenerated, do not edit " > skels/Kbuild
echo "ccflags-y += -Wno-unused-function -Wno-unused-label -Wno-unused-variable " >> skels/Kbuild
for i in ./kernel_api/7-list-test ./kernel_api/2-sched-spin ./kernel_api/1-mem ./kernel_api/6-list-sync ./kernel_api/5-list-full ./kernel_api/4-list ./kernel_api/3-memory; do echo "obj-m += $i/" >> skels/Kbuild; done
...

Copierea modulelor pe mașina virtuală

Putem copia modulele generate pe mașina virtuală folosind target-ul copy al comenzii make, atunci când mașina virtuală este oprită.

student@eg106:~/so2/linux/tools/labs$ make copy
student@eg106:~/so2/linux/tools/labs$ make boot

Alternativ, putem copia fișierele prin scp, pentru e evita repornirea mașinii virtuale. Pentru detalii despre folosirea interacțiunea prin rețea cu mașina virtuală citiți Interacțiunea cu mașina virtuală.

Testarea modulelor

Modulele generate sunt copiate pe mașina virtuală în directorul /home/root/skels/<lab_name>/<task_name>.

root@qemux86:~/skels/kernel_api# ls
1-mem         3-memory      5-list-full   7-list-test
2-sched-spin  4-list        6-list-sync
root@qemux86:~/skels/kernel_api$ ls 1-mem
mem.ko

După pornirea mașinii virtuale QEMU vom putea folosi comenzi în fereastra QEMU (sau în minicom) pentru a încărca și descărca modulul de kernel:

root@qemux86:~# insmod skels/<lab_name>/<task_name>/<module_name>.ko 
root@qemux86:~# rmmod skels/<lab_name>/<task_name>/<module_name>.ko 

Pentru dezvoltarea laboratorului, este recomandat să folosim trei terminale sau, mai bine, trei tab-uri de terminal. Pentru a deschide un nou tab de terminal folosim combinația de taste Ctrl+Shift+t. Cele trei tab-uri de terminal îndeplinesc următoarele roluri:

  1. În primul tab de terminal dezvoltăm modulul de kernel: editare, compilare, copiere în directorul dedicat pentru mașina virtuală QEMU. Lucrăm în directorul aferent rezultat în urma generării scheletului de laborator, din ~/so2/linux/tools/labs/skels.
  2. În al doilea tab de terminal pornim mașina virtuală QEMU și apoi testăm modulul de kernel: încărcare/descărcare modul, rulare teste. Lucrăm în directorul aferent mașinii virtuale: ~/so2/linux/tools/labs.
  3. În al treilea tab de terminal pornim minicom sau un server UDP care să primească mesajele de netconsole. Nu contează în ce director ne aflăm. Folosim comanda
    student@asgard:~$ nc -l -p 6000 -u

Citiți cu atenție toate precizările unui exercițiu înainte de a începe rezolvarea acestuia.

[0.5p] Intro

Identificați, folosind cscope, definițiile următoarelor simboluri:

  • structura list_head;
  • funcția INIT_LIST_HEAD;
  • funcția list_add;
  • macro-ul list_for_each;
  • macro-urile list_entry, container_of și offsetof. Pentru offsetof, găsiți varianta generică, independentă de compilator.

Observați detaliile de implementare ale macro-urilor și funcțiilor.

Pentru mai multe detalii despre cscope parcurgeți secțiunea cscope din primul laborator.

[4.5p] Memoria

1. [1p] Alocarea de memorie în Linux

Înainte de rezolvarea exercițiilor, generați scheletul de laborator. Această operație este descrisă mai sus.

Intrați în directorul 1-mem/ și parcurgeți conținutul fișierului mem.c. Observați folosirea apelului kmalloc pentru alocare de memorie.

  1. Compilați codul sursă într-un modul de kernel și copiați-l pe mașina virtuală, conform informațiilor de mai sus (make build și make copy).
  2. Încărcați modulul în kernel folosind comanda insmod mem.ko.
  3. Vizualizați mesajele nucleului folosind comanda dmesg.
  4. Descărcați modulul din kernel folosind comanda rmmod mem.

Revedeți secțiunea Alocare memorie din laborator.

2. [1p] Folosirea de operații blocante în context atomic

Intrați în directorul 2-sched-spin/ și parcurgeți conținutul fișierului sched-spin.c. Hint: Funcția schedule_timeout, coroborată cu macro-ul set_current_state, forțează procesul curent să aștepte 5 secunde.

  1. Compilați codul sursă într-un modul de kernel și copiați-l pe mașina virtuală, conform informațiilor de mai sus (make build și make copy).
  2. Încărcați modulul în kernel folosind comanda insmod sched-spin.ko.
    • Observați că se așteaptă 5 secunde până la definitivarea comenzii de inserare.
  3. Descărcați modulul din kernel.
  4. Modificați modulul în locurile marcate cu TODO 0, astfel încât să includeți operația de sleep într-o secțiune atomică, folosind spinlock-ul deja definit. Compilați din nou codul sursă și reîncărcați modulul în kernel. Hint: spin_lock și spin_unlock.

Ați obținut eroare. Urmăriți stack trace-ul. Care este cauza erorii? Hint: Urmăriți, în mesajul de eroare, linia care conține BUG pentru o descriere a erorii. Nu aveți voie să realizați operații blocante în context atomic. Contextul atomic este dat de o secțiune aflată între o operație de lock și una de unlock pe un spinlock.

Revedeți secțiunile Contexte de execuție, Locking și Spinlock-uri din laborator.

3. [2.5p] Alocarea și lucrul cu memoria în kernel

Intrați în directorul 3-memory/ și parcurgeți conținutul fișierului memory.c. Observați comentariile marcate cu TODO. Trebuie să alocați 4 structuri de tipul struct task_info și să le inițializați (în funcția memory_init), apoi să le afișați și dezalocați (în funcția memory_exit).

  1. (TODO 1) Alocați structura struct task_info și inițializați câmpurile acesteia:
    • câmpul pid la PID-ul transmis ca parametru;
    • câmpul timestamp la valoarea variabilei jiffies, care menține timpul de activitate al sistemului.
      • Hint: Detalii despre variabila jiffies se găsesc în laboratorul 6.
  2. (TODO 2) Folosiți funcția completată anterior pentru a inițializa cele 4 structuri cu următoarele informații:
    • PID-ul procesului curent, dat de macro-ul current, de tipul struct task_struct *.
      • Hint: Căutați câmpul relevant pentru PID în structura task_struct.
    • PID-ul procesului părinte al procesului curent.
      • Hints:
        • Căutați câmpul relevant din structura task_struct.
        • Căutați după șirul “parent”.
    • PID-ul procesului următor din lista de procese.
      • Hints:
        • Folosiți macro-ul next_task.
        • Macro-ul întoarce pointer-ul la următorul proces, adică de tipul struct task_struct *.
    • PID-ul următorului proces după următorul.
      • Hint: Folosiți de două ori macro-ul next_task.
  3. (TODO 3) Afișați cele patru structuri.
    • Folosiți printk pentru a afișa cele două câmpuri ale acestora: pid și timestamp.
  4. (TODO 4) Eliberați spațiul ocupat de structuri.
    • Folosiți kfree.

Structura task_struct conţine două câmpuri pentru a desemna părintele unui task:

  • real_parent indică spre procesul care a creat taskul respectiv sau spre procesul 1 (init) dacă părintele şi-a încheiat execuţia.
  • parent indică spre părintele curent al taskului (procesul care va fi semnalat în cazul în care taskul îşi încheie execuţia).

În general valorile celor două câmpuri sunt identice, dar apar şi situaţii când ele diferă, de exemplu atunci când se foloseşte apelul de sistem ptrace.

Revedeți secțiunea Alocare memorie din laborator.

[6p] Liste

4. [2p] Lucrul cu liste în kernel

Intrați în directorul 4-list/. Parcurgeți conținutul fișierului list.c și observați comentariile marcate cu TODO. Procesul curent va adăuga cele patru structuri menționate anterior într-o listă. Lista va fi construită în funcția list_init și în funcția task_info_add_for_current. Lista va fi afișată în funcția task_info_print_list și ștearsă în funcția task_info_purge_list, ambele fiind apelate din list_exit.

  1. (TODO 1) Completați funcția task_info_add_to_list pentru a aloca o structură struct task_info și a o adăuga la listă. Funcția este deja apelată de 4 ori din task_info_add_for_current.
  2. (TODO 2) Completați funcția task_info_purge_list pentru a șterge toate elementele din listă.
  3. Compilați modulul de kernel și copiați-l pe mașina virtuală. Încărcați și descărcați modulul urmărind mesajele afișate de kernel.

Revedeți secțiunea Liste din laborator.

Atunci când ștergeți elemente din listă, va trebui să folosiți apelul list_for_each_safe. Puteți folosi și apelul list_for_each_entry_safe.

5. [2p] Lucrul cu liste în kernel pentru prelucrarea proceselor

Intrați în directorul 5-list-full/. Parcurgeți conținutul fișierului list-full.c și observați comentariile marcate cu TODO. Pe lângă funcționalitățile de la 4-list vom adăuga următoarele:

  • Un câmp count care arată de câte ori a fost “adăugat” un proces.
    • Dacă un proces este “adăugat” de mai multe ori, nu se creează o nouă intrare în listă, ci:
      • Se actualizează câmpul timestamp.
      • Se incrementează contorul count.
  • Pentru implementarea facilității de contor, se adaugă o funcție task_info_find_pid care caută un pid în lista existentă.
    • Dacă este găsit se întoarce referința la structura task_info. Dacă nu, se întoarce NULL.
  • O facilitate de expirare.
    • Dacă un proces nu este “adăugat” în vreme de 3 secunde și dacă nu are contor mai mare de 5, atunci este considerat eliminat și este extras din listă.
    • Facilitatea de expirare este implementată deja în funcția task_info_remove_expired.
  1. (TODO 1) Implementați funcția task_info_find_pid așa cum este descris mai sus și în comentariul funcției.
  2. (TODO 2) Modificați un câmp al unui element din listă astfel încât acesta să nu expire. Hint: Trebuie să nu satisfacă o parte a condiției de expirare din funcția task_info_remove_expired.
  3. Compilați, copiați, încărcați și descărcați modulul urmărind mesajele afișate. Va dura încărcarea modulului întrucât este prezent un “sleep” prin intermediul funcției schedule_timeout.

Pentru TODO 2 extrageți primul element din listă (cel referit de head.next) și puneți câmpul count al structurii de tip ti aferente pe o valoare suficient de mare (10) folosind funcția atomic_set.

6. [1p] Sincronizarea lucrului cu liste

Intrați în directorul 6-list-sync/.

  1. Fișierul 6-list-sync/list-sync.c conține aceeași funcționalitate ca cea adăugată la exercițiul anterior.
  2. Folosiți un spinlock sau un read-write lock pentru a sincroniza accesul la lista folosită. Urmăriți comentariile marcate cu TODO.
  3. Compilați, copiați, încărcați și descărcați modulul de kernel.

Parcurgeți secțiunea Spinlock-uri din laborator.

7. [1p] Testare modul de lucru cu liste

Intrați în directorul 7-list-test/ și parcurgeți conținutul fișierului list-test.c. Vom folosi un modul de testare. Acesta va apela funcții exportate de modulul de la 6-list-sync/. Funcțiile exportate sunt cele descrise cu extern în cadrul fișierului list-test.c.

Pentru exportarea funcțiilor de mai sus, din modulul de la 6-list-sync/ este nevoie de următorii pași:

  1. Funcțiile nu trebuie să fie statice.
  2. Se folosește macro-ul EXPORT_SYMBOL pentru a exporta simbolurile în kernel. De exemplu: EXPORT_SYMBOL(task_info_remove_expired);. Macro-ul trebuie folosit pentru fiecare funcție, după definirea funcției.
  3. Eliminați din modulul de la 6-list-sync/ secvența cu evitarea expirării unui element al listei (se bate cap în cap cu ce face testul).
  4. Compilați și încărcați modulul de la 6-list-sync/. Odată încărcat va expune funcțiile exportate și vor putea fi folosite de modulul de test. Puteţi verifica acest lucru căutând numele funcţiilor în /proc/kallsyms înainte şi după încărcarea modulului.
  5. Compilați modulul de test și apoi încărcați-l.
  6. Folosiţi lsmod ca să verificaţi că cele două module s-au încărcat. Ce observaţi?
  7. Descărcați modulul de test din kernel.

Care trebuie să fie ordinea de descărcare a celor două module (cel din 6-list-sync/ și cel de test)? Ce se întâmplă dacă folosiți altă ordine?

Soluții

so2/laboratoare/lab03/exercitii.txt · Last modified: 2018/03/06 23:09 by anda.nicolae
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0