Você está na página 1de 138

Dispense del corso di Calcolatori Elettronici II

Titolari:
Prof. Tullio Vernazza
Prof. Renato Zaccaria

Versione on line su http://lips.dist.unige.it/corsi/calcolatori2/dispense/

PARTE HARDWARE

Capitolo 1:

Il Bus

Introduzione
Caratteristiche di un bus
Considerazioni macroscopiche sul bus
Elementi definiti dallo standard

Capitolo 2:

Funzioni del Bus

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

Gestione delle interruzioni

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

Politiche di schedulazione Real-Time


Gestione delle priorit
Signals
Shared memory
Clocks e Timers
System calls
Echelon

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).

MultiBus (proposto da Intel)


VME (progettato da Motorola)
IEEE 488 (nato come HPIB)
EtherNet (nato da una proposta Digital - Xerox)

3) Standard "da progetto" o "de iure"

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.

Bus Velocit Indirizzamento Ampiezza dati Lunghezza Terminazione


Globale 32 MB/s 32 bit 32 bit 19" s
Locale alta 32 bit 32 bit piccola no
I/O 3-4 MB/s nessuno 8-16 bit fino a 1 m spesso
Strumenti 1 MB/s nessuno 8 bit decine di metri s
Seriale dipende nessuno 1 bit alla volta fino a 1,5 Km s

Differenza tra bit/s e baud

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!

La codifica NRZ si usa nei bus ma non nelle linee di comunicazione.

Terminazione del bus

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.

Considerazioni macroscopiche sul 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).

Ne esistono due tipi: sincrono e asincrono.

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).

Elementi definiti dallo standard

1) Caratteristiche meccaniche:

Sono quelle relative alla scheda e al connettore.

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:

Specificano i livelli di tensione e corrente dei segnali in gioco. Ad esempio:

Driver:

VIH 2.4 V IOH -24 mA


VIL 0.4 V IOL 48 mA

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.

Le Funzioni del Bus


Trasferimento dati

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:

a) ADDRESS: specificano gli indirizzi che individuano l'unit di I/O;


b) ADDRESS STROBE: comunica se ci che in transito sul bus un segnale valido o meno. Tipicamente, questo segnale
rimane a livello durante l'intera trasmissione.
c) QUALIFIERS: Sono usati soprattutto nei bus grandi. Portano informazioni aggiuntive e logicamente fanno parte
dell'indirizzo. Ad esempio, si possono avere:
- un bit che informa se il programma in esecuzione di sistema (kernel) o no (user) (ci serve essenzialmente per le
protezioni);
- un bit che informa se ci che sta transitando sul bus un dato o un'istruzione;
- alcuni bit che indicano la lunghezza dell'indirizzo (ci pu essere necessario se lo spazio di indirizzamento del bus pi
ampio di quello della CPU);

d) DATA: trasmettono i dati (in genere sono 1,2,4 byte).


e) DATA STROBE: stabiliscono le modalit del trasferimento (un sistema deve poter trasmettere dati di diversa ampiezza:
1, 2, 4 byte). Ciascuno standard adotta un metodo diverso per gestire la trasmissione di quest'informazione.

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:

ENWORD: segnala l'accesso a tutti e quattro i byte della cella;


ENHFWD: segnala l'accesso ai byte di met (sinistra o destra) della cella;
ENBYTE : segnale l'accesso a un singolo byte della cella;
STROBE : d il via alla transazione;
B0 : seleziona il byte in una half word;
B1 : seleziona la half word cui accedere.

La selezione dell'ampiezza avverr tramite logica che implementa le equazioni:

ST0 = STROBE(ENWORD + ENHFWD NOT(B1) + ENBYTENOT(B0) NOT(B1) )


ST1 = STROBE(ENWORD + ENHFWDNOT(B1) + ENBYTEB0NOT(B1) )
ST2 = STROBE(ENWORD + ENHFWDB1 + ENBYTE NOT(BO)B1)
ST3 = STROBE(ENWORD + ENHFWDB1 + ENBYTEB0B1)

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.

Ad esempio, ENWORD = NOT (ENHFWD)NOT (ENBYTE) .

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.

Trasferimento sequenziale (Read-Write sequential)


I singoli accessi in memoria, siano essi in lettura o in scrittura, avvengono come illustrato nelle figure seguenti.

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).

Bus splitting (o slicing)

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.

Test & Set (o Read-Modify)


In questo caso, con un solo indirizzo si effettua un'operazione di lettura seguita da una di scrittura senza rilasciare il bus. Il
ciclo di lettura e scrittura diventa cos un'operazione atomica, semplificando la realizzazione di semafori in sistemi
multiprocessori.

Errori sul bus

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 dello Slave


Si verificano quando si indirizza uno slave che, per qualche motivo, non riesce a svolgere l'operazione richiesta. Sono molto
pi probabili di quelli di trasferimento e molto pi gravi perch difficilmente recuperabili.
Il caso pi frequente quello di errore di parit nella memoria. In tale situazione il sistema si blocca e lo slave genera i
segnali:
- ACK con cui segnala di aver ricevuto la richiesta;
- NACK che indica che la richiesta non pu essere soddisfatta.
In grossi sistemi, dove le percentuali di errori sono maggiori, si utilizzano schede di memoria con controllo di parit.
Ricordiamo, infine, che vi sono soluzioni pi robuste e raffinate che fanno uso di tecniche CRC (Cyclic Redundancy Code)
e ECC (Error Correction Code) all'interno delle schede.

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.

Arbitraggio del BUS

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:

Arbitraggio in daisy chain a controllo decentralizzato


Arbitraggio in daisy chain a controllo centralizzato

a 1 livello
a pi livelli

Arbitraggio a richieste indipendenti a controllo centralizzato


Arbitraggio a richieste indipendenti a controllo distribuito

a priorit decodificata
a priorit non decodificata
serializzato
a linee spezzate

Daisy chain a controllo decentralizzato

Lo schema di principio il seguente:

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.

Questa situazione espressa dal seguente diagramma temporale:

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:

i. l'arbitraggio e la trasmissione dei dati avvengono in successione;


ii. il bus inutilizzato (perch la CPU inattiva) nell'intervallo di tempo che intercorre fra l'accettazione
della richiesta (e, quindi, la segnalazione di GRANT) e l'inizio della transazione, cio nel tempo che
il segnale di GRANT impiega a raggiungere il dispositivo richiedente;
iii. la priorit dei dispositivi fissa e il round robin garantisce un numero di accessi equilibrato solo in
intervalli di tempo molto lunghi e in presenza di molte richieste;
iv. la CPU rischia di rimanere inattiva per lungo tempo perch non possibile selezionare le richieste da

Troviamo un esempio di questa tecnica nella gestione del bus dello Z80.

Daisy chain a controllo centralizzato

Lo schema di principio il seguente:

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.

Il seguente diagramma temporale illustra la successione degli eventi:

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).

Richieste indipendenti a controllo centralizzato

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:

1. eventuali suoi malfunzionamenti pregiudicano l'attivit dell'intero sistema;


2. necessita di scheda dedicata con conseguente aumento dei costi.

Richieste indipendenti a controllo distribuito

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:

CPU 1 CPU 2 CPU 3 ....


000 001 011 ....
001 010 100 ....

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:

i. l'arbitro vincitore porta la propria priorit a 0 ;


ii. tutti gli arbitri con priorit minore di quella del vincitore la aumentano di 1 ;
iii. tutti gli arbitri con priorit maggiore di quella del vincitore la mantengono invariata.

ESEMPIO: Supponiamo la seguente situazione: le richieste vengono inoltrate sempre dalle CPU1-2-3 e CPU1 stata
l'ultima a vincere.

CPU 1 CPU 2 CPU 3 CPU 4 Vincitore


000 001 011 101 3
001 010 000 101 2
010 000 001 101 1

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.

A priorit non decodificata

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:

tutti gli arbitri che vogliono accedere al bus ne fanno richiesta;


per ogni bit a partire dal pi significativo, ciascun arbitro confronter il valore presente sul bus con il proprio: se
diverso ritirer la sua richiesta.

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.

2) Arbitraggio distribuito con struttura a linee spezzate.

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.

La scheda vincitrice avr i Pi interni corrisponderanno ai Pi del bus globale. In formule:

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.

Il procedimento descritto sopra verr formalizzato dalle seguenti equazioni logiche:

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:

1) Cablaggio della priorit sulla scheda.

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).

2) Cablaggio di switch sulla scheda.

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.

3) Caricamento della priorit dal bus.

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.

Gestione delle interruzioni

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.

1) Funzionamento sui fronti

La richiesta alla control unit viene filtrata da un flip-flop come nella figura a sinistra:

Questo metodo usato sui PC che usano il bus AT.


Lo svantaggio di questo metodo che se due richieste si susseguono velocemente, rischiamo di perdere la seconda perch il
secondo fronte arriva mentre il primo interrupt non ancora stato servito (figura sopra a destra) .

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

E' difficile per un interrupt sapere a quale CPU andare.


Possiamo decidere che ciascuna CPU risponda solo ad alcuni livelli di interrupt. Il problema che se si danneggia una CPU,
gli interrupt ad essa inviati vanno persi irrimediabilmente. A questo punto pi semplice vincolare alla CPU anche i
dispositivi che generano quegli interrupt.

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:

i. chiede il controllo del bus (come in una normale richiesta di DMA);


ii. ottiene il controllo del bus;
iii. pone il vettore di interrupt sul bus dati, l'indirizzo della CPU che deve rispondere su CPUADDR e un valore su TAG
per indicare che si tratta di un interrupt .

Caratteristiche critiche di alcuni bus

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

Futurebus 32-64 32-64-128-25 A S 400-3200 Mb/s


SBUS 32 16-32 S N 57 Mb/s
SCI 64 64 minimo A/S S 1 Gbit/nodo

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

Tempi medi di accesso

TA = TA-cache + % Miss TA-Mp + altro

Facciamo un paio di esempi sui vantaggi offerti dalla cache:

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:

CPIeffettivo = 8.5 + 3 * 6 = 26.5 senza cache


CPIeffettivo = 8.5 + 0.11 * 18 = 10.5 con cache

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:

CPIeffettivo = 1.5 + 10 1.4 = 15.5 senza cache


CPIeffettivo = 1.5 + 0.1114 = 3 con cache

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.

Schema di principio e differenti realizzazioni


La cache una memoria associativa e pu essere implementata in diverse maniere.

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:

lettura del dato in Mp;


aggiornamento RAM Tag;
aggiornamento RAM Dati.

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.

Dimensionamento di una cache


La ricerca del miglior rapporto prestazioni/prezzo nel progetto di un sistema si traduce, per quanto riguarda la cache, nel
problema di determinarne le dimensioni pi opportune.

La formula utilizzata per calcolare le dimensioni della cache :

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

Nel caso 2 il clock deve essere pi lento.

i. TES/istr = 1.520 + 0.0391.3200 = 40.1 ns


ii. TES/istr = 1.521.7 + 0.031.3200 = 40.4 ns

Esistono degli effetti collaterali che peggiorano le prestazioni della macchina. Se non aumentiamo il clock bisogna
mettere un wait state:

TES = 1.520 + 1.30.3200 + 1.320 = 66.4 56.1

Scelta della dimensione del set (SET-SIZE PROBLEM)

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.

Scelta della dimensione del blocco (LINE-SIZE PROBLEM)

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.

I vantaggi di utilizzare un blocco invece di una parola sono:

1. diminuzione della capacit della RAM Tag (a parit di capacit complessiva)

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).

Strategie per la lettura


In un'operazione di lettura, la CPU cerca il dato prima nella cache e poi, se la ricerca fallisce, nella Mp. Se il dato presente
nella cache si ha un 'Hit', viceversa si ha un 'Miss', la lettura del dato dalla Mp e la sua copia nella cache.
La lettura dalla Mp avviene solitamente con una delle seguenti tecniche:

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.

Strategie per la scrittura

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.

Copy back (o Write Back)


In caso di modifica, il dato scritto solo nella cache. Esso viene copiato nella Mp quando si sostituisce il blocco oppure
quando il task terminato. Questo sistema decisamente pi complesso ma consente di ottenere prestazioni migliori perch
risparmia accessi nella Mp.
Anche in questo caso c' un buffer per le scritture in attesa ma deve essere pi grande che nel caso precedente sia per quanto
riguarda il taglio (perch si trasferiscono blocchi di dati) che per quanto riguarda il numero di elementi contenuti (perch i
blocchi da trasferire possono essere molti).
Inoltre, il buffer deve consentire di effettuare delle ricerche nei blocchi contenuti al suo interno (nel caso in cui si verifichi
un Miss e sia necessario accedere a un blocco sostituito ma ancora non aggiornato in memoria primaria), quindi molto
complesso.
Il copy back riduce il traffico sul bus. Inoltre, spesso accade che in un programma vi siano variabili che vengono modificate
senza necessit di aggiornarne il valore nella Mp (indici/contatori).

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:

1. allungare la time slice assegnata a ciascun task;


2. aumentare le dimensioni della cache perch possa contenere i dati di pi task;
3. modificare l'algoritmo di scheduling;
4. cercare di ripristinare il contenuto della cache prima di caricare un task;
5. disporre di pi cache (una per task).

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.

Metodi per la conservazione della coerenza dei dati


Snooping

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 il blocco si trova nella cache 1 e pu essere modificato avremo

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.

Miss in fase di lettura

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

poi, dopo la lettura,

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

poi, dopo le operazioni descritte,

Miss in fase di scrittura

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

poi, dopo la lettura,

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

poi, dopo le operazioni descritte,

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.

Metodi di accelerazione della cache


Partizionatura

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

Finora abbiamo visto sistemi strutturati come in figura a).

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.

Stesso indirizzo virtuale corrispondente a indirizzi fisici distinti

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.

Il numero di piani associati sar pari a:

2offset-(b+2)

dove:

offset = bit che identificano il numero di blocchi nel set;


b = bit che identificano il numero di parole nel blocco;
2 = bit che identificano il numero di byte nella parola.

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

4 K = 12 bit - 4 = 8 = 256 set

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.

Viene realizzata con

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.

ESEMPIO: VAX 6000

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.

ESEMPIO: Motorola 88110

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:

1. ridurre i livelli della rete (soluzione costosa)


2. introdurre il meccanismo di pipeline.

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.

La Pipeline in una CPU

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:

e la sua risoluzione consisterebbe nel prolungamento della fase di ID:

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:

Read after Write (RAW)


Write after Read (WAR)
Write after Write (WAW)

Read after write

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.

Vediamo cosa avviene per l'esempio precedente:

La fase di ID va ripetuta perch la fase di prelievo operandi non era stata compiuta.

Compilatore "ad hoc"

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

Cerchiamo ora di eliminare le bolle. Cominciamo a considerare la temporizzazione.

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.

In questo modo, le due istruzioni possono seguirsi senza bolle:

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:

CPIeffettivo = 1 + 0.2 0.5 1 = 1.1

Senza il bypass avremmo invece:

CPIeffettivo = 1 + 0.2 0.5 2 = 1.2

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.

Write after read

Questo tipo di hazard si verifica con istruzioni pi complesse (come quelle dei processori CISC).

ESEMPIO: Istruzioni con autoincremento

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.

ESEMPIO: Se la percentuale di JMP del 30% e il CPI=1, avremo che

CPIeffettivo = 1 + 0.3 3 = 1.9

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:

Branch Target Cache

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:

CPIeff = CPIid + prob. di avere salto prob. di non saltare CK persi =


= 1 + 0.30.31 = 1.09

abbiamo guadagnato un fattore dieci da quando siamo partiti.

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:

1. sospendere le scritture di tutte le istruzioni successive a quella che ha causato l'interrupt


2. salvare PC dell'istruzione che ha generato interrupt (nel nostro caso n+1 LD)
3. lasciar finire le istruzioni precedenti (nel nostro caso ADD), serviamo l'interrupt e ripartiamo dalla n+1.

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.

IF page fault , protezione memoria, errore di trasferimento


ID codice non ammesso
EX errore aritmetico (divisione per 0, overflow della capacit di rappresentazione)
MEM come per IF
WB nessun interrupt

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:

a. annullare tutte le istruzioni in corso e non completate


b. mettere il PC alla prima istruzione non completata,
c. servire l'interrupt,
d. ripartire dall'istruzione non completata

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.

Gestione dei bit di stato (condition code)


In una macchina pipeline c' il rischio che il bit di stato usato per condizionare un salto abbia una validit molto breve,
anche di un solo ciclo di clock.

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.

R1= R2 no! R1= 0 si!

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

Pipeline con doppio tipo di istruzione

Pipeline con test all'interno del JMP

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.

Questo comporta due cose:

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).

2. Usare registri di appoggio (file storico o future file)


vi possono essere istruzioni molto lunghe e quindi il numero dei registri d'appoggio cresce a dismisura per tenere
tutti i dati intermedi prima che l'istruzione finisca. Inoltre, si ripropone il problema del bypass: per poter prelevare
gli eventuali operandi dal register file provvisorio e non dal register file vero esso deve essere realizzato tra tutti i
registri d'appoggio e le varie unit (cosa molto laboriosa). Una piccola differenza nella realizzazione del bypass
che qui non dobbiamo considerare la terminazione della singola istruzione ma la terminazione di tutte le istruzioni
precedenti. Finch abbiamo operazioni in corso, non possiamo aggiornare il register file e quindi il funzionamento
meno regolare.

3. Rendere precisi gli interrupt via software


l'interrupt, di simulare le istruzioni rimaste in sospeso, fornire i risultati che queste avrebbero generato e riprendere
dall'istruzione che ha generato l'interrupt. La sequenza delle operazioni che il s.o. deve fare, nel nostro esempio
- servire l'interrupt - eseguire DIV - caricare F0 - ripartire da SUB
Non banale eseguire le istruzioni del programma via software, perch il s.o. deve estrarle da esso e fermarsi subito
dopo la loro esecuzione (se le istruzioni sono molte, la cosa si complica notevolmente). Per semplificare il lavoro del
s.o., si pu decidere di limitare la sovrapposizione delle operazioni FP, in modo che le operazioni da eseguire siano
poche. Ad esempio, si pu limitare a 2, il numero di operazioni FP che si sovrappongono (soluzione adottata dallo
SPARC). In questo modo l'uso delle unit multiple non ottimale (se ne usano 2 su 3), per l'unica istruzione che
pu non terminare quella che ha chiesto l'interrupt, perch la successiva gi terminata (se la prima gi finita e
l'interrupt viene dalla successiva non ci sono problemi).

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).

ESEMPIO: Consideriamo la sequenza di istruzioni seguente:

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.

Dal punto di vista dei blocchi del sistema avremo

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:

verifica dell'esistenza di un'unit libera cui assegnare l'istruzione


controllo degli operandi

Rimangono i problemi del WAW e del WAR.


Il WAW eliminabile o eliminando una scrittura o bloccando la macchina quando si accorge del data hazard. Comunque,
un hazard che avviene raramente e la penalizzazione minima.
Il WAR si risolve decidendo che quando l'istruzione assegnata all'unit floating point, il prelievo degli operandi
disponibili fatto subito, senza aspettare l'esecuzione effettiva. Ad esempio, ADD preleva subito F7 quando l'istruzione
passata all'unit mentre F0 viene mandato dopo; ADD aspetta ma non ho pi il data hazard perch l'operando preso
quando metto in esecuzione l'istruzione e non quando l'istruzione eseguita.
La gestione di tutti i data hazard con schedulazione dinamica complessa; il pipeline interlock non riesce pi a gestirla e
occorrono nuove tecniche.

Tecnica dello Scoreboard

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:

Tabella dello stato delle istruzioni


Contiene informazioni sullo stato di ciascuna istruzione:
- se stata mandata in esecuzione, cio se ha superato il primo pezzo della fase di decodifica;
- se ha gli operandi, cio se ha passato il secondo pezzo della fase di decodifica;
- se stata eseguita;
- se ha scritto i risultati;
Supponiamo che sia stata eseguita la prima istruzione e riempiamo la tabella. Sono messe in esecuzione tutte le
istruzioni tranne ADD (perch avrebbe bisogno dell'unit FP utilizzata da SUB). Man mano che si va avanti, le righe
completate (corrispondenti a istruzioni completate) vengono eliminate per avere spazio nella tabella.

Tabella dello stato delle unit aritmetiche


Contiene informazioni sullo stato di ciascuna unit aritmetica:
- numero dell'unit;
- se occupata o meno;
- funzione occupante;
- qual' la destinazione, cio dove va a finire il risultato;
- quali sono gli operandi;

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.

Tabella dello stato dei registri


Serve a tener conto della disponibilit degli operandi a livello del register file. Se il registro disponibile inserito
null (-), altrimenti c' il numero dell'unit da cui deve provenire il valore.

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.

Gestione dei salti


Volendo ottimizzare le prestazioni si pu prevedere qualche meccanismo per anticipare i salti.

Branch prediction buffer


Ha senso solo nel caso in cui il calcolo della condizione all'interno dell'istruzione di salto (prima si calcola dove saltare e
poi si valuta la condizione).
Il BPB una memoria RAM che viene indirizzata con i bit meno significativi del PC. Essa contiene i dati sull'esito di tutti
gli ultimi salti condizionati incontrati. In base a tali dati si pu fare una previsione (corretta in un'alta percentuale dei casi)
sull'effettuazione o meno dei salti successivi. Il discorso appare ragionevole se pensiamo che, ad esempio, in un loop
molto probabile che si ritorni allo stesso indirizzo e che la condizione di salto sia verificata.
Tralasciamo per il momento il fatto che (poich la RAM viene indirizzata con i bit meno significativi dell'indirizzo) ad una
stessa cella del BPB fanno riferimento pi indirizzi destinazione e analizziamone il funzionamento.
Ogni volta che viene incontrato un nuovo salto (miss nel BPB) se ne inserisce l'esito (cio la valutazione della relativa
condizione) nella cella a lui riservata.
Se invece il salto gi stato effettuato (hit in BPB) allora si va a guardare nella cella relativa e in base alla condizione che si
legge, si decide se il salto va o meno effettuato.
Nel caso che la previsione risulti errata, si deve aggiornare la RAM ed eseguire le istruzioni giuste, se va bene risparmiamo
delle bolle.
Osserviamo che in un loop in cui il salto viene effettuato 9 volte su 10, la previsione esatta 8 volte su 10 perch l'unica
volta che il salto non viene fatto causa di due situazioni di errore: la prima quando il salto non da fare (e viene cambiata
la condizione), la seconda al salto successivo, che da fare ma trovo in RAM che non da fare per l'aggiornamento
precedente.
Per rimediare a tale situazione, si possono usare 2 bit di informazione e gestirli tramite una specie di macchina a stati.

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:

0.9 x 0.9 + 0.1 x 0.5 = 0.86

Branch target cache

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:

0.9 x 0.1 x 2 + 0.1 x 0.7 x 2 = 0.18 + 0.14 = 0.32

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:

0.9 x 0.1 x 1 + 0.1 x 0.7 x 2 = 0.09 + 0.14 = 0.23

La tecnica del doppio fetch usata dagli ultimi modelli di SPARC (processori usati da SUN); ad esempio, nella SPARC 10.

L'unrolling dei Loop


Vediamo subito un esempio di unrolling su una macchina normale. Consideriamo un loop che sommi a tutti gli elementi di
un vettore una costante e valutiamo quante bolle vi sono.

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:

1. una prima parte che tratta i resti


2. una seconda parte che un multiplo della sequenza base

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:

1. una sequenza per gestire il resto di n/k


2.

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

Il concetto di evento nasce con Unix intorno agli anni '70.

64 of 138
Glossario:

EVENTI Trap: primo esempio di


SIGNAL Interrupt Software in
ECCEZIONE Assembly.
TRAP Trap e Interrupt Software
"INTERRUPT SOFTWARE" sono termini "parenti".

- Evento o Signal

E' importante premettere che in Unix i processi di un utente sono gerarchici:

Semantica di creazione di processi


in Unix (ad albero).

Un evento viene:

- generato (lanciato)
- propagato (da chi e come)
- trattato (gestione dell'evento)

ed ha:

- un effetto

Gli eventi elementari sono un SET, ad esempio numerati (1..N).

- Generazione degli eventi

gli eventi sono generati da:

65 of 138
- un "evento H/W" (ad esempio un tasto di interruzione come ^c)

- Propagazione degli eventi

verso il processo padre. Di solito, "trattare" un evento significa arrestarne la


propagazione
- H/W: simile, partendo dal processo che lo ha originato

- 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).

- Effetto degli eventi

Set di eventi tipico:

E0, E1, E2, ..., E9, E10, ...

Un evento pu essere sempre trattato, altrimenti posso distinguere:

- eventi che, se non trattati, provocano la morte del processo (C)


- eventi che, se non trattati, non hanno effetto (B)
- eventi intrattabili che provocano la morte del processo (CC)

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.

EVENT DRIVEN (evento: messaggio asincrono senza conten


inoltrato in tempo reale)

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:

Breve storia dello sviluppo H/W e S/W secondo Brinch Hansen


55-60 : Comincia lo sviluppo H/W
60-65 : Crisi del S/W, la mancanza di standard e modelli rende i primi S.O. altamente instabili
65-70 : Nascono i primi modelli concettuali di base per scrivere S.O.
70-75 : Sulla base dei modelli nascono i linguaggi
75-80 : Si perfezionano i linguaggi
80-2000 : Nuovo sviluppo H/W
2000- : Comprensione dei sistemi distribuiti

Programmazione concorrente

Def. Programma concorrente: un programma concorrente composto da n programmi sequenziali ("processi") che vengono
eseguiti con velocit vi (i = 1N), questi:

comunicano tra loro


accedono a risorse in comune (risorse "passive" : periferiche, celle di memoria e non CPU).

Le vi possono essere qualsiasi, ma devono essere positive: vi>0.


Il risultato del programma lo stesso a prescindere dalle diverse vi dei singoli processi .
Un programma concorrente pu essere implementato in parallelo (pi CPU) o no (una CPU con un algoritmo di scheduling
di processi); se no, il suo risultato indipendente dal tipo di scheduling (preemptive, non preemptive, fifo, round robin

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:

non entra mai in deadlock ("abbraccio mortale", stallo)


rispetta vincoli temporali nell'elaborazione (mi aspetto una risposta entro un tempo stabilito)

Minime primitive per esprimere qualunque algoritmo concorrente

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:

1. inizializzazione a un valore non negativo

S : = initial value

2. wait (s) {o P(s)}

IF s <> 0 THEN s : = s-1

else block process

3. signal (s) {o V(s)}

68 of 138
IF queue (coda) empty THEN s : = s+1

else free process

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:

Value(s) = initial value(s) + number of signal(s) - number of completed wait(s)

Con evidenza value(s) non potr mai essere negativo: iv(s) + ns(s) - nw(s) >= 0

Mutua esclusione attraverso semafori

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)

Un processo alla volta esegue S1 Sn e accede alla regione critica

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.

Sincronizzazione attraverso semafori

Il processo attende il segnale [P(b)] di un altro per proseguire.

[P(a)]
S1
S2
.
P(b)
.
Sn
[V(a)]

Esempio1 Problema produttore/consumatore

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

I due tipi di processi possono essere implementati usando due semafori:

p che indica una posizione libera nel buffer ed inizializzato a N


c che indica un dato disponibile nel buffer ed inizializzato a 0

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

E' facile notare che:

1. Numero di "items" nel buffer >= ns(c) - nw(c) = - iv(c) = 0


2. Numero di "items" nel buffer <= nw(p) - ns(p) = iv(p) = N

Le regole di comunicazione tra i due processi sono rispettate.

Esempio2 Problema Lettori/Scrittori

Consideriamo il problema in questi termini:

a. diversi processi concorrenti desiderano accedere ad una risorsa (file) comune


b. alcuni desiderano leggere, altri desiderano scrivere
c. sono permessi accessi condivisi per i lettori, mentre gli scrittori devono avere accessi esclusivi

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).

Possiamo considerare due casi topici:

a. i lettori hanno "priorit" (in senso qualitativo) sugli scrittori


b. gli scrittori hanno "priorit" (in senso qualitativo) sui lettori

Il problema tipico basti pensare ad un sistema di prenotazione dei posti per aerei, treni

70 of 138
a) "Priorit" dei lettori

Questa soluzione del problema implica che:

i. ogni Reader che chiede di entrare accettato;


ii. anche un solo Reader inibisce i Writers;
iii. il primo Writers che entra inibisce ogni altro R/W

L'accesso esclusivo alla risorsa pu essere realizzato con un singolo semaforo w

Scrittore

wait (w)

Esclusive access to alter the file [Regione critica]

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)

read the file (risorsa condivisa).

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")

b) "Priorit" degli scrittori

Questa soluzione del problema implica che:

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.

In questo caso il rispetto delle specifiche assicurato da:

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)

write to the file [Regione critica (accesso alla risorsa)]

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 (z) z indica la sincronizzazione con qualche altro processo

wait (r) stop alla lettura

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)

Vediamo la casistica delle attese in "coda"

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.

Assunzioni e problemi in programmazione concorrente:

a) finite progress (time-out)


b) fairness
c) no deadlocks
d) no busy waiting
e) no starvation

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.

b) Fairness: nessun processo deve essere ritardato indefinitamente:

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

E' un concetto pi astratto (pi ad alto livello) del semaforo.


Gestisce contemporaneamente i due compiti che svolge il semaforo:

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.

- Regioni critiche (semafori)


- Monitor
- Messaggi (mail box)
- Memoria condivisa
- RPC (Remote Procedure Call)

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.

Altri concetti importanti sono:

- 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).

E' caratterizzato da:

- 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:

Bounded buffer monitor:

74 of 138
Dove:

P = Processi

M = Monitor (un solo processo alla volta pu eseguire una procedura dentro un monitor)

C = classe (dato astratto senza la semantica dell'accesso concorrente)

type buffer(T)= 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

- "Assiomi" del Monitor

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]).

Si definiscono due tipi di code:

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)

Primitive per il controllo dello scheduler:

- 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;

- PG pu essere in competizione con: code a breve termine, altri Pi riattivati.

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
....

procedura k(...){ P33 era sospeso


continue (q3);
... P11 era sospeso
continue (q1);
... Esecuzione di P*
... P12 era sospeso
continue (q2);
...
}

1 e P2 hanno chiamato la procedura "c" e


attendono in coda. Coda possibile:

[H] P2 ; P1 ; P33 ; P11 [T]

Quindi la coda a breve termine , in generale, estesa a tutti i Pi


Real Time, primitive per controllare le code a breve termine.

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)".

Torniamo all'esempio 1: implementazione di un "bounded buffer" in Concurrent Pascal

type BOUNDEDBUFFER = monitor


const LEN = 10;
type BUFFERDEF = array [ 1..LEN] of DATA;
var BUFFER : BUFFERDEF;
INPTR,OUTPTR,SIZE : INTEGER;
SENDER,RECEIVER : queue;

procedure entry PUT(x : DATA);


begin
if SIZE = LEN then delay(SENDER)
BUFFER[INPTR] := X;
INPTR := (INPTR mod LEN) +1;
SIZE := SIZE +1;

77 of 138
continue(RECEIVER);
end;

procedure entry GET( var x : DATA) ;


begin
if SIZE=0 then delay(RECEIVER);
x := BUFFER[OUTPTR];
OUTPTR := (OUTPTR mod LEN) + 1;
SIZE := SIZE -1;
continue (SENDER);
end;

begin
INPTR := 1;
OUTPTR := 1;
SIZE := 0
end;

La variabile buffer [array (1..10) of DATA] simula un array circolare:

La variabile SIZE indica il numero di dati nell'array.


Ogni "produttore" scrive da INPTR in avanti usando la procedura entry PUT (incremento SIZE); ogni "consumatore"
consuma da OUTPTR in avanti usando la procedura entry GET (decremento SIZE).
Le variabili condivise sono quindi SIZE, BUFFER, INPTR, OUTPTR.

Assiomi dei processi:

- ogni Pi ciclico;
- la sua velocit ignota, ma mediamente > 0 in tempo infinito;

- i suoi dati non sono accessibili dall'esterno;


- effettua chiamate a entry di Monitor.

- Messaggi

Servono a "portare a zero" il codice delle procedure del Monitor (rendo "standard" l'accesso al Monitor).

P1 "deposita" il dato per P2 ("send", "write");


P2 "preleva il dato ("receive", "read");
eseguo quindi uno scambio di MESSAGGIO.

COMUNICAZIONE INTERPROCESSO

78 of 138
E' evidente, per che, con l'uso dei messaggi, perdo la sincronizzazione del Monitor; si definiscono quindi:

- messaggi sincroni, bloccanti


- messaggi asincroni, non bloccanti
l'operazione terminata.

Definizioni conseguenti sono:

- read/write (parzialmente) anonime: tipicamente si ha lettura anonima.

Esempio 1: blocking message (messaggi sincroni, bloccanti)

P1
... P2
dato x ...
... ...
write (x,P2) read (y,P1) (+)
(*) ...
... ...

Supponiamo che arrivi per prima l'istruzione write di P1:

Supponiamo che arrivi per prima l'istruzione read di P2:

E' l'equivalente di uno schedulig a lungo termine in un monitor.

- 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).

Esempio 2: non blocking message (messaggi asincroni, non bloccanti)

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}

Vediamo alcuni importanti concetti:

- trap, interrupt S/W


- signal
- eventi
- compliting routine

P1::
{
...
dato (x)
...
write (x, P2, CR) CR: interrupt software o "trap" (evento puro, non dato)
...
}

CR ( )
{
...
return
} [SIGNAL HANDLER (in generale)]

IL "trap":

- Non richiede busy waiting


- Non richiede scheduling esplicito
- E' efficiente
- E' potente e generale
- E', per, molto de-strutturato!

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:

Supponiamo che P4 legga da tutti i P1..3, il programma sarebbe:

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].

- CSP (Hoare 1978)

Communicating Sequential Processes: linguaggio (formalismo) di programmazione per la comunicazione interprocesso.

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:

a) alleggerire la macchina che ospita il processo (evito busy waiting)


b) evitare possibili deadlock

In alternativa un'altra strada (non CSP) poteva essere quella di usare dei TIMER: rischedulo il processo con tempi fissi.

- I/O nelle guardie

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).

Un processo (CSP) termina se:

1) tutte le guardie sono false [senza contenere "?" e "!"]


2) un processo con cui comunica termina

Data una catena Processo-Messaggio-Guardia:

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)
[]
...
]

P offre dei servizi a Q: P = server, Q = client.


funzioni (pattern matching): tipizzazione dei
messaggi.

Caso particolare:
P::
*[
x ? insert(n) --> INSERT // inserire un dato
[]
x ? has(n) --> [SEARCH ; x ! i] // ritrovare un dato
]

Il processo x:: pu inserire o cercare:

x::
[
P ? insert(121)
...
P ? has(13) ; P ? k
]

La politica di scambio tra due o pi processi (Blocco/Sincronia + Simmetria) chiamata protocollo.

- 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).

- The Dining Philosophers (G. W. Dijkstra)

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":

. tutti i filosofi hanno pari diritto


. un filosofo F* ha priorit
- se ci sono diversi filosofi in attesa, F* vince
- F* fa alzare un altro
- F* deve mangiare entro T* secondi (Real-Time)
- ...

. mangia prima chi sar pi svelto


. ...
Il tutto strutturato su tre processi: PHIL, FORK e ROOM (tiene conto del numero di occupanti).
Supponiamo inoltre che ogni filosofo prenda (o cerchi di prendere) le due forchette a lui contigue.

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( );
]

Naturalmente, cos come rappresentato, il sistema non esente da deadlock.


Vi un problema di temporizzazione, infatti se tutti i filosofi prendessero le forchette contemporaneamente si andrebbe in
stallo.
Fortunatamente in questo caso basterebbe rompere la simmetria: per esempio impedendo l'ingresso di tutti i filosofi nella
stanza.

- RPC: Remote Procedure Call (da ADA, in particolare "Tasking")

Partiamo da ADA (il tasking circa equivalente al processo)

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:

- Condivisione: "alla Monitor"

Il task che tiene la procedura condivisa fa accedere ai suoi dati tramite la


procedura stessa.

- Sincronizzazione

effettuato la RPC e il task che ha la procedura condivisa ha effettuato l'accept


- l'accept permette scambio dati sia verso la procedura p( ) sia da p( ) verso il
chiamante, perch segue le regole di attivazione delle procedure (sia ? che ! del
CSP)

tornando all'esempio sopra descritto, se T3 e T1 arrivano alla chiamata di p( )

- i task che eseguono "accept" sono sospesi in attesa di chiamata alla propria

I/O)

- Estensioni

- priorit dei task sulle code "accept"

88 of 138
- controllo lunghezza code
- rimaneggiamenti code
- ...

Come controllo la sincronizzazione?

- non ho delay/continue o simili (scheduling esplicito)


- uso le guardie (come CSP)

Non determinismo:

- Rendez-Vous (Comunicazione asimmetrica)


- Guardie e alternative

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)
...

Valgono le regole per le guardie gi viste per il CSP.


Nell'esempio precedente il codice "condiviso" (la Remote Procedure) dalla put( )/get( ) il minimo possibile: non ingloba la
gestione puntatori che lasciata al task BUFFER (il server). E' perci possibile "sganciare" il client appena termina la
necessit di sincronizzazione: ci migliora efficienza e leggibilit (tutto ci non avveniva con i Monitor, accadeva con i

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
...

- Esempio 3 (Priorit scrittori)

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
}
}

Le impostazioni viste negli esempi 2 e 3 soddisfano solo:

- mutua esclusione
- assenza di deadlock

Per risolvere anche il problema della starvation


su "write" e "read" siano statisticamente equilibrate e non crescano oltre una certa lunghezza L.
Abbiamo quindi visto le seguenti regole:

- i Readers non sono mutualmente esclusivi, i Writers s


- in presenza di soli Readers: libert di accesso (Algoritmo 1)
- in presenza di soli Readers e un Writer, il Writer blocca i Readers (Algoritmo 2)
- in presenza di molti Readers e molti Writers deve attivarsi un algoritmo di scheduling "fair" fra Readers e
Writers (per esempio attraverso una serializzazione).

- Esempio 3 (semplice serializzazione FIFO)

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:

Exit una primitiva molto potente, infatti


presuppone l'accesso alla lunghezza delle
code.
select
accept start.read ( ) IF coda.p ( ) > 0 || coda.q ( ) > 0 THEN
... {
else exit select
...
end select
}

Il costrutto appena visto molto specifico ed equivale a:

IF <coda su start.read ( ) > 0> THEN


accept start.read ( )

- Schedulazione Real-Time

Noi ci occupiamo di programmazione Real Time ad "alto livello", cio seguendo le seguenti specifiche:

- scheduling non pre-emptive: uso primitive esplicite


- processi ciclici
- nessun problema di resource-sharing

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

Dopo i correttivi, gestendo adeguatamente le problematiche di comunicazione e sincronizzazione, posso tornare ad


occuparmi esclusivamente di programmazione ad "alto livello".

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:

Posso usare due tipi di politiche di acquisizione:

- ON DEMAND: solo in presenza di dati eseguo l'elaborazione

- CICLICA: con un'operazione di monitoraggio prelevo ciclicamente i dati e, se sono validi, li elaboro.

Vediamo in dettaglio la politica di acquisizione CICLICA.


Ogni processo deve svolgere le sue funzioni impegnando una percentuale piccola del tempo ciclico disponibile per tutti i
processi: se ci non avvenisse il ritardo dei processi non sarebbe pi gestibile.

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

Dal punto di vista dello scheduler un esempio rappresentato in figura:

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.

Abbiamo diversi processi (tasks):


algoritmo sequenziale, ciclico
T1 e non pre-emptable
di Input dati
...
T4
...
Tn
posso vederli come parti di codice separate all'interno dello stesso processo:

Process P1:: Process P2::


} }
while TRUE { while TRUE {
< cod. rescheduling> < cod. rescheduling>
... ...
<cod. 1> <cod. 2>
... ...
} }
} }

o come implementazione di un unico algoritmo sequenziale, ciclico, non preemptive:

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 ...

- Scheduling mediante monitor:

Un importante regola empirica in sistemi a Monitor :

- minimizzare le code a breve termine

Rifacendoci all'esempio vediamo due casi:

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.

- Schema a memoria condivisa di un generico controllore

Monitor:

- FIFO con coda circolare (produce ritardo)


- FIFO "sincroni" (coda di un posto) [nessun ritardo]
- perdita accettata di dati: Pinput e Poutput comunque garantiscono t1 e t2.
Pcontrollo pu perdere dati da Pinput e/o verso Poutput.

- Esempio (produttore-consumatore)

Ho un buffer in input e molti output:

- scritture singole e letture multiple

97 of 138
uslm: MONITOR
VAR reader, writer: QUEUE;
buf: "qualcosa";
first, full: BOOL;

PROC ENTRY PUT (dato: "qualcosa")


IF full THEN DELAY (writer)
buf : = dato;
full : = TRUE;
first : = FALSE;
CONTINUE (reader)

PROC ENTRY GET (VAR dato: "qualcosa")


IF first THEN DELAY (reader)
dato : = buf;
full : = FALSE;
CONTINUE (writer)
END MONITOR

/* costruttore del monitor */


BEGIN
first : = TRUE;
full : = FALSE;
END

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).

- letture singole e scritture multiple:

msls: MONITOR
VAR reader: QUEUE;
buf: "qualcosa";
empty: BOOL;

PROC ENTRY PUT (dato: "qualcosa")


buf : = dato;
empty : = FALSE;
CONTINUE (reader)

PROC ENTRY GET (VAR dato: "qualcosa")


IF empty THEN DELAY (reader)
dato : = buf;
empty : = TRUE;
END MONITOR

/* costruttore del monitor */


BEGIN
empty : = TRUE;
END

98 of 138
Torniamo all'esempio:

- Programmazione in Concurrent Pascal:

- 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.

type timetable = monitor(waiting: tasEqueue)


A timetable needs access to the task queue in which task processes are wait ing to be resumed. Initially, all tasks are
inactive.

procedure start(task: processindex; time: real)


Makes a task active and defines its start time.

procedure period(task: processindex; time: real)


Defines the period of a task.

procedure stop(task: processindex)


Makes a task inactive.

procedure examine(time: real)


Examines all active tasks and resumes them if the current time equals or exceeds their start times. When a task is resumed
its
start time is incremented by its period (module midnight).

IMPLEMENTATION:

type taskschedule = record


active: boolean;
start, period: real
end;

type timetable = monitor(waiting: taskqueue);


var table: array (.processindex.) of taskschedule;

procedure initialize;
var task: processindex;
begin
for task:= 1 to processcount do
table(.task.).active:= false;
end;

function reached(time, start: real): boolean;


var diff: real;
begin
cliff:= time - start;
if abs(diff) >= halfday
then reached:= (cliff < 0.0)
else reached:= (cliff >= 0.0);
end;

procedure entry start(task: processindex; time: real);


begin
with table(.task.) do
begin active:= true; start:= time
end;
end;

99 of 138
procedure entry period(task: processindex; time: real);
begin
table(.task.).period:= time
end;

procedure entry stop(task: processindex);


begin
table(.task.).active:= false
end;

procedure entry examine(time: real);


var task: processindex;
begin
for task:= 1 to processcount do
with table(.task.) do
if active then if reached(time, start) do
begin
waiting.resume(task); start:= start + period;
if start >= oneday then start:= start - oneday;
end;
end;
begin initialize end;

- Clock Process

A clock process increments a clock every second and examines a timetable of task processes waiting to be resumed.

type clockprocess = process(watch: clock; schedule: timetable)

A clock process needs access to a clock and a timetable.

IMPLEMENTATION:

The standard procedure wait delays the calling process for 1 sec.

type clockprocess = process(watch: clock; schedule: timetable);


begin
with watch, schedule do
cycle wait; tick; examine(value) end;
end;

- 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.

time(hour: min :sec)


Sets the current time.

The arguments of these commands are of the following types

task: identifier; hour: 0..23; min, sec: 0..59;

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.

- Scheduling mediante "messaggi puri":

Supponiamo per semplicit che ci sia un unico processo da schedulare:

- 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
]

1- Struttura logica di TQ (nel caso di pi tasks)

In questo caso non fa altro che realizzare la sequenzializzazione dei processi:

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
]
]

2- Struttura logica di TQ (nel caso di pi tasks)

In questo caso la struttura migliore in quanto asincrona:

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:

a) Unix RT (Posix) [in particolare Linux, OSF]


b) Echelon [Processore Bus per applicazioni distribuite: non preemptive, ma permette di fare Real-Time]

Elementi comuni dei sistemi Real-Time:

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

Tools comuni dei sistemi Real-Time

1. Modalit di Controllo della Schedulazione


2. Real-Time Timers e Utilizzo
3. Lock e Gestione della Memoria
4. I/O Asincrono e Segnali
5. Memoria Condivisa

102 of 138
6. Semafori
7. IPC

Ci che serve sapere per fare Real-Time

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.

E' necessario che gli ingressi riescano ad influenzare


le uscite

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:

Sistemi di misura per esperimenti di fisica delle particelle (1 ms)


. Sistemi per il monitoraggio di satelliti metereologici (1-2 min)
. Controllo di un missile a ricerca automatica del bersaglio (1 sec)
. Sistemi di irrigazione automatica, controllati con sonde di temperatura, umidit ecc. (5-15 min.)
Ampio utilizzo di sistemi real-time viene fatto nelle seguenti aree:

. Controllo di processo
. Shop floor automation
. Robotica
. Simulazione
. Acquisizione dati
. Image processing
. Sistemi di test
. Sintetizzatori musicali
. Tests sperimentali
Vediamo alcune definizioni:

- Applicazioni Real -Time "rigide"

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.

- Applicazioni Real-Time "soft"

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.

- Prestazioni de sistemi real-time

Spesso i sistemi real-time richiedono:

1. Alto throughput di dati di ingresso ed uscita


2. Basso tempo di risposta ad eventi asincroni
3. Alta capacit di storaggio di dati (sistemi di acquisizione)
4. Processamento di dati (input --> elaborazione --> uscita) in tempo prestabilito

L'alto throughput tipico del signal processing

. Analisi sonar e radar


. Telemetria
. Analisi di vibrazioni
. Riconoscimento della voce e analisi
. Sintesi musicale
Problematiche che richiedono acquisizione di dati con campionamento alto ed estremamente preciso sono

. Cromatografia di gas e liquidi


Colorimetria

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.

Unix (miglior real-time nell'ordine dei 10


poich dotato di 2 livelli di funzionamento:

- modo user
- modo kernel

In modalit user vengono eseguite:

chiamate ad utility
funzioni di libreria
altre funzioni

In modalit user il processo pu essere in ogni istante interrotto (preemption) da processi pi prioritari.

In modalit kernel vengono eseguite le system calls


essere interrotto fino al termine dell'esecuzione della system call chiamata.
In pratica i sistemi real-time si distinguono dagli altri a seconda se sono con kernel premptive o kernel non preemptive.

- Unix con kernel non preemptive

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.) .

- Unix con kernel preemptive

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.).

- Diagramma delle transizioni di Unix

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:

1. qualcuno lo mette in sleep (4)


2. si autosospende perch mancano dati (6)
oppure si interrompe da solo e si tiene pronto a ripartire non appena gli arriva la risorsa.

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).

- Diagramma dei tempi di latenza

Tempo di latenza non preemptive (latenza variabile)

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

OSF fornisce due tecniche di schedulazione:

schedulazione nice (Time sharing)


schedulazione real-time (supportata dallo standard POSIX 1003.4)

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.

Entriamo nel dettaglio:

- Politiche di schedulazione Real-Time

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:

1. Semafori (vd. Programmazione Concorrente)


2. Memoria condivisa (va protetta con semafori)
3. Canali di comunicazione

(ne esiste una per


qualsiasi processo: gli
3.1 Code di messaggi altri processi possono
mettere dei messaggi in
coda)
modalit di
comunicazione a
3.2 Sockets
pacchetto (con canale,
senza canale)

La schedulazione in Posix fatta per blocchi (threads


differenti blocchi da schedulare con politiche o priorit differenti.
Threads: il kerrel real-time di OSF supporta pthreads (basato su POSIX 1003.4).
Da notare che questo documento d posix e ancora in stato di draft e quindi suscettibile di cambiamenti.
Pthreads e utile per il controllo di devices lente come dischi, reti terminali e stampanti.
Le applicazioni multithread sono utili quando si devono attendere dati da devices esterne.
Un thread lo si crea tramite pthread_create. Un thread pu essere definito come un flusso sequenziale all'interno di una
applicazione, in pratica l'esecuzione di una routine con tutte le relative esecuzioni innestate.
Ogni thread ha il suo identificatore, la sua politica di schedulazione e la relativa priorit, sono inoltre caratteristiche del
thread le
risorse richieste, dati e dimensione di stack.
Alla creazione un thread eredita le caratteristiche dal padre, in caso che si voglia un thread che nasca con caratteristiche
differenti necessario disabilitare il meccanismo di eredit usando pthread_attr_setinheritsched, quindi sar necessario
settare le nuove caratteristiche tramite pthread_attr_ setsched, pthread_attr_setprio.
I thread vengono creati pronti e quindi eseguiti immediatamente, nel caso che abbia una priorit pari a quella del processo in

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.

- il processo in esecuzione entra in stato di runnable o waiting


- un processo a priorit pi alta diventa runnable
- un processo cambia la politica di schedulazione

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:

- Time sharing (schedulazione basata sui nice)


- Schedulazione real-time (supportata dallo standard POSIX 1003.4)

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

Le priorit vengono modificate dallo schedulatore solo in caso di schedulazione time-sharing.


Le due interfacce (real-time, nice) sono completamente indipendenti, un processo real-time non reagisce al nice e viceversa.

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.

- Gestione delle priorit

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:

SCHED_PRIO_USER_MIN --> SCHED_PRIO_USER_MAX: sono le priorit utilizzate dai processi non


real-time o processi a bassissima priorit (corrispondenti ai processi utente tradizionali), nella scale delle
priorit real-time vanno da 0 a 19 che corrisponde alle priorit 20 - 0 della schedulazione time-sharing.
SCHED_PRIO_SYSTEM_MIN --> SCHED_PRIO_SYSTEM_MAX: sono le priorit assegnate ai processi
di sistema, nella scala realtime vanno da 20 a 31 e nella scala time-sharing da -1 a -20. Tramite
schedulazione tradizionale si pu arrivare solo a 29 del real-time.
SCHED_PRIO_RT_MIN --> SCHED_PRIO_RT_MAX: vanno da 32 a 63, i processi real-time alla loro
creazione hanno priorit 19 (RT).

- 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

- Configurazione priorit real-time

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.

Le priorit possono essere modificate utilizzando i seguenti sistemi:

stabilendo la priorit di default all'interno delle priorit assolute


utilizzando le funzioni sched_set_priority_max e sched_set_priority_min
utilizzando un file di inzializzazione .rc che modifica la priorit di default, o utilizzando variabili di
environment
modificando la priorit in fase di inizializzazione tramite sched_setparam.

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)

Che quindi possono essere utilizzate per riferimento.


E' consigliabile il debugging delle applicazioni a priorit tradizionali (dove possibile) poich buona parte dei bachi del
programma possono portare il sistema al crash.

- Funzioni per schedulazione

sched_getscheduler(pid): ritorna la politica di schedulazione di un processo (pid)


sched_getparam(pid, &param): ritorna la priorit di schedulazione di un processo (pid)
sched_get_priority_max(policy): ritorna la massima priorit permessa all'interno di una politica di schedulazone (policy)
sched_get_priority_min(policy)
sched_get_ rr_interval(pid): ritorna il quanto di tempo corrente nella schedulazione round-robin
sched setscheduler(pid, policy, &param)
sched_setparam(pid, &param): stabilisce la priorit di schedulazione di un processo
sched_yield( ): forza il passaggio dell'esecuzione ad un altro processo con la stessa priorit. E' una funzione che modifica la
schedulazione, ponendo il processo chiamante in stato di runnable, mettendolo in fondo alla lista dei processi con la stessa
priorit e quindi mettendo in esecuzione il processo con stessa priorit in cima alla lista. Questo serve nella politica FIFO,
poich in Real Time automatico al termine del quanto di tempo.

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.

Sigaction definita in signal.h e contiene le variabili:


int sig_ev_signo;
union sigval sigev_value;
la sigval contiene inoltre i seguenti membri:
int sival_int;
void *sival_ptr;
la sigev_value un valore che deve essere passato alla funzione che riceve il segnale. Attualmente non ancora del tutto
implementato ed assume il valore NULL.
Il sigev_signo specifica il numero del segnale che deve essere mandato al completamento dell'operazione di I/O asincrono, o
al termine del tempo di attesa dal timer, in entrambi i casi necessario: deve essere settato il signal handler in modo tale che
vengano eseguite le azioni collegate al segnale.
Le azioni che sono gestite con i segnali sono la spedizione ed il ricevimento.
Un segnale pu essere spedito da un processo o dal kernel, ad esempio i segnali di kill, gli errori hardware o i caratteri di
terminale (^c, ^z) mandano segnali.
Il processo che riceve il segnale pu reagire in diversi modi a seconda di come stato programmata la reazione (morte del
processo, esecuzione di azioni particolari...).
Un segnale spedito viene sicuramente ricevuto dal pocesso destinatario, a meno che il processo stesso sia stato impostato in
modo da ignorare certi segnali. Per determinare se esistono segnali "non recapitati o pendenti" si usa la funzione sigpending.

Nella maggiore parte dei casi i segnali sono utilizzati per recapitare eventi asincroni.

Alcuni segnali utilizzati sono:

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.

Per la dichiarazione e l'uso di un signal possiamo quindi definire:

- Primo modo

signal(sgn, val)
sgn: il segnale

113 of 138
val: handler

SGN_IGN /* ignorare il segnale */


SGN_DFL /* riporta il comportamento del segnale a quello di default */

void handler(int sig)


{
...
}

- 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

SA_ONS TACK : usa lo stack addizionale dei segnali


SA_INTERRUPI: non fa ripartire la SC dopo la chiamata
SA_RESEAND: riporta il comportamento a SIG_DFL dopo uso
SA_NOCLDSTOP: non manda SIGCHLD al figlio

sigset_t sigsetmask(mask)
int sigprocmask(int how, sigset_t *set, sigset_t *oset)
how: SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK

- Sigaction

Sigaction(int sig, const struct sigaction *act, struct sigaction *oact);

int sig: segnale da trappare


struct sigaction *act: struttura base comprendente

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

- Funzioni per il controllo dei segnali

sigaction - specifica l'azione che deve essere eseguita dal processo


sigpending - ritorna i segnali bloccati
sigprocmask - esamina o modifica la maschera dei segnali del processo chiamante
sigsuspend - modifica la maschera dei segnali e sospende il processo chiamante
alarm - spedisce il segnale SlGALRM dopo un certo tempo
kill - spedisce un segnale a uno o pi processi
nanosleep - sospende l'esecuzione del processo chiamante per un certo numero di nanosecondi
sleep - sospende l'esecuzione del processo per un numero di secondi
pause - sospende l'esecuzione di un processo fino alla delivery di un certo segnale
wait - permette ad un processo di ottenere informazioni sullo stato da un processo figlio, e quindi sospenderlo fino all'arrivo

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.

- Bloccaggio dei segnali

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:

sigprocmask(SIG_BLOCK, NULL, &procmask);

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.

sigtset_t, newmask, oldmask;


sigemptyset(&newmask);
sigemptyset(&oldmask);
sigaddset(&newmask, SIGSYS);
sigaddset(&newmask, SIGTRAP);
sigprocmask(SIG_ BLOCK,&newmask, &oldmask);
< regione critica >
sigprocmask(SIGSETMASK, &oldmask, NULL);
L'ultima istanza ripristina la maschera originaria.
La chiamata a sigsetops sigset, quindi possibile
definire una

115 of 138
maschera generale.

- Controllo dei segnali

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

processo nel punto ove il segnale viene ricevuto.


Utilizzando la sigaction viene stabilito il comportamento legato al verificarsi di un certo segnale, questo comportamento
fissato fino al momento in cui questo comportamento viene modificato tramite un'altra sigaction o equivalenti.
La sigaction utilizza una struttura specifica per descrivere l'azione che deve essere eseguita.

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);
}

Tramite la funzione signal possibile la completa gestione dei segnali:


signal ha due parametri:

sig - identifica il segnale


func - identifica la azione che deve essere eseguita, pu essere un puntatore ad una funzione, o i valori
SIG_DFL, SIG_IGN definiti in signal.h

signal(SIGIO, SIG_IGN);
signal(SIGCHLD, SIG_DFL);
signal(SIGALRM myhandler)
Un signal handler ha tre argomenti, quindi sempre necessario dichiararli:

signal_number - che contiene il segnale ricevuto


code - che il codice aggiuntivo che viene gestito da alcuni segnali
scp - che punta ad una struttura di tipo sigcontext, in cui memorizzato il contesto del processo prima che
fosse mandato il signal.

Le primitive di sigsetops servono per manipolare i segnali gestiti, in particolare:

sigaddset - aggiunge un segnale specifico ad un set


sigdelset - elimina un segnale da un set
sigemptyset - inizializza un set di messaggi privo dei messaggi
sigfillset - inserisce tutti i segnali previsti da posix 1003.4
sigismember- testa la presesenza di un segnale specifico in un set

- Esempio di programma completo

Example 53: Handlig Signals

/* This program prompts for input in file "tmp". */


/* If interrupted by Ctrl/C, remove "tmp" and exit. */

#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 */

signal (SIGINT, sigint_handler);

/* Make sigint_handler handle all SIGINT */


/* Signals. signal( ) blocks other SIGINTs */
/* While a SIGINT is being handled. */

fp=fopen("tmp", w); /* Open file "tmp" for writing */


printf("Enter text. /n"); /* Prompt for text */
while ((c=getchar( )) != EOF) /* Get a char and write it to "tmp" */
putc(c, fp);
puts("EOF typed before CTRL/C");
exit (0); /* Successful exit */
}

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:

- Ottenere un file-descriptor con il comando open o shm_open


- Mappare l'oggetto con una chiamata alla funzione mmap
- Alla fine dell'utilizzo smappare l'oggetto dalla memoria tramite munmap
- Chiudere l'oggetto con close
- Eliminare l'oggetto dalla memoria condivisa tramite una chiamata a shm_unlink, o nel caso di file mappato
unlink

Nel caso di file mappato prima di operare l'unlink

- Apertura della memoria condivisa

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;

md = shm_open("my memory", O_CREAT | O_RDWR, 0);


pg size = sysconf( _SC_PAGE_SIZE);
virt_addr = mmap(0, pg size, PROT_WRITE, MAP_SHARED, md, 0);
....
status = munmap(vir_ addr, pg_size);
status = close(md);
status = shm_unlink("my memory");
}

- 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(addr, size, prot, flags, fd, offset)

addr: indirizzo iniziale della memoria


size: dimensione dell'area
prot: protezioni
flags: flags e caratteristiche
fd: file descriptor
offset: offset rispetto all'indirizzo

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

I flags forniscono altre informazioni sulla gestione dei dati condivisi:


MAP_SHARED: le modifiche sono condivise
MAP_PRIVATE: le modifiche sono private
Questi due flag controllano gli accessi all'area condivisa (si deve "semaforizzare" l'area condivisa per evitare conflitti e
incongruenze), in particolare con MAP_SHARED le modifiche sono immediatamente visibili per tutti o trascritte sul file,
con MAP_PRIVATE le modifiche sono invisibili agli altri processi ed invisibili al file.

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

shm_unlink(name): rimuove un oggetto di tipo memoria condivisa


mmap(mode, size, flags, fd): mappa un oggetto di tipo memoria condivisa o file nello spazio di indirizzamento del processo
mprotect(fd, flags): modifica le protezioni dell'oggetto
msync(fd): sincronizza l'oggetto in memoria con il file fisico
mlock(addr, size): blocca la pagina in memoria
munlock(addr,size): sblocca la pagina in memoria
munmap(addr, size): smappa l'oggetto dalla memoria

- Shared memory con funzioni dei files

Gli oggetti di tipo shared memory possono essere gestiti tramite le tradizionali funzioni di accesso ai files, come le funzioni
definite nel POSIX.1:

- fchmod: modifica i diritti di accesso


- fcntl: controlla le operazioni su files e memoria
- flock: blocca una risorsa come esclusiva, alle volte non viene ereditato in una fork, poich lock
caratteristico del file e non del descriptor
- fstat: d informazioni sullo status del file
- ftruncate: setta la lunghezza di un oggetto in memoria

- 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);
}

- Shared memory e semafori

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.

- Lock e unlock dell'intero processo

La funzione mlockall locka tutte le pagine di un processo.


mlockall ha due flags:

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

lock_memory(char *addr, size_t size)


{
unsigned long page_offset, page size;
page_size = sysconf( _SC_PAGE_SIZE);
page_offset = (unsigned long) addr % poge_size;
addr - = page_offset;
size + = page_offset;
return(mlock(addr, size));
}
unlock_memory( char *addr, size_t size)
}
unsigned long page_offset, page_size;
page_size = sysconf(_SC_PAGE_SIZE);
page_offset = (unsigned long) addr % page_size;
addr - = page_offset;
size + = page_offset;
return (munlock (addr, size));
}

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:

Applicazioni ad alto throughput, quindi continuo flusso di dati ad alta velocit


Applicazioni con tempi di risposta critici

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:

. Permettono di settare un clock di sistema molto preciso e di modificarne le caratteristiche durante


l'esecuzione dei programmi
Permettono la definizione di timers dei processi, sia one-shot che ciclici.
Gestiscono i segnali asincroni
Gestiscono le interruzioni e le schedulazioni dei processi.

La prima funzione di clock presente il CLOCK_REALTIME, definita in time.h.


CLOCK_REALTIME l'orologio del sistema, visibile da tutti i processi, ad ogni "tick" dell'orologio viene lanciato un
interrupt, che pu essere utilizzato dai processi.

- Timers

Sono previsti due tipi di 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:

Dichiarare il signal handler


Settare la sigevent, relativa al segnale che si vuole mandare all'attivazione del timer
Collegare il segnale con il signal handler
Creare il timer

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)

typedef struct timespec


{
time_t tv_sec;
long tv_nsec;
}
typedef struct itimerspec
{
struct timespec it_interval;
struct timespec it_value;
}

A seconda che i valori siano zero o no la struttura permette di definire timers one-shot o periodici.

- Funzioni per la gestione dei timers

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:

timerid: identificatore del timer


flags: definisce le caratteristiche del timer, in particolare il funzionamento assoluto o relativo:
TIMER_ABSTIME (settato) - il timer si riferisce al real-time clock, quindi il tempo di partenza assoluto, se
questo flag non settato il tempo relativo al tempo attuale
value: punta ad una struttura del tipo itimerspec, che quindi definisce le modalit di funzionamento del timer.

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.

- Operazioni per l'uso di timers

1) Includere time.h e signal.h


2) Dichiarare variabili del tipo itimerspec
3) Definire la struttura sigevent contenente il signal da passare al processo
4) Settare il signal handler nella procedura chiamante
5) Chiamare timer_create per associarlo al clock
6) Inizializzare itimespec
7) Chiamare timer_settitime
....
n) Chiamare timer_delete

- 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.

- I/O System Calls

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)

VOID leggi (INT sig)


{
STATIC INT i = 0; /* mantiene l'ultimo valore di i */
INT k = totbytes; /* costante del numero di bytes da leggere */
IF ( i + = READ (fd, (PTR + i), k - i)) = = k)
SIGNAL (SIGIO; SIGIGN);
}

L' accesso alla periferica avviene solo quando quest'ultima disponibile (con evidente risparmio di tempo ed una maggiore
sicurezza).

- Gestione dei processi

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

execute(char **args, int sin, int sout, int serr)


{
int pid, status;
if((pid = fork( )) < 0)
{
perror("fork");
exit(1 );
} /* PADRE */
if(pid = = 0)
{
if(sin != 0)
{
close(0);
dup(sin);
}
if (sout != 1)
{
close(1);
dup(sout);
}
if(serr!= 3)
{
close(3);
dup(serr);
}
execvp(*args, args);
perror(*args);
exit(1);
} /* FIGLIO */
while(wait(&status) != pid);
} /* PADRE */

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).

- Pipe (gestisce i flussi di dati)

In un sistema operativo esistono tre tipi di flussi di dati:

- STDIN (Standard Input) --> 0 (PID)


- STDOUT (Standard Output) --> 1 (PID)
- STDERR (Standard Error) --> 2 (PID)

La system call per creare una pipe pipe.


Ha un singolo argomento, un array di due int, che conterr i due file descriptors:

main( )
{
FILE *fp;

127 of 138
int pid, pipefds[2];
char *username, *getlogin( );

if ((username - getlogin( )) = = NULL)


exit(1);
if(pipe(pipefds) < 0)
exit (1);
if((pid = fork( ))< 0)
exit(1 );
if(pid = = 0)
{
close(0);
dup(pipefds[0]);
close(pipefds[0]);
close(pipefds[1]);
execl("/bin/mail","mail", username, 0);
exit(1 );
}
close(pipefds[0]);
fp = fdopen(pipefds[1],"w");
fprintf(fp,"hello!\n");
fclose(fp);
while(wait((int *) 0) != pid);
exit(0);
}

- Esempio (compressione dati)

TARCUF - * | COMPRESS > PIPPO.TAR.Z

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

La System Call principale per il controllo di una device ioctl:

ioctl(fd, void, args)

consente la programmazione dei device drivers a seconda della tipologia dei drivers considerati.

Ogni device driver ha le sue SC:


/dev/eth0 - ha una sua open
/dev/us - ha una sua open
.....
esempio: device driver di scheda Ultrasuoni:
Lo scopo l'acquisizione di misure di distanza tramite l'utilizzo di una scheda basata su un chip 8255 con hw dedicato per
controllo di sensoristica US.
Il device driver ha le seguenti opzioni:
- apertura della comunicazione (lock della device) - open

- start del sistema di acquisizione (ioctl)


- lettura dati (read)
- spegnimento acquisizione (ioctl)
- chiusura (close)

- Programma principale dimostrativo

#define NUMSENS 4 /* Per Borenstein DEVE essere 4,8 oppure 12!!*/


tiposens sensori[NUMSENS] = {
{1, SHORTRANGE},
{2, SHORTRANGE},

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));

printf ("start acquisizione: %d\n", ioctl (fd, TIOSTART, &schedt));

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;

if (read (fd, &valsensori, NUMSENS * sizeof (float)) = =-1)


{
printf ("Fine acquisizione: la read non legge! %d\n", ioctl (fd, TIOSTOP, 0));
close (fd);
timer1.it_interval.tv_sec = 0;
timer1.it_interval.tv_usec = 0;
timer1.it_value.tv_sec = 0;
timer1.it_value.tv_usec = 0;
setitimer (ITIMER_REAL, &timer1, 0);
exit (-1);
}
else
{
printf ("#%d: %5.1f " "#%d: %5.1f " "#%d: %5.1f " "#%d: %5.1f\n"
, sensori[0].numsensore+1, valsensori[0]
, sensori[1].numsensore+1, valsensori[1]
, sensori[2].numsensore+1, valsensori[2]
, sensori[3].numsensore+1, valsensori[3] );
}
}

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 struct timer_list mytimer = {NULL, NULL, 0,0,USTimer;


int num_us_active; /* numero di sensori utilizzati sulla scheda */
int currsens; /* sensore corrente utilizzato */
tiposens *csensori; /* dati sensori e ranges utilizzati */
float *misure; /* misure raccolte */
int timer_sched;
static struct wait_queue *wait_queue = NULL;
int tval;
int base;
extern double Cfact;

/* relativa alla SC read */


/* ritorna l'array di float relativa alle ultime misure effettuate */
/* legge un numero di bytes pari a count, st all'utente richiedere*/
/* i bytes giusti */

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);
}

static int usopen_funct(struct inode * inode, struct file *file)


{
MOD_INC_USE_COUNT; /* Increment the use count!!! */

130 of 138
wait_queue= NULL;
#ifdef DEBUG
printk("Driver US aperto\n");
#endif
SetMode(2,MOD1 );
WriteBank(2,1 ,RESETSIG);

if (!(csensori = (tiposens *) kmalloc(sizeof(tiposens) * SENSSCHEDA,GFP_KERNEL)))


return(-1 );
if(!(misure = (float *) kmalloc(sizeof(fioat) * SENSSCHEDA,GFP_KERNEL)))
return(-1 );
return(0);
}

static void usrelease_funct(struct inode *inode, struct file *file)


{
MOD_DEC_USE_COUNT; /* Decrement the use count!!! */
#ifdef DEBUG
printk("Driver US rilasciato\n");
#endif
WriteBank(2, 1 ,0x00);
SetMode(2,MOD0);
if(csensori != NULL)
kfree(csensori);
if(misure != NULL)
kfree(misure);
}

static int usioctl_funct(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
double PCfact;

switch(cmd) {

case TIOSETNUMS: /* Settaggio numero sensori utilizzati,copio da mem user a kernel */


memcpy_fromfs(&num_us_active,(int *) arg, sizeof(int));
if((num_us_active < 1 ) || (num_us_active > SENSSCHEDA))
return(WRONGNUM);
breek;

case TIOSETCFACT: /* settaggio fattore di conversione sensori */


memcpy_fromfs(&PCfact,(double*)arg, sizeof(double));
if((PCfact > 1) || (PCfact < 0))
return(WRONGNUM);
else
Cfact= PCfact;
break;

case TIOSETSENS: /* settaggio modalita' funzionamento sensori */


if((num_us_active < 1) || (num_us_active > SENSSCHEDA))
return(WRONGNUM);
else
memcpy_fromfs((tiposens *) csensori,(tiposens *) arg,(num_us_active * sizeof(tiposens)));
break;

case TIOSTART: /* Accensione anello sensoriale */


del_timer(&mytimer);
memcpy_fromfs(&timer_sched,(int*) arg,sizeof(int));
/*#ifdef DEBUG*/
printk("timer mandato:%d\n",timer_sched);
/*#endif*/
if ((timer_sched != SLOWSCHEDTIME)&&

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;

case TIOSTOP: /* Spegnimento anello sensoriale */


del_timer(&mytimer);
break;
default: eturn(-1);
}
return(0);
}

static struct file_operations ustest_fops = {


NULL, /* funct seek */
usread_funct, /* funct read */
NULL, /* funct write */
NULL, /* funct readdir*/
NULL, /* funct select */
usioctl_funct, /* funct ioctl */
NULL, /* funct mmap */
usopen_funct, /* funct open */
usrelease_funct /* funct release */
};

void
cleanup_module (void)
{
printk("USsens unloaded\n");
release_region (base, 8);
unregister_chrdev (USDRIVER, "USDriver");
}

int init_module (void)


{
base = CARD_ADDRESS;

if ((register_chrdev (USDRIVER, "USDriver", &ustest_fops)) = = -EBUSY)


{
printk ("\n\nImpossibile installare Driver Scheda Ultrasuoni alla locazione %d\n\n", USDRIVER);
return -EI0;
}
else
printk ("\n**********\n Driver Scheda Ultrasuoni by Giugu 'n' Sad, pos %d\n**********\n", USDRIVER);
if (check_region (base, 8) != 0)
printk ("\nOh, shit!: No free memory found!!\n");

printk ("\nCheck Region: OK!\n");


request_region (base, 8, "USDriver");
printk ("Region Request: OK!\n");
return 0;
}

static void USTimer(unsigned long a)


{
static short chsnd;
short chin1, chin2;
float newmes;

132 of 138
/* lettura risultati contatore e reset scheda */

if(a = = STOP_A) /* collect risultati */


{
del_timer(&mytimer);
ReadBank(2,2,&chin1 );

/* se la misura non e'pronta aspetto ciclicamente di WAITNMES */

if (!(chin1 & RDYMES))


{
del_timer(&mytimer);
mytimer.expires = WAITNMES + jiffies;
mytimer.data = STOP_A;
add_timer(&mytimer);
return;
}

/* altimenti leggo il secondo byte e resetto la scheda */

ReadBank(2,3,&chin2);
#ifdef DEBUG
printk("collect risultati, letti %x %x \n",chin1, chin2);
#endif

ComputeMeasure(chin1, chin2, &newmes);


if(newmes > SOGLIAERR)
misure[currsens] = newmes;
mytimer.expires = WAITRESET + jiffies;
mytimer.data = STOP_B; WriteBank(2,1 ,RESETSIG);
add_timer(&mytimer);
return;
}

/* preparazione nuova acquisizione */


/* viene selezionato il nuovo sensore */
/* spedita l'abilitazione alla scheda con id sensore e range */
/* nel caso che il range nell'array e' sbagliato e' forzato al breve */

if(a == STOP_B) /* Inizio Cicio */


{
del_timer(&mytimer);
if(currsens < (num_us_active-1)) /* selezione nuovo sensore */
currsens++;
else
{
currsens = 0;
wake_up_interruptible(&wait_queue);
}
/* la lettura e'abilitata alla fine di ogni giro */
/* nel caso che sia stata richiesta la lettura,questa aspetta la */
/* fine nella raccolta dei dati sensoriali, la chiamata alla read */
/* pone il processo in sleep fino alla fine del giro */

ComputeInitSensor(csensori[currsens].numsensore, csensori[currsens].range, &chsnd);


WriteBank(2, 1 ,chsnd);
mytimer.expires = WAITSW + jiffies; /* il driver riaddormentato per WAITSW */
mytimer.data = STOP_C;
add_timer(&mytimer);
return;
}

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;
}
}

Particolare attenzione deve essere rivolta a:

- static int usopen_funct


- case TIOSETNUMS e case TIOSETCFACT (usati come argomenti della IOCTL nel main, ritorna -1 se c'
errore e 0 se va bene), case TIOSTART (gestita con un timer che si basa sui jiffies [tempo di risposta minimo
garantito])
- static struct file_operations
- int init_module (void)
- static void USTimer (unsigned long a), le letture vengono effettuate dinamicamente

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.

- Echelon neuron chip

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

Network Communication Port


Single-ended mode
Differential mode
Special purpose Mode
Selectable transmission rates: 610bits/sec to 1.25Mbits/sec
600 packets/sec sustained throughput, 1000 packets/sec peak throughput at 1.25Mbps
40mA current output for differentially driving twisted pair networks
Optional collision detect input
Firmware
LonTaLkC protocol conforming to 7-layer OSI reference model
I/O drivers
Event-driven task scheduler
Service pin for remote identification and diagnostics
Unique 48-bit internal Neuron ID
Built-in low voltage detection for added EEPROM protection

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

Comparazioni tra i modelli Neuron Chip

Diagramma a blocchi del Neuron Chip

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)

- Detail Schedule Logic (politica di schedulazione)

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 */
} }

/* NVO = Network Variable Out */ /* NVI = Network Variable Input */

138 of 138

Você também pode gostar