[Come farsi un newsfeed] [About] [Copertina] [Uki-Debian]

Articoli



Ristampato con il permesso del Linux Journal.

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:

	#include 
	int 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

se 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 (writtenobuf [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;
	}

Come usare 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, ...)

dove i puntini indicano un argimento di tipo 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).

  1. Si scelga un numero `magico' libero usando la lista in Documentation/ioctl-number.txt, questo numero cosituira' gli otto bit alti del numero a 16 bit che constituisce il comando.
  2. 2) Si numerino sequenzialmente i comandi negli otto bit piu' bassi.

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;

Ogni occorrenza di 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 0x3402
Nel 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 */
	    }
	}

Il prossimo numero

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


[Come farsi un newsfeed] [About] [Copertina] [Uki-Debian]