User Tools

Site Tools


lab:2015:drivers

Drivers

Un device driver reprezintă o bucată de cod al cărui scop este de a controla o anumită componentă hardware. Avantajele aduse de către un driver provin în principal din oferirea unei interfețe mai simple de comunicare cu hardware-ul, scutind aplicațiile de complexitatea comunicării directe cu acesta. Se pot defini diferite operații de nivel înalt necesare aplicațiilor, care apoi sunt implementate de către driver. Centralizarea acestor operații în driver permite ca ele să fie implementate o singură dată pentru toate aplicațiile, reducând astfel dimensiunea totală a codului și implicit numărul de bug-uri. Interfața de nivel înalt poate fi de asemenea extinsă și la nivelul mai multor componente hardware diferite, făcând posibilă astfel accesarea acestora printr-o interfață comună, oferind astfel o decuplare între aplicații și componentele hardware.

Pe lângă simplificarea comunicării cu dispozitivele hardware un driver poate oferi și o arbitrare a accesului la resursele dispozitivul, un lucru util atunci când mai multe aplicații concurente doresc accesul la același dispozitiv. Fiind plasat într-un punct central între aplicații și hardware, un driver este cel mai în măsură să medieze accesul aplicațiilor la dispozitiv, pentru a preveni conflictele.

În sistemele care oferă separare între codul privilegiat și cel neprivilegiat, un device driver va rula în general în modul privilegiat. Acest lucru este necesar deoarece de obicei comunicarea directă cu dispozitivele hardware necesită execuția de instrucțiuni privilegiate sau accesarea registrelor hardware-ului care, în general, sunt mapate la adrese restricționate pentru codul neprivilegiat.

În kernel-ul Linux implementarea unui device driver se face sub forma unui modul. Astfel, driverul va avea accesul necesar la modul privilegiat și, ca un bonus, el va putea fi încărcat în kernel la cerere, ca orice modul, atunci când serviciile oferite sunt necesare (ex: un dispozitiv nou a fost conectat la sistem). Bineînțeles, tot ca orice modul din Linux, driverul poate fi integrat și în imaginea kernel-ului, dacă se cunoaște că serviciile oferite sunt necesare oricând, sau atunci când sistemul nu poate porni fără componenta controlată de driver.

Din punctul de vedere al Linux driverele se împart în mai multe categorii, în funcție de operațiile care pot fi efectuate de dispozitivul hardware: drivere pentru dispozitive de tip caracter (character devices), drivere pentru dispozitive de tip bloc (block devices), drivere dispozitive de rețea (network devices) etc. De exemplu, deoarece accesarea directă a unui hard-disk se face la nivel de sectoare, vom folosi un driver pentru un dispozitiv de tip bloc (block driver) pentru a expune funcțiile hard-disk-ului pentru restul sistemului. Interfața oferită de acest tip de driver se mapează mai bine pe funcțiile hard-disk-ului decât interfața oferită de un driver pentru un dispozitiv de tip caracter (character driver). Un character driver oferă o interfață generală, pentru accesul nestructurat la hardware, folositoare atunci când dispozitivul nu se încadrează într-una din celelalte categorii (ex: port serial, tastatură, mouse, dispozitive de sunet, controllere I2C etc.)

Character driver

Implementarea unui character driver pentru Linux presupune scrierea unui modul care urmărește aceeași structură folosită și în laboratorul 8. Pentru a putea folosi însă interfața de comunicare specifică unui character driver este necesar ca modulul să se înregistreze ca un character driver și să implementeze anumite operații.

Înregistrare

Acest lucru se face de obicei în funcția de inițializare a modulului printr-un apel la funcția register_chrdev:

int register_chrdev(unsigned int major, const char *name,
                    const struct file_operations *fops);

Aceasta primește ca argumente:

  • major - un număr care identifică tipul de dispozitiv; se poate trimite 0 pentru alocarea dinamică a unui major de către kernel
  • name - un șir de caractere prin care tipul de dispozitiv poate fi mai ușor identificat
  • fops - o structură cu pointeri către funcțiile care implementează interfața driver-ului

și returnează 0, dacă majorul a fost specificat static, sau valoarea alocată pentru major, dacă acesta a fost alocat dinamic. În caz de eroare funcția întoarce un număr negativ care identifică tipul erorii.

Deînregistrare

În mod asemănător, deînregistrarea unui character driver se face în funcția de clean-up a modulului cu funcția unregister_chrdev:

void unregister_chrdev(unsigned int major, const char *name);

Argumentele acestei funcții sunt:

  • major - majorul asociat cu driverul care este înlăturat
  • name - șirul de caractere înregistrat pentru acest driver

iar apelarea ei este necesară pentru ca modulul să poată fi descărcat.

Accesare

Într-un sistem Linux orice dispozitiv este reprezentat în general printr-un fișier disponibil în directorul /dev. Operațiile obișnuite asupra fișierelor: open, read, write, close făcute pe fișierele speciale din /dev sunt mapate în operații asupra dispozitivelor. Crearea fișierului asociat unui dispozitiv nu se face însă automat. Acesta trebuie creat cu ajutorul utilitarului mknod:

mknod <path> <type> <major> <minor>

unde argumentele comenzii reprezintă:

  • <path> - calea fișierului care va fi creat
  • <type> - tipul dispozitivului; se folosește c pentru dispozitive de tip caracter
  • <major> - majorul înregistrat de către driver pentru tipul de dispozitiv
  • <minor> - identifică dispozitivul (dacă există mai multe); se folosește 0 pentru primul dispozitiv

Aflarea majorului pentru un anumit tip de dispozitiv se poate face prin afișarea conținutului fișiserului /proc/devices. Acest fișier conține toate tipurile de dispozitive înregistrate de driverele încărcate în sistem.

Operații

Structura file_operations necesară pentru înregistrarea unui character driver conține operațiile prin care un driver poate comunica cu restul sistemului. Nu este necesar ca driverul să implementeze toate operațiile, ci doar cele care au o semnificație pentru dispozitivul controlat. Cele mai folosite operații sunt:

open

Se execută în momentul în care un proces userspace deschide, cu funcția open sau echivalentă, fișierul special asociat dispozitivului. În cadrul driver-ului această operație este implementată de o funcție cu prototipul:

static int mydevice_open(struct inode *, struct file *);

Argumentele inode și file conțin informații despre fișierul accesat de userspace și pot fi folosite pentru a extrage, printre altele, care dispozitiv a fost accesat în cazul în care un driver deservește mai multe dispozitive. Dacă driverul deservește un singur dispozitiv ele pot fi ignorate. Valoarea de return a funcției va fi întoarsă userspace-ului și poate semnala erori în deschiderea dispozitivului.

Pentru a efectua maparea între un apel open din userspace pe dispozitiv și funcția care trateaza operația open în driver, kernel-ul are nevoie ca adresa acesteia să fie atribuită câmpului .open al structurii fops

release

Se execută în momentul în care un proces userspace închide un fișier de dispozitiv, cu funcția close sau echivalentă. În cadrul driver-ului această operație este implementată de o funcție cu prototipul:

static int mydevice_release(struct inode *, struct file *);

Asemănător cu operația open argumentele inode și file conțin informații despre dispozitivul accesat și sunt folosite atunci când driverul deservește mai multe dispozitive, iar valoarea de return poate transmite informații userspace-ului despre succesul operației.

Pentru a efectua maparea între un apel close din userspace pe dispozitiv și funcția care trateaza operația release în driver, kernel-ul are nevoie ca adresa acesteia să fie atribuită câmpului .release al structurii fops.

read

Se execută în momentul în care un proces userspace citește dintr-un fișier de dispozitiv, cu funcția read sau echivalentă. În cadrul driver-ului această operație este implementată de o funcție cu prototipul:

static int mydevice_read(struct file *file, char __user *user_buffer, size_t size, loff_t *offset);

Argumentele importante ale acestei funcții sunt:

  • user_buffer - pointerul către zona de memorie unde procesul userspace așteaptă să fie copiate datele citite
  • size - dimensiunea zonei de memorie alocată de procesul userspace pentru primirea datelor; cantitatea datelor returnate de driver nu trebuie să depășească această valoare
  • offset - un offset pentru memorarea poziției în stream-ul de date returnate de driver; trebuie incrementat de către driver cu cantitatea de date returnată și este folosit de userspace pentru a putea citi un buffer prin mai multe apeluri read

Valoarea de return este folosită pentru a transmite procesului userspace dimensiunea citită efectiv.

Pentru a efectua maparea între un apel read din userspace pe dispozitiv și funcția care trateaza operația read în driver, kernel-ul are nevoie ca adresa acesteia să fie atribuită câmpului .read al structurii fops.

Un lucru important care trebuie avut în vedere la implementarea operației read este că pointerul și dimensiunea transmise de userspace s-ar putea să fie invalide. Dacă ele sunt folosite fără a fi verificate, driver-ul ar putea accesa involuntar zone inaccesibile de memorie, ceea ce va duce la un kernel panic. Pentru a rezolva această problemă și pentru a copia datele în zona de memorie a userspace-ului se pot folosi funcțiile copy_to_user sau put_user.

write

Se execută în momentul în care un proces userspace scrie într-un fișier de dispozitiv, cu funcția write sau echivalentă. În cadrul driver-ului această operație este implementată de o funcție cu prototipul:

static int mydevice_write(struct file *file, const char __user *user_buffer, size_t size, loff_t *offset);

Argumentele importante ale acestei funcții sunt:

  • user_buffer - pointerul către zona de memorie unde procesul userspace a pus datele pentru scriere
  • size - dimensiunea zonei de memorie care conține datele ce vor fi scrise; driverul nu este obligat să scrie toată cantitatea
  • offset - un offset pentru memorarea poziției în stream-ul de date; trebuie incrementat de către driver cu cantitatea de date scrisă și este folosit de userspace pentru a putea efectua o scriere prin mai multe apeluri write

Valoarea de return este folosită pentru a transmite procesului userspace dimensiunea scrisă efectiv.

Pentru a efectua maparea între un apel write din userspace pe dispozitiv și funcția care trateaza operația write în driver, kernel-ul are nevoie ca adresa acesteia să fie atribuită câmpului .write al structurii fops.

La fel ca la operația read, un lucru important care trebuie avut în vedere este că pointerul și dimensiunea transmise de userspace s-ar putea să fie invalide. Dacă ele sunt folosite fără a fi verificate driver-ul ar putea accesa involuntar zone inaccesibile de memorie, ceea ce va duce la un kernel panic. Pentru a rezolva această problemă și pentru a copia datele din zona de memorie a userspace-ului se pot folosi funcțiile copy_from_user sau get_user.

Exerciții

  • 1. Implementati un device driver pentru dispozitive de tip caracter care să aprinda un LED la scrierea caracterului 1 și să stingă LED-ul la scrierea caracterului 0. Dispozitivul trebuie să poată fi controlat de un singur proces la un moment dat.
    • a. (2p) Implementați înregistrarea și deînregistrarea modulului ca driver pentru dispozitivele de tip caracter.

Alocați majorul dinamic și folosiți numele led_act pentru tipul de dispozitiv.

  • b. (2p) Implementați restricționarea accesului la dispozitiv pentru un singur proces la un moment dat.

Folosiți-vă de operațiile open și release. Puteți întoarce codul de eroare -EPERM pentru refuzarea accesului.

  • c. (2p) Implementați stingerea și aprinderea LED-ului ACT la scrierea caracterelor '0', respectiv '1'.

Nu accesați direct pointer-ul trimis de userspace. Acesta poate fi NULL cea ce va duce la un acces invalid în kernelspace și în mod implicit la un kernel panic.

  • d. (1p) Implementați citirea stării LED-ului din userspace: pentru LED aprins se va returna string-ul "on", iar pentru LED stins se va returna string-ul "off".

Nu scrieți direct în pointer-ul oferit de userspace. Acesta poate pointa către memoria protejată a kernel-ului ceea ce ar duce la modificarea structurilor interne ale kernel-ului sub controlul unui proces neprivilegiat.

Folosiți funcția copy_to_user pentru a copia în siguranță datele în buffer-ul din userspace

  • e. (1p) Creați fișierul special aferent dispozitivului controlat de către driver-ul vostru.

Fișierele speciale pot fi create doar de către root și implicit doar root are drepturi pe el. Modificați drepturile de acces astfel încât orice utilizator să poată scrie și citi fișierul.

Puteți testa funcționarea modulului cu utilitarele echo și cat.

  • 2. (2p) Implementați o aplicație userspace care aprinde și stinge LED-ul periodic, cu frecvența de 1Hz.

Resurse

Referințe

lab/2015/drivers.txt · Last modified: 2016/10/13 20:47 by Dan Dragomir