Sei sulla pagina 1di 4

IO INGRESSO – USCITA

COMPITI DELLE UNITA’ DI I/O E COOPERAZIONE CON I PROCESSI DELLA CPU:


Riprendiamo lo schema base dell’architettura di un elaboratore (non sono indicate le cache della CPU):
UNIT: arbitro che gestisce le interruzioni
Tutte le I/O sono collegate al BUS I/O, ma solo alcune
sono collegate anche al BUS DMA.
Le unità periferiche I/O fanno qualcosa ordinato dal
processore, ma in un tempo che è diverso dal ciclo di
clock del processore.
L’unità di I/O dice al Processore “chi è e cosa deve
fare” tramite il BUS di I/O
Ogni unità di I/O svolge il compito di interfacciare, nei
confronti dell’unità centrale, un certo tipo di dispositivo periferico.

L’I/O fa due cose:


1) dice al processore chi è e cosa deve fare = avviene tramite il BUS I/O
2) se ne ha la possibilità, trasferisce direttamente dalla/verso la memoria centrale alcuni dati = avviene
tramite il BUS DMA

Interruzioni ed eccezioni:
La distinzione fondamentale tra questi due tipi di eventi è la seguente :
• le eccezioni (ad esempio, quelle segnalate dalla MMU, oppure quelle di overflow nelle operazioni
aritmetiche) sono eventi sincroni rispetto all’esecuzione del processo corrente : infatti, esse si verificano in
quanto provocati dalla stessa computazione in corso sul processore, la quale è espressa in modo da testare
esplicitamente la presenza di ben determinate eccezioni (solo quelle che interessano) a istanti ben
determinati (a livello firmware, o a livello assembler, o a livelli superiori) ;
• le interruzioni sono eventi asincroni rispetto all’esecuzione del processo corrente : infatti, essi non sono
provocati dalla computazione in corso sul processore, bensì da computazioni che evolvono parallelamente
a essa su unità di I/O (o altri processori); non avrebbe senso che il processo eseguito dal processore
testasse la presenza di ben determinate interruzioni a istanti scelti arbitrariamente ; convenzionalmente si
sceglie allora la fine del microprogramma di ogni istruzione per testare la presenza di una qualsiasi
interruzione, proseguendo subito alla chiamata dell’istruzione successiva se non sono attualmente
segnalate interruzioni presenti.

Distinguiamo due modelli:


• Direct Memory Access (DMA): il trasferimento di dati tra I/O e memoria principale, e viceversa, avviene
indipendentemente attraverso un canale distinto, utilizzando uno o più Bus DMA. Questi trasferimenti, per
definizione, avvengono in parallelo all’elaborazione in corso sul processore. Ovviamente, gli accessi del
processore alla memoria principale avvengono in parallelo all’elaborazione sulle unità di I/O;
• Memory Mapped I/O (MMl/O): poiché a ogni unità di I/O è associata una certa capacità di memoria
locale, questa è indirizzabile direttamente anche dal processore P: il trasferimento dati tra P e I/O avviene
dunque attraverso letture e scritture da parte di P nelle memorie locali di I/O. In altre parole, un
programma in esecuzione su P vede le unità di I/O come memoria. Anche in questo caso gli accessi alla
memoria locale di I/O da parte di una delle due unità - P o I/O - avvengono in parallelo all’elaborazione
dell’altra unità.
Nel modello DMA, M è condivisa tra P e le unità di I/O operanti in DMA. Nel modello MMl/O, ogni memoria
locale di unità di I/O (MI/O) è condivisa tra P e tale unità.
I due modelli possono essere utilmente combinati, dando luogo a un modello in cui ogni trasferimento di
I/O è sempre effettuato tramite una memoria condivisa.

1
Modello DMA: Questo modello viene adottato per unità di I/O che effettuano trasferimenti a blocchi in
modo indipendente da, e quindi in parallelo a, l’elaborazione del processore centrale. Ad esempio, una
semplice unità disco può ricevere richieste così espresse:
• lettura, indirizzo base del blocco su disco da cui leggere, indirizzo base in M del blocco in cui scrivere,
• scrittura, indirizzo base del blocco su disco in cui scrivere, indirizzo base in M del blocco da cui leggere.
Nel caso di unità di I/O realizzate con processori general-purpose, gli indirizzi generati dalle unità di I/O
sono logici e vengono tradotti in indirizzi fisici da una propria MMU.
Nel caso di unità specializzate a firmware, se non fosse prevista una MMU gli indirizzi da esse generati sono
fisici. L ’arbitraggio del Bus DMA è indipendente da quello del Bus di I/O. La memoria principale M si trova
ora ad avere due interfacce: una dedicata con MMU, l’altra con il Bus DMA. Il microprogramma di M è
nondeterministico in quanto deve arbitrare le situazioni di conflitto, per la presenza contemporanea di
messaggi sulle due interfacce d’ingresso, dando priorità a una di esse (tipicamente, al Bus DMA), oppure
stabilendo una disciplina di priorità variabile.

Modello Memory Mapped I/O:


Come detto, a differenza del modello DMA (che ha come scopo quello di implementare trasferimenti diretti
tra M e I/O, “scavalcando” il processore), il modello MMI/O ha come scopo quello di implementare
efficientemente trasferimenti tra processore e unità di I/O.
In molte architetture che usano questo modello, a livello assembler non esistono esplicite istruzioni di I/O,
essendo sufficienti le istruzioni di LOAD e STORE: poiché gli indirizzi generati dal processore sono logici, il
processore ha visibilità di uno spazio uniforme di memoria, senza distinzione tra M e {MI/O}; è solo la MMU
che, mediante l ’indirizzo fìsico ottenuto dalla traduzione dell’indirizzo logico, ha visibilità di tale distinzione.
Questo modello è adottato anche in D-RISC.
Ogni unità di I/O, accedendo solo sulla rispettiva memoria locale, potrà operarvi con indirizzi logici
(processori veri e propri) o fisici (unità specializzate a firmware): questa distinzione è del tutto invisibile ai
programmi che interagiscono con l’unità.
È importante notare come, con l’architettura Memory Mapped I/O, qualunque comunicazione tra
processore e unità di I/O avviene mediante istruzioni di LOAD e STORE. In altri termini, agli effetti del
processore, qualunque informazione debba essere scambiata con una unità di I/O è come se
corrispondesse a una locazione di memoria:
1 . in certi casi tale locazione esisterà realmente, e allora verranno effettivamente eseguite letture/scritture
in essa,
2. in altri sarà una astrazione cui il processore fa riferimento, e allora sarà l’unità di LO a interpretare
opportunamente le richieste di lettura/scrittura per eseguire, in realtà, operazioni diverse.
Ad esempio, supponiamo che un processo interno debba richiedere a una certa unità di I/O le seguenti
operazioni esterne:
1 . provocare il trasferimento di un blocco di dati dal dispositivo alla memoria interna dell’unità,
2. provocare il trasferimento di un blocco di dati dalla memoria interna all’unità al dispositivo,
3. leggere un dato dalla memoria interna dell’unità,
4. scrivere un dato nella memoria interna all’unità.  Si ricordi che un processo (e quindi il processore)
genera indirizzi logici. La dizione “memoria interna all’unità di I/O” non è, quindi, formalmente corretta; si
tratta solo di una abbreviazione per “parte della memoria virtuale che risulta allocata nello spazio fisico di
I/O”.

I casi 3, 4 corrispondono in maniera naturale a istruzioni di Load e Store rispettivamente.


Per implementare i casi 1, 2 si può pensare di implementare l’unità di I/O in modo che una specifica
locazione della propria memoria, ad esempio quella di indirizzo fisico zero (internamente al modulo di
memoria allocato all’unità), sia fittizia: ricevendo una richiesta di scrittura a tale indirizzo, l’unità interpreta
il valore del dato come un “codice di operazione esterna”, che potrà non venire realmente scritto nella
locazione di indirizzo zero, bensì usato per acquisire informazioni sulla richiesta del processo. Nel nostro
caso, ad esempio, a seconda che il dato “da scrivere” sia un numero pari o dispari, il significato della

2
richiesta del processore è di eseguire l’operazione esterna 2 oppure la 1, rispettivamente. È dunque
sufficiente che il processo interno esegua istruzioni di STORE con indirizzo e dato sorgente opportuni.

TRATTAMENTO DELLE INTERRUZIONI


Trattamento ai vari livelli:
Come detto più volte, a livello firmware le interruzioni hanno il significato di segnali di richiesta all’arbitro
del Bus di I/O di inviare un messaggio di I/O su tale bus. Ai livelli superiori, il significato di una interruzione è
di segnalare, attraverso il messaggio di I/O, un evento. Una procedura del processo in esecuzione, che
chiameremo Handler dell’interruzione (o dell’evento), provvede a compiere le azioni necessarie per
trattare l’evento stesso, eventualmente chiedendo la collaborazione di altri programmi o processi del
sistema. Il trattamento dell’interruzione deve prevedere due fasi (come schematizzato nella figura a pagina
seguente):
• una prima fase firmware, nella quale il processore accetta l’interruzione, attende il messaggio di I/O e,
usando tale messaggio come parametro, chiama una procedura indicata con Routine di Interfacciamento
Interruzioni. Il messaggio di I/O (ad esempio, due parole) conterrà l’identificatore dell’evento e un dato
elementare;
• una seconda fase assembler, nella quale viene eseguita la Routine di Interfacciamento Interruzioni e la
procedura Handler dell’evento comunicato con l’interruzione. La Routine di Interfacciamento Interruzioni
serve a permettere alla fase firmware di collegare qualunque istruzione di qualunque processo agli Handler,
svincolando la progettazione firmware del processore della conoscenza a priori degli Handler stessi.

Fase Firmware: Le scelte ora fatte, in particolare l’uso dei registri riservati RG[60 .. 63], valgono solo a titolo
di esempio e non devono essere prese per oro colato; avremmo potuto scegliere quattro locazioni di
memoria di indirizzo noto, oppure una qualsiasi combinazioni di registri generali e locazioni di memoria. La
fase firmware è descritta dal seguente microprogramma (all’etichetta tratt_int, cui, come sappiamo, si salta
da ogni fase di esecuzione istruzione se e quando INT = 1):
tratt_int. reset INT, set ACKINT, int1
int1. (RDYINM, or(ESITO), = 0 – ) nop, int1;
(= 10) reset RDYINM, set ACKINM (cioè RDYOUTM), DATAIN  REG[61], int2
(ricevuto ii codice dell'evento, questo viene passato alla Routine di interfacciamento interruzioni via
RG[61]}
(= 11) reset RDYIN, tratt_ecc ;
int2. (RDYINM, or(ESITO), = 0 - ) nop, int2;
(= 10) reset RDYINM, set ACKINM, DATAIN -> REG[62], ICREG[63], REG[60]IC, ch0
(ricevuta la seconda parola dei messaggio di I/O, questa viene passata alla Routine di interfacciamento
interruzioni via REG(62], viene salvato t’indirizzo d i ritorno dalla Routine, ed effettuato il salto alla Routine
stessa)
(=11) reset RDYINM, tratt_ecc ;

Istruzioni per la gestione delle interruzioni:


Sono state introdotte, nel linguaggio assembler D-RISC, alcune istruzioni speciali, tra le quali quelle per far sì
che il programma in esecuzione possa, durante certe sequenze di istruzioni, ignorare la presenza di alcune
o tutte le interruzioni. Ad esempio, conviene che la stessa fase assembler del trattamento interruzioni
venga eseguita a interruzioni disabilitate, in modo da semplificare la sua progettazione e/o non provocare
inconsistenze (ad esempio, gli Handler potrebbero condividere dei dati modificabili). La stessa situazione si
ha per le procedure di trattamento delle eccezioni, e in generale per le funzionalità del supporto dei
processi.Come visto, per ragioni legate alla facilità di progettazione di codice di sistema, conviene adottare
anche una soluzione più flessibile che permetta di specificare in una qualsiasi istruzione anche la
disabilitazione o abilitazione delle interruzioni. Ad esempio, per concludere la fase assembler del
trattamento interruzioni, la Routine Interfacciamento Interruzioni esegue l’istruzione “GOTO Rret_prog”. La
riabilitazione delle interruzioni non può quindi essere inserita “dopo” l’ultima istruzione di tale Routine; con
il metodo introdotto si ha invece:

3
LOAD Rtab_int, Revent, Rhandler, DI
CALL Rhandler, Rret_handler
GOTO Rret_prog, EI
//dove Revent è R61, mentre Rret_prog è R63
Si noti che anche l’Handler, essendo una procedura chiamata dalla Routine Interfacciamento Interruzioni,
viene eseguito a interruzioni disabilitate. In generale, questa “facility” è utile in tutte le situazioni (e sono
molto frequenti) in cui il codice da eseguire in modo non interrompibile è una procedura (o un
annidamento di procedure).
Un altro esempio significativo si ha nella fase di commutazione di contesto, nella quale è conveniente
riabilitare le interruzioni durante l’ultima istruzione della fase stessa (START_PROCESS)

TRASFERIMENTI DI I/O E PROCESSI ESTERNI:


Le applicazioni concorrenti possono, in generale, contenere funzionalità di ingresso-uscita, e quindi
prevedere trasferimenti di dati con unità di I/O. Le unità di /O possono avere associati dei processi driver
che provvedono a interfacciare (“wrappare”) le unità stesse nei confronti dei processi dell’applicazione in
modo da mascherarne i dettagli implementativi e i meccanismi che l’architettura firmware-assembler
prevede per i trasferimenti di ingresso-uscita e le interruzioni. Il problema di conoscere e trattare questi
dettagli è comunque presente nella progettazione dei driver stessi. Inoltre, è possibile che i processi
dell’applicazione possano comunicare direttamente con le unità di I/O, senza passare attraverso il filtro dei
driver, oppure passandoci solo parzialmente; questo può essere dovuto a ragioni di
• ottimizzazione delle prestazioni: si consideri il caso in cui l’unità di I/O invia direttamente dati a un
processo applicativo che ne aveva fatto richiesta, risparmiando così la latenza di comunicazione di dati, di
dimensione anche elevata, attraverso il driver;
• modifica/evoluzione dell’architettura di I/O: la modifica, aggiunta o sostituzione di unità di I/O, ad
esempio in seguito ad evoluzione tecnologica, rende necessario la riprogettazione dei driver sia dal punto di
vista funzionale che prestazionale. In conclusione, dobbiamo essere capaci di trattare i trasferimenti di I/O
in programmi paralleli, senza fare affidamento sui processi driver (ciò non toglie che, ove i driver siano
presenti e ben progettati, se ne possa ricavare vantaggi in termini di modularità). A tale scopo, il primo,
fondamentale passo, come detto nella sez. 1.2, consiste nel considerare le unità di I/O come processi, detti
processi esterni, cooperanti con gli altri processi eseguibili sulla CPU (processi interni) mediante meccanismi
opportuni.

Distinguiamo due situazioni operative per l’implementazione di processi esterni:


1. il sistema dispone di un linguaggio concorrente (LC, nel nostro caso) e tutti i processi, sia interni sia
esterni, sono descritti in LC;
2. il livello LC non esiste esplicitamente, o almeno non è disponibile per i processi esterni, e va emulato
direttamente a programma utilizzando tutte le caratteristiche dell’architettura firmware-assembler.