Architetture scalari
Nelle architetture scalari, le istruzioni sono eseguite una dopo l'altra, quindi gestire l'interruzione al termine del fetch della prossima o quando si conclude la execute della corrente è indifferente o quasi. Più precisamente, se l'interruzione è non ripristinabile farlo prima dell'avvio del fetch fa risparmiare un accesso alla memoria, ma essendo non ripristinabile non ha senso completare comunque la fase execute, che può essere anche molto impegnativa e richiedere diversi cicli di clock per essere completata, mentre per quelle ripristinabili proprio perchè è fondamentale salvare lo stato (stabile) del sistema è bene servire l'interruzione prima della fase fetch, se così non fosse bisognerebbe considerare stabile lo stato dell'istruzione precedente e riavviare (fetch) l'istruzione sospesa, richiedendo così 2 accessi alla memoria per la medesima istruzione. Avendo un'idea dell'occorrenza delle interruzioni, nonchè della loro tipologia, è possibile valutare l'incidenza delle stesse sulle prestazioni del sistema ed adottare la soluzione che le ottimizza.
Le cose si complicano notevolmente se le architetture scalari hanno istruzioni che richiedono moltissimi cicli di clock, p. e. nel caso fossero dotate di MOVE in grado di copiare porzioni anche non troppo ampie di memoria, in questo caso può insorgere un problema. Essendo istruzioni la cui esecuzione è molto lunga c'è un'elevata la probabilità che siano interrotte, anche più di una volta, in questo caso preservare lo stato del sistema è piuttosto complicato perchè è costituito da 2 elementi indipendenti: la memoria e lo stato interno del processore.
L'interruzione preserva lo stato del processore, ma non quello dello della memoria centrale, nel caso della copia della memoria se fosse grarantito anche quest'ultimo, la copia richiederebbe un tempo infinito in caso di interruzioni sincrone; dovendo riavviare ogni volta l'intero processo, che verrebbe interrotto nel medesimo punto! Ammesso e non concesso che non si cada in un loop infinito e si possa riavviare l'istruzione al termine dell'interruzione ripartendo dal fetch delle stessa, lo stato della memoria sarebbe diverso, nel caso della copia ciò non è catastrofico, perchè comunque i dati nell'area di destinazione sarebbero sovrascritti da quelli dell'area sorgente, ma nel caso di operazioni aritmetiche su dati in memoria ciò può avere gravi conseguenze se i risultati vanno a sovrascrivere parte dei dati originali, infatti riavviando l'istruzione dalla fase fatch i dati da elaborare non sarebbero più quelli corretti!
Una soluzione, trovata dagli ingegneri della Digital Equipment Corp. che lavoravano al VAX, consiste nel preservare uno stato modificato del processore che tenga conto della porzione di compito già svolto, in pratica i registri che fungono da puntatori e da indici sono aggiornati man mano che il microprogramma procede. L'interruzione al suo termine fa ripartire l'elaborazione dalla fase di execute, la presenza dei valori modificati nei registri dell'istantanea dello stato fa si che il compito venga correttamente completato e senza l'onere di ripetere parte del compito già correttamente eseguito. In pratica e come se si usassero dei Check-point dai quali ripartire per svolgere un compito complesso.
Architetture pipeline
Nelle architetture pipeline le cose si complicano per 2 motivi:
1) la sovrapposizioni di più istruzioni fa si che debbano essere preservate più informazioni
2) lo stato del processore può essere modificato in qualsiasi momento da un'istruzione in uno stadio avanzato delle pipeline
Consideriamo una tipica pipeline a 5 stadi: IF, ID, EX, MEM, WB. Le interruzioni esterne, ovvero quelle legate ad eventi esterni al processore, non fanno altro che alterare il flusso delle istruzioni eseguite, quindi sono assimilabili a delle istruzioni di salto incondizionato se di tipo non ripristinabile o a subroutine se del tipo ripristinabile. Consideriamo quelle ripristinabili perchè più complesse; sono assimilabili a delle JSR: il PC deve essere aggiornato, questa volta però con l'indirizzo del gestore delle interruzioni (modulo del Sistema Operativo). Quando va fatta questa operazione? Va assolutamente preservato lo stato del processore e va anche memorizzato l'indirizzo di rientro, essento l'interruzione di tipo ripristinabile. Per far si che si riduca il numero di istruzioni che debbano essere riavviate, ma anche la quantità di informazioni da preservare, bisogna congelare lo stadio IF interdicendo le sue linee di uscita verso lo stadio ID, allo stesso tempo si deve svuotare la pipeline in modo da completare le altre istruzioni ancora pendenti, quindi lo stato del processore è consolidato dall'istruzione che precede quella che è rimasta congelata nella fase IF, appena si esegue la WB dell'ultima istruzione si preserva il contenuto del PC, che però già punta alla successiva ed andrà quindi opportunamente decrementato e lo si sovrascrive con l'indirizzo del gestore delle interruzioni, dopo di che si riabiltano le linee di uscita dello stadio IF e si riparte con tale fase. In questo modo che le istruzioni del gestore iniziano a fluire nella pipeline. La ripresa del flusso originario avviente quando si incontra l'istruzione RTI. Tale compito è alquanto banale, infatti è sufficiente risvuotare la pipeline e ripristinare lo stato del processore aggiornando il PC con l'istruzione precedentemente congelata nella fase IF. Questa tecnica, anche se provoca per ben 2 volte uno svuotamento della pipeline è molto semplice e consente di risolvere il problema numero 1), infatti facendo completare tutte le istruzioni nella pipeline, tranne l'ultima, si riduce notevolmente il numero delle informazioni da preservare: bastano PC, PSW e il banco dei registri.
Le interruzioni interne invece sono le più difficili da gestire se debbono essere di tipo ripristinabile essendo per definizione anche di tipo sincrono, ovvero si ripresentano ogni volta che la macchina si trova nel medesimo stato; quelle non ripristinabili sono assimilabili a dei salti incondizionati (JMP) e determinano una prematura fine dell'esecuzione del codice. In realtà le cose, anche in questo caso, sono più complesse, ma fortunatamente sono gestite a livello software grazie all'intervento del Sistema Operativo o meglio del suo sottoprogramma che funge da gestore delle interruzioni, che provvede ad informare l'utente ed a rimuovere dalla memoria l'applicativo con il rilascio delle risorse ad esso precedentemente attribuite.
Riportiamo in una tabella, per ciascuno stadio, le interruzioni interne che possono insorgere:
Stadio Interruzione
IF Mancanza di pagina al momento del prelievo di un'istruzione Accesso disallineato alla memoria Violazione dei diritti di accesso alla memoria
ID Codice operativo non definito o non consentito
EX Interruzione di tipo aritmetico
MEM Mancanza di pagina al momento dell'accesso alla memoria Accesso disalineato alla memoria Violazione dei diritti di accesso alla memoria WB Nessuna
Ciò che rende più difficile trattare le interruzioni interne ripristinabili in una architettura di tipo pipeline è la possibilità che ciascuna istruzione presente nella pipeline, tranne quella nello stadio WB, possa generare un interrupt. In apparenza ciò non sembra un problema, infatti si potrebbe gestire la cosa con una logica First Came First Served (FIFO), ciò porta talvolta ad un paradosso: servire prima un interrupt di una istruzione che segue un'altra istruzione che a sua volta genererà tra breve una interruzione, ovvero le interruzioni sono generate e quindi servite in ordine diverso da quello imposto dalla schedulazione delle istruzioni.
Supponiamo che 2 istruzioni consecutive, che indicheremo con i e i+1, generino 2 interruzioni durante la loro elaborazione e quella della i+1 venga sollevata in uno dei primi stadi, mentre quella della istruzione i in uno stadio successivo, in altre parole viene generata per prima quella relativa all'istruzione i+1. Ci sono 2 modi distinti di procedere:
1) Il primo approccio è di tipo completamente sincrono e richiede solo la presenza di un vettore di stato, nel quale la CU deve inserire le interruzioni man mano che si verificano senza servirle. Non appena un'istruzione sta per modificare lo stato della macchina (stato del processore + memoria) viene controllato il vettore di stato se ci sono interruzioni pendenti queste vengono servite, facendo però in modo da servire per prime quelle delle istruzioni precedenti secondo un oridinamento crescente del PC, ripristinando così l'ordine esatto delle interruzioni. Al tempo stesso viene garantita la ripristinabilità dell'interruzione non essendo stato modificato lo stato della macchina prima che l'interruzione sia servita.
2) Il secondo approccio rilassa un po' la condizione di sincronicità (quasi sincrono) accettando che di tanto in tanto si verifichi il paradosso. In questo approccio però la pipeline viene arrestata immediatamente annullando tutte le istruzioni che non abbiano ancora modificato lo stato della macchina, il programma riprederà dalla prima istruzione annullata.
Il primo approccio mira a ridurre il costo del riavvio del programma a fronte di una maggiore complessità circuitale, infatti è neccessario aggiungere il vettore di stato, che altro non è che un banco di registri e della logica addizionale per aggiornare ed interrogare il vettore di stato. Il secondo approccio invece sacrifica un po' di potenza di calcolo pur di semplificare l'hardware.
Nella prossima puntata parleremo delle interruzioni nelle architetture pipeline con unità multiciclo, daremo uno sguardo alla gestione delle stesse nell'architettura DLX e l'interazione con il Sistema Operativo.