C dla każdego (cz. 12.)
Zarządzanie pamięcią
W każdym systemie istnieją funkcje przydzielające
pamięć do dyspozycji programu. W wypadku Amigi są
one umieszczone w bibliotece będącej sercem
systemu, czyli w Execu, który przechowuje
informacje o wolnych obszarach pamięci. Pamięć
można podzielić na kilka rodzajów. Każdemu z nich
odpowiada opisująca go flaga.
MEMF_CHIP -- Pamięć jest dostępna dla układów
specjalizowanych komputera. Przechowywane są w
niej: aktualnie wyświetlane obrazy, sprite'y,
dźwięki. Częstym błędem, popełnianym przez
programistów, mających wyłącznie pamięć Chip, jest
zapominanie o ustawieniu flagi MEMF_CHIP dla
danych graficznych lub muzycznych, co powoduje
bardzo poważne problemy na maszynach z pamięcią
Fast. Z drugiej jednak strony nadużywanie cennego
dla grafiki Chipu nie jest wskazane, ponieważ jego
ilość jest poważnie ograniczona.
MEMF_FAST -- Pamięć tego typu jest poza obszarem
adresowym układów specjalizowanych do wyłącznej
dyspozycji procesora. Jej zaleta to krótszy czas
dostępu, wynikający z braku zainteresowania nią ze
strony układów specjalizowanych. Użycie pamięci
typu Fast jest wskazane do przechowywania kodu
oraz prywatnych danych programu (patrz wyżej).
Niestety, w wypadku gdy system jest pozbawiony
pamięci Fast, próba jej przydzielenia zakończy się
niepowodzeniem (właściwym rozwiązaniem jest użycie
następnej flagi).
MEMF_ANY -- "daj, co Ci zbywa". Flaga oznacza, że
program nie ma specjalnych wymagań co do pamięci.
System przydzieli mu w pierwszej kolejności pamięć
Fast, a dopiero w wypadku jej braku pamięć Chip.
Jest najwłaściwsza przy zajmowaniu pamięci na
podręczne dane.
MEMF_PUBLIC -- Oznacza, że przydzielona pamięć ma
być dostępna dla innych zadań (procesów). Obecnie
każdy obszar pamięci jest pamięcią publiczną,
jednak w przyszłości system będzie zabezpieczał
przed możliwością "bazgrania" po cudzej pamięci
(tzw. memory protection, obecne w systemach typu
Unix oraz Windows 95). Zabezpieczenie pamięci
wymaga zastosowania procesora wyposażonego w układ
zarządzania pamięcią MMU (ang. memory management
unit). Układ ten występuje w pełnych wersjach
procesorów 030+, nie mają go wersje EC (ang.
embedded controler -- niepełna kontrola). Aby
zapewnić z pamięcią wirtualną, flagę tę należy
ustawiać, gdy do przydzielonej pamięci ma mieć
dostęp nie tylko nasz proces.
MEMF_CLEAR -- oznacza, że przed przydzieleniem
pamięć ma być wypełniona zerami. Bywa to bardzo
przydatne, jednak wymaga nieco czasu.
Wszystkie powyższe flagi znajdują się w pliku
"exec/memory.h", często w programach można spotkać
zamiast umieszczonych flag wartość zero. Nie
powoduje to żadnych błędów, gdyż flaga MEMF_ANY
jest równa zero.
Do przydzielania pamięci służy funkcja:
APTR AllocMem( ULONG byteSize, ULONG requirements
);
Pierwszy argument to wielkość pamięci, która ma
zostać przydzielona. Drugi definiuje typ pamięci,
o jaką prosi program. W tym właśnie miejscu
umieszcza się powyższe flagi. Rezultatem jest
wskaźnik na przydzielony obszar pamięci lub NULL,
gdy pamięci brakowało. APTR to, jak już wcześniej
wspominaliśmy, wskaźnik na typ void -- nie trzeba
go rzutować na inne typy -- jest to wskaźnik
uniwersalny.
Właściwością pamięci przydzielanej przez funkcję
AllocMem() jest wyrównanie jej do longa (ang.
longword aligned), to znaczy, że adres jest
podzielny przez 4. Dzięki takiej organizacji
możliwy jest zapis długich słów oraz wszelkich
innych struktur systemowych bez obawy wystąpienia
błędu (np. nieparzystego adresowania procesora
MC68000). Ze względu na budowę list wolnej pamięci
nie jest możliwe pozostawienie kawałka, którego
adres nie byłby podzielny przez 8. W związku z tym
podczas przydzielania pamięci rozmiar bywa często
zaokrąglany w górę.
System traci kontrolę nad pamięcią po jej
przydzieleniu. W związku z tym przy wychodzeniu z
programu konieczne jest zwolnienie całej
przydzielonej pamięci -- system tego za nas nie
zrobi. W tym celu należy wywołać funkcję:
void FreeMem( APTR memoryBlock, ULONG byteSize );
Jej argumentami są: wskaźnik na przydzielony
uprzednio blok oraz jego rozmiar. Pamięci nie
wolno oddawać na raty, więc drugim argumentem musi
być rozmiar identyczny z istniejącym podczas
przydzielania. Błędem jest również kilkakrotne
zwalnianie jednego bloku pamięci.
Od systemu 2.04 istnieją funkcje:
APTR AllocVec( ULONG byteSize, ULONG requirements
);
void FreeVec( APTR memoryBlock );
Użycie pierwszej jest identyczne z użyciem funkcji
AllocMem(). W wypadku drugiej różnica polega na
tym, że nie podaje się rozmiaru bloku pamięci --
jest on odnotowywany podczas przydzielania i
program nie musi go pamiętać. Pamięć przydzielana
jest również wyrównywana do długiego słowa.
Przydzielanie pamięci przez system jest procesem
kosztownym. Aby zabezpieczyć się przed
konfliktami, system musi wyłączyć multitasking i
przejrzeć spory kawałek globalnej listy wolnej
pamięci. Alternatywnym rozwiązaniem jest
utworzenie lokalnych dla jednego programu list
pamięci -- od wersji 3.0 system ma odpowiednie
mechanizmy, tzw. memory pools, zwane przez nas
dalej blokami pamięci. Ich przydatność
spowodowała, że w systemie 3.1 funkcje te
umieszczono również w bibliotece linkera
"amiga.lib", z przedrostkiem "Lib" w nazwach --
umożliwia to korzystanie z bloków pamięci również
pod systemami starszymi od 3.0.
Do inicjacji obsługi bloków służy funkcja:
APTR CreatePool( ULONG requirements, ULONG
puddleSize, ULONG threshSize );
Jej pierwszym argumentem jest flaga definiująca
rodzaj pamięci, jaka ma być przydzielana.
Następnym ("puddleSize") jest rozmiar pojedynczego
bloku, jaki ma być przydzielany z zasobów systemu.
Powinien on być przynajmniej kilkanaście razy
większy od średniej wielkości obszaru pamięci,
przydzielanego następnie z naszej listy. Cały
"trik" bloków pamięci polega na tym, że pamięć
jest od systemu przydzielana "hurtem", blokami, a
następnie wydzielana aplikacji na żądanie, małymi
porcjami. Gdy kończy się jeden przydzielony blok,
automatycznie pobierany jest od systemu kolejny.
Ostatni argument określa, powyżej jakiego rozmiaru
żądany obszar pamięci ma być przydzielany
bezpośrednio od systemu, a nie z bloku -- wielkość
ta ma być nie większa niż "puddleSize".
Funkcja zwraca "czarną skrzynkę", którą należy
przekazywać do wszystkich funkcji, obsługujących
bloki pamięci. Rezultatem może być również NULL w
wypadku błędu.
Do przydzielania i zwalniania pamięci w sposób
blokowy służą funkcje:
APTR AllocPooled( APTR poolHeader, ULONG memSize
);
void FreePooled( APTR poolHeader, APTR memory,
ULONG memSize );
Argumentem pierwszej jest "czarna skrzynka" oraz
rozmiar obszaru do przydzielenia, rezultatem zaś
wyrównany do longa wskaźnik na przydzielony obszar
lub NULL w wypadku problemów. W celu zwrócenia
zajętej pamięci należy wywołać funkcję
FreePooled(), której argumentami są: "czarna
skrzynka", wskaźnik na blok przydzielony za pomocą
AllocPooled() oraz rozmiar bloku (ten sam co
podczas przydzielania).
Po zakończeniu współpracy z prywatną listą pamięci
należy zwolnić ją za pomocą funkcji:
void DeletePool( APTR poolHeader );
Jej jedynym argumentem jest "czarna skrzynka".
Funkcja ta zwraca całą przydzieloną pamięć, nie
jest więc konieczne wywoływanie FreePooled() dla
wszystkich przydzielonych obszarów: funkcja
DeletePool() zrobi to globalnie i znacznie
szybciej. Dzięki tej właściwości użycie bloków
jest wygodne, poza tym ich obsługa nie wymaga
częstego wyłączania multitaskingu i w mniejszym
stopniu "dziurawi" pamięć.
Czasami program jest ciekaw, ile jest wolnej
pamięci. W tym celu powstała funkcja:
ULONG AvailMem( ULONG requirements );
Zwraca ona wielkość wolnej pamięci w bajtach.
Jedynym jej argumentem jest flaga opisująca typ
pamięci, którą program jest zainteresowany.
Dodatkowo mogą wystąpić flagi:
MEMF_LARGEST -- zostanie podana wielkość
największego ciągłego obszaru;
MEMF_TOTAL -- zostanie podana fizyczna wielkość
pamięci.
Obie te flagi można łączyć z podanymi wcześniej.
Rezultaty pracy funkcji AvailMem() nie są w pełni
wiarygodne ze względu na multitasking.
Podczas dzisiejszego odcinka operowałem dość
swobodnie pojęciem listy. Najogólniej rzecz
ujmując, lista składa się z odrębnych bloków
pamięci, z których każdy zawiera oprócz
użytecznych danych wskaźnik na następny element
(pole "Succ") -- listę taką nazywamy
jednokierunkową. Jeśli każdy element listy zawiera
dodatkowo wskaźnik na element poprzedni (pole
"Pred"), to listę nazwiemy dwukierunkową (patrz
struktury "Node" i "MinNode" w pliku
"exec/nodes.h"). Aby odwołać się do następnego
elementu listy, należy odczytać pole "Succ",
będące adresem następnego elementu (pole "Pred"
dla poprzedniego elementu).
Lista ma początek i koniec -- w najprostszym
wypadku w polu "Succ" ostatniego oraz w polu
"Pred" pierwszego elementu umieszcza się NULL.
Dwukierunkowe listy, istniejące w systemie Amigi,
mają inaczej oznaczony element końcowy:
"ostatni->Succ->Succ==NULL", analogicznie dla
pierwszego elementu. Rozwiązanie takie nie wymaga
specjalnej obsługi skrajnych elementów w funkcjach
manipulujących listami, jednak niezbędne stają się
specjalne, nie wykorzystywane, elementy krańcowe.
Zastosowany został "trik": oba krańcowe elementy
są jednym kawałkiem pamięci, sprawującym
równocześnie funkcję nagłówka listy. Zawiera on
pola, które są wskaźnikami: "Head" -- na pierwszy
ważny element, "Tail" -- zawsze NULL, "TailPred"
-- na ostatni ważny element (patrz struktury
"List" i "MinList" w pliku "exec/lists.h"). Aby
zainicjować listę, należy polu "Head" przypisać
adres pola "Tail", polu "TailPred" zaś adres pola
"Head" (patrz rys. 1.). Do inicjacji listy można
wykorzystać funkcję NewList() z biblioteki linkera
"amiga.lib". Jedynym jej argumentem jest wskaźnik
na strukturę "List" lub "MinList". Na rysunku 2.
przedstawiona jest lista zawierająca trzy
elementy.
Oto podstawowe funkcje do obsługi list z
biblioteki Exec:
void Insert( struct List *list, struct Node *node,
struct Node *pred );
void AddHead( struct List *list, struct Node *node
);
void AddTail( struct List *list, struct Node *node
);
void Remove( struct Node *node );
struct Node *RemHead( struct List *list );
struct Node *RemTail( struct List *list );
Pierwsze trzy funkcje dodają jeden element, trzy
kolejne usuwają. Insert() dodaje element "node" do
listy po elemencie "pred". Możliwe jest również
dołączenie elementu "node" jako pierwszego na
liście -- w takim wypadku argument "pred" musi
mieć wartość NULL. AddHead() dodaje element na
początek listy, a AddTail() na koniec. Funkcja
Remove() usuwa wyszczególniony element, RemHead()
pierwszy element listy (zwraca jego adres bądź
NULL, gdy lista jest pusta), a RemTail() ostatni.
Powyższe funkcje mogą pracować zarówno z listą
złożoną ze struktur "Node" i "List", jak również
"MinNode" i "MinList".