GESTIONE DELLA MEMORIA




Abbiamo visto che in un sistema multitasking sono presenti in memoria più processi attivi ciascuno dei quali si trova in uno dei tre possibili stati di pronto, esecuzione o bloccato. Questo fatto comporta una gestione più complessa della memoria.

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:

Il problema della rilocazione

Per rilocazione indendiamo la possibilità di spostare un programma o una sua parte, da un punto ad un altro della memoria, mentre è esso in esecuzione. Per chiarire meglio questo concetto molto importante vediamo in che modo nei sistemi monoprogrammati viene caricato ed eseguito un programma.

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(){

      .........
      .........
    }


  void main() {
     .....
     .....
     Miafunz();
     .......
     printf(......);
     ........
     ........
}


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

        .......
        .......
012C  call 0100           //chiamata alla funzione Miafunz
         .........
02A0  call 04A0    //chiamata a printf()
....... 
.......

//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

  1. selezionare un'indirizzo base disponibile a partire dalla quale caricare l'intero programma
  2. caricare il programma all'indirizzo fisico ottenuto sommando l'indirizzo base del segmento con l' offset  (0100, 0101, 0102 etc...)
Il tipo di allocazione appena descritto viene chiamata statica e presenta dei limiti che non la rendono adatta ai sistemi multitasking. ( Infatti veniva impiegata nei sistemi monoprogrammati come il  DOS).

I limiti sono principalmente tre:

Allo scopo di gestire in modo efficiente della memoria, sono stati proposti diversi modelli di memoria. 

I più diffusi sono: