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".