[Linux, telefoni e pigrizia] [About] [Copertina] [CHAT-QSO]

Articoli



Il testo seguente è la traduzione di quello che è apparso sul Linux Journal di Maggio '96. Chi di voi riceve la rivista troverà poche novità rispetto a quello che ha già letto (ci sono solo alcune note aggiunte durante la traduzione). 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.

Se a qualcuno interessa avere sotto mano il testo originale di tutti e cinque gli articoli, li può trovare sotto ftp://ftp.systemy.it/pub/develop/kernel-korner.

Questo è il quarto articolo di una serie sulla scrittura di device drivers a caratteri sotto forma di moduli del kernel. Questo mese approfondiremo l'argomento della gestione delle interruzioni (interrupts). Nonostante l'argomento sia concettualmente semplice, limitazioni pratiche rendono questa parte una delle più ``interessanti'' nella scrittura dei driver, e il kernel offre una serie di funzioni di supporto per le differenti problematiche. In quest'articolo cercheremo anche di introdurre il complesso argomento del DMA (direct memory access).

Nonostante il precedente articolo possa aver dato l'idea di coprire tutto l'argomento della gestione delle interruzioni, questo mese vedremo i dettagli più profondi dell' `interrupt handling'. Introdurremo anche all'affascinate mondo della gestione della memoria spiegando cosa deve fare un driver che gestisca il DMA.

Cambiamenti nelle nuove versioni di Linux.

Prima di cominciare ci piacerebbe specificare due cambiamenti che sono avvenuti nelle versioni di Linux più `recenti'.

NDT: Si tratta dei cambiamenti che non ho avuto tempo
di specificare durante la traduzione del precedente articolo.

Nella versione 1.3.70 viene supportata lo condivisione delle linee di interrupt. L'idea è che diverse periferiche, e quindi i loro driver, condividano la stessa interrupt, e che il software sia in grado di gestire ogni interruzione correttamente.

Per poter correttamente gestire diversi driver che rispondono alla stessa interruzione, il kernel ha bisogno di identificare ciascun driver. I prototipi delle funzioni request_irq e free_irq sono stati quindi cambiati nei seguenti:

	extern int request_irq(unsigned int irq,
	   void (*handler)(int, void *, struct pt_regs *),
	   unsigned long flags,
	   const char *device,
	   void *dev_id);
	extern void free_irq(unsigned int irq, void *dev_id);

Quando si registra un gestore di interrupt, quindi, un quarto parametro va passato a request_irq(): un puntatore che identifichi il dispositivo. Lo stesso puntatore deve poi essere passato quando l'interrupt viene rilasciata. Nella maggior parte dei casi il driver può semplicemente passare un puntatore nullo come dev_id, a meno che non permetta la condivisione della sua linea di interruzione. Se la condivisione è supportata dal driver, il bit SA_SHIRQ deve essere settato nel parametro flags, e dev_id deve essere un puntatore che identifichi univocamente il dispositivo.

Il secondo cambiamento non è un vero cambiamento, ma piuttosto un aggiornamento stilistico: get_user_byte() e put_user_byte() sono considerati obsoleti, e non dovrebbero essere usati sul codice nuovo. Questi sono stati sostituiti con le più flessibili chiamate get_user e put_user.

Linus spiega che queste funzioni usano caratteristichew particolari del gcc per conoscere,tramite il puntatore che ricevono come argomento, la giusta dimensione dell'oggetto. Questo significa che non potete usare void * o unsigned long come puntatore; dovete sempre usare un puntatore al tipo corretto. Inoltre, se date un char *, ottenete come ritorno un char, non un unsigned char, diversamente dal vecchio get_fs_byte(). Se necessitate di un valore unsigned, usate un puntatore a un tipo unsigned. Non forzate mai il valore di ritorno per avere la dimensione di accesso che volete---se pensate di averne bisogno, vi state sicuramente sbagliando.

Essenzialmente, dovete pensare a get_user() come dereferenza di un semplice puntatore (un pò come *(xxx) in C, solo che i dati vengono presi dallo spazio utente. In effetti, su alcune architetture, questo è proprio quello che accade.

Mentre cerchiamo di sistemare le precedenti sviste, vale la pena notare che il kernel fornisce una funzione per individuare automaticamente le linee di interrupt. È leggermente differente rispetto a quanto era stato esposto qualche mese fa. Chi è interessato ad ottenere maggior documentazione a riguardo, può

Torniamo ora alla nostra schedulazione dei programmi.

Interrupt divisi in due parti.

Come vi ricorderete dal precedente articolo, la gestione degli interrupt è effettuata da una singola funzione del driver. La nostra precedente implementazione si è occupata sia di questioni a basso-livello (intercettare l'interrupt) che ad alto-livello ( come l'attivazione di altri task). Questo modo di procedere può funzionare per drivers semplici, ma è destinato a fallire se la gestione è troppo lenta.

Se si guarda al codice, à chiaro che sia l'intercettazione dell'interrupt che la richiesta o l'invio di dati sono solo una minima parte del lavoro. Con i comuni device in cui si spostano solo pochi bytes per interrupt, la maggior parte del tempo è utilizzato per gestire strutture dati specifiche del device come code, chains, o altre strane strutture utilizzate nell'implementazione del device stesso. Non prendete il nostro skel_interrupt() come un esempio, poichè si tratta del più semplice gestore di interrupt possibile; un vero device potrebbe avere più di un modo di operare e diverse informazioni sullo stato. Se si spende troppo tempo nell'elaborare strutture dati è possibile che vengano perduti uno o più interrupt successivi e quindi accumulare o perdere dati, poichè quando un gestore di interrupt è in esecuzione, almeno quell'interrupt è bloccato, e se il gestore è di tipo `veloce' (cioè se SA_INTERRUPT è stato specificato durante la sua registrazione), tutti gli interrupt sono bloccati.

La soluzione escogitata per questo problema è quella di dividere il lavoro di gestione dell'interrupt in due parti:

Fortunatamente, il kernel prevede un particolare sistema per effettuare lo scheduling del ``bottom half'', che non è necessariamente legato ad un processo in particolare; questo significa che sia la richiesta di esecuzione della funzione che la esecuzione stessa sono fatte fuori del contesto di qualsivoglia processo. Per fare ciò è necessario un meccanismo speciale perchè le altre funzioni del kernel operano tutte nel contesto di un processo---un ordinato flusso di istruzioni, normalmente associato ad un'istanza di un programma correntemente in esecuzione---mentre la gestione degli interrupt è asincrona, e non correlata ad un particolare processo.

Bottom half: quando e come.

Dal punto di vista di un programmatore, la ``bottom half'' è molto simile alla ``top half'', nel senso che essa non può chiamare la schedule() e può solo effettuareallocazioni di memoria atomiche (cioè con priorità GPM_ATOMIC). Ciò è comprensibile, poichè la funzione non è chiamata nel contesto di un processo; la bottom half è asincrona, proprio come la top half---il tradizionale gestore di interrupt. La differenza principale è che gli interrupt sono abilitati e non è in esecuzione codice critico. Quindi, quando sono eseguite esattamente queste routines?

Come sapete, il processore lavora principalmente per conto di un qualche processo, sia in ``user space'' che in ``kernel space'' (durante la esecuzione delle chiamate dis sitema). Le principali eccezioni sono la gestione degli interrupt e lo scheduling di un altro processo al posto di quello corrente: durante queste operazioni la variabile puntatore current non ha significato, anche se rimane un puntatore valido ad una struttura struct task_struct. Inoltre il kernel controlla la CPU quando processo entra o esce da una system call, e ciò accade piuttosto spesso, poichè una singola funzione gestisce tutte le chiamate di sistema.

Tenendo presente questo, è evidente che se volete che il vostro bottom-half sia eseguito il più presto possibile, esso deve essere invocato dallo scheduler e all'ingresso o all'uscita di una system call, poichè non è possibile farlo durante la gestione degli interrupt. Attualmente Linux chiama do_bottom_half() (definita in kernel/softirq.c) dall'interno di schedule() (in kernel/sched.c) e da ret_from_sys_call() (definita in un file dipendente dall'architettura della macchina, normalmente entry.S).

I bottom-half non sono legati al numero di interrupt, anche se il kernel tiene un array statico di 32 funzioni di questo tipo. Attulamente (io uso la versione 1.3.71 del kernel) no c'è momdo di richiedere al kernel un numero (o id) di bottom-half non utilizzato, quindi deve esserene definito uno a priori. Questo non è un modo elegante di programmare, ma è usato solo per rendere l'idea; più avanti elimineremo questo ``furto'' di id.

Per eseguire il bottom-half, dovrete preventivamente segnalarlo al kernel. Questo si ottiene con la funzione mark_bh(), che accetta un argomento: la id del bottom-half.

Questo listato mostra il codice di un gestore di interrupt diviso in due parti che usa una allocazione di id poco ortodossa.


	#define SKEL_BH 16 /* dirty planning */

	/*
	 * This is the top half, argument to request_irq()
	 */
	static void skel_interrupt(int irq,
	                           struct pt_regs *regs)
	{
	  do_top_half_stuff();

	  /* tell the kernel to run the bh later */
	  mark_bh(SKEL_BH);
	}

	/*
	 * This is the bottom half
	 */
	static void do_skel_bh(void)
	{
	  do_bottom_half_stuff();
	}

	/*
	 * But the bh must be initialized ...
	 */
	int init_module(void)
	{
	  /* ... */
	
	  init_bh(SKEL_BH, do_skel_bh);
	
	  /* ... */
	}

	/*
	 * ... and cleaned up
	 */
	void cleanup_module(void)
	{
	  /* ... */

	  disable_bh(SKEL_BH)

	  /* ... */
	}
Utilizzo delle ``task queues''

In realtà, la allocazione forzata dell id di un bottom-half non è necessaria poichè il kernel implementa un meccanismo più sofisticato che sicuramente apprezzerete.

Questo meccanismo è chiamato delle ``task queues'' perchè le funzioni che devono essere chiamate sono tenute in code costruite con liste linkate. Questo significa che potete registrare più di un bottom-half relativo all'hardware gestito dal vostro driver. Inoltre le task queues non sono direttamente legate alla gestione degli interrupt e possono essere usate indipendentemente da questa.

Una task queue è una lista di struct tq_struct come dichiarato in .

	struct tq_struct {
	    /* linked list of active bh's */
	    struct tq_struct *next;
	    /* must be initialized to zero */
	    int sync;
	    /* function to call */
	    void (*routine)(void *);
	    /* argument to function */
	    void *data;
	};
	typedef struct tq_struct * task_queue;

	void queue_task(struct tq_struct *bh_pointer,
	                task_queue *bh_list);
	void run_task_queue(task_queue *list);

Avrete notato che il campo routine della tq_struct è una funzione che accetta come argomento un puntatore. Questa è una caratteristica utile, come presto scoprirete da soli, ma ricordate che la gestione del campo data è sotto la vostra completa responsabilità: se punta a memoria allocata con la kmalloc(), essa deve essere rilasciata da voi.

Un'altra cosa da tenere a mente è che il campo next è usato per mantenere la lista consistente, perciò dovrete fare attenzione a non modificarlo, e a non inserire mai la stessa tq_struct in code diverse nè due volte nella stessa coda.

Ci sono alcune altre funzioni simili alla queue_task() nell'header che vale la pena di guardare.

Per usare una task queue dovrete dichiarare una vostra lista o aggiungere i task ad una lista predefinita. Nel seguito esamineremo entrambi i metodi.

Questo listato mostra chome eseguire più di un bottom-half nel vostro gestore di interrupt mediante una coda da voi creata.


	DECLARE_TASK_QUEUE(tq_skel);

	#define SKEL_BH 16 /* dirty planning */

	/*
	 * Two different tasks
	 */

	static struct tq_struct task1;
	static struct tq_struct task2;

	/*
	 * The bottom half only runs the queue
	 */
	static void do_skel_bh(void)
	{
	  run_task_queue(&tq_skel);
	}

	/*
	 * The top half queues the different tasks based
	 * on some conditions
	 */
	static void skel_interrupt(int irq,
	                           struct pt_regs *regs)
	{

	  do_top_half_stuff();

	  if (condition1()) {
	    queue_task(&task1, &tq_skel);
	    mark_bh(SKEL_BH);
	  }

	  if (condition2()) {
	    queue_task(&task2, &tq_skel);
	    mark_bh(SKEL_BH);
	  }
	}

	/*
	 * And init as usual
	 */

	int init_module(void)
	{
	  /* ... */

	  task1.routine=proc1; task1.data=arg1;
	  task2.routine=proc2; task2.data=arg2;
	  init_bh(SKEL_BH, do_skel_bh);

	  /* ... */
	}

	void cleanup_module(void)
	{
	  /* ... */

	  disable_bh(SKEL_BH)

	  /* ... */
	}

Usare le task queue è una esperienza divertente e aiuta a mantenere il vostro codice leggibile. Per esempio, se state eseguendo la skel-machine descritta nei precedenti articoli del kernel Korner, potrete gestire più di un dispositivo hardware utilizzando lo stesso gestore di interrupt che riceva come argomento informazioni specifiche sull'hardware che ha causato l'interrupt. Questo comportamento può essere ottenuto inserendo una tq_struct come membro della struttura Skel_Hw. Il grosso vantaggio che si ottiene è che se più dispositivi richiedono attenzione quasi contemporaneamente, tutti sono messi nella coda e gestiti tutti insieme in un secondo momento (con gli interrupt abilitati). Naturalmente ciò funziona solo se l'hardware non è troppo pignolo sul quando gli interrupt sono intercettati e gestiti.

Utilizzo delle liste predefinite.

Il kernel definisce tre task queue a disposizione del programmatore. Un device driver dovrebbe normalmente usare una di queste code invece di dichiararne di nuove. L'unica ragione per cui vi sono code speciali per alcune delle funzionalità del kernel è per ottenere una performance più elevata: code con un id minore sono eseguite prima.

Le tre code predefinite sono:

	struct tq_struct *tq_timer;
	struct tq_struct *tq_scheduler;
	struct tq_struct *tq_immediate;

La prima è eseguita ad ogni interrupt del clock (timer) e la sua discussione viene lasciata come esercizio al lettore. La successiva è eseguita ogniqualvolta avviene lo scheduling di un processo mentre i task associati all'ultima vengono eseguiti ``immediatamente'' all'uscita del gestore di interrupt, come bottom half generici. Questa coda è quella che viene normalmente utilizzata al posto del sistema di gestione dei bottom half precedentemente descritto.

La coda tq_immediate come la tq_skel dell'esempio precedente. Non c'è bisogno di scegliere un id e di dichiararlo, anche se mark_bh() deve ancora essere chiamata, con l'argomento IMMEDIATE_BH come mostrato di seguito. Corrispondentemente la coda tq_timer usa mark_bh(TIMER_BH) mentre la coda tq_scheduler non necessita di essere marcata per essere eseguita.

Questo listato mostra un esempio di utilizzo della coda ``immediata''.


	/*
	 * Two different tasks
	 */

	static struct tq_struct task1;
	static struct tq_struct task2;

	/*
	 * The top half queues tasks, and no bottom
	 * half is there
	 */
	static void skel_interrupt(int irq,
	                           struct pt_regs *regs)
	{

	  do_top_half_stuff();

	  if (condition1()) {
	    queue_task(&task1,&tq_immediate);
	    mark_bh(IMMEDIATE_BH);
	    }

	  if (condition2()) {
	    queue_task(&task2,&tq_skel);
	    mark_bh(IMMEDIATE_BH);
	    }
	}

	/*
	 * And init as usual, but nothing has to be
	 * cleaned up
	 */

	int init_module(void)
	{
	  /* ... */

	  task1.routine=proc1; task1.data=arg1;
	  task2.routine=proc2; task2.data=arg2;

	  /* ... */
	}
Un esempio: utilizzo della tq_scheduler.

Le task queue sono degli oggetti interessanti da utilizzare ma la maggioranza di noi non possiede hardware che necessiti una gestione posticipata degli interrupt. Fortunatamente l'implementazione di run_task_queue() è abbastanza flessibile da permetterne l'uso anche senza hardware adatto.

La buona notizia è che run_task_queue() chiama le funzioni ad essa accodate dopo averle rimosse dalla coda. Cosi' potete reinserire un task nella coda dall'interno del task stesso. Attenzione però che questo funziona solo dalla version 1.3.70 in poi: reinserire lo stesso task in una coda blocca i kernel più vecchi.

Questo task d'esempio si limita a stampare un messaggio ogni 10 secondi, fino alla fine del mondo. Necessita unicamente di essere attivato una sola volta e sarà in grado di cavarsela da solo per il resto della sua vita.

	struct tq_struct silly;

	void silly_task(void *unused)
	{
	  static unsigned long last;
	  if (jiffies/HZ/10 != last) {
	    last=jiffies/HZ/10;
	    printk(KERN_INFO "I'm there\n");
	  }
	queue_task(&silly, &tq_scheduler);
	/* tq_scheduler doesn't need mark_bh() */
	}

Se temete di trovarvi dinnanzi ad un virus apsettate, e ricordate che un amministratore prudente non esegue nulla come root senza leggere prima il codice :-).

Ma adesso lasciamo le code dei task e iniziamo ad esplorere le funzionatità della memoria...

DMA sui PC---Dannato Mostruoso Accrocchio

Vi ricordate i vecchi tempi dei PC? Quei giorni in cui un PC veniva venduto con 128 kB di RAM, un 8086, una interfaccia per cassette magnetiche ed un floppy da 360 kB. Quelli erano i giorni in cui il DMA su bus ISA era considerato veloce. L'idea del DMA è quella di trasferire un blocco di dati da un dispositivo verso la memoria o viceversa senza che la CPU debba occuparsene.

Se si usa il DMA, invece, dopo aver inizializzato sia il device che il controller DMA sulla motherboard, il device segnala al controller DMA che ha dei dati da trasferire. Il controller pone la RAM in uno stato di ricezione dal bus, il device mette il dato sul bus, e alla fine della operazione il controller incrementa un registro di indirizzo e decrementa un contatore di dimensione, cosicchè gli ulteriori trasferimenti andranno in locazioni successive.

A quei tempi questa tecnica era veloce, permettendo transfer rate fino a 800 kB/sec. su bus ISA a 8 bit. Oggi si parla di 132 MB/sec. su bus PCI 2.0. Ma il buon vecchio ISA-DMA ha ancora delle applicazioni: immaginate una scheda sonora che riproduca un campione musicale a 16 bit alla frequenza di 48 kHz in stereo. Questo si traduce in 192 kB/sec. Trasmettere questi dati mediante richieste di interrupt, circa 2 word ogni 20 microsecondi, porterebbe la CPU a perdere molti interrupt. Viceversa la lettura continua della scheda da parte della CPU (polling) a quella frequenza non permetterebbe alla stessa di effettuare molte altre operazioni. Quello di cui abbiamo bisogno è un flusso continuo di dati a velocità media---perfetto per il DMA. Linux deve solo far partire e fermare il flusso---al resto ci pensa l'hardware.

In questo articolo tratteremo solamente l'ISA-DMA---la maggioranza delle schede di espansione è ancora ISA e l'ISA-DMA è abbastanza veloce per molte applicazioni. Tuttavia il DMA sul bus ISA ha notevoli limitazioni:

Allocare un buffer DMA

Bene, ora conoscete le limitazioni---e siete quindi in grado di decidere se proseguire la lettura oppure lasciare perdere!

La prima cosa necessaria per il DMA è un buffer. Le restrizioni (primi 16 MB di memoria, blocco contiguo nella memoria fisica) sono soddisfatte se si alloca il buffer con:

	void  *dma_buf;
	dma_buf = kmalloc(buffer_size,
	                  GFP_BUFFER | GFP_DMA);

L'indirizzo ritornato non sarà mai l'inizio di una pagina, per quanto voi desideriate che lo sia. La ragione è che Linux ha un sistema di gestione dei blocchi di pagine usate o no piuttosto avanzato mediante la funzione kmalloc(). Essa mantiene una lista di intervalli liberi delle dimensioni minime di 32 bytes (64 su DEC Alpha), un'altra lista per blocchi di dimensione doppia, un'altra per dimensioni quadruple, ecc. Ogni volta che si libera memoria precedentemente allocata con kmalloc(), Linux tenterà di unire il blocco rilasciato con uno libero vicino. Se il vicino è anch'esso libero essi vengono passati nella lista di dimensioni doppie, dove il controllo è effettuato nuovamente per reiterare il procedimento.

Le dimensioni che kmalloc() supporta al momento (tutti i kernel da 1.2.x fino a 2.0.24) vanno da PAGE_SIZE >> 7 (32 bytes) a PAGE_SIZE << 5 (128 kB). Ogni incremento nella potenza di due è una lista, per cui due blocchi contigui in una lista formano un blocco unico nella lista di ordine superiore.

Voi potreste chiedere perchè non sia possibile richiedere semplicemente una intera pagina. Perchè all'inizio di ogni segmento sono contenute alcune informazioni sulla lista stessa. Queste informazioni sono chiamate (a volte scorrettamente) page_descriptor, e la loro lunghezza è attualmente compresa fra 16 e 32 bytes (a seconda del tipo di architettura). Perciò, se chiedete a Linux 64 kB di RAM, Linux dovrà usare un blocco libero delle dimensioni di 128 kB e passarvi 128 kB - 32 Bytes.

	NDT: Esiste però un metodo di allocazione migliore, che gli
	autori ignoravano al momento della stesura dell'articolo
	(poverini). La funzione get_free_pages() permette di
	allocare da una a 32 pagine (andando per potenze di due)
	senza la perdita dell'header di kmalloc(). Tali
	pagine si libereranno con free_pages(). I lettori
	interessati sono invitati a guardare negli header del kernel.

Grandi blocchi di memoria libera contigui sono difficili da ottenere---ad es. il driver del floppy a volte non riesce ad allocare il suo buffer DMA a run-time per la mancanza di blocchi contigui di memoria. Perciò ragionate sempre in termini di potenze di due, ma poi sottraete sempre alcuni bytes (circa 32) se usate kmalloc(). Se volete dare un'occhiata ai numeri magici, sbirciate in mm/kmalloc.c.

Il ruolo degli interrupt

La maggior parte dei dispositivi che utilizzano il DMA generano degli interrupt. Per esempio, una scheda sonora genera un interrupt per avvisare la cpu: ``Dammi nuovi dati, gli altri sono già stati elaborati''.

Il sistema per il quale abbiamo scritto il nostro driver è particolarmente strano: è una interfaccia da laboratorio con la sua CPU, la sua RAM, ingressi ed uscite digitali ed analogiche ed tutti gli ammennicoli di contorno. Per il suo controllo essa implementa un character device ed usa il DMA per il trsferimento di blocchi di dati campionati. Perciò la generazione di un interrupt può avere le seguenti ragioni:

Il vostro gestore di interrupt deve interpretare il significato dell'interrupt. Normalmente leggerà un registro di stato sul dispositivo nel quale troverà più dettagliate sul da farsi.

Come si può vedere ci siamo allontanati molto dal generale semplice caricamento e scaricamento di un modulo e siamo nel pieno della specializzazione più spinta. Nella scrittura del driver abbiamo stabilito che esso dovesse implementare le seguenti funzioni:

Questo comportamento differisce da quello del driver del floppy ove voi non ``guarderete'' mai direttamente il buffer DMA. Ma probabilmente vi risulterà ugualmente utile: potreste decidere di usarlo in un frame grabber. L'utente potrebbe allocare diversi buffer, inizializzare il trasferimento DMA per uno, e utilizzare gli altri mentre il primo si riempie. Poichè l'unica cosa che l'utente e il sistema devono fare è alternare l'indirizzo nel quale devono essere scritti i dati ed i buffer sonon mappati nello user-space, neanche un singolo byte dell'immagine deve essere trasferito dalla CPU---essi semplicemente arrivano dove l'utente li desidera. Descriveremo questa procedura in un prossimo articolo.

Prima di presentarvi un esempio di codice vero, permetteteci di ricapitolare i passi che vengono percorsi per un trasferimento completo:

  1. L'interrupt viene generato e un nuovo trasferimento delve iniziare.
  2. L'interrupt handler inizia il trasferimento.
  3. L'interrupt handler ritorna, mentre la cpu inizia la sua normale attività e il trasferimento è in corso.
  4. Un nuovo interupt viene generato, per indicare che il trasferimento è finito.
  5. L'interrupt handler finisce il trasferimento...
  6. ...e probabilmente ne richiede un altro al dispositivo.

Non offendetevi se non scriviamo il vostro device driver per intero---le cose sono molto differenti da situazione a situazione! Ecco il codice per i punti 2 e 5:

	static int skel_dma_start (unsigned long dma_addr,
	                           int dma_chan,
	                           unsigned long dma_len,
	                           int want_to_read) {
	    unsigned long flags;

	    if (!dma_len || dma_len > 0xffff)
	        /* Invalid length */
	        return -EINVAL;
	    if (dma_addr & 0xffff !=
	       (dma_addr + dma_len) & 0xffff)
	        /* We would cross a 64kB-segment */
	        return -EINVAL;
	    if (dma_addr + dma_len > MAX_DMA_ADDRESS)
	        /* Only lower 16 MB */
	        return -EINVAL;
	    /* Don't need any irqs here... */
	    save_flags (flags); cli ();
	    /* Get a well defined state */
	    disable_dma (dma_chan);
	    clear_dma_ff (dma_chan);
	    set_dma_mode (dma_chan,
	                  want_to_read ?
	                  /* we want to get data */
	                  DMA_MODE_READ
	                  /* we want to send data */
	                 : DMA_MODE_WRITE);
	    set_dma_addr (dma_chan, dma_addr);
	    set_dma_count (dma_chan, dma_len);
	    enable_dma (dma_chan);
	    restore_flags (flags);
	    return 0;
	}

	static void skel_dma_stop (int dma_chan) {
	    disable_dma (dma_chan);
	}

Siamo spiacenti di non potervi fornire codice più dettagliato poichè Al solito, il modo migliore di far funzionare le cose è di guardare a qualche implementazione funzionante.

Approfondimenti

Se volete addentrarvi maggiormente nell'argomento trattato, la miglior fonte di insegnamento, come sempre, è il codice. Gli interrupt handler divisi a metà e le code dei task sono usati dappertutto nella versione attuale del kernel, mentre l'implementazione del DMA mostrata in questo articolo è ottenuta dal nostro ceddrv-0.17, disponibile via ftp su tsx-11.mit.edu o su uno dei suoi mirror.

Il prossimo articolo tratterà di argomenti più concreti---ci rendiamo conto che il DMA e le task queue possono apparire argomenti piuttosto esoterici. Mostraremo il funzionamento della funzione mmap() e come i driver possono implementarla.

Georg and Alessandro sono entrambi ventisettenni appasionati di Linux con una predilezione per gli aspetti pratici dell'informatica e una tendenza a dilazionare il sonno. Questo li aiuta a rispettare le loro scadenze sfruttando i differenti fusi orari.

di Georg v. Zezschwitz and Alessandro Rubini


[Linux, telefoni e pigrizia] [About] [Copertina] [CHAT-QSO]