# Periferiche (dispositivi) e loro controllo/2

La CPU utilizza quasi tutto il tempo a caricare le istruzioni dalla memoria ed ad eseguirle. Tuttavia la CPU e la memoria principale sono solo due dei tanti componenti in un computer reale. Un sistema completo contiene altri **device** (**periferiche**) come:

* Un **hard disk (HDD - Hard Disk Drivers)** o **disco allo stato solido** **(SSD - Solid State Drivers)** per immagazzinare programmi e file. (La memoria principale contiene solo una piccola parte di informazioni in paragone e le immagazzina solo fino a quando il sistema è acceso. Un **disco fisso** o un **disco allo stato solido** è **utilizzato per l'immagazzinamento permanente di grosse quantità di informazioni**, **ma i programmi devono essere caricati nella memoria principale prima di essere eseguiti**. Un hard disk immagazzina i dati su un disco magnetico, mentre un disco allo stato solido è un device puramente elettronico senza parti in movimento.)
* una **tastiera** o un **mouse** per input dell'utente;
* un **monitor** o una **stampante** per mostrare l'output di un computer;
* un **device audio** per permettere al computer di emettere suoni;
* una **network interface** (interfaccia di rete) che permette a un computer di comunicare con altri computer che sono connessi in rete, sia wireless o con cavi;
* uno **scanner** che converte immagini in insieme di byte che possono essere immagazzinati e modificati dal computer.

La lista dei device è aperta, un computer è costruito in modo da essere facilmente espandibile aggiungendo altri device. In qualche modo la CPU deve comunicare e controllare questi device. La CPU può solo fare questo eseguendo delle istruzioni in linguaggio macchina (la CPU sa fare solo questo). Il modo in cui funziona è che per ogni device nel sistema, c'è un **device driver** (detto anche solo driver), che è un software che la CPU esegue quando deve interagire con il driver. Installare un nuovo device su un sistema generalmente consiste in due step: aggiungere fisicamente il device al computer, e installare il device driver. Senza il driver, il device fisico sarebbe inutilizzabile, perché la CPU non sarebbe in grado di comunicare con esso.

Un computer in quanto ha molti device è organizzato collegando questi device da uno o più **bus**. Un bus è un insieme di cavi che trasportano diversi tipi di informazioni tra i device connessi da questi cavi. I cavi portano dati, indirizzi e segnali di controllo (comandi). L' indirizzo serve per dirigere il dato a un device particolare o a un registro particolare del device. Il segnale di controllo potrebbe servire per dare comandi al device. Un semplice esempio di computer potrebbe essere:

![](https://875337804-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lt58INaXg-cFL27pnx0%2F-M6ACuNBz7NX-CnxSsDg%2F-M6ADOgkCpofBsrTbhjp%2Foverview-fig2.png?alt=media\&token=11cea003-f938-4828-a29e-fb703bc91396)

Ora, device come periferiche, *mouse* e *network interface* possono produrre input che devono essere processati dalla CPU. Come fa la CPU a sapere che ci sono dei dati? Un modo semplice, ma non molto soddisfacente, potrebbe essere quello di continuare al controllare da parte della CPU se sono arrivati nuovi dati. Fino a quando non trova il dato e lo processa. Questo metodo si chiama **polling**, perché la CPU fa il 'polls' (sondaggio) del device di input continuamente per vedere se hanno dei dati di input arrivati. Sfortunatamente sebbene il polling sia molto semplice, è anche molto inefficiente. La CPU può sprecare inutilmente tanto tempo aspettando che arrivi l'input.

Per evitare questa inefficienza, gli **interrupt** sono generalmente utilizzati. Un interrupt è un segnale inviato dal device alla CPU. La CPU risponde a un segnale di interrupt, mettendo da parte quello che stava facendo in modo da rispondere immediatamente all'interrupt. Una volta che ha gestito l'interrupt, cioè ha fatto tutte le azioni necessarie che richiedeva la periferica che ha mandato il segnale di interrupt, solo allora ritorna a fare quello che stava facendo prima che l'interrupt accadesse. Ad esempio quando voi pigiate un tasto della tastiera del computer, un interrupt della tastiera è inviato alla CPU. La CPU risponde a questo interrupt interrompendo cosa stava facendo, leggendo il tasto che abbiamo premuto, processandolo, a poi ritornando al task (compito) che stava compiendo prima che avessimo pigiato il tasto.

E' da capire che questo è un processo puramente meccanico: Un device segnala un interrupt semplicemente inviano un impulso elettrico su un cavo. La CPU è costruita in modo, che quando arriva il segnale, la CPU salvi abbastanza informazioni di quello che stava facendo, così da poter tornare poi a riprendere quello che stava facendo. Questa informazione consiste nel contenuto di alcuni registri importanti della CPU come il program counter. Allora la CPU salta in alcuni zone di memoria predeterminate e comincia ad eseguire le istruzioni li immagazzinate. Queste istruzione fanno parte dell' **interrupt handler** (codice che gestisce l'interrupt, detto anche **gestore dell'interrupt** o **routine di interrupt**) che fa le operazioni per rispondere all'interrupt.   (Questo interrupt handler è parte del software del device driver della periferica che ha inviato l'interrupt). Al termine del interrupt handler c'è un istruzione che dice alla CPU di tornare a cosa stava facendo; lo fa ripristinando lo stato precedentemente salvato.

Gli interrupt permettono alla CPU di gestire **eventi asincroni**. Nel ciclo normale fetch-and-execute (carica l'istruzione ed eseguila), le cose avvengono nell'ordine predeterminato; ogni cosa che accade è "sincronizzata" con tutte le altre. Il meccanismo degli interrupt permette alla CPU di gestire in modo efficiente eventi che accadono in modo "asincrono", cioè, in momenti non predefiniti.

Un altro esempio di come gli interrupt sono usati, consideriamo cosa succede quando la CPU ha bisogno di accedere a dati che sono immagazzinati sull'hard disk. La CPU  può accedere ai dati direttamente solo se sono nella memoria principale. I dati sul disco devono essere copiati nella memoria principale prima che possano essere acceduti. Sfortunatamente, rispetto alla scala di velocità con cui la CPU opera, l'hard disk è molto lento. Quando la CPU ha bisogno di dati dal disco, la CPU invia un segnale all'hard disk dicendo di prendere i dati e prepararli. (Il segnale è inviato in modo sincrono, sotto il controllo di un programma normale) A questo punto, invece di aspettare per un tempo indefinito e lungo (respetto ai tempi della CPU) che l'hard disk completi l'operazione, la CPU va avanti ed esegue qualche altro task (compito - programma). Quando l'hard disk ha i dati pronti, esso invia un segnale di interrupt alla CPU. L'interrupt handler può a questo punto leggere i dati richiesti.

### Accesso alle periferiche

Per svolgere funzioni utili il processore deve poter interagire con le periferiche del sistema. A questo scopo molti processori utilizzano delle istruzioni macchina specializzate, ad esempio IN e OUT, che fanno riferimento agli indirizzi dei registri delle periferiche. Ogni periferica infatti possiede alcuni registri che servono alla sua gestione; alcuni registri, detti **registri dati**, servono a contenere i dati che la periferica deve leggere o scrivere, altri registri detti *registri di controllo e stato*, servono a contenere delle indicazioni sulle operazioni che la periferica deve svolgere oppure sullo stato della periferica (ad esempio, periferica "pronta" oppure periferica "occupata").

Si tenga presente che il significato dello stato di pronto di una periferica è relativo alla possibilità, per il processore, di svolgere un'operazione di lettura o di scrittura di un dato. Pertanto una periferica di ingresso, come la tastiera, è in uno stato di pronto quando un tasto è stato premuto e quindi esiste un carattere che il processore può leggere tramite l'istruzione IN; una periferica di uscita, come una stampante, è pronta se è disponibile per stampare un dato, in modo che il processore possa eseguire un'istruzione OUT verso di essa.

Tramite l'esecuzione delle istruzioni IN e OUT è possibile leggere e scrivere tali registri. Normalmente il programma che gestisce una periferica funziona secondo il seguente schema (detto schema a **controllo di programma**):

1. leggi il registro di stato della periferica (IN registro di stato)
2. se lo stato è "pronta", procedi, altrimenti ritorna a leggere il registro di stato
3. esegui l'operazione voluta (ad esempio, lettura di un dato dalla tastiera oppure invio di un dato alla stampante) (IN oppure OUT sul registro dati)

Nella figura sotto è illustrata la sequenza di istruzioni di ingresso/uscita, cioè di interazione tra il processore e una stampante durante l'esecuzione di un programma di stampa del carattere "A" sulla stampante. Si noti che il processore è obbligato a restare nella situazione iniziale fino a quando la stampante non cambierà di stato.

![](https://875337804-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lt58INaXg-cFL27pnx0%2F-M6AEGRqK58-DQ7VVULT%2F-M6AEbMJAOXxA9_Dz54R%2FEsempio_stampa.png?alt=media\&token=4f27f85c-1201-4dbb-94ef-82ed52808180)

La figura mostra inoltre che è opportuno distinguere tra l'adattatore della stampante, che interagisce con il processore e contiene i registri di stato e dati, e la stampante vera a propria.

### Meccanismo di Interruzione (Interrupt)

La gestione delle periferiche a controllo di programma ha il difetto di obbligare il processore a restare in un ciclo di attesa che la periferica diventi pronta; per questo motivo tutti i processori possiedono un meccanismo diverso detto **meccanismo di interrupt**: analizziamo prima come funziona tale meccanismo, poi vedremo perché esso migliora grandemente la gestione delle periferiche rispetto al controllo di programma.

Il meccanismo di interrupt si basa sulla definizione di un sistema di eventi rilevati dall'Hardware (ad esempio, un particolare segnale proveniente da una una periferica, una condizione di errore, ecc...) ad ognuno dei quali è associata una particolare funzione detta **gestore dell'interrupt** o **routine di interrupt** (interrupt handler); quando il processore si accorge del verificarsi di un particolare evento E, esso esegue il salto all'esecuzione della funzione associata a tale evento. Quando la funzione termina, il processore riprende l'esecuzione del programma che è stato interrotto. Per poter riprendere tale esecuzione il processore ha salvato, al momento del salto alla routine di interrupt, sulla pila di sistema l'indirizzo della prossima istruzione di tale programma, in modo tale che, dopo l'esecuzione della routine di interrupt tale indirizzo sia disponibile per eseguire il ritorno. Come si vede, il meccanismo di interrupt è a tutti gli effetti equivalente ad un'invocazione di funzione, con la differenza che le funzioni normali sono attivate dal programma in esecuzione mentre le routine di interrupt sono attivate da eventi riconosciuti dal processore. Le routine di interrupt non utilizzano la stessa istruzione RFS (return from subroutine) per terminare, ma un'istruzione diversa, che chiameremo **IRET** (**interrupt return**). Per il momento possiamo pensare all'interruzione IRET come identica alla RFS.

![](https://875337804-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lt58INaXg-cFL27pnx0%2F-M6AEGRqK58-DQ7VVULT%2F-M6AEie0ozmn94gIsQlW%2FEsempio_stampa2.png?alt=media\&token=e344a4a1-714d-449c-975d-2ec7c501db5a)

Nella figura questo meccanismo è applicato stampa di un carattere A sulla stampante; la figura illustra un possibile svolgimento temporale degli eventi. La differenza fondamentale rispetto al meccanismo descritto prima (a controllo di programma) consiste nell'assenza del ciclo di attesa iniziale; il processore viene impegnato solo a svolgere le istruzioni utili.

![](https://875337804-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lt58INaXg-cFL27pnx0%2F-M6AEGRqK58-DQ7VVULT%2F-M6AF-XxSu98D9f98wWP%2FVettori_Interrupt4.png?alt=media\&token=ec24529c-3c60-4dfc-b35e-a0339cbd7730)

Come fa il processore a sapere quale sia l'indirizzo della routine di interrupt che deve essere eseguita quando vi verifica un certo evento? Tale indirizzo è scritto dal SO durante la fase di inizializzazione di una particolare cella di memoria, detta **vettore di interrupt**, che è nota al processore perché è associata univocamente all'evento che ha causato l'interrupt. Quindi quando si verifica un evento, il processore legge dal vettore di interrupt l'indirizzo della routine di interrupt, esegue la routine stessa e poi ritorna al programma interrotto. Nella figura sono mostrati alcuni vettori di interrupt e le corrispondenti routine di interrupt; in tale figura si adotta l'ipotesi, valida in molti processori, che i vettori di interrupt siano posizionati nelle celle iniziali di memoria.

#### Interrupt e gestione degli errori

Durante l'esecuzione delle istruzione possono verificarsi degli errori che impediscono al processore di proseguire; un esempio classico è l'istruzione di divisione con 0, altri errori sono legati agli indirizzi di memoria non validi o il tentativo di eseguire istruzioni non permesse.

La maggior parte dei processori prevede di trattare l'errore come se fosse un particolare tipo di interrupt. In questo modo, quando si verifica un errore che impedisce al processore di procedere normalmente con l'esecuzione delle istruzioni, viene attivata, attraverso un opportuno vettore di interrupt, una routine del SO che decide come gestire l'errore stesso. Spesso la gestione consiste nella terminazione forzata (abort) del programma che ha causato l'errore, eliminando il processo.

#### Priorità e abilitazione degli interrupt

Abbiamo visto che gli interrupt possono essere nidificati, cioè che un interrupt può interrompere una routine di interrupt relativa ad un evento precedente. Non sempre è opportuno concedere questa possibilità - ovviamente è sensato che un evento molto importante e che richiede una risposta urgente possa interrompere la routine di interrupt che serve un evento meno importante, ma il contrario invece deve essere evitato. Un meccanismo molto diffuso per gestire questo aspetto è il seguente:

1. il processore possiede un livello di priorità che è scritto in un apposito registro all'interno del processore stesso (**registro di stato** - **PSR**)
2. il livello di priorità del processore può essere modificato dal software tramite opportune istruzioni macchina che scrivono nel registro di stato
3. anche agli interrupt viene associato un livello di priorità, che è fissato nella configurazione fisica del calcolatore: ad esempio i livelli sono 8, da 0 (minimo) a 7 (massimo)
4. un interrupt viene accettato, cioè si passa ad eseguire la sua routine di interrupt, solo se il suo livello di priorità è superiore al livello di priorità del processore in quel momento, altrimenti l'interrupt viene tenuto in sospeso fino al momento in cui il priorità del processore non sarà abbassato sufficientemente

Utilizzando questo meccanismo hardware il sistema operativo può alzare e abbassare la priorità del processore in modo che durante l'esecuzione delle routine di interrupt più importanti non vengano accettati interrupt meno importanti.

Priorità di alcuni tipi di periferica:

| Periferica | Priorità |
| ---------- | -------- |
| Terminali  | 4        |
| Dischi     | 5        |
| Orologio   | 6        |

#### Accesso diretto alla memoria da parte delle periferiche (DMA)

Alcune periferiche veloci (ad esempio i dischi magnetici non richiedono al processore di intervenire per la lettura o scrittura di ogni singolo dato ma possono trasferire il contenuto di molte parole da (o alla) memoria autonomamente.&#x20;

Tipicamente la periferica possiede dei registri nei quali il processore scrive le seguenti informazioni:

1. l'indirizzo della memoria dal quale iniziare il trasferimento
2. l'indirizzo sulla periferica dal quale iniziare il trasferimento
3. numero di parole da trasferire
4. la direzione del trasferimento (lettura o scrittura in memoria)

Il processore, dopo aver inizializzato tali registri, ordina alla periferica di iniziare l'operazione scrivendo il comando "start" nel registro del comando e poi passa a eseguire altri programmi; dopo aver completato tutta l'operazione la periferica genera un interrupt per avvisare il processore. Nella figura è mostrato questo meccanismo con riferimento a un disco al quale viene richiesta un'operazione di lettura. Si tenga presente che i dati sul disco sono organizzati in settori; ogni settore è identificato dal proprio numero.

Ai fini della realizzazione del SO l'esistenza delle periferiche in DMA è importante per due motivi: primo perché richiede che il SO gestisca le aree di memoria adatte  al trasferimento in DMA dalle periferiche (in particolare dai dischi), dette buffer, e secondo perché il SO distingue tra i gestori di periferiche in DMA (**block device**) e quelli delle periferiche normali (**character device**).

Sotto, lettura da disco tramite DMA:

![](https://875337804-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lt58INaXg-cFL27pnx0%2F-M6JG1u7eSCILd06yY-B%2F-M6JX9BPQU-KcDr34cVy%2FDMA_a.png?alt=media\&token=8b01099e-faf4-4837-b9a2-e68fad3bc761)

![](https://875337804-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lt58INaXg-cFL27pnx0%2F-M6Jfyp3h98VFKlMLeGR%2F-M6JfkWuH1E9BQUKWVvF%2FDMA_B.png?alt=media\&token=bdedad11-835d-47e0-b453-1f5db82d6e7e)

![](https://875337804-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lt58INaXg-cFL27pnx0%2F-M6Jfyp3h98VFKlMLeGR%2F-M6Jkfy7Qcn7pewGA_48%2FDMA_C.png?alt=media\&token=7413fa2b-5c34-4503-b529-27fd79e9f86e)

#### LE SLIDE

{% file src="<https://875337804-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Lt58INaXg-cFL27pnx0%2F-M6JkiGf06fLcaO7aQHI%2F-M6JngQJwsLt7lLard-r%2FEventi_asincroni_periferiche.pptx?alt=media&token=2c4a48ec-1a43-4bff-8921-5295b434808e>" %}
Eventi asincroni - periferiche
{% endfile %}
