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