Laborator 4: 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 device_drivers.

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 ./device_drivers/kernel ./device_drivers/extra/char-driver-lin; 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/device_drivers# ls 
extra/   kernel/  user/
root@qemux86:~/skels/device_drivers# ls kernel/
so2_cdev.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

Exerciții

  • Pentru crearea unui modul de kernel folosiți resursele din directorul kernel/.
  • Pentru crearea unui modul de test folosiți resursele din directorul user/.
  • Task-urile vor fi rezolvate prin completarea fișierului kernel/so2_cdev.c cu noi funcții.
  • Urmăriți conținutul fișierul kernel/so2_cdev.c și folosiți macro-urile definite.
  • Citiți cu atenție toate precizările unui exercițiu înainte de a începe rezolvarea acestuia.

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

[0.5p] Intro

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

  • structura file;
  • structura file_operations;
  • constanta generic_ro_fops;
  • funcțiile vfs_read, new_sync_read și generic_file_read_iter;

Urmăriți definiția funcției vfs_read. Observați că pentru un dispozitiv care nu are funcția read și nici funcția read_iter se va apela new_sync_read. Această funcție folosește read_iter care în mod implicit este definită la generic_file_read_iter.

La căutarea simbolurilor în LXR, asiguraţi-vă că selectaţi aceeaşi versiune de kernel ca cea din laborator (4.19).

Pentru cscope, folosiți comanda :cs f g nume pentru căutarea definiţiilor şi comanda :cs f s nume pentru căutarea simbolurilor. Pentru mai multe informaţii despre comenzile disponibile folosiţi comanda :help cscope.

[10.5p] Character Device Driver

1. [2p] Înregistrarea unui dispozitiv de tip caracter

Driver-ul va controla un singur dispozitiv cu majorul MY_MAJOR și minorul MY_MINOR (macro-urile definite în fișierul kernel/so2_cdev.c) Ca prim pas, va trebui să creați fișierul de tip caracter /dev/so2_cdev folosind utilitarul mknod.

Citiți secțiunea Identificator major și minor din laborator.

Pentru a evita folosirea mknod la fiecare boot-are a mașinii, adăugați comanda mknod cu parametrii relevanți la finalul fișierului /etc/rcS din mașina virtuală.

Implementați înregistrarea și deînregistrarea dispozitivului cu numele so2_cdev, respectiv în funcția de intrare și cea de ieșire a modulului. Nu uitați că driver-ul controlează un singur dispozitiv. La acest exercițiu nu este nevoie să folosiți apelurile cdev_init și cdev_add. Le veți folosi la exercițiul 3. Urmăriți comentariile marcate cu TODO 1.

Afișati, folosind pr_info, un mesaj după operațiile de înregistrare, respectiv deînregistrare, care să confirme realizarea cu succes a acestora.

Încărcați apoi modulul în kernel

insmod so2_cdev.ko

și consultați /proc/devices în secțiunea Character devices

cat /proc/devices | less

Identificați tipul de device înregistrat după majorul 42. Rețineți că în /proc/devices apar tipurile de dispozitive (adică majorul) nu și dispozitivele efective (adică minorii).

Intrările în /dev nu sunt create în urma inserării modulului. Acestea se pot crea în două moduri:

  • manual, folosind comanda mknod așa cum vom face în exercițiile următoare
  • automat, folosind udev; nu vom insista pe acest mod în laboratoarele de SO2

Descărcați la final modulul din kernel:

rmmod so2_cdev

2. [1p] Înregistrarea unui dispozitiv alocat

Modificați modulul de kernel pentru a încerca înregistrarea unui dispozitiv deja alocat. Consultați LXR pentru a identifica eroarea întoarsă la înregistrarea modulului.

Reveniți la configurația inițială a modulului.

Consultați /proc/devices pentru a obține un dispozitiv deja alocat.

3. [1p] Implementarea deschiderii și închiderii dispozitivului

Rulați comanda cat peste dispozitivul de tip caracter creat (/dev/so2_cdev). Citirea nu funcționează pentru că driver-ul nu are implementate funcțiile de deschidere, citire și închidere fișier. Urmăriți comentariile marcate cu TODO 2 pentru a realiza următoarele:

  1. Inițializați dispozitivul
  2. Implementați funcțiile open și release în driver.
  3. Afișați un mesaj în funcțiile de tip open și release.
  4. Rulați comanda cat peste dispozitiv după inserarea modulului. Urmăriți mesajele afișate de kernel după rularea comenzii cat. Primiți mesajul de eroare pentru că driverul nu implementează funcția de citire din fișier.

Prototipul operațiilor unui device driver se găsesc în structura file_operations.

Parcurgeți secțiunea open și release.

4. [1.5p] Restricționare acces

Restricționați accesul la dispozitiv cu variabile atomice, astfel încât un singur proces să poată deschide dispozitivul la un moment dat. Restul vor primi eroarea "Device busy" (''-EBUSY''). Restricționarea accesului se va face în funcția open expusă de driver. Urmăriți comentariile marcate cu TODO 3.

  1. Adăugați o variabilă de tip atomic_t în structura dispozitivului.
  2. Inițializați variabila la inițializarea dispozitivului.
  3. Folosiți variabila în funcția open pentru a restricționa accesul la dispozitiv. Recomandăm folosirea atomic_cmpxchg 1).
  4. Resetați variabila în cadrul funcției de tip release, pentru a repermite accesul la dispozitiv.
  5. Pentru a testa implementarea va trebui să simulați o utilizare îndelungată a dispozitivului. Apelați scheduler-ul la sfârşitul deschiderii dispozitivului:
    set_current_state(TASK_INTERRUPTIBLE);
    schedule_timeout(1000);
  6. Testați folosind cat /dev/so2_cdev & cat /dev/so2_cdev.

Înainte de a testa va trebui să fie creată intrarea de tip dispozitiv /dev/so2_cdev.

Avantajul funcției atomic_cmpxchg este că poate verifica vechea valoare a variabilei și seta condiționat o nouă valoare, totul într-o singură operație atomică.

Mai multe detalii despre parametrii funcției găsiți aici.

Un exemplu de folosire găsiți aici.

5. [2.5p] Citirea de mesaje

Implementați funcția read în driver. Urmăriți comentariile marcate cu TODO 4.

  1. Păstrați în cadrul structurii dispozitivului un buffer pe care să îl inițializați la mesajul dat de macro-ul MESSAGE. Inițializarea acestui buffer se va face odată cu inițializarea dispozitivului.
  2. La un apel read copiați în bufferul de user space conținutul buffer-ului din kernel space.
    • Folosiți funcția copy_to_user pentru a copia informații din kernel space în user space.
    • Ignorați pentru moment parametrii size și offset. Puteți presupune că buffer-ul din user space este suficient de mare. Nu este nevoie să verificați validitatea argumentului size al funcției read.
    • Valoarea întoarsă de apelul read este numărul de octeți transmiși din buffer-ul din kernel space către buffer-ul din user space.
  3. După implementare, testați folosind cat /dev/so2_cdev.

Execuția comenzii cat /dev/so2_cdev nu se termină (folosiți Ctrl+C).

Citiți secțiunile read și write și Accesul la spațiul de adresă al procesului din laborator.

Dacă doriți să afișați valoarea offsetului folosiți o construcție de forma:

    printk(LOG_LEVEL "Offset: %lld\n", *offset);

E important modificatorul de afișare %lld însemnând o afișare pentru un tip de date long long int. Tipul de date loff_t (folosit de offset) este un typedef pentru long long int.

Comanda cat citește până la sfârșitul fișierului, iar sfârșitul fișierului e semnalat prin întoarcerea valorii 0 din read. Astfel, pentru o implementare corectă va trebui să actualizați și să folosiți offset-ul primit ca parametru în funcția read și să întoarceți valoarea 0 atunci când utilizatorul a ajuns la sfârșitul buffer-ului.

Modificați driver-ul în așa fel încât rularea cat să se termine.

  1. Folosiți parametrul size.
  2. La fiecare citire actualizați în mod corespunzător parametrul offset.
  3. Asigurați-vă că funcția read întoarce numărul de bytes care au fost copiați în buffer-ul utilizatorului.

Prin dereferențierea parametrului offset se permite citirea și deplasarea poziției curente din fișier. Valoarea acesteia trebuie actualizată de fiecare dată când o citire se efectuează cu succes.

6. [1.5p] Scrierea de mesaje

Adăugați posibilitatea de a scrie un mesaj care să îl înlocuiască pe cel predefinit. Implementați funcția write în driver. Urmăriți comentariile marcate cu TODO 5.

Ignorați pe moment parametrul offset. Puteți presupune că buffer-ul driverului este suficient de mare. Nu este nevoie să verificați validitatea argumentului size al funcției write.

Prototipul operațiilor unui device driver se găsesc în structura file_operations.

Testați folosind comenzile

echo "arpeggio" > /dev/so2_cdev
cat /dev/so2_cdev

Citiți secțiunile read și write și Accesul la spațiul de adresă al procesului din laborator.

7. [1p] Operație de tip ''ioctl''.

Pentru acest exercițiu dorim să adăugăm operația ioctl MY_IOCTL_PRINT care să afișeze mesajul dat de macro-ul IOCTL_MESSAGE din driver. Urmăriți comentariile marcate cu TODO 6.

Pentru aceasta:

  1. Implementați funcția ioctl în driver.
  2. Folosiți printk pentru a afișa mesajul din driver.
  3. Pentru testare, vom folosi un program în user-space (user/so2_cdev_test.c) care să apeleze funcția ioctl cu parametrii corespunzători.

Macrodefiniția MY_IOCTL_PRINT este definită în fișierul include/so2_cdev.h din scheletul laboratorului (folosește _IOC pentru a defini operația). Acest fișier antet este inclus de ambele fișiere sursă (modulul de kernel și programul de test din user space).

Citiți secțiunile ioctl și open și release din laborator.

Codul sursă de user space este compilat automat în momentul în care rulați make build. După make copy, veți găsi executabilul în directorul /home/root/skels/device_drivers/user.

Extra

Urmăriți comentariile marcate cu TODO 7.

1. (2 karma) Ioctl cu transmitere de mesaje Adăugați două operații ioctl pentru modificarea mesajului asociat driver-ului. Folosiți buffer de lungime fixă (BUFFER_SIZE).

  1. Adaugați funcției ioctl din driver operațiile:
    • MY_IOCTL_SET_BUFFER pentru scrierea unui mesaj către dispozitiv;
    • MY_IOCTL_GET_BUFFER pentru citirea unui mesaj de la dispozitiv.
  2. Modificați programul din user-space pentru a permite testarea.

Citiți secțiunile ioctl și Accesul la spațiul de adresă al procesului din laborator.

2. (2 karma) Ioctl cu cozi de așteptare

Adăugați două operații ioctl în device driver pentru lucrul cu cozi de așteptare.

  1. Adaugați funcției ioctl din driver operațiile:
    • MY_IOCTL_DOWN pentru a adăuga procesul într-o coadă de așteptare;
    • MY_IOCTL_UP pentru a scoate procesul dintr-o coadă de așteptare.
  2. Completați structura dispozitivului cu un câmp de tipul wait_queue_head_t și un flag pentru condiția de așteptare.
  3. Nu uitați să inițializați coada de așteptare și flag-ul.
  4. Renunțați la condiția de acces exclusiv de la exercițiul 4.
  5. Modificați programul din user-space pentru a permite testarea.

La adăugarea procesului în coada de așteptare, acesta va rămâne blocat în execuție; pentru rularea comenzii de scoatere din coadă deschideți o nouă consolă în mașina virtuală cu Alt+F2; puteți reveni la consola anterioară cu Alt+F1. Dacă sunteți conectați prin SSH la mașina virtuală deschideți o nouă consolă.

Citiți secțiunile ioctl și Sincronizare - cozi de așteptare din laborator.

3. (2 karma) Implementați flagul O_NONBLOCK.

  • Modificați programul din userspace pentru a permite testarea.

Dacă nu sunt date asociate dispozitivului și fișierul:

  • a fost deschis cu O_NONBLOCK, citirea din el va întoarce -EWOULDBLOCK 2)
  • nu a fost deschis cu O_NONBLOCK, taskul curent va fi pus în așteptare folosind cozi de așteptare

  • Pentru a putea debloca operația de read, renunțați la condiția de exclusivitate pusă la punctul 4.
  • Folosiți dimensiunea datelor pe post de condiție de deblocare din coadă.
  • Puteți să folosiți coada deja declarată la task-ul anterior.
  • Puteți ignora offsetul în cadrul fișierului.
    • Modificați dimensiunea inițiala a datelor reținute la 0, pentru a facilita testarea.
    • Adăugați o nouă operație în userspace care:
      • schimbă flagurile de deschidere (folosind fcntl),
      • face o citere
  • Cu ce flaguri este deschis fișierul atunci când se rulează comanda cat /dev/so2_dev?

Soluții

1) atomic_cmpxchg întoarce vechea valoare a variabilei atomice
2) -EAGAIN
so2/laboratoare/lab04/exercitii.txt · Last modified: 2019/03/12 21:54 by constantin.ghioc
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