Laborator 6: Exerciții

Pentru desfășurarea laboratorului pornim de la arhiva de sarcini a laboratorului. Descărcăm și decomprimăm arhiva în directorul so2/ din directorul home al utilizatorului student de pe sistemul de bază (stația asgard):

student@asgard:~$ cd so2/
student@asgard:~/so2$ wget http://elf.cs.pub.ro/so2/res/laboratoare/lab06-tasks.zip
student@asgard:~/so2$ unzip lab06-tasks.zip
student@asgard:~/so2$ tree lab06-tasks

În cadrul directorului lab06-tasks/ se găsesc resursele necesare pentru dezvoltarea exercițiilor de mai jos: fișiere schelet de cod sursă, fișiere Makefile și Kbuild, scripturi și programe de test.

Vom dezvolta exercițiile pe sistemul de bază (stația asgard) și apoi le vom testa pe mașina virtuală QEMU. După editarea și compilarea unui modul de kernel îl vom copia în directorul dedicat pentru mașina virtuală QEMU folosind o comandă de forma

student@asgard:~/so2$ cp /path/to/module.ko ~/so2/qemu-so2/fsimg/root/modules/

unde /path/to/module.ko este calea către fișierul obiect aferent modulului de kernel. Apoi vom porni, din directorul ~/so2/qemu-so2/, mașina virtuală QEMU folosind comanda

student@asgard:~/so2/qemu-so2$ make

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

# insmod modules/module-name.ko
# rmmod module/module-name

unde module-name este numele modulului de kernel.

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

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/ din arhiva de sarcini a laboratorului 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/ din arhiva de sarcini a laboratorului. 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 arhiva de sarcini. 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).

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 arhiva de sarcini a 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 karma î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: 2017/03/29 12:49 by george.muraru
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