Ramki, strony i ich wymiana

Wojciech Dudek

listopad 2001


Spis tresci

Obsługa tablicy stron

Wprowadzenie

Linux korzysta z mechanizmu pamięci wirtualnej. Znaczy to, że każdy proces w systemie dysponuje własną oddzielną przestrzenią pamięci wirtualnej. Rozmiar tej przestrzeni jest ograniczony jedynie architekturą procesora, a nie ilością pamięci fizycznej. Dla przykładu dla architektury i386 maksymalny rozmiar pamięci wirtualnej wynosi 4GB.

Zwykłe procesy w Linuxie używające adresów, w rzeczywistości używają adresów logicznych. Gdy proces odwołuje się do jakiejś komórki pamięci, to adres logiczny jest tłumaczony sprzętowo (przez procesor) na adres fizyczny. Proces translacja zależy od architektury procesora.

W architekturze i386 adres logiczny, w wyniku procesu zwanego segmentacją, jest tłumaczony na adres liniowy. Następnie wynik podlega kolejnej translacji zwanej stronicowaniem ostatecznie otrzymując adres fizyczny.

W procesorach RISC-owych nie ma segmnetacji, zatem adres logiczny jest równoznaczny adresowi liniowemu.

Programiści Linux'a stwierdzili, że jeżeli mamy stronicowanie to segmentacja jest nadmiarowa. Z tego powodu Linux używa segmentacji w bardzo ograniczonym zakresie, a dokładniej tylko wtedy kiedy musi. W szczególności wszystkie procesy używają tych samych segmentów.

Ponieważ Linux koncentruje swoje wysiłki na stronicowaniu, nie będziemy omawiać segmentacji. Ogólny proces stronicownia jest opisany w [Siber 99], wiec niewtajemniczonych odsyłam do tej pozycji. My skoncentrujemy się na stronicowaniu w Linux'ie.

Stronicowanie w Linux'ie

Pamiętajmy, że cała pamięć wirtualna procesu podzielona jest na spójne części o jednakowym rozmiarze zwane stronami. W wyniku stronicowania (zachodzącego sprzętowo w procesorze) numery stron są tłumaczone na numery ramek - spójne części w pamięci fizycznej o takiej samym rozmiarze co strony. Każda ramka ma swój numer PFN (page frame number).

Dla architektury 32-bitowej wystarczy stronicowanie 2-poziomowe. W i386 mamy 10 bitów na poziom I, 10-bitów na II poziom i 12 bitów przesunięcia w obrębie jednej strony. Jednak gdy mamy do czynienia z architektura 64-bitową to nie wystarcza. Stosuje się wtedy stronicowanie 3-poziomowe. W 64-bitowych procesorach Alpha strony są wielkości 8KB, czyli 13 bitów na offset i 10 bitów na każdy stopień stronicowania. Zostaje jeszcze 21 bitów których się nie używa. Do wykorzystania jest zatem 43 bity (Kto potrzebuje więcej niż 8192 TB?).

By umożliwić działanie także archtekturom 64-bitowym, Linux uogólnia stronicowanie do modelu trzypoziomowego. Model taki prezentuje obrazek:

Rysunek 1: Stronicowanie z trzema poziomami
\begin{figure}\center{\mbox{\epsfbox{stron1.eps}}}
\end{figure}

W specjalnym rejestrze procesora jest załadowany numer ramki, w którym znajduje się Globalny Katalog Stron (ang. PGD - Page Global Directory). Najstarsze bity GLOBAL_DIR wskazują na pewną pozycje w Globalnym Katalogu. Znajduje się tam między innymi PFN dla Pośredniego Katalogu (ang. PMD - Page Middle Directory). Podobnie jest z drugim poziomem i trzecim. Z kolei w Tablicy Stron (ang. PTE - Page Table Entry) mamy ostateczny numer ramki, a nasz adres fizyczny składa się z bitów numeru ramki i przesunięcia (OFFSET). Każdy proces ma własna niezależną przestrzeń adresową i co za tym idzie własny Globalny Katalog Stron.

Stronicowanie wygląda trochę inaczej na różnych procesorach, zatem które bity odpowiadają za którą część musi być zdefiniowane osobno dla każdego procesora. W szczególność tam gdzie jest tylko stronicowanie dwupoziomowe, Pośrednia Tablica Stron składa się z 0 bitów (czyli jest pomijana).

Pozycje w tablicy stron i w katalogach stron zawierają oprócz PFN, pewne atrybuty ramki o zadanym PFN, które określają czy ramka jest dostępna, czy była używana, czy zawartość się zmieniła i jakie ma prawa dostępu. Może się tez zdarzyć , że ramka została usunięta do pliku wymiany, wtedy zamiast PFN przetrzymywana jest lokalizacja ramki na dysku.

Jeżeli podczas stronicowania procesor wykryje jakąś nieprawidłowość (czy to przekroczenie uprawnienia, czy próba odwołania do nieprawidłowej ramki) jest zgłaszany wyjątek strony. Co się wtedy dzieje opiszę w 3 rozdziale.

Trzeba sobie zdać sprawę, że stronicowanie jest dosyć drogie. Aby odwołać się do jakiegoś adresu musimy sięgnąć 3 do 4 razy do pamięci. Dlatego w procesorze umieszcza się specjalną pamięć zwaną buforami translacji adresów (ang. Translation Lookaside Buffers TLB), która przyspiesza tłumaczenie adresów liniowych. (Jak działa taka pamięć można przeczytać w: [Siber 99].) Jądro musi się także zając obsługa tej pamięci, kiedy dochodzi do modyfikacji którejś z pozycji w tablicy stron.

Tablice stron

Ponieważ co procesor to inne parametry stronicowania, skoncentrujemy się na architekturze i386.

Pośrednie Katalogu Stron nie ma, jest tylko PGD i Tablica Stron. Pozycje w tych tablicach są takie same i zawierają następujące pola:

Jak łatwo zauważyć, nie istnieje ochrona kodu wykonywalnego podczas stronicownia na i386, chociaż istnieje podczas segmentacji.

Linux używa pięciu atrybutów stron, które są realizowane za pomocą różnych bitów ochrony. Oto one:

W plikach: includesm-i386/page.h, includesm-i386/pgtable.h includesm-i386/pgtable-2level.h są zdefiniowane wszystkie definicje, funkcje i makra potrzebne do obsługi tablicy stron. Z najważniejszych należy wymienić:

Uwagi

Każdy proces w opisie swojej struktury task_struct ma pole mm typu struct mm_struct, gdzie z kolei znajduje się wskaźnik do pgd (Globalnego katalogu stron).

W procesorach Pentium Pro i wyższych, jest możliwość korzystania z prawdziwego stronicowania trzypoziomowego. Linux wykorzystuje to odpowiednie definiując makra, stałe i funkcje w pliku pgtable-3level.h

Tablica Ramek

Wstęp

Konsekwencją stronicowania jest podział pamięci fizycznej na ramki. System operacyjny musi trzymać informacje o każdej ramce. Musi wiedzieć czy ramka jest dostępna, wolna, ile stron jest mapowanych na tą ramkę, jaki ma status i tak dalej. Dodatkowo trzeba wiedzieć ile jest wszystkich ramek, i ile jest ramek dostępnych.

Pamiętajmy, że w systemie mogą występować takie kawałki pamięci, które nie mogą być wykorzystane przez procesy nawet przez jądro. Są to obszary BIOS'u czy okno pamięci video. Występują też ramki do których nie ma dostępu mechanizm DMA. Na te wszystkie ograniczenia architektury system musi zwracać uwagę.

Opis ramek

Strukturą opisującą jedną ramkę jest mem_map_t (zamiennie z struct page) zdefiniowana w pliku includeinux/mm.h następująco:

typedef struct page {
  struct list_head list;
  struct address_space *mapping;
  unsigned long index;
  struct page *next_hash;
  atomic_t count;
  unsigned long flags;
  struct list_head lru;
  unsigned long age;
  wait_queue_head_t wait;
  struct page **pprev_hash;
  struct buffer_head * buffers;
  void *virtual;
  struct zone_struct *zone;
} mem_map_t;

Najważniejsze pola tej struktury to:

Ramka może należeć do mapowania pliku (dokładniej jakiś inode). W tym przypadku pole mapping jest wskaźnikiem do struktury opisującej mapowanie inode'a, a index jest przesunięciem zawartości ramki. Dodatkowo wszystkie ramki mappujące pliki, są w tablicy haszującej (z kluczem (inode,offset)), a next_hash i pprev_hash wskazują na następny i poprzedni element w liście o tej samej wartości funkcji haszującej.

Jeżeli ramka jest przydzielona jako bufor, to buffers opisuje listę cykliczną w jakiej znajduje się ramka(, w przeciwnym razie NULL).

Ostatecznie ramka może być używana przez allokator płytowy (Slab) wtedy jest nanizany na listę list używaną przez ten allokator. Ramka tez może być zwolniona , wtedy jest nanizana na listę wolnych ramek.

Podczas operacji swapowania zawartości ramki flaga PG_locked jest ustawiona. Wszystkie zadania czekają na kolejce wiat gdy ramka jest zablokowana.

Bity opisujące flagi ramki są następujące:

PG_locked blokuje ramke
PG_error pojawił się błąd we/wy przy swapowaniu tej ramki
PG_referenced z ramki korzystano za pośrednictwem tablicy przemieszczania pamięci podręcznej stron
PG_uptodate mówi czy zawartość ramki jest prawidłowa, ustawiana po prawidłowej operacji we/wy
PG_dirty strona jest brudna (nie używana)
PG_active PG_inactive_dirty  
PG_inactive_clean aktywna, nie aktywna brudna, nie aktywna czysta (używane przez allokatora)
PG_swap_cache znajduje się w pamięci podręcznej wymiany
PG_highmem ramki z tą flaga nie są ciągle mapowane w przestrzeń wirtualną jądra
PG_skip jest używany w architekturze sparcparc64 do pomijania przestrzeni adresowej
PG_reserved jest ustawiony dla specjalnych ramek, które nigdy nie mogą być swapowane. Niektóre nawet mogą nie istnieć

Tablica ramek

W systemie istnieje globalna tablica mem_map której polami są rekordy mem_map_t. Tablica ta jest tworzona podczas startu systemu operacyjnego, wtedy dopiero wiadomo ile komputer mam pamięci, czyli też ile ma ramek. Odwołania do tablicy wykonuje się konstrukcją: mem_map[MAP_NR(a)] 2, gdzie a to adres wirtualny w kontekście jądra, lub mem_map + nr_ramki.

Przydział pamięci na tablice ramek (lub deskryptorów ramek) następuje w funkcji free_area_init_node(), w wczesnej fazie inicjacji jądra. Funkcja ta wywołuje free_area_init_core(), liczy ilość ramek we wszystkich strefach, znajduje dobre miejsce dla tablicy, ustawia pg_dat_t contig_page_data (informujące o węzłach pamięci), rezerwuje całą pamięć, czyści kolejki ramek i bitmapy pamięci.

Wymiana stron

Pamięć wirtualna umożliwia przydział większej ilości pamięci niż w rzeczywistości ma komputer, pozwala także aby system operacyjny mógł wykonywać operacje na pamięci tylko wtedy gdy jest to konieczne. Taka leniwość znacznie przyspiesza uruchamianie programów, i kopiowanie pamięci.

Zawartość strony może nie znajdować w ramce (zawartość jest w pliku wymiany), albo strona jest nieprawidłowa, ewentualnie przekroczono uprawnienia dostępu. Kiedy jednostka stronicowania w procesorze wykryje taką sytuacje procesor podnosi wyjątek błędu strony. Wyjątek ten (jak zresztą wszystkie inne) obsługuje system operacyjny. Jego zadaniem jest najpierw stwierdzenie dlaczego nastąpił sytuacja wyjątkowa, czy to było rzeczywiste naruszenie bezpieczeństwa, czy to tylko skutek polityki oddalania w czasie przydziału pamięci (leniwość).

Obsługa błędu strony

Obsługą braku strony zajmuję się do_page_fault() (arch/i386/mm/faul.c). Najpierw określa co spowodowało błąd stronicowania badając bity error_code przekazane od jednostki stronicowania. Następnie pobierany jest adres błędnego odwołania do strony, wskaźniki do wykonywanego procesu i jego opisu dostępnej pamięci. Jeżeli jesteśmy przy obsłudze przerwania lub nie ma kontekstu użytkownika, to jeżeli nie da się naprawić odwołania (search_exception_table() ) problem spowodowało jądro - jest wykonywany Oops (czyli jądro się zawiesza z informacją o spowodowanym błędzie).

Natomiast jeżeli mamy dostęp do kontekstu użytkownika, sprawdzamy obszar wirtualnej przestrzeni adresowej pamięci procesu który spowodował błąd. Jeżeli adres nie należy do obszaru procesu to Segmentation fault - proces umiera. Jeżeli przekroczyliśmy stos to jest rozszerzany. Jeżeli piszemy lub czytamy a nie mamy na to zezwolenia to też wysyłamy sygnał SIGSEGV. Jeżeli jednak błąd powstał wskutek leniwej polityki to odpalana jest wysokopoziomowa procedura handle_mm_fault() (linux/mm/memory.c). Tam próbuje się przydzielić nową ramkę albo sprowadzić ją do pamięci.

Jeżeli ramka nie jest obecna to mamy do czynienia z stronicowaniem na żądanie (page demanding). Testowane jest czy strona była wykorzystywana wcześniej (pte_none). Jeżeli tak to ramkę mamy w swap'ie - sprowadzamy ramkę do_swap_page() 3. W przeciwnym wypadku wykonywana jest funkcja do_no_page(), a ona w zależności czy próbowaliśmy pisać lub czytać, allokuje nową (czystą) ramkę albo podstawia ramkę ZERO_PAGE wypełnioną zerami4.

Jeżeli ramka jest obecna (zostało tylko możliwość, że chcemy pisać) to wykonywane jest tak zwane kopiowanie na żądanie (copy on write). Obsługuje to funkcja do_wp_page() przydzielając nową ramkę pamięci. Jeżeli próbowaliśmy pisać do ZERO_PAGE to ramka jest wypełniana zerami, jeżeli nie a dla strony $count>0$ to kopiujemy zawartość odwoływanej ramki do nowej, ostatecznie ustawiamy prawo do pisania.

W trakcie całej tej skomplikowanej procedury sprawdzane są kilkakrotnie atrybuty strony do której się odwoływaliśmy i ustawiane dla nowy pozycji w PTE. \begin{figure}\center{\mbox{\epsfbox{pf.ps}}}
\end{figure}

Wyrzucanie do pliku wymiany

Wspomniałem , że ramki mogą być w swap'ie. Jak się jednak tam mogły znaleźć? Otóż co w tle działa demon kswapd i sekundę sprawdza czy jest wystarczająca ilość wolnych ramek. Jeżeli nie to próbuje zwolnić jakieś ramki zrzucając ich zawartość do pliku wymiany. Wolnych ramek szuka w przestrzeni procesów lub w strukturach jądra (prosi pewne funkcje o zminimalizowanie zajmowanego obszaru).

Spis Literatury

Siber 99
A.Silberschatz , P.B Galvin :Podstawy systemów operacyjnych. Warszawa, WNT 1993.

Kernel
Źródła Linuxa wersja 2.4.7 (gdziekolwiek na sieci)

KernelL
D.P. Bovet M. Cesati :LINUX KERNEL warszawa 2001 O'REILLY

LinuxASD
Opracowania z SO na MIMUW z roku 2000/2001.

About this document ...

Ramki, strony i ich wymiana

This document was generated using the LaTeX2HTML translator Version 99.2beta8 (1.46)

Copyright © 1993, 1994, 1995, 1996, Nikos Drakos, Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999, Ross Moore, Mathematics Department, Macquarie University, Sydney.


Footnotes

... jądra1
W przestrzeń wirtualną każdego procesu jest też mapowana przestrzeń jądra. Oczywiście tylko w trybie jądra można się tam dostać.
...mem_map[MAP_NR(a)] 2
Jądro mapuje całą pamięć fizyczną, no chyba że jest jej więcej niż 1GB. Wtedy mamy do czynienia z pamięcią HIGHMEM, której jądro nie mapuje do swojej przestrzeni wirtualnej na stałe.
...do_swap_page() 3
wtedy zamiast PFN w PTE znajduje się pozycja(offset) i numer pliku wymiany
... zerami4
Taka ramka jest jedna w systemie i nie można do niej pisać. Takie podstawienie to operacja chwilowa realizująca strategie leniwości.

wd181255@zodiac.mimuw.edu.pl