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:
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
deferred_work.
Similar, putem genera și scheletul pentru un singur exercițiu, atribuind valoarea <lab_name>/<task_name> variabilei LABS.
tools/labs/skels.
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 ...
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ă.
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
Ctrl+Shift+t. Cele trei tab-uri de terminal îndeplinesc următoarele roluri:
~/so2/qemu-so2/.student@asgard:~$ netcat -lup 6666
Găsiți definițiile următoarelor simboluri în nucleul Linux:
jiffies;timer_list;spin_lock_bh;
git pull --rebase in directorul ~/so2/linux, pentru a obține ultima versiune a scheletului de laborator.
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.
printk(LOG_LEVEL ... ). Mesajele vor fi afișate la consolă și pot fi vizualizate și folosind dmesg și consola de netconsole.
jiffies. Pentru a indica timpul după TIMER_TIMEOUT secunde folosim construcția jiffies + TIMER_TIMEOUT * HZ.
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.
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.3-4-5-deferred/user) direct o valoare, nu un pointer.MY_IOCTL_TIMER_CANCEL pentru dezactivarea timer-ului.
jiffies. Pentru a indica timpul după N secunde folosim construcția jiffies + N * HZ.
ioctl revedeți secțiunea aferentă din laboratorul 4.
Anularea unui timer este echivalentă cu apelarea mod_timer(..., 0).
În rutina de tratare a timer-ului, afișați identificatorul procesului curent (PID-ul) și numele imaginii de executabil.
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.
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.
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.
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).
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); ... }
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.
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).
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.
Parcurgeți secțiunea Kernel threads din laborator.
Thread-ul se va sincroniza cu funcția de descărcare a modulului. Adică:
Pentru sincronizare folosiți două cozi de așteptare și două flag-uri aferente.
Pentru flag-uri folosiți variabile atomice. Pentru modul de utilizare al variabilelor atomice revedeți Laboratorul 3.
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.
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.
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.
od -s /dev/deferred
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).