Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
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
▪ 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.
Esempi:
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.
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
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.
▪ Se 0 il segno è +
▪ Se 1 il segno è −
[−(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:
▪ 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
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.
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)]
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].
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.
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.
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.
▪ 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.
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:
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 gli 8 bit dell’esponente valgono 101000112 = 16310 , l’esponente vale 163 − 127 = +36
Numeri normalizzati
Un numero normalizzato espresso in floating point è definito come segue: ±𝟏, 𝒙𝒙𝒙𝟐 × 𝟐𝒚𝒚𝒚
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:
▪ 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)
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.
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.
Conclusioni:
▪ 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 ±∞.
▪ 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:
Somma/sottrazione:
Prodotto/divisione :
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 :
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:
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.
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.
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.
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.
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.
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.
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.
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:
𝑇𝐶𝑂𝑁𝑉𝐸𝑁𝑍𝐼𝑂𝑁𝐴𝐿𝐸 𝑁∙(𝐾𝑇)
Velocità esecuzione: 𝑇𝑃𝐼𝑃𝐸
= [𝐾+(𝑛−1)]∙𝑇 dove:
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.
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.
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.
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.
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.
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.
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).
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:
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.
▪ 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
▪ Traccia: sequenza circolare di bit letti/scritti mentre il disco compie una rotazione completa.
▪ Settore: Ogni traccia è suddivisa in settori.
▪ 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:
▪ Aumento di prestazioni
▪ Aumento di capacità di memorizzazione disponibile
▪ Aumento tolleranza ai guasti
▪ Migliore 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.
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.
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.
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.
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.
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.
In breve:
Definizioni:
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.
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:
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:
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:
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.
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
Il primo bit di controllo deve controllare la parità delle posizioni 1,3,5,7,9,11 ; di conseguenza:
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:
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
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:
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.
Pos1 Pos2 Pos3 Pos4 Pos5 Pos6 Pos7 Pos8 Pos9 Pos10
0 1 1 0 0 1 1 0 0 0
▪ 1001101
▪ 0010100
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.
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.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.
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:
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.
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.
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.
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.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).
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.
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.
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:
▪ 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à.
▪ 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.
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.
▪ 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:
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:
▪ 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.
Cioè è possibile inserire i dati in questa pila e il dato in cima alla pila è il primo dato che può essere letto.
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.
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.
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.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:
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.
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)
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.
▪ 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: 𝐴𝐵 + 𝐶𝐷 −/
Istruzioni:.
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:
Esercizi:
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.
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:
• 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
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 .
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.
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.
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).
▪ 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
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.
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.
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.
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:
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.
È 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.
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.
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.
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.
• 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.
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.
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.
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:
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 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.
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.
SOLUZIONE: Usare l’aging (invecchiamento), ovvero, al passare del tempo in coda ready, la priorità del
processo viene aumentata.
La schedulazione Round Robin fornisce una buona condivisione delle risorse del sistema:
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.
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: 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.
Ogni coda può adottare un proprio algoritmo di schedulazione. Avendo più code di schedulazione ci sarà
bisogno di uno scheduling delle code:
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:
Scheduling:
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.
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.
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.
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.
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).
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.
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).
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é:
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:
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.
▪ 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.
▪ 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.
▪ 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
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
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.
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.
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.
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.
In un sistema multiprocessore si verifica lo stesso problema di protezione delle risorse condivise e valgono
le stesse soluzioni.
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. 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.
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:
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.
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ù:
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.
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.
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.
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
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.
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).
Swap
L’uso di un’istruzione macchina speciale per garantire la mutua esclusione ha vari vantaggi:
▪ 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.
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.
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:
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.
A un certo punto il processo 2 (o 1) esegue la wait, mentre il primo processo è ancora in sezione critica:
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:
Quando anche il processo 2 (o 1) terminerà la sezione critica eseguirà la signal, portando a 1 il contatore 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).
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).
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.
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.
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ò;
I processi, quindi, sono “congelati” nella situazione di stallo a meno di un intervento da parte del SO.
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:
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:
Le righe delle matrici rappresentano i processi mentre le colonne il numero di risorse per tipologia.
Esempio:
Notazioni:
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.
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.
Quando lo stallo è stato rilevato, serve una strategia di ripristino; Ci sono vari approcci possibili:
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.
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.
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.
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).
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:
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.
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.
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.
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).
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:
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.
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.
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.
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.
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
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.
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.
▪ 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.
▪ 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:
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.
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:
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.
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:
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.
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.
Gli algoritmi di rilocazione più comuni per selezionare un’area di memoria libera all’interno della quale
creare una partizione sono:
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:
È possibile applicare la compattazione della memoria solo se il binding fra gli indirizzi logici e fisici è
effettuato a tempo di esecuzione.
Vantaggi:
Lo svantaggio è quello di non poter allocare processi il cui spazio richiesto è maggiore di quello disponibile.
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.
▪ 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.
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.
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.
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.
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:
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.
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.
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:
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.
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.
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:
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.
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).
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:
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:
▪ 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.
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.
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.
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.
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:
Svantaggi:
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:
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.
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.
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.
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.
▪ 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).
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..
▪ 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.
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:
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
Il sistema mantiene il working set di ogni processo in memoria aggiornandolo dinamicamente, in base al
principio di località temporale:
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.