[About] [Copertina] |
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.
Utilizzo delle ``task queues''
#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)
/* ... */
}
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.
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''.
Un esempio: utilizzo della
/*
* 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;
/* ... */
}
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 AccrocchioVi 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:
A0
--A15
sui canali 0-3,
A1
--A16
sui canali 4-7). Gli 8 bit superiori
dell'indirizzo sono rappresentati in un page register. Essi non
cambieranno durante il trasferimento, ciò significa che i trasferimenti
possono avvenire solamente all'interno di un segmento (64 kB) di memoria.
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 funzioneget_free_pages()
permette di allocare da una a 32 pagine (andando per potenze di due) senza la perdita dell'header dikmalloc()
. Tali pagine si libereranno confree_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
.
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:
Prima di presentarvi un esempio di codice vero, permetteteci di ricapitolare i passi che vengono percorsi per un trasferimento completo:
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
[About] [Copertina] |