La gestione della memoria
Il secondo livello
del modello a macchine virtuali simula l’esistenza di una pluralità di unità di
memoria centrale, ciascuna associata ad uno dei processi generati dal nucleo.
Si consideri il
seguente esempio che illustra le modalità attraverso le quali viene realizzata
la convivenza dei processi all’interno della RAM. Si abbia una RAM di 2000
locazioni o indirizzi, ciascuno dei quali può contenere un’istruzione in
linguaggio macchina, 500 dei quali sono occupati dal sistema operativo. Restano
disponibili 1500 indirizzi per i processi degli utenti. Un modo di dividere
questi indirizzi consiste nel ripartirli in blocchi di dimensioni predefinite,
per esempio 10 blocchi di 150 indirizzi. Ciascun blocco può essere libero o
occupato da un programma di dimensioni inferiori a 150 istruzioni. Una tabella
contiene le informazioni relative a ciascun blocco: se sia libero o occupato, e
da quale programma. Quando si richiede l’esecuzione di un programma, il sistema
operativo consulta la tabella e lo carica in un blocco libero, mentre quando un
programma termina provvede a liberare il blocco corrispondente.
|
Blocco
|
Programma
|
|
1
|
P1
|
|
2
|
P2
|
|
3
|
P3
|
|
4
|
0
|

Questo metodo viene
detto a partizioni fisse ed è il più semplice da realizzare, ma presenta due
limiti fondamentali:
- Il numero massimo
di processi che possono essere allocati in memoria è prefissato;
- Un blocco
contenente un processo molto breve viene considerato comunque occupato e perciò
lo spazio rimanente risulta sprecato.
Questi problemi
sono evitati se non si impone un insieme fisso di partizioni, ma si caricano i
programmi ovunque ci sia spazio sufficiente ad accoglierli. All’inizio il
caricamento avviene in sequenza, ma al procedere dell’attività, lo scaricamento
dei programmi terminati genera spazi di lunghezza variabile in posizioni
casuali della memoria. Ne consegue che il sistema operativo deve usare una
tabella più complessa della precedente, contenente informazioni
sull’allocazione dei singoli processi e sugli spazi liberi disponibili.
|
Indirizzo iniziale Lunghezza Contenuto
|
|
Sistema
Operativo
|
|
P1
|
|
Inutilizzato
|
|
P3
|
|
Inutilizzato
|
|
P2
|
|
Libero
|
|
500
|
100
|
P1
|
|
|
|
|
|
620
|
184
|
P3
|
|
|
|
|
|
1008
|
224
|
P2
|
|
|
|
|
Questa seconda
politica di uso della RAM è detta a partizioni variabili e sfrutta meglio della
precedente la risorsa, al prezzo però di un maggior carico di lavoro svolto dal
sistema operativo. La scelta della partizione dove caricare un nuovo programma
può essere fatta:
- Si carica nella
prima zona libera sufficientemente grande per contenere il programma, usando
l’algoritmo first – fit;
- Si carica nella
zona di memoria più piccola tra quelle adatte a contenere il programma con
l’algoritmo best – fit.
La seconda
soluzione è più lenta della prima, ma più razionale, in quanto mantiene ampi
spazi liberi per quanto più tempo possibile.
In realtà,
l’analisi statistica sui due algoritmi ha mostrato come il punto 1 risulti
essere più efficiente del punto 2: infatti si perde meno tempo usando il primo
posto libero e creandolo se non si trova, piuttosto che cercando tutte le volte
il posto migliore.
Quale che sia la
soluzione scelta, nel tempo si vengono a formare spazi vuoti tra un programma e
l’altro, che sono troppo piccoli per ospitare un programma e che costituiscono
perciò uno spreco.
Si può ovviare a
questo inconveniente ricompattando il codice in memoria quando la situazione
peggiora oltre un limite fissato, per esempio quando ci sono più di x spazi
liberi di dimensione inferiore a y locazioni.
Finora si è
supposto implicitamente che il caricamento di un programma in memoria riguardi
tutto il suo codice: questo però non è sempre possibile e, se anche lo fosse,
può non essere necessario.
Nel caso di
programmi molto grandi di dimensioni confrontabili con quelle della RAM, la
loro allocazione sarebbe problematica e, una volta avvenuta, monopolizzerebbe
la risorsa. Se poi la dimensione del programma fosse maggiore, la sua
allocazione non sarebbe possibile. Si adottano perciò tecniche dette di
funzionamento overlay (sovrapposizione), basata sull’osservazione che è
sufficiente avere in RAM una porzione per volta di ciascun programma, lasciando
il resto in memoria di massa, caricando altre porzioni solo quando necessario,
e liberando memoria quando non servono più. La zona del disco riservata per
questa gestione si chiama area di swap. Concretamente, questo significa usare
parti del disco (memoria lenta) per memorizzare informazioni che dovrebbero
risiedere in RAM (memoria veloce), ma che in un certo momento non sono
necessarie. La RAM
virtualmente disponibile diviene allora più grande. I processi hanno a
disposizione la RAM che serve, anche se solo virtuale, in quanto non
corrisponde alla memoria fisica. In questo contesto, il caricamento di un
programma viene realizzato con l’allocazione della prima porzione e delle
informazioni relative alla localizzazione, sui supporti di memoria di massa,
delle porzioni restanti. I programma che realizzano questi meccanismi (sistema
operativo) si collocano al secondo livello del modello onion skin e generano
una macchina virtuale che assegna una memoria centrale di dimensioni quasi
arbitrariamente grandi ad ogni processo: il limite superiore di tali dimensioni
è costituito dalla capacità dei dischi, molto superiore a quella della RAM
fisica.
Ovviamente la
memoria virtuale impone un rallentamento nell’esecuzione dovuta ai
trasferimenti di codice dal disco: se possono essere eseguiti parallelamente
alla normale attività del sistema l’effetto complessivo sull’efficienza è però
minimo.
Comunque nel
progetto del sistema operativo è qualificante l’uso di algoritmi che
minimizzano il numero di tali trasferimenti in modo da evitare, per quanto
possibile, le interruzioni dell’elaborazione.
La gestione della memoria
virtuale si realizza attraverso due diverse tecniche di suddivisione di
programmi in blocchi: la paginazione e la segmentazione.
Paginazione
Ogni programma
viene considerato diviso in blocchi di diverse dimensioni detti pagine
logiche; analogamente la memoria centrale viene divisa in pagine fisiche
di dimensioni uguali a quelle delle pagine logiche. Una tabella riassume la
situazione in cui si trovano le varie pagine di ogni programma. In particolare,
per ogni pagina occorre sapere:
- Se occupa una
pagina fisica
- La pagina
occupata
- La posizione sul
disco della pagina logica
- Altre
informazioni come il flag (costituito da 1 bit) che indica se la pagina è stata
modificata.
Ad ogni accesso
alla memoria si verifica se la pagina richiesta è presente in RAM e, se non lo
è, il sistema operativo provvede a caricarla in una pagina fisica libera. Se
tutte le pagine fisiche sono occupate, occorre scaricare una delle pagine
presenti in RAM (swapping). Per migliorare l’efficienza del sistema è opportuno
che la pagina che si decide di scaricare non venga richiesta subito dopo: la
decisione riguardo a quale pagina debba essere scaricata è un esempio di
politica di gestione; una scelta ragionevole potrebbe essere quella di
scaricare una pagina che da un certo tempo non viene richiesta, supponendo che
non lo sarà ancora per un po’. Si può allora aggiungere, nella tabella, un flag
per ogni pagina, che viene azzerato periodicamente dal sistema (per esempio
ogni secondo) e, posto questo uguale ad 1 quando la pagina viene richiesta
Quando è necessario scaricare
una pagina, il sistema ne sceglie una tra quelle col segnale e il segnalatore
azzerato.
|
P
|
IF
|
ID
|
M
|
|
...
|
...
|
...
|
...
|
|
...
|
...
|
...
|
...
|
|
.
.
.
|
.
.
.
|
.
.
.
|
.
.
.
|
Per ogni pagina
logica facente parte del programma, la tabella di gestione della paginazione
contiene:
- Un bit P: vale 1
se la pagina logica occupa una pagina fisica, 0 se non la occupa;
- Un gruppo di bit
IF: se la pagina logica non è presente il contenuto di questi bit non ha
importanza;
- Un gruppo di bit
ID: questo serve anche se la pagina logica è presente in memoria centrale,
perché se essa contiene dati che vengono modificati durante l’elaborazione
occorre ricopiarla su disco quando viene scaricata;
- Un bit M: vale 0
fintanto che nessuna parola della pagina è modificata dal momento in cui questa
è stata allocata in memoria l’ultima volta; diventa 1 alla prima modifica.
Se al momento di
scaricare la pagina, M vale 0 si può evitare la ricopiatura su disco
risparmiando tempo. Al momento della richiesta della pagina logica, la
consultazione della tabella permette di verificare se la pagina logica è
allocata e, se non lo è, ne viene effettuato il caricamento a cura del sistema
operativo; nella stessa tabella si legge l’indirizzo su hard disk della pagina
richiesta.
Il vantaggio
principale della paginazione è la semplicità dovuta al fatto che tutte le
pagine, logiche o fisiche, hanno uguali dimensioni, mentre il difetto più
importante è l’arbitrarietà con cui le pagine logiche vengono generate
suddividendo il programma, il che generalmente aumenta il numero di chiamate
tra pagine diverse e perciò diminuisce l’efficienza. È opportuno precisare due
concetti lasciati finora sottintesi:
- Anche le routine
del sistema operativo risiedono in memoria centrale;
- Le tabelle delle
pagine occupano anch’esse memoria.
La paginazione
riguarda solo la parte di memoria centrale libera, nel senso che le pagine
contenenti il sistema operativo e le tabelle non devono mai essere scaricate.
In realtà alcuni moduli del sistema operativo possono essere scaricati in caso
di bisogno perché il loro uso non è molto frequente. Le pagine contenenti il
nucleo del sistema operativo (codice e dati su cui si opera) restano fissate
nella memoria e non vengono mai paginate su disco. Il meccanismo che impedisce
il paging su disco di alcune parti della memoria viene indicato col termine
pinning (puntare con uno spillo).
Segmentazione
Con questa seconda
tecnica di gestione della memoria virtuale la suddivisione del programma viene
effettuata sulla base di criteri logici e può essere controllata dal programmatore.
Ciascun blocco risultante da tale suddivisione ha lunghezza arbitraria,
talvolta limitata a valori inferiori ad una lunghezza massima, e viene detto
segmento. Un programma non molto complesso può essere chiuso in un segmento che
contiene i dati ed un segmento per il codice da eseguire mentre, al crescere
della complessità, è opportuno raggruppare in segmenti le procedure che più
frequentemente si chiamano fra loro, e i dati che vengono più spesso usati
insieme. La ripartizione deve, cioè, seguire il criterio del minimo numero di
chiamate tra segmenti diversi.
Compatibilmente con
tale criterio è bene ridurre le dimensioni dei segmenti, perché ciò facilita il
compito del sistema operativo. La memoria centrale non viene divisa in blocchi
statici predefiniti, ma viene occupata, come visto, parlando delle partizioni
variabili. Questo conduce immediatamente a due conseguenze negative: da un lato
la memoria diventa soggetta a frammentazione, e dall’altro le tabelle da
impiantare per gestire la memoria virtuale con i segmenti sono più complesse da
usare che nel caso della paginazione. Per ogni segmento dei programmi da
eseguire occorre sapere:
- Se è presente in
memoria
- Indirizzo
iniziale
- Dove si trova su
disco
- Dimensioni
È utile disporre anche di una
tabella delle aree di memoria libere da consultare ed aggiornare ad ogni
richiesta di allocazione di nuovi segmenti e ad ogni terminazione di programma.
|
P
|
IDI
|
DS
|
ID
|
|
...
|
...
|
...
|
...
|
|
...
|
...
|
...
|
...
|
|
.
.
.
|
.
.
.
|
.
.
.
|
.
.
.
|
La tabella della
segmentazione contiene le informazioni richieste per ogni segmento ossia: P, un
bit; IDI, l’indirizzo dell’allocazione in memoria centrale che contiene la
prima parola del segmento; DS, che serve quando si deve allocare il segmento,
per trovare un’area libera adatta; ID. La segmentazione presenta, come la
paginazione, vantaggi e svantaggi impliciti; tra questi, evidenziamo i
seguenti:
- La memoria si
frammenta rapidamente, dato che quando si scarica un segmento esso viene
rimpiazzato normalmente da un segmento più piccolo, lasciando spazio
inutilizzato. A questo si può ovviare con routine che provvedano a compattare
tale spazio, spostando i segmenti in modo che siano adiacenti.
- Non è semplice
costruire con scaricamenti aree libere
di dimensioni assegnate. L’esempio della figura seguente mostra un’immagine
della memoria in un istante in cui contiene diversi segmenti: si osservi che
per poter caricare un nuovo segmento che richiede 100 locazioni occorre
scaricare due segmenti. Un modo semplice di risolvere il problema consiste nel
partire dall’inizio, e scaricare segmenti finché si raggiunge la dimensione
voluta, ma in questo modo i segmenti all’inizio sono costantemente penalizzati,
mentre quelli alla fine vengono scaricati raramente.
|
Libero (10)
|
|
Occupato (70)
|
|
Libero (10)
|
|
Occupato (50)
|
|
Libero (20)
|
|
Occupato (40)
|
|
.
.
.
.
|
100
locazioni

Un modo migliore può consistere nello scaricare segmenti e partire da
un punto variabile della memoria: la prima volta dalla locazione 0, la seconda
dalla locazione x, la terza dalla locazione 2x, e così via, in modo da
garantire che tutta la memoria (e tutti i segmenti) venga coinvolta. Ancora
meglio, si possono assegnare dei punteggi ad ogni segmento, e favorire lo
scaricamento dei segmenti con punteggi peggiori. È chiaro che, per ottenere
prestazioni migliori, occorre complicare notevolmente gli algoritmi di
controllo. Tra i vantaggi principali della segmentazione elenchiamo:
-
La possibilità di far condividere a più processi alcuni segmenti. Questo ha
un’importanza notevole in alcune applicazioni: per esempio, nei sistemi
transazionali, nei quali molti utenti operano sugli stessi dati, si possono
allocare segmenti di dati sui quali tutti gli utenti eseguono operazioni,
risparmiando spazio in memoria centrale. Inoltre, anche le performance
migliorano, dato che non occorre accedere molte volte ai dischi per reperire le
informazioni richieste. Viceversa, diversi utenti che usano una stessa
procedura potranno condividere il segmento contenente il codice lavorando,
però, su segmenti di dati differenti.
-
La mancanza di vincoli sul numero di segmenti caricabili in memoria, al
contrario del numero fisso delle pagine. Questo è vantaggioso soprattutto se si
hanno molti segmenti piccoli, che possono essere caricati in numero elevato.