Laborator 6: 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 deferred_work.

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 ./deferred_work/6-kthread ./deferred_work/1-2-timer ./deferred_work/3-4-5-deferred/kernel; 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>.

root@qemux86:~/skels/deferred_work# ls
1-2-timer       3-4-5-deferred  6-kthread
root@qemux86:~/skels/deferred_work# ls 1-2-timer/
timer.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 decomprimării arhivei de sarcini a laboratorului.
  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/qemu-so2/.
  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:~$ netcat -lup 6666

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

[0.5p] Intro

Găsiți definițiile următoarelor simboluri în nucleul Linux:

  • definiția lui jiffies;
  • structura timer_list;
  • funcția spin_lock_bh;

[10.5p] Exerciții

Înainte de începerea rezolvării laboratorului, rulați comanda git pull --rebase in directorul ~/so2/linux, pentru a obține ultima versiune a scheletului de laborator.

1. [2p] Timer

Urmărim crearea unui modul simplu de kernel care să afișeze un mesaj la TIMER_TIMEOUT secunde de la încărcarea modulului în kernel.

În directorul 1-2-timer/ este scheletul de cod timer.c de unde să porniți pentru implementare. Urmăriți secțiunile marcate cu TODO 1 în scheletul de laborator.

Pentru afișare folosiți printk(LOG_LEVEL ... ). Mesajele vor fi afișate la consolă și pot fi vizualizate și folosind dmesg și consola de netconsole.

Planificarea pentru rularea timer-ului se face folosind timpul absolut al sistemului în număr de tick-uri. Timpul absolut al sistemului în număr de tick-uri este dat de variabila jiffies. Pentru a indica timpul după TIMER_TIMEOUT secunde folosim construcția jiffies + TIMER_TIMEOUT * HZ.

Parcurgeți secțiunea Timere din laborator.

2. [1p] Timer periodic

Ne propunem realizarea unui timer periodic.

Modificați modulul anterior pentru a afișa mesajul în dmesg o dată la fiecare TIMER_TIMEOUT secunde. Urmăriți secțiunea marcată cu TODO 2 în scheletul de laborator.

3. [2p] Controlare timer folosind ioctl

Ne propunem să afișăm informații despre procesul curent după N secunde de la primirea unei comezi de tipul ioctl apelată din user space. N este transmis ca paramentru prin ioctl.

Porniți de la fișierele din subdirectorul 3-4-5-deferred/kernel/. Urmăriți secțiunile marcate cu TODO 1 în scheletul de laborator.

Va trebui să implementați următoarele operații ioctl.

  • MY_IOCTL_TIMER_SET pentru planificarea unui timer să ruleze dupa un număr de secunde primit ca argument de rutina ioctl. Timer-ul nu rulează periodic.
    • Această comandă primește ca argument din user space (din programul de test din 3-4-5-deferred/user) direct o valoare, nu un pointer.
  • MY_IOCTL_TIMER_CANCEL pentru dezactivarea timer-ului.

Planificarea pentru rularea timer-ului se face folosind timpul absolut al sistemului în număr de tick-uri. Timpul absolut al sistemului în număr de tick-uri este dat de variabila jiffies. Pentru a indica timpul după N secunde folosim construcția jiffies + N * HZ.

Pentru modalitatea de acces a unui argument de ioctl revedeți secțiunea aferentă din laboratorul 4.

Anularea unui timer este echivalentă cu apelarea mod_timer(..., 0).

Parcurgeți secțiunea Timere din laborator pentru informații legate de activarea/dezactivarea unui timer.

În rutina de tratare a timer-ului, afișați identificatorul procesului curent (PID-ul) și numele imaginii de executabil.

Identificatorul procesului curent îl puteți afla folosind construcția current->pid, iar imaginea de executabil folosind construcția current->comm. Pentru detalii, revedeți Laboratorul 2.

Pentru a putea folosi device driver-ul din user-space, trebuie sa creați fișierul pentru dispozitivul de tip caracter /dev/deferred folosind utilitarul mknod. Alternativ, puteți rula scriptul makenode din 3-4/deferred/kernel/, care realizează aceste operații.

Scriptul makenode trebuie copiat în mașina virtuală QEMU pentru a-l rula acolo.

Activați și dezactivați timer-ul prin apelul operațiilor ioctl din user-space. Utilizați programul 3-4-5-deferred/user/test/ din scheletul laboratorului. Rulați programul pentru a testa planificarea și dezactivarea unui timer. Programul primește ca parametri în linie de comandă operația de tip ioctl și parametrii acesteia (dacă e cazul).

Executabilul rezultat (test) trebuie să îl copiați pe mașina virtuală QEMU la fel ca modulul de kernel și să îl rulați pe mașina virtuală pentru a valida implementarea corectă a ioctl.

Rulați executabilul rezultat (test) fără argumente pentru a observa opțiunile de rulare în linia de comandă ale acestuia.

Pentru a activa timer-ul la 3 secunde folosind executabilul test pe mașina virtuală QEMU folosiți comanda

./test s 3

Pentru a dezactiva timer-ul folosind executabilul test pe mașina virtuală QEMU folosiți comanda

./test c

Observați că la fiecare rulare a timer-ului procesul afișat este swapper/0 cu PID-ul 0. Procesul swapper/0 este procesul idle pe un sistem Linux. E procesul rulat când nu există altceva de rulat. Întrucât sistemul de operare de pe mașina virtuală nu face mare lucru, este natural că procesul swapper/0 va rula mai tot timpul. Iar rutina de rulare a timer-ului rulează foarte aproape de momentul întreruperii de ceas, care va întrerupe procesul care rulează atunci pe procesor, adică swapper/0.

4. [1.5p] Operații blocante

Urmărim să vedem ce se întâmplă atunci când într-o rutină de tratare a unui timer realizăm operații blocante. Pentru aceasta urmărim să apelăm în rutine de tratare a timer-ului o funcție numită alloc_io() care simulează o operație blocantă.

Modificați modulul astfel încât la primirea comenzii MY_IOCTL_TIMER_SET să se păstreze funcționalitatea de la task-ul numărul 3, iar la primirea comenzii MY_IOCTL_TIMER_ALLOC rutina de tratare a timerului să apeleze funcția alloc_io(). Urmăriți secțiunile marcate cu TODO 2 în scheletul de laborator.

Folosiți același timer. Pentru a diferenția funcționalitățile în rutina de tratare a timer-ului, folosiți un flag în structura device-ului. Pentru valorile pe care le poate lua flag-ul folosiți constantele TIMER_TYPE_ALLOC și TIMER_TYPE_SET definite în scheletul de cod. Pentru inițializare folosiți TIMER_TYPE_NONE.

Rulați programul de test pentru a verifica funcționalitatea de la task-ul 3. Rulați din nou programul de test pentru a apela funcția alloc_io().

Observați că programul cauzează eroare pentru că se apelează o funcție blocantă în context atomic (handler-ul de timer rulează în context amânabil/întrerupere).

Pentru a obține un pointer la datele private ale modulului (structura my_device_data), puteți folosi macro-ul from_timer, care este similar cu container_of.

static void timer_handler(struct timer_list *tl)
{
    struct my_device_data *data = from_timer(data, tl, timer);
    ...
}

5. [1.5p] Workqueues

Vom modifica modulul pentru a preîntâmpina eroarea observată la task-ul numărul 4.

Pentru aceasta apelați funcția alloc_io() folosind workqueues. Veți planifica un work din rutina de tratare a timer-ului care va fi planificat pentru rulare în context proces. În work handler (care rulează în context proces) veți apela funcția alloc_io(). Urmăriți secțiunile marcate cu TODO 3 în scheletul de laborator.

Adăugați un câmp work de tipul struct work_struct în structura de dispozitiv. Inițializați acest câmp. Submiterea work-ului după N secunde o veți face din rutina de tratare a timer-ului, folosind funcția schedule_work. Timer-ul va fi planificat să ruleze după N secunde, rutina de tratare a timer-ului va planifica work-ul și acesta va rula cât mai aproape de acel moment. Nu folosiți funcția schedule_delayed_work_on.

Folosiți workqueue-ul implicit (adică nu creați un workqueue, adică nu apelați create_workqueue).

Parcurgeți secțiunea Workqueues și secțiunea Timere din laborator.

6. [2.5p] Kernel thread

Dorim să creăm un modul simplu în care să creăm un kernel thread cu ajutorul căruia să afișăm identificatorul procesului curent.

Porniți de la fișierele din subdirectorul 6-kthread/ din scheletul laboratorului și urmăriți TODO-urile din codul sursă.

Veți crea și veți porni kernel thread-ul la încărcarea modulului.

Pentru crearea unui thread aveți două opțiuni:

Parcurgeți secțiunea Kernel threads din laborator.

Thread-ul se va sincroniza cu funcția de descărcare a modulului. Adică:

  • Thread-ul își va încheia execuția doar după ce funcția de descărcare a modulului a fost apelată.
  • Funcția de descărcare a modulului își va încheia execuția numai după ce thread-ul și-a încheiat execuția.

Pentru sincronizare folosiți două cozi de așteptare și două flag-uri aferente.

Pentru modalitatea de utilizare a cozilor de așteptare, revedeți Laboratorul 4.

Pentru flag-uri folosiți variabile atomice. Pentru modul de utilizare al variabilelor atomice revedeți Laboratorul 3.

Extra

[3 karma] Buffer partajat între timer și proces

Dorim să ne acomodăm cu modul de sincronizare între o rutină amânabilă (un timer) și contextul proces.

Pentru aceasta, dorim să colectăm informații periodice despre procesul idle (swapper/0), procesul cu PID-ul 0, pe care să le scriem într-un buffer. Informațiile le vom colecta în momentul în care este invocată rutina de tratare a timer-ului. Informația colectată va fi numărul de schimbări involuntare de până atunci ale procesului idle. Acest număr este dat de câmpul nivcsw (de tipul unsigned long) din cadrul structurii task_struct, adică folosind current->nivcsw.

În rutina de timer, dacă la orice moment de timp procesul întrerupt este procesul idle (adică are PID-ul 0), se stochează într-un buffer o nouă valoarea a numărului de schimbări involuntare. Dacă buffer-ul este plin nu se stochează nimic.

Citirea datelor o veți face la fiecare secundă dacă flag-ul dispozitivului este inițializat la valoarea TIMER_TYPE_ACCT. Pentru inițializarea flag-ului veți comanda din user space dispozitivul (prin ioctl) folosind executabilul de test, după încărcarea modulului și crearea dispozitivului, astfel:

./test t

În rutina de read a modulului se transferă în user space datele din buffer și se resetează buffer-ul.

Folosiți un spinlock și operațiile corespunzătoare pentru a asigura accesul corect la buffer din rutina de timer și din rutina de read.

Veți folosi un buffer temporar pentru a copia datele în user space. Nu puteți copia date în user space cu un spinlock deținut; rutina copy_to_user poate fi blocantă.

Spinlock-ul, bufferul și bufferul temporar folosit pentru copiere sunt definite în structura dispozitivului.

O parte din rutina de read este implementată; trebuie să implementați partea în care se face sincronizarea.

În implementare porniți de la codul din subdirectorul 3-4-5-deferred/. Urmăriți secțiunile marcate cu TODO 4 în scheletul de laborator.

Pentru a testa modulul de kernel space puteți citi date din dispozitiv folosind comanda

od -s /dev/deferred

[1 karma] Citire date din user space folosind read

Actualizați modulul de test din user space (user/test.c) ca să ofere opțiunea r. În momentul în care se transmite opțiunea către modulul de test acesta citește conținutul buffer-ului din kernel space folosind apelul read() și afișează în format zecimal (%lu) rezultatele primite (vector de unsigned long).

Soluții

so2/laboratoare/lab06/exercitii.txt · Last modified: 2018/03/28 09:17 by ionel.ghita
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