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
).