Laborator 4: 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/lab04-tasks.zip
student@asgard:~/so2$ unzip lab04-tasks.zip
student@asgard:~/so2$ tree lab04-tasks

În cadrul directorului lab04-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

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

[0.5p] Intro

Identificați, folosind cscope sau LXR, 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.9).

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. [1p] Î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 qemu-vm/fsimg/etc/rcS.

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.

Afișati, folosind macro-ul LOG_LEVEL, 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.

  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.

  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.

  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.

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. [2p] 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.

Pentru aceasta:

  1. Implementați funcția ioctl în driver.
  2. Trebuie să scrieți un program în user-space (user/so2_cdev_test.c) care să apeleze funcția ioctl cu parametrii corespunzători. În fișierul de testare trebuie să apelați ioctl pentru fișierul dispozitivului.
  3. Folosiți printk pentru a afișa mesajul din driver.

Macrodefiniția MY_IOCTL_PRINT este definită în fișierul include/so2_cdev.h din arhiva de sarcini a laboratorului (folosește _IOC pentru a defini operația)

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

Pentru a compila codul sursă de user space folosiți compilatorul gcc-5. Pentru aceasta rulați comanda:

/usr/bin/gcc-5 -m32 -static -Wall -g -o so2_cdev_test so2_cdev_test.c

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

Extra

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: 2017/03/13 16:30 by octavian.purdila
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