Przydział i zwalnianie bloków stronicowych, demon kswapd
Michał Siwak
2001-11-30
W starych procesorach Intela adres fizyczny komórki pamięci reprezentowany był
jako 32-bitowa liczba całkowita bez znaku. Umożliwiało to zaadresowanie do 4GB pamięci.
Współczesne procesory obsługują dodatkowo tzw. PAE (Physical Address Extensions).
Powoduje to rozszerzenie zwykłego, 32-bitowego adresu fizycznego o cztery dodatkowe
bity, co pozwala zaadresować do 64GB pamięci. Jednakże adres liniowy w systemie Linux
ciągle reprezentowany jest przez liczbę 32-bitową. Z tego względu w danej chwili jądro
może stale odwzorowywać jedynie 4GB RAMu.
Z drugiej strony, w trybie DMA dostępne jest tylko pierwsze 16MB pamięci. Powyższe
dwa fakty sprawiły, że Linux dzieli całą dostępną w komputerze pamięć na trzy
strefy (include/linux/mmzone.h):
Nazwa |
Zakres |
Typ pamięci |
ZONE_DMA |
16MB |
pamięć ISA DMA |
ZONE_NORMAL |
16MB - 896MB |
pamięć stale odwzorowywana przez jądro |
ZONE_HIGHMEM |
896MB |
pamięć nie odwzorowywana stale przez jądro - tylko
bufor stron i procesy użytkownika |
Informacje o każdej ze stref przechowywane są w strukturze typu zone_struct. Jej
najważniejsze pola to:
- lock - spinlock,
- free_area[10] - lista wolnych obszarów opisana niżej,
- free_pages - liczba wolnych bloków stronicowych w strefie,
- pages_min - minimalna liczba wolnych bloków stronicowych, zarezerwowana dla
jądra. W wyniku przydzielenia pamięci procesowi użytkownika, liczba wolnych stron nigdy
nie może zejść poniżej tej wartości,
- pages_high - jeśli liczba wolnych bloków stronicowych spadnie poniżej tej
wartości rozpoczynamy swapowanie w tle,
- pages_low - jeśli liczba wolnych bloków stronicowych spadnie poniżej tej
wartości rozpoczynamy intensywne swapowanie,
- inactive_clean_list - lista nieaktywnych, czystych bloków stronicowych.
Pola pages_high, pages_low, pages_min inicjowane są podczas startu systemu
przez funkcję free_area_init_core znajdującą się w pliku mm/page_alloc.c.
Ich domyślne wartości to odpowiednio:
,
gdzie jest liczbą
megabajtów dostępnej pamięci fizycznej.
Linux przydziela i zwalnia bloki stronicowe korzystając z tzw. algorytmu bliźniaków
(buddy algorithm).
Poniżej znajduje się krótki opis jego zasady działania.
Wszystkie bloki stronicowe w systemie rozdzielone są między ciągłe obszary stron o
rozmiarach
stron dla .
Kolejny numer pierwszego bloku stronicowego obszaru jest wielokrotnością rozmiaru grupy. Innymi
słowy blok składający się z 32 stron może zaczynać się jedynie na pozycji o numerze postaci
dla pewnego
.
Dla przykładu, załóżmy, że pewien system dysponuje szesnastoma stronami pamięci RAM, z
czego jedna (12) jest zajęta. Wówczas podział na grupy mógłby wyglądać tak:
Grupy czterostronicowe zaczynają się na pozycjach 4 i 8, a grupy dwustronicowe - na
pozycjach 2 i 14, spełniona jest więc powyższa definicja.
Bliźniakami nazywamy dwie grupy
bloków stronicowych o poniższych własnościach:
- zarówno ,
jak i zawierają
bloków
stronicowych,
- i
tworzą w
pamięci ciągły obszar (leżą więc koło siebie),
-
wielkości
jest poprawną grupą bloków stronicowych, tzn. rozpoczyna się na pozycji o numerze postaci
(innymi słowy:
i
powstały z podziału bloku stronicowego wielkości
na pół).
W powyższym przykładzie bliźniakami są grupy jednostronicowe o numerach
i
oraz
i
, nie są nimi
natomiast grupy czterostronicowe rozpoczynające się na pozycjach
i
(bo
nie jest
wielokrotnością ).
Przydział
stron pamięci odbywa się według następującego algorytmu: jeśli dysponujesz conajmniej jedną wolną
grupą -stronicową, to przydziel tę grupę, wpp. znajdź najmniejsze takie
, że
i dysponujesz wolną grupą
-stronicową.
Przydziel blok wielkości
, resztę podziel na grupy rozmiaru
(na
podstawie wzoru ).
Podczas zwalniania bloku -stronicowego
system sprawdza, czy przypadkiem jego bliźniak nie jest zaznaczony jako wolny. Jeśli tak, to łączy
oba bloki w jeden o wielkości
i powtarza
procedurę dla powstałego w ten sposób bloku. Scalanie bloków kończy się, gdy uzyskamy blok wielkości
lub gdy
bliźniak zwolnionego bloku będzie zajęty.
Deskryptor każdej ze stref zawiera m.in. pole free_area[10]. Jest to tablica
dziesięciu pól typu *free_area_t. Pole
-te tablicy
zawiera:
- dwukierunkową listę cykliczną (ze strażnikiem) wolnych grup rozmiaru
(free_list)
- wskaźnik do pomocniczej mapy bitowej ułatwiającej sprawdzenie stanu bliźniaka dowolnej
grupy (map).
Każdy bit w mapie bitowej free_area[i]->map opisuje status dwóch sąsiednich obszarów
o wielkości .
Jeśli bit jest wyłączony, oznacza to, że oba obszary są wolne lub oba są zajęte, wpp. dokładnie
jeden z nich jest zajęty.
UWAGA: Gdy oba sąsiednie obszary są wolne, są one traktowane jako część większego obszaru o rozmiarze
.
A oto schematyczne przedstawienie zawartości pola free_area dla przykładowej strefy
zawierającej 16 stron (strona 12 jest używana, pozostałe są wolne; ,,S'' oznacza strażnika):
.
Zawartość odpowiednich map bitowych przedstawia poniższa tabela:
free_area[0]->map |
0 |
0 |
0 |
0 |
0 |
0 |
1 |
0 |
free_area[1]->map |
0 |
0 |
0 |
1 |
free_area[2]->map |
0 |
1 |
free_area[3]->map |
1 |
UWAGA: Rysunek nie jest całkowicie poprawny. Dlaczego?
Odp: Przedstawiona sytuacja nigdy nie zdarzy się w prawdziwym systemie. Bloki 0 i 1
są bliźniakami, więc zostałyby połączone w większy blok 0-1. Ten natomiast
zostałby połączony z blokiem 2-3, a powstały blok z blokiem 4-7. Ostatecznie
otrzymalibyśmy blok 0-7 umieszczony na liście free_area[3].
Proces może zażądać przydzielenia grupy
bloków
stronicowych wywołując jedną z pięciu funkcji zdefiniowanych w pliku mm/page_alloc.c:
- __get_free_pages(mask, order) - przydziela ciągły obszar
bloków
stronicowych,
- __get_free_page(mask) - makro przydzielające jeden blok stronicowy,
- get_zeroed_page(mask) - alokuje jeden blok stronicowy i wypełnia go zerami,
- get_free_page(mask) - stara nazwa funkcji get_zeroed_page(mask), w serii
2.5 już jej nie będzie,
- __get_dma_pages(mask, order) - przydziela ciągły obszar
bloków
stronicowych dostępnych w trybie DMA (w architekturze i386 - pierwsze 16MB).
Parametr mask może być alternatywą bitową kilku flag, które wpływają na zachowanie
się funkcji get_free_pages w następujący sposób:
Flaga |
Znaczenie |
__GPF_DMA |
Przydziel pamięć w strefie DMA |
__GPF_HIGHMEM |
Przydziel pamięć w strefie pamięci wysokiej |
__GPF_WAIT |
Czy możemy po obudzeniu demona kswapd wywołać planistę i oddać procesor? |
__GPF_IO |
Czy możemy zainicjować operację I/O? |
__GPF_FS |
Czy możemy korzystać bezpośrednio z systemu plików (swap)? |
__GPF_HIGH |
Czy mamy wysoki priorytet? (Najczęściej oznacza to, że żądanie pochodzi
od procesu działającego w trybie jądra.) |
- rmqueue(zone, order) - zwraca blok wielkości
, gdzie k jest
najmniejszą liczbą spełniającą warunki:
oraz
istnieje wolny blok wielkości
. Funkcja
aktualizuje odpowiednią mapę bitową i licznik wolnych stron.
- expand(zone, page, index, low, high, area) - dzieli blok wielkości
,
na
mniejsze fragmenty tak, jak wymaga tego algorytm bliźniaków. Powstałe fragmenty dodaje do
odpowiednich list wolnych bloków.
- __alloc_pages_limit(zonelist, order, limit, direct_reclaim) - jeśli
suma pustych-nieaktywnych bloków stronicowych oraz bloków wolnych jest większa od
odpowiedniego wskaźnika poziomu wyznaczonego przez parametr limit
(PAGES_MIN, PAGES_LOW lub PAGES_HIGH), funkcja ta przydziela
bloków stronicowych.
- page_launder(mask, sync) - w miarę możliwości czyści nieaktywne, brudne
strony i przenosi je na listę inactive_clean_list.
Całą pracę związaną z przydziałem pamięci wykonuje funkcja
__alloc_pages(mask, order, zonelist) wywoływana przez opisane wyżej funkcje
z rodziny *get*page(s). Jej parametry to:
- order - logarytm liczby bloków stronicowych, których żąda proces,
- mask - flagi opisane powyżej,
- zonelist - ustawiana podczas startu systemu lista stref, funkcja
__alloc_pages próbuje przydzielić pamięć w kolejnych strefach tej listy
(tzn. najpierw w strefie DMA, później NORMAL i HIGHMEM).
Algorytm działania tej funkcji opiszę w dwóch krokach. Najpierw pokażę, w jaki sposób
przydzielana jest znaleziona już grupa wolnych bloków stronicowych, później opiszę
sposób w jaki te strony są odnajdowane i odzyskiwane (pre-paging).
- Znajdź strefę, w której suma bloków pustych i nieaktywnych oraz wolnych jest większa
od wskaźnika poziomu limit (PAGES_MIN, PAGES_LOW lub
PAGES_HIGH). Jeśli nie ma takiej strefy, zwróć NULL.
- Zajmij lock zone->lock.
- Znajdź najmniejsze takie
, że:
oraz lista free_area[k]->free_list jest niepusta. Niech B będzie pierwszym
blokiem z tej listy (rozmiaru
).
Zwróć NULL jeśli nie uda się znaleźć takiego bloku.
- Usuń B z listy free_area[k]->free_list.
- Uaktualnij zawartość mapy bitowej free_area[k]->map zmieniając bit
odpowiadający blokowi B na przeciwny.
- Zmniejsz licznik wolnych bloków stronicowych zone->free_pages.
- while
do
podziel
na dwóch bliźniaków
i
;
wstaw na listę free_area[k-1]->free_list;
zmień na przeciwny bit odpowiadający
w
free_area[k-1]->map;
:=
;
end
- Zwolnij lock zone->lock.
- Zaznacz, że strona jest używana.
- if istnieje strefa taka, że (free_pages
pages_low)
then alokuj pamięć w tej strefie
(* Alokujemy pamięć w strefie, w której jest dużo nieużywanej jeszcze pamięci, bo
nie zawiera ona danych, które trzebaby swapować. *)
- if (free_pages
pages_min)
then obudź demona kreclaimd
(* Demon kreclaimd przenosi strony z listy inactive_clean na listy
wolnych stron free_list. *)
- if istnieje strefa taka, że (free_pages + inactive_clean_pages
pages_high)
then alokuj pamięć w tej strefie
(* Jeśli w systemie działa dużo procesów, to najprawdopodobniej tutaj nam się uda,
gdyż strony są w takiej sytuacji szybko dezaktywowane. *)
- if istnieje strefa taka, że (free_pages + inactive_clean_pages
pages_low)
then alokuj pamięć w tej strefie
(* Jeśli w systemie jest dużo pamięci i nie zostało zaalokowanych wiele nieciągłych
obszarów pamięci, to najprawdopodobniej tutaj nam się uda. *)
- Obudź demona kswapd;
if (GPF_WAIT)
then schedule()
(* Oddajemy procesor, by dać kswapd szansę zwolnienia trochę pamięci. *)
- Spróbuj alokować pamięć w strefie takiej, że free_pages + inactive_clean_pages
pages_min.
Demon kswapd powinien za chwilę zwolnić trochę pamięci i sytuacja wróci do normy.
- if
then
page_launder();
while lista inactive_clean_pages jest niepusta do
przenieś stronę z inactive_clean_pages na odpowiednią
listę free_list;
spróbuj zaalokować pamięć;
end
(* Czyścimy nieaktywne, brudne strony i przenosimy strony z listy
inactive_clean_list na odpowiednie listy free_pages, aby uzyskać
duży, ciągły obszar wolnej pamięci. *)
- if
&& GFP_WAIT
then
try_to_free_pages();
if udało się coś zwolnić
then goto 1
else zwróć NULL
(* Jeśli chcemy zaalokować tylko jedną stronę, działamy w pętli do skutku.
Jeśli potrzebny blok jest większy, poddajemy się, aby nie zawiesić się w pętli
nieskończonej w przypadku dużej fragmentacji pamięci. *)
- Nie udało się, zwróć NULL
Zwolnienie grupy
bloków stronicowych zawierającej blok znajdujący się pod
adresem addr, następuje w wyniku wywołania funkcji free_pages(addr, order).
Jest to krótka funkcja, która sprawdza, czy addr nie jest równy NULL oraz czy strona
jest rzeczywiście zaalokowana.
Jeśli tak, wywołana zostaje funkcja __free_pages_ok(page, order), która wykonuje
za nas całą czarną robotę. Oto algorytm jej działania.
- Sprawdź, czy strona kwalifikuje się do usunięcia (tzn. nie jest brudna, aktywna,
zablokowana, nie znajduje się w podręcznej pamięci wymiany, etc.).
- Wylicz adres początku grupy bloków stronicowych, do której należy strona page.
Niech będzie to .
- Zajmij lock zone->lock.
- Zwiększ licznik free_pages wolnej pamięci w strefie.
- while
do
zmień bit odpowiadający
w mapie bitowej map
na przeciwny;
if bliźniak
bloku
jest zaalokowany
then przerwij pętlę;
else
usuń
z listy wolnych bloków;
;
(* Skończymy scalanie bloków, gdy bliźniak zwalnianego bloku będzie używany lub gdy
uzyskamy ciągłą grupę
bloków stronicowych. *)
- Dodaj do listy wolnych
bloków zone->free_area[k]->free_list, gdzie k jest logarytmem wielkości bloku
.
- Zwolnij lock zone->lock.
W czasie działania funkcji __alloc_pages budzony jest demon kswapd. Cóż
to takiego? Jest to wątek jądra aktywujący odzyskiwanie pamięci. Jest on uaktywniany co
sekundę oraz w chwili, kiedy jądro ,,zauważy'', że liczba wolnych bloków stronicowych
w danej strefie spadła poniżej wartości pages_low. Wątek ten wykonuje w pętli
nieskończonej funkcję kswapd(), której algorytm jest przedstawiony poniżej.
- if nr_free_pages + nr_inactive_clean_pages
freepages.high
then do_try_to_free_pages()
- if udało się odzyskać wystarczająco dużo pamięci
then zaśnij na sekundę
else
if w systemie jest bardzo mało wolnej pamięci
then oom_kill();
Dodatkowo co sekundę wątek kswapd wykonuje następujące dwie czynności:
- recalculate_vm_stats() - przeliczenie statystyk wykorzystania pamięci wirtualnej,
- run_task_queue(&tq_disk) - wykonanie ,,dolnych połówek'' zaszeregowanych
w kolejce tq_disk.
Funkcja oom_kill() zabija w sytuacji krytycznej jakiś proces, aby odzyskać trochę
pamięci. Próbuje ona zgadnąć, zabicie którego procesu pozwoli na odzyskanie maksymalnej
ilości pamięci oraz nie będzie zbyt uciążliwe dla użytkownika. Do oceny, który proces
najlepiej spełnia to kryterium służy funkcja badness. Oblicza ona wartość
gdzie , gdy
prawdziwe,
wpp.
Proces o największej wartości badness zostaje zabity.
Funkcja do_try_to_free_pages() próbuje odzyskać trochę pamięci. W tym celu:
czyści brudne strony, dezaktywuje pewne strony i odzyskuje nieużywaną pamięć wymiany
alokatora płytowego. Dokładny opis działania tej funkcji został przedstawiony w innym
referacie.