Następna Poprzednia Spis Linux, jądro 2.4.7

2. Algorytm wymiany stron

Rozdział ten opisuje sposób, w jaki Linux wybiera te strony pamięci, których zawarość powinna zostać wymieniona na dysk. Można się też z niego dowiedzieć, kiedy zajęte strony pamięci są zwalniane.

2.1 Struktury danych

Wymiana stron powinna się odbywać w momencie, w którym w systemie zaczyna brakować pamięci. Linux stara się, by liczba wolnych stron w systemie nie spadła poniżej ustalonego poziomu. Rezerwa ta jest potrzebna, gdyż niektóre żądania alokacji pamięci mogą zostać zgłoszone w sytuacji, gdy oczekiwanie na zapis strony w obszarze wymiany lub operacje wejścia/wyjścia nie mogą mieć miejsca.
Wynika z tego, że wymiana powinna odbywać się wtedy, gdy liczba wolnych stron spadnie poniżej pewnego minimum. Minimum to jest osobno określone dla każdej z trzech stref pamięci (dla przypomnienia: Linux dzieli pamięć RAM na trzy różne strefy (ang. zones) - DMA, 'zwykłą' i 'wysoką' - pamięć nie odwzorowywaną na stałe przez jądro).

Spójrzmy na strukturę reprezentującą deskryptor strefy pamięci (wybrane pola):
struct zone_struct  			/* struktura opisująca strefę pamięci */
	unsigned long	free_pages; 	/* liczba wolnych stron */
	/* liczba stron zakwalifikowanych do wymiany, 
	które nie posiadają aktualnej kopii w obszarze wymiany */
	unsigned long	inactive_dirty_pages;		
	/* lista stron zakwalifikowanych do wymiany, 
	które można  szybko zwolnić bez zapisywania w obszarze wymiany */
	struct list_head	inactive_clean_list;
	unsigned long	inactive_clean_pages;	/* liczba tych stron */
	unsigned long	pages_high,	/* wymagana liczba wolnych stron */
			pages_low,	/* niska liczba wolnych stron*/
			pages_min,	/* minimalna liczba wolnych stron */
Deskryptor strefy posiada pola pages_high, pages_low i pages_min - przechowywane tam wartości definiują wspomniany wcześniej minimalny próg wolnych stron w systemie. Pola te inicjowane są następująco:
	pages_min = rozmiar_strefy * współczynnik_zbalansowania
		gdzie współczynnik_zbalansowania wynosi:
			strefa "DMA"			= 1/32
			strefa "Normal" i "HighMem"	= 1/128
	pages_low	= pages_min*2
	pages_high	= pages_min*3
Jeśli zawartość strony pamięci zostanie uznana z pewnych powodów za chwilowo niepotrzebną, strona taka wędruje do jednej z list stron nieaktywnych. Jeśli zawartość strony jest unikatowa - nie mamy jej aktualnej kopii na dysku, to strona zostaje dodana do listy stron nieaktywnych zabrudzonych, na której początek wskazuje zmienna globalna inactive_dirty_list.
Jeśli natomiast posiadamy kopię strony, to zostaje ona dodana do listy wskazywanej przez pole inactive_clean_list deskryptora strefy, do której należy.
Strony uznane za aktualnie używane - aktywne - znajdują się na liście, której początek wyznacza globalna zmienna active_list.
Inne pomocne zmienne globalne to:

2.2 Mechanizm postarzania stron

Na jakiej podstawie stwierdzić, czy strona przestała być 'aktywna' i powinna zostać usunięta z listy active_list? Pomaga nam w tym pole age struktury page opisującej stronę. Choć nazwa tego pola sugeruje, że symuluje ono wiek strony, w istocie nie jest to wiek w intuicyjnym tego słowa znaczeniu.
Co oznacza naprawdę, dowiemy się już niebawem.
Wiek nowej strony ustawiany jest na 2. Nigdy nie przekracza on wartości PAGE_AGE_MAX (tu: 64).
Manipulacje wiekiem stron dokonywane są za pośrednictwem funkcji refill_inactive_scan(). Wywołuje ją opisana w dalszej części pracy funkcja do_try_to_free_pages() oraz co sekundę Kernel Swap Deamon.
Spójrzmy na kod jakże interesującej funkcji refill_inactive_scan():

refill_inactive_scan(unsigned int priority, int target)
	/* Parametry:
		priority - określa, jak wiele stron aktywnych mamy przejrzeć
		target - ile nieużywanych stron chcemy znaleźć */

	dla każdej strony z active_list rób {
		/* Uaktualnij wiek strony */
		jeśli strona ma ustawiony bit PG_referenced rób
			wiek_strony = min(wiek_strony+3, PAGE_AGE_MAX)
		wpp {
			wiek_strony = wiek_strony / 2;
			jeśli wiek_strony == 0 and
				liczba użytkowników korzystających ze strony == 0 rób
				przenieś stronę na listę nieaktywnych		
		}

		Wyczyść bit PG_referenced strony
		jeśli strona nie została zdezaktywowana rób
			przenieś ją na koniec active_list
		
		jeśli przejrzano wystarczająco dużo stron lub 
			target <> 0 i zdezaktywowano target stron rób
			wyjdź z pętli
	}
	return liczba_zdeaktywowanych_stron;
Jak widzimy funkcja ta przegląda listę stron aktywnych. Jeśli ostatnio ktoś odwoływał się do strony, sprzętowo została ustawiona jej flaga PG_referenced. Wtedy wiek strony jest zwiększany. W przeciwnym przypadku zmniejsza się go. Tak więc duża wartość pola age oznacza, iż zawartość strony jest często wykorzystywana, co przemawia za tym, by jej nie wymieniać.
Gdy wiek strony spadnie do zera, strona zostaje przeniesiona na odpowiednią listę stron nieaktywnych.

Zauważmy, że jeśli wiek strony maleje, to przebiega to wykładniczo, a jeśli rośnie, to liniowo, czyli asymptotycznie o wiele wolniej. Jak mniemam dzięki temu jeśli strona jest na active_list oznacza to, iż odwołania do niej są naprawdę częste i wymiana jej najprawdopodobniej nie ma sensu.
Strona poddana operacji aktualizacji wieku wędruje na koniec listy stron aktywnych. Dzięki temu wiek każdej strony aktualizowany jest z podobną częstotliwością.
Mechanizm postarzania stron wspiera i udoskonala algorytm zastępowania stron używanych najdawniej (ang. Least Recently Used) dzięki zapamiętywaniu częstości odwołań do strony w ostatnim czasie.

2.3 Gdy zaczyna brakować pamięci

Gdy przyjdą ciężkie czasy i liczba wolnych stron pamięci RAM w systemie spadnie poniżej magicznego poziomu, funkcja do_try_to_free_pages() wkracza do akcji. Wywołują ją: funkcja __alloc_pages zaspakajająca żądania systemu bliźniaków (posrednio, przez try_to_free_pages()) oraz wątek kswapd. Funkcja ma tylko jeden cel: zwolnienie najmniej używanych stron. Mężnie giermkuje jej w tym mechanizm wymiany.

do_try_to_free_pages(unsigned int gfp_mask, int user)
	jeśli liczba wolnych stron + 
		liczba nieaktywnych niezabrudzonych stron < freepages.high rób
		/* Wyczyść zabrudzone strony - jeśli trzeba wymień je na dysk, 
		i przenieś je do listy inactive_clean_list */
		page_launder(gfp_mask, user);

		/* spróbuj zwolnić strony pamięci podręcznej pozycji katalogów 
		(przyspieszającej tłumaczenie ścieżki dostępu do pliku)
		Po usunięciu pewnych obiektów pozycji katalogów może się okazać, 
		że odpowiadające im obiekty reprezentujące i-węzły przestały być używane - 
		- próbujemy więc zwolnić pamięć podręczną, w której są one przechowywane. */
		shrink_dcache_memory(DEF_PRIORITY, gfp_mask);
		shrink_icache_memory(DEF_PRIORITY, gfp_mask);
	}

	jeśli liczba wolnych oraz nieaktywnych stron jest zbyt niska rób
		/* przesuń jak najwięcej stron z listy stron aktywnych 
		do listy stron nieaktywnych */
		refill_inactive(gfp_mask, user);

	/* Spróbuj odzyskać strony z pamięci podręcznej alokatora płytowego*/
	kmem_cache_reap(gfp_mask);

2.4 Stron czyszczenie.

Funkcją odpowiadającą za czyszczenie 'zabrudzonych' stron i przygotowanie ich do zwolnienia do systemu bliźniaków jest

page_launder(int gfp_mask, int sync)
Godnym podkreślenia jest fakt, że to właśnie ta funkcja jest bezpośrednio odpowiedzialna za inicjację procesu wymienienia zawartości stron na dysk.

Działa ona według następującego algorytmu:

  • Wykonaj Przejście I po elementach listy inactive_dirty_list. Jeśli strona używana jest jako bufor, spróbuj go usunąć. Jeśli się to uda, przenieś stronę do listy inactive_clear_list lub zwolnij ją.

  • Jeśli nadal liczba wolnych i nieaktywnych niezabrudzonych stron jest zbyt mała:
  • Obudź demona bdflush() - wątek jądra, który wyczyści pewną liczbę 'brudnych' buforów przechowywanych w pamięci zapisując je na dysku.
  • Wykonaj Przejście II po inactive_dirty_list:
    strona zabrudzona? -> asynchronicznie przenieś ją do obszaru wymiany.

  • Zwróć liczbę stron przeniesionych do inactive_clean_list.

    2.5 Znajdowanie stron najmniej potrzebnych

    Funkcja refill_inactive() wywoływana jest przez do_try_to_free_pages(). Jej zadaniem jest odebranie działającym w systemie procesom najmniej/najdawniej używanych stron, a także uaktualnienie wieku stron.
    Algorytm działania funkcji jest następujący:

    refill_inactive
    	jeśli działamy na zlecenie procesu użytkownika rób {
    		/* wymieniane strony staramy się zbierać w pakiety 
    		o wielkości 2^page_cluster. Zmienna ta, w zależności od liczby 
    		wszystkich dostępnych stron w pamięci ustawiana jest na 2,3 lub 4. */
    		ile_zwolnić = 2^page_cluster; 
    		liczba_prób = 6; 
    	} wpp {
    		ile_zwolnić = aktualny niedobór;
    		liczba_prób = 2^6;
    	}
    	powtarzaj liczba_prób razy {
    		/* spróbuj odebrać jakieś strony działającym procesom */
    		swap_out(DEF_PRIORITY, gfp_mask);
    
    		/* aktualizuj wiek stron, wyłapując nieużywane */
    		refill_inactive_scan(DEF_PRIORITY, ile_jeszcze_zwolnić);
    
    		jeśli zaspokojono niedobór stron rób
    			return
    	}
    
    Jak widzimy dokonuje ona serii prób znalezienia stron nieaktywnych, starając się zaspokoić niedobór, który powstał w systemie. W tym celu próbuje odebrać jakieś strony działającym procesom, a także aktualizuje wiek stron, wyłapując w ten sposób najmniej nieużywane.

    2.6 Odbieranie stron procesom.

    Funkcja

    void swap_out(unsigned int priority, int gfp_mask)
    przegląda strony działających w systemie procesów, w poszukiwaniu tych, które można zwolnić lub wymienić. Poszukiwania rozpoczynają się zawsze od tego procesu, który spowodował, że trzeba zwolnić pewne strony pamięci, gdyż zażądał alokacji nowych.
    Liczba procesów, którym mają być odebrane strony pamięci zależy wykładniczo od parametru priority.

    Aby przejrzeć pozycje Tablicy Stron poszczególnych procesów, wywołuje się szereg funkcji pomocniczych przeglądając w głąb struktury opisujące zajmowaną przez procesy pamięć. Przegląda się:
    listę deskryptorów przestrzeni adresowych procesów ---> swap_out()
    regiony pamięci procesu ---> swap_out_mm()
    pozycje Globalnego Katalogu Stron danego regionu ---> swap_out_vma()
    pozycje Pośredniego Katalogu Stron ---> swap_out_pgd()
    pozycje Tablicy Stron ---> swap_out_pmd()
    każdą Stronę z danej Tablicy Stron ---> try_to_swap_out()

    Jeśli już chcemy odbierać jakiemuś procesowi strony, to staramy się zwolnić ok. 1/32 stron, które posiada, ale nie mniej niż 8. Zarówno deskryptory przestrzeni adresowej jak i regiony pamięci procesu przegląda się zaczynając od następnika elementu przeglądanego ostatnim razem, dzięki czemu każdy proces i każdy jego region pamięci jest krzywdzony na równi.

    Do funkcji try_to_swap_out() przekazywane są tylko strony, których wymiana nie jest zabroniona (zgaszony bit PG_reserved)


    Funkcja try_to_swap_out() określa, czy daną stronę pamięci można zapisać w obszarze wymiany. Jeśli stronę można zwolnić bez zapisywania w obszarze wymiany, jest ona zwracana do systemu bliźniaków. W starszych wersjach jądra, jeśli strona nadawała się do wymiany, funkcja dokonywała fizycznej wymiany. Obecnie tego nie robi, a jedynie przenosi ją na listę inactive_dirty_list, umożliwiając wymienienie jej w przyszłości funkcji page_launder().

    Wymienione mogą być tylko te strony, dla których opisująca je pozycja Tablicy Stron ma wyczyszczoną flagę _PAGE_BIT_ACCESSED. Ponieważ bit ten jest ustawiany przez jednostkę stronnicowania procesora przy każdym dostępie do strony, strona może być wymieniona tylko wtedy, gdy nie była wykorzystywana od poprzedniego wywołania na niej funkcji try_to_swap_out(). Jest to uproszczona wersja algorytmu zastępowania stron używanych najdawniej (ang. Least Recently Used) (algorytm drugiej szansy).

    try_to_swap_out(mm, vma, address, page_table, page)
    	/* Pojęcie 'strona' używane poniżej tyczy się strony, którą chcemy wymienić, 
    	a którą wyznaczają przekazywane funkcji parametry */
    
    	jeśli test_and_clear(flaga Accessed strony) rób {
    		wiek_strony = min(wiek_strony+3, PAGE_AGE_MAX)
    		return
    	}
    	jeśli strona zablokowana (flaga PG_locked) rób
    		return
    
    	jeśli strona należy do pamięci podręcznej wymiany lub
    		strona ma wyczyszczony bit PG_dirty lub
    		strona odwzorowuje plik rób {
    		/* stronę można zdezaktywować bez zapisywania w obszarze wymiany */ 
    		jeśli wiek_strony == 0 rób
    			przenieś ją do listy inactive_dirty_list
    
    		jeśli liczba użytkowników korzystających ze strony == 0 rób
    			zwróć stronę do systemu bliźniaków (__free_pages_ok)
    	} wpp { /* Mamy do czynienia z 'zabrudzoną', wymienialną stroną */
    		entry = get_swap_page();/* znajdź wolny slot w jednym z obszarów wymiany */
    		jeśli znaleziono wolny slot rób
    			Zapisz identyfikator wymienionej strony do pozycji Tablicy Stron
    			/* Zapamiętaj w pamięci podręcznej wymiany parę (page,entry) */
    			add_to_swap_cache(page, entry);
    			Oznacz stronę jako 'brudną'
    			jeśli wiek_strony == 0 rób
    				przenieś ją do listy inactive_dirty_list
    
    	}
    
    Warto też zwrócić uwagę na to, iż strony, które mają zgaszoną flagę PG_dirty - tzw. bit modyfikacji, wędrują na listę inactive_dirty_list bez przydzielenia im slotu w obszarze wymiany. Flaga ta jest bowiem ustawiana sprzętowo w momencie modyfikacji zawartości strony. Gdy jest zgaszona oznacza to, iż posiadamy na dysku kopię rozpatrywanej strony, więc nie trzeba jej wymieniać.
    Nie wymieniamy też stron odwzorowujących pliki, bo odwzorowywany plik to przecież kopia strony.

    2.7 Zmiany w stosunku do poprzednich wersji jądra

  • Drzewiej funkcja try_to_swap_out() po znalezieniu strony nadającej się do wymiany dokonywała fizycznego transferu. Teraz jedynie znajduje wolny slot w obszarze wymiany, zaś sama wymiana jest inicjowana przez funkcję page_launder()
  • Zmienił się moment zwracania wolnych stron do systemu bliźniaków - teraz dokonuje tego głównie demon systemowy kreclaimd, dawniej zaś robiła to głównie funkcja try_to_swap_out() zaraz po wymianie strony na dysk.
  • Dodano mechanizm postarzania stron, który wsparł dotychczasową strategię wymiany.
  • 2.8 Źródła

    Źródła omawianych tutaj struktur i funkcji można znaleźć w plikach:

    mm/vmscan.c 
    include/linux/mmzone.h
    mm/swap.c
    


    Następna Poprzednia Spis Autor: Tomasz Pylak, 2001