Sei sulla pagina 1di 104

Architettura degli elaboratori e Sistemi operativi Pag.

1 a 103

Sommario
1.0 Tipi di dati e loro rappresentazione ................................................................................................... 5
1.1.1 Conversione da base x in base 10..................................................................................................... 5
1.1.2 Conversione da base 10 a base x ...................................................................................................... 5
1.1.3 Operazioni di somma e sottrazione .................................................................................................. 6
1.2 Rappresentazione dei numeri relativi ...................................................................................................... 6
1.2.1 Rappresentazione in modulo e segno .............................................................................................. 6
1.2.2 Rappresentazione in complemento a uno ....................................................................................... 7
1.2.3 Rappresentazione in complemento a due........................................................................................ 7
1.2.4 Confronto tra le varie rappresentazioni ........................................................................................... 9
1.3 Rappresentazione dei numeri razionali ................................................................................................... 9
1.3.1 Floating Point .................................................................................................................................. 10
2.0 Organizzazione dei sistemi di calcolo ............................................................................................... 13
2.1 Macchina di Von Neumann ................................................................................................................... 14
2.2 Processore.............................................................................................................................................. 14
2.2.1 Data Path ........................................................................................................................................ 15
2.2.2 Esecuzione dell’istruzione .............................................................................................................. 15
2.2.3 Tassonomia di Flynn ....................................................................................................................... 16
2.2.4 Parallelismo a livello d’istruzione ................................................................................................... 16
2.2.5 Parallelismo a livello di processore ................................................................................................ 18
2.3 Memoria ................................................................................................................................................ 19
2.3.1 RAM ................................................................................................................................................ 20
2.3.2 ROM ................................................................................................................................................ 20
2.3.3 Memoria cache ............................................................................................................................... 20
2.3.4 Dischi magnetici.............................................................................................................................. 21
2.3.5 RAID ................................................................................................................................................ 21
2.5 Codice di Hamming ................................................................................................................................ 25
3.0 Livello logico digitale ...................................................................................................................... 29
3.1 Porte logiche .......................................................................................................................................... 29
3.1.1 Legge di de Morgan ........................................................................................................................ 29
3.2 Circuiti combinatori ............................................................................................................................... 30
3.2.1 Comparatore................................................................................................................................... 30
3.2.2 Decodificatore ................................................................................................................................ 30
3.2.3 Multiplexer ..................................................................................................................................... 31
3.3 Circuiti per l’aritmetica .......................................................................................................................... 32
3.3.1 ALU.................................................................................................................................................. 32

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 2 a 103

3.3.2 Circuiti sommatori .......................................................................................................................... 33


3.4 Circuiti di memorizzazione ..................................................................................................................... 34
3.4.1 Latch SR .......................................................................................................................................... 34
3.4.2 Latch SR temporizzato .................................................................................................................... 34
3.4.3 Latch D ............................................................................................................................................ 35
3.4.4 Generatore di impulsi ..................................................................................................................... 35
3.4.5 Flip-flop D ....................................................................................................................................... 35
3.5 Bus del calcolatore ................................................................................................................................. 36
3.5.1 Ampiezza del bus ............................................................................................................................ 36
3.5.2 Temporizzazione del bus ................................................................................................................ 37
3.5.3 Arbitraggio del bus ......................................................................................................................... 37
4.0 Livello ISA, istruzioni e indirizzamento ............................................................................................ 38
4.1 Tipologie di istruzioni ............................................................................................................................. 38
4.1.1 Formato istruzioni .......................................................................................................................... 40
4.1 Indirizzamento ....................................................................................................................................... 41
4.2.1 Immediato ...................................................................................................................................... 41
4.2.2 Diretto............................................................................................................................................. 41
4.2.3 A registro ........................................................................................................................................ 41
4.2.4 A registro indiretto ......................................................................................................................... 41
4.2.5 Indicizzato ....................................................................................................................................... 42
4.2.6 Indicizzato esteso ........................................................................................................................... 42
4.2.7 A stack............................................................................................................................................. 42
4.3 Notazione polacca inversa ..................................................................................................................... 42
5.0 Sistema operativo, gestione dei processi e schedulazione ................................................................ 45
5.1 Sistema operativo .................................................................................................................................. 45
5.2 Processo ................................................................................................................................................. 46
5.2.1 Modi di esecuzione ......................................................................................................................... 47
5.2.2 Creazione processo......................................................................................................................... 47
5.2.3 Terminazione processo................................................................................................................... 48
5.2.4 Descrizione del processo ................................................................................................................ 48
5.2.5 Commutazione di contesto ............................................................................................................ 48
5.2.6 Modello a 2 stati ............................................................................................................................. 49
5.2.7 Modello a 5 stati ............................................................................................................................. 49
5.2.8 Modello a 6 stati ............................................................................................................................. 51
5.2.9 Modello a 7 stati ............................................................................................................................. 52
5.3 Schedulazione ........................................................................................................................................ 53

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 3 a 103

5.3.1 Scheduler di lungo termine ............................................................................................................ 54


5.3.2 Lo scheduler di medio termine ....................................................................................................... 54
5.3.3 Scheduler di breve termine (dispatcher) ........................................................................................ 54
5.4 Algoritmi di schedulazione..................................................................................................................... 55
5.4.1 Algoritmo first come first Served - non Preemptive....................................................................... 55
5.4.2 Algoritmo event Driven – Preemptive ............................................................................................ 55
5.4.3 Algoritmo Round Robin – Preemptive ............................................................................................ 56
5.4.4 Algoritmo Highest response Ratio next – non Preemptive ............................................................ 56
5.4.5 Algoritmo Shortest Process Next – non Preemptive ...................................................................... 57
5.4.6 Schedulazione a code multiple ....................................................................................................... 57
6.0 Thread, SMP, Kernel ....................................................................................................................... 58
6.1 Thread.................................................................................................................................................... 58
6.1.2 Multithreading ................................................................................................................................ 59
6.1.3 Stati dei thread ............................................................................................................................... 59
6.1.4 Categorie di thread ......................................................................................................................... 60
6.2 Symmetric Multi Processing (SMP) ........................................................................................................ 62
6.3 Kernel e MicroKernel.............................................................................................................................. 63
7.0 Concorrenza ................................................................................................................................... 65
7.1 Mutua esclusione ................................................................................................................................... 66
7.1.1 Approccio software: Algoritmo di dekker ...................................................................................... 67
7.1.2 Approccio hardware: test&set/swap ............................................................................................. 70
7.1.3 Utilizzare il supporto del SO: semafori/monitor ............................................................................ 72
7.2 Stallo ...................................................................................................................................................... 76
7.2.1 Rilevare ed eliminare lo stallo ........................................................................................................ 77
7.2.2 Prevenire lo stallo ........................................................................................................................... 79
7.2.3 Impedire lo stallo rimuovendo le condizioni .................................................................................. 82
8.0 Gestione della memoria centrale .................................................................................................... 84
8.0.1 Spazi degli indirizzi .......................................................................................................................... 84
8.1 Gestione della memoria contigua.......................................................................................................... 87
8.1.1 Monoallocazione ............................................................................................................................ 87
8.1.2 Partizionamento statico ................................................................................................................. 88
8.1.3 Partizionamento dinamico ............................................................................................................. 90
8.2 Gestione della memoria non contigua................................................................................................... 92
8.2.1 Segmentazione ............................................................................................................................... 92
8.2.2 Paginazione..................................................................................................................................... 95
8.2.3 Memoria virtuale ............................................................................................................................ 99

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 4 a 103

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 5 a 103

1.0 Tipi di dati e loro rappresentazione


Rappresentazione di un numero

▪ Per rappresentare un numero 𝑛 ∈ ℕ (numeri interi positivi) in base b sono necessarie log 𝑏 𝑛 cifre.
▪ Per rappresentare un numero 𝑛 ∈ ℕ in base 2 sono necessarie log 2 𝑛 bit.

1.1.1 Conversione da base x in base 10


Per convertire in base 10 un numero rappresentato in una qualsiasi base x, bisogna procedere nel seguente
modo: Sommare le cifre del numero moltiplicate per la base b, elevata alla potenza della posizione che
occupa la cifra.

Esempi:

235 𝑖𝑛 𝑏𝑎𝑠𝑒 8 = 2 × 82 + 3 × 81 + 5 × 80 = 128 + 24 + 5 = 157 𝑖𝑛 𝑏𝑎𝑠𝑒 10

10011 𝑖𝑛 𝑏𝑎𝑠𝑒 2 = 1 × 24 + 0 × 23 + 0 × 22 + 1 × 21 + 1 × 20 = 16 + 2 + 1 = 19 𝑖𝑛 𝑏𝑎𝑠𝑒 10

1.1.2 Conversione da base 10 a base x


Si procede nel seguente modo:

1. Dividere il numero da convertire per la base x fino a quando l’ultimo quoziente è diverso da 0
2. Dopodiché, il numero convertito si ottiene prendendo i resti ottenuti partendo dell’ultimo al primo e
scrivendoli da sinistra verso destra.

Esempio: convertire il numero 12 da base 10 a base 2

12 ÷ 2 = 6 𝑐𝑜𝑛 𝑟𝑒𝑠𝑡𝑜 0
6 ÷ 2 = 3 𝑐𝑜𝑛 𝑟𝑒𝑠𝑡𝑜 0
Quindi 1100 in base 2
3 ÷ 2 = 1 𝑐𝑜𝑛 𝑟𝑒𝑠𝑡𝑜 1
1 ÷ 2 = 0 𝑐𝑜𝑛 𝑟𝑒𝑠𝑡𝑜 1
Esempio: convertire il numero 120 da base 10 a base 8

120 ÷ 8 = 15 𝑐𝑜𝑛 𝑟𝑒𝑠𝑡𝑜 0


15 ÷ 8 = 1 𝑐𝑜𝑛 𝑟𝑒𝑠𝑡𝑜 7 Quindi 170 in base 8

1÷8=0 𝑐𝑜𝑛 𝑟𝑒𝑠𝑡𝑜 1


Osservazione: Si osservi che la conversione dalla base 2 alla base 16 e/o 8, e viceversa, è più semplice e
veloce di quella da decimale ad altre basi. Infatti, basta considerare che per rappresentare le sedici cifre
diverse del codice esadecimale occorrono 4 bit (24 = 16), mentre per rappresentare le 8 cifre diverse del
codice ottale occorrono 3 bit (23 = 8).

Ne risulta che per convertire un numero binario in esadecimale, o in ottale, è sufficiente raggruppare le
cifre binarie rispettivamente in gruppi di quattro o tre cifre, a partire da quelle meno significative (quelle
più a destra). Si ricava immediatamente il numero grazie alla sostituzione dei bit così ricavati con la cifra
esadecimale o ottale corrispondente.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 6 a 103

Esempio: conversione da binario in esadecimale

111 1111 0001 1010 𝑖𝑛 𝑏𝑎𝑠𝑒 2 = 7𝐹1𝐴 𝑖𝑛 𝑏𝑎𝑠𝑒 16 , dove:

▪ 1010 (𝑐𝑜𝑛𝑣𝑒𝑟𝑠𝑖𝑜𝑛𝑒 𝑑𝑎 𝑏𝑖𝑛𝑎𝑟𝑖𝑜 𝑎 𝑑𝑒𝑐𝑖𝑚𝑎𝑙𝑒) = 10 𝑐ℎ𝑒 𝑖𝑛 𝑒𝑠𝑎𝑑𝑒𝑐𝑖𝑚𝑎𝑙𝑒 𝑐𝑜𝑟𝑟𝑖𝑠𝑝𝑜𝑛𝑑𝑒 𝑎𝑑 𝐴


▪ 0001 = 1 𝑐ℎ𝑒 𝑖𝑛 𝑒𝑠𝑎𝑑𝑒𝑐𝑖𝑚𝑎𝑙𝑒 𝑐𝑜𝑟𝑟𝑖𝑠𝑝𝑜𝑛𝑑𝑒 𝑎𝑑 1
▪ 1111 = 15 𝑐ℎ𝑒 𝑖𝑛 𝑒𝑠𝑎𝑑𝑒𝑐𝑖𝑚𝑎𝑙𝑒 𝑐𝑜𝑟𝑟𝑖𝑠𝑝𝑜𝑛𝑑𝑒 𝑎𝑑 𝐹
▪ 111 = 7 𝑐ℎ𝑒 𝑖𝑛 𝑒𝑠𝑎𝑑𝑒𝑐𝑖𝑚𝑎𝑙𝑒 𝑐𝑜𝑟𝑟𝑖𝑠𝑝𝑜𝑛𝑑𝑒 𝑎𝑑 7
Esempio: conversione da binario in ottale

101 111 110 = 574 𝑖𝑛 𝑏𝑎𝑠𝑒 8, 𝑑𝑜𝑣𝑒:


▪ 101 = 5 𝑐ℎ𝑒 𝑖𝑛 𝑜𝑡𝑡𝑎𝑙𝑒 𝑐𝑜𝑟𝑟𝑖𝑠𝑝𝑜𝑛𝑑𝑒 𝑎 5
▪ 111 = 7 𝑐ℎ𝑒 𝑖𝑛 𝑜𝑡𝑡𝑎𝑙𝑒 𝑐𝑜𝑟𝑟𝑖𝑠𝑝𝑜𝑛𝑑𝑒 𝑎 7
▪ 110 = 5 𝑐ℎ𝑒 𝑖𝑛 𝑜𝑡𝑡𝑎𝑙𝑒 𝑐𝑜𝑟𝑟𝑖𝑠𝑝𝑜𝑛𝑑𝑒 𝑎 5
Esempio: conversione da esadecimale in binario

𝐴𝐵2 𝑖𝑛 𝑏𝑎𝑠𝑒 16 = 1010 1011 0010 𝑖𝑛 𝑏𝑎𝑠𝑒 2, 𝑑𝑜𝑣𝑒:


▪ 𝐴16 = 1010 = 1010 𝑖𝑛 𝑏𝑎𝑠𝑒 2
▪ 𝐵16 = 1110 = 1011 𝑖𝑛 𝑏𝑎𝑠𝑒 2
▪ 216 = 210 = 0010 𝑖𝑛 𝑏𝑎𝑠𝑒 2

1.1.3 Operazioni di somma e sottrazione


Nel sistema binario le operazioni di somma e sottrazione si
effettuano nella stessa maniera in cui le effettueremmo in
base 10, considerando però i riporti alla base 2.

1.2 Rappresentazione dei numeri relativi


1.2.1 Rappresentazione in modulo e segno
Con questa rappresentazione viene destinato il bit più significativo al segno in modo tale
che:

▪ Se 0 il segno è +
▪ Se 1 il segno è −

Con la rappresentare modulo e segno è possibile rappresentare:

[−(2𝑛−1 − 1), (2𝑛−1 − 1)] valori. Un esempio su una parola da 3 bit: [−22 − 1, 22 − 1] = [−3, +3]

Lo zero può essere rappresentato in due modi diversi (±0), è quindi ridondante. Il problema della
rappresentazione in modulo e segno è l’overflow che si produce laddove la somma dei moduli di due parole
costituisce un valore non rappresentabile con il numero di bit disponibili. Il bit in eccesso dovuto ai vari
riporti, detto carry, deve essere gestito per garantire la validità del risultato.

Per effettuare un’operazione di somma, occorre prima verificare il segno dei due addendi e poi decidere se
fare la somma o la sottrazione:

▪ 0001 + 0101 | entrambi sono positivi, si fa la somma e il risultato è positivo: 0110

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 7 a 103

▪ 0101 + 1001 | Il secondo è negativo ed il valore assoluto è minore del primo, quindi va fatta la
sottrazione: 101 − 001 = 100 ed il risultato è positivo: 0100
▪ 0001 + 1111 | Va fatta ancora la sottrazione, ma il secondo è in valore assoluto maggiore del primo;
quindi, 111 − 001 = 110 ed il risultato è negativo: 1110
▪ 1101 + 1010 | Entrambi negativi, si fa la somma 101 + 010 = 111, il risultato è negativo: 1111

Esempi con numeri in base 10:

▪ 15 + 10 | entrambi positivi si fa la somma: 15 + 10 = 25


▪ 15 + (−10) | il secondo è negativo ed il valore assoluto è minore del primo; quindi, va fatta la
sottrazione: 15 − 10 = 5
▪ −5 + 10 | Il primo è negativo ed il secondo è in valore assoluto maggiore del primo, quindi va fatta
la sottrazione: 10 − 5 = 5
▪ −5 − 5 | entrambi negativi, si fa la somma: −5 − 5 = −10

In generale:

▪ PRO: La rappresentazione in modulo e segno è molto comprensibile, perché analizzando il bit più
significativo riesco subito a individuare il segno.
▪ CONTRO: Possibili overflow e underflow e meccanismi somma/sottrazione troppo complicati da
implementare.

1.2.2 Rappresentazione in complemento a uno


Con la rappresentazione in complemento a uno i numeri positivi sono rappresentati normalmente, mentre i
numeri negativi sono rappresentati dal complemento a 1 del corrispondente numero positivo. Con
operazione di complemento a 1 s’intende quell’operazione che trasforma ciascun bit ‘0’ in ‘1’ e viceversa.

Per esempio: la rappresentazione in complemento a 1 di −5 con 6 bit si ottiene considerando che la


rappresentazione (con 6 bit) di +5 è 000101. Effettuando il complemento a 1 si ottiene 111010.

Come per il modulo e segno anche in questo caso il valore 0 sarà rappresentato con due configurazioni,
cioè +0 e −0, di conseguenza, l’intervallo rappresentabile sarà: [−(2𝑛−1 − 1), (2𝑛−1 − 1)]

La somma o la sottrazione in complemento ad


uno consiste nel sommare i valori, e, laddove
l’operazione produce un riporto successivo al
bit del segno, quest’ultimo viene aggiunto al
risultato.

▪ PRO: Somma e sottrazione immediata


▪ CONTRO: Il riporto eccessivo va sommato.
La somma o la sottrazione deve stare
comunque nell’intervallo rappresentabile.

1.2.3 Rappresentazione in complemento a due


Il complemento a due è il metodo più diffuso per la rappresentazione dei numeri relativi in informatica. È
stato ideato per evitare la presenza di due zeri (uno positivo e uno negativo) e per utilizzare tutte le
possibili 2𝑛 combinazioni che si possono ottenere con n bit.

Visto che lo zero viene rappresentato una sola volta, la rappresentazione occupata dallo zero nel
complemento a uno diventa la rappresentazione di un altro valore; in particolare viene rappresentato un
valore negativo in più, quindi l’intervallo rappresentabile diventa: [−2𝑛−1 , 2𝑛−1 − 1].

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 8 a 103

I numeri negativi vengono rappresentato con il complemento a uno incrementato di 1.

Esempio: Rappresentiamo −5 con 4 bit in complemento a due. Possiamo vedere −5 come il complemento
a 2 di +5 : si scrive la rappresentazione binaria del numero +5 = 0101 e si calcola il complemento a uno,
invertendo i bit, ed ottenendo 1010. Per ottenere il complemento a due di +5 aggiungiamo 1 al
complemento a uno, ottenendo 1011 = −5.

Dimostrazione unica rappresentazione dello zero. Si può notare che ±0 si equivalgono, in quanto la somma
finale genera un overflow che non viene considerato in quanto sfora il numero di bit in considerazione.

Effetto ruota complemento a due: Se mostriamo in ordine di rappresentazione


binaria si può notare che ad un certo punto, superato il massimo numero
positivo rappresentabile si scatta al massimo numero negativo. Per esempio: se
si calcola 7 + 1 su 4 bit ho un risultato errato, perché 8 non è rappresentabile
quindi scatta a −8 (errore).

Se due operandi dello stesso segno danno un risultato di segno opposto vuol dire
che è stata superata la capacità di calcolo (overflow). Banalmente, con due
operandi di segno opposto l’overflow non può mai verificarsi.

Conversione binario-decimale: Avendo un numero scritto in complemento a due


è possibile calcolare velocemente il suo valore: considero solo i valori 1 senza
considerare il bit di segno. Sommo al valore di potenza di 2 negato
corrispondente al bit più significativo i valori di potenza di due corrispondenti ai
bit avvalorati a 1:

Somma e sottrazione: Si procede normalmente ignorando l’eventuale bit di overflwow.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 9 a 103

In generale:

▪ PRO: È possibile rappresentare tutte le 2𝑛 combinazioni di n bit dato che lo zero viene rappresentato
solo con una configurazione binaria. Somma e sottrazione sono molto veloci.
▪ CONTRO: Somma e sottrazione deve stare nell’intervallo rappresentabile.

1.2.4 Confronto tra le varie rappresentazioni

1.3 Rappresentazione dei numeri razionali


▪ Numeri irrazionali: Numeri con cifre dopo la virgola infinite. I numeri periodici però sono razionali
(perché per quanto hanno un numero infinito di cifre decimali, esiste uno schema di ripetizione).
▪ Numeri razionali: Numeri che si possono esprimere sotto forma di frazione n/m. Dove n ed m sono due
numeri relativi.

Per rappresentazione un numero razionale vi sono due strategie:

▪ Notazione in virgola fissa: dedico un numero di bit (fisso) alla parte intera e un numero di bit (fisso) alla
parte decimale. I soldi sono un esempio pratico di applicazione della notazione in virgola fissa.
▪ Notazione in virgola mobile (floating point): faccio scorrere la virgola secondo le esigenze di
rappresentazione.

Qualsiasi numero può essere scritto secondo la formula: ±𝑀 × 𝑏 ±𝑒 , dove:

▪ M: mantissa = valore espresso in 0,xxx


▪ B: base
▪ E: esponente

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 10 a 103

Quando tale sistema viene applicato alla base 10 prende il nome di notazione scientifica. Naturalmente tale
rappresentazione dovrà essere approssimata destinando un certo numero di bit alla mantissa e un certo
numero di bit all’esponente. La base rimane fissa cioè 2 in caso di numeri binari.

Osservazioni:

▪ La mantissa determina la precisione


▪ L’esponente determina la posizione della virgola
▪ Moltiplicare un numero per una potenza della base equivale e far scorrere il numero di un numero di
posizioni pari all’esponente (destra o sinistra). Se il segno dell’esponente è positivo, si fa scorrere la
virgola verso destra, se negativo a sinistra.

1.3.1 Floating Point


Il floating point è stato definito con due precisioni di base:

▪ Precisione singola (float)


▪ Precisione doppia (double)

Un numero in floating point in precisione singola occupa 4


byte e un numero in precisione doppia occupa 8 byte.

Gli esponenti non sono rappresentati in complemento a due, perché altrimenti bisognerebbe
rappresentare un esponente negativo in più. Ma i valori rappresentati sono meno di quelli potenzialmente
rappresentabili, mancano 2 bit di esponente che non sono rappresentabili. Mancano le configurazioni
corrispondenti a tutti i bit 0 e tutti 1, perché quest’ultimi sono utilizzati per configurazioni riservate.

All’interno di una qualsiasi codifica, si dice che una configurazione è riservata nel momento in cui, sto
decidendo il tipo di codifica (complemento a uno, due, ...) e specifico che alcuni particolari valori
rappresentino un’eccezione, cioè che possono assumere un altro significativo rispetto alla codifica scelta.

Esempio pratico: in complemento a uno abbiamo due rappresentazioni dello zero. Potremmo decidere di
usare una configurazione dello zero per rappresentare una situazione di errore (configurazione riservata).

L’esponente è memorizzato con un bias, cioè con uno sfasamento. Si rappresentano gli esponenti in forma
polarizzata, cioè memorizzo il valore binario corrispondente all’esponente sommato ad una costante (bias).

▪ Se devo memorizzare −126, devo fare −126 + 127 = 1 e memorizzare 1.


▪ Se devo memorizzare 127, devo fare 127 + 127 = 255
▪ Non è possibile rappresentare −127, poiché −127 + 127 = 0 che è una configurazione riservata.

Se gli 8 bit dell’esponente valgono 101000112 = 16310 , l’esponente vale 163 − 127 = +36

Perché polarizzato?: Risulta efficiente se dobbiamo controllare la maggioranza o la minoranza di un


numero: A parità di mantissa, il numero più grande sarà quello con l’esponente più alto. Rappresentando
l’esponente in complemento a due si avrà il problema della ruota: il valore 1111 (−1) espresso in binario è
più grande del valore 0000 (0), ma −1 è più piccolo. Vi sono diverse tipologia di numeri nel floating point:

±∞ viene utilizzato per rappresentare la


somma/sottrazione di due numeri andati in
overflow/underflow.

NaN (not a number) è il risultato di un’operazione


non valida, ad esempio la divisione tra zero e
zero.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 11 a 103

Numeri normalizzati
Un numero normalizzato espresso in floating point è definito come segue: ±𝟏, 𝒙𝒙𝒙𝟐 × 𝟐𝒚𝒚𝒚

Lo standard 𝐼𝐸𝐸𝐸754 indica che:

▪ Precisione singola a 32 bit : 1 bit x segno, 8 bit x esponente, 23 bit x mantissa


▪ Precisione doppia a 64 bit : 1 bit x segno, 11 bit x esponente, 52 bit x mantissa

Tutte le mantisse iniziano per 1 (non memorizzato poiché è una convenzione) e tutti i bit corrispondenti alla
mantissa espressi in base 2 moltiplicato base per esponente (yyyy) espresso in forma polarizzata.

Per la parte intera si procede come già visto: si converte il numero decimale in binario. Per la parte
decimale si moltiplica il valore per 2 e si prende la cifre intera ricavata, la si sottrae e si procede fin quando:

1. Non si esaurisce la precisione (numero di cifre binarie che è possibile memorizzare)


2. Il risultato è 1

Esempio: 19,312510 =?2

Step 1: Parte intera 1910 = 100112

Step 2: Parte decimale (3125):

▪ 0,3125 × 2 = 0,625
▪ 0,625 × 2 = 1,250
▪ 0,250 × 2 = 0,500
▪ 0,500 × 2 = 1,000 (FINE ALGORITMO, ho ottenuto 1,000)

Quindi 19,312510 = 10011,01012

A differenza della conversione dei numeri interi non si legge il risultato al contrario!

Per convertirlo in floating point scarto il primo 1 (perché esso è implicito, tutte le mantisse iniziano con 1),
diventa 00110101 e tutti zero fino a quando non si esauriscono i bit destinati alla mantissa.

Per verifica la correttezza: 16 + 2 + 1 + 0,25 + 0,0625 = 19,3125

𝟐𝟒 𝟐𝟑 𝟐𝟐 𝟐𝟏 𝟐𝟎 𝟐−𝟏 𝟐−𝟐 𝟐−𝟑 𝟐−𝟒


1 0 0 1 1 , 0 1 0 1
16 2 1 0,25 0,0625

Dopo aver effettuato la conversione si imposta l’esponente in maniera tale da far scorrere a sinistra la
virgolare del numero di posizioni necessarie per rappresentare il numero corrente. L’esponente è 4,
memorizzato in forma polarizzata: sommo 4 con 127 (bias) = 131 e converto in binario = 1000 00112 .
Quindi abbiamo:

0 1 0 0 0 0 0 1 1 0 0 1 1 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Numeri denormalizzati
Un numero denormalizzato espresso in floating point è definito come: ±𝟎, 𝒙𝒙𝒙𝒙𝟐 . Non c’è moltiplicazione
con la base e l’esponente perché 20 = 1. Quindi la mantissa è sempre espressa come tra 0 e 1 e i bit
dell’esponente sono impostati a 0.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 12 a 103

Conclusioni:

Numeri rappresentati in floating point:

▪ Zero assoluto.
▪ Insieme di numeri che vanno da 0 a 2−149 e
−2−149 che non è possibile rappresentare,
quindi sarà 0. Questo è detto assorbimento: si
ha un numero così piccolo, che non potendolo
rappresentare, viene approssimato con lo zero
▪ L’intervallo dei numeri negativi rappresentabili e numeri positivi rappresentabili dove ci sono i numeri
normalizzati e denormalizzati. −2128 e 2128 sono esclusi, poiché sono configurazioni riservate.
▪ Numero in overflow corrisponde a ±∞.

In generale l’aritmetica virgola mobile (aritmetica di macchina) è affetta da alcune problematiche:

▪ Non sono valide, in generale, la proprietà associativa e la proprietà distributiva. Cioè, non è garantita la
certezza del funzionamento.
▪ Assorbimento: ad esempio 1015 + 1 = 1015
▪ Cancellazione: si ottiene quando sottraendo due numeri molti vicini si ottiene 0.
▪ Arrotondamento.

Confronti di uguaglianza: non è possibile confrontare direttamente due numeri: Anche lo stesso numero,
derivato da due operazioni differenti potrebbero essere approssimato. Quindi, il confronto si effettua
utilizzando la formula della distanza: 𝐴 = 𝐵 ⟺ |𝐴 − 𝐵| < 𝜀 . Dove ε, è un margine di tolleranza.

Confronto maggiore/minore: non a caso i valori vengono memorizzati nell’ordine segno, esponente,
mantissa. Per confrontarli è sufficiente scorrere trai i bit dei due numeri, partendo da sinistra verso destra,
fino a quando non si trova un bit diverso:

▪ Se si trova nel segno, è più grande il numero con il segno positivo.


▪ Se si trova nell’esponente o nella mantissa è più grande il numero con il bit a 1.

Somma/sottrazione:

▪ Allineo i due numeri per raggiungere lo stesso esponente


▪ Si sommano le mantisse
▪ Si normalizza il risultato, cioè si converte in floating point
▪ Si controlla se è overflow o underflow
▪ Si arrotonda il numero
▪ Se non è normalizzato, lo si normalizza

Prodotto/divisione :

▪ Si sommano gli esponenti


▪ Si moltiplicano le mantisse
▪ Si normalizza il risultato, cioè si converte in floating point
▪ Si controlla se è overflow o underflow
▪ Si arrotonda il numero
▪ Se non è normalizzato, lo si normalizza

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 13 a 103

2.0 Organizzazione dei sistemi di calcolo


Un calcolatore è una macchina fisica che può risolvere problemi eseguendo le istruzioni che le vengono
assegnate. Con il termine programma intendiamo una sequenza d’istruzioni che descrive come portare a
termina un dato compito. I circuiti elettronici dei computer possono riconoscere ed eseguire direttamente
soltanto un insieme limitato d’istruzioni elementari:

▪ Sommare due numeri.


▪ Controllare se un numero vale zero.
▪ Copia dei dati da una parte all’altra della memoria.

L’insieme di queste istruzioni primitive formano un linguaggio, chiamato linguaggio macchina, attraverso il
quale è possibile comunicare con il computer. A causa della difficoltà di strutturare un programma in
linguaggio macchina si è pensato di sviluppare i computer secondo una serie di macchine astratte.
Una macchina astratta non è altro che un'astrazione del concetto di calcolatore fisico. Questo metodo si
chiama approccio strutturale.

Si è pensato quindi di strutturare l’elaboratore secondo gerarchie definite: consideriamo la macchina come
il livello 0 dell’elaboratore, ovvero il linguaggio macchina L0 dove si opera a livello di bit. Sulla base del
linguaggio macchina si creano una serie di livelli di astrazione tali da rendere più facile la programmazione
dell’elaboratore. Comunque noi scriviamo programmi questi devono essere necessariamente, in qualche
modo, trascritti in linguaggio macchina L0 per essere eseguiti :

▪ Compilazione: il compilatore traduce tutte le istruzioni di un programma scritto in Ln in linguaggio


macchina, creando un modulo oggetto. I linguaggi compilati sono più veloci poiché vengono letti
immediatamente dalla macchina, ma per essere avviati devono essere, per lo appunto, prima compilati,
cioè trasformati in linguaggio macchina. Se si desidera effettuare delle modifiche al programma, si
dovrà ricompilare il codice.
▪ Interpretazione: Traduce ed esegue ogni singola istruzione del programma. Legge ed esegue il codice
sorgente del programma senza creare un modulo oggetto. Questi linguaggi sono molto versatili poiché
vengono interpretati nel momento in cui vengono eseguiti. Lo svantaggio è che devono essere tradotti
ogni volta che vengono avviati e la traduzione spreca tempo computazionale.

Si può quindi immaginare l’elaboratore strutturato in una serie di macchine astratte M con un linguaggio L
dedicato. L’elaboratore moderno è costituito, in generale, da sei livelli:

▪ LV-0) Rappresenta il vero e proprio hardware della macchina.


▪ LV-1) Occupato dai registri e l’ALU (data path), questo livello si occupa di come i dati vengono veicolati.
▪ LV-2) Livello ISA (Livello di architettura dell’insieme d’istruzione) Si tratta dell’insieme di istruzione base
su cui vengono scritti i relativi programmi nei vari linguaggi di programmazione a più alto livello.
▪ LV-3) Livello del sistema operativo.
▪ LV-4) Linguaggio assemblativo.
▪ LV-5) Linguaggi di alto livello, come c++/java/php.

Quando un’istruzione di alto livello deve essere eseguita, viene decodificata e passata a quella di livello
inferiore. Questo muoverà tutte le macchina astratte fino al livello 0, dove verrà processata, e il risultato
risalirà i livelli fino alla macchina astratta di partenza. L’insieme dei tipi di dati, delle operazioni e delle
funzionalità di ciascun livello è chiamato architettura.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 14 a 103

2.1 Macchina di Von Neumann


Una delle macchine più semplici è quella di Von Neumann
(“FON-NOI-MAN”). Le componenti della macchina di Von
Neumann sono:

▪ Memoria: consente di operare in maniera volatile


▪ Control Unit: gestisce le istruzioni.
▪ ALU: parte del processore che contiene i circuiti che
consentono di effettuare le operazioni.
▪ All’interno dell’ALU è presente un registro detto
accumulatore: consente di accumulare i vari risultati del singolo calcolo specifico (il resto va in RAM).
▪ INPUT/OUTPUT: registri per interfacciarsi con la macchina o visualizzarne i risultati.

Legge di Moore: la legge di moore non è una legge dimostrata con tecniche matematiche, ma osservando
la realtà. La legge descrive l’avanzamento tecnologico delle macchine e dice che ogni 18 mesi le
architetture cambiano e migliorano del 60% in più.

2.2 Processore
La CPU (Central Processing Unit) è il “cervello” del computer e la sua funzione è quella di eseguire i
programmi contenuti nella memoria principale prelevando le loro istruzioni, esaminandole ed eseguendole
una dopo l’altra. I componenti della CPU sono connessi fra loro mediante un bus, cioè un insieme di cavi sui
quali vengono trasmessi indirizzi, dati e segnali di controllo. I bus possono essere esterni alla CPU, per
connetterla alla memoria e ai dispositivi di i/o, oppure interni. La CPU è composta da vari parti:

1. Unità di controllo: (core) preleva le istruzioni dalla memoria principale e ne determina la tipologia.
2. Unità aritmetica logica (ALU): esegue le operazioni aritmetiche (+,-,*,/) o logiche (AND, OR, NOT ecc.…)
3. Registri: una piccola memoria ad alta velocità, per la memorizzazione temporanea dei dati e delle
informazioni di controllo. Dato che sono interni alla CPU possono essere letti e scritti a velocità elevate.

I registri sono divisi in due categorie:

1. Ad uso generale: possono essere utilizzati per memorizzare temporaneamente dati o informazioni di
controllo di vario genere.
2. Uso specifico: sono dedicati sempre ed esclusivamente ad uno scopo specifico. Il registro più
importante è il Program Counter (PC) che punta alla successiva istruzione che dovrà essere prelevata
per l’esecuzione. Un altro registro è l’instruction Register (IR), contiene l’indirizzo dell’istruzione in fase
di esecuzione.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 15 a 103

2.2.1 Data Path


La seguente figura mostra com’è organizzata internamente
una parte di una tipica CPU di Von-Neumann: essa è
composta da:

▪ Registri: memoria interna.


▪ ALU: unità aritmetica logica.
▪ Bus: connette le diverse componenti.

I registri alimentano due registri di input della ALU


e mantengono i dati d’ingresso dell’ALU mentre questa
è occupata nell’esecuzione di eventuali operazioni. L’ALU
esegue alcune semplici operazioni su input, come addizioni
e sottrazioni, e genera un risultato che viene memorizzato in
un suo apposito registro di output.

Questo valore può essere successivamente memorizzato in uno dei registri della CPU e copiato in memoria
in un secondo momento. Il processo che consiste nel portare i due operandi in ALU e nel memorizzare il
risultato è chiamato ciclo del percorso dati e rappresenta il cuore della maggior parte delle CPU. Più veloce
è il ciclo del percorso dati e maggiore sarà la velocità del calcolatore.

2.2.2 Esecuzione dell’istruzione


La CPU esegue ogni istruzione compiendo una serie di piccoli passi che, in linea generale, possono essere
descritti nel seguente modo:

1. (fetch) Prelevare la successiva istruzione dalla memoria per portarla nell’IR* (registro).
2. (fetch) Modificare il valore del PC* (registro) per farlo puntare alla successiva istruzione.
3. (decode) Determinare il tipo di istruzione (OPCODE) appena prelevata.
4. (decode) Se l’istruzione prevede degli operandi (valore dati) viene determinata la posizione in memoria.
5. (decode) Gli operandi vengono prelevati dalla memoria e caricati in un registro della CPU.
6. (execute) Esecuzione istruzione e generazione valore di output.
7. (execute) Tornare al punto 1 per iniziare l’esecuzione dell’istruzione successiva.

(*) II registro istruzione (IR) è un registro della CPU che memorizza l’istruzione in fase di elaborazione.
Ogni istruzione viene caricata dentro l’IR mentre viene decodificata ed eseguita.

(*) Il Program counter (PC) è un registro della CPU la cui funzione è quella di memorizzare l’indirizzo
di memoria della prossima istruzione da eseguire.

Punto 4-5 Fetch degli operandi: se c’è qualche elemento dell’istruzione che non è presente nei registri, ma in
memoria centrale, devo determinare dove si trova, prelevarlo e spostarlo in un registro della CPU.

L’esecuzione è gestita da quattro componenti:

1. Le operazioni per l'elaborazione dei dati primitivi sono le usuali operazioni aritmetico-logiche realizzate
dalla ALU: Operazioni su interi, booleani, shift, confronti ecc.
2. Riguardo al controllo della sequenza di esecuzione delle operazioni, la struttura principale è costituita
dal Program Counter che contiene l'indirizzo della prossima istruzione da eseguire.
3. Per controllare i trasferimento dati, le strutture sono costituite dai registri della CPU che interfacciano
la memoria principale.
4. La gestione della memoria è affidata al sistema operativo. L'esecuzione di un programma può venir
sospesa per concedere la CPU ad altri programmi, per ottimizzare la gestione delle risorse ecc.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 16 a 103

2.2.3 Tassonomia di Flynn


La tassonomia di Flynn è un sistema di classificazione delle architetture dei calcolatori. I calcolatori vengono
classificati a seconda della molteplicità del flusso di istruzioni e del flusso dei dati che possono gestire.
▪ SISD: Calcolatori sequenziali, cioè istruzioni eseguite una per volta su dati a singolo flusso
▪ SIMD: Istruzione singola che viene eseguita su più flussi di dati (dati diversi). Esempio: Bisogna
elaborare un’immagine e cercare un volto di una ragazza. L'immagine viene suddivisa in quadranti e
ogni quadrante viene assegnato ad un processore diverso, che esegue lo stesso algoritmo di ricerca
degli altri (istruzioni).
▪ MISD: Su un singolo flusso di dati, più processori eseguono istruzioni differenti.
▪ MIMD: Più processori eseguono istruzioni differenti su dati differenti.

I progettisti di calcolatori si sforzano costantemente di migliorare le prestazioni delle loro macchine.


Aumentare la velocità di clock per rendere i processori più veloci è una possibilità, ma per qualsiasi nuova
architettura esiste un limite. Per questo motivo si utilizza il parallelismo, cioè compiere più azioni allo
stesso tempo al fine di ottenere prestazioni più elevate. Il parallelismo può essere presente in due forme:

1. A livello d’istruzione: l’obbiettivo è eseguire un maggior numero di istruzioni al secondo.


2. A livello di processore: Più processori che lavorano congiuntamente su uno stesso problema.

2.2.4 Parallelismo a livello d’istruzione


È assodato che uno dei maggiori colli di bottiglia nella velocità di esecuzione delle istruzioni è rappresentato
dal prelievo delle istruzioni dalla memoria centrale. Per risolvere questo problema, i primi calcolatori erano
dotati della capacità di poter prelevare in anticipo le istruzioni dalla memoria, in modo da averle già a
disposizione nel momento in cui dovessero rendersi necessarie. Le istruzioni venivano memorizzate in un
insieme di registri chiamati buffer di prefetch. In pratica la tecnica di prefetching divideva l’esecuzione in
due stadi: il prelievo dell’istruzione e la sua esecuzione effettiva.

Con la pipeline, invece di dividere l’esecuzione di un’istruzione solamente in due fasi, la si divide in un
numero maggiore in modo che possono essere eseguite in parallelo; ciascun di queste parti è gestita da
componenti hardware dedicate (core del processore). Con la pipeline non si riduce il tempo di esecuzione
della singola istruzione, bensì si aumenta il Throughtput, cioè il numero di istruzioni eseguite nell’unità di
tempo. L’incremento potenziale di velocità è direttamente proporzionale alla quantità di stadi progettati
per quel processore.

Un’operazione viene suddivise in diversi stadi e i dati vengono fatti scorrere in modo sequenziale lungo la
pipeline ad ogni ciclo di clock. La durata del ciclo di clock della pipeline è determinata dalla durata dello
stadio più lento. Quindi se una determinata istruzione, all’interno di un particolare stadio, impiega molto
più tempo delle altre, quest’ultime per proseguire (e quindi scorrere nei successivi stadi della pipeline)
devono attendere il completamento dell’istruzione ancora in esecuzione.

Ipotizziamo un’architettura con singolo processore e diverse control unit, ad ognuna delle quali viene
assegnato uno stadio specifico del data path, come nel seguente esempio:

▪ S1: Fetch dell’istruzione, consiste nel determinare l’istruzione successiva e caricarla in un buffer.
▪ S2: Decodifica dell’istruzione, consiste nel determinare il codice operativo e le specifiche degli operandi
▪ S3: Calcolo degli operandi, consiste nel determinare l’indirizzo degli operandi (indirizzamento).
▪ S4: Fetch degli operandi, estrazioni degli operandi dalla memoria.
▪ S5: Esecuzione dell’istruzione, comprende l’esecuzione delle operazioni richieste dall’istruzione e la
memorizzazione degli eventuali risultati.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 17 a 103

A questo punto si cerca di replicare il meccanismo della catena di montaggio con il processore. Cioè ad ogni
stadio la CPU provvede a svolgere in maniera sequenziale un solo compito specifico per l’elaborazione di
una certa istruzione. Il lavoro della pipeline è il seguente:

Clock 1 Clock 2 Clock 3 Clock 4 Clock 5 Clock 6 Clock 7 Clock 8 Clock 9


S1 A B C D E
S2 A B C D E
S3 A B C D E
S4 A B C D E
S5 A B C D E

𝑇𝐶𝑂𝑁𝑉𝐸𝑁𝑍𝐼𝑂𝑁𝐴𝐿𝐸 𝑁∙(𝐾𝑇)
Velocità esecuzione: 𝑇𝑃𝐼𝑃𝐸
= [𝐾+(𝑛−1)]∙𝑇 dove:

▪ K: numero di stadi della pipeline.


▪ N: numero di istruzioni da eseguire.
▪ T: tempo di uno stadio (massimo).

Svantaggi nell’utilizzo della pipeline:

1. Criticità dovuta all’esecuzione parallela: Per esempio: supponiamo che una CPU con pipeline debba
eseguire il seguente frammento di codice: 𝐶 = 𝐴 + 𝐵; 𝐷 = 𝐶 + 1; La prima istruzione deve prelevare
il valore delle variabili A e B, sommarli e porli nella variabile C. La seconda istruzione deve prelevare il
valore contenuto nella variabile C, aggiungere 1 e salvare il risultato in D. Ma la seconda istruzione non
potrà essere elaborata fino a quando il dato della prima operazione non sarà disponibile in memoria, e
quindi la seconda operazione dovrà bloccarsi per attendere il completamento della prima, riducendo il
Throughtput (capacità) della pipeline.

2. Dipendenza dai controlli: Tutte le istruzioni che modificano il normale incremento del PC (salti
condizionati, chiamate e ritorno di procedure, interruzioni, ...) invalidano la pipeline. Un’istruzione di
salto condizionato rende impredicibile l’indirizzo dell’istruzione della quale eseguire il successivo fetch.
In tale situazione, la fase successiva di fetch non può procedere fino a che non riceve l’indirizzo
dell’istruzione successiva dalla fase precedente in esecuzione.

Precedendo come esempio lo schema di pipeline precedente, se A è un’istruzione condizionale,


l’indirizzo dell’istruzione in cui deve continuare il flusso d’esecuzione verrà calcolato solo in fase di
esecuzione, quindi al clock 5. Nel frattempo B,C,D effettueranno il fetch di istruzioni che
potenzialmente non serviranno.

3. Non tutte le istruzioni richiedono lo stesso tempo di


esecuzione; È possibile che si verifichi una situazione in cui
uno stadio della pipeline impieghi molto più tempo degli altri
a terminare l’esecuzione. In questo caso, tutti gli altri stadi
entreranno in uno stato di attesa e la pipeline non potrà
continuare il normale flusso operativo fino a quando tutti gli
stadi non termineranno.

4. Se uno stadio fallisce la pipeline si blocca. Come nel caso 3, lo stadio fallito prolunga l’attesa degli altri,
in questo modo nessun’altro stadio può continuare ad operare.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 18 a 103

Possibili soluzioni per risolvere il problema dei salti:

1. Flussi multipli: In presenza di un'istruzione di salto condizionato, una normale pipeline deve scegliere,
tra due possibili istruzioni successive, quella della quale eseguire il fetch, e può scegliere in modo
errato. Un possibile approccio consiste nel consentire alla pipeline di eseguire il fetch di entrambe le
istruzioni, facendo uso di flussi multipli. Uno degli inconvenienti che si possono presentare, tuttavia, ha
luogo quando ulteriori istruzioni di salto condizionato entrano nella pipeline attraverso i vari flussi
prima che il salto originale sia stato risolto; tutte queste istruzioni possono allora richiedere dei loro
propri flussi multipli, in quantità totale superiore a quella che può essere supportata dall'hardware.

2. Prelievo anticipato della destinazione: Quando viene riconosciuta un'istruzione di salto condizionato,
viene eseguito il prefetch della destinazione del salto, in aggiunta al fetch dell'istruzione successiva a
quella di salto. L'istruzione all'indirizzo destinazione viene quindi salvata fino a quando l'istruzione di
salto non viene completata: se il salto viene eseguito, il fetch dell'istruzione all'indirizzo destinazione è
già stato effettuato.

3. Predizione di salto: Si possono usare varie tecniche per predire se un salto sarà effettuato, basate
sull'analisi storica delle precedenti esecuzioni del programma, oppure su una qualche misura dinamica
della frequenza dei salti precedenti.

4. Salto ritardato: È possibile migliorare le prestazioni della pipeline riorganizzando opportunamente le


istruzioni all'interno del programma in modo che un'istruzione di salto avvenga il più tardi possibile.

Architetture superscalari

Se è bene avere una pipeline, averne due è sicuramente meglio. In questa situazione una singola unità di
fetch preleva due istruzioni alla volta e le inserisce nelle pipeline, ognuna delle quali è dotata di una ALU.
Affinché le due istruzioni possano essere eseguite in parallelo, non devono esserci conflitti nell’uso delle
risorse e nessuna delle due istruzioni deve dipendere dal risultato dell’altra.

2.2.5 Parallelismo a livello di processore


Il parallelismo a livello d’istruzione aiuta in parte ad aumentare la velocità di un elaboratore, ma
difficilmente l’uso della pipeline e le operazioni superscalari possono aumentare le prestazioni di un fattore
cinque o dieci. Per ottenere guadagni di 50, 100 e più, l’unica soluzione è quella di progettare calcolatori
con più CPU. Esistono varie implementazioni:

Array di processori: consiste di un gran numero di


processori identici che eseguono la stessa sequenza
d’istruzioni su insiemi diversi di dati. Una delle
prime macchine era costituita da una griglia
quadrata di processori e memorie private.
Una singola unità di controllo trasmetteva in
broadcast le istruzioni, che venivano eseguite a
passi sincronizzati da tutti i processori della griglia,
ciascuno dei quali utilizzava i dati letti dalla propria
memoria privata. Talvolta si utilizza il termina SIMD,
cioè istruzioni singole dati multipli per indicare
questo tipo di architetture.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 19 a 103

Multiprocessori: In un array di processori le unità di elaborazione non sono delle CPU indipendenti, dato
che c’è un’unica unità di controllo condivisa fra tutte. Un multiprocessore è composto da più CPU con una
memoria comune. Dato che ogni CPU può leggere e scrivere una qualsiasi parte della memoria condivisa,
esse devono coordinarsi via software per evitare di ostacolarsi a vicenda. Sono possibili vari schemi di
implementazione:

▪ Il più semplice dei quali consiste nell’avere un singolo bus con più CPU,
tutte connesse a un’unica memoria condivisa. Ma potrebbero verificarsi
dei conflitti se un gran numero di processi veloci tenta costantemente di
accedere alla memoria attraverso lo stesso bus.
▪ Un altro modo è quello di utilizzare un’architettura in cui ogni processore
possiede una propria memoria locale, non accessibile agli altri. Tutti i
processori sono collegati ad una memoria condivisa utilizzata per scambiarsi
informazioni. La memoria privata di ogni CPU è utilizzabile per contenere il
codice del programma e quei dati che non devono essere condivisi. L’accesso
a questa memoria non utilizza il bus principale, riducendo in modo
significativo il traffico sul bus.

Multicomputer: Se da un lato è relativamente semplice costruire multiprocessori composti da un modesto


numero di CPU, è invece decisamente più complicato realizzarne di più grandi. La difficoltà risiede nel
connettere tutti i processori alla memoria centrale condivisa. Per aggirare questi problemi si è pensato di
costruire sistemi composti da un gran numero di calcolatori interconnessi, ciascuno dotato di una memoria
privata.

Le CPU dei Multicomputer comunicano fra loro inviandosi messaggi molto


veloci. Nel caso di grandi sistemi, dato che non è efficiente connettere
mutualmente (vedi capitolo mutua esclusione) tutti i calcolatori, si utilizzano
topologie ad anelli (come nell’immagine seguente). Ne consegue che i
messaggi tra due calcolatori per spostarsi dalla sorgente alla destinazione
devono passare attraverso uno o più macchine intermedie.

2.3 Memoria
La memoria è un componente del computer capace di memorizzare dati e istruzioni. Una memoria può
essere considerata esattamente come una sequenza finita di celle in cui ogni cella contiene una sequenza
finita di bit. Ogni posizione in memoria è individuata da un preciso indirizzo.

La soluzione che viene tradizionalmente adottata per memorizzare una grande mole di dati consiste
nell’organizzare gerarchicamente la memoria, come
in figura.

Nella parte alta della gerarchia si trovano i registri


della CPU, che sono molto veloci, ma di dimensione
molto piccola. Successivamente vi è la memoria
cache con dimensioni di qualche megabyte. Di
seguito abbiamo la memoria centrale, dove risiedono
i dati e i programmi, con dimensioni di qualche
gigabyte. Infine ci sono i dischi magnetici per
l’archiviazione di massa.

Muovendosi verso il basso della gerarchia


aumentano tre parametri chiave:

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 20 a 103

1. Il tempo di accesso diventa via via più grande: Dai registri alla memoria centrale la velocità di accesso è
molto veloce, si parla qualche decina di nanosecondi. Un grande salto si verifica con le memorie di
memorizzazione di massa in cui il tempo di accesso ai dischi è di qualche decina di millisecondi.
2. Capacità di memorizzazione aumenta: I registri della CPU vanno bene per immagazzinare qualche
centinaia di byte, invece, i dischi magnetici possono arrivare a memorizzare anche centinaia di gigabyte.
3. Diminuiscono i costi: Sebbene i prezzi attuali cambiano continuamente, i costi della memoria centrale
sono più alti rispetto a quelli della memoria secondaria.

2.3.1 RAM
La memoria principale (RAM) è quella parte del calcolatore in cui sono depositati programmi e dati in modo
volatile. L’unità della memoria è il bit, che può assumere 1 e 0, ed è l’unità più semplice possibile.

Le memorie sono costituite da un certo numero di celle (o locazioni) ciascuna delle quali può memorizzare
informazioni. Ciascuna cella ha un numero, chiamato indirizzo, attraverso il quale un qualsiasi programma
può riferirsi ad essa. Se una memoria ha 𝑛 celle, i suoi indirizzi variano da 0 a 𝑛 − 1. Tutte le celle hanno la
stessa dimensione, ovvero contengono lo stesso numero di bit. Attualmente si usa considerare
raggruppamenti di bit in byte (8 bit) e a loro volta i byte possono essere raggruppati in parole (word).

2.3.2 ROM
Con Read Only Memory (ROM) si indica un tipo di memoria non volatile in cui i dati sono memorizzati
tramite collegamenti elettronici fisici. È una memoria scritta in fase di fabbricazione e non modificabile.
Questa memoria contiene delle informazioni importanti per il funzionamento del PC, per esempio il BIOS
(programma che gestisce il boot del pc).

2.3.3 Memoria cache


La memoria cache è una memoria veloce, relativamente piccola, che memorizza i dati più recentemente
usati dalla memoria principale. La funzione della memoria cache è di velocizzare gli accessi alla memoria
principale aumentando le prestazioni del sistema. Il buon funzionamento della cache deriva dalla capacità
del sistema nel mantenere in essa le informazioni che saranno necessarie.

Il processore, se necessita di un dato, cerca inizialmente in cache, se non trova nulla va in memoria.
La velocità di esecuzione e lettura di un dato è direttamente proporzionale alla possibilità che questo dato
risieda in memoria cache. Bisogna, quindi, possedere una strategia o un algoritmo che preveda cosa può
servire al calcolatore in futuro (in base all’uso attuale) così da ottimizzare e velocizzare il sistema.

La CPU può utilizzare due criteri per conservare o meno le informazioni dentro la cache, ovvero il criterio
spazio-temporale:

▪ Principio di località spaziale: Se in un certo istante viene referenziato un indirizzo di memoria è


altamente probabile che in istanti immediatamente successivi possano venire referenziati indirizzi
vicini.
▪ Principio di località temporale: Se in un certo istante viene referenziato un indirizzo di memoria è
altamente probabile che in istanti immediatamente successivi lo stesso indirizzo possa essere
nuovamente referenziato.

La cache non agisce sul singolo indirizzo ma su blocchi di dati. Quando viene referenziato un dato all’interno
di un blocco, l’algoritmo spazio-temporale sposta l’intero blocco in cache. Esistono vari tipi di cache:

▪ Cache unificata: una sola memoria cache che contiene sia dati che istruzioni.
▪ Cache specializzata: due memorie cache dedicate singolarmente a dati e istruzioni.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 21 a 103

Le memoria cache è divisa in tre livelli:

▪ Livello 1: cache più veloce che contiene le istruzione che devono essere eseguite successivamente dalla
CPU, solitamente in questo livello vengono memorizzati i dati più usati.
▪ Livello 2: al suo interno vengono salvati i dati che potrebbero servire alla CPU in un secondo momento.
▪ Livello 3: cache più lenta dove vengono salvati i dati meno importanti.

Prestazioni: Se durante un piccolo intervallo di tempo una parola è letta o scritta k volte, il calcolatore
dovrà effettuare (se il dato non è presente già in cache) un accesso non riuscito in cache, + 1 riferimento
alla memoria centrale + 𝑘 − 1 riferimenti alla cache (perché il dato verrà caricato, secondo i principi di
località). Sia C il tempo di accesso alla cache ed M il tempo di accesso alla memoria, si ha che
𝑡𝑐𝑎𝑐ℎ𝑒 (𝐶+𝑀)+(𝐾−1)𝐶 𝐶+𝑀 (𝐾−1) 𝐶
𝑡𝑚𝑒𝑚𝑜𝑟𝑦
= 𝐾𝑀
= 𝐾𝑀
+ 𝐾
∙𝑀

Uno dei parametri fondamentali per avere benefici con la cache è il rapporto tra C e M, ovvero quante volte
il tempo di cache è più piccolo del tempo per accedere in memoria centrale.

Hit Ratio: Un hit ratio cache descrive la situazione in cui il contenuto viene servito correttamente dalla
cache e non dalla memoria centrale. Sia H la frequenza di successi nell’accesso alla cache: Esempio 90% di
accessi con successo: H=0.9. In caso contrario si definisce miss ratio (1 − ℎ) = 1 − 0.9 = 0.1

Si ottiene che il tempo medio di accesso è pari:

𝐻𝐶 + (1 − 𝐻)(𝐶 + 𝑀) = 𝐻𝐶 + 𝐶 − 𝐻𝐶 + (1 − 𝐻)𝑀 = 𝐶 + (1 − 𝐻)𝑀


Da questa formula notiamo che se la percentuale di successo è del 100%, il tempo medio di accesso è C.
Perché di fatto ogni volta si accederà solo alla cache.

2.3.4 Dischi magnetici


Il disco magnetico è un dispositivo non volatile di memorizzazione di massa. Consiste in uno o più piani di
alluminio, rivestiti di materiale magnetico. Ogni superficie ha una testina dedicata che ha il compito di
leggere e scrivere dati sul disco. Un disco è organizzato logicamente in elementi:

▪ Traccia: sequenza circolare di bit letti/scritti mentre il disco compie una rotazione completa.
▪ Settore: Ogni traccia è suddivisa in settori.

Prestazioni: La lettura o la scrittura di un dato in memoria è influenzata da:

▪ Tempo di seek: ricerca della traccia di interessa. Cioè il tempo di movimento radiale della testina di
lettura e scrittura.
▪ Tempo di rotate: tempo necessario affinché il settore a cui noi siamo interessati si posizioni sotto la
testina di lettura e scrittura.
▪ Tempo di trasferimento: Essendo il settore passato sotto la testina, le informazioni sono state acquisite
e spostate in buffer o dal buffer memorizzato sul settore. Quindi è il tempo di trasferimento dal disco al
buffer o viceversa.

2.3.5 RAID
I RAID, insieme ridondante di dischi indipendenti, è una tecnica d’installazione che consente di collegare
diversi dischi magnetici al computer in modo tale che il sistema operativo riesca a identificarli come se
fosse un unico volume di memorizzazione. I RAID utilizzano, con modalità diverse a seconda del tipi di
realizzazione, principi di ridondanza dei dati e di parallelismo per garantire, rispetto a un solo disco singoli:

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 22 a 103

▪ Aumento di prestazioni
▪ Aumento di capacità di memorizzazione disponibile
▪ Aumento tolleranza ai guasti
▪ Migliore affidabilità

Esistono almeno sei livelli di RAID:

RAIR 0: Il livello 0 di RAID non è un vero componente della famiglia RAID, in


quanto non comprende la ridondanza per il miglioramento delle prestazioni.
Comunque vi sono applicazioni in cui prestazioni e capacità sono le principali
preoccupazioni e il basso costo è più importante di un aumento di affidabilità.

In RAID 0, i dati utente e i dati di sistema sono distribuiti su tutti i dischi dell’array.
Questo ha un vantaggio notevole rispetto all’uso di un unico disco di maggiori
dimensioni: se due diverse richieste di i/o sono in attesa di due diversi blocchi di
dati, c’è una buona probabilità che i due blocchi richiesti si trovino su dischi
diversi. Pertanto le due richieste possono essere emesse in parallelo.

Ma RAID 0, così come tutti i livelli di RAID, va al di là della distribuzione di dati in


un array di dischi: i dati sono sistemati a strisce su dischi disponibili, ossia si effettua quello che è detto lo
striping di dati. Tutti i dati utente e di sistema sono visti come fossero memorizzati in un disco logico, diviso
in fette (strip), che possono essere blocchi fisici o settori.

Le fette sono mappate su elementi consecutivi dell’array. In un array di 𝑛 dischi, le prime 𝑛 fette logiche
sono memorizzate fisicamente sulla prima fetta di ciascun disco, le secondo 𝑛 fette sono distribuite sulle
seconde 𝑛 fette di ogni disco, e così via. Il vantaggio di questo schema è che se una richiesta di I/O si
riferisce a più fette logicamente contigue, allora si possono elaborare in parallelo fino a 𝑛 fette, riducendo
moltissimo il tempo di trasferimento. Il requisito di tale vantaggio è che l’applicazione deve effettuare
richieste di i/o che sfruttino in modo efficiente l’array dei dischi.

▪ PRO: Lettura e scrittura potenzialmente parallela, grazie allo striping.


▪ CONTRO: Nessuna ridondanza.

RAID 1: RAID 1 si differisce da RAID ai livelli da 2 a 5 per il modo in cui la ridondanza


viene raggiunta. Negli schemi RAID superiori, si usa il calcolo della parità per
realizzare la ridondanza, mentre in RAID 1 la ridondanza si ottiene semplicemente
duplicando tutti i dati.

Si utilizza sempre la tecnica dello striping di dati, ma, in questo caso ogni fetta logica
viene mappata in due dischi fisici separati al fine di ottenere dischi gemelli. Vi sono
molti PRO nell’organizzazione raid 1:

▪ Una richiesta di lettura può essere servita da uno qualunque dei due dischi che contengono i dati
richiesti o addirittura in maniera parallela.
▪ Una richiesta di scrittura richiede che entrambe le fette corrispondenti siano aggiornate, ma questo
può essere fatto in parallelo; pertanto, le prestazioni dell’operazione di scrittura sono legate a quella
più lenta delle due scritture.
▪ Il recupero di un guasto è semplice: quando un disco fallisce, i dati possono essere recuperati dal
secondo disco.

Il principale CONTRO di RAID 1 è il costo; richiede uno spazio fisico pari al doppio dello spazio logico.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 23 a 103

RAID 2: I livelli 2 e 3 di RAID fanno uso di una


tecnica di accesso parallelo. In un array ad accesso
parallelo, tutti i dischi partecipano a ogni richiesta
di I/O; le testine di ogni disco sono sincronizzate in
modo da occupare la stessa posizione in ogni
istante di tempo.

Come in altri schemi RAID, si usa lo striping dei dati


e, nel caso di RAID 2 e 3, le fette sono molto
piccole, spesso di un byte o una parola. In RAID 2, il
codice di correzione di errore (codice di Hamming) viene calcolato sui bit corrispondenti su ogni disco, e i
bit del codice sono memorizzati nelle corrispondenti posizioni in dischi di parità multipli.

I dati richiesti e il codice di correzione errori associati sono inviati al controller dell’array; se vi è un singolo
bit errato, il controller può riconoscerlo e correggerlo istantaneamente, senza rallentare gli accessi in
lettura. Per una singola scrittura, si accede a tutti i dischi di dati e ai dischi di parità. Data l’alta affidabilità
dei singoli dischi, il RAID 2 è eccessivo e non viene implementato.

▪ PRO: Individua e corregge errori su singoli bit.


▪ CONTRO: Rotazione sincronizzata dei dischi.

RAID 3: organizzato in modo simile a RAID 2, con la


differenza che RAID 3 richiede un solo disco
ridondante, indipendentemente dalla dimensione
dell’array di dischi. RAID 3 utilizza l’accesso parallelo
e i dati sono distribuiti in fette piccole. Invece di un
codice di correzione di errore, si calcola un semplice
bit di parità per l’insieme di bit che si trovano in
posizioni corrispondenti lungo i dischi dati.

In caso di guasto di un disco, si accede al drive di parità e i dati vengono ricostruiti a partire dai dispositivi
rimanenti; Siccome i dati sono divisi in fette molto piccole, RAID 3 può raggiungere altissimi tassi di
trasferimento dati; ogni richiesta di I/O conterrà il trasferimento in parallelo di dati da tutti i dischi
dell’array.

▪ PRO: Lettura/scrittura in parallelo.


▪ CONTRO: Rotazione sincronizzata dei dischi.

RAID 4: I livelli 4 e 5 di RAID utilizzano una tecnica di accesso


indipendente; in un array ad accesso indipendente, ogni disco opera in
maniera indipendente, cosicché richieste di i/o separate possono
essere espletate in parallelo.

Come negli altri schemi RAID, viene utilizzato lo striping di dati e nel
caso di RAID 4 e 5, le fatte sono relativamente grandi. Con RAID 4,
viene calcolata una parità strip-per-strip e memorizzata in un disco
aggiuntivo.

▪ PRO: Affidabile e non richiede la sincronizzazione dei dischi.


▪ CONTRO: Prestazioni scarse quando si aggiornano piccole quantità di dati. Infatti, se si modifica un
settore è necessario leggere tutti i dischi, in quanto occorre ricalcolare e riscrivere la parità.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 24 a 103

RAID 5: è organizzato in modo simile a RAID 4. La


differenza è che RAID 5 distribuisce le fette di parità
su tutti i dischi.

I blocchi di dati vengono sottoposti a striping tra le


unità e su un’unità viene scritta la parità. I dati di
parità non vengono scritti su un’unità fissa, ma sono
distribuiti.

▪ PRO: La parità è distribuita per una maggiore


affidabilità.
▪ CONTRO: Tecnologia complessa.

In breve:

▪ RAID 0: striping su più dischi.


▪ RAID 1: striping su più dischi e mirroring.
▪ RAID 2: striping a livello di bit (4) con 3 bit di parità.
▪ RAID 3: striping a livello di bit con un solo bit di parità.
▪ RADI 4: striping e parità calcolata per ogni strip su un disco extra.
▪ RAID 5: striping e parità calcolata per ogni strip su un disco casuale.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 25 a 103

2.5 Codice di Hamming


Un aspetto importante nel trattamento dell’informazione è quello del rilevamento e della gestione degli
errori che alterano il codice di nostro interesse. Per esempio, può capitare che si verifichino degli errori
all’interno delle memorie RAM dei computer a causa di picchi di tensione. Per risolvere il problema è
necessario includere nei dati sufficiente informazione ridondante da permettere l’individuazione e la
correzione dell’errore. Questa strategia utilizza i codici di correzione degli errori

Definizioni:

- Distanza di Hamming: Numero di bit in cui due stringhe binarie differiscono.


Per esempio: D(10001100 e 00101110) = 3
- Distanza minima: Considerando un intero codice (formato da n stringhe), il valore minimo di distanza di
Hamming tra due parole qualsiasi diventa la distanza minima del codice. Esempio:
• 0000000000
• 1111100000
• 0000011111
• 0000000001

La distanza minima è 1

La distanza di Hamming tra le parole gioca un ruolo fondamentale nel meccanismo di rilevazioni di errori.
Per rendere un codice affidabile ci sarà bisogno di includere informazioni ridondante; più ridondante è più
saremo capaci di rilevare errori su più bit.

Per esempio, analizziamo la seguente codifica: on=1 e off=0. Questo è un codice non ridondante, quindi, se
durante la trasmissione del messaggio un bit viene alterato non sarà possibile rilevare l’errore.

Bisogna aumentare la distanza di Hamming, quindi ci sarà bisogno di includere informazione ridondante
come nel seguente esempio: on=11 e off=00. Se un bit viene alterato, si passa sempre in una situazione non
valida, quindi, è sempre possibile individuare la presenza di un solo errore (con d.Hamming=2).

Analizziamo ora la seguente codifica: on=111 e off=000. Se in ricezione ottengo 110 sarà possibile
individuare l’errore ma anche correggerlo: Infatti in questo modo con un solo cambiamento di bit la parola
di codice ricevuta continua ad essere “più vicina” al codice on rispetto a quello off.

▪ RILEVARE: Un codice con distanza di Hamming pari a d permette di rilevare 𝑑 − 1 errori.


𝑑−1
▪ CORREGGERE: Un codice con distanza di Hamming pari a d ci permette di correggere 2
errori.

Nei codici di rilevazione e/o correzione di errori si utilizzano alcuni bit ridondanti che vengono aggiunti alla
parola stessa. Questi bit ridondanti si chiamano bit di controllo. L’idea è quella di creare codici con distanza
di Hamming maggiore al fine di poter rilevare e correggere errori. Da notare che l’errore potrebbe capitare
anche nei bit di controllo aggiunti per cui si dovrà tenere conto di questa possibilità trattandoli come bit di
dati alla pari degli altri.

Indichiamo con il termine Codework la parola codice completa dei bit di controllo. I bit di controllo sono
calcolati secondo la seguente disequazione: 𝑏𝑖𝑡𝑑𝑎𝑡𝑖 + 𝑏𝑖𝑡𝑐𝑜𝑛𝑡𝑟𝑜𝑙𝑙𝑜 + 1 ≤ 2𝑏𝑖𝑡_𝑐𝑜𝑛𝑡𝑟𝑜𝑙𝑙𝑜

Un esempio di bit di controllo è il bit di parità. Tale sistema prevede l’aggiunta di un bit ridondante, alla
fine della parola, calcolato in modo tale che il numero di bit 1 totali sia sempre pari. Per esempio:

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 26 a 103

Parola: 01001 (il numero di bit 1 è già pari, quindi aggiungo 0) -> 010010

Parola: 01000 (il numero di bit 1 è dispari, quindi aggiungo 1) -> 010001

Se un numero dispari di bit è cambiato durante la trasmissione di una parola, allora il bit di parità non
risulterà in accordo con il numero di bit 1 e di conseguenza indicherà che è avvenuto un errore durante la
trasmissione. Quindi il bit di parità è un codice di rilevazione, ma non di correzione d’errore: non c’è modo
di determinare quale particolare bit è stato alterato. Esempio di trasmissione:

▪ A vuole trasmettere: 1001


▪ A calcola il bit di parità: 1^0^0^1 = 0
▪ A aggiunge il bit di parità e invia 10010
***si verifica un errore durante la trasmissione***
▪ B riceve 11010
▪ B calcola la parità 1^1^0^1^0 = 1
▪ B può affermare che si è verificato un errore durante la trasmissione

Il bit di parità garantisce di rilevare solo un numero dispari di errori. Se avviene un numero pari di errori lo
schema non funziona. Per esempio:

▪ A vuole trasmettere 1001


▪ A calcola il bit di parità 1^0^0^1=0
▪ A aggiunge il bit di parità e invia 10010
***si verifica un errore durante la trasmissione***
▪ B riceve 11011
▪ B calcola la parità 1^1^0^1^1 = 0
▪ B può afferma che la trasmissione è avvenuta correttamente

B osserva una parità pari, come aspettato, e quindi fallisce nel cercare due errori.

Un altro esempio di bit di controllo è il codice di Hamming. Innanzitutto vengono numerati i bit della
parola da trasmettere a partire da sinistra verso destra, inserendo a ogni posizione avente per indice una
potenza del 2 il bit di controllo. In una Codework di 12 bit (8 bit dati + 4 bit CTR controllo) i bit di controllo
saranno posizionati in 20 = 1, 21 = 2, 22 = 4, 23 = 8 nel seguente modo:

1 2 3 4 5 6 7 8 9 10 11 12
Codework CTR1 CTR2 Bit 1 CTR3 Bit 2 Bit 3 Bit 4 CTR4 Bit 5 Bit 6 Bit 7 Bit 8

Ogni bit di controllo è la parità degli altri bit che hanno, nella loro rappresentazione binaria come posizione,
un 1 alla posizione corrispondente a quella del bit di controllo nella Codework.

Il bit 1 della Codework (CTR1) è il primo bit di controllo (bisogna vedere la prima riga della
rappresentazione binaria) e andrà a controllare i bit in posizione 1,3,5,7,9,11.

Il bit 2 della Codework (CTR1) è il secondo bit di controllo (bisogna vedere la seconda riga
della rappresentazione binaria) e andrà a controllare i bit in posizione 2,3,6,7,10,11.

Il bit 4 della Codework (CTR3) è il terzo bit di controllo (bisogna vedere la terza riga della
rappresentazione binaria) e andrà a controllare i bit in posizione 4,5,6,7,12.

Il bit 8 della Codework (CTR4) è il quarto bit di controllo (bisogna vedere la quarta riga della
rappresentazione binaria) e andrà a controllare i bit in posizione 8,9,10,11,12.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 27 a 103

FASE 1: Disporre i bit dei dati nei relativi spazi vuoti della Codework. Sia 00101101 il messaggio da
codificare:

0 0 1 0 1 1 0 1

CTR1 CTR2 0 CTR3 0 1 0 CTR4 1 1 0 1


Pos1 Pos2 Pos3 Pos4 Pos5 Pos6 Pos7 Pos8 Pos9 Pos10 Pos11 Pos12

FASE 2: Calcolare le parità con i bit di controllo

Il primo bit di controllo deve controllare la parità delle posizioni 1,3,5,7,9,11 ; di conseguenza:

▪ In posizione 1 il bit non è ancora stato calcolato.


▪ In posizione 3 il bit è 0.
▪ In posizione 5 il bit è 0.
▪ In posizione 7 il bit è 0.
▪ In posizione 9 il bit è 1.
▪ In posizione 11 il bit è 0.

Il numero di 1 è dispari, quindi il bit di parità da inserire in posizione 1 è 1 (così facendo i bit 1 saranno pari).

Il secondo bit di controllo deve controllare la parità delle posizioni 2,3,6,7,10,11 ; di conseguenza:

▪ In posizione 2 il bit non è ancora stato calcolato.


▪ In posizione 3 il bit è 0.
▪ In posizione 6 il bit è 1.
▪ In posizione 7 il bit è 0.
▪ In posizione 10 il bit è 1.
▪ In posizione 11 il bit è 0.

Il numero di 1 è pari, quindi il bit di parità da inserire in posizione 2 è 0. Si replica il passaggio per ogni bit di
controllo. La Codework ottenuta è la seguente:

1 0 0 0 0 1 0 1 1 1 0 1
Pos1 Pos2 Pos3 Pos4 Pos5 Pos6 Pos7 Pos8 Pos9 Pos10 Pos11 Pos12

RILEVAZIONE DELL’ERORE: A ha inviato la Codework, precedentemente calcolata, e B ottiene:

1 0 0 0 0 0 0 1 1 1 0 1
Pos1 Pos2 Pos3 Pos4 Pos5 Pos6 Pos7 Pos8 Pos9 Pos10 Pos11 Pos12

Immaginiamo che l’errore avvenga sul sesto bit. Visto che tutti i bit sono coinvolti in conteggi di parità,
l’errore di trasmissione farà alterare almeno una delle parità calcolate per i 4 bit di controllo. In effetti:

▪ Calcolo parità di CTR1 in pos1: 1-0-0-0-1-0-0 | nessun errore


▪ Calcolo parità di CTR2 in pos 2: 0-0-0-0-1-0 | discordanza verificata
▪ Calcolo parità di CTR3 in pos 4: 0-0-0-0-1 | discordanza verificata
▪ Calcolo parità di CTR4 in pos 8: 1-1-1-0-1 | nessun errore

CORREZIONE ERRORE: Il bit errato sarà quello dato dalla somma, come posizione, dei bit di parità sbagliati:
È sbagliato il bit di parità in posizione 2 e in posizione 4, quindi 2 + 4 = 𝟔. Il sesto bit è errato.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 28 a 103

Esercizi di codifica con codice di Hamming:

Formula per calcolare bit di controllo: 𝑏𝑖𝑡𝑑𝑎𝑡𝑖 + 𝑏𝑖𝑡𝑐𝑜𝑛𝑡𝑟𝑜𝑙𝑙𝑜 + 1 ≤ 2𝑏𝑖𝑡_𝑐𝑜𝑛𝑡𝑟𝑜𝑙𝑙𝑜

Posizioni da verificare per calcolare i bit di controllo:

pos Bit controllati


1 1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31
2 2 3 6 7 10 11 14 15 18 19 22 23 26 27 30 31
4 4 5 6 7 12 13 14 15 20 21 22 23 28 29 30 31
8 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
16 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

▪ 0110 Bit di controllo = 3

Pos1 Pos2 Pos3 Pos4 Pos5 Pos6 Pos7


1 1 0 0 1 1 0

▪ 101100 Bit di controllo = 4

Pos1 Pos2 Pos3 Pos4 Pos5 Pos6 Pos7 Pos8 Pos9 Pos10
0 1 1 0 0 1 1 0 0 0

▪ 10101 Bit di controllo = 4

Pos1 Pos2 Pos3 Pos4 Pos5 Pos6 Pos7 Pos8 Pos9


0 0 1 1 0 1 0 1 1

Esercizi di rilevamento errore con codice di Hamming:

▪ 1001101

Pos1 Pos2 Pos3 Pos4 Pos5 Pos6 Pos7


1 0 0 1 1 0 1

- Calcolo parità di CTR1 in pos 1: 1-0-1-1 | discordanza verificata


- Calcolo parità di CTR2 in pos 2: 0-0-0-1 | discordanza verificata
- Calcolo parità di CTR3 in pos 4: 1-1-0-1 | discordanza verificata

Bit errato = 1+2+4 = bit 7

▪ 0010100

Pos1 Pos2 Pos3 Pos4 Pos5 Pos6 Pos7


0 0 1 0 1 0 0

- Calcolo parità di CTR1 in pos 1: 0-1-1-0 | nessun errore


- Calcolo parità di CTR2 in pos 2: 0-1-0-0 | discordanza verificata
- Calcolo parità di CTR3 in pos 4: 0-1-0-0 | discordanza verificata

Bit errato = 2+4 = bit 6

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 29 a 103

3.0 Livello logico digitale


Un circuito digitale, in elettronica, è un circuito elettronico il cui funzionamento è basato su un numero
finito di livelli di tensione elettrica. Nella maggior parte dei casi sono presenti solamente due livelli di
tensione identificati con l’uno o con lo zero della logica binaria. I due livelli di tensione usati nei circuiti
digitali rappresentano i numeri binari 0 e 1, detti livelli logici.

Generalmente si associa il livello basso/LOW allo 0 e il livello alto/HIGH all’1. In elettronica, il livello LOW è
associato a tensioni comprese tra 0v e 2v, mentre il livello HIGH è associato a tensioni comprese tra 3v e 5v.

3.1 Porte logiche


Le porte logiche sono i circuiti minimi per l’elaborazione di segnali binari. Sono circuiti che operano su più
segnali di ingresso e producono un segnale di uscita. Circuiti, nei quali vengono utilizzate più porte logiche
connesse tra di loro, vengono chiamati reti logiche.

Allo scopo di descrivere i comportamenti dei circuiti logici si può usare l’algebra booleana che specifica
l’operazione di ogni porta. Le variabili di questa algebra sono binarie, possono assumere solo due valori
(0,1) e vengono indicate con le lettere. Le porte logiche fondamentali sono:

La tabella delle verità è l’elencazione di tutte le possibili configurazioni dei valori di ingresso associate ai
corrispondenti valori assunti dalle uscita. Simulatore di circuiti logici: https://logic.ly/demo/

3.1.1 Legge di de Morgan


Le uniche due porte di cui abbiamo bisogno sono la porta NOT e una porta AND/OR. Avendo a disposizione
infinite porte NOT e AND/OR possiamo costruire qualsiasi circuito logico.

PRIMA LEGGE: la negazione del prodotto logico di due proposizione


equivale alla somma logica delle negazione delle due proposizioni.

SECONDA LEGGE: la negazione della somma logica di due proposizioni


equivale al prodotto logico delle negazione delle due proposizioni.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 30 a 103

3.2 Circuiti combinatori


Un circuito combinatorio è un circuito il cui funzionamento dipende solo dalla relazione degli input. Gli
ingressi e le uscite possono assumere solo due stati corrispondenti ai livello alto o basso, e le uscite sono
funzione unicamente degli ingressi. Per tale motivo sono chiamati circuiti senza memoria: le uscite in ogni
istante sono funzione esclusiva dei valori di input in quell’istante.

3.2.1 Comparatore
Un comparatore permette di confrontare due
stringhe di bit. Il comparatore rappresentato in
figura accetta due input, A e B, ciascuno lungo 3
bit, e genera 1 se sono uguali, mentre 0 se sono
diversi. Il circuito è basato sulla porta logica XOR
che produce in output un valore 0 se i suoi input
sono uguali e 1 se sono diversi.

Se due stringhe in ingresso sono uguali, tutte e tre


le porte XOR devono generare come risultato 0.
Nel nostro esempio abbiamo utilizzato una porte
NOR, nell’ultimo stadio del circuito, in modo da
invertire il risultato del test: 1 significa uguale,
mentre 0 diverso.

3.2.2 Decodificatore
Il circuito decodificatore accetta come input un numero a n bit e lo utilizza per impostare a 1 una sola delle
2𝑛 linee di output. Ciascuna porta AND ha n input, il primo dei quali è A o notA, il secondo B o notB e il
terzo C o notC ecc., e viene abilitata da una diversa combinazione dei valori ABC... Nel seguente esempio,
abbiamo un decodificatore 3 bit e i vari ingressi sono configurati nel seguente modo:

▪ AND 0: 3 NOT (A/B /C).


▪ AND 1: 2 NOT (B e C) + porta del bit A.
▪ AND 2: 2 NOT (A e C) + porta bit B.
▪ AND 3: 1 NOT (C) + porta bit A e B.
▪ AND 4: 2 NOT(A e B) + porta bit C.
▪ AND 5: 1 NOT (B) + porta bit A e C.
▪ AND 6: 1 NOT (C) + porta bit A e B.
▪ AND 7: porta bit A/B/C.

A seconda del valore dei 3 input solo una


delle linee di output assumerà il valore 1,
mentre le altre rimangono a 0. Se voglio
attivare una linea, devo portare il valore
HIGH del bit ad un ingresso e il valore LOW
negato.

Il decodificatore può essere per switchare


determinate sezione. Per esempio, in una
ALU, le linee di controllo mi servono per
abilitare le porte del circuito della somma, moltiplicazione ecc.. 1 = somma, 2 = divisione ecc..

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 31 a 103

3.2.3 Multiplexer
In logica digitale, un multiplexer è un circuito con 2𝑛 dati di input, n input di controllo, un valore di output.
Il circuito multiplexer non è altro che un’estensione del circuito decodificatore con l’aggiunta di dati input
che vengono instradati in base alla combinazione del decodificatore: gli input di controllo permettono di
selezionare uno dei dati di input, che viene instradato verso l’output. Nel seguente esempio mostriamo un
multiplexer con 8 input. Le tre linee di controllo A,B,C, codificano un numero a 3 bit che specifica quale
delle otto linee di input deve essere instradata verso la porta OR e quindi verso l’output.

Indipendentemente dal valore definito dalle linee di controllo, sette delle porte AND genereranno sempre il
valore 0, mentre quella rimanente produrrà in output 0 oppure 1, a seconda del valore della linea
d’ingresso selezionata. Ciascuna porta AND può essere abilitata da una diversa combinazione degli input di
controllo.

La porta AND ha n+1 input: Per esempio un multiplexer a 2 bit potrà gestire 4 output ed ogni porta AND
avrà 3 input (2 bit + dato da instradare). Ogni output della porta AND è collegata ad un ingresso di una
porta OR, in modo da raggruppare i vari output.

Un esempio pratico: Un microcontrollore può elaborare i dati provenienti da vari sensori con un solo input
(dato dall’output del multiplexer). Ad ogni ingresso del multiplexer è collegato un sensore diverso; il
microcontrollore decide di quale segnale necessita specificandone i bit di controllo.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 32 a 103

3.3 Circuiti per l’aritmetica


3.3.1 ALU
La maggior parte dei calcolatori contengono un unico circuito in grado di effettuare le operazioni binarie. La
seguente figura è un esempio di un’unità aritmetico logica o ALU; Questo tipo di ALU a 2 bit può calcolare
una qualsiasi delle quattro funzioni: A AND B, A OR B, A XOR B, NOT A (un’ALU non si limita solo a queste
quattro operazioni).

Attraverso un multiplexer è possibile selezionare quale delle 4 operazioni effettuare (le porte
AND,OR,XOR,NOT corrispondono agli input del multiplexer). L’output dell’operazione sul singolo bit viene
instradata sulla porta OR che genera l’output finale. In questo caso la codifica del multiplexer è la seguente:

▪ 0 attiva la linea A OR B.
▪ 1 attiva la linea A AND B.
▪ 2 attiva la linea A XOR B.
▪ 3 attiva la linea NOT A.

Il settore a sinistra contiene le porte logiche necessarie per calcolare le operazioni. In base alle linee di
attivazione, solo uno di questi risultati viene passato alla porta logica finale. Dato che solo uno degli output
del decodificatore varrà 1, soltanto una delle quattro porte AND, che forniscono i valori d’ingresso alla
porta OR, verrà attivata.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 33 a 103

3.3.2 Circuiti sommatori


Un circuito somma si compone dall’half adder e full adder:

Half Adder : in input riceve i due bit da sommare e in output


restituirà la somma e il riporto. Questo circuito è in grado si
sommare correttamente i bit meno significativi di due
stringhe binarie, ma non è in grado di eseguire correttamente
la somma degli altri bit, dato che non riesce a gestire il riporto
che arriva dalle posizioni precedenti.

Full adder : sommatore completo, circuito somma che tiene


conto in input anche del riporto. Il circuito è costruito a
partire da due half adder. Per esempio, per generare un
sommatore di due parole a 16 bit occorre replicare il circuito
per 16 volte.

OR dopo i due AND perché? Perché non ci saranno mai due


volte il riporto, quindi basta un OR

Circuito somma 3 bit (1 half adder + 2 full adder)

Circuito somma 3 bit (3 full adder)

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 34 a 103

3.4 Circuiti di memorizzazione Tabella di verità NOR

La memoria è utilizzata per conservare sia le istruzioni da eseguire sia i dati.

3.4.1 Latch SR
P1
Per creare una memoria a 1 bit è necessario disporre di un circuito
che in quale modo “ricordi” i precedenti valori di input. La seguente
figura mostra come sia possibile costruire un circuito di questo tipo
utilizzando due porte NOR.

Il seguente circuito è chiamato latch SR e ha due input: S per


impostare il valore del latch e R per azzerarlo. Il circuito ha anche due
output: Valore negato (NOT Q) e valore memorizzato (Q). P2

Quando gli ingressi vengono attivati, attraverso opportuni livelli logici, l’uscita Q e di conseguenza NOT Q
possono essere portati in uno dei due stati 0 o 1, nei quali può rimanere stabilmente anche quando gli
ingressi vengono disattivati. Si tratta di un dispositivo bistabile, cioè dotato di due stati stabili nei quali può
rimanere bloccato e mantenere un bit. Si possono verificare diverse situazioni:

▪ S=R=0: In questo caso le uscite delle porte NOR non dipendono dagli ingressi, ma da Q e NOT Q. La
condizione S=R=0 causa una condizione di mantenimento delle uscite, cioè di memorizzazione dei valori
logici precedentemente assunti.
▪ S=0 ed R=1: La porta P1 (la prima in alto) ricevendo in ingresso un 1, commuta la sua uscita a 0. La
porta P2, a sua volta, avendo in ingresso S0 e Q0 assume in uscita il livello logico NOT Q=1. Lo stato di
ingresso S=0 ed R=1 determina una situazione di azzeramento dell’uscita Q.
▪ S=1 ed R=0: L’uscita della porta P2, che riceve in ingresso un 1 commuta la sua uscita a 0, di
conseguenza l’uscita di P1, avendo entrambi gli ingressi a 0, commuta a livello logico 1. La condizione
S=1 ed R=0 provoca l’impostazione dello stato logico 1 sull’uscita Q.
▪ S=R=1: Le uscite di entrambe le porte andranno a 0. Di conseguenza le due uscite non sono più l’una il
complemento dell’altro.

FUNZIONAMENTO: Il latch viene spesso utilizzato nel seguente modo: Se si vuole memorizzare 1, si pone
S=1 e R=0. Successivamente si torna nello stato di riposo S=0 e R=0 e in tal caso l’uscita conserva lo stato
precedente. Se si vuole memorizzare 0 si pone S=0 e R=1. Successivamente si torna nello stato di riposo:
S=0 e R=0 e in tal caso l’uscita conserva lo stato precedente.

3.4.2 Latch SR temporizzato


Spesso è preferibile impedire che un latch cambi stato se non in
specifici momenti. Un circuito che gode di questa caratteristica è
detto latch SR temporizzato;

Per costruirlo bisogna modificare leggermente il circuito latch


aggiungendo un input aggiuntivo (clock, CK). Quando il clock vale
0 entrambe le porte AND genereranno in output il valore 0
(indipendentemente dai valori di set e reset, impedendo così al
latch di cambiare stato). Quando il clock vale 1 le porte AND non
bloccano più i segnali S e R che possono dunque tornare a pilotare
lo stato del latch.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 35 a 103

3.4.3 Latch D
Il latch di tipo D è un circuito nel quale viene eliminata la condizione
S=R=1 tipica del latch SR. Basta notare che quando bisogna
memorizzare 0 si imposta SET=0 e RESET=1, mentre per
memorizzare 1 si imposta SET=1 e RESET=0, quindi, SET e RESET
sono sempre l’uno l’opposto dell’altro. Per risolvere il problema
della condizione S=R=1 basta porre R come NOT D e S=D.

Problema: Essendo il reset sempre il contrario del set, non ci sarà mai la configurazione in cui SET=0 e
RESET=0, quindi il latch D, così com’è, non potrà mai memorizzare un valore. Per risolvere questo problema
si aggiungono 2 porte AND a monte del buffer S e R e un nuovo segnale (abilitazione). Gli input della prima
porta AND sono D e Abilitazione, mentre gli input della seconda porta AND sono NOT D e abilitazione. A
causa delle porte AND, quando abilitazione=0 il circuito sarà
0 (S=0, R=0).

Per memorizzare: imposto HIGH o LOW D e abilitazione=1.


Quando abilitazione sarà 0, il valore precedentemente
impostato verrà mantenuto. Il circuito finale sarà il
seguente:

3.4.4 Generatore di impulsi


Teoricamente il generatore di impulsi non dovrebbe mai
funzionare, poiché essendoci una porta NOT in input alla porta
AND, quest’ultima non sarà mai vera. Ma in realtà, le porta hanno
un piccolo ritardo che è pari al tempo necessario alla porta per
calcolare la funzione.

In questo caso la porta NOT impiega qualche istante per calcolare la negazione dell’input; quindi, per un
certo istante la porta AND sarà VERA. L’impulso sarà breve e pari al tempo necessario alla porta NOT per
calcolare la negazione.

3.4.5 Flip-flop D
Il circuito flip-flop è la forma più comune di memoria in grado di immagazzinare un bit. Possiede uno
stato stabile che si mantiene definitivamente nel tempo, se non intervengono cause esterne a modificarlo
(D), e un generatore di impulsi detto clock. Il compito del generatore è quello di controllare la
temporizzazione del dispositivo logico e di regolarne la velocità di esecuzione. La base del circuito è quella
del latch D con abilitazione, ma l’abilitazione è sostituita dal clock. In questo modo sarà possibile
memorizzare il valore D solo durante gli impulsi positivi del generatore.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 36 a 103

3.5 Bus del calcolatore


Un bus è un collegamento elettrico che unisce diversi
dispositivi. I bus possono essere classificati in base alla
loro funzione; alcuni di loro sono impiegati
internamente alla CPU per trasferire dati da e verso la
ALU, mentre altri sono esterni e servono per connettere
la CPU con la memoria o altri tipo di dispositivi. Mentre i
progettisti della CPU sono liberi di utilizzare, all’interno
del chip, il tipo di bus che preferiscono, per i bus esterni
bisogna definire delle regole precise di funzionamento.
L’insieme di queste regole è detto protocollo del bus.

Alcune periferiche che si collegano al bus sono attive e possono iniziare un trasferimento dati, mentre altre
sono passive e restano in attesa di una richiesta. Quelle attive sono chiamate master, e quelle passive
slave. Quando la CPU ordina al controllore di un disco di leggere o scrivere un blocco, svolge il ruolo di
master, e il controllore del disco quello di slave. Successivamente il controllore del disco fungerà da master
nel momento in cui ordina alla memoria di accettare le parole che sta leggendo dal disco.
Modalità di trasferimento:

▪ Seriale: Viene trasmesso un bit per volta su un’unica linea del bus.
▪ Parallelo: Vengono trasmessi più bit utilizzando più linee del bus.

Molto spesso i segnali digitali generati dalle periferiche sono troppo deboli per alimentare un bus,
soprattutto se è relativamente lungo o se è collegato a molti dispositivi. Per questo motivo molti master
sono connessi al bus mediante un chip chiamato driver del bus che funge da amplificatore digitale; in
modo analogo la maggior parte degli slave sono connessi al bus attraverso un ricevitore del bus. I
dispositivi che possono svolgere sia il ruolo di master sia quello di slave utilizzano un chip chiamato
trasmettitore-ricevitore del bus.

3.5.1 Ampiezza del bus


Nella progettazione dei bus il parametro più importante da considerare è la sua ampiezza. Maggiore è il
numero di linee del bus, maggiore sarà la quantità di dati che possono essere trasferiti. Esistono due modi
per aumentare la larghezza di banda dei dati su un bus:

▪ Aumentare il numero di linee del bus, aumentando di conseguenza il costo .


▪ Aumentare il numero di trasferimenti per secondo diminuendo il periodo di clock.
▪ Aumentare la velocità del bus.

Quest’ultima soluzione pone alcune difficoltà: i segnali su linee distinte viaggiano a velocità leggermente
diverse. Questo problema è conosciuto come disallineamento del bus, e più il bus è veloce e più questo
problema diventa evidente. Un altro problema è quello della perdita della retrocompatibilità. Schede
progettate per bus più lenti non funzioneranno con il nuovo bus.

La soluzione migliore è quella di aumentare le linee del bus; Un bus generico può essere diviso in 3 linee:

▪ Linee dati: Linea in cui vengono trasmessi i dati


▪ Linea di controllo: Trasmette informazioni di
controllo (segnali read/write, interrupt ecc.) e
temporizzazione.
▪ Linee di indirizzo: Identifica la sorgente o la
destinazione dei dati.
Corso di studi in Informatica (A-L) A.A. 2021/2022
Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 37 a 103

3.5.2 Temporizzazione del bus


La temporizzazione è il modo con cui gli eventi (scrittura, lettura, ecc..) sono coordinati sul bus. In base al
modo in cui gli eventi sono sincronizzati possiamo distinguere due categorie di bus:

▪ Bus sincrono:

Ha una linea pilotata da un oscillatore a cristalli. Su questa linea un segnale consiste in un’onda quadra con
frequenza generalmente compresa tra 5 e 100 Mhz. Tutte le operazioni sul bus richiedono un numero
intero di questi cicli, chiamati cicli del bus. Per esempio, se una CPU e una memoria sono in grado di
completare un trasferimento in 3,3 cicli, sono tuttavia obbligate ad allungare il tempo necessario a 4 cicli,
dato che ogni operazione sul bus si svolge in tempi multipli del clock (frequenza).

Svantaggio: Il tempo di esecuzione di una comunicazione è vincolato dai multipli dei cicli di clock. Inoltre,
una volta costruito un bus sincrono e collegati i rispettivi dispositivi, risulterà difficile in futuro adattarsi a
eventuali sviluppi tecnologici; Anche se in un sistema ci sono dispositivi più veloci, la velocità di
comunicazione è vincolata dal dispositivo più lento.

▪ Bus asincrono:

Nei bus asincroni le operazioni non sono scandite dai cicli di clock, ma ogni operazione è causa della
successiva. Attraverso dei segnali di controllo di inizio e fine trasmissione, sarà possibile sincronizzare le
varie unità.

3.5.3 Arbitraggio del bus


Che cosa succede se due o più dispositivi vogliono diventare master del bus nello stesso momento? La
risposta è che si rende necessario qualche forma di arbitraggio del bus per evitare conflitti. Il meccanismo
di arbitraggio può essere:

▪ Centralizzato:

In questo schema un singolo arbitro del bus determina chi sarà il prossimo master. Quando l’arbitro vede
una richiesta di utilizzo del bus, lo concede assegnandone una linea per la trasmissione. Quando il
dispositivo fisicamente più vicino all’arbitro vede la connessione, effettua un controllo per verificare se ne
ha fatto richiesta. In caso affermativo si impossessa del bus, senza propagare la richiesta lungo il resto della
linea. Se invece non ha fatto richiesta, propaga la concessione sulla linea in direzione del prossimo
dispositivo che si comporterà allo stesso modo, e così pure i successivi, finché un dispositivo non accetterà
la connessione e si impossesserà del bus.

Molti bus, per aggirare il fatto che le priorità sono


implicitamente determinate dalla distanza rispetto
all’arbitro, definiscono dei livelli di priorità. Per
ciascun livello esiste una linea per effettuare una
richiesta e una per segnalare la connessione. A
ciascun dispositivo viene assegnato un livello per
richiesta del bus; a quelli per cui la temporizzazione
è più critica vengono assegnate priorità più elevate.

▪ Decentralizzato: Il controllo delle richieste viene effettuato da ogni dispositivo.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 38 a 103

4.0 Livello ISA, istruzioni e indirizzamento


Il livello ISA descrive l’insieme delle istruzioni che una CPU è in grado di eseguire in hardware. Scrivere
programmi complessi utilizzando direttamente istruzioni ISA è difficile e spesso inutile: I linguaggi di alto
livello sono molto complessi e definire delle primitive ISA articolate richiederebbe la realizzazione di CPU
troppo costose.

In quasi tutte le architetture è possibile scrivere programmi utilizzando linguaggi di alto livello che vengono
compilati da programmi chiamati compilatori. In informatica esistono due tipologie popolari di architetture
basate sul set di istruzioni. Sono:

▪ Architettura CISC (Complex Instruction Set Computer): Indica un’architettura per i microprocessori
formata da un set di istruzioni in grado di eseguire operazioni complesse. Per esempio, con una sola
istruzione è possibile leggere un dato, modificare e salvare su disco contemporaneamente.

Spesso le architettura CISC traducono le loro istruzioni in un lotto di operazioni elaborate con RISC. Questa
traduzione (attraverso interprete) è effettuata a livello di processo in modo che l’istruzione complessa sia
spacchettata in più istruzioni semplici. I processi dell’architettura x86 di Intel adottano questa tipologia.

Il vantaggio di queste architetture è il poter avvicinare il linguaggio macchina ai linguaggio di alto livello.

▪ Architettura RISC (Reduced Instruction Set Computer): Indica un’architettura per microprocessori che
possiede un set di istruzioni più semplici.

Il vantaggio è il poter eseguire le operazioni in maniera più veloce.

4.1 Tipologie di istruzioni


La caratteristica principale del livello ISA è l’insieme d’istruzioni macchina che definisce ciò che la macchina
è in grado di fare. Le istruzioni comprese nel livello ISA possono essere classificate nelle seguenti categorie:

▪ Istruzioni di trasferimento dati: Poter copiare dati da una locazione all’altra. Per esempio, le istruzioni
di assegnamento (A=B, copia in A i bit contenuti nella locazione di B). Le istruzioni disponibili sono:
- LOAD: Trasferimento dati dalla memoria ai registri.
- STORE: Trasferimento dati dai registri alla memoria.
- MOVE: Per la copia dei dati tra registri.

▪ Istruzioni binarie: Le istruzioni binarie producono un risultato dalla combinazione di due operandi. Tutti
gli ISA, ad esempio, posseggono operazioni per la somma e sottrazione di numeri. Tra le operazioni
binarie vi sono anche le operazioni booleane come AND/OR (e NOT che è unario) e a volta
XOR/NOR/NAND. Le operazioni booleane sono dette operazioni bitwise, cioè il calcolo viene effettuato
bit per bit.

Esempio: Un uso importante dell’AND è l’estrazione della parola. Ciò avviene mediante l’utilizzo di un AND
tra il dato originale e una costante, detta maschera, che identifica la parola da estrarre. La maschera si
comporrà di 1 dove vi sono i bit da estrarre e di 0 negli altri bit. Il risultato viene fatto scorrere in modo da
isolare a destra il risultato. Come nel seguente esempio:

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 39 a 103

Esempio: Un uso importante dell’OR è quello di impacchettare bit in una parola. Nel seguente esempio,
abbiamo una word A in cui l’ultimo byte è tutto a 0. Desidero scrivere 11011010 nell’ultimo byte. Quindi si
fa A OR B.

Gli operatori di shifting sono operatori unari che realizzano lo spostamento a sinistra o a destra dei bit di
una variabile di tipo intero, mettendo a 0 i bit extra. Per esempio, con uno shifting a destra, i bit che
scorrono creano uno spazio vuoto a sinistra che viene colmato con bit a 0. Questo fenomeno provoca una
perdita di informazione, poiché i bit shiftati a destra vengono persi e non sarà possibile recuperarli in un
secondo momento. Infatti, lo shifting è un operazione irreversibile.

Un altro operatore è la rotazione. Si effettua sempre uno shifting ma i bit che fuoriescono vengono inseriti
negli spazi vuoti che si generano nella direzione opposta. L’operazione di rotazione, invece, non comporta
perdita di informazione, perché essa è un’operazione reversibile.

▪ Istruzioni di confronto: Uguaglianza tra parole, verificare se una parola è zero, maggiore e minore.
▪ Istruzioni di trasferimento in ingresso/uscita: Istruzioni che interagiscono con i dispositivi di i/o.
▪ Istruzioni di salto: Il codice è scritto mediante l’ausilio di etichette, che definiscono le sezioni del
programma. Le istruzioni di salto consentono al programma di passare da una sezione all’altra. Le
operazioni di salto si dividono in:

o Salti incondizionati: Si passa direttamente ad un’altra sezione del


programma. Esempio, GOTO().
o Salti condizionati: Se si verifica una condizione, in base all’esito, si passa ad
un’altra sezione del programma. Un esempio pratico è l’if/while.

Funzionamento if(): Ad ogni condizione if, il compilatore genera una sezione di


codice vero e una falsa (else). Alla fine delle sezioni ci sarà il ritorno (GOTO-
salto incondizionato) nel flusso di partenza.

Spaghetti code: È un termine dispregiativo attribuito a un codice che ha una


struttura complessa e/o incomprensibile, con uso esagerato di salti. Il suo
nome deriva dal fatto che questo tipo di codice tende ad assomigliare a un
piatto di spaghetti, ovvero un mucchio di fili intrecciati ed annodati.

▪ Chiamata a funzione: Quando una funzione termina la propria esecuzione, il programma deve
riprendere dall’istruzione successiva alla chiamata a funzione.

Dunque l’indirizzo di ritorno deve essere memorizzato oppure passato alla funzione chiamata. Il
meccanismo così espresso può essere fallace nei casi di funzioni ricorsive, in quanto, se la variabile in cui
memorizzare l’indirizzo di ritorno è unica, potrebbe essere sovrascritta continuamente. È necessario che
l’indirizzo di ritorno venga memorizzato ogni volta in una locazione differente.

Si utilizza lo stack dei record di attivazione. Lo stack (pila) è una struttura


dati le cui modalità di accesso ai suoi contenuti seguono una modalità LIFO.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 40 a 103

Cioè è possibile inserire i dati in questa pila e il dato in cima alla pila è il primo dato che può essere letto.

Esempio: fattoriale ricorsivo

4.1.1 Formato istruzioni


Un’istruzione consiste in un opcode (etichetta che identifica l’operazione) e altre informazioni quali la
provenienza (indirizzo memoria) degli operandi (valori di variabili) o la destinazione (indirizzi memoria) dei
risultati. La scelta dei formati d’istruzioni prevede che i progettisti tengano conto di molti fattori:

1 A parità di progetto, le istruzioni più corte sono preferibili, poiché sono più veloci da elaborare.
2 È necessario prevedere spazio sufficiente per rappresentare tutte le istruzioni desiderate: Se voglio che
il processore possa eseguire 100 istruzioni diverse devo prevedere 100 codici di OPCODE. È necessario
considerare degli OPCODE in eccedenza per eventuali evoluzioni del progetto.
3 La dimensione degli indirizzi dipende dalla dimensione delle parole in memoria: Se bisogna sviluppare
un’architettura che ha una memoria di 1gb con parole da 1 byte, i 1000 indirizzi devono poter essere
rappresentati; Per poterli rappresentare serviranno log 2 𝑖𝑛𝑑𝑖𝑟𝑖𝑧𝑧𝑖 bit.

Consideriamo un’istruzione lunga 𝑛 + 𝑘 bit, dove 𝑛 è il numero di bit per l’opcode e 𝑘 il numero di bit per
l’operando. Quindi posso rappresentare 2𝑛 istruzioni e 2𝑘 indirizzi. Lo stesso spazio potrebbe essere diviso
in 𝑛 − 1 bit dedicati all’OPCODE e 𝑘 + 1 bit dedicati all’operando, dimezzando il set di istruzioni ma
raddoppiando la memoria raggiungibile. Allo stesso modo posso fare il contrario.

Una volta impostata la dimensione dell’opcode è possibile aumentare i bit utilizzando il codice operativo
espandibile. Utilizzo una configurazione riservata, nei bit destinati all’OPCODE, per indicare che l’istruzione
è in un formato differente.

Per esempio: se l’istruzione è composta da


16 bit, 4 bit opcode + 3 operandi da 4 bit, è
possibile utilizzare la configurazione 1111,
come opcode, per indicare che l’istruzione è
nel formato OPCODE + 2 operandi.
Utilizzando questo meccanismo espando il
set delle istruzioni a:

• 24 − 1 istruzioni con 3 operandi (-1 perché 1111 è riservato).


• 24 istruzioni con due operandi.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 41 a 103

4.1 Indirizzamento
Molte istruzioni contengono operandi e si pone il problema di come specificarne la posizione in memoria.
Stiamo dunque parlando di indirizzamento, che può essere nelle seguenti tipologie:

4.2.1 Immediato
Il modo più semplici con cui un’istruzione può specificare un operando è di contenere, nel campo riservato
al suo indirizzo, l’operando stesso invece che un indirizzo o qualunque altra informazione che ne descriva la
posizione in memoria. Un operando così specificato si dice immediato, poiché viene recuperato
automaticamente dalla memoria nello stesso momento in cui viene effettuato il fetch dell’istruzione;
dunque, è immediatamente disponibile all’uso.

4.2.2 Diretto
L’indirizzamento diretto utilizza come operando direttamente
l’indirizzo in memoria in cui è memorizzato.

Se da una parte il valore contenuto può cambiare, la


locazione non può. Per questa ragione l’indirizzamento
diretto serve solo ad accedere a variabili globali il cui indirizzo
è noto in fase di compilazione. Le variabili di
procedure/funzioni non hanno sempre la stessa locazione,
poiché non è possibile sapere a priori la loro posizione in
memoria.

Le variabili di procedure e funzioni vengono allocate (posizionate in memoria) solo quando la funzione
viene invocata, e cancellate quando la funzione termina; questo non si applica per le variabili globali,
perché l’indirizzo viene definito a priori. Nonostante ciò, tale modalità è molto usata, perché molti
programmi definiscono variabili globali.

4.2.3 A registro
L’indirizzamento a registro è concettualmente analogo
all’indirizzamento diretto, ma specifica un registro invece di una
locazione di memoria. Si tratta della modalità d’indirizzamento
più utilizzata dai computer, dato che i registri sono veloci in
accesso e hanno indirizzi brevi. Molti compilatori si sforzano di
prevedere quali variabili saranno richiamate più spesso e le
destinano ai registri (principio di località spazio-temporale).

4.2.4 A registro indiretto


In questa modalità l’operando proviene o è destinato alla
memoria (es: variabili), ma il suo indirizzo non è incorporato
nell’istruzione. L’indirizzo è contenuto in un registro; Quando
un indirizzo è usato in questa maniera prende il nome di
puntatore.

Quindi non bisogna specificare, in fase di compilazione,


l’indirizzo della parola in memoria, ma semplicemente utilizzare dei registri puntatori. Inoltre la stessa
istruzione può essere utilizzata su diverse parola in memoria, semplicemente cambiando il valore del
registro.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 42 a 103

4.2.5 Indicizzato
L’indirizzamento indicizzato consente di referenziare
una parola in memoria che si trova a un certo
spiazzamento rispetto a un registro.

Definizione spiazzamento:

𝑖𝑛𝑡 𝑣[100] ; // v non è una variabile, ma un puntatore


alla prima cella del vettore

Per accedere alla quinta cella del vettore, utilizzo il


puntatore (indirizzo base) e lo spiazzamento (5): 𝑎 = 𝑣 + 5;

Questo tipo di indirizzamento si ottiene specificando il registro e lo spiazzamento (costante). Questo


meccanismo viene utilizzato in alcune occasioni nelle quali è nota a priori la distanza tra una variabile e
l’altra.

Nel vettore, la distanza tra due posizioni non è costante: 𝑣[𝑖] non ha uno spiazzamento costante, poiché (i)
è un valore variabile. È possibile utilizzare l’indirizzamento indicizzato solo quando specifico, attraverso una
costate, lo spiazzamento; Per esempio, 𝑣[4] (4 è costante). Un esempio pratico in cui è nota la distanza tra
le variabili: Quando scrivo un codice in cui dichiaro 3 variabili, queste verranno referenziate in memoria in
successione.

𝐼𝑛𝑡 𝑎, 𝑏, 𝑐 ; a[loc:50] b[loc:54] c[loc.58]

Sapendo la dimensione di un intero (4 byte), posso utilizzare come punto di riferimento la prima locazione
e sfruttare lo spiazzamento. Per accedere a c: 𝑎 + 8 𝑏𝑦𝑡𝑒 (spiazzamento:8)

4.2.6 Indicizzato esteso


L’indirizzamento esteso consente di referenziare un indirizzo in memoria ottenuto sommando tra loro il
contenuto di due registri, più un eventuale offset aggiuntivo.

Per esempio, caso vettore: Inserisco nel primo registro il puntatore alla locazione base del vettore e nel
secondo registro un puntatore allo spiazzamento variabile (i). Quando bisognerà accedere a 𝑣[𝑖], il
processore farà la somma dei due registri.

4.2.7 A stack
Alcune istruzioni possono essere utilizzate in combinazione con una struttura a stack. Un esempio pratico di
utilizzo è quello legato alla notazione polacca inversa.

4.3 Notazione polacca inversa


Si possono scrivere le operazioni algebriche:

▪ Notazione infissa, l’operatore è posto tra gli operandi: 𝐴 + 𝐵


▪ Notazione prefissa: +𝐴𝐵
▪ Notazione postfissa: 𝐴𝐵 +

La notazione postfissa (polacca inversa) presenta alcuni vantaggi:

▪ Ogni operazione può essere scritta senza parentesi.


▪ Gli operatore non hanno un ordine di priorità. In notazione infissa: 𝑎 + 𝑏 × 𝑐 corrisponde a 𝑎+(𝑏 × 𝑐).
▪ Le formule in notazione polacca inversa si addicono ai compilatori con stack.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 43 a 103

Alcuni esempi di notazione polacca inversa:

Notazione infissa Notazione polacca inversa


𝐴+𝐵×𝐶 𝐴𝐵𝐶 × +
𝐴×𝐵+𝐶 𝐴𝐵 × 𝐶 +
𝐴×𝐵+𝐶×𝐷 𝐴𝐵 × 𝐶𝐷 × +
(𝐴 + 𝐵)/(𝐶 − 𝐷) 𝐴𝐵 + 𝐶𝐷 −/
𝐴 × 𝐵/𝐶 𝐴𝐵 × 𝐶/

▪ CASO 1: Primo operando A, secondo operando 𝐵 × 𝐶. Quindi bisogna scrivere primo operando,
secondo operando e la somma. Il secondo operando è 𝐵 × 𝐶, quindi 𝐴𝐵𝐶 × +
▪ CASO 2: Somma+Molitplicazione: 𝐴𝐵𝐶 +, ma 𝐴𝐵 è una moltiplicazione, quindi 𝐴𝐵 × 𝐶 +
▪ CASO 3: Primo operando 𝐴𝐵 ×, secondo operando 𝐶𝐷 ×. Somma dei due operandi: 𝐴𝐵 × 𝐶𝐷 × +
▪ CASO 4: Primo operando 𝐴𝐵 +, secondo operando 𝐶𝐷 −. Divisione dei due operandi: 𝐴𝐵 + 𝐶𝐷 −/

Tutte le operazioni devono essere espresse due per volta, quindi 𝐴 + 𝐵 + 𝐶 = 𝐴𝐵 + 𝐶 +

Come il computer elabora le operazioni di notazione


polacca inversa:

▪ Quando trova un valore (es: AB) viene aggiunto in


cima allo stack.
▪ Quando trova un operatore (es: +), prendo gli
ultimi due valori dello stack ed effettuo
l’operazione, inserendo il risultato in cima allo
stack.

Per esempio: 3 × (10 + 5)

Un esempio più complesso: data la seguente espressione (8 + 2 × 5)/(1 + 3 × 2 − 4), il calcolatore


esegue l’espressione nel seguente modo:

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 44 a 103

Istruzioni:.

▪ BIPUSH: Inserisco valore nello stack.


▪ IMUL: moltiplicazione di due valori.
▪ IADD: somma di due valori.
▪ ISUB: differenza di due valori.
▪ IDIV: divisione di due valori.

Vantaggio: Se l’espressione è scritta nella forma classica, per verificare eventuali errori devo calcolare tutte
le priorità e parentesi. Invece, con la notazione polacca è molto più semplice perché possono verificarsi solo
due situazioni:

1. Trovo un operatore ma non ci sono due valori nello stack.


2. Finisco l’operazione ma ho più di un valore nello stack.

Esercizi:

▪ 20 + 15 × (100 − 5) − 21 = 20 15 100 5 −× +21 −


▪ 5 + (10 × 2) = 5 10 2 × +
▪ ((10 × 2) + (4 − 5))/2 = 10 2 × 4 5 − +2/
▪ (7/3)/((1 − 4) × 2) + 1 = 1 7 3 / 1 4 − 2 ×/+

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 45 a 103

5.0 Sistema operativo, gestione dei processi e schedulazione


5.1 Sistema operativo
Un sistema operativo è un software di base adibito a gestire le risorse hardware e software di un
computer. Un sistema operativo deve garantire:

▪ Convenienza: Un SO è ciò che rende conveniente l’uso di un computer.


▪ Efficienza Nell’utilizzo del calcolatore e delle sue parti hardware (gestione processi, cpu, memoria ecc.).
▪ Capacità di evolversi: Un SO dovrebbe essere costruito in modo da permettere lo sviluppo
e l’introduzione di nuove funzioni di sistema senza interferire con l’attività del sistema operativo stesso.

Il sistema operativo si pone come interfaccia tra utente/programmi e hardware e permette loro di
comunicare e interagire in modo sicuro. Il SO, nasconde i dettagli dell’hardware all’utente e gli fornisce
un’interfaccia conveniente per usare il sistema. Esso agisce come un mediatore, rendendo più semplice
l’accesso e l’uso di funzioni e servizi. Per questo si dice che il SO agisce in maniera trasparente. Non può
esistere una comunicazione diretta tra l’utente e l’hardware, dovuta sia dalla complessità dell’architettura
e soprattutto dal voler mettere a disposizione dell’utente un sistema affidabile e di facile utilizzo.

Trasparenza vuol dire anche portabilità: Se un SO è stato progettato in maniera trasparente vuol dire che è
stato pensato per funzionare in generale. La portabilità di un SO è la sua capacità di potersi adattare, più o
meno facilmente, per funzionare in diversi ambienti di esecuzione in base alla sua capacità di astrarsi dalle
specificità dell’hardware. La grande difficoltà è avere una raccolta di driver di dispositivo sufficientemente
ampia da rendere interessante l’utilizzo del SO. I servizi offerti dal sistema operativo sono:

1. Creare programmi: Il SO fornisce diverse funzioni e servizi, come l’editor e il debugger, in forma di
programmi di utilità che non fanno parte del SO, ma sono accessibili attraverso di esso.
2. Esecuzione dei programmi: Per eseguire un programma, è necessario che siano compiute diverse
azioni, dal caricamento nella memoria principale di istruzioni e dati, inizializzazione dei dispositivi I/O,
alla preparazione di altre risorse. Il SO gestisce tutto questo in modo trasparente.
3. Accesso ai dispositivi I/O: Ogni dispositivo I/O opera attraverso un proprio insieme particolare di
istruzioni. Il SO si occupa dei dettagli, in modo che l’utente possa utilizzare facilmente questi dispositivi.
4. Accesso controllato ai file: Un SO si occupa della compressione dei file, meccanismi di protezione
(se un file è utilizzato da un programma X, quel file non può essere elaborato contemporaneamente dal
programma Y) e supporti di memorizzazione sicura.
5. Rilevazione e correzione degli errori: Mentre un sistema di elaborazione sta funzionando, possono
verificarsi molti errori, sia hardware che software. In tal caso il SO deve fornire una risposta rapida che
elimini la condizione di errore con il minore impatto possibile sulle applicazioni in esecuzione.
6. Contabilità: Un buon SO raccoglie statistiche d’uso delle risorse e tiene sotto controllo i vari parametri
di prestazione (per esempio: task Manager di Windows).

Un sistema operativo ha il compito di gestire le risorse del sistema e la temporizzazione dell’esecuzione dei
programmi. Quindi il SO decide quando e come una risorsa deve essere utilizzata, quali tipi di file possono
essere modificati dall’utente ecc. Parte del sistema operativo risiede nel kernel, situato in memoria
centrale, e contiene tutte le funzione del SO usate più frequentemente.

Un sistema è detto monoprogrammato quando in un dato istante contiene in memoria centrale un solo
programma. In questo modo il SO si concentra al 100% nell’esecuzione di un singolo programma con
l’utilizzo di tutte le risorse hardware. Quando il programma termina, la memoria diventa disponibile per
accogliere un nuovo programma da eseguire. Questo è il modo più semplice per gestire un elaboratore ma
risulta inefficiente nell’effettuare operazioni multitasking.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 46 a 103

Un sistema multiprogrammato gestisce simultaneamente più programmi indipendenti con l’obbiettivo di


impiegare in maniera intelligente le risorse hardware: Quando un job (istruzione) non utilizza più la CPU,
essa al posto di entrare in uno stato di wait, verrà utilizzata per l’esecuzione di altre istruzioni. Ci può essere
multiprogrammazione non solo a livello di sistema di elaborazione ma anche a livello di CPU (parallelismo),
in questo caso i job verranno suddivisi tra i vari processori.

La difficoltà dei sistemi multiprogrammati è la gestione della memoria e la schedulazione dei processi.
Per soddisfare queste esigenze, il SO deve assolvere cinque compiti principali:

1. Isolare i processi: Il SO deve impedire a processi indipendenti di interferire tra di loro.


2. Allocazione e gestione automatica della memoria: In base alle esigenze, i programmi dovrebbero
essere allocati dinamicamente (memoria virtuale) e tale allocazione dovrebbe essere trasparente
all’utente. Il SO guadagna in efficienza assegnando la memoria solo quando necessario.
3. Supporto alla programmazione modulare: Il SO deve gestire la variazione di dimensione dei
programmi. Cioè, invece di caricare in memoria l’intero programma, si caricano dei moduli in base alla
necessità del momento (modificando automaticamente la dimensione del processo).
4. Protezione e controllo dell’accesso: Il SO deve proteggere lo spazio di indirizzamento del processo.
Per esempio, il SO deve impedire a due processi di lavorare su gli stessi file o sulle stesse variabili, a
meno che i due programmi non siano stati progettati appositamente.
5. Memorizzazione a lungo termine: Il SO deve consentire la memorizzare di file in memoria secondaria.

Questi cinque compiti vengono soddisfatti da:

• Memoria virtuale: È una funzionalità che permette ai programmi di indirizzare la memoria da un punto
di vista logico, senza preoccuparsi della quantità di memoria fisicamente disponibile.
• File system: Implementare la memorizzazione dei dati a lungo termine.

5.2 Processo
Un processo (Task) è un programma in esecuzione, cioè un’entità assegnata ad un processore e caricata in
memoria centrale. Il processo è uno strumento fondamentale al fine di ottenere la massima efficienza del
sistema (multiprogrammazione):

Con molti task, diventa necessario adottare algoritmi o schemi di coordinamento e cooperazione tra
processi. Tuttavia non sempre possibile trovare una soluzione ad hoc, poiché è difficile diagnosticare un
problema in una piattaforma così grande e complesso come lo è un sistema operativo. Gli errori sono
osservabili solo al verificarsi di determinate sequenze di azioni relativamente rare, inoltre, bisogna
distinguerli da quelli hardware.

Trovato l’errore, è tuttavia problematico individuarne la causa, poiché non è facile riprodurre con
precisione le condizioni in cui si era manifestato. In sintesi, questi errori sono provocati da 4 cause
principali:

1. Sincronizzazione impropria: Accade spesso che un processo debba essere sospeso in attesa di un
evento nel sistema. Ad esempio, un programma inizia una lettura di I/O e, prima di procedere, deve
aspettare che i dati siano disponibili: in tal caso il processo rimane sospeso in attesa di un segnale di
conferma. La progettazione impropria del meccanismo di gestione dei segnali può portare alla perdita
di segnali o alla duplicazione di quelli ricevuti.
2. Fallimento della mutua esclusione: Spesso più di un task tenta di usare contemporaneamente una
risorsa condivisa. Se questi accessi in memoria non sono controllati può verificarsi un errore. Deve
esistere un qualche meccanismo di mutua esclusione, che permetta ad un solo task alla volta di
effettuare una lettura/scrittura su una determinare area di memoria.
Corso di studi in Informatica (A-L) A.A. 2021/2022
Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 47 a 103

3. Operazioni del programma non determinate: In un sistema condiviso, i risultati di un particolare


programma dovrebbero dipendere solo dall’input del programma stesso e non dall’attività degli altri
programmi. Quando i programmi condividono la memoria e la loro esecuzione è interlacciata, possono
interferire l’un l’altro, sovrascrivendo aree comuni di memoria. Pertanto, l’ordine in cui i vari
programmi sono schedulati può influire sull’esito di uno qualsiasi di essi.
4. Stallo: È possibile che due o più programmi, siano sospesi in reciproca attesa.

Per affrontare questi problemi, occorre monitorare e controllare in modo sistematico i vari programmi
eseguiti dal processore. Il concetto di processo fornisce la base a questo controllo. Si può pensare che un
processo sia costituito da tre componenti:

1. Un programma eseguibile.
2. I dati del programma (variabili, spazio di lavoro, buffer, ecc.).
3. Il contesto di esecuzione del programma .

Quest’ultimo è essenziale: il contesto di esecuzione comprende tutte le informazioni necessarie al sistema


operativo per gestire il processo, e ciò di cui necessita il processore per eseguirlo correttamente. Il contesto
di esecuzione comprende:

• Contenuto dei registri della CPU.


• Priorità del processo.
• Stato di esecuzione/attesa.

Quindi, il processo è realizzato come una struttura dati, che permette lo sviluppo di tecniche per assicurare
il coordinamento e la cooperazione fra i processi. I processi vengono classificati in:

• CPU bound (veloci): Processi che sfruttano le risorse computazionali del processore (es: calcoli +,-,/...).
• I/O bound (lenti): Processi che hanno bisogno di accedere in memoria secondaria. Il processo è spesso
sospeso per attendere il completamento delle richieste di I/O.

5.2.1 Modi di esecuzione


La maggior parte dei processori supporta almeno due modi di esecuzione, e certe istruzioni si possono
eseguire solo nel modo più privilegiato, ad esempio, la lettura o la scrittura di un registro di un controllo.
A certe regioni della memoria, inoltre, è possibile accedere solo in modalità più privilegiata.

Il modo meno privilegiato è definito modo utente, in quanto di norma i programmi utente sono eseguiti in
tale modalità. Il modo privilegiato è chiamato modo di sistema o kernel; quest’ultimo si riferisce al nucleo
del sistema operativo, ovvero a quella sua parte che comprende le funzioni di sistema più importanti.
È necessario suddividere i modi di esecuzione per proteggere il SO dalle interferenze dei programmi utente.

5.2.2 Creazione processo


Quando è aggiunto un nuovo processo, il SO costruisce le strutture dati utili a gestirlo e alloca lo spazio
d’indirizzamento. La creazione di un processo è determinata da tre eventi principali:

1. Richiesta da terminale.
2. Il SO può creare un processo per svolgere una funzione per conto di un programma utente (es: stampa)
3. Il SO può creare un processo in base alle esigenze dell’utente (es: utente apre word).

Istanziare: significa creare un elemento figlio da un elemento padre comune (finestre Google Chrome:
processo padre Chrome, processo figlio le varie finestre).

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 48 a 103

5.2.3 Terminazione processo


Un processo termina:

• Automaticamente dopo l’ultima istruzione del suo codice.


• Oppure quando viene chiamata una funzione di exit. Sarà poi il SO a provvedere e rimuovere le risorse
che erano state allocate. I dati di output del processo terminato possono essere inviati al padre,
se questo è in attesa per la terminazione del figlio.
• In alcuni sistemi, un processo o il SO può uccidere un altro processo. Questo si verifica quando:
o Il processo sta usando troppe risorse (processore, memoria, ...).
o Il suo processo padre è morto. In questo caso si verifica una terminazione a cascata.
o Fallimento di un’operazione aritmetica (𝑖𝑛𝑡 𝑎 = 1/0).

5.2.4 Descrizione del processo


Il SO gestisce i processi associando a ciascuno di essi una struttura dati detta descrittore di processo (PCB).
Il PCB contiene:

▪ Process Identification (PID): valore numerico unico di ogni processo.


▪ Registri dati visibili all’utente, in base all’architettura del calcolatore.
▪ Registri di controllo e stato:
o Program counter: contiene l’indirizzo della prossima istruzione da eseguire.
o Registri di stato.
o Registro che contengono i codici relativi alla condizione (segno, overflow, ecc.).
▪ Puntatori allo stack, per esempio il puntatore allo stack di esecuzione. In sostanza esegue procedure e
funzioni caricandole in una struttura a pila.
▪ Informazioni di controllo del processo:
o Schedulazione e informazioni di stato (stato del processo, priorità, eventi di attesa ecc.).
o Strutturazione dati, conterrà puntatori a processi figli/padri.
o Informazioni per la comunicazione tra i vari processi, per operare con memorie condivise.
o Privilegi, cioè i permessi associati al processo. Per esempio, utilizzo della memoria, scrittura ecc.
o Contabilizzazione delle risorse, contiene la lista dei file aperti, lista dei dispositivi I/O ecc.

5.2.5 Commutazione di contesto


La commutazione di contesto o context switch è quella parte del kernel che cambia il processo
correttamente in esecuzione. Questo consente di eseguire più programmi contemporaneamente e
consente di ottenere migliore bilanciamento del carico. Le cause di un cambio del processo possono essere:

▪ Clock Interrupt, Il SO determina se il processo correttamente in esecuzione sia stato eseguito per il
tempo massimo permesso. In caso affermativo, il processo passa allo stato Ready.
▪ I/O interrupt: Il SO determina quale operazioni di I/O sia avvenuta. Nel caso si tratti di un evento che
uno o più processi attendono, il SO li passa dallo stato di Blocked a Ready (e i processi blocked-
suspended passano allo stato Ready-suspended).
▪ Memory fault: Se devo accedere a una locazione che non sta in RAM, si genera il memory fault, cioè il
processore deve andare a reperire le informazioni sul disco e portarle in RAM. Il SO carica il blocco, nel
frattempo il processo che ha generato la richiesta è in blocked, al termine del trasferimento andrà in
ready.
▪ Trap: Con una trap il SO determina se un errore sia fatale o meno. In caso affermativo il SO può, in base
alle caratteristiche di progettazione, terminare il processo o tentare una procedura di recupero.
▪ Chiamata dal supervisore: file open: il processo utente va in blocked fino a quando non ottiene
l’accesso al file.
Corso di studi in Informatica (A-L) A.A. 2021/2022
Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 49 a 103

Le operazioni svolte dal SO al momento del context switch sono:

1. Salvataggio del contesto del processo che abbandona la cpu (valori registri, pcb ecc.).
2. Cambio del valore di stato nel PCB (running -> ready, blocked o exit).
3. Spostamento del pcb in nuovo coda (ready o blocked) o deallocate le sue risorse del vecchio processo.
4. Aggiornamento delle strutture dati gestione della memoria.
5. Lo schedulatore (dispatcher) sceglie il nuovo processo che deve andare in esecuzione, in base alla
propria politica di scheduling.
6. Aggiornamento del suo stato nel PCB.
7. Ripristino del contesto.

5.2.6 Modello a 2 stati


Il SO può adottare uno schema di gestione dei processi basato sul modello a due stati. In questo modo, in
ogni momento un processo è, oppure non è eseguito da un processore.

FUNZIONAMENTO: Quando il SO crea un


nuovo processo, lo introduce in una coda
di attesa nello stato di not-running: il
processo esiste, è noto al sistema
operativo ed aspetta l’occasione di essere
eseguito. Da un momento all’altro, il
processo correttamente in esecuzione
sarà interrotto mentre il dispatcher sposterà il processo in attesa in prima posizione nello stato di running.
Se il processo interrotto ha terminato la sua esecuzione esce dal ciclo, altrimenti si reinserisce in coda
ritornando nello stato di not running.

Questo modello, per quanto semplice, ci consente di apprezzare alcuni degli elementi della progettazione
di un SO: ogni processo deve essere rappresentato in modo che il SO possa conservarne traccia, in altre
parole devono esistere informazioni relative a ciascun processo, incluso lo stato corrente.

Problema: Lo stato di not running sottintende 2 diverse possibilità, ovvero in coda in attesa di andare in run
e in coda in attesa di I/O. Ma il dover attendere qualcosa, implica che il processo non può essere eseguito.

Se il processo in cima alla coda di ready ha bisogno di un dato da tastiera, essendo in prima posizione, viene
comunque caricato dal processore. Il processore si accorge di non poter eseguire il processo a causa
dell’attesa del dato di input e sposta il processo in coda. Questo ovviamente è uno spreco di risorse e di
tempo di elaborazione.

5.2.7 Modello a 5 stati


Il modello a 5 stadi è l’evoluzione del modello a 2 stadi. Se tutti i processi fossero sempre pronti per
l’esecuzione, la strategia di gestione a 2 stadi sarebbe sempre efficace:

In assenza di uno schema a priorità, la coda è di solito costituita da una lista first in first out (il primo
elemento ad entrare è il primo ad uscire), ed il processore opera in modo round-Robin sui processi
disponibili (ciascun processo dispone di una certa quantità di tempo per l’esecuzione alla cui scadenza
ritorna in coda di ready).

Tuttavia, lo schema di gestione a 2 stadi risulta inadeguato. Infatti, mentre alcuni processi nello stato di
Not-Running sono pronti per l’esecuzione, altri sono bloccati in attesa del completamento di un’operazione
di I/O. Utilizzando una singola coda, il dispatcher non può selezionare il processo da più tempo in attesa,
ma deve percorrere la lista ready, cercando il processo che non sia bloccato e che sia da più tempo in coda.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 50 a 103

La soluzione più efficace consiste nel suddividere lo stato di not-running in due stati: Ready e blocked.
Di conseguenza, il modello a 5 stadi si compone:

1. Running: Processo correttamente in esecuzione.


2. Ready: Processi pronti per l’esecuzione.
3. Blocked: Processi che non possono essere eseguito sino al verificarsi di un certo evento.
4. New: Un processo appena creato, ma non ancora ammessa dal SO nell’insieme dei processi eseguibili.
5. Exit: Un processo che è stato rilasciato, o perché è terminato, o perché è fallito.

FUNZIONAMENTO: Un processo appena creato entra nello stato di NEW. In questo stato il SO effettua
i necessari compiti: associa un identificatore al processo, alloca e costruisce le tabelle necessarie per la
gestione del processo stesso (PCB). Il processo rimane nello stato NEW, il SO ha effettuato le operazioni
necessarie per crearlo, ma non si è ancora impegnato ad eseguirlo. Infatti il SO può limitare il numero dei
processi presenti nel sistema, per non limitarne le prestazioni, o per non sovraccaricare la memoria.

Dallo stato NEW, l’ADMIT (l’operazione di ammissione nelle code di schedulazione) porta il processo nello
stato di READY. Il processo in questo stato può andare in una sola direzione, ovvero RUNNING, mediante
l’operazione di Dispatch (modulo che passa il controllo della CPU al processo). Arrivato a questo punto il
processo ha tre alternative:

1. TIMEOUT: Questa transizione è di solito motivata dal fatto che il processo ha raggiunto il tempo
massimo permesso per un’esecuzione ininterrotta (Schedulazione Round Robin, preemptive).
2. EVENT WAIT: Un processo va nello stato Blocked, se sta aspettando una risorsa. La richiesta di una
risorsa al SO viene di solito effettuata in forma di una chiamata di servizio al sistema. Un processo nello
stato di BLOCKED passa a quello di READY quando accade l’evento che stava aspettando (event occurs).
3. RELEASE: Il processo correttamente in esecuzione è fatto terminare dal SO se il processo stesso indica
di aver terminato, o se fallisce.

Dallo stato di READY e BLOCKED un processo può andare in EXIT. Questo è di solito motivato dal fatto che
un processo genitore può far terminare un figlio in ogni momento. Inoltre, se un genitore termina, tutti
i processi figli associati al genitore potrebbero terminare.

Infine, quando accade un evento, perché tutti i processi nella coda dei BLOCKED che ne sono in attesa
passino alla coda dei READY, il SO deve scandire l’intera coda dei BLOCKED cercando i processi in attesa di
quell’evento. In un grande sistema operativo, in quella coda potrebbero trovarsi centinaia o migliaia di
processi: sarebbe assai più efficiente disporre di diverse code, una per ciascun tipo di evento, in modo da
portare allo stato di READY l’intera lista dei processi presenti nella coda.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 51 a 103

È possibile migliorare ulteriormente questo schema: se la scelta dei processi è stabilita secondo uno
schema a priorità, è conveniente avere diverse code dei Ready, una per ciascun livello di priorità. Il SO
potrebbe così determinare velocemente quale sia il processo pronto a più elevata priorità.

Problema: I tre stati principali del modello a 5 stati (Ready, running e Blocked), permettono di costruire un
modello di comportamento dei processi. Esiste tuttavia un problema: Se consideriamo un sistema privo di
memoria virtuale, ciascun processo, per essere eseguito, deve essere totalmente caricato nella memoria
principale.

Due possibili soluzioni:

1. Espandere la memoria principale per accogliere più processi: tale approccio presenta però un
problema: La fame di memoria dei programmi è cresciuta tanto rapidamente quanto è sceso il costo
della memoria stessa, col risultato che attualmente memorie più grandi ospitano processi più grandi,
ma non più numerosi.
2. Un’altra soluzione è lo swapping, che consiste nello spostare un processo, o una sua parte dalla
memoria principale a quella secondaria. Questa è in ogni modo un’operazione di I/O e quindi esiste il
rischio di peggiorare la situazione, ma poiché l’I/O del disco è generalmente quello più veloce nel
sistema, lo swapping di norma migliorerà le prestazioni.

5.2.8 Modello a 6 stati


Il modello a 6 stati è molto simile al modello a 5 stati con l’aggiunta dello stato di sospensione.

FUNZIONAMENTO: Quando la memoria principale è in saturazione e sono presenti molti processi bloccati,
il SO può sospendere un processo, ponendolo nello stato Suspend e trasferendolo sul disco (swap out).
Lo spazio rimasto libero nella memoria principale può essere utilizzato per caricare un altro processo
(swap-in): Il SO può decidere di ammettere un processo appena creato oppure uno precedentemente
sospeso.

Problema: Il modello a 6 stati pone un problema analogo a quello a 2 stati. La CPU non sa quale processo
nella coda di sospensione è pronto per l’esecuzione. La CPU potrebbe spostare un processo che è ancora in
attesa del completamento dell’evento dalla memoria secondaria alla memoria principale. Non ha senso
spostare un processo in attesa in Ready, in quanto arrivato in running sarà immediatamente spostato in
blocked. Quindi, scindiamo lo stato di suspended in Ready/suspended e blocked/suspended.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 52 a 103

5.2.9 Modello a 7 stati


Il modello a 7 stati è molto simile al modello a 5 stati con l’aggiunta dello stato di sospensione diviso in
blocked suspended e ready suspended:

▪ Blocked suspend: Il processo è in memoria secondaria e in attesa di un evento.


▪ Ready suspend: Il processo è in memoria secondaria ma è disponibile per l’esecuzione.

FUNZIONAMENTO: Un nuovo processo appena creato può essere aggiunto alla coda dei Ready oppure a
quella dei Ready-Suspend. In entrambi i casi, il SO deve costruire delle tabelle per gestire il processo ed
allocare il relativo spazio di indirizzamento:

▪ Il SO potrebbe preferire di effettuare subito queste operazioni, in modo da ottenere un ampio insieme
di processi non bloccati. Con questa strategia però, lo spazio nella memoria principale sarebbe spesso
insufficiente per un nuovo processo; da qui l’uso della transizione New - Ready Suspend.
▪ D’altro canto ritardare il più possibile l’allocazione dei processi riduce il sovraccarico del sistema.

Il processo arrivato in READY può andare in esecuzione o essere sospeso (swap out). Di norma, il SO
preferirebbe sospendere un processo bloccato piuttosto che uno pronto, poiché quello pronto può essere
eseguito subito, mentre uno bloccato sta occupando spazio di memoria principale e non può essere
eseguito. Tuttavia, può essere necessario sospendere un processo pronto se questo ha priorità bassa,
piuttosto che un processo bloccato a priorità maggiore, se si ritiene che il processo bloccato sarà pronto in
breve tempo. In caso di saturazione della memoria principale un processo bloccato può essere sospeso e
spostato in Blocked-Suspend (swap out) al fine di far spazio ad un altro che non lo è. Questa transizione è
possibile anche se esiste un processo in Ready-Suspend che richiede più memoria di quella disponibile.

Un processo in Blocked-Suspend può:

• essere spostato in Ready-suspend al verificarsi dell’evento per cui il processo era stato bloccato.
• essere spostato in blocked quando il processo sospeso ha priorità maggiore di tutti i processi della coda
Ready-Suspend, e il SO ha ragione di credere che l’evento bloccante di quel processo accadrà presto.
Corso di studi in Informatica (A-L) A.A. 2021/2022
Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 53 a 103

Di norma, un processo in esecuzione passa allo stato di ready quando termina il tempo che gli era stato
destinato. Se però il sistema operativo sta scegliendo un processo a più alta priorità appena sbloccato,
potrebbe spostare il processo in esecuzione direttamente in coda Ready-suspend, liberando memoria.

In ogni caso, da qualsiasi stato un processo può andare in EXIT se termina a causa del genitore o a causa di
un errore fatale.

Esiste uno schema di gestione della memoria noto come memoria virtuale, nel quale un processo può
trovarsi solo parzialmente in RAM. Quando si fa riferimento a un indirizzo su disco, la locazione viene
caricata in RAM. Se noi abbiamo uno schema a memoria virtuale, gli stati di sospensione non hanno più
senso, perché i processi sono comunque parzialmente caricati in RAM.

5.3 Schedulazione
Un compito essenziale dei sistemi operativi è la gestione delle diverse risorse disponibili e lo schedulare il
loro utilizzo. Lo scheduler è un componente del SO che implementa un algoritmo di scheduling, il quale,
dato un insieme di richieste di accesso al processore, stabilisce un ordinamento temporale per l’esecuzione
di tali richieste, privilegiando quelle che rispettano determinati parametri secondo una certa politica di
scheduling.

Una politica di schedulazione deve considerare tre fattori:

1. Equità: Tutti i processi che sono in competizione per l’utilizzo di una particolare risorsa dovrebbero
godere della stessa possibilità di accesso a quella risorsa.
2. Tempi di risposta differenziale: È possibile che il SO debba discriminare fra classi differenti di task.
Se un processo chiede il processore per poco tempo e poche volte viene avvantaggiato.
3. Efficienza: Entro i vincoli dell’equità e dell’efficienza, il SO dovrebbe cercare di massimizzare il
throughput, cioè minimizzare il tempo di risposta.

Lo scheduling è quell’insieme di tecniche e meccanismi interni del SO che amministrano l’ordine in cui il
lavoro viene svolto. L’obbiettivo dello scheduling è l’ottimizzazione delle prestazioni del sistema.

Il SO può prevedere fino a 3 tipi di scheduler:

1. Scheduler di lungo termine (SLT)


2. Scheduler di medio termina (SMT)
3. Scheduler di breve termine (SBT)

Gli scheduler intervengono secondo il


seguente schema (modello a 7 stati):

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 54 a 103

5.3.1 Scheduler di lungo termine


Lo scheduler di lungo termine determina quali programmi inserire nel sistema per l’esecuzione, perciò
controlla il grado di multiprogrammazione. La decisione di creare un nuovo processo è generalmente
guidata dal grado di multiprogrammazione desiderato. Più processi sono creati, minore è il tempo a
disposizione di ogni processo per l’esecuzione. Opera nella transizione:

▪ New -> Ready.

Lo scheduler funziona seconda delle stime effettuate dal programmatore o dal sistema, in modo da fornire
un’idea su quante risorse ha bisogno un processo (dimensione della memoria, tempo di esecuzione, ecc.).
Quindi, lo scheduler di lungo termine, lavora stimando il comportamento dei processi. Le strategie
principali:

1. Fornire alla coda dei processi pronti gruppi di processi che siano bilanciati tra loro nello sfruttamento
della CPU e dell’I/O (Cpu bound e i/o bound). Esagerare nello schedulare processi CPU-bound
significherebbe sfruttare al meglio il processore, ma annullare le interazioni con l’utente. Utilizzare
troppi I/O bound garantirebbe invece una corretta e fluida interazione, ma con tempi di elaborazione
eterni.
2. Se lo scheduler si accorge di avere tanti processi i/o bound in ready, l’uso complessivo della cpu
diminuisce (perché i processi vanno in blocked, in attesa dell’evento). In questo caso lo scheduler carica
nuovi processi cpu bound per aumentare l’efficienza del processore.
3. Se il carico dei processi è alto o i tempi di risposta del sistema operativo diminuiscono, lo scheduler
diminuisce (fino a bloccare) i lavori provenienti dalla coda batch.

Lo scheduler di lungo termine è usualmente lento e complesso, perché, scegliere la combinazione più
ragionevole di processi da caricare in memoria per sfruttare al meglio il processore, non è affatto un lavoro
banale. Deve perciò essere eseguito poco frequentemente per evitare di sovraccaricare il sistema.

5.3.2 Lo scheduler di medio termine


Lo scheduler di medio termine è impiegato nel gestire la permanenza e il trasferimento in memoria dei
processi non in esecuzione. Opera quindi nelle transizioni:

▪ Ready suspended <--> ready.


▪ Blocked suspended <--> blocked.

La presenza di molti processi sospesi in memoria riduce la possibilità per i nuovi processi pronti (meno
memoria disponibile). In questo caso lo scheduler di breve termine è obbligato a scegliere tra i pochi
processi pronti. Si sta verificando uno sbilanciamento: quei pochi processi in RAM stanno ricevendo la
massima computazione. Viene attivato quando:

1. Si rende disponibile lo spazio in memoria.


2. L’arrivo di processi pronti scende al di sotto di una soglia specificata.

5.3.3 Scheduler di breve termine (dispatcher)


Lo scheduler di breve termine si occupa della selezione del nuovo processo da eseguire. Il dispatcher è un
modulo del SO che passa il controllo (effettua il context switch) della CPU ai processi scelti dallo scheduler
di breve termine: poiché la sua attivazione è molto frequente, il dispatcher dovrebbe essere quanto più
rapido possibile. Lo scheduler di breve termine opera nella transizione:

▪ Ready -> run.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 55 a 103

5.4 Algoritmi di schedulazione


Gli algoritmi di schedulazione necessitano di due tempi:

Tempo di ricircolo: Tempo trascorso dall’avvio di un processo (la sua immissione nel sistema) e la
terminazione dello stesso. Non è un parametro fisso, perché un processo eseguito n volte in diversi contesti
avrà diversi tempi di risposta. Per confrontare due processi bisogna confrontarli in condizioni simili. Non è
uguale al tempo di esecuzione (tempo effettivo di lavoro), il tempo di ricircolo tiene conto anche delle
pause (attesa di i/o, risorse, ecc.).

Tempo di attesa: È la differenza tra il tempo di ricircolo e il tempo di esecuzione. Il tempo che un processo
trascorre in attesa delle risorse a causa di conflitti con altri processi. Sostanzialmente valuta l’inefficienza
del sistema, se i processi hanno un tempo di attesa elevato vuol dire che il carico generale del SO è elevato.
Queste sono valutazioni che fa lo scheduling, ma non vuol dire che siano sempre corrette: per esempio, se
l’utente forza la priorità di un processo, potrebbe alterare il normale funzionamento dello scheduler.

Un buon algoritmo di scheduling cerca di bilanciare l’esecuzione dei processi al meglio, massimizzando l’uso
del processo e riducendo i tempi di attesa. Gli algoritmi di schedulazione possono essere divisi in due
categorie:

Un algoritmo Preemptive è interrompibile: La pianificazione preemptive è quella in cui i processi con


priorità più elevate vengono eseguiti per primi. Un processo in esecuzione può essere interrotto da un altro
processo a priorità più elevata.

▪ Vantaggio: Nessun processo può monopolizzare il processore.


▪ Svantaggio: Crea problemi dove vi sono processi che condividono dati.

Un algoritmo non Preemptive non è interrompibile: La pianificazione non preventiva consiste nel fatto che
una volta che il processo è stato assegnato alla CPU, rimane in esecuzione fino a quando non termina o
fallisce.

5.4.1 Algoritmo first come first Served - non Preemptive


L’algoritmo FCFS è un tipo di algoritmo che implementa una coda di tipo FIFO (first in first out) : esegue i
processi nello stesso ordine in cui essi vengono sottomessi al sistema.

▪ PRO: Semplicità nell’implementazione dell’algoritmo.


▪ CONTRO: Il problema di questa strategie consiste nel fatto che tende a favorire i processi CPU-bound a
discapito di quelli I/O bound. Se ci sono molti processi CPU-bound e pochi I/O bound, il SO andrà a
favorire i processi CPU-bound rallentando così i processi interattivi (I/O bound).

Di conseguenza non è una scelta conveniente per un sistema a singolo processore, ma è spesso combinato
con uno schema di priorità per fornire uno scheduler efficiente. Pertanto, lo scheduler può avere alcune
code, una per ogni livello di priorità, e distribuire i processi all’interno di ogni coda utilizzando il first-come-
first-served. Un esempio di tale sistema è l’algoritmo di scheduling a code multiple con feedback.

5.4.2 Algoritmo event Driven – Preemptive


L’algoritmo Event Driven ragiona secondo un valore di priorità assegnato a ciascun processo. Lo scheduler
sceglierà sempre il processo pronto con maggiore priorità (cpu bound). La priorità può essere assegnata
dall’utente o dal SO in base alle caratteristiche e il comportamento del processo.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 56 a 103

Esempio pratico: coda del pronto soccorso.

▪ PRO: Sistema preemptive.


▪ CONTRO: Non è in grado di garantire il completamento di un processo in un intervallo di tempo finito
dalla sua creazione, in quanto potrebbe essere continuamente sorpassato da processi a priorità alta.
Tale situazione prende il nome di starvation (morte di fame).

SOLUZIONE: Usare l’aging (invecchiamento), ovvero, al passare del tempo in coda ready, la priorità del
processo viene aumentata.

5.4.3 Algoritmo Round Robin – Preemptive


L’algoritmo RR utilizza un principio di time slice: Ogni processo ha a disposizione una piccola quantità di
tempo di CPU prefissata uguale per tutti. Se entro questo arco di tempo il processo non lascia la CPU, viene
interrotto e portato nella coda di ready, gestita in modo FIFO. Con il RR, la principale preoccupazione in
fase di progettazione è la lunghezza del tempo di slice da usare. Il comportamento del RR dipende molto
dal valore del quanto di tempo q scelto:

▪ Se q tende all’infinito rende RR uguale all’algoritmo FCFS.


▪ Se q tende a zero procedure un maggior effetto di parallelismo virtuale tra i processi, ma, aumenta il
numero di context switch, e quindi l’overhead (richieste superiori al necessario) consumando risorse.

La schedulazione Round Robin fornisce una buona condivisione delle risorse del sistema:

▪ I processi più brevi possono completare l’operazione in un q (buon tempo di risposta).


▪ I processi più lunghi sono forzati a passare più volte per la coda dei processi pronti (tempo
proporzionale alle loro richieste di risorse).

La realizzazione di uno scheduler RR richiede il supporto di un Timer che invia un’interruzione alla scadenza
di ogni q, forzando lo scheduler a sostituire il processo in esecuzione. Il timer viene azzerato se un processo
cede il controllo al SO prima della scadenza del suo q.

▪ PRO: Tutti i processi hanno un tempo di CPU uguale.


▪ CONTRO: Costanti context switch e di tempo di completamento di processi onerosi lunghi.

5.4.4 Algoritmo Highest response Ratio next – non Preemptive


L’algoritmo HRRN manda in esecuzione il processo con il più alto valore di Response ratio. Siano:
▪ W = Tempo speso in coda di ready.
▪ S = Tempo di servizio previsto.
𝑤+𝑠
Si definisce il response ratio: 𝑅𝑅 = 𝑠

Quando un processo entra in coda ready per la prima volta ha un 𝑅𝑅 = 1 (perché W = 0). Perciò la regola
di scheduling da seguire è: quando il processo in esecuzione termina o si blocca, si sceglie il processo Ready
con il più grande valore di RR. Questo algoritmo tiene in considerazione l’età del processo, quindi applica
un meccanismo di aging (W):

Mentre sono favoriti processi più brevi (un denominatore più piccolo conduce ad un maggior rapporto),
invecchiare senza essere serviti aumenta il rapporto, così un processo più lungo supererà prima o poi i
processi più brevi.

▪ PRO: Applica un meccanismo di aging.


▪ CONTRO: Carico eccessivo sul processore, in quanto ogni secondo la formula RR va aggiornata.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 57 a 103

5.4.5 Algoritmo Shortest Process Next – non Preemptive


Questa è una strategie senza prerilascio, nella quale si sceglie come successivo processo da eseguire il
processo con il minor tempo di esecuzione previsto. Perciò un processo breve balzerà in testa alla coda
sorpassando i job più lenti.

▪ PRO: Questo algoritmo è vantaggioso data la sua semplicità, perché riduce al minimo il tempo medio
che ogni processo deve attendere fino al completamento dell’esecuzione.
▪ CONTRO: I processi più lunghi rischiano la starvation se c’è un arrivo stabile di processi più brevi. Per
starvation si intende l’impossibilità perpetua, da parte di un processo ready, di ottenere le risorse per
essere eseguito. Un altro problema è dovuto dalla difficoltà nello stimare la durata della prossima
sequenza di CPU.

Questo algoritmo ha una versione preemptive: se arriva un nuovo processo, con un tempo di esecuzione
minore del tempo necessario per la conclusione dell’esecuzione del processo attualmente in esecuzione, si
ha il prerilascio della CPU a favore del processo appena arrivato. Questo schema è noto anche come
Shortest Remaining Time First.

5.4.6 Schedulazione a code multiple


La schedulazione degli algoritmi non è detto che abbia una coda unica in ogni caso. Possono essere definite
delle code differenti. La coda di ready può essere divisa in sotto-code:

▪ Coda Foreground (processi con interazione a video).


▪ Coda background (processi batch, non interazione a video).

Ogni coda può adottare un proprio algoritmo di schedulazione. Avendo più code di schedulazione ci sarà
bisogno di uno scheduling delle code:

▪ A priorità fissa e con prelazione (diritto di preferenza).


▪ Time slice: ad ogni coda è associato un certo ammontare di tempo di CPU.

Schedulazione a code multiple con feedback

La schedulazione avviene implementando un meccanismo di aging, cioè un processo può essere spostato
da una coda all’altra. Ci saranno delle code multilevel-feedback definite dai seguenti parametri:

▪ Numero di code.
▪ Algoritmo di scheduling per ogni coda.
▪ Metodi usati per l’up-grading e il down-grading di ogni processo.

Esempio: Sia una schedulazione multilevel feedback composta dalle seguenti code:

▪ Q0 – RR con time quantum 8ms


▪ Q1 – RR con time quantum 16ms
▪ Q2 – FCFS

Scheduling:

1) Quando entra un processo entra nella coda Q0 (FCFS).


2) Quando ottiene la CPU, la impegna per 8ms. Se non termina entro gli 8ms è spostato in Q1.
3) Il processo in Q1 viene nuovamente servito con la politica RR e riceve la CPU per ulteriori 16ms.
4) Se ancora non termina viene spostato in Q2 e servito con FCFS.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 58 a 103

6.0 Thread, SMP, Kernel

6.1 Thread
Un thread è una suddivisione di un processo in due o più filoni, o sottoprocessi,
che vengono eseguiti concorrentemente da un sistema di elaborazione.

Un processo può essere diviso in più Thread per:

▪ Ottenere un parallelismo dei flussi di esecuzione all’interno del processo.


▪ Gestire chiamate bloccanti.

Un thread è composto da 3 elementi:

1. Program Counter.
2. Valore dei registri (non condiviso tra i vari thread del processo).
3. Stack (blocco contiguo di memoria contenente dati).

Una differenza sostanziale fra thread e processi consiste nel modo con cui essi condividono le risorse:
mentre i processi sono di solito fra loro indipendenti, utilizzando diverse aree di memoria ed interagendo
soltanto mediante appositi meccanismi di comunicazione, al contrario, i thread di un processo tipicamente
condividono le medesime informazioni.

L’altra differenza sostanziale è insita nel meccanismo di attivazione: la creazione di un nuovo processo è
sempre onerosa per il sistema (context switch), in quanto devono essere allocate risorse necessarie alla sua
esecuzione; il thread invece è parte di un processo e quindi una sua nuova attivazione viene effettuata in
tempi minimi.

In un sistema che non supporta i thread, se si vuole eseguire contemporaneamente più volte lo stesso
programma, è necessario creare più processi basati sullo stesso programma. Tale tecnica funzione, ma è
dispendiosa di risorsa, sia perché bisogna effettuare tanti context switch, sia perché permettere la
comunicazione tra i vari processi è necessario eseguire delle lente chiamate di sistema (livello kernel).
Avendo più thread nello stesso processo si può ottenere lo stesso risultato allocando una sola volta le
risorse necessarie, e scambiando i dati fra i thread tramite la memoria del processo, che è accessibile a tutti
i suoi thread dello stesso processo.

Un thread viene chiamato anche Light weight Process, cioè processi a piccolo peso (perché non hanno
risorse proprie, utilizzano quelle del processo). Anche i thread hanno un’unità di esecuzione: hanno un
proprio stato, una propria priorità e devono essere schedulati. Tutte le informazioni relative al thread sono
contenute nel Thread Control Block.

Vantaggi:

▪ Condivisione delle risorse: i vari thread di un singolo processo condividono tutte le risorse del processo
▪ Reattività: Se il processo è diviso in più thread, se un thread completa la sua esecuzione, il suo output
può essere restituito immediatamente.
▪ Efficienza: Se abbiamo più thread in un singolo processo, possiamo programmare più thread su più
processori. Cioè rendere più veloce l’esecuzione del processo.
▪ Comunicazione: la comunicazione tra più thread è più semplice, poiché condividono uno spazio di
memoria comune.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 59 a 103

Svantaggi:

▪ La sospensione di un processo richiede che tutti i thread siano sospesi contemporaneamente, perché si
deve liberare spazio in memoria (tutti i thread utilizzano lo stesso spazio di memoria condiviso).
▪ La terminazione di un processo richiede che tutti i thread associati siano terminati.

6.1.2 Multithreading
Multithreading è la capacità di un SO di supportare più thread per ogni processo.

Nelle architetture a processore singolo, quando la CPU esegue alternativamente istruzioni di thread
differenti si parla di multithreading a divisione di tempo: la commutazione fra i thread avviene tanto
frequentemente da dare all’utente l’impressione che tutti i task siano eseguiti contemporaneamente.

Nelle architetture multiprocessore i thread vengono invece realmente eseguiti contemporaneamente, cioè
in parallelo, ciascuno su un distinto processore.

Alcuni esempi di multithreading:

▪ Esecuzione in Foreground e in background: in un foglio di calcolo, un thread, potrebbe gestire i menu e


leggere l’input dell’utente (Foreground), mentre un altro thread potrebbe eseguire i comandi
dell’utente e l’aggiornamento del foglio di calcolo (background).
▪ Elaborazione asincrona: Ad esempio, come protezione contro interruzioni di corrente elettrica, si può
progettare un elaboratore di testo in modo che scriva su disco ogni minuto il contenuto del buffer in
memoria. Per fare ciò, si può programmare un thread il cui unico compito sia il salvataggio periodico.
▪ Una remote procedure call (RPC) è la chiamata che consente da parte di un processo A (client) di
invocare una funzione di un processo B (server).

Il primo caso è quello di un architettura monothread. Se è monothread, mentre effettua la chiamata al


server, il processo 1 si blocca (perché deve aspettare l’esito dell’RPC)-
Il secondo caso è quello di un architettura multithreading. Se devo effettuare due chiamate RPC, posso
suddividerle tra i due thread del processo. Così facendo si dimezza il tempo di attesa.

6.1.3 Stati dei thread


Come per i processi, gli stati chiave di un thread sono Running, Ready e blocked. In generale non ha senso
associare stati Suspend ai thread, perché tali stati sono concettualmente operativi a livello di processi. In
particolare, se un processo viene scaricato dalla memoria centrale, lo stesso deve accadere per tutti i suoi
thread, perché condividono lo stesso spazio di indirizzamento.

Ci sono 4 operazioni di base associate ad un cambiamento dello stato di un thread:

1. Creazione: quando un processo viene creato, si crea anche un thread. Successivamente un thread può
creare un altro thread a cui deve fornire il puntatore delle istruzioni e gli argomenti. Il nuovo thread è
messo nella coda dei ready.
2. Blocco: quando un thread deve aspettare un particolare evento entra in stato di blocked.
Corso di studi in Informatica (A-L) A.A. 2021/2022
Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 60 a 103

3. Sblocco: quando si verifica l’evento per cui il processo era stato posto in stato blocked, il thread passa
allo stato di ready.
4. Terminazione: quando un thread completa il suo compito, vengono deallocati i registri e lo stack.

6.1.4 Categorie di thread


User Level thread (ULT)

I thread di tipo ULT si trovano nello spazio utente, progettato dallo sviluppatore dell’applicazione che
utilizza una libreria di thread per la gestione. Essendo thread creati da delle librerie, all’interno del
processo, il SO rileva i thread ULT come istruzioni di codice, quindi sono trasparente al kernel. Visto che il
kernel, può gestire un thread solo, il blocco di un thread di livello utente blocca l’intero processo, perché
per il kernel il thread è trasparente (i diversi thread del processo vengono visti dal kernel come un processo
intero).

Esempio: 3 thread ULT gestite dalla libreria: Il lavoro di gestione dei


thread è svolto dalla libreria utente. Infatti il kernel ignora l’esistenza
dei thread, ma li considera come codice di un processo. Si parla
quindi di modello molti a uno, cioè molti thread per un solo
processo. La libreria permette:

▪ Creazione dei thread.


▪ Comunicazione tra thread.
▪ Schedulazione.
▪ Salvataggio e caricamento dei contesti dei thread.

Tali attività sono svolte all’interno del singolo processo. Il kernel non è conscio di queste attività, e continua
a schedulare i processi come delle unità, assegnando ad ogni processo un singolo stato di esecuzione.

PRO:

▪ Ogni applicazione può utilizzare un proprio algoritmo di schedulazione in base alle proprie esigenze,
dunque, ottimizzando l’efficienza di esecuzione.
▪ Scheduling cooperativo: un thread decide di passare il controllo ad un altro. Non viene coinvolto lo
scheduler del kernel (niente context switch), ma si sviluppa tutto a livello di libreria.
▪ Risparmio di sovraccarico: il cambio thread non richiede privilegi in modalità kernel, poiché tutte le
attività avvengono nello spazio utente.
▪ Lavorando con thread a livello utente: si può implementare questa strategia su qualsiasi sistema
operativo perché non bisogna modificare il kernel. (basta implementare le librerie di gestione).

CONTRO:

▪ Supponiamo che un thread legga dalla tastiera prima che un qualsiasi tasto sia premuto; lasciare che il
thread esegua effettivamente la chiamata è inaccettabile, poiché bloccherebbe tutti i thread.
▪ Un’applicazione multithreading non può sfruttare il multiprocessing, infatti il kernel assegna un solo
processo ad un solo processore alla volta, quindi in un dato istante, un solo thread per processore è in
esecuzione.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 61 a 103

Soluzioni parziali:

▪ Jacketing: Esiste una chiamata di sistema select, che permette di dire al chiamante se una eventuale
read si bloccherà. Quando questa chiamata è presente, la procedura di libreria read può essere
sostituita con una nuova, che prima esegue una chiamata alla select e solo dopo esegue la chiamata
read se questa è sicura (cioè non si bloccherà). Se la read si bloccasse, la chiamata non verrebbe
eseguita, ma verrebbe mandato in esecuzione un altro thread.
▪ Sviluppo dell’applicazione a livello di processo (addio vantaggi dei thread).

Kernel Level thread (KLT)

In caso di KLT, tutto il lavoro di gestione dei thread viene effettuato dal kernel. È il kernel che si occupa
della creazione, scheduling e gestione dei thread. I processi KLT possono essere eseguiti su diversi
processori, ottenendo parallelismo. Lo svantaggio è che sono più lenti da gestire rispetto ai thread ULT,
perché:

▪ ULT è codice (trasparente al kernel).


▪ KLT richiedono l’intervento del SO.

Il lavoro di gestione dei thread è svolto dal kernel: modello uno a uno, cioè
per ogni thread utente c’è un thread esistente nello spazio kernel. A livello
utente una chiamata API consente l’accesso alla parte del kernel che gestisce
i thread. Mentre il kernel mantiene info:

▪ Contesto del processo.


▪ Contesto dei threads.
▪ Scambio dei messaggi tra threads.

PRO:

▪ Se un thread effettua una chiamata bloccante, non blocca gli altri thread.
▪ Thread di uno stesso processo possono essere schedulati su diversi processi.

CONTRO:

▪ I thread sono gestiti dallo scheduler dei processi del kernel (context switch) .
▪ Overhead: trasferimento del controllo da un thread ad un altro richiede l’intervento del kernel e quindi
una perdita di tempo.

Approcci misti

In un sistema misto, la creazione dei thread è effettuata completamente nello spazio utente (attraverso
libreria) e lo stesso accade per la schedulazione. Nella zona kernel esisteranno altri thread che sono in
stretta corrispondenza dei thread a livello utente.

Vari thread di uno stesso processo possono essere eseguiti contemporaneamente su più processori, con un
livello di parallelismo pari al numero di thread livello kernel presenti.

CONTRO: necessità di comunicazione fra kernel e libreria di thread per mantenere un appropriato numero
di KLT allocati all’applicazione. Esiste quindi un light weight process (LWP), cioè una struttura intermedia
che appare alla libreria di ULT come un processore virtuale sul quale schedulare l’esecuzione.

Esempi: un applicazione CPU-bound su un sistema monoprocessore implica che un solo thread per volta
possa essere eseguito; quindi, per essa sarà sufficiente un unico LWP per thread. Nel caso di applicazioni
I/O bound tipicamente richiederebbe un LWP per ciascuna chiamata di sistema bloccante.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 62 a 103

6.2 Symmetric Multi Processing (SMP)


Con SMP, ovvero sistema multiprocessore simmetrico, ci si riferisce all’architettura di un computer in cui
più processori sono interconnessi a un’unica memoria principale condivisa, con piena accessibilità a tutti i
dispositivi I/O. Un sistema SMP gestisce i processori e le altre risorse in maniera tale, che alla visione
dell’utente, risulti un sistema monoprocessore con multiprogrammazione. Infatti, un utente può realizzare
applicazione che fanno uso di processori o thread multipli senza preoccuparsi del numero di processori
disponibili. Nei calcolatori SMP:

▪ I processori condividono le stesse risorse.


▪ Tutti i processori possono effettuare le stesse funzioni.
▪ Ogni funzione gestisce la schedulazione dei processi o thread disponibili .

Le difficoltà di un sistema SMP sono:

▪ I processi non devono schedulare lo stesso processo (tranne nel caso di KLT), perché altrimenti si
verificherebbero delle problematiche.
▪ C’è bisogno di mettere in comunicazione i vari processori.

Punti critici della progettazione di un sistema operativo per SMP sono:

▪ Concorrenza tra processi e thread del kernel: l’esecuzione contemporanea su diversi processori non
deve compromettere le strutture di gestione del SO ed evitare gli stalli.
▪ Schedulazione: può essere effettuata da qualunque processore. Se viene usato multithreading a livello
kernel, è possibile schedulare più thread dello stesso processo simultaneamente su più processori.
▪ Sincronizzazione, poiché vari processi attivi possono accedere allo spazio di memoria condiviso, diventa
fondamentale realizzare la sincronizzazione grazie alla quale si ottiene la mutua esclusione.
▪ Tolleranza ai guasti: il SO deve permettere un decadimento graduale in caso di fallimento dei
processori. Lo schedulatore deve riconoscere la perdita di un processore e ricostruire le tabelle di
gestione.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 63 a 103

6.3 Kernel e MicroKernel


Il kernel è un software che garantisce un accesso sicuro e controllato all’hardware. Un kernel non è
strettamente necessario per far funzionare un computer: I programmi possono essere infatti direttamente
caricati ed eseguiti dalla macchina, a patto che i loro sviluppatori ritengano necessario fare a meno del
supporto del sistema operativo. Il kernel è responsabile:

▪ Gestione dei processi.


▪ Gestione e allocazione della memoria.
▪ Gestione della comunicazione input e output.
▪ Gestione delle componenti hardware per mezzo dei driver.

L’accesso diretto al kernel da parte di un utente può avvenire in


modalità utente o modalità kernel :

▪ Modalità utente: stato caratterizzato da un numero relativamente basso di privilegi verso la memoria,
l’hardware e altre risorse.
▪ Modalità kernel: stato di privilegi massimo. Il codice eseguito in tale modalità ha accesso illimitato alla
memoria, hardware e altre risorse.

Il kernel può essere organizzato in un insieme di moduli, uno per ciascuna delle funzioni da svolgere,
strutturandoli in vari livelli. Ciascun modulo di un livello utilizza le funzionalità offerte dai moduli di livello
sottostante e fornisce a sua volta servizi ai moduli del livello superiore. La comunicazione tra i vari moduli
avviene attraverso le chiamate di sistema. Un modulo di un livello superiore richiede i servizi o le risorse
del modulo inferiore. Questa stratificazione porta ad un funzionamento meno efficiente in termini di
velocità di esecuzione. Ad esempio per esecuzione un operazione, un programma, potrebbe effettuare una
chiamata di sistema al livello sottostante, la quale, a sua volta, ne richiama un’altra, e questa un’altra
ancora, e così via. In altre parole, il programma applicativo, per ottenere un servizio, potrebbe attendere
l’esecuzione di N funzioni di sistema.

CONTRO:

▪ Difficoltà nell’utilizzare nuove periferiche: Se si volesse utilizzare, ad esempio, uno scanner e se nella
progettazione del kernel non fosse stato inserito il modulo adatto, questa periferica non riuscirebbe ad
interfacciarsi con le componenti del computer e del SO. L’utente sarebbe costretto a modificare il
kernel.
▪ Essendo una struttura, i cui moduli sono strettamente collegati l’uno all’altro, un bug in una qualsiasi
delle parti che lo compongono potrebbe portare ad un malfunzionamento generale.

MicroKernel

Mentre un kernel contiene tutti i moduli necessari al


funzionamento del computer, un microkernel implementa
solamente alcune funzioni basilari. La filosofia del
microkernel è che solo le funzioni assolutamente essenziali
del nucleo del SO dovrebbero essere nel kernel; i servizi
meno essenziali e le applicazioni sono costruiti sopra al
microkernel e vengono eseguite in modalità utente.

Sopra tale kernel minimalista vengono innestati dei server, ovvero programmi (servizi esterni al
microkernel) separati dal microkernel che comunicano con questo attraverso le chiamate di sistema (IPC,
Inter Process comunication). Ogni server può accedere alle risorse e alle funzionalità di un altro server solo

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 64 a 103

attraverso uno scambio di messaggi filtrati dal microkernel. Così il microkernel gestisce lo scambio di
messaggi; valida i messaggi, li passa fra i componenti esterni e concede i permessi di accesso all’hardware.

Per esempio, se un’applicazione vuole aprire un file, manda un messaggio al server del file system; se deve
stampare un file, manda un messaggio al server della stampante. I vari server possono scambiarsi messaggi
e chiamare le funzioni primitive del microkernel: ciò permette di costruire un’architettura client/server
all’interno di un singolo computer.

Differenza Kernel e MicroKernel

PRO MicroKernel:

▪ Interfaccia uniforme: I processi non devono fare distinzione fra servizi a livello di kernel e a livello
utente, perché ogni servizio è fornito tramite passaggio di messaggi.
▪ Estensibilità: l’introduzione di nuovi servizi o modifiche non richiedono modifiche al kernel.
▪ Flessibilità: a seconda delle applicazioni, certe caratteristiche possono essere ridotte o potenziate per
soddisfare al meglio le richieste dei clienti. Per esempio Windows home – pro – ultimate.
▪ Affidabilità: lo sviluppo di piccole porzioni di codice ne permette una migliore ottimizzazione e test.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 65 a 103

7.0 Concorrenza
La concorrenza è una caratteristica dei sistemi di elaborazione nei quali può verificarsi che un insieme di
processi, o thread, siano in esecuzione nello stesso istante. La concorrenza è l’insieme delle tecniche per
risolvere i problemi associati all’esecuzione concorrente.

In un sistema a singolo processore con multiprogrammazione, i processi sono alternati nel tempo per dare
l’illusione dell’esecuzione simultanea. In un sistema multiprocessore è possibile non solo alternare
l’esecuzione dei processi ma anche sovrapporle. L’alternanza o la sovrapposizione dei processi presentano
alcune difficoltà:

▪ La condivisione di risorse globali è pericolosa: Ad esempio, se due processi fanno entrambi uso della
stessa variabile globale ed effettuano letture e scritture su quella variabile, allora l’ordine in cui le
operazioni di lettura e scrittura sono eseguite è critico.
▪ Trovare un errore di programmazione diventa molto difficile, perché i risultati tipicamente non sono
riproducibili.

Caso architettura monoprocessore:

Si consideri la seguente procedura:

Questa procedura mostra gli elementi essenziali di un programma che


realizza l’echo, in altre parole copia l’input nell’output (l’input è letto
da tastiera, un tasto alla volta). Ogni programma può chiamare questa
procedura ripetutamente per leggere l’input dell’utente e visualizzarlo
a schermo. Ogni applicazione usa la tastiera per l’input e lo stesso
schermo per l’output. Poiché ogni applicazione deve usare la procedura
echo, ha senso che tale procedura sia condivisa e sia caricata in una
porzione di memoria globale, accessibile da tutte le applicazioni.

Questa condivisione può creare dei problemi; Consideriamo la seguente sequenza:

1. Il processo P1 chiama la procedura echo ed è interrotto subito dopo la conclusione della lettura del
primo input. A questo punto l’ultimo carattere digitato (x) è memorizzato nella variabile 𝑖𝑛 (si ferma
allo scanf).
2. Il processo P2 viene attivato e chiama la procedura echo, che è eseguita fino alla fine. A questo punto
l’ultimo carattere digitato (y) è memorizzato nella variabile 𝑖𝑛 e stampato a schermo.
3. Il processo P1 viene riattivato (riprende l’esecuzione da dove era rimasto, quindi dopo scanf), e a
questo punto la variabile 𝑖𝑛 è stata sovrascritta e il valore x è andato perso, sostituito da (y), che è
copiato in out e stampato a schermo.

Così il primo carattere è perso e il secondo stampato due volte. In sintesi: Tutti i processi che hanno accesso
a una variabile condivisa: se un processo la sovrascrive e poi viene interrotto, un altro processo ne può
alterare il valore prime che il primo processo possa riutilizzarlo.

Soluzione: Bisogna imporre una regola secondo il quale un solo processo alla volta può eseguire la
procedura, e che una volta in esecuzione è necessario concluderla prima di dare accesso a un altro
processo.

Con architettura multiprocessore

In un sistema multiprocessore si verifica lo stesso problema di protezione delle risorse condivise e valgono
le stesse soluzioni.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 66 a 103

Esempio: I processi P1 e P2 sono


entrambi in esecuzione, ciascuno su
un processore diverso, ed entrambi
chiamano la procedura echo. Di
conseguenza l’ultimo valore letto
da tastiera sarà l’output dei due
processi:

In questo caso il risultato è che il


carattere letto da P1 è perso prima di essere visualizzato (perché l’input di P2 viene chiamato dopo di P1,
quindi si perde il valore 𝑖𝑛 letto da P1), e il carattere letto da P2 è stampato sia da P1 che da P2.

Soluzione: la soluzione è la stessa di quella utilizzata da un’architettura multiprocessore.

7.1 Mutua esclusione


Definizione sezione critica: Quando due o più processi sono eseguiti in concorrenza per una determinata
risorsa, può essere necessario eseguire parte del codice del processo in modo mutuamente esclusivo.
Quando processi diversi accedono a una risorsa comune in tempo diversi, ognuno di essi può completare la
propria operazione senza essere disturbato dagli altri. Se questa condizione vale sempre per una certa
risorsa, allora si dice che l’operazione è atomica: ogni processo può intervenire solo prima o dopo che
l’altro abbia completato la propria sezione critica (esempio: test&set).

I processi concorrenti entrano in conflitto quando competono per l’uso della stessa risorsa. Fra i processi in
competizione non c’è scambio di informazioni; quindi, l’esecuzione di un processo può influenzare il
comportamento degli altri. Bisogna distinguere tre problemi dovuti alla competizione fra processi:

1. Mutua esclusione: Un solo programma alla volta può entrare in sezione critica.

Una volta che si è garantita la mutua esclusione nascono altri due problemi:

2. Deadlock (Stallo): Se due o più processi sono in attesa di un evento che può essere determinato solo da
uno dei processi in attesa, allora sono in stallo.
3. Starvation (morte per fame): Situazione nella quale un processo non riceve mai l’utilizzo di una risorsa
e viene costantemente scavalcato da altri processi.

Soluzione concorrenza: il controllo della competizione coinvolge il SO che adotta dei sistemi per applicare i
principi della mutua esclusione.

Ogni meccanismo per fornire il supporto alla mutua esclusione deve avere questi requisiti:

1. Un solo processo alla volta deve accedere alla sezione critica.


2. Un processo che si ferma mentre è fuori dalla sezione critica non deve interferire con gli altri processi.
3. Non deve essere possibile che un processo che chiede l’accesso alla sezione critica sia fatto aspettare
indefinitamente: non si devono verificare stalli o starvation.
4. Quando nessun processo è nella sezione critica, a ogni processo deve essere concesso di entrare nella
sua sezione critica senza attese.
5. Non ci devono essere supposizioni sulla velocità di esecuzione dei processi. Se il SO inizia ad effettuare
supposizioni di questo tipo potrebbe verificarsi una race condition: Situazione nella quale thread o
processi leggono e scrivono un dato condiviso e il risultato dipende dalla loro velocità. Un esempio è la
procedura echo in ambienti multiprocessori (analizzata prima).
6. Ogni processo può rimanere nella sua sezione critica solo per un tempo finito.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 67 a 103

Ci sono vari modi per soddisfare i requisiti della mutua esclusione:

1. Approccio software: Ovvero scaricare la responsabilità ai processi che sono eseguiti concorrentemente.
In tal modo i processi dovrebbero coordinarsi fra loro per garantire la mutua esclusione.
2. Approccio hardware: Utilizzare delle istruzioni macchina particolari.
3. Utilizzare il supporto del SO: Utilizzare il SO o il linguaggio di programmazione.

7.1.1 Approccio software: Algoritmo di dekker


Nel caso di processi concorrenti si possono adottare approcci software per garantire la mutua esclusione.
L’algoritmo di dekker costituisce una soluzione completa al problema della mutua esclusione, impedendo lo
stallo ed assicurando che soltanto un processo alla volta possa eseguire una sezione critica. Di seguito
analizziamo diversi tentativi di applicazione dell’algoritmo:

Primo tentativo

Qualunque approccio alla mutua esclusione deve basarsi su qualche meccanismo fondamentale di
esclusione a livello hardware: il più comune è il vincolo di effettuare un solo accesso alla volta ad una
locazione di memoria. Possiamo usare il protocollo dell’iglù come metafora di tale arbitraggio:

L’entrata e l’iglù stesso sono talmente piccoli che una sola


persona alla volta può entrare all’interno, dove c’è una lavagna
su cui si può scrivere un unico valore (turno). Un processo che
vuole eseguire la sua sezione critica entra nell’iglù e guarda la
lavagna: se c’è scritto il suo numero può uscire dall’iglù ed
eseguire la sua sezione critica; altrimenti, esce dall’iglù e aspetta.
Di tanto in tanto il processo entra nuovamente nell’iglù per
controllare la lavagna, finché non arriva il momento in cui può
entrare nella sua sezione critica.

Questo fenomeno è detto attesa attiva, perché il processo non


può fare niente di produttivo finché non ha il permesso di entrare
nella sua sezione critica; invece deve periodicamente controllare
l’iglù, e perciò consuma tempo di elaborazione, rimanendo attivo,
mentre aspetta il proprio turno.

Dopo che un processo ha avuta accesso e dopo aver completato la sezione critica, deve ritornare nell’iglù e
scrivere sulla lavagna il numero del processo successivo che può entrare in sezione critica.

Questa soluzione garantisce la proprietà di mutua esclusione ma ha due punti deboli:

1. La velocità di esecuzione è data dal processo più lento: Se P0 usa la sua sezione critica solamente una
volta all’ora, ma P1 vorrebbe usare la propria 1000 volte all’ora, P1 è forzato a mantenere il ritmo di P0.
2. Se un processo fallisce, l’altro rimane bloccato (wait) per sempre.

Secondo tentativo

Il problema del primo tentativo è che si scrive il nome del processo che può entrare nella sua sezione
critica, quando in realtà è utile avere anche informazioni sullo stato di entrambi i processi: Ogni processo ha
la propria chiave (iglù) della sezione critica, così che se un processo fallisce, l’altro può ancora accedere alla
propria sezione critica. Sempre utilizzando la metafora dell’iglù:

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 68 a 103

Ogni processo ora ha il suo iglù, e può guardare la lavagna dell’altro senza modificarla. Quando un processo
desidera entrare nella propria sezione critica, controlla periodicamente la lavagna dell’altro finché non vede
che c’è scritto “falso”, il che indica che l’altro processo non è nella propria sezione critica; allora può
scrivere “vero” sulla sua lavagna, ed entrare nella sezione critica. Quando lascia la sezione critica scrive
“falso” sulla propria lavagna.

In questo caso la variabile globale condivisa è: boolean flag[0...1]


ed è inizializzata a falso. Se un processo fallisce all’esterno della sua
sezione critica, compreso il codice per modificare il flag, allora
l’altro processo non è bloccato (flag=0). Se invece fallisce
all’interno della propria sezione critica, o dopo aver messo il flag a
vero prima di entrare, allora l’altro processo è bloccato per sempre.

Questa soluzione non garantisce neppure la mutua esclusione. Si


consideri la seguente sequenza:

1. P0 esegue il comando while e trova flag[1]=falso.


2. P1 esegue il comando while e trova flag[0]=falso.
3. P0 scrive in flag[0] ed entra in sezione critica.
4. P1 scrive in flag[1] ed entra in sezione critica.

Se entrambi i processi vedono il flag dell’altro false, mettono il loro


flag a true, entrambi vanno nella sezione critica, senza mutua
esclusione. Causa: soluzione analizzata non è indipendente dalla
velocità di esecuzione dei processi.

Terzo tentativo

Il secondo tentativo è fallito perché un processo può cambiare il proprio stato dopo che l’altro processo lo
ha letto ma prima che l’altro processo sia entrato nella sezione critica. Soluzione proposta:

Se un processo fallisce all’interno della propria sezione critica, compreso il codice per modificare il flag,
allora l’altro processo è bloccato, e se un processo fallisce all’esterno della propria sezione critica, l’altro
processo non è bloccato. Dal punto di vista di P0: Una volta che P0 ha messo flag[0] a vero, P1 non può
entrare nella sua sezione critica finché P0 non è uscito dalla propria sezione critica. Quando P0 modifica il
flag, è possibile che P1 sia già nella propria sezione critica, e in quel caso P0 sarà bloccato sul comando
while finché P1 non esce dalla sezione critica. Lo stesso ragionamento si applica dal punto di vista di P1.

Questo garantisce la mutua esclusione ma crea un nuovo problema: se entrambi i processi mettono i propri
flag a vero quando nessuno dei due ha ancora eseguito il comando while, allora ognuno penserà che l’altro
sia entrato nella propria sezione critica, causando uno stallo.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 69 a 103

Quarto tentativo

Nel terzo tentativo un processo modifica il proprio flag senza conoscere lo stato dell’altro processo. La
soluzione è che ciascun processo mette il flag a vero per indicare il desiderio di entrare nella propria
sezione critica, ma è pronto a rimettere il flag a falso per lasciare spazio all’altro processo.

La mutua esclusione è ancora


garantita, ma si consideri questa
sequenza:

1. P0 scrive flag[0] = vero


2. P1 scrive flag[1] = vero
3. P0 controlla flag[1]
4. P1 controlla flag[0]
5. P0 scrive flag[0] = falso
6. P1 scrive flag[1] = falso
7. P0 scrive flag[0] = vero
8. P1 scrive flag[1] = vero

Se questa sequenza si ripetesse indefinitamente, nessun processo potrebbe entrare nella propria sezione
critica. Non si entra in stallo, perché qualunque modifica delle velocità dei due processi romperebbe il ciclo
e uno dei due riuscirebbe ad entrare nella sezione critica. Inoltre se un processo fallisce entro la sua sezione
critica l’altro è bloccato per sempre.

Soluzione corretta

Si è visto che bisogna osservare lo stato di entrambi i processi, tramite


l’array flag, ma come mostrato nel quarto tentativo, ciò non è
sufficiente. Per evitare il problema è necessario imporre un ordine
alle attività dei due processi. Per fare ciò si può usare la variabile
turno del primo tentativo; in questo caso la variabile indica quale
processo ha il diritto di insistere nel tentativo di entrare nella propria
sezione critica.

Questa soluzione può essere descritta in termini di iglù: Ora c’è un


iglù “arbitro” con una lavagna chiamata turno. Quando P0 vuole
entrare nella sezione critica, mette il proprio flag a vero, e poi va a
controllare il flag di P1: se è falso, P0 può entrare nella sezione critica.
Altrimenti P0 va a controllare l’arbitro: se turno=0 sa che è il suo
turno di insistere, e controlla periodicamente l’iglù di P1. P1 ad un
certo punto noterà che è il suo turno di rinunciare, e scriverà falso
sulla propria lavagna, permettendo a P0 di entrare. Dopo avere usato
la propria sezione critica, P0 mette il proprio flag a falso per liberare la
sezione critica, e mette turno a 1 per trasferire a P1 il diritti di
insistere.

Questa soluzione garantisce la mutua esclusione, ma ha dei problemi:

▪ È un algoritmo complesso da implementare.


▪ Un processo controlla continuamente il flag dell’altro se pari a
vero (attesa attiva).
▪ Se un processo fallisce nella propria sezione critica, l’altro rimane bloccato per sempre.
Corso di studi in Informatica (A-L) A.A. 2021/2022
Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 70 a 103

7.1.2 Approccio hardware: test&set/swap


1) Disabilitare le interruzioni

In una macchina a singolo processore, i processi concorrenti non


possono sovrapporsi: possono solamente alternarsi; pertanto, un
processo continua l’esecuzione fin a quando non chiama un servizio
del SO o viene interrotto. Quindi per garantire la mutua esclusione è
sufficiente evitare che un processo venga interrotto: Questo può
essere realizzato con apposite primitive del kernel per abilitare e
disabilitare gli interrupt (interruzioni):

Poiché la sezione critica non può essere mai interrotta, la mutua esclusione è garantita. Ma l’efficienza può
peggiorare perché il processore non può alternare i programmi liberamente (interrupt disabilitati). Non si
può applicare per sistemi multiprocessore poiché richiederebbe un enorme spreco di tempo necessario a
notificare la disabilitazione delle interruzione in tutte le unità di elaborazione.

2) Istruzioni macchina speciali

A livello hardware, l’accesso a una locazione di memoria esclude qualunque altro accesso alla stessa
locazione. Basandosi su questo principio, è possibile eseguire istruzioni macchina che permettono di
controllare e modificare il contenuto di una parola di memoria (test&set), oppure scambiare il contenuto di
due parole (swap) in modo atomico (non interrompibile).

Test and set

Si può definire l’istruzione test&set in questo modo: La


funzione riceve in input un valore per indirizzo (target)
e restituisce in output il valore originale (val) con target
impostato a true.

Sia lock una variabile globale condivisa tra i processi


inizializzata a false. Il primo processo che accede
all’interno del primo while potrà entrare in sezione
critica:

FUNZIONAMENTO: Processo P0 (lock=false), entra nel


secondo while, la funzione test_and_set restituirà false
(val) e lock impostato a true. Così facendo P0 esce dal
while, perché la condizione non è rispettata (val deve
essere true per rimanere nel ciclo while) ed entra in
sezione critica. P1 tenta di entrare in sezione critica,
entra nel secondo while e la funzione test_and_set
restituirà true (val) e lock impostato a true. Così
facendo P1, rimarrà nel while in attesa attiva, fino a
quando il processo in sezione critica non termina.
Quando P0 termina, imposterà lock false. P1 chiamerà il test_and_set che restituirà false e lock a true,
permettendo a P1 di entrare in sezione critica.

Questa implementazione garantisce la mutua esclusione, inoltre se due processi contemporaneamente


chiamano il test&set, il processore li serializza (esegue le due richieste in successione). D’altra parte, se un
processo viene attivato e un altro processo è già nella sua sezione critica, il processo appena attivato
rimarrà in attesa attiva finché l’altro processo non termina la sua sezione critica.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 71 a 103

Swap

Si può definire l’istruzione swap in questo modo: La


funzione riceve in input due valori per indirizzo e
restituisce in output i due valori scambiati. Per
esempio: se a=false e b=true, in output si avrà a=true e
b=false.

Sia lock una variabile globale condivisa tra i processi e


inizializzata a false. Il primo processo che accede
all’interno del primo while potrà entrare in sezione
critica. Sia key la volontà del processi di entrare in
sezione critica.

FUNZIONAMENTO: Processo P0 (lock=false), entra nel


secondo while, la funzione swap restituirà lock=true e
key=false. Così facendo P0 esce dal while, perché la
condizione non è rispettata (key deve essere true) ed entra in sezione critica. P1 tenta di entrare in sezione
critica, entra nel secondo while e la funzione swap restituirà lock=true e key=true. Così facendo P1, rimarrà
nel while in attesa attiva, fino a quando il processo in sezione critica non termina. Quando P0 termina,
imposterà lock a false. P1 chiamerà swap che restituirà lock=true e key=false, permettendo a P1 di entrare
in sezione critica.

Proprietà dell’approccio con soluzioni hardware

L’uso di un’istruzione macchina speciale per garantire la mutua esclusione ha vari vantaggi:

▪ Si può applicare a qualunque numero di processi, su un singolo processore o su un multiprocessore con


memoria condivisa.
▪ Si può usare per gestire più di una sezione critica: ciascuna sezione avrà una propria variabile lock.

Ci sono però degli svantaggi:

▪ Bisogna usare ancora la tecnica dell’attesa attiva, quindi mentre un processo aspetta di avere accesso
alla sezione critica, usa il processore.
▪ È possibile che si verifichi starvation: quando un processo abbandona la sezione critica e ci sono vari
processi in attesa, la scelta del processo da attivare è arbitraria (casuale); Il processo in attesa deve
avere una garanzia di accedere alla risorsa richiesta in un tempo ragionevole, ad esempio utilizzando
delle code a priorità o a tempo. Senza questi meccanismi, è possibile che un processo debba aspettare
per un tempo illimitato.
▪ È possibile che si verifichi uno stallo: si consideri il caso di un singolo processore, in cui un processo P1
esegue l’istruzione speciale (test&set o swap) ed entra nella sezione critica; dopo di che viene
interrotto per concedere il processore a P2, che ha una priorità più alta. Se P2 tenta di accedere alla
risorsa di P1, riceverà un rifiuto a causa del meccanismo di mutua esclusione, così entrerà in un ciclo di
attesa attiva. Comunque P1 non verrà mai attivato perché la sua priorità è più bassa di quella di P2, che
è attivo.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 72 a 103

7.1.3 Utilizzare il supporto del SO: semafori/monitor


Il SO e i linguaggi di programmazione offrono due soluzioni per gestire la concorrenza: semafori e monitor.

1) Semafori
Un semaforo può essere definito come una variabile condivisa tra i processi o tipo di dato astratto che
serve per realizzare la sincronizzazione tra processi. Un semaforo è costituito da due parti:

1. Una variabile contatore, inizializzata a 1, che può assumere valori positivi o negativi.
2. Coda di processi associati al semaforo che sono in attesa di entrare in sezione critica.

Il valore del contatore indica “occupato” o “libero”:

▪ Se il valore è negativo, il semaforo indica occupato e il


valore assoluto indica il numero di processi in attesa. Ad
esempio, se il valore del contatore è -3, vuol dire che ci
sono 3 processi in attesa di entrare in sezione critica per
una determinata risorsa.
▪ Se il valore è 0, il semaforo indica occupato ma non ci
sono processi in attesa. Ad esempio, è come se ci
trovassimo ad un incrocio stradale, il semaforo è rosso ma
non ci sono macchine che aspettano di passare.
▪ Se il valore è positivo, il semaforo indica libero. Il valore
del contatore indica quanti sono i processi che possono
entrare in sezione critica in quel determinato momento.

Si possono distinguere tra due diversi tipi di semafori:


1. Semafori binari: può assumere un valore 0 o 1, utilizzato per gestire una risorsa per tipologia.
2. Semaforo contatore: può assumere qualsiasi valore ed è utilizzato per gestire più risorse per tipologia.

Per operare sul semaforo contatore, il programmatore


ha a disposizione 2 procedure primitive (livello kernel):

1. L’operazione wait decrementa il valore del


semaforo; se il valore diventa negativo, il processo
che esegue la wait viene bloccato.
2. L’operazione signal incrementa il valore del
semaforo; se il valore non è positivo allora uno dei
processi bloccati sull’operazione wait viene
riattivato.

Le operazioni wait/signal sul semaforo binario si


semplificano, poiché la variabile del contatore potrà
assumere solo valori 0 o 1. Con semaforo binario:

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 73 a 103

Entrambe le procedure devono essere realizzate in modo atomico. Se un processo sta eseguendo
l’operazione wait/signal su un determinato semaforo S, finché l’operazione stessa non è terminata, nessun
altro processo può eseguire wait/signal sullo stesso semaforo. Si può rendere wait/signal atomiche nei
seguenti modi:

▪ Se il sistema è monoprocessore basta disabilitare gli interrupt durante le operazioni.


▪ Algoritmo di dekker : sovraccarico.
▪ Utilizzando l’istruzione test&set : attesa attiva .
▪ Implementare l’atomicità a livello hardware o firmware.

Un semaforo si utilizza con il contatore inizializzato a 1 (semaforo libero). Prima di ogni sezione critica
bisogna chiamare wait e dopo la fine della sezione critica bisogna invocare signal.

FUNZIONAMENTO: Innanzitutto, in una sezione


esterna al programma concorrente, bisogna dichiarare
il semaforo, con il contatore inizializzato a 1.
All’interno del programma concorrente si avrà una
parte di codice eseguita senza mutua esclusione e
almeno una sezione critica.

A un certo punto il processo 1 (o 2) esegue la wait:

▪ Il valore del contatore viene decrementato: contatore = 0.


▪ La condizione dell’if non è rispettata, quindi la wait termina e P1 entra in sezione critica.

A un certo punto il processo 2 (o 1) esegue la wait, mentre il primo processo è ancora in sezione critica:

▪ Il valore del contatore viene decrementato: contatore = −1.


▪ Viene eseguito il test (if) sul contatore, che risulta positivo . Quindi il processo che sta eseguendo la
wait viene inserito nella coda del semaforo e sospeso.

Questo accadrà per ogni programma che dovesse provare ad entrare in sezione critica. Prima o poi il
processo 1 (o 2) terminerà il codice della sezione critica ed eseguirà la signal:

▪ Il valore del contatore viene incrementato: contatore = 0.


▪ Viene eseguito il test sul contatore, che risulta positivo; quindi, verrà estratto un processo dalla coda
del semaforo (es: first-in-first-out). Il processo 2 (o 1) estratto verrà riattivato; quindi, passerà da
blocked a ready ed entrerà in sezione critica.

Quando anche il processo 2 (o 1) terminerà la sezione critica eseguirà la signal, portando a 1 il contatore del
semaforo.

Vantaggi del semaforo:

▪ Attesa attiva limitata: Con i semafori si limita l’attesa attiva solo sulle operazioni wait e signal, che sono
molto brevi. Non si ha attesa attiva durante l’esecuzione di una sezione critica, poiché i processi in
attesa vengono sospesi (blocked) a differenza delle soluzioni test&set/swap dove i processi eseguono
un while potenzialmente infinito.
▪ Il comportamento del semaforo è equo: i processi che entrano in attesa verranno estratti e riattivati
secondo ordini precisi (per esempio first-in-first-out: processo che è stato sospeso più a lungo viene
riattivato per primo).

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 74 a 103

Svantaggi del semaforo:

▪ Possibili stalli: Siano S e Q due semafori inizializzati a 1. P1 non


rilascia Q fino a quando non ottiene S. P0 non rilascia S fino a
quando non ottiene Q. Di conseguenza le signal non saranno mai
eseguite e i due processi entreranno in stallo.
▪ Starvation: Se il semaforo usa un algoritmo di gestione della coda
di tipo LIFO (last-in-first-out, l’ultimo che fa il wait è il primo che
riceve la risorsa) ci potrebbero essere processi che non usciranno
mai dalla coda.

Codice semaforo contatore con test&set

Problema del produttore/consumatore

Il problema del produttore-consumatore è un esempio classico di sincronizzazione tra processi. Il problema


descrive due processi, uno produttore ed uno consumatore, che condividono un buffer comune. Compito
del produttore è generare dati e depositarli nel buffer in continuo. Contemporaneamente, il consumatore
utilizzerà i dati prodotti, rimuovendoli di volta in volta dal buffer.

Il problema è impedire la sovrapposizione di operazioni nel buffer, cioè un solo agente (produttore o
consumatore) alla volta può accedere al buffer. Quindi, si supponga che il buffer sia infinito e si componga
di un array di elementi. Le funzioni produttori e consumatore sono le seguenti:

Il produttore può generare elementi e aggiungerli al buffer senza vincoli di velocità; ad ogni istante è
incrementato un indice (in) nel buffer. Il consumatore deve prestare attenzione a non tentare di leggere il
buffer quando è vuoto (in>out).

Soluzione semaforo binario: Si usano due semafori:

1. s : per garantire la mutua esclusione


2. ritardo : per forzare il consumatore
ad aspettare se il buffer è pieno.

Il produttore effettua il wait sul


semaforo S prima di aggiungere un
elemento nel buffer e signal dopo, per
impedire un eventuale accesso del
consumatore. Inoltre, nella sezione
critica, il produttore incrementa il valore
di n; se 𝑛 = 1 allora il buffer era vuoto
prima dell’aggiunta, così il produttore

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 75 a 103

effettua il signal su ritardo per avvisare il consumatore. Il consumatore inizia facendo la wait su ritardo, in
modo da aspettare che il primo elemento sia prodotto; in seguito, prende un elemento e decrementa n
all’interno della sezione critica.

Se il produttore è più veloce del consumatore, quest’ultimo si bloccherà sul semaforo ritardo, perché n sarà
positivo. Quindi il produttore e il consumatore potranno lavorare senza problemi. Se il consumatore ha
svuotato il buffer 𝑖𝑓(𝑛 = 0) 𝑤𝑎𝑖𝑡(𝑟𝑖𝑡𝑎𝑟𝑑𝑜), si mette in attesa che venga prodotto un nuovo elemento.

2) Monitor
Un monitor è un costrutto di sincronizzazione di alto livello, usato da due o più processi, per rendere
mutuamente esclusivo l’accesso a risorse condivise. Un monitor è una classe, implementata da un
linguaggio orientato agli oggetti, formato da:

▪ Variabili locali, dichiarate private, accessibili solo dalle procedure del monitor.
▪ Procedura d’inizializzazione delle variabili (detto anche costruttore).
▪ Un insieme di procedure o funzioni.

Le caratteristiche principali di un monitor sono:

1. Le variabili del monitor sono accessibili solo dalle procedure del monitor e non dalle procedure esterne.
2. Un processo entra nel monitor chiamando una delle sue procedure.
3. Solo un processo alla volta può essere in esecuzione all’interno del monitor, i restanti sono sospesi.

Garantendo l’esecuzione di un solo processo alla volta, un monitor può essere usato per la mutua
esclusione; poiché le variabili locali sono accessibili da un solo processo alla volta, si possono proteggere le
strutture dati o sezioni critiche condivise semplicemente mettendole all’interno del monitor. Quando un
processo invoca una procedura del monitor, la richiesta viene accodata e soddisfatta non appena il monitor
è libero.

Per essere utile nell’elaborazione concorrente, un monitor deve contenere degli strumenti di
sincronizzazione. Ad esempio, si supponga che un processo chiami il monitor, e che, mentre è al suo
interno, sia sospeso finché non si verifica una certa condizione. È necessario fare in modo che il processo
sospeso rilasci il monitor, in modo che altri processi possano entrare. Quando in seguito la condizione viene
soddisfatta e il monitor è nuovamente libero, il processo deve essere riattivato e poter rientrare nel
monitor nello stesso punto in cui era stato sospeso. Un monitor fornisce la sincronizzazione mediante l’uso
di variabili di condizioni, accessibili solo dall’interno del monitor. Due funzioni operano su queste variabili:

1. wait() : sospende l’esecuzione del processo chiamante sulla condizione; il monitor diventa disponibile
per gli altri processi.
2. signal() : riattiva un processo sospeso sulla condizione.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 76 a 103

7.2 Stallo
Lo stallo (deadlock) si può definire come il blocco permanente di un insieme di processi che competono
per le risorse di sistema o comunicano fra loro. In pratica, è una situazione in cui due o più processi si
bloccano a vicenda aspettando che uno esegue una certa azione (es: rilasciare una risorsa) che serve
all’altro e viceversa.

Un esempio è rappresentato da due persone che vogliono disegnare: Le due persone hanno a disposizione
solo una gomma e una matita, entrambe sono necessarie per disegnare. Potendo prendere un solo oggetto
per volta: uno prende la matita e l’altro prende la gomma; se ad un certo punto uno di loro ha bisogno della
risorsa dell’altro, e questa risorsa non è libera, si genera uno stallo.

Lo stallo comporta che tutti i processi in attesa dell’evento passano allo stato di blocked (in attesa). Così
facendo nessuno dei processi in stallo può;

▪ andare in running: perché necessita il verificarsi dell’evento.


▪ liberare le risorse: perché non può andare in running.

I processi, quindi, sono “congelati” nella situazione di stallo a meno di un intervento da parte del SO.

Condizioni per lo stallo

Affinché lo stallo sia possibile, si devono verificare quattro condizioni:

1. Mutua esclusione: Un solo processo alla volta può usare la risorsa.


2. Possesso e attesa (Hold&Wait): Un processo può mantenere il possesso delle risorse allocate mentre
aspetta di avere altre risorse.
3. Assenza di prerilascio: Se un processo possiede una risorsa, non può essere forzato a rilasciarla
4. Attesa circolare: C’è una catena chiusa di processi, tale che ogni processo possieda almeno una risorsa
richiesta dal processo successivo della catena.

Le prime tre condizioni sono necessarie ma non


sufficienti per avere stallo: in presenza delle prime
tre condizioni si può avere una sequenza di eventi
che portano ad una situazione di attesa circolare non
risolvibile. L’attesa circolare non è risolvibile quando
le prime tre condizioni sono soddisfatte; quindi, le 4
condizioni insieme formano una condizione
necessaria e sufficiente per lo stallo.

Possiamo utilizzare dei grafi per ragionare sull’allocazione delle risorse ai processi. Un grafo di allocazione
delle risorse è un grafo che rappresenta in che modo le risorse sono assegnate ai processi e le richieste dei
processi.

Soluzioni

Sono possibili quattro diverse strategie per affrontare il problema dello stallo dei processi:

1. Algoritmo dello struzzo: ignorare il problema (solo se lo stallo capita raramente).


2. Rilevare ed eliminare lo stallo.
3. Prevenire lo stallo.
4. Rimuovendo le condizioni dello stallo.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 77 a 103

7.2.1 Rilevare ed eliminare lo stallo


La seconda strategia prevede l’identificazione e la risoluzione; il sistema non tenta di impedire il verificarsi
dei deadlock, ma al contrario, permette che si abbia stallo, prova a identificarlo e a risolverlo.

Rilavare lo stallo: caso una risorsa per tipologia

Avendo a disposizione una solo risorsa di ogni tipo (1 per la stampante, 1


per tastiera ecc.) si può rilevare lo stallo costruendo il grafo di assegnazione
delle risorse. Se si verifica un ciclo all’interno di questo grafo sicuramente
c’è uno stallo.

CONTRO: Sebbene sia relativamente semplice trovare i processi in stallo


guardando semplicemente il grafo, nei sistemi ci vuole un algoritmo
formale. Inoltre, questa soluzione è adatta solo per sistemi a singola risorsa
per tipologia.

Rilevare lo stallo: caso più risorse per tipologia

Serve quindi una soluzione che funzioni in generale, anche nei casi in cui si hanno più risorse per ogni
tipologia. Definiamo quindi quattro strutture dati di cui necessitiamo:

1) E: Vettore che contiene tutte le risorse esistenti.


2) A: Vettore che contiene le risorse disponibili.
3) C: Matrice delle allocazioni: quante risorse per ogni tipo sono state allocate ad ognuno dei processi.
4) R: Matrice delle richieste: quante risorse per ogni tipo necessita il processo.

Le righe delle matrici rappresentano i processi mentre le colonne il numero di risorse per tipologia.

Esempio:

𝐸 = (4 2 3 1) : 4 stampanti totali, 2 plotter totali, 3 scanner totali, 1 lettore cd totale

𝐴 = (2 1 0 0) : 2 stampanti disponibili, 1 plotter disponibile, 0 scanner e cd disponili


0010
Le risorse disponibili sono suddivise tra i processi nel seguente modo: 𝐶 = [ 2 0 0 1 ]
0120
𝐶1 (processo 1) ha 0 stampanti, 0 plotter, 1 scanner e 0 cd allocati

𝐶2 (processo 2) ha 2 stampanti, 0 plotter, 0 scanner e 1 cd allocati

𝐶3 (processo 3) ha 0 stampanti, 1 plotter, 2 scanner e 0 cd allocati

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 78 a 103

Notazioni:

▪ Sia 𝑅𝑖 l’i-esima riga della matrice R.


▪ Si definisce la relazione 𝐴 ≤ 𝐵 su due vettori A e B per dire che ogni elemento di A è minore o uguale al
corrispondente elemento di B.

L’algoritmo di individuazione dello stallo opera nel seguente modo:

1) Inizialmente ogni processo 𝑃𝑖 non è marcato.


2) Cerca un processo non marcato tale che 𝑅𝑖 ≤ 𝐴.
(Per ogni processo non mancato, se le risorse richieste sono minori o uguali a quelle disponibili).
• Le risorse vengono assegnate al processo ed eseguito.
• Quando termina, le risorse che deteneva (C+R) vengono aggiunte a quelle disponibili (A).
• Il processo viene marcato.
3) Alla fine del ciclo, se rimangono processi non marcati allora sono in stallo. Nessuno di loro poteva
acquisire risorse sufficienti per l’esecuzione, tenendo occupato le risorse già in possesso.

Con questo algoritmo è possibile descrivere una strategie di rilevamento dello stallo che non permette a un
processo di iniziare l’esecuzione se le sue richieste porterebbero ad uno stallo.

Esempio pratico (non stallo) :

Risorse esistenti: 𝐸 = (4 2 3 1) Risorse libere: 𝐴 = (2 1 0 0)


0010 2001
Risorse assegnate già ai processi: 𝐶 = [ 2 0 0 1 ] Risorse richieste dai processi: 𝑅 = [ 1 0 1 0 ]
0120 2100
(righe della matrice: i-esimo processo, colonne della matrice numero risorse per tipologia)

1. Il processo 𝑃1 (2 0 0 1) può essere soddisfatto ? No, risorse libere 𝐴 = (2 1 0 0)


2. Il processo 𝑃2 (1 0 1 0) può essere soddisfatto ? No, risorse libere 𝐴 = (2 1 0 0)
3. Il processo 𝑃3 (2 1 0 0) può essere soddisfatto ? Si, viene marcato 𝑃3 e viene eseguito, 𝐴 = (0 0 0 0).
Al termine bisogna liberare le risorse del processo 𝑃3 (2 2 2 0) : Quindi 𝐴 = (2 2 2 0)
4. Il processo 𝑃1 (2 0 0 1) può essere soddisfatto ? No, risorse libere 𝐴 = (2 2 2 0)
5. Il processo 𝑃2 (1 0 1 0) può essere soddisfatto ? Si, viene marcato 𝑃2 , 𝐴 = (1 2 1 0). Quando il
processo termina, il nuovo vettore di disponibilità diventa 𝐴 = (4 2 2 1)
6. Il processo 𝑃1 (2 0 0 1) può essere soddisfatto ? Si, viene mancato 𝑃1 , ...

Esempio pratico (in stallo) :

Risorse esistenti: 𝐸 = (2 1 1 2 1) Risorse libere: 𝐴 = (0 0 0 0 1)


10110 01001
11000 00101
Risorse assegnate già al processo: 𝐶 = [ ] Risorse richieste dal processo: 𝑅 = [ ]
00010 00001
00000 10101
1. Il processo 𝑃1 (0 1 0 0 1) può essere soddisfatto ? No, risorse libere: 𝐴 = (0 0 0 0 1)
2. Il processo 𝑃2 (0 0 1 0 1) può essere soddisfatto ? No, risorse libere: 𝐴 = (0 0 0 0 1)
3. Il processo 𝑃3 (0 0 0 0 1) può essere soddisfatto ? Si, marchio 𝑃3 : 𝐴 = (0 0 0 1 2)
4. Il processo 𝑃1 (0 1 0 0 1) può essere soddisfatto ? No, risorse libere: 𝐴 = (0 0 0 1 2)
5. Il processo 𝑃2 (0 0 1 0 1) può essere soddisfatto ? No, risorse libere: 𝐴 = (0 0 0 1 2)
6. Il processo 𝑃4 (1 0 1 0 1) può essere soddisfatto ? No, risorse libere: 𝐴 = (0 0 0 1 2)
Pertanto i processi non marcati 𝑃1 , 𝑃2 , 𝑃4 sono in stallo.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 79 a 103

Quanto spesso invocare l’algoritmo dipende dalla frequenza presunta con la quale si verifica lo stallo e dal
numero di processi coinvolti nello stallo. Possiamo richiamare l’algoritmo quando:
▪ Ad ogni richiesta: consente la determinazione dello stallo immediatamente. Ma causa un aumento del
carico di lavoro (overhead)
▪ La percentuale di utilizzo delle risorse scende al di sotto di una soglia: Perché se la percentuale di
utilizzo della risorse scende può essere che si sia verificato uno stallo
▪ Ad istanti arbitrari (a cazzo di cane). Nel grafo di assegnazione delle risorse potrebbero risultare molti
cicli e diventa difficile determinare quale processo ha causato lo stallo. Risulterebbe un unico grande
stallo.

Eliminare lo stallo rilevato

Quando lo stallo è stato rilevato, serve una strategia di ripristino; Ci sono vari approcci possibili:

1) Tutti i processi in stallo vengono eliminati: soluzione più comune.


2) Ripristinare tutti i processi in stallo a uno stato precedente (rollback) e riattivarli. Il rischio è che lo stallo
originali capiti ancora, ma il nondeterminismo dei processi concorrenti dovrebbe assicurare che questo
non avvenga.
3) Eliminare i processi in stallo uno dopo l’altro fino all’eliminazione dello stallo: l’ordine in cui si scelgono
i processi da abortire dovrebbe basarsi su un criterio di minimo costo (stima di quello che fa meno
danni). Dopo ogni aborto è necessario attivare nuovamente l’algoritmo di rilevamento per vedere se c’è
ancora stallo.
4) Scelgo un processo vittima ed effettuo il rollback (macchina del tempo) del processo a cui è stata
sottratta la risorsa. Quindi ritorno a uno stato sicuro di quel processo per poterlo riavviare in seguito.
Problema: Starvation: Alcuni processi potrebbero essere selezionati costantemente come vittime.

7.2.2 Prevenire lo stallo


Nel discutere l’identificazione del deadlock, l’algoritmo di individuazione, si è supposto che quando un
processo richiede delle risorse le richiede tutte insieme (matrice richieste). In molti sistemi, invece le
risorse, sono richieste uno alla volta, ed il sistema deve poter decidere se assegnare una risorsa è un’azione
sicura e fare l’assegnazione solo in questo caso.

La strategie di prevenzione dello stallo consiste nell’utilizzare delle politiche di allocazione in modo da
definire un ordine di assegnazione delle risorse tale che non si verifichi mai uno stallo.). L’algoritmo
principale per evitare il deadlock si basa sul concetto degli stati sicuri. Prima di descriverlo si parlerà del
problema della sicurezza utilizzando un disegno che ne renderà più facile la comprensione.

In figura si vede un modello in grado


di trattare due processi (A e B) e due
risorse (stampante e plotter). L’asse
orizzontale rappresenta il numero di
istruzioni eseguite dal processo A;
l’asse verticale rappresenta il numero
di istruzioni eseguite dal processo B.

In i1, A richiede una stampante; in I2,


ha bisogno di un plotter. La
stampante e il plotter sono rilasciati
in I3 e I4; rispettivamente il processo
B ha bisogno del plotter da I5 a I7, e
della stampante da I6 a I8.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 80 a 103

Ogni punto del grafico rappresenta uno stato congiunto dei due processi. Inizialmente, lo stato è in P, e
nessuno dei due processi ha eseguito alcuna istruzione. Se lo scheduler sceglie il processo A, si arriva al
punto q, nel quale A ha eseguito un certo numero di istruzioni mentre B nessuna. Nel punto q, la traiettoria
diventa verticale, indicando che lo scheduler ha scelto di eseguire B.

Se si ha un singolo processore, tutti i cammini devono essere orizzontali o verticali, mai diagonali. Inoltre, lo
spostamento avviene sempre verso l’alto o verso destra, mai il contrario (i processi non possono ritornare
indietro).

Quando A attraversa la linea I1 sul cammino da r a s, significa che esso ha richiesto ed ottenuto la
stampante; Quando B raggiunge il punto t, esso richiede il plotter. Le regioni tratteggiate indicano lo stato
in cui entrambi i processi detengono la stampante/plotter, e la regola di mutua esclusione fa sì che sia
impossibile entrare in questa zona. Se il sistema entrasse nella sezione compresa tra I6-I7 e I2-I3, A
richiederebbe il plotter e B la stampante, ma entrambe queste risorse sono già state assegnate.

Al punto t, la cosa sicura che si può fare è di far eseguire il processo A (sospendendo B), finché arrivi a I4.
Dopo questo punto una qualunque traiettoria che arrivi ad u può essere scelta con successo.

Stato sicuro e non sicuro

L’algoritmo per evitare i deadlock si basano sugli stati sicuri e non sicuri:

▪ Stato sicuro: Se non è in stallo, e se c’è un modo per soddisfare tutte le richieste eseguendo i processi
in un ordine preciso.
▪ Non sicuro: Il contrario, ovviamente. Lo stato non sicuro non indica certezza, ma una certa possibilità
per cui potrebbe verificarsi lo stallo.

Deadlock avoidance: significa assicurarsi che il sistema non esca mai dallo stato sicuro.

Algoritmo del banchiere

L’algoritmo del banchiere è un algoritmo di scheduling che permette di evitare le situazioni di stallo ed è
un’estensione dell’algoritmo di identificazione del deadlock.

Tale strategie prende il nome dal comportamento del banchiere: nessun banchiere presterebbe dei soldi
senza avere la certezza che questi tornino a sé. Il banchiere sa che non tutti i clienti avranno bisogno
immediatamente del loro credito massimo, così facendo il banchiere gestisce l’ordine con cui erogare i
prestiti, evitando il fallimento della banca (deadlock).

L’algoritmo del banchiere analizza ogni richiesta ed analizza se soddisfarla porta ad uno stato sicuro. Se lo
fa, soddisfa la richiesta, altrimenti viene posticipata. Per vedere se uno stato è sicuro: il banchiere controlla
di avere abbastanza risorse a disposizione per soddisfare la richiesta di un cliente. Se e così, questo prestito
può essere considerato restituito (non erogato), e si può controllare il cliente successivo. Se tutti i prestiti
possono essere restituititi, rimanendo in uno stato sicuro, il banchiere inizia ad erogare i soldi (nell’ordine
calcolato).

Lo stato sicuro viene determinato utilizzando l’algoritmo di individuazione dello stallo (marcare processi).

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 81 a 103

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 82 a 103

PRO:

▪ Non è necessario interrompere i processi e riportarli a uno stato precedente (rollback). In questo caso,
prima di assegnare le risorse si effettua una verifica.

CONTRO:

▪ Il numero di risorse richieste da ogni processo deve essere definito a priori.


▪ Le risorse totali (risorse[m]) devono essere definite a priori.
▪ I processi devono essere indipendenti. Perché altrimenti altererebbe l’ordine di esecuzione dei
processi.
▪ Quando un processo richiede risorse potrebbe essere posto in attesa.
▪ Quando un processo ottiene le risorse di cui necessita, deve rilasciarle in un tempo finito.

Questo è un algoritmo che teoricamente risolverebbe il problema dello stallo. Ma esso è privo di ogni utilità
pratica per via del fatto che raramente i processi sono in grado di stabilire in anticipo il massimo numero di
risorse di cui hanno bisogno per completare l’esecuzione. Oltre a ciò, il numero dei processi non è di solito
fisso, ma varia dinamicamente. Infine, risorse che vengono ritenute disponibili ad un certo istante possono
non esserlo più all’istante successivo: ad esempio un disco potrebbe rompersi.

7.2.3 Impedire lo stallo rimuovendo le condizioni


È possibile progettare un sistema in modo che la possibilità di avere stallo sia esclusa a priori. Si distinguono
due classi di prevenzione:

1. Metodo diretto: previene il verificarsi di un’attesa circolare.


2. Metodo indiretto: previene il verificarsi di una delle tre condizioni necessarie.

Negare attesa circolare

La condizione di attesa circolare si può prevenire definendo un ordine lineare fra i tipi di risorsa: se un
processo possiede delle risorse di tipo R, in seguito può richiede solo risorse il cui tipo segue R
nell’ordinamento.

Svantaggi: La prevenzione dell’attesa circolare può essere inefficiente, rallentando i processi e forzandoli a
seguire un ordine specifico.

Negare mutua esclusione

Se nessuna risorsa fosse mai assegnata in maniera esclusiva ad un unico processo, non avremmo mai
situazioni di stallo. D’altra parte, è altrettanto chiaro che permettere a due processi di scrivere
contemporaneamente su di una stampante porterebbe al caos. Con lo spooling (buffer su disco) dell’uscita
su stampante, più processi potrebbero generare stampa nello stesso istante. Per esempio:

I documenti da stampare vengono caricati in un buffer, dove vengono inviati alla stampante ed eliminati via
via che questa riesce a gestirli, di solito con tempi relativamente lunghi (coda di stampa elaborata in
background). In questo modello, l’unico processo che richiederebbe fisicamente la stampante è il gestore di
stampa che, poiché non richiederebbe mai altre risorse, per la stampante il problema dello stallo è
eliminato.

Questa situazione non è applicabile in generale, perché non tutti i dispositivi possono essere gestiti con lo
spool (spreco di memoria). Quindi è consigliato evitare di assegnare una risorsa quando non strettamente
necessario. Dall’altro lato bisogna fare in modo che il numero minor possibile di processi possa richiedere
una risorsa. Lo stallo si verifica quando più processi richiedono la stessa risorsa. Se si riuscisse a limitare
questa strategia, lo stallo potrebbe non verificarsi.
Corso di studi in Informatica (A-L) A.A. 2021/2022
Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 83 a 103

Negare hold-and-wait

Se si potesse evitare che un processo che già detiene delle risorse ne richieda dalle altre, si potrebbero
eliminare i deadlock. Quindi bisogna obbligare tutti i processi a richiedere tutte le risorse di cui hanno
bisogno prima di iniziare l’esecuzione. Se tutte le risorse richieste risultano disponibili, allora il processo
potrà essere mandato in esecuzione, avrà tutto quello di cui ha bisogno, e potrà essere eseguito fino alla
fine. Al contrario, se una o più risorse sono occupate, il processo non viene mandato in esecuzione e rimane
in attesa.

Svantaggi: Un primo problema che si pone è che i processi non sanno di quante risorse hanno bisogno
finché non cominciano l’esecuzione: se lo sapessero, si potrebbe usare l’algoritmo del banchiere. Per
esempio un’istruzione if(), non si sa anticipatamente dove andrà l’esecuzione se non si calcola la
condizione.

Un secondo problema è dato dal fatto che in questa maniera non viene ottimizzato l’uso delle risorse. Ad
esempio: se un processo che legge dati da un disco, li analizza per un’ora e poi stampa i risultati... se tutte
le risorse devono essere richieste anticipatamente, il processo terrà occupato il disco e la stampante per
un’ora inutilmente.

Negare l’assenza di prerilascio

Vuol dire che il SO può espropriare le risorse a un processo. Si possono verificare due situazioni:

1. Se un processo non riesce ad ottenere le risorse di cui necessita, fino a quando il SO non riesce ad
allocare le risorse, il processo rilascia quelle che detiene per richiederle nuovamente in un secondo
momento.
2. Il SO potrebbe richiedere il pre-rilascio delle risorse al processo che le detiene per assegnarle al
richiedente.

Questo è un approccio applicabile solo se lo stato della risorsa è facilmente salvabile (così da riprendere
l’esecuzione in un secondo momento).

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 84 a 103

8.0 Gestione della memoria centrale


La memoria centrale o RAM (Random Access Memory) è una risorsa condivisa
fra i processi;

Se guardiamo dentro a un computer, la RAM, sul piano fisico, non è che ci dà una percezione del contenuto
al suo interno e di come gestirlo. È necessario imporre sulla struttura fisica, una sovrastruttura che fornisce
un significato al contenuto delle diverse componenti, e permette al sistema, nel suo complesso, di
interpretare le informazioni in maniera corretta, accedere a delle porzioni di memoria a cui ha diritto di
accedere; Quindi, il SO implementa tutta una serie di meccanismi di controllo e di garanzia che sono
fondamentali in un elaboratore.

La RAM è importante perché è l’unica memoria, di grande dimensioni, direttamente accessibile alla CPU
attraverso l’esecuzione di un ciclo predefinito che prende il nome di fetch-decode-execute:

▪ Fetch: Individuare la prossima istruzione (PC) da eseguire e caricarla nell’IR.


▪ Decode: Decodificare l’operazione tramite un codice operativo.
▪ Execute: Individuare e caricare i dati richiesti ed eseguire l’operazione.

La RAM è un generatore e utilizzatore di un flusso di indirizzi di memoria. Quando la CPU esegue


un’operazione fetch deve identificare un’operazione all’interno della RAM accedendo tramite un indirizzo.
L’operazione viene decodificata e nel momento in cui l’istruzione deve essere eseguita si generano altri
indirizzi che permettono l’accesso ai dati da elaborare; e per alcune istruzioni è necessario individuare
quella locazione di memoria in cui andare a salvare il risultato dell’operazione compiuta.

La RAM è una risorsa condivisa tra i processi e, per ogni processo la RAM contiene il suo codice (completo o
parziale) e i dati da elaborare. È necessario far sì che un processo non vada a sovrascrivere il codice/dati di
un altro (specie se quest’ultimo è un processo del SO). Il SO deve implementare dei meccanismi di
protezione per fare in modo che ogni singola porzione della RAM acceda soltanto il processo, o gruppo di
processi, che sono deputati a farlo.

8.0.1 Spazi degli indirizzi


Analizziamo il seguente codice: 𝑓𝑜𝑟( 𝑖𝑛𝑡 𝑖 = 0 ; 𝑖 < 10 ; 𝑖 + + ) 𝑚𝑖𝑜𝐴𝑟𝑟𝑎𝑦[ 𝑖 ] = 0 ;

Questo codice sintatticamente corretto è detto codice sorgente, cioè il programma scritto e facilmente
comprensibile da un essere umano. Questo programma per essere eseguito deve sottostare a dei passi di
traduzione. La fase di compilazione trasforma le istruzioni e le variabili in una forma differente, ma
equivalente, a cui alle diverse parti del programma sono assegnati degli identificatori numerici detti
indirizzi.

Quindi, la riga di codice precedentemente scritta verrà trasformata in una rappresentazione di più basso
livello, equivalente, che verrà caricata da qualche parte nella porzione di RAM associata al processo.

Registro base e limite

Il modo più semplice per associare ad un processo un identificatore della


porzione di RAM associata al processo stesso si basa sugli indici, cioè indirizzi,
della prima (es: j) e dell’ultima cella (es: k) del segmento contiguo di memoria
associata al processo. Si possono implementare dei meccanismi di protezione
in modo che se il processo tenta di accedere alla locazione 𝑖 < 𝑗 o 𝑖 > 𝑘, il SO
non fornisce l’autorizzazione.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 85 a 103

Il meccanismo appena descritto si basa sull’utilizzo di due registri: base e limite. La CPU è dotata di una
coppia di registri (oltre a quelli usuali) in cui vengono caricate delle informazioni sul processo running.
L’indice j viene caricato nel registro base e l’indice k nel registro limite. Il sistema operativo ha così modo di
controllare automaticamente se gli indirizzi prodotti dalla CPU, nell’esecuzione del processo, sono indirizzi
compresi tra base e limite (spazio di memoria contiguo associato al processo). Se il processo tenta di
accedere ad una porzione di RAM compresa tra il registro base e limite, l’accesso viene consentito,
altrimenti negato.

Come vengono generati gli indirizzi?

Il punto di partenza è il codice sorgente ( es: prova.c ). Il programma C non può essere eseguito poiché va
compilato. Quindi il codice sorgente viene dato in input ad un processo, detto compilatore, che effettua
una traduzione. Il compilatore ha, al suo interno, codificate le regole sintattiche del linguaggio, cioè riesce a
capire quando una stringa data in input è ben formata (se non è ben formata segnala degli errori). La
compilazione è un’operazione di trasformazione in cui i dati sorgenti vengono tradotti in un
modulo oggetto.

▪ Nel modulo oggetto gli indirizzi associati ad ogni istruzione/dati sono indirizzi in forma rilocabile: I vari
indirizzi che saranno generati sono dinamici, cioè possono variare in base alle esigenze del SO.

Per esempio: Supponiamo che il segmento contiguo di ram associato al processo P sia identificato da un
indirizzo base (es: 2045) e un indirizzo limite (es: 2065). Sia k una variabile, che nel sistema, è stato tradotto
come “base+10”. Questo è un modo di indirizzare la variabile k che ci fa capire come individuarla
all’interno del segmento contiguo (spostandoci di 10 posizioni rispetto all’indirizzo base del processo).
L’indirizzo è rilocabile perché qualunque sia il valore dell’indirizzo della cella base, la variabile k sarà
collocata nell’indirizzo di partenza+10. Quindi è rilocabile perché calcolabile (base+10).

▪ Invece utilizzando un indirizzo in forma assoluta rappresenterei la variabile k con l’indirizzo 2055.
Questo tipo di traduzione si poggia su un assunto molto forte: l’assunto è che il compilatore sappia
esattamente in quale porzione della ram la variabile k verrà caricata. Lo svantaggio principale è che il
SO non avrà possibilità, né la flessibilità di gestire autonomamente il posizionamento del processo in
RAM, poiché l’indirizzo è stato già definito dal compilatore.

Il modulo oggetto, in genere non basta, poiché un programma non viene mai scritto in maniera completa.
Per esempio il codice delle funzioni di libreria viene sempre incluso, cioè allegato, e non scritto per intero in
ogni programma. Il modulo oggetto, prodotto dalla compilazione del file sorgente, viene composto con i
moduli oggetto di eventuali librerie o funzioni esterne. Questa composizione viene effettuata dal linker che
genera il modulo rilocabile. È detto rilocabile perché questo oggetto si trova ancora nel file system; quindi,
gli indirizzi possono essere ancora gestiti in modo dinamico.

Si occupa del caricamento in RAM del modulo rilocabile il loader che genera il modulo eseguibile.

Indirizzi logici e fisici

Gli indirizzi prodotti dalla CPU, durante l’esecuzione del processo, devono essere trasformati per diventare
indirizzi fisici, cioè la posizione esatta di una cella di RAM. L’indirizzo prodotto dalla CPU prende il nome di
indirizzo logico e parte sempre da 0.

L’indirizzo logico e fisico deve essere differente perché la RAM deve essere gestita dal SO per essere
assegnata ai vari processi. Il SO avrà dei suoi criteri, che permettono di distribuire la RAM ai processi e
questi criteri richiedono delle sovrastrutture. Questo passaggio intermedio è necessario per rendere più
efficiente la gestione della ram: perché il SO ha modo di spostare porzioni codice da una parte della ram ad

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 86 a 103

un’altra senza che la CPU se ne accorga, cioè senza che la CPU abbia un danno o un interruzione
nell’esecuzione di un determinato programma.

Lo spazio degli indirizzi logici è l’insieme degli indirizzi prodotti dalla CPU. Se ci stiamo riferendo allo spazio
di indirizzi logici di un processo allora staremo parlando degli insieme degli indirizzi logici che la CPU può
produrre durante l’esecuzione di un determinato processo. Lo spazio degli indirizzi fisici è l’insieme degli
indirizzi che costituiscono gli indici delle parole di memoria a cui la CPU può accedere durante l’esecuzione.

Binding

Per poter essere utilizzati, gli indirizzi logici devono essere associati ai corrispondenti indirizzi fisici.
L’operazione di associazione è detta binding: mapping dello spazio degli indirizzi logici di un processo allo
spazio dei suoi indirizzi fisici. Il binding può essere effettuato durante la compilazione, durante la fase di
caricamento in ram (loader) o durante la fase di esecuzione del programma:

Il binding viene effettuato durante la compilazione quando il compilatore è già a conoscenza della
destinazione del programma in memoria. Il compilatore sostituisce il nome delle variabili/istruzioni con gli
indirizzi assoluti delle locazioni di memoria. Questo riduce la flessibilità del sistema operativo.

Il binding viene effettuato durante il caricamento quando il SO identifica una porzione di memoria in cui
inserire le istruzione/dati del processo. Nella fase di caricamento in RAM, gli indirizzi vengono fissati, cioè la
CPU produrrà direttamente degli indirizzi fisici; quindi, l’indirizzo logico, come nel caso precedente,
corrisponderà all’indirizzo fisico.

Il binding viene effettuato durante l’esecuzione quando l’indirizzo k viene prodotto. La CPU produce un
indirizzo logico k e, associato dal SO, ad un indirizzo fisico. Questa tipo di binding richiede un modulo
dedicato alla trasformazione degli indirizzi logici in indirizzi fisici detto memory management unit (MMU).
Caratteristica molto importante è che il binding degli indirizzi viene effettuato se e solo se è e ogni volta che
la CPU deve accedere a quella particolare cella di memoria.

Per esempio un programma potrebbe avere degli if: a seconda dei dati che il
programma elabora, la condizione che mi dice quale via dell’if seguire sarà
vera oppure falsa. A priori non è possibile conoscere il percorso dell’if che
verrà eseguito, si saprà solo durante l’esecuzione. Se il binding è effettuato
durante l’esecuzione, soltanto gli indirizzi corrispondenti al percorso che
viene effettivamente eseguito vengono tradotti.

La MMU è un oggetto che riceve in input un indirizzo logico compreso tra 0 (base) e max (limite). L’MMU,
attraverso il registro di rilocazione traduce l’indirizzo logico in indirizzo fisico. L’indirizzo di rilocazione
contiene l’indirizzo di base del processo in RAM.

Gli schemi di gestione della memoria si differiscono in base al tipo di allocazione, che può essere:

▪ Contigua: La memoria viene allocata in modo tale che ciascun oggetto occupa un insieme di locazioni i
cui indirizzi sono strettamente consecutivi.
▪ Non contigua: La memoria viene allocata in modo tale che un unico oggetto logico viene posto in aree
separate e non adiacenti.

Ogni volta che si parla di schemi di gestione della RAM significa che il SO deve avere delle apposite strutture
per ricordarsi, associando questa informazione al PCB di ogni processo, quale porzione di ram gli è dedicata.
La forma di queste strutture dipende dal modello di allocazione che è stato adottato.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 87 a 103

8.1 Gestione della memoria contigua


Ad ogni processo è associato un segmento contiguo di memoria, cioè tutti gli indirizzi associati sono l’uno il
successivo dell’altro. Il processo non può accedere a informazioni che si trovano fuori del segmento
associato ad esso. Esistono vari tipi di allocazione contigua:

1) Monoallocazione
2) Partizionamento statico
3) Partizionamento dinamico

In questi schemi di gestione il processo viene caricato per intero in memoria; quindi, tutto il codice del
programma viene caricato e viene riservata una porzione di RAM sufficiente a contenere quello che si
pensa possa essere l’occupazione massima del programma (viene effettuata una stima dal SO). Viene
riservata una certa quantità di celle in grado di memorizzare l’intero spazio degli indirizzi fisici del processo.

8.1.1 Monoallocazione
Lo schema di gestione più semplice possibile è quello che consente di
eseguire soltanto un programma alla volta, condividendo la memoria tra il
processo ed il sistema operativo. Quando l’utente avvia un’applicazione, il SO
carica il programma dal disco in memoria e lo esegue. Quando il processo
termina, il SO è pronto per caricare un nuovo processo, sovrapponendolo al
primo.

Per la sua semplicità e limitazione, questo schema viene utilizzato solo da sistemi operativi
monoprogrammati. Il sistema operativo:

▪ Tiene traccia solo della prima (serve per fare la somma e trovare la posizione in memoria) e dell’ultima
(limite della locazione) locazione disponibile per i processi utente.
▪ Viene posto ad uno dei due estremi della memoria.
▪ Ingloba i vettori di interrupt.

Alla richiesta di esecuzione di un programma, il SO:

▪ Si assicura che le dimensioni del processo siano compatibili con la memoria disponibile.
▪ Conferisce il controllo al processo utente fino al suo complementarmente.
▪ Al termine del processo la memoria viene liberata e può essere assegnata ad un altro processo in attesa

Meccanismi di protezione

Raramente la Monoallocazione supporta meccanismi di protezione tra processi utente in quanto in ogni
istante vi può essere al massimo un solo processo residente in memoria. Gli eventuali meccanismi si
riferiscono alla protezione del codice del SO da eventuali sconfinamenti del processo utente in esecuzione.
La protezione del SO dai processi utente è effettuata utilizzando:

▪ Registro barriera: È utilizzato per tracciare un confine tra le aree dei processi di sistema e dei processi
utente. Nel registro viene memorizzato la prima locazione disponibile al processo e il SO effettua i
controlli di sconfinamento. Questo metodo richiede la capacità di distinguere l’esecuzione del SO da
quella dei processi utente definendo gli stati “utente” e “supervisore/kernel”.
▪ I diritti di accesso mediante bit di protezione: Ad ogni parola di memoria viene associato un bit di
protezione che viene posto a 1 nelle zone che contengono il SO e a 0 nelle restanti . I processi utente
accedono solo a parole con bit 0, mentre il SO ha accesso illimitato.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 88 a 103

▪ Sistema operativo in modalità a solo lettura. Nelle applicazioni generiche, come i computer, questo
metodo non è solitamente utilizzato per la sua scarsa flessibilità e impossibilità di correggere o
aggiornare il codice del SO.

Vantaggi:

▪ Basso costo di progettazione del modulo di gestione della memoria.


▪ Supporti specifici per la gestione della memoria (registri barriera).

Svantaggi:

▪ Non c’è multiprogrammazione, perché in ogni istante un solo processo alla volta può essere in running.
▪ La memoria può risultare sovradimensionata per la maggioranza dei processi.
▪ Processi di dimensione più grande della memoria disponibile non possono essere eseguiti.

8.1.2 Partizionamento statico


Un modo per supportare la multiprogrammazione consiste nel
dividere la memoria fisica in differenti partizioni, ciascuna delle
quali verrà allocata ad un processo. Il partizionamento statico
implica che la suddivisione venga fatta a monte e che da quel
momento in poi le partizioni rimangono di dimensioni fisse.

Il numero e la dimensione di ciascuna partizione vengono


determinate durante la generazione del SO, considerando:

▪ Capacità della memoria fisica.


▪ Grado desiderato di multiprogrammazione (es: 3 partizioni = 3 processi contemporaneamente).
▪ Dimensione dei processi più frequentemente eseguiti.

Per tenere traccia delle partizioni definite, il SO utilizza una


tabella di descrizione delle partizioni (TDP), dove indica, per ogni
partizione lo stato corrente e i relativi attributi. I campi della
tabella sono fissi, l’unico campo variabile è lo stato della
partizione che indica in ogni istante se la partizione è allocato o
meno. Ogni riga della TDP rappresenta lo spazio di indirizzamento
fisico di un singolo processo.

Quando un processo non residente (non è ancora caricato in memoria) deve essere creato o attivato, il SO
tenta di allocare una partizione libera di dimensioni sufficienti per contenerlo, consultando la TDP. Se il test
è positivo, il campo “stato della partizione” viene impostato su “allocata” e l’immagine del processo viene
caricata nella corrispondente locazione. Quando un processo termina è necessario effettuarne uno
swapping dalla memoria (spostare il processo dalla memoria al disco), svuotando la riga della tabella
corrispondente. Gli algoritmi di ricerca di una partizione libera si basano su uno di questi due meccanismi:

▪ First fit: consiste nell’allocare la prima partizione libera sufficientemente larga da contenere il processo.
Questo metodo permette di allocare il processo molto velocemente, ma può capitare che un processo
piccolo sia allocato in una partizione molto più grande, rendendo inefficiente il sistema.
▪ Best fit: consiste nell’allocare la più piccola delle partizioni libere disponibili. Il SO effettua uno scan di
tutte le partizioni e cerca quella più adatta per quel determinato processo. Questo metodo permette di
risparmiare spazio a discapito della velocità (ogni volta il SO deve controllare tutte le partizioni).

Non è detto che l’algoritmo di ricerca riesca a trovare sempre una partizione libera; Le cause possono
essere:

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 89 a 103

▪ Tutte le partizioni sono allocate.


▪ Non vi sono partizioni sufficientemente grandi per contenere il processo entrante.

Nel primo caso, il processo o viene messo in coda o viene eseguito lo swapping: Il gestore dello swapping
decide di rimuovere un processo per liberare spazio, in base alla priorità e il tempo trascorso in memoria.

Tutti i processi che hanno subito uno swapping possono essere mantenuti in un file unico o in file separati.
Un file di swapping (file unico) viene solitamente creato al momento dell’inizializzazione del sistema e
memorizzato su una periferica veloce di memorizzazione secondaria. L’indirizzo e la dimensione di tale file
sono solitamente statici in modo da beneficiare di un indirizzamento diretto su disco. In alternativa, è
possibile rendere il file di swapping dinamico, associandone uno ad ogni processo. Questa tecnica è
efficiente, perché permette di risparmiare spazio ma aumenta i tempi di accesso al file: Dato che i file di
swapping sono memorizzati sul disco; quindi, distribuiti in memoria secondo uno schema di gestione del
filesystem, ed ogni volta bisognerà calcolare tutto lo schema del filesystem per rigenerare il processo.

Nel secondo caso la soluzione potrebbe essere quelle di ridimensionare l’intera TDP, oppure progettare il
processo utilizzando una schema di overlay: dividere il programma in moduli e caricarli in memoria quando
necessari.

Esempio: Un programma è strutturato in dati, codice e output. Nella programmazione procedurale il


programma è caricato in memoria per intero, di conseguenza in una partizione saranno memorizzati i dati,
codice e output. Nella programmazione modulare, ogni parte del programma diventa un modulo; il SO
elabora il processo caricando in memoria il modulo di cui ha bisogno in quel determinato momento, usando
la stessa partizione di memoria.

Meccanismi di protezione Spazio indirizzi fisici


L’integrità di un sistema multiprogrammato dipende anche dalla sua
capacità di garantire l’isolamento tra spazi di indirizzi separati. Non solo
si deve proteggere il SO dall’interferenza dei processi utente ma bisogna
anche proteggere i processi utente tra di loro, in modo tale che nessuno
possa invadere lo spazio riservato all’altro. Si può implementare il
meccanismo di protezione tramite due registri, detti registro base e
limite:

▪ Registro base: contiene il più piccolo indirizzo fisico associato al


processo. Per esempio 300040KB
▪ Registri limite: contiene la dimensione massima del processo. Per
esempio 120900KB

Indirizzamento: (traduzione indirizzo logico


in indirizzo fisico) Ad ogni accesso alla
memoria la CPU controlla che il
programma non richieda l’accesso a
indirizzi non autorizzati. Qualsiasi tentativo
da parte di un processo eseguito in
modalità utente di accedere alle aree di
memoria riservate al SO o di un altro
processo comporta l’invio di una eccezione
(trap) che restituisce il controllo al SO che,
a sua volta, interpreta l’evento come un errore fatale.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 90 a 103

Il processore genera un indirizzo logico che rappresenta lo scostamento rispetto al registro base del
processo. Ogni indirizzo logico viene confrontato con il registro limite del processo per eliminare eventuali
sconfinamenti. Se l’indirizzo logico è minore del registro limite, viene sommato l’indirizzo con il registro
base del processo per trovare l’indirizzo fisico in memoria.

Meccanismi di condivisione

Un buon sistema di gestione della memoria deve occuparsi, oltre che della protezione, anche della
condivisione controllata di dati e codice tra processi cooperanti. I sistemi a partizioni fisse non sono adatti
alla condivisione, poiché basano i propri meccanismi sull’isolamento degli spazi di indirizzamento. Alcuni
possibili metodi di condivisione sono:

▪ Affidare gli oggetti condivisi al SO che così è in grado di accedere a tutte le risorse. Lo svantaggio è
dato dal fatto che l’area del SO dovrebbe poter variare dinamicamente.
▪ Ogni processo possiede una copia identica dell’oggetto condiviso, che usa e diffonde gli
aggiornamenti. Lo svantaggio di questa tecnica consiste nel fatto che se il sistema supporta lo
swapping, uno o più processi potrebbe non essere in memoria e quindi non essere pronti per ricevere
aggiornamenti.
▪ Collocare i dati in una partizione comune dedicata. In questo caso, però il SO considera, come
violazione, i tentativi dei processi partecipanti alla condivisione di accedere a zone di memoria esterne
alle rispettive partizioni. Se il sistema usa registri base e limite sono necessari accorgimenti per
indirizzare partizioni che potrebbero non essere contigue.

Vantaggi:

▪ Uno dei metodi più semplici di gestione della memoria che supporta la multiprogrammazione.
▪ Si adegua ad ambienti statici con carico predicibile di lavoro.

Svantaggi:

▪ Qualunque programma, indipendentemente dalla sua dimensione,


occupa un’intera partizione in memoria. Se il processo è più piccolo della
partizione assegnata, si verifica una frammentazione interna.
▪ Non si adatta a contenere processi o oggetti dinamici, che quindi possono
crescere durante l’esecuzione.
▪ Il numero di partizioni fissate limita il grado di multiprogrammazione.

8.1.3 Partizionamento dinamico


Per superare alcune difficoltà che si riscontrano nel partizionamento statico, è stato sviluppato un
approccio conosciuto come partizionamento dinamico. Con il partizionamento dinamico, si usano
partizione in numero e lunghezza variabile;

Il gestore della memoria può creare e allocare partizioni finché non viene esaurita la memoria
fisica o finché non viene raggiunto il massimo grado di multiprogrammazione.

L’indirizzamento del partizionamento dinamico è analogo al partizionamento statico.

Allocazione: Alla richiesta di una partizione, il gestore della memoria, ricerca una zona di
memoria libera contigua di dimensione sufficiente. Se si trova un’area adatta il SO vi ricava
una partizione in modo da soddisfare esattamente le necessità del processo. La partizione
viene creata registrando la base, la sua dimensione e il suo stato nella TDP o in una tabella
equivalente.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 91 a 103

Deallocazione: Quando un processo termina o


subisce uno swapping, il SO restituisce la partizione
all’area libera e cancella la riga corrispondente
nella TDP. Se ci sono due aree di memoria libere
contigue vengono accorpate in un'unica area di
memoria. L’insieme delle aree libere varia
dinamicamente (cioè non è un insieme fisso), gli
spazi interni all’area libera vengono utilizzati per
mantenere una lista a puntatori: invece di gestire
tutto come se fosse un'unica tabella (vettore
memoria) si utilizzano vari puntatori.

Gli algoritmi di rilocazione più comuni per selezionare un’area di memoria libera all’interno della quale
creare una partizione sono:

▪ First-fit: scandisce la memoria partendo dall’inizio e sceglie il primo blocco disponibile.


▪ Next-fit: scandisce la memoria partendo dalla locazione dell’ultima assegnazione.
▪ Best-fit: Ogni volta si alloca il blocco di memoria libero più piccolo che può contenere il processo.
▪ Worst-fit: Procedimento contrario del best-fit.

Lo schema di partizionamento dinamico genera (al momento della


deallocazione di un processo), buchi di memoria inutilizzabili. Per
questo motivo può anche diventare impossibile l’allocazione di
memoria richiesta da un processo, pur contenendo globalmente in
memoria un’area di dimensione sufficiente. Questo fenomeno si
chiama frammentazione esterna.

Una tecnica per superare la frammentazione esterna è la compattazione: periodicamente o in base alle
necessità, il SO sposta i processi in modo tale che essi siano contigui e tutta la memoria sia riunita in un
blocco. La compattazione può essere effettuata in due modi:

1. Spostamento selettivo: Ricerca di una strategia


ottima di movimento per compattare al meglio
la memoria. Questo consente di risparmiare
tempo negli spostamenti ma spreca molta CPU.
2. Spostamento globale: Tutte le partizioni
vengono riallocate ad uno dei due estremi della
memoria. Il vantaggio è che non richiede
strategie particolari

È possibile applicare la compattazione della memoria solo se il binding fra gli indirizzi logici e fisici è
effettuato a tempo di esecuzione.

Vantaggi:

▪ Richiede un supporto hardware modesto (compattazione frequente).


▪ Elimina la frammentazione interna ma produce quella esterna.
▪ Si adegua ad ambienti con carico non predicibile di lavoro.

Lo svantaggio è quello di non poter allocare processi il cui spazio richiesto è maggiore di quello disponibile.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 92 a 103

8.2 Gestione della memoria non contigua


Con gli schemi di gestione della memoria non contigui, il processo non deve obbligatoriamente occupare
uno spazio consecutivo in memoria. Molto spesso è utile spezzettare il processo in moduli o parti al fine di
rendere più efficiente la gestione della memoria centrale. Esistono vari tipi di allocazione non contigua:

▪ Segmentazione (soluzione ibrida)


▪ Paginazione
▪ Memoria virtuale

8.2.1 Segmentazione
La segmentazione è uno degli schemi di gestione della memoria che si basa sulla suddivisione della
memoria fisica disponibile in blocchi di lunghezza variabile detti segmenti.

Un programma potrebbe essere composto da un certo numero di segmenti secondo la


strutturazione dei moduli che lo compongono: un segmento per il main, uno per un
determinato sottoprogramma, un altro per i dati e così via. La segmentazione riflette la
visione che il programmatore ha del proprio programma. Invece con paginazione o
memoria virtuale, il processo viene suddiviso dal SO in maniera in modo casuale, senza
rispettare la strutturazione dei moduli. Questo è molto importante, perché utilizzando la
segmentazione, si semplificano notevolmente i criteri di condivisione.

Il compilatore struttura il modo in cui il processo accederà alla memoria, pensando


un’associazione della RAM con segmentazione. Non è detto che il compilatore decida di
creare un solo segmento per genere, ma è comune la scelta di prevedere più segmenti per
ridurre la dimensione in memoria del singolo segmento e quindi la frammentazione
esterna.

Poiché i segmenti usati possono avere lunghezza diversa, la segmentazione è simile al


partizionamento dinamico. La differenza è che, con la segmentazione, un programma può
utilizzare più di una partizione e le partizioni usate possono essere non contigue. Visto che il
processo è suddiviso in pezzi più piccoli, la frammentazione interna ed esterna è ridotta al
minimo. In particolare più i segmenti sono piccoli e minore sarà la frammentazione, di
conseguenza un minore utilizzo delle tecniche di compattazione.

▪ Partizionamento dinamico: vaso riempito con pietre grandi (perché il processo è caricato per intero).
▪ Segmentazione: vaso riempito con pietre piccole (processo spezzettato in parti piccole).

La segmentazione è uno schema di gestione della memoria ibrido, poiché condivide alcune proprietà:

▪ Degli schemi di allocazione contigui relativamente ad un singolo segmento, poiché i dati di ogni singola
entità devono essere posti in un’area contigua di memoria. Quindi bisogna tenere traccia degli estremi
di ogni segmento attraverso i soliti registri base e limite.
▪ Degli schemi di allocazione non contigui relativamente all’intero spazio di indirizzamento del processo,
poiché i blocchi logici diversi possono essere messi in segmenti non contigui.

Delle informazioni relative alla posizione dei vari segmenti associati al processo vengono memorizzate nella
tabella dei descrittori di segmento. In particolare, per ogni processo, il sistema operativo crea una TDS, e
per ogni TDS è memorizzato l’indirizzo fisico d’inizio e dimensione di ogni segmento associato al processo.
La TDS viene tenuta in memoria, in uno dei segmenti associati al processo. Due registri hardware RBTDS e
RLTDS, contengono l’indirizzo di base e limite della TDS associata al processo in esecuzione.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 93 a 103

Dato che TDS si trova in memoria (nei precedenti schemi i valori base e limite erano memorizzati nei registri
della CPU), la traduzione di ciascun indirizzo logico in fisico richiede due accessi in RAM:

1) Uno per accedere alla TDS e tradurre l’indirizzo del segmento da logico a fisico.
2) Il secondo accesso per accedere fisicamente alla locazione calcolata precedentemente.

La segmentazione, quindi, raddoppia il tempo per l’accesso ad una locazione di memoria. Se i descrittori di
segmento sono pochi, o se alcuni sono utilizzati più frequentemente, conviene memorizzarli in opportuni
registri hardware, riducendo drasticamente il processo di indirizzamento del segmento.

Il caricamento di un processo segmentato in memoria avviene nel seguente modo (allocazione):

1) Il processo viene suddiviso dal compilatore in segmenti.


2) Il SO cerca di allocare N partizioni adatte per N segmenti in cui è suddiviso il processo.
3) Il SO crea un descrittore di segmento registrandovi l’indirizzo fisico in cui è stato posto il segmento
(base) e la sua dimensione (limite).
4) L’insieme dei descrittori di segmenti di un processo viene memorizzato nella TDS.

Quando un processo subisce uno swapping, al ritorno in memoria bisogna aggiornare la sua TDS, perché
non è detto che rientri nelle stesse locazioni di partenza (generalmente si preferisce rigenerarla
totalmente). Anche l’eventuale compattazione richiede l’aggiornamento nella TDS delle righe relative ai
segmenti spostati.

Processo di traduzione indirizzi logici in fisici (indirizzamento)

Mentre nei precedenti schemi di gestione della memoria, l’indirizzo logico era costituito solo dallo
spiazzamento, con la segmentazione l’indirizzo logico è di tipo bidimensionale formato dall’id/numero del
segmento e spiazzamento. La memoria fisica, naturalmente, mantiene l’indirizzamento lineare, per cui è
necessario un meccanismo per la traduzione di indirizzi logici in fisici. Quando la CPU produce un indirizzo
logico (id/numero+spiazzamento):

▪ Il numero del segmento viene usato per accedere alla tabella dei segmenti. Nella tabella dei segmenti
viene identificata una coppia base e limite.
▪ Il parametro limite della tabella viene utilizzato per verificare lo spiazzamento prodotto dalla CPU. In
particolare, se lo spiazzamento è maggiore del limite significa che l’indirizzo a cui il processo sta
tentando di accedere non è di competenza del segmento. Se non supera il limite, lo spiazzamento viene
sommato all’indirizzo base relativo a quel determinato segmento.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 94 a 103

Meccanismi di protezione

Poiché ogni riga della tabella dei segmenti contiene una lunghezza e un indirizzo di base, un programma
non può inavvertitamente accedere ad una locazione di memoria il cui indirizzo superi l’indirizzo limite del
segmento. Essendo un processo suddiviso in segmenti, i cui elementi del segmento hanno delle
caratteristiche in comune, è possibile operare a livello del singolo segmento ed implementare dei criteri di
protezione dedicati.

Per esempio:

▪ Accesso in modalità execute (read-only) al segmento codice. Andare a modificare un codice in


esecuzione non ha molto senso e permettere ciò potrebbe generare un vulnerabilità sfruttabile da
attacchi hacker.
▪ Accesso in modalità read/write al segmento stack
▪ Determinati dati in read-only o read/write.

Il livello di protezione della segmentazione sono più elevati poiché è possibile definire dei criteri specifici
per ogni singolo segmento. I diritti di accesso possono essere registrati in un campo della TDS.

Meccanismi di condivisione

Gli oggetti condivisi sono collocati in segmenti dedicati e separati. Il segmento può essere mappato,
attraverso la TDS, nello spazio di indirizzamento logico di tutti i processi autorizzati ad accedervi. Lo
spiazzamento interno di un dato risulta identico per tutti i processi che lo condividono.

Linking dinamico: Dal momento in cui sto dividendo tutto in maniera logica, se ho delle parti di codice che
non vengono utilizzati frequentemente non ha senso tenerle in memoria (vengono collocate in un
segmento dedicato caricato all’occorrenza). Il linking dinamico indica il caricamento di una procedura in
fase di esecuzione di processo e solo su sua richiesta. Lo spazio in memoria per una procedura viene
occupato se e solo se quando tale procedura viene richiesta. La segmentazione consente di aggiungere
nuovi segmenti al processo richiedente, aggiornando la TDS e i registri base e limiti della TDS.

Vantaggi:

▪ Eliminando il vincolo della contiguità si rende più efficiente la gestione della RAM, poiché riduco la
frammentazione.
▪ Criteri di protezione e condivisione più efficienti.
▪ Supporta il linking dinamico.

Svantaggi:

▪ Necessità di effettuare comunque la compattazione che è resa più complicata (perché bisogna
compattare anche i segmenti).
▪ Il duplice accesso alla memoria deve essere supportato da un hardware apposito.
▪ La gestione della memoria da parte del SO è più complessa rispetto a quella per il partizionamento
statico e dinamico ( permessi, tabelle nello spazio di indirizzamento del processo, doppio accesso ...).
▪ Non rende possibile l’esecuzione di processi più grandi della dimensione fisica disponibile.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 95 a 103

8.2.2 Paginazione
RAM
La tecnica della paginazione permette di associare ad un processo
un’area di memoria che non è un segmento contiguo. La memoria Spazio logico
principale è suddivisa in blocchi casuali relativamente piccoli di
uguale dimensione, fissa, e che ogni processo sia a sua volta diviso
in piccoli blocchi della stessa dimensione. Il blocco di un processo,
detto pagina, può essere assegnato ad un blocco di memoria
disponibile, detto frame.

Tabella delle pagine

Una tabella delle pagine (TDP), associata ad ogni processo, contiene la locazione del frame corrispondente
ad ogni pagina del processo. Ogni indirizzo logico generato si compone di un numero di pagine e di uno
spiazzamento. La dimensione delle pagine è fissa e identica per tutti i processi ed è definita
dall’architettura; In generale è rappresentata da una potenza di 2 e la sua dimensione è normalmente
compresa tra 512 byte e 16MB. Supponiamo che:

▪ Dimensione di una pagina sia 2𝑛


▪ Dimensione totale della memoria logica associata ad un processo sia 2𝑚

Il numero di pagine sarà: 2𝑚 /2𝑛 = 𝟐𝒎−𝒏. (pagine richieste per includere 2𝑚 indirizzi differenti)

Esempio: Spazio logico del processo composto da 8 indirizzi ( 23 ) e ogni pagina può contenere 2 indirizzi
( 21 ). Sono richieste: 8/4 = 23−1 = 22 = 4 pagine.

Quanti bit servono rappresentare il numero di pagina? 𝒎 − 𝒏 bit.

Esempio: 22 pagine = 4. Quanti bit sono necessari per identificare in modo univoco 4 pagine? 2 bit, perché
con due bit è possibile rappresentare 4 diverse combinazioni: pag0=00, pag1=01, pag2=10, pag3=11

Ma l’indirizzo logico non è composto solo dal riferimento alla pagina, poiché essa è composta da diversi
elementi e ogni elemento è individuato da uno scostamento rispetto all’indirizzo base.

Quanti bit servono per rappresentare lo scostamento? Lo scostamento indica la posizione di un elemento
all’interno della singola pagina 2𝑛 . Per individuare in maniera univoca 2𝑛 elementi all’interno della pagina
servono n bit.

Complessivamente l’indirizzo logico è rappresentato da 𝑛𝑃𝑎𝑔𝑖𝑛𝑎 + 𝑠𝑐𝑜𝑠𝑡𝑎𝑚𝑒𝑛𝑡𝑜 = 𝑚 − 𝑛 + 𝑛 = 𝒎 bit.


L’indirizzo logico è composto dai primi 𝑚 − 𝑛 bit (più significativi) dal numero di pagina e 𝑛 bit (meno
significativi) dallo scostamento all’interno della singola pagina.

Esempio: Supponiamo di avere pagine di dimensione 512 byte e una RAM di dimensione 1MB, quante
pagine saranno necessarie? Innanzi tutto devo riportarmi a potenze di 2 nella stessa unità di misura:

▪ Pagina : 512 𝑏𝑦𝑡𝑒 = 29 𝑏𝑦𝑡𝑒


▪ Memoria logica: 1 𝑀𝐵 = 220 𝑏𝑦𝑡𝑒

Calcoli:

▪ Numero di pagine necessarie: 220 /29 = 220−9 = 211 pagine. Per rappresentare il numero di pagine
occorrono 11 bit.
▪ Per rappresentare lo scostamento occorrono 9 bit
▪ Per rappresentare l’indirizzo logico sono necessari 11 + 9 = 𝟐𝟎 bit.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 96 a 103

Indirizzamento paginazione

La traduzione dell’indirizzo da logico a fisico è ancora fatta dall’hardware del processore, che accede alla
tabella della pagine del processo corrente. In presenza di un indirizzo logico (numero pagina, scostamento),
il processore usa la tabella delle pagine per produrre un indirizzo fisico (numero del frame, scostamento).

Un meccanismo hardware divide l’indirizzo logico in 2 elementi per permettere la successiva


concatenazione. I bit più significativi dell’indirizzo logico, che rappresentano il numero di pagina, fanno
riferimento all’indice della tabella delle pagine in cui andare a prelevare l’indirizzo del frame desiderato.
L’indirizzo del frame espresso in bit viene concatenato con i bit meno significativi dell’indirizzo logico che
rappresentano lo scostamento dell’elemento desiderato nel frame.

Dal punto di vista logico la memoria riservata al processo è


vista come contigua e sequenziale ma dal punto di vista
fisica la memoria associata non è contigua e sequenziale.

Allocazione: Il SO deve sapere in ogni istante quanti e quali frame sono liberi: perché quando si genera un
nuovo processo o si carica un processo da memoria secondaria, con un operazione di swapping, il SO deve
fare dei controlli:

▪ Il SO deve verificare che il numero di frame liberi sia sufficiente a contenere tutte le pagine del
processo.
▪ Se il numero è sufficiente, per ogni pagina il SO crea una tabella delle pagine in cui, per ogni riga della
tabella, indica il numero di frame in cui verrà caricata. Man mano che le pagine vengono assegnate il SO
marca nella tabella dei frame liberi il frame appena assegnato.
▪ Infine, una volta che il SO ha trovato abbastanza frame liberi, costruita la tabella delle pagine,
aggiornata la tabella dei frame liberi, carica il processo in RAM.

Se il processo richiede uno spazio di indirizzamento non multiplo della dimensione delle pagine fisiche si
crea un fenomeno di frammentazione interna della pagina. Poiché anche se lo spazio di indirizzamento è
minore del frame di RAM, il SO alloca il frame per intero. Questo è comunque un fenomeno molto ridotto e
limitato all’ultima pagina associata al processo (poiché tutte le altre verranno riempite per intere).

Deallocazione: Quando un processo termina o subisce uno swapping, il SO restituisce le pagine fisiche,
precedentemente assegnate, ed elimina la relativa TDP.

Implementazione TDP
Poiché l’indirizzamento passa ogni volta dalla tabella delle pagine, quest’ultima deve essere efficiente per
consentire che venga individuato velocemente il dato richiesto dalla CPU. La tabella delle pagine può essere
implementata:

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 97 a 103

1. Attraverso registri della CPU: La CPU presenta dei registri dedicati in cui andare ad inserire gli indirizzi dei
frame in cui le pagine sono posizionate.

▪ PRO: la velocità di accesso è molto veloce, poiché i registri della CPU sono il tipo di memoria che
permette l’accesso più rapido all’informazione. Memorizzare la TDP nei registri della CPU permette di
ottenere dei context switch molto veloci, poiché se esiste un set di registri per ogni processo sarà molto
rapido lo switch fra questi.
▪ CONTRO: I registri sono una memoria molto costosa quindi il numero di registri che è possibile dedicare
alla TDP è limitato. Avere una TDP di piccola dimensione pone dei limiti sulla dimensione dei processi
che possono essere riferiti. Per questo motivo questa soluzione viene adottata solo per processi
particolari come quelli del sistema operativo.

2. Normalmente la TDP è implementata attraverso un registro (Page Table Base Register, PTBR) che
contiene un riferimento alla tabella delle pagine contenuta in RAM.

▪ PRO: Se bisogna effettuare un context switch, basta sostituire l’indirizzo del riferimento contenuto nel
registro con l’indirizzo di base della tabella delle pagine del processo entrante.
▪ CONTRO: I tempi di accesso in RAM raddoppiano perché ogni volta che bisogna effettuare un
indirizzamento, la CPU, dovrà effettuare due accessi in RAM: Il primo accesso per la ottenere il dato
dalla tabella delle pagine; il secondo accesso per accedere all’informazione attraverso l’indirizzo fisico
calcolato nel primo accesso.

3. Molte architetture mettono a disposizione del SO una cache (memoria ad accesso veloce) che si
frappone tra la CPU e la RAM. Questo tipo di cache prende il nome di Translation look-aside buffer (TLB),
ed è una tabella costituita da due colonne, una per le chiavi e una per i valori. L’accesso al TLB è rapidissimo
perché la consultazione viene fatta su tutte le coppie contenute nella tabella simultaneamente.

La chiave rappresenta il numero di pagina e il valore rappresenta il numero di frame. Dentro al TLB è
presente una parte della tabella delle pagine del processo in esecuzione. La parte della TLB presente in
cache è quella parte che riguarda soltanto le pagine che sono state utilizzate durante l’esecuzione del
processo fino a quel momento. Se la CPU ha prodotto almeno un indirizzo logico per la pagina x, allora, il
riferimento al frame e il numero di pagine viene memorizzato all’interno del TLB.

È utile avere un riferimento ad una pagina già indirizzata in cache perché i processi eseguono per località;
quindi, tentano a utilizzare le stesse pagine o pagine vicine. Se la CPU ha prodotto un indirizzo logico che fa
riferimento ad una pagina nel prossimo futuro, con molta probabilità, continuerà a produrre indirizzi che si
riferiranno alla stessa pagina. (principio di località temporale)

Esempio: la CPU che esegue il codice di una funzione, fino a quando la funzione non termina il processore
continuerà a lavorare con parole di memoria appartenenti sempre a quella funzione.

Quindi, dentro al TLB, ci sono le informazioni delle pagine che il processo sta utilizzando, che possono
essere disposte anche in ordine sparso (perché dipende da come il processo le richiede). Per questo motivo
non basta memorizzare solo il riferimento al numero del frame ma anche il numero di pagine, al fine di
poter ricostruire in maniera corretta l’indirizzo fisico.

Quando la CPU produrrà un indirizzo logico, verificherà in primis se il numero di pagina prodotto è presente
all’interno della TLB (ricerca veloce e parallela su tutte le righe) e successivamente, se la ricerca non ha
avuto successo, la CPU andrà a controllare la tabella delle pagine in RAM. Se l’accesso la TLB fallisce (non è
presente la pagina richiesta ) allora:

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 98 a 103

▪ Se c’è spazio nel TLB si inserisce la nuova coppia <pagina, frame> individuata in RAM.
▪ Se non c’è spazio, si sostituisce una coppia già presente con quella nuova, di solito viene scelta per la
sostituzione la coppia usata meno di recente.

Quando lo scheduler identifica un processo da rendere running allora bisognerà attuare un meccanismo di
protezione che impedisca al nuovo processo di accedere alle pagine di un altro. Ci sono due soluzioni:

1. Se si identifica il numero di pagina solo con questo valore allora potrebbe verificarsi una
sovrapposizione di numeri, poiché più processi, potrebbero avere lo stesso numero di pagina di altri
(visto che le pagine di ogni processo partono da 0). Alcuni TLB arricchiscono l’informazione contenuta in
essi, aggiungendo un identificatore univoco dello spazio degli indirizzi di un processo.
2. Il dispatcher svuota il TLB ad ogni context switch.

Criteri di protezione e condivisione delle pagine

Un aspetto ulteriore concerne la modalità di accesso alle pagine, memorizzata nella tabella delle pagine. Ad
ogni riferimento nella TDP viene aggiunto un ID di protezione che rappresenta la modalità in cui è stata
etichettata la pagina. Una pagina può essere etichettata come:

▪ Solo scrittura: in questo caso solo il processo proprietario della pagina può effettuare scritture.
▪ Solo lettura: in questa modalità permette di ottenere una pagina condivisibile in modo che più processi
possano leggere la stessa pagina senza creare problemi. Esempio pagina librerie condivise.
▪ Lettura/scrittura: Permette ai processi di condividere delle pagine in modo da realizzare il modello di
scambio di informazioni noto come memoria condivisa. La memoria condivisa è implementabile
attraverso frame condivisi che corrispondono a pagine accessibili in modalità lettura/scrittura.

Aggiungendo questa informazione si rende il modello più flessibile e si permette comunque al SO di


effettuare controlli. Se una certa pagina è dichiarata come accessibile solo scrittura, e se un processo non
autorizzato cerca di scriverci, quest’ultimo compie un’operazione illegale e viene bloccato dal SO.

Il registro limite della TDP (contiene il numero di pagina più alto) permette di implementare dei meccanismi
di protezione per bloccare tentativi di accesso a pagine oltre il limite assegnato.

Swapping: Lo swapping ci permette di avere lo scheduling e


medio termine. Consiste nel sospendere un processo e
spostarlo su disco in aree di swap (swap out) e riportato in
RAM quando necessario (swap in). Affinché questo
meccanismo possa avvenire in maniera efficiente è
necessario che il codice sia rilocabile, ovvero che l’esecuzione
del processo sia indipendente dalla posizione effettivamente
occupata in RAM. Se il processo effettua uno swap in non è
detto che venga riportato in RAM nella stessa posizione che
occupava precedentemente e, se il codice è rilocabile
l’operazione è efficiente e veloce.

Questo è possibile poiché è presente un mapping tra indirizzi logici e fisici mediato dalla tabella delle
pagine. Quando il processo viene portato in memoria principale (swap in) il SO riconosce quante pagine
necessita il processo; quindi, predispone all’interno della RAM uno spazio per contenere la tabella delle
pagine (una per ogni pagina). Per ciascuna pagina, il SO dovrà cercare un frame libero e caricare l’indirizzo
all’interno della tabella delle pagine. La CPU non si accorge dei frame aggiornati poiché essi sono
trasparenti.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 99 a 103

Il meccanismo di mapping che passa attraverso la tabella delle pagine permette di ottenere la rilocazione,
poiché cambiando il numero di frame all’interno della tabella delle pagine, la CPU continua il suo operato
senza intoppi.

Vantaggi Paginazione:

▪ Elimina il vincolo della contiguità.


▪ Frammentazione interna ridotta.
▪ Frammentazione esterna eliminata.
▪ Criteri di protezione e condivisione più efficienti.

Svantaggi:

▪ La gestione della memoria è più complessa.


▪ Non rende ancora possibile l’esecuzione di processi più grandi delle dimensioni fisiche della memoria.

8.2.3 Memoria virtuale


La memoria virtuale è uno schema che evolve la paginazione, con la differenza che un processo può andare
in running anche se del processo in RAM viene caricato solo una parte (il resto su disco). Con questo tipo di
gestione si ha che lo spazio degli indirizzi logici di un processo attivo può superare la capacità della memoria
fisica.

Come per la paginazione, per i programmatori la memoria virtuale è trasparente: Il programmatore non
deve fornire dettagli sui metodi di gestione della memoria o di adeguare il programma per una memoria
limitata.

Vantaggi diretti:

▪ Riducendo la RAM necessaria a ciascun processo è possibile eseguire molti più programmi
contemporaneamente, aumentando il grado di multiprogrammazione.
▪ Maggiore libertà al programmatore; un programmatore che scrive un programma, che per essere
eseguito deve essere caricato per intero in RAM, ha un vincolo di memoria: Il programmatore quando
scrive un programma deve scrivere un codice che sia possibile caricare nella RAM disponibile: Se la
RAM del sistema ha una capacità pari ad M, il processo P non deve richiedere più di M quantità di
memoria. Questo richiede una maggiore attenzione del programmatore. Con la memoria virtuale il
vincolo decade.
▪ Liberando i programmatori dal vincolo di memoria è possibile eseguire un programma su un computer
con poca memoria e vederlo comunque funzionare (portabilità).
▪ Migliore utilizzo della RAM: Un programma non utilizza tutte le funzioni di una libreria, perché caricarle
tutte in RAM? Spesso i vettori o matrici sono sovradimensionati, perché non allocare lo spazio solo se
necessario?
▪ Meno tempo per fare swap, poiché banalmente si deve swappare meno dati/codice.
▪ Avvio dei processi più rapido, poiché il caricamento in RAM include solo una parte del programma.

Svantaggi diretti:

▪ Il SO deve tener traccia delle pagine di programma caricati in RAM e su disco.


▪ La CPU potrebbe generare indirizzi logici che non possono essere indirizzati poiché il loro riferimento in
memoria non esiste (esiste su disco). Bisogna prevedere dei meccanismi che permettono al SO di
avviare un processo di caricamento del codice del processo mancante in RAM.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 100 a 103

La memoria virtuale richiede l’implementazione di due meccanismi: algoritmo di distribuzione dei frame
liberi ai processi e l’algoritmo di sostituzione delle pagine.

Distribuzione frame: Quando l’utente avvia l’esecuzione di un programma e genera un nuovo processo P, il
SO deve capire quanti frame di RAM assegnare a P. Tolto il vincolo di totalità, quindi, non verranno
assegnati tutti i frame che servono a coprire tutto lo spazio degli indirizzi logico del processo, però, rimane
l’incognita di quanti frame assegnare.

Sostituzione pagine: Quando tutti i frame del processo sono caricati con delle pagine in RAM, se il processo
richiede una pagina che risiede su disco ma tutti i frame assegnati al processo sono pieni, come si può
trovare spazio?

Strategia di ricerca

Occorre definire un meccanismo per mantenere traccia delle pagine in RAM e delle pagine in memoria
secondaria. Ecco che la tabella delle pagine viene arricchita con un bit di validità, che indica quali sono le
pagine caricate in un frame di RAM o quali sono le pagine memorizzate su disco.

Se il processo tenta di accedere a una pagina non valida si genera un evento (page fault) che viene
segnalato al sistema operativo. Il SO accede alla tabella delle pagine per verificare se la richiesta che ha
generato il page fault è una richiesta lecita: il processo potrebbe voler accedere ad un area di memoria
riservata ad un altro processo. Se la pagina non appartiene allo spazio degli indirizzi del processo, il
processo viene terminato. Altrimenti vuol dire che la pagina richiesta non è ancora stata caricata in RAM,
allora il SO:

1. Identifica un frame libero adatto per la pagina. Se non esiste il SO cerca o sostituisce una pagina, in
base a degli algoritmi di sostituzione, o sospende il processo.
2. Richiede una copiatura da disco della pagina desiderata. Per tutto il tempo necessario per effettuare
l’operazione, il processo passa allo stato di waiting.
3. Ad un certo punto l’operazione termina, il SO aggiorna la tabella delle pagine (bit di validità) e il
processo passa allo stato di ready.
4. Quando il processo torna allo stato di running verrà riavviata l’operazione interrotta dal page fault.

Il page fault può occorrere in momenti diversi dell’esecuzione di un’istruzione. Consideriamo per esempio
l’istruzione 𝐴𝐷𝐷 𝐴 𝐵 𝐶 (equivale a 𝐶 = 𝐴 + 𝐵):

▪ Il page fault può verificarsi all’atto del caricamento dell’istruzione: Durante il fetch dell’istruzione la
CPU cerca di accedere ad un indirizzo logico che corrisponde ad una pagina invalida (non in RAM).
▪ Il page fault può verificarsi quando si cerca di accedere a un operando: L’istruzione risulta accessibile
ma quando si accede all’indirizzo dell’operando, l’operando è contenuto in una pagina non in RAM.
▪ Il page fault può verificarsi quando si deve salvare il risultato in C: L’indirizzo della locazione di memoria
in cui salvare il risultato dell’operazione non è in RAM.

In generale, l’interruzione del ciclo fetch-decode-execute causa il riavvio del ciclo stesso. Per questo
motivo, la memoria virtuale può essere implementata solo su sistemi che supportano l’interrompibilità
della singola istruzione.

Strategia di allocazione: Paginazione su richiesta

Quando viene avviato un programma, il processo viene avviato con 0 frame assegnati. Quando la CPU
cercherà di eseguire la prima istruzione del codice produrrà un page fault che causerà il caricamento della
prima pagina in memoria. La paginazione su richiesta è un meccanismo che carica in memoria una pagina
residente su disco solo nel momento in cui è realmente utilizzata. Ogni istruzione può causare diversi page
fault, di conseguenza, il tempo di esecuzione potrebbe moltiplicarsi a dismisura.
Corso di studi in Informatica (A-L) A.A. 2021/2022
Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 101 a 103

Questa scelta non è molto efficiente, se fosse possibile assegnare qualche frame di RAM caricando le prime
pagine di codice di un processo (main) ecco che si adotterebbe una soluzione migliore. In ogni modo,
l’esecuzione deve essere avviata, è un po' inutile aspettare che il SO segnali un page fault per la prima
istruzione poiché è prevedibile. È possibile agire in maniera preventiva assegnando un numero minimo di
pagine.

▪ L’allocazione di un numero di pagine molto grande riduce il numero di processi attivi.


▪ L’allocazione di un numero di pagine troppo ridotto produce un aumento dei page fault e lo spreco di
CPU.

Un buon algoritmo di allocazione deve variare dinamicamente il numero di pagine allocate ad un processo,
in base alle sue richieste ed allo stato del sistema tenendo presente:

▪ Il limite inferiore: sotto il quale l’aumento dei page fault aumenta rapidamente.
▪ Il limite superiore: oltre il quale l’allocazione di altre pagine produce un miglioramento delle prestazioni

Algoritmi troppo semplici possono provare il trashing: eccessivo scambio di pagine di dati tra la RAM e la
memoria secondaria. Si ha trashing quando i processi hanno a disposizione meno frame del numero di page
attive, quindi, l’attività della CPU tende a diminuire. I processi tendono a rimanere in attesa dello swap.

Algoritmo di sostituzione FIFO (first in first out)

Ad ogni pagine nella TDP viene associato un marcatore temporale che identifica l’instante in cui è stata
caricata in memoria. Se tutti i frame associati al processo sono occupati e la CPU deve effettuare uno swap,
il SO sceglie di sostituire la pagina caricata da più tempo in memoria.

Esempio: Supponiamo di avere una RAM con soli 3 frame associati ad un processo P. La CPU produce la
seguente sequenza di riferimenti 7,0,1,2,0,3,0,4,2 (numeri di pagina): la CPU produce una sequenza di
indirizzi in cui richiede la pagina 7, poi la pagina 0, poi 1 ec... Supponiamo che la RAM sia allocata secondo
uno schema di paginazione su richiesta, quindi, inizialmente i 3 frame sono vuoti:

I1: I primi 3 riferimenti causano 3 page fault con conseguente caricamento delle pagine (7,0,1) in memoria
e definizione dei relativi marcatori temporali (7-T0, 0-T1, 1-T1).

I2: Quando il processo chiede la pagina 2, quest’ultima non è caricata in memoria, però a differenza di
prima il processo ha raggiunto il massimo numero di frame che il SO gli consente di avere. In questo
momento scatta l’algoritmo di sostituzione FIFO. L’algoritmo scandisce la tabella per cercare quella pagina
che è caricata da più tempo in memoria. Visto che 𝑇0 < 𝑇1 < 𝑇2, la pagina 7 viene sostituita.

I3: A questo punto, viene richiesta la pagina 0 che è già in RAM (il marcatore temporale non cambia).

I4: Successivamente viene richiesta la pagina 3 e sostituita la pagina 0, poiché è quella pagina caricata da
più tempo in memoria RAM.

I5: Viene referenziata la pagina 0 e sostituita con la pagina 1. E così via...

Istante 0 Istante 1 Istante 2 Istante 3 Istante 4 Istante 5 Istante 6 Istante 7


Vuoto Pag. 7, T0 Pag. 2, T3 Pag. 2, T3 Pag. 2, T3 Pag. 2, T3 Pag. 4, T6 Pag.4, T6
Vuoto Pag. 0, T1 Pag. 0, T1 Pag. 0, T1 Pag. 3, T4 Pag. 3, T4 Pag. 3, T4 Pag.2, T7
Vuoto Pag. 1, T2 Pag. 1, T2 Pag. 1, T2 Pag. 1, T2 Pag. 0, T5 Pag. 0, T5 Pag.2, T5

Totale page fault: 8

▪ Vantaggio: FIFO è una tecnica molto semplice da realizzare ma non sempre si comporta in modo
ottimale.
Corso di studi in Informatica (A-L) A.A. 2021/2022
Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 102 a 103

▪ Svantaggio: Il fatto che una pagina sia stata caricata tempo fa non significa che non sia più in uso, al
contrario potrebbe essere una pagina di uso frequente: rimuoverla causerebbe sicuramente un
successivo page fault (principio di località temporale).

Algoritmo di sostituzione LRU

L’algoritmo RLU (least-recently-used, meno recentemente usato) è simile all’algoritmo FIFO, ma anziché
basare la scelta sul tempo di caricamento la basa sul tempo di utilizzo (istante di ultimo utilizzo). La pagina
meno utilizzata recentemente è quella che ha minore probabilità di essere referenziata in futuro.

Principio di utilizzo:

▪ Quando una pagina viene caricata il SO associa un marcatore temporale che identifica l’istante in cui è
stata utilizzata.
▪ Quando una pagina viene referenziata, ma è già presente in memoria, il SO aggiorna il suo marcatore
con il valore dell’ultimo istante d’utilizzo.
▪ Quando tutte le pagine sono allocate e il SO deve effettuare uno swap cerca di sostituire la pagina che è
stata meno utilizzata, cioè il suo marcatore deve essere minore di tutti gli altri.

Seguendo l’esempio precedente cioè 3 frame liberi e una sequenza di richieste 7,0,1,2,0,3,0,4,2:

I1: Per le prime 3 pagine (7,0,1) richieste si genereranno 3 page fault in sequenza. Ad ogni pagina caricata in
memoria viene associato un marcatore temporale, rispettivamente T0, T1, T2.

I2: Quando viene richiesta la pagina 2 causerà un page fault. La pagina 2 viene caricata al posto della pagina
7 (𝑇0 < 𝑇1 < 𝑇2), associando il marcatore temporale (T3).

I3: La richiesta successiva è per una pagina già presente in memoria, cioè la pagina 0. Con l’algoritmo FIFO,
la richiesta di una pagina già caricata non cambiava il marcatore temporale. Con l’algoritmo LRU, quando la
pagina viene richiesta il marcatore viene aggiornato (da T1 a T4), per tenere traccia dell’ultimo istante in cui
la pagina è stata richiesta.

I4: Successivamente viene richiesta la pagina 3: in FIFO, in I4 la pagina sostituita era la pagina 0 (risultava
caricata da più tempo), invece con LRU si cerca la pagina utilizzata meno di recente quindi la pagina 1.

I5: Viene richiesta la pagina 0 che è già presente in memoria; quindi, si aggiorna solo il relativo marcatore
temporale. Di conseguenza la pagina 0 non genererà un page fault (come nel caso FIFO). E così via..

Istante 0 Istante 1 Istante 2 Istante 3 Istante 4 Istante 5 Istante 6 Istante 7


Vuoto Pag. 7, T0 Pag. 2, T3 Pag. 2, T3 Pag. 2, T3 Pag. 2, T3 Pag.4, T7 Pag.4, T7
Vuoto Pag. 0, T1 Pag. 0, T1 Pag. 0, T4 Pag 0, T4 Pag. 0, T6 Pag. 0, T6 Pag.0, T6
Vuoto Pag. 1, T2 Pag. 1, T2 Pag. 1, T2 Pag. 3, T5 Pag. 3, T5 Pag. 3, T5 Pag.2, T8

Totale page fault: 7

▪ Vantaggio: Più una singola pagina verrà referenziata e minore sarà il numero di page fault.
▪ Svantaggio: La ricerca delle pagine è sequenziale, cioè in ogni istante bisogna controllare il marcatore di
ogni pagina nella TDP.

Una possibile soluzione al problema della ricerca sequenziale è mantenere uno stack di pagine. Tutte le
volte, in cui la CPU fa riferimento ad una pagina, quest’ultima viene inserita in cima allo stack. La pagina in
fondo allo stack è sempre quella usata meno di recente, quindi si semplifica la ricerca. L’organizzazione
dell’LRU è tale da richiedere un supporto hardware sofisticato e dedicato alle operazioni relative
all’aggiornamento dello stack per non inferire sulle prestazioni della CPU.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka
Architettura degli elaboratori e Sistemi operativi Pag. 103 a 103

Algoritmo di sostituzione Belady

L’algoritmo di Belady è l’algoritmo perfetto poiché sostituisce le pagine che saranno referenziate nel più
lontano futuro. Così facendo l’algoritmo generebbe solo i page fault strettamente necessari ma non è
realizzabile poiché è difficile prevedere quali pagine verranno referenziate in futuro. È possibile stimare il
comportamento di un gruppo di processi ma è importante ricordare che:

▪ Il flusso d’esecuzione non è lineare (ramificazioni, goto).


▪ Tempo computazione necessario per stimare l’andamento molto alto.

Di conseguenza il Belady è di importanza teorica poiché consente di confrontare le prestazioni degli


algoritmi.

Algoritmo di sostituzione NRU

L’algoritmo NRU combina il basso costo FIFO con la registrazione delle pagine residenti tipico dell’LRU
sostituendo le pagine non recentemente usate. Principio di funzionamento:

▪ Associa 1 bit a ciascuna pagina nella TDP, inizializzato a 0, che viene posto ad 1 quando la CPU genera
un indirizzo logico appartenente alla pagina.
▪ Ogni tot i bit di tutte le pagine vengono resettati.

Nel momento in cui si dovesse verificare la necessità di effettuare una sostituzione si andrà a sostituire le
pagine con bit a 0.

Working set

Per evitare il trashing bisogna assicurare che un


processo abbia sempre il numero di frame
necessari allocati. Esistono diverse tecniche, tra
cui la strategia del working set che osserva
quanti frame sta attualmente usando il processo.

Il working set di un processo P (nell’istante T) è


l’insieme di pagine ∆(𝑡) indirizzate da P nei più
recenti ∆ riferimenti. ∆ definisce la finestra di
lavoro del working set. Ad esempio: ∆= 7.

Il sistema mantiene il working set di ogni processo in memoria aggiornandolo dinamicamente, in base al
principio di località temporale:

▪ All’istante t vengono mantenute le pagine usate dal processo nell’ultima finestra ∆.


▪ Le altre pagine (esterne al ∆) possono essere sostituite.

La dimensione del working set è dinamica poiché dipende dai processi referenziati. Se in un ∆ sono
referenziate le pagine 3,4,5,3,4 nel working set verranno mantenute le pagine 3,4,5 (dimensione =3). Se le
pagine referenziate sono differenti la dimensione del working set differisce.

Il parametro ∆ caratterizza il working set, esprimendo l’estensione della finestra dei riferimenti:

▪ ∆ piccolo: il working set non è sufficiente a garantire località e a contenere il numero di page fault.
▪ ∆ grande: allocazione di pagine non necessarie.

La teoria del working set suggerisce che un programma deve essere in esecuzione se il working set è in
memoria e una pagina di memoria può essere rimpiazzata solo se non fa parte del working set. La
determinazione accurata del working set richiede l’intervento del SO dopo ogni accesso in memoria.

Corso di studi in Informatica (A-L) A.A. 2021/2022


Appunti di Giovanni Cirigliano - @cirigka

Potrebbero piacerti anche