CAPITOLO 8:
GESTIONE DELLA MEMORIA LOCALE








Il gestore della memoria lavora in modo ottimale quando i blocchi hanno dimensione di 2–6 K, ma quando si devono usare piccole parti di questi blocchi i problemi di overloading diventano significativi: per questo si hanno delle routine per gestire la memoria locale; è, infatti, possibile allocare un blocco di memoria e designarlo come un Local Memory Heap, all’interno di questo, poi, le applicazione potranno allocare parti di memoria più piccole. Queste routine sono, anche, usate per manipolare gli oggetti.

La problematica che determina l’introduzione della memoria locale è che GEOS permette solo l’uso di un numero limitato di global handle: se un’applicazione ha usato i blocchi per piccole quantità di dati, potrebbe usare troppi handle. D’altra parte, la memorizzazione di molti piccole pezzi di dati in un unico blocco globale da parte della stessa applicazione potrebbe richiedere routine di gestione della memoria molte complicate.

Una volta che un blocco è stato scelto per essere un local heap, in esso possono essere memorizzate piccole quantità di dati, che possono essere spostate al suo interno per soddisfare altre richieste.

Un vantaggio notevole introdotto con la memoria locale è la possibilità di trattare in modo uniforme le piccole porzioni di memoria.

 

8.1: Struttura del local heap
 

Uno heap locale di memoria appare e si comporta come uno globale, la differenza sta nel fatto che uno heap localeè completamente contenuto in un solo blocco globale. Questo blocco viene inizializzato con uno header di 16 byte, una Local Memory Handle Table e un local heap memory; potrebbe essere allocata della memoria per dati anche tra lo header e la handle table per contenere dati extra.

Ogni sezione di memoria all’interno dello heap locale è chiamato chunk e il suo handle chunk handle. Un chunk comprende la parte di dati da memorizzare preceduta da una sezione in cui viene indicata la sua lunghezza in bytes. Quando viene creato uno heap locale viene allocato un certo numero di chunk handle: se viene richiesto un chunk dopo che tutti quelli presenti sono stati assegnati, ci sono delle routine che si occupano di allargare la local handle table, rilocando i chunk presenti, se necessario; i chunk non usati vengono memorizzati in una lista collegata.

Il blocco globale da far diventare heap locale può essere fixed, swapable o discardable: quello che è importante osservare è che un blocco può contenere altri dati oltre il local heap. Al momento della creazione di un nuovo local heap, la sua posizione all’interno deve essere definita attraverso un parametro (in questo caso un offset); a questo punto lo heap definisce e alloca la struttura dello header e la handle table (all’offset specificato); lo spazio tra queste due parti resta "intoccabile" per le routine che si occupano della gestione di questo tipo di memoria. L’offset deve essere più grande dello header oppure zero (in questo caso va usato l’offset di default).

La figura 10 mostra graficamente la struttura di un local heap in corrispondenza di un blocco globale.

Prima di fare una qualsiasi operazione sul local heap, un’applicazione deve avere un lock sul blocco che lo contiene come avviene per un qualsiasi altro blocco; in questo modo si possono evitare problemi di sincronizzazionedovuti alla richiesta ( da parte di altre routine) di un ridimensionamento dello heap e/o di un eventuale spostamento del blocco all’interno della memoria globale; è possibile definire uno heap locale impedendo operazioni di ridimensionamento (questo soprattutto quando lo heap viene definito su un blocco di tipo fixed).

Figura 10: struttura di uno heap locale.

Come è stato già detto in precedenza l’accesso ai chunk avviene attraverso degli handle (chunk handle): questi rappresentano l’offset, all’interno del blocco globale, a partire dal quale è stato memorizzato il chunk in questione. L’indirizzo del segmento dello heap bloccato, combinato con il chunk handle, fornisce un puntatore ad una cella della local handle table; quando viene, poi, combinato il contenuto di tale cella (l’offset del chunk nel blocco globale) con il suo indirizzo (appena calcolato) si ottiene un puntatore al chunk attuale.

La figura 11 (nella pagina seguente) schematizza questo modo di accedere ai chunk.

I chunk possono essere spostati all’interno dello heap: quest’operazione può accadere ogni qual volta il sistema crea o ridimensiona un chunk. Non esiste un meccanismo di locking per i chunk: ogni operazione che porta alla modifica o allo spostamento di un chunk, può, potenzialmente, invalidare i puntatori a tutti gli altri chunk e forzare, conseguentemente, l’applicazione a dereferenziare gli handle per il chunk che sta usando (per conoscerne la nuova locazione).

Figura 11: modo di riferirsi ad un chunk.

Per ovviare a questi inconvenienti sarebbe opportuno seguire la regola generale secondo cui non bisognerebbe salvare un chunk mentre circolano dei messaggi nel sistema e si dovrebbe dereferenziare il chunk handle ogni volta che si vuole ottenere l’indirizzo corrente del chunk.

I chunk vengono ordinati in base alla lunghezza della loro area dati, in modo da accelerare le operazioni di spostamento dei chunk stessi: può, quindi, capitare di ottenere un chunk leggermente più grosso di quello richiesto.

Gli oggetti sono dei tipi particolari di chunk; un Object pointer non è altro che una coppia di handle: quello per individuare un blocco nella memoria globale e quello per individuare un chunk all’interno di un determinato blocco globale.

Lo heap locale ha vari usi, oltre a quello legato alla gestione della memoria: per questo quando si crea un nuovo heap locale bisogna passare, anche, un parametro di tipo enumerativo con cui viene specificato l’uso cui è destinato lo heap appena creato. Tra i tipi d’uso possibili ci sono:

Una volta allocato uno heap, bisogna definire le proprietà da conferirgli fissando degli opportuni flag che vengono memorizzati in record della lunghezza di una parola di memoria; tra questi flag alcuni sono dedicati all’indicazione dello stato corrente dello heap.

 

8.2: Uso della memoria locale
 

Molte applicazioni non hanno bisogno di usare i local heap direttamente; nonostante ciò avranno bisogno di usare meccanismi di gestione dei dati basati su local heap.

La creazione di un local heap è preceduto da due operazioni: l’allocazione e il locking di un blocco nel global heap; la routine che si occupa della creazione e dell’inizializzazione del local heap ha bisogno di parecchi parametri in ingresso come:

Una volta che lo heap è inizializzato è possibile allocare, usare e liberare chunk a seconda delle necessità; l’unico aspetto da tenere a mente è che ogni chunk può essere manipolato quando il blocco globale che lo contiene è di tipo fixed oppure è locked nel global heap.

La routine che si occupa di allocare nuovi chunk dà come risultato il loro handle; la dimensione di tali chunk viene controllata spesso in modo da garantire l’ordinamento in base alla lunghezza dell’area dati dei chunk: quest’operazione può comportare un compattamento del local heap che potrebbe invalidare tutti i puntatori di quello heap; in quest’ottica possono essere spostati anche i blocchi fixed se questo è necessari per espanderli e allocarvi all’interno nuovi chunk.

Ovviamente, come accade per i blocchi globali, una volta allocato un nuovo chunk bisogna dereferenziare il suo handle per poterlo utilizzare: il puntatore che si ottiene come risultato rimarrà valido finché il lock sul blocco non viene rilasciato o finché non viene chiamata una routine che causa una compaction dello heap o un ridimensionamento del blocco. È molto importante usare le routine che garantiscono la sincronizzazione dei dati quando più thread accedono allo heap dal momento che alcune delle routine che possono lanciare gli altri thread, che lavorano sullo stesso heap, potrebbero invalidare i puntatori.

È già stato detto come i chunk possono essere ridimensionati dopo la loro creazione. Se la nuova dimensione è più grande della precedente, il chunk viene ingrandito sistemando alla fine la parte da aggiungere. Per poter fare questa operazione deve essere possibile spostare il chunk all’interno dello heap oppure ridimensionare il blocco globale stesso, ma queste azioni porterebbero il sistema ad invalidare tutti i puntatori dello heap. La parte nuova non viene inizializzata impostando i bytes a zero. Se, al contrario, la nuova dimensione è più piccola della precedente, il chunk viene troncato senza invalidare i suoi puntatori. Nel caso in cui lo heap non sia ridimensionabile le routine che gestiscono questi aspetti falliscono.

È possibile anche aggiungere e cancellare bytes ad un determinato offset all’interno del chunk: nel caso dell’inserimento (a differenza della cancellazione) il chunk va espanso e quindi si potrebbero avere i puntatori invalidati; in questo caso, però, i bytes che vengono aggiunti sono inizializzati a zero.

 

8.3: Chunk array
 

Molto spesso un’applicazione ha bisogno di tener traccia di differenti pezzi di dati e accedervi attraverso un indice. A questo scopo viene messo a disposizione un meccanismo adatto: i chunk array. Le routine che si occupano della gestione dei chunk array permettono di aggiungere o cancellare dinamicamente elementi che si trovano all’interno dell’array, di avere un puntatore ad un determinato elemento specificato dal suo indice, di ordinare l’array secondo dei criteri scelti arbitrariamente.

I chunk array possono avere:

Questi due tipi di array vengono gestiti dinamicamente, a prescindere dalle caratteristiche che possono avere gli elementi che li compongono.

I chunk array sono implementati in testa alle routine della memoria locale; l’intero array è visto come un singolo chunk nello heap locale. Le dimensioni di tali array devono aggirarsi intorno ai 6K perché la memoria venga gestita efficientemente, nel caso di array più grandi (che non devono superare i 64K) si ricorre all’uso di huge array.

Un chunk array è composto da:

Tra lo header e gli elementi può esserci dello spazio richiesto dall’applicazione.

La struttura di un chunk array cambia a seconda delle caratteristiche degli elementi che contiene.

 

8.3.1: Array a dimensione uniforme

Nel caso di array a dimensione uniforme la struttura è piuttosto semplice: dopo lo header e lo spazio extra vengono direttamente gli elementi; questi si susseguono uno dopo l’altro senza lasciare spazio tra uno e l’altro. Un’applicazione può richiedere un puntatore a qualsiasi elemento specificando l’indice: questo è ottenuto moltiplicando l’indice per la dimensione (fissa) di ciascun elemento e aggiungendo il tutto all’indirizzo del primo elemento. Questo non toglie la possibilità di avere un accesso sequenziale agli elementi dell’array, passando da un elemento al successivo senza dover richiedere specificatamente un puntatore.

Figura 12: chunk array a dimensione uniforme


 

8.3.2: Array a dimensione variabile

La struttura di un’array a dimensione variabile è leggermente più complessa. Dopo lo header e lo spazio extra viene allocata una look-up table con entry di due bytes che contengono l’offset dall’inizio del chunk allo specifico elemento; questa tabella viene mantenuta automaticamente dalle routine di gestione dell’array.

Figura 13: chunk array a dimensione variabile

Quando un’applicazione vuole riferirsi ad un certo elemento la routine addetta all’accesso all’array raddoppia l’indice dell’elemento cui accedere e somma il tutto all’offset tra l’inizio del chunk e quello della look-up table: in questo modo si produce l’indirizzo ad una entry della tabella che contiene l’offset per raggiungere l’elemento corrente. Si può notare come un’array a dimensione variabile contenga due array con lo stesso numero di elementi: uno a dimensione uniforme (la look-up table) e uno a dimensione variabile (l’insieme degli elementi dell’array). Questa caratteristica è trasparente all’applicazione che non fa altro che chiedere un puntatore all’n-esimo elemento.

 

8.4: Element array
 

Molte volte è necessario creare un array con un alto tasso di duplicazione, quindi siamo di fronte ad un array che contiene molti elementi identici. La normale gestione diventa inefficiente quando il tasso di duplicazione è molto alto, per ovviare a questi inconvenienti di gestione si introducono gli element array: ogni elemento dell’array ha un reference count (contenuto nello header). Quando viene inserito un nuovo elemento la routine che si occupa dell’inserzione controlla se un altro elemento identico è già presente nell’array; se il risultato è positivo non viene introdotta una nuova copia dell’elemento, ma viene incrementato il reference count dell’elemento già presente e viene ritornato il suo indice; se, invece, non esiste un altro elemento identico nell’array questo viene copiato nell’array, viene fissato a 1 il suo reference count e viene ritornato il suo indice.

Gli elementi di questo tipo di chunk array possono essere fissi, di dimensione uniforme o variabile: in ogni caso va indicata la dimensione degli elementi all’atto della creazione dell’array (tale valore sarà zero nel caso di array a dimensione variabile).

Tutti gli elementi di un element array mantengono il loro indice finchè non vengono liberati; quando un elemento viene cancellato, le routine di gestione ridimensionano l’elemento a zero e lo aggiungono ad una lista degli elementi da liberare. Questo significa che, a differenza dei chunk array, l’elemento con indice 12 potrebbe non essere il tredicesimo elemento dell’array, perché potrebbero esserci degli elementi liberati prima; per questo motivo con gli element array si parla di token e non di indici; i token, infatti, sono considerati come dei valori opachi.

Ogni qual volta si cancella il riferimento ad un elemento, il suo refernce count viene decrementato, quando raggiunge il valore zero l’elemento corrispondente viene eliminato fisicamente dall’array.

L’inserimento richiede una ricerca (con costo lineare) tra gli elementi esistenti nell’array: questo tipo di array diventa inefficiente da gestire quando contiene molti elementi e si continua ad aggiungerne di nuovi. L’accesso agli elementi, invece, richiede un tempo costante, se le routine di gestione riescono a tradurre velocemente il token di un elemento nel corrispondente offset.

 

8.5: Name array
 

I name array sono tipi particolari di element array nei quali si può accedere agli elementi sia attraverso il token sia usando un’etichetta (nome) associata in modo univoco ad ogni elemento.

I name array hanno elementi con dimensione variabile, questi sono composti da tre parti:

La creazione di un name array, come per gli element array richiede un ricerca lineare tra tutti gli elementi presenti nell’array; in questo caso, però, anche la traduzione del nome in un token richiede una ricerca lineare attraverso gli elementi dell’array; questo tipo di array diventa doppiamente inefficiente quando si deve lavorare con array troppo grossi. L’accesso ad un elemento usando il token resta costante.