Device driver: di cosa si tratta e come scriverne uno?
Un device driver è un software che consente il controllo di una periferica. In questo breve tutorial, vediamo come approcciarsi correttamente alla scrittura di un device driver in ambiente Linux.
Un device driver è quello strato di software che serve a controllare una periferica. Più precisamente il device driver permette all’utente, tramite il sistema operativo, di accedere a una periferica e allo stesso tempo consente alla periferica di essere integrata nel sistema al quale viene collegata.
Approcciandosi alla scrittura di un device driver è doveroso fare una distinzione tra Kernel Space e User Space.
Il Kernel Space gestisce l’hardware della macchina e fornisce alle applicazioni che ne hanno bisogno l’accesso alle risorse del sistema. Lo User Space, invece, è lo spazio di esecuzione delle normali applicazioni.
I due spazi sono divisi attraverso la separazione dei privilegi e le applicazioni in User Space non hanno accesso diretto al Kernel Space. Il device driver, pur vivendo nel Kernel Space, permette un collegamento trai due mondi.
Schematicamente, nel mondo di Unix, esistono tre tipologie di periferiche e di conseguenza tre tipologie di driver.
Si tratta di:
– Periferiche a carattere: considerate come file, possono essere lette o scritte in maniera sequenziale.
– Periferiche a blocchi: possono essere lette o scritte solo in dimensioni multiple di un blocco definito di byte. Possono inoltre ospitare un filesystem.
– Interfacce di rete: lo scambio di dati avviene tramite “pacchetti di dati”. Si usano funzioni differenti rispetto ai casi precedenti.
In ambiente Linux esistono due modi di programmare un device driver. Il primo è quello di compilare il driver insieme al kernel (monolitico). Il secondo metodo permette invece di implementare il driver come un modulo esterno rispetto al kernel.
Scrivere un driver come modulo fa sì che non sia necessario ricompilare tutto il sistema operativo ogni volta che si apporta una modifica al codice: questo può essere molto utile, ad esempio, durante la fase di sviluppo del codice. Un modulo può essere così caricato solo quando necessario, o si può decidere di caricarlo solo dopo il boot, in modo da non appesantire la fase di avvio del sistema. Scrivere un device driver come modulo non impedisce comunque, in un secondo momento, di compilarlo all’interno del kernel.
In questo breve tutorial mostreremo come è possibile scrivere un semplice device driver a carattere per Linux, che permette di creare un device nel quale si può scrivere e leggera una stringa.
Come primo step è necessario procedere alla scrittura di un prototipo di modulo, che può essere caricato o rimosso dal kernel.
Le funzioni driver_facile_init e driver_facile_exit vengono chiamate nel momento del caricamento o della rimozione del driver. Devono essere della forma mostrata nell’esempio e passate alle due macro module_init e module_exit definite nel file linux/init.h.
Come si può notare, è stata usata la funzione del kernel printk. Questa funzione viene utilizzata come la comune printf e permette di stampare messaggi nel registro del kernel secondo la priorità loro assegnata. Se la priorità non è esplicitamente specificata, come nel nostro caso, verrà usato il valore predefinito in vigore al momento. Questi messaggi possono essere visualizzati sul terminale tramite il comando dmesg.
A questo punto il codice mostrato precedentemente può già essere compilato e caricato nel kernel.
Per buildare è necessario che la compilazione avvenga contro i sorgenti del kernel nel quale si vuole usare il driver. Per l’esempio precedente e per il resto del tutorial sarà sufficiente usare un semplice Makefile del tipo:
Il risultato della compilazione è un file del tipo .ko che può essere caricato nel kernel tramite il comando insmod e rimosso tramite il comando rmmod. Il driver così scritto non fa nulla, ma funge da prototipo per l’implementazione successiva. Per non avere problemi in fase di caricamento è sempre buona regola inserire nel codice la macro relativa alla licenza che il driver deve avere. Per esempio:
Il driver così come è stato scritto non genera errori, ma non è nemmeno in grado di svolgere alcuna funzione. Per renderlo funzionante è necessario che, al momento della sua inizializzazione, il modulo si registri nel kernel e gli passi una struttura che permetta di chiamare dallo spazio utente ogni altra funzione utile all’interno del driver. Questa struttura si chiama file_operations ed è contenuta nel file linux/fs.h. La struttura in questione implementa la chiamata a molteplici funzioni, non tutte necessarie. Le entry corrispondenti a quelle non implementate sono automaticamente settate a NULL. All’interno del codice del driver è necessario specificare solo quelle funzioni che si vorranno utilizzare. Nel nostro caso la struttura è globale e statica e sarà riempita come segue:
A questo punto per poter registrare il modulo dovremo chiamare nella nostra funzione di “init” la funzione:
Solitamente i device si trovano nella cartella /dev e i sistemi Linux hanno due modi per identificare i device file:
– major device number, che identifica il modulo che serve un device file o un gruppo di essi;
– minor device number, che identifica uno specifico device all’interno di un gruppo.
Nel nostro caso nella funzione register_chrdev è necessario solo il major number. Si può decidere di specificare esplicitamente il numero che si vuole assegnare (tra 1 e 255) oppure di lasciare che sia il sistema ad assegnare dinamicamente questo valore. Nel primo caso se un altro device dovesse avere lo stesso numero il sistema ci restituirà un errore, nel secondo caso invece è necessario passare alla funzione il numero 0.
La stringa che identifica il nome è il nome del modulo con cui sarà registrato e identifica il device nel file /sys/devices.
Una volta che la funzione register_chrdev è andata a buon fine, il device e la struttura file_operations sono associati.
Al momento della rimozione del modulo sarà invece necessario fare l’operazione inversa per rilasciare il numero assegnato al device tramite la funzione:
Le funzioni esplicitate nella struttura file_operations scritta precedentemente, e che dovranno essere implementate, servono a svolgere delle operazioni su file. Come ovvio dai nomi servono rispettivamente per aprire, chiudere, leggere e scrivere. Queste funzioni per poter svolgere il loro compito devono essere della forma appropriata esplicitata dalla struttura file_operations:
Per brevità di seguito verrà mostrata solo l’implementazione di read.
La funzione così scritta leggerà dei caratteri dal device. Come si può vedere, all’interno della funzione viene chiamata la funzione copy_to_user.
Questa funzione, dichiarata nel file linux/uaccess.h, serve a copiare i dati dallo spazio del kernel allo spazio utente e restituisce il numero di byte che non sono stati copiati. Di conseguenza, un valore diverso da 0 indica un errore. Ovviamente per poter fare il processo inverso, cioè leggere, esiste la funzione corrispondente copy_from_user.
Una volta scritte tutte le funzioni il driver è pronto e può essere caricato nel kernel, ma non è sufficiente affinché funzioni: al momento, si tratta solo di un file. Per renderlo funzionante è necessario che venga creato il nodo al device. Per fare questa operazione è necessario digitare da terminale il comando: mknod /dev/nome tipo major minor.
Il tipo può essere “b” per un device a blocchi e “c” per un device a carattere. Il major corrisponde al major number con cui è stato registrato il device. Nel nostro caso, essendoci un device solo del nostro tipo, il minor può invece essere assunto uguale a 0. Anche il minor number può in ogni caso variare tra 0 e 255.
Esistono modi per creare il nodo in maniera automatica all’interno del driver ma in questo articolo sono stati omessi per brevità.
Un’altra operazione da fare per rendere operativo il device è cambiare i permessi affinché l’utente possa abilitare i processi di lettura e scrittura su di esso. Per fare questo è sufficiente digitare da terminale il comando: chmod a+w+r /dev/nome.
A questo punto il driver è pronto e il nodo è stato creato. Per poterlo testare è sufficiente scrivere una semplice applicazione che faccia le operazioni implementate all’interno del driver.
Scarica qui i file sorgenti completi a cui fa riferimento questo tutorial.