Quando uno dei processi presenti termina, la memoria da esso occupata deve essere resa disponibile, in modo da permettere a qualche nuovo processo di essere caricato ed eseguito. Il SO quindi deve tenere traccia di quali parti della memoria risultano occupate e quali libere.
Se il processo da caricare ha dimensioni più grandi della memoria disponibile, il SO, piuttosto che rinunciare a caricarlo, ne trasferisce in memoria solo una parte lasciando quella rimanente su disco. Al momento opportuno, quando cioè sarà necessario eseguire istruzioni o fare riferimento a dati situati nella parte di programma non presente in memoria, il SO provvede a caricare la parte mancante. Se non dovesse trovare spazio sufficiente, esso utilizza una parte di quello ccupato da altri processi attivi.
Questo procedimento è del tutto trasparente, per cui l'effetto apparente è quello di lavorare con un sistema che dispone di una memoria maggiore di quella che realmente il sistema possiede. Esso è chiamato virtualizzazione della memoria, e lo analizzeremo fra poco in dettaglio.
La memoria “apparente” con cui il sistema lavora si chiama memoria virtuale.
Possiamo concludere, da quanto esposto, che il gestore della memoria di un SO svolge principalmente le seguenti funzioni:
Sappiamo che un programma è composto da un insieme di istruzioni e di chiamate a funzioni ciascuna delle quali assolve un determinato compito come ad es. printf() Alcune di queste funzioni non risiedono inizialmente nel file del programma e vengono chiamate esterne perchè il loro codice si trova in un file oggetto esterno al programma che generalmente si trova nella directory /lib.
Quando si compila un programma ciò che si ottiene è un file di estensione *.obj. Questo file contiene la traduzione in linguaggio assembler del programma ma non è ancora eseguibile, perchè non contiene gli "indirizzi fisici" delle istruzioni ma solo indirizzi logici. Vediamo meglio che significa.
Esaminiamo es.il seguente programma C
prova.cpp |
void MiaFunz(){ |
Il programma chiama prima una funzione interna MiaFunz e poi una la funzione esterna printf()
Il file *.obj si potrebbe presentare così (i numeri sono puramente indicativi):
prova.obj |
//miafunz 0100 ...... ...... 011A ....... //main ....... ....... 012C call 0100 //chiamata alla funzione Miafunz ......... 02A0 call 0000:0000 //chiamata a printf() ....... ....... |
Come si vede il compilatore sostituisce la chiamata ad una funzione interna con call 0100 perchè la funzione fa parte del programma, mentre sostituisce printf() con call 0000:0000 in quanto siamo ancora nella fase della compilazione e il codice do printf() non è ancora stato "inserito" nel programma.
Il compito di creare un file eseguibile è affidato al linker. Il linker mette insieme i due file, quello del nostro programma e quello contenente la funzione esterna creando uno spazio d'indirizzi unico
Ecco come potrebbe presentarsi il file *.exe creato dal linker
prova.exe |
//miafunz 0100 ..... ..... 011A .....
//main
....... //la
libreria esterna che contiene printf() viene caricata a partire da 020A
020A //inizio libreria ..... 04A0 //inizio codice della funzione printf() .... .... |
Quando il programma dovrà essere eseguito, il loader deve semplicemente
I limiti sono principalmente tre:
I più diffusi sono: