AROS implementa un sistema di liste linkate, le cosiddette liste exec. Una
lista linkata è costituita da una serie di nodi, ognuno di essi punta all'altro.
Sono definiti due tipi di nodi in exec/nodes.h:
- struct MinNode
- è il modo base. Non avete bisogno di conoscerne la struttura, in quanto
ogni possibile azione di su essi è gestita da qualche funzione di libreria.
- struct Node
estende la struttura semplice MinNode. Fornisce alcuni campi aggiuntivi:
- ln_Name
- Ogni Node contiene un puntatore a una stringa, che descrive quel nodo.
- ln_Type
- E' definita una lista di tipi in exec/nodes.h.
- ln_Pri
- Una priorità, usata per ordinare la lista.
Entrambe le strutture possono essere integrate in altre strutture. Per esempio
struct Library (definita in exec/libraries.h) contiene una struct Node
all'inizio. In questo modo tutte le librerie possono essere contenute in una
lista. Il campo ln_Name punta al nome della libreria, ln_Type è settato a
NT_LIBRARY per fare vedere che questo nodo è una librerie e ln_Pri
riflette l' importanza di una libreria.
Ovviamente, abbiamo bisogno di contenitori di liste. Questi sono definiti in
exec/lists.h. Come per i nodi, per le liste abbiamo due tipi diversi:
- struct MinList
- è la lista minimale. Non avete bisogno di conoscerne i membri; guardatela
come una scatola nera.
- struct List
- contiene un campo aggiuntivo lh_Type, che corrisponde al ln_Type di
struct Node.
Le MinList hanno dei MinNode come membri, mentre le List usano i Node.
Non sono intercambiabili. Mentre è tecnicamente possibile usare i Node nelle
MinList, perdereste tutti i vantaggi.
FIXME: Macros
La exec.library e la link-library amiga.lib contengono alcune funzioni per
manipolare le liste di exec. Prima che una lista possa essere usata, deve
essere inizializzata, usando la seguente funzione di amiga.lib:
#include <proto/alib.h>
void NewList( struct List *list );
E' possibile aggiungere nodi alle liste le seguenti funzioni di exec.library:
#include <proto/exec.h>
void AddHead( struct List *list, struct Node *node );
void AddTail( struct List *list, struct Node *node );
void Enqueue( struct List *list, struct Node *node );
void Insert( struct List *list, struct Node *node, struct Node *pred );
Con AddHead() e AddTail() un node viene inserito rispettivamente
all'inizio o alla fine di una list. Enqueue inserisce un node a
seconda del suo campo ln_Pri. Un nodo può essere inserito dopo un altro
usanto Insert(). In questo caso, bisogna fornire pred, il puntatore al
nodo che precede node.
I nodi possono essere rimossi usando le funzioni di exec.library:
#include <proto/exec.h>
void Remove( struct Node *node );
struct Node *RemHead( sruct List *list );
struct Node *RemTail( struct List *list );
Mentre RemHead() e RemTail() rimuovono rispettivamente il primo e l'ultimo
nodo di una list e ritornano un puntatore ad esso, Remove() rimuove il
node da qualunque lista esso si trovi.
Ovviamente, tutte le funzioni di lista (eccetto Enqueue()) possono anche
lavorare su una struct MinList e una struct MinNode.
E' possibile cercare un nodo in un lista usando:
#include <proto/exec.h>
struct Node *FindName( struct List *list, STRPTR name );
name è il puntatore a una stringa che deve essere confrontata con il campo
ln_Name dei nodi in list. Il confronto è case-sensitive! Se name
corrisponde a un qualunque campo ln_Name, verrà ritornato un puntatore al
nodo corrispondente. Se non viene trovato alcun nodo verrà restituito NULL.
Nota
Se usate FindName() su una lista, questa non deve contenere alcuna
struct MinList. Altrimenti la memoria verrà corrotta!
Nell'esempio seguente, creiamo una lista, ci aggiungiamo tre nodi, ne cerchiamo
uno per nome e quindi lo rimuoviamo:
#include <proto/alib.h>
#include <proto/exec.h>
#include <exec/types.h>
#include <exec/lists.h>
#include <exec/nodes.h>
#include <dos/dos.h> /* Per RETURN_OK */
struct List list;
/* I nostri nodi */
struct Node node1 =
{
NULL, NULL, /* Ancora nessun precedecessore e successore */
NT_UNKNOWN, 0, /* Tipo sconosciuto, priorità ignorata */
"Primo nodo" /* Il nome del nodo */
};
struct Node node2 =
{
NULL, NULL,
NT_UNKNOWN, 0,
"Secondo nodo"
};
struct Node node3 =
{
NULL, NULL,
NT_UNKNOWN, 0,
"Terzo nodo"
};
int main(int argc, char *argv[])
{
struct Node *node;
/* Prepariamo la lista per l'uso */
NewList(&list);
/* Aggiungiamo i primi due nodi alla fine della lista. */
AddTail(&list, &node1);
AddTail(&list, &node2);
/* Inseriamo il terzo nodo dopo il primo nodo. */
Insert(&list, &node3, &node1);
/* Troviamo il secondo nodo */
node = FindName(&list, "Secondo nodo");
/*
Se il nodo viene trovato (e succede sempre in questo esempio),
lo rimuoviamo.
*/
if (node)
Remove(&node);
return RETURN_OK;
}
AROS definisce alcune macro in vari file header. Tutte le macro fanno il
casting dei loro parametri al tipo corretto, quindi dovete fornire un input
valido ma potete stare sicuri del cast (le macro sono concepite per rendere la
vita più semplice).
- NEWLIST(list)
Compatibile: | Sì |
Posizione: | exec/lists.h |
Inizializza una lista. Non dovete mai usare una lista prima di averla
inizializzata.
- GetHead(list)
Compatibile: | Sì |
Posizione: | exec/lists.h |
Restituisce un puntatore al primo nodo della lista o NULL se la lista
è vuota.
- GetTail(list)
Compatibile: | Sì |
Posizione: | exec/lists.h |
Restituisce un puntatore all'ultimo nodo della lista o NULL se la lista
è vuota.
- GetSucc(node)
Compatibile: | Sì |
Posizione: | exec/lists.h |
Restituisce un puntatore al prossimo nodo della lista o NULL se non ce
ne sono.
- GetPred(list)
Compatibile: | Sì |
Posizione: | exec/lists.h |
Restituisce un puntatore al precedente nodo della lista o NULL se non ce
ne sono più.
- ForeachNode(list,node)
Compatibile: | Sì |
Posizione: | exec/lists.h |
Itera attraverso una lista. Un blocco di codice deve seguire questa macro.
Il blocco non verrà eseguito se la lista è vuota. Quando la lista termina
node non contiene NULL ma node->ln_Succ sarà NULL. Non potete
usare questa macro se volete cancellare i nodi di una lista (es. non potete
chiamare Remove() dentro il blocco di codice che segue questa macro).
Usate ForeachNodeSafe() se volete cancellare dei nodi.
Esempio:
/* Itera attraverso una lista completa di nodi e stampa i loro nomi */
t = 1;
ForeachNode(list,node)
{
if (node->ln_Name)
{
printf ("Nodo %d: %s\n", t++, node->ln_Name);
if (!strcmp (node->ln_Name, "end"))
break;
}
}
if (node->ln_Succ)
printf ("Non tutti i nodi sono stati processati\n");
else
printf ("La lista non contiene un nodo con nome \"end\"\n");
- ForeachNodeSafe(list,node,tmpNode)
Compatibile: | Sì |
Posizione: | exec/lists.h |
Itera attraverso una lista. Un blocco di codice deve seguire questa macro.
Il blocco non verrà eseguito se la lista è vuota. Quando la lista termina
node non contiene NULL ma node->ln_Succ sarà NULL. Potete
usare questa macro con codice che cancella i nodi della lista.
- SetNodeName(node,name)
Compatibile: | Sì |
Posizione: | exec/lists.h |
Assegna un nome nuovo a un nodo. Il nome non viene copiato, la macro farà
semplicemente puntatore ln_Name a name.
La macro farà il casting di node a struct Node *
quindi accertatevi che node sia un nodo completo.
- GetNodeName(node)
Compatibile: | Sì |
Posizione: | exec/lists.h |
Ritorna il nome del nodoo.
La macro farà il casting di node a struct Node *
quindi accertatevi che node sia un nodo completo.
- ListLength(list,count)
Compatibile: | Sì |
Posizione: | exec/lists.h |
Questo memorizza in count il numero di nodi nella list.
In un programma avete bisogno di memoria pressocchè per ogni cosa. Si possono
fare molte cose usando lo stack. Ma spesso avete bisogno di blocchi di memoria
più grandi o non volete usare lo stack per qualche motivo. In questi casi
dovete allocare la memoria voi stessi. exec.library fornisce diversi modi per
allocare la memoria. Le due funzioni più importanti sono:
#include <proto/exec.h>
APTR AllocMem( ULONG size, ULONG flags );
APTR AllocVec( ULONG size, ULONG flags );
Entrambe le funzioni restituiscono un puntatore a un'area di memoria della
grandezza specificata in size. Se non c'è sufficiente memoria disponibile,
verrà restituito NULL al posto del puntatore. Dovete controllare questa
eventualità prima di usare la memoria. Se la memoria è stata allocata con
successo, potete farne quello che volete.
Potete specificare dei flags aggiuntivi per ottenere tipi speciali di
memoria. I seguenti flags sono definiti in exec/memory.h:
- MEMF_CLEAR
- La memoria allocata viene inizializzata con zeri.
- MEMF_LOCAL
- Restituisce memoria che non viene ripulita se il computer viene resettato.
- MEMF_CHIP
- Restituisce memoria accessibile dai chip grafici e sonori. Questo tipo di
memoria è necessario per alcune funzioni.
- MEMF_FAST
- Restituisce memoria non accessibile dai chip grafici e sonori. Normalmente
non dovreste mai settare questo flag! E' necessario solo per alcune
funzioni molto esoteriche. La maggioranza dei sistemi non ha questo tipo di
memoria.
- MEMF_PUBLIC
- Questo flag deve essere settato se la memoria che state allocando deve
essere accessibile ad altri task. Se non lo settate, la memoria allocata
è privata per il vostro task. Questo problema verrà discusso in dettaglio
nel capitolo su
.. FIXME:: inter-task communication.
- MEMF_REVERSE
- Se questo flag è settato, verrà invertito l'ordine di ricerca di blocchi di
memoria liberi. I blocchi che si trovano alla fine della memoria libera
verranno trovati per primi.
- MEMF_NO_EXPUNGE
- Normalmente, se non viene trovata libera la quantità di memoria richista,
AROS tenta di liberare la memoria non utilizzata, per esempio liberando la
memoria da librerie non utilizzate. Se questo flag è settato, questo
comportamento viene disabilitato.
La memoria allocata con queste funzioni deve essere liberata dopo l'uso con
una delle seguenti funzioni. Ricordatevi che non dovete usare memoria che è
già stata liberata.:
#include <proto/exec.h>
void FreeMem( APTR memory, ULONG size );
void FreeVec( APTR memory );
Ovviamente, FreeMem() deve essere usata per memoria allocata con AllocMem()
e FreeVec() per memoria allocata con AllocVed(). La sintassi di queste
due funzioni mostra la differenza tra AllocMem() e AllocVec(): AllocVed()
tiene traccia della dimensione del blocco di memoria che ha allocato. Quindi,
se usate AllocVed(), non avete bisogno di memorizzare la grandezza richiesta,
mentre dovete farlo se usate AllocMem().
A volte c'è bisogno di effettuare allocazioni multiple di memoria in una volta
sola. Il metodo usuale è chiamare AllocVed() con la grandezza complessiva di
tutti i blocchi di memoria e quindi tenere dei puntatori relativi al puntatore
restituito dall'allocazione. Ma cosa fareste se avete bisogno di diversi tipi
di memoria, con diversi MEMF_ settati? Potete fare allocazioni multiple o
usare la funzione:
#include <proto/exec.h>
struct MemList *AllocEntry( struct MemList *oldlist );
Come avrete notato, AllocEntry() utilizza un puntatore a una
struct MemList come unico parametro e come valore di ritorno. Troviamo la
definizione di questa struttura in exec/memory.h:
struct MemEntry
{
union
{
ULONG meu_Reqs;
APTR meu_Addr;
} me_Un;
ULONG me_Length;
};
struct MemList
{
struct Node ml_Node;
UWORD ml_NumEntries;
struct MemEntry ml_ME[1];
};
L'array ml_ME di struct MemList ha un numero variabile di elementi. Il
numero dei suoi elementi è settato in ml_NumEntries. La struct MemEntry
descrive una singola unità di memoria. Vengono memorizzate la sua grandezza
(me_Length), le sue richieste (es. MEMF_, in me_Un.meu_Reqs) e
possibilmente un puntatore a un blocco di memoria (me_Un.meu_Addr). La
struct MemList che passiamo a oldlist, deve avere settato il campo
ml_NumEntries al numero di struct MemEntry contenute in ml_ME. La
struct MemEntry deve avere settati i campi me_Length e me_Un.meu_Reqs.
Gli altri campi vengono ignorati. La funzione restituisce un puntatore a una
copia della struct MemEntry, passata in oldlist, con tutti i campi
rilevanti settati (specialmente me_Un.meu_Addr). Viene segnalato un errore
settando il bit più significativo del puntatore che viene restituito. Quindi
dovete sempre controllarlo, prima di utilizzare il puntatore ritornato. La
memoria allocata con AllocEntry() deve essere liberata usando FreeMem().
AROS gestisce diversi cosiddetti memory-pools. Ogni memory-pool contiene una
lista di aree di memoria. Il memory-pool più importante è quello che contiene
tutta la memoria libera del sistema. Ma potete anche creare dei memory-pool voi
stessi. Questo ha alcuni vantaggi:
- Ogni volta che allocate della memoria, la memoria nel sistema diventa sempre
più frammentata. Questa frammentazione fa sì che i blocchi di memoria
disponibili diventino sempre più piccoli. In questo modo le allocazioni più
grandi falliranno. Per prevenire questo problema vengono introdotti i
memory-pool. Invece di allocare molti piccoli blocchi di memoria, le routine
di gestione dei pool allocano grossi blocchi e quindi restituiscono piccoli
blocchi di essi, quando vengono fatte le richieste di memoria.
- I memory-pool privati hanno l'abilità di tenere traccia di tutte le
allocazioni che avete fatto, cosicchè tutta la memoria in un pool potrà essere
liberata con una semplice chiamata di funzione (ma potete anche liberare la
memoria individualmente).
Prima che un memory-pool possa essere usato, deve essere creato. Questa
operazione viene eseguita dalla funzione:
#include <proto/exec.h>
APTR CreatePool( ULONG flags, ULONG puddleSize, ULONG threshSize );
I flags specificano il tipo di memoria che volete ottenere dalla funzione
AllocPooled(). Tutte le definizioni MEMF_ descritte sopra sono permesse
anche qui.
puddleSize è la grandezza dei blocchi di memoria allocati dalle funzioni di
pooling. Normalmente, una grandezza dieci volte superiore alla grandezza di
memoria media che avete bisogno di allocare, è una buona opzione. Ma non di
meno il puddleSize non dovrebbe essere troppo grande. Normalmente potreste
limitarlo a circa 50kb. Nota bene, questi sono solo suggerimenti, non reali
limitazioni.
Infine, threshSize specifica quanto può essere grande la memoria che si deve
allocare, cosìcchè nessun nuovo blocco viene allocato automaticamente. Se, per
esempio, il threshSize è settato a 25kb e volete allocare una porzione di
memoria grande 30kb, non viene cercata la lista interna di blocchi, ma invece
la memoria verrà allocata direttamente. Se la memoria da allocare fosse di soli
20kb, allora per prima cosa verrebbe fatta una ricerca nella lista dei blocchi
per trovare una porzione di memoria di quella grandezza. Ovviamente, threshSize
non deve essere più grande di puddleSize non dovrebbe essere neanche troppo
piccolo. La metà di puddleSize è un buon compromesso.
CreatePool() ritorna un puntatore privato a una struttura-pool che deve essere
salvato per usi futuri. Se non è disponibile memoria per la struttura-pool,
allora verrà restituito NULL. Dovete controllare sempre il verificarsi di
questa condizione.
Dopo l'uso, tutti i memory-pool devono essere distrutti chiamando:
#include <proto/exec.h>
void DeletePool( APTR pool );
Questa funzione cancella il pool che le è stato passato. Inoltre, tutta la
memoria allocata in quel pool verrà liberata. In questo modo, non avete bisogno
di ricordare ogni singolo pezzo di memoria che avete allocato in quel pool.
Basterà chiamare DeletePool() alla fine. Ricordate di fare attenzione a non
utilizzare memoria dopo che il suo pool è stato cancellato.
Se volete allocare memoria da un pool, dovete chiamare:
#include <proto/exec.h>
void *AllocPooled( APTR pool, ULONG size );
A prescindere dal pool da cui allocare memoria, deve essere specificata la
grandezza (size) della memoria da allocare. Verrà restituito un puntatore a
un blocco di memoria della grandezza richista o NULL per indicare che non
c'è sufficiente memoria disponibile.
La memoria allocata con AllocPooled() può essere liberata sia distruggendo
l'intero pool con DeletePool() o individualmente chiamando:
#include <proto/exec.h>
void FreePooled( APTR pool, void *memory, ULONG size );
Questa funzione libera esattamente un pezzo di memoria che è stato
precedentemente allocato usando AllocPooled(). Devono essere specificato come
argomenti: il puntatore alla memoria memory resituito da AllocPooled(), la
sua grandezza size e il pool in cui si trova.
Nota
Potreste chiedervi: "Se DeletePool() cancella tutta la memoria di un pool,
perchè mai dovrei usare FreePooled()?" La risposta è semplice: per
risparmiare memoria. Normalmente è un buono stile quello di liberare memoria
immediatamente quando non ne avete più bisogno. Ma a volta è più semplice
liberare un memory-pool dopo un mucchio di allocazioni. Ciò nonostante,
non dovreste usare questa caratteristica, se non ne siete sicuri, quando
il memory-pool viene cancellato. Immaginate un programma come questi (non
provate a compilarlo, non compilerebbe):
#define <exec/types.h>
#define <exec/memory.h>
#define <dos/dos.h>
int main(int argc, char *argv[])
{
APTR pool;
APTR mem;
/* Creiamo il nostro memory pool e testiamo se è stato creato con
successo. */
pool = CreatePool(MEMF_ANY, 50*1024, 25*1024);
if (pool)
{
/* Una semplice funzione fittizia. Immaginate che questa
funzione apra una finestra con due bottoni "Fai azione" e
"Quit". */
open_our_window();
for(;;)
{
/* Un'altra funzione fittizia che restituisce una delle
definizioni qui sotto */
switch(get_action())
{
/* Viene restituita questa se viene rilasciato il bottone
"Fai azione" */
case DOACTION:
mem = AllocPooled(pool, 10*1024);
if (mem)
{
/* Un'altra funzione che usa la nostra memoria. */
silly_function(mem);
}
break;
/* Viene restituita questa se viene rilasciato il bottone
"Quit" */
case QUIT:
return RETURN_OK;
}
}
/* Chiude la finestra che abbiamo aperto prima */
close_our_window();
/* Cancella il nostro pool. */
DeletePool(pool);
}
}
Ogni volta che il bottone Fai azione è rilasciato, viene allocata un
po' di memoria. Questa memoria viene liberata alla fine del programma,
quando viene chiamata DeletePool(). Ovviamente, più a lungo il programma
viene utilizzato, più memoria verrà impegnata. E' questo il motivo per cui
è molto meglio liberare la memoria dopo l'uso. Questo viene implementato
sostituendo la parte tra case DOACTION: e case QUIT: con:
mem = AllocPooled(pool, 10*1024);
if (mem)
{
silly_function(mem);
FreePooled(pool, mem, 10*1024);
}
break;
I pool di memoria vengono gestiti con le struct MemHeader. Se avete un
puntatore a una struttura come questa, potete provare ad allocare della memoria
dal suo pool:
#include <proto/exec.h>
void *Allocate( struct MemHeader *mh, ULONG size );
Oltre al puntatore a una struct MemHeader passato in mh, dovete fornire
la grandezza size del blocco di memoria che volete allocare. Questa funzione
restituisce un puntatore al primo blocco di memoria trovato o NULL se non
viene trovato alcun blocco.
Dovete liberare ogni blocco di memoria allocato con Allocate() con:
#include <proto/exec.h>
void Deallocate( struct MemHeader *mh, APTR mem, ULONG size );
Dovete passare a Deallocate() gli stessi mh e size passati ad
Allocate() e, in aggiunta, il puntatore che vi è stato restituito da essa.
intuition.library fornisce un altro modo per gestire memory pool con le funzioni
AllocRemember() e FreeRemember(). Notate, tuttavia, che queste sono
obsolete. Usate le normali funzioni di pooling di exec.library.
In certe circostanze molto rare, potreste avere bisogno di allocare memoria a
un indirizzo specifico. Per farlo usate:
#include <proto/exec.h>
void *AllocAbs( ULONG size, APTR address );
Questa funzione prova ad allocare size bytes all'indirizzo address. Se ci
riesce, verrà restituito un puntatore all'indirizzo richiesto. Se nel blocco
richiesto c'è della memoria già allocata o non è disponibile nel sistema,
allora verrà restituito NULL.
Avvertenza
L'inizio del blocco di memoria richiesto verrà usato da exec per
memorizzare i dati del nodo (la grandezza esatta si calcola con
(2*sizeof (void *)) ). Per questo, non dovete scrivere all'inizio
del blocco di memoria! A causa di questi ostacoli non dovreste usare
AllocAbs(), tranne che non ne abbiate veramente bisogno.
La memoria allocata con AllocAbs() deve essere liberata usando FreeMem().
Per ottenere la grandezza della memoria disponibile, usate la funzione:
#include <proto/exec.h>
ULONG AvailMem( ULONG type );
Il parametri type è rappresentato da alcuni dei seguenti flag, definiti in
exec/memory.h:
- MEMF_ANY
- Restituisci la grandezza di tutta la memoria libera nel sistema.
- MEMF_CHIP
- Restituisci la grandezza della memoria, quella accessibile ai chip grafici
e sonori.
- MEMF_FAST
- Restituisci la grandezza della memoria che non è accessibile ai chip
grafici e sonori.
- MEMF_LARGEST
- Ritorna solo il blocco più grande, al posto di tutta la memoria del tipo
specificato.
Potreste specificato anche altri flag MEMF_, ma verrebbero ignorati.
Nota
Notate bene che la grandezza di memoria interrogata non deve riflettere
necessariamente la grandezza reale della memoria disponibile, in quanto
questa cambia sempre in un sistema multitasking, anche mentre viene
eseguita AvailMem().
Ecco un programma per elencare la memoria disponibile nel sistema:
#include <stdio.h>
#include <exec/memory.h>
int main(int argc, char *argv[])
{
printf("Memoria totale libera: %h, blocco più grande: %h\n",
AvailMem(MEMF_ANY), AvailMem(MEMF_ANY|MEMF_LARGEST));
printf("Memoria chip libera: %h, blocco più grande: %h\n",
AvailMem(MEMF_CHIP), AvailMem(MEMF_CHIP|MEMF_LARGEST));
printf("Memoria fast libera: %h, blocco più grande: %h\n",
AvailMem(MEMF_FAST), AvailMem(MEMF_FAST|MEMF_LARGEST));
}
Questo capitolo è solo per interessarvi, se volete scrivere un driver per un
componente hardware che aggiunge memoria al sistema:
#include <proto/exec.h>
void AddMemList
(
ULONG size, ULONG type, LONG priority,
APTR address, STRPTR name
);
aggiunge memoria alla lista della memoria libera nel sistema. Dovete fornire
l'indirizzo address e la grandezza size della memoria da aggiungere. In
type dovete settare almeno uno dei flag MEMF_ definiti in exec/memory.h:
- MEMF_FAST
- La vostra memoria non deve essere accessibile ai chip grafici e sonori.
- MEMF_CHIP
- La vostra memoria sarà accessibile ai chip grafici e sonori.
Potete fornire una priorità priority, con la quale la vostra memoria sarà
aggiunta alla lista di memoria. La regola gerale è: Più veloce è la vostra
memoria, più alta dovrebbe essere la sua priorità. Se non sapete cosa
specificare qui, fornite 0. Infine, potete specificare un nome name, con
il quale la vostra memoria può essere identificata dal sistema e dagli utenti.
Potete specificate NULL al posto di un nome, ma dare un nome alla vostra
memoria è raccomandato.
Una volta che la vostra memoria è stata aggiunta alla lista della memoria
libera, non può essere più rimossa.
FIXME: AddMemHandler()/RemMemHandler()`exec/types.h`.