Escolar Documentos
Profissional Documentos
Cultura Documentos
Titolari:
Prof. Tullio Vernazza
Prof. Renato Zaccaria
PARTE HARDWARE
Capitolo 1:
Il Bus
Introduzione
Caratteristiche di un bus
Considerazioni macroscopiche sul bus
Elementi definiti dallo standard
Capitolo 2:
Trasferimento dati
Introduzione
Particolari tipi di trasferimento dati
Errori sul bus
Arbitraggio
Introduzione
Daisy chain a controllo decentralizzato
Daisy chain a controllo centralizzato
Richieste indipendenti a controllo centralizzato
Richieste indipendenti a controllo distribuito
Inizializzazione
Introduzione
Sistemi monoprocessore
Sistemi multiprocessore
Caratteristiche critiche di alcuni bus
Capitolo 3:
La cache
Introduzione
Sistemi monoprocessore
Sistemi multiprocessore
Capitolo 4:
La pipeline
1 of 138
Introduzione
La Pipeline in una CPU
Inserimento di unit multiciclo Floating Point
Schedulazione dinamica
Gestione dei salti
L'unrolling dei Loop
Macchine Superscalari
PARTE SOFTWARE
Capitolo 1:
Programmazione concorrente
Eventi e Segnali
Semafori
Monitor
Messaggi
Csp
Rpc
Scedulazione Real Time
CORSO DI:
Real Time
Indroduzione
Real-Time e Unix
Politiche di schedulazione
Time Sharing
Priorit Fissa
FIFO
Round Robin
Il Bus
Introduzione
I bus nascono dall'esigenza di dover collegare tra loro i vari elementi (CPU, memoria, unit di I/O, etc..) di un calcolatore.
Un sistema per l'elaborazione dell'informazione pu essere pensato anche senza un bus, con dei collegamenti punto a punto,
Un bus ha molti vantaggi: in particolare permette di mettere insieme parti costruite da case diverse (con conseguente discesa
dei prezzi grazie al maggior volume di produzione). Per fare ci, necessario avere uno standard di bus. Esistono diversi
tipi di standard:
2 of 138
1) Standard "de facto"
E' uno standard decretato tale dalla vasta diffusione di un bus sul mercato. Lo svantaggio di un tale tipo di standard che
non completamente formalizzato.
ISA (IBM)
UNIBUS (Digital)
2) Standard "promosso"
E' il caso pi frequente. Proviene da una "promozione", cio dalla formalizzazione ufficiale di un bus esistente. Questa
operazione viene fatta da un'apposita organizzazione internazionale, che sottopone il bus a prove (e, in alcuni casi, lo
migliora aggiungendo potenzialit che alla casa costruttrice non interessavano).
I bus di questo tipo sono quelli tecnicamente pi validi perch nascono senza dover garantire continuit le realizzazioni
preesistenti. Sono per penalizzati dal fatto che non esiste una forza (grosso produttore o acquirente) che li "sponsorizzi" e
faccia in modo che si impongano sul mercato. Quindi sono generalmente poco diffusi.
Camac
FutureBus
Caratteristiche di un bus
Gli elementi caratteristici di un bus sono la velocit di trasmissione, lo spazio indirizzabile, l'ampiezza dei dati, l'immunit
al rumore, la distanza da coprire, il supporto del multiprocessing, il costo.
Non esiste il bus "perfetto", ma spesso averlo non ci servirebbe perch il resto del sistema non riuscirebbe a sfruttarlo al
massimo. Per questa ragione, all'interno di un sistema possono esistere diversi bus. Lo schema che segue un esempio il pi
possibile generico.
3 of 138
BUS GLOBALE: E' un bus veloce, lungo 19" (dimensione standard rack) e terminato.
BUS LOCALE: E' un bus molto veloce (pi del bus globale perch gestisce un carico minore), molto corto e non terminato.
BUS I/O : Collega il controller ai dischi, non molto veloce perch non serve. Lungo fino a 1 metro, molto spesso
terminato.
BUS STRUMENTI: E' un bus lento (1 MB/s), lungo anche decine di metri e terminato.
BUS SERIALE: Serve per il collegamento in rete. La sua velocit dipende dalla scheda utilizzata: 10 MB/s (Ethernet),
300-19200 bit/s (EIA RS232C). Pu essere lungo fino a 1500 metri e trasmette un bit alla volta.
Il bit/s considera l'informazione trasmessa nell'unit di tempo, il baud considera le transizioni viste nell'unit di tempo. Se
abbiamo una codifica NRZ (Non Ritorna Zero) le due unit di misura sono equivalenti, con codifiche pi complesse no!
4 of 138
Ad alte frequenze, le linee di un bus sono caratterizzate da un'impedenza Z0 (detta impedenza caratteristica) dovuta al fatto
che si pu considerare ogni singola linea come un insieme di induttanze in serie e condensatori in parallelo. In una
motherboard, Z0 circa 100 Ohm.
Un segnale impiega circa 5-6 ns per percorrere un metro. Propagandosi lungo una linea non terminata, giunto in fondo
trover un'impedenza infinita. Ci fa rimbalzare il segnale, che comincia a compiere (in modo diverso a seconda della
lunghezza della linea e del tipo di gate) oscillazioni smorzate. Per questo motivo non possibile trasferire dati troppo
velocemente perch necessario attendere che l'oscillazione termini.
Per migliorare la situazione possiamo:
1) diminuire l'impedenza interna del gate, usando bus driver invece di gate logici;
2) evitare segnali dai fronti troppo ripidi, usando bus driver con fronti lenti;
3) modificare il bus.
Per smorzare l'oscillazione, alla fine della linea si pone un resistore, che per assorbe corrente elevata. In alternativa,
inseriamo un partitore di tensione che in continua dissipa solo su uno dei due resistori, mentre durante un transitorio
equivalente ad un parallelo; i resistori si dimensionano in modo da ottenere un valore il pi vicino possibile all'impedenza
caratteristica Z0.
Spesso, occorre terminare entrambi gli estremi della linea perch non possibile conoscere a priori da quale parte proviene
il segnale.
Il problema della terminazione diventa sempre meno preoccupante col diminuire della velocit di trasferimento, sempre pi
con l'aumentare della lunghezza del bus.
Tipo di trasmissione
Ne esistono due: trasmissione parallela e trasmissione seriale. La prima si utilizza quando il bus deve essere corto e veloce.
La seconda quando il bus deve coprire distanze considerevoli ed essere economico (il driver di gestione costoso ma uno
solo, mentre quelli per i bus paralleli sono uno per filo). La velocit massima dell'ordine delle centinaia di Mbit/s (in
situazione di fibra ottica, non di rame).
Modalit di trasmissione
Anche per quanto riguarda la modalit del trasferimento, vi sono due possibilit. La prima prevede un dispositivo master e
uno slave: il master manda sul bus un indirizzo che identifica lo slave con cui colloquiare. La seconda (usata, per esempio,
nei bus per strumentazione) prevede tre dispositivi: un controller, un sender e un receiver. Il controller assegna i compiti di
sender e receiver a due dispositivi e li lascia lavorare da soli mentre si occupa d'altro. Questa seconda alternativa rende
evidentemente il sistema pi flessibile.
5 of 138
Tipi di protocollo
Il protocollo la sequenza di operazioni che necessario svolgere per stabilire la connessione, trasferire i dati e
interrompere la connessione (transazione).
I bus sincroni sono quelli in cui si ha un segnale di riferimento temporale (il clock) e tutto avviene in corrispondenza dei
fronti di salita (o di discesa) di questo segnale.
Un sistema sincrono pi semplice da realizzare, perch se ci sono dei cambiamenti essi avvengono solo in determinati
momenti. Il limite del protocollo sincrono che tutto il sistema deve avere la stessa velocit.
I bus asincroni sono quelli in cui la cadenza degli eventi fornita, di volta in volta, dai sottosistemi che trasferiscono i dati.
Il vantaggio del bus asincrono che si pu adattare allo slave che stiamo interrogando, per pi complicato da gestire e
pi delicato per quanto riguarda la sensibilit al rumore. Spesso troveremo dei bus asincroni collegati ad una CPU che
campiona con il proprio CK i segnali asincroni. Quindi, l'asincronicit rimane solo per la comunicazione. Tutto ci perch
una CPU asincrona molto difficile da realizzare e gestire.
Multiplexaggio
Un bus "non multiplexato" un bus in cui tutti i segnali hanno un significato fisso, che non cambia nel tempo.
Un bus "multiplexato" un bus in cui il significato di alcuni segnali cambia nel tempo ed determinato dal valore di altri
segnali. Il primo vantaggio del multiplexaggio risiede nell'utilizzo di un minor numero di linee. Inoltre, poich diminuisce
conseguentemente il numero di driver e receiver necessari, un sistema che sfrutti il multiplexaggio del bus risulta anche pi
economico. Per esempio, siccome i segnali di indirizzi e dati sono in alternativa possibile metterli su un'unica linea (vedi
figura pagina successiva).
1) Caratteristiche meccaniche:
6 of 138
Per quanto riguarda la scheda, a livello industriale uno standard molto diffuso chiamato Europa e ha il modulo base di
100160 mm2 (Europa 1). I vari modelli alternativi al modulo base hanno dimensioni diverse: Europa 2 (220160 mm2),
Europa 2 allungata, Europa 3 e Europa 4. Gli ultimi due modelli sono meno diffusi perch, date le grosse dimensioni,
tendono a flettersi facilmente (quindi il rischio di rompere le piste pi alto) e vengono usate solo in sistemi molto stabili.
Per quanto riguarda il connettore, pu essere a pettine o indiretto.
Il primo tipo quello adoperato sui PC: costituito da una serie di lamelle ricoperte da un sottile strato d'oro e va inserito in
un apposito alloggiamento. Non molto affidabile perch le lamelle si rovinano con l'uso e non fanno pi contatto. Inoltre,
poich le lamelle sono trattenute da un sistema a molla, se ci sono forti vibrazioni la scheda tende ad fuoriuscire
dall'alloggiamento in cui installata.
Il secondo tipo pi sicuro e, conseguentemente, pi apprezzato e utilizzato a livello industriale, anche se pi costoso.
2) Caratteristiche elettriche:
Driver:
Receiver: dovr avere una certa isteresi: il segnale avr una soglia diversa a seconda che stia salendo o scendendo.
3) Caratteristiche logiche:
Specificano la temporizzazione dei segnali (i fronti devono rientrare in certi margini, come in figura sotto), il protocollo di
accesso e quello di trasmissione.
Introduzione
In un bus, per eseguire il trasferimento di un dato, non servono solo i fili che portano l'indirizzo e quelli che portano il dato
7 of 138
stesso ma sono necessari anche altri segnali:
ESEMPIO: Il bus VME (Motorola) utilizza quattro segnali distinti, ognuno dei quali controlla l'accesso a un byte della
parola. La selezione di ampiezze dati maggiori di un byte avviene attivando pi segnali contemporaneamente.
ESEMPIO: Il Multibus (Intel) adotta una soluzione pi complessa ma pi resistente alle interferenze. I segnali utilizzati
sono:
Nota 1: I due metodi adottati da Intel e Motorola non sono esattamente equivalenti: il VME permette anche di riferirsi al 1
e 3 byte contemporaneamente.
Nota 2: Nel secondo esempio uno dei segnali di abilitazione superfluo perch, essendo mutuamente esclusivi, possibile
esprimerlo come negazione degli altri due.
8 of 138
Particolari tipi di trasferimento
Broadcast
E' un tipo di trasferimento previsto da tutti i bus. Esso consiste in una trasmissione da un master a pi slave (ad esempio,
tutte le stampanti di un sistema). In questo modo, si pu inizializzare un sistema con un indirizzo comune che viene
accettato da tutti gli slave.
Per ridurre i tempi morti nell'uso del bus, gli accessi in memoria possono essere multiplexati come mostrato nella figura
successiva.
Tuttavia, i metodi visti richiedono di ripetere l'intero ciclo (di lettura o di scrittura) nel caso sia necessario accedere a pi
dati consecutivi. In tal caso, per incrementare la velocit delle operazioni, utilizziamo un metodo di trasferimento
sequenziale. Il miglioramento dato dal fatto che si invia solo il primo indirizzo (i restanti sono omessi perch consecutivi).
Inoltre il tempo tra l'emissione dell'indirizzo e quella del primo dato molto pi lungo di quello fra due dati successivi.
9 of 138
Questo tipo di trasferimento si adatta bene ai sistemi che si riferiscono alla memoria con una decodifica a matrice perch la
parte pi significativa dell'indirizzo, rimanendo sempre uguale, trasmessa una sola volta.
Il trasferimento sequenziale anche usato per far comunicare la memoria primaria con la cache (mentre il trasferimento da
CPU a cache a dato singolo).
Usando il trasferimento sequenziale in un sistema con pi schede di memoria occorre fare attenzione all'indirizzamento,
perch alcuni dati potrebbero trovarsi a cavallo fra due schede: in questo caso la scheda n dovrebbe capire che il resto dei
dati si trova sulla scheda n+1, disattivarsi e passarle il controllo (soluzione peraltro difficile da realizzare).
Esiste un intervallo di tempo, tra la trasmissione degli indirizzi e la trasmissione dati, in cui il bus non viene utilizzato: in
tale intervallo il dato viene prelevato dalla memoria. Tale spreco inevitabile se il sistema ha un'unica CPU. Invece, in un
sistema multiprocessore quando il bus inattivo per una CPU, lo si pu affidare ad un'altra. Ad esempio supponiamo di
avere tre CPU, una delle quali ha inoltrato una richiesta sul bus ed in attesa di risposta: le altre possono sfruttare questo
intervallo di tempo per inoltrare a loro volta una richiesta oppure per ricevere un dato che avevano richiesto in precedenza.
Ovviamente, servono due codici di identificazione della CPU richiedente: uno per l'indirizzo (ID) e uno per il dato (TAG),
ciascuno emesso con la corrispondente richiesta.
Con il bus splitting non solo la CPU che si comporta da master: anche la memoria pu esserlo, in tal caso la CPU si
10 of 138
comporta come slave.
Sostanzialmente questa soluzione veloce come il trasferimento sequenziale. Il guadagno rispetto ad un sistema standard
di un fattore due o tre. Di solito un sistema del genere realizzato con un bus sincrono.
Negli esempi fatti finora abbiamo (ottimisticamente) considerato l'esistenza di un solo segnale di ACK. Normalmente
necessario prevederne almeno un altro (NACK) per segnalare errori che dovessero eventualmente verificarsi.
Errori di trasferimento
Destano poca preoccupazione perch hanno bassa probabilit di verificarsi e, anche quando si verificano, vi si pone
facilmente rimedio ripetendo la trasmissione. Per rilevarli sufficiente aggiungere un bit di parit all'informazione
trasmessa.
Errori di indirizzamento
Si verificano quando la memoria indirizzata inesistente o rotta; poich non c' risposta (n ACK n NACK) la CPU si
blocca. Per evitare questo tipo di problema, utilizziamo un dispositivo detto watchdog o timeout. Se la memoria non
risponde entro un tempo prestabilito (ad es. 10 ms), esso genera un segnale di NACK e un'interrupt alla CPU per
11 of 138
interrompere il processo che ha causato l'errore.
Introduzione
L'arbitraggio la funzione di gestione del possesso del bus. Spesso, infatti, accade che CPU, memoria e altri dispositivi ne
richiedano simultaneamente l'utilizzo. In questi casi, esistono varie tecniche per decidere chi ha diritto al trasferimento.
Prima di analizzarle in dettaglio, ne presentiamo uno schema riassuntivo:
a 1 livello
a pi livelli
a priorit decodificata
a priorit non decodificata
serializzato
a linee spezzate
Ciascun dispositivo ha una sua priorit, dipendente dalla distanza dalla CPU: priorit minima per il DMA pi lontano,
massima per quello pi vicino.
Quando un dispositivo ha necessit di accedere al bus, manda un segnale di BUSREQ alla CPU. Quest'ultima interrompe
ci che sta facendo e manda in risposta un segnale di GRANT che attraversa tutti i dispositivi alla ricerca di quello che ha
effettuato la richiesta. Il dispositivo richiedente, quando raggiunto dal GRANT, ha la certezza che la CPU ferma in attesa
del suo accesso al bus e pu cos effettuare la transazione.
In caso di pi richieste, dato il collegamento a margherita, verr servito (tra quelli che hanno effettuato la richiesta) il
dispositivo pi vicino alla CPU, poich verr attraversato dal GRANT per primo e bloccher la propagazione del segnale ai
successivi (fig. sotto).
12 of 138
Un sistema che implementasse la schematizzazione vista, per, avrebbe un'alta probabilit di bloccarsi. Questo perch
GRANT un segnale funzionante a livello e, una volta attraversato un dispositivo della catena, non pu essere interrotto.
ESEMPIO: Supponiamo di che DMA5 abbia inoltrato una richiesta alla CPU, che questa abbia risposto con GRANT e
che GRANT abbia gi attraversato DMA2.
Quest'ultimo, in caso di necessit di accesso al bus, inoltrerebbe il segnale di BUSREQ2 alla CPU e si metterebbe in
attesa del segnale di ritorno. A questo punto, interpreterebbe come proprio il segnale di GRANT diretto a DMA5,
cominciando cos una transazione sicuramente destinata a entrare in conflitto con quella dell'altro dispositivo.
Per fare in modo che il GRANT, una volta trasmesso, non possa pi essere bloccato, necessario inserire un flip-flop che
faccia funzionare il sistema della propagazione del GRANT in modo impulsivo invece che a livello (figura sotto).
In questo modo, si riesce anche ad avere rotazione delle priorit, perch prima di ritornare alla CPU, il GRANT si propaga
sino in fondo. Questa realizzazione del mantenimento del GRANT puramente indicativa.
Svantaggi:
Troviamo un esempio di questa tecnica nella gestione del bus dello Z80.
13 of 138
La struttura analoga al caso precedente, ha in pi un segnale di acknowledge che comunica alla CPU che il dispositivo
pronto per usare il bus. Quando un dispositivo ha necessit di utilizzare il bus segnala con BUSREQ alla CPU la volont di
compiere l'accesso, la CPU risponde con GRANT ma non interrompe la sua attivit. Sar il segnale di BUSACK a
comunicarle che la transazione sta per avere inizio. Solo allora sospender l'esecuzione del processo in corso, sospender
l'invio di GRANT e rilascer il bus.
In questo modo:
1. il bus non mai inutilizzato, poich la CPU continua ad usarlo anche mentre invia GRANT e attende BUSACK;
2. il sistema non si blocca come nel caso precedente, perch anche se ci sono pi richieste e nessuno capisce a chi
diretto il GRANT, BUSACK non viene generato e dopo un po' la CPU toglie anche GRANT;
3. l'arbitraggio si pu sovrapporre alla transazione perch una volta arrivato BUSACK il GRANT 'resettato' ed
possibile ricevere una nuova richiesta.
Richieste a pi livelli
Per risolvere il problema del 'basso valore di priorit' della CPU, si utilizza una struttura con pi livelli di richiesta con
priorit diverse.
Nella CPU avremo un registro contenente il valore della priorit del programma: verranno servite solo le richieste con
priorit maggiore di quella del programma.
Ad esempio, se la priorit del programma 3 e abbiamo 7 livelli di richieste, accetteremo solo le richieste dei livelli 0, 1, 2,
3 (per convenzione le richieste a priorit maggiore sono indicate col valore pi basso).
14 of 138
L'utilizzo di una struttura di tipo daisy chain in un sistema multiprocessore ne vanificherebbe i vantaggi. Per questo motivo,
in un tale tipo di sistema il compito di gestire l'arbitraggio non viene dato a una CPU ma a un dispositivo esterno che
accolga separatamente le richieste di ciascuna CPU. In questo modo, viene rispettata la necessit che le CPU abbiano pari
priorit.
La figura a destra mostra il contenuto dell'arbitro. Un registro filtra i segnali di richiesta provenienti dalle CPU e li mantiene
stabili per il priority encoder. Questo valuta a quale CPU corrisponde il bit pi significativo fra quelli pervenuti e manda il
valore della priorit della CPU che ha vinto l'arbitraggio al decoder. Il compito di quest'ultimo trasformare il valore della
priorit in un segnale di GRANT diretto alla CPU vincitrice.
Anche questo schema poco conveniente, perch le CPU hanno priorit fissa. Per ottenere un funzionamento pi equo,
quindi, utilizziamo una struttura di tipo Round Robin: aggiungiamo nelle unit un contatore che scandisce tutti i livelli di
priorit, assicurando alle CPU la medesima probabilit di essere servite.
Gli svantaggi dell'arbitro centralizzato sono:
La realizzazione di un arbitro suddiviso in pi parti - tutte identiche - aggregate ad ogni CPU risolve i rischi (lasciati irrisolti
dalla soluzione precedente) di blocco del sistema in caso di guasto dell'unica scheda contenente l'arbitro.
Ogni arbitro avanza una richiesta da una linea dedicato ma controlla tutte le linee e rinuncia al bus solo in caso di richiesta
pi prioritaria. Terminato l'arbitraggio, un algoritmo di Round Robin effettuer il riassortimento delle priorit.
A priorit decodificata
Per avere una ridistribuzione delle priorit si introduce nell'arbitro un contatore, che viene incrementato e decodificato
prima di inviare la richiesta sul bus.
15 of 138
ESEMPIO: Se abbiamo 4 CPU (figura sopra), pur essendo ciascun decodificatore collegato ad ogni linea, un segnale in
uscita sar inviato sulla linea abilitata dal registro prioritario. Se per capita che due schede CPU abbiano la stessa
priorit, il sistema collassa. I contatori dovranno perci essere ben inizializzati e gestiti.
Il Round Robin garantisce la stessa probabilit di accesso se consideriamo tempi lunghi; tuttavia pu accadere che una CPU
dopo aver avuto accesso al bus aumenti la sua priorit invece che diminuirla.
ESEMPIO: Supponiamo di avere 8 CPU, con priorit pi bassa 000. Se CPU 1 accede al bus, la sua priorit aumenta
invece che diminuire:
Nel Round Robin, quindi, ad ogni ricalcolo della priorit tutte le CPU incrementano la propria di 1, eccettuata quella a
priorit massima che la decrementa di 1. Se per non quest'ultima ad aver fatto l'accesso viene a mancare l'equilibrio
desiderato.
Inoltre, sarebbe auspicabile assegnare priorit massima alla CPU che accede meno frequentemente al bus. La soluzione a
entrambi i problemi porta alla seguente modifica dell'algoritmo di Round Robin:
ESEMPIO: Supponiamo la seguente situazione: le richieste vengono inoltrate sempre dalle CPU1-2-3 e CPU1 stata
l'ultima a vincere.
In questo modo, un dispositivo che effettua poche richieste aumenta la sua priorit, mentre uno che ne effettua molte la
mantiene sempre bassa perch va pi spesso a 0.
Un sistema cos strutturato risulta alquanto complesso in presenza di un grosso numero di master: se avessimo, ad esempio,
16 CPU e 16 DMA, sarebbero necessarie 32 linee soltanto per l'arbitraggio. La complessit pu essere ridotta usando il
codice senza decodificarlo. Il vantaggio evidente: 32 master richiederanno ora solo 5 linee (25=32).
Gestire direttamente il codice della priorit non semplice. Due possibili strade sono l'arbitraggio serializzato e l'arbitraggio
a linee spezzate.
16 of 138
1) Arbitraggio serializzato.
Avendo eliminato il codificatore e il decodificatore, le linee che escono dal registro dovranno servire anche da ingresso.
Siccome le linee che escono dal registro sono gestite da driver open collector, baster che uno qualunque dei master
imponga uno 0 su una linea perch essa risulti inevitabilmente bloccata sul livello basso. La struttura cos concepita, in caso
di pi richieste, mostra sul bus un valore che 'somma' in qualche modo i valori della priorit dei dispositivi richiedenti. A
causa dell'uso degli open collector, la probabilit che sul bus si vedano tutti zeri molto alta.
Per risalire al dispositivo che ha diritto di accedere al bus, stato concepito un sistema di eliminazione per esclusione dei
dispositivi ad esso collegati:
Con questa tecnica, l'arbitraggio su 2n master svolto al massimo in n passi. Alla fine delle n iterazioni l'unico dispositivo a
non aver ritirato la richiesta sar proprio il pi prioritario tra i richiedenti. Potr quindi accedere al bus senza pericolo di
conflitti.
Il seguente diagramma temporale, considerando un bus sincrono, illustra quanto detto:
Il sistema visto lento perch, anche se si trova subito il dispositivo vincitore, sempre necessario attendere n cicli di clock
per poter effettuare l'accesso al bus. Di solito, comunque, la lentezza un fattore poco determinante poich il bus trasferisce
sequenzialmente molti dati.
La riassegnazione delle priorit avviene sull'ultimo colpo di CK, mediante l'algoritmo di Round Robin modificato.
Il segnale di INIT non pu provenire dall'esterno di questa struttura (altrimenti viene vanificata la decentralizzazione
dell'arbitro): esso inviato dal master con un anticipo tale da permettere di stabilire il master successivo per il momento
della cessione del bus.
17 of 138
Pi tardi lanciato INIT, pi possibilit abbiamo di scegliere un master che abbia priorit maggiore degli altri. Questo
perch lasciamo pi tempo perch vengano formulate richieste da parte di master diversi.
L'alternativa all'arbitraggio serializzato gestire diversamente l'hardware e costruire una struttura che permetta di compiere
tutti i confronti necessari in un solo colpo.
In questo caso, abbiamo n linee solo dal punto di vista geometrico; dal punto di vista dell'informazione trasferita le linee
sono 2n-1. Ciascun bit identifica i probabili vincitori all'interno di ciascun sottoinsieme e ogni arbitro effettua n confronti: il
dispositivo che conquister il bus sar quello che li vincer tutti.
L'arbitraggio svolto in un solo ciclo di clock, per l'assegnazione delle priorit vincolata al valore dei singoli bit nei
sottogruppi. In altri termini, le CPU (eventualmente prese a gruppi) che condividono un tratto di linea devono avere uguale
il bit sul livello pi alto, non importa se 0 o 1.
E' evidente che un riadeguamento delle priorit non pu essere eseguito con un round robin 'semplice'. Infatti, per mantenere
intatta la possibilit di lavorare nel modo visto poco sopra, necessario che i bit pi significativi, all'interno di un gruppo
(destro o sinistro) associato ad un livello, siano uguali. In caso contrario, dai confronti potrebbero essere selezionati pi
dispositivi per il possesso del bus con conseguenti conflitti.
Per evitare questo genere di problemi basta modificare l'algoritmo di round robin.
18 of 138
ESEMPIO: Supponiamo di avere otto dispositivi (n=7) collegati al bus. Identifichiamo i fili di ciascun dispositivo
(corrispondenti ai bit che ne codificano la priorit) con due indici: il primo indicante il bit, il secondo indicante il
dispositivo: P2,x , P1,x , P0,x .
Dal punto di vista del bus avremo che, globalmente (tenendo conto che lavoriamo in logica negativa, cio True=0),
avremo un P0, due P1 e quattro P2.
L'algoritmo di ridistribuzione delle priorit sar sostanzialmente analogo a quello visto in precedenza: si imporr al
vincitore il valore minimo di priorit e la priorit degli altri dispositivi verr incrementata con un meccanismo che tenga
conto della nuova struttura (fig. pag. precedente):
1. il vincitore prende la priorit minima e, per come strutturata l'architettura, trascina tutti quelli del suo gruppo
(collegati a P2) con s (poich pone a zero il bit pi significativo). Parallelamente l'altro gruppo di schede pone il
proprio bit pi significativo a uno;
2. ripetiamo lo stesso procedimento, considerando poi il secondo bit e i due sottogruppi contenuti nella met del
vincitore.
E' ovvio che il trascinamento verso il basso delle schede adiacenti alla vincitrice penalizzi parzialmente l'avvicendarsi
delle priorit. Notate che il trascinamento lascia invariate le priorit relative all'interno dei due gruppi.
La struttura necessaria per implementare ad hardware questo algoritmo sar la seguente:
Inizializzazione
Qualsiasi tipo di dispositivo di arbitraggio lavora sui valori delle priorit che provengono dalle unit del sistema. E' molto
importante, quindi, che i valori della priorit dei dispositivi siano inizializzati in modo corretto all'accensione del sistema.
Esistono varie tecniche:
19 of 138
Questa una tecnica molto costosa (sia in termini economici che di manutenzione, immagazzinaggio delle schede di
ricambio ecc.) perch le schede non sono pi intercambiabili anche se sono identiche. Pu avere senso se le schede sono gi
non intercambiabili (perch destinate a impieghi differenti).
Con gli switch per entra in gioco l'errore umano: se l'utente sbaglia e assegna la medesima priorit a due arbitri il sistema si
blocca.
In questo modo, all'atto del reset i valori della priorit verranno caricati direttamente sul registro. E' la soluzione pi adatta
ad un sistema molto grande, sebbene siano necessarie linee aggiuntive.
4) Autoinizializzazione.
Prevediamo che ci sia una MSF su ciascun arbitro. Il sistema parte non inizializzato: ad un segnale di inizializzazione la
prima scheda si assegna il valore di priorit presente sul bus, lo incrementa e passa il controllo alla successiva. Questo il
metodo pi elegante ma anche pi costoso perch abbiamo hardware ridondante e usato solo in fase di accensione.
Il sistema di autoinizializzazione usato sul Futurebus che per asincrono e usa 15 bit per la priorit; ovviamente ciascuna
scheda deve preoccuparsi della temporizzazione.
Introduzione
20 of 138
Le interruzioni sono molto pi perturbative dei DMA. Infatti, mentre in caso di DMA la CPU continua ad elaborare
(seppure con rallentamenti), in caso di interrupt sospende immediatamente il programma in esecuzione e si dedica
totalmente alla sua gestione.
Come tutti i segnali, anche l'interrupt pu funzionare a livello o sui fronti.
La richiesta alla control unit viene filtrata da un flip-flop come nella figura a sinistra:
2) Funzionamento a livello
La richiesta viene inviata direttamente alla control unit e il dispositivo che l'ha inoltrata deve mantenerla finch non
servita.
Sistemi monoprocessore
Di solito ad una CPU sono collegati pi dispositivi in grado di generare interrupt. Per gestirli esistono diversi tecniche,
eventualmente combinate con una strutturazione del sistema a differenti livelli di priorit.
Polling
Consiste nell'interrogazione ciclica dei dispositivi da parte della CPU, alla ricerca di quello che ha richiesto l'interruzione.
Interrupt vector
E' un metodo pi efficiente. La periferica inoltra la richiesta di interruzione (INT) e riceve un segnale di ricevuto (INTACK)
che comunica che il bus libero, successivamente essa invia sul bus dati un vettore che contiene informazioni relative
21 of 138
all'interrupt richiesta (l'interrupt vector, IV) a cui la CPU risponde con un segnale ACK che si propaga in daisy chain.
L'interrupt vector potrebbe contenere il motivo della richiesta dell'interrupt o, meglio ancora, il puntatore a una tabella
contenente la descrizione dell'interrupt e l'indirizzo della sua routine di gestione.
Sistemi multiprocessore
In un sistema multiprocessore c' anche la necessit di inviare segnali di interrupt da una CPU all'altra. In questa situazione,
tutto risulta pi facile se si usa il bus splitting o slicing.
Il dispositivo:
Di seguito, per i bus pi diffusi, riportiamo i dati relativi agli aspetti del bus trattati in questo capitolo.
22 of 138
Spazio
Nome Ampiezza dati Protocollo Multiplexaggio Velocita'
indirizzabile
VME 16-24-32 16-32 A N 40 Mb/s
Multibus 32 16-32 S S 40 Mb/s
Microchannel 16-24-32 8-16-32 A N 34 Mb/s
EISA 16-24 8-16-32 S N 33 Mb/s
La cache
Introduzione
La memoria primaria non in grado di offrire le prestazioni richieste dalle odierne CPU; per questo motivo, si introduce tra
la CPU e la Mp un ulteriore livello nella gerarchia di memoria: la cache. Essa una memoria di tipo associativo, cio una
memoria che viene referenziata tramite il contenuto invece che tramite l'indirizzo. Fisicamente realizzata con due memorie
distinte: RAM Tag e RAM Dati.
Il miglioramento delle prestazioni dovuto a due fattori principali: la tecnologia di realizzazione (SRAM) e l'algoritmo di
gestione (che sfrutta i noti principi di localit nel tempo e nello spazio dell'informazione).
Il costo e l'elevato ingombro, principali svantaggi dell'utilizzo della tecnologia SRAM rispetto alla DRAM, sono poco
rilevanti poich la memoria cache ha sempre dimensioni molto ridotte rispetto alla Mp.
Sistemi Monoprocessore
23 of 138
ESEMPIO: Il VAX 11/780 ha una cache di 8 KB, associativa a due vie, blocco da 8 byte, 512 set. Supponiamo di avere i
seguenti dati: 8.5 CPI, 11% miss, 3 accessi in MP per istruzione, accesso in memoria 6 CK. Avremo:
L'importanza della cache cresce se usiamo una macchina con CPI minore e CK pi veloce
Se i dati fossero 1.5 CPI, 11% miss, 1.4 accessi in memoria/istruzione, accesso in memoria 10 CK, avremmo:
Nel capitolo 1, abbiamo detto che per valutare una macchina serve poco valutare le prestazioni della CPU. Un i486 a
66MHz con una cache che funziona male vale meno di un i486 a 33 MHz con una cache che funziona bene.
Questo schema ideale e non esiste nella realt, perch sarebbe necessario un mucchio di transistor molto scomodo da
mettere su chip, allora lo simuliamo con altri metodi.
E' un sistema semplice, ma ha l'inconveniente di funzionare male se le celle che si usano nel programma hanno i bit meno
significativi uguali. In questo modo, assegniamo una stessa locazione a tutte le celle che hanno i bit meno significativi
dell'indirizzo uguali, ovviamente quando c' un Miss si dovr effettuare la ricerca dell'informaione desiderata nella Mp e
successivamente aggiornare la cache.
24 of 138
ESEMPIO: Supponiamo di avere un sistema dotato di una RAM Tag da 1 KB 18 bit ed una RAM Dati da 1 KB 32 bit
e di voler leggere un dato all'indirizzo fisico 00123468h. La cella della cache che ci interessa la 11Ah (468h 4).
Se in tale cella contenuto il valore 00123h si otterr un Hit e quindi il dato desiderato si trover nella corrispondente
cella della RAM Dati; altrimenti avremo un Miss ed occorrer effettuare tre operazioni:
Nota: se il sistema avesse bisogno alternativamente di due valori aventi la parte meno significativa uguale, esso
funzionerebbe peggio che se fosse privo di cache.
Ci che possiamo fare prevedere pi locazioni dove memorizzare i dati associati ad indirizzi che hanno una parte finale
uguale. Questo sistema diminuisce molto la probabilit di conflitto. Ovviamente, se n=1 abbiamo direct mapping.
D = S * E * B.
dove:
S numero di set;
E numero di blocchi o linee in un set (livello di associativit);
B numero di celle in un blocco.
25 of 138
L'operazione di dimensionamento consiste nel trovare i valori ottimali dei tre parametri. Si noti che per valori limite dei tre
parametri si ottengono i tipi di cache visti nel paragrafo precedente.
Dimensionare bene una cache difficile, perch vi sono numerosi fattori che si influenzano vicendevolmente ed possibile
che si peggiori la situazione invece che migliorarla.
ESEMPIO: Consideriamo un sistema con CPI = 1.5, CK = 50 MHZ (1 ciclo ogni 20 ns), 1.3 accessi per istruzioni, TA in
Mp pari a 200 ns. Analizziamo l'incremento delle prestazioni ottenibile inserendo una cache (1) direct mapped (Miss
3,9%) o (2) associativa a 2 vie (Miss 3%, con un incremento del ritardo interno alla cache di 1.7 ns).
i. TA = 20 + 0.039200 = 27.8 ns
ii. TA = 21.7 + 0.03200 = 27.7 ns
Esistono degli effetti collaterali che peggiorano le prestazioni della macchina. Se non aumentiamo il clock bisogna
mettere un wait state:
Nella scelta della dimensione del set entrano in gioco i bit meno significativi. Non conviene optare per un numero troppo
grande perch questa struttura pi complicata e pi lenta del direct mapping. Un valore ottimale dal punto di vista delle
sole prestazioni compreso fra 4 e 8.
Dal punto di vista del costo, una memoria associativa pi costosa perch costruita in parallelo. Ad esempio, una memoria
set associative a 4 vie e 16 KB (4 KB per livello) costa circa quattro volte una direct mapped da 16 KB. Un valore di n=2
il compromesso pi usato tra prezzo e prestazioni.
Le dimensioni del blocco sono multipli di una parola (ad esempio: 8 parole = 32 byte).
Scegliere un blocco piccolo costringe la cache a lavorare continuamente per caricare nuovi dati, ma permette una
realizzazione circuitale pi semplice. Scegliere un blocco di grandi dimensioni permette di ridurre il numero di MISS ma,
eccedendo, si rischia di infrangere il principio di localit.
Le due soluzioni sono indicate in due momenti diversi dell'elaborazione: la prima quando il sistema gi a regime, la
seconda quando il sistema appena stato acceso e ha bisogno di molte informazioni per poter cominciare a lavorare.
26 of 138
2. riduzione della probabilit di Miss.La riduzione sar minore del rapporto fra parola e blocco, ma pur sempre consistente.
3. ottimizzazione delle operazioni sequenziali sul bus.
Si tratta di fare una lettura multipla invece di quattro singole. Quindi, c' un arbitraggio unico invece di quattro. Ci
particolarmente importante se abbiamo pi cache perch riduciamo i conflitti sul bus.
Un blocco non pu avere dimensioni molto grandi perch potrebbe accadere di gettare via informazioni ancora utili per far
posto a nuove informazioni di nessuna utilit. Inoltre, aumentando le dimensioni del blocco si allungano i tempi di lettura e
scrittura. Ci significa che le CPU rimangono bloccate molto pi a lungo.
Questo problema pu essere superato realizzando una cache che sblocchi la CPU senza aspettare che il blocco sia
completamente trasferito ma appena stata riempita la parte di blocco che ci interessa.
Se vogliamo fare i raffinati, possiamo cominciare a riempire dalla parola che ha fatto la richiesta. E' ovvio che non facile
costruire una cache che funzioni in questo modo (questa realizzazione implica due ulteriori gradi di complessit).
Fetch su Miss
Consiste nel prelevare dalla Mp il dato richiesto e andarlo a copiare direttamente nella memoria cache.
Prefetch
Consiste nel prelevare dalla Mp un dato che si suppone verr richiesto in futuro dalla CPU. Possiamo avere:
1. prefetch su Miss, col quale si preleva all'atto del Miss anche il blocco successivo a quello che interessa;
2. prefetch sul primo accesso a un blocco, equivalente al precedente ma effettuato non dopo un Miss ma ogni volta che
si effettua il primo accesso ad un dato blocco.
27 of 138
In questo caso, all'informazione contenuta nella RAM Tag si aggiunge un flag che stabilisce se il blocco gi stato usato. In
caso di Hit, se il flag vale 0 (cio stiamo effettuando il primo accesso) si carica il blocco successivo e si pone a 1 il flag del
blocco che era gi presente, a 0 quello del blocco appena caricato. In caso di Miss, invece, si ha in effetti un prefetch su
Miss e si attribuisce 1 al blocco richiesto e 0 a quello preso per il prefetch.
In un'operazione di scrittura, la CPU scrive il dato prima nella cache e poi, a seconda dell'algoritmo, nella Mp.
Write Through
Il dato viene sempre scritto sia nella cache che nella Mp ogni volta che viene modificato. Questa tecnica si avvale di un
buffer che memorizza le informazioni da scrivere nella Mp e restituisce subito il controllo alla CPU. Quest'ultima
provveder a terminare l'operazione di scrittura in un secondo tempo (la scrittura nella cache richiede circa un terzo del
tempo di scrittura nella Mp). In generale, il write through usato in macchine in cui il bus non il collo di bottiglia.
Sistemi Multiprocessore
In sistemi multitasking e multiprocessore, oltre a quanto gi visto si aggiungono altri problemi. Il contenuto della cache
dovr cambiare con il task, quindi si avr un forte calo di prestazioni poich nella cache, per un certo periodo, si
verificheranno solo Miss. Per ridurre gli effetti di questo inconveniente si pu:
I sistemi pi usati sono 1) e 3) perch realizzabili mediante software, mentre gli altri (soprattutto 2) e 5) ) sono pi costosi
poich richiedono hardware dedicato.
28 of 138
In sistemi multiprocessore il problema principale dato dalla consistenza dei dati: fondamentale che i dati in comune fra i
vari processori (cio i semafori utilizzati per la sincronizzazione e le informazioni su cui si lavora o i messaggi scambiati)
siano sempre correttamente aggiornati, per impedire che una CPU utilizzi un dato che non pi aggiornato. L'utilizzo del
write through ridurre parzialmente il problema, mentre il copy back lo acuisce sensibilmente.
ESEMPIO:
Se CPU 1 modifica il dato ci avverr anche nella Mp, ma nella cache 2 abbiamo ancora il vecchio dato. Col Write
Through abbiamo, quindi, due copie su tre del dato col valore aggiornato. Col Copy Back abbiamo invece una sola copia
(o peggio nessuna) aggiornata su tre.
Questa tecnica viene utilizzata soltanto se l'algoritmo di scrittura il Write Through e consiste in un controllo da parte delle
cache sui trasferimenti che avvengono sul bus. Una cache invalida il dato in suo possesso qualora si accorga che l'indirizzo
ad esso corrispondente sta transitando sul bus per un'operazione di scrittura: in questo modo costretta a compiere in Mp il
successivo prelievo dello stesso dato.
Si potrebbe pensare di aggiornare direttamente il contenuto della cache invece di invalidarlo; tuttavia non si sceglie questa
soluzione perch vi sarebbe un grande numero di bit da trasferire (e scarsa probabilit di trovare qualcosa in cache ).
E' possibile che la CPU richieda un valore contenuto nella cache, che questo valore sia stato modificato e che il suo
indirizzo si trovi ora in coda per un confronto. Per questo motivo, ad ogni Hit sar necessario verificare se l'indirizzo nel
buffer. Per compiere tale verifica, che non pu essere trascurata sebbene dia quasi sempre esito negativo, si usa hardware
aggiuntivo.
ESEMPIO:
Supponiamo di disporre di CPU che effettuano una scrittura ogni dieci istruzioni. Utilizzandone 4, avremmo sul bus
scritture per il 40% del tempo. Inoltre, ogni CPU perderebbe il 30% del suo tempo per fare controlli. Quindi, con 4 CPU
abbiamo una prestazione complessiva a quella di 4 * 0.7 CPU equivalenti.
Una prima possibilit di miglioramento delle prestazioni consiste nel limitare il controllo a indirizzi dichiarati condivisibili,
identificati da un bit "Global" posto a 1. Le operazioni viste finora vengono quindi svolte soltanto in fase di scrittura su una
pagina condivisa.
In alternativa, possibile realizzare questo sistema con hardware dedicato, riprogettando la cache e dotandola di due RAM
29 of 138
Quando c' un Hit nella seconda RAM Tag il dato deve essere invalidato e si interviene sull'altra RAM Tag. Quando si
scrive un nuovo blocco in seguito a un MISS, le due RAM Tag funzionano in parallelo, mentre vengono trattate
diversamente in fase di lettura. Con questo sistema, la CPU non viene pi bloccata, ma il costo aumenta.
Directory centralizzate
Consideriamo un sistema dotato di n CPU, ciascuna con la propria cache. Introduciamo in Mp una struttura, detta
"directory", contenente una serie di bit per ogni blocco. Pi precisamente utilizziamo n bit "Valid" (uno per ogni cache) che
indicano in quali cache si trova una copia del blocco e un bit "Modifiable" (uno per directory) che indica se il blocco pu
essere modificato. Per mantenere la coerenza dei dati, se il bit Modifiable vale 1 sar necessario che soltanto una cache
possa contenere il blocco e modificarlo, viceversa pi cache potranno contenerlo senza per scrivervi sopra.
ESEMPIO: consideriamo un sistema con 5 CPU. La directory di un dato blocco nella Mp avr la seguente struttura
Se invece il blocco si trova nelle cache 1, 3 e 4 ed ovviamente non pu essere modificato avremo
La gestione dei Miss nella cache viene eseguita in diversi modi a seconda dei valori dei bit di stato della directory
interessata.
Dato che il blocco da leggere non presente nella cache, abbiamo sicuramente V = 0, quindi per prima cosa la cache
richieder il blocco alla Mp. A seconda del valore di M riferito a quel blocco possono verificarsi due situazioni:
1) M = 0
30 of 138
ci significa che nessuna cache pu avere modificato il blocco, quindi sufficiente trasferirlo nella cache che ha fatto
richiesta ed aggiornare nella directory il bit ad essa riferito. All'interno della cache avremo quindi il bit M sempre a 0 e il bit
V ora uguale a 1. Nella Mp la directory relativa sar prima
2) M = 1
ci significa che il blocco presente all'interno di un'altra cache che potrebbe averne modificato il contenuto. Per questa
ragione occorre presumere che Mp non abbia il blocco aggiornato, quindi dovr richiederlo alla cache, porre M = 0 e
consentire il trasferimento alla cache richiedente. Da questo momento si prosegue come nel caso precedente: nella cache
abbiamo V = 1 e M = 0, nella Mp
Dato che il blocco su cui scrivere non presente in cache abbiamo sicuramente V = 0, quindi per prima cosa la cache
richieder il blocco alla Mp. A seconda del valore di M riferito a quel blocco possono verificarsi tre situazioni:
1) M = 0 e tutti i bit V = 0
Valgono le stesse considerazioni della fase di lettura. Il blocco viene trasferito direttamente e M viene portato a 1 per
consentire la modifica. Nella cache avremo V=1 e M=1; nella Mp
2) M = 0 e almeno un bit V = 1
In questo caso almeno una cache conterr il blocco desiderato: di conseguenza non sar sufficiente abilitare la modifica, ma
si dovranno informare dell'operazione tutte le cache che lo contenevano prima della modifica. Mp quindi invalider il
31 of 138
contenuto delle suddette cache, poi abiliter il trasferimento alla cache richiedente e porter M a 1 per consentire la
modifica. Avremo V = 1 e M = 1 nella cache richiedente, V = 0 e M = 0 nelle altre cache che contenevano il blocco in
questione. Nella Mp questa volta troveremo
Dal punto di vista pratico, questo sistema scomodo perch ci obbliga ad avere una Mp di dimensioni maggiori (a parit di
blocco) per poter contenere tutte le informazioni aggiuntive ed poco flessibile perch bisogna definire il numero della
cache prima di realizzare la Mp.
Una variante possibile consiste nell'utilizzare directory distribuite: nel sistema si prevedono solo i due bit V ed M associati
al blocco contenuti nelle cache.
Directory distribuite
Le informazioni sul blocco sono contenute nelle singole cache e la directory viene composta sul bus solo quando c' un
trasferimento.
La cache 1 fa una specie di Snooping e, se contiene il blocco e l'ha modificato, manda un segnale sul bus.
Ora la Mp si accorge che sta trasferendo una vecchia copia del blocco, cos costretta, dato che gi sta facendo il
trasferimento, a invalidarlo.
Oltre che nel caso di Miss su Read, ci avviene anche quando si verifica un Miss su Write o un Hit con il bit M = 0.
Broadcast
La richiesta del blocco viene effettuata alla cache che attualmente lo contiene invece che alla Mp, per cui abbiamo una sola
copia di ciascun blocco.
Cache Inhibit
Il problema viene risolto con un metodo piuttosto drastico: si impedisce che i blocchi condivisi vengano letti nelle cache,
cos la Mp rimane sempre aggiornata. E' per evidente che, in questo caso, l'utilit della cache risulta nulla.
Qualcosa di simile a questa tecnica gi stato analizzato parlando dello Snooping: l'uso del bit Global pu essere visto
come una versione pi blanda del Cache Inhibit. Un blocco con il bit Global a 0 pu essere trasferito nella cache senza
alcun controllo, mentre se il bit a 1 pu essere trasferito nella cache solo con controllo.
Se Cache Inhibit vale 1 il blocco non pu essere trasferito.
Considerazioni generali
In linea di principio, nei sistemi multiprocessore si utilizza una politica di Copy Back; tuttavia, dati i problemi che essa
comporta, se ne utilizza una versione modificata rispetto a quanto descritto finora. Il Motorola 88000, ad esempio, durante
la prima scrittura di un blocco usa il Write Through con tecnica snooping; poi usa il Copy Back unito ad una tecnica tipo
directory distribuite (tutte le cache controllano se un blocco da loro modificato viene richiesto da un'altra cache).
32 of 138
In presenza di un sistema monoprocessore realizzato come sopra potremmo trovarci di fronte agli stessi problemi di un
sistema multiprocessore, anche se in misura minore. Per risolvere il problema si possono utilizzare directory distribuite, ma
una soluzione costosa e quindi giustificata solo in casi particolarmente delicati. Altrimenti si dirotta il DMA attraverso la
cache e si utilizza il Broadcast. Purtroppo questo metodo peggiora le prestazioni perch rallenta la CPU e crea notevoli
interferenze, a causa del fatto che il DMA lavora spesso con indirizzi consecutivi e svuota pezzi di cache utili alla CPU.
Si pu dividere la cache in due parti, una per le istruzioni e l'altra per i dati, cos da scindere il prelievo di istruzioni e quello
di operandi e ottimizzare differentemente le due memorie. Questo tipo di soluzione vantaggiosa solo se esistono 2 bus
indirizzi. In questo modo, possiamo fare fetch di istruzioni in parallelo a prelievo di operandi:
A seconda delle macchine potremmo anche trovare due bus di indirizzi, per non rallentare il prelievo istruzioni quando si
preleva un operando.
Ovviamente questo tipo di cosa deve prevedere una CPU pipeline (i RISC adottano tutti questa soluzione).
Vi sono due principali ostacoli all'utilizzo di questa soluzione: il primo dato dal codice automodificante, che costringe a
vuotare le due cache e aggiornare la Mp prima di poter prelevare le nuove istruzioni; il secondo rappresentato dalla scarsa
flessibilit, in quanto le due cache hanno dimensioni prefissate e non possibile agire diversamente nel caso in cui un
programma richieda pi spazio per i dati e meno per le istruzioni (o viceversa).
Nota:
l'architettura a bus separati viene denominata "Harvard" dal nome del calcolatore Harvard Mk.III del 1950, che possedeva
tamburi magnetici separati per dati ed istruzioni. L'architettura che prevede un bus unico invece detta "Princeton"
dall'omonima universit che, all'epoca, progettava i propri calcolatori seguendo le idee di Von Neumann, il quale
propugnava l'uso di una memoria unificata.
Se la cache ha un alto grado di associativit, occorre scegliere quale blocco eliminare all'interno di un set per fare posto ad
un nuovo blocco. E' ragionevole pensare che il migliore algoritmo sia di tipo LRU. Purtroppo, realizzare un LRU hardware
molto difficile perch ci sarebbero da riordinare spesso i set e l'operazione dovrebbe terminare in pochi ns. Un algoritmo
FIFO sarebbe decisamente pi semplice, ma limiterebbe le prestazioni. Comunque, nella maggior parte dei casi si effettua
una scelta casuale del blocco, che dal punto di vista delle prestazioni il metodo peggiore, ma il pi facile da realizzare.
33 of 138
Utilizzo di indirizzi virtuali
Questo tipo di organizzazione lenta perch la MMU e la cache lavorano in serie. Un sistema migliore sarebbe far lavorare
in parallelo cache e MMU su indirizzi virtuali (figura b)). Di fatto per, pur riuscendo ad approssimarla bene, questa
soluzione non viene realizzata per la presenza di problemi nella corrispondenza tra indirizzi virtuali ed indirizzi fisici.
Un task potrebbe indirizzare un proprio dato con lo stesso indirizzo virtuale del task precedente. Per ovviare si pu operare
in due direzioni: invalidare la cache ad ogni cambio di task (cosicch l'accesso venga sempre fatto in Mp), oppure
aggiungere nella RAM Tag un identificatore del task attuale.
Questo metodo tuttavia impedisce di vedere uno stesso dato da task diversi. La soluzione finale consiste in un ulteriore
identificatore (n. di job), che indica che un blocco accessibile indipendentemente dal task che lo richiede.
Indirizzi virtuali distinti corrispondenti allo stesso indirizzo fisico (problema dei sinonimi)
Due task (o due parti dello stesso task) potrebbero indirizzare lo stesso dato usando indirizzi virtuali differenti: in questo
caso, potrebbe anche accadere che una delle due copie non fosse aggiornata. La soluzione pi elegante al problema consiste
nell'introduzione del Reverse Translation Buffer (RTB), realizzato mediante una memoria associativa, che viene aggiornato
ad ogni cambiamento del contenuto della cache.
34 of 138
Quando si verifica un Miss bisogna verificare se nella cache esiste gi il dato cercato sotto forma di un sinonimo dello
stesso e, in caso affermativo, bisogna utilizzarlo invece di leggerlo dalla Mp. Se ci non avvenisse perderemmo la coerenza
dei dati.
Si inizia con la traduzione dell'indirizzo virtuale in fisico come al solito, ma ora si accede alla cache con il VA, mentre il PA
viene passato allo RTB per compiere la traduzione inversa PA/VA: se esiste in cache lo stesso dato con un diverso VA si
avr un Miss nella cache e un Hit nello RTB, rilevando cos la presenza di un sinonimo. Il nuovo VA con cui cercare il dato
va sostituito al vecchio perch ragionevole pensare che si continui ad effettuare la ricerca allo stesso modo. Occorre per
sottolineare che questa procedura non banale, poich i due VA potrebbero appartenere a due set diversi e quindi
dovremmo spostare il dato da un set ad un altro.
2offset-(b+2)
dove:
Con 5 bit andiamo poco lontano. Cos, se vogliamo pi set ci tocca aumentare l'associativit. Ad esempio, se volessimo una
cache da 16 KB dovremmo fare una struttura associativa a ben 32 vie!
Una soluzione a questo problema aumentare la dimensione della pagina, ad es., 4KB
Adesso abbiamo 16 KB di cache, che possono essere realizzati con una memoria associativa a 4 vie (situazione gi pi
ragionevole; per cos non pi il VAX).
Cache a pi livelli
E' il compromesso migliore fra le varie soluzioni e perci oggi viene adottato da quasi tutti i costruttori.
35 of 138
i. un primo livello (L1) altamente associativo, di dimensioni ridotte, spesso sdoppiato (istruzioni e dati) e normalmente
contenuto nella CPU;
ii. un secondo livello (L2) direct mapped, di dimensioni maggiori, normalmente residente all'esterno della CPU.
Cache di I livello
In genere associativa a due vie e ha dimensioni ridotte (tra 8 e 16 KB), perch deve essere contenuta all'interno della CPU.
Quasi sempre mescola indirizzi fisici e virtuali.
Cache di II livello
Normalmente direct mapped (perch essendo abbastanza grande difficile che ci siano conflitti sul set) e ha dimensioni
comprese tra 256 KB e 1 MB. Lavora su indirizzi fisici.
Il I livello associativo a due vie e contiene soltanto istruzioni. La dimensione 1 KB, il blocco da 8 byte e TA= 80 ns.
Il II livello direct mapped, da 256 KB con TA=160 ns; il blocco da 64 byte, diviso in due sottoblocchi. Per mantenere
la coerenza dei dati si usa lo Snooping insieme ad una doppia RAM Tag.
Il I livello ha cache dati ed istruzioni, associative a due vie, da 8 KB. Da un punto di vista delle prestazioni, le due cache
di primo livello sono considerate a ritardo zero (ci non vero in assoluto, circa 20 ns, ma risulta tale perch nel
frattempo la CPU continua a lavorare).
Il II livello direct mapped e pu raggiungere la dimensione di 1 MB. Il II livello introduce un CK di ritardo.
In caso di Miss, viene trasferita per prima la parola richiesta (ci accelera lo sgancio della CPU dal trasferimento dati), il
blocco pu essere di 32 o 64 KB. Il meccanismo per verificare la coerenza un misto di snooping e directory.
Nota conclusiva
L'uso di memoria cache non limitato alla CPU: spesso, infatti, si associa una cache al controller del disco fisso per
velocizzare il trasferimento dati dalle memorie di massa alla Mp.
36 of 138
Le prestazioni di questa memoria possono anche essere molto basse in confronto a quelle della cache della CPU, perch
comunque il disco non in grado di sfruttare appieno una cache molto veloce. Infatti, in assenza di hardware dedicato, si
pu utilizzare una parte della Mp gestita da software che lo simuli.
La Pipeline
Introduzione
I circuiti necessari all'esecuzione di una funzione logica qualsiasi possono essere cos schematizzati:
La rete logica pu essere molto estesa (ben pi di due livelli). Se abbiamo molti dati da processare e vogliamo velocizzare il
tutto possiamo:
Nata inizialmente per sistemi di elaborazione dei segnali, questa tecnica consiste nella divisione dell'attivit in pi fasi e
nella loro sovrapposizione. In altri termini, la pipeline consente di disaccoppiare momenti logicamente a s stanti
dell'elaborazione ed eseguirli in parallelo con altri. Per riuscire a compiere una tale operazione sono necessari dei registri
che mantengano l'informazione parzialmente elaborata in ogni fase.
Questa tecnica pu essere applicata facilmente in macchine con struttura molto regolare, i RISC, pi difficilmente nei CISC.
L'istruzione di una macchina RISC pu essere divisa in cinque parti fondamentali:
37 of 138
1. Instruction Fetch Prelievo istruzione, incremento PC
2. Instruction Decode Decodifica istruzione e prelievo operandi
3. Execution Esecuzione istruzione
4. Memory Accesso in memoria (scrittura o lettura)
5. Write Buffer Scrittura su register file
Con l'organizzazione appena vista, non si possono compiere operazioni su operandi prelevati direttamente dalla memoria (in
tal caso la fase 4 precederebbe la 3). Per esempio, non sono possibili operazioni come ADD (R3),(R1),(R2).
La suddivisione del tutto generale: non detto che tutte le istruzioni abbiano tutte e cinque le parti. Ad esempio, ADD R3,
R1, R2 non usa la fase MEM.
L'introduzione di registri intermedi accresce il tempo di esecuzione della singola istruzione. Inoltre, poich ciascuna fase
deve poter funzionare in parallelo con quelle delle altre istruzioni, necessario imporre che tutte quante abbiano la stessa
durata: quella della fase pi lenta. L'aumento del tempo di esecuzione di ogni istruzione per ampiamente compensato
dalla riduzione (dovuta al parallelismo) di quello del complesso delle istruzioni.
ESEMPIO: Supponiamo di avere tre istruzioni in cui la durata delle fasi sia di 50 ns ad eccezione della
fase di EX che dura 60 ns.
L'introduzione della pipeline aumenta il tempo di esecuzione della singola istruzione di 65 ns, ma ogni
65 ns viene eseguita una nuova istruzione.
Da quanto detto finora sembrerebbe che la tecnica di pipeline porti vantaggi illimitati: non cos! Oltre al rallentamento del
tempo di esecuzione della singola istruzione esistono altri limiti ai benefici che una pipeline pu apportare all'architettura di
un sistema:
i. il costo sale perch la pipeline ha bisogno di risorse hardware in pi (ad esempio, bisogna duplicare il bus per evitare
che il fetch di indirizzi e dati entrino in conflitto);
ii. la suddivisione in fasi ancora pi elementari diventa pi difficile;
iii. la parallelizzazione crea conflitti (hazard) che riducono durante il run-time l'efficienza della pipeline. Esistono tre
classi di hazard: strutturale, sui dati e sui controlli.
38 of 138
Lo scambio con la memoria presuppone che ci sia una cache che funziona molto bene e che non perda troppo tempo nella
ricerca di un dato. In caso contrario, MEM potrebbe salire a 200-300 ns.
Analizziamo ora i tre tipi di hazard.
Hazard strutturali
Gli hazard strutturali si verificano quando una stessa risorsa richiesta da pi istruzioni. Essi dipendono da come costruita
la CPU. Vediamo alcuni casi di hazard strutturali e, ove possibile, un tentativo di dar loro una soluzione.
ESEMPIO: Abbiamo un register file e due istruzioni stanno cercando di scrivere su due registri in esso
contenuti contemporaneamente.
Per ottimizzare si inserisce una "bolla" o "stallo", cio un ritardo, nella struttura pipeline della seconda
istruzione. La bolla elimina il conflitto facendo slittare la fase che lo genera.
39 of 138
ESEMPIO: Un altro tipo di hazard pu verificarsi se inseriamo nel sistema, parallelamente all'unit intera,
un'unit in virgola mobile pi lenta: per procedere con l'elaborazione si deve aspettare che termini la fase pi
lenta. La struttura generale pu essere:
In questa situazione, o accettiamo il rallentamento o realizziamo con un'architettura pipeline anche l'unit
floating point. Senza pipeline, si verificherebbero hazard strutturali quando vi fossero due istruzioni floating
point vicine tra loro. Supponiamo, ad esempio, che le istruzioni dell'unit intera e di quella floating point
siano siffatte:
Il problema del conflitto, sempre accettando il ritardo, non si pone se una delle due istruzioni intera. In
questo caso, invece, avremmo un "sorpasso" di istruzione, cio l'istruzione floating point terminerebbe dopo
quella intera (pur essendo iniziata prima).
Per evitare i problemi appena esaminati possiamo inserire la pipeline anche nell'unit FP.
Questa soluzione, per, viene scelta difficilmente perch ha un costo elevato e, in alcuni casi, peggiora
addirittura la situazione (cio il sistema risulta pi lento dell'equivalente non pipelinizzato). Il peggioramento
si verifica quando le operazioni FP sono poche rispetto a quelle non FP e le potenzialit della pipeline non
vengono sfruttate appieno.
40 of 138
ESEMPIO: Altro caso di hazard strutturale si pu verificare sul bus.
La fase IF della SUB entra in conflitto con la fase MEM della LD (figura a). Allora, si inserisce una bolla che ritardi il
fetch della SUB di un ciclo di clock (figura b). Si noti che il conflitto sembra riproporsi tra la fase MEM della OR e il
fetch della AND. In realt, il conflitto non esiste perch la fase MEM delle operazioni aritmetiche non usata.
Valutiamo il peggioramento che abbiamo causato introducendo la bolla. Supponendo CPI=1.2 ideale (non vale 1 perch
ipotizziamo che da qualche parte ci sia un ritardo: ad es un miss nella cache) e di avere il 30% di LD e ST (lo ST ha lo
stesso problema del LD).
CPIeffettivo = 1.2 +1 CK quando l'istruzione non potr essere eseguita = 1.2 + 0.31=1.5
La cosa pi semplice per evitare questo tipo di hazard eliminare la causa del conflitto. Ecco perch tutte le macchine
pipeline hanno due bus: uno per le istruzioni e uno per i dati.
Data hazard
I programmatori scrivono il software seguendo un filo logico sequenziale. Poich la parallelizzazione operata dalla pipeline
sconvolge il flusso delle istruzioni, esiste la possibilit che venga sovvertito l'ordine di accesso ai dati.
I data hazard derivano da quest'eventualit. Essi si possono verificare quando uno stesso dato usato da pi istruzioni.
Esistono tre tipi di data hazard:
Si ha quando si richiede la lettura di un dato prima che esso sia stato aggiornato da un'istruzione precedente.
ESEMPIO:
In questo caso, R1 funge da registro destinazione per la prima operazione e da registro operando per la seconda. A
causa della pipeline, il contenuto di R1 viene letto prima del compimento dell'addizione. Ci significa che
l'operazione di OR verr eseguita tra il contenuto di R5 e il valore di R1 non ancora aggiornato, diversamente dalle
intenzioni del programmatore. Non neanche certo che ci che si legge nella fase di ID della OR sia il valore
vecchio. Se, ad esempio, tra le due istruzioni ADD e OR si verifica un interrupt oppure un Miss utilizziamo il
valore nuovo.
41 of 138
Vediamo quali soluzioni si possono adottare per ovviare a questo tipo di hazard.
Pipeline Interlock
E' un'unit che verifica che gli operandi richiesti nella fase di ID siano validi, in caso contrario inserisce delle bolle. Per
sapere se un registro operando accessibile o meno gli si affianca un bit di valid. Quest'ultimo, durante un'istruzione, viene
settato opportunamente ogni volta che diventa o meno disponibile per le altre istruzioni.
La fase di ID va ripetuta perch la fase di prelievo operandi non era stata compiuta.
Un'alternativa al pipeline interlock costituita da un compilatore che calcola dove va messa una bolla e ci mette un NOP
(soluzione adottata nel MIPS).
Bypass
Gli accumulatori sono diversi dai registri: questi ultimi sono visibili al programmatore.
Se la scrittura avviene alla fine del periodo di clock della ima istruzione, per mettere l'operando nell'accumulatore per la i+3
devo avere tre bolle di ritardo. Se invece la scrittura sul register file avvenisse a met del ciclo di clock, potremmo scrivere
42 of 138
l'operando negli accumulatori alla fine del ciclo. Cos facendo eliminiamo una bolla:
Quindi, ogni istruzione che seguita da un'altra che usa il suo risultato come operando, viene ritardata di 2 cicli di clock.
R1 comunque presente nel sistema prima che noi possiamo averlo dove ci serve.
La gestione dei multiplexer non per banale. La soluzione pi semplice operare una gestione a livello decentrato.
Bisogna associare quindi a ogni dato l'informazione sul registro che la contiene.
Devo prevedere il bypass anche nei confronti del MDR e del MAR.
43 of 138
ESEMPIO:
In questo caso il bypass non risolutivo perch il dato non ancora presente nella CPU quando serve. Vediamo quanto
peggiorano le prestazioni della nostra macchina in questo caso. Se supponiamo di avere il 20% di LD, met dei quali
seguiti da un'istruzione che utilizza il dato prelevato, e di avere CPI=1 avremo che:
Un caso frequente di data hazard si ha quando dobbiamo sommare 2 numeri contenuti in memoria e mettere il risultato
ancora in memoria (A=B+C).
In questo caso, per, la bolla recuperabile dal compilatore. Infatti, immaginando di avere pi operazioni successive,
possibile separare le istruzioni in conflitto:
Cos facendo, per, aumenta il numero dei registri usati. Inoltre, non detto che sia sempre possibile.
Questo tipo di hazard si verifica con istruzioni pi complesse (come quelle dei processori CISC).
44 of 138
Write after write
Si ha quando il valore di un registro viene aggiornato prima dall'istruzione pi recente e poi da quella meno recente. Quindi,
la seconda scrittura va persa.
ESEMPIO:
Control Hazard
Si verificano quando vi sono istruzioni di salto che potrebbero cambiare il flusso del programma. Innanzitutto vediamo
come eseguito un JMP: il nuovo valore del PC calcolato alla fine della fase EX ed aggiornato alla fine della fase
MEM.
Dobbiamo usare la ALU per calcolare l'indirizzo di destinazione (alla fine della fase EX ho il nuovo valore del PC) e
valutare la condizione di salto (durante la fase MEM).
In caso di control hazard, quindi, la situazione peggiore prevede l'inserimento di tre bolle.
In realt, le bolle sono di meno: la prima una fase di IF normale (i cicli di CK impiegati sono per sempre tre).
Nel calcolo delle prestazioni allora dovremo innanzitutto tener conto della percentuale di istruzioni di salto presenti
mediamente in un programma.
In realt, la statistica ci dice che i JMP non condizionati sono solo il 2-8%, mentre quelli condizionati
sono solo 11-17% (RISC) 13-25% (CISC).
Un'altra considerazione da fare sull'effettivo compimento del salto condizionato (i JMP non condizionati sono compiuti
45 of 138
sempre, mentre quelli condizionati sono compiuti per pi del 50%).
Per migliorare il nostro sistema, cerchiamo un modo per ridurre il numero di bolle. Se aggiornassimo il PC alla fine della
fase di ID otterremmo una sola bolla.
Per realizzare questa prima approssimazione della soluzione, dovremmo avere un'ALU dedicata al calcolo dell'indirizzo
(quella gi esistente sicuramente occupata dall'esecuzione di altre istruzioni precedenti). L'ALU dedicata dovrebbe
lavorare su tutte le istruzioni (non solo quelle di JMP) prima di sapere di che istruzione si tratta, conservando il risultato se
l'istruzione un JMP, buttandolo via altrimenti.
Per eliminare anche l'ultima bolla si pu inserire hardware aggiuntivo che confronti i due PC (prima e dopo
l'aggiornamento) e decida se ripetere o meno la fase di IF. In questo modo, la bolla scompare quando il salto non viene
effettuato. Poich per statisticamente si dimostrato che la percentuale di salti effettuati maggiore di quella dei salti non
effettuati (70% vs 30%), possiamo cercare una soluzione che faccia scomparire la bolla rimasta nel caso di salto effettuato:
essa consiste nel prelevare l'istruzione successiva al salto come se esso dovesse essere sempre eseguito. In caso contrario, si
ripeter la fase IF.
Le tecniche per cercare di recuperare la bolla sono due:
Questa tecnica cerca di predire il nuovo valore del PC. Essa utilizza una cache che usa come TAG il valore corrente del PC
e come DATO il nuovo valore del PC. All'inizio della fase IF si cerca nella TAG il valore attuale del PC se ho un hit eseguo
la IF successiva con questo nuovo valore, altrimenti eseguo la IF dell'istruzione a PC+1.
La cache viene aggiornata quando un salto viene effettuato per la prima volta: si ha un MISS, si fa IF a PC+1,
successivamente durante ID ci si accorge che in realt si tratta di un JMP quindi si scarta la IF fatta e si ripete la fase di IF
con il PC giusto e si aggiorna la cache. Dal punto di vista delle prestazioni abbiamo che:
Salto ritardato
Consiste nel mettere subito dopo il JMP un'istruzione che verr sicuramente eseguita. Per realizzare questa soluzione
dobbiamo istruire il compilatore a effettuare una tra le seguenti operazioni (elencate in ordine di preferenza):
a. mettere dopo il JMP un'istruzione che dovrebbe essere eseguita prima (se si riesce a fare questo si ottiene che
CPIideale = CPIeffettivo).
b. se la soluzione precedente non possibile (perch serve la valutazione della condizione di salto), prelevare in
anticipo un'istruzione dal codice che dovrebbe essere eseguito dopo il salto.
46 of 138
c. se neanche la soluzione b) possibile, allora non si sposta nulla.
d. anche possibile avere situazioni in cui tutte e tre le soluzioni precedenti non possono essere adottate. In questi
casi, si mette un NOP dopo il JMP.
ESEMPIO
Col metodo del salto ritardato l'istruzione dopo il JMP viene sempre eseguita. Il compilatore deve quindi verificare che essa
non sia incompatibile con il codice in cui viene inserita (ad esempio, che non cambi il valore di un registro usato da un'altra)
ed eventualmente aggiungerne una che ne annulli gli effetti.
Sia il salto ritardato che le altre soluzioni sono cablate nella macchina, cio la scelta dell'una o dell'altra effettuata in fase
di progetto. La soluzione del salto ritardato utilizzata da quasi tutti i RISC perch economica e permette di migliorare
maggiormente le prestazioni.
Gestione interrupt
Se la macchina non organizzata a pipeline, ogni istruzione comincia dopo che finita la precedente. Quindi se arriva un
interrupt a met di un'istruzione, la sua accettazione viene ritardata all'intervallo tra due istruzioni: quella in cui arrivato e
la successiva.
In una macchina pipeline non possibile individuare un istante in cui le istruzioni sono finite, perch c' sempre
un'istruzione che sta lavorando. Allora l'interrupt deve essere preso in considerazione senza aspettare che il flusso di
istruzioni si fermi.
Il primo problema dato dalla provenienza dell'interrupt. Se arriva dall'esterno non ci sono problemi a gestirlo, se invece
proviene dall'interno (es. page fault, perch un LD andato a indirizzare una cella non in memoria fisica) possiamo operare
come segue:
Tutto ci funziona perch l'istruzione molto semplice. Se invece complessa (es. un registro che si autoincrementa
quando viene utilizzato) le cose si complicano.
47 of 138
ESEMPIO:
Quando si verifica l'interrupt per page fault, R2 gi stato incrementato e non pi possibile far ripartire il programma
con l'algoritmo visto, perch riprendendo a n+1, ci ritroveremmo con R2 cambiato
I problemi non nascono se la macchina rimanda tutte le modifiche all'ultima fase (WB). Per implementare questo
procedimento si possono usare una serie di registri di appoggio come buffer temporaneo per tutti i valori parziali.
Ovviamente, esistono due modi di sfruttare quest'architettura.
Il primo consiste nel memorizzare nel buffer di appoggio i nuovi valori che vengono calcolati durante l'istruzione e nel
trasferirli nel register file solo alla fine dell'istruzione. Se si verifica qualche problema, si ripete l'operazione con i vecchi
valori (invece che proseguire con quelli appena modificati).
Il secondo modo consiste nel memorizzare nel buffer di appoggio i vecchi valori invece delle modifiche, che verranno
invece effettuate sul register file. In caso di problemi, si andr a cercare nel buffer di appoggio (o file storico) i vecchi
valori.
Quando l'istruzione suvccessiva utilizza un dato modificato, occorre confrontare register file e registri di appoggio e, se
sono diversi, prendere il valore nel registro pi recente. E' necessario una specie di bypass e i registri di appoggio devono
contenere una coda con i valori parziali che vanno di volta in volta scritti sul register file.
ESEMPIO:
In questo caso, se seguiamo i tre passi elencati all'inizio del paragrafo, sorgono problemi legati al fatto che
i valori di R2 sono sbagliati.
Dopo ID di n+1, abbiamo R2V (vecchio) sul register file, R2N (nuovo) sul registro di appoggio. Dopo ID
di n+2 abbiamo R2V sul register file, R2N sul registro di appoggio e R2NN ancora sul registro di
appoggio, calcolato dopo aver usato R2N del registro di appoggio.
Quando n+1 finisce si scrive R2N sul register file e quando finisce n+2 vi si scrive R2NN.
Nel caso del file storico, il discorso lo stesso solo che, man mano, sul register file abbiamo il valore pi
nuovo e man mano sul registro di appoggio cancelliamo i dati vecchi a fine istruzione.
Dopo la fase di ID di n+2, abbiamo R2N e R2V sul registro di appoggio e R2NN sul register file. Quando
termina n+1, viene cancellato R2V sul registro di appoggio.
In sostanza, i metodi che sfruttano i registri d'appoggio non fanno altro che rendere "preciso" un interrupt che di per s non
lo sarebbe.
Un interrupt si dice "preciso" se consente di individuare un punto del programma tale che tutte le istruzioni precedenti al
punto in questione hanno terminato il loro compito e tutte quelle successive non hanno ancora modificato nulla all'interno
della macchina (per cui possono essere rieseguite senza problemi).
Con la divisione in fasi vista, possibile che si verifichino pi interrupt contemporaneamente.
48 of 138
Allora data una qualsiasi sequenza di istruzioni si possono avere fino a 4 interrupt contemporanei (uno per ogni istruzione in
corso, fase WB esclusa).
Supponiamo ad esempio, che n+1 generi interrupt per errore da memoria e l'istruzione n+3 generio un interrupt per codice
inesistente.
Si pu applicare l'algoritmo visto: identifichiamo la prima istruzione che ha generato l'interrupt, salviamo PC=n+1,
disabilitiamo le scritture a partire dall'istruzione n+1, serviamo l'interrupt; ripartiamo da PC+1 e riavremo subito un altro
interrupt per la ID di n+3. Questa volta lasciamo terminare n+1 e n+2, salviamo PC=n+3, disabilitiamo da n+3 e cos via.
La situazione esaminata di facile soluzione, ma non sempre si cos fortunati.
Supponiamo che n+2 abbia un interrupt per page fault e n+1 abbia un interrupt per errore aritmetico.
Il primo interrupt generato appartiene alla seconda istruzione. L'algoritmo visto non va pi bene perch l'istruzione n+1 non
riesce a terminare. Esso fallisce, quindi, nel caso in cui un'istruzione generi un interrupt dopo quello generato da una
successiva.
Facciamo perci le modifiche necessarie a risolvere anche questa situazione. I passi sono:
Allora, nel nostro caso, salviamo il PC della n-1 (prima istruzione non completata), serviamo l'interrupt e ripartiamo da n-1.
Dopodich riceviamo l'interrupt di n+1 (che prima bloccava la macchina), salviamo il PC di n (perch la n-1 finita),
serviamo l'interrupt e ripartiamo da n.
Le istruzioni precedenti non completate non possono essere lasciate andare avanti, perch c' il rischio di un interrupt
successivo (temporalmente) a quello ricevuto, ma in un'istruzione precedente.
Vettore di interrupt
un metodo pi pulito ed elegante di gestire interrupt che si accavallano. Il vettore di interrupt un vettore, associato a
ogni istruzione, che raccoglie le varie cause di interrupt man mano che si verificano. Esso viene esaminato a fine istruzione
e ha il vantaggio di servire gli interrupt nell'ordine delle istruzioni che li generano: quando si verifica un'interrupt, si
disabilita la scrittura in quella istruzione e nelle successive.
Ad esempio, nell'ultimo caso analizzato poco fa, il vettore di n+1 dice che c' stato interrupt nella fase di EX, il vettore di
n+2 dice che c' stato interrupt nella fase di IF ma, poich vengono esaminati a fine istruzione, gli interrupt sono serviti in
ordine logico di presentazione e non in ordine temporale di presentazione, come era avvenuto prima.
Ovviamente occorre che, quando si verifica un interrupt, si disabiliti la scrittura nell'istruzione che lo ha generato e nelle
Questo sistema pi costoso, migliora molto poco le prestazioni ma semplifica moltissimo la situazione se il set di
49 of 138
istruzioni complesso.
ESEMPIO:
ADD
JUMP se zero
In questo caso, ADD genera la condizione di salto. A seconda della macchina si pu inserire un'altra istruzione
tra ADD e JMP, per permettere che il condition code possa cambiare ed essere pronto per il JMP. Se la nuova
istruzione cambia il condition code usato dal JMP, per, sorgono dei problemi: possibile che il c.c. venga
modificato prima di essere usato dal JMP. A questo punto, se intervenisse un interrupt, il JMP, alla ripresa del
programma, vedrebbe l'ultimo set di condition code e non quello che avrebbe dovuto considerare.
In una macchina con pi unit in parallelo, la fase di test inglobata nel salto
Su macchine senza pipeline si tende a modificare automaticamente i condition code con molte istruzioni (questo riduce il
numero di istruzioni). Spesso lo cambiano anche LD e ST (VAX 780).
Su macchine con pipeline abbiamo due alternative:
a. si pu prevedere un doppio tipo per ciascun istruzione: uno con e uno senza aggiornamento del condition code (ad
esempio, IBM 360)
b. si pu inserire il test della condizione all'interno del JMP (opzione frequentemente adottata nei RISC), svincolando
cos le istruzioni dal mantenere una data successione. Per, poich in questo caso il test va fatto nella fase di
decodifica, serve altro hardware dedicato per poter verificare la condizione di JMP. Quindi si cerca sempre di avere
condizioni di salto molto semplici.
Nel caso di macchina pipeline necessario anche una specie di pipeline interlock dedicato a gestire i condition code:
avremo invalidazione del condition code quando troviamo un'istruzione che lo deve calcolare, una validazione quando
arriva in fondo alla stessa istruzione. Ad esempio, nel caso in cui l'istruzione sia "JMP se R1=0", il pipeline interlock si
occuper di bloccare tutti quelli che vorrebbero modificare R1.
50 of 138
ESEMPIO: Se avessimo la sequenza di istruzioni seguente
a=b+d
jmp se b=0
Senza pipeline
Tutte le macchine RISC hanno al proprio interno un'unit floating point. Introduciamo il calcolo delle operazioni FP,
mantenendo intatta la struttura in cinque fasi delle istruzioni.
L'unit che esegue il FP lenta e, pensando alla sequenza di fasi vista, la EX molto lunga se la ALU FP (ad es., una
divisione pu richiedere 40 cicli di clock per EX).
Condizionare la macchina a tale unit fa peggiorare notevolmente le prestazioni. Cambiamo allora la struttura della
macchina prevedendo pi unit d'esecuzione, ad esempio 4.
51 of 138
Se l'operazione su interi, impieghiamo un solo ciclo di clock, se FP avremo un ricircolo sull'unit interessata. Abbiamo
quindi istruzioni FP a struttura non fissa, mentre per l'istruzione intera manteniamo la solita temporizzazione. L'operazione
FP ha una sequenza di fasi con pi EX. Un'istruzione LOAD, ad esempio, finir prima di una FP.
La conseguenza che pur mantenendo una struttura semplice abbiamo un'infinit di hazard.
Hazard strutturali
La prima considerazione che le FPU sono realizzate senza pipeline (se lo fossero non avremmo hazard strutturali a questo
livello). Il motivo che l'uso del FP troppo limitato rispetto al costo che avrebbe la pipeline.
Perci, avendo due addizioni FP di seguito la seconda aspetta che la prima finisca.
Vi quindi l'introduzione di un certo numero di bolle, perch la seconda ADDFP andr in esecuzione solo dopo che la
prima sar uscita dalla fase di EX. Se invece avessimo avuto
non avremmo avuto bolle perch sarebbero andate su due unit diverse e le 4 unit di esecuzione avrebbero potuto lavorare
in parallelo.
L'hazard strutturale appena visto blocca l'istruzione nella fase di decodifica, perch l'unit FP non riesce a gestire due
istruzioni contemporaneamente.
Altro hazard possibile su WB, perch abbiamo istruzioni di durata diversa.
Nella struttura pipeline vista, WB era la quinta fase; in questo modo, quando l'istruzione si trovava nell'ultima fase, era
l'unica ad usare il register file (evitando hazard strutturali). Con l'inserimento di un'unit FP ci non vale pi perch le
52 of 138
istruzioni hanno durata diversa.
L'hazard strutturale sulla fase di scrittura inevitabile perch non posso prevedere n la sequenza di istruzioni n la loro
durata.
A. Possibilit di blocco in fasi diverse da ID: quando ci sono due istruzioni che devono scrivere contemporaneamente
sul register file, abbiamo una bolla; quindi le istruzioni si possono fermare non solo nella fase ID, ma anche pi
avanti e questo complica la macchina (prima, invece, se un'istruzione superava la fase di ID, non c'era pi possibilit
di hazard strutturali).
B. Necessit di un doppio register file: se un'istruzione non usa la fase WB al quinto ciclo di clock (perch EX pi
lunga) sicuramente abbiamo un hazard strutturale per sovrapposizione di due WB. Quindi, si ha bisogno di un
doppio register file, uno per interi e uno per FP. In tal modo, comunque, non si eliminano completamente questo tipo
di hazard; essi rimangono ancora in due casi:
1. due istruzioni FP di durata diversa;
2. un'istruzione FP e un load sul register file FP.
Data hazard
Con l'unit intera, i data hazard presenti erano tipicamente quelli RAW. Adesso abbiamo hazard di questo tipo (quindi
avremo bisogno di un eventuale bypass) ma anche altri data hazard. Il WAR non si verifica se continuiamo a prendere gli
operandi nella fase di ID e blocchiamo l'operazione se non sono pronti.
Facciamo invece un esempio di WAW, considerando il register file FP di registri F0, F1, ... e due istruzioni FP.
Probabilmente abbiamo un WAW perch SUB pi veloce di DIV e quindi viene eseguita prima e la prima scrittura che
facciamo quella relativa a SUB. Alla fine F0 ha il valore calcolato da DIV e non da SUB
Se avessimo avuto in mezzo qualche istruzione che utilizzava F0, non avremmo avuto il data hazard, cio il WAW si
verifica solo quando le due istruzioni non sono intervallate da un'altra che usi F0 come sorgente. Ad esempio, la sequenza di
istruzioni
non ha hazard WAW. ADD si ferma perch F0 non disponibile e quindi ho delle bolle in attesa del risultato. Anche SUB
si blocca, essendo dopo ADD: la decodifica di SUB fatta solo quando ADD riparte, il che accade dopo che DIV ha finito
la fase di EX e quindi SUB finisce dopo DIV.
Questa seconda eventualit si verifica raramente. Per eliminare sicuramente il WAW, occorre rilevarlo a livello di pipeline
interlock e bloccare la seconda scrittura finch non stata eseguita la prima. Una soluzione migliore (che richiede una
macchina pi intelligente) quella di eliminare la prima scrittura (perch inutile) o addirittura la prima istruzione
(alternativa ancora migliore perch teniamo libera un'unit che pu essere utilizzata da altre istruzioni, riducendo gli hazard
strutturali). L'eliminazione deve essere fatta dalla CPU e non dal compilatore perch l'istruzione potrebbe essere quella
inserita dopo un salto ritardato.
53 of 138
La gestione di interrupt con la FPU
I problemi che saltano fuori sono legati al fatto che l'ordine con cui terminano le istruzioni non lo stesso con cui
cominciano.
Supponiamo di avere una divisione (che, essendo pi lunga, quella che genera pi problemi) e di avere un interrupt
generato da SUB. In tale situazione potremmo avere DIV in corso di esecuzione e ADD terminata.
L'istruzione SUB ha delle bolle perch nella fase di EX richiederebbe la stessa unit FP usata da ADD e deve aspettare che
ADD la liberi.
Si potrebbe pensare di far finire DIV e poi servire l'interrupt, rendendolo preciso, ma non sempre ci possibile (ad
esempio, se c' anche un interrupt su DIV).
A questo punto possiamo adottare tre tipi di politiche:
1. Accettare interrupt non precisi.In questo caso, se si verificano dei problemi la macchina si blocca. Questa soluzione
pu essere accettabile in un sistema monoprogrammato e monoutente, ma sicuramente non lo in uno
multiprogrammato (lo se gli interrupt imprecisi sono limitati alle istruzioni FP e l'effetto quello di abortire un
programma che comunque sarebbe ormai privo di risultati significativi).
4. Prevedere che ogni unit FP dia un consenso per eseguire l'istruzione successiva.Tale consenso dato dall'unit FP
che ha deciso che non ci sono pi possibilit di interrupt. Questa soluzione quella adottata dal MIPS. Ad esempio:
DIV eseguita quando ADD d l'OK.L'unit FP deve fare la verifica il pi velocemente possibile, per dare il
consenso. Questo non significa che ADD finita, ma che non ci sono pi possibilit di interrupt, in modo tale che,
se ho un interrupt su DIV, so che ADD pu finire e non provoca il blocco della macchina.
54 of 138
Schedulazione dinamica
La schedulazione a cui siamo abituati , in prima approssimazione, statica perch fatta dal compilatore, cio fuori linea, e
quando arriva alla CPU, la sequenza delle istruzioni fissa (il compilatore avr ottimizzato al meglio la sequenza).
Nel caso di schedulazione dinamica invece il processore che decide l'ordine con cui vanno eseguite le istruzioni (cercher
quello che permette lo svolgimento pi veloce).
Se la schedulazione statica, si inizia con DIV e si trova un data hazard su ADD (che si ferma e blocca
anche SUB). Quindi, DIV va avanti ma le altre unit sono ferme.
Con la schedulazione dinamica eseguiamo un sorpasso: DIV e SUB sono eseguite in parallelo e poi si
esegue ADD. C' un miglioramento delle prestazioni perch vengono eseguite le istruzioni con gli
operandi disponibili.
L'esecuzione della sequenza di istruzioni appena vista nell'esempio pu essere ottimizzata anche dal compilatore. Esistono
per dei casi in cui la la schedulazione dinamica si comporta meglio di quella statica: in generale in caso di imprevisti. Ad
esempio, quando c' un Miss in cache o quando il compilatore non conosce la CPU da analizzare (ad esempio la SPARC
costruita con molti chip diversi).
La schedulazione dinamica fa nascere molti data hazard (RAW , WAR e WAW).
ESEMPIO: WAR
SUB pu essere anticipato rispetto ad ADD perch ha gli operandi pronti, ma allora si verifica un data
hazard di tipo WAR perch dovrei leggere F7 prima che sia modificato da SUB, invece anticipando tale
istruzione, in ADD prenderei un valore diverso da quello previsto dal programmatore.
ESEMPIO: WAW
la scrittura di SUB sullo stesso registro usato da ADD e le due scritture sono invertite.
Per realizzare la schedulazione dinamica la macchina deve avere una scissione della fase di ID in due fasi elementari: una in
cui si decide l'esecuzione dell'istruzione e una in cui si effettua la verifica di disponibilit e il prelievo degli operandi.
55 of 138
La fase di verifica della disponibilit degli operandi (e relativa attesa) stavolta inglobata nell'unit di esecuzione. Una
volta assegnata un'istruzione a ciascuna unit FP, l'eventuale attesa avviene all'interno della singola FPU e non blocca tutte
le altre.
Il controllo sulla fattibilit delle istruzioni quindi diviso in due parti:
Quella dello scoreboard una tecnica usata sul CDC 6600 (macchina che assomiglia a un supercalcolatore) che ha 16 unit
aritmetiche. Per semplicit, studiamo il funzionamento di una struttura dotata di sole 4 unit aritmetiche:
56 of 138
Per eseguire un'operazione dal momento che arriva nella decodifica, i passi da fare sono:
a. verificare se c' l'unit disponibile ( quello che si fa nella vecchia fase di decodifica)
b. verificare i data hazard di tipo WAW (questa la politica del CDC 6600, ma una verifica che pu essere fatta
altrove)
c. mettere in esecuzione un'istruzione su un unit aritmetica. Verificare la disponibilit degli operandi.
d. eseguire l'istruzione
e. verificare i data hazard di tipo WAR e , se sono presenti, bloccare la scrittura (cio l'esecuzione finita e il dato
disponibile, ma lo si tiene in un'unit e non lo si mette nel register file finch non c' pi pericolo di WAR). Se
l'unit FP avesse i registri per prendere subito gli operandi disponibili, tale controllo non ci sarebbe. Nel CDC 6600
tali registri non ci sono e quindi va fatto.
f. verificare gli hazard strutturali, che sono numerosi avendo tante unit che lavorano in parallelo e quindi molte
possibilit di scrittura contemporanea su register file. Nel caso una scrittura va bloccata e si fa eseguire l'altra.
Se qualcosa non va nei punti a) o b) l'istruzione si ferma e blocca tutta la coda. Una volta superato il punto c) l'istruzione
pu fermarsi senza bloccare le successive.
Lo scoreboard un blocco hardware che gestisce le informazioni per scoprire e risolvere i data hazard. E' costituito da una
parte di elaborazione hardware e una piccola memoria contenente le informazioni. Le informazioni sono gestite con tre
tabelle:
57 of 138
- qual' la loro disponibilit;
- qual' la provenienza degli operandi, nel caso non siano disponibili (cio da quale unit deve provenire l'operando
per controllare quando disponibile e caricarlo).
Questa tabella permette di seguire la disponibilit degli operandi e sbloccare le varie unit non appena essi siano
disponibili. Provenienza Op1 e Provenienza Op2 contengono il numero delle unit da cui deve arrivare il relativo
operando.
Vediamo un esempio applicativo di come lo scoreboard implementa una tale logica. Prendiamo, ad esempio, la seguente
sequenza di istruzioni:
Abbiamo molti hazard: F2 destinazione di LD e operando di MOLT e SUB (RAW); F0 destinazione di MOLT e
operando di DIV (RAW); F8 destinazione di SUB e operando di ADD (RAW); F6 operando di DIV e destinazione di
ADD (WAR).
Vediamo come risultano le tabelle prima della scrittura di F0 da parte di MOLT.
SUB ha finito perch pi veloce di MOLT, allora ADD messa in esecuzione e ha gli operandi disponibili. Quindi viene
eseguita ma non pu scrivere i risultati per un WAR sul registro F6, che deve essere prima letto da DIV. Poich F6 deve
essere scritto dall'unit di ADD e non disponibile (nel senso che non ha il valore aggiornato), mettiamo nella tabella di
stato dei registri il numero 4, che indica l'unit da cui si aspetta il valore.
Vediamo l'ultima situazione prima della scrittura di F10 da parte di DIV (DIV l'ultima a finire perch la pi lenta).
58 of 138
ADD ha scritto perch DIV ha preso gli operandi; tutte le istruzioni sono finite, tranne DIV che deve scrivere. Tutte le unit
sono libere tranne quella di DIV.
In realt, le tabelle non rimangono mai vuote: vengono riempite dai dati delle istruzioni che si avvicendano a quelle che
terminano e vengono tolte dalla tabella di stato delle istruzioni.
Considerazioni:
i. Il modo in cui stata realizzata la scoreboard pu essere migliorato. Ad esempio, per la gestione dei data hazard di
tipo WAR e per il fatto che la struttura non prevede il bypass (quindi si perde tempo nella scrittura sui registri)
ii. Le prestazioni di una macchine dotata di scoreboard aumentano da 1.7 a 2.5 a seconda del programma. Ci significa
che con la schedulazione dinamica abbiamo circa un raddoppio delle prestazioni.
iii. il costo elevato perch la complessit circuitale e delle memorie per le tabelle quella di un'unit FP. Tuttavia, se
le unit sono 16 (di cui una in realt rappresenta lo scoreboard) allora il costo del 6% e il rapporto
costo/prestazioni vantaggioso.
59 of 138
Supponiamo di essere, ad esempio, nello stato di salto effettuato. Quando la previsione risulta sbagliata cambiamo stato ma
non previsione. Prima di cambiare previsione, aspettiamo che essa risulti sbagliata 2 volte. In questo modo riusciamo ad
eliminare gli eventi eccezionali e nel caso del 90% di salti effettuati si ha una previsione esatta al 90% (invece che del 80%)
sui salti.
Recuperiamo il "piccolo" particolare lasciato da parte, cio il fatto che ogni cella del BPB non riservata a un solo indirizzo
ma a tutti gli indirizzi che terminano con gli stessi bit meno significativi. Ad esempio, se il BPB fatto da 1024 celle, le
istruzioni 38294 e 56294 faranno riferimento alla stessa cella e l'informazione contenuta sar quella relativa all'ultimo
indirizzo di salto terminante con 294 (qualcosa di analogo a una cache direct mapping).
Nonostante questo fatto sembri complicare il funzionamento del meccanismo di previsione non c' alcun accorgimento che
verifichi se la condizione veramente relativa al salto che stiamo effettuando. Questo permetter di utilizzare comunque
l'informazione nel BPB anche la prima volta che incontreremo un'istruzione di salto.
Se volessimo fare questa verifica (controllare che l'informazione sia proprio relativa all'istruzione considerata) occorrerebbe
una vera e propria cache, in cui la RAM Tag contenga i bit pi significativi dell'indirizzo in modo da effettuare il confronto
con i bit provenienti dal PC. Questa soluzione non viene adottata sia per motivi di costo sia perch in realt non cos
vantaggiosa come sembra. Infatti, pur non avendo l'informazione effettivamente relativa al salto in questione, l'informazione
relativa a un salto qualsiasi ci d una probabilit del 50% di decidere correttamente.
Se non utilizzassimo alcuna informazione dovremmo sempre aspettare la decodifica della condizione di salto mettendo delle
bolle; utilizzando l'informazione, invece, il 50% delle volte decidiamo correttamente (risparmiando quindi delle bolle).
Valutiamo le prestazioni e calcoliamo la probabilit di effettuare una previsione corretta.
Supponiamo che la probabilit di trovare informazioni sul salto sia 0.9 (cio nel 90% dei casi l'informazione relativa al
salto considerato) e che la probabilit di previsione corretta sia 0.9. Nel caso in cui l'informazione non sia relativa al salto
considerato, abbiamo il 50% di possibilit di previsione giusta. Allora la probabilit di effettuare previsione corretta vale:
Se vogliamo lavorare su tutto l'indirizzo usiamo una cache che abbia nella RAM Dati due informazioni:
1. nuovo PC
2. salto effettuato o meno (utile per abbreviare la verifica se l'istruzione o meno da buttare)
60 of 138
Alla BTC accediamo all'inizio del fetch di ogni istruzione per anticipare l'istruzione stessa. Il valore del PC, usato per fare il
fetch, viene usato anche per la cache, in modo da avere pronto il nuovo valore di PC al fetch dell'istruzione successiva.
Il funzionamento logico tramite BTC pu essere schematizzato come segue
Se si verifica un miss in cache, vuol dire che non abbiamo ancora incontrato un'istruzione di salto con quell'indirizzo.
Quindi aggiorniamo PC con PC+1, andiamo a prelevare l'istruzione e verifichiamo se o meno un salto. Se non lo , si
segue il programma, se lo si deve aggiornare la cache calcolando le informazioni in fase di ID.
Sia n l'istruzione in corso, abbiamo un miss in cache se non un'istruzione di salto oppure se lo ma la incontriamo per la
prima volta. In quest'ultimo caso, aggiorniamo la RAM Dati con NPC e condizione di salto e la RAM Tag con i bit pi
significativi dell'indirizzo in PC.Se il salto va effettuato, oltre ad aggiornare la cache, dobbiamo scartare l'istruzione
successiva, di indirizzo PC+1, che sbagliata perch dovevamo saltare. Se non effettuiamo il salto bisogna solo aggiornare
la cache e seguire il programma, per cui l'istruzione PC+1 va bene.In caso di miss in cache le penalit valgono: 0 se
l'istruzione successiva non un salto, 1 se salto ma non va effettuato (per cui si deve aggiornare solo la cache), 2 se salto
e va effettuato (un ciclo di clock per aggiornare la cache e uno per buttare via l'istruzione).In caso di hit in cache le penalit
valgono: 0 se la previsione corretta, 2 se la previsione sbagliata (bisogna aggiornare la cache e scartare
un'istruzione).Volendo riassumere in una tabella:
Il bit di informazione sul salto serve per verificare in fretta la correttezza della previsione. Se non ci fosse dovremmo
confrontare i PC, che un lavoro pi oneroso.
Valutiamo il ritardo medio che consegue a un salto, supponendo che la probabilit di hit sia 0.9, la probabilit di previsione
errata sia 0.1 e che i salti siano effettuati al 70%.
61 of 138
0.9 x 0.1 x 2 + 0.1 x (0.7 x 2 + 0.3 x 1) = 0.18 + 0.17 = 0.35
Perdiamo 0.35 cicli di clock con la BTC ed meglio di quanto facciamo con il salto ritardato (dove arriviamo a 0.5).
L'algoritmo della BTC migliora se cancelliamo il caso di miss e salto non effettuato (in tale situazione non serve aggiornare
la cache perch inutile trovare l'hit, in quanto PC va aggiornato comunque con PC+1). In tal modo la penalit media
diminuisce:
Per migliorare ancora occorre ridurre i valori delle penalit. Questo si fa prendendo contemporaneamente un'istruzione da
NPC di BTC e un'istruzione da PC+1.
Ci si pu fare solo avendo una macchina capace di effettuare un doppio fetch, cio di prendere due istruzioni a due
indirizzi diversi nella fase di fetch. In tal caso, riduciamo a 1 le penalit nel caso di hit perch aggiorniamo solo la cache
(non dobbiamo scartare alcun'istruzione perch abbiamo gi quella giusta). La penalit media quindi scende a:
La tecnica del doppio fetch usata dagli ultimi modelli di SPARC (processori usati da SUN); ad esempio, nella SPARC 10.
Tra LD e ADD c' una bolla perch il dato dalla memoria arriva con ritardo (data hazard non superabile). Tra ADD e ST ci
sono due bolle perch l'addizione floating point, cio eseguita in 3 cicli di clock. Dopo BNEZ c' un'altra bolla, quindi
abbiamo in tutto 9 cicli di clock per ogni elemento del vettore (cinque istruzioni in pipeline e quattro bolle).
Un primo miglioramento si ha riordinando la sequenza:
In tal modo sembra che SUB alteri il dato per ST; occorre allora un compilatore che esegua ST 8(R1),F4 , cio cambi
l'indirizzo a cui fare lo store. Spesso i compilatori spostano le istruzioni solo se non ci sono legami con le successive e in tal
caso non si sarebbero potute togliere le due bolle perch non avremmo potuto spostare SUB.
Supponendo di avere un compilatore intelligente scendiamo, quindi, a 6 cicli di clock.
Quelli che effettivamente usiamo sono 3 perch SUB e BNEZ sono istruzioni di solo controllo. Il compilatore pu allora
fare un unrolling del loop ripetendo pi volte le istruzioni utili e una sola volta quelle di controllo.
Ad esempio, un unroll di 4 loop produce:
62 of 138
Ho ovviamente bisogno di pi registri (perch quelli utilizzati prima non sono disponibili) e il compilatore deve essere
intelligente (perch deve gestire tutti gli indirizzamenti e la nuova costante in SUB).
Quanti cicli di clock servono? Con questa sequenza ho 27 cicli di clock, cio 6.75 cicli per elemento del vettore.
Ottimizzando la sequenza di istruzioni le prestazioni migliorano ancora: 14 cicli di clock (cio 3.5 cicli per elemento).
Da un punto di vista delle prestazioni le cose sono cambiate parecchio: abbiamo guadagnato quasi un fattore 3. Anche se il
programma occupa pi memoria e occorrono pi registri , il miglioramento tale che la tecnica conviene.
Considerazioni:
1. l'ottimizzazione ha reso di pi dopo aver fatto l'unrolling, perch stata fatta su un numero di istruzioni pi alto.
Allora l'unroling ha 2 vantaggi: - togliere le istruzioni non utili (di controllo) - permettere all'ottimizzazione
compattazioni migliori
2. Non sar sempre possibile fare un unrolling unico dei loop che si incontrano. Ad esempio, se dobbiamo fare 17 volte
un loop, non ha senso fare l'unrolling su tutte e 17 le ripetizioni perch avremmo un miglioramento minimo.
L'unrolling viene fermato quando abbiamo eliminato tutte le bolle; andare oltre significherebbe solo spalmare le istruzioni
non ripetute (di controllo) su un numero pi elevato di istruzioni utili che comporta un cambiamento minimo.Ad esempio,
gi passare da 4 a 8 loop nella nostra sequenza fa scendere a 3.25 cicli di clock per elemento, che non pi conveniente
rispetto agli svantaggi apportati.Quindi, esiste un limite al miglioramento apportato dall'unrolling.Se il loop lungo, lo si
divide un due parti:
63 of 138
Ad esempio, se ho 17 loop, considero una sequenza per gestire un elemento e 4 sequenze per gestire 4 elementi in un
unrolling.
In generale, con un loop di n volte e un unrolling di k ho:
Lo scopo dell'unrolling di fornire una sequenza piuttosto lunga di istruzioni senza salto, risparmiando cos un certo
numero di bolle.
Macchine Superscalari
Le macchine superscalari sono quelle che portano a termine pi di un'istruzione in ogni ciclo di clock. Se le istruzioni del
processore sono a 32 bit, per eseguirne due contemporaneamente necessario raddoppiare il numero di bit prelevati dalla
cache:
Da quanto detto, si comprende come anche l'instruction register debba avere dimensione doppia: esso deve accogliere due
istruzioni da inviare alle altre parti della macchina
L'architettura di cui abbiamo accennato la descrizione considerevolmente pi complessa di quella studiata finora. In
particolare, ha come conseguenza un massiccio incremento di hazard strutturali (perch le istruzioni contemporanee
potrebbero richiedere le stesse unit),
hazard sui dati (perch non possibile effettuare il bypass di un'istruzione contemporanea a un'altra) e hazard sui controlli
(perch se una delle due istruzioni contemporanee un salto, devo saltare sia quella che viene eseguita in parallelo che
quelle che seguono).
Per ridurre le situazioni di conflitto, si possono imporre dei vincoli al parallelismo delle istruzioni. Un primo esempio di
vincolo potrebbe essere il permettere l'esecuzione in parallelo solo se le due istruzioni sono una intera e l'altra FP. Cos
facendo riduciamo la possibilit di hazard strutturale e hazard sui dati all'unico caso di un'istruzione intera che cerchi di
accedere ai registri FP.
In queste ipotesi, la macchina funziona in modo completamente superscalare solo se riusciamo ad accoppiare sempre
istruzioni operanti su dati di tipo diverso, altrimenti funziona come una macchina normale (al massimo un'operazione per
ciclo di clock). Appare ovvio che una soluzione come quella appena descritta accettabile solo per programmi che
utilizzino intensamente operazioni FP.
Programmazione concorrente
- Eventi e Segnali
- Introduzione
64 of 138
Glossario:
- Evento o Signal
Un evento viene:
- generato (lanciato)
- propagato (da chi e come)
- trattato (gestione dell'evento)
ed ha:
- un effetto
65 of 138
- un "evento H/W" (ad esempio un tasto di interruzione come ^c)
- Trattare un evento
All'interno di un processo avr una o pi regioni di codice che vengono attivate da un certo evento:
P::
{
S1 ! EVENTO E2
S2
...
}
/* Regione di EVENT HANDLING */
when E1 --> collezione di
{ procedure o funzioni
...
}
when E2 -->
{
...
} ritorno al chiamante
...
Trattare un evento E all'interno di un processo P vuol dire prevedere una procedura ("Event Handler"), separata dal codice
"principale" di P, la quale verr attivata asincronamente all'occorrenza di E, con il ritorno al punto del codice di P
immediatamente successivo all'istruzione "sulla quale" si era verificato E. Gli Eventi sono propagati molto velocemente dal
Kernel e possono avere il significato di "ECCEZIONI" (EXCEPTION).
Gli eventi possono essere trattati anche non come "eccezioni", si pu cio pensare ad una "EVENT DRIVEN
66 of 138
PROGRAMMING": storicamente nacque intorno agli anni '70, '80 per risolvere problemi legati alla grafica e alla gestione
di finestre, mouse, multimedialit, ecc.
ECCEZIONI
P::
{
20% codice
e1;
e2; 80% device, timer, eccezioni, ecc.
e3;
...
en
}
Un altro uso degli eventi , infine, quello di riattivare un Processo in deadlock: sospeso (esplicitamente o in attesa
messaggio o RPC) e nessuno stato computazionale dell'algoritmo concorrente pu riattivarlo.
Introduzione:
Programmazione concorrente
Def. Programma concorrente: un programma concorrente composto da n programmi sequenziali ("processi") che vengono
eseguiti con velocit vi (i = 1N), questi:
67 of 138
ecc.).
Def. Programma Real-Time: la computazione dipende (sia come velocit, sia come risultati) da valori e/o eventi esterni. Un
programma concorrente Real-Time ha un'esecuzione che dipende dalle sequenze di eventi/dati esterni. Segue due regole
fondamentali:
Negli anni 1965/68 Dijkstra (Daistra) diede vita al primo esempio di Sistema Operativo basato su primitive di concorrenza
Def. Semaforo: primitiva per gestire e risolvere i problemi tipici di un programma concorrente; esso comprende i concetti
di:
i. mutua esclusione
ii. regione critica
iii. variabile condivisa ("shared variable")
iv. sincronizzazione (punto di vista temporale)
Come esplicato in figura la regione critica una parte di codice che non ammette la condivisione da parte di pi di un
processo, mentre gli accessi alla variabile condivisa sono gestiti con la mutua esclusione: uso un SEMAFORO (dato
condiviso elementare + primitiva di sincronizzazione).
La mutua esclusione e la sincronizzazione sono concetti che riguardano l'invio di eventi temporali (eventi interprocesso)
mentre la regione critica e la variabile condivisa riguardano lo scambio di dati (comunicazioni).
Semafori
La primitiva "semaforo" nasce per risolvere i problemi di sincronizzazione e mutua esclusione, una variabile intera sulla
quale sono state definite 3 operazioni:
S : = initial value
68 of 138
IF queue (coda) empty THEN s : = s+1
Le operazioni di "wait" e "signal" sono indivisibili e non c' possibilit che due processi incrementino o decrementino un
semaforo contemporaneamente. Un'importante propriet l'invarianza dei semafori, cio se s un semaforo si ha:
Con evidenza value(s) non potr mai essere negativo: iv(s) + ns(s) - nw(s) >= 0
La mutua esclusione pu essere realizzata usando un semaforo s inizializzato a 1 e individuando la regione critica tra le due
operazioni di signal (s) e wait (s):
wait (s)
critical region
signal (s)
P(a)
S1
S2
. [Regione critica]
.
Sn
V(a)
Il numero di processi nella regione critica uguale al numero di processi che hanno eseguito l'operazione wait (s) senza
eseguire il corrispondente signal (s).
Dall'invarianza del semaforo si pu immediatamente vedere che nw(s) - ns(s) >= iv(s) e finch iv(s) = 1 ci comporta che
"Il numero dei processi nelle sezioni critiche <= 1" - definizione di mutua esclusione.
[P(a)]
S1
S2
.
P(b)
.
Sn
[V(a)]
Supponiamo di avere un "set" di processi che comunicano tramite un buffer condiviso di N locazioni (come in un sistema
monoprocessore con "job scheduler" e coda gestita con algoritmo FIFO).
Ogni sistema che inserisce dati nel buffer chiamato produttore, ogni sistema che rimuove i dati dal buffer chiamato
69 of 138
consumatore.
Possono essere definite due regole per rendere la comunucazione tra i due processi soddisfacente:
1. il numero di dati inseriti nel buffer e non rimossi deve essere >= 0
2. il numero di dati inseriti nel buffer e non rimossi deve essere <= N
Producer Consumer
REPEAT REPEAT
Produce item; wait (c);
wait (p); take item from buffer;
place item in buffer; signal (p);
signal (c); process item;
FOREVER FOREVER
Come nel precedente esempio, per quanto riguarda i processi in attesa, assumiamo che ci sia una coda di tipo FIFO senza
priorit (Scheduler non Preemptive o Preemptive con coda FIFO).
Il problema tipico basti pensare ad un sistema di prenotazione dei posti per aerei, treni
70 of 138
a) "Priorit" dei lettori
Scrittore
wait (w)
signal (w)
Lettore
E' necessaria una variabile (condivisa) readcount (inizializzata a 0) che memorizzi costantemente il numero di processi in
lettura. Il primo lettore setta il semaforo w per fermare gli scrittori, mentre l'ultimo lo sblocca per dare libero acceso alla
scrittura.
wait (x)
readcount : = readcount + 1
IF readcount = 1 THEN wait (w) [Regione Critica]
signal (x)
wait (x)
readcount : = readcount - 1
IF readcount = 0 THEN signal (w) [Regione Critica]
signal (x)
La variabile readcount accessibile da tutti i lettori simultaneamente, pu essere "protetta" includendola in una regione
critica controllata dal semaforo x.
Inconveniente:
Con la soluzione appena presentata finch ci sono lettori che occupano la risorsa gli scrittori non possono entrare: in Tempo
Reale non risponde in tempi finiti agli scrittori (violazione di "Fairness")
ogni Reader che chiede di entrare accettato, eccetto se un Writer chiede di entrare: in questo caso viene inibito
l'ingresso di Readers successivi;
anche un solo Reader inibisce i Writers;
il primo Writers che entra inibisce ogni altro R/W.
71 of 138
un semaforo r che blocca tutti i lettori mentre gli scrittori stanno accedendo alla risorsa
una variabile writecount che controlla il semaforo r
un semaforo y che controlla l'accesso alla variabile writecount (mutua esclusione sulla regione critica)
Scrittore
wait (y)
writecount : = writecount + 1
IF writecount = 1 THEN wait (r) [Regione Critica (stop readers)]
signal (y)
wait (w)
signal (w)
wait (y)
writecount : = writecount - 1
IF writecount = 0 THEN signal (r) [Regione Critica (Free readers)]
signal (y)
Lettori
Dobbiamo soddisfare alle seguenti specifiche: deve essere garantito l'accesso multiplo in lettura, quindi ancora necessaria
la variabile readcount; il semaforo r deve essere settato prima del semaforo w per evitare situazioni di deadlock inoltre,
prima della sequenza di lettura, ci deve essere un "signal" sul semaforo r .
Per evitare una palese violazione di Fairness necessario che su r non ci sia una coda troppo lunga (lettori) altrimenti gli
scrittori non sarebbero in grado di superarla, quindi, sfruttando un semaforo addizionale z subito prima del "wait" su r ,viene
concesso ad un solo lettore di "accodarsi" sul semaforo (r).
wait (x)
readcount : = readcount + 1
IF readcount = 1 THEN wait (w) [ho tre regioni critiche
annidate]
signal (x)
signal (r)
signal (z)
read the file
wait (x)
readcount : = readcount - 1
IF readcount = 0 THEN signal (w)
signal (x)
72 of 138
i. solo lettori nel sistema: w fermo, nessuna coda;
ii. solo scrittori nel sistema: w e r fermi, gli scrittori in coda su w;
iii. lettori e scrittori insieme nel sistema con un lettore per primo: w fermato dal lettore ed r fermato dallo scrittore
quindi tutti gli scrittori sono in coda su w, 1 lettore in coda su r e tutti gli altri lettori sono in coda su z;
iv. lettori e scrittori insieme nel sistema con uno scrittore per primo:w ed r fermati dallo scrittore quindi tutti gli scrittori
sono in coda su w, 1 lettore in coda su r e tutti gli altri lettori sono in coda su z;
Quindi se per primo un lettore occupa il sistema blocca gli scrittori, mentre se il primo uno scrittore blocca i lettori.
a) Finite progress: tutti i processi devono avere una durata finita per evitare deadlochs e fairness.
NOTA: in programmazione concorrente un processo sempre ciclico.
Ogni processo legato ad una sua propria velocit, ma globalmente
il programma evolver con la velocit del processo pi lento.
i) Scheduling implicito
ii) Scheduling esplicito
i) derivante dalla semantica delle primitiva (cio se ho un semaforo devo supporre che vi siano n processi che si bloccano in
attesa di accedere alla risorsa condivisa)
ii) ho una coda di processi in ingresso alla risorsa condivisa (si vedano gli esempi precedenti).
c) Deadlocks: stalli, abbracci mortali. Sono, al contrario del Fairness, veri e propri blocchi logici:
errori di programmazione che portano all'interruzione completa del programma concorrente.
d) Busy waiting: durante i periodi di attesa tengo occupata la CPU con un ciclo di controllo o con un test attivo sul
verificarsi di una certa condizione. E' un modo per realizzare la sincronizzazione tra processi: logicamente corretto, ma
produce un decadimento delle prestazioni ed quindi da evitare (usato nei sistemi a Polling).
- Monitor
i) regioni critiche
ii) sincronizzazione
Il Monitor contribuisce a migliorare la "localit" del programma (l'effetto di un'istruzione si ritrova in una zona di codice il
pi possibile vicina), si basa sull'idea di avere un'area di memoria comune tra i due processi [dati in comune].
Vediamo alcune definizioni legate al Monitor:
73 of 138
a) Processo: programma, algoritmo sequenziale ciclico.
b) Sincronizzazione: attesa di un "evento" per proseguire l'esecuzione [sequenziale] o generazione di tale evento.
Nota: secondo questa visione anche l'interrupt un evento di sincronizzazione.
c) H/W periferico: concettualmente un processo che genera eventi [e dati].
Con il Monitor entriamo quindi nel merito della Comunicazione tra processi: scambio di dati.
Vediamo le definizioni:
- regioni critiche (semafori): primitiva di sincronizzazione usata per proteggere dati condivisi;
- monitor: dato condiviso definito come tipo astratto e protezioni "automatiche" (non erano tali nei semafori;
- messaggi: nessun dato condiviso, trasferimento per copiatura (automatico) fra processi;
- memoria condivisa: metodo "libero" (molti S.O. non hanno sistemi di protezione automatica della memoria) da proteggere
(pi processi hanno uno stesso dato in memoria condiviso in comune);
- RPC: messaggi e memoria condivisa.
- Variabile statica: variabili alle quali viene dedicata una specifica allocazione di memoria dal momento del caricamento
fino alla fine del processo (in linguaggio PASCAL sono le variabili globali).
- Variabile dinamica: variabili locali (in linguaggio PASCAL sono quelle dichiarate all'interno di funzioni e procedure),
sono allocate in memoria solo durante l'esecuzione di funzioni e procedure. Anche le variabili puntatore appartengono a
questa categoria.
- Tipo di dato:definisce un insieme di valori che il dato pu assumere e l'elenco delle primitive con le quali si pu accedere
ad esso (con la loro semantica).
- un dominio
- un insieme di operazioni possibili su tale tipo di dato
- Overload: uno stesso nome di funzione, dentro un programma, pu avere diversi significati. Ad esempio il "+" tra due char
pu essere diverso dal "+" tra due integer; incarico del compilatore sapere quale operazione eseguire.
Esempio 1:
74 of 138
Dove:
P = Processi
M = Monitor (un solo processo alla volta pu eseguire una procedura dentro un monitor)
var
{Struttura del Monitor}
slots: array [O..N-1] of T;
head, tail: O..N-I;
mname: monitor;
size: O..N;
notfull, notempty: condition;
var declarations of permanent variables;
procedure deposit(p: T);
begin
procedure opl(parameters);
if size = N then notfull.wait; [coda]
var declaretions of variables local to opl;
slots [tail] := p;
begin
size := size + 1;
code to implement opl
tail:=(tail+1) mod N;
end;
notempty.signal
end;
...
procedure fetch(var it: T);
procedure opN(parameters);
begin
var declaretions of variables local to opN;
if size = 0 then notempty.wait;
begin
it := slots [head];
code to implement opN
size := size-1;
end;
head := (head + 1) mod N;
notfull.signal
begin
end;
code to initialize permanent variables
end
begin
size := 0; head := 0; tail := 0
end
Assioma 1: un solo processo alla volta pu eseguire una qualsiasi procedura all'interno di un monitor (se il monitor
occupato il processo viene messo in una coda "fair" [ad esempio di tipo FIFO]).
75 of 138
Coda a breve termine (short term) fornita dal sistema: MUTUA ESCLUSIONE (garantisce la regione critica);
AUTOMATICA, non controllabile, [forse] non leggibile.
Coda a lungo termine (long term) gestita dall'utente: SINCRONIZZAZIONE gestita dal programma mediante opportune
primitive.
Primitiva minima:
queue q;
delay(q)
continue(q)
- priorit;
- politica (Round Robin, Time shering, FIFO, preemptive, non preemptive, ...);
- parametri.
Assioma 2
Assioma 3: se un processo P esegue "continue (g)", il processo PG, precedentemente sospeso su "g" pu essere riattivato:
- poich PG si era sospeso dentro lo stesso monitor, PG pu ripartire quando P lascia il monitor
o si sospende;
76 of 138
Esempio analitico:
monitor M
queue q1, q2, q3 ;
procedura a(...)
delay (q1);
procedura b(...)
delay (q2);
procedura c(...)
delay (q3); P1 richiesta in coda
P2 richiesta in coda
....
monitor M {
dato:...;
coda:...; [dichiarazioni]
f1 ( ){
... }
f2 ( ){
... } [funzioni]
{init code} [codice]
... }
Assioma 4: Non definito il significato di "delay(q)" se era gi stata effettuata una "delay(q)" precedente senza una
corrispondente "continue(q)".
77 of 138
continue(RECEIVER);
end;
begin
INPTR := 1;
OUTPTR := 1;
SIZE := 0
end;
- ogni Pi ciclico;
- la sua velocit ignota, ma mediamente > 0 in tempo infinito;
- Messaggi
Servono a "portare a zero" il codice delle procedure del Monitor (rendo "standard" l'accesso al Monitor).
COMUNICAZIONE INTERPROCESSO
78 of 138
E' evidente, per che, con l'uso dei messaggi, perdo la sincronizzazione del Monitor; si definiscono quindi:
P1
... P2
dato x ...
... ...
write (x,P2) read (y,P1) (+)
(*) ...
... ...
- Assioma 1: il primo processo Pi che esegue una read (x,Pj) [write (x,Pj)] si arresta fino a completa esecuzione da parte di Pj
della prima write (y,Pi) [ read (y,Pi) ].
- Assioma 2: lo scambio dati (il "messaggio") avviene con o senza buffer FIFO trasparente ai processi (fornito dal Kernel).
P1::
...
dato (x)
...
write (x,P2)
...{il processo continua ignorando cosa sia accaduto al dato x, per sapere se P2 ha ricevuto x
introduco una routine booleana}
...
IF complete_com (P2)
...
79 of 138
ELSE
... {busy waiting}
wait complete_com (P2) {scheduling a lungo termine}
P1::
{
...
dato (x)
...
write (x, P2, CR) CR: interrupt software o "trap" (evento puro, non dato)
...
}
CR ( )
{
...
return
} [SIGNAL HANDLER (in generale)]
IL "trap":
Pi in generale, gli Eventi/Signal/Trap possono essere generati da primitive esplicite (messaggio senza contenuto).
P1::
...
trap (ev1)
...
trap (ev2,P2)
...
Spesso in sistemi con Eventi/Signal/Trap, un trap non gestito provoca la terminazione del Processo.
In programmazione Real Time bisogna gestire tutti gli eventi.
Ritorniamo ora al messaggio sincrono: evidente che tale tipo di messaggio insufficiente a gestire la sincronizzazione di
pi processi. Vediamo un esempio:
80 of 138
P4::
while True {
read (x, P1)
...
read (y, P2)
...
read (z, P3)
}
In questo modo, per, vado alla velocit del processo pi lento: per eliminare il problema introduco una nuova primitiva:
- GUARDIE, COMANDI (di comunicazione) ALTERNATIVI.
Agisco in maniera non deterministica: controllare l'esistenza di dati da scambiare prima di effettuare la read [write].
Sintassi:
"read" di x da P1: P1 ? x
"write" di x a P2: P2 ! x
*[
"loop infinito": ...
]
"alternativa" []
"guardia" (condizione
-->
logica)
Esempio:
P::
*[ west ? c
east ! c
]
Il programma esegue in "loop infinito" la lettura di c da parte di west e la scrittura da parte di east. I messaggi sono sincroni
o bloccanti e simmetrici: il sender invia il messaggio M al receiver; il receiver riceve il messaggio M dal sender, questo
concetto scorrelato dal tipo di sincronizzazione dei messaggi (comunicazione bloccante/non bloccante).
81 of 138
- Guardie e alternative:
[
x >= y --> m = x
[]
y >= x --> m = y
]
L'alternativa svolge la funzione di "IF PARALLELO", una sorta di "CASE" o "SWITCH", in questo caso, per, le
alternative sono esplorate in "parallelo" ed eseguite in maniera non deterministica. Infatti pi guardie (condizioni logiche)
possono essere vere contemporaneamente (non c' un ordine predefinito per valutare le guardie): ne viene eseguita una con
FAIRNESS.
Fairness: nessuna guardia "vera" (open) pu essere "ritardata" indefinitamente rispetto ad altre nel comando ripetuto *[...].
Esempio:
82 of 138
*[ g1 --> {sequenza 1}
[]
g2 --> {sequenza 2}
[]
...
gn --> {sequenza n}
]
Almeno una guardia, in un'alternativa, deve essere vera (a meno di I/O nelle guardie).
Aggiungo questa condizione per:
In alternativa un'altra strada (non CSP) poteva essere quella di usare dei TIMER: rischedulo il processo con tempi fissi.
z::
*[
x ? P --> ...
[]
(y ? q) ^ (p > q) --> ... //AND
[]
...
] //guardie con messaggi
Nel caso di guardie con messaggi queste ultime possono essere tutte false, ma almeno in una di esse deve esserci un
messaggio ("?", "!"), un'attesa di comunicazione veri. In questo caso siamo sicuri di non avere busy waiting (basta un
modesto scheduler).
83 of 138
la terminazione di x provoca la terminazione di P (se no avrei un potenziale deadlock).
Se vi sono delle condizioni per cui un processo P dipende da un processo Q per poter ripartire [Q manda un messaggio a P],
l'arresto di Q deve determinare l'arresto di P.
Esempio
Caso generale:
P::
*[
Q ? comando ; Q ? dato // ; ordine stretto senza alternative
[]
Q ? f1(dato)
[]
Q ? f2(dato)
[]
...
]
Caso particolare:
P::
*[
x ? insert(n) --> INSERT // inserire un dato
[]
x ? has(n) --> [SEARCH ; x ! i] // ritrovare un dato
]
x::
[
P ? insert(121)
...
P ? has(13) ; P ? k
]
- Protocollo
Regole di sincronizzazione fra due processi che si scambiano dati: garantiscono l'efficacia della comunicazione e il
trattamento sicuro di tutti gli errori possibili (non le correzioni).
Esaminiamo come esempio il problema Produttore-Consumatore:
84 of 138
x::
T buffer (10); int in = 0; int out = 0;
*[
(in < out + 10) ^ PROD ? buffer (in mod 10) --> in++
[]
(out < in) ^ CONS ? more ( ) --> CONS ! buffer (out mod 10) ; out++
]
PROD:: CONS::
... ...
x ! dato x ! more ( );
... x ? dato
Produttore e consumatore comunicano parallelamente, se arriva un comando da PROD x riempe il buffer: la guardia
consiste nel fatto che sia per il Consumatore, sia per il Produttore un comando e inaccessibile se le condizioni non sono
vere.
Non vi uno schedulig esplicito, l'unico meccanismo l'attesa dell'esito della comunicazione.
Nel monitor era usato uno schedulig esplicito (Delay, Continue), qui l'effetto realizzato ritardando il completamento della
comunicazione tramite l'uso di guardie. Anche in questo caso, comunque, sono possibili i deadlock.
Vediamo un esempio: problema di scheduling di risorse (scheduler).
Supponiamo che vi siano 5 filosofi che devono accedere in una stanza nella quale vi una risorsa condivisa (spaghetti
infiniti!)
Ogni filosofo ha bisogno di 2 forchette per mangiare: nasce un evidente problema di gestione della risorsa condivisa.
85 of 138
Ciclo di vita del filosofo:
loop ::
{
pensa;
entra;
siede;
mangia; /* con 2 forchette*/
si alza;
esce;
}
Specifiche "tipo":
86 of 138
Vediamo in dettaglio i processi:
PHIL (i) ::
*[
THINK; .........
room!enter( ); phil (i - 1): fork (i - 1) & fork (i);
fork(i)!pickup( ); fork((i + 1) mod 5)!pickup( ); phil (i): fork (i) & fork (i + 1);
EAT; phil (i + 1): fork (i +1) & fork (i +2)
fork(i)!putdown( ); fork((i + 1) mod 5)!putdown( ); .........
room!exit
]
ROOM ::
int n=0;
*[
(i: 0..4) phil (i)?enter( ) --> n++
[]
(i: 0..4) phil (i)?exit( ) --> n--
]
FORK (i) ::
*[
phil (i)?pickup( ) --> phil (i)?putdown( );
[]
phil (i - 1 mod 5)?pickup( )
--> phil (i - 1 mod 5)?putdown( );
]
87 of 138
T1::
{
...
T2 . p(z) Remote Procedure Call
... accede a x tramite p( )
} T1 si blocca se T2 non accetta la p( )
T3::
{
...
T2 . p(x) Remote Procedure Call
... accede a x tramite p( )
}
T2::
x: dato
...
accept p(k) Remote Procedure
l'esecuzione di p( ) mutuamente
{
esclusiva.
... x disponibile a p( ) Assumiamo che gli spazi di indirizzamento
} siano limitati ai Ti
...
p ( ) pu essere chiamata quando T2 esegue l'accept, T2 si blocca finch non riceve un'altra chiamata (accept non
l'equivalente della "proc entry" del Monitor). Cos facendo definisco la sincronizzazione dei processi ("Rendez-Vous").
Vediamo la struttura della RPC:
- Sincronizzazione
- i task che eseguono "accept" sono sospesi in attesa di chiamata alla propria
I/O)
- Estensioni
88 of 138
- controllo lunghezza code
- rimaneggiamenti code
- ...
Non determinismo:
Si pu dire che: "il chiamante esegue un pezzo di codice del chiamato, ovvero T1 esegue un pezzo del codice di T2".
Vediamo alcuni esempi classici realizzati con RPC:
- Esempio 1
task BUFFER
INT len = 10;
DATA buffer [10];
INT in_ptr, out_ptr = 1;
INT size = 0;
{
loop{
select
when size < len --> GUARDIA
accept put (x) { alternativa non deterministica
buffer [in_ptr] = x;
}
in_ptr = in_ptr + 1 MOD len;
size++
or come nel CSP: ha il significato della []
when size > 0 --> GUARDIA
accept get (x){ alternativa non deterministica
x = buffer [out_ptr];
}
out_ptr = out_ptr + 1 MOD len;
size--
end select
}
}
Ad esempio:
CLIENT TC::
...
buffer . put (752)
...
89 of 138
messaggi dove, per, si incontravano altri problemi quali la rigidit della struttura).
Bisogna inoltre notare che il buffer non limitato ad 1 produttore ed 1 consumatore ma, grazie alla asimmetria della RPC,
viene formata una coda (tipicamente FIFO) sulla accept.
Ad ogni "punto di accettazione" (accept) esiste una coda per tutti i task che effettuano la chiamata quando quest'ultima non
disponibile (tipicamente guardia "chiusa").
- Esempio 2
Problema lettori-scrittori
task M_R_W
ELEM variable; dato condiviso
INT readers = 0;
{
procedure read(v) Disponibilit di:
{ PROCEDURE RIENTRANTI
M_R_W . startread( ); condivisibili senza protezione
v = variable;
M_R_W . stopread( );
}
/* MAIN */
{
accept write(e) variable = e
loop
select
accept startread( )
readers++
or ALTERNATIVE
accept stopread( )
readers--
or
when readers = 0 -->
accept write(e) variable = e
end select
}
}
Questa soluzione (priorit dei lettori), come visto nei Semafori, non FAIR; cio il task M_R_W pu provocare starvation
(non deadlock). Starvation: ritardo indefinito di un processo dovuto ad una configurazione particolare di velocit di altri
processi.
- Lettore
task R1
elem v;
...
M_R_W . read (v) Procedura non protetta
...
- Scrittore
90 of 138
task Wn
elem v;
...
v = 777;
M_R_W . write (v) Remote Procedure Call
...
loop
select
when <coda write> = = 0 --> <coda write>: lunghezaza coda entry write
accept start . read ( ) = = 0: posso avere altre condizioni, per es. <= k
readers ++
or
accept stop . read ( )
readers --
or
when readers = = 0 -->
accept write (e) {variable = e}
loop evita un'esagerata priorit degli scrittori
select
accept start . read ( )
readers ++
else exit
}
}
- mutua esclusione
- assenza di deadlock
91 of 138
Ad esempio posso controllare le code su write( ) e start.read( ).
Un modo corretto che realizza la serializzazione fra Readers e Writers alternandoli modificare l'algoritmo dell'esempio 2
in questo modo:
...
when readers = = 0 -->
accept write (e) {variable = e}
select
accept start.read ( )
readers + +
else exit
end select
}
Dopo ogni Writer se c' un Reader in coda sulla start.read ( ), questa viene eseguita una volta; poi, se ci sono altri writers,
essendo la coda write > 0, viene eseguita un'altra write ( ).
Questo si basa su:
- Schedulazione Real-Time
Noi ci occupiamo di programmazione Real Time ad "alto livello", cio seguendo le seguenti specifiche:
92 of 138
- scheduler (CPU come risorsa)
- gestione risorse (memoria)
Programmazione a "basso livello" (livello
- I/O
Kernel)
- file system
- networking
Risulta chiaro che con una programmazione ad "alto livello" mi affido spesso ai servizi del Kernel specialmente per quanto
riguarda la gestione della memoria, pensiamo a pi processi paralleli:
lo scheduler del Kernel virtualizza il parallelismo come il Kernel virtualizza la memoria.
Il fallimento dipende dal tempo di scheduling dei singoli processi (task) che rischia di essere incompatibile con la politica di
scheduling del Kernel. In questo caso non posso mantenermi in una programmazione ad "alto livello", ma devo per forza
cambiare la politica di schedulazione del Kernel.
- Correttivi:
- Memory lock
- Priorit (per es. Round Robin)
- Scheduling esplicito pre-emptive
Illustriamo i concetti attraverso un esempio elementare di scheduling (si veda a questo proposito anche: Monitor, esempio
1):
Pensiamo ad un sistema di acquisizione ed elaborazione dati:
- CICLICA: con un'operazione di monitoraggio prelevo ciclicamente i dati e, se sono validi, li elaboro.
93 of 138
In un programma concorrente posso avere la necessit di controllare l'esecuzione dei singoli processi indipendentemente da
problemi legati alla condivisione di risorse e/o comunicazione. Per ogni processo (Task) bisogna quindi conoscere:
- tempo d'inizio
- tempo di arresto
- periodo {informazioni minime}
- eventuale uso di (modeste) risorse comuni
94 of 138
1- I Pi
2- Il clock va periodicamente a verificare sulla tabella oraria se il momento di riattivare un processo fermo sulla coda dei
task.
3- Il clock, tramite la procedura all'interno di time table, riattiva il processo opportuno.
Process P::
}
while TRUE {
< cod. rescheduling>
switch x
case 1:
<cod. T1>
case 2:
<cod. T2>
...
}
}
Il TASK PROCESS dell'esempio in figura ha una struttura simile a quella appena rappresentata: molto spesso proprio
questa la soluzione migliore, infatti la gestione dei tasks da parte di un processo principale semplifica la schedulazione e
95 of 138
diminuisce la possibilit di deadlock, starvation ...
Task Process e Operator Process vogliono, asincronamente, impadronirsi del terminale e scrivere messaggi; analizziamo i
due casi:
- CASO A
Process TP::
...
{
risorsa.request ( ) "P" zona sicura:
apri.finestra ( ) garantisce l'accesso esclusivo
scrivi.messaggio ("...") alla risorsa
...
risorsa.release ( ) "V"
...
}
Monitor risorsa
proc: BOOL
q: queue
{
proc.entry request ( )
{
if free = TRUE
free = FALSE
else raramente due processi
delay (q) vorranno accedere
} contemporaneamente a risorsa
(caso ottimo)
proc.entry release ( )
{
if free = FALSE
free = TRUE
else
continue (q)
}
}
- CASO B
96 of 138
Process TP::
...
{
terminal.write (<messaggio>) avr una lunga permanenza
... in un monitor
monitor terminal (sistema non equilibrato)
entry write (...)
{
apri.finestra (...)
scrivi.messaggi (...)
}
}
E' chiaro quindi che il maggior problema proprio quello del controllo dei vari processi e della loro sincronizzazione.
Monitor:
- Esempio (produttore-consumatore)
97 of 138
uslm: MONITOR
VAR reader, writer: QUEUE;
buf: "qualcosa";
first, full: BOOL;
Il consumatore dopo la prima volta pu sempre accedere. Il produttore non pu scrivere due volte se il
consumatore non ha consumato ( pi lento del consumatore).
msls: MONITOR
VAR reader: QUEUE;
buf: "qualcosa";
empty: BOOL;
98 of 138
Torniamo all'esempio:
- Timetable
A timetable holds the start time and period of all tasks. It also schedule the execution of all active tasks.
The period of a task cannot exceed 13 hours.
An attempt to start a task process before it has completed its las cycle has no effect.
IMPLEMENTATION:
procedure initialize;
var task: processindex;
begin
for task:= 1 to processcount do
table(.task.).active:= false;
end;
99 of 138
procedure entry period(task: processindex; time: real);
begin
table(.task.).period:= time
end;
- Clock Process
A clock process increments a clock every second and examines a timetable of task processes waiting to be resumed.
IMPLEMENTATION:
The standard procedure wait delays the calling process for 1 sec.
- Operator Process
An operator process executes commands input from a typewriter. The human operator must push the BEL key on the
typewriter before typing a command. The commands are
start(task, hour:min:sec)
Defines the start time of a task and makes it active.
period(task, hour:min:sec)
Defines the period of a task.
stop(task)
Makes a task inactive.
100 of 138
type operatorprocess= process(typeuse: resource; tasklist: taskset; watch: clock; schedule: timetable)
An operator process needs access to a typewriter resource, a task set, a clock, and a timetable.
- Processi ciclici
Pi::
*[
<codice>
TQ!x( ) /* sospensione su messaggio sincrono vuoto */
]
- Processo schedulatore
TQ::
*[
/* appena possibile scrive un ID di processo da risvegliare
TT?awake(x)
e
x = Pi --> Pi_OK = TRUE pone TRUE la guardia corrispondente */
[]
Pi_OK --> Pi?y( )
Pi_OK = FALSE
]
TQ::
*[
TT?awake(x)
<pone TRUE la guardia corrispondente a x>
[
P1_OK --> P1?y( )
P1_OK = FALSE
[]
P2_OK --> P2?y( )
P2_OK = FALSE
[]
...
Pi_OK --> Pi?y( )
Pi_OK = FALSE
]
]
101 of 138
TQ::
*[
TT?awake(x)
<pone TRUE la guardia corrispondente a x>
[]
P1_OK --> P1?y( )
P1_OK = FALSE
[]
P2_OK --> P2?y( )
P2_OK = FALSE
[]
...
Pi_OK --> Pi?y( )
Pi_OK = FALSE
]
NOTA: guardie e alternative sui messaggi possono essere realizzate tramite funzioni che restituiscono la lunghezza della
coda sulla entry.
- Time Table
TT::
*[
clock?tick( )
<controlla se il processo x da riattivare a quest'ora>
TQ!awake(x)
]
Real Time
- REAL-TIME
Architetture trattate:
1. Il Kernel, Tradizionale/Micro
2. Lo Scheduler, e i sistemi Event Based
3. Gestione Eventi
4. I Device Drivers
5. La Gestione della Memoria
6. Le Periferiche
7. Threads
102 of 138
6. Semafori
7. IPC
1. Signals
2. Timers
3. Controllo Processi
4. IPC (Inter Processing Communication)
I sistemi real-time permettono azioni, o risposte ad eventi esterni con un tempo di risposta noto a priori.
Si usano quindi segnali di Interrupt (One-shot, Polling) per la giusta schedulazione temporale delle diverse applicazioni
(non necessariamente Real-Time sinonimo di velocit). Il problema sar quello di gestire il tempo di latenza, cio il tempo
intercorrente tra il lancio del segnale di Interrupt e la messa in esecuzione di un processo. Esso dipende dal S.O. e da cosa
sta facendo la macchina all'arrivo del segnale.
In un sistema Real-Time il tempo di latenza noto a priori e il buon funzionamento dipende essenzialmente dall'effettivo
rispetto dei tempi di risposta richiesti. Ad esempio:
. Controllo di processo
. Shop floor automation
. Robotica
. Simulazione
. Acquisizione dati
. Image processing
. Sistemi di test
. Sintetizzatori musicali
. Tests sperimentali
Vediamo alcune definizioni:
Si definiscono applicazioni real-time "rigide" o critiche, quelle applicazioni ove a priori noto un tempo di risposta minimo,
al di sopra del quale si ha il malfunzionamento del sistema.
La maggior parte dei sistemi real-time rigidi richiedono elaboratori con alte prestazioni.
Le applicazioni real-time rigide spesso non richiedono schedulazioni granulari, (alta risoluzione nel tempo di
schedulazione), ma tempi di risposta estremamente critici: ad esempio in un sistema di guida di un missile.
Si definiscono applicazioni real-time "soft" quelle applicazioni ove non si verifica un effettivo arresto del sistema
controllato, quanto un deterioramento delle prestazioni del sistema.
Questi sistemi real-time spesso gestiscono una grande quantit di dati o richiedono tempi di risposta molto bassi, ma nel
senso della schedulazione temporale di eventi.
103 of 138
Il fallimento di una schedulazione spesso implica la perdita di dati, ma non il crollo dell'intero sistema: ad esempio in un
sistema di prenotazione dei voli.
In alcuni casi il throughput del singolo canale basso, ma viene richiesta l'analisi di molti canali contemporaneamente, il
carico
totale quindi alto. Importante anche il tempo di risposta ad eventi asincroni e la capacit di effettuare la comunicazione
tra task multipli. Nei sistemi pi difficili si ha l'unione di tutti i fattori descritti: ad esempio nei simulatori di volo .
- Real-Time e Unix
L'elemento spesso pi critico di un sistema real-time e la sua capacit di reagire ad eventi asincroni (interruzioni).
Per fare questo il sistema deve essere in grado di reagire in tempi estremamente limitati.
Questo richiede una politica di schedulazione dei processi particolare.
- modo user
- modo kernel
chiamate ad utility
funzioni di libreria
altre funzioni
In modalit user il processo pu essere in ogni istante interrotto (preemption) da processi pi prioritari.
Unix tradizionale non ha un kemel interrompibile, ossia quando viene istanziata una system call, il cambio di contesto
disabilitato fino al ritorno del sistema in modo user.
Il tempo di esecuzione di una system call non noto a priori.
Ad esempio: Read di un file system presente su NFS (Network File System), accedo alla rete, ma se c' traffico devo
attendere --> 1. Time Out (molto lungo), 2. Leggo il blocco.
104 of 138
Durante il tempo in cui si trova in modo kernel, il processo blocca il processore impedendo l'esecuzione di tasks anche pi
prioritari.
Si definisce massimo tempo di latenza di un processo (maximum process preemption latency) il tempo che impiega un
processo
che si trova in modo kernel a tornare in modo user ed essere interrotto da un processo pi prioritario.
Il MPPL per sistemi non real-time pu essere superiore al secondo (1 sec.) .
Un kernel preemptive un kernel real-time, ossia permette l'interruzione di processi da parte di processi pi prioritari anche
se si trovano ad operare in modo kernel.
Sono inoltre disponibili algoritmi di schedulazione differenti dal tradizionale time sharing . Sono forniti sistemi per la
sincronizazione dei processi, che permettono la risposta in tempi limitati garantendo l'integrit dei dati e del kernel stesso.
L'MPPL per un kernel preemptive il tempo che richiesto al sistema per garantire l'integrit dei dati e del sistema stesso,
interrompendo il processo in corso. (<= 1 ms.).
Tramite un Fork creo un processo e lo invio ad una shell (8). Il processo portato in memoria (Unix usa una memoria
paginata su richiesta): porto in memoria solo una parte del processo e lo scheduler lo mette in esecuzione (3) in modalit
Time Sharing.
Il processo finisce quando:
Il processo interrotto alla fine del quanto di tempo messo in fondo alla coda di scheduling.
Infine nel caso che un processo termini male lo tolgo dalla memoria con un exit (9).
105 of 138
Tempo di latenza preemptive (latenza fissa)
- Politiche di schedulazione
Lo schedulatore determina come vengono utilizzate le risorse del sistema (CPU's) dai processi che devono essere eseguiti.
Il quanto di tempo di esecuzione di un processo legato alle caratteristiche della schedulazione.
La schedulazione scelta in base alla tipologia del processo che viene eseguito, possiamo definire tre principali categorie:
1) Processi time-sharing: utilizzati per applicazioni iterative e non, senza tempistiche critiche.
2) Processi di sistema: utilizzati nella gestione del sistema, la tempistica di esecuzione di questi processi
fondamentale per il buon funzionamento del sistema.
3) Processi real-time: applicazioni critiche che devono essere completate in tempi definiti.
La politica di schedulazione controllata tramite la funzione sched_setscheduler, che ammette tre stati possibili:
SCHED_OTHER: schedulazione time-sharing
SCHED_FIFO: schedulazione first-in, first-out
SCHED_RR: schedulazione round-robin
Le priorit di schedulazione delle tre politiche sono in parte sovrapposte per avere una maggiore flessibilit.
I processi vengono accodati secondo una priorit.
La priorit viene di volta in volta stabilita dallo schedulatore a seconda della tipologia dei processi in esecuzione e delle
risorse
disponibili. La priorit si modifica durante l'esecuzione del processo stesso: le operazioni di sistema hanno priorit pi
elevata, la priorit diminuisce al progredire del processo.
L'algoritmo che stabilisce le priorit da associare ai processi in coda, e quindi le modalit di esecuzione dei processi si
definisce
politica di schedulazione.
In un sistema real-time non pensabile una politica di schedulazione che modifica in modo autonomo le priorit di
La schedulazione nice contiene le funzioni di schedulazione tradizionale time-sharing con ricalcolo automatico delle priorit
106 of 138
dei
processi (funzionamento tradizionale).
La schedulazione real-time viene eseguita tramite algoritmi a priorit fissa (FIFO - Round Robin).
Le due modalit possono coesistere in quanto lo schedulatore pu operare, indifferentemente, una delle due politiche
basandosi sul processo. Le responsabilit della schedulazione sono quindi di colui che scrive le applicazioni.
possibile modificare la politica di schedulazione modificando le priorit prestabilite.
Posix real-time ha modificato anche le classi di priority nella schedulazione e nei processi.
Le priority pi alte sono riservate ai processi real-time, mentre i processi di sistema hanno le priorit medie.
La classe bassa di priority lasciata ai processi utente.
Le priority alte sono raggiungibili solo con algoritmi di schedulazione real-time .
Non necessariamente le applicazioni real-time devono essere eseguite a priorit "real-time": in genere la schedulazione
inizia
sempre come time sharing per poi essere modificata sia in politica che in priorit in presenza di regioni o processi critici.
(Ad esempio: in Unix tradizionale le priorit vanno da +19 a -19 a seconda del processo: un processo utente ha priorit P=0,
un processo di sistema ha priorit media PM 15-19; in Unix Real-Time, invece, le priorit vanno da 0 a 70: i processi di
sistema hanno priorit intermedia e l'utente ha la possibilit di creare processi a priorit maggiore della PM di sistema).
La schedulazione Time Sharing permette a processi real-time di tornare a modalit di funzionamento non real-time.
La priorit di un processo schedulato in questa modalit pu essere modificata sia dal processo che dallo schedulatore.
Il processo che esegue in modalit time-sharing viene eseguito finch lo schedulatore non ricalcola le priorit.
La schedulazione time-sharing implica che il processo schedulato venga eseguito per un quanto di tempo stabilito a priori.
La schedulazione a Priorit Fissa: in questa modalit lo schedulatore non ha la possibilit di modificare le priorit dei
processi, solo i processi stessi possono intervenire sulla schedulazione.
Nel caso di un processo che deve terminare la sua esecuzione senza essere schedulato, sufficiente impostare una priorit
realtime alta (ad ex. 30) e una politica FIFO, in tal caso garantito che il processo non verr mai messo in fondo alla coda
dei
La schedulazione FIFO non interrompe l'esecuzione di un processo schedulato fino a che non interviene una richiesta di un
processo pi prioritario. In questo senso esiste una scala delle priorit (lista di puntatori) alla quale "aggancio" i processi.
Quindi un processo Real-Time pu completare la sua esecuzione prima che si verifichi una condizione capace di
interromperlo.
In caso di processi con stessa priorit il processo da schedulare viene scelto da una lista ordinata in base al tempo di
esecuzione effettuato, quando la lista vuota si passa ad una lista di processi con priorit minore.
Le regole utilizzate da questa politica di schedulazione sono:
- quando un processo e rischedulato, viene posto all'inizio della lista dei processi a quella priorit
- quando un processo interrotto viene schedulato, viene posto alla fine della lista dei processi della sua
priorit
- quando un processo in esecuzione modifica la priorit, o la politica di schedulazione di un altro processo,
quest'ultimo viene posto alla fine della coda della priorit relativa
- quando un processo volontariamente interrompe l'esecuzione, viene posto alla fine della lista.
La schedulazione Round Robin assegna un quanto di tempo al processo con priorit pi alta.
107 of 138
Il processo resta in esecuzione fino a che non si presenta una richiesta di un processo pi prioritario o si esaurisce il quanto
di
tempo. Al termine del quanto entra un esecuzione un altro processo egualmente prioritario del precedente.
In assenza di processi egualmente prioritari il processo continua la sua esecuzione.
Questo permette l'esecuzione contemporanea di pi processi critici.
Quando un processo viene interrotto in modalit real-time viene salvato il suo contesto, pur essendo sospeso resta in stato
"running". La schedulazione Round Robin serve per implementare sistemi che necessitino di time slice.
Il quanto di tempo viene letto tramite la sched_get_rr_interval.
Il controllo della schedulazione fondamentale nelle applicazioni real-time. La schedulazione non come nel time sharing
incontrollabile, ma stabilita a priori dal progettista dell'applicazione.
Il controllo sulla schedulazione viene fatto nella scelta dei processi da schedulare e nel controllo delle priorit dei processi
presenti. La politica di schedulazione stabilisce come viene scelto il processo da eseguire tra quelli in attesa, la legge di
accodamento e la dimensione del quanto di tempo da assegnare al processo. Come gi detto le priorit intervengono nel
processo di schedulazione, ma ogni politica ha associato dei ranges di priorit.
Le funzionalit Posix real-time permettono il controllo sia delle politiche di schedulazione che delle priorit di esecuzione
dei
processi, permettendo cos un totale controllo sulle risorse del sistema.
In una applicazione real-time multiprocesso, i programmi vengono eseguiti concorrentemente, ma le regioni critiche poste
nei
diversi processi devono essere disposte temporalmente in modo tale da non scontrarsi.
- IPC (Inter Processing Comunication) un'insieme di strumenti necessari per la comunicazione interprocesso:
esecuzione, viene inserito nella politica di schedulazione in vigore, nel caso che abbia una priorit pi alta viene
automaticamente
108 of 138
passato a running; mentre, in caso sia stato creato con priorit pi bassa, salta immediatamente nello stato runnable.
Posix definisce la schedulazione multithread in cui tutti i blocchi vengono trattati secondo le politiche di schedulazione
scegliendo
il thread pi prioritario (i thread in esecuzione hanno tutti lo stesso spazio di indirizzamento).
Nella schedulazione, real-time esistono solo tre stati per un processo:
. running
. runnable
. waiting
Un processo alla sua creazione parte come running, il passaggio tra runnable e runring viene controllato dallo scheduler, il
quale
tiene una tabella di tutti i processi runnable con le relative priorit, lo stato di wait interviene solo nel caso di interruzioni
indipendenti dal processo, come timers o I/O asincrono.
molto importante l'uso del lock della memoria, perch impedisce la paginazione dei processi runnable, poich in caso di
swap il tempo di latenza nella fase di caricamento diventa lungo.
Il processo in stato di running definito come il processo corrente ed ha il controllo del Kernel. Se un processo in stato di
runnable oppure waiting pu essere invece eliminato dalla schedulazione in ogni istante. Solo i processi in stato runnable
possono entrare in esecuzione, quelli in stato di waiting attendono che si verifichino una o pi condizioni.
- Ordine di esecuzione
Processi runnable
Prima del cambio di priorit Dopo il cambio di priorit
I processi A, B, C sono quelli a maggiore priorit nella process list. A ha priorit 30 all'inizio della schedulazione, quindi
viene seguito per primo, quindi B e C. Se nessun altro processo raggiunge priorit 30, al termine della schedulazione di C
verranno eseguiti processi a priorit pi bassa (D).
Quando un processo cambia la priorit, si va ad inserire nella coda relative alla nuova priorit che assume.
La politica di schedulazione stabilisce il tempo globale di esecuzione di un processo. La priorit combinata con la politica
dischedulazione stabilisce come il processo viene eseguito. Con schedulazione time sharing la priorit viene ricalcolata
runtime.
Quando uno di questi eventi si verifica lo schedulatore riesamina lo schema di schedulazione e stabilisce quale processo
deve
entrare in esecuzione. La schedulazione viene effettuata solo tra i processi runnable, l'introduzione nella lista di un nuovo
processo runnable a priorit pi alta di quello attualmente in esecuzione implica la sua rischedulazione.
In caso di pi processi runnable con stessa priorit, la schedulazione viene fatta sulla base della politica di schedulazione.
Riassumiamo e ampliamo quanto detto:
109 of 138
- OSF ha due politiche di schedulazione:
Nice
La schedulazione nice ha le seguenti caratteristiche:
- supporta solo la schedulazione time-sharing
- le priorit vanno da 20 a -20
- la priorit di default 0
- la priorit pi bassa rappresenta il processo pi prioritario
- le priorit si cambiano con nice, renice o setpriority
Real-Time
La schedulazione real-time prevede un supporto per politiche di schedulazione multiple,
compreso il time-sharing. La priorit e la politica pu essere cambiata con ogni politica e
priorit attiva.
La schedulazione real-time ha le seguenti caratteristiche:
- supporta time-sharing, FIFO e round-robin
- le priorit vanno da 0 a 63
- le priorit sono fisse
- la priorit maggiore indica una maggiore priorit del processo
- il cambio di priorit viene fatto con sched_setparam o sched_setscheduler
- la politica di schedulazione viene modificata tramite sched_setscheduler
In ogni caso, indipendentemente dalla politica di schedulazione in vigore, lo schedulatore usa sempre la politica di mettere
in esecuzione il processo con la priorit pi alta.
Ogni processo ha una priorit iniziale, che nel caso time sharing data dal sistema e nel cavo di real-time e data dal
processo stesso.
- nice
I processi nascono con priorit 0, pu essere modificata durante l'esecuzione tramite le funzioni nice, renice o setpriority.
La priorit di un processo pu essere letta tramite la funzione getpriority.
Le priorit da 0 a 20 (basse) possono essere settate dall'utente, le priorit -1 -20 (alte) solo dal superuser.
- real-time
Le priorit dell'interfaccia real-time si dividono in tre fasce:
- Visualizzazione Priorit
Le priorit dei processi vengono visualizzate sempre tramite il comando ps, anche se per ragioni temporali non pu essere
preciso.
La modalit che permette di vedere le priorit effettive dei processi psxpri, quindi il comando:
ps -aeO psxpri
ci fornisce l'elenco dei processi in esecuzione con i relativi PID, PPR (priorit dei processi in modalit assoluta), STAT e
altre cose.
110 of 138
PID PPR STAT TIME COMMAND
0 31 R< 29171:32:53 [kernel idled]
1 18 I 0:17.37 /sbin/init -a
[exception
2 19 I 0:00.00
handler]
7720 60 S 0:00.01 ./tests/ex1
7725 18 R 0:00.06 ps -aeO psxpri
La priorit in modalit real-time deve essere assegnata con una certa discrezione, poich risultando pi alta delle priorit di
sistema pu alterarne pesantemente le funzionalit.
Processi seguiti in modalit real-time per lunghi periodi possono portare malfunzionamenti nei servizi base (rete, gestione
devices varie ecc.). In molti casi e sufficiente operare real-time a priorit 29 (inferiore alle priorit dei processi di sistema).
I processi critici devono invece essere schedulati con le vere priorit del real-time (32-63).
Posix 1003.4 permette sia la modifica delle priorit che dei modelli di schedulazione in ogni fase dell'esecuzione di un
processo. E' per consigliabile modificare la politica di schedulazione solo nella fase di creazione del processo stesso.
Ogni processo deve avere una priorit di partenza adeguata all'attivit che deve svolgere.
Per facilitare la gestione e conveniente "cablare" come priorit di default la massima che il processo pu assumere.
All'interno, del file sched.h sono definite le priorit di transizione delle tre fasce:
SCHED_PRIO_USER_MIN (0)
SCHED_PRIO_USER_MAX (19)
SCHED_ PRIO_SYSTEM_MIN (20)
SCHED_PRIO_SYSTEM_MAX (31)
SCHED_PRIO_RT_MIN (32)
SCHED_PRIO_RT_MAX (63)
In tutti i casi ove il pid 0 si considera il processo chiamante. In caso di scrittura di applicazioni portabili, sempre
necessario
settare priorit non assolute, ma realtive a quelle date dai vari SCHED_PRIO, ed inoltre non bisogna assumere che priority
111 of 138
sia
l'unico campo della struttura sched_param. Tutti i campi presenti in questa struttura vanno sempre inizializzati, prima che
la struttura venga passata alla funzione utilizzatrice.
- Signals (segnali)
Nella programmazione ad eventi si gestiscono le risorse tramite i segnali. Ogni segnale ha un comportamento di default:
morte del processo e/o esecuzione di una procedura [ handler ( )]. I segnali possono essere subiti o gestiti dal processo che li
riceve.
Di seguito viene fornito un elenco dei segnali usati in UNIX.
presente in tutti i sistemi mandato ad un processo all'hangup della linea telefonica in utilizzo di
SIGHUP
terminali
SIGQUIT
SIGINT presente in tutti i sistemi, interrupt da tastiera
SIGILL presente in tutti i sistemi, porta al core, istruzione illegale
SIGTRAP presente in tutti i sistemi, porta al core, traccia un trap
SIGIOT presente su tutti i sistemi, porta al core, segnala un trap di 10
SIGEMT presente su tutti i sistemi, porta al core, emulator trap
SIGFPE presente su tutti i sistemi, porta a core, floating point exception
SIGKILL presente su tutti i sistemi, non pu essere trappato n ignorato
SIGBUS presente su tutti i sistemi, porta al core, errore di bus
SIGSEGV presente su tutti i sistemi, porta al core, errore di segmentazione
SIGSYS presente su tutti i sistemi, porta al core, bad argument system call
SIGPIPE presente su tutti i sistemi, scrittura su pipe senza lettura
SIGALRM presente su tutti i sistemi
SIGTERM presente su tutti i sistemi, terminazione software
SIGURG presente su 4.2 4.3 dimenticato se non trappato, segnale di socket urgente presente in porta
SIGSTOP presente su tutti i sistemi, sospende il processo che lo riceve
SIGSTP presente su BSD, stop generato da tastiera
SIGCONT presente su BSD, ignorato se non trappato
SIGCHLD presente su BSD, ignorato se non trappato, segnala il cambiamento di stato del figlio
SIGTTIN
SIGTTOU disponibile su BSD, ignorato se non trappato, segnala out a terminale
SIG IO disponibile su BSD, ignorato se non trappato, segnala la presenza di 10 su descrittore
SIGXCPU disponibile su BSD, time cpu exceeded
SIGXFSZ disponibile su BSD, file size exceeded
SIGVTALRM disponibile su BSD, virtual alarm
SIGPROF BSD profiling alarm
SIGWINCH BSD window changed size
SIGCLD sysV morte di un figlio
SIGPWR sysV power failure
SIGUSR1 segnale disponibile agli utenti
SIGUSR2 segnale disponibile agli utenti
I signals (segnali) sono la forma pi tradizionale di IPC (Interrupt software [Unix]), sono presenti in tutte le versioni di
Unix, non solamente in quelle realtime.
Sono generalmente utilizzati per scambiare segnali tra processi. I segnali sono asincroni, il processo destinatario non pu
prevedere il momento di arrivo di un segnale, n chi lo manda.
112 of 138
Il processo destinatario deve contenere il codice che deve essere eseguito all'arrivo del segnale.
Questo codice pu implicare l'esecuzione di un azione, la terminazione del processo stesso, o la sua prosecuzione tramite
l'esecuzione di una parte di codice particolare.
I signals sono spesso intesi come interruzioni software e sono l'equivalente a livello di kernel degli interrupt hardware.
Esiste una vasta scelta di segnali, in parte utilizzabili dall'utente ed in parte utilizzati dal sistema operativo e "trappati" dai
processi utente (Trap: modifica un comportamento di default del segnale), inoltre sono state introdotte metodologie per
l'utilizzo di signals in Real-time.
- Signals real-time
POSIX ha definito una nuova utilizzazione di segnali per applicazioni real-time. In particolare la gestione dell'I/O asincrono
ed i timers generano segnali come parametri espliciti delle funzioni, quindi utilizzando queste funzioni non risulta
necessario chiamare esplicitamente i segnali, poich gi fatto dalle funzioni stesse.
Il lancio dei segnali in posix viene fatto tramite la struttura Sigaction (vedi oltre) passata come argomento in modo diretto o
indiretto alla funzione. E' una struttura che definisce il tipo e la politica di gestione per un determinato segnale.
Nella maggiore parte dei casi i segnali sono utilizzati per recapitare eventi asincroni.
SIGALRM: spesso utilizzato per prescrivere azioni che devono essere effettuate dal processo ricevente
SIGCLD: segnale che comunica l'occorrenza di un evento
SIGFPE - segnale legato ad eccezioni
Molte funzioni di uso comune sono basate sui segnali:
wait
pause
waitpid
sono funzioni che sospendono l'esecuzione di un processo fino al verificarsi di un evento
sigemptyset
utilizzata per creare un set vuoto di segnali, infine
sigpending
che controlla l'esistenza di segnali bloccati.
Per ogni segnale possibile settare all'interno di un processo le azioni che devono essere consequenzialmente svolte tramite
l'uso della sigaction. La sigaction viene eseguita in modo asincrono quando il segnale arriva al processo, il processo pu
quindi
decidere di accettare il segnale o ignorarlo.
- Primo modo
signal(sgn, val)
sgn: il segnale
113 of 138
val: handler
- Secondo modo
struct sigaction
}
void (*sa_handler)(); oppure SIG_DFL, SIG_IGN
sigset_t sa_mask; maschera segnali bloccati
int sa_flags; flags del segnale
}
flags
sigset_t sigsetmask(mask)
int sigprocmask(int how, sigset_t *set, sigset_t *oset)
how: SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK
- Sigaction
struct sigaction {
void (*sa_handier)();
void (*sa_sigaction) (int, siginfo_t*, void *)
sigset_t mask;
int flags;
}
flags:
SA_RESETHAND: il segnale dopo la delivery viene riportato al comportamento standard: SIG_DFL
SA-NODEFER: il segnale non bloccato dal kernel
SA-RESTART: le funzioni interrotte dall'arrivo del segnale vengono fatte ripartire
SA_SIGINFO: consente, se settata, il passaggio di due argomenti addizionali all'event handler
114 of 138
di un segnale o la terminazione di un figlio.
waitpid - permette ad un processo di avere informazioni sullo stato da uno specifico processo figlio, e quindi sospenderlo
fino all'arrivo di un segnale specifico.
- Spedizione di Segnali
I segnali vengono spediti dai processi utente, dal kernel o dai device drivers, le condizioni per la generazione di un segnale
sono:
Un processo utente lancia un segnale ad un altro processo utente.
Il kernel lancia un segnale ad un processo utente.
Un diriver lancia un segnale ad un processo utente.
Un processo utente lancia un segnale a se stesso.
Usando kill il primo argomento corrisponde all'ID del processo destinatario, il secondo al segnale che si vuole spedire o il
gruppo di processi da segnalare.
I segnali possono essere spediti tramite tastiera, tutti i segnali emessi da tastiera vengono spediti a tutti i processi di
terminale.
I segnali possono essere bloccati per proteggere certe zone di codice da possibili interruzioni.
Il segnale non viene per ignorato, ma solo postposto.
Ad ogni processo viene assegnata una maschera (evita di bloccare l'event handler durante l'esecuzione), in cui ad ogni bit
corrisponde un segnale definito in signal.h, i segnali vengano bloccati e sbloccati tramite la maschera.
La maschera viene inizializzata in fase di creazione del processo sulla base della maschera del processo padre.
SIG_MASK: creazione della maschera (OR dei segnali di cui vogliamo la maschera).
Il processo utente pu modificare la sua maschera tramite la sigprocmask e la sigsuspend, lanci successivi di uno stesso
segnale vengono ignorati. La sigprocmask permette di modificare la maschera di un processo, il primo argomento determina
la azione da svolgere: nel caso di SIG_SETMASK, la maschera viene rimpiazzata con quella passata (SIG_SETMASK (0)
riabilita tutti i segnali), tramite la SIG_BLOCK e la SIG_UNBLOCK vengono modificati i segnali bloccati (secondo
parametro), il terzo parametro la maschera.
Tramite la sigsuspend si pu incrementare il numero dei segnali che sono sospesi fino all'arrivo di un certo evento.
Per avere informazioni sullo stato dei segnali bloccati si usa il costrutto:
Tramite la sigpending invece si hanno informazioni sui segnali sospesi e in attesa di essere sbloccati.
Al termine di una regione critica bene ripristinare la maschera senza eccessive limitazione sui segnali, sbloccando quindi
quelli
sospesi.
115 of 138
maschera generale.
I segnali sono gestiti tramite la sigaction e la signal. Entrambe le funzioni permettono di gestire le seguenti azioni:
Ignorare un segnale
Eseguire una azione
Prendere il segnale, eseguendo una routine
Quando un processo ignora un segnale, come se il segnale non fosse stato lanciato.
Nella maggiore parte dei casi i segnali vengono presi ed eseguite routine apposite, in questi casi il controllo viene passato al
struct sigaction
{
void *sa_handler;
sigset_t sa_mask;
int sa_flags;
Se l'azione specificata diversa da NULL, l'azione descritta da una sigaction caratteristica, nel caso che sia NULL, la
chiamata restituisce la sigaction attualmente in funzione.
sa_handler contiene l'azione collegata al segnale.
sa_mask contiene la maschera di segnali che saranno mascherati al verificarsi dell'evento, al termine verr ripristinata la
maschera oginaria.
- Esempio
#include<unistd>
#include<signol.h>
#include<stdio.h>
main(argc, argv)
int argc;
char *argv[ ];
{
void announce( );
struct sigaction action;
if(argc(!= 2)
{
fprintf(stderr,"Usage: %s seconds\n",argv[0]);
exit(1 );
}
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
action.sa_handler = announce;
sigaction(SIGALARM,&action, NULL);
alarm((unsigned) atoi(argv(1));
pause( );
printf("\n signal arrived");
}
void announce(signo)
int signo;
116 of 138
{
printf("Received signal: %d/n",signo);
}
signal(SIGIO, SIG_IGN);
signal(SIGCHLD, SIG_DFL);
signal(SIGALRM myhandler)
Un signal handler ha tre argomenti, quindi sempre necessario dichiararli:
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
main( )
{
FILE *fp; /* File pointer to "tmp" */
char c; /* Character read from terminal */
void sigint_handler( ); /* The SIGINT signal handler */
if (signal (SIGINT, SIG_IGN) != SIG_IGN)
/* If SIGINT is already being ignored, */
/* Don't declare a handler for it */
117 of 138
/* Remove "tmp" file, and kill this */
/* Program. Do not return to main( ) */
void sigint_handler( )
{ if ( unlink("tmp") != -1)
puts ("The tmp file has been removed.");
exit(l);
}
- Shared Memory
La shared memory ed i files mappati in memoria permettono ai processi di comunicare tramite uno spazio di indirizzamento
comune. Quando un processo scrive un dato in uno spazio di indirizzamento questo immediatamente disponibile a tutti
coloro che lo possono leggere.
Questo tipo di comunicazione estremamente veloce poich non porta nessun tipo di overhead legato alle system calls o
altro ed inoltre non viene utilizzato nessun buffer.
Le funzioni per la gestione della shared memory e dei files mappati permettono di controllare l'accesso alle aree condivise,
in
modo da coordinarne l'uso.
Utilizzando un file mappato in memoria, le modifiche fatte da un processo sul file stesso sono viste da tutti gli altri.
Il mappaggio in memoria persiste fino a che tutti i processi cooperanti esistono.
Sia files che aree di memoria condivisa vengono utilizzati con la stessa politica:
shm_open(name, flags, mode): apre un oggetto di tipo memoria condivisa, tornandone il file descriptor, nel caso che
sh_open
Ogni processo che vuole utilizzare quell'oggetto deve aprire un collegamento utilizzando la shm_open.
L'oggetto pu essere aperto con diversi flags:
118 of 138
O_RDONLY apre un accesso con sola lettura
O_RDWR apre un accesso in lettura e scrittura
O_CREAT crea un oggetto di tipo memoria condivisa
O_EXCL usato in unione a O_CREAT, crea un oggetto esclusivo
O_TRUNC azzera la lunghezza dell'oggetto
- Esempio:
#include <unistd.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <fcntl.h>
main( )
{
int md;
int status;
long pg size;
carrd_t virt_addr;
- Files mappati
La funzione open punta al file che si vuole mappare, mmap stabilisce la dimensione della finestra che viene mappata in
memoria e in che modalit avverr l'accesso ai dati.
I flags di open
mmap mappa la memoria richiesta dall'oggetto considerato
mmap restituisce l'indirizzo fisico dove viene mappata la memoria o il file, la dimensione deve essere multipla della
dimensione della pagina, in caso contrario ogni indirizzo compreso tra la fine del mapping e la fine della pagina risulta
perso.
L'offset deve essere trattato allo stesso modo.
Le protezioni possono valere:
PROT_READ: acceso in lettura
PROT_WRITE: accesso in scrittura
PROT_EXEC: accesso esclusivo
PROT_NONE: accesso negato
119 of 138
MAP_FIXED: l'addr viene interpretato esattamente come fisico, in pratica il processo creatore dell'area chiama mmap senza
questo flag, l'indirizzo ottenuto quindi passato a tutti i processi che vogliono utilizzare l'area e che mappano, quindi,
l'indirizzo fornito. Esistono poi altri flags non supportati dallo standard POSIX:
MAP_ANONYMOUS
MAP_FILE
MAP_VARIABLE
- Altre funzioni
Gli oggetti di tipo shared memory possono essere gestiti tramite le tradizionali funzioni di accesso ai files, come le funzioni
definite nel POSIX.1:
- Sincronizzazione
msync sincronizza il contenuto della parte di file mappata in memoria con il file fisico.
La sincronizzazione pu essere fatta in modo sincrono (MS_SYNC) o asincrono (MS_ASYNC), nel caso di
sincronizzazione sincrona la funzione non ritorna fino al termine delle operazioni di scrittura, nel caso asincrono la funzione
ritorna immediatamente.
mprotect modifica le caratteristiche dell'accesso alla zona condivisa, la modifica di accesso pu per solo essere applicata
all'intera area e non a pezzi della stessa .
L'area di memoria condivisa pu essere bloccata tramite mlock, in modo da evitarne la paginazione.
- Esempio
#include<unistd.h>
#include<sys/types.h>
#include<stdio.h>
#include<sys/file.h>
#include<sys/mman.h>
#include<sys/stat.h>
#include<errno.h>
main( )
{
int fd;
caddr_t pg_addr;
int size = 500;
int mode = S_IRWXO|S_IRWXG|S_IRWXU;
fd = shm_open("example",O_RDWR|O_CREAT, mode);
if(fd<0)
{
perror("open error");
exit(0);
}
120 of 138
if((ftruncate(fd,size)) = = -1)
}
perror("ftruncate failure");
exit(0);
}
pg_addr = (caddr_t) mmap(0,size,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_SHARED,fd,0);
if(pg_addr = = 0)
{
perror("map failure");
exit(0);
}
mlock(pg_addr,size);
munmap(pg_addr,size);
close(fd);
shm_unlink("example");
exit(0);
}
In caso di utilizzo condiviso da parte di pi processi di un'area di memoria condivisa, risulta necessario regolamentare gli
accessi all'area, per evitare conflitti e perdere la validit dei dati presenti.
I semafori binari sono un sistema estremamente semplice per controllare gli accessi in memoria.
In pratica ogni unit che vuole accedere all'area deve prima controllare lo stato del semaforo, se libero "lockarlo", accedere
all'area e quindi sbloccare il semaforo.
Nel caso in cui il semaforo risulti bloccato, il processo deve attendere la sua liberazione prima di accedere alla memoria.
- Memory Management
In un sistema multiprogrammato, essenziale una gestione della memoria che ottimizzi lo sfruttamento delle risorse
disponibili,
garantendo ai processi in esecuzione la disponibilit dello spazio di indirizzamento necessario.
Il sistema di memory management di un sistema operativo disegnato per massimizzare il numero di processi eseguibili,
limitando i conflitti, che riducono le prestazioni.
Se un processo resta in memoria, il kernel deve occupare della memoria e quindi ridurre le possibilit di allocazione per gli
altri
processi in esecuzione.
Riducendo lo spazio di memoria occupato da un processo si aumentano le possibilit di allocazione.
Lo spazio di indirizzamento virtuale diviso in parti uguali chiamate pagine, ogni processo occupa un certo numero di
pagine, che vengono spostate tra la memoria primaria e la secondaria in fasi alterne dell'esecuzione del processo.
Solo un sottoinsieme delle pagine resta in memoria primaria.
La paginazione di un processo implica una riduzione delle prestazioni dovuta al tempo di caricamento.
Il locking delle pagine in memoria elimina questo peggioramento, introducendo per limiti nella gestione della memoria,
poich
parte della stessa utilizzata per il locking dei processi.
- Memory locking
La gestione della memoria permette che tutti i processi abbiano egualmente accesso alle risorse presenti nel sistema.
Il sistema operativo mappa la memoria fisica nello spazio di indirizzamento virtuale del processo.
In generale la gestione della memoria risulta trasparente all'utente e totalmente controllata dal sistema operativo.
Nelle applicazioni Real-Time necessario un utilizzo della memoria differente da quello tradizionale evitando la
paginazione dei dati del processo e riducendo cos i tempi d'accesso alla schedulazione del processo.
Nelle applicazioni real-time la memoria ha molta importanza, poich fondamentale il lock dei processi in memoria durante
la
loro esecuzione, quindi deve esserci abbastanza memoria sia per i processi real-time che per i processi time-sharing.
Il tempo di latenza dovuto alla paginazione risulta quasi sempre inaccettabile per le applicazioni real-time.
- Locking e Unlocking
Il locking della memoria parte delle inizializzazioni da fare alla creazione di un processo real-time.
Alcune applicazioni devono restare "lockate" per tutta la durata dell'esecuzione, in casi meno critici possibile liberare la
121 of 138
memoria in alcune fasi dell'elaborazione dove non fase critica.
Il lock della memoria si applica allo spazio di indirizzamento del processo.
Le funzioni che gestiscono il lock della memoria sono:
. mlock : permette di lockare nella memoria una regione dello spazio di indirizzamento
mlockall : locka l'intero spazio di indirizzamento del processo considerato
munlock : sblocca una regione di memoria
munlockall: sblocca l'intero spazio di indirizzamento di un processo
Sysconf(SC_PAGE_SIZE) restituisce la dimensione della pagina, parametro che pu variare da sistema a sistema.
La memoria lockata viene automaticamente rilasciata al termine del processo, tramite munlock possibile rilasciare parte
dello spazio di indirizzamento, o, nel caso di lock successivi, rilasciare la memoria lockata tutta assieme.
MCL_FUTURE: devono essere lockate tutte le pagine dello spazio di indirizzamento del processo attualmente non in
memoria
L'OR dei due flag d tutte le pagine del processo, tranne quelle gi terminate di utilizzare
munlochall sblocca tutte le pagine relative al processo.
122 of 138
- Esempio
#include<unistd.h>
#include<sys/mman.h>
#define DATASIZE 2048
mainl( )
{
char data(DATA_SIZE);
if(lock_memory(data, DATA_SIZE) = = -1)
perror("lock memory");
123 of 138
if(unlock_memory(data, DATA_SIZE) = = -1)
perror("unlock memory");
}
- Clocks e Timers
Le applicazioni real-time devono operare con tempi limitati nella schedulazione degli eventi.
Esistono due classi di applicazioni che devono essere schedulate a tempi limitati:
Le applicazioni ad alto throughput richiedono la gestione di grande quantit di dati in modo continuo (stream), le
applicazioni con tempi critici operano generalmente in modo asincrono.
L'efficenza dei sistemi real-time spesso si basa sul soddisfacimento di questi requisiti temporali, alla base di tutto deve
quindi essere presente una buona gestione delle temporizzazioni.
Le funzioni di Posix 1003.4 che regolano i timers hanno le seguenti caratteristiche:
- Timers
Timers one-shot
Timers periodici
I timers one-shot sono settati con un unico tempo di termine e in quell'istante vengono disattivati, i timers periodici, allo
scadere del tempo stabilito non vengono disattivati, ma "riarmati".
Il tempo di ciclo di un timer pu essere relativo o assoluto. Il timer relativo si basa sul tempo trascorso dall'istante di
armamento, il timer assoluto si riferisce sempre al real-time timer.
La creazione di un timer pu essere effettuata tramite la funzione timer_create a cui associata una struttura di tipo
sigevent.
In fase di creazione possibile stabilire un primo tempo di attivazione e quindi un intervallo di attivazione.
Quando il timer viene attivato, il kernel manda un segnale al processo che ha attivato il timer, quindi necessario settare un
signal handler per ricevere questo messaggio.
Per utilizzare un timer necessario fare i seguenti passi:
In presenza di timers multipli che attivano gli stessi segnali, questi ultimi si perderanno.
L'include time.h contiene le strutture base per la manipolazione dei clock e dei timers, in particolare esistono le strutture
timespec e itimerspec definite dal posix 1003.4.
124 of 138
timespec contiene sia i secondi che i nanosecondi, itimerspec contiene due strutture del tipo timespec (tempo inizio, ciclo)
A seconda che i valori siano zero o no la struttura permette di definire timers one-shot o periodici.
timer_create: si usa per creare i timers, ritorna l'ID del timer creato, il timer creato non armato fino alla chiamata alla
timer_settitime, il numero di timer massimi attivabili definita in limits.h tramite TIMER_MAX.
Timer_create definisce la sigevent collegata al timer, e quindi sia il segnale che l'evento collegato al timer, per default viene
usato SIGALARM
timer_delete: si usa per eliminare il timer
timer_gettitime: restituisce il tempo mancante all'attivazione di un timer ed il tempo di ciclo, nel caso che il timer risulti non
armato, la funzione nel valore della it_time ritorner 0.
timer_settitime: si usa sia per definire alcune delle caratteristiche del timer, che per armare il timer stesso.
Gli argomenti di timer_settitimer sono:
La disabilitazione di un timer one-shot automatica, legata alla attivazione del timer stesso, in caso di timer
relativo viene fatta con l'uso di una settitimer con it_value a 0
ovalue: contiene una struttura di tipo iterspec, con valore != 0 se il timer armato, in particolare conterr il
tempo mancante all'attivazione.
- System Calls
125 of 138
Bisogna distinguere le System Calls dalle Funzioni
System Call una chiamata che richiede l'intervento del sistema, per compiere una operazione richiesta dal programma in
esecuzione (codice facente parte del Kernel).
Read richiede al sys di riempire un buffer con dati che si trovano su un disco o su un'altra device.
Visto che l'accesso diretto delle device da parte dei programmi comporterebbe enormi problemi, si richiede l'intervento del
sys per fare questo.
Funzione non richiede l'intervento del sys per svolgere le sue funzionalit: sin, cos, tan esegue i suoi calcoli senza
richiedere l'intervento del sys.
La gestione delle system call era diversa a seconda del sistema operativo (Unix) usato:
. SYS V: interrompeva la system call quando veniva chiamato un signal, mandando un segnale di errore
. BSD: rilanciava automaticamente la system call interrotta non appena conclusa la gestione
dell'interruzione.
read(int fd, void *, int) - consente l'accesso ad un file descriptor (identificatore dell'accesso ad una periferica) ed il
trasferimento di dati da una device ad un buffer
write(int fd, void *, int) - consente l'accesso ad un file descriptor, trasferendo dati da un buffer alla device
dup(int fd) - duplica un file descriptor (serve a mantenere il puntatore al file)
close(int fd) - chiude un file descriptor
- Esempio (read)
L' accesso alla periferica avviene solo quando quest'ultima disponibile (con evidente risparmio di tempo ed una maggiore
sicurezza).
fork( ) - consente la creazione di un nuovo processo, viene fatta la copia del processo che chiama la fork e quindi messo in
esecuzione in parallelo al creatore (padre), se la call fallisce ritorna -1 (non si pu "forkare" se la memoria insufficiente o
abbiamo superato il numero di processi gestibili).
Interessante che fork( ) ritorna due valori diversi al padre ed al figlio:
- al padre ritorna il process identifier (pid)
126 of 138
- al figlio ritorna 0
Il padre e il figlio di una fork non hanno lo stesso spazio di indirizzamento (il figlio ha il codice uguale al padre, ma PID
diverso).
exec( ) - consente l'esecuzione di programmi, quando viene chiamata si ha l'overlaid ossia la memoria utilizzata dal processo
chiamate viene rilasciata al nuovo processo che entra in esecuzione, non vi quindi ritorno al processo chiamante.
- I files aperti dal processo chiamante restano disponibili anche al processo nuovo, tranne che venga disposto il contrario
(SC fcntl).
- I segnali mascherati dal processo chiamante restano mascherati anche al processo nuovo
wait( ) - consente di addormentare un processo fino a che non venga rilasciato un segnale o un figlio termini la sua
esecuzione exit( ) - richiede la terminazione di un processo.
- Esempio
Mentre il figlio in esecuzione il padre "dorme". La shall in un sistema multitasking si mette da parte e lancia in esecuzione
un altro processo (nell'esempio avviene nel figlio in execvp).
main( )
{
FILE *fp;
127 of 138
int pid, pipefds[2];
char *username, *getlogin( );
Prendiamo l'uscita del comando TAR (sullo STANDARD OUTPUT) e la trasferiamo, tramite una pipe (|), a COMPRESS.
COMPRESS comprime i dati e li mette su un file.
- I/0 control
consente la programmazione dei device drivers a seconda della tipologia dei drivers considerati.
128 of 138
{3, SHORTRANGE},
{19, SHORTRANGE},
};
int fd;
struct sigaction action1;
struct itimerval timer1;
float distanza;
void lettsens (int);
void
main (void)
{
int sens;
int schedt;
sens = NUMSENS;
schedt = SCHEDTIME;
if ((fd = open ("/dev/us", O_RDWR)) = = -1 )
{
printf ("Impossibile aprire driver us!\n");
exit (0);
}
prinff ("Driver aperto\n");
printf ("Settaggio num sensori: %d\n", ioctl (fd, TIOSETNUMS, &sens));
printf ("Settaggio parametri sensori: %d\n", ioctl (fd, TIOSETSENS, sensori));
getchar ( );
printf ("Fine acquisizione: %d\n", ioctl (fd, TIOSTOP, 0));
close (fd);
}
void
lettsens (int sig)
{
float valsensori[NUMSENS];
static double lettura = 0.0;
static double somma_misure = 0.0;
static int cnt = 0;
static int i = 0;
129 of 138
Driver
#define MODULE
#include <linux/config.h>
#include <linux/types.h>
#include <linux/module.h>
#include <linux/version.h>
#include <linux/kernel.h>
#include <linux/malloc.h>
#include <linux/sched.h>
#include <linuxltqueue.h>
#include <linux/tty.h>
#include <linux/kd.h>
#include <linux/timer.h>
#include <linuxisignal.h>
#include <linux/errno.h>
#include <linux/ioport.h>
#include <asm/io.h>
#include <asm/segment.h>
#include <asm/system.h>
#include <asm/irq.h>
#include "io.h"
#include "strutture.h"
#include "segnali.h"
#define USDRIVER 55 /* indirizzo chr_dev */
/* Function prototypes*/
int init_module (void);
void cleanup_module (void);
static void USTimer(unsigned long);
static int usread_funct(struct inode *inode, struct file *file, char *buffer, int count)
{
current->timeout = jiffies+3000;
interruptible_sieep_on (&wait_queue);
if (verify_area(VERIFY_WRITE, buffer, count) = = -EFAULT)
{
printk("Errore nella verify_area\n")
return -EFAULT;
}
if (!current->timeout) return -EIO;
memcpy_tofs((float *) buffer,(float *) misure, count);
return(count);
}
130 of 138
wait_queue= NULL;
#ifdef DEBUG
printk("Driver US aperto\n");
#endif
SetMode(2,MOD1 );
WriteBank(2,1 ,RESETSIG);
static int usioctl_funct(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
double PCfact;
switch(cmd) {
131 of 138
(timer_sched != MIDSCHEDTIME) &&
(timer_sched != HISCHEDTIME))
return(WRONGTIME);
currsens = SENSSCHEDA; /* init num sensori per ripartire da 0 */
mytimer.expires = (WAITRESET-1 ) + jiffies; /* alla prima lo waito di WAITRESET */
mytimer.data = STOP_B;
add_timer(&mytimer);
break;
void
cleanup_module (void)
{
printk("USsens unloaded\n");
release_region (base, 8);
unregister_chrdev (USDRIVER, "USDriver");
}
132 of 138
/* lettura risultati contatore e reset scheda */
ReadBank(2,3,&chin2);
#ifdef DEBUG
printk("collect risultati, letti %x %x \n",chin1, chin2);
#endif
133 of 138
/* start nuova acquisizione */
/* nel caso che il t schedulazione e' stretto con long range, viene */
/* raddoppiato, con gli altri ranges e' invariato */
if (a == STOP_C)
{
del_timer(&mytimer);
if((timer_sched = = HISCHEDTIME) && (csensori[currsens].range = = LONGRANGE))
tval = (timer_sched * 2);
else
tval = timer_sched;
ComputeStartSensor(&chsnd);
WriteBank(2,1,chsnd); /* metto in OR l'ottavo bit a 1 e lo ributto giu dopo 1 ms */
#ifdef DEBUG1
printk("Fire sensore %d, spedito %x\n",currsens,chsnd);
#endif
mytimer.expires = jiffies + WAITBL1;
mytimer.data = STOP_D;
add_timer(&mytimer);
return;
}
if(a == STOP_D)
{
del_timer(&mytimer);
WriteBank(2,1,chsnd I LONGRANGE);
mytimer.expires = jiffies + WAITBL2;
mytimer.data = STOP_E;
add_timer(&mytimer);
return;
}
if(a = = STOP_E)
{
del_timer(&mytimer);
WriteBank(2, 1 ,chsnd);
mytimer.expires = tval-WAITBL1-WAITBL2 + jiffies;
mytimer.data = STOP_A;
add_timer(&mytimer);
return;
}
}
Tutte le funzioni vengono eseguite in modalit Kernel, vanno aggiunte una funzione di LOAD e una di UNLOAD in modo
che il driver venga caricato solo quando necessario e non sempre al BOOT del sistema.
134 of 138
Caratteristiche
Three 8-bit pipelined CPUs Selectable input clock rates: 625kHz to 10MHz
On-chip memory 2Kbyte static RAM (Neuron 3150)
11 programmable I/O pins
34 selectable modes of operation
Programmable pull-ups
20mA current sink
Two 16-bit timer/counters for frequency and timer I/O
Applicazioni
Instrumentation
Machine automation
Process control
Diagnostic equipment
Environmental monitoring & control
Power distribution & control
Discrete control
135 of 138
Lighting control
Building automation
Security systems
Robotics
Home automation
Consumer electronics
Automotive electronics
136 of 138
Mappa di memoria Chip Neuron 3150
- Programmazione
Per la programmazione:
- creazione dei costrutti WHEN (programmazione ad eventi): quando succede un evento --> Esegui
- BINDING: associare variabili di rete alle variabili I/O (interfaccia diretta con I/O)
Il sistema , a priori, non real-time (non preemptive): compito del programmatore tendere ad un sistema real-time
137 of 138
Si definiscono diversi livelli di priorit:
- Task Prioritari (si segue la priorit dell'evento: Priority When), in questa categoria vi sono anche i Task di
sistema (Initialization Task, Chip Reset Task)
- Task ordinari (When clause --> Task): eseguo un Round Robin.
- Esempio: vogliamo controllare lo stato di I/O0 (sensore), ad ogni cambiamento si deve accedere una lampadina.
{ {
WHEN (I/O CHANGES ( )) WHEN (NV_UPDATE_OCCURS (NVI_LAMP))
IF I/O0 = = 1 IF NVI_LAMP = = 1
NVO_LAMP = = 1 I/O_OUT (I/O0, 1) /* accendo lampadina */
} }
138 of 138