giovedì 2 settembre 2010

Device drivers modulari: i moduli sono una funzionalità importante offerta dal kernel di Linux.

Maria Susana Diaz | 13:39 |
Questo articolo e i seguenti sono la traduzione di quelli che sono apparsi sul Linux Journal. Chi di voi riceve la rivista non troverà niente di nuovo rispetto a quello che ha già letto. Perdonate l'abuso di termini inglesi: in questo argomento ci sono molte parole che non si prestano alla traduzione, e rendono meglio in forma originale.

I moduli sono una funzionalità importante offerta dal kernel di Linux. Anche se la maggior parte degli utenti li vedono solo come un modo di liberare un po' di memoria, per esempio caricando il driver del dischetto solo quando questo viene usato, secondo me il maggior vantaggio nell'uso dei moduli sta nella possibilità di aggiungere il supporto per nuove periferiche senza dover modificare il sorgente ufficiale. In questo articolo e nei seguenti Georg Zezschwitz e io tenteremo di presentare ``l'arte'' di scrivere moduli, evitando gli errori più comuni.


Cos'è un ``device driver''?

Un device driver (forse traducibile come ``controllore di periferica'') è il software di livello più basso che gira su un calcolatore, in quanto è direttamente connesso alle caratteristiche hardware della periferica.

In effetti il concetto di ``device driver'' è abbastanza astratto, e il kernel stesso può essere considerato un grande device driver per una periferica chiamata ``calcolatore''. Di solito comunque il calcolatore non viene visto tanto come una entità indivisibile, quanto come un processore con le sue periferiche. In questo caso il kernel può essere considerato un'applicazione che si appoggia sui device drivers: ogni driver si occupa di una singola parte del calcolatore, mentre il kernel propriamente detto fornisce il multitasking e l'accesso ai files usando le periferiche a disposizione. Alcuni driver sono irrinunciabili, e fanno parte del kernel propriamente detto, come il ``driver'' del processore e il controllo della memoria; gli altri sono opzionali, e il calcolatore è utilizzabile con o senza di essi. In effetti un kernel senza il driver della console che manchi anche di un sottosistema di rete è inutilizzabile per un utente convenzionale.


C'è da dire che la descrizione precedente è un po' semplicistica e tendenzialmente filosofica. I driver reali interagiscono secondo complessi meccanismi, e una distinzione netta tra loro è difficile da tracciare.


Nel mondo Unix, cose come il driver di rete e altri driver complessi appartengono al kernel (infatti si parla di ``sottosistema di rete''), e il nome di `device driver' è riservato allo strato software di più basso livello, che controlla direttamente l'hardware. Tali periferiche appartengono ai seguenti tre gruppi:


  • Periferiche a carattere.

    Tali periferiche possono essere considerate dei file, per il fatto che possono venire lette o scritte. La console (il video e la tastiera) e le porte seriale e parallela sono esempi di periferiche a carattere. L'accesso avviene tramite files come /dev/tty0 o /dev/cua0. Una periferica a carattere di solito può solo venire letta o scritta sequenzialmente.

  • Periferiche a blocchi.

    Storicamente si trattava di dispositivi che potevano essere letti o scritti solamente in dimensioni multiple della dimensione del blocco: spesso 512 o 1024 bytes. Si tratta delle periferiche sulle quali è possibile montare un filesystem; le periferiche a blocchi più importanti sono i dischi. Si accede globalmente a tali periferiche tramite files come /dev/hda1. I blocchi di un dispositivo vengono immagazzinati nel `buffer cache' per migliorare l'efficienza nell'accesso `casuale'. Unix solitamente offre dei device a a carattere (`raw') associati ai device a blocchi, ma Linux non lo fa.

  • Interfaccie di rete.

    Le interfaccie di rete non ricadono nell'astrazione del file. Anche le interfaccie sono identificate da un nome (come eth0 o plip1), ma non vengono mappate nell'albero dei files. Tale mappatura sarebbe possibile, in teoria, ma non sarebbe comoda né per il programmatore né per la performance ottenuta: un'interfaccia di rete può solo trasferire dei pacchetti di dati, e l'astrazione del file in /dev non gestisce efficientemente il trasferimento di dati strutturati.


La descrizione predente è un po' schematica, e ciascuna versione di Unix differisce nei dettagli di cosa sia una perferica a blocchi. Per noi non fa comunque molta differenza, in quanto tali dettagli sono solo importanti all'interno del kernel, e noi non parleremo dei driver a blocchi.

Quello che manca nella descrizione fatta finora è che il kernel agisce anche come se fosse una libreria per i device drivers: i driver possono richiedere servizi al kernel. Un modulo, per esempio, sarà in grado di chiamare funzioni per l'allocazione di memoria, l'accesso al filesystem, e così via.


Per quanto riguarda i moduli, ciascun tipo di driver descritto sopra può essere progettato sotto forma di modulo. Si possono anche scrivere moduli che implementano dei tipi di filesystem, ma questo rimane fuori dal nostro ambito attuale.


Questo articolo e i seguenti si occupano di device driver a carattere perchè l'hardware speciale, o quello costruito in casa, nella maggior parte dei casi si adatta all'astrazione del dispositivo a carattere. In effetti ci sono solo differenze minori tra i tre tipi, ma per evitare confusione conviene soffermarsi sul tipo più comune.

Cos'è un modulo?

Un modulo è un segmento di codice che si registra all'interno del kernel come device driver, viene chiamato dal kernel per comunicare con la periferica in questione e a sua volta invoca altre funzioni del kernel per svolgere il suo compito. I moduli utilizzano un'interfaccia ben definita tra il kernel vero e proprio e il driver. L'uso di un'interfaccia precisa semplifica la scrittura di nuovi moduli, e aiuta a tenere più pulito il sorgente del kernel.

Il modulo deve essere compilato come codice oggetto (senza invocare il linker: deve essere un file .o), e poi caricato nel kernel corrent tramite il comando insmod. Tale programma è un ``run-time linker'', che risolve i simboli non definiti nel modulo utilizzando la tabella dei simboli del kernel.


Questo vuol dire che un modulo è simile ad un programma convenzionale in linguaggio C: si possono chiamare funzioni che non vengono definite, come di solito si chiama printf() e fopen() dall'interno di un programma applicativo. A differenza dei programmi normali, però, si può contare solo su un insieme minimo di funzioni esterne, che sono poi le funzioni `pubbliche' dichiarate dal kernel. insmod metterà i corretti indirizzi del kernel nel codice compilato del modulo ogniqualvolta il modulo invoca una funzione del kernel, ed inserirà infine il modulo all'interno del kernel corrente.


In caso di dubbi se una funzione del kernel sia pubblica o no, si può cercare il suo nome nel file /usr/src/linux/kernel/ksyms.c (ma alcune vengono dichiarate altrove, in particolare nelle versioni più recenti), oppure nella tabella di run-time in /proc/ksyms.


Per utilizzare make al fine di compilare un modulo, occorre un Makefile, che può essere semplice come il seguente:

 TARGET = myname

ifdef DEBUG
# -O is needed, because of "extern inline"
CFLAGS = -g -O -DDEBUG_$(TARGET) -D__KERNEL__ -Wall
else
CFLAGS = -O3 -D__KERNEL__ -fomit-frame-pointer
endif

all: $(TARGET).o


Come si nota, non occorrono regole speciali per costruire un modulo, solo un valore corretto per CFLAGS. Raccomando di includere supporto per il debugging all'interno del sorgente, perché gdb non è in grado di usare le informazioni fornite da -g una volta che il modulo sia caricato nel kernel. A meno di non modificare gdb stesso.

``Supporto per il dedugging'' di solito significa la presenza di codice aggiuntivo per stampare messaggi dall'interno del driver. L'uso di printk() è una scelta funzionale per il debugging, in quanto le alternative sono quella di far funzionare un debugger sul kernel corrente, sbirciare in /dev/mem e altre tecniche estremamente di basso livello. Esistono alcuni strumenti disponibili sulla rete per aiutare ad usare queste tecniche alternative, ma per beneficiare di questi strumenti occorre almeno esser pratici di gdb ed essere in grado di leggere il codice del kernel. Il pacchetto più interessante al momento della scrittura di questo articolo era kdebug-1.1, che permette di usare gdb su un kernel funzionante, esaminando e cambiando le strutture dati del kernel, ed anche quelle dei moduli. Kdebug è disponibile tramite ftp da sunsite.unc.edu e mirrors nella directory /pub/Linux/kernel.


Per complicare un poco le cose, l'equivalente della funzione printf() all'interno del kernel si chiama printk(), in quanto il suo funzionamento non è esattamente uguale a printf(). Prima della versione 1.3.37 normali chiamate a printk() generavano delle linee nel file /var/adm/messages, ma kernel più recenti stampano anche sulla console. Se si desidera un logging meno intrusivo (cioè solo nel file dei messaggi, tramite syslogd), bisogna far precedere la stringa di formato da KERN_DEBUG. KERN_DEBUG e altri simboli simili sono stringhe, che vengono concatenate dal compilatore alla stringa di formato. Questo significa che non bisogna mettere una virgola tra KERN_DEBUG e il formato della stampa. Tutti questi simboli sono definiti e documentati in linux/kernel.h. L'altra cosa da ricordare è che printk() non supporta la stampa di valori in virgola mobile, che non si usano all'interno del kernel.


Bisogna ricordare che syslogd scrive sul file dei messaggi il più presto possibile, al fine di salvare i messaggi su disco in caso di imminente caduta del sistema. Questo significa che un modulo contentente troppe chiamate a printk() sarà decisamente più lento, e riempirà velocemente il disco.

Quasi tutti gli errori dei moduli causano la generazione di un messaggio di ``Oops''. Un Oops è la risposta del kernel ad una eccezione generata dal kernel stesso. In altre parole, gli oops sono l'equivalente dei ``Segmentation Fault'' nelle applicazioni, ma senza generare un core file. Di solito un oops causa la distruzione immediata del processo nel cui contesto è avvenuto l'errore, unitamente alla stampa di alcune linee di informazione di basso livello nel file dei messaggi (e sulla console). La maggior parte degli oops sono causati dall'utilizzo di puntatori nulli.


Questa maniera di gestire i disastri è abbastanza amichevole, e i programmatori imparano presto ad apprezzarla: quasi tutte le altre vesioni di Unix in questi casi si piantano generando un ``kernel panic''. Questo non vuol dire che Linux non si pianta mai; bisogna aspettarsi di piantare il sistema quando ci sono errori nelle funzioni che operano al di fuori del contesto di un processo, come durante la gestione di interrupt e nelle azioni controllate dal timer di sistema.


L'informazione limitata, e quasi incomprensibile, che viene inclusa nel messaggio di Oops rappresenta lo stato del processore al momento dell'errore, e può essere usata per capire dove stia l'errore. Esiste uno strumento chiamato ksymoops, che è in grado di stampare informazioni più leggibili del messaggio di Oops stesso, a patto di avere una mappa del sistema a portata di mano: la mappa è quello che rimane in /usr/src/linux/System.map dopo una compilazione del kernel. Ksymoops è stato distribuito con util-linux-2.4, ma è stato rimosso dalla 2.5 perché nel frattempo è stato incluso nella distribuzione del kernel.


Se si capisce davvero il contenuto di un Oops, si può usarlo come si vuole, per esempio chiamando gdb automaticamente per disassemblare la funzione responsabile dell'errore. Se invece non si capisce né l'Oops né l'output di ksymoops, conviene aggiungere qualche printk() al codice, ricompilare e riprodurre l'errore.


Il codice seguente può facilitare la gestione dei messaggi di debugging. Deve risiedere nell'header pubblico del modulo e può essere usato sia nello spazio del kernel (nel modulo) che nello spazio utente (nelle applicazioni); si tratta però di codice dipendente da caratteristiche del gcc: non dovrebbe essere un problema per un modulo di Linux, che dipende comunque dal gcc. Questo codice mi è stato suggerito da Linus, come miglioramento rispetto alla mia versione precedente, che usava solo funzionalità del C parte dello standard ansi.


 #ifndef PDEBUG
# ifdef DEBUG_modulename
# ifdef __KERNEL__
# define PDEBUG(fmt, args...) printk (KERN_DEBUG fmt , ## args)
# else
# define PDEBUG(fmt, args...) fprintf (stderr, fmt , ## args)
# endif
# else
# define PDEBUG(fmt, args...)
# endif
#endif

#ifndef PDEBUGG
# define PDEBUGG(fmt, args...)
#endif


dopo questo codice, ogni chiamata PDEBUG("%i %a\n", i, s); nel modulo causerà la stampa di un messaggio solo se il codice è stato compilato con -DDEBUG_modulename, e PDEBUGG() con gli stessi argomenti verrà espanso in un'istruzione vuota. Nelle applicazioni le cose funzionano allo stesso modo, tranne che il messaggio verrà stampato su sdterr invece che sul file dei messaggi.

Utilizzando questo codice un singolo messaggio può essere attivato o disattivato semplicemente togliendo od aggiungendo una G.


Scrittura di codice.

Vediamo ora che tipo di codice deve far parte di un modulo: la risposta più scontata è ``quello che serve''. In pratica bisogna ricordare che il modulo viene a far parte del kernel, e deve inserirsi nel resto di Linux tramite una interfaccia ben definita (come ho già detto).

Di solito si inizia il sorgente includendo gli header necessari, e già qui ci troviamo di fronte ad alcuni vincoli: bisogna sempre definire il simbolo __KERNEL__ prima di includere qualsiasi file, a meno che tale simbolo non sia già stato definito nel Makefile; inoltre si possono solo includere header appartenenti alle gerarchie linux/* e asm/*. Certamente si può anche includere un header specifico del driver in questione, ma non bisogna mai includere dei files di libreria, come stdio.h o sys/time.h.


Il codice nel listato seguente rappresenta le prime linee di sorgente di un tipico device driver a carattere. Se si intende scrivere un modulo conviene però copiare queste linee da un sorgente esistente piuttosto che copiarle a mano da questo articolo (in effetti chi ha scaricato l'articolo in formato elettronico potrebbe anche copiarlo da qui; io consiglio comunque di tenere sott'occhio un sorgente vero).


 #define __KERNEL__         /* kernel code */

#define MODULE /* always as a module */
#include &ltlinux/module.h> /* can't do without it */
#include &ltlinux/version.h> /* and this too */

/*
* Then include whatever header you need.
* Most likely you need the following:
*/
#include &ltlinux/types.h> /* ulong and friends */
#include &ltlinux/sched.h> /* current, task_struct, other goodies */
#include &ltlinux/fcntl.h> /* O_NONBLOCK etc. */
#include &ltlinux/errno.h> /* return values */
#include &ltlinux/ioport.h> /* request_region() */
#include &ltlinux/config.h> /* system name and global items */
#include &ltlinux/malloc.h> /* kmalloc, kfree */

#include &ltasm/io.h> /* inb() inw() outb() ... */
#include &ltasm/irq.h> /* unreadable, but useful */

#include "modulename.h" /* your own material */


Dopo l'inclusione degli header, si arriva al codice vero e proprio. Prima di parlare delle funzionalità specifiche di un driver (cioè della parte più importante), vale la pena di sottolineare che esistono due specifiche funzioni che devono essere definite perché un modulo possa essere caricato nel kernel:

 int init_module (void);
void cleanup_module (void);


La prima funzione si occupa dell'inizializzazione del modulo: ricerca dell'hardware e registrazione del nuovo driver all'interno delle tabelle del kernel; la seconda ha invece il compito di rilasciare le risorse usate dal modulo e di cancellare il driver dalle tabelle del kernel.

Se queste funzioni non sono definite, insmod si rifiuta di caricare il modulo.


La funzione di inizializzazione ritorna zero se va tutto bene e un valore negativo in caso di errore. La funzione di pulizia ritorna void perché viene solo invocata quando si è sicuri che il modulo può essere rimosso dal kernel: ogni modulo tiene un contatore di utilizzo, e cleanup_module() viene solo chiamata quando tale contatore è a zero (ma ne parleremo più avanti).


Il prossimo articolo presenterà il codice schematico per queste due funzioni. Una corretta scrittura di queste funzioni è fondamentale per un funzionamento corretto del modulo, e bisogna gestire correttamente alcuni dettagli: in questo articolo vengono presentati questi dettagli, così la volta prossima verranno presentate le funzioni senza dover spiegare i particolari.


Come ottenere un ``major number''

I driver a carattere, e anche quelli a blocchi, devono registrarsi all'interno di un vettore di puntatori del kernel; questo passo è fondamentale perché un driver possa essere usato. Dopo l'esecuzione di init_module, il driver è parte effettiva del kernel, ma non può essere invocato se non ha reso disponibili le sue funzionalit&agrave. Linux, come la maggior parte dei sistemi Unix, mantiene un array di device drivers, e ciascuno di essi è identificato da un numero, chiamato ``major number''. Tale numero non è altro che l'indice del driver all'interno del vettore di tutti i drivers.

Il ``major number'' di un dispositivo è il primo numero che appare nel listato (ls -l) di un file associato ad una periferica. L'altro numero è il ``minor number'', come ci si poteva immaginare. Tutti i dispositivi (files associati) con lo stesso major number vengono serviti dallo stesso driver.


Chiaramente anche un driver modulare ha bisogno di un major number. Il problema è che il kernel attualmente usa un vettore statico per mantenere le informazioni dei drivers, e tale array contiene solo 64 drivers (sono stati 32 fino a poco tempo fa, ma durante lo sviluppo di Linux-1.2 si è raddoppiato tale numero a causa dell'esaurimento dei numeri disponibili).


Fortunatamente il kernel permette l'assegnamento dinamico dei major numbers.

La chiamata alla funzione

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

registra un driver a carattere nella tabella del kernel. Il primo argomento passato a questa funzione può essere o il numero che viene richiesto oppure 0, nel qual caso viene eseguita una allocazione dinamica. La funzione ritorna un numero negativo per indicare un errore, e un numero maggiore o uguale a zero in caso di successo. Se è stata chiesta un'allocazione dinamica, il valore di ritorno, se positivo, è il numero che è stato assegnato. L'argomento name è il nome del driver, ed è quello che appare all'interno del file /proc/devices. Infine, fops è la struttura che viene usata per chiamare ogni altra funzione all'interno del driver, e verrà descritta in seguito.

L'allocazione dinamica del major number è una scelta vincente per i driver scritti dall'utente: si ha la sicurezza di avere un numero che non entra in conflitto con altri drivers all'interno del sistema: register_chrder() riesce sicuramente nel suo compito, a meno che si siano caricati così tanti driver da finire i numeri disponibili, situazione alquanto improbabile.


Carico e scarico dei moduli.

Siccome il major number è registrato nel file che viene usato dalle applicazioni per accedere al dispositivo, l'uso dell'allocazione dinamica implica che non si posso creare i files una volta per tutte nella directory /dev. Occorre una maniera per ricreare tali files ogni volta che il modulo viene caricato.

I programmi in questa pagina sono quelli che io uso per caricare e scaricare i miei moduli: piccole modifiche basteranno per adattarli ad altre situazioni: occorre solo cambiare il nome del dispositivo e il nome del modulo.


Il comando mknod crea un nodo (file) di dispositivo con i numeri (major e minor) che vengono specificati, e chmod viene usato per dare i permessi corretti ai nuovi files. Dei minor numbers parlerò la volta prossima.


Il programma per caricare il modulo può essere chiamato drvnamne_load, dove drvname è il prefisso usato per identificare il driver: lo stesso usato come name nella chiamato a register_chrdrv(). Il programma può essere invocato esplicitamente durante lo sviluppo del driver, e tramite rc.local dopo l'installazione del modulo. Bisogna ricordare che insmod cerca i moduli da installare sia nella directory corrente, sia nella directory di installazione dei moduli (sotto /lib/modules).


Se il modulo in questione dipende da altri moduli, o se la configurazione del sistema è strana, si può usare modprobe invece di insmod. Il programma modprobe è una versione più raffinata di insmod che gestisce le dipendenze tra i moduli ed il caricamento condizionale. Lo strumento è abbastanza potente e ben documentato. Se occorre una gestione particolare per un driver, la cosa migliore è leggere la pagina del manuale per modprobe.


In questo momento (aprile 96), però, nessuno degli strumenti standard gestisce la generazione dei nodi in /dev per major numbers allocati dinamicamente, e in effetti non è facile immaginare una soluzione pulita per tale problema. Questo significa che un programma specifico per caricare il module è inevitabile.


Questo è il mio drvname_load

 #!/bin/sh
# Install the drvname driver,
# including creating device nodes.

# FILE and DEV may be the same.
# The former is the object file to load,
# the latter is the official name within
# the kernel.

FILE="drvname"
DEV="devicename"

/sbin/insmod -f $FILE $* || (echo "$DEV not inserted" ; exit 1)

# retrieve major just assigned
major=`grep $DEV /proc/devices | awk '{print $1}'`

# make defice nodes
cd /dev
rm -f mynode0 mynode1

mknod mynode0 c $major 0
mknod mynode1 c $major 1

# edit this line to suit your needs
chmod go+rw mynode0 mynode1

e questo è drvname_unload:
 #!/bin/sh
# Unload the drvname driver

FILE="drvname"
DEV="devicename"

/sbin/rmmod $FILE $* || (echo "$DEV not removed" ; exit 1)

# remove device nodes
cd /dev
rm -f mynode0 mynode1

Allocazione di risorse.

L'altro compito importante di init_module() è l'allocazione di tutte le risorse di cui il driver ha bisogno per operare correttamente. Qui viene chiamata ``risorsa'' ogni pezzetto di calcolatore, dove con ``pezzetto'' intendo una rappresentazione logica (software) di una parte del calcolatore fisico. Di solito un driver richiede memoria, porte di I/O e linee in interrupt.

I programmatori sono familiari con l'allocazione di memoria: nel kernel la funzione kmalloc() si occupa di questo, e si usa esattamente come se fosse malloc(), tranne per il fatto che occorre specificare un secondo argomento: GFP_KERNEL andrà bene come secondo argomento, tranne che in situazioni molto particolari.


L'allocazione di porte di I/O, invece è una cosa strana: le porte ci sono e basta usarle, inoltre il processore non protegge l'uso delle porte tramite segmentazione o altre tecniche. Il problema qui sta nel fatto che scrivere su porte di I/O relative ad altri dispositivi potrebbe far cadere il sistema.


Linux implementa per le porte la stessa politica che viene usata per la memoria: L'unica vera differenza sta nel fatto che il processore non genera della eccezioni in caso di accesso non autorizzato alle porte. La registrazione delle porte, come quella della memoria, inoltre aiuta il kernel a mantenere pulita la gestione della macchina.

Spesso capita di chiedersi quali indirizzi di porta sono liberi nel proprio calcolatore al fine di configurare una nuova scheda prima di inserirla nel suo slot; gli utenti di Linux non hanno di questi problemi, in quanto il file /proc/ioports ed il file /proc/interrupts contengono tutte le informazioni sull'hardware già presente nel calcolatore. Questo non sarebbe possibile senza una politica di allocazione delle porte.

La registrazione delle porte è leggermente più complicata della gestione della memoria, in quanto spesso occorre fare delle prove per sapere a quale indirizzo si trova il dispositivo. Per evitare di fare le prove su porte già registrate da altri dispositivi, si può chiamare check_region() per sapere se la regione di porte candidata è già stata richiesta da altri driver. Questo va fatto per ogni regione candidata. Una volta che il dispositivo è stato trovato, la funzione request_region() viene chiamata per effettuare la allocazione vera e propria. Quando il driver viene rimosso dal sistema, bisogna chiamare release_region() per rilasciare le porte. Questi sono i prototipi delle funzioni, come si trovano in linux/ioport.h:

int check_region(unsigned int from,
unsigned int extent);
void request_region(unsigned int from,
unsigned int extent,
const char *name);
void release_region(unsigned int from,
unsigned int extent);

In queste funzioni, l'argomento from indica l'inizio di una regione contigua di porte, extent è il numero di porte richiesto, e name è il nome del driver.

Se ci si dimentica di registrare le porte non succede niente di male, a meno che non ci siano due driver che lo fanno, e a meno che non occorra sapere le porte occupate per aggiungere una scheda nel calcolatore. Se ci si dimentica di rilasciare le porte alla fine, invece, ogni programma che legga /proc/ioports causerà un Oops, perché il nome del driver che aveva richiesto le porte non sarà più accessibile in memoria. Inoltre, non sarà più possibile caricare il driver, perché non ci sarà più modo di ottenere le porte necessarie, che appaiono bloccate. bisogna quindi sempre ricordarsi di rilasciare tutte le porte che si sono richieste.

Una politica di allocazione simile esiste per le linee di interrupt (i prototipi seguenti stanno in linux/sched.h, e sono cambiati con la versione 1.3.70 -- questo sono quelli vecchi)

int request_irq(uint irq,
void (*handler)(int, struct pt_regs *),
ulong flags, const char *name);
void free_irq(uint irq);


Ancora una volta, name è quello che appare in /proc/interrupts; conviene quindi che si tratti di myhardware piuttosto che mydrv.

Se ci si dimentica di registrare le linee di interrupt, l'handler non verrà richiamato; se ci si dimentica di rilasciare le linee, non si potrà più leggere /proc/interrupts. Inoltre, se la scheda continua a generare interrupts dopo che il driver è stato rimosso, succedono brutte cose (non so cosa esattamente, perchè non mi è mai successo, e non sono intenzionato a provare solo per scriverlo qui).


L'ultimo punto che voglio toccare qui è ben espresso dal commento di Linus in linux/io.h: ``bisogna trovare il proprio hardware''. Se si vuole scrivere un driver utilizzabile, bisogna implementare l'autodetection. L'autodetection è fondamentale se si vuole distribuire il driver a pubblico non specialista; basti pensare che se ne sono accorti anche gli altri (e hanno pure inventato la parola ``plug'n'play'', per sottolineare la ``novità'' della cosa).


Il driver dovrebbe essere in grado di trovare automaticamente sia le porte di I/O del dispositivo, sia la linea di interrupt usata. Se la scheda non dice quale linea di interrupt usa, si può sempre andare attraverso una tecnicha per tentativi ed errori che se viene fatta con attenzione funziona bene. Tale tecnica verrà trattata in un altro articolo.


Quando il numero di interrupt è noto, conviene chiamare free_irq() prima di ritornare da module_init(), in quanto la linea di interrupt può essere richiesta ancora al momento dell'apertura del dispositivo. Se si tenesse stretta l'interrupt, non sarebbe possibile far andare diversi dispositivi sulla stessa linea (e le piattaforme intel hanno troppe poche linee di interrupt perché si possa sprecarle). Per esempio io ho fatto andare PLIP e il frame grabber sulla stessa linea, senza bisogno di scaricare il modulo: basta usare un solo dispositivo per volta.


Bisogna notare che dalla versione 1.3.70 in poi Linux ha il supporto per la condivisione delle linee di interrupt da parte di più dispositivi; chi è interessato può approfondire la cosa andando a guardare nei sorgenti del kernel (cosa che io consiglio sempre, ma non è mai abbastanza :-).


Sfortuantamente, esistono rari casi in cui l'autodetection non funziona ed occorre prevedere un modo per passare la driver informazioni sulle porte di I/O e sulle interrupt. Le ricerche dell'hardware possono fallire solo durante l'inizializzazione del sistema, in quanto i primi driver hanno accesso anche ai dispositivi che non sono ancora stati registrati, e possono erroneamente prendere un'altra periferica per quella che stanno cercando. A volte invece la ricerca di una periferica può essere 'distruttiva' per altre periferiche, e prevenire la sua futura inizializzazione. Entrambi questi problemi non possono accadere ad un modulo, in quanto viene caricato per ultimo, e quindi non può richiedere porte utilizzate da altre periferiche. Ciò nonostante, un modo per disabilitare l'autodetection, e specificare esplicitamente i valori all'interno del driver è una cosa importante da implementare; Per lo meno, è più facile dell'autodetection, e può aiutare a caricare il proprio modulo prima di scrivere il codice per l'autodetection.


La configurazione al caricamento sarà il primo argomento trattato nel prossimo articolo, in cui sarà presentato il codice completo per init_module() e cleanup_module().


Ovviamente, chi è abbonato al Linux Journal ha già ricevuto il prossimo articolo e, se non l'ha già fatto, può andare a leggerlo subito.


Informazioni aggiuntive.

I prossimi articoli andranno avanti con questo argomento, e i sorgenti del kernel sono pieni di esempi interessanti. Altri moduli sono disponibili nei vari archivi ftp di linux.

In particolare, quello che dico è basato sulla mia esperienza personale con i device drivers: sia ceddrv-0.xx e cxdrv-0.xx assomigliano al codice che descrivo. ceddrv è stato scritto da Georg Zezschwitz e me, e si tratta di un driver per un macchinario A/D D/A da laboratorio. Il cxdrv è più semplice, e pilota un frame grabber molto semplice. L'ultima versione di entrambi si trova tramite ftp sotto iride.unipv.it:/pub/linux, ceddrv è anche sotto tsx-11, e cxdrv è anche sotto sunsite. Entrambi sono molto vecchi, però, in quanto non ho avuto tempo di aggiornarli.


Esistono dei libri sui device driver, ma spesso sono troppo specifici per un sistema particolare, e descrivono intrefacce molto ostiche. Linux è molto più facile. Se occorrono informazioni io consiglio libri generici sulla struttura interna di Unix, e certamente il sorgente di Linux stesso.


I seguenti libri sono molto interessanti. So che esiste una traduzione italiana almeno del primo e del terzo (entrambe della Jackson), ma non ho con me i dati e sono troppo pigro per cercarli.

Maurice J. Bach,
The Design of the UNIX Operating System,
Prentice Hall, 1986

Andrew S. Tanenbaum,
Operating Systems: Design and Implementation,
Prentice Hall, 1987

Andrew S. Tanenbaum,
Modern Operating Systems,
Prentice Hall, 1992

Se ti è piaciuto l'articolo, iscriviti al feed per tenerti sempre aggiornato sui nuovi contenuti del blog:

Trovato questo articolo interessante? Condividilo sulla tua rete di contatti in Twitter, sulla tua bacheca su Facebook, in Linkedin, Instagram o Pinterest. Diffondere contenuti che trovi rilevanti aiuta questo blog a crescere. Grazie!

LINKEDIN