[About] [Copertina] |
Articoli
Se a qualcuno interessa avere sotto mano il testo originale di tutti e cinque gli articoli, li puo' trovare sotto ftp://ftp.systemy.it/pub/develop/kernel-korner.
Questo articolo e' il terzo di cinque parti sulla scrittura di
device driver a carattere, e introduce i concetti riguardanti la
lettura e la scrittura del dispositivo, come pure l'implementazione di
ioctl()
.
Sulla base dell'ambiente che abbiamo predisposto nei due
precedenti articoli, cercheremo questa volta di riempire la struttura
con il materiale relativo alle interrupt. Straordinariamente, Linux ci
nasconde la maggior parte delle questioni problematiche, cosi` che non
dovremo scrivere nemmeno una linea di assembler.
Leggere e scrivere
Finora il nostro driver per la `skel-machine
' e' in grado di
agganciarsi e sganciarsi dal kernel senza problemi, ma non siamo
ancora stati in grado di leggere o scrivere nemmeno un
carattere. Inizieremo quindi fornendo il codice per le funzioni
skel_read()
e skel_write()
, che abbiamo presentato
nel precedente articolo, sotto il paragrafo `Fops e filp'. Ad entrambe
le funzioni vengono passati quattro argomenti:
Static int skel_read (struct inode *inode,
struct file *file,
char *buf, int count)
Static int skel_write (struct inode *inode,
struct file *file,
char *buf, int count)
La struttura inode
ci fornisce le informazioni che abbiamo
gia' usato all'interno di skel_open()
. Per esempio, avevamo
determinato quale scheda l'utente sta aprendo (ricordiamo che il
nostro driver supporta dispositivi multipli) usando
inode->i_rdev
; avevamo poi trasferito questa informazione,
insieme all'indirizzo di I/O della scheda e il numero di IRQ, nel
campo private_data
della struttura file
. Possiamo quindi
ignorare queste informazioni nella read()
, ma se non avessimo
usato private_data
l'unico modo per sapere a quale scheda stiamo
per parlare sarebbe la lettura di inode
.
La struttura file
, invece, contiene informazioni probabilmente
piu; utili: si possono esplorare tutti i suoi campi in
.
Se si vuole usare il campo private_data
lo si trova qui; anche il
campo f_flags
spesso utile in quanto rivela, per esempio,
se il programma desidera operare in modo bloccante o non-bloccante --
questo argomento verra' spiegato dettagliatamente piu' avanti.
Gli argomenti buf
e count
dicono dove mettere i dati
che leggiamo (o dove trovare i dati che dobbiamo scrivere), e quanti
bytes ci sono. Bisogna pero' ricordare che ogni processo ha il suo
spazio di indirizzamento privato, mentre il codice del kernel risiede
in uno spazio di indirizzamento che e' comune a tutti i
processi. Quando le chiamate di sistema eseguono per conto di uno
specifico processo, eseguono nello spazio del kernel ma devono (e
possono) accedere allo spazio utente. Storicamente lo spazio utente
veniva indirizzato in assembler usando il registro fs
; le
versioni attuali di linux nascondono le istruzioni specifiche
all'interno di funzioni chiamate get_user_byte()
per leggere
un byte dallo spazio utente, put_user_byte()
per scriverne
uno, eccetera. Queste funzioni si chiamavano precedentemente con nomi
come get_fs_byte
, mentre adesso solo memcpy_tofs()
e
memcpy_fromfs()
revelano la vecchia implementazione, anche su
piattaforme diverse da x86. Chi volesse esplorare tutte queste
funzioni puo' trovarne le definizioni in
.
Immaginiamo ora un hardware ideale che non aspetta altro che di
ricevere i nostri dati, legge e scrive velocemente, e viene visto
attraverso una semplice porta dati da 8 bit all'indirizzo base della
nostra interfaccia. Nonostante questo non sia realistico, il
programmatore impaziente puo' provare il codice seguente:
Static int skel_read (struct inode *inode, struct file *file, char *buf, int count) { int n = count; char *port = PORT0 ((struct Skel_Hw*) (file->private_data)); while (n--) { aspetta_che_la_periferica_sia_pronta(); put_user_byte (inb_p (port), buf); buf++; } return count; }
Occorre spendere alcune parole sulla funzione inb_p()
:
questa e' la funzione di lettura dei dati dall'hardware. Si puo'
decidere di usare il suo equivalente veloce, inb()
,
che omette una minima attesa che puo' essere necessaria
quando si pilote dell'hardware lento, ma noi preferiamo l'approccio
piu' cauto.
La funzione equivalente skel_write()
e' quasi uguale, tranne
che bisogna rimpiazzare la linea contenente put_user_byte()
con la seguente:
outb_p (get_user_byte (buf), port);
C'e' da dire, pero' che questa implementazione ha un sacco di svantaggi. Cosa succede se il kernel si ferma in una di queste funzioni aspettando per un dispositivo che non diventera' mai pronto a ricevere/restuire dati?
Torniamo per un attimo ai discorsi teorici: cosa ci aspettiamo dal
nostro driver? Certamente dovrebbe dedicare il tempo di attesa
ad altri processi che usino il nostro costoso processore, e dovrebbe
avere un buffer di ingresso e uno di uscita in modo da gestire i
dati che arrivano mentre i processi non sono in skel_read()
o skel_write()
. Dovrebbe avere un time-out per riconoscere
gli errori, e dovrebbe supportare operazioni bloccanti e non bloccanti.
Modo bloccante (blocking) e non bloccante (non-blocking)
Immaginiamo un processo che voglia leggere 256 bytes. Sfortunatamente,
in nostro buffer di ingresso e' vuoto quando skel_read()
viene chiamata. Cosa dovremmo fare allora: ritornare dicendo che non
ci sono ancora dati, o aspettare finche' non arriva almeno qualche
byte?
La risposta e` ``entrambe le cose''. Il modo `blocking' significa che l'utente vuole attendere finche' non ci sono dati da leggere. Il modo `non-blocking' significa che il driver deve ritornare il piu' presto possibile -- deve solo leggere tutti i bytes che sono disponibili. Regole simili si applicano alla scrittura: una scrittura `bloccante' significa "non ritornare finche' non puoi accettare alcuni dati", mentre una `non bloccante' significa "ritorna anche se non puoi accettare niente".
Le chiamate read()
e write()
di solito ritornano il
numero di bytes trasferiti. Se, pero', l'operazione e'
`non-blocking' e non possono essere trasferiti dati, bisogna ritornare
-EAGAIN
che significa ``Play it again, Sam''. Una volta
si ritornava -EWOULDBLOCK
, che in Linux e' la stessa cosa
che -EAGAIN
.
Forse adesso state sorridendo felici come quando io ho sentito per la
prima volta di questi due modi. Se questi concetti sono nuovi per voi,
potrete trovare utili i seguenti suggerimenti. Ogni dispositivo viene
aperto per default in modo `blocking', ma l'utente puo' scegliere di
usare il modo `non-blocking' settando O_NONBLOCK
nella chiamata
open()
. Si puo' anche cambiare il comportamento di un file
piu' avanti usando la funzione fcntl()
. La chiamata
fcntl()
e' abbastansza semplice, e la pagina del manuale
fornisce informazioni sufficienti per un programmatore.
La bella addormentata
C'era una volta una bella pricipessa che era stata posta in un lungo e profondo sonno da una strega. Il mondo quasi si era dimenticato di lei e del suo castello circondato ormai dalle rose; finche' un giorno un bel principe venne e la bacio', svegliandola dal suo sonno... e successero tutte le altre belle cose di cui parlano le fiabe.
Il nostro driver mentre attende i dati deve fare esattamente quello
che faceva la bella addormentata: dormire, lasciando girare il mondo
per conto suo. Linux offre un meccanismo per questo, chiamato
interruptible_sleep_on()
. Ogni processo che chiama questa
funzione cade addormentato e lascia il suo tempo al resto del
mondo. Il processo rimane all'interno di questa funzione finche'
un altro processo chiama wake_up_interruptible()
. Questo
`principe' e' di solito un gestore di interrupt che ha ricevuto
o spedito dati; oppure Linux stesso, se viene raggiunta una condizione
di timeout.
Installazione di un gestore di interrupt
Il precedente numero di questa rubrica aveva mostrato un gestore di
interruzione minimale, chiamato skel_trial_fn()
, ma il suo
funzionamento non era stato spiegato. Codesentiamo qui un gestore di
interrupt completo, che gestira' sia l'input che l'output al
dispositivo hardware.
Quando il driver attende in modo `blocking' che il
dispositivo diventi pronto, si addormenta chiamando
interruptible_sleep_on()
. Una interruzione valida
termina il sonno, facendo ripartire skel_write()
.
Non vi e` doppio ciclo che occorre quando si lavora con
in buffer interno di output. La ragione e' che se possiamo scrivere
solo dall'interno di
skel_write()
non c'e' bisogno di un buffer di output.
Ma il nostro driver deve anche essere in grado di ricevere dati anche
quando non si trova in skel_read()
, a dovrebbe
spedire i dati di output in background anche quando non si trova
in skel_write()
. Percio', cambieremo skel_write()
in modo da sostituire la scrittura diretta all'hardware con il salvataggio
in un buffer di output, e lascieremo al gestore di interruzione
il compito di fare il trasferimento reale verso l'hardware. L'interrupt
e skel_write()
saranno poi collegati dal `meccanismo della
bella addormentata' e dal buffer di output.
Il gestore di interruzione viene installato e disiinstallato
durante le chiamate di apertura e chiusura del dispositivo, come
suggerito nell'articolo precedente. Il compito viene gestito
dalle seguenti funzioni del kernel:
#includeint request_irq(unsigned int irq, void (*handler) (int, struct pt_regs *), unsigned long flags, const char *device); void free_irq(unsigned int irq);
handler
e' la funzione che vogliamo installare. Il ruolo
dell'argomento flags
e' di selezionare le caratterische del
gestore di interruzione, la piu' importante e' se il gestore di
interruzione e' veloce (SA_INTERRUPT
e' settato in
flags
) oppure lento (SA_INTERRUPT
non e'
settato). Un gestore veloce viene fatto girare con tutte le
interruzioni disabilitate, mentre uno lento viene eseguito con tutte
le altre interruzione (tranne la sua stessa) abilitate.
Infine, l'argomento device
viene usato per identificare
il gestore quando si legge /proc/interrupts
.
NDT: Con la versione 1.3.70 del kernel viene aggiunto un
argomento a request_interrupt()
, a
free_interrupt()
e al gestore di interruzione vero e
proprio. Purtroppo non ho tempo di specificare le differenze
prima che questa versione del Pluto Journal venga pubblicata
-- mea culpa.
Il gestore di interruzione installato tramite request_irq()
riceve come argomenti solo il numero di interruzione e il contenuto,
spesso inutile, dei registri del processore.
Il nostro gestore di interruzione dovra' quindi come prima cosa determinare a quale scheda l'interruzione si riferisce. Se non possiamo trovare di quale scheda si tratta, chiameremo questa situazione ``interruzione spuria'' e la ignoreremo. Nella maggior parte dei casi le interruzioni sono usate per dire che il dispositivo e' pronto o per accettare una lettura o una scrittura, quindi dovremo trovare tramite dei test hardware cosa il dispositivo si aspetta che noi facciamo.
Naturalmente, dovremo lasicare il nostro gestore di interruzione
il piu' velocemente possibile; stranamente, pero', printk()
(e quindi PDEBUG()
), sono permesse anche all'interno di un gestore
di interruzione `veloce'. Questa e' una caratteristica molto comoda
della implementazine di Linux. Guardando in kernel/printk.c
si
trovera' che l'implementazione e' basata, ancora una volta, su pre
di attesa, in quanto la effettiva consegna dei messaggi ai files di
log viene gestita da un processo esterno (solitamente klogd).
Linux puo' gestire un timeout durante
interruptible_sleep_on()
. Per esempio, se si stanno mandando
dei dati ad un dispositivo che dovrebbe rispondere entro un tempo
limitato, la gestione di un timeout al fine di segnalare un errore di
I/O (ritornando -EIO
) al processo che sta usando il dispositivo
potrebbe essere una buona scelta.
Certamente il processo utente potrebbe gestire la cosa da solo,
usando il mecchanismo di alarm()
, ma risulta decisamente piu'
comodo gestire la cosa nel driver stesso. Il tempo limite (il timeout)
viene specificato da SKEL_TIMEOUT
, che viene contato in `jiffies'.
Un `jiffy' e' il tempo del battito del cuore di un sistema Linux, e
la variabile jiffies
viene incrementata ad ogni battito.
La frequenza dell'interruzione del clock (il battito in questione) e'
HZ
, definito in
(incluso da
). Tale frequenza varia tra le differenti
a'rchiterrure supportate e vale, per esempio, 100 Hz su x86 e 1kHz su
Alpha). Per definire un timeour prima di interruptible_sleep_on()
bastano le seguenti linee:
#define SKEL_TIMEOUT (timeout_seconds * HZ) /* ... */ current->timeout = jiffies + SKEL_TIMEOUT
interruptible_sleep_on
dovesse protrarsi oltre il timeout,
current->timeout
verrebbe azzerato prima che la funzione
ritorni, un controllo di current->timeout
e' quindi sufficiente
per riconoscere la condizione di errore.
Bisogna fare attenzione al fatto che le interruzioni possono venire generate
a'nche durante l'esecuzione di skel_read()
e skel_write()
.
Le variabili che possono essere cambiate all'interno di un gestore di
interruzione devono percio' essere dichiarate volatile
, e
devono essere protette contro le corse critiche (race conditions).
La classica sequenza di codice per proteggere le regioni critiche
e' la seguente:
unsigned long flags; save_flags (flags); cli (); regione_critica(); restore_flags (flags);
Ed ecco, infine, il codice vero e proprio per la lettura e la scrittura.
#define SKEL_IBUFSIZ 512 #define SKEL_OBUFSIZ 512 /* for 5 seconds timeout */ #define SKEL_TIMEOUT (5*HZ) /* This should be inserted in the Skel_Hw structure */ typedef struct Skel_Hw { /* write position in input-buffer */ volatile int ibuf_wpos; /* read position in input-buffer */ int ibuf_rpos; /* the input-buffer itself */ char *ibuf; /* write position in output-buffer */ int obuf_wpos; /* read position in output-buffer */ volatile int buf_rpos; char *obuf; struct wait_queue *skel_wait_iq; struct wait_queue *skel_wait_oq; ... } #define SKEL_IBUF_EMPTY(b) \ ((b)->ibuf_rpos==(b)->ibuf_wpos) #define SKEL_OBUF_EMPTY(b) \ ((b)->obuf_rpos==(b)->obuf_wpos) #define SKEL_IBUF_FULL(b) \ (((b)->ibuf_wpos+1)%SKEL_IBUFSIZ==(b)->ibuf_rpos) #define SKEL_OBUF_FULL(b) \ (((b)->obuf_wpos+1)%SKEL_OBUFSIZ==(b)->obuf_rpos) Static int skel_open (struct inode *inode, struct file *filp) { /* .... */ /* First we allocate the buffers */ board->ibuf = (char*) kmalloc (SKEL_IBUFSIZ, GFP_KERNEL); if (board->ibuf == NULL) return -ENOMEM; board->obuf = (char*) kmalloc (SKEL_OBUFSIZ, GFP_KERNEL); if (board->obuf == NULL) { kfree_s (board->ibuf, SKEL_IBUFSIZ); return -ENOMEM; } /* Now we clear them */ ibuf_wpos = ibuf_rpos = 0; obuf_wpos = obuf_rpos = 0; board->irq = board->hwirq; if ((err=request_irq(board->irq, skel_interrupt, SA_INTERRUPT, "skel"))) return err; } Static void skel_interrupt(int irq, struct pt_regs *unused) { int i; Skel_Hw *board; for (i=0, board=skel_hw; iirq==irq) break; if (i==skel_boards) return; if (board_is_ready_for_input()) skel_hw_write (board); if (board_is_ready_for_output()) skel_hw_read (board); } Static inline void skel_hw_write (Skel_Hw *board){ int rpos; char c; while (! SKEL_OBUF_EMPTY (board) && board_ready_for_writing()) { c = board->obuf [board->obuf_rpos++]; write_byte_c_to_device(); board->obuf_rpos %= SKEL_OBUF_SIZ; } /* Sleeping Beauty */ wake_up_interruptible (board->skel_wait_oq); } Static inline void skel_hw_read (Skel_Hw *board) { char c; /* If space left in the input buffer & device ready: */ while (! SKEL_IBUF_FULL (board) && board_ready_for_reading()) { read_byte_c_from_device(); board->ibuf [board->ibuf_wpos++] = c; board->ibuf_wpos %= SKEL_IBUFSIZ; } wake_up_interruptible (board->skel_wait_iq); } Static int skel_write (struct inode *inode, struct file *file, char *buf, int count) { int n; int written=0; Skel_Hw board = (Skel_Hw*) (file->private_data); for (;;) { while (written obuf [board->obuf_wpos] = get_user_byte (buf); buf++; board->obuf_wpos++; written++; board->obuf_wpos %= SKEL_OBUFSIZ; } if (written) return written; if (file->f_flags & O_NONBLOCK) return -EAGAIN; current->timeout = jiffies + SKEL_TIMEOUT; interruptible_sleep_on ( &(board->skel_wait_oq)); /* Why did we return? */ if (current->signal & ~current->blocked) /* If the signal is not not being blocked */ return -ERESTARTSYS; if (!current->timeout) /* no write till timout: i/o-error */ return -EIO; } } Static int skel_read (struct inode *inode, struct file *file, char *buf, int count) { Skel_Hw board = (Skel_Hw*) (file->private_data); int bytes_read = 0; if (!count) return 0; if (SKEL_IBUF_EMPTY (board)) { if (file->f_flags & O_NONBLOCK) /* Non-blocking */ return -EAGAIN; current->time_out = jiffies+SKEL_TIMEOUT; for (;;) { skel_tell_hw_we_ask_for_data(); interruptible_sleep_on ( &(board->skel_wait_iq)); if (current->signal & ~current->blocked) return -ERESTARTSYS; if (! SKEL_IBUF_EMPTY (board)) break; if (!current->timeout) /* Got timeout: return -EIO */ return -EIO; } } /* if some bytes are here, return them */ while (! SKEL_IBUF_EMPTY (board)) { put_user_byte (board->ibuf [board->ibuf_rpos], buf); buf++; board->ibuf_rpos++; bytes_read++; board->ibuf_rpos %= SKEL_IBUFSIZ; if (--count == 0) break; } if (count) /* still looking for some bytes */ skel_tell_hw_we_ask_for_data(); return bytes_read; }
select()
L'ultima importante funzione di I/O che mostriamo e'
select()
, una delle parti piu' interessanti du Unix, nella
nostra opinione.
La chiamata di sistema select()
viene usara per attendere che
qualche dispositivo diventi pronto, ed e' una delle funzioni piu'
difficili per il novello programmatore C. Mentre il suo uso
dall'interno dell'applicazione non viene mostrato qui, la parte
dell'implementazione specifica del device driver e' mostrata qui
sotto, e la sua caratteristica che colpise di piu' e' la sua
compattezza.
Ecco il codice completo:
Static int skel_select(struct inode *inode, struct file *file, int sel_type, select_table *wait) { Skel_Clientdata *data=filp->private_data; Skel_Board *board=data->board; if (sel_type==SEL_IN) { if (! SKEL_IBUF_EMPTY (board)) /* readable */ return 1; skel_tell_hw_we_ask_for_data(); select_wait(&(hwp->skel_wait_iq), wait); /* not readable */ return 0; } if (sel_type==SEL_OUT) { if (! SKEL_OBUF_FULL (board)) return 1; /* writable */ /* hw knows */ select_wait (&(hwp->skel_wait_oq), wait); return 0; } /* exception condition: cannot happen */ return 0; }
Come si vede, il kernel si prende cura di tutti i problemi della gestione delle pre di attesa, e occurre solo controllare che il dispositivo sia pronto.
Quando abbiamo scritto la prima implementazione di select()
per
il nostro driver, non avevamo ancora capito la implementazione delle
pre id attesa, ma questo in effetti non serve ad un programmatore. Occorre
solo sapere che il codice mostrato funziona. Le pre di attesa
sono problematiche, e di solito durante la scrittura di un
driver non si ha il tempo di affrontare il problema nei dettagli.
In effetti, select()
e' compresa meglio nelle sue relazioni
con read()
e write()
: se select()
dice che
il file e' leggibile, allora la prossima read()
non si
blocchera' (indipendentemente da O_NONBLOCK
). Questo
significa che occorre dire all'hardware di ritornare qualche dato.
L'interruzione raccogliera' i dati e svegliera' la
coda di attesa. Se l'utente sta selezionando per la
scrittura, la situazione e' simile: il driver deve
dire se la prossima write()
si blocchera' o no. Se il buffer e'
pieno si blocchera', ma non occorre riferire questo evento all'hardware
in quanto write()
doverbbe averlo gia' notificato (durante il
riempimento del buffer). Se il buffer non e' pieno, write()
non si blocchera', quindi select()
ritornera' 1.
Questo modo di pensare select()
per la scrittura puo' sembrare
strano, in quanto capita di aver bisogno di scritture sincrone, e
un programmatore si aspetterebbe che un dispositivo sia scrivibile
quando ha gia' accettato tutto l'output precedente. Sfortunatamente,
questo modo di implementare le cose romperrebbe tutto il macchinario
delle operazioni blocking/non-blocking, e quindi viene fornita
a'l programmatore esigente una chiamata di sistema extra: il driver
dovrebbe in questo caso offrire una operazione fsync()
a'll'interno delle sue fops
. L'applicazione invochera' allora
fops->fsync
attraverso la chiamata di sistema fsync
,
e se il driver non supporta tale funzione, -EINVAL
verra'
ritornato al programma.
ioctl()
--- Passaggio di informazioni di controllo
Immaginiamo di voler cambiare la baud-rate della scheda
serialie multiporta che abbiamo costruito. O che dobbiamo dire al
frame grabber di cambiare la risoluzione. O quasiasi altra cosa....
Si potrebbero impacchettare queate istruzxioni all'interno di
sequenze di escape, come, per esempio, si fa per posizionare il
cursore sui terminali ansi, ma ill metodo comunemente usato e'
la funzione ioctl()
.
Le chiamate ioctl()
sono definite in
e hanno la forma
ioctl (int file_handle, int command, ...)
char *
(in base
a'lla pagina del manuale). Stranamente, il kernel riceve questi
a'rgomenti tipizzati diversamente in fs/ioctl.c
, dove appare
la forma:
int sys_ioctl (unsigned int fd, unsigned int cmd, unsigned long arg);
Per aumentare la confusione,
da le regole
dettagliate riguardo a come devono essere costruiti i comandi che
a'ppaiono come secondo parametro, ma nonostante cio' sono ancora pochi
i driver che seguono queste buone idee -- principalemente perche` i
vecchi driver devono mantenere i vecchi comandi, per non creare grosse
incompatibilita' con le applicazioni.
Invece di pulire i sorgenti del kernel cerchiami di capire l'idea
generale delle chiamate ioctl()
; il modo piu' corretto per
definire i comandi di ioctl e' descritto bene in
Documentation/ioctl-number.txt
, ma noi ci limiteremo qui
a'lla forma semplice, anche se e' sconsigliata.
Il programma passa al kernel come primi argomenti di una chiamata
ioctl()
il file descriptor e un numero di comando, un puntatore
ad una struttura dati che deve essere letta, scritta, o letta-e-scritta,
dal driver viene passato come terzo parametro (opzionale). Alcuni comandi
vengono interpretati dal kernel prima di passare il controllo
al dirver, come FIONBIO
, che cambia il valore del
flag blocking/non-blocking per il file. Il resto dei comandi, la
maggior parte, vengono passati alla nostra funzione ioctl()
all'interno del driver. Il prototipo di tale funzione e' il seguente:
int skel_ioctl (struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
Prima di mostrare un breve esempio di implementazione per skel_ioctl()
, diamo la regola per costruire i numeri dei comandi (ma ricordiamo che
questo e' il metodo sconsigliato: un programmatore attento dovrebbe
seguire le regole in ioctl-number.txt
).
Documentation/ioctl-number.txt
, questo numero
cosituira' gli otto bit alti del numero a 16 bit che constituisce
il comando.
Perche' questo? Immaginiamo che Pierino faccia partire il suo favorito
programma di comunicazione per connettersi alla sua casella postale
favorita. Pierino ha casualmente cambiato la linea seriale usata da
minicom
da /dev/ttyS0
a /dev/skel0
(e' stata
una mossa abbastanza stupida, in effetti). La prima operazione
effettuata da minicom e' inizializzare la `linea seriale', usando
una ioctl()
per passare il comando TCGETA
.
Sfortunatamente, il nostro driver che sta dietro /dev/skel0
usa lo stesso numero per controllare il voltaggio di un esperimento
che sta girando da giorni nel laboratorio...
Se gli otto bit alti dei comandi di ioctl()
sono diversi da un driver all'altro, ogni ioctl()
che
si riferisca ad un device inappropriato fara ritornare il
valore -EINVAL
, proteggendoci da risultati inaspettati
come quello descritto sopra.
Per finire questa sezione implementeremo ora una funzione
ioctl()
per leggere o modificare il valore attuale del timeout
del nostro driver. Se si vuole usare questa funzione occorre
pero' introdurre una nuova variabile nel driver dopo la definizione
di SKEL_TIMEOUT
:
unsigned long skel_timeout = SKEL_TIMEOUT;
SKEL_TIMEOUT
andra' poi sostiuita con
skel_timeout
.
Abbiamos poi scelto il numero magico '4'
(il codice ascii per il
numero 4), e abbiamo definito due comandi:
# define SKEL_GET_TIMEOUT 0x3401 # define SKEL_SET_TIMEOUT 0x3402Nel nostro programma, queste linee saranno usate per raddoppiare il valore corrente del timeout:
/* ... */ unsigned long timeout; if (ioctl (skel_hd, SKEL_GET_TIMEOUT, &timeout) < 0) { /* an error occured (Silly Billy?) */ /* ... */ } timeout *= 2; if (ioctl (skel_hd, SKEL_SET_TIMEOUT, &timeout) < 0) { /* another error */ /* ... */ }
Nel driver, invece, queste linee saranno quelle che compiono il lavoro:
int skel_ioctl (struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg) { switch (cmd) { case SKEL_GET_TIMEOUT: put_user_long(skel_timeout, (long*) arg); return 0; case SKEL_SET_TIMEOUT: skel_timeout = get_user_long((long*) arg); return 0; default: return -EINVAL; /* for Silly Billy */ } }
Nelle prossime sezioni investigheremo i segreti della
gestione di meoria, e entreremo in contatto con il DMA e
mmap()
, fornendo al nostro driver il meglio
della gestione di memoria di Linux.
Georg e Alessandro sono linuxers di 27 anni con un gusto per il lato pratico dell'informatica e una tendenza a dilazionare il sonno.
di Georg v. Zezschwitz and Alessandro Rubini
[About] [Copertina] |