|
La gestione della memoria Heap
Cenni sui processi
Si definisce Programma un insieme di Istruzioni destinate a adempiere ad una specifica funzione.
Il programma è staticamente memorizzato in un file insieme alle costanti del problema.
Quando il Sistema Operativo (su nostro ordine) porta in esecuzione un programma, esegue le seguenti operazioni:
- Legge l’intestazione del file con le richieste del programma.
- Riserva in memoria spazio sufficiente per collocare:
- il codice eseguibile del programma
- le costanti, le variabili globali e le variabili dichiarate static;
- uno Stack per le variabili locali dinamiche, per il passaggio di parametri alle subroutine e per gli indirizzi di ritorno dalle stesse;
- un certo quantitativo di memoria Heap per la gestione dinamica delle variabili.
- Riserva tutte le altre eventuali risorse necessarie (porta seriale, scheda audio, ecc…).
- Assegna il ProgramCounter sulla prima istruzione ed inizia l’esecuzione.
A questo punto non parleremo più di Programma ma di Task o Processo (ogni programma
può generare uno o più processi). Al termine del Processo tutte le risorse occupate
vengono rilasciate.
Allineamento dei dati
A seconda del Microprocessore e del S.O. utilizzato, l’accesso alla memoria è più efficiente se i dati sono allineati sugli indirizzi multipli di un quanto base (2, 4, 8 byte) in genere corrispondente al tipo
int.
Occorre evitare, quindi, di dichiarare variabili che creino disallineamento, in particolare i
char.
Ad esempio, se l’accesso ottimale è a 4 byte, la dichiarazione
char a, b, s[31];
andrebbe sostituita con
char a, b, a1, b1, s[32];
Alcuni compilatori consentono la scelta di ottimizzare per spazio o per velocità:
nel secondo caso allineano automaticamente i dati agli indirizzi più efficienti, inserendo spazi vuoti.
Multiprogrammazione
Verrà definito
Multitasking un S.O. che sia in grado di eseguire più Processi contemporaneamente, in un apparente parallelismo.
Quindi, con il termine multitasking, definiamo la capacità di un elaboratore di servire più processi dando a ciascuno l’impressione di utilizzare la macchina reale e mettendo invece a loro disposizione solo il servizio di una macchina virtuale; il multitasking simula la presenza di più elaboratori all’interno di uno stesso sistema, gestendo adeguatamente le diverse risorse di cui dispone.
Il tempo della C.P.U. viene suddiviso in quanti di alcuni millisecondi, che vengono assegnati ai vari Thread (ovvero le unità minime di elaborazione).
La multielaborazione è la modalità secondo la quale più processi avanzano, in un parallelismo reale, su un sistema costituito da due o più C.P.U. autonome, ma interconnesse, che condividono l’utilizzo dalla memoria centrale, e delle periferiche.
Risorse
Per risorsa si intende qualsiasi entità hardware o software necessaria alla creazione o all’avanzamento di un processo.
Le risorse sono dette riusabili quando possono essere date in uso ripetutamente a diversi processi. Il numero massimo di processi che possono usare contemporaneamente la risorsa R è detta
molteplicità di R.
Se la molteplicità di R è unitaria essa è detta seriale: ad esempio la C.P.U., la stampante, un file con operazioni di lettura/scrittura.
Sono risorse non seriali, anzi a molteplicità infinita, ad esempio, le aree di memoria accessibili per la sola lettura.
Se R è una risorsa a molteplicità finita, ogni processo deve svolgere la seguente sequenza di azioni:
-
Richiesta e ottenimento di R;
-
Uso di R;
-
Rilascio di R.
Memoria Heap
Un Heap è un lotto di memoria assegnato ad un processo specifico, per immagazzinare strutture dati, la cui esistenza o dimensione non possa essere determinata prima che il programma sia eseguito.
Quando sono richiesti blocchi di memoria, un processo richiama una funzione per allocare memoria nell’heap, utilizza lo spazio ottenuto e, in seguito, libera la memoria con un’altra funzione (memoria dinamica).
Se necessario, il processo può chiedere heap aggiuntivi al S.O.
Il S.O. gestisce l’heap in paragrafi minimi (di 16, 32 o 64 byte); quindi, la richiesta di un certo quantitativo di memoria viene arrotondato per eccesso al multiplo successivo del paragrafo minimo; inoltre, viene generalmente aggiunto un paragrafo per informazioni accessorie.
L’uso continuo di allocazione e rilascio di memoria heap causa un frazionamento dello spazio disponibile con conseguente peggioramento delle prestazioni. Solo alcuni S.O. possono deframmentare lo spazio, ridisponendo tutti i blocchi.
Il programmatore deve attentamente valutare costi e benefici della gestione Heap, sostituendola quando possibile con un’assegnazione iniziale di tutto lo spazio necessario, o con la creazione dinamica di spazio sullo Stack.
Memoria dinamica in C
In C la gestione dell’Heap avviene attraverso le funzioni
-
malloc
-
calloc
-
realloc
-
free
Per tutte è richiesto l’header <malloc.h>.
void *malloc ( size_t Size );
La funzione malloc alloca un blocco di memoria della dimensione di Size byte, e restituisce :
Per ottenere un puntatore a tipi diversi da void, occorre usare un operatore
cast sul valore restituito; lo spazio di memoria assegnato può contenere dati di qualsiasi tipo.
Se size è 0, malloc alloca un elemento di lunghezza nulla nell’heap e ne restituisce l’indirizzo.
Occorre sempre controllare il valore restituito da malloc, anche se la memoria richiesta è piccola.
malloc alloca memoria almeno di Size byte, ma il blocco occupato può essere più grande a causa dello spazio richiesto per l’allineamento dei dati e per la manutenzione dei blocchi.
Esempio:
#include <stdio.h>
#include <malloc.h>
void main( void )
{ char *Buffer;
Buffer = (char *) malloc ( 200 );
if ( Buffer == NULL )
{ printf ( "Memoria disponibile insufficiente\n" );
}
else
{ printf( "Spazio allocato\n" );
gets ( Buffer );
printf( "%s\n", Buffer );
free ( Buffer );
printf( "Memoria liberata\n" );
}
}
void *calloc ( size_t Numero, size_t Size );
La funzione calloc alloca spazio di memoria heap per un array di Numero elementi, ciascuno di dimensioni
Size byte. Ogni elemento è inizializzato a zero.
Per il resto calloc esegue le stesse operazioni della funzione malloc.
Esempio:
#include <stdio.h>
#include <malloc.h>
void main( void )
{ long *Buffer;
Buffer = (long *) calloc ( 40, sizeof(long) );
if (Buffer == NULL )
{ printf( "Memoria disponibile insufficiente\n" );
}
else
{ printf ( "Disponibili 40 longint\n" );
. . . . . .
free ( Buffer );
}
}
void *realloc ( void *BlockAddress, size_t NewSize );
La funzione realloc cambia la dimensione di un blocco di memoria Heap precedentemente allocato con malloc, calloc o realloc (indirizzo iniziale BlockAddress) portandolo a NewSize byte; restituisce un puntatore void al blocco di memoria riallocato ed eventualmente spostato: il nuovo indirizzo restituito, dunque, potrebbe essere differente da BlockAddress.
Il contenuto del blocco non è modificato per un numero di byte pari alla minore tra vecchia e nuova dimensione, anche se il nuovo blocco potrebbe risiedere in una diversa zona di memoria.
-
Se BlockAddress è NULL e NewSize è diverso da zero, realloc è equivalente a malloc.
-
Se non c’è abbastanza memoria disponibile per espandere il blocco alla dimensione richiesta, realloc restituisce NULL e il blocco non è modificato.
-
Se BlockAddress è un puntatore valido e NewSize è zero, realloc libera il blocco (come una chiamata a free) e restituisce NULL.
Per ottenere un puntatore a tipi diversi da void, occorre usare un operatore cast sul valore restituito; lo spazio di memoria assegnato può contenere dati di qualsiasi tipo.
void free ( void *BlockAddress );
La funzione free rilascia un blocco di memoria heap, di indirizzo BlockAddress, che sia stato precedentemente allocato con chiamate a malloc, calloc o realloc.
Se BlockAddress è NULL, il puntatore è ignorato e free esegue un immediato ritorno dalla funzione.
Tentativi di liberare memoria non allocata da malloc, calloc o realloc, ovvero già liberata,
possono influire sulle successive allocazioni e causare errori imprevedibili.
La funzione free non restituisce alcun valore.
ATTENZIONE:
Quando utilizziamo la memoria dinamica Heap, è necessario rilasciare lo spazio non più necessario
per evitare il collasso del processo per mancanza di memoria.
Un errore comune è la perdita dell’indirizzo del blocco prima di liberarlo:
#include <stdio.h>
#include <malloc.h>
void main( void )
{ int x, *Pt;
Pt = (int *) malloc ( 200 ); // occupa 50 interi
if (Pt == NULL )
{ printf ( "Memoria disponibile insufficiente\n" );
}
else
{ printf ( "Spazio allocato\n" );
. . . .
Pt = &x; // Il puntatore
perde il blocco Heap
*Pt = 12345;
. . . .
free ( Pt ); // errore nel gestore dell’Heap
}
}
|