La memoria virtuale
Tecniche di gestione della memoria nella famiglia x86 e in Os/2
A cura di
Antonio Arreghini
Indice Generale
Premesse e cenni al funzionamento di un programma.........................................................................3
Capitolo 1: Tecniche per la gestione della memoria............................................................................5
1.1: La memoria reale......................................................................................................................5
1.2: La segmentazione.....................................................................................................................6
1.3: La paginazione..........................................................................................................................9
1.4: La segmentazione con paginazione........................................................................................11
Capitolo 2: La gestione della memoria sulle architetture x86............................................................13
2.1: La memoria nell'8086.............................................................................................................13
2.1: La memoria nell'8086.............................................................................................................13
2.2: La memoria nell'80286...........................................................................................................15
2.2.1: La modalità reale.............................................................................................................15
2.2.2: La modalità protetta........................................................................................................15
2.3: La memoria nell'80386-486-P5..............................................................................................16
2.3.1: La paginazione................................................................................................................17
2.3.2: La segmentazione............................................................................................................18
2.3.3: La paginazione con segmentazione.................................................................................19
2.3.4: La modalità V86.............................................................................................................20
Capitolo 3: La gestione della memoria in Os/2..................................................................................22
3.1: Os/2 a 16 bit............................................................................................................................22
3.2: Os/2 a 32 bit............................................................................................................................22
3.2.1: I 3 formati di indirizzi.....................................................................................................22
3.2.2: Primo livello di traduzione in indirizzo lineare..............................................................23
3.2.3: Le arene di sistema..........................................................................................................23
3.2.4: La Paginazione a 2 livelli................................................................................................25
3.2.5: La gestione delle pagine fisiche e virtuali.......................................................................26
3.2.6: La gestione dei moduli....................................................................................................27
3.2.7: Il file di pagina................................................................................................................27
3.2.8: L'High Memory Support.................................................................................................28
Premesse e cenni al funzionamento di un programma
Questo documento si propone di illustrare il funzionamento dei sistemi di indirizzamento a
memoria virtuale, con particolare attenzione alle implementazioni delle varie tecniche sui processori
x86 e a come Os/2 sfrutta queste possibilità. In ogni capitolo, per prima cosa vengono spiegati i
concetti generali, quindi vengono forniti maggiori chiarimenti di natura tecnica. Il lettore non
interessato ai «tecnicismi» può tranquillamente saltare dette parti senza pregiudicare la
comprensione grossolana dell'argomento.
Per agevolare colore che non conoscono neanche in parte le nozioni che in questo documento
vengono date per scontate, ho voluto scrivere le seguenti (ahimè tutt'altro che esaurienti) righe per
indirizzare meglio la comprensione di un argomento piuttosto complesso.
Un programma è una sequenza ordinata di istruzioni che viene caricata nella memoria primaria (la
RAM) e viene quindi ordinatamente eseguita dal processore. Il programma al suo interno contiene
istruzioni (che sono i comandi che il processore deve eseguire, ad esempio somma, sottrai, carica un
dato...) e operandi. Queste due categorie di dati sono apparentemente indistinguibili dall'esterno,
infatti si tratta semplicemente di numeri binari. Come vedremo è il processore (o meglio il
compilatore) ad operare la distinzione: in pratica il processore legge il programma dall'inizio
supponendo che il primo dato sia un'istruzione, poi procede con i successivi dati sempre
supponendo che siano istruzioni... se si arriva ad un blocco di operandi, questo dovrà essere
preceduto da un'apposita istruzione che dica alla CPU l'indirizzo della successiva istruzione.
La memoria è quindi una sorta di pila di contenitori di numeri binari a 8 bit (ossia celle da 1 byte).
Ogni locazione di memoria (ossia ogni «celletta di memoria») è identificata univocamente da un
indirizzo, che a sua volta ovviamente è un numero. I dispositivi fisici di controllo della memoria
permettono di recuperare dati o salvarli in qualunque posizione si trovino (e per questo la memoria
è detta ad accesso CASUALE, in quanto i tempi di accesso non dipendono dalla posizione che il
dato occupa).
Il processore colloquia con la memoria tramite tre bus distinti o «multiplexati» (ossia «fusi» nelle
stesse piste) a seconda del processore stesso: questi sono il data bus, che permette di scambiare i
dati puri, l'address bus, che permette al processore di specificare su quale locazione di memoria
operare e il control bus, che permette di sincronizzare i due dispositivi, di richiedere dati, di
specificare se si vogliono effettuare letture o scritture, ecc...
Le istruzioni si classificano in tre specie fondamentali:
• Istruzioni di operazioni in memoria: il processore prima di poter eseguire qualunque operazione
deve caricare al suo interno (in cellette chiamate registri) gli operandi presenti in memoria. Ecco
quindi che servono determinate istruzioni per specificare quali dati caricare prima di ogni
operazione matematica. Queste istruzioni servono anche per movimentare dati da un posizione
all'altra, oppure per caricarli o inviarli alle periferiche (dischi fissi, scanner, stampanti, schede di
rete, modem, ecc...). A questo proposito va infatti detto che tali dispositivi esterni dispongono di
un proprio indirizzo di memoria.
• Istruzioni matematiche: prevedono operazioni logiche e aritmetiche sui dati precaricati nei
registri del processore; se l'operazione viene compiuta su numeri interni si utilizza l'unità
denominata ALU (Arithmetic Logic Unit) se su numeri in virgola mobile (numeri reali) entra in
gioco l'unità FPU (Floating Point Unit).
• Istruzioni di salto condizionato: come detto prima in teoria l'istruzione successiva a quella in
esecuzione si trova in memoria all'indirizzo immediatamente seguente all'istruzione corrente: c'è
Memoria Virtuale
Pag. 3
pertanto un «contatore di programma», il Program Counter, che viene incrementato al termine
di ogni istruzione e il suo contenuto di fatto rappresenta l'indirizzo dell'istruzione da caricare;
questo sistema tuttavia permette strutture di programmazione estremamente elementari. È stata
quindi introdotta questa classe di istruzioni che consente di caricare un nuovo valore all'interno
del program counter, di fatto effettuando un «salto». Tale salto può essere incondizionato
(avviene sempre) oppure condizionato (avviene se si verificano certe situazioni, ad esempio se il
contenuto di un certo registro è minore di 0). Si possono così implementare strutture di
programmazione molto più complesse quali i cicli e le chiamate a funzione.
La distinzione in queste 3 classi è netta solo in una architettura nota come «RISC» (Reduced
Instruction Set Computer) che prevede istruzioni semplici e tutte della stessa lunghezza (ogni
istruzione è lunga 16, 32 o 64 bit a seconda del parallelismo della macchina).
I processori della famiglia Intel x86 (quelli su cui funziona Os/2) usano invece delle istruzioni più
complesse (da questo il nome di CISC, Complex Instruction Set Computer) la cui lunghezza è
variabile (da 1 a 17 byte) e la cui esecuzione prevede di «confondere» le prime due classi (ossia ci
sono istruzioni matematiche che specificano gli operando direttamente in memoria).
Quando si parla di programmi a 16 o a 32 bit si intende quella che è la dimensione della word
standard che si adotta per quel programma: la word (detta anche parallelismo) è il numero di bit su
cui il processore può lavorare in parallelo. Nella modalità a 16 bit numeri, dati e istruzioni (sui
RISC) saranno in genere lunghi 2 bytes (16 bit); di conseguenza il program counter conterà a passi
di 2 bytes alla volta. Il vantaggio di avere un computer a 32 bit piuttosto che uno a 16 è quello di
poter lavorare con operandi più larghi su un tempo minore. Al giorno d'oggi quasi tutti i computer
sono a 32 bit; i server di rete e i supercomputer sono generalmente a 64 bit.
Memoria Virtuale
Pag. 4
Capitolo 1
Tecniche per la gestione della memoria
1.1: La memoria reale
Come visto la memoria è un contenitore di bytes: ciascuna cella ha un indirizzo univoco identificato
da una variabile a lunghezza finita. Questo vuol dire che ogni processore riesce ad indirizzare solo
una quantità finita di memoria.
La tecnica più antica ed intuitiva di gestione della memoria, è nota come «memoria reale». In
pratica è una non gestione della memoria, ossia gli indirizzi specificati nel programma
corrispondono sempre ad indirizzi fisici di memoria. I sistemi operativi e gli hardware che
permettono ai programmi di lavorare direttamente sulla memoria fisica sono molto antichi e molto
semplici e quindi hanno a disposizione una quantità molto limitata di memoria fisica, tanto che la
stragrande maggioranza dei programmi che riescono ad eseguire richiedono più spazio di quanto sia
effettivamente presente all'interno del computer.
Per fortuna accade che questa memoria ha una dimensione nota a priori e in genere sempre
disponibile in toto (solo nei sistema più evoluti è possibile installare solo parte della memoria
allocabile dal processore) per cui non è complicato escogitare una soluzione: il codice essenziale del
programma viene caricato in una parte di memoria, i dati essenziali in un'altra area; è fondamentale
che queste due sezioni riescano ad entrare completamente nella memoria disponibile (che in genere
è la memoria totale installata, sottratta dello spazio usato dal sistema operativo). La parte del codice
non essenziale viene invece caricata solo quando strettamente necessaria (ossia quando il
programma deve eseguire quelle istruzioni); una volta terminata l'esecuzione di quel pezzo di
codice e il medesimo non è più utile, i dati essenziali vengono salvati (o aggiornati) nella zona dei
dati globali, quindi quel codice viene cancellato per lasciare il posto ad un'altra sezione a sua volta
da caricare dal disco. L'area di memoria in cui avvengono questi processi di scambio è chiamata
Overlay.
Indirizzi crescenti
Sistema
operativo
Overlay
Dati programma
Area di
overlay
Codice
programma
Overlay
Overlay
Memoria
Disco fisso
La struttura della memoria reale con overlay
Questa tecnica ha più svantaggi che vantaggi: anzitutto il caricamento da memoria di secondo
livello (il disco fisso) è decisamente lento, ma questo è uno svantaggio che non si può colmare in
alcun modo se non aumentando la dimensione della memoria fisica (in questo caso il tempo di
caricamento viene assorbito per intero all'inizio dell'esecuzione del programma che poi procede
«fluido»). In secondo luogo il compito della gestione dell'overlay è interamente sotto il controllo del
Memoria Virtuale
Pag. 5
programma stesso, che deve monitorare quindi le risorse ed agire di conseguenza. In terzo luogo
questo modello non si adatta bene alla multiprogrammazione, ossia a quella pratica che prevede il
caricamento e l'esecuzione contemporanea di diversi programmi. Questi programmi ovviamente
non verranno eseguiti contemporaneamente (essendo in genere la CPU una sola): il sistema
operativo farà lavorare a turno per pochi millisecondi ciascuno di esso, dando l'impressione che i
programmi avanzino parallelamente. Resta però il fatto che tutti i programmi sono effettivamente
residenti in memoria; ancora peggio, ogni programma deve essere indipendente l'uno dall'altro.
Quando si lavora in memoria reale i programmi al loro interno specificano direttamente gli indirizzi
fisici per ogni locazione di memoria. Questo implica che più programmi possano richiedere l'uso
della stessa locazione di memoria (ad esempio tutti iniziano in posizione 0000, hanno degli
operandi in 0E01 e via dicendo).
Una possibile soluzione è quella di salvare in un puntatore (ossia una variabile di memoria che
contiene un indirizzo) l'indirizzo a cui inizia il programma e specificare al suo interno soltanto
indirizzi relativi a questo: l'indirizzo della memoria fisica si ottiene perciò sommando al puntatore
la posizione del codice da caricare relativa all'inizio del programma; questo indirizzo relativo
prende il nome di offset. In realtà questa tecnica è piuttosto scomoda da implementare per il
programmatore (che deve programmare tenendo conto di questo fatto), è decisamente più lenta
(ogni indirizzamento necessita di una somma), non permette ai programmi di espandersi (infatti
dopo la fine del proprio spazio può benissimo iniziare un nuovo programma) e specialmente non è
sicura, infatti un indirizzo sbagliato può comportare la lettura (o peggio la scrittura) di una cella di
memoria che appartiene ad un altro programma. In altre parole è inaccettabile.
1.2: La segmentazione
Per compensare queste carenze del modello a memoria reale (che trova comunque spazio in vecchi
sistemi monoprogrammati come il DOS) è nata la cosiddetta segmentazione.
Gli indirizzi di un programma non si riferiscono più ad una locazione fisica nella memoria, ma ad
una «locazione virtuale». In sostanza i programmi lavorano con una memoria «teorica» (chiamata
memoria virtuale); la traduzione e la mappatura di questa memoria virtuale nella RAM del
computer è un compito che spetta al sistema operativo ed al processore, liberando così
definitivamente il programmatore dal gestire questa problematica (come invece non succedeva per
l'overlay).
Gli indirizzi vengono specificati come una coppia di due numeri: un selettore e un offset. Il primo
identifica il «segmento» su cui si sta lavorando, il secondo indica la posizione all'interno del
segmento stesso.
Memoria Virtuale
Pag. 6
Segmento 1
Indirizzi crescenti
Segmento 2
Libera
Segmento 3
Libera
Memoria
Un esempio di segmentazione
L'implementazione di questa tecnica necessita di un hardware apposito che agevoli la traduzione di
questi indirizzi virtuali in indirizzi fisici. In sostanza la CPU deve avere un'unità (chiamata MMU,
Memory Management Unit) che all'interno deve contenere il puntatore ad un tabella dei segmenti:
all'interno di questa sono presenti una serie di voci (entry) che descrivono tutti i segmenti presenti
nel sistema. La traduzione procede automaticamente seguendo questa trafila:
• La MMU carica il selettore del segmento dall'indirizzo virtuale fornito dal programma
• La MMU conosce già l'indirizzo base della Segment Table (che identifica l'inizio della
medesima) e gli somma il selettore trovando così la posizione (l'entry) nella tabella dei segmenti
dove è descritto il segmento in questione
• Le informazioni del descrittore del segmento contengono l'indirizzo base del segmento cui viene
sommato l'offset ottenendo finalmente l'effettivo indirizzo fisico.
Tale procedimento è riassunto nella figura sottostante.
Indirizzo base
segment table
B
+
S
Indirizzo Virtuale
Selettore
Offset
O
B+S
B Segment table
S'
+
S'+O
Indirizzo fisico
Descrittore
La traduzione secondo il sistema di segmentazione
Memoria Virtuale
Pag. 7
Un programmatore può a questo punto scrivere il codice di un programma su diversi segmenti; il
bello è che quando un programma richiede al sistema operativo di creargli un segmento di una certa
dimensione, il SO controlla se c'è tale spazio libero in memoria; in caso contrario lui
automaticamente copia sul disco fisso uno o più segmenti, liberando spazio in memoria fisica; tutto
ciò viene fatto all'insaputa del programma e del programmatore che quindi può dimenticarsi la
fastidiosa gestione degli overlay e la «condivisione della memoria tra programmi» e concentrarsi
esclusivamente sulla programmazione.
Appena qualche programma richiederà un segmento che era stato copiato sul disco il SO
provvederà a liberare lo spazio necessario in memoria (sempre copiando altri segmenti sul disco) e
a ricaricare quello richiesto; questo sempre in modo totalmente trasparente all'utente.
Ovviamente il lavoro di copia è piuttosto lungo e rallenta l'esecuzione, per cui la segmentazione
prevede anche che sia installata una quantità di memoria fisica adeguata alle esigenze dei
programmi che vengono eseguiti (per evitare di trascorrere la maggior parte del tempo a copiare
segmenti e non ad eseguire i programmi).
Un altro vantaggio della segmentazione è la protezione dei dati: la CPU automaticamente controlla
se l'offset richiesto si trova all'interno del segmento oppure se un certo programma cerca di accedere
a segmenti che non gli appartengono. Se queste condizioni pericolose si verificano la CPU notifica
al sistema operativo l'errore; il programma viene quindi terminato prima che possa nuocere agli altri
processi caricati in memoria.
Purtroppo non sono tutte rose e fiori. Anzitutto bisogna considerare che i segmenti hanno
dimensioni diverse; inserirli e toglierli dalla memoria provoca molta frammentazione, per cui si
generano tanti «buchi» in memoria difficili da riutilizzare (spesso un segmento non riesce ad
entrarci). Una soluzione potrebbe essere una ricompattazione periodica della memoria, ma è
un'operazione lunga e costosa. Inoltre il SO deve possedere algoritmi relativamente complessi per
sapere qual è la posizione migliore in cui caricare un certo segmento.
Segmento 1
Indirizzi crescenti
Segmento 2
Libera
Segmento 3
?
Segmento 4
Libera
Memoria
Disco fisso
Questo è un esempio di frammentazione: ci sarebbe sufficiente memoria libera per caricare il
segmento 4, ma non essendo continua non è possibile caricarlo se non ricompattando il segmento 3
oppure swappandolo su disco
Memoria Virtuale
Pag. 8
Un altro svantaggio è che il copiare su disco segmenti molto lunghi richiede molto tempo; a questo
proposito c'è da dire che ai programmatori non piace molto lavorare con questi segmenti, per cui
cercano di far stare tutto il programma su un singolo segmento molto grande (e quindi scomodo da
copiare sul disco!); infatti quello che succede all'interno di ogni segmento è che l'offset rappresenta
un indirizzo lineare: c'è a disposizione l'indirizzo 0000, il 0001 e così via per cui sono lecite
operazioni matematiche sui puntatori stessi, pratica che può sembrare poco ortodossa se fatta in un
linguaggio ad alto livello, ma è indubbiamente comoda da utilizzare (e utilizzata moltissimo nella
realtà).
Un ulteriore svantaggio della segmentazione risiede nella dimensione massima del segmento:
questa è necessariamente limitata dalla dimensione della memoria fisica; se uno ha 2 Mb di
memoria fisica non può caricare (neanche copiando sul disco tutti gli altri segmenti) un segmento di
3Mb!
In sostanza, la segmentazione porta molti vantaggi ma non risolve tutti i problemi.
1.3: La paginazione
Indirizzi crescenti
Una soluzione a parte dei problemi si trova nella tecnica chiamata paginazione. Si tratta di dividere
lo spazio di indirizzamento lineare (è così chiamato l'insieme di tutti gli indirizzi di memoria
virtuale, lineare perché gli indirizzi sono contigui) in tante pagine di memoria; ognuna di queste è
piuttosto piccola e la dimensione è prefissata ed immodificabile. Ogni pagina rappresenta la minima
quantità allocabile dal sistema operativo. Questo sistema permette di ridurre la frammentazione
perché non ci sono «buchi troppo stretti» nei quali non si possa caricare un'altra pagina. Per caricare
una pagina in una memoria piena basta scaricarne esattamente una sola!
Inoltre in questo modo è possibile che un programma sia più grande dell'effettiva dimensione fisica
della memoria, questo perché eventuali eccedenze dello stesso programma vengono compensate
semplicemente copiando su files parte di questo.
Memoria
Disco fisso
Il sistema di paginazione evita il problema della frammentazione: ogni pagina swappata entra
esattamente in ogni pagina libero in memoria
Memoria Virtuale
Pag. 9
La paginazione è totalmente trasparente all'utente: non è più necessario specificare il numero di
segmento (di pagina in questo caso) perché questo è insito nell'indirizzo stesso, indirizzo che viene
appunto chiamato indirizzo lineare. Siccome la dimensione della pagine è fissa ed è pari ad una
potenza esatta di 2, l'offset è costituito esattamente dagli ultimi bit dell'indirizzo lineare e il numero
di pagina è costituito dai primi bit: ad esempio in un indirizzo a 32 bit i primi 20 possono
rappresentare il numero pagina e gli ultimi 12 l'offset.
Il meccanismo di traduzione da indirizzo lineare a indirizzo fisico assomiglia molto a quello della
segmentazione:
• La MMU carica il numero di pagina dall'indirizzo lineare fornito dal programma
• La MMU conosce già l'indirizzo base della Page Table (che identifica l'inizio della medesima) e
gli somma il numero di pagina trovando così la posizione (l'entry) nella tabella delle pagine dove
è descritta la pagina in questione
• Le informazioni del descrittore della pagina contengono l'indirizzo base in memoria fisica cui
viene sommato l'offset ottenendo finalmente l'effettivo indirizzo fisico.
Indirizzo base
page table
B
+
P
Indirizzo Lineare
Numero di pagina
Offset
O
B+P
B Page table
Page table entry
P'
Indirizzo fisico
La traduzione secondo lo schema della paginazione
Questo meccanismo ha però anche dei punti deboli: anzitutto con dimensioni di memoria piuttosto
grandi è facile che si generi una page table molto grande e quindi difficile da gestire; la soluzione
per questo è quella di adottare la cosiddetta paginazione a due livelli: si tratta di dividere il numero
della pagina in due parti (ad esempio ciascuna di 10 bit): la prima parte identifica la posizione della
page table nella directory delle pagine, la seconda parte identifica il numero di pagina dentro la
page table selezionata.
Memoria Virtuale
Pag. 10
Indirizzo base
page directory
F
+
Indirizzo Lineare
P
Indice 2
Indice 1
Offset
O
P'
Page Directory
F+P
F
Indirizzo fisico
B'
+
B' Page Table
B'+P'
Page dir. entry
Page table entry
La traduzione secondo lo schema della doppia paginazione
Un ulteriore problema è dovuto al fatto che il programmatore non avendo alcun controllo sulle
pagine non può imporre che parte del codice stia in certe pagine piuttosto che in altre. Succede
allora che il concetto di protezione diventa totalmente inapplicabile (pur essendoci la possibilità di
assegnare una protezione ad una certa pagina).
Un altro svantaggio è che la paginazione (a differenza della segmentazione) prevede un unico
spazio di indirizzamento lineare creando così non pochi problemi in ambienti multiprogrammati.
Anche qui esiste una soluzione che è quella di assegnare ad ogni programma una sua page table (o
una page directory); all'atto del context switching (ossia quell'insieme di operazione che il sistema
operativo deve compiere per cambiare il processo attivo) viene caricato l'indirizzo nella MMU della
page table corretta; in questo modo si può realizzare anche una certa protezione della memoria..
1.4: La segmentazione con paginazione
Quello che però effettivamente si usa nella maggior parte degli ambienti operativi commerciali è la
combinazione delle due tecniche per sfruttare i vantaggi dati da ognuna delle due; si utilizza infatti
un sistema di segmentazione che permette ai programmi di avere quanti spazi di indirizzamento
lineari desiderano; poi questi vengono paginati in modo da poter essere comodamente gestiti
dall'hardware. Certo che la MMU deve essere decisamente più potente e all'interno deve prevedere
un'unità di paginazione e un'unità di segmentazione.
La traduzione degli indirizzi virtuali in indirizzi fisici avviene con due stadi successivi: prima si
adotta la traduzione secondo il sistema di segmentazione per passare da indirizzo virtuale a
indirizzo lineare. Poi si adotta la paginazione su questo indirizzo lineare e si arriva all'indirizzo
fisico.
Memoria Virtuale
Pag. 11
Indirizzo base
segment table
B
Indirizzo Virtuale
S
+
Selettore
Offset
Indirizzo fisico
O
B+S
B Segment table
B'
Descrittore
+
O'
Indirizzo lineare
Numero di pagina
Offset'
Indirizzo base
page table
P
B'' Page Table
+
B''+P
B''
Page table entry
P'
La traduzione secondo lo schema della segmentazione con paginazione
La tabella sottostante riassume schematicamente le caratteristiche delle tecniche analizzate.
Overlay
Paginazione
Segmentazione
Paginazione +
Segmentazione
SI
NO
SI
Solo della
segmentazione
Uno solo
Uno solo
Molti
Molti
Si può superare il
limite della
memoria fisica?
SI
SI
NO
SI
Prevede forme di
protezione?
NO
NO
SI
SI
Per superare il
limite della
memoria fisica
Per superare il
limite della
memoria fisica
Il programmatore
ne è consapevole?
Spazi lineari
disponibili
Perché viene
implementata?
Memoria Virtuale
Creare più spazi di Combinare tutti i
indirizzamento
vantaggi
lineari
Pag. 12
Capitolo 2
La gestione della memoria sulle architetture x86
2.1: La memoria nell'8086
Il primo processore della fortunata famiglia x86 di Intel è stato l'8086; si trattava di un processore
che in parte riciclava i vecchi lavori di Intel e le sue vecchie istruzioni; la base è infatti facilmente
riconducibile al vetusto 4004, primo microprocessore a 4 bit della ben nota ditta californiana (e
anche primo microcomputer in assoluto). Questo si è poi evoluto nell'8008 (a 8 bit) e
successivamente nell'8080 trovando grande diffusione dentro i primi sistemi IBM. Quando IBM
decise di entrare nel mercato dei Personal Computer per fare concorrenza alla Apple, l'Intel preparò
un'evoluzione a 16 bit del precedente processore, l'8086 appunto.
Il fatto che il processore fosse a 16 bit implicava che la dimensione interna dei registri e del data
bus avessero proprio questo parallelismo. La maggior parte delle istruzioni (che erano ereditate dal
vecchio sistema CISC a 8 bit) erano lunghe 16 bit, ma gli operando potevano aumentare il numero
di letture in memoria. L'address bus e il program counter erano a 20 bit e questo significava che la
massima memoria indirizzabile da questo processore era 1 Mb (un bel passo avanti rispetto ai 64
KB dei vecchi 8080).
Gli indirizzi venivano scritti su due parole distinte: della prima (i primi 16 bit) soltanto i 4 bit meno
significativi venivano utilizzati, i rimanenti ignorati; questi identificavano l'area di memoria su cui
operare (Intel parla di segmento di memoria, ma questa tecnica aveva ben poco a che vedere con la
segmentazione in quanto mancava ogni forma di protezione e mancava la segment table, per cui in
questo documento parleremo di aree di memoria); la seconda word rappresentava l'offset; erano
quindi possibili 16 aree di memoria da 64 Kb, per un totale di appunto 1 Mb.
Inutilizzati
Area
Offset
12 bit
4 bit
16 bit
Si mostra qui come venivano sfruttate le 2 parole di memoria degli indirizzi.
In realtà non tutte queste aree erano accessibili ai programmi, parte era riservata ai sistemi di
controllo hardware ed al BIOS. In particolare le prime 10 aree (640 Kb) erano allocabili dai
programmi e dal sistema operativo (allora il vetusto MsDOS 1) e formavano quella che fu chiamata
memoria convenzionale. Le ultime 6 aree (384 Kb) formavano la cosiddetta memoria superiore
(o anche UMB, Upper Memory Block).
Memoria Virtuale
Pag. 13
1024 Kb
System BIOS ROM
960 Kb
ROM to RAM option
816 Kb
Hard disk controller
800 Kb
Video Bios Option
768 Kb
Video Ram Space
640 Kb
Memoria
Convenzionale
0 Kb
La figura mostra come venivano distribuite questi aree: in particolare si noti che le prime due aree
di memoria superiore erano riservate alla memoria video, le successive 3 servivano per copiare il
codice dei BIOS di sistema e della scheda video sulla RAM per accelerare la loro esecuzione (per
attivare questa copia c'è tuttora una opzione nel BIOS) e per contenere il codice di controllo dei
dischi fissi. L'ultima invece conteneva il sistema di controllo hardware.
Questo Megabyte di memoria era necessariamente sempre presente in tutti i computer e il
processore non poteva funzionare in sua assenza.
Col passare del tempo 1 Mb cominciava a diventare una dimensione un po' limitata, per cui
occorreva trovare un rimedio; una soluzione fu quella di inserire dei connettori sulla scheda madre
per poter leggere un nuovo tipo di memoria (fisicamente diverso dal precedente) chiamato memoria
EMS (expanded memory specification, ossia memoria espansa). Così si poteva aggiungere
memoria oltre il primo Mb, ma per poterla sfruttare un programma doveva utilizzare un complicato
meccanismo di allocazione che era del tutto simile alla paginazione. Non entriamo ulteriormente nei
dettagli, ma in questo modo era possibile espandere la memoria di 32 Mb rispetto alla dimensione
iniziale.
L'8086 fu anche prodotto in una variante: l'8088. La differenza stava nel data bus, che era di soli 8
bit. In questo modo si riuscì a montare l'8086 anche sulle vecchie schede madri IBM basate su
8080. Per questo l'architettura x86 ebbe successo e fu adottata quasi subito. In realtà l'8088 era
necessariamente più lento perché ogni lettura da memoria andava fatta con due operazioni distinte
per recuperare 8 bit alla volta dei 16 della parola.
All'8086/88 seguì un nuovo processore: l'80186 (prodotto anche nella versione 80188). Questo chip
segnò un'importante lezione per Intel: oltre a piccole migliorie interne praticamente trascurabili
erano state integrate nella CPU una serie di chip esterni (controllori IRQ, buffer di memoria e altri
chip di servizio simili) per semplificare la circuiteria sulla scheda madre. Purtroppo così facendo la
compatibilità con l'8086 non era perfetta; questo decretò l'immediato fallimento del progetto su
mercato, che vedeva il problema della compatibilità come quello principale.
Memoria Virtuale
Pag. 14
2.2: La memoria nell'80286
Il successore dell'8086/88 fu chiamato 80286 e venne prodotto in una singola versione (non c'era
l'80288); si prese consuetudine allora di chiamarlo semplicemente 286.
La grande novità di questa CPU era la presenza di una prima unità MMU in grado di sfruttare
realmente un sistema di segmentazione. L'address bus e il program counter erano stati allargati a 24
bit, di conseguenza la massima memoria installabile era 16 Mb. Ma tale tipo di memoria non era
fisicamente diversa dal primo MB (come nel caso dell'EMS); era situata sugli stessi banchi, i
«vecchi» moduli SIMM a 30 contatti.
Poiché il DOS e tutti i suoi programmi non erano compatibili con formati di indirizzi a 24 bit, ma
solo a 20 bit, in teoria il DOS non poteva essere eseguito sul 286; ulteriore ostacolo stava nel fatto
che il 286 non era vincolato ad avere installati tutti e 16 i MB di memoria. La soluzione fu di dotare
il processore di un dispositivo per poter far funzionare la macchina in due modi diversi: la modalità
reale e la modalità protetta.
2.2.1: La modalità reale
Il sistema di modalità reale è lo stato nel quale la macchina si accende di default (anche gli attuali
computer partono nella stessa modalità reale di allora). La memoria installata viene configurata
come se si stesse usando un 8086: il primo Mb viene diviso al solito in 640 Kb di memoria
convenzionale e 384 di memoria superiore. La memoria installata in eccesso (e non utilizzabile
direttamente con indirizzi a 20 bit) viene vista dal sistema come XMS (extended memory
specification, ossia memoria estesa). Tale memoria è utilizzabile dai programmi che la prevedono,
ma tramite un meccanismo piuttosto lento e macchinoso. Non per questo restava comunque
inutilizzata, infatti con il DOS 3.3 in poi venivano forniti alcuni dispositivi che ne facevano uso:
• l'himem.sys: serviva per creare nei primi 64 Kb di XMS un'area di memoria nota come HMA
(High memory area, memoria alta) nella quale si poteva caricare il sistema operativo DOS
liberando memoria convenzionale
• il ramdisk.sys: serviva per creare un «disco fisso» virtuale in memoria
• lo smartdrive: era una cache per disco con caratteristiche write back che utilizzava l'XMS per
accelerare le operazioni
• l'xaem86.sys (solo nel DOS 4): permetteva di emulare nell'XMS una memoria EMS in modo che
potesse essere utilizzata (tutto sommato più agevolmente dell'XMS) dai programmi che la
prevedevano
2.2.2: La modalità protetta
Per fortuna però la memoria aggiuntiva poteva essere sfruttata anche in modo più dignitoso, nella
modalità protetta: veniva abilitato un sistema di memoria virtuale, comandato dall'MMU del
processore e dal sistema operativo (occorrevano sistemi operativo che fossero compatibili, come
Windows 3.0 e Os/2). Gli indirizzi virtuali erano indirizzi segmentati nel formato 16:16 (16 bit di
selettore e 16 di offset).
Il selettore era fatto nel seguente modo: 13 bit di identificatore del segmento, 1 bit di selezione della
segment table, 2 bit di RPL (Requested Privilege Level); questi ultimi servivano a definire il livello
di privilegio necessario per poter accedere a quel segmento: 00 => ring 0, 01 => ring 1, 10 => ring
2, 11 => ring 3, dove il ring 0 ha la priorità massima d'accesso, il ring 3 quella minima. Il bit di
Memoria Virtuale
Pag. 15
selezione permette di specificare se il segmento di trova nella GDT o nella LDT: ogni processo ha
infatti a disposizione una tavola dei segmenti proprietaria (Local Descriptor Table) e condivide con
tutti gli altri processi una tavola condivisa (Global Descriptor Table) nella quale sono contenuti i
segmenti che possono essere letti da qualunque programma. All'atto del context switch il sistema
operativo doveva caricare la LDT corretta.
Numero segmento
ST
13 bit
1 bit
RPL
2 bit
L'indirizzo virtuale
Offset
16 bit
Di conseguenza ogni programma aveva a disposizione teoricamente 8192 segmenti da 64 Kb l'uno,
per un totale di 512 Mb di memoria. La divisione del programma in segmenti era necessaria vista la
limitata dimensione dei segmenti stessi.
Il 286 in questa modalità era quindi in grado di allocare 512 MB di codice condiviso e 512 MB di
codice privato; per cui ogni spazio di indirizzamento (=ogni programma) constava di 1GB al
massimo.
All'interno della segment table (chiamata descriptor table sui sistemi Intel) ci sono tutti i descrittori
dei segmenti che includono dati quali l'indirizzo base, la lunghezza, la protezione, il DPL
(Descriptor Privilege level) e la presenza o meno del segmento in memoria. Qualora la memoria
fisica (che poteva contenere soltanto 16 Mb al massimo) fosse piena uno o più segmenti potevano
essere swappati su disco nello swapfile per estendere così la memoria virtuale oltre il limite di
quella reale.
2.3: La memoria nell'80386-486-P5
Il processore che arrivò dopo il 286 fu chiamato 80386 o semplicemente 386. Rispetto a tutti gli
altri modelli il 386 era un processore a 32 bit: possedeva nuove istruzioni a 32 bit, dati interi
standard di 32 bit, larghezza dei bus interni, esterni e dei registri di 32 bit. Anche l'address bus e il
program counter avevano 32 bit. Era quindi possibile montare sul sistema un massimo di 4 Gb
(2^32) di memoria, una quantità che tuttora è ancora lontana dall'essere raggiunta anche se si sta
gradualmente avvicinando.
Il 386 poteva funzionare in modalità reale (con istruzioni a 16 bit), in modalità protetta (con
istruzioni a 16 bit) e in modalità protetta «386» (con istruzioni a 32 bit). Un'importante novità fu poi
l'introduzione della modalità virtual86.
In pratica il 386 poteva emulare in pieno un 286 (anche se effettivamente era più lento di questo), in
più introduceva questo nuovo set di istruzioni che permetteva di sfruttare il maggior parallelismo, le
maggiori quantità di memoria e le nuove potenzialità della MMU che integrava sia l'unità di
paginazione che di segmentazione.
I processori successivi (486, Intel Pentium, AMD K5 e AMD K6) utilizzano questo stesso modello
e non fanno altro che migliore le prestazioni con l'introduzione della pipeline normali (nel 486) e
superscalari (dal Penitum in poi), ossia la possibilità di eseguire più istruzioni contemporaneamente.
Se si eccettua l'MMX (improbabile estensione «multimediale») l'unica novità nell'instruction set sta
esclusivamente in 4 nuove (e quasi inutili...) istruzioni nel 486.
Altri processori (gli Intel Pentium PRO, Pentium 2, 3 e 4, nonché gli AMD K6 2 e 3 e i K7 Athlon)
Memoria Virtuale
Pag. 16
invece introducono una serie di migliorie nelle istruzioni, aggiungendo le «istruzioni per il 3D» e
aggiungendo una nuova modalità di paginazione a 4 MB (con dimensioni dell'address bus di 36 bit).
Queste CPU dovrebbero in teoria essere in grado di installare 64 GB di memoria, ma in pratica non
esistono sistemi operativi sul mercato che fruttino questa nuova paginazione.
La modalità protetta a 32 bit funziona in 3 diverse sottomodalità: il sistema a paginazione, il sistema
a segmentazione e il sistema a segmentazione paginata.
2.3.1: La paginazione
La paginazione permette di applicare la tecnica che abbiamo già visto su uno spazio di
indirizzamento lineare di 4 Gb. I programmi possiedono indirizzi a 32 bit che vengono paginati dal
sistema con pagine grandi 4 Kb.
Sarebbe perciò possibile che ogni programma teoricamente potesse disporre di 4 Gb lineari; una
page table proprietaria di quel programma potrebbe essere caricata dal sistema operativo (all'atto del
context switch) nella MMU; purtroppo c'è una limitazione: le parti del «codice condiviso» nonché
la zona del sistema operativo devono essere SEMPRE visibili (per chiamate a sistema, ecc...). Di
conseguenza parte dello spazio di indirizzamento viene destinato esclusivamente a questo scopo; le
page tables che mappano tale area non subiscono sostituzione. Questo ovviamente riduce anche lo
spazio disponibile per il singolo programma.
Page tables
inattive
Codice condiviso
Programma 1
Page tables
Programma 2
Programma 3
Page tables
inattive
Page tables
attive
Spazio di indirizzamento lineare attivo
4 Gb
O
Ad ogni context switch vengono attivate diverse Page Tables per mappare la parte dell'indirizzo
lineare privata. La parte condivisa invece non subisce la sostituzione delle page tables
Per gestire in modo efficiente la memoria, il 386 prevede la paginazione a due livelli: i primi 10 bit
dell'indirizzo lineare identificano all'interno della page directory l'indirizzo della page table da
caricare. Ogni entry nella page directory è lunga 4 byte (indirizzo fisico della page table) per cui la
dimensione totale della page directory è di 4 K. I secondi 10 bit dell'indirizzo identificano la pagina
richiesta all'interno della page table selezionata; anche qui ci sono 4 byte per pagina e quindi una
page table è grande 4 Kb: in altre parole occupa esattamente una pagina di memoria, per cui è facile
swappare anche le page tables qualora la situazione lo richiedesse. Come sempre gli ultimi 12 bit
rappresentano l'offset dentro la pagina selezionata.
Da notare che nella page table solo 20 bit servono ad identificare l'indirizzo base della pagina (che
di fatto differisce ogni volta di 4 Kb); gli altri 12 bit sono utilizzati per ulteriori informazioni sulla
pagina: ad esempio indicano il PPL (page privilege level, ossia i privilegi che un programma deve
avere per poter accedere al contenuto di quella pagina), il permesso di solo-lettura/scrittura, la
presenza della pagina in memoria oppure nello swapfile, il tipo di pagina (di programma, di sistema
Memoria Virtuale
Pag. 17
operativo), il permesso di poter copiare sul disco quella pagina (alcune pagine non possono essere
swappate, come quelle del sistema operativo), ecc...
Prima di caricare una pagina un'opportuna unità della CPU (l'unità di protezione e test) controlla
questi dati. Se si verifica una violazione questa viene notificata attraverso un apposito interrupt al
sistema operativo che può così prendere le sue precauzioni.
Da notare che lo spazio di indirizzamento di ogni singolo programma è quindi di 4GB, ma in tale
spazio deve risiedere anche il codice condiviso. Per comodità quello che molti sistemi operativi
fanno è quello di utilizzare 2 page directory: entrambe mappano gli stessi 4GB di memoria, ma la
prima contiene solo le pagine di codice condiviso e la secondo di codice privato. Il context switch si
riduce quindi a dover sostituire questa seconda page directory.
2.3.2: La segmentazione
La funzione di segmentazione è analoga a quella vista per il 286, ma più potente: questa volta
l'offset è di 32 bit, ossia con la segmentazione gli indirizzi virtuali sono composti da addirittura 48
bit (16 di selettore e 32 di offset).
Ogni descrittore di segmenti nelle descriptor tables è composto da 64 bit (8 bytes): 32 di questi
vengono utilizzati per identificare l'indirizzo base del segmento, 20 la dimensione, 1 identifica se si
sta utilizzando la paginazione a 4 Kb (vedi in seguito); 2 bit sono il DPL (descriptor privilege level,
che specifica quale ring ha accesso all'oggetto),1 bit descrive se si tratta di un segmento di sistema
operativo o di programma, 1 se il segmento è presente in memoria, uno definisce la dimensione
degli operandi, gli altri 3 definiscono la politica di lettura e scrittura per il segmento. Vedi anche la
figura sottostante.
La protezione della segmentazione è quindi più completa di quella della paginazione. Tuttavia
questa modalità non viene usata così com'è da alcun sistema in commercio.
Limite
Base
S, Tipo,
(Prima parte)
(prima parte)
DPL, P
16 bit
24 bit
8 bit
Il descrittore del segmento per l'Intel 386
Limite Flag
4 bit
4 bit
Base
8bit
Legenda:
Limite: diviso in due parti (tra i bit 0 e 15 e tra il 48 e il 51) indica la lunghezza del segmento
Base: divisa in due parti (tra i bit 16 e 39 e il 56, 63) è l'indirizzo fisico a 32 bit di partenza del
segmento.
S: (bit 44) Tipo di segmento, 0=di sistema, 1=di applicazione
Tipo: (bit 40->43) bit 40: 0=non acceduto, 1=acceduto
A seconda del bit 44, ci sono diversi significati per i bit 41->43, oggetto dati/codice, espandibilità
in alto/basso, Read-only/read-write, solo-esecuzione/lettura-esecuzione. In caso di un segmento di
sistema: LDT, TSS, Call Gate, Interrupt Gate, ecc...
DPL: (bit 45-46) Descriptor Privilege Level Descrive quale ring ha accesso all'oggetto.
P: (bit 47) Presente, 0=segmento non presente, 1=segmento presente
Flag: (bit 52-55) bit 52: usato dagli oggetti UVirt; bit 53: dimensione degli operandi 0=16 bit,
1=32 bit; bit 54: grandezza degli indirizzi 0=16 bit, 1=32; bit 55: 0= il limite è espresso in bytes,
1= il limite è espresso in pagine da 4 Kb.
Memoria Virtuale
Pag. 18
Usando la sola funzione di segmentazione è quindi possibile avere per ogni programma 32 TB di
memoria, più altrettanti sempre disponibili per il codice condiviso. Il context switch non fa altro che
sostituire la LDT.
2.3.3: La paginazione con segmentazione
Si tratta della variante più potente del precedente schema a segmentazione e rispecchia in tutto e per
tutto quanto già espresso in precedenza. Come visto nei descrittori dei segmenti si può specificare
che il segmento sia paginato: questo non solo permette di avere segmenti da 4 Gb, ma permette
specialmente di attivare la paginazione all'interno del segmento.
Dai 48 bit di indirizzo virtuale viene ricostruito uno spazio di indirizzamento lineare a 32 bit, che
poi viene paginato con le tecniche già viste della paginazione a 2 livelli. Questo sistema permette di
combinare il meglio delle due tecniche: la protezione avviene sia a livello di segmenti che di
pagine, il programmatore ha teoricamente a disposizione più spazi di indirizzamento (se usa anche
il selettore) oppure può accontentarsi di un solo segmento evitando di specificare il selettore. È
possibile usare segmenti con programmi a 16 bit e a 32 bit; nonché segmenti ii modalità virtuale
(vedi sotto).
Permangono però delle limitazioni: anzitutto l'hardware di paginazione prevede spazi di
indirizzamento di soli 4 GB e non di 64 TB come prevedeva la segmentazione pura. Questo può
considerarsi irrisorio viste le comunque generose dimensioni di cui si parla, ma è pur sempre una
limitazione. Come se non bastasse i segmenti della GDT (ossia quelli che devono essere visti
sempre e da tutti i programmi) devono essere mappati in tutti gli spazi lineari; per cui in effetti un
programma non può struttare nemmeno tutti e 4 i GB messi a disposizione. Anzi, più codice
condiviso c'è e meno spazio resta per il codice proprietario.
La figura sotto riassume gli aspetti della paginazione con segmentazione nel 386.
Memoria Virtuale
Pag. 19
2.3.4: La modalità V86
Uno dei limiti introdotti con la modalità protetta nel 286 era quello di non poter eseguire alcun
programma DOS (né il DOS stesso) quando questa era attiva. Ciò creava diversi fastidi in quanto i
programmi erano limitati a 640 Kb, l'accesso all'XMS era difficoltoso e sotto Windows non era
teoricamente possibile eseguire programmi DOS se non interrompendo il multitasking.
Una soluzione venne con il 386, il quale era in grado di creare all'interno di un segmento speciale
della modalità protetta una macchina virtuale dos (VDM, Virtual Dos Machine); il processore per
eseguire quel segmento entrava nella cosiddetta «modalità Virtual 86»: all'interno del segmento
veniva ricostruita (in realtà emulata) la struttura classica della modalità reale ed era così possibile
eseguire la maggior parte dei programmi DOS.
Il primo ambiente a sfruttare questa possibilità fu Windows 3.0 se fatto partire con il comando win
/386, che abilitava la cosiddetta «modalità 386 avanzata»; al suo interno era quindi possibile
caricare programmi DOS.
Anche il DOS stesso con la versione 5 abilitò un particolare supporto per lavorare sui 386: il driver
emm386.exe. Questo permetteva di portare la macchina in modalità protetta e di creare un segmento
Memoria Virtuale
Pag. 20
VDM; il vantaggio di ciò era la possibilità di avere un controllo molto maggiore della memoria
stessa e di poter caricare i driver e i TSR in memoria superiore liberando spazio in memoria
convenzionale. Un altro gestore di memoria molto comune era il qemm386, una versione ancora più
avanzata che permetteva anche di estendere la memoria HMA a 384 Kb e sfruttarla per caricarci
sopra tutto quanto non ci stava nell'UMB.
Memoria Virtuale
Pag. 21
Capitolo 3
La gestione della memoria in Os/2
3.1: Os/2 a 16 bit
La prima versione di Os/2 realizzata da IBM e Microsoft per fornire un'alternativa valida alla
pietosa accoppiata DOS+Windows era nata per poter essere eseguita sui processori 286. Era quindi
basata su un codice a 16 bit e i programmi che poteva eseguire erano a loro volta a 16 bit. Per
fornire la Crash Protection e gestire al meglio l'ambiente multiprogrammato Os/2 usava ovviamente
la modalità protetta. Una parte del kernel denominata Memory Manager si occupava di colloquiare
con la MMU e di gestire le segment tables.
In accordo con le specifiche del 286 il sistema di segmentazione prevedeva 2 segment table (tabelle
dei descrittori) per processo, la GDT e la LDT. Nella prima erano contenuti i segmenti che erano
visibili da tutti i programmi (fino ad un massimo di 8192 segmenti) e in essi venivano caricate le
DLL condivise, il kernel e i dispositivi del sistema operativo, nonché gli IFS e le cache per il disco.
Tale codice funzionava a ring 0 ed aveva quindi il massimo del controllo su tutto il sistema.
Ogni programma aveva la sua LDT, lista che specificava tutti i segmenti allocati dalla singola
applicazione; ogni LDT ovviamente era a sua volta un'entry della GDT ed era protetta in modo che
solo il sistema operativo potesse averne l'accesso in scrittura. Il codice applicativo girava a ring 3,
per cui aveva il minimo dei privilegi di accesso e di fatto non poteva colloquiare direttamente con
l'hardware (a dire il vero c'erano alcuni moduli nel sistema a ring 2 i quali potevano avere accesso
diretto all'hardware).
Quando la memoria fisica era piena veniva utilizzato uno swapfile e i segmenti meno utilizzati
venivano scaricati su disco; quando un programma li tornava a richiedere questi venivano ricaricati
in memoria.
3.2: Os/2 a 32 bit
Con l'avvento dei processori 386, IBM (la Microsoft aveva abbandonato il progetto per dedicarsi a
Windows e a Windows NT) creò la prima versione a 32 bit del suo Os/2, che di fatto fu il primo
sistema operativo a 32 bit sull'architettura x86. La versione 2.0 manteneva la compatibilità con il
vecchio formato di programmi a 16 bit e inoltre sfruttava la modalità virtual 86 per poter eseguire
anche programmi DOS e Windows al suo interno. Per fare tutto questo il sistema sfruttava (e sfrutta
tuttora) la modalità protetta a 32 bit nella versione «segmentazione con paginazione».
3.2.1: I 3 formati di indirizzi
Os/2 32 bit supporta 3 diversi formati di indirizzi all'interno di un programma:
• Il formato 16:16 -> 16 bit di selettore e 16 di offset, è il formato di indirizzo virtuale usato dai
programmi a 16 bit per Os/2; ci sono alcuni moduli del sistema che per necessità o limitazione
sono ancora a 16 bit, anche questi adottano tale formato. Ad ognuno di questi programmi è
assegnata una specifica LDT in modo da poter emulare le funzioni di segmentazione pura.
• Il formato 16:32 -> 16 bit di selettore e 32 di offset, è il nuovo formato di indirizzo virtuale
possibile in Os/2 a 32 bit. Le parti interne a 32 bit del sistema operativo specificano sempre il
Memoria Virtuale
Pag. 22
loro indirizzo in questo formato; trattandosi di elementi strutturali del sistema (e quindi globali)
non è necessario assegnare loro una LDT.
• Il formato 0:32 -> 0 bit di selettore e 32 di offset. È stato ritenuto che difficilmente si sarebbero
visti programmi di dimensione superiore a 4 Gb; inoltre ai programmatori non è mai piaciuto
lavorare su segmenti multipli, per cui avendo a disposizione uno spazio di indirizzamento da 4
Gb avrebbero comunque scritto i loro programmi per un unico segmento. Ecco quindi che
prevedendo questa esigenza (che invero si concretizza in tutti i sistemi operativi per personal
computer) è stato deciso che i programmi a 32 bit dovessero avere un unico segmento. La LDT
contiene quindi un descrittore di un unico segmento la cui dimensione è variabile ma il cui
indirizzo base è sempre 0. Questo indirizzo virtuale «implicito» non necessita quindi di venir
tradotto in indirizzo lineare, in quanto tale traduzione darebbe come risultato l'offset stesso. Se
da un lato questa scelta comporta la perdita della maggior parte dello spazio virtuale di
un'applicazione (che potrebbe essere 8192 volte maggiore), rappresenta un buon compromesso in
termini di velocità (si salta la prima traduzione, o meglio è implicita) e permette di risparmiare
spazio in quanto si codificano gli indirizzi come soli 32 bit e non come 48 bit. Certamente però
per accedere ad un segmento del sistema operativo (di memoria condivisa) occorre specificare
l'indirizzo a 48 bit completo.
È possibile passare facilmente dal formato 16:16 al formato 0:32 semplicemente shiftando alcuni
bit, mentre la conversione inversa necessita di una somma e di una moltiplicazione.
3.2.2: Primo livello di traduzione in indirizzo lineare
La traduzione da indirizzo virtuale in formato 16:32 e 16:16 segue fedelmente gli schemi già visto
per la segmentazione nel 386: all'interno del processore viene caricato l'indirizzo base della GDT e
della LDT (questo cambia ad ogni context switching). Il selettore identifica la posizione dentro
queste due tabelle del descrittore del segmento attivato. Identificato il descrittore avvengono una
serie di operazioni di controllo: anzitutto si verifica che l'offset sia minore della massima
dimensione specificata in quel segmento, quindi si controlla che l'RPL dell'indirizzo attualmente
attivo in memoria abbia privilegi sufficienti (sia ad un ring minore o uguale) a quello specificato nel
campo DPL del descrittore. Poi si verificano le flag read-only/read-write per sapere se è possibile
scrivere su dato segmento oppure soltanto leggere. Se i controlli non vanno a buon fine si genera un
«segment fault» (detto anche trap D) che viene notificato al sistema operativo, e Os/2 non può far
altro che terminare il programma che ha causato il problema; se invece i controlli passano l'offset
del segmento viene sommato all'indirizzo base. In questo modo si può determinare l'indirizzo
lineare univoco.
Le applicazioni a 32 bit non ammettono più di un segmento per applicazione, il che significa che
non possono avere più di una singola voce nella LDT. Di questa voce molti dei campi sono
standard: un'applicazione viene eseguita a ring 3, ha sempre i permessi di scrittura da parte dello
stesso ring negati, ha sempre lo stesso indirizzo base (00000000) ed è sempre paginata; cambia solo
la lunghezza del segmento: di default ai programmi vengono assegnati segmenti di 64 MB, ma
questo segmento può crescere in dimensioni su richiesta dell'applicazione stessa.
3.2.3: Le arene di sistema
Come accennato in precedenza il 386 prevede la mappatura obbligatoria nello spazio di
indirizzamento lineare del codice contenuto nella GDT perché questo sia disponibile per tutte le
Memoria Virtuale
Pag. 23
applicazioni. È pertanto necessario riservare alcuni indirizzi in ogni spazio di indirizzamento (ogni
applicazione ne ha uno diverso) per questi segmenti. Per un problema di compatibilità con i
programmi a 16 bit (che come abbiamo visto non potevano indirizzare più di 512 MB di memoria) è
stato imposto che il limite allocabile dalle singole applicazioni fosse appunto 512 MB. Lo spazio
sopra questo limite (ossia da 512 Mb a 4 Gb) è riservato per i segmenti di sistema.
Os/2 gestisce queste problematiche introducendo dei controllori d'area, chiamati arene. Os/2 lavora
con 3 arene distinte: l'arena privata, l'arena condivisa e l'arena di sistema. La prima contiene tutto
il codice proprietario del programma attivo e ad ogni context switch quella parte viene riallocata
(come vedremo) attivando diverse page directories. L'arena condivisa contiene tutto il codice
accessibile dalle applicazioni che deve essere visibile da parte di tutti i programmi: in sostanza si
tratta delle DLL di sistema che vengono tutte caricate in questa zona. L'arena di sistema contiene
invece tutto il codice del sistema operativo; le applicazioni non possono utilizzare dato codice per
cui è stato scelto di mapparlo sopra il limite dei 512 MB. L'arena condivisa e di sistema sono il
risultato della mappatura dei segmenti della GDT nella memoria lineare e quindi non sono soggette
al cambio delle page directories. Poiché i programmi devono necessariamente poter accedere
all'arena condivisa (oltre che a quella privata) le due arene convivono entrambe al di sotto del limite
dei 512 Mb.
In particolare l'arena privata inizia all'indirizzo lineare 0000000 (anche se i programmi Os/2 non
possono allocare dati nei primi 64 Kb di memoria) e termina di default a 64 Mb. L'arena condivisa
inizia all'indirizzo lineare corrispondente a 304 MB e termina a 512 MB. La zona compresa tra 64
Mb e 304 Mb è una zona di espansione: l'arena privata si espande verso l'alto su richiesta dei
programmi, l'arena condivisa si espande verso il basso quando si richiede il caricamento di ulteriori
librerie.
Da questo si deduce che prima o poi le due arene andranno in conflitto: il limite inferiore dell'arena
condivisa rappresenta la massima memoria allocabile dalla singola applicazione e oltre quel limite il
sistema operativo non autorizza l'accrescimento ulteriore del programma dichiarando di aver
esaurito la memoria; in realtà la memoria non è affatto esaurita (nel qual caso verrebbe
semplicemente allargato il file di appoggio sul disco fisso) sono esauriti gli indirizzi lineari.
Analogamente se c'è almeno un programma caricato nel sistema che alloca memoria fino al limite
consentitogli, non è più possibile allargare l'arena condivisa (ossia caricare altre DLL), altrimenti
questa sconfinerebbe (anche se soltanto in quel contesto di programma) nell'arena privata. Di fatto
quindi la massima dimensione del singolo programma sotto Os/2 è di circa 300 Mb in condizioni di
utilizzo normale.
L'arena di sistema invece utilizza come indirizzo base 1.5 Gb e termina praticamente a 4 Gb (c'è
una piccola area riservata attorno a tale limite). Tra 512 Mb e 1.5 Gb c'è una parte della memoria
non mappata e riservata per utilizzi futuri.
Memoria Virtuale
Pag. 24
4 Gb
Arena di
sistema
1.5 Gb
Riservata per
usi futuri
512 Mb
Arena condivisa
304 Mb
Area di
espansione
64 Mb
Arena privata
PID 1
Arena privata
PID 2
Arena privata
PID 3
0
La figura mostra la configurazione ad arene della memoria lineare
3.2.4: La Paginazione a 2 livelli
Lo spazio di indirizzamento lineare viene mappato in memoria fisica attraverso la tecnica della
paginazione a due livelli. In Os/2 per comodità si fa uso di 2 page directory separate: la SPD
(System Page Directory) che mappa l'arena di sistema, e la PPD (Process Page Directory) che
mappa l'arena privata e l'arena condivisa; a dire il vero a partire dalla versione «Warp 3» l'arena
condivisa è mappata dalla SPD ad eccezione delle pagine marcate come «Read-Write».
Come è facile intuire ogni processo ha la sua PPD che viene sostituita all'atto del context switch.
Le page directory sono costituite dalle cosiddette PDE (Page Directory Entry) che contengono i dati
identificativi della page table a cui puntano come specificato in precedenza. Le page table hanno
una struttura analoga alla page directory.
Anche sulle pagine sono definiti dei privilegi di accesso e il controllo Read-Write/Read-Only che
sono ereditati da quelli impostati a livello del relativo segmento. Qualora si verificasse una
violazione di questi permessi il processore notifica il problema al sistema operativo, errore che è
chiamato «Page Fault» (detto anche Trap E). Da notare che il page fault va analizzato con più
attenzione da parte del sistema operativo: è infatti possibile che l'operazione fosse corretta, ma la
pagina si trovasse sul disco fisso e non in memoria; in tal caso sarà compito del sistema operativo
recuperare tale pagina prima di far proseguire l'esecuzione del programma. Qualora invece l'errore
sia effettivamente un errore, il programma viene di norma terminato.
Memoria Virtuale
Pag. 25
3.2.5: La gestione delle pagine fisiche e virtuali
Per accelerare la gestione della memoria da parte del sistema operativo, Os/2 utilizza altre tre
tabelle:
• La VPT (Virtual Page Table): ogni pagina allocata nel sistema da qualunque programma viene
registrata in questa tavola
• La PFT (Page Frame Table): contiene una voce per ogni pagina indirizzabile di memoria fisica;
se la pagina è utilizzata allora la relativa voce conterrà un puntatore alla page table entry che
mappa tale pagina
• La PRT (Page Range Table): contiene una lista della pagine che possono essere indirizzate
Nel sistema esistono fondamentalmente due tipi di pagine: pagine fisiche che corrispondono a 4 Kb
di RAM in memoria centrale e pagine virtuali che rappresentano 4 Kb di memoria allocati per
qualsiasi ragione dal sistema operativo.
Ogni pagina di memoria fisica (ossia tutte quelle che hanno la propria entry nella PFT) si può
trovare nei seguenti stati:
• Free: la pagina non è attualmente in uso da alcuna applicazione
• Attached: la pagina è allocata dal sistema, ossia una PTE (un elemento di una page table) punta a
tale area di memoria. Ci sono 3 sottostati possibili:
• Swappable: la pagina può essere salvata sullo swapfile
• Locked: la pagina è momentaneamente in uso da un Device Driver e temporaneamente deve
restare in memoria
• Resident: La pagina deve rimanere in memoria per tutto il tempo, non può essere copiata sullo
swapfile
• Idle: La pagina non è stata usata di recente. Ci possono essere due sottostati:
• Dirty: la pagina è stata modificata rispetto all'ultimo (eventuale) salvataggio su disco e deve
essere risaltata prima di essere cancellata
• Clean: la pagina non ha subito modifiche rispetto all'ultimo salvataggio per cui può essere
tranquillamente cancellata
Per quanto riguarda invece le pagine virtuali (ossia tutte quelle che hanno la propria entry nella
VPT) esistono i seguenti stati possibili:
• Free: la pagina non è stata allocata
• Decommitted: la pagina è stata «prenotata» ma non è ancora in uso
• Committed: la pagina è in uso e sono possibili i seguenti sottostati:
• Guard: uno stato utilizzato dagli stack dei programmi
• AOD: Allocated On Demand, ossia non ci sono ancora scritti i dati all'interno (appena una
pagina diventa committed entra in questo stato)
• TBL: To Be Loaded, ossia la pagina deve venire caricata da un files presente sul disco
• Present: identifica che la pagina è normalmente in uso
• Idle: la pagina non ha ricevuto accessi da diverso tempo; la pagina ritorna nello stato present
dopo il suo utilizzo
• Swapped: la pagina si trova fisicamente sul disco
Memoria Virtuale
Pag. 26
Memoria Fisica
Memoria Virtuale
Swappable
Idle
Decommitted
Free
Swapped
Resident
AOD
TBL
Diagramma di Venn della memoria: l'ellisse di destra rappresenta la Memoria Virtuale, quella di
sinistrala Memoria Fisica; l'intersezione è la memoria fisica utilizzata.
3.2.6: La gestione dei moduli
Un modulo in Os/2 è qualunque entità indipendente; generalmente un modulo può essere visto
come il codice contenuto in ogni segmento allocato all'interno del sistema operativo. Alcuni moduli
appartengono al sistema operativo e possono essere il kernel stesso, ogni device driver, i driver
virtuali, la cache, ecc... altri moduli appartengono chiaramente all'arena condivisa, tipicamente le
DLL; i moduli utente sono generalmente chiamati processi (task in altri sistemi operativi) e sono in
effetti i programmi in esecuzione. Tutti i moduli caricati nel sistema sono registrati in un'apposita
tabella chiamata MT (Module Table).
Ad ogni programma che si carica viene assegnata un'area di memoria chiamata PTDA (Per Task
Data Area) che contiene tutte le informazioni di carattere «amministrativo» del processo stesso:
ogni PTDA contiene il PID (Process ID, identificativo univoco del processo), il PPID (Parent PID,
ossia il PID del processo da cui è stato generato, e teoricamente l'unico processo che può invocarne
la terminazione), la lista delle risorse allocata, informazioni sui semafori, le DLL utilizzate, il
puntatore alla MTE (Module Table Entry) per quel processo, il nome del processo stesso (che
usualmente è quello del file eseguibile eseguibile da cui è stato generato), informazioni sul
concatenamento tra i gestori delle arene, l'indirizzo base della PPD (Process Page Directory) il
riferimento alla LDT e alla GDT (la PTDA è puntata da un elemento della GDT) nonché una serie
di informazioni vitali aggiuntive per il processo stesso.
Ricapitolando: ogni «oggetto di memoria» allocato comporta:
8 byte -> nelle DTE (Entry nella GDT o LDT)
4 byte * numero delle pagine dell'oggetto -> nella PTE (Page Table Entry)
12 byte * numero totale delle pagine in RAM -> per un Page Frame Record (una entry della PFD)
12 byte * numero delle pagine dell'oggetto -> nella VPT
3.2.7: Il file di pagina
Come abbiamo visto il Memory Manager di Os/2 lavora su un file (che si chiama swapper.dat) nel
quale il sistema operativo può salvare pagine provenienti dalla memoria per poter permettere di
allocare più memoria virtuale di quanta memoria fisica sia effettivamente installata.
Questo swapfile ha una dimensione iniziale che viene settata nel comando menman del config.sys di
Os/2 ma ovviamente non è limitato a tale dimensione: può accrescere qualora occorra ed
eventualmente restringersi; il motivo di assegnarli una dimensione iniziale è quello di ridurre il
tempo perso dal sistema operativo stesso ad accrescerlo o a rimpiccilirlo, operazione che viene fatta
Memoria Virtuale
Pag. 27
il meno possibile e con incrementi di 1 Mb; inoltre al boot il sistema operativo cerca di costruire
uno swapfile il più possibile contiguo per ridurre il tempo di ricerca al suo interno; quando il file si
ridimensiona non è detto che trovi dello spazio contiguo, il che prelude ad un decremento della
velocità e di efficienza.
La dimensione massima del file di scambio è limitata a 2 Gb, questo per una limitazione delle
partizioni HPFS che non potevano gestire files più lunghi di 2 Gb. Ora con il JFS la limitazione è
scomparsa, ma non la flag nel memory manager per cui tuttora la massima memoria swappabile è 2
Gb (per l'esattezza ci sono solo 31 bit per identificare la posizione sul disco nelle VPE, virtual page
entry).
Quando una pagina viene copiata sullo swapfile e rimossa in memoria, la relativa page table riporta
l'avvenuta copiatura e non identifica più l'indirizzo base in memoria, bensì crea un puntatore al
relativo oggetto della VPT; tale oggetto (ossia una VPE) conterrà la posizione fisica in cui la pagina
viene salvata nello swapfile.
È interessante notare che già quando una pagina di memoria passa in stato idle, la sua PTE già non
punta più alla pagina in memoria fisica, bensì alla relativa entry nella VPT: questa punterà ancora in
RAM (e non sul disco fisso) fintanto che la pagina non verrà scaricata dalla memoria.
Le pagine swappate sul disco vi rimangono fintanto che il programma a cui appartengono con
interrompe la sua esecuzione: a dire il vero anche oltre la terminazione, in quanto la rimozione delle
suddette è un'operazione che viene fatta nei «tempi morti» in modo da non appesantire inutilmente
il sistema stesso; ecco perché in situazioni di grande utilizzo dello swapfile, la chiusura di un
programma non corrisponderà ad un immediato decremento delle dimensioni del file, anzi ad un
possibile incremento qualora ci siano altri programmi che stiano utilizzando attivamente la
memoria.
Una pagina ricaricata mantiene le informazioni nella VPT sulla sua posizione nello swapfile;
qualora questa debba venir rimossa nuovamente dalla memoria, va a sostituire la precedente
scrittura solo se è stata modificata, altrimenti viene semplicemente cancellata per risparmiare
tempo. Questo sistema però comporta notevoli dimensioni del file di scambio che (almeno a livello
teorico) potrebbe crescere fino a che tutti i programmi attivi non vengono swappati.
Il gestore del file di scambio (che è a sua volta un componente del kernel di Os/2) tende a far
rimanere lo swapfile entro i limiti della dimensione iniziale: una volta che questa è esaurita, viene
ingrandito a blocchi di 1 Mb; periodicamente, negli idle time del sistema, avviene una
deframmentazione del file stesso, ossia le pagine salvate oltre la dimensione iniziale tendono a venir
spostate in eventuali «buchi» presenti nelle posizione precedenti. Qualora lo swapfile avesse più di
2 Mb liberi in fondo ad esso, la sua dimensione verrebbe ridotta di 1 Mb e questo fino a raggiungere
la dimensione iniziale. È questo un ulteriore motivo per cui è opportuno scegliere con «saggezza»
tale dimensione iniziale, visto che queste elaborazioni (allargamenti, deframmentazione,
rimpicciolimenti, ecc...) causano notevoli rallentamenti.
3.2.8: L'High Memory Support
Come si è detto nelle sezioni precedenti ogni programma ha a disposizione all'incirca 300 Mb di
memoria. Questa quantità, malgrado fosse soltanto il 7% della memoria effettivamente allocabile
dagli indirizzi lineari a 32 bit, era pur sempre una quantità considerevole e fino a qualche anno fa
più che sufficiente per tutti i programmi.
Recentemente c'è stata una forte spinta verso l'occupazione «smodata» della memoria ed ecco che
Memoria Virtuale
Pag. 28
300 Mb potrebbero essere considerati un limite eccessivamente piccolo. A partire dalla versione
Warp Server Advanced for SMP e nelle successive WSeB, ACP, MCP ed eComStation, il kernel di
Os/2 riesce a gestire altre due arene di memoria. Questa tecnologia è stata chiamata HMS e le due
arene sono la High Shared Arena e la High Private Arena (arene condivise e private «alte»). Con un
apposito comando del config.sys è possibile specificare l'indirizzo di partenza dell'arena di sistema
(il comando è il virtualaddresslimit) di fatto riducendo la dimensione dell'arena di sistema; il limite
massimo di questo valore (chiamato virtual address space) è 3 Gb. Tra 0.5 Gb e 3 Gb (o il valore
impostato) si forma così un nuovo spazio di indirizzamento che viene mappato dalle due nuove
arene. Di fatto si forma un sistema speculare a quello che succede sotto i 512 Mb: c'è un indirizzo di
partenza sopra al quale (e fino al virtual address space) si estende l'arena condivisa alta. Da 512 Mb
fino a dato valore si estende invece l'arena privata alta. I programmi che ne fanno richiesta possono
andare a caricare DLL e codice operativo in queste due aree, estendendo di fatto il loro spazio di
indirizzamento fino a 3 Gb (in realtà contando le arene condivise lo spazio si riduce circa a 2.5 Gb,
comunque una quantità estremamente grande, superiore a quelle degli altri sistemi operativi, quali
Linux e Windows).
La figura sottostante riassume il comportamento della memoria con l'HMS attivo. È importante
notare che SOLTANTO i programmi che la prevedono possono utilizzare la nuova arena alta. Il
motivo per cui evitare di ridurre eccessivamente la dimensione dell'arena di sistema è la riduzione
del numero di processi totali caricabili dal sistema operativo.
4 Gb
Virtual address
limit
Limite variabile
Arena di
sistema
Arena condivisa
alta
Arena privata
alta
PID 1
Arena privata
alta
PID 2
Arena privata
alta
PID 3
Arena privata
PID 2
Arena privata
PID 3
512 Mb
Arena condivisa
304 Mb
Area di
espansione
64 Mb
Arena privata
PID 1
0
La configurazione delle arene con abilitata la funzione HMS
Memoria Virtuale
Pag. 29