INTERRUZIONI ED ECCEZIONI

1. Introduzione

Per un calcolatore, lo scorrere del tempo è legato al cambiamento di stato del clock. Quando il segnale emesso dal circuito temporizzatore passa da alto a basso o viceversa, per il computer è passata un'unità di tempo.
Alla luce di ciò, se si interrompe il clock, è possibile fermare il "tempo macchina". In questo modo, il calcolatore non si accorgerà più del passare del tempo.

Partendo da questa idea è nato il concetto di interruzioni ed eccezioni. Al presentarsi di un apposito segnale che arriva alla CPU dall'hardware o dal software, il normale flusso del programma in esecuzione viene interrotto e lo stato volatile della macchina viene salvato.
Sarà possibile interrompere il programma in esecuzione "non facendo più arrivare il segnale di clock a quel programma", ovvero interrompendo lo scandire delle sue istruzioni da parte del PC. Questo, però, non è sufficiente: soltanto salvando nello stack tutti i registri utilizzati dal programma, nonchè il valore puntato dal program counter e il registro di stato della macchina, qualsiasi cosa succeda, quando si ritorna ad eseguire il programma sospeso, questo non si accorgerà di essere stato interrotto.
Il salvataggio e ripristino di tutti questi registri viene detto rispettivamente salvataggio e ripristino dello stato volatile della macchina.
Viene utilizzato lo stack proprio per la sua filosofia di accesso LIFO, ideale in questi casi di salvataggio e successivo ripristino. Lo stack è ideale anche per gestire interruzioni annidate.

2. Funzionamento

Un'interruzione viene richiesta alla macchina tramite un opportuno segnale che arriva alla Control Unit.
Questo segnale può avere tre provenienze, generando tre tipi di interruzioni Questa divisione è puramente concettuale, dato che la macchina tratta tutti e tre i tipi allo stesso modo.
Ricapitolando, le interruzioni esterne ed interne vengono generate dalla macchina stessa, mentre quelle software vengono generate dal programmatore assembly.
Spesso si usa chiamare eccezioni solo le interruzioni software, mentre si indicano con interruzioni gli altri due tipi. Di seguito, invece, i termini interruzione ed eccezione saranno usati come sinonimi.

Quando la macchina riceve una richiesta di interruzione, sia essa interna, esterna o software, esegue i seguenti passi: Il programmatore assembly che scrive i programmi di servizio come prima cosa deve provvedere a salvare nello stack tutti i registri che modificherà. Di conseguenza, al termine del programma di servizio dovrà ripristinare tali registri. Questi due passaggi, come già visto, sono detti rispettivamente salvataggio e ripristino dello stato volatile della macchina e sono assolutamente necessari perchè il programma interrotto non si accorga dell'interruzione.
Il salto al sottoprogramma di servizio è per molti versi simile ad un salto a subroutine. Esistono tuttavia alcune differenze, che comportano l'esistenza di istruzioni diverse per il salto ed il ritorno da subroutine e per il salto ed il ritorno da sottoprogrammi di servizio. La prima differenza è che nel salto a sottoprogramma di servizio viene salvato, oltre al contenuto del PC, anche il contenuto del registro di stato della CPU, cosa che non accade nel salto a sottoprocedura. Questo salvataggio aggiuntivo è dovuto al fatto che l'interruzione, al contrario del salto a subroutine, è un evento asincrono, ovvero può accadere in qualsiasi momento, senza possibilità di prevedere quale sarà questo momento. Un'altra differenza sta nel fatto che il salto a sottoprogramma di servizio ha un tramite: il vettore delle interruzioni.

Le interruzioni verranno trattate approfonditamente anche nel corso di "Sistemi Operativi". Si vedrà, ad esempio, che per evitare che i programmi che non generano mai interruzioni monopolizzino l'uso della CPU finchè non terminano, il SO genera interruzioni fasulle a cadenza fissa (sistemi operativi a time sharing che usano il real-time clock).

3. Utilizzi

3.1 Multiprogrammazione

Per multiprogrammazione si intende la possibilità di far girare più programmi contemporaneamente su una stessa macchina. Si badi bene al significato del termine contemporaneamente, che non indica la contemporaneità di esecuzione delle istruzioni dei due programmi - cosa questa che contrasterebbe con la definizione di macchina di Von Newman -, ma indica che più programmi si "contendono" le attenzioni della CPU, interrompendosi a vicenda (le macchine di Von Neumann hanno un solo P.C., quindi per eseguire più programmi non possono far altro che zompettare da uno all'altro).
Sarà possibile quindi eseguire un'altro programma mentre quello precedentemente in esecuzione è in sospeso, "non facendo più arrivare il segnale di clock a quel programma", ovvero interrompendo lo scandire delle sue istruzioni da parte del PC, che passerà a scandire le istruzioni del secondo programma. Questo, però, non è sufficiente: soltanto salvando nello stack tutti i registri che il secondo programma modifica, nonchè il valore puntato dal program counter e il registro di stato della macchina, quando si ritorna ad eseguire il programma sospeso, questo non si accorgerà di essere stato interrotto. Il salvataggio e ripristino di tutti questi registri viene detto rispettivamente salvataggio e ripristino dello stato volatile della macchina. Viene utilizzato lo stack proprio per la sua filosofia di accesso LIFO, ideale in questi casi di salvataggio e successivo ripristino. Lo stack è ideale anche per gestire interruzioni annidate.

La multiprogrammazione ha rivoluzionato il modo di utilizzare i calcolatori. In sua assenza, per eseguire un programma in contemporanea ad un altro, bisognerebbe linkarli. Si veda nel file 6 il paragrafo 1.3: "L'importanza delle interruzioni software".
Si pensi che ai nostri giorni, ad esempio, ci sono sempre almeno due programmi in esecuzione contemporaneamente: il sistema operativo e il programma che si sta utilizzando. Senza la multiprogrammazione, i sistemi operativi non potrebbero esistere, o meglio, potrebbero esistere, ma ogni volta che si aggiunge un nuovo programma, bisognerebbe linkarlo col resto dei programmi.

3.2 Altro

In generale, il colloquio fra CPU e periferiche esterne al calcolatore avviene tramite interruzioni. Basti pensare alla tastiera: ogni volta che si batte un carattere, viene generata un'interruzione in attesa dell'immissione del carattere successivo. In questo modo, fra l'immissione di un carrattere e quello successivo, il calcolatore può eseguire altri programmi.
Le interruzioni interne vengono quasi sempre utilizzate per gestire errori. Errori tipici che generano interruzioni interne sono: istruzione inesistente, cella di memoria inesistente, violazione dello stack (stack overflow), errori aritmetici, ecc.
Ciò non toglie che anche le interruzioni esterne e software vengano utilizzate per gestire errori.
A tal proposito, un secondo tipo di calssificazione delle interruzioni è il seguente: La gestione degli errori segnalati tramite interruzioni spetta poi al programma in esecuzione?


Eccezioni ed Interruzioni nel MIPS
[Lezione di Laboratorio di Architetture; ho deciso di trattare questo argomento, vista la sua difficoltà]

Interruzioni ed eccezioni sono eventi che alterano il normale svolgimento del programma.
Quando si verificano, il programma viene interrotto e il controllo passa ad un programma di servizio che decide cosa fare.
Le eccezioni sono causate dal programma utente in esecuzione ed in genere segnalano che l'istruzione corrente ha creato problemi al processore mentre tentava di eseguirla. Le interruzioni sono dovute, al contrario, a cause esterne ed in genere vengono utilizzate per l'I\O su periferiche.

L'architettura del MIPS prevede un apposito coprocessore - detto coprocessore zero - per gestire le interruzioni\eccezioni (reagisce ai segnali di interruzione\eccezione, memorizza nei suoi registri se si tratta di una interruzione o di una eccezione, memorizza la periferica che l'ha generata nel caso di interruzioni, ecc.).
SPIM simula soltanto 4 registri di questo coprocessore:
  • status ($12): abilita o meno il coprocessore a servire le interruzioni\eccezioni;
    il bit meno significativo è detto interrupt enable e se è alzato abilita le interruzioni;
    il bit successivo è detto kernel\user e stabilisce se l'interruzione è avvenuta in modalità utente o sistema operativo; esistono dunque due livelli di priorità: kernel ad alta priorità e user a bassa priorità. Quando la CPU sta servendo una interruzione kernel disabilita automaticamente tutte le interruzioni; questi due bit vengono ripeturi 3 volte, tenendo una traccia storica delle interruzioni, shiftata via via a sinistra di due posizioni (LIFO).
    nei bit dal 10 al 15 si trova la interrupt mask: ogni bit corrisponde all'abilitazione o meno delle interruzioni di una certa periferica; tastiera e schermo corrispondono ai due bit meno significativi (forse).
  • cause ($13): identifica il tipo di interruzione e maschera le eccezioni;
    i bit dal decimo al quindicesimo contengono le pending interrupt ovvero quelle interruzioni che si sono verificate mentre erano disabilitate le interruzioni e che quindi non sono state ancora servite (maschera le interruzioni);
    i bit dal 2 al 5 contengono l'exception code: un numero che identifica il tipo di interruzione\eccezione che si è verificata
    Ad esempio:
    0->interruzione: interruzione da periferica
    4->eccezione: accesso ad un indirizzo di memoria inesistente con load
    5->eccezione: accesso ad un indirizzo di memoria inesistente con store
    6,7->eccezione: problemi col bus
    8->eccezione: syscall
    12->eccezione: overflow
    In genere: se è zero è una interruzione, altrimenti è una eccezione. E' necessario distinguere i due casi, poichè vengono trattati in modo leggermente diverso fra loro.
  • epc ($14): (exception program counter) contiene l'indirizzo di memoria dell'istruzione che ha causato l'eccezione\durante la quale si è verificata l'interruzione
  • baddvaddr ($8): (bad virtual address) se l'eccezione è stata causata da una istruzione che ha tentato di accedere ad un indirizzo di memoria non esistente, questo registro contiene tale indirizzo.
A questi registri si accede tramite le istruzioni:
  • mfc0 rdest,rsrc: (move from coprocessor 0) copia il contenuto del registro rsrc del coprocessore 0 al registro rdest del processore principale
  • mtc0 rsrc,rdest: (move to coprocessor 0) viceversa
  • lwc0 rdest,address: analogo a lw
  • swc0 rsrc,address: analogo a sw
L'istruzione rfe (return from exception) ripristina il contenuto registro status del coprocessore zero al termine della gestione di una interruzione\eccezione.
Le direttive .ktext e .kdata sono analoghe a .text e .data, ma agiscono nell'area di memoria che finora abbiamo definito riservata al kernel e quindi servono per scrivere il programma di servizio.

Il programma di servizio per interruzioni ed eccezioni è unico qualsiasi tipo esse siano (interruzioni ed eccezioni non sono vettorizzate: il programma in esecuzione salta allo stesso entry point) ed è contenuto in un opportuno file.
In SPIM il file è "trap.handler" e gestisce solo le eccezioni.
In realtà, questo file svolge le veci di sistema operativo della macchina MIPS, gestendo eccezioni, caricando il programma che si vuole eseguire e chiudendolo. Per questo, quando viene lanciato, SPIM carica "trap.handler" nella zona di memoria a partire dall'indirizzo 0x80000080 che finora abbiamo detto essere riservata al kernel.

Per gestire le eccezioni (non sono sicuro che sia giusto):
  • viene interrotta l'esecuzione dell'istruzione che ha causato l'eccezione
  • si passa a modo sistema operativo e il controllo va al programma di servizio nel quale bisogna:
    • salvare in EPC il conteuto del PC più quattro (è necessario tornare all'istruzione successiva a quella che ha causato l'eccezione, altrimenti si avrebbe di nuovo l'eccezione)
    • salvare in memoria centrale il registro di stato del coprocessore zero; nota: non si può usare lo stack perchè l'eccezione potrebbe essere stata causata proprio da un'errore sullo stack
    • caricare nei bit dal 2 al 5 del registro cause del coprocessore zero il codice dell'eccezione verificatasi
    • caricare nel PC della CPU l'indirizzo di "trap.handler"
    • servire l'eccezione
    • tornare al programma che era in esecuzione: rfe...i registri $k1 e $k0 della CPU...
      oppure, in caso di errore irreversibile, terminare l'esecuzione del programma
Per gestire le interruzioni:
  • viene interrotta l'esecuzione dell'istruzione che ha causato l'eccezione
  • si passa a modo sistema operativo e il controllo va al programma di servizio nel quale bisogna:
    • caricare in EPC il contenuto del PC (non è necessario tornare all'istruzione successiva a quella durante la quale si è verificata l'interruzione - ATTENZIONE: per un errore in trap.handler, anche in questo caso viene sommato 4)
    • salvare in memoria centrale il registro di stato del coprocessore zero
    • disattivare tutte le interruzioni: mettere a zero il bit del registro status
    • caricare nei bit dal 2 al 5 del registro cause del coprocessore zero il codice dell'interruzione verificatasi
    • caricare nella maschera del registro cause gli opportuni valori
    • caricare nel PC della CPU l'indirizzo di "trap.handler"
    • servire l'interruzione
    • tornare al programma che era in esecuzione: rfe...i registri $k1 e $k0 della CPU...
SPIM simula due periferiche che utilizzano l'I\O con interruzioni: tastiera (input) e schermo (output).
Queste funzionano tramite una estenzione all'esterno della memoria centrale (ai loro registri si accede tramite indirizzi di memoria centrale). ATTENZIONE: perchè ciò avvenga bisogna attivare l'opzione "mapped I\O" del programma.
La tastiera lancia una interruzione subito dopo che è stato premuto un tasto.
I registri della tastiera che vengono simulati sono:
  • reciver control (0xffff0000):
    il bit meno significativo è detto ready flag e, se è zero, indica che è stato digitato un carattere e che questo non è ancora stato letto; durante questa fase, il carattere da leggere si trova nel registro reciver data;
    il bit successivo indica se questa periferica può lanciare o meno interruzioni: non basta abilitare le interruzioni nel coporcessore zero, ma occorre abilitarle anche nella periferica; ATTENZIONE: una volta deciso che alla tastiera si accede tramite interruzioni (alzando questo bit), non vi si può più accedere con la syscall
  • reciver data (0xffff0004):
    il byte meno significativo contiene il carattere da leggere; appena viene letto, il ready flag del registro control viene alzato; ATTENZIONE: SPIM simula anche i ritardi nel trsferimento di dati tastiera-calcolatore
Il monitor lancia una interruzione quando l'operazione di scrittura su schermo è terminata.
I registri dello schermo che vengono simulati sono:
  • transmitter control (0xffff0008):
    il bit meno significativo è detto ready flag e, se è basso, indica che l'operazione di stampa a video è ancora in corso; durante questa fase, il carattere da scrivere si trova nel registro transmitter data; quando è uno, la periferica è pronta ad accettare un nuovo carattere;
    il bit successivo indica se questa periferica può lanciare o meno interruzioni: non basta abilitare le interruzioni nel coporcessore zero, ma occorre abilitarle anche nella periferica; ATTENZIONE: una volta deciso che allo schermo si accede tramite interruzioni (alzando questo bit), non vi si può più accedere con la syscall
  • transmitter data (0xffff000c):
    il byte meno significativo contiene il carattere da stampare; appena viene letto, il ready flag del registro control viene alzato; ATTENZIONE: SPIM simula anche i ritardi nel trsferimento di dati calcolatore-schermo